Sharding sphere实现读写分离+分库分表

2021-07-29  本文已影响0人  54番茄

    数据库要实现读写分离,比较主流使用的有sharding spheremycatsharding作为一个组件集成在应用内,Sharding是一个Jar形式,在本地应用层重写Jdbc原生的方法,实现数据库分片形式。而mycat则作为一个独立的应用需要单独部署,是一个基于第三方应用中间件数据库代理框架,客户端所有的jdbc请求都必须要先交给MyCat,再有MyCat转发到具体的真实服务器中。MyCat属于服务器端数据库中间件,更倾向于运维层面,而Sharding是一个本地数据库中间件框架。

从设计理念上看,两则确实有一定的相似性。主要流程都是SQL 解析 -> SQL 路由 -> SQL 改写 -> SQL 执行 -> 结果归并。但架构设计上是不同的。Mycat是基于 Proxy,它复写了MySQL 协议,将 Mycat Server伪装成一个 MySQL数据库,而 Sharding是基于 JDBC 的扩展,是以 jar 包的形式提供轻量级服务的。

借张图

要实现Sharding完成读写分离,提前需要设置一下数据源的主从配置,以常用的Mysql数据库为例子,其他的我也不会。

第一步:数据库的主从配置

原理描述:

  • 主服务器上面的任何修改都会通过自己的 I/O tread(I/O 线程)保存在二进制日志 Binary log 里面。从服务器上面也启动一个 I/O thread,通过配置好的用户名和密码, 连接到主服务器上面请求读取二进制日志,然后把读取到的二进制日志写到本地的一个Realy log(中继日志)里面。
  • 从服务器上面同时开启一个 SQL thread 定时检查 Realy log(这个文件也是二进制的),如果发现有更新立即把更新的内容在本机的数据库上面执行一遍。
  • 每个从服务器都会收到主服务器二进制日志的全部内容的副本,从服务器设备负责决定应该执行二进制日志中的哪些语句,如果需要,您可以将从服务器配置为仅处理一些特定数据库或表的事件。

主从复制是异步的,从库可以获取到所有主库的数据,也可以指定某个库或者某个表。

1、Master(主)数据库节点的配置,进入/etc/my.cnf中(Master节点服务器上):

主服务器mysql.cnf配置截图

操作以下命令,主服务器完成配置后,重启一下mysql:

1、执行  vim  /etc/my.cnf 进行编辑操作
#设置mysql服务id,同一个网段不能是一样的,必须
server-id=100  
#开启二进制,数据同步需要读取Binary log,必须
log-bin=mysql-bin
#同步过滤,可以指定某个库或者某张表同步,例如本例子只需要对seal_sign_record库需要同步
binlog-do-db=seal_sign_record
## 主从复制的格式(mixed,statement,row,默认格式是statement)
binlog_format=mixed  
## 二进制日志自动删除/过期的天数。默认值为0,表示不自动删除。
expire_logs_days=20
##为每个session 分配的内存,主要是在事务过程中用来存储二进制日志的缓存,不涉及事物的可以不用管
binlog_cache_size=1M

2、配置完成后重启 mysql ,使用如下命令:
service mysql restart

2、Slave(从)数据库节点配置,进入/etc/my.cnf (Slave节点服务器上):

1、执行  vim  /etc/my.cnf 进行编辑操作
#设置mysql服务id,同一个网段不能是一样的,必须
server-id=101
## 开启二进制日志功能,以备Slave作为其它Slave的Master时使用,如Master->Slave1->Slave2
log-bin=mysql-slave1-bin   
## 开启relay_log配置中继日志,
##relay log很多方面都跟binary log差不多,
##作用:从服务器I/O线程将主服务器的二进制日志读取过来记录到从服务器本地文件,然后从服务器SQL线程会读取relay-log日志的内容并应用到从服务器,从而使从服务器和主服务器的数据保持一致
relay_log=edu-mysql-relay-bin
## 主从复制的格式(mixed,statement,row,默认格式是statement)
binlog_format=mixed  
#同步过滤,可以指定某个库或者某张表同步,例如本例子只需要对seal_sign_record库需要同步
replicate_do_db=seal_sign_record
## 二进制日志自动删除/过期的天数。默认值为0,表示不自动删除。
expire_logs_days=20
##为每个session 分配的内存,主要是在事务过程中用来存储二进制日志的缓存,不涉及事物的可以不用管
binlog_cache_size=1M

2、配置完成后重启 mysql ,使用如下命令:
service mysql restart

3、在Master节点授权Slave节点,拥有同步权限(在Master节点服务器上执行):

登录Master服务器上的Mysql授权,记得要flush
mysql > mysql -u root -p12345678
# 授予slave服务器可以同步master服务
mysql > grant replication slave, replication client on *.* to 'root'@'slave服务的ip' identified by 'slave服务器的密码';
mysql > flush privileges;
#查询主节点的mysql中 binlog文件名和位置,如下图,日志文件名:mysql-bin.000018,复制的位置:25262
mysql > show master status;
Master节点的信息

4、在Slave节点上,关联Master节点(在Slave节点服务器上执行):

mysql > mysql -u root -p12345678
##开始在Slave服务器节点上,关联主节点
##master_log_file=指定 Slave 从哪个日志文件开始复制数据,即上文中提到的 File 字段的值
##master_log_pos=从哪个 Position 开始读,即上文中提到的 Position 字段的值
##master_connect_retry=当重新建立主从连接时,如果连接失败,重试的时间间隔,单位是秒,默认是60秒
mysql> change master to master_host='master服务器ip', master_user='root', master_password='master密码', master_port=3306, master_log_file='mysql-bin.000018',master_log_pos=25262;
##启动主从复制
mysql> start slave;
##查看从节点状态
mysql> show slave status\G;
Slave节点信息
    主从复制设置完成后,可以在主节点的Mysql中新增数据,在从节点的Mysql就可以看到了。这样后面,就可以做读写分离,主节点为写,从节点为读。

第二步:项目搭建,并引入相关jar包,增加相关配置,注意有大坑

相关jar引入需要注意版本问题,com.mysql.jdbc.Driver驱动是mysql-connector-java 5中的
com.mysql.cj.jdbc.Driver驱动是mysql-connector-java 6以上版本的,以下项目目录 +POM.xml

项目目录
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.XXX</groupId>
    <artifactId>sign-record</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>sign-record</name>
    <description>project for xxxx record</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR8</spring-cloud.version>
        <fastjson.version>1.2.44</fastjson.version>
        <!-- 数据库 -->
        <mysql.version>8.0.16</mysql.version>
        <druid.version>1.1.23</druid.version>
        <!-- mybatis版本号 -->
        <mybatis-spring.version>1.3.1</mybatis-spring.version>
        <sharding-sphere.version>4.0.0-RC1</sharding-sphere.version>
    </properties>


    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--数据库连接池 Druid引入 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>
<!--        分页查询插件-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>4.2.1</version>
        </dependency>
<!--        mybatis-plus依赖-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis-spring.version}</version>
        </dependency>
<!--        数据源驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        <!-- lombok注解,需要安装lombok插件 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.18</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.23</version>
        </dependency>

        <!--依赖sharding-->
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
            <version>${sharding-sphere.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-core-common</artifactId>
            <version>${sharding-sphere.version}</version>
        </dependency>
    </dependencies>


    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>

        <resources>
            <resource>
                <!--设置在java目录下的xml文件,能打进入包,也就是mapper.xml-->
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <!--设置resources中properties、xml、yml 能打进入包-->
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.yml</include>
                    <include>**/*.xml</include>
                </includes>
            </resource>
        </resources>
    </build>

</project>

yml文件配置:此yml根据数据插入时间自然月进行分表,并设置了record1(主)写,record2(从)读,实现读写分离,表名规则为seal_log_202107,分表依据为action_time时间字段

spring:
  main:
    allow-bean-definition-overriding: true
  shardingsphere:
    # 参数配置,显示sql
    props:
      sql:
        show: true
    # 配置数据源
    datasource:
      # 给每个数据源取别名,record*
      names: record1,record2
      # 给master-record1每个数据源配置数据库连接信息
      record1:
        # 配置druid数据源
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.200.5:3306/seal_sign_record?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false
        username: root
        password: 123456
        maxPoolSize: 100
        minPoolSize: 5
      # 配置record2-slave
      record2:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.200.20:3306/seal_sign_record?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false
        username: root
        password: 123456
        maxPoolSize: 100
        minPoolSize: 5
    # 配置默认数据源ds1
    sharding:
      # 配置数据源的读写分离,但是数据库一定要做主从复制
      master-slave-rules:
        # 配置主从名称,可以任意取名字
        ms:
          # 配置主库master,负责数据的写入
          master-data-source-name: record1
          # 配置从库slave节点,多个从节点以逗号分隔
          slave-data-source-names: record2
          # 配置slave节点的负载均衡均衡策略,采用轮询机制
          load-balance-algorithm-type: round_robin
      # 默认数据源,主要用于写,注意一定要配置读写分离 ,注意:如果不配置,那么就会把三个节点都当做从slave节点,新增,修改和删除会出错。
      default-data-source-name: ms
      # 配置分表的规则
      tables:
      #分表表名,表名规则seal_log_20210721,分表依据为action_time时间字段
        seal_log:
          actual-data-nodes: ms.seal_log_$->{2021}${(7..9).collect{t ->t.toString().padLeft(2,'0')} }
          table-strategy:
            standard:
              shardingColumn: action_time
              preciseAlgorithmClassName: com.XXXXXX.config.DatePreciseShardingAlgorithm

本文使用的是精确分片算法PreciseShardingAlgorithm,从写了此接口的方法,在ymlstandard下配置生效。

import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.NumberFormat;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;

/**
 * @author fanqie
 * @ClassName DatePreciseShardingAlgorithm
 * @date 2021/7/28 下午4:40
 **/

public class DatePreciseShardingAlgorithm implements PreciseShardingAlgorithm<Date>  {


    private static final Logger logger = LoggerFactory.getLogger(DatePreciseShardingAlgorithm.class);
    @Override
    public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Date> preciseShardingValue) {
        Date date = preciseShardingValue.getValue();
        logger.info("Sharding input:" + preciseShardingValue.getValue());
        String suffix = getSuffixByYearMonth(date);
        for (String tableName : availableTargetNames) {
            logger.info("suffix:" + suffix + ", 表明:{}" + tableName);
            if (tableName.endsWith(suffix)) {
                return tableName;
            }
        }
        throw new IllegalArgumentException("未找到匹配的数据表");

    }
/**
 * 自然月为个位数的,进行补零,如07 、08 、09
 **/
    private static String getSuffixByYearMonth(Date date) {
        NumberFormat nf = NumberFormat.getInstance();
        nf.setMinimumIntegerDigits(2);
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        return calendar.get(Calendar.YEAR)  +"" +  nf.format((calendar.get(Calendar.MONTH) + 1));
    }
}

注意哈有个大坑:
在很多网上教程中,要么只做了读写分离、要么只做了分库分表,如果你在完成读写分离后,在再同配置中进行分库分表,有个读写分离配置masterslave标签,这个masterslave会造成自定义配置类DatePreciseShardingAlgorithm不起效,无法进行分库分表,血泪啊这个问题找了一下午,区别看下上面ymlmaster-slave-rules这段配置:
错误配置错误配置错误配置错误配置错误配置如下

sharding:
      default-data-source-name: record1
    masterslave:
      name: ms
      master-data-source-name: record1
      slave-data-source-names: record2
      load-balance-algorithm-type: round_robin
    # 配置分表的规则
    tables:
      seal_log:
       actual-data-nodes: record1.seal_log_$->{2021}${(7..9).collect{t ->t.toString().padLeft(2,'0')} }
       table-strategy:
         standard:
           shardingColumn: action_time
           preciseAlgorithmClassName: com.XXXXX.DatePreciseShardingAlgorithm

有么有中招的小伙伴?

----------------分割线-------------
主从权限没设定好,正常从库的用户账号权限应该设置成只读权限,保证不会因修改从库造成,主从复制错误的问题,我遇到的就是在从库中增加了字段数据,又返回主库添加字段数据,造成从库已经有这个字段跟主库冲突了,必须控制权限,才能彻底解决问题。
** 解决方式:**
第一步:查看从数据库状态:

mysql> show  slave status\G;
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 192.168.200.5
                  Master_User: root
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000018
          Read_Master_Log_Pos: 459455617
               Relay_Log_File: localhost-relay-bin.000002
                Relay_Log_Pos: 459420641
        Relay_Master_Log_File: mysql-bin.000018
             Slave_IO_Running: Yes
            Slave_SQL_Running: No
              Replicate_Do_DB: seal_sign_record
          Replicate_Ignore_DB: 
           Replicate_Do_Table: 
       Replicate_Ignore_Table: 
      Replicate_Wild_Do_Table: 
  Replicate_Wild_Ignore_Table: 
                   Last_Errno: 1060
                   Last_Error: Error 'Duplicate column name 'seal_type'' on query. Default database: 'seal_sign_record'. Query: 'alter table ps_seal_log_202107 add seal_type tinyint(2) DEFAULT NULL COMMENT '类型''
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 459420983
              Relay_Log_Space: 459455486
              Until_Condition: None
               Until_Log_File: 
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File: 
           Master_SSL_CA_Path: 
              Master_SSL_Cert: 
            Master_SSL_Cipher: 
               Master_SSL_Key: 
        Seconds_Behind_Master: NULL
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error: 
               Last_SQL_Errno: 1060
               Last_SQL_Error: Error 'Duplicate column name 'seal_type'' on query. Default database: 'seal_sign_record'. Query: 'alter table ps_seal_log_202107 add seal_type tinyint(2) DEFAULT NULL COMMENT '类型''
  Replicate_Ignore_Server_Ids: 
             Master_Server_Id: 100
                  Master_UUID: 9e968da7-05b0-11ea-a9b4-98be9446c9a2
             Master_Info_File: /var/lib/mysql/master.info
                    SQL_Delay: 0
          SQL_Remaining_Delay: NULL
      Slave_SQL_Running_State: 
           Master_Retry_Count: 86400
                  Master_Bind: 
      Last_IO_Error_Timestamp: 
     Last_SQL_Error_Timestamp: 210730 03:23:40
               Master_SSL_Crl: 
           Master_SSL_Crlpath: 
           Retrieved_Gtid_Set: 9e968da7-05b0-11ea-a9b4-98be9446c9a2:4214744-4214887
            Executed_Gtid_Set: 18d7f02c-c861-11eb-86fd-b4055d9da070:1-68781027,
9e968da7-05b0-11ea-a9b4-98be9446c9a2:4214744-4214810
                Auto_Position: 0
         Replicate_Rewrite_DB: 
                 Channel_Name: 
           Master_TLS_Version: 
1 row in set (0.00 sec)

第二步:找到错误码1060
常见一些error code代表的错误如下:

错误码 错误描述
1007 数据库已存在,创建数据库失败
1008: 数据库不存在,删除数据库失败
1050: 数据表已存在,创建数据表失败
1051: 数据表不存在,删除数据表失败
1054: 字段不存在,或程序文件跟数据库有冲突
1060: 字段重复,导致无法插入
1061: 重复键名
1068: 定义了多个主键
1094: 位置线程ID
1146: 数据表缺失,请恢复数据库
1053: 复制过程中主服务器宕机
1062: 主键冲突 Duplicate entry '%s' for key %d

第三部:删除从库中增加的字段,按照以下步骤,让从库跳过这个错误事物
他可以有两种方式跳过,先说的是GTID模式,这一种是用得比较多的,注入空事务跳过:

#找到冲突的GTID号,Executed_Gtid_Set
mysql>show slave status\G
    。
    。
    。
Executed_Gtid_Set: 18d7f02c-c861-11eb-86fd-b4055d9da070:1-68781030
    。
    。
    。
#停止复制
mysql>stop slave;
#然后执行设置一个事务GTID来跳过,因为我们就是卡在这里,所以要跳过68781030这个事务的意思,记住去掉 1-  不然报错
mysql> SET gtid_next = '18d7f02c-c861-11eb-86fd-b4055d9da070:68781030';
#注入空事务
mysql>  BEGIN;COMMIT;
#把GTID设置回自动模式
mysql> set gtid_next = 'AUTOMATIC';
#重新开启复制
mysql> start slave;
#这就可以跳过一个事务了,原理在于通过执行一个空事务代替master传递过来的冲突事务.

操作如下图:


还有想实现分库分表,都需要提前创建好库表,想实现动态创建,我查了一下,追溯了源码,没找到好的方式,如果有好的方式请留言告知!

上一篇下一篇

猜你喜欢

热点阅读