确保接口安全:六大方案有效解决幂等性问题

news/2024/12/22 10:56:29/

文章目录

  • 六大方案解决接口幂等问题
    • 什么是接口幂等?
    • 天然幂等
    • 不做幂等会怎么样?
  • 解决方案
    • 1)insert前先select
    • 2)使用唯一索引
    • 3)去重表加悲观锁
    • 4)加乐观锁之版本号机制
    • 5)使用 Redisson 分布式锁
    • 6)Token 机制

六大方案解决接口幂等问题

什么是接口幂等?

幂等(idempotency)本身是一个数学概念,常见与抽象代数中,代表一个函数或操作的结果不受其输入或者执行次数的影响,例如,f(n) = 1^n,无论 n 为多少,f(n)的值永远为 1 。

在软件开发领域,幂等对请求执行结果的一个描述,这个描述就是无论执行多少次相同的请求,产生的效果和返回的结果和发出单个请求是一样的。

举个例子🌰:

  • 有时我们在填写某些form表单时,保存按钮不小心快速点了两次,表中竟然产生了两条重复的数据,只是id不一样。
  • 我们在项目中为了解决接口超时问题,通常会引入了重试机制。第一次请求接口超时了,请求方没能及时获取返回结果(此时有可能已经成功了),为了避免返回错误的结果(这种情况不可能直接返回失败吧?),于是会对该请求重试几次,这样也会产生重复的数据。

天然幂等

那有没有有些情况是天然支持幂等的呢?当然有!比如说我要更新一个某记录的状态 status = 1 具体的 sql 为 update table set status = 1 where id = 1 这种情况,无论我执行多少次这条 sql 他的效果是一样的,这就是天然支持幂等的。

不做幂等会怎么样?

比如说用户在付款的时候,同时点击多次付款按钮,后端处理了多次扣款请求,结果导致用户的账户扣了多次钱。妥妥 p0 事故呀!

到这你又会说,前端做个置灰按钮不就行了吗,第一次付款完毕后,那用户或者恶意攻击你服务器的人直接用脚本搞你不走前端,你是防止不了的。

那接下来,我将介绍六大解决接口幂等的方案,速速点赞上车!!!

解决方案

1)insert前先select

通常情况下,在保存数据的接口中,我们为了防止产生重复数据,一般会在insert前,先根据namecode字段select一下数据。如果该数据已存在,则执行update操作,如果不存在,才执行 insert操作。

在这里插入图片描述

该方案可能是我们平时在防止产生重复数据时,使用最多的方案。但是该方案不适用于并发场景,在并发场景中,要配合其他方案一起使用,否则同样会产生重复数据。

2)使用唯一索引

通过在表中加上唯一索引,保证数据的唯一性。如果有重复的数据插入,会抛出DuplicateKeyException异常,程序可以捕获异常并处理。不过,这种方法只适用于插入数据的场景。

create table t_order(id int unsigned PRIMARY KEY AUTO_INCREMENT COMMENT "主键",code varchar(200) not null COMMENT "流水号",user_id  int unsigned COMMENT "用户id",amount decimal(10,2) unsigned not null COMMENT "总金额",UNIQUE unq_code(code)
) COMMENT="订单表";

不要依靠唯一索引来保证接口幂等,但建议使用唯一索引作为兜底,避免产生脏数据

伪代码如下:

java">public void idempotent(OrderDO orderDO){try {// 执行核心业务...orderMapper.insert(orderDO);}catch(DuplicateKeyException e) {// 有重复的数据插入}
}

3)去重表加悲观锁

去重表本质上也是一种唯一索引方案。去重表是一张专门用于记录请求信息的表,其中某个字段需要建立唯一索引,用于标识请求的唯一性当客户端发出请求时,服务端会将这次请求的一些信息(如订单号、交易流水号等)插入到去重表中,如果插入成功,说明这是第一次请求,可以执行后续的业务逻辑;如果插入失败,说明这是重复请求,可以直接返回或者忽略。

CREATE TABLE deduplication_table (id int unsigned PRIMARY KEY AUTO_INCREMENT COMMENT "主键",processed_code varchar(200) not null COMMENT "已处理的订单流水号",-- 省略其他字段UNIQUE unq_processed_code(processed_code)
) COMMENT="去重表";

使用for update加锁每次查询到都是最新的数据

select * from deduplication_table where processed_code = 'xxx' for update

伪代码如下:

java">public boolean idempotent(OrderDO orderDO){// 执行核心业务之前DoMain domain = deduplicationMapper.selectForUpdate(orderDO.getProcessedCode);if(doamin != null) {// 订单已经支付}
}

4)加乐观锁之版本号机制

既然悲观锁有性能问题,为了提升接口性能,我们可以使用乐观锁。需要在表中增加一个timestamp或者version字段,这里以version字段为例。

在更新数据之前先查询一下数据:

select id,amount,version from user id=123;

如果数据存在,假设查到的version等于1,再使用idversion字段作为查询条件更新数据:

update user set amount = amount + 100, version = version + 1 where id = 123 and version = 1;

更新数据的同时version+1,然后判断本次update操作的影响行数,如果大于0,则说明本次更新成功,如果等于0,则说明本次更新没有让数据变更。

由于第一次请求version等于1是可以成功的,操作成功后version变成2了。这时如果并发的请求过来,再执行相同的sql:

update user set amount = amount + 100,version = version + 1 where id = 123 and version = 1;

update操作不会真正更新数据,最终sql的执行结果影响行数是0,因为version已经变成2了,where中的version=1肯定无法满足条件。但为了保证接口幂等性,接口可以直接返回成功,因为version值已经修改了,那么前面必定已经成功过一次,后面都是重复的请求。

具体流程如下:在这里插入图片描述

具体步骤:

  1. 先根据id查询用户信息,包含version字段
  2. 根据id和version字段值作为where条件的参数,更新用户信息,同时version+1
  3. 判断操作影响行数,如果影响1行,则说明是一次请求,可以做其他数据操作。
  4. 如果影响0行,说明是重复请求,则直接返回成功。

5)使用 Redisson 分布式锁

基于 MySQL 也可以实现分布式锁,但一般我们不会采用这种方式。

通常情况下,我们一般会选择基于 Redis 或者 ZooKeeper 实现分布式锁,Redis 用的要更多一点。

java">// 唯一标识
String uniqueId = "orderId123";
// 1. 根据唯一标识生成分布式锁对象
RLock lock = redisson.getLock("lock:" + uniqueId);try {// 2. 尝试获取锁(Watch Dog 自动续期机制) if (lock.tryLock()) {// 3. 如果成功获取到锁,说明请求还没有被处理,执行业务逻辑} else {// 请求已经被处理,直接返回}
} finally {// 4. 释放锁lock.unlock();
}

6)Token 机制

Token 机制的核心思想是为每一次操作生成一个唯一性的凭证 token。这个 token 需要由服务端生成的,因为服务端可以对 token 进行签名和加密,防止篡改和泄露。如果由客户端生成 token,可能会存在安全隐患,比如客户端伪造或重复 token,导致服务端无法识别和校验。

这样的话,就需要两次请求才能完成一次业务操作:

  1. 请求获取服务器端 token,token 需要设置有效时间(可以设置短一点),服务端将该 token 保存起来。

  2. 执行真正的请求,将上一步获取到的 token 放到 header 或者作为请求参数。服务端验证 token 的有效性,如果有效(一般是通过删除 token 的方式来验证,删除成功则有效),执行业务逻辑,并删除 token,防止重复提交;如果无效,拒绝请求,返回提示信息。

java">// 获取token
public String getToken(Long busId, Long userId){String UUID = UUID.randomUUID().toString();stringRedisTemplate.opsForValue().set(busId+userId, UUID, 20, TimeUnit.SECONDS)return UUID;
}
// 发起业务请求携带token
public void doSomeBusiness(Parameter parameter) {Long busId = parameter.getBusId;Long userId = UserContext.getUserId();// 判断token是否存在Boolean deleted = stringRedisTemplate.delete(busId+userId);if(deleted) {// 删除成功,代表重复请求不进行操作,直接返回return;}// doSomeBusiness...
}

在这里插入图片描述

具体步骤:

  1. 用户访问页面时,浏览器自动发起获取 token 请求。
  2. 服务端生成 token,保存到 redis 中,然后返回给浏览器。
  3. 用户通过浏览器发起请求时,携带该 token。
  4. redis 中尝试删除 token 如果删除失败,说明是第一次请求,做则后续的数据操作。
  5. 如果删除成功,说明是重复请求,不做任何操作。

http://www.ppmy.cn/news/1534458.html

相关文章

Grafana链接iframe嵌入Web前端一直跳登录页面的问题记录

概述 公司有个项目使用到Grafana作为监控界面,因为项目方的环境极其复杂,仅物理隔离的环境就有三四个,而且每个都得部署项目,今天在某个环境测试,查看界面遇到一个比较奇怪的Grafana问题,后面针对该问题进行跟踪分析并解决,故而博文记录,用于备忘。 问题 登录项目We…

30min 的OpenCV learning Note

1.安装python和pycharm与环境搭配 打开Windows终端:(winR)(一般使用清华镜像网站安装库比较快) pip install opencv-contrib-python -i https://pypi.mirrors.ustc.edu.cn/simple 或者 python -m pip install open…

geodatatool(地图资源下载工具)3.8更新

geodatatool(地图资源下载工具)3.8(新)修复更新,修复更新包括: 1.高德POI数据按行政区划下载功能完善。 2.修正高德POI数据类型重复问题。 3.对高德KEY数据访问量超过最大限制时,提示错误并终止…

python的内存管理机制

python的内存管理机制主要分为三个部分:引用计数、垃圾回收和内存池机制。 引用计数机制: python通过维护每个对象的引用计数来跟踪内存中的对象。当对象被创建时就会有一个引用计数,当对象不再被使用时,引用计数为0&#xff0c…

win系统网络重置

重置网络命令:netsh winsock reset 输入winR 调用运行窗口,回车 输入重置网络命令:netsh winsock reset 注意空格

3D Gaussian Splatting 学习笔记

目录 突破3DGS泛化和实用壁垒!GS-Net:首个具有跨场景泛化能力的即插即用3DGS模块 升级版 SGD 一问读懂3D Gaussian Splatting 3DGS高斯泼溅技术 系统首先对 SfM 点云进行初始化 代码概要 突破3DGS泛化和实用壁垒!GS-Net:首个…

Android-Handle消息传递和线程通信

本文为作者学习笔记,如有误,请各位大佬指点 目录 一、同步异步 二、Java多线程通信 三、Handler是什么 四、Handler相关的类 五、Handler常用方法 1. 发送消息 2. 接收处理消息 3. 切换线程 六、使用Handler 使用Handler更新UI 使用Handler延…

【Android 源码分析】Activity短暂的一生 -- 目录篇 (持续更新)

1. 前言 忽然有一天,我想要做一件事:去代码中去验证那些曾经被“灌输”的理论。                                                                                  …