浅谈数据库事务及四种隔离级别
数据库事务
例如,银行的转账系统,储户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,持久性,只要事务提交完成,数据就被数据库持久化保存
使用事务
隐式事务
默认地,单条语句就被作为一个事务执行,这称为隐式事务
显式事务
若要执行显式事务
,可以使用以下命令
- 开始事务:BEGIN或START 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完成,或超时。这是最严格的隔离级别,但也会对性能造成很大影响,因此在实际开发中应当根据需求场景选择合适的隔离级别。