Mysql 锁机制全面解析

本文深入解析Mysql的锁机制,包括并发控制、读写锁、锁粒度、表锁、行级锁、页级锁等内容,探讨不同存储引擎的锁粒度支持,并通过实例演示加锁问题排查。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

并发控制

无论何时,只要有多个查询需要在同一时刻修改数据,就会有并发控制的问题。
我们以 unix 系统的email box 为例。典型的 mbox 文件格式是非常简单的。一个mbox中所有的邮件都串行在一起,彼此首尾相连。这种格式对于读取和分析邮件信息非常友好,同时投递邮件也很容易,只要在文件末尾附加新的邮件内容即可。

那如果两个进程同时投递邮件会怎样?可想而知,如果不增加锁机制,邮件内容势必会乱序追加,这肯定是不合理的。
like this…
在这里插入图片描述

那如果一个进程正在读邮件,而另一个进程要删除邮件,又该怎么办?结果是不可预知的,可能是读取到错误的内容,也可能会报错退出。
在这里插入图片描述
当然涉及良好的邮箱投递系统会通过锁(lock)来防止上述问题的发生。

读写锁

那如果我们把数据库中的表类比成Unix 的 email box ,数据中的的一行数据比作一封邮件。就不难发现,数据库也会存在这样的并发问题。解决这类经典问题的方法就是并发控制,而加锁就是一种途径。

再回到我们的邮件系统遇到的问题上:

如果A进程发邮件的同时,B进程也要发邮件,怎么才能避免乱序追加呢。那就是加锁,在A进程发邮件的时候,给邮件系统加锁,禁止其他进程修改或查看邮件。只有在A进程处理完之后,其他进程才可以介入。这种加锁方式就是写锁,因为这种加锁方式是完全占有,排斥其他进程的任何操作,所以也叫排他锁

如果A进程读邮件的同时,B进程又要修改邮件呢。那当然还是加锁了,A进程读邮件的时候,先加锁,但是这个锁是区别于写锁的,因为如果B进程也是来读邮件,是不会影响A的,但是要修改肯定是不行的。所以这种加锁方式就叫做读锁,因为这种加锁方式并非完全占有,可以跟其他进程共享读操作,所以也叫共享锁

在实际的数据库系统中,每时每刻都在发生锁,当用户在修改某一部分数据时,Mysql会通过加锁来防止其他用户读取同一数据。

锁粒度

我们在来聊一下锁粒度的问题。所谓锁粒度,简单来理解就是锁定的数据范围。

一种提高并发性的方式就是让锁定对象更有选择性。尽量只锁定需要修改的部分数据,而不是全部数据。更理想的方式是,只对会修改的数据便进行精确的锁定。

任何时候,在给定的资源上,锁定的数据量越少,则系统的并发程度越高,只要相互之间不发生冲突即可。

那是不是在任何时候把锁粒度调到最精确就好呢?当然不是

系统需要花费资源时间来管理锁,如果系统把大量的精力放到锁上,那势必会对系统性能造成影响。

所谓的锁策略就是在锁的开销和数据的安全性之间需求平衡。

而Mysql提供了多种选择。每种存储引擎都可以实现自己的锁策略和锁粒度。将锁粒度固定在某个级别,可以为某些特定的应用场景提供更好的性能,但同时会失去对另外一些应用场景的良好支持。

表锁

表锁是Mysql中最基本的锁策略,并且是开销最小的策略.

特点是:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低

一个用户在表进行写操作(插入、删除、更新等)前,需要先获得写锁,这会阻塞其他用户的所有读写操作。

只有没有写锁时,其他用户才能获取读锁,读锁之间是不相互阻塞的。

尽管存储引擎可以管理自己的锁,Mysql 本身还是会使用各种有效的表锁来实现不同的目的。例如,服务器会为诸如 ALTER TABLE 之类的语句使用表锁,而忽略存储引擎的锁机制。

行级锁

行级锁可以最大程度地支持并发处理,当然也带来了最大的锁开销。
特点:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

页级锁

开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。

不同存储引擎的锁粒度支持

  • MyISAM和MEMORY存储引擎采用的是表级锁(table-level locking)
  • BDB存储引擎采用的是页面锁(page-level locking),但也支持表级锁
  • InnoDB存储引擎既支持行级锁(row-level locking),也支持表级锁

加锁问题排查

本节涉及的都是innode引擎
首先我们可以用 show processlist 查看当前是否有阻塞的任务。

在5.5中,information_schema库中增加了三个关于锁的表,可以用来排查加锁问题。
innodb_trx ## 当前运行的所有事务
innodb_locks ## 当前出现的锁
innodb_lock_waits ## 锁等待的对应关系

innodb_trx 当前运行的事务
这个表里记录了当前运行的事务详细信息。

mysql> use information_schema;
Database changed
mysql>
mysql> desc innodb_trx;
+----------------------------+---------------------+------+-----+---------------------+-------+
| Field                      | Type                | Null | Key | Default             | Extra | 
+----------------------------+---------------------+------+-----+---------------------+-------+
| trx_id                     | varchar(18)         | NO   |     |                     |       |#事务ID
| trx_state                  | varchar(13)         | NO   |     |                     |       |#事务状态
| trx_started                | datetime            | NO   |     | 0000-00-00 00:00:00 |       |#事务开始时间
| trx_requested_lock_id      | varchar(81)         | YES  |     | NULL                |       |#innodb_locks.lock_id
| trx_wait_started           | datetime            | YES  |     | NULL                |       |#事务开始等待的时间
| trx_weight                 | bigint(21) unsigned | NO   |     | 0                   |       |# 事务权重
| trx_mysql_thread_id        | bigint(21) unsigned | NO   |     | 0                   |       |# 线程id
| trx_query                  | varchar(1024)       | YES  |     | NULL                |       |# 具体SQL语句
| trx_operation_state        | varchar(64)         | YES  |     | NULL                |       |#事务当前操作状态
| trx_tables_in_use          | bigint(21) unsigned | NO   |     | 0                   |       |# 事务中有多少个表被使用
|
| trx_tables_locked          | bigint(21) unsigned | NO   |     | 0                   |       |# 事务拥有多少个锁
| trx_lock_structs           | bigint(21) unsigned | NO   |     | 0                   |       |# 
| trx_lock_memory_bytes      | bigint(21) unsigned | NO   |     | 0                   |       |# 事务锁住的内存大小(B)
| trx_rows_locked            | bigint(21) unsigned | NO   |     | 0                   |       |# 加锁的行数
| trx_rows_modified          | bigint(21) unsigned | NO   |     | 0                   |       |# 事务更改的行数
| trx_concurrency_tickets    | bigint(21) unsigned | NO   |     | 0                   |       |#
| trx_isolation_level        | varchar(16)         | NO   |     |                     |       |#事务的隔离级别
| trx_unique_checks          | int(1)              | NO   |     | 0                   |       |#是否唯一性检查
| trx_foreign_key_checks     | int(1)              | NO   |     | 0                   |       |#是否外键检查
| trx_last_foreign_key_error | varchar(256)        | YES  |     | NULL                |       |
| trx_adaptive_hash_latched  | int(1)              | NO   |     | 0                   |       |
| trx_adaptive_hash_timeout  | bigint(21) unsigned | NO   |     | 0                   |       |
| trx_is_read_only           | int(1)              | NO   |     | 0                   |       |
| trx_autocommit_non_locking | int(1)              | NO   |     | 0                   |       |
+----------------------------+---------------------+------+-----+---------------------+-------+
24 rows in set (0.01 sec)

innodb_locks 当前出现的锁
当出现锁等待的时候,这个表里才会有记录。

mysql> desc innodb_locks;
+-------------+---------------------+------+-----+---------+-------+
| Field       | Type                | Null | Key | Default | Extra |
+-------------+---------------------+------+-----+---------+-------+
| lock_id     | varchar(81)         | NO   |     |         |       |#锁id
| lock_trx_id | varchar(18)         | NO   |     |         |       |#事务id
| lock_mode   | varchar(32)         | NO   |     |         |       |#锁模式
| lock_type   | varchar(32)         | NO   |     |         |       |#锁类型
| lock_table  | varchar(1024)       | NO   |     |         |       |#被加锁的表
| lock_index  | varchar(1024)       | YES  |     | NULL    |       |#被锁的索引
| lock_space  | bigint(21) unsigned | YES  |     | NULL    |       |#被锁的表空间号
| lock_page   | bigint(21) unsigned | YES  |     | NULL    |       |#被锁的页号
| lock_rec    | bigint(21) unsigned | YES  |     | NULL    |       |#被锁的记录号
| lock_data   | varchar(8192)       | YES  |     | NULL    |       |#被锁的数据
+-------------+---------------------+------+-----+---------+-------+
10 rows in set (0.01 sec)

innodb_lock_waits 所等待的对应关系
当出现锁等待的时候,我们可以从这个表里查到是哪个事务在持有锁,哪个事务在等待。

mysql> desc innodb_lock_waits;
+-------------------+-------------+------+-----+---------+-------+
| Field             | Type        | Null | Key | Default | Extra |
+-------------------+-------------+------+-----+---------+-------+
| requesting_trx_id | varchar(18) | NO   |     |         |       |#请求锁的事务ID
| requested_lock_id | varchar(81) | NO   |     |         |       |#请求锁的锁ID
| blocking_trx_id   | varchar(18) | NO   |     |         |       |#当前拥有锁的事务ID
| blocking_lock_id  | varchar(81) | NO   |     |         |       |#当前拥有锁的锁ID
+-------------------+-------------+------+-----+---------+-------+
4 rows in set (0.01 sec)

现在我们通过具体的案例来分析一下:
首先,我们建一张测试表:

CREATE TABLE `lock_test` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
  `lenth` int(10) NOT NULL DEFAULT '-1' COMMENT '长度',
  `name` varchar(255) NOT NULL DEFAULT '' COMMENT '书名',
  `auth` varchar(255) NOT NULL DEFAULT '' COMMENT '作者',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

在加几条测试数据

INSERT INTO `lock_test` (`id`, `lenth`, `name`, `auth`)
VALUES
	(1, 100, '平凡的世界', '陈忠实'),
	(2, 100, '解忧杂货店', '');

然后我们需要打开多个窗口来连接Mysql.
下面的我会用 connection_id来区分不同的窗口:

# connection_id 281
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> update lock_test set lenth=200 where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
# connection_id 312
mysql> select * from  innodb_trx\G;
*************************** 1. row ***************************
                    trx_id: 5485848
                 trx_state: RUNNING
               trx_started: 2020-06-16 17:45:01
     trx_requested_lock_id: NULL
          trx_wait_started: NULL
                trx_weight: 3
       trx_mysql_thread_id: 281  #事务执行的客户端线程id
                 trx_query: NULL
       trx_operation_state: NULL
         trx_tables_in_use: 0
         trx_tables_locked: 0
          trx_lock_structs: 2
     trx_lock_memory_bytes: 360
           trx_rows_locked: 1		#当前事务锁了1行数据
         trx_rows_modified: 1    #当前事务更新了1行数据
   trx_concurrency_tickets: 0
       trx_isolation_level: REPEATABLE READ  # 事务的隔离级别是可重复读
         trx_unique_checks: 1  
    trx_foreign_key_checks: 1
trx_last_foreign_key_error: NULL
 trx_adaptive_hash_latched: 0
 trx_adaptive_hash_timeout: 10000
          trx_is_read_only: 0
trx_autocommit_non_locking: 0
1 row in set (0.00 sec)

我们还能看到事务锁定的行数,更新的行数,以及事务隔离级别等信息,这对排查问题有很大帮助。

现在我们在新的窗口,修改同样的资源试一下。

#connection_id 282
mysql> update lock_test set lenth=500 where id=1;

这时候就阻塞住了,因为在 id 为 1 的数据的写锁被 281 持有,这期间是禁止其他任何读写操作的。

我们再来看下具体的加锁信息:

#connection_id  312
mysql>
mysql> select * from  innodb_trx\G;
*************************** 1. row ***************************
                    trx_id: 5485857
                 trx_state: LOCK WAIT  #只能等上一个事务释放锁才能继续执行
               trx_started: 2020-06-16 18:09:05   # 事务开始时间
     trx_requested_lock_id: 5485857:5668:3:4  # innode_locks 表的id
          trx_wait_started: 2020-06-16 18:09:05 # 事务开始等待的时间
                trx_weight: 2
       trx_mysql_thread_id: 282
                 trx_query: update lock_test set lenth=500 where id=1  # 还可以看到具体的sql哦
       trx_operation_state: starting index read
         trx_tables_in_use: 1
         trx_tables_locked: 1
          trx_lock_structs: 2
     trx_lock_memory_bytes: 360
           trx_rows_locked: 1
         trx_rows_modified: 0
   trx_concurrency_tickets: 0
       trx_isolation_level: REPEATABLE READ
         trx_unique_checks: 1
    trx_foreign_key_checks: 1
trx_last_foreign_key_error: NULL
 trx_adaptive_hash_latched: 0
 trx_adaptive_hash_timeout: 10000
          trx_is_read_only: 0
trx_autocommit_non_locking: 0
*************************** 2. row ***************************
                    trx_id: 5485856
                 trx_state: RUNNING  #由于正在执行,是不允许其他的写操作介入的
               trx_started: 2020-06-16 18:09:02
     trx_requested_lock_id: NULL
          trx_wait_started: NULL
                trx_weight: 3
       trx_mysql_thread_id: 281
                 trx_query: NULL
       trx_operation_state: NULL
         trx_tables_in_use: 0
         trx_tables_locked: 0
          trx_lock_structs: 2
     trx_lock_memory_bytes: 360
           trx_rows_locked: 1
         trx_rows_modified: 1
   trx_concurrency_tickets: 0
       trx_isolation_level: REPEATABLE READ
         trx_unique_checks: 1
    trx_foreign_key_checks: 1
trx_last_foreign_key_error: NULL
 trx_adaptive_hash_latched: 0
 trx_adaptive_hash_timeout: 10000
          trx_is_read_only: 0
trx_autocommit_non_locking: 0
2 rows in set (0.00 sec)

由于前一个事务还在 running ,新进来的事务只能等待写锁释放。

mysql> select * from innodb_lock_waits\G;
*************************** 1. row ***************************
requesting_trx_id: 5485857  # 请求的事务id
requested_lock_id: 5485857:5668:3:4  
  blocking_trx_id: 5485856   # 当前持有锁的事务id
 blocking_lock_id: 5485856:5668:3:4
1 row in set (0.00 sec)

可以看到锁等待的关系,到底是谁在等谁!

mysql> select * from innodb_locks\G;
*************************** 1. row ***************************
    lock_id: 5485857:5668:3:4
lock_trx_id: 5485857
  lock_mode: X
  lock_type: RECORD
 lock_table: `nc_collection`.`lock_test`
 lock_index: PRIMARY
 lock_space: 5668
  lock_page: 3
   lock_rec: 4
  lock_data: 1
*************************** 2. row ***************************
    lock_id: 5485856:5668:3:4
lock_trx_id: 5485856
  lock_mode: X
  lock_type: RECORD
 lock_table: `nc_collection`.`lock_test`
 lock_index: PRIMARY
 lock_space: 5668
  lock_page: 3
   lock_rec: 4
  lock_data: 1
2 rows in set (0.00 sec)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

csw_coder

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值