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两个事物
| A | B |
|---|---|
| select | select |
| update | |
| commit | |
| select |
RC:A能读到最新的更新记录
RR:A不可以读到B更新的数据
原子性: 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
| SessionA | SessionB |
|---|---|
| set session transaction isolation level read uncommitted; 设置事务隔离级别为RU | |
| begin | begin |
| 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已经提交的修改的数据
操作:
SessionA设置事物隔离级别为read committed(RC),开启事务,查询id=1的数据SessionB开启事务,修改id=1的数据,不提交事务,在SessionA中查询id=1的数据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已经提交的新增的数据
操作:
SessionA设置事务隔离级别为repeatable read,开启事务,查询tx_test所有数据SessionB开始事务,修改id=1的数据,并且插入一条数据,提交事务,在SessionA中查询tx_test所有数据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可以避免幻读问题,但是会极大降低数据库并发能力
操作:
- SessionA事务隔离级别设置为serializable,查询所有数据
- 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的事物一直不提交,SessionB中insert/update/delete的操作都会被阻塞至超时,该事务隔离级别能解决脏读,不可重复读,幻读
事务隔离级别总结
| 事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| read uncommitted | 存在 | 存在 | 存在 |
| read committed | 不存在 | 存在 | 存在 |
| repeatable read | 不存在 | 不存在 | 存在 |
| serializable | 不存在 | 不存在 | 不存在 |
MVCC
MVCC表达的是维持一个数据的多个版本,使得读写操作没有冲突这么一个思想。
MVCC在read committed和repeatable read两个事务隔离级别下工作。
隐藏字段
InnoDB存储引擎在每行数据后面添加了三个隐藏字段
DB_TRX_ID(6字节):表示最近一次对本记录行做修改(insert或update)的事务ID。至于delete操作,InnoDB认为是一个update操作,不过会更新一个另外的删除位,将行标识为deleted。并非真正删除。DB_ROLL_PTR(7字节):回滚指针,指向当前记录行的undo log信息。DB_ROW_ID(6字节):随着新行插入而单调递增的行ID。当表没有主键或唯一非空索引时,InnoDB就会使用这个行ID自动产生聚集索引。前文《一文读懂MySQL的索引结构及查询优化》中也有所提及。这个DB_ROW_ID跟MVCC关系不大。
undo log
undo log中存储的是老版本数据,当一个事务需要读取记录行时,如果当前记录行不可见,可以顺着undo log链表找到满足其可见性条件的记录行版本。
对数据的变更操作主要包括insert/update/delete,在InnoDB中,undo log分为如下两类:
insert undo log: 事务对insert新记录时产生的undo log, 只在事务回滚时需要, 并且在事务提交后就可以立即丢弃。update undo log: 事务对记录进行delete和update操作时产生的undo log,不仅在事务回滚时需要,快照读也需要,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被purge线程删除。
Purge线程:为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下旧记录的deleted_bit,并不真正将旧记录删除。为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。purge线程自己也维护了一个read view,如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。
不同事务或者相同事务的对同一记录行的修改形成的undo log如下图所示:
可见链首就是最新的记录,链尾就是最早的旧记录。
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
参考:
blog.csdn.net/qq_35190492…
blog.csdn.net/SnailMann/a… (mvcc 锁)