引言
在MySQL数据库中,事务是一组不可分割的操作单元,这些操作要么全部成功,要么全部失败。事务的四大特性,通常被称为ACID特性,包括原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。以下是对这些特性以及并发事务问题的详细介绍:
一、事务的四大特性(ACID)
-
原子性(Atomicity)
- 事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。
- 实现原子性的关键在于回滚日志(undo log),它记录了事务所执行的修改操作,在回滚时反向执行这些修改操作即可。
-
一致性(Consistency)
- 事务必须使数据库从一个一致性状态变换到另一个一致性状态。
- 一致性是事务追求的最终目标,它的实现既需要数据库层面的保障,也需要应用层面的保障。
-
隔离性(Isolation)
- 当多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
- MySQL通过锁机制(如行级锁)和多版本并发控制(MVCC)来实现隔离性。
-
持久性(Durability)
- 一旦事务提交,则其所做的修改就会永久保存到数据库中,即使系统崩溃,修改的数据也不会丢失。
- 持久性是通过重做日志(redo log)来实现的,它保证了数据在事务提交后不会因宕机等原因而丢失。
二、并发事务问题
-
脏读(Dirty Read)
- 一个事务读取了另一个未提交事务的数据。
- 例如,事务T1修改了一个数据,事务T2随后读取了这个数据。如果T1撤销了这次修改,那么T2读取的数据就是脏数据。
- 脏读问题在读未提交(Read Uncommitted)隔离级别下可能发生。
-
不可重复读(Non-repeatable Read)
- 在一个事务范围内,多次查询却返回了不同的数据值。
- 这通常是因为在查询间隔内,被另一个事务修改并提交了数据。
- 不可重复读问题在读已提交(Read Committed)和读未提交(Read Uncommitted)隔离级别下可能发生。下面事务隔离演示会介绍
-
幻读(Phantom Read)
- 一个事务读取到的记录在其后续的读取请求中发生变化,造成原本能够查询到的数据在后续查询中消失或改变的现象。
- 例如,在一个事务中执行查询后,另一个事务插入了新记录,从而导致第一个事务再次执行同样的查询时会看到一个“幻影”记录。
- 幻读问题在可重复读(Repeatable Read)隔离级别下可能发生,但在串行化(Serializable)隔离级别下可以避免。
三、事务的隔离级别
MySQL数据库提供了四种事务隔离级别,从低到高分别为:
-
读未提交(Read Uncommitted)
- 一个事务可以读取另一个未提交事务的数据。
- 最低级别,任何情况都无法保证数据的一致性。
-
读已提交(Read Committed)
- 一个事务只能读取已经提交的事务所做的修改。
- 可避免脏读的发生。
-
可重复读(Repeatable Read)
- 保证在同一个事务中多次读取同样数据的结果是一样的。
- 可避免脏读和不可重复读的发生。
- MySQL的默认隔离级别。
-
串行化(Serializable)
- 事务串行化顺序执行,可避免脏读、不可重复读与幻读的发生。
- 级别最高,执行效率最低。
这些隔离级别可以解决哪些事务问题:
查看事务隔离级别:
# 查看隔离级别
select @@transaction_isolation;
设置事务隔离级别:
# 设置隔离级别为读未提交
set session transaction isolation LEVEL read uncommitted ;
# 设置隔离级别为读已提交
set session transaction isolation level read committed ;
# 设置隔离级别为可重复读
set session transaction isolation level repeatable read ;
# 设置隔离级别为串行化
set session transaction isolation level SERIALIZABLE ;
四.事务的隔离演示:
开启2个客服端使用同一个表来模拟:启动
开启演示:
1) 来演示隔离级别 读未提交:
(未解决 : 脏读 可重复读 幻读)
Mysql默认隔离级别是可重复读,因此
先设置隔离级别为 读未提交(是最低级的,并发事务问题都不能解决,这么就试试脏读)
脏读:一个事务读取了另一个未提交事务的数据。
# 设置隔离级别为读未提交
set session transaction isolation LEVEL read uncommitted ;
然后开启事务,我们到后面自己手动提交事务
左边我们先查询表的数据,然后在右边我们开启事务,修改了数据,但是并没有提交,然后左边我们再次查询会发现与开始的数据不一致了,导致我们2次查询数据不一样,并且这是在同一个事务下,我还未提交,数据却不一致,这就是脏读,一个事务读取了另一个还未提交的事务的数据。
2) 来演示隔离级别 读已提交:
(解决:脏读 为解决:可重复读 幻读)
再次之前我们恢复好原来数据
同理,我们设置隔离级别
# 设置隔离级别为读已提交
set session transaction isolation level read committed ;
演示:
然后右边我们提交:
这就虽然解决了脏读,但是没有解决可重复读,每次查询的数据是不一致的!!!
其实这和他生成的readview的时机不同有关,他是我们快照读(就是平常的select (不加锁))SQL执行时MVCC提取数据的依据。他每一次快照读都会生成一个新的ReadView,然后根据新的规则去MVCC的版本控制链中去寻找对应的undo log 日志的,他就是用来记录我们inset update delete 这些操作后记录的日志,用来回滚的。
简单点概括,他会去查询他最近一次提交的数据的。所有我们这里在右边没有提交前,尽管他怎么修改,也是查询的以前数据,没有改动,但是当右边提交后,我们发现数据就变更了,就查询的是这次提交后改动的数据了
3)演示隔离级别 可重复读
(解决:脏读 可重复读 未解决:幻读)
同理:我们恢复数据 ,设置隔离级别
# 设置隔离级别为可重复读
set session transaction isolation level repeatable read ;
演示:
解决了脏读
解决不可重复读,这里我们可以看到,右边经管提交了,但是左边数据查询依然是一致的,这里其实和上面读已提交隔离级别一样的,只是在这种RR(可重复读)隔离级别下他其实只会在第一次执行快照读的时候会生成一个ReadView,后面在执行相同就会复用上的ReadView,规则不变,你每次查询的都是一样,可以看看MVCC的原理就明白了。
但是:并未解决幻读:
比如我们这里左边开启事务,查询id为3的没有,然后右边开启事务插入一条id为3的信息,在去左边插入,发现会提示主键重复了,但是查询会发现又没有,这不就是见鬼了吗??
4)演示隔离级别 串行化
(解决:脏读 不可重复读 幻读)
设置隔离级别:
# 设置隔离级别为串行化
set session transaction isolation level SERIALIZABLE ;
演示;
一段时间后可以发现右边超时了!!!
Lock wait timeout exceeded; try restarting transaction
即使普通的select查询也会上共享锁,进行当前读,因此右边事务进行insert操作时想要获取排它锁需要一直等待,最终超时。
然后左边提交后:
右边也在执行会发现id为4的已经存在了!!!