一个复杂系统的拆分改造实践( 三 )


4) 内存拼接
4.1)通过RPC调用来获取另一张表的数据 , 然后再内存拼接 。 1)适合job类的sql , 或改造后RPC查询量较少的sql;2)不适合大数据量的实时查询sql 。 假设10000个ID , 分页RPC查询 , 每次查100个 , 需要5ms , 共需要500ms , rt太高 。
一个复杂系统的拆分改造实践文章插图
4.2)本地缓存另一张表的数据
适合数据变化不大、数据量查询大、接口性能稳定性要求高的sql 。
一个复杂系统的拆分改造实践文章插图
3.1.4切库方案设计与实现(两种方案)以上步骤准备完成后 , 就开始进入真正的切库环节 , 这里提供两种方案 , 我们在不同的场景下都有使用 。
a)DB停写方案
一个复杂系统的拆分改造实践文章插图
优点:快 , 成本低;
缺点:
1)如果要回滚得联系DBA执行线上停写操作 , 风险高 , 因为有可能在业务高峰期回滚;
2)只有一处地方校验 , 出问题的概率高 , 回滚的概率高
举个例子 , 如果面对的是比较复杂的业务迁移 , 那么很可能发生如下情况导致回滚:
sql联表查询改造不完全;
sql联表查询改错&性能问题;
索引漏加导致性能问题;
字符集问题
此外 , binlog逆向回流很可能发生字符集问题(utf8mb4到gbk) , 导致回流失败 。 这些binlog同步工具为了保证强最终一致性 , 一旦某条记录回流失败 , 就卡住不同步 , 继而导致新老表的数据不同步 , 继而无法回滚!
b)双写方案
一个复杂系统的拆分改造实践文章插图
第2步“打开双写开关 , 先写老表A再写新表B” , 这时候确保写B表时try catch住 , 异常要用很明确的标识打出来 , 方便排查问题 。 第2步双写持续短暂时间后(比如半分钟后) , 可以关闭binlog同步任务 。
【一个复杂系统的拆分改造实践】优点:
1)将复杂任务分解为一系列可测小任务 , 步步为赢;
2)线上不停服 , 回滚容易;
3)字符集问题影响小
缺点:
1)流程步骤多 , 周期长;
2)双写造成RT增加
3.1.5 开关要写好不管什么切库方案 , 开关少不了 , 这里开关的初始值一定要设置为null!
如果随便设置一个默认值 , 比如”读老表A“ , 假设我们已经进行到读新表B的环节了 。 这时重启了应用 , 在应用启动的一瞬间 , 最新的“读新表B”的开关推送等可能没有推送过来 , 这个时候就可能使用默认值 , 继而造成脏数据!
3.2 拆分后一致性怎么保证?以前很多表都在一个数据库内 , 使用事务非常方便 , 现在拆分出去了 , 如何保证一致性?
1)分布式事务
性能较差 , 几乎不考虑 。
2)消息机制补偿(如何用消息系统避免分布式事务?)
3)定时任务补偿
用得较多 , 实现最终一致 , 分为加数据补偿 , 删数据补偿两种 。
3.3 应用拆分后稳定性怎么保证?一句话:怀疑第三方 , 防备使用方 , 做好自己!
一个复杂系统的拆分改造实践文章插图
1)怀疑第三方
a)防御式编程 , 制定好各种降级策略;

  • 比如缓存主备、推拉结合、本地缓存……
b)遵循快速失败原则 , 一定要设置超时时间 , 并异常捕获;
c)强依赖转弱依赖 , 旁支逻辑异步化
  • 我们对某一个核心应用的旁支逻辑异步化后 , 响应时间几乎缩短了1/3 , 且后面中间件、其它应用等都出现过抖动情况 , 而核心链路一切正常;
d)适当保护第三方 , 慎重选择重试机制
2)防备使用方
a)设计一个好的接口 , 避免误用
  • 遵循接口最少暴露原则;很多同学搭建完新应用后会随手暴露很多接口 , 而这些接口由于没人使用而缺乏维护 , 很容易给以后挖坑 。 听到过不止一次对话 , ”你怎么用我这个接口啊 , 当时随便写的 , 性能很差的“;
  • 不要让使用方做接口可以做的事情;比如你只暴露一个getMsgById接口 , 别人如果想批量调用的话 , 可能就直接for循环rpc调用 , 如果提供getMsgListByIdList接口就不会出现这种情况了 。
  • 避免长时间执行的接口;特别是一些老系统 , 一个接口背后对应的可能是for循环select DB的场景 。
b)容量限制