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

图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 | 1. 登录后默认广蓄 |
切换接口明明返回 true,但 /user/info 和访客预约列表都没动。这种”接口说成功、数据没动”的诡异表现,几乎都是缓存写错了地方。
三、根因:不是访客预约接口的问题,是登录态缓存写错 Redis DB
继续往上游追——切园区走的是 /admin/user/userParkSwitch/{parkId},链路是这样的:
1 | 切园区请求 |
而每次普通请求的链路是:
1 | 普通请求 |
发现没有?两条链路各自读写 Redis,但它们读写的可能是不同的 Redis DB。
| 服务 | Redis DB | 来源 |
|---|---|---|
gateway 登录写 jwt_token |
DB 7 |
gateway 的 bootstrap-dev.yml |
gateway 每次请求读 loginUser |
DB 7 |
同上 |
admin-service userParkSwitch 写 loginUser |
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 关键代码点回看
我们当时是怎么被表象误导的?
- 第一反应是
VisitorAppointmentServiceImpl写错了,去查SecurityExtUtils.getCurrentLoginInfo()的调用——调用是对的,写法也没问题。 - 然后看
userParkSwitch的 service 实现,逻辑也简单:assertBizCondition(parkAllowed)之后enhanceLoginInfo(loginUser),返回true。 - 再去看
/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 | spring: |
把 database: 6 改成 database: 7,和 gateway 的 dev 配置对齐。
修改前:
1 | spring: |
修改后:
1 | spring: |
注意:这是本地 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 | 1. 登录后默认 |
没有出现番禺对照数据,说明当前园区过滤生效,且超级管理员的回退逻辑没被破坏。
六、经验总结
这种”切了等于没切”的 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 步定位法
- 接口返回是否符合预期?(切园区返回 true,符合)
- 写入侧是否真的写了?(Redis DB 6 有新 parkId,符合)
- 读取侧从哪里读?(gateway 从 DB 7 读,不符合)
- 配置是否一致?(admin-service 是 DB 6、gateway 是 DB 7,不一致 → 定位到根因)
- 修复 + 验证完整链路。
下次再遇到”切了等于没切”的问题,先别急着怀疑业务代码,从第 1 步开始走一遍,大概率 5 分钟内就能定位。
七、结语
一个 dev 配置的历史遗留,差点让我们把 visitorAppointment 的过滤逻辑推翻重写。多服务共享 Redis 时,DB 编号这种小细节的错位,足以让业务接口表现得像一个完全不同的 bug。把这次排障完整记录下来,下次团队里任何同学遇到类似的”切换无效”现象,都能直接复用这次的诊断清单。
切园区不生效?先别动业务代码,去看 Redis DB 是不是一致。