作者的话:这是我第一次写博客,本来只是想用作自己的备忘录,但是写着写着发现写个博客也不是那么简单的,也是不太熟悉这个操作,我以为我点了保存以后会存草稿箱的,然而事实是直接发布了。就在这个间隙已经被人看到了,还被人无情嘲讽了一下(好伤心...).
背景
由于最近在准备换工作,所以开始补充一些基础知识,以前准备的时候总是去硬背一些知识点,这次花了不少时间去问了问为什么,年前关于Mysql的知识内容有了点心得,为了不遗忘,也是为了只有能讲出来才算是真的理解了,借着这边博客自己在复习一下。
事务的四要素
- 原子性:事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行
- 一致性:事务开始前和结束后,数据库的完整性约束没有被破坏。
- 隔离性:同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。
- 持久性:事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
事务不同隔离级别的问题
| 事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 | 是 | 是 | 是 |
| 读已提交 | 否 | 是 | 是 |
| 可重复读 | 否 | 否 | 是 |
| 串行化 | 否 | 否 | 否 |
幻读与不可重复读的区别
至于Mysql的InnoDB存储引擎的事务的四个隔离级别具体内容我这里就不在赘述了,这里主要说一下幻读这个词,之前我也一直不太明白这个词,现在我尝试这去解释一下。在《高性能Mysql》里对于幻读是这样定义的:
幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行。
根据定义,感觉这和不可重复读表达的意思差不太多,那么他们的区别到底是什么呢,简单来说,可以理解为:不可重复读指的是前后多次读取,数据内容不一致,幻读是指:前后多次读取,数据总量不一致。
幻读可以理解为某一次的select出的结果集不能支撑下一次的操作(这里的操作主要是指插入),更具体些,当前的事务下select某个记录发现不存在,此时进行插入操作,提示记录已经存在,再次select发现记录还是不存在。对于上面的这个现象我认为是发生了幻读。以上是我对幻读概念的理解。
什么情况下会出现幻读
根据事务的四个隔离级别的定义,读未提交,读已提交,可重复读,串行化,前面三个级别下都不能避免幻读的发生,但是在现实中各个不同的数据库在可重复读(RR)的级别下都实现了避免幻读现象的发生。对于Mysql而言是采用的是MVCC+Next-Key实现避免幻读的。那什么是MVCC和Next-Key呢。我们一个一个来说:
MVCC
首先MVCC的中文名字叫做多版本并发控制 (Multiversion concurrency control),这个是用来解决重复读的,保证同一个事务下读取到的数据是一致的,那么它是怎么做到的呢(这里不提供具体的代码,只讲思路)?幻读幻读,强调的是读,在MVCC里有两种读快照读和当前读,
- 快照读:读取数据的某个版本
- 当前读:读取当前数据库最新的数据 其中Select操作读取的是快照读,Insert,Update,Delete是当前读。简单来说就是数据库引擎会隐式的给每条记录里存储引擎会给你自动加上两个字段,类似创建时间和删除时间只不过这里存的不是时间戳而是系统的某个版本(可以理解为事务id)。
虽然Select操作读取的是快照,可以理解为每次读取都是读取的历史版本,如此一来表面上看是解决了幻读的问题,但是光靠MVCC就能解决幻读的问题吗?仔细想想却并非如此,我们假设目前只用MVCC来实现避免幻读的情况,比如说下面的这种情况:
当前的数据;
| id | name | age |
|---|---|---|
| 1 | John | 13 |
| 2 | Mike | 14 |
| 3 | Bob | 12 |
这时有两个事务到来
| 事务/步骤 | 事务1 | 事务2 |
|---|---|---|
| 1 | begin; | |
| 2 | select * from table; | begin; |
| 3 | insert into table(name,age) values('Bruce', 15); | |
| 4 | commit; | |
| 5 | select * from table; | |
| 6 | update table set age = 16 where id = 4; | |
| 7 | select * from table; |
数据库的隔离级别为可重复读,我们看到,事务1在第2步和第5步的select的数量是一致的,都是看不到事务2提交的数据的,当前为止确实是避免了幻读,但是如果此时事务1对后续的id进行修改的话我们发现是可以更新的,更新完成以后再次进行select操作,发现是可以看到事务2提交的那条数据了,并且是经过事务1更新后的版本。
注:以上所描述的场景是了解了MVCC的实现原理以后模拟出得场景,并没有经过实际检验。
虽说上面的场景并没有检验,但是理论上是存在这种情况的,所以,结论是:单独依靠MVCC是不能够解决幻读问题的。
Next-Key Lock
既然MVCC单独不能解决幻读问题,那怎么办呢,所以Mysql引入了Next-key锁的概念,什么是Next-key锁呢,实际上,他不是一个单独的锁,他是由Record Lock和Gap Lock两个锁共同组成的。
- Record Lock: Mysql 索引记录的锁
- Gap Lock: 间隙锁
我们都知道,在Innodb引擎中,主键索引是聚簇索引(一个索引包含所有数据),普通索引是非聚簇索引,所以在Innodb引擎下,如果通过普通索引查询数据则需要查询两次索引(先通过普通索引查到主键索引,再通过主键索引查到数据)才能拿到数据。
对于Record Lock而言,Innodb是在索引上是行级锁,他锁住的其实是索引,锁住了索引就相当于锁住了这条数据。
当前库里的数据;
| id | name | age | card |
|---|---|---|---|
| 1 | John | 13 | 2 |
| 4 | Mike | 14 | 5 |
| 8 | Bob | 12 | 7 |
| 13 | Bob | 12 | 16 |
| 20 | Bob | 12 | 19 |
假如当前数据库里的数据如上表所示,其中id是主键索引,card字段是普通索引,当某个事务要修改card=7的值时,Gap-lock(间隙锁)锁住的范围是(5,7],(7,16]这两个区间,也就是说这个时候不允许其他事务在card处于上面那两个区间的值进行插入,更改,删除操作。同时,更重要的,我们说了,普通索引是非聚簇索引,他得通过主键索引查询才能找到真正的数据,所以,间隙锁锁住的不光是普通索引,他还需要同时将对应范围的主键索引的区间加上间隙锁,即锁住主键id在(4,8],(8,13]的这两个区间。也就是说主键id在这个区间内的数据也是不允许被插入,更新,删除的。
如此一来,通过Record-Lock锁锁住要求改的值,通过Gap锁通过锁住索引的间隙和主键的间隙,这样其他的事务就不能对相应的数据进行修改,配合MVCC就在RR(可重复读)级别下解决了幻读的操作。
Mysql的日志
Mysql的日志中我们需要关心的有三种,binlog,redolog,undolog。
- binlog:Mysql中由Server层以追加的形式记录数据库的写入操作,可通过max_binlog_size参数设置最大值,主要用于主从复制和数据恢复。 刷盘时机:
- 0: 不强制要求,有操作系统自行决定
- 1: 每次commit后立即刷新到磁盘
- N: N个事务后立即刷新到磁盘
- redolog:InnoDB引擎事务操作生成的日志,包括两部分,分别是内存中的日志缓冲(redo log buffer) 和磁盘上的日志文件 。 WAL(Write-Ahead Log):每个事务提交时先写日志后刷新到磁盘的技术。
redo log 刷盘时机:
- 0: 延迟写,延迟刷,现将日志写到redo log buffer中,每隔1秒写入到OS buffer并调用fsync()刷新到磁盘。
- 1: 实时写,实时刷,事务每次提交时都立即将日志通过redo log buffer写入OS buffer并调用fsync()刷新到磁盘。
- 2: 实时写,延迟刷,事务提交的时候立即通过redo log buffer写入到OS buffer中,有操作系统每秒调用fsync()刷新到磁盘中。 记录形式:采用固定大小循环记录的方式,两个指针,一个记录当前日志记录的位置,另一个记录数据刷新到磁盘的位置,达到指定大小后,记录当前日志位置的指针指向文件开头。
- undo log:InnoDB中保障事务原子性的措施,用来做回滚操作的。事务中每一条Insert语句对应undo log一条delete语句,一句Update语句分别对应一条相反的update语句。同时这也是MVCC的关键。
SQL优化
- 最左前缀匹配
- 分页查询 cloud.tencent.com/developer/a…
- 减少回表查询:与主键建立联合索引
参考资料: