本文目录导读:

幂等设计在Java后端开发中非常常见,核心目的是防止重复提交、重复处理导致的数据不一致或资源浪费。
以下列出最适合做幂等设计的Java案例,从业务场景到技术实现进行分类:
支付与交易类(最典型、最严格)
案例:支付扣款 / 订单创建
- 场景:用户在支付页面点击“确认支付”按钮,由于网络波动或用户手抖,请求被发送了多次,如果服务端不处理幂等,用户会被扣两次款,产生两个订单。
- 业务要求:无论请求多少次,最终只扣一次款,只生成一个订单。
- 核心矛盾:支付成功后需要回调通知、或用户主动查询状态时,系统必须能识别“这是一笔已经处理过的请求”。
Java实现方案
- 利用数据库唯一索引:
- 在订单或流水表中,将
order_id或transaction_id设为唯一键。 - 插入前先尝试插入,如果报
DuplicateKeyException(主键/唯一键冲突),说明已处理。
- 在订单或流水表中,将
- Redis分布式锁 + 防重Token:
- 前端请求前先获取一个Token(如:
pay:token:{userId}:{orderId})。 - 后端处理时,使用
SET key uuid NX EX 30(只在不存在时设置,30秒过期)。 - 只有成功获取锁的请求才能继续处理,处理完立即删除锁。
- 注意:需要结合业务状态(如订单状态已为“已支付”)做最终兜底。
- 前端请求前先获取一个Token(如:
消息队列消费(最易忽略、影响面广)
案例:订单支付成功的回调消息处理
- 场景:支付服务发送“支付成功”消息到MQ(消息队列),消费者收到后执行发货或积分兑换,由于MQ支持至少一次投递(At Least Once),消息可能重复消费(例如消费者处理完后宕机,消息未ack,MQ重试)。
- 业务要求:同一笔订单的支付成功消息,无论消费多少次,只发一次货,只加一次积分。
Java实现方案
- 业务主键去重表:
- 在数据库中创建一张去重表,主键或唯一键为
message_id或business_key(如orderId+eventType)。 - 消费逻辑:
INSERT INTO dedup_table(business_key) VALUES(?),若成功则执行业务逻辑;若失败(唯一键冲突),则直接ack消息并跳过。
- 在数据库中创建一张去重表,主键或唯一键为
- Redis Set/布隆过滤器:
- 消费前
SADD dedup:msg:set {messageId},若返回1(新添加)则处理,返回0则忽略,适合高并发且允许小概率误判的场景。
- 消费前
表单提交与接口调用(最常见、用户体验)
案例:创建投票、提交表单、确认发货
- 场景:用户快速双击“提交”按钮,或前端防抖失效,导致请求被连续发送,虽然前端做了按钮置灰,但总有绕过(如F5刷新、浏览器回退重发)。
- 业务要求:同一用户、同一表单内容,多次提交只产生一条记录。
Java实现方案:Token + 一次消费
- 生成Token:后端生成一个唯一的Token(如UUID),存入Redis(Key:
submit:token:{userId}:{formId}),并返回给前端。 - 提交校验:前端提交时必须携带该Token。
- 后端消费:使用
DEL submit:token:{userId}:{formId}并检查返回值。- 若返回1(删除成功),说明是首次请求,执行插入。
- 若返回0(Key不存在或已删除),说明重复请求,直接返回“处理中”或“已提交”的提示。
外部系统回调通知
案例:第三方支付回调、短信/邮件发送回调
- 场景:支付宝/微信回调你的
notify_url,它们会重试通知(3-7次),且通知顺序可能乱序。 - 业务要求:同一笔交易的回调,只做一次状态更新(如将订单从未支付改为已支付)。
Java实现方案:CAS(Compare And Set)+ 数据库乐观锁
- SQL原生幂等:
UPDATE orders SET status = 'paid', update_time = NOW() WHERE id = ? AND status = 'unpaid';
- 影响行数
= 1:成功处理,这是第一次。 - 影响行数
= 0:说明订单状态已经不是 unpaid(已被其他回调处理过),忽略此次回调。
- 影响行数
- 配合唯一约束:在订单状态变更流水表中,设置
(order_id, target_status)为唯一联合索引。
文件上传或大对象操作
案例:上传用户头像、上传商品图片
- 场景:用户上传同一个文件(相同MD5),或上传时网络重试。
- 业务要求:的文件,只存储一份,不浪费存储空间;相同标识的附件,只关联一次。
Java实现方案:内容Hash + 唯一索引
- 计算文件MD5:上传完成后计算文件的MD5(或SHA-256)。
- 去重存储:
- 在文件存储表(OSS或本地文件系统记录表)中,设置
file_md5为唯一键。 - 存储前查询:若存在相同MD5的文件,直接复用原文件路径/ID,不重新上传。
- 插入时捕获唯一键冲突即可。
- 在文件存储表(OSS或本地文件系统记录表)中,设置
定时任务与批量处理
案例:每日报表生成、用户积分结算
- 场景:凌晨的定时任务,可能因分布式节点重复触发(例如两台服务器同时执行 cron job)。
- 业务要求:同一日期、同一业务范围的报表,只生成一份。
Java实现方案:分布式调度 + 任务标识幂等
- 使用Quartz/Elastic-Job的
@DisallowConcurrentExecution或分片机制:保证同一时间只有一个节点执行。 - 数据库任务状态锁:
- 记录一个任务执行记录表,主键为任务名+任务日期(如
report:2023-10-01)。 - 执行前
INSERT时,若冲突则说明已执行过,直接跳过。 - 配合
SELECT ... FOR UPDATE可精确控制。
- 记录一个任务执行记录表,主键为任务名+任务日期(如
选择方案的决策树
| 业务场景 | 推荐幂等方案 | 核心原因 |
|---|---|---|
| 支付扣款 | 数据库唯一索引 + 状态机乐观锁 | 强一致性,绝不允许重复扣款 |
| MQ消费 | 去重表(DB)或Redis Set | 处理海量消息,去重代价低 |
| 表单提交 | Redis Token(一次删除) | 高并发,低延迟,性能最优 |
| 回调通知 | 数据库乐观锁(update where status=unpaid) | 利用数据库行锁,天然幂等 |
| 文件上传 | 文件MD5唯一索引 | 业务特征(相同内容)适合哈希去重 |
| 定时任务 | 分布式锁或任务记录表唯一键 | 跨域一致性要求不高,防止重复生产 |
关键原则:
- 优先选择数据库唯一键/乐观锁:这是最基础、最可靠的幂等实现,多数MySQL/PostgreSQL场景都可以兜底。
- Redis适合做第一道拦截:性能极高,但在极端宕机或网络分区时可能有漏网之鱼,需要DB做最后的SC(最终一致性)保障。
- 接口设计上,天然提供幂等性:
PUT /order/{id}天然就是幂等的(多次更新相同内容结果相同),而POST /order不是,需要额外处理。
在Java开发中,推荐将幂等逻辑封装成注解 + AOP(如 @Idempotent),通过切面统一实现Token校验或状态判断,避免业务代码中侵入大量判断逻辑。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播