Java幂等性的4种解决方案实战讲解(附通俗案例)

 更新时间:2025年07月29日 10:11:15   作者:要阿尔卑斯吗.  
幂等性应该是合格程序员的一个基因,在设计系统时,是首要考虑的问题,尤其是在像支付宝,银行,互联网金融公司等涉及的都是钱的系统,这篇文章主要介绍了Java幂等性的4种解决方案的相关资料,需要的朋友可以参考下

1. 什么是幂等?

幂等性(Idempotence)
在分布式、高并发场景中,同一操作无论执行一次还是执行多次,其对系统的最终影响是一样的。

通俗解释:

你点了个外卖,点了之后系统扣了你账户的钱。网络延迟了,你又点了一次。

如果系统不控制幂等,平台就会给你扣两次钱,要求商家送两份外卖。

所以,后端系统要判断:

这是不是重复请求?是不是已经处理过了?

如果是,直接返回成功;如果不是,才继续处理。

Tips:“幂等性”和 “我想点多份外卖”到底有什么区别?

这两个看起来都可能发送多个请求,但业务语义和系统处理方式完全不同

一句话区别:

概念是重复请求?用户想干嘛?后端该处理几次?
幂等性是(用户误操作或系统重试)用户只想下一个订单只处理一次
我要点多份外卖否(用户有意多次下单)用户想下多个订单处理多次

场景对比解释

幂等性场景(系统要"去重")

点了一次外卖按钮,但由于网络慢,或者手抖点了两下,系统收到了两个一模一样的下单请求

但你心里是想买一份,不是两份。

系统这时必须判断:“这些请求是不是重复的?是不是我们已经处理过了?”
如果是重复的,就不能再扣一次钱,不能再发一份外卖。
这就是幂等性要解决的核心问题:重复请求,只处理一次。

用户主动下多个订单(系统不能去重)

你今天太饿了,就是想点两份外卖:一份给自己吃,一份留着晚上吃。

所以你在 APP 上点了一次 → 下了一单;又点了一次 → 又下一单。

这两次请求虽然是一样的商品、一样的地址,但它们是你主动发出的两个下单请求

系统这时不能把这两次请求当成“重复”来过滤掉。
每次都要扣一次钱,生成一个订单,通知商家送餐。

再简单点说

比喻幂等性多份外卖
行为动机“我点了一次,但系统误以为我点了多次”“我自己就点了两次”
你心里的目标只想买一份就是要买两份
系统正确做法识别重复请求,只处理一次每次都要处理,不能去重

一句话总结

幂等性是系统帮你“防止你不小心多下单”;
而“我要点多份外卖”是你故意多下单,系统必须每次都处理。

2. 实际例子:外卖平台“确认收货”

我们来用“外卖平台确认收货按钮”这个更容易理解的例子:

业务背景:

用户下单外卖,骑手送到之后,点击“确认收货”按钮。

后端的处理逻辑大致如下:

1. 更新订单状态为“已完成”
2. 给骑手发放配送提成
3. 给商家结算费用
4. 给用户发放优惠券

问题来了:

用户点了多次“确认收货”怎么办?或者说用户网络不好点了2次?甚至 App 自动重发了请求怎么办?

如果没处理幂等,就会导致:

  • 订单状态多次更新
  • 骑手提成发多次
  • 商家收到多笔钱
  • 用户领好几张券

→ 系统直接崩了,账全乱了。

3. 数据库设计

我们先准备两张表模拟场景:

-- 用户表
CREATE TABLE t_user (
    id VARCHAR(50) PRIMARY KEY,
    name VARCHAR(50),
    coupon_count INT DEFAULT 0
);

-- 订单表
CREATE TABLE t_order (
    id VARCHAR(50) PRIMARY KEY,
    user_id VARCHAR(50),
    status VARCHAR(20), -- "待收货"、"已完成"
    version BIGINT DEFAULT 0
);

初始化数据:

-- 用户张三,没有优惠券
INSERT INTO t_user VALUES ('u1', '张三', 0);

-- 一笔待收货订单
INSERT INTO t_order VALUES ('o1', 'u1', '待收货', 0);

4. 幂等的四种解决方案实战讲解

方案一:UPDATE带条件字段控制(最常用)

原理:

利用数据库行级锁 + WHERE status = '待收货' 来保证只有一次能成功更新状态。

实现步骤:

@Transactional
public String confirmOrder(String orderId) {
    // 查询订单状态
    Order order = orderMapper.selectById(orderId);
    if ("已完成".equals(order.getStatus())) {
        return "SUCCESS";
    }

    // 核心:只更新待收货的订单
    int rows = orderMapper.updateStatusIfUnfinished(orderId, "已完成", "待收货");

    if (rows == 1) {
        // 发优惠券
        userMapper.addCoupon(order.getUserId(), 1);
        return "SUCCESS";
    } else {
        throw new RuntimeException("系统繁忙,请稍后重试");
    }
}

并发测试:

  • 100 并发同时确认订单

  • 最终结果:

    • 订单状态:已完成
    • 用户优惠券数量:1(不会重复发)

方案二:乐观锁version控制(适用于高并发)

原理:

每次操作都要带上 version 字段,只有匹配时才更新。

实现步骤:

@Transactional(rollbackFor = Exception.class)
public String handleRecharge(String rechargeId) {
    // 根据rechargeId去找充值记录,如果已处理过,则直接返回成功
    RechargePO rechargePo = rechargeMapper.selectById(rechargeId);
    if (rechargePo == null) {
        throw new IllegalArgumentException("充值记录不存在");
    }

    // 充值记录已处理过,直接返回成功
    if (rechargePo.getStatus() == 1) {
        return "SUCCESS";
    }

    // 开启Spring事务(由 @Transactional 控制)

    // 在where后面要加 status = 0 这个条件;count表示影响行数
    int count = rechargeMapper.updateStatusToProcessed(rechargeId, 0, 1);

    // count = 1,表示上面sql执行成功
    if (count != 1) {
        // 走到这里,说明有并发,直接抛出异常
        throw new RuntimeException("系统繁忙,请重试");
    } else {
        // 给账户加钱
        accountMapper.increaseBalance(rechargePo.getAccountId(), rechargePo.getPrice());
    }

    // 提交Spring事务(由 @Transactional 控制)

    return "SUCCESS";
}

并发测试:

  • 最终结果:

    • 状态:已完成
    • 优惠券:1

方案三:唯一约束表控制幂等(通用方案)

原理:

在操作前插入一条带有唯一索引的幂等 key,插入失败说明是重复请求。

新增辅助表:

CREATE TABLE t_idempotent (
    id VARCHAR(50) PRIMARY KEY,
    idempotent_key VARCHAR(100) NOT NULL,
    UNIQUE KEY uq_idempotent_key (idempotent_key)
);

实现步骤:

@Transactional
public String confirmOrderWithUniqueKey(String orderId) {
    String idempotentKey = "order_confirm:" + orderId;

    try {
        idempotentMapper.insert(idempotentKey);
    } catch (DuplicateKeyException e) {
        return "SUCCESS";
    }

    Order order = orderMapper.selectById(orderId);
    if ("已完成".equals(order.getStatus())) return "SUCCESS";

    orderMapper.updateStatus(orderId, "已完成");
    userMapper.addCoupon(order.getUserId(), 1);
    return "SUCCESS";
}

并发测试:

  • 最终结果:

    • 插入 t_idempotent 成功 1 次
    • 优惠券发放一次

方案四:分布式锁(适合跨服务或非数据库操作)

原理:

利用 Redis 锁来控制并发,只允许一个线程进入。

实现步骤(伪代码):

public String confirmOrderWithRedisLock(String orderId) {
    String lockKey = "lock:order:confirm:" + orderId;

    if (!redisLock.tryLock(lockKey, 5秒)) {
        throw new RuntimeException("系统繁忙,请稍后重试");
    }

    try {
        // 幂等处理逻辑
        ...
    } finally {
        redisLock.release(lockKey);
    }
}

并发测试:

  • Redis 锁只允许 1 个线程进入
  • 其余请求返回“系统繁忙”,防止重复处理

5. 四种方案对比总结

方案是否通用可靠性成本
方案1:UPDATE + 条件仅 DB 操作
方案2:乐观锁 version仅 DB 操作
方案3:唯一约束表通用
方案4:Redis 锁非 DB 操作场景较高

6. 实战建议 & 常见套路

  • 接口幂等性建议统一封装,如方案3可以做成注解 + 拦截器模式
  • 支付回调、确认订单、接口重试场景、MQ消费 等业务必须加幂等控制
  • 数据库层幂等优先,跨服务幂等使用分布式锁或唯一幂等表

总结 

到此这篇关于Java幂等性的4种解决方案的文章就介绍到这了,更多相关Java幂等性解决方案内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java IO流体系继承结构图_动力节点Java学院整理

    Java IO流体系继承结构图_动力节点Java学院整理

    这篇文章主要介绍了Java IO流体系继承结构图,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2017-05-05
  • SpringBoot中分页插件PageHelper的使用详解

    SpringBoot中分页插件PageHelper的使用详解

    分页查询是为了高效展示大量数据,通过分页将数据划分为多个部分逐页展示,原生方法需手动计算数据起始行,而使用PageHelper插件则简化这一过程,本文给大家介绍SpringBoot中分页插件PageHelper的使用,感兴趣的朋友一起看看吧
    2024-09-09
  • Java动态循环队列是如何实现的

    Java动态循环队列是如何实现的

    今天带大家学习java队列的相关知识,文章围绕着如何实现Java动态循环队列展开,文中有非常详细的介绍及代码示例,需要的朋友可以参考下
    2021-06-06
  • Java Atomic类及线程同步新机制原理解析

    Java Atomic类及线程同步新机制原理解析

    这篇文章主要介绍了Java Atomic类及线程同步新机制原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-07-07
  • java中反射Reflection的4个作用详解

    java中反射Reflection的4个作用详解

    反射Reflection是Java等编程语言中的一个重要特性,它允许程序在运行时进行自我检查和对内部成员(如字段、方法、类等)的操作,本文将详细介绍反射的主要作用,并通过Java示例来说明,感兴趣的朋友跟随小编一起看看吧
    2025-07-07
  • SpringBoot重启后,第一次请求接口请求慢的问题及解决

    SpringBoot重启后,第一次请求接口请求慢的问题及解决

    这篇文章主要介绍了SpringBoot重启后,第一次请求接口请求慢的问题及解决,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-05-05
  • Mybatis操作数据时出现:java.sql.SQLSyntaxErrorException: Unknown column 'XXX' in 'field list'的问题解决

    Mybatis操作数据时出现:java.sql.SQLSyntaxErrorException: Unknown c

    这篇文章主要介绍了Mybatis操作数据时出现:java.sql.SQLSyntaxErrorException: Unknown column 'XXX' in 'field list',需要的朋友可以参考下
    2023-04-04
  • 生产环境jvm常用的参数设置建议分享

    生产环境jvm常用的参数设置建议分享

    在Java应用程序的部署过程中,合理配置JVM(Java虚拟机)参数对于提升应用性能、稳定性和资源利用效率至关重要,本文将探讨一些常用的JVM参数设置建议,帮助开发者在生产环境中优化Java应用,需要的朋友可以参考下
    2025-04-04
  • Java中的ThreadLocalMap源码解读

    Java中的ThreadLocalMap源码解读

    这篇文章主要介绍了Java中的ThreadLocalMap源码解读,ThreadLocalMap是ThreadLocal的内部类,是一个key-value数据形式结构,也是ThreadLocal的核心,需要的朋友可以参考下
    2023-09-09
  • IDEA 热部署设置(JRebel插件激活)

    IDEA 热部署设置(JRebel插件激活)

    这篇文章主要介绍了IDEA 热部署设置(JRebel插件激活),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-08-08

最新评论