一个bug,差点丢掉Q2绩效奖金

你好,我是猿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 由一对大括号 {} 包围,可以将其中的内容视为一个整体来处理。

{}的主要用途包括:

  1. 强制将多个 key 分组:在执行命令时,Redis 将哈希标签中的内容视为一个整体,这样就可以将多个 key 分组在一起,使它们被视为同一个分片。这对于在分片集群中对多个相关 key 执行原子操作非常有用。
  2. 提高数据在集群中的分布均衡性:当使用哈希标签时,Redis 将根据标签中的内容计算哈希槽(Hash Slot),而不是整个 key。这样可以确保具有相同标签的 key 被映射到相同的哈希槽,从而提高了数据在集群中的分布均衡性。

例如,假设有两个 key:{sku}:saleStock{sku}:avalibleStock。如果不使用哈希标签,即sku:saleStocksku:avalibleStock,这两个 key 将被视为不同的 key,可能被映射到不同的哈希槽。这样,同一个 sku的不同库存可能被 hash到不同的 slot,但是,如果使用哈希标签 {sku},这样,不管 {sku}拼接什么内容,都会被视为同一个分片,从而确保它们被映射到相同的哈希槽,以保证原子性操作的一致性。

更多{}使用,可以参考redis的官方文档:

Redis hash tags文档,

Redis hash slot计算

发现问题

监控报警,于是研发查排线上日志,如下:

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。

总结

  1. Redis中运行 Lua脚本能保证原子性,已经有生产经验进行验证
  2. 如果想对Lua中的多个 key hash到同一个slot,可以使用 {}语法,Hash Tag 由一对大括号 {} 包围,可以将其中的内容视为一个整体来处理
  3. 对于 Long类型会被转成科学记数法,这个点一定要特别注意。记得曾经和前端对接时,出现过过长的 Long 会被截断的问题
  4. 灰度发布在生产环境是个很不错的选择
  5. 告警系统可以帮助我们更快的感知问题

原创好文

相关推荐
Asthenia04127 分钟前
理解词法分析与LEX:编译器的守门人
后端
uhakadotcom8 分钟前
视频直播与视频点播:基础知识与应用场景
后端·面试·架构
Asthenia04121 小时前
Spring扩展点与工具类获取容器Bean-基于ApplicationContextAware实现非IOC容器中调用IOC的Bean
后端
bobz9651 小时前
ovs patch port 对比 veth pair
后端
Asthenia04122 小时前
Java受检异常与非受检异常分析
后端
uhakadotcom2 小时前
快速开始使用 n8n
后端·面试·github
JavaGuide2 小时前
公司来的新人用字符串存储日期,被组长怒怼了...
后端·mysql
bobz9652 小时前
qemu 网络使用基础
后端
Asthenia04123 小时前
面试攻略:如何应对 Spring 启动流程的层层追问
后端
Asthenia04123 小时前
Spring 启动流程:比喻表达
后端