Spring-基于自定义注解和Aop动态数据源配置
在实际项目中,经常会因为需要增强数据库并发能力而设计分库分表或者读写分离等策略,每在旧项目中引进新技术的时候都会带来一系列的问题,我们的目的就是去解决问题,带着思考方式去重构系统,从中找到乐趣,对应引进自定义注解和Aop动态数据源配置技术带来的问题,我会在文章末尾介绍,也希望大神给予正确的引导,我们当时的需求就是:有一个XXX旧系统,我们在这个旧系统的基础上开发一个PC端的程序用于收银;对方提供他们的数据库文档和对接人员,旧系统代码他们不给,我们只能通过沟通去了解他们旧系统的设计思路,带着一万个艹尼玛去写代码了;我们属于二次开发,需要在旧系统的数据库基础上开发自己的业务数据库,到这里就设计到二个数据库了(一个是旧系统的数据库,一个收银系统的数据库),项目之前能想到得就是自定义注解和Aop动态数据源配置来实现,但存在坑,下面我会提出坑点;现在就让我们先从配置(本文是基于SSM框架下集成的动态数据源切换):
1. 配置pom.xml,使用的是阿里巴巴数据源包和Mysql 5.1.30的驱动
com.alibaba
druid
1.0.2
mysql
mysql-connector-java
5.1.30
2. spring-dispatcher.xml 核心配置如下:
destroy-method="close">
destroy-method="close">
dialect=mysql
reasonable=true
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
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 contextHolder = new ThreadLocal();
// 设置数据源类型
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。
@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吗?