Innodb - 有关事务的一切
终于开始聊事务了。
所谓事务
刚学数据库的时候,就知道事务。然而什么是事务,只能模模糊糊说出两个概念:要么不做,要么全做;ACID。说懂不很懂,说不懂又差不多懂。直到最近看到一句话:数据库的操作变更和现实世界的实际情况并不总是一致的,我们只是想办法让数据库的操作符合现实世界的状态转换。
恍然大悟!
数据库已经做了一些和现实世界一致的约束,not null、unique key,甚至还有一些check语法,比如年龄恒大于0。然而,还是有一些操作会出现和现实的不一致:
- 现实中A向B转账,A减少的同时B应该增加。现实中,这应该是个“同时发生”的操作;
- 实际上数据库里的A向B转账,A减了,再去B里增加,可能就出意外宕机了,导致A减了B也没加,就和现实不一致了。归根结底,程序在操作的时候,就算间隔再短,也还是个“分步操作”,并不能做到现实中的“同时”;
所以,数据库引入了事务的概念:虽然实际操作上不能做到和现实一模一样,但是从逻辑上,数据库要模拟出现实世界的行为。
比如上面的转账,数据库可能呈现三个状态:
- 转账前:A和B的余额都没动;
- 转账中:A减了,B还没加;
- 转账后:A减了,B加了;
现实世界只有第一种和第三种状态,不存在第二种状态,数据库就想办法不让自己停留在第二种状态。
什么时候会停留在第二种状态?A扣减之后,崩了,B没加上,就停在第二种状态了。重启后,数据库会通过undo日志撤销这一状态,强行让自己回到第一种状态,从而和现实的逻辑保持一致。
为了让自己符合现实世界的状态,数据库想了想,需要保证这几条:ACID。
原子性Atomicity:单个事务要做的
这个大家都会:要么不做,要么全做。
为什么?因为现实世界有些事情是“同时”发生的,比如上面的转账。而数据库不是同时发生的,分了好几步,所以为了让自己模拟出同时的效果,就“要么不做,要么全做”。
这就是原子性。
为了达到不让数据库“停留在中间状态”的效果,数据库使用了undo日志,让只做到一半的事务回滚到开始的状态。
持久性Durability:单个事务要做的
数据写磁盘实际是先写到buffer pool,后续再写到磁盘。写了buffer pool如果崩了,等于没写。也就是说事务提交后产生的数据没有真正保存下来。
为了让数据一定保存下来,MySQL使用了redo日志:
- 事物的修改操作,先写redo日志;
- 再把数据写入buffer pool;
- 最后提交事务;
数据库使用redo日志让崩溃的数据库恢复到崩溃前的状态。如果崩溃前“做了”(提交了事务,只是还没有刷到磁盘),重启后也能恢复到“做了”的效果。
隔离性Isolation:事务之间不能相互影响
从A账户扣钱,分别转给B账户2元、C账户3元,现实世界里,无论这两次转账是分别进行,还是“同时”进行,都得保证A的账户被扣了5元。但是在程序里,两个线程并发操作的情况下,可能会出现:
- 线程1读A余额为10;
- 线程2读A余额为10;
- 线程1扣2元;
- 线程2扣3元;
- 线程1写回8元;
- 线程2写回7元;
最终A的余额为7元,只扣了3元,而不是5元。这是不符合现实世界的逻辑的。
所以事务有另一个要求:其他的事务不能影响到本事务的状态,就好像隔离的一样。
隔离性怎么做到?串行,一定可以做到。但为了效率,一般都不使用串行:
- 写事务和写事务隔离:加锁;
- 写事务和读事务隔离:MVCC;
一致性Consistency:最终想达到的状态
一致,指的是和现实世界的一致。这是程序最终想达到的状态。
如果没有原子性,一致性肯定不可能达成;同理如果没有隔离性、持久性,一致性也不可能达成。但是即使A、I、D都满足了,一致性也未必达成,还需要数据库保证(not null、unique),程序员保证(余额扣减之后,要给别的账户加上)。
总之,一致性是让程序的表现和现实一致,是程序最终想要达到的效果。
它和数据库本身的事务实现好像并没有什么关系……
事务语法
开启事务:
1
BEGIN;
或者:
1
START TRANSACTION;
提交事务:
1
COMMIT;
回滚事务:
1
ROLLBACK;
但是回滚事务有点儿伤,类似于从头再来。如果能像打游戏一样不停保存,每次挂了就可以从上次存档的地方恢复了。savepoint就是存档的点:
1
SAVEPOINT <name>;
回滚到上一个存档点:
1
RELLBACK TO SAVEPOINT <name>;
游戏的存档点。存档后可劲儿造啊。
autocomit
默认情况下,如果不使用begin显式开启事务,所有的语句都是一个独立的事务。也就是说会自动提交。可以把这个变量改为false,就需要显式commit了。
redo log:为了提升性能的同时保证一致性
Innodb - Buffer Pool介绍了innodb使用buffer pool作为缓存来加快记录的读写。
buffer pool的页在修改之后成为脏页,为什么不直接刷到磁盘?
- 每个页16KB,一次就要写16KB数据。一个page即使改动一点儿也要刷16KB,太浪费;
- 每次刷的page不定,所以是随机IO;
因此innodb 使用redo log,记录事务对数据页进行了哪些修改,然后把redo log记到磁盘上。即使buffer pool的脏页还没有持久化到磁盘上数据库就发生了崩溃,也可以根据redo log,把这些要进行的修改replay一遍,数据库又会恢复到崩溃前的状态。
为什么把redo log写到磁盘就高效?
- redo log是一条一条顺序写的,所以是 顺序IO;
- 单条redo log占用空间小,肯定比一个page小,所以 写的少;
因此redo log写得又少又快,比直接写脏页高效。
可以比较粗暴地理解为:写redo log到磁盘,就相当于“写脏页到磁盘”了。
说一千道一万,redo log的存在,本质上是因为buffer pool不愿意在更新后刷盘,如果更新后立刻刷新到磁盘上,redo log就没有存在的意义了。所以说,redo log的存在是为了提升性能,如果不考虑性能,它并不是功能上必须需要的东西。
当然,如果直接写磁盘,不使用buffer pool,也不会有持久性什么事儿了。如果直接写磁盘,innodb可以保证数据一定是持久的。
又一个pool:redo log刷盘时机
事实上redo log也有类似于buffer pool的log pool,redo log也不是直接往磁盘写,而是先往log pool写,再周期性写到磁盘上。
额,又一个pool???引入redo log就是因为有个buffer pool。现在同样的问题——redo log还没从log pool写到磁盘上数据库就崩溃了怎么办?再来个redo redo log?
不需要。redo log还有一个很重要的刷盘时机是事务提交时。也就是说,事务提交完成前,redo log一定会先刷盘完成。如果redo log没刷盘就崩了,说明事务没完成就崩了。一个没完成的事务,有没有写到磁盘上都不重要:
- 如果没刷脏:相当于事务没做,符合事务的特性;
- 如果刷脏了:相当于记录了一个半成品的事务,结果还是要被回滚掉,回到没做前的状态,所以刷盘也和没刷盘一样
所以log pool和buffer pool不一样:
- buffer pool在事务完成后未必刷盘,所以需要redo log先刷盘,时刻准备在崩溃后恢复未刷盘的数据;
- log pool在事务完成后一定已经刷盘了,此时就算崩溃,也无所谓了;
- 如果事务在完成前就崩了,那刷不刷盘都不重要:如前所述。
所以redo log不需要再搞个redo redo log。不然岂不无限套娃,没完了……
redo log影响多大性能
虽然不直接刷buffer pool的脏页到磁盘而是选择写入顺序的redo log里,已经极大提升了性能。但实际上,redo log刷盘时机也可以优化。redo log默认是一个事务结束时,redo log才会刷盘,对于语句比较多的事物,有多大影响?
参考这个,如果事务里有1000条update,组成一个事务,时间能从4s降到0.2s,还是很多的。
redo log结构
redo log为什么很小?因为需要记录的东西不多:
- type:redo log类型;
- space id:表空间id;
- page number:页号;
- data:redo日志的内容;
比如有种极其简单的redo log类型,data部分是“offset、数据”,代表“在哪个表的哪个page的哪个offset写入xxx”。所以内容很小。
其他类型的redo log,比如范围修改,删除某区间内的记录,如果每条修改都对应一条redo log,太占空间了,所以有MLOG_COMP_LIST_START_DELETE
/MLOG_COMP_LIST_END_DELETE
类型的redo log,一个start一个end就确定了所有的范围。
redo log类型很多,具体有哪些类型,不用操心。
MTR: redo log组
数据库内的某些操作,本身必须得是原子性的,比如向B+树插入一条记录,可能涉及多条修改:页分裂、数据页插入了记录、给新的页在非叶子节点里创建一条目录项。不能说只做前两步,不做最后一步,这样的B+树是不完整的。
这种对底层页面的原子访问,就是MTR(mini transaction)。
一次插入操作实际对应好几处修改,每个修改的地方都需要redo log。为了保证MTR,这一组redo log要么都生效,要么都不生效,不能说这一组redo log“只记录了一半”,系统恢复的时候也只“恢复一半”。
实际实现方式,是给redo log组的最后添加一条独立的MLOG_MULTI_REC_END
类型的redo log,代表一组结束了。如果重启之后发现redo log最后没有一条这样的结束log,就放弃这一组。
而那些本身就只有一条的redo log,有标志表明他们本身就是单条的,就不需要再加一条额外的end redo log了。
checkpoint:开始恢复的点
如果数据库重启,从哪里恢复redo log?从数据库刚建立时候的空白页吗?不现实。有两个原因:
- 从头恢复很慢;
- redo log没那么大的地方,记录不了那么多redo log;
redo log是循环使用的,也就是说redo log的空间写满之后,从开头接着写,覆盖掉一开始的redo log。如果覆盖了,redo log就丢了。所以 在redo log写“追尾”之前,必须得把要覆盖的那些redo log对应的脏页刷盘。如果脏页刷盘了,对应的redo log就没用了,可以被覆盖了。
脏页刷盘的行为就叫checkpoint,也就是说,下次如果要恢复,从这个点开始恢复就行了,没必要从头恢复(况且头也没了……已经被覆盖了……)。checkpoint就是数据库开始恢复的起点。
每个redo log对应一个编号,叫lsn(log sequence number)。刷脏之后,会产生一个checkpoint_lsn,它就是恢复的起点。
checkpoint不是savepoint。
redo log的终点在哪儿?最后一个没写满的redo log的page。
我咋感觉终点就是lsn对应的那条redo log呢?
checkpoint时机
- sharp checkpoint:完全检查点。产生于正常关机时,所有脏页完全刷脏,所以redo log都没用了;
- fuzzy checkpoint:部分页刷脏
- 周期性刷脏;
- buffer pool快满了;
- redo log快满了;
checkpoint更多参考:
- https://www.cnblogs.com/geaozhang/p/7341333.html
快速恢复
确定了起点终点,redo log就可以一条一条恢复数据了。但有innodb还是嫌慢,所以使用hash,把同一个表的同一个page的所有redo log放在一起执行,然后这个页面刷脏一次就行了。等等等等,反正做了很多优化。
这不是我数据同步的时候合并binlog的思路嘛 :D
undo log:为了原子性
undo log是为了撤销已有操作,撤销就是在悔棋。悔棋的前提是记得上一步、甚至上几步的状况。如果忘了三步之前是什么样子的,想悔三步棋也悔不了。所以undo log需要记录每一步做了什么操作,从而能够在想要反悔的时候顺利恢复到之前的情况。
和悔棋不同的是,undo log不是什么情况都能悔棋,只能在事务操作失败后,回滚只做了一半的事务操作,是为了保证原子性。
undo,一定是对do的撤销。select这种只读语句对数据没有影响,自然不需要撤销。事务提交之后,也没法后悔了,只能在提交前撤销已经做了的操作。所以 undo log撤销的是含有增删改的事务的某些操作步骤。
innodb只对拥有增删改操作的事务分配事务id,只读事务没有事务id。那些不包含增删改操作的读写事务也没有事务id。
事务id是全局的。innodb不想每次更新事务id都持久化到磁盘上,又怕崩了之后事务id丢失,所以每增加256时持久化一次,减少持久化次数。下次重启的时候,把持久化的事务id加上256,崩溃时候的事务id肯定小于这个数,从而做到事务id的唯一性。innodb经常用这个套路,比如隐藏的自增主键值也是这么搞的。
很鸡贼,可以学学。
undo log格式
undo log记录的是增删改中的一种:
- insert:插了一条记录,把它删掉就行了;
- delete:删了一条记录,把它加回来就行了;
- update:改了一条记录,把它改回去就行了;
所以undo log的记录有以下几个通用field:
- type:上述三种之一;
- undo no:undo日志的id,不是全局的,每个事务中都从0开始;
- table id:表id;
- 主键列表:每一个构成主键的列的
<存储空间大小,真实值>
构成的列表。有了主键的值,就能定位这条数据了;
insert’s undo log
撤销insert,就是把它删掉。上面这么多字段就够了。因为想删掉这条记录,知道它的主键就够了。
类型为
TRX_UNDO_INSERT_REC
。
delete’s undo log
撤销delete,就是把删掉的记录再加回来。似乎在删除的时候需要把这条记录所有的列的值都记录下来,这样在恢复时才能把旧值都找回来。
如果真这么做,那在delete操作比较多的情况下,undo log岂不是相当于变相copy了一遍数据库?innodb才不会这么低效!每次delete,innodb都只是标记删除,是一种逻辑删除,并没有在物理层面把记录真的删掉。
所以Innodb - 行里说到,每一行都有一个delete flag。
但是,标记为已删除的这一行并没有被加入这一页的垃圾链表。因为加入垃圾链表之后,新插入的列就有可能覆盖这个位置,导致旧行被抹掉。只要不加入垃圾链表,这一行就处于一种被删了,但又不会被抹掉的状态。
事务:只有我知道你被删了,别的事务都不知道。
什么时候加入垃圾链表呢?当事务被提交之后。因为提交后的事务就不能反悔了,删了就是删了,被别人覆盖了抹掉了都无所谓,已经不需要再找回来了。
事务提交之后undo log还存在吗?见后文。
delete undo log在通用field的基础上,还加了几个field:
- trx_id:本次修改这条记录的事务的id;
- roll_pointer;
这两个就是Innodb - 行里说到的行的两个隐藏列。把一个列标记删除之前,它的roll_pointer的值记录为undo log的地址(其实就是这条记录的指针指向这条undo log),同时这条undo log的指针指向前一条修改这个列的undo log。这么一来,所有对这条记录的增删改操作的undo log,倒着连成了一条串,叫做 版本链:
1
记录 -> 倒数第一个undo log -> 倒数第二个undo log -> ... -> 第一个undo log(也就是insert undo log)
版本链记录着这条记录的各个版本的内容(其实是可以根据这些undo log恢复到各个版本的内容,所以相当于记录了各个版本的内容)。
类型为
TRX_UNDO_DEL_MARK_REC
。
update’s undo log
撤销update,就是把改掉的值再改回来。
update分为两种情况:
- 没有修改主键:需要记录下这条记录在修改前的值;
- 改了主键,其实就相当于先delete一条记录,再insert一条记录:需要先把insert的删了,再把delete的插回来。
没修改主键的情况下,update undo log 要保留被修改列的旧值,用于恢复:
- n_updated:包含各个被修改的列的
<位置,旧值长度,旧值内容>
; - trx_id;
- roll_pointer:这两列肯定也是需要的,和delete undo log一样,用于构建版本链;
类型为
TRX_UNDO_UPD_EXIST_REC
。
修改主键的情况:
- 先把记录标记删除,但不加入垃圾链表。产生一个delete undo log;
- 再插入一条数据。产生一个insert undo log;
类型为
TRX_UNDO_DEL_MARK_REC
和TRX_UNDO_INSERT_REC
。
undo log page
表空间有许多种page组成。index page放的是数据,undo log page是用来放undo log的。
undo log存储的时候分为两大类:insert undo log、update undo log(包括update undo log和delete undo log),两类log存的时候隔离开,因为insert undo log在事务结束后就可以删掉了,其他类型的undo log还要留着,MVCC会用到。
很显然,只要是page,innodb都会把它们组成page链表。undo log page也是。
不同事务的undo log分别写入不同的undo log链表。
回滚
一个undo log链表只保存一个事务的undo log,所以每个undo log链表都有标志,代表这个事务是否提交了。如果重启后,事务还处于活跃状态,说明崩的时候它还没有结束,这就是个半成品事务,需要回滚。按照undo log的记录全部回滚就行了。
如果说的复杂点,把redo log也掺和进来,就是:
- redo log把数据恢复到崩溃前的状态(把本来应该写盘但是没写盘的脏页写盘);
- 然而崩溃前有的事务可能只部分执行(redo log把这些部分执行的也写盘了);
- 所以undo log把未完成的事务回滚到事务进行前的状态;
如果把buffer pool理解为一个“有脏页就写盘”的东西,那redo log的作用就可以忽略了,只需要理解undo log的作用就行了,理解起来更简单。
参考:
- https://www.zhihu.com/question/368847138/answer/996629614
事务隔离级别
一条数据是可能在多个事务中并发访问的。隔离性就是在说,并发的事物之间不能相互干扰。
怎么保证并发事务访问不会让数据出错?这一点了解Java并发编程应该就能给出很好的回答:加锁!
- 串行执行可以看做加了一个非常粗粒度的锁,导致一个线程执行的整个过程中另一个线程只能等待,直到这个线程执行完了,另一个才能执行;
- 尽可能细粒度地加锁,能够最大限度地提高并发度:只在必须互斥访问的时候才互斥访问,尽量不让一个线程阻塞另一个线程;
加锁就能解决并发问题。只不过锁粒度加的不一样,性能不一样罢了:
- 最粗暴的情况,加个互斥锁比如synchronized、ReentrantLock;
- 优化一点儿使用读写锁:ReentrantReadWriteLock;
- 或者根据实际情况使用乐观锁、CAS;
无论怎样,问题都是能解决的,正确性都是能得到保障的。
但数据库的想法更骚气——有没有可能用户会接受这么一种观念:在某些情况下,为了让并发访问的性能更高一些,即使没法完全保证正确性也是可以的?即:牺牲部分隔离性(不做到并发事务之间的完全隔离),来换取性能的提升。
可以想象为地铁安检:
- 100%安全的安检需要对每个人非常仔细的检查,虽然绝对安全,但为此给大家带来非常大的不便;
- 如果不检查那么严格,通行效率就高了,但是这就意味着有时候回不小心漏过去一些小危险品,比如水果刀,使得安全不能得到100%的保证。但也不是不能接受;
- 完全不检查,或者几乎没检查,通行毫无障碍,效率非常高,但是带着管制刀具也不会被查出来,安全度极大下降;
虽然人们几乎都不接受最后一种情况,但是不见得人人都会喜欢第一种情况。相反,如果第二种情况在通行效率和安全之间的权衡能得到大家的满意,它可能会成为大家的首选。
事物的隔离级别就是这样:不见得需要100%不出现并发错误、100%保证事务的隔离性。有时候为了性能牺牲点儿隔离性可能完全是可以接受的。
隔离级别
既然如此,还回到地铁的例子,那接下来要做的就是要给安全性分分级了,看看大家能接受的安全性是哪种:
- 城市A认为安全最重要,所以最终选择了100%安全的方案;
- 城市B认为有点儿小事故无所谓,所以选择了90%安全的方案,换取一定的通行效率;
- 城市C认为出现事故不重要,平时通行欢快才重要,所以选择了20%安全的方案;
同样地,数据库也会对隔离性划分一下等级:无关紧要的并发问题,有些人就觉得没必要保证,严重的问题,大家一致认为需要避免。
读一个数据是原子性的,不存在所谓的事务问题。所以一般举例子都是举读两个数据的例子,比如:读一个点的
(x, y)
坐标。理论上,读到的x和y必须都是旧值(读得早),或者都是新值(读得晚),不能说一个坐标读到了旧值,一个坐标读到了新值,这样的点不存在。
(我觉得直接看MVCC解决这些问题的方式,比看这些问题更直观一些,毕竟这些问题太抽象了……)
脏写 Dirty Write
一个事务修改了一个数据,还没提交;另一个事务也修改了这个数据。
比如两个事务分别要修改(1, 2)为(3, 4)和(5, 6),结果改出了一个(5, 4)。
它会发生在写-写的场景,严重性最高,因为如果不处理的话,数据就出错了。
写包裹了另一个写。
脏读 Dirty Read
脏读:读到的数据是不干净的。何谓不干净?就是已经被修改了。所以脏读指的是一个事务修改了数据,还没提交;另一个事务此时读到了这个数据。那它读的其实是不准的。
比如一个事务修改(1, 2)为(3, 4),另一个线程读点的坐标,结果读到了(3, 2)这个不存在的点。
它会发生在写-读的场景,严重性次之。
写包裹了另一个读。
不可重复读 Non-Repeatable Read
一个未提交事务读取了一个数据;另一个事务修改了这个数据。
比如一个线程读点(1, 2)的坐标,另一个事务修改(1, 2)为(3, 4),结果读到了(1, 4)这个不存在的点。
它会发生在读-写的场景。
读包裹了另一个写。
幻读 Phantom
一个事务按照条件查到一些记录,事务没提交,再查,结果跟上次不一样了。两次看到的结果不一样,是眼花了吗?所以叫幻读。
比如一个线程读点(1, 2)的坐标,另一个事务修改(1, 2)为(3, 4),第一次读到了(1, 2),然后同一个事务里第二次读到了(3, 4)。
它会发生在读写的场景,严重性最低。
读包裹了另一个写,但是和不可重复读区别在,读到的数据都是真的,只不过两次读到的不一样。所以它严重性最低,因为都是真实数据。
有了四种隔离性问题,数据就指定了四种隔离级别,分别代表能满足不同的隔离性问题:直接看下一章。
MVCC
Multi-Version Concurrency Control,多版本并发控制。之前已经说了,记录的每次修改,都会由undo log记录下当前版本的信息。所以所有的修改记录穿成了一串,成为了版本链。
READ UNCOMMITTED:不解决脏读
每次都读版本链最新的数据,非常激进地获取新数据,以至于另一个事务还没提交,修改也被它读到了。所以会脏读。
READ COMMITTED:解决脏读,不解决可重复读
每次 读取前,给所有的事务snapshot一下,如果本次snapshot里的事务已经提交,就可以读到它的数据。否则认为读不到,继续看它的更老的(前一条undo log)历史。
所以它只能读到已提交事务的数据(要不然为啥叫READ COMMITTED),就不会产生脏读。
但是两次读取,生成两个snapshot,可能第一次snapshot时另一个事务还没结束,所以第一次没读这个事务的写数据,第二次snapshot时它就结束了,所以第二次读了它的写数据。不可重复读。
READ REPEATABLE:解决不可重复读,不解决幻读
第一次 读取前,给所有事务snapshot一下,只读已提交事务的数据。所以两次读取,中间即使有事务变成已提交了,还是读不到它的数据。
也就是说,在我读的过程中,无论你怎么修改这些数据,我都读不到。所以保证了可重复读。
SERIALIZABLE:解决幻读
加锁串行来访问记录,读期间别人连其他行的数据也改不了,数据自然不会幻读。
MVCC的本质:爱读哪个是你的自由
所以MVCC的本质就是——数据的任何历史版本都记下来了,愿意读哪个是你自己的选择:
- 总想读最新的,就会脏读;
- 每次总想读已提交的最新的,不会脏读,但会不可重复读;
- 只想读第一次时候的已提交的最新的,就能可重复读;
- 只有串行,我读的时候别人啥也别动,连其他行也都别改,才能防止幻读。
MVCC不能管控写写
MVCC仅能处理读与写的情况:我写的时候,爱读哪个是你的自由。但是解决不了多个事务都要写同一条记录的场景,innodb加锁来保证两个事务不能交叉更新同一条记录。所以脏写是通过加锁解决的,上述四种隔离级别都能解决脏写。
- MVCC为什么效率高?读和写不冲突,读的可以不是在写的版本。
- MVCC为什么功能强?读哪个版本是你的自由。读的版本不一样,体现出来的功能就不一样。
参考:
- https://www.cnblogs.com/kismetv/p/10331633.html
锁
这里的锁仅指数据库的锁。
写写
如前所述,用锁来避免写写冲突。
读写
如果Java并发见多了,第一反应应该用读写锁。或者如果读多写少,乐观锁也可以。能够避免读读阻塞。
MVCC
但是数据库里,提供了另一种思路——MVCC。即写和读不是同一个地方,不会冲突,所以完全不用加锁,性能更好。但是引入了多个版本,会消耗空间,相当于以空间换时间。同时,因为读可以采用不同的策略,能产生不同的隔离性。
共享锁、独占锁(排它锁)
数据库虽然有MVCC,但是也提供了类似于读写锁的方式解决读写问题:
- 共享锁:Shared Lock,简称S,其实就是读锁;
- 独占锁(排它锁):Exclusive Lock,简称X,其实就是写锁;
二者共同构成读写锁的理念。
他们就是读写锁,所以他们共享互斥的关系,完全按照读写锁去理解就行。
加读锁:
1
SELECT ... LOCK IN SHARE MODE;
加写锁:
1
SELECT ... FOR UPDATE;
多粒度锁
就像Java尽量加细粒度的锁,以让线程之间执行效率更高一样,innodb也提供了不同粒度的共享锁或者独占锁:
- 行锁:给这一行加读锁,或写锁;
- 行共享锁;
- 行独占锁;
- 表锁:给这个表加读锁,或写锁;
- 表共享锁;
- 表独占锁;
显然,如果有行写锁,表是不能加读锁也不能加写锁的。
但是,如果想要给表加锁,怎么才能知道这个表的某些行是否已经加了某种行锁呢?遍历?
遍历是不可能比遍历的,这辈子都不可能遍历的。
很简单,行加锁了,在表处报备一下不就行了!
所以innodb又提出了所谓的意向锁:
- 意向共享锁:Intention Shared Lock,IS锁。给行加共享锁的时候,给表加个IS锁,报备一下;
- 意向独占锁:Intention Exclusive Lock,IX锁。给行加独占锁的时候,给表加个IX锁,报备一下;
所以现在:
- 想加个表共享锁,只要看看有没有表IX锁,就知道有没有行加了写锁;
- 想加个表独占锁,只要看看有没有表IS锁或IX锁,就知道有没有行锁;
MyISAM只支持表锁,不支持行锁。
关于表锁和行锁,还可以看看:
- https://www.cnblogs.com/itdragon/p/8194622.html
加锁解决REPEATABLE READ?
既然对于读写的情况,innodb提供了锁和MVCC两种解决方案。那锁是怎么实现MVCC类似可重复读级别的隔离呢?
innodb的人真的有意思……还用了多种解法……
- Record Lock:行锁;
- Gap Lock:只要给一行加了gap lock,它和它前一条记录之间的区间不能再插值。防止幻读的;
- Next-Key Lock:Record Lock + Gap Lock,既锁住了这一行,又锁住了它前面的区间;
- Insert Intention Lock;
具体怎么玩的,累了……
死锁
说到锁,就不得不提一下死锁。如何避免死锁,学操作系统时介绍的很详细,比如采用相同的加锁顺序等等……
感想
想不动了……明天回老家,冲冲冲~