一,前言
这里介绍的事务都是声明式事务,基于spring的@Transactional
注解来实现的。可以更加简洁快速且一致地使用事务。无需显示性地使用编程式事务去管理应用。
二,事务的基本认识
- 事务(Transaction): 是代码的一组操作,这些操作要么全部成功执行,要么全部失败回滚。
- 事务管理(Transaction Management): 管理事务的方式,包括事务的开始、提交、回滚和隔离级别等。
事务的四大特性
事务具有四大特性,通常缩写为 ACID,分别是:
1. 原子性(Atomicity)
原子性指的是事务是一个不可分割的操作单位,事务中的所有操作要么全部提交成功,要么全部回滚失败,不存在部分提交或部分回滚的情况。即使在系统发生故障的情况下,也要保证事务的原子性,以确保数据的一致性。
2. 一致性(Consistency)
一致性指的是事务在完成时,数据的状态必须是一致的。事务开始前和结束后,数据库的完整性约束没有被破坏,例如唯一性约束、外键约束等。如果事务执行成功,则所有相关的数据都应该在事务结束后处于一致的状态。
3. 隔离性(Isolation)
隔离性指的是并发环境下,多个事务之间应该相互隔离,一个事务的执行不应该受到其他事务的干扰。隔离级别越高,事务之间的相互影响越少,但是性能开销也会相应增加。隔离级别包括:读未提交(Read Uncommitted)、读提交(Read Committed)、可重复读(Repeatable Read)、串行化(Serializable)。
4. 持久性(Durability)
持久性指的是事务一旦提交,其结果应该是永久性的,即使系统发生故障也不会丢失提交的数据。通常通过将事务的操作持久化到数据库的持久存储设备(如硬盘)来保证,以确保在系统故障后能够恢复数据。
事务的隔离级别
事务的隔离级别是数据库管理系统(DBMS)中用来控制事务之间相互影响程度的一个重要概念。隔离级别定义了一个事务内部的操作和其他事务之间的可见性和影响范围,从而确保在并发环境下事务的正确性和数据的一致性。在关系数据库中,通常有四种标准的隔离级别,每种级别在实现上有不同的方式来处理并发事务。
1. 读未提交(Read Uncommitted)
- 定义:最低的隔离级别,事务中的修改即使未提交,对其他事务也是可见的。
- 问题:可能导致脏读(Dirty Read),即一个事务可以读取到另一个未提交事务的数据,但是未提交事务最终可能会回滚,这样读取的数据就是无效的。
2. 读提交(Read Committed)
- 定义:一个事务只能读取到已经提交的其他事务的数据,即读取的数据是稳定的。
- 问题:可能导致不可重复读(Non-repeatable Read),即在一个事务内两次读取同一数据,结果不一致,因为其他事务可能在此期间修改了数据并提交。
3. 可重复读(Repeatable Read)
- 定义:确保在同一个事务中多次读取同样的数据时,结果是一致的,即不会受到其他事务的影响。
- 实现方式:通常通过在事务开始时记录一个快照(Snapshot)来实现,以确保后续读取的数据是一致的。
- 问题:仍然可能存在幻读(Phantom Read),即在同一个事务中多次执行同一查询,结果集不一致,因为其他事务插入了新的数据行。
4. 串行化(Serializable)
- 定义:最高的隔离级别,确保每个事务都能独立执行,避免并发问题。
- 实现方式:通过对事务进行严格的串行执行,避免并发执行。
- 问题:性能开销较大,因为可能会引起大量的锁竞争和资源争用,降低系统的并发性能。
选择隔离级别的考虑因素
- 并发性能:隔离级别越高,通常并发性能越差,因为需要更多的锁和资源管理。
- 数据一致性要求:根据业务需求和数据完整性要求选择合适的隔离级别。
- 系统复杂性:较高的隔离级别可能需要更复杂的系统设计和优化,以保证性能和数据一致性的平衡。
当然事务的是否实现最终是由数据库引擎决定的,如MySQL 的 InnoDB 引擎是支持事务的,但 MyISAM 就不支持。
MySQL 默认的隔离级别是 可重复读(Repeatable Read)。
Oracle 默认的隔离级别是 读提交(Read Committed)。
PostgreSQL(通常简称为 pgsql)的默认隔离级别是 读提交(Read Committed)。
事务的传播行为
事务的传播行为(Transaction Propagation)指的是在多个事务方法调用之间定义事务边界和行为的规则。在 Spring 框架中,可以通过 @Transactional
注解的 propagation
属性来指定事务的传播行为。
常见的事务传播行为
-
REQUIRED
- 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的传播行为,也是 Spring 默认的传播行为。
- 适用于大多数业务场景,确保方法在一个事务中执行,若上层方法已经开启了事务,则方法将加入到该事务中,否则开启新的事务。
-
REQUIRES_NEW
- 无论当前是否存在事务,都会开启一个新的事务。
- 适用于需要独立事务执行的场景,确保方法能够独立于外部事务运行,且不受外部事务影响。
-
SUPPORTS
- 支持当前事务,如果当前没有事务,就以非事务方式执行。
- 适用于不需要事务支持的方法,但是如果存在事务,则方法可以在该事务中运行。
-
MANDATORY
- 强制要求当前方法在一个事务中运行,如果没有事务则抛出异常。
- 适用于需要在事务内运行的方法,如果方法在没有事务的环境下被调用,则抛出异常。
-
NESTED
- 如果当前存在事务,则在嵌套事务中执行。嵌套事务是外部事务的一部分,它有自己的保存点,可以回滚到保存点之前的状态。
- 适用于需要嵌套事务的场景,外部事务的提交和回滚不会影响到嵌套事务,但是嵌套事务可以独立提交或回滚。
传播类型 | 如果当前无事务 | 如果当前有事务 |
MANDATORY | 抛异常 | 使用当前事务 |
NEVER | 不创建新的事务,在无事务的状态下执行方法 | 抛异常 |
NOT_SUPPORTED | 不创建新的事务,在无事务的状态下执行方法 | 暂停当前事务,在无事务的状态下执行方法 |
SUPPORTS | 不创建新的事务,在无事务的状态下执行方法 | 使用当前事务 |
REQUIRED(默认) | 创建新的事务 | 使用当前事务 |
REQUIRES_NEW | 创建新的事务 | 暂停当前事务,创建新的独立事务 |
NESTED | 创建新的事务 | 创建新的内嵌事务 |
事务的失效场景(十五个常见)
失效场景介绍
Spring 中事务失效的场景通常是由于配置、并发、异常处理或逻辑错误等多种因素引起的。以下是一些常见的可能导致 Spring 事务失效的场景:
-
未添加
方法需要在事务中执行,但是忘记添加@Transactional
注解:@Transactional
注解,导致方法在没有事务的情况下执行,事务失效。 -
事务方法内部调用:
在同一个类中,一个@Transactional
方法直接调用另一个@Transactional
方法,由于 Spring 默认通过代理实现事务,内部调用不会触发事务增强,导致事务失效。 -
异常未被捕获或未重新抛出:
事务方法中的异常未被捕获或者捕获后没有重新抛出(或者被处理后没有继续抛出),Spring 无法感知异常,事务无法正确地执行回滚操作。 -
只读事务中的写操作:
在声明为只读事务 (readOnly = true
) 的方法中进行了数据修改操作,例如增删改操作,这种情况下事务无法回滚,因为只读事务不支持写操作。 -
事务注解应用的位置不正确:
@Transactional
注解应用在了一个不被 Spring 代理管理的类或者方法上,例如私有方法或者非公共方法,导致事务增强无法生效,事务失效。 -
方法没有抛出受检查异常:
如果方法抛出的异常是受检查异常(checked exception),而@Transactional
注解默认只回滚运行时异常(unchecked exception),则该异常不会触发事务回滚,导致事务失效。 -
多个数据源:
当应用中配置了多个数据源,而事务管理器只能管理一个数据源的事务,如果在跨数据源的操作中,事务管理器无法管理全局事务,导致事务失效。 -
并发问题:
在高并发环境下,多个事务同时操作相同数据,没有使用适当的并发控制(如乐观锁或悲观锁),可能导致数据不一致,事务回滚,从而造成事务失效。 -
事务传播设置错误:
错误地设置了事务的传播行为,例如应该使用REQUIRED
但是设置为SUPPORTS
,导致方法在没有事务的环境中执行,或者无法加入到外部事务中,事务失效。 -
嵌套事务问题:
使用了嵌套事务(NESTED
),但是数据库不支持保存点(savepoint),或者配置不正确,导致内部事务的回滚没有达到预期效果,事务失效。 -
方法内部捕获异常并处理:
事务方法内部捕获了异常并处理了,但是没有继续抛出异常或手动触发事务回滚,导致事务未能按照预期回滚,事务失效。 -
跨越事务边界的异常处理:
当一个事务方法调用另一个事务方法时,如果前一个方法抛出异常,而后一个方法中捕获并处理了该异常,但没有重新抛出或手动回滚事务,可能导致事务失效。 -
Spring 上下文缺失:
在非 Spring 管理的上下文中执行事务方法,如普通的 Java 类中调用了被@Transactional
注解修饰的方法,由于缺少 Spring 管理的事务增强,事务失效。 -
数据库连接问题:
数据库连接超时、数据库异常或者连接被关闭等问题,导致事务无法正常提交或回滚,事务失效。 -
事务超时设置不合理:
如果事务方法执行时间超过了设置的事务超时时间,事务可能会因为超时而被回滚,造成事务失效。
当前也有比较基础的事务方法被final、static修饰的场景也是失效的
final修饰无法被增强控(final
方法无法被继承或重写)。;
static修饰无法被动态代理。
事务方法内部调用失效
这里着重介绍一下事务方法内部调用失效的情况。
当一个 @Transactional
方法直接调用同类中的另一个 @Transactional
方法时,Spring 无法通过代理拦截和增强内部方法调用。具体来说:
-
基于代理的事务管理:Spring 在运行时通过动态代理或者字节码生成来创建一个代理对象,该代理对象包装了被
@Transactional
注解修饰的方法。这样,当方法被调用时,事务管理器能够拦截并应用事务增强。 -
同类方法调用问题:当一个
@Transactional
方法内部调用同类中的另一个@Transactional
方法时,Spring 的代理机制将无法拦截这个调用。因为代理对象只能拦截从外部类调用的方法,对于同一个类中的方法调用,Spring 将直接调用方法而不是通过代理对象。
内部调用的 @Transactional
方法不会启动新的事务,而是直接执行,导致事务管理器无法管理内部调用的事务边界。
解决方法也很简单,启用中间类或者引入当前类
@Service
public class MyService {
@Autowired
private MyService self; // Autowire the bean itself
@Transactional
public void transactionalMethodA() {
self.transactionalMethodB(); // Call using autowired bean
}
@Transactional
public void transactionalMethodB() {
// Method B logic
}
}
@Transactional
注解认识
常见的 @Transactional
属性
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
@AliasFor("transactionManager") // 配置事务管理器名称
String value() default "";
@AliasFor("value")
String transactionManager() default ""; // 配置事务管理器名称
String[] label() default {};
/**
* 事务传播行为
* 可选值包括:
* Propagation.REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务(默认值)。
* Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。
* Propagation.MANDATORY:必须在一个已有的事务中执行,否则抛出异常。
* Propagation.REQUIRES_NEW:无论当前是否存在事务,都会创建一个新的事务。
* Propagation.NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,则将其挂起。
* Propagation.NEVER:以非事务方式执行操作,如果当前存在事务,则抛出异常。
* Propagation.NESTED:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则执行类似于REQUIRED 的操作。
**/
Propagation propagation() default Propagation.REQUIRED;
/** 事务隔离级别
* 可选值包括:
* Isolation.DEFAULT:使用默认的隔离级别。
* Isolation.READ_UNCOMMITTED:允许读取未提交的数据变更。
* Isolation.READ_COMMITTED:只能读取已提交的数据变更。
* Isolation.REPEATABLE_READ:可重复读,确保多次读取同一数据时,数据保持一致性。
* Isolation.SERIALIZABLE:可串行化,最高的隔离级别,确保事务串行执行。
**/
Isolation isolation() default Isolation.DEFAULT;
// 事务超时时间(定义事务在超过指定时间后自动回滚);默认值为 -1,表示没有超时限制;单位:秒
int timeout() default -1;
String timeoutString() default "";
// 是否只读事务(定义事务是否为只读,只读事务中不允许进行数据修改操作,只能进行查询操作);默认值为false
boolean readOnly() default false;
// 定义需要回滚的异常类型数组,当方法抛出指定的异常时,事务会回滚
Class<? extends Throwable>[] rollbackFor() default {};
// 与rollbackFor类似,但是使用异常类名的字符串表示。
String[] rollbackForClassName() default {};
// 定义不需要回滚的异常类型数组,当方法抛出指定的异常时,事务不会回滚。
Class<? extends Throwable>[] noRollbackFor() default {};
// 与noRollbackFor类似,但是使用异常类名的字符串表示。
String[] noRollbackForClassName() default {};
}
spring事务的工作原理
Spring 的声明式事务是通过 AOP(面向切面编程)实现的。当调用被 @Transactional
注解修饰的方法时,Spring 在运行时会通过动态代理机制为方法织入事务管理相关的逻辑。具体流程包括:
-
拦截器(Interceptor):Spring 使用事务拦截器拦截
@Transactional
注解的方法调用。 -
事务管理器(Transaction Manager):根据配置,事务管理器负责事务的开始、提交和回滚等操作。
-
事务定义和隔离级别:根据
@Transactional
注解的配置,创建事务定义对象,确定事务的隔离级别、超时时间等参数。
注意点
@Transational注解用在类上,表示这个类的所有公共非静止(static)方法都将启用事务
三,事务的实际使用
1,启动类
package com.luojie;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@EnableAsync // springboot 开启异步支持
@EnableTransactionManagement // 开启事务控制
public class Applications {
public static void main(String[] args) {
SpringApplication.run(Applications.class, args);
}
}
2,config配置类
这里数据库的配置就不复述了,有不懂的请查看Spring配置多数据库(采用数据连接池管理)_spring连接多个数据库-CSDN博客
package com.luojie.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
@Configuration
@EnableTransactionManagement
public class TxConfig {
@Autowired
DataSource dataSource2;
@Bean
public DataSourceTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource2);
}
}
3,controller类
package com.luojie.controller;
import com.luojie.common.ResponseCommonImpl;
import com.luojie.common.ResponseUtil;
import com.luojie.controImpl.TxTestImpl;
import com.luojie.moudle.UserBuyBook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TxTestController {
@Autowired
private TxTestImpl txTest;
@PostMapping("/tx/test")
public ResponseCommonImpl testTs(@RequestBody UserBuyBook userBuyBook) {
txTest.testTs(userBuyBook.getUserid(), userBuyBook.getBookName(), userBuyBook.getAmount());
return ResponseUtil.success("ok", null);
}
}
对应的入参modle
package com.luojie.moudle;
import lombok.Data;
@Data
public class UserBuyBook {
private String userid;
private Integer amount;
private String bookName;
}
4,实现类Impl
package com.luojie.controImpl;
import com.luojie.controImpl.interfaceImpl.TxTestInterface;
import com.luojie.dao.mapper2.Mapper2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
public class TxTestImpl implements TxTestInterface {
@Autowired
private Mapper2 mapper2;
@Transactional(rollbackFor = Exception.class)
@Override
public void testTs(String userid, String bookName, Integer amount) {
// 获取图书的价格
Integer bookPrice = Integer.valueOf(mapper2.getBookPrice(bookName));
// 扣去图书库存
mapper2.buyBook(bookName, amount);
// 扣去用户余额
mapper2.userDeductMoney(userid, BigDecimal.valueOf((long) amount * bookPrice));
}
}
对应接口
package com.luojie.controImpl.interfaceImpl;
public interface TxTestInterface {
void testTs(String userid, String bookName, Integer amount);
}
5,dao层mapper配置
接口类
package com.luojie.dao.mapper2;
import com.luojie.moudle.LibraryModel;
import com.luojie.moudle.UserModel;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
@Mapper
public interface Mapper2 {
void addUserBalance(UserModel model);
void addLibrary(LibraryModel model);
void buyBook(@Param("name") String name, @Param("amount")Integer amount);
void userDeductMoney(@Param("userid")String userid, @Param("money") BigDecimal money);
Integer getBookPrice(String name);
}
xml配置
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--<mapper> 标签用于定义 Mapper XML 文件。-->
<!--namespace 属性指定了该 Mapper XML 文件对应的 Mapper 接口的类路径。-->
<mapper namespace="com.luojie.dao.mapper2.Mapper2">
<update id="addUserBalance" parameterType="com.luojie.moudle.UserModel">
update users set money = money + #{money} where userid = #{userid}
</update>
<update id="addLibrary" parameterType="com.luojie.moudle.LibraryModel">
update library set amount = amount + #{amount} where id = #{id}
</update>
<select id="getBookPrice" resultType="java.lang.Integer">
SELECT price FROM library WHERE name = #{name}
</select>
<update id="buyBook">
UPDATE library SET amount = amount - #{amount} WHERE name = #{name}
</update>
<update id="userDeductMoney">
UPDATE users SET money = money - #{money} WHERE userid = #{userid}
</update>
</mapper>
四,测试使用
准备数据库和数据
测试失败的场景(到最后一步扣除余额失败,查看图书的库存数量是否一致)
查看数据库数据
调接口,查看成功的场景
查看图书数量
查看用户余额
测试事务成功!!!
希望对各位大佬有帮助。麻烦加个关注点个赞谢谢!