RedisLock 锁注解,基于springboot2.0.4,lettuce

最近因为业务需要用到加锁,所以就想用redis锁,因为对于业务来说,redis锁已经能够满足需求了。

但是,因为需要很多地方需要用到加锁,项目又是基于springboot,所以,就想写个springboot-starter,然后封装个注解,需要的项目中只要引入starter,并且在需要加锁的方法上加上注解就可以了。

1、添加pom依赖

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-autoconfigure</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-pool2</artifactId>
    </dependency>

2、定义锁注解

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

    /**
     * 锁key
     */
    String key() default "";

    /**
     * key前缀
     */
    String prefix() default "";

    /**
     * 过期时间,单位毫秒
     */
    long expire() default 15000;

    /**
     * 重试次数
     */
    int retryTimes() default 0;

    /**
     * 重试间隔,单位毫秒
     */
    int retryInterval() default 1000;

    /**
     * 绑定类型(作用于key的生成)
     */
    BindType bindType() default BindType.DEFAULT;

    /**
     * 绑定参数索引,从0开始,与 bindType.ARGS_INDEX 组合使用
     */
    int[] bindArgsIndex() default 0;

    /**
     * 对象参数属性 示例:ClassName.field, 与bingType.OBJECT_PROPERTIES 组合使用
     */
    String[] properties() default "";

    /**
     * 失败策略
     */
    ErrorStrategy errorStrategy() default ErrorStrategy.THROW_EXCEPTION;

    /**
     * 参数key绑定类型
     */
    enum BindType {
        /**
         * 默认,将所有参数toString
         */
        DEFAULT,
        /**
         * 参数索引,从0开始
         */
        ARGS_INDEX,
        /**
         * 对象属性
         */
        OBJECT_PROPERTIES;
    }


    /**
     * 获取锁失败策略
     */
    enum ErrorStrategy {
        /**
         * 抛异常
         */
        THROW_EXCEPTION,

        /**
         * 返回NULL
         */
        RETURN_NULL;
    }

}

3、定义加锁逻辑

@Slf4j
public class DistributedRedisLock {

    private RedisTemplate<Object, Object> redisTemplate;

    private ThreadLocal<String> lockKey = new ThreadLocal<>();

    public static final String UNLOCK_LUA;

    static {
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
        sb.append("then ");
        sb.append("    return redis.call(\"del\",KEYS[1]) ");
        sb.append("else ");
        sb.append("    return 0 ");
        sb.append("end ");
        UNLOCK_LUA = sb.toString();
    }

    public DistributedRedisLock(RedisTemplate<Object, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 加锁
     * @param key 锁key
     * @param expire 过期时间
     * @param retryTimes 重试次数
     * @param retryInterval 重试间隔
     * @return true 加锁成功, false 加锁失败
     */
    public boolean lock(String key, long expire, int retryTimes, long retryInterval) {
        boolean result = setRedisLock(key, expire);
        /**
         * 如果获取锁失败,进行重试
         */
        while((!result) && retryTimes-- > 0){
            try {
                log.info("lock failed, retrying..." + retryTimes);
                Thread.sleep(retryInterval);
            } catch (InterruptedException e) {
                return false;
            }
            result = setRedisLock(key, expire);
        }
        return result;
    }

    /**
     * 释放锁
     * @param key 锁key
     * @return true 释放成功, false 释放失败
     */
    public boolean unLock(String key) {
        /**
         * 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除
         * 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
         */
        try {
            RedisCallback<Boolean> callback = (connection) -> {
                String value = lockKey.get();
                return connection.eval(UNLOCK_LUA.getBytes(), ReturnType.BOOLEAN ,1, key.getBytes(), value.getBytes());
            };
            return redisTemplate.execute(callback);
        } catch (Exception e) {
            log.error("release lock occured an exception", e);
        } finally {
            lockKey.remove();
        }
        return false;
    }

    /**
     * 设置redis锁
     * @param key  锁key
     * @param expire 过期时间
     * @return true 设置成功,false 设置失败
     */
    private boolean setRedisLock(String key, long expire) {
        try {
            RedisCallback<Boolean> callback = (connection) -> {
                String uuid = UUID.randomUUID().toString();
                lockKey.set(uuid);
                return connection.set(key.getBytes(), uuid.getBytes(), Expiration.milliseconds(expire), RedisStringCommands.SetOption.SET_IF_ABSENT);
            };
            return redisTemplate.execute(callback);
        }catch (Exception e){
            log.error("set redis error", e);
        }
        return false;
    }


}

4、定义redis锁切面

@Slf4j
@Aspect
public class DistributedRedisLockAspect {

    private DistributedRedisLock distributedRedisLock;

    public DistributedRedisLockAspect(DistributedRedisLock distributedRedisLock){
        this.distributedRedisLock = distributedRedisLock;
    }

    @Pointcut("@annotation(com.springboot.starter.redis.annotation.RedisLock)")
    private void redisLockPoint(){}

    @Around("redisLockPoint() && @annotation(redisLock)")
    public Object around(ProceedingJoinPoint pjp, RedisLock redisLock) throws Throwable {
        String key = redisLock.key();
        if(StringUtils.isBlank(key)){
            Object[] args = pjp.getArgs();
            if(redisLock.bindType().equals(RedisLock.BindType.DEFAULT)){
                key = StringUtils.join(args);
            }else if(redisLock.bindType().equals(RedisLock.BindType.ARGS_INDEX)){
                key = getArgsKey(redisLock, args);
            }else if(redisLock.bindType().equals(RedisLock.BindType.OBJECT_PROPERTIES)){
                key = getObjectPropertiesKey(redisLock, args);
            }
        }
        Assert.hasText(key, "key does not exist");
        String prefix = redisLock.prefix();
        boolean lock = distributedRedisLock.lock(prefix + key, redisLock.expire(), redisLock.retryTimes(), redisLock.retryInterval());
        if(!lock) {
            log.error("get lock failed : " + key);
            if(redisLock.errorStrategy().equals(RedisLock.ErrorStrategy.THROW_EXCEPTION)){
                throw new RedisLockException("Get redis lock failed");
            }
            return null;
        }
        log.info("get lock success : {}" ,key);
        try {
            return pjp.proceed();
        }finally {
            boolean result = distributedRedisLock.unLock(prefix + key);
            log.info("release lock : {} {}", prefix + key ,result ? " success" : " failed");
        }
    }

    /**
     * 通过绑定的args生成key
     * @param redisLock redisLock注解
     * @param args 所有参数
     * @return key
     */
    private String getArgsKey(RedisLock redisLock, Object[] args){
        int[] index = redisLock.bindArgsIndex();
        Assert.notEmpty(Arrays.asList(index), "ArgsIndex is empty");

        int len = index.length;
        Object[] indexArgs = new Object[index.length];
        for(int i = 0; i < len; i++){
            indexArgs[i] = args[index[i]];
        }
        return StringUtils.join(indexArgs);
    }

    /**
     * 通过绑定的对象属性生成key
     * @param redisLock redisLock注解
     * @param args 所有参数
     * @return key
     */
    private String getObjectPropertiesKey(RedisLock redisLock, Object[] args) throws NoSuchFieldException, IllegalAccessException {

        String[] properties = redisLock.properties();
        List<Object> keylist = new ArrayList<>(properties.length);

        // 可以通过className获取args的位置
        Map<String, Integer> classNamesArgsIndex = getClassNameArgsIndex(args);
        // 可以通过className获取Class类型
        Map<String, Class<?>> classNameClass = getClassNameClass(args);

        for (String ppts : properties) {
            String[] classProperties = StringUtils.split(ppts, ".");
            String className = classProperties[0];
            String propertiesName = classProperties[1];
            Object argObject = args[classNamesArgsIndex.get(className)];

            Class<?> clazz = classNameClass.get(className);
            Field field = clazz.getDeclaredField(propertiesName);
            field.setAccessible(true);
            Object object = field.get(argObject);
            keylist.add(object);
        }
        return StringUtils.join(keylist.toArray());
    }

    /**
     * 获取类名和参数位置的对应关系
     * @param args 所有参数
     * @return Map<类名, 参数位置>
     */
    private Map<String, Integer> getClassNameArgsIndex(Object[] args){
        int len = args.length;
        Map<String, Integer> nameIndex = new HashMap<>();
        for(int i = 0; i < len; i++){
            String name = StringUtils.substringAfterLast(args[i].getClass().toString(), ".");
            nameIndex.put(name, i);
        }
        return nameIndex;
    }

    /**
     * 获取类名和类的对应关系
     * @param args 所有参数
     * @return Map<类名, 类>
     */
    private Map<String, Class<?>> getClassNameClass(Object[] args){
        int len = args.length;
        Map<String, Class<?>> nameClass = new HashMap<>();
        for(int i = 0; i < len; i++){
            Class<?> clazz = args[i].getClass();
            String name = StringUtils.substringAfterLast(clazz.toString(), ".");
            nameClass.put(name, clazz);
        }
        return nameClass;
    }

5、定义springboot的autoConfiguration配置类

@Slf4j
@Configuration
public class ReidsLockAutoConfiguration {

    @Bean
    public DistributedRedisLock distributedRedisLock(RedisTemplate redisTemplate){
        log.info("init Distributed Redis Lock");
        return new DistributedRedisLock(redisTemplate);
    }

    @Bean
    public DistributedRedisLockAspect distributedRedisLockAspect(DistributedRedisLock distributedRedisLock){
        log.info("init Distributed Redis Lock Aspect");
        return new DistributedRedisLockAspect(distributedRedisLock);
    }

}

6、在resources/META-INF/下添加spring.facotries文件

这样一个可以加redis锁注解的springboot-starter就封装完成了,只要引入该starter,配置上redis连接地址,就可以通过@RedisLock() 进行加锁了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章