1. 什么是本地事务?
本地事务是指在单一数据库系统内执行的一组操作,这些操作要么全部完成,要么全部不执行,是一个不可分割的工作单元。本地事务具有ACID特性:
- 原子性(Atomicity):事务中的所有操作都作为一个整体提交或回滚;如果事务的一部分失败,则整个事务都会被撤销。
- 一致性(Consistency):事务将数据库从一个一致状态转换到另一个一致状态,确保数据的完整性和规则得到遵守。
- 隔离性(Isolation):多个并发事务之间相互隔离,一个事务的中间状态对其他事务是不可见的。
- 持久性(Durability):一旦事务成功提交,它对数据库的更改就是永久性的,即使系统出现故障。
2. 使用JDBC进行本地事务管理
2.1. 基本步骤
- 获取数据库连接:使用DriverManager.getConnection()方法获取数据库连接。
- 关闭自动提交:调用connection.setAutoCommit(false)关闭自动提交模式。
- 执行SQL操作:在同一个连接上执行多个SQL语句。
- 提交或回滚事务:根据操作结果调用connection.commit()或connection.rollback()。
- 关闭连接:释放数据库连接资源。
2.2. 示例代码
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class JdbcTransactionExample {
private static final String DB_URL = "jdbc:mysql://localhost:3306/test";
private static final String USER = "root";
private static final String PASS = "root";
public static void main(String[] args) {
Connection conn = null;
PreparedStatement stmt1 = null;
PreparedStatement stmt2 = null;
try {
// 1. 获取数据库连接
conn = DriverManager.getConnection(DB_URL, USER, PASS);
// 2. 关闭自动提交
conn.setAutoCommit(false);
// 3. 执行SQL操作
String sql1 = "INSERT INTO users (name, email) VALUES (?, ?)";
stmt1 = conn.prepareStatement(sql1);
stmt1.setString(1, "John Doe");
stmt1.setString(2, "john.doe@example.com");
stmt1.executeUpdate();
// 模拟异常
// int a = 0/0;
String sql2 = "UPDATE accounts SET balance = balance - 100 WHERE user_id = ?";
stmt2 = conn.prepareStatement(sql2);
stmt2.setInt(1, 1);
stmt2.executeUpdate();
// 4. 提交事务
conn.commit();
System.out.println("Transaction committed successfully.");
} catch (SQLException e) {
if (conn != null) {
try {
// 5. 回滚事务
conn.rollback();
System.out.println("Transaction rolled back.");
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
} finally {
// 6. 关闭连接
try {
if (stmt1 != null) stmt1.close();
if (stmt2 != null) stmt2.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
3. 使用Spring框架进行本地事务管理
3.1. 编程式事务管理
编程式事务管理是指在代码中显式地控制事务的开始、提交和回滚。这种方式提供了更大的灵活性,但也会增加代码的复杂性。
3.1.1. 使用TransactionTemplate进行编程式事务管理
TransactionTemplate是Spring提供的一个类,用于简化编程式事务管理。它封装了事务管理的逻辑,使得开发者可以更方便地控制事务。
3.1.1.1. 配置TransactionTemplate
首先,需要配置一个TransactionTemplate bean。通常在Spring Boot项目中,可以通过DataSourceTransactionManager来配置。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import javax.sql.DataSource;
@Configuration
public class TransactionConfig {
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
// 设置事务隔离级别
// transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
// 设置事务只读
// transactionTemplate.setReadOnly(true);
// 设置传播行为
// transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// 设置超时时间为30秒
// transactionTemplate.setTimeout(30);
return transactionTemplate;
}
}
注意: 在Spring
Boot中,PlatformTransactionManager、TransactionTemplate通常会被JPA、JDBC、MyBatis、Hibernate等常见框架自动配置和注入,所以通常不需要手动配置PlatformTransactionManager、TransactionTemplate。
3.1.2. 使用TransactionTemplate管理事务
在需要事务管理的方法中,注入TransactionTemplate并使用它来控制事务。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private AccountRepository accountRepository;
@Autowired
private TransactionTemplate transactionTemplate;
public void registerUser(User user) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
// 1. 保存用户信息
userRepository.save(user);
// 设置保存点
// Object savepoint = status.createSavepoint();
// ... 其他操作
// 操作失败,回滚到保存点
// status.rollbackToSavepoint(savepoint);
// 2. 更新账户余额
Account account = accountRepository.getById(user.getId());
account.setBalance(account.getBalance() - 100);
accountRepository.updateById(account);
// 释放保存点
// status.releaseSavepoint(savepoint);
} catch (Exception e) {
// 3. 设置事务回滚
status.setRollbackOnly();
e.printStackTrace();
}
}
});
}
}
3.1.2 使用PlatformTransactionManager进行编程式事务管理
除了TransactionTemplate,还可以直接使用PlatformTransactionManager来管理事务。
3.1.2.1 配置PlatformTransactionManager
与前面的配置相同,确保已经配置了PlatformTransactionManager。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
@Configuration
public class TransactionConfig {
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
注意: 在Spring
Boot中,PlatformTransactionManager通常会被JPA、JDBC、MyBatis、Hibernate等常见框架自动配置和注入,所以通常不需要手动配置PlatformTransactionManager。
3.1.2.2 使用PlatformTransactionManager管理事务
在需要事务管理的方法中,注入PlatformTransactionManager并使用它来控制事务。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private AccountRepository accountRepository;
@Autowired
private PlatformTransactionManager transactionManager;
public void registerUser(User user) {
// 1. 定义事务属性
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// 设置传播行为
// def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// 设置隔离级别
// def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
// 设置是否只读
// def.setReadOnly(false);
// 2. 获取事务状态
TransactionStatus status = transactionManager.getTransaction(def);
try {
// 3. 保存用户信息
userRepository.save(user);
// 创建保存点
// Object savepoint = status.createSavepoint();
// 回滚到保存点
// status.rollbackToSavepoint(savepoint);
// 4. 更新账户余额
Account account = accountRepository.getById(user.getId());
account.setBalance(account.getBalance() - 100);
accountRepository.updateById(account);
// 释放保存点
// status.releaseSavepoint(savepoint);
// 5. 提交事务
transactionManager.commit(status);
} catch (Exception e) {
// 6. 回滚事务
transactionManager.rollback(status);
e.printStackTrace();
}
}
}
3.1.3 TransactionTemplate 和 PlatformTransactionManager 区别和联系
特性/功能 | TransactionTemplate | PlatformTransactionManager |
---|---|---|
用途 | 提供模板化的编程方式来管理事务,简化事务管理代码。 | 定义事务管理的基本接口,提供事务管理的核心功能。 |
编程模型 | 编程式事务管理。 | 可以用于编程式和声明式事务管理。 |
使用方式 | 直接在代码中使用 TransactionTemplate 来管理事务。 | 通常通过 TransactionTemplate 或 @Transactional 注解使用。 |
配置属性 | 可以设置隔离级别、传播行为、超时时间、只读属性等。 | 通过 TransactionTemplate 配置,也可以通过其他方式配置。 |
回滚规则 | 通过 TransactionCallback 手动处理异常来控制回滚。 | 通过 TransactionAttribute 配置回滚规则。 |
保存点 | 支持保存点(savepoints),可以部分回滚事务。 | 支持保存点(savepoints),可以部分回滚事务。 |
事务状态管理 | 使用 TransactionStatus 对象来管理事务状态。 | 使用 TransactionStatus 对象来管理事务状态。 |
依赖 | 依赖于 PlatformTransactionManager 。 | 是事务管理的核心接口,TransactionTemplate 依赖于它。 |
灵活性 | 提供更高的灵活性,适合复杂的事务逻辑。 | 提供基本的事务管理功能,适合大多数情况。 |
3.2. 声明式事务管理
Spring框架提供了强大的声明式事务管理,简化了事务处理代码。通过使用@Transactional注解,可以方便地定义方法级别的事务边界。
3.2.1 声明式事务使用
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private AccountRepository accountRepository;
@Transactional
public void registerUser(User user) {
// 1. 保存用户信息
userRepository.save(user);
// 2. 更新账户余额
Account account = accountRepository.getById(user.getId());
account.setBalance(account.getBalance() - 100);
accountRepository.updateById(account);
// 如果抛出运行时异常,事务将回滚
}
}
常用的 @Transactional 参数及其说明:
- propagation
- 类型: Propagation
- 默认值: Propagation.REQUIRED
- 描述: 定义了事务的传播行为,即当前事务与新事务之间的关系。
- 可选值:
- REQUIRED: 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
- SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。
- MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
- REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则挂起当前事务。
- NOT_SUPPORTED: 以非事务方式执行操作,如果当前存在事务,则挂起当前事务。
- NEVER: 以非事务方式执行操作,如果当前存在事务,则抛出异常。
- NESTED: 如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则创建一个新的事务。
- isolation
- 类型: Isolation
- 默认值: Isolation.DEFAULT
- 描述: 定义了事务的隔离级别,决定了事务之间如何相互影响。
- 可选值:
- DEFAULT: 使用底层数据库的默认隔离级别。
- READ_UNCOMMITTED: 最低的隔离级别,允许读取未提交的数据。
- READ_COMMITTED: 只能读取已经提交的数据,不能读取未提交的数据。
- REPEATABLE_READ: 在同一个事务中,多次读取同一数据的结果是一致的。
- SERIALIZABLE: 最高的隔离级别,事务串行执行,完全隔离。
- timeout
- 类型: int
- 默认值: -1
- 描述: 事务的超时时间,单位为秒。如果事务在这个时间内没有完成,则自动回滚。
- readOnly
- 类型: boolean
- 默认值: false
- 描述: 标记当前事务是否为只读事务。对于某些数据库,标记为只读可以提高性能。
- rollbackFor
- 类型: Class<? extends Throwable>[]
- 默认值: {}
- 描述: 指定哪些异常需要触发事务回滚。默认情况下,运行时异常(RuntimeException)和错误(Error)会触发回滚,而检查型异常(Exception)不会触发回滚。
- noRollbackFor
- 类型: Class<? extends Throwable>[]
- 默认值: {}
- 描述: 指定哪些异常不需要触发事务回滚。即使这些异常被抛出,事务也不会回滚。
3.2.2 @Transactional失效场景
该部分转载于 一口气怼完12种@Transactional的失效场景
3.2.2.1 代理不生效
声明式事务是基于 AOP(面向切面编程)代理机制来拦截方法调用,并在方法执行前后进行事务管理。Spring 生成代理的方式主要有两种:JDK
动态代理和CGLIB 代理。
- JDK 动态代理:
- 基本概念:
- 基于接口的代理:JDK 动态代理是基于 Java 的反射机制实现的,它只能代理实现了接口的类。
- 生成代理类:在运行时动态生成一个实现了相同接口的代理类。
- 适用场景:
- 目标类实现了接口:只有当目标类实现了至少一个接口时,才能使用 JDK 动态代理。
- 优点:
- 性能较好:由于基于标准的 Java 反射机制,性能相对较高。
- 符合面向接口编程:符合 Java 的面向接口编程原则。
- 缺点:
- 仅限接口:只能代理实现了接口的类,无法代理没有实现接口的类。
- CGLIB 代理:
- 基本概念:
- 基于类的代理:CGLIB(Code Generation Library)代理是通过生成目标类的子类来实现的,因此可以代理没有实现接口的类。
- 生成代理类:在运行时动态生成一个继承自目标类的代理类。
- 适用场景:
- 目标类没有实现接口:当目标类没有实现任何接口时,只能使用 CGLIB 代理。
- 需要代理类本身:某些情况下需要代理类本身而不是接口。
- 优点:
- 无需接口:可以代理没有实现接口的类。
- 灵活性高:可以代理类本身。
- 缺点:
- 性能稍差:由于需要生成子类,性能相对 JDK 动态代理稍差。
- 无法代理 final 类:CGLIB 无法代理 final 类和 final 方法。
- Spring 如何选择代理方式:
Spring 会根据以下条件自动选择使用 JDK 动态代理还是 CGLIB 代理:
- 默认行为:
- 如果目标类实现了接口,默认使用 JDK 动态代理。
- 如果目标类没有实现接口,默认使用 CGLIB 代理。
- 配置选项:
- proxy-target-class 属性:可以通过配置 proxy-target-class 属性来强制使用 CGLIB 代理。
- proxy-target-class=true:强制使用 CGLIB 代理。
- proxy-target-class=false:强制使用 JDK 动态代理(如果可能)。
- proxy-target-class 属性:可以通过配置 proxy-target-class 属性来强制使用 CGLIB 代理。
3.2.2.1.1 将注解标注在接口方法上
失效原因:
- @Transactional是支持标注在方法与类上的。一旦标注在接口上,对应接口实现类的代理方式如果是CGLIB,将通过生成子类的方式生成目标类的代理,将无法解析到@Transactional,从而事务失效。
解决方案:
- 将@Transactional标注在接口方法上,将接口实现类的代理方式改为JDK动态代理。
- 将@Transactional标注在方法与类上的。
3.2.2.1.2 被final、static关键字修饰的类或方法
失效原因:
- final 方法:对于 final 方法,由于它们不能被子类重写,JDK 动态代理和 CGLIB 代理都无法生成有效的代理逻辑来拦截这些方法。
- static 方法:AOP 代理机制依赖于实例级别的拦截,而 static 方法属于类级别,不是实例的一部分。因此,Spring 的 AOP 代理无法拦截
static 方法。
解决方案:
- 将 final、static 修饰的类方法改为非final、static 修饰。
3.2.2.1.3 类方法内部调用
失效原因:
- 当一个类中的方法直接调用另一个带有 @Transactional 注解的方法时,这个调用是通过 this 引用进行的,而不是通过代理对象。因此,AOP
代理无法拦截到这个调用,事务管理逻辑也不会被触发。
解决方案:
- 将业务逻辑移到非内部调用的方法中:将实际的业务逻辑移到另一个服务类或方法中,避免在同一类中自调用带有 @Transactional
注解的方法。 - 自我注入的方式:通过构造函数注入或字段注入自身实例,确保调用的是代理对象而不是直接通过 this 引用调用。
- 使用代理对象调用:通过AopContext.currentProxy()获取代理对象
AspectJ 解决事务失效:
- 引入依赖
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency>
- 暴露代理对象
在启动类上添加注解@EnableAspectJAutoProxy(exposeProxy=true),暴露代理对象:@Configuration @EnableAspectJAutoProxy(exposeProxy = true) public class AppConfig { }
- 使用代理对象
内部调用失效示例:import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class TransactionalService { @Autowired private AnotherService anotherService; @Transactional public void methodA() { // 一些数据库操作 System.out.println("Method A is running"); methodB(); // 内部方法调用 } @Transactional public void methodB() { // 一些数据库操作 System.out.println("Method B is running"); } }
使用代理对象调用示例:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class TransactionalService { @Autowired private AnotherService anotherService; @Transactional public void methodA() { // 一些数据库操作 System.out.println("Method A is running"); ((TransactionalService) AopContext.currentProxy()).methodB(); // 使用AopContext获取代理对象 } @Transactional public void methodB() { // 一些数据库操作 System.out.println("Method B is running"); } }
3.2.2.1.4 当前类没有被Spring管理
失效原因:
- 如果类不是由 Spring 容器管理的(例如通过 new 关键字直接实例化),那么即使该类的方法上有 @Transactional 注解,事务管理也不会生效。
解决方案:
- 将类注册到 Spring 容器中,使其成为 Spring 管理的 bean。
3.2.2.2 框架或底层不支持的功能
这类失效场景主要聚焦在框架本身在解析@Transactional时的内部支持。如果使用的场景本身就是框架不支持的,那事务也是无法生效的。
3.2.2.2.1 非public修饰的方法
失效原因:
- 默认情况下,Spring 的 AOP 代理只拦截 public 方法。对于非 public 方法(如 private、protected
或包级私有方法),由于方法不可见,代理机制无法拦截这些方法,因此事务管理不会生效。
解决方案:
- 将方法设置为 public。
3.2.2.2.2 多线程调用
在 TransactionAspectSupport.prepareTransactionInfo 方法中
txInfo.bindToThread() 无论是否创建了新的事务,都会将TransactionInfo对象绑定到当前线程。这是为了确保事务信息栈的完整性,即使没有创建新事务。
3.2.2.2.3 数据库本身不支持事务
某些NoSQL数据库或特定版本的关系型数据库,比如Mysql的Myisam存储引擎是不支持事务的。Spring
的声明式事务管理将无法正常工作,因为这些数据库缺乏必要的事务特性(如ACID属性)。
3.2.2.2.4 未开启事务
在MVC项目中还需要在applicationContext.xml文件中,手动配置事务相关参数。如果忘了配置,事务肯定是不会生效的。
3.2.2.3 错误使用@Transactional
3.2.2.3.1 错误的传播机制
失效原因:
- 传播行为不匹配:如果使用了不合适的传播行为,例如 SUPPORTS 或 NOT_SUPPORTED,可能会导致事务管理失效。这些传播行为在某些情况下不会创建或加入事务。
- 方法调用链中的传播行为冲突:在方法调用链中,不同方法可能有不同的传播行为,如果这些行为不兼容,可能会导致事务管理失效。
解决方案:
- 使用合适的传播行为:确保在方法调用链中使用正确的传播行为,以匹配事务的预期行为。
3.2.2.3.2 rollbackFor属性设置错误
失效原因:
- rollbackFor 属性用于指定哪些异常会触发事务回滚。默认情况下,Spring 只会在遇到未检查异常(即继承自 RuntimeException
的异常)时回滚事务。对于已检查异常(即不继承自 RuntimeException 的异常),你需要显式地指定它们以触发回滚。
解决方案:
- 使用 rollbackFor 属性:在方法上添加 rollbackFor 属性,并指定需要触发回滚的异常类型。
3.2.2.3.3 异常被内部 try-catch
失效原因:
- 当异常在方法内部被捕获并处理时,Spring 的事务管理机制无法感知到异常的发生,因此不会触发回滚操作。这会导致事务管理失效,即使你正确配置了
rollbackFor 属性。
解决方案:
- 避免在方法内部处理异常:避免在方法内部处理异常,或者将异常抛出给上层调用者,让上层调用者处理异常。
3.2.2.3.4 嵌套事务
预期场景:
- 外层事务不因内层事务的异常而回滚。
失效原因:
- Spring 的事务管理不支持嵌套事务。当一个方法开始一个新的事务时,如果该方法内部再次调用另一个方法,则该方法将不会创建一个新的事务,而是将使用外层方法的事务。
解决方案:
- 使用 Propagation.REQUIRES_NEW:在内层方法上使用 Propagation.REQUIRES_NEW
传播行为,将创建一个新的事务,而不管外层方法是否已经创建了事务。这样,内层事务与外层事务相互独立,内层事务的异常不会影响外层事务。 - 使用 try-catch:在外层方法调用内层方法时,在外层方法中捕获异常,并在异常处理中回滚事务。