可选方案
1.基于Spring的AbstractRoutingDataSource并用AOP动态切换
2.基于Mybatis多SqlSession实例分开扫描各自Mapper
1、基于Spring的AbstractRoutingDataSource并用AOP动态切换
优点:能灵活的控制多数据源,支持读写分离。
缺点:
1)AOP表达式需要自己配置,切换需要写对应的注解。
2)开启事务时,需要注意AOP顺序( 数据库事务的注解 要在 数据源切换的注解 之后)。
3)数据源动态切换,会有一定性能开销。
2、基于Mybatis多SqlSession实例分开扫描各自Mapper
优点:使用简单,配置后就和单数据源使用一样。性能好,不存在AOP那样的性能开销。
缺点:不支持读写分离。
3、结合方案一和方案二
方案一和方案二,并不冲突,可以结合使用。
应用场景和常见问题
1.多个独立数据源,每个数据源一套代码,独立使用。
2.读写分离,一套代码,根据不同方法,使用不同的数据源。
3.一套代码,同样的方法,根据传入的参数,使用不同的目标数据源。
1、多个独立数据源,每个数据源一套代码,独立使用
这个场景,方案一和方案二都可以解决,但是方案二更好。
2、读写分离,一套代码,根据不同方法,使用不同的数据源
考虑项目使用了一个可写数据源和多个只读数据源( 一主多从 模式),为了减少数据库压力,使用轮循的方式选择只读数据源。但是应当避免在同一个 Service 方法中写入后立即查询,因为此时可能 从库 数据还未同步,如果必须在执行写入操作后立即读取(有这种情况吗?),应该强制读取使用主数据源,即,一些实时性要求很高的select语句,可能需要放到master上执行,因为slave上可能存在延时。(为了解决这个问题,可以在查询的方法上加上数据源AOP注解,指定读取主库)
问题:数据库事务@Transactional应该放在Service层还是Dao层?它和数据源AOP注解,哪个在前,哪个在后?
开启事务时,肯定就要操作数据源了,DataSource此时必然做出选择,如果此时还没有使用AOP注解选择数据源,那将直接使用默认数据源。如果开启了事务后,又遇到了AOP注解切换数据源,那么势必造成数据源错乱。所以,数据源AOP必须在事务注解之前,如果数据源AOP注解在Dao方法上,那么事务注解也必须在Dao方法上,不能在Service层,另外,建议把@Transactional 注解加在方法上,不要加在类上。
这样就要考虑,Dao方法调用Dao方法的情况,每个Dao方法都有AOP切面,要保证 层层调用后,数据源不改变,不丢失。举个例子,UserDao.foo() 方法,调用了同源的MenuDao.bar() 方法、UserDao.insert() 方法 和 不同数据源的RemoteDao.query() 方法,如下所示:
@Transactional UserDao.foo() { MenuDao.bar(); RemoteDao.query(); UserDao.insert(); .... }
进入UserDao.foo()时,数据源会切换成 testDB,
进入MenuDao.bar() 时,又切换成 testDB,退出 MenuDao.bar() 时,数据源被清空,
再进入RemoteDao.query() 方法,数据源被切换成 remoteDB,退出RemoteDao.query() 方法时,数据源被清空,
再进入UserDao.insert() 方法,数据源被切换成 testDB,退出时,数据源被清空,
最后,退出UserDao.foo(),数据源被清空。(注意,上面说的退出,包括了异常退出,切面的@After能捕获异常的退出)
但是这个方案存在以下问题或疑问:
问题:当调用UserDao.insert() 方法时,由于和 外层的UserDao.foo() 方法处于同一个类中,就直接调用,AOP不会起作用,所以此时insert方法的数据源不能正常切换。为了解决这个问题,foo方法,不能放在UserDao中,必须得单独建一个类,建议放到Service中,然后在Service层就先切换好数据源。如下:
@Transactional @SelectDB("testDB") UserService.foo() { MenuDao.bar(); RemoteDao.query(); UserDao.insert(); .... }
这样一来,首先切换了testDB数据源并开启基于testDB事务,然后执行bar、query、insert,每一个AOP都会切换一次数据源,执行完后又清空,最后退出 foo(),事务提交,清空数据源。
疑问:如果UserDao.insert() 之后执行过程中报异常,要回滚insert,那么此时数据源被清空了,能否回滚?答案:能回滚,因为事务的开启和提交都是基于Connection连接的,而不是基于DataSource,如下所示:
public void transferAccounts(double money) { Connection conn = null; try { conn = JdbcUtils.getConnection(); conn.setAutoCommit(false); someDao.update(con, -money); someDao.update(con, +money); // 提交事务 conn.commit(); } catch (Exception e) { // 回滚 conn.rollback(); } }
开启事务时就保留了数据源连接,执行完之后,就可以在原数据源连接的基础上进行提交或者回滚。所以即使数据源丢失了,但是原来的连接Connection不变,故不会影响提交或回滚。详细分析如下:
由于外层开启了事务,那么方法内在同一个数据源下的所有连接,执行完成后,理论上都不会提交(只有外层方法执行完后,所有连接才能提交),或者说在事务内同一个数据源下只会开启一个连接,所有操作共用一个连接,那么Spring的Transaction机制到达是怎么实现的呢?翻看源码才能解答!
public void create(String name, Integer age, Integer marks, Integer year) { TransactionDefinition def = new DefaultTransactionDefinition(); TransactionStatus status = transactionManager.getTransaction(def); try { String SQL1 = "insert into Student (name, age) values (?, ?)"; jdbcTemplate.update( SQL1, name, age); String SQL2 = "select max(id) from Student"; int sid = jdbcTemplate.queryForInt( SQL2 ); String SQL3 = "insert into Marks(sid, marks, year) values (?, ?, ?)"; jdbcTemplate.update( SQL3, sid, marks, year); System.out.println("Created Name = " + name + ", Age = " + age); transactionManager.commit(status); } catch (DataAccessException e) { System.out.println("Error in creating record, rolling back"); transactionManager.rollback(status); throw e; } }
从这段代码可以看出,jdbc的连接,在最后才提交,那么一共开启了多少个连接,最后是怎么提交的呢?得看看JdbcTemplate源码和transactionManager.commit(status)。从JdbcTemplate源码可以查到,它getConnection是通过TransactionSynchronizationManager.getResource(dataSource),而这个方法最终会从下面的Map获取:
ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
和我的预期一样,说明Spring的Transaction会保留所有的连接,就拿以上示例的方法为例,它在一个事务里面,执行了3个SQL,这3次数据库操作都是在同一个Connection下的,而这个Connection在TransactionManager的掌握之中,最终可以进行commit或者rollback。就拿jdbcTemplate.update() 来分析,它执行完后会调用releaseConnection,但是是否真的关闭连接,还是TransactionManager说了算,下面一段代码为证:
void doReleaseConnection(Connection con, DataSource dataSource) {
ConnectionHolder conHolder = TransactionSynchronizationManager.getResource(dataSource);
if (conHolder != null && connectionEquals(conHolder, con)) {
// It's the transactional Connection: Don't close it.
conHolder.released();
return;
} else {
logger.debug("Returning JDBC Connection to DataSource");
doCloseConnection(con, dataSource);
}
同时,也可以看到,是根据DataSource来获取连接的,所以如果不是当前的DataSource,连接就会交给JDBC去close。
注意到foo()方法连接了一个其他数据源,对这个数据源,执行完成后就会立即提交,无法进行统一的事务控制。需要用到分布式事务,可以考虑结合Atomikos来处理。
现在,再回到上面的问题,最外层foo()方法针对于testDB开启了事务,内部insert也是基于testDB数据源的,所以它执行完后不会提交,当foo()结束后才会提交或回滚。
3、一套代码,同样的方法,根据传入的参数,使用不同的目标数据源
对于这个场景,可以将数据源参数,在入口(Controller)传入ThreadLocal,然后执行数据源选择时,根据当前Thread的key来决定数据源目标。这样,就能和场景2公用一套解决方案。
参考资料:
1.基于Mybatis多SqlSession实例分开扫描各自Mapper
https://blog.csdn.net/isea533/article/details/46815385
http://www.cnblogs.com/ityouknow/p/6102399.html
https://blog.csdn.net/maoyeqiu/article/details/74011626
https://blog.csdn.net/neosmith/article/details/61202084
https://www.cnblogs.com/Alandre/p/6611813.html
2.动态AOP数据源
3.另外一种思路: