1. 程式人生 > 其它 >mysqljson轉字串,作為Java程式設計師

mysqljson轉字串,作為Java程式設計師

1.為什麼要使用分散式鎖

使用分散式鎖的目的,無外乎就是保證同一時間只有一個客戶端可以對共享資源進行操作。

1.1舉一個很長的例子

系統 A 是一個電商系統,目前是一臺機器部署,系統中有一個使用者下訂單的介面,但是使用者下訂單之前一定要去檢查一下庫存,確保庫存足夠了才會給使用者下單。由於系統有一定的併發,所以會預先將商品的庫存儲存在 Redis 中,使用者下單的時候會更新 Redis 的庫存。此時系統架構如下:

但是這樣一來會產生一個問題:假如某個時刻,Redis 裡面的某個商品庫存為 1。

此時兩個請求同時到來,其中一個請求執行到上圖的第 3 步,更新資料庫的庫存為 0,但是第 4 步還沒有執行。

而另外一個請求執行到了第 2 步,發現庫存還是 1,就繼續執行第 3 步。這樣的結果,是導致賣出了 2 個商品,然而其實庫存只有 1 個。

很明顯不對啊!這就是典型的庫存超賣問題。此時,我們很容易想到解決方案:用鎖把 2、3、4 步鎖住,讓他們執行完之後,另一個執行緒才能進來執行第 2 步。

按照上面的圖,在執行第 2 步時,使用 Java 提供的 Synchronized 或者 ReentrantLock 來鎖住,然後在第 4 步執行完之後才釋放鎖。

這樣一來,2、3、4 這 3 個步驟就被“鎖”住了,多個執行緒之間只能序列化執行

當整個系統的併發飆升,一臺機器扛不住了。現在要增加一臺機器,如下圖:

增加機器之後,系統變成上圖所示,假設此時兩個使用者的請求同時到來,但是落在了不同的機器上,那麼這兩個請求是可以同時執行了,還是會出現庫存超賣的問題。

因為上圖中的兩個 A 系統,執行在兩個不同的 JVM 裡面,他們加的鎖只對屬於自己 JVM 裡面的執行緒有效,對於其他 JVM 的執行緒是無效的。

因此,這裡的問題是:Java 提供的原生鎖機制在多機部署場景下失效了,這是因為兩臺機器加的鎖不是同一個鎖(兩個鎖在不同的 JVM 裡面)。

那麼,我們只要保證兩臺機器加的鎖是同一個鎖,問題不就解決了嗎?此時,就該分散式鎖隆重登場了。

分散式鎖的思路是:在整個系統提供一個全域性、唯一的獲取鎖的“東西”,然後每個系統在需要加鎖時,都去問這個“東西”拿到一把鎖,這樣不同的系統拿到的就可以認為是同一把鎖。

至於這個“東西”,可以是 Redis、Zookeeper,也可以是資料庫。此時的架構如圖:

通過上面的分析,我們知道了庫存超賣場景在分散式部署系統的情況下使用 Java 原生的鎖機制無法保證執行緒安全,所以我們需要用到分散式鎖的方案。

2.高效的分散式鎖

在設計分散式鎖的時候,應該考慮分散式鎖至少要滿足的一些條件,同時考慮如何高效的設計分散式鎖,以下幾點是必須要考慮的:

(1) 互斥

在分散式高併發的條件下,最需要保證在同一時刻只能有一個執行緒獲得鎖,這是最基本的一點。

(2) 防止死鎖

在分散式高併發的條件下,比如有個執行緒獲得鎖的同時,還沒有來得及去釋放鎖,就因為系統故障或者其它原因使它無法執行釋放鎖的命令,導致其它執行緒都無法獲得鎖,造成死鎖。所以分散式非常有必要設定鎖的有效時間,確保系統出現故障後,在一定時間內能夠主動去釋放鎖,避免造成死鎖的情況。

(3) 效能

對於訪問量大的共享資源,需要考慮減少鎖等待的時間,避免導致大量執行緒阻塞。

所以在鎖的設計時,需要考慮兩點。

1、 鎖的顆粒度要儘量小。比如你要通過鎖來減庫存,那這個鎖的名稱你可以設定成是商品的ID,而不是任取名稱。這樣這個鎖只對當前商品有效,鎖的顆粒度小。

2、 鎖的範圍儘量要小。比如只要鎖2行程式碼就可以解決問題的,那就不要去鎖10行程式碼了。

(4) 重入

我們知道ReentrantLock是可重入鎖,那它的特點就是:同一個執行緒可以重複拿到同一個資源的鎖。重入鎖非常有利於資源的高效利用。關於這點之後會做演示。

3.基於Redis實現分散式鎖

3.1 使用Redis命令實現分散式鎖

3.1.1加鎖

加鎖實際上就是在redis中,給Key鍵設定一個值,為避免死鎖,並給定一個過期時間。

使用的命令:SET lock_key random_value NX PX 5000

值得注意的是:

random_value 是客戶端生成的唯一的字串。

NX 代表只在鍵不存在時,才對鍵進行設定操作。

PX 5000 設定鍵的過期時間為5000毫秒。

也可以使用另外一條命令:SETNX key value

只不過過期時間無法設定。

這樣,如果上面的命令執行成功,則證明客戶端獲取到了鎖。

3.1.2解鎖

解鎖的過程就是將Key鍵刪除,但要保證安全性,舉個例子:客戶端1的請求不能將客戶端2的鎖給刪除掉。

釋放鎖涉及到兩條指令,這兩條指令不是原子性的,需要用到redis的lua指令碼支援特性,redis執行lua指令碼是原子性的。指令碼如下:

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

這種方式比較簡單,但是也有一個最重要的問題:鎖不具有可重入性

3.2使用Redisson實現分散式鎖

3.2.1Redisson介紹

Redisson是架設在Redis基礎上的一個Java駐記憶體資料網格(In-Memory Data Grid)。充分的利用了Redis鍵值資料庫提供的一系列優勢,基於Java實用工具包中常用介面,為使用者提供了一系列具有分散式特性的常用工具類。使得原本作為協調單機多執行緒併發程式的工具包獲得了協調分散式多機多執行緒併發系統的能力,大大降低了設計和研發大規模分散式系統的難度。同時結合各富特色的分散式服務,更進一步簡化了分散式環境中程式相互之間的協作。

3.2.2Redisson簡單使用

Config config = new Config(); 
config.useClusterServers() 
.addNodeAddress("redis://192.168.31.101:7001") 
.addNodeAddress("redis://192.168.31.101:7002") 
.addNodeAddress("redis://192.168.31.101:7003") 
.addNodeAddress("redis://192.168.31.102:7001") 
.addNodeAddress("redis://192.168.31.102:7002") 
.addNodeAddress("redis://192.168.31.102:7003"); 

RedissonClient redisson = Redisson.create(config); 

RLock lock = redisson.getLock("anyLock"); 

lock.lock(); 

lock.unlock(); 

只需要通過它的 API 中的 Lock 和 Unlock 即可完成分散式鎖,而且考慮了很多細節:

l Redisson 所有指令都通過 Lua 指令碼執行,Redis 支援 Lua 指令碼原子性執行

l Redisson 設定一個 Key 的預設過期時間為 30s,但是如果獲取鎖之後,會有一個WatchDog每隔10s將key的超時時間設定為30s。

另外,Redisson 還提供了對 Redlock 演算法的支援,它的用法也很簡單:

RedissonClient redisson = Redisson.create(config); 
RLock lock1 = redisson.getFairLock("lock1"); 
RLock lock2 = redisson.getFairLock("lock2"); 
RLock lock3 = redisson.getFairLock("lock3"); 
RedissonRedLock multiLock = new RedissonRedLock(lock1, lock2, lock3); 
multiLock.lock(); 

multiLock.unlock(); 

3.2.3Redisson原理分析

(1) 加鎖機制

執行緒去獲取鎖,獲取成功: 執行lua指令碼,儲存資料到redis資料庫。

執行緒去獲取鎖,獲取失敗: 一直通過while迴圈嘗試獲取鎖,獲取成功後,執行lua指令碼,儲存資料到redis資料庫。

(2) WatchDog自動延期機制

在一個分散式環境下,假如一個執行緒獲得鎖後,突然伺服器宕機了,那麼這個時候在一定時間後這個鎖會自動釋放,也可以設定鎖的有效時間(不設定預設30秒),這樣的目的主要是防止死鎖的發生。但是在實際情況中會有一種情況,業務處理的時間可能會大於鎖過期的時間,這樣就可能導致解鎖和加鎖不是同一個執行緒。所以WatchDog作用就是Redisson例項關閉前,不斷延長鎖的有效期。

如果程式呼叫加鎖方法顯式地給了有效期,是不會開啟後臺執行緒(也就是watch dog)進行延期的,如果沒有給有效期或者給的是-1,redisson會預設設定30s有效期並且會開啟後臺執行緒(watch dog)進行延期

多久進行一次延期:(預設有效期/3),預設有效期可以設定修改的,即預設情況下每隔10s設定有效期為30s

(3) 可重入加鎖機制

Redisson可以實現可重入加鎖機制的原因:

l Redis儲存鎖的資料型別是Hash型別

l Hash資料型別的key值包含了當前執行緒的資訊

下面是redis儲存的資料

這裡表面資料型別是Hash型別,Hash型別相當於我們java的 <key,<key1,value>> 型別,這裡key是指 'redisson'

它的有效期還有9秒,我們再來看裡們的key1值為078e44a3-5f95-4e24-b6aa-80684655a15a:45它的組成是:

guid + 當前執行緒的ID。後面的value是就和可重入加鎖有關。value代表同一客戶端呼叫lock方法的次數,即可重入計數統計。

舉圖說明

上面這圖的意思就是可重入鎖的機制,它最大的優點就是相同執行緒不需要在等待鎖,而是可以直接進行相應操作。

3.2.4 獲取鎖的流程

其中的指定欄位也就是hash結構中的field值(構成是uuid+執行緒id),即判斷鎖是否是當前執行緒

3.2.5 加鎖的流程

3.2.6 釋放鎖的流程

4. 使用Redis做分散式鎖的缺點

Redis有三種部署方式

l 單機模式

l Master-Slave+Sentienl選舉模式

l Redis Cluster模式

如果採用單機部署模式,會存在單點問題,只要 Redis 故障了。加鎖就不行了

採用 Master-Slave 模式,加鎖的時候只對一個節點加鎖,即便通過 Sentinel 做了高可用,但是如果 Master 節點故障了,發生主從切換,此時就會有可能出現鎖丟失的問題。

基於以上的考慮,Redis 的作者也考慮到這個問題,他提出了一個 RedLock 的演算法。

這個演算法的意思大概是這樣的:假設 Redis 的部署模式是 Redis Cluster,總共有 5 個 Master 節點。

通過以下步驟獲取一把鎖:

  • 獲取當前時間戳,單位是毫秒。
  • 輪流嘗試在每個 Master 節點上建立鎖,過期時間設定較短,一般就幾十毫秒。
  • 嘗試在大多數節點上建立一個鎖,比如 5 個節點就要求是 3 個節點(n / 2 +1)。
  • 客戶端計算建立好鎖的時間,如果建立鎖的時間小於超時時間,就算建立成功了。
  • 要是鎖建立失敗了,那麼就依次刪除這個鎖。
  • 只要別人建立了一把分散式鎖,你就得不斷輪詢去嘗試獲取鎖。

但是這樣的這種演算法,可能會出現節點崩潰重啟,多個客戶端持有鎖等其他問題,無法保證加鎖的過程一定正確。例如:

假設一共有5個Redis節點:A, B, C, D, E。設想發生瞭如下的事件序列:

(1)客戶端1成功鎖住了A, B, C,獲取鎖成功(但D和E沒有鎖住)。

(2)節點C崩潰重啟了,但客戶端1在C上加的鎖沒有持久化下來,丟失了。

(3)節點C重啟後,客戶端2鎖住了C, D, E,獲取鎖成功。

這樣,客戶端1和客戶端2同時獲得了鎖(針對同一資源)。

最後

由於篇幅限制,小編在此截出幾張知識講解的圖解

本文已被CODING開源專案:【一線大廠Java面試題解析+核心總結學習筆記+最新講解視訊+實戰專案原始碼】收錄