一、什么是接口幂等性
1.1 前言
接口幂等性
不知道你有没有遇到过这些场景:
有时在填写某些
form表单
时,保存按钮不小心快速点了两次,表中竟然产生了两条重复的数据,只是id不一样。
在项目中为了解决
接口超时
问题,通常会引入
重试机制
。第一次请求接口超时了,请求方没能及时获取返回结果(此时有可能已经成功了),为了避免返回错误的结果(这种情况不可能直接返回失败吧?),于是会对该请求重试几次,这样也会产生重复的数据。
mq消费者在读取消息时,有时候会读取到
重复消息
(至于什么原因这里先不说,有兴趣的朋友可以私聊),如果处理不好,也会产生重复的数据。
没错,这些都是幂等性问题。
是指用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生副作用。
这类问题多发于接口的:
- INSERT 操作,这种情况下多次请求,可能会产生重复数据。
- UPDATE 操作,如果只是单纯的更新数据,比如:
UPDATE user set status=1 where id=1,是没有问题的。如果还有计算,比如:UPDATE user set status=status+1 where id=1,这种情况下多次请求,可能会导致数据错误。
那么要如何保证接口幂等性?下面会告诉你答案。
二、保证接口幂等性的方法
2.1 insert前先select
通常情况下,在保存数据的接口中,我们为了防止产生重复数据,一般会在 INSERT 前,先根据 name 或 code 字段 SELECT 一下数据。如果该数据已存在,则执行 UPDATE 操作,如果不存在,才执行 INSERT 操作。
操作。
该方案可能是我们平时在防止产生重复数据时,使用最多的方案。但是该方案不适用于并发场景,在并发场景中,要配合其他方案一起使用,否则同样会产生重复数据。我在这里提一下,是为了避免大家踩坑。
2.2 加悲观锁
在支付场景中,用户A的账号余额有150元,想转出100元,正常情况下用户A的余额只剩50元。一般情况下,sql是这样的: UPDATE user amount = amount-100 where id=123; ``
如果出现多次相同的请求,可能会导致用户A的余额变成负数。这种情况,用户A来可能要哭了。于此同时,系统开发人员可能也要哭了,因为这是很严重的系统bug。
为了解决这个问题,可以加悲观锁,将用户A的那行数据锁住,在同一时刻只允许一个请求获得锁,更新数据,其他的请求则等待。
通常情况下通过如下sql锁住单行数据: SELECT * from user id=123 for update; ``
具体流程如下:
具体步骤:
多个请求同时根据id查询用户信息。
判断余额是否不足100,如果余额不足,则直接返回余额不足。
如果余额充足,则通过for update再次查询用户信息,并且尝试获取锁。
只有第一个请求能获取到行锁,其余没有获取锁的请求,则等待下一次获取锁的机会。
第一个请求获取到锁之后,判断余额是否不足100,如果余额足够,则进行update操作。
如果余额不足,说明是重复请求,则直接返回成功。
悲观锁需要在同一个事务操作过程中锁住一行数据,如果事务耗时比较长,会造成大量的请求等待,影响接口性能。
此外,每次请求接口很难保证都有相同的返回值,所以不适合幂等性设计场景,但是在防重场景中是可以的使用的。
在这里顺便说一下,
防重设计
和
幂等设计
,其实是有区别的。
防重设计主要为了避免产生重复数据,对接口返回没有太多要求。
而幂等设计除了避免产生重复数据之外,还要求每次请求都返回一样的结果。
2.3 加乐观锁
既然悲观锁有性能问题,为了提升接口性能,我们可以使用乐观锁。需要在表中增加一个timestamp
或者version
字段,这里以
字段为例。
在更新数据之前先查询一下数据: SELECT id,amount,version from user id=123; ``
如果数据存在,假设查到的
等于1
,再使用id
和
字段作为查询条件更新数据:
UPDATE user set amount=amount+100,version=version+1
1 | ```sql |
1 |
|
操作的影响行数,如果大于0,则说明本次更新成功,如果等于0,则说明本次更新没有让数据变更。
由于第一次请求
等于1
是可以成功的,操作成功后
变成2
了。这时如果并发的请求过来,再执行相同的sql:
1 | UPDATE user set amount=amount+100,version=version+1 |
1 |
|
操作不会真正更新数据,最终sql的执行结果影响行数是0
,因为
已经变成2
了,where
1 | 中的 `version=1` 肯定无法满足条件。但为了保证接口幂等性,接口可以直接返回成功,因为 |
值已经修改了,那么前面必定已经成功过一次,后面都是重复的请求。
1 | 具体流程如下: |
具体步骤:
先根据id查询用户信息,包含version字段
根据id和version字段值作为where条件的参数,更新用户信息,同时version+1
判断操作影响行数,如果影响1行,则说明是一次请求,可以做其他数据操作。
如果影响0行,说明是重复请求,则直接返回成功。
2.4 加唯一索引
绝大数情况下,为了防止重复数据的产生,我们都会在表中加唯一索引,这是一个非常简单,并且有效的方案。
1 | ALTER table `order` add UNIQUE KEY `un_code` (`code`); |
1 |
|
第一次请求时,该订单的状态是已支付
,值是2
,所以该
1 | 语句可以正常更新数据,sql执行结果的影响行数是 |
,完成业务操作。
第一步,先获取token。
第二步,做具体业务操作。
具体步骤:
用户访问页面时,浏览器自动发起获取token请求。
服务端生成token,保存到redis中,然后返回给浏览器。
用户通过浏览器发起请求时,携带该token。
在redis中查询该token是否存在,如果不存在,说明是第一次请求,做则后续的数据操作。
如果存在,说明是重复请求,则直接返回成功。
在redis中token会在过期时间之后,被自动删除。
以上方案是针对幂等设计的。
如果是防重设计,流程图要改改:
1 | 个人微信 |
本文标题: 高并发下如何保证接口的幂等性
发布时间: 2022年01月06日 00:00
最后更新: 2025年12月30日 08:54
原始链接: https://haoxiang.eu.org/4d59f616/
版权声明: 本文著作权归作者所有,均采用CC BY-NC-SA 4.0许可协议,转载请注明出处!

