基于自定义注解和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吗?

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值