数据库的锁定机制是为了确保数据一致性而设计的重要规则。通过锁定机制,数据库管理系统能够管理并发访问数据时可能产生的冲突,确保数据的正确性和完整性。另一方面,MySQL也存在多种数据存储引擎,每种存储引擎的锁机制都是为各自的特定场景而优化设计,所以各存储引擎的锁机制也有较大区别。
我们可能听过Mysql各种叫法的锁,他们大部分是从不同维度来去划分而命名的,如下图。
乐观锁就是持比较乐观态度的锁。在操作数据时非常乐观,认为别的线程不会同时修改数据,所以不会上锁,但是在更新的时候会判断在此期间别的线程有没有更新过这个数据。
实现方式:乐观锁不会使用数据库本身的锁机制,而是依据数据本身来保证数据的正确性。一般是通过在数据表中增加一个版本号或时间戳字段来实现。在更新数据时,检查版本号或时间戳是否与读取时一致,如果一致则更新,否则返回异常信息,让应用层决定需要如何处理(比如重试)。
应用场景:这是一种假设冲突很少发生的并发控制机制。它适用于读多写少的场景,比如,在读操作远多于写操作的场景中,乐观锁的性能优势明显。
悲观锁就是持比较悲观态度的锁。总是假设最坏的情况,每次读取数据的时候都默认其他线程会更改数据,因此需要进行加锁操作,当其他线程想要访问数据时,都需要阻塞挂起。在对一条数据修改的时候,为了避免同时被其他人修改,在修改数据之前先锁定再修改。这种锁具有强烈的独占和排它特性。在Mysql中,要使用悲观锁,必须关闭 MySQL 数据库的自动提交属性。因为 MySQL 默认使用 autocommit 模式,也就是说,当执行一个更新操作后,MySQL 会立刻将结果进行提交。
实现方式:共享锁和排他锁是悲观锁的不同实现,但都属于悲观锁范畴。悲观锁的实现,往往依靠数据库提供的锁机制。
应用场景:悲观锁是一种假设冲突经常发生的并发控制机制。它适用于需要强一致性和高冲突概率的场景或者高并发环境中多事务频繁读取修改数据。比如,银行账户余额更新、库存管理系统中的库存更新等。
MySQL 中的锁可以按照粒度分为锁定整个表的表级锁(table-level locking)和锁定数据行的行级锁(row-level locking)。InnoDB 存储引擎同时支持行级锁(row-level locking)和表级锁(table-level locking),默认情况下采用行级锁。
表级锁是指对整个表进行锁定,防止其他事务对该表进行任何写操作(写锁)或读操作(读锁)。MyISAM、InnoDB 等存储引擎都支持。 特点
示例
sqlLOCK TABLES users WRITE;
-- 事务A持有了users表的写锁
行级锁是指对特定的行(记录)进行锁定,防止其他事务对该行进行并发操作。
特点
sqlSELECT * FROM users WHERE user_id = 1 FOR UPDATE;
-- 事务A持有了user_id为1的记录锁
行级锁的实现 InnoDB通过在数据页上维护一个位图(BitMap)来管理行级锁。位图中的每一位对应于数据页中的一行,这种设计可以显著降低锁的管理成本。
每次锁定相邻的一组记录,页级锁定的特点是锁定颗粒度介于行级锁定与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力也同样是介于上面二者之间。
共享锁(S):也称为读锁。当一个事务获取了共享锁后,其他事务也可以获取相同的共享锁,允许它们同时读取同一资源,但不允许任何事务修改该资源。在Mysql中,可以使用以下语句获取该锁。
比如,事务A对记录添加了S锁,可以对记录进行读操作,但不能修改,其他事务可以对该记录追加S锁,但是不能追加X锁,如果需要追加,需要等记录的S锁全部释放。
sqlSELECT ... LOCK IN SHARE MODE;
START TRANSACTION;
SELECT ... FOR SHARE;
排他锁(X):也称为写锁。当一个事务获取了排他锁后,其他事务不能获取相同的排他锁或共享锁,也不能修改该资源,确保了资源的独占性,但允许持有锁的事务更新/删除行 。在Mysql中,可以使用以下语句获取该锁。
比如,事务A对记录添加了X锁,可以对记录进行读和修改操作,其他事务不能对记录做读和修改操作。
sqlSELECT ... FOR UPDATE;
START TRANSACTION;
SELECT ... FOR UPDATE;
关系
兼容性 | S | X |
---|---|---|
S | 兼容 | 不兼容 |
X | 不兼容 | 不兼容 |
在MySQL中,除了行级锁和表级锁之外,还引入了意向锁(Intention Lock),这种锁定机制用于更细粒度地控制事务对数据的访问。意向锁是一种表级别的锁,用来表示一个事务准备获取何种类型的行级锁。它并不实际限制其他事务对行的访问,而是指示了事务打算在表的哪些部分(例如某些行)获取共享锁(S锁)或排他锁(X锁)。
特点
InnoDB引擎 vs MyISAM引擎
它表示一个事务打算在表的某些行上获取共享锁(S锁)。意向共享锁不是真正的锁,不会阻止其他事务获取共享锁或意向排他锁,但是它会阻止其他事务获取排他锁(X锁),因为排他锁要求表中没有任何共享锁或意向排他锁。已加S锁的表肯定会有IS锁,反之,有IS锁的表不一定会有S锁。
它表示一个事务打算在表的某些行上获取排他锁(X锁)。意向排他锁同样也不是真正的锁,会阻止其他事务获取任何类型的锁,因为排他锁要求表中没有任何共享锁或排他锁。已加X锁的表肯定会有IX锁,反之有IX锁的表,不一定会有X锁。 意向锁的原则:
InnoDB 引擎行锁是通过对索引数据页上的记录加锁实现的,主要实现算法有 3 种:记录锁(Record Lock)、间隙锁(Gap Lock)和 临键锁(Next-key Lock)。
记录锁是加在索引上的(RC、RR隔离级别都支持),一种锁定特定行记录的锁,持有记录锁之后,其他事务无法插入/更新/删除该索引对应的行。它用于确保在多事务环境中对特定行的并发访问不会导致数据不一致。
特点
记录锁锁住的永远是索引,而非记录本身,即使该表上没有任何索引,那么innodb会在后台创建一个隐藏的聚集主键索引,锁住的就是这个隐藏的聚集主键索引。
示例
sqlSELECT * FROM users WHERE user_id = 1 FOR UPDATE;
在上面这个示例中,FOR UPDATE语句会对user_id为1的行加上记录锁,防止其他事务对这行数据进行修改。但需要注意的是,user_id 列必须为唯一索引列或主键列,否则上述语句加的锁就会变成临键锁。同时查询语句必须为精准匹配(=),不能为 >、<、like 等,否则也会退化成临键锁。 另外,在通过主键索引或唯一索引对数据行进行 UPDATE 操作时,也会对该行数据加记录锁。
sqlUPDATE SET sex = 1 WHERE user_id = 1 FOR UPDATE;
其它注意事项
间隙锁加在索引之间的空隙上,仅仅锁住一个索引区间(开区间)。 间隙锁的目的主要是为了防止幻读,保证索引间的不会被插入数据。间隙锁需要在性能和一致性上去做取舍,并非所有的事务隔离级别都有间隙锁,需要可重复读或更高的级别。
特点
幻读是指一个事务在读取某个范围内的记录后,另一个事务在该范围内插入了新的记录,导致前一个事务再次读取时,发现了“幻影”般的新记录。
示例
sqlSELECT c1 FROM t WHERE c1 BETWEEN 10 AND 20 FOR UPDATE
SELECT * FROM orders WHERE order_date BETWEEN '2023-01-01' AND '2023-01-31' FOR UPDATE
第一条sql会阻止其他事务插入 c1 = 15,无论之前DB里有没有15这条记录。 第二条sql持有了订单日期在2023年1月1日至2023年1月31日之间的所有记录的间隙锁。 间隙锁的作用就是防止其他事物插入数据,间隙锁可以共存,允许在相同的间隙上加多个锁。类似于S锁。
临键锁是行锁和间隙锁的结合体(左开右闭区间),锁住当前索引本身以及它之前的间隙。用于防止幻读现象,确保事务的隔离性和一致性。InnoDB 中行级锁是基于索引实现的,临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁。
默认情况下,innodb使用临键锁来锁定记录。但当查询的索引含有唯一属性的时候,临键锁会进行优化,将其降级为记录锁,即仅锁住索引本身,不是范围。
假设有如下表:id 主键,age 普通索引。
示例
id | name | age |
---|---|---|
1 | zhangsan | 10 |
2 | lisi | 20 |
3 | wangwu | 32 |
表中 age 列潜在的临键锁有:(-∞, 10],(10, 20],(20, 32],(32, +∞]。
sqlUPDATE SET sex = 1 WHERE age = 10
根据非唯一索引列 UPDATE 某条记录 。
总结
事务(Transaction)是指一组操作,这些操作要么全部成功,要么全部失败。事务的主要目的是确保数据库在并发环境下的数据一致性和完整性。事务具有以下四个关键属性,这些属性统称为ACID特性:
在读未提交隔离级别下,一个事务可以读取其他事务尚未提交的数据。这意味着一个事务可能会读取到其他事务正在修改但尚未提交的“脏数据”。
问题
在读已提交隔离级别下,一个事务只能读取到其他事务已经提交的数据。这意味着一个事务不会读取到未提交的数据,但可能会读取到其他事务在本事务执行期间提交的数据。
问题
在可重复读隔离级别下,一个事务在开始时看到的数据,在整个事务过程中都不会改变。其他事务在本事务执行期间,不能修改或删除本事务已经读取的数据。
问题
在可序列化隔离级别下,事务完全隔离,仿佛每个事务是按顺序执行的,而不是并发执行的。任何事务的执行结果与其独占执行的结果相同。 问题 最高隔离级别:提供了最严格的数据一致性保证,防止脏读、不可重复读和幻读。 对比
隔离级别 | 脏读(Dirty Read) | 不可重复读(Non-repeatable Read) | 幻读(Phantom Read) | 性能 |
---|---|---|---|---|
读未提交(Read Uncommitted) | 可能 | 可能 | 可能 | 最高 |
读已提交(Read Committed) | 不可能 | 可能 | 可能 | 较高 |
可重复读(Repeatable Read) | 不可能 | 不可能 | 可能 | 较低 |
可序列化(Serializable) | 不可能 | 不可能 | 不可能 | 最低 |
事务隔离级别相当于事务并发控制的整体解决方案,本质上是对锁和MVCC使用的封装,隐藏了底层细节。 锁是数据库实现并发控制的基础,事务隔离性是采用锁来实现,对相应操作加不同的锁,就可以防止其他事务同时对数据进行读写操作。
对开发者来说,首先选择使用隔离级别,当选用的隔离级别不能解决并发问题或需求时,才有必要在开发中手动的设置锁。
在MySQL中,不同的SQL操作会触发不同类型的锁,以确保数据的一致性和并发控制。类似的sql可能会锁行,也可能会锁Gap/Next-Key,取决于事务隔离级别以及后续的where条件能否命中唯一索引 。以下是常见的各种操作和锁类型的详细解释:
死锁DeadLock是指两个或两个以上的进程在执行过程中, 因争夺资源而造成的一种互相等待的现象, 若无外力作用,它们都将无法推进下去.此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
InnoDB存储引擎具有内置的死锁检测机制,可以检测并处理死锁,以确保数据库系统的正常运行。以下是MySQL如何判断和处理死锁的详细解释:
示例 假设有两个事务A和B,分别执行以下操作: 事务A
sqlBEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- 事务A持有account_id=1的X锁
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
-- 事务A等待account_id=2的X锁
事务B
sqlBEGIN;
UPDATE accounts SET balance = balance - 50 WHERE account_id = 2;
-- 事务B持有account_id=2的X锁
UPDATE accounts SET balance = balance + 50 WHERE account_id = 1;
-- 事务B等待account_id=1的X锁
在这个示例中,事务A和事务B相互等待对方持有的锁,形成了一个死锁。
更新等待图:事务A等待事务B释放account_id=2的锁。事务B等待事务A释放account_id=1的
Section 14.7.5.3, “How to Minimize and Handle Deadlocks”
下文会进一步讨论三条insert引发死锁的问题。
本文作者:sora
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!