MySQL—锁(补充中...)

脏读:因为当前事务读取了另一个未提交事务写的一条记录,如果另一个事务在写记录的时候就给这条记录加锁,那么当前事务就无法继续读取该记录了,所以也就不会有脏读问题的产生了。
不可重复读:因为当前事务先读取一条记录,另外一个事务对该记录做了改动之后并提交之后,当前事务就无法修改该记录,自然也不会发生不可重复读了。
幻读:因为当前事务读取了一个范围的记录,然后另外的事务向该范围内插入了新记录,当前事务再次读取该范围的记录时发现了新插入的新纪录。
并发问题的解决方案:
怎样解决脏读,幻读,不可重复读?
方案一:读操作利用多版本并发控制( MVCC ),写操作进行加锁 。
普通的SELECT语句在READ COMMITTED和REPEATABLE READ隔离级别下会使用到MVCC读取记录。
在 READ COMMITTED 隔离级别下,一个事务在执行过程中每次执行SELECT操作时都会生成一个ReadView,ReadView的存在本身就保证了 事务不可以读取到未提交的事务所做的更改 ,也就是避免了脏读现象;
在 REPEATABLE READ 隔离级别下,一个事务在执行过程中只有 第一次执行SELECT操作 才会生成一个ReadView,之后的SELECT操作都 复用 这个ReadView,这样也就避免了不可重复读和幻读的问题。
方案二:读、写操作都采用 加锁 的方式。
读操作和写操作也要像写-写操作一样排队执行。
采用加锁的方式解决幻读问题就有一些麻烦,因为当前事务再第一次读取记录时幻读记录并不存在,所以读取的时候加锁就不知道加给谁。

小结对比发现:
采用 MVCC 方式的话, 读-写 操作彼此并不冲突, 性能更高 。
采用 加锁 方式的话, 读-写 操作彼此需要 排队执行 ,影响性能。
一般情况下我们当然愿意采用 MVCC 来解决 读-写 操作并发执行的问题,但是业务在某些特殊情况下,要求必须采用 加锁 的方式执行。

锁的不同角度分类

图片.png

对数据的操作类型划分

读锁、写锁
对于数据库中并发事务的读-读操作情况并不会引起什么问题。对于写-写、读-写、写-读这些情况可能会引起一些问题,需要使用MVCC或者加锁的方式来解决他们。在使用加锁的方式解决时,由于既要允许读-读情况不受影响,又要使写-写、读-写、写-读这些情况相互阻塞,所以MySQL实现一个由两种类型的锁组成的锁系统来解决。这两种类型的锁通常被称为共享锁和排他锁,也叫读锁和写锁。
读锁:共享锁、S。针对用一份数据,多个事务的读操作可以同时进行而不会相互影响,相互不阻塞。
写锁:排他锁、X。当前写操作没有完成前,他会阻断其他的写锁和读锁。这样就能确保在给定时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源。
注:对于innoDB来说,读锁和写锁可以加在表上,也可以加在行上。
eg:如果有一个事务T1已经获得了某行的读锁,那么此时另一个事务T2是可以去获得这行的读锁,因为读取操作并没有改变行的数据。但是,如果事务T3想获得行的写锁,则它必须要等待事务T1 T2释放掉行上的锁才可以。


图片.png

1、锁读

在采用加锁的方式解决脏读、不可重复读、幻读这些问题时
对读取的记录加S锁

select ... ... lock in mooe;
或
sekect... ...for share;

对读取的记录加X锁

select ... ... for update;

2、锁写

delete:
对一条记录做delete操作过程其实是先将B+树中定位到这条记录的位置,然后获取这条记录的X锁,再执行delete操作
update:对一条记录做update操作时分为三种情况:
1、未修改该记录的键值,并且被更新的列占用的存储空间在修改前后未发生变化。
则先在B+树中定位这条记录的位置,然后再获取一下记录的X锁,最后在原纪录的位置进行修改操作。
2、未修改该记录的键值,并且至少有一个被更新的列占用的存储空间在修改前后发生变化。
则先在B+树中定位到这条纪录的位置,然后获取一下记录的X锁,将该记录彻底删除,最后再插入一条新纪录。
3、修改了该记录的键值,则相当于再原记录上做delete操作之后再来一次insert操作,加锁操作就需要按照delete和insert的规则进行了。
insert:
新插入一条记录的操作并不加锁,通过一种隐式锁的结构来保护这条新插入的记录在本事务提交前不被别的事务访问。

表级锁,页级锁,行锁

表级的S X锁

表级别的S锁、X锁

在对某个表执行SELECT、INSERT、DELETE、UPDATE语时,InnoDB存储引擎是不会为这个表添加表级别的 S锁 或者 X锁 的。在对某个表执行一些诸如 ALTER TABLE 、 DROP TABLE 这类的 DDL 语句时,其他事务对这个表并发执行诸如SELECT、INSERT、DELETE、UPDATE的语句会发生阻塞。同理,某个事务中对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,在其他会话中对这个表执行 DDL 语句也会发生阻塞。这个过程其实是通过在 server层 使用一种称之为 元数据锁(英文名: Metadata Locks ,简称 MDL )结构来实现的。
一般情况下,不会使用InnoDB存储引擎提供的表级别的 S锁 和 X锁 。只会在一些特殊情况下,比方说崩溃恢复 过程中用到。比如,在系统变量autocommit=0,innodb_table_locks = 1 时, 手动 获取InnoDB存储引擎提供的表t 的 S锁 或者 X锁 可以这么写:
LOCK TABLES t READ :InnoDB存储引擎会对表 t 加表级别的 S锁 。
LOCK TABLES t WRITE :InnoDB存储引擎会对表 t 加表级别的 X锁 。

总结:myisam在执行查询语句前,会给涉及的所有表加锁,在执行增删改操作前,会给涉及的表加写锁。innoDB存储引擎是不会为这个表添加表级别的读锁或写锁的。

MySQL的表级锁有两种模式:(以MyISAM表进行操作的演示)
表共享读锁(Table Read Lock)
表独占写锁(Table Write Lock)


图片.png
表级别的意向锁

innoDB支持多粒度锁,它允许行级锁与表级锁共存,而意向锁就是其中的一种表锁。
分类
意向共享锁:事务有意向对表中的某些行加共享锁

事务要获取某些行的S锁,必须先获得表的IS锁
select column from table ...lock in share mode;

意向排他锁:

事务要获取某些行的X锁,必须先获得表的IX锁
select column from table ...for update;

意向锁解决的问题:
现在有两个事务,T1 T2,其中T2试图在该表级别上应用共享或排他锁,如果没有意向锁的存在,那么T2就需要检查各个页或行是否存在锁;如果存在意向锁,那么此时就会受到由T1控制的表级别意向锁的阻塞。T2在锁定该表前不必检查各个页或行锁,而只需检查表的意向锁。简单来说就是给更大一级别的空间示意里面是否已经上过锁。
在数据表的场景中,如果给某一行加上了排他锁,数据库会自动给更大一级的空间,比如数据页或数据表加上意向表,告诉其他人这个数据页或数据表已经有人上个排他锁了,这样当其他人想要获取数据表排他锁的时候,只需要了解是否有人已经获取了这个数据表的意向排他锁即可。

  • 如果事务想要获得数据表中某些记录的共享锁,就需要在数据表上添加意向共享锁。
  • 如果事务想要获得数据表中某些记录的排他锁,就需要在数据表上添加意向排他锁。


    图片.png

意向锁的并发性
意向锁不会与行级锁的共享/排他锁互斥!意向锁并不会影响到多个事务对不同数据行加排他锁时的并发性。
结论:
1、innoDB支持多粒度锁,特定场景下,行级锁可以与表级锁共存。
2、意向锁之间互不排斥,但除了IS与S兼容外,意向锁会与共享锁/排他锁互斥。
3、IX、IS是表级锁,不会和行级锁的X、S发生冲突。只会和表级的X、S发生冲突。

死锁

死锁产生的条件:
1、两个或者两个以上事务。
2、每个事务都已经持有锁并且申请新的锁。
3、锁资源同时只能被同一个事务持有或者不兼容。
4、事务之间因为持有锁和申请锁导致彼此循环等待。
注:死锁的关键在于两个(或两个以上)的session加锁顺序不一致。
如何处理死锁:

一:等待,直到锁超时
即两个事务互相等待时,当一个事务等待时间超过设置的阈值时,就将其回滚。这种方法简单有效,在innodb中,参数innodb_lock_wait_timeout用来设置超时时间。
缺点:对于在线业务来说,等待时间是不被接受的,如果将等待时间缩短,会误伤到普通的锁等待。
二:使用死锁检测进行死锁检查
innodb提供了wait_for graph算法来主动进行死锁检测,每当加锁请求无法立即满足需要并进入等待时,wait_for graph算法会被触发。
要求数据库保存锁的信息链表和事务等待链表两部分信息。

图片.png

基于信息链表和事务等待链表两部分信息可以绘制wait_for graph(等待图)


图片.png

注:死锁检测的原理是构建一个以事务为顶点,锁为边的有向图,判断有向图是否存在环,存在既有死锁。
一旦检测到有回路,有死锁,这是innoDB存储引擎会选择会回滚undo量最小的事务,让其他事务继续执行。(innodb_deadlock_detect 设置为
on ,表示开启这个逻辑。)
缺点:每个新的被阻塞的线程,都要判断是不是由于自己的加入导致了死锁,这个操作时间复杂度是O(n)。如果100个并发线程同时更新同一行,意味着要检测100*100次,1万个线程就会有1千万次检测。
如何解决:
1、关闭死锁检测,但意味着可能会出现大量的超时,回导致业务有损。
2、控制并发访问的数量。比如在中间件中实现对于相同行的更新,在进入引擎之前排队,这样在innodb内部就不会有大量的死锁检测工作。
如何避免发生死锁:
1、合理设计索引,是业务sql尽可能通过索引定位更少的行,减少锁竞争。
2、调整业务逻辑sql执行顺序,避免update/delete长时间持有锁的sql在事务前面。
3、避免大事务,尽量将大事务拆成小事务来处理,小事务缩短锁定资源的时间,发生锁冲突的几率更小。
4、在并发比较高的系统中,不要显式加锁。如select ... for update语句,如果是在事务里运行了start transaction 或设置了autocommit=0,那么就会有锁定所查到的记录。
5、降低隔离级别。如果业务允许,将隔离级别调低,将隔离级别从RR-->RC,可以避免掉很多因为gap锁造成的死锁。

锁的内存结构:

一条记录在内存中创建一个锁结构,但是记录太多生成太多的表结构也是不行的。所以在对不同记录加锁时,如果符合一下条件会放在同一个锁结构中:

  • 在同一个事务中进行加锁操作
  • 被加锁的记录在同一个页面中
  • 加锁的类型是一样的
  • 等待的状态是一样的


    图片.png

    结构解析:
    1、表所在事务信息
    不论是表锁还是行锁,都是在事务执行过程中生成的,哪个事务生成了这个锁结构,这里就记录这个事务的信息。此锁所在的事务信息在内存结构中只是一个指针,通过指针可以找到内存中关于该事务的更多信息,比如事务ID等。
    2、索引信息
    对于行锁来说,需要记录一下加锁的记录是属于哪个索引的。这里也是一个指针。
    3、表锁/行锁信息

  • 表锁:记载着是对哪个表加的锁
  • 行锁:记载着
    Space ID :记录所在表空间。
    Page Number :记录所在页号。
    n_bits :对于行锁来说,一条记录就对应着一个比特位,一个页面中包含很多记录,用不同的比特位来区分到底是哪一条记录加了锁。为此在行锁结构的末尾放置了一堆比特位,这个n_bits 属性代表使用了多少比特位。
    注:n_bits的值一般都比页面中记录条数多一些。主要是为了之后在页面中插入了新记录后也不至于重新分配锁结构。
    4、type_mode
    这是一个32位的数,被分成了 lock_mode 、 lock_type 和 rec_lock_type 三个部分。


    图片.png

    锁的模式( lock_mode ),占用低4位,可选的值如下:
    LOCK_IS (十进制的 0 ):表示共享意向锁,也就是 IS锁 。
    LOCK_IX (十进制的 1 ):表示独占意向锁,也就是 IX锁 。
    LOCK_S (十进制的 2 ):表示共享锁,也就是 S锁 。
    LOCK_X (十进制的 3 ):表示独占锁,也就是 X锁 。
    LOCK_AUTO_INC (十进制的 4 ):表示 AUTO-INC锁 。
    在InnoDB存储引擎中,LOCK_IS,LOCK_IX,LOCK_AUTO_INC都算是表级锁的模式,LOCK_S和LOCK_X既可以算是表级锁的模式,也可以是行级锁的模式。
    锁的类型( lock_type ),占用第5~8位,不过现阶段只有第5位和第6位被使用:
    LOCK_TABLE (十进制的 16 ),也就是当第5个比特位置为1时,表示表级锁。
    LOCK_REC (十进制的 32 ),也就是当第6个比特位置为1时,表示行级锁。
    行锁的具体类型( rec_lock_type ),使用其余的位来表示。只有在lock_type 的值为LOCK_REC 时,也就是只有在该锁为行级锁时,才会被细分为更多的类型:
    LOCK_ORDINARY (十进制的 0 ):表示 next-key锁 。
    LOCK_GAP (十进制的 512 ):也就是当第10个比特位置为1时,表示 gap锁 。
    LOCK_REC_NOT_GAP (十进制的 1024 ):也就是当第11个比特位置为1时,表示正经记录锁 。
    LOCK_INSERT_INTENTION (十进制的 2048 ):也就是当第12个比特位置为1时,表示插入意向锁。其他的类型:还有一些不常用的类型我们就不多说了。
    is_waiting 属性呢?基于内存空间的节省,所以把 is_waiting 属性放到了 type_mode 这个32位的数字中:
    LOCK_WAIT (十进制的 256 ) :当第9个比特位置为 1 时,表示is_waiting 为 true ,也就是当前事务尚未获取到锁,处在等待状态;当这个比特位为 0 时,表示 is_waiting 为false ,也就是当前事务获取锁成功。
    5、其他信息
    为了更好的管理系统运行过程中生成的各种锁结构而设计了各种哈希表和链表。
    6、一堆比特位
    如果是 行锁结构 的话,在该结构末尾还放置了一堆比特位,比特位的数量是由上边提到的 n_bits 属性表示的。InnoDB数据页中的每条记录在 记录头信息 中都包含一个 heap_no 属性,伪记录 Infimum 的heap_no 值为 0 ,Supremum 的 heap_no 值为 1 ,之后每插入一条记录, heap_no 值就增1。 锁结构 最后的一堆比特位就对应着一个页面中的记录,一个比特位映射一个 heap_no ,即一个比特位映射到页内的一条记录。

锁监控

通过innoDB_row_lock的等状态变量来分析系统上的行锁的争夺情况。


图片.png

对各个状态量的说明如下:
Innodb_row_lock_current_waits:当前正在等待锁定的数量;
Innodb_row_lock_time :从系统启动到现在锁定总时间长度;(等待总时长)
Innodb_row_lock_time_avg :每次等待所花平均时间;(等待平均时长)
Innodb_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间;
Innodb_row_lock_waits :系统启动后到现在总共等待的次数;(等待总次数)

next-key lock的加锁规则
总结的加锁规则里面,包含了两个 “ 原则 ” 、两个 “ 优化 ” 和一个 “bug” 。

  1. 原则 1 :加锁的基本单位是 next-key lock 。 next-key lock 是前开后闭区间。
  2. 原则 2 :查找过程中访问到的对象才会加锁。任何辅助索引上的锁,或者非索引列上的锁,最终都要回溯到主键上,在主键上也要加一把锁。
  3. 优化 1 :索引上的等值查询,给唯一索引加锁的时候, next-key lock 退化为行锁。也就是说如果InnoDB扫描的是一个主键、或是一个唯一索引的话,那InnoDB只会采用行锁方式来加锁
  4. 优化 2 :索引上(不一定是唯一索引)的等值查询,向右遍历时且最后一个值不满足等值条件的时候, next-keylock 退化为间隙锁。
  5. 一个 bug :唯一索引上的范围查询会访问到不满足条件的第一个值为止。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容