client方式的优势是实现简单,只需要通过简单的配置即可完成拆分操作。
在本地通过分片进行计算,得到真实的库和表进行路由,性能相对较高。
不依赖于三方,没有单点故障。
client方式的劣势是每个项目都要去管理分片,读写分离等信息,没办法统一进行管理。
当需要升级的时候只能所有项目都进行升级,没办法统一升级。
最难的点在于需要推动各个业务域进行升级,升级的周期较长,对业务方的压力也比较大。
相对于proxy方式,client方式连接数占用的比较多,一个数据源10个连接,部署10个实例就是100个连接。
proxy方式指的是部署一个独立的服务,这个服务会实现Mysql协议,应用中只需要连接这个独立的proxy服务,把它当做一个完整的独立的数据库使用即可。
分库分表等规则全部由这个proxy去管理,对应用透明。
彩虹桥DAL是得物基础架构组研发的proxy方式的代理服务,有了彩虹桥就可以统一管理所有数据库,可以对数据库操作做限流保护,可以做压测时的数据自动进入影子库,可以监控慢SQL等等。
接入方案
直接修改老数据源-改成连接彩虹桥
最简单,最快速的方式就是直接将所有老的数据源改成新的代理数据源,然后重新发布就可以了。
这个方案确实快,但是不够好,不够完美,不够稳定,为啥?
这其实跟改接口是一样的道理,当某天加了个新的需求,你会发现直接在老接口的基础上改一下就完成了。改完后测试发现当前没问题,但是老版本的产品还在用,然后兼容出问题了。这种场景下如果你是对接口进行升级,之前是v1,现在改成v2,老的逻辑完全不动,就完美了呀。
如果我们直接改成最新的方式,那么如何平滑的发布上线呢?
全部发布,万一彩虹桥那边配置有问题,或者有其他的问题导致不可用,你这不就尴尬了么,当然也可以回滚历史版本。
另一种方式就是灰度发布一台机器,进行测试,没问题后全部发布,有问题就不发,其实也不错。就怕过了一段时间,万一彩虹桥有Bug, 我们还是得回滚到接入彩虹桥之前的版本。
新增一套数据源 - 支持开关切换到新数据源,下线老数据源
新增数据源,支持开关切换就跟升级API版本是一样的逻辑。保持老逻辑不变,上线时还是走老的数据源,然后通过开关动态切换到新的数据源,完成上线动作。
当然这边也会出现上面提到的问题,比如彩虹桥不可用之类的情况,也可以通过灰度配置的方式来测试。将开关配置灰度到某个节点上,只切这个节点的数据源,进行测试,没问题后可以扩大范围。有问题开关一关就还原到之前的逻辑了,无影响。
如果用了一段时间,彩虹桥有Bug的话同样改下开关的配置即可回滚,也不用重新发布项目。
此方案不足的点就是会占用一分部数据库的连接,因为老的数据源还存在。此时需要改代码去掉老的数据源,就能释放了。如果后面还要去改代码,那还能叫完美的方案么?
这点我也考虑到了,还是通过配置开关来关闭老的数据源,但是这个操作得重启服务,重启后就只有一套数据源了。(请确保以后只用彩虹桥的方式才用此开关)
实现步骤
- 配置2套新数据源,连接彩虹桥的地址,订单的库分为老订单库和新的分库分表库。
- 新增2个动态数据源,用于开关动态切换,使用AbstractRoutingDataSource管理。
-
老数据源通过@ConditionalOnProperty来控制是否加载,默认加载,用于后期下线老数据源。
-
读写分离兼容client和proxy方式。
-
分片算法重写,之前用的Sharding-Jdbc3.X版本,新的彩虹桥基于5.X版本深度定制开发,在自定义算法这块有变化,目前彩虹桥的分片算法全部在彩虹桥的扩展包中,不在订单里面。
注意事项
select last_insert_id()不支持
在insert中通过select last_insert_id()实时返回当前插入的自增ID场景需要修改,目前订单中就一个地方用到了,而且上层其实没消费这个ID, 如果后面有其他场景需要获取刚插入的ID可以手动提前获取分布式ID,然后再用这个ID存到表中。
强制走从节点查询
老的读写分离用的Sharding-Jdbc做的,正常情况下默认查询走从节点,其他走主节点。而现在订单里面默认都是走主节点,如果需要强制走从节点需要使用HintManagerHolder.clear() 清空Hint信息来实现。
猜测最开始没有从节点,业务都走主节点。后面加了从节点,如果用默认的方式查询都会走从节点,但是在某些业务场景中一致性比较高,必须走主节点查询,相对于标记出哪些查询走从节点来说,默认全部走主节点更稳。然后根据业务场景再标记是否要走从节点,这样几乎不会影响老的逻辑。
接入彩虹桥后,默认是彩虹桥和Sharding-Jdbc两套数据源共存的,所以在HintManagerHolder.clear() 这块也需要做兼容,同时支持彩虹桥和Sharding-Jdbc方式的强制走从节点。
第一步:兼容老代码
将所有使用HintManagerHolder.clear()改成SqlHintUtils.clear()。
javapublic class SqlHintUtils { /* 清空Hint信息 */ public static void clear() { // ShardingJdbc HintManagerHolder.clear(); // 彩虹桥DAL BifrostContext.clearNotAutoClearPrimary(); } }
第二步:新增注解强制走从节点,比代码更优雅
以后有新的查询需要走从节点就在Dao方法上增加@ForceSlave注解即可,此注解只能作用于Dao方法上,加在其它层无效。如果Dao方法上加了注解,那么方法内所有的查询操作都将走从节点。
老的clear相关的代码其实可以用注解代替,但为了保险起见还是不改变原有的方式,新的可以用注解的方式。