惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Martin Fowler
Martin Fowler
Jina AI
Jina AI
J
Java Code Geeks
Microsoft Security Blog
Microsoft Security Blog
Recent Announcements
Recent Announcements
I
InfoQ
L
LangChain Blog
The Cloudflare Blog
IT之家
IT之家
博客园 - 叶小钗
Apple Machine Learning Research
Apple Machine Learning Research
B
Blog
A
About on SuperTechFans
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Last Week in AI
Last Week in AI
Blog — PlanetScale
Blog — PlanetScale
罗磊的独立博客
云风的 BLOG
云风的 BLOG
Microsoft Azure Blog
Microsoft Azure Blog
Engineering at Meta
Engineering at Meta
F
Fortinet All Blogs
博客园 - 聂微东
美团技术团队
博客园_首页

博客园 - 三国梦回

oracle中date类型在mybatis中查询时遇到的坑 mitmweb在linux上进行反向代理及浏览器中实时查看请求响应 codex对接智谱coding plan,我发现连cc switch也不用了 windows codex更新失败如何处理 windos上codex沙盒报错问题记录 廉颇老矣,装个EclipseMemoryAnalyzer都能卡一小时 codex windows平台中elevated sandbox机制学习 codex windows平台中unelevated sandbox机制学习 bug排查:nacos做配置管理,给服务返回的配置竟然是错的 windows安装codex desktop和cc-switch,接deepseek v4 flash windows安装ddns-go,上传动态的ipv6到自己的域名 immortalWrt路由器上抓包查看电脑联网如何获取ip 路由器刚刷的immortalWrt系统,我是如何配置ipv6的 路由器刷openwrt之ImmortalWrt过程记录--上 spring boot 项目中oracle datasource设置schema spring cloud项目中,在bootstrap.yml中指定了active的profile,结果不生效 线上服务重启后,从nacos取不到配置了,怎么回事 nginx location没学好,把自己坑了一把 技术问题记录20260125 最近遇到的两个技术问题记录 linux服务器文件上传失败 复杂业务系统线上问题排查过程 nacos中配了一个数字,springboot取回来怎么变了 一个java空指针异常的解决过程 简单记录下最近2个月完成的线上系统迁移工作 centos停服,迁移centos7.3系统到新搭建的openEuler 端口telnet不通排查过程 https证书中的subject alternative name字段作用及如何生成含该字段的证书 linux中如何判断一个rpm是手动安装还是通过yum安装的 对接服务升级后仅支持tls1.2,jdk1.7默认使用tls1.0,导致调用失败
线上遇到的redis和数据库数据未同步问题、redisson内部实现问题
三国梦回 · 2025-11-30 · via 博客园 - 三国梦回

背景

从离职同事手里接了一堆系统,其中一个系统,是对app提供服务的,虽说名义上这是一个系统,实际上,里面包含了五六个微服务,这其中有一个微服务,主要负责用户设备的上报和安全提醒,我们就叫设备管理服务:

1、老手机abc首次登录

如果用户首次用这个app,直接把当前这个设备(abc)设置为已信任设备,后台有个表device,会记录:

用户名 设备id 是否已被用户信任(trust) 安全提醒消息id
张三 abc null

而且由于引入了redis缓存来存储信任设备列表,所以会同步设置缓存(如果没有缓存key存在,就会从数据库中查询并设置缓存):

key:用户名

value类型:redis set

value:[abc]

2、换新手机def后登录

等到用户换新手机(设备id:def)的时候,这次上报上来后,发现222这个设备不在用户已信任设备id的集合中(select 设备id from devie where trust = true),理论上,就需要触发一次安全提醒消息,消息是发给老设备abc.

总的来说,就是,当前这个设备管理服务,会去调用消息微服务,得到消息id后,会在数据库中,加一条记录,如下的第二条(未被用户信任,且发送了安全提醒消息)

用户名 设备id 是否已被用户信任(trust) 安全提醒消息id
张三 abc null
张三 def 1

同时呢,消息服务那边,会记录一条消息:

消息id 用户名 可拉取该消息的设备id列表 消息是否被拉取
1 张三 abc false

3、老手机abc登录

老手机此时如果去登录,就会从消息服务拉取到上述的安全提醒消息,此时消息表的拉取状态变成true:

消息id 用户名 可拉取该消息的设备id列表 消息是否被拉取
1 张三 abc true

并发mq通知设备管理服务。设备管理服务这边,就会修改设备表,将设备def改成:已被用户信任。

用户名 设备id 是否已被用户信任(trust) 安全提醒消息id
张三 abc null
张三 def 1

在这个步骤中,由于新增了用户信任设备,会将缓存过期,过期时间是1秒后。

4、新手机def再次登录

由于此时,def在数据库中,trust标志为true:已被用户信任,所以就不会再触发安全提醒的消息了。

线上问题

然而,接过来也没多久,产品就说线上有问题(好像没接手之前也有吧,忘了),偶尔会存在乱发短信的问题(就是不该发但发了),比如明明是用户自己的设备,但也会提醒你:你的账号在1台或者某几台(这里是占位符,设备型号,如iphone/小米/华为等)上登录,请注意安全,必要时联系客服或报警。

由于这个app和钱相关,而且是大量的钱,所以真就有不少用户去报警了。有的客户可能感觉是程序bug,但和钱相关后,就谨慎了,至少报警留个痕,对吧,免得后续扯皮时吃亏。

这些线上问题出了之后,我当前也是才接了这个系统没多久,出于一些原因和工作优先级的问题,也没花特别多时间来处理这个,产品说这个功能有开关,只能就是把这个安全提醒的功能给关闭了先。

等后续高优先级工作完成后,还是抽空看了下这块。

代码存在的一些问题

其实,大家看上面这个逻辑,是不是也不算复杂,如果纯粹的数据库操作的话,逻辑还是蛮简单;但我接手后,从代码和部分文档,来梳理出上述的逻辑,费了不少事。1个就是,代码里引入了缓存redis,来存储:已被用户信任的设备id集合,key是用户名,value就是redis的set类型,元素类型就是字符串,比如,最开始老手机登录,存的就是abc,等到上面的第三步后(老手机拉取了消息后),缓存里存的就应该是abc和def。

另外呢,引入了mq机制,说实话,mq也就是个用,我看也没有相关的确保消息不丢失的代码,不过这都还好,本次的问题也不是mq的问题。

再一个问题就是,这个设备id号,其实是app来生成的,但它也是可能变化的,同一台手机上,我如果把app完全卸载重装,设备id就会发生变化。其实后来讨论过,为啥不使用手机的硬件相关数据,如:IMEI;app同事好像是说,现在权限管得很严,不让获取IMEI字段,具体我也不清楚。

还有个麻烦的问题,用户app端是可以看到所有这些设备的,就是能查询如下表的记录,而且可以改,可以删:

用户名 设备id 是否已被用户信任(trust) 安全提醒消息id
张三 abc null

可以删,还是他么的物理删除。

当时查问题的时候,一方面代码难看,没啥注释,代码也是面条式代码,再加上数据库里记录被物理删除,真的只能各种翻日志了,日志可能还是横跨好多天的。我这里也给大家放一下这个设备上报接口的逻辑图:

image-20251130123735017

问题处理

思路

首先,这个问题不是必现的,是偶现。我在本地也没复现出来。

所以,我当时的思路是比较暴力的:

1、数据库物理删除改成逻辑删除;

2、代码简单重构,增加日志,规范变量命名,抽取函数等等;

3、讨论是否直接去掉redis缓存。

第三点,由于涉及架构变动,产品和测试认为动作有点大了,就先只改了前两点。

测试

测试时,没想到测试得同事偶然复现了这个问题,由于这次增加的日志多了不少,几乎就确认了,是缓存和数据库存在不同步的问题。

是这样的,前面背景中说到的,第三步是:老手机去拉取安全提醒的消息,然后此时,消息服务会调用设备管理服务,提示消息被消费了,可以将设备def改成常用了。我们从测试时的日志发现,数据库确实是改了。

用户名 设备id 是否已被用户信任(trust) 安全提醒消息id
张三 abc null
张三 def 1

然后,缓存操作这块呢,代码是这样的:

image-20251130125119402

然后,后续这个用户的新设备def再登录的话,按理来说,就会发现缓存过期了,就会从数据库中加载:

image-20251130125338893

如果真是走了这段数据库加载的逻辑,基本可以说,上面那行日志是一定会打印的,但是,很遗憾啊,没打这行日志,看来,程序是发现从缓存中取到的set不为空。

但是,这怎么会呢,我第三步设置缓存1s后过期;后续def上报时,都过了好几十秒了,不可能缓存还在吧?

那就是这几十秒中间,出了什么事吧。

然后在这几十秒中间的日志里面找,发现:在第三步设置缓存1s过期后,马上几十毫秒内,又有一个该用户的设备上报请求(测试同事手里手机比较多,有4部,这一步我们命名为设备h吧),此时,那个本应该1s后过期的缓存,此时还存在于redis中。

设备h上报时,触发下面的代码:

image-20251130125935097

我们进去看看:

image-20251130130028557

上述这个红框代码,将缓存过期时间又给延长了啊。。。导致本应该1s后过期的缓存(内容:abc),生命周期又变得很长很长,此时,数据库中的已信任设备为:abc、def,而缓存中则是abc,这就产生了不一致。

问题根源

根源就是代码写得有问题。你改了数据库中def为常用设备了,缓存直接删除了不就行了,为啥要1s后过期呢,这不就1s内就是不一致的吗,直接删了也就不存在这个问题了。

尤其是,这个获取缓存的动作,还包含了设置过期时间的功能:

image-20251130130415766

另外,由于这里混用了两个redis框架(spring redisTemplate和redisson),我一开始还怀疑偏了,以为是redisson内部有缓存之类的问题。

所以,对于一般的应用来说,不是严格要求数据库和缓存要一致的那种,一般:先修改数据库,再把缓存key删了,就够用了;对于互联网大厂那种,这种可能还是不够用,就会用什么binlog同步啥的,我就不操心那么多了,大家自己搜索吧。

redisson的问题

判空

在debug的时候,发现redisson原来是这样的。

就比如,我们上述的程序中,获取key:

RSet<String> ret = redissonClient.getSet(keyPrefix.prefix() + key);

其中,Rset是redisson中的类型:

image-20251130131012064

当我们在判断rset是否为空时(org.apache.commons.collections4.CollectionUtils#isEmpty):

image-20251130131104455

image-20251130131214316

image-20251130131227115

image-20251130131249229

最终,就会触发一个对redis的SCARD命令,也就是执行scard,获取集合中元素的数量:

image-20251130131404458

contains

我们代码中还包括contains:

image-20251130131542065

这个是触发如下命令:

image-20251130131621937

获取集合中元素

我们还有如下代码:

image-20251130131656175

这个触发的是:

image-20251130131812589

image-20251130131830704

相关抓包截图

image-20251130132140017

image-20251130132214408

image-20251130132231173

总结

引入缓存,是为了解决问题,但是,如果不注意的话,就会发生:缓存数据库不同步问题;

引入redisson,是为了避免频繁操作数据库,但是,不了解内部的话,就会发现,简单的几个方法调用,就触发了一堆的网络调用,这是否真的高性能了呢?