幂等性(idempotence)的定义
幂等性(idempotence)是一个数学和计算机学概念,指的是对于同一操作,无论是一次还是多次执行,产生的结果是一致的,不会因为多次执行而产生副作用。在编程中,幂等操作是指可以使用相同参数重复执行,对系统产生的影响是一样的,即对资源的作用是一样的,不会发生副作用。
为什要实现幂等性
在分布式系统和网络通信中,幂等性尤为重要,以防止数据重复或丢失更新问题。开发人员在日常开发中必须要考虑幂等性的,尤其是转账、支付等涉及金额交易的场景,如果出现幂等性的问题,造成的后果是非常严重的。
非幂等的常见原因
非幂等的重要因素是重复提交引起的,一般情况下,接口调用时都能正常返回信息,不会重复提交,但遇见以下情况时就可能会出现问题,常见的场景如下:
●因网络波动,用户重复提交请求
●用户恶意进行刷单行为
●接口超时重试请求
●定时任务重试 ●使用消息队列时,重复消费现象
如何解决幂等性
幂等设计一般有两种处理方法:
(1)需要下游系统提供相关的查询接口。超时业务重试时,先通过接口查询,如果查询到数据,表明上次的调用已经成功,如果失败了就走失败流程。
(2)通过幂等性的方式。也就是这个查询操作交给下游系统,上游系统只管重试,由下游系统保证一次和多次的请求结果是一样的。
幂等的解决方案非常多,需要根据具体的业务场景选择具体策略。
方案一:数据库唯一索引实现幂等性
在保存数据前,可以先select一下数据是否存在。如果数据已存在,则不再写入数据,如果数据不存在,则执行insert操作。
但在高并发的场景下,可能会出现两个请求select的时候,都没有查到数据,然后都执行了insert操作,所以此时会有重复数据产生,因此在数据库中,我们需要添加唯一索引来保证幂等。
在数据库中,唯一索引是不会引起重复数据的兜底策略。
方案二:防重表机制
防重表机制与唯一索引机制是相同的原理,只不过是单独建一个防重表,防重表也必须引入唯一索引,而且防重表与业务表必须在同一数据库,并且操作要在同一个事务中。
防重表机制的主要流程:把唯一主键插入防重表,再进行业务操作,且它们处于同一个事务中。当重复请求时,因为去重表有唯一约束,导致请求失败,可以避免幂等问题。注意去重表和业务表应该在同一个库中,这样就保证了在同一个事务中,即使业务操作失败,也会把去重表的数据回滚。这样可以很好地保证数据的一致性。该方案也是比较常用的,去重表跟业务无关,很多业务可以共用同一个去重表,只要规划好唯一主键即可。
方案三:数据库乐观锁实现幂等性
数据库乐观锁方案适用于执行更新操作,通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号等于数据库表当前版本号,则予以更新,否则认为是过期数据。
update account set amount=amount-50,version=version+1
where id=123 and version = 1;
方案四:悲观锁实现幂等性
悲观锁的实现,往往依靠数据库提供的锁机制,具有强烈的独占和排他特性。
通过mysql 的sql 语句 for update 可以锁住数据;
select * from account where id = 123 for update;
这里id字段一定要是主键或者唯一索引,不然会锁住整张表,严重影响性能。
方案五:防重Token令牌实现幂等
此方案包含两个请求阶段:
1.客户端请求服务端申请获取token。
2.客户端携带token再次请求,服务端校验token后进行操作。
方案六:分布式锁
分布式锁的逻辑是,每次请求都通过业务唯一ID来尝试获取锁,如果获取成功,就进行后续业务逻辑操作,如果获取失败,就舍弃请求直接返回。
分布式锁通常是基于redis来实现的。
方案六:状态机
很多时候,业务流程是有状态流转的,这个时候可以使用状态机来保证幂等性。
如订单业务中,存在状态「1-已下单,2-已支付,3-已完成,4-已取消」,按照业务流程,状态是依次流转的,所以在update操作时,我们就要根据本次的状态来更新下一次的状态。
状态机其实是乐观锁的一种特例。
update order_info set status = 3
where id = 123 and status = 2;