大家好,这里是「敖行客研发实战日记」GGB。
这次的活儿是把一套外购的即时通讯系统(下称 IM)整体融进我们自研的微服务平台:IM 自己的登录注册整个砍掉,用户体系和平台合成一套,历史聊天数据一条不丢。听起来是“搬家加接线”,真动手才发现,最难的不是代码,是两套用户身份攒了几年的历史包袱。下面按踩坑顺序复盘。
一、两套主键:459 处身份引用悬在一根线上
IM 全站的用户身份,是它自己用户表的自增 id——好友、群成员、私聊、群消息、黑名单、收藏,所有业务表的外键都指向这个自增号;而平台侧的用户主键是另一套长整型业务 id。要把“两边的同一个人”真正合成一个人,等于要给 IM 整个社交图谱的主键换血。
把代码整个扒了一遍,先得到一个吓人的数字:全项目 459 处身份引用,散在 21 个文件里。但继续扒下去发现天大的好消息——459 处全部经由“会话上下文取当前用户 id”这一个出口流进各张表,没有一处绕道。也就是说,根本不用碰那 459 处业务代码:只要改“进入会话的那个值”,再把存量数据做一次 id 重映射,下游原封不动地跟着新值走。方案的核心就此定型:平台用户表是唯一身份,IM 用户表降级成“扩展资料表”,主键直接等于平台用户 id。
**经验:**融合两套系统之前,先找“身份的唯一出口”。出口收得越窄,融合的代价越小;身份取值散落得到处都是,那才是真正的灾难。
二、注释会说谎:号称“桥接列”的字段只填了 4 行
IM 用户表里有一列,注释明明白白写着“平台用户 id”,看起来天生就是为融合准备的桥。差点直接拿它当迁移依据——好在动手前先跑了一轮只读诊断:35 个 IM 用户里,这一列只填了 4 个。再回头翻代码,更狠:IM 自己的代码从头到尾没有任何一处写这个列,它是当年不知道谁手工补的数据。
**经验:**注释和字段名描述的是“愿望”,代码和数据才是“事实”。任何要拿来当迁移依据的字段,先数一遍填充率,再确认到底有没有人在写它。
三、数字虚高二十倍:JOIN 扇出与逻辑删除的幽灵
正式迁移前做匹配预演:明明只有 35 个用户,预演统计却跑出 780 条匹配。数字对不上,立刻停手。
根因有两层。第一层是统计 SQL 里四个 LEFT JOIN 发生了笛卡尔扇出,一个用户的手机号能 JOIN 出几十行;第二层才是真凶——平台用户表里躺着大量“逻辑删除”的历史账号,它们还揣着和活人一模一样的手机号和用户名,是这批幽灵把 JOIN 撑爆的。把逻辑删除的账号滤掉再跑,数字严丝合缝:35 = 16 个能匹配 + 19 个纯 IM 历史孤儿,且无一冲突。
这一步在迁移脚本里被做成了硬性“STOP 闸门”:只读诊断的数字对不上总量,后面的破坏性阶段一律不准执行。事后看,这道闸门至少避免了一次“把两个人的聊天记录并到一个人头上”的事故。
**经验:**数据迁移脚本必须分段执行,且第一段永远是只读诊断;预演数字和总量对不上,说明有你还没看见的数据质量问题在前面等着。
四、匹配规则:从四级优先级砍到一条
最初的匹配设计很“完备”:桥接列、手机号、用户名、邮箱四级优先级层层兜底,尽可能多地把人对上。和项目经理对完,最后砍成一条:未删除账号且手机号一致才映射,其余一律不映射,宁可留孤儿。
孤儿并不可怕:老 id 和新体系的 id 永不冲突,历史消息原样可查,无非是这批账号没有平台身份、登不进来。错误映射才可怕——把 A 的聊天记录挂到 B 头上,在公司里这叫事故。为了保险,映射表上还压了一条唯一约束:任何情况下都不允许两个 IM 账号并到同一个平台账号,撞上就报错停下,绝不静默吞掉。
**经验:**迁移匹配规则,“简单且可解释”压倒“覆盖率高”。每多一级模糊兜底,就多一分把人对错的概率。
五、差点白干三周:动手之前先盘家底
用户合并方案定了,下一步是把 IM 从新框架降级改造成平台的老框架微服务:343 个 Java 文件,包名、依赖体系、模块拆分全要动,怎么估都是两三周起步的体力活。
动手前习惯性翻了翻历史工程,结果发现一个月前有个实验版本已经把这件事干完了:降级、五模块拆分、连“共享会话登录”都写好了。把两边的类集合做了个 diff:341 对 344,业务类几乎完全一致,差异清一色是启动类和配置类。于是策略从“从头降级”变成“整目录搬移加三处构建配置”,第一次编译就通过,预估三周的活变成了一天。
**经验:**接到“体力活”先别埋头干,花半小时盘一遍家底。团队的历史分支和旧工程里,往往埋着已经趟平的路。
六、最后一个急转弯:把“同步”整个扔掉
资料一致性的初版方案是“改哪同步哪”:平台改了昵称头像,就调接口把 IM 侧的数据刷一遍,登录时兜底同步一次,搜索时再落一次库——三条链路维护两份数据。写到一半越写越不对劲:这分明是在用三个补丁去掩盖“数据存了两份”这个根问题。
推翻重来。IM 侧所有“这个人叫什么、头像是什么”的展示读取,收拢到一个资料门面:读的时候实时批量取平台数据,平台查不到的历史孤儿账号回落本地兜底。IM 自己的用户表彻底降级成“扩展资料表”,只存 IM 专属的设置项;平台侧一行代码都不用改。“同步”这个概念直接消失了——不存在同步不同步,因为数据只有一份。
顺带着,“登录”的认知也被扭了过来:融合之后不存在“平台登录之后 IM 再自动登录一次”这回事。两边共享同一套会话,登录了平台,访问 IM 天然就是登录态;真正要接的只剩前端拿一张临时凭证去建立长连接,一个接口的事。
**经验:**当你发现自己在写第三条同步链路的时候,停下来想想能不能改成“只有一份数据”。同步是补丁,单一数据源才是解。
七、小结
融合两套系统,先找“身份的唯一出口”:出口越窄,换血的代价越小。
迁移必须设只读诊断闸门:数字对不上就停,别让破坏性操作跑在疑问上面。
注释、字段名、文档都会说谎,代码和数据不会:迁移依据只认查证过的事实。
匹配规则宁简勿全:留下孤儿是遗憾,把人对错是事故。
动手之前盘家底:历史工程里往往埋着已经趟平的路,半小时能省三周。
能实时读就不要同步:单一数据源,永远优于多份拷贝加一堆补丁。
至此,用户体系合一、历史数据分毫未丢、一次登录全端通行,IM 从一座孤岛变成了平台里的一个普通微服务。融合这种活没有银弹,能做的就是让每一步都踩在“验证过的事实”上,而不是“看起来对”的假设上。