常规调用
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
methodA{
……
try{
methodB();
} catch (Exception e) {
……
}
localA();
……
}
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
methodB{
……
}
methodA调用methodB时,没有用try-catch语句包住,事务提交/回滚的结果如下:
场景 | 同一个类 | 非同一个类 |
---|---|---|
localA异常 | 回滚 | 回滚 |
methodB捕获异常/无异常 | 提交 | 提交 |
methodB抛异常 | 回滚 | 回滚 |
methodA调用methodB时,用try-catch语句包住,事务提交/回滚的结果如下:
场景 | 同一个类 | 非同一个类 |
---|---|---|
localA异常 | 回滚 | 回滚 |
methodB捕获异常/无异常 | 提交 | 提交 |
methodB抛异常 | 提交 | 回滚 |
这里要重点强调下两个method是否写在同一个类中的区别。
笔者这里是通过在配置文件中添加<tx:annotation-driven/>
来支持事务注解的,该标签会查找和它在相同的应用上下文件中定义的bean上面的@Transactional注解。打上@Transactional注解的类会生成一个代理类,调用时会被事务切面锁拦截。而如果写在同一个类中,methodA是通过this指针调用methodB,这样不会被事务切面所拦截的。
一、当事务方法被切面拦截后,是如何开启Spring事务的呢?
createTransactionIfNecessary方法会根据事务属性有需要地创建一个事务。实际的事务创建是在doBegin方法中,
/**
* This implementation sets the isolation level but ignores the timeout.
*/
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = null;
try {
if (txObject.getConnectionHolder() == null ||
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
// 创建Connection
Connection newCon = this.dataSource.getConnection();
if (logger.isDebugEnabled()) {
logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
}
// 根据Connection创建ConnectionHolder
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}
txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
con = txObject.getConnectionHolder().getConnection();
Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
txObject.setPreviousIsolationLevel(previousIsolationLevel);
// Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
// so we don't want to do it unnecessarily (for example if we've explicitly
// configured the connection pool to set it already).
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
if (logger.isDebugEnabled()) {
logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
}
//将自动提交改为false
con.setAutoCommit(false);
}
txObject.getConnectionHolder().setTransactionActive(true);
int timeout = determineTimeout(definition);
if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
}
// Bind the session holder to the thread.
if (txObject.isNewConnectionHolder()) {
// 重头戏在这里,将ConnectionHolder绑定到当前线程
TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder());
}
}
catch (Throwable ex) {
if (txObject.isNewConnectionHolder()) {
DataSourceUtils.releaseConnection(con, this.dataSource);
txObject.setConnectionHolder(null, false);
}
throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
}
}
创建Connection后,通过bindResource方法,将ConnectionHolder相关信息存入ThreadLocal对象中,这样ConnectionHolder和线程就绑定起来了。
public static void bindResource(Object key, Object value) throws IllegalStateException {
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
Assert.notNull(value, "Value must not be null");
Map<Object, Object> map = resources.get();
// set ThreadLocal Map if none found
if (map == null) {
map = new HashMap<Object, Object>();
resources.set(map);
}
Object oldValue = map.put(actualKey, value);
// Transparently suppress a ResourceHolder that was marked as void...
if (oldValue instanceof ResourceHolder && ((ResourceHolder) oldValue).isVoid()) {
oldValue = null;
}
if (oldValue != null) {
throw new IllegalStateException("Already value [" + oldValue + "] for key [" +
actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]");
}
if (logger.isTraceEnabled()) {
logger.trace("Bound value [" + value + "] for key [" + actualKey + "] to thread [" +
Thread.currentThread().getName() + "]");
}
}
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<Map<Object, Object>>("Transactional resources");
我们查看下resources.get()中的数据,
{
CreateTime:"2017-08-21 11:36:04",
ActiveCount:1,
PoolingCount:2,
CreateCount:3,
DestroyCount:0,
CloseCount:0,
ConnectCount:1,
Connections:[
{ID:103750239, ConnectTime:"2017-08-21 11:36:05", UseCount:0, LastActiveTime:"2017-08-21 11:36:05"},
{ID:1397252899, ConnectTime:"2017-08-21 11:36:05", UseCount:0, LastActiveTime:"2017-08-21 11:36:05"}
]
}
二、如果没有Spring事务托管,仅通过mybatis框架是如何管理事务的呢?
mybatis框架本身也可以做事务管理,和Spring管理事务类似,mybatis将sessionHolder保存在ThreadLocal对象中,这样sessionHolder和线程也绑定了起来。
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
// 从ThreadLocal对象中获取SqlSessionHolder
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Creating a new SqlSession");
}
// 如果不存在SqlSessionHolder,则执行openSession创建session
session = sessionFactory.openSession(executorType);
// 将session存入ThreadLocal对象中
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
三、spring事务管理+mybatis事务管理,ThreadLocal对象中的数据如何?
{{
CreateTime:"2017-08-21 10:54:03",
ActiveCount:1,
PoolingCount:2,
CreateCount:3,
DestroyCount:0,
CloseCount:0,
ConnectCount:1,
Connections:[
{ID:748532070, ConnectTime:"2017-08-21 10:54:03", UseCount:0, LastActiveTime:"2017-08-21 10:54:03"},
{ID:250688639, ConnectTime:"2017-08-21 10:54:03", UseCount:0, LastActiveTime:"2017-08-21 10:54:03"}
]
}=org.springframework.jdbc.datasource.ConnectionHolder@44df9749, org.apache.ibatis.session.defaults.DefaultSqlSessionFactory@4e44f87e=org.mybatis.spring.SqlSessionHolder@a8dab8e}
我们看到,resources.get()中的数据不仅包含了ConnectionHolder,又包含了SqlSessionHolder。而SqlSession又封装了Connection信息,我们认为ConnectionHolder和SqlSessionHolder都指向了同一个数据库连接。
四、我们来分析一下methodA在同一个类中调用methodB、methodB抛出异常的场景
methodA会开启Spring事务,methodB不走Spring事务,但methodB会从ThreadLocal对象中获取SqlSession信息,和methodA拥有同一个数据库连接。
(1) 如果methodA用try/catch语句块包住了methodB,
既然methodA会捕获异常,那么代码执行完毕会执行commitTransactionAfterReturning方法,最终会提交数据。
(2) 如果methodA没有用try/catch语句块包住了methodB,
那么代码执行完毕会执行completeTransactionAfterThrowing方法,最终会回滚数据。
五、我们再分析下methodA调用另一个类中的methodB方法、methodB捕获异常/无异常的场景
根据Spring事务传播性,methodA和methodB会加入同一个事务,由于执行过程无异常,methodA和methodB都会执行commitTransactionAfterReturning方法,但只会提交一次。
private void processCommit(DefaultTransactionStatus status) throws TransactionException {
try {
boolean beforeCompletionInvoked = false;
try {
prepareForCommit(status);
triggerBeforeCommit(status);
triggerBeforeCompletion(status);
beforeCompletionInvoked = true;
boolean globalRollbackOnly = false;
if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) {
globalRollbackOnly = status.isGlobalRollbackOnly();
}
if (status.hasSavepoint()) {
if (status.isDebug()) {
logger.debug("Releasing transaction savepoint");
}
status.releaseHeldSavepoint();
}
// 新事务才会执行doCommit方法
else if (status.isNewTransaction()) {
if (status.isDebug()) {
logger.debug("Initiating transaction commit");
}
doCommit(status);
}
// Throw UnexpectedRollbackException if we have a global rollback-only
// marker but still didn't get a corresponding exception from commit.
if (globalRollbackOnly) {
throw new UnexpectedRollbackException(
"Transaction silently rolled back because it has been marked as rollback-only");
}
}
....
methodB率先执行commitTransactionAfterReturning方法,但是methodB并未开启新事务,是加入到methodA创建的事务的里,这样就无法执行doCommit方法。methodA再执行commitTransactionAfterReturning方法的时候,status.isNewTransaction()判断为真,执行doCommit方法,就将事务整体提交了。
新开线程调用
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
methodA{
……
new Thread(new Runnable() {
public void run() {
methodB();
}
}).start();
localA();
……
}
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
methodB{
……
}
场景 | 同一个类 | 非同一个类 |
---|---|---|
localA异常 | A回滚,B提交 | A回滚,B提交 |
methodB捕获异常/无异常 | 提交 | 提交 |
methodB抛异常 | 提交 | A提交,B回滚 |
这个场景的结果我们不用多分析了。既然事务信息是保存在ThreadLocal对象里,新开线程调用其它方法,不同线程维护各自的数据库连接,这必然无法保证事务一致。
新开事务
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
methodA{
……
methodB();
localA();
……
}
@Transactional(propagation = Propagation.PROPAGATION_REQUIRES_NEW, rollbackFor = Exception.class)
methodB{
……
}
场景 | 同一个类 | 非同一个类 |
---|---|---|
localA异常 | 回滚 | A回滚,B提交 |
methodB捕获异常/无异常 | 提交 | 提交 |
methodB抛异常 | 回滚 | 回滚 |
同时更新同一条数据 | methodA更新值为准 | 死锁 |
一、新开事务后,数据库连接是不是真的不一样?
我们来追踪下ThreadLocal对象中的值的变化。
(1) methodA
依次执行bindResource
和registerSessionHolder
后,ThreadLocal对象中的值如下:
{org.apache.ibatis.session.defaults.DefaultSqlSessionFactory@6af9383b=org.mybatis.spring.SqlSessionHolder@35ccbc0b, {
CreateTime:"2017-08-21 13:45:15",
ActiveCount:1,
PoolingCount:2,
CreateCount:3,
DestroyCount:0,
CloseCount:0,
ConnectCount:1,
Connections:[
{ID:1405383256, ConnectTime:"2017-08-21 13:45:15", UseCount:0, LastActiveTime:"2017-08-21 13:45:15"},
{ID:1808673804, ConnectTime:"2017-08-21 13:45:15", UseCount:0, LastActiveTime:"2017-08-21 13:45:15"}
]
}=org.springframework.jdbc.datasource.ConnectionHolder@3f3d370a}
(2) methodB
由于methodB的传播性是PROPAGATION_REQUIRES_NEW
,根据源码,其先后执行
doGetTransaction
suspend //挂起当前事务
unbindResource //将methodA绑定在线程上数据库连接信息解绑,也就是resources.get()为null
doBegin //新开启事务
bindResource //将methodB的ConnectionHolder绑定在当前线程上
registerSessionHolder //将methodB的SqlSessionHolder绑定在当前线程上
ThreadLocal对象中的值如下:
{org.apache.ibatis.session.defaults.DefaultSqlSessionFactory@6af9383b=org.mybatis.spring.SqlSessionHolder@334a8b18, {
CreateTime:"2017-08-21 13:45:15",
ActiveCount:2,
PoolingCount:1,
CreateCount:3,
DestroyCount:0,
CloseCount:0,
ConnectCount:2,
Connections:[
{ID:1405383256, ConnectTime:"2017-08-21 13:45:15", UseCount:0, LastActiveTime:"2017-08-21 13:45:15"}
]
}=org.springframework.jdbc.datasource.ConnectionHolder@78c36125}
大家看,明显这两个方法的数据库连接是不一样的,比如ActiveCount、PoolingCount等。接下来,methodB将继续执行:
commitTransactionAfterReturning //无异常,执行commitTransactionAfterReturning
doCommit //提交当前事务
cleanupAfterCompletion
methodB执行完毕,methodA继续执行,
resume //还原methodA被挂起时的数据库连接信息
bindResource //绑定到当前线程
ThreadLocal对象中的值如下:
{org.apache.ibatis.session.defaults.DefaultSqlSessionFactory@6af9383b=org.mybatis.spring.SqlSessionHolder@35ccbc0b, {
CreateTime:"2017-08-21 13:45:15",
ActiveCount:1,
PoolingCount:2,
CreateCount:3,
DestroyCount:0,
CloseCount:1,
ConnectCount:2,
Connections:[
{ID:1405383256, ConnectTime:"2017-08-21 13:45:15", UseCount:0, LastActiveTime:"2017-08-21 13:45:15"},
{ID:1808673804, ConnectTime:"2017-08-21 13:45:15", UseCount:1, LastActiveTime:"2017-08-21 13:56:24"}
]
}=org.springframework.jdbc.datasource.ConnectionHolder@3f3d370a}
最后,methodA执行commitTransactionAfterReturning
和doCommit
等方法,提交事务。
二、重点分析一下methodA调用非同一个类中的methodB方法,同时这两个方法会更新同一条数据的情况
methodB新开事务时,methodA对应的事务被挂起。新事务执行完毕才会执行老的事务。而oracle在更新数据时,会把该行记录锁住,老事务迟迟不提交,新事务又无法获得锁,因此一直等待。这就会出现死锁的情况。
不开事务的方法调用开事务的方法
以ClassA中不带事务注解的methodA调用ClassB中带事务注解的methodB、methodB抛出异常为例,
(1) methodA
(2) methodB
看起来methodA和methodB的数据库连接是一样的,其实是methodA提交完了事务,将数据库连接释放后、methodB又从数据库连接池取得了相同的那个数据库连接。但它们并不在同一个数据库连接里,不能保持事务统一。methodA提交完毕,methodB回滚。