在分布式系统开发中,幂等处理和分布式锁是两个绕不开的核心技术点。但我见过太多开发者,要么把两者混为一谈,用分布式锁去实现幂等,最终导致线上资损;要么盲目叠加两者,给系统增加不必要的复杂度和性能开销;更有甚者,在核心交易链路只做了其中一项,最终引发超卖、重复支付、数据错乱等严重线上故障。
一、底层本质:先搞懂你要解决的到底是什么问题
很多人用错的根源,是从一开始就没搞懂这两个技术的核心定位,它们解决的是分布式系统中两个完全不同维度的风险。
1.1 核心定义拆解
幂等处理
幂等处理的本质,是解决时间维度上的重复执行问题。它的核心承诺是:同一个操作,无论被执行1次还是N次,最终产生的业务结果完全一致,不会出现额外的副作用。
核心关键词:重复执行、结果唯一性、无额外副作用。 通俗类比:你用ATM机转账1000元,由于网络卡顿连续提交了两次请求。幂等处理会保证,无论你提交多少次,你的账户只会被扣1000元,收款人只会收到1000元。
分布式锁
分布式锁的本质,是解决同一时刻的并发竞争问题。它的核心承诺是:在分布式环境下,同一时刻,只有一个客户端/线程,能对指定的共享资源执行操作。
核心关键词:并发执行、互斥性、资源独占性。 通俗类比:银行窗口只有一个业务员,同时来了10个客户要办业务。分布式锁就是叫号机,保证同一时刻只有一个客户能在窗口办理业务,其余客户只能排队等待。
1.2 相似点与核心区别
表象相似点
很多人会把两者混为一谈,核心是因为它们有三个共性特征:
- 终极目标一致:都是为了保证分布式系统下的数据正确性和一致性,防止脏数据产生。
- 实现工具重叠:Redis、MySQL、ZooKeeper等中间件,既可以用来实现分布式锁,也可以作为幂等处理的存储介质。
- 适用场景有交集:在高并发核心交易链路中,两者经常需要配合使用,这也是很多人误以为两者必须绑定的原因。
核心本质区别
| 对比维度 | 幂等处理 | 分布式锁 |
| 核心解决问题 | 时间维度的重复执行(请求先后发生,时间上错开) | 空间维度的并发竞争(请求同一时刻发生,并行执行) |
| 核心关注点 | 操作的最终结果,无论执行多少次,结果唯一 | 操作的执行过程,同一时间只允许一个执行主体操作 |
| 核心特性 | 结果唯一性、重试友好性 | 互斥性、串行化、资源独占性 |
| 业务侵入性 | 业务逻辑的核心组成部分,与业务规则强绑定 | 独立的并发协调机制,与业务逻辑解耦 |
| 失败处理策略 | 天然支持重试,重试是其设计的核心场景 | 不鼓励盲目重试,抢锁失败通常代表资源正在被占用 |
| 性能开销 | 较低,通常为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,而是能精准定位业务场景的核心风险,选择最适合的技术方案,用最低的成本,解决最核心的问题。希望本文能帮你彻底理清两者的底层逻辑,避开高频踩坑点,写出更健壮、更高性能的分布式系统代码。