在使用redis的setnx指令过程中,需要在每一个用到的地方手动加锁,手动释放锁,接下来介绍使用aop和redis实现一个轻量级的分布式锁。

新建两个注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface RedisLock {
    /**
     * 超时时间
     *
     * @return
     */
    int timeoutSecond() default 10;
}


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface RedisLockParam {

}

aop拦截器实现

@Slf4j
@Aspect
@Component
public class ReidsLockInterceptor {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Pointcut("@annotation(com.example.ent.corecommon.annotation.RedisLock)")
    public void redislock() {
    }

    @Before("redislock()")
    public void nxlock(JoinPoint joinPoint) throws Throwable {
        String className = joinPoint.getSignature().getDeclaringType().getName();
        String methodName = joinPoint.getSignature().getName();

        Object[] params = joinPoint.getArgs();

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        RedisLock redisLock = method.getAnnotation(RedisLock.class);
        //参数注解,二维数组
        Annotation[][] annotations = method.getParameterAnnotations();
        for (int i = 0; i < annotations.length; i++) {
            //获取参数上的所有注解
            Annotation[] annotationArray = annotations[i];
            for (Annotation annotation : annotationArray) {
                if (annotation instanceof RedisLockParam) {
                    // 有标记为redislockParam
                    nxLock(className, methodName, params[i], redisLock.timeoutSecond());
                    return;
                }
            }
        }
        //没有标记参数
        nxLock(className, methodName, null, redisLock.timeoutSecond());
    }

    @After("redislock()")
    public void nxUnLock(JoinPoint joinPoint) {
        HttpServletRequest request = request();
        String lockKey = (String) request.getAttribute("lockKey");
        String lockId = (String) request.getAttribute("lockId");
        if (!StringUtils.isEmpty(lockKey) && !StringUtils.isEmpty(lockId)) {
            ValueOperations<String, String> stringStringValueOperations = stringRedisTemplate.opsForValue();
            if (lockId.equals(stringStringValueOperations.get(lockKey))) {
                stringRedisTemplate.delete(lockKey);
            }
        }
    }

    /**
     * 执行nxlock 锁
     *
     * @param className  类名
     * @param methodName 方法名
     * @param param      参数
     */
    public void nxLock(String className, String methodName, Object param, int timeout) {
        //生成redis key
        String hashMd5 = HashTookit.hashMd5(className + methodName);
        String lockKey = "lock:" + hashMd5 + (param == null ? "" : ":" + param.toString());

        String lockId = IdTookit.uuid();
        ValueOperations<String, String> stringStringValueOperations = stringRedisTemplate.opsForValue();
        Boolean nxlockState = stringStringValueOperations.setIfAbsent(lockKey, lockId, timeout, TimeUnit.SECONDS);
        if (nxlockState) {
            //加锁成功
            request().setAttribute("lockKey", lockKey);
            request().setAttribute("lockId", lockId);
            log.info("nx lock success lockkey :[{}]  lockId:[{}]", lockKey, lockId);
        } else {
            //获取锁失败,抛出自定义异常,注:理应在统一异常处理器中处理该异常
            throw new CustomException("系统繁忙,请尝试稍后重试!");
        }
    }

    public HttpServletRequest request() {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        return requestAttributes.getRequest();
    }
}

使用方法,可以在controller、service等地方使用

@RedisLock
@PostMapping("product_sell")
public String productSell(@RedisLockParam String productId) {
    
    return "SUCCESS";
}