事务管理

本文档详细介绍 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 事务需要:

  1. 副本集或分片集群配置
  2. MongoDB 4.0+ 版本
  3. 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);
    }
}

下一步