一、简介

事务隔离是数据库管理的基本概念之一,它是ACID属性中的I,规定了如何处理并发事务。

事务隔离级别是为了解决多个事务并发执行时可能出现的问题而设定的不同级别的隔离机制,它决定了事务在数据库系统中执行时的独立性和并发性。

根据SQL标准,事务隔离级别由低到高可以分为以下四种:

  • Read Uncommitted(读未提交)

    读未提交是指一个事务可以读取其他事务尚未提交的数据,这可能导致脏读、不可重复读和幻读等并发问题。

    脏读(Dirty Reads):当一个事务可以读取另一个事务已更新但未提交的数据时,就会发生脏读。

    不可重复读(Non-repeatable Reads):当同一个查询在一个事务中产生不同的结果时,就会发生不可重复读。

    幻读(Phantom Reads):主要发生在涉及范围查询的场合:当某个事务读取某个范围内的记录时,另一个并发事务插入或删除了满足这个范围的记录。

  • Read Committed(读已提交)

    读已提交是指一个事务只能读取已经提交的数据,这可以避免脏读问题,但仍然可能存在不可重复读和幻读问题。

  • Repeatable Read(可重复读)

    可重复读是指一个事务在执行过程中多次读取相同的数据时,能够保证读取的数据不会被其他事务修改。

  • Serializable(串行化)

    此级别下的事务会按顺序一个接一个地执行,不允许并发执行;这可以避免脏读、不可重复读和幻读等并发问题,但会降低系统的并发性能。

隔离级别 脏读 不可重复读 幻读
读未提交
读已提交 x
可重复读 x x
串行化 x x x

二、隔离级别

样例中使用的数据表:

create table account(
	id int auto_increment primary key,
	name varchar(10),
	balance int
);

表数据:

id name balance
1 Tom 1000

设置隔离级别:

SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE]

查看隔离级别:

SELECT @@tx_isolation;

1、读未提交

读未提交是并发事务中提供的最低的隔离级别,所有的读都以非锁定的方式发生。使用此隔离级别时,事务可以从其他事务读取未提交的数据,从而导致脏读。

  • 样例

创建两个会话,设置隔离级别为:READ-UNCOMMITTED,并开启事务:

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

start transaction;

会话一在事务中更新余额balance字段但不提交:

update account set balance = balance + 100 where id = 1;

之后在会话二中查询余额:

select * from account where id = 1;

读未提交

会话二读取余额为1100而不是1000;如果会话一回滚,并且会话二已经在其自己的应用程序线程中使用了金额的值1100,则发生脏读的场景。

2、读已提交

使用此隔离级别,事务仍然能够访问其他事务的更新数据;但只能访问已提交的数据,可以避免脏读的情况。

  • 样例

创建两个会话,设置隔离级别为:READ-COMMITTED,并开启事务:

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

start transaction;

会话一在事务中更新余额balance字段但不提交:

update account set balance = balance + 100 where id = 1;

select * from account where id = 1;

之后在会话二中查询余额:

select * from account where id = 1;

读已提交

会话二中读取到的余额为1000,可以看到避免了脏读;但在此隔离级别下仍然会发生不可重复读:

会话一在事务中更新余额后提交:

commit;

读已提交

会话二的查询中出现了不可重复读;这是因为在Read Committed隔离级别下,InnoDB会在最后一次DML操作之后创建并读取新快照。

3、可重复读

可重复读(Repeatable Read)是MySQL InnoDB引擎的默认隔离级别,此级别通过建立和使用在事务开始时创建的快照来解决不可重复读的问题。因此,在同一事务中的查询会产生相同的值。

  • 样例一

创建两个会话,隔离级别为默认级别,并开启事务:

SELECT @@tx_isolation;

start transaction;

会话一更新并提交事务:

update account set balance = balance + 100 where id = 1;

commit;

select * from account where id = 1;

在会话二查询:

select * from account where id = 1;

可重复读

可以看出,避免了不可重复读的问题。但仍不能解决幻读问题。

  • 样例二

创建两个会话,隔离级别为默认级别,并开启事务:

SELECT @@tx_isolation;

start transaction;

会话一插入一条新记录并提交:

insert into account(name, balance) values('Jack', 2000);

select * from account;

commit;

会话二查询所有记录:

select * from account;

可重复读

由于在可重复读隔离级别中,使用了在事务开始时建立的隔离级别快照,因此会话二仍然没有查到新记录。

在会话二中更新新插入的记录:

update account set balance = balance + 100 where id = 2;

select * from accout;

可重复读

可以看出,虽然建立的快照没有任何新插入的记录,但会话二仍然能够更新和读取此记录。

4、串行化

串行化(Serializable)是并发事务之间的最高隔离级别;如果启用了autocommit,它的行为很像Repeatable Read;否则,所有的读取都以锁定方式执行。

  • 样例

创建两个会话,隔离级别设置为:SERIALIZABLE,并开启事务:

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

start transaction;

在会话一中查询具有某些条件的记录:

select * from account where id = 1;

临时调整会话二的锁超时时长为5秒:

SET SESSION innodb_lock_wait_timeout = 5;

在会话二中更新相同条件的记录:

update account set balance = balance + 100 where id = 1;

串行化

可以看出,由于操作都是以锁定方式执行的,因此会话二中的更新操作需要等待会话一的事务完成。

此隔离级别是最严格的,避免了并发事务中的所有异常情况。在实际中会根据对性能和可靠性的需求来设置隔离级别,很大程度上取决于应用程序使用的场景以及在性能和可靠性之间所需的平衡。

参考资料: