[后台]设计合理的幂等方案
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、幂等建都不存在:不幂等
|
|
实现
实现方法:
- 令牌发放:服务端/客户端生成一个token(幂等键),给客户端用,客户端带着token前来请求,token是一次性的,用过就直接幂等
- 幂等表:mysql的数据表里加一列幂等键,这个表一般是操作流水表,用来记操作。每次来请求的时候从流水表查幂等键存不存在,存在则直接幂等
- 实体表uk:实体表加一列
create_idem_key
作为uk,幂等的时候直接报错uk冲突。缺点是只能做实体创建的幂等,不能做实体操作的幂等 - 分布式锁:redis里存幂等key,用setnx+幂等key+超时时间控制
我们举例一个轻量级的幂等应用:
- 创建广告计划时,我们用计划表的
create_idem_key
作为uk来实现幂等,达到防重的效果 - 编辑、失效广告计划时,我们直接用分布式锁锁住计划id,接口不设置幂等键,防止并发出现的错误
注意点
- 锁住幂等:如果接口要加锁,幂等判断是要被分布式锁锁住的。如果幂等的实现是查数据库数据的话,数据库可能被别的请求刷新/读到备库都会导致幂等失效。
- 幂等视为成功:幂等不宜返回错误,应该视为成功并忽略。这就需要包掉数据库或者分布式锁返回的错误
- 两层加锁:如果用分布式锁作为幂等方案,锁住了幂等键,外层还需要再次用业务id锁住,避免并发操作的问题