诡异的hibernate和jdbc混用bug,局部事务-----------------记录一下

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

public class HibernateBaseDao implements ConnectionManager  {

private Log log = LogFactory.getLog(HibernateBaseDao.class);

@Resource

private SessionFactory factory;

/**

* 当前线程的session,获取session后捆绑到sessionThreadLoacl对象中,用于保证同一线程中只存在一个session

*/

public static final ThreadLocal sessionThreadLoacl = new ThreadLocal();

/**

* @description 获取Hibernate session

* @return

*/

public Session getSession() {

Session session = sessionThreadLoacl.get();

if (session == null) {

session = factory.openSession();

sessionThreadLoacl.set(session);

}

//为兼容与JDBC混用,放弃hibernate一级缓存机制,根本解决方式是:所有数据处理都通过session处理,即使用session处理重写BaseDao中getResultString和execute两个JDBC处理方法

//session.flush();

//session.clear();

return session;


}


/*

* (non-Javadoc)

* @see com.founder.cms.core.database.ConnectionManager#getConnection()

*/

@Override

public Connection getConnection() {

Connection conn = null;

Session session = getSession();

Method connMethod;

try {

// 在与hibernate混用时,取session所用的connection,保证jdbc与hibernate使用同一个connection

connMethod = session.getClass().getMethod("connection");

conn = (Connection) ReflectionUtils.invokeMethod(connMethod, session);

} catch (Exception e) {

log.error("getConnectionError", e);

}

return conn;

}


/*

* (non-Javadoc)

* @see

* com.founder.cms.core.database.ConnectionManager#getTransactionConnection(

* )

*/

@Override

public Connection beginTransaction() {

Connection conn = getConnection();

try {

conn.setAutoCommit(false);

} catch (Exception e) {

log.error("getTransactionConnectionError", e);

}

return conn;

}


/*

* (non-Javadoc)

* @see com.founder.cms.core.database.ConnectionManager#commit()

*/

@Override

public boolean commit() {

try {

Connection conn = getConnection();

conn.commit();

conn.setAutoCommit(true);

return true;

} catch (SQLException e) {

log.error("commitError", e);

return false;

}

}


/*

* (non-Javadoc)

* @see com.founder.cms.core.database.ConnectionManager#rollBack()

*/

@Override

public boolean rollback() {

try {

Connection conn = getConnection();

if(!conn.getAutoCommit()){

conn.rollback();

}

return true;

} catch (SQLException e) {

log.error("rollbackError", e);

return false;

}

}

/**

* 释放Session,专门由接口使用,接口调用完session后要进行close操作 web请求可不用此方法,请求结束后自动释放

*/

@Override

public void close() {

Session session = sessionThreadLoacl.get();

if (session != null) {

session.close();

log.info("Current thread session is closed graceful!");

sessionThreadLoacl.set(null);

}

}


}

=====================分割线,以上是主要dao代码,下面描述bug=====================

代码中jdbc所用的connection是取自hibernate的封装session,目的是为了统一起来,但是按照上面的方式出了奇怪的bug。如果混用的时候:事务提交,同时刷新缓存数据是可以更新到数据库的,这时候看不出Bug,在提交增删改事务时忘记刷新缓存之后,页面会出现随机性结果(页面每次点击按钮增删改就是一次请求,就回来刷新页面),有时跟数据库一致,有时不一致,几种错误结果随机出现。(一个用户在操作的情况是这样的,多用户多线程要更加复杂)。

分析问题原因:sessionThreadLoacl 模仿spring HibernateTemplate保存了一个session绑定线程,但是它没有spring中间的上下文判断;由于WEB请求以tomcat为例来讲,每次接收一个请求处理完毕后就释放请求线程放回线程池了,这样多次点击结果就有一个未刷到数据库的中间操作结果进入了线程池,而线程里面的session没有关闭,没有释放,再次发起请求可能就会又被服务器分配到这些线程,这时候就出现了随机性的错误结果。

每次取session都把session刷缓存,然后clear,相当于session每次用都是空的,且刷缓存,清理缓存都有开销,对性能有影响,也起不到缓存该有的作用。

在hdbc和hibernate的session混合的事务里,进行了flush刷缓存,每次操作结果都刷到缓存了,线程都回到线程池,但是每次请求都会随机被分配到前面几次操作结果的线程中的session,导致随机性的错误出现。

========================解决这个问题的思路=============================

1、采用getCurrentSession()来开启session,或者采用spring HibernateTemplate来操作session

================================下面是参考文章========================

在说他们之间的区别之前,先考虑如下几个问题: 
1、getCurrentSession()与openSession()的区别? 
* 采用getCurrentSession()创建的session会绑定到当前线程中,而采用openSession() 
  创建的session则不会 
* 采用getCurrentSession()创建的session在commit或rollback时会自动关闭,而采用openSession() 
  创建的session必须手动关闭 
  
2、使用getCurrentSession()需要在hibernate.cfg.xml文件中加入如下配置: 
* 如果使用的是本地事务(jdbc事务) 
thread 
* 如果使用的是全局事务(jta事务) 
jta 

以上是hibernate中一些使用,下面来说说jdbc与jta的区别:    
JDBC 事务 
JDBC 事务是用 Connection 对象控制的。JDBC Connection 接口( java.sql.Connection )提供了两种事务模式:自动提交和手工提交。 
#在jdbc中,事务操作缺省是自动提交。也就是说,一条对数据库的更新表达式代表一项事务操作,操作成功后,系统将自动调用commit()来提交,否则将调用rollback()来回滚。 
# 在jdbc中,可以通过调用setAutoCommit(false)来禁止自动提交。之后就可以把多个数据库操作的表达式作为一个事务,在操作完成后调 用commit()来进行整体提交,倘若其中一个表达式操作失败,都不会执行到commit(),并且将产生响应的异常;此时就可以在异常捕获时调用 rollback()进行回滚。这样做可以保持多次更新操作后,相关数据的一致性,示例如下: 

    try { 

conn = 

DriverManager.getConnection    

("jdbc:oracle:thin:@host:1521:SID","username","userpwd"; 

       conn.setAutoCommit(false);//禁止自动提交,设置回滚点 

       stmt = conn.createStatement(); 

stmt.executeUpdate(“alter table …”); //数据库更新操作1 

stmt.executeUpdate(“insert into table …”); //数据库更新操作2 

       conn.commit(); //事务提交 

     }catch(Exception ex) {    

         ex.printStackTrace(); 

         try { 

          conn.rollback(); //操作不成功则回滚 

          }catch(Exception e) { 

e.printStackTrace(); 

           } 

} 

JDBC 事务的一个缺点是事务的范围局限于一个数据库连接。一个 JDBC 事务不能跨越多个数据库。 
JTA事务 
JTA(Java Transaction API) 为  平台提供了分布式事务服务。 
要用 JTA 进行事务界定,应用程序要调用 javax.transaction.UserTransaction 接口中的方法。例如: 
utx.begin(); 
      // ... 
      DataSource ds = obtainXADataSource(); 
      Connection conn = ds.getConnection(); 
      pstmt = conn.prepareStatement("UPDATE MOVIES ..."); 
      pstmt.setString(1, "Spinal Tap"); 
      pstmt.executeUpdate(); 
      // ... 
      utx.commit(); 

让我们来关注下面的话: 
“用 JTA 界定事务,那么就需要有一个实现 javax.sql.XADataSource 、 javax.sql.XAConnection 和 javax.sql.XAResource 接口的 JDBC 驱动程序。一个实现了这些接口的驱动程序将可以参与 JTA 事务。一个 XADataSource 对象就是一个 XAConnection 对象的工厂。 XAConnection s 是参与 JTA 事务的 JDBC 连接。” 
要使用JTA事务,必须使用XADataSource来产生数据库连接,产生的连接为一个XA连接。 
XA连接(javax.sql.XAConnection)和非XA(java.sql.Connection)连接的区别在于:XA可以参与JTA的事务,而且不支持自动提交。 
注意: 
Oracle, Sybase, DB2, SQL Server等大型数据库才支持XA, 支持分布事务。 
My SQL 连本地都支持不好,更别说分布事务了。 
JTA方式的实现过程: 
   用XADataSource产生的XAConnection它扩展了一个getXAResource()方法,事务通过这个方法把它加入到事务容器中进行 管理.对于调用者来说,根本看不到事务是如果管理的,你只要声明开始事务,告诉容器我下面的操作要求事务参与了,最后告诉事务说到这儿可以提交或回滚了, 别的都是。 
在使用JTA之前,你必须首先实现一个Xid类用来标识事务(在普通情况下这将由程序来处理)。Xid bao含三个元素:formatID、gtrid(全局事务标识符)和bqual(分支修饰词标识符)。 
下面的例子说明Xid的实现: 

import javax.transaction.xa.*; 
public class MyXid implements Xid 
{ 
protected int formatId; 
protected byte gtrid[]; 
protected byte bqual[]; 
public MyXid() 
{ 
} 
public MyXid(int formatId, byte gtrid[], byte bqual[]) 
{ 
this.formatId = formatId; 
this.gtrid = gtrid; 
this.bqual = bqual; 
} 

public int getFormatId() 
{ 
return formatId; 
} 

public byte[] getBranchQualifier() 
{ 
return bqual; 
} 

public byte[] getGlobalTransactionId() 
{ 
return gtrid; 
} 

} 
其次,你需要创建一个你要使用的数据库的数据源: 
public DataSource getDataSource() 
throws SQLException 
{ 
SQLServerDataSource xaDS = new 
com.merant.datadirect.jdbcx.sqlserver.SQLServerDataSource(); 
xaDS.setDataSourceName("SQLServer"); 
xaDS.setServerName("server"); 
xaDS.setPortNumber(1433); 
xaDS.setSelectMethod("cursor"); 
return xaDS; 
} 

例1 这个例子是用“两步提交协议”来提交一个事务分支: 
XADataSource xaDS; 
XAConnection xaCon; 
XAResource xaRes; 
Xid xid; 
Connection con; 
Statement stmt; 
int ret; 
xaDS = getDataSource(); 
xaCon = xaDS.getXAConnection("jdbc_user", "jdbc_password"); 
xaRes = xaCon.getXAResource(); 
con = xaCon.getConnection(); 
stmt = con.createStatement(); 
xid = new MyXid(100, new byte[]{0x01}, new byte[]{0x02}); 
try { 
xaRes.start(xid, XAResource.TMNOFLAGS); 
stmt.executeUpdate("insert into test_table values (100)"); 
xaRes.end(xid, XAResource.TMSUCCESS); 
ret = xaRes.prepare(xid); 
if (ret == XAResource.XA_OK) { 
xaRes.commit(xid, false); 
} 
} 
catch (XAException e) { 
e.printStackTrace(); 
} 
finally { 
stmt.close(); 
con.close(); 
xaCon.close(); 
} 
当然,实际过程中,我们不需要写这些代码,这些代码是JTA最终的实现代码。 
关于“两步提交协议”,可以参看下面的文章: 


两阶段提交(Two-Phase-Commit)协议 

首先,两阶段提交(Two-Phase-Commit)事务的启动与常规的单阶段提交(One-Phase-Commit)事务类似。接着,应用程序/客 户机对该两阶段提交(Two-Phase-Commit)操作中所涉及的所有数据库执行其修改工作。现在,在最终提交该事务之前,客户机通知参与的数据库准备提交(第 1 阶段)。如果客户机从数据库收到一条“okay”,就发出命令向数据库提交该事务(第 2 阶段)。最后分布式事务(Distributed Transaction)结束。 

上面的例子演示了如何在 Java 中使用 JTA 实现两阶段提交(Two-Phase-Commit)协议。在该应用程序中,如果一个事务分支报告了错误,您就要负责进行错误处理。但是“两阶段提交协议 简介”小节中提到仍然存在一个问题,那就是如果第 2 阶段中一个事务分支发生故障,该怎么办呢? 

如果再次查看程序代码,您可以看到在“第 1 阶段”和“第 2 阶段”之间有一个很小的时间间隔。在这一时间间隔中,出于某种理由,其中某一参与数据库可能崩溃。如果发生了,我们将陷入分布式事务已经部分提交的情形中。 

假 定下列情形:在“第 1 阶段”之后,您从 DB2 和 IDS 数据库中都收到了“okay”。在下一步中,应用程序成功提交了 DB2 的事务分支。接着,应用程序通知 DB2 事务分支提交事务。现在,在应用程序可以通知 IDS 事务分支提交它这一部分之前,IDS 引擎由于断电发生崩溃。这就是一种部分提交全局事务的情形。您现在该怎么办呢? 

在重启之后,DB2 和 IDS 都将尝试恢复打开的事务分支。该引擎等待来自应用程序的提示如何做。如果应用程序没有准备重新发送“第 2 阶段”的提交,该事务分支将被引擎所启动的试探性回滚中止。这是非常糟糕的,因为这将使该全局事务处于不一致状态。 

一种解决方案是用一个小型应用程序连接引擎中打开的事务分支,并通知引擎提交或回滚这一打开的事务。如果您使用 IDS 作为后端,那么还有一个隐藏的 onmode 标志,允许您结束打开的事务分支。(onmode -Z xid)。 

在 DB2 UDB 中,您可以发出 LIST INDOUBT TRANSACTIONS 来获得打开的 XA 事务的有关信息。您必须查看 DB2 Information Center 中的描述来解决该问题。 

上面描述的情形是一个很好的例子,也是使用应用程序服务器(Application Server)或事务监控器(Transaction Monitor)的理由。在使用一个中间层服务器时,就由该服务器负责保持事情正常。


转载于:https://my.oschina.net/u/2458458/blog/667333

你可能感兴趣的:(诡异的hibernate和jdbc混用bug,局部事务-----------------记录一下)