MySQL深度理解-MySQL锁机制

发布于:2025-08-04 ⋅ 阅读:(18) ⋅ 点赞:(0)

1.锁机制详解

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

        在数据库中,除了传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供需要用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。

1.1锁分类

        在性能上,MySQL的锁分为乐观锁(version版本比对+CAS)和悲观锁。

        在对数据操作的类型上,MySQL的锁区分为读锁和写锁(都属于悲观锁),还有意向锁。

        在对数据操作的粒度分,MySQL的锁区分为表锁、页锁和行锁。

1.2乐观锁和悲观锁

        之所以从性能上区分乐观锁和悲观锁,是因为这两种锁在不同的特定场景展现出不同的性能表现。

        对于乐观锁来说,在多读的场景性能较高,而对于悲观锁来说,在多写的场景性能较高。这个比较是根据两种锁的底层实现机理得出的,对于乐观锁,其底层采用的是version版本号 + CAS机制实现的,如果写操作比较多,CAS机制会使得多个线程处于CPU占用空转的状态,十分影响性能,此时应该尝试使用悲观锁去优化。

1.3读锁、写锁和意向锁

        读锁(共享锁,S锁即Shared):针对同一份数据,多个读操作可以同时进行而不会互相影响,但是会阻断写操作,比如:

SELECT * FROM T WHERE id = 1 LOCK IN SHARE MODE;

        写锁(排它锁,X锁即exclusive):当前写操作没有完成前,它会阻断其他写锁和读锁,数据修改操作都会加写锁,查询也可以通过 for update加写锁,比如:

 SELECT * FROM T WHERE id = 1 FOR UPDATE;

        意向锁(Intention Lock):意向锁又被称之为I锁,主要是针对表锁设定的,目的是为了优化表锁加锁的效率,是MySQL数据库自己给自己加的锁。当有事务给数据表中的数据行加了共享锁/排它锁之后,同时会给表设置一个标识位,声明表中现在已经有行锁了,其他事务想对表进行加锁时,无需遍历数据表中的每一行查看是否有行锁,而是直接查看锁的对应的行锁标记位即可,就可以很高效率的判断表中是否有行锁。特别是在表中数据特别多的情况下,逐行去判断表中每行数据是否有锁会十分影响表锁的整体效率。使用每张表的行锁标记位可以很大程度上提升表锁的添加效率,这个标志位就是意向锁。

        意向锁主要分为:

        意向共享锁,IS锁,对整个表加共享锁之前,需要先获取到意向共享锁。

        意向排它锁,IX锁,对整个表加排它锁之前,需要先获取到意向排它锁。

1.4表锁、页锁和行锁

1.4.1表锁

        表锁是每次都锁住整张表。开销小,加锁快。不会出现死锁,锁定粒度大,发生锁冲突的概率最高,并发度最低,一般用于整张表迁移的场景,开发可以不用去深入了解表级锁,一般左整张表迁移的都是运维人员,DBA等。

        使用SQL语句加表级锁:

LOCK TABLE 表名称 [READ | WRITE], 表名称2 [READ | WRITE];

        查看表上加过的表级锁:

SHOW OPEN tables;

        现在对test库中的t1表进行加表级写锁:

USE test;
LOCK TABLE t1 WRITE;

        查看表上加过的表级锁:

SHOW OPEN tables;

        可以发现t1表目前正在有一个锁使用中:

        删除表锁:

UNLOCK tables;

        使用删除表锁的指令之后,再次使用查看表级锁的指令,就会发现t1表的锁已经消失了:

1.4.2页锁

        只有BDB存储引擎是支持页锁的,InnoDB存储引擎和MyISAM存储引擎都是不支持页锁的。页锁是在页的粒度上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。当使用页锁的时候,会出现数据浪费的现象,但是这样的浪费最多出现在一个页上的所有数据行。页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。

        页锁只需要了解了即可,因为一般使用的InnoDB存储引擎不会使用到页锁的。

1.4.3行锁

        每次操作锁住一行数据,开销大,加锁慢,会出现死锁,锁定粒度最小,发生锁冲突的概率最低,并发度最高。

        InnoDB相对于MyISAM最大的不同有两点:

        1.InnoDB支持事务(Transaction),但是MyISAM是不支持事务的。

        2.InnoDB支持行锁,MyISAM不支持行锁。

        注意,InnoDB的行锁实际上是针对索引加的锁(在索引对应的索引项上做标记),不是针对整个行记录加锁。并且该索引不能失效,也不能WHERE条件使用的条件不带索引项,否则会从行锁升级为表锁。(RR级别会升级为表锁,RC级别不会升级为表锁)

        接下来我们详细分析一下为什么在RR隔离级别下,如果WHERE条件语句不使用索引,为什么行级锁会升级为表级锁,需要牢记一点的是,锁是加到索引上面了,而不是数据行上面了。

        比如在RR级别下执行下面的SQL:

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

BEGIN;

SELECT * FROM t1 WHERE name = 'LiLei' FOR UPDATE; -- WHERE条件里的name无索引

        将当前会话的事务隔离级别设置为可重复读,开启事务,并使用SELECT读操作,加排它锁,并设定WHERE条件使用的筛选字段name无索引。

        可以发现执行了这条语句后,其他会话再次对表进行相关操作的时候,就会卡住了,因为name字段中没有索引,相当于加行锁的时候索引失效了,加的行锁会相应升级为表级锁,会阻塞其他Session对该表任意一行记录做的所有修改操作。

        当会话被提交之后:

COMMIT;

        锁就会被释放了,其他会话的修改操作就可以正常执行了:

        为什么RR隔离级别行锁升级为表锁呢?

        因为在RR隔离级别下,需要解决不可重复读和幻读的问题,所以在遍历扫描聚集索引记录时,为了防止扫描过的索引被其他事务修改(不可重复读问题)或者间隙被其他事务插入数据(幻读问题),从而导致数据不一致,所以MySQL的解决方案就是把所有扫描过的索引记录和间隙都锁上,这里需要注意的是,并不是直接将整张表加表锁,因为不一定能加上表锁,可能会有其它事务所著了表里的的其它行记录。

1.5间隙锁(Gap Lock)

        间隙锁,锁的就是两个值之间的间隙,间隙锁是在RR隔离级别下才会生效。

        MySQL事务的默认隔离级别是RR,有幻读问题,间隙锁是可以解决幻读问题的。

        假设t1表中有如下数据:

        那么数据表中的间隙就有id为(2, 5),(5, 20),(20, 正无穷)这三个区间,在Session_1下面执行如下SQL:

SELECT * FROM t1 WHERE id = 18 FOR UPDATE;

        执行了这个SQL语句之后,其它的Session则无法在这个(5, 20)这个间隙中插入任何数据了。

如何执行了下面这条SQL:

SELECT * FROM t1 WHERE id = 25 FOR UPDATE;

        则其它Session便无法在(20, 正无穷)这个间隙中插入任何数据了。

        也就是说,只要在间隙范围内锁了一条不存在的记录会锁住整个间隙范围,不锁边界记录,这样就能防止其它Session在这个间隙范围内插入数据,就解决了可重复读隔离级别的幻读问题。

1.7临键锁(Next-key Locks)

        Next-Key Locks是行锁和间隙锁的组合,即包含边界的间隙锁。

1.8MyISAM引擎中的锁

        在MyISAM引擎中是没有行级锁概念的,执行操作时会使用表级锁。

        MyISAM在执行查询语句SELECT前,会自动给涉及的所有表加读锁,在执行UPDATE、INSERT和DELETE操作会自动给涉及的表加写锁。

        虽然MyISAM引擎中使用的都是表级锁,但是MyISAM引擎中也没有事务的概念,所以一般锁等待的时间也不会特别久。

1.9InnoDB引擎中锁的总结

        InnoDB在执行查询语句SELECT时(非串行隔离级别),不会加锁,但是UPDATE、INSERT和DELETE操作会加锁。

        InnoDB存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的行呢个损耗可能会比表级锁定还要高一些,但是整体的并发处理能力方面要远远由于MyISAM的表级锁定。当系统并发量高的时候,InnoDB的整体性能和MyISAM相比就会有明显的优势了。

        但是,InnoDB的行级锁同样也有其脆弱的一面,当使用不当的时候,可能会让InnoDB的整体性能表现不仅不能比MyISAM高,甚至可能会更差。

2.锁等待分析

        可能通过检查Innodb_row_lock状态变量来分析系统上的行锁争夺情况。

        MySQL提供了很多Innodb_row_lock_xx变量来帮助系统分析行锁争夺情况,使用下面的SQL语句可以查看所有的行锁变量:

        详细解答一下MySQL中提供的这些innodb_row_lock_xx变量:

        1.Innodb_row_lock_current_waits:当前正在等待锁的数量。

        2.Innodb_row_lock_time:从系统启动到现在锁定的总时间长度。

        3.Innodb_row_lock_time_avg:每次等待所平均花费的时间。

        4.Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花的时间。

        5.Innodb_row_lock_waits:系统启动后到现在总共等待的次数。

        对于这五个变量,最主要的是:平均等待时长,总共等待次数和等待总时长。

        尤其是当等待次数很高,而且平均等待时长也不小时,我们就需要分析系统中为什么有如此多的等待,然后根据分析结果着手制定优化计划。

3.INFOMATION_SCHEMA系统库锁相关数据表

3.1查询系统中的事务

        INFOMATION_SCHEMA系统库时MySQL内置提供的一个数据库,其中存储着事务、锁和锁等待的相关信息。

-- 查看事务
SELECT * FROM information_schema.INNODB_TRX;

        查询出来的数据如下:

        里面存储着数据库中当前的所有事务信息,由于当前没有启动的事务,所有数据表并没有相关的事务信息。

        现在启动一个事务:

BEGIN;
SELECT * FROM t1 WHERE id = 3 OR id = 5 FOR UPDATE;

        需要注意的是,启动事务并不是在BEGIN/START TRANSACTION语句执行后启动的,而是在事务执行了第一句SQL语句(S/U/D/I)之后才会真正启动。

        此时再使用下面的语句:

SELECT * FROM information_schema.INNODB_TRX;

        便可以查询出刚刚启动的事务数据:

3.2查询系统中的锁

        可以使用下面的语句查询当前数据库中存在的锁:

-- 查看锁
SELECT * FROM information_schema.INNODB_LOCKS; -- 8.0版本之前使用这个
SELECT * FROM performance_schema.DATA_LOCKS; -- 8.0版本之后使用这个

        查询出来的数据:

        当前是没有锁数据的,现在启动一个事务,并开启一把锁:

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

BEGIN;

SELECT * FROM t1 WHERE id = 3 FOR UPDATE;

        再次去使用下面的查询语句:

SELECT * FROM performance_schema.DATA_LOCKS; -- 8.0版本之后使用这个

        可以看到现在就可以查询出当前数据库中存在的锁了:

3.3查询锁等待

        可以使用以下语句去查询数据库中的的锁等待数据:

-- 查看锁等待
SELECT * FROM information_schema.INNODB_LOCK_WAITS; -- 8.0版本之前使用这个
SELECT * FROM performance_schema.DATA_LOCK_WAITS; -- 8.0版本之后使用这个

        查询出来的数据如下:

        现在开启一个事务,并开启一个锁:

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

BEGIN;

SELECT * FROM t1 WHERE id = 2 FOR UPDATE;

        再开启一个事务,也去尝试对id=2的行加行级锁:

BEGIN;

UPDATE t1 SET `name` = 'CC' WHERE id = 2;

        可以发现此时新的事务被卡住了,无法继续向下执行:

        执行下面的语句查看锁等待:

SELECT * FROM performance_schema.DATA_LOCK_WAITS; -- 8.0版本之后使用这个

        发现结果中确实存在锁等待记录:

        等待久了之后,MySQL会自动检测到这是一个死锁,会抛出锁等待超时的错误·:

3.4查看锁等待的详细信息

        在MySQL中可以查看MySQL的执行日志,进行分析锁等待的详细信息。

        使用下面的SQL语句可以查询锁等待的详细信息:

SHOW ENGINE Innodb status;

        执行SQL语句后会打印一些关于MySQL的日志信息,其中有关于事务的详细信息:

=====================================
2025-07-23 09:19:36 0xabb0 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 18 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 1 srv_active, 0 srv_shutdown, 306 srv_idle
srv_master_thread log flush and writes: 0
    ----------
    SEMAPHORES
    ----------
    OS WAIT ARRAY INFO: reservation count 1
    OS WAIT ARRAY INFO: signal count 1
    RW-shared spins 0, rounds 0, OS waits 0
    RW-excl spins 0, rounds 0, OS waits 0
    RW-sx spins 0, rounds 0, OS waits 0
    Spin rounds per wait: 0.00 RW-shared, 0.00 RW-excl, 0.00 RW-sx
    ------------
    TRANSACTIONS
    ------------
    Trx id counter 4110
Purge done for trx's n:o < 4105 undo n:o < 0 state: running but idle
History list length 0
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 284225957403144, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 284225957402368, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 284225957401592, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 284225957400816, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 4109, ACTIVE 33 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1128, 1 row lock(s)
MySQL thread id 13, OS thread handle 74528, query id 201 localhost 127.0.0.1 root statistics
/* ApplicationName=IntelliJ IDEA 2024.2.3 */ SELECT * FROM t1 WHERE id = 2 FOR UPDATE
------- TRX HAS BEEN WAITING 33 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 4 page no 4 n bits 80 index PRIMARY of table `test`.`t1` trx id 4109 lock_mode X locks rec but not gap waiting
Record lock, heap no 8 PHYSICAL RECORD: n_fields 5; compact format; info bits 64
 0: len 4; hex 80000002; asc     ;;
 1: len 6; hex 000000000e23; asc      #;;
 2: len 7; hex 0100000148018f; asc     H  ;;
 3: len 6; hex 4e69754d6132; asc NiuMa2;;
 4: len 4; hex 80000000; asc     ;;

------------------
---TRANSACTION 4108, ACTIVE 75 sec
2 lock struct(s), heap size 1128, 1 row lock(s)
MySQL thread id 12, OS thread handle 49656, query id 203 localhost 127.0.0.1 root
Trx read view will not see trx with id >= 4107, sees < 4107
--------
FILE I/O
--------
I/O thread 0 state: wait Windows aio (insert buffer thread)
I/O thread 1 state: wait Windows aio (log thread)
I/O thread 2 state: wait Windows aio (read thread)
I/O thread 3 state: wait Windows aio (read thread)
I/O thread 4 state: wait Windows aio (read thread)
I/O thread 5 state: wait Windows aio (read thread)
I/O thread 6 state: wait Windows aio (write thread)
I/O thread 7 state: wait Windows aio (write thread)
I/O thread 8 state: wait Windows aio (write thread)
I/O thread 9 state: wait Windows aio (write thread)
Pending normal aio reads: [0, 0, 0, 0] , aio writes: [0, 0, 0, 0] ,
 ibuf aio reads:, log i/o's:
Pending flushes (fsync) log: 0; buffer pool: 0
1507 OS file reads, 244 OS file writes, 50 OS fsyncs
0.00 reads/s, 0 avg bytes/read, 0.00 writes/s, 0.00 fsyncs/s
-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
    -------------------------------------
    Ibuf: size 1, free list len 0, seg size 2, 0 merges
    merged operations:
insert 0, delete mark 0, delete 0
    discarded operations:
insert 0, delete mark 0, delete 0
    Hash table size 34679, node heap has 0 buffer(s)
    Hash table size 34679, node heap has 0 buffer(s)
    Hash table size 34679, node heap has 0 buffer(s)
    Hash table size 34679, node heap has 0 buffer(s)
    Hash table size 34679, node heap has 0 buffer(s)
    Hash table size 34679, node heap has 0 buffer(s)
    Hash table size 34679, node heap has 2 buffer(s)
    Hash table size 34679, node heap has 5 buffer(s)
    0.00 hash searches/s, 0.00 non-hash searches/s
    ---
    LOG
    ---
    Log sequence number          19726800
    Log buffer assigned up to    19726800
    Log buffer completed up to   19726800
    Log written up to            19726800
    Log flushed up to            19726800
    Added dirty pages up to      19726800
    Pages flushed up to          19726800
    Last checkpoint at           19726800
    18 log i/o's done, 0.00 log i/o's/second
    ----------------------
    BUFFER POOL AND MEMORY
    ----------------------
    Total large memory allocated 0
    Dictionary memory allocated 526792
    Buffer pool size   8191
    Free buffers       7088
    Database pages     1096
    Old database pages 424
    Modified db pages  0
    Pending reads      0
    Pending writes: LRU 0, flush list 0, single page 0
    Pages made young 0, not young 0
    0.00 youngs/s, 0.00 non-youngs/s
    Pages read 954, created 142, written 171
    0.00 reads/s, 0.00 creates/s, 0.00 writes/s
    Buffer pool hit rate 1000 / 1000, young-making rate 0 / 1000 not 0 / 1000
    Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
    LRU len: 1096, unzip_LRU len: 0
    I/O sum[0]:cur[0], unzip sum[0]:cur[0]
    --------------
    ROW OPERATIONS
    --------------
    0 queries inside InnoDB, 0 queries in queue
    1 read views open inside InnoDB
    Process ID=58160, Main thread ID=25416 , state=sleeping
    Number of rows inserted 0, updated 0, deleted 0, read 1
    0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s
    Number of system rows inserted 0, updated 317, deleted 0, read 7232
    0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s
    ----------------------------
    END OF INNODB MONITOR OUTPUT
    ============================

3.5释放锁

        MySQL提供的相关SQL语句去释放锁,如果有的锁是死锁或者等待太久了不想等待了,可以使用释放锁的语句将锁释放掉。

        使用下面的语句去释放锁:

KILL trx_mysql_thread_id;

        这个语句需要事务的线程ID,MySQL实现释放锁是通过杀死事务实现的。其中trx_mysql_thread_id可以在INNODB_TRX表中查询到。

        执行下面的SQL语句启动一个事务,并对id=2的行上一把行级锁:

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

BEGIN;

SELECT * FROM t1 WHERE id = 2 FOR UPDATE;

        使用下面的SQL语句,查询事务的线程ID:

SELECT * FROM information_schema.INNODB_TRX;

        可以看到查询出的事务的线程ID为12:

        使用KILL语句杀死事务以释放锁:

KILL 12;

4.死锁问题分析

        MySQL中如果遇到死锁现象,该如何分分析解决呢?

        首先,MySQL中有可以自动检测出的死锁和不能自动检测出的死锁。MySQL可以自动检测出的死锁可以自动帮我们通过杀死事务的方式,将死锁破坏。MySQL不能自动检测出的死锁,需要我们使用日志信息来判断系统中超时等待的锁是不是因为出现了死锁现象。

4.1MySQL可自动检测的死锁

        首先先测试MySQL可以检测出的死锁。

        准备两个事务,同时启动:

        会话1启动事务:

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;

        会话2启动事务:

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;

        会话1的事务去给id = 1的行上行级锁:

SELECT * FROM t1 WHERE id = 1 FOR UPDATE;

        会话2的事务去给id = 2的行上行级锁:

SELECT * FROM t1 WHERE id = 2 FOR UPDATE;

        会话1的事务尝试给id = 2的行上行级锁:

SELECT * FROM t1 WHERE id = 2 FOR UPDATE;

        此时由于会话2持有着id = 2的行级锁,所以会话2会被卡住。

        会话2的事务尝试给id = 1的行上行级锁:

SELECT * FROM t1 WHERE id = 1 FOR UPDATE;

        此时会话1的事务已经被阻塞住了,并且还持有id = 1的行级锁,但是如果会话2的事务不去释放,会话1会一致阻塞下去,但是会话2也会一直阻塞着,所以就会产生死锁现象。

        由于会话1的事务和会话2的事务都被阻塞住了,所以会触发MySQL的自动检测死锁机制,将触发死锁的事务进行杀死,以破坏死锁,这里是直接将会话2的事务杀死了,会话1解除阻塞,拿到了id = 2的行级锁:

4.2MySQL不可自动检测的死锁

        如果在项目中,遇到了频繁的锁超时问题,并且MySQL也没有自动检测到死锁现象,就需要查看日志进行分析。

        可以先使用INFOMATION_SCHEMA系统库去查询事务、锁和锁等待,找出经常锁超时的锁和事务。

        再通过下面的SQL语句打印相关的日志:

SHOW ENGINE Innodb status;

        根据打印的相关日志就可以分析出MySQL出现锁超时的原因,并进一步判断是否有死锁。


网站公告

今日签到

点亮在社区的每一天
去签到