事务管理
本文档详细介绍 Unabo 框架的事务管理功能。
目录
事务概述
Unabo 框架支持三种事务管理方式:
| 事务类型 | 适用场景 | 优势 | 劣势 |
|---|---|---|---|
| JDBC 事务 | 纯 Java 项目、多数据源 | 灵活、支持多数据源 | 需要手动管理 |
| Spring 事务 | Spring Boot 项目 | 声明式、自动管理 | 只支持单数据源 |
| MongoDB 事务 | MongoDB 操作 | 原生支持 | 需要副本集配置 |
TIP
- 单数据源项目推荐使用 Spring 事务
- 多数据源项目推荐使用 JDBC 事务
- MongoDB 需要单独配置事务
JDBC 事务
JDBC 事务是最基础的事务管理方式,适用于所有场景。
配置 JDBC 事务工厂
import online.sanen.unabo.api.structure.enums.TransactionFactoryEnum;
Bootstrap bootstrap = Unabo.load("sys", configuration -> {
configuration.setUrl("jdbc:mysql://localhost:3306/test");
configuration.setDriverOption(DriverOption.MYSQL_CJ);
configuration.setUsername("root");
configuration.setPassword("123456");
// 设置事务工厂为 JDBC 事务
configuration.setTransactionFactory(TransactionFactoryEnum.JdbcTransactionFactory);
});
方式一:使用 Unabo.openSession(推荐)
TIP
Unabo.openSession 方法(版本 1.1.7+)提供了更简洁的事务管理方式。
// 单数据源事务
Unabo.openSession(() -> {
// 在这里执行所有数据库操作
bootstrap.query(user).insert();
bootstrap.query(order).insert();
// 如果发生异常,自动回滚
}, bootstrap);
// 多数据源事务(跨数据源事务)
Unabo.openSession(() -> {
// 数据源1操作
bootstrap1.query(user).insert();
// 数据源2操作
bootstrap2.query(order).insert();
// 任意数据源操作失败,所有数据源都会回滚
}, bootstrap1, bootstrap2);
方式二:手动开启事务
try {
// 开启事务
bootstrap.openSession();
// 执行数据库操作
bootstrap.query(user).insert();
bootstrap.query(order).insert();
// 提交事务
bootstrap.commit();
} catch (Exception e) {
try {
// 回滚事务
bootstrap.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
} finally {
// 关闭会话(可选,如果使用 try-with-resources)
// bootstrap.closeSession();
}
事务示例
import online.sanen.unabo.api.Bootstrap;
import online.sanen.unabo.api.condition.C;
import online.sanen.unabo.api.structure.enums.DriverOption;
import online.sanen.unabo.api.structure.enums.TransactionFactoryEnum;
import online.sanen.unabo.sql.factory.Unabo;
public class TransactionExample {
public static void main(String[] args) {
try (Bootstrap bootstrap = Unabo.load("sys", configuration -> {
configuration.setUrl("jdbc:mysql://localhost:3306/test");
configuration.setDriverOption(DriverOption.MYSQL_CJ);
configuration.setUsername("root");
configuration.setPassword("123456");
configuration.setTransactionFactory(TransactionFactoryEnum.JdbcTransactionFactory);
})) {
// 转账操作
transferMoney(bootstrap, 1, 2, 100);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 转账操作(事务示例)
* @param bootstrap Bootstrap 实例
* @param fromId 转出账户ID
* @param toId 转入账户ID
* @param amount 转账金额
*/
public static void transferMoney(Bootstrap bootstrap, int fromId, int toId, double amount) {
try {
// 开启事务
bootstrap.openSession();
// 1. 查询转出账户余额
Map<String, Object> fromAccount = bootstrap.queryTable("account")
.addCondition(C.eq("id", fromId))
.unique()
.orElseThrow(() -> new RuntimeException("转出账户不存在"));
double fromBalance = (double) fromAccount.get("balance");
if (fromBalance < amount) {
throw new RuntimeException("余额不足");
}
// 2. 查询转入账户
Map<String, Object> toAccount = bootstrap.queryTable("account")
.addCondition(C.eq("id", toId))
.unique()
.orElseThrow(() -> new RuntimeException("转入账户不存在"));
// 3. 扣除转出账户余额
fromAccount.put("balance", fromBalance - amount);
bootstrap.queryMap("account", fromAccount).update();
// 4. 增加转入账户余额
double toBalance = (double) toAccount.get("balance");
toAccount.put("balance", toBalance + amount);
bootstrap.queryMap("account", toAccount).update();
// 5. 提交事务
bootstrap.commit();
System.out.println("转账成功!");
} catch (Exception e) {
try {
// 回滚事务
bootstrap.rollback();
System.out.println("转账失败,事务已回滚:" + e.getMessage());
} catch (SQLException e1) {
e1.printStackTrace();
}
}
}
}
Spring 事务
Spring 事务是声明式事务管理,集成 Spring Boot 后使用非常方便。
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
配置 Spring 事务
application.yml 配置
unabo:
enable: true
sql-instances:
- id: sys
driver-option: mysql-cj
datasource-type: hikaricp
url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC
username: root
password: 123456
transaction: SpringManagedTransactionFactory # 配置 Spring 事务工厂
启动类配置
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableTransactionManagement // 启用事务管理
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
WARNING
必须排除 DataSourceAutoConfiguration,否则会与 Unabo 的数据源配置冲突。
使用 @Transactional 注解
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
public class UserService {
@Resource
private Bootstrap bootstrap;
/**
* 转账操作
* @param fromId 转出账户ID
* @param toId 转入账户ID
* @param amount 转账金额
*/
@Transactional // 开启事务
public void transferMoney(int fromId, int toId, double amount) {
// 执行转账操作
// 如果发生异常,Spring 会自动回滚
// 1. 扣除转出账户余额
bootstrap.queryTable("account")
.addCondition(C.eq("id", fromId))
.update(Collections.singletonMap("balance", "balance - " + amount));
// 2. 增加转入账户余额
bootstrap.queryTable("account")
.addCondition(C.eq("id", toId))
.update(Collections.singletonMap("balance", "balance + " + amount));
}
/**
* 只读事务
*/
@Transactional(readOnly = true)
public List<Account> getAllAccounts() {
return bootstrap.queryTable("account").list(Account.class);
}
}
@Transactional 属性
@Transactional(
rollbackFor = Exception.class, // 指定回滚异常
noRollbackFor = BusinessException.class, // 指定不回滚异常
timeout = 30, // 超时时间(秒)
readOnly = true, // 只读事务
isolation = Isolation.READ_COMMITTED, // 隔离级别
propagation = Propagation.REQUIRED // 传播行为
)
public void method() {
// 方法体
}
事务传播行为
| 传播行为 | 说明 |
|---|---|
| REQUIRED | 默认值。如果当前存在事务,则加入该事务;否则创建一个新事务 |
| REQUIRES_NEW | 创建一个新事务,如果当前存在事务,则挂起当前事务 |
| SUPPORTS | 如果当前存在事务,则加入该事务;否则以非事务方式执行 |
| NOT_SUPPORTED | 以非事务方式执行,如果当前存在事务,则挂起当前事务 |
| MANDATORY | 必须在事务中运行,否则抛出异常 |
| NEVER | 以非事务方式运行,如果当前存在事务,则抛出异常 |
| NESTED | 如果当前存在事务,则嵌套事务执行;否则创建一个新事务 |
@Service
public class OrderService {
@Resource
private Bootstrap bootstrap;
@Resource
private UserService userService;
@Transactional
public void createOrder(Order order) {
// 1. 创建订单
bootstrap.query(order).insert();
// 2. 调用用户服务(加入当前事务)
userService.updateUserPoints(order.getUserId(), order.getPoints());
// 3. 调用库存服务(新事务)
inventoryService.deductStock(order.getProductId(), order.getQuantity());
}
}
@Service
public class InventoryService {
@Transactional(propagation = Propagation.REQUIRES_NEW) // 新事务
public void deductStock(int productId, int quantity) {
// 扣减库存(独立事务)
}
}
MongoDB 事务
MongoDB 事务需要副本集配置支持。
配置 MongoDB 事务
application.yml 配置
unabo:
enable: true
nosql-instances:
- id: mongodb
ip: 127.0.0.1
port: 27017
username: root
password: 123456
schema: test
transaction-factory: MongoDBTransactionFactory # 配置 MongoDB 事务工厂
show-log: false
使用 MongoDB 事务
import online.sanen.unabo.nosql.Bootstrap;
import com.mongodb.client.MongoClient;
import com.mongodb.client.ClientSession;
public class MongoTransactionExample {
public void transferDocument(Bootstrap bootstrap, String fromId, String toId) {
try {
// 开启 MongoDB 事务
bootstrap.openSession(() -> {
// 执行 MongoDB 操作
bootstrap.queryTable("account")
.addCondition(C.eq("_id", fromId))
.update(Collections.singletonMap("balance", "balance - 100"));
bootstrap.queryTable("account")
.addCondition(C.eq("_id", toId))
.update(Collections.singletonMap("balance", "balance + 100"));
});
} catch (Exception e) {
System.out.println("事务失败,已回滚:" + e.getMessage());
}
}
}
WARNING
MongoDB 事务需要:
- 副本集或分片集群配置
- MongoDB 4.0+ 版本
- WiredTiger 存储引擎
多数据源事务
当项目中使用多个数据源时,需要特别注意事务管理。
配置多个数据源
// 配置数据源1
Bootstrap bootstrap1 = Unabo.load("db1", configuration -> {
configuration.setUrl("jdbc:mysql://localhost:3306/db1");
configuration.setDriverOption(DriverOption.MYSQL_CJ);
configuration.setUsername("root");
configuration.setPassword("123456");
configuration.setTransactionFactory(TransactionFactoryEnum.JdbcTransactionFactory);
});
// 配置数据源2
Bootstrap bootstrap2 = Unabo.load("db2", configuration -> {
configuration.setUrl("jdbc:mysql://localhost:3306/db2");
configuration.setDriverOption(DriverOption.MYSQL_CJ);
configuration.setUsername("root");
configuration.setPassword("123456");
configuration.setTransactionFactory(TransactionFactoryEnum.JdbcTransactionFactory);
});
跨数据源事务(使用 JDBC 事务)
public void crossDataSourceTransfer(int userId, int orderId, double amount) {
try {
// 使用 Unabo.openSession 管理多数据源事务
Unabo.openSession(() -> {
// 数据源1:更新用户余额
bootstrap1.queryTable("user")
.addCondition(C.eq("id", userId))
.update(Collections.singletonMap("balance", "balance - " + amount));
// 数据源2:创建订单记录
Map<String, Object> order = new HashMap<>();
order.put("user_id", userId);
order.put("amount", amount);
bootstrap2.queryMap("order", order).insert();
// 任意数据源操作失败,所有数据源都会回滚
}, bootstrap1, bootstrap2);
System.out.println("跨数据源事务执行成功!");
} catch (Exception e) {
System.out.println("事务失败:" + e.getMessage());
}
}
WARNING
Spring 事务不支持多数据源同时回滚,多数据源场景必须使用 JDBC 事务或手动管理。
Spring Boot 多数据源事务(不推荐)
如果必须在 Spring Boot 中使用多数据源事务,可以使用编程式事务:
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
@Service
public class CrossDataSourceService {
@Resource
private Bootstrap bootstrap1;
@Resource
private Bootstrap bootstrap2;
@Resource
private PlatformTransactionManager transactionManager1;
@Resource
private PlatformTransactionManager transactionManager2;
public void crossDataSourceOperation() {
TransactionTemplate template1 = new TransactionTemplate(transactionManager1);
TransactionTemplate template2 = new TransactionTemplate(transactionManager2);
try {
// 执行数据源1操作
template1.executeWithoutResult(status -> {
bootstrap1.query(user).insert();
});
// 执行数据源2操作
template2.executeWithoutResult(status -> {
bootstrap2.query(order).insert();
});
} catch (Exception e) {
// 需要手动处理回滚逻辑
System.out.println("操作失败:" + e.getMessage());
}
}
}
事务最佳实践
1. 事务范围尽可能小
// ❌ 不好:事务范围过大
@Transactional
public void largeTransaction() {
// 查询操作(不需要事务)
User user = queryUser(id);
// 远程调用(不应该在事务中)
remoteService.call();
// 文件操作(不应该在事务中)
fileService.write();
// 数据库操作(需要事务)
updateUser(user);
}
// ✅ 好:只对必要的数据库操作使用事务
public void goodTransaction() {
// 查询操作(不需要事务)
User user = queryUser(id);
// 远程调用(不在事务中)
remoteService.call();
// 文件操作(不在事务中)
fileService.write();
// 数据库操作(使用事务)
updateUserInTransaction(user);
}
@Transactional
public void updateUserInTransaction(User user) {
updateUser(user);
}
2. 避免在事务中进行耗时操作
// ❌ 不好:在事务中进行耗时操作
@Transactional
public void badExample() {
// 查询
User user = queryUser(id);
// 耗时计算(不应该在事务中)
Thread.sleep(5000);
// 更新
updateUser(user);
}
// ✅ 好:将耗时操作移到事务外
public void goodExample() {
// 查询
User user = queryUser(id);
// 耗时计算(不在事务中)
Thread.sleep(5000);
// 事务内只执行数据库操作
updateUserInTransaction(user);
}
@Transactional
public void updateUserInTransaction(User user) {
updateUser(user);
}
3. 合理设置事务隔离级别
@Transactional(
isolation = Isolation.READ_COMMITTED // 读已提交(推荐)
)
public void method() {
// 方法体
}
| 隔离级别 | 说明 | 适用场景 |
|---|---|---|
| DEFAULT | 使用数据库默认隔离级别 | 大多数场景 |
| READ_UNCOMMITTED | 读未提交 | 极少使用 |
| READ_COMMITTED | 读已提交 | 推荐使用 |
| REPEATABLE_READ | 可重复读 | 需要一致性读 |
| SERIALIZABLE | 串行化 | 极少使用,性能差 |
4. 正确处理异常
// ❌ 不好:捕获异常但未抛出,事务不会回滚
@Transactional
public void badExample() {
try {
// 数据库操作
updateData();
} catch (Exception e) {
// 只打印日志,不抛出异常
log.error("操作失败", e);
}
}
// ✅ 好:抛出异常,让事务回滚
@Transactional(rollbackFor = Exception.class)
public void goodExample() {
try {
// 数据库操作
updateData();
} catch (Exception e) {
log.error("操作失败", e);
throw e; // 抛出异常,触发回滚
}
}
5. 使用只读事务优化性能
// 只读事务(优化性能)
@Transactional(readOnly = true)
public List<User> queryUsers() {
return bootstrap.queryTable("user").list(User.class);
}
TIP
只读事务可以让数据库进行查询优化,提升性能。
6. 避免长事务
// ❌ 不好:长事务
@Transactional
public void longTransaction() {
for (int i = 0; i < 10000; i++) {
// 循环执行数据库操作
bootstrap.query(item).insert();
}
}
// ✅ 好:分批处理
public void batchProcess() {
int batchSize = 100;
int total = 10000;
for (int i = 0; i < total; i += batchSize) {
processBatch(i, Math.min(i + batchSize, total));
}
}
@Transactional
public void processBatch(int start, int end) {
for (int i = start; i < end; i++) {
bootstrap.query(item).insert();
}
}
完整示例
import online.sanen.unabo.api.Bootstrap;
import online.sanen.unabo.api.condition.C;
import online.sanen.unabo.api.structure.enums.DriverOption;
import online.sanen.unabo.api.structure.enums.TransactionFactoryEnum;
import online.sanen.unabo.sql.factory.Unabo;
import java.util.*;
public class TransactionCompleteExample {
public static void main(String[] args) {
try (Bootstrap bootstrap = Unabo.load("sys", configuration -> {
configuration.setUrl("jdbc:mysql://localhost:3306/test");
configuration.setDriverOption(DriverOption.MYSQL_CJ);
configuration.setUsername("root");
configuration.setPassword("123456");
configuration.setTransactionFactory(TransactionFactoryEnum.JdbcTransactionFactory);
})) {
// 初始化数据
initDatabase(bootstrap);
// 执行转账
transferMoney(bootstrap, 1, 2, 100);
// 查询结果
System.out.println("用户1余额: " + getBalance(bootstrap, 1));
System.out.println("用户2余额: " + getBalance(bootstrap, 2));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 初始化数据库
*/
private static void initDatabase(Bootstrap bootstrap) {
// 创建账户表
Map<String, Object> schema = new HashMap<>();
schema.put("id", 1);
schema.put("name", "");
schema.put("balance", 0.0);
bootstrap.queryMap("account", schema).setPrimary("id").create();
// 插入测试数据
Map<String, Object> account1 = new HashMap<>();
account1.put("name", "张三");
account1.put("balance", 1000.0);
bootstrap.queryMap("account", account1).insert();
Map<String, Object> account2 = new HashMap<>();
account2.put("name", "李四");
account2.put("balance", 500.0);
bootstrap.queryMap("account", account2).insert();
}
/**
* 转账操作(使用 Unabo.openSession)
*/
private static void transferMoney(Bootstrap bootstrap, int fromId, int toId, double amount) {
try {
// 开启事务
Unabo.openSession(() -> {
// 1. 查询转出账户
Map<String, Object> fromAccount = bootstrap.queryTable("account")
.addCondition(C.eq("id", fromId))
.unique()
.orElseThrow(() -> new RuntimeException("转出账户不存在"));
double fromBalance = (double) fromAccount.get("balance");
if (fromBalance < amount) {
throw new RuntimeException("余额不足");
}
// 2. 查询转入账户
Map<String, Object> toAccount = bootstrap.queryTable("account")
.addCondition(C.eq("id", toId))
.unique()
.orElseThrow(() -> new RuntimeException("转入账户不存在"));
// 3. 扣除转出账户余额
fromAccount.put("balance", fromBalance - amount);
bootstrap.queryMap("account", fromAccount).update();
// 4. 增加转入账户余额
double toBalance = (double) toAccount.get("balance");
toAccount.put("balance", toBalance + amount);
bootstrap.queryMap("account", toAccount).update();
}, bootstrap);
System.out.println("转账成功!");
} catch (Exception e) {
System.out.println("转账失败:" + e.getMessage());
}
}
/**
* 查询余额
*/
private static double getBalance(Bootstrap bootstrap, int id) {
return bootstrap.queryTable("account")
.addCondition(C.eq("id", id))
.unique()
.map(account -> (double) account.get("balance"))
.orElse(0.0);
}
}
下一步
- 📖 性能优化 - 学习性能优化技巧
- 📖 高级查询 - 掌握复杂查询
- 📖 Spring Boot 集成 - Spring Boot 集成指南