数据库事务处理
在现代应用开发中,确保数据的完整性和一致性至关重要。数据库事务(Transaction)是实现这一目标的核心机制。它将一系列数据库操作捆绑成一个不可分割的逻辑工作单元,确保这些操作要么全部成功执行,要么在出现任何错误时全部回滚,使数据恢复到操作前的状态。
本文将详细介绍 ActiveRecord 框架中支持的三种主流事务处理方式,并提供最佳实践建议,帮助您根据不同场景选择最合适的方案。
1. 编程式事务:Db.tx
(官方推荐)
Db.tx
是框架官方首推的事务处理方式。它通过 Lambda 表达式将事务代码块包裹起来,实现了代码量最小化、控制粒度最细的事务管理。
基本用法
Db.tx
的核心优势在于其简洁的语法和直观的控制逻辑。
Db.tx(() -> {
// 业务操作1:更新 t1 表
Db.update("update t1 set f1 = ?", 123);
// 业务操作2:更新 t2 表
Db.update("update t2 set f2 = ?", 456);
// 返回 true 表示所有操作成功,提交事务
return true;
});
在上述代码中:
- 两个
Db.update
操作被自动包含在同一个事务中。 - Lambda 表达式的返回值直接控制事务的最终状态:
return true;
:提交事务,所有数据更改将永久生效。return false;
:回滚事务,所有数据更改将被撤销。
- 这种方式无需抛出异常即可优雅地回滚事务,非常适合处理复杂的业务逻辑校验。
指定数据源与事务级别
Db.tx
方法同样支持对多数据源和事务隔离级别的精细控制。
操作非主数据源:若要对其他数据源开启事务,只需在调用
tx
方法前使用Db.use(configName)
指定数据源。Db.use("other_ds").tx(() -> { // 这里的数据库操作将作用于名为 "other_ds" 的数据源 return true; });
指定事务隔离级别:
Db.tx
方法提供了一个重载版本,允许传入 JDBC 定义的事务隔离级别。Db.tx(Connection.TRANSACTION_SERIALIZABLE, () -> { // 此事务块内的所有操作将在“可串行化”隔离级别下执行 Db.update(...); new User().setNickName("james").save(); return true; });
这对于需要灵活应对不同并发场景的应用来说非常实用。
注意:请确保您的 MySQL 数据库表使用了支持事务的 InnoDB 存储引擎,MyISAM 引擎不支持事务。
2. 声明式事务:拦截器 (Tx
)
声明式事务通过 AOP(面向切面编程)思想,使用拦截器将事务逻辑与业务代码解耦。开发者只需在 Controller 的 Action 方法或 Service 方法上添加注解或配置,即可实现事务管理。
基本用法
通过 @Before(Tx.class)
注解,可以轻松地为整个 Action 方法添加事务支持。
// 本例仅为示例, 并未严格考虑账户状态等业务逻辑
@Before(Tx.class)
public void trans_demo() {
// 1. 获取转账参数
Integer transAmount = getInt("transAmount");
Integer fromAccountId = getInt("fromAccountId");
Integer toAccountId = getInt("toAccountId");
// 2. 执行转出操作
Db.update("update account set cash = cash - ? where id = ?", transAmount, fromAccountId);
// 模拟一个异常,触发事务回滚
// if (true) throw new RuntimeException("发生未知错误,触发回滚!");
// 3. 执行转入操作
Db.update("update account set cash = cash + ? where id = ?", transAmount, toAccountId);
}
工作原理:Tx
拦截器会捕获其作用范围内抛出的任何 Exception
(及其子类)。一旦捕获到异常,拦截器会自动回滚事务;如果方法正常执行完毕没有抛出异常,则自动提交事务。
异常处理与响应控制
您可以在事务方法内部使用 try-catch
块来控制异常发生时的页面响应,但必须将异常重新抛出,以便外层的 Tx
拦截器能够感知并执行回滚。
@Before(Tx.class)
public void trans() {
try {
service.justDoIt(...);
render("ok.html");
} catch (Exception e) {
// 捕获异常,渲染错误页面
render("error.html");
// 必须重新抛出异常,否则 Tx 拦截器无法感知,事务不会回滚!
throw e;
}
}
多种配置方式
除了 @Before
注解,框架还提供了多种全局配置方式,可以根据方法名、ActionKey 等规则批量添加事务。
public void configInterceptor(Interceptors me) {
// 根据方法名正则表达式,为所有 save 或 update 开头的方法添加事务
me.add(new TxByMethodRegex("(.*save.*|.*update.*)"));
// 根据方法名,为名为 "save" 和 "update" 的方法添加事务
me.add(new TxByMethods("save", "update"));
// 根据 ActionKey 正则表达式,为 /trans/ 开头的 Action 添加事务
me.add(new TxByActionKeyRegex("/trans.*"));
// 为指定的 ActionKey 添加事务
me.add(new TxByActionKeys("/tx/save", "/tx/update"));
}
针对非主数据源
声明式事务默认作用于主数据源。若要使其作用于其他数据源,需使用 @TxConfig
注解。
@TxConfig("otherConfigName") // 指定事务作用于名为 "otherConfigName" 的数据源
@Before(Tx.class)
public void doIt() {
// ... 业务代码 ...
}
3. 手动管理事务
在极少数情况下,例如需要与旧有代码或其他不支持框架事务抽象的 JDBC 操作集成时,您可能需要手动管理事务。这种方式提供了最底层的控制能力,但代码繁琐且极易出错,通常不推荐使用。
讲解与代码分析
手动管理事务遵循标准的 JDBC 事务处理流程:获取连接 -> 关闭自动提交 -> 执行操作 -> 提交或回滚 -> 恢复连接状态 -> 归还连接。
import java.sql.Connection;
import java.sql.SQLException;
import org.junit.Test;
import com.litongjava.db.activerecord.ActiveRecordException;
import com.litongjava.db.activerecord.Config;
import com.litongjava.db.activerecord.Db;
import com.litongjava.db.activerecord.DbPro;
public class DbTransactionTest {
@Test
public void test() {
DbPro db = Db.use(); // 获取默认数据源的 DbPro 实例
Config config = db.getConfig();
Connection conn = null;
boolean oldAutoCommit = true; // 用于保存连接原始的 autoCommit 状态
try {
// 步骤 1: 从连接池获取一个连接
conn = config.getConnection();
// 步骤 2: 关闭自动提交,这是手动开启事务的标志
oldAutoCommit = conn.getAutoCommit();
conn.setAutoCommit(false);
// 步骤 3: 执行一系列数据库操作
// 重点:所有操作必须使用同一个 Connection 对象 (conn)
String fromAccountId = "acc001";
db.update(config, conn, "UPDATE account SET balance = balance - ? WHERE id = ?", 100, fromAccountId);
db.save(config, conn, "INSERT INTO account_log(account_id, change_amt) VALUES(?, ?)", fromAccountId, -100);
// 步骤 4: 如果所有操作成功,手动提交事务
conn.commit();
} catch (Exception e) {
// 步骤 4 (备选): 如果发生任何异常,手动回滚事务
if (conn != null) {
try {
conn.rollback();
} catch (SQLException ex) {
// 记录回滚失败的异常,但通常不向外抛出,避免覆盖原始业务异常
}
}
// 将原始业务异常封装后抛出,方便上层调用者感知
throw new ActiveRecordException(e);
} finally {
// 步骤 5: 无论成功还是失败,都必须执行资源清理
if (conn != null) {
try {
// 恢复连接原始的自动提交状态,避免污染连接池
conn.setAutoCommit(oldAutoCommit);
} catch (SQLException e) {
// 记录异常
} finally {
// 将连接归还给连接池,而不是真正关闭它
config.close(conn);
}
}
}
}
}
核心要点解读:
- 获取连接 (
config.getConnection()
):从框架管理的连接池中获取一个java.sql.Connection
对象。 - 关闭自动提交 (
conn.setAutoCommit(false)
):这是手动事务的起点。默认情况下,JDBC 连接处于自动提交模式,每条 SQL 语句都是一个独立的事务。关闭后,所有后续操作都将处于同一个事务中,直到显式提交或回滚。 - 共享连接:所有参与事务的数据库操作(如
db.update
,db.save
)都必须传入同一个Connection
对象conn
,以确保它们在同一个事务上下文中执行。 - 提交与回滚 (
conn.commit()
,conn.rollback()
):在try
块的末尾成功执行commit()
,在catch
块中捕获异常后执行rollback()
。 finally
块的重要性:这是手动事务管理中最关键也最容易出错的部分。- 恢复
autoCommit
状态:将连接归还连接池之前,必须将其autoCommit
状态恢复原样。否则,这个被“污染”的连接可能会影响后续使用它的代码。 - 归还连接 (
config.close(conn)
):此处的close
并非物理关闭连接,而是将其安全地返回到连接池中,以供重用。忘记归还连接会导致连接池耗尽,是严重的资源泄漏问题。
- 恢复
4. 最佳实践与选择建议
综合以上三种方式,我们给出以下建议:
首选
Db.tx
:- 性能更优:事务代码块范围最小,仅包裹必要的操作,减少了事务持有时间。
- 控制更灵活:可以通过返回值
true/false
控制提交或回滚,无需依赖异常机制,代码逻辑更清晰。 - 代码更简洁:Java 8 的 Lambda 语法让事务代码非常紧凑和优雅。
慎用声明式事务 (
Tx
拦截器):- 虽然声明式事务能让业务代码看起来更“干净”,但它依赖异常来触发回滚,这在某些复杂业务场景下可能不够灵活。
- 当使用
Tx
拦截器时,通常需要配合一个全局的ExceptionInterceptor
来捕获Tx
拦截器抛出的异常,并向客户端返回友好的错误信息(如 JSON 或错误页面),否则用户只会看到一个通用的 500 错误页。
避免手动管理事务:
- 手动管理事务代码冗长、逻辑复杂,且极易因忘记资源清理(如归还连接、恢复状态)而引发严重的性能问题和资源泄漏。仅在无法使用框架提供的高级抽象时,作为最后手段考虑。
5. 事务隔离级别与性能
JDBC 默认的事务隔离级别通常是 Connection.TRANSACTION_READ_COMMITTED
(读已提交)。为了提供更高的数据一致性保障,JFinal 的 ActiveRecordPlugin
默认使用 Connection.TRANSACTION_REPEATABLE_READ
(可重复读)。
虽然可重复读能避免“不可重复读”问题,但它会施加更严格的锁,在高并发、高竞争的场景下(例如秒杀系统中的库存扣减),可能会导致性能下降。
如果您的应用场景允许较低的隔离级别,或者遇到了由锁竞争引发的性能瓶颈,可以通过以下方式调整全局默认的事务隔离级别来提升性能:
public void configPlugin(Plugins me) {
ActiveRecordPlugin arp = new ActiveRecordPlugin(...);
// 将默认事务隔离级别调整为“读已提交”
arp.setTransactionLevel(Connection.TRANSACTION_READ_COMMITTED);
me.add(arp);
}
明智地选择事务隔离级别,是在数据一致性与系统性能之间取得平衡的关键。