一、前言 上篇介绍了数据库 分库分表 的基本概念以及演化过程,同时分库分表带来的繁琐问题。本篇就上篇的问题,来介绍和讲解这些问题的解决方案。
本篇的主角为 ShardingSphere-JDBC 。
二、基本介绍 Apache ShardingSphere 是一套开源的分布式数据库解决方案组成的生态圈,它由 JDBC、Proxy 和 Sidecar(规划中)这 3 款既能够独立部署,又支持混合部署配合使用的产品组成。 它们均提供标准化的数据水平扩展、分布式事务和分布式治理等功能,可适用于如 Java 同构、异构语言、云原生等各种多样化的应用场景。
ShardingSphere-JDBC 是 Apache ShardingSphere 中的子模块,定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。 它使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架。
其特点:
适用于任何基于 JDBC 的 ORM 框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template 或直接使用 JDBC。 支持任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP 等。 支持任意实现 JDBC 规范的数据库,目前支持 MySQL,Oracle,SQLServer,PostgreSQL 以及任何遵循 SQL92 标准的数据库。
ShardingSphere-JDBC 的核心功能为数据分片和读写分离。通过 ShardingSphere-JDBC ,应用可以透明的使用 JDBC 访问已经分库分表、读写分离的多个数据源,而不用关心数据源的数量以及数据如何分布。
2.1 功能列表 数据分片
分库 & 分表 读写分离 分片策略定制化 无中心化分布式主键 分布式事务
数据库治理
注意:ShardingSphere-JDBC 不是用于分库分表的工具,而是解决分库分表带来的路由、查询等问题。
2.2 核心概念 逻辑表: 水平拆分的数据库(表)的相同逻辑和数据结构表的总称。例:订单数据根据主键尾数拆分为 10 张表,分别是 t_order_0 到 t_order_9,他们的逻辑表名为 t_order。
真实表: 在分片的数据库中真实存在的物理表。即上个示例中的 t_order_0 到 t_order_9。
数据节点: 数据分片的最小单元。由数据源名称和数据表组成,例:ds_0.t_order_0。
绑定表: 指分片规则一致的主表和子表。例如:t_order 表和 t_order_item 表,均按照 order_id 分片,则此两张表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。
广播表: 指所有的分片数据源中都存在的表,表结构和表中的数据在每个数据库中均完全一致。适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表。
单表: 指所有的分片数据源中只存在唯一一张的表。适用于数据量不大且不需要做任何分片操作的场景。
分片键: 用于分片的数据库字段,是将数据库(表)水平拆分的关键字段。例:将订单表中的订单主键的尾数取模分片,则订单主键为分片字段。 SQL 中如果无分片字段,将执行全路由,性能较差。
分片算法: 通过分片算法将数据分片,支持通过 =
、>=
、<=
、>
、<
、BETWEEN
和 IN
分片。 分片算法需要应用方开发者自行实现,可实现的灵活度非常高。
分片策略: 包含分片键和分片算法,由于分片算法的独立性,将其独立抽离。真正可用于分片操作的是分片键 + 分片算法,也就是分片策略。
在 ShardingSphere 中,还定义了很多概念,上边只列举了其部分概念,主要用于下边实战演练配置中会提及的概念。如需了解更多内容,请查看文章末尾提供的参考资料。
三、实战演练 我们以水平分库为例,作为最简单的案例进行实战。
3.1 创建数据库和表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 CREATE TABLE `t_order_1` ( `order_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `user_id` BIGINT(20) NOT NULL DEFAULT '0' COMMENT '用户 id', `total_price` DECIMAL(10,2) NOT NULL DEFAULT '0.00' COMMENT '订单价格', `state` TINYINT(4) NOT NULL DEFAULT '0' COMMENT '订单状态', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`order_id`), INDEX `user_id` (`user_id`) ) COLLATE='utf8_general_ci' ENGINE=InnoDB ; CREATE TABLE `t_order_2` ( `order_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `user_id` BIGINT(20) NOT NULL DEFAULT '0' COMMENT '用户 id', `total_price` DECIMAL(10,2) NOT NULL DEFAULT '0.00' COMMENT '订单价格', `state` TINYINT(4) NOT NULL DEFAULT '0' COMMENT '订单状态', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`order_id`), INDEX `user_id` (`user_id`) ) COLLATE='utf8_general_ci' ENGINE=InnoDB ;
补充:t_order_1 和 t_order_2 为真实表,在后边的 SQL
编写中,我们以 t_order 作为逻辑表进行编写。
3.2 引入依赖 修改项目 pom.xml 文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.4</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- jdbc 驱动包 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.16</version> </dependency> <!-- sharding-jdbc --> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-boot-starter</artifactId> <version>4.1.1</version> </dependency>
3.3 配置分片规则 修改 application.properties 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 spring.application.name=sharding-jdbc mybatis.config-location=classpath:mybatis/mybatis-config.xml mybatis.mapper-locations=classpath:mybatis/mapper/*.xml # shardingsphere 配置开关,如果使用 java api 配置方式,需要修改为 false spring.shardingsphere.enabled=true # 定义数据源 spring.shardingsphere.datasource.names=m1 # 下边用到的 m1 就是上边定义的 spring.shardingsphere.datasource.m1.type=com.zaxxer.hikari.HikariDataSource spring.shardingsphere.datasource.m1.driver-class-name=com.mysql.jdbc.Driver spring.shardingsphere.datasource.m1.jdbc-url=jdbc:mysql://localhost:3306/db_order?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&rewriteBatchedStatements=true&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true spring.shardingsphere.datasource.m1.username=root spring.shardingsphere.datasource.m1.password=tiger # 下边 t_order 是逻辑表,真实查询对应真实的 t_order_1 和 t_order_2 # 指定 t_order 表的主键生成策略为 SNOWFLAKE,order_id 就是 t_order 表的主键 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.actual-data-nodes=m1.t_order_$->{1..2} # 指定 t_order 表的分片策略,分片策略包括分片键和分片算法 spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column=order_id # order_id 值为奇数插入到 t_order_2 表,为偶数插入到 t_order_1 表 spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression=t_order_$->{order_id%2+1} # 打开 sql 输出日志 spring.shardingsphere.props.sql.show = true logging.level.root=info logging.level.org.springframework.web=info logging.level.com.light.sharding.jdbc=debug
3.4 实现持久层 定义 Mapper 接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Mapper public interface OrderMapper { /** * 插入 * @param order */ void insert(Order order); /** * 批量查询 * @param idList * @return */ List<Order> selectListByIds(@Param("idList") List<Long> idList); }
Mapper 映射文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.light.sharding.jdbc.mapper.OrderMapper"> <insert id="insert" parameterType="com.light.sharding.jdbc.model.Order"> INSERT INTO t_order(user_id, total_price, state, create_time, update_time) VALUES(#{userId}, #{totalPrice}, #{state}, #{createTime}, #{updateTime}) </insert> <select id="selectListByIds" resultType="com.light.sharding.jdbc.model.Order"> SELECT order_id, user_id, total_price, state FROM t_order WHERE order_id IN <foreach collection="idList" item="id" open="(" separator="," close=")"> #{id} </foreach> </select> </mapper>
注意: SQL
的编写使用的是逻辑表 。
3.5 单元测试 测试插入操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @RunWith(SpringRunner.class) @SpringBootTest(classes = ShardingJdbcApplication.class) public class OrderMapperTest { @Autowired private OrderMapper orderMapper; @Test public void testInsert() { for (int i = 0 ; i < 10; i++){ Order order = new Order(); order.setUserId(1L); order.setState(0); order.setTotalPrice(new BigDecimal((i + 1) * 5)); order.setCreateTime(LocalDateTime.now()); order.setUpdateTime(order.getCreateTime()); this.orderMapper.insert(order); } } }
执行上边测试方法,最终结果如下图:
通过上图可知,插入的数据结果符合配置规则。
测试查询操作:
1 2 3 4 5 6 @Test public void testSelectList() { List<Long> idList = Arrays.asList(593141944457101312L, 593141944922669057L); List<Order> orderList = this.orderMapper.selectListByIds(idList); System.out.println(orderList); }
我们从 t_order_1 和 t_order_2 中各拿一条数据进行查询测试,执行结果如下图:
通过日志可知,ShardingSphere-JDBC 向两张表都发送 SQL 进行查询。
3.6 java api 配置 该小节作为配置方式的补充内容,如果不喜欢使用上文的配置文件方式,可采用如下配置:
创建配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 @Configuration public class ShardingJdbcConfig { // sharding‐Jdbc 数据源 @Bean public DataSource getShardingDataSource() throws SQLException { // 定义数据源,此处我们定义数据源名称为 m2 HikariDataSource dataSource = new HikariDataSource(); dataSource.setDriverClassName("com.mysql.jdbc.Driver"); dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/db_order?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&rewriteBatchedStatements=true&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true"); dataSource.setUsername("root"); dataSource.setPassword("tiger"); Map<String, DataSource> dataSourceMap = new HashMap<>(); dataSourceMap.put("m2", dataSource); // 分片规则 ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration(); shardingRuleConfig.getTableRuleConfigs().add(getOrderTableRuleConfiguration()); // 其他配置 Properties properties = new Properties(); properties.put("sql.show","true"); return ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig, properties); } // 分片规则 private TableRuleConfiguration getOrderTableRuleConfiguration() { // 指定 t_order 表的数据分布情况,配置数据节点 TableRuleConfiguration result = new TableRuleConfiguration("t_order","m2.t_order_$->{1..2}"); // 指定 t_order 表的分片策略,分片策略包括分片键和分片算法 result.setTableShardingStrategyConfig(new InlineShardingStrategyConfiguration("order_id", "t_order_$->{order_id%2+1}")); result.setKeyGeneratorConfig(getKeyGeneratorConfiguration()); return result; } // 定义主键生成策略 private static KeyGeneratorConfiguration getKeyGeneratorConfiguration() { return new KeyGeneratorConfiguration("SNOWFLAKE","order_id"); } }
我们定义数据源名称为 m2 。
关闭文件配置,修改 application.properties 文件:
1 spring.shardingsphere.enabled=false
执行查询,结果如下图:
图中日志可知,使用了名为 m2 的数据源,请求查询成功。
四、执行流程 当 Sharding-JDBC 接受到一条 SQL 语句时,会陆续执行 SQL解析
=> 查询优化
=> SQL路由
=> SQL改写
=> SQL执行
=> 结果归并
,最终返回执行结果。
4.1 SQL解析 解析过程分为词法解析和语法解析。 词法解析器用于将 SQL 拆解为不可再分的原子符号,称为 Token 。并根据不同数据库方言所提供的字典,将其归类为关键字,表达式,字面量和操作符。 再使用语法解析器将词法解析器的输出转换为抽象语法树。
例如,以下 SQL 为例:
1 SELECT id, name FROM t_user WHERE status = 'ACTIVE' AND age > 18
为了便于理解,抽象语法树中的关键字的 Token 用绿色表示,变量的 Token 用红色表示,灰色表示需要进一步拆分。
最后,通过 visitor 对抽象语法树遍历构造域模型,通过域模型(SQLStatement)去提炼分片所需的上下文,并标记有可能需要改写的位置。 供分片使用的解析上下文包含查询选择项(Select Items)、表信息(Table)、分片条件(Sharding Condition)、自增主键信息(Auto increment Primary Key)、排序信息(Order By)、分组信息(Group By)以及分页信息(Limit、Rownum、Top)。 SQL 的一次解析过程是不可逆的,一个个 Token 按 SQL 原本的顺序依次进行解析,性能很高。 考虑到各种数据库 SQL 方言的异同,在解析模块提供了各类数据库的 SQL 方言字典。
4.2 查询优化 合并和优化分片条件,如 OR 等。
4.3 SQL路由 根据解析上下文匹配用户配置的分片策略,并生成路由路径。 对于携带分片键的 SQL ,根据分片键的不同可以划分为单片路由(分片键的操作符是等号)、多片路由(分片键的操作符是 IN )和范围路由(分片键的操作符是 BETWEEN )。 不携带分片键的 SQL 则采用广播路由。
4.4 SQL改写 我们面向逻辑库与逻辑表书写的 SQL ,并不能够直接在真实的数据库中执行,SQL 改写用于将逻辑 SQL 改写为在真实数据库中可以正确执行的 SQL 。 它包括正确性改写和优化改写两部分。
以下列为例,若逻辑 SQL 为:
1 SELECT order_id FROM t_order WHERE order_id = 1;
假设该 SQL 配置分片键 order_id,并且 order_id=1 的情况,将路由至分片表1。那么改写之后的 SQL 应该为:
1 SELECT order_id FROM t_order_1 WHERE order_id = 1;
4.5 SQL执行 ShardingSphere 采用一套自动化的执行引擎,负责将路由和改写完成之后的真实 SQL 安全且高效发送到底层数据源执行。 它不是简单地将 SQL 通过 JDBC 直接发送至数据源执行;也并非直接将执行请求放入线程池去并发执行。它更关注平衡数据源连接创建以及内存占用所产生的消耗,以及最大限度地合理利用并发等问题。
4.6 结果归并 将从各个数据节点获取的多数据结果集,组合成为一个结果集并正确的返回至请求客户端,称为结果归并。
ShardingSphere 支持的结果归并从功能上分为遍历、排序、分组、分页和聚合 5 种类型,它们是组合而非互斥的关系。 从结构划分,可分为流式归并、内存归并和装饰者归并。流式归并和内存归并是互斥的,装饰者归并可以在流式归并和内存归并之上做进一步的处理。
五、参考资料 ShardingSphere 官网