文章同步:http://www.guofeian.cn/article_details/624949244733960192
为什么需要主从数据库
当主数据库出现问题时,可以当从数据库代替主数据库,可以避免数据的丢失。
-
Mysql内建的复制功能是构建大型,高性能应用程序的基础。
-
将Mysql的数据分布到多个系统上去,这种分布的机制,是通过将Mysql的某一台主机的数据复制到其它主机(slaves)上,并重新执行一遍来实现的。
-
复制过程中一个服务器充当主服务器,而一个或多个其它服务器充当从服务器。主服务器将更新写入二进制日志文件,并维护文件的一个索引以跟踪日志循环。
-
当一个从服务器连接主服务器时,它通知主服务器从服务器在日志中读取的最后一次成功更新的位置。从服务器接收从那时起发生的任何更新,然后封锁并等待主服务器通知新的更新。
Mysql主从能解决的问题
-
数据分布 (Data distribution )
-
负载平衡(load balancing)
-
数据备份(Backups) ,保证数据安全
-
高可用性和容错行(High availability and failover)
-
实现读写分离,缓解数据库压力
读写分离
-
当主数据库进行对数据的增删改也就是写操作时,将查询的任务交给从数据库。
-
减轻主数据库的压力。因为进行写操作更耗时,所以如果不进行读写分离的话,写操作将会影响到读操作的效率。
分离有两种实现方式
1、第一种是依靠中间件(比如:MyCat),也就是说应用程序连接到中间件,中间件帮我们做SQL分离;
2、第二种是应用程序自己去做分离。这里我们选择程序自己来做,主要是利用Spring提供的路由数据源,以及AOP
然而,应用程序层面去做读写分离最大的弱点(不足之处)在于无法动态增加数据库节点,因为数据源配置都是写在配置中的,新增数据库意味着新加一个数据源,必然改配置,并重启应用。当然,好处就是相对简单。
Mysql 主从同步
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代理了,它的后端可以支持MySQL、SQL Server、Oracle、DB2、PostgreSQL等主流数据库,也支持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&useSSL=false&characterEncoding=utf8" user="root" password="123456">
<!-- slave01 读取 -->
<readHost host="localhost1_2" url="jdbc:mysql://localhost:3308?serverTimezone=Asia/Shanghai&useSSL=false&characterEncoding=utf8" user="root" password="123456" />
<readHost host="localhost1_3" url="jdbc:mysql://localhost:3309?serverTimezone=Asia/Shanghai&useSSL=false&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 测试
运行结果
使用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