在之前的項目中分布式鎖和限流是基於redis進行的,分布式鎖基於setnx和expire命令實現,也可以基於lua腳本實現。限流是采用固定時間窗算法進行的。
最近了解到redisson這個工具類,而且基於其分布式鎖的實現是比較常見的,簡單研究下其使用。
官網:wiki地址 https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95
1. 概述
官網解釋如下:Redisson是一個在Redis的基礎上實現的Java駐內存數據網格(In-Memory Data Grid)。它不僅提供了一系列的分布式的Java常用對象,還提供了許多分布式服務。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最簡單和最便捷的方法。Redisson的宗旨是促進使用者對Redis的關注分離(Separation of Concern),從而讓使用者能夠將精力更集中地放在處理業務邏輯上。
2.研究其分布式鎖的用法
pom增加:
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.15.0</version> </dependency>
增加日志文件 logback.xml
<?xml version="1.0" encoding="UTF-8" ?> <configuration scan="true" scanPeriod="60000"> <property name="LOG_HOME" value="/export/logs/cmdb/"/> <property name="APP_NAME" value="cmdb"/> <property name="LOG_FILE_EXPIRE_TIME" value="180"/> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} | ${APP_NAME} - %p | %thread | %c | line:%L - %m%n</pattern> </encoder> </appender> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_HOME}${APP_NAME}.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>${LOG_FILE_EXPIRE_TIME}</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} | ${APP_NAME} - %p | %thread | %c | line:%L - %m%n</pattern> </encoder> </appender> <root> <level value="ERROR"/> <appender-ref ref="STDOUT"/> <!--<appender-ref ref="FILE"/>--> </root> <!-- 不同包,設置不同的日志級別 --> <logger name="com.xm.ggn" level="INFO"/> </configuration>
1. 簡單的使用
package com.xm.ggn.test.redisson; import lombok.extern.slf4j.Slf4j; import org.redisson.Redisson; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import java.util.concurrent.TimeUnit; @Slf4j public class Client { private static final Long TIME_LOCKED = 50 * 1000l; private static final String KEY_LOCKED = "myLock"; private static RedissonClient redissonClient = null; public static void main(String[] args) { initRedissonClient(); lock(); } private static void initRedissonClient() { // 1. Create config object Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); // 2. Create Redisson instance Client.redissonClient = Redisson.create(config); } private static void lock() { RLock lock1 = redissonClient.getLock(KEY_LOCKED); log.error("lock1 clas: {}", lock1.getClass()); lock1.lock(); log.info("lock, ThreadName: {} id: {} locked, 重入次數: {}", Thread.currentThread().getName(), Thread.currentThread().getId(), lock1.getHoldCount()); // 處理業務邏輯 try { Thread.sleep(TIME_LOCKED); reLock(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock1.unlock(); log.info("lock, ThreadName: {} id: {} unlock, 重入次數: {}", Thread.currentThread().getName(), Thread.currentThread().getId(), lock1.getHoldCount()); } } /** * 測試鎖的重入 */ private static void reLock() { RLock lock1 = redissonClient.getLock(KEY_LOCKED); lock1.lock(); log.info("reLock, ThreadName: {} id: {} locked, 重入次數: {}", Thread.currentThread().getName(), Thread.currentThread().getId(), lock1.getHoldCount()); // 處理業務邏輯 try { Thread.sleep(TIME_LOCKED); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock1.unlock(); log.info("reLock, ThreadName: {} id: {} unlock, 重入次數: {}", Thread.currentThread().getName(), Thread.currentThread().getId(), lock1.getHoldCount()); } } }
結果:
2021-02-01 16:20:23.013 | cmdb - ERROR | main | com.xm.ggn.test.redisson.Client | line:35 - lock1 clas: class org.redisson.RedissonLock 2021-02-01 16:20:23.050 | cmdb - INFO | main | com.xm.ggn.test.redisson.Client | line:37 - lock, ThreadName: main id: 1 locked, 重入次數: 1 2021-02-01 16:21:13.056 | cmdb - INFO | main | com.xm.ggn.test.redisson.Client | line:57 - reLock, ThreadName: main id: 1 locked, 重入次數: 2 2021-02-01 16:22:03.059 | cmdb - INFO | main | com.xm.ggn.test.redisson.Client | line:66 - reLock, ThreadName: main id: 1 unlock, 重入次數: 1 2021-02-01 16:22:03.061 | cmdb - INFO | main | com.xm.ggn.test.redisson.Client | line:47 - lock, ThreadName: main id: 1 unlock, 重入次數: 0
注意:
(1)redis中是通過HASH來存儲鎖的,key是UUID+":"+ThreadId;value 是重入的層數,例如上面過程中查看redis數據如下:
127.0.0.1:6379> hgetall myLock 1) "c20d8714-89c6-485f-ad4f-8dbb54271ebf:1" 2) "1" 127.0.0.1:6379> hgetall myLock 1) "c20d8714-89c6-485f-ad4f-8dbb54271ebf:1" 2) "2" 127.0.0.1:6379> hgetall myLock (empty list or set)
(2)也可以使用lock(long var1, TimeUnit var3); 方法自動釋放鎖
private static void lock3() { RLock lock1 = redissonClient.getLock(KEY_LOCKED); log.error("lock1 clas: {}", lock1.getClass()); // 500s 后自動釋放鎖 lock1.lock(500, TimeUnit.SECONDS); try { Thread.sleep(TIME_LOCKED); } catch (InterruptedException ignore) { // ignore } }
測試查看日志如下:
127.0.0.1:6379> ttl myLock (integer) 493 127.0.0.1:6379> hgetall myLock 1) "3cdf7b21-1e36-4f1f-b0ba-f0339286f416:1" 2) "1"
(3)tryLock(long time, TimeUnit unit) 可以嘗試一定時間去獲取鎖,返回Boolean值
private static void lock2() { for (int i = 0; i < 3; i++) { new Thread(new Runnable() { @Override public void run() { log.info(Thread.currentThread().getName() + " \t 運行"); RLock lock1 = redissonClient.getLock(KEY_LOCKED); try { // 嘗試獲取鎖60s boolean b = lock1.tryLock(7, TimeUnit.SECONDS); if (!b) { log.info(Thread.currentThread().getName() + " \t 獲取鎖失敗"); return; } } catch (InterruptedException e) { } log.info(Thread.currentThread().getName() + " \t 獲取鎖"); try { // 模擬處理邏輯用時50s Thread.sleep(5 * 1000); } catch (InterruptedException e) { } lock1.unlock(); log.info(Thread.currentThread().getName() + " \t 釋放鎖"); } }).start(); } }
結果:
2021-02-01 17:17:04.915 | cmdb - INFO | Thread-3 | com.xm.ggn.test.redisson.Client | line:121 - Thread-3 運行 2021-02-01 17:17:04.915 | cmdb - INFO | Thread-1 | com.xm.ggn.test.redisson.Client | line:121 - Thread-1 運行 2021-02-01 17:17:04.915 | cmdb - INFO | Thread-2 | com.xm.ggn.test.redisson.Client | line:121 - Thread-2 運行 2021-02-01 17:17:04.949 | cmdb - INFO | Thread-1 | com.xm.ggn.test.redisson.Client | line:133 - Thread-1 獲取鎖 2021-02-01 17:17:09.952 | cmdb - INFO | Thread-1 | com.xm.ggn.test.redisson.Client | line:143 - Thread-1 釋放鎖 2021-02-01 17:17:09.954 | cmdb - INFO | Thread-3 | com.xm.ggn.test.redisson.Client | line:133 - Thread-3 獲取鎖 2021-02-01 17:17:11.925 | cmdb - INFO | Thread-2 | com.xm.ggn.test.redisson.Client | line:127 - Thread-2 獲取鎖失敗 2021-02-01 17:17:14.956 | cmdb - INFO | Thread-3 | com.xm.ggn.test.redisson.Client | line:143 - Thread-3 釋放鎖
(4) tryLock(long var1, long var3, TimeUnit var5) 接收3個參數,第一個指定最長等待時間waitTime,第二個指定最長持有鎖的時間 holdTime, 第三個是單位
private static void lock2() { for (int i = 0; i < 3; i++) { final int index = i; new Thread(new Runnable() { @Override public void run() { log.info(Thread.currentThread().getName() + " \t 運行"); RLock lock1 = redissonClient.getLock(KEY_LOCKED); try { // 嘗試獲取7s // boolean b = lock1.tryLock(7, TimeUnit.SECONDS); // 嘗試獲取鎖7s, 最多占有鎖2s,超過后自動釋放,調用unlock可以提前釋放。 boolean b = lock1.tryLock(7, 2, TimeUnit.SECONDS); if (!b) { log.info(Thread.currentThread().getName() + " \t 獲取鎖失敗"); return; } } catch (InterruptedException e) { } log.info(Thread.currentThread().getName() + " \t 獲取鎖"); try { // 模擬處理邏輯用時 Thread.sleep((index * 2) * 1000); } catch (InterruptedException e) { } // 如果是當前線程持有鎖,手動釋放 if (lock1.isHeldByCurrentThread()) { lock1.unlock(); log.info(Thread.currentThread().getName() + " \t 釋放鎖"); } } }).start(); } }
結果:
2021-02-01 20:51:24.938 | cmdb - INFO | Thread-2 | com.xm.ggn.test.redisson.Client | line:162 - Thread-2 運行 2021-02-01 20:51:24.938 | cmdb - INFO | Thread-3 | com.xm.ggn.test.redisson.Client | line:162 - Thread-3 運行 2021-02-01 20:51:24.938 | cmdb - INFO | Thread-1 | com.xm.ggn.test.redisson.Client | line:162 - Thread-1 運行 2021-02-01 20:51:24.981 | cmdb - INFO | Thread-2 | com.xm.ggn.test.redisson.Client | line:176 - Thread-2 獲取鎖 2021-02-01 20:51:26.980 | cmdb - INFO | Thread-3 | com.xm.ggn.test.redisson.Client | line:176 - Thread-3 獲取鎖 2021-02-01 20:51:28.984 | cmdb - INFO | Thread-1 | com.xm.ggn.test.redisson.Client | line:176 - Thread-1 獲取鎖 2021-02-01 20:51:28.988 | cmdb - INFO | Thread-1 | com.xm.ggn.test.redisson.Client | line:188 - Thread-1 釋放鎖
(5)RedissonLock 的ttl也不是永久的,默認是30s。
在加鎖成功后,會注冊一個定時任務監聽這個鎖,每隔10秒就去查看這個鎖,如果還持有鎖,就對過期時間進行續期。默認過期時間30秒,過10秒檢查一次,一旦加鎖的業務沒有執行完,就會進行一次續期,把鎖的過期時間再次重置成30秒。 如果在執行過程中線程死掉,不會續期。會等ttl到期后自動消失。
(6) 查看RLock的繼承關系如下

2. 公平鎖的使用
默認使用的是非公平鎖,不過一般情況使用的都是公平鎖,也就是先到先得。
(1)默認非公平鎖
private static void lock1() { for (int i = 0; i < 5; i++) { // 休眠一下使線程按照順序啟動 try { Thread.sleep(1 * 100); } catch (InterruptedException e) { } Thread thread = new Thread(new Runnable() { @Override public void run() { log.info(Thread.currentThread().getName() + " \t 運行"); // 下面方式獲取到的是非公平鎖 RLock lock1 = redissonClient.getLock(KEY_LOCKED); // RLock lock1 = redissonClient.getFairLock(KEY_LOCKED); log.error("lock1 clas: {}", lock1.getClass()); lock1.lock(); log.info(Thread.currentThread().getName() + " \t 獲取鎖"); try { Thread.sleep(TIME_LOCKED); } catch (InterruptedException e) { e.printStackTrace(); } log.info(Thread.currentThread().getName() + " \t 釋放鎖"); lock1.unlock(); } }); thread.setName("MyThread: " + i); thread.start(); } }
結果:
2021-02-01 17:34:02.226 | cmdb - INFO | MyThread: 0 | com.xm.ggn.test.redisson.Client | line:96 - MyThread: 0 運行 2021-02-01 17:34:02.233 | cmdb - ERROR | MyThread: 0 | com.xm.ggn.test.redisson.Client | line:100 - lock1 clas: class org.redisson.RedissonLock 2021-02-01 17:34:02.325 | cmdb - INFO | MyThread: 1 | com.xm.ggn.test.redisson.Client | line:96 - MyThread: 1 運行 2021-02-01 17:34:02.325 | cmdb - ERROR | MyThread: 1 | com.xm.ggn.test.redisson.Client | line:100 - lock1 clas: class org.redisson.RedissonLock 2021-02-01 17:34:02.426 | cmdb - INFO | MyThread: 2 | com.xm.ggn.test.redisson.Client | line:96 - MyThread: 2 運行 2021-02-01 17:34:02.426 | cmdb - ERROR | MyThread: 2 | com.xm.ggn.test.redisson.Client | line:100 - lock1 clas: class org.redisson.RedissonLock 2021-02-01 17:34:02.526 | cmdb - INFO | MyThread: 3 | com.xm.ggn.test.redisson.Client | line:96 - MyThread: 3 運行 2021-02-01 17:34:02.526 | cmdb - ERROR | MyThread: 3 | com.xm.ggn.test.redisson.Client | line:100 - lock1 clas: class org.redisson.RedissonLock 2021-02-01 17:34:02.627 | cmdb - INFO | MyThread: 4 | com.xm.ggn.test.redisson.Client | line:96 - MyThread: 4 運行 2021-02-01 17:34:02.627 | cmdb - ERROR | MyThread: 4 | com.xm.ggn.test.redisson.Client | line:100 - lock1 clas: class org.redisson.RedissonLock 2021-02-01 17:34:16.038 | cmdb - INFO | MyThread: 3 | com.xm.ggn.test.redisson.Client | line:103 - MyThread: 3 獲取鎖 2021-02-01 17:34:21.038 | cmdb - INFO | MyThread: 3 | com.xm.ggn.test.redisson.Client | line:110 - MyThread: 3 釋放鎖 2021-02-01 17:34:21.043 | cmdb - INFO | MyThread: 1 | com.xm.ggn.test.redisson.Client | line:103 - MyThread: 1 獲取鎖 2021-02-01 17:34:26.044 | cmdb - INFO | MyThread: 1 | com.xm.ggn.test.redisson.Client | line:110 - MyThread: 1 釋放鎖 2021-02-01 17:34:26.047 | cmdb - INFO | MyThread: 0 | com.xm.ggn.test.redisson.Client | line:103 - MyThread: 0 獲取鎖 2021-02-01 17:34:31.047 | cmdb - INFO | MyThread: 0 | com.xm.ggn.test.redisson.Client | line:110 - MyThread: 0 釋放鎖 2021-02-01 17:34:31.050 | cmdb - INFO | MyThread: 4 | com.xm.ggn.test.redisson.Client | line:103 - MyThread: 4 獲取鎖 2021-02-01 17:34:36.050 | cmdb - INFO | MyThread: 4 | com.xm.ggn.test.redisson.Client | line:110 - MyThread: 4 釋放鎖 2021-02-01 17:34:36.054 | cmdb - INFO | MyThread: 2 | com.xm.ggn.test.redisson.Client | line:103 - MyThread: 2 獲取鎖 2021-02-01 17:34:41.055 | cmdb - INFO | MyThread: 2 | com.xm.ggn.test.redisson.Client | line:110 - MyThread: 2 釋放鎖
(2) 公平鎖的使用
主要代碼同上,只是獲取鎖變為公平鎖
RLock lock1 = redissonClient.getFairLock(KEY_LOCKED);
結果:
2021-02-01 17:38:04.689 | cmdb - INFO | MyThread: 0 | com.xm.ggn.test.redisson.Client | line:96 - MyThread: 0 運行 2021-02-01 17:38:04.696 | cmdb - ERROR | MyThread: 0 | com.xm.ggn.test.redisson.Client | line:98 - lock1 clas: class org.redisson.RedissonFairLock 2021-02-01 17:38:04.721 | cmdb - INFO | MyThread: 0 | com.xm.ggn.test.redisson.Client | line:101 - MyThread: 0 獲取鎖 2021-02-01 17:38:04.787 | cmdb - INFO | MyThread: 1 | com.xm.ggn.test.redisson.Client | line:96 - MyThread: 1 運行 2021-02-01 17:38:04.787 | cmdb - ERROR | MyThread: 1 | com.xm.ggn.test.redisson.Client | line:98 - lock1 clas: class org.redisson.RedissonFairLock 2021-02-01 17:38:04.888 | cmdb - INFO | MyThread: 2 | com.xm.ggn.test.redisson.Client | line:96 - MyThread: 2 運行 2021-02-01 17:38:04.888 | cmdb - ERROR | MyThread: 2 | com.xm.ggn.test.redisson.Client | line:98 - lock1 clas: class org.redisson.RedissonFairLock 2021-02-01 17:38:04.989 | cmdb - INFO | MyThread: 3 | com.xm.ggn.test.redisson.Client | line:96 - MyThread: 3 運行 2021-02-01 17:38:04.989 | cmdb - ERROR | MyThread: 3 | com.xm.ggn.test.redisson.Client | line:98 - lock1 clas: class org.redisson.RedissonFairLock 2021-02-01 17:38:05.089 | cmdb - INFO | MyThread: 4 | com.xm.ggn.test.redisson.Client | line:96 - MyThread: 4 運行 2021-02-01 17:38:05.089 | cmdb - ERROR | MyThread: 4 | com.xm.ggn.test.redisson.Client | line:98 - lock1 clas: class org.redisson.RedissonFairLock 2021-02-01 17:38:09.723 | cmdb - INFO | MyThread: 0 | com.xm.ggn.test.redisson.Client | line:108 - MyThread: 0 釋放鎖 2021-02-01 17:38:09.729 | cmdb - INFO | MyThread: 1 | com.xm.ggn.test.redisson.Client | line:101 - MyThread: 1 獲取鎖 2021-02-01 17:38:14.729 | cmdb - INFO | MyThread: 1 | com.xm.ggn.test.redisson.Client | line:108 - MyThread: 1 釋放鎖 2021-02-01 17:38:14.732 | cmdb - INFO | MyThread: 2 | com.xm.ggn.test.redisson.Client | line:101 - MyThread: 2 獲取鎖 2021-02-01 17:38:19.732 | cmdb - INFO | MyThread: 2 | com.xm.ggn.test.redisson.Client | line:108 - MyThread: 2 釋放鎖 2021-02-01 17:38:19.734 | cmdb - INFO | MyThread: 3 | com.xm.ggn.test.redisson.Client | line:101 - MyThread: 3 獲取鎖 2021-02-01 17:38:24.734 | cmdb - INFO | MyThread: 3 | com.xm.ggn.test.redisson.Client | line:108 - MyThread: 3 釋放鎖 2021-02-01 17:38:24.737 | cmdb - INFO | MyThread: 4 | com.xm.ggn.test.redisson.Client | line:101 - MyThread: 4 獲取鎖 2021-02-01 17:38:29.738 | cmdb - INFO | MyThread: 4 | com.xm.ggn.test.redisson.Client | line:108 - MyThread: 4 釋放鎖
3. 讀寫鎖的使用
類似於JDK的讀寫鎖,讀鎖共享,寫鎖排斥。
private static void lock4() { for (int i = 0; i < 3; i++) { try { Thread.sleep(1 * 1000); } catch (InterruptedException e) { } new Thread(new Runnable() { @Override public void run() { log.info(Thread.currentThread().getName() + " \t 運行"); RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(KEY_LOCKED); readWriteLock.readLock().lock(); log.info(Thread.currentThread().getName() + " \t 獲取讀鎖"); try { // 模擬處理邏輯用時5s Thread.sleep(5 * 1000); } catch (InterruptedException e) { } readWriteLock.readLock().unlock(); log.info(Thread.currentThread().getName() + " \t 釋放讀鎖"); readWriteLock.writeLock().lock(); log.info(Thread.currentThread().getName() + " \t 獲取寫鎖"); try { // 模擬處理邏輯用時5s Thread.sleep(5 * 1000); } catch (InterruptedException e) { } readWriteLock.writeLock().unlock(); log.info(Thread.currentThread().getName() + " \t 釋放寫鎖"); } }).start(); } }
結果:
2021-02-01 20:17:34.375 | cmdb - INFO | Thread-1 | com.xm.ggn.test.redisson.Client | line:37 - Thread-1 運行 2021-02-01 20:17:34.399 | cmdb - INFO | Thread-1 | com.xm.ggn.test.redisson.Client | line:41 - Thread-1 獲取讀鎖 2021-02-01 20:17:35.374 | cmdb - INFO | Thread-2 | com.xm.ggn.test.redisson.Client | line:37 - Thread-2 運行 2021-02-01 20:17:35.376 | cmdb - INFO | Thread-2 | com.xm.ggn.test.redisson.Client | line:41 - Thread-2 獲取讀鎖 2021-02-01 20:17:36.374 | cmdb - INFO | Thread-3 | com.xm.ggn.test.redisson.Client | line:37 - Thread-3 運行 2021-02-01 20:17:36.376 | cmdb - INFO | Thread-3 | com.xm.ggn.test.redisson.Client | line:41 - Thread-3 獲取讀鎖 2021-02-01 20:17:39.404 | cmdb - INFO | Thread-1 | com.xm.ggn.test.redisson.Client | line:48 - Thread-1 釋放讀鎖 2021-02-01 20:17:40.378 | cmdb - INFO | Thread-2 | com.xm.ggn.test.redisson.Client | line:48 - Thread-2 釋放讀鎖 2021-02-01 20:17:41.378 | cmdb - INFO | Thread-3 | com.xm.ggn.test.redisson.Client | line:48 - Thread-3 釋放讀鎖 2021-02-01 20:17:41.379 | cmdb - INFO | Thread-3 | com.xm.ggn.test.redisson.Client | line:51 - Thread-3 獲取寫鎖 2021-02-01 20:17:46.381 | cmdb - INFO | Thread-3 | com.xm.ggn.test.redisson.Client | line:58 - Thread-3 釋放寫鎖 2021-02-01 20:17:46.383 | cmdb - INFO | Thread-2 | com.xm.ggn.test.redisson.Client | line:51 - Thread-2 獲取寫鎖 2021-02-01 20:17:51.385 | cmdb - INFO | Thread-2 | com.xm.ggn.test.redisson.Client | line:58 - Thread-2 釋放寫鎖 2021-02-01 20:17:51.389 | cmdb - INFO | Thread-1 | com.xm.ggn.test.redisson.Client | line:51 - Thread-1 獲取寫鎖 2021-02-01 20:17:56.390 | cmdb - INFO | Thread-1 | com.xm.ggn.test.redisson.Client | line:58 - Thread-1 釋放寫鎖
至此簡單研究下redisson分布式鎖使用。一般是基於AOP自定義注解實現分布式鎖。
3. 基於AOP封裝分布式鎖
1.配置類
package com.xm.ggn.test.redisson; import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @Author: qlq * @Description * @Date: 21:49 2021/2/1 */ @Configuration public class RedissonConfiguration { @Bean public RedissonClient redissonClient() { // 1. Create config object Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); // 2. Create Redisson instance return Redisson.create(config); } }
2. 分布式鎖注解
package com.xm.ggn.test.redisson.anno; import java.lang.annotation.*; /** * @author: 喬利強 * @date: 2021/2/1 20:43 * @description: */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface DistributedLock { /** * 分布式鎖的key,可以理解為前綴 */ String value(); /** * 分布式鎖的最長等待時間 */ long waitTime() default 60; /** * 最長持有時間 */ long holdTime() default 60; }
3.分布式鎖參數注解,用於打在參數上,針對不同的參數生成不同的鎖 (這種相當於是使鎖更加細粒度)
package com.xm.ggn.test.redisson.anno; import java.lang.annotation.*; /** * key 注解,用於特殊標記分布式鎖的key,用於對不同的參數生成不同的鎖 */ @Target({ElementType.PARAMETER, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface DistributedLockKey { }
4. LockAspect AOP處理
package com.xm.ggn.test.redisson; import com.xm.ggn.test.redisson.anno.DistributedLock; import com.xm.ggn.test.redisson.anno.DistributedLockKey; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.sql.SQLException; import java.util.concurrent.TimeUnit; /** * @author: 喬利強 * @date: 2021/2/1 20:57 * @description: */ @Component @Aspect @Slf4j public class LockAspect { @Autowired private RedissonClient redissonClient; @Around("@annotation(com.xm.ggn.test.redisson.anno.DistributedLock)") public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable { // 1.方法執行前的處理,相當於前置通知 // 獲取方法簽名 MethodSignature methodSignature = (MethodSignature) pjp.getSignature(); // 獲取方法 Method method = methodSignature.getMethod(); // 獲取方法上面的注解 DistributedLock distributedLock = method.getAnnotation(DistributedLock.class); // 獲取操作描述的屬性值 String lockKey = distributedLock.value(); long waitTime = distributedLock.waitTime(); long holdTime = distributedLock.holdTime(); // method獲取參數信息。 如果參數帶有DistributedLockKey 注解,lockKey 拼接參數的value Parameter[] parameters = method.getParameters(); if (ArrayUtils.isNotEmpty(parameters)) { for (int index = 0, length_1 = parameters.length; index < length_1; index++) { DistributedLockKey annotation = parameters[index].getAnnotation(DistributedLockKey.class); if (annotation != null) { // 獲取參數值 Object[] args = pjp.getArgs(); String param = String.valueOf(args[index]); if (StringUtils.isNotBlank(param)) { lockKey += ":" + param; break; } } } } log.info("lockKey: {}", lockKey); RLock fairLock = redissonClient.getFairLock(lockKey); boolean lock = fairLock.tryLock(waitTime, holdTime, TimeUnit.SECONDS); if (!lock) { throw new RuntimeException("獲取鎖失敗"); } Object result = null; try { //讓代理方法執行 result = pjp.proceed(); // 2.相當於后置通知(方法成功執行之后走這里) } catch (SQLException e) { // 3.相當於異常通知部分 } finally { // 4.相當於最終通知 if (fairLock.isHeldByCurrentThread()) { fairLock.unlock(); } } return result; } }
5.測試Controller:
package com.xm.ggn.test.redisson; import com.xm.ggn.test.redisson.anno.DistributedLock; import com.xm.ggn.test.redisson.anno.DistributedLockKey; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @Author: qlq * @Description * @Date: 21:53 2021/2/1 */ @RestController("redissonTestController") @RequestMapping("/redisson") public class TestController { @GetMapping("test1") @DistributedLock("redissonTest:test1") public void test1(@DistributedLockKey String value1) { try { Thread.sleep(50 * 1000); } catch (InterruptedException e) { } } @GetMapping("test2") @DistributedLock("redissonTest:test2") public void test2(String value1) { try { Thread.sleep(50 * 1000); } catch (InterruptedException e) { } } }
6.訪問后查看redis
127.0.0.1:6379> keys * 1) "redissonTest:test2" 2) "redissonTest:test1:123456" 127.0.0.1:6379> hgetall "redissonTest:test2" 1) "f43c6823-021c-468b-8384-86fb2384775e:403" 2) "1" 127.0.0.1:6379> hgetall "redissonTest:test1:123456" 1) "f43c6823-021c-468b-8384-86fb2384775e:402" 2) "1"
補充:redisson實際也是基於lua腳本執行的。Redis 腳本使用 Lua 解釋器來執行腳本。 Redis 2.6 版本通過內嵌支持 Lua 環境。執行腳本的常用命令為 EVAL。Lua 是一種輕量小巧的腳本語言,用標准C語言編寫並以源代碼形式開放, 其設計目的是為了嵌入應用程序中,從而為應用程序提供靈活的擴展和定制功能。
(1)org.redisson.RedissonLock#tryLock() 源碼如下:
public boolean tryLock() { return ((Boolean)this.get(this.tryLockAsync())).booleanValue(); }
(2)org.redisson.RedissonLock#tryLockAsync()
public RFuture<Boolean> tryLockAsync() { return this.tryLockAsync(Thread.currentThread().getId()); }
(3)org.redisson.RedissonLock#tryLockAsync(long)
public RFuture<Boolean> tryLockAsync(long threadId) { return this.tryAcquireOnceAsync(-1L, -1L, (TimeUnit)null, threadId); }
(4)org.redisson.RedissonLock#tryAcquireOnceAsync
private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) { if (leaseTime != -1L) { return this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN); } else { RFuture<Boolean> ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN); ttlRemainingFuture.onComplete((ttlRemaining, e) -> { if (e == null) { if (ttlRemaining.booleanValue()) { this.scheduleExpirationRenewal(threadId); } } }); return ttlRemainingFuture; } }
(5)org.redisson.RedissonLock#tryLockInnerAsync 看出來也是執行了個lua腳本
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) { this.internalLockLeaseTime = unit.toMillis(leaseTime); return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)}); }
3. 封裝為獨立的lock組件,用於其他多個項目引入
1. 新建項目cloud-common-lock
2. 修改pom
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>cloud</artifactId> <groupId>cn.qz.cloud</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-common-lock</artifactId> <properties> <redisson.version>3.10.1</redisson.version> </properties> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- common --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <!-- 引入 spring aop 依賴 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>${redisson.version}</version> </dependency> </dependencies> </project>
3. 增加注解類
DistributedLock 用於AOP攔截
package cn.qz.lock.anno; import java.lang.annotation.*; @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface DistributedLock { /** * 分布式鎖的key,可以理解為前綴 */ String value(); /** * 分布式鎖的最長等待時間 */ long waitTime() default 60; /** * 最長持有時間 */ long holdTime() default 60; }
DistributedLockKey 用於標記在參數上,為不同的參數生成不同的redis的鎖的key
package cn.qz.lock.anno; import java.lang.annotation.*; /** * key 注解,用於特殊標記分布式鎖的key,用於對不同的參數生成不同的鎖 */ @Target({ElementType.PARAMETER, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface DistributedLockKey { }
EnableDistributedLock 用於引入分布式鎖的相關配置,開啟分布式鎖,使AOP生效
package cn.qz.lock.anno; import cn.qz.lock.config.LockAutoConfiguration; import org.springframework.context.annotation.Import; import java.lang.annotation.*; /** * 此注解的作用是開啟分布式鎖開關, 加此注解實際為了將bean注入到Spring。如果包名可以被掃描到不需要打此注解也可以 * * @Import 引入cn.qz.lock.config.LockAutoConfiguration,使其自動配置 */ @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented // @Import用來導入@Configuration注解的配置類、聲明@Bean注解的bean方法、導入ImportSelector的實現類或導入ImportBeanDefinitionRegistrar的實現類。 @Import({LockAutoConfiguration.class}) public @interface EnableDistributedLock { }
4. AOP處理類
package cn.qz.lock.aspect; import cn.qz.lock.anno.DistributedLock; import cn.qz.lock.anno.DistributedLockKey; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.sql.SQLException; import java.util.concurrent.TimeUnit; /** * @author: 喬利強 * @date: 2021/2/2 11:03 * @description: */ @Component @Aspect @Slf4j public class LockAspect { @Autowired private RedissonClient redissonClient; @Around("@annotation(cn.qz.lock.anno.DistributedLock)") public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable { // 1.方法執行前的處理,相當於前置通知 // 獲取方法簽名 MethodSignature methodSignature = (MethodSignature) pjp.getSignature(); // 獲取方法 Method method = methodSignature.getMethod(); // 獲取方法上面的注解 DistributedLock distributedLock = method.getAnnotation(DistributedLock.class); // 獲取操作描述的屬性值 String lockKey = distributedLock.value(); long waitTime = distributedLock.waitTime(); long holdTime = distributedLock.holdTime(); // method獲取參數信息。 如果參數帶有DistributedLockKey 注解,lockKey 拼接參數的value Parameter[] parameters = method.getParameters(); if (ArrayUtils.isNotEmpty(parameters)) { for (int index = 0, length_1 = parameters.length; index < length_1; index++) { DistributedLockKey annotation = parameters[index].getAnnotation(DistributedLockKey.class); if (annotation != null) { // 獲取參數值 Object[] args = pjp.getArgs(); String param = String.valueOf(args[index]); if (StringUtils.isNotBlank(param)) { lockKey += ":" + param; break; } } } } log.info("lockKey: {}", lockKey); RLock fairLock = redissonClient.getFairLock(lockKey); boolean lock = fairLock.tryLock(waitTime, holdTime, TimeUnit.SECONDS); if (!lock) { throw new RuntimeException("獲取鎖失敗"); } Object result = null; try { //讓代理方法執行 result = pjp.proceed(); // 2.相當於后置通知(方法成功執行之后走這里) } catch (SQLException e) { // 3.相當於異常通知部分 } finally { // 4.相當於最終通知 if (fairLock.isHeldByCurrentThread()) { fairLock.unlock(); } } return result; } }
5. 自動配置類
package cn.qz.lock.config; import cn.qz.lock.aspect.LockAspect; import org.redisson.spring.starter.RedissonAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.context.annotation.Import; /** * Lock 自動配置類。 該類的Configuration 注解沒有被掃描到是因為和Boot的主啟動類不在同一個子包下,所以掃描不到。通過cn.qz.lock.anno.EnableDistributedLock中的@Import可以引入 * 該類的主要作用是引入LockAspect,注冊到Spring中,使其AOP生效;也可以做一些其他的自動配置。 */ @Configuration @AutoConfigureAfter({RedissonAutoConfiguration.class}) @EnableAspectJAutoProxy( exposeProxy = true ) // @Import用來導入@Configuration注解的配置類、聲明@Bean注解的bean方法、導入ImportSelector的實現類或導入ImportBeanDefinitionRegistrar的實現類。 @Import({RedissonAutoConfiguration.class, LockAspect.class}) public class LockAutoConfiguration { public LockAutoConfiguration() { } }
6. 測試
(1) 其他項目引入依賴
<!--引入分布式鎖--> <dependency> <groupId>cn.qz.cloud</groupId> <artifactId>cloud-common-lock</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
(2) 主啟動類開啟分布式鎖
package cn.qz.cloud; import cn.qz.lock.anno.EnableDistributedLock; import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.cloud.netflix.hystrix.EnableHystrix; import org.springframework.context.annotation.Bean; /** * @Author: qlq * @Description * @Date: 22:08 2020/10/17 */ @SpringBootApplication @EnableEurekaClient @EnableCircuitBreaker @EnableHystrix // 開啟分布式鎖注解(此注解實際為了將bean注入到Spring,如果包名可以被掃描到不需要打此注解也可以) @EnableDistributedLock public class PaymentHystrixMain8081 { public static void main(String[] args) { SpringApplication.run(PaymentHystrixMain8081.class, args); } /** * 此配置是為了服務監控而配置,與服務容錯本身無關,springcloud升級后的坑 * ServletRegistrationBean因為SpringBoot的默認路徑不是 “/hystrix.stream" * 只要在自己的項目里配置上下的servlet就可以了 */ @Bean public ServletRegistrationBean getServlet() { HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet(); ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet); registrationBean.setLoadOnStartup(1); registrationBean.addUrlMappings("/hystrix.stream"); registrationBean.setName("HystrixMetricsStreamServlet"); return registrationBean; } }
(3) 建立 Controller進行測試
/********S 測試分布式鎖*****/ @GetMapping("test1") @DistributedLock("redissonTest:test1") public void test1(@DistributedLockKey String value1) { try { Thread.sleep(50 * 1000); } catch (InterruptedException e) { } } @GetMapping("test2") @DistributedLock("redissonTest:test2") public void test2(String value1) { try { Thread.sleep(50 * 1000); } catch (InterruptedException e) { } } /********E 測試分布式鎖*****/
(4) 訪問Controller后,查看redis生成的數據即可驗證分布式鎖是否生效
需要注意AOP的自調用問題。
補充:關於鎖的刪除問題
刪除的時候肯定不是任何一個線程都可以刪除,只有加鎖的線程可以刪除,這也就明白了上面用hash存儲並且key設計為UUID:ThreadId的原因,是為了在刪除的時候便於判斷刪除的線程是否是加鎖時候的線程。

比如:key里面包含了當前lock的id信息和加鎖時候的線程ID,value 是重入的次數。如下是lock的信息

查看其刪除方法:org.redisson.RedissonFairLock#unlockInnerAsync
protected RFuture<Boolean> unlockInnerAsync(long threadId) { return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "while true do local firstThreadId2 = redis.call('lindex', KEYS[2], 0);if firstThreadId2 == false then break;end; local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));if timeout <= tonumber(ARGV[4]) then redis.call('zrem', KEYS[3], firstThreadId2); redis.call('lpop', KEYS[2]); else break;end; end;if (redis.call('exists', KEYS[1]) == 0) then local nextThreadId = redis.call('lindex', KEYS[2], 0); if nextThreadId ~= false then redis.call('publish', KEYS[4] .. ':' .. nextThreadId, ARGV[1]); end; return 1; end;if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; end; redis.call('del', KEYS[1]); local nextThreadId = redis.call('lindex', KEYS[2], 0); if nextThreadId ~= false then redis.call('publish', KEYS[4] .. ':' .. nextThreadId, ARGV[1]); end; return 1; ", Arrays.asList(this.getName(), this.threadsQueueName, this.timeoutSetName, this.getChannelName()), new Object[]{LockPubSub.unlockMessage, this.internalLockLeaseTime, this.getLockName(threadId), System.currentTimeMillis()}); }
單獨拿出來其腳本如下:
while true do local firstThreadId2 = redis.call('lindex', KEYS[2], 0);if firstThreadId2 == false then break;end; local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));if timeout <= tonumber(ARGV[4]) then redis.call('zrem', KEYS[3], firstThreadId2); redis.call('lpop', KEYS[2]); else break;end; end;if (redis.call('exists', KEYS[1]) == 0) then local nextThreadId = redis.call('lindex', KEYS[2], 0); if nextThreadId ~= false then redis.call('publish', KEYS[4] .. ':' .. nextThreadId, ARGV[1]); end; return 1; end;if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; end; redis.call('del', KEYS[1]); local nextThreadId = redis.call('lindex', KEYS[2], 0); if nextThreadId ~= false then redis.call('publish', KEYS[4] .. ':' .. nextThreadId, ARGV[1]); end; return 1;
keys如下:

params如下:

