事务&@Transactional注解

事务的定义

在数据库和软件开发中,事务(Transaction) 是一个不可分割的工作逻辑单元,它由一个或多个数据库操作序列组成。事务的核心目标是确保数据操作的一致性(Consistency)可靠性(Reliability),即使在系统发生故障或并发访问时也是如此。

1.事务的四大特性(ACID)

事务通常通过 ACID 特性来保证其正确性:

  1. 原子性(Atomicity)

    • 定义:事务中的所有操作要么全部成功完成,要么全部不执行,不会停留在中间状态。
    • 实现 :通常通过数据库的回滚(Rollback) 机制实现。如果事务中的任何一步失败,系统会将数据库状态恢复到事务开始之前。
  2. 一致性(Consistency)

    • 定义:事务必须使数据库从一个一致性状态转换到另一个一致性状态。这意味着事务的执行不会破坏数据库的完整性约束(如主键、外键、唯一性约束等)。
    • 实现:由应用程序和数据库的约束共同保证。
  3. 隔离性(Isolation)

    • 定义:多个事务并发执行时,一个事务的执行不应影响其他事务。这防止了脏读、不可重复读、幻读等问题。
    • 实现 :通过数据库的锁机制多版本并发控制(MVCC) 来实现不同的隔离级别。
  4. 持久性(Durability)

    • 定义:一旦事务提交,它对数据库的修改就是永久性的,即使系统发生故障(如断电、崩溃)也不会丢失。
    • 实现:通常通过将事务日志写入持久化存储(如硬盘)来实现。

2. 事务的生命周期

一个典型的事务遵循以下生命周期:

  1. 开始(Begin):标记事务的开始。
  2. 执行(Execute):执行一系列数据库操作(增、删、改、查)。
  3. 提交(Commit):如果所有操作都成功,则将修改永久保存到数据库。
  4. 回滚(Rollback):如果任何操作失败或主动取消,则撤销事务中的所有操作。

3. 示例(伪代码)

sql 复制代码
-- 1. 开始事务
BEGIN TRANSACTION;

-- 2. 执行操作
UPDATE accounts SET balance = balance - 100 WHERE user_id = 'A'; -- A账户扣款
UPDATE accounts SET balance = balance + 100 WHERE user_id = 'B'; -- B账户收款

-- 3. 判断并结束事务
IF (所有操作成功) THEN
    COMMIT; -- 提交,更改生效
ELSE
    ROLLBACK; -- 回滚,所有更改撤销
END IF;

在上面的转账示例中,扣款和收款必须作为一个原子操作。如果扣款成功但收款失败,事务会回滚,A账户的余额不会减少,从而保证了数据的一致性。

4. 总结

简单来说,事务就是一组要么全部成功、要么全部失败的数据库操作集合 ,它是保证数据正确性和系统可靠性的基石。在Java开发中,我们通常通过Spring框架的 @Transactional 注解来声明和管理事务,其底层正是依赖于数据库的ACID特性和事务管理机制。

事务的传播机制

1.事务的传播机制的定义

当一个事务方法被另一个事务方法调用时,事务应该如何行为的机制。

2.定义事务的传播机制的目的

  • 控制事务的边界和嵌套关系
  • 避免不必要的事务创建和事务一致性
  • 支持灵活的业务逻辑组合(如部分操作需要独立事务,部分操作需要合并事务)

3.事务的传播机制分类

  • REQUIRED(默认机制)

    有事务就加入,没有事务就创建,

  • REQUIRES_NEW

    创建新事物,挂起旧事物

    适合单独提交/回滚的操作,如日志

  • SUPPORTS

    有事务就加入,没有事务就不使用事务

  • MANDATORY

    必须存在于现有事务中执行,否则抛异常(只能在已有事务中执行)

    有事务时加入事务,没有事务时抛异常

  • NOT_SUPPORTED

    有事务时,暂停事务,以非事务方式执行;

    无事务时,以非事务方式执行,不走事务,防止事务影响mq发送等等

  • NEVER

    有事务时,抛异常;

    无事务时,以非事务方式运行(必须在非事务环境下运行,否则抛异常)

  • NESTED

    在当前事务中创建savepoint(嵌套事务)

    MySQL需要支持savepoint

    有事务时,创建嵌套点;无事务时,创建新事务

    savepoint:一个快照点,该点之后的sql执行失败,会回滚该点之后的数据,该点之前的事务不受影响

select xx for update (数据库排它锁)

1.含义

数据库中的行级排它锁

2.作用

用于解决并发场景下数据竞争的问题

3.与事务的关系

  • 该语句必须搭配@Transactional注解使用;
  • 该语句必须在同一个数据库事务中才生效,如果后续查询和更新不在一个事务中,锁会在查询结束后立即释放,其他请求就能插入/更新了,就失去了保护意义。
  • 所以,正确的做法是,在方法上加上@Transactional注解,方法开始执行时开启事务,在整个方法内,方法中的查询、更新、插入都在同一个事务中,这些操作时原子且是排他的,解决了并发问题。

4. 锁类型

5. 锁定类型

6. 阻塞对象

7.锁的释放时机

事务提交或回滚,锁释放;若没有事务,查询后立即释放。

7.1 无事务

在没有显示配置@Transactional时(事务),该语句锁的释放时机取决于数据库的自动提交设置

7.1.1 示例

MySQL innodb的默认行为:默认情况下,MySQL的autocommit=1,每条语句都是一个独立的事务:

sql 复制代码
set autocommit = 1;
select xx from user where id =1 for update;--该语句执行完,锁立即释放,因为每条语句会自动提交

关闭自动提交时,必须显示提交事务或回滚事务,锁才会释放:

sql 复制代码
select autocommit = 0;
select xx from user where id = 1 for update;--锁不释放
commit ; -- 执行提交语句后,锁释放。
7.2 有事务

方法上配置了@Transactional注解或配置文件配置时,锁的释放时机就是方法执行完时,因为方法执行完,事务结束,所以锁释放。

关键点:锁的生命周期与事务的生命周期一致,事务结束,锁释放

@Transactional注解

1.作用

开启一个事务,方法内的所有sql的执行在一个事务内,方法不结束,事务没有提交/回滚,锁就会持续存在,可以让锁的生命周期覆盖整个方法,方法结束后,事务提交/回滚,锁会被释放。

2.注意

@Transactional注解的本质是spring的AOP切面,走代理,切面执行的前提是在不同的类中,同一个类的方法调用不会走代理,所以当在同一个类中,A方法、B方法都有@Transactional注解时,A、B两个方法相互调用时,被调用的方法的@Transactional注解不会生效。

场景1 :A B 两个方法都有@Transactional注解,且不在一个类中(即,会走spring代理)

A方法调用B方法,B方法的事务行为取决于B方法注解中的配置;

场景2 :A B 两个方法都有@Transactional注解,A B两个方法在一个类中

A方法调用B方法,因为不走切面,B方法上的注解不生效,B方法会在A方法的事务中执行

3.参数

@Transactional注解中的参数代表事务管理器,在没有显示指定时,spring会默认使用 transactionManager 或 dataSourceTransactionManager,当表使用了shardingsphere分表时,spring默认的事务管理器会失败,所以需要手动指定一个事务管理器

问答

1)问 :线程持有数据库连接及释放连接的时机,线程会一直持有数据库连接吗?
:线程不会一直持有数据库连接,只在事务期间持有,若没有事务,临时获取归还

场景 连接状态
进入 @Transactional 方法 从池获取,绑定 ThreadLocal
事务方法执行中 持有连接
退出 @Transactional 方法 提交事务,归还连接池
无事务的普通方法 不持有连接(每次 SQL 临时获取归还)
线程空闲(线程池等待) 不持有任何连接

2)问 :两个请求同时执行一个方法,同时开启事务时,只有一个请求能够获取锁成功,另一个请求是会等待还是立即返回?
:默认会阻塞等待,不会立即结束
分析 :默认行为是阻塞等待,等另一个线程提交/回滚释放锁,或等待超时,抛异常
如何设置不等待

1.nowait(MySQL 8.0+),获取不到锁,抛异常

sql 复制代码
select xx from user where id = 1 for update nowait;

RROR 3572 (HY000): Statement aborted because lock(s)
  could not be acquired immediately and NOWAIT is set.

2.skip locked(MySQL 8.0+):跳过被锁定的行,返回未被锁定的行

sql 复制代码
select xx from user where id in ('1','2','3') for update skip locked;
  1. 设置超时等待时间
sql 复制代码
@Transactional
public void updateAccount(long id){
// 设置当前会话锁等待超时时间
jdbcTemplate.execute("SET innodb_lock_wait_timeout = 5");
Account acc = accountMapper.selectForUpdate(id);
}

3)问 :两个线程同时执行同一个被@Transactional注解修饰的方法时,开启的是同一个事务吗?
结论 :不是同一个是事务,会开启两个完全独立的事务。
分析 :每个线程都会开启自己的事务,互不影响。原因是事务绑定到ThreadLocal,spring将数据库连接和事务状态绑定到ThreadLocal,每个线程独立持有。

4)问 :在被@Transactional注解修饰的方法中,有两次select for update的查询,第二次能获取到锁吗?
:第二次查询不会阻塞,能正常获取锁,或者说锁已经被持有了。

分析:分析:数据库事务的锁机制,核心原理:

事务 ────► 连接C1(来自连接池) ── ─► begin transaction

├── 第一次 SELECT FOR UPDATE ──► 锁定 rows ───► 持有锁

├── 第二次 SELECT FOR UPDATE ──► 发现这些行已经被本事务锁定 ───► 直接返回,不阻塞

└── COMMIT/ROLLBACK ────► 释放所有锁

同一个事务内,多次对同一行加锁,数据库内部会识别为"锁是由本事务持有的",不会重复申请,也就不会阻塞。

5)问 :同一线程内的调用,A方法调用其他类中的B方法,B方法配置的事务传播机制是REQUIRES_NEW,此时,线程会创建新的事务和数据库连接吗?如果A B两个方法中操作了同一条数据,在执行B方法时,会等待A方法中的锁吗?该线程会绑定新的数据库连接吗?
结论 :线程会创建新的事务和数据库连接,会将新的数据库连接绑定到ThreadLocal中,但旧链接会先被保存(挂起);在执行B方法时会等待A方法中的锁,可能会导致死锁。
分析 :B方法配置的事务传播机制是REQUIRES_NEW,该传播机制的行为是创建新的事务,因此会创建新的事务和数据库连接,同一时刻,ThreadLocal只绑定一个连接,所以会:

① 将旧连接暂存到SuspendedResource;

② 将新连接绑定到ThreadLocal;

③ 新事务结束,恢复旧连接到ThreadLocal

在执行B方法时,是在新的事务和数据库连接中,所以会重新获取锁,若A B两方法请求的是同一行数据,那么就会等待A方法中的锁,A方法在等待B方法执行完,因此会产生死锁。

核心 :新连接被绑定到同一线程,旧连接被挂起保存,新事务结束后才恢复,两个独立的连接在相互等待。

核心原则:REQUIRES_NEW不要操作外层事务中已加锁的同一行数据。

相关推荐
我登哥MVP3 分钟前
Spring Boot 从“会用”到“精通”:ReturnValueHandler原理
java·spring boot·后端·spring·java-ee·maven·intellij-idea
这个DBA有点耶6 分钟前
时序数据库选型:吞吐、压缩与查询延迟的均衡之术
数据库·sql·架构·时序数据库·dba
snow@li6 分钟前
数据库:MySQL vs PostgreSQL 详尽对比(2026版)
java·mysql·postgresql
luck_bor9 分钟前
数据库简介
数据库·oracle
丑过三八线10 分钟前
Runc 深度解析:从原理到实操
java·linux·开发语言·docker·容器·rpc
STDD13 分钟前
ntfy 自托管推送通知服务搭建:一条 curl 命令向手机发送通知
java·开发语言·智能手机
hikktn19 分钟前
Oracle批量UPDATE空值覆盖陷阱:CASE WHEN优雅防御方案【宗申集团】
数据库·oracle
周末也要写八哥22 分钟前
线程的生命周期之线程睡眠
java·开发语言·jvm
Han_han91922 分钟前
数据库基本操作:
数据库
炸薯条!27 分钟前
二叉树的链式表示(2)
java·数据结构·算法