MySQL浅析之事务
1. 事务定义
首先我们需要明白什么是事务。
简单来说,事务就是要保证一组数据库操作,要么全部成功,要么全部失败。在MySQL中,事务支持是在引擎层实现的。你现在知道,MySQL是一个支持多引擎的系统,但并不是所有的引擎都支持事务。比如MySQL原生的MyISAM引擎就不支持事务,这也是MyISAM被InnoDB取代的重要原因之一。
2. 事务的特性
- 原子性(A):事务是最小单位,不可再分。
- 一致性©:事务要求所有的DML语句操作的时候,必须保证同时成功或者同时失败。
- 隔离性(I):事务A和事务B之间具有隔离性。
- 持久性(D):是事务的保证,事务终结的标志(内存的数据持久到硬盘文件中)。
3. 隔离性与隔离级别
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | 产生 | 产生 | 产生 |
读已提交 | 不会产生 | 产生 | 产生 |
可重复读 | 不会产生 | 不会产生 | 产生 |
串行化 | 不会产生 | 不会产生 | 不会产生 |
4. 脏读、不可重复读和幻读
脏读:当前事务(A)中可以读到其他事务(B)未提交的数据(脏数据),这种现象是脏读。
时间 | 事务A | 事务B |
---|---|---|
T1 | 开启事务 | 开启事务 |
T2 | 将id为1的学生张三的姓名改为李四 | |
T3 | 查询id为1的学生,查到的结果为李四 | |
T4 | 提交事务 |
不可重复读:在事务A中先后两次读取同一个数据,两次读取的结果不一样,这种现象称为不可重复读。脏读与不可重复读的区别在于:前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交的数据。
时间 | 事务A | 事务B |
---|---|---|
T1 | 开启事务 | 开启事务 |
T2 | 查询id为1的学生姓名,结果为张三 | |
T3 | 将id为1的学生张三的姓名改为李四 | |
T4 | 提交事务 | |
T5 | 查询id为1的学生姓名,结果为李四 |
幻读:在事务A中按照某个条件先后两次查询数据库,两次查询结果的条数不同,这种现象称为幻读。不可重复读与幻读的区别可以通俗的理解为:前者是数据变了,后者是数据的行数变了。
时间 | 事务A | 事务B |
---|---|---|
T1 | 开启事务 | 开启事务 |
T2 | 查询age大于18岁的学生,结果为15条数据 | |
T3 | 添加一个age大于18岁的学生 | |
T4 | 提交事务 | |
T5 | 查询age大于18岁的学生,结果为16条数据 |
5. 读已提交 – 是如何解决脏读的
首先我们需要明白一点的是,在MySQL中记录真实的数除了我们自己定义的列数据以外,还会有三个隐藏列:
列名 | 是否必须 | 占用空间 | 描述 |
---|---|---|---|
row_id | 否 | 6个字节 | 行ID,唯一标识一条记录 |
transaction_id | 是 | 6个字节 | 事务ID |
roll_pointer | 是 | 7个字节 | 回滚指针 |
我们通过一个例子具体看看:
create table Student(id int primary key, age int);
insert into Student values(1, 18);
假设事务A的事务ID为100,事务B的事务ID为200。
事务A执行:
update Student set age = 19 where id = 1;
事务B执行
update Student set age = 20 where id = 1;
那么我们就可以画一个表来看看:
通过版本号(transaction_id)和版本链(roll_pointer) 来堆数据进行修改,那么我们就知道到底是哪个事务修改的哪一个数据,能够一一对应。
例如事务A,在修改数据后,那么对应的那一行数据中transaction_id就会设置为100。
我们了解了版本号,版本链的作用,以及行数据修改的具体过程,那么我们又是如何读取数据呢?
事务A是如何能够读到age等于19的数据,而事务B能够读到age等于20的数据?
有人可能会说,我们只要根据版本号找到对应的事务ID不久可以了吗。例如事务A若要读取数据,先找到transaction_id等于200,不合适。继续往下找,找到transaction_id等于100,合适,读取数据,不就OK了。
这样看来似乎是没问题,但是这样的前提是,必须先对数据有修改。如果事务A对数据不进行修改,那么它就查不到数据了,不是吗。
因此为了解决这个问题,我们引入一个新的概念:readview试图。
在readiew中有一个字段叫m_ids。 它的作用是存放活跃事务的ID。
假设此时有个事务C版本号为300,而此时事务A,B都为活跃的状态。那么按理来说,事务C应该读取到的是age等于18的这个数据。它是如何操作的:
首先,将活跃的事务ID放入m_ids,即m_ids=[100,200]。
当事务C查询时:
select * from Student where id = 1;
根据m_ids来筛选,发现事务ID为200的是活跃的,跳过;发现事务ID为100的是活跃的,跳过。
最后发现事务ID为0的不是活跃的,表示已提交,那么就读取这个数据。
因此,通过这种方法来解决脏读。也正是因为这样,当事务C获取到age等于18的数据后,如果此时事务B刚好结束,提交了事务。那么,等事务C下次再次读取时,那么我们会发现,此时m_ids中只有[100],那么就会读取到事务B提交后的数据,即age等于20。这样就发生了不可重复读。
6. 可重复读 – 如何解决脏读、不可重复读
由上面的读已提交我们能够了解到,通过版本号、版本链,以及readbiew中的m_ids可以解决脏读。那么不课重复读又该如何解决呢?
其实原理很简单,还是上面的例子。
当事务C读取数据后,此时m_ids中数据为[100,200]。
此时事务B提交事务。
事务C再次查询,此时就和读已提交不一样了。此时,事务C中m_ids依然时[100,200],没错可重复读的解决方法是,以后的m_ids都复制第一次select时的m_ids中的数据。那么,此时事务C查询到的数据依然是age等于18。
由此来解决不可重复读的问题。
7. MySQL如何解决幻读
其实,上面的解决方案有一个统称叫:MVCC(多版本并发控制)。
而在读取数据时我们一般按照是否使用一致性非锁定读来分为快照读和当前读。
我们先看看快照读和当前读的定义:
- 快照读:MySQL使用MVCC (Multiversion Concurrency Control)机制来保证被读取到数据的一致性,读取数据时不需要对数据进行加锁,且快照读不会被其他事物阻塞。
- 当前读:也称锁定读(locking read),通过对读取到的数据(索引记录)加锁来保证数据一致性,当前读会对所有扫描到的索引记录进行加锁,无论该记录是否满足WHERE条件都会被加锁。
这个时候我们应该明白了:
为什么有的博客上说MVCC解决了幻读,有的博客上说没有解决。
其实是因为,通过MVCC只是实现了快照读读数据一致性问题,而对于当前读并没有实现。
那么当前读如何实现呢?通过next-key锁原理来实现的。
next-key锁原理: 将当前数据行与上一条数据和下一跳数据之间的间隙锁定,保证此范围内读取数据是一致的。
next-key锁包含:
- 记录锁:加在索引上的锁
- 间隙锁:加在索引之间的锁
了解了这些,我们就能够解答这个问题了:
- 在快照读读情况下,mysql通过mvcc来避免幻读。
- 在当前读读情况下,mysql通过next-key来避免幻读。
8. 总结
MySQL事务特性:原子性、一致性、隔离性、持久性。
MySQL隔离级别:读未提交、读已提交、可重复读、串行化。
MySQL如何解决脏读、不可重复读、幻读。
MySQL中的MVCC和next-key。