聊聊幂等性


幂等性

1.幂等的数学概念

如果在一元运算中,x 为某集合中的任意数,如果满足 f(x) = f(f(x)) ,那么该 f 运算具有幂等性。

绝对值运算 abs(a) = abs(abs(a)) 就是幂等性函数

如果在二元运算中,x 为某集合中的任意数,如果满足 f(x,x) = x,前提是 f 运算的两个参数均为 x,那么我们称 f 运算也有幂等性。

求大值函数 max(x,x) = x 就是幂等性函数

2.幂等概述

2.1幂等业务场景分析

生产环境经常出现过重复的数据?在排查问题的时候,数据又是正常的。这个是何解呢?怎么会出现这种情况,而且还很难排查问题。

原因 :产生重复数据或数据不一致(假定程序业务代码没问题),绝大部分就是发生了重复的请求,重复请求是指同一个请求因为某些原因被多次提交。导致这个情况会有几种场景:(本质上:多次请求)

1)微服务场景,在我们传统应用架构中调用接口,要么成功,要么失败。但是在微服务架构下,会有第三个情况【未知】,也就是超时。如果超时了,微服务框架会进行重试。
2)用户交互的时候多次点击。如:快速点击按钮多次。
3)MQ消息中间件,消息重复消费。
4)第三方平台的接口(如:支付成功回调接口),因为异常也会导致多次异步回调。
5)其他中间件/应用服务根据自身的特性,也有可能进行重试。

2.2接口幂等

接口的幂等性实际上就是 接口可重复调用,在调用方多次调用的情况下,接口最终得到的结果是一致的。更准确的讲:多次调用对系统的产生的影响是一样的,即对资源的作用是一样的,但是返回值允许不同。

2.3幂等业务场景举例

场景1:支付场景

1.一个订单创建接口,第一次调用超时了,然后调用方重试了一次
2.在订单创建时,我们需要去扣减库存,这时接口发生了超时,调用方重试了一次
3.当这笔订单开始支付,在支付请求发出之后,在服务端发生了扣钱操作,接口响应超时了,调用方重试了一次
4.一个订单状态更新接口,调用方连续发送了两个消息,一个是已创建,一个是已付款。但是你先接收到已付款,然后又接收到了已创建
5.在支付完成订单之后,需要发送一条短信,当一台机器接收到短信发送的消息之后,处理较慢。消息中间件又把消息投递给另外一台机器处理

场景2:一键三连

小破站有一个一键三连的功能,长按可以对up主进行激励,每个人对每个视频只有一个一键三连的机会。就算再喜欢某个视频,多次操作,也只能有一键三连一次。

场景3:统计DAU/MAU

DAU/MAU,又叫日活/月活,是用于反映网站、互联网应用或网络游戏的运营情况的统计指标。所以一个用户当天或者当月登录多次(或者达到某种活跃用户判断机制多次),也只能看作一个活跃用户,不能重复计算。

2.4CRUD与幂等

有些接口可以天然的实现幂等性,比如查询接口,对于查询来说,查询一次和多次,对于系统来说,没有任何影响,查出的结果也是一样,而其他功能,例如:增加、更新、删除都要保证幂等性。
以user表举例

1、查询,select * from user where xxx,不会对数据产生任何变化,具备幂等性

2、新增,insert into user(userid, name) values(1, ‘a’)

如 userid 为唯一主键,即重复操作上面的业务,只会插入一条用户数据,具备幂等性

如 userid 不是主键,可以重复,那上面业务多次操作,数据都会新增多条,不具备幂等性

3、修改,区分直接赋值和计算赋值

直接赋值,update user set point = 20 where userid = 1,不管执行多少次,point都一样,具备幂等性

计算赋值,update user set point = point + 20 where userid = 1,每次操作 point 数据都不一样,不具备幂等性

4、删除,delete from user where userid = 1,多次操作,结果一样,具备幂等性

因此,我们可以得出,没有唯一主键约束的数据,和修改计算赋值数据的操作都不具备幂等性

3.解决方案

3.1token + redis机制

token + redis 的幂等方案,适用于绝大部分场景。主要思想:

token作为请求的唯一性标示
redis作为存储token的数据库
每次请求先去redis查看token是否存在
不存在,将返回结果缓存到redis
存在,直接返回缓存结果
设置缓存有效期

3.2乐观锁机制

乐观锁这里解决了计算赋值型的修改场景

update user set point = #{point}+ 20, version = #{version}+ 1 where userid=#{userid} and version=#{version}

加上了版本号后,就让此计算赋值型业务,具备了幂等性

缺点:就是在操作业务前,需要先查询出当前的version版本,version实例如下:

  • 多版本控制

这种方法适合在更新的场景中,比如我们要更新商品的名字,这时我们就可以在更新的接口中增加一个版本号,来做幂等

boolean updateGoodsName(int id,String newName,int version);

在实现时可以如下

update goods set name=#{newName},version=#{version} where id=#{id} and version<${version}
  • 状态机控制

这种方法适合在有状态机流转的情况下,比如就会订单的创建和付款,订单的付款肯定是在之前,这时我们可以通过在设计状态字段时,使用int类型,并且通过值类型的大小来做幂等,比如订单的创建为0,付款成功为10,付款失败为-1。

在做状态机更新时,我们就这可以这样控制

update `order` set status=#{status} where id=#{id} and status<#{status}

3.3唯一主键机制

唯一主键机制就是根据业务的操作和内容生成一个全局唯一的主键ID,在执行操作前先根据这个全局唯一的主键ID是否存在,来判断这个操作是否已经执行。如果不存在则把全局ID,存储到存储系统中,比如数据库、redis等。如果存在则表示该方法已经执行。

从工程的角度来说,使用全局ID做幂等可以作为一个业务的基础的微服务存在,在很多的微服务中都会用到这样的服务,在每个微服务中都完成这样的功能,会存在工作量重复。另外打造一个高可靠的幂等服务还需要考虑很多问题,比如一台机器虽然把全局唯一的主键ID先写入了存储,但是在写入之后挂了,这就需要引入全局唯一的主键ID的超时机制。使用全局唯一ID是一个通用方案,可以支持插入、更新、删除业务操作。但是这个方案看起来很美但是实现起来比较麻烦。

总而言之,这个机制是利用了数据库的主键唯一约束的特性,解决了在insert场景时幂等问题。但主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键 ID来解决。

如果是分库分表场景下,路由规则要保证相同请求下,落点在同一个数据库和同一表中,要不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关。

因为对主键有一定的要求,这个方案就跟业务有点耦合了,无法用自增主键了。

3.4去重表机制

这种方法适用于在业务中有唯一标的插入场景中,比如在以上的支付场景中,如果一个订单只会支付一次,所以订单ID可以作为唯一标识。这时,我们就可以建一张去重表,并且把唯一标识作为唯一索引,在我们实现时,把创建支付单据和写入去去重表,放在一个事务中,如果重复创建,数据库会抛出唯一约束异常,操作就会回滚。

简单来说,就是 把唯一主键插入去重表,再进行业务操作,且他们在同一个事务中。这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题。

这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。

这个方案也是比较常用的,去重表是跟业务无关的,很多业务可以共用同一个去重表,只要规划好唯一主键就行了。

3.5门票机制

支付场景:单次支付请求,也就是直接支付了,不需要额外的数据库操作了,这个时候发起异步请求创建一个唯一的ticketId,就是门票,这张门票只能使用一次就作废。

具体步骤:

1.异步请求获取门票
2.调用支付,传入门票
3.根据门票ID查询此次操作是否存在,如果存在则表示该操作已经执行过,直接返回结果;如果不存在,支付扣款,保存结果
4.返回结果到客户端
如果步骤4通信失败,用户再次发起请求,那么最终结果还是一样的.

文章作者: Beaumon
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Beaumon !
评论
  目录