文章目录
-
-
- [1. 数据库唯一索引 / 幂等表(最强硬的物理防御)](#1. 数据库唯一索引 / 幂等表(最强硬的物理防御))
- [2. 状态机控制(最优雅的业务内功)](#2. 状态机控制(最优雅的业务内功))
- [[详解幂等: https://hwg985.blog.csdn.net/article/details/161398733?fromshare=blogdetail\&sharetype=blogdetail\&sharerId=161398733\&sharerefer=PC\&sharesource=weixin_46028606\&sharefrom=from_link\](https://hwg985.blog.csdn.net/article/details/161398733?fromshare=blogdetail\&sharetype=blogdetail\&sharerId=161398733\&sharerefer=PC\&sharesource=weixin_46028606\&sharefrom=from_link)](#详解幂等: https://hwg985.blog.csdn.net/article/details/161398733?fromshare=blogdetail&sharetype=blogdetail&sharerId=161398733&sharerefer=PC&sharesource=weixin_46028606&sharefrom=from_link)
-
- [3. 分布式锁(最常用的前置拦截)](#3. 分布式锁(最常用的前置拦截))
- [4. 去重 Token 机制(经典的防重试令牌)](#4. 去重 Token 机制(经典的防重试令牌))
- [5. 消息唯一 ID + 消费记录(MQ 消费的终极防线)](#5. 消息唯一 ID + 消费记录(MQ 消费的终极防线))
- [6. 乐观锁机制(无锁并发的利器)](#6. 乐观锁机制(无锁并发的利器))
- [💡 总结:项目实战中的"降维组合拳"](#💡 总结:项目实战中的“降维组合拳”)
-
面试官让你聊"实现幂等的常见方式"时,最忌讳的是像背课文一样把这几个词吐出来。大厂更看重的是"因地制宜"的选型能力------每一个方案都有其致命的软肋和无可替代的王牌。
我们把图里的这几种核心方案拆开,从核心优势、致命缺点、适用场景这三个维度来降维打击:
1. 数据库唯一索引 / 幂等表(最强硬的物理防御)
这是最基础也最坚固的防线,利用数据库底层 B+ 树索引的唯一性约束(Unique Index)来强制去重 。
- 核心优势: 绝对可靠。 由数据库主权保证,哪怕在高并发下两笔一模一样的请求同时杀到,也只可能有一笔成功,另一笔铁定报
DuplicateKeyException,绝无并发漏洞。
致命缺点: 性能有瓶颈,且不支持逻辑删除。 高并发下直接硬抗主库的写入压力,容易把数据库拖垮;此外,如果业务表存在逻辑删除字段(如 is_deleted = 1),唯一索引的联合流转会变得极其恶心,通常需要单独建立一张独立的"防重幂等表" 。
- 适用场景: 创建类(Insert)操作。 比如:用户首次提交订单、用户注册、积分首次开户。
2. 状态机控制(最优雅的业务内功)
通过业务自身的状态流转逻辑来进行天然的卡点(即带有条件更新的 SQL )。
例如:UPDATE order SET status = 'PAID' WHERE id = 123 AND status = 'UNPAID' 。
- 核心优势: 零额外成本。 不需要引入 Redis,不需要建额外的表,仅靠一行 SQL 自身的业务逻辑就能完美实现幂等。
- 致命缺点: 只适用于有前后状态因果关系的业务。 如果一个操作不涉及状态改变(比如只是修改了用户昵称、更新了笔记正文),状态机就完全派不上用场。
适用场景: 状态流转类操作。 比如:订单支付状态变更(未支付 → \rightarrow → 已支付)、物流状态变更 。
详解幂等: https://hwg985.blog.csdn.net/article/details/161398733?fromshare=blogdetail&sharetype=blogdetail&sharerId=161398733&sharerefer=PC&sharesource=weixin_46028606&sharefrom=from_link
3. 分布式锁(最常用的前置拦截)
在业务执行的最前端,用 Redis(SETNX)或者 Redisson 强行拦截短时间内的重复请求 。
- 核心优势: 极高并发下的性能保护伞。 把压力在缓存层(Redis)就拦截掉了,根本不需要走到数据库那一步,极大地保护了后端数据库。
- 致命缺点: 无法做到 100% 防御。 分布式锁是有"过期时间"的。如果后端业务由于特殊原因执行卡顿(比如 GC、RPC 超时),导致锁过期自动释放了,而此时第二笔重复请求进来,分布式锁就会防线失守。
适用场景: 高并发的前置防重。 比如:用户高频连续点击"点赞"按钮、防刷接口。通常它需要配合数据库唯一索引做双重防御 。
4. 去重 Token 机制(经典的防重试令牌)
前后端交互的经典范式。用户进入页面时,后端发一个唯一的 Token 存入 Redis,前端提交表单时必须带着这个 Token。后端校验成功并立刻删除 Token(Lua脚本:先检测Redis里面是不是存在这个token,然后删除),才允许执行业务。
- 核心优势: 精准拦截"前端重复提交"。 完美解决用户因为网络卡顿,连续狂点"提交"按钮导致的重复写入。
致命缺点: 链路长,多了一次网络 IO。 必须先申请 Token 才能做业务,且在高并发下"校验 Token 并在单点内将其删除(保证原子性)"必须借助 Lua 脚本,增加了架构复杂度 。
- 适用场景: 前端表单提交、复杂订单创建页。
5. 消息唯一 ID + 消费记录(MQ 消费的终极防线)
这正是你在面试中和面试官聊到的 RocketMQ 经典防重消费方案 :每条消息自带业务唯一键(如 bizNo),消费前先去查一下这条消息处理过没有 。
- 核心优势: 完美契合异步解耦场景。 它是应用层最直观的防重手段,确保 MQ 在由于网络抖动导致重试发射时,后端不至于重复消费。
致命缺点: 依然存在并发时差漏洞。 典型的"先查后写(Select-Then-Insert)"逻辑。如果在高并发下两条相同的重试消息同时进来,两边都查到"未消费",会同时往下走。因此,查询动作必须配合分布式锁或唯一索引才能闭环 。
适用场景: 异步消息队列(MQ)的消费者端。 比如:消费点赞消息落库、消费计费消息 。
6. 乐观锁机制(无锁并发的利器)
通过在数据库表中增加一个自增的 version(版本号)字段。每次更新时执行:SET money = money - 10, version = version + 1 WHERE id = 1 AND version = #{version}。
- 核心优势: 完全无锁,性能极高。 不引发任何线程阻塞和死锁,特别适合读多写少的高并发场景。
- 致命缺点: 容易带来高频的重试失败。 一旦并发稍微变高,多个线程拿到同一个版本号,只会有一笔成功,其他线程全部更新失败,业务层需要写大量的自旋重试代码。
- 适用场景: 账户余额扣减、库存扣减等纯数值更新场景。
💡 总结:项目实战中的"降维组合拳"
在真实的大厂大流量生产环境里,我们从来不单独使用某一个方案,而是采用"前置缓存拦截 + 后置物理兜底"的组合拳:
"在我们的微服务项目里,对于核心的消费和写入接口,我们一般先通过 Redis 分布式锁(防重 Token) 在最外层把 95% 的用户手抖重复请求拦截掉 ;
然后在底层,如果是消息消费,我们会建立一张防重表并设置唯一索引 ,或者利用状态机作为最后的物理防线 。
这样既用缓存保证了高并发下的吞吐量,又用数据库保证了数据的绝对准确性 。"