一、分布式鎖
1.1、分布式鎖是什么?
是一種在分布式系統中協調多個進程/服務對共享資源進行互斥訪問的機制;確保在任意時刻,只有一個客戶端可以訪問資源。
1.2、為什么需要分布式鎖?
- 解決多個服務/進程對同共享資源競爭,比如雙11,618購物節,多名用戶對同一個商品下單,導致庫存超賣問題。
- 防止重復操作,比如用戶連續點擊導致重復下單
1.3、分布式鎖需要具備的條件和剛需有哪些?
- 獨占性:任何時刻只能有且僅有一個線程持有
- 高可用:
- 若redis集群環境下,不能因為某一個節點掛了而出現獲取鎖和釋放鎖失敗的情況
- 高并發請求下,依舊性能好使
- 防死鎖:杜絕死鎖,必須有超時控制機制或撤銷操作,有個兜底終止跳出方案
- 不亂搶:不能unlock別人的鎖,只能自己加鎖自己釋放,自己的鎖自己解
- 重入性:同一個節點的同一個線程如果獲取鎖之后,它也可以再次獲取這個鎖
1.4、建立分布式鎖
# 第一種
setnx key value
EXPIRE KEY 60# 第二種
set key value [EX seconds][PX milliseconds][NX|XX]
使用setnx確保只有一個客戶端能成功設置鍵,通過EXPIRE設置過期時間,防止死鎖,但setnx + expire不安全,兩條命令非原子性。可以通過 Lua腳本 來實現分布式鎖。
-- 加鎖
if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 thenredis.call('PEXPIRE', KEYS[1], ARGV[2])return 1
end-- 釋放鎖
if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])
elsereturn 0
end
二、RedLock
官網地址:https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/
2.1、RedLock是什么?
ReadLock是一種算法,是一個更為規范的算法來使用Redis實現分布式鎖,實現了比普通單實例方法更安全的DLM(分布式鎖管理器)。
2.2、為什么使用RedLock?
導致問題:
-
- 主機宕機了,從機上位,變成新的master,用戶依舊可以建鎖成功,出現 一鎖被多建多用 ,違法安全規定
-
- 多個線程獲取到同一把鎖,可能會導致各種預期之外的情況發生,比如臟數據
2.3、RedLock算法設計理念
大致方案如下:
假設有5個Redis主節點,不使用復制或任何其他隱式協調系統,為了獲得鎖客戶端執行以下操作:
步驟 | 說明 |
---|---|
1 | 獲取當前時間,以毫秒為單位 |
2 | 依次嘗試從5個實例,使用相同的key和隨機值(如UUID)獲取值,當向Redis請求獲取值時,客戶端應該設置一個超時時間,這個超時時間應該小于鎖的失效時間。這樣可以防止客戶端在試圖與一個宕機的Redis節點對話時,長時間處于阻塞狀態。如果一個實例不可用,客戶端應該盡快嘗試去另外一個Redis實例請求數據 |
3 | 客戶端通過當前時間減去步驟1記錄的時間 = 獲取鎖使用的時間。當且僅當從大多數(N/2+1)的Redis節點都渠道鎖,并且獲取鎖使用的時間 < 鎖失效時間時,鎖才算獲取成功 |
4 | 如果取到鎖,其真正有效時間 = 初始有效時間 - 獲取鎖使用時間 |
5 | 如果由于某些原因未能獲得鎖(無法在至少N/2 + 1個Redis實例獲取鎖、或獲取鎖的時間超過了有效時間),客戶端應該在所有的Redis實例上進行解鎖 |
注意:
客戶端只有在滿足以下兩個條件時,才認為加鎖成功
-
- 客戶端從超過半數(>= N/2 + 1)的Redis實例上成功獲取了鎖
-
- 客戶端獲取鎖的總耗時沒有超過鎖的有效時間
2.4、容錯公式
N:最終部署機器數,X:容錯機器數
N = 2X + 1
為什么是奇數?
從成本上來考慮,用最少的機器,達到最多的產出效果
三、實際操作
3.1、手寫分布式鎖
- 注意點:
- lock關鍵:
- 加鎖:在redis中,設置鍵,并設置過期時間
- 自旋
- 續期
- unlock關鍵:不能unlock別人的鎖,只能自己加鎖自己釋放,自己的鎖自己解
- lock關鍵:
3.2、利用redlock算法實現分布式鎖
使用開源庫redis-plus-plus: https://github.com/sewenew/redis-plus-plus
redlock代碼路徑:redis-plus-plus/src/sw/redis++/patterns/
類名 | 作用 |
---|---|
RedLockUtils | 工具類,提供ttl(計算時間差)和鎖ID |
RedMutexTx | 基于Redis事務的分布式鎖實現 |
RedMutex | 作者推薦用戶直接操作的分布式鎖的類,根據配置選擇使用腳本(RedLockMutex )或事務(RedMutexTx ) |
RedLockMutex | 針對單個Redis實例的分布式鎖,使用腳本實現 |
RedLockMutexVessel | 管理多個Redis實例的分布式鎖,使用腳本實現 |
RedMutexOptions | RedLock的配置選項,包括鎖的TTL、重試延遲、是否使用腳本 |
RedMutexImpl | 抽象基類 |
RedMutexImplTpl | 模板類,繼承自RedMutexImpl ,根據模板參數(RedLockMutex 或RedMutexTx )實現具體操作 |
RedLock | 是一個模板類,用于封裝分布式鎖的核心操作,根據模板參數(RedLockMutex 或RedMutexTx )實現具體操作 |
LockWatcher | 用于監控鎖的生命周期 |
/** RedLock類 **/
/* 作者不是很推薦使用,更為推薦使用RedMutex,RedLock只是RedMutex的簡單封裝.
*/
template <typename RedisInstance>
class RedLock {
public:RedLock(RedisInstance &mut, std::defer_lock_t) : _mut(mut), _lock_val(RedLockUtils::lock_id()) {}~RedLock() {if (owns_lock()) {unlock();}}// Try to acquire the lock for *ttl* milliseconds.// Returns how much time still left for the lock, i.e. lock validity time.bool try_lock(const std::chrono::milliseconds &ttl) {auto time_left = _mut.try_lock(_lock_val, ttl);if (time_left <= std::chrono::milliseconds(0)) {return false;}_release_tp = std::chrono::steady_clock::now() + time_left;return true;}......void unlock() {try {_mut.unlock(_lock_val);_release_tp = std::chrono::time_point<std::chrono::steady_clock>{};} catch (const Error &) {_release_tp = std::chrono::time_point<std::chrono::steady_clock>{};throw;}}bool owns_lock() const {if (ttl() <= std::chrono::milliseconds(0)) {return false;}return true;}std::chrono::milliseconds ttl() const {auto t = std::chrono::steady_clock::now();return std::chrono::duration_cast<std::chrono::milliseconds>(_release_tp - t);}private:RedisInstance &_mut;std::string _lock_val;// The time point that we must release the lock.std::chrono::time_point<std::chrono::steady_clock> _release_tp{};
};/** 使用方法 **/
// 使用Lua腳本版本
{RedLockMutex mtx({redis1, redis2, redis3}, "resource");RedLock<RedLockMutex> lock(mtx, std::defer_lock);auto validity_time = lock.try_lock(std::chrono::seconds(30));validity_time = lock.extend_lock(std::chrono::seconds(10));lock.unlock();
} // Redis事務版本
{RedMutex mtx({redis1, redis2, redis3}, "resource");RedLock<RedMutex> lock(mtx, std::defer_lock);auto validity_time = lock.try_lock(std::chrono::seconds(30));validity_time = lock.extend_lock(std::chrono::seconds(30));lock.unlock();
}
/** RedMutex類 **/
class RedMutex {
public:RedMutex(std::shared_ptr<Redis> master,const std::string &resource,std::function<void (std::exception_ptr)> auto_extend_err_callback = nullptr,const RedMutexOptions &opts = {},const std::shared_ptr<LockWatcher> &watcher = nullptr) :RedMutex(std::initializer_list<std::shared_ptr<Redis>>{master},resource, std::move(auto_extend_err_callback), opts, watcher) {}RedMutex(std::initializer_list<std::shared_ptr<Redis>> masters,const std::string &resource,std::function<void (std::exception_ptr)> auto_extend_err_callback = nullptr,const RedMutexOptions &opts = {},const std::shared_ptr<LockWatcher> &watcher = nullptr) :RedMutex(masters.begin(), masters.end(),resource, std::move(auto_extend_err_callback), opts, watcher) {}template <typename Input>RedMutex(Input first, Input last,const std::string &resource,std::function<void (std::exception_ptr)> auto_extend_err_callback = nullptr,const RedMutexOptions &opts = {},const std::shared_ptr<LockWatcher> &watcher = nullptr) {if (opts.scripting) { //根據配置選項,選擇腳本還是事務實現_mtx = std::make_shared<RedMutexImplTpl<RedLockMutex>>(first, last, resource,std::move(auto_extend_err_callback), opts, watcher);} else {_mtx = std::make_shared<RedMutexImplTpl<RedMutexTx>>(first, last, resource,std::move(auto_extend_err_callback), opts, watcher);}}...private:std::shared_ptr<RedMutexImpl> _mtx;
};/** 使用方法 **/
auto redis = std::make_shared<Redis>("tcp://127.0.0.1");auto redis1 = std::make_shared<Redis>("tcp://127.0.0.1:7000");
auto redis2 = std::make_shared<Redis>("tcp://127.0.0.1:7001");
auto redis3 = std::make_shared<Redis>("tcp://127.0.0.1:7002");try {{// 單個實例RedMutex mtx(redis, "resource");std::lock_guard<RedMutex> lock(mtx);}{// 多個實例RedMutex mtx({redis1, redis2, redis3}, "resource");std::lock_guard<RedMutex> lock(mtx);}{RedMutexOptions opts;opts.ttl = std::chrono::seconds(5);auto watcher = std::make_shared<LockWatcher>();RedMutex mtx({redis1, redis2, redis3}, "resource",[](std::exception_ptr err) {try {std::rethrow_exception(err);} catch (const Error &e) {}},opts, watcher);std::unique_lock<RedMutex> lock(mtx, std::defer_lock);lock.lock();lock.unlock();lock.try_lock();}
} catch (const Error &err) {
}
先創建多個Redis獨立的實例
創建多線程模擬多個客戶端并發訪問,模擬多個客戶端并發操作。
/** 創建多線程模擬多個客戶端并發訪問 **/
std::unique_lock<RedMutex> lock(mtx, std::defer_lock);
auto client_thread = [&](string client_id, int task_count){try{// 嘗試獲取鎖(非阻塞版本) // if (!lock.try_lock()) {// cerr << "[" << client_id << "] Failed to acquire lock - operation skipped" << endl;// return;// }lock.lock();cout << "[" << client_id << "] acquired lock\n";// 模擬臨界區操作for (int i = 1; i <= task_count; ++i) {process_order(client_id, i);}// 手動釋放鎖(析構時也會自動釋放)lock.unlock();cout << "[" << client_id << "] released lock\n";// 隨機延遲后執行下一個任務dis.param(uniform_int_distribution<>::param_type(50, 300));this_thread::sleep_for(chrono::milliseconds(dis(gen)));}catch(const exception& e) {cerr << "[" << client_id << "] Error: " << e.what() << endl;}catch(const Error& e) {cerr << "Redis error: " << e.what() << endl;}};// 4. 創建多線程模擬多個客戶端并發訪問vector<thread> clients;vector<string> client_ids = {"ClientA", "ClientB", "ClientC"};for (const auto& id : client_ids) {clients.emplace_back(client_thread, id, 5);}
注意: try_lock() 非阻塞版本,lock() 阻塞版本, 盡量別混合使用,否則會死鎖。
正常運行結果:
四、總結
4.1、什么是分布式鎖?
是一種在分布式系統中協調多個進程/服務對共享資源進行互斥訪問的機制;確保在任意時刻,只有一個客戶端可以訪問資源。
4.2、分布式鎖和常見的鎖有什么區別?
鎖類型 | 作用范圍 | 實現方式 | 性能特點 | 典型應用場景 |
---|---|---|---|---|
分布式鎖 | 跨進程,跨機器 | 外部存儲系統(如Redis、Zookeeper等) | 網絡開銷大 | 多服務共享資源訪問 |
互斥鎖 | 單進程內-單機鎖 | 原子操作 | 低延遲,高效率 | 多線程共享內存訪問 |
讀寫鎖 | 單進程內-單機鎖 | 計數器+條件變量 | 讀操作并發性好 | 讀多寫少的共享數據 |
自旋鎖 | 單進程內-單機鎖 | CPU忙等待循環 | 低延遲,但浪費CPU | 短時間占用資源場景 |
4.3、RedLock分布式鎖的數據存儲與高可用性分析
-
- RedLock算法,將每個節點看作是獨立的,即沒有主從關系,每個節點都可以獨立地獲取和釋放鎖,并且它們之間沒有任何數據同步。
-
- RedLock算法本身不存儲業務數據,它只負責管理分布式鎖的狀態。
-
- 部分節點宕機,只要滿足 N/2+1 節點可用,鎖服務仍然正常;宕機節點上的鎖會在TTL過期后自動清理。
4.4、RedLock適用場景
個人認為,它不適用于需要強一致性的場景,只是在某些時間段,并發量突發時,避免超賣這類問題,比如雙11,618等活動,常規時間段還是還原成集群部署模式,使用單機鎖,保證數據的一致性。
Code
0vice·GitHub