【MySQL】04. 事务管理(一):ACID与隔离级别
在多用户数据库环境中,事务的ACID属性与隔离级别是确保数据一致性和操作安全性的关键。本文通过介绍事务的原子性、一致性、隔离性和持久性,以及常见的并发问题如脏读、不可重复读和幻读,结合MySQL四种隔离级别(读未提交、读已提交、可重复读、串行化)的实操演示,旨在帮助读者理解和掌握如何在不同的业务场景下,有效利用事务隔离机制,构建稳定可靠的数据库应用。
1 事务ACID属性
事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个属性,通常简称为事务的ACID属性。
- 原子性(Atomicity) :事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
- 一致性(Consistent) :在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性。
- 隔离性(Isolation) :数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。
- 持久性(Durable) :事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。
2 事务并发问题
数据库一般都会并发执行多个事务,多个事务对相同的一批数据进行增删改查操作,就容易导致常说的脏读、不可重复读、幻读这些问题。
- 脏读:事务A读取到了事务B已经修改但尚未提交的数据,还在这个数据基础上做了操作。此时,如果B事务回滚,A读取的数据无效,不符合一致性要求。
- 不可重复读:事务A内部的相同查询语句在不同时刻读出的结果不一致。
- 幻读:侧重的方面是某一次的 select 操作得到的结果无法支撑后续的业务操作。比如:select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读。
注意,不可重复读和幻读虽然都涉及事务多次查询结果不一致的问题,但它们侧重点不同:
- 不可重复读:指的是在一个事务中,两次查询同一数据,结果却不一样,这通常是因为在两次查询之间,有其他事务修改了这部分数据。不可重复读强调的是对相同数据记录的读取结果不一致。
- 幻读:指的是在一个事务中,对同一范围数据的两次查询,第二次查询出现了第一次查询时不存在的新记录。幻读关注的是数据集的变化,即数据集合中元素数量的增加。
3 事务隔离级别
“脏读”、“不可重复读”和“幻读”,其实都是数据库读一致性问题,为了解决多事务并发问题,数据库设计了读未提交、读已提交、可重复读和串行化4种事务隔离机制,不同事务隔离级别特点如下:
隔离级别 | 脏读 | 不可重复读 | 幻读 | 作用 | 实现原理 |
---|---|---|---|---|---|
读未提交 | 可能 | 可能 | 可能 | 允许读取未提交的数据 | |
读已提交 | 不可能 | 可能 | 可能 | 事务只能读取已提交的数据 | MVCC |
可重复读 | 不可能 | 不可能 | 可能 | 一个事务内,多次读取同一数据时结果一致 | MVCC |
可串行化 | 不可能 | 不可能 | 不可能 | 强制事务串行执行 | 锁机制 |
Mysql默认的事务隔离级别是可重复读,通过下面sql可查看当前数据库的事务隔离级别:
-- MySQL5.7
show variables like 'tx_isolation';
-- MySQL8.0
show variables like 'transaction_isolation';
设置事务隔离级别:
-- MySQL5.7
set tx_isolation='REPEATABLE-READ';
-- MySQL8.0
set transaction_isolation='REPEATABLE-READ';
接下来,以下面的数据分析不同隔离级别下的事务处理情况
CREATE TABLE account (
id int not null AUTO_INCREMENT,
name VARCHAR(255) DEFAULT NULL,
balance INT DEFAULT NULL,
PRIMARY KEY (id)
);
INSERT INTO `test`.`account` (`name`, `balance`) VALUES ('lilei', '450');
INSERT INTO `test`.`account` (`name`, `balance`) VALUES ('hanmei', '16000');
INSERT INTO `test`.`account` (`name`, `balance`) VALUES ('lucy', '2400');
3.1 读未提交
(1)客户端A:设置当前事务模式为read-uncommitted
(读未提交),开启事务,查询表account
的初始记录。
set transaction_isolation='read-uncommitted';
START TRANSACTION;
select * from account;
(2)客户端B:开启事务,更新表account
,但是不进行提交。
START TRANSACTION;
update account set balance = balance - 50 where id = 1;
select * from account;
(3)客户端A:对account
表进行查询。
select * from account;
可以看到,客户端A能够查询到客户端B事务中还未提交的数据。一旦客户端B中的事务因为某种原因回滚,所有的操作都将会被撤销,那客户端A之前查询到的数据其实就是脏数据。
3.2 读已提交
(1)客户端A:设置当前事务模式为read-committed
(读已提交),开启事务,查询表account
的初始记录。
set transaction_isolation='read-committed';
START TRANSACTION;
select * from account;
(2)客户端B:开启事务,更新表account
,但是不进行提交。
START TRANSACTION;
update account set balance = balance - 50 where id = 1;
select * from account;
(3)客户端A:对account
表进行查询,可以看到在read-committed
事务模式下,解决了脏读问题。
select * from account;
(4)客户端B:提交事务。
COMMIT;
(5)客户端A:对account
表进行查询,可以看到产生了不可重复读的问题。
select * from account;
3.3 可重复读
(1)客户端A:设置当前事务模式为repeatable-read
,开启事务,查询表account
的初始记录。
set transaction_isolation='repeatable-read';
START TRANSACTION;
select * from account;
(2)客户端B:开启事务,更新表account
并进行提交。
START TRANSACTION;
update account set balance = balance - 50 where id = 1;
select * from account;
COMMIT;
(3)客户端A:对account
表进行查询,可以看到在repeatable-read
事务模式下,解决了不可重复读的问题。
select * from account;
4)客户端A:执行语句更新表account
。可以看到,balance
字段没有变成400-50=350,因为balance
值用的是步骤2中的350来算的,所以是300,数据的一致性没有被破坏。
update account set balance = balance - 50 where id = 1;
(5)客户端B:插入一条新数据并提交。
START TRANSACTION;
insert into account values(4, 'lily', 700);
COMMIT;
(6)客户端A:查询表account
的所有记录,没有查出新增数据。
select * from account;
(7)客户端A:执行B插入的语句,提示记录已存在,可以看到产生了幻读问题。
insert into account values(4, 'lily', 700);
3.4 可串行化
(1)客户端A:设置当前事务模式为serializable
(可串行化),开启事务,查询表account
的初始记录。
set transaction_isolation='serializable';
START TRANSACTION;
select * from account where id = 1;
(2)客户端B:更新相同的id为1的记录会被阻塞等待超时。
START TRANSACTION;
update account set balance = balance - 50 where id = 1;
(3)客户端B:更新id为2的记录可以成功,说明在串行模式下innodb的查询也会被加上行锁。
update account set balance = balance - 50 where id = 2;
注意:如果客户端A执行的是一个范围查询,那么该范围内的所有行包括每行记录所在的间隙区间范围都会被加锁。此时如果客户端B在该范围内插入数据都会被阻塞,所以就避免了幻读,即对同一范围数据的两次查询,第二次查询出现了第一次查询时不存在的新记录。
4 结语
综上所述,事务隔离级别是数据库设计中不可或缺的一部分,它在保证数据一致性与提升系统并发性能之间架起了一座桥梁。通过对事务的ACID属性的理解,以及对脏读、不可重复读和幻读等并发问题的剖析,我们看到了不同隔离级别如何在实际场景中发挥作用。从读未提交的灵活性到串行化的严格控制,每种隔离级别的选择都需要根据具体的应用需求和场景来权衡,希望本文的探讨能帮助读者在数据库设计和优化时,更加明智地选择适合的事务隔离策略,从而构建出既高效又可靠的数据库系统。