接口幂等性

发布于 2023-11-30 12:53:34 字数 2843 浏览 19 评论 0

分布式系统中的幂等性概念:用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。

一. 幂等场景

可能会发生重复请求或消费的场景,在微服务架构中是随处可见的。

  • 网络波动:因网络波动,可能会引起重复请求
  • 分布式消息消费:任务发布后,使用分布式消息服务来进行消费
  • 用户重复操作:用户在使用产品时,可能无意地触发多笔交易,甚至没有响应而有意触发多笔交易
  • 未关闭的重试机制:因开发人员、测试人员或运维人员没有检查出来,而开启的重试机制(如 Nginx 重试、RPC 通信重试或业务层重试等)

二. CRUD 操作分析

  • 新增类请求:不具备幂等性
  • 查询类动作:重复查询不会产生或变更新的数据,查询具有天然幂等性
  • 更新类请求:
    • 基于主键的计算式 Update,不具备幂等性,即 UPDATE goods SET number=number-1 WHERE id=1
    • 基于主键的非计算式 Update:具备幂等性,即 UPDATE goods SET number=newNumber WHERE id=1
    • 基于条件查询的更新,不一定具备幂等性(需要根据实际情况进行分析判断)
  • 删除类请求:
    • 基于主键的 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

沙与沫

暂无简介

0 文章
0 评论
430 人气
更多

推荐作者

13886483628

文章 0 评论 0

流年已逝

文章 0 评论 0

℡寂寞咖啡

文章 0 评论 0

笑看君怀她人

文章 0 评论 0

wkeithbarry

文章 0 评论 0

素手挽清风

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文