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

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hacker News: Front Page
P
Palo Alto Networks Blog
T
ThreatConnect
Apple Machine Learning Research
Apple Machine Learning Research
博客园_首页
T
True Tiger Recordings
P
Privacy & Cybersecurity Law Blog
B
Blog
IT之家
IT之家
Last Week in AI
Last Week in AI
F
Full Disclosure
Hacker News: Ask HN
Hacker News: Ask HN
C
Comments on: Blog
Microsoft Azure Blog
Microsoft Azure Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Microsoft Security Blog
Microsoft Security Blog
博客园 - 【当耐特】
N
News and Events Feed by Topic
NISL@THU
NISL@THU
腾讯CDC
雷峰网
雷峰网
Security Latest
Security Latest
李成银的技术随笔
M
Microsoft Research Blog - Microsoft Research
L
LangChain Blog
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Check Point Blog
Y
Y Combinator Blog
Recent Announcements
Recent Announcements
博客园 - Franky
N
News | PayPal Newsroom
V
V2EX
A
About on SuperTechFans
The Register - Security
The Register - Security
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google Online Security Blog
Google Online Security Blog
MyScale Blog
MyScale Blog
Cisco Talos Blog
Cisco Talos Blog
Vercel News
Vercel News
WordPress大学
WordPress大学
C
Cyber Attacks, Cyber Crime and Cyber Security
The Hacker News
The Hacker News
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
爱范儿
爱范儿
A
Arctic Wolf
L
LINUX DO - 最新话题
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

博客园 - 三国梦回

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,导致调用失败 网络抓包文件太大,如何切分 分页查询不加排序有问题,加了排序怎么还有问题 利用mybatis拦截器记录sql,辅助我们建立索引(二) 利用mybatis拦截器记录sql,辅助我们建立索引(一) sql server版本太老,java客户端连接失败问题定位
线上遇到的redis和数据库数据未同步问题、redisson内部实现问题
三国梦回 · 2025-11-30 · via 博客园 - 三国梦回

背景

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

1、老手机abc首次登录

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

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

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

key:用户名

value类型:redis set

value:[abc]

2、换新手机def后登录

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

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

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

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

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

3、老手机abc登录

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

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

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

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

在这个步骤中,由于新增了用户信任设备,会将缓存过期,过期时间是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端是可以看到所有这些设备的,就是能查询如下表的记录,而且可以改,可以删:

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

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

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

image-20251130123735017

问题处理

思路

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

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

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

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

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

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

测试

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

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

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

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

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