Fork me on GitHub

彻底弄懂MySQL锁机制

伟大的人物都走过了荒沙大漠,才登上光荣的高峰。
巴尔扎克

前言

之前对MySQL的锁机制基本是敬而远之,因为设计的太过于复杂,而且现实中还没有遇到并发量很大的情况,故而锁也很少遇到。但随着并发量的增大,我决定要啃一下这块硬骨头!!

锁的分类

按概念分:

  • 悲观锁:

    假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
  • 乐观锁:

    假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。 乐观锁不能解决脏读的问题。

按粒度分:

  • 行锁:
    给某个数据记录加锁,实际是对索引加锁
  • 表锁:
    给某个表加锁
  • 页锁:
    给某个页加锁

乐观锁, 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。


悲观锁,顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。


行锁:锁开销大,粒度小,发生冲突概率低,但并发度高,可能出现死锁。


表锁: 锁开销小,加锁快,不会出现死锁,锁粒度大,但并发度低,发生冲突概率高


页锁:基于行锁和表锁之间

概念前提

MyISAM中读和共享一个意思,写和独占一个意思,这是因为读时就隐式加了共享,写时就隐式加了独占。
而InnoDB中不同,写和排他一个意思,读和共享不一个意思,即读也可以加排他锁,因为select不存在隐式加锁。


MyISAM采取的是读写均隐式加锁。
InnoDB采取的是基于多版本的并发控制协议----MVCC:即读不加锁,读写不冲突。(后面会提到MVCC的概念)

MyISAM中的锁

MyISAM中的锁为表锁,其只有两种模式:表共享读锁+表独占写锁。
MyISAM不会死锁的原因:Mysql一次获取了sql语句需要的所有的锁,不存在去等待他人释放的问题。

默认情况下,写锁比读锁具有更高的优先级:当一个锁释放时,这个锁会优先给写锁队列中等候的获取锁请求,然后再给读锁队列中等候的获取锁请求。 这样就不适合有大量更新操作和查询操作的情景,会出现"饿死"现象。

可以设置改变读锁和写锁的优先级:

  • 通过指定启动参数low-priority-updates,使MyISAM引擎默认给予读请求以优先的权利。
  • 通过执行命令SET LOW_PRIORITY_UPDATES=1,使该连接发出的更新请求优先级降低。
  • 通过指定INSERT、UPDATE、DELETE语句的LOW_PRIORITY属性,降低该语句的优先级。
  • 给系统参数max_write_lock_count设置一个合适的值,当一个表的读锁达到这个值后,MySQL就暂时将写请求的优先级降低,给读进程一定获得锁的机会。

对于MyISAM的加锁情况与其他用户行为关系,请看下图:
图1

隐式加锁

MyISAM在执行读写之前都会自动加锁,隐式加锁的作用如下:

注:因为隐式加锁只涉及一条sql语句所以不存在对加锁方的影响

  • 表共享读锁:不会阻塞其他用户读,但会阻塞其他用户写(也可以说不会阻塞其他用户加共享锁,但会阻塞其他用户加排他锁)
  • 表独占写锁:既会阻塞其他用户读,也会阻塞其他用户写(也可以说既会阻塞其他用户加共享锁,也会阻塞其他用户加排他锁)

显式加锁

利用lock table(s) xxxx read/write [local]命令加锁,相当于多条SQL语句形成个整体或者是会话,然后加锁。有点像InnoDB中的事务。显式加锁的作用:

  • 对加锁方而言:
    • 表读锁:当前会话只能执行加锁的表上的select,既不能对加锁的表进行update、insert、delete,也不能执行其他未加锁表上的任何操作
    • 表写锁:当前会话仍不能访问其他未加锁表的任何操作,但可以对加锁表执行select、update、insert、delete操作
  • 对其他并发用户而言:
    • 表读锁:能顺利select,执行update/delete会阻塞,如果开了并发插入功能,且无空隙那么能顺利insert,否则阻塞
    • 表写锁:做任何操作都会阻塞。

并发插入

特别需要提一下的是并发插入,对于MyISAM表存在这个原理。

如果你使用LOCK TABLES显式获取表锁,则可以请求READ LOCAL锁而不是READ锁,以便在锁定表时,其他会话可以使用并发插入。

  • 当concurrent_insert设置为0时,不允许并发插入。
  • 当concurrent_insert设置为1时,如果MyISAM表中没有空洞(即表的中间没有被删除的行),MyISAM允许在一个线程读表的同时,另一个线程从表尾插入记录。这也是MySQL的默认设置。
  • 当concurrent_insert设置为2时,无论MyISAM表中有没有空洞,都允许在表尾并发插入记录。

InnoDB中的锁

众所周知,InnoDB中的锁分为行锁和表锁,那下面就分开来谈谈行锁和表锁~~

对于InnoDB的行加锁情况与其他用户行为关系,请看下图:
图1

InnoDB的行锁

InnoDB 实现了以下两种类型的行锁:

  • 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
  • 排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。

为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB 还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁

  • 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的 IS 锁。
  • 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的 IX 锁。
隐式加锁
  • 意向锁(IS/IX)是引擎自己加,不需要用户干预
  • 对于 UPDATE、 DELETE 和 INSERT 语句, InnoDB会自动给涉及数据集加排他锁(X)
  • 对于普通 SELECT 语句,InnoDB 不会加任何锁

注:InnoDB在事务执行过程中,使用两阶段锁协议:
随时都可以执行锁定,InnoDB会根据隔离级别在需要的时候自动加锁;
锁只有在执行commit或者rollback的时候才会释放,并且所有的锁都是在同一时刻被释放。

显式加锁

select … lock in share mode //共享锁

select … for update //排他锁

由上图可以看出:

加了共享锁,其他 session 仍然可以查询记录,并也可以对该记录加 share mode 的共享锁。但是如果当前事务需要对该记录进行更新操作,则很有可能造成死锁。
而加了排他锁,其他 session仍 可以查询该记录,但是不能对该记录加共享锁或排他锁,而是等待获得锁

行锁实现及MVCC

实现方式:

InnoDB 行锁是通过给索引上的索引项加锁来实现的,这一点 MySQL 与 Oracle 不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB 这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB 才使用行级锁,否则,InnoDB 将使用表锁!

但是,如果索引是个非主键非唯一索引,那么可能存在两行数据其中的索引列数据一致,这时候加锁就会出现锁冲突!!


MVCC解释:
多版本控制: 指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。
(更多MVCC内容请参见MVCC

InnoDB的表锁

值得一提的是,InnoDB的表锁实际是由MySQL Server管理的,而非InnoDB引擎,仅当autocommit=0、innodb_table_locks=1(默认设置),InnoDB层才知道MySQL加的表锁,因此做表锁时,记得set autocommit = 0

参考资料:

https://zhuanlan.zhihu.com/p/29150809
https://segmentfault.com/a/1190000012650596

-------------本文结束感谢您的阅读-------------
坚持原创技术分享,您的支持将鼓励我继续创作!
0%