幂等的概念
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
在编程中,一个幂等操作的特点是对于同一个系统,在同样条件下,一次请求和重复多次请求对资源的影响是一致的,就称该操作为幂等的。
幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。用通俗的话讲:就是针对一个操作,不管做多少次,产生效果或返回的结果都是一样的。
幂等性与并发安全
幂等性是系统接口对外的一种承诺,而不是实现,承诺多次相同的操作的结果都会是一样的。
而并发安全问题是当多个线程同时对同一个资源操作时,由于操作顺序等原因导致结果不正确。
这两个实际上是完全独立的两个问题,比如说同一笔订单即使你不停的提交支付,如果扣除了多次钱,就说明该操作不幂等。而有多笔订单同时进行支付,最后扣除金额不是这多笔金额的总和,那么说明该操作有并发安全问题。所以幂等性和并发安全是完全两个维度的问题,要分开讨论解决。
我在一些讨论幂等性的文章中看到中给出的解决方案为‘悲观锁’和‘乐观锁’,这两个方案可以很好的解决并发问题,但是却不应该是幂等性问题的解决方案,特别是悲观锁是用于防止多个线程同时修改一个资源的。倒是乐观锁的版本号机制可以勉强以 token 或者状态标识 作为版本号来实现幂等性(下文解释token 和状态标识),勉强说的过去。
所以说幂等性与并发安全是不同的。
Http 协议与幂等性
如果把操作按照功能分类,那就是增删改查四种,在 http 协议中则表现为 Get、Post、Put、Delete 四种。
查询操作 (Get)
Get 方法用于获取资源,不应当对系统资源进行改变,所以是幂等的。注意这里的幂等提现在对系统资源的改变,而不是返回数据的结果,即使返回结果不相同但是该操作本身没有副作用,所以幂等。
删除操作 (Delete)
Delete 方法用于删除资源,虽然改变了系统资源,但是第一次和第N次删除操作对系统的作用是相同的,所以是幂等的。比如要删除一个 id 为 1234 的资源,可能第一次调用时会删除,而后面所有调用的时候由于系统中已经没有这个 id 的资源了,但是第一次操作和后面的操作对系统的作用是相同的,所以这也是幂等的,调用者可以多次调用这个接口不必担心错误。
修改操作 (Put)
Put操作必须为幂等的,即如果声明为Put协议时就相当于对外声明这个接口是幂等的。所以例如账户中金额减少50元这种操作在Put协议中是不允许的,只能做类似于账户中金额改为 1000 元的操作
新增操作 (Post)
Post 新增操作天生就不是一个幂等操作,其在 http 协议的定义如下:
The POST method is used to request that the origin server accept the entity enclosed in the request as a new subordinate of the resource identified by the Request-URI in the Request-Line.
在其定义中表明了 Post 请求用于创建新的资源,这意味着每次调用都会在系统中产生新的资源,所以该操作注定不是幂等操作。这时候想要幂等就必须在业务中实现,方案在下文会讨论。
实现幂等性的方案
去重表
利用数据库的特性来实现幂等。通常是在表上构建一个唯一索引,那么只要某一个数据构建完毕,后面再次操作也无法成功写入。
使用数据库防重表的方式它有个严重的缺点,那就是系统容错性不高,如果幂等表所在的数据库连接异常或所在的服务器异常,则会导致整个系统幂等性校验出问题。
Redis实现
Redis实现的方式就是将唯一序列号作为Key。这里需要设置一个 key 的过期时间,否则 Redis 中会存在过多的 key。
Redis一般都会是集群的方式出现,至少肯定也会部署两台Redis服务器。所以我们使用Redis来实现接口的幂等性是最适合不过的了。
状态标识/状态机幂等
状态标识是很常见的幂等设计方式,主要思路就是通过状态标识的变更,保证业务中每个流程只会在对应的状态下执行,如果标识已经进入下一个状态,这时候来了上一个状态的操作就不允许变更状态,保证了业务的幂等性。
状态标识经常用在业务流程较长,修改数据较多的场景里。最经典的例子就是订单系统,假如一个订单要经历 创建订单 -> 订单支付\取消 -> 账户计算 -> 通知商户 这四个步骤。那么就有可能一笔订单支付完成后去账户里扣除对应的余额,消耗对应的优惠卷。但是由于网络等原因返回了错误信息,这时候就会重试再次去进行账户计算步骤造成数据错误。
所以为了保证整个订单流程的幂等性,可以在订单信息中增加一个状态标识,一旦完成了一个步骤就修改对应的状态标识。比如订单支付成功后,就把订单标识为修改为支付成功,现在再次调用订单支付或者取消接口,会先判断订单状态标识,如果是已经支付过或者取消订单,就不会再次支付了。
Token 机制
Token 机制应该是适用范围最广泛的一种幂等设计方案了,具体实现方式也很多样化。但是核心思想就是每次操作都生成一个唯一 Token 凭证,服务器通过这个唯一凭证保证同样的操作不会被执行两次。这个 Token 除了字面形式上的唯一字符串,也可以是多个标志的组合(比如上面提到的状态标志),甚至可以是时间段标识等等。
由于 Token 机制适用较广,所以其设计中要注意的要求也会根据业务不同而不同。
Token 在何时生成,怎么生成?这是该机制的核心。Token 生成的时机必须保证能够使该操作具多次执行都是相同的效果才行。使用 Token 机制就要求开发者对业务流程有较好的理解。
处理流程:
数据提交前要向服务的申请token,token放到redis或jvm内存,token有效时间 提交后后台校验token,同时删除token,生成新的token返回
token特点:
要申请,一次有效性,可以限流
注意:
redis要用删除操作来判断token,删除成功代表token校验通过,如果用select+delete来校验token, 存在并发问题,不建议使用
结语
幂等性是开发当中很常见也很重要的一个需求。尤其是金融、支付等行业对其要求更加严格,既要有好的性能也要有严格的幂等性。除了对其概念的掌握,理解自身业务需求更是实现幂等功能的要点,必须处理好每一个结点细节,一旦某个地方没有设计完善,最后的结果可能仍旧达不到要求。
参考
- 聊聊开发中幂等性问题 juejin.cn/post/684490…
- 我们来谈下高并发和分布式中的幂等处理juejin.cn/post/684490…
- 分布式系统中接口的幂等性 juejin.cn/post/684490…