MySql 主从同步、读写分离

本文详细介绍了MySQL主从复制的原理和配置步骤,包括binlog同步流程,以及如何处理主从数据库的故障切换。同时,探讨了读写分离的重要性,讲解了如何在Spring应用中实现读写分离,以及MyCat作为数据库中间件在读写分离中的作用和配置。此外,还提到了MyCat的特性、优势和使用场景。
摘要由CSDN通过智能技术生成

文章同步:http://www.guofeian.cn/article_details/624949244733960192

为什么需要主从数据库

mysql主从

当主数据库出现问题时,可以当从数据库代替主数据库,可以避免数据的丢失。

  • Mysql内建的复制功能是构建大型,高性能应用程序的基础。

  • 将Mysql的数据分布到多个系统上去,这种分布的机制,是通过将Mysql的某一台主机的数据复制到其它主机(slaves)上,并重新执行一遍来实现的。

  • 复制过程中一个服务器充当主服务器,而一个或多个其它服务器充当从服务器。主服务器将更新写入二进制日志文件,并维护文件的一个索引以跟踪日志循环

  • 当一个从服务器连接主服务器时,它通知主服务器从服务器在日志中读取的最后一次成功更新的位置。从服务器接收从那时起发生的任何更新,然后封锁并等待主服务器通知新的更新。

Mysql主从能解决的问题

  • 数据分布 (Data distribution )

  • 负载平衡(load balancing)

  • 数据备份(Backups) ,保证数据安全

  • 高可用性和容错行(High availability and failover)

  • 实现读写分离,缓解数据库压力

读写分离

  • 当主数据库进行对数据的增删改也就是写操作时,将查询的任务交给从数据库。

  • 减轻主数据库的压力。因为进行写操作更耗时,所以如果不进行读写分离的话,写操作将会影响到读操作的效率。

分离有两种实现方式

1、第一种是依靠中间件(比如:MyCat),也就是说应用程序连接到中间件,中间件帮我们做SQL分离;

2、第二种是应用程序自己去做分离。这里我们选择程序自己来做,主要是利用Spring提供的路由数据源,以及AOP

然而,应用程序层面去做读写分离最大的弱点(不足之处)在于无法动态增加数据库节点,因为数据源配置都是写在配置中的,新增数据库意味着新加一个数据源,必然改配置,并重启应用。当然,好处就是相对简单。

Mysql 主从同步

binlog原理

binlog 同步流程

  • 主库db的更新事件(update、insert、delete)被写到binlog。

  • 主库创建一个binlog dump thread,把binlog的内容发送到从库。

  • 从库启动并发起连接,连接到主库。

  • 从库启动之后,创建一个I/O线程,读取主库传过来的binlog内容并写入到relay log。

  • 从库启动之后,创建一个SQL线程,从relay log里面读取内容,从Exec_Master_Log_Pos位置开始执行读取到的更新事件,将更新内容写入到slave的db。

本文章基于数据库8.x 实现,使用docker 安装多个数据库测试。

拉取mysql镜像

docker pull mysql

创建master 库

docker run -itd -p 33067:3306  --name mysql_8.0.master \
--privileged=true  \
-v /Users/guofeichu/Public/CodeApplication/Docker/mysql8.0.master/conf.d/my.cnf:/etc/mysql/my.cnf \
-v /Users/guofeichu/Public/CodeApplication/Docker/mysql8.0.master/logs:/logs   \
-v /Users/guofeichu/Public/CodeApplication/Docker/mysql8.0.master/mysql:/var/lib/mysql \
 -e MYSQL_ROOT_PASSWORD=123456   mysql

创建 Slave 库

docker run -itd -p 33067:3306  --name mysql_8.0.slaves01 \
--privileged=true  \
-v /Users/guofeichu/Public/CodeApplication/Docker/mysql8.0.slaves01/conf.d/my.cnf:/etc/mysql/my.cnf \
-v /Users/guofeichu/Public/CodeApplication/Docker/mysql8.0.slaves01/logs:/logs   \
-v /Users/guofeichu/Public/CodeApplication/Docker/mysql8.0.slaves01/mysql:/var/lib/mysql \
 -e MYSQL_ROOT_PASSWORD=123456   mysql

要修改数据密码执行下面命令

select user,host from user;
# 修改root用户可以远程调用, 出现1130的错误执行下 (1130 - Host XXX is not allowed to connect to this MySQL server。)
update user set host = '%' where user ='root';
# 修改密码
alter user 'root'@'%' identified by '123456';
flush privileges;

创建slave访问用户

也可以不创建,使用登录的用户密码

# 创建用户
CREATE USER 'slave'@'%' IDENTIFIED WITH mysql_native_password BY '123456';
# 每个slave使用标准的MySQL用户名和密码连接master。进行复制操作的用户会授予REPLICATION SLAVE 权限。
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'slave'@'%';
# 查看用户
select host, user, authentication_string, plugin from user; 
# 授权
GRANT ALL ON *.* TO 'slave'@'%';
# 刷新权限
flush privileges;

参数说明:

  • restart=always: 当Docker 重启时,容器会自动启动。
  • privileged=true:容器内的root拥有真正root权限,否则容器内root只是外部普通用户权限
  • -v /mydata/mysql/log:/var/log/mysql 映射日志文件
  • -v /opt/mysql/data/:/var/lib/mysql 映射数据目录
  • -v /mydata/mysql/my.cnf:/etc/mysql/my.cnf 映射配置文件
  • -v /mydata/mysql/conf.d:/etc/mysql/conf.d 映射配置文件
  • -e MYSQL_ROOT_PASSWORD=123456 映射mysql root用户密码
  • -d mysql 以后台方式启动

在本地卷文件中修改my.conf 文件

master

[mysqld]
user=mysql
character-set-server=utf8
default_authentication_plugin=mysql_native_password
secure_file_priv=/var/lib/mysql
expire_logs_days=7
sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
max_connections=1000
default-time_zone='+8:00'


###############################master主配置##############################
pid-file=/var/run/mysqld/mysqld.pid
socket=/var/run/mysqld/mysqld.sock
datadir=/var/lib/mysql
#lower_case_table_names=1
symbolic-links=0
#设置同步的binary log二进制日志文件名前缀,默认为binlog
log-bin=mysql-bin
#服务器唯一id,默认为1,主数据库和从数据库的server-id不能重复
server-id=1      
#开启GTID主从同步模式
gtid_mode=on
enforce_gtid_consistency=true


###############################可选配置#################################
#需要主从复制的数据库 
#replicate-do-db=test
#忽略同步的数据库
#binlog-ignore-db=mysql
#为每个session分配的内存,在事务过程中用来存储二进制日志的缓存 
binlog_cache_size=1M
#主从复制的格式(mixed,statement,row,默认格式是statement。建议是设置为row,主从复制时数据更加能够统一) 
binlog_format=row
#设置二进制日志自动删除/过期的天数,避免占用磁盘空间。默认值为0,表示不自动删除。 
expire_logs_days=7
# 跳过主从复制中遇到的所有错误或指定类型的错误,避免slave端复制中断。 
# 如:1062错误是指一些主键重复,1032错误是因为主从数据库数据不一致 
slave_skip_errors=1062
#relay_log配置中继日志,默认采用主机名-relay-bin的方式保存日志文件 
relay_log=replicas-mysql-relay-bin
#log_slave_updates表示slave将复制事件写进自己的二进制日志
log_slave_updates=1
#防止改变数据(只读操作,除了特殊的线程)


[client]
default-character-set=utf8

[mysql]
default-character-set=utf8

slave

[mysqld]
user=mysql
character-set-server=utf8
default_authentication_plugin=mysql_native_password
secure_file_priv=/var/lib/mysql
expire_logs_days=7
sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
max_connections=1000
# 设置默认时区
default-time_zone='+8:00'



##############################slave从配置##############################
pid-file=/var/run/mysqld/mysqld.pid
socket=/var/run/mysqld/mysqld.sock
datadir=/var/lib/mysql
symbolic-links=0
#设置同步的binarylog二进制日志文件名前缀,默认是binlog
log-bin=mysql-bin
# #服务器唯一ID,主数据库和从数据库的server-id不能重复
server-id=2
#开启GTID同步模式
gtid_mode=on
enforce_gtid_consistency=true



[client]
default-character-set=utf8

[mysql]
default-character-set=utf8

slave 服务器开启主从同步服务

CHANGE MASTER TO master_host = '172.17.0.2',
master_user = 'root',
master_password = '123456',
master_port = 3306,
master_log_file = 'mysql-bin.000002',
master_log_pos = 3843,
master_connect_retry = 30,
get_master_public_key = 1;

# 开启slave
start slave;

参数说明

  • master_port: Master的端口,指的是容器的端口号,不是映射端口
  • master_user:用于数据同步的用户(可以创建一个新的用户用于slave访问)
  • master_password:用户同步的用户的密码
  • master_log_file:指定Slave从哪个日志文件开始复制数据,即上文中提到的File字段的值
  • master_log_pos:指从哪个Position开始读,即上文中提到的Position字段的值
  • master_connect_retry:如果连接失败,重试的时间间隔,单位是秒,默认是60秒
    备注:之所以设置get_master_public_key=1,是因为开启主从复制的时候可能会报出下方的异常。原因是mysql8默认使用插件caching_sha2_password,有些client连接报这个错误,需要拿到server的public key来加密password

在这里插入图片描述

master_port

如果使用的是docker部署,需要找到容器的映射ip .不能写127.0.0.1或者loaclhost

实现:

# 进入容器
docker exec -it 容器id /bin/bash
# 查看容器ip
tail /etc/hosts;

# 查询结果
127.0.0.1	localhost
::1	localhost ip6-localhost ip6-loopback
fe00::0	ip6-localnet
ff00::0	ip6-mcastprefix
ff02::1	ip6-allnodes
ff02::2	ip6-allrouters
172.17.0.4	3696b19b9a8c

master_log_file

在master上查看bin log 的Position

show master status;

在这里插入图片描述

查看从主机同步状态

show slave status \G;

在这里插入图片描述

查看 server_id

show variables like ‘server_uuid’;

Slave_IO_Running:Yes、Slave_SQL_Running:Yes表示配置成功。然后在主库中做表操作就可以实现主从同步了。

同步结果

输入一段描述

后面我又创建了一个从库,数据表都同时同步了.

输入一段描述

读写分离

参考文章:https://www.cnblogs.com/xyfds/articles/8659170.html

读写分离的好处

  • 增加冗余

  • 增加了机器的处理能力

  • 对于读操作为主的应用,使用读写分离是最好的场景,因为可以确保写的服务器压力更小,而读又可以接受点时间上的延迟。

MyCat

Mycat 是基于 cobar 演变而来,对 cobar 的代码进行了彻底的重构,使用 NIO 重构了网络模块,并且优化了 Buffer 内核,增强了聚合,Join 等基本特性,同时兼容绝大多数数据库成为通用的数据库中间件。

输入一段描述

MyCat发展到目前的版本,已经不是一个单纯的MySQL代理了,它的后端可以支持MySQLSQL ServerOracleDB2PostgreSQL等主流数据库,也支持MongoDB这种新型NoSQL方式的存储,未来还会支持更多类型的存储。而在最终用户看来,无论是那种存储方式,在MyCat里,都是一个传统的数据库表,支持标准的SQL语句进行数据的操作,这样一来,对前端业务系统来说,可以大幅降低开发难度,提升开发速度。

MyCat官网:http://www.mycat.io/

PDF教学:http://www.mycat.org.cn/document/mycat-definitive-guide.pdf

环境下载地址:https://github.com/MyCATApache/Mycat-download

docker.mycat : https://github.com/dekuan/docker.mycat

MyCat1.6 无法连接8.0数据库

  • https://github.com/MyCATApache/Mycat-Server/issues/2385

  • https://blog.csdn.net/zhangjing7809/article/details/115522835

MyCAT特性

  • 支持 SQL 92标准
  • 支持Mysql集群,可以作为Proxy使用
  • 支持JDBC连接ORACLE、DB2、SQL Server,将其模拟为MySQL Server使用
  • 支持galera for mysql集群,percona-cluster或者mariadb cluster,提供高可用性数据分片集群
  • 自动故障切换,高可用性
  • 支持读写分离,支持Mysql双主多从,以及一主多从的模式
  • 支持全局表,数据自动分片到多个节点,用于高效表关联查询
  • 支持独有的基于E-R 关系的分片策略,实现了高效的表关联查询
  • 多平台支持,部署和实施简单

Mycat目录说明

  • bin:启动目录

  • conf:配置文件目录

    • server.xml:是Mycat服务器参数调整和用户授权的配置文件

    • schema.xml:是逻辑库定义和表以及分片定义的配置文件

    • rule.xml: 是分片规则的配置文件,分片规则的具体一些参数信息单独存放为文件,也在这个目录下,配置文件修改需要重启MyCAT

    • log4j.xml: 日志存放在logs/log中,每天一个文件,日志的配置是在conf/log4j.xml中,根据自己的需要可以调整输出级别为debug

    • debug级别下,会输出更多的信息,方便排查问题

    • autopartition-long.txt,partition-hash-int.txt,sequence_conf.properties, sequence_db_conf.properties 分片相关的id分片规则配置文

  • lib:jar包目录

  • logs :日志目录

  • tmlogs:临时日志目录

schema.xml

可以说是最重要的配置文件,管理着 MyCat 的逻辑库、表、分片规则、DataNode 以及 DataSource。

  • schema是实际逻辑库的配置,多个schema代表多个逻辑库
  • dataNode是逻辑库对应的分片,如果配置多个分片则需要添加多个dataNode即可
  • dataHost是实际的物理库配置,可以根据业务需要配置多主、主从等其他配置,多个dataHost代表分片对应的物理库地址,writeHost、readHost代表该分片是否配置多写,主从,读写分离等高级特性
  • 添加如下配置:水平切分,数据按Id取模均匀划分到两个数据库中

MyCat 配置

schema.xml 配置读写分离

<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">


	<!--
	name:为mycat逻辑库的名字,对应server<property name="schemas">mydatabase</property>,
	建议设置跟数据库一样的名称
	checkSQLschema:自动检查逻辑库名称并拼接,true会在sql语句中的表名前拼接逻辑库名,
	例如select * from mydatabase.t_user;
	sqlMaxLimit:查询保护、如果没有写limit条件,会自动拼接。只查询100条。
	-->
	<schema name="mydatabase" checkSQLschema="true" sqlMaxLimit="100">
		<!--
      name:为物理数据库的表名,命名与物理数据库的一致 
      dataNode:为dataNode标签(<dataNode name="dn1" dataHost="dtHost1" database="db1" />)里面的name值
      dataNode里面填写的节点数量必须和rule里面的规则数量一致
      例如rule里面只定义了两个0-1M=0  1M-2M=1那么此处只可以指定两个节点,1M=10000,M为单位万
      primaryKey:为表的ID字段,建议和rule.xml里面指定的ID和物理库的ID一致
      rule:分片规则,对应rule.xml中<tableRule name="student_id">的name
      type:表格类型,默认非global,用于全局表定义
		-->
		<table name="sys_user" dataNode="dn1,dn2,dn3" primaryKey="id" rule="auto-sharding-long"></table>
    
  </schema>
		
		<!-- name:节点名称,用于在table标签里面调用
		dataHost:dataHost标签name值(<dataHost name="dtHost1">)
		database:物理数据库名,需要提前创建好实际存在的-->
		<dataNode name="dn1" dataHost="dtHost1" database="slave_test" />
		<dataNode name="dn2" dataHost="dtHost1" database="slave_test" />
		
	<!--
    name:节点名称,在上方dataNode标签中调用
    maxCon:底层数据库的链接最大数
    minCon:底层数据库的链接最小数
    balance:值可以为0,1,2,3,分别表示对当前datahost中维护的数据库们的读操作逻辑
    0: 不开启读写分离,所有的读写操作都在最小的索引号的writeHost(第一个writeHost标签)
    1: 全部的readHost和备用writeHost都参与读数据的平衡,如果读的请求过多,负责写的第一个writeHost也分担一部分
    2 :所有的读操作,都随机的在所有的writeHost和readHost中进行
    3 :所有的读操作,都到writeHost对应的readHost上进行(备用writeHost不参加了),在集群中没有配置ReadHost的情况下,读都到	第一个writeHost完成

    writeType:控制当前datahost维护的数据库集群的写操作
    0:所有的写操作都在第一个writeHost标签的数据库进行
    1:所有的写操作,都随机分配到所有的writeHost(mycat1.5完全不建议配置了)
    dbtype:数据库类型(不同数据库配置不同名称,mysql)
    dbDriver:数据库驱动,native,动态获取
    switchType:切换的逻辑
    -1:故障不切换
    1:故障切换,当前写操作的writeHost故障,进行切换,切换到下一个writeHost;
    slaveThreshold:标签中的<heartbeat>用来检测后端数据库的心跳sql语句;本属性检查从节点与主节点的同步情况(延迟时间数),配合心
    跳语句show slave status; 读写分离时,所有的readHost的数据都可靠
	-->
	<dataHost name="localhost1" maxCon="1000" minCon="10" balance="0"
			  writeType="0" dbType="mysql" dbDriver="jdbc" switchType="-1"  slaveThreshold="100">
    <!-- 心跳维护 -->
		<heartbeat>select user()</heartbeat>
		<!-- master主机读写入 -->
		<writeHost host="localhost1_1" url="jdbc:mysql://localhost:3307?serverTimezone=Asia/Shanghai&amp;useSSL=false&amp;characterEncoding=utf8" user="root" password="123456">
			<!-- slave01 读取  -->
			 <readHost host="localhost1_2" url="jdbc:mysql://localhost:3308?serverTimezone=Asia/Shanghai&amp;useSSL=false&amp;characterEncoding=utf8" user="root" password="123456" />
			<readHost host="localhost1_3" url="jdbc:mysql://localhost:3309?serverTimezone=Asia/Shanghai&amp;useSSL=false&amp;characterEncoding=utf8" user="root" password="123456" />
		</writeHost>
	</dataHost>

Server.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mycat:server SYSTEM "server.dtd">
<mycat:server xmlns:mycat="http://io.mycat/">
	<system>
    <!-- 0为需要密码登陆、1为不需要密码登陆 ,默认为0,设置为1则需要指定默认账户-->
	<property name="nonePasswordLogin">0</property> 
   <!-- 0遇上没有实现的报文(Unknown command:),就会报错、1为忽略该报文,返回ok报文。
	在某些mysql客户端存在客户端已经登录的时候还会继续发送登录报文,mycat会报错,该设置可以绕过这个错误-->
	<property name="ignoreUnknownCommand">0</property>
	<property name="useHandshakeV10">1</property>
    <property name="removeGraveAccent">1</property>
   <!-- 1为开启实时统计、0为关闭 -->
	<property name="useSqlStat">0</property> 
	<property name="useGlobleTableCheck">0</property>  <!-- 1为开启全加班一致性检测、0为关闭 -->
	<property name="sqlExecuteTimeout">300</property>  <!-- SQL 执行超时 单位:秒-->
		<property name="sequenceHandlerType">1</property>
	<!--<property name="sequnceHandlerPattern">(?:(\s*next\s+value\s+for\s*MYCATSEQ_(\w+))(,|\)|\s)*)+</property>
	INSERT INTO `travelrecord` (`id`,user_id) VALUES ('next value for MYCATSEQ_GLOBAL',"xxx");
	-->
	<!--必须带有MYCATSEQ_或者 mycatseq_进入序列匹配流程 注意MYCATSEQ_有空格的情况-->
	<property name="sequnceHandlerPattern">(?:(\s*next\s+value\s+for\s*MYCATSEQ_(\w+))(,|\)|\s)*)+</property>
	<property name="subqueryRelationshipCheck">false</property> <!-- 子查询中存在关联查询的情况下,检查关联字段中是否有分片字段 .默认 false -->
	<property name="sequenceHanlderClass">io.mycat.route.sequence.handler.HttpIncrSequenceHandler</property>
      <!--  <property name="useCompression">1</property>--> <!--1为开启mysql压缩协议-->
        <!--  <property name="fakeMySQLVersion">5.6.20</property>--> <!--设置模拟的MySQL版本号-->
	<!-- <property name="processorBufferChunk">40960</property> -->
	<!-- 
	<property name="processors">1</property> 
	<property name="processorExecutor">32</property> 
	 -->
        <!--默认为type 0: DirectByteBufferPool | type 1 ByteBufferArena | type 2 NettyBufferPool -->
		<property name="processorBufferPoolType">0</property>
		<!--默认是65535 64K 用于sql解析时最大文本长度 -->
		<!--<property name="maxStringLiteralLength">65535</property>-->
		<!--<property name="sequenceHandlerType">0</property>-->
		<!--<property name="backSocketNoDelay">1</property>-->
		<!--<property name="frontSocketNoDelay">1</property>-->
		<!--<property name="processorExecutor">16</property>-->
		<!--
			<property name="serverPort">8066</property> <property name="managerPort">9066</property> 
			<property name="idleTimeout">300000</property> <property name="bindIp">0.0.0.0</property>
			<property name="dataNodeIdleCheckPeriod">300000</property> 5 * 60 * 1000L; //连接空闲检查
			<property name="frontWriteQueueSize">4096</property> <property name="processors">32</property> -->
		<!--分布式事务开关,0为不过滤分布式事务,1为过滤分布式事务(如果分布式事务内只涉及全局表,则不过滤),2为不过滤分布式事务,但是记录分布式事务日志-->
		<property name="handleDistributedTransactions">0</property>
		
		<!--  off heap for merge/order/group/limit  1开启   0关闭 -->
		<property name="useOffHeapForMerge">0</property>
		<!-- 单位为m -->
    <property name="memoryPageSize">64k</property>
		<!--  单位为k -->
		<property name="spillsFileBufferSize">1k</property>

		<property name="useStreamOutput">0</property>

		<!-- 单位为m -->
		<property name="systemReserveMemorySize">384m</property>

		<!--是否采用zookeeper协调切换  -->
		<property name="useZKSwitch">false</property>

		<!-- XA Recovery Log日志路径 -->
		<!--<property name="XARecoveryLogBaseDir">./</property>-->

		<!-- XA Recovery Log日志名称 -->
		<!--<property name="XARecoveryLogBaseName">tmlog</property>-->
		<!--如果为 true的话 严格遵守隔离级别,不会在仅仅只有select语句的时候在事务中切换连接-->
		<property name="strictTxIsolation">false</property>
		<!--如果为0的话,涉及多个DataNode的catlet任务不会跨线程执行-->
		<property name="parallExecute">0</property>
		<!-- 1为数据库方式配置序列,0为文本方式 -->
	    <property name="sequnceHandlerType">1</property>		
	</system>
	
	<!-- 全局SQL防火墙设置 -->
	<!--白名单可以使用通配符%或着*-->
	<!--例如<host host="127.0.0.*" user="root"/>-->
	<!--例如<host host="127.0.*" user="root"/>-->
	<!--例如<host host="127.*" user="root"/>-->
	<!--例如<host host="1*7.*" user="root"/>-->
	<!--这些配置情况下对于127.0.0.1都能以root账户登录-->
	<!--
	<firewall>
	   <whitehost>
	      <host host="1*7.0.0.*" user="root"/>
	   </whitehost>
       <blacklist check="false">
       </blacklist>
	</firewall>
	-->

	<user name="root" defaultAccount="true">
		<property name="password">123456</property>
		<property name="schemas">TESTDB</property>
		<property name="defaultSchema">TESTDB</property>
		<!--No MyCAT Database selected 错误前会尝试使用该schema作为schema,不设置则为null,报错 -->
		
		<!-- 表级 DML 权限设置 -->
		<!-- 		
		<privileges check="false">
			<schema name="TESTDB" dml="0110" >
				<table name="tb01" dml="0000"></table>
				<table name="tb02" dml="1111"></table>
			</schema>
		</privileges>		
		 -->
	</user>

	<user name="user">
		<property name="password">user</property>
		<property name="schemas">TESTDB</property>
		<property name="readOnly">true</property>
		<property name="defaultSchema">TESTDB</property>
	</user>

</mycat:server>

启动Mycat

在Mycat bin目录启动。

./mycat start

然后就可以在logs目录看到启动日志。

启动日志

Spring Boot + MyBatis 测试

Spring Boot + MyBatis 测试

运行结果

同步数据

使用Aop + 多数据源切换 实现读写分离

项目目录

项目目录

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.5.2</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>MyCatTest</name>
    <description>Demo project for Spring Boot</description>



    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
			
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>


        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.4</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

application.yml


spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.jdbc.Driver

    druid:
      # 主数据源
      master:
        url: jdbc:mysql://localhost:3307/slave_test?allowMultiQueries=true&useUnicode=true&characterEncoding=utf8
        username: root
        password: 123456

      # 从数据源
      slave:
        enabled: false
        url: jdbc:mysql://localhost:3308/slave_test?allowMultiQueries=true&useUnicode=true&characterEncoding=utf8
        username: root
        password: 123456


      # 初始连接数
      initialSize: 5
      # 最小连接池数量
      minIdle: 10
      # 最大连接池数量
      maxActive: 20
      # 配置获取连接等待超时的时间
      maxWait: 60000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      minEvictableIdleTimeMillis: 300000
      # 配置一个连接在池中最大生存的时间,单位是毫秒
      maxEvictableIdleTimeMillis: 900000
      # 配置检测连接是否有效
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      webStatFilter:
        enabled: true
      statViewServlet:
        enabled: true
        # 设置白名单,不填则允许所有访问
        allow:
        url-pattern: /druid/*
        # 控制台管理用户名和密码
        login-username:
        login-password:
      filter:
        stat:
          enabled: true
          # 慢SQL记录
          log-slow-sql: true
          slow-sql-millis: 1000
          merge-sql: true
        wall:
          config:
            multi-statement-allow: true

创建 枚举

/**
 * @author YiuFaiChu
 * @description 数据源类型
 **/
public enum DataSourceType {

    /**
     * 主库
     */
    MASTER,

    /**
     * 从库
     */
    SLAVE
}

Druid 配置


/**
 * @author YiuFaiChu
 * @description Druid 配置
 **/
@Configuration
public class DruidConfig {

    /**
     * 主数据源注入
     *
     * @return
     */
    @Bean(name = "masterDataSource")
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource masterDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    /**
     * 从数据源注入
     *
     * @return
     * @ConditionalOnProperty 可以控制注解是否生效,这里通过配置文件控制从数据源是否可用
     */
    @Bean(name = "slaveDataSource")
    @ConfigurationProperties("spring.datasource.druid.slave")
    public DataSource slaveDataSource() {
        return DruidDataSourceBuilder.create().build();
    }


    /**
     * 获取动态数据源
     *
     * @return
     */
    @Bean(name = "dynamicDataSource")
    @Primary
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        // 设置默认数据源为
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
        // 配置多数据源,
        // 添加数据源标识和DataSource引用到目标源映射
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put(DataSourceType.MASTER.name(), masterDataSource());
        dataSourceMap.put(DataSourceType.SLAVE.name(), slaveDataSource());
        dynamicDataSource.setTargetDataSources(dataSourceMap);
        return dynamicDataSource;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dynamicDataSource());
    }


}

数据源切换处理


/**
 * 数据源切换处理
 */
@Slf4j
public class DynamicDataSourceContextHolder
{
    /**
     * https://www.zhihu.com/question/341005993
     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
     *  所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
     *  主要是做数据隔离,填充的数据只属于当前线程。
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * 设置数据源的变量
     */
    public static void setDataSourceType(String dsType)
    {
        log.info("切换到{}数据源", dsType);
        CONTEXT_HOLDER.set(dsType);
    }

    /**
     * 获得数据源的变量
     */
    public static String getDataSourceType()
    {
        return CONTEXT_HOLDER.get();
    }

    /**
     * 清空数据源变量
     */
    public static void clearDataSourceType()
    {
        CONTEXT_HOLDER.remove();
    }
}

动态获取数据源

/**
 * 动态获取数据源
 * 
 */
public class DynamicDataSource extends AbstractRoutingDataSource
{

    @Override
    protected Object determineCurrentLookupKey()
    {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}

自定义注解


/**
 * 自定义多数据源切换注解
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource {

    /**
     * 切换数据源名称
     */
    public DataSourceType value() default DataSourceType.MASTER;
}

使用Aop进行数据源切换


/**
 * @author YiuFaiChu
 * @description 数据源Aop实现
 **/
@Aspect
@Order(1)
@Component
public class DataSourceAop {
    protected Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * 设置切入点
     */
    @Pointcut("@annotation(com.example.demo.aop.annotations.DataSource)"
            + "|| @within(com.example.demo.aop.annotations.DataSource)")
    public void pointCut() {
    }


    /**
     * 执行要环绕的方法
     *
     * @param point
     * @return
     * @throws Throwable
     */
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        DataSource dataSource = getDataSource(point);

        if (dataSource != null) {
            DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
        }

        try {
            return point.proceed();
        } finally {
            // 销毁数据源 在执行方法之后
            DynamicDataSourceContextHolder.clearDataSourceType();
        }
    }

    /**
     * 获取需要切换的数据源
     */
    public DataSource getDataSource(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        // 获取注解上面的类型
        DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
        if (Objects.nonNull(dataSource)) {
            return dataSource;
        }
        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
    }


}

测试

/**
 * @author YiuFaiChu
 * @description mycat测试
 **/
@Mapper
public interface SysUserMapper {

   @Select("select * from sys_user")
    // 使用 MASTER 数据源获取数据
    @DataSource(DataSourceType.MASTER)
    List<Map<String, Object>> selectList();

    @Insert("insert into sys_user (id,name) values (#{id},#{name})")
    // 使用 SLAVE 数据源添加数据
    @DataSource(DataSourceType.SLAVE)
    int insertOne(int id, String name);

    @Delete("delete from sys_user where id = #{id}")
    int delOne(String id);
}

运行结果

运行结果

源码同步

https://gitee.com/guofeimayun/my-cat-test.git

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值