别再乱用了!幂等处理与分布式锁,90% 开发者都踩过的坑与正确落地姿势

在分布式系统开发中,幂等处理和分布式锁是两个绕不开的核心技术点。但我见过太多开发者,要么把两者混为一谈,用分布式锁去实现幂等,最终导致线上资损;要么盲目叠加两者,给系统增加不必要的复杂度和性能开销;更有甚者,在核心交易链路只做了其中一项,最终引发超卖、重复支付、数据错乱等严重线上故障。


一、底层本质:先搞懂你要解决的到底是什么问题

很多人用错的根源,是从一开始就没搞懂这两个技术的核心定位,它们解决的是分布式系统中两个完全不同维度的风险。

1.1 核心定义拆解

幂等处理

幂等处理的本质,是解决时间维度上的重复执行问题。它的核心承诺是:同一个操作,无论被执行1次还是N次,最终产生的业务结果完全一致,不会出现额外的副作用。

核心关键词:重复执行、结果唯一性、无额外副作用。 通俗类比:你用ATM机转账1000元,由于网络卡顿连续提交了两次请求。幂等处理会保证,无论你提交多少次,你的账户只会被扣1000元,收款人只会收到1000元。

分布式锁

分布式锁的本质,是解决同一时刻的并发竞争问题。它的核心承诺是:在分布式环境下,同一时刻,只有一个客户端/线程,能对指定的共享资源执行操作。

核心关键词:并发执行、互斥性、资源独占性。 通俗类比:银行窗口只有一个业务员,同时来了10个客户要办业务。分布式锁就是叫号机,保证同一时刻只有一个客户能在窗口办理业务,其余客户只能排队等待。

1.2 相似点与核心区别

表象相似点

很多人会把两者混为一谈,核心是因为它们有三个共性特征:

  1. 终极目标一致:都是为了保证分布式系统下的数据正确性和一致性,防止脏数据产生。
  2. 实现工具重叠:Redis、MySQL、ZooKeeper等中间件,既可以用来实现分布式锁,也可以作为幂等处理的存储介质。
  3. 适用场景有交集:在高并发核心交易链路中,两者经常需要配合使用,这也是很多人误以为两者必须绑定的原因。

核心本质区别

对比维度 幂等处理 分布式锁
核心解决问题 时间维度的重复执行(请求先后发生,时间上错开) 空间维度的并发竞争(请求同一时刻发生,并行执行)
核心关注点 操作的最终结果,无论执行多少次,结果唯一 操作的执行过程,同一时间只允许一个执行主体操作
核心特性 结果唯一性、重试友好性 互斥性、串行化、资源独占性
业务侵入性 业务逻辑的核心组成部分,与业务规则强绑定 独立的并发协调机制,与业务逻辑解耦
失败处理策略 天然支持重试,重试是其设计的核心场景 不鼓励盲目重试,抢锁失败通常代表资源正在被占用
性能开销 较低,通常为1次查询/校验操作,无锁等待开销 相对较高,包含加锁、锁等待、解锁、锁超时兜底等全链路开销
兜底能力 业务正确性的最终兜底防线 并发场景下的前置防护手段,无法作为最终兜底

1.3 最核心的认知误区纠正

用分布式锁无法实现幂等,这是90%开发者都踩过的致命坑。

分布式锁的生命周期,是单次请求的执行周期。它只能保证,在锁持有期间,没有其他请求能同时执行这段业务逻辑。但当第一个请求执行完成、释放锁之后,第二个重复的请求,完全可以再次拿到锁,重新执行一遍完整的业务逻辑。

典型场景:用户支付订单,连续发起了两次支付请求,两次请求间隔200ms。第一次请求拿到锁,执行扣款耗时100ms,执行完成释放锁。200ms后第二个请求到达,顺利拿到锁,再次执行扣款逻辑。最终结果就是用户被扣了两次钱,资损就此发生。

分布式锁根本就不是用来解决重复执行问题的,它只能解决并发竞争,这一点必须刻在骨子里。


二、场景化落地:什么时候只用一个,什么时候必须联用?

技术选型的核心,是精准匹配场景的核心风险。下面我们分三类场景,讲透不同场景下的正确选型逻辑。

2.1 只用幂等处理就足够的场景

这类场景的核心共性:主要矛盾是「重复执行/重试导致的业务副作用」,不存在高频并发竞争,或者并发冲突概率极低,幂等机制可以完全兜底。此时加分布式锁,只会徒增系统RT、复杂度和故障风险,完全没有必要。

典型场景1:MQ消息消费去重

核心风险:MQ的重试机制(网络抖动、消费超时、服务重启)会导致同一条消息被重复投递,而主流MQ(RocketMQ、Kafka、RabbitMQ)的队列模型,天然保证了同一条消息同一时间只会被投递给一个消费者,不存在并发竞争问题。

正确落地:用消息的唯一MessageId,或者业务唯一键(如订单号)做去重校验,消费前先判断是否已经消费过,已消费则直接返回ACK,无需加锁。

补充:只有当消费逻辑涉及共享资源的并发修改(如批量扣减多个商品的库存),才需要额外的并发控制,普通的通知类、数据同步类、报表统计类消息,纯幂等完全足够。

典型场景2:前端非交易类重复提交防护

核心风险:用户连续点击按钮、网络重试导致的重复表单提交,而非高并发的资源竞争。典型场景:用户提交反馈、修改个人信息、发布文章/评论、申请非资金类服务等。

正确落地:前端按钮置灰+后端「请求唯一令牌Token」/业务唯一号去重,即可完全解决问题。极端情况下,数据库唯一索引会直接拦截重复写入,其开销远小于分布式锁的全链路开销。

典型场景3:天然幂等的操作

核心特征:操作本身执行1次和N次的结果完全一致,无任何业务副作用,连额外的幂等处理都无需做,更不需要分布式锁。

典型例子:

  • 所有GET类的查询接口,不修改任何数据,天然幂等;
  • 固定值覆盖更新:UPDATE user SET name = 'xxx' WHERE id = 1,无论执行多少次,最终name的值都是固定的;
  • 按唯一条件的删除操作:DELETE FROM order WHERE order_no = 'xxx',无论执行多少次,最终的结果都是这条订单被删除,无额外副作用。

典型场景4:低并发定时任务的重复执行防护

核心风险:定时任务重试、多实例误触发导致的重复执行,而非集群高并发抢占。典型场景:每日凌晨的对账数据归档、用户账单生成、非核心数据同步任务。

正确落地:记录任务执行日期+执行状态,只要当日任务已执行成功,再次触发直接返回,纯幂等即可完全兜底,无需分布式锁。

2.2 只用分布式锁就足够的场景

这类场景的核心共性:主要矛盾是「同一时刻的并发抢占共享资源」,不存在重复执行的业务风险,或者操作本身天然幂等,重复执行无任何副作用。此时无需额外做幂等处理,分布式锁已经完全解决了核心问题。

典型场景1:缓存击穿防护

核心风险:热点Key过期时,大量并发请求同时穿透到数据库,导致数据库压力骤增,甚至宕机。这里的核心问题是并发,而非重复执行的副作用。

正确落地:用分布式锁保证,同一时间只有1个请求去查询数据库并刷新缓存,其余请求等待缓存刷新完成,或降级返回默认值。

底层逻辑:就算多个请求先后刷新缓存,最终缓存中的值都是完全一致的,重复刷新只会浪费极少量的数据库资源,无任何业务副作用,锁的互斥性已经解决了核心的缓存击穿问题。

典型场景2:分布式集群主节点选举/任务抢占

核心风险:集群多实例同时抢主节点身份、抢任务执行权,必须保证同一时间只有1个实例承担核心职责,避免重复调度、数据错乱。典型场景:分布式定时任务的集群抢占、大数据任务的资源分片、主从架构的节点选举。

正确落地:用分布式锁实现抢占机制,只有抢到锁的实例,才能成为主节点,执行核心任务。

底层逻辑:主节点选举、任务抢占这类操作,重复执行的结果要么是维持原主节点,要么是正常的主备切换,无任何不可逆的业务副作用,锁的互斥性已经完全覆盖了核心风险。

典型场景3:分布式限流的令牌桶并发控制

核心风险:高并发下,多个请求同时扣减限流令牌,导致限流规则失效,无法精准控制QPS。这里的核心问题是并发修改共享的令牌数量,而非重复执行的副作用。

正确落地:用分布式锁(或Redis原子操作)保证令牌扣减的原子性,同一时间只有1个请求能修改令牌数量,确保限流精度。

底层逻辑:每个请求对应一次独立的令牌扣减,重复请求本身就是独立的流量,重复扣减完全符合限流规则,无业务副作用。

典型场景4:分布式环境下的排他性资源占用

核心风险:多个实例同时占用同一排他性资源,导致资源冲突、操作失败。典型场景:分布式环境下的端口占用、独占文件写入、硬件设备的独占访问。

正确落地:用分布式锁保证,同一时间只有1个实例能占用该资源,其余实例只能等待或降级。

底层逻辑:资源一旦被占用,其他实例就算重复抢锁也只会失败,不会产生任何业务副作用,锁的互斥性完全覆盖了核心风险。

2.3 必须两者联用的场景

这类场景的核心共性:同时存在高并发共享资源竞争 + 重复执行风险,且操作本身有不可逆的累积效应,会产生严重的业务副作用。此时缺了任何一个,都会导致数据错乱、资损等严重线上故障。

典型场景1:核心交易链路(订单支付、扣款、退款、提现)

风险双叠加:

  • 重复执行风险:用户连续点击支付、支付网关重试、MQ消息重发、分布式事务重试,都会导致重复的支付请求。只用分布式锁的话,第一个请求执行完释放锁后,第二个重复请求仍能拿到锁,再次执行扣款,导致用户被扣两次钱。
  • 并发竞争风险:高并发下,多个请求同时处理同一笔订单,只用幂等处理的话,会出现竞态问题。两个请求同时查询到订单状态为「待支付」,同时进入扣款流程,最终导致重复支付、账户余额超扣。

正确落地:用分布式锁解决并发竞态问题,保证同一时间只有1个请求能处理该订单;用幂等处理做最终兜底,保证就算锁失效、请求重试,也不会重复执行扣款。

典型场景2:库存扣减、商品秒杀、优惠券核销

风险双叠加:

  • 重复执行风险:用户重复下单、MQ重试导致重复扣减库存、重复核销优惠券,只用分布式锁无法防止时间错开的重复扣减。
  • 并发竞争风险:秒杀场景下,数万请求同时抢同一商品的库存,只用幂等处理会出现超卖。多个请求同时查到库存充足,同时执行扣减,最终导致库存为负。

正确落地:用分布式锁保证同一时间只有1个请求能扣减该商品的库存,解决并发超卖问题;用订单号做库存扣减的唯一凭证,保证同一订单不会重复扣减库存,做最终兜底。

典型场景3:分布式事务(TCC/SAGA/AT模式)

风险双叠加:

  • 重复执行风险:分布式事务的网络抖动、超时重试,会导致Confirm/Cancel/补偿阶段被重复调用,必须保证幂等,否则会出现重复提交、重复回滚。
  • 并发竞争风险:多个分支事务同时操作同一笔资源(如同一账户的余额、同一商品的库存),必须用分布式锁保证操作的原子性,防止数据错乱。

行业规范:在分布式事务的实现规范中,幂等性是硬性要求,而分布式锁是并发场景下的必要保障,两者缺一不可。

典型场景4:高并发下的状态机流转场景

典型场景:订单状态流转(待支付→已支付→已发货→已完成)、理赔单审核流转、合同审批流程。

风险双叠加:

  • 重复执行风险:重复请求会导致状态被重复流转,比如已完成的订单再次触发发货逻辑。
  • 并发竞争风险:多个请求同时修改同一订单的状态,导致状态流转错乱,比如已取消的订单被标记为已支付。

正确落地:用分布式锁保证同一时间只有1个请求能修改订单状态,防止并发状态覆盖;用状态机的前置校验实现幂等,保证状态只能正向流转一次,重复请求直接拦截。


三、代码实战

3.1 环境依赖配置

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.4</version>
        <relativePath/>
    </parent>
    <groupId>com.jam</groupId>
    <artifactId>idempotent-lock-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>idempotent-lock-demo</name>
    <properties>
        <java.version>17</java.version>
        <mybatis-plus.version>3.5.6</mybatis-plus.version>
        <redisson.version>3.27.0</redisson.version>
        <fastjson2.version>2.0.49</fastjson2.version>
        <guava.version>33.1.0-jre</guava.version>
        <springdoc.version>2.5.0</springdoc.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>${redisson.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>${fastjson2.version}</version>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${guava.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>${springdoc.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.32</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

3.2 数据库表结构(MySQL 8.0)

sql 复制代码
CREATE TABLE `t_order_info` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `order_no` varchar(64) NOT NULL COMMENT '订单号',
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `sku_id` bigint NOT NULL COMMENT '商品SKU ID',
  `buy_num` int NOT NULL DEFAULT '1' COMMENT '购买数量',
  `order_amount` decimal(10,2) NOT NULL COMMENT '订单金额',
  `order_status` tinyint NOT NULL DEFAULT '0' COMMENT '订单状态:0-待支付,1-已支付,2-已发货,3-已完成,4-已取消',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_no` (`order_no`),
  KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单表';

CREATE TABLE `t_sku_stock` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `sku_id` bigint NOT NULL COMMENT '商品SKU ID',
  `stock_num` int NOT NULL DEFAULT '0' COMMENT '库存数量',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_sku_id` (`sku_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品库存表';

CREATE TABLE `t_idempotent_record` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `unique_key` varchar(128) NOT NULL COMMENT '幂等唯一键',
  `business_type` varchar(32) NOT NULL COMMENT '业务类型',
  `request_info` text COMMENT '请求信息',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_unique_key` (`unique_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='幂等去重表';

3.3 核心实体类

kotlin 复制代码
package com.jam.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 订单实体类
 * @author ken
 */
@Data
@TableName("t_order_info")
public class OrderInfo {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String orderNo;
    private Long userId;
    private Long skuId;
    private Integer buyNum;
    private BigDecimal orderAmount;
    private Integer orderStatus;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}
kotlin 复制代码
package com.jam.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * 商品库存实体类
 * @author ken
 */
@Data
@TableName("t_sku_stock")
public class SkuStock {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long skuId;
    private Integer stockNum;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}
kotlin 复制代码
package com.jam.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * 幂等去重记录实体类
 * @author ken
 */
@Data
@TableName("t_idempotent_record")
public class IdempotentRecord {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String uniqueKey;
    private String businessType;
    private String requestInfo;
    private LocalDateTime createTime;
}

3.4 Mapper层

java 复制代码
package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.OrderInfo;
import org.apache.ibatis.annotations.Mapper;

/**
 * 订单Mapper
 * @author ken
 */
@Mapper
public interface OrderInfoMapper extends BaseMapper<OrderInfo> {
}
less 复制代码
package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.SkuStock;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;

/**
 * 商品库存Mapper
 * @author ken
 */
@Mapper
public interface SkuStockMapper extends BaseMapper<SkuStock> {

    /**
     * 扣减库存
     * @param skuId 商品SKU ID
     * @param num 扣减数量
     * @return 影响行数
     */
    @Update("UPDATE t_sku_stock SET stock_num = stock_num - #{num} WHERE sku_id = #{skuId} AND stock_num >= #{num}")
    int deductStock(@Param("skuId") Long skuId, @Param("num") Integer num);
}
java 复制代码
package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.IdempotentRecord;
import org.apache.ibatis.annotations.Mapper;

/**
 * 幂等记录Mapper
 * @author ken
 */
@Mapper
public interface IdempotentRecordMapper extends BaseMapper<IdempotentRecord> {
}

3.5 幂等处理通用实现

typescript 复制代码
package com.jam.demo.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jam.demo.entity.IdempotentRecord;
import com.jam.demo.mapper.IdempotentRecordMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
 * 幂等处理服务
 * @author ken
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class IdempotentService {

    private final IdempotentRecordMapper idempotentRecordMapper;

    /**
     * 幂等性校验
     * @param uniqueKey 业务唯一键
     * @param businessType 业务类型
     * @param requestInfo 请求信息
     * @return true-校验通过(首次请求),false-校验不通过(重复请求)
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean checkIdempotent(String uniqueKey, String businessType, String requestInfo) {
        if (!StringUtils.hasText(uniqueKey) || !StringUtils.hasText(businessType)) {
            throw new IllegalArgumentException("幂等唯一键和业务类型不能为空");
        }

        LambdaQueryWrapper<IdempotentRecord> queryWrapper = new LambdaQueryWrapper<IdempotentRecord>()
                .eq(IdempotentRecord::getUniqueKey, uniqueKey)
                .eq(IdempotentRecord::getBusinessType, businessType);
        IdempotentRecord existRecord = idempotentRecordMapper.selectOne(queryWrapper);
        if (!ObjectUtils.isEmpty(existRecord)) {
            log.warn("重复请求,uniqueKey:{}, businessType:{}", uniqueKey, businessType);
            return false;
        }

        try {
            IdempotentRecord record = new IdempotentRecord();
            record.setUniqueKey(uniqueKey);
            record.setBusinessType(businessType);
            record.setRequestInfo(requestInfo);
            idempotentRecordMapper.insert(record);
            return true;
        } catch (DuplicateKeyException e) {
            log.warn("唯一索引拦截重复请求,uniqueKey:{}, businessType:{}", uniqueKey, businessType);
            return false;
        }
    }

    /**
     * 删除幂等记录(用于业务执行失败时回滚)
     * @param uniqueKey 业务唯一键
     * @param businessType 业务类型
     */
    @Transactional(rollbackFor = Exception.class)
    public void deleteIdempotentRecord(String uniqueKey, String businessType) {
        if (!StringUtils.hasText(uniqueKey) || !StringUtils.hasText(businessType)) {
            return;
        }
        LambdaQueryWrapper<IdempotentRecord> queryWrapper = new LambdaQueryWrapper<IdempotentRecord>()
                .eq(IdempotentRecord::getUniqueKey, uniqueKey)
                .eq(IdempotentRecord::getBusinessType, businessType);
        idempotentRecordMapper.delete(queryWrapper);
    }
}

3.6 分布式锁通用实现

java 复制代码
package com.jam.demo.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

/**
 * 分布式锁服务
 * @author ken
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class DistributedLockService {

    private final RedissonClient redissonClient;

    private static final long DEFAULT_WAIT_TIME = 3L;
    private static final long DEFAULT_LEASE_TIME = 30L;
    private static final TimeUnit DEFAULT_TIME_UNIT = TimeUnit.SECONDS;

    /**
     * 加锁执行业务(无返回值)
     * @param lockKey 锁的key
     * @param business 要执行的业务逻辑
     */
    public void lockAndRun(String lockKey, Runnable business) {
        lockAndRun(lockKey, DEFAULT_WAIT_TIME, DEFAULT_LEASE_TIME, DEFAULT_TIME_UNIT, business);
    }

    /**
     * 加锁执行业务(自定义超时时间,无返回值)
     * @param lockKey 锁的key
     * @param waitTime 等待获取锁的最大时间
     * @param leaseTime 锁的持有时间
     * @param timeUnit 时间单位
     * @param business 要执行的业务逻辑
     */
    public void lockAndRun(String lockKey, long waitTime, long leaseTime, TimeUnit timeUnit, Runnable business) {
        if (!StringUtils.hasText(lockKey)) {
            throw new IllegalArgumentException("锁的key不能为空");
        }
        if (ObjectUtils.isEmpty(business)) {
            throw new IllegalArgumentException("业务逻辑不能为空");
        }

        RLock lock = redissonClient.getLock(lockKey);
        boolean lockAcquired = false;
        try {
            lockAcquired = lock.tryLock(waitTime, leaseTime, timeUnit);
            if (!lockAcquired) {
                log.warn("获取分布式锁失败,lockKey:{}", lockKey);
                throw new RuntimeException("系统繁忙,请稍后再试");
            }
            business.run();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("获取分布式锁被中断,lockKey:{}", lockKey, e);
            throw new RuntimeException("系统繁忙,请稍后再试");
        } finally {
            if (lockAcquired && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    /**
     * 加锁执行业务(有返回值)
     * @param lockKey 锁的key
     * @param business 要执行的业务逻辑
     * @return 业务执行结果
     * @param <T> 返回值类型
     */
    public <T> T lockAndGet(String lockKey, Supplier<T> business) {
        return lockAndGet(lockKey, DEFAULT_WAIT_TIME, DEFAULT_LEASE_TIME, DEFAULT_TIME_UNIT, business);
    }

    /**
     * 加锁执行业务(自定义超时时间,有返回值)
     * @param lockKey 锁的key
     * @param waitTime 等待获取锁的最大时间
     * @param leaseTime 锁的持有时间
     * @param timeUnit 时间单位
     * @param business 要执行的业务逻辑
     * @return 业务执行结果
     * @param <T> 返回值类型
     */
    public <T> T lockAndGet(String lockKey, long waitTime, long leaseTime, TimeUnit timeUnit, Supplier<T> business) {
        if (!StringUtils.hasText(lockKey)) {
            throw new IllegalArgumentException("锁的key不能为空");
        }
        if (ObjectUtils.isEmpty(business)) {
            throw new IllegalArgumentException("业务逻辑不能为空");
        }

        RLock lock = redissonClient.getLock(lockKey);
        boolean lockAcquired = false;
        try {
            lockAcquired = lock.tryLock(waitTime, leaseTime, timeUnit);
            if (!lockAcquired) {
                log.warn("获取分布式锁失败,lockKey:{}", lockKey);
                throw new RuntimeException("系统繁忙,请稍后再试");
            }
            return business.get();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("获取分布式锁被中断,lockKey:{}", lockKey, e);
            throw new RuntimeException("系统繁忙,请稍后再试");
        } finally {
            if (lockAcquired && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

3.7 两者联用的核心业务实现

java 复制代码
package com.jam.demo.service;

import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jam.demo.entity.OrderInfo;
import com.jam.demo.entity.SkuStock;
import com.jam.demo.mapper.OrderInfoMapper;
import com.jam.demo.mapper.SkuStockMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.ObjectUtils;

import java.math.BigDecimal;
import java.util.UUID;

/**
 * 订单支付服务
 * @author ken
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderPayService {

    private final OrderInfoMapper orderInfoMapper;
    private final SkuStockMapper skuStockMapper;
    private final IdempotentService idempotentService;
    private final DistributedLockService distributedLockService;
    private final TransactionTemplate transactionTemplate;

    private static final String BUSINESS_TYPE_ORDER_PAY = "ORDER_PAY";
    private static final String LOCK_KEY_PREFIX_ORDER = "order:pay:";
    private static final String LOCK_KEY_PREFIX_STOCK = "stock:deduct:";

    /**
     * 创建订单
     * @param userId 用户ID
     * @param skuId 商品SKU ID
     * @param buyNum 购买数量
     * @return 订单号
     */
    public String createOrder(Long userId, Long skuId, Integer buyNum) {
        if (ObjectUtils.isEmpty(userId) || ObjectUtils.isEmpty(skuId) || ObjectUtils.isEmpty(buyNum) || buyNum <= 0) {
            throw new IllegalArgumentException("参数异常");
        }

        SkuStock skuStock = skuStockMapper.selectOne(new LambdaQueryWrapper<SkuStock>().eq(SkuStock::getSkuId, skuId));
        if (ObjectUtils.isEmpty(skuStock) || skuStock.getStockNum() < buyNum) {
            throw new RuntimeException("商品库存不足");
        }

        String orderNo = UUID.randomUUID().toString().replace("-", "");
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setOrderNo(orderNo);
        orderInfo.setUserId(userId);
        orderInfo.setSkuId(skuId);
        orderInfo.setBuyNum(buyNum);
        orderInfo.setOrderAmount(new BigDecimal("100.00").multiply(new BigDecimal(buyNum)));
        orderInfo.setOrderStatus(0);
        orderInfoMapper.insert(orderInfo);

        return orderNo;
    }

    /**
     * 订单支付(幂等+分布式锁联用核心实现)
     * @param orderNo 订单号
     * @return 支付结果
     */
    public Boolean payOrder(String orderNo) {
        // 1. 幂等性前置校验,拦截重复请求
        boolean idempotentPass = idempotentService.checkIdempotent(orderNo, BUSINESS_TYPE_ORDER_PAY, JSON.toJSONString(orderNo));
        if (!idempotentPass) {
            log.info("订单重复支付,直接返回成功,orderNo:{}", orderNo);
            return Boolean.TRUE;
        }

        // 2. 分布式锁,解决并发竞争问题,锁粒度为订单号,保证同一时间只有一个请求处理该订单
        String lockKey = LOCK_KEY_PREFIX_ORDER + orderNo;
        return distributedLockService.lockAndGet(lockKey, () -> {
            // 3. 编程式事务,保证库存扣减和订单状态更新的原子性
            return transactionTemplate.execute(status -> {
                try {
                    // 4. 订单状态二次校验,防止并发修改
                    OrderInfo orderInfo = orderInfoMapper.selectOne(new LambdaQueryWrapper<OrderInfo>().eq(OrderInfo::getOrderNo, orderNo));
                    if (ObjectUtils.isEmpty(orderInfo)) {
                        throw new RuntimeException("订单不存在");
                    }
                    if (orderInfo.getOrderStatus() != 0) {
                        log.warn("订单状态异常,无法支付,orderNo:{}, orderStatus:{}", orderNo, orderInfo.getOrderStatus());
                        return Boolean.TRUE;
                    }

                    // 5. 库存扣减加锁,解决高并发超卖问题
                    String stockLockKey = LOCK_KEY_PREFIX_STOCK + orderInfo.getSkuId();
                    int deductResult = distributedLockService.lockAndGet(stockLockKey, () ->
                            skuStockMapper.deductStock(orderInfo.getSkuId(), orderInfo.getBuyNum())
                    );
                    if (deductResult <= 0) {
                        throw new RuntimeException("商品库存不足,支付失败");
                    }

                    // 6. 更新订单状态为已支付
                    orderInfo.setOrderStatus(1);
                    orderInfoMapper.updateById(orderInfo);

                    log.info("订单支付成功,orderNo:{}", orderNo);
                    return Boolean.TRUE;
                } catch (Exception e) {
                    // 7. 业务执行失败,回滚事务,删除幂等记录,允许后续重试
                    status.setRollbackOnly();
                    idempotentService.deleteIdempotentRecord(orderNo, BUSINESS_TYPE_ORDER_PAY);
                    log.error("订单支付失败,orderNo:{}", orderNo, e);
                    throw new RuntimeException(e.getMessage(), e);
                }
            });
        });
    }
}

3.8 接口层实现

less 复制代码
package com.jam.demo.controller;

import com.jam.demo.service.OrderPayService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

/**
 * 订单支付接口
 * @author ken
 */
@RestController
@RequestMapping("/order")
@RequiredArgsConstructor
@Tag(name = "订单管理", description = "订单创建与支付接口")
public class OrderController {

    private final OrderPayService orderPayService;

    @PostMapping("/create")
    @Operation(summary = "创建订单", description = "创建商品订单,返回订单号")
    public ResponseEntity<String> createOrder(
            @Parameter(description = "用户ID", required = true) @RequestParam Long userId,
            @Parameter(description = "商品SKU ID", required = true) @RequestParam Long skuId,
            @Parameter(description = "购买数量", required = true) @RequestParam Integer buyNum) {
        String orderNo = orderPayService.createOrder(userId, skuId, buyNum);
        return ResponseEntity.ok(orderNo);
    }

    @PostMapping("/pay/{orderNo}")
    @Operation(summary = "订单支付", description = "订单支付接口,包含幂等处理与分布式锁控制")
    public ResponseEntity<Boolean> payOrder(
            @Parameter(description = "订单号", required = true) @PathVariable String orderNo) {
        Boolean result = orderPayService.payOrder(orderNo);
        return ResponseEntity.ok(result);
    }
}

四、高频踩坑指南

4.1 坑1:用分布式锁实现幂等

错误原因:锁的生命周期是单次请求,无法解决时间错开的重复请求,最终导致资损。 正确做法:幂等是业务正确性的兜底,必须单独实现,锁只能解决并发问题。

4.2 坑2:有了幂等处理,就不需要分布式锁

错误原因:纯业务幂等的「先查询,再判断,再写入」,不是原子操作,高并发下会出现竞态问题。比如两个请求同时查询到订单待支付,同时进入扣款流程,最终导致重复支付。 正确做法:高并发共享资源修改场景,必须在幂等的基础上,叠加分布式锁,或者用数据库的唯一索引+行锁来实现原子性的幂等校验。

4.3 坑3:分布式锁超时释放,导致业务逻辑还没执行完,锁就没了

错误原因:自己实现的Redis锁,没有锁续期机制,业务执行时间超过锁超时时间,锁被自动释放,导致多个请求同时进入临界区。 正确做法:生产环境优先使用Redisson等成熟的分布式锁框架,自带看门狗机制,会自动续期,避免锁提前释放。

4.4 坑4:幂等去重表没有设置唯一索引,导致重复数据

错误原因:只在业务代码里做了去重查询,没有在数据库表中给唯一键设置唯一索引,高并发下还是会插入重复数据。 正确做法:幂等去重表,必须给业务唯一键设置唯一索引,用数据库的底层约束做最终兜底。

4.5 坑5:分布式锁的粒度太粗,导致系统性能急剧下降

错误原因:比如秒杀场景,用商品ID做锁Key,导致所有抢该商品的请求都串行执行,QPS上不去。 正确做法:锁的粒度要尽可能细,比如库存扣减可以用分段锁,把库存分成多段,每段一个锁,大幅提升并发性能。


五、最佳实践决策指南


总结

在分布式系统开发中,没有万能的银弹,只有适合场景的正确方案。幂等处理和分布式锁,一个是业务正确性的最终兜底防线,一个是并发场景下的前置防护手段,两者各司其职,互为补充,绝不能相互替代,也不是必须绑定使用。

作为开发者,我们最核心的能力,不是会用多少框架和API,而是能精准定位业务场景的核心风险,选择最适合的技术方案,用最低的成本,解决最核心的问题。希望本文能帮你彻底理清两者的底层逻辑,避开高频踩坑点,写出更健壮、更高性能的分布式系统代码。

相关推荐
ALex_zry2 小时前
现代C++设计模式实战:从AIDC项目看工业级代码架构
c++·设计模式·架构
CoovallyAIHub2 小时前
AAAI 2026 | AnoStyler:文本驱动风格迁移实现零样本异常图像生成,轻量高效(附代码)
算法·架构·github
CoovallyAIHub2 小时前
500M参数就能跑视觉语言模型?Moondream把VLM塞进了边缘设备
算法·架构·github
heimeiyingwang2 小时前
【架构实战】数据库分库分表实战
数据库·架构
一水鉴天2 小时前
智能代理体系 之2 20260325 (腾讯元宝)
人工智能·重构·架构·自动化
梦里花开知多少3 小时前
浅谈SDK设计
架构
东芝、铠侠总代136100683933 小时前
从混合存储架构看SSD与HDD的互补性:技术特性决定应用场景
服务器·架构·ssd·hdd
梦里花开知多少4 小时前
聊聊MVVM与MVI
架构