欢迎光临
我们一直在努力

孤舟笔记 分布式与微服务篇九 什么是幂等性?为什么面试总问它?解决思路一次讲透

文章目录

    • 先说结论
    • 哪些场景会重复执行
    • 方案一:唯一 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)状态机——利用业务状态约束防止重复流转。原则:天然幂等的操作无需处理,非幂等操作根据场景选方案。

加分回答

  • 去重表要和业务表在同一个事务中:否则去重记录插入了但业务失败了,下次请求被拦截就永远无法处理。正确做法是去重记录和业务操作在同一个数据库事务中
  • MQ 消费幂等:RocketMQ 和 Kafka 都可能重复投递消息。最佳实践是消费端用"消息 ID + 去重表"保证幂等,而不是依赖 MQ 的 exactly-once 语义
  • HTTP 幂等性规范:GET/PUT/DELETE 天然幂等,POST 非幂等。RESTful 设计中,创建资源用 POST(非幂等),更新用 PUT(幂等),这是一个重要的设计原则
  • 面试官点评

    这道题考的是你对 分布式可靠性 的理解。最忌讳的回答是只知道"防止重复提交"——面试官想听的是 具体场景和解决方案的对应关系:扣款用什么方案?订单状态流转用什么方案?能根据场景选方案,再讲清实现细节,就是高分回答。

    原文阅读


    内容有帮助?点赞、收藏、关注三连!评论区等你 💪

    赞(0)
    未经允许不得转载:171主机测评 » 孤舟笔记 分布式与微服务篇九 什么是幂等性?为什么面试总问它?解决思路一次讲透
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址