你好,我是猿java。
最近遇到一个线上事故,差点丢掉Q2绩效奖金,故事是这样的...
背景
在 阿里 P7二面:Redis 执行 Lua,能保证原子性吗? 这篇文章中,我们分析了 Redis中运行 Lua脚本是如何保证原子性的。实际上,在我们的电商业务中也是使用 Redis + Lua来保证库存的原子性操作,Redis是 Cluster集群部署,Lua脚本大致如下(本文的数据都经过脱敏处理):
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的官方文档:
发现问题
监控报警,于是研发查排线上日志,如下:
log
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 脚本摘要如下:
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例子的解释,开始排查我们线上的 Lua脚本,整个 Lua是使用{sku}
进行 hash,然后{sku}
的结果不稳定,导致几条语句执行的时候 hash到不同的 slot中?
顺着上面 Google 例子的思路,排查 {sku}
hash后的值是否出现变更,线上跑的代码,sku都是 14位的 Long,先上新的 sku 变成了 15位的 Long,会不会是长度变更导致问题?
于是,在中间件部门同事的配合下,找到了中间件的执行log:
text
stockskey:stock:40-248-000008:{1.112422310001e+14}
太奇怪了,sku传入的是 Long类型,现在变成{1.112422310001e+14}
,最后发现在 Redis中间件有个cjson的操作,当传入的 Long类型位数大于 14时,会把 Long转成科学计数法,导致{sku}
改变了原有的语义。
解决问题
在Java 端把 sku 从 Long型转成 String类型,再传入Lua。
事故定级
因为架构中有小流量集群,每次有新 sku上线,都会在小流量集群上进行灰度发布,所以受影响的面有限,最后定级 P4。
总结
- Redis中运行 Lua脚本能保证原子性,已经有生产经验进行验证
- 如果想对Lua中的多个 key hash到同一个slot,可以使用
{}
语法,Hash Tag 由一对大括号{}
包围,可以将其中的内容视为一个整体来处理 - 对于 Long类型会被转成科学记数法,这个点一定要特别注意。记得曾经和前端对接时,出现过过长的 Long 会被截断的问题
- 灰度发布在生产环境是个很不错的选择
- 告警系统可以帮助我们更快的感知问题