使用ShardingSphere实现分布式数据库

原文链接

欢迎大家对于本站的访问 - AsterCasc

前言

当我们在聊如何加快从数据库获取数据的效率时,可能会谈及如果数据库的查询缓慢并不是由于语句、索引、设计或者服务调用导致,单纯是数据表中数据过多影响 的解决方案,一般我们会认为如果数据文件超过2G或者表中数据超过500万条的情况下,我们使用常规手段对于获取数据的效率提升已经不会很明显了,此时分库分表就是一个相对来说非常值得考虑的选择

在数据量足够大的情况下,访问量一般也不会小,所以不仅仅是分库分表,我们还需要构建数据库集群,解决单库的连接查询压力。所以一般的解决方案是首先构建数据库集群分散连接查询压力,做读写分离,然后利用特定方法实现纵向/横向的分库分表提高查询效率。此时我们就需要一个Database plus介入服务层和持久层中间,屏蔽持久层设计变动对于服务层的影响,让不同端的开发设计内聚解耦

一般常用选择方案有两个mycatshardingsphere,目前mycat的维护状态堪忧无论是哪个版本,新项目的话还是建议使用shardingsphere作为中间层。目前shardingsphere主要提供两种使用模式,JDBCProxyJDBC利用Java本身对于数据库连接的支持,增强其JDBC驱动,故而可以支持绝大多数的主流数据库结构,Proxy虽然目前只能支持MysqlPostgreSql且需要独立部署,但是可以支持所有主流语言连接,两者在配置和服务层区别不大,这里以常用的JDBC举例

实现

数据库集群构建

正常情况下,都需要分库分表了,数据库集群在某种程度上也算是应当存在的,为了后续说明方便,这里从数据库集群构建开始,以Mysql 为例,版本为8.0.27

和之前所有文章一样,这里我们使用docker部署,docker-compose.yml如下,我们这里使用一主两从的配置:

version: '3'
services:
  mysql_master:
    image: 'mysql:latest'
    restart: always
    container_name: 'mysql_master'
    hostname: 'mysql_master'
    ports:
      - '33060:3306'
    environment:
      - "MYSQL_ROOT_PASSWORD=123456"
      - "TZ=Asia/Shanghai"
    volumes:
      - /root/docker/mysql/master/mydir:/mydir
      - /root/docker/mysql/master/datadir:/var/lib/mysql
      - /root/docker/mysql/master/my.cnf:/etc/my.cnf
      - /root/docker/mysql/master/source:/docker-entrypoint-initdb.d

  mysql_s1:
    image: 'mysql:latest'
    restart: always
    container_name: 'mysql_s1'
    hostname: 'mysql_s1'
    ports:
      - '33061:3306'
    environment:
      - "MYSQL_ROOT_PASSWORD=123456"
      - "TZ=Asia/Shanghai"
    volumes:
      - /root/docker/mysql/s1/mydir:/mydir
      - /root/docker/mysql/s1/datadir:/var/lib/mysql
      - /root/docker/mysql/s1/my.cnf:/etc/my.cnf
      - /root/docker/mysql/s1/source:/docker-entrypoint-initdb.d

  mysql_s2:
    image: 'mysql:latest'
    restart: always
    container_name: 'mysql_s2'
    hostname: 'mysql_s2'
    ports:
      - '33062:3306'
    environment:
      - "MYSQL_ROOT_PASSWORD=123456"
      - "TZ=Asia/Shanghai"
    volumes:
      - /root/docker/mysql/s2/mydir:/mydir
      - /root/docker/mysql/s2/datadir:/var/lib/mysql
      - /root/docker/mysql/s2/my.cnf:/etc/my.cnf
      - /root/docker/mysql/s2/source:/docker-entrypoint-initdb.d

在对于masters1s2的文件夹下增加my.cnf文件,下面给出简单示例:

[mysqld]
default-storage-engine=INNODB
character-set-server=utf8mb4
server-id=100
[client]
default-character-set=utf8mb4
[mysql]
default-character-set=utf8mb4

这里只需要设置server-id即可(需要保证不同服务不同id),不需要设置log-bin

From MySQL 8.0, binary logging is enabled by default, and is only disabled if you specify the --skip-log-bin or --disable-log-bin option at startup.

其他的比如binlog_format等,就自行根据需求配置了,配置完成后即可启动容器docker compose up -d

主节点配置

此时我们只是构建了三个mysql的示例,需要将他们联通起来,首先我们需要在主节点上创建有复制权限的从节点登录用户:

CREATE USER 'replica'@'%' IDENTIFIED WITH mysql_native_password 'replica_password';
GRANT REPLICATION SLAVE ON *.* TO 'replica'@'%';
FLUSH PRIVILEGES;

需要加WITH mysql_native_password,否则在8.x版本可能会包Authentication plugin ‘caching_sha2_password‘ reported error: Authentication的错误,但是如果是相对高更版本的话,也不需要这个设置了,因为:

As of MySQL 8.0.34, the mysql_native_password authentication plugin is deprecated and subject to removal in a future version of MySQL.

此时,我们观察主节点的binlog文件名和位置,用于从节点的拉取,

SHOW MASTER STATUS;

获取结果:

File         |Position|Binlog_Do_DB|Binlog_Ignore_DB|Executed_Gtid_Set|
-------------+--------+------------+----------------+-----------------+
binlog.000002|    1708|            |                |                 |
从节点配置

至此,主节点的配置基本结束,来到从节点,先配置连接主节点的参数:

CHANGE MASTER TO
MASTER_HOST='your_server_ip',
MASTER_PORT=33060,
MASTER_USER='replica',
MASTER_PASSWORD='replica_password',
MASTER_LOG_FILE='binlog.000002',
MASTER_LOG_POS=1708;

这里如果你使用的是docker容器,可以使用内部桥接网络,也可以直接走公网访问,不过考虑到后面的读写分离、分库分表操作的配置还是统一使用公网地址会更好。在MASTER_LOG_FILEMASTER_LOG_POS填入刚才在主节点获取的值,即可执行start slave开始同步,两个从节点连接相同的主节点,故而配置是相同的

执行同步后,使用show slave status观察执行结果,如果Slave_IO_RunningSlave_SQL_Running都标记为YES则连接成功,否则可以观察Last_IO_Error字段获取连接失败的原因进行修复

至此1主2从的mysql集群配置就基本完成了,我们可以简单测试一下,在主节点执行创建数据库,数据表,插入数据可以观测到两个从节点已经经由binglog同步执行,

CREATE TABLE book.`book_base` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
  `name` varchar(20) NOT NULL COMMENT '名称',
  `book_desc` varchar(255) NOT NULL COMMENT '描述',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=88 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='书籍基本表';
insert into book.book_base(`name`, `desc`) values('母猪的产后护理1', '1111'), ('母猪的产后护理2', '2222');
insert into book.book_base(`name`, `desc`) values('母猪的产后护理3', '3333'), ('母猪的产后护理4', '4444');
数据不同步异常处理

数据不同步的可能原因有很多,比如网络连接,主库/从库挂掉,不小心写入从库等等,关于预防措施我们会在专门的数据库集群的文章中聊到,关于增强半同步、异步复制等。这里我们简单说一下不一致的紧急的处理方案

首先先锁主库防止后续变动flush tables with read lock,然后将通过mysqldump -u root -p --all-databases > 1.sql导出SQL,根据我们这里的容器配置,直接将1.sql放在/mydir下,然后cp /root/docker/mysql/master/mydir/1.sql /root/docker/mysql/s1/mydir/,即可将文件传入另一个容器,此时在从数据库中执行stop slave; source /mydir/1.sql即可重新同步数据,然后再按照上文方法重新进行从节点配置即可,最后解开主库锁

读写分离

大多数情况下,我们都是读多写少,在面临高并发的请求的时候,如果在同表读写可能会由于行锁间隙锁等造成堵塞。所以我们一般的处理是,将数据库分为主库和从库,通过数据集群的主从复制完成同步,然后在从库中做负载均衡,进一步通过减少单库的连接计算压力来更好地应对高并发情况

引入依赖:

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core</artifactId>
    <version>5.4.1</version>
</dependency>
dependencies {  
	implementation("org.apache.shardingsphere:shardingsphere-jdbc-core:5.4.1")
}

可能大家看到过使用spring-boot-start的引用方式,但是目前在新版本中该方式已经被移除可以参考Removing Spring configuration from ShardingSphere

  1. When the API changes, it is necessary to adjust various configuration files, and the workload is heavy;
  2. The community needs to maintain various configuration documents;
  3. Spring bean life cycle management is easily affected by other dependencies of the project, such as PostProcessor cannot be executed normally;
  4. Spring Boot Starter and Spring NameSpace are influenced by Spring, and the configuration style is quite different from YAML;
  5. . Spring Boot Starter and Spring NameSpace are affected by the Spring version, and there will be configuration compatibility issues. For example, Spring Boot recently released version 3.0, and the previous registration file spring.factories will become invalid.

手边的测试项目是Kotlin,我们这里就以Kotlin代码为例了,不过使用shardingsphere主要是配置上,不会太涉及代码层面举例。基本模式分为单例和集群,集群调用的话目前注册中心原生只支持ZooKeeper,比如你和本站的一样使用的consul注册的,需要引用shardingsphere-cluster-mode-repository-consul进行相关配置调整,这里以单例模式举例,

spring:  
  application:  
    name: sharding-demo  
  datasource:  
    driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver  
    url: jdbc:shardingsphere:classpath:sharding.yaml

shardingshpere配置:

mode:  
  type: Standalone  
  repository:  
    type: JDBC  
  
dataSources:  
  master:  
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource  
    driverClassName: com.mysql.cj.jdbc.Driver  
    jdbcUrl: jdbc:mysql://your_domin:33060/book?characterEncoding=utf8&serverTimezone=Asia/Shanghai  
    username: root  
    password: 123456  
  s1:  
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource  
    driverClassName: com.mysql.jdbc.Driver  
    jdbcUrl: jdbc:mysql://your_domin:33061/book?characterEncoding=utf8&serverTimezone=Asia/Shanghai  
    username: root  
    password: 123456  
  s2:  
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource  
    driverClassName: com.mysql.jdbc.Driver  
    jdbcUrl: jdbc:mysql://your_domin:33062/book?characterEncoding=utf8&serverTimezone=Asia/Shanghai  
    username: root  
    password: 123456  
  
rules:  
  - !SINGLE  
    tables:  
      - "*.*"  
  - !READWRITE_SPLITTING  
    dataSources:  
      readwrite_ds:  
        writeDataSourceName: master  
        readDataSourceNames:  
          - s1  
          - s2  
        transactionalReadQueryStrategy: PRIMARY  
        loadBalancerName: random  
    loadBalancers:  
      random:  
        type: RANDOM

如果你使用的是5.4.1版本的shardingsphere,并且使用高版本的spring的话(比如3.2.X),那么在5.4.2发布前,你可能需要通过github拉取代码然后编译推送到本地仓库,以此来解决snakeyaml冲突的问题,相关讨论可以参考Updates ElasticJob to 3.0.4 to block CVEs for SnakeYAML delivery以及shardingsphere 5.4.1 yaml dependency must be 1.33 but we want to upgrade spring boot 3.2.0

配置完成后我们可以通过测试,观察是否实现读写分离,

@SpringBootTest  
class ShardingDemoApplicationTests @Autowired constructor(  
    private val bookBaseMapper: BookBaseMapper  
) {  
    @Test  
    fun contextLoads() {  
        for (count in 1..10) {  
            bookBaseMapper.insertSelective(  
                BookBase(  
                    id = count.toLong(),  
                    name = "猫猫翻跟头:从入门到精通", bookDesc = "0 0",  
                    createTime = null, updateTime = null  
                )  
            )  
        }  
        val book = bookBaseMapper.selectId()  
        println(book.toString())  
    }  
  
}

同时可以在shardingsphere配置中增加:

props:
  sql-show: true

获取更详细的日志确认负载均衡:

2024-04-03T17:04:33.131+08:00  INFO 2696 --- [    Test worker] ShardingSphere-SQL                       : Logic SQL: SELECT id,name,book_desc,create_time,update_time  FROM book_base
2024-04-03T17:04:33.131+08:00  INFO 2696 --- [    Test worker] ShardingSphere-SQL                       : Actual SQL: s1 ::: SELECT id,name,book_desc,create_time,update_time  FROM book_base

分库分表

这里我们稍微修改一下docker-compose.yml增加一个容器:

  mysql_master2:
    image: 'mysql:latest'
    restart: always
    container_name: 'mysql_master2'
    hostname: 'mysql_master2'
    ports:
      - '33063:3306'
    environment:
      - "MYSQL_ROOT_PASSWORD=123456"
      - "TZ=Asia/Shanghai"
    volumes:
      - /root/docker/mysql/master2/mydir:/mydir
      - /root/docker/mysql/master2/datadir:/var/lib/mysql
      - /root/docker/mysql/master2/my.cnf:/etc/my.cnf
      - /root/docker/mysql/master2/source:/docker-entrypoint-initdb.d

将原book_base表中数据删除,并在master2中建立该表,并在上文配置文件中增加master2的相关配置,这样我们分库的另一个库就准备完成了,接下来配置以book_baseid对2取模进行横向分表:

rules:  
  - !SHARDING  
    tables:  
      book_base:  
        actualDataNodes: master_${0..1}.book_base  
    defaultShardingColumn: id  
    bindingTables:  
      - book_base  
    defaultDatabaseStrategy:  
      standard:  
        shardingColumn: id  
        shardingAlgorithmName: database_inline  
    shardingAlgorithms:  
      database_inline:  
        type: INLINE  
        props:  
          algorithm-expression: master_${id % 2}  
          allow-range-query-with-inline-sharding: true  
    keyGenerators:  
      snowflake:  
        type: SNOWFLAKE  
    auditors:  
      sharding_key_required_auditor:  
        type: DML_SHARDING_CONDITIONS

此时我们可以观察到日志输出为:

Logic SQL: INSERT INTO book_base  ( id,name,book_desc ) VALUES( ?,?,? )
Actual SQL: master_1 ::: INSERT INTO book_base  ( id,name,book_desc ) VALUES(?, ?, ?) ::: [1, 猫猫翻跟头:从入门到精通, 0 0]
Logic SQL: INSERT INTO book_base  ( id,name,book_desc ) VALUES( ?,?,? )
Actual SQL: master_0 ::: INSERT INTO book_base  ( id,name,book_desc ) VALUES(?, ?, ?) ::: [2, 猫猫翻跟头:从入门到精通, 0 0]

如果你对于不同库中表名有定义需求的话,还可以通过表名配置下的tableStrategy等属性进行定义:

rules:
  - !SHARDING
    tables:
      book_base:
        actualDataNodes: master_${0..1}.book_base_${0..1}
        tableStrategy:
          standard:
            shardingColumn: id
            shardingAlgorithmName: book_base_inline
        keyGenerateStrategy:
          column: id
          keyGeneratorName: snowflake
        auditStrategy:
          auditorNames:
            - sharding_key_required_auditor
          allowHintDisable: true
    defaultShardingColumn: id
    bindingTables:
      - book_base
    defaultDatabaseStrategy:
      standard:
        shardingColumn: id
        shardingAlgorithmName: database_inline
    shardingAlgorithms:
      database_inline:
        type: INLINE
        props:
          algorithm-expression: master_${id % 2}
          allow-range-query-with-inline-sharding: true
      book_base_inline:
        type: INLINE
        props:
          algorithm-expression: book_base_${id % 2}
          allow-range-query-with-inline-sharding: true
    keyGenerators:
      snowflake:
        type: SNOWFLAKE
    auditors:
      sharding_key_required_auditor:
        type: DML_SHARDING_CONDITIONS

此时的实际执行为:

Logic SQL: INSERT INTO book_base  ( id,name,book_desc ) VALUES( ?,?,? )
Actual SQL: master_1 ::: INSERT INTO book_base_1  ( id,name,book_desc ) VALUES(?, ?, ?) ::: [1, 猫猫翻跟头:从入门到精通, 0 0]
Logic SQL: INSERT INTO book_base  ( id,name,book_desc ) VALUES( ?,?,? )
Actual SQL: master_0 ::: INSERT INTO book_base_0  ( id,name,book_desc ) VALUES(?, ?, ?) ::: [2, 猫猫翻跟头:从入门到精通, 0 0]

当然你也可以使用继承ShardingAlgorithm接口,去实现doSharding接口,通过返回表名或库名的方式来进行分片,这里需要在配置文件中的shardingAlgorithms.your_algorithm.type中使用全限定名进行注册,这种处理方式和其他类型的中间件类似这里就不赘述了

本文只是简单介绍了ShardingSphere的基本使用,其他更多内容,比如其他自动分片算法,分片审计算法,数据加密及脱敏,以及关于联合seata一起作为分布式事务的解决方案等,会在后续文章中讨论

参考资料

Apache Shardingsphere Current

Apache Shardingsphere Github

原文链接

欢迎大家对于本站的访问 - AsterCasc

  • 25
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值