實現宣告式鎖,支援分散式鎖自定義鎖、SpEL和結合事務

2023-04-19 09:00:21

工作中遇到事務一般使用宣告式事務,一個註解@Transactional搞定。程式設計式事務則顯得略繁瑣。

	@Autowired
    private PlatformTransactionManager transactionManager;

    public void service() throws Exception {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            transactionManager.rollback(status);
            throw e;
        }
        transactionManager.commit(status);
    }

但是遇到需要加鎖的情況呢?
絕大多數情況都是使用程式設計式鎖。例如:

	ReentrantLock lock = new ReentrantLock();
    public void service() throws Exception {
        try {
            lock.lock();
            Thread.sleep(1000);
        } finally {
            lock.unlock();
        }
    }

但為什麼不搞個輪子把程式設計式鎖變為宣告式鎖呢?
本文嘗試著寫一個,歡迎大家指正。


2.實現

眾所周知,宣告式事務使用註解+AOP,同理宣告式鎖也應該一樣。那麼先搞一個註解。

2.1 定義註解

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Locked {

    /**
     * 鎖名稱,如果不傳會預設使用同一把鎖
     * 支援SpEL表示式
     * @return
     */
    String key() default "";


    /**
     * 鎖型別
     * @return
     */
    LockType lockType() default LockType.ReentrantLock;

    /**
     * 獲取鎖的時間,超出時間沒有獲取到鎖返回false
     * 預設為0
     * -1 為永不超時,這種情況下,tryLock()會一直阻塞到獲取鎖
     * @return
     */
    long time() default 0;

    /**
     * 獲取鎖的時間單位
     * 預設為秒
     * @return
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
}

再定義一個列舉型別的鎖型別

public enum LockType {
     ReentrantLock,
     RedissonClient,
     ELSE
}

2.2 定義鎖介面

然後仿照java.util.concurrent.locks.Lock定義一個鎖的介面。

public interface LockedService {
   boolean lock();

    boolean tryLock();

    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    void unLock();

    /**
     * 如果是redisson分散式鎖,呼叫方通過此方法將相應的redissonClient物件和key傳到具體實現類
     * @param redissonClient
     * @param key
     * @return
     */
    LockedService setRedissonClient(RedissonClient redissonClient, String key);

    /**
     * 如果是ReentrantLock,呼叫方通過此方法將相應的lock物件傳到具體實現類
     * @param lock
     * @return
     */
    LockedService setLock(Lock lock);
}

主要是加鎖解鎖的動作。加鎖分為阻塞與非阻塞,有時間引數與非時間引數之分。
與jdk的 lock介面完全相似。
裡面多餘的方法分別為:

  1. setRedissonClient()
    如果是redisson分散式鎖,呼叫方通過此方法將相應的redissonClient物件和key傳到具體實現類
  2. setLock()
    如果是ReentrantLock,呼叫方通過此方法將相應的lock物件傳到具體實現類

如果是自定義鎖,在自定義的實現類裡面可以忽略這兩個方法,自定義獲取鎖物件。

2.3 鎖的實現

鎖的介面至少需要兩個實現類,一個是ReentrantLock,另一個是RedissonClient
如果是直接定義兩個類,

@Component
public class LockedServiceImpl implements LockedService{}
@Component
public class LockedRedissonServiceImpl implements LockedService{}

然後在切面裡直接使用

 @Autowired
 private LockedService lockedService;

啟動就會報錯:

Field lockedService in com.nyp.test.service.LockAspect required a single bean, but 2 were found:
	- lockedRedissonServiceImpl: defined in file [\target\classes\com\nyp\test\service\LockedRedissonServiceImpl.class]
	- lockedServiceImpl: defined in file [\target\classes\com\nyp\test\service\LockedServiceImpl.class]

就算通過@Primary或者@Qualifier將這兩個實現類都注入了進來,也不好分辨究竟使用哪一個。

這個使用就需要用到SPI了。

2.3.1 什麼是SPI

SPI全稱Service Provider Interface,是Java提供的一套用來被第三方實現或者擴充套件的API,它可以用來啟用框架擴充套件和替換元件。

有點類似於介面+策略模式+組態檔組合實現的動態載入機制。

比如spring mvc/springboot裡面有個HttpMessageConverters,HTTP的request和response的轉換器。當一個請求完成時,返回一個物件,需要它將物件轉換成json串(或其它),然後以流的形式寫到使用者端。這個的工具有很多,比如jackson,gson等,spring預設採用jackson框架,AbstractJackson2HttpMessageConverter.

也很多同學實際上使用的是fastjson,FastJsonHttpMessageConverter。或者其它。

不管使用哪一種框架,它都要去實現HttpMessageConverters介面。但springboot容器載入的時候怎麼知道需要去載入哪些實現類,具體又使用哪個實現類呢。

這裡就使用到了SPI機制。

2.3.2 通過SPI實現鎖的多個實現類

SPI有jdk的實現,有spring boot的實現。這裡使用springboot的實現方法。

resources/META-INF目錄下新裝置檔案 spring.factories,內容為鎖介面及其實現類的全限定類名。

com.nyp.test.service.LockedService=\
com.nyp.test.service.LockedServiceImpl,\
com.nyp.test.service.LockedRedissonServiceImpl

同時在類LockedServiceImplLockedRedissonServiceImpl就不需要加@Component註解。
LockedServiceImpl類:

import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;

/**
 * @projectName: Test
 * @package: com.nyp.test.service
 * @className: com.nyp.test.service.LockedService
 * @author: nyp
 * @description: jdk ReentrantLock鎖實現
 * @date: 2023/4/13 11:45
 * @version: 1.0
 */
@Component
public class LockedServiceImpl implements LockedService{

    private Lock lock;

    @Override
    public boolean lock(){
        if (lock != null) {
            lock.lock();
            return true;
        }
        return false;
    }

    @Override
    public boolean tryLock() {
        return lock.tryLock();
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return lock.tryLock(time, unit);
    }

    @Override
    public void unLock(){
        if (lock != null) {
            lock.unlock();
        }
    }

    @Override
    public LockedService setRedissonClient(RedissonClient redissonClient, String key) {
        return this;
    }

    @Override
    public LockedService setLock(Lock lock) {
        this.lock = lock;
        return this;
    }

}

LockedRedissonServiceImpl類:

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;

/**
 * @projectName: Test
 * @package: com.nyp.test.service
 * @className: com.nyp.test.service.LockedService
 * @author: nyp
 * @description: redisson分散式鎖實現
 * @date: 2023/4/13 11:45
 * @version: 1.0
 */
public class LockedRedissonServiceImpl implements LockedService {
    private RedissonClient redissonClient;
    private String key;

    @Override
    public boolean lock() {
        RLock rLock = redissonClient.getLock(key);
        if (rLock != null) {
            rLock.lock();
            return true;
        }
        return false;
    }

    @Override
    public boolean tryLock() {
        RLock rLock = redissonClient.getLock(key);
        return rLock.tryLock();
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        RLock rLock = redissonClient.getLock(key);
        return rLock.tryLock(time, unit);
    }

    @Override
    public void unLock() {
        RLock rLock = redissonClient.getLock(key);
        if (rLock != null && rLock.isHeldByCurrentThread()) {
            rLock.unlock();
        }
    }

    @Override
    public LockedService setRedissonClient(RedissonClient redissonClient, String key) {
        this.redissonClient = redissonClient;
        this.key = key;
        return this;
    }

    @Override
    public LockedService setLock(Lock lock) {
        return this;
    }
}

那麼spring容器裡面就有兩個鎖介面實現了,到底使用哪一個呢?
模仿HttpMessageConverters搞一個轉換器。在init()方法裡面指定預設使用com.nyp.test.service.LockedServiceImpl.

public class LockedServiceConverters {
    private LockedService lockedService;
    private String lockedServiceImplClass;

    public LockedServiceConverters(String lockedServiceImplClass) {
        this.lockedServiceImplClass = lockedServiceImplClass;
    }

    public LockedService getLockedService() {
        return lockedService;
    }

    @PostConstruct
    public void init() throws Exception {
        if (StringUtils.isBlank(lockedServiceImplClass)) {
            lockedServiceImplClass = "com.nyp.test.service.LockedServiceImpl";
        }

        List<LockedService> lockedServiceList = SpringFactoriesLoader.loadFactories(LockedService.class, null);
        for (LockedService lockedService : lockedServiceList){
            System.out.println(lockedService.getClass().getName());
            if(lockedService.getClass().getName().equals(lockedServiceImplClass)){
                this.lockedService = lockedService;
            }
        }
        if (lockedService == null) {
            throw new Exception("未發現lockedService : " + lockedServiceImplClass);
        }
    }
}

使用的時候可以通過以下方式指定使用LockedRedissonServiceImpl,或其它自定義的鎖實現類。

@Configuration
public class WebConfig {
    @Bean
    public LockedServiceConverters lockedServiceConverters() {
        return new LockedServiceConverters("com.nyp.test.service.LockedRedissonServiceImpl");
    }
}

2.3.3 通過SPI自定義實現鎖

首先實現一個LockedService介面。

public class LockedServiceUserDefine implements LockedService{
    @Override
    public boolean lock() {
        log.info("鎖住");
        return true;
    }

    @Override
    public boolean tryLock() {
        return true;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return true;
    }

    @Override
    public void unLock() {
        log.info("解鎖");
    }

    @Override
    public LockedService setRedissonClient(RedissonClient redissonClient, String key) {
        return this;
    }

    @Override
    public LockedService setLock(Lock lock) {
        return this;
    }
}

然後將此類加到spring.factory裡面去

com.nyp.test.service.LockedService=\
com.nyp.test.service.LockedServiceImpl,\
com.nyp.test.service.LockedRedissonServiceImpl,\
com.nyp.test.service.LockedServiceUserDefine

再將LockedServiceConverters改為LockedServiceUserDefine即可。

@Bean
    public LockedServiceConverters lockedServiceConverters() {
        return new LockedServiceConverters("com.nyp.test.service.LockedServiceUserDefine");
    }

3.定義切面

3.1 切面實現

import com.nyp.test.config.LockType;
import com.nyp.test.config.Locked;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.ehcache.impl.internal.concurrent.ConcurrentHashMap;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @projectName: Test
 * @package: com.nyp.test.service
 * @className: LockAspect
 * @author: nyp
 * @description: TODO
 * @date: 2023/4/13 10:46
 * @version: 1.0
 */
@Aspect
@Component
@Slf4j
public class LockAspect {

    /**
     * 用於儲存ReentrantLock類的鎖
     */
    private volatile ConcurrentHashMap<String, Lock> locks = new ConcurrentHashMap<>();

    @Autowired
    private LockedServiceConverters lockedServiceConverters;

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 用於SpEL表示式解析.
     */
    private SpelExpressionParser parser = new SpelExpressionParser();
    /**
     * 用於獲取方法引數定義名字.
     */
    private DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();

    @Around("@annotation(locked)")
    public Object around(ProceedingJoinPoint joinPoint, Locked locked) throws Throwable {
        String lockKey = getKeyBySpEL(locked.key(), joinPoint);
        if (StringUtils.isBlank(lockKey)) {
            lockKey = joinPoint.getTarget().toString();
            log.info("lockKey = {}", lockKey);
        }
        Lock lock = locks.get(lockKey);
        lock = getLockFromLocks(lock, locked.lockType(), lockKey);
        boolean isLock;
        if (locked.time() != 0) {
            if (locked.time() < 0) {
                isLock = lockedServiceConverters.getLockedService().setLock(lock).setRedissonClient(redissonClient, lockKey).tryLock();
            } else {
                isLock = lockedServiceConverters.getLockedService().setLock(lock).setRedissonClient(redissonClient, lockKey).tryLock(locked.time(), locked.timeUnit());
            }
        } else {
            isLock = lockedServiceConverters.getLockedService().setLock(lock).setRedissonClient(redissonClient, lockKey).lock();
        }
        Object obj;
        try {
            obj = joinPoint.proceed();
        } catch (Throwable throwable) {
            throw  throwable;
        } finally {
            if(isLock){
                // 如果有事務,保證事務完成(commit or rollback)過後再釋放鎖
                // 這裡不管宣告式事務,還是程式設計式事務,只要還沒完成就會進入
				// TODO 這裡要考慮到事務的傳播,特別是巢狀事務
                if(TransactionSynchronizationManager.isActualTransactionActive()){
                    Lock finalLock = lock;
                    String finalLockKey = lockKey;
                    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                        @Override
                        public void afterCompletion(int status) {
                            lockedServiceConverters.getLockedService().setLock(finalLock).setRedissonClient(redissonClient, finalLockKey).unLock();
                        }
                    });
                } else{
                    lockedServiceConverters.getLockedService().setLock(lock).setRedissonClient(redissonClient, lockKey).unLock();
                }
            }
        }
        return obj;
    }

    private Lock getLockFromLocks(Lock lock, LockType lockType, String lockKey) {
        // TODO 非Lock類鎖可以不呼叫此方法
        if (lock == null) {
            synchronized (LockAspect.class){
                lock = locks.get(lockKey);
                if (lock == null) {
                    if (lockType == LockType.ReentrantLock) {
                        lock = new ReentrantLock();
                        locks.put(lockKey, lock);
                    }
                }
            }
        }
        return lock;
    }

    public String getKeyBySpEL(String expressionStr, ProceedingJoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        String[] paramNames = discoverer.getParameterNames(methodSignature.getMethod());
        Expression expression = parser.parseExpression(expressionStr);
        EvaluationContext context = new StandardEvaluationContext();
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; i++) {
            context.setVariable(paramNames[i], args[i]);
        }
        return expression.getValue(context) == null ? "" : expression.getValue(context).toString();
    }

}


1.其中locks用於儲存ReentrantLock類的鎖,每個key一個鎖物件,如果key為空,預設以方法名為key,意味著同一個方法共用一個鎖物件。

2.lockedServiceConverters為鎖轉換器,通過它可以獲取容器當中使用的真正鎖物件。
3.redissonClient物件在這裡注入,通過lockedServiceConverters獲取的鎖實現類將此鎖物件注入到實現方法內部。
4.SpelExpressionParserDefaultParameterNameDiscoverer物件用於通過SpEL表示式動態從目標方法引數中獲取鎖的key。

5.@Around("@annotation(locked)")這裡使用一個環繞通知,攔截加了locked註解的方法,進行鎖操作。

具體執行流程為,攔截到方法:
1.先獲取到lockkey,然後根據lockkey獲取鎖物件。
2.根據lockedServiceConverters拿到具體的鎖實現類物件,根據鎖物件型別以及time等引數,將需要的redissonClient,lock等引數傳入,再呼叫鎖方法實現,進行加鎖操作。
3.呼叫目標方法。
4.目標方法呼叫完畢,進行解鎖操作。這裡判斷一下是否還在一個事務當中,如果是的話,在事務完成之後再進行解鎖。這塊在後面再詳細說明。

3.2 SpEL表示式獲取動態key

定義一個測試目標方法。key為#person.name

@Transactional(rollbackFor = Exception.class)
@Locked(key = "#person.name", lockType = LockType.RedissonClient, time = 10, timeUnit = TimeUnit.MILLISECONDS)
    public void service(Person person) throws Exception {
        Thread.sleep(1000);
        log.info("業務方法");
    }

再呼叫

Person person = new Person();
person.setName("張三");
lockTest.service(person);

準確獲取到了張三

3.3 鎖與事務的結合

鎖與事務結合為什麼要單獨拿出來講?有什麼問題嗎?

具體可以看我的另一篇博文 https://juejin.cn/post/7213636024112234551 當transcational遇上synchronized。
不管是synchronized還是Lock。道理是一樣的。

簡單說,spring使用動態代理加AOP實現事務管理。那麼上面的方法實際上至少需要簡化成3個步驟:

void begin();

@Transactional(rollbackFor = Exception.class)
public void service(Person person) throws Exception {
  try{
  	lock.lock();
  } finally{
  	lock.unlock();
  }
}

void commit();
// void rollback();

如果在service()中向資料庫insert/update一條資料,在serive()執行完畢鎖釋放之後,commit之前,有另一個執行緒拿到了鎖開始執行service()方法,那麼這時候它是讀不到資料庫裡最新的記錄的。除非事務隔離級別為讀未提交。

但實際生產環境中,少有人使用讀未提交這種隔離級別,為了避免上述的執行緒安全問題,就得藉助事務同步器TransactionSynchronization來實現。當執行緒中存在未完成的事務時,需要在afterCompletion方法裡釋放鎖。afterCompletion表示事務完成,包括提交與回滾。

4.測試


4.1 ReentrantLock測試

	@Bean
    public LockedServiceConverters lockedServiceConverters() {
        return new LockedServiceConverters("com.nyp.test.service.LockedServiceImpl");
    }

業務方法阻塞,鎖獲取超時時間為60秒。

	@Transactional(rollbackFor = Exception.class)
    @Locked(key = "#person.name", lockType = LockType.ReentrantLock, time = 60000, timeUnit = TimeUnit.MILLISECONDS)
    public void service(Person person) throws Exception {
        Thread.sleep(10000000);
        log.info("業務方法");
    }

先測試張三鎖,再測試李四鎖,再獲取李四鎖
預期結果是張三獲取到鎖,李四因為是新的key所以也能獲取鎖,李四再次獲取鎖,因為之前李四獲取的鎖還沒釋放,所以一直阻塞獲取不了鎖。

執行紀錄檔:

[2023-04-18 19:02:17.639] INFO 53268 [http-nio-8657-exec-1] [com.nyp.test.service.LockAspect] : key = 張三
[2023-04-18 19:02:17.640] INFO 53268 [http-nio-8657-exec-1] [com.nyp.test.service.LockAspect] : 張三 嘗試獲取鎖 
[2023-04-18 19:02:17.641] INFO 53268 [http-nio-8657-exec-1] [com.nyp.test.service.LockedServiceImpl] : 張三 獲取到了鎖
[2023-04-18 19:02:20.280] INFO 53268 [http-nio-8657-exec-4] [com.nyp.test.service.LockAspect] : key = 李四
[2023-04-18 19:02:20.281] INFO 53268 [http-nio-8657-exec-4] [com.nyp.test.service.LockAspect] : 李四 嘗試獲取鎖 
[2023-04-18 19:02:20.281] INFO 53268 [http-nio-8657-exec-4] [com.nyp.test.service.LockedServiceImpl] : 李四 獲取到了鎖
[2023-04-18 19:02:22.181] INFO 53268 [http-nio-8657-exec-3] [com.nyp.test.service.LockAspect] : key = 李四
[2023-04-18 19:02:22.181] INFO 53268 [http-nio-8657-exec-3] [com.nyp.test.service.LockAspect] : 李四 嘗試獲取鎖 
[2023-04-18 19:03:22.186] INFO 53268 [http-nio-8657-exec-3] [com.nyp.test.service.LockedServiceImpl] : 李四 獲取鎖超時

執行紀錄檔符合預期,李四第2次嘗試獲取鎖,在60秒後超時失敗。

4.2 RedissonClient測試


@Bean
    public LockedServiceConverters lockedServiceConverters() {
        return new LockedServiceConverters("com.nyp.test.service.LockedRedissonServiceImpl");
    }

還是上面的測試程式碼,結果符合預期。

[2023-04-18 19:10:47.895] INFO 117888 [http-nio-8657-exec-1] [com.nyp.test.service.LockAspect] : key = 張三
[2023-04-18 19:10:47.896] INFO 117888 [http-nio-8657-exec-1] [com.nyp.test.service.LockAspect] : 張三 嘗試獲取鎖 
[2023-04-18 19:10:47.904] INFO 117888 [http-nio-8657-exec-1] [com.nyp.test.service.LockedRedissonServiceImpl] : 張三 獲取到了鎖
[2023-04-18 19:10:50.555] INFO 117888 [http-nio-8657-exec-5] [com.nyp.test.service.LockAspect] : key = 李四
[2023-04-18 19:10:50.556] INFO 117888 [http-nio-8657-exec-5] [com.nyp.test.service.LockAspect] : 李四 嘗試獲取鎖 
[2023-04-18 19:10:50.557] INFO 117888 [http-nio-8657-exec-5] [com.nyp.test.service.LockedRedissonServiceImpl] : 李四 獲取到了鎖
[2023-04-18 19:10:55.882] INFO 117888 [http-nio-8657-exec-8] [com.nyp.test.service.LockAspect] : key = 李四
[2023-04-18 19:10:55.883] INFO 117888 [http-nio-8657-exec-8] [com.nyp.test.service.LockAspect] : 李四 嘗試獲取鎖 
[2023-04-18 19:11:55.896] INFO 117888 [http-nio-8657-exec-8] [com.nyp.test.service.LockedRedissonServiceImpl] : 李四 獲取鎖超時

4.3 自定義鎖測試

使用LockedServiceUserDefine

    @Bean
    public LockedServiceConverters lockedServiceConverters() {
        return new LockedServiceConverters("com.nyp.test.service.LockedServiceUserDefine");
    }
    @Transactional(rollbackFor = Exception.class)
    @Locked(key = "#person.name", lockType = LockType.ELSE, time = 60000, timeUnit = TimeUnit.MILLISECONDS)
    public void service(Person person) throws Exception {
        log.info("業務方法");
    }

紀錄檔:

[2023-04-18 19:19:38.897] INFO 6264 [http-nio-8657-exec-1] [com.nyp.test.service.LockAspect] : key = 張三
[2023-04-18 19:19:38.898] INFO 6264 [http-nio-8657-exec-1] [com.nyp.test.service.LockAspect] : 張三 嘗試獲取鎖 
[2023-04-18 19:19:38.898] INFO 6264 [http-nio-8657-exec-1] [com.nyp.test.service.LockedServiceUserDefine] : 鎖住
[2023-04-18 19:19:38.900] INFO 6264 [http-nio-8657-exec-1] [com.nyp.test.service.LockTest] : 業務方法
[2023-04-18 19:19:38.901] INFO 6264 [http-nio-8657-exec-1] [com.nyp.test.service.LockedServiceUserDefine] : 解鎖  

這裡只是證明容器執行了自定義的實現類。沒有真正實現鎖。

5.尾聲


5.1 todo list

1.將locks裡不用的鎖定期清除。
2.getLockFromLocks()方法非Lock類的鎖可不執行。
3.重要:在切面裡,在事務同步器afterCompletion後再釋放鎖,這裡要考慮到事務的傳播性,特別是巢狀事務的情況。這種情況下,會把鎖的範圍擴大。

5.2 小結

在這裡我們實現了一個宣告式的鎖,在註解裡支援鎖的粒度(key和SpEL動態key),支援定義獲取鎖的超時時間,預設支援ReentrantLockRedissonClient兩種鎖。

也使用SPI支援自定義鎖實現。

支援鎖與事務的整合(考慮到事務的傳播,或者叫有限支援)。


但是!!沒經過嚴格的測試,特別是並行測試,不建議在生產環境中使用,僅作學習交流之用。
希望能夠對各位看官有所啟發,歡迎留言討論。