redis分散式鎖-可重入鎖
上篇redis實現的分散式鎖,有一個問題,它不可重入。
所謂不可重入鎖,即若當前執行緒執行某個方法已經獲取了該鎖,那麼在方法中嘗試再次獲取鎖時,就會獲取不到被阻塞。 同一個人拿一個鎖 ,只能拿一次不能同時拿2次。
1、什麼是可重入鎖?它有什麼作用?
可重入鎖,也叫做遞迴鎖,指的是在同一執行緒內,外層函式獲得鎖之後,內層遞迴函式仍然可以獲取到該鎖。 說白了就是同一個執行緒再次進入同樣程式碼時,可以再次拿到該鎖。 它的作用是:防止在同一執行緒中多次獲取鎖而導致死鎖發生。
2、那麼java中誰實現了可重入鎖了?
在java的程式設計中synchronized 和 ReentrantLock都是可重入鎖。我們可以參考ReentrantLock的程式碼
3、基於ReentrantLock的可重入鎖
ReentrantLock,是一個可重入且獨佔式的鎖,是一種遞迴無阻塞的同步鎖。
3.1、看個ReentrantLock的例子
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public class ReentrantLockDemo {
//鎖
private static ReentrantLock lock = new ReentrantLock();
public void doSomething(int n){
try{
//進入遞迴第一件事:加鎖
lock.lock();
log.info("--------lock()執行後,getState()的值:{} lock.isLocked():{}",lock.getHoldCount(),lock.isLocked());
log.info("--------遞迴{}次--------",n);
if(n<=2){
this.doSomething(++n);
}else{
return;
}
}finally {
lock.unlock();
log.info("--------unlock()執行後,getState()的值:{} lock.isLocked():{}",lock.getHoldCount(),lock.isLocked());
}
}
public static void main(String[] args) {
ReentrantLockDemo reentrantLockDemo=new ReentrantLockDemo();
reentrantLockDemo.doSomething(1);
log.info("執行完doSomething方法 是否還持有鎖:{}",lock.isLocked());
}
}
3.2、執行結果
16:35:58.051 [main] INFO com.test.ReentrantLockDemo - --------lock()執行後,getState()的值:1 lock.isLocked():true
16:35:58.055 [main] INFO com.test.ReentrantLockDemo - --------遞迴1次--------
16:35:58.055 [main] INFO com.test.ReentrantLockDemo - --------lock()執行後,getState()的值:2 lock.isLocked():true
16:35:58.055 [main] INFO com.test.ReentrantLockDemo - --------遞迴2次--------
16:35:58.055 [main] INFO com.test.ReentrantLockDemo - --------lock()執行後,getState()的值:3 lock.isLocked():true
16:35:58.055 [main] INFO com.test.ReentrantLockDemo - --------遞迴3次--------
16:35:58.055 [main] INFO com.test.ReentrantLockDemo - --------unlock()執行後,getState()的值:2 lock.isLocked():true
16:35:58.055 [main] INFO com.test.ReentrantLockDemo - --------unlock()執行後,getState()的值:1 lock.isLocked():true
16:35:58.055 [main] INFO com.test.ReentrantLockDemo - --------unlock()執行後,getState()的值:0 lock.isLocked():false
16:35:58.055 [main] INFO com.test.ReentrantLockDemo - 執行完doSomething方法 是否還持有鎖:false
3.3、 從上面栗子可以看出ReentrantLock是可重入鎖,那麼他是如何實現的了,我們看下原始碼就知道了
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//先判斷,c(state)是否等於0,如果等於0,說明沒有執行緒持有鎖
if (c == 0) {
//通過cas方法把state的值0替換成1,替換成功說明加鎖成功
if (compareAndSetState(0, acquires)) {
//如果加鎖成功,設定持有鎖的執行緒是當前執行緒
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {//判斷當前持有鎖的執行緒是否是當前執行緒
//如果是當前執行緒,則state值加acquires,代表了當前執行緒加鎖了多少次
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
ReentrantLock的加鎖流程是:
1,先判斷是否有執行緒持有鎖,沒有加鎖進行加鎖
2、如果加鎖成功,則設定持有鎖的執行緒是當前執行緒
3、如果有執行緒持有了鎖,則再去判斷,是否是當前執行緒持有了鎖
4、如果是當前執行緒持有鎖,則加鎖數量(state)+1
/**
* 釋放鎖
* @param releases
* @return
*/
protected final boolean tryRelease(int releases) {
int c = getState() - releases;//state-1 減加鎖次數
//如果持有鎖的執行緒,不是當前執行緒,丟擲異常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {//如果c==0了說明當前執行緒,已經要釋放鎖了
free = true;
setExclusiveOwnerThread(null);//設定當前持有鎖的執行緒為null
}
setState(c);//設定c的值
return free;
}
看ReentrantLock的解鎖程式碼我們知道,每次釋放鎖的時候都對state減1,
當c值等於0的時候,說明鎖重入次數也為0了,
最終設定當前持有鎖的執行緒為null,state也設定為0,鎖就釋放了。
4、那麼redis要怎麼實現可重入的操作了?
看ReentrantLock的原始碼我們知道,它是加鎖成功了,記錄了當前持有鎖的執行緒,並通過一個int型別的數字,來記錄了加鎖次數。
我們知道ReentrantLock的實現原理了,那麼redis只要下面兩個問題解決,就能實現重入鎖了:
1、怎麼儲存當前持有的執行緒
2、加鎖次數(重入了多少次),怎麼記錄維護
4.1、第一個問題:怎麼儲存當前持有的執行緒
1.上一篇文章我們用的是redis 的set命令存的是string型別,他能儲存當前持有的執行緒嗎?
valus值我們可以儲存當前執行緒的id來解決。
2. 但是叢集環境下我們執行緒id可能是重複了那怎麼解決?
專案在啟動的生成一個全域性程式id,使用程式id+執行緒id 那就是唯一的了
4.2、第二個問題:加鎖次數(重入了多少次),怎麼記錄維護
-
他能記錄下來加鎖次數嗎?
如果valus值存的格式是:系程式id+執行緒id+加鎖次數,那可以實現 -
存沒問題了,但是重入次數要怎麼維護了, 它肯定要保證原子性的,能解決嗎?
好像用java程式碼或者lua指令碼都沒法解決,因為都是實現都需要兩步來維護這個重入次數的
- 第一步:先獲取到valus值,把取到加鎖次數+1
- 第二部:把新的值再設定進去
- 在執行第二步操作之前,如果這個key失效了(設定持有鎖超時了),如果還能再設定進去,就會有併發問題了
5、我們已經知道SET是不支援重入鎖的,但我們需要重入鎖,怎麼辦呢?
目前對於redis的重入鎖業界還是有很多解決方案的,最流行的就是採用Redisson。
6、什麼是 Redisson?
Redisson是Redis官方推薦的Java版的Redis客戶端。 它基於Java實用工具包中常用介面,為使用者提供了一系列具有分散式特性的常用工具類。 它在網路通訊上是基於NIO的Netty框架,保證網路通訊的高效能。 在分散式鎖的功能上,它提供了一系列的分散式鎖;如:可重入鎖(Reentrant Lock)、公平鎖(Fair Lock、聯鎖(MultiLock)、 紅鎖(RedLock)、 讀寫鎖(ReadWriteLock)等等。
7、Redisson的分佈鎖如何使用
引入依賴包
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.15.5</version>
</dependency>
程式碼
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 org.redisson.config.SingleServerConfig;
@Slf4j
public class ReentrantLockDemo1 {
//鎖
public static RLock lock;
static {
//Redisson需要的配置
Config config = new Config();
String node = "127.0.0.1:6379";//redis地址
node = node.startsWith("redis://") ? node : "redis://" + node;
SingleServerConfig serverConfig = config.useSingleServer()
.setAddress(node)
.setTimeout(3000)//超時時間
.setConnectionPoolSize(10)
.setConnectionMinimumIdleSize(10);
//serverConfig.setPassword("123456");//設定redis密碼
// 建立RedissonClient客戶端例項
RedissonClient redissonClient = Redisson.create(config);
//建立redisson的分散式鎖
RLock rLock = redissonClient.getLock("666");
lock = rLock;
}
public void doSomething(int n){
try{
//進入遞迴第一件事:加鎖
lock.lock();
log.info("--------lock()執行後,getState()的值:{} lock.isLocked():{}",lock.getHoldCount(),lock.isLocked());
log.info("--------遞迴{}次--------",n);
if(n<=2){
this.doSomething(++n);
}else{
return;
}
}finally {
lock.unlock();
log.info("--------unlock()執行後,getState()的值:{} lock.isLocked():{}",lock.getHoldCount(),lock.isLocked());
}
}
public static void test(){
log.info("--------------start---------------");
ReentrantLockDemo1 reentrantLockDemo=new ReentrantLockDemo1();
reentrantLockDemo.doSomething(1);
log.info("執行完doSomething方法 是否還持有鎖:{}",ReentrantLockDemo1.lock.isLocked());
log.info("--------------end---------------");
}
public static void main(String[] args) {
test();
}
}
執行結果
2021-05-23 22:49:01.322 INFO 69041 --- [nio-9090-exec-1] org.redisson.Version : Redisson 3.15.5
2021-05-23 22:49:01.363 INFO 69041 --- [sson-netty-5-22] o.r.c.pool.MasterConnectionPool : 10 connections initialized for /127.0.0.1:6379
2021-05-23 22:49:01.363 INFO 69041 --- [sson-netty-5-23] o.r.c.pool.MasterPubSubConnectionPool : 1 connections initialized for /127.0.0.1:6379
2021-05-23 22:49:01.367 INFO 69041 --- [nio-9090-exec-1] com.test.ReentrantLockDemo1 : --------------start---------------
2021-05-23 22:49:01.435 INFO 69041 --- [nio-9090-exec-1] com.test.ReentrantLockDemo1 : --------lock()執行後,getState()的值:1 lock.isLocked():true
2021-05-23 22:49:01.436 INFO 69041 --- [nio-9090-exec-1] com.test.ReentrantLockDemo1 : --------遞迴1次--------
2021-05-23 22:49:01.442 INFO 69041 --- [nio-9090-exec-1] com.test.ReentrantLockDemo1 : --------lock()執行後,getState()的值:2 lock.isLocked():true
2021-05-23 22:49:01.442 INFO 69041 --- [nio-9090-exec-1] com.test.ReentrantLockDemo1 : --------遞迴2次--------
2021-05-23 22:49:01.448 INFO 69041 --- [nio-9090-exec-1] com.test.ReentrantLockDemo1 : --------lock()執行後,getState()的值:3 lock.isLocked():true
2021-05-23 22:49:01.448 INFO 69041 --- [nio-9090-exec-1] com.test.ReentrantLockDemo1 : --------遞迴3次--------
2021-05-23 22:49:01.456 INFO 69041 --- [nio-9090-exec-1] com.test.ReentrantLockDemo1 : --------unlock()執行後,getState()的值:2 lock.isLocked():true
2021-05-23 22:49:01.461 INFO 69041 --- [nio-9090-exec-1] com.test.ReentrantLockDemo1 : --------unlock()執行後,getState()的值:1 lock.isLocked():true
2021-05-23 22:49:01.465 INFO 69041 --- [nio-9090-exec-1] com.test.ReentrantLockDemo1 : --------unlock()執行後,getState()的值:0 lock.isLocked():false
2021-05-23 22:49:01.467 INFO 69041 --- [nio-9090-exec-1] com.test.ReentrantLockDemo1 : 執行完doSomething方法 是否還持有鎖:false
2021-05-23 22:49:01.467 INFO 69041 --- [nio-9090-exec-1] com.test.ReentrantLockDemo1 : --------------end---------------
看控制檯列印能清楚知道Redisson是支援可重入鎖了。
8、那麼Redisson是如何實現的了?
我們跟一下lock.lock()的程式碼,發現它最終呼叫的是org.redisson.RedissonLock#tryLockInnerAsync的方法,具體如下:
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return evalWriteAsync(getRawName(), 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(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}
8.1、上面的程式碼,用到的redis命令先梳理一下
- exists 查詢一個key是否存在
EXISTS key [key ...]
返回值
如下的整數結果
1 如果key存在
0 如果key不存在
- hincrby :將hash中指定域的值增加給定的數字
- pexpire:設定key的有效時間以毫秒為單位
- hexists:判斷field是否存在於hash中
- pttl:獲取key的有效毫秒數
8.2、看lua指令碼傳入的引數我們知道:
- KEYS[1] = key的值
- ARGV[1]) = 持有鎖的時間
- ARGV[2] = getLockName(threadId) 下面id就算系統在啟動的時候會全域性生成的uuid 來作為當前程式的id,加上執行緒id就是getLockName(threadId)了,可以理解為:程式ID+系統ID = ARGV[2]
protected String getLockName(long threadId) {
return id + ":" + threadId;
}
8.3、程式碼截圖
從截圖上可以看到,它是使用lua指令碼來保證多個命令執行的原子性,使用了hash來實現了分散式鎖
現在我們來看下lua指令碼的加鎖流程
8.4、第一個if判斷
- 204行:它是先判斷了當前key是否存在,從EXISTS命令我們知道返回值是0說明key不存在,說明沒有加鎖
- 205行:hincrby命令是對 ARGV[2] = 程式ID+系統ID 進行原子自增加1
- 206行:是對整個hash設定過期期間
8.5、下面來看第二個if判斷
- 209行:判斷field是否存在於hash中,如果存在返回1,返回1說明是當前程式+當前執行緒ID 之前已經獲得到鎖了
- 210行:hincrby命令是對 ARGV[2] = 程式ID+系統ID 進行原子自增加1,說明重入次數加1了
- 211行:再對整個hash設定過期期間
8.6、下圖是redis視覺化工具看到是如何在hash儲存的結構
Redisson的整個加鎖流程跟ReentrantLock的加鎖邏輯基本相同
8.7、解鎖程式碼位於 org.redisson.RedissonLock#unlockInnerAsync,如下:
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"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; " +
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end; " +
"return nil;",
Arrays.asList(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}
看這個解鎖的Lua指令碼,流程跟Reentrantlock的解鎖邏輯也基本相同沒啥好說的了。