幂等性解决方案

1、幂等性

在计算机中,表示对【同一个过程】应用【相同的参数】多次和应用一次产生的效果是一样,这样的过程即被称为满足幂等性。

幂等:

update user set age = 25 where user_id=2

这中情况无论执行多少次,结果都不受影响,所以是幂等的。

非幂等:

update user set age= age + 1 where user_id = 2

,这样的更新语句每执行一次,结果都会不一样,所以是非幂等的。

2、幂等的应用场景

2.1 网络波动引起的重试

不同微服务、中间件间基于http、rpc或mq进行网络通信,当出现网络波动时(规定时间内客户端未收到服务端响应),客户端会进行重试。

比如http客户端设置200ms超时时间,但因为网络问题,201ms服务端才收到请求。此时客户端误认为服务端丢失请求,重试发送第二次,服务端此时会受到两条一样的请求。

2.2 用户重复操作

  • 同一时刻多次点击按钮,发送多次请求
  • 页面后退后重新进入,发送两次请求

3、需要关注幂等性的接口

通常只需要对计算式写请求(新增 &更新)作幂等性保证。
计算式:需要进行变化的

3.1 查询 GET

SELECT * FROM users WHERE xxx;

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

3.2 新增 PUT

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

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

3.3 修改 POST

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

UPDATE users SET score = 30 WHERE user_id = 1;

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

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

3.4 删除 DELETE

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

DELETE FROM users WHERE id = 1;

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

DELETE top(3) FROM users;

4、如何解决幂等性问题

4.1 从源头上控制重复请求

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

主要解决方案:

  • 控制操作次数:提交按钮仅可操作一次(提交动作后按钮置灰)
  • 及时重定向:下单/支付成功后跳转到成功提示页面,消除浏览器前进或后退造成的重复提交问题。

缺点:
只能算是解决问题的辅助方法,用户可以直接绕过前端发送请求

4.2 过滤重复动作

控制过滤重复动作,是指在动作流转过程中控制有效请求数量。

  • 分布式锁
    利用 Redis 记录当前处理的业务标识,当检测到没有此任务在处理中,就进入处理,否则判为重复请求,可做过滤处理。

订单发起支付请求,支付系统会去 Redis 缓存中查询是否存在该订单号的 Key,如果不存在,则向 Redis 增加 Key 为订单号。查询订单支付已经支付,如果没有则进行支付,支付完成后删除该订单号的 Key。通过 Redis 做到了分布式锁,只有这次订单订单支付请求完成,下次请求才能进来。

分布式锁相比去重表,将放并发做到了缓存中,较为高效。思路相同,同一时间只能完成一次支付请求。

  • 先读后写
    业务上过滤掉已经成功执行的操作

给用户发红包时先查询当前用户是否发过红包,发过则过滤

缺点:
可能会出现并发问题。比如两个线程都执行抢优惠券操作

select count from coupon;
update coupon set count=count-1;

两个用户查询后都是1,两次更新后库存为-1,出现超卖问题
一般用于并发量小的后台接口或者对并发安全问题不在意的场景下,比如后台需要封禁用户。封禁前先查询是否已被封禁,已被封禁则过滤。即使出现查询时未被封禁,执行时发现用户封禁表已经存在,只需捕获唯一索引异常即可。

相较分布式锁方案来讲,实现简单,不需要关注redis

4.3 解决重复写

常见的方式有:悲观锁(for update)、乐观锁、唯一约束。
按照应用上的最优收益,推荐排序为:乐观锁 > 唯一约束 > 悲观锁。

  • 悲观锁(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; # 提交事务
  • 乐观锁(Optimistic Lock)
    每次去拿数据的时候都认为别人不会修改。更新时如果 version 变化了,更新不会成功。不过,乐观锁存在失效的情况,就是常说的 ABA 问题,不过如果 version 版本一直是自增的就不会出现 ABA 的情况。
UPDATE users 
SET name='xiaoxiao', version=(version+1) 
WHERE id=1 AND version=version;

缺点:就是在操作业务前,需要先查询出当前的 version 版本

另外,还存在一种:状态机控制
例如:支付状态流转流程:待支付->支付中->已支付
具有一定要的前置要求的,严格来讲,也属于乐观锁的一种。

update users
set status=2
where status=1
  • 唯一约束
    常见的就是利用数据库唯一索引或者全局业务唯一标识(如:source+序列号等)。
    这个机制是利用了数据库的主键唯一约束的特性,解决了在 insert 场景时幂等问题。但主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键。

全局 ID 生成方案:
• UUID:结合机器的网卡、当地时间、一个随记数来生成 UUID;

• 数据库自增 ID:使用数据库的 id 自增策略,如 MySQL 的 auto_increment。Redis 实现:通过提供像 INCR 和 INCRBY 这样的自增原子命令,保证生成的 ID 肯定是唯一有序的。

• 雪花算法-Snowflake:由 Twitter 开源的分布式 ID 生成算法,以划分命名空间的方式将 64-bit 位分割成多个部分,每个部分代表不同的含义。

5、总结

  • 前端控制控制重复请求
  • 分布式锁:适合核心业务或涉及到钱的操作
  • 先查后写:使用并发量低的场景
  • 悲观锁(for update):适合核心业务(不推荐使用)
  • 乐观锁-版本号:常用
  • 乐观锁-状态机控制:适合修改状态的场景
  • 唯一索引:业务上防重

高并发或核心业务:前端控制(筛选80%)+分布式锁(19%)+先查后写(过滤复杂逻辑)+唯一索引或版本号(1%)=100%

非高并发及非核心业务:前端控制(筛选80%)+先查后写(过滤复杂逻辑)+唯一索引或版本号(20%)=100%

状态流转:前端控制(筛选80%)+先查后写(过滤复杂逻辑)+乐观锁-状态机控制(20%)=100%

  • 50
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值