罗列一下常见幂等操作

文章目录

面试官让你聊"实现幂等的常见方式"时,最忌讳的是像背课文一样把这几个词吐出来。大厂更看重的是"因地制宜"的选型能力------每一个方案都有其致命的软肋和无可替代的王牌。

我们把图里的这几种核心方案拆开,从核心优势、致命缺点、适用场景这三个维度来降维打击:


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 → 已支付)、物流状态变更 。

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% 的用户手抖重复请求拦截掉 ;

然后在底层,如果是消息消费,我们会建立一张防重表并设置唯一索引 ,或者利用状态机作为最后的物理防线 。

这样既用缓存保证了高并发下的吞吐量,又用数据库保证了数据的绝对准确性 。"

相关推荐
ma_king11 小时前
后端开发者工程实践指南
java·后端
霸道流氓气质11 小时前
从零理解 Redisson:Java 分布式工具箱的入门与实战
java·开发语言·分布式
Java识堂11 小时前
并发工具类:AQS有哪些作用?(三)
java
Shan120511 小时前
一文读懂:C++中单例模式的实现
java·c++·单例模式
吱奇物语11 小时前
Maven高级—分模块设计与开发、继承、聚合和私服
java·maven
c++之路11 小时前
责任链模式(Chain of Responsibility Pattern)
java·前端·责任链模式
woniu_buhui_fei11 小时前
JDK8 开发最常用的新特性
java·开发语言
逸Y 仙X11 小时前
Elasticsearch安全集群构建的常见问题
java·大数据·安全·elasticsearch·搜索引擎·全文检索
huahailing102411 小时前
Java基于【S3】协议实现游标分页查询
java·开发语言