事务已关闭无法提交(500 错误)
突然遭遇了 HTTP 500 内部服务器错误,日志中明确提示 Cannot commit, transaction is already closed(无法提交,事务已关闭)。排查后发现,问题出在最基础的 DAO 层代码逻辑上
一、问题复现:转账功能触发 500 错误
1. 错误现象
开发银行转账核心功能时,前端发起转账请求后直接返回 500 错误,服务器日志关键信息如下:
HTTP Status 500 - Internal Server Error
Root Cause: org.apache.ibatis.executor.ExecutorException: Cannot commit, transaction is already closed
错误堆栈指向 DAO 层的 selectAccountByActno 和 updateByActbo 方法,正是这两个查询和更新账户的核心方法出了问题。
2. 错误代码(DAO 层)
当时写的 DAO 层代码如下,看似完成了查询和更新功能
package com.powernode.bank.dao.impl;import com.powernode.bank.pojo.Account;
import com.powernode.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;public class AccountDao implements com.powernode.bank.dao.AccountDao {@Overridepublic Account selectAccountByActno(String actno) {// 每个方法独立创建SqlSessionSqlSession sqlSession = SqlSessionUtil.openSession();Account account = sqlSession.selectOne("account.selectByActno", actno);// 先关闭SqlSession,再提交事务sqlSession.close();sqlSession.commit();return account;}@Overridepublic int updateByActbo(Account account) {SqlSession sqlSession = SqlSessionUtil.openSession();int count = sqlSession.update("account.updateByActno", account);// 同样犯了“先关闭后提交”的错误sqlSession.close();sqlSession.commit();return count;}
}
2 个致命错误导致事务崩溃
1. 顺序颠倒:先关闭 SqlSession,再提交事务(最直接原因)
MyBatis 中,SqlSession 是事务的载体,其生命周期与事务强绑定:
sqlSession.close():关闭会话时,会自动终止当前事务(无论是否提交),释放数据库连接;- 错误代码中先执行
close(),再调用commit()—— 此时事务已被终止,自然会触发 “事务已关闭无法提交” 的异常。
这就像 “先关火再炒菜”,完全违背了操作逻辑,是最基础也最致命的语法顺序错误。
2. 事务粒度错误:DAO 层独立管理 SqlSession,破坏事务原子性
转账业务的核心要求是 “原子性”:查询转出账户、扣款、查询转入账户、入账,这 4 个操作必须在同一个事务中(要么全成功,要么全失败)。
但错误代码中,每个 DAO 方法都独立创建 SqlSession、关闭、提交 —— 意味着每个 DAO 操作都是一个独立事务:
- 查询转出账户是 “事务 1”(已关闭);
- 扣款是 “事务 2”(已关闭);
- 入账是 “事务 3”(已关闭);
- 后果:若扣款成功后入账失败,“事务 2” 已提交无法回滚,会导致用户余额减少但对方未到账的严重 bug,同时重复的关闭 + 提交操作会让事务状态彻底混乱。
三、解决方案:重构代码,统一事务管理
核心修复思路:事务管理移至 Service 层,DAO 层只负责执行 SQL,不管理 SqlSession 生命周期。让一个业务(如转账)共用一个 SqlSession,确保事务统一。
第一步:修正 DAO 层 —— 只执行 SQL,不管理事务
DAO 层的职责是 “数据访问”,不应涉及事务控制。修改后去掉 close() 和 commit(),由 Service 层传入 SqlSession,保证所有操作共用一个会话:
package com.powernode.bank.dao.impl;import com.powernode.bank.pojo.Account;
import org.apache.ibatis.session.SqlSession;public class AccountDao implements com.powernode.bank.dao.AccountDao {// 新增SqlSession参数,由Service层传入@Overridepublic Account selectAccountByActno(SqlSession sqlSession, String actno) {// 只执行查询SQL,不创建、不关闭、不提交SqlSessionreturn sqlSession.selectOne("account.selectByActno", actno);}@Overridepublic int updateByActbo(SqlSession sqlSession, Account account) {// 只执行更新SQL,不管理事务return sqlSession.update("account.updateByActno", account);}
}
第二步:完善 SqlSession 工具类 —— 确保事务手动控制
SqlSessionUtil 需提供 “不自动提交” 的 SqlSession(MyBatis 默认不自动提交),让 Service 层手动控制提交 / 回滚:
package com.powernode.bank.utils;import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;import java.io.IOException;
import java.io.InputStream;public class SqlSessionUtil {private static SqlSessionFactory sqlSessionFactory;// 静态代码块初始化SqlSessionFactorystatic {try {InputStream is = Resources.getResourceAsStream("mybatis-config.xml");sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);} catch (IOException e) {throw new RuntimeException("MyBatis配置文件加载失败", e);}}// 打开会话(false=不自动提交,手动控制事务)public static SqlSession openSession() {return sqlSessionFactory.openSession(false);}
}
第三步:Service 层统一管理事务 —— 保证业务原子性
Service 层是业务逻辑层,负责组合 DAO 操作并统一控制事务。以转账业务为例,用 try-catch-finally 确保:
- 无异常:提交事务;
- 有异常:回滚事务;
- 最终:关闭 SqlSession 释放资源。
package com.powernode.bank.service.impl;import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.dao.impl.AccountDaoImpl;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import com.powernode.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;public class AccountServiceImpl implements AccountService {private AccountDao accountDao = new AccountDaoImpl();@Overridepublic void transfer(String fromActno, String toActno, double amount) {SqlSession sqlSession = null;try {// 1. 统一获取SqlSession(整个转账业务共用一个会话=同一个事务)sqlSession = SqlSessionUtil.openSession();// 2. 校验转出账户Account fromAccount = accountDao.selectAccountByActno(sqlSession, fromActno);if (fromAccount == null) {throw new RuntimeException("转出账户不存在!");}if (fromAccount.getBalance() < amount) {throw new RuntimeException("余额不足!");}// 3. 转出账户扣款fromAccount.setBalance(fromAccount.getBalance() - amount);accountDao.updateByActbo(sqlSession, fromAccount);// 4. 校验转入账户Account toAccount = accountDao.selectAccountByActno(sqlSession, toActno);if (toAccount == null) {throw new RuntimeException("转入账户不存在!");}// 5. 转入账户入账toAccount.setBalance(toAccount.getBalance() + amount);accountDao.updateByActbo(sqlSession, toAccount);// 6. 无异常提交事务sqlSession.commit();System.out.println("转账成功!");} catch (Exception e) {// 7. 有异常回滚事务(关键:保证原子性)if (sqlSession != null) {sqlSession.rollback();}System.out.println("转账失败:" + e.getMessage());throw e; // 向上抛出异常,便于前端处理} finally {// 8. 最终关闭SqlSession(释放资源,必须在finally中执行)if (sqlSession != null) {sqlSession.close();}}}
}
四、测试验证:事务正常工作
- 正常场景:转出账户余额充足、转入账户存在时,转账成功,两个账户余额正确更新,事务提交。
- 异常场景:
- 转出账户不存在 / 余额不足:事务回滚,账户余额无变化;
- 入账时抛出异常:扣款操作会随事务回滚,不会出现 “单边账”。
- 错误日志消失:不再出现 “事务已关闭无法提交” 的 500 错误,功能稳定运行。
五、踩坑总结:3 个关键经验
-
事务管理层级:Service 层负责,DAO 层不插手DAO 层的核心职责是 “执行 SQL”,事务控制必须放在 Service 层 —— 因为 Service 层是业务逻辑的聚合点,能确保多个 DAO 操作在同一个事务中,保证原子性。
-
SqlSession 生命周期:先提交 / 回滚,再关闭牢记 MyBatis 事务操作顺序:
获取SqlSession → 执行业务 → 提交/回滚 → 关闭SqlSession,关闭后绝对不能再操作事务。 -
事务原子性:核心业务必须共用一个 SqlSession涉及多步数据库操作的业务(如转账、下单支付),必须让所有操作共用一个 SqlSession,否则无法保证 “要么全成,要么全败”,容易出现数据一致性问题。

