Sharding sphere实现读写分离+分库分表
数据库要实现读写分离,比较主流使用的有sharding sphere
和mycat
,sharding
作为一个组件集成在应用内,Sharding
是一个Jar形式,在本地应用层重写Jdbc原生的方法,实现数据库分片形式。而mycat
则作为一个独立的应用需要单独部署,是一个基于第三方应用中间件数据库代理框架,客户端所有的jdbc请求都必须要先交给MyCa
t,再有MyCat
转发到具体的真实服务器中。MyCat
属于服务器端数据库中间件,更倾向于运维层面,而Sharding
是一个本地数据库中间件框架。
从设计理念上看,两则确实有一定的相似性。主要流程都是SQL 解析 -> SQL 路由 -> SQL 改写 -> SQL 执行 -> 结果归并。但架构设计上是不同的。
借张图Mycat
是基于Proxy
,它复写了MySQL
协议,将Mycat Server
伪装成一个MySQL
数据库,而Sharding
是基于 JDBC 的扩展,是以 jar 包的形式提供轻量级服务的。
要实现Sharding
完成读写分离,提前需要设置一下数据源的主从配置,以常用的Mysql数据库为例子,其他的我也不会。
第一步:数据库的主从配置
- 准备两台 MySQL 服务器
- 配置主服务器(Master),配置从服务器(Slave)
- 完成Master和Slave授权
原理描述:
- 主服务器上面的任何修改都会通过自己的 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
,从写了此接口的方法,在yml
中standard
下配置生效。
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
不起效,无法进行分库分表,血泪啊这个问题找了一下午,区别看下上面yml
中master-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传递过来的冲突事务.
操作如下图: