使用ShardingJDBC进行水平分库分表

1,583 阅读7分钟

背景

近些年随着数据量的暴涨,但凡涉及到互联网的产品或者其他传统行业的产品,都会涉及到海量的数据。对于单业务数据过于庞大的情况,由于单库容量有限,且在已经进行垂直分库的情况下,数据量仍然非常庞大且持续扩增,不但后续数据无法存储,且对吸性能也会产生影响。当前流行的方案一般有NoSQL和分库分表,NoSQL近期开源,我对其的了解并不多,且并没有在项目中使用过,所以此次讲下分库分表的方式来满足我们的业务扩张的需求。

ShardingSphere的简单介绍

ShardingJDBC作为ShardingSphere生态圈中的一员,以数据库中间件的身份,为应用提供了数据库路由的功能。引用ShardingSphere官方的一段介绍:

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

Apache ShardingSphere 定位为关系型数据库中间件,旨在充分合理地在分布式的场景下利用关系型数据库的计算和存储能力,而并非实现一个全新的关系型数据库。 它通过关注不变,进而抓住事物本质。关系型数据库当今依然占有巨大市场,是各个公司核心业务的基石,未来也难于撼动,我们目前阶段更加关注在原有基础上的增量,而非颠覆。

Apache ShardingSphere 5.x 版本开始致力于可插拔架构,项目的功能组件能够灵活的以可插拔的方式进行扩展。 目前,数据分片、读写分离、数据加密、影子库压测等功能,以及对 MySQL、PostgreSQL、SQLServer、Oracle 等 SQL 与协议的支持,均通过插件的方式织入项目。 开发者能够像使用积木一样定制属于自己的独特系统。Apache ShardingSphere 目前已提供数十个 SPI 作为系统的扩展点,而且仍在不断增加中。

ShardingSphere 已于2020年4月16日成为 Apache 软件基金会的顶级项目。

鉴于其最新开源,且文档和资源较为丰富,我们采用ShardingJDBC作为我们的中间件来实现分库分表。

定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。 它使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架。

简单的分库分表实现

在参照官网的demo实现的时候,有些概念没有理解,所以把demo照搬下来的时候有很多问题,跑不起来,很是苦恼。所以整理一个肯定可以跑起来的demo供给大家参考。

准备工作

  • MySQL数据库
  • Maven,网络环境畅通,可以正常下载依赖
  • 开发Java的IDE(IDEA Ultimate最佳)

开发步骤

  1. 创建一个Maven工程,我们使用了SpringBoot作为我们的框架,所以也可以使用Spring Initializer来进行项目初始化,这里不做要求。在工程创建好之后,我们编写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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.example</groupId>
  <artifactId>ShardingJdbcDemo</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
  </properties>

  <parent>
    <artifactId>spring-boot-starter-parent</artifactId>
    <groupId>org.springframework.boot</groupId>
    <version>2.4.1</version>
  </parent>
  
  <dependencies>
    <!-- spring boot web 依赖 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- spring boot test 依赖 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <!-- mybatis 依赖 -->
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.1.3</version>
    </dependency>
    <!-- 数据库驱动 -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!-- 引入数据源,这里注意下不要引入Spring Boot的数据源的Starter,ShardingJDBC和SpringBoot都会进行配置,会冲突导致无法启动 -->
    <dependency>
      <groupId>com.zaxxer</groupId>
      <artifactId>HikariCP</artifactId>
      <version>3.4.5</version>
    </dependency>
    <!-- 引入ShardingJDBC依赖,当前的最新版本为4.1.1 -->
    <dependency>
      <groupId>org.apache.shardingsphere</groupId>
      <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
      <version>4.1.1</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>
  
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>2.4.0</version>
      </plugin>
    </plugins>
  </build>
</project>
  1. 创建数据库表。我们等下要模拟水平分库和分表,所以我们创建一个这样的数据模型,即订单模型。订单模型有订单id和创建该订单的userId,我们通过订单id进行分表,用户id进行分库。分库需要两个数据库,我们命名为db1和db2,然后分别在db1和db2下面创建两张表,分别是t_order_1和t_order_2。我这里使用的是本地的mysql,建表语句如下:
# 创建数据库db1并创建对应的表
drop database if exists db1;
create database db1;
use db1;
create table t_order_1
(
    order_id bigint       not null,
    `desc`   varchar(128) null,
    user_id  bigint       null,
    constraint t_order_1_order_id_uindex
        unique (order_id)
);
alter table t_order_1
    add primary key (order_id);
create table t_order_2
(
    order_id bigint       not null,
    `desc`   varchar(128) null,
    user_id  bigint       null,
    constraint t_order_2_order_id_uindex
        unique (order_id)
);
alter table t_order_2
    add primary key (order_id);

# 创建数据库db2并创建对应的表
drop database if exists db2;
create database db2;
use db2;
create table t_order_1
(
    order_id bigint       not null,
    `desc`   varchar(128) null,
    user_id  bigint       null,
    constraint t_order_1_order_id_uindex
        unique (order_id)
);
alter table t_order_1
    add primary key (order_id);
create table t_order_2
(
    order_id bigint       not null,
    `desc`   varchar(128) null,
    user_id  bigint       null,
    constraint t_order_2_order_id_uindex
        unique (order_id)
);
alter table t_order_2
    add primary key (order_id);
  1. 在resources下创建application.properties,并进行如下的配置。虽然官方推荐可以用yaml的方式,但是yaml的缩进在不熟悉的情况下容易搞错,各位可以自行决策。 application.properties:
spring.application.name=sharding-demo
server.port=8080

# 声明数据源,名称不需要和数据库名同名,这里可以随意命名(必须)
spring.shardingsphere.datasource.names=ds1,ds2
# 配置ds1数据源(必须)。其中type为数据源类型,这里选用Hikari,其余的根据数据源的需求来配置。
# jdbc-url这个属性是Hikari要用的,其他的数据源如Druid的为url属性,不同的数据源的key不同,请按照实际情况处理
spring.shardingsphere.datasource.ds1.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds1.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.ds1.jdbc-url=jdbc:mysql://localhost:3306/db1?useSSL=false&useUnicode=true&characterEncoding=UTF-8
spring.shardingsphere.datasource.ds1.username=root
spring.shardingsphere.datasource.ds1.password=Huawei12#$
# 配置ds2数据源(必须)
spring.shardingsphere.datasource.ds2.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds2.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.ds2.jdbc-url=jdbc:mysql://localhost:3306/db2?useSSL=false&useUnicode=true&characterEncoding=UTF-8
spring.shardingsphere.datasource.ds2.username=root
spring.shardingsphere.datasource.ds2.password=Huawei12

# 配置分库策略(可选),我这里让t_order表根据user_id进行分库。在spring boot中,占位符需要用$->{}来表示,官方示例中的${}无法使用,这里要注意
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.sharding-column=user_id
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.algorithm-expression=ds$->{user_id % 2 + 1}
# 配置实际的数据节点(必须),我们既分库又分表,且库和表的数量都是2,所以下面的表达式这样写:
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes=ds$->{1..2}.t_order_$->{1..2}
# 配置分表策略(必须)。我们通过order_id进行分表,奇数的order_id会路由到t_order_2,偶数路由到t_order_1。
# 如果使用了ShardingJDBC,那么即使不分表的话,也要配置该分表的策略(直接把表写死即可,不添加变量)
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}
# 配置主键生成策略(可选),我们只配置order_id的生成,因为是表的主键,
# 我们在写插入的语句的时候就可以不需要传order_id,ShardingJDBC会自动使用SNOWFLAKE算法生成,然后拼接到SQL中插入
spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id
spring.shardingsphere.sharding.tables.t_order.key-generator.type=SNOWFLAKE
# 配置打印实际执行的SQL(生产环境关闭)
spring.shardingsphere.props.sql.show=true
  1. 创建SpringBoot的启动类
package org.example.sharding;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApp {
    public static void main(String[] args) {
        SpringApplication.run(DemoApp.class, args);
    }
}
  1. 创建OrderMapper接口类
package org.example.sharding;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

@Mapper
@Repository
public interface OrderMapper {

    @Insert("insert into t_order (desc, user_id) values (#{desc}, #{userId})")
    int insert(@Param("desc") String desc, @Param("userId") long userId);
}
  1. 创建测试用例,我们模拟5个用户,每个用户创建10个订单,看下插入后的数据分布情况:
package org.example.sharding;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.Locale;

@RunWith(SpringRunner.class)
@SpringBootTest
public class OrderMapperTest {

    @Autowired
    private OrderMapper orderMapper;

    @Test
    public void testInsert() {
        for (long user = 1L; user <= 5L; ++user) {
            for (long i = 1L; i <= 10L; ++i) {
                orderMapper.insert(String.format(Locale.CHINA, "%s%d", "订单", i), user);
            }
        }
    }
}

在执行的日志中,我们可以看到如下的日志: 根据user_id将数据插入了ds2和ds1;根据order_id将数据分散到t_order_2和t_order_1,至此我们的分库和分表已经成功了。感兴趣还可以进行查询等操作,会根据传入的参数进行解析,散到不同的库和表中。

写在后面

这个分库分表花了点时间才把这个跑通,上面这些只能说我们初步的完成了分库和分表。在扩容方面、主从同步、读写分离等方面我们还没有介绍,需要我以及各位继续去学习和试验。中间遇到的错误也不少,诸如:如果不配置数据库的字符集会出现sql无法解析报空指针的错误等。如果时间允许的话,我后续再做主从同步和读写分离的demo,欢迎各位提问。

参考

  1. ShardingSphere
  2. 2020最新技术ShardingJDBC分库分表