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() 進行加鎖了。

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