前言:这是在慕课网上学习剑指Java面试-Offer直通车时所做的笔记,主要供本人复习之用.
目录
第一章 锁模块之MyISAM与InnoDB关于锁方面的区别
1.1 表级锁
1.2 行级锁
1.3 锁与索引的关系
1.4 两个引擎的优劣
1.4.1 适合MyISAM的场景
1.4.2 适合InnoDB的场景
1.5 锁的分类
第二章 数据库事务的四大特性
第三章 事务隔离级别以及各级别下的并发访问问题
3.1 并发访问出现的问题
3.1.1 更新丢失问题
3.1.2 脏读
3.1.3 不可重复读
3.1.4 幻读
3.2 隔离级别
第四章 InnoDB可重复度隔离级别下如何避免幻读
4.1 当前读与快照读
4.1.1 当前读
4.1.2 快照读
4.2 next-key锁(行锁+gap锁)
4.2.1 主键与唯一索引的间隙锁情况
4.2.2 非唯一索引与不走索引的gap锁情况
第五章 RC,RR级别下的InnoDB的非阻塞读如何实现
5.1 DB_TRX_ID ,DB_ROLL_PTR, DB_ROW_ID
5.2 undo日志
5.3 read view
先上结论:
无论是表锁还是行锁均分为共享锁和排它锁.关系如图所示.
上共享锁的写法:lock in share mode
例如: select math from zje where math>60 lock in share mode;
上排它锁的写法:for update
例如:select math from zje where math >60 for update;
共享锁排他锁在两个存储引擎中都适用,只不过一个是作用在表中,一个是作用在行级?
我们用实例进行说明:
person_info_large
person_info_myisam
每张表都有200W余条数据,这样我们查找更新数据的时候会有几秒的时间,这时候可以操作不同的session对数据库进行操作.
我们先执行语句1,再执行语句2,发现语句2会被1阻塞,直到1完成后才会运行语句2.
这意味着当表进行查询的时候MyISAM会自动给表上一个表锁.会锁住表,block其它数据对其的更新(对表增加删除都会遇到同样的结果)
原因:对于MyISAM,进行select时候会自动为我们加上表级的读锁,而对数据进行增删改的时候,会为我们操作的表加上表级别的写锁,当读锁未被释放时,另一个session想要对同一张表加上写锁,它就会被阻塞.
显示加上读锁,写锁时write
释放:
读锁也叫共享锁
我们执行语句3,再执行语句4,发现4没有被阻塞,说明读锁是可以共享的.
我们执行语句5,再执行语句6,发现6被阻塞,证明读写是不能共享的.
先上写锁,再上写锁也是不可能的.所以写锁有了其别名,排他锁.
我们除了对增删改上排他锁之外,select也是可以上排他锁的.
我们先执行语句7,再执行语句8,发现8被block住了.
总结:MyISAM默认支持表级锁,不支持行级锁,表级锁会锁住整张表,锁按级别划分有两种,共享锁和排他锁,上了共享锁之后依然支持上共享锁,不支持上排他锁,先上排他锁,另外的读或者写都是不允许的.当语句执行完后会自动解锁.
共享锁和排他锁的这种情况同样也支持InnoDB的引擎.
由于InnoDB支持事务,我们可以通过session获取锁暂时不自动提交的方式模拟并发访问的过程.mysql默认是自动提交事务的.在这里我们将要使用person_info_large这张表.
打开两个session,同时执行下面的语句9,我们发现两条session都是无需等待自动执行的.这看起来和不支持事务的MyISAM一样sql跑完了之后会自动解锁,其实InnoDB使用的是二段锁,也就是加锁和解锁是分成两个步骤来执行的.即先对一批事务里的操作分别进行加锁,到commit之后再对事务上加上的锁进行解锁.而commit是自动提交的,所以看起来和MyISAM没有太大区别.
为了验证上面的结论,我们执行语句10关闭自动提交,注意该设置仅能关闭当前session的自动提交.当然我们在这里执行begin也能实现同样的效果.
值得注意的是select是不会自动上锁的,所以select后即使不commit也能update.我们执行语句11,再执行语句12发现也能成功.
我们执行语句13主动给select加上读锁,再执行语句12,发现12被阻塞.直到语句13用commit才能成功.也就是当前行加了共享锁之后其它session就不能加排他锁了.
我们再求证一下InnoDB是否真的会加上行级锁,我们先执行语句13给id为3的行加上共享锁,再执行语句14修改id为4的行,发现语句14并没有被block住,证明InnoDB的锁会默认支持行级.
我们再验证一下锁的兼容性,刚才是先读后写,现在我们先读后读.
我们先执行语句13,再执行语句15,发现语句15被顺利执行,说明共享锁是兼容的.
总的来说,写写互斥,读写互斥,读读兼容.
用到表级锁,只要操作到表里的数据,均会上表锁,与索引无关.行级锁与索引有关吗?
我们查看图2,找到没有索引的列motto,执行语句16,再在另外一个session中执行语句17,本来两个motto是不一样的,InnoDB又是行锁,按理说不会受影响,但是17的执行却被堵塞了.要等待16解锁后再做更新.
我们发现当不走索引的时候,整张表就会被锁住,此时的查询用的是表级锁,所以InnoDB再没有用到索引的时候用的是表级锁,在用的索引的时候用的是行锁.InnoDB除了支持行级锁之外,还支持表级的意向锁,共享读锁IS,排他写锁IX,主要是为了进行表级别的操作时不用轮询每一行是否上了行锁,感兴趣的话可以私下再去了解一下.
优劣问题:行级锁不一定优于表级锁,相比表级锁在表的头部直接加锁来讲,行级锁还要扫描到表的某行进行加锁,这样代码比较大,InnoDB支持事务的同时也比MyISAM带来了更大的开销.
详细介绍:
自动锁与MyISAM的表锁以及update,insert,delete加上的锁就是自动锁,这是mysql自动为我们上的,select for update,lock in sharemode这些我们显式去加的锁就是显式锁.
对数据进行操作的是DML锁,对表结构进行变更的就是DDL锁.
悲观锁:对外界的修改持保守态度,在数据处理过程中对数据处于锁定状态.悲观锁的实现往往依靠数据库提供的锁机制,也只有数据库提供的锁机制才能真正的保证数据库访问的排他性.1.2节演示的操作大多就是悲观锁的部分.
乐观锁:乐观锁认为数据一般情况下不会造成冲突,所以在数据进行更新的时候才会进行数据冲突与否的检测.一般乐观锁的实现是记录数据版本,一种是使用版本号,一种是使用时间戳,我们在下面进行演示.
创建一个表,version就是,当我们读取数据时我们将version一起读取出来,数据每更新一次,我们就对version+1,当我们进行更新时我们将当前的version与数据库中的version进行比对,如果相等就更新,如果不当等就认为时过期数据.如图3所示,我们在更新的时候再检查版本,而不是在select的时候就把表锁住.但如果简单这么做,还是可能会遇到不可预期的结果,例如两个事务都读取了表中的某一行,经过修改后回写数据库,这时就会遇到问题.
A(atomicity)原子性。一个事务的执行被视为一个不可分割的最小单元。事务里面的操作,要么全部成功执行,要么全部失败回滚,不可以只执行其中的一部分。
C(consistency)一致性。一个事务的执行不应该破坏数据库的完整性约束。如果上述例子中第2个操作执行后系统崩溃,保证A和B的金钱总计是不会变的。
I(isolation)隔离性。事务之间相互独立,互不干挠。
D(durability)持久性。事务提交之后,需要将提交的事务持久化到磁盘。即使系统崩溃,提交的数据也不应该丢失。
mysql会用锁机制创造出不同的隔离级别.我们从低到高进行说明.
更新丢失问题在数据库上不好模拟的原因是mysqInnoDB各种事务隔离级别在数据库层面上几乎已经避免了这种现象的发生.
脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了,主要是针对update.
例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。幻读主要是针对insert与delete.
READ UNCOMMITTED(未提交读),事务中的修改,即使没有提交,在其他事务也都是可见的。事务可以读取未提交的数据,这也被称为脏读。
READ COMMITTED(提交读),一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。这个级别有时候也叫做不可重复读,因为两次执行相同的查询,可能会得到不一样的结果。因为在这2次读之间可能有其他事务更改这个数据,每次读到的数据都是已经提交的。
REPEATABLE READ(可重复读),解决了脏读,也保证了在同一个事务中多次读取同样记录的结果是一致的。但是理论上,可重读读隔离级别还是无法解决另外一个幻读的问题,指的是当某个事务在读取某个范围内的记录时,另外一个事务也在该范围内插入了新的记录,当之前的事务再次读取该范围内的记录时,会产生幻行。
SERIALIZABLE(可串行化),它通过强制事务串行执行,避免了前面说的幻读的问题。
总的来说,就是下图:
当前读就是加了锁的增删改查语句,不管上的是共享锁还是排它锁上的都是当前读,因为它读取的是最新版本,读取后还保证其它并发事务不能读取当前记录.对读取的记录加锁,除了select lock...会加共享锁之外,其它的操作加的都是排它锁.
update,delete,insert也都是当前读,RDBMS主要由程序实例和存储组成,如图4所示.程序实例在这里指的是mysqlServer的程序实例,存储就是InnoDB,拿update来举个例子,当update发送给mysql之后,mysqlserver会根据where读取第一条满足where的条件记录,InnoDB会将第一条数据返回并加锁.mysqlserver收到加锁的记录后会发起一个update操作去更新这条记录,一条记录读取完成后再去读取下一条记录,直至没有满足条件的记录出现.update操作就包括一个当前读来获取数据的最新版本,就如之前在已提交读的隔离级别下出现的幻读的情况一下,由于先前事务新提交了一个数据,当前事务update全表的时候就莫名其妙多了一条数据,即读取到了数据的最新版本,同理delete也一样,insert会稍有不同,简单来说insert会触发唯一键的检查,也会进行一个当前读.
快照读与当前读不太一样,它就是简单的select操作,不加锁,是在隔离级别不在串行化的条件下实现的,在serializable下由于是串行读,所以快照读也退化成当前读的lock in share mode的模型.
之所以出现快照读是基于提升并发性能的考虑,快照读的实现是基于多半版并发控制即MVCC,可以认为MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低,但是快照读读取的可能不是最新版本,是历史版本.
在Read committed情况下当前读和快照读读到的数据是一样的.
在Repeatable read的情况下
情况1:session1,session2都开启事务,先在session1中读取账户余额发现是600,在session2中修改账户余额为300,再在session1中用当前读查看账户余额为300,用快照读查询账户余额还是600.图5中第一条语句为快照读,第二条语句为当前读.这里快照读读到的就是历史版本.
情况2:session1,session2都开启事务,我们在session2中更新账户余额,在session1中当前读与快照读查询到的都是最新版本.
在RR级别下可以让我们看不到幻读,是因为采用了伪MVCC机制,关于伪MVCC机制更多的可以去看第五章,其实伪MVCC机制有一些掩耳盗铃的感觉,已经做了更改就是看不见,真正实现避免幻读的还是使用了间隙锁.
行锁:就是对单个行记录上的锁,上面也说了.
gap锁:首先理解什么是gap,gap就是索引树种插入新数据的空隙,gapLock就是锁定一个范围但不包括记录本身,gap锁的目的是为了防止同一事务的两次当前读出现幻读的情况,因此我们抓重点,主要讨论gap锁的情况,gap锁在RC级别下是不存在的,所以这就是RC及更低的隔离级别无法避免幻读的原因,这里我们主要讨论RR下的gap锁.
分情况:
全部查询的时候,所有记录都有.比如where id in(1,3,5),如果id为1,3,5的数据都在并且出现,那就是全部命中,如果只出现部分如1,3等,则为部分命中.
实例:
我们这里执行删除id为9的数据,先给系数索引中的数据加上排它锁,再给密集索引中的数据加上排它锁.
解析:部分命中包含了范围查询,精确查询.
实例:
全不命中的情况:
我们表中id为7和8的没有数据.表结构在图6红线处.
我们开启事务,删除id为7的数据
在另外一个session中插入id为8的数据
发现8被阻塞,证明7的间隙加了锁.
部分命中:
我们开启事务,执行语句18,
另一个session中执行插入操作.
先插入4,成功.
再插入7,被block住,也即是说对5到9之间间隙上了gap锁.
插入8,被block住
插入10,成功
也就是部分命中也会部分加gap锁.
全部命中:
我们开启事务,执行语句19
插入7,8都成功,也就是都命中的话不会上gap锁的.
1.非唯一索引的情况
非唯一索引:表结构如图7所示.有非唯一普通键id.在删除id为9的数据的过程中,如果我们增加了一个id为9的数据就会导致幻读,所以我们要锁住.具体锁的范围的官方文档如图8所示,在这里我们删除id为9的数据时要锁的范围是(6,9],(9,11]上锁,当我们向其中插入数据时会上锁.同时根据字母表的排序来说,b 我们开启事务,执行语句20, 另一个session中执行插入操作. 执行语句21,成功 执行语句22,被阻塞 执行语句23,成功 执行语句24,成功 执行语句25,阻塞 2. 不走索引的情况 当当前读不走索引的时候,会对所有的get都上锁,也就是锁表.下图id是没有索引的,当删除id时,会将整张表进行锁住, 非阻塞读也就是快照读.要实现快照读离不开三个因子, 每行数据记录除了存储数据外,还有额外的一些字段,其中最关键的是三个字段,DB_TRX_ID ,DB_ROLL_PTR, DB_ROW_ID. DB_TRX_ID用来标识最近一次对本行做修改(不管是insert还是update)的事务的标识符,即最后一次修改本行事务的id,至于delete操作在InnoDB看来也不过是一次update操作,将行标识为deleted,也就是说数据行除了这3列,还有别的隐藏列,有个deleted的隐藏列,如果删除了就会将行列标识为deleted,并非真正的去做删除. DB_ROLL_PTR:回滚指针只写入回滚段roll_backsagment的undo日志记录,如果一行记录被更新,则undoLogRecord包含重建该行记录被更新之前内容所记录的信息. DB_ROW_ID:行号,包含一个随新行插入而单调递增的行id,当有InnoDB自动产生索引时,聚集索引会包含这个行id的值,否则这个行id不会出现在任何索引中. insert undoLog:事务对insert新纪录产生的undoLog,只在事务回滚时需要,并且在事务提交后,就可以立即丢弃. update undoLog:讲解重点,事务对记录进行delete,update产生的undoLog,不仅在事务回滚时需要,在快照读也需要,不能随便删除,只有在数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被perge线程删除. 日志的工作方式: 假设将Field2中的值从12变成32,修改的流程:首先用排它锁修改该行,将修改前的值拷贝一份到undoLog中,之后修改当前行的值,修改事务id(DB_TRX_ID),使用回滚指针指向undoLog的修改前的行. 在这之后假如数据库还有别的快照读在用事务在读取该日志记录,那么对应的undoLog还没有被清除,此时又有事务对同一行数据做修改,那么效果和第一张图一样,又多了一条undoLog. 当我们去执行快照读select时候,会针对我们select的数据创建出一个read view,来决定当前事务能看到的是哪个版本的数据,可能是当前最新版本的数据,也可能是undoLog中某个版本的数据,read view遵循一个可见性算法,将要修改的数据的DB_TRX_ID取出来,与系统其它活跃事务id做对比,如果大于或者等于这些事务id的话,就通过DB_ROLL_PTR去取出undoLog上一层的DB_TRX_ID,直到小于这些活跃事务id为止,这样就保证了我们获取到的事务版本是当前的最稳定的版本. 正是因为生成时机的不同,造成了RC,RR两种不同级别的可见性,在RR级别下,session在开启事务后的第一条快照读,会创建一个快照即read view,将当前系统中活跃的其它事务记录起来,此后在调用快照读的时候还是用的是同一个read view,而在read committed级别下,事务中每条select语句每次调用快照读的时候都会创建一个新的快照,这就是为什么在我们能在RC级别下看到别的事务的增删改.而在RR下,如果首次快照读是在别的事务做出增删改并提交之前,此后别的事务做了提交也读不到修改的原因. 为什么是伪MVCC呢,因为实现多版本共存只是undo串行化的结果,并没有实际实现多版本共存. 第五章 RC,RR级别下的InnoDB的非阻塞读如何实现
5.1 DB_TRX_ID ,DB_ROLL_PTR, DB_ROW_ID
5.2 undo日志
5.3 read view