Mysql--基础篇--事务(ACID特征及实现原理,事务管理模式,隔离级别,并发问题,锁机制,行级锁,表级锁,意向锁,共享锁,排他锁,死锁,MVCC)
在MySQL中,事务(Transaction)是一组SQL语句的集合,这些语句一起被视为一个单一的工作单元。事务具有ACID特性,确保数据的一致性和完整性。通过事务,可以保证多个操作要么全部成功执行,要么全部不执行,从而避免部分操作成功而另一部分失败的情况。
一、ACID特性介绍
- 原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成。如果事务中的任何一个操作失败,整个事务将被回滚,恢复到事务开始之前的状态。
- 一致性(Consistency):事务必须确保数据库从一个一致状态转换到另一个一致状态。事务执行前后,数据库的完整性约束不能被破坏。
- 隔离性(Isolation):并发执行的多个事务之间是相互隔离的,一个事务的中间状态不会影响其他事务。事务的隔离性通过不同的隔离级别来实现。
- 持久性(Durability):一旦事务提交,其对数据库的更改将永久保存,即使系统发生故障也不会丢失。
1、原子性(Atomicity)
原子性确保事务中的所有操作要么全部成功执行,要么全部不执行。如果事务中的任何一个操作失败,整个事务将被回滚,恢复到事务开始之前的状态。原子性保证了事务是一个不可分割的工作单元。
示例:
假设我们有一个银行转账系统,用户A向用户B转账100元。这个操作涉及两个步骤:
1.从用户A的账户中扣除100元。
2.将100元添加到用户B的账户中。
如果这两个步骤中的任何一个失败,整个转账操作应该被取消,以避免资金丢失或重复转账。
sql:
-- 开始事务
START TRANSACTION;
-- 步骤1:从用户A的账户中扣除100元
UPDATE accounts SET balance = balance - 100 WHERE user_id = 'A';
-- 步骤2:将100元添加到用户B的账户中
UPDATE accounts SET balance = balance + 100 WHERE user_id = 'B';
-- 提交事务
COMMIT;
如果发生错误:
假设在执行UPDATE accounts SET balance = balance + 100 WHERE user_id = ‘B’;时,系统突然崩溃或出现其他错误。此时,MySQL会自动回滚整个事务,撤销对用户A和用户B账户的所有更改,确保数据的一致性。
回滚事务:
ROLLBACK;
结果:
用户A的账户余额不会减少,用户B的账户余额也不会增加,系统恢复到事务开始之前的状态。
2、一致性(Consistency)
一致性确保事务执行前后,数据库始终处于一致状态。事务必须遵守数据库的完整性约束,如外键约束、唯一约束、检查约束等。事务完成后,数据库的完整性约束不能被破坏。
示例:
假设我们有一个订单系统,订单表orders中有一个外键customer_id,引用客户表customers中的id列。我们希望确保每个订单都必须关联到一个有效的客户。
sql:
-- 创建客户表
CREATE TABLE customers (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50)
);
-- 创建订单表,包含外键约束
CREATE TABLE orders (
order_id INT AUTO_INCREMENT PRIMARY KEY,
customer_id INT,
order_date DATE,
FOREIGN KEY (customer_id) REFERENCES customers(id)
);
插入无效的订单数据:
插入的订单数据,该订单的customer_id不存在于customers表中。
INSERT INTO orders (customer_id, order_date) VALUES (999, '2024-01-01')
结果:
MySQL会抛出错误,阻止插入操作,因为customer_id = 999不存在于customers表中。这确保了数据库的一致性,防止了无效数据的插入。
Error Code: 1452. Cannot add or update a child row: a foreign key constraint fails (database_name.orders, CONSTRAINT orders_ibfk_1 FOREIGN KEY (customer_id) REFERENCES customers (id))
插入有效的订单数据:
只有当customer_id存在于customers表中时,插入操作才会成功。
INSERT INTO customers (id, name) VALUES (1, 'Alice');
INSERT INTO orders (customer_id, order_date) VALUES (1, '2024-01-01');
结果:
订单成功插入,数据库保持一致状态。
3、隔离性(Isolation)
隔离性确保多个并发事务之间是相互隔离的,一个事务的中间状态不会影响其他事务。事务的隔离性通过不同的隔离级别来实现。
常见的隔离级别包括:
- 读未提交(Read Uncommitted)
- 读已提交(Read Committed)
- 可重复读(Repeatable Read)
- 可序列化(Serializable)
示例:
假设我们有两个事务T1和T2,它们同时对同一行数据进行操作。我们将通过不同的隔离级别来展示隔离性的作用。
示例1:读未提交(Read Uncommitted)
在读未提交隔离级别下,事务可以读取其他事务尚未提交的数据,即“脏读”。
sql:
-- 设置隔离级别为读未提交
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- T1 开始
START TRANSACTION;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 'A'; -- 未提交
-- T2 开始
START TRANSACTION;
SELECT balance FROM accounts WHERE user_id = 'A'; -- 读取到未提交的数据
结果说明:
T2会读取到了T1尚未提交的数据,导致脏读。如果T1最终回滚,T2读取到的数据实际就是无效的。
示例2:读已提交(Read Committed)
在读已提交隔离级别下,事务只能读取已经提交的数据,避免了脏读。
sql:
-- 设置隔离级别为读已提交
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- T1 开始
START TRANSACTION;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 'A'; -- 未提交
-- T2 开始
START TRANSACTION;
SELECT balance FROM accounts WHERE user_id = 'A'; -- 读取到旧版本数据
-- T1 提交
COMMIT;
-- T2 再次读取
SELECT balance FROM accounts WHERE user_id = 'A'; -- 读取到新版本数据
结果说明:
T2在T1提交之前读取的是旧版本数据,避免了脏读。
T1提交后,T2再次查询才能看到更新后的数据。
示例3:可重复读(Repeatable Read)
在可重复读隔离级别下,事务在整个生命周期内看到的是事务开始时的一致性视图,避免了脏读和不可重复读。
sql:
-- 设置隔离级别为可重复读
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- T1 开始
START TRANSACTION;
SELECT balance FROM accounts WHERE user_id = 'A'; -- 读取到旧版本数据
-- T2 开始
START TRANSACTION;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 'A';
COMMIT;
-- T1 再次读取
SELECT balance FROM accounts WHERE user_id = 'A'; -- 仍然读取到旧版本数据
结果说明:
T1在整个事务期间看到的始终是事务开始时的快照,即使T2已经提交了更新,T1也不会看到这些更改。这确保了可重复读。
示例4:可序列化(Serializable)
在可序列化隔离级别下,事务按顺序执行,完全隔离并发操作,避免了所有并发问题(如脏读、不可重复读和幻读)。
简单理解:并发的事务会同步执行,后提交的事务必须等待前面的事务提交或回滚后才可以执行。
sql:
-- 设置隔离级别为可序列化
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- T1 开始
START TRANSACTION;
SELECT * FROM orders WHERE customer_id = 1; -- 读取订单
-- T2 开始
START TRANSACTION;
INSERT INTO orders (customer_id, order_date) VALUES (1, '2024-01-02'); -- 插入新订单
-- T1 再次读取
SELECT * FROM orders WHERE customer_id = 1; -- 仍然读取到旧版本数据
结果说明:
T2的插入操作会被阻塞,直到T1提交或回滚。T1在整个事务期间看到的始终是事务开始时的快照,避免了幻读。
4、持久性(Durability)
持久性确保一旦事务提交,其对数据库的更改将永久保存,即使系统发生故障也不会丢失。持久性通常通过将事务的日志写入磁盘来实现,确保即使在系统崩溃后,事务的更改也可以恢复。
示例:
假设我们执行了一个事务,向银行账户中存入100元:
sql:
-- 开始事务
START TRANSACTION;
-- 向用户 A 的账户中存入 100 元
UPDATE accounts SET balance = balance + 100 WHERE user_id = 'A';
-- 提交事务
COMMIT;
解释:
- 持久性保证:当事务提交时,MySQL会将事务的日志写入磁盘,并确保数据已经安全存储。即使系统在此时发生故障(如断电或崩溃),事务的更改仍然可以通过日志恢复,确保数据不会丢失。
为了进一步提高持久性,MySQL使用了WAL(Write-Ahead Logging,预写式日志)技术。在事务提交之前,MySQL会先将更改写入日志文件,然后再将数据写入实际的数据库文件。这样,即使系统在事务提交后立即崩溃,日志文件也可以用于恢复事务的更改。
二、事务管理模式
1、显式提交事务
MySQL事务的基本操作包括:
- 开始事务:使用START TRANSACTION或BEGIN语句显式开始一个事务。
- 提交事务:使用COMMIT语句提交事务,使所有操作生效并永久保存。
- 回滚事务:使用ROLLBACK语句回滚事务,撤销所有未提交的操作,恢复到事务开始之前的状态。
提交事务示例:
-- 开始事务
START TRANSACTION;
-- 执行多个 SQL 操作
INSERT INTO employees (name, age) VALUES ('Alice', 30);
UPDATE employees SET age = 31 WHERE name = 'Alice';
-- 提交事务
COMMIT;
回滚事务示例:
如果在事务执行过程中发生错误,可以使用ROLLBACK来撤销所有操作。
-- 开始事务
START TRANSACTION;
-- 执行多个 SQL 操作
INSERT INTO employees (name, age) VALUES ('Bob', 25);
UPDATE employees SET age = -1 WHERE name = 'Bob'; -- 错误操作
-- 回滚事务
ROLLBACK;
2、隐式提交事务(Autocommit)
MySQL默认启用了自动提交模式,这意味着每个SQL语句都会被视为一个独立的事务,并且在执行后立即提交。在这种模式下,不需要显式使用START TRANSACTION、COMMIT或ROLLBACK。
自动提交事务标识autocommit为1时,表示启用自动提交事务。为0表示禁用自动提交模式。默认开启,值为1。
查看事务的自动提交状态:
SELECT @@autocommit;
运行结果:
禁用自动提交模式:
SET autocommit = 0;
启用自动提交模式:
SET autocommit = 1;
注意事项:
- 当autocommit设置为0时,所有的SQL语句都会被包含在一个事务中,直到你显式调用COMMIT或ROLLBACK。
- 如果你希望在禁用自动提交模式的情况下执行单个SQL语句并立即提交,可以使用START TRANSACTION WITH CONSISTENT SNAPSHOT; 或SET autocommit = 1;。
三、并发控制
1、事务的隔离级别
事务的隔离级别确定了多个并发事务之间的可见性和交互方式。MySQL支持四种标准的隔离级别,每种级别提供了不同程度的隔离性,以平衡性能和一致性。
(1)、读未提交(Read Uncommitted)
允许一个事务读取另一个事务尚未提交的数据(即“脏读”)。这是最低的隔离级别,可能会导致读取到不一致的数据。
特点:
- 可能会出现脏读、不可重复读和幻读。
- 性能最好,但一致性最差。
适用场景:
很少使用,除非对数据一致性要求非常低。
sql:
-- 设置隔离级别为读未提交
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- T1 开始
START TRANSACTION;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 'A'; -- 未提交
-- T2 开始
START TRANSACTION;
SELECT balance FROM accounts WHERE user_id = 'A'; -- 读取到未提交的数据
结果说明:
T2会读取到了T1尚未提交的数据,导致脏读。如果T1最终回滚,T2读取到的数据实际就是无效的。
(2)、读已提交(Read Committed)
一个事务只能读取另一个事务已经提交的数据。它防止了脏读,但仍然可能出现不可重复读和幻读。
特点:
- 防止脏读。
- 可能会出现不可重复读和幻读。
适用场景:
适用于大多数应用程序,尤其是那些对数据一致性要求较高但对并发性能有一定要求的场景。
sql:
-- 设置隔离级别为读已提交
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- T1 开始
START TRANSACTION;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 'A'; -- 未提交
-- T2 开始
START TRANSACTION;
SELECT balance FROM accounts WHERE user_id = 'A'; -- 读取到旧版本数据
-- T1 提交
COMMIT;
-- T2 再次读取
SELECT balance FROM accounts WHERE user_id = 'A'; -- 读取到新版本数据
结果说明:
T2在T1提交之前读取的是旧版本数据,避免了脏读。
T1提交后,T2再次查询才能看到更新后的数据。
(3)、可重复读(Repeatable Read)
在同一事务中,多次读取同一行数据的结果是一致的。它防止了脏读和不可重复读,但仍然可能出现幻读。
特点:
- 防止脏读和不可重复读。
- 可能会出现幻读。
适用场景:
这是MySQL的默认隔离级别,适用于大多数应用场景,尤其是在需要保证读取一致性的情况下。
sql:
-- 设置隔离级别为可重复读
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- T1 开始
START TRANSACTION;
SELECT balance FROM accounts WHERE user_id = 'A'; -- 读取到旧版本数据
-- T2 开始
START TRANSACTION;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 'A';
COMMIT;
-- T1 再次读取
SELECT balance FROM accounts WHERE user_id = 'A'; -- 仍然读取到旧版本数据
结果说明:
T1在整个事务期间看到的始终是事务开始时的快照,即使T2已经提交了更新,T1也不会看到这些更改。这确保了可重复读。
(4)、可序列化(Serializable)
最高的隔离级别,完全隔离并发事务。它通过锁定机制确保事务按顺序执行,防止任何并发问题(如脏读、不可重复读和幻读)。
特点:
- 防止脏读、不可重复读和幻读。
- 性能较差,因为并发度较低。
适用场景:
适用于对数据一致性要求极高的场景,但在高并发环境下可能会影响性能。
sql:
-- 设置隔离级别为可序列化
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- T1 开始
START TRANSACTION;
SELECT * FROM orders WHERE customer_id = 1; -- 读取订单
-- T2 开始
START TRANSACTION;
INSERT INTO orders (customer_id, order_date) VALUES (1, '2024-01-02'); -- 插入新订单
-- T1 再次读取
SELECT * FROM orders WHERE customer_id = 1; -- 仍然读取到旧版本数据
结果说明:
T2的插入操作会被阻塞,直到T1提交或回滚。T1在整个事务期间看到的始终是事务开始时的快照,避免了幻读。
2、设置事务隔离级别
查看当前隔离级别:
SELECT @@transaction_isolation;
设置当前会话的事务隔离级别:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
设置全局隔离级别:
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
3、事务的并发问题
在并发环境下,多个事务同时访问和修改相同的数据时,可能会出现以下问题:
(1)、脏读(Dirty Read)
一个事务读取了另一个事务尚未提交的数据。如果该事务后来回滚,读取到的数据将是无效的。
(2)、不可重复读(Non-repeatable Read)
在同一个事务中,两次读取同一行数据的结果不同。这通常是由于另一个事务在这两次读取之间修改了该行并提交了更改。
(3)、幻读(Phantom Read)
在同一个事务中,两次查询返回的行数不同。这通常是由于另一个事务在这两次查询之间插入或删除了某些行。
4、解决并发问题的方法
- 提高隔离级别:通过提高事务的隔离级别(如从READ COMMITTED提升到REPEATABLE READ或SERIALIZABLE),可以减少并发问题的发生。
- 使用锁:MySQL提供了多种锁机制(如行级锁、表级锁等),可以在必要时手动加锁以确保数据的一致性。
- 优化查询和索引:通过优化查询和索引,减少事务的执行时间,降低并发冲突的可能性。
5、事务的锁机制
MySQL的锁机制是确保数据一致性和并发控制的核心技术。通过锁,多个事务可以安全地同时访问和修改数据库中的数据,而不会导致数据不一致或冲突。不同的存储引擎(如 InnoDB和MyISAM)支持不同类型的锁机制。
MySQL使用锁机制来确保事务的隔离性和一致性。根据锁的作用范围和粒度,MySQL支持以下几种锁类型:
(1)、行级锁(Row-Level Locking)
作用范围:锁定单个行记录。
优点:允许高并发,多个事务可以同时访问不同的行。
缺点:管理开销较大,适用于更新频繁但并发度较高的场景。
适用存储引擎:InnoDB支持行级锁。
示例:行级锁
假设我们有一个employees表,包含员工的ID、姓名和年龄。我们希望在两个事务中分别更新不同员工的年龄,而不相互阻塞。
sql:
-- 事务 T1:更新员工A的年龄
START TRANSACTION;
SELECT * FROM employees WHERE id = 1 FOR UPDATE; -- 获取行级锁
UPDATE employees SET age = 30 WHERE id = 1;
COMMIT;
-- 事务 T2:更新员工B的年龄
START TRANSACTION;
SELECT * FROM employees WHERE id = 2 FOR UPDATE; -- 获取行级锁
UPDATE employees SET age = 28 WHERE id = 2;
COMMIT;
结果说明:
T1和T2可以同时执行,因为它们锁定的是不同的行。行级锁确保了每个事务只能修改自己锁定的行,而不会影响其他事务对其他行的操作。
扩展一下行级锁:
1、行级排他锁FOR UPDATE
FOR UPDATE是MySQL中用于显式获取行级排他锁的一种方式。当你在查询中使用FOR UPDATE时,MySQL会为查询结果中的每一行加上排他锁(X锁),确保其他事务在这段时间内不能对该行进行读取或写操作。
具体作用:
- FOR UPDATE获取行级排他锁:其他事务不能对该行加任何类型的锁(包括共享锁和排他锁),直到当前事务提交或回滚。
- 锁的持续时间:行级锁会在事务结束时自动释放,即当事务提交(COMMIT)或回滚(ROLLBACK)后,锁才会被释放。
- 查询结束后不会自动释放锁:即使查询已经执行完毕,行级锁仍然会保持,直到事务结束。
示例:FOR UPDATE行级排他锁阻塞行为
假设我们有一个employees表,包含员工的ID、姓名和年龄。我们希望在两个事务中分别更新不同员工的年龄,并确保在这段时间内没有其他事务可以修改这些员工的数据。
-- 事务T1:更新员工A的年龄
START TRANSACTION;
SELECT * FROM employees WHERE id = 1 FOR UPDATE; -- 获取行级排他锁
UPDATE employees SET age = 30 WHERE id = 1;
COMMIT;
-- 事务T2:尝试更新员工A的年龄
START TRANSACTION;
SELECT * FROM employees WHERE id = 1 FOR UPDATE; -- 被阻塞,直到T1释放锁
UPDATE employees SET age = 35 WHERE id = 1;
COMMIT;
结果说明:
T2在T1持有排他锁期间,无法获取该行的任何类型的锁(包括共享锁和排他锁),因此T2的查询会被阻塞,直到T1提交或回滚事务并释放锁。
FOR UPDATE的工作原理:
(1)、获取排他锁:当你执行SELECT … FOR UPDATE时,MySQL会为查询结果中的每一行加上排他锁(X锁)。这意味着其他事务在这段时间内不能对该行进行读取或写操作。
(2)、锁的持续时间:行级锁会在事务结束时自动释放。也就是说,锁会一直保持,直到你显式地执行COMMIT或ROLLBACK。即使查询已经执行完毕,锁也不会自动释放。
(3)、查询结束后不会自动释放锁:这是非常重要的一个概念。很多开发者误以为查询结束后锁就会自动释放,但实际上,锁会一直保持,直到事务结束。因此,如果你在事务中执行了FOR UPDATE,其他事务将无法对该行进行任何操作,直到你提交或回滚事务。
(4)、避免长时间持有锁:由于FOR UPDATE会锁定行,建议尽量缩短事务的持续时间,以减少对其他事务的影响。长时间持有锁可能会导致其他事务等待,甚至引发死锁。
FOR UPDATE的应用场景:
- 防止并发修改:当你需要确保某个行在一段时间内不被其他事务修改时,可以使用FOR UPDATE来获取排他锁。例如,在银行转账系统中,确保同一笔资金不会被多个事务同时修改。
- 防止幻读:虽然FOR UPDATE主要用于防止并发修改,但它也可以防止其他事务插入新的行,从而避免幻读问题。幻读是指在同一事务中,两次查询返回的行数不同,通常是由于其他事务插入了新行。
- 批量更新:当你需要批量更新某些行时,可以使用FOR UPDATE来确保这些行在这段时间内不会被其他事务修改。
2、行级共享锁SHARE MODE
除了FOR UPDATE,MySQL还提供了另一种锁机制LOCK IN SHARE MODE,它用于获取行级共享锁(S锁)。
示例:共享锁LOCK IN SHARE MODE
-- 事务 T1:获取共享锁
START TRANSACTION;
SELECT * FROM employees WHERE id = 1 LOCK IN SHARE MODE; -- 获取共享锁
-- 读取员工信息
SELECT name, age FROM employees WHERE id = 1;
COMMIT;
-- 事务 T2:尝试获取共享锁
START TRANSACTION;
SELECT * FROM employees WHERE id = 1 LOCK IN SHARE MODE; -- 可以获取共享锁
-- 读取员工信息
SELECT name, age FROM employees WHERE id = 1;
COMMIT;
-- 事务 T3:尝试获取排他锁
START TRANSACTION;
SELECT * FROM employees WHERE id = 1 FOR UPDATE; -- 被阻塞,直到所有共享锁被释放
UPDATE employees SET age = 35 WHERE id = 1;
COMMIT;
结果说明:
T1和T2可以同时持有共享锁,读取同一行数据。然而,T3在T1和T2持有共享锁期间,无法获取排他锁,因此T3的查询会被阻塞,直到T1和T2提交或回滚事务并释放共享锁。
3、两种行级锁区别
- FOR UPDATE:获取排他锁,阻止其他事务对该行进行读取或写操作。
- LOCK IN SHARE MODE:获取共享锁,允许多个事务同时读取该行,但阻止其他事务对该行进行写操作。
4、行级锁总结
- FOR UPDATE:获取行级排他锁,防止其他事务对该行进行读取或写操作。锁会一直保持,直到事务提交或回滚,而不是在查询结束后自动释放。
- 锁的持续时间:从FOR UPDATE执行开始,直到事务结束。
- 查询结束后不会自动释放锁:锁会一直保持,直到事务结束。
- 避免长时间持有锁:尽量缩短事务的持续时间,以减少对其他事务的影响。
(2)、表级锁(Table-Level Locking)
在MySQL中,表级锁(Table-Level Locking)是一种锁定整个表的机制,确保在事务执行期间没有其他事务可以对该表进行读取或写入操作。表级锁适用于需要对整个表进行独占访问的场景,例如批量插入、删除或更新数据。
MySQL的不同存储引擎对表级锁的支持有所不同:
- MyISAM:默认使用表级锁。
- InnoDB:支持行级锁,但在某些情况下也可以获取表级锁(如LOCK TABLES和ALTER TABLE操作)。
作用范围:锁定整个表
优点:实现简单,适合批量操作。
缺点:并发度较低,锁定整个表会阻塞其他事务对该表的访问。
适用存储引擎:MyISAM支持表级锁。
示例:表级锁
假设我们有一个orders表,包含订单信息。我们希望在事务中插入多条订单记录,并确保在这段时间内没有其他事务可以修改该表。
sql:
-- 事务T1:插入订单并获取表级锁
START TRANSACTION;
LOCK TABLE orders WRITE; -- 获取写锁,阻止其他事务读写该表
INSERT INTO orders (order_id, customer_id, order_date) VALUES (1, 1001, '2024-01-01');
INSERT INTO orders (order_id, customer_id, order_date) VALUES (2, 1002, '2024-01-02');
UNLOCK TABLES; -- 释放表级锁
COMMIT;
结果说明:
在T1持有表级锁期间,其他事务无法对该表进行任何读写操作,直到T1释放锁。这确保了T1的操作是独占的,避免了并发冲突。
扩展一下表级锁:
1、显式获取表级锁
使用LOCK TABLES和UNLOCK TABLES显示设置和释放表级别锁。
LOCK TABLES命令用于显式地获取表级锁,而UNLOCK TABLES用于释放这些锁。你可以为一个或多个表指定不同的锁类型,具体包括:
- 读锁(Read Lock):允许其他事务读取该表,但不允许写入。
- 写锁(Write Lock):禁止其他事务对该表进行任何读取或写入操作,确保当前事务对该表有独占访问权。
(1)、获取表级读锁
假设我们有一个employees表,包含员工的ID、姓名和年龄。我们希望在事务中读取所有员工的信息,但不希望其他事务在此期间修改该表的数据。
sql:
-- 获取读锁
LOCK TABLES employees READ;
-- 读取员工信息
SELECT * FROM employees;
-- 释放锁
UNLOCK TABLES;
结果说明:
在事务持有读锁期间,其他事务可以继续读取employees表中的数据,但不能对该表进行任何写操作(如插入、更新或删除)。当执行完成UNLOCK TABLES,锁将被释放,其他事务才可以恢复对该表的写操作。
注意:
- 读锁允许多个事务同时持有,因此多个事务可以并发读取同一张表。但是会阻止其他事务对该表进行写操作。
(2)、获取表级写锁
假设我们有一个orders表,包含订单信息。我们希望在事务中插入多条订单记录,并确保在这段时间内没有其他事务可以修改该表。
sql:
-- 获取写锁
LOCK TABLES orders WRITE;
-- 插入多条订单记录
INSERT INTO orders (order_id, customer_id, order_date) VALUES (1, 1001, '2024-01-01');
INSERT INTO orders (order_id, customer_id, order_date) VALUES (2, 1002, '2024-01-02');
-- 释放锁
UNLOCK TABLES;
结果说明:
在持有写锁期间,其他事务无法对该表进行任何读/写操作,直到当前事务释放锁。这确保了当前事务对该表有独占访问权,避免了并发冲突。
注意:
写锁是排他性的,只能由一个事务持有。写锁会阻止其他事务对该表进行任何读取或写入操作,即使是读取也不允许。
(3)、同时获取多个表的锁
你可以在同一个LOCK TABLES语句中为多个表指定不同的锁类型。例如,假设我们有两个表employees和departments,我们希望在事务中读取employees表并更新departments表。
sql:
-- 获取employees表的读锁和departments表的写锁
LOCK TABLES employees READ, departments WRITE;
-- 读取员工信息
SELECT * FROM employees;
-- 更新部门信息
UPDATE departments SET location = 'New York' WHERE department_id = 1;
-- 释放锁
UNLOCK TABLES;
结果说明:
在持有锁期间,其他事务可以继续读取employees表,但不能对其进行写操作;同时,其他事务无法访问departments表,直到当前事务释放锁。
注意:
- 多个表的锁 可以在同一个LOCK TABLES语句中指定,但必须确保锁的顺序不会导致死锁。
- 锁的兼容性:读锁和写锁之间是不兼容的,确保在设计事务时避免潜在的锁冲突。
2、隐式获取表级锁
除了显式使用LOCK TABLES和UNLOCK TABLES来获取表级锁,MySQL在某些操作中会隐式地获取表级锁。以下是一些常见的隐式获取表级锁的操作:
(1)、ALTER TABLE操作
当你对表结构进行修改时,MySQL会隐式地获取表级锁,以确保在修改过程中没有其他事务对该表进行读取或写入操作。例如,添加或删除列、修改索引等操作都会触发表级锁。
sql:
-- 添加新列
ALTER TABLE employees ADD COLUMN email VARCHAR(100);
-- 删除列
ALTER TABLE employees DROP COLUMN email;
结果说明:
在ALTER TABLE操作执行期间,MySQL会隐式地获取表级锁,阻止其他事务对该表进行任何读取或写入操作,直到操作完成。
注意:
- ALTER TABLE操作通常会锁定整个表,尤其是在MyISAM存储引擎中。对于InnoDB,某些ALTER TABLE操作可能会使用更细粒度的锁,但这取决于具体的MySQL版本和优化设置。
(2)、TRUNCATE TABLE操作
TRUNCATE TABLE用于快速删除表中的所有数据。与DELETE不同,TRUNCATE TABLE会隐式地获取表级锁,并且不会触发触发器或生成回滚日志。
sql:
-- 清空表中的所有数据
TRUNCATE TABLE employees;
结果说明:
在TRUNCATE TABLE操作执行期间,MySQL会隐式地获取表级锁,阻止其他事务对该表进行任何读取或写入操作,直到操作完成。
注意:
- TRUNCATE TABLE操作通常比DELETE更快,因为它不会逐行删除数据,而是直接删除表的数据文件。然而,它也会隐式地获取表级锁,影响并发性能。
(3)、CREATE INDEX操作
当你为表创建索引时,MySQL会隐式地获取表级锁,以确保在创建索引的过程中没有其他事务对该表进行修改。
sql:
-- 创建索引
CREATE INDEX idx_age ON employees (age);
结果说明:
在CREATE INDEX操作执行期间,MySQL会隐式地获取表级锁,阻止其他事务对该表进行任何写入操作,直到索引创建完成。
注意:
- CREATE INDEX操作通常会锁定整个表,尤其是在MyISAM存储引擎中。对于InnoDB,某些版本的MySQL支持在线创建索引(即在创建索引的同时允许其他事务继续访问表),但这取决于具体的MySQL版本和配置。
3、表级锁的注意事项
虽然表级锁可以确保数据的一致性和独占访问,但它也会影响并发性能。以下是使用表级锁时需要注意的几点:
(1)、避免长时间持有锁
- 原因:长时间持有表级锁会导致其他事务等待,甚至引发死锁。表级锁会阻塞其他事务对该表的访问。因此,应尽量避免在事务中长时间持有锁,减少对其他事务的影响。
- 建议:将事务中的操作简化,避免在事务中执行耗时的操作。尽量在最短的时间内完成必要的操作,然后尽快释放锁。如果可能,使用行级锁而不是表级锁,以提高并发性能。
(2)、合理选择锁类型
- 原因:不同的锁类型(读锁和写锁)有不同的影响。读锁允许多个事务并发读取,但阻止写操作;写锁则是排他性的,阻止所有其他事务的访问。
- 建议:根据实际需求选择合适的锁类型。如果只需要读取数据,使用读锁;如果需要修改数据,使用写锁。
(3)、避免死锁
- 原因:当两个或多个事务相互等待对方释放锁时,就会发生死锁。虽然MySQL会自动检测并解决死锁,但频繁的死锁会影响系统性能。
- 建议:尽量按照固定的顺序获取锁,避免循环等待。如果可能发生死锁,捕获死锁错误并在应用程序中重试事务。
4、表级锁总结
MySQL的表级锁机制是确保数据一致性和独占访问的重要工具。通过显式的LOCK TABLES和UNLOCK TABLES语句,你可以手动获取和释放表级锁;而在某些操作(如ALTER TABLE、TRUNCATE TABLE和CREATE INDEX)中,MySQL会隐式地获取表级锁。
- 显式获取表级锁:使用LOCK TABLES和UNLOCK TABLES显式地获取和释放表级锁,适用于需要独占访问整个表的场景。
- 隐式获取表级锁:某些DDL操作(如ALTER TABLE、TRUNCATE TABLE和CREATE INDEX)会隐式地获取表级锁,确保在操作期间没有其他事务对该表进行访问。
- 注意事项:尽量缩短锁的持续时间,避免长时间持有锁,合理选择锁类型,并避免死锁。
(3)、意向锁(Intention Locks)
意向锁(Intention Locks)是MySQL中用于协调行级锁和表级锁之间冲突的一种机制。它本身并不直接锁定数据行或表,而是表示事务打算对表中的某些行加锁。通过意向锁,MySQL 可以更高效地管理锁的兼容性和冲突检测。
1、什么是意向锁?
- 意向锁是一种元锁(meta-lock),它不直接锁定数据行或表,而是表示事务打算对表中的某些行加锁。
- 意向锁的作用是告诉其他事务:当前事务打算对表中的某些行加锁,因此其他事务在考虑对该表进行表级操作时需要注意。
- 意向锁主要用于协调行级锁和表级锁之间的冲突,确保不会发生意外的锁升级或死锁。
简单理解:
意向锁本身并不是实质意义上的锁,而是一种事务之间协调使用锁的机制。当一个事务使用行锁或表锁时,Mysql会自动为该表添加对应的意向锁。这样其他的事务访问该表时,就能得知该表的当前状态,由Mysql决定其他事务是否能操作表。
2、意向锁的类型
MySQL支持两种主要的意向锁:
- 意向共享锁(Intention Shared Lock, IS):
- 表示事务打算对表中的某些行加共享锁(S锁)。
- 它允许其他事务对该表进行读取操作,但阻止其他事务对该表加表级排他锁(X锁)。
- 意向排他锁(Intention Exclusive Lock, IX):
- 表示事务打算对表中的某些行加排他锁(X锁)。
- 它允许其他事务对该表进行读取操作,但阻止其他事务对该表加表级排他锁(X锁)或 表级共享锁(S锁)。
3、意向锁的工作原理
当你在事务中使用SELECT … FOR UPDATE或SELECT … LOCK IN SHARE MODE时,MySQL会自动为该表添加相应的意向锁。
具体来说:
- 如果你使用SELECT … FOR UPDATE,MySQL会为表添加意向排他锁(IX锁),并为查询到的每一行加行级排他锁(X锁)。
- 如果你使用SELECT … LOCK IN SHARE MODE,MySQL会为表添加意向共享锁(IS锁),并为查询到的每一行加行级共享锁(S锁)。
关键点:
- 意向锁是一种元锁,它不直接锁定数据行或表,而是表示事务打算对表中的某些行加锁。
- 意向锁的存在是为了协调行级锁和表级锁之间的冲突,确保不会发生意外的锁升级或死锁。
4、意向锁与行级锁的结合示例
sql:
START TRANSACTION;
SELECT * FROM employees WHERE id = 1 FOR UPDATE; -- 获取意向排他锁 + 行级排他锁
COMMIT;
解释:
- 获取行级排他锁:
当你执行SELECT * FROM employees WHERE id = 1 FOR UPDATE时,MySQL会为查询到的行(即id = 1的行)加行级排他锁(X锁)。这意味着其他事务在这段时间内不能对该行进行读取或写操作。 - 同时获取意向排他锁:
当你执行SELECT * FROM employees WHERE id = 1 FOR UPDATE行时,除了获取行级排他锁,MySQL还会为整个employees表加意向排他锁(IX锁)。这表示当前事务打算对表中的某些行加排他锁,其他事务在考虑对该表进行表级操作时需要注意。
具体执行:
- 意向排他锁(IX锁)并不直接锁定表中的任何行,但它告诉其他事务:当前事务打算对表中的某些行加排他锁。因此,其他事务不能对该表加表级排他锁(X锁)或表级共享锁(S锁)。
- 行级排他锁(X锁)直接锁定id = 1的行,确保其他事务在这段时间内不能对该行进行任何操作。
5、意向锁的作用
(1)、协调行级锁和表级锁
意向锁确保了行级锁和表级锁之间的兼容性。例如,如果一个事务已经持有意向排他锁(IX锁),其他事务就不能对该表加表级排他锁(X锁)或表级共享锁(S锁),从而避免了潜在的锁冲突。
(2)、防止锁升级
意向锁有助于防止不必要的锁升级。如果没有意向锁,MySQL可能在某些情况下将行级锁 升级为表级锁,这会导致性能下降。通过意向锁,MySQL可以更细粒度地管理锁,避免不必要的锁升级。
(3)、提高并发性能
意向锁允许多个事务同时对同一张表的不同行加锁,而不会相互阻塞。例如,两个事务可以分别对id = 1和id = 2的行加行级排他锁(X锁),而不会相互影响,因为它们都只持有意向排他锁(IX锁),而不是表级排他锁(X锁)。
6、意向锁与表级锁的冲突示例
假设我们有两个事务T1和T2:
-- 事务T1:获取意向排他锁 + 行级排他锁
START TRANSACTION;
SELECT * FROM employees WHERE id = 1 FOR UPDATE; -- 获取意向排他锁(IX锁)+ 行级排他锁(X锁)
COMMIT;
-- 事务T2:尝试获取表级排他锁
START TRANSACTION;
LOCK TABLE employees WRITE; -- 被阻塞,直到T1释放意向排他锁(IX锁)
UNLOCK TABLES;
COMMIT;
结果说明:
T2在T1 持有意向排他锁(IX锁)期间,无法获取表级排他锁(X锁),因此T2的LOCK TABLE 操作会被阻塞,直到T1提交或回滚事务并释放 意向排他锁(IX锁)。
7、意向锁总结
- 意向锁是一种元锁,它不直接锁定数据行或表,而是表示事务打算对表中的某些行加锁。
- 意向锁的作用是协调行级锁和表级锁之间的冲突,确保不会发生意外的锁升级或死锁。
- 意向排他锁(IX锁)表示事务打算对表中的某些行加排他锁(X锁),而意向共享锁(IS锁)表示事务打算对表中的某些行加共享锁(S锁)。
- 意向锁的存在使得MySQL可以更高效地管理锁的兼容性和冲突检测,避免不必要的锁升级和死锁。
(4)、共享锁(Shared Locks, S锁)
允许多个事务同时读取同一行数据,但不允许其他事务对该行进行写操作。
获取方式:
SELECT * FROM table_name WHERE id = 1 LOCK IN SHARE MODE;
示例:共享锁
假设我们有两个事务T1和T2,它们都想要读取同一个员工的信息,但不希望其他事务在此期间修改该员工的数据。
sql:
-- 事务T1:获取共享锁
START TRANSACTION;
SELECT * FROM employees WHERE id = 1 LOCK IN SHARE MODE; -- 获取共享锁
-- 读取员工信息
SELECT name, age FROM employees WHERE id = 1;
COMMIT;
-- 事务T2:获取共享锁
START TRANSACTION;
SELECT * FROM employees WHERE id = 1 LOCK IN SHARE MODE; -- 获取共享锁
-- 读取员工信息
SELECT name, age FROM employees WHERE id = 1;
COMMIT;
结果说明:
T1和T2可以同时持有共享锁,读取同一行数据。然而,如果有第三个事务尝试对该行加排他锁(例如进行更新操作),它将被阻塞,直到所有共享锁被释放。
(5)、排他锁(Exclusive Locks, X锁)
阻止其他事务对该行进行读取或写操作,确保只有当前事务可以修改该行。
获取方式:
SELECT * FROM table_name WHERE id = 1 FOR UPDATE;
示例:排他锁
假设我们有两个事务T1和T2,T1想要更新某个员工的年龄,而T2想要读取该员工的信息。
sql:
-- 事务T1:获取排他锁并更新员工年龄
START TRANSACTION;
SELECT * FROM employees WHERE id = 1 FOR UPDATE; -- 获取排他锁
UPDATE employees SET age = 30 WHERE id = 1;
COMMIT;
-- 事务T2:尝试获取共享锁
START TRANSACTION;
SELECT * FROM employees WHERE id = 1 LOCK IN SHARE MODE; -- 被阻塞,直到 T1 释放排他锁
COMMIT;
结果说明:
T2在T1持有排他锁期间,无法获取共享锁或排他锁,因此T2的查询会被阻塞,直到T1提交或回滚事务并释放锁。
(6)、锁的冲突规则
- S锁与S锁:相容,多个事务可以同时持有同一行的共享锁。
- S锁与X锁:不相容,持有共享锁的事务会阻止其他事务获取排他锁。
- X锁与X锁:不相容,只有一个事务可以持有某一行的排他锁。
简单记忆:
排他锁X和谁都不兼容。
(7)、事务的死锁处理
当两个或多个事务相互等待对方释放锁时,就会发生死锁。MySQL会自动检测死锁,并选择其中一个事务进行回滚,以解除死锁。
死锁检测:
MySQL使用死锁检测算法来识别死锁。当检测到死锁时,MySQL会选择一个代价最小的事务进行回滚。
死锁回滚:
被选中的事务将被回滚,释放其持有的所有锁,从而使其他事务可以继续执行。
死锁预防:
虽然无法完全避免死锁,但可以通过以下方法减少死锁的发生:
- 尽量缩短事务的持续时间。
- 尽量减少事务中的锁数量。
- 按照固定的顺序获取锁,避免循环等待。
处理死锁的建议:
- 捕获死锁错误:在应用程序中捕获死锁错误(通常为错误码1213),并在捕获后重试事务。
- 优化事务逻辑:尽量减少事务的复杂度,避免长时间持有锁。
- 使用适当的隔离级别:选择合适的隔离级别,既能保证数据一致性,又能提高并发性能。
(8)、锁的调试与监控
为了更好地理解和优化表级锁的使用,MySQL提供了一些工具和命令来监控和调试锁的状态。
1、SHOW OPEN TABLES
这个命令可以显示当前打开的表及其锁状态。你可以通过查询information_schema数据库中的TABLES表来查看哪些表被锁定。
sql:
SHOW OPEN TABLES WHERE In_use > 0;
示例:
先获取employees表的读锁。
LOCK TABLES employees READ;
查看锁定的表:
SHOW OPEN TABLES WHERE In_use > 0;
释放表锁:
UNLOCK TABLES;
再次查询锁定的表:
SHOW OPEN TABLES WHERE In_use > 0;
解释:
该命令会返回当前被锁定的表及其锁状态。In_use列表示有多少线程正在使用该表。
2、performance_schema
MySQL的performance_schema提供了对锁的详细监控功能。你可以通过查询 performance_schema中的相关表来获取表级锁的使用情况。
相关的内置表:
- data_locks:显示当前持有的锁。
- data_lock_waits:显示当前的锁等待情况。
- innodb_lock_waits:显示InnoDB的锁等待情况。
示例:查询当前的表级锁等待情况
SELECT * FROM performance_schema.data_lock_waits;
结果说明:
该查询会返回当前正在等待表级锁的事务及其相关信息,帮助你分析锁的竞争情况。
3、SHOW ENGINE INNODB STATUS
这个命令可以显示InnoDB存储引擎的当前状态,包括锁的等待队列、死锁检测等信息。
sql:
SHOW ENGINE INNODB STATUS;
输出内容:
该命令会输出详细的InnoDB状态信息,包括锁的等待队列、最近发生的死锁、事务的状态等。
运行结果:
四、事务的存储引擎支持
并不是所有的MySQL存储引擎都支持事务。常见的存储引擎及其事务支持情况如下:
- InnoDB:支持事务,提供完整的ACID特性。它是MySQL的默认存储引擎,广泛用于需要事务支持的场景。
- MyISAM:不支持事务,仅支持表级锁。适用于读多写少的场景,如日志记录、报表生成等。
- MEMORY:不支持事务,数据存储在内存中,适用于临时数据的快速查询。
- NDB Cluster:支持事务,适用于分布式数据库环境。
五、事务的性能优化
虽然事务提供了强大的数据一致性和完整性保证,但不当使用事务可能会对性能产生负面影响。以下是一些优化事务性能的建议:
- 尽量缩短事务的持续时间:事务越长,锁定的资源越多,导致其他事务等待的时间越长。尽量将事务中的操作简化,减少不必要的查询和更新。
- 批量处理数据:如果需要对大量数据进行插入、更新或删除操作,尽量使用批量操作(如 INSERT … VALUES (…)或UPDATE … WHERE IN (…)),而不是逐条执行。
- 合理使用索引:确保查询和更新操作使用了适当的索引,以减少扫描的行数,提高查询效率。
- 避免长时间持有锁:尽量减少事务中的锁数量,避免长时间持有锁,以提高并发性能。
- 选择合适的隔离级别:根据应用的需求选择合适的隔离级别,既能保证数据一致性,又能提高并发性能。
六、多版本并发控制 (MVCC)
MVCC(Multi-Version Concurrency Control,多版本并发控制)是一种用于数据库管理系统的技术,旨在提高并发性能的同时保持事务的隔离性。通过MVCC,多个事务可以同时读取和写入数据,而不会相互阻塞,从而减少了锁的竞争,提高了系统的吞吐量。
在MySQL的InnoDB存储引擎中,MVCC是实现可重复读(Repeatable Read)和读已提交(Read Committed)隔离级别的核心技术。它允许不同事务看到不同的数据版本,确保事务之间的隔离性,同时避免了长时间持有锁带来的性能问题。
1、数据行的多个版本
在MVCC中,每个数据行可以有多个版本。当一个事务对某一行进行更新或删除时,不会立即修改该行的现有版本,而是创建一个新的版本。旧版本的数据仍然保留,直到没有事务需要访问它为止。
具体实现如:
- 插入操作:为表中添加新行时,直接插入新版本。
- 更新操作:更新某一行时,不会覆盖原有版本,而是创建一个新版本,并将旧版本标记为历史版本。
- 删除操作:删除某一行时,不会立即物理删除该行,而是将其标记为“已删除”,并创建一个新版本表示删除状态。
2、隐藏列:事务ID和回滚指针
为了支持多版本并发控制,InnoDB在每行数据中添加了两个隐藏列:
- DB_TRX_ID:记录最后一次对该行进行插入或更新的事务ID。每次事务对行进行修改时,都会更新这个字段。
- DB_ROLL_PTR:指向回滚段(rollback segment),存储了该行的历史版本信息。通过回滚指针,可以追溯到该行的旧版本。
这些隐藏列使得InnoDB能够跟踪每个数据行的不同版本,并根据事务的隔离级别决定哪个版本是可见的。
3、MVCC工作模式
快照读 (Snapshot Read)
也称为一致性读(Consistent Read),在可重复读隔离级别下,事务在整个生命周期内看到的是事务开始时的一致性视图。即使其他事务在此期间修改了数据,当前事务也不会看到这些更改。
快照读会在事务开始时生成一个一致的快照,事务在读取数据时只会看到该快照中的数据版本,而不会受到其他事务的影响。
在可重复读和读已提交的隔离级别下,事务执行的快照读操作不会加锁。
当前读(Current Read)
某些操作(如SELECT … FOR UPDATE、SELECT … LOCK IN SHARE MODE、INSERT、UPDATE、DELETE)会执行当前读,即读取最新的数据版本,并加锁以防止其他事务修改同一行。
4、垃圾回收(Purge)
随着事务的不断执行,数据库中会积累大量的旧版本数据。为了释放空间并保持数据库的高效运行,InnoDB会定期执行垃圾回收操作,删除不再需要的历史版本。
垃圾回收的条件是:
当所有活跃事务都不再需要某个旧版本时,该版本可以被安全地删除。
5、MVCC的工作原理
假设我们有两个事务T1和T2,它们同时对同一行数据进行操作。
(1)、可重复读隔离级别下的 MVCC
1、T1开始:
- T1执行START TRANSACTION;,事务ID为100。
- T1执行SELECT * FROM table WHERE id = 1;,读取到行id = 1, value = 'old',此时该行的DB_TRX_ID为99(表示该行是由事务99插入的)。
2、T2开始:
- T2执行START TRANSACTION;,事务ID为101。
- T2执行UPDATE table SET value = 'new' WHERE id = 1;,创建了一个新的版本id = 1, value = 'new',并将DB_TRX_ID设置为101。
3、T1再次读取:
- T1再次执行SELECT * FROM table WHERE id = 1;,仍然读取到id = 1, value = 'old',因为T1的快照是在事务开始时生成的,它看不到T2创建的新版本,读取数据任然是DB_TRX_ID为99的行。
4、T2提交:
- T2执行COMMIT;,新版本id = 1, value = 'new'成为最新版本。
5、T1结束:
- T1执行COMMIT;,事务结束。从现在起,之后所有新的事务都将看到id = 1, value = 'new'。
(2)、读已提交隔离级别下的 MVCC
1、T1开始:
- T1执行START TRANSACTION;,事务ID为100。
- T1执行SELECT * FROM table WHERE id = 1;,读取到行id = 1, value = 'old',此时该行的DB_TRX_ID为99。
2、T2开始:
- T2执行START TRANSACTION;,事务ID为101。
- T2执行UPDATE table SET value = 'new' WHERE id = 1;,创建了一个新的版本id = 1, value = 'new',并将DB_TRX_ID设置为101。
3、T1再次读取:
- T1再次执行SELECT * FROM table WHERE id = 1;,仍然读取到 id = 1, value = 'old',因为在读已提交隔离级别下,T1只能看到已经提交的事务所做的更改,而T2尚未提交。
4、T2提交:
- T2执行COMMIT;,新版本id = 1, value = 'new'成为最新版本。
5、T1再次读取:
- T1再次执行SELECT * FROM table WHERE id = 1;,这次读取到id = 1, value = 'new',因为T2已经提交,T1现在可以看到T2的更改(即当前读模式)。
6、T1结束:
- T1执行COMMIT;,事务结束。
6、MVCC的优点
- 提高并发性能:通过允许多个事务同时读取和写入数据,减少了锁的竞争,提高了系统的吞吐量。
- 减少锁冲突:快照读操作不需要加锁,避免了读写冲突,使得多个事务可以并行执行。
- 保证数据一致性:事务在读取数据时,始终看到的是事务开始时的一致性视图,确保了数据的隔离性和一致性。
- 简化应用程序开发:开发者不需要频繁使用显式锁来保护数据,减少了复杂性。
7、MVCC的局限性
- 额外的存储开销:由于每个数据行可能有多个版本,数据库需要额外的空间来存储历史版本。随着事务的增多,可能会导致磁盘空间的增加。
- 垃圾回收的开销:虽然垃圾回收机制可以清理不再需要的历史版本,但频繁的垃圾回收操作可能会对性能产生一定的影响。
- 幻读问题:在可重复读隔离级别下,MVCC无法完全解决幻读问题。幻读是指在同一事务中,两次查询返回的行数不同,通常是由于其他事务插入或删除了数据。
8、MVCC与不同隔离级别的关系
- 读未提交(Read Uncommitted):不使用MVCC,允许脏读,事务可以看到未提交的数据。
- 读已提交(Read Committed):使用MVCC,事务只能看到已经提交的数据,避免了脏读。每次读取时,事务都会获取最新的提交版本。
- 可重复读(Repeatable Read):使用MVCC,事务在整个生命周期内看到的是事务开始时的一致性视图,避免了脏读和不可重复读。然而,幻读仍然可能发生。
- 可序列化(Serializable):不依赖MVCC,通过严格的锁定机制确保事务按顺序执行,避免了所有并发问题,但并发性能较低。
9、MVCC总结
MVCC(多版本并发控制)是MySQL InnoDB存储引擎中实现高并发和事务隔离的核心技术。通过为每个数据行维护多个版本,并结合快照读和当前读机制,MVCC允许多个事务同时读取和写入数据,而不会相互阻塞。这不仅提高了系统的并发性能,还确保了数据的一致性和隔离性。
七、ACID的实现原理
事务的ACID特性是数据库系统中确保数据一致性和可靠性的核心原则。每个特性都对应了数据库在处理事务时必须满足的特定要求。下面介绍下MySQL(特别是InnoDB存储引擎)是如何实现这些特性的。
1、原子性~实现原理
原子性确保一个事务中的所有操作要么全部成功,要么全部失败。如果事务中的任何一个操作失败,整个事务将被回滚,恢复到事务开始之前的状态。
实现方式:
日志记录(Redo Log,Undo Log) + 二阶段提交。
(1)、日志记录(Redo Log和Undo Log)
InnoDB使用重做日志(Redo Log)和撤销日志(Undo Log)来实现原子性。
- Redo Log:用于记录事务对数据页的修改。即使在事务提交之前,数据页的修改已经被写入内存或磁盘,但只有当Redo Log被持久化后,事务才算真正提交。如果系统崩溃,可以通过Redo Log恢复未完成的事务。
- Undo Log:用于记录事务的修改前的旧值。如果事务失败或被回滚,可以通过Undo Log恢复到事务开始之前的状态。
(2)、两阶段提交(Two-Phase Commit, 2PC)
InnoDB在提交事务时使用两阶段提交协议,确保事务的原子性。具体步骤如下:
1、准备阶段:事务的所有修改都被写入Redo Log和Undo Log,但尚未提交。此时,事务处于“准备”状态。
2、提交阶段:如果所有操作都成功,事务会被标记为已提交,Redo Log被持久化,事务正式生效。如果任何操作失败,事务会被回滚,使用Undo Log恢复到事务开始之前的状态。
示例:
START TRANSACTION;
-- 修改数据
UPDATE employees SET salary = 5000 WHERE id = 1;
-- 如果这里发生错误,事务将被回滚
INSERT INTO departments (department_id, name) VALUES (1, 'HR');
COMMIT;
说明:
如果INSERT操作失败,整个事务将被回滚,salary的修改也会被撤销。
2、一致性~实现原理
一致性确保事务执行前后,数据库的状态始终满足所有的约束条件和规则(如外键约束、唯一性约束等)。事务不会破坏数据库的完整性。
实现方式:
约束检查:在事务执行过程中,MySQL会自动检查并强制执行各种约束条件,如主键、外键、唯一性、非空性等。如果事务违反了这些约束,事务将被回滚,确保数据库的一致性。
MVCC(多版本并发控制):InnoDB使用多版本并发控制(MVCC)来实现一致性。MVCC 允许多个事务同时读取和写入数据,而不会相互干扰。通过为每个事务提供数据的不同版本,确保事务看到的数据是一致的。
示例:
-- 插入一条违反外键约束的数据
INSERT INTO employees (id, department_id, name) VALUES (1, 999, 'John Doe');
说明:
如果department_id = 999不存在于departments表中,插入操作将失败,事务将被回滚,确保数据库的一致性。
3、隔离性~实现原理
隔离性确保多个并发事务之间的操作相互隔离,避免相互干扰。不同事务之间不能看到彼此未提交的修改,除非它们显式地依赖于其他事务的结果。
实现方式:
(1)、隔离级别
MySQL提供了四种不同的隔离级别,每个级别决定了事务之间可见性的程度。隔离级别越高,事务之间的隔离性越强,但也可能会影响并发性能。
- 读未提交(Read Uncommitted):事务可以看到其他事务未提交的修改。这是最低的隔离级别,容易引发脏读、不可重复读和幻读问题。
- 读已提交(Read Committed):事务只能看到其他事务已经提交的修改。这可以防止脏读,但仍然可能出现不可重复读和幻读。
- 可重复读(Repeatable Read):事务在整个事务期间看到的数据是一致的,即事务开始后,其他事务的修改对当前事务不可见。这是MySQL的默认隔离级别,可以防止脏读和不可重复读,但仍然可能出现幻读。
- 可序列化(Serializable):这是最高的隔离级别,事务完全隔离,不允许任何并发操作。它通过加锁的方式确保事务的顺序执行,防止所有类型的并发问题(脏读、不可重复读和幻读)。
(2)、MVCC(多版本并发控制)
InnoDB使用MVCC来实现隔离性。MVCC通过为每个事务提供数据的不同版本,确保事务在读取数据时不会受到其他事务的影响。具体来说:
- 快照读(Snapshot Read):事务读取的是事务开始时的数据快照,而不是最新的数据。这意味着事务可以看到的数据是它开始时的状态,而不受其他事务的修改影响。
- 当前读(Current Read):某些操作(如SELECT … FOR UPDATE或SELECT … LOCK IN SHARE MODE)会获取最新的数据,并加锁以确保其他事务不能修改这些数据。
示例:
-- 事务T1:读取员工信息
START TRANSACTION;
SELECT * FROM employees WHERE id = 1; -- 读取的是事务开始时的数据快照
-- 事务T2:更新员工信息
START TRANSACTION;
UPDATE employees SET salary = 6000 WHERE id = 1;
COMMIT;
-- 事务T1:再次读取员工信息
SELECT * FROM employees WHERE id = 1; -- 仍然读取的是事务开始时的数据快照,看不到 T2的修改
结果说明:
因为Mysql默认的事务隔离级别为可重复读。在这种隔离级别下,T1在整个事务期间看到的数据是一致的,不会受到T2的修改影响。这是因为T1使用了MVCC数据快照。
4、持久性~实现原理
持久性确保一旦事务提交成功,其修改将永久保存在数据库中,即使系统发生故障也不会丢失。
实现方式:
(1)、Redo Log(重做日志):InnoDB使用Redo Log来确保事务的持久性。当事务提交时,Redo Log会被持久化到磁盘。即使系统崩溃,MySQL可以通过Redo Log恢复未完成的事务,确保数据不会丢失。
(2)、双写缓冲区(Doublewrite Buffer):为了防止部分页面写入失败导致数据损坏,InnoDB使用双写缓冲区。在将数据页写入磁盘之前,先将数据页的副本写入双写缓冲区。如果系统崩溃,MySQL可以通过双写缓冲区恢复损坏的数据页。
(3)、同步机制:MySQL提供了多种同步机制,确保Redo Log和数据页能够及时持久化到磁盘。例如,innodb_flush_log_at_trx_commit参数可以控制Redo Log的同步频率:
- = 0:每秒同步一次Redo Log,性能较好,但可能存在最多1秒的数据丢失。
- = 1(默认):每次事务提交时同步Redo Log,确保数据的持久性,但性能稍差。
- = 2:每次事务提交时将Redo Log写入操作系统缓存,但不立即同步到磁盘,性能较好,但可能存在数据丢失的风险。
sql:
START TRANSACTION;
-- 修改数据
UPDATE employees SET salary = 7000 WHERE id = 1;
COMMIT;
解释:
一旦事务提交成功,Redo Log会被持久化到磁盘,确保即使系统崩溃,数据也不会丢失。
5、ACID实现原理总结
- 原子性(Atomicity):通过Redo Log和Undo Log实现,确保事务中的所有操作要么全部成功,要么全部失败。
- 一致性(Consistency):通过约束检查和MVCC实现,确保事务执行前后数据库的状态始终满足所有的约束条件。
- 隔离性(Isolation):通过隔离级别和MVCC实现,确保多个并发事务之间的操作相互隔离,避免相互干扰。
- 持久性(Durability):通过Redo Log和双写缓冲区实现,确保事务提交后的修改永久保存在数据库中,即使系统发生故障也不会丢失。
乘风破浪会有时,直挂云帆济沧海!!!