前言
PostgreSQL 支持多用户并发访问,并且保证多个用户同时访问相同的数据时不会造成数据的不一致性。当多个用户同时访问相同的数据时,如果不进行任何隔离控制,可能导致以下问题:
事务并发问题
- 脏读(dirty read),一个事务能够读取其他事务未提交的修改。例如,B 的初始余额为 0;A 向 B 转账 1000 元但没有提交;此时 B 能够看到 A 转过来的 1000 元,并且成功取款 1000 元;然后 A 取消了转账;银行损失了 1000 元。
- 不可重复读(nonrepeatable read),一个事务读取某个记录后,再次读取该记录时数据发生了改变(被其他事务修改并提交)。例如,B 查询初始余额为 1000,取款 1000;同时 A 向 B 转账 1000 元并且提交;B 再次查询发现余额还是 1000 元,以为取款机出错了(当然,通过查询流水记录可以发现真相;数据库的状态仍然是一致的)。
- 幻读(phantom read),一个事务按照某个条件查询一些数据后,再次执行相同查询时结果的数量发生了变化(另一个事务增加或者删除了某些数据并且完成提交)。幻读和非重复读有点类似,都是由于其他事务修改数据导致的结果变化。
- 更新丢失(lost update),第一类:当两个事务更新相同的数据时,第一个事务被提交,然后第二个事务被撤销;那么第一个事务的更新也会被撤销(所有隔离级别都不允许发生这种情况)。第二类:当两个事务同时读取某一记录,然后分别进行修改提交;就会造成先提交的事务的修改丢失。
事务隔离级别
为了解决并发问题,SQL 标准定义了 4 种不同的事务隔离级别(从低到高):
事务的隔离级别从低到高依次为:
- Read Uncommitted(读未提交):最低的隔离级别,实际上就是不隔离,任何事务都可以看到其他事务未提交的修改;该级别可能产生各种并发异常。不过,PostgreSQL 消除了 Read Uncommitted 级别时的脏读,因为它的实现等同于 Read Committed。
- Read Committed(读已提交):一个事务只能看到其他事务已经提交的数据,解决了脏读问题,但是存在不可重复读、幻读和第二类更新丢失问题。这是 PostgreSQL 的默认隔离级别。
- Repeated Read(可重复读):一个事务对于同某个数据的读取结果不变,即使其他事务对该数据进行了修改并提交;不过如果其他事务删除了该记录,则无法再查询到数据(幻读)。SQL 标准中的可重复读可能出现幻读,但是 PostgreSQL 在可重复读级别消除了幻读。
- Serializable(可串行化):最高的隔离级别,事务串行化执行,没有并发。
只有 Serializable
真正实现了事务的完全隔离,但是不支持并发的数据库系统应用场景非常有限。因此,需要对并发性能和隔离性进行平衡,PostgreSQL 的默认隔离级别为 Read Committed
;此时,可以避免脏读,同时拥有不错的并发性能。
使用SHOW命令可以查看当前的隔离级别:
show transaction_isolation;
transaction_isolation|
---------------------+
read committed |
1 row(s) fetched.
如果需要修改当前事务的隔离级别,可以在事务的最开始执行SET TRANSACTION
命令:
begin;
SET TRANSACTION ISOLATION LEVEL { SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED };
LightDB 默认隔离级别(READ COMMITTED)演示
LightDB 默认级别(READ COMMITTED)不会发生脏读
,但是存在不可重复读
、幻读
和更新丢失
问题
事务1 | 事务2 |
---|---|
begin;select balance from accounts where id = 1; – 返回6000.0000 | |
begin;update accounts set balance = balance + 1000 where id = 1; select balance from accounts where id = 1; - 返回7000.0000 | |
select balance from accounts where id = 1; – 仍然返回 6000,没有脏读 | |
commit; – 提交事务 | |
select balance from accounts where id = 1; – 此时返回 7000,出现不可重复读 | |
select * from accounts where id=4; – 返回 UserC | |
begin;delete from accounts where id = 4;commit; – 删除 UserC 并提交事务 | |
select * from accounts where id=4; – 没有结果,出现幻读 | |
select balance from accounts where id = 1; – 此时返回 7000 | |
begin; select balance from accounts where id = 1; – 此时返回 7000 | |
update accounts set balance = 6000 where id = 1; – 更新为 6000 | |
update accounts set balance = 8000 where id = 1; – 等待事务 1 提交 | |
commit; | |
commit; | |
select balance from accounts where id = 1; – 返回 8000,而不是自己修改成的 6000,更新丢失 |
在以上过程中,PostgreSQL 使用了锁加 MVCC(Multiversion Concurrency Control)技术来实现数据的隔离和一致性。MVCC
简单来说,就是保留每次数据修改之前的旧版本,根据隔离级别决定读取哪个版本的数据。这种实现的最大好处就是读操作永远不会阻塞写操作、写操作永远不会阻塞读操作。
如果一个事务已经修改某个数据而且未提交,则另一个事务不允许同时修改该数据(必须等待),写操作一定是相互阻塞的,需要按照顺序执行。