前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于自定义注解和Aop动态数据源配置

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

作者头像
秋日芒草
发布2018-05-15 17:34:52
1.4K0
发布2018-05-15 17:34:52
举报
文章被收录于专栏:JavaWebJavaWeb

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

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

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

代码语言:javascript
复制
<!-- 阿里巴巴数据源包 -->
<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 核心配置如下:

代码语言:javascript
复制
<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配置文件如下:

代码语言:javascript
复制
# =====================数据源切换数据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 如下:

代码语言:javascript
复制
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 如下:

代码语言:javascript
复制
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 如下:

代码语言:javascript
复制
package cn.edu.his.pay.dynamic.datasource;

public enum DataSourceType {
	 MASTER, SLAVE
}

8.      DynamicDataSource.java 如下:

代码语言:javascript
复制
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 如下:

代码语言:javascript
复制
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。

代码语言:javascript
复制
<!-- 配置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>
代码语言:javascript
复制
@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方法上的,所以导致不能自动切换数据源,采用的手手动切换,切换代码如下:

代码语言:javascript
复制
DynamicDataSourceHolder.setDataSourceType(DataSourceType.SLAVE.name()); 
securityAdditionMapper.insert(securityAddition);
DynamicDataSourceHolder.clearDataSourceType();

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

代码语言:javascript
复制
@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吗?

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 基于自定义注解和Aop动态数据源配置
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档