说明:
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。这里也没有做到。
该怎么做呢?
沉思良久,自觉才疏学浅,不敢提笔。