幂等性简单介绍
1. 幂等性是什么
- 核心定义:操作重复执行多次的最终效果与执行一次的效果完全相同。
- 通俗理解:无论调用多少次,结果都像只调用一次,不会因多次调用产生额外副作用。
- 关键点:
- 关注最终状态而非过程
- 强调重复执行的安全性
- 是分布式系统、网络通信、数据库操作的重要特性
2. 需要防止幂等性问题的场景
常见诱因:
- 用户重复点击/刷新页面
- 网络超时触发重试
- 消息队列重复消费
- 微服务调用重试
- 恶意攻击(如重复支付)
- 定时任务重复触发
导致的后果:
- 重复下单(多次扣款单次发货)
- 重复扣款/发放优惠券
- 数据状态混乱(如库存负数)
- 业务逻辑错误
3. 需要幂等性的场景
天然幂等的操作(无需额外设计)
操作类型 | 示例 | 说明 |
---|---|---|
查询 (Read) | SELECT * FROM orders WHERE id=123; | 多次查询返回相同结果 |
纯删除 (Delete) | DELETE FROM users WHERE id=456; | 首次删除后后续操作无影响 |
设值更新 (Update) | UPDATE product SET price=99 WHERE id=1; | 最终状态固定为设定值 |
完整替换 (PUT) | PUT /user/123 {完整数据} | 多次替换结果相同 |
需设计幂等性的操作
- 创建操作:
INSERT INTO orders...
(会生成重复数据) - 条件更新:
UPDATE account SET balance=balance-100...
(多次执行多扣款) - 状态转换:订单状态从
UNPAID
→PAID
(需防重复支付) - 部分更新:
PATCH /user {points:+10}
(多次执行累加积分) - 外部调用:支付接口、短信通知等有副作用的操作
4. 幂等性实现方案(以用户重复下单为例)
方案1:Token 机制
1. 用户进入下单页 → 服务端生成Token存Redis并返回前端
2. 首次提交:携带Token → 服务端验证Token存在 → 执行业务 → 删除Token
3. 重复提交:携带相同Token → 服务端检查Token不存在 → 拒绝操作
特点:防止前段重复提交,需要配合Redis
方案2:唯一索引
ALTER TABLE orders ADD UNIQUE INDEX (order_no); -- 订单号唯一约束
首次提交:INSERT订单成功(订单号=SN20250723001)
重复提交:INSERT相同订单号 → 触发唯一约束异常 → 拦截重复订单
特点:数据库层面强校验,适合创建操作
方案3:乐观锁(版本号)
-- 订单表结构
id | status | version
------------------------
123 | UNPAID | 1-- 支付操作SQL
UPDATE orders
SET status='PAID', version=version+1
WHERE id=123 AND version=1 AND status='UNPAID';
首次支付:版本号1匹配 → 更新成功(version→2)
重复支付:版本号1不匹配(当前version=2)→ 更新0行 → 判定为重复请求
特点:适合更新操作,需维护版本字段
方案4:状态机控制
定义状态流:UNPAID → PAID → SHIPPED
首次支付:UNPAID→PAID(允许)
重复支付:PAID→PAID(禁止状态跃迁)→ 拒绝操作
很多时候,业务流程是有状态流转的,这个时候可以使用状态机来保证幂等性。
如订单业务中,存在状态「1-已下单,2-已支付,3-已完成,4-已取消」,按照业务流程,状态是依次流转的,所以在update操作时,我们就要根据本次的状态来更新下一次的状态。
状态机其实是乐观锁的一种特例。
update order_info set status = 3 where id = 123 and status = 2;
特点:业务逻辑自约束,适合状态转换场景(乐观锁的一种实现方式)
方案5:分布式锁
锁可以解决幂等性问题,但是一般不用(性能较低)
1. 生成锁Key:lock:order:user_001
2. 首次请求:获取Redis锁成功 → 创建订单 → 释放锁
3. 重复请求:获取相同Key锁失败 → 直接返回"操作频繁"
特点:强一致性,但性能开销大