幂等设计的8种实现方式
什么是幂等性
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致,不会因为多次点击而产生了副作用。
什么场景需要幂等性设计
一般对数据要求比较高的场景,如:金钱交易(对数据一致性至关重要)的业务场景:
- 在线支付:当用户发起支付请求时,避免重复扣款。
- 银行交易:确保同一笔交易不会因网络重试等原因被执行多次。
- 票务系统:在线购票平台在用户购票时,检查所选座位是否已被重复预订。
- 通信服务:如短信或通话服务,系统会检查是否已为相同内容的请求计费。
- 任务调度:在定时任务或批处理系统中,确保不会因为任务重启或重试而重复执行相同的操作。
- 用户注册:防止因重复提交表单而导致用户信息被创建多次。
如何产生幂等性问题
产生幂等性问题的原因主要有:
1)网络请求重试:网络波动或超时,客户端可能会重复发送相同的请求。
2)用户界面重复提交:用户在用户界面上可能会不小心重复点击按钮,导致相同的请求被发送多次。
3)消息队列重试机制:使用消息队列(如Kafka、RabbitMQ)时,消息可能会被重复消费。
4)数据库并发操作:数据库的插入、更新和删除操作多个事务同时修改同一条记录,而没有使用适当的锁机制或事务隔离级别。
5)外部系统API接口重试:对外提供的API接口可能由于调用方的重试逻辑,导致数据库操作被重复调用。
解决方案
幂等性设计解决方案通常在分布式系统中,常见的幂等性设计方案如下:
1、唯一性约束
利用数据库的唯一性约束,如唯一索引或主键,来避免插入重复数据。
mysql> INSERT INTO `mydb`.`orders` (`order_id`, `user_id`, `product_id`, `quantity`, `order_status`, `create_time`, `pay_time`, `version`) VALUES ('ORD-20231023-0001', 'USR-A123456', 'PRD-X123', 2, 0, '2023-10-23 10:15:30', NULL, 1);
ERROR 1062 (23000): Duplicate entry 'ORD-20231023-0001' for key 'orders.PRIMARY'
注意:业务上要求生成全局唯一的主键。且不是自增策略,否则在分库分表的场景下,不同的表之间主键互不关联。
2. 乐观锁
通过记录数据的版本号或时间戳,仅当数据未被其他事务修改时,才允许更新操作执行。每次更新数据时,版本号都会递增。
UPDATE orders
SET
quantity = 1,
order_status = 1,
pay_time = '2024-04-30 10:20:00',
version = version + 1
WHERE
order_id = 'ORD-20231023-0001' AND
version = 1;
如果 Session-01 已经提交了事务,Session-02 的更新操作将不会影响任何行,因为 version 已经从 1 增加到了 2。
3. 悲观锁
使用悲观锁,事务在读取数据时会锁定相应的数据行,直到事务结束(提交或回滚)。这可以防止其他事务在锁定期间修改这些数据,从而确保数据的一致性。
在执行读取操作时,使用 SELECT ... FOR UPDATE
语句来锁定相关记录。
-- 锁定记录
SELECT * FROM orders WHERE order_id = 'ORD-20231023-0001' FOR UPDATE;
-- 执行业务逻辑
UPDATE orders SET quantity = 1, order_status = 1, pay_time = '2023-10-23 10:20:00' WHERE order_id = 'ORD-20231025-0003';
由此可见,悲观锁确保每个事务也能安全地执行,而不会导致数据不一致的问题。但是,悲观锁可能会因为锁定机制而导致 性能问题
,尤其是在高并发的系统中,这可能会引起锁争用和死锁。
4. 分布式锁
在分布式系统中,使用分布式锁来保证同一时间只有一个实例处理特定消息或请求。
5. Token令牌机制
为每个请求生成一个唯一的Token,并在服务端进行校验,一旦处理了对应的请求,就丢弃该Token,避免重复处理。
6. 状态机
使用状态机是判断业务流程,确保操作只执行一次。
状态机设计:
- 订单创建:订单初始化,状态为 PENDING(待支付)。
- 支付操作:当订单状态为 PENDING 时,允许执行支付操作,支付成功后状态变为 PAID(已支付)。
- 重复支付检查:如果再次尝试支付一个已经是 PAID 状态的订单,状态机将拒绝该操作,保持订单状态不变。
幂等性保证:
-
支付操作pay在订单状态不是PENDING 时不会被执行,从而保证了幂等性。
-
如果有重复的支付请求,由于状态机的保护,第二次及后续的支付请求将不会改变订单状态,因此不会执行重复的支付逻辑。
7. 去重表
记录已经处理过的请求标识,对于重复的请求直接返回结果,而不再次执行业务逻辑。
8. 全局请求唯一ID
调用接口时,生成一个唯一 id,redis 将数据保存到集合中(去重),存在即处理过。