在 SpringBoot中整合ShardingSphere 分库分表

在 SpringBoot中整合ShardingSphere 分库分表

Apache 于北京时间2020年4月15日 宣布 Apache ShardingSphere 毕业成为 Apache顶级项目,

Apache ShardingSphere 是一款分布式数据库中间件,该项目由当当网在 2018年11月10日 捐入 Apache,并在京东数科逐渐发展壮大,成为 业界首个Apache分布式数据库中间件项目。

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

主要分为3个独立的项目

  • Sharding-JDBC :定义为轻量级Java框架,在Java的JDBC层提供额外的服务,完全兼容各种ORM框架
  • Sharding-Proxy :定位为数据库代理端,我们的项目通过它去访问数据库,统一管理配置
  • Sharding-Sidecar(规划中):用于K8S上的

本篇主要讲解 通过Sharding-JDBC 的使用去实现分库分表

1. 准备环境

  • ​ Apache ShardingSphere 作为分库分表中间件

SpringData+JPA 作为数据访问层


* ```
 Docker + Mysql  数据库

1. 创建项目配置依赖

1.1 创建一个SpringBoot项目

勾选Web环境和Mysql 驱动

1.2 添加 Sharding-JDBC 依赖 (最新版本4.0.1)

 <!-- Apache shardingsphere-jdbc -->
	 <dependency>
      <groupId>org.apache.shardingsphere</groupId>
      <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
      <version>4.0.1</version>
   </dependency>

附上POM文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.johnny</groupId>
    <artifactId>shardingsphere</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>shardingsphere</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>



        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>


        <!-- shardingsphere -->
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
            <version>4.0.1</version>
        </dependency>

        <!-- lombpk -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- starter-data-jpa -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>


    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

2. 准备2个Mysql数据库

既然要分库 那么先准备 2个Mysql 数据库, 我这里为了方便直接使用Docker 创建2个Mysql,我选择的是mysql5.7版本

2.1 拉取Mysql5.7依赖

docker pull mysql:5.7

image

2.2 启动Mysql

映射端口 3336 初始化密码 123456

docker run -itd --name mysql01  -p 3336:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql:5.7

image
映射端口 3337 初始化密码 123456

docker run -itd --name mysql02  -p 3337:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql:5.7

image
使用数据库连接工具测试一下 连接正常
image
image

3. 分库分表配置

3.1 先配置 端口和jpa等基础配置

如果你使用Mybatis可以直接配置Mybatis的配置 因为Sharding-JDBC在DataSource之上的一层 不关心你使用什么ORM

server:
  port: 6018
spring:
  application:
    name: ShardingSphere

  jpa:
    database: mysql
    show-sql: true
    open-in-view: true
    hibernate:
      ddl-auto: update
      naming:
        strategy: org.hibernate.cfg.DefaultComponentSafeNamingStrategy
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect
        format_sql: true

3.2 实体订单表Order和Repository

/**
 * 订单表
 *
 * @author johnny
 * @create 2020-04-23 下午1:07
 **/
@Data
@Table(name = "tb_order")
@Entity
public class Order  implements Serializable {

    @Column(name = "id")
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "order_id")
    private Long orderId;

    @Column(name = "order_name")
    private String orderName;

    @Column(name = "user_id")
    private Long userId;


}
/**
 * @author johnny
 * @create 2020-04-23 下午1:12
 **/
public interface OrderRepository extends JpaRepository<Order, Long> {


}

3.3 分库分表配置

这里打算分 2个库 和 2个表

order0 → order0.tb_order0 order0.tb_order1

order1-> order1.tb_order1 order1.tb_order1

spring:
  shardingsphere: #不是spring.datasouce,而是 spring.shardingsphere.datasouce 说明在datasouce上层
    props:
      sql:
        show: true


    datasource:
      names: ds0,ds1 #数据库名称 分片策略时候使用
      ds0:
        type: com.zaxxer.hikari.HikariDataSource  #配置数据库连接池,你可以选择 你喜欢的DBCP,C3P0等
        driver-class-name: com.mysql.jdbc.Driver #Mysql数据库驱动
        jdbc-url: jdbc:mysql://127.0.0.1:3336/order0? useUnicode=true&zeroDateTimeBehavior=convertToNull&characterEncoding=UTF-8&characterSetResults=UTF-8&autoReconnect=true&useSSL=false
        #docker安装的mysql地址 数据库order0 需要手动先创建好
        username: root
        password: 123456
        hikari:
          minimum-idle: 5
          idle-timeout: 600000
          maximum-pool-size: 10
          auto-commit: true
          pool-name: MyHikariCP
          max-lifetime: 1800000
          connection-timeout: 30000
          connection-test-query: SELECT 1



      ds1:  #和上面一样
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.jdbc.Driver
        jdbc-url: jdbc:mysql://127.0.0.1:3337/order1?useUnicode=true&zeroDateTimeBehavior=convertToNull&characterEncoding=UTF-8&characterSetResults=UTF-8&autoReconnect=true&useSSL=false
        username: root
        password: 123456
        hikari:
          minimum-idle: 5
          idle-timeout: 600000
          maximum-pool-size: 10
          auto-commit: true
          pool-name: MyHikariCP
          max-lifetime: 1800000
          connection-timeout: 30000
          connection-test-query: SELECT 1


		#重要!
    sharding:
      default-database-strategy:   #数据库的分片策略 ,我这里根据 user_id % 2去 分片到不同的数据库
        inline:
          sharding-column: user_id  #分片的 列
          algorithm-expression: ds$->{user_id % 2}   #具体的分片算法  这是 groovy语法 
      binding-tables: tb_order   #需要分片的 表
      tables:
        tb_order:
          actual-data-nodes: ds$->{0..1}.tb_order$->{0..1} #实际表选择的节点
          table-strategy.inline.sharding-column: order_id  #表分片的列 
          table-strategy.inline.algorithm-expression: tb_order$->{order_id % 2} #分表的算法和上面类型
					

大致意思就是 先配置 2个数据源 ,然后配置 数据库的分片策略 ,再配置 Order表的分片策略 ,还是很简单的 仔细看看

4. 编写测试类 ,测试分库分表

@SpringBootTest
class ShardingsphereApplicationTests {

    @Autowired
    private OrderRepository orderRepository;
    @Test
    void testInsert() {

        //根据策略  会被定位到  ds1.tb_order1
        Order order = new Order();
        order.setUserId(1L);   //userId  1 % 2 = 1 会分到 ds1 数据库
        order.setOrderId(1L); //orderId  1 % 2 = 1 会被分到 tb_order1 数据表
        order.setOrderName("Apache ShardingSphere 入门与精通");

        //根据策略  会被定位到  ds0.tb_order0
        Order order1 = new Order();
        order1.setUserId(2L);  //userId  2 % 2 = 0 会分到 ds0 数据库
        order1.setOrderId(2L);  //orderId  2 % 2 = 0 会被分到 tb_order0 数据表
        order1.setOrderName("Kotlin 入门到精通");

        orderRepository.save(order);
        orderRepository.save(order1);

        System.out.println(orderRepository);

    }
 }

运行前 先创建 Order0 Order1 数据库
image
Log打印 先打印我们配置的分片规则


JPA会自动创建表,根据分片策略 会在 2个数据库里创建 4个表

order0 → order0.tb_order0 order0.tb_order1
order1-> order1.tb_order1 order1.tb_order1

Hibernate: 
    
    create table tb_order (
       id bigint not null auto_increment,
        order_id bigint,
        order_name varchar(255),
        user_id bigint,
        primary key (id)
    ) engine=InnoDB
2020-04-23 23:18:49.586  INFO 25550 --- [           main] ShardingSphere-SQL                       : Rule Type: sharding
2020-04-23 23:18:49.586  INFO 25550 --- [           main] ShardingSphere-SQL                       : Logic SQL: 
    create table tb_order (
       id bigint not null auto_increment,
        order_id bigint,
        order_name varchar(255),
        user_id bigint,
        primary key (id)
    ) engine=InnoDB
2020-04-23 23:18:49.586  INFO 25550 --- [           main] ShardingSphere-SQL                       : SQLStatement: CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.ddl.CreateTableStatement@5ce0f50a, tablesContext=TablesContext(tables=[Table(name=tb_order, alias=Optional.absent())], schema=Optional.absent()))
2020-04-23 23:18:49.587  INFO 25550 --- [           main] ShardingSphere-SQL                       : Actual SQL: ds0 ::: 
    create table tb_order0 (
       id bigint not null auto_increment,
        order_id bigint,
        order_name varchar(255),
        user_id bigint,
        primary key (id)
    ) engine=InnoDB
2020-04-23 23:18:49.587  INFO 25550 --- [           main] ShardingSphere-SQL                       : Actual SQL: ds0 ::: 
    create table tb_order1 (
       id bigint not null auto_increment,
        order_id bigint,
        order_name varchar(255),
        user_id bigint,
        primary key (id)
    ) engine=InnoDB
2020-04-23 23:18:49.587  INFO 25550 --- [           main] ShardingSphere-SQL                       : Actual SQL: ds1 ::: 
    create table tb_order0 (
       id bigint not null auto_increment,
        order_id bigint,
        order_name varchar(255),
        user_id bigint,
        primary key (id)
    ) engine=InnoDB
2020-04-23 23:18:49.587  INFO 25550 --- [           main] ShardingSphere-SQL                       : Actual SQL: ds1 ::: 
    create table tb_order1 (
       id bigint not null auto_increment,
        order_id bigint,
        order_name varchar(255),
        user_id bigint,
        primary key (id)
    ) engine=InnoDB

最后打印了 sql语句, 可以看到逻辑sql 还是 insert into tb_order 但是


打印日志如下:

Hibernate: 
    insert 
    into
        tb_order
        (order_id, order_name, user_id) 
    values
        (?, ?, ?)
2020-04-23 23:18:51.373  INFO 25550 --- [           main] ShardingSphere-SQL                       : Rule Type: sharding
2020-04-23 23:18:51.373  INFO 25550 --- [           main] ShardingSphere-SQL                       : Logic SQL: insert into tb_order (order_id, order_name, user_id) values (?, ?, ?)
2020-04-23 23:18:51.373  INFO 25550 --- [           main] ShardingSphere-SQL                       : SQLStatement: InsertSQLStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.InsertStatement@63f08f9f, tablesContext=TablesContext(tables=[Table(name=tb_order, alias=Optional.absent())], schema=Optional.absent())), columnNames=[order_id, order_name, user_id], insertValueContexts=[InsertValueContext(parametersCount=3, valueExpressions=[ParameterMarkerExpressionSegment(startIndex=61, stopIndex=61, parameterMarkerIndex=0), ParameterMarkerExpressionSegment(startIndex=64, stopIndex=64, parameterMarkerIndex=1), ParameterMarkerExpressionSegment(startIndex=67, stopIndex=67, parameterMarkerIndex=2)], parameters=[1, Apache ShardingSphere 入门与精通, 1])])
2020-04-23 23:18:51.373  INFO 25550 --- [           main] ShardingSphere-SQL                       : Actual SQL: ds1 ::: insert into tb_order1 (order_id, order_name, user_id) values (?, ?, ?) ::: [1, Apache ShardingSphere 入门与精通, 1]
Hibernate: 
    insert 
    into
        tb_order
        (order_id, order_name, user_id) 
    values
        (?, ?, ?)
2020-04-23 23:18:51.426  INFO 25550 --- [           main] ShardingSphere-SQL                       : Rule Type: sharding
2020-04-23 23:18:51.426  INFO 25550 --- [           main] ShardingSphere-SQL                       : Logic SQL: insert into tb_order (order_id, order_name, user_id) values (?, ?, ?)
2020-04-23 23:18:51.426  INFO 25550 --- [           main] ShardingSphere-SQL                       : SQLStatement: InsertSQLStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.InsertStatement@63f08f9f, tablesContext=TablesContext(tables=[Table(name=tb_order, alias=Optional.absent())], schema=Optional.absent())), columnNames=[order_id, order_name, user_id], insertValueContexts=[InsertValueContext(parametersCount=3, valueExpressions=[ParameterMarkerExpressionSegment(startIndex=61, stopIndex=61, parameterMarkerIndex=0), ParameterMarkerExpressionSegment(startIndex=64, stopIndex=64, parameterMarkerIndex=1), ParameterMarkerExpressionSegment(startIndex=67, stopIndex=67, parameterMarkerIndex=2)], parameters=[2, Kotlin 入门到精通, 2])])
2020-04-23 23:18:51.426  INFO 25550 --- [           main] ShardingSphere-SQL                       : Actual SQL: ds0 ::: insert into

查看Mysql 数据库 发现确实分到了不同的库 和不通的表
image

5. 自增主键遗留的问题

由于分到了不同的表,而自增主键会重复 ,导致查询出来后 id 会重复,所以在这种情况下不适合使用自增主键

  @Test
    void textSelect() {
        List<Order> orderList = orderRepository.findAll();
        System.out.println(orderList);
    }

可以看到 id =1 重复


这时候你可能会想到使用 UUID代替自增注解,其实Mysql是不建议使用UUID作为主键的,过长而且无序会导致查询性能问题,其实有美团提供的 Leaf 基于雪花算法的 分布式主键ID 可以解决这个问题,后面我会写一篇关于Leaf 的文章到时候结合这个

6. 总结

本篇主要讲解 如何使用Sharding-JDBC 去实现 数据库的分库分表操作,其实很简单 主要是分片策略的制定,还遗留了一个关于自增主键ID 的问题, 后续使用 Leaf 替代, 下一篇我会介绍如何使用 Sharding-JDBC 实现读写分离,结合本篇的 分库分表


原文:Johnny小屋