【MySQL】04. 事务管理(二):锁机制

in #study3 days ago

在之前的章节中,我们详细探讨了事务的ACID属性以及事务隔离级别的概念,理解了事务如何在并发环境中保持数据的一致性和完整性。本章我们将进一步深入事务管理的核心——锁机制,它是事务隔离级别的基石,也是实现数据库并发控制的关键。

1 锁分类

在数据库中,锁是一种用于控制并发访问的机制,它确保了数据的一致性和事务的隔离性。锁主要分为以下几类:

1.1 从性能角度划分

  • 乐观锁:基于假设数据不太可能被其他事务修改,通常通过版本号或时间戳来检测数据是否已被修改。乐观锁适用于读多写少的场景,因为它们在事务提交时才检查冲突,减少了锁的使用,提高了并发性能。
  • 悲观锁:假设数据很可能被其他事务修改,因此在数据被读取或修改时立即加锁。悲观锁适用于写操作频繁的场景,虽然降低了并发性,但确保了数据的一致性。

1.2 按锁粒度划分

  • 表锁:锁定整个表,适用于低并发或数据迁移场景,因其锁的范围最大,可以迅速锁定整个表,但并发性能较差。
  • 行锁:锁定单行数据,适用于高并发场景,因为锁的范围较小,可以减少锁冲突,提高并发性能。

1.3 按操作类型划分

  • 读锁(共享锁):允许多个事务同时读取数据而不相互干扰,不会阻止其他事务读取数据,但会阻止写操作。
  • 写锁(排他锁):独占数据,阻止任何其他事务读取或写入数据,用于数据的修改操作。
  • 意向锁(Intention Locks):为简化表级锁的管理,当事务需要对表加锁时,首先会申请意向锁。意向锁不与行级锁冲突,只存在于表级,用于指示事务将要对表进行的操作类型。
    • 意向共享锁(IS Lock):事务想要对整个表加读锁前,需先获取意向共享锁。
    • 意向排他锁(IX Lock):事务想要对整个表加写锁前,需先获取意向排他锁。

2 锁与隔离级别关系

  • 读未提交(Read Uncommitted):事务不加锁,可能导致脏读,即读取到未提交的数据。
  • 读已提交(Read Committed):事务在修改数据时加排他锁,读取时不加锁,解决了脏读问题,但可能发生不可重复读。
  • 可重复读(Repeatable Read):使用Next-Key Locks,事务在其生命周期内可以重复读取相同的数据集,解决了不可重复读问题。
  • 串行化(Serializable):使用全表锁,事务完全串行执行,保证了最高程度的一致性,但牺牲了并发性。

3 行锁

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

3.1 监控行锁状态

可以通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况:

show status like 'innodb_row_lock%';

  • Innodb_row_lock_current_waits:当前正在等待锁定的数量
  • Innodb_row_lock_time:从系统启动到现在锁定总时间长度
  • Innodb_row_lock_time_avg:每次等待所花平均时间
  • Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花时间
  • Innodb_row_lock_waits:系统启动后到现在总共等待的次数

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

3.2 锁升级

InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。如果对如果对非索引字段进行更新或者索引失效,则都会从行锁升级为表锁。

-- Session_1 执行,name字段无索引
update account set balance = 800 where name = 'lilei';

此时Session_2 对该表任一行操作都会被阻塞。在日常工作中,应尽可能让数据检索都通过索引来完成,避免因无索引使行锁升级为表锁。

4 间隙锁(Gap Lock)

间隙锁锁定的是两个值之间的空隙。在可重复读隔离级别下,InnoDB会使用间隙锁来防止其他事务在已经锁定的行之间插入新的行,从而避免幻读现象的发生。

假设account表里数据如下,那么间隙就有 id 为 (3,10),(10,20),(20,正无穷) 这三个区间。

在Session_1执行下面语句:

 update account set name = 'zhuge' where id > 8 and id <18

这条语句会锁定所有满足条件的行,同时还会锁定这些行之间的间隙,即(id=3, id=10)和(id=10, id=20)这一区间。这意味着其他会话将无法在(3,10)和(10, 20)这个范围内插入新的行。

5 临键锁(Next-key Locks)

临键锁是行锁与间隙锁的组合。它既锁定具体的行,也锁定该行之后的下一个间隙,像上面那个例子里的这个(3,20]的整个区间可以叫做临键锁。这种类型的锁是为了防止其他事务在已经锁定的行之间插入新的行,同时也防止修改已锁定的行。

继续使用之前的例子,Session_1执行的SQL语句:

 update account set name = 'zhuge' where id > 8 and id <18

这条语句将锁定id=20的行,同时也会锁定id=3和id=20之间的所有行及其间隙。这意味着在(3, 20]区间内,其他会话无法插入、删除或更新数据。

6 死锁/锁等待超时处理

6.1 场景示例

假设我们有两个会话(Session),Session_1 和 Session_2,并且这两个会话尝试按照不同的顺序锁定同一组行,如下所示:

-- Session_1 执行
BEGIN;
SELECT * FROM account WHERE id = 1 FOR UPDATE;

-- Session_2 执行
BEGIN;
SELECT * FROM account WHERE id = 2 FOR UPDATE;

-- Session_1 继续执行
SELECT * FROM account WHERE id = 2 FOR UPDATE;

-- Session_2 继续执行
SELECT * FROM account WHERE id = 1 FOR UPDATE;

在这个例子中,Session_1 首先锁定了ID为1的行,然后尝试锁定ID为2的行。与此同时,Session_2 锁定了ID为2的行,然后尝试锁定ID为1的行。这会导致一个死锁,因为每个会话都在等待另一个会话释放它所需要的锁。

6.2 处理步骤

6.2.1 定位事务线程

为了诊断和处理死锁,可以使用以下SQL查询来检查当前的事务状态和锁信息:

-- 查看所有事务
select * from INFORMATION_SCHEMA.INNODB_TRX;

定位事务线程-1

-- 查看所有锁的事务信息  MySQL8以前查看INFORMATION_SCHEMA.INNODB_LOCKS
select * from PERFORMANCE_SCHEMA.DATA_LOCKS;

定位事务线程-2

-- 查看等待锁的事务  MySQL8以前查看INFORMATION_SCHEMA.INNODB_LOCK_WAITS
select * from PERFORMANCE_SCHEMA.DATA_LOCK_WAITS;

定位事务线程-3

可以看到此时trx_id为251173的事务等待trx_id为251174的事务释放锁,trx_id为251174对应的trx_mysql_thread_id为11。

6.2.2 终止事务线程

对于死锁,通常MySQL会自动选择回滚一个事务来解决,但我们也可以选择手动干预,对长期占有锁资源的事务进行终止操作:

-- 释放锁,trx_mysql_thread_id可以从INNODB_TRX表里查看到
kill 11;

6.2.3 查看锁详细信息

还可以使用SHOW ENGINE INNODB STATUS命令来查看更详细的锁信息和其他InnoDB的状态信息:

-- 查看锁对应sql
show engine innodb status;

查看锁详细信息

7 结语

锁机制是确保数据库数据一致性和事务隔离性的关键。通过分类如乐观锁与悲观锁、表锁与行锁以及不同类型的锁如读锁和写锁等,数据库能够有效管理并发访问。行锁提升了并发性能,但也可能引发锁等待和死锁。监控行锁状态帮助我们及时发现并解决性能瓶颈,而锁升级则是在特定条件下确保数据一致性的重要策略。

间隙锁和临键锁可以在可重复读隔离级别下防止幻读现象,但可能增加等待时间。对于死锁这类问题,MySQL通常会自动回滚一个事务来解决,但在某些情况下可能需要手动终止某个事务以减少影响。使用SHOW ENGINE INNODB STATUS等命令可以获取详细的锁信息和死锁情况,有助于更好地诊断和解决问题。

总之,合理使用锁机制不仅能够提高数据库应用的性能,还能确保数据完整性和事务正确执行。在设计和优化数据库系统时,理解锁的工作原理及其对系统性能的影响至关重要。

Coin Marketplace

STEEM 0.18
TRX 0.13
JST 0.028
BTC 64657.99
ETH 3153.39
USDT 1.00
SBD 2.59