一個(gè)bug,差點(diǎn)損失幾萬(wàn)
你好,我是猿java
最近遇到一個(gè)線(xiàn)上事故,差點(diǎn)損失好幾萬(wàn),故事是這樣的...

背景
在之前的文章里我們分析了 Redis中運(yùn)行 Lua腳本是如何保證原子性的。實(shí)際上,在我們的電商業(yè)務(wù)中也是使用 Redis + Lua來(lái)保證庫(kù)存的原子性操作,Redis是 Cluster集群部署,Lua腳本大致如下(本文的數(shù)據(jù)都經(jīng)過(guò)脫敏處理):
-- type都是java代碼中傳入的String值,sku為L(zhǎng)ong型
local function availableRealSaleCal(type,sku)
local key = formatKey(type, sku)
-- 銷(xiāo)售庫(kù)存 =(if 可售賣(mài)量 then 銷(xiāo)售庫(kù)存 = min(可售庫(kù)存,可售賣(mài)量)
-- else 銷(xiāo)售庫(kù)存 = 可售庫(kù)存 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
-- 拼接庫(kù)存 key,比如:stock:sale:{13523551512}, 注意這里有一個(gè) {sku}
local function formatKey(type, sku)
return "stock:"..type..":"..":{"..sku.."}"
end;在上面的 Lua腳本中,有 {sku}語(yǔ)法的使用,{}是在 Redis cluster 模式下特有的 Hash Tag,Redis 的哈希標(biāo)簽是一種特殊的語(yǔ)法,用于在執(zhí)行命令時(shí)將多個(gè) key 分組在一起。Hash Tag 由一對(duì)大括號(hào) {} 包圍,可以將其中的內(nèi)容視為一個(gè)整體來(lái)處理。
{}的主要用途包括:
- 強(qiáng)制將多個(gè) key 分組:在執(zhí)行命令時(shí),Redis 將哈希標(biāo)簽中的內(nèi)容視為一個(gè)整體,這樣就可以將多個(gè) key 分組在一起,使它們被視為同一個(gè)分片。這對(duì)于在分片集群中對(duì)多個(gè)相關(guān) key 執(zhí)行原子操作非常有用。
- 提高數(shù)據(jù)在集群中的分布均衡性:當(dāng)使用哈希標(biāo)簽時(shí),Redis 將根據(jù)標(biāo)簽中的內(nèi)容計(jì)算哈希槽(Hash Slot),而不是整個(gè) key。這樣可以確保具有相同標(biāo)簽的 key 被映射到相同的哈希槽,從而提高了數(shù)據(jù)在集群中的分布均衡性。
例如,假設(shè)有兩個(gè) key:{sku}:saleStock 和 {sku}:avalibleStock。如果不使用哈希標(biāo)簽,即sku:saleStock 和 sku:avalibleStock,這兩個(gè) key 將被視為不同的 key,可能被映射到不同的哈希槽。這樣,同一個(gè) sku的不同庫(kù)存可能被 hash到不同的 slot,但是,如果使用哈希標(biāo)簽 {sku},這樣,不管 {sku}拼接什么內(nèi)容,都會(huì)被視為同一個(gè)分片,從而確保它們被映射到相同的哈希槽,以保證原子性操作的一致性。
更多{}使用,可以參考redis的官方文檔。
發(fā)現(xiàn)問(wèn)題
監(jiān)控報(bào)警,于是研發(fā)查排線(xiàn)上日志,如下:
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)看到這個(gè)錯(cuò)誤,一臉懵,代碼上線(xiàn)半年沒(méi)有出現(xiàn)過(guò)問(wèn)題,怎么會(huì)突然出問(wèn)題呢?
搜索問(wèn)題
因?yàn)榈谝淮斡龅竭@個(gè)問(wèn)題,于是 Google了一下,找到幾個(gè)類(lèi)似的問(wèn)題,大致意思差不多,下面給出一個(gè)stackover上面的例子,鏈接如下:stackoverflow相同的錯(cuò)誤,Lua 腳本摘要如下:
local f3=redis.call('HGET',KEYS[1],'1');
local f4=redis.call('HGET',f3,'1') ;
return f4;對(duì)于錯(cuò)誤的解釋是:在 Lua中執(zhí)行多條語(yǔ)句,要保證key hash的 slot是同一個(gè),否則就會(huì)出現(xiàn)上面的錯(cuò)誤,比如:KEYS[1]和 f3 hash后不在同一個(gè) slot就會(huì)出現(xiàn)上述錯(cuò)誤。
定位問(wèn)題
順著上面 Google 例子的思路,排查 {sku} hash后的值是否出現(xiàn)變更,線(xiàn)上跑的代碼,sku都是 14位的 Long,新上線(xiàn)的 sku 變成了 15位的 Long,會(huì)不會(huì)是長(zhǎng)度變更導(dǎo)致問(wèn)題?
于是,在中間件部門(mén)同事的配合下,找到了中間件的執(zhí)行l(wèi)og:
stockskey:stock:40-248-000008:{1.112422310001e+14}太奇怪了,sku傳入的是 Long類(lèi)型,現(xiàn)在變成{1.112422310001e+14},最后發(fā)現(xiàn)在 Redis中間件有個(gè)cjson的操作,當(dāng)傳入的 Long類(lèi)型位數(shù)大于 14時(shí),會(huì)把 Long轉(zhuǎn)成科學(xué)計(jì)數(shù)法,導(dǎo)致{sku}改變了原有的語(yǔ)義。
解決問(wèn)題
在 Java 端,把 sku 從 Long型轉(zhuǎn)成 String類(lèi)型,再傳入Lua,這樣可以避免 Long被轉(zhuǎn)換成科學(xué)記數(shù)法。
事故定級(jí)
因?yàn)榧軜?gòu)中有小流量集群,每次有新 sku上線(xiàn),都會(huì)在小流量集群上進(jìn)行灰度發(fā)布,所以受影響的面有限,最后定級(jí) P4,保住了 Q2的績(jī)效。
總結(jié)
- Redis中運(yùn)行 Lua腳本的確能保證原子性,而且經(jīng)過(guò)線(xiàn)上環(huán)境驗(yàn)證。
- 如果想對(duì) Lua中的多個(gè) key hash到同一個(gè) slot,可以使用 Hash Tag 語(yǔ)法,Hash Tag 由一對(duì)大括號(hào) {} 包圍,可以將 {} 里面的內(nèi)容視為一個(gè)整體來(lái)處理。
- 特別注意,在很多場(chǎng)景 Long類(lèi)型會(huì)被轉(zhuǎn)成科學(xué)記數(shù)法,記得曾經(jīng)和前端對(duì)接時(shí),出現(xiàn)過(guò) Long 類(lèi)型被截?cái)嗟膯?wèn)題。
- 灰度發(fā)布在生產(chǎn)環(huán)境是個(gè)很不錯(cuò)的選擇,對(duì)于大的功能上線(xiàn),可以局部是試錯(cuò)驗(yàn)證。
- 告警系統(tǒng)可以幫助我們更快的感知問(wèn)題,對(duì)于大廠是標(biāo)配,對(duì)于中小公司,建議盡量去搭建告警系統(tǒng),即便簡(jiǎn)陋一些也無(wú)所謂。




























