接口幂等性
一、什么是幂等
幂等(Idempotence)是一个数学和计算科学概念,简单的来说:一个操作多次执行产生的结果与一次执行产生的结果一致,不会因多次执行而产生负面影响。如等电梯时,我们按一次按钮和按多次结果是一样,或者刷公交卡时,二次刷卡会提示刷卡重复。
二、幂等发生场景
-
表单重复提交:
在填写页面form表单时,不小心快速保存了两次,对应的后台产生了两条相同的记录。
-
接口超时重试:
为解决接口调用超时,我们引入了重试机制,但如果因为网络抖动、临时故障等问题导致第一次请求没及时收到返回结果(实际上服务端业务处理成功),那么就会重试接口,从而产生重复数据。
-
消息重复消费:
mq消费者在读取消息时,如果读取到重复的消息,也会导致重复消费问题。
三、幂等发生在哪些操作
所有的操作最终对应的数据库都是CURD。因此幂等最终反映在select、update、insert、delete上。
对于select查询,无论查询一次还是多次,其返回的结果都是一致的,因此,所以select查询是天然幂等;
对于delete删除,如果在不考虑返回结果情况下,删除一次或多次,其也是具有幂等性;
对于insert新增,新增一次或多次,对应产生重复记录,其不具有幂等性;
对于update更新,如果是直接更新设置,不管是执行多少次,都是幂等的(比如:
update user set status=1 where id=1
),但如果是更新计算,那么就存在幂等问题(比如:update user set status=status+1 where id=1
)。
因此,幂等问题主要发生在新增操作以及更新计算操作中。
四、如何保证幂等
-
insert前先select判断
在执行新增操作时,先查询是否存在,如果存在,执行更新操作,如果不存在,才进行新增操作。方案简单实用,但如果对于多节点并发场景中,该方案仍然会存在重复数据的幂等问题,需要结合其他方案进行优化。
-
增加唯一索引
在表中建立唯一索引,需要注意的是,如果二次请求操作,需要对数据库抛出DuplicateKeyException进行捕获,正常返回处理成功结果。
-
引入token
需要两次请求完成一次业务操作,第一次请求获取token,第二次请求带上这个token,完成业务操作。
-
采用悲观锁
悲观锁适合高并发并且对数据的准确性要求很高的场景,如支付、库存场景。悲观锁常用的是数据库表的for update
语句,原则:一锁二判三更新
如下对于支付场景案例,多个端扣除账户金额时,需要保证账户金额充足才能扣减,否则会造成预支情形。引入悲观锁机制,在每笔金额扣减时,先查询金额,对当前用户金额记录进行加锁,防止并发修改数据,导致并发事务预支扣减。`begin;` `select * from user_account where userId=xxx for update;` `update user_account set account=account-pay where userId=xxx;` `commit;`
-
采用乐观锁
由于悲观锁,锁的记录行,如商品库存系统,某类商品并发场景下会造成大量的请求累积,从而直接影响接口性能。为提高性能问题,采用乐观锁机制。乐观锁,通过在表中引入timestamp或version字段进行版本控制更新。假设有一张商品库存表goods_stock,包含id,goodsId(商品id), stock(库存数量),version(版本号)四个字段。
步骤1:根据商品id查询库存表信息 `select goodsId, stock,version from goods_stock where goodId=xxx` 步骤2:根据商品id和当前版本号进行扣减库存 `update goods_stock set stock=stock - buyCount,version=version+1 where goodsId=xxx and version=xxx` 步骤3:如果更新失败,进入重试 。
采用分布式锁
不论是悲观锁,还是乐观锁,都是属于数据库层面上分布式锁。还有基于zookeeper和redis的分布式锁,一般常用的redis来作为分布式锁,同时spring官网也推荐使用redisson这个框架api。这里不做详细介绍,像分布式锁单点问题;集群后,加锁成功,master未来及复制到slave上导致锁信息丢失问题;由于GC或网络延迟导致的任务时间变长,从而导致执行时间超过锁过期时间,其他线程获取锁;以及为解决以上问题,引入redLock是如何的原理。在分布式锁文章中再做详细介绍。-
建立防重表
防重表,也是利用数据库的唯一索引约束,在业务表所在的数据库中单独创建一个去重表。每次请求来时,先执行去重表插入记录,如果成功执行业务表逻辑,如果失败,结束。
这里,有个小插曲,很多人可能会疑惑,既然是用了唯一索引约束,那又跟上面方案2唯一索引有啥区别的,为什么不直接在业务表里采用唯一索引进行去重,反而单独设计一张去重表里增加维护数据库表的难度。是这样的,我们的业务可能涉及到的不止一张表,而唯一索引又是通过这个多个业务表的多个字段来确认唯一性的,那么这时候,就只能通过去重表来去解决。
采用状态机
对于有些业务存在业务状态控制流转,每个状态都有前置状态和后置状态,例如工单系统:待审批、审批中、撤销、审批通过、审批拒绝。订单支付系统:待提交、待支付、已支付、取消。待提交的后置状态为待支付,已支付的上一状态必须为待支付状态,取消的上一状态必须为待支付状态。
状态机方案从某一定的程度上可以理解为乐观锁的版本号方案,但不同的是前者基于业务层面的,后者基于数据库层面,同时后者是递增方式。总体来说,处理的业务场景不同。