浅谈数据库事务及四种隔离级别

数据库事务

例如,银行的转账系统,储户1给储户2转1000元钱,那么显然需要以下操作:

UPDATE user SET balance = balance - 1000 WHERE id = 1;
UPDATE user SET balance = balance + 1000 WHERE id = 2;

如果此时发生了意外,导致只执行了第一条操作,扣了账户1的钱款,而钱款没有打到2的账户上,显然是不可以的。也就是说,这两个工作要么都做,要么都不做,如果只做了1不做2,则需要回滚到最初的状态(都什么都没有做),而把多个语句整合为一个操作,就叫数据库事务

事务的特性

事务具有ACID特性

  • A:Atomic,原子性,要么都做,要么都不做
  • C:Consistent,一致性,事务前后数据完整性保持一致,如转账,A-1000则必须B+1000
  • I:Isolation,隔离性,事务并发执行时,事务作出的修改要与其他事务隔离
  • D:Duration,持久性,只要事务提交完成,数据就被数据库持久化保存

使用事务

隐式事务

默认地,单条语句就被作为一个事务执行,这称为隐式事务

显式事务

若要执行显式事务,可以使用以下命令

  • 开始事务:BEGINSTART TRANSACTION
  • 提交事务:COMMIT
  • 回滚事务:ROLLBACK

例如,银行转账的操作

START TRANSACTION;
UPDATE user SET balance = balance - 1000 WHERE id = 1;
UPDATE user SET balance = balance + 1000 WHERE id = 2;
COMMIT;

事务的隔离级别

如果多个事务并发执行,可能会导致数据的不一致,进而产生问题(如脏读不可重复读幻读等等),数据库为我们提供了四种隔离级别以避免这些问题。隔离级别越高问题越少,但性能也越差,因此我们应该根据实际需要选择合适的事务隔离级别。

  • Read Uncommitted:A事务未提交的数据,B也可以读取
  • Read Committed:A事务已提交的数据,B才可以读取
  • Repeatable Read:B在整个事务过程中多次读取的数据保持一致(MySQL默认级别)
  • Serializable:串行化执行事务,在事务A执行时,事务B必须等待

四种事务隔离级别的对比

隔离级别性能脏读不可重复读幻读
Read Uncommitted最好
Read Committed低于前者
Repeatable Read低于前者
Serializable低于前者

Read Uncommitted

即使事务A未提交的数据,B也可以读取。如果A修改了某条数据,而B又读取了它,恰好A事务执行失败进行了回滚,则B就读到了错误的数据(脏读

事务A

初始余额为1000,对余额减去1000,此时不提交事务等待B读取,随后回滚

START TRANSACTION;
UPDATE user SET balance = balance - 1000 WHERE id = 1;
-- WAIT B READ
ROLLBACK;

事务B

读取余额

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
SELECT balance FROM user WHERE id = 1;
+----+---------+
| id | balance |
+----+---------+
|  1 |       0 |
+----+---------+

问题

在B读取数据后,A回滚了事务(余额回滚到1000),则B读取的0就是脏数据(脏读),数据库中的数据仍然是1000(因为没有commit)

Read Committed

为了解决脏读,可以使只有A事务提交的数据才能被B读取,但这存在不可重复读的问题

事务A

初始余额为1000,对余额减去1000,等待B读取,随后提交事务,之后B再读取一次

START TRANSACTION;
UPDATE user SET balance = balance - 1000 WHERE id = 1;
-- WAIT B READ
COMMIT;

事务B

在事务A提交数据前后,分别读取两次数据

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT balance FROM user WHERE id = 1;
-- A COMMIT
SELECT balance FROM user WHERE id = 1;
+---------+
| balance |
+---------+
|    1000 |
+---------+
-- A COMMIT
+---------+
| balance |
+---------+
|       0 |
+---------+

问题

可以看到,事务B在事务A提交前后读取的数据是不一致的(不可重复读),假如转账事务的扣款分为两步:检查余额是否足够->扣除余额,此时发生不可重复读就会出现问题:检查时余额足够,但扣款时却发现余额变成了0

Repeatable Read

可重复读,这也是MySQL默认的事务隔离级别。但如果B读取数据后,A又更新了数据,B再次读取得到的依旧是第一次的结果集,并且不能写入破坏数据完整性的记录(幻读)。

事务A

开始A事务,等待B事务开始并查询所有账户,A事务插入一条数据

START TRANSACTION;
-- WAIT B READ
INSERT INTO user(id, balance) VALUE(3, 1000);
COMMIT;

事务B

开始B事务,查询所有账户,等待A事务提交,尝试插入和A相同的数据

START TRANSACTION;
SELECT * FROM user;
-- A COMMIT
INSERT INTO user(id, balance) VALUE(3, 1000);
ERROR 1062 (23000): Duplicate entry '3' for key 'PRIMARY'

问题

也就是说,数据库中已经插入了用户3,但要保证事务的隔离性(允许重复读),只好显示第一次查询的结果集,同时为了保证数据的一致性,只能禁止事务B再插入和事务A冲突的数据。明明没有记录3,却不能插入记录3,这就是幻读

Serializable

串行化事务,事务只能单个串行执行,不存在脏读、不可重复读、幻读的问题,但事务A执行时,事务B必须陷入等待,直到事务A完成,可能会导致严重的性能问题

事务A

事务A开启串行事务,检查所有用户,什么也不做

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;
SELECT * FROM user;

事务B

B事务尝试写入数据

-- WAIT A
START TRANSACTION;
INSERT INTO user(id, balance) VALUE(4, 1000);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

事务B会一直阻塞到事务A完成,或超时。这是最严格的隔离级别,但也会对性能造成很大影响,因此在实际开发中应当根据需求场景选择合适的隔离级别。

Azure99

底层码农,休闲音游玩家,偶尔写写代码

看看这些?

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注