1. 首页 > PostgreSQL教程 > 正文

PostgreSQL教程FG059-PG并发控制:事务隔离级别与锁机制详解

本文档风哥主要介绍PostgreSQL教程059相关内容。风哥教程参考PostgreSQL官方文档Server Administration, SQL Language内容编写,适合DBA人员在学习和测试中使用,如果要应用于生产环境则需要自行确认。

更多视频教程www.fgedu.net.cn

1. 事务隔离级别概述

PostgreSQL支持四种事务隔离级别,用于控制并发事务之间的可见性。

注意:PostgreSQL的可重复读实际上也防止了幻读,这是通过MVCC机制实现的。

— 查看当前隔离级别
SHOW default_transaction_isolation;

执行结果:

default_transaction_isolation
——————————-
read committed
(1 row)

2. 读未提交

PostgreSQL的读未提交实际上等同于读已提交,不会读取脏数据。

— 创建测试表
CREATE TABLE fgedu_accounts_isolation (
account_id SERIAL PRIMARY KEY,
account_name VARCHAR(50),
balance NUMERIC(10, 2)
);

INSERT INTO fgedu_accounts_isolation (account_name, balance) VALUES
(‘账户A’, 1000.00),
(‘账户B’, 2000.00);

— 会话1:设置读未提交
BEGIN TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT * FROM fgedu_accounts_isolation WHERE account_id = 1;

执行结果:

account_id | account_name | balance
————+————–+———
1 | 账户A | 1000.00
(1 row)
— 会话2:修改数据但不提交
BEGIN;
UPDATE fgedu_accounts_isolation SET balance = 1500.00 WHERE account_id = 1;
— 不提交,回到会话1查询

— 会话1:再次查询
SELECT * FROM fgedu_accounts_isolation WHERE account_id = 1;
COMMIT;

执行结果:

account_id | account_name | balance
————+————–+———
1 | 账户A | 1000.00 — 仍然看到原值,不会读到脏数据
(1 row)

COMMIT

3. 读已提交

读已提交是PostgreSQL默认的隔离级别,每个查询看到的是查询开始时已提交的数据。

— 会话1:设置读已提交
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT * FROM fgedu_accounts_isolation WHERE account_id = 1;

执行结果:

from oracle:www.itpux.com风哥提示:

account_id | account_name | balance
————+————–+———
1 | 账户A | 1000.00
(1 row)
— 会话2:修改并提交
BEGIN;
UPDATE fgedu_accounts_isolation SET balance = 1200.00 WHERE account_id = 1;
COMMIT;

— 会话1:再次查询(同一事务内)
SELECT * FROM fgedu_accounts_isolation WHERE account_id = 1;
COMMIT;

执行结果:

account_id | account_name | balance
————+————–+———
1 | 账户A | 1200.00 — 看到了会话2提交的修改
(1 row)

COMMIT

不可重复读问题:在同一事务内,两次查询可能看到不同的数据(如果其他事务提交了修改)。

4. 可重复读

可重复读保证在同一事务内,多次读取同样的数据看到的结果相同。

— 会话1:设置可重复读
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT * FROM fgedu_accounts_isolation WHERE account_id = 1;

执行结果:

account_id | account_name | balance
————+————–+———
1 | 账户A | 1200.00
(1 row)
— 会话2:修改并提交
BEGIN;
UPDATE fgedu_accounts_isolation SET balance = 1500.00 WHERE account_id = 1;
COMMIT;

— 会话1:再次查询(同一事务内)
SELECT * FROM fgedu_accounts_isolation WHERE account_id = 1;
COMMIT;

执行结果:

account_id | account_name | balance
————+————–+———
1 | 账户A | 1200.00 — 仍然是原来的值,实现了可重复读
(1 row)

COMMIT

— 会话1结束后再查询
SELECT * FROM fgedu_accounts_isolation WHERE account_id = 1;

执行结果:

account_id | account_name | balance
————+————–+———
1 | 账户A | 1500.00 — 新事务可以看到最新提交的数据
(1 row)

5. 可串行化

可串行化是最高的隔离级别,完全隔离并发事务的影响。

学习交流加群风哥微信: itpux-com

— 会话1:设置可串行化
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT SUM(balance) FROM fgedu_accounts_isolation;

执行结果:

学习交流加群风哥QQ113257174

sum
———
3500.00
(1 row)
— 会话2:插入新数据
BEGIN;
INSERT INTO fgedu_accounts_isolation (account_name, balance) VALUES (‘账户C’, 500.00);
COMMIT;

— 会话1:再次统计
SELECT SUM(balance) FROM fgedu_accounts_isolation;
COMMIT;

执行结果:

更多学习教程公众号风哥教程itpux_com

sum
———
3500.00 — 仍然是原来的值,没有看到新插入的数据
(1 row)

COMMIT

— 尝试并发更新可能导致序列化失败
— 会话1
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
UPDATE fgedu_accounts_isolation SET balance = balance + 100 WHERE account_id = 1;

— 会话2(同时执行)
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
UPDATE fgedu_accounts_isolation SET balance = balance – 100 WHERE account_id = 1;
COMMIT;

— 会话1提交
COMMIT; — 可能报错:ERROR: could not serialize access due to concurrent update

执行结果:

ERROR: could not serialize access due to concurrent update

6. 锁机制详解

6.1 表级锁

— 查看表级锁类型
SELECT locktype, mode, description
FROM pg_locks
JOIN pg_class ON pg_locks.relation = pg_class.oid
WHERE pg_class.relname = ‘fgedu_accounts_isolation’;

执行结果:

locktype | mode | description
———-+——+————-
(0 rows) — 当前没有锁
— 显式获取表锁
BEGIN;
LOCK TABLE fgedu_accounts_isolation IN SHARE MODE;
SELECT * FROM fgedu_accounts_isolation;
COMMIT;

执行结果:

LOCK TABLE
account_id | account_name | balance
————+————–+———
1 | 账户A | 1500.00
2 | 账户B | 2000.00
3 | 账户C | 500.00
(3 rows)

COMMIT

6.2 行级锁

— 行级锁示例
BEGIN;
SELECT * FROM fgedu_accounts_isolation WHERE account_id = 1 FOR UPDATE;
— 此时其他事务无法修改这一行

— 查看当前锁
SELECT
locktype,
mode,
relation::regclass,
page,
tuple
FROM pg_locks
WHERE relation = ‘fgedu_accounts_isolation’::regclass;

执行结果:

locktype | mode | relation | page | tuple
———-+—————+————————-+——+——-
tuple | RowExclusiveLock | fgedu_accounts_isolation | 0 | 1
(1 row)
— 释放锁
COMMIT;

6.3 行锁类型

— 不同类型的行锁
SELECT * FROM fgedu_accounts_isolation WHERE account_id = 1 FOR UPDATE; — 排他锁
SELECT * FROM fgedu_accounts_isolation WHERE account_id = 1 FOR NO KEY UPDATE; — 较弱的排他锁
SELECT * FROM fgedu_accounts_isolation WHERE account_id = 1 FOR SHARE; — 共享锁
SELECT * FROM fgedu_accounts_isolation WHERE account_id = 1 FOR KEY SHARE; — 较弱的共享锁

执行结果:

account_id | account_name | balance
————+————–+———
1 | 账户A | 1500.00
(4 rows)

7. 实战案例:并发问题

7.1 丢失更新问题

— 重置数据
UPDATE fgedu_accounts_isolation SET balance = 1000.00 WHERE account_id = 1;

— 模拟丢失更新(两个事务同时读取并更新)
— 会话1
BEGIN;
SELECT balance FROM fgedu_accounts_isolation WHERE account_id = 1;
— balance = 1000

— 会话2(同时执行)
BEGIN;
SELECT balance FROM fgedu_accounts_isolation WHERE account_id = 1;
— balance = 1000
UPDATE fgedu_accounts_isolation SET balance = 1000 + 200 WHERE account_id = 1;
COMMIT;

— 会话1继续
UPDATE fgedu_accounts_isolation SET balance = 1000 + 100 WHERE account_id = 1;
COMMIT;

— 最终结果
SELECT balance FROM fgedu_accounts_isolation WHERE account_id = 1;

执行结果:

balance
———
1100.00 — 会话2的更新被覆盖了
(1 row)

7.2 使用SELECT FOR UPDATE防止丢失更新

— 重置数据
UPDATE fgedu_accounts_isolation SET balance = 1000.00 WHERE account_id = 1;

— 使用FOR UPDATE防止丢失更新
— 会话1
BEGIN;
SELECT balance FROM fgedu_accounts_isolation WHERE account_id = 1 FOR UPDATE;
— balance = 1000,持有行锁

— 会话2(同时执行,会等待)
BEGIN;
SELECT balance FROM fgedu_accounts_isolation WHERE account_id = 1 FOR UPDATE;
— 等待会话1释放锁…

— 会话1继续
UPDATE fgedu_accounts_isolation SET balance = 1000 + 100 WHERE account_id = 1;
COMMIT; — 释放锁

— 会话2现在可以继续
— balance = 1100(看到会话1提交的值)
UPDATE fgedu_accounts_isolation SET balance = 1100 + 200 WHERE account_id = 1;
COMMIT;

— 最终结果
SELECT balance FROM fgedu_accounts_isolation WHERE account_id = 1;

执行结果:

balance
———
1300.00 — 两个更新都生效了
(1 row)

8. 清理环境

— 删除测试表
DROP TABLE IF EXISTS fgedu_accounts_isolation;

执行结果:

DROP TABLE
风哥教程风哥教程风哥教程总结:

  • 读已提交是默认隔离级别,适合大多数场景
  • 可重复读适合需要一致性读取的场景
  • 可串行化适合对一致性要求极高的场景
  • 使用SELECT FOR UPDATE可以防止丢失更新
  • 合理选择隔离级别和锁策略,平衡并发性和一致性

本文由风哥教程整理发布,仅用于学习测试使用,转载注明出处:http://www.fgedu.net.cn/10327.html

联系我们

在线咨询:点击这里给我发消息

微信号:itpux-com

工作日:9:30-18:30,节假日休息