打工人打工魂,打工仔hellohello-tom上线啦🤣
tom哥真是越来越懒了,懒得动笔,有很多粉丝一直在催我更新,所以tom哥整理了一下,今天打算来场硬核输出,继续更新人人能看懂系列,文字较多,建议多读几遍
人人都能看懂系列:《分布式系统改造方案——老旧系统改造篇》。
很多同学到一家公司相信说的最多的一句话都是,窝xxxx,这什么垃圾代码,我可没办法维护,让我改的话只能推翻重写,毕竟每个人都只熟悉自己的代码,但是老板可不会让你这么轻而易举答应你的要求,尤其是项目已经正在运营,正在产生收益,很多同学只能心里MMB接受,继续在混乱不堪的代码上迭代功能,迭代不动了,就离开这家公司,把坑留给下个人,下个程序员继续重复如上的循环,是不是很真实🤣
今天tom哥就把自己的亲身经历讲给大家,不用怕老旧系统,技术债是早晚都要还的,而公司也愿意花大价钱把能填坑的人给招来,tom哥希望每位同学都能成为那个填坑的人(毕竟能得到不菲的收入),如果想成为那个填坑的人来看看tom哥怎么处理的。
数据库
大部分程序的业务都围绕在model上,数据库肯定跑不了,举个用户表的例子
字段名 | 备注 |
user_id | 用户id |
nick_name | 昵称 |
... | ... |
tom哥刚来公司的时候,一看用户表,妈呀将近100个字段,各种各样纬度的字段,昵称基本信息不说了,还有什么点赞数量、收入量,是不是vip等等乱起八遭的数据全部都冗余在了用户表,完全不符合数据库设计三范式的原则,并且缓存设计也是经典的把一行数据对应的model直接存到redis内,导致一个用户数据在缓存占用将近300多kb,可真不小。并且现有的用户表的数据早已经过了千万,按照mysql的单表500W的原则,也是时候做横向拆分了。
大家在面试的时候都应该会被问到分库分表相关问题,我们可能会做水平分表,纵向分表,但是往往初期业务没有那么复杂,像tom哥公司这样的情况应该会存在好多公司内,尤其是项目已经上线,如何做好平滑迁移实数不易,所以很多同学应该都是只有理论知识,没有实战经验。既然要拆,并且还是用户无感知的情况下拆分,tom哥在实践中总结的影响最小的三步法则。
1、数据同步,老表同步新表
2、查询替换
3、更换写入,干掉老表
看tom哥具体怎么实践这三个步骤
1、数据同步,老表同步新表
我们的线上系统是一直在跑的,每分钟都产生订单都在给公司创造利润,一刀切肯定不可能,再加上老表设计的不合理,已经没办法做水平切分,我们只能整理业务,把新表重新梳理
比如tom哥把原来的100多个字段的用户大表按照具体的职责进行了垂直划分,这里划分的原则需要结合多维度去考虑,例如业务场景、字段更新频率等等,需要充分了解业务后才能做出正确的划分原则。
再划分表后与公司产品总监(老板)商讨后具体就要实施数据迁移了,本来tom哥想着是在现有老代码的基础上用户注册成功之后发送一个异步事件,由异步事件去把新注册的数据给写入,但是创建用户不仅仅在注册产生,并且在老代码上修改也很不安全,要是加个这,导致服务崩了,tom哥可就要背个P0事故了,并且更新用户表单个字段的地方非常多,不可能在每个地方都要加个事件异步去刷新吧,所以直接pass。
综合考虑之下,tom哥使用canal进行了mysql binlog订阅的模式,在确定数据入库成功后,由canal订阅老库的binlog,发送到rocketmq内,再由消费端把数据写入到新库新表内。
这样新产生的数据就能用户无感知的处理了,有的同学会问那老的用户数据怎么办呢,别急接下来tom哥会说老的数据怎么办,既然都统一通过binlog订阅处理这种模式来进行数据入库,那能不能我写个客户端程序,把历史数据模拟成binlog的插入模式,发送到mq,由mq订阅消费处理呢,逻辑图参考如下:
这里有个坑,生产productor不能和消费Binlog同时开始处理,因为会有这种情况,生产的数据程序在查出来那一刻其实已经可以称为历史数据,你是没办法判定你当前的数据到底在老库内是不是最新的(老的表内压根不存在什么版本号和更新时间戳啥的),所以tom哥的方案是,先开启binlog,但是不消费,我们可以创建两个topic来进行控制,同时我们也要保证一个用户数据路由到一个queue进行处理,离散处理的话搞不好数据就又乱了,时序没法保证,挤压的话问题就更多了。
topicName | 备注 | 简称 |
user_oldmysql_binlog_topic | 老库产生的binlog日志 | A |
user_productor_binlog_topic | 生产者模拟产生的binlog日志 | B |
我们开始先把老库内canal产生的binlog让它挤压到A内,不进行消费,然后我们开启我们的生产者模拟insert binlog模式,生产消息并发送到B
(A、B消费顺序很重要),我们的consumer就正常开始消费B
topic内的消息,当我们的生产者把老库的用户数据取完之后(早晚都会取完的,或者我们定一个时间点在开启A
topic之后任意时间点都可以),这时候户数据基本都已经入到新库了。接下来开启A
topic,我们的consumer开始消费A的消息,这里面我们可以做个容错机制,就是针对A
topic插入如果发生userId的主键冲突我们可以直接丢弃。然后慢慢就像看动画书一样,慢慢把A
topic 内binlog数据一点一点演变到新库的表字段上,直到只消费老库产生的实时binlog日志就完成了。
这里很重要。建议没看明白的同学自己多思考几遍,想清楚这个顺序。上面tom哥说的这个容错处理,其实就是一个时间交叉的问题,有可能你productor取到的数据已经入库了,在消费A
topic真正的产生的binlog时,会重复插入,所以我们不用重复插入,捕获主键冲突异常直接丢弃就可以了。至于更新操作,前一步productor生产的消息有可能是最新的也有可能是老的,所以我们结合A
topic实时binlog按照顺序慢慢把数据还原到最终的那个快照,没想明白多想想哈。很重要!!!
2、查询替换
解决了第一步那个难题,这第二步就很简单了,基础数据我们都有了,那不是随心所欲了,tom哥我直接给用户分库分表了,按照我们日活100万的原则,一口气分他个2个库,每个库16个表,用户id取模均匀落库,考虑数据库读写瓶颈,以及日后动态扩容用户表,tom哥这里引入了shardingjdbc,再来个读写分离,perfect,一次搞定(PS:sharding真是方便)。
库 | 表 |
user_master_0(0~5000万的用户在这里放) | user_0,user_1,... |
user_master_1(5001万~1亿的用户在这里放) | user_0,user_1,... |
user_master_.. | user_0,user_1,... |
接下来然后我们直接和APP商量吧,把在老程序内的查询操作慢慢替换为新写的程序的接口,慢慢迭代呗,这快不了。中间还可能会有很多问题呢。
说到查询替换了,tom哥这里简单说说ES的问题,用户索引设计可能很多同学也没思路,我们公司基本这样来布局:
1、用户索引考虑把接口用的和管理后台用的索引分开设计,
2、假设现在有4台ES集群,3台高配(A、B、C),1台低配(D)
接口用户索引定义为hot节点,控制索引文档生成到hot node (A、B、C)上,按照模板创建user_index_0(1、2、3)接口索引按照40G大小为一个单位,控制用户每5000万在一个,后期把日活用户。例如100万或者200万、300等,再维护到一个索引内(hot_user_index)。
3、管理后台索引定义为cold节点,控制索引生成到cold node(D)上,manage_user_index
4、管理后台索引直接扔一个索引了(manage_user_index)
核心思路就是(冷热隔离),避免管理后台和接口共用时,多维度查询顶替掉热数据
这样基本也都够用了,很多大型系统里面看你注册用户有个几千万,可能日活的用户连100万用户都不到,这时候考虑好冷热隔离就行,别冷不丁的什么查询把es内的热数据顶掉,全部都是冷数据导致把es拖垮可就GG了。
3、更换写入,干掉老表
最后一步最简单了,前期的查询基本都替换完了,接下来就是更改数据写入了,原来我们的新库都是基于老库的binlog做数据同步的,那现在目标很明确,把所有数据写入用户相关的全部替换到我们新的程序内,也是慢慢迭代吧,中间肯定还是要有一段时间新老共存的阶段,全部替换完,停掉canal订阅,整个老旧系统就全部改造完成了。
看完tom哥说的是不是很简单,实操起来还是有很多复杂点和意料不到的情况的,好了今天分享就结束了,下期内容tom即兴发挥,哈哈
我是hellohello-tom,一个二线城市的程序员