
04
SQL 事务隔离级别
4.1 ACID 的四个特性
原子性(Atomicity):把多个操作放到一个事务中,保证这些操作要么都成功,要么都不成功;一致性(Consistency):理解成一串对数据进行操作的程序执行下来,不会对数据产生不好的影响,比如凭空产生,或消失;隔离性(Isolation,又称独立性):隔离性的意思就是多个事务之间互相不干扰,即使是并发事务的情况下,他们只是两个并发执行没有交集,互不影响的东西;当然实现中,也不一定需要这么完整隔离性,即不一定需要这么的互不干扰,有时候还是允许有部分干扰的。所以 MySQL 可以支持 4 种事务隔离性;持久性(Durability):当某个操作操作完毕了,那么结果就是这样了,并且这个操作会持久化到日志记录中。PS:ACID 中 C 与 CAP 定理中 C 的区别
ACID 的 C 着重强调单数据库事务操作时,要保证数据的完整和正确性,数据不会凭空消失跟增加。CAP理论中的 C 指的是对一个数据多个备份的读写一致性
4.2 事务操作可能会出现的数据问题
脏读(dirty read):B 事务更改数据还未提交,A 事务已经看到并且用了。B 事务如果回滚,则 A 事务做错了;不可重复读(non-repeatable read):不可重复读的重点是修改: 同样的条件, 你读取过的数据, 再次读取出来发现值不一样了,只需要锁住满足条件的记录 ;幻读(phantom read):事务 A 先修改了某个表的所有纪录的状态字段为已处理,未提交;事务 B 也在此时新增了一条未处理的记录,并提交了;事务 A 随后查询记录,却发现有一条记录是未处理的造成幻读现象,幻读仅专指新插入的行。幻读会造成语义上的问题跟数据一致性问题;在可重复读 RR 隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的。因此,幻读在当前读下才会出现。要用间隙锁解决此问题。在说隔离级别之前,你首先要知道,你隔离得越严实,效率就会越低。因此很多时候,我们都要在二者之间寻找一个平衡点。SQL 标准的事务隔离级别由低到高如下:

上图从上到下的模式会导致系统的并行性能依次降低,安全性依次提高。
读未提交:别人改数据的事务尚未提交,我在我的事务中也能读到。
读已提交(Oracle 默认):别人改数据的事务已经提交,我在我的事务中才能读到。
可重复读(MySQL 默认):别人改数据的事务已经提交,我在我的事务中也不去读,以此保证重复读一致性。
串行:我的事务尚未提交,别人就别想改数据。
标准跟实现:上面都是关于事务的标准,但是每一种数据库都有不同的实现,比如 MySQL InnDB 默认为 RR 级别,但是不会出现幻读。因为当事务 A 更新了所有记录的某个字段,此时事务 A 会获得对这个表的表锁,因为事务 A 还没有提交,所以事务 A 获得的锁没有释放,此时事务 B 在该表插入新记录,会因为无法获得该表的锁,则导致插入操作被阻塞。只有事务 A 提交了事务后,释放了锁,事务 B 才能进行接下去的操作。所以可以说,MySQL 的 RR 级别的隔离是已经实现解决了脏读,不可重复读和幻读的。
05
MySQL 中的锁
无论是 Java 的并发编程还是数据库的并发操作都会涉及到锁,研发人员引入了悲观锁跟乐观锁这样一种锁的设计思想。
悲观锁:
优点:适合在写多读少的并发环境中使用,虽然无法维持非常高的性能,但是在乐观锁无法提更好的性能前提下,可以做到数据的安全性
缺点:加锁会增加系统开销,虽然能保证数据的安全,但数据处理吞吐量低,不适合在读书写少的场合下使用
乐观锁:
优点:在读多写少的并发场景下,可以避免数据库加锁的开销,提高 DAO 层的响应性能,很多情况下 ORM 工具都有带有乐观锁的实现,所以这些方法不一定需要我们人为的去实现。
缺点:在写多读少的并发场景下,即在写操作竞争激烈的情况下,会导致 CAS 多次重试,冲突频率过高,导致开销比悲观锁更高。
实现:数据库层面的乐观锁其实跟 CAS 思想类似, 通数据版本号或者时间戳也可以实现。
数据库并发场景主要有三种:
读-读:不存在任何问题,也不需要并发控制;
读-写:有隔离性问题,可能遇到脏读,幻读,不可重复读;
写-写:可能存更新丢失问题,比如第一类更新丢失,第二类更新丢失。
两类更新丢失问题:
第一类更新丢失:事务 A 的事务回滚覆盖了事务 B 已提交的结果
第二类更新丢失:事务 A 的提交覆盖了事务 B 已提交的结果
为了合理贯彻落实锁的思想,MySQL 中引入了杂七杂八的各种锁:

锁分类
MySQL 支持三种层级的锁定,分别为:
表级锁定:MySQL 中锁定粒度最大的一种锁,最常使用的 MYISAM 与INNODB 都支持表级锁定。
页级锁定:是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁,表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。
行级锁定:Mysql 中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大行级锁不一定比表级锁要好:锁的粒度越细,代价越高,相比表级锁在表的头部直接加锁,行级锁还要扫描找到对应的行对其上锁,这样的代价其实是比较高的,所以表锁和行锁各有所长。
MyISAM 中的锁
虽然 MySQL 支持表、页、行三级锁定,但 MyISAM 存储引擎只支持表锁。所以 MyISAM 的加锁相对比较开销低,但数据操作的并发性能相对就不高。但如果写操作都是尾插入,那还是可以支持一定程度的读写并发从 MyISAM 所支持的锁中也可以看出,MyISAM 是一个支持读读并发,但不支持通用读写并发,写写并发的数据库引擎,所以它更适合用于读多写少的应用场合,一般工程中也用的较少。InnoDB 中的锁
该模式下支持的锁实在是太多了,具体如下:
共享锁和排他锁(Shared and Exclusive Locks)、意向锁(Intention Locks)、记录锁(Record Locks)、间隙锁(Gap Locks)、临键锁(Next-Key Locks)、插入意向锁(Insert Intention Locks)、主键自增锁(AUTO-INC Locks)、空间索引断言锁(Predicate Locks for Spatial Indexes)。
举个栗子,比如行锁里的共享锁跟排它锁:lock in share modle 共享读锁:
为了确保自己查到的数据没有被其他的事务正在修改,也就是说,确保查到的数据是最新的数据,并且不允许其他人来修改数据。但是自己不一定能够修改数据,因为有可能其他的事务也对这些数据使用了 in share mode 的方式上了 S 锁。如果不及时的 commit 或者 rollback 也可能会造成大量的事务等待。
for update 排它写锁:
为了让自己查到的数据确保是最新数据,并且查到后的数据只允许自己来修改的时候,需要用到 for update。相当于一个 update 语句。在业务繁忙的情况下,如果事务没有及时的 commit 或者 rollback 可能会造成其他事务长时间的等待,从而影响数据库的并发使用效率。
Gap Lock 间隙锁:
行锁只能锁住行,如果在记录之间的间隙插入数据就无法解决了,因此MySQL 引入了间隙锁(Gap Lock)。间隙锁是左右开区间。间隙锁之间不会冲突。间隙锁和行锁合称 NextKeyLock,每个 NextKeyLock 是前开后闭区间。间隙锁加锁原则:
加锁的基本单位是 NextKeyLock,是前开后闭区间。查找过程中访问到的对象才会加锁。索引上的等值查询,给唯一索引加锁的时候,NextKeyLock 退化为行锁。索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,NextKeyLock 退化为间隙锁。唯一索引上的范围查询会访问到不满足条件的第一个值为止。
06
MVCC
MVCC:全称 Multi-Version Concurrency Control,即多版本并发控制。MVCC 是一种并发控制的理念,维持一个数据的多个版本,使得读写操作没有冲突。
MVCC 在 MySQL InnoDB 中实现目的主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。
MySQL InnoDB 下的当前读和快照读
当前读:像 select lock in share mode(共享锁)、select for update 、update、insert、delete(排他锁)这些操作都是一种当前读,就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。当前读可以认为是悲观锁的具体功能实现
快照读:
不加锁的 select 就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即 MVCC。可以认为 MVCC 是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。快照读就是 MVCC 思想在 MySQL 的具体非阻塞读功能实现,MVCC 的目的就是为了实现读-写冲突不加锁,提高并发读写性能,而这个读指的就是快照读。快照读就是 MySQL 为我们实现 MVCC 理想模型的其中一个具体非阻塞读功能。因为大佬不满意只让数据库采用悲观锁这样性能不佳的形式去解决读-写冲突问题,而提出了 MVCC,所以我们可以形成两个组合:
MVCC + 悲观锁:MVCC 解决读写冲突,悲观锁解决写写冲突MVCC + 乐观锁:MVCC 解决读写冲突,乐观锁解决写写冲突
MVCC的实现原理
MVCC 实现原理主要是依赖记录中的四个隐式字段、undo 日志、Consistent Read View 来实现的。
四个隐式字段:
DB_TRX_ID:6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务 ID;DB_ROLL_PTR:7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment 里);DB_ROW_ID:6byte,隐含的自增 ID(隐藏主键),如果数据表没有主键,InnoDB 会自动以 DB_ROW_ID 产生一个聚簇索引;FLAG:一个删除 flag 隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除 flag 变了。事务对一条记录的修改,会导致该记录的 undo log 成为一条记录版本线性表(链表),undo log 的链首就是最新的旧记录,链尾就是最早的旧记录。
undo 日志:此知识点上文已经说过了,对 MVCC 有帮助的实质是 update undo log,undo log,实际上就是存在 rollback segment 中旧记录链。
一致读视图 Consistent Read View:Read View 是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID(InnoDB) 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向InnoDB 的事务系统申请的,是按申请顺序严格递增的)。拿着这个 ID 跟记录中 ID 对比进行选择性展示,这里说下大致的思维。
你可以简单的理解为 MVCC 为每一行增加了两个隐藏字段,两个字段分别保存了这个行的当前事务 ID 跟行的删除事务 ID。
insert 时:InnoDB 为新插入的每一行保存当前系统版本号作为版本号。select时:InnoDB 只会查找版本早于当前事务版本的数据行(也就是行的系统版本号<=事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。行的删除版本要么未定义,要么大于当前事务版本号,这可以确保事务读取到的行在事务开始之前未被删除。只有以上同时满足的记录,才能返回作为查询结果。delete时:InnoDB 会为删除的每一行保存当前系统的版本号(事务的ID )作为删除标识.update时:InnoDB 执行 update,实际上是新插入了一行记录,并保存其创建时间为当前事务的 ID,同时保存当前事务 ID 到要 update 的行的删除时间。上面只是一个浅显的讲解 MVCC 选择标准流程,源码层面应该是根据低水位跟高水位来截取的。具体实现可自行百度。
重点:
事务中快照读的结果是非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读的地方非常关键,它有决定该事务后续快照读结果的能力。在 RC 隔离级别下,是每个快照读都会生成并获取最新的 Read View;而在RR 隔离级别下,则是同一个事务中的第一个快照读才会创建 Read View, 之后的快照读获取的都是同一个 Read View。
07
缓冲池(buffer pool)

应用系统分层架构,为了加速数据访问,会把最常访问的数据,放在缓存(cache)里,避免每次都去访问数据库。操作系统,会有缓冲池(buffer pool)机制,避免每次访问磁盘,以加速数据的访问。MySQL 作为一个存储系统,同样具有缓冲池(buffer pool)机制,以避免每次查询数据都进行磁盘 IO。
7.1 主要作用:
存在的意义是加速查询缓冲池(buffer pool) 是一种常见的降低磁盘访问 的机制;缓冲池通常以页(page 16K)为单位缓存数据;缓冲池的常见管理算法是 LRU,memcache,OS,InnoDB 都使用了这种算法;InnoDB 对普通 LRU 进行了优化:将缓冲池分为老生代和新生代,入缓冲池的页,优先进入老生代,该页被访问,才进入新生代,以解决预读失效的问题页被访问。且在老生代停留时间超过配置阈值的,才进入新生代,以解决批量数据访问,大量热数据淘汰的问题。7.2 预读失效:

由于预读(Read-Ahead),提前把页放入了缓冲池,但最终 MySQL 并没有从页中读取数据,称为预读失效
7.3 缓冲池污染:
当某一个 SQL 语句,要批量扫描大量数据时,可能导致把缓冲池的所有页都替换出去,导致大量热数据被换出,MySQL 性能急剧下降,这种情况叫缓冲池污染。
解决办法:加入老生代停留时间窗口策略后,短时间内被大量加载的页,并不会立刻插入新生代头部,而是优先淘汰那些,短期内仅仅访问了一次的页。


按二维码关注我们




