别再乱用了!幂等处理与分布式锁,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 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)

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 核心实体类

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;
}

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;
}

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层

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> {
}

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);
}

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 幂等处理通用实现

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 分布式锁通用实现

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 两者联用的核心业务实现

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 接口层实现

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,而是能精准定位业务场景的核心风险,选择最适合的技术方案,用最低的成本,解决最核心的问题。希望本文能帮你彻底理清两者的底层逻辑,避开高频踩坑点,写出更健壮、更高性能的分布式系统代码。

目录
相关文章
|
5天前
|
人工智能 JSON 机器人
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
本文带你零成本玩转OpenClaw:学生认证白嫖6个月阿里云服务器,手把手配置飞书机器人、接入免费/高性价比AI模型(NVIDIA/通义),并打造微信公众号“全自动分身”——实时抓热榜、AI选题拆解、一键发布草稿,5分钟完成热点→文章全流程!
10731 63
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
|
5天前
|
人工智能 IDE API
2026年国内 Codex 安装教程和使用教程:GPT-5.4 完整指南
Codex已进化为AI编程智能体,不仅能补全代码,更能理解项目、自动重构、执行任务。本文详解国内安装、GPT-5.4接入、cc-switch中转配置及实战开发流程,助你从零掌握“描述需求→AI实现”的新一代工程范式。(239字)
3111 126
|
1天前
|
人工智能 自然语言处理 供应链
【最新】阿里云ClawHub Skill扫描:3万个AI Agent技能中的安全度量
阿里云扫描3万+AI Skill,发现AI检测引擎可识别80%+威胁,远高于传统引擎。
1199 1
|
11天前
|
人工智能 JavaScript API
解放双手!OpenClaw Agent Browser全攻略(阿里云+本地部署+免费API+网页自动化场景落地)
“让AI聊聊天、写代码不难,难的是让它自己打开网页、填表单、查数据”——2026年,无数OpenClaw用户被这个痛点困扰。参考文章直击核心:当AI只能“纸上谈兵”,无法实际操控浏览器,就永远成不了真正的“数字员工”。而Agent Browser技能的出现,彻底打破了这一壁垒——它给OpenClaw装上“上网的手和眼睛”,让AI能像真人一样打开网页、点击按钮、填写表单、提取数据,24小时不间断完成网页自动化任务。
2563 6
|
25天前
|
人工智能 JavaScript Ubuntu
5分钟上手龙虾AI!OpenClaw部署(阿里云+本地)+ 免费多模型配置保姆级教程(MiniMax、Claude、阿里云百炼)
OpenClaw(昵称“龙虾AI”)作为2026年热门的开源个人AI助手,由PSPDFKit创始人Peter Steinberger开发,核心优势在于“真正执行任务”——不仅能聊天互动,还能自动处理邮件、管理日程、订机票、写代码等,且所有数据本地处理,隐私完全可控。它支持接入MiniMax、Claude、GPT等多类大模型,兼容微信、Telegram、飞书等主流聊天工具,搭配100+可扩展技能,成为兼顾实用性与隐私性的AI工具首选。
24388 122