MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

PostgreSQL事务隔离级别详解与实践

2024-07-157.9k 阅读

事务隔离级别的概念

在数据库系统中,多个事务可能会同时并发执行。如果不对这些并发事务进行适当的隔离控制,可能会导致数据不一致等问题。事务隔离级别定义了一个事务与其他并发事务之间的隔离程度,它决定了一个事务在执行过程中能够看到其他并发事务的哪些修改。

PostgreSQL 支持四种事务隔离级别,分别是:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。不同的隔离级别在并发控制的强度和性能之间进行了不同的权衡。

读未提交(Read Uncommitted)

读未提交是最低的隔离级别。在这种隔离级别下,一个事务可以读取到其他事务尚未提交的数据修改。这意味着可能会出现脏读(Dirty Read)的情况。

脏读示例: 假设我们有两个事务 T1T2

事务 T1

BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- 此时尚未提交事务

事务 T2

BEGIN;
-- 在T1未提交时读取数据
SELECT balance FROM accounts WHERE account_id = 1;
-- 可能读取到修改后但未提交的余额
COMMIT;

在上述示例中,如果 T2T1 提交之前读取到了 T1 修改后但未提交的数据,就发生了脏读。PostgreSQL 默认不支持读未提交隔离级别,因为脏读可能导致数据的不一致性,严重影响数据的可靠性。

读已提交(Read Committed)

读已提交是 PostgreSQL 的默认隔离级别。在这种隔离级别下,一个事务只能读取到其他事务已经提交的数据修改。这避免了脏读的问题。

示例代码: 假设有两个事务 T1T2,以及一个 accounts 表,表结构如下:

CREATE TABLE accounts (
    account_id SERIAL PRIMARY KEY,
    balance DECIMAL(10, 2)
);
INSERT INTO accounts (balance) VALUES (1000.00);

事务 T1

BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- 此时T2无法读取到修改后的数据
COMMIT;

事务 T2

BEGIN;
-- 在T1提交前读取,获取的是原始数据
SELECT balance FROM accounts WHERE account_id = 1; 
-- T1提交后再次读取
SELECT balance FROM accounts WHERE account_id = 1; 
COMMIT;

在这个例子中,T2T1 提交之前读取到的是原始的 balance 值。当 T1 提交后,T2 再次读取就能获取到更新后的值。读已提交虽然避免了脏读,但仍然存在不可重复读(Non - Repeatable Read)和幻读(Phantom Read)的问题。

不可重复读和幻读

  1. 不可重复读:在一个事务内多次读取同一数据,在两次读取之间,如果其他事务修改并提交了该数据,那么本事务两次读取到的数据可能不一样。
  2. 幻读:在一个事务内按照某个条件多次查询,在两次查询之间,如果其他事务插入或删除了满足该条件的新数据,那么本事务两次查询到的结果集可能不一样。

可重复读(Repeatable Read)

可重复读隔离级别确保在一个事务内多次读取同一数据时,读取到的数据是一致的,避免了不可重复读的问题。它通过使用多版本并发控制(MVCC)来实现这一点。

示例代码: 假设我们有 products 表,表结构如下:

CREATE TABLE products (
    product_id SERIAL PRIMARY KEY,
    product_name VARCHAR(100),
    price DECIMAL(10, 2)
);
INSERT INTO products (product_name, price) VALUES ('Product A', 100.00);

事务 T1

BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT price FROM products WHERE product_name = 'Product A';
-- 假设此时T2修改了价格并提交
SELECT price FROM products WHERE product_name = 'Product A';
COMMIT;

事务 T2

BEGIN;
UPDATE products SET price = 120.00 WHERE product_name = 'Product A';
COMMIT;

T1 中,由于设置了可重复读隔离级别,即使 T2 修改并提交了 Product A 的价格,T1 两次读取到的价格仍然是最初读取到的 100.00。然而,可重复读并不能完全避免幻读。

幻读示例及解决方案

假设 products 表中有以下数据:

INSERT INTO products (product_name, price) VALUES ('Product B', 200.00);

事务 T1

BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT COUNT(*) FROM products WHERE price > 100;
-- 假设此时T2插入了一条满足条件的数据并提交
SELECT COUNT(*) FROM products WHERE price > 100;
COMMIT;

事务 T2

BEGIN;
INSERT INTO products (product_name, price) VALUES ('Product C', 150.00);
COMMIT;

在上述示例中,T1 在可重复读隔离级别下,两次查询 COUNT(*) 的结果可能不同,这就是幻读。要解决幻读问题,需要使用串行化隔离级别。

串行化(Serializable)

串行化是最高的隔离级别。在串行化隔离级别下,所有的事务都是按照顺序依次执行的,就好像没有并发一样。这完全避免了脏读、不可重复读和幻读的问题。

示例代码: 假设有 orders 表,表结构如下:

CREATE TABLE orders (
    order_id SERIAL PRIMARY KEY,
    order_date TIMESTAMP,
    customer_id INT
);

事务 T1

BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
INSERT INTO orders (order_date, customer_id) VALUES (NOW(), 1);
COMMIT;

事务 T2

BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
INSERT INTO orders (order_date, customer_id) VALUES (NOW(), 2);
COMMIT;

在串行化隔离级别下,T1T2 会依次执行,不会出现并发冲突导致的数据不一致问题。然而,由于所有事务串行执行,并发性能会受到较大影响。

不同隔离级别下的性能比较

  1. 读未提交:虽然理论上性能最高,因为几乎没有并发控制,但由于可能导致严重的数据一致性问题,PostgreSQL 不支持,所以实际中不考虑其性能。
  2. 读已提交:这是 PostgreSQL 的默认隔离级别,在性能和数据一致性之间有较好的平衡。MVCC 的使用使得读操作一般不会阻塞写操作,写操作也不会阻塞读操作,适用于大多数并发场景。
  3. 可重复读:相比读已提交,可重复读需要维护更多的版本信息来确保事务内数据的一致性,因此在并发写操作较多的情况下,性能可能会略低于读已提交。但在一些对数据一致性要求较高且读操作频繁的场景下,可重复读是更好的选择。
  4. 串行化:由于是串行执行事务,在高并发场景下,性能会受到很大影响。每个事务都需要等待前一个事务完成才能执行,会导致大量的等待时间,适用于对数据一致性要求极高且并发量相对较低的场景。

如何选择合适的隔离级别

  1. 考虑业务场景:如果业务对数据一致性要求不高,例如一些统计类的应用,读已提交可能就足够了,这样可以获得较好的并发性能。如果业务涉及到关键数据的读取和修改,如金融交易等,对数据一致性要求极高,可能需要选择可重复读甚至串行化隔离级别。
  2. 分析并发模式:如果并发操作以读为主,写操作较少,那么读已提交或可重复读都可以满足需求,并且能保持较好的性能。但如果写操作频繁,并且对数据一致性有严格要求,就需要仔细权衡可重复读和串行化的利弊。
  3. 测试与优化:在实际应用中,应该对不同的隔离级别进行性能测试和功能验证。通过模拟实际的并发场景,观察不同隔离级别下系统的性能表现和数据一致性情况,从而选择最合适的隔离级别。

事务隔离级别与锁机制的关系

在 PostgreSQL 中,不同的事务隔离级别与锁机制密切相关。

  1. 读已提交:读操作使用快照读,不会获取锁,因此不会阻塞其他事务的写操作。写操作会获取行级排他锁(X锁),防止其他事务同时对同一行进行写操作。
  2. 可重复读:读操作同样使用快照读,但为了保证可重复读,在事务开始时会获取一个全局的快照。写操作除了获取行级排他锁外,还会对相关的索引项加锁,以防止幻读。
  3. 串行化:在串行化隔离级别下,每个事务在执行前会获取一个全局的排它锁,确保事务是串行执行的。这使得锁的粒度更大,并发性能更低。

示例场景及隔离级别选择

  1. 银行转账场景:假设在银行系统中进行转账操作,从账户 A 向账户 B 转账。这个场景对数据一致性要求极高,因为涉及到资金的变动。应该选择可重复读或串行化隔离级别。例如,使用可重复读隔离级别可以保证在转账事务执行过程中,读取到的账户 A 和账户 B 的余额是一致的,不会出现不可重复读的情况。
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 读取账户A余额
SELECT balance INTO a_balance FROM accounts WHERE account_id = a_id;
-- 读取账户B余额
SELECT balance INTO b_balance FROM accounts WHERE account_id = b_id;
-- 进行转账操作
UPDATE accounts SET balance = a_balance - amount WHERE account_id = a_id;
UPDATE accounts SET balance = b_balance + amount WHERE account_id = b_id;
COMMIT;
  1. 电商商品库存管理场景:在电商系统中,多个用户可能同时购买商品,需要实时更新商品库存。这里对数据一致性要求较高,但并发量也较大。读已提交隔离级别可能是一个不错的选择,它既能保证库存数据的一致性,又能有较好的并发性能。
BEGIN;
-- 读取商品库存
SELECT stock INTO product_stock FROM products WHERE product_id = product_id;
-- 判断库存是否足够
IF product_stock >= quantity THEN
    -- 更新库存
    UPDATE products SET stock = stock - quantity WHERE product_id = product_id;
    -- 记录订单等操作
    INSERT INTO orders (product_id, quantity, customer_id) VALUES (product_id, quantity, customer_id);
END IF;
COMMIT;

事务隔离级别在分布式系统中的应用

在分布式数据库环境中,事务隔离级别面临更多挑战。由于数据分布在多个节点上,保证事务的一致性变得更加复杂。PostgreSQL 可以通过一些扩展,如 Postgres - XC 等,来支持分布式事务。在分布式系统中选择事务隔离级别时,除了考虑单机环境下的因素外,还需要考虑网络延迟、节点故障等问题。

例如,在一个分布式电商系统中,订单数据可能分布在多个节点上。如果选择串行化隔离级别,虽然可以保证数据的强一致性,但由于网络延迟等因素,可能会导致事务执行时间过长,影响系统性能。此时,可能需要在数据一致性和性能之间进行更细致的权衡,可能会选择读已提交或可重复读隔离级别,并结合一些分布式锁机制来尽量保证数据的一致性。

总结事务隔离级别在 PostgreSQL 中的要点

  1. PostgreSQL 提供了四种事务隔离级别,每种级别在数据一致性和并发性能上有不同的表现。
  2. 读已提交是默认隔离级别,适用于大多数场景。可重复读适用于对数据一致性要求较高且读操作频繁的场景。串行化用于对数据一致性要求极高的场景,但并发性能较低。
  3. 选择合适的隔离级别需要综合考虑业务场景、并发模式,并进行充分的测试和优化。
  4. 事务隔离级别与锁机制紧密相关,不同级别下锁的获取和使用方式不同。
  5. 在分布式系统中应用事务隔离级别时,需要考虑更多的因素,如网络延迟和节点故障等。

通过深入理解和合理应用 PostgreSQL 的事务隔离级别,可以在保证数据一致性的前提下,最大程度地提高系统的并发性能,满足不同业务场景的需求。在实际开发中,要根据具体情况仔细权衡和选择,确保数据库系统的高效稳定运行。