Contents

[后台]设计合理的幂等方案

幂等的作用

幂等的作用:保证可重入性,防止重复操作导致脏数据出现。使用场景可能有

  • 前端防抖
  • 接口超时重试
  • 消息重试
  • 只能调一次接口,如一个用户只能领一次券

我们的upsert、redis分布式锁、version乐观锁、for update、唯一索引、状态机其实都有幂等的功能,会在请求重复的时候报错

  • 分布式锁:一般用来防止并发操作,可以处理重复操作的问题。可以加在请求处理的最开始
  • 乐观锁:锁住读实体~写实体期间,这期间的任何其他操作都会失败。但自己也可能失败,要处理好失败的回滚逻辑
方案 支持维度 性能 幂等期 判断幂等时机 使用场景 备注
分布式锁 任意 加锁期间 请求开始 操作实体
乐观锁 实体id 读-写db期间 写db时 修改实体db行 失败需要回滚前面的操作
insert+uk 任意 写db之后 写db时 创建实体 失败需要回滚前面的操作

幂等键设计

结论:一定要用有业务语义的幂等键!最好的幂等键组合是entity_id+idem_key的组合

接口维度的幂等:幂等的控制交给上游,由上游保证自己的请求是可以幂等/不被幂等的。比如上游直接传一个md5sum(req)作为幂等键进来(其他常用的包括req里的核心参数、reqid、时间戳、消息id)。我们检测这个幂等键是否存在:

  • 幂等键已存在:直接幂等。问题:下游可能传错了,不幂等的也结果被幂等,比如批量请求、同一个请求里发起多次请求等。幂等应该自己来控制
  • 幂等键不存在:不幂等,这个不会出错

为了避免上面问题的出现,我们可以结合数据库的实体uk做判断。
数据维度的幂等:采用数据库的带业务语义的uniq key+幂等键联合判断。假设uk就是实体的id,幂等键是业务传过来的自定义值。我们去查找幂等键

  • uk已存在,幂等键不存在:说明用户希望再次操作同一个数据实体,不幂等
  • uk不存在,幂等键存在:说明用户希望再次操作其他数据实体,不幂等
  • uk、幂等键都存在:直接幂等
  • uk、幂等建都不存在:不幂等
1
2
3
4
5
6
7
// 这个例子实现了一个幂等键
// 业务场景:假设我们要设计一个领奖接口,每个用户只能领一次奖品。
// seq: 如果此奖励一个user_id可以重复领取多次,需要用seq标识唯一
// 用户id、奖励id、和seq表达一次幂等
func getIdemKey(userId string, prizeId string, seq string) (uniqueId string){
   return fmt.Sprintf("%v_%v_%v", userId, prizeId, seq)
}

实现

实现方法:

  1. 令牌发放:服务端/客户端生成一个token(幂等键),给客户端用,客户端带着token前来请求,token是一次性的,用过就直接幂等
  2. 幂等表:mysql的数据表里加一列幂等键,这个表一般是操作流水表,用来记操作。每次来请求的时候从流水表查幂等键存不存在,存在则直接幂等
  3. 实体表uk:实体表加一列create_idem_key作为uk,幂等的时候直接报错uk冲突。缺点是只能做实体创建的幂等,不能做实体操作的幂等
  4. 分布式锁:redis里存幂等key,用setnx+幂等key+超时时间控制

我们举例一个轻量级的幂等应用:

  1. 创建广告计划时,我们用计划表的create_idem_key作为uk来实现幂等,达到防重的效果
  2. 编辑、失效广告计划时,我们直接用分布式锁锁住计划id,接口不设置幂等键,防止并发出现的错误

注意点

  1. 锁住幂等:如果接口要加锁,幂等判断是要被分布式锁锁住的。如果幂等的实现是查数据库数据的话,数据库可能被别的请求刷新/读到备库都会导致幂等失效。
  2. 幂等视为成功:幂等不宜返回错误,应该视为成功并忽略。这就需要包掉数据库或者分布式锁返回的错误
  3. 两层加锁:如果用分布式锁作为幂等方案,锁住了幂等键,外层还需要再次用业务id锁住,避免并发操作的问题