接口幂等性
分布式系统中的幂等性概念:用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。
一. 幂等场景
可能会发生重复请求或消费的场景,在微服务架构中是随处可见的。
- 网络波动:因网络波动,可能会引起重复请求
- 分布式消息消费:任务发布后,使用分布式消息服务来进行消费
- 用户重复操作:用户在使用产品时,可能无意地触发多笔交易,甚至没有响应而有意触发多笔交易
- 未关闭的重试机制:因开发人员、测试人员或运维人员没有检查出来,而开启的重试机制(如 Nginx 重试、RPC 通信重试或业务层重试等)
二. CRUD 操作分析
- 新增类请求:不具备幂等性
- 查询类动作:重复查询不会产生或变更新的数据,查询具有天然幂等性
- 更新类请求:
- 基于主键的计算式 Update,不具备幂等性,即
UPDATE goods SET number=number-1 WHERE id=1
- 基于主键的非计算式 Update:具备幂等性,即
UPDATE goods SET number=newNumber WHERE id=1
- 基于条件查询的更新,不一定具备幂等性(需要根据实际情况进行分析判断)
- 基于主键的计算式 Update,不具备幂等性,即
- 删除类请求:
- 基于主键的 Delete 具备幂等性
- 一般业务层面都是逻辑删除(即 update 操作),而基于主键的逻辑删除操作也是具有幂等性的
三. 幂等性的重要性
针对一个微服务架构,如果不支持幂等操作,那将会出现以下情况:
- 电商超卖现象
- 重复转账、扣款或付款
- 重复增加金币、积分或优惠券
超卖现象
比如某商品的库存为 1,此时用户 1 和用户 2 并发购买该商品,用户 1 提交订单后该商品的库存被修改为 0,而此时用户 2 并不知道的情况下提交订单,该商品的库存再次被修改为-1 这就是超卖现象。
究其深层原因,是因为数据库底层的写操作和读操作可以同时进行,虽然写操作默认带有隐式锁(即对同一数据不能同时进行写操作)但是读操作默认是不带锁的,所以当用户 1 去修改库存的时候,用户 2 依然可以都到库存为 1,所以出现了超卖现象。
解决方案 A:可以对读操作加上显式锁(即在 select … 语句最后加上 for update)这样一来用户 1 在进行读操作时用户 2 就需要排队等待了。但问题来了,如果该商品很热门并发量很高那么效率就会大大的下降,如何解决呢?(解决方案 B)
解决方案 B:我们可以有条件有选择的在读操作上加锁,比如可以对库存做一个判断,当库存小于一个量时开始加锁,让购买者排队,这样一来就解决了超卖现象。
四. 解决方案
4.1 全局唯一 ID
如果使用全局唯一 ID,就是根据业务的操作和内容生成一个全局 ID,在执行操作前先根据这个全局唯一 ID 是否存在,来判断这个操作是否已经执行。如果不存在则把全局 ID,存储到存储系统中,比如数据库、Redis 等。如果存在则表示该方法已经执行。
使用全局唯一 ID 是一个通用方案,可以支持插入、更新、删除业务操作。但是这个方案看起来很美但是实现起来比较麻烦,下面的方案适用于特定的场景,但是实现起来比较简单。
4.2 去重表
这种方法适用于在业务中有唯一标识的插入场景中,比如在以上的支付场景中,如果一个订单只会支付一次,所以订单 ID 可以作为唯一标识。这时,我们就可以建一张去重表,并且把唯一标识作为唯一索引,在我们实现时,把创建支付单据写入去重表,放在一个事务中,如果重复创建,数据库会抛出唯一约束异常,操作就会回滚。
4.3 插入或更新
这种方法插入并且有唯一索引的情况,比如我们要关联商品品类,其中商品的 ID 和品类的 ID 可以构成唯一索引,并且在数据表中也增加了唯一索引。这时就可以使用 InsertOrUpdate 操作。
4.4 多版本控制
这种方法适合在更新的场景中,比如我们要更新商品的名字,这时我们就可以在更新的接口中增加一个版本号,来做幂等:
boolean updateGoodsName(int id,String newName,int version);
在实现时可以如下:
update goods set name=#{newName},version=#{version} where id=#{id} and version<${version}
4.5 状态机控制
这种方法适合在有状态机流转的情况下,比如就会订单的创建和付款,订单的付款肯定是在之前,这时我们可以通过在设计状态字段时,使用 int 类型,并且通过值类型的大小来做幂等,比如订单的创建为 0,付款成功为 100,付款失败为 99。在做状态机更新时,我们就这可以这样控制:
update goods_order set status=#{status} where id=#{id} and status<#{status}
以上就是保证接口幂等性的一些方法。
总结
幂等性设计不能脱离业务来讨论,一般情况下,去重表同时也是业务数据表,而针对分布式的去重 ID,可以参考以下几种方式:
- UUID
- Snowflake
- 数据库自增 ID
- 业务本身的唯一约束
- 业务字段+时间戳拼接
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 什么是分布式事务?
下一篇: 谈谈自己对于 AOP 的了解
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论