什么是幂等性?如何解决幂等性问题?

一、什么是幂等性?

幂等(idempotence),来源于数学中的一个概念,例如:幂等函数/幂等方法(指用相同的参数重复执行,并能获得相同结果的函数,这些函数不影响系统状态,也不用担心重复执行会对系统造成改变)。

通俗来说:就是多次调用对系统的产生的影响是一样的,即对资源的作用是一样的。

幂等性,强调的是外界通过接口对系统内部的影响, 只要一次或多次调用对某一个资源应该具有同样的副作用就行。

注意:这里指对资源造成的副作用必须是一样的,但是返回值允许不同!
在这里插入图片描述

二、幂等性的主要场景有哪些?

从上面幂等性的定义可知:产生重复数据或数据不一致,这个绝大部分是由于发生了重复请求。
这里的重复请求是指同一个请求在一些情况下被多次发起。那么,导致这个情况会有哪些场景呢?

  • 微服务架构下,不同微服务间会有大量的基于 http,rpc 或者 mq 消息的网络通信,会有第三个情况【未知】,也就是超时。如果超时了,微服务框架会进行重试。
  • 用户交互的时候多次点击,无意地触发多笔交易。
  • MQ 消息中间件,消息重复消费
  • 第三方平台的接口(如:支付成功回调接口),因为异常也会导致多次异步回调
  • 其他中间件/应用服务根据自身的特性,也有可能进行重试。

三、幂等性的作用?

幂等性主要保证多次调用对资源的影响是一致的。在阐述作用之前,我们利用资源处理应用来说明一下:

HTTP 与数据库的 CRUD 操作对应:

PUT :CREATE
GET :READ
POST :UPDATE
DELETE :DELETE

(其实不光是数据库,任何数据如文件图表都是这样)

1)查询

SELECT * FROM users WHERE xxx;

不会对数据产生任何变化,天然具备幂等性。

2)新增

INSERT INTO users (user_id, name) VALUES (1, 'zhangsan');

case1:带有唯一索引(如:user_id),重复插入会导致后续执行失败,具有幂等性;
case2:不带有唯一索引,多次插入会导致数据重复,不具有幂等性。

3)修改
case1:直接赋值,不管执行多少次 score 都一样,具备幂等性。

UPDATE users SET score = 30 WHERE user_id = 1;

case2:计算赋值,每次操作 score 数据都不一样,不具备幂等性。

UPDATE users SET score = score + 30 WHERE user_id = 1;

4)删除
case1:绝对值删除,重复多次结果一样,具备幂等性。

DELETE FROM users WHERE id = 1;

case2:相对值删除,重复多次结果不一致,不具备幂等性。

DELETE top(3) FROM users;

小结:通常只需要对写请求(新增 &更新)作幂等性保证。

四、如何解决幂等性问题?

我们在网上搜索幂等性问题的解决方案,会有各种各样的解法,但是如何判断哪种解决方案对于自己的业务场景是最优解,这种情况下,就需要我们抓问题本质。

经过以上分析,我们得到了解决幂等性问题就是要控制对资源的写操作。

我们从问题各个环节流程来分析解决:
在这里插入图片描述
幂等性问题分析

4.1 控制重复请求

控制动作触发源头,即前端做幂等性控制实现

相对不太可靠,没有从根本上解决问题,仅算作辅助解决方案。

主要解决方案:

  • 控制操作次数,例如:提交按钮仅可操作一次(提交动作后按钮置灰)

  • 及时重定向,例如:下单/支付成功后跳转到成功提示页面,这样消除了浏览器前进或后退造成的重复提交问题。

4.2 过滤重复请求

控制过滤重复动作,是指在动作流转过程中控制有效请求数量。
1)分布式锁
利用 Redis 记录当前处理的业务标识,当检测到没有此任务在处理中,就进入处理,否则判为重复请求,可做过滤处理。
关于具体实现可以参考我的另外两篇文章:Redis分布式锁解决幂等问题使用Redisson实现分布式锁解决幂等问题

  1. 订单发起支付请求,支付系统会去 Redis 缓存中查询是否存在该订单号的 Key。
  2. 如果缓存key不存在,则向 Redis 增加缓存 Key为订单号(主要是通过Redis的SETNX命令来实现),添加Key成功的那个线程开始执行业务逻辑。业务逻辑处理完成后需要删除该订单号的缓存 Key,进行释放锁资源。
  3. 如果缓存key存在,则直接返回重复标记给客户端,这样通过 Redis做到了分布式锁,只有这次请求完成,下次请求才能进来。

分布式锁相比去重表,将并发做到了缓存中,较为高效。思路相同,同一时间只能完成一次支付请求。
2)token 令牌
应用流程如下:

  1. 服务端提供了发送 token 的接口。执行业务前先去获取 token,同时服务端会把 token 保存到 redis 中;
  2. 然后业务端发起业务请求时,把 token 一起携带过去,一般放在请求头部;
  3. 服务器判断 token 是否存在 redis 中,存在即第一次请求,可继续执行业务,执行业务完成后将 token 从 redis 中删除;
  4. 如果判断 token 不存在 redis 中,就表示是重复操作,直接返回重复标记给 client,这样就保证了业务代码不被重复执行。
    在这里插入图片描述
    问题一:先执行业务再删除token

如果业务逻辑比较耗时或者其他原因,有可能出现第一次访问时token存在,开始执行具体业务操作。但在还没有删除token时,客户端又携带token发起同样的请求,此时,因为token还存在,第二次请求也会验证通过,执行具体业务操作。这样就没有保证幂等性。

解决办法:先删除token再执行业务

直接执行redis的del()方法,成功说明当前线程占有资源,可以执行业务逻辑,后面的请求进来,调用del()方法失败,则将其放行即可。从而达到幂等目的。

在这里插入图片描述

问题二:先删除token再执行业务

这种方案也会存在一个问题,假设具体业务代码执行超时或失败,没有向客户端返回明确结果,那客户端就很有可能会进行重试,但此时之前的token已经被删除了,则会被认为是重复请求,不再进行业务处理。

注意:这种方案一个token只能代表一次请求。一旦业务执行出现异常,则让客户端重新获取令牌,重新发起一次访问即可。恰恰对于我们解决幂等的场景来说没有影响,因此推荐这种方案!!!

3)缓冲队列
把所有请求都快速地接下来,通过缓冲队列,后续使用异步任务处理队列中的数据,过滤掉重复的请求数据,然后进行业务逻辑处理,更新数据库。
优点:同步转异步,实现高吞吐。
缺点:不能及时返回处理结果,需要后续监听处理结果的异步返回数据。
在这里插入图片描述

4.3 解决重复写问题

实现幂等性常见的方式有:悲观锁(for update)、乐观锁、唯一约束。
1)悲观锁(Pessimistic Lock)
简单理解就是:假设每一次拿数据,都有认为会被修改,所以给数据库的行或表上锁。
当数据库执行 select for update 时会获取被 select 中的数据行的行锁,因此其他并发执行的 select for update 如果试图选中同一行则会发生排斥(需要等待行锁被释放),因此达到锁的效果。

select for update 获取的行锁会在当前事务结束时自动释放,因此必须在事务中使用。(注意 for update 要用在索引上,不然会锁表)

# 开启事务
START TRANSACTION; 
SELETE * FROM users WHERE id=1 FOR UPDATE;
UPDATE users SET name= 'xiaoming' WHERE id = 1;
COMMIT; 
# 提交事务

使用悲观锁的场景:(对数据的并发更新操作比较多,冲突概率较高的场景)

  • 抢购
  • 秒杀
  • 金融交易

2)乐观锁(Optimistic Lock)
简单理解就是:就是很乐观,每次去拿数据的时候都认为别人不会修改。更新时如果 version 变化了,更新不会成功。

不过,乐观锁存在失效的情况,就是常说的 ABA 问题,不过如果 version 版本一直是自增的就不会出现 ABA 的情况。

UPDATE users 
SET name='xiaoxiao', version=(version+1) 
WHERE id=1 AND version=version;

缺点:就是在操作业务前,需要先查询出当前的 version 版本
另外,还存在一种:状态机控制

例如:支付状态流转流程:待支付->支付中->已支付

具有一定要的前置要求的,严格来讲,也属于乐观锁的一种。

使用乐观锁的场景:(对数据的并发更新操作比较少,冲突概率较低的情况)

  • 商品库存量更新
  • 用户账户余额更新
  • 点赞、收藏等操作

3)唯一主键

常见的就是利用数据库主键或者唯一索引(原理都是基于数据库字段的唯一性)。
唯一主键这个机制是利用了数据库的主键唯一约束的特性,解决了在 insert 场景时幂等问题。但主键的要求不能是自增的主键,这样就需要业务生成全局唯一的主键。

全局 ID 生成方案:

  • UUID:结合机器的网卡、当地时间、一个随记数来生成 UUID;
  • 数据库自增 ID:使用数据库的 id 自增策略,如 MySQL 的 auto_increment。
  • Redis 实现:通过提供像 INCR 和 INCRBY 这样的自增原子命令,保证生成的 ID 肯定是唯一有序的。
  • 雪花算法-Snowflake:由 Twitter 开源的分布式 ID 生成算法,以划分命名空间的方式将 64-bit 位分割成多个部分,每个部分代表不同的含义。

使用数据库唯一索引的场景:(插入数据时需要保证幂等性的场景)

  • 订单创建
  • 用户注册

小结:

选择哪种方法来解决幂等问题,需要根据具体的业务场景来进行权衡。

  • 乐观锁的优点是并发性能好,缺点是无法保证数据的强一致性。
  • 悲观锁的优点是可以保证数据的强一致性,缺点是并发性能差。
  • 数据库唯一索引可以保证数据的唯一性,但无法解决并发冲突问题。

按照应用上的最优收益,推荐排序为:乐观锁 > 唯一约束 > 悲观锁。

五、总结

通常情况下,非幂等问题,主要是由于重复且不确定的写操作造成的。

  1. 解决重复的主要思考点

从请求全流程,控制重复请求触发以及重复数据处理:

  • 客户端 控制发起重复请求
  • 服务端 过滤重复无效请求
  • 底层数据处理 避免重复写操作
  1. 控制不确定性主要思考点

从服务设计思路上做改变,尽量避免不确定性:

  • 统计变量改为数据记录方式
  • 范围操作改为确定操作

温馨提示:

  • 幂等性处理 虽然复杂了业务处理,也可能会降低接口的执行效率,但是为了保证系统数据的准确性,是非常有必要的;
  • 遇到问题,善于发现并挖掘本质问题,这样解决起来才能高效且精准;
  • 选择自身业务场景适合的解决方案,而不要去硬套一些现成的技术实现,无论是组合还是创新,要记住适合的才是最好的。
  • 分享一个开源的保证幂等性的项目:幂等starter,针对实际项目的业务场景对实现方式进行调整,可以作为starter引入到实际的项目中进行使用。
  • 23
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

技术杠精

你的鼓励是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值