2022-09-19

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

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

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

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

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

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

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

在采用加锁方式解决脏读不可重复读幻读这些问题时,读取一条记录时需要获取该记录的S锁,其实是不严谨的,有时候需要在读取记录时就获取记录的X锁,来禁止别的事务读写该记录,为此MySQL提出了两种比较特殊的SELECT语句格式:

  • 对读取的记录加S锁

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

    在普通的SELECT语句后边加LOCK IN SHARE MODE,如果当前事务执行了该语句,那么它会为读取到的记录加S锁,这样允许别的事务继续获取这些记录的S锁(比如说别的事务也使用SELECT ... LOCK IN SHARE MODE语句来读取这些记录),但是不能获取这些记录的X锁(比如使用SELECT ... FOR UPDATE语句来读取这些记录,或者直接修改这些记录)。如果别的事务想要获取这些记录的X锁,那么他们会阻塞,直到当前事务提交之后将这些记录上的S锁释放掉。

  • 对读取的记录加X锁

    SELECT ... FOR UPDATE;
    

    在普通的SELECT语句后边加FOR UPDATE,如果当前事务执行了该语句,那么它会为读取到的记录加X锁,这样既不允许别的事务获取这些记录的S锁(比方说别的事务使用SELECT .. LOCK IN SHARE MODE语句来读取这些记录),也不允许获取这些记录的X锁(比如使用SELECT ... FOR UPDATE语句来读取这些记录,或者直接修改这些记录)。如果别的事务想要获取这些记录的S锁或者X锁,那么它们会阻塞,直到当前事务提交之后将这些记录上的X锁释放掉。

MySQL8.0新特性:

在5.7及之前的版本,SELECT ... FOR UPDATE,如果获取不到锁,会一直等待,直到innodb_lock_wait_timeout超时。在8.0版本中,SELECT ... FOR UPDATE, SELECT ... FOR SHARE添加NOWAITSKIP LOCKED语法,跳过锁等待,或者跳过锁定。

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

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

平常所用到的写操作无非是UPDATEDELETEINSERT这三种:

  • DELETE

    针一条记录做DELETE操作的过程其实是先在B+树中定位到这条记录的位置,然后获取这条记录的X锁,再执行delete mark操作。我们也可以把这个定位待删除记录在B+树中位置的过程看成是一个获取X锁的锁定读。

  • UPDATE:在对一条记录做UPDATE操作时分为三种情况:

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

      则先在B+树中定位到这条记录的位置,然后再获取一下记录的X锁,最后在原记录的位置进行修改操作。我们也可以把这个定位待修改记录在B+树中位置的过程看成是一个获取X锁锁定读

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

      则现在B+树中定位到这条记录的位置,然后获取一下记录的X锁,将该记录彻底删除掉(就是把记录彻底移入垃圾链表),最后再插入一条新记录。这个定位待修改记录在B+树中位置的过程看成是一个获取X锁锁定读,新插入的记录由INSERT操作提供的隐式锁进行保护。

    • 情况3:修改了该记录的键值,则相当于在原记录上做DELETE操作之后再来一次INSERT操作,加锁操作就需要按照DELETEINSERT的规则进行了。

  • 隐式锁

    一般情况下,新插入一条记录的操作并不加锁,通过一种称之为隐式锁的结构来保护这条新插入的记录在本事务提交前不被别的事务访问。

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

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

对一条记录加锁影响的也只是这条记录而已,我们就说这个锁的粒度比较细;其实一个事务也可以在表级别进行加锁,自然就被成为表级锁或者表锁,对一个表加锁影响整个表中的记录,我们就说这个锁的粒度比较粗。锁的粒度主要分为表级锁、页级锁和行锁。

1. 表锁(Table Lock)

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

①表级别的S锁、X锁

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

一般情况下,不会使用InnoDB存储引擎提供的表级别的S锁X锁。只会在一些特殊情况下,比方说崩溃恢复过程中用到。比如,在系统变量autocommic=0,innodb_table_locks=1时,手动获取InnoDB存储引擎提供的表t的S锁或者X锁可以这么写:

  • LOCK TABLES t READ:InnoDB存储引擎会对表t加表级别的S锁
  • LOCK TABLES t WRITE:InnoDB存储引擎会对表t加表级别的X锁

不过尽量避免在使用InnoDB存储引擎的表上使用LOCK TABLES这样的手动锁表语句,它们并不会提供什么额外的保护,只是会降低并发能力而已。InnoDB的厉害之处还是实现了更细粒度的行锁,关于InnoDB表级别的S锁X锁了解一下即可。

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

你可能感兴趣的:(2022-09-19)