种一棵树最好的时间是10年前,其次就是现在,加油!
--by蜡笔小柯南
还在每个方法里重复写着 RLock lock = redissonClient.getLock(key) 这样的模板代码吗?不仅繁琐,还容易忘记释放锁导致死锁。
想象一下,如果能像使用@Transactional一样简单地使用分布式锁,会是怎样的体验?本文将带你实现功能更强大的@DistributeLock注解,支持SpEL表达式等高级特性!
依赖导入
需要导入Redisson、Aspect 等依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.24.3</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.20.1</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.21</version>
</dependency>
自定义注解
创建DistributeLock注解,并配置此注解可以使用的范围,@Target和@Retention是Java中的元注解, @Target说明了注解所能使用的范围,即注解可以用在哪些地方,@Retention用于描述注解的生命周期,即注解在什么范围内有效。
-
@Target: 表示注解可以用在哪里
- ElementType.METHOD: 方法声明,表示用在方法上
- ElementType.TYPE:用于类、接口(包括注解接口)、枚举或记录声明
- ElementType.FIELD:字段声明(包括枚举常量),用于字段上
- ElementType.PARAMETER:形参声明,用于参数上
- ElementType.CONSTRUCTOR:构造声明,用于构造方法上
- ElementType.LOCAL_VARIABLE:局部变量声明,用于局部变量上
- ElementType.ANNOTATION_TYPE:注解接口声明
- ElementType.PACKAGE:包声明
- ElementType.TYPE_PARAMETER:类型参数声明
- ElementType.TYPE_USE:类型的使用
- ElementType.MODULE:模块声明
- ElementType.RECORD_COMPONENT:记录组件
-
@Retention:表述注解的生命周期
- RetentionPolicy.SOURCE:只保留在源文件,编译成class时将丢弃
- CLASS:被保留在class文件中,但在运行时不会保留,即在运行时无法获取,这是默认的生命周期策略
- RUNTIME:不但在class文件中存在,运行时也存在,因此可以通过反射去获取
我们自定义的DistributeLock注解用于方法上,所以将@Target定义为ElementType.METHOD,在运行时需要获取注解的相关信息,所以将@Retention定义为RetentionPolicy.RUNTIME
/**
* 分布式锁注解
* @author 蜡笔小柯南
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributeLock {
/**
* 锁的key
*/
public String key() default DistributeLockConstant.NONE_KEY;
/**
* 锁的使用场景
*/
public String scene();
/**
* key的SPEL表达式
*/
public String keyExpression() default DistributeLockConstant.NONE_KEY;
/**
* 超时时间,毫秒
* 默认情况下不设置超时时间,会自动续期
*/
public int expireTime() default DistributeLockConstant.DEFAULT_EXPIRE_TIME;
/**
* 加锁等待时长,毫秒
* 默认情况下不设置等待时长,会一直等待直到获取到锁
*/
public int waitTime() default DistributeLockConstant.DEFAULT_WAIT_TIME;
}
自定义注解常量类
创建名为DistributeLockConstant的常量类,用于设置默认值
/**
* 分布式锁常量
*
* @author 蜡笔小柯南
*/
public class DistributeLockConstant {
/**
* 默认的key名称
*/
public static final String NONE_KEY = "NONE";
/**
* 默认超时时间,默认值:-1
*/
public static final int DEFAULT_EXPIRE_TIME = -1;
/**
* 默认枷锁等待时间,默认值:-1
*/
public static final int DEFAULT_WAIT_TIME = -1;
}
自定义切面类
创建名为DistributeLockAspect的切面,使得自定义注解生效
/**
* 分布式锁切面
*
* @author 蜡笔小柯南
*/
@Component
@Aspect
public class DistributeLockAspect {
@Autowired
private RedissonClient redissonClient;
@Around("@annotation(com.yogurt.lock.DistributeLock)")
public Object process(ProceedingJoinPoint pjp) throws Exception {
// 获取方法
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
// 获取方法上的注解
DistributeLock distributeLock = method.getAnnotation(DistributeLock.class);
// key 先尝试获取key,如果key有值,则使用key;如果key是NONE,则尝试获取keyExpression
String key = distributeLock.key();
if (DistributeLockConstant.NONE_KEY.equals(key)) {
// SpEL表达式
String keyExpression = distributeLock.keyExpression();
// 如果keyExpression是NONE,则抛出异常,提示错误信息
if (DistributeLockConstant.NONE_KEY.equals(keyExpression)) {
throw new RuntimeException("key和keyExpression不能同时为空");
}
EvaluationContext context = new StandardEvaluationContext();
// 获取运行时参数的名称
DefaultParameterNameDiscoverer discoverer
= new DefaultParameterNameDiscoverer();
// 获取参数名称
String[] parameterNames = discoverer.getParameterNames(method);
// 将参数绑定到context中
if (parameterNames != null) {
// 获取参数值,如果参数是对象,则获取的是对象的地址
Object[] args = pjp.getArgs();
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}
}
// 解析SpEL表达式
SpelExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(keyExpression);
// 解析表达式,获取结果
key = expression.getValue(context, String.class);
}
// 获取锁的场景
String scene = distributeLock.scene();
// 锁的key,锁的场景+key
String lockKey = scene + "#" + key;
// 锁的过期时间
int expireTime = distributeLock.expireTime();
// 锁的等待时间
int waitTime = distributeLock.waitTime();
// 获取锁
RLock rLock= redissonClient.getLock(lockKey);
Object result = null;
try {
// 加锁是否成功标识 true:成功 false:失败
boolean lockFlag = false;
// 如果等待时间是默认值,说明没有设置等待时间
if (waitTime == DistributeLockConstant.DEFAULT_WAIT_TIME) {
// 锁的过期时间是默认值,说明没有设置锁的过期时间
if (expireTime == DistributeLockConstant.DEFAULT_EXPIRE_TIME) {
// 加锁,无任何参数
rLock.lock();
} else {
// 说明锁的过期时间不是默认值,则设置锁的过期时间
rLock.lock(expireTime, TimeUnit.MILLISECONDS);
}
lockFlag = true;
} else {
// 等待时间不是默认值,说明设置了等待时间,继续判断锁的过期时间是否是默认值
// 过期时间如果是默认值,则只设置锁的等待时间
if (expireTime == DistributeLockConstant.DEFAULT_EXPIRE_TIME) {
lockFlag = rLock.tryLock(waitTime, TimeUnit.MILLISECONDS);
} else {
// 锁的过期时间不是默认值,则设置锁的等待时间和锁的过期时间
lockFlag = rLock.tryLock(waitTime, expireTime, TimeUnit.MILLISECONDS);
}
}
if (!lockFlag) {
throw new RuntimeException("获取锁失败... key : " + lockKey);
}
// 实际方法执行
result = pjp.proceed();
} catch (Throwable e) {
throw new Exception(e);
} finally {
if (rLock.isHeldByCurrentThread()) {
// 释放锁
rLock.unlock();
}
}
return result;
}
}
使用@Around环绕通知的方式,其中@Around注解中,使用@annotation(com.yogurt.mp.lock.DistributeLock),@annotation中指定我们自定义注解的全限定类名,如:我的在com.yogurt.mp.lock包下。
表示我们这个AOP切面,在标注了@DistributeLock注解的情况下,才会生效。
整个AOP切面的作用是,在实际方法执行之前,先去获取锁,成功获取到锁后,再调用pjp.proceed()执行实际的方法,在finally块中释放锁,最终返回数据。
测试
使用key的方式
创建Controller,名为GoodsController
/**
* @author 蜡笔小柯南
*/
@RestController
@RequestMapping("/goods")
public class GoodsController {
@PostMapping("/test")
@DistributeLock(scene = "goods", key = "1001")
public void test(@RequestBody Goods goods) {
System.out.println("商品ID:" + goods.getId());
System.out.println("商品名称:" + goods.getGoodsName());
System.out.println("商品数量:" + goods.getGoodsCount());
System.out.println("商品价格:" + goods.getPrice());
}
}
使用@DistributeLock注解,设置scene和key
测试数据入参:
{
"goodsName": "橙子",
"goodsCount": 1,
"price": "3"
}
以debug的方式启动,在redis中会以Hash的形式保存一个键值对,键为:goods#1001,即scene + # + key,在注解中,scene设置的是goods,key设置的是1001,所以最终保存到redis中的key就是goods#1001。

使用key的方式,我们发现key的值,是在标注注解的时候,就写死的,如:@DistributeLock(scene = "goods", key = "1001"),可以看到,key被固定写成了1001,如果所有的请求需要获取同一把锁,只有获取成功才能继续后续处理,这种方式可以实现。
而如果我们需要根据商品的id来锁,比如:多个请求都是要访问商品id为1的数据,要为商品id为1的加锁,谁获取锁成功,谁能继续执行后续逻辑,其余的请求等待锁的释放。这时,一个新请求要访问商品id为2的数据,商品id为2的数据从来没有被访问过,那么这个新的请求就能成功获取到锁,进行执行后续逻辑。
如果使用固定key的方式,就是不管访问哪个商品的数据,都要去竞争同一把锁,即便是请求1要访问商品id为1的数据,请求2要访问商品id为2的数据,在请求1获取锁成功还未释放锁时,请求2只能等待锁的释放。
接下来,使用SpEL表达式的方式实现!
Spel表达式方式
将注解中的key替换为keyExpression
@PostMapping("/test")
@DistributeLock(scene = "goods", keyExpression = "#goods.id")
public void test(@RequestBody Goods goods) {
System.out.println("商品ID:" + goods.getId());
System.out.println("商品名称:" + goods.getGoodsName());
System.out.println("商品数量:" + goods.getGoodsCount());
System.out.println("商品价格:" + goods.getPrice());
}
#是SpEL表达式的固定写法,goods.id 表示取 goods 对象中的 id 属性值
goods需要和形参列表中的参数名称保持一致,#goods.id是通过get方法来获取的,要保证对象中有对应属性的get方法
如:
- #goods.id:需要有
getId()方法 - #goods.goodsName:需要有
getGoodsName()方法
测试数据入参,添加id字段
{
"id": 1,
"goodsName": "菠萝",
"goodsCount": 20,
"price": "66"
}
我们看redis中保存的键值对,key是:goods#1,入参的参数中,id的值为1,所以拼接好最后的key就是goods#1

如果将入参中id的值修改为2,我们再看redis中是如何保存的
{
"id": 2,
"goodsName": "榴莲",
"goodsCount": 20,
"price": "666"
}
可以看到,key是:goods#2,根据我们传入id参数的不同,能够动态的改变

控制台输出结果
商品ID:2
商品名称:榴莲
商品数量:20
商品价格:666
两种方式的区别
key的方式:
如果需要获取同一把锁,则可以使用固定key的方式
SpEL表达式方式:
如果需要根据参数的不同,获取不同的锁,可以使用SpEL表达式的方式
现在,你的武器库里又多了一件神器。@DistributedLock注解的强大超乎你的想象!
不管在任何时候,我希望你永远不要害怕挑战,不要畏惧失败。每一个错误都是向成功迈出的一步,每一个挑战都是成长的机会,因为每一次的努力,都会使我们离梦想更近一点。只要你行动起来,任何时候都不算晚。最后,把座右铭送给大家:种一棵树最好的时间是10年前,其次就是现在,加油!共勉 💪。
1205

被折叠的 条评论
为什么被折叠?



