哪些Java案例适合做幂等设计?

wen java案例 5

本文目录导读:

哪些Java案例适合做幂等设计?

  1. 支付与交易类(最典型、最严格)
  2. 消息队列消费(最易忽略、影响面广)
  3. 表单提交与接口调用(最常见、用户体验)
  4. 外部系统回调通知
  5. 文件上传或大对象操作
  6. 定时任务与批量处理
  7. 选择方案的决策树

幂等设计在Java后端开发中非常常见,核心目的是防止重复提交、重复处理导致的数据不一致或资源浪费。

以下列出最适合做幂等设计的Java案例,从业务场景到技术实现进行分类:

支付与交易类(最典型、最严格)

案例:支付扣款 / 订单创建

  • 场景:用户在支付页面点击“确认支付”按钮,由于网络波动或用户手抖,请求被发送了多次,如果服务端不处理幂等,用户会被扣两次款,产生两个订单。
  • 业务要求无论请求多少次,最终只扣一次款,只生成一个订单。
  • 核心矛盾:支付成功后需要回调通知、或用户主动查询状态时,系统必须能识别“这是一笔已经处理过的请求”。

Java实现方案

  1. 利用数据库唯一索引
    • 在订单或流水表中,将 order_idtransaction_id 设为唯一键
    • 插入前先尝试插入,如果报 DuplicateKeyException(主键/唯一键冲突),说明已处理。
  2. Redis分布式锁 + 防重Token
    • 前端请求前先获取一个Token(如:pay:token:{userId}:{orderId})。
    • 后端处理时,使用 SET key uuid NX EX 30(只在不存在时设置,30秒过期)。
    • 只有成功获取锁的请求才能继续处理,处理完立即删除锁。
    • 注意:需要结合业务状态(如订单状态已为“已支付”)做最终兜底。

消息队列消费(最易忽略、影响面广)

案例:订单支付成功的回调消息处理

  • 场景:支付服务发送“支付成功”消息到MQ(消息队列),消费者收到后执行发货或积分兑换,由于MQ支持至少一次投递(At Least Once),消息可能重复消费(例如消费者处理完后宕机,消息未ack,MQ重试)。
  • 业务要求同一笔订单的支付成功消息,无论消费多少次,只发一次货,只加一次积分。

Java实现方案

  1. 业务主键去重表
    • 在数据库中创建一张去重表,主键或唯一键为 message_idbusiness_key(如 orderId + eventType)。
    • 消费逻辑:INSERT INTO dedup_table(business_key) VALUES(?),若成功则执行业务逻辑;若失败(唯一键冲突),则直接ack消息并跳过。
  2. Redis Set/布隆过滤器
    • 消费前 SADD dedup:msg:set {messageId},若返回1(新添加)则处理,返回0则忽略,适合高并发且允许小概率误判的场景。

表单提交与接口调用(最常见、用户体验)

案例:创建投票、提交表单、确认发货

  • 场景:用户快速双击“提交”按钮,或前端防抖失效,导致请求被连续发送,虽然前端做了按钮置灰,但总有绕过(如F5刷新、浏览器回退重发)。
  • 业务要求同一用户、同一表单内容,多次提交只产生一条记录。

Java实现方案:Token + 一次消费

  1. 生成Token:后端生成一个唯一的Token(如UUID),存入Redis(Key:submit:token:{userId}:{formId}),并返回给前端。
  2. 提交校验:前端提交时必须携带该Token。
  3. 后端消费:使用 DEL submit:token:{userId}:{formId} 并检查返回值。
    • 若返回1(删除成功),说明是首次请求,执行插入。
    • 若返回0(Key不存在或已删除),说明重复请求,直接返回“处理中”或“已提交”的提示。

外部系统回调通知

案例:第三方支付回调、短信/邮件发送回调

  • 场景:支付宝/微信回调你的 notify_url,它们会重试通知(3-7次),且通知顺序可能乱序。
  • 业务要求同一笔交易的回调,只做一次状态更新(如将订单从未支付改为已支付)。

Java实现方案:CAS(Compare And Set)+ 数据库乐观锁

  1. SQL原生幂等
    UPDATE orders SET status = 'paid', update_time = NOW() WHERE id = ? AND status = 'unpaid';
    • 影响行数 = 1:成功处理,这是第一次。
    • 影响行数 = 0:说明订单状态已经不是 unpaid(已被其他回调处理过),忽略此次回调。
  2. 配合唯一约束:在订单状态变更流水表中,设置 (order_id, target_status) 为唯一联合索引。

文件上传或大对象操作

案例:上传用户头像、上传商品图片

  • 场景:用户上传同一个文件(相同MD5),或上传时网络重试。
  • 业务要求的文件,只存储一份,不浪费存储空间;相同标识的附件,只关联一次。

Java实现方案:内容Hash + 唯一索引

  1. 计算文件MD5:上传完成后计算文件的MD5(或SHA-256)。
  2. 去重存储
    • 在文件存储表(OSS或本地文件系统记录表)中,设置 file_md5 为唯一键。
    • 存储前查询:若存在相同MD5的文件,直接复用原文件路径/ID,不重新上传。
    • 插入时捕获唯一键冲突即可。

定时任务与批量处理

案例:每日报表生成、用户积分结算

  • 场景:凌晨的定时任务,可能因分布式节点重复触发(例如两台服务器同时执行 cron job)。
  • 业务要求同一日期、同一业务范围的报表,只生成一份。

Java实现方案:分布式调度 + 任务标识幂等

  1. 使用Quartz/Elastic-Job的 @DisallowConcurrentExecution 或分片机制:保证同一时间只有一个节点执行。
  2. 数据库任务状态锁
    • 记录一个任务执行记录表,主键为任务名+任务日期(如 report:2023-10-01)。
    • 执行前 INSERT 时,若冲突则说明已执行过,直接跳过。
    • 配合 SELECT ... FOR UPDATE 可精确控制。

选择方案的决策树

业务场景 推荐幂等方案 核心原因
支付扣款 数据库唯一索引 + 状态机乐观锁 强一致性,绝不允许重复扣款
MQ消费 去重表(DB)或Redis Set 处理海量消息,去重代价低
表单提交 Redis Token(一次删除) 高并发,低延迟,性能最优
回调通知 数据库乐观锁(update where status=unpaid) 利用数据库行锁,天然幂等
文件上传 文件MD5唯一索引 业务特征(相同内容)适合哈希去重
定时任务 分布式锁或任务记录表唯一键 跨域一致性要求不高,防止重复生产

关键原则

  1. 优先选择数据库唯一键/乐观锁:这是最基础、最可靠的幂等实现,多数MySQL/PostgreSQL场景都可以兜底。
  2. Redis适合做第一道拦截:性能极高,但在极端宕机或网络分区时可能有漏网之鱼,需要DB做最后的SC(最终一致性)保障。
  3. 接口设计上,天然提供幂等性PUT /order/{id} 天然就是幂等的(多次更新相同内容结果相同),而 POST /order 不是,需要额外处理。

在Java开发中,推荐将幂等逻辑封装成注解 + AOP(如 @Idempotent),通过切面统一实现Token校验或状态判断,避免业务代码中侵入大量判断逻辑。

抱歉,评论功能暂时关闭!