你好,我是猿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 类型被截断的问题。
- 灰度发布在生产环境是个很不错的选择,对于大的功能上线,可以局部是试错验证。
- 告警系统可以帮助我们更快的感知问题,对于大厂是标配,对于中小公司,建议尽量去搭建告警系统,即便简陋一些也无所谓。