目錄html
redis分佈式鎖,Lua,Lua腳本,lua redis,redis lua 分佈式鎖,redis setnx ,redis分佈式鎖, Lua腳本在redis分佈式鎖場景的運用。java
鎖是一種能夠封鎖資源的東西。這種資源一般是共享的,一般會發生使用競爭的。python
須要保護共享資源正常使用,不出亂子。
比方說,公司只有一間廁所,這是個共享資源,你們須要共同使用這個廁所,因此避免不了有時候會發生競爭。若是一我的正在使用,另一我的進去了,咋辦呢?若是兩我的同時鑽進了一個廁所,那該怎麼辦?結果如何?誰先用,仍是一塊兒使用?特別的,假如是一男一女同時鑽進了廁所,事情會怎樣呢?反正我是不懂……程序員
若是這個時候廁所門前有個鎖,每一個人都無法隨便進入,而是須要先獲得鎖,才能進去。而獲得這個鎖,就須要裏邊的人先出來。這樣就能夠保證同一時刻,只有一我的在使用廁所,這我的在上廁所的期間不會有不安全的事情發生,不會中途被人闖進來了。redis
在 java 編碼的時候,爲了保護共享資源,使得多線程環境下,不會出現「很差的結果」。咱們可使用鎖來進行線程同步。因而咱們能夠根據具體的狀況使用synchronized 關鍵字來修飾一個方法,或者一段代碼。這個方法或者代碼就像是前文中提到的「受保護的廁所,加鎖的廁所」。也可使用 java 5之後的 Lock 來實現,與 synchronized 關鍵字相比,Lock 的使用更靈活,能夠有加鎖超時時間、公平性等優點。算法
上面咱們所說的 synchronized 關鍵字也好,Lock 也好。其實他們的做用範圍是啥,就是當前的應用啊。你的代碼在這個 jar 包或者這個 war 包裏邊,被部署在 A 機器上。那麼實際上咱們寫的 synchronized 關鍵字,就是在當前的機器的 JVM在執行代碼的時候發生做用的。假設這個代碼被部署到了三臺機器上 A,B,C。那麼 A 機器中的部署的代碼中的synchronized 關鍵字並不能控制 B,C 中的內容。spring
假如咱們須要在 A,B,C 三臺機器上運行某段程序的時候,實現「原子操做」,synchronized 關鍵字或者 Lock 是不能知足的。很顯然,這個時候咱們須要的鎖,是須要協同這三個節點的,因而,分佈式鎖就須要上場了,他就像是在A,B,C的外面加了一個層,經過它來實現鎖的控制。shell
在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; } }
這裏執行加鎖,不必定能成功。當別人正在持有鎖的時候,加鎖的線程須要繼續嘗試。這個「繼續嘗試」一般是「忙等待」,實現代碼以下:
/** * 獲取一個分佈式鎖 , 超時則返回失敗 * @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; }
根據上面所述,咱們在加鎖的時候執行了: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 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; } }
如此,咱們便實現了鎖的安全釋放。同時,咱們還須要結合業務邏輯,進行具體健壯性的保證,好比若是結束了必定不能忘記釋放鎖,異常了也要釋放鎖,某種狀況下是否須要回滾事務等。總結這個分佈式鎖使用的過程即是:
上面的文字中,咱們討論如何使用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的時候纔會發生,大多數狀況下、大多數系統均可以容忍,可是不是全部的系統都能容忍這種瑕疵。
爲了解決故障轉移狀況下的缺陷,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異常處理流程及解決方法