前言

之前使用的读写分离的方案是在mybatis中配置两个数据源,然后生成两个不同的SqlSessionTemplate然后手动去识别执行sql语句是操作主库还是从库。如下图所示:

好处是,你可以人为的去控制操作的数据库。缺点也显而易见,就是代码非常麻烦,总是需要去判断使用什么库,而且遇到事务的时候还必须特别小心。

这次我们利用spring抽象路由数据源+MyBatis拦截器来实现自动的读写分离,并且保证在使用事务的情况下也能正确。结构如下图所示

我们还是按照老套路,首先我会直接进行代码的实现,然后根据源码进行分析,最后做一个总结。

代码实现

我们一共需要5个类和两个配置文件

首先来说类

/**
* 全局动态数据源实体
* @author LinkinStar
*
*/
public enum DynamicDataSourceGlobal {
READ, WRITE;
}

这是一个枚举的实体,后面会用到

/**
* 动态数据源线程持有者
* @author LinkinStar
*
*/
public final class DynamicDataSourceHolder { private static final ThreadLocal<DynamicDataSourceGlobal> holder = new ThreadLocal<DynamicDataSourceGlobal>(); /**
* 设置当前线程使用的数据源
*/
public static void putDataSource(DynamicDataSourceGlobal dataSource){
holder.set(dataSource);
} /**
* 获取当前线程需要使用的数据源
*/
public static DynamicDataSourceGlobal getDataSource(){
return holder.get();
} /**
* 清空使用的数据源
*/
public static void clearDataSource() {
holder.remove();
} }

以上是两个工具,下面就是重点了

一个是我们的主角,动态数据源,它继承自spring的抽象动态路由数据源

/**
* 动态数据源(继承自spring抽象动态路由数据源)
* @author LinkinStar
*
*/
public class DynamicDataSource extends AbstractRoutingDataSource { private Object writeDataSource; //写数据源 private Object readDataSource; //读数据源 /**
* 在初始化之前被调用,设置默认数据源,以及数据源资源(这里的写法是参考源码中的)
*/
@Override
public void afterPropertiesSet() {
//如果写数据源不存在,则抛出非法异常
if (this.writeDataSource == null) {
throw new IllegalArgumentException("Property 'writeDataSource' is required");
}
//设置默认目标数据源为主库
setDefaultTargetDataSource(writeDataSource);
//设置所有数据源资源,有从库添加,没有就添加
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DynamicDataSourceGlobal.WRITE.name(), writeDataSource);
if(readDataSource != null) {
targetDataSources.put(DynamicDataSourceGlobal.READ.name(), readDataSource);
}
setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
} /**
* 这是AbstractRoutingDataSource类中的一个抽象方法,而它的返回值是你所要用的数据源dataSource的key值
*/
@Override
protected Object determineCurrentLookupKey() {
//根据当前线程所使用的数据源进行切换
DynamicDataSourceGlobal dynamicDataSourceGlobal = DynamicDataSourceHolder.getDataSource(); //如果没有被赋值,那么默认使用主库
if(dynamicDataSourceGlobal == null
|| dynamicDataSourceGlobal == DynamicDataSourceGlobal.WRITE) {
return DynamicDataSourceGlobal.WRITE.name();
}
//其他情况使用从库
return DynamicDataSourceGlobal.READ.name();
} public void setWriteDataSource(Object writeDataSource) {
this.writeDataSource = writeDataSource;
} public Object getWriteDataSource() {
return writeDataSource;
} public Object getReadDataSource() {
return readDataSource;
} public void setReadDataSource(Object readDataSource) {
this.readDataSource = readDataSource;
}
}

然后是我们的另一个主角,动态数据源插件,实现MyBatis拦截器接口

import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap; import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.transaction.support.TransactionSynchronizationManager; /**
* 动态数据源插件,实现MyBatis拦截器接口
* @author LinkinStar
*
*/
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {
MappedStatement.class, Object.class }),
@Signature(type = Executor.class, method = "query", args = {
MappedStatement.class, Object.class, RowBounds.class,
ResultHandler.class }) })
public class DynamicPlugin implements Interceptor { /**
* 匹配SQL语句的正则表达式
*/
private static final String REGEX = ".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*"; /**
* 这个map用于存放已经执行过的sql语句所对应的数据源
*/
private static final Map<String, DynamicDataSourceGlobal> cacheMap = new ConcurrentHashMap<>(); @Override
public Object intercept(Invocation invocation) throws Throwable {
//获取当前事务同步性进行判断
boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();
//如果当前正在使用事务,则使用默认的库
if (synchronizationActive) {
return invocation.proceed();
} //从代理类参数中获取参数
Object[] objects = invocation.getArgs();
//其中参数的第一个值为执行的sql语句
MappedStatement ms = (MappedStatement) objects[0]; //当前sql语句所应该使用的数据源,通过sql语句的id从map中获取,如果获取到,则之前已经执行过直接取,
DynamicDataSourceGlobal dynamicDataSourceGlobal = cacheMap.get(ms.getId());
if (dynamicDataSourceGlobal != null) {
DynamicDataSourceHolder.putDataSource(dynamicDataSourceGlobal);
return invocation.proceed();
} //如果没有,则重新进行存放
//ms中获取方法,如果是查询方法
if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
//!selectKey 为自增id查询主键(SELECT LAST_INSERT_ID() )方法,使用主库
if(ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) {
dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE;
} else {
BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]);
//通过正则表达式匹配,确定使用那一个数据源
String sql = boundSql.getSql().toLowerCase(Locale.CHINA).replaceAll("[\\t\\n\\r]", " ");
if(sql.matches(REGEX)) {
dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE;
} else {
dynamicDataSourceGlobal = DynamicDataSourceGlobal.READ;
}
}
} else {
dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE;
}
//将sql对应使用的数据源放进map中存放
cacheMap.put(ms.getId(), dynamicDataSourceGlobal); //最后设置使用的数据源
DynamicDataSourceHolder.putDataSource(dynamicDataSourceGlobal); //执行代理之后的方法
return invocation.proceed();
} @Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
} else {
return target;
}
} @Override
public void setProperties(Properties properties) {
}
}

最后是我们的配角,动态数据源的事务管理器

import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition; /**
* 动态数据源事务管理器
* @author LinkinStar
*
*/
public class DynamicDataSourceTransactionManager extends DataSourceTransactionManager { private static final long serialVersionUID = 1L; /**
* 只读事务到读库,读写事务到写库
*/
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
//根据事务可读性进行判断
boolean readOnly = definition.isReadOnly();
//只读类型事务可以只用从库
if(readOnly) {
DynamicDataSourceHolder.putDataSource(DynamicDataSourceGlobal.READ);
} else {
DynamicDataSourceHolder.putDataSource(DynamicDataSourceGlobal.WRITE);
}
super.doBegin(transaction, definition);
} /**
* 清理本地线程的数据源(会被默认调用,调用时清除相应数据源)
*/
@Override
protected void doCleanupAfterCompletion(Object transaction) {
super.doCleanupAfterCompletion(transaction);
DynamicDataSourceHolder.clearDataSource();
}
}

然后是两个配置文件,根据你自己的需要进行修改

<?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:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd"> <context:property-placeholder location="classpath:resources/jdbc.properties"/> <bean id="abstractDataSource" abstract="true" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}" />
<property name="minIdle" value="${jdbc.minIdle}"></property>
<property name="maxIdle" value="${jdbc.maxIdle}"></property>
<property name="maxWait" value="${jdbc.maxWait}"></property>
<property name="maxActive" value="${jdbc.maxActive}"></property>
<property name="initialSize" value="${jdbc.initialSize}"></property>
<property name="testWhileIdle"><value>true</value></property>
<property name="testOnBorrow"><value>true</value></property>
<property name="testOnReturn"><value>false</value></property>
<property name="validationQuery"><value>SELECT 1 FROM DUAL</value></property>
<property name="validationQueryTimeout"><value>1</value></property>
<property name="timeBetweenEvictionRunsMillis"><value>3000</value></property>
<property name="numTestsPerEvictionRun"><value>2</value></property>
</bean> <bean id="dataSourceRead" parent="abstractDataSource">
<property name="url" value="${jdbc.url.read}" />
<property name="username" value="${jdbc.username.read}"/>
<property name="password" value="${jdbc.password.read}"/>
</bean> <bean id="dataSourceWrite" parent="abstractDataSource">
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean> <bean id="dataSource" class="com.ssm.dao.data.DynamicDataSource">
<property name="writeDataSource" ref="dataSourceWrite"></property>
<property name="readDataSource" ref="dataSourceRead"></property>
</bean> <!--配置基于注解的声明式事务,默认使用注解来管理事务行为-->
<tx:annotation-driven transaction-manager="transactionManager"/> <!--配置事务管理器(mybatis采用的是JDBC的事务管理器)-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--注入数据库连接池-->
<property name="dataSource" ref="dataSource" />
<!--扫描entity包,使用别名,多个用;隔开-->
<property name="typeAliasesPackage" value="com/ssm/entity" />
<!--扫描sql配置文件:mapper需要的xml文件-->
<property name="mapperLocations" value="classpath*:com/ssm/dao/sqlxml/*.xml"></property>
<property name="plugins">
<array>
<bean class="com.ssm.dao.data.DynamicPlugin" />
</array>
</property>
</bean> <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean> <!--配置扫描Dao接口包,动态实现DAO接口,注入到spring容器-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--注入SqlSessionFactory-->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<!-- 给出需要扫描的Dao接口-->
<property name="basePackage" value="com.ssm.dao"/>
</bean> </beans>

另外就是jdbc的配置文件,也需要根据自己进行修改,这边使用两个

jdbc.driverClassName=com.mysql.jdbc.Driver

jdbc.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8
jdbc.username=root
jdbc.password=123456 jdbc.url.read=jdbc:mysql://localhost:3306/xxx?useUnicode=true&characterEncoding=UTF-8
jdbc.username.read=root
jdbc.password.read=123456 jdbc.maxActive = 2
jdbc.maxIdle =5
jdbc.minIdle=1
jdbc.initialSize =3
jdbc.maxWait =3000

至此所有的配置都已经完成,现在你已经可以进行测试,看看在查询和新增的时候是否使用的是不同的数据库。

看看在使用事务的情况下,是否使用相同的数据库。

实现分析

首先我们来分析两个主角

动态数据源(继承自spring抽象动态路由数据源)

先看一下源码中父类的说明

/**
* Abstract {@link javax.sql.DataSource} implementation that routes {@link #getConnection()}
* calls to one of various target DataSources based on a lookup key. The latter is usually
* (but not necessarily) determined through some thread-bound transaction context.
*
* @author Juergen Hoeller
* @since 2.0.1
* @see #setTargetDataSources
* @see #setDefaultTargetDataSource
* @see #determineCurrentLookupKey()
*/
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

我们写的这个类中重写了父类两个重要的方法

1、afterPropertiesSet

首先源码中是这样的:

@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = new HashMap<Object, DataSource>(this.targetDataSources.size());
for (Map.Entry<Object, Object> entry : this.targetDataSources.entrySet()) {
Object lookupKey = resolveSpecifiedLookupKey(entry.getKey());
DataSource dataSource = resolveSpecifiedDataSource(entry.getValue());
this.resolvedDataSources.put(lookupKey, dataSource);
}
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}

而我们重写的目的就是为了设置默认我们的主库和从库

2、determineCurrentLookupKey

这是AbstractRoutingDataSource类中的一个抽象方法,而它的返回值是你所要用的数据源dataSource的key值

在这个方法中我们通过DynamicDataSourceHolder获取当前线程所应该使用的数据源,然后将数据源的名字返回。也就是dataSource的key值。

然后是下一个主角,动态数据源插件,实现MyBatis拦截器接口,这个类一共干了下面几个事情

(当我们实现了MyBatis拦截器接口之后就能在数据库执行sql之前做操作,具体请参考别的博客,这里不细说)

1、通过当前是否使用事务确定数据源,如果使用事务,那么默认使用主库

2、从sql语句中获取sql执行的类型,根据具体的类型确定使用的数据源

3、利用cacheMap缓存已经进行判断过的sql和对应执行时使用的数据源

4、通过DynamicDataSourceHolder保存当前线程所需要使用的数据源

最后一个是动态数据源事务管理器

这个类主要是保证,当一些事务是只读类型的事务时,使用的数据源是从库。

然后保存到DynamicDataSourceHolder中

总结

1、使用此种方式实现数据库读写分离,对于代码来说不会对现有代码造成影响,没有入侵性,容易剥离和加入。

2、对于事务使用同一个数据库能保证读写的一致性。

3、不需要人为去判断使用哪一个数据库,不用担心会出现人物问题。

4、扩展性上面,当有多个从库的时候,不要想着配置多个从库数据源解决问题,而是应该配置数据库负载均衡然后实现多个从数据库的访问。

参考博客:http://www.jianshu.com/p/2222257f96d3

http://blog.csdn.net/x2145637/article/details/52461198

通过spring抽象路由数据源+MyBatis拦截器实现数据库自动读写分离的更多相关文章

  1. 基于Spring和Mybatis拦截器实现数据库操作读写分离

    首先需要配置好数据库的主从同步: 上一篇文章中有写到:https://www.cnblogs.com/xuyiqing/p/10647133.html 为什么要进行读写分离呢? 通常的Web应用大多数 ...

  2. 玩转SpringBoot之整合Mybatis拦截器对数据库水平分表

    利用Mybatis拦截器对数据库水平分表 需求描述 当数据量比较多时,放在一个表中的时候会影响查询效率:或者数据的时效性只是当月有效的时候:这时我们就会涉及到数据库的分表操作了.当然,你也可以使用比较 ...

  3. spring boot 实现mybatis拦截器

    spring boot 实现mybatis拦截器 项目是个报表系统,服务端是简单的Java web架构,直接在请求参数里面加了个query id参数,就是mybatis mapper的query id ...

  4. 解决mybatis拦截器无法注入spring bean的问题

    公司要整合rabbitmq与mybatis拦截器做一个数据同步功能. 整合过程中大部分环节都没什么问题,就是遇到了mybatis拦截器 @Intercepts(@Signature(type = Ex ...

  5. Mybatis拦截器实现分页

    本文介绍使用Mybatis拦截器,实现分页:并且在dao层,直接返回自定义的分页对象. 最终dao层结果: public interface ModelMapper { Page<Model&g ...

  6. mybatis拦截器使用

    目录 mybatis 拦截器接口Interceptor spring boot + mybatis整合 创建自己的拦截器MyInterceptor @Intercepts注解 mybatis拦截器入门 ...

  7. Mybatis拦截器执行过程解析

    上一篇文章 Mybatis拦截器之数据加密解密 介绍了 Mybatis 拦截器的简单使用,这篇文章将透彻的分析 Mybatis 是怎样发现拦截器以及调用拦截器的 intercept 方法的 小伙伴先按 ...

  8. MyBatis拦截器自定义分页插件实现

    MyBaits是一个开源的优秀的持久层框架,SQL语句与代码分离,面向配置的编程,良好支持复杂数据映射,动态SQL;MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyB ...

  9. Mybatis拦截器实现原理深度分析

    1.拦截器简介 拦截器可以说使我们平时开发经常用到的技术了,Spring AOP.Mybatis自定义插件原理都是基于拦截器实现的,而拦截器又是以动态代理为基础实现的,每个框架对拦截器的实现不完全相同 ...

随机推荐

  1. windows 命令行出现中文乱码

    1.打开CMD.exe命令行窗口 2.通过 chcp命令改变代码页chcp 65001  //UTF-8的代码页为65001

  2. boost的下载和安装(windows版)

    1 简介 boost是一个准C++标准库,相当于STL的延续和扩充,它的设计理念和STL比较接近,都是利用泛型让复用达到最大化. boost主要包含以下几个大类: 字符串及文本处理.容器.迭代器(it ...

  3. 利用maven将项目打包成一个可以运行的独立jar包

    目标:希望把Java项目打包成一个完整的jar包,可以独立运行,不需要再依赖其他jar包. 我们在用eclipse中mvn创建mvn项目的时候,选择非webapp,会默认的以jar打包形式,如下图: ...

  4. 51nod1305

    可以暴力,但这里学习了一个新思路,就是把原式进行分解会得到[1/a[i]+1/a[j]],因为向下取整,我们可以发现,1作用于1结果为2,1作用于除了1之外的数结果为1,2作用于2结果为1,所以我们只 ...

  5. 判断jquery对象是否具有某指定属性或者方法的几种方法

    1.typeof 运算符:返回一个用来表示表达式的数据类型的字符串.("number", "string", "boolean", &quo ...

  6. mysql 在原有的时间上加10个月或者一年

    UPDATE SERVER_TIME_LEFT SET END_TIME = DATE_ADD(END_TIME, INTERVAL 10 MONTH) WHERE SHOP_ID BETWEEN 1 ...

  7. css概括2

    Css内容: 常用样式:字体.颜色.背景... 字体:大小.颜色.粗细.字体 Text-decoration:文本修饰{overline 上 Underline 下 Line-throung 中} T ...

  8. 端口转发工具lcx使用两类

    lcx是一款强大的内网端口转发工具,用于将内网主机开放的内部端口映射到外网主机(有公网IP)任意端口.它是一款命令行工具,当然也可以在有权限的webshell下执行,正因如此lcx常被认为是一款黑客入 ...

  9. Vuejs——(8)Vuejs组件的定义

    版权声明:出处http://blog.csdn.net/qq20004604   目录(?)[+]   本篇资料来于官方文档: http://cn.vuejs.org/guide/components ...

  10. 微服务日志之.NET Core使用NLog通过Kafka实现日志收集

    一.前言 NET Core越来越受欢迎,因为它具有在多个平台上运行的原始.NET Framework的强大功能.Kafka正迅速成为软件行业的标准消息传递技术.这篇文章简单介绍了如何使用.NET(Co ...