发布时间:2025-11-05 08:13:48 来源:码上建站 作者:应用开发
你好,差点我是损失猿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;1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.在上面的 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到不同的b2b供应网 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)1.2.3.看到这个错误,一脸懵,代码上线半年没有出现过问题,怎么会突然出问题呢?
搜索问题因为第一次遇到这个问题,于是 Google了一下,找到几个类似的问题,大致意思差不多,下面给出一个stackover上面的例子,链接如下:stackoverflow相同的错误,Lua 脚本摘要如下:
复制local f3=redis.call(HGET,KEYS[1],1); local f4=redis.call(HGET,f3,1) ; return f4;1.2.3.对于错误的解释是:在 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}1.太奇怪了,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 类型被截断的问题。灰度发布在生产环境是个很不错的选择,对于大的功能上线,可以局部是试错验证。告警系统可以帮助我们更快的感知问题,对于大厂是标配,对于中小公司,建议尽量去搭建告警系统,即便简陋一些也无所谓。随便看看