使用AOP动态切换数据源实现mysql的读写分离
基于docker搭建Mysql主从复制
1、创建两个mysql容器,一主一从,并将数据持久化到本地
master节点:
docker run -p 3301:3306 --name mysql_master \
-v /mydata/mysql_copy/mysql_master/log:/var/log/mysql \
-v /mydata/mysql_copy/mysql_master/data:/var/lib/mysql \
-v /mydata/mysql_copy/mysql_master/conf:/etc/mysql -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7
slave节点:
docker run -p 3302:3306 --name mysql_slave \
-v /mydata/mysql_copy/mysql_slave/log:/var/log/mysql \
-v /mydata/mysql_copy/mysql_slave/data:/var/lib/mysql \
-v /mydata/mysql_copy/mysql_slave/conf:/etc/mysql -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7
2、master节点的配置
1)配置master节点的my.cnf文件,主要配置server-id节点唯一标识,和log-bin二进制文件位置
[mysqld]
#mysql 服务ID,保证整个集群环境中唯一
server-id=1
#开启并binlog 日志的存储路径和文件名
log-bin=/var/lib/mysql/mysqlbin
#是否只读,1 代表只读, 0 代表读写
read-only=0
#忽略的数据, 指不需要同步的数据库
binlog-ignore-db=mysql
2)执行完毕后需要重启mysql,或者直接重启容器
#重启mysql
service mysql restart
#重启容器
docker restart mysql_master
3)创建同步数据的账户,并且进行授权操作
#为slave节点创建一个账号,用户名为slave,密码为slave,拥有对主节点的所有访问权限
grant replication slave on *.* to 'slave'@'%' identified by 'slave';
#刷新权限生效
flush privileges;
4)查看master状态:
show master status;
注意这里生成的log-bin文件的文件名和position,后面的从节点需要根据这个信息进行配置
3、slave节点的配置
1)配置slave节点的my.cnf文件
[mysqld]
#mysql服务端ID,唯一
server-id=2
#开启并指定binlog日志
log-bin=/var/lib/mysql/mysqlbin
2)执行完毕后需要重启mysql,或者直接重启容器
#重启mysql
service mysql restart
#重启容器
docker restart mysql_master
3)在mysql客户端中使用命令配置master节点的信息,指定当前从库对应的主库的IP地址,用户名,密码,从哪个日志文件开始的那个位置开始同步推送日志。
master_host:主节点的ip,在docker中使用docker inspect mysql_master
查看容器的内部局域网ip,通过局域网让两个容器互通
master_log_file和master_log_pos根据主节点进行配置
master_user和master_password也就是前面创建的访问用户
change master to master_host= '172.17.0.2', master_user='slave', master_password='slave', master_log_file='mysqlbin.000001', master_log_pos=413;
4)开启同步操作
start slave;
5)show slave status
展示slave节点信息:
至此,mysql的一主一从搭建完毕
AOP动态切换数据源
1、流程图
2、思路描述:使用AOP在请求进入service层之前根据调用的service的方法名切换此次请求使用的数据源,我们在在配置文件中配置数据源的集合和拦截方法名前缀与数据源的集合的key的对应关系注入到我们自定义的AbstractRoutingDataSource子类我们就可以在切面中使用他们
1)在配置文件中配置数据源集合和拦截方法名前缀与数据源的集合的key的对应关系
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--读数据源-->
<bean id="readDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.read.driver}"></property>
<property name="jdbcUrl" value="${jdbc.read.url}"></property>
<property name="user" value="${jdbc.read.username}"></property>
<property name="password" value="${jdbc.read.password}"></property>
</bean>
<!--写数据源-->
<bean id="writeDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.write.driver}"></property>
<property name="jdbcUrl" value="${jdbc.write.url}"></property>
<property name="user" value="${jdbc.write.username}"></property>
<property name="password" value="${jdbc.write.password}"></property>
</bean>
<!--将配置的数据源注入到容器中-->
<bean id="dataSource" class="cn.itcast.aop.datasource.ChooseDataSource">
<!--配置目标数据源集合-->
<property name="targetDataSources">
<map key-type="java.lang.String" value-type="javax.sql.DataSource">
<entry key="write" value-ref="writeDataSource"></entry>
<entry key="read" value-ref="readDataSource"></entry>
</map>
</property>
<!--默认使用写库-->
<property name="defaultTargetDataSource" ref="writeDataSource"></property>
<!--配置拦截方法的前缀与数据源集合的key的对应关系-->
<property name="methodType">
<map key-type="java.lang.String" value-type="java.lang.String">
<entry key="write" value="add,update,create,delete,remove,insert"></entry>
<entry key="read" value="get,select,count,list,query,find"></entry>
</map>
</property>
</bean>
</beans>
2)编写一个类存储此次请求用到的数据源的key
/**
* 此方法保存此次处理请求的数据源的key
*/
public class DataSourceHandler {
//线程独享,每个请求之间不会影响,线程安全
private static ThreadLocal<String> hand = new ThreadLocal<>();
public static String get() {
return hand.get();
}
public static void set(String hand) {
DataSourceHandler.hand.set(hand);
}
}
3)编写一个AbstractRoutingDataSource的子类ChooseDataSource,维护配置拦截方法的前缀与数据源集合的key的对应关系的集合
public class ChooseDataSource extends AbstractRoutingDataSource {
//key对应的拦截方法
public static Map<String, List<String>> METHOD_TYPE_MAP = new HashMap<>();
/**
* 选择当前使用的数据源在数据源集合中的key
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return DataSourceHandler.get();
}
/**
* 配置文件通过此方法注入方法名前缀与数据源key的对应关系,注入时需要将字符串解析成集合存储
* @param map
*/
public void setMethodType(Map<String,String> map){
for (String key : map.keySet()) {
List<String> list = new ArrayList<>();
String[] methods = map.get(key).split(",");
for (String method : methods) {
if(!StringUtils.isEmpty(method)) {
list.add(method);
}
}
METHOD_TYPE_MAP.put(key,list);
}
System.out.println("METHOD_TYPE_MAP : "+METHOD_TYPE_MAP);
}
}
分析一下AbstractRoutingDataSource的源码
//AbstractRoutingDataSource
//通过这个方法获取数据源
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = this.determineCurrentLookupKey();//调用我们在ChooseDataSource中重写的选择数据源集合的key的方法
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);//通过key获取我们选择的数据源
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {//如果没有,就走默认的,我们在配置文件中指定的默认的是主(写)数据库
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
} else {
return dataSource;
}
}
4)在AOP中根据请求的service方法切换数据源
/**
* 拦截调用service的方法,切换数据源
*/
@Component
@Aspect
@Order(-9999)//和xml文件的事物管理器配置冲突,让切面先注入进容器
@EnableAspectJAutoProxy(proxyTargetClass = true)//配置文件中没有开启service层的自动代理,这里需要手动开启自动代理
public class DataSourceAspect {
@Resource
private ChooseDataSource chooseDataSource;
@Before("execution(* cn.itcast.service.*.*(..))")
@Order(-9999)
public void beforeService(JoinPoint point) {
String method = point.getSignature().getName();
System.out.println("拦截的方法名:" + method);
//获取拦截的方法名
String methodName = point.getSignature().getName();
Map<String, List<String>> map = ChooseDataSource.METHOD_TYPE_MAP;
for (String key : map.keySet()) {
for (String pre : map.get(key)) {
if (methodName.startsWith(pre)) {
//如果找到key,注入后返回
System.out.println("key:" + key);
DataSourceHandler.set(key);
break;
}
}
}
}
}
测试查询,观察数据库:
插入测试: