MySQL(二十)锁(一)锁的概述与S、X锁

MySQL(二十)锁(一)锁概述、S锁和X锁


1 概述

  • 是计算机协调多个线程或进程并发访问某一资源的机制

    ​ 在程序开发过程中会存在多线程同步的问题,当多个线程并发访问某个数据的时候,尤其是一些敏感的数据(如金额、订单等),就需要保证这个数据在任何时刻都最多只有一个线程在访问,以保证数据的一致性和完整性。

  • 在开发过程中加锁就是为了保证数据的一致性

  • 锁机制也为MySQL各个隔离级别提供了保证。

  • 锁冲突也是影响数据库并发访问性能的一个重要因素

2 MySQL并发事务访问相同记录

​ 并发事务访问相同记录的情况主要有以下三种:

2.1 读读情况

​ 即并发事务读取相同的记录,由于读取记录不会对记录产生任何影响,因此不会引起各种问题,所以允许这种情况的发生。

2.2 写写情况

​ 即并发事务相继对相同的记录做出改动,这种情况下会发生脏写,而脏写在任何隔离级别下都是不被允许的,解决这一问题正是通过锁机制其实是内存中的一种结构,加锁的过程如下:

  • 当一个事务想要对记录进行改动,首先会查看内存中有没有和记录做关联的锁结构

  • 如果没有的话,就会生成一个锁结构与记录相关联,获取锁成功

    image-20230506134633101
  • 有的话则表示该事务获取锁失败,也会生成一个锁结构与记录关联,并设置is_waiting表示获取锁失败

    image-20230506135057522.

  • 事务提交后,会将由该事务生成的锁结构释放掉,然后看看还有没有别的事务在等待获取锁,如果有则将其is_waiting改为false,并将该事务对应的线程唤醒

    image-20230506135332983

​ 锁结构内的信息有很多,主要的信息有:

  • trx信息:记录是哪个事务产生的锁结构
  • is_waiting:代表当前事务是否在等待
2.3 读写或者写读情况

​ 即一个事务执行读操作,另一个事务执行改动操作,这种情况下可能会发生脏读不可重复读幻读问题。

🌟 2.4 并发问题的解决方案:
  • 读操作利用多版本并发控制MVCC),写操作加

    MVCC就是生成一个ReadView,通过ReadView能够找到符合条件的记录版本(历史版本由undo log提供查询),查询语句执行查询已经提交的事务做出的更改,对于没由提交的事务和ReadView创建之后的事务做出的更改是看不到的。而写操作肯定是针对的最新版本的记录,因此读记录的历史版本和写操作的最新记录版本并不会冲突,也就是采用MVCC时,读写操作并不会冲突

    普通的SELECT语句在READ COMMITTED 和 REPEATABLE READ隔离级别下的读操作就是利用MVCC进行的读

    • READ COMMITTED:由于不会读取没有提交的事务修改的数据版本,因此避免了脏读问题
    • REPEATABLE READ:由于不会读取Read View创建之后的事务更改的数据(一个事务只有在第一次执行SELECT语句才会生成一个Read View,之后的SELECT语句都在复用),因此避免了可重复读和幻读问题
  • 读、写操作都采用加锁的方式

    在一些业务场景中,不允许读取数据的历史版本,即每次都需要去读取磁盘中最新的数据,这样也就意味着读操作也需要和写操作一样排队执行。

    如此一来,脏读不可重复读问题都得到了解决,因为读操作和写操作的串行执行,不会出现一个事务读取另一个未提交事务的数据以及一个事务读取过程中另一个事务修改数据提交导致前一个事务前后读取数据不一致的情况(第二个事务根本无法开始)

    🌟 但是,幻读问题有些尴尬,试想一个事务在进行读操作,因此给表中的一定范围内的数据加锁,但是另一个事务要写的这个幻影数据可不在这个范围里面,也就是两个读写操作并不会冲突,仍然会出现幻读问题

  • 小结

    • MVCC方式下的读写操作不会冲突,效率更高
    • 读写加锁的方式读写操作需要排队执行,影响性能
    • 一般情况下,都采用MVCC解决读写并发问题,但也有业务的特殊需求要求使用加锁的方式保证读操作获取最新数据

3 锁的不同角度分类

image-20230506142345526
3.1 从数据类型划分:读锁(共享锁Share Lock,S锁)和写锁(排他锁Exclusive Lock,X锁)

​ 对于数据库并发事务的读-读情况不会引发问题,而对于写-写操作以及读写操作则会出现脏读不可重复读幻读的并发问题,需要借助MVCC或者读写加锁的方式解决。MySQL设计了一种由两种数据访问类型的锁组成的锁体系来解决:

  • 读锁:也称为共享锁(Share Lock)S锁,多个事务针对同一份数据的读操作不会互相影响,也不会相互堵塞。
  • 写锁:也称为排它锁(Exclusive Lock)、X锁,会堵塞其他的写锁和读锁,保证在同一时间里,只能有一个事务对同一份数据执行写入,并在写入过程中防止其他事务进行读取操作。

​ 对于InnoDB,读锁和写锁既能够加在行上,也能加在表上。如一个行级的读写锁:假设一个事务获取了某行记录的读锁,那么其他的事务只能再获取改行的读锁,想要获取写锁则需要等待前面事务提交释放创建的锁结构。

image-20230506144029968
锁定读

​ 在使用锁机制来解决脏读不可重复读幻读问题的时候,除了向读操作上添加S锁也需要向读操作上添加X锁,来禁止其他事务来读写该记录:

  • 对读操作添加X锁
SELECT ... LOCK IN SHARE MODE;
SELECT ... FOR SHARE;(mysql 8.0 新写法)
image-20230506144957922
  • 对写操作添加S锁
SELECT ... FOR UPDATE;

image-20230506145028652

写操作

​ 写操作主要包括updateinsertdelete三种:

  • delete:对一条记录做DELETE操作,首先在B+树中定位到这条记录的位置,然后获取这条记录的X锁,执行delete mark操作,可以把这个定位待删除记录在B+树中位置的过程看做是获取X锁的锁定读

  • update:update操作分为三种情况:

    • 情况一:未修改记录的主键值,并且修改前后列的存储空间未发生变化

      首先在B+树中定位到这条记录的位置,然后获取这条记录的X锁,对原数据进行修改,因此也可以把这个过程看做是获取X锁的锁定读

    • 情况二:未修改记录的主键值,至少有一个列修改前后存储空间发生了变化

      首先在B+树中定位到这条记录的位置,然后获取这条记录的X锁,然后将记录连入垃圾链表彻底删除掉同样可以把这个定位过程看做是获取X锁的锁定读。然后插入新的记录由 INSERT 操作提供的隐式锁进行保护。

    • 情况三:修改了记录的主键值,相当于执行一次 DELETEINSERT操作

      这个和情况二的区别在哪?

  • insert:一般情况下新插入的数据并不加锁,而是通过一种隐式锁的结构来保证在该事务提交之前新插入记录不被其他事务访问到。

MySQL8.0 新特性

​ 在5.7及之前的版本,如果事务无法获得锁,会一直等待,直到innodb_lock_wait_timeout超时,在8.0版本,使用SELECT ... FOR UPDATE、SELECT ... FOR SHARE语句后面跟NOWAITSKIP LOCKED,会跳过锁等待、跳过锁定行。

  • 通过添加NOWAITSKIP LOCKED语法,能够立刻返回
    • NOWAIT会立刻返回报错
    • SKIP LOCKED会返回不包括被锁定的行

​ 测试,首先事务一执行:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account for share;
+----+----------+---------+
| id | name     | balance |
+----+----------+---------+
|  1 | zhangsan |  100.00 |
|  2 | lisi     |    0.00 |
+----+----------+---------+
2 rows in set (0.00 sec)

​ 事务二执行:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account for update nowait;
ERROR 3572 (HY000): Statement aborted because lock(s) could not be acquired immediately and NOWAIT is set.

​ 提交上面的两个事务,事务一重新执行:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account where id = 1 for update;
+----+----------+---------+
| id | name     | balance |
+----+----------+---------+
|  1 | zhangsan |  100.00 |
+----+----------+---------+
1 row in set (0.00 sec)

​ 事务二执行:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account for share;
^C^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted
mysql> select * from account for share skip locked;
+----+------+---------+
| id | name | balance |
+----+------+---------+
|  2 | lisi |    0.00 |
+----+------+---------+
1 row in set (0.00 sec)

posted @ 2023-05-09 13:55  Tod4  阅读(291)  评论(0编辑  收藏  举报