基于spring 切面(AOP)实现动态多数据源切换,基于 MyBatis 插件方式实现动态分表查询

基于spring 切面(AOP)实现动态多数据源切换;基于 MyBatis 插件方式实现动态分表查询。 来源于多个已上线项目实践,本项目有完整的测试示例。

mybatis-plugin-shard

  • 基于spring 切面(AOP)实现动态多数据源切换。
  • 基于 MyBatis 插件方式实现动态分表策略。
  • 来源于多个已上线项目实践。
  • 本项目有完整的测试示例。

以后会出详细的文档,敬请期待。

todo

  • 将分库分表配置与数据源配置统一放到文件 db-config.xml,并作为配置的切面的参数,在整个分库分表过程都可访问。
  • 完善分表逻辑,比起之前将分库分表配置在一个文件中更加优雅,也更加灵活,扩展性越好。
  • 完善文档

项目地址

配套 MBG 增强插件

查看 MBG 增强插件请移步:mybatis-generator

  • 用该 MBG 增强插件生成的 {xxx}Mapper.xml,会把表名用[`](不包括中括号)引起来,这样做的目的是分表时,动态给表名添加后缀后替换原始表名时不会“添乱”。
  • 注意 [`] 并非单引号,是在ESC 键下面、Q 键左上角的数字键 1 的左边那个键对应的“单引号”。
  • 比如有两张表:biz_trade、biz_trade_order,现在需要动态将 biz_trade 替换成 biz_trade_9,如果表名前后没有[`],则 biz_trade_order 也会被替换,替换后为:biz_trade_9_order,这显然不是我们希望发生的。

功能概述

  • 分库:简单的分库功能,更确切的讲,是多数据源管理,可根据业务动态切换,基于切面(AOP)。
  • 分表:对于同一数据源或不同数据源下的相同表结构的表,通过简单配置,实现分表查询功能。
    • 适用数据量增加迅速的业务场景。
    • 底层实现:基于 MyBatis 插件,拦截最终执行的 SQL 语句并且根据分表配置对 SQL 语句中的表名进行修改之后再执行。
      • 要求表名必须用 [`](不包括中括号)引起来。请使用增强插件(mybatis-generator)生成 Mapper 和 entity model。

动态切换数据源的三种方式

  • 通过参数 ShardRequest.java 指定:优先级最高,也最灵活。
    • 可以根据具体业务场景决定要连接哪个数据源。
  • 注解:可用在类和方法上,方法注解优先于类注解。
  • biz service 配置
    • 以上两种方式均没有的情况下,会读取 ShardConfig.shardSchemaInterfaceClassNameList 配置信息,在运行过程中,通过 AOP 拦截 biz.service,从而识别应该使用哪个数据源,达到分库/多数据源动态切换的目的。
    • 这种方式的优点:可以由专人统一管理,同时生产环境与开发、测试环境可以用不同的配置信息,开发人员与测试人员不用关注分库的细节。

如果以上三种方式都没有找到数据源,则使用默认的数据源。

分库分表思路

  • 分库思路:
    • 每个库有一个唯一的标志,起名叫 shardKeySchema,每个数据库的 shardKeySchema 与 db-source.xml 定义的数据源 dataSource -> targetDataSources -> map -> key 一一对应。
    • 用户在初始化时根据业务规则分配到某一个库,将该库的 shardKeySchema 保存到用户表。
  • 分表思路:
    • 每个用户分配一个用于分表的数字编号 shardKeyTableNumber,同样保存到用户表。
  • 用户表:
    • 集中在一个库用于统一登录验证,登录时获取用户 shardKeySchema 和 shardKeyTableNumber 并将用户登录信息缓存于 Session 或非关系型数据库,业界常用的如 redis、memcached。
  • 业务操作请求:
    • 在请求数据时,就可以根据 shardKeySchema 动态切换数据源,根据 shardKeyTableNumber 决定查哪张表了(分表操作通过 MyBatis 插件实现)。

分表分库场景

  • 场景一:
    • SaaS 平台,用户量成千上万,交易表 biz_trade 每天100万级增长,如果只用一个库的一张表,写入和读取压力会非常大,会成为瓶颈,所以需要分库分表。
    • 请求数据时,需要通过 ShardRequest.java 传 shardKeySchema 和 shardKeyTableNumber 参数。
    • 业务场景之:平均分配
      • 每个数据库实例最多分配 10 万用户,超过 10 万的用户,再分配到新库。
      • 交易记录平均分到 10 张表,这就意味着用于分表的 shardKeyTableNumber,一个数字编号最多同时分配给一万个用户。
      • 用户请求数据时,将用户的 shardKeyTableNumber 除以 10,将余数作为分表后缀,比如用户的 shardKeyTableNumber=8888,那么,8888%10=8,则用户的交易表是 biz_trade_8。
      • 同理,如果要平均分配到 100 张表,那么就除以 100 再取余作为分表后缀,8888%100=88,则用户的交易表是 biz_trade_88。
    • 业务场景之:区别对待
      • 在平均分配的基础上,由于运营需要,现在有 vip 客户,要保证 vip 客户的用户体验,vip 客户的数据库读写速度要快,那怎么办呢?
      • 其实只要针对这部分用户再制定一套规则就可以了,因为 shardKeySchema 和 shardKeyTableNumber 都是可以指定的。
      • 如果用户由一般用户变为了 vip 用户,那么在重新指定 shardKeySchema 和 shardKeyTableNumber 之后,用户原来的数据做相应的迁移即可。
  • 场景二:
    • 不同于场景一,在某一些业务场景,需要与其它业务系统做对接,在其它系统不能提供 api 的情况下,直接操作数据库无疑是最快也最直接的方式。
    • 这种情况,不同业务数据保存在不同的数据库,请求数据的时候,对于从哪个数据库请求数据是明确的,那么最直接的方式就是使用注解,或者配置 ShardConfig.shardSchemaInterfaceClassNameList。
    • 在不需要分表的情况下,用注解和配置 ShardConfig.shardSchemaInterfaceClassNameList 就够了,这种情况下请求数据时,不需要通过 ShardRequest(ShardRequest.java)传 shardKeySchema 和 shardKeyTableNumber 参数。
    • 当然,也可以不用注解也不用配置 ShardConfig.shardSchemaInterfaceClassNameList,还是通过 ShardRequest 传递参数也行,怎么灵活怎么来。
  • 场景三:
    • 分表是确定的,不是动态分配的,那么 ShardRequest.java 只传 shardKeyTable 即可。

运行

  • git clone https://github.com/uncleAndyChen/mybatis-plugin-shard.git
  • 因为依赖统一管理,添加了一个父模块:dependencies,只有一个 pom.xml 文件,需要先把这个 model 安装到本地仓库,否则会去 maven 配置的仓库下载。打开 cmd 窗口,在项目根目录下操作:
cd dependencies
mvn clean
mvn compile
mvn install
  • 强烈建议:maven 远程仓库添加阿里云镜像。
    • 修改 maven 根目录下 config/settings.xml,在 <mirrors> 下添加:
<mirror> 
    <id>alimaven</id> 
    <name>aliyun maven</name> 
    <url>https://maven.aliyun.com/repository/jcenter</url> 
    <mirrorOf>central</mirrorOf> 
</mirror>
  • 用你喜欢的 IDE 导入项目,如果你要我推荐一款 IDE,那么我强烈推荐 IntelliJ IDEA,官网:http://www.jetbrains.com/
  • IDE 安装 Lombok 插件。
  • MySQL 数据库,导入 docs/schemas.sql
  • 修改 biz/biz-config/src/main/resources/jdbc.properties 中连接数据库的参数
  • 启动
  • 访问:http://localhost:81,可以测试以三种不同方式切换数据源来查询数据。具体细节请看源代码,以后会出详细的文档,敬请期待。
    image

数据源配置(部分)

<bean id="dataSource" class="common.aspect.ChooseDataSource" primary="true">
    <property name="defaultTargetDataSource" ref="dataSourceSystem"/>
    <!-- 下面的各个 0key 需要配置到 shardTableConfigView 的 schemaKeyList -->
    <property name="targetDataSources">
        <map key-type="java.lang.String">
            <entry key="system" value-ref="dataSourceSystem"/>
            <entry key="student" value-ref="dataSourceStudent"/>
            <entry key="finance" value-ref="dataSourceFinance"/>
            <entry key="biz" value-ref="dataSourceBiz"/>
        </map>
    </property>
</bean>

配置分表分库配置类

<!-- 以下配置,部分表名只是用于配置示例,仅为了更好的展示如何配置。
    本项目没有用到的表名有:edu_class、biz_trade_order、biz_item、biz_item_sku
-->
<bean id="shardConfig" class="common.shard.ShardConfig" >
    <!-- 列表值为 dataSource.targetDataSources 的 keys  -->
    <property name="schemaKeyList">
        <list>
            <value>system</value>
            <value>student</value>
            <value>finance</value>
            <value>biz</value>
        </list>
    </property>
    <!-- 基于服务接口分库策略,
        把针对某个 schema 的接口配置在该数据源 key 对应的 list 下,没有就不配置
    -->
    <property name="shardSchemaInterfaceClassNameList">
        <map>
            <entry key="student">
                <list>
                    <value>biz.service.facade.IEduStudentService</value>
                </list>
            </entry>
        </map>
    </property>
    <!-- 分表策略
        直接将 ShardRequest.shardKeyTable(优先级高于后者) 或 ShardRequest.shardKeyTableNumber 作为分表后缀的表。
         ShardRequest 参见:https://github.com/uncleAndyChen/mybatis-plugin-shard/blob/master/common/common-shard/src/main/java/common/shard/ShardRequest.java
     -->
    <property name="shardTableDirectlyList">
        <list>
            <value>edu_student</value>
            <value>edu_class</value>
        </list>
    </property>
    <!-- 分表策略
        通过两个数相除取余作为后缀的表,配合 ShardRequest.shardKeyTableNumber 使用
        ShardRequest 参见:https://github.com/uncleAndyChen/mybatis-plugin-shard/blob/master/common/common-shard/src/main/java/common/shard/ShardRequest.java
    -->
    <!-- key 将作为 shardKeyTableNumber 的除数(取余), 余数作为分表后缀-->
    <!-- shardKeyTableNumber 通过 ShardRequest 传递,在请求 api 时传递 -->
    <property name="shardTableDivideList">
        <map>
            <entry key="10">
                <list>
                    <value>biz_trade</value>
                    <value>biz_trade_order</value>
                </list>
            </entry>
            <entry key="5">
                <list>
                    <value>biz_item</value>
                    <value>biz_item_sku</value>
                </list>
            </entry>
        </map>
    </property>
    <!-- 打印分表的 sql 语句,默认为 false 即不打印。-->
    <property name="printShardSqlInfo" value="true" />
    <!-- 不需要分表的 sql 语句列表,以下这句为 MyBatis 操作数据库新增记录时,查询新增的主键值的语句 -->
    <property name="notNeedShardSqlList">
        <list>
            <value>SELECT LAST_INSERT_ID()</value>
        </list>
    </property>
</bean>

切面配置

<!-- 用于切面,实现拦截数据库操作,实现分库分表的类 -->
<bean id="dataSourceAspect" class="common.aspect.DataSourceAspect">
    <property name="shardTableConfigView" ref="shardConfig" />
</bean>

<!-- 定义切面,用于拦截数据库操作,实现分库分表 -->
<aop:config proxy-target-class="true">
    <aop:aspect id="dataSourceAspect" ref="dataSourceAspect" order="1">
        <aop:pointcut id="point" expression="(execution(* biz.service.impl.*.*(..)))"/>
        <aop:before pointcut-ref="point" method="before"/>
        <aop:after pointcut-ref="point" method="afterHandler"/>
    </aop:aspect>
</aop:config>

请求参数 ShardRequest.java 类

public class ShardRequest {
    /**
     * 分库标志 key,是定义数据源时指定的 key,在执行数据库操作之前,通过该 key 动态切换数据源。
     * 如果只是分库,除了用到个属性,还可利用 ShardTableConfig.shardSchemaInterfaceNameList 实现。
     *      有关这两项配置的详细信息,请参见:https://github.com/uncleAndyChen/mybatis-plugin-shard/blob/master/biz/biz-config/src/main/resources/db-source.xml
     */
    private String shardKeySchema;

    /**
     * 分表标志 key,直接用作分表后缀的 key 值,针对直接添加后缀的表
     *      举例:应用该规则的原始表名为 table_name,则对应的分表为:table_name_key
     * 需要配合 ShardTableConfig 使用,与该类位于同一个目录,在 db-source.xml 中配置各属性值
     *     应用该规则的原始表名:ShardTableConfig.shardTableDirectlyList
     *          详细描述,请参见:https://github.com/uncleAndyChen/mybatis-plugin-shard/blob/master/biz/biz-config/src/main/resources/db-source.xml
     */
    private String shardKeyTable;

    /**
     * 动态分表参数编号,整形,一般与用户绑定,针对需要除一个数得到后缀的表
     * 需要配合 ShardTableConfig 使用,与该类位于同一个目录,在 db-source.xml 中配置各属性值
     *     应用该规则的原始表名:ShardTableConfig.shardTableDivideList
     *          详细描述,请参见:https://github.com/uncleAndyChen/mybatis-plugin-shard/blob/master/biz/biz-config/src/main/resources/db-source.xml
     *
     * 场景:SaaS 平台,每个用户分配一个编码值,可以按一定规则平均分配,比如现有有10万个用户,我们打算分10张表,那么,平均分配的话,就意味着每一万个用户有一个分表编号。
     * 极端地,对于 SasS 的超级 VIP 用户,可以分配一个唯一的分表编号,这就意味着这个 VIP 用户独享一套表。
     * 多个用户的数据可能存在于同一个数据库实例,也可能存在于多个数据库实例,可根据业务灵活分配。
     */
    private int shardKeyTableNumber;

    // getter and setter
    // ...
}

重新生成 mapper 和 entity

请参考 生成 Mapper 操作

有关 {xxx}Mapper.xml 文件

我是直接把 MBG 生成的 {xxx}Mapper.xml 文件放到了 biz-service-dal 模块下与 {xxx}Mapper.java 平级的目录下了,包名为:biz.mapper.xml.originalbiz.mapper.xml.extend

默认情况下,xml 文件不会被打包,所以,运行的时候会出现类似这样的错误:

Invalid bound statement (not found): biz.service.dal.mapper.original.EduStudentMapper.selectByExample

解决:需要在 pom.xml 里设置为需要将 xml 一起打包,如下:

<build>
    <resources>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
            <filtering>false</filtering>
        </resource>
    </resources>
</build>

directory 配置到 xml 的父目录 src/main/java/biz/mapper/xml 不会生效,配置成 src/main/java 就好。

技术清单

  • JDK 1.8,理论上支持 1.8 以上的版本,如需升级,比如要改为 JDK 11,将文件 ./dependencies/pom.xml<java.version>1.8</java.version> 改为 <java.version>11</java.version>
  • MySQL 5.6.46、MySQL 5.7,用这两个版本作的测试,理论上支持 5.6 及以上版本。
  • maven 依赖库
    • maven 依赖版本在 ./dependencies/pom.xml 维护,如果要升级某一框架的版本,只需要修改这个文件就行,模块 dependencies 被作为其它模块的 parent,目的就是统一管理版本,同样的依赖库只定义一次版本号。
    • 以下依赖为当前(2020-01-06)最新版本
      • Spring Boot 2.2.2.RELEASE
      • Spring Framework 5.2.2.RELEASE (common-shard 模块直接依赖了 spring framework 下的 spring-aspects)
      • MyBatis 3.5.3
      • druid 1.1.21
      • lombok 1.18.10
      • jackson 2.10.1

支持

如果有疑问或建议,欢迎请提 Issue
可能不会立即回复,尤其上班时间,不过我会尽量抽业余时间回复的。

如果帮到了你

请 Star 一下,让我有动力继续完善和优化。

关于作者

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 吴越永安之邑,有绿竹参天,苍翠巍然之山者,出通幽小道,隐清泉于其中。秋露微寒,余以偷闲为乐,驱而往之,亦欲托山水之...
    白色乌托邦阅读 291评论 0 3
  • 已经分手好多天了 昨晚是除夕之夜 凌晨看到你朋友圈 那是东京塔星光灿烂 你给它取名中国红 点赞后睡去 竟梦到你发朋...
    你是我心里最难忘的记逸阅读 315评论 0 1
  • 总要忙起来,才会过的充实, 不能虚度光阴,多做有意义的事!
    京心达查晓旭阅读 28评论 0 0
  • 初一 看到年老的奶奶完全看不见东西,鞋子跟小孩一样反着穿,忽然觉得心理一阵难受。 初二 妈妈从亲戚口中听说了春庆叔...
    吾不里阅读 241评论 0 0
  • 生活中很少见到绑架案,但是当老板说你要不完成任务就要被解雇,对手说你不出高价我就卖给别人了,孩子说你不给我玩手机我...
    皓熹阅读 362评论 0 1