天天看点

Sharding-JDBC实现分库分表和读写分离1、分库分表定义2、分库分表案例3、读写分离案例4、其它问题

目录

1、分库分表定义

2、分库分表案例

3、读写分离案例

4、其它问题

在遇到数据量大、性能瓶颈的时候,分库分表和读写分离能够很好解决此类问题。

Sharding-JDBC是ShardingSphere生态圈的产品之一。

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

本文参考官方文档:https://shardingsphere.apache.org/document/legacy/3.x/document/cn/overview/

1、分库分表定义

分库分表包括分库和分表两个部分,在生产中通常包括:垂直(纵向)分表、垂直(纵向)分库、水平分库、水平分表四种方式。

很好理解,垂直就是拆分,水平就是扩展。

  • 垂直分表:可以把一个宽表的字段按访问频次、是否是大字段的原则拆分为多个表,这样既能使业务清晰,还能提升部分性能。拆分后,尽量从业务角度避免联查,否则性能方面将得不偿失。
  • 垂直分库:可以把多个表按业务耦合松紧归类,分别存放在不同的库,这些库可以分布在不同服务器,从而使访问压力被多服务器负载,大大提升性能,同时能提高整体架构的业务清晰度,不同的业务库可根据自身情况定制优化方案。但是它需要解决跨库带来的所有复杂问题。
  • 水平分库:可以把一个表的数据(按数据行)分到多个不同的库,每个库只有这个表的部分数据,这些库可以分布在不同服务器,从而使访问压力被多服务器负载,大大提升性能。它不仅需要解决跨库带来的所有复杂问题,还要解决数据路由的问题。
  • 水平分表:可以把一个表的数据(按数据行)分到多个同一个数据库的多张表中,每个表只有这个表的部分数据,这样做能小幅提升性能,它仅仅作为水平分库的一个补充优化。

2、分库分表案例

现在有一张用户表t_user,打算采用水平分库分表。根据数据量和访问量,拆成4张表,平均存到2个库中。以自增的user_id取模进行路由。

(1)算法分析:假定10条数据,user_id是1~10。

  • 首先是平均分到2个库,user_id%2结果是0或1,就命名两个库(其实是数据源)为ds0、ds1,ds0存的记录user_id为2、4、6、8、10,ds1存的记录user_id为1、3、5、7、9。
  • 然后是每个库平均分到2张表,怎么分呢?user_id%4:ds0的结果是0或2,那么两张表可以命名为t_user0和t_user2;ds1的结果是1或3,那么两张表可以命名为t_user1和t_user3。
  • 综上,分库采用user_id%2(2个库),分表采用user_id%4(4张表)。最终存储情况是:4、8存到ds0.t_user0,2、6、10存到ds0.t_user2,1、5、9存到ds1.t_user1,3、7存到ds1.t_user3。

据此,我们建立2个库和4张表:ds0.t_user0、ds0.t_user2、ds1.t_user1、ds1.t_user3。示例如下(不用纠结是否规范)

CREATE TABLE `t_user0` (
  `user_id` bigint unsigned NOT NULL COMMENT '主键',
  `name` varchar(64) NOT NULL COMMENT '姓名',
  PRIMARY KEY (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表'
           

(2)新建Spring Boot项目,引入依赖

<!-- for spring boot -->
        <dependency>
            <groupId>io.shardingsphere</groupId>
            <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
            <version>3.1.0</version>
        </dependency>
           

(3)配置properties.yml

sharding:
  jdbc:
    datasource:
      names: ds0,ds1
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://ip:port/ds0
        username: root
        password: 123456
      ds1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://ip:port/ds1
        username: root
        password: 123456

    config:
      sharding:
        # 数据分片
        default-database-strategy:
          inline:
            sharding-column: user_id
            algorithm-expression: ds$->{user_id % 2}
        tables:
          t_user:
            actual-data-nodes: ds$->{0..1}.t_user$->{0..3}
            key-generator-column-name: user_id
            # 内置:SNOWFLAKE、UUID
            key-generator-class-name: cn.zhh.keygen.AutoIncrementKeyGenerator
            table-strategy:
              inline:
                sharding-column: user_id
                algorithm-expression: t_user$->{user_id % (2 * 2)}
           

其中的主键生成器,我们为了测试方便,就随便写了一个(实际肯定不能这么干,得采用分布式ID方案)。

package cn.zhh.keygen;

import io.shardingsphere.core.keygen.KeyGenerator;

import java.util.concurrent.atomic.AtomicLong;

/**
 * 自增主键生成器
 */
public class AutoIncrementKeyGenerator implements KeyGenerator {

    private static final AtomicLong KEY_GENERATOR = new AtomicLong(1L);

    @Override
    public Number generateKey() {
        return KEY_GENERATOR.getAndIncrement();
    }
}
           

(4)测试

我们直接用JdbcTemplate操作,插入10条记录,自动生成它们的user_id为1~10。

@Test
    public void testInsert() {
        for (int i = 0; i < 10; i++) {
            String sql = String.format("insert into t_user (name) values ('%s')", UUID.randomUUID().toString());
            jdbcTemplate.execute(sql);
        }
    }
           

结果如我所料,比如ds0.t_user0的数据

Sharding-JDBC实现分库分表和读写分离1、分库分表定义2、分库分表案例3、读写分离案例4、其它问题

3、读写分离案例

我们设定slave0是ds0的从库,slave1是ds1的从库。人懒+节省时间,就不搭建真正的主从了,直接利用Navicat的数据传输工具把ds0和ds1的结构和数据分别同步到slave0和slave1,就算配置好了#_#。

修改下properties.yml的配置

sharding:
  jdbc:
    datasource:
      names: ds0,ds1,slave0,slave1
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://ip:port/ds0
        username: root
        password: 123456
      ds1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://ip:port/ds1
        username: root
        password: 123456
      slave0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://ip:port/slave0
        username: root
        password: 123456
      slave1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://ip:port/slave1
        username: root
        password: 123456

    config:
      sharding:
        # 数据分片
        default-database-strategy:
          inline:
            sharding-column: user_id
            algorithm-expression: ds$->{user_id % 2}
        tables:
          t_user:
            actual-data-nodes: ds$->{0..1}.t_user$->{0..3}
            key-generator-column-name: user_id
            # 内置:SNOWFLAKE、UUID
            key-generator-class-name: cn.zhh.keygen.AutoIncrementKeyGenerator
            table-strategy:
              inline:
                sharding-column: user_id
                algorithm-expression: t_user$->{user_id % (2 * 2)}
        # 读写分离
        master-slave-rules:
          ds0:
            master-data-source-name: ds0
            slave-data-source-names: slave0
          ds1:
            master-data-source-name: ds1
            slave-data-source-names: slave1
           

为了看出读哪个库,我们改下从库里面表数据的name值

update slave0.t_user0 set name = 'slave0.t_user0';
update slave0.t_user2 set name = 'slave0.t_user2';
update slave1.t_user1 set name = 'slave1.t_user1';
update slave1.t_user3 set name = 'slave1.t_user3';
           

测试一下

@Test
    public void testSelect() {
        for (int i = 0; i < 10; i++) {
            int userId = i + 1;
            String sql = String.format("select name from t_user where user_id = %s", userId);
            String name = jdbcTemplate.queryForObject(sql, String.class);
            System.out.printf("userId: %s, name: %s%n", userId, name);
        }
    }
           

结果正常

Sharding-JDBC实现分库分表和读写分离1、分库分表定义2、分库分表案例3、读写分离案例4、其它问题

4、其它问题

采用了分库分表,难免会遇到一些问题。这里稍微列举一下

(1)join、order by、limit:尽量避免 / 代码层面处理 / 有些工具支持。

(2)分布式事务:强一致性几种方案 / 考虑最终一致性 / 有些工具支持。

(3)唯一ID

  • 雪花算法(Sharding-JDBC内置)
  • UUID(Sharding-JDBC内置)
  • Redis生成
  • 专门一个数据库的自增生成

(4)其它条件查询

  • 映射法:比如t_user需要根据手机号码查询,可以建立一张user_id与mobile的映射表。
  • 冗余法:比如t_user需要每个月注册的用户,可以冗余一份以月份路由的数据。
  • NoSQL中间件:全量数据同步ES等。

文章案例代码已上传至github:https://github.com/zhouhuanghua/sharding-jdbc-demo.git。

继续阅读