写个分布式锁的aop

写在开头的话:

       本篇涉及到AspectJ,springAop,springel表达式,redis分布式锁使用,授人以鱼不如授人以渔,我写的不一定对,有错的地方欢迎指正,一起共同进步呗。

 

进入主题,老规矩pom文件如下

        <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjweaver</artifactId>
			<version>1.9.4</version>
		</dependency>
		<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
		</dependency>
		<dependency>
			<groupId>org.redisson</groupId>
			<artifactId>redisson</artifactId>
			<version>3.7.2</version>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>

为啥想到写这篇博客呢,其实是有原因的,贴个代码你们感受一下

@Override
    public BaseResponse wipPrepareConfirm(RequestParameter<WipPrepareConfirmedListParams> params) {

        SourceMaterialLock lock = new SourceMaterialLock(params.getBody().getBatches().get(0).getActivityId(), LoginInfoUtils.getTenantId());
        redisLockService.lockElseThrowError(lock);
        try {
            ResultInfo<Long> resultInfo = workOrderMaterialService.appConfirmWipPrepare(params.getBody());
            if (!resultInfo.isRight()) {
                return new BaseResponse(ResponseCodeEnum.FORBIDDEN, resultInfo.getMessage());
            }
        } finally {
            redisLockService.unLock(lock);
        }
        return new BaseResponse(ResponseCodeEnum.SUCCESS, "success");
    }


@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class SourceMaterialLock extends RedissionLock {

    private static final String REDISSION_LOCK_KEY_WORK_ORDER_LOCK = "REDISSION_LOCK_KEY_WORK_ORDER_LOCK";

    private Long key;

    public SourceMaterialLock(Long key, String tenantId) {
        super(tenantId);
        this.key = key;
    }

    @Override
    public String buildLockKey() {
        return  REDISSION_LOCK_KEY_WORK_ORDER_LOCK + Constants.LOCK_KEY_SEPARATOR + this.key + Constants.LOCK_KEY_SEPARATOR + super.getTenantId();
    }
}

这是他们之前封装出来的锁,写死的释放时间,写死的默认的等待锁时间,而且可以看到,每个不同的方法因为参数不同,所以所需要写出来的锁都需要新建一个类,这种不但代码不美观,而且会有类爆炸(锁的类太多了)的影响,那么怎么办呢?其实很简单,aop的思想,我写一个切面,在需要加锁的方法上切一下不就ok了么?

     那么我们先分析一下我们的已知内容和需求,首先:

1. 已知redisson这是一个封装了redis的一个比较高级应用的jar包,它已经实现了redis分布式锁。

2.每个需要加锁的方法中,参数不一定是一样的吧,那么对应生成的锁可能也是不一样的吧,这个怎么处理呢。

3.切面的规则要怎么定,每个人的习惯可能都是不大一样的吧!

4.最最重要的一点,切面怎么写?需要些什么?

 

那么我们先从上述第4点开始吧!怎么写?最好的选择当然是看spring的官网啦!

https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#aop-introduction-defn

5.Aspect Oriented programming with spring  ,spring的面向切面编程,多的我就不说了,没用过aop,总听过很多概念了,那么接下来就直入主题了,怎么用!

 

5.4.1

官网案例,那我们也很简单,照着做即可

5.4.2声明一个切面

声明完切面当然是声明切点了,但是官网给了那么多的例子,我们选择哪种呢?这里就不一一解释了,笔者选择

@annotation 是笔者最近几年用的比较多了,还是更习惯注解一些,而且注解也比较的灵活

5.4.4中的案例告诉了我们怎么使用@annotation

那接着我们直接来到5.4.7,官方网站提供了一个案例供给我们参考,那么准备工作就差不多了,照葫芦画瓢开搞

@SpringBootApplication
@EnableAspectJAutoProxy
public class AppManufactureApplication {

   public static void main(String[] args) {
        SpringApplication.run(AppManufactureApplication.class, args);
   }
}


@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLock {
    //以# + 参数名  例如 #param
    String key();

    int waitTime() default 1;
    //默认锁600秒
    int lockTime() default 600;

    boolean isLog() default false;

    boolean ischeck() default false;

}


@Aspect
@Component
@Slf4j
public class RedisLockAspect {

    @Resource
    private RedissonClient redisson;

    @Around( value ="@annotation(rLock)")
    public Object doAround(ProceedingJoinPoint pjp, RedisLock rLock){
        System.out.println("123");
        Object obj = pjp.proceed();
        System.out.println("321");
        return obj;
    }
}

来,找个方法测一测看看能不能拦截到,写个简单controller

@RestController
@PostMapping("/api/test")
public class TestController {
    @RedisLock(key = "#param",ischeck = true)
    @RequestMapping("/aop")
    public Object appTest(RequestParameter<TestParams> param) {

        System.out.println("3456");
        return "";
    }
}

 

好的,看起来没什么问题。

那么接下来,其实可以看到我的controller里面有一个稍微封装了的一个参数,既然要做分布式的锁,那么就得把这次请求的参数的信息给拿过来,怎么拿呢,其实注释上已经有了,用 "#" + 参数名的形式,这个并非是我定义的,而是springel表达式,还是官网的,但是这次我们来到4.3.10  

通过这样的方式就可以进行变量的赋值,这里还有个官方的案例

// create an array of integers
List<Integer> primes = new ArrayList<Integer>();
primes.addAll(Arrays.asList(2,3,5,7,11,13,17));

// create parser and set variable 'primes' as the array of integers
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataAccess();
context.setVariable("primes", primes);

// all prime numbers > 10 from the list (using selection ?{...})
// evaluates to [11, 13, 17]
List<Integer> primesGreaterThanTen = (List<Integer>) parser.parseExpression(
        "#primes.?[#this>10]").getValue(context);

 

那么接下来其实就是一系列业务代码的实现了

画瓢,第一步,照着官网案例先写一个springel表达式的一个解析

public class Parser {

    private static ExpressionParser parser =  new SpelExpressionParser();
    public static RequestParameter getKey(String key, String[] paramNames, Object[] args){

        Expression exp = parser.parseExpression(key);
        EvaluationContext context = new StandardEvaluationContext();
        if(args.length <= 0){
            return null;
        }
        for (int i = 0;i < args.length; i++){
            context.setVariable(paramNames[i],args[i]);
        }
        return exp.getValue(context, RequestParameter.class);
    }


}

这个主要做的就是我们传入的param其实是个对象,#param与之匹配,怎么拿到传入controller参数中的param对象,就需要用这种方法了。

private String getKey(String key, ProceedingJoinPoint pjp) {
        Method method = ((MethodSignature) pjp.getSignature()).getMethod();
        Parameter[] parameters = method.getParameters();
        String[] parameterNames = new String[parameters.length];
        for (int i = 0; i <parameters.length ; i++) {
            parameterNames[i] = parameters[i].getName();
        }
        RequestParameter parameter = Parser.getKey(key, parameterNames, pjp.getArgs());
        assert parameter != null;
        Object body = parameter.getBody();
        Class<?> aClass = body.getClass();
        //这个判断下是否属于该类,比如impl了这个类,那这里判断就会使true
        if(aClass.isAssignableFrom(IRedisLock.class)){
            //userId --> 这里用一个唯一值就好
            return  ((IRedisLock)body).getLock() + "-" + "userId";
        }
        return "";
    }

这个方法其实也很简单,就是进行了动态获取一个锁的key

之后总的Aspect类的写法就改成了

//这里需要自己去注入一个Bean
    @Resource
    private RedissonClient redisson;

    @Around( value ="@annotation(rLock)")
    public Object doAround(ProceedingJoinPoint pjp, RedisLock rLock) throws Throwable {

        String key = getKey(rLock.key(),pjp);
        if(rLock.isLog()){
            log.info("得到锁:{}",key);
        }
        RLock lock = null;
        Object proceed = null;
        try {
            lock = redisson.getLock(key);
            boolean b = lock.tryLock(rLock.waitTime(), rLock.lockTime(), TimeUnit.SECONDS);
            if(!b){
                throw new RuntimeException("加锁失败");
            }
            if(rLock.isLog()){
                log.info("加锁成功");
            }
            proceed= pjp.proceed();
        }catch (Exception e){
            if(rLock.isLog()){
                log.error("加锁出错,错误信息:{}",e.getMessage());
            }
        }
        finally {
            if(lock != null) {
                try {
                    //redisson 会去判断当前线程是不是你加锁的线程,如果不是会抛出错误
                    lock.unlock();
                }catch (Exception e){

                }
                if(!lock.isLocked() && rLock.isLog()){
                    log.info("解锁成功");
                }
            }
        }
        return proceed;
    }

来吧,测试走一波,为了能测出效果,我会在controller的调用方法处睡个3s,来模拟业务代码的运行

可以看到,只答应了一行3456,所以测试到此结束。

这样写的优势呢,比如说以前controller要写检验参数的的代码,要写锁的代码,还需要打印日志,最关键的还是需要多增加一个类,还有写死的锁时间,这样一个注解会显得代码不但干净,类不会爆炸,还能根据实际情况去设定锁的一个合理超时时间。

问题当时是存在的,比如说如果是同一个用户点击两次会不会就导致解锁了(你看出来了么)。一个方法所做的事情是不是太多了,又要检验方法(isCheck),又要加锁,还要打印日志,并不符合设计类的单一职责。行吧,到此结束,有问题希望能得到大佬的指正。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值