自定义注解+AOP实现redis分布式锁

最近项目中用到比较多的redis分布式锁

每个方法都类似于这样

String key = "";

//尝试加锁
if (! jedisManager.tryLock(key)) {
    throw new BizException("请稍后重试");
}

try {

    //do your biz

}

catch (Exception e) {
    throw e;
}

finally {
    //释放锁
    jedisManager.release(key);
}

非常的麻烦,而且每个人有每个人的写法。所以,决定将分布式锁与业务进行分离,便于我们以后后续开发

我们需要定义一个分布式锁注解(RedisLock),分布式锁aop,分布式锁对象基类(LockDomian)

RedisLock

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RedisLock {

	RedisLocKeyEnum bizKey();
	
	/**
	* 默认毫秒
	* @return
	*/
	int expire() default 15000;
	
	/**
	* 默认给前端的提示
	* @return
	*/
	String errorMsg() default "请稍后重试";

}

LockDomian

@Slf4j
public class LockDomain {

    public static String KEY = "model = [%s], logKey = [%s] : [%s]";

    public String redisKey() {
        throw new BizException("请重写你的分布式锁对象的redisKey方法");
    }

    /**
     * 建议继承的类重写这个方法 方便日志查找
     * @return
     */
    public String logKey() {
        return String.valueOf(this.hashCode());
    }

    public void tryLockSucLog(MutexModelEnum model) {

        log.info(String.format(KEY, model.getCode(), this.logKey(), "获取锁成功"));

    }

    public void tryLockFaildLog(MutexModelEnum model) {

        log.info(String.format(KEY, model.getCode(), this.logKey(), "获取锁失败"));

    }

    public void releaseLog(MutexModelEnum model) {

        log.info(String.format(KEY, model.getCode(), this.logKey(), "释放锁成功"));

    }

    public void bizError(MutexModelEnum model) {

        log.info(String.format(KEY, model.getCode(), this.logKey(), "业务异常"));

    }

}

RedisLockAspect

@Slf4j
@Order(1)
@Aspect
@Component
public class RedisLockAspect {

    @Autowired
    private JedisComponent jedisComponent;

    @Resource
    private Validator validator;

    @Around("@annotation(com.csy.core.aop.RedisLock)")
    public Result around(ProceedingJoinPoint joinPoint) {

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Method targetMethod = AopUtils.getMostSpecificMethod(method, joinPoint.getTarget().getClass());

        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            String message = BeanValidators.validateWithErrorMessage(validator, arg);
            if (StringUtils.isNotBlank(message)) {
                return Result.wrapErrorResult(message);
            }
        }

        RedisLock redisLock = AnnotationUtils.findAnnotation(targetMethod, RedisLock.class);
        if (redisLock == null) {
            return Data.wrapErrorResult("框架异常");
        }
        if (! (args[0] instanceof LockDomain)) {
            return Data.wrapErrorResult("请继承LockDomain");
        }

        LockDomain lockObj = (LockDomain) args[0];
        String key = redisLock.bizKey().getCode() + "_" + lockObj.redisKey();

        try {

            if (! jedisComponent.tryLock(key, redisLock.expire())) {

                lockObj.tryLockFaildLog(redisLock.bizKey());
                return Result.wrapErrorResult(redisLock.errorMsg());
            }

            lockObj.tryLockSucLog(redisLock.bizKey());
            return joinPoint.proceed();
        }

        catch (Throwable e) {

            lockObj.bizError(redisLock.bizKey());

            //参数异常捕获
            if (e instanceof ParamException) {
                log.error("ParamException:", e);
                ParamException paramException = (ParamException) e;
                return Data.wrapErrorResult(paramException.getError().getErrorCode(), paramException.getError().getErrorMsg());
            }

            //自定义异常捕获
            if (e instanceof BizException) {
                log.error("BizException", e);
                BizException bizException = (BizException) e;
                return Data.wrapErrorResult(bizException.getError().getErrorCode(), bizException.getError().getErrorMsg());
            }

            log.error("系统异常:", e);

            return Result.wrapErrorResult(ErrorCode.SERVER_ERROR);
        }

        finally {

            lockObj.releaseLog(redisLock.bizKey());
            jedisComponent.delKey(key);

        }
    }

}

分布式锁业务实现

public class RedisLockDemo {

    @RedisLock(bizKey = MutexModelEnum.TEST)
    public Result<Boolean> RedisLockTest(RedisLockTestRequest request) {

        //do your biz

        return Result.success(true);
    }

    @Getter
    @Setter
    public class RedisLockTestRequest extends LockDomain {

        private Long userId;

        private String userName;

        /**
         * 以userId作为分布式锁的key
         * @return
         */
        @Override
        public String redisKey() {
            return String.valueOf(this.userId);
        }

        @Override
        public String logKey() {
            return String.valueOf(this.userId);
        }
        
    }
}

可以看到。我们只要在方法上加上@RedisLock,指定锁的Model,再对入参继承LockDomain,指定redisKey和logKey就行。

注意事项

aop相关问题

有的同学可能想降低锁的粒度或者单纯的想抽出一个方法。比如:

public void a(RedisLockTestRequest request) {
    this.RedisLockTest(request);
}

@RedisLock(bizKey = MutexModelEnum.TEST)
public Result<Boolean> RedisLockTest(RedisLockTestRequest request) {

    //do your biz

    return Result.success(true);
}

这种情况下,外部调用a方法,aop是不起作用的

因为aop是运行时织入,获取调用的目标方法(也就是a),判断是否是切点,再匹配切面。内部方法(不是目标方法)不进行织入。

如何解决上述情况?
我们使用AopContext

public void a(RedisLockTestRequest request) {
    RedisLockDemo currentProxy = (RedisLockDemo) AopContext.currentProxy();
    currentProxy.RedisLockTest(request);
}

@RedisLock(bizKey = MutexModelEnum.TEST)
public Result<Boolean> RedisLockTest(RedisLockTestRequest request) {

    //do your biz

    return Result.success(true);
}

通过AopContext获取到当前的代理类,然后调用。

上述涉及到的aop原理我会后续出个专门的aop浅析。

分布式锁相关问题

可能网上有一些分布式锁它是先setnx 然后 expire。其实是有问题的。因为它不是一个原子操作。
我们应该使用Jedis类下的set方法。一步设置值和过期时间

public String set(final String key, final String value, final String nxxx, final String expx, final long time) {
    checkIsInMulti();
    client.set(key, value, nxxx, expx, time);
    return client.getStatusCodeReply();
}
posted @ 2019-05-07 14:18  chenshengyue  阅读(3017)  评论(0编辑  收藏  举报