6000 字 |Redis 分佈式鎖|從青銅到鑽石的演進方案

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上篇我們講到如何用本地內存和 Redis","attrs":{}},{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/3325c0e17956a9f65485038ba","title":"","type":null},"content":[{"type":"text","text":"https://xie.infoq.cn/article/3325c0e17956a9f65485038ba","attrs":{}}]},{"type":"text","text":" 做緩存來增強系統的性能,另外探討了加鎖解決緩存擊穿的問題。但是","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"本地加鎖","attrs":{}}],"attrs":{}},{"type":"text","text":"的方式在分佈式的場景下就不適用了,所以本文我們來探討下如何引入","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"分佈式鎖","attrs":{}}],"attrs":{}},{"type":"text","text":"解決本地鎖的問題。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本篇主要內容如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/52/5216847c25537a393c7479477a31ee65.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文的代碼和教程已經同步到 Github 和 碼雲。","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Github","attrs":{}},{"type":"text","text":": ","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/Jackson0714/PassJava-Platform","title":"","type":null},"content":[{"type":"text","text":"https://github.com/Jackson0714/PassJava-Platform","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"碼雲","attrs":{}},{"type":"text","text":":","attrs":{}},{"type":"link","attrs":{"href":"https://gitee.com/jayh2018/PassJava-Platform","title":"","type":null},"content":[{"type":"text","text":"https://gitee.com/jayh2018/PassJava-Platform","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"配套教程","attrs":{}},{"type":"text","text":":","attrs":{}},{"type":"link","attrs":{"href":"http://www.passjava.cn","title":"","type":null},"content":[{"type":"text","text":"http://www.passjava.cn","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"本文已同步至","attrs":{}},{"type":"text","text":":","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/Jackson0714/PassJava-Learning","title":"","type":null},"content":[{"type":"text","text":"https://github.com/Jackson0714/PassJava-Learning","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"一、本地鎖的問題","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先我們來回顧下本地鎖的問題:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前題目微服務被拆分成了四個微服務。前端請求進來時,會被轉發到不同的微服務。假如前端接收了 10 W 個請求,每個微服務接收 2.5 W 個請求,假如緩存失效了,每個微服務在訪問數據庫時加鎖,通過鎖(synchronzied 或 lock)來鎖住自己的線程資源,從而防止緩存擊穿。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這是一種本地加鎖的方式,在分佈式情況下會帶來數據不一致的問題:比如服務 A 獲取數據後,更新緩存 key =100,服務 B 不受服務 A 的鎖限制,併發去更新緩存 key = 99,最後的結果可能是 99 或 100,但這是一種未知的狀態,與期望結果不一致。流程圖如下所示:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/fd/fd4450687098dbd6a460180813617aca.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"二、什麼是分佈式鎖","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基於上面本地鎖的問題,我們需要一種支持分佈式集羣環境下的鎖:查詢 DB 時,只有一個線程能訪問,其他線程都需要等待第一個線程釋放鎖資源後,才能繼續執行。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以把鎖看成房門外的一把鎖,所有併發線程比作人,他們都想進入房間,房間內只能有一個人進入。當有人進入後,將門反鎖,其他人必須等待,直到進去的人出來。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/9b/9b8ce6f7c9e59ac2dc74083904b99e5a.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們來看下分佈式鎖的基本原理,如下圖所示:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/01/0164f68825cfd430d46b2ec89085d2d3.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們來分析下上圖的分佈式鎖:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1.前端將 10W 的高併發請求轉發給四個題目微服務。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2.每個微服務處理 2.5 W 個請求。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3.每個處理請求的線程在執行業務之前,需要先搶佔鎖。可以理解爲“佔坑”。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4.獲取到鎖的線程在執行完業務後,釋放鎖。可以理解爲“釋放坑位”。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"5.未獲取到的線程需要等待鎖釋放。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"6.釋放鎖後,其他線程搶佔鎖。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"7.重複執行步驟 4、5、6。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大白話解釋:所有請求的線程都去同一個地方","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"“佔坑”","attrs":{}}],"attrs":{}},{"type":"text","text":",如果有坑位,就執行業務邏輯,沒有坑位,就需要其他線程釋放“坑位”。這個坑位是所有線程可見的,可以把這個坑位放到 Redis 緩存或者數據庫,這篇講的就是如何用 Redis 做","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"“分佈式坑位”","attrs":{}}],"attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"三、Redis 的 SETNX","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Redis 作爲一個公共可訪問的地方,正好可以作爲“佔坑”的地方。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用 Redis 實現分佈式鎖的幾種方案,我們都是用 SETNX 命令(設置 key 等於某 value)。只是高階方案傳的參數個數不一樣,以及考慮了異常情況。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們來看下這個命令,","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"SETNX","attrs":{}}],"attrs":{}},{"type":"text","text":"是","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"set If not exist","attrs":{}}],"attrs":{}},{"type":"text","text":"的簡寫。意思就是當 key 不存在時,設置 key 的值,存在時,什麼都不做。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Redis 命令行中是這樣執行的:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"sh"},"content":[{"type":"text","text":"set NX\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以進到 redis 容器中來試下 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"SETNX","attrs":{}}],"attrs":{}},{"type":"text","text":" 命令。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"先進入容器:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"sh"},"content":[{"type":"text","text":"docker exec -it redid-cli\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後執行 SETNX 命令:將 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"wukong","attrs":{}}],"attrs":{}},{"type":"text","text":" 這個 key 對應的 value 設置成 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"1111","attrs":{}}],"attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"sh"},"content":[{"type":"text","text":"set wukong 1111 NX\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"返回 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"OK","attrs":{}}],"attrs":{}},{"type":"text","text":",表示設置成功。重複執行該命令,返回 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"nil","attrs":{}}],"attrs":{}},{"type":"text","text":"表示設置失敗。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a1/a198a8a2f7aa0c427e144c8c18814055.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"四、青銅方案","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們先用 Redis 的 SETNX 命令來實現最簡單的分佈式鎖。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.1 青銅原理","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們來看下流程圖:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/18/18d5123f916c6ababeb423c148affd63.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"多個併發線程都去 Redis 中申請鎖,也就是執行 setnx 命令,假設線程 A 執行成功,說明當前線程 A 獲得了。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其他線程執行 setnx 命令都會是失敗的,所以需要等待線程 A 釋放鎖。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"線程 A 執行完自己的業務後,刪除鎖。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其他線程繼續搶佔鎖,也就是執行 setnx 命令。因爲線程 A 已經刪除了鎖,所以又有其他線程可以搶佔到鎖了。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代碼示例如下,Java 中 setnx 命令對應的代碼爲 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"setIfAbsent","attrs":{}}],"attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"setIfAbsent 方法的第一個參數代表 key,第二個參數代表值。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"// 1.先搶佔鎖\nBoolean lock = redisTemplate.opsForValue().setIfAbsent(\"lock\", \"123\");\nif(lock) {\n // 2.搶佔成功,執行業務\n List typeEntityListFromDb = getDataFromDB();\n // 3.解鎖\n redisTemplate.delete(\"lock\");\n return typeEntityListFromDb;\n} else {\n // 4.休眠一段時間\n sleep(100);\n // 5.搶佔失敗,等待鎖釋放\n return getTypeEntityListByRedisDistributedLock();\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個小問題:那爲什麼需要休眠一段時間?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲該程序存在遞歸調用,可能會導致棧空間益處。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.2 青銅方案的缺陷","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"青銅之所以叫青銅,是因爲它是最初級的,肯定會帶來很多問題。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"設想一種家庭場景","attrs":{}},{"type":"text","text":":晚上小空一個人開鎖進入了房間,打開了電燈💡,然後突然","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"斷電","attrs":{}}],"attrs":{}},{"type":"text","text":"了,小空想開門出去,但是找不到門鎖位置,那小明就進不去了,外面的人也進不來。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/4a/4aa6c3ca920106b029107932fdaa61f2.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從技術的角度看:setnx 佔鎖成功,業務代碼出現異常或者服務器宕機,沒有執行刪除鎖的邏輯,就造成了死鎖。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那如何規避這個風險呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"設置鎖的自動過期時間,過一段時間後,自動刪除鎖,這樣其他線程就能獲取到鎖了。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"四、白銀方案","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.1 生活中的例子","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面提到的青銅方案會有死鎖問題,那我們就用上面的規避風險的方案來設計下,也就是我們的白銀方案。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a9/a9401e7ef4f7600feda21e51e9ae45ff.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"還是生活中的例子:小空開鎖成功後,給這款智能鎖設置了一個沙漏倒計時⏳,沙漏完後,門鎖自動打開。即使房間突然斷電,過一段時間後,鎖會自動打開,其他人就可以進來了。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.2 技術原理圖","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"和青銅方案不同的地方在於,在佔鎖成功後,設置鎖的過期時間,這兩步是分步執行的。如下圖所示:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/0b/0b2f822de7e63bee6490f3071d8847c7.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.3 示例代碼","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"清理 redis key 的代碼如下","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"// 在 10s 以後,自動清理 lock\nredisTemplate.expire(\"lock\", 10, TimeUnit.SECONDS);\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"完整代碼如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"// 1.先搶佔鎖\nBoolean lock = redisTemplate.opsForValue().setIfAbsent(\"lock\", \"123\");\nif(lock) {\n // 2.在 10s 以後,自動清理 lock\n redisTemplate.expire(\"lock\", 10, TimeUnit.SECONDS);\n // 3.搶佔成功,執行業務\n List typeEntityListFromDb = getDataFromDB();\n // 4.解鎖\n redisTemplate.delete(\"lock\");\n return typeEntityListFromDb;\n}\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.4 白銀方案的缺陷","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"白銀方案看似解決了線程異常或服務器宕機造成的鎖未釋放的問題,但還是存在其他問題:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲佔鎖和設置過期時間是分兩步執行的,所以如果在這兩步之間發生了異常,則鎖的過期時間根本就沒有設置成功。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以和青銅方案有一樣的問題:鎖永遠不能過期。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"五、黃金方案","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"5.1 原子指令","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的白銀方案中,佔鎖和設置鎖過期時間是分步兩步執行的,這個時候,我們可以聯想到什麼:事務的原子性(Atom)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"原子性","attrs":{}},{"type":"text","text":":多條命令要麼都成功執行,要麼都不執行。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"將兩步放在一步中執行:佔鎖+設置鎖過期時間。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Redis 正好支持這種操作:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"sh"},"content":[{"type":"text","text":"# 設置某個 key 的值並設置多少毫秒或秒 過期。\nset PX NX\n或\nset EX NX\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後可以通過如下命令查看 key 的變化","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"sh"},"content":[{"type":"text","text":"ttl \n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面演示下如何設置 key 並設置過期時間。注意:執行命令之前需要先刪除 key,可以通過客戶端或命令刪除。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"sh"},"content":[{"type":"text","text":"# 設置 key=wukong,value=1111,過期時間=5000ms\nset wukong 1111 PX 5000 NX\n# 查看 key 的狀態\nttl wukong\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"執行結果如下圖所示:每運行一次 ttl 命令,就可以看到 wukong 的過期時間就會減少。最後會變爲 -2(已過期)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/5e/5e7973ce1267106b07079ba971678558.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"5.2 技術原理圖","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"黃金方案和白銀方案的不同之處:獲取鎖的時候,也需要設置鎖的過期時間,這是一個原子操作,要麼都成功執行,要麼都不執行。如下圖所示:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ef/eff1c75b8a475e8b2f4ec5e9aa134ebb.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"5.3 示例代碼","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"設置 lock 的值等於 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"123","attrs":{}}],"attrs":{}},{"type":"text","text":",過期時間爲 10 秒。如果 10 秒 以後,lock 還存在,則清理 lock。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"setIfAbsent(\"lock\", \"123\", 10, TimeUnit.SECONDS);\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"5.4 黃金方案的缺陷","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們還是舉生活中的例子來看下黃金方案的缺陷。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"5.4.1 用戶 A 搶佔鎖","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e4/e49510e7799ee1c104a365722af07704.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用戶 A 先搶佔到了鎖,並設置了這個鎖 10 秒以後自動開鎖,鎖的編號爲 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"123","attrs":{}}],"attrs":{}},{"type":"text","text":"。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"10 秒以後,A 還在執行任務,此時鎖被自動打開了。","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"5.4.2 用戶 B 搶佔鎖","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/9b/9b11a91dd57a8d77269b5041f9066d8c.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用戶 B 看到房間的鎖打開了,於是搶佔到了鎖,設置鎖的編號爲 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"123","attrs":{}}],"attrs":{}},{"type":"text","text":",並設置了過期時間 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"10 秒","attrs":{}}],"attrs":{}},{"type":"text","text":"。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因房間內只允許一個用戶執行任務,所以用戶 A 和 用戶 B 執行任務","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"產生了衝突","attrs":{}}],"attrs":{}},{"type":"text","text":"。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用戶 A 在 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"15 s","attrs":{}}],"attrs":{}},{"type":"text","text":" 後,完成了任務,此時 用戶 B 還在執行任務。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用戶 A 主動打開了編號爲 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"123","attrs":{}}],"attrs":{}},{"type":"text","text":"的鎖。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用戶 B 還在執行任務,發現鎖已經被打開了。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用戶 B 非常生氣:","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"我還沒執行完任務呢,鎖怎麼開了?","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"5.4.3 用戶 C 搶佔鎖","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/92/9235c2481212b49fad066b98f2c4ba22.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用戶 B 的鎖被 A 主動打開後,A 離開房間,B 還在執行任務。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用戶 C 搶佔到鎖,C 開始執行任務。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因房間內只允許一個用戶執行任務,所以用戶 B 和 用戶 C 執行任務產生了衝突。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從上面的案例中我們可以知道,因爲用戶 A 處理任務所需要的時間","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"大於","attrs":{}},{"type":"text","text":"鎖自動清理(開鎖)的時間,所以在自動開鎖後,又有其他用戶搶佔到了鎖。當用戶 A 完成任務後,會把其他用戶搶佔到的鎖給主動打開。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"**這裏爲什麼會打開別人的鎖?**因爲鎖的編號都叫做 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"“123”","attrs":{}}],"attrs":{}},{"type":"text","text":",用戶 A 只認鎖 id,看見編號爲 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"“123”","attrs":{}}],"attrs":{}},{"type":"text","text":"的鎖就開,結果把用戶 B 的鎖打開了,此時用戶 B 還未執行完任務,當然生氣了。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"六、鉑金方案","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"6.1 生活中的例子","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的黃金方案的缺陷也很好解決,給每個鎖設置不同的編號不就好了~","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如下圖所示,B 搶佔的鎖是藍色的,和 A 搶佔到綠色鎖不一樣。這樣就不會被 A 打開了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"做了個動圖,方便理解:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/35/35d4b476b8b820b4e01c96ae9d067b34.gif","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"靜態圖更高清,可以看看:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/28/28a5d629a2619b93f889ead5776539e6.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"6.2 技術原理圖","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"與黃金方案的不同之處:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"設置鎖的過期時間時,還需要設置唯一編號。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"主動刪除鎖的時候,需要判斷鎖的編號是否和設置的一致,如果一致,則認爲是自己設置的鎖,可以進行主動刪除。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/30/30ab80646197b272f9724b1632c788a2.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"6.3 代碼示例","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"// 1.生成唯一 id\nString uuid = UUID.randomUUID().toString();\n// 2. 搶佔鎖\nBoolean lock = redisTemplate.opsForValue().setIfAbsent(\"lock\", uuid, 10, TimeUnit.SECONDS);\nif(lock) {\n System.out.println(\"搶佔成功:\" + uuid);\n // 3.搶佔成功,執行業務\n List typeEntityListFromDb = getDataFromDB();\n // 4.獲取當前鎖的值\n String lockValue = redisTemplate.opsForValue().get(\"lock\");\n // 5.如果鎖的值和設置的值相等,則清理自己的鎖\n if(uuid.equals(lockValue)) {\n System.out.println(\"清理鎖:\" + lockValue);\n redisTemplate.delete(\"lock\");\n }\n return typeEntityListFromDb;\n} else {\n System.out.println(\"搶佔失敗,等待鎖釋放\");\n // 4.休眠一段時間\n sleep(100);\n // 5.搶佔失敗,等待鎖釋放\n return getTypeEntityListByRedisDistributedLock();\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1.生成隨機唯一 id,給鎖加上唯一值。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2.搶佔鎖,並設置過期時間爲 10 s,且鎖具有隨機唯一 id。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3.搶佔成功,執行業務。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4.執行完業務後,獲取當前鎖的值。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"5.如果鎖的值和設置的值相等,則清理自己的鎖。","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"6.4 鉑金方案的缺陷","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的方案看似很完美,但還是存在問題:第 4 步和第 5 步並不是原子性的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/51/5143499893e60879ae6d2d473425c1d1.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"時刻:0s。線程 A 搶佔到了鎖。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"時刻:9.5s。線程 A 向 Redis 查詢當前 key 的值。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"時刻:10s。鎖自動過期。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"時刻:11s。線程 B 搶佔到鎖。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"時刻:12s。線程 A 在查詢途中耗時長,終於拿多鎖的值。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"時刻:13s。線程 A 還是拿自己設置的鎖的值和返回的值進行比較,值是相等的,清理鎖,但是這個鎖其實是線程 B 搶佔的鎖。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那如何規避這個風險呢?鑽石方案登場。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"七、鑽石方案","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的線程 A 查詢鎖和刪除鎖的邏輯不是原子性的,所以將查詢鎖和刪除鎖這兩步作爲原子指令操作就可以了。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"7.1 技術原理圖","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如下圖所示,紅色圈出來的部分是鑽石方案的不同之處。用腳本進行刪除,達到原子操作。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/8b/8b1286907922f4aadd95bfe0fc7aff07.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"7.2 代碼示例","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那如何用腳本進行刪除呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們先來看一下這段 Redis 專屬腳本:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"sh"},"content":[{"type":"text","text":"if redis.call(\"get\",KEYS[1]) == ARGV[1]\nthen\n return redis.call(\"del\",KEYS[1])\nelse\n return 0\nend\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這段腳本和鉑金方案的獲取key,刪除key的方式很像。先獲取 KEYS[1] 的 value,判斷 KEYS[1] 的 value 是否和 ARGV[1] 的值相等,如果相等,則刪除 KEYS[1]。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼這段腳本怎麼在 Java 項目中執行呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"分兩步:先定義腳本;用 redisTemplate.execute 方法執行腳本。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"// 腳本解鎖\nString script = \"if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end\";\nredisTemplate.execute(new DefaultRedisScript(script, Long.class), Arrays.asList(\"lock\"), uuid);\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的代碼中,KEYS[1] 對應","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"“lock”","attrs":{}}],"attrs":{}},{"type":"text","text":",ARGV[1] 對應 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"“uuid”","attrs":{}}],"attrs":{}},{"type":"text","text":",含義就是如果 lock 的 value 等於 uuid 則刪除 lock。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而這段 Redis 腳本是由 Redis 內嵌的 Lua 環境執行的,所以又稱作 Lua 腳本。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那鑽石方案是不是就完美了呢?有沒有更好的方案呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下篇,我們再來介紹另外一種分佈式鎖的王者方案:Redisson。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"八、總結","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本篇通過本地鎖的問題引申出分佈式鎖的問題。然後介紹了五種分佈式鎖的方案,由淺入深講解了不同方案的改進之處。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從上面幾種方案的不斷演進的過程中,知道了系統中哪些地方可能存在異常情況,以及該如何更好地進行處理。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"舉一反三,這種不斷演進的思維模式也可以運用到其他技術中。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面總結下上面五種方案的缺陷和改進之處。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"青銅方案","attrs":{}},{"type":"text","text":":","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"缺陷:業務代碼出現異常或者服務器宕機,沒有執行主動刪除鎖的邏輯,就造成了死鎖。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"改進:設置鎖的自動過期時間,過一段時間後,自動刪除鎖,這樣其他線程就能獲取到鎖了。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"白銀方案","attrs":{}},{"type":"text","text":":","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"缺陷:佔鎖和設置鎖過期時間是分步兩步執行的,不是原子操作。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"改進:佔鎖和設置鎖過期時間保證原子操作。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"黃金方案","attrs":{}},{"type":"text","text":":","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"缺陷:主動刪除鎖時,因鎖的值都是相同的,將其他客戶端佔用的鎖刪除了。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"改進:每次佔用的鎖,隨機設爲較大的值,主動刪除鎖時,比較鎖的值和自己設置的值是否相等。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"鉑金方案","attrs":{}},{"type":"text","text":":","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"缺陷:獲取鎖、比較鎖的值、刪除鎖,這三步是非原子性的。中途又可能鎖自動過期了,又被其他客戶端搶佔了鎖,導致刪鎖時把其他客戶端佔用的鎖刪了。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"改進:使用 Lua 腳本進行獲取鎖、比較鎖、刪除鎖的原子操作。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"鑽石方案","attrs":{}},{"type":"text","text":":","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"缺陷:非專業的分佈式鎖方案。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"改進:Redission 分佈式鎖。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"王者方案","attrs":{}},{"type":"text","text":",下篇見~","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述所有代碼都基於 PassJava 開源項目,後端、前端、小程序都上傳到同一個倉庫裏面了,大家可以通過 github 或 碼雲訪問。地址如下:","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Github","attrs":{}},{"type":"text","text":": ","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/Jackson0714/PassJava-Platform","title":"","type":null},"content":[{"type":"text","text":"https://github.com/Jackson0714/PassJava-Platform","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"碼雲","attrs":{}},{"type":"text","text":":","attrs":{}},{"type":"link","attrs":{"href":"https://gitee.com/jayh2018/PassJava-Platform","title":"","type":null},"content":[{"type":"text","text":"https://gitee.com/jayh2018/PassJava-Platform","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"配套教程","attrs":{}},{"type":"text","text":":","attrs":{}},{"type":"link","attrs":{"href":"http://www.passjava.cn","title":"","type":null},"content":[{"type":"text","text":"http://www.passjava.cn","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"本文已同步至","attrs":{}},{"type":"text","text":":","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/Jackson0714/PassJava-Learning","title":"","type":null},"content":[{"type":"text","text":"https://github.com/Jackson0714/PassJava-Learning","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"參考資料:http://redis.cn/commands/set.html","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"歡迎關注我的公衆號:「悟空聊架構」","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章