Mysql使用Sharding-JDBC配置教程
作者:赵广陆
1 需求说明
使用Sharding-JDBC完成对订单表的水平分表,通过快速入门程序的开发,快速体验Sharding-JDBC的使用方法。
人工创建两张表,t_order_1和t_order_2,这两张表是订单表拆分后的表,通过Sharding-Jdbc向订单表插入数据,按照一定的分片规则,主键为偶数的进入t_order_1,另一部分数据进入t_order_2,通过Sharding-Jdbc 查询数据,根据 SQL语句的内容从t_order_1或t_order_2查询数据。
2 环境搭建
2.1 环境说明
- 操作系统: 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
2.2 创建数据库
创建订单库 order_db
CREATE DATABASE `order_db` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
在order_db中创建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.3.引入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>
具体spring boot相关依赖及配置请参考资料中dbsharding/sharding-jdbc-simple工程,本指引只说明与Sharding-JDBC相关的内容。
3 编写程序
3.1 分片规则配置
分片规则配置是sharding-jdbc进行对分库分表操作的重要依据,配置内容包括:数据源、主键生成策略、分片策略等。
在application.properties中配置
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 # 以下是分片规则配置 # 定义数据源 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/order_db?useUnicode=true spring.shardingsphere.datasource.m1.username = root spring.shardingsphere.datasource.m1.password = root # 指定t_order表的数据分布情况,配置数据节点 spring.shardingsphere.sharding.tables.t_order.actual‐data‐nodes = m1.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
- 首先定义数据源m1,并对m1进行实际的参数配置。
- 指定t_order表的数据分布情况,他分布在m1.t_order_1,m1.t_order_2
- 指定t_order表的主键生成策略为SNOWFLAKE,SNOWFLAKE是一种分布式自增算法,保证id全局唯一
- 定义t_order分片策略,order_id为偶数的数据落在t_order_1,为奇数的落在t_order_2,分表策略的表达式为t_order_$->{order_id % 2 + 1}
3.2. 数据操作
@Mapper @Component public interface OrderDao { /** * 新增订单 * @param price 订单价格 * @param userId 用户id * @param status 订单状态 * @return */ @Insert("insert into t_order(price,user_id,status) value(#{price},#{userId},#{status})") int insertOrder(@Param("price") BigDecimal price, @Param("userId")Long userId, @Param("status")String status); /** * 根据id列表查询多个订单 * @param orderIds 订单id列表 * @return */ @Select({"<script>" + "select " + " * " + " from t_order t" + " where t.order_id in " + "<foreach collection='orderIds' item='id' open='(' separator=',' close=')'>" + " #{id} " + "</foreach>"+ "</script>"}) List<Map> selectOrderbyIds(@Param("orderIds")List<Long> orderIds); }
3.3 测试
编写单元测试:
@RunWith(SpringRunner.class) @SpringBootTest(classes = {ShardingJdbcSimpleDemoBootstrap.class}) public class OrderDaoTest { @Autowired private OrderDao orderDao; @Test public void testInsertOrder(){ for (int i = 0 ; i<10; i++){ orderDao.insertOrder(new BigDecimal((i+1)*5),1L,"WAIT_PAY"); } } @Test public void testSelectOrderbyIds(){ List<Long> ids = new ArrayList<>(); ids.add(373771636085620736L); ids.add(373771635804602369L); List<Map> maps = orderDao.selectOrderbyIds(ids); System.out.println(maps); } }
执行 testInsertOrder:
通过日志可以发现order_id为奇数的被插入到t_order_2表,为偶数的被插入到t_order_1表,达到预期目标。
执行testSelectOrderbyIds:
通过日志可以发现,根据传入order_id的奇偶不同,sharding-jdbc分别去不同的表检索数据,达到预期目标。
4 流程分析
通过日志分析,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的结果进行汇总合并,返回。
5 其他集成方式
Sharding-JDBC不仅可以与spring boot良好集成,它还支持其他配置方式,共支持以下四种集成方式。
5.1 Spring Boot Yaml 配置
定义application.yml,内容如下:
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/order_db?useUnicode=true username: root password: mysql 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
如果使用 application.yml则需要屏蔽原来的application.properties文件。
5.2 Java 配置
添加配置类:
@Configuration public class ShardingJdbcConfig { // 定义数据源 Map<String, DataSource> createDataSourceMap() { DruidDataSource dataSource1 = new DruidDataSource(); dataSource1.setDriverClassName("com.mysql.jdbc.Driver"); dataSource1.setUrl("jdbc:mysql://localhost:3306/order_db?useUnicode=true"); dataSource1.setUsername("root"); dataSource1.setPassword("root"); Map<String, DataSource> result = new HashMap<>(); result.put("m1", dataSource1); return result; } // 定义主键生成策略 private static KeyGeneratorConfiguration getKeyGeneratorConfiguration() { KeyGeneratorConfiguration result = new KeyGeneratorConfiguration("SNOWFLAKE","order_id"); return result; } // 定义t_order表的分片策略 TableRuleConfiguration getOrderTableRuleConfiguration() { TableRuleConfiguration result = new TableRuleConfiguration("t_order","m1.t_order_$‐> {1..2}"); result.setTableShardingStrategyConfig(new InlineShardingStrategyConfiguration("order_id", "t_order_$‐>{order_id % 2 + 1}")); result.setKeyGeneratorConfig(getKeyGeneratorConfiguration()); return result; } // 定义sharding‐Jdbc数据源 @Bean DataSource getShardingDataSource() throws SQLException { ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration(); shardingRuleConfig.getTableRuleConfigs().add(getOrderTableRuleConfiguration()); //spring.shardingsphere.props.sql.show = true Properties properties = new Properties(); properties.put("sql.show","true"); return ShardingDataSourceFactory.createDataSource(createDataSourceMap(), shardingRuleConfig,properties); } }
由于采用了配置类所以需要屏蔽原来 application.properties文件中spring.shardingsphere开头的配置信息。
还需要在SpringBoot启动类中屏蔽使用spring.shardingsphere配置项的类:
@SpringBootApplication(exclude = {SpringBootConfiguration.class}) public class ShardingJdbcSimpleDemoBootstrap {....}
5.3 Spring Boot properties配置
此方式同快速入门程序。
# 定义数据源 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/order_db?useUnicode=true spring.shardingsphere.datasource.m1.username = root spring.shardingsphere.datasource.m1.password = root # 指定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.actual‐data‐nodes = m1.t_order_$‐>{1..2} # 指定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}
5.4 Spring命名空间配置
此方式使用xml方式配置,不推荐使用。
<?xml version="1.0" encoding="UTF‐8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema‐instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:sharding="http://shardingsphere.apache.org/schema/shardingsphere/sharding" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring‐beans.xsd http://shardingsphere.apache.org/schema/shardingsphere/sharding http://shardingsphere.apache.org/schema/shardingsphere/sharding/sharding.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring‐context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring‐tx.xsd"> <context:annotation‐config /> <!‐‐定义多个数据源‐‐> <bean id="m1" class="com.alibaba.druid.pool.DruidDataSource" destroy‐method="close"> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/order_db_1?useUnicode=true" /> <property name="username" value="root" /> <property name="password" value="root" /> </bean> <!‐‐定义分库策略‐‐> <sharding:inline‐strategy id="tableShardingStrategy" sharding‐column="order_id" algorithm‐ expression="t_order_$‐>{order_id % 2 + 1}" /> <!‐‐定义主键生成策略‐‐> <sharding:key‐generator id="orderKeyGenerator" type="SNOWFLAKE" column="order_id" /> <!‐‐定义sharding‐Jdbc数据源‐‐> <sharding:data‐source id="shardingDataSource"> <sharding:sharding‐rule data‐source‐names="m1"> <sharding:table‐rules> <sharding:table‐rule logic‐table="t_order" table‐strategy‐ ref="tableShardingStrategy" key‐generator‐ref="orderKeyGenerator" /> </sharding:table‐rules> </sharding:sharding‐rule> </sharding:data‐source> </beans>
6 sharding-jdbc4种分片策略
如果我一部分表做了分库分表,另一部分未做分库分表的表怎么处理?怎么才能正常访问?
这是一个比较典型的问题,我们知道分库分表是针对某些数据量持续大幅增长的表,比如用户表、订单表等,而不是一刀切将全部表都做分片。那么不分片的表和分片的表如何划分,一般有两种解决方案。
- 严格划分功能库,分片的库与不分片的库剥离开,业务代码中按需切换数据源访问
- 设置默认数据源,以
Sharding-JDBC
为例,不给未分片表设置分片规则,它们就不会执行,因为找不到路由规则,这时我们设置一个默认数据源,在找不到规则时一律访问默认库。
# 配置数据源 ds-0 spring.shardingsphere.datasource.ds-0.type=com.alibaba.druid.pool.DruidDataSource spring.shardingsphere.datasource.ds-0.driverClassName=com.mysql.jdbc.Driver spring.shardingsphere.datasource.ds-0.url=jdbc:mysql://47.94.6.5:3306/ds-0?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT spring.shardingsphere.datasource.ds-0.username=root spring.shardingsphere.datasource.ds-0.password=root # 默认数据源,未分片的表默认执行库 spring.shardingsphere.sharding.default-data-source-name=ds-0
这篇我们针对具体的SQL使用场景,实践一下4种分片策略的用法,开始前先做点准备工作。
- 标准分片策略
- 复合分片策略
- 行表达式分片策略
- Hint分片策略
6.1 准备工作
先创建两个数据库 ds-0
、ds-1
,两个库中分别建表 t_order_0
、t_order_1
、t_order_2
、t_order_item_0
、t_order_item_1
、t_order_item_2
6张表,下边实操看看如何在不同场景下应用 sharding-jdbc
的 4种分片策略。
t_order_n
表结构如下:
CREATE TABLE `t_order_0` ( `order_id` bigint(200) NOT NULL, `order_no` varchar(100) DEFAULT NULL, `user_id` bigint(200) NOT NULL, `create_name` varchar(50) DEFAULT NULL, `price` decimal(10,2) DEFAULT NULL, PRIMARY KEY (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
t_order_item_n
表结构如下:
CREATE TABLE `t_order_item_0` ( `item_id` bigint(100) NOT NULL, `order_id` bigint(200) NOT NULL, `order_no` varchar(200) NOT NULL, `item_name` varchar(50) DEFAULT NULL, `price` decimal(10,2) DEFAULT NULL, PRIMARY KEY (`item_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
分片策略分为分表策略和分库策略,它们实现分片算法的方式基本相同,不同是一个对库ds-0
、ds-1
,一个对表 t_order_0
··· t_order_n
等做处理。
6.2 标准分片策略
使用场景:SQL 语句中有>
,>=
, <=
,<
,=
,IN
和 BETWEEN AND
操作符,都可以应用此分片策略。
标准分片策略(StandardShardingStrategy
),它只支持对单个分片健(字段)为依据的分库分表,并提供了两种分片算法 PreciseShardingAlgorithm
(精准分片)和 RangeShardingAlgorithm
(范围分片)。
在使用标准分片策略时,精准分片算法是必须实现的算法,用于 SQL 含有 =
和 IN
的分片处理;范围分片算法是非必选的,用于处理含有 BETWEEN AND
的分片处理。
一旦我们没配置范围分片算法,而 SQL 中又用到 BETWEEN AND
或者 like
等,那么 SQL 将按全库、表路由的方式逐一执行,查询性能会很差需要特别注意。
接下来自定义实现 精准分片算法 和 范围分片算法。
6.3 精准分片算法
精准分库算法
实现自定义精准分库、分表算法的方式大致相同,都要实现 PreciseShardingAlgorithm
接口,并重写 doSharding()
方法,只是配置稍有不同,而且它只是个空方法,得我们自行处理分库、分表逻辑。其他分片策略亦如此。
SELECT * FROM t_order where order_id = 1 or order_id in (1,2,3);
下边我们实现精准分库策略,通过对分片健 order_id
取模的方式(怎么实现看自己喜欢)计算出 SQL 该路由到哪个库,计算出的分片库信息会存放在分片上下文中,方便后续分表中使用。
/** * @author * @description 自定义标准分库策略 */ public class MyDBPreciseShardingAlgorithm implements PreciseShardingAlgorithm<Long> { @Override public String doSharding(Collection<String> databaseNames, PreciseShardingValue<Long> shardingValue) { /** * databaseNames 所有分片库的集合 * shardingValue 为分片属性,其中 logicTableName 为逻辑表,columnName 分片健(字段),value 为从 SQL 中解析出的分片健的值 */ for (String databaseName : databaseNames) { String value = shardingValue.getValue() % databaseNames.size() + ""; if (databaseName.endsWith(value)) { return databaseName; } } throw new IllegalArgumentException(); } }
其中 Collection<String>
参数在几种分片策略中使用一致,在分库时值为所有分片库的集合 databaseNames
,分表时为对应分片库中所有分片表的集合 tablesNames
;PreciseShardingValue
为分片属性,其中 logicTableName
为逻辑表,columnName
分片健(字段),value
为从 SQL 中解析出的分片健的值。
而 application.properties
配置文件中只需修改分库策略名 database-strategy
为标准模式 standard
,分片算法 standard.precise-algorithm-class-name
为自定义的精准分库算法类路径。
### 分库策略 # 分库分片健 spring.shardingsphere.sharding.tables.t_order.database-strategy.standard.sharding-column=order_id # 分库分片算法 spring.shardingsphere.sharding.tables.t_order.database-strategy.standard.precise-algorithm-class-name=com.oldlu.sharding.algorithm.dbAlgorithm.MyDBPreciseShardingAlgorithm
精准分表算法
精准分表算法同样实现 PreciseShardingAlgorithm
接口,并重写 doSharding()
方法。
/** * @description 自定义标准分表策略 */ public class MyTablePreciseShardingAlgorithm implements PreciseShardingAlgorithm<Long> { @Override public String doSharding(Collection<String> tableNames, PreciseShardingValue<Long> shardingValue) { /** * tableNames 对应分片库中所有分片表的集合 * shardingValue 为分片属性,其中 logicTableName 为逻辑表,columnName 分片健(字段),value 为从 SQL 中解析出的分片健的值 */ for (String tableName : tableNames) { /** * 取模算法,分片健 % 表数量 */ String value = shardingValue.getValue() % tableNames.size() + ""; if (tableName.endsWith(value)) { return tableName; } } throw new IllegalArgumentException(); } }
分表时 Collection<String>
参数为上边计算出的分片库,对应的所有分片表的集合 tablesNames
;PreciseShardingValue
为分片属性,其中 logicTableName
为逻辑表,columnName
分片健(字段),value
为从 SQL 中解析出的分片健的值。
application.properties
配置文件也只需修改分表策略名 database-strategy
为标准模式 standard
,分片算法 standard.precise-algorithm-class-name
为自定义的精准分表算法类路径。
# 分表策略 # 分表分片健 spring.shardingsphere.sharding.tables.t_order.table-strategy.standard.sharding-column=order_id # 分表算法 spring.shardingsphere.sharding.tables.t_order.table-strategy.standard.precise-algorithm-class-name=com.oldlu.sharding.algorithm.tableAlgorithm.MyTablePreciseShardingAlgorithm
看到这不难发现,自定义分库和分表算法的实现基本是一样的,所以后边我们只演示分库即可
6.4 范围分片算法
使用场景:当我们 SQL中的分片健字段用到 BETWEEN AND
操作符会使用到此算法,会根据 SQL中给出的分片健值范围值处理分库、分表逻辑。
SELECT * FROM t_order where order_id BETWEEN 1 AND 100;
自定义范围分片算法需实现 RangeShardingAlgorithm
接口,重写 doSharding()
方法,下边我通过遍历分片健值区间,计算每一个分库、分表逻辑。
/** * @description 范围分库算法 */ public class MyDBRangeShardingAlgorithm implements RangeShardingAlgorithm<Integer> { @Override public Collection<String> doSharding(Collection<String> databaseNames, RangeShardingValue<Integer> rangeShardingValue) { Set<String> result = new LinkedHashSet<>(); // between and 的起始值 int lower = rangeShardingValue.getValueRange().lowerEndpoint(); int upper = rangeShardingValue.getValueRange().upperEndpoint(); // 循环范围计算分库逻辑 for (int i = lower; i <= upper; i++) { for (String databaseName : databaseNames) { if (databaseName.endsWith(i % databaseNames.size() + "")) { result.add(databaseName); } } } return result; } }
和上边的一样 Collection<String>
在分库、分表时分别代表分片库名和表名集合,RangeShardingValue
这里取值方式稍有不同, lowerEndpoint
表示起始值, upperEndpoint
表示截止值。
在配置上由于范围分片算法和精准分片算法,同在标准分片策略下使用,所以只需添加上 range-algorithm-class-name
自定义范围分片算法类路径即可。
# 精准分片算法 spring.shardingsphere.sharding.tables.t_order.database-strategy.standard.precise-algorithm-class-name=com.oldlu.sharding.algorithm.dbAlgorithm.MyDBPreciseShardingAlgorithm # 范围分片算法 spring.shardingsphere.sharding.tables.t_order.database-strategy.standard.range-algorithm-class-name=com.oldlu.sharding.algorithm.dbAlgorithm.MyDBRangeShardingAlgorithm
6.5 复合分片策略
使用场景:SQL 语句中有>
,>=
, <=
,<
,=
,IN
和 BETWEEN AND
等操作符,不同的是复合分片策略支持对多个分片健操作。
下面我们实现同时以 order_id
、user_id
两个字段作为分片健,自定义复合分片策略。
SELECT * FROM t_order where user_id =0 and order_id = 1;
我们先修改一下原配置,complex.sharding-column
切换成 complex.sharding-columns
复数,分片健上再加一个 user_id
,分片策略名变更为 complex
,complex.algorithm-class-name
替换成我们自定义的复合分片算法。
### 分库策略 # order_id,user_id 同时作为分库分片健 spring.shardingsphere.sharding.tables.t_order.database-strategy.complex.sharding-column=order_id,user_id # 复合分片算法 spring.shardingsphere.sharding.tables.t_order.database-strategy.complex.algorithm-class-name=com.oldlu.sharding.algorithm.dbAlgorithm.MyDBComplexKeysShardingAlgorithm
自定义复合分片策略要实现 ComplexKeysShardingAlgorithm
接口,重新 doSharding()
方法。
/** * @description 自定义复合分库策略 */ public class MyDBComplexKeysShardingAlgorithm implements ComplexKeysShardingAlgorithm<Integer> { @Override public Collection<String> doSharding(Collection<String> databaseNames, ComplexKeysShardingValue<Integer> complexKeysShardingValue) { // 得到每个分片健对应的值 Collection<Integer> orderIdValues = this.getShardingValue(complexKeysShardingValue, "order_id"); Collection<Integer> userIdValues = this.getShardingValue(complexKeysShardingValue, "user_id"); List<String> shardingSuffix = new ArrayList<>(); // 对两个分片健同时取模的方式分库 for (Integer userId : userIdValues) { for (Integer orderId : orderIdValues) { String suffix = userId % 2 + "_" + orderId % 2; for (String databaseName : databaseNames) { if (databaseName.endsWith(suffix)) { shardingSuffix.add(databaseName); } } } } return shardingSuffix; } private Collection<Integer> getShardingValue(ComplexKeysShardingValue<Integer> shardingValues, final String key) { Collection<Integer> valueSet = new ArrayList<>(); Map<String, Collection<Integer>> columnNameAndShardingValuesMap = shardingValues.getColumnNameAndShardingValuesMap(); if (columnNameAndShardingValuesMap.containsKey(key)) { valueSet.addAll(columnNameAndShardingValuesMap.get(key)); } return valueSet; } }
Collection<String>
用法还是老样子,由于支持多分片健 ComplexKeysShardingValue
分片属性内用一个分片健为 key
,分片健值为 value
的 map
来存储分片键属性。
6.6 行表达式分片策略
行表达式分片策略(InlineShardingStrategy
),在配置中使用 Groovy
表达式,提供对 SQL语句中的 =
和 IN
的分片操作支持,它只支持单分片健。
行表达式分片策略适用于做简单的分片算法,无需自定义分片算法,省去了繁琐的代码开发,是几种分片策略中最为简单的。
它的配置相当简洁,这种分片策略利用inline.algorithm-expression
书写表达式。
比如:ds-$->{order_id % 2}
表示对 order_id
做取模计算,$
是个通配符用来承接取模结果,最终计算出分库ds-0
··· ds-n
,整体来说比较简单。
# 行表达式分片键 sharding.jdbc.config.sharding.tables.t_order.database-strategy.inline.sharding-column=order_id # 表达式算法 sharding.jdbc.config.sharding.tables.t_order.database-strategy.inline.algorithm-expression=ds-$->{order_id % 2}
6.7 Hint分片策略
Hint分片策略(HintShardingStrategy
)相比于上面几种分片策略稍有不同,这种分片策略无需配置分片健,分片健值也不再从 SQL中解析,而是由外部指定分片信息,让 SQL在指定的分库、分表中执行。ShardingSphere
通过 Hint
API实现指定操作,实际上就是把分片规则tablerule
、databaserule
由集中配置变成了个性化配置。
举个例子,如果我们希望订单表t_order
用 user_id
做分片健进行分库分表,但是 t_order
表中却没有 user_id
这个字段,这时可以通过 Hint API 在外部手动指定分片健或分片库。
下边我们这边给一条无分片条件的SQL,看如何指定分片健让它路由到指定库表。
SELECT * FROM t_order;
使用 Hint分片策略同样需要自定义,实现 HintShardingAlgorithm
接口并重写 doSharding()
方法。
/** * @description hit分表算法 */ public class MyTableHintShardingAlgorithm implements HintShardingAlgorithm<String> { @Override public Collection<String> doSharding(Collection<String> tableNames, HintShardingValue<String> hintShardingValue) { Collection<String> result = new ArrayList<>(); for (String tableName : tableNames) { for (String shardingValue : hintShardingValue.getValues()) { if (tableName.endsWith(String.valueOf(Long.valueOf(shardingValue) % tableNames.size()))) { result.add(tableName); } } } return result; } }
自定义完算法只实现了一部分,还需要在调用 SQL 前通过 HintManager
指定分库、分表信息。由于每次添加的规则都放在 ThreadLocal
内,所以要先执行 clear()
清除掉上一次的规则,否则会报错;addDatabaseShardingValue
设置分库分片健键值,addTableShardingValue
设置分表分片健键值。setMasterRouteOnly
读写分离强制读主库,避免造成主从复制导致的延迟。
// 清除掉上一次的规则,否则会报错 HintManager.clear(); // HintManager API 工具类实例 HintManager hintManager = HintManager.getInstance(); // 直接指定对应具体的数据库 hintManager.addDatabaseShardingValue("ds",0); // 设置表的分片健 hintManager.addTableShardingValue("t_order" , 0); hintManager.addTableShardingValue("t_order" , 1); hintManager.addTableShardingValue("t_order" , 2); // 在读写分离数据库中,Hint 可以强制读主库 hintManager.setMasterRouteOnly();
debug 调试看到,我们对 t_order
表设置分表分片健键值,可以在自定义的算法 HintShardingValue
参数中成功拿到。
properties
文件中配置无需再指定分片健,只需自定义的 Hint分片算法类路径即可。
# Hint分片算法 spring.shardingsphere.sharding.tables.t_order.table-strategy.hint.algorithm-class-name=com.oldlu.sharding.algorithm.tableAlgorithm.MyTableHintShardingAlgorithm
7 水平分表
上述案例为水平分表是在同一个数据库内,把同一个表的数据按一定规则拆到多个表中,我们已经对水平分库进行实现,这里不再重复介绍。
8 水平分库
前面已经介绍过,水平分库是把同一个表的数据按一定规则拆到不同的数据库中,每个库可以放在不同的服务器上。接下来看一下如何使用Sharding-JDBC实现水平分库,咱们继续对快速入门中的例子进行完善。
(1)将原有order_db库拆分为order_db_1、order_db_2
(2)分片规则修改
由于数据库拆分了两个,这里需要配置两个数据源。
分库需要配置分库的策略,和分表策略的意义类似,通过分库策略实现数据操作针对分库的数据库进行操作。
# 定义多个数据源 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:3306/order_db_1?useUnicode=true spring.shardingsphere.datasource.m1.username = root spring.shardingsphere.datasource.m1.password = root 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/order_db_2?useUnicode=true spring.shardingsphere.datasource.m2.username = root spring.shardingsphere.datasource.m2.password = root ... # 分库策略,以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}
分库策略定义方式如下:
#分库策略,如何将一个逻辑表映射到多个数据源 spring.shardingsphere.sharding.tables.<逻辑表名称>.database‐strategy.<分片策略>.<分片策略属性名>= # 分片策略属性值 #分表策略,如何将一个逻辑表映射为多个实际表 spring.shardingsphere.sharding.tables.<逻辑表名称>.table‐strategy.<分片策略>.<分片策略属性名>= #分 片策略属性值
Sharding-JDBC支持以下几种分片策略:
不管理分库还是分表,策略基本一样。
standard :标准分片策略
,对应StandardShardingStrategy。提供对SQL语句中的=, IN和BETWEEN AND的分片操作支持。StandardShardingStrategy只支持单分片键,提供PreciseShardingAlgorithm和- RangeShardingAlgorithm两个分片算法。PreciseShardingAlgorithm是必选的,用于处理=和IN的分片。RangeShardingAlgorithm是可选的,用于处理BETWEEN AND分片,如果不配置 RangeShardingAlgorithm,SQL中的BETWEEN AND将按照全库路由处理。
complex :符合分片策略
,对应ComplexShardingStrategy。复合分片策略。提供对SQL语句中的=, IN- 和BETWEEN AND的分片操作支持。ComplexShardingStrategy支持多分片键,由于多分片键之间的关系复杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片操作符透传至分片算法,完全由应用开发者实现,提供最大的灵活度。
inline :行表达式分片策略
,对应InlineShardingStrategy。使用Groovy的表达式,提供对SQL语句中的=和- IN的分片操作支持,只支持单分片键。对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发,如: t_user_$ ->{u_id % 8} 表示t_user表根据u_id模8,而分成8张表,表名称为 t_user_0 到t_user_7 。
hint :Hint分片策略
,对应HintShardingStrategy。通过Hint而非SQL解析的方式分片的策略。对于分片字段非SQL决定,而由其他外置条件决定的场景,可使用SQL Hint灵活的注入分片字段。例:内部系统,按照员工登录主键分库,而数据库中并无此字段。SQL Hint支持通过Java API和SQL注释(待实现)两种方式使用。none :不分片策略,对应NoneShardingStrategy。不分片的策略。
(3) 插入测试
修改testInsertOrder方法,插入数据中包含不同的user_id
@Test public void testInsertOrder(){ for (int i = 0 ; i<10; i++){ orderDao.insertOrder(new BigDecimal((i+1)*5),1L,"WAIT_PAY"); } for (int i = 0 ; i<10; i++){ orderDao.insertOrder(new BigDecimal((i+1)*10),2L,"WAIT_PAY"); } }
执行testInsertOrder:
通过日志可以看出,根据user_id的奇偶不同,数据分别落在了不同数据源,达到目标。
(4)查询测试
调用快速入门的查询接口进行测试:
List<Map> selectOrderbyIds(@Param("orderIds")List<Long> orderIds);
通过日志发现,sharding-jdbc将sql路由到m1和m2:
问题分析:
由于查询语句中并没有使用分片键user_id,所以sharding-jdbc将广播路由到每个数据结点。
下边我们在sql中添加分片键进行查询。
在OrderDao中定义接口:
@Select({"<script>", " select", " * ", " from t_order t ", "where t.order_id in", "<foreach collection='orderIds' item='id' open='(' separator=',' close=')'>", "#{id}", "</foreach>", " and t.user_id = #{userId} ", "</script>" }) List<Map> selectOrderbyUserAndIds(@Param("userId") Integer userId,@Param("orderIds")List<Long> orderIds);
编写测试方法:
@Test public void testSelectOrderbyUserAndIds(){ List<Long> orderIds = new ArrayList<>(); orderIds.add(373422416644276224L); orderIds.add(373422415830581248L); //查询条件中包括分库的键user_id int user_id = 1; List<Map> orders = orderDao.selectOrderbyUserAndIds(user_id,orderIds); JSONArray jsonOrders = new JSONArray(orders); System.out.println(jsonOrders); }
执行testSelectOrderbyUserAndIds:
查询条件user_id为1,根据分片策略m$->{user_id % 2 + 1}计算得出m2,此sharding-jdbc将sql路由到m2,见上图日志。
9 垂直分库
前面已经介绍过,垂直分库是指按照业务将表进行分类,分布到不同的数据库上面,每个库可以放在不同的服务器上,它的核心理念是专库专用。接下来看一下如何使用Sharding-JDBC实现垂直分库。
(1)创建数据库
创建数据库user_db
CREATE DATABASE
user_db CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
在user_db中创建t_user表
DROP TABLE IF EXISTS `t_user`; CREATE TABLE `t_user` ( `user_id` bigint(20) 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 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
(2)在Sharding-JDBC规则中修改
# 新增m0数据源,对应user_db 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:3306/user_db?useUnicode=true spring.shardingsphere.datasource.m0.username = root spring.shardingsphere.datasource.m0.password = root .... # t_user分表策略,固定分配至m0的t_user真实表 spring.shardingsphere.sharding.tables.t_user.actual‐data‐nodes = m$‐>{0}.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
(3) 数据操作
新增UserDao:
@Mapper @Component public interface UserDao { /** * 新增用户 * @param userId 用户id * @param fullname 用户姓名 * @return */ @Insert("insert into t_user(user_id, fullname) value(#{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); }
(4)测试
新增单元测试方法:
@Test public void testInsertUser(){ for (int i = 0 ; i<10; i++){ Long id = i + 1L; userDao.insertUser(id,"姓名"+ id ); } } @Test public void testSelectUserbyIds(){ List<Long> userIds = new ArrayList<>(); userIds.add(1L); userIds.add(2L); List<Map> users = userDao.selectUserbyIds(userIds); System.out.println(users); }
执行 testInsertUser:
通过日志可以看出t_user表的数据被落在了m0数据源,达到目标。
执行testSelectUserbyIds:
通过日志可以看出t_user表的查询操作被落在了m0数据源,达到目标。
10 公共表
公共表属于系统中数据量较小,变动少,而且属于高频联合查询的依赖表。参数表、数据字典表等属于此类型。可以将这类表在每个数据库都保存一份,所有更新操作都同时发送到所有分库执行。接下来看一下如何使用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)在Sharding-JDBC规则中修改
# 指定t_dict为公共表 spring.shardingsphere.sharding.broadcast‐tables=t_dict
(3)数据操作
新增DictDao:
@Mapper @Component public interface DictDao { /** * 新增字典 * @param type 字典类型 * @param code 字典编码 * @param value 字典值 * @return */ @Insert("insert into t_dict(dict_id,type,code,value) value(#{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); }
(4)字典操作测试
新增单元测试方法:
@Test public void testInsertDict(){ dictDao.insertDict(1L,"user_type","0","管理员"); dictDao.insertDict(2L,"user_type","1","操作员"); } @Test public void testDeleteDict(){ dictDao.deleteDict(1L); dictDao.deleteDict(2L); }
执行testInsertDict:
通过日志可以看出,对t_dict的表的操作被广播至所有数据源。
测试删除字典,观察是否把所有数据源中该 公共表的记录删除。
(5)字典关联查询测试
字典表已在各各分库存在,各业务表即可和字典表关联查询。
定义用户关联查询dao:
在UserDao中定义:
/** * 根据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); JSONArray jsonUsers = new JSONArray(users); System.out.println(jsonUsers); }
执行测试方法,查看日志,成功关联查询字典表:
11 配置中遇到的一些问题
11.1 数据库链接池找不到
springboot2.0之后,采用的默认数据库连接池就是Hikari
11.2 错误java.lang.IllegalArgumentException: jdbcUrl is required with driverClassName.
原因
- HikariConfig校验配置中没有jdbcUrl配置
处理方式
- springboot 1.x 版本中,数据源配置是 xxxx.url=
- 在2.x中,更改为 jdbc-url
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。