一個bug,差點損失幾萬
你好,我是猿java
最近遇到一個線上事故,差點損失好幾萬,故事是這樣的...
背景
在之前的文章里我們分析了 Redis中運行 Lua腳本是如何保證原子性的。實際上,在我們的電商業務中也是使用 Redis + Lua來保證庫存的原子性操作,Redis是 Cluster集群部署,Lua腳本大致如下(本文的數據都經過脫敏處理):
-- type都是java代碼中傳入的String值,sku為Long型
local function availableRealSaleCal(type,sku)
local key = formatKey(type, sku)
-- 銷售庫存 =(if 可售賣量 then 銷售庫存 = min(可售庫存,可售賣量)
-- else 銷售庫存 = 可售庫存 end)
local availableRealSale = 0;
local availableSale = redis.call('INCRBY', key..":AVAILABLE_SALE", 0);
local saleLimit = redis.call('HGET', key, 'sale_limit');
redis.call('SET', stocksKey .. ":AVAILABLE_REAL_SALE", availableRealSale);
return availableRealSale
end
-- 拼接庫存 key,比如:stock:sale:{13523551512}, 注意這里有一個 {sku}
local function formatKey(type, sku)
return "stock:"..type..":"..":{"..sku.."}"
end;
在上面的 Lua腳本中,有 {sku}語法的使用,{}是在 Redis cluster 模式下特有的 Hash Tag,Redis 的哈希標簽是一種特殊的語法,用于在執行命令時將多個 key 分組在一起。Hash Tag 由一對大括號 {} 包圍,可以將其中的內容視為一個整體來處理。
{}的主要用途包括:
- 強制將多個 key 分組:在執行命令時,Redis 將哈希標簽中的內容視為一個整體,這樣就可以將多個 key 分組在一起,使它們被視為同一個分片。這對于在分片集群中對多個相關 key 執行原子操作非常有用。
- 提高數據在集群中的分布均衡性:當使用哈希標簽時,Redis 將根據標簽中的內容計算哈希槽(Hash Slot),而不是整個 key。這樣可以確保具有相同標簽的 key 被映射到相同的哈希槽,從而提高了數據在集群中的分布均衡性。
例如,假設有兩個 key:{sku}:saleStock 和 {sku}:avalibleStock。如果不使用哈希標簽,即sku:saleStock 和 sku:avalibleStock,這兩個 key 將被視為不同的 key,可能被映射到不同的哈希槽。這樣,同一個 sku的不同庫存可能被 hash到不同的 slot,但是,如果使用哈希標簽 {sku},這樣,不管 {sku}拼接什么內容,都會被視為同一個分片,從而確保它們被映射到相同的哈希槽,以保證原子性操作的一致性。
更多{}使用,可以參考redis的官方文檔。
發現問題
監控報警,于是研發查排線上日志,如下:
Caused by: redis.clients.jedis.exceptions.JedisDataException:
ERR Error running script (call to f_1fbde7f097d74a7d77c854c93b308d36d164dbf9): @user_script:371: @user_script: 371:
Lua script attempted to access a non local key in a cluster node at redis.clients.jedis.Protocol.processError(Protocol.java:115)
看到這個錯誤,一臉懵,代碼上線半年沒有出現過問題,怎么會突然出問題呢?
搜索問題
因為第一次遇到這個問題,于是 Google了一下,找到幾個類似的問題,大致意思差不多,下面給出一個stackover上面的例子,鏈接如下:stackoverflow相同的錯誤,Lua 腳本摘要如下:
local f3=redis.call('HGET',KEYS[1],'1');
local f4=redis.call('HGET',f3,'1') ;
return f4;
對于錯誤的解釋是:在 Lua中執行多條語句,要保證key hash的 slot是同一個,否則就會出現上面的錯誤,比如:KEYS[1]和 f3 hash后不在同一個 slot就會出現上述錯誤。
定位問題
順著上面 Google 例子的思路,排查 {sku} hash后的值是否出現變更,線上跑的代碼,sku都是 14位的 Long,新上線的 sku 變成了 15位的 Long,會不會是長度變更導致問題?
于是,在中間件部門同事的配合下,找到了中間件的執行log:
stockskey:stock:40-248-000008:{1.112422310001e+14}
太奇怪了,sku傳入的是 Long類型,現在變成{1.112422310001e+14},最后發現在 Redis中間件有個cjson的操作,當傳入的 Long類型位數大于 14時,會把 Long轉成科學計數法,導致{sku}改變了原有的語義。
解決問題
在 Java 端,把 sku 從 Long型轉成 String類型,再傳入Lua,這樣可以避免 Long被轉換成科學記數法。
事故定級
因為架構中有小流量集群,每次有新 sku上線,都會在小流量集群上進行灰度發布,所以受影響的面有限,最后定級 P4,保住了 Q2的績效。
總結
- Redis中運行 Lua腳本的確能保證原子性,而且經過線上環境驗證。
- 如果想對 Lua中的多個 key hash到同一個 slot,可以使用 Hash Tag 語法,Hash Tag 由一對大括號 {} 包圍,可以將 {} 里面的內容視為一個整體來處理。
- 特別注意,在很多場景 Long類型會被轉成科學記數法,記得曾經和前端對接時,出現過 Long 類型被截斷的問題。
- 灰度發布在生產環境是個很不錯的選擇,對于大的功能上線,可以局部是試錯驗證。
- 告警系統可以幫助我們更快的感知問題,對于大廠是標配,對于中小公司,建議盡量去搭建告警系統,即便簡陋一些也無所謂。