自定义注解——原生分布式锁,Redis + lua脚本实现

DBC 263 0

前言

温馨提示

有些项目并没有集成Redisson(看门狗),所以有可能会需要用到原生的Redis实现:redisTemplate

定义自定义注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLock {
    String key() default ""; // Redis锁的键,使用占位符表示动态参数
    String paramName() default ""; // 指定用作动态键的参数名
    long expireSeconds() default 15; // 过期时间,单位秒
}

定义环绕方法

@Aspect
@Component
public class RedisLockAspect {

    // 注入RedisTemplate,用于与Redis进行交互
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    // 使用@Around注解,处理带有@RedisLock注解的方法
    @Around("@annotation(redisLock)")
    public Object handleRedisLock(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable {
        // 从注解中获取锁的键和过期时间
        String key = redisLock.key();
        long expireSeconds = redisLock.expireSeconds();

        // 获取方法上的@RedisLock注解
        RedisLock thisRedisLock = getShardingAnnotation(joinPoint);

        // 获取动态参数的名字
        String paramName = getShardingAnnotation(thisRedisLock,
                ((MethodSignature) joinPoint.getSignature()).getParameterNames(), joinPoint.getArgs());

        // 如果没有指定动态参数名字,或者动态参数值为空,则不加锁
        if (StrUtil.isBlank(paramName)) {
            return joinPoint.proceed();
        }

        // 构建锁的键,使用动态参数值替换占位符
        String lockKey = String.format(key, paramName);

        // 尝试获取锁
        Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "lock", Duration.ofSeconds(expireSeconds));
        if (lockAcquired != null && lockAcquired) {
            try {
                // 锁获取成功,继续执行方法逻辑
                return joinPoint.proceed();
            } finally {
                // 使用Lua脚本释放锁
                String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
                DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
                Object result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), "lock");
            }
        } else {
            // 锁获取失败,可以抛出异常或者根据需要进行处理
            throw new DefaultException("数据正在处理中,请勿频繁点击!", ResponseEnum.REQUEST_FREQUENTLY);
        }
    }

    // 通过注解中的参数名获取动态参数的值
    private String getShardingAnnotation(RedisLock redisLock, String[] parameterNames, Object[] args) {
        String[] properties = redisLock.paramName().split("\\.");

        Object obj = null;

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

            if (i == 0) {
                for (int j = 0; j < parameterNames.length; j++) {
                    // 找到与动态参数名字匹配的参数值
                    if (Objects.equals(properties[i], parameterNames[j])) {
                        obj = args[j];
                        break;
                    }
                }
            } else {
                // 通过反射获取嵌套对象的属性值
                obj = obj != null ? ReflectUtil.getFieldValue(obj, properties[i]) : null;
            }
        }

        return obj == null ? null : obj.toString();
    }

    /**
     * 获取方法中声明的注解
     *
     * @param joinPoint
     * @return
     * @throws NoSuchMethodException
     */
    // 通过反射获取方法上的@RedisLock注解
    public RedisLock getShardingAnnotation(ProceedingJoinPoint joinPoint) throws NoSuchMethodException {
        // 获取方法名
        String methodName = joinPoint.getSignature().getName();
        // 反射获取目标类
        Class<?> targetClass = joinPoint.getTarget().getClass();
        // 拿到方法对应的参数类型
        MethodSignature methodSignature = ((MethodSignature) joinPoint.getSignature());

        Class<?>[] parameterTypes = methodSignature.getParameterTypes();
        // 根据类、方法、参数类型(重载)获取到方法的具体信息
        Method objMethod = targetClass.getMethod(methodName, parameterTypes);

        // 拿到方法定义的注解信息
        return objMethod.getDeclaredAnnotation(RedisLock.class);
    }
}

使用方法

    @RedisLock(key = "xxxx:%s", paramName = "xxx.xxx")
温馨提示

大功告成!如果还有什么自定义的需求,只需要修改一下即可

发表评论 取消回复
表情 图片 链接 代码

分享