SpringBoot使用Redis+SpringEL表达式实现分布式并发锁注解

电玩女神 2023-09-25 11:37 90阅读 0赞



  接SpringBoot使用redis搭配lua脚本实现分布式并发锁一文,我使用redis+lua脚本实现了一个分布式并发锁RedisLockService,本文将对该功能进行扩展,通过注解+aop的形式使其更加方便使用。

  简单说一下我的想法:

  1、定义一个注解RedisLock,有以下3个属性值

  (1)key:锁的key,即redis中的key(这里使用SpringEL表达式进行扩展,使其可以可以实现从入参动态获取key值)

  (2)timeout:为了避免死锁,设计了一个超时时间,单位是秒,默认5分钟也就是300秒

  (3)msg:加锁失败时的异常信息提示语,默认值为“请求已发起,请勿重复操作!”

  2、以注解为切入点,使用aop对需要使用分布式并发锁的方法进行切面拦截

  3、使用线程变量来保存锁的key值等信息

  4、加锁和解锁流程:

  (1)进入注解方法前:加锁,并记录锁的key值等信息

  (2)方法执行结束后或方法抛出异常:解锁

  5、因为能力有限,所以目前注解仅支持单层,即存在多个注解嵌套的情况下,仅最外层注解会生效

  以上就是大概的实现思路,剩下的废话不多说,直接上代码:

  注解代码(SpringEL表达式的使用参考下方代码注释)

  @Target({ElementType.METHOD})

  @Retention(RetentionPolicy.RUNTIME)

  public @interface RedisLock {

  String key();

  long timeout() default 300L;

  String msg() default “请求已发起,请勿重复操作!”;

  }

  aop切面代码(锁的key通过getRedisKey解析SpringEL表达式得出,锁的value值使用uuid)aop切点的部分知识可以参考:SpringBoot使用aop切面记录@Scheduled定时任务开始时间和结束时间

  @Aspect

  @Component

  @Slf4j

  public class RedisLockAspect {

  private ThreadLocal threadLocalKey=new ThreadLocal<>();

  private ThreadLocal threadLocalValue=new ThreadLocal<>();

  private ThreadLocal threadLocalCount=new ThreadLocal<>();

  @Autowired

  private RedisLockService redisLockService;

  @Pointcut(“@annotation(com.example.demo.redis.RedisLock)”)

  public void pointCut() {

  }

  @Before(“pointCut()&&@annotation(redisLock)”)

  public void beforeHandle(JoinPoint joinPoint, RedisLock redisLock) {

  //避免threadLocalCount发生空指针问题,如果是null值设置为0

  if (threadLocalCount.get()==null) {

  threadLocalCount.set(0);

  }

  if (isFirst()) {

  //注解层数+1

  threadLocalCount.set(threadLocalCount.get()+1);

  String key=this.getRedisKey(joinPoint, redisLock);

  String value=Thread.currentThread().getName() +”-“+ UUID.randomUUID();

  long expire=redisLock.timeout();

  boolean lock=redisLockService.lock(key, value, expire);

  if (lock) {

  //加锁成功

  threadLocalKey.set(key);

  threadLocalValue.set(value);

  }else{

  //加锁失败

  throw new RedisLockException(redisLock.msg());

  }

  }else{

  //注解层数+1

  threadLocalCount.set(threadLocalCount.get()+1);

  }

  }

  @AfterReturning(“pointCut()”)

  public void afterHandle(JoinPoint joinPoint) {

  unlock();

  }

  @AfterThrowing(pointcut=”pointCut()”, throwing=”e”)

  public void afterThrowable(JoinPoint joinPoint, Throwable e) {

  unlock();

  }

  private void unlock() {

  //注解层数-1

  threadLocalCount.set(threadLocalCount.get()-1);

  if (isFirst()) {

  //进行解锁操作

  String key=threadLocalKey.get();

  String value=threadLocalValue.get();

  redisLockService.unlock(key, value);

  //进行线程remove操作

  removeThreadLocal();

  }

  }

  private void removeThreadLocal() {

  threadLocalCount.remove();

  threadLocalKey.remove();

  threadLocalValue.remove();

  }

  private boolean isFirst() {

  return threadLocalCount.get()==0;

  }

  private String getRedisKey(JoinPoint joinPoint, RedisLock redisLock) {

  //获取注解上的key

  String key=redisLock.key();

  //使用SpringEL表达式解析注解上的key

  SpelExpressionParser parser=new SpelExpressionParser();

  Expression expression=parser.parseExpression(key);

  //获取方法入参

  Object[] parameterValues=joinPoint.getArgs();

  //获取方法形参

  MethodSignature signature=(MethodSignature)joinPoint.getSignature();

  Method method=signature.getMethod();

  DefaultParameterNameDiscoverer nameDiscoverer=new DefaultParameterNameDiscoverer();

  String[] parameterNames=nameDiscoverer.getParameterNames(method);

  if (parameterNames==null || parameterNames.length==0) {

  //方法没有入参,直接返回注解上的key

  return key;

  }

  //解析表达式

  EvaluationContext eval();

  // 给上下文赋值

  for(int i=0 ; i < parameterNames.length ; i++) {

  eval(parameterNames[i], parameterValues[i]);

  }

  try {

  Object expressionValue=expression.getValue(evaluationContext);

  if (expressionValue !=null && !””.equals(expressionValue.toString())) {

  //返回el解析后的key

  return expressionValue.toString();

  }else{

  //使用注解上的key

  return key;

  }

  } catch (Exception e) {

  //解析失败,默认使用注解上的key

  return key;

  }

  }

  }

  分布式锁工具代码(实现原理参考SpringBoot使用redis搭配lua脚本实现分布式并发锁一文,此处直接引用复制)

  @Component

  public class RedisLockService {

  @Autowired

  private RedisTemplate redisTemplate;

  private final static RedisScript LOCK_LUA_SCRIPT=new DefaultRedisScript<>(

  “if redis.call(“setnx”, KEYS[1], KEYS[2])==1 then return redis.call(“expire”, KEYS[1], KEYS[3]) else return 0 end”

  , Long.class

  );

  private final static Long LOCK_FAIL=0L;

  private final static RedisScript UNLOCK_LUA_SCRIPT=new DefaultRedisScript<>(

  “if redis.call(“get”,KEYS[1])==KEYS[2] then return redis.call(“del”,KEYS[1]) else return -1 end”

  , Long.class

  );

  private final static Long UNLOCK_FAIL=-1L;

  public boolean lock(String key, String value, Long expire){

  if (key==null || value==null || expire==null) {

  return false;

  }

  List keys=Arrays.asList(key, value, expire.toString());

  Long res=redisTemplate.execute(LOCK_LUA_SCRIPT, keys);

  return !LOCK_FAIL.equals(res);

  }

  public boolean unlock(String key, String value){

  if (key==null || value==null) {

  return false;

  }

  List keys=Arrays.asList(key, value);

  Long res=redisTemplate.execute(UNLOCK_LUA_SCRIPT, keys);

  return !UNLOCK_FAIL.equals(res);

  }

  }

  异常类代码

  public class RedisLockException extends RuntimeException {

  public RedisLockException() {

  }

  public RedisLockException(String message) {

  super(message);

  }

  public RedisLockException(String message, Throwable cause) {

  super(message, cause);

  }

  public RedisLockException(Throwable cause) {

  super(cause);

  }

  public RedisLockException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {

  super(message, cause, enableSuppression, writableStackTrace);

  }

  }

  到这里注解已经可以使用了,具体的使用以及一个并发扣库存的样例可以参考:https://gitee.com/lqccan/blog-demo demo14中的测试代码及注释

发表评论

表情:
评论列表 (有 0 条评论,90人围观)

还没有评论,来说两句吧...

相关阅读

    相关 SpringBoot实现分布式

    SpringBoot实现分布式锁 先了解一下线程数: 线程锁 线程锁:主要用来给类,方法,代码加锁,当某个方法或者某块代码使用synchronize关键字来修饰,