Sharding-JDBC
Sharding-JDBC介绍
Sharding-JDBC是当当网研发的开源分布式数据库中间件,定位为轻量级Java框架,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架,从3.0开始Sharding-JDBC被包含在Sharding-Sphere中。
ShardingSphere是一套开源的分布式数据库中间件解决方案组成的生态圈,它由Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar(计划中)这3款相互独立的产品组成。 他们均提供标准化的数据分片、分布式事务和数据库治理功能,可适用于如Java同构、异构语言、云原生等各种多样化的应用场景
Sharding-JDBC的作用
Sharding-JDBC的核心功能为数据分片和读写分离,通过Sharding-JDBC,应用可以透明的使用Jdbc访问已经分库分表、读写分离的多个数据源,而不用关心数据源的数量以及数据如何分布。
- 适用于任何基于JDBC的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Temhplate或直接使用JDBC。
- 支持任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
- 支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer,PostgreSQL以及任何遵循SQL92标准的数据库。
官网地址:概览 :: ShardingSphere (apache.org)
什么是分库分表
初创时用户规模有限,但随着用户的增加以及公司业务快速发展,我们的内容也与日俱增。然而,随着数据库中的数据量猛增,而关系型数据库本身的单机存储容量、连接数、处理能力有限,产生系统瓶颈,导致访问性能下降严重,在多维度查询下,即使添加从库、优化索引也无法很好的提升单表数据量过大所带来的性能问题。
解决方案一:通过提升服务器硬件能力来提高数据处理能力,比如增加存储容量、CPU等这种方案成本很高,并且如果瓶颈在MySQL本身那么提高硬件也是有很有必要的。
解决方案二:把数据分散在不同的数据库中,使得单一数据库的数据量变小来缓解单一数据库的性能问题,从而达到提升数据库性能的目的,如下图:将学茶的数据库拆分为若干独立的数据库,并且对于大表也拆分为若干小表,通过这种数据库拆分的方法来解决数据库的性能问题。
分库分表就是为了解决由于数据量过大而导致数据库性能降低的问题,将原来独立的数据库拆分成若干数据库组成 ,将数据大表拆分成若干数据表组成,使得单一数据库、单一数据表的数据量变小,从而达到提升数据库性能的目的。
分库分表的方式
分库分表包括分库和分表两个部分,在生产中通常包括:垂直分库、水平分库、垂直分表、水平分表四种方式
垂直分表:将一个表按照字段分成多表,每个表存储其中一部分字段。
将访问频次低的商品描述信息单独存放在一张表中,访问频次较高的商品基本信息单独放在一张表中
通过垂直分表所带来的提升是:
- 为了避免IO争抢并减少锁表的几率,查看详情的用户与商品信息浏览互不影响
- 充分发挥热门数据的操作效率,商品信息的操作的高效率不会被商品描述的低效率所拖累。
为什么大字段IO效率低:
1.由于数据量本身大,需要更长的读取时间;
2.跨页,页是数据库存储单位,很多查找及定位操作都是以页为单位,单页内的数据行越多数据库整体性能越好,而大字段占用空间大,单页内存储行 数少,因此IO效率较低。
3.数据库以行为单位将数据加载到内存中,这样表中字段长度较短且访问频率较高,内存能加载更多的数据,命中率更高,减少了磁盘IO,从而提升 了数据库性能。
什么是锁表:
锁表是数据库中一种机制,用于确保在对数据库进行读取或写入操作时数据的一致性和完整性。当一个事务在对某张表进行修改时,系统会将该表锁定,阻止其他事务对其进行修改,直到该事务完成操作释放锁。这有助于避免数据冲突和并发问题,确保数据操作的正确性。
垂直分库:按照业务将表进行分类,分布到不同的数据库上面,每个库可以放在不同的服务器上,它的核心理念是专库专用
通过垂直分表性能得到了一定程度的提升,但是还没有达到要求,并且磁盘空间也快不够了,因为数据还是始终限制在一台服务器,库内垂直分表只解决了单一表数据量过大的问题,但没有将表分布到不同的服务器上,因此每个表还是竞争同一个物理机的CPU、内存、网络IO、磁盘。
它带来的提升是:
- 解决业务层面的耦合,业务清晰
- 能对不同业务的数据进行分级管理、维护、监控、扩展等
- 高并发场景下,垂直分库一定程度的提升IO、数据库连接数、降低单机硬件资源的瓶颈
注意:垂直分库通过将表按业务分类,然后分布在不同数据库,并且可以将这些数据库部署在不同服务器上,从而
达到多个服务器共同分摊压力的效果,但是依然没有解决单表数据量过大的问题。
水平分库:把同一个表的数据按一定规则拆到不同的数据库中,每个库可以放在不同的服务器上。
经过垂直分库后,数据库性能问题得到一定程度的解决,但是随着业务量的增长,tmall-server-mall(商品库)单库存储数据已经超出预估。粗略估计,目前商品数量得往1500w+上预估,并且tmall-server-mall(商品库)属于访问非常频繁的资源,单台服务器已经无法支撑。
从业务角度分析,目前情况已经无法再次垂直分库
通过水平分库,将类别ID为单数和类别ID为双数的商品信息分别放到两个库中
具体规则如下:
要操作某条数据,先分析这条数据所属的类别ID。如果类别ID为双数,将此操作映射至tmall-server-mall1(商品库1);如果类别ID为单数,将操作映射至tmall-server-mall2(商品库2)。此操作要访问数据库名称的表达式为tmall-server-mall【类别ID%2+1】
它带来的提升是:
-
解决了单库大数据,高并发的性能瓶颈。
-
提高了系统的稳定性及可用性。稳定性体现在IO冲突减少,锁定减少,可用性指某个库出问题,部分可用
对比:垂直分库是把不同表拆到不同数据库中,它是对数据行的拆分,不影响表结构
**水平分表:**是在同一个数据库内,把同一个表的数据按一定规则拆到多个表中
通过水平分库和垂直分库可以解决上述部分问题,但是如果单表数据量依旧较为庞大,那么再次进行水平分库会过多的增加数据库实例,提升数据库运维的难度和压力,可以按照水平分库的思路对把tmall-server-mall X(商品库)内的表也可以进行水平拆分,其目的也是为解决单表数据量大的问题
与水平分库的思路类似,不过这次操作的目标是表,商品信息及商品描述被分成了两套表。如果商品ID为双数,将此操作映射至商品信息1表:如果商品ID为单数,将操作映射至商品信息2表。此操作要访问表名称的表达式为商品信息【商品ID%2+1】
它带来的提升是:
-
解决了单库大数据,高并发的性能瓶颈。
-
提高了系统的稳定性及可用性。稳定性体现在IO冲突减少,锁定减少,可用性指某个库出问题,部分可用
分库分表带来的问题
分库分表能有效的缓解了单机和单库带来的性能瓶颈和压力,突破网络IO、硬件资源、连接数的瓶颈,同时也带来了一些问题。
事务一致性问题
由于分库分表把数据分布在不同库甚至不同服务器,不可避免会带来分布式事务问题。
跨节点关联查询
在没有分库前,比如说我们要在文章中去嵌入一个商品信息,那么可以通过文章中的商品ID进行关联查询获取到商品详情,但是此时我们发现,商品和文章已经经过分库分表,不在同一个数据库,甚至部署在不同的服务器上,无法进行关联查询
可将原关联查询分为两次查询,第一次查询的结果集中找出关联数据id,然后根据id发起第二次请求得到关联数据,最后将获得到的数据进行拼装。
跨节点分页、排序函数
跨节点多库进行查询时,limit分页、order by排序等问题,就变得比较复杂了。需要先在不同的分片节点中将数据进行排序并返回,然后将不同分片返回的结果集进行汇总和再次排序。
以上流程是取第一页的数据,性能影响不大,但由于商品信息的分布在各数据库的数据可能是随机的,如果是取第N页,需要将所有节点前N页数据都取出来合并,再进行整体的排序,操作效率可想而知。所以请求页数越大,系统的性能也会越差。
在使用Max、Min、Sum、Count之类的函数进行计算的时候,与排序分页同理,也需要先在每个分片上执行相应的函数,然后将各个分片的结果集进行汇总和再次计算,最终将结果返回。
主键重复
在分库分表环境中,由于表中数据同时存在不同数据库中,主键值平时使用的自增长将无用武之地,某个分区数据库生成的ID无法保证全局唯一。因此需要单独设计全局主键,以避免跨库主键重复问题。
由于分库分表之后,数据被分散在不同的数据库、服务器。因此,对数据的操作也就无法通过常规方式完成,并且它还带来了一系列的问题。好在,这些问题不是所有都需要我们在应用层面上解决,市面上有很多中间件可供我们选择,其中Sharding-JDBC使用流行度较高。
Sharding-JDBC 入门(水平分表)
需求说明
人工创建两张表,mall_order_1和mall_order_2,这两张表是订单表拆分后的表,通过Sharding-Jdbc向订单表插入数据,按照一定的分片规则,主键为偶数的进入mall_order_1,另一部分数据进入mall_order_2,通过Sharding-Jdbc 查询数据,根据 SQL语句的内容从mall_order_1或mall_order_2查询数据。
环境搭建
主机名 | IP地址 |
---|---|
mysql | 192.168.8.100/24 |
[root@mysql ~]# dnf -y install mysql-server mysql
[root@mysql ~]# systemctl start mysqld
[root@mysql ~]# systemctl enable mysqld
[root@mysql ~]# ss -nutlp | grep :3306
[root@mysql ~]# mysqladmin -uroot password "123qqq...A" #修改root用户密码
创建数据库:tmall_server-mall
mysql> CREATE DATABASE `tmall_server-mall`;
授权root@'%'用户
mysql> CREATE USER root@'%' IDENTIFIED BY '123qqq...A';
mysql> GRANT ALL ON *.* TO root@'%';
在tmall_server-mall数据库下创建mall_order_1表和mall_order_2表
USE `tmall_server-mall`;
CREATE TABLE mall_order_1
(
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '数据ID',
buyer_id BIGINT UNSIGNED DEFAULT 0 COMMENT '用户ID',
buyer_username VARCHAR(50) DEFAULT '' COMMENT '用户名',
order_no VARCHAR(50) DEFAULT '' COMMENT '订单编号',
receiver_name VARCHAR(32) DEFAULT '' COMMENT '收货人',
receiver_phone VARCHAR(32) DEFAULT '' COMMENT '收货电话',
receiver_address VARCHAR(255) DEFAULT '' COMMENT '收货地址',
goods_num INT UNSIGNED DEFAULT 0 COMMENT '商品数量',
total_price DECIMAL(10, 2) DEFAULT 0 COMMENT '商品销售总价',
logistics_no VARCHAR(50) DEFAULT '' COMMENT '物流单号',
pay_channel INT UNSIGNED DEFAULT 0 COMMENT '支付渠道:1=支付宝,2=微信',
pay_method INT UNSIGNED DEFAULT 0 COMMENT '支付方式:1=在线支付,2=货到付款',
order_state TINYINT UNSIGNED DEFAULT 0 COMMENT '订单状态: 0=待支付,1=已支付,待发货, 2=已发货/待收货,3=确认收货/已完成,4=用户关闭,5=平台关闭(商家),6=系统调度关闭',
gmt_pay DATETIME DEFAULT NULL COMMENT '支付时间',
gmt_create DATETIME DEFAULT NULL COMMENT '数据创建时间',
gmt_modified DATETIME DEFAULT NULL COMMENT '数据最后修改时间',
PRIMARY KEY (id)
) COMMENT '商城-订单' CHARSET = utf8mb4;
CREATE TABLE mall_order_2
(
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '数据ID',
buyer_id BIGINT UNSIGNED DEFAULT 0 COMMENT '用户ID',
buyer_username VARCHAR(50) DEFAULT '' COMMENT '用户名',
order_no VARCHAR(50) DEFAULT '' COMMENT '订单编号',
receiver_name VARCHAR(32) DEFAULT '' COMMENT '收货人',
receiver_phone VARCHAR(32) DEFAULT '' COMMENT '收货电话',
receiver_address VARCHAR(255) DEFAULT '' COMMENT '收货地址',
goods_num INT UNSIGNED DEFAULT 0 COMMENT '商品数量',
total_price DECIMAL(10, 2) DEFAULT 0 COMMENT '商品销售总价',
logistics_no VARCHAR(50) DEFAULT '' COMMENT '物流单号',
pay_channel INT UNSIGNED DEFAULT 0 COMMENT '支付渠道:1=支付宝,2=微信',
pay_method INT UNSIGNED DEFAULT 0 COMMENT '支付方式:1=在线支付,2=货到付款',
order_state TINYINT UNSIGNED DEFAULT 0 COMMENT '订单状态: 0=待支付,1=已支付,待发货, 2=已发货/待收货,3=确认收货/已完成,4=用户关闭,5=平台关闭(商家),6=系统调度关闭',
gmt_pay DATETIME DEFAULT NULL COMMENT '支付时间',
gmt_create DATETIME DEFAULT NULL COMMENT '数据创建时间',
gmt_modified DATETIME DEFAULT NULL COMMENT '数据最后修改时间',
PRIMARY KEY (id)
) COMMENT '商城-订单' CHARSET = utf8mb4;
创建Spring Boot工程
name: spring-boot-sharding-demo
package name: cn.tedu.springboot.sharding.demo
使用IDEA连接数据库,用于查看数据库
引入maven依赖
<!--引入sharding-jdbc和Spring Boot整合的Jar包-->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.0.0-RC1</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.16</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
点击右上角m标志,下载依赖,下载完毕之后,查看依赖是否下载成功
编写代码
分片规则配置
分片规则配置是sharding-jdbc进行对分库分表操作的重要依据,配置内容包括:数据源、主键生成策略、分片策略等。在application.properties中配置
创建资源文件:application.properties
server.port=9090
spring.application.name =sharding-demo
spring.main.allow-bean-definition-overriding = true
mybatis.configuration.map-underscore-to-camel-case=true
#配置数据源,名字可以自行定义,需要与下方配置保持一致
spring.shardingsphere.datasource.names=m1
#配置druid的连接池
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://192.168.8.100:3306/tmall_server-mall?useUnicode=true
spring.shardingsphere.datasource.m1.username=root
spring.shardingsphere.datasource.m1.password=123qqq...A
#指定mall_order表的数据分布情况,配置数据节点
spring.shardingsphere.sharding.tables.mall_order.actual-data-nodes=m1.mall_order_$->{1..2}
#配置表的主键生成策略,使用雪花算法可以自动解决主键冲突的问题
spring.shardingsphere.sharding.tables.mall_order.key-generator.column=id
spring.shardingsphere.sharding.tables.mall_order.key-generator.type=SNOWFLAKE
#配置表的分片策略,分片策略包含了分片健和分片算法
spring.shardingsphere.sharding.tables.mall_order.table-strategy.inline.sharding-column=id
spring.shardingsphere.sharding.tables.mall_order.table-strategy.inline.algorithm-expression=mall_order_$->{id % 2 + 1}
#打开SQL输出日志
spring.shardingsphere.props.sql.show=true
通过MyBatis新增数据
创建持久层接口: 在mapper包下创建OrderMapper
实现插入功能
package cn.tedu.springboot.sharding.demo.mapper;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface OrderMapper {
//编写新增方法,通过insert注解,指定新增的SQL语句
//当调用insertOrder方法的时候,等于执行上方注解指定的SQL语句
//Param注解用于指定新增时的参数
@Insert("insert into mall_order(buyer_id,buyer_username,order_no,receiver_name) " +
"values(#{buyerId},#{buyerUsername},#{orderNo},#{receiverName})")
int insertOrder(@Param("buyerId")Long buyerId,@Param("buyerUsername")String buyerUsername
,@Param("orderNo")String orderNo,@Param("receiverName")String receiverName);
}
编写测试类:
package cn.tedu.springboot.sharding.demo;
import cn.tedu.springboot.sharding.demo.mapper.OrderMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringBootShardingDemoApplicationTests {
//注入对象,OrderMapper
@Autowired
OrderMapper orderMapper;
@Test
void contextLoads() {
//调用insertOrder方法
orderMapper.insertOrder(2000L, "11帅哥", "jal12u911223", "1帅气老李");
}
}
运行测试:
运行结果如下:
查看结果验证
为了更好的演示分表的插入效果,优化测试类:
package cn.tedu.springboot.sharding.demo;
import cn.tedu.springboot.sharding.demo.mapper.OrderMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringBootShardingDemoApplicationTests {
//注入对象,OrderMapper
@Autowired
OrderMapper orderMapper;
@Test
void contextLoads() {
//使用循环调用insertOrder方法,连续插入10组数据
for (int i = 0; i < 10; i++) {
orderMapper.insertOrder(2000L, "11帅哥", "jal12u911223", "1帅气老李");
}
}
}
运行结果如下:
id字段值为偶数的行对2求模取余+1结果为1的数据插入mall_order_1表
id字段值为奇数的行对2求模取余+1结果为2的数据插入mall_order_2表
实现查询功能
编写OrderMapper的查询方法
package cn.tedu.springboot.sharding.demo.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 java.util.List;
import java.util.Map;
@Mapper
public interface OrderMapper {
//编写新增方法,通过insert注解,指定新增的SQL语句
//当调用insertOrder方法的时候,等于执行上方注解指定的SQL语句
//Param注解用于指定新增时的参数
@Insert("insert into mall_order(buyer_id,buyer_username,order_no,receiver_name) " +
"values(#{buyerId},#{buyerUsername},#{orderNo},#{receiverName})")
int insertOrder(@Param("buyerId") Long buyerId, @Param("buyerUsername") String buyerUsername
, @Param("orderNo") String orderNo, @Param("receiverName") String receiverName);
//查询订单
@Select("<script>" +
"select" +
" id,buyer_id,buyer_username" +
" from mall_order " +
" where id in " +
"<foreach collection='orderIds' open='(' separator=',' close=')' item='id'>" +
"#{id} " +
"</foreach>" +
"</script>")
List<Map> selectOrders(@Param("orderIds") List<Long> orderIds);
}
编写测试方法
package cn.tedu.springboot.sharding.demo;
import cn.tedu.springboot.sharding.demo.mapper.OrderMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@SpringBootTest
class SpringBootShardingDemoApplicationTests {
//注入对象,OrderMapper
@Autowired
OrderMapper orderMapper;
@Test
void contextLoads() {
//调用insertOrder方法
for (int i = 0; i < 10; i++) {
orderMapper.insertOrder(2000L, "硕博11帅哥", "jal12u911223", "1帅气老李");
}
}
//查询数据
@Test
public void selectOrderIds(){
//此处的ID号可以换成自己的表记录中的数据
List<Long> orderIds = Arrays.asList(1008183953959419904l,1008183953980391425l);
List<Map> orders = orderMapper.selectOrders(orderIds);
orders.forEach( order -> System.out.println(order));
}
}
测试结果如下:
-
能够看到来自mall_order_1表和mall_order_2表的数据
分析查询过程 -
在查询的过程中是对mall_order这个逻辑表进行查询
-
ShardingJDBC会自己对原SQL进行改写,实际上数据来自于mall_order_1表和mall_order_2表
-
ShardingJDBC还会自己对查询结果进行整合
流程分析
通过日志分析,Sharding-JDBC在拿到用户要执行的sql的后续工作
(1)解析SQL,获取片键值,在本例中是id
(2)Sharding-JDBC通过规则配置mall_order_$->{id % 2 + 1},知道了当id为偶数时,应该往mall_order_1表插数据,为奇数时,往mall_order_2插数据。
(3)Sharding-JDBC根据id的值改写SQL语句,改写后的SQL语句是真实所要执行的SQL语句。
(4)执行改写后的真实SQL语句
(5)将所有真正执行sql的结果进行汇总合并,返回。
其他配置方式
Sharding-JDBC官方提供了四种配置方式,除了properties以外,还有三种方式:
Java配置
DataSource getShardingDataSource() throws SQLException {
ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
shardingRuleConfig.getTableRuleConfigs().add(getOrderTableRuleConfiguration());
shardingRuleConfig.getTableRuleConfigs().add(getOrderItemTableRuleConfiguration());
shardingRuleConfig.getBindingTableGroups().add("t_order, t_order_item");
shardingRuleConfig.getBroadcastTables().add("t_config");
shardingRuleConfig.setDefaultDatabaseShardingStrategyConfig(new InlineShardingStrategyConfiguration("user_id", "ds${user_id % 2}"));
shardingRuleConfig.setDefaultTableShardingStrategyConfig(new StandardShardingStrategyConfiguration("order_id", new ModuloShardingTableAlgorithm()));
return ShardingDataSourceFactory.createDataSource(createDataSourceMap(), shardingRuleConfig, new Properties());
}
private static KeyGeneratorConfiguration getKeyGeneratorConfiguration() {
KeyGeneratorConfiguration result = new KeyGeneratorConfiguration("SNOWFLAKE", "order_id");
return result;
}
TableRuleConfiguration getOrderTableRuleConfiguration() {
TableRuleConfiguration result = new TableRuleConfiguration("t_order", "ds${0..1}.t_order${0..1}");
result.setKeyGeneratorConfig(getKeyGeneratorConfiguration());
return result;
}
TableRuleConfiguration getOrderItemTableRuleConfiguration() {
TableRuleConfiguration result = new TableRuleConfiguration("t_order_item", "ds${0..1}.t_order_item${0..1}");
return result;
}
Map<String, DataSource> createDataSourceMap() {
Map<String, DataSource> result = new HashMap<>();
result.put("ds0", DataSourceUtil.createDataSource("ds0"));
result.put("ds1", DataSourceUtil.createDataSource("ds1"));
return result;
}
Yaml配置
dataSources:
ds0: !!org.apache.commons.dbcp.BasicDataSource
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/ds0
username: root
password:
ds1: !!org.apache.commons.dbcp.BasicDataSource
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/ds1
username: root
password:
shardingRule:
tables:
t_order:
actualDataNodes: ds${0..1}.t_order${0..1}
databaseStrategy:
inline:
shardingColumn: user_id
algorithmExpression: ds${user_id % 2}
tableStrategy:
inline:
shardingColumn: order_id
algorithmExpression: t_order${order_id % 2}
keyGenerator:
type: SNOWFLAKE
column: order_id
t_order_item:
actualDataNodes: ds${0..1}.t_order_item${0..1}
databaseStrategy:
inline:
shardingColumn: user_id
algorithmExpression: ds${user_id % 2}
tableStrategy:
inline:
shardingColumn: order_id
algorithmExpression: t_order_item${order_id % 2}
bindingTables:
- t_order,t_order_item
broadcastTables:
- t_config
defaultDataSourceName: ds0
defaultTableStrategy:
none:
defaultKeyGenerator:
type: SNOWFLAKE
column: order_id
props:
sql.show: true
Spring命名空间配置
<?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="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="shardingDataSource" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" p:database="MYSQL" />
</property>
<property name="packagesToScan" value="org.apache.shardingsphere.example.core.jpa.entity" />
<property name="jpaProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
<prop key="hibernate.hbm2ddl.auto">create</prop>
<prop key="hibernate.show_sql">true</prop>
</props>
</property>
</bean>
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager" p:entityManagerFactory-ref="entityManagerFactory" />
<tx:annotation-driven />
<bean id="ds0" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/ds0" />
<property name="username" value="root" />
<property name="password" value="" />
</bean>
<bean id="ds1" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/ds1" />
<property name="username" value="root" />
<property name="password" value="" />
</bean>
<bean id="preciseModuloDatabaseShardingAlgorithm" class="org.apache.shardingsphere.example.algorithm.PreciseModuloShardingDatabaseAlgorithm" />
<bean id="preciseModuloTableShardingAlgorithm" class="org.apache.shardingsphere.example.algorithm.PreciseModuloShardingTableAlgorithm" />
<sharding:standard-strategy id="databaseShardingStrategy" sharding-column="user_id" precise-algorithm-ref="preciseModuloDatabaseShardingAlgorithm" />
<sharding:standard-strategy id="tableShardingStrategy" sharding-column="order_id" precise-algorithm-ref="preciseModuloTableShardingAlgorithm" />
<sharding:key-generator id="orderKeyGenerator" type="SNOWFLAKE" column="order_id" />
<sharding:key-generator id="itemKeyGenerator" type="SNOWFLAKE" column="order_item_id" />
<sharding:data-source id="shardingDataSource">
<sharding:sharding-rule data-source-names="ds0,ds1">
<sharding:table-rules>
<sharding:table-rule logic-table="t_order" actual-data-nodes="ds$->{0..1}.t_order$->{0..1}" database-strategy-ref="databaseShardingStrategy" table-strategy-ref="tableShardingStrategy" key-generator-ref="orderKeyGenerator" />
<sharding:table-rule logic-table="t_order_item" actual-data-nodes="ds$->{0..1}.t_order_item$->{0..1}" database-strategy-ref="databaseShardingStrategy" table-strategy-ref="tableShardingStrategy" key-generator-ref="itemKeyGenerator" />
</sharding:table-rules>
<sharding:binding-table-rules>
<sharding:binding-table-rule logic-tables="t_order, t_order_item" />
</sharding:binding-table-rules>
<sharding:broadcast-table-rules>
<sharding:broadcast-table-rule table="t_config" />
</sharding:broadcast-table-rules>
</sharding:sharding-rule>
</sharding:data-source>
</beans>
概念名词解释
在了解Sharding-JDBC的执行原理前,需要了解以下概念:
逻辑表: 水平拆分的数据表的总称。例:订单数据表根据主键尾数拆分为9张表,分别是mall_order_1、mall_order_2到mall_order_9,他们的逻辑表名为mall_order。
**真实表: ** 在分片的数据库中真实存在的物理表。即上个示例中的 mall_order_1 到 mall_order_9 。
数据节点: 数据分片的最小物理单元。由数据源名称和数据表组成,例:tmall-server-mall_0.mall_order_1
分片键: 用于分片的数据库字段,是将数据库(表)水平拆分的关键字段。例:将订单表中的订单主键的尾数取模分片,则订单主键为分片字段。SQL中如果无分片字段,将执行全路由,性能较差。除了对单分片字段的支持,Sharding-Jdbc也支持根据多个字段进行分片。
执行原理
当Sharding-JDBC接受到一条SQL语句时,会陆续执行 SQL解析 => 查询优化 => SQL路由 => SQL改写 => SQL执行 => 结果归并,最终返回执行结果。
水平分库
水平分库是把同一个表的数据按一定规则拆到不同的数据库中,每个库可以放在不同的服务器上。
(1)将原有tmall_server-mall库拆分为tmall_server-mall_1、tmall_server-mall_2
-- --------------------------------------------------------
-- 主机: 127.0.0.1
-- 服务器版本: 10.3.7-MariaDB - mariadb.org binary distribution
-- 服务器操作系统: Win64
-- HeidiSQL 版本: 9.4.0.5125
-- --------------------------------------------------------
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET NAMES utf8 */;
/*!50503 SET NAMES utf8mb4 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
-- 导出 tmall-server-mall 的数据库结构
DROP DATABASE IF EXISTS `tmall-server-mall_1`;
CREATE DATABASE IF NOT EXISTS `tmall-server-mall_1` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
USE `tmall-server-mall_1`;
-- 导出 表 tmall-server-mall.mall_order_1 结构
DROP TABLE IF EXISTS `mall_order_1`;
CREATE TABLE IF NOT EXISTS `mall_order_1` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '数据ID',
`buyer_id` bigint(20) unsigned DEFAULT 0 COMMENT '用户ID',
`buyer_username` varchar(50) DEFAULT '' COMMENT '用户名',
`order_no` varchar(50) DEFAULT '' COMMENT '订单编号',
`receiver_name` varchar(32) DEFAULT '' COMMENT '收货人',
`receiver_phone` varchar(32) DEFAULT '' COMMENT '收货电话',
`receiver_address` varchar(255) DEFAULT '' COMMENT '收货地址',
`goods_num` int(10) unsigned DEFAULT 0 COMMENT '商品数量',
`total_price` decimal(10,2) DEFAULT 0.00 COMMENT '商品销售总价',
`logistics_no` varchar(50) DEFAULT '' COMMENT '物流单号',
`pay_channel` int(10) unsigned DEFAULT 0 COMMENT '支付渠道:1=支付宝,2=微信',
`pay_method` int(10) unsigned DEFAULT 0 COMMENT '支付方式:1=在线支付,2=货到付款',
`order_state` tinyint(3) unsigned DEFAULT 0 COMMENT '订单状态: 0=待支付,1=已支付,待发货, 2=已发货/待收货,3=确认收货/已完成,4=用户关闭,5=平台关闭(商家),6=系统调度关闭',
`gmt_pay` datetime DEFAULT NULL COMMENT '支付时间',
`gmt_create` datetime DEFAULT NULL COMMENT '数据创建时间',
`gmt_modified` datetime DEFAULT NULL COMMENT '数据最后修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1002699311877718017 DEFAULT CHARSET=utf8mb4 COMMENT='商城-订单';
-- 正在导出表 tmall-server-mall.mall_order_1 的数据:~5 rows (大约)
/*!40000 ALTER TABLE `mall_order_1` DISABLE KEYS */;
INSERT INTO `mall_order_1` (`id`, `buyer_id`, `buyer_username`, `order_no`, `receiver_name`, `receiver_phone`, `receiver_address`, `goods_num`, `total_price`, `logistics_no`, `pay_channel`, `pay_method`, `order_state`, `gmt_pay`, `gmt_create`, `gmt_modified`) VALUES
(1002699311798026240, 2000, '11帅哥', 'jal12u911223', '1帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002699311814803456, 2000, '11帅哥', 'jal12u911223', '1帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002699311839969280, 2000, '11帅哥', 'jal12u911223', '1帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002699311856746496, 2000, '11帅哥', 'jal12u911223', '1帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002699311877718016, 2000, '11帅哥', 'jal12u911223', '1帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL);
/*!40000 ALTER TABLE `mall_order_1` ENABLE KEYS */;
-- 导出 表 tmall-server-mall.mall_order_2 结构
DROP TABLE IF EXISTS `mall_order_2`;
CREATE TABLE IF NOT EXISTS `mall_order_2` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '数据ID',
`buyer_id` bigint(20) unsigned DEFAULT 0 COMMENT '用户ID',
`buyer_username` varchar(50) DEFAULT '' COMMENT '用户名',
`order_no` varchar(50) DEFAULT '' COMMENT '订单编号',
`receiver_name` varchar(32) DEFAULT '' COMMENT '收货人',
`receiver_phone` varchar(32) DEFAULT '' COMMENT '收货电话',
`receiver_address` varchar(255) DEFAULT '' COMMENT '收货地址',
`goods_num` int(10) unsigned DEFAULT 0 COMMENT '商品数量',
`total_price` decimal(10,2) DEFAULT 0.00 COMMENT '商品销售总价',
`logistics_no` varchar(50) DEFAULT '' COMMENT '物流单号',
`pay_channel` int(10) unsigned DEFAULT 0 COMMENT '支付渠道:1=支付宝,2=微信',
`pay_method` int(10) unsigned DEFAULT 0 COMMENT '支付方式:1=在线支付,2=货到付款',
`order_state` tinyint(3) unsigned DEFAULT 0 COMMENT '订单状态: 0=待支付,1=已支付,待发货, 2=已发货/待收货,3=确认收货/已完成,4=用户关闭,5=平台关闭(商家),6=系统调度关闭',
`gmt_pay` datetime DEFAULT NULL COMMENT '支付时间',
`gmt_create` datetime DEFAULT NULL COMMENT '数据创建时间',
`gmt_modified` datetime DEFAULT NULL COMMENT '数据最后修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1002699311865135106 DEFAULT CHARSET=utf8mb4 COMMENT='商城-订单';
-- 正在导出表 tmall-server-mall.mall_order_2 的数据:~11 rows (大约)
/*!40000 ALTER TABLE `mall_order_2` DISABLE KEYS */;
INSERT INTO `mall_order_2` (`id`, `buyer_id`, `buyer_username`, `order_no`, `receiver_name`, `receiver_phone`, `receiver_address`, `goods_num`, `total_price`, `logistics_no`, `pay_channel`, `pay_method`, `order_state`, `gmt_pay`, `gmt_create`, `gmt_modified`) VALUES
(1002698066274287617, 1000, '帅哥', 'jal12u91123', '帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002698385767006209, 1000, '帅哥', 'jal12u91123', '帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002698438611042305, 1000, '帅哥', 'jal12u91123', '帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002698520777457665, 1000, '帅哥', 'jal12u91123', '帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002698562431090689, 1000, '帅哥', 'jal12u91123', '帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002698715539963905, 2000, '11帅哥', 'jal12u911223', '1帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002699311512813569, 2000, '11帅哥', 'jal12u911223', '1帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002699311806414849, 2000, '11帅哥', 'jal12u911223', '1帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002699311827386369, 2000, '11帅哥', 'jal12u911223', '1帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002699311848357889, 2000, '11帅哥', 'jal12u911223', '1帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002699311865135105, 2000, '11帅哥', 'jal12u911223', '1帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL);
/*!40000 ALTER TABLE `mall_order_2` ENABLE KEYS */;
/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */;
/*!40014 SET FOREIGN_KEY_CHECKS=IF(@OLD_FOREIGN_KEY_CHECKS IS NULL, 1, @OLD_FOREIGN_KEY_CHECKS) */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
-- --------------------------------------------------------
-- 主机: 127.0.0.1
-- 服务器版本: 10.3.7-MariaDB - mariadb.org binary distribution
-- 服务器操作系统: Win64
-- HeidiSQL 版本: 9.4.0.5125
-- --------------------------------------------------------
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET NAMES utf8 */;
/*!50503 SET NAMES utf8mb4 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
-- 导出 tmall-server-mall 的数据库结构
DROP DATABASE IF EXISTS `tmall-server-mall_2`;
CREATE DATABASE IF NOT EXISTS `tmall-server-mall_2` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
USE `tmall-server-mall_2`;
-- 导出 表 tmall-server-mall.mall_order_1 结构
DROP TABLE IF EXISTS `mall_order_1`;
CREATE TABLE IF NOT EXISTS `mall_order_1` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '数据ID',
`buyer_id` bigint(20) unsigned DEFAULT 0 COMMENT '用户ID',
`buyer_username` varchar(50) DEFAULT '' COMMENT '用户名',
`order_no` varchar(50) DEFAULT '' COMMENT '订单编号',
`receiver_name` varchar(32) DEFAULT '' COMMENT '收货人',
`receiver_phone` varchar(32) DEFAULT '' COMMENT '收货电话',
`receiver_address` varchar(255) DEFAULT '' COMMENT '收货地址',
`goods_num` int(10) unsigned DEFAULT 0 COMMENT '商品数量',
`total_price` decimal(10,2) DEFAULT 0.00 COMMENT '商品销售总价',
`logistics_no` varchar(50) DEFAULT '' COMMENT '物流单号',
`pay_channel` int(10) unsigned DEFAULT 0 COMMENT '支付渠道:1=支付宝,2=微信',
`pay_method` int(10) unsigned DEFAULT 0 COMMENT '支付方式:1=在线支付,2=货到付款',
`order_state` tinyint(3) unsigned DEFAULT 0 COMMENT '订单状态: 0=待支付,1=已支付,待发货, 2=已发货/待收货,3=确认收货/已完成,4=用户关闭,5=平台关闭(商家),6=系统调度关闭',
`gmt_pay` datetime DEFAULT NULL COMMENT '支付时间',
`gmt_create` datetime DEFAULT NULL COMMENT '数据创建时间',
`gmt_modified` datetime DEFAULT NULL COMMENT '数据最后修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1002699311877718017 DEFAULT CHARSET=utf8mb4 COMMENT='商城-订单';
-- 正在导出表 tmall-server-mall.mall_order_1 的数据:~5 rows (大约)
/*!40000 ALTER TABLE `mall_order_1` DISABLE KEYS */;
INSERT INTO `mall_order_1` (`id`, `buyer_id`, `buyer_username`, `order_no`, `receiver_name`, `receiver_phone`, `receiver_address`, `goods_num`, `total_price`, `logistics_no`, `pay_channel`, `pay_method`, `order_state`, `gmt_pay`, `gmt_create`, `gmt_modified`) VALUES
(1002699311798026240, 2000, '11帅哥', 'jal12u911223', '1帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002699311814803456, 2000, '11帅哥', 'jal12u911223', '1帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002699311839969280, 2000, '11帅哥', 'jal12u911223', '1帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002699311856746496, 2000, '11帅哥', 'jal12u911223', '1帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002699311877718016, 2000, '11帅哥', 'jal12u911223', '1帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL);
/*!40000 ALTER TABLE `mall_order_1` ENABLE KEYS */;
-- 导出 表 tmall-server-mall.mall_order_2 结构
DROP TABLE IF EXISTS `mall_order_2`;
CREATE TABLE IF NOT EXISTS `mall_order_2` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '数据ID',
`buyer_id` bigint(20) unsigned DEFAULT 0 COMMENT '用户ID',
`buyer_username` varchar(50) DEFAULT '' COMMENT '用户名',
`order_no` varchar(50) DEFAULT '' COMMENT '订单编号',
`receiver_name` varchar(32) DEFAULT '' COMMENT '收货人',
`receiver_phone` varchar(32) DEFAULT '' COMMENT '收货电话',
`receiver_address` varchar(255) DEFAULT '' COMMENT '收货地址',
`goods_num` int(10) unsigned DEFAULT 0 COMMENT '商品数量',
`total_price` decimal(10,2) DEFAULT 0.00 COMMENT '商品销售总价',
`logistics_no` varchar(50) DEFAULT '' COMMENT '物流单号',
`pay_channel` int(10) unsigned DEFAULT 0 COMMENT '支付渠道:1=支付宝,2=微信',
`pay_method` int(10) unsigned DEFAULT 0 COMMENT '支付方式:1=在线支付,2=货到付款',
`order_state` tinyint(3) unsigned DEFAULT 0 COMMENT '订单状态: 0=待支付,1=已支付,待发货, 2=已发货/待收货,3=确认收货/已完成,4=用户关闭,5=平台关闭(商家),6=系统调度关闭',
`gmt_pay` datetime DEFAULT NULL COMMENT '支付时间',
`gmt_create` datetime DEFAULT NULL COMMENT '数据创建时间',
`gmt_modified` datetime DEFAULT NULL COMMENT '数据最后修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1002699311865135106 DEFAULT CHARSET=utf8mb4 COMMENT='商城-订单';
-- 正在导出表 tmall-server-mall.mall_order_2 的数据:~11 rows (大约)
/*!40000 ALTER TABLE `mall_order_2` DISABLE KEYS */;
INSERT INTO `mall_order_2` (`id`, `buyer_id`, `buyer_username`, `order_no`, `receiver_name`, `receiver_phone`, `receiver_address`, `goods_num`, `total_price`, `logistics_no`, `pay_channel`, `pay_method`, `order_state`, `gmt_pay`, `gmt_create`, `gmt_modified`) VALUES
(1002698066274287617, 1000, '帅哥', 'jal12u91123', '帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002698385767006209, 1000, '帅哥', 'jal12u91123', '帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002698438611042305, 1000, '帅哥', 'jal12u91123', '帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002698520777457665, 1000, '帅哥', 'jal12u91123', '帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002698562431090689, 1000, '帅哥', 'jal12u91123', '帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002698715539963905, 2000, '11帅哥', 'jal12u911223', '1帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002699311512813569, 2000, '11帅哥', 'jal12u911223', '1帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002699311806414849, 2000, '11帅哥', 'jal12u911223', '1帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002699311827386369, 2000, '11帅哥', 'jal12u911223', '1帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002699311848357889, 2000, '11帅哥', 'jal12u911223', '1帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL),
(1002699311865135105, 2000, '11帅哥', 'jal12u911223', '1帅气老李', '', '', 0, 0.00, '', 0, 0, 0, NULL, NULL, NULL);
/*!40000 ALTER TABLE `mall_order_2` ENABLE KEYS */;
/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */;
/*!40014 SET FOREIGN_KEY_CHECKS=IF(@OLD_FOREIGN_KEY_CHECKS IS NULL, 1, @OLD_FOREIGN_KEY_CHECKS) */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
(2)分片规则修改
由于数据库拆分了两个,这里需要配置两个数据源(m1和m2,注意也要把原来的库改为两个新的库)
分库需要配置分库的策略,和分表策略的意义类似,通过分库策略实现数据操作针对分库的数据库进行操作。
server.port=9090
spring.application.name =sharding-demo
spring.main.allow-bean-definition-overriding = true
mybatis.configuration.map-underscore-to-camel-case=true
#配置数据源,名字可以自行定义,需要与下方配置保持一致
spring.shardingsphere.datasource.names=m1,m2
#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://192.168.8.100:3306/tmall-server-mall_1?useUnicode=true
spring.shardingsphere.datasource.m1.username=root
spring.shardingsphere.datasource.m1.password=123qqq...A
#m2数据源配置
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://192.168.8.100:3306/tmall-server-mall_2?useUnicode=true
spring.shardingsphere.datasource.m2.username=root
spring.shardingsphere.datasource.m2.password=123qqq...A
#配置分库策略,以buyer_id为分片键,分片策略为buyer_id %2+1
#buyer_id 为偶数操作m1数据源,buyer_id为奇数则操作m2数据源
spring.shardingsphere.sharding.tables.mall_order.database-strategy.inline.sharding-column=buyer_id
spring.shardingsphere.sharding.tables.mall_order.database-strategy.inline.algorithm-expression=m$->{buyer_id % 2 + 1}
#指定mall_order表的数据分布情况,配置数据节点
spring.shardingsphere.sharding.tables.mall_order.actual-data-nodes=m1.mall_order_$->{1..2}
#指定主键生成策略为SNOWFLAKE,避免主键重复
spring.shardingsphere.sharding.tables.mall_order.key-generator.column=id
spring.shardingsphere.sharding.tables.mall_order.key-generator.type=SNOWFLAKE
#配置表的分片策略,分片策略包含了分片健和分片算法
spring.shardingsphere.sharding.tables.mall_order.table-strategy.inline.sharding-column=id
spring.shardingsphere.sharding.tables.mall_order.table-strategy.inline.algorithm-expression=mall_order_$->{id % 2 + 1}
#打开SQL输出日志
spring.shardingsphere.props.sql.show=true
为了方便验证,删除原有数据
mysql> DELETE FROM `tmall-server-mall_1`.mall_order_1;
mysql> DELETE FROM `tmall-server-mall_1`.mall_order_2;
mysql> DELETE FROM `tmall-server-mall_2`.mall_order_1;
mysql> DELETE FROM `tmall-server-mall_2`.mall_order_2;
编写测试类验证,因为水平分库是根据buyer_id进行的分片,所以每次循环都应该不一样,所以为buyer_id+i
package cn.tedu.springboot.sharding.demo;
import cn.tedu.springboot.sharding.demo.mapper.OrderMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@SpringBootTest
class SpringBootShardingDemoApplicationTests {
//注入对象,OrderMapper
@Autowired
OrderMapper orderMapper;
@Test
void contextLoads() {
//调用insertOrder方法
for (int i = 0; i < 10; i++) {
//此处让buyer_id+i,分库的分片规则是按照buyer_id进行的分片,所以需要每次都不一样
orderMapper.insertOrder(2000L+i, "11帅哥", "jal12u911223", "1帅气老李");
}
}
//查询数据
@Test
public void selectOrderIds(){
//此处的ID号可以换成自己的表记录中的数据
List<Long> orderIds = Arrays.asList(1008183953959419904l,1008183953980391425l);
List<Map> orders = orderMapper.selectOrders(orderIds);
orders.forEach( order -> System.out.println(order));
}
}
通过执行发现,
根据库的分片规则(buyer_id)有的数据写进入了tmall-server-mall_1库,有的数据进入了tmall-server-mall_2库
根据表的分片规则(id)有的数据进入了tmall-server-mall_1.mall_order_1表
根据表的分片规则(id)有的数据进入了tmall-server-mall_1.mall_order_2表
根据表的分片规则(id)有的数据进入了tmall-server-mall_2.mall_order_1表
根据表的分片规则(id)有的数据进入了tmall-server-mall_2.mall_order_2表
buyer_id%2+1决定进入哪个库
id%2+1决定进入库的哪个表
验证查询操作,修改配置文件application.properties
server.port=9090
spring.application.name =sharding-demo
spring.main.allow-bean-definition-overriding = true
mybatis.configuration.map-underscore-to-camel-case=true
#配置数据源,名字可以自行定义,需要与下方配置保持一致
spring.shardingsphere.datasource.names=m1,m2
#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://192.168.8.100:3306/tmall-server-mall_1?useUnicode=true
spring.shardingsphere.datasource.m1.username=root
spring.shardingsphere.datasource.m1.password=123qqq...A
#m2数据源配置
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://192.168.8.100:3306/tmall-server-mall_2?useUnicode=true
spring.shardingsphere.datasource.m2.username=root
spring.shardingsphere.datasource.m2.password=123qqq...A
#配置分库策略,以buyer_id为分片键,分片策略为buyer_id %2+1
#buyer_id 为偶数操作m1数据源,buyer_id为奇数则操作m2数据源
spring.shardingsphere.sharding.tables.mall_order.database-strategy.inline.sharding-column=buyer_id
spring.shardingsphere.sharding.tables.mall_order.database-strategy.inline.algorithm-expression=m$->{buyer_id % 2 + 1}
#指定mall_order表的数据分布情况,配置数据节点,有两个库,修改这里,告知有两个库
spring.shardingsphere.sharding.tables.mall_order.actual-data-nodes=m$->{1..2}.mall_order_$->{1..2}
#指定主键生成策略为SNOWFLAKE,避免主键重复
spring.shardingsphere.sharding.tables.mall_order.key-generator.column=id
spring.shardingsphere.sharding.tables.mall_order.key-generator.type=SNOWFLAKE
#配置表的分片策略,分片策略包含了分片健和分片算法
spring.shardingsphere.sharding.tables.mall_order.table-strategy.inline.sharding-column=id
spring.shardingsphere.sharding.tables.mall_order.table-strategy.inline.algorithm-expression=mall_order_$->{id % 2 + 1}
#打开SQL输出日志
spring.shardingsphere.props.sql.show=true
修改测试类中查询部分的id值
package cn.tedu.springboot.sharding.demo;
import cn.tedu.springboot.sharding.demo.mapper.OrderMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@SpringBootTest
class SpringBootShardingDemoApplicationTests {
//注入对象,OrderMapper
@Autowired
OrderMapper orderMapper;
@Test
void contextLoads() {
//调用insertOrder方法
for (int i = 0; i < 10; i++) {
orderMapper.insertOrder(2000L+i, "11帅哥", "jal12u911223", "1帅气老李");
}
}
//查询数据
@Test
public void selectOrderIds(){
//此处的ID号可以换成自己的表记录中的数据,数据从表中查询即可
List<Long> orderIds = Arrays.asList(1008204780222283776L);
List<Map> orders = orderMapper.selectOrders(orderIds);
orders.forEach( order -> System.out.println(order));
}
}
已经执行了查询,但此刻在m1库和m2库都做了查询,是因为分库的时候是根据buyer_id进行的分片
解决方案,查询的时候只在一个库里边查询
修改OrderMapper,支持根据buyerid进行查找
package cn.tedu.springboot.sharding.demo.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 java.util.List;
import java.util.Map;
@Mapper
public interface OrderMapper {
//编写新增方法,通过insert注解,指定新增的SQL语句
//当调用insertOrder方法的时候,等于执行上方注解指定的SQL语句
//Param注解用于指定新增时的参数
@Insert("insert into mall_order(buyer_id,buyer_username,order_no,receiver_name) " +
"values(#{buyerId},#{buyerUsername},#{orderNo},#{receiverName})")
int insertOrder(@Param("buyerId") Long buyerId, @Param("buyerUsername") String buyerUsername
, @Param("orderNo") String orderNo, @Param("receiverName") String receiverName);
//查询订单
@Select("<script>" +
"select" +
" id,buyer_id,buyer_username" +
" from mall_order " +
" where id in " +
"<foreach collection='orderIds' open='(' separator=',' close=')' item='id'>" +
"#{id} " +
"</foreach>" +
"and buyer_id = #{buyerID }" +
"</script>")
List<Map> selectOrders(@Param("orderIds") List<Long> orderIds,@Param("buyerID") Long buyerID );
}
修改测试类
package cn.tedu.springboot.sharding.demo;
import cn.tedu.springboot.sharding.demo.mapper.OrderMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@SpringBootTest
class SpringBootShardingDemoApplicationTests {
//注入对象,OrderMapper
@Autowired
OrderMapper orderMapper;
@Test
void contextLoads() {
//调用insertOrder方法
for (int i = 0; i < 10; i++) {
orderMapper.insertOrder(2000L+i, "硕博11帅哥", "jal12u911223", "1帅气老李");
}
}
//查询数据
@Test
public void selectOrderIds(){
//此处的ID号可以换成自己的表记录中的数据,数据从表中查询即可
//增加buyerId的定义,通过orderIds和buyerId来查询
//注意buyerId和id的值要对应
List<Long> orderIds = Arrays.asList(1008204780222283776L);
Long buyerId=2001L;
List<Map> orders = orderMapper.selectOrders(orderIds,buyerId);
orders.forEach( order -> System.out.println(order));
}
}
执行流程分析
SQL 解析:
-
insert into mall_order(buyer_id,buyer_username,order_no,receiver_name) values(1999L,"硕博11帅哥","jal12u911223","1帅气老李")
-
Sharding-JDBC 解析该SQL语句,识别出需要插入
mall_order
表,并提取出各个字段和值
路由计算:
- 根据分片配置,Sharding-JDBC 使用
buyerId=1999
进行路由计算,决定该记录应插入到m2:tall-mall-order_2
库中 - 再根据 id 值进行路由计算,决定该记录插入到 mall_order_1 表中
SQL重写:
-
构建新的 SQL 语句
-
insert into mall_order_1 (buyer_id, buyer_username, order_no, receiver_name, id) VALUES (?, ?, ?, ?, ?) ::: [1999, 硕博11帅哥, jal12u911223, 1帅气老李, 1005082312909520896]
SQL执行:
-
将重写后的 SQL 语句发送到
m2
数据库的表mall_order_1
中进行插入操作。 -
返回插入成功的结果,或处理任何可能出现的错误。
垂直分库(补充)
垂直分库是指按照业务将表进行分类,分布到不同的数据库上面,每个库可以放在不同的服务器上,它的核心理念是专库专用
创建tmall-server-user`数据库
DROP DATABASE IF EXISTS `tmall-server-user`;
CREATE DATABASE IF NOT EXISTS `tmall-server-user` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
USE `tmall-server-user`;
在tmall-server-user库中创建 mall_user表
CREATE TABLE mall_user
(
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '数据ID',
username VARCHAR(32) DEFAULT '' COMMENT '用户名',
password VARCHAR(128) DEFAULT '' COMMENT '密码(密文)',
avatar VARCHAR(255) DEFAULT '' COMMENT '头像URL',
phone VARCHAR(32) DEFAULT '' COMMENT '手机号码',
email VARCHAR(64) DEFAULT '' COMMENT '电子邮箱',
description VARCHAR(255) DEFAULT '' COMMENT '简介',
enable TINYINT UNSIGNED DEFAULT 0 COMMENT '是否启用,1=启用,0=未启用',
last_login_ip VARCHAR(32) DEFAULT '' COMMENT '最后登录IP地址(冗余)',
login_count INT UNSIGNED DEFAULT 0 COMMENT '累计登录次数(冗余)',
gmt_last_login DATETIME DEFAULT NULL COMMENT '最后登录时间(冗余)',
gmt_create DATETIME DEFAULT NULL COMMENT '数据创建时间',
gmt_modified DATETIME DEFAULT NULL COMMENT '数据最后修改时间',
PRIMARY KEY (id)
) DEFAULT CHARSET = utf8mb4 COMMENT ='用户';
配置分片策略
server.port=9090
spring.application.name =sharding-demo
spring.main.allow-bean-definition-overriding = true
mybatis.configuration.map-underscore-to-camel-case=true
#配置数据源,名字可以自行定义,需要与下方配置保持一致
spring.shardingsphere.datasource.names=m1,m2,m3
#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/tmall-server-mall_1?useUnicode=true
spring.shardingsphere.datasource.m1.username=root
spring.shardingsphere.datasource.m1.password=root
#m2数据源配置
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/tmall-server-mall_2?useUnicode=true
spring.shardingsphere.datasource.m2.username=root
spring.shardingsphere.datasource.m2.password=root
#m3数据源配置
spring.shardingsphere.datasource.m3.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m3.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.m3.url=jdbc:mysql://localhost:3306/tmall-server-user?useUnicode=true
spring.shardingsphere.datasource.m3.username=root
spring.shardingsphere.datasource.m3.password=root
#配置分库策略,以user_id为分片键,分片策略为user_id %2+1 ,user_id 为偶数操作m1 数据源,为奇数则操作m2数据源
spring.shardingsphere.sharding.tables.mall_order.database-strategy.inline.sharding-column=buyer_id
spring.shardingsphere.sharding.tables.mall_order.database-strategy.inline.algorithm-expression=m$->{buyer_id % 2 + 1}
#指定mall_order表的数据分布情况,配置数据节点
#spring.shardingsphere.sharding.tables.mall_order.actual-data-nodes=m1.mall_order_$->{1..2}
spring.shardingsphere.sharding.tables.mall_order.actual-data-nodes=m$->{1..2}.mall_order_$->{1..2}
#配置tmall-order-user下的数据节点
spring.shardingsphere.sharding.tables.mall_user.actual-data-nodes=m3.mall_user
#指定主键生成策略为SNOWFLAKE,避免主键重复
spring.shardingsphere.sharding.tables.mall_order.key-generator.column=id
spring.shardingsphere.sharding.tables.mall_order.key-generator.type=SNOWFLAKE
#配置表的分片策略,分片策略包含了分片健和分片算法
spring.shardingsphere.sharding.tables.mall_order.table-strategy.inline.sharding-column=id
spring.shardingsphere.sharding.tables.mall_order.table-strategy.inline.algorithm-expression=mall_order_$->{id % 2 + 1}
#配置表的分片策略,分片策略包含了分片健和分片算法
spring.shardingsphere.sharding.tables.mall_user.table-strategy.inline.sharding-column=id
spring.shardingsphere.sharding.tables.mall_user.table-strategy.inline.algorithm-expression=mall_user
#打开SQL输出日志
spring.shardingsphere.props.sql.show=true
用户数据操作
编写UserMapper持久层接口,完成查询方法和新增方法定义
package cn.tedu.springboot.sharding.demo.dao;
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 java.util.List;
import java.util.Map;
@Mapper
public interface UserMapper {
@Insert("insert into mall_user(username,password) values(#{username},#{password})")
int insertUser(@Param("username") String username,@Param("password")String password);
//查询用户
@Select("<script>" +
"select" +
" *" +
" from mall_user " +
" where id in " +
"<foreach collection='userIds' open='(' separator=',' close=')' item='id'>" +
"#{id} " +
"</foreach>" +
"</script>")
List<Map> selectOrders(@Param("userIds") List<Long> userIds);
}
在SpringBootShardingDemoApplicationTests类中,进行userMapper的数据新增功能测试
@Autowired
private UserMapper userMapper;
@Test
public void insertUser() {
for (int i = 0; i < 10; i++) {
userMapper.insertUser("liyuan","123456");
}
}
@Test
public void selectUserByIds() {
List<Map> maps =
userMapper.selectUsers(Arrays.asList(1L,2L,3L));
maps.forEach(map -> System.out.println(map));
}
测试结果如下: