古语云:“一而再,再而三,其效不二”
俗语讲:被虐千百遍,依然如初恋
数学符号:f(f(f(x))) = f(x)
即无论操作执行一次还是多次,其效果始终如一,不会有差异。这就是幂等性。
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。
注意:数据库可能产生幂等性问题,但是幂等性问题不只发生在数据库。
比如在订单系统中,订单可以直接支付、积分抵扣、余额支付等不同的支付方式,除直接支付外,其他资产在获取的过程中可能存在以下过程:
由于网络存在不稳定的因素,这个通知可能会发送多次,极端请况下,同一笔订单的多次通知可能会同时到达商城服务端,如果不做幂等,那么同一笔订单就可能被多次处理,继而出现多次发放、ABA等一系列并发问题。
如何产生幂等问题
网络请求重试:网络波动或超时,客户端可能会重复发送相同的请求。
用户界面重复提交:用户在用户界面上可能会不小心重复点击按钮,导致仙童的请求多次发送。
消息队列重试机制:使用消息队列时,消息可能会被重复消费。
数据库并发操作:数据库插入、更新和删除操作多个事物同时修改同一条记录,而没有使用适当的锁机制或事务隔离级别。
外部系统API接口重试:对外提供的API接口可能由于调用方的重试逻辑,导致数据库操作被重复调用。
如何解决这个问题
唯一性约束
利用数据库的唯一性约束,如唯一索引或主键,来避免插入重复数据。
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'
乐观锁
通过记录数据的`版本号`或`时间戳`,仅当数据未被其他事务修改时,才允许更新操作执行。每次更新数据时,版本号都会递增。
UPDATE ordersSET quantity = 1, order_status = 1, pay_time = '2024-04-30 10:20:00', version = version + 1WHERE order_id = 'ORD-20231023-0001' AND version = 1;
悲观锁
使用悲观锁,事务在读取数据时会锁定相应的数据行,直到事务结束(提交或回滚)。这可以防止其他事务在锁定期间修改这些数据,从而确保数据的一致性。
在执行读取操作时,使用 SELECT ... FOR UPDATE 语句来锁定相关记录。
在执行读取操作时,使用 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';
悲观锁确保每个事务也能安全地执行,而不会导致数据不一致的问题。但是,悲观锁可能会因为锁定机制而导致 性能问题 ,尤其是在高并发的系统中,这可能会引起 锁争用和死锁 。
分布式锁
在分布式系统中,使用分布式锁来保证同一时间只有一个实例处理特定消息或请求。
状态机
使用状态机是判断业务流程,确保操作只执行一次。
状态机设计:
订单创建:订单初始化,状态为 PENDING(待支付)。
支付操作:当订单状态为 PENDING 时,允许执行支付操作,支付成功后状态变为 PAID(已支付)。
重复支付检查:如果再次尝试支付一个已经是 PAID 状态的订单,状态机将拒绝该操作,保持订单状态不变。
全局请求唯一ID
调用接口时,生成一个唯一 id,redis 将数据保存到集合中(去重),存在即处理过。可以使用 nginx 设置每一个请求的唯一 id;
proxy_set_header X-Request-Id $request_id;
来源:http://zxse.cn/archives/1717684955907