侧边栏壁纸
博主头像
太上问情 博主等级

人非太上,岂能忘情。

  • 累计撰写 12 篇文章
  • 累计创建 9 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

Sharding Sphere

太上问情
2024-11-26 / 0 评论 / 0 点赞 / 10 阅读 / 0 字 / 正在检测是否收录...

Sharding Sphere

一、概念

什么是Sharding Sphere?

Apache ShardingSphere 是一款分布式的数据库生态系统, 可以将任意关系型数据库转换为分布式数据库,并通过数据分片、弹性伸缩、加密等能力对原有数据库进行增强。

ShardingSphere-JDBC 定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。

ShardingSphere-Proxy 定位为透明化的数据库代理端,通过实现数据库二进制协议,对异构语言提供支持。

什么是分库分表?

字面意思,将一个数据库拆分为多个数据库,将一个表拆分为多个表。其中又有水平切分和垂直切分两种。分库分表的出现是为了解决

垂直切分

垂直分库

将一个数据库中的多个表,根据不同的业务场景或者其他规则,切分为多个不同的数据库。

垂直分表

将一个表中的多个字段,根据不同的业务场景或者其他规则,拆分为多个不同的表。

水平切分

水平分库

将一个数据库拆分为多个具有相同表的数据库,数据按照一定的规则,存放到不同的数据库中。

水平分表

将一张表拆分为多个具有相同表结构的表,数据按照一定的规则,存放到不同的表中。

分库分表的应用和问题

应用

  1. 垂直切分:在数据库设计阶段的时候,就应该考虑好垂直分库和垂直分表的情况。因为随着系统功能的不断完善,后续在进行垂直切分的难度是指数级的。
  2. 水平切分:随着数据库数据量增加,不要马上考虑做水平切分,首先考虑通过索引、缓存、读写分离等手段处理,当上述方式都无法解决问题的时候,在考虑要不要进行水平分库或水平分表。

分库分表的问题

  1. 跨节点关联查询问题,分页、排序问题
  2. 多数据源管理的问题
  3. 分布式事务问题

各种分库分表方案和思考

Hash分表

使用某个字段的计算hash值,根据计算得到的hash值,将数据存储到对应的表。

1730491725708

没有热点数据问题,适合高并发场景,但扩容迁移数据痛苦

range分表(范围分表)

按照一定的范围划分存储数据,比如0-1000万存储到1表,后续每1000万存储一张表。

1730491784785

不需要迁移数据,但是有热点数据问题,高并发场景下可能 压力都在某个表里面

分库分表后的数据迁移

分库分表后大数据量的查询可以考虑

二、Sharding-JDBC

简介

1、Sharding-JDBC是一个开源的轻量级Java框架,在JDBC层提供服务,原先是当当网开发的,后来加入了Apache孵化器,成为了其中的顶级项目。

2、Sharding-JDBC提供了数据分片和读写分离等功能,简化了对分库分表之后数据的相关操作。

1730483945888

sharding-jdbc实现水平分表

假设在数据库sharding中,你有两张表:t_order_0t_order_1,订单信息存放到这两张表中。

我们将根据订单ID 进行分表,使用以下算法:

order_id % 2 == 0 时,路由到 t_order_0

order_id % 2 == 1 时,路由到 t_order_1

1、sql

-- 创建数据库
CREATE DATABASE sharding;
USE sharding;

-- 创建 t_order_0 表
CREATE TABLE t_order_0 (
    order_id BIGINT PRIMARY KEY COMMENT '订单ID,主键',
    user_id INT NOT NULL COMMENT '用户ID',
    status VARCHAR(50) NOT NULL COMMENT '订单状态',
    create_time DATETIME NOT NULL COMMENT '订单创建时间'
) COMMENT='订单表0';

-- 创建 t_order_1 表
CREATE TABLE t_order_1 (
    order_id BIGINT PRIMARY KEY COMMENT '订单ID,主键',
    user_id INT NOT NULL COMMENT '用户ID',
    status VARCHAR(50) NOT NULL COMMENT '订单状态',
    create_time DATETIME NOT NULL COMMENT '订单创建时间'
) COMMENT='订单表1';

2、pom.xml

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
            <version>5.0.0</version>
        </dependency>
        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
            <version>5.0.0</version>
        </dependency>
    </dependencies>

3、application.yml

spring:
  shardingsphere:
    props:
      sql-show: true
    datasource:
      names: ds
      ds:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/sharding
        username: root
        password: taishang
    rules:
      sharding:
        tables:
          t_order:
            actual-data-nodes: ds.t_order_$->{0..1}
            # 分表策略
            table-strategy:
              standard:
                # 分片算法名称
                sharding-algorithm-name: order-inline
                # 分片所用的列
                sharding-column: order_id
            # 主键生成策略
            key-generate-strategy:
              column: order_id
              key-generator-name: snowflake
        # 分片算法
        sharding-algorithms:
          order-inline:
            type: INLINE
            props:
              algorithm-expression: t_order_$->{order_id % 2}

4、实体类

@Data
@TableName("t_order")
public class Order implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 订单ID,主键
     */
    @TableId
    private Long orderId;

    /**
     * 用户ID
     */
    private Integer userId;

    /**
     * 订单状态
     */
    private String status;

    /**
     * 订单创建时间
     */
    private Date createTime;

}

5、Mapper

public interface OrderMapper extends BaseMapper<Order> {}

6、测试类

我们不需要区分数据在哪张表上,sharding-jdbc会自动处理路由。

@SpringBootTest
public class Test1 {

    @Autowired
    private OrderMapper orderMapper;

    @Test
    void selectAll() {
        List<Order> orders = orderMapper.selectList(null);
        orders.forEach(System.out::println);
    }

    @Test
    void selectById() {
        QueryWrapper<Order> qw = new QueryWrapper<>();
        qw.eq("order_id", "1058846700250071041");

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

    @Test
    void insert() {
        for (int i = 0; i < 100; i++) {
            Order order = new Order();
            order.setUserId(i);
            order.setStatus("1");
            order.setCreateTime(new Date());
            orderMapper.insert(order);
        }
    }

    @Test
    void update() {
        QueryWrapper<Order> qw = new QueryWrapper<>();
        qw.eq("user_id", 2);
        Order order = orderMapper.selectOne(qw);
        // 如果使用uer_id作为分片规则,需要将user_id放到where后面,并且保证update语句中两个值一致。否则修改将会报错。
        order.setStatus("测试");
        orderMapper.updateById(order);
    }

    @Test
    void deleteAll() {
        orderMapper.delete(null);
    }
}

7、插入结果

1730384274683

sharding-jdbc实现水平分库

假设你有两个数据库:db0db1,用户表 user 分布在这两个数据库中。

我们将根据用户 ID 进行分库,使用以下算法:

user_id % 2 == 0 时,路由到 db0

user_id % 2 == 1 时,路由到 db1

1、sql

CREATE DATABASE db0;
CREATE DATABASE db1;

USE db0;
CREATE TABLE user (
                      user_id INT PRIMARY KEY COMMENT '用户ID',
                      username VARCHAR(100) COMMENT '用户名',
                      email VARCHAR(100) COMMENT '电子邮件',
                      created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) COMMENT='用户表';

USE db1;
CREATE TABLE user (
                      user_id INT PRIMARY KEY COMMENT '用户ID',
                      username VARCHAR(100) COMMENT '用户名',
                      email VARCHAR(100) COMMENT '电子邮件',
                      created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) COMMENT='用户表';

2、pom.xml

同上面水平分表。

3、application.yaml

spring:
  shardingsphere:
    props:
      sql-show: true
    datasource:
      names: db0,db1
      db0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/db0
        username: root
        password: taishang
      db1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/db1
        username: root
        password: taishang
    rules:
      sharding:
        tables:
          user:
            actual-data-nodes: db$->{0..1}.user

            #分库策略
            database-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: db-inline

            # 主键生成策略
            key-generate-strategy:
              column: user_id
              key-generator-name: snowflake

        # 分片算法
        sharding-algorithms:
          db-inline:
            type: INLINE
            props:
              algorithm-expression: db$->{user_id % 2}

4、实体类

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("user")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 用户ID
     */
    @TableId(value = "user_id")
    private Long userId;

    /**
     * 用户名
     */
    @TableField("username")
    private String username;

    /**
     * 电子邮件
     */
    @TableField("email")
    private String email;

    /**
     * 创建时间
     */
    @TableField("created_at")
    private LocalDateTime createdAt;
}

5、mapper

public interface UserMapper extends BaseMapper<User> {}

6、测试类

@SpringBootTest
public class Test2 {

    @Autowired
    private UserMapper userMapper;

    @Test
    void selectAll() {
        List<User> users = userMapper.selectList(null);
        users.forEach(System.out::println);
    }

    @Test
    void selectById() {
        QueryWrapper<User> qw = new QueryWrapper<>();
        qw.eq("user_id", "1058846700250071041");

        User user = userMapper.selectOne(qw);
        System.out.println(user);
    }

    @Test
    void insert() {
        for (int i = 0; i < 20; i++) {
            User user = new User();
            user.setUsername("测试" + i);
            user.setEmail(i + "@qq.com");
            user.setCreatedAt(LocalDateTime.now());
            userMapper.insert(user);
        }
    }

    @Test
    void insert2() {
        userMapper.insert(getUser(true));
    }
    @Test
    void insert3() {
        userMapper.insert(getUser(false));
    }


    User getUser(boolean isOdd) {
        User user = new User();
        int number = generateRandomNumber(100000, 1000000, isOdd);

        user.setUsername("测试" + number);
        user.setEmail(number + "@qq.com");
        user.setCreatedAt(LocalDateTime.now());

        return user;
    }

    /**
     * 生成指定范围内的随机奇数或偶数
     *
     * @param min 最小值(包含)
     * @param max 最大值(包含)
     * @param isOdd 是否生成奇数
     * @return 生成的随机数
     */
    public int generateRandomNumber(int min, int max, boolean isOdd) {
        if (min > max) {
            throw new IllegalArgumentException("最小值不能大于最大值");
        }

        Random random = new Random();
        int range = (max - min) + 1;
        int randomNumber = random.nextInt(range) + min;

        // 调整为奇数或偶数
        if (isOdd) {
            if (randomNumber % 2 == 0) {
                randomNumber++;
            }
        } else {
            if (randomNumber % 2 != 0) {
                randomNumber++;
            }
        }

        // 确保生成的数在范围内
        if (randomNumber > max) {
            randomNumber -= 2;
        }

        return randomNumber;
    }

    @Test
    void update() {
        QueryWrapper<User> qw = new QueryWrapper<>();
        qw.eq("user_id", 2);
        User user = userMapper.selectOne(qw);
    }

    @Test
    void deleteAll() {
        userMapper.delete(null);
    }
}

7、插入结果

1730389969742

sharding-jdbc实现垂直分库、分表

对于sharding-jdbc来说,垂直分库、垂直分表其实就是多配置一些数据源和表。

公共表

# 配置公共表(所有数据源中都需要有这个表,并且增、删、改都同步操作所有数据源)
spring.shardingsphere.rules.sharding.tables.broadcast-tables=t_public
spring.shardingsphere.rules.sharding.tables.t_public.key-generate-strategy.column=public_id 
spring.shardingsphere.rules.sharding.tables.t_public.key-generate-strategy.key-generator-name=snowflake 

sharding-jdbc读写分离

sharding-jdbc具有读写分离功能,能够将读、写分别使用不同的数据源进行操作。目前支持单主库、多从库。

我们基于此实现一个简单的读写分离Demo。配置两个数据库:db0db1,其中db0作为主库,db1作为从库,两个数据库做好数据同步操作。我们在此基础上实现读写分离的操作:

db0(主库)负责写操作(增删改)操作,db1(从库)负责查询操作。

1、sql

CREATE TABLE t_order (
    order_id BIGINT PRIMARY KEY COMMENT '订单ID,主键',
    user_id INT NOT NULL COMMENT '用户ID',
    status VARCHAR(50) NOT NULL COMMENT '订单状态',
    create_time DATETIME NOT NULL COMMENT '订单创建时间'
) COMMENT='订单表';

2、application.yaml

spring:
  shardingsphere:
    # 属性配置
    props:
      sql-show: true

    # 数据源配置
    datasource:
      # 数据源名称不能使用下划线!不能使用下划线!!不能使用下划线!!!(只要是后面需要作为key的都不能使用下划线)
      names: db-master,db-slave-1
      # 主数据源,负责增、删、改,以及事物内的读操作
      db-master:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/sharding
        username: root
        password: taishang
      # 从数据源,可以配置多个,负责读操作(查询操作)
      db-slave-1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3308/sharding
        username: taishang
        password: taishang

    # 规则配置
    rules:

      # 声明使用读写分离规则
      readwrite-splitting:
        data-sources:
          readwrite_ds:
            # 静态策略配置
            write-data-source-name: db-master
            read-data-source-names:
              - db-slave-1

            # 负载均衡算法名称
            loadBalancerName: random

        # 负载均衡算法配置
        load-balancers:
          random:
            type: RANDOM

3、测试类及结果

写操作

增、删、改操作将会使用主库进行。

测试代码:
    @Test
    void writerTest() {
        for (int i = 0; i < 10; i++) {
            User user = new User();
            user.setUsername("测试");
            user.setEmail("12313@qq.com");
            user.setCreatedAt(LocalDateTime.now());
            userMapper.insert(user);
        }
    }
结果:

1730489591826

查询操作

普通查询操作将会使用从库进行。

测试代码:
    @Test
    void readTest() {
        for (int i = 0; i < 10; i++) {
            userMapper.selectList(null);
        }
    }
结果:

1730489357236

事务内查询操作

事务内的查询操作将会使用主库进行查询。

测试代码:
    @Transactional
    @Test
    void transactionTest() {
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        for (int i = 0; i < 10; i++) {
            mapper.selectList(null);
            // 清空一级缓存
            sqlSession.clearCache();
        }
    }
结果:

1730489194608

数据加密

shardingsphere-jdbc-core-spring-boot-starter版本从5.0.0改为5.2.0,5.0.0设置加密算法类型后会报找不到key的错误,如下面的rc4加密算法后,会报rc4-key-value不能为空的错!!!!

不知道是傻逼官方埋的坑还是maven仓库的坑,我先截图保存记在笔记里,以后有空了再看是哪个傻逼埋的坑。如果后面发现是官方,我看看这个特么是谁写的!!!

妈的,刚准备开喷,改个版本号回来,莫名其妙的好了,不报错了!!!

1730516355981

按照如下配置,配置好相应字段后,即可实现加密存储,解密返回。但是如果使用非对称加密,则返回的是密文,因为非对称加密不能还原出原始数据。

spring:
  shardingsphere:
    # 属性配置
    props:
      sql-show: true

    # 数据源配置
    datasource:
      # 数据源名称不能使用下划线!不能使用下划线!!不能使用下划线!!!(只要是后面需要作为key的都不能使用下划线)
      names: db
      # 主数据源,负责增、删、改,以及事物内的读操作
      db:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/sharding
        username: root
        password: taishang

    rules:
      encrypt:
        tables:
          t_user:
            columns:
              password:
                # 指定明文存储在哪个列
                plain-column: password
                # 指定加密后的数据存储在哪个列
                cipher-column: password_encrypt
                # 设置加密器
                encryptor-name: md5-encryptor
                # 设置查询辅助列
#                assisted-query-column: password_query
#                assisted-query-encryptor-name: pwd_query_encryptor
#                query-with-cipher-column: true
              username:
                cipher-column: username
                encryptor-name: rc4-encryptor
#                query-with-cipher-column: false


        # 配置加密器算法
        encryptors:
          # 使用MD5非对称加密,无法还原
          md5-encryptor:
            type: MD5
          rc4-encryptor:
            type: rc4
            # 天坑!! 5.0.0使用该配置报aes-key-value不能为空,升级到5.2.1解决
            props:
              rc4-key-value: 1231231

        # 使用加密列进行查询(开启后会自动将查询条件加密后查询)
#        query-with-cipher-column: true
0

评论区