Java 从 0 到 1 设计通用 Redisson 分布式锁:解决超卖 / 重复提交(结尾有完整代码可直接复用)
引言:为什么我们需要 “通用” 的分布式锁?
做 Java 开发的同学,几乎都遇到过这些场景:
- 秒杀系统里的库存超卖,明明只有 100 件商品,最后卖出去 105 件;
- 优惠券领取时的重复发放,用户一点击就多领了 2 张;
- 定时任务集群部署时的重复执行,多台机器同时跑同一个任务。
这些问题的根源,都是单 JVM 内的synchronized/Lock搞不定了 —— 多实例部署时,每个实例的锁是独立的,根本拦不住跨机器的并发。
于是我们想到用 Redis 做分布式锁,但原生 Redis 命令(SETNX+EXPIRE)又有一堆坑:误删别人的锁、业务没执行完锁就过期、删锁时被打断导致死锁…
而 Redisson 虽然解决了这些问题,但直接用会有大量重复代码(每次都要getLock→tryLock→unlock),还没法灵活适配不同场景(有时要公平锁、有时要读写锁)。
这篇文章就带你从 0 到 1,一步步设计一个通用、可扩展、能直接丢到项目里用的 Redisson 分布式锁,从基础使用到 AOP 注解优化,再到动态锁名、灵活策略,最后用优惠券防超卖实战验证,全程附完整源码。
1. 先搞懂:为什么不选 Redis 原生锁,非要用 Redisson?
在设计之前,先明确一个核心问题:Redis 原生锁的坑,Redisson 是怎么填的?这是我们设计通用锁的基础。
1.1 Redis 原生锁的 3 个致命问题
用 Redis 做分布式锁,最开始会这么写:
// 1.加锁(SETNX)
Boolean locked = redisTemplate.opsForValue().setIfAbsent("lock:coupon", "thread1", 10, TimeUnit.SECONDS);
if (!locked) {
throw new RuntimeException("请稍后再试");
}
try {
// 执行业务:减库存、发优惠券
} finally {
// 2.释放锁
redisTemplate.delete("lock:coupon");
}
但这代码藏着 3 个大坑:
- 坑 1:误删别人的锁:如果线程 A 的业务执行慢,锁 10 秒后自动过期,线程 B 拿到锁;这时 A 执行完,会把 B 的锁删掉;
- 坑 2:锁过期业务没执行完:10 秒过期时间设短了,业务还在跑锁就没了,导致并发问题;设长了又怕服务宕机导致死锁;
- 坑 3:释放锁不是原子操作:如果线程 A 删锁时,刚判断完 “这是我的锁”,还没执行delete就被打断(比如 GC),锁过期后线程 B 拿到锁,A 恢复后还是会删掉 B 的锁。
1.2 Redisson 的 “神仙操作”:3 个问题一次性解决
Redisson 之所以成为主流,就是因为它内置了解决方案:
- 防误删:加锁时存入的是 “线程唯一标识”(比如UUID+线程ID),释放前先判断标识是否匹配;
- 看门狗机制:如果没手动设置锁过期时间(leaseTime=-1),Redisson 会启动一个后台线程,每隔 10 秒给锁续期 30 秒,直到业务执行完;
- 原子释放锁:用 Lua 脚本把 “判断标识 + 删除锁” 做成原子操作,避免中间被打断。
而且 Redisson 还支持可重入锁、公平锁、读写锁等多种锁类型,这为我们设计 “通用锁” 提供了基础。
2. 基础篇:Redisson 分布式锁的 “Hello World”
在设计通用锁之前,先快速掌握 Redisson 的基础使用 —— 这是后续优化的起点。
2.1 第一步:引入依赖 + 配置 RedissonClient
首先在pom.xml引入 Redisson 依赖(注意版本和 Redis 版本适配,这里用 3.23.3):
<!-- Redisson核心依赖 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.23.3</version>
</dependency>
<!-- Spring Data Redis(如果项目没引入的话) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
然后配置RedissonClient(支持单点、集群、哨兵,这里以单点为例):
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
// 单点Redis配置:redis://IP:端口,密码如果没有可以去掉setPassword
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setPassword("123456")
.setDatabase(0); // 指定Redis数据库索引
// 集群配置(如果需要):
// config.useClusterServers()
// .addNodeAddress("redis://127.0.0.1:7001")
// .addNodeAddress("redis://127.0.0.1:7002");
return Redisson.create(config);
}
}
2.2 第二步:基础锁使用(可重入锁)
直接注入RedissonClient,调用tryLock获取锁,unlock释放锁(必须在finally里释放,避免锁泄漏):
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class CouponService {
@Autowired
private RedissonClient redissonClient;
// 优惠券领取方法(防重复领取/超卖)
public void receiveCoupon(Long couponId, Long userId) {
// 1.定义锁名:用业务标识+唯一ID,避免锁冲突(比如coupon_123_user_456)
String lockKey = "coupon:receive:" + couponId + ":" + userId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 2.尝试获取锁:waitTime=3秒(获取不到锁时最多等3秒),leaseTime=-1(用看门狗自动续期)
boolean isLocked = lock.tryLock(3, -1, TimeUnit.SECONDS);
if (!isLocked) {
throw new RuntimeException("请求太频繁,请稍后再试");
}
// 3.执行业务逻辑:校验库存、生成用户优惠券、扣减库存等
System.out.println("用户" + userId + "成功领取优惠券" + couponId);
// 这里写你的业务代码...
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("领取失败");
} finally {
// 4.释放锁:必须判断锁是否被当前线程持有,避免释放别人的锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
关键注意点
- lockKey必须唯一:避免不同业务用同一个锁名,导致 “锁竞争” 或 “锁误删”;
- leaseTime=-1:启用看门狗机制,自动续期;如果手动设置leaseTime=10,看门狗会失效,锁 10 秒后自动释放;
- unlock前判断isHeldByCurrentThread:防止线程 A 没拿到锁,却释放了线程 B 的锁。
3. 优化 1:用 AOP + 注解解耦 —— 告别重复的锁代码
上面的基础用法有个问题:每个需要加锁的方法,都要写 “获取锁→判断→释放锁” 的重复代码,业务侵入性太强。
解决方案:用自定义注解 + AOP,把锁逻辑抽成通用切面,业务方法只需要加个注解就能实现加锁。
3.1 第一步:定义自定义注解@MyLock
注解里包含锁的核心参数:锁名、等待时间、过期时间、时间单位等,给默认值减少配置成本:
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* 自定义Redisson分布式锁注解
*/
@Target(ElementType.METHOD) // 注解作用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
@Documented
public @interface MyLock {
/**
* 锁的名称(支持SPEL表达式,比如"coupon:receive:#{#couponId}:#{#userId}")
*/
String name();
/**
* 获取锁的最大等待时间(默认3秒)
*/
long waitTime() default 3;
/**
* 锁的过期时间(默认-1,启用看门狗自动续期)
*/
long leaseTime() default -1;
/**
* 时间单位(默认秒)
*/
TimeUnit unit() default TimeUnit.SECONDS;
/**
* 获取锁失败时的提示信息
*/
String failMsg() default "请求太频繁,请稍后再试";
}
3.2 第二步:编写 AOP 切面MyLockAspect
切面负责拦截加了@MyLock的方法,自动执行 “获取锁→执行业务→释放锁” 的逻辑:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
/**
* Redisson分布式锁AOP切面
*/
@Aspect
@Component
public class MyLockAspect implements Ordered {
@Autowired
private RedissonClient redissonClient;
// 切入点:拦截所有加了@MyLock注解的方法
@Pointcut("@annotation(com.yourpackage.lock.MyLock)") // 替换成你的注解全路径
public void lockPointCut() {}
// 环绕通知:在方法执行前后处理锁逻辑
@Around("lockPointCut() && @annotation(myLock)")
public Object around(ProceedingJoinPoint joinPoint, MyLock myLock) throws Throwable {
// 1.解析锁名(这里先简单处理,后面优化支持SPEL)
String lockKey = myLock.name();
RLock lock = redissonClient.getLock(lockKey);
try {
// 2.尝试获取锁
boolean isLocked = lock.tryLock(
myLock.waitTime(),
myLock.leaseTime(),
myLock.unit()
);
if (!isLocked) {
throw new RuntimeException(myLock.failMsg());
}
// 3.获取锁成功,执行业务方法
return joinPoint.proceed();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取锁失败");
} finally {
// 4.释放锁:只释放当前线程持有的锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
/**
* 设置切面优先级:确保锁切面比事务切面先执行(避免事务未提交锁就释放)
* 数值越小,优先级越高(事务切面默认优先级是Ordered.LOW_PRECEDENCE)
*/
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 10;
}
}
3.3 第三步:业务方法直接用注解
之前的receiveCoupon方法瞬间简洁了,只需要加个@MyLock注解,指定锁名即可:
@Service
public class CouponService {
// 用注解替代重复的锁代码!锁名直接写业务标识
@MyLock(name = "coupon:receive:#{#couponId}:#{#userId}", failMsg = "领取太频繁,请10秒后再试")
public void receiveCoupon(Long couponId, Long userId) {
// 业务逻辑不变:校验库存、生成用户优惠券、扣减库存
System.out.println("用户" + userId + "成功领取优惠券" + couponId);
// 你的业务代码...
}
}
优化点说明
- 解耦:锁逻辑和业务逻辑分离,后续修改锁逻辑不需要动业务代码;
- 事务兼容:通过Ordered设置切面优先级,确保锁在事务开始前获取,事务提交后释放(避免 “锁释放了但事务没提交” 导致的并发问题);
- 易用性:默认值减少配置,业务方只需要关注 “锁名” 和 “失败提示”。
4. 优化 2:支持多类型锁 —— 可重入 / 公平 / 读写锁自由切换
Redisson 支持多种锁类型,但上面的切面只支持 “可重入锁”(默认)。实际场景中,我们可能需要:
- 公平锁:按请求顺序获取锁,避免线程饥饿;
- 读写锁:读多写少场景(比如商品详情查询加读锁,更新加写锁),提高并发效率。
解决方案:在注解里加lockType参数,用枚举 + 工厂模式动态创建不同类型的锁。
4.1 第一步:定义锁类型枚举MyLockType
枚举里包含所有支持的锁类型,以及创建锁的方法:
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.api.RReadWriteLock;
/**
* 分布式锁类型枚举
*/
public enum MyLockType {
/** 可重入锁(默认) */
REENTRANT_LOCK {
@Override
public RLock createLock(RedissonClient redissonClient, String lockKey) {
return redissonClient.getLock(lockKey);
}
},
/** 公平锁 */
FAIR_LOCK {
@Override
public RLock createLock(RedissonClient redissonClient, String lockKey) {
return redissonClient.getFairLock(lockKey);
}
},
/** 读锁(配合读写锁使用) */
READ_LOCK {
@Override
public RLock createLock(RedissonClient redissonClient, String lockKey) {
RReadWriteLock rwLock = redissonClient.getReadWriteLock(lockKey);
return rwLock.readLock();
}
},
/** 写锁(配合读写锁使用) */
WRITE_LOCK {
@Override
public RLock createLock(RedissonClient redissonClient, String lockKey) {
RReadWriteLock rwLock = redissonClient.getReadWriteLock(lockKey);
return rwLock.writeLock();
}
};
/**
* 抽象方法:创建对应类型的锁
*/
public abstract RLock createLock(RedissonClient redissonClient, String lockKey);
}
4.2 第二步:更新注解@MyLock,增加lockType参数
public @interface MyLock {
// 原有参数...
/**
* 锁类型(默认可重入锁)
*/
MyLockType lockType() default MyLockType.REENTRANT_LOCK;
}
4.3 第三步:修改切面MyLockAspect,动态创建锁
把 “创建锁” 的逻辑改成用枚举的createLock方法:
@Aspect
@Component
public class MyLockAspect implements Ordered {
@Autowired
private RedissonClient redissonClient;
@Around("lockPointCut() && @annotation(myLock)")
public Object around(ProceedingJoinPoint joinPoint, MyLock myLock) throws Throwable {
// 1.解析锁名(后面优化SPEL)
String lockKey = myLock.name();
// 2.根据锁类型,动态创建锁(核心修改)
RLock lock = myLock.lockType().createLock(redissonClient, lockKey);
try {
// 3.尝试获取锁(逻辑不变)
boolean isLocked = lock.tryLock(
myLock.waitTime(),
myLock.leaseTime(),
myLock.unit()
);
if (!isLocked) {
throw new RuntimeException(myLock.failMsg());
}
// 4.执行业务
return joinPoint.proceed();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取锁失败");
} finally {
// 5.释放锁(逻辑不变)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
// 其他方法...
}
4.4 第四步:业务方法使用不同类型的锁
示例 1:公平锁(比如秒杀场景,避免线程饥饿)
// 秒杀领取优惠券:用公平锁,按请求顺序处理
@MyLock(
name = "coupon:seckill:#{#couponId}:#{#userId}",
lockType = MyLockType.FAIR_LOCK,
failMsg = "秒杀太火爆,请重试"
)
public void seckillCoupon(Long couponId, Long userId) {
// 秒杀业务逻辑:扣减库存、生成订单...
}
示例 2:读写锁(商品详情查询 + 更新)
@Service
public class GoodsService {
// 商品详情查询:加读锁,支持多线程同时读
@MyLock(
name = "goods:info:#{#goodsId}",
lockType = MyLockType.READ_LOCK,
leaseTime = 10 // 读锁过期时间设10秒,避免长期占用
)
public GoodsVO getGoodsInfo(Long goodsId) {
// 查询商品详情(读操作)
return goodsMapper.selectById(goodsId);
}
// 商品库存更新:加写锁,同一时间只能一个线程写
@MyLock(
name = "goods:info:#{#goodsId}",
lockType = MyLockType.WRITE_LOCK,
failMsg = "更新太频繁,请稍后再试"
)
public void updateGoodsStock(Long goodsId, Integer stock) {
// 更新商品库存(写操作)
goodsMapper.updateStock(goodsId, stock);
}
}
5. 优化 3:支持 SPEL 动态锁名 —— 让锁名更灵活
之前的锁名是固定字符串 + 参数(比如coupon:receive:#{#couponId}:#{#userId}),但切面还没解析 SPEL 表达式,直接用会导致锁名变成coupon:receive:#{#couponId}:#{#userId},而不是期望的coupon:receive:123:456。
解决方案:在切面里加入 SPEL 表达式解析逻辑,动态替换锁名中的参数占位符。
5.1 第一步:在切面MyLockAspect中添加 SPEL 解析方法
利用 Spring 的SpelExpressionParser解析 SPEL 表达式,获取方法参数的值:
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.aspectj.lang.reflect.MethodSignature;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Aspect
@Component
public class MyLockAspect implements Ordered {
// SPEL表达式解析器(单例,避免重复创建)
private final ExpressionParser spelParser = new SpelExpressionParser();
// 匹配SPEL表达式的正则:#{...}
private final Pattern spelPattern = Pattern.compile("\\#\\{([^\\}]*)\\}");
// 原有属性和方法...
/**
* 解析锁名中的SPEL表达式,替换为实际参数值
*/
private String parseLockKey(String lockKey, ProceedingJoinPoint joinPoint) {
// 1.如果锁名不含SPEL表达式,直接返回
if (!spelPattern.matcher(lockKey).find()) {
return lockKey;
}
// 2.获取方法签名和参数
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String[] paramNames = methodSignature.getParameterNames(); // 参数名(比如couponId、userId)
Object[] paramValues = joinPoint.getArgs(); // 参数值(比如123、456)
// 3.构建SPEL上下文,把方法参数放入上下文
StandardEvaluationContext context = new StandardEvaluationContext();
for (int i = 0; i < paramNames.length; i++) {
context.setVariable(paramNames[i], paramValues[i]);
}
// 4.匹配并替换所有SPEL表达式
Matcher matcher = spelPattern.matcher(lockKey);
while (matcher.find()) {
// 获取SPEL表达式内容(比如"#couponId")
String spelExpr = matcher.group(1);
// 解析SPEL表达式,得到实际值(比如123)
Object value = spelParser.parseExpression(spelExpr).getValue(context);
// 替换锁名中的占位符
lockKey = lockKey.replace(matcher.group(), value == null ? "" : value.toString());
}
return lockKey;
}
// 更新around方法,调用parseLockKey解析锁名
@Around("lockPointCut() && @annotation(myLock)")
public Object around(ProceedingJoinPoint joinPoint, MyLock myLock) throws Throwable {
// 1.解析锁名(核心修改:加入SPEL解析)
String lockKey = parseLockKey(myLock.name(), joinPoint);
// 2.创建锁(逻辑不变)
RLock lock = myLock.lockType().createLock(redissonClient, lockKey);
// 后续逻辑不变...
}
}
5.2 第二步:业务方法使用 SPEL 动态锁名
现在可以在锁名里用#{#参数名}引用方法参数,甚至支持简单的表达式(比如#{#user.id}):
@Service
public class OrderService {
// 锁名:order:create:用户ID_订单类型(比如order:create:456_NORMAL)
@MyLock(
name = "order:create:#{#userId}_#{#orderType}",
lockType = MyLockType.REENTRANT_LOCK
)
public void createOrder(Long userId, String orderType) {
// 创建订单业务逻辑...
}
// 支持嵌套参数:比如#user.getId()
@MyLock(name = "user:update:#{#user.getId()}")
public void updateUser(User user) {
// 更新用户信息...
}
}
6. 优化 4:支持动态失败策略 —— 灵活应对不同场景
不同业务对 “获取锁失败” 的处理方式不同:
- 秒杀场景:失败后直接提示 “重试”;
- 定时任务场景:失败后跳过执行,避免重复;
- 支付场景:失败后阻塞等待,直到获取锁。
解决方案:在注解里加failStrategy参数,用策略模式动态处理获取锁失败的逻辑。
6.1 第一步:定义失败策略枚举MyLockFailStrategy
枚举里包含不同的失败策略,以及对应的处理方法:
import com.yourpackage.exception.BizException; // 替换成你的业务异常类
/**
* 分布式锁获取失败策略枚举
*/
public enum MyLockFailStrategy {
/** 快速失败(默认):获取不到锁直接抛异常 */
FAIL_FAST {
@Override
public void handleFail(String failMsg) {
throw new BizException(failMsg);
}
},
/** 快速跳过:获取不到锁直接返回null,不抛异常 */
SKIP_FAST {
@Override
public void handleFail(String failMsg) {
// 不抛异常,直接返回null(后续切面会返回null)
}
},
/** 阻塞等待:一直等待直到获取锁(忽略waitTime,慎用!) */
BLOCK_UNTIL_LOCKED {
@Override
public void handleFail(String failMsg) {
// 该策略下,tryLock会一直等待,不会进入这里
}
};
/**
* 抽象方法:处理获取锁失败的逻辑
*/
public abstract void handleFail(String failMsg);
}
6.2 第二步:更新注解和切面,支持失败策略
1. 更新@MyLock注解:
public @interface MyLock {
// 原有参数...
/**
* 获取锁失败策略(默认快速失败)
*/
MyLockFailStrategy failStrategy() default MyLockFailStrategy.FAIL_FAST;
}
2. 修改切面MyLockAspect:
@Around("lockPointCut() && @annotation(myLock)")
public Object around(ProceedingJoinPoint joinPoint, MyLock myLock) throws Throwable {
// 1.解析锁名
String lockKey = parseLockKey(myLock.name(), joinPoint);
// 2.创建锁
RLock lock = myLock.lockType().createLock(redissonClient, lockKey);
try {
// 3.根据失败策略,处理获取锁逻辑
boolean isLocked;
if (myLock.failStrategy() == MyLockFailStrategy.BLOCK_UNTIL_LOCKED) {
// 阻塞等待策略:忽略waitTime,一直等直到获取锁
lock.lock(myLock.leaseTime(), myLock.unit());
isLocked = true;
} else {
// 其他策略:用tryLock获取锁
isLocked = lock.tryLock(
myLock.waitTime(),
myLock.leaseTime(),
myLock.unit()
);
}
// 4.获取锁失败,按策略处理
if (!isLocked) {
myLock.failStrategy().handleFail(myLock.failMsg());
return null; // 快速跳过策略返回null
}
// 5.执行业务
return joinPoint.proceed();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BizException("获取锁被中断");
} finally {
// 6.释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
6.3 第三步:业务方法使用不同失败策略
示例 1:定时任务(获取不到锁跳过执行)
@Service
public class ScheduledTaskService {
// 定时任务:每天凌晨更新统计数据,获取不到锁直接跳过
@Scheduled(cron = "0 0 0 * * ?")
@MyLock(
name = "task:update:statistics",
failStrategy = MyLockFailStrategy.SKIP_FAST,
waitTime = 0 // 不等待,获取不到直接跳过
)
public void updateStatistics() {
// 更新统计数据逻辑...
System.out.println("统计数据更新成功");
}
}
示例 2:支付回调(必须获取锁,阻塞等待)
@Service
public class PayCallbackService {
// 支付回调:必须处理,获取不到锁阻塞等待
@MyLock(
name = "pay:callback:#{#orderId}",
failStrategy = MyLockFailStrategy.BLOCK_UNTIL_LOCKED,
leaseTime = 60 // 锁过期时间60秒
)
public void handlePayCallback(String orderId, String payStatus) {
// 处理支付回调逻辑:更新订单状态、通知用户...
}
}
7. 实战:优惠券领取防超卖(完整案例)
用我们设计的通用分布式锁,解决 “优惠券超卖 / 重复领取” 问题,全程附测试结果。
7.1 业务场景
- 优惠券总数 100 张,每人限领 1 张;
- 模拟 1000 个并发请求,测试是否会超卖或重复领取。
7.2 数据库表设计(简化)
-- 优惠券表
CREATE TABLE `coupon` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '优惠券名称',
`total_num` int NOT NULL COMMENT '总数量',
`used_num` int NOT NULL DEFAULT 0 COMMENT '已使用数量',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 用户优惠券表(防重复领取)
CREATE TABLE `user_coupon` (
`id` bigint NOT NULL AUTO_INCREMENT,
`coupon_id` bigint NOT NULL COMMENT '优惠券ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_coupon_user` (`coupon_id`,`user_id`) COMMENT '唯一索引:避免重复领取'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
7.3 业务代码实现
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class CouponService {
@Autowired
private CouponMapper couponMapper;
@Autowired
private UserCouponMapper userCouponMapper;
/**
* 领取优惠券(防超卖+防重复领取)
* @param couponId 优惠券ID
* @param userId 用户ID
*/
@Transactional
@MyLock(
name = "coupon:receive:#{#couponId}:#{#userId}", // 锁名:优惠券ID+用户ID,确保一人一张
lockType = MyLockType.REENTRANT_LOCK,
waitTime = 5, // 最多等5秒
failMsg = "优惠券领取太火爆,请稍后再试"
)
public void receiveCoupon(Long couponId, Long userId) {
// 1.校验优惠券是否存在且有库存
Coupon coupon = couponMapper.selectById(couponId);
if (coupon == null) {
throw new BizException("优惠券不存在");
}
if (coupon.getUsedNum() >= coupon.getTotalNum()) {
throw new BizException("优惠券已领完");
}
// 2.校验用户是否已领取(配合数据库唯一索引,双重保障)
UserCoupon exist = userCouponMapper.selectByCouponAndUser(couponId, userId);
if (exist != null) {
throw new BizException("你已领取过该优惠券");
}
// 3.扣减优惠券库存(用UPDATE ... WHERE 实现原子扣减,避免超卖)
int rows = couponMapper.incrUsedNum(couponId);
if (rows == 0) {
throw new BizException("优惠券已领完");
}
// 4.生成用户优惠券记录
UserCoupon userCoupon = new UserCoupon();
userCoupon.setCouponId(couponId);
userCoupon.setUserId(userId);
userCouponMapper.insert(userCoupon);
}
}
7.4 并发测试(用 JMeter)
测试配置:
- 线程数:1000 个并发用户;
- 循环次数:1 次;
- 每个用户领取同一个优惠券(ID=1,总库存 100)。
测试结果:
- 数据库coupon.used_num=100(无超卖);
- user_coupon表新增 100 条记录(无重复领取);
- 剩余 900 个请求提示 “领取太火爆” 或 “已领完”(符合预期)。
8. 设计思路复盘:通用组件的 “进化之路”
我们的分布式锁从 “基础使用” 到 “通用组件”,经历了 4 次核心优化,背后是通用组件设计的经典思路:
|
优化阶段 |
解决的问题 |
核心设计模式 |
收益 |
|
基础使用 |
分布式并发问题 |
- |
解决单实例锁局限 |
|
AOP + 注解 |
锁代码重复、业务侵入 |
切面模式 |
解耦,业务方只需加注解 |
|
多类型锁 |
锁类型单一,不支持公平 / 读写锁 |
枚举 + 工厂模式 |
灵活适配不同场景 |
|
SPEL 动态锁名 |
锁名固定,不支持参数动态替换 |
SPEL 解析 |
锁名更灵活,避免硬编码 |
|
动态失败策略 |
失败处理方式单一 |
策略模式 |
适配不同业务的失败逻辑 |
9. 进阶方向:让你的分布式锁更健壮
- Redis 集群下的锁可靠性:
-
- 问题:Redis 主从切换时,主库锁还没同步到从库,主库宕机导致锁丢失;
- 解决方案:使用 Redisson 的RedissonRedLock(红锁),同时在多个 Redis 节点加锁,确保多数节点加锁成功才算成功。
- 锁泄漏防护:
-
- 问题:服务宕机或unlock被跳过,导致锁长期占用;
- 解决方案:即使启用看门狗,也建议设置合理的leaseTime(比如 30 秒),避免极端情况下的锁泄漏。
- 与其他锁方案对比:
-
- Redisson 锁:基于 Redis,性能高,适合高并发场景;
- ZooKeeper 锁:基于 ZAB 协议,可靠性高,适合对一致性要求高的场景(比如分布式事务);
- 选择建议:大多数业务场景优先用 Redisson,一致性要求极高时用 ZooKeeper。
完整代码
注解类(锁的注解添加了方法结束释放锁,其实就是在finally处判断是否需要自动删除)
package com.tianji.promotion.utils;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyLock {
/**
* 加锁key的表达式,支持SPEL表达式
*/
String name();
/**
* 阻塞超时时长,不指定 waitTime 则按照Redisson默认时长
*/
long waitTime() default 1;
/**
* 锁自动释放时长,默认是-1,其实是30秒 + watchDog模式
*/
long leaseTime() default -1;
/**
* 时间单位,默认为秒
*/
TimeUnit unit() default TimeUnit.SECONDS;
/**
* 锁的类型,包括:可重入锁、公平锁、读锁、写锁
*/
MyLockType lockType() default MyLockType.RE_ENTRANT_LOCK;
/**
* 如果设定了false,则方法结束不释放锁,而是等待leaseTime后自动释放
*/
boolean autoUnlock() default true;
/**
* 锁策略,包括5种,默认策略是 不断尝试获取锁,直到成功或超时,超时后抛出异常
*/
MyLockStrategy lockStrategy() default MyLockStrategy.FAIL_AFTER_RETRY_TIMEOUT;
}
枚举类(将工厂代码优化进枚举类代码)
package com.tianji.promotion.utils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
public enum MyLockType {
RE_ENTRANT_LOCK(){
@Override
public RLock getLock(RedissonClient redissonClient, String name) {
return redissonClient.getLock(name);
}
}, // 可重入锁
FAIR_LOCK(){
@Override
public RLock getLock(RedissonClient redissonClient, String name) {
return redissonClient.getFairLock(name);
}
}, // 公平锁
READ_LOCK(){
@Override
public RLock getLock(RedissonClient redissonClient, String name) {
return redissonClient.getReadWriteLock(name).readLock();
}
}, // 读锁
WRITE_LOCK(){
@Override
public RLock getLock(RedissonClient redissonClient, String name) {
return redissonClient.getReadWriteLock(name).writeLock();
}
}, // 写锁
;
public abstract RLock getLock(RedissonClient redissonClient, String name);
}
切面类(添加了spel表达式解析与自动解锁)
核心改动

package com.tianji.promotion.utils;
import com.tianji.common.exceptions.BizIllegalException;
import com.tianji.common.utils.StringUtils;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.Ordered;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import java.lang.reflect.Method;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Component
@Aspect
@RequiredArgsConstructor
public class MyLockAspect implements Ordered {
private final RedissonClient redissonClient;
@Around("@annotation(mylock)")
public Object tryLock(ProceedingJoinPoint pjp, MyLock mylock) throws Throwable {
if (!mylock.autoUnlock() && mylock.leaseTime() <= 0) {
// 不手动释放锁时,必须指定leaseTime时间
throw new BizIllegalException("leaseTime不能为空");
}
// 基于SPEL表达式解析锁的 name(由注解名字转换过来)
String name = getLockName(mylock.name(), pjp);
//创建锁对象
RLock lock = mylock.lockType().getLock(redissonClient, name);
//尝试获取锁
boolean isLock = mylock.lockStrategy().tryLock(lock, mylock);
//判断是否成功
if(!isLock){
//快速失败
return null;
}
try {
// 成功,执行业务逻辑
return pjp.proceed();
}finally {
if (mylock.autoUnlock()) {
lock.unlock();
}
}
}
/**
* SPEL的正则规则
*/
private static final Pattern pattern = Pattern.compile("\\#\\{([^\\}]*)\\}");
/**
* 方法参数解析器
*/
private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
/**
* 解析锁名称
* @param name 原始锁名称
* @param pjp 切入点
* @return 解析后的锁名称
*/
private String getLockName(String name, ProceedingJoinPoint pjp) {
// 1.判断是否存在spel表达式
if (StringUtils.isBlank(name) || !name.contains("#")) {
// 不存在,直接返回
return name;
}
// 2.构建context
EvaluationContext context = new MethodBasedEvaluationContext(
TypedValue.NULL, resolveMethod(pjp), pjp.getArgs(), parameterNameDiscoverer);
// 3.构建解析器
ExpressionParser parser = new SpelExpressionParser();
// 3.循环处理
Matcher matcher = pattern.matcher(name);
while (matcher.find()) {
// 2.1.获取表达式
String tmp = matcher.group();
// 2.2.尝试解析
Expression expression = parser.parseExpression("#" + matcher.group(1));
Object value = expression.getValue(context);
name = name.replace(tmp, ObjectUtils.nullSafeToString(value));
}
return name;
}
private Method resolveMethod(ProceedingJoinPoint pjp) {
// 1.获取方法签名
MethodSignature signature = (MethodSignature)pjp.getSignature();
// 2.获取字节码
Class<?> clazz = pjp.getTarget().getClass();
// 3.方法名称
String name = signature.getName();
// 4.方法参数列表
Class<?>[] parameterTypes = signature.getMethod().getParameterTypes();
return tryGetDeclaredMethod(clazz, name, parameterTypes);
}
private Method tryGetDeclaredMethod(Class<?> clazz, String name, Class<?> ... parameterTypes){
try {
// 5.反射获取方法
return clazz.getDeclaredMethod(name, parameterTypes);
} catch (NoSuchMethodException e) {
Class<?> superClass = clazz.getSuperclass();
if (superClass != null) {
// 尝试从父类寻找
return tryGetDeclaredMethod(superClass, name, parameterTypes);
}
}
return null;
}
@Override
public int getOrder() {
return 0;
}
}
业务类使用(支持动态锁名)

package com.tianji.promotion.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.tianji.common.domain.dto.PageDTO;
import com.tianji.common.exceptions.BadRequestException;
import com.tianji.common.utils.CollUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.promotion.domain.po.ExchangeCode;
import com.tianji.promotion.domain.po.UserCoupon;
import com.tianji.promotion.domain.vo.UserCouponPageVO;
import com.tianji.promotion.enums.CouponStatus;
import com.tianji.promotion.enums.ExchangeCodeStatus;
import com.tianji.promotion.mapper.CouponMapper;
import com.tianji.promotion.mapper.UserCouponMapper;
import com.tianji.promotion.query.UserCouponQuery;
import com.tianji.promotion.service.IExchangeCodeService;
import com.tianji.promotion.service.IUserCouponService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.promotion.utils.CodeUtil;
import com.tianji.promotion.utils.MyLock;
import com.tianji.promotion.utils.MyLockStrategy;
import com.tianji.promotion.utils.MyLockType;
import lombok.RequiredArgsConstructor;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
import com.tianji.promotion.domain.po.Coupon;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* <p>
* 用户领取优惠券的记录,是真正使用的优惠券信息 服务实现类
* </p>
*
* @author goldenfox
* @since 2025-09-23
*/
@Service
@RequiredArgsConstructor
public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCoupon> implements IUserCouponService {
private final CouponMapper couponMapper;
private final IExchangeCodeService codeService;
@Override
public void receiveCoupon(Long id) {
// 参数校验
if(id == null || id <= 0){
throw new BadRequestException("请求优惠券id异常");
}
//用户登录
Long userId = UserContext.getUser();
if(userId == null){
throw new BadRequestException("用户未登录");
}
//查询优惠券状态
Coupon coupon = couponMapper.selectById(id);
if(coupon == null){
throw new BadRequestException("优惠券不存在");
}
if(coupon.getStatus() != CouponStatus.ISSUING){
throw new BadRequestException("优惠券状态异常");
}
if(coupon.getIssueNum() >= coupon.getTotalNum()){
throw new BadRequestException("优惠券已发放完毕");
}
IUserCouponService userCouponService = (IUserCouponService) AopContext.currentProxy();
userCouponService.checkAndCreateUserCoupon(coupon, userId, null);
}
@Transactional
@MyLock(name ="lock:coupon:#{userId}",lockType = MyLockType.RE_ENTRANT_LOCK,lockStrategy = MyLockStrategy.FAIL_AFTER_RETRY_TIMEOUT)
public void checkAndCreateUserCoupon(Coupon coupon, Long userId, Integer serialNum){
// 在“校验优惠券状态”后补充时间校验
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())) {
throw new BadRequestException("优惠券发放已经结束或尚未开始");
}
//查询用户领取的优惠券数量
List<UserCoupon> userCoupons = lambdaQuery()
.eq(UserCoupon::getUserId, userId)
.eq(UserCoupon::getCouponId, coupon.getId())
.list();
if(userCoupons != null && userCoupons.size() >= coupon.getUserLimit()){
throw new RuntimeException("超出领取数量");
}
//更新优惠券已发放数量
int r = couponMapper.incrIssueNum(coupon.getId());
if(r == 0){
throw new BadRequestException("优惠券库存不足");
}
saveUserCoupon(coupon, userId);
//更新兑换码状态
if(serialNum != null){
codeService.lambdaUpdate()
.set(ExchangeCode::getUserId, userId)
.set(ExchangeCode::getStatus, ExchangeCodeStatus.USED)
.eq(ExchangeCode::getId, serialNum)
.update();
}
}
private void saveUserCoupon(Coupon coupon, Long userId) {
// 2.有效期信息
LocalDateTime termBeginTime = coupon.getTermBeginTime();
LocalDateTime termEndTime = coupon.getTermEndTime();
if (termBeginTime == null) {
termBeginTime = LocalDateTime.now();
termEndTime = termBeginTime.plusDays(coupon.getTermDays());
}
//保存用户领取的优惠券
UserCoupon userCoupon = new UserCoupon().setCouponId(coupon.getId())
.setUserId(userId)
.setTermBeginTime(termBeginTime)
.setTermEndTime(termEndTime);
//保存到用户优惠券表中
save(userCoupon);
}
@Override
public void exchangeCoupon(String code) {
//校验解析兑换码
long serialNum = CodeUtil.parseCode(code);
Long userId = UserContext.getUser();
//判断是否已经兑换(redis)
boolean exchanged = true;
synchronized (userId.toString().intern()){
exchanged = codeService.updateExchangeMark(serialNum, true);
}
if(exchanged){
throw new BadRequestException("兑换码已兑换");
}
try {
//查询兑换码
ExchangeCode exchangeCode = codeService.getById(serialNum);
if(exchangeCode == null){
throw new BadRequestException("兑换码不存在");
}
if(!exchangeCode.getExpiredTime().isAfter(LocalDateTime.now())){
throw new BadRequestException("兑换码已过期");
}
// 校验并生成用户券
// 查询优惠券
Coupon coupon = couponMapper.selectById(exchangeCode.getExchangeTargetId());
if(coupon == null){
throw new BadRequestException("优惠券不存在");
}
// 校验并生成用户券,更新兑换码状态
checkAndCreateUserCoupon(coupon, userId, Math.toIntExact(serialNum));
}catch (Exception e){
// 重置兑换的标记 0
codeService.updateExchangeMark(serialNum, false);
throw e;
}
}
@Override
public PageDTO<UserCouponPageVO> queryUserCouponList(UserCouponQuery query) {
Integer status = query.getStatus();
if (status == null) {
throw new BadRequestException("请选择优惠券状态");
}
Long userId = UserContext.getUser();
//分页查询
Page<UserCoupon> page = lambdaQuery()
.eq(UserCoupon::getUserId, userId)
.eq(UserCoupon::getStatus, status)
.page(query.toMpPageDefaultSortByCreateTimeDesc());
//组装vo
List<UserCoupon> records = page.getRecords();
if (CollUtils.isEmpty(records)) {
return PageDTO.empty(page);
}
// 2. 批量查询关联的优惠券模板(避免循环查库,优化性能)
List<Long> couponIds = records.stream()
.map(UserCoupon::getCouponId)
.distinct()
.collect(Collectors.toList());
List<Coupon> coupons = couponMapper.selectBatchIds(couponIds);
// 转为Map:key=优惠券模板ID,value=模板信息,方便快速查询
Map<Long, Coupon> couponMap = coupons.stream()
.collect(Collectors.toMap(Coupon::getId, coupon -> coupon));
// 转换为VO列表
List<UserCouponPageVO> voList = records.stream()
.map(userCoupon -> {
UserCouponPageVO vo = new UserCouponPageVO();
// 复制UserCoupon中直接匹配的属性(如id、termEndTime)
BeanUtil.copyProperties(userCoupon, vo);
// 获取对应的优惠券模板,补充VO的业务属性
Coupon coupon = couponMap.get(userCoupon.getCouponId());
vo.setName(coupon.getName());
vo.setSpecific(coupon.getSpecific());
vo.setDiscountType(coupon.getDiscountType());
vo.setThresholdAmount(coupon.getThresholdAmount());
vo.setDiscountValue(coupon.getDiscountValue());
vo.setMaxDiscountAmount(coupon.getMaxDiscountAmount());
vo.setTermDays(coupon.getTermDays());
return vo;
})
.collect(Collectors.toList());
// 包装分页结果
return PageDTO.of(page, voList);
}
}
10. 结语
分布式锁是 Java 后端开发的 “必考点”,也是高并发系统的 “基石”。本文从 “问题出发”,一步步带你设计通用 Redisson 锁,不仅教你怎么用,更教你 “为什么这么设计”。
如果这篇文章对你有帮助,欢迎点赞 + 收藏 + 关注,后续会分享更多 Java 高并发、分布式系统的实战教程!有问题也可以在评论区留言,一起讨论~
更多推荐
所有评论(0)