Lua腳本在redis分布式鎖場景的運用


redis分布式鎖,Lua,Lua腳本,lua redis,redis lua 分布式鎖,redis setnx ,redis分布式鎖, Lua腳本在redis分布式鎖場景的運用。

鎖和分布式鎖

鎖是什么?

鎖是一種可以封鎖資源的東西。這種資源通常是共享的,通常會發生使用競爭的。

為什么需要鎖?

需要保護共享資源正常使用,不出亂子。
比方說,公司只有一間廁所,這是個共享資源,大家需要共同使用這個廁所,所以避免不了有時候會發生競爭。如果一個人正在使用,另外一個人進去了,咋辦呢?如果兩個人同時鑽進了一個廁所,那該怎么辦?結果如何?誰先用,還是一起使用?特別的,假如是一男一女同時鑽進了廁所,事情會怎樣呢?反正我是不懂……

如果這個時候廁所門前有個鎖,每個人都沒法隨便進入,而是需要先得到鎖,才能進去。而得到這個鎖,就需要里邊的人先出來。這樣就可以保證同一時刻,只有一個人在使用廁所,這個人在上廁所的期間不會有不安全的事情發生,不會中途被人闖進來了。

Java中的鎖

在 java 編碼的時候,為了保護共享資源,使得多線程環境下,不會出現“不好的結果”。我們可以使用鎖來進行線程同步。於是我們可以根據具體的情況使用synchronized 關鍵字來修飾一個方法,或者一段代碼。這個方法或者代碼就像是前文中提到的“受保護的廁所,加鎖的廁所”。也可以使用 java 5以后的 Lock 來實現,與 synchronized 關鍵字相比,Lock 的使用更靈活,可以有加鎖超時時間、公平性等優勢。

分布式鎖

上面我們所說的 synchronized 關鍵字也好,Lock 也好。其實他們的作用范圍是啥,就是當前的應用啊。你的代碼在這個 jar 包或者這個 war 包里邊,被部署在 A 機器上。那么實際上我們寫的 synchronized 關鍵字,就是在當前的機器的 JVM在執行代碼的時候發生作用的。假設這個代碼被部署到了三台機器上 A,B,C。那么 A 機器中的部署的代碼中的synchronized 關鍵字並不能控制 B,C 中的內容。

假如我們需要在 A,B,C 三台機器上運行某段程序的時候,實現“原子操作”,synchronized 關鍵字或者 Lock 是不能滿足的。很顯然,這個時候我們需要的鎖,是需要協同這三個節點的,於是,分布式鎖就需要上場了,他就像是在A,B,C的外面加了一個層,通過它來實現鎖的控制。

redis 如何實現加鎖

在redis中,有一條命令,可以實現類似 “鎖” 的語法是這樣的:

SETNX key value

他的作用是,將 key 的值設為 value ,當且僅當 key 不存在。若給定的 key 已經存在,則 SETNX 不做任何動作。設置成功,返回 1 ;設置失敗,返回 0

使用 redis 來實現鎖的邏輯就是這樣的

線程 1 獲取鎖  -- > setnx mylock lockvalue
              -- >  1  獲取鎖成功
線程 2 獲取鎖  -- > setnx mylock lockvalue 
              -- >  0  獲取鎖失敗  (繼續等待,或者其他邏輯)
線程 1 釋放鎖  -- > 
線程 2 獲取鎖  -- > setnx mylock lockvalue
              -- > 1 獲取成功

鎖超時

在這個例子中,我們梳理了使用 redis setnx 命令 來實現鎖的邏輯。這里還需要考慮的是,鎖超時的問題 ,因為當線程 1 獲取了鎖之后,如果業務邏輯執行很長很長時間,那么其他線程只能死等,這可不行。所以需要加上超時,結合這些考慮的情況,實際的 Java 代碼可以這樣寫:

	public static boolean lock(String key,String lockValue,int expire){
		if(null == key){
			return false;
		}
		try {
			Jedis jedis = getJedisPool().getResource();
			String res = jedis.set(key,lockValue,"NX","EX",expire);
			jedis.close();
			return res!=null && res.equals("OK");
		} catch (Exception e) {
			return false;
		}
	}
	

retry

這里執行加鎖,不一定能成功。當別人正在持有鎖的時候,加鎖的線程需要繼續嘗試。這個“繼續嘗試”通常是“忙等待”,實現代碼如下:

	/**
	 * 獲取一個分布式鎖 , 超時則返回失敗
	 * @param key			鎖的key
	 * @param lockValue		鎖的value
	 * @param timeout		獲取鎖的等待時間,單位為 秒
     * @return				獲鎖成功 - true | 獲鎖失敗 - false
     */
	public static boolean tryLock(String key,String lockValue,int timeout,int expire){
		final long start = System.currentTimeMillis();
		if(timeout > expiredNx) {
			timeout = expiredNx;
		}
		final long end = start + timeout * 1000;
		boolean res = false; // 默認返回失敗
		while(!(res = lock(key,lockValue,expire))){ // 調用了上面的 lock方法
			if(System.currentTimeMillis() > end) {
				break;
			}
		}
		return res;
	}

redis 如何釋放鎖

根據上面所述,我們在加鎖的時候執行了:setnx mylock lockvalue , 這種加鎖的本質其實就是 “占座位”,我把一本書放在自習室第一排的第一個座位上,別人就不能坐了,就得等着我走了,把東西拿走了,他就可以使用這個座位了。所以很容易想到,在我們需要釋放鎖的時候,只需要調用 del mylock 就行了,這樣別的線程想去執行加鎖的時候執行就可以執行 setnx mylock lockvalue 了。

不該釋放的鎖

但是,直接執行del mylock 是有問題的,我們不能直接執行 del mylock 為什么?—— 會導致 “信號錯誤”,釋放了不該釋放的鎖 。假設如下場景:

時間線 線程1 線程2 線程3
時刻1 執行 setnx mylock val1 加鎖 執行 setnx mylock val2 加鎖 執行 setnx mylock val2 加鎖
時刻2 加鎖成功 加鎖失敗 加鎖失敗
時刻3 執行任務... 嘗試加鎖... 嘗試加鎖...
時刻4 任務繼續(鎖超時,自動釋放了) setnx 獲得了鎖(因為線程1的鎖超時釋放了) 仍然嘗試加鎖...
時刻5 任務完畢,del mylock 釋放鎖 執行任務中... 獲得了鎖(因為線程1釋放了線程2的)
...

上面的表格中,有兩個維度,一個是縱向的時間線,一個是橫線的線程並發競爭。我們可以發現線程 1 在開始的時候比較幸運,獲得了鎖,最先開始執行任務,但是,由於他比較耗時,最后鎖超時自動釋放了他都還沒執行完。 因此,線程 2 和線程3 的機會來了。而這一輪,線程2 比較幸運,得到了鎖。可是,當線程2正在執行任務期間,線程1 執行完了,還把線程2的鎖給釋放了。這就相當於,本來你鎖着門在廁所里邊尿尿,進行到一半的時候,別人進來了,因為他配了一把和你一模一樣的鑰匙!這就亂套了啊

因此,我們需要安全的釋放鎖——“不是我的鎖,我不能瞎釋放”。所以,我們在加鎖的時候,就需要標記“這是我的鎖”,在釋放的時候在判斷 “ 這是不是我的鎖?”。這里就需要在釋放鎖的時候加上邏輯判斷,合理的邏輯應該是這樣的:

1. 線程1 准備釋放鎖 , 鎖的key 為 mylock  鎖的 value 為 thread1_magic_num
2. 查詢當前鎖 current_value = get mylock
3. 判斷    if current_value == thread1_magic_num -- > 是  我(線程1)的鎖
          else                                   -- >不是 我(線程1)的鎖
4. 是我的鎖就釋放,否則不能釋放(而是執行自己的其他邏輯)。          

為了實現上面這個邏輯,我們是無法通過 redis 自帶的命令直接完成的。如果,再寫復雜的代碼去控制釋放鎖,則會讓整體代碼太過於復雜了。所以,我們引入了lua腳本。結合Lua 腳本實現釋放鎖的功能,更簡單,redis 執行lua腳本也是原子的,所以更合適,讓合適的人干合適的事,豈不更好。

通過Lua腳本實現鎖釋放

Lua是啥,Lua是一種功能強大,高效,輕量級,可嵌入的腳本語言。其官方的描述是:

Lua is a powerful, efficient, lightweight, embeddable scripting language. It supports procedural programming, object-oriented programming, functional programming, data-driven programming, and data description.

Lua 調用 redis 非常簡單,並且 Lua 腳本語法也易學,對於有別的編程語言基礎的程序員來說,在不學習Lua腳本語法的情況下,直接看 Lua 的代碼 也是可以看懂的。例子如下:

if redis.call('get', KEYS[1]) == ARGV[1] 
    then 
	    return redis.call('del', KEYS[1]) 
	else 
	    return 0 
end

上面的代碼,邏輯很簡單,if 中的比較如果是true , 那么 執行 del 並返回del結果;如果 if 結果為false 直接返回 0 。這不就滿足了我們釋放鎖的要求嗎?——“ 是我的鎖,我就釋放,不是我的鎖,我不能瞎釋放”。

其中的KEYS[1] , ARGV[1] 是參數,我們只調用 jedis 執行腳本的時候,傳遞這兩個參數就可以了。

使用redis + lua 來實現釋放鎖的代碼如下:

private static final Long lockReleaseOK = 1L;
static String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";// lua腳本,用來釋放分布式鎖

public static boolean releaseLock(String key ,String lockValue){
	if(key == null || lockValue == null) {
		return false;
	}
	try {
		Jedis jedis = getJedisPool().getResource();
		Object res =jedis.eval(luaScript,Collections.singletonList(key),Collections.singletonList(lockValue));
		jedis.close();
		return res!=null && res.equals(lockReleaseOK);
	} catch (Exception e) {
		return false;
	}
}

如此,我們便實現了鎖的安全釋放。同時,我們還需要結合業務邏輯,進行具體健壯性的保證,比如如果結束了一定不能忘記釋放鎖,異常了也要釋放鎖,某種情況下是否需要回滾事務等。總結這個分布式鎖使用的過程便是:

  • 加鎖時 key 同,value 不同。
  • 釋放鎖時,根據value判斷,是不是我的鎖,不能釋放別人的鎖。
  • 及時釋放鎖,而不是利用自動超時。
  • 鎖超時時間一定要結合業務情況權衡,過長,過短都不行。
  • 程序異常之處,要捕獲,並釋放鎖。如果需要回滾的,主動做回滾、補償。保證整體的健壯性,一致性。

用redis做分布式鎖真的靠譜嗎

上面的文字中,我們討論如何使用redis作為分布式鎖,並討論了一些細節問題,如鎖超時的問題、安全釋放鎖的問題。目前為止,似乎很完美的解決的我們想要的分布式鎖功能。然而事情並沒有這么簡單,用redis做分布式鎖並不“靠譜”。

不靠譜的情況

上面我們說的是redis,是單點的情況。如果是在redis sentinel集群中情況就有所不同了。關於redis sentinel 集群可以看這里。在redis sentinel集群中,我們具有多台redis,他們之間有着主從的關系,例如一主二從。我們的set命令對應的數據寫到主庫,然后同步到從庫。當我們申請一個鎖的時候,對應就是一條命令 setnx mykey myvalue ,在redis sentinel集群中,這條命令先是落到了主庫。假設這時主庫down了,而這條數據還沒來得及同步到從庫,sentinel將從庫中的一台選舉為主庫了。這時,我們的新主庫中並沒有mykey這條數據,若此時另外一個client執行 setnx mykey hisvalue , 也會成功,即也能得到鎖。這就意味着,此時有兩個client獲得了鎖。這不是我們希望看到的,雖然這個情況發生的記錄很小,只會在主從failover的時候才會發生,大多數情況下、大多數系統都可以容忍,但是不是所有的系統都能容忍這種瑕疵。

redlock

為了解決故障轉移情況下的缺陷,Antirez 發明了 Redlock 算法,使用redlock算法,需要多個redis實例,加鎖的時候,它會想多半節點發送 setex mykey myvalue 命令,只要過半節點成功了,那么就算加鎖成功了。釋放鎖的時候需要想所有節點發送del命令。這是一種基於【大多數都同意】的一種機制。感興趣的可以查詢相關資料。在實際工作中使用的時候,我們可以選擇已有的開源實現,python有redlock-py,java 中有Redisson redlock。

redlock確實解決了上面所說的“不靠譜的情況”。但是,它解決問題的同時,也帶來了代價。你需要多個redis實例,你需要引入新的庫 代碼也得調整,性能上也會有損。所以,果然是不存在“完美的解決方案”,我們更需要的是能夠根據實際的情況和條件把問題解決了就好。

至此,我大致講清楚了redis分布式鎖方面的問題(日后如果有新的領悟就繼續更新)。

redis單點、redis主從、redis集群cluster配置搭建與使用

Netty開發redis客戶端,Netty發送redis命令,netty解析redis消息

spring如何啟動的?這里結合spring源碼描述了啟動過程

SpringMVC是怎么工作的,SpringMVC的工作原理

spring 異常處理。結合spring源碼分析400異常處理流程及解決方法

Mybatis Mapper接口是如何找到實現類的-源碼分析

使用Netty實現HTTP服務器

Netty實現心跳機制

Netty系列

Lua腳本在redis分布式鎖場景的運用

CORS詳解,CORS原理分析
Docker & k8s 系列一:快速上手docker
Docker & k8s 系列二:本機k8s環境搭建
Docker & k8s 系列三:在k8s中部署單個服務實例
Docker & Kubenetes 系列四:集群,擴容,升級,回滾
alt 逃離沙漠公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM