TOC
什么是幂等性?
接口的幂等性是指相同的参数,调用一次或多次产生一致的效果;
为什么要保证幂等性?
在系统运行过程中,总会由于各种原因导致重复调用接口;例如:
- 前端没有用户重复提交限制,用户不断点击;
- 恶意用户通过直接访问 api,重复提交请求;
- 网络丢失重传,消息队列失败重传问题;
由于无法避免上述问题,因此要实现的接口要达到幂等性,这样即使重复调用,系统仍然处于正确的状态;
如何实现幂等性?
接口的幂等性不存在万金油解法,我们需要根据业务分析,选取合适的方式实现幂等性;
token 机制
整体上看,token 机制可分为两种操作,分别是申请 token 操作和执行业务操作;
对于订单支付功能的操作流程如下:
- 客户端发起申请 token 情况,服务端返回一个 token,并缓存起来;
- 客户端调用支付接口,同时传入获取到的 token;
- 服务端检查缓存中是否有 token;如果有则表示是第一次调用,反之则是重复调用,直接返回;
- 服务端执行订单支付逻辑,并从缓存中删除 token;
从流程中,我们可以看到 token 机制的关键点是通过判断 token 是否仍然被缓存,来判断调用是否重复;
同时要解决并发调用的问题,判断 token 是否存在,执行订单支付逻辑,删除 token,三个操作必须在同一个事务中执行;订单支付逻辑与删除 token 操作两者的顺序可随意;
token 机制的一个缺点是在于每次请求都需要申请 token 再调用接口;对于不出现重复调用时,性能有所损耗;
全局唯一 ID
全局性唯一 ID 是一种通用方案,能够处理更新,插入,删除等业务操作,一般的全局性 Id 会作为一个基础服务建设,避免各个服务模块重复构建;其操作流程如下:
- 根据业务操作和内容生成全局 ID;
- 检查是否存在全局 ID 存储在存储系统中(数据库,Redis)
- 如果不存在,则存储至存储系统中,并执行业务操作;
- 如果存在,则表示已执行过;
虽然这个方案能够解决许多场景,但是一个高可用的全局性 ID 服务比较复杂;例如要考虑服务将全局 ID 写入存储后挂了,此时需要引入超时机制; 以及全局性 ID 服务将成为一个许多业务都需要访问的流量聚集点,更要考虑高扩展,高性能;
下面列举一些实现较为简单,但只能处理某些情况的方案;
防重表
防重表适用于具有唯一标识的业务场景;对于订单支付场景,一个订单只会被支付一次,因此,订单 ID 可以作为订单支付场景的唯一标识;操作过程如下:
- 建立一张防重表,并将订单 ID 作为唯一索引;
- 把支付操作和插入防重表,放在一个事务中,如果重复操作,则会引起唯一索引冲突,操作将自动回滚;
InsertOrUpdate
InsertOrUpdate 适用于具有唯一标识的配置场景;例如,在处理订单与支付单据的映射关系时,其订单 Id 与 单据 Id 则会联合唯一索引;操作过程如下:
- 判断联合唯一索引是否存在;
- 如果不存在,则执行插入操作;
- 如果存在,则进行更新操作;
版本控制
版本控制适用于更新的业务场景;例如更新订单收货地址;操作过程如下:
- 业务接口增加一个版本号的字段,表中也增加一个版本号的字段
- 接口逻辑大致如下
update order set receiveAddress=#{receiveAddress},version=#{version} where id=#{id} and version<${version}
状态机控制
状态机控制适用于业务操作有状态转换的场景,例如订单的创建与订单的支付,订单的创建一定在订单的支付之前执行,订单支付成功后,不会变成订单支付失败;操作过程如下:
- 设计一个 int 类型的状态字段,其中 0:订单创建,99:订单支付失败,100:订单支付成功;
- 进行订单更新操作,其逻辑如下:
update `order` set status=#{status} where id=#{id} and status<#{status}