多数据源的源码在mybatis底下–书接上文,我们在自己实现多数据源的同时,springboot自带的数据源就失去了作用(DataSourceTransactionManager),那么我们来尝试自己实现一下事务控制,首先我们理一下思路:我们由于业务需要,所以选择了多数据源的解决方案,使用注解与AOP拦截的方式,通过继承AbstractRoutingDataSource抽象类,实现里边的determineCurrentLookupKey()(这是个坑)方法,通过改变返回值,实现了DataSource的切换,然而当我们使用spring事务管理的时候傻眼了,我们一般这么写:
@Transactional(rollbackFor = {Exception.class})
点击进入Transactional注解类,发现transactionManager的value字段是个String,那么问题来了,我们有多个数据源,当然我们也可以根据自己的DataSource生成不同的transactionManager,但是我们如果一个方法只有一个增删改的还好,但是当一个方法同时操作两个或以上的库是,我们会发现Transactional的value只能写一个transactionManager,导致事务失效。
解决思路:
既然Transactional只能注入一个transactionManager,那么我们自己来维护一个注解,可以注入多个,然后在方法开始的时候全部开启事务,然后方法都执行完没问题,我们同时doCommit,如果有一个报错,所有都rollback,话不多说,代码开撸:
话不多说,撸个注解:
@Target({ElementType.METHOD, ElementType.TYPE,ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MoreTransaction {
DataSourceType[] value() default {};
}
有注解了,我们再自己来定义transactionManager
@Configuration
public class TransactionManager {
@Bean(name = "MASTER")
public PlatformTransactionManager master(@Qualifier("masterDataSource") DataSource dataSourceOne) {
return new DataSourceTransactionManager(dataSourceOne);
}
@Bean(name = "SLAVE")
public PlatformTransactionManager slave(@Qualifier("slaveDataSource") DataSource dataSourceOne) {
return new DataSourceTransactionManager(dataSourceOne);
}
}
然后拦截注解,同时提交同时失败
@Aspect
@Order(2)
@Component
public class TransactionAop {
private final Logger log = LoggerFactory.getLogger(getClass());
@Pointcut("@annotation(com.springbootx.mybatis.config.transactional.MoreTransaction)")
public void moreTransaction() {
}
@Around("moreTransaction()&&@annotation(annotation)")
public Object aroundTransaction(ProceedingJoinPoint thisJoinPoint, MoreTransaction annotation) throws Throwable {
DataSourceType[] transactionMangerNames = annotation.value();
if (annotation.value().length==0) {
return false;
}
Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack=new Stack<>();
Stack<TransactionStatus> dataSourceTransactionStack=new Stack<>();
Stack<DataSourceType> transactionMangerNamesStack=new Stack<>();
for (DataSourceType beanName : transactionMangerNames) {
DataSourceTransactionManager dataSourceTransactionManager = ApplicationContextUtil.getBean(beanName.name());
dataSourceTransactionManagerStack.push(dataSourceTransactionManager);
TransactionStatus transactionStatus = dataSourceTransactionManager
.getTransaction(new DefaultTransactionDefinition());
dataSourceTransactionStack.push(transactionStatus);
transactionMangerNamesStack.push(beanName);
}
Object proceed;
try {
proceed = thisJoinPoint.proceed();
} catch(Exception ex){
while (!dataSourceTransactionManagerStack.isEmpty()) {
DynamicDataSourceContextHolder.setDataSourceType(transactionMangerNamesStack.pop().name());
dataSourceTransactionManagerStack.pop().rollback(dataSourceTransactionStack.pop());
DynamicDataSourceContextHolder.clearDataSourceType();
}
log.info("事务回滚");
return null;
// throw ex;
}
log.info("事务提交,{}次事务",dataSourceTransactionManagerStack.size());
while (!dataSourceTransactionManagerStack.isEmpty()) {
DynamicDataSourceContextHolder.setDataSourceType(transactionMangerNamesStack.pop().name());
dataSourceTransactionManagerStack.pop().commit(dataSourceTransactionStack.pop());
DynamicDataSourceContextHolder.clearDataSourceType();
}
return proceed;
}
}
看着没啥问题,逻辑也没错,跑一下傻眼了,不管有没有报错,他都提交了,报错也没有走rollback方法,而且最骚的是我在代码里是这样写的:
他们都提交到第一个库去了,瞬间懵逼,然后就开始找原因:
- AOP可以触发数据源字符串的切换,这个没问题
- 数据源真正切换的关键是 AbstractRoutingDataSource 的 determineCurrentLookupKey() 被调用,此方法是在open connection时触发
- 事务是在connection层面管理的,启用事务后,一个事务内部的connection是复用的,所以就算AOP切了数据源字符串,但是数据源并不会被真正修改
综上所述:如果要使用事务,还是别用determineCurrentLookupKey()这种方法切数据源了,得配置多个才行
以上是我在博客找的原因,说的没错,原因确实是这样的,因为spring的事务管理使用了aop代理,在方法开始前,已经将当前数据源绑定在了线程中,所以无论怎样切换,使用的都是同一个数据源,spring事务管理代码截图如下:
那么我们只能放弃spring的事务管理方法自己来写一套事务管理方法,在我们写之前我们还要知道一件事:当事务不再由spring管理后,mybatis会使用自己的事务管理机制,即在操作完数据库后自动提交和关闭,所以解决方法就是重写Connection对象的提交和关闭方法,使mybatis的自动提交和关闭不生效,所以我们还得重写mybatis的事务管理机制,那么我们重新撸代码:
写注解:
@Target({ElementType.METHOD, ElementType.TYPE,ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MoreTransaction {
DataSourceType[] value() default {};
IsolationLevel isolationLevel() default IsolationLevel.TRANSACTION_READ_UNCOMMITTED;
}
写一个事务的隔离级别枚举:
public enum IsolationLevel{
/**
* 事务隔离级别枚举
*/
TRANSACTION_NONE("TRANSACTION_NONE", 0),
TRANSACTION_READ_UNCOMMITTED("TRANSACTION_READ_UNCOMMITTED", 1),
TRANSACTION_READ_COMMITTED("TRANSACTION_READ_COMMITTED", 2),
TRANSACTION_REPEATABLE_READ("TRANSACTION_REPEATABLE_READ", 4),
TRANSACTION_SERIALIZABLE("TRANSACTION_SERIALIZABLE",8);
private final int value;
private final String name;
IsolationLevel(String name, int value){
this.value = value;
this.name = name;
}
public int getValue() {
return value;
}
public String getName() {
return name;
}
}
自己实现的Connection
public class ConnectWarp implements Connection {
private final Connection connection;
public ConnectWarp(Connection connection) {
this.connection = connection;
}
/**
* 当mybatis自身执行完成后调用commit方法后没有实质性的commit
*
* @throws SQLException SQLException
*/
@Override
public void commit() throws SQLException {
// connection.commit();
}
/**
* 该方法为我们自己想要提交事务的时候调用
*
* @throws SQLException SQLException
*/
public void realCommit() throws SQLException {
connection.commit();
}
/**
* 当mybatis自身执行完成后调用close方法后没有实质性的close
*
* @throws SQLException SQLException
*/
@Override
public void close() throws SQLException {
//connection.close();
}
/**
* 如果包装类 要用这个方法关闭
*
* @throws SQLException SQLException
*/
public void realClose() throws SQLException {
connection.close();
}
/**
* 创建一个 Statement对象,用于将SQL语句发送到数据库。
*/
@Override
public Statement createStatement() throws SQLException {
return connection.createStatement();
}
/**
* 创建一个 PreparedStatement对象,用于将参数化的SQL语句发送到数据库。
*
* @param sql 预处理sql foo:select * from foo where id = ?
*/
@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {
return connection.prepareStatement(sql);
}
/**
* 创建一个调用数据库存储过程的 CallableStatement对象。
*
* @param sql
*/
@Override
public CallableStatement prepareCall(String sql) throws SQLException {
return connection.prepareCall(sql);
}
/**
* 将给定的SQL语句转换为系统的本机SQL语法
*
* @param sql
*/
@Override
public String nativeSQL(String sql) throws SQLException {
return connection.nativeSQL(sql);
}
/**
* 将此连接的自动提交模式设置为给定状态。
*
* @param autoCommit 是否自动提交
*/
@Override
public void setAutoCommit(boolean autoCommit) throws SQLException {
connection.setAutoCommit(autoCommit);
}
/**
* 对象的当前自动提交模式
*/
@Override
public boolean getAutoCommit() throws SQLException {
return connection.getAutoCommit();
}
/**
* 撤消在当前事务中所做的所有更改,并释放此 Connection对象当前持有的任何数据库锁。
*/
@Override
public void rollback() throws SQLException {
connection.rollback();
}
/**
* 此Connection对象是否已关闭。
*/
@Override
public boolean isClosed() throws SQLException {
return connection.isClosed();
}
/**
* 获取对象的数据源信息
*/
@Override
public DatabaseMetaData getMetaData() throws SQLException {
return connection.getMetaData();
}
/**
* 设置连接是否只读
*/
@Override
public void setReadOnly(boolean readOnly) throws SQLException {
connection.setReadOnly(readOnly);
}
/**
* 检索连接的只读状态
*/
@Override
public boolean isReadOnly() throws SQLException {
return connection.isReadOnly();
}
@Override
public void setCatalog(String catalog) throws SQLException {
connection.setCatalog(catalog);
}
@Override
public String getCatalog() throws SQLException {
return connection.getCatalog();
}
/**
* 设置事物的隔离级别
*/
@Override
public void setTransactionIsolation(int level) throws SQLException {
connection.setTransactionIsolation(level);
}
/**
* 获取当前连接的事物隔离级别
*/
@Override
public int getTransactionIsolation() throws SQLException {
return connection.getTransactionIsolation();
}
/**
* 获取连接报告中的第一个警告
*/
@Override
public SQLWarning getWarnings() throws SQLException {
return connection.getWarnings();
}
/**
* 清空所有连接报告
*/
@Override
public void clearWarnings() throws SQLException {
connection.clearWarnings();
}
@Override
public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
return connection.createStatement(resultSetType, resultSetConcurrency);
}
@Override
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
return connection.prepareStatement(sql, resultSetType, resultSetConcurrency);
}
@Override
public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
return connection.prepareCall(sql, resultSetType, resultSetConcurrency);
}
@Override
public Map<String, Class<?>> getTypeMap() throws SQLException {
return connection.getTypeMap();
}
@Override
public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
connection.setTypeMap(map);
}
@Override
public void setHoldability(int holdability) throws SQLException {
connection.setHoldability(holdability);
}
@Override
public int getHoldability() throws SQLException {
return connection.getHoldability();
}
@Override
public Savepoint setSavepoint() throws SQLException {
return connection.setSavepoint();
}
@Override
public Savepoint setSavepoint(String name) throws SQLException {
return connection.setSavepoint(name);
}
@Override
public void rollback(Savepoint savepoint) throws SQLException {
connection.rollback(savepoint);
}
@Override
public void releaseSavepoint(Savepoint savepoint) throws SQLException {
connection.releaseSavepoint(savepoint);
}
@Override
public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
return connection.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability);
}
@Override
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
return connection.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability);
}
@Override
public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
return connection.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability);
}
@Override
public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
return connection.prepareStatement(sql, autoGeneratedKeys);
}
@Override
public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {
return connection.prepareStatement(sql, columnIndexes);
}
@Override
public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
return connection.prepareStatement(sql, columnNames);
}
@Override
public Clob createClob() throws SQLException {
return connection.createClob();
}
@Override
public Blob createBlob() throws SQLException {
return connection.createBlob();
}
@Override
public NClob createNClob() throws SQLException {
return connection.createNClob();
}
@Override
public SQLXML createSQLXML() throws SQLException {
return connection.createSQLXML();
}
@Override
public boolean isValid(int timeout) throws SQLException {
return connection.isValid(timeout);
}
@Override
public void setClientInfo(String name, String value) throws SQLClientInfoException {
connection.setClientInfo(name, value);
}
@Override
public void setClientInfo(Properties properties) throws SQLClientInfoException {
connection.setClientInfo(properties);
}
@Override
public String getClientInfo(String name) throws SQLException {
return connection.getClientInfo(name);
}
@Override
public Properties getClientInfo() throws SQLException {
return connection.getClientInfo();
}
@Override
public Array createArrayOf(String typeName, Object[] elements) throws SQLException {
return connection.createArrayOf(typeName, elements);
}
@Override
public Struct createStruct(String typeName, Object[] attributes) throws SQLException {
return connection.createStruct(typeName, attributes);
}
@Override
public void setSchema(String schema) throws SQLException {
connection.setSchema(schema);
}
@Override
public String getSchema() throws SQLException {
return connection.getSchema();
}
@Override
public void abort(Executor executor) throws SQLException {
connection.abort(executor);
}
@Override
public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {
connection.setNetworkTimeout(executor, milliseconds);
}
@Override
public int getNetworkTimeout() throws SQLException {
return connection.getNetworkTimeout();
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return connection.unwrap(iface);
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return connection.isWrapperFor(iface);
}
}
然后来增强以前写的DataSourceRouting
/**
* 保存当前线程使用了事务的数据库连接(connection)
* 当我们自己管理事务的时候即可从此处获取到当前线程使用了哪些连接从而让这些被使用的连接commit/rollback/close
*/
private final ThreadLocal<Map<String, ConnectWarp>> connectionThreadLocal = new ThreadLocal<>();
/**
* mybatis在使用mapper接口执行sql的时候会从该方法获取connection执行sql
* 如果事务是spring或者mybatis在管理,那么直接返回原生的connection
* 如果是我们自己控制事务,则返回我们自己实现的ConnetWarp
*
* @return Connection
* @throws SQLException SQLException
*/
@Override
public Connection getConnection() throws SQLException {
Map<String, ConnectWarp> stringConnectionMap = connectionThreadLocal.get();
if (stringConnectionMap == null) {
// 没开事物 直接返回
return determineTargetDataSource().getConnection();
} else {
// 开了事物 从当前线程中拿 而且拿到的是 包装过的connect 只有手动去提交和关闭连接
String currentName = (String) determineCurrentLookupKey();
return stringConnectionMap.get(currentName);
}
}
/**
* 开启事物的时候,把连接放入 线程中,后续crud 都会拿对应的连接操作
*
* @param key 事务的key
* @param connection 连接
*/
public void bindConnection(String key, Connection connection) {
Map<String, ConnectWarp> connectionMap = connectionThreadLocal.get();
if (connectionMap == null) {
connectionMap = new HashMap<>();
connectionThreadLocal.set(connectionMap);
}
ConnectWarp connectWarp = new ConnectWarp(connection);
connectionMap.put(key, connectWarp);
}
/**
* 提交事物
*
* @throws SQLException SQLException
*/
public void doCommit() throws SQLException {
Map<String, ConnectWarp> stringConnectionMap = connectionThreadLocal.get();
if (stringConnectionMap == null) {
return;
}
for (String dataSourceName : stringConnectionMap.keySet()) {
ConnectWarp connection = stringConnectionMap.get(dataSourceName);
connection.realCommit();
connection.realClose();
}
removeConnectionThreadLocal();
}
/**
* 回滚事物
*
* @throws SQLException SQLException
*/
public void rollback() throws SQLException {
Map<String, ConnectWarp> stringConnectionMap = connectionThreadLocal.get();
if (stringConnectionMap == null) {
return;
}
for (String dataSourceName : stringConnectionMap.keySet()) {
ConnectWarp connection = stringConnectionMap.get(dataSourceName);
connection.rollback();
connection.realClose();
}
removeConnectionThreadLocal();
}
protected void removeConnectionThreadLocal() {
connectionThreadLocal.remove();
}
然后写aop实现具体逻辑:
@Aspect
@Order(2)
@Component
public class TransactionAop {
private final Logger log = LoggerFactory.getLogger(getClass());
@Resource
BeanFactory beanFactory;
@Pointcut("@annotation(com.springbootx.mybatis.config.transactional.MoreTransaction)")
public void moreTransaction() {
}
@Around("moreTransaction()&&@annotation(annotation)")
public Object aroundTransaction(ProceedingJoinPoint thisJoinPoint, MoreTransaction annotation) throws Throwable {
DataSourceType[] transactionMangerNames = annotation.value();
if (annotation.value().length==0) {
return false;
}
IsolationLevel isolationLevel = annotation.isolationLevel();
DataSourceRouting dataSourceRouting=ApplicationContextUtil.getBean(DataSourceRouting.class);
Map<Object, DataSource> resolvedDataSources = dataSourceRouting.getResolvedDataSources();
for (DataSourceType beanName : transactionMangerNames) {
DataSource dataSource =resolvedDataSources.get(beanName.name());
Connection connection = dataSource.getConnection();
if (connection != null) {
// 设置事务隔离级别
int transaction= isolationLevel.getValue();
connection.setTransactionIsolation(transaction);
if (connection.getAutoCommit()) {
connection.setAutoCommit(false);
}
}
dataSourceRouting.bindConnection(beanName.name(), connection);
}
Object proceed;
try {
proceed = thisJoinPoint.proceed();
} catch(Exception ex){
dataSourceRouting.rollback();
log.info("事务回滚");
throw ex;
}
dataSourceRouting.doCommit();
return proceed;
}
}
里边用到一个ApplicationContext的util,从上下文获取合格的bean:
@Component
public class ApplicationContextUtil implements ApplicationContextAware, DisposableBean {
private static final Logger logger = LoggerFactory.getLogger(ApplicationContextUtil.class);
private static ApplicationContext applicationContext = null;
/**
* 取得存储在静态变量中的ApplicationContext.
*/
public static ApplicationContext getApplicationContext() {
assertContextInjected();
return applicationContext;
}
/**
* 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
public static <T> T getBean(String name) {
assertContextInjected();
return (T) applicationContext.getBean(name);
}
/**
* 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
public static <T> T getBean(Class<T> requiredType) {
assertContextInjected();
return applicationContext.getBean(requiredType);
}
/**
* 检查ApplicationContext不为空.
*/
private static void assertContextInjected() {
if (applicationContext == null) {
throw new IllegalStateException("applicaitonContext属性未注入, 请在applicationContext" +
".xml中定义SpringContextHolder或在SpringBoot启动类中注册SpringContextHolder.");
}
}
/**
* 清除SpringContextHolder中的ApplicationContext为Null.
*/
public static void clearHolder() {
logger.debug("清除SpringContextHolder中的ApplicationContext:"
+ applicationContext);
applicationContext = null;
}
@Override
public void destroy() throws Exception {
ApplicationContextUtil.clearHolder();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (ApplicationContextUtil.applicationContext != null) {
logger.warn("SpringContextHolder中的ApplicationContext被覆盖, 原有ApplicationContext为:" + ApplicationContextUtil.applicationContext);
}
ApplicationContextUtil.applicationContext = applicationContext;
}
接下来我们来测试:
首先不报错来看看能不能提交到不同的库:
执行之后看库
分别插入到不同的库里了,下边再来测试报错会不会同时回滚,删除刚才的数据
执行代码:
可以看到我们的事务回滚日志打出来了,而且也抛出了错,我们再看库里:
可以看到都没有插入成功,我们增加难度:
我们在一个主方法调用另一个主方法,在第二个主方法抛错,然后依然回滚成功,那么我们就成功的完成了多数据源下的事务控制。