一、什么是 ACID

  • 原子性
  • 一致性
  • 隔离性
  • 持久性

二、DDL 事务

在PostgreSQL中,与其他数据库最大的不同是,大多数DDL也是可以包含在一个事务中的,而且也是可以回 滚的

适合场景:PostgreSQL作为Sharding的分布式数据系统的底层数据库

原因:Sharding中,常常需要在多个节点中建相同的表,此时可以考虑把建表语句放在同一个事务中,这样就 可以在各个节点上先启动一个事务,然后再执行建表语句,保证整个集群的一致性

三、事务的使用方法

在psql的默认配置下,自动提交功能“AUTOCOMMIT”是打开 可以通过设置psql中的内置变量“AUTOCOMMIT”来关闭自动提交功能: 比如:

\set AUTOCOMMIT off; 
begin;

四、SAVEPOINT

PostgreSQL支持保存点(SAVEPOINT)的功能,在一个大的事务中,可以把操作过程分成几个部分,第一个 部分执行成功后可以建一个保存点,若后面的部分执行失败,则回滚到此保存点,而不必回滚整个事务

案例:

-- 开启事务
begin;

insert into testtab01 values(1);
insert into testtab01 values(2);

-- 创建保存点
savepoint my_savepoint01;

-- 这两条插入会导致主键/唯一约束报错
insert into testtab02 values(1);
insert into testtab02 values(1);

-- 报错后,回滚到保存点(不会影响 testtab01 的插入)
rollback to SAVEPOINT my_savepoint01;

-- 此时 testtab02 无数据,可继续执行其他操作
select * from testtab02;

-- 最后提交/回滚整个事务
-- commit;
-- rollback;

五、事务隔离级别

数据库的事务隔离级别有以下4种。

  • READ UNCOMMITTED:读未提交。
  • READ COMMITTED:读已提交。
  • REPEATABLE READ:重复读。
  • SERIALIZABLE:串行化。

对于并发事务,我们不希望发生不一致的情况,这类情况的级别从高到低排序如下

  • 脏读
  • 不可重复读
  • 幻读

postgresql中只支持三种隔离级别:

  • 读已提交
  • 可重复读
  • 串行化

需要说明:在PostgreSQL的一 个事务中不可能读到其他事务中未提交的数据。在选择可重复读级别的时候,实际上仍是可串行化,所以实际 的隔离级别可能比你选择的更加严格。

六、两阶段提交

多台数据库之间的原子性就需要通过两阶段提交来实现了,两阶段提交是实现分布式事务的关键 PostgreSQL数据库支持两阶段提交协议

两阶段提交协议有如下5个步骤。

1、应用程序调用事务协调器

2、通知准备提交事务(PostgreSQL中一般是调用PREPARE TRANSACTION命令)

3、接收到PREPARE TRANSACTION命令,保证将自己置于准备提交中的状态

4、事务协调器接收所有数据库的响应

5、如果任何一个数据库在第一阶段返回失败,则事务协调器将会发一个回滚命令“ROLLBACK PREPARED”给 各台数据库

6.1 PostgreSQL 持久化事务案例

前提:max_prepared_transactions”设置成一 个大于0的数字,不然会报错

set max_prepared_transactions = 10; 报错
postgresql.conf文件中的max_prepared_transactions”为“10
create table testtab01(id int primary key);
#启动事务
begin;
insert into testtab01 values(1);
PREPARE TRANSACTION 'global_trans_1'; #全局事务的ID
pg_ctl stop -D $PGDATA
pg_ctl start -D $PGDATA

#连接数据库提交两阶段事务
COMMIT PREPARED 'global_trans_1';
查询数据
select * from testtab01;

七、postgresql 锁机制

7.1 表级锁

表级锁只有 SHARE 和 EXCLUSIVE 这两种,表级锁共有八种模式,其存在于 PG 的共享内存中,可以通过 pg_locks 系统视图查询。

锁模式 解释
ACCESS SHARE 访问共享 SELECT 命令在被引用的表上会获得一个这种模式的锁。通常,任何只读取表而不修改它的查询都将获取这种表模式。
ROW SHARE 行共享 SELECT FOR UPDATE 和 SELECT FOR SHARE 命令在目标表上会获得一个这种模式的锁。(加上在被引用但没有选择 FOR UPDATE / FOR SHARE 的任何其他表上的 ACCESS SHARE 锁。)
ROW EXCLUSIVE 行独占 UPDATE、DELETE 和 INSERT 命令在目标表上会获得一个这种模式的锁。
SHARE UPDATE EXCLUSIVE ALTER INDEX 和 ALTER TABLE 命令的变体会获得。这种模式保护一个表不受并发模式改变。
SHARE 共享 CREATE INDEX(不带 CONCURRENTLY)命令会获得。这种模式保护一个表不受并发数据改变的影响。
SHARE ROW EXCLUSIVE 共享行独占 CREATE TRIGGER 命令和某些形式的 ALTER TABLE 命令会获得。
EXCLUSIVE 排他 REFRESH METERIALIZED VIEW CONCURRENTLY 命令会获得。

7.2 表锁特点

锁粒度非常大

通过类似于mysql意向锁的方式,PostgreSQL中也是这样实现的,如ROWSHARE、ROW EXCLUSIVE这两个 锁

7.3 行级锁

行级锁模式比较简单,只有两种,即“共享锁”“排它锁”, 在PostgreSQL中由于有多版本的实现,所以实际读取行数据时,并不会在行上执行任何锁

7.4 表级锁命令 LOCK TABLE

LOCK [ TABLE ] [ ONLY ] name [, ...] [ IN lockmode MODE ] [ NOWAIT ] 
--NOWAIT:表示不等待

7.5 行级锁命令

SELECT ..... FOR { UPDATE | SHARE } [ OF table_name [, ...] ] [ NOWAIT ] [...] ]

此命令中的NOWAIT关键字与在LOCK TABLE中是相同的,加了NOWAIT关键字后,如果无法获得锁则直接报 错,而不会一直等待

7.6 锁的查看

pg_locks 可以查询到当前锁的信息 比如:哪个事务被哪个事务阻塞了,若执行一条SQL语句时阻塞住了,需 要查询为什么阻塞,是谁阻塞住的

image-20260405124033445

7.6.1 表锁查询案例

session1

-- 1. 创建测试表
create table testtab02(id int primary key);
insert into testtab02 values(1);

-- 2. 开启事务并锁表
begin;
lock table testtab02;

-- 3. 查询数据库锁信息
select 
  locktype, 
  relation::regclass as rel, 
  virtualxid as vxid, 
  transactionid as xid,
  virtualtransaction as vxid2, 
  pid, 
  mode, 
  granted 
from pg_locks;

结果解释:

其中一行显示的是事务在自己的“virtualxid”上 加的ExclusiveLock锁,这是必定会加上的。 另一行才是我们实际在表上加的锁“AccessExclusiveLock”

session2

begin;
lock table testtab02;
查询锁
select locktype, relation::regclass as rel, virtualxid as vxid, transactionid as xid ,
virtualtransaction as vxid2, pid, mode, granted from pg_locks;

7.6.2 行锁查询案例

session1

-- 窗口1:普通查询(无锁)
select * from testtab02 where id=1;

-- 窗口2:开启事务 + 行锁
begin;
select * from testtab02 where id=1 for update;

-- 窗口3:查看锁信息
select 
  locktype, 
  relation::regclass as rel, 
  virtualxid as vxid, 
  transactionid as xid,
  virtualtransaction as vxid2, 
  pid, 
  mode, 
  granted 
from pg_locks;

问题1:

为什么没有发现行锁?

答案:

实际上pg_locks并不能显示出每个行锁的信息,原因也很简单,行锁信息并不会记录到共享内存中

问题2

在pg_locks中没有行锁信息,如何知道一个进程被另一个进程的行锁阻塞了呢?

session2

begin;
select * from testtab02 where id=1 for update;

想查看哪个进程被阻塞住了,只需要查看“granted”字段值为“False”的PID

查看session的 bid

select pg_backend_pid();