Sharding Sphere分库分表


Sharding Sphere

(Sharding jdbc分库分表学习已基本完成,相关整合SpringBoot使用实战项目netty_taxi存放在笔记相关实战目录中,并且实现了更完美的日期分表。)

官网地址:http://shardingsphere.apache.org/index_zh.html

简述

Sharding Sphere是一套开源的分布式数据库中间件解决方案,目前主要产品由Sharding-JDBC和Sharding-Proxy组成,定位为关系型数据库中间件,可以合理的在分布式环境下利用关系型数据库的计算和存储能力。

此次学习,主要学习如何使用Sharding-JDBC和Sharding-Proxy进行数据库分库分表操作。

什么是分库分表

当数据库数据量愈发庞大的时候,单表数据量过多会造成数据crud的压力,让服务的效率变低。为解决这种情况,可以根据业务对数据库数据进行分库分表操作,使得单个数据库和单个表中数据量降低,减少数据访问的压力,提高服务的性能。

Sharding-JDBC

简介

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


Sharding-JDBC不是用来实现分库分表的,分库分表是需要在数据库设计的时候就完成的,进行提前建表和分库,而Sharding-JDBC只是以jar包的形式提供给我们一个方便访问和管理多个数据源和多个表的中间件。

实现水平分表

1、引入sharding-jdbc依赖

        
            org.apache.shardingsphere
            sharding-jdbc-spring-boot-starter
            4.0.0-RC1
        

2、准备数据库资源

sharding-jdbc只是简化对于分表分库操作的中间件,所以在使用sharding-jdbc之前需要先准备分库分表的基本数据库和表结构。

在此,准备一个名为traffic_pay的数据库,数据库中创建order_01、order_02两个数据库表,约定根据订单号的奇偶将不同的订单数据存放到不同的order表中。

如下图所示:


order表结构如下:

3、准备操作数据库相关代码

准备数据库表实体、操作数据库mapper、单元测试等文件

4、进行数据源和sharding-jdbc配置

(关于sharding-jdbc相关配置,可以在官网找到参考案例......)

# sharding-jdbc分片策略配置
# 配置数据源名称
spring:
  shardingsphere:
    datasource:
      names: traffic-pay

# 配置数据源的详细内容
      traffic-pay:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/traffic_pay
        username: root
        password: 123456

# 配置数据库表的分布情况,例如表的名称,表的所在数据库
    sharding:
      tables:
        order:
          actual-data-nodes: traffic-pay.order_$->{01..02}

# 指定数据库表的主键以及主键的生成策略  SNOWFLAKE:雪花算法生成主键
          key-gengrator:
            column: id
            type: SNOWFLAKE

# 指定分片策略,约定orderId为偶数的添加到order_01表,orderId为基数的添加到order_02表
          table-strategy:
            inline:
              sharding-column: order_id
              algorithm-expression: order_$->{order_id % 2 + 1}

# 允许sql日志输出
    props:
      sql:
        show: true

# 允许一个实体类覆盖多个相同结构的表来操作
  main:
    allow-bean-definition-overriding: true

以上就是对于分表的全部配置,其中表在数据库的分布情况以及分片策略用到了行表达式来进行配置。

traffic-pay.order_$->{01..02}表示的是这个配置所操作的表是针对于traffic-pay.order_1traffic-pay.order_2表进行的。

order_$->{order_id % 2 + 1}表示的是针对order_id字段来进行分表策略的规定,order_id除以二之后再加一的值拼接上前面的表名部分即为当前要操作的表。

5、进行junit单元测试

package com.xsh.study;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xsh.study.bean.Order;
import com.xsh.study.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.Date;

@SpringBootTest
class StudyApplicationTests {

    @Autowired
    private OrderMapper orderMapper;

    @Test
    void addOrder(){

        Order order = new Order();
        order.setOrderId(2L);
        order.setOrderMessage("first insert");
        order.setCreateTime(new Date());

        orderMapper.insert(order);

    }

    @Test
    void getOrder(){

        QueryWrapper orderQueryWrapper = new QueryWrapper<>();
        orderQueryWrapper.eq("order_id",1);
        Order order = orderMapper.selectOne(orderQueryWrapper);
        System.out.println(order);

    }
}

实战:根据日期进行水平分表

上面我们实现了根据order_id的奇偶性来进行水平分表,但是工作中我大多接触到的是对于庞大的订单进行每日的分表策略,接下来看下如何实现。

1、创建数据库表结构


event表结构如下:


可以看到,表设计中有一个shard_date的分表键,shard_date是日期信息分表键,例如event_20210606表中shard_date的值就都会是20210606日期信息,我将根据这个分表键字段来进行数据的分片。在进行数据插入和查询的时候可以获取系统当前时间来维护分表键,来决定数据是操作哪张表。

2、更改数据库配置

# sharding-jdbc分片策略配置
# 配置数据源名称
spring:
  shardingsphere:
    datasource:
      names: traffic-pay

# 配置数据源的详细内容
      traffic-pay:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://lcoalhost:3306/traffic_pay
        username: root
        password: 123456

# 配置数据库表的分布情况,例如表的名称,表的所在数据库
    sharding:
      tables:
        event:
          actual-data-nodes: traffic-pay.event_$->{20210606..20210608}
          table-strategy:
            standard:
              sharding-column: shard_date
              precise-algorithm-class-name: com.xsh.study.stratepy.DatePreciseShardStrategy


# 允许sql日志输出
    props:
      sql:
        show: true

# 允许一个实体类覆盖多个相同结构的表来操作
  main:
    allow-bean-definition-overriding: true

由yml配置可以看到,对于简单的订单id取模实现奇偶分片不同,采取了别样的配置。

  • actual-data-nodes: traffic-pay.event_$->{20210606..20210608}这里的行表达式20210606..20210608表示数据库表命名在这个范围区间的表,在此表示event_20210606、event_20210607、event_20210608三张表;

  • sharding-column: shard_date指定分表键字段名称,在插入和查询的时候都会自动根据分表键去决定操作哪张表;

  • precise-algorithm-class-name: com.xsh.study.stratepy.DatePreciseShardStrategy指定分表策略配置类

3、创建分表策略配置类

package com.xsh.study.stratepy;

import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;

import java.util.Collection;

/**
 * Created with IntelliJ IDEA.
 *
 * @Auther: xiashihua
 * @Date: 2021/06/06/10:19
 * @Description:
 */
public class DatePreciseShardStrategy implements PreciseShardingAlgorithm {

    @Override
    public String doSharding(Collection collection, PreciseShardingValue preciseShardingValue) {
        String value = preciseShardingValue.getValue();

        for (String str : collection) {
            if(str.endsWith(value)){
                return str;
            }
        }
        throw new IllegalArgumentException("未找到匹配的数据表");
    }
}

分表策略配置需要实现PreciseShardingAlgorithm接口,其中有一个doSharding(),有两个参数:

  • PreciseShardingValue preciseShardingValue表示的是查询sql语句中分表键的信息,通过preciseShardingValue.getValue()可以获取到分表键的值;
  • Collection collection表示的是配置中的表名列表,循环这个列表并且和分表键参数匹配就可以找到此次sql操作想要操作的表空间。

4、编写单元测试

    @Test
    void addEvent(){
        Event event = new Event();
        event.setShardDate("20210608");
        event.setEventId(1L);
        event.setEventMessage("20210606 message");
        event.setCreateTime(new Date());

        eventMapper.insert(event);
    }

    @Test
    void getEvent(){
        QueryWrapper eventQueryWrapper = new QueryWrapper<>();
        eventQueryWrapper.eq("shard_date","20210607");

        List events = eventMapper.selectList(eventQueryWrapper);
        events.stream().forEach(x -> {
            System.out.println(events);
        });
    }

(需要注意的是,sharding-jdbc只是负责对于分库分表之后sql的操作,在之前我们还需要自己准备好多张分表。如上方实例所示,如果按照订单的日期进行分表的话可以使用定时任务的方式,每天启动提前执行建表语句来创建好对应的表。)

实现水平分库

水平分库即在水平分表的基础之上进行水平分库,将创建traffic_pay1和traffic_pay2两个数据库,其中同时存在order_1和order_2相同结构的表。约定,将主键id为奇数的存放在traffic_pay2数据库,id为偶数的存放在traffic_pay1数据库,并且,order_id为奇数的存放在order_2表中,为偶数的存放在order_1中。

1、准备数据库和表结构

2、进行水平分库配置

# sharding-jdbc分片策略配置
# 配置数据源名称,因为分库所以有多数据源
spring:
  shardingsphere:
    datasource:
      names: traffic-pay1,traffic-pay2

      # 配置数据源的详细内容
      traffic-pay1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/traffic_pay1
        username: root
        password: 123456
      traffic-pay2:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/traffic_pay2
        username: root
        password: 123456

    # 配置数据库及数据库表的分布情况,例如表的名称,表的所在数据库
    sharding:
      tables:
        order:
          actual-data-nodes: traffic-pay$->{1..2}.order_$->{1..2}

          # 指定数据库表的主键以及主键的生成策略  SNOWFLAKE:雪花算法生成主键
          key-gengrator:
            column: id
            type: SNOWFLAKE

          # 指定表的分片策略,约定orderId为偶数的添加到order_01表,orderId为奇数的添加到order_02表
          table-strategy:
            inline:
              sharding-column: order_id
              algorithm-expression: order_$->{order_id % 2 + 1}
          # 指定数据库的分片策略,约定id为偶数的添加到traffic_pay1数据库中,id为奇数的添加到traffic_pay2数据库中
          data-base-strategy:
            inline:
              sharding-column: id
              algorithm-expression: traffic-pay$->{id % 2 + 1}
#      default-database-strategy:
#        inline:
#          sharding-column: id
#          algorithm-expression: traffic_pay$->{id % 2 +1}   对所配置的数据源中所有表添加此策略

    # 允许sql日志输出
    props:
      sql:
        show: true

  # 允许一个实体类覆盖多个相同结构的表来操作
  main:
    allow-bean-definition-overriding: true

如上配置所示,水平分库我们需要配置多个数据库的数据源,然后对数据库和数据库表都进行数据分片的策略。其中,数据库分片策略有两种配置方式:


截图中,注释起来的部分是针对于数据库所有的表都使用该分片策略,为default策略,而上面的配置是可以根据tables的配置来针对性的对某个表进行分片策略配置。

3、单元测试编写

    @Test
    void partDatabaseAddOrder(){
        for (int i = 0; i < 10; i++) {
            Order order = new Order();
            order.setOrderId(2L);
            order.setOrderMessage("......");
            order.setCreateTime(new Date());

            orderMapper.insert(order);
        }
    }

    @Test
    void partDatabaseGetOrder(){
        QueryWrapper queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("id",1401826029816061953L);
        queryWrapper.eq("order_id",2L);

        Order order = orderMapper.selectOne(queryWrapper);
        System.out.println(order);
    }

实现垂直分库

垂直分库的概念体现在专表专库,例如用户信息的表存放在user_db中,订单相关的表存放在order_db中,互不干扰,垂直切分。使用sharding-jdbc可以快速的在多个数据库中操作数据,避免繁琐的操作。

1、创建数据库和表结构


2、准备对应实体类以及mapper

3、进行垂直分库策略配置

# sharding-jdbc分片策略配置
# 配置数据源名称,因为分库所以有多数据源
spring:
  shardingsphere:
    datasource:
      names: user,order

      # 配置数据源的详细内容
      user:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://8.131.86.156:3306/user_db
        username: root
        password: 123456
      order:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://8.131.86.156:3306/order_db
        username: root
        password: 123456

    # 配置数据库及数据库表的分布情况,例如表的名称,表的所在数据库
    sharding:
      tables:
        t_user:
          actual-data-nodes: user.t_user
          key-gengrator:
            column: id
            type: SNOWFLAKE
          table-strategy:
            inline:
              sharding-column: id
              algorithm-expression: t_user
        t_order:
          actual-data-nodes: order.t_order
          key-gengrator:
            column: id
            type: SNOWFLAKE
          table-strategy:
            inline:
              sharding-column: id
              algorithm-expression: t_order
    # 允许sql日志输出
    props:
      sql:
        show: true

  # 允许一个实体类覆盖多个相同结构的表来操作
  main:
    allow-bean-definition-overriding: true

4、编写测试用例

    @Test
    public void addUser(){
        User user = new User();
        user.setUserId(1L);
        user.setUserName("张三");

        userMapper.insert(user);
    }

    @Test
    public void getUser(){
        QueryWrapper userQueryWrapper = new QueryWrapper<>();
        userQueryWrapper.eq("user_name","张三");
        System.out.println(userMapper.selectOne(userQueryWrapper));
    }

    @Test
    public void addOrder(){
        Order order = new Order();
        order.setOrderId(1L);
        order.setOrderMessage("111");

        orderMapper.insert(order);
    }

关于使用sharding-jdbc垂直分库做多数据源管理的猜想

sharding-jdbc垂直分库可以通过配置的方式使我们在操作多个数据源时得心应手,这让我想到,平常开发中遇到多数据源的情况还是有很多的,使用sharding-jdbc做多数据源管理是否更方便。

然后,我在order_db中建立了表t_event:


然后添加对应的数据分片策略:

    sharding:
      tables:
        t_user:
          actual-data-nodes: user.t_user
          key-gengrator:
            column: id
            type: SNOWFLAKE
          table-strategy:
            inline:
              sharding-column: id
              algorithm-expression: t_user
        t_order:
          actual-data-nodes: order.t_order
          key-gengrator:
            column: id
            type: SNOWFLAKE
          table-strategy:
            inline:
              sharding-column: id
              algorithm-expression: t_order
        t_event:
          actual-data-nodes: order.t_event
          key-gengrator:
            column: id
            type: SNOWFLAKE
          table-strategy:
            inline:
              sharding-column: id
              algorithm-expression: t_event

果然,在进行配置之后多数据源操作变得非常简单,不需要再额外的添加任何切换数据源的操作,并且在同一个单元测试中操作两个不同的数据源也正常无误!

之前,面对多数据源操作我一般会选择Mybatis Plus的@DS注解来实现,使用这个注解需要引入如下的依赖包:

        
            com.baomidou
            dynamic-datasource-spring-boot-starter
            3.0.0
        

并且在同一事务中会出现数据源切换失败的情况,这个时候就需要对失败的持久层方法进行新开启一个事务来完成操作,新开启一个事务需要使用到这个注解:@Transactional(propagation = Propagation.REQUIRES_NEW)

    @DS("houseDepartment")
    @MapKey(value = "id")
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    Map getDictionValueByMap(@Param("dictType") String dictType);

如上的使用方法就可以解决@DS注解失效的问题。

所以,采用shardng-jdbc来维护多数据源的关系似乎就不需要这么繁琐的操作,但是同时也增加了基础架构以及配置文件的负担,而且如果表非常多的话会造成配置文件非常臃肿。