事务的定义
在数据库和软件开发中,事务(Transaction) 是一个不可分割的工作逻辑单元,它由一个或多个数据库操作序列组成。事务的核心目标是确保数据操作的一致性(Consistency) 和可靠性(Reliability),即使在系统发生故障或并发访问时也是如此。
1.事务的四大特性(ACID)
事务通常通过 ACID 特性来保证其正确性:
-
原子性(Atomicity)
- 定义:事务中的所有操作要么全部成功完成,要么全部不执行,不会停留在中间状态。
- 实现 :通常通过数据库的回滚(Rollback) 机制实现。如果事务中的任何一步失败,系统会将数据库状态恢复到事务开始之前。
-
一致性(Consistency)
- 定义:事务必须使数据库从一个一致性状态转换到另一个一致性状态。这意味着事务的执行不会破坏数据库的完整性约束(如主键、外键、唯一性约束等)。
- 实现:由应用程序和数据库的约束共同保证。
-
隔离性(Isolation)
- 定义:多个事务并发执行时,一个事务的执行不应影响其他事务。这防止了脏读、不可重复读、幻读等问题。
- 实现 :通过数据库的锁机制 或多版本并发控制(MVCC) 来实现不同的隔离级别。
-
持久性(Durability)
- 定义:一旦事务提交,它对数据库的修改就是永久性的,即使系统发生故障(如断电、崩溃)也不会丢失。
- 实现:通常通过将事务日志写入持久化存储(如硬盘)来实现。
2. 事务的生命周期
一个典型的事务遵循以下生命周期:
- 开始(Begin):标记事务的开始。
- 执行(Execute):执行一系列数据库操作(增、删、改、查)。
- 提交(Commit):如果所有操作都成功,则将修改永久保存到数据库。
- 回滚(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;
- 设置超时等待时间
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不要操作外层事务中已加锁的同一行数据。