事务

在事务的实现机制上,MySQL采用的是WAL:Write-ahead logging,预写式日志,机制来实现的。在使用WAL的系统中,所有的修改都先被写入到日志中,然后再被应用到系统中。通常包含redo和undo两部分信息。
redo log称为重做日志,每当有操作时,在数据变更之前将操作写入redo log,这样当发生掉电之类的情况时系统可以在重启后继续操作。——记录的是操作
undo log称为撤销日志,当一些变更执行到一半无法完成时,可以根据撤销日志恢复到变更之前的状态。——记录的是旧数据
MySQL中用redo log来在系统Crash重启之类的情况时修复数据,而undo log来保证事务的原子性。

事务id

如果某个事务执行过程中对某个表执行了增、删、改操作,那么InnoDB存储引擎就会给它分配一个独一无二的事务id。
注:查询操作是不会分配事务id的,除非是执行了增删改操作(即,一个事务全是查询语句,那么该事务也不会有id)

MVCC

全称 Multi-Version Concurrency Control,即多版本并发控制,主要是为了提高数据库的并发性能(在可重复读隔离级别下有效)。同一行数据平时发生读写请求时,会上锁阻塞住。但MVCC用更好的方式去处理读写请求,做到在发生读写请求冲突时不用加锁。这个读是指的快照读,而不是当前读,当前读是一种加锁操作,是悲观锁。

原理

在事务并发执行有可能会遇到的问题:

  • 脏读:如果一个事务读到了另一个未提交事务修改过的数据,那就意味着发生了脏读;
  • 不可重复读:如果一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值,那就意味着发生了不可重复读;
  • 幻读:如果一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来,那就意味着发生了幻读,幻读强调的是一个事务按照某个相同条件多次读取记录时,后读取时读到了之前没有读到的记录,幻读只是重点强调了读取到了之前读取没有获取到的记录。

MySQL在Repeatable Read隔离级别下,是可以很大程度避免幻读问题的发生的。

版本链

对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列

  • trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务id赋值给trx_id隐藏列;
  • roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息;

演示

  1. -- 创建表
  2. CREATE TABLE mvcc_test (
  3. id INT,
  4. name VARCHAR(100),
  5. domain VARCHAR(100),
  6. PRIMARY KEY (id)
  7. ) Engine=InnoDB CHARSET=utf8;
  8. -- 添加数据
  9. INSERT INTO mvcc_test VALUES(1,'YPF007','演示mvcc');

假设之后两个事务id分别为70,90的事务对这条记录进行UPDATE操作。

trx_id=70 trx_id=90
begin;
begin;
update mvcc_test set name=’ypf_trx_id_70_01’ where id=1
update mvcc_test set name=’ypf_trx_id_70_02’ where id=1
commit;
update mvcc_test set name=’ypf_trx_id_90_01’ where id=1
update mvcc_test set name=’ypf_trx_id_90_02’ where id=1
commit;

每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性,可以将这些undo日志都连起来,串成一个链表。
image.png
对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,把这个链表称之为版本链,版本链的头节点就是当前版本时对应的事务id。于是可以利用这个记录的版本链来控制并发事务访问相同记录的行为,那么这种机制就被称之为:多版本并发控制,即 MVCC。

ReadView

对于使用READ UNCOMMITTED隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了。
对于使用SERIALIZABLE隔离级别的事务来说,InnoDB使用加锁的方式来访问记录。
对于使用READ COMMITTEDREPEATABLE READ隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是:READ COMMITTEDREPEATABLE READ隔离级别在不可重复读和幻读上的区别是从哪里来的,其实结合前面的知识,这两种隔离级别关键是需要判断一下版本链中的哪个版本是当前事务可见的。
为此,InnoDB提出了一个ReadView的概念,这个ReadView中主要包含4个比较重要的内容:

  • m_ids:表示在生成 ReadView时当前系统中活跃(指未提交的)的读写事务的事务id列表;
  • min_trx_id:表示在生成 ReadView时当前系统中活跃(指未提交的)的读写事务中最小的事务id,也就是m_ids中的最小值;
  • max_trx_id:表示在生成 ReadView时系统中应该,分配给下一个事务的id值,注:max_trx_id并不是m_ids中的最大值,事务id是递增分配的。比方说现在有id为1,2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成 ReadView时,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4;
  • creator_trx_id:表示生成该 ReadView的事务的事务id;

有了这个ReadView(在select语句下生成),这样在访问某条件记录时,只需要按照下边的步骤判断记录的某个版本是否可见:

  1. 如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问;
  2. 如果被访问版本的trx_id属性值小于ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问;
  3. 如果被访问版本的trx_id属性值大于或等于 ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成 ReadView后才开启,所以该版本不可以被当前事务访问;
  4. 如果被访问版本的 trx_id属性值在 ReadView的min_trx_id和max_trx_id之间min_trx_id<trx_id<max_trx_id,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问;
  5. 如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,以此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录;

在MySQL中,READ COMMITTED 和 REPEATABLE READ 隔离级别的一个非常大的区别就是它们生成ReadView的时机不同。
还是以表 mvcc_test 为例,假设现在表 mvcc_test 中只有一条由事务id为50的事务插入的一条记录,接下来看一下 READ COMMITTED 和 REPEATABLE READ 所谓的生成 ReadView 的时机不同到底不同在哪里。
READ COMMITTED:每次读取数据前都生成一个ReadView;
比方说现在系统里有两个事务id分别为70、90的事务在执行:

-- T 70
UPDATE mvcc_test SET name = 'ypf_trx_id_70_01' WHERE id = 1;
UPDATE mvcc_test SET name = 'ypf_trx_id_70_02' WHERE id = 1;

此时表 mvcc_test 中 id 为1的记录得到的版本链表如下所示:
image.png
假设现在有一个使用 READ COMMITTED 隔离级别的事务开始执行:

-- 使用 READ COMMITTED 隔离级别的事务
BEGIN;
-- SELECT1:Transaction 70、90 未提交
SELECT * FROM mvcc_test WHERE id = 1; --得到的列 name 的值为‘ypf007’

这个 SELECT1的执行过程如下:
在执行 SELECT 语句时会先生成一个 ReadView,ReadView的m_ids列表的内容就是[70,90],min_trx_id 为70,max_trx_id为91,creator_trx_id为0。
然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 name 的内容是 ypf_trx_id_02,该版本的trx_id为70,在m_ids列表内,所以不符合可见性要求第4条。根据roll_pointer跳到下一个版本。
下一个版本的列 name 的内容是 ypf_trx_id_70_01,该版本的trx_id也是70,也在m_ids内,所以也不符合要求,继续跳到下一个版本。
下一个版本的列 name 的内容是 ypf007,该版本的trx_id为50,小于ReadView中的min_trx_id值,所以这个版本是符合要求的第2条。最后返回的版本就是这条列 name 为 ypf007的记录。
之后把事务id为70的事务提交一下,然后再到事务id为90的事务中更新一下表mvcc_test中id为1的记录:

-- T 90
UPDATE mvcc_test SET name = 'ypf_trx_id_90_01' WHERE id = 1;
UPDATE mvcc_test SET name = 'ypf_trx_id_90_02' WHERE id = 1;

此时表mvcc中id为1的记录的版本链就长这样:
image.png
然后再到刚才使用READ COMMITTED隔离级别的事务中继续查找这个id为1的记录,如下:

-- 使用 READ COMMITTED 隔离级别的事务
BEGIN;
-- SELECT1:Transaction 70 、90 均未提交
SELECT * FROM mvcc_test WHERE id = 1; --得到的列 name 的值为‘ypf007’
-- SELECT2:Transaction 70 提交,Transaction 90 未提交
SELECT * FROM mvcc_test WHERE id = 1; --得到的列 name 的值为‘ypf_trx_id_70_02’

这个SELECT2的执行过程如下:
在执行 SELECT 语句时又会单独生成一个ReadView,该ReadView的m_ids列表的内容就是[90],min_trx_id 为90,max_trx_id为91,creator_trx_id为0。
然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 name 的内容是ypf_trx_id_90_02,该版本的trx_id值为90,在m_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。
下一个版本的列 name 的内容是 ypf_trx_id_90_01,该版本的trx_id值为90,也在m_ids内,所以也不符合要求,继续跳到下一个版本。
下一个版本的列 name 的内容是 ypf_trx_id_70_02,该版本的trx_id为70,小于ReadView中的min_trx_id值90,所以这个版本是符合要求的。最后返回的版本就是这条列 name 为 ypf_trx_id_70_02的记录。
以此类推,如果之后事务 id 为90的记录也提交了,再次在使用 READ COMMITTED 隔离级别的事务中查询表 mvcc_test中id值为1的记录时,得到的结果就是ypf_trx_id_90_02了。
总结:使用 READ COMMITTED 隔离级别的事务在每次查询开始时都会生成一个独立的ReadView。
REPEATABLE READ:在第一次执行查询语句时生成一个ReadView,之后的查询就不会重复生成了。
现在系统里有两个事务id分别为70、90的事务在执行:

-- T 70
UPDATE mvcc_test SET name = 'ypf_trx_id_70_01' WHERE id = 1;
UPDATE mvcc_test SET name = 'ypf_trx_id_70_02' WHERE id = 1;

此时表 mvcc_test 中 id 为1的记录得到的版本链表如下所示:
image.png
假设现在有一个使用REPEATABLE READ隔离级别的事务开始执行:

-- 使用 REPEATABLE READ 隔离级别的事务
BEGIN;
-- SELECT1:Transaction 70、90 未提交
SELECT * FROM mvcc_test WHERE id = 1; --得到的列 name 的值为‘ypf007’

这个SELECT1的执行过程如下:
在执行 SELECT 语句时会生成一个ReadView,该ReadView的m_ids列表的内容就是[70,90],min_trx_id 为70,max_trx_id为91,creator_trx_id为0。
然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 name 的内容是ypf_trx_id_70_02,该版本的trx_id值为70,在m_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。
下一个版本的列 name 的内容是 ypf_trx_id_70_01,该版本的trx_id值为70,也在m_ids内,所以也不符合要求,继续跳到下一个版本。
下一个版本的列 name 的内容是 ypf007,该版本的trx_id为50,小于ReadView中的min_trx_id的值,所以这个版本是符合要求的。最后返回的版本就是这条列 name 为 ypf007的记录。
之后把事务id为70的事务提交一下,然后再到事务id为90的事务中更新一下表mvcc_test中id为1的记录:

-- 使用 REPEATABLE READ 隔离级别的事务
BEGIN;
UPDATE mvcc_test SET name = 'ypf_trx_id_90_01' WHERE id = 1;
UPDATE mvcc_test SET name = 'ypf_trx_id_90_02' WHERE id = 1;

此时表 mvcc_test 中 id 为1的记录得到的版本链表如下所示:
image.png
然后再到刚才使用REPEATABLE READ隔离级别的事务中继续查找这个id为1的记录,如下:

-- 使用 REPEATABLE READ 隔离级别的事务
BEGIN;
-- SELECT1:Transaction 70 、90 均未提交
SELECT * FROM mvcc_test WHERE id = 1; --得到的列 name 的值为‘ypf007’
-- SELECT2:Transaction 70 提交,Transaction 90 未提交
SELECT * FROM mvcc_test WHERE id = 1; --得到的列 name 的值为‘ypf007’

这个SELECT2的执行过程如下:
因为当前事务的隔离级别为REPEATABLE READ,而之前在执行SELECT1时已经生成过ReadView了,所以此时直接复用之前的ReadView,之前的ReadView的m_ids列表的内容就是[70,90],min_trx_id 为70,max_trx_id为91,creator_trx_id为0。
然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 name 的内容是ypf_trx_id_90_02,该版本的trx_id值为90,在m_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。
下一个版本的列 name 的内容是 ypf_trx_id_90_01,该版本的trx_id值为90,也在m_ids内,所以也不符合要求,继续跳到下一个版本。
下一个版本的列 name 的内容是 ypf_trx_id_70_02,该版本的trx_id值为70,而m_ids列表中是包含值为70的事务id的,所以该版本也不符合要求,同理下一个列name的内容是ypf_trx_id_70_01的版本也不符合要求。继续跳到下一个版本。
下一个版本的列 name 的内容是 ypf007,该版本的trx_id为50,小于ReadView中的min_trx_id的值70,所以这个版本是符合要求的。最后返回的版本就是这条列 name 为 ypf007的记录。
也就是说两次 SELECT 查询得到的结果时重复的,记录的列 name 值都是ypf007,这就是可重复读的含义。如果之后再把事务id为90的记录提交了,然后再到刚才使用REPEATABLE READ隔离级别的事务中继续查找这个id为1的记录,得到的结果还是ypf007。

MVCC下的幻读解决

REPEATABLE READ隔离级别下MVCC可以解决不可重复读的问题,那么幻读呢?MVCC是怎么解决的?幻读是一个事务按照某个相同条件多次读取记录时,后读取时读到了之前没有读到的记录,而这个记录来自另一个事务添加的新纪录。
可以想想,在REPEATABLE READ隔离级别下的事务T1先根据某个搜索条件读取到多条记录,然后事务T2插入一条符合相应搜索条件的记录并提交,然后事务T1再根据相同搜索条件执行查询。结果会是什么?按照ReadView中的比较规则中的第3条和第4条不管事务T2比事务T1是否先开启,事务T1都是看不到T2的提交的。
但是,在REPEATABLE READ隔离级别下InnoDB中的MVCC可以很大程度地避免幻读现象,而不是完全禁止幻读。怎么回事呢?看下面的情况:
image.png
首先在事务T1中执行:select * from mvcc_test where id = 30;这个时候是找不到id=30的记录的。
在事务T2中,执行插入语句:insert into mvcc_test values(30,’luxi’,’luxi’);
此时回到事务T1,执行:

update mvcc_test set domain='luxi_t1' where id = 30;
select * from mvcc_test where id = 30;

事务T1很明显出现了幻读现象。
在REPEATABLE READ隔离级别下,T1第一次执行普通的SELECT语句时生成了一个ReadView,之后T2向mvcc_test表中新插入一条记录并提交。
ReadView并不能阻止T1执行UPDATE或者DELETE语句来改动这个新插入的记录,由于T2已经提交,因此改动该记录并不会造成阻塞,但是这样一来,这条新纪录的trx_id隐藏列的值就变成了T1的事务id。之后T1再使用普通的SELECT语句去查询这条记录时就可以看到这条记录了,也就可以把这条记录返回给客户端。因为这个特殊现象的存在,可以认为MVCC并不能完全禁止幻读。

MVCC总结

从上边的描述中可以看出来,所谓的MVCC(Multi-Version Concurrency Control,即多版本并发控制)指的就是在使用READ COMMITTED、REPEATABLE READ这两种隔离级别的事务在执行普通的SELECT操作时访问记录的版本链的过程,这样子可以使不同事务的读写、写读操作并发执行,从而提升系统性能。
READ COMMITTED、REPEATABLE READ这两个隔离级别的一个很大不同的是:生成ReadView的时机不同,READ COMMITTED在每一次进行普通SELECT操作前都会生成一个ReadView,而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了,从而基本上可以避免幻读现象。

MySQL日志

日志类型

image.png
redo和binlog这两种日志有以下三点不同:

  1. redo log是InnoDB引擎持有的;

bin log是MySQL的Server层实现的,所有引擎都可以使用。

  1. redo log是物理日志,记录的是“在某个数据页上做了什么修改”;

bin log是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这一行的c字段加1”。

  1. redo log是循环写的,空间固定会用完;

binlog是可以追加写入的。
“追加写”是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
binlog,redolog都是顺序写磁盘

二阶段提交

因为两种日志属于不同的组件,

  • binlog属于MySQL,不管什么执行引擎都会用到
  • redolog属于innodb的,专门用于重做

所以为了保证数据的一致性,两个日志都要写
image.png
为什么要通过二阶段来提交?
因要保证binlog和redolog一致,所以有了二阶段提交的概念
假设 c = 0,现在更新为 c = 1

  1. 先写redolog后写binlog

假设在redo log写完,binlog还没有写完的时候,MySQL进程异常重启。
由于redo log写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行c的值是1。
但是由于binlog没写完就crash了,这时候binlog里面就没有记录这个语句。因此,之后备份日志的时候,存起来的binlog里面就没有这条语句。如果需要用这个binlog来恢复临时库的话,由于这个语句的binlog丢失,这个临时库就会少了这一次更新,恢复出来的这一行c的值就是0,与原库的值不同。

  1. 先写binlog后写redolog

如果在binlog写完之后crash,由于redolog还没写,崩溃恢复以后这个事务无效,所以这一行c的值是0。但是binlog里面已经记录了“把c从0改成1”这个日志。所以,在之后用binlog来恢复的时候就多了一个事务出来,恢复出来的这一行c的值就是1,与原库的值不同。
二阶段提交流程
image.png
image.png

BufferPool缓存机制

image.png
记录旧值

  1. 把磁盘里的name=zhuge加载到Buffer Pool
  2. 通过Buffer Pool把name=zhuge记录到undo日志(以备回滚时使用)

新值更新到内存

  1. 执行器更新Buffer Pool内存数据,name=zhuge666

新值写入磁盘日志

  1. 执行器写redo日志到Redo Log Buffer缓存
  2. (准备提交事务阶段)redo日志写入redo日志文件磁盘,此时在磁盘中的操作是顺序写(效率高,甚至可以媲美内存的速度)
  3. (准备提交事务阶段)执行器 写入binlog日志
  4. binlog日志写完后,要在redo日志中写入commit标记,保证redo和binlog的数据一致
  5. 写入ibd文件,此时在磁盘中的操作时随机写(效率低非常低)


    为什么Mysql不能直接更新磁盘上的数据而且设置这么一套复杂的机制来执行SQL了?
    因为 来一个请求 就直接对磁盘文件进行 随机读写,然后更新磁盘文件里的数据性能可能相当差。 因为磁盘随机读写的性能是非常差的,所以直接更新磁盘文件是不能让数据库抗住很高并发的。
    Mysql这套机制看起来复杂,

  • 但它可以 保证 每个更新请求都是更新内存BufferPool,然后顺序写日志文件,
  • 同时还能 保证 各种异常情况下的数据一致性。

更新内存的性能是极高的,然后顺序写磁盘上的日志文件的性能也是非常高的,要远高于随机读写磁盘文件。 正是通过这套机制,才能让我们的MySQL数据库在较高配置的机器上每秒可以抗下几干的读写请求。

问题一:Java 中的同步集合与并发集合有什么区别?

同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。
在 Java1.5 之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。
Java1.5加入了并发集合像 ConcurrentHashMap, 不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。它们大部分位于JUC包下。

问题二:BufferPool缓存淘汰机制是怎样的,推荐优化设置多大?

一般设置为物理内存的60%左右
主要使用了LRU算法
1.3/8的list信息是作为old list,这些信息是被驱逐的对象。
2.list的中点就是我们所谓的old list头部和new list尾部的连接点,相当于一个界限
3.新数据的读入首先会插入到old list的头部,
4.如果是old list的数据被访问到了,这个页信息就会变成new list,变成young page,就会将数据页信息移动到new sublist的头部。
5.在数据库的buffer pool里面,不管是new sublist还是old sublist的数据如果不会被访问到,最后都会被移动到list的尾部作为牺牲者
一般推荐BufferPool大小为机器物理内存的60%左右

问题三:如何避免死锁?

死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务,死锁的发生必须满足以下四个条件:
互斥条件:一个资源每次只能被一个进程使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。

问题四:解释下MVCC多版本并发控制机制

Mysql在可重复读隔离级别下如何保证事务较高的隔离性,这个隔离性就是靠MVCC(Multi-Version Concurrency Control)机制来保证的,对一行数据的读和写两个操作默认是不会通过加锁互斥来保证隔离性,避免了频繁加锁互斥,而在串行化隔离级别为了保证较高的隔离性是通过将所有操作加锁互斥来实现的。,Mysql在读已提交和可重复读隔离级别下都实现了MVCC机制。
注意:多理解下“版本链比对规则”