幂等

幂等与防重

wiki定义:

Idempotence is the property of certain operations in mathematics and computer science whereby they can be applied multiple times without changing the result beyond the initial application.

rest解释:

From a RESTful service standpoint, for an operation (or service call) to be idempotent, clients can make that same call repeatedly while producing the same result. In other words, making multiple identical requests has the same effect as making a single request. Note that while idempotent operations produce the same result on the server (no side effects), the response itself may not be the same (e.g. a resource’s state may change between requests).

The PUT and DELETE methods are defined to be idempotent. However, there is a caveat on DELETE. The problem with DELETE, which if successful would normally return a 200 (OK) or 204 (No Content), will often return a 404 (Not Found) on subsequent calls, unless the service is configured to “mark” resources for deletion without actually deleting them. However, when the service actually deletes the resource, the next call will not find the resource to delete it and return a 404. However, the state on the server is the same after each DELETE call, but the response is different.

从wiki的定义上看,幂等更关心的是是否“changing the result beyond the initial application.”,这里是beyond the initial application,而不是必须equal,所以从DELETE的角度看,虽然可能返回的200和404不一样,但是从系统的角度看,第二次404,对于系统来说影响是比第一次更小的,可以勉强算作幂等。

但是在分布式的系统中POST也会有重试,这个时候,就需要进行防重处理,可以说防重是实现幂等的一种手段。

场景与解决方式

+1问题

一些场景下,可以记录+1的原因记录代替+1的操作。

比如,增加抽奖次数,每个人的抽奖的次数其实是有限的,可以记录每个人增加抽奖次数的原因记录,在抽奖的时候,再去更改这个原因记录的状态,在查询抽奖次数的时候可以直接去统计抽奖次数。原因的记录可以通过类似活动id+抽奖类型+参数的方式建立唯一的约束,这样通过表约束的方式,重试的时候就不会有多级的记录。

比如:约束条件为活动id+下载抽奖+下载资源id,或者活动id+登陆抽奖+登陆时间端id。通过额外增加的一些信息不仅能防重复,出现问题还能更方便定位原因。

-1问题

库存

第一个思路,-1是否能转换成+1的操作。当然也是有一定场景的,还是抽奖的场景,如果奖品不多,那么可以把“中奖的手机的数目减少一”,这个操作理解为“增加一条中奖的手机的记录”,然后通过这增加记录的操作,反向去确定还有多个个奖品信息。

典型的有库存扣减问题,而库存扣减中,可能有并发的扣减。实现幂等可重试,还是需要有信息,如一张流水表,记录订单id与商品id,扣减库存先尝试插入流水表,失败则表示已经增加过了。

扣减库存方式如下:


- 悲观锁,需要使用声明式事物。

public void buy(String productName, Integer buyQuantity) { // 其他校验… Product product = select * from table where name = productName for update; if (查询的剩余数量 > buyQuantity) { 影响行数 = update table set surplus = (surplus - buyQuantity) where name = productName ; } else { return “库存不足”; }

// 记录日志...
// 其他业务... ```

以上三种方式,只能防止超卖,但是在无法实现幂等,如果要实现幂等,需要把写入流水日志也放到事物中来。

  • select stock from stock_table where product_id=xxx for update;行锁对应商品的库存,只有第一个开启事务的请求会继续向下执行,其他请求事务会挂起在该SQL等待。
  • update stock_table set stock=stock-N where product_id=xxx更新库存。
  • 插入流水insert into stock_log_table(order_id, product_id, count) values…,插入一条(订单ID,商品ID,数量)流水。

不使用流水日志,如果做到幂等,可以使用分布式锁的方式,创建订单时客户端发送一个随机的值用来去重:

弱一致性场景

比如评论,对于防重键可以直接使用添加时间(精确到纳秒)。

参考

rest
wiki

Table of Contents