MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

MySQL行级锁与表级锁的应用场景

2022-12-187.6k 阅读

一、MySQL 锁概述

在数据库系统中,锁是一种非常重要的机制,用于控制多个并发事务对共享资源的访问,从而保证数据的一致性和完整性。MySQL 作为一款广泛使用的开源数据库,提供了多种锁机制,其中表级锁和行级锁是应用最为普遍的两种类型。

1.1 什么是锁

锁本质上是一种数据结构,数据库通过它来管理并发事务对数据的访问。当一个事务获取了某个资源(比如表中的一行数据或者整个表)的锁时,其他事务在锁被释放之前就无法以冲突的方式访问该资源。例如,如果一个事务获取了某行数据的写锁,其他事务就不能再获取该行数据的写锁或读锁,直到第一个事务释放锁。

1.2 MySQL 锁的分类

MySQL 的锁从粒度上主要分为表级锁、行级锁和页级锁。表级锁是对整个表进行锁定,开销小,加锁快,但并发度低;行级锁则是针对表中的某一行数据进行锁定,开销大,加锁慢,但并发度高;页级锁介于两者之间,锁定的是数据页,开销和并发度也处于中间水平。本文主要聚焦于表级锁和行级锁。

二、表级锁

2.1 表级锁的原理

表级锁在对表进行操作时,会将整个表锁定。例如,当一个事务要对表进行写入操作(INSERT、UPDATE、DELETE 等)时,会获取表的写锁,此时其他事务不能对该表进行任何读写操作,直到写锁被释放。当事务进行读取操作(SELECT)时,会获取表的读锁,此时其他事务可以获取读锁,但不能获取写锁。

2.2 表级锁的类型

  1. 读锁(共享锁):也称为共享锁(Shared Lock,简称 S 锁),多个事务可以同时获取读锁,允许多个事务并发读取数据。例如,多个用户同时查询同一张表,他们都可以获取读锁,从而同时读取数据。读锁之间不冲突,但读锁与写锁冲突。
  2. 写锁(排他锁):也称为排他锁(Exclusive Lock,简称 X 锁),一个事务获取写锁后,其他事务不能再获取该表的任何锁(包括读锁和写锁),直到写锁被释放。这确保了在写操作时,数据不会被其他事务修改,保证数据的一致性。

2.3 表级锁的加锁与解锁

在 MySQL 中,可以使用 LOCK TABLES 语句手动对表加锁,例如:

-- 对表 user 加读锁
LOCK TABLES user READ;
-- 执行读取操作
SELECT * FROM user;
-- 解锁
UNLOCK TABLES;

-- 对表 user 加写锁
LOCK TABLES user WRITE;
-- 执行写入操作
INSERT INTO user (name, age) VALUES ('John', 25);
-- 解锁
UNLOCK TABLES;

在上述代码中,LOCK TABLES user READ 语句对 user 表加了读锁,此时可以执行读取操作,执行完毕后使用 UNLOCK TABLES 解锁。同理,LOCK TABLES user WRITE 对表加写锁,用于写入操作。

2.4 表级锁的应用场景

  1. 小表或写入操作频繁的表:对于数据量较小的表,表级锁的开销相对较小,加锁和解锁的速度快。同时,如果表的写入操作非常频繁,使用行级锁可能会因为频繁的加锁解锁操作导致性能下降,此时表级锁是一个不错的选择。例如,系统配置表,数据量通常不大,而且可能会有定时任务对其进行更新,使用表级锁可以简单高效地保证数据一致性。
  2. 批量操作:当需要对表中的大量数据进行批量操作时,如表的备份、数据的大规模迁移等,使用表级锁可以减少锁的竞争。因为如果使用行级锁,在处理大量数据时,锁的获取和释放次数会非常多,增加系统开销。例如,要将一张订单表的数据全部迁移到历史订单表中,可以先对订单表加写锁,然后一次性完成数据迁移操作。

三、行级锁

3.1 行级锁的原理

行级锁只锁定表中的某一行数据,而不是整个表。这使得在并发访问时,不同事务可以同时操作表中的不同行,大大提高了并发度。例如,当一个事务对某一行数据进行写入操作时,只会锁定这一行,其他事务仍然可以操作表中的其他行数据。

3.2 行级锁的类型

  1. 共享锁(S 锁):与表级锁的共享锁类似,用于读取操作。多个事务可以同时获取同一行数据的共享锁,从而实现并发读取。
  2. 排他锁(X 锁):用于写入操作,当一个事务获取了某一行数据的排他锁时,其他事务不能再获取该行数据的任何锁,直到排他锁被释放。

3.3 行级锁的加锁与解锁

在 MySQL 中,InnoDB 存储引擎默认使用行级锁。在执行 SELECT 语句时,如果使用了 FOR UPDATELOCK IN SHARE MODE 子句,则会对选中的行加锁。例如:

-- 使用 FOR UPDATE 对行加排他锁
START TRANSACTION;
SELECT * FROM user WHERE id = 1 FOR UPDATE;
-- 执行更新操作
UPDATE user SET age = age + 1 WHERE id = 1;
COMMIT;

-- 使用 LOCK IN SHARE MODE 对行加共享锁
START TRANSACTION;
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;
-- 执行读取操作
SELECT * FROM user WHERE id = 1;
COMMIT;

在上述代码中,SELECT * FROM user WHERE id = 1 FOR UPDATE 语句对 id 为 1 的行加了排他锁,在事务内可以安全地对该行进行更新操作,最后通过 COMMIT 提交事务并释放锁。SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE 则对 id 为 1 的行加了共享锁,用于并发读取。

3.4 行级锁的应用场景

  1. 高并发读写的表:对于一些读多写少或者读写并发度都很高的表,行级锁可以显著提高并发性能。例如电商网站的商品详情表,大量用户同时查询商品信息(读操作),同时可能有管理员对商品信息进行更新(写操作)。使用行级锁可以保证不同用户查询不同商品时不会相互干扰,管理员更新某一商品信息时也不会影响其他商品的查询操作。
  2. 数据一致性要求高的事务:在一些对数据一致性要求极高的事务中,行级锁可以精确控制对数据的访问。例如银行转账操作,需要保证在转账过程中,转出账户和转入账户的金额数据一致性,通过对相关账户行数据加锁,可以避免并发操作导致的数据错误。

四、表级锁与行级锁的性能对比

4.1 并发性能对比

  1. 表级锁:由于表级锁锁定整个表,在并发场景下,当有一个事务获取了表锁后,其他事务只能等待锁的释放,这大大限制了并发度。例如,在一个有大量并发读操作的场景中,如果使用表级读锁,虽然可以保证数据一致性,但其他事务的读操作也会被阻塞,导致性能下降。
  2. 行级锁:行级锁只锁定特定的行数据,不同事务可以同时操作不同行,并发度高。在高并发读写场景下,行级锁可以显著提高系统的吞吐量。例如,在一个多用户同时操作订单表的场景中,每个用户可能只操作自己的订单(不同行),行级锁可以让这些操作并发执行,提高系统性能。

4.2 锁开销对比

  1. 表级锁:表级锁的加锁和解锁操作相对简单,开销小。因为只需要对整个表进行锁定和解锁,不需要考虑行的具体情况。在数据量较小或者并发度较低的场景下,表级锁的开销优势明显。
  2. 行级锁:行级锁需要精确锁定每一行数据,加锁和解锁操作相对复杂,开销大。在处理大量数据时,由于需要频繁地获取和释放行锁,会增加系统的开销。例如,在对一张有百万行数据的表进行全表扫描并更新操作时,使用行级锁会因为频繁的锁操作导致性能瓶颈。

五、选择表级锁还是行级锁

5.1 根据业务场景选择

  1. 写入密集型业务:如果业务场景中写入操作非常频繁,并且对并发度要求不是特别高,例如一些配置表、日志表等,表级锁可能更合适。因为表级锁的加锁开销小,虽然并发度低,但可以满足写入操作的需求,同时减少锁的竞争。
  2. 读写并发型业务:对于读多写少或者读写并发度都很高的业务,如电商平台的商品信息展示与管理、社交平台的用户信息展示与修改等,行级锁是更好的选择。行级锁可以在保证数据一致性的前提下,提高并发性能,满足大量用户同时访问的需求。

5.2 根据数据量选择

  1. 小数据量表:对于数据量较小的表,表级锁的开销相对较小,加锁和解锁速度快。此时使用表级锁可以简单高效地保证数据一致性,不需要考虑行级锁的复杂开销。
  2. 大数据量表:当表的数据量非常大时,行级锁可以更好地控制锁的粒度,提高并发性能。如果使用表级锁,在高并发场景下,很容易出现锁争用,导致系统性能下降。

六、锁的优化策略

6.1 减少锁的持有时间

尽量缩短事务的执行时间,在获取锁后尽快完成操作并释放锁。例如,在一个包含多个步骤的事务中,如果某些步骤不涉及对锁定数据的操作,可以将这些步骤放在事务之外执行。

-- 优化前
START TRANSACTION;
SELECT * FROM user WHERE id = 1 FOR UPDATE;
-- 执行一些耗时操作,如复杂计算
SET @result = 1 + 2 + 3 + 4 + 5;
UPDATE user SET age = age + 1 WHERE id = 1;
COMMIT;

-- 优化后
-- 执行一些耗时操作,如复杂计算
SET @result = 1 + 2 + 3 + 4 + 5;
START TRANSACTION;
SELECT * FROM user WHERE id = 1 FOR UPDATE;
UPDATE user SET age = age + 1 WHERE id = 1;
COMMIT;

在上述代码中,优化前将复杂计算放在事务内,导致锁的持有时间变长;优化后将复杂计算放在事务外,减少了锁的持有时间,提高了并发性能。

6.2 合理使用索引

索引可以加快数据的查找速度,同时也能影响锁的性能。在使用行级锁时,如果查询条件能够命中索引,MySQL 可以精确地锁定需要的行,而不是全表扫描导致锁定过多行数据。例如:

-- 表 user 有索引 idx_name 基于 name 字段
-- 命中索引,精确锁定行
SELECT * FROM user WHERE name = 'John' FOR UPDATE;

-- 未命中索引,可能全表扫描,锁定过多行
SELECT * FROM user WHERE SUBSTRING(name, 1, 3) = 'Joh' FOR UPDATE;

在上述代码中,第一个查询条件 name = 'John' 可以命中索引 idx_name,从而精确锁定相关行;而第二个查询条件 SUBSTRING(name, 1, 3) = 'Joh' 不能命中索引,可能导致全表扫描,锁定过多行数据,增加锁争用。

6.3 避免死锁

死锁是指两个或多个事务在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。为了避免死锁,可以采取以下措施:

  1. 按照相同顺序访问资源:在多个事务中,尽量按照相同的顺序获取锁。例如,如果事务 A 先获取表 A 的锁,再获取表 B 的锁,那么事务 B 也应该先获取表 A 的锁,再获取表 B 的锁,这样可以避免死锁。
  2. 设置合理的锁等待超时时间:MySQL 可以设置 innodb_lock_wait_timeout 参数来控制锁等待的超时时间。当一个事务等待锁的时间超过该参数设置的值时,会自动回滚,从而避免死锁的发生。例如:
-- 设置锁等待超时时间为 50 秒
SET innodb_lock_wait_timeout = 50;

七、案例分析

7.1 表级锁案例

假设我们有一个简单的博客系统,其中有一张文章表 articles,用于存储文章信息。文章表的数据量不大,主要用于展示文章列表和管理员对文章进行修改。

CREATE TABLE articles (
    id INT PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(255),
    content TEXT,
    publish_date DATETIME
);

当管理员需要对文章进行批量更新,比如修改文章的版权信息时,使用表级锁是一个不错的选择。

-- 管理员批量更新文章版权信息
LOCK TABLES articles WRITE;
UPDATE articles SET copyright = 'Copyright (c) 2023' WHERE publish_date < '2023-01-01';
UNLOCK TABLES;

在这个案例中,由于文章表数据量不大,并且是批量更新操作,使用表级锁可以简单高效地完成任务,避免了行级锁频繁加锁解锁的开销。

7.2 行级锁案例

以电商平台的订单表 orders 为例,该表记录了用户的订单信息,数据量较大,并且有大量的并发读写操作。

CREATE TABLE orders (
    order_id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT,
    product_id INT,
    quantity INT,
    order_date DATETIME,
    FOREIGN KEY (user_id) REFERENCES users(user_id),
    FOREIGN KEY (product_id) REFERENCES products(product_id)
);

当用户查询自己的订单详情时,使用行级锁可以保证并发查询的性能。

START TRANSACTION;
SELECT * FROM orders WHERE user_id = 1 AND order_id = 100 LOCK IN SHARE MODE;
COMMIT;

当管理员需要修改某个订单的状态时,也可以使用行级锁。

START TRANSACTION;
SELECT * FROM orders WHERE order_id = 100 FOR UPDATE;
UPDATE orders SET status = 'completed' WHERE order_id = 100;
COMMIT;

在这个案例中,由于订单表数据量较大且并发度高,行级锁可以精确控制对数据的访问,提高并发性能,满足大量用户同时查询和管理员更新订单状态的需求。

八、总结

表级锁和行级锁在 MySQL 中各有其适用的场景。表级锁适用于小表、写入密集型业务以及批量操作;行级锁适用于高并发读写的表和数据一致性要求高的事务。在实际应用中,需要根据业务场景、数据量等因素综合考虑选择合适的锁机制,并通过优化策略提高系统性能,避免死锁等问题的发生。通过合理使用表级锁和行级锁,可以充分发挥 MySQL 的性能优势,保证数据库系统的高效稳定运行。同时,开发人员和数据库管理员需要不断学习和实践,深入理解锁的原理和应用,以应对日益复杂的业务需求。