mysql事务、锁、MVCC

事务特性(ACID)

  • 原子性 Atomicity。每个事务中的操作,要么都成功,要么都失败
  • 一致性 Consistency。事务执行前后,数据库中的数据应该保持一致
  • 隔离性 Isolation。事务之间应该是隔离的,事务之间互不影响、干扰
  • 持久性 Durability。事务一旦提交,便会将修改持久化到数据库

数据库事务隔离级别

  • 读未提交:read uncommitted
  • 读已提交:read committed
  • 可重复读:repeatable read
  • 串行化:serializable

事务带来的问题

脏读:读取了其他事务未提交的数据

举例:当前事务隔离级别read uncommitted,开启两个事务A、B。

ID为1的ACCOUNT为100.

//事务A
start transaction;
update salary SET ACCOUNT = 300 WHERE ID = 1;
SELECT SLEEP(10);
ROLLBACK;
SELECT * FROM salary;

//事务B
start transaction;
SELECT * FROM salary;
COMMIT;

事务B读取的ACCOUNT为300,实际这是个A事务产生的脏数据。

由此可见,read uncommitted会出现脏读。

不可重复读:同一事务内多次读取同一数据出现不一致

举例:当前事务隔离级别read committed,开启两个事务A、B。

//事务A
start transaction;
SELECT * FROM salary;
SELECT SLEEP(10);
SELECT * FROM salary;
COMMIT;

//事务B
start transaction;
SELECT * FROM salary;
update salary SET ACCOUNT = 300 WHERE ID = 1;
COMMIT;

事务A两次查询的结果不一致,第一次ACCOUNT为100,第二次为300.

read committed是否会出现脏读的情况呢,执行前面示例中的语句,会发现事务B读取的还是100,没有读到A产生的脏数据。

综上,read committed可以解决脏读,但是不能解决不可重复读。

幻读:像是读取了不存在的数据

举例:当前事务隔离级别repeatable read,开启两个事务A、B

//事务A
start transaction;
SELECT * FROM salary;
SELECT SLEEP(10);
SELECT * FROM salary;
COMMIT;

//事务B
start transaction;
DELETE FROM salary WHERE ID = 1;
COMMIT;

事务A两次查询的结果一致,都有ID为1的数据,说明repeatable read解决了不可重复读。但是实际上事务B中把ID为1的数据已经删除并提交了,事务A第二次查询仍然能看到这个实际不存在的数据。

如果此时事务A在开启事务后,并没有立即查询,即事务B的操作在事务A的第一次查询前完成

//事务A
start transaction;
SELECT SLEEP(10);
SELECT * FROM salary;
SELECT SLEEP(10);
SELECT * FROM salary;
COMMIT;

此时两次查询的结果和事务B执行后的结果一致,没有出现幻读。repeatable read采用快照读解决不可重复读的问题,即事务中第一次查询时读取一个快照,而不是开启事务就读取快照,所以看到两次执行结果会有不同。

如果两个事务同时插入一条ID为2的数据

//事务A
start transaction;
SELECT * FROM salary;
SELECT SLEEP(10);
INSERT INTO salary (ID,NAME,ACCOUNT) VALUES (2, 'lisi',200);
COMMIT;

//事务B
start transaction;
INSERT INTO salary (ID,NAME,ACCOUNT) VALUES (2, 'lisi',200);
COMMIT;

事务B插入成功,事务A查询的时候没有ID为2的数据,插入数据的时候提示重复主键。此时就是产生了幻读。

不可重复读VS幻读

  • 不可重复读:针对已存在数据的多次读取结果不一致,update产生的影响

  • 幻读:针对数据量的前后不一致,有新增和删除数据,insert和delete产生的影响

Record Lock

作用于索引上的锁,而不是行记录本身。

Gap Lock

作用于索引之间的锁,不锁索引本身。

Next-Key Lock

repeatable read默认采用的锁。Record Lock+record之前的Gap lock。

locking read:当前读

select xxx for update

select xxx lock in share mode

locking read、update、delete操作时

  • 当检索条件为非唯一索引时,需要获取next-key锁或gap锁
  • 当检索条件为主键或唯一索引,获取record锁
  • 当检索条件没有任何索引,全表扫描

关于next-key、gap的详细分析见理解innodb的锁mysql锁详解

读锁只能和读锁并行,读锁和写锁、写锁和写锁之间不能并行。比如两个请求可以同时读数据,但是不能一个读一个写或者两个都写。

MVCC

MVCC(Multi-Version Concurrency Control),多版本并发控制。通过版本控制消除一些不必要的加锁操作,实现不加锁也可以读写并行,提供了并发能力。

InnoDB的MVCC是通过在每行记录的后面保存两个隐藏的列来实现的,一个保存了行创建时间,一个保存了过期时间。其中保存的并不是实际时间,而是系统版本号。每开始一个新事务,系统版本号递增。

MVCC只在read committed和repeatable read下工作。read uncommitted总是读取最新行,serializable则是对所有读取行加锁。

repeatable read下的MVCC:

  • SELECT

1、InnoDB只查找版本早于当前事务版本的数据行(行的系统版本号小于等于当前事务的系统版本号)。确保当前事务读取的行都是在事务开始前已提交的,或者是事务本身修改的。

2、行的删除版本号要么未定义,要么大于当前事务的版本号。确保事务读取的数据都是在事务开始时存在的数据。

以上两个条件均满足,才能作为查询结果返回。

  • INSERT

InnoDB新插入一行时,将当前系统版本号作为行版本号,删除版本号此时为未定义。

  • DELETE

InnoDB删除一行时,将当前系统版本号作为该行的删除版本号。

  • UPDATE

InnoDB插入一行数据,保存当前系统版本号作为行版本号,同时将当前系统版本号保存到原来行的删除版本号。

ReadView

所有开启的事务都会被维护在一个事务链表中,当事务结束后,从事务链表中删除。

ReadView就是当开启事务时,对当前事务链表的一个快照。其中包含了4类信息:

  1. m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
  2. min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
  3. max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。
  4. creator_trx_id:表示生成该ReadView的事务的事务id。

比如事务链中有1、2、3,3提交了,那么此时一个新事务生成ReadView,m_ids就是1、2,min_trx_id是1,max_trx_id是4,creator_trx_id是4.

根据ReadView就可以判断当前事务可以看到哪些数据:

  1. 如果某行的版本号小于min_trx_id,表示ReadView创建时,该行已经被提交,可以被访问。
  2. 如果某行的版本号大于max_trx_id,表示ReadView创建时,修改该行数据的事务还未创建,不可被访问。
  3. 如果某行的版本号大于min_trx_id并小于max_trx_id,则看m_ids是否存在该版本号,如果存在(如上面的事务2),表示ReadView创建时,事务还未提交,不可访问;如果不存在(如上面的事务3),表示ReadView创建时,事务已提交,可以被访问。
  4. 如果某行的版本号等于creator_trx_id,表示是当前事务自己修改的数据,可以被访问。

read committed在每次读取数据时生成ReadView。这也就是read committed不可重复读的原因,

repeatable read在开启事务后的第一次读取时生成ReadView。所以上面的例子中开启事务后立即读和不立即读结果会不一样,就是因为两次生成的ReadView不一样。由于只生成一次ReadView,所以可以实现重复读。

Redo log和Undo log

事务隔离性由锁来实现,原子性和、一致性和持久性由redo log和undo log保证。

Redo log

redo log记录的是物理上的数据页变化,而不是逻辑上的操作。
Redo log又可以分为两部分:内存中的日志缓冲(redo log buffer)和磁盘中的重做日志(redo log file)。
关系如图
在这里插入图片描述

  1. 从磁盘中读取数据到内存拷贝
  2. 修改数据的内存拷贝
  3. 内存中记录redo log
  4. 提交事务
  5. 将内存中的redo log持久化,写入到磁盘中
  6. 后台线程同步,非实时的
    内存中的修改是先修改数据,再生成redo log,而持久化时相反,先持久化redo log,再持久化数据。为了性能,不可能每次数据修改完成后都实时持久化数据,所以采用了异步的方式。那么当这个同步的过程中如果宕机了,由于先持久化了redo log,就可以根据redo log恢复数据。

Undo log

undo log记录逻辑上的修改,比如一个insert操作生成一个delete的undo log,便于进行回滚数据,所以在修改数据前就要生成undo log。
redo log和undo log的生成过程:
假设有A、B两个数据,值分别为1,2.

  1. 事务开始
  2. 记录A=1到undo log
  3. 修改A=3
  4. 记录A=3到 redo log
  5. 记录B=2到 undo log
  6. 修改B=4
  7. 记录B=4到redo log
  8. 将redo log写入磁盘
  9. 事务提交

注意:undo log不是redo log的逆向,redo log注重数据上的修改,undo log注重逻辑上的修改

参考

  1. 《高性能Mysql》
  2. MySQL事务
  3. 浅析MySQL事务中的redo与undo
  4. mysql版本链和readView原理
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章