0
点赞
收藏
分享

微信扫一扫

自己动手写一个分库分表中间件(二)数据源定义和分片代理层设计

分库分表最核心的功能是数据源路由。首先要确定怎么样算是一个数据源。

数据源定义

选择自研就是为了更适配我们的业务,在上一篇文章《​​自己动手写一个分库分表中间件(一)​​》中介绍了我们业务特有的数据源定义:

业务1(master+slave+report)/业务2(master+slave+report)/业务3(master+slave+report)

在这里 master、slave、report 从 ​​DataSource​​ 的角度是不同的数据源,从 MySQL 的角度,master 就是主库,slave 和 report 都是从库;业务目前是根据订单号按照年份划分的。

在分库分表之前我们一般是这么使用:我们会为 master、slave、report 配置三套数据源配置(包括 ​​DataSource​​​、​​SqlSessionFactory​​​ 、​​Mapper​​​ 、​​TransactionManager​​​ 等),开发人员根据当前的业务场景一般查询走从库、更新和对数据实时性要求高的场景走主库、报表功能走 report 库)去选择使用对应的 ​​Mapper​​ 操作即可。

所以在分库分表的设计中,数据源由三部分构成:

business(业务)+model(数据库类型,固定 master/slave/report)+分表

物理上的数据源数量= business 数量 * model 数量(固定 3),但是逻辑上的数据源数量= business 数量 * model 数量(固定 3)* 分表数量。

实际开发的时候研发同学需要关心 business 和 model:

  1. 根据业务场景设置当前数据路由的 model
  2. 如果当前 SQL 带有路由标识(目前仅支持订单号),那么指定该字段为路由标识后框架底层会根据开发人员实现的规则自动匹配 business 和表路由
  3. 真正跟路由相关的是 business 和分表,model 是我们的业务读写分离场景

缺省数据源

这里再谈谈数据源的缺省值问题。

缺省分表

其实分表没有必要缺省,分表路由强依赖路由字段。

如果指定了路由字段,那么可以根据路由字段直接精准定位出 business 和分表;如果没有路由字段,是不支持绕开 business 直接指定分表的。因为物理上 business+model 才是一个数据源,物理上的表是要落在一个具体的库上的,绕开库直接指定表,这个就会显得很奇怪。

缺省 business

在上一篇文章《​​自己动手写一个分库分表中间件(一)​​》中谈到底层中间件主要要为业务服务,而且我们的业务大多数场景其实都是在操作特定的某个业务,所以这个业务就作为缺省数据源即可,由于缺省数据源这个配置很简单,多一个配置无伤大雅,所以中间件可以提供一个缺省的数据源配置项

缺省 model

在上文中提到我们的系统目前已经为 master、slave、report 提供了不同的 ​​Mapper​​, 所以分库分表环境中研发人员也需要指定 model。

最开始我设想的是在事务环境下缺省 master,非事务环境下 slave,但是这个设想很快就否决了,因为这里的“事务环境”其实指的是 Spring 的 ​​@Transactional​​​ 包裹的环境,如果历史代码中有单个的 ​​update​​​ 操作没有加 ​​@Transactional​​​(也没必要),此时“事务环境”的判断就会失效,就会造成 ​​update​​ 操作跑到 slave 库去了,这个是肯定不行的,所以缺省 model 是 master 没毛病。

还有一个点是,我们期望的肯定是常规业务读都到 slave,写都到 mater。历史代码啥都没加那不是会造成压力都到 master 库了嘛,接下来就谈谈在本次分库分表中间件中的数据分片代理层。

静态数据分片代理层

先解释一下分片代理层在项目开发中的分层位置,之前我们项目开发都是:

Controller->Service->Mapper

但是这次改造后会变成:

Controller->Service->MapperProxy->Mapper

所以中间件会提供一个配置 Proxy 层 package 的配置,当然也支持直接配置到 mapper 的 package,但是我们是建议增加一个 Proxy 层的,虽然 Proxy 层的存在可能会增加一点点的代码量。

这里再谈一下为什么会有一个 Proxy 层。

事务聚合

这个其实跟分库分表关系不大。平时我们经常会有一个更新操作要同时更新多个表的场景,此时同时更新多个表这个操作肯定要放在一个事务中,比如这样:

@Service
public UserService{
@Resource
UserMapper userMapper;

@Resource
AddressMapper addressMapper;

public int saveUser(User user){
userMapper.select(..)
addressMapper.select(..)
userMapper.insert(..);
addressMapper.insert(..);
}

}

这里两个 ​​insert​​​ 肯定要放在一个事务中,但是事务加在 ​​saveUser​​​ 方法上是不太合适的,这样会让事务粒度过大,此时两个 ​​insert​​ 操作就可以合并成一个事务方法放在 Proxy 层。

读写分离

在 Sharding-JDBC 的读写分离中设计的是写操作就是主库,读操作默认从库。这样设计对研发人员来说的确是更方便。但是也会造成研发人员过于忽视了“什么场景该读主,什么场景下该读从”,我们就曾出现过并发场景下由于查询的从库造成分布式锁失效,从而引发线上问题的场景。

当然上面这个观点属于仁者见仁。这里 Proxy 层还有一个作用是适配之前项目的使用习惯,之前我们系统读写分离是这么用的:

  1. master 一套 Mapper,slave 一套 Mapper,想用哪个就用对应的 Mapper 即可,缺陷就是数据源配置麻烦点(​​SqlSessionFactory​​​ 、​​TransactionManager​​ 都要配置多套),部分 master 和 slave Mapper 都要用的 SQL 要写两遍;
  2. 基于注解的方式,Mapper 和数据源配置都只有一套,对外提供两个方法,比如 ​​selectFromMaster​​​ 和 ​​selectFromSlave​​,其实两个方法内部都是调用的同一个 Mapper 的同一个方法,只是加的注解不一样;

这里的 Proxy 层也是这个作用。

定义数据路由

Proxy 层最主要的作用就是在这一层去指定数据路由。

在上一篇文章《​​自己动手写一个分库分表中间件(一)​​》中介绍过我们系统其实大部分操作都在“某个业务”中,我们更需要的是“写+从读+报表读”的数据应用分离的场景,此时我们更多的场景是手动指定数据路由(或者根据路由键),这里跟 Sharding-JDBC 中的数据分片是有略微差异的,也就是说我们可能手动的场景比较多(虽然有缺省机制,手动非必要)。

既然有手动的场景那肯定是希望所有对数据路由的指定都统一放在这一层来做(虽然中间件提供的大部分路由能力并没有严格限制只能在 Proxy 层生效),避免混用,或者指定路由粒度过大等场景。

抛开分库分表,中间件能力混用和粒度控制问题其实在平时开发中很常见,最经典的比如 Spring 中 ​​this​​​ 调用事务失效的问题,还有比如之前分享的《​​排查项目中读写分离失效原因​​》,就是因为读写分离注解和事务注解的粒度控制问题导致的。

最后

已经到《自己动手写一个分库分表中间件》系列的第二篇的结尾了,却还没有提到任何代码,还是在“空谈”设计,感觉有点小小的尴尬。其实我个人也非常讨厌 talk 一堆,but not any code,但的确这就是个人在 coding 中的一些感想和感悟。其实分库分表写起来真的不难,关键是怎么让使用者 0 学习成本,更容易理解,真正做到只用关注业务逻辑,这里面就需要一些相应的思考。

下一篇会开始展示代码,介绍分库分表中间件最主要(但不是最麻烦)的功能:数据路由的具体实现。

自己动手写一个分库分表中间件(二)数据源定义和分片代理层设计_数据源


举报

相关推荐

0 条评论