事务隔离级别
- 读未提交是指,一个事务还没提交时,它做的变更就能被别的事务看到。
- 读提交是指,一个事务提交之后,它做的变更才会被其他事务看到。
- 可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
- 串行化,顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
// session A
begin;
select * from t1 where id = 1 //
//session B
begin;
select * from t1 where id = 1
update t1 set amonut=amount+1 where id = 1
//session A
select * from t1 where id = 1 //v1
// session B
commit
// session A
select * from t1 where id = 1 //v2
commit
select * from t1 where id = 1 //v3
上面的查询分析来分析每个事务隔离级别下v1,v2,v3不同的显示
- 若隔离级别是“读未提交”, 则 V1 的值就是 2。这时候事务 B 虽然还没有提交,但是结果已经被 A 看到了。因此,V2、V3 也都是 2
- 若隔离级别是“读提交”,则 V1 是 1,V2 的值是 2。事务 B 的更新在提交后才能被 A 看到。所以, V3 的值也是 2。
- 若隔离级别是“可重复读”,则 V1、V2 是 1,V3 是 2。之所以 V2 还是 1,遵循的就是这个要求:事务在执行期间看到的数据前后必须是一致的。
- 若隔离级别是“串行化”,则在事务 B 执行“将 1 改成 2”的时候,会被锁住。直到事务 A 提交后,事务 B 才可以继续执行。所以从 A 的角度看, V1、V2 值是 1,V3 的值是 2。
事务隔离的实现
MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值
假设一个值从1被按顺序的改为2,3,4,在回滚日志里面就会有下面的记录
read-view A -> 将2改为1
read-view B -> 将3改为2 将4改为3
read-view C -> 当前值4
通过不同时刻启动不同的read-view,在视图A,B,C里面,这个记录的值分别为1,2,4,同一记录存在多个版本,就是数据库的多版本(MVCC).对于 read-view A,要得到 1,就必须将当前值依次执行图中所有的回滚操作得到.同时你会发现,即使现在有另外一个事务正在将 4 改成 5,这个事务跟 read-view A、B、C 对应的事务是不会冲突的
事务日志的删除和长事务
在不需要的时候才删除。也就是说,系统会判断,当没有事务再需要用到这些回滚日志时,回滚日志会被删除,当系统里没有比这个回滚日志更早的 read-view 的时候就会删除.
长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间.==长事务还占用锁资源,也可能拖垮整个库==
事务MVCC的简单分析
-
每个事务都有一个事务ID,叫做transaction id(递增)
-
事务在启动时,找到已提交的最大事务ID记为up_limit_id。
-
事务在更新一条语句时,比如id=1改为了id=2.会把id=1和该行之前的row trx_id写到undo log里,并且在数据页上把id的值改为2,并且把修改这条语句的transactionid记在该行行头
-
一个事务要查看一条数据时,必须先用该事务的up_limit_id与该行的transaction id做比对,如果up_limit_id>=transactionid,那么可以看.如果up_limit_id<transaction id,则只能去undo log里去取。去undo log查找数据的时候,也需要做比对,必须up_limit_id>transaction id,才返回数据
事务的可见性
// session A
start transaction with consistent snapshot //马上启动事务
// session B
start transaction with consistent snapshot
// session C
UPDATE t set k=k+1 where id =1
// session B
UPDATE t set k=k+1 where id =1
SELECT k from t where id = 1 //查询结果 3
// session A
SELECT k from t where id = 1 //查询结果 1
commit
// session B
comit
- A的查询语句是1 因为他的查询是在B视图查询之后,可重复读的特性是在其他事务的更改对当前事务部可见
- B的查询语句是3 因此B的查询语句是在C的提交之后,在当前事务进行的更改,未提交对当前事务也是可见的,B的查询是3是因为更新的时候需要去读一次,而C已经进行语句提交了,因此B的更新语句先在当前事务查询到k的值为2,再进行更新一次为3.因此==更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)==
一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况
- 版本未提交,不可见;
- 版本已提交,但是是在视图创建后提交的,不可见;
- 版本已提交,而且是在视图创建前提交的,可见。
事务和锁
上面的例子 我们将事务C也进行声明事务,接下来看看会怎么样
// session A
start transaction with consistent snapshot //马上启动事务
// session B
start transaction with consistent snapshot
// session C
start transaction with consistent snapshot // C也启动事务
UPDATE t set k=k+1 where id =1
// session B
UPDATE t set k=k+1 where id =1
SELECT k from t where id = 1 //查询结果 3
// session C
commit
// session A
SELECT k from t where id = 1 //查询结果 1
commit
// session B
comit
相对于上个版本来说,事务B的更新的时候,事务C还未进行提交,这样的话当前A,B的查询结果会是什么样子呢?
上一篇文章中提到的“两阶段锁协议”就要上场了。事务 C’没提交,也就是说 (1,2) 这个版本上的写锁还没释放。而事务 B 是当前读,必须要读最新版本,而且必须加锁,因此就被锁住了,==必须等到事务 C’释放这个锁==,才能继续它的当前读
可重复读的核心就是==一致性读==;而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。而读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:
- 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;
- 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。