一、ShardingSphere

image.png
ShardingSphere是一款起源于当当网内部的应用框架。2015年在当当网内部诞生,最初就叫ShardingJDBC。2016年的时候,由其中一个主要的开发人员张亮,带入到京东数科,组件团队继续开发。在国内历经了当当网、电信翼支付、京东数科等多家大型互联网企业的考验,在2017年开始开源。并逐渐由原本只关注于关系型数据库增强工具的ShardingJDBC升级成为一整套以数据分片为基础的数据生态圈,更名为ShardingSphere。到2020年4月,已经成为了Apache软件基金会的顶级项目。
ShardingSphere包含三个重要的产品,ShardingJDBC、ShardingProxy和ShardingSidecar。其中sidecar是针对service mesh定位的一个分库分表插件,目前在规划中。而我们今天学习的重点是ShardingSphere的JDBC和Proxy这两个组件。
其中,ShardingJDBC是用来做客户端分库分表的产品,而ShardingProxy是用来做服务端分库分表的产品。这两者定位有什么区别呢?我们看下官方资料中给出的两个重要的图:
ShardingJDBC:
image.png
shardingJDBC定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。它使⽤客户端直连数据库,以 jar 包形式提供服务,⽆需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架。
ShardingProxy
image.png
ShardingProxy定位为透明化的数据库代理端,提供封装了数据库⼆进制协议的服务端版本,⽤于完成对异构语⾔的⽀持。⽬前提供 MySQL 和 PostgreSQL 版本,它可以使⽤任何兼容 MySQL/PostgreSQL 协议的访问客⼾端。
那这两种方式有什么区别呢?

Sharding-JDBC Sharding-Proxy
数据库 任意 MySQL/PostgreSQL
连接消耗数
异构语言 仅java 任意
性能 损耗低 损耗略高
无中心化
静态入口

很显然,ShardingJDBC只是客户端的一个工具包,可以理解为一个特殊的JDBC驱动包,所有分库分表逻辑均由业务方自己控制,所以他的功能相对灵活,支持的数据库也非常多,但是对业务侵入大,需要业务方自己定制所有的分库分表逻辑。而ShardingProxy是一个独立部署的服务,对业务方无侵入,业务方可以像用一个普通的MySQL服务一样进行数据交互,基本上感觉不到后端分库分表逻辑的存在,但是这也意味着功能会比较固定,能够支持的数据库也比较少。这两者各有优劣。

二、ShardingJDBC实战

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

1、核心概念:

  • 逻辑表:水平拆分的数据库的相同逻辑和数据结构表的总称
  • 真实表:在分片的数据库中真实存在的物理表。
  • 数据节点:数据分片的最小单元。由数据源名称和数据表组成
  • 绑定表:分片规则一致的主表和子表。
  • 广播表:也叫公共表,指素有的分片数据源中都存在的表,表结构和表中的数据在每个数据库中都完全一致。例如字典表。
  • 分片键:用于分片的数据库字段,是将数据库(表)进行水平拆分的关键字段。SQL中若没有分片字段,将会执行全路由,性能会很差。
  • 分片算法:通过分片算法将数据进行分片,支持通过=、BETWEEN和IN分片。分片算法需要由应用开发者自行实现,可实现的灵活度非常高。
  • 分片策略:真正用于进行分片操作的是分片键+分片算法,也就是分片策略。在ShardingJDBC中一般采用基于Groovy表达式的inline分片策略,通过一个包含分片键的算法表达式来制定分片策略,如tuser$->{u_id%8}标识根据u_id模8,分成8张表,表名称为t_user_0到t_user_7。

    2、测试项目介绍

    测试项目参见配套的ShardingDemo项。首先我们对测试项目的结构做下简单的梳理:
    image.png
    注:1、引入MyBatisPlus依赖,简化JDBC操作,这样我们就不需要在代码中写SQL语句了。
    2、entity中的实体对象就对应数据库中的表结构。而mapper中的接口则对应JDBC操作。
    3、所有操作均使用JUnit的测试案例执行。 后续所有测试操作都会配合application.properties中的配置以及JUnit测试案例进行。
    4、关于ShardingSphere版本,由于目前最新的5.0版本还在孵化当中,所以我们使用已发布的4.1.1版本来进行学习。

    3、快速实战

    我们先运行一个简单的实例,来看下ShardingJDBC是如何工作的。
    3.1、在application.properties配置文件中写入application01.properties文件的内容来测试一下分表: ```java

    垂直分表策略

    配置真实数据源

    spring.shardingsphere.datasource.names=m1

配置第 1 个数据源

spring.shardingsphere.datasource.m1.type=com.alibaba.druid.pool.DruidDataSource spring.shardingsphere.datasource.m1.driver-class-name=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.m1.url=jdbc:mysql://localhost:3306/coursedb?serverTimezone=GMT%2B8 spring.shardingsphere.datasource.m1.username=root spring.shardingsphere.datasource.m1.password=root

指定表的分布情况 配置表在哪个数据库里,表名是什么。水平分表,分两个表:m1.course_1,m1.course_2

spring.shardingsphere.sharding.tables.course.actual-data-nodes=m1.course_$->{1..2}

指定表的主键生成策略

spring.shardingsphere.sharding.tables.course.key-generator.column=cid spring.shardingsphere.sharding.tables.course.key-generator.type=SNOWFLAKE

雪花算法的一个可选参数

spring.shardingsphere.sharding.tables.course.key-generator.props.worker.id=1

使用自定义的主键生成策略

spring.shardingsphere.sharding.tables.course.key-generator.type=MYKEY

spring.shardingsphere.sharding.tables.course.key-generator.props.mykey-offset=88

指定分片策略 约定cid值为偶数添加到course_1表。如果是奇数添加到course_2表。

选定计算的字段

spring.shardingsphere.sharding.tables.course.table-strategy.inline.sharding-column= cid

根据计算的字段算出对应的表名。

spring.shardingsphere.sharding.tables.course.table-strategy.inline.algorithm-expression=course_$->{cid%2+1}

打开sql日志输出。

spring.shardingsphere.props.sql.show=true

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

  1. 1、首先定义一个数据源m1,并对m1进行实际的JDBC参数配置<br />2spring.shardingsphere.sharding.tables.course开头的一系列属性即定义了一个名为course的逻辑表。<br />actual-data-nodes属性即定义course逻辑表的实际数据分布情况,他分布在m1.course_1m1.course_2两个表。<br />key-generator属性配置了他的主键列以及主键生成策略。ShardingJDBC默认提供了UUIDSNOWFLAKE两种分布式主键生成策略。<br />table-strategy属性即配置他的分库分表策略。分片键为cid属性。分片算法为course_$->{cid%2+1},表示按照cid2+1的结果,然后加上前面的course__ 部分作为前缀就是他的实际表结果。注意,这个表达式计算出来的结果需要能够与实际数据分布中的一种情况对应上,否则就会报错。<br />sql.show属性表示要在日志中打印实际SQL<br />3coursedb的表结构见示例中sql文件夹中的sql语句。<br />然后我们执行测试案例中的addcourse案例。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22260675/1630725100055-0a2aded9-a85c-4796-81a8-3e1042b6314c.png#clientId=uc096723d-ad55-4&from=paste&id=u7c68edf3&margin=%5Bobject%20Object%5D&name=image.png&originHeight=291&originWidth=971&originalType=url&ratio=1&size=57798&status=done&style=none&taskId=uc1bebe38-bd97-4479-bc46-bc965ccbc3e)<br />执行后,我们可以在控制台看到很多条这样的日志:
  2. ```java
  3. .......
  4. 2020-12-15 18:35:16.426 INFO 22412 --- [ main] ShardingSphere-SQL : Logic SQL: INSERT INTO course ( cname,
  5. user_id,
  6. cstatus ) VALUES ( ?,
  7. ?,
  8. ? )
  9. 2020-12-15 18:35:16.427 INFO 22412 --- [ main] ShardingSphere-SQL : SQLStatement: InsertStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.InsertStatement@1cbc5693, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@124d26ba), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@124d26ba, columnNames=[cname, user_id, cstatus], insertValueContexts=[InsertValueContext(parametersCount=3, valueExpressions=[ParameterMarkerExpressionSegment(startIndex=59, stopIndex=59, parameterMarkerIndex=0), ParameterMarkerExpressionSegment(startIndex=62, stopIndex=62, parameterMarkerIndex=1), ParameterMarkerExpressionSegment(startIndex=65, stopIndex=65, parameterMarkerIndex=2), DerivedParameterMarkerExpressionSegment(super=ParameterMarkerExpressionSegment(startIndex=0, stopIndex=0, parameterMarkerIndex=3))], parameters=[java, 1001, 1])], generatedKeyContext=Optional[GeneratedKeyContext(columnName=cid, generated=true, generatedValues=[545674405561237505])])
  10. 2020-12-15 18:35:16.427 INFO 22412 --- [ main] ShardingSphere-SQL : Actual SQL: m1 ::: INSERT INTO course_2 ( cname,
  11. user_id,
  12. cstatus , cid) VALUES (?, ?, ?, ?) ::: [java, 1001, 1, 545674405561237505]
  13. .....

从这个日志中我们可以看到,程序中执行的Logic SQL经过ShardingJDBC处理后,被转换成了Actual SQL往数据库里执行。执行的结果可以在MySQL中看到,course_1和course_2两个表中各插入了五条消息。这就是ShardingJDBC帮我们进行的数据库的分库分表操作。
image.png
然后,其他的几个配置文件依次对应了其他几种分库分表策略,我们可以一一演示一下。
3.2、application02.properties: 分库分表示例配置。内置分片算法示例, inline、standard、complex、hint。广播表配置示例。
注意要创建另外一个数据库m2。

  1. #配置多个数据源
  2. spring.shardingsphere.datasource.names=m1,m2
  3. spring.shardingsphere.datasource.m1.type=com.alibaba.druid.pool.DruidDataSource
  4. spring.shardingsphere.datasource.m1.driver-class-name=com.mysql.cj.jdbc.Driver
  5. spring.shardingsphere.datasource.m1.url=jdbc:mysql://localhost:3306/coursedb?serverTimezone=GMT%2B8
  6. spring.shardingsphere.datasource.m1.username=root
  7. spring.shardingsphere.datasource.m1.password=root
  8. spring.shardingsphere.datasource.m2.type=com.alibaba.druid.pool.DruidDataSource
  9. spring.shardingsphere.datasource.m2.driver-class-name=com.mysql.cj.jdbc.Driver
  10. spring.shardingsphere.datasource.m2.url=jdbc:mysql://localhost:3306/coursedb2?serverTimezone=GMT%2B8
  11. spring.shardingsphere.datasource.m2.username=root
  12. spring.shardingsphere.datasource.m2.password=root
  13. #真实表分布,分库,分表
  14. spring.shardingsphere.sharding.tables.course.actual-data-nodes=m$->{1..2}.course_$->{1..2}
  15. spring.shardingsphere.sharding.tables.course.key-generator.column=cid
  16. spring.shardingsphere.sharding.tables.course.key-generator.type=SNOWFLAKE
  17. spring.shardingsphere.sharding.tables.course.key-generator.props.worker.id=1
  18. #inline分片策略
  19. #spring.shardingsphere.sharding.tables.course.table-strategy.inline.sharding-column=cid
  20. #spring.shardingsphere.sharding.tables.course.table-strategy.inline.algorithm-expression=course_$->{cid%2+1}
  21. #spring.shardingsphere.sharding.tables.course.database-strategy.inline.sharding-column=cid
  22. #spring.shardingsphere.sharding.tables.course.database-strategy.inline.algorithm-expression=m$->{cid%2+1}
  23. #standard标准分片策略
  24. #spring.shardingsphere.sharding.tables.course.table-strategy.standard.sharding-column=cid
  25. #spring.shardingsphere.sharding.tables.course.table-strategy.standard.precise-algorithm-class-name=com.roy.shardingDemo.algorithem.MyPreciseTableShardingAlgorithm
  26. #spring.shardingsphere.sharding.tables.course.table-strategy.standard.range-algorithm-class-name=com.roy.shardingDemo.algorithem.MyRangeTableShardingAlgorithm
  27. spring.shardingsphere.sharding.tables.course.database-strategy.standard.sharding-column=cid
  28. spring.shardingsphere.sharding.tables.course.database-strategy.standard.precise-algorithm-class-name=com.roy.shardingDemo.algorithem.MyPreciseDSShardingAlgorithm
  29. spring.shardingsphere.sharding.tables.course.database-strategy.standard.range-algorithm-class-name=com.roy.shardingDemo.algorithem.MyRangeDSShardingAlgorithm
  30. #complex复杂分片策略
  31. #spring.shardingsphere.sharding.tables.course.table-strategy.complex.sharding-columns= cid, user_id
  32. #spring.shardingsphere.sharding.tables.course.table-strategy.complex.algorithm-class-name=com.roy.shardingDemo.algorithem.MyComplexTableShardingAlgorithm
  33. #
  34. #spring.shardingsphere.sharding.tables.course.database-strategy.complex.sharding-columns=cid, user_id
  35. #spring.shardingsphere.sharding.tables.course.database-strategy.complex.algorithm-class-name=com.roy.shardingDemo.algorithem.MyComplexDSShardingAlgorithm
  36. #hint强制路由策略
  37. spring.shardingsphere.sharding.tables.course.table-strategy.hint.algorithm-class-name=com.roy.shardingDemo.algorithem.MyHintTableShardingAlgorithm
  38. #广播表配置
  39. spring.shardingsphere.sharding.broadcast-tables=t_dict
  40. spring.shardingsphere.sharding.tables.t_dict.key-generator.column=dict_id
  41. spring.shardingsphere.sharding.tables.t_dict.key-generator.type=SNOWFLAKE
  42. spring.shardingsphere.props.sql.show = true
  43. spring.main.allow-bean-definition-overriding=true

然后同样执行上面的代码

  1. @Test
  2. public void addCourse(){
  3. for(int i = 0 ; i < 10 ; i ++){
  4. Course c = new Course();
  5. // c.setCid(Long.valueOf(i));
  6. c.setCname("shardingsphere");
  7. c.setUserId(Long.valueOf(""+(1000+i)));
  8. c.setCstatus("1");
  9. courseMapper.insert(c);
  10. }
  11. }

此时数据会按分片策略算法分别插入到m1、m2的数据库表中。
执行以下语句测试一下查找:

  1. @Test
  2. public void queryCourse(){
  3. //select * from course
  4. QueryWrapper<Course> wrapper = new QueryWrapper<>();
  5. wrapper.orderByDesc("cid");
  6. // wrapper.eq("cid",553684818806706177L);
  7. // wrapper.in()
  8. List<Course> courses = courseMapper.selectList(wrapper);
  9. courses.forEach(course -> System.out.println(course));
  10. }

3.3、application03.properties: 绑定表示例配置

  1. spring.shardingsphere.datasource.names=m1
  2. spring.shardingsphere.datasource.m1.type=com.alibaba.druid.pool.DruidDataSource
  3. spring.shardingsphere.datasource.m1.driver-class-name=com.mysql.cj.jdbc.Driver
  4. spring.shardingsphere.datasource.m1.url=jdbc:mysql://localhost:3306/coursedb?serverTimezone=GMT%2B8
  5. spring.shardingsphere.datasource.m1.username=root
  6. spring.shardingsphere.datasource.m1.password=root
  7. spring.shardingsphere.sharding.tables.t_dict.actual-data-nodes=m1.t_dict_$->{1..2}
  8. spring.shardingsphere.sharding.tables.t_dict.key-generator.column=dict_id
  9. spring.shardingsphere.sharding.tables.t_dict.key-generator.type=SNOWFLAKE
  10. spring.shardingsphere.sharding.tables.t_dict.key-generator.props.worker.id=1
  11. spring.shardingsphere.sharding.tables.t_dict.table-strategy.inline.sharding-column=ustatus
  12. spring.shardingsphere.sharding.tables.t_dict.table-strategy.inline.algorithm-expression=t_dict_$->{ustatus.toInteger()%2+1}
  13. spring.shardingsphere.sharding.tables.user.actual-data-nodes=m1.t_user_$->{1..2}
  14. spring.shardingsphere.sharding.tables.user.key-generator.column=user_id
  15. spring.shardingsphere.sharding.tables.user.key-generator.type=SNOWFLAKE
  16. spring.shardingsphere.sharding.tables.user.key-generator.props.worker.id=1
  17. spring.shardingsphere.sharding.tables.user.table-strategy.inline.sharding-column=ustatus
  18. spring.shardingsphere.sharding.tables.user.table-strategy.inline.algorithm-expression=t_user_$->{ustatus.toInteger()%2+1}
  19. #绑定表示例
  20. spring.shardingsphere.sharding.binding-tables[0]=user,t_dict
  21. spring.shardingsphere.props.sql.show = true
  22. spring.main.allow-bean-definition-overriding=true

3.4、application04.properties: 读写分离示例配置
要注意理解在读写分离策略中,ShardingJDBC只能帮我们把读写操作分发到不同的数据库上,而数据库之间的数据同步,还是需要由MySQL主从集群来完成。

  1. #配置主从数据源,要基于MySQL主从架构。
  2. spring.shardingsphere.datasource.names=m0,s0
  3. spring.shardingsphere.datasource.m0.type=com.alibaba.druid.pool.DruidDataSource
  4. spring.shardingsphere.datasource.m0.driver-class-name=com.mysql.cj.jdbc.Driver
  5. spring.shardingsphere.datasource.m0.url=jdbc:mysql://localhost:3307/masterdemo?serverTimezone=GMT%2B8
  6. spring.shardingsphere.datasource.m0.username=root
  7. spring.shardingsphere.datasource.m0.password=root
  8. spring.shardingsphere.datasource.s0.type=com.alibaba.druid.pool.DruidDataSource
  9. spring.shardingsphere.datasource.s0.driver-class-name=com.mysql.cj.jdbc.Driver
  10. spring.shardingsphere.datasource.s0.url=jdbc:mysql://localhost:3308/masterdemo?serverTimezone=GMT%2B8
  11. spring.shardingsphere.datasource.s0.username=root
  12. spring.shardingsphere.datasource.s0.password=root
  13. #读写分离规则, m0 主库,s0 从库
  14. spring.shardingsphere.sharding.master-slave-rules.ds0.master-data-source-name=m0
  15. spring.shardingsphere.sharding.master-slave-rules.ds0.slave-data-source-names[0]=s0
  16. #基于读写分离的表分片
  17. spring.shardingsphere.sharding.tables.t_dict.actual-data-nodes=ds0.t_dict
  18. spring.shardingsphere.sharding.tables.t_dict.key-generator.column=dict_id
  19. spring.shardingsphere.sharding.tables.t_dict.key-generator.type=SNOWFLAKE
  20. spring.shardingsphere.sharding.tables.t_dict.key-generator.props.worker.id=1
  21. spring.shardingsphere.props.sql.show = true
  22. spring.main.allow-bean-definition-overriding=true

4、ShardingJDBC的分片算法

ShardingJDBC的整个实战完成后,可以看到,整个分库分表的核心就是在于配置的分片算法。我们的这些实战都是使用的inline分片算法,即提供一个分片键和一个分片表达式来制定分片算法。这种方式配置简单,功能灵活,是分库分表最佳的配置方式,并且对于绝大多数的分库分片场景来说,都已经非常好用了。但是,如果针对一些更为复杂的分片策略,例如多分片键、按范围分片等场景,inline分片算法就有点力不从心了。所以,我们还需要学习下ShardingSphere提供的其他几种分片策略。
ShardingSphere目前提供了一共五种分片策略:

  • NoneShardingStrategy不分片。这种严格来说不算是一种分片策略了。只是ShardingSphere也提供了这么一个配置。
  • InlineShardingStrategy最常用的分片方式
    • 配置参数: inline.shardingColumn 分片键;inline.algorithmExpression 分片表达式
    • 实现方式: 按照分片表达式来进行分片。
  • StandardShardingStrategy只支持单分片键的标准分片策略。
    • 配置参数:standard.sharding-column 分片键;standard.precise-algorithm-class-name 精确分片算法类名;standard.range-algorithm-class-name 范围分片算法类名
    • 实现方式:shardingColumn指定分片算法。preciseAlgorithmClassName 指向一个实现了io.shardingsphere.api.algorithm.sharding.standard.PreciseShardingAlgorithm接口的java类名,提供按照 = 或者 IN 逻辑的精确分片 示例:com.roy.shardingDemo.algorithm.MyPreciseShardingAlgorithmrangeAlgorithmClassName 指向一个实现了 io.shardingsphere.api.algorithm.sharding.standard.RangeShardingAlgorithm接口的java类名,提供按照Between 条件进行的范围分片。示例:com.roy.shardingDemo.algorithm.MyRangeShardingAlgorithm
    • 说明:其中精确分片算法是必须提供的,而范围分片算法则是可选的。
  • ComplexShardingStrategy支持多分片键的复杂分片策略。
    • 配置参数:complex.sharding-columns 分片键(多个); complex.algorithm-class-name 分片算法实现类。
    • 实现方式:shardingColumn指定多个分片列。algorithmClassName指向一个实现了org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingAlgorithm接口的java类名。提供按照多个分片列进行综合分片的算法。示例:com.roy.shardingDemo.algorithm.MyComplexKeysShardingAlgorithm
  • HintShardingStrategy不需要分片键的强制分片策略。这个分片策略,简单来理解就是说,他的分片键不再跟SQL语句相关联,而是用程序另行指定。对于一些复杂的情况,例如select count() from (select userid from tuser where userid in (1,3,5,7,9)) 这样的SQL语句,就没法通过SQL语句来指定一个分片键。这个时候就可以通过程序,给他另行执行一个分片键,例如在按userid奇偶分片的策略下,可以指定1作为分片键,然后自行指定他的分片策略。而Hint分片策略并没有完全按照SQL解析树来构建分片策略,是绕开了SQL解析的,所有对某些比较复杂的语句,Hint分片策略性能有可能会比较好(情况太多了,无法一一分析)。但是要注意,Hint强制路由在使用时有非常多的限制:— 不支持UNION_ SELECT FROM torder1 UNION SELECT FROM t_order2 INSERT INTO tbl_name (col1, col2, …) SELECT col1, col2, … FROM tbl_name *WHERE col3 = ? — 不支持多层子查询 SELECT COUNT() FROM (SELECT FROM t_order o WHERE o.id IN (SELECT id FROM t_order WHERE status = ?)) — 不支持函数计算。ShardingSphere只能通过SQL字面提取用于分片的值_ SELECT FROM t_order WHERE *to_date(create_time, ‘yyyy-mm-dd’) = ‘2019-01-01’;
    • 配置参数:hint.algorithm-class-name 分片算法实现类。
    • 实现方式:algorithmClassName指向一个实现了org.apache.shardingsphere.api.sharding.hint.HintShardingAlgorithm接口的java类名。 示例:com.roy.shardingDemo.algorithm.MyHintShardingAlgorithm在这个算法类中,同样是需要分片键的。而分片键的指定是通过HintManager.addDatabaseShardingValue方法(分库)和HintManager.addTableShardingValue(分表)来指定。使用时要注意,这个分片键是线程隔离的,只在当前线程有效,所以通常建议使用之后立即关闭,或者用try资源方式打开。

示例详见application02.properties配置。
从这里也能看出,即便有了ShardingSphere框架,分库分表后对于SQL语句的支持依然是非常脆弱的。