基于自定义注解和Aop动态数据源配置

基于自定义注解和Aop动态数据源配置

        在实际项目中,经常会因为需要增强数据库并发能力而设计分库分表或者读写分离等策略,每在旧项目中引进新技术的时候都会带来一系列的问题,我们的目的就是去解决问题,带着思考方式去重构系统,从中找到乐趣,对应引进自定义注解和Aop动态数据源配置技术带来的问题,我会在文章末尾介绍,也希望大神给予正确的引导,我们当时的需求就是:有一个XXX旧系统,我们在这个旧系统的基础上开发一个PC端的程序用于收银;对方提供他们的数据库文档和对接人员,旧系统代码他们不给,我们只能通过沟通去了解他们旧系统的设计思路,带着一万个艹尼玛去写代码了;我们属于二次开发,需要在旧系统的数据库基础上开发自己的业务数据库,到这里就设计到二个数据库了(一个是旧系统的数据库,一个收银系统的数据库),项目之前能想到得就是自定义注解和Aop动态数据源配置来实现,但存在坑,下面我会提出坑点;现在就让我们先从配置(本文是基于SSM框架下集成的动态数据源切换):

1.     配置pom.xml,使用的是阿里巴巴数据源包和Mysql 5.1.30的驱动

<!-- 阿里巴巴数据源包 -->
<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>druid</artifactId>
	<version>1.0.2</version>
</dependency>
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<version>5.1.30</version>
</dependency>

2.     spring-dispatcher.xml 核心配置如下:

<context:property-placeholder location="classpath:config.properties" />

<!-- 阿里 druid数据库连接池 -->
<bean id="masterDataSource" class="com.alibaba.druid.pool.DruidDataSource"
	destroy-method="close">
	<!-- 数据库基本信息配置 -->
	<property name="url" value="${jdbc.url}" />
	<property name="username" value="${jdbc.username}" />
	<property name="password" value="${jdbc.password}" />
	<property name="driverClassName" value="${jdbc.driverClassName}" />
	<property name="filters" value="${jdbc.filters}" />
	<!-- 最大并发连接数 -->
	<property name="maxActive" value="${jdbc.maxActive}" />
	<!-- 初始化连接数量 -->
	<property name="initialSize" value="${jdbc.initialSize}" />
	<!-- 配置获取连接等待超时的时间 -->
	<property name="maxWait" value="${jdbc.maxWait}" />
	<!-- 最小空闲连接数 -->
	<property name="minIdle" value="${jdbc.minIdle}" />
	<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
	<property name="timeBetweenEvictionRunsMillis" value="${jdbc.timeBetweenEvictionRunsMillis}" />
	<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
	<property name="minEvictableIdleTimeMillis" value="${jdbc.minEvictableIdleTimeMillis}" />
	<property name="validationQuery" value="${jdbc.validationQuery}" />
	<property name="testWhileIdle" value="${jdbc.testWhileIdle}" />
	<property name="testOnBorrow" value="${jdbc.testOnBorrow}" />
	<property name="testOnReturn" value="${jdbc.testOnReturn}" />
	<property name="maxOpenPreparedStatements" value="${jdbc.maxOpenPreparedStatements}" />
	<!-- 打开removeAbandoned功能 -->
	<property name="removeAbandoned" value="${jdbc.removeAbandoned}" />
	<!-- 1800秒,也就是30分钟 -->
	<property name="removeAbandonedTimeout" value="${jdbc.removeAbandonedTimeout}" />
	<!-- 关闭abanded连接时输出错误日志 -->
	<property name="logAbandoned" value="${jdbc.logAbandoned}" />
</bean>

<!-- 阿里 druid数据库连接池 -->
<bean id="slaveDataSource" class="com.alibaba.druid.pool.DruidDataSource"
	destroy-method="close">
	<!-- 数据库基本信息配置 -->
	<property name="url" value="${jdbc.slave.url}" />
	<property name="username" value="${jdbc.slave.username}" />
	<property name="password" value="${jdbc.slave.password}" />
	<property name="driverClassName" value="${jdbc.slave.driverClassName}" />
	<property name="filters" value="${jdbc.filters}" />
	<!-- 最大并发连接数 -->
	<property name="maxActive" value="${jdbc.maxActive}" />
	<!-- 初始化连接数量 -->
	<property name="initialSize" value="${jdbc.initialSize}" />
	<!-- 配置获取连接等待超时的时间 -->
	<property name="maxWait" value="${jdbc.maxWait}" />
	<!-- 最小空闲连接数 -->
	<property name="minIdle" value="${jdbc.minIdle}" />
	<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
	<property name="timeBetweenEvictionRunsMillis" value="${jdbc.timeBetweenEvictionRunsMillis}" />
	<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
	<property name="minEvictableIdleTimeMillis" value="${jdbc.minEvictableIdleTimeMillis}" />
	<property name="validationQuery" value="${jdbc.validationQuery}" />
	<property name="testWhileIdle" value="${jdbc.testWhileIdle}" />
	<property name="testOnBorrow" value="${jdbc.testOnBorrow}" />
	<property name="testOnReturn" value="${jdbc.testOnReturn}" />
	<property name="maxOpenPreparedStatements" value="${jdbc.maxOpenPreparedStatements}" />
	<!-- 打开removeAbandoned功能 -->
	<property name="removeAbandoned" value="${jdbc.removeAbandoned}" />
	<!-- 1800秒,也就是30分钟 -->
	<property name="removeAbandonedTimeout" value="${jdbc.removeAbandonedTimeout}" />
	<!-- 关闭abanded连接时输出错误日志 -->
	<property name="logAbandoned" value="${jdbc.logAbandoned}" />
</bean>

<bean id="dynamicDataSource" class="cn.edu.his.pay.dynamic.datasource.DynamicDataSource">
	<property name="targetDataSources">
		<map key-type="java.lang.String">
			<!-- 指定lookupKey和与之对应的数据源 -->
			<entry key="MASTER" value-ref="masterDataSource"></entry>
			<entry key="SLAVE" value-ref="slaveDataSource"></entry>
		</map>
	</property>
	<!-- 这里可以指定默认的数据源 -->
	<property name="defaultTargetDataSource" ref="masterDataSource" />
</bean>

<!-- mybatis文件配置,扫描所有mapper*xml.文件 -->
<bean id="sessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
	<property name="dataSource" ref="dynamicDataSource" />
	<property name="typeAliasesPackage" value="cn.edu.his.pay.model.entity" />
	<property name="mapperLocations" value="classpath:mybatis/xml/*Mapper.xml" />
	<property name="plugins">
		<array>
			<bean class="com.github.pagehelper.PageHelper">
				<property name="properties">
					<value>
						dialect=mysql
						reasonable=true
					</value>
				</property>
			</bean>
		</array>
	</property>
</bean>

<!-- spring与mybatis整合配置,扫描所有mapper -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
	<property name="basePackage" value="cn.edu.his.pay.mapper" />
	<property name="sqlSessionFactoryBeanName" value="sessionFactory" />
</bean>

<!-- 对数据源进行事务管理 -->
<bean id="transactionManager"
	class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
	<property name="dataSource" ref="dynamicDataSource" />
</bean>

<!-- 配置哪些方法要加入事务控制 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
	<tx:attributes>
		<!-- 让所有的方法都加入事务管理,为了提高效率,可以把一些查询之类的方法设置为只读的事务 -->
		<tx:method name="*" propagation="REQUIRED" read-only="true" />
		<!-- 以下方法都是可能设计修改的方法,就无法设置为只读 -->
		<tx:method name="add*" propagation="REQUIRED" />
		<tx:method name="insert*" propagation="REQUIRED" />
		<tx:method name="del*" propagation="REQUIRED" />
		<tx:method name="update*" propagation="REQUIRED" />
		<tx:method name="save*" propagation="REQUIRED" />
		<tx:method name="clear*" propagation="REQUIRED" />
		<tx:method name="handle*" propagation="REQUIRED" />
	</tx:attributes>
</tx:advice>

<!-- 配置AOP,Spring是通过AOP来进行事务管理的 -->
<aop:config>
	<!-- 设置pointCut表示哪些方法要加入事务处理 -->
	<!-- 以下的事务是声明在DAO中,但是通常都会在Service来处理多个业务对象逻辑的关系,注入删除,更新等,此时如果在执行了一个步骤之后抛出异常 
		就会导致数据不完整,所以事务不应该在DAO层处理,而应该在service,这也就是Spring所提供的一个非常方便的工具,声明式事务 -->
	<aop:pointcut id="allMethods" expression="(execution(* cn.edu.his.pay.service.*.*(..)))" />
	<!-- 通过advisor来确定具体要加入事务控制的方法 -->
	<aop:advisor advice-ref="txAdvice" pointcut-ref="allMethods" />
</aop:config>

3.     spring-dispatcher.xml依赖的config.properties配置文件如下:

# =====================数据源切换数据master和slave数据库=====================
# master 也是默认的数据源(默认为旧系统的:原因是他们的表比较多)
jdbc.url=jdbc:mysql://127.0.0.1:3306/his?useUnicode=true&characterEncoding=utf8
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.username=root
jdbc.password=root2

# slave 需要切换的数据源(slave,原因是我们的表比较少)
jdbc.slave.url=jdbc:mysql://127.0.0.1:3306/his_pay?useUnicode=true&characterEncoding=utf8
jdbc.slave.driverClassName=com.mysql.jdbc.Driver
jdbc.slave.username=root
jdbc.slave.password=root
# =====================数据源切换数据master和slave数据库=====================

jdbc.filters=stat
   
jdbc.maxActive=20
jdbc.initialSize=1
jdbc.maxWait=60000
jdbc.minIdle=10
jdbc.maxIdle=15
   
jdbc.timeBetweenEvictionRunsMillis=60000
jdbc.minEvictableIdleTimeMillis=300000
   
jdbc.validationQuery=SELECT 'x'
jdbc.testWhileIdle=true
jdbc.testOnBorrow=false
jdbc.testOnReturn=false

jdbc.maxOpenPreparedStatements=20
jdbc.removeAbandoned=true
jdbc.removeAbandonedTimeout=1800
jdbc.logAbandoned=true

4.     和controller包同目录dynamic.datasource包下有如下几个类:

 DataSource.java(自定义的注解),DataSourceAspect.java(Aop切面),DataSourceType.java(枚举:用于指定是数据源名),DynamicDataSource.java,DynamicDataSourceHolder.java。

5.      DataSource.java 如下:

package cn.edu.his.pay.dynamic.datasource;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/*
@Target(ElementType.TYPE) //接口、类、枚举、注解 
@Target(ElementType.FIELD) //字段、枚举的常量 
@Target(ElementType.METHOD) //方法 
@Target(ElementType.PARAMETER) //方法参数 
@Target(ElementType.CONSTRUCTOR) //构造函数 
@Target(ElementType.LOCAL_VARIABLE)//局部变量 
@Target(ElementType.ANNOTATION_TYPE)//注解 
@Target(ElementType.PACKAGE) ///包 

@Retention(RetentionPolicy.SOURCE) //注解仅存在于源码中,在class字节码文件中不包含 
@Retention(RetentionPolicy.CLASS) //默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得, 
@Retention(RetentionPolicy.RUNTIME)//注解会在class字节码文件中存在,在运行时可以通过反射获取到 
*/
/**
 * @author 93287
 *
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
	DataSourceType value();
}

6.      DataSourceAspect.java 如下:

package cn.edu.his.pay.dynamic.datasource;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import cn.edu.his.pay.common.log.Logger;

@Aspect
@Order(-1)
// 保证该AOP在@Transactional之前执行
@Component
public class DataSourceAspect {

	private static final Logger LOG = new Logger(DataSourceAspect.class);

	@Before("@annotation(ds)")
	public void changeDataSource(JoinPoint point, DataSource ds) throws Throwable {
		LOG.debug("=======================SET START=======================");
		LOG.debug("Use DataSource : {} > {}", ds.value(), point.getSignature());
		DynamicDataSourceHolder.setDataSourceType(ds.value().name());
		LOG.debug("[annotation.set] datasource====》{}",ds.value().name());
		LOG.debug("=======================SET END=======================");
	}
		
	@After("@annotation(ds)")
	public void restoreDataSource(JoinPoint point, DataSource ds) {
		LOG.debug("=======================CLEAR START=======================");
		LOG.debug("Revert DataSource : {} > {}", ds.value().name(), point.getSignature());
		DynamicDataSourceHolder.clearDataSourceType();
		LOG.debug("[annotation.remove] datasource====》{}",ds.value().name());
		LOG.debug("=======================CLEAR END=======================");
	}

}

7.      DataSourceType.java 如下:

package cn.edu.his.pay.dynamic.datasource;

public enum DataSourceType {
	 MASTER, SLAVE
}

8.      DynamicDataSource.java 如下:

package cn.edu.his.pay.dynamic.datasource;

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


public class DynamicDataSource extends AbstractRoutingDataSource {

	@Override
	protected Object determineCurrentLookupKey() {
		return DynamicDataSourceHolder.getDataSourceType();
	}

}

9.      DynamicDataSourceHolder.java 如下:

package cn.edu.his.pay.dynamic.datasource;

import org.springframework.util.Assert;

import cn.edu.his.pay.common.log.Logger;

public class DynamicDataSourceHolder {
	
	private static final Logger LOG = new Logger(DynamicDataSourceHolder.class);
	
	// 线程本地环境
	  private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
	  
	  // 设置数据源类型
	  public static void setDataSourceType(String dataSourceType) {
	    Assert.notNull(dataSourceType, "DataSourceType cannot be null");
	    contextHolder.set(dataSourceType);
	    LOG.debug("[this.set] datasource====》{}",dataSourceType);
	  }
	 
	  // 获取数据源类型
	  public static String getDataSourceType() {
	    return contextHolder.get();
	  }
	 
	  // 清除数据源类型
	  public static void clearDataSourceType() {
		  LOG.debug("[this.remove] datasource====》{}",contextHolder.get());
	    contextHolder.remove();
	    
	  }
	  
}

9.     基本核心配置和核心代码已经如上了,那我们要怎么使用了,如spring-dispatcher.xml 配置中配置Aop的切点是service包下的所有方法。所以需要将数据源切换到Slave上就直接使用如下注解配置到方法对应的方法上就行,不配置注解默认走Master。

<!-- 配置AOP,Spring是通过AOP来进行事务管理的 -->
<aop:config>
	<!-- 设置pointCut表示哪些方法要加入事务处理 -->
	<!-- 以下的事务是声明在DAO中,但是通常都会在Service来处理多个业务对象逻辑的关系,注入删除,更新等,此时如果在执行了一个步骤之后抛出异常 
		就会导致数据不完整,所以事务不应该在DAO层处理,而应该在service,这也就是Spring所提供的一个非常方便的工具,声明式事务 -->
	<aop:pointcut id="allMethods" expression="(execution(* cn.edu.his.pay.service.*.*(..)))" />
	<!-- 通过advisor来确定具体要加入事务控制的方法 -->
	<aop:advisor advice-ref="txAdvice" pointcut-ref="allMethods" />
</aop:config>
@Override
@DataSource(value = DataSourceType.SLAVE)
public int insert(Admin record) {
	return adminMapper.insert(record);
}

10.     疑问:如上配置是基于service为切入点,在百度的同时说可以将mapper(dao层)做切入点来做,但我实验了好几次也没成功,不知道这种方式是否能实现?

11.     开始我对于自己的实现是挺有信心的,可惜还是没有避免入坑,等代码测试人员测试的时候,发现功能不好用,之后各种排查,排查了一天居然是数据连错了,数据各种不对;找到后bug修复了,那边测试人员又开始测试主要流程支付,结果发现还是不好用,结果又是一顿排查,发现业务抛出异常后居然没有回滚,这里还好用的是测试库,结果发现问题出现在,spring的嵌套事务下执行得坑,啥话没说又一顿百度,又由于service方法中执行的业务比较多,数据源切换也比较频繁,数据源来回切换消耗的资源开销太大,所以我决定放弃,使用分布式事务管理jta来实现嵌套事务的ACID问题(使用jta来实现分布式事务会在下篇文章中介绍),虽然使用了其他方式解决了分布式事务的问题,但在这里我将整个问题描述一遍,希望和大家一块讨论并分析出问题出现在哪块?

12.     在同一个service方法中由于涉及到二个库的增删改查,但切换数据源注解是配置在service方法上的,所以导致不能自动切换数据源,采用的手手动切换,切换代码如下:

DynamicDataSourceHolder.setDataSourceType(DataSourceType.SLAVE.name()); 
securityAdditionMapper.insert(securityAddition);
DynamicDataSourceHolder.clearDataSourceType();

13.     嵌套事务演示代码如下:

@Override
@Transactional(rollbackFor = Exception.class)
public ApiCommonResultVo handlePay(){
	handlePayFinish();

    DynamicDataSourceHolder.setDataSourceType(DataSourceType.SLAVE.name()); 
    securityAdditionMapper.insert(securityAddition);
    DynamicDataSourceHolder.clearDataSourceType();
}


@Override
@Transactional(rollbackFor = Exception.class)
public void handlePayFinish(){
	// 业务代码

}

14.     有上述对需求的描述我总结了如下几个问题,请大神给予正确的解答:

    1)只用spring的事务管理能做到多数据源切换事务相关的ACID?

    2)spring事务支持嵌套事务吗?

    3)spring事务中去切换数据源为什么不可以?

    4)像spring这样的事务但程序跑到一半后系统全面奔溃,这个时候还能保住数据的ACID吗?

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏腾讯NEXT学位

那些让编码效率起飞(前端)的工具了解一下

? | 导语 想晚上吃鸡?前端编码效率提升工具了解一下? 一、Bash篇(Mac) iTerm2 iTerm 2 is a terminal emulato...

1983
来自专栏加米谷大数据

技术分享 | kafka的使用场景以及生态系统

kafka的使用场景 今天介绍一些关于Apache kafka 流行的使用场景。这些领域的概述 消息 kafka更好的替换传统的消息系统,消息系统被用于各种场景...

9518
来自专栏FreeBuf

Cisco Linksys无线路由固件安全分析与后门研究

最近我对嵌入式设备安全方面比较感兴趣,所以我决定找点东西练练手,于是我在淘宝上搜了一下,发现Linksys WRT54Gv5无线路由比较流行,决定就拿这个下手了...

3245
来自专栏Jerry的SAP技术分享

利用CRM中间件Middleware从ERP下载Customer Material的常见错误

下图是我在ERP创建的Material,为其维护了一个Customer Material AOP。

3988
来自专栏沈唁志

如何在CentOS 7上安装Asterisk

Asterisk是一个开源专用交换机(PBX)服务器,它使用会话发起协议(SIP)来路由和管理电话呼叫。值得注意的功能包括客户服务队列,待机音乐,电话会议和电话...

1.1K3
来自专栏Timhbw博客

Hexo-完全免费全平台搭建个人博客(2)-域名主题设置

2017-03-1011:01:58 发表评论 913℃热度 Hexo-完全免费全平台搭建个人博客(1)-整体搭建 上一篇文章把 Hexo 博客整体搭建一遍了...

41912
来自专栏乐沙弥的世界

MySQL 主从延迟监控脚本(pt-heartbeat)

    对于MySQL数据库主从复制延迟的监控,我们可以借助percona的有力武器pt-heartbeat来实现。pt-heartbeat通过使用时间戳方式在...

1431
来自专栏Golang语言社区

从零开始创建一个基于Go语言的web service

20个小时的时间能干什么?也许浑浑噩噩就过去了,也许能看一些书、做一些工作、读几篇博客、再写个一两篇博客,等等。而黑客马拉松(HackAthon),其实是一种自...

5189
来自专栏FH云彩

本站使用的WordPress插件

1785
来自专栏刘望舒

Android P 适配指南

Google自 android L (5.0) 以来就持续对安装系统进行 安全 以及 性能上的升级,此次的 android P (9.0)也不例外, 更大程度...

1.2K2

扫码关注云+社区

领取腾讯云代金券