MySQL分库分表

下面几个因素会影响数据库性能:

1.数据量:MySQL单库数据量在5000万以内性能比较好,超过阈值后性能会随着数据量的增大而变弱。MySQL单表数据量是500w-1000w之间性能比较好,超过1000w性能也会下降。

2.磁盘IO:因为单个服务的磁盘空间是有限制的,如果并发压力下,所有的请求都访问同一个节点,肯定会对磁盘IO造成非常大的影响。

3.数据库连接:数据库连接是非常稀少的资源,如果一个库里既有用户、商品、订单相关的数据,当海量用户同时操作时,数据库连接就很可能成为瓶颈。

为了提升性能,必须要解决上述几个问题,就有必要引进分库分表。

数据拆分(Sharding)是指在数据库或数据表层面进行的一种数据分割技术,主要用于解决随着数据量的增长而导致的数据库性能瓶颈问题。当单个数据库的表数据量过大或者访问请求过高时,通过数据拆分可以分散负载,提高系统的可扩展性和响应速度。

分库分表就是为了解决由于数据量过大而导致数据库性能降低的问题,将原来独立的数据库拆分成若干数据库组成,将数据大表拆分成若干数据表组成,使得单一数据库、单一数据表的数据量变小,从而达到提升数据库性能的目的。

数据拆分根据其拆分类型,可以分为两种方式:垂直(纵向)拆分和水平(横向)拆分。

当单个库太大时,先要看一下是因为表太多还是数据量太大,如果是表太多,则应该将部分表进行迁移(可以按业务区分),这就是所谓的垂直拆分。如果是数据量太大,则需要将表拆成更多的小表,来减少单表的数据量,这就是所谓的水平拆分。

垂直拆分有垂直分库和垂直分表两种方式,水平拆分有水平分库和水平分表两种方式。

垂直拆分的优缺点

优点:

  • 解决业务系统层面的耦合,业务清晰。
  • 与微服务的治理类似,也能对不同业务的数据进行分级管理、维护、监控、扩展等。
  • 高并发场景下,垂直切分一定程度的提升IO、数据库连接数、单机硬件资源的瓶颈。

缺点:

  • 部分表无法join,只能通过接口聚合方式解决,提升了开发的复杂度。
  • 单机的ACID被打破,需要引入分布式事务,而分布式事务处理复杂。
  • 依然存在单表数据量过大的问题(需要水平切分)。
  • 靠外键去进行约束的场景会受到影响。

水平拆分的优缺点

优点:

  • 不存在单库数据量过大、高并发的性能瓶颈,提升系统稳定性和负载能力。
  • 应用端改造较小,不需要拆分业务模块。

缺点:

  • ACID被打破,跨分片的事务一致性难以保证。
  • 跨库的join关联查询性能较差。
  • 数据多次扩展难度和维护量极大。
  • 靠外键去进行约束的场景会受到影响。
  • 依赖单库的自增ID会受到影响。

1、分库分表的方式

1、垂直分表

垂直分表是指将一张表按照字段分成多张表,每张表存储其中一部分字段。

垂直分表的结果是每个表的结构都不一样,每个表的数据也不一样,一般来说,每个表的字段至少有一列交集,一般是主键,用于关联数据,所有表的并集是全量数据。

使用场景:系统绝对并发量并没有上来,表的记录并不多,但是字段多,并且热点数据和非热点数据在一起,单行数据所需的存储空间较大。以至于数据库缓存的数据行减少,查询时会去读磁盘数据产生大量的随机读IO,产生IO瓶颈。

例如,原来的商品信息表中包含商品id、商品名称、商品价格、商品图片、所属店铺、规格、产地和商品描述等字段,可以将商品id、商品名称、商品价格、商品图片、所属店铺等常用字段放到一张基础表中,将商品id、商品规格、产地和商品描述等字段放到另一张扩展表中。当用户浏览商品时只查询基础表中的数据进行展示,点击具体某个商品查看详情时再从扩展表中查询数据。

通常按以下原则将数据表进行垂直拆分:

  1. 把不常用的字段单独放在一张表。
  2. 把text,blob等大字段拆分出来放在附表中。
  3. 经常组合查询的列放在一张表中。

2、水平分表

水平分表是指将一张表中的数据分到多张表中,每张表只存储一部分数据。

水平分表的结果是每个表的结构都一样,但是每个表的数据都不一样,没有交集,所有表的并集是全量数据。

使用场景:系统绝对并发量并没有上来,只是单表的数据量太多,影响了SQL效率,加重了CPU负担,以至于成为瓶颈。

3、垂直分库

如果数据库服务器的磁盘空间快要满了,垂直分表和水平分表的性能提升没有达到预期,同一个数据库中的多个数据表还是竞争同一个物理机的CPU、内存、网络IO、磁盘,此时可以考虑分库。

垂直分库是指以表为依据,按照业务归属不同,将不同的表拆分到不同的库中,每个库可以放在不同的服务器上。

垂直分库的结果是每个库的表结构都不一样,每个库的数据也不一样,没有交集,所有库的并集是全量数据。

使用场景:系统绝对并发量上来了,并且可以抽象出单独的业务模块。

数据库多了,服务器的磁盘IO和CPU的压力自然可以成倍缓解。

4、水平分库

经过垂直分库后,数据库性能问题得到一定程度的解决,但是随着业务量的增长,同一个业务数据库中的表数据越来越多,单台数据库服务器已经无法支撑,并且已无法再根据业务进行垂直分库,此时可以使用水平分库。

水平分库是把同一个表的数据按一定规则拆到不同的数据库中,每个库可以放在不同的服务器上。水平分库拆分得到的多个库是属于同一个业务的。

水平分库的结果是每个库的表结构都一样,但是每个库的数据都不一样,没有交集,所有库的并集是全量数据。

使用场景:系统绝对并发量上来了,分表难以根本上解决问题,并且还没有明显的业务归属来垂直分库。

垂直分库是把不同表拆分到不同数据库中,水平分库是把同一张表中的不同数据分到不同库中,不影响表结构。

一般来说,在系统设计阶段就应该根据业务耦合松紧来确定垂直分库和垂直分表方案,在数据量及访问压力不是特别大的情况,首先考虑缓存、读写分离、索引技术等方案。若数据量极大,且持续增长,再考虑水平分库和水平分表方案。

2、分库分表带来的问题

分库分表能有效的缓解单机和单库带来的性能瓶颈和压力,突破网络IO、硬件资源、连接数的瓶颈,同时也带来了一些问题。

1、事务一致性问题

由于分库分表把数据分布在不同库甚至不同服务器上,不可避免会带来分布式事务问题。

2、跨节点关联查询

在分库之前,可以通过一条SQL语句使用join关联查询多张表中的数据,但是分库之后,多张表不在一个数据库,甚至不在一台服务器,无法进行关联查询。

只能将原来的一次关联查询分为两次查询,从第一次查询的结果集中找出关联数据id,然后根据id发起第二次请求得到关联数据,最后将获得到的数据进行拼装。

3、跨节点分页、排序函数

跨节点多库进行查询时,limit分页、order by排序等问题,就变得比较复杂了。需要先在不同的分片节点中将数据进行排序并返回,然后将不同分片返回的结果集进行汇总和再次排序。如果是取第N页,需要将所有节点前N页数据都取出来合并,再进行整体的排序,操作效率会很低。所以请求页数越大,系统的性能也会越差。

在使用Max、Min、Sum、Count之类的函数进行计算的时候,与排序分页同理,也需要先在每个分片上执行相应的函数,然后将各个分片的结果集进行汇总和再次计算,最终将结果返回。

4、主键避重

在分库分表环境中,由于表中数据同时存在不同数据库中,主键值平时使用的自增长将无用武之地,某个分区数据库生成的ID无法保证全局唯一。因此需要单独设计全局主键,以避免跨库主键重复问题。

由于分库分表之后,数据被分散在不同的数据库、服务器上,对数据库的操作会带来一些问题,分库分表中间件可以帮助解决某个或某些问题,再结合其他手段解决剩余的问题,从而可以保证分库分表能够满足业务需求。

常用的分库分表中间件有:Sharding-JDBC、Sharding-Sphere(其前身是Sharding-JDBC,从 3.0 开始Sharding-JDBC被包含在 Sharding-Sphere中)、Taobao Distribute Data Layer(TDDL)和Mycat。

这些分库分表中间件并不是帮助分库分表的,而是帮助解决分库分表之后操作数据库时遇到的一些问题,需要开发人员提前手动完成分库分表。

3、Sharding-JDBC的基本概念和执行流程

官网:http://shardingsphere.apache.org/index_zh.html

下载地址:https://shardingsphere.apache.org/document/current/cn/downloads/

快速入门:https://shardingsphere.apache.org/document/current/cn/quick-start/shardingsphere-jdbc-quick-start/

Sharding-JDBC是当当网研发的开源分布式数据库中间件,从 3.0 开始Sharding-JDBC被包含在 Sharding-Sphere中,之后该项目进入进入Apache孵化器,4.0版本之后的版本为Apache版本。

ShardingSphere是一套开源的分布式数据库中间件解决方案组成的生态圈,它由Sharding-JDBC、ShardingProxy和Sharding-Sidecar(计划中)这3款相互独立的产品组成。 他们均提供标准化的数据分片、分布式事务和数据库治理功能,可适用于如Java同构、异构语言、容器、云原生等各种多样化的应用场景。

Sharding-JDBC使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。

Sharding-JDBC的核心功能为数据分片和读写分离,通过Sharding-JDBC,应用可以透明的使用jdbc访问已经分库分表、读写分离的多个数据源,而不用关心数据源的数量以及数据如何分布。

  • 适用于任何基于Java的ORM框架,如: Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
  • 基于任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
  • 支持任意实现JDBC规范的数据库,目前支持MySQL,Oracle,SQLServer和PostgreSQL。

使用Sharding-Jdbc前需要人工对数据库进行分库分表,在应用程序中加入Sharding-Jdbc的Jar包,应用程序通过Sharding-Jdbc操作分库分表后的数据库和数据表,由于Sharding-Jdbc是对Jdbc驱动的增强,使用Sharding-Jdbc就像使用Jdbc驱动一样,在应用程序中无需指定具体要操作的分库和分表。

1、基本概念

逻辑表:水平拆分的数据表的总称。

真实表:在分片的数据库中真实存在的物理表。

例如,将订单数据表t_order拆分成t_order_1和t_order_2 两张表,t_order是逻辑表,t_order_1和t_order_2是真实表。

在配置文件中配置水平分表的分片策略时,需要配置逻辑表与真实表的路由关系。

数据节点:数据分片的最小物理单元。由数据源名称和真实表组成,例如ds_0.t_order_1。

spring:
  shardingsphere:
    sharding:
      tables:
        # t_order 是逻辑表名
        t_order:
          # 数据节点:多数据源$->{0..N}.逻辑表名$->{0..N} 多个数据源的表相同
          actual-data-nodes: ds$->{0..2}.t_order$->{0..1}
          # 数据节点:多数据源$->{0..N}.逻辑表名$->{0..N} 多个数据源的表不同
          actual-data-nodes: ds0.t_order$->{0..1},ds1.t_order$->{2..4}
          # 指定单数据源的配置方式
          actual-data-nodes: ds0.t_order$->{0..4}
          # 全部手动指定
          actual-data-nodes: ds0.t_order0,ds1.t_order0,ds0.t_order1,ds1.t_order1

分片键:用于分片的数据库字段,是将数据库(表)水平拆分的关键字段。

例如,将订单表根据订单主键对2取模分片分成两张表,则订单主键为分片字段。 SQL中如果无分片字段,将执行全路由,性能较差。 除了对单分片字段的支持,Sharding-Jdbc也支持根据多个字段进行分片。

分片算法:通过分片算法将数据分片,支持通过 = 、 BETWEEN 和 IN 分片。分片算法需要应用程序的开发人员自行实现,可实现的灵活度非常高。包括:精确分片算法、范围分片算法、复合分片算法等。

例如:

where order_id = ? 将采用精确分片算法,

where order_id in (?,?,?)将采用精确分片算法,

where order_id BETWEEN ? and ? 将采用范围分片算法,

复合分片算法用于分片键有多个复杂的情况。

分片策略:包含分片键和分片算法,由于分片算法的独立性,将其独立抽离。真正可用于分片操作的是分片键 + 分片算法,也就是分片策略。内置的分片策略大致可分为尾数取模、哈希、范围、标签、时间等。

由用户方配置的分片策略则更加灵活,常用的分片策略是使用行表达式配置分片策略,它采用Groovy表达式表示,如: t_user_$->{u_id % 8} 表示t_user表根据u_id模8,而分成8张表,表名称为 t_user_0 到 t_user_7 。

绑定表:指分片规则一致的主表和子表。

例如将一张字段较多的订单信息表t_order_info拆分成订单表t_order 和订单详情表t_order_item,拆分后的两张表均按照 order_id 分片,这两张表的分区键完全相同,都是order_id,则此两张表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。举例说明,如果逻辑SQL为:

SELECT i.* FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE o.order_id in (10,11);

在不配置绑定表关系时,假设分片键 order_id 将数值10保存到分表后的t_order_1表,将数值11保存到分表后的t_order_2表,那么路由后的SQL应该为4条,它们呈现为笛卡尔积:

SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in(10, 11);
SELECT i.* FROM t_order_1 o JOIN t_order_item_2 i ON o.order_id=i.order_id WHERE o.order_id in(10, 11);
SELECT i.* FROM t_order_2 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in(10, 11);
SELECT i.* FROM t_order_2 o JOIN t_order_item_2 i ON o.order_id=i.order_id WHERE o.order_id in(10, 11);

在配置绑定表关系后,路由的SQL应该为2条:

SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in(10, 11);
SELECT i.* FROM t_order_2 o JOIN t_order_item_2 i ON o.order_id=i.order_id WHERE o.order_id in(10, 11);

广播表:指所有的分片数据源中都存在的表,表结构和表中的数据在每个数据库中均完全一致。适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表。

2、执行流程

当Sharding-JDBC接受到一条SQL语句时,由于SQL语句中写的是逻辑表名,Sharding-JDBC需要改写SQL语句,路由到拆分后的数据库和真实表,所以会依次执行 SQL解析 => 查询优化 => SQL路由 => SQL改写 => SQL执行 =>结果归并 ,最终返回执行结果。

1、SQL解析

SQL解析过程分为词法解析和语法解析。 词法解析器用于将SQL拆解为不可再分的原子符号,称为Token。并根据不同数据库方言所提供的字典,将其归类为关键字、表达式、字面量和操作符,再使用语法解析器将SQL转换为抽象语法树。

例如,以下SQL:

SELECT id, name FROM t_user WHERE status = 'ACTIVE' AND age > 18

解析之后的抽象语法树如下图所示:

为了便于理解,抽象语法树中的关键字的Token用绿色表示,变量的Token用红色表示,灰色表示需要进一步拆分。

最后,通过对抽象语法树的遍历去提炼分片所需的上下文,并标记有可能需要SQL改写(后边介绍)的位置。 供分片使用的解析上下文包含查询选择项(Select Items)、表信息(Table)、分片条件(Sharding Condition)、自增主键信息(Auto increment Primary Key)、排序信息(Order By)、分组信息(Group By)以及分页信息(Limit、Rownum、Top)。

2、SQL路由

SQL路由就是把针对逻辑表的数据操作映射到对数据结点操作的过程。

根据解析上下文匹配数据库和表的分片策略,并生成路由路径。对于携带分片键的SQL,根据分片键操作符的不同可以划分为单片路由(分片键的操作符是等号)、多片路由(分片键的操作符是IN)和范围路由(分片键的操作符是BETWEEN),不携带分片键的SQL则采用广播路由。根据分片键进行路由的场景可分为直接路由、标准路由、笛卡尔路由等。

标准路由

标准路由是Sharding-Jdbc最为推荐使用的分片方式,它的适用范围是不包含关联查询或仅包含绑定表之间关联查询的SQL。 当分片运算符是等号时,路由结果将落入单库(表),当分片运算符是BETWEEN或IN时,则路由结果不一定落入唯一的库(表),因此一条逻辑SQL最终可能被拆分为多条用于执行的真实SQL。举例说明,如果按照 order_id 的奇数和偶数进行数据分片,一个单表查询的逻辑SQL如下:

SELECT * FROM t_order WHERE order_id IN (1, 2);

那么路由的结果应为:

SELECT * FROM t_order_0 WHERE order_id IN (1, 2);
SELECT * FROM t_order_1 WHERE order_id IN (1, 2);

绑定表的关联查询与单表查询复杂度和性能相当。

笛卡尔路由

笛卡尔路由是最复杂的情况,它无法根据绑定表的关系定位分片规则,因此非绑定表之间的关联查询需要拆解为笛卡尔积组合执行。 例如,如果下面SQL并未配置绑定表关系,

SELECT * FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE order_id IN (1, 2);

那么路由的结果应为:

SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE order_id IN (1,2);
SELECT * FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE order_id IN (1,2);
SELECT * FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE order_id IN (1,2);
SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE order_id IN (1,2);

全库表路由

对于不携带分片键的SQL,则采取广播路由的方式。根据SQL类型又可以划分为全库表路由、全库路由、全实例路由、单播路由和阻断路由这5种类型。其中全库表路由用于处理对数据库中与其逻辑表相关的所有真实表的操作,主要包括不带分片键的DQL(数据查询)和DML(数据管理),以及DDL(数据定义)等。例如:

SELECT * FROM t_order WHERE good_prority IN (1, 10);

则会遍历所有数据库中的所有表,逐一匹配逻辑表和真实表名,能够匹配得上则执行。路由后成为:

SELECT * FROM t_order_0 WHERE good_prority IN (1, 10);
SELECT * FROM t_order_1 WHERE good_prority IN (1, 10);
SELECT * FROM t_order_2 WHERE good_prority IN (1, 10);
SELECT * FROM t_order_3 WHERE good_prority IN (1, 10);

3、SQL改写

开发人员面向逻辑表编写的SQL,并不能够直接在真实的数据库中执行,SQL改写用于将逻辑SQL改写为在真实数据库中可以正确执行的SQL。

如一个简单的例子,若逻辑SQL为:

SELECT order_id FROM t_order WHERE order_id=1;

假设该SQL配置分片键order_id,并且order_id=1的情况,将路由至分片表1。那么改写之后的SQL应该为:

SELECT order_id FROM t_order_1 WHERE order_id=1;

再比如,Sharding-JDBC需要在结果归并时获取相应数据,但该数据并未能通过查询的SQL返回,这种情况主要是针对GROUP BY和ORDER BY。结果归并时,需要根据 GROUP BY 和 ORDER BY 的字段项进行分组和排序,但如果原始SQL的选择项中若并未包含分组项或排序项,则需要对原始SQL进行改写。

先看一下原始SQL中带有结果归并所需信息的场景:

SELECT order_id, user_id FROM t_order ORDER BY user_id;

由于使用user_id进行排序,在结果归并中需要能够获取到user_id的数据,而上面的SQL是能够获取到user_id数据的,因此无需补列。

如果选择项中不包含结果归并时所需的列,则需要进行补列,如以下SQL:

SELECT order_id, user_id AS uid FROM t_order ORDER BY user_id;

4、SQL执行

Sharding-JDBC采用一套自动化的执行引擎,负责将路由和改写完成之后的真实SQL安全且高效发送到底层数据源执行。它不是简单地将SQL通过JDBC直接发送至数据源执行,也并非直接将执行请求放入线程池去并发执行。它更关注平衡数据源连接创建以及内存占用所产生的消耗,以及最大限度地合理利用并发等问题。 执行引擎的目标是自动化的平衡资源控制与执行效率,它能在以下两种模式自适应切换。

1. 内存限制模式

使用此模式的前提是,Sharding-JDBC对一次操作所耗费的数据库连接数量不做限制。如果实际执行的SQL需要对某数据库实例中的200张表做操作,则对每张表创建一个新的数据库连接,并通过多线程的方式并发处理,以达成执行效率最大化。

2. 连接限制模式

使用此模式的前提是,Sharding-JDBC严格控制对一次操作所耗费的数据库连接数量。如果实际执行的SQL需要对某数据库实例中的200张表做操作,那么只会创建唯一的数据库连接,并对其200张表串行处理。 如果一次操作中的分片散落在不同的数据库,仍然采用多线程处理对不同库的操作,但每个库的每次操作仍然只创建一个唯一的数据库连接。

内存限制模式适用于OLAP(连接数据分析)操作,可以通过放宽对数据库连接的限制提升系统吞吐量。连接限制模式适用于OLTP(连接事务处理)操作,OLTP通常带有分片键,会路由到单一的分片,因此严格控制数据库连接,以保证在线系统数据库资源能够被更多的应用所使用,是明智的选择。

5、结果归并

将从各个数据节点获取的多数据结果集,组合成为一个结果集并正确的返回至请求客户端,称为结果归并。

Sharding-JDBC支持的结果归并从功能上可分为遍历、排序、分组、分页和聚合5种类型,它们是组合而非互斥的关系。

归并引擎的整体结构划分如下图。

结果归并从结构划分可分为流式归并、内存归并和装饰者归并。流式归并和内存归并是互斥的,装饰者归并可以在流式归并和内存归并之上做进一步的处理。

  • 内存归并是将所有分片结果集的数据都遍历并存储在内存中,再通过统一的分组、排序以及聚合等计算之后,再将其封装成为逐条访问的数据结果集返回。
  • 流式归并是指每一次从数据库结果集中获取到的数据,都能够通过游标逐条获取的方式返回正确的单条数据,它与数据库原生的返回结果集的方式最为契合。

下面举例说明流式归并的过程,如下图是一个通过分数进行排序的示例图,它采用流式归并方式。图中展示了3张表返回的数据结果集,每个数据结果集已经根据分数排序完毕,但是3个数据结果集之间是无序的。将3个数据结果集的当前游标指向的数据值进行排序,并放入优先级队列,t_score_0的第一个数据值最大,t_score_2的第一个数据值次之,t_score_1的第一个数据值最小,因此优先级队列根据t_score_0,t_score_2和t_score_1的方式排序队列。

下图则展现了进行next调用的时候,排序归并是如何进行的。 通过图中可以看到,当进行第一次next调用时,排在队列首位的t_score_0将会被弹出队列,并且将当前游标指向的数据值(也就是100)返回至查询客户端,并且将游标下移一位之后,重新放入优先级队列。 而优先级队列也会根据t_score_0的当前数据结果集指向游标的数据值(这里是90)进行排序,根据当前数值,t_score_0排列在队列的最后一位。之前队列中排名第二的t_score_2的数据结果集则自动排在了队列首位。

在进行第二次next时,只需要将目前排列在队列首位的t_score_2弹出队列,并且将其数据结果集游标指向的值返回至客户端,并下移游标,继续加入队列排队,以此类推。 当一个结果集中已经没有数据了,则无需再次加入队列。

可以看到,对于每个数据结果集中的数据有序,而多数据结果集整体无序的情况下,Sharding-JDBC无需将所有的数据都加载至内存即可排序。 它使用的是流式归并的方式,每次next仅获取唯一正确的一条数据,极大的节省了内存的消耗。

装饰者归并是对所有的结果集归并进行统一的功能增强,比如归并时需要聚合SUM,在进行聚合计算前,都会通过内存归并或流式归并查询出结果集。因此,聚合归并是在内存归并或流式归并之上追加的归并能力,即装饰者模式。

4、Sharding-JDBC简单使用

分库分表配置大致流程:

  1. 配置数据源。
  2. 若需要水平分库,则配置分库策略。
  3. 必须配置数据节点,如spring.shardingsphere.sharding.tables.t_order.actual-data-nodes = m1.t_order_$->{1..2}。
  4. 为了避免跨库主键重复,需要配置主键生成策略,如SNOWFLAKE雪花算法生成主键。
  5. 配置每个表的分片策略。

1、水平分表

提前创建两张表t_order_1和t_order_2,这两张表是订单表拆分后的表,通过Sharding-Jdbc向订单表插入数据,按照一定的分片规则,主键id为偶数的数据保存到t_order_1,主键id为奇数的数据保存到t_order_2,通过Sharding-Jdbc 查询数据,根据SQL语句的内容从t_order_1或t_order_2查询数据。

1、创建数据库和数据表

创建数据库order_db:

CREATE DATABASE `order_db` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';

创建数据表t_order_1和t_order_2:

DROP TABLE IF EXISTS `t_order_1`;
CREATE TABLE `t_order_1` (
`order_id` bigint(20) NOT NULL COMMENT '订单id',
`price` decimal(10, 2) NOT NULL COMMENT '订单价格',
`user_id` bigint(20) NOT NULL COMMENT '下单用户id',
`status` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '订单状态',
PRIMARY KEY (`order_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;


DROP TABLE IF EXISTS `t_order_2`;
CREATE TABLE `t_order_2` (
`order_id` bigint(20) NOT NULL COMMENT '订单id',
`price` decimal(10, 2) NOT NULL COMMENT '订单价格',
`user_id` bigint(20) NOT NULL COMMENT '下单用户id',
`status` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '订单状态',
PRIMARY KEY (`order_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

2、引入Maven依赖

引入 sharding-jdbc和SpringBoot整合的Jar包:

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding‐jdbc‐spring‐boot‐starter</artifactId>
    <version>4.0.0‐RC1</version>
</dependency>

3、编写配置文件application.properties

配置内容包括:数据源、主键生成策略、分片策略等,分片策略是sharding-jdbc进行分库分表操作的重要依据。

server.port=56081

spring.application.name = sharding-jdbc-simple-demo

server.servlet.context-path = /sharding-jdbc-simple-demo
spring.http.encoding.enabled = true
spring.http.encoding.charset = UTF-8
spring.http.encoding.force = true

spring.main.allow-bean-definition-overriding = true

mybatis.configuration.map-underscore-to-camel-case = true

# 打开sql输出日志
spring.shardingsphere.props.sql.show = true

swagger.enable = true

logging.level.root = info
logging.level.org.springframework.web = info
logging.level.com.dbsharding  = debug
logging.level.druid.sql = debug

#sharding-jdbc分片规则配置
#数据源
spring.shardingsphere.datasource.names = m1

spring.shardingsphere.datasource.m1.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m1.url = jdbc:mysql://localhost:3306/test?useUnicode=true
spring.shardingsphere.datasource.m1.username = root
spring.shardingsphere.datasource.m1.password = 123456

# 指定t_order表的数据分布情况,配置数据节点 m1.t_order_1、m1.t_order_2
# 这里...tables.t_order指在ORM中操作的表名t_order(逻辑表,不是数据库中的真实表名)
# m1.t_order_$->{1..2}表示$可以替换为1到2
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes = m1.t_order_$->{1..2}

# 指定t_order表的主键(order_id)生成策略为SNOWFLAKE
spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id
# 雪花算法主键
spring.shardingsphere.sharding.tables.t_order.key-generator.type=SNOWFLAKE

# 指定t_order表的分片策略,分片策略包括分片键和分片算法
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column = order_id
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression = t_order_$->{order_id % 2 + 1}

或者application.yaml配置文件:

server:
  port: 56081
  servlet:
    context-path: /sharding-jdbc-simple-demo
spring:
  application:
    name: sharding-jdbc-simple-demo
  http:
    encoding:
      enabled: true
      charset: utf-8
      force: true
  main:
    allow-bean-definition-overriding: true
  shardingsphere:
    datasource:
      names: m1
      m1:
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/test?useUnicode=true
        username: root
        password: 123456
    sharding:
      tables:
        t_order:
          actualDataNodes: m1.t_order_$->{1..2}
          tableStrategy:
            inline:
              shardingColumn: order_id
              algorithmExpression: t_order_$->{order_id % 2 + 1}
          keyGenerator:
            type: SNOWFLAKE
            column: order_id
    props:
      sql:
        show: true
mybatis:
  configuration:
    map-underscore-to-camel-case: true
swagger:
  enable: true
logging:
  level:
    root: info
    org.springframework.web: info
    com.itheima.dbsharding: debug
druid.sql: debug

sharding-jdbc分片规则配置:

  1. 首先定义数据源m1,并对m1进行实际的参数配置。
  2. 指定t_order逻辑表的数据分布情况,数据实际分布在m1.t_order_1,m1.t_order_2两张物理表中。
  3. 指定t_order表的主键生成策略为SNOWFLAKE,SNOWFLAKE是一种分布式自增算法,保证id全局唯一
  4. 定义t_order分片策略,order_id为偶数的数据保存在t_order_1,为奇数的保存在t_order_2,分表策略的表达式为t_order_$->{order_id % 2 + 1}

4、编写Mapper接口操作数据

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;

@Mapper
@Component
public interface OrderDao {

    /**
     * 插入订单
     * @param price
     * @param userId
     * @param status
     * @return
     */
    @Insert("insert into t_order(price,user_id,status)values(#{price},#{userId},#{status})")
    int insertOrder(@Param("price")BigDecimal price,@Param("userId")Long userId,@Param("status")String status);

    /**
     * 根据id列表查询订单
     * 在MyBatis中,<script>标签用于构建复杂的、条件性的SQL语句。
     * @param orderIds
     * @return
     */
    @Select("<script>" +
            "select" +
            " * " +
            " from t_order t " +
            " where t.order_id in " +
            " <foreach collection='orderIds' open='(' separator=',' close=')' item='id'>" +
            " #{id} " +
            " </foreach>" +
            "</script>")
    List<Map> selectOrderbyIds(@Param("orderIds") List<Long> orderIds);
}

5、编写单元测试

import com.dbsharding.simple.dao.OrderDao;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {ShardingJdbcSimpleBootstrap.class})
public class OrderDaoTest {

    @Autowired
    OrderDao orderDao;

    @Test
    public void testInsertOrder(){
        for(int i=0;i<20;i++){
            orderDao.insertOrder(new BigDecimal(i),1L,"SUCCESS");
        }
}
    @Test
    public void testSelectOrderbyIds(){
        List<Long> ids = new ArrayList<>();
        ids.add(373897739357913088L);
        ids.add(373897037306920961L);
        List<Map> maps = orderDao.selectOrderbyIds(ids);
        System.out.println(maps);
    }
}

执行单元测试的插入操作之后,可以看到如下日志:

2022-11-12 19:59:34.403  INFO 19472 --- [           main] ShardingSphere-SQL                       : Rule Type: sharding
2022-11-12 19:59:34.403  INFO 19472 --- [           main] ShardingSphere-SQL                       : Logic SQL: insert into t_order(price,user_id,status)values(?,?,?)
2022-11-12 19:59:34.403  INFO 19472 --- [           main] ShardingSphere-SQL                       : SQLStatement: InsertStatement(super=DMLStatement(super=AbstractSQLStatement(type=DML, tables=Tables(tables=[Table(name=t_order, alias=Optional.absent())]), routeConditions=Conditions(orCondition=OrCondition(andConditions=[AndCondition(conditions=[])])), encryptConditions=Conditions(orCondition=OrCondition(andConditions=[])), sqlTokens=[TableToken(tableName=t_order, quoteCharacter=NONE, schemaNameLength=0), SQLToken(startIndex=19)], parametersIndex=3, logicSQL=insert into t_order(price,user_id,status)values(?,?,?)), deleteStatement=false, updateTableAlias={}, updateColumnValues={}, whereStartIndex=0, whereStopIndex=0, whereParameterStartIndex=0, whereParameterEndIndex=0), columnNames=[price, user_id, status], values=[InsertValue(columnValues=[org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@60c1663c, org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@f5cf29b, org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@3c66b7d8])])
2022-11-12 19:59:34.403  INFO 19472 --- [           main] ShardingSphere-SQL                       : Actual SQL: m1 ::: insert into t_order_1 (price, user_id, status, order_id) VALUES (?, ?, ?, ?) ::: [18, 1, SUCCESS, 1037637147802206208]
2022-11-12 19:59:34.406 DEBUG 19472 --- [           main] c.i.d.simple.dao.OrderDao.insertOrder    : <==    Updates: 1
2022-11-12 19:59:34.407 DEBUG 19472 --- [           main] c.i.d.simple.dao.OrderDao.insertOrder    : ==>  Preparing: insert into t_order(price,user_id,status)values(?,?,?) 
2022-11-12 19:59:34.407 DEBUG 19472 --- [           main] c.i.d.simple.dao.OrderDao.insertOrder    : ==> Parameters: 19(BigDecimal), 1(Long), SUCCESS(String)
2022-11-12 19:59:34.408  INFO 19472 --- [           main] ShardingSphere-SQL                       : Rule Type: sharding
2022-11-12 19:59:34.408  INFO 19472 --- [           main] ShardingSphere-SQL                       : Logic SQL: insert into t_order(price,user_id,status)values(?,?,?)
2022-11-12 19:59:34.408  INFO 19472 --- [           main] ShardingSphere-SQL                       : SQLStatement: InsertStatement(super=DMLStatement(super=AbstractSQLStatement(type=DML, tables=Tables(tables=[Table(name=t_order, alias=Optional.absent())]), routeConditions=Conditions(orCondition=OrCondition(andConditions=[AndCondition(conditions=[])])), encryptConditions=Conditions(orCondition=OrCondition(andConditions=[])), sqlTokens=[TableToken(tableName=t_order, quoteCharacter=NONE, schemaNameLength=0), SQLToken(startIndex=19)], parametersIndex=3, logicSQL=insert into t_order(price,user_id,status)values(?,?,?)), deleteStatement=false, updateTableAlias={}, updateColumnValues={}, whereStartIndex=0, whereStopIndex=0, whereParameterStartIndex=0, whereParameterEndIndex=0), columnNames=[price, user_id, status], values=[InsertValue(columnValues=[org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@60c1663c, org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@f5cf29b, org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@3c66b7d8])])
2022-11-12 19:59:34.408  INFO 19472 --- [           main] ShardingSphere-SQL                       : Actual SQL: m1 ::: insert into t_order_2 (price, user_id, status, order_id) VALUES (?, ?, ?, ?) ::: [19, 1, SUCCESS, 1037637147823177729]
2022-11-12 19:59:34.412 DEBUG 19472 --- [           main] c.i.d.simple.dao.OrderDao.insertOrder    : <==    Updates: 1

通过日志可以发现order_id为奇数的被插入到t_order_2表,为偶数的被插入到t_order_1表,达到预期效果。

执行单元测试的查询操作之后,可以看到如下日志:

2022-11-13 21:49:22.886  INFO 30544 --- [           main] ShardingSphere-SQL                       : Rule Type: sharding
2022-11-13 21:49:22.887  INFO 30544 --- [           main] ShardingSphere-SQL                       : Logic SQL: select *  from t_order t  where t.order_id in   (   ?   ,  ?   )
2022-11-13 21:49:22.887  INFO 30544 --- [           main] ShardingSphere-SQL                       : SQLStatement: SelectStatement(super=DQLStatement(super=AbstractSQLStatement(type=DQL, tables=Tables(tables=[Table(name=t_order, alias=Optional.of(t))]), routeConditions=Conditions(orCondition=OrCondition(andConditions=[AndCondition(conditions=[Condition(column=Column(name=order_id, tableName=t_order), operator=IN, compareOperator=null, positionValueMap={}, positionIndexMap={0=0, 1=1, 2=2, 3=3})])])), encryptConditions=Conditions(orCondition=OrCondition(andConditions=[])), sqlTokens=[TableToken(tableName=t_order, quoteCharacter=NONE, schemaNameLength=0)], parametersIndex=4, logicSQL=select *  from t_order t  where t.order_id in   (   ?   ,  ?  ))), containStar=true, firstSelectItemStartIndex=7, selectListStopIndex=7, groupByLastIndex=0, items=[StarSelectItem(owner=Optional.absent())], groupByItems=[], orderByItems=[], limit=null, subqueryStatement=null, subqueryStatements=[], subqueryConditions=[])
2022-11-13 21:49:22.888  INFO 30544 --- [           main] ShardingSphere-SQL                       : Actual SQL: m1 ::: select *  from t_order_1 t  where t.order_id in   (  ? ,  ?  ) ::: [373897739357913088, 373897037306920961]
2022-11-13 21:49:22.888  INFO 30544 --- [           main] ShardingSphere-SQL                       : Actual SQL: m1 ::: select *  from t_order_2 t  where t.order_id in   (  ? ,  ?  ) ::: [373897739357913088, 373897037306920961]
2022-11-13 21:49:22.937 DEBUG 30544 --- [           main] c.i.d.s.dao.OrderDao.selectOrderbyIds    : <==      Total: 4

通过日志可以发现,根据传入order_id的奇偶不同,sharding-jdbc分别去不同的表检索数据,达到预期效果。

通过日志分析,Sharding-JDBC在拿到用户要执行的SQL之后执行的流程如下:

  1. 解析SQL,获取片键值,在本例中是order_id。
  2. Sharding-JDBC通过规则配置 t_order_$->{order_id % 2 + 1},知道了当order_id为偶数时,应该往t_order_1表插数据,为奇数时,应该往t_order_2表插数据。查询也是如此。
  3. 于是Sharding-JDBC根据order_id的值改写SQL语句,改写后的SQL语句是真实要执行的SQL语句。
  4. 执行改写后的真实SQL语句。
  5. 若是查询操作,将所有真实执行SQL的结果进行汇总合并,然后返回。

2、水平分库

水平分库是把同一个表的数据按一定规则拆到不同的数据库中,每个库可以放在不同的服务器上。

提前创建两个数据库order_db_1和order_db_2,将原有的order_db库拆分为order_db_1、order_db_2,并在这两个数据库中分别创建t_order_1和t_order_2两张表。

1、创建数据库和数据表

创建数据库order_db_1和order_db_2:

CREATE DATABASE `order_db_1` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
CREATE DATABASE `order_db_2` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';

创建数据表t_order_1和t_order_2:

DROP TABLE IF EXISTS `t_order_1`;
CREATE TABLE `t_order_1` (
`order_id` bigint(20) NOT NULL COMMENT '订单id',
`price` decimal(10, 2) NOT NULL COMMENT '订单价格',
`user_id` bigint(20) NOT NULL COMMENT '下单用户id',
`status` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '订单状态',
PRIMARY KEY (`order_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;


DROP TABLE IF EXISTS `t_order_2`;
CREATE TABLE `t_order_2` (
`order_id` bigint(20) NOT NULL COMMENT '订单id',
`price` decimal(10, 2) NOT NULL COMMENT '订单价格',
`user_id` bigint(20) NOT NULL COMMENT '下单用户id',
`status` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '订单状态',
PRIMARY KEY (`order_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

2、引入Maven依赖

引入 sharding-jdbc和SpringBoot整合的Jar包:

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding‐jdbc‐spring‐boot‐starter</artifactId>
    <version>4.0.0‐RC1</version>
</dependency>

3、编写配置文件application.properties

配置内容包括:数据源、主键生成策略、分片策略(分库策略和分表策略)等,分片策略是sharding-jdbc进行分库分表操作的重要依据。

server.port=8889

spring.application.name = sharding-jdbc-simple-demo

server.servlet.context-path = /sharding-jdbc-simple-demo
spring.http.encoding.enabled = true
spring.http.encoding.charset = UTF-8
spring.http.encoding.force = true

spring.main.allow-bean-definition-overriding = true

mybatis.configuration.map-underscore-to-camel-case = true

# sharding-jdbc分片规则配置 
# 定义多个数据源
spring.shardingsphere.datasource.names = m1,m2

spring.shardingsphere.datasource.m1.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m1.url=jdbc:mysql://localhost:3308/order_db_1?useUnicode=true
spring.shardingsphere.datasource.m1.username = root
spring.shardingsphere.datasource.m1.password = 123456

spring.shardingsphere.datasource.m2.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m2.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m2.url=jdbc:mysql://localhost:3308/order_db_2?useUnicode=true
spring.shardingsphere.datasource.m2.username = root
spring.shardingsphere.datasource.m2.password = 123456

# 分库策略:以user_id为分片键,分片策略为user_id % 2 + 1,user_id为偶数操作m1数据源,否则操作m2。
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.sharding-column = user_id
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.algorithm-expression = m$->{user_id % 2 + 1}

# 指定t_order表的数据分布情况,配置数据节点 m1.t_order_1、m1.t_order_2、m2.t_order_1、m2.t_order_2
# 如果这里配置如m1.t_order_$->{1..2},则查询时只会查询m1库的表
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes = m$->{1..2}.t_order_$->{1..2}

# 指定t_order表的主键生成策略为SNOWFLAKE
spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id
spring.shardingsphere.sharding.tables.t_order.key-generator.type=SNOWFLAKE

# 分表策略:指定t_order表的分片策略,分片策略包括分片键和分片算法
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column = order_id
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression = t_order_$->{order_id % 2 + 1}

# 打开sql输出日志
spring.shardingsphere.props.sql.show = true

swagger.enable = true

logging.level.root = info
logging.level.org.springframework.web = info
logging.level.com.itheima.dbsharding  = debug
logging.level.druid.sql = debug

#分库策略,如何将一个逻辑表映射到多个数据源

spring.shardingsphere.sharding.tables.<逻辑表名称>.database-strategy.<分片策略>.<分片策略属性名> = #分片策略属性值

#分表策略,如何将一个逻辑表映射为多个物理表

spring.shardingsphere.sharding.tables.<逻辑表名称>.table-strategy.<分片策略>.<分片策略属性名> = #分片策略属性值

Sharding-JDBC支持以下几种分片策略,不管是分库还是分表,分片策略基本一样。

1. standard

standard:标准分片策略,对应StandardShardingStrategy。

提供对SQL语句中的=, IN和BETWEEN AND的分片操作支持。

StandardShardingStrategy只支持单分片键,提供PreciseShardingAlgorithm和RangeShardingAlgorithm两个分片算法。

PreciseShardingAlgorithm是必选的,用于处理=和IN的分片。

RangeShardingAlgorithm是可选的,用于处理BETWEEN AND分片,如果不配置RangeShardingAlgorithm,SQL中的BETWEEN AND将按照全库路由处理。

2. complex

complex:复合分片策略,对应ComplexShardingStrategy。

提供对SQL语句中的=, IN和BETWEEN AND的分片操作支持。

ComplexShardingStrategy支持多分片键,由于多分片键之间的关系复杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片操作符透传至分片算法,完全由应用开发者实现,提供最大的灵活度。

3. inline

inline:行表达式分片策略,对应InlineShardingStrategy。

使用Groovy的表达式,提供对SQL语句中的=和IN的分片操作支持,只支持单分片键。

对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发,如: t_user_$->{u_id % 8} 表示t_user表根据u_id模8,而分成8张表,表名称为 t_user_0 到t_user_7 。

4. hint

hint:Hint分片策略,对应HintShardingStrategy。

通过Hint而非SQL解析的方式分片的策略。对于分片字段非SQL决定,而由其他外置条件决定的场景,可使用SQL Hint灵活的注入分片字段。

例:内部系统,按照员工登录主键分库,而数据库中并无此字段。SQL Hint支持通过Java API和SQL注释(待实现)两种方式使用。

5. none

none:不分片策略,对应NoneShardingStrategy。不分片的策略。

目前例子中都使用inline分片策略,若对其他分片策略细节若感兴趣,请查阅官方文档:

https://shardingsphere.apache.org

4、编写Mapper接口操作数据

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;

@Mapper
@Component
public interface OrderDao {
    /**
     * 插入订单
     * @param price
     * @param userId
     * @param status
     * @return
     */
    @Insert("insert into t_order(price,user_id,status)values(#{price},#{userId},#{status})")
    int insertOrder(@Param("price")BigDecimal price,@Param("userId")Long userId,@Param("status")String status);

	/**
	 * 根据id列表和用户id查询订单
	 * @param orderIds
	 * @return
	 */
	@Select("<script>" +
	        "select" +
	        " * " +
	        " from t_order t " +
	        " where t.order_id in " +
	        " <foreach collection='orderIds' open='(' separator=',' close=')' item='id'>" +
	        " #{id} " +
	        " </foreach>" +
	        " and user_id = #{userId} " +
	        "</script>")
	List<Map> selectOrderbyUserAndIds(@Param("userId") Long userId,@Param("orderIds") List<Long> orderIds);
}

5、编写单元测试

import com.dbsharding.simple.dao.OrderDao;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {ShardingJdbcSimpleBootstrap.class})
public class OrderDaoTest {

    @Autowired
    OrderDao orderDao;

    @Test
    public void testInsertOrder(){
//        orderDao.insertOrder(new BigDecimal(1.1),1L,"SUCCESS");
        for(int i=0;i<20;i++){
			// 根据分库策略,若userId为偶数则插入到order_db_1库,否则插入到order_db_2库
            orderDao.insertOrder(new BigDecimal(i),1L,"SUCCESS");
        }
}
    @Test
    public void testSelectOrderbyUserAndIds(){
        List<Long> ids = new ArrayList<>();
        ids.add(1037479531180457984L);
//        ids.add(1037479530777804801L);

        List<Map> maps = orderDao.selectOrderbyUserAndIds(1L,ids);
        System.out.println(maps);
    }
}

执行单元测试的插入操作之后,可以看到如下日志:

2022-11-13 21:21:50.473  INFO 25308 --- [           main] ShardingSphere-SQL                       : Rule Type: sharding
2022-11-13 21:21:50.473  INFO 25308 --- [           main] ShardingSphere-SQL                       : Logic SQL: insert into t_order(price,user_id,status)values(?,?,?)
2022-11-13 21:21:50.473  INFO 25308 --- [           main] ShardingSphere-SQL                       : SQLStatement: InsertStatement(super=DMLStatement(super=AbstractSQLStatement(type=DML, tables=Tables(tables=[Table(name=t_order, alias=Optional.absent())]), routeConditions=Conditions(orCondition=OrCondition(andConditions=[AndCondition(conditions=[Condition(column=Column(name=user_id, tableName=t_order), operator=EQUAL, compareOperator=null, positionValueMap={}, positionIndexMap={0=1})])])), encryptConditions=Conditions(orCondition=OrCondition(andConditions=[])), sqlTokens=[TableToken(tableName=t_order, quoteCharacter=NONE, schemaNameLength=0), SQLToken(startIndex=19)], parametersIndex=3, logicSQL=insert into t_order(price,user_id,status)values(?,?,?)), deleteStatement=false, updateTableAlias={}, updateColumnValues={}, whereStartIndex=0, whereStopIndex=0, whereParameterStartIndex=0, whereParameterEndIndex=0), columnNames=[price, user_id, status], values=[InsertValue(columnValues=[org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@518ddd3b, org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@939ff41, org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@6e0e5dec])])
2022-11-13 21:21:50.474  INFO 25308 --- [           main] ShardingSphere-SQL                       : Actual SQL: m2 ::: insert into t_order_1 (price, user_id, status, order_id) VALUES (?, ?, ?, ?) ::: [18, 1, SUCCESS, 1037476657251745792]
2022-11-13 21:21:50.476 DEBUG 25308 --- [           main] c.i.d.simple.dao.OrderDao.insertOrder    : <==    Updates: 1
2022-11-13 21:21:50.476 DEBUG 25308 --- [           main] c.i.d.simple.dao.OrderDao.insertOrder    : ==>  Preparing: insert into t_order(price,user_id,status)values(?,?,?)
2022-11-13 21:21:50.476 DEBUG 25308 --- [           main] c.i.d.simple.dao.OrderDao.insertOrder    : ==> Parameters: 19(BigDecimal), 1(Long), SUCCESS(String)
2022-11-13 21:21:50.476  INFO 25308 --- [           main] ShardingSphere-SQL                       : Rule Type: sharding
2022-11-13 21:21:50.476  INFO 25308 --- [           main] ShardingSphere-SQL                       : Logic SQL: insert into t_order(price,user_id,status)values(?,?,?)
2022-11-13 21:21:50.476  INFO 25308 --- [           main] ShardingSphere-SQL                       : SQLStatement: InsertStatement(super=DMLStatement(super=AbstractSQLStatement(type=DML, tables=Tables(tables=[Table(name=t_order, alias=Optional.absent())]), routeConditions=Conditions(orCondition=OrCondition(andConditions=[AndCondition(conditions=[Condition(column=Column(name=user_id, tableName=t_order), operator=EQUAL, compareOperator=null, positionValueMap={}, positionIndexMap={0=1})])])), encryptConditions=Conditions(orCondition=OrCondition(andConditions=[])), sqlTokens=[TableToken(tableName=t_order, quoteCharacter=NONE, schemaNameLength=0), SQLToken(startIndex=19)], parametersIndex=3, logicSQL=insert into t_order(price,user_id,status)values(?,?,?)), deleteStatement=false, updateTableAlias={}, updateColumnValues={}, whereStartIndex=0, whereStopIndex=0, whereParameterStartIndex=0, whereParameterEndIndex=0), columnNames=[price, user_id, status], values=[InsertValue(columnValues=[org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@518ddd3b, org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@939ff41, org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@6e0e5dec])])
2022-11-13 21:21:50.476  INFO 25308 --- [           main] ShardingSphere-SQL                       : Actual SQL: m2 ::: insert into t_order_2 (price, user_id, status, order_id) VALUES (?, ?, ?, ?) ::: [19, 1, SUCCESS, 1037476657264328705]
2022-11-13 21:21:50.478 DEBUG 25308 --- [           main] c.i.d.simple.dao.OrderDao.insertOrder    : <==    Updates: 1

通过日志可以发现user_id为奇数的数据被插入到m2(order_db_2)数据源中,另外根据order_id为奇数则将数据插入到m2.t_order_2表,为偶数的数据被插入到m2.t_order_1表,达到预期效果。

执行单元测试的查询操作之后,可以看到如下日志:

2022-11-13 21:42:30.440 DEBUG 16652 --- [           main] c.i.d.s.d.O.selectOrderbyUserAndIds      : ==>  Preparing: select * from t_order t where t.order_id in ( ? ) and user_id = ?
2022-11-13 21:42:30.455 DEBUG 16652 --- [           main] c.i.d.s.d.O.selectOrderbyUserAndIds      : ==> Parameters: 1037479531180457984(Long), 2(Long)
2022-11-13 21:42:30.867  INFO 16652 --- [           main] ShardingSphere-SQL                       : Rule Type: sharding
2022-11-13 21:42:30.869  INFO 16652 --- [           main] ShardingSphere-SQL                       : Logic SQL: select *  from t_order t  where t.order_id in   (   ?   )  and user_id = ?
2022-11-13 21:42:30.869  INFO 16652 --- [           main] ShardingSphere-SQL                       : SQLStatement: SelectStatement(super=DQLStatement(super=AbstractSQLStatement(type=DQL, tables=Tables(tables=[Table(name=t_order, alias=Optional.of(t))]), routeConditions=Conditions(orCondition=OrCondition(andConditions=[AndCondition(conditions=[Condition(column=Column(name=order_id, tableName=t_order), operator=IN, compareOperator=null, positionValueMap={}, positionIndexMap={0=0}), Condition(column=Column(name=user_id, tableName=t_order), operator=EQUAL, compareOperator==, positionValueMap={}, positionIndexMap={0=1})])])), encryptConditions=Conditions(orCondition=OrCondition(andConditions=[])), sqlTokens=[TableToken(tableName=t_order, quoteCharacter=NONE, schemaNameLength=0)], parametersIndex=2, logicSQL=select *  from t_order t  where t.order_id in   (   ?   )  and user_id = ?)), containStar=true, firstSelectItemStartIndex=7, selectListStopIndex=7, groupByLastIndex=0, items=[StarSelectItem(owner=Optional.absent())], groupByItems=[], orderByItems=[], limit=null, subqueryStatement=null, subqueryStatements=[], subqueryConditions=[])
2022-11-13 21:42:30.869  INFO 16652 --- [           main] ShardingSphere-SQL                       : Actual SQL: m2 ::: select *  from t_order_1 t  where t.order_id in   (   ?   )  and user_id = ? ::: [1037479531180457984, 1]
2022-11-13 21:42:30.895 DEBUG 16652 --- [           main] c.i.d.s.d.O.selectOrderbyUserAndIds      : <==      Total: 1

通过日志可以发现,查询条件user_id为1,根据分片策略m$->{user_id % 2 + 1}计算得出m2,sharding-jdbc将SQL路由到m2数据源,达到预期效果。

当使用user_id查询时,因为分库策略中以user_id为分片键,所以会在user_id为奇数时操作m2数据源,又根据当order_id为偶数时,根据t_order表的分片策略使用t_order_1表。所以结果为使用m2库中的t_order_1表查询。

在分库分表中根据条件查询时,若查询条件能加上分片字段就一定要加上,没有分片键时会广播路由,查询所有的数据源。

3、垂直分库

垂直分库是指以表为依据,按照业务归属不同,将不同的表拆分到不同的库中,每个库可以放在不同的服务器上。

之前已经创建了order_db数据库用于存储订单相关的数据,此处创建user_db数据库用于存储用户相关的数据。

1、创建数据库和数据表

创建数据库user_db:

CREATE DATABASE ` user_db` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';

创建数据表t_ user:

CREATE TABLE `t_user` (
  `user_id` bigint NOT NULL COMMENT '用户id',
  `fullname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户姓名',
  `user_type` char(1) DEFAULT NULL COMMENT '用户类型',
  PRIMARY KEY (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC;

2、引入Maven依赖

引入 sharding-jdbc和SpringBoot整合的Jar包:

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding‐jdbc‐spring‐boot‐starter</artifactId>
    <version>4.0.0‐RC1</version>
</dependency>

3、编写配置文件application.properties

配置内容包括:数据源、主键生成策略、分片策略(分库策略和分表策略)等,分片策略是sharding-jdbc进行分库分表操作的重要依据。

只需要在上面水平分库的配置文件中添加与user_db数据库有关的数据源信息并配置t_user表的数据节点即可。

# user_db数据库保存到数据源m0上
spring.shardingsphere.datasource.m0.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m0.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m0.url = jdbc:mysql://localhost:3308/user_db?useUnicode=true
spring.shardingsphere.datasource.m0.username = root
spring.shardingsphere.datasource.m0.password = 123456
# 指定t_user表的数据分布情况,配置数据节点,保存到m0数据源上
spring.shardingsphere.sharding.tables.t_user.actual-data-nodes = m$->{0}.t_user

完整的配置文件如下:

server.port=8889

spring.application.name = sharding-jdbc-simple-demo

server.servlet.context-path = /sharding-jdbc-simple-demo
spring.http.encoding.enabled = true
spring.http.encoding.charset = UTF-8
spring.http.encoding.force = true

spring.main.allow-bean-definition-overriding = true

mybatis.configuration.map-underscore-to-camel-case = true

#sharding-jdbc分片规则配置
#数据源
spring.shardingsphere.datasource.names = m0,m1,m2
# user_db数据库保存到数据源m0上
spring.shardingsphere.datasource.m0.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m0.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m0.url = jdbc:mysql://localhost:3308/user_db?useUnicode=true
spring.shardingsphere.datasource.m0.username = root
spring.shardingsphere.datasource.m0.password = 123456

spring.shardingsphere.datasource.m1.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m1.url=jdbc:mysql://localhost:3308/order_db_1?useUnicode=true
spring.shardingsphere.datasource.m1.username = root
spring.shardingsphere.datasource.m1.password = 123456

spring.shardingsphere.datasource.m2.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m2.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m2.url=jdbc:mysql://localhost:3308/order_db_2?useUnicode=true
spring.shardingsphere.datasource.m2.username = root
spring.shardingsphere.datasource.m2.password = 123456

# 水平分库策略,以user_id为分片键,分片策略为user_id % 2 + 1,user_id为偶数操作m1数据源,否则操作m2。将order_db数据库水平分成order_db_1和order_db_2
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.sharding-column = user_id
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.algorithm-expression = m$->{user_id % 2 + 1}

# 指定t_order表的数据分布情况,配置数据节点 m.t_order_1、m1.t_order_2、m2.t_order_1、m2.t_order_21
# 如果这里配置如m1.t_order_$->{1..2},则查询时只会查询m1库的表
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes = m$->{1..2}.t_order_$->{1..2}
# 指定t_user表的数据分布情况,配置数据节点,保存到m0数据源上
spring.shardingsphere.sharding.tables.t_user.actual-data-nodes = m$->{0}.t_user

# 指定t_order表的主键生成策略为SNOWFLAKE
spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id
spring.shardingsphere.sharding.tables.t_order.key-generator.type=SNOWFLAKE

# 指定t_order表的分片策略,分片策略包括分片键和分片算法
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column = order_id
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression = t_order_$->{order_id % 2 + 1}
# 指定t_user表的分片策略,水平分表需要该配置,此处是垂直分库,可以不写该配置
spring.shardingsphere.sharding.tables.t_user.table-strategy.inline.sharding-column = user_id
spring.shardingsphere.sharding.tables.t_user.table-strategy.inline.algorithm-expression = t_user

# 打开sql输出日志
spring.shardingsphere.props.sql.show = true

swagger.enable = true

logging.level.root = info
logging.level.org.springframework.web = info
logging.level.com.itheima.dbsharding  = debug
logging.level.druid.sql = debug

4、编写Mapper接口操作数据

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;

@Mapper
@Component
public interface UserDao {
    /**
     * 新增用户
     * @param userId 用户id
     * @param fullname 用户姓名
     * @return
     */
    @Insert("insert into t_user(user_id,fullname) values(#{userId},#{fullname})")
    int insertUser(@Param("userId")Long userId,@Param("fullname")String fullname);

	/**
	* 根据id列表查询多个用户
	* @param userIds 用户id列表
	* @return
	*/
	@Select({"<script>",
			" select",
			" * ",
			" from t_user t ",
			" where t.user_id in",
			"<foreach collection='userIds' item='id' open='(' separator=',' close=')'>",
			"#{id}",
			"</foreach>",
			"</script>"
	})
	List<Map> selectUserbyIds(@Param("userIds") List<Long> userIds);
}

5、编写单元测试

import com.dbsharding.simple.dao.UserDao;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {ShardingJdbcSimpleBootstrap.class})
public class UserDaoTest {

    @Autowired
    UserDao userDao;

    @Test
    public void testInsertUser(){
        for(int i=0;i<10;i++){
			Long id = i + 1L;
            userDao.insertUser(id, "姓名" + id);
        }
}
    @Test
    public void testSelectOrderbyUserAndIds(){
        List<Long> userIds = new ArrayList<>();
        userIds.add(1L);
        userIds.add(2L);
        List<Map> users = userDao.selectUserbyIds(userIds);
        System.out.println(users);
    }
}

执行单元测试的插入操作之后,可以看到如下日志:

通过日志可以发现User数据被插入到m0(user_db)数据源中。

执行单元测试的查询操作之后,可以看到如下日志:

4、公共表

公共表属于系统中数据量较小,变动少,而且属于高频联合查询的依赖表。参数表、数据字典表等属于此类型。可以将这类表在每个数据库都保存一份,所有更新操作都同时发送到所有分库执行。接下来看一下如何使用Sharding-JDBC实现公共表。

1、创建数据表

分别在user_db、order_db_1、order_db_2三个数据库中创建t_dict表:

CREATE TABLE `t_dict` (
`dict_id` bigint(20) NOT NULL COMMENT '字典id',
`type` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '字典类型',
`code` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '字典编码',
`value` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '字典值',
PRIMARY KEY (`dict_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

2、引入Maven依赖

引入 sharding-jdbc和SpringBoot整合的Jar包:

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding‐jdbc‐spring‐boot‐starter</artifactId>
    <version>4.0.0‐RC1</version>
</dependency>

3、编写配置文件application.properties

与上面的配置文件一样,此处只需在上面的配置文件中加上如下配置即可。

# 指定t_dict为公共表
spring.shardingsphere.sharding.broadcast-tables=t_dict
完整的配置文件如下所示:
server.port=8889

spring.application.name = sharding-jdbc-simple-demo

server.servlet.context-path = /sharding-jdbc-simple-demo
spring.http.encoding.enabled = true
spring.http.encoding.charset = UTF-8
spring.http.encoding.force = true

spring.main.allow-bean-definition-overriding = true

mybatis.configuration.map-underscore-to-camel-case = true

#sharding-jdbc分片规则配置
#数据源
spring.shardingsphere.datasource.names = m0,m1,m2

spring.shardingsphere.datasource.m0.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m0.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m0.url = jdbc:mysql://localhost:3308/user_db?useUnicode=true
spring.shardingsphere.datasource.m0.username = root
spring.shardingsphere.datasource.m0.password = 123456

spring.shardingsphere.datasource.m1.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m1.url = jdbc:mysql://localhost:3308/order_db_1?useUnicode=true
spring.shardingsphere.datasource.m1.username = root
spring.shardingsphere.datasource.m1.password = 123456

spring.shardingsphere.datasource.m2.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m2.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m2.url = jdbc:mysql://localhost:3308/order_db_2?useUnicode=true
spring.shardingsphere.datasource.m2.username = root
spring.shardingsphere.datasource.m2.password = 123456

# 分库策略,以user_id为分片键,分片策略为user_id % 2 + 1,user_id为偶数操作m1数据源,否则操作m2。
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.sharding-column = user_id
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.algorithm-expression = m$->{user_id % 2 + 1}

# 指定t_order表的数据分布情况,配置数据节点 m.t_order_1,m1.t_order_2,m2.t_order_1,m2.t_order_21
# 如果这里配置如m1.t_order_$->{1..2},则查询时只会查询m1库的表
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes = m$->{1..2}.t_order_$->{1..2}
spring.shardingsphere.sharding.tables.t_user.actual-data-nodes = m$->{0}.t_user

# 指定t_order表的主键生成策略为SNOWFLAKE
spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id
spring.shardingsphere.sharding.tables.t_order.key-generator.type=SNOWFLAKE

# 指定t_order表的分片策略,分片策略包括分片键和分片算法
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column = order_id
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression = t_order_$->{order_id % 2 + 1}

spring.shardingsphere.sharding.tables.t_user.table-strategy.inline.sharding-column = user_id
spring.shardingsphere.sharding.tables.t_user.table-strategy.inline.algorithm-expression = t_user

# 指定t_dict为公共表
spring.shardingsphere.sharding.broadcast-tables=t_dict

# 打开sql输出日志
spring.shardingsphere.props.sql.show = true

swagger.enable = true

logging.level.root = info
logging.level.org.springframework.web = info
logging.level.com.itheima.dbsharding  = debug
logging.level.druid.sql = debug

4、编写Mapper接口操作数据

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;

@Mapper
@Component
public interface DictDao {
	/**
	* 新增字典
	* @param type 字典类型
	* @param code 字典编码
	* @param value 字典值
	* @return
	*/
	@Insert("insert into t_dict(dict_id,type,code,value) values(#{dictId},#{type},#{code},#{value})")
	int insertDict(@Param("dictId") Long dictId,@Param("type") String type, @Param("code")String code, @Param("value")String value);
	
	/**
	* 删除字典
	* @param dictId 字典id
	* @return
	*/
	@Delete("delete from t_dict where dict_id = #{dictId}")
	int deleteDict(@Param("dictId") Long dictId);
}

5、编写单元测试

@Test
public void testInsertDict(){
    //t_dict设置为公共表后,插入数据时会同时插入所有数据源
    dictDao.insertDict(1L,"user_type","1","超级管理员");
    dictDao.insertDict(2L,"user_type","2","二级管理员");
}

通过日志可以看出,对t_dict表的操作被广播至所有数据源。

6、公共表关联查询测试:

字典表已在每个分库存在,各业务表即可与字典表关联查询。

在UserDao中添加如下查询方法,从t_user和t_dict表中关联查询:

/**
 * 根据id列表查询多个用户
 * @param userIds 用户id列表
 * @return
 */
@Select({"<script>",
        " select",
        " * ",
        " from t_user t, t_dict b",
        " where t.user_type = b.code and t.user_id in",
        "<foreach collection='userIds' item='id' open='(' separator=',' close=')'>",
        "#{id}",
        "</foreach>",
        "</script>"
})
List<Map> selectUserInfobyIds(@Param("userIds") List<Long> userIds);

添加测试方法:

@Test
public void testSelectUserInfobyIds(){
    //分库的表可以直接在库中关联查询公共表
    List<Long> userIds = new ArrayList<>();
    userIds.add(1L);
    userIds.add(2L);
    List<Map> users = userDao.selectUserInfobyIds(userIds);
    System.out.println(users);
}

5、读写分离

MySQL的读写分离是一种常见的数据库架构模式,用于提高数据库的可用性和扩展性。通过将读取请求分散到多个从服务器(slave servers),可以减轻主服务器(master server)的负载,从而提高读取性能。同时,写入请求仍然集中在一个主服务器上执行,保证了数据的一致性和完整性。

面对日益增加的系统访问量,数据库的吞吐量面临着巨大瓶颈。 对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库和从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。

Sharding-JDBC读写分离则是根据SQL语义的分析,将写操作和读操作分别路由至主库与从库。它提供透明化读写分离,让使用方尽量像使用一个数据库一样使用主从数据库集群。

Sharding-JDBC提供一主多从的读写分离配置,可独立使用,也可配合分库分表使用,同一线程且同一数据库连接内,如有写入操作,以后的读操作均从主库读取,用于保证数据一致性。Sharding-JDBC不提供主从数据库的数据同步功能,需要采用其他机制支持。

为了实现Sharding-JDBC的读写分离,首先要进行mysql的主从同步配置。具体配置可参考上面的《3、主从复制》小节。

1、创建数据库和数据表

在主库和从库上都需要创建数据库user_db:

CREATE DATABASE ` user_db` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';

只需在主库上创建数据表t_ user,从库会自动同步:

CREATE TABLE `t_user` (
  `user_id` bigint NOT NULL COMMENT '用户id',
  `fullname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户姓名',
  `user_type` char(1) DEFAULT NULL COMMENT '用户类型',
  PRIMARY KEY (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC;

2、引入Maven依赖

<properties>
     <java.version>1.8</java.version>
     <sharding-sphere.version>4.0.0-RC1</sharding-sphere.version>
 </properties>
 <!-- 依赖web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 依赖mybatis和mysql驱动 -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.4</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<!--依赖lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<!--依赖sharding-->
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    <version>${sharding-sphere.version}</version>
</dependency>
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-core-common</artifactId>
    <version>${sharding-sphere.version}</version>
</dependency>
<!--依赖数据源druid-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.21</version>
</dependency>

3、编写配置文件application.properties

配置文件中需要包含主库数据源和从库数据源的配置信息,并告知Sharding-JDBC哪个是主库,哪个是从库。

# 主库数据源信息
spring.shardingsphere.datasource.m0.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m0.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m0.url = jdbc:mysql://localhost:3308/user_db?useUnicode=true
spring.shardingsphere.datasource.m0.username = root
spring.shardingsphere.datasource.m0.password = 123456
# 从库数据源信息
spring.shardingsphere.datasource.s0.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.s0.driver‐class‐name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.s0.url = jdbc:mysql://localhost:3309/user_db?useUnicode=true
spring.shardingsphere.datasource.s0.username = root
spring.shardingsphere.datasource.s0.password = 123456
# 主库与从库逻辑数据源定义,ds0为逻辑数据源名称,可随意命名,ds0数据源包含m0和s0两个真实数据源
spring.shardingsphere.sharding.master-slave-rules.ds0.master-data-source-name=m0
spring.shardingsphere.sharding.master-slave-rules.ds0.slave-data-source-names=s0
# 配置需要读写分离的数据表, ds0数据源包含m0和s0,写入m0.t_user表,读取s0.t_user
spring.shardingsphere.sharding.tables.t_user.actual-data-nodes = ds0.t_user

完整配置文件内容如下:

server.port=56081

spring.application.name = sharding-jdbc-simple-demo

server.servlet.context-path = /sharding-jdbc-simple-demo
spring.http.encoding.enabled = true
spring.http.encoding.charset = UTF-8
spring.http.encoding.force = true

spring.main.allow-bean-definition-overriding = true

mybatis.configuration.map-underscore-to-camel-case = true

#sharding-jdbc分片规则配置
#数据源
spring.shardingsphere.datasource.names = m0,m1,m2, s0

spring.shardingsphere.datasource.m0.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m0.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m0.url = jdbc:mysql://localhost:3308/user_db?useUnicode=true
spring.shardingsphere.datasource.m0.username = root
spring.shardingsphere.datasource.m0.password = 123456

spring.shardingsphere.datasource.m1.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m1.url=jdbc:mysql://localhost:3306/order_db_1?useUnicode=true
spring.shardingsphere.datasource.m1.username = root
spring.shardingsphere.datasource.m1.password = 123456

spring.shardingsphere.datasource.m2.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m2.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m2.url=jdbc:mysql://localhost:3307/order_db_2?useUnicode=true
spring.shardingsphere.datasource.m2.username = root
spring.shardingsphere.datasource.m2.password = 123456

spring.shardingsphere.datasource.s0.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.s0.driver‐class‐name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.s0.url = jdbc:mysql://localhost:3309/user_db?useUnicode=true
spring.shardingsphere.datasource.s0.username = root
spring.shardingsphere.datasource.s0.password = 123456

# 主库与从库逻辑数据源定义,ds0为逻辑数据源名称,可随意命名,ds0数据源包含m0和s0两个真实数据源
spring.shardingsphere.sharding.master-slave-rules.ds0.master-data-source-name=m0
spring.shardingsphere.sharding.master-slave-rules.ds0.slave-data-source-names=s0

# 分库策略,以user_id为分片键,分片策略为user_id % 2 + 1,user_id为偶数操作m1数据源,否则操作m2。
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.sharding-column = user_id
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.algorithm-expression = m$->{user_id % 2 + 1}

# 指定t_order表的数据分布情况,配置数据节点 m.t_order_1、m1.t_order_2、m2.t_order_1、m2.t_order_21
# 如果这里配置如m1.t_order_$->{1..2},则查询时只会查询m1库的表
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes = m$->{1..2}.t_order_$->{1..2}
#spring.shardingsphere.sharding.tables.t_user.actual-data-nodes = m$->{0}.t_user
# 配置需要读写分离的数据表, ds0数据源包含m0和s0,写入m0.t_user表,读取s0.t_user
spring.shardingsphere.sharding.tables.t_user.actual-data-nodes = ds0.t_user

# 指定t_order表的主键生成策略为SNOWFLAKE
spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id
spring.shardingsphere.sharding.tables.t_order.key-generator.type=SNOWFLAKE

# 指定t_order表的分片策略,分片策略包括分片键和分片算法
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column = order_id
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression = t_order_$->{order_id % 2 + 1}

spring.shardingsphere.sharding.tables.t_user.table-strategy.inline.sharding-column = user_id
spring.shardingsphere.sharding.tables.t_user.table-strategy.inline.algorithm-expression = t_user

# 指定t_dict为公共表
spring.shardingsphere.sharding.broadcast-tables=t_dict

# 打开sql输出日志
spring.shardingsphere.props.sql.show = true

swagger.enable = true

logging.level.root = info
logging.level.org.springframework.web = info
logging.level.com.itheima.dbsharding  = debug
logging.level.druid.sql = debug

或者application.yaml配置文件:

server:
  port: 8085
spring:
  main:
    allow-bean-definition-overriding: true
  shardingsphere:
    # 参数配置,显示sql
    props:
      sql:
        show: true
    # 配置数据源
    datasource:
      # 给每个数据源取别名,下面的ds1,ds2,ds3任意取名字
      names: ds1,ds2,ds3
      # 给master-ds1每个数据源配置数据库连接信息
      ds1:
        # 配置druid数据源
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://47.115.94.78:3306/ksd-sharding-db?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
        username: root
        password: mkxiaoer1986.
        maxPoolSize: 100
        minPoolSize: 5
      # 配置ds2-slave
      ds2:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://114.215.145.201:3306/ksd-sharding-db?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
        username: root
        password: mkxiaoer1986.
        maxPoolSize: 100
        minPoolSize: 5
      # 配置ds3-slave
      ds3:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://114.215.145.201:3306/ksd-sharding-db?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
        username: root
        password: mkxiaoer1986.
        maxPoolSize: 100
        minPoolSize: 5
    # 配置默认数据源ds1
    sharding:
     # 默认数据源,主要用于写,注意一定要配置读写分离 ,注意:如果不配置,那么就会把三个节点都当做从slave节点,新增,修改和删除会出错。
      default-data-source-name: ds1
    # 配置数据源的读写分离,但是数据库一定要做主从复制
    masterslave:
      # 配置主从名称,可以任意取名字
      name: ms
      # 配置主库master,负责数据的写入
      master-data-source-name: ds1
      # 配置从库slave节点
      slave-data-source-names: ds2,ds3
      # 配置slave节点的负载均衡均衡策略,采用轮询机制
      load-balance-algorithm-type: round_robin
# 整合mybatis的配置XXXXX
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.xuexiangban.shardingjdbc.entity

4、编写Mapper接口操作数据

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;

@Mapper
@Component
public interface UserDao {
    /**
     * 新增用户
     * @param userId 用户id
     * @param fullname 用户姓名
     * @return
     */
    @Insert("insert into t_user(user_id,fullname) values(#{userId},#{fullname})")
    int insertUser(@Param("userId")Long userId,@Param("fullname")String fullname);

	/**
	* 根据id列表查询多个用户
	* @param userIds 用户id列表
	* @return
	*/
	@Select({"<script>",
			" select",
			" * ",
			" from t_user t ",
			" where t.user_id in",
			"<foreach collection='userIds' item='id' open='(' separator=',' close=')'>",
			"#{id}",
			"</foreach>",
			"</script>"
	})
	List<Map> selectUserbyIds(@Param("userIds") List<Long> userIds);
}

5、编写单元测试

import com.dbsharding.simple.dao.UserDao;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {ShardingJdbcSimpleBootstrap.class})
public class UserDaoTest {

    @Autowired
    UserDao userDao;

    @Test
    public void testInsertUser(){
        for(int i=0;i<14;i++){
			Long id = i + 1L;
            userDao.insertUser(id, "姓名" + id);
        }
}
    @Test
    public void testSelectOrderbyUserAndIds(){
        List<Long> userIds = new ArrayList<>();
        userIds.add(1L);
        List<Map> users = userDao.selectUserbyIds(userIds);
        System.out.println(users);
    }
}

执行单元测试的插入操作之后,可以看到如下日志:

通过日志可以发现User数据被插入到m0(user_db)主数据源中。

执行单元测试的查询操作之后,可以看到如下日志,查询操作落入s0从数据源:

2022-11-14 07:29:46.292 DEBUG 4776 --- [           main] c.i.d.s.dao.UserDao.selectUserInfobyIds  : ==>  Preparing: select * from t_user t ,t_dict b where t.user_type = b.code and t.user_id in ( ? )
2022-11-14 07:29:46.310 DEBUG 4776 --- [           main] c.i.d.s.dao.UserDao.selectUserInfobyIds  : ==> Parameters: 16(Long)
2022-11-14 07:29:46.653  INFO 4776 --- [           main] ShardingSphere-SQL                       : Rule Type: sharding
2022-11-14 07:29:46.656  INFO 4776 --- [           main] ShardingSphere-SQL                       : Logic SQL: select  *   from t_user t ,t_dict b  where t.user_type = b.code and t.user_id in  (   ?  )
2022-11-14 07:29:46.656  INFO 4776 --- [           main] ShardingSphere-SQL                       : SQLStatement: SelectStatement(super=DQLStatement(super=AbstractSQLStatement(type=DQL, tables=Tables(tables=[Table(name=t_user, alias=Optional.of(t)), Table(name=t_dict, alias=Optional.of(b))]), routeConditions=Conditions(orCondition=OrCondition(andConditions=[AndCondition(conditions=[Condition(column=Column(name=user_id, tableName=t_user), operator=IN, compareOperator=null, positionValueMap={}, positionIndexMap={0=0})])])), encryptConditions=Conditions(orCondition=OrCondition(andConditions=[])), sqlTokens=[TableToken(tableName=t_user, quoteCharacter=NONE, schemaNameLength=0), TableToken(tableName=t_dict, quoteCharacter=NONE, schemaNameLength=0)], parametersIndex=1, logicSQL=select  *   from t_user t ,t_dict b  where t.user_type = b.code and t.user_id in  (   ?  ))), containStar=true, firstSelectItemStartIndex=8, selectListStopIndex=8, groupByLastIndex=0, items=[StarSelectItem(owner=Optional.absent())], groupByItems=[], orderByItems=[], limit=null, subqueryStatement=null, subqueryStatements=[], subqueryConditions=[])
2022-11-14 07:29:46.656  INFO 4776 --- [           main] ShardingSphere-SQL                       : Actual SQL: s0 ::: select  *   from t_user t ,t_dict b  where t.user_type = b.code and t.user_id in  (   ?  ) ::: [1]
2022-11-14 07:29:46.694 DEBUG 4776 --- [           main] c.i.d.s.dao.UserDao.selectUserInfobyIds  : <==      Total: 1
[{dict_id=2, user_type=1, code=1, user_id=1, fullname=姓名1, type=order_type, value=1}]

5、Sharding-JDBC分库分表实战

Sharding-JDBC的核心功能为数据分片和读写分离,通过Sharding-JDBC,应用可以透明的使用jdbc访问已经分库分表、读写分离的多个数据源,而不用关心数据源的数量以及数据如何分布。

1、需求描述

电商平台商品列表展示,每个列表项中除了包含商品基本信息、商品描述信息之外,还包括了商品所属的店铺信息。

需要实现如下功能:

  1. 添加商品
  2. 商品分页查询
  3. 商品统计

2、数据库设计

首先根据不同业务对商品信息与店铺信息进行垂直分库,分成PRODUCT_DB(商品库)和STORE_DB(店铺库)。其次对商品信息进行垂直分表,分成商品基本信息(product_info)和商品描述信息(product_descript),地理区域信息(region)作为公共表,冗余在两库中。

考虑到商品信息的数据增长性,对PRODUCT_DB(商品库)进行了水平分库,分成PRODUCT_DB_1和PRODUCT_DB_2两个数据库,分片键使用店铺id,分片策略为店铺id%2 + 1,因此商品描述信息表需要添加所属店铺id,对所属店铺id进行了冗余。另外对商品基本信息(product_info)和商品描述信息(product_descript)进行水平分表,分片键使用商品id,分片策略为商品id%2 + 1,并将这两个表设置为绑定表,避免笛卡尔积join。

为避免主键冲突,ID生成策略采用雪花算法来生成全局唯一ID,最终数据库设计如下。

最好使用读写分离提升读写性能。

3、编码实现

环境说明:

操作系统:Win10

数据库:MySQL-5.7.25

JDK:64位 jdk1.8.0_201

应用框架:spring-boot-2.1.3.RELEASE,Mybatis3.5.0

Sharding-JDBC:sharding-jdbc-spring-boot-starter-4.0.0-RC1

1、环境准备:

1. 创建数据库和数据表

在Master节点与Slave节点都创建store_db数据库,并执行以下脚本创建表(可只在Master节点创建数据表,Slave节点会自动同步数据表):

DROP TABLE IF EXISTS `region`;
CREATE TABLE `region` (
	`id` bigint(20) NOT NULL COMMENT 'id',
	`region_code` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT
	'地理区域编码',
	`region_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
	COMMENT '地理区域名称',
	`level` tinyint(1) NULL DEFAULT NULL COMMENT '地理区域级别(省、市、县)',
	`parent_region_code` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
	COMMENT '上级地理区域编码',
	PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

INSERT INTO `region` VALUES (1, '110000', '北京', 0, NULL);
INSERT INTO `region` VALUES (2, '410000', '河南省', 0, NULL);
INSERT INTO `region` VALUES (3, '110100', '北京市', 1, '110000');
INSERT INTO `region` VALUES (4, '410100', '郑州市', 1, '410000');


DROP TABLE IF EXISTS `store_info`;
CREATE TABLE `store_info` (
	`id` bigint(20) NOT NULL COMMENT 'id',
	`store_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT
	'店铺名称',
	`reputation` int(11) NULL DEFAULT NULL COMMENT '信誉等级',
	`region_code` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT
	'店铺所在地',
	PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

INSERT INTO `store_info` VALUES (1, 'XX零食店', 4, '110100');
INSERT INTO `store_info` VALUES (2, 'XX饮品店', 3, '410100');

在Master节点与Slave节点都创建product_db_1、product_db_2数据库,并分别对两库执行以下脚本创建表:

DROP TABLE IF EXISTS `product_descript_1`;
CREATE TABLE `product_descript_1` (
    `id` bigint(20) NOT NULL COMMENT 'id',
    `product_info_id` bigint(20) NULL DEFAULT NULL COMMENT '所属商品id',
    `descript` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '商品描述',
    `store_info_id` bigint(20) NULL DEFAULT NULL COMMENT '所属店铺id',
    PRIMARY KEY (`id`) USING BTREE,
    INDEX `FK_Reference_2`(`product_info_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

DROP TABLE IF EXISTS `product_descript_2`;
CREATE TABLE `product_descript_2` (
	`id` bigint(20) NOT NULL COMMENT 'id',
	`product_info_id` bigint(20) NULL DEFAULT NULL COMMENT '所属商品id',
	`descript` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '商品描述',
	`store_info_id` bigint(20) NULL DEFAULT NULL COMMENT '所属店铺id',
	PRIMARY KEY (`id`) USING BTREE,
	INDEX `FK_Reference_2`(`product_info_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

DROP TABLE IF EXISTS `product_info_1`;
CREATE TABLE `product_info_1` (
	`product_info_id` bigint(20) NOT NULL COMMENT 'id',
	`store_info_id` bigint(20) NULL DEFAULT NULL COMMENT '所属店铺id',
	`product_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
	COMMENT '商品名称',
	`spec` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '规
	格',
	`region_code` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT
	'产地',
	`price` decimal(10, 0) NULL DEFAULT NULL COMMENT '商品价格',
	`image_url` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT
	'商品图片',
	PRIMARY KEY (`product_info_id`) USING BTREE,
	INDEX `FK_Reference_1`(`store_info_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

DROP TABLE IF EXISTS `product_info_2`;
CREATE TABLE `product_info_2` (
	`product_info_id` bigint(20) NOT NULL COMMENT 'id',
	`store_info_id` bigint(20) NULL DEFAULT NULL COMMENT '所属店铺id',
	`product_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
	COMMENT '商品名称',
	`spec` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '规
	格',
	`region_code` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT
	'产地',
	`price` decimal(10, 0) NULL DEFAULT NULL COMMENT '商品价格',
	`image_url` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT
	'商品图片',
	PRIMARY KEY (`product_info_id`) USING BTREE,
	INDEX `FK_Reference_1`(`store_info_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

DROP TABLE IF EXISTS `region`;
CREATE TABLE `region` (
    `id` bigint(20) NOT NULL COMMENT 'id',
    `region_code` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT
    '地理区域编码',
    `region_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
    COMMENT '地理区域名称',
    `level` tinyint(1) NULL DEFAULT NULL COMMENT '地理区域级别(省、市、县)',
    `parent_region_code` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
    COMMENT '上级地理区域编码',
    PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

INSERT INTO `region` VALUES (1, '110000', '北京', 0, NULL);
INSERT INTO `region` VALUES (2, '410000', '河南省', 0, NULL);
INSERT INTO `region` VALUES (3, '110100', '北京市', 1, '110000');
INSERT INTO `region` VALUES (4, '410100', '郑州市', 1, '410000');

2. MySQL主从同步配置,具体配置可参考上面的《3、主从复制》小节。

在主库(Master节点)的MySQL安装目录下修改my.ini配置文件:

[mysql]
# 设置mysql客户端默认字符集
default-character-set=utf8
[mysqld]
# 设置3308端口
port = 3308
server_id = 1    # 主库和从库需要不一致,分别配一个唯一的ID编号
log_bin=master-bin    # 二进制文件存放路径,存放在根目录data文件夹下
#设置需要同步的数据库
binlog_do_db=user_db
binlog_do_db=store_db
binlog_do_db=product_db_1
binlog_do_db=product_db_2
#屏蔽系统库同步
binlog_ignore_db=mysql
binlog_ignore_db=information_schema
binlog_ignore_db=performance_schema
# 设置mysql的安装目录
basedir=E:\\tool\\mysql-5.7.37-winx64
# 设置mysql数据库的数据存放目录
datadir=E:\\tool\\mysql-5.7.37-winx64\\data
# 允许最大连接数
max_connections=10000
# 允许最大连接人数
max_user_connections=1000
# 服务端使用的字符集默认为8比特编码的latin1字符集
character-set-server=utf8
# 创建新表时将使用的默认存储引擎
default-storage-engine=INNODB
 
# 连接时间一年
wait_timeout=31536000
interactive_timeout=31536000

在从库(Slave节点)的MySQL安装目录下修改my.ini配置文件:

[mysql]
# 设置mysql客户端默认字符集
default-character-set=utf8
[mysqld]
# 设置3309端口
port = 3309
server_id = 2    # 主库和从库需要不一致,分别配一个唯一的ID编号
log_bin=master-bin    # 二进制文件存放路径,存放在根目录data文件夹下
#设置需要同步的数据库
replicate_wild_do_table=user_db.%
replicate_wild_do_table=store_db.%
replicate_wild_do_table=product_db_1.%
replicate_wild_do_table=product_db_2.%
#屏蔽系统库同步
replicate_wild_ignore_table=mysql.%
replicate_wild_ignore_table=information_schema.%
replicate_wild_ignore_table=performance_schema.%
# 设置mysql的安装目录
basedir=E:\\tool\\mysql-5.7.37-winx64-s1
# 设置mysql数据库的数据存放目录
datadir=E:\\tool\\mysql-5.7.37-winx64-s1\\data
# 允许最大连接数
max_connections=10000
# 允许最大连接人数
max_user_connections=1000
# 服务端使用的字符集默认为8比特编码的latin1字符集
character-set-server=utf8
# 创建新表时将使用的默认存储引擎
default-storage-engine=INNODB
 
# 连接时间一年
wait_timeout=31536000
interactive_timeout=31536000

2、引入Maven依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.3.RELEASE</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.0</version>
    </dependency>

    <dependency>
        <groupId>javax.interceptor</groupId>
        <artifactId>javax.interceptor-api</artifactId>
        <version>1.2</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>

    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.0.0</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.16</version>
    </dependency>

    <dependency>
        <groupId>org.apache.shardingsphere</groupId>
        <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
        <version>4.0.0-RC1</version>
    </dependency>
	<!-- 使用Sharding-JDBC支持分布式事务 -->
    <dependency>
        <groupId>io.shardingsphere</groupId>
        <artifactId>sharding-transaction-spring-boot-starter</artifactId>
        <version>3.1.0</version>
    </dependency>

    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.1.0</version>
    </dependency>

    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-generator</artifactId>
        <version>3.1.0</version>
    </dependency>

    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-typehandlers-jsr310</artifactId>
        <version>1.0.2</version>
    </dependency>
</dependencies>

3、编写配置文件application.properties

server.port=56082

spring.application.name = shopping
spring.profiles.active = local

server.servlet.context-path = /shopping
spring.http.encoding.enabled = true
spring.http.encoding.charset = UTF-8
spring.http.encoding.force = true

spring.main.allow-bean-definition-overriding = true

mybatis.configuration.map-underscore-to-camel-case = true

# sharding-jdbc分片规则配置
# 配置数据源
# 数据源m0与数据源m1/m2的区别是根据不同业务配置店铺信息与商品信息垂直分库的数据源信息
# 数据源m1与数据源m2的区别是配置商品库product_db水平分库的数据源信息
# 数据源m0与数据源s0的区别是配置店铺库store_db的主从同步和读写分离
# 数据源m1/m2与数据源s1/s2的区别是配置商品库product_db_1/2的主从同步和读写分离
spring.shardingsphere.datasource.names = m0,m1,m2,s0,s1,s2
# 主库数据源信息
spring.shardingsphere.datasource.m0.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m0.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m0.url = jdbc:mysql://localhost:3306/store_db?useUnicode=true
spring.shardingsphere.datasource.m0.username = root
spring.shardingsphere.datasource.m0.password = 123456

spring.shardingsphere.datasource.m1.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m1.url = jdbc:mysql://localhost:3306/product_db_1?useUnicode=true
spring.shardingsphere.datasource.m1.username = root
spring.shardingsphere.datasource.m1.password = 123456

spring.shardingsphere.datasource.m2.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m2.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m2.url = jdbc:mysql://localhost:3306/product_db_2?useUnicode=true
spring.shardingsphere.datasource.m2.username = root
spring.shardingsphere.datasource.m2.password = 123456
# 从库数据源信息
spring.shardingsphere.datasource.s0.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.s0.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.s0.url = jdbc:mysql://localhost:3307/store_db?useUnicode=true
spring.shardingsphere.datasource.s0.username = root
spring.shardingsphere.datasource.s0.password = 123456

spring.shardingsphere.datasource.s1.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.s1.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.s1.url = jdbc:mysql://localhost:3307/product_db_1?useUnicode=true
spring.shardingsphere.datasource.s1.username = root
spring.shardingsphere.datasource.s1.password = 123456

spring.shardingsphere.datasource.s2.type = com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.s2.driver-class-name = com.mysql.jdbc.Driver
spring.shardingsphere.datasource.s2.url = jdbc:mysql://localhost:3307/product_db_2?useUnicode=true
spring.shardingsphere.datasource.s2.username = root
spring.shardingsphere.datasource.s2.password = 123456

# 配置主从关系,主从逻辑数据源定义,ds0为逻辑数据源名称,ds0数据源包含m0和s0两个真实数据源
spring.shardingsphere.sharding.master-slave-rules.ds0.master-data-source-name=m0
spring.shardingsphere.sharding.master-slave-rules.ds0.slave-data-source-names=s0
spring.shardingsphere.sharding.master-slave-rules.ds1.master-data-source-name=m1
spring.shardingsphere.sharding.master-slave-rules.ds1.slave-data-source-names=s1
spring.shardingsphere.sharding.master-slave-rules.ds2.master-data-source-name=m2
spring.shardingsphere.sharding.master-slave-rules.ds2.slave-data-source-names=s2


# 水平分库策略
# 配置product_db库的分片策略,以store_info_id为分片键,分片策略为store_info_id % 2 + 1,
# 若store_info_id为偶数则操作ds1数据源,否则操作ds2数据源。
spring.shardingsphere.sharding.database-strategy.inline.sharding-column = store_info_id
spring.shardingsphere.sharding.database-strategy.inline.algorithm-expression = ds$->{store_info_id % 2 + 1}


# 配置数据节点 和 水平分表策略
# 指定 store_info 表的数据分布情况,store_info表属于store_db数据库,对应的数据源是ds0,
# 另外没有对该表进行水平分表,所以逻辑表就是真实表,配置的数据节点是ds0.store_info
spring.shardingsphere.sharding.tables.store_info.actual-data-nodes = ds$->{0}.store_info
# 配置 store_info表的分片策略,以id为分片键,并配置逻辑表到真实表的路由
spring.shardingsphere.sharding.tables.store_info.table-strategy.inline.sharding-column = id
spring.shardingsphere.sharding.tables.store_info.table-strategy.inline.algorithm-expression = store_info

# 指定 product_info 表的数据分布情况,product_info表对应的数据源是ds1和ds2,并对该表进行了水平分表,
# 需要配置逻辑表到真实表的路由,product_info 是逻辑表名,product_info_1 和 product_info_2 是真实表名,
# 配置数据节点是ds1.product_info_1、ds1.product_info_2、ds2.product_info_1、ds2.product_info_2
spring.shardingsphere.sharding.tables.product_info.actual-data-nodes = ds$->{1..2}.product_info_$->{1..2}
# 配置 product_info 表的分片策略,以product_info_id为分片键,分片策略为product_info_id % 2 + 1,
# 若product_info_id为偶数,则将数据写入product_info_1表,否则写入product_info_2表
spring.shardingsphere.sharding.tables.product_info.table-strategy.inline.sharding-column = product_info_id
spring.shardingsphere.sharding.tables.product_info.table-strategy.inline.algorithm-expression = product_info_$->{product_info_id % 2 + 1}
# 指定 product_info 表的product_info_id字段值生成策略为雪花算法SNOWFLAKE
spring.shardingsphere.sharding.tables.product_info.key-generator.column=product_info_id
spring.shardingsphere.sharding.tables.product_info.key-generator.type=SNOWFLAKE

# 指定 product_descript 表的数据分布情况,product_descript表对应的数据源是ds1和ds2,并对该表进行了水平分表,
# 需要配置逻辑表到真实表的路由,product_descript 是逻辑表名,product_descript_1 和 product_descript_2 是真实表名,
# 配置数据节点是ds1.product_descript_1、ds1.product_descript_2、ds2.product_descript_1、ds2.product_descript_2
spring.shardingsphere.sharding.tables.product_descript.actual-data-nodes = ds$->{1..2}.product_descript_$->{1..2}
# 配置 product_descript 表的分片策略,以product_info_id为分片键,分片策略为product_info_id % 2 + 1,
# 若product_info_id为偶数,则将数据写入product_descript_1表,否则写入product_descript_2表
spring.shardingsphere.sharding.tables.product_descript.table-strategy.inline.sharding-column = product_info_id
spring.shardingsphere.sharding.tables.product_descript.table-strategy.inline.algorithm-expression = product_descript_$->{product_info_id % 2 + 1}
# 指定 product_descript 表的id字段值生成策略为SNOWFLAKE
spring.shardingsphere.sharding.tables.product_descript.key-generator.column=id
spring.shardingsphere.sharding.tables.product_descript.key-generator.type=SNOWFLAKE


#设置 product_info 和 product_descript 为绑定表,防止产生笛卡尔积关联查询
spring.shardingsphere.sharding.binding-tables[0] = product_info,product_descript

# 配置region为广播表(公共表),每次更新操作会发送至所有数据源
spring.shardingsphere.sharding.broadcast-tables=region

# 打开sql输出日志
spring.shardingsphere.props.sql.show = true

swagger.enable = true

logging.level.root = info
logging.level.org.springframework.web = info
logging.level.com.itheima.dbsharding  = debug
logging.level.druid.sql = debug

4、编写实体类

@Data
public class ProductDescript {
    private Long id;
    /**
     * 所属商品id
     */
    private Long productInfoId;

    /**
     * 商品描述
     */
    private String descript;

    /**
     * 所属店铺id
     */
    private Long storeInfoId;
}

@Data
public class ProductInfo {
    private Long productInfoId;
    /**
     * 所属店铺id
     */
    private Long storeInfoId;

    /**
     * 商品名称
     */
    private String productName;

    /**
     * 规格
     */
    private String spec;

    /**
     * 产地
     */
    private String regionCode;

    /**
     * 商品价格
     */
    private BigDecimal price;

    /**
     * 商品图片
     */
    private String imageUrl;

    // ===============关联信息===================
    /**
     * 商品描述
     */
    private String descript;

    /**
     * 产地名称
     */
    private String placeOfOrigin;

    /**
     * 店铺名称
     */
    private String storeName;

    /**
     * 店铺信誉等级
     */
    private int reputation;

    /**
     * 店铺所在地名称
     */
    private String storeRegionName;
}

@Data
public class region {
    private Long id;
    /**
     * 地理区域编码
     */
    private String regionCode;

    /**
     * 地理区域名称
     */
    private String regionName;

    /**
     * 地理区域级别(省、市、县)
     */
    private int level;

    /**
     * 上级地理区域编码
     */
    private String parentRegionCode;
}

@Data
public class StoreInfo {
    private Long id;
    /**
     * 店铺名称
     */
    private String storeName;

    /**
     * 信誉等级
     */
    private int reputation;

    /**
     * 店铺所在地
     */
    private String regionCode;

    /**
     * 店铺所在地名称
     */
    private String regionName;
}

5、编写Mapper接口

@Mapper
@Component
public interface ProductDao {
    //添加商品基本信息
    @Insert("insert into product_info(store_info_id,product_name,spec,region_code,price) " +
            " values (#{storeInfoId},#{productName},#{spec},#{regionCode},#{price})")
    //@Options注解配置生成主键并映射到productInfoId属性
    //作用:插入数据后,mybatis返回创建主键,并映射到productInfoId属性中
    @Options(useGeneratedKeys = true,keyProperty = "productInfoId",keyColumn = "product_info_id")
    int insertProductInfo(ProductInfo productInfo);

    //添加商品描述信息
    @Insert("insert into product_descript(product_info_id,descript,store_info_id) " +
            " value(#{productInfoId},#{descript},#{storeInfoId})")
    @Options(useGeneratedKeys = true,keyProperty = "id",keyColumn = "id")
    int insertProductDescript(ProductDescript productDescript);

    //查询商品列表
    @Select("select i.*,d.descript,r.region_name placeOfOrigin " +
            "from product_info i " +
            "join product_descript d on i.product_info_id = d.product_info_id " +
            "join region r on i.region_code = r.region_code " +
            "order by product_info_id desc " +
            "limit #{start},#{pageSize}")
    List<ProductInfo> selectProductList(@Param("start")int start, @Param("pageSize") int pageSize);

    //商品总数
    @Select("select count(1) from product_info")
    int selectCount();

    //商品分组统计
    @Select("select t.region_code,count(1) as num " +
            "from product_info t " +
            "group by t.region_code having num > 1 " +
            "order by region_code ")
    List<Map> selectProductGroupList();

}

6、编写Service接口及其实现类

import com.shopping.entity.ProductInfo;
import java.util.List;

public interface ProductService {
    //添加商品
    public void createProduct(ProductInfo product);

    //查询商品
    public List<ProductInfo> queryProduct(int page, int pageSize);
}


import com.shopping.dao.ProductDao;
import com.shopping.entity.ProductDescript;
import com.shopping.entity.ProductInfo;
import com.shopping.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;

@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    ProductDao productDao;

    //添加商品,使用Sharding-JDBC支持分布式事务
@Override
@ShardingTransactionType(TransactionType.XA)
    @Transactional (rollbackFor = Exception.class)
    public void createProduct(ProductInfo productInfo) {
        ProductDescript productDescript =new ProductDescript();
        //设置商品描述信息
        productDescript.setDescript(productInfo.getDescript());
        //调用dao向商品信息表插入数据
        productDao.insertProductInfo(productInfo);
        //将商品信息id设置到productDescript
        productDescript.setProductInfoId(productInfo.getProductInfoId());
        //设置店铺id
        productDescript.setStoreInfoId(productInfo.getStoreInfoId());
        //向商品描述信息表插入数据
        productDao.insertProductDescript(productDescript);
    }

    @Override
    public List<ProductInfo> queryProduct(int page, int pageSize) {
        int start = (page - 1) * pageSize;
        return productDao.selectProductList(start,pageSize);
    }
}

7、编写测试类

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = ShoppingBootstrap.class)  // ShoppingBootstrap是springboot服务启动类
public class ShardingTest {
    @Autowired
    ProductService productService;

    @Autowired
    ProductDao productDao;

    //添加商品
    @Test
    public void testCreateProduct(){
        for (int i=0;i<10;i++){
            ProductInfo productInfo = new ProductInfo();
            //注意:分库根据storeInfoId的奇偶为分片策略
            productInfo.setStoreInfoId(2L);//店铺id

            productInfo.setProductName("Java编程思想"+i);//商品名称
            productInfo.setSpec("大号");
            productInfo.setPrice(new BigDecimal(60));
            productInfo.setRegionCode("110100");
            productInfo.setDescript("Java编程思想不错!!!"+i);//商品描述
            productService.createProduct(productInfo);
        }
}
// 分页查询商品
@Test
public void testQueryProduct(){
    List<ProductInfo> productInfos = productService.queryProduct(1, 10); 
    // List<ProductInfo> productInfos = productService.queryProduct(2, 2);
    System.out.println(productInfos);
}
// 统计商品总数
@Test
public void testSelectCount(){
    int i = productDao.selectCount();
    System.out.println(i);
}
// 分组统计商品数量
@Test
public void testSelectProductGroupList(){
    List<Map> maps = productDao.selectProductGroupList();
    System.out.println(maps);
}
}

执行添加商品的测试方法得到日志如下所示:

[main] ShardingSphere-SQL : Logic SQL: insert into product_info(store_info_id,product_name,spec,region_code,price)  values (?,?,?,?,?)
[main] ShardingSphere-SQL : SQLStatement: InsertStatement(super=DMLStatement(super=AbstractSQLStatement(type=DML, tables=Tables(tables=[Table(name=product_info, alias=Optional.absent())]), routeConditions=Conditions(orCondition=OrCondition(andConditions=[AndCondition(conditions=[Condition(column=Column(name=store_info_id, tableName=product_info), operator=EQUAL, compareOperator=null, positionValueMap={}, positionIndexMap={0=0})])])), encryptConditions=Conditions(orCondition=OrCondition(andConditions=[])), sqlTokens=[TableToken(tableName=product_info, quoteCharacter=NONE, schemaNameLength=0), SQLToken(startIndex=24)], parametersIndex=5, logicSQL=insert into product_info(store_info_id,product_name,spec,region_code,price)  values (?,?,?,?,?)), deleteStatement=false, updateTableAlias={}, updateColumnValues={}, whereStartIndex=0, whereStopIndex=0, whereParameterStartIndex=0, whereParameterEndIndex=0), columnNames=[store_info_id, product_name, spec, region_code, price], values=[InsertValue(columnValues=[org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@7df3da0b, org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@4e5d5ac1, org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@70777a65, org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@236fd411, org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@402f61f5])])
[main] ShardingSphere-SQL : Actual SQL: m1 ::: insert into product_info_2 (store_info_id, product_name, spec, region_code, price, product_info_id) VALUES (?, ?, ?, ?, ?, ?) ::: [2, Java编程思想0, 大号, 110100, 60, 1045832812957007873]
[main] ShardingSphere-SQL : Logic SQL: insert into product_descript(product_info_id,descript,store_info_id)  value(?,?,?)
[main] ShardingSphere-SQL : SQLStatement: InsertStatement(super=DMLStatement(super=AbstractSQLStatement(type=DML, tables=Tables(tables=[Table(name=product_descript, alias=Optional.absent())]), routeConditions=Conditions(orCondition=OrCondition(andConditions=[AndCondition(conditions=[Condition(column=Column(name=product_info_id, tableName=product_descript), operator=EQUAL, compareOperator=null, positionValueMap={}, positionIndexMap={0=0}), Condition(column=Column(name=store_info_id, tableName=product_descript), operator=EQUAL, compareOperator=null, positionValueMap={}, positionIndexMap={0=2})])])), encryptConditions=Conditions(orCondition=OrCondition(andConditions=[])), sqlTokens=[TableToken(tableName=product_descript, quoteCharacter=NONE, schemaNameLength=0), SQLToken(startIndex=28)], parametersIndex=3, logicSQL=insert into product_descript(product_info_id,descript,store_info_id)  value(?,?,?)), deleteStatement=false, updateTableAlias={}, updateColumnValues={}, whereStartIndex=0, whereStopIndex=0, whereParameterStartIndex=0, whereParameterEndIndex=0), columnNames=[product_info_id, descript, store_info_id], values=[InsertValue(columnValues=[org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@4c2bea52, org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@11015ca0, org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@6ebbc06])])
[main] ShardingSphere-SQL : Actual SQL: m1 ::: insert into product_descript_2 (product_info_id, descript, store_info_id, id) VALUES (?, ?, ?, ?) ::: [1045832812957007873, Java编程思想不错!!!0, 2, 1045832813284163585]
[main] ShardingSphere-SQL : Logic SQL: insert into product_info(store_info_id,product_name,spec,region_code,price)  values (?,?,?,?,?)
[main] ShardingSphere-SQL : SQLStatement: InsertStatement(super=DMLStatement(super=AbstractSQLStatement(type=DML, tables=Tables(tables=[Table(name=product_info, alias=Optional.absent())]), routeConditions=Conditions(orCondition=OrCondition(andConditions=[AndCondition(conditions=[Condition(column=Column(name=store_info_id, tableName=product_info), operator=EQUAL, compareOperator=null, positionValueMap={}, positionIndexMap={0=0})])])), encryptConditions=Conditions(orCondition=OrCondition(andConditions=[])), sqlTokens=[TableToken(tableName=product_info, quoteCharacter=NONE, schemaNameLength=0), SQLToken(startIndex=24)], parametersIndex=5, logicSQL=insert into product_info(store_info_id,product_name,spec,region_code,price)  values (?,?,?,?,?)), deleteStatement=false, updateTableAlias={}, updateColumnValues={}, whereStartIndex=0, whereStopIndex=0, whereParameterStartIndex=0, whereParameterEndIndex=0), columnNames=[store_info_id, product_name, spec, region_code, price], values=[InsertValue(columnValues=[org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@7df3da0b, org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@4e5d5ac1, org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@70777a65, org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@236fd411, org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@402f61f5])])
[main] ShardingSphere-SQL : Actual SQL: m1 ::: insert into product_info_1 (store_info_id, product_name, spec, region_code, price, product_info_id) VALUES (?, ?, ?, ?, ?, ?) ::: [2, Java编程思想1, 大号, 110100, 60, 1045832813321912320]
[main] ShardingSphere-SQL : Logic SQL: insert into product_descript(product_info_id,descript,store_info_id)  value(?,?,?)
[main] ShardingSphere-SQL : SQLStatement: InsertStatement(super=DMLStatement(super=AbstractSQLStatement(type=DML, tables=Tables(tables=[Table(name=product_descript, alias=Optional.absent())]), routeConditions=Conditions(orCondition=OrCondition(andConditions=[AndCondition(conditions=[Condition(column=Column(name=product_info_id, tableName=product_descript), operator=EQUAL, compareOperator=null, positionValueMap={}, positionIndexMap={0=0}), Condition(column=Column(name=store_info_id, tableName=product_descript), operator=EQUAL, compareOperator=null, positionValueMap={}, positionIndexMap={0=2})])])), encryptConditions=Conditions(orCondition=OrCondition(andConditions=[])), sqlTokens=[TableToken(tableName=product_descript, quoteCharacter=NONE, schemaNameLength=0), SQLToken(startIndex=28)], parametersIndex=3, logicSQL=insert into product_descript(product_info_id,descript,store_info_id)  value(?,?,?)), deleteStatement=false, updateTableAlias={}, updateColumnValues={}, whereStartIndex=0, whereStopIndex=0, whereParameterStartIndex=0, whereParameterEndIndex=0), columnNames=[product_info_id, descript, store_info_id], values=[InsertValue(columnValues=[org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@4c2bea52, org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@11015ca0, org.apache.shardingsphere.core.parse.old.parser.expression.SQLPlaceholderExpression@6ebbc06])])
[main] ShardingSphere-SQL : Actual SQL: m1 ::: insert into product_descript_1 (product_info_id, descript, store_info_id, id) VALUES (?, ?, ?, ?) ::: [1045832813321912320, Java编程思想不错!!!1, 2, 1045832813330300928]
[main] ShardingSphere-SQL : Rule Type: sharding

在添加商品的测试方法中,storeInfoId(店铺id)是2,根据配置文件中配置的水平分库策略可知,若store_info_id为偶数则操作ds1数据源,否则操作ds2数据源。ds1数据源对应的数据库是product_db_1库,ds2数据源对应的数据库是product_db_2库。ds1数据源包含m1和s1两个主从数据源,所以插入操作会将数据写入到m1数据源对应的数据库中。

另外根据配置文件中配置的水平分表策略可知,product_info 是逻辑表名,product_info_1 和 product_info_2 是真实表名,配置 product_info 表的分片策略为product_info_id % 2 + 1,若product_info_id为偶数,则将数据写入product_info_1表,否则写入product_info_2表。

根据上面的日志可知,当product_info_id为偶数时,将数据写入m1数据源的product_info_1表,当product_info_id为奇数时,将数据写入m1数据源的product_info_2表,符合预期需求。

执行分页查询商品的测试方法,得到的日志如下所示。

如果分页查询不是从第一页开始,假如从第二页开始,每页查询2条数据,即limit 2,4,但是Sharding-JDBC实际查询的范围是[0,4],如下图所示,可以发现Sharding-jdbc改写了sql,本来应该为limit ?,? ::: [2, 4]改写成了limit ?,? ::: [0, 4]。但其实Sharding-jdbc大部分情况都通过流式归并获取数据结果集,不会出现大量无意义的数据加载至内存中,造成内存溢出风险。

分页查询是业务中最常见的场景,Sharding-jdbc支持常用关系数据库的分页查询,不过Sharding-jdbc的分页功能比较容易让使用者误解,用户通常认为分页归并会占用大量内存。

在分布式的场景中,将 LIMIT 10000000 , 10改写为 LIMIT 0, 10000010 ,才能保证其数据的正确性。 用户非常容易产生ShardingSphere会将大量无意义的数据加载至内存中,造成内存溢出风险的错觉。 其实大部分情况都通过流式归并获取数据结果集,因此ShardingSphere会通过结果集的next方法将无需取出的数据全部跳过,并不会将其存入内存。

但同时需要注意的是,由于排序的需要,大量的数据仍然需要传输到Sharding-Jdbc的内存空间。 因此,采用LIMIT这种方式分页,并非最佳实践。 由于LIMIT并不能通过索引查询数据,因此如果可以保证ID的连续性(使用一个数据库表单独维护主键id),通过ID进行分页是比较好的解决方案,例如:

SELECT * FROM t_order WHERE id > 100000 AND id <= 100010 ORDER BY id;

或通过记录上次查询结果的最后一条记录的ID进行下一页的查询,例如:

SELECT * FROM t_order WHERE id > 10000000 LIMIT 10;

排序功能是由Sharding-jdbc的排序归并来完成,由于在SQL中存在 ORDER BY 语句,因此每个数据结果集自身是有序的,因此只需要将数据结果集当前游标指向的数据值进行排序即可。 这相当于对多个有序的数组进行排序,归并排序是最适合此场景的排序算法。

执行统计商品总数的测试方法得到如下日志信息,根据日志可知,从s1.product_info_1、s1.product_info_2、s2.product_info_1、s2.product_info_2四张表查询数据合并后返回总数10。

执行分组统计商品数量的测试方法得到如下日志信息。

分组统计也是业务中常见的场景,分组功能的实现由Sharding-jdbc分组归并完成。分组归并的情况最为复杂,它分为流式分组归并和内存分组归并。 流式分组归并要求SQL的排序项与分组项的字段必须保持一致(例如此处是根据region_code字段进行分组和排序的),否则只能通过内存归并才能保证其数据的正确性。内存分组归并比较消耗内存。

以下是Sharding-JDBC不支持的SQL格式及原因分析。

‌不支持的SQL类型‌

‌示例‌

‌不支持原因‌

‌VALUES中的运算表达式‌

INSERT INTO tbl_name VALUES(1+2, ?)

VALUES语句中不允许包含运算表达式

‌派生表(临时表)写法‌

SELECT t1.* FROM t1 JOIN (SELECT cid FROM course LIMIT 1) t2 ON...

无法正确识别逻辑表并进行分片路由

‌批量UPDATE操作‌

<foreach>批量UPDATE语句(如分表更新)

批量操作时无法正确代理到实际分表

‌原生复杂SQL(如CASE WHEN)‌

SELECT CASE WHEN... 或 包含子查询的复杂逻辑

需通过注解或配置排除复杂查询以避免分片路由错误

‌JDBC 4.1新增功能‌

如国际化字符操作、多结果集支持等

Sharding-JDBC未实现JDBC 4.1接口扩展功能

‌非SELECT的存储过程调用‌

直接执行存储过程(非SELECT结果集)

不支持存储过程、函数等非SELECT操作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值