MySQL并发相关原理

  2020-5-12 


隔离级别、封锁协议、MVCC、快照读、当前读、缓存、redo日志、undo日志、锁

事务四大特性 ACID

Atomicity(原子性):事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。通过Undo Log实现

Consistency(一致性):数据库在事务执行前后都保持一致性状态。在一致性状态下,所有事务对同一个数据的读取结果都是相同的。

Isolation(隔离性):一个事务所做的修改在最终提交以前,对其它事务是不可见的。

Durability(持久性):一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。通过Redo Log实现

在满足AID的情况下,就会满足C。四者非并行关系

隔离级别、封锁协议

四个隔离级别:RU、RC、RR、SERIALIZABLE

对应四个级别封锁协议。不同级别封锁协议目的就是达到对应隔离级别

两段封锁协议是指可加锁阶段和锁释放阶段分为两个阶段进行,这样保证可串行化调度。可串行化调度指通过并发控制,使得并发执行的结果=串行执行的结果

MySQL的InnoDB储存引擎采用两段锁协议,会根据隔离级别在需要的时候自动加锁

MySQL默认隔离级别:RR

注意RU读未提交:指的是线程A修改后还没有提交!万一线程A没修改完,还在继续修改,或者线程A回滚了,那这个时候线程B读取的就是脏数据,此即脏读。这个时候脏是指的读取了线程A没有完成修改的数据,只修改了一半

四个隔离级别的实现

假设两个事务,写事务和读事务

RU(读未提交):写事务未提交,读事务读取了写的脏数据

RC(读已提交):在读事务未提交的过程中,写事务完成了多次提交,导致读事务同一个事务内看到的数据前后不一致,不可重复读。通过MVCC的当前读实现,需要在语句中手动加读锁/写锁,读锁:... lock in share mode;写锁:... for update

RR(可重复读):在读事务未提交的过程中,可以重复读取已提交的写事务的数据。通过MVCC的快照读实现,且是InnoDB默认隔离级别,无需多余操作

S(串行化):所有事务严格按照串行化进行

同时通过MVCC RC隔离级别+Nexy-Key-Lock 解决幻读:... between 10 and 20 for update

四个级别的封锁协议,即对应四个隔离级别,封锁协议是隔离级别的逻辑实现

MVCC原理、快照读

MySQL的InnoDB通过MVCC(多版本并发控制)实现RC和RR隔离级别(封锁协议)

在MVCC中,事务的写操作会为数据行新增一个版本快照,储存有事务ID,也是快照的版本号(事务ID递增)。

一行数据维护一个快照列表,每当开始一个写事务,它就copy一份行数据,生成一个快照(Undo日志),然后自己写自己的。将所有写版本快照(事务信息)串起来,后来的在队尾,版本号最大,最早的在队头,版本号最小。有两个指针,max指针是最新开始的事务(还未提交)的事务号(版本号最大),min指针指向当前最新提交的事务的事务号(版本号最小)。也就是说,快照列表中间,全都是活跃事务(已开始,未提交)。

活跃事务提交失败,则回滚。

于是我们可以根据select查询的数据快照的ID和快照队列的ID进行对比,

快照储存在Undo日志中。这个快照列表叫ReadView。

于是我们要读的时候:

①对于RC:读取最新提交事务(版本号最大)的快照版本(复用快照列表)

②对于RR:读取最老提交事务(版本号最小)的快照版本(每次查询都会重新生成快照列表)也就是我只要开始读了,那一串写快照链都雨我无瓜了。

在读写锁(即共享锁S和互斥锁X)中,我们已经实现了读-读兼容

MVCC的意义在于①实现读-写兼容。也就是实现了不阻塞查询,即允许了读时写。②同时由于没有加锁,也使得效率提高。

快照读、当前读

SQL默认快照读:就是默认的MVCC方法(不含锁)(即我们平时默认SQL语句比如select * from tb1

当前读:不使用MVCC方案,而是采用读写锁(需要SQL显示加锁:select * from tb1 where ? lock in share mode,select * from tb1 where ? for update

快照读是非一致性读,当前度是一致性读

当前读用在一致性要求高的地方,比如多个并发数据库请求,当读被写阻塞的时候,我就得等写完(释放写锁),再进行读(加读锁)。当一致性要求不高的时候,使用MVCC,那即使前面有事务在写,那我也可以读,只不过读的是个旧的快照罢了!


Undo log也用于事务回滚

MVCC+Next-Key Locks(临键锁=Gap Lock间隙锁+Record Lock记录锁(行锁))解决幻读

MySQL实现高并发的基本原理:

最开始是使用普通锁,各种操作互斥,

然后通过引入读写锁,使得读-读兼容,

再然后通过MVCC,使得读-写也能兼容,快照读就是InnoDB能做到高并发的核心原因

缓存(无关)

  1. 写缓冲:数据修改后会先写入缓存,然后从缓存定期将数据刷回磁盘,将随机写,通过缓存后变为顺序写,使得写速度非常快。

    更多:写缓冲(change buffer),这次彻底懂了!!!

    业务是写多读少,或者不是写后立刻读取时适用,默认占缓冲池25%

  2. 读缓冲:缓存同时也有作用加速查询的作用,如果缓存有的数据就不必去磁盘IO查了

    更多:缓冲池(buffer pool),这次彻底懂了!!!:预读(局部性原理),非传统LRU(解决预读失败;分为新生代、老年代;为防止大量数据读入的缓冲池污染,页被访问,且在老生代停留时间超过配置阈值的,才进入新生代)

    读缓冲区就是一个查找表(lookup table);数据缓存就是内存中的一块存储区域,其存储了用户的SQL文本以及相关的查询结果

    通常情况下,用户下次查询时,如果所使用的SQL文本是相同的,并且自从上次查询后,相关的表记录没有被更新(插入数据)过,此时数据库就直接采用缓存中的内容

缓冲池通常以页(page)为单位缓存数据

优化:

①因此尽量不要进行大的SQL语句而是分开成小的重复的SQL语句有利于查询效率

②进行表分区。将频繁更新的表字段和基本不变动的表字段分开。对于写任务频繁的程序,关闭查询缓存可能会改进性能。

③成批的进行写入操作而不是逐个执行,以避免数据更新后被清出缓存

④根据实际场景和表的特性选择读写缓冲区分配

undo日志、redo日志(无关)

undo日志记录了数据库未提交的事务的操作,当数据回滚、崩溃的时候可以使用就版本数据,撤销未提交事务对数据库产生的影响(如前MVCC原理所述):undo 日志用于保障,未提交事务不会对数据库的 ACID 特性产生影响。

redo日志redo日志记录了修改数据库的物理操作(不是数据缓存,是物理操作缓存)当数据库崩溃,redo日志里的内容会重做,使得数据能刷会磁盘。:redo日志保障了已提交事务的ACID特性

共享锁S:也叫读锁

排它锁X:也叫写锁

行锁:InnoDB的行锁给索引项加锁,所以只有通过索引条件检索数据才会对行加行锁,否则加表锁(非索引查询方式需要全索引/全表扫描,所以上表锁。这也是不加索引查询很慢的原因之一)同时,非主键索引被加行锁时,其对应的主键索引也会一起被加行锁。行锁属于读写锁

临键锁:临键锁Next-Key Locks=Gap Lock间隙锁+Record Lock记录锁,这个记录锁就是行锁

意向锁意向锁是一种表锁,行锁在加锁前要先加意向锁。与其说它是锁不如说它是标志位。意向锁只与表锁冲突,即当事务2想要申请表锁的时候,如果此时表有意向锁,那就申请失败。意向锁的目的在于解决事务想申请表锁的时候,需要先一行行判断表中是否有行锁,耗费时间的问题。如果加行锁之前必加意向锁,那么只要存在意向锁就代表表中有行锁,那就不能给另一个事务申请表锁。

事务与日志

(以下摘自网络)

参考:https://www.jianshu.com/p/4bcfffb27ed5

MySQL中是如何实现事务提交和回滚的?

为了保证数据的持久性,数据库在执行SQL操作数据之前会先记录redo log和undo log,这俩叫事务日志

redo log是重做日志,通常是物理日志,记录的是物理数据页的修改(比如某字段的值是多少balabala,而不是操作记录),它可以用来恢复提交后的物理数据页。它缓存了运行时的部分修改,然后提交的时候,将这些修改刷到磁盘里

对数据页的修改是先redo log记录到os kernel的buffer中,再使用fsync命令刷到磁盘里(落盘,和redis的aof方案一样)。(redo log也一样,redo/undo log都是写先写到日志缓冲区,再通过缓冲区写到磁盘日志文件中进行持久化保存)

redo log的buffer是一个环形的缓冲记录空间,只会记录运行时的修改

有了redo log,当数据库发生宕机重启后,可通过redo log将【未落盘】的数据恢复,即保证【已经提交的事务记录】不会丢失。

undo log是回滚日志,用来回滚行记录到某个版本,undo log一般是逻辑日志,根据行的数据变化进行记录

undo日志还有一个用途就是用来控制数据的多版本(MVCC)

redo log是用来恢复数据的,用于保障已提交事务的持久性

undo log是用来回滚事务的,用于保障未提交事务的原子性

redo log里记录了提交前的操作

不提交 不回滚,那么事务就不会结束(表现为事务中对物理数据页的修改仍然只记录在redo log中,等待下一步安排,要么回滚,撤销redo log中的这些未提交操作;要么提交,redo log将操作刷回磁盘)】


bin log是sql server级别的,为了保binlog和redolog的一致性,commit后,redo log刷盘后还要写bin log。binlog负责主从同步,数据恢复。

如下为写commit的时候redo log和bin log发生了什么

  1. 会话发起COMMIT动作
  2. 存储引擎层开启[Prepare]状态:在对应的Redo日志记录上打上Prepare标记
  3. 写入binlog并执行fsync(刷盘)
  4. redo日志记录上打上COMMIT标记表示记录提交完成

bin log和redo log的区别:

1.redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。

2.redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;bin log 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。

3.redo log 是循环写的,空间固定会用完,只会记录一定时间内的数据修改binlog 是追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

所以说,要恢复数据,还得看bin log


提交过程中,主要做了4件事情,

1、清理undo段信息

2、释放锁资源(事务结束),mysql通过锁互斥机制保证不同事务不同时操作一条记录,事务执行后才会真正释放所有锁资源,并唤醒等待其锁资源的其他事务;

3、刷redo日志,前面我们说到,mysql实现事务一致性和持久性的机制。通过redo日志落盘操作,保证了即使修改的数据页没有即使更新到磁盘,只要日志是完成了,就能保证数据库的完整性和一致性;

4、清理保存点列表,每个语句实际都会有一个savepoint(保存点),保存点作用是为了可以回滚到事务的任何一个语句执行前的状态,由于事务都已经提交了,所以保存点列表可以被清理了。


且听风吟