目錄
一、本機鎖定的問題" >一、本機鎖定的問題
二、什麼是分散式鎖定" >二、什麼是分散式鎖定
三、Redis 的 SETNX" >三、Redis 的 SETNX
四、青铜方案" >四、青铜方案
3.1 青铜原理" >3.1 青铜原理
3.2 青銅方案的缺陷" >3.2 青銅方案的缺陷
四、白銀方案" >四、白銀方案
6.1 生活中的例子" >6.1 生活中的例子
6.2 技術原理圖" >6.2 技術原理圖
5.3 示例代码" >5.3 示例代码
4.4 白银方案的缺陷" >4.4 白银方案的缺陷
五、黄金方案" >五、黄金方案
5.1 原子指令" >5.1 原子指令
7.1 技术原理图" >7.1 技术原理图
5.4 黄金方案的缺陷" >5.4 黄金方案的缺陷
5.4.1 用戶A 搶佔鎖
5.4.2 用戶B 搶佔鎖定
5.4.3 用戶C 搶佔鎖定
六、鉑金方案" >六、鉑金方案
7.2 代码示例" >7.2 代码示例
6.4 铂金方案的缺陷" >6.4 铂金方案的缺陷
七、钻石方案" >七、钻石方案
八、總結" >八、總結
首頁 Java java教程 Redis 分散式鎖|從青銅到鑽石的五種演進方案

Redis 分散式鎖|從青銅到鑽石的五種演進方案

Aug 23, 2023 pm 02:54 PM
redis

本篇主要內容如下:

Redis 分散式鎖|從青銅到鑽石的五種演進方案

一、本機鎖定的問題

#首先我們來回顧下本地鎖定的問題:

目前題目微服務被拆分成了四個微服務。前端請求進來時,會被轉送到不同的微服務。假如前端接收了10 W 個請求,每個微服務接收2.5 W 個請求,假如快取失效了,每個微服務在存取資料庫時加鎖,透過鎖(synchronziedlock )來鎖住自己的執行緒資源,從而防止快取擊穿

這是一種本地加鎖的方式,在分散式情況下會帶來資料不一致的問題:例如服務A 取得資料後,更新緩存key =100,服務B 不受服務A 的鎖定限制,並發去更新快取key = 99,最後的結果可能是99 或100,但這是未知的狀態,與期望結果不一致 。流程圖如下:

Redis 分散式鎖|從青銅到鑽石的五種演進方案

二、什麼是分散式鎖定

基於上面本地鎖定的問題,我們需要一個支援分散式叢集環境下的鎖定:查詢DB 時,只有一個執行緒可以訪問,其他執行緒都需要等待第一個執行緒釋放鎖定資源後,才能繼續執行。

生活中的案例:可以把鎖看成房門外的一把鎖定,所有並發線程比作,他們都想進入房間,房間內只能有一個人進入。當有人進入後,將門反鎖,其他人必須等待,直到進去的人出來。

Redis 分散式鎖|從青銅到鑽石的五種演進方案

我們來看下分散式鎖定的基本原理,如下圖所示:

Redis 分散式鎖|從青銅到鑽石的五種演進方案

我們來分析下上圖的分散式鎖定:

  • 1.前端將 10W 的高並發請求轉送給四個題目微服務。
  • 2.每個微服務處理 2.5 W 個請求。
  • 3.每個處理請求的執行緒在執行業務之前,需要先搶佔鎖定。可以理解為「佔坑」。
  • 4.取得到鎖定的執行緒在執行完業務後,釋放鎖定。可以理解為「釋放坑位」。
  • 5.未取得的執行緒需要等待鎖定釋放。
  • 6.釋放鎖定後,其他執行緒搶佔鎖定。
  • 7.重複步驟 4、5、6。

大白話解釋:所有請求的線程都去同一個地方「佔坑」,如果有坑位,就執行業務邏輯,沒有坑位,就需要其他線程釋放“坑位”。這個坑位是所有執行緒可見的,可以把這個坑位放到 Redis 快取或資料庫,這篇講的就是如何用 Redis 做「分散式坑位」

三、Redis 的 SETNX

Redis 作為一個公共可訪問的地方,剛好可以作為「佔坑」的地方。

用 Redis 實作分散式鎖定的幾個方案,我們都是用 SETNX 指令(設定 key 等於某 value)。只是高階方案傳的參數個數不一樣,也考慮了異常情況。

我們來看這個指令,SETNXset If not exist的簡寫。意思是當 key 不存在時,設定 key 的值,存在時,什麼都不做。

在 Redis 命令列中是這樣執行的:

set <key> <value> NX
登入後複製

我們可以轉到 redis 容器中來試下 SETNX 命令。

先進入容器:

docker exec -it <容器 id> redis-cli
登入後複製

然后执行 SETNX 命令:将 wukong 这个 key 对应的 value 设置成 1111

set wukong 1111 NX
登入後複製

返回 OK,表示设置成功。重复执行该命令,返回 nil表示设置失败。

Redis 分散式鎖|從青銅到鑽石的五種演進方案

四、青铜方案

我们先用 Redis 的 SETNX 命令来实现最简单的分布式锁。

3.1 青铜原理

我们来看下流程图:

Redis 分散式鎖|從青銅到鑽石的五種演進方案
  • 多个并发线程都去 Redis 中申请锁,也就是执行 setnx 命令,假设线程 A 执行成功,说明当前线程 A 获得了。
  • 其他线程执行 setnx 命令都会是失败的,所以需要等待线程 A 释放锁。
  • 线程 A 执行完自己的业务后,删除锁。
  • 其他线程继续抢占锁,也就是执行 setnx 命令。因为线程 A 已经删除了锁,所以又有其他线程可以抢占到锁了。

代码示例如下,Java 中 setnx 命令对应的代码为 setIfAbsent

setIfAbsent 方法的第一个参数代表 key,第二个参数代表值。

// 1.先抢占锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "123");
if(lock) {
  // 2.抢占成功,执行业务
  List<TypeEntity> typeEntityListFromDb = getDataFromDB();
  // 3.解锁
  redisTemplate.delete("lock");
  return typeEntityListFromDb;
} else {
  // 4.休眠一段时间
  sleep(100);
  // 5.抢占失败,等待锁释放
  return getTypeEntityListByRedisDistributedLock();
}
登入後複製

一个小问题:那为什么需要休眠一段时间?

因为该程序存在递归调用,可能会导致栈空间溢出。

3.2 青銅方案的缺陷

#青銅之所以叫青銅,是因為它是最初級的,肯定會帶來很多問題。

設想一種家庭場景:晚上小空一個人開鎖進入了房間,打開了電燈?,然後突然斷電了,小空想開門出去,但是找不到門鎖位置,那小明就進不去了,外面的人也進不來。

Redis 分散式鎖|從青銅到鑽石的五種演進方案

從技術的角度看:setnx 佔鎖成功,業務代碼出現異常或伺服器宕機,沒有執行刪除鎖的邏輯,就造成了死鎖

那要如何規避這個風險呢?

設定鎖的自動過期時間,過一段時間後,自動刪除鎖,這樣其他執行緒就能取得到鎖了。

四、白銀方案

4.1 生活中的例子

上面提到的青銅方案會有死鎖問題,那我們就用上面的規避風險的方案來設計下,也就是我們的白銀方案。

Redis 分散式鎖|從青銅到鑽石的五種演進方案

還是生活中的例子:小空開鎖成功後,給這款智慧鎖設定了一個沙漏倒數⏳,沙漏完後,門鎖自動打開。即使房間突然斷電,過一段時間後,鎖會自動打開,其他人就可以進來了。

4.2 技術原理圖

和青銅方案不同的地方在於,在佔鎖成功後,設定鎖的過期時間,這兩步是逐步執行的。如下圖所示:

Redis 分散式鎖|從青銅到鑽石的五種演進方案

4.3 示例代码

清理 redis key 的代码如下

// 在 10s 以后,自动清理 lock
redisTemplate.expire("lock", 10, TimeUnit.SECONDS);
登入後複製

完整代码如下:

// 1.先抢占锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "123");
if(lock) {
    // 2.在 10s 以后,自动清理 lock
    redisTemplate.expire("lock", 10, TimeUnit.SECONDS);
    // 3.抢占成功,执行业务
    List<TypeEntity> typeEntityListFromDb = getDataFromDB();
    // 4.解锁
    redisTemplate.delete("lock");
    return typeEntityListFromDb;
}
登入後複製

4.4 白银方案的缺陷

白银方案看似解决了线程异常或服务器宕机造成的锁未释放的问题,但还是存在其他问题:

因为占锁和设置过期时间是分两步执行的,所以如果在这两步之间发生了异常,则锁的过期时间根本就没有设置成功。

所以和青铜方案有一样的问题:锁永远不能过期

五、黄金方案

5.1 原子指令

上面的白银方案中,占锁和设置锁过期时间是分步两步执行的,这个时候,我们可以联想到什么:事务的原子性(Atom)。

原子性:多条命令要么都成功执行,要么都不执行。

将两步放在一步中执行:占锁+设置锁过期时间。

Redis 正好支持这种操作:

# 设置某个 key 的值并设置多少毫秒或秒 过期。
set <key> <value> PX <多少毫秒> NX
或
set <key> <value> EX <多少秒> NX
登入後複製

然后可以通过如下命令查看 key 的变化

ttl <key>
登入後複製

下面演示下如何设置 key 并设置过期时间。注意:执行命令之前需要先删除 key,可以通过客户端或命令删除。

# 设置 key=wukong,value=1111,过期时间=5000ms
set wukong 1111 PX 5000 NX
# 查看 key 的状态
ttl wukong
登入後複製

执行结果如下图所示:每运行一次 ttl 命令,就可以看到 wukong 的过期时间就会减少。最后会变为 -2(已过期)。

Redis 分散式鎖|從青銅到鑽石的五種演進方案

5.2 技术原理图

黄金方案和白银方案的不同之处:获取锁的时候,也需要设置锁的过期时间,这是一个原子操作,要么都成功执行,要么都不执行。如下图所示:

Redis 分散式鎖|從青銅到鑽石的五種演進方案

5.3 示例代码

设置 lock 的值等于 123,过期时间为 10 秒。如果 10 秒 以后,lock 还存在,则清理 lock。

setIfAbsent("lock", "123", 10, TimeUnit.SECONDS);
登入後複製

5.4 黄金方案的缺陷

我们还是举生活中的例子来看下黄金方案的缺陷。

5.4.1 用戶A 搶佔鎖

Redis 分散式鎖|從青銅到鑽石的五種演進方案
  • #用戶A 先搶佔了鎖,並設定了這個鎖10 秒以後自動開鎖,鎖的編號為123
  • 10 秒以後,A 還在執行任務,此時鎖定被自動開啟了。

5.4.2 用戶B 搶佔鎖定

Redis 分散式鎖|從青銅到鑽石的五種演進方案
  • #用戶B 看到房間的鎖打開了,於是搶佔到了鎖,設定鎖的編號為123,並設定了過期時間10 秒
  • 因為房間內只允許一個使用者執行任務,所以使用者 A 和 使用者 B 執行任務產生了衝突
  • 用戶 A 在 15 s 後,完成了任務,此時 使用者 B 還在執行任務。
  • 使用者 A 主動開啟了編號為 123的鎖定。
  • 用戶 B 還在執行任務,發現鎖定已經被打開了。
  • 用戶 B 非常生氣:我還沒執行完任務呢,鎖怎麼開了?

5.4.3 用戶C 搶佔鎖定

Redis 分散式鎖|從青銅到鑽石的五種演進方案
  • 用戶B 的鎖被A 主動打開後,A 離開房間,B 還在執行任務。
  • 用戶 C 搶占到鎖,C 開始執行任務。
  • 因為房間內只允許一個使用者執行任務,所以使用者 B 和 使用者 C 執行任務產生了衝突。

從上面的案例中我們可以知道,因為使用者A 處理任務所需的時間大於鎖定自動清理(開鎖)的時間,所以在自動開鎖後,又有其他用戶搶佔了鎖。當使用者 A 完成任務後,會把其他使用者搶佔的鎖主動開啟。

這裡為什麼會打開別人的鎖? 因為鎖的編號都叫做「123」,用戶A 只認鎖編號,看見編號為「123」的鎖就開,結果把用戶B 的鎖打開了,此時用戶B 還未執行完任務,當然生氣了。

六、鉑金方案

6.1 生活中的例子

上面的黃金方案的缺陷也很好解決,給每個鎖設定不同的編號不就好了~

如下圖所示,B 搶佔的鎖是藍色的,和A 搶占到綠色鎖不一樣。這樣就不會被 A 打開了。

做了個動圖,方便理解:

Redis 分散式鎖|從青銅到鑽石的五種演進方案
動圖示範

靜態圖更高清,可以看看:

Redis 分散式鎖|從青銅到鑽石的五種演進方案

6.2 技術原理圖

#與黃金方案的差異:

  • 设置锁的过期时间时,还需要设置唯一编号。
  • 主动删除锁的时候,需要判断锁的编号是否和设置的一致,如果一致,则认为是自己设置的锁,可以进行主动删除。
Redis 分散式鎖|從青銅到鑽石的五種演進方案

6.3 代码示例

// 1.生成唯一 id
String uuid = UUID.randomUUID().toString();
// 2. 抢占锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 10, TimeUnit.SECONDS);
if(lock) {
    System.out.println("抢占成功:" + uuid);
    // 3.抢占成功,执行业务
    List<TypeEntity> typeEntityListFromDb = getDataFromDB();
    // 4.获取当前锁的值
    String lockValue = redisTemplate.opsForValue().get("lock");
    // 5.如果锁的值和设置的值相等,则清理自己的锁
    if(uuid.equals(lockValue)) {
        System.out.println("清理锁:" + lockValue);
        redisTemplate.delete("lock");
    }
    return typeEntityListFromDb;
} else {
    System.out.println("抢占失败,等待锁释放");
    // 4.休眠一段时间
    sleep(100);
    // 5.抢占失败,等待锁释放
    return getTypeEntityListByRedisDistributedLock();
}
登入後複製
  • 1.生成随机唯一 id,给锁加上唯一值。
  • 2.抢占锁,并设置过期时间为 10 s,且锁具有随机唯一 id。
  • 3.抢占成功,执行业务。
  • 4.执行完业务后,获取当前锁的值。
  • 5.如果锁的值和设置的值相等,则清理自己的锁。

6.4 铂金方案的缺陷

上面的方案看似很完美,但还是存在问题:第 4 步和第 5 步并不是原子性的。

Redis 分散式鎖|從青銅到鑽石的五種演進方案
  • 时刻:0s。线程 A 抢占到了锁。

  • 时刻:9.5s。线程 A 向 Redis 查询当前 key 的值。

  • 时刻:10s。锁自动过期。

  • 时刻:11s。线程 B 抢占到锁。

  • 时刻:12s。线程 A 在查询途中耗时长,终于拿多锁的值。

  • 时刻:13s。线程 A 还是拿自己设置的锁的值和返回的值进行比较,值是相等的,清理锁,但是这个锁其实是线程 B 抢占的锁。

那如何规避这个风险呢?钻石方案登场。

七、钻石方案

上面的线程 A 查询锁和删除锁的逻辑不是原子性的,所以将查询锁和删除锁这两步作为原子指令操作就可以了。

7.1 技术原理图

如下图所示,红色圈出来的部分是钻石方案的不同之处。用脚本进行删除,达到原子操作。

Redis 分散式鎖|從青銅到鑽石的五種演進方案

7.2 代码示例

那如何用脚本进行删除呢?

我们先来看一下这段 Redis 专属脚本:

if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end
登入後複製

这段脚本和铂金方案的获取key,删除key的方式很像。先获取 KEYS[1] 的 value,判断 KEYS[1] 的 value 是否和 ARGV[1] 的值相等,如果相等,则删除 KEYS[1]。

那么这段脚本怎么在 Java 项目中执行呢?

分两步:先定义脚本;用 redisTemplate.execute 方法执行脚本。

// 脚本解锁
String script = "if redis.call(&#39;get&#39;,KEYS[1]) == ARGV[1] then return redis.call(&#39;del&#39;,KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
登入後複製

上面的代码中,KEYS[1] 对应“lock”,ARGV[1] 对应 “uuid”,含义就是如果 lock 的 value 等于 uuid 则删除 lock。

而这段 Redis 脚本是由 Redis 内嵌的 Lua 环境执行的,所以又称作 Lua 脚本。

那钻石方案是不是就完美了呢?有没有更好的方案呢?

下篇,我们再来介绍另外一种分布式锁的王者方案:Redisson。

八、總結

本篇透過本地鎖定的問題引申出分散式鎖定的問題。接著介紹了五種分散式鎖的方案,由淺入深講解了不同方案的改進之處。

從上面幾種方案的不斷演進的過程中,知道了系統中哪些地方可能存在異常情況,以及該如何更好地進行處理。

舉一反三,這種不斷演進的思維模式也可以運用到其他技術中。

以下總結下上面五種方案的缺陷和改進之處。

青銅方案

  • 缺陷:業務程式碼出現例外或伺服器當機,沒有執行主動刪除鎖的邏輯,就造成了死鎖。
  • 改進:設定鎖的自動過期時間,過一段時間後,自動刪除鎖,這樣其他執行緒就能取得到鎖了。

白銀方案

  • #缺陷:佔鎖和設定鎖定過期時間是逐步兩步驟執行的,不是原子操作。
  • 改進:佔鎖和設定鎖定過期時間保證原子操作。

黃金方案

  • #缺陷:主動刪除鎖定時,因鎖的值都是相同的,將其他客戶端佔用的鎖刪除了。
  • 改進:每次佔用的鎖,隨機設為較大的值,主動刪除鎖定時,比較鎖的值和自己設定的值是否相等。

鉑金方案

  • #缺陷:取得鎖定、比較鎖定的值、刪除鎖定,這三步驟是非原子性的。中途又可能鎖自動過期了,又被其他客戶端搶佔了鎖,導致刪鎖時把其他客戶端佔用的鎖刪了。
  • 改進:使用 Lua 腳本進行取得鎖定、比較鎖定、刪除鎖定的原子操作。

鑽石方案

  • 缺陷:非專業的分散式鎖定方案。
  • 改進:Redission 分散式鎖定。

王者方案,下篇見~

以上是Redis 分散式鎖|從青銅到鑽石的五種演進方案的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1677
14
CakePHP 教程
1431
52
Laravel 教程
1334
25
PHP教程
1280
29
C# 教程
1257
24
redis集群模式怎麼搭建 redis集群模式怎麼搭建 Apr 10, 2025 pm 10:15 PM

Redis集群模式通過分片將Redis實例部署到多個服務器,提高可擴展性和可用性。搭建步驟如下:創建奇數個Redis實例,端口不同;創建3個sentinel實例,監控Redis實例並進行故障轉移;配置sentinel配置文件,添加監控Redis實例信息和故障轉移設置;配置Redis實例配置文件,啟用集群模式並指定集群信息文件路徑;創建nodes.conf文件,包含各Redis實例的信息;啟動集群,執行create命令創建集群並指定副本數量;登錄集群執行CLUSTER INFO命令驗證集群狀態;使

redis數據怎麼清空 redis數據怎麼清空 Apr 10, 2025 pm 10:06 PM

如何清空 Redis 數據:使用 FLUSHALL 命令清除所有鍵值。使用 FLUSHDB 命令清除當前選定數據庫的鍵值。使用 SELECT 切換數據庫,再使用 FLUSHDB 清除多個數據庫。使用 DEL 命令刪除特定鍵。使用 redis-cli 工具清空數據。

redis怎麼讀取隊列 redis怎麼讀取隊列 Apr 10, 2025 pm 10:12 PM

要從 Redis 讀取隊列,需要獲取隊列名稱、使用 LPOP 命令讀取元素,並處理空隊列。具體步驟如下:獲取隊列名稱:以 "queue:" 前綴命名,如 "queue:my-queue"。使用 LPOP 命令:從隊列頭部彈出元素並返回其值,如 LPOP queue:my-queue。處理空隊列:如果隊列為空,LPOP 返回 nil,可先檢查隊列是否存在再讀取元素。

centos redis如何配置Lua腳本執行時間 centos redis如何配置Lua腳本執行時間 Apr 14, 2025 pm 02:12 PM

在CentOS系統上,您可以通過修改Redis配置文件或使用Redis命令來限制Lua腳本的執行時間,從而防止惡意腳本佔用過多資源。方法一:修改Redis配置文件定位Redis配置文件:Redis配置文件通常位於/etc/redis/redis.conf。編輯配置文件:使用文本編輯器(例如vi或nano)打開配置文件:sudovi/etc/redis/redis.conf設置Lua腳本執行時間限制:在配置文件中添加或修改以下行,設置Lua腳本的最大執行時間(單位:毫秒)

redis命令行怎麼用 redis命令行怎麼用 Apr 10, 2025 pm 10:18 PM

使用 Redis 命令行工具 (redis-cli) 可通過以下步驟管理和操作 Redis:連接到服務器,指定地址和端口。使用命令名稱和參數向服務器發送命令。使用 HELP 命令查看特定命令的幫助信息。使用 QUIT 命令退出命令行工具。

redis計數器怎麼實現 redis計數器怎麼實現 Apr 10, 2025 pm 10:21 PM

Redis計數器是一種使用Redis鍵值對存儲來實現計數操作的機制,包含以下步驟:創建計數器鍵、增加計數、減少計數、重置計數和獲取計數。 Redis計數器的優勢包括速度快、高並發、持久性和簡單易用。它可用於用戶訪問計數、實時指標跟踪、遊戲分數和排名以及訂單處理計數等場景。

redis過期策略怎麼設置 redis過期策略怎麼設置 Apr 10, 2025 pm 10:03 PM

Redis數據過期策略有兩種:定期刪除:定期掃描刪除過期鍵,可通過 expired-time-cap-remove-count、expired-time-cap-remove-delay 參數設置。惰性刪除:僅在讀取或寫入鍵時檢查刪除過期鍵,可通過 lazyfree-lazy-eviction、lazyfree-lazy-expire、lazyfree-lazy-user-del 參數設置。

如何優化debian readdir的性能 如何優化debian readdir的性能 Apr 13, 2025 am 08:48 AM

在Debian系統中,readdir系統調用用於讀取目錄內容。如果其性能表現不佳,可嘗試以下優化策略:精簡目錄文件數量:盡可能將大型目錄拆分成多個小型目錄,降低每次readdir調用處理的項目數量。啟用目錄內容緩存:構建緩存機制,定期或在目錄內容變更時更新緩存,減少對readdir的頻繁調用。內存緩存(如Memcached或Redis)或本地緩存(如文件或數據庫)均可考慮。採用高效數據結構:如果自行實現目錄遍歷,選擇更高效的數據結構(例如哈希表而非線性搜索)存儲和訪問目錄信

See all articles