一个bug,差点损失几万

数据库 Redis
最近遇到一个线上事故,差点损失好几万,故事是下面这样的,我们一起来看!

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

2021-10-08 07:50:57

软件设计程序

2021-04-30 07:09:48

SQLP0事故

2020-02-28 08:00:33

企业异常损失

2020-03-04 17:04:00

业务异常人工智能

2021-07-19 08:41:49

蓝屏用户Bug

2015-04-29 06:36:43

2016-09-09 16:47:46

2020-04-23 08:27:21

运维软件系统

2021-12-19 22:00:31

APP软件开发开发

2021-06-07 10:20:31

2014-12-17 09:40:22

dockerLinuxPaaS

2009-09-14 17:08:02

WebFormView

2018-02-10 09:02:27

DevOps持续交付模型

2011-03-03 21:04:08

bug程序员

2017-10-10 15:14:23

BUGiOS 11苹果

2015-08-24 10:07:13

程序员bug

2010-11-17 15:43:55

软件测试Bug

2019-08-01 12:59:21

Bug代码程序

2023-03-13 08:09:03

Protobuffeature分割

2022-06-15 08:14:40

Go线程递归
点赞
收藏

51CTO技术栈公众号