数据库锁是一种机制,用于管理并发访问数据库的方式。当多个用户或事务同时访问数据库时,可能会导致数据不一致或冲突的问题。数据库锁的作用是确保数据的一致性和完整性,同时允许多个用户并发地访问数据库。
需要注意的是,加锁是消耗资源的,锁的各种操作,包括获取锁、校验锁是否已解除、释放锁等,都会增加系统的开销。
这里以MySQL为例,介绍下数据库锁,关于数据库锁的理论知识可以参考数据库锁概述一文。
按照不同的划分标准,可以将锁分为多种类型。如悲观锁和乐观锁、共享锁和排他锁、意向锁、表锁和行锁和页锁等。
根据加锁的时机,可以将数据库锁可以分为两类:悲观锁和乐观锁。
所谓悲观锁,就是对数据的修改抱有悲观态度。当要对一个数据库中的一条数据进行修改时,为了避免同时被其他人修改,直接对该数据进行加锁以防止并发。这种借助数据库锁机制在修改数据之前先锁定,再修改的方式被称之为悲观并发控制(又名"悲观锁",Pessimistic Concurrency Control,简称"PCC")。
悲观锁采用的是"先取锁再访问"的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会;另外,还会降低并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。
所谓乐观锁,是相对悲观锁而言的,对数据的修改持有乐观的态度。当要对一个数据库中的一条数据进行修改时,假设数据一般不会造成冲突,只在数据进行提交的时候,对数据进行冲突检测,如果没有冲突,则直接修改,如果存在冲突则提供补偿策略,如执行回滚操作或执行重试操作。这次推迟并发冲突校验的方式被称为乐观并发控制(又名"乐观锁",Optimistic Concurrency Control,简称"OCC")。
乐观锁采用的是"推迟冲突校验"的积极策略,在数据争用不大、冲突较少的场景下中,可以获得更高的吞吐量,且不会引入锁,更不会带来死锁的问题。但是,对于数据争用较大,冲突较多的场景,乐观锁则会因补偿策略,带来回滚压力或CPU消耗。
根据是否独占数据还是共享数据,可以将数据库的锁分为共享锁和排他锁。
共享锁(shared lock,记为S),也称读锁,如果事务 T T T获得了数据项Q上的共享型锁,则 T T T对数据项Q可读但不可写,其他事务也可对数据项Q加共享锁,但不能加排他锁。
排他锁(exclusive lock,记为X),也称写锁,如果事务 T T T获得了数据项Q上的排他型锁,则 T T T对数据项Q即可写又可读,其他事务对数据项Q不能加任何锁。
共享锁和排他锁的兼容关系如下:
锁的类型 | 共享锁(S) | 排他锁(X) |
---|---|---|
共享锁(S) | 兼容 | 冲突 |
排他锁(X) | 冲突 | 冲突 |
从上面的表格可知,共享锁和共享锁是兼容的,也就是说,如果事务 T T T对数据加了共享锁,那么其他事务是可以继续加共享锁的。但是,如果事务 T T T对数据加了排他锁,那么其他事务是不可加任何锁的。
从数据隔离性的角度来说,共享锁主要是为了支持并发的读取数据而出现的,读取数据时,不允许其他事务对当前数据进行修改操作,从而有效避免"不可重复读"的问题。排他锁主要是为了解决在修改数据时,不允许其他事务对当前数据进行修改和读取操作,从而可以有效避免"脏读"的问题。
意向锁是一种表锁,用来表示一个事务接下来要获取的行锁类型(共享锁还是排他锁)。MySQL官方文档给出的描述如下:
Intention locks are table-level locks that indicate which type of lock (shared or exclusive)
a transaction requires later for a row in a table.
当一个事务想要获取某行的锁时,它必须先获取该行所在表的意向锁。根据意图获取的行锁类型,可以将意向锁细分为意向共享锁(Intention Shared lock,IS)和意向排他锁(Intention eXclusive lock,IX)。意向共享锁表示该事务要获取的行锁类型是共享锁,而意向排他锁则表示该事务要获取的行锁类型是排他锁。
意向锁主要用于表锁和行锁共存的场景。举例来说,如果一个事务对表中的某一行加行锁,那么当另一个事务想对表加锁时,如果没有意向锁,则需要遍历该表的所有行,确保不存在锁冲突。如果有意向锁,如果可以兼容,则另一个事务进一步加锁,否则,直接返回,提示锁冲突。意向锁的兼容关系如下:
锁的类型 | 共享锁(S) | 排他锁(X) | 意向共享锁(IS) | 意向排他锁(IX) |
---|---|---|---|---|
共享锁(S) | 兼容 | 冲突 | 兼容 | 冲突 |
排他锁(X) | 冲突 | 冲突 | 冲突 | 冲突 |
意向共享(IS) | 兼容 | 冲突 | 兼容 | 兼容 |
意向排他锁(IX) | 冲突 | 冲突 | 兼容 | 兼容 |
从上面的表格可知,意向锁之间是兼容的。也就是说,对同一个表,是可以随便加意向锁的。但是,对于意向共享锁来说,和共享锁是兼容的,和排他锁是冲突的。这主要是因为意向共享锁目的是给特定的行添加共享锁,而共享锁与排他锁是冲突的。同样的,对于意向排他锁来说,对共享锁或排他锁都是冲突的,这主要是因为意向排他锁目的是给特定的行添加排他锁,而排他锁与共享锁或排他锁是冲突的。
需要说明的是,对InnoDB来说,意向锁是由InnoDB执行引擎自己维护的,用户无法手动操作意向锁。当向数据行加共享锁/排他锁之前,InooDB 会自动先获取该数据行所在数据表的对应意向锁。
此外,除了存在意向共享锁和意向排他锁,还有共享排他型意向锁(shared and intention-exclusive, SIX),见于多粒度封锁协议中。有兴趣的同学可以自行学习。
根据加锁的粒度,可以将数据库的锁细分为表锁、行锁、页锁。
表锁(Table Lock)是一种粗粒度的锁,它锁定整个表,阻止其他事务访问表中的任何行。表锁适用于需要对整个表进行操作的情况,表锁开销小,且加锁快,但因锁的粒度较大,发生锁冲突的概率较高,数据库的并发性能较低。
行锁(Row Lock)是一种细粒度的锁,它锁定指定的行。行锁开销大,且加锁慢,但因其加锁粒度较小,发生锁冲突的概率较低,数据库的并发性能较高。
页锁(Page Lock)是介于行锁和表锁之间的一种锁机制,它锁定表的一个页或多个页。页锁通常在特定情况下使用,例如当需要批量操作数据时。页锁的开销和加锁速度也介于行锁和表锁之间,数据库的并发性能一般。注意,页级锁是 MySQL 中比较独特的一种锁定级别,在其他数据库中并不常见。页级锁主要应用于 BDB(BerkeleyDB )存储引擎中,这里不过多介绍,有兴趣的同学还请自行学习。
此外,无论是表锁,还是行锁,都可以根据是否独占数据还是共享数据,进一步细分为共享锁和排他锁。
对不同的存储引擎,锁的支持情况和使用方式是不同的。这里重点介绍下MyISAM和InnoDB两种存储引擎的锁的使用,其他引擎的锁的使用还请自行学习。
MyISAM 只支持表级锁,不支持行锁。在执行 select,update,delete,insert 等语句都会给表自动加锁(针对读操作加共享锁、针对写操作加排他锁)。
InnoDB默认采用行锁,在未使用索引字段查询时升级为表锁。需要说明的是,即便在条件中使用了索引字段,MySQL会根据自身的执行计划,考虑是否使用索引。如果MySQL认为全表扫描效率更高,即使指定了索引字段,也不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,有必要进一步检查SQL的执行计划,以确认是否真正使用了索引。注意,以下场景下,会优先使用表锁:
(1) 全表更新。事务需要更新大部分或全部数据,且表又比较大。若使用行锁,会导致事务执行效率低,从而可能造成其他事务长时间锁等待和更多的锁冲突。
(2) 多表查询。事务涉及多个表,比较复杂的关联查询,很可能引起死锁,造成大量事务回滚。这种情况若能一次性锁定事务涉及的表,从而可以避免死锁、减少数据库因事务回滚带来的开销。
InnoDB为实现行锁,提供了多种实现方式。如记录锁(Record Lock)、间隙锁(Gap Lock)、临键锁(Next-Key Lock)等。需要说明的是,行锁仅在使用索引字段时才会生效,否则会升级为表锁。
记录锁是封锁记录,记录锁也叫行锁,示例如下:
SELECT * FROM `test_table` WHERE `id` = 1 FOR UPDATE;
它会在 id=1 的记录上加上记录锁,以阻止其他事务插入,更新,删除 id=1 这一行。
间隙锁,锁定一个范围,但不包含记录本身,间隙锁是封锁索引记录中的间隔,或者第一条索引记录之前的范围,又或者最后一条索引记录之后的范围。间隙锁主要目的,也是为了避免出现幻读(Phantom Read),对应的事务的隔离级别降级为可重复读。如果将事务的隔离性级别调整成读已提交或读未提交,间隙锁都会失效。间隙锁对应的事务隔离级别是可重复。示例如下:
SELECT * FROM `test_table` WHERE `id` > 100 FOR UPDATE
更多间隙锁的使用示例可以参考MySQL锁 —— 记录锁、间隙锁、临键锁、自增锁 一文。
以下情况均会产生间隙锁:
(1) 使用普通索引,且需要锁定一个区间。
(2) 使用多列唯一索引,且需要锁定一个区间。
(3) 使用唯一索引,且需要锁定一个区间。
注意,间隙锁封锁的不是多条记录,而是一个区间,以防止出现幻读现象。
临键锁,是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间。同间隙锁一样,临键锁也是为了避免出现幻读(Phantom Read)。同样的,临键锁对应的事务隔离级别是可重复。示例如下:
SELECT * FROM table WHERE age <= 24 FOR UPDATE;
更多临键锁的使用示例可以参考MySQL锁 —— 记录锁、间隙锁、临键锁、自增锁 一文。
临键锁会锁住一段左开右闭区间的数据。需要强调的一点是,InnoDB 中临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁。
MySQL锁机制是确保数据库数据一致性和完整性的关键技术,同时也是提高数据库性能的挑战之一。了解不同类型的锁以及它们的适用场景是设计高效数据库应用程序的重要一步。
在使用锁时,需要根据应用程序的需求选择合适的锁类型,同时考虑事务隔离级别以及优化策略,以提高并发性能并避免常见的锁问题。
为了优化MySQL锁性能,可以考虑以下几个策略:
(1) 选择合适的隔离级别
(2) 减小事务的规模
(3) 尽量在SQL中使用索引
(4) 避免长时间的锁定
(5) 定期清理无效的锁
数据库系统概念 Abraham Silberschatz, Henry F. Korth, S. Sudarshan 著
https://www.cnblogs.com/qlqwjy/p/7798266.html 乐观锁和悲观锁的区别
https://www.cnblogs.com/kexinxin/p/11620345.html 数据库中的锁
https://cloud.tencent.com/developer/article/2332879 MySQL 锁的完全解析:提高数据库性能的关键技术
https://learnku.com/articles/39212 一张图彻底搞懂 MySQL 的锁机制
https://www.zhihu.com/tardis/zm/art/52312376?source_id=1005 数据库基础(三)Mysql里的锁
https://zhuanlan.zhihu.com/p/185003485 详解 MySql InnoDB 中意向锁的作用
https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html#innodb-intention-locks InnoDB Locking
https://blog.csdn.net/zcl_love_wx/article/details/82015281 一分钟深入Mysql的意向锁——《深究Mysql锁》
https://zhuanlan.zhihu.com/p/52879825 浅谈数据库共享锁与排它锁
https://juejin.cn/post/7088256473574309919 MySQL表锁、行锁和页锁
https://www.mysqlzh.com/doc/218.html BDB (BerkeleyDB)存储引擎
https://blog.csdn.net/kuyuyingzi/article/details/87710259 MySQL 表锁和行锁机制(很详细)
https://blog.csdn.net/qq_16268979/article/details/103441178 MyISAM的表锁和InnoDB的行锁
https://www.jianshu.com/p/478bc84a7721 MySQL记录锁、间隙锁、临键锁(Next-Key Locks)详解
https://blog.csdn.net/zhangchaoyang/article/details/108809249 MySQL锁 —— 记录锁、间隙锁、临键锁、自增锁