文章目录
-
- 先说结论
- 哪些场景会重复执行
- 方案一:唯一 ID + 去重表
- 方案二:乐观锁(版本号)
- 方案三:Token 机制
- 方案四:状态机
- 回答技巧与点评
-
- 加分回答
- 面试官点评
个人网站
用户点了一次"支付",网络超时了,又点了一次——会不会扣两次钱?这就是幂等性问题。在分布式环境下,网络抖动、超时重试、消息重复消费都可能导致同一操作被执行多次。面试官问这题,他想听的是:你能不能讲清幂等的概念、产生重复的场景、以及具体的解决方案?
先说结论
| 定义 | 同一操作执行一次和多次,结果完全一致 |
| 核心问题 | 防止重复提交/重复消费导致数据不一致 |
| 产生原因 | 网络重试、MQ 重复消费、用户双击 |
| 解决方案 | 唯一ID + 去重表、乐观锁、Token 机制、状态机 |
| 关键原则 | 天然幂等优先,改造幂等其次 |
|一句话记住:幂等就像"盖章"——同一章盖十次,纸上还是一个印"
哪些场景会重复执行
场景1:用户双击
用户点"提交" → 网络慢 → 以为没反应 → 又点一次 → 两次请求
场景2:超时重试
支付请求发出 → 网络超时 → 框架自动重试 → 请求到达两次 👈
场景3:MQ 重复消费
消费者处理完消息 → 准备确认ACK → 挂了 → MQ重新投递 → 消费两次
场景4:微服务重试
服务A调服务B → 超时 → 服务A重试 → 服务B实际已处理 → 处理两次
天然幂等的操作不用管:
| 查询 | SELECT * FROM user WHERE id = 1 |
| 绝对值设置 | UPDATE user SET name = ‘张三’ WHERE id = 1 |
| 删除 | DELETE FROM temp WHERE id = 1 |
非幂等的操作才需要处理:
| 新增 | INSERT INTO order VALUES (…) |
| 相对值更新 | UPDATE account SET balance = balance – 100 👈 |
| 状态流转 | UPDATE order SET status = ‘PAID’ WHERE status = ‘UNPAID’ |
方案一:唯一 ID + 去重表
每个请求带一个唯一 ID,处理前先查去重表:
// 幂等控制
public Result processOrder(OrderRequest req) {
String reqId = req.getRequestId(); // 👈 客户端生成的唯一ID
// 1. 查去重表,是否已处理
if (dedupMapper.exists(reqId)) {
return Result.success("已处理"); // 👈 直接返回,不重复执行
}
// 2. 插入去重表(利用唯一索引保证原子性)
dedupMapper.insert(reqId);
// 3. 执行业务逻辑
orderService.createOrder(req);
return Result.success();
}
去重表设计:idempotent_log (request_id PK, result, created_at)
就像医院的"挂号系统"——同一个挂号号只能看一次病,来了第二次直接告诉你"已经看过了"。
方案二:乐观锁(版本号)
更新数据时带上版本号,版本号不对就拒绝:
// 扣款——乐观锁
UPDATE account
SET balance = balance – 100, version = version + 1
WHERE id = 1 AND version = 5; // 👈 版本号必须匹配
// 影响行数 = 0 → 说明已被其他人/重试修改过,不重复扣款
乐观锁适合"更新"场景,保证同一版本只更新一次。
方案三:Token 机制
服务端先发 Token,提交时带上 Token,服务端验证并删除:
1. 进入页面 → GET /api/token → 服务端生成 Token 存 Redis
2. 提交表单 → POST /api/order?token=xxx → 服务端删除 Token
3. 重复提交 → POST /api/order?token=xxx → Token 已不存在 → 拒绝 👈
// 提交时校验 Token
Boolean deleted = redis.delete(token); // 👈 原子操作,删除成功才处理
if (!deleted) {
return Result.fail("请勿重复提交");
}
// 执行业务逻辑
就像电影票——一张票只能检一次,撕了就不能再用。
方案四:状态机
业务本身有状态流转,利用状态约束保证幂等:
// 支付——只有"待支付"才能变"已支付"
int rows = orderMapper.updateStatus(orderId, "UNPAID", "PAID");
// UPDATE order SET status='PAID' WHERE id=? AND status='UNPAID' 👈
if (rows == 0) {
// 状态已经不是UNPAID,可能已支付,直接返回成功
return Result.success("已支付");
}
幂等性全景
重复原因
├── 用户双击
├── 网络重试
├── MQ重复消费
└── 微服务重试
天然幂等
├── 查询、绝对值设置、删除
└── 不需要额外处理
解决方案
├── 唯一ID+去重表 → 通用,适合新增场景
├── 乐观锁 → 适合更新场景
├── Token机制 → 适合前端防重复提交
└── 状态机 → 适合有状态流转的业务
口诀:同一操作多次行,结果一致叫幂等;
去重表查唯一ID,乐观锁靠版本号;
Token一次就作废,状态机控流转方向;
天然幂等不用管,非幂等才要防范
回答技巧与点评
标准回答:幂等性指同一操作执行一次和多次结果一致。分布式环境下,网络重试、MQ 重复消费、用户双击都可能导致重复执行。解决方案:1)唯一 ID + 去重表——请求带唯一 ID,处理前查表判断;2)乐观锁——更新时校验版本号;3)Token 机制——服务端发 Token,提交时验证并删除;4)状态机——利用业务状态约束防止重复流转。原则:天然幂等的操作无需处理,非幂等操作根据场景选方案。
加分回答
面试官点评
这道题考的是你对 分布式可靠性 的理解。最忌讳的回答是只知道"防止重复提交"——面试官想听的是 具体场景和解决方案的对应关系:扣款用什么方案?订单状态流转用什么方案?能根据场景选方案,再讲清实现细节,就是高分回答。
原文阅读
内容有帮助?点赞、收藏、关注三连!评论区等你 💪