一、背景介绍
随着高并发场景的逐渐增多,各类系统中(尤其是类似秒杀系统这种对于并发度要求很高的场景下)对于分布式锁的需求逐渐增大。
而在分布式锁的代码实现中,往往会存在如下情况
public void method(){
// 业务代码
try{
boolean b = redisClient.tryLock(key,value);
if(b){
// ......逻辑判断
}
// 获取锁后所要执行的业务代码
}catch(Exception e){
log.error("error");
throw new Exception();
}finally{
if(redisClient.get(key).equals(value)){
redisClient.tryRelease(key);
}
}
// 业务代码
}
大概写了段比较简单的代码,意思是在很多场景下,都是这样子在实现分布式锁的,具体redisClient是怎么实现的这个根据情况可以自己选择。
这段代码的问题在于,如果需要分布式锁的场景太多,那么整个系统中就会处处都存在这样的try catch finally的代码块,这样会使得整体代码显得十分臃肿,重复代码太多
那么怎么去解决这种使得代码过于臃肿的问题呢?
答案很简单:使用AOP技术,将加入分布式锁的部分以切面的方式剥离出业务代码中
具体怎么实现呢?
二、分布式锁注解+切面的简单实现
首先我们实现一个注解,注解命名建议要直观,让人可以一眼看出该注解的作用
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RedisDistributedLock {
// 锁定的资源的值,即存入redis的key
String key() default "default_key";
// 锁保持时间(以毫秒为单位)
int keepMills() default 1 * 10 * 1000;
}
关于自定义注解的方式,本文不再赘述,有兴趣的可以自行去了解,
我们希望使用 @RedisDistributedLock
这个注解去实现分布式锁,即如下所示。
@RedisDistributedLock(key = "key_example", keepMills = 2*10*1000)
public void method(){
// 业务代码
}
即我们想在执行method方法之前,可以对 key_example
加锁,如果不设置key,则默认为default_key,释放锁的时间为20秒,如果不加设置,则默认为10秒
那么具体AOP切面的简单实现如下
@Aspect
@Component
@Slf4j
public class RedisDistributedLockAspect {
@Autowired
private RedisClient redisClient;
@Around(value = "@annotation(redisDistributedLock)")
public Object invoke(ProceedingJoinPoint point) throws Throwable {
Signature signature = point.getSignature();
MethodSignature methodSignature = (MethodSignature)signature;
Method targetMethod = methodSignature.getMethod();
//获取方法上的注解
RedisDistributedLock redisLock=targetMethod.getAnnotation(RedisDistributedLock.class);
String key=redisLock.key();
if(StringUtils.isEmpty(key)){
key=point.getTarget().getClass().getName()+"."+targetMethod.getName();
}
Boolean isLocked = redisClient.tryLock(key, "true", redisLock.keepMills());
if (!isLocked) {
logger.debug("get lock failed : " + key);
return null;
}
logger.debug("get lock success : " + key);
try {
return proceedingJoinPoint.proceed();
} catch (Exception e) {
logger.error("execute locked method occured an exception", e);
} finally {
if (redisClient.get(key, String.class).equals(value)) {
redisClient.tryRelease(key);
}
}
return null;
}
}
三、动态的方法入参作为分布式锁的key
那么通过这种方式就可以实现对于method()
方法上锁了,但是这个样还是有点不太方便
如果我想对于 method()
方法传入的参数加锁而不是只能写死一个key该怎么办
如下:
@RedisDistributedLock(key = "param", keepMills = 2*10*1000)
public void method(String param){
// 业务代码
}
如果我们可以动态的对于方法中传入的参数加锁,那么这个注解使用起来就更加的方便了
此处我们可以使用Spel表达式来进行处理,关于SPel表达式,请各位自己去查询资料,我这里只给出了最终的代码
@Aspect
@Component
@Slf4j
public class RedisDistributedLockAspect {
@Autowired
private RedisClient redisClient;
private static Logger logger = LoggerFactory.getLogger(RedisDistributedLockAspect.class);
@Around(value = "@annotation(redisDistributedLock)")
public Object invoke(ProceedingJoinPoint proceedingJoinPoint, RedisDistributedLock redisDistributedLock) throws Throwable {
String key = redisDistributedLock.key();
Object[] arg = proceedingJoinPoint.getArgs();
Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod();
// 获取被拦截方法参数名列表(使用Spring支持类库)
LocalVariableTableParameterNameDiscoverer localVariableTable = new LocalVariableTableParameterNameDiscoverer();
String[] parameterNames = localVariableTable.getParameterNames(method);
// 使用SPEL进行key的解析
ExpressionParser parser = new SpelExpressionParser();
// SPEL上下文
StandardEvaluationContext context = new StandardEvaluationContext();
// 把方法参数放入SPEL上下文中
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], arg[i]);
}
// 使用变量方式传入业务动态数据
if (valueOfKey.matches("^#.*.$")) {
valueOfKey = parser.parseExpression(key).getValue(context, String.class);
}
// 使用UUID生成随机数作为value,避免出现锁被错误释放的问题
String value = UUID.randomUUID().toString();
Boolean isLocked = redisClient.tryLock(key, value, redisDistributedLock.keepMills());
if (!isLocked) {
logger.debug("get lock failed : " + key);
return null;
}
logger.debug("get lock success : " + key);
try {
return proceedingJoinPoint.proceed();
} catch (Exception e) {
logger.error("execute locked method occured an exception", e);
} finally {
if (redisClient.get(key, String.class).equals(value)) {
redisClient.tryRelease(key);
}
}
return null;
}
}
这样子的话,在方法中只要以如下的方式,就可以对于方法中传入的参数加锁了
@RedisDistributedLock(key = "#param", keepMills = 2*10*1000)
public void method(String param){
// 业务代码
}