-
概念:一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。
-
我们常用的HTTP协议的方法是具有幂等性语义要求的,比如:get方法用于获取资源,不应有副作用,因此是幂等的;post方法用于创建资源,每次请求都会产生新的资源,因此不具备幂等性;put方法用于更新资源,是幂等的;delete方法用于删除资源,也是幂等的。
-
幂等的应用场景
1)微服务场景,除了成功、失败两种状态,还会有第三个情况【未知】,也就是超时。如果超时了,微服务框架一般会进行重试。
2)用户交互的时候多次点击。如:快速点击按钮多次。
3)MQ消息中间件,消息重复消费。
4)第三方平台的接口(如:支付成功回调接口),因为异常也会导致多次异步回调。
5)其他应用服务根据自身的特性,也有可能进行重试。
-
保证幂等的手段
-
唯一业务单号
最简单的,需要通过唯一的业务单号来保证幂等。也就是说相同的业务单号,认为是同一笔业务。使用这个唯一的业务单号来确保,后面多次的相同的业务单号的处理逻辑和执行效果是一致的。
-
对变更行为加锁
上述的保证幂等方案是分成两步的,第②步依赖第①步的查询结果,无法保证原子性的。在高并发下就会出现下面的情况:第二次请求在第一次请求第②步订单状态还没有修改为‘已支付状态’的情况下到来。既然得出了这个结论,余下的问题也就变得简单:把查询和变更状态操作加锁,将并行操作改为串行操作。
-
唯一索引
但是,在某些场景,你可能又想提供无锁的高并发幂等,那么你可以选择为业务单号加上唯一的索引或者组合索引,在并发的场景中,只有第一笔插入的交易请求能够成功,后续的请求哪怕是慢1ms或者更短时间,都会触发数据库的唯一索引异常而失败,那么你可以捕获这个异常。
-
数据库操作时使用插入或更新(select + insert)
如果已经存在就更新,不存在时才插入
-
多版本控制
这种方法适合在更新的场景中,比如我们要更新商品的名字,这时我们就可以在更新的接口中增加一个版本号,来做幂等
-
防重-Redis缓存
又或者你想把幂等放在服务的最前端,减少实际服务处理的资源浪费,在请求一到达时就提前去重,不让他有执行的机会,那么你可以考虑引入一个redis或类似的组件,将业务请求单号缓存在这个分布式锁的组件内。那么,每当订单发起交易请求,交易系统会去Redis缓存中查询是否存在该订单号的Key,如果不存在,则向Redis增加Key为订单号。查询订单是否已经执行,如果没有则转发到交易系统,执行完成后删除该订单号的Key。当然,Redis是提供分布式节点下的原子事务操作的。
-
状态机
通常是根据业务流程构建一个状态机,保证业务中每个流程只会在对应的状态下执行。如果一个业务操作步骤完成就进入下一个状态,这时候来了上一个状态的操作就不允许变更状态,保证了业务的幂等性。
-
token机制
通常是每次操作都生成一个唯一 token 凭证,服务器通过这个唯一凭证保证同样的操作不会被执行两次,服务器在去重实现上可以采用独立kv数据库,去重表等多种实现。Token 机制应该是适用范围最广泛的一种幂等设计方案。比如AWS的API就是采用这一机制,调用方生成client token,要求token全局唯一,AWS根据client token做幂等操作。
-
-
参考
学妹的整理
概念
在编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。
如果幂等设计放在越前端,那么提供的其实是一种防重方案;越往后端,随着业务逻辑的深入,幂等的设计方案也就更加复杂。这其实取决于相关交易系统的业务场景以及部署架构的特点。
应用场景
https://km.sankuai.com/page/210841811
2 幂等分析
为了帮助大家更好的梳理,我们先分析一下可能存在幂等的场景和解决幂等的常用方法:
2-1 查询场景
查询场景如:select 字段 from 表 where 条件 ,这种是不会对数据产生任何变化,所以查询场景本身就具备幂等性;注意这里的幂等是对系统资源的改变,而不是返回数据的结果,即使返回结果不相同但是该操作本身没有副作用,所以幂等。
2-2 新增场景
新增场景如:insert into 表 (id,字段) value(1,字段) ,分析:
1)如果id为唯一主键,即重复操作上面的业务,只会插入一条数据,具备幂等性。
2)如id不是主键,可以重复,那上面业务多次操作,数据都会新增多条,不具备幂等性。
所以新增场景需要注意,如果我们的新增接口里面没有唯一主键,那就需要特别注意怎么保证幂等
2-3 修改场景
修改场景有两种:
1)直接赋值场景:update 表 set price = 20 where id=1 ;分析: 这种场景不管执行多少次,price都一样,具备幂等性;
2)计算赋值场景:update 表 set price = price + 20 where id=1,每次操作price数据都不一样,不具备幂等性;
所以修改场景可能存在幂等、也可能不幂等,需要我们认真分析
2-4 删除场景 删除场景分两种:
1)delete from 表 where id=1 ,多次操作,结果一样,具备幂等性
2)delete from biao where price > 20,多次操作,结果一样,具备幂等性
删除虽然改变了系统资源,但是第一次和第N次删除操作对系统的作用是相同的,所以是幂等的
所以删除场景一般是本身就具备幂等的
2-5 常见应用场景
1)微服务场景,除了成功、失败两种状态,还会有第三个情况【未知】,也就是超时。如果超时了,微服务框架一般会进行重试,这个时候我们需要做到幂等;
2)用户交互的时候多次点击。如:快速点击按钮多次;这个时候需要做到幂等;
3)MQ消息中间件,消息重复消费;这个时候需要做到幂等;
4)第三方平台的接口(如:支付成功回调接口),因为异常也会导致多次异步回调;这个时候需要做到幂等;
5)其他应用服务根据自身的特性,也有可能进行重试;这个时候需要做到幂等;
实现方案
唯一业务单号
最简单的,需要通过唯一的业务单号来保证幂等。也就是说相同的业务单号,认为是同一笔业务。使用这个唯一的业务单号来确保,后面多次的相同的业务单号的处理逻辑和执行效果是一致的。
对变更行为加锁
上述的保证幂等方案是分成两步的,第②步依赖第①步的查询结果,无法保证原子性的。在高并发下就会出现下面的情况:第二次请求在第一次请求第②步订单状态还没有修改为‘已支付状态’的情况下到来。既然得出了这个结论,余下的问题也就变得简单:把查询和变更状态操作加锁,将并行操作改为串行操作。
唯一索引
但是,在某些场景,你可能又想提供无锁的高并发幂等,那么你可以选择为业务单号加上唯一的索引或者组合索引,在并发的场景中,只有第一笔插入的交易请求能够成功,后续的请求哪怕是慢1ms或者更短时间,都会触发数据库的唯一索引异常而失败,那么你可以捕获这个异常。
数据库操作时使用插入或更新
如果已经存在就更新,不存在时才插入
多版本控制
这种方法适合在更新的场景中,比如我们要更新商品的名字,这时我们就可以在更新的接口中增加一个版本号,来做幂等
防重-Redis缓存
又或者你想把幂等放在服务的最前端,减少实际服务处理的资源浪费,在请求一到达时就提前去重,不让他有执行的机会,那么你可以考虑引入一个redis或类似的组件,将业务请求单号缓存在这个分布式锁的组件内。那么,每当订单发起交易请求,交易系统会去Redis缓存中查询是否存在该订单号的Key,如果不存在,则向Redis增加Key为订单号。查询订单是否已经执行,如果没有则转发到交易系统,执行完成后删除该订单号的Key。当然,Redis是提供分布式节点下的原子事务操作的。
状态机
通常是根据业务流程构建一个状态机,保证业务中每个流程只会在对应的状态下执行。如果一个业务操作步骤完成就进入下一个状态,这时候来了上一个状态的操作就不允许变更状态,保证了业务的幂等性。
token机制
通常是每次操作都生成一个唯一 token 凭证,服务器通过这个唯一凭证保证同样的操作不会被执行两次,服务器在去重实现上可以采用独立kv数据库,去重表等多种实现。Token 机制应该是适用范围最广泛的一种幂等设计方案。比如AWS的API就是采用这一机制,调用方生成client token,要求token全局唯一,AWS根据client token做幂等操作。https://docs.aws.amazon.com/AWSEC2/latest/APIReference/Run_Instance_Idempotency.html
Reference
https://km.sankuai.com/page/70451271
https://km.sankuai.com/page/205786224
https://km.sankuai.com/page/182642364
https://km.sankuai.com/page/13624213