Redis分布式鎖引發的工作事故

2022-06-15 12:12:13 字數 3612 閱讀 2965

有一次,運營做了乙個飛天茅台的搶購活動,庫存100瓶,但是卻超賣了!要知道,這個地球上飛天茅台的稀缺性啊!!!事故定為p0級重大事故...只能坦然接受。整個專案組被扣績效了~~

經過一番了解後,得知這個搶購活動介面以前從來沒有出現過這種情況,但是這次為什麼會超賣呢?

原因在於:之前的搶購商品都不是什麼稀缺性商品,而這次活動居然是飛天茅台,通過埋點資料分析,各項資料基本都是成倍增長,活動熱烈程度可想而知!話不多說,直接上核心**,機密部分做了偽**處理。。。

public seckillactivityrequestvo seckillhandle(seckillactivityrequestvo request)  else 

}} finally 

return response;

}

以上**,通過分布式鎖過期時間有效期10s來保障業務邏輯有足夠的執行時間;採用try-finally語句塊保證鎖一定會及時釋放。業務**內部也對庫存進行了校驗。看起來很安全啊~  別急,繼續分析。

但也正因如此,讓使用者服務一直處於較高的執行負載中。

搶購活動開始的一瞬間,大量的使用者校驗請求打到了使用者服務。導致使用者服務閘道器出現了短暫的響應延遲,有些請求的響應時長超過了10s,但由於http請求的響應超時我們設定的是30s,這就導致介面一直阻塞在使用者校驗那裡,10s後,分布式鎖已經失效了,此時有新的請求進來是可以拿到鎖的,也就是說鎖被覆蓋了。這些阻塞的介面執行完之後,又會執行釋放鎖的邏輯,這就把其他執行緒的鎖釋放了,導致新的請求也可以競爭到鎖~這真是乙個極其惡劣的迴圈。

這個時候只能依賴庫存校驗,但是偏偏庫存校驗不是非原子性的,採用的是get and compare 的方式,超賣的悲劇就這樣發生了~~~

沒有其他系統風險容錯處理

由於使用者服務吃緊,閘道器響應延遲,但沒有任何應對方式,這是超賣的導火索。

看似安全的分布式鎖其實一點都不安全

雖然採用了set key value [ex seconds] [px milliseconds] [nx|xx]的方式,但是如果執行緒a執行的時間較長沒有來得及釋放,鎖就過期了,此時執行緒b是可以獲取到鎖的。當執行緒a執行完成之後,釋放鎖,實際上就把執行緒b的鎖釋放掉了。

這個時候,執行緒c又是可以獲取到鎖的,而此時如果執行緒b執行完釋放鎖實際上就是釋放的執行緒c設定的鎖。這是超賣的直接原因。

非原子性的庫存校驗

非原子性的庫存校驗導致在併發場景下,庫存校驗的結果不準確。這是超賣的根本原因。

通過以上分析,問題的根本原因在於庫存校驗嚴重依賴了分布式鎖。因為在分布式鎖正常set、del的情況下,庫存校驗是沒有問題的。但是,當分布式鎖不安全可靠的時候,庫存校驗就沒有用了。

知道了原因之後,我們就可以對症下藥了。

相對安全的定義:set、del是一一對映的,不會出現把其他現成的鎖del的情況。從實際情況的角度來看,即使能做到set、del一一對映,也無法保障業務的絕對安全。

因為鎖的過期時間始終是有界的,除非不設定過期時間或者把過期時間設定的很長,但這樣做也會帶來其他問題。故沒有意義。

要想實現相對安全的分布式鎖,必須依賴key的value值。在釋放鎖的時候,通過value值的唯一性來保證不會勿刪。我們基於lua指令碼實現原子性的get and compare,如下:

public void safedunlock(string key, string val) 

我們通過lua指令碼來實現安全地解鎖。

// redis會返回操作之後的結果,這個過程是原子性的

long currstock = redistemplate.opsforhash().increment("key", "stock", -1);

發現沒有,**中的庫存校驗完全是「畫蛇添足」。

經過以上的分析之後,我們決定新建乙個distributedlocker類專門用於處理分布式鎖。

public seckillactivityrequestvo seckillhandle(seckillactivityrequestvo request) 

// 使用者活動校驗

// 庫存校驗,基於redis本身的原子性來保證

long currstock = stringredistemplate.opsforhash().increment(key + ":info", "stock", -1);

if (currstock < 0)  else 

} finally 

return response;

}

改進之後,其實可以發現,我們借助於redis本身的原子性扣減庫存,也是可以保證不會超賣的。對的。但是如果沒有這一層鎖的話,那麼所有請求進來都會走一遍業務邏輯,由於依賴了其他系統,此時就會造成對其他系統的壓力增大。這會增加的效能損耗和服務不穩定性,得不償失。基於分布式鎖可以在一定程度上攔截一些流量。

有人提出用redlock來實現分布式鎖。redlock的可靠性更高,但其代價是犧牲一定的效能。在本場景,這點可靠性的提公升遠不如效能的提公升帶來的價效比高。如果對於可靠性極高要求的場景,則可以採用redlock來實現。

由於bug需要緊急修復上線,因此我們將其優化並在測試環境進行了壓測之後,就立馬熱部署上線了。實際證明,這個優化是成功的,效能方面略微提公升了一些,並在分布式鎖失效的情況下,沒有出現超賣的情況。

然而,還有沒有優化空間呢?有的!

由於服務是集群部署,我們可以將庫存均攤到集群中的每個伺服器上,通過廣播通知到集群的各個伺服器。閘道器層基於使用者id做hash演算法來決定請求到哪一台伺服器。這樣就可以基於應用快取來實現庫存的扣減和判斷。效能又進一步提公升了!

// 通過訊息提前初始化好,借助concurrenthashmap實現高效執行緒安全

private static concurrenthashmapseckill_flag_map = new concurrenthashmap<>();

// 通過訊息提前設定好。由於atomicinteger本身具備原子性,因此這裡可以直接使用hashmap

private static mapseckill_stock_map = new hashmap<>();

...public seckillactivityrequestvo seckillhandle(seckillactivityrequestvo request) 

// 使用者活動校驗

// 庫存校驗

if(seckill_stock_map.get(seckillid).decrementandget() < 0) 

// 生成訂單

// 發布訂單建立成功事件

// 構建響應

return response;

}

通過以上的改造,我們就完全不需要依賴redis了。效能和安全性兩方面都能進一步得到提公升!

當然,此方案沒有考慮到機器的動態擴容、縮容等複雜場景,如果還要考慮這些話,則不如直接考慮分布式鎖的解決方案。

稀缺商品超賣絕對是重大事故。如果超賣數量多的話,甚至會給平台帶來非常嚴重的經營影響和社會影響。

對於乙個開發者而言,在設計開發方案時,一定要將方案考慮周全。怎樣才能將方案考慮周全?唯有持續不斷地學習!

redis分布式鎖

redis分布式鎖 直接上 我寫了四個redis分布式鎖的方法,大家可以提個意見 第一種方法 redis分布式鎖 param timeout public void lock long timeout thread.sleep 100 catch exception e override publi...

Redis分布式鎖

分布式鎖一般有三種實現方式 1.資料庫樂觀鎖 2.基於redis的分布式鎖 3.基於zookeeper的分布式鎖.首先,為了確保分布式鎖可用,我們至少要確保鎖的實現同時滿足以下四個條件 互斥性。在任意時刻,只有乙個客戶端能持有鎖。不會發生死鎖。即使有乙個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保...

redis分布式鎖

使用redis的setnx命令實現分布式鎖 redis為單程序單執行緒模式,採用佇列模式將併發訪問變成序列訪問,且多個客戶端對redis的連線並不存在競爭關係。redis的setnx命令可以方便的實現分布式鎖。setnx key value 將key的值設為value,當且僅當key不存在。如給定的...