幂等性
幂等(idempotent、idempotence)是一个数学与计算机学概念,对于同一操作,无论是发起一次请求还是多次请求,最终的执行结果是一致的。不会因为多次请求而产生副作用,不影响系统状态。
在分布式环境中,网络环境更加复杂,因前端操作抖动、网络故障、消息重复、响应速度慢等原因,对接口的重复调用概率会比集中式环境下更大,尤其是重复消息在分布式环境中很难避免。
数据库 DML 幂等性
SELECT 查询天然幂等;
DELETE 删除也是幂等,删除同一个数据多次其效果一样;
UPDATE 直接更新某个值时,幂等;
UPDATE 更新累加操作的的结果,非幂等;
INSERT 操作会每次都新增一条,非幂等;
RESTful 中幂等性
HTTP Method | Idempotent |
---|---|
OPTIONS | yes |
GET | yes |
HEAD | yes |
PUT | yes |
POST | no |
DELETE | yes |
PATCH | no |
解决方案
通常由客户端和服务端约定一个唯一ID,在请求中携带唯一 ID,然后由服务端进行验证其唯一性。服务端会保存已经收到的唯一 ID,若出现唯一性冲突,就说明是处理过的请求,则可以返回给客户端之前的结果或是提示客户端请求重复。
对于幂等键通常是业务单据 ID 等,如何分布式环境下生成全局唯一 ID 也是需要考虑的。
幂等键:分布式唯一ID
以 MySQL 自增 ID 方案为例:
- 多个 mysql DB:每个 DB 是独立的发射器,相互产生的ID各不相同,假设集群一共有 4 个DBs,DB0 产生的 ID%4=0,DB1 的 ID%4=1,…,DB3 的 ID%4=3;
- 解决 DB 性能问题:一次从 DB 获取 N 个 ID,N 可配置,对应 DB 的操作就两个:然后在内存中记录 4*N 个 ID 中计算出模 4 为 DB_index 的 N 个 ID,放在内存 list 中。
- query 当前 ID
- update current_id = current_id + 4*N where current_id=id_queried
- 服务可水平扩展:每个服务会访问集群内的4个 DBs,实现对 DB 访问的负载均衡;
- 高可用:合理配置 N,单个 DB 可以满足足够高的 ID QPS 需求,因此,即使4个 DB 中的3个都挂了,单个 DB 完全能够撑住任意高的 ID QPS,服务还是 OK 的;
- 接口低延迟:调用方获取 ID 是从内存list中直接获取,list 有最低长度校验,低于该值,将异步触发从 DB 中获取 ID;
- 预加载:启动时,发号服务会加载 30W 个 ID 到内存 list 中,以保证,例如按照 QPS 峰值 500 计算,若所有 DB 全挂,内存list还能支撑 30 分钟的 ID 使用量;
- 降级:当所有 DB 全挂,并且内存 list 里面的ID全部用光时,可以降级到 timestamp 方案;
- 异地多活:要求跨 IDC ID 不重复。因为有每个集群 IDC 标识不同,可保证 ID 不重。
更多可以参考美团的分布式 ID 方案、滴滴TinyId、百度 uid-generator
流程
服务端幂等处理正常流程一般是:
- 判断幂等键是否已经存在,存在就不执行后续了,不存在则保存(可以保存在单独的幂等表);
- 处理业务逻辑;
- 返回结果。
这里还有一个细节是如果阶段 1 保存幂等键成功,但是阶段 2 执行失败了如何处理,因为可能导致无法重试的情况。所以要么不支持重试,要么把幂等键保存和业务逻辑放到同一个事务。
方案上常用的包括:
- 数据库利用唯一索引建立幂等表(推荐,支持事务);
- 利用乐观锁(如带版本号更新);
- 分布式锁实现(如 redis);
- 悲观锁实现(正常不用,性能较差)。
实例
以扣库存举例实现幂等,可以以 order_id, product_id
作为唯一索引 (且先 insert 能减少锁时间):
- 插入流水
insert into stock_log_table(order_id, product_id, count) values…
// 插入一条(订单ID,商品ID,数量)流水。 -
更新库存
update stock_table set stock=stock-N where product_id=xxx and stock >= N
参考