前面咱们搞定了异步处理、监控、数据导出、文件操作,但若遇到 “图书限时秒杀”“高并发下单减库存”“用户重复提交订单” 这类场景,很容易出现两个致命问题:① 超卖(10 本库存被抢 15 本);② 重复提交(用户连续点击导致多下单)—— 就像奶茶店 “限量 10 杯爆款奶茶秒杀”,前台同时接 50 个订单,后厨没做好库存同步导致超卖,或用户连续点两次下单按钮导致重复下单,既亏成本又影响用户体验。
Spring Boot 生态搭配Redis 分布式锁(解决超卖)+ 接口幂等性设计(解决重复提交),就是应对高并发的 “双保险”:分布式锁像奶茶店的 “库存管理员”,同一时间只允许一个人操作库存;幂等性设计像 “订单唯一凭证”,同一用户同一请求只能生效一次,两者结合确保高并发下数据一致。
今天以图书管理系统的 “限时图书秒杀” 为案例,手把手实现 Redis 分布式锁(可重入、防死锁)和接口幂等性(Token+Redis),解决超卖、重复提交、并发安全等核心痛点,全程 Spring Boot+Redis 实战,代码可直接复制落地!
一、先搞懂:核心概念与企业需求(奶茶店类比)
1. 核心概念拆解(高并发 = 奶茶店秒杀)
| 技术 | 类比奶茶店 | 作用说明(Spring Boot 中) |
|---|---|---|
| Redis 分布式锁 | 库存管理员(管爆款奶茶库存) | 高并发下独占库存操作(如减库存),避免多人同时操作导致超卖 |
| 接口幂等性 | 订单唯一凭证(一人一次秒杀资格) | 同一用户同一请求(如重复点击下单)仅生效一次,避免重复提交 |
2. 解决的企业核心痛点
超卖问题:图书库存 10 本,100 个用户并发抢购,最终下单 15 本,库存出现负数;重复提交:用户网络卡顿或连续点击,导致同一订单提交多次,重复扣库存、重复生成订单;并发安全:高并发下库存计数混乱(如 A 用户减库存还没完成,B 用户已读取旧库存);分布式部署适配:多台 Spring Boot 服务器集群部署时,本地锁(synchronized)失效,需分布式锁跨服务器同步。
3. 技术选型优势(Spring Boot+Redis)
轻量高效:Redis 基于内存操作,分布式锁和幂等性验证响应时间均为毫秒级,不影响接口性能;无缝整合:Spring Boot 提供依赖,配合
spring-boot-starter-data-redis快速操作 Redis;可靠性高:Redis 支持持久化,配合锁超时释放机制,避免死锁导致库存冻结;通用性强:一套方案可复用在秒杀、下单、支付等所有高并发场景。
RedisTemplate
二、实操 1:Redis 分布式锁实现(解决超卖)
分布式锁的核心逻辑:同一时间只有一个线程能获取锁,操作完成后释放锁,超时未释放自动失效(避免死锁)。基于 Redis 的命令实现(原子操作,确保加锁和设置超时原子性)。
SET NX EX
步骤 1:环境准备(Redis 依赖 + 配置)
1. 加 Redis 依赖
xml
<!-- Spring Boot整合Redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.7.10</version>
</dependency>
<!-- 连接池依赖(提升Redis性能) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.11.1</version>
</dependency>
2. Redis 配置(application-dev.yml)
yaml
spring:
redis:
host: localhost # Redis地址(服务器填IP)
port: 6379 # 端口
password: # Redis密码(默认无)
database: 0 # 数据库索引
lettuce:
pool:
max-active: 8 # 最大连接数
max-idle: 8 # 最大空闲连接数
min-idle: 2 # 最小空闲连接数
max-wait: 1000ms # 连接超时时间
# 分布式锁配置
distributed-lock:
prefix: "book:lock:" # 锁Key前缀(区分不同业务)
expire-seconds: 30 # 锁默认超时时间(30秒,避免死锁)
3. RedisTemplate 配置(序列化 + 工具类)
新建,配置 Redis 序列化(避免 Key/Value 乱码):
config/RedisConfig.java
java
运行
package com.example.springbootdemo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis配置(序列化+模板)
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Key序列化:String序列化(避免Key乱码)
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// Value序列化:JSON序列化(支持对象存储,可读性强)
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
步骤 2:实现分布式锁工具类(可重入 + 自动释放)
新建,封装加锁、释放锁、续期逻辑,支持可重入(同一线程可多次获取锁):
utils/RedisDistributedLock.java
java
运行
package com.example.springbootdemo.utils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* Redis分布式锁(可重入、超时自动释放、防死锁)
*/
@Component
public class RedisDistributedLock implements Lock {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Value("${distributed-lock.prefix}")
private String lockPrefix;
@Value("${distributed-lock.expire-seconds}")
private long defaultExpireSeconds;
// 线程本地存储:存储当前线程获取的锁的计数(支持可重入)
private final ThreadLocal<LockInfo> lockInfoThreadLocal = new ThreadLocal<>();
// 解锁Lua脚本(原子操作,避免误解锁)
private static final String UNLOCK_SCRIPT = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
""";
/**
* 锁信息(存储锁Key、锁值、超时时间)
*/
private static class LockInfo {
String lockKey;
String lockValue;
long expireSeconds;
int reentrantCount; // 重入计数
public LockInfo(String lockKey, String lockValue, long expireSeconds) {
this.lockKey = lockKey;
this.lockValue = lockValue;
this.expireSeconds = expireSeconds;
this.reentrantCount = 1;
}
}
/**
* 加锁(阻塞式,直到获取锁成功)
*/
@Override
public void lock() {
lock(defaultExpireSeconds);
}
/**
* 加锁(指定超时时间)
*/
public void lock(long expireSeconds) {
String lockKey = getCurrentLockKey();
String lockValue = generateLockValue();
// 1. 检查当前线程是否已持有锁(可重入)
LockInfo existingLock = lockInfoThreadLocal.get();
if (existingLock != null && existingLock.lockKey.equals(lockKey) && existingLock.lockValue.equals(lockValue)) {
existingLock.reentrantCount++;
return;
}
// 2. 循环获取锁(阻塞式)
while (!tryLock(expireSeconds)) {
// 短暂休眠,减少Redis压力
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取分布式锁失败", e);
}
}
}
/**
* 尝试加锁(非阻塞式,获取成功返回true,失败返回false)
*/
@Override
public boolean tryLock() {
return tryLock(defaultExpireSeconds);
}
/**
* 尝试加锁(指定超时时间)
*/
public boolean tryLock(long expireSeconds) {
String lockKey = getCurrentLockKey();
String lockValue = generateLockValue();
// 检查可重入
LockInfo existingLock = lockInfoThreadLocal.get();
if (existingLock != null && existingLock.lockKey.equals(lockKey) && existingLock.lockValue.equals(lockValue)) {
existingLock.reentrantCount++;
return true;
}
// Redis SET NX EX命令:原子操作(不存在则设置,同时设置过期时间)
Boolean success = redisTemplate.opsForValue().setIfAbsent(
lockKey, lockValue, expireSeconds, TimeUnit.SECONDS
);
if (Boolean.TRUE.equals(success)) {
// 加锁成功,存储锁信息到ThreadLocal
lockInfoThreadLocal.set(new LockInfo(lockKey, lockValue, expireSeconds));
return true;
}
return false;
}
/**
* 释放锁
*/
@Override
public void unlock() {
LockInfo lockInfo = lockInfoThreadLocal.get();
if (lockInfo == null) {
throw new IllegalMonitorStateException("当前线程未持有锁");
}
// 重入计数减1,不为0则不释放锁
if (--lockInfo.reentrantCount > 0) {
return;
}
// 执行Lua脚本解锁(原子操作,避免误解锁其他线程的锁)
DefaultRedisScript<Long> script = new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList(lockInfo.lockKey),
lockInfo.lockValue
);
if (result == null || result == 0) {
throw new RuntimeException("释放分布式锁失败,锁可能已过期或被其他线程持有");
}
// 清除ThreadLocal中的锁信息
lockInfoThreadLocal.remove();
}
// 生成锁Key(业务Key+前缀)
private String getCurrentLockKey() {
// 从调用栈中获取业务Key(实际场景可通过参数传入,这里简化为固定业务Key)
// 实际使用时建议:lockPrefix + 业务标识(如图书ID),如"book:lock:1001"
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
for (StackTraceElement element : stackTrace) {
if (element.getMethodName().startsWith("seckill")) { // 秒杀业务方法名
return lockPrefix + "seckill:" + element.getFileName();
}
}
throw new RuntimeException("无法获取业务锁Key");
}
// 生成锁Value(UUID+线程ID,确保唯一性)
private String generateLockValue() {
return UUIDUtil.fastUUID() + ":" + Thread.currentThread().getId();
}
// 以下方法为Lock接口必填实现,分布式场景暂不使用,直接抛出异常
@Override
public void lockInterruptibly() throws InterruptedException {
throw new UnsupportedOperationException("不支持可中断锁");
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
throw new UnsupportedOperationException("不支持超时可中断锁");
}
@Override
public Condition newCondition() {
throw new UnsupportedOperationException("不支持Condition");
}
}
步骤 3:秒杀业务实现(结合分布式锁防超卖)
以 “图书限时秒杀” 为场景,实现减库存、创建订单逻辑,用分布式锁确保并发安全:
java
运行
package com.example.springbootdemo.service;
import com.example.springbootdemo.entity.Book;
import com.example.springbootdemo.entity.Order;
import com.example.springbootdemo.mapper.BookMapper;
import com.example.springbootdemo.mapper.OrderMapper;
import com.example.springbootdemo.utils.RedisDistributedLock;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.Date;
@Service
public class SeckillService {
@Resource
private BookMapper bookMapper;
@Resource
private OrderMapper orderMapper;
@Resource
private RedisDistributedLock distributedLock;
/**
* 图书秒杀(核心业务:减库存+创建订单)
* @param bookId 图书ID
* @param userId 用户ID
* @return 秒杀结果
*/
@Transactional(rollbackFor = Exception.class)
public String seckillBook(Long bookId, Long userId) {
String lockKey = "book:lock:seckill:" + bookId; // 锁Key:按图书ID粒度(避免全局锁)
try {
// 1. 获取分布式锁(指定超时时间10秒)
boolean locked = distributedLock.tryLock(10);
if (!locked) {
return "秒杀太火爆,请稍后再试!";
}
// 2. 查询图书库存(加锁后查询,确保库存是最新值)
Book book = bookMapper.selectById(bookId);
if (book == null) {
return "图书不存在!";
}
if (book.getStock() <= 0) {
return "秒杀已结束,库存不足!";
}
// 3. 检查用户是否已秒杀过(避免重复下单,配合幂等性)
Order existingOrder = orderMapper.lambdaQuery()
.eq(Order::getBookId, bookId)
.eq(Order::getUserId, userId)
.one();
if (existingOrder != null) {
return "您已秒杀过该图书,不可重复购买!";
}
// 4. 减库存(数据库乐观锁或悲观锁,这里用悲观锁配合分布式锁)
int updateCount = bookMapper.decrementStock(bookId);
if (updateCount == 0) {
return "库存已被抢完,请稍后再试!";
}
// 5. 创建秒杀订单
Order order = new Order();
order.setBookId(bookId);
order.setUserId(userId);
order.setOrderTime(new Date());
order.setAmount(book.getPrice());
order.setStatus(0); // 0=待支付
orderMapper.insert(order);
return "秒杀成功!订单ID:" + order.getId();
} finally {
// 6. 释放锁(必须在finally中,确保锁一定释放)
distributedLock.unlock();
}
}
}
步骤 4:秒杀接口(暴露 HTTP 接口供前端调用)
java
运行
package com.example.springbootdemo.controller;
import com.example.springbootdemo.common.Result;
import com.example.springbootdemo.service.SeckillService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/seckill")
public class SeckillController {
@Resource
private SeckillService seckillService;
/**
* 秒杀接口(需登录,配合JWT认证)
*/
@PostMapping("/book")
@PreAuthorize("isAuthenticated()") // 已登录用户可访问
public Result<String> seckillBook(
@RequestParam Long bookId,
@RequestParam Long userId) { // 实际场景从JWT令牌中获取userId
String result = seckillService.seckillBook(bookId, userId);
return Result.success(result);
}
}
步骤 5:测试分布式锁防超卖效果
准备数据:图书 ID=1001,库存 = 10;用 JMeter 模拟 100 个并发请求(调用接口,bookId=1001,userId=1~100);结果验证:
/seckill/book
数据库表库存 = 0(无超卖);
book表新增 10 条订单(与库存一致);日志中无 “库存不足” 和 “重复购买” 的冲突报错。
order
三、实操 2:接口幂等性实现(解决重复提交)
分布式锁解决了 “多用户并发超卖”,但没解决 “同一用户重复提交”(如用户连续点击下单按钮)。接口幂等性通过 “Token 验证机制” 实现:用户下单前先获取 Token,提交订单时携带 Token,验证通过则处理业务,处理后失效 Token,重复提交则直接返回失败。
步骤 1:幂等性 Token 工具类
新建,封装 Token 生成、验证、失效逻辑:
utils/IdempotentTokenUtil.java
java
运行
package com.example.springbootdemo.utils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* 幂等性Token工具类(Redis存储Token)
*/
@Component
public class IdempotentTokenUtil {
@Resource
private RedisTemplate<String, Object> redisTemplate;
// Token前缀(区分不同业务)
private static final String TOKEN_PREFIX = "book:idempotent:token:";
// Token有效期(30分钟,避免Token长期有效)
private static final long TOKEN_EXPIRE_MINUTES = 30;
/**
* 生成幂等性Token
* @param userId 用户ID(区分不同用户)
* @param businessType 业务类型(如图书秒杀:"seckill_book")
* @return Token字符串
*/
public String generateToken(Long userId, String businessType) {
String token = UUIDUtil.fastUUID();
String tokenKey = getTokenKey(userId, businessType);
// 存储Token到Redis(用户+业务类型唯一,避免同一用户同一业务重复获取Token)
redisTemplate.opsForValue().set(tokenKey, token, TOKEN_EXPIRE_MINUTES, TimeUnit.MINUTES);
return token;
}
/**
* 验证Token有效性(验证通过后失效Token)
* @param userId 用户ID
* @param businessType 业务类型
* @param token 待验证Token
* @return 验证结果(true=有效,false=无效/已使用)
*/
public boolean validateToken(Long userId, String businessType, String token) {
String tokenKey = getTokenKey(userId, businessType);
// Redis GETANDDELETE命令:原子操作(获取Token并删除,避免重复验证)
Object storedToken = redisTemplate.opsForValue().getAndDelete(tokenKey);
return storedToken != null && storedToken.equals(token);
}
// 生成Token的Redis Key(用户ID+业务类型)
private String getTokenKey(Long userId, String businessType) {
return TOKEN_PREFIX + userId + ":" + businessType;
}
}
步骤 2:修改秒杀业务(整合幂等性 Token)
1. 新增获取 Token 接口
java
运行
@RestController
@RequestMapping("/idempotent")
public class IdempotentController {
@Resource
private IdempotentTokenUtil idempotentTokenUtil;
/**
* 获取秒杀Token(用户下单前必须调用)
*/
@PostMapping("/token/seckill")
@PreAuthorize("isAuthenticated()")
public Result<String> getSeckillToken(@RequestParam Long userId) {
// 业务类型:图书秒杀
String token = idempotentTokenUtil.generateToken(userId, "seckill_book");
return Result.success("Token获取成功", token);
}
}
2. 修改秒杀接口(添加 Token 验证)
java
运行
@RestController
@RequestMapping("/seckill")
public class SeckillController {
@Resource
private SeckillService seckillService;
@Resource
private IdempotentTokenUtil idempotentTokenUtil;
@PostMapping("/book")
@PreAuthorize("isAuthenticated()")
public Result<String> seckillBook(
@RequestParam Long bookId,
@RequestParam Long userId,
@RequestParam String token) { // 前端携带Token提交
// 1. 幂等性Token验证
boolean tokenValid = idempotentTokenUtil.validateToken(userId, "seckill_book", token);
if (!tokenValid) {
return Result.error(4001, "重复提交或Token已失效,请刷新页面重试!");
}
// 2. 调用秒杀业务(已加分布式锁)
String result = seckillService.seckillBook(bookId, userId);
return Result.success(result);
}
}
步骤 3:测试幂等性效果
用户先调用获取 Token(如
/idempotent/token/seckill);第一次提交:携带
token=xxx123、
bookId=1001、
userId=1001,秒杀成功;第二次提交:相同参数再次调用,返回 “重复提交或 Token 已失效”;结果验证:
token=xxx123表仅新增 1 条订单(无重复提交)。
order
四、分布式锁 + 幂等性的 6 个要小心
分布式锁粒度太粗,导致性能瓶颈:
坑:用 “book:lock:seckill” 全局锁,所有图书秒杀共用一把锁,并发性能差;解决:锁粒度细化到 “图书 ID”(如),不同图书秒杀互不影响。
book:lock:seckill:1001
锁超时时间设置不合理,导致死锁 / 业务中断:
坑:锁超时设 5 秒,而秒杀业务(查库存 + 创建订单)需要 10 秒,锁提前释放导致并发问题;解决:超时时间略大于业务最大执行时间(如业务最长 8 秒,超时设 10 秒),或实现锁续期(如定时任务延长未释放锁的超时时间)。
误解锁其他线程的锁:
坑:释放锁时直接,可能删除其他线程持有的锁(如当前线程锁超时,其他线程已获取锁);解决:用 Lua 脚本原子验证锁值(
del key)后再删除,避免误解锁。
get key == value
幂等性 Token 未绑定用户,导致 Token 被盗用:
坑:Token 仅按业务类型生成,攻击者获取他人 Token 后可恶意提交;解决:Token 绑定 “用户 ID + 业务类型”,确保只有 Token 所属用户能使用。
Redis 宕机导致分布式锁失效:
坑:单节点 Redis 宕机,分布式锁无法获取,秒杀业务中断;解决:生产环境用 Redis 集群(主从 + 哨兵),确保 Redis 高可用;或降级为本地锁(仅单机部署时)。
重复提交验证依赖前端,导致绕过:
坑:仅依赖前端禁用按钮防止重复提交,攻击者直接调用接口可绕过;解决:前端禁用按钮(友好体验)+ 后端 Token 验证(核心防护),双重保障。
高并发场景的核心是 “锁 + 幂等” 双重保障
Spring Boot+Redis 实现的 “分布式锁 + 接口幂等性”,完美解决了高并发下的超卖和重复提交问题:分布式锁确保 “多用户并发安全”,幂等性确保 “单用户重复提交安全”,两者结合覆盖了高并发场景的核心风险点。
就像奶茶店的秒杀活动:分布式锁是 “库存管理员”,确保同一时间只有一个人操作库存;幂等性 Token 是 “唯一抢购凭证”,确保每个人只能抢购一次。掌握这两个技能,你就能应对秒杀、下单、支付等所有高并发业务场景,配合之前的系列实战技能,你的 Spring Boot 项目将具备 “高并发、高可用、高安全” 的企业级能力。
📦 给你的实战包:我整理了 “Spring Boot 高并发实战全量包”,包含:① Redis 分布式锁工具类;② 幂等性 Token 工具类;③ 秒杀业务完整代码;④ JMeter 压测脚本 + Postman 测试用例;⑤ Redis 集群配置指南,你不用手动写代码,解压后导入 IDEA 就能运行,需要评论区留言。

