spring 读写分离和多数据源配置的详解

说明:

1。我所写的是建立在查看学习其他人所写的基础之上的,如果有相似的代码,恳求请原谅.

2。本方法完全无侵入式。但需要友好的命名约定。

3。读写分离和多数据源是建立在主从数据库同步的基础上,请查看我的另一篇文章:主从同步

4。为什么要读写分离,读写分离到什么地步?

    为什么要读写分离?因为数据表明,一个application,查的数量非常多,增删改的数量非常小。所以为了均衡压力,初步可以让增删改在一个主数据库上,再添些从数据库用来查询。

    读写分离到什么地步呢?我认为并不是每个查询都在从库上,打个比方,有一个service里的Upadte()方法,里面做了一个先查询select,看是否为null或者状态是否XX,然后再决定要不要update更改,那么这个更改肯定是在主库上,然而select是否去从库上呢?我觉得没有必要,因为我们并不是来均衡这种select的,而是均衡那些findListByPage的,所以不必那么细微。本详解就是建立在这种思想上利用事务控制达到读写分离的效果的。

5。如果有什么疏漏和错误,各位大神还请教我啊,准备用到项目上了啊!!

先看配置文件:

1.jdbc配置文件,这个是建了3个数据库,分别是test1,test2,test3,里面各有一个user表,字段 id int ,age int

jdbc.slave1.driver=com.mysql.jdbc.Driver
jdbc.slave1.url=jdbc:mysql://localhost:3306/test1?useUnicode=true&characterEncoding=utf-8&autoReconnect=true
jdbc.slave1.username=root
jdbc.slave1.password=123


jdbc.slave2.driver=com.mysql.jdbc.Driver
jdbc.slave2.url=jdbc:mysql://localhost:3306/test2?useUnicode=true&characterEncoding=utf-8&autoReconnect=true
jdbc.slave2.username=root
jdbc.slave2.password=123

jdbc.master.driver=com.mysql.jdbc.Driver
jdbc.master.url=jdbc:mysql://localhost:3306/test3?useUnicode=true&characterEncoding=utf-8&autoReconnect=true
jdbc.master.username=root
jdbc.master.password=123

2.spring的配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:task="http://www.springframework.org/schema/task" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
		http://www.springframework.org/schema/beans/spring-beans-4.0.xsd 
		http://www.springframework.org/schema/mvc 
		http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd 
		http://www.springframework.org/schema/context 
		http://www.springframework.org/schema/context/spring-context-4.0.xsd 
		http://www.springframework.org/schema/aop 
		http://www.springframework.org/schema/aop/spring-aop-4.0.xsd 
		http://www.springframework.org/schema/tx 
		http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
		http://www.springframework.org/schema/task
   		http://www.springframework.org/schema/task/spring-task-4.0.xsd
		http://code.alibabatech.com/schema/dubbo        
		http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

	<!-- 配置 读取properties文件 jdbc.properties -->
	<context:property-placeholder location="classpath:jdbc.properties" />
	
	<!-- 配置主 数据源 -->
	<bean id="masterDataSource" class="com.alibaba.druid.pool.DruidDataSource">
		<!-- 驱动 -->
		<property name="driverClassName" value="${jdbc.master.driver}" />
		<!-- url -->
		<property name="url" value="${jdbc.master.url}" />
		<!-- 用户名 -->
		<property name="username" value="${jdbc.master.username}" />
		<!-- 密码 -->
		<property name="password" value="${jdbc.master.password}" />
	</bean>
	
	
	
	<!-- 配置 从数据源 -->
	<bean id="slavesDataSource1" class="com.alibaba.druid.pool.DruidDataSource">
		<!-- 驱动 -->
		<property name="driverClassName" value="${jdbc.slave1.driver}" />
		<!-- url -->
		<property name="url" value="${jdbc.slave1.url}" />
		<!-- 用户名 -->
		<property name="username" value="${jdbc.slave1.username}" />
		<!-- 密码 -->
		<property name="password" value="${jdbc.slave1.password}" />
	</bean>
	<!-- 配置 从数据源 -->
	<bean id="slavesDataSource2" class="com.alibaba.druid.pool.DruidDataSource">
		<!-- 驱动 -->
		<property name="driverClassName" value="${jdbc.slave2.driver}" />
		<!-- url -->
		<property name="url" value="${jdbc.slave2.url}" />
		<!-- 用户名 -->
		<property name="username" value="${jdbc.slave2.username}" />
		<!-- 密码 -->
		<property name="password" value="${jdbc.slave2.password}" />
	</bean>
	
	<bean id="dataSource" class="com.mysql.MyDataSource">
		<property name="masterDataSource" ref="masterDataSource"></property>
		<property name="slavesDataSource">
			<list>
				<ref bean="slavesDataSource1"/>
				<ref bean="slavesDataSource2"/>
			</list>
		</property>
	</bean>
	


	<!-- 事务管理器 -->
	<bean id="transactionManager" class="com.mysql.MyDataSourceTransactionManager">
		<property name="dataSource" ref="dataSource" />
	</bean>
	
	<!-- 通知 -->
	<tx:advice id="txAdvice" transaction-manager="transactionManager">
		<tx:attributes>
			<!-- 传播行为 -->
			<tx:method name="save*" propagation="REQUIRED"
				rollback-for="java.lang.Exception" />
			<tx:method name="insert*" propagation="REQUIRED"
				rollback-for="java.lang.Exception" />
			<tx:method name="add*" propagation="REQUIRED" rollback-for="java.lang.Exception" />
			<tx:method name="create*" propagation="REQUIRED"
				rollback-for="java.lang.Exception" />
			<tx:method name="delete*" propagation="REQUIRED"
				rollback-for="java.lang.Exception" />
			<tx:method name="update*" propagation="REQUIRED"
				rollback-for="java.lang.Exception" />
			<tx:method name="find*" propagation="SUPPORTS" read-only="true" />
			<tx:method name="select*" propagation="SUPPORTS" read-only="true" />
			<tx:method name="get*" propagation="SUPPORTS" read-only="true" />
		</tx:attributes>
	</tx:advice>
	<!-- 切面 -->
	<aop:config>
		<aop:advisor advice-ref="txAdvice" pointcut="execution(* com.service.*.*(..))" />
	</aop:config>

	<!-- 配置 Mybatis的工厂 -->
	<bean class="org.mybatis.spring.SqlSessionFactoryBean">
		<!-- 数据源 -->
		<property name="dataSource" ref="dataSource" />
		<!-- 配置Mybatis的核心 配置文件所在位置 -->
		<property name="configLocation" value="classpath:SqlMapConfig.xml" />
		<!-- 配置pojo别名 -->
	</bean>

	<!-- 配置 1:原始Dao开发 接口实现类 Mapper.xml 三个 2:接口开发 接口 不写实现类 Mapper.xml 二个 (UserDao、ProductDao 
		、BrandDao。。。。。。。) 3:接口开发、并支持扫描 cn.itcast.core.dao(UserDao。。。。。) 写在此包下即可被扫描到 -->
	<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
		<property name="basePackage" value="com.dao" />
	</bean>
</beans>

以上配置是:

首先,我自定义了两个类。分别属于DataSource 和 DataSourceTransactionManager.

即:com.mysql.MyDataSource和com.mysql.MyDataSourceTransactionManager这俩

然后把主数据源和另外两个从数据源塞到自定义的数据源里。并且事务管理器采用自定义的数据源事务管理器

我们将要怎么做?

事务!事务!还是事务!

我要将凡是增删改的请求都加上事务,将查询的请求都加上read-only,那么在上面那个配置文件里我们就可以看到这些配置。但如果你要是这样配置,对于service层的方法名就要准确的命名了。

如果配好了事务,就要想办法,如果走了事务,我就选择数据源为主写数据源,如果不走事务,我就选择数据源为从数据源。

代码解析:

package com.mysql;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import javax.sql.DataSource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * 自定义数据源
 * @author dumingwei
 *
 */
public class MyDataSource extends AbstractRoutingDataSource {
	private static final String MASTER = "master";
	private static final String SLAVER = "slaver";

	private DataSource masterDataSource; // 主数据源
	private List<DataSource> slavesDataSource; // 从数据源
	private boolean hasSlaver = true;//是否有从数据源
	private static int slaverCounts=0;//从数据源的个数
	/**
	 * 该方法在初始化的时候会执行一次
	 */
	@Override
	public void afterPropertiesSet() {
		//1.把主数据源设置为默认的数据源
		if (this.masterDataSource == null) {
			throw new IllegalArgumentException("主数据源是必须的");
		}
		setDefaultTargetDataSource(masterDataSource);
		//2.新建数据源容器 把主数据源放进去
		Map<Object, Object> targetDataSources = new HashMap<>();
		targetDataSources.put(MASTER, masterDataSource);
		//3.判断是否有从数据源,如果没有,那么设定hasSlaver=false;
		//如果有,把从数据源放进去.并且给从数据源起名字叫SLAVER+i ,把size()记录为slaverCounts
		if (slavesDataSource != null && slavesDataSource.size() > 0) {
			slaverCounts=slavesDataSource.size();
			for (int i = 0; i < slavesDataSource.size(); i++) {
				Object object = slavesDataSource.get(i);
				targetDataSources.put(SLAVER + i, object);
			}
		} else {
			hasSlaver = false;
		}
		//4.设定数据源容器为该容器
		setTargetDataSources(targetDataSources);
		super.afterPropertiesSet();
	}
	/**
	 * 该方法在初始化的时候以及每次访问的时候会执行一次.
	 * 这个方法非常重要,是选择数据源的核心方法
	 * 本方法是如果选择从数据源,随机选择从数据源
	 */
	@Override
	protected Object determineCurrentLookupKey() {
		//1.看是否有从数据源,如果没有那么就用主数据源
		if (!hasSlaver) {
			return MASTER;
		} 
		//2.看是否人为设定为主数据源,如果设定,那么就用主数据源
		//这句是读写分离主从控制的核心
		if(MyDataSourceHolder.isUseMaster()){
			return MASTER;
		}
		//3.其他的就是从数据源了,然后随机选择从数据源,使用SLAVER+i这个数据源
		int nextInt = new Random().nextInt(slaverCounts);
		return SLAVER+nextInt;
	}

	public DataSource getMasterDataSource() {
		return masterDataSource;
	}

	public void setMasterDataSource(DataSource masterDataSource) {
		this.masterDataSource = masterDataSource;
	}

	public List<DataSource> getSlavesDataSource() {
		return slavesDataSource;
	}

	public void setSlavesDataSource(List<DataSource> slavesDataSource) {
		this.slavesDataSource = slavesDataSource;
	}	

}
我们可以看到,在配置文件里,主数据源和从数据源的list已经放到这个类里面了。在加载配置文件完之后,会执行了afterPropertiesSet()这个方法。

这个方法把主数据源和从数据源放到一个map容器里面,并且给他们起了名字分别是:master,slave0,slave1......,同时记住了从数据源的数量 slaverCounts;

如果没有放从数据源,那么把类属性hasSlave=false.

另外以一个determineCurrentLookupKey()方法先不看,还不该它执行。(虽然它启动时会执行一次,但没什么用);

那么当一个请求发来的时候,会经过数据源事务管理器。

我们看另外一个数据源事务管理器类。

package com.mysql;

import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
/**
 * 数据源事务控制器
 * @author dumingwei
 *
 */
public class MyDataSourceTransactionManager extends DataSourceTransactionManager {
	/**
     * 本方法是如果开启了事务,才执行.
     * @param transaction
     * @param definition
     */
    @Override
    protected void doBegin(Object transaction, TransactionDefinition definition) {
        //1.是否是readOnly?
        boolean readOnly = definition.isReadOnly();
        if(!readOnly) {
        	//2.如果不是,那么就人为设定为主数据源
        	MyDataSourceHolder.useMaster(true);
        }
        super.doBegin(transaction, definition);
    }

    /**
     * 清理本地线程的数据源
     * @param transaction
     */
    @Override
    protected void doCleanupAfterCompletion(Object transaction) {
        super.doCleanupAfterCompletion(transaction);
        //线程结束后,记得要清理垃圾
        MyDataSourceHolder.clear();
    }
}

这个就是事务控制器。它的两个方法doBegin()是在事务启动时执行的,doCleanupAfterCompletion()是在事务结束时执行的。

我们要做的是什么?就是在事务开启的时候选择主数据源啊。

看代码,如果事务不是readOnly,执行了MyDataSourceHolder.useMaster(true);就这一句,其他都没什么用。

这个又是什么?

贴出来:

package com.mysql;
/**
 * 线程的弱容器.只放一个是否使用主数据源的boolean值.
 * @author dumingwei
 *
 */
public final class MyDataSourceHolder {
	
    private static final ThreadLocal<Boolean> holder = new ThreadLocal<Boolean>();

    private MyDataSourceHolder() {
       
    }
    public static void useMaster(boolean useMaster){
        holder.set(useMaster);
    }
    public static Boolean isUseMaster(){
        return holder.get()==null?false:holder.get();
    }

    public static void clear(){
        holder.remove();
    }

}

其实什么都不是,只是绑定在当前线程上的一个弱容器,里面就放了一个boolean值,如果是true,代表了我们启用了主数据源,如果是false或者是null,代表了我们没有启动主数据源。

就是说,如果事务不是readOnly,我们让当前线程存了一个true而已。

请求发来后,在事务里如果访问数据库,那么就会走最上面那个MyDataSource里剩下的那个方法determineCurrentLookupKey();(事实上有没有事务只要访问数据库都会走那个方法)

这个方法究竟是做什么的呢?它其实就是选择数据源的核心,它还有个返回值,返回值是什么,就选择哪个数据源来执行sql语句。

把上面的代码再粘来:

protected Object determineCurrentLookupKey() {
		//1.看是否有从数据源,如果没有那么就用主数据源
		if (!hasSlaver) {
			return MASTER;
		} 
		//2.看是否人为设定为主数据源,如果设定,那么就用主数据源
		//这句是读写分离主从控制的核心
		if(MyDataSourceHolder.isUseMaster()){
			return MASTER;
		}
		//3.其他的就是从数据源了,然后随机选择从数据源,使用SLAVER+i这个数据源
		int nextInt = new Random().nextInt(slaverCounts);
		return SLAVER+nextInt;
	}

第一句。如果没有从数据库,那就选择主数据库。

第二句。从当前线程里取出那个Boolean值,如果是true,我们就选择主数据源。

其他,选择一个从0到从数据源数量的随机数,并且被SLAVER加上,我们随机选择了一个名字叫SLAVER+i的从数据源。

这个代码,每一次访问数据库的时候都会执行。无论什么时候。

我们如果只配置了主数据源。那么就会执行主数据源。

我们如果配置了从数据源,但如果没有事务或者是readonly这种,代码里并没有设置当前线程为useMaster=true,所以会随机选择一个从数据源。如果开启了事务,且不是readonly,就会选择主数据源。

OK!已经完成了。那么开始测试

测试:

3张表, 

主表 id =1,age=100  

从表1 id=1 ,age=1000 

从表2 id =1 age=10000

这里只是纯粹测试分离,并没有测主从。

写方法去查询id为1的age。就会发现偶尔会查到1000,偶尔会查到10000,这说明随机分配从数据源是成功了。

写方法修改id为1的age。就会发现修改的必定是主表。这说明读写分离成功了。

结语

其实是不完善的。

之所以随机从数据库,原因在于大量随机会更加均衡,但是这里随机是伪随机。会出现问题吗?

是不是加上使用次数的标记,然后选择使用次数最少的那个才好?不过这样要考虑多线程的问题了。但也无伤大雅,毕竟不是纯粹按标记走,只要不让某个从库完全承担压力就好,偏离一些也无妨。

还有如果数据库出现问题,连接不上等,应该可以做到剔除无效dataSource。这里也没有做到。

该怎么做呢?

沉思良久,自觉才疏学浅,不敢提笔。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值