Spring Boot + Redis 实现分布式锁(秒杀场景)完整指南!告别超卖“噩梦”
·
🛠️ 一、准备工作(只需 2 分钟)
你需要:
- JDK 17
- IDEA
- Redis(本地或 Docker)
- JMeter(用于压测验证)
💡 没有 Redis? 用 Docker 一行命令启动:
docker run -d -p 6379:6379 --name redis-lock redis:7.0
💻 二、分布式锁核心原理(Redis 实现)
为什么需要分布式锁?
- 单机锁(synchronized)只在当前 JVM 有效
- 多台服务器部署,需要 Redis 这样的共享存储来协调
核心命令:SET NX EX
SET lock:product:123 "thread-1" NX EX 30
NX:key 不存在才设置(保证原子性)EX 30:30秒过期(防死锁)"thread-1":锁的值(用于解锁时验证)
🔐 三、实现分布式锁(完整代码)
步骤 1:添加依赖(pom.xml)
<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-aop</artifactId>
</dependency>
</dependencies>
步骤 2:配置 Redis(application.yml)
spring:
redis:
host: localhost
port: 6379
database: 2
timeout: 2000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
# 分布式锁配置
lock:
default-expire: 30 # 锁默认过期时间(秒)
retry-count: 3 # 获取锁失败时重试次数
retry-delay: 100 # 重试间隔(毫秒)
步骤 3:创建分布式锁工具类(util/DistributedLock.java)
package com.example.lockdemo.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Component
public class DistributedLock {
@Autowired
private StringRedisTemplate redisTemplate;
@Value("${lock.default-expire:30}")
private int defaultExpireSeconds;
@Value("${lock.retry-count:3}")
private int retryCount;
@Value("${lock.retry-delay:100}")
private long retryDelay;
private final ThreadLocal<String> lockValue = new ThreadLocal<>();
/**
* 尝试获取分布式锁
*/
public boolean tryLock(String lockKey, int expireSeconds) {
String value = UUID.randomUUID().toString();
lockValue.set(value);
for (int i = 0; i < retryCount; i++) {
// 使用 SET NX EX 命令原子性地设置锁
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, value, expireSeconds, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(result)) {
return true; // 获取锁成功
}
// 获取锁失败,等待后重试
try {
Thread.sleep(retryDelay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
return false; // 获取锁失败
}
/**
* 释放分布式锁
*/
public boolean releaseLock(String lockKey) {
String value = lockValue.get();
if (value == null) {
return false;
}
// 使用 Lua 脚本保证原子性:只释放当前线程持有的锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
Long result = redisTemplate.execute(
(connection) -> connection.eval(script.getBytes(), 1,
lockKey.getBytes(), value.getBytes()),
null);
lockValue.remove();
return result != null && result == 1;
}
/**
* 获取锁(带默认过期时间)
*/
public boolean tryLock(String lockKey) {
return tryLock(lockKey, defaultExpireSeconds);
}
}
🔍 关键点:
SET NX EX:原子性加锁UUID:防止误删其他线程的锁Lua 脚本:保证解锁的原子性
步骤 4:创建秒杀服务(service/SeckillService.java)
package com.example.lockdemo.service;
import com.example.lockdemo.util.DistributedLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class SeckillService {
@Autowired
private DistributedLock distributedLock;
// 模拟商品库存(实际项目中应从数据库获取)
private volatile int productStock = 10; // 10个商品用于秒杀
/**
* 执行秒杀(带分布式锁)
*/
public String seckill(Long userId, Long productId) {
String lockKey = "lock:seckill:" + productId;
// 尝试获取分布式锁
if (distributedLock.tryLock(lockKey, 10)) { // 10秒过期
try {
// 双重检查库存
if (productStock <= 0) {
return "秒杀失败:库存不足";
}
// 扣减库存(模拟数据库操作)
productStock--;
System.out.println("用户 " + userId + " 秒杀成功,剩余库存:" + productStock);
// 模拟业务处理时间
Thread.sleep(100);
return "秒杀成功!";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return "秒杀失败:系统异常";
} finally {
// 释放锁
distributedLock.releaseLock(lockKey);
}
} else {
return "秒杀失败:获取锁失败,请重试";
}
}
}
步骤 5:创建控制器(controller/SeckillController.java)
package com.example.lockdemo.controller;
import com.example.lockdemo.service.SeckillService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/seckill")
public class SeckillController {
@Autowired
private SeckillService seckillService;
/**
* 秒杀接口
*/
@PostMapping("/buy")
public String seckill(@RequestParam Long userId, @RequestParam Long productId) {
return seckillService.seckill(userId, productId);
}
/**
* 获取库存
*/
@GetMapping("/stock/{productId}")
public String getStock(@PathVariable Long productId) {
// 这里简化,实际应从数据库获取
return "剩余库存: " + 10; // 模拟返回固定值,实际项目中应动态获取
}
}
🧪 四、测试验证(JMeter 压测)
1. 启动项目
mvn spring-boot:run
2. JMeter 配置(模拟 100 并发)
- 线程数:100
- 循环次数:1
- 请求路径:
POST http://localhost:8080/api/seckill/buy - 参数:
userId=1&productId=123
3. 预期结果
- 10个用户秒杀成功
- 90个用户返回“库存不足”或“获取锁失败”
- 无超卖现象
✅ 控制台日志显示:
剩余库存:0→秒杀失败:库存不足
💡 五、Bonus:分布式锁常见坑点
坑 1:锁过期时间设置不当
- 过短:业务还没执行完,锁就过期了 → 可能出现超卖
- 过长:某个线程挂了,锁一直不释放 → 系统阻塞
- 建议:业务执行时间的 2-3 倍
坑 2:锁的粒度过粗
- 错误:用全局锁锁整个系统
- 正确:按业务粒度锁,如
lock:seckill:product:123
坑 3:忘记释放锁
- 必须:在
finally块中释放锁 - 使用:try-with-resources 或 AOP 切面自动释放
坑 4:Redis 单点故障
- 生产建议:使用 Redis Cluster 或哨兵模式
- 考虑:引入 Redission 等成熟的分布式锁框架
🚀 六、进阶:AOP 自动加锁解锁
// 创建注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
String key() default "";
int expireSeconds() default 30;
}
// AOP 切面
@Aspect
@Component
public class DistributedLockAspect {
@Autowired
private DistributedLock lockUtil;
@Around("@annotation(lockAnnotation)")
public Object around(ProceedingJoinPoint joinPoint, com.example.lockdemo.annotation.DistributedLock lockAnnotation) throws Throwable {
String lockKey = lockAnnotation.key();
if (lockKey.isEmpty()) {
// 可以根据方法名和参数生成锁 key
lockKey = "lock:" + joinPoint.getSignature().getName();
}
if (lockUtil.tryLock(lockKey, lockAnnotation.expireSeconds())) {
try {
return joinPoint.proceed();
} finally {
lockUtil.releaseLock(lockKey);
}
} else {
throw new RuntimeException("获取锁失败");
}
}
}
// 使用注解
@DistributedLock(key = "seckill:123", expireSeconds = 10)
public String seckill(Long userId, Long productId) {
// 业务逻辑...
}
💬 七、写在最后
分布式锁,看似高深,实则是 Redis 命令 + 原子性 + 防死锁 的组合。
记住三句话:
- SET NX EX 是核心,保证原子性
- UUID + Lua 脚本,防止误删锁
- 锁粒度要细,过期时间要合理
学会分布式锁,你就掌握了 高并发场景下的数据一致性保障!
更多推荐
所有评论(0)