参考文档:
 
我们的需求达到的目标和现有的条件:
 
  • 不同类型数据源都可能存在master和slave区分;
  • 数据源之间已经可以通过package区分,不同package对应的service也不同;
  • aop在service层面,对应不同数据源的service之间可能存在互相调用;
  • 最外层方法的名称决定了该数据源应该使用master(可写)还是slave数据源(不可写);
  • 在嵌套使用其他service的过程中,根据情况分析该service方法是否使用slave数据源;
 
我们在spring中的配置文件中使用了切面式的配置来定义声明式事务:
 
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="save*" propagation="REQUIRED"/>
<tx:method name="add*" propagation="REQUIRED"/>
<tx:method name="create*" propagation="REQUIRED"/>
<tx:method name="insert*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="merge*" propagation="REQUIRED"/>
<tx:method name="del*" propagation="REQUIRED"/>
<tx:method name="remove*" propagation="REQUIRED"/>
<!--hibernate4必须配置为开启事务 否则 getCurrentSession()获取不到 -->
<tx:method name="get*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="count*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="list*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="*" propagation="SUPPORTS" read-only="true"/>
</tx:attributes>
</tx:advice> <!-- 只对业务逻辑层实施事务 -->
<aop:config expose-proxy="true">
<aop:pointcut id="txPointcut" expression="(execution(* com.api.example.*.*.service..*.*(..)))
or (execution(* com.api.example.*.service..*.*(..)))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>
 
在aop:config中,只对业务逻辑层实施事务管理,此时需要定义pointCut:用于确定实行动态织入用到的方法条件,和advisor:用于确定方法中使用到的事务管理器,事务管理器中定义了各种类型方法前缀所定义的事务范围和传播属性。
 
我们的数据源需要动态定义,需要在事务开启之前,切入数据源去选择该方法执行过程到底是该使用读库还是写库来开启该事务,因此这个切面需要在org.springframework.transaction.interceptor.TransactionInterceptor之前就要起作用。
 
 

实现方案

 
完成的整体类图如下:
 


 
 
从AbstractDataSource中继承,增加写库以及多个从库,以便于在spring配置文件中能够配置该数据源,由于需要支持多个数据源的master/slave主从库配置,所以MasterSlaveDataSourceDecision中不能简单地定义静态ThreadLocal变量来维持当前事务状态,而且每个TransactionManager都需要定义对应的数据源决策类。
 
public class MasterSlaveDataSource extends AbstractDataSource implements InitializingBean {

    private static final Logger log = LoggerFactory.getLogger(MasterSlaveDataSource.class);

    private DataSource masterDataSource;
private Map<String, DataSource> slaveDataSourceMap;
 
重写其中的getConnection()方法:
 
 
@Override
public Connection getConnection() throws SQLException {
return determineDataSource().getConnection();
} @Override
public Connection getConnection(String username, String password) throws SQLException {
return determineDataSource().getConnection(username, password);
}
通过determineDataSource()方法来决定使用写库还是从库,对于多个从库来说,可以采用其他算法来支持,也可以根据线程ID,让同一个线程能够使用同一从库(当前实现并没有这么做):
 
public DataSource determineDataSource() {
if (masterSlaveDataSourceDecision.isChoiceWrite()) {
log.debug("current determine write datasource");
return masterDataSource;
} else if (masterSlaveDataSourceDecision.isChoiceNone()) {
log.debug("no choice read/write, default determine write datasource");
return masterDataSource;
} else {
return selectReadDataSource();
}
}
 
定义切换数据源使用到的切面方法,当进行到需要启动事务的方法时,根据需要选择。我们一般会定义一个txAdvice,用于声明式事务的传播属性以及readonly属性,如果我们需要使用到该属性,需要利用spring的Bean加载完成通知,实现BeanPostProcessor接口中的postProcessAfterInitialization方法
 
public class MasterSlaveDataSourceProcessor implements BeanPostProcessor {

    private Map<String, Boolean> readWriteMethodMap = new HashMap<String, Boolean>();
private String txAdviceName;
private MasterSlaveDataSourceDecision masterSlaveDataSourceDecision; public void setTxAdviceName(String txAdviceName) {
this.txAdviceName = txAdviceName;
} public void setMasterSlaveDataSourceDecision(MasterSlaveDataSourceDecision masterSlaveDataSourceDecision) {
this.masterSlaveDataSourceDecision = masterSlaveDataSourceDecision;
} @Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (txAdviceName.equalsIgnoreCase(beanName)) {
try {
TransactionInterceptor transactionInterceptor = (TransactionInterceptor) bean;
NameMatchTransactionAttributeSource transactionAttributeSource =
(NameMatchTransactionAttributeSource) transactionInterceptor.getTransactionAttributeSource();
Field nameMapField = ReflectionUtils.findField(NameMatchTransactionAttributeSource.class, "nameMap");
nameMapField.setAccessible(true);
Map<String, TransactionAttribute> nameMap = (Map<String, TransactionAttribute>) nameMapField
.get(transactionAttributeSource); for (Entry<String, TransactionAttribute> entry : nameMap.entrySet()) {
RuleBasedTransactionAttribute attr = (RuleBasedTransactionAttribute) entry.getValue();
// 仅对read-only的处理
String methodName = entry.getKey();
if (attr.isReadOnly()) {
if (forceChoiceReadWhenWrite) {
// 不管之前操作是写,默认强制从读库读 (设置为NOT_SUPPORTED即可)
// NOT_SUPPORTED会挂起之前的事务
attr.setPropagationBehavior(Propagation.NOT_SUPPORTED.value());
} else {
// 否则 设置为SUPPORTS(这样可以参与到写事务)
attr.setPropagationBehavior(Propagation.SUPPORTS.value());
}
} log.info("read/write transaction process method:{} force read:{}", methodName,
forceChoiceReadWhenWrite);
readWriteMethodMap.put(methodName, attr.isReadOnly());
} } catch (Exception e) {
throw new ReadWriteDataSourceTransactionException("process read/write transaction error", e);
} }
return bean;
}
 
由于我们的环境中允许存在多个数据源的主从库设置,也存在多个事务管理器,当然也会有多个txAdvice,这里在其中设置一个属性txAdvice名称,每个不同数据源的MasterSlaveDataSourceProcessor监听不同的txAdvice。
 
我们将其中的tx method属性分成两种类型,一种为只读(从库),一种为写(写库),将其放置到对应的readWriteMethodMap中,
 
<tx:method name="save*" propagation="REQUIRED"/>
<tx:method name="add*" propagation="REQUIRED"/>
<tx:method name="create*" propagation="REQUIRED"/>
<tx:method name="insert*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="merge*" propagation="REQUIRED"/>
<tx:method name="del*" propagation="REQUIRED"/>
<tx:method name="remove*" propagation="REQUIRED"/>
<tx:method name="get*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="count*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="list*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="*" propagation="SUPPORTS" read-only="true"/>
 
根据切面的方法名称,以及刚才获得的readWriteMethodMap,来确定该方法是否可以读从库来减轻压力:
 

public Object selectDataSource(ProceedingJoinPoint pjp) throws Throwable {
if (isChoiceReadDB(pjp.getSignature().getName())) {
masterSlaveDataSourceDecision.markRead();
} else {
masterSlaveDataSourceDecision.markWrite();
}
try {
return pjp.proceed();
} catch (Throwable t) {
masterSlaveDataSourceDecision.reset();
throw t;
} finally {
masterSlaveDataSourceDecision.pop();
} }
spring可以通过内置的PatternMatchUtils工具类,来进行简单匹配工作,实现最长路径匹配,找到最合适的matchName(该代码是从NameMatchTransactionAttributeSource.getTransactionAttribute()中获得)。
 
private boolean isChoiceReadDB(String methodName) {

        String bestNameMatch = null;
for (String mappedName : this.readWriteMethodMap.keySet()) {
if (PatternMatchUtils.simpleMatch(mappedName, methodName)&&
(bestNameMatch == null || bestNameMatch.length() <= mappedName.length())) {
bestNameMatch = mappedName;
}
}
// 默认走写库
boolean currentRead = (bestNameMatch == null ? false : readWriteMethodMap.get(bestNameMatch));
// 如果当前为读库,并且设置了强制读,则忽略当前主库写状态
if (currentRead && forceChoiceReadWhenWrite) {
return true;
}
// 如果之前选择了写库,则当前使用写库
if (masterSlaveDataSourceDecision.isChoiceWrite()) {
return false;
}
return currentRead;
}
 
由于需要支持service方法之间的嵌套操作,MasterSlaveDataSourceDecision需要使用ThreadLocal<Stack>的方法保存当前上下文对应的数据源配置:
 
public class MasterSlaveDataSourceDecision {

    public enum DataSourceType {
write, read;
} private final ThreadLocal<Stack<DataSourceType>> holder = new ThreadLocal<Stack<DataSourceType>>() {
@Override
protected Stack<DataSourceType> initialValue() {
return new Stack<>();
}
}; public void markWrite() {
holder.get().push(DataSourceType.write);
} public void markRead() {
holder.get().push(DataSourceType.read);
} public void reset() {
holder.get().clear();
} public boolean isChoiceNone() {
return holder.get().isEmpty();
} public boolean isChoiceWrite() {
return !isChoiceNone() && DataSourceType.write == holder.get().peek();
} public boolean isChoiceRead() {
return !isChoiceNone() && DataSourceType.read == holder.get().peek();
} public DataSourceType pop() {
return isChoiceNone() ? null : holder.get().pop();
}
}
 

配置方法

 
数据源定义应该使用我们定义的主从DataSource,其中包含了写库以及从库的相关配置:
 
<bean id="masterSlaveDataSource" class="com.api.example.tx.MasterSlaveDataSource">
<property name="masterSlaveDataSourceDecision" ref="masterSlaveDataSourceDecision"/>
<property name="masterDataSource" ref="dataSource"/>
<property name="slaveDataSourceMap">
<map><entry key="slave1" value-ref="dataSourceSlave"/></map>
</property>
</bean>
 
定义transactionManager使用该数据源:
 
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="masterSlaveDataSource"/>
</bean>
定义txAdvice,关联该transactionManager,并定义事务的传播属性,以及readOnly属性
 
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="save*" propagation="REQUIRED"/>
<tx:method name="add*" propagation="REQUIRED"/>
<tx:method name="create*" propagation="REQUIRED"/>
<tx:method name="insert*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="merge*" propagation="REQUIRED"/>
<tx:method name="del*" propagation="REQUIRED"/>
<tx:method name="remove*" propagation="REQUIRED"/>
<!--hibernate4必须配置为开启事务 否则 getCurrentSession()获取不到 -->
<tx:method name="get*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="count*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="list*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="*" propagation="SUPPORTS" read-only="true"/>
</tx:attributes>
</tx:advice>
 
定义对应的aop config,使用aop:aspect,注意要设置order,保证该interceptor在事务启动之前能够选择到对应的dataSource,
 
    <!-- 只对业务逻辑层实施事务 -->
<aop:config expose-proxy="true">
<aop:pointcut id="txPointcut" expression="execution(* com.api.example.service..*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/> <aop:aspect order="-1" ref="masterSlaveDataSourceProcessor">
<aop:around method="selectDataSource" pointcut-ref="txPointcut"/>
</aop:aspect>
</aop:config>
 
加入Processor,以及decision,注意DataSourceProcessor需要关联对应的decision类,以及txAdvice名称(其id名称),以保证Processor会在对应的txAdvice加载完成后使用其定义的txAttributes属性信息,用于判断该事务的方法是否读主库,或从库。
 
    <bean id="masterSlaveDataSourceProcessor" class="com.api.example.tx.MasterSlaveDataSourceProcessor">
<property name="txAdviceName" value="txAdvice"/>
<property name="masterSlaveDataSourceDecision" ref="masterSlaveDataSourceDecision"/>
</bean> <bean id="masterSlaveDataSourceDecision" class="com.api.example.tx.MasterSlaveDataSourceDecision"/>
 
此外com.api.example.tx.MasterSlaveDataSourceProcessor中forceChoiceReadWhenWrite属性用于控制该执行该方法时,是否需要强制读操作(如果存在嵌套事务,将当前事务挂起)。
 
经过测试,可以满足我们的需求,达到根据service包中方法名称动态切换主从数据源的目的。
 
 
 
 
 
 

Spring主从数据源动态切换的更多相关文章

  1. Spring多数据源动态切换

    title: Spring多数据源动态切换 date: 2019-11-27 categories: Java Spring tags: 数据源 typora-root-url: ...... --- ...

  2. spring 多数据源动态切换

    理解spring动态切换数据源,需要对spring具有一定的了解 工作中经常遇到读写分离,数据源切换的问题,那么以下是本作者实际工作中编写的代码  与大家分享一下! 1.定义注解 DataSource ...

  3. Spring Boot 如何动态切换数据源

    本章是一个完整的 Spring Boot 动态数据源切换示例,例如主数据库使用 lionsea 从数据库 lionsea_slave1.lionsea_slave2.只需要在对应的代码上使用 Data ...

  4. 实战:Spring AOP实现多数据源动态切换

    需求背景 去年底,公司项目有一个需求中有个接口需要用到平台.算法.大数据等三个不同数据库的数据进行计算.组装以及最后的展示,当时这个需求是另一个老同事在做,我只是负责自己的部分. 直到今年回来了,这个 ...

  5. Spring3.3 整合 Hibernate3、MyBatis3.2 配置多数据源/动态切换数据源 方法

    一.开篇 这里整合分别采用了Hibernate和MyBatis两大持久层框架,Hibernate主要完成增删改功能和一些单一的对象查询功能,MyBatis主要负责查询功能.所以在出来数据库方言的时候基 ...

  6. Springboot多数据源配置--数据源动态切换

    在上一篇我们介绍了多数据源,但是我们会发现在实际中我们很少直接获取数据源对象进行操作,我们常用的是jdbcTemplate或者是jpa进行操作数据库.那么这一节我们将要介绍怎么进行多数据源动态切换.添 ...

  7. springboot多数据源动态切换和自定义mybatis分页插件

    1.配置多数据源 增加druid依赖 完整pom文件 数据源配置文件 route.datasource.driver-class-name= com.mysql.jdbc.Driver route.d ...

  8. Spring3.3 整合 Hibernate3、MyBatis3.2 配置多数据源/动态切换数据源方法

    一.开篇 这里整合分别采用了Hibernate和MyBatis两大持久层框架,Hibernate主要完成增删改功能和一些单一的对象查询功能,MyBatis主要负责查询功能.所以在出来数据库方言的时候基 ...

  9. mybatis 多数据源动态切换

    笔者主要从事c#开发,近期因为项目需要,搭建了一套spring-cloud微服务框架,集成了eureka服务注册中心. gateway网关过滤.admin服务监控.auth授权体系验证,集成了redi ...

随机推荐

  1. Recover Binary Search Tree,恢复二叉排序树

    问题描述:题意就是二叉树中有两个节点交换了,恢复结构. Two elements of a binary search tree (BST) are swapped by mistake. Recov ...

  2. 史上最强大的40多个纯CSS绘制的图形[转]

    今天在国外的网站上看到了很多看似简单却又非常强大的纯CSS绘制的图形,里面有最简单的矩形.圆形和三角形,也有各种常见的多边形,甚至是阴阳太极和网站小图标,真的非常强大,分享给大家. Square(正方 ...

  3. spark udf 初识初用

    直接上代码,详见注释 import org.apache.spark.sql.hive.HiveContext import org.apache.spark.{SparkContext, Spark ...

  4. window下rabbitmq环境安装

    最近项目想用个MQ来做业务分离,看了市面上众多产品,最后选了rabbitmq,理由很简单,对window的支持很到位(其实是公司的系列产品都是.net的). 安装方法什么的就不说了,直接到官网下载双击 ...

  5. PHP--------微信网页开发实现微信扫码功能

    今天说说微商城项目中用到的扫一扫这个功能,分享一下,希望对各位有所帮助. 前提:要有公众号,和通过微信认证,绑定域名,得到相应信息,appid,appsecret等. 微信开发文档:https://m ...

  6. 部分函数依赖 && 完全函数依赖

    部分函数依赖:若x->y 并且,存在X的真子集x1,使得x1->y,则 y部分依赖于x. 完全函数依赖:若x->y并且,对于x的任何一个真子集x1,都不存在x1->y,则称y完 ...

  7. 005——php字符串中的处理函数(四)

    <?php /** * 字符串处理函数: * parse_url 解析URL.返回其组成部分 */ /* $url="http://www.lantianwang.com/admin/ ...

  8. windows下使用python的scrapy爬虫框架,爬取个人博客文章内容信息

    scrapy作为流行的python爬虫框架,简单易用,这里简单介绍如何使用该爬虫框架爬取个人博客信息.关于python的安装和scrapy的安装配置请读者自行查阅相关资料,或者也可以关注我后续的内容. ...

  9. [置顶] VS 2017 众多重构插件

    孙广东  2017.7.22 http://blog.csdn.NET/u010019717 1.没有任何插件的情况下:  (就是Ctrl + .)   注意:这个.  要是英文的才行! 右键菜单也是 ...

  10. [爬虫] 学Scrapy,顺便把它的官方教程给爬下来

    想学爬虫主要是因为算法和数据是密切相关的,有数据之后可以玩更多有意思的事情,数据量大可以挖掘挖掘到更多的信息. 之前只会通过python中的request库来下载网页内容,再用BeautifulSou ...