mysql MVCC

108 阅读7分钟

MVCC: 基于快照读,读写时不用锁,无锁竞争,提高性能

acid

原子性:是用undo log实现的 持久性: redo log实现 隔离性:加锁和MVCC实现的

四种隔离级别

读未提交
读已提交 RC
可重复读 RR --- 数据了默认隔离级别
串行化

当前读、快照读

当前读: 读取的总是最新的数据,发生于以下语句

select xxx lock in share mode 读锁
select xxx for update         写锁
update/delete/insert

快照读:读取的是历史版本的数据,发生于以下语句

select xxx

对于如下A、B两个事物

AB
selectselect
update
commit
select

RC:A能读到最新的更新记录
RR:A不可以读到B更新的数据

image.png

image.png

原子性: undolog 隔离性: MVCC 持久性: redolog 一致性:是依赖于上面三个特性的

 create table tx_test(id int(11), name varchar(10), age int(11), primary key (id));
 
 insert into tx_test values(1, '1', 100);
 insert into tx_test values(2, '2', 200);
 insert into tx_test values(3, '3', 300);

脏读(read uncommitted)

事务A读到了事务B已经修改但未提交的数据

开两个session,为A和B

SessionASessionB
set session transaction isolation level read uncommitted; 设置事务隔离级别为RU
beginbegin
select * from tx_test; ==> (3,3,300)
update tx_test set name = 33 where id = 3;
select * from tx_test; ==> (3,33,300)
rollback
select * from tx_test; ==> (3,3,300)
commit;

事务A读到了事务B还未提交的中间状态,产生了脏读
SessionA

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from tx_test where id = 3;
+----+------+------+
| id | name | age  |
+----+------+------+
|  3 | 3    |  300 |
+----+------+------+
1 row in set (0.00 sec)

mysql> select * from tx_test where id = 3;
+----+------+------+
| id | name | age  |
+----+------+------+
|  3 | 33   |  300 |
+----+------+------+
1 row in set (0.00 sec)

mysql> select * from tx_test where id = 3;
+----+------+------+
| id | name | age  |
+----+------+------+
|  3 | 3    |  300 |
+----+------+------+
1 row in set (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql>

SessionB

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update tx_test set name = 33 where id = 3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> rollback;
Query OK, 0 rows affected (0.00 sec)

mysql>

不可重复读 (read committed)

事务A读到了事务B已经提交的修改的数据 操作:

  1. SessionA设置事物隔离级别为read committed(RC),开启事务,查询id=1的数据
  2. SessionB开启事务,修改id=1的数据,不提交事务,在SessionA中查询id=1的数据
  3. SessionB提交事务,在SessionA中查询id=1的数据 SessionA:
mysql> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from tx_test where id = 1;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | 1    |  100 |
+----+------+------+
1 row in set (0.01 sec)

mysql> select * from tx_test where id = 1;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | 1    |  100 |
+----+------+------+
1 row in set (0.01 sec)

mysql> select * from tx_test where id = 1;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | 1111 |  100 |
+----+------+------+
1 row in set (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.01 sec)

mysql>

SessionB:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update tx_test set name = '1111' where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit;
Query OK, 0 rows affected (0.01 sec)

mysql>

结论:事务B没有提交事务时,事务A不会读到事务B的中间状态,所以read committed解决了脏读问题。但是当事务B提交后,事务A读到了修改后的记录,所以最后一次读到了不同的结果,违背了事务之间的隔离性,所以在该隔离级别下产生了不可重复读的问题

幻读 (repeatable read)

事务A读到了事务B已经提交的新增的数据
操作:

  1. SessionA设置事务隔离级别为repeatable read,开启事务,查询tx_test所有数据
  2. SessionB开始事务,修改id=1的数据,并且插入一条数据,提交事务,在SessionA中查询tx_test所有数据
  3. SessionA中更新SessionB新增的那个数据,查询tx_test所有数据 SessionA:
mysql>
mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from tx_test ;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | 1    |  100 |
|  2 | 2    |  200 |
|  3 | 3    |  300 |
+----+------+------+
3 rows in set (0.00 sec)

mysql> select * from tx_test ;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | 1    |  100 |
|  2 | 2    |  200 |
|  3 | 3    |  300 |
+----+------+------+
3 rows in set (0.00 sec)

mysql> update tx_test set name = '444' where id = 4;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from tx_test ;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | 1    |  100 |
|  2 | 2    |  200 |
|  3 | 3    |  300 |
|  4 | 444  |  400 |
+----+------+------+
4 rows in set (0.00 sec)

mysql>

SessionB:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update tx_test set name = '1111' where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit;
Query OK, 0 rows affected (0.01 sec)

mysql> update tx_test set name = '111' where id = 1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> insert into tx_test values(4, '4', 400);
Query OK, 1 row affected (0.01 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql>

结论:事务B已提交的修改记录,在事务A中是不可见的,说明该事务隔离级别解决了不可重复读的问题,但是,事务A虽然读不到事务B新增的记录,却能够更新这个记录,并且执行更新后,却可见该新增记录了,便产生了幻读

注意:在快照读模式下,MVCC能够解决幻读,在当前读模式下,不能解决。

串行化 (serializable)

serializable可以避免幻读问题,但是会极大降低数据库并发能力
操作:

  1. SessionA事务隔离级别设置为serializable,查询所有数据
  2. SessionB 分别执行 insert/update/delete SessionA:
mysql>
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from tx_test;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | 1    |  100 |
|  2 | 2    |  200 |
|  3 | 3    |  300 |
+----+------+------+
3 rows in set (0.00 sec)

mysql>

SessionB:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into tx_test values(4, '4', 400);

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql>
mysql>

结论:只要SessionA的事物一直不提交,SessionBinsert/update/delete的操作都会被阻塞至超时,该事务隔离级别能解决脏读不可重复读幻读

事务隔离级别总结

事务隔离级别脏读不可重复读幻读
read uncommitted存在存在存在
read committed不存在存在存在
repeatable read不存在不存在存在
serializable不存在不存在不存在

MVCC

MVCC表达的是维持一个数据的多个版本,使得读写操作没有冲突这么一个思想。

MVCC在read committedrepeatable read两个事务隔离级别下工作。

隐藏字段

InnoDB存储引擎在每行数据后面添加了三个隐藏字段

image.png

  1. DB_TRX_ID(6字节):表示最近一次对本记录行做修改(insertupdate)的事务ID。至于delete操作,InnoDB认为是一个update操作,不过会更新一个另外的删除位,将行标识为deleted。并非真正删除。
  2. DB_ROLL_PTR(7字节):回滚指针,指向当前记录行的undo log信息。
  3. DB_ROW_ID(6字节):随着新行插入而单调递增的行ID。当表没有主键或唯一非空索引时,InnoDB就会使用这个行ID自动产生聚集索引。前文《一文读懂MySQL的索引结构及查询优化》中也有所提及。这个DB_ROW_IDMVCC关系不大。

undo log

undo log中存储的是老版本数据,当一个事务需要读取记录行时,如果当前记录行不可见,可以顺着undo log链表找到满足其可见性条件的记录行版本。

对数据的变更操作主要包括insert/update/delete,在InnoDB中,undo log分为如下两类:

  • insert undo log: 事务对insert新记录时产生的undo log, 只在事务回滚时需要, 并且在事务提交后就可以立即丢弃。
  • update undo log: 事务对记录进行deleteupdate操作时产生的undo log,不仅在事务回滚时需要,快照读也需要,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被purge线程删除。

Purge线程:为了实现InnoDBMVCC机制,更新或者删除操作都只是设置一下旧记录的deleted_bit,并不真正将旧记录删除。为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bittrue的记录。purge线程自己也维护了一个read view,如果某个记录的deleted_bittrue,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。

不同事务或者相同事务的对同一记录行的修改形成的undo log如下图所示: image.png

可见链首就是最新的记录,链尾就是最早的旧记录。

read view

read view就是判断当前事务能够看见哪些版本的undo log的,主要包含以下几部分

  • m_ids:生成 ReadView 时有哪些事务在执行但是还没提交的(称为 ”活跃事务“),这些活跃事务的 id 就存在这个字段里

  • min_trx_id:m_ids 里最小的值

  • max_trx_id:生成 ReadView 时 InnoDB 将分配给下一个事务的 ID 的值(事务 ID 是递增分配的,越后面申请的事务 ID 越大)

  • creator_trx_id:当前创建 ReadView 事务的 ID

image.png 参考:
blog.csdn.net/qq_35190492…

juejin.cn/post/701216…

www.cnblogs.com/bytesfly/p/…

blog.csdn.net/liujianyang…

blog.csdn.net/huangzhilin…

blog.csdn.net/SnailMann/a… (mvcc 锁)

juejin.cn/post/684490…

www.1024sou.com/article/214…