springboot基於Redisson實現的一套分佈式鎖,支持註解形式使用,key支持el表達式

1.背景

項目中最近迫切的需要一套分佈式鎖來解決一些接口的併發問題,在網上搜集了各種資料。並結合自己的項目場景,依賴Redis實現了一套分佈式鎖,使用簡單,支持註解。在這裏分享一下實現過程,希望能對你們有一些幫助。

 

2.項目結構

核心包:

 

3.maven依賴

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.2.5.RELEASE</version>
            <exclusions>
                <exclusion>
                    <groupId>redis.clients</groupId>
                    <artifactId>jedis</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.13.0</version>
        </dependency>

 

4.Redis配置

無需多說

spring:
    redis:
        host: xxxxx
        password: xxxxx
        timeout: 3000
        port: 6379

5.Redisson配置

redisson配置項需要單獨配置一份yaml文件,在springboot的yml中配置是無法生效的。

redisson配置有兩種方式:編碼形式和配置式

方式一:純編碼形式無需編寫yaml文件,直接對所需配置項進行配置就好:如下

@Configuration
public class RedissonConfig {

  

    @Bean
    @ConditionalOnProperty("spring.redis.host")
    public RedissonClient redissonClient(){
        Config config = new Config();
        config.setTransportMode(TransportMode.EPOLL);
        config.useClusterServers()
                // use "rediss://" for SSL connection
                .addNodeAddress("perredis://127.0.0.1:7181");

        return  Redisson.create(config);
    }

}

配置方式二 配置式:新增redission.yaml配置文件

clusterServersConfig:
  idleConnectionTimeout: 10000
  connectTimeout: 10000
  timeout: 3000
  retryAttempts: 3
  retryInterval: 1500
  failedSlaveReconnectionInterval: 3000
  failedSlaveCheckInterval: 60000
  password: null
  subscriptionsPerConnection: 5
  clientName: null
  loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {}
  subscriptionConnectionMinimumIdleSize: 1
  subscriptionConnectionPoolSize: 50
  slaveConnectionMinimumIdleSize: 24
  slaveConnectionPoolSize: 64
  masterConnectionMinimumIdleSize: 24
  masterConnectionPoolSize: 64
  readMode: "SLAVE"
  subscriptionMode: "SLAVE"
  nodeAddresses:
  - "redis://127.0.0.1:7004"
  - "redis://127.0.0.1:7001"
  - "redis://127.0.0.1:7000"
  scanInterval: 1000
  pingConnectionInterval: 0
  keepAlive: false
  tcpNoDelay: false
threads: 16
nettyThreads: 32
codec: !<org.redisson.codec.FstCodec> {}
transportMode: "NIO"

之後編寫配置類,將此配置文件引入即可(注意路徑)。

Config config = Config.fromYAML(new File("redisson.yaml"));  
RedissonClient redisson = Redisson.create(config);

配置參數根據不同的redis使用場景(單機、主備、cluster等)需要自行去選擇,官方文檔都做了詳細的講解,具體配置參數請移步官網進行詳細查看:redisso官方文檔

6.編寫分佈式鎖代碼

編寫註解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedLock {

    /**
     * 分佈式鎖的key值,非空。支持el表達式獲取入參對象參數
     */
    String[]  keys();
    /**
     * 分佈式鎖key前綴,默認採用類全限定名:方法名
     */
    String  prefix() default "";
    /**
     * key與前綴的分隔符
     */
    String  separator() default ":";
    /**
     * 等待獲取鎖的時間
     */
    long  waitTime() default 8;
    /**
     * 鎖的租期,超時後自動釋放
     */
    long  leaseTime() default 3;
    /**
     * 時間單位
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
}

考慮到我們的業務場景,這裏的keys我是用了數組形式,可以獲取多個入參的參數。支持EL

配置切面及解析邏輯

@Aspect
@Component
@Slf4j
public class DistributedLockAspect {

    @Resource
    DistributedLocker distributedLocker;

    @Pointcut("@annotation(xxx.lock.annotation.DistributedLock)")
    public void pointCut(){}


    /**
     * 環繞增強,嘗試獲取鎖/釋放鎖
     *
     * @param joinPoint 切面
     * @return Object
     * @throws Throwable
     */
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method targetMethod = methodSignature.getMethod();
        DistributedLock annotation = AnnotationUtils.findAnnotation(targetMethod, DistributedLock.class);
        assert annotation != null;
        String key=getLockKey(targetMethod,joinPoint,annotation);
        boolean lockFlag =false;
        Object proceed =null;
        try {
            lockFlag=distributedLocker.tryLock(key,annotation.timeUnit(),annotation.waitTime(),annotation.leaseTime());
            if (lockFlag) {
                log.info("success to get distributed lock with key {}",key);
                proceed = joinPoint.proceed();
            }
        } catch (Exception exception) {
            log.error("exception occurred while getting distributed lock ",exception);
            return null;
        }finally {
            if (lockFlag){
                distributedLocker.unlock(key);
                log.info("lock {} has been released",key);
            }
        }
        return proceed;
    }


    /**
     * 獲取攔截到的方法,解析分佈式鎖key值(如果包含el表達式,則從中解析出內容)
     *
     * @param joinPoint 切點
     * @return redisKey
     */
    private String getLockKey(Method targetMethod,
                              ProceedingJoinPoint joinPoint, DistributedLock targetAnnotation) {
        Object target = joinPoint.getTarget();
        Object[] arguments = joinPoint.getArgs();
        StringBuilder stringBuilder=new StringBuilder();
        for (int i=0;i<targetAnnotation.keys().length;i++){
            String subKey=targetAnnotation.keys()[i];
            if (StringUtils.isNotBlank(subKey) && StringUtils.contains(subKey, Constants.Symbol.SHARP)) {
                stringBuilder.append(SpelUtil.parse(target, subKey , targetMethod, arguments));
            }else {
                stringBuilder.append(subKey);
            }
        }
        if (StringUtils.isNotBlank(targetAnnotation.prefix())){
            return StringUtil.concat(targetAnnotation.prefix(),targetAnnotation.separator(),stringBuilder);
        }else{
            return StringUtil.concat(target.getClass().getName(),targetAnnotation.separator(),targetMethod.getName(),targetAnnotation.separator(),stringBuilder);
        }
    }
}

EL解析工具(此段代碼爲引用工具,出處未知):

public class SpelUtil {
    public static String parse(String spel, Method method, Object[] args) {
        //獲取被攔截方法參數名列表(使用Spring支持類庫)
        LocalVariableTableParameterNameDiscoverer u =
                new LocalVariableTableParameterNameDiscoverer();
        String[] paraNameArr = u.getParameterNames(method);
        //使用SPEL進行key的解析
        ExpressionParser parser = new SpelExpressionParser();
        //SPEL上下文
        StandardEvaluationContext context = new StandardEvaluationContext();
        //把方法參數放入SPEL上下文中
        if (paraNameArr != null) {
            for (int i = 0; i < paraNameArr.length; i++) {
                context.setVariable(paraNameArr[i], args[i]);
            }
        }
        return parser.parseExpression(spel).getValue(context, String.class);
    }

    /**
     * 支持 #p0 參數索引的表達式解析
     * @param rootObject 根對象,method 所在的對象
     * @param spel 表達式
     * @param method ,目標方法
     * @param args 方法入參
     * @return 解析後的字符串
     */
    public static String parse(Object rootObject,String spel, Method method, Object[] args) {
        //獲取被攔截方法參數名列表(使用Spring支持類庫)
        LocalVariableTableParameterNameDiscoverer u =
                new LocalVariableTableParameterNameDiscoverer();
        String[] paraNameArr = u.getParameterNames(method);
        //使用SPEL進行key的解析
        ExpressionParser parser = new SpelExpressionParser();
        //SPEL上下文
        StandardEvaluationContext context = new MethodBasedEvaluationContext(rootObject,method,args,u);
        //把方法參數放入SPEL上下文中
        if (paraNameArr != null) {
            for (int i = 0; i < paraNameArr.length; i++) {
                context.setVariable(paraNameArr[i], args[i]);
            }
        }
        return parser.parseExpression(spel).getValue(context, String.class);
    }
}

編寫鎖及實現

接口DistributedLocker

/**
 * 分佈式鎖
 *
 * @author 小菜瓜
 * @since 2020/6/8
 */
public interface DistributedLocker {

    /**
     * 以默認配置獲取鎖,等待鎖默認5s,鎖租期默認3s
     *
     * @param lockKey lockKey
     * @return RLock
     */
    RLock lock(String lockKey) ;

    /**
     * 自定義超時時間的鎖,最多等待timeout秒,
     *
     * @param lockKey lockKey
     * @param waitTime 超時時間,單位爲秒
     * @return lock
     */
    RLock lock(String lockKey, long waitTime);

    /**
     * 自定義超時時間和時間單位的鎖,
     *
     * @param lockKey lockKey
     * @param waitTime 超時時間
     * @param unit 時間單位
     * @return lock
     */
    RLock lock(String lockKey, TimeUnit unit, long waitTime);

    /**
     * 嘗試加鎖。自定義時間單位和鎖等待時間,租期默認3s
     *
     * @param lockKey lockKey
     * @param unit 時間單位
     * @param waitTime 超時時間
     * @return boolean
     */
    boolean tryLock(String lockKey, TimeUnit unit, long waitTime);


    /**
     *  嘗試加鎖,自定義時間單位和鎖等待時間以及鎖過期時間
     *
     * @param lockKey key
     * @param timeUnit 時間單位
     * @param waitTime 等待超時時間
     * @param leaseTime 鎖租期,超過leaseTime自動釋放
     * @return boolean
     */
    boolean tryLock(String lockKey,TimeUnit timeUnit, long waitTime , long leaseTime);

    /**
     * 公平鎖,多個Redisson客戶端線程同時請求加鎖時,優先分配給先發出請求的線程。
     * 嘗試加鎖,最多等待waitTime後強制獲取鎖,上鎖以後leaseTime自動解鎖
     *
     * @param lockKey   鎖key
     * @param unit      鎖時間單位
     * @param waitTime  等到最大時間,強制獲取鎖
     * @param leaseTime 鎖自動時間,
     * @return 如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他線程獲取),則返回false
     */
    boolean fairLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime);


    /**
     *  獲取公平鎖,單位爲秒
     *
     * @param lockKey lockKey
     * @param waitTime 等待時間
     * @param leaseTime 租期
     * @return boolean
     */
    boolean fairLock(String lockKey, long waitTime,long leaseTime);

    /**
     * 解鎖
     *
     * @param lockKey lockKey
     */

    void unlock(String lockKey);

    /**
     * 解鎖RLock
     *
     * @param lock
     */
    void unlock(RLock lock);

}

實現類DistributedLockImpl :在這裏我們依賴RedissonClient 進行鎖的實現

@Slf4j
@Component
public class DistributedLockImpl implements DistributedLocker{
    /**
     * 默認時間單位:秒
     */
    public static final TimeUnit DEFAULT_TIME_UNIT= TimeUnit.SECONDS;

    /**
     * 默認鎖等待超時時間
     */
    public static final int DEFAULT_TIMEOUT=8;
    /**
     * 默認鎖過期時間
     */
    public static final int DEFAULT_LEASE_TIME=3;


    @Resource
    private RedissonClient redissonClient;



    @Override
    public RLock lock(String lockKey) {
        RLock lock =redissonClient.getLock(lockKey);
        try {
            lock.tryLock(DEFAULT_TIMEOUT,DEFAULT_LEASE_TIME,DEFAULT_TIME_UNIT);
        } catch (InterruptedException e) {
           log.error("get lock with key {} failed,cause ",lockKey,e);
           return null;
        }
        return lock;
    }

    @Override
    public RLock lock(String lockKey, long timeout) {
        return lock(lockKey,DEFAULT_TIME_UNIT,timeout);
    }

    @Override
    public RLock lock(String lockKey, TimeUnit unit, long timeout) {
        RLock lock =redissonClient.getLock(lockKey);
        try {
            lock.tryLock(timeout,DEFAULT_LEASE_TIME,unit);
        } catch (InterruptedException e) {
            log.error("get lock with key {} failed. cause",lockKey,e);
            return null;
        }
        return lock;
    }

    @Override
    public boolean tryLock(String lockKey, TimeUnit unit, long timeout) {
        return  tryLock(lockKey,unit,timeout,DEFAULT_LEASE_TIME);
    }

    @Override
    public boolean tryLock(String lockKey, TimeUnit timeUnit, long waitTime, long leaseTime) {
        RLock lock=redissonClient.getLock(lockKey);
        try {
            return lock.tryLock(waitTime,leaseTime,timeUnit);
        } catch (InterruptedException e) {
            log.error("get lock with key {} failed. cause",lockKey,e);
            return false;
        }
    }

    @Override
    public boolean fairLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime) {
        RLock lock=redissonClient.getFairLock(lockKey);
        try {
            return  lock.tryLock(waitTime,leaseTime,unit);
        } catch (InterruptedException e) {
            log.error("get lock with key {} failed. cause",lockKey,e);
            return false;
        }
    }

    @Override
    public boolean fairLock(String lockKey, long waitTime, long leaseTime) {
        return fairLock(lockKey,DEFAULT_TIME_UNIT,waitTime,leaseTime);
    }


    @Override
    public void unlock(String lockKey) {
        RLock lock=redissonClient.getLock(lockKey);
        lock.unlock();
    }

    @Override
    public void unlock(RLock lock) {
        lock.unlock();
    }
}

完畢!

以上這一套分佈式鎖可以拿來就用,具體的 waitTime() default 8; leaseTime() default 3;等參數還需要各位根據自己的業務場景去進行調整。

如果有更好的改進方案和不對的地方還請各位大佬糾正和指教,感興趣也可以可以一起討論一下。

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