Laravel 原子鎖概念講解

引言

什么是競爭條件 (Race Condition)?

在并發編程中,當多個進程或線程同時訪問和修改同一個共享資源時,最終結果會因其執行時序的微小差異而變得不可預測,甚至產生錯誤。這種情況被稱為“競爭條件”。

  • 例子1:定時執行某個耗時的任務,如果第一個任務執行時還沒有更新數據源,第二個任務就開始了,那么同一個數據源可能被更新或新增兩次數據,最終導致數據源錯誤。
  • 例子2:商品秒殺場景:若庫存僅剩 1 件,兩個請求可能在同一時刻都讀取到庫存為 1,并各自執行扣減操作,最終導致商品超賣。

Laravel 原子鎖如何解決此問題

Laravel 的原子鎖 (Atomic Lock) 機制提供了一種優雅的解決方案。它能確保在分布式環境中的任何時刻,只有一個進程能夠獲得對特定資源的“鎖”,從而獨占地執行關鍵代碼塊,有效防止競爭條件的發生。

一、配置與原理

1.1 支持的緩存驅動

Laravel 的原子鎖功能依賴于其緩存系統。要使用此功能,應用程序的默認緩存驅動必須配置為以下之一:

  • memcached
  • dynamodb
  • redis
  • database
  • array (此驅動僅在單次請求生命周期內有效,主要用于測試)

1.2 為什么不支持 file 驅動?

file 驅動的緩存數據存儲在服務器的本地文件系統上。在多服務器、負載均衡的分布式環境中,一臺服務器創建的鎖文件對另一臺服務器是不可見的。這將導致不同服務器上的進程可以同時獲取“同一個”鎖,使得鎖機制失效。因此,file 驅動因其固有的本地化局限性,不被原子鎖支持。

1.3 指定和配置驅動

.env 中使用 CACHE_DRIVER

指定默認緩存驅動最直接的方式是在項目根目錄的 .env 文件中設置 CACHE_DRIVER 變量。

CACHE_DRIVER=redis
config/cache.php 的作用

該文件是 Laravel 緩存系統的主要配置文件。

  • default: 定義了默認的緩存驅動。它會首先讀取 .env 文件中的 CACHE_DRIVER 變量,如果不存在,則使用此文件中設定的備用值。

    'default' => env('CACHE_DRIVER', 'file'),
    
  • stores 數組: 定義了每一種緩存驅動的詳細連接參數。可以在此配置 Redis 的連接信息、數據庫緩存的表名等。

    'stores' => ['redis' => ['driver' => 'redis','connection' => 'cache','lock_connection' => 'default', // 可為鎖指定獨立的 Redis 連接],// ...
    ],
    

1.4 database 驅動的表結構要求

當使用 database 驅動時,需要手動創建一個用于存儲鎖信息的表。可通過 Artisan 命令 php artisan make:migration create_cache_locks_table 創建遷移文件,并定義表結構如下:

Schema::create('cache_locks', function ($table) {// 鎖的唯一標識符,主鍵$table->string('key')->primary();// 鎖持有者的唯一令牌$table->string('owner');// 鎖的過期時間(Unix 時間戳)$table->integer('expiration');
});

最后運行 php artisan migrate 以創建該表。

二、核心 API:獲取與釋放

2.1 Cache::lock(): 創建鎖實例

所有鎖操作都始于 Cache::lock() 方法。它返回一個鎖實例,代表獲取鎖的“意圖”,但此時并未真正鎖定資源。

// 創建一個名為 'foo',最長持有 10 秒的鎖實例
$lock = Cache::lock('foo', 10);

關于超時時間參數:

Cache::lock('foo', $seconds) 中的第二個參數 $seconds 代表鎖的“生存時間”(Time To Live, TTL),即鎖的自動過期時間。此參數并非必需,但強烈建議設置,它是一個防止“死鎖”的關鍵安全機制。

  • 作用:設想一個進程獲取鎖后意外崩潰,無法執行到釋放鎖的步驟。如果設置了 TTL(如 10 秒),該鎖會在 10 秒后被緩存系統自動清除,使系統能夠自我恢復。

  • 風險:若省略該參數(即 Cache::lock('foo')),鎖將永不過期。一旦持有該鎖的進程崩潰,會造成永久性死鎖,其他進程將永遠無法獲取該鎖,除非手動清理緩存。

  • 例外情況(閉包模式):只有在使用閉包時,才可以安全地省略超時時間。因為 Laravel 保證無論閉包是否成功執行,鎖最終都會被自動釋放。鎖的生命周期與閉包的執行周期綁定。

    // 在此模式下,可以安全地省略超時時間
    Cache::lock('foo')->get(function () {// ...
    });
    

???關于閉包模式的補充:???

上面說,在使用閉包時可以安全地省略超時時間,因為Laravel保證會自動釋放鎖。

// Laravel 會在閉包執行后自動釋放鎖
Cache::lock('foo')->get(function () {// ...
});

這種自動釋放的原理是Laravel在內部使用了 try...finally 結構來執行閉包,確保了無論閉包是成功完成還是拋出程序內異常finally塊中的 release() 方法都會被調用。

然而,這種自動釋放機制有一個重要的前提:執行鎖操作的PHP進程本身必須正常運行至結束。

如果進程被外部信號(如 kill -9)強制終止,或者服務器因斷電等原因宕機,finally 代碼塊將沒有機會執行。在這種極限情況下,如果鎖沒有設置TTL,它同樣會變成一個永久性死鎖

因此,最嚴謹、最安全的實踐是:即使在使用方便的閉包模式時,也始終為其設置一個合理的TTL。 將閉包的自動釋放視為第一層保障,而將TTL視為應對進程級別災難的最終保險。

2.2 獲取鎖的策略:get() vs block()

  • get(): 立即嘗試獲取鎖,不等待。
    • 成功獲取,返回 true
    • 若鎖已被占用,立即返回 false
  • block($seconds): 阻塞式等待獲取。
    • 嘗試獲取鎖,若被占用,會阻塞并等待最多 $seconds 秒。
    • 在等待時間內成功獲取,返回 true
    • 等待超時后仍未獲取,拋出 Illuminate\Contracts\Cache\LockTimeoutException 異常。

2.3 鎖的原子性原理 (A/B 進程競爭)

讓我們來澄清一個關鍵概念:

  • $lock = Cache::lock('foo', 10);這一行并沒有真正去鎖定任何東西。它只是在內存中創建了一個“鎖的意圖”對象。你可以把它想象成“準備好了一張要去搶占資源的申請表”。此時,共享的緩存服務器里還沒有任何關于 foo 鎖的記錄。A 和 B 兩個進程都可以成功執行這一行,各自拿著一張申請表。
  • $lock->get() 這一行才是真正的行動。當代碼執行到這里時,Laravel
    會拿著這張“申請表”去訪問中央緩存服務器,并嘗試執行一個原子操作。

以 Redis 為例,當調用 $lock->get()$lock->block() 時,Laravel 會在底層執行一個類似 SET my_lock_key "random_owner_string" NX PX 10000 的原子命令。

  • NX 選項意為 “if Not eXists” (如果不存在)。
  • 整個過程如下
    1. 進程 A 和 B 幾乎同時嘗試獲取鎖。
    2. 假設進程 A 的 SET...NX 命令先到達 Redis 服務器。由于 my_lock_key 不存在,命令執行成功,鎖被 A 持有。
    3. 緊接著,進程 B 的 SET...NX 命令到達。此時 my_lock_key 已存在,NX 條件不滿足,命令執行失敗。進程 B 獲取鎖失敗。

整個“檢查并設置”的過程由 Redis 在一個不可分割的原子操作中完成,從而杜絕了競爭條件。

2.4 鎖的釋放與異常處理

自動釋放:使用閉包 (推薦)

將業務邏輯包裹在閉包中傳遞給 get()block() 方法,是管理鎖生命周期的最佳實踐。Laravel 會確保在閉包執行完畢后(無論正常結束還是拋出異常)自動釋放鎖。

// 立即獲取,成功則執行閉包
Cache::lock('foo', 10)->get(function () {// 執行關鍵任務...
});// 最多等待 5 秒,成功則執行閉包
Cache::lock('foo', 10)->block(5, function () {// 執行關鍵任務...
});
手動釋放:release()try...finally

若不使用閉包,則必須手動調用 release() 方法釋放鎖。為確保在任何情況下鎖都能被釋放(即使發生異常),必須將 release() 調用放在 try...finally 代碼塊中。

$lock = Cache::lock('foo', 10);if ($lock->get()) {try {// 執行關鍵任務...} finally {$lock->release();}
}
超時處理:捕獲 LockTimeoutException

使用 block() 方法時,必須準備捕獲 LockTimeoutException 異常,以處理等待超時的情況。

use Illuminate\Contracts\Cache\LockTimeoutException;$lock = Cache::lock('foo', 10);try {$lock->block(5);// 成功獲取鎖...
} catch (LockTimeoutException $e) {// 獲取鎖超時,執行備用邏輯...
} finally {optional($lock)->release();
}

三、進階用法

3.1 跨進程鎖管理 (owner() & restoreLock())

在某些場景下(如 Web 請求分發任務到隊列),需要在 A 進程中獲取鎖,在 B 進程中釋放鎖。

  • owner(): 在成功獲取鎖后,調用此方法可獲得一個唯一的“所有者令牌”。
  • restoreLock($key, $owner): 在另一個進程中,使用鎖的 key 和傳遞過來的 owner 令牌,可以恢復對該鎖的控制權并進行釋放。

示例:

// 在控制器中
$lock = Cache::lock('process-podcast-123', 120);
if ($result = $lock->get()) {// 將 owner 令牌傳遞給 JobProcessPodcast::dispatch($podcast, $lock->owner());
}// 在 ProcessPodcast Job 的 handle 方法中
$owner = $this->owner; // 從構造函數中獲取的令牌
Cache::restoreLock('process-podcast-123', $owner)->release();

3.2 強制釋放鎖 (forceRelease())

此方法可以無視鎖的所有者,強行刪除一個鎖。它主要用于管理和修復場景,如處理卡死的任務。

Cache::lock('stuck-task')->forceRelease();

四、實戰演練:Artisan 命令

4.1 實驗目標與環境準備

通過 Artisan 命令模擬并發進程,直觀體驗 block() 的等待超時機制與 get() 的立即失敗機制。
確保 .env 文件中的 CACHE_DRIVER 已正確配置為 redisdatabase

4.2 完整代碼

將提供兩個版本的 Artisan 命令,以便進行對比實驗。

4.2.1 阻塞式等待 (block) 版本

此版本在獲取鎖失敗時會等待一段時間。

執行 php artisan make:command DemoLockTestBlock 創建命令,并使用以下代碼:

<?phpnamespace App\Console\Commands;use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Contracts\Cache\LockTimeoutException;class DemoLockTestBlock extends Command
{// 將命令簽名更改為 demo:lock-test-blockprotected $signature = 'demo:lock-test-block';protected $description = 'A demo for "block()" method to showcase atomic locks.';const LOCK_KEY = 'my-long-running-task';const TASK_DURATION = 10;const LOCK_TTL = 30;const WAIT_TIMEOUT = 5;public function handle(){$this->info('進程啟動,準備嘗試獲取鎖 ['.self::LOCK_KEY.']...');$this->comment('將使用 block() 方法,最多等待 '.self::WAIT_TIMEOUT.' 秒。');try {Cache::lock(self::LOCK_KEY, self::LOCK_TTL)->block(self::WAIT_TIMEOUT, function () {$this->info('? 鎖獲取成功!');$this->comment('現在開始執行一項耗時任務,將持續 '.self::TASK_DURATION.' 秒...');$progressBar = $this->output->createProgressBar(self::TASK_DURATION);$progressBar->start();for ($i = 0; $i < self::TASK_DURATION; $i++) {sleep(1);$progressBar->advance();}$progressBar->finish();$this->info("\n? 任務執行完畢!鎖已被自動釋放。");});} catch (LockTimeoutException $e) {$this->error('? 獲取鎖失敗!等待了 '.self::WAIT_TIMEOUT.' 秒后超時。');$this->error('這說明有另一個進程正在持有該鎖。');}$this->info('進程執行結束。');return 0;}
}
4.2.2 立即失敗 (get) 版本

此版本在獲取鎖失敗時會立即放棄。

執行 php artisan make:command DemoLockTestGet 創建命令,并使用以下代碼:

<?phpnamespace App\Console\Commands;use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;class DemoLockTestGet extends Command
{// 將命令簽名更改為 demo:lock-test-getprotected $signature = 'demo:lock-test-get';protected $description = 'A demo for "get()" method to showcase atomic locks.';const LOCK_KEY = 'my-long-running-task';const TASK_DURATION = 10;const LOCK_TTL = 30;public function handle(){$this->info('進程啟動,準備嘗試獲取鎖 ['.self::LOCK_KEY.']...');$this->comment('將使用 get() 方法,立即嘗試,不等待。');// 使用 get() 的返回值來判斷是否成功$lockAcquired = Cache::lock(self::LOCK_KEY, self::LOCK_TTL)->get(function () {$this->info('? 鎖獲取成功!');$this->comment('現在開始執行一項耗時任務,將持續 '.self::TASK_DURATION.' 秒...');$progressBar = $this->output->createProgressBar(self::TASK_DURATION);$progressBar->start();for ($i = 0; $i < self::TASK_DURATION; $i++) {sleep(1);$progressBar->advance();}$progressBar->finish();$this->info("\n? 任務執行完畢!鎖已被自動釋放。");return true;});// 如果 get() 方法因鎖被占用而失敗,其返回值為 falseif (!$lockAcquired) {$this->error('? 獲取鎖失敗!鎖已被其他進程占用。');}$this->info('進程執行結束。');return 0;}
}

4.3 動手操作步驟 (以get版本為例)

  1. 打開終端 1,運行 get 版本的命令:

    php artisan demo:lock-test-get
    

    觀察到任務開始執行,進度條前進。

  2. 在終端 1 的任務結束前,打開終端 2,運行 get 版本的命令:

    php artisan demo:lock-test-get
    
  3. 再次打開終端 3 (或等待終端 2 執行完畢后),在終端 1 任務仍在進行時,運行 get 版本的命令:

    php artisan demo:lock-test-get
    

五、總結

Laravel 原子鎖是構建健壯、高并發應用的有力工具。掌握其配置方法、getblock 兩種核心策略、以及閉包自動管理的模式,可以有效避免數據競爭問題。對于復雜的跨進程通信,ownerrestoreLock 提供了解決方案。在實際項目中,應積極應用原子鎖來保護關鍵業務邏輯,確保數據的一致性和準確性。

參考資料 (References)

  • Laravel 8 中文文檔(8.x) - 緩存 #原子鎖 - 本文部分概念和示例最初來源于 LearnKu 社區翻譯的 Laravel 官方文檔。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/89972.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/89972.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/89972.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

83、形式化方法

形式化方法&#xff08;Formal Methods&#xff09; 是基于嚴格數學基礎&#xff0c;通過數學邏輯證明對計算機軟硬件系統進行建模、規約、分析、推理和驗證的技術&#xff0c;旨在保證系統的正確性、安全性和可靠性。以下從核心思想、關鍵技術、應用場景、優勢與挑戰四個維度展…

解決 Ant Design v5.26.5 與 React 19.0.0 的兼容性問題

#目前 Ant Design v5.x 官方尚未正式支持 React 19&#xff08;截至我的知識截止日期2023年10月&#xff09;&#xff0c;但你仍可以通過以下方法解決兼容性問題&#xff1a; 1. 臨時解決方案&#xff08;推薦&#xff09; 方法1&#xff1a;使用 --legacy-peer-deps 安裝 n…

算法與數據結構(課堂2)

排序與選擇 算法排序分類 基于比較的排序算法&#xff1a; 交換排序 冒泡排序快速排序 插入排序 直接插入排序二分插入排序Shell排序 選擇排序 簡單選擇排序堆排序 合并排序 基于數字和地址計算的排序方法 計數排序桶排序基數排序 簡單排序算法 冒泡排序 void sort(Item a[],i…

跨端分欄布局:從手機到Pad的優雅切換

在 UniApp X 的世界里&#xff0c;我們常常需要解決一個現實問題&#xff1a; “手機上是全屏列表頁&#xff0c;Pad上卻要左右分欄”。這時候&#xff0c;很多人會想到 leftWindow 或 rightWindow。但別急——這些方案 僅限 Web 端&#xff0c;如果你的應用需要跨平臺&#xf…

華為服務器管理工具(Intelligent Platform Management Interface)

一、核心功能與技術架構 硬件級監控與控制 全維度傳感器管理:實時監測 CPU、內存、硬盤、風扇、電源等硬件組件的溫度、電壓、轉速等參數,支持超過 200 種傳感器類型。例如,通過 IPMI 命令ipmitool sdr elist可快速獲取服務器傳感器狀態,并通過正則表達式提取關鍵指標。 遠…

Node.js Express keep-alive 超時時間設置

背景介紹隨著 Web 應用并發量不斷攀升&#xff0c;長連接&#xff08;keep-alive&#xff09;策略已經成為提升性能和資源復用的重要手段。本文將從原理、默認值、優化實踐以及潛在風險等方面&#xff0c;全面剖析如何在 Node.js&#xff08;Express&#xff09;中正確設置和應…

學習C++、QT---30(QT庫中如何自定義控件(自定義按鈕)講解)

每日一言你比想象中更有韌性&#xff0c;那些看似艱難的日子&#xff0c;終將成為勛章。自定義按鈕我們要知道自定義控件就需要我們創建一個新的類加上繼承父類&#xff0c;但是我們還要注意一個點&#xff0c;就是如果我們是自己重頭開始造控件的話&#xff0c;那么我們就直接…

【補充】Linux內核鏈表機制

專題文章&#xff1a;Linux內核鏈表與Pinctrl數據結構解析 目標&#xff1a; 深入解析Pinctrl子系統中&#xff0c;struct pinctrl如何通過內核鏈表&#xff0c;來組織和管理其多個struct pinctrl_state。 1. 問題背景&#xff1a;一個設備&#xff0c;多種引腳狀態 一個復雜的…

本地部署Dify、Docker重裝

需要先安裝一個Docker&#xff0c;Docker就像是一個容器&#xff0c;將部署Dify的空間與本地環境隔離&#xff0c;避免因為本地環境的一些問題導致BUG。也確保了環境的統一&#xff0c;不會出現在自己的電腦上能跑但是移植到別人電腦上就跑不通的情況。那么現在就開始先安裝Doc…

【每天一個知識點】非參聚類(Nonparametric Clustering)

ChatGPT 說&#xff1a;“非參聚類”&#xff08;Nonparametric Clustering&#xff09;是一類不預先設定聚類數目或數據分布形式的聚類方法。與傳統“參數聚類”&#xff08;如高斯混合模型&#xff09;不同&#xff0c;非參聚類在建模過程中不假設數據來自于已知分布數量的某…

人形機器人CMU-ASAP算法理解

一原文在第一階段&#xff0c;用重定位的人體運動數據在模擬中預訓練運動跟蹤策略。在第二階段&#xff0c;在現實世界中部署策略并收集現實世界數據來訓練一個增量&#xff08;殘差&#xff09;動作模型來補償動態不匹配。&#xff0c;ASAP 使用集成到模擬器中的增量動作模型對…

next.js刷新頁面時二級菜單展開狀態判斷

在 Next.js 中保持二級菜單刷新后展開狀態的解決方案 在 Next.js 應用中&#xff0c;當頁面刷新時保持二級菜單的展開狀態&#xff0c;可以通過以下幾種方法實現&#xff1a; 方法1&#xff1a;使用 URL 參數保存狀態&#xff08;推薦&#xff09; import { useRouter } from n…

網絡基礎DAY13-NAT技術

NAT技術internet接入方式&#xff1a;ADLS技術&#xff1a;能夠將不同設備的不同信號通過分離器進行打包之后再internet中傳輸&#xff0c;到另一端的分離器之后再進行分離。傳輸到不同的設備中去。常見光纖接入方式internet接入認證方式&#xff1a;PPPoE&#xff1a;先認證再…

HBuilderX中設置 DevEco Studio路徑,但是一直提示未安裝

前言&#xff1a; HBuilderX中設置 DevEco Studio路徑&#xff0c;但是一直提示未安裝。 報錯信息&#xff1a; 檢測到鴻蒙工具鏈&#xff0c;請在菜單“工具->設置->運行配置”中設置鴻蒙開發者工具路徑為 DevEco Studio 的安裝路徑&#xff0c;請參考 報錯原因…

什么是GNN?——聚合、更新與循環

在傳統的深度學習中&#xff0c;卷積神經網絡&#xff08;CNN&#xff09;擅長處理網格結構數據&#xff08;如圖像&#xff09;&#xff0c;循環神經網絡&#xff08;RNN&#xff09;擅長處理序列數據&#xff08;如文本&#xff09;。但當數據以圖的形式存在時&#xff08;如…

深入解析 Django REST Framework 的 APIView 核心方法

在 Python 3 中&#xff0c;Django 的 APIView 類是 Django REST Framework&#xff08;DRF&#xff09;中用于構建 API 視圖的核心基類。它提供了一個靈活的框架來處理 HTTP 請求&#xff0c;并通過一系列方法支持認證、權限檢查和請求限制等功能。self.perform_authenticatio…

神經網絡——卷積層

目錄 卷積層介紹 Conv2d 卷積動畫演示 卷積代碼演示 綜合代碼案例 卷積層介紹 卷積層是卷積神經網絡&#xff08;CNN&#xff09;的核心組件&#xff0c;它通過卷積運算提取輸入數據的特征。 基本原理 卷積層通過卷積核&#xff08;過濾器&#xff09;在輸入數據&…

神經網絡——線性層

在機器學習中&#xff0c;線性層&#xff08;Linear Layer&#xff09; 是一種基礎的神經網絡組件&#xff0c;也稱為全連接層&#xff08;Fully Connected Layer&#xff09; 或密集層&#xff08;Dense Layer&#xff09;。 其嚴格的數學定義為&#xff1a;對輸入數據執行線…

大模型高效適配:軟提示調優 Prompt Tuning

The Power of Scale for Parameter-Efficient Prompt Tuning ruatishi 軟提示向量 具體是什么 《The Power of Scale for Parameter-Efficient Prompt Tuning》中增加的部分是“軟提示(soft prompts)”,這是一種針對特定下游任務,添加到輸入文本中的可調參數序列。它與傳統…

https正向代理 GoProxy

背景&#xff1a; 在安全隔離的內網環境中&#xff0c;部署于內網的應用如需調用公網第三方接口&#xff08;如支付、短信&#xff09;&#xff0c;可通過正向代理服務實現訪問。 GoProxy 下載&#xff1a; https://github.com/snail007/goproxy/releases 使用文檔&#xff…