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幂等性解决方案内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Spring Boot 2 实战:自定义启动运行逻辑实例详解

    Spring Boot 2 实战:自定义启动运行逻辑实例详解

    这篇文章主要介绍了Spring Boot 2 实战:自定义启动运行逻辑,结合实例形式详细分析了Spring Boot 2自定义启动运行逻辑详细操作技巧与注意事项,需要的朋友可以参考下
    2020-05-05
  • SpringBoot结合Redis实现缓存

    SpringBoot结合Redis实现缓存

    本文主要介绍了SpringBoot结合Redis实现缓存,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-06-06
  • SpringBoot加载配置6种方式分析

    SpringBoot加载配置6种方式分析

    这篇文章主要介绍了SpringBoot加载配置6种方式分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-10-10
  • Java实现图片对比功能

    Java实现图片对比功能

    个人从来没有研究过图像学,也没看过什么论文或者相关文档,写这个完全是靠google和百度,自己写了个实验了下,测试用例也少,估计有大BUG的存在,所以看的人权当学习交流,切勿生产使用。
    2014-09-09
  • 深入理解hibernate的三种状态

    深入理解hibernate的三种状态

    本篇文章主要介绍了深入理解hibernate的三种状态 ,主要包括了transient(瞬时状态),persistent(持久化状态)以及detached(离线状态),有兴趣的同学可以了解一下
    2017-05-05
  • java实现excel导出合并单元格的步骤详解

    java实现excel导出合并单元格的步骤详解

    这篇文章主要介绍了java实现excel导出合并单元格,通过使用Apache POI库,我们可以方便地创建Excel文件、填充数据、合并单元格和导出Excel文件,需要的朋友可以参考下
    2023-04-04
  • 一文搞懂Java顶层类之Object类的使用

    一文搞懂Java顶层类之Object类的使用

    java.lang.Object类是Java语言中的根类,即所有类的父类。它中描述的所有方法子类都可以使用。本文主要介绍了Object类中toString和equals方法的使用,感兴趣的小伙伴可以了解一下
    2022-11-11
  • java网络编程之socket网络编程示例(服务器端/客户端)

    java网络编程之socket网络编程示例(服务器端/客户端)

    这篇文章主要介绍了java socket网络编程的示例,分为服务器端和客户端,大家参考使用吧
    2014-01-01
  • 如何基于java语言实现八皇后问题

    如何基于java语言实现八皇后问题

    这篇文章主要介绍了如何基于java语言实现八皇后问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-12-12
  • Java多线程中的ThreadPoolExecutor使用解析

    Java多线程中的ThreadPoolExecutor使用解析

    这篇文章主要介绍了Java多线程中的ThreadPoolExecutor使用解析,作为线程池的缓冲,当新增线程超过maximumPoolSize时,会将新增线程暂时存放到该队列中,需要的朋友可以参考下
    2023-12-12

最新评论