忙活了一个星期,看了N多Spring、Hibernate源码,查了N多资料,走了N多弯路,总算实现了分布式事务。回头看看,实现这个功能对自己的提升确实是巨大的。
进入正题,官网之前的选库方案是在DataSource层面进行选库的,继承org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource来进行的选库,用SpringAOP做切面,在进入DAO方法之前进行选库(比如某个参数或者注解等)。事务用的是org.springframework.orm.hibernate3.HibernateTransactionManager。在一次操作单个数据库的情况下是没有问题的,但是在一次操作多个数据库时,比如有A、B两个库里面有一个相同的表,在A插入一条数据a,在B插入一条数据b,事务便会出问题,在提交的时候会全部提交到A库去,究其原因,应该是在事务开启时就打开了session,而一般打开事务时是在service层,没有进入DAO层。也就是说在打开事务时根本没有进行选库操作,而是进入的默认的数据库,导致事务提交时提交错了数据库。
为了解决这个问题,在网上浏览了一番后,找到一个CobarClient的阿里出的分布式数据库访问层,找到其中的事务管理类查看源码 com.alibaba.cobar.client.transaction.MultipleDataSourcesTransactionManager ,研究了一番后,按照自己的思路实现,发现根本没有进入事务控制,数据直接就到数据库了。发现事情没有这么简单,CobarClient是根据DataSource进行的事务开启和关闭操作,和官网环境不同,是用的Spring-Hibernate,事务管理也是Spring的 tx:annotation-driven 标签,而且CobarClient几年前就不更新了,还不知道用的是那个Spring-Hibernate版本,所以只能另寻解决办法。
在经历过上次失败后,突然想到官网用的最原始的org.springframework.orm.hibernate3.HibernateTransactionManager这个事务管理器是有效果的,只是最后commit的时候数据放错库了而已。那我写个类继承这个,重写相关方法不就行了,然后我就根据CobarClient的思想进行改造,在doBegin方法中把配置的全部的数据库连接开启了事务(事实上这样做很蠢,如果有100个配置,不就有100个事务了。。。),doCommit和doRollback方法循环调用的父类方法按照FILO规则来,然后又出问题了。因为SessionFactory是同一个,Spring会报一个同步错误,大意就是说你的session开启了一个以上事务,便会报错。
这是才意识到,选库和分布式事务是要相互配合的。
于是进行了一次比较大的修改,抛弃org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource选库方案,在初始化时进行SessionFactory初始化(为此又研究了Spring动态注册,以及org.springframework.beans.factory.FactoryBean.getObject(),一路艰辛啊。。),在选库时由选择DataSource改为选择SessionFactory,DAO层在调用时根据线程参数(java.lang.ThreadLocal)中的选库结果调用SessionFactory。事务方面研究了org.springframework.transaction.support.AbstractPlatformTransactionManager的源码,重点在于getTransaction方法发现doGetTransaction返回的结果并不重要(返回类型是Object),在框架中并没有其他使用它的地方,只有在必须实现的方法doBegin、doCommit、doRollback,以及挂起doSuspend,恢复挂起doResume方法isExistingTransaction是否已开启事务方法,以及doCleanupAfterCompletion事务结束后的销毁方法中用到。而这些方法都需要我们来实现,那么就有了一个想法。
在doGetTransaction方法中标识事务是否开启:
private ThreadLocal<Object> isTransaction = new ThreadLocal<Object>();
@Override
protected Object doGetTransaction() throws TransactionException {
Object o = new Object();
isTransaction.set(o);
return o;
}
事实上这里返回的object没有任何用处。而isTransaction只是标识事务开启而已,如果get出来为空则是没有开启事务。
在doBegin中将TransactionDefinition存起来(根据源码调用,这个东西从头到尾都是一个)
private ThreadLocal<TransactionDefinition> localTransactionDefinition = new ThreadLocal<TransactionDefinition>();
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException {
//将TransactionDefinition保存起来
try {
localTransactionDefinition.set(ObjectUtil.<TransactionDefinition>deepClone(definition));
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
重点在于选库的时候添加事务(插一句 Assert 这个用来判断参数挺不错的):
/**
* DataSourceAspect中选库时开启事务操作,如之前(线程中)就以开启事务,则不做操作
* @param sessionFactory 选中的库
* @return 是否成功开启事务
* @throws Exception
*/
public boolean chooseDataSource(SessionFactory sessionFactory) throws Exception {
Assert.notNull(sessionFactory, "Empty SessionFactory!");
//check sessionFactory is correct
if (!checkSessionFactory(sessionFactory)) return false;
//check transaction is start
if (isTransaction.get() == null) return false;
//check sessionFactory is started transaction
if (isInTransaction(sessionFactory)) return false;
List<HibernateTransactionObject> localObj = getLocalObj();
HibernateTransactionObject txObject = getHibernateTransactionObject(sessionFactory);
doBegin(txObject);
localObj.add(txObject);
setLocalObj(localObj);
return true;
}
private HibernateTransactionObject getHibernateTransactionObject(SessionFactory sessionFactory) {
Assert.notNull(sessionFactory, "Empty SessionFactory!");
HibernateTransactionObject txObject = new HibernateTransactionObject();
txObject.setSavepointAllowed(isNestedTransactionAllowed());
txObject.setSessionFactory(sessionFactory);
SessionHolder sessionHolder =
(SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
if (sessionHolder != null) {
if (logger.isDebugEnabled()) {
logger.debug("Found thread-bound Session [" +
SessionFactoryUtils.toString(sessionHolder.getSession()) + "] for Hibernate transaction");
}
txObject.setSessionHolder(sessionHolder);
}
DataSource dataSource = SessionFactoryUtils.getDataSource(sessionFactory);
if (dataSource != null) {
txObject.setDataSource(dataSource);
ConnectionHolder conHolder = (ConnectionHolder)
TransactionSynchronizationManager.getResource(dataSource);
txObject.setConnectionHolder(conHolder);
}
return txObject;
}
/**
* 开启一个事务
* @param txObject
* @throws Exception
*/
private void doBegin(HibernateTransactionObject txObject) throws Exception {
TransactionDefinition definition = localTransactionDefinition.get();
if (definition == null) throw new Exception("Transaction is not begin!");
if (txObject.hasConnectionHolder() && !txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
throw new IllegalTransactionStateException(
"Pre-bound JDBC Connection found! HibernateTransactionManager does not support " +
"running within DataSourceTransactionManager if told to manage the DataSource itself. " +
"It is recommended to use a single HibernateTransactionManager for all transactions " +
"on a single DataSource, no matter whether Hibernate or JDBC access.");
}
Session session = null;
try {
if (txObject.getSessionHolder() == null || txObject.getSessionHolder().isSynchronizedWithTransaction()) {
Session newSession = txObject.getSessionFactory().openSession();
if (logger.isDebugEnabled()) {
logger.debug("Opened new Session [" + SessionFactoryUtils.toString(newSession) +
"] for Hibernate transaction");
}
txObject.setSession(newSession);
}
session = txObject.getSessionHolder().getSession();
if (isSameConnectionForEntireSession(session)) {
// We're allowed to change the transaction settings of the JDBC Connection.
if (logger.isDebugEnabled()) {
logger.debug(
"Preparing JDBC Connection of Hibernate Session [" + SessionFactoryUtils.toString(session) + "]");
}
Connection con = session.connection();
Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
txObject.setPreviousIsolationLevel(previousIsolationLevel);
}
else {
// Not allowed to change the transaction settings of the JDBC Connection.
if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
// We should set a specific isolation level but are not allowed to...
throw new InvalidIsolationLevelException(
"HibernateTransactionManager is not allowed to support custom isolation levels: " +
"make sure that its 'prepareConnection' flag is on (the default) and that the " +
"Hibernate connection release mode is set to 'on_close' (SpringTransactionFactory's default). " +
"Make sure that your LocalSessionFactoryBean actually uses SpringTransactionFactory: Your " +
"Hibernate properties should *not* include a 'hibernate.transaction.factory_class' property!");
}
if (logger.isDebugEnabled()) {
logger.debug(
"Not preparing JDBC Connection of Hibernate Session [" + SessionFactoryUtils.toString(session) + "]");
}
}
if (definition.isReadOnly() && txObject.isNewSession()) {
// Just set to MANUAL in case of a new Session for this transaction.
session.setFlushMode(FlushMode.MANUAL);
}
if (!definition.isReadOnly() && !txObject.isNewSession()) {
// We need AUTO or COMMIT for a non-read-only transaction.
FlushMode flushMode = session.getFlushMode();
if (flushMode.lessThan(FlushMode.COMMIT)) {
session.setFlushMode(FlushMode.AUTO);
txObject.getSessionHolder().setPreviousFlushMode(flushMode);
}
}
Transaction hibTx;
// Register transaction timeout.
int timeout = determineTimeout(definition);
if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
// Use Hibernate's own transaction timeout mechanism on Hibernate 3.1+
// Applies to all statements, also to inserts, updates and deletes!
hibTx = session.getTransaction();
hibTx.setTimeout(timeout);
hibTx.begin();
}
else {
// Open a plain Hibernate transaction without specified timeout.
hibTx = session.beginTransaction();
}
// Add the Hibernate transaction to the session holder.
txObject.getSessionHolder().setTransaction(hibTx);
// Register the Hibernate Session's JDBC Connection for the DataSource, if set.
if (txObject.getDataSource() != null) {
Connection con = session.connection();
ConnectionHolder conHolder = new ConnectionHolder(con);
if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
conHolder.setTimeoutInSeconds(timeout);
}
if (logger.isDebugEnabled()) {
logger.debug("Exposing Hibernate transaction as JDBC transaction [" + con + "]");
}
TransactionSynchronizationManager.bindResource(txObject.getDataSource(), conHolder);
txObject.setConnectionHolder(conHolder);
}
// Bind the session holder to the thread.
if (txObject.isNewSessionHolder()) {
TransactionSynchronizationManager.bindResource(txObject.getSessionFactory(), txObject.getSessionHolder());
}
txObject.getSessionHolder().setSynchronizedWithTransaction(true);
}
catch (Exception ex) {
if (txObject.isNewSession()) {
try {
if (session.getTransaction().isActive()) {
session.getTransaction().rollback();
}
}
catch (Throwable ex2) {
logger.debug("Could not rollback Session after failed transaction begin", ex);
}
finally {
SessionFactoryUtils.closeSession(session);
}
}
throw new CannotCreateTransactionException("Could not open Hibernate Session for transaction", ex);
}
}
/**
* Hibernate transaction object, representing a SessionHolder.
* Used as transaction object by HibernateTransactionManager.
*/
public class HibernateTransactionObject extends JdbcTransactionObjectSupport {
private SessionHolder sessionHolder;
private boolean newSessionHolder;
private boolean newSession;
private DataSource dataSource;
private SessionFactory sessionFactory;
public void setSession(Session session) {
this.sessionHolder = new SessionHolder(session);
this.newSessionHolder = true;
this.newSession = true;
}
public void setExistingSession(Session session) {
this.sessionHolder = new SessionHolder(session);
this.newSessionHolder = true;
this.newSession = false;
}
public void setSessionHolder(SessionHolder sessionHolder) {
this.sessionHolder = sessionHolder;
this.newSessionHolder = false;
this.newSession = false;
}
public SessionHolder getSessionHolder() {
return this.sessionHolder;
}
public boolean isNewSessionHolder() {
return this.newSessionHolder;
}
public boolean isNewSession() {
return this.newSession;
}
public DataSource getDataSource() {
return dataSource;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public SessionFactory getSessionFactory() {
return sessionFactory;
}
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
public boolean hasSpringManagedTransaction() {
return (this.sessionHolder != null && this.sessionHolder.getTransaction() != null);
}
public boolean hasHibernateManagedTransaction() {
return (this.sessionHolder != null && this.sessionHolder.getSession().getTransaction().isActive());
}
public void setRollbackOnly() {
this.sessionHolder.setRollbackOnly();
if (hasConnectionHolder()) {
getConnectionHolder().setRollbackOnly();
}
}
public boolean isRollbackOnly() {
return this.sessionHolder.isRollbackOnly() ||
(hasConnectionHolder() && getConnectionHolder().isRollbackOnly());
}
@Override
public void flush() {
try {
this.sessionHolder.getSession().flush();
}
catch (HibernateException ex) {
throw SessionFactoryUtils.convertHibernateAccessException(ex);
}
}
}
好吧,除了chooseDataSource方法之外 基本copy的org.springframework.orm.hibernate3.HibernateTransactionManager。HibernateTransactionObject 中添加了SessionFactory和DataSource两个属性。
至于doCommit和doRollback基本就是按照FILO循环操作了
完成代码后试验,发现成功了。成功提交回滚,不会提交到一个库里面了。事务挂起没有实验成功,因为并没有进挂起方法。原因暂时不明,需要继续查资料。