2022-09-19

3.1 从数据操作的类型划分:读锁、写锁

对于数据库中并发事务的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">读-读</span>情况并不会引起什么问题。对于<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">写-写</span>、<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">读-写</span>或者<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">写-读</span>这些情况可能会引起一些问题,需要使用<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">MVCC</span>或者<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">加锁</span>的方式来解决它们。在使用<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">加锁</span>的方式解决问题时,由于既要允许<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">读-读</span>情况不受影响,又要<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">写-写</span>、<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">读-写</span>或者<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">写-读</span>情况下的操作<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">相互阻塞</span>,所以MySQL实现了一种由两种类型的锁组成的锁系统来解决。这两种类型的锁通常被称为共享锁(Shared Lock,S Lock)排他锁(Exclusive Lock, X Lock),也叫读锁(read lock)写锁(write lock)

  • <span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">读锁</span>:也称为<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">共享锁</span>、英文用<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S</span>表示。针对同一份数据,多个事务的读操作可以同时进行而不会互相影响,相互不阻塞的。
  • <span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">写锁</span>:也称为<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">排他锁</span>、英文用<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X</span>表示。当前写操作没有完成之前,它会阻断其他写锁和读锁。这样就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源。

需要注意的是对于InnoDB引擎来说,读锁和写锁可以加在表上,也可以加在行上。

举例(行级读写锁:)如果一个事务T1已经获得了某个行r的读锁,name此时另外的一个事务T2是可以取获得这个行r的读锁的,因为读取操作并没有改变行r的数据;但是,如果事务T3想获得行r的写锁,则必须等待事务T1、T2释放行r上的写锁才行。

总结:这里的兼容是指对同一张表或记录的锁的兼容性情况。

X 锁 S 锁
X 锁 不兼容 不兼容
S 锁 不兼容 兼容
1. 锁定读

在采用<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">加锁</span>方式解决<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">脏读</span>、<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">不可重复读</span>、<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">幻读</span>这些问题时,读取一条记录时需要获取该记录的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S锁</span>,其实是不严谨的,有时候需要在读取记录时就获取记录的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>,来禁止别的事务读写该记录,为此MySQL提出了两种比较特殊的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">SELECT</span>语句格式:

  • 对读取的记录加<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S锁</span>:

    SELECT ... LOCK IN SHARE MODE;
    # 或
    SELECT ... FOR SHARE;#(8.0新增语法)
    

    在普通的SELECT语句后边加<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">LOCK IN SHARE MODE</span>,如果当前事务执行了该语句,那么它会为读取到的记录加<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S锁</span>,这样允许别的事务继续获取这些记录的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S锁</span>(比如说别的事务也使用<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">SELECT ... LOCK IN SHARE MODE</span>语句来读取这些记录),但是不能获取这些记录的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>(比如使用<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">SELECT ... FOR UPDATE</span>语句来读取这些记录,或者直接修改这些记录)。如果别的事务想要获取这些记录的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>,那么他们会阻塞,直到当前事务提交之后将这些记录上的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S锁</span>释放掉。

  • 对读取的记录加<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>

    SELECT ... FOR UPDATE;
    

    在普通的SELECT语句后边加<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">FOR UPDATE</span>,如果当前事务执行了该语句,那么它会为读取到的记录加<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>,这样既不允许别的事务获取这些记录的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S锁</span>(比方说别的事务使用<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">SELECT .. LOCK IN SHARE MODE</span>语句来读取这些记录),也不允许获取这些记录的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>(比如使用<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">SELECT ... FOR UPDATE</span>语句来读取这些记录,或者直接修改这些记录)。如果别的事务想要获取这些记录的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S锁</span>或者<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>,那么它们会阻塞,直到当前事务提交之后将这些记录上的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>释放掉。

MySQL8.0新特性:

在5.7及之前的版本,SELECT ... FOR UPDATE,如果获取不到锁,会一直等待,直到<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">innodb_lock_wait_timeout</span>超时。在8.0版本中,SELECT ... FOR UPDATE, SELECT ... FOR SHARE添加<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">NOWAIT</span>、<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">SKIP LOCKED</span>语法,跳过锁等待,或者跳过锁定。

  • 通过添加NOWAIT、SKIP LOCKED语法,能够立即返回。如果查询的行已经加锁:

    • 那么NOWAIT会立即报错返回
    • 而SKIP LOCKED也会立即返回,只是返回的结果不包含被锁定的行。
    select * from t1 for update nowait;
    
    select * from t1 for update skip locked;
    
2. 写操作

平常所用到的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">写操作</span>无非是<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">UPDATE</span>、<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">DELETE</span>、<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">INSERT</span>这三种:

  • <span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">DELETE</span>:

    针一条记录做DELETE操作的过程其实是先在<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">B+</span>树中定位到这条记录的位置,然后获取这条记录的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>,再执行<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">delete mark</span>操作。我们也可以把这个定位待删除记录在B+树中位置的过程看成是一个获取<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>的锁定读。

  • <span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">UPDATE</span>:在对一条记录做UPDATE操作时分为三种情况:

    • 情况1:未修改该记录的键值,并且被更新的列占用存储空间在修改见后未发生变化。

      则先在<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">B+</span>树中定位到这条记录的位置,然后再获取一下记录的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>,最后在原记录的位置进行修改操作。我们也可以把这个定位待修改记录在<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">B+</span>树中位置的过程看成是一个获取<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">锁定读</span>。

    • 情况2:未修改该记录的键值,并且至少有一个被更新的列占用的存储空间在修改前后发生变化。

      则现在<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">B+</span>树中定位到这条记录的位置,然后获取一下记录的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>,将该记录彻底删除掉(就是把记录彻底移入垃圾链表),最后再插入一条新记录。这个定位待修改记录在<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">B+</span>树中位置的过程看成是一个获取<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">锁定读</span>,新插入的记录由<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">INSERT</span>操作提供的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">隐式锁</span>进行保护。

    • 情况3:修改了该记录的键值,则相当于在原记录上做<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">DELETE</span>操作之后再来一次<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">INSERT</span>操作,加锁操作就需要按照<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">DELETE</span>和<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">INSERT</span>的规则进行了。

  • <span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">隐式锁</span>:

    一般情况下,新插入一条记录的操作并不加锁,通过一种称之为<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">隐式锁</span>的结构来保护这条新插入的记录在本事务提交前不被别的事务访问。

3.2 从数据操作的粒度划分:表级锁、页级锁、行锁

为了尽可能提高数据库的并发度,每次锁定的数据范围越小越好,理论上每次只锁定当前操作的数据的方案会得到最大的并发度,但是管理锁是很<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">消耗资源</span>的事情(涉及获取、检查、释放锁等动作)。因此数据库系统需要在<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">并发响应</span>和<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">系统性能</span>两方面进行平衡,这样就产生了“<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">锁粒度(Lock granularity)</span>”的概念。

对一条记录加锁影响的也只是这条记录而已,我们就说这个锁的粒度比较细;其实一个事务也可以在<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">表级别</span>进行加锁,自然就被成为<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">表级锁</span>或者<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">表锁</span>,对一个表加锁影响整个表中的记录,我们就说这个锁的粒度比较粗。锁的粒度主要分为表级锁、页级锁和行锁。

1. 表锁(Table Lock)

该锁会锁定整张表,它是MySQL中最基本的锁策略,并<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">不依赖于存储引擎</span>(不管你是MySQL的什么存储引擎,对于表锁的策略都是一样的),并且表锁是<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">开销最小</span>的策略(因为锁粒度比较大)。由于表级锁一次会将整个表锁定,所以可以很好的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">避免死锁</span>问题。当然,锁的粒度大所带来最大的负面影响就是出现锁征用的概率也会最高,导致<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">并发率大打折扣</span>。

①表级别的S锁、X锁

在对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,InnoDB存储引擎是不会为这张表添加表级别的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S锁</span>或者<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>的。在对某个表执行一些诸如<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">ALTER TABLE</span>、<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">DROP TABLE</span>这类的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">DDL</span>语句时,其他事务对这个表并发执行诸如SELECT、INSERT、DELETE、UPDATE的语句会发生阻塞。同理,某个事务中对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,在其他会话中对这个表执行<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">DDL</span>语句也会发生阻塞。这个过程其实是通过在<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">server层</span>使用一种称之为<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">元数据锁</span>(英文名:<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">Metadata Locks</span>,简称<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">MDL</span>)结构来实现的。

一般情况下,不会使用InnoDB存储引擎提供的表级别的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S锁</span>和<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>。只会在一些特殊情况下,比方说<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">崩溃恢复</span>过程中用到。比如,在系统变量<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">autocommic=0,innodb_table_locks=1</span>时,<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">手动</span>获取InnoDB存储引擎提供的表t的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S锁</span>或者<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>可以这么写:

  • <span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">LOCK TABLES t READ</span>:InnoDB存储引擎会对表<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">t</span>加表级别的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S锁</span>。
  • <span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">LOCK TABLES t WRITE</span>:InnoDB存储引擎会对表<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">t</span>加表级别的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>。

不过尽量避免在使用InnoDB存储引擎的表上使用<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">LOCK TABLES</span>这样的手动锁表语句,它们并不会提供什么额外的保护,只是会降低并发能力而已。InnoDB的厉害之处还是实现了更细粒度的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">行锁</span>,关于InnoDB表级别的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S锁</span>和<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>了解一下即可。

举例:下面我们讲解MyISAM引擎下的表锁。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容