园区切换不生效?Redis DB 不一致这个坑我替你踩过了

切换园区之后 /admin/user/info 还是旧园区,访客预约列表也没刷新。代码没改业务逻辑,问题却在「写 Redis 的那个地方」。本文把这次完整的排查、定位、修复和验证链路写下来,下次再遇到类似的”切换不生效”问题,你大概率可以一眼定位。

🎧 文章导读

🎵 背景音乐

Redis DB 不一致导致登录态缓存错位

图1:admin-service 与 gateway 写入不同 Redis DB,登录态出现”两套真相”

一、需求背景

业务方要求:用户在首页右上角切换园区之后,访客预约列表应该只看当前登录态园区的数据。超级管理员默认可以查看全部数据;普通用户则被强制按当前园区过滤,即使前端请求体里传了别的 parkId 也要被后端忽略

接口:/gateway/through/visitorAppointment/page

需求很清晰,过滤逻辑也不复杂:

用户类型 过滤策略
普通用户 SecurityExtUtils.getCurrentLoginInfo() 取当前登录态 parkId,强制覆盖请求体里的 parkId
超级管理员 通过 RoleClient.checkSuperAdmin(currentUser.getUserId()) 判断,请求体不传 parkId 时看全部,传了就按 mapper 原有条件过滤

底层 SQL 也已经具备 AND ta.park_id = #{model.parkId} 的能力,理论上只差一段 service 的强制覆盖逻辑。我们改完 VisitorAppointmentServiceImpl 之后,本地构建、单测全绿,看起来没毛病。

二、问题现象

测试用户 testvpark

  • 默认园区:广蓄电厂 59258e4f235c4cac64302b5f20658e54
  • 可切园区:广蓄电厂 + 海蓄电厂 3e9293d609fc34ec4b6740aa7f324b6f

按理说,调用切换接口之后,访客预约列表应该立即变成”海蓄”的数据。实际现象:

1
2
3
4
5
6
7
8
9
10
11
1. 登录后默认广蓄
/admin/user/info = 广蓄电厂
/visitorAppointment/page = 测试-广蓄A、测试-广蓄B,共 2 条

2. 调用切换接口
GET /gateway/admin/user/userParkSwitch/3e9293d609fc34ec4b6740aa7f324b6f
→ data: true

3. 切完之后再查
/admin/user/info = 广蓄电厂(没变!)
/visitorAppointment/page = 还是广蓄的 2 条(没刷新!)

切换接口明明返回 true,但 /user/info 和访客预约列表都没动。这种”接口说成功、数据没动”的诡异表现,几乎都是缓存写错了地方。

三、根因:不是访客预约接口的问题,是登录态缓存写错 Redis DB

继续往上游追——切园区走的是 /admin/user/userParkSwitch/{parkId},链路是这样的:

1
2
3
4
切园区请求
→ gateway 路由到 admin-service
→ UserController.userParkSwitch(parkId)
→ UserSecurityUtils.enhanceLoginInfo(...) 把新 parkId 写入 Redis 登录态

而每次普通请求的链路是:

1
2
3
4
5
普通请求
→ gateway AccessControlFilter
→ JwtTokenService.getUserInfo(token) 从 Redis 读 loginUser
→ 写入下游请求 header(loginUser)
→ downstream 服务从 header 读 loginUser

发现没有?两条链路各自读写 Redis,但它们读写的可能是不同的 Redis DB

服务 Redis DB 来源
gateway 登录写 jwt_token DB 7 gateway 的 bootstrap-dev.yml
gateway 每次请求读 loginUser DB 7 同上
admin-service userParkSwitchloginUser DB 6 admin-service 的 bootstrap-dev.yml

也就是说:

  • 切园区把新的 parkId 写到了 DB 6
  • 后续请求经过 gateway,gateway 仍从 DB 7 读取 loginUser,于是拿到的还是旧 parkId
  • /user/info 和 through-service 看到的都是旧园区。

登录态缓存读写路径示意

图2:两条链路分别读写 DB7/DB6,”切换”和”读取”完全错位

3.1 关键代码点回看

我们当时是怎么被表象误导的?

  1. 第一反应是 VisitorAppointmentServiceImpl 写错了,去查 SecurityExtUtils.getCurrentLoginInfo() 的调用——调用是对的,写法也没问题。
  2. 然后看 userParkSwitch 的 service 实现,逻辑也简单:assertBizCondition(parkAllowed) 之后 enhanceLoginInfo(loginUser),返回 true
  3. 再去看 /user/info 的实现——它走 SecurityExtUtils.getCurrentLoginUser(request)只从 request/session/header 拿 loginUser,并不会主动从 Redis 反查。

也就是说,/user/info 看到的 loginUser,永远是 gateway 在 AccessControlFilter 里写入 header 的那一份。gateway 写的是什么?是从 DB 7 读出来的。DB 7 里的 loginUser 从来没有被改过,所以整个系统看起来就像”切园区无效”。

3.2 为什么本地单测没发现

因为单测里 mock 了 JwtTokenService.getUserInfo,直接返回新构造的 loginUser,跳过了 Redis 这一层。在测试环境里一切正常,到了 dev 多服务一起跑、gateway 也在链路上的时候,差异就暴露出来了。

这就是「本地单测通过 ≠ 集成链路正确」的经典场景。单测验证业务逻辑,集成验证”系统集成后的真相”。

四、修复

dev 环境配置差异,统一即可。位置:service-provider/admin-service/src/main/resources/bootstrap-dev.yml

1
2
3
spring:
redis:
database: 7

database: 6 改成 database: 7,和 gateway 的 dev 配置对齐。

修改前:

1
2
3
spring:
redis:
database: 6

修改后:

1
2
3
spring:
redis:
database: 7

注意:这是本地 dev 配置修复;admin-service 需要重启才生效。

为什么只在 dev 改、不统一所有环境?因为我们检查了 prod、staging 的 gateway 和 admin-service 配置,两边的 Redis DB 是一致的;只有 dev 这一份历史配置没人改、也没人注意到。生产环境不用动。

五、验证

构建:

1
mvn -pl service-provider/admin-service -am -DskipTests package

结果:

1
[INFO] BUILD SUCCESS

重启 admin-service 之后再走一遍完整链路:

1
2
3
4
5
6
7
8
9
10
11
1. 登录后默认
/admin/user/info = 广蓄电厂
/visitorAppointment/page = 测试-广蓄A、测试-广蓄B,共 2 条

2. 切到海蓄
/admin/user/userParkSwitch/3e9293d609fc34ec4b6740aa7f324b6f
→ data: true

3. 切换后再查
/admin/user/info = 海蓄电厂 ✓
/visitorAppointment/page = 测试-海蓄A、测试-海蓄B,共 2 条 ✓

没有出现番禺对照数据,说明当前园区过滤生效,且超级管理员的回退逻辑没被破坏

六、经验总结

这种”切了等于没切”的 bug,排障时一定要警惕几件事:

6.1 看清「写」和「读」是不是同一个数据源

写不一定读,读不一定写。当某个值”应该被改了但没改”,先去查”谁在写、谁在读”,而不是去查”业务逻辑是不是错的”。

6.2 单测通过 ≠ 链路正确

mock 跳过了基础设施层(Redis、DB、RPC),只验证了纯业务逻辑。要验证”切园区后整个系统都看到新园区”,必须跑真实链路的集成测试,至少把 gateway、admin-service、through-service 三个服务串起来。

6.3 多服务共享基础设施时,配置必须收敛

风险 缓解
Redis DB 各服务各写一份 application.yml 公共配置里集中维护
MySQL 数据源各服务一份 同样集中维护
中间件地址、端口 通过 Nacos / 配置中心统一

如果 gateway 和 admin-service 都从同一个 spring.redis.database 取值,这种 bug 根本不会发生。我们这次是吃了 dev 配置历史遗留的亏。

6.4 排查清单:缓存错位的 5 步定位法

  1. 接口返回是否符合预期?(切园区返回 true,符合)
  2. 写入侧是否真的写了?(Redis DB 6 有新 parkId,符合)
  3. 读取侧从哪里读?(gateway 从 DB 7 读,不符合)
  4. 配置是否一致?(admin-service 是 DB 6、gateway 是 DB 7,不一致 → 定位到根因)
  5. 修复 + 验证完整链路。

下次再遇到”切了等于没切”的问题,先别急着怀疑业务代码,从第 1 步开始走一遍,大概率 5 分钟内就能定位。

七、结语

一个 dev 配置的历史遗留,差点让我们把 visitorAppointment 的过滤逻辑推翻重写。多服务共享 Redis 时,DB 编号这种小细节的错位,足以让业务接口表现得像一个完全不同的 bug。把这次排障完整记录下来,下次团队里任何同学遇到类似的”切换无效”现象,都能直接复用这次的诊断清单。

切园区不生效?先别动业务代码,去看 Redis DB 是不是一致。