你有没有遇到过这种场景:临到月末,财务要出报表,业务系统还在不停往里写数据,DBA 接到通知说“今晚把数据库锁成只读”;或者项目要整体迁移,甲方要求源库在指定时间后不允许有任何数据变更。在 PostgreSQL 里,把整个数据库实例做成只读,并不是一条命令这么简单,它背后牵扯到事务语义、权限体系、连接管理和复制机制。这篇文章不打算写成枯燥的官方文档,而是结合我这些年实际处理过的数据库项目实例,围绕“PostgreSQL 数据库实例只读锁定”这个需求,把从方案选型到操作踩坑的完整过程讲清楚。不管你是刚照着 PostgreSQL 安装教程把库装起来的新手,还是正在做 SAP 系统迁移、Oracle 转 PostgreSQL 的运维同学,这部分知识点都会用得上。
1. 先把“只读锁定”这四个字拆明白
1.1 数据库实例到底指什么
很多人一说“实例”,脑子里先冒出 SAP 系统里那一堆名词:message 实例、PAS 实例、AAS 实例。这里要先划一道线:SAP 语境下的 message 实例、PAS、AAS,本质上是应用服务器层的进程实例,负责调度对话、队列、更新等业务作业;而数据库实例在 PostgreSQL 语境下,指的是一个正在运行的 postgres 服务进程加上它打开的那套数据目录。一台物理机上可以同时跑多个 PostgreSQL 实例,最常见的是不同端口、不同数据目录,通过pg_ctl -D或 systemd 单元分开管理。
所以“数据库实例只读锁定”这句话,真正的落点是整套正在运行的数据库服务体系,而不是某一张表、某一个 schema。你锁表的锁,锁行的锁,在 PostgreSQL 里都有专门语法,但那种锁和“实例只读”是完全不同维度的事。如果领导跟你说“把数据库锁上,别让任何人改数据”,你得先确认他到底是要锁整库、锁某个业务库,还是锁某个账号,否则后面所有操作都会跑偏。
1.2 只读锁定不是表级锁,也不是行级锁
PostgreSQL 平时聊得最多的锁,是表锁、行锁,以及 MVCC 机制下的并发控制。LOCK TABLE、SELECT FOR UPDATE这些属于并发控制锁,解决的是“多个事务同时操作同一份数据时怎么排队、怎么避免互相踩踏”。而只读锁定解决的是“这个实例是不是允许发生任何写操作”的问题。
真正把实例锁成只读后,INSERT、UPDATE、DELETE、MERGE、COPY 写入、DDL 建表改表,统统会被拒绝。它不是去抢什么锁,而是从事务属性或权限边界上直接关掉写通道。理解这一点很重要,因为很多从 Oracle 过来的人会习惯性地去找“改库状态”的命令,但 PostgreSQL 的哲学是“事务本身是只读的,数据库就只读”,方向上完全不同。
1.3 Oracle 用户最容易蒙圈的差异点
聊到 Oracle 和 PostgreSQL 语法区别,这一块算是最典型的代表。Oracle 里有ALTER DATABASE OPEN READ ONLY,可以把整个数据库切到只读挂载状态;也有ALTER TABLESPACE ... READ ONLY这种东西。PostgreSQL 没有直接对应的单条命令。
在 PostgreSQL,让实例只读通常是一组机制的组合:核心是default_transaction_read_only这个 GUC 参数,辅助手段包括权限回收、连接控制、物理从库等。Oracle 改的是“数据库状态”,PG 改的是“事务读写属性”和“权限边界”。你如果还按 Oracle 的老思路去操作,很容易卡在“找不到这个语法”这一步。后面我会把这些做法拆开讲,并给出可以直接抄的实操流程。
2. 让实例只读的几种主流方式与取舍
2.1 会话级只读:速度快,但不持久
最轻量的一种做法,是在单个会话里执行:
SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY;这等价于把当前会话的default_transaction_read_only设为 on。执行之后,这个会话新开启的事务都不能写数据;你甚至能在事务中间用SET TRANSACTION READ ONLY把当前事务直接切成只读。
但会话级只读有致命短板:它只管当前这一个连接,换个连接就失效,数据库重启也失效。所以我一般只在临时排查、或者给某个测试账号做心理安慰时用。生产环境如果说“今晚整个实例必须只读”,靠这一句是远远不够的,你必须把级别拉到实例级。
2.2 实例级全局只读:最常用的一套组合拳
真正要做实例级只读,核心命令是:
ALTER SYSTEM SET default_transaction_read_only = on; SELECT pg_reload_conf();ALTER SYSTEM会把配置写进postgresql.auto.conf,pg_reload_conf()触发所有进程重新加载配置,不需要重启数据库。执行完后,新建连接里的新事务默认都是只读的。
如果你的业务允许按库或按账号隔离,还可以用更细的粒度:
ALTER DATABASE mydb SET default_transaction_read_only = on; ALTER ROLE readonly_user SET default_transaction_read_only = on;ALTER DATABASE适合只锁某个业务库;ALTER ROLE适合给只读账号做固定属性。生产环境我常用的策略是:先用ALTER SYSTEM做全局兜底,再配合账号层权限限制,双保险。
注意:
ALTER SYSTEM不等于立刻把已经存在的连接全部切成只读。它对“新建立的连接”最有效,老连接、连接池里的残留连接,不一定马上感受到变化。后面我会讲怎么处理。
2.3 从权限层面禁止写入
除了事务属性,权限回收也是一条路。比如创建一个专门账号,只给 SELECT 权限:
CREATE ROLE app_readonly LOGIN PASSWORD 'xxx'; GRANT CONNECT ON DATABASE mydb TO app_readonly; GRANT USAGE ON SCHEMA public TO app_readonly; GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_readonly; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app_readonly;这种方案适合给“永远只读”的分析账号、报表账号用,但它有几个问题:第一,superuser 和对象 owner 可以无视大多数权限限制;第二,如果业务账号本身已经建好了,靠 GRANT/REVOKE 一条条往里补,很容易漏,后面某张新表建出来还带着默认权限,应用照样能写。权限方案更适合长期设计的只读账号,不适合临时把生产实例锁死。
还有人会想用 event trigger 拦截 DDL,比如在ddl_command_start里抛异常。这个能拦 CREATE TABLE、ALTER TABLE 这类结构变更,但拦不住 INSERT、UPDATE、DELETE,所以它顶多算补充手段,不能当主力方案。
2.4 standby 和文件系统只读,到底能不能用
物理从库(standby)在热备模式下也是一个“只读实例”,你可以在备库上跑 SELECT、跑报表,但不能写。这种只读是复制机制自带的属性,不是把主库锁住的手段。如果你是为了“读扩展”或“报表分离”,搭一个 hot standby 非常合适;但你是想把生产主库临时锁定做迁移,那 standby 解决不了主库问题。
文件系统级只读就更极端的。你可以把整个数据目录所在文件系统 remount 成只读,PostgreSQL 确实也没法往里写 WAL 和临时文件,但这种做法风险极高,数据库进程如果正在写数据,会出现一堆 IO 错误,搞不好直接把实例搞崩。我见过有人这么“锁库”,结果恢复的时候费了大力气。所以我的态度很明确:正规维护窗口里不要用文件系统只读,优先用参数和连接控制。
下表把这几种方式的核心差异汇总一下,方便你选型:
| 方案 | 生效级别 | 对已有连接 | 推荐场景 | 注意点 |
|---|---|---|---|---|
| 会话级 SET | 当前会话 | 当前会话后续事务 | 临时自测、单个会话调试 | 重启失效、换连接失效 |
| ALTER SYSTEM | 实例级 | 新连接有效,老连接需重连/确认 | 生产实例锁定 | 配合断开连接更稳妥 |
| ALTER DATABASE/ROLE | 库/账号级 | 按配置来源决定 | 只锁业务库、报表库 | 可能和已有连接配置冲突 |
| 权限回收 GRANT/REVOKE | 账号级 | 对新命令生效 | 长期只读账号 | superuser/owner 可绕过 |
| standby 备库 | 实例级 | 备库自带只读 | 报表分离、灾备 | 不能被当主库锁定的手段 |
| 文件系统只读 | 实例级 | 强制但危险 | 不建议 | 可能引发 IO 异常 |
3. 完整实操:把生产 PG 实例锁成只读的安全操作流程
3.1 锁定前必须做的检查清单
“锁库”听起来是十秒钟的事,但摔过跟头的人都知道,真正的功夫全在锁之前的检查。结合我处理过的生产故障,下面这几项一定别跳:
- 确认维护窗口:有没有定时任务、批处理、ETL 在这个时间段跑。
- 摸清连接来源:把
pg_stat_activity拉出来,看看有哪些应用、哪些 IP、哪些连接池在连。 - 检查复制状态:如果有从库,主库锁定只读本身不影响复制,但要注意备库延迟和主备切换策略。
- 确认备份任务:
pg_basebackup、WAL 归档、第三方备份工具,都要跟窗口错开。 - 找好“逃生门”:确保你自己有一条管理员连接通道,别把自己关在门外。
检查长事务和活跃写入,可以用这条 SQL:
SELECT pid, usename, application_name, state, query, xact_start, now() - xact_start AS duration FROM pg_stat_activity WHERE state <> 'idle' AND pid <> pg_backend_pid() ORDER BY duration DESC;如果发现某个事务已经跑了几个小时,先别急着锁,要跟业务确认这个长事务能不能打断。只读参数对“已经开启且正在写入的事务”有时候也管不到,把长事务排在前面处理,是保住数据一致性最关键的一步。
3.2 绑定实例配置并重载参数
确认没问题后,执行:
ALTER SYSTEM SET default_transaction_read_only = on; SELECT pg_reload_conf();然后验证:
SHOW default_transaction_read_only;正常情况下应该看到on。再开一个新连接,执行一个最简单的写入:
CREATE TABLE test_no_write(id int); -- ERROR: cannot execute CREATE TABLE in a read-only transaction如果只想锁某一个业务库,用:
ALTER DATABASE business_db SET default_transaction_read_only = on;如果要恢复,同样是ALTER SYSTEM RESET和pg_reload_conf():
ALTER SYSTEM RESET default_transaction_read_only; SELECT pg_reload_conf(); SHOW default_transaction_read_only;这里有个细节:如果当初你用了ALTER DATABASE或ALTER ROLE做了库级、账号级设置,恢复时也要对应地ALTER DATABASE ... RESET、ALTER ROLE ... RESET,否则全局虽然放开了,某个库某账号还是只读,很容易留下“幽灵只读”的坑。
3.3 断掉现有连接,别让老连接拖后腿
如前面说的,ALTER SYSTEM对已有连接不一定立刻生效。稳妥的做法分两步:先限制新连接进来,再把老写连接切掉。
限制新连接,最直接是临时改pg_hba.conf,把应用账号的连接规则改成reject,然后pg_ctl reload或执行SELECT pg_reload_conf();。这一步要特别小心,建议先保证自己有一条独立的本地管理连接,否则把自己拒之门外就尴尬了。
切断老连接,使用:
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'business_db' AND pid <> pg_backend_pid();pg_terminate_backend会强制终止连接,正在执行的事务也会被取消。生产环境建议逐步批量清理,别一次性把所有连接都断掉,尤其是业务高峰期。维护窗口内做这件事,配合应用团队重启连接池,效果最好。
如果你用了 pgbouncer、JDBC 连接池这类中间件,光在数据库层断连接还不够,应用连接池里可能还有空闲连接会被复用。最理想的做法是让应用团队在锁库的同时把连接池清掉或重连一次,这样新连接就会拿到全局只读属性,行为才是确定性的。
3.4 验证“锁”到底锁没锁死
很多新手以为执行完SHOW default_transaction_read_only就万事大吉,其实不然。真正到位的验证应该是尽量模拟业务写入:
-- DML 验证 BEGIN; INSERT INTO t1 VALUES (1); -- ERROR: cannot execute INSERT in a read-only transaction -- DDL 验证 CREATE TABLE tmp_test(id int); -- ERROR: cannot execute CREATE TABLE in a read-only transaction -- 数据装载验证 \copy t1 from '/tmp/data.csv' with csv -- ERROR: cannot execute COPY FROM in a read-only transaction理论上,只读模式下你还能执行 SELECT、SET、SHOW、EXPLAIN,这是符合预期的。如果某个应用账号还能写,优先检查它是不是 superuser、是不是表 owner,或者它的连接是不是没有重新建立。搞清楚这三个原因,基本能覆盖 90% 的“没锁住”现场。
3.5 恢复流程要按反方向走
锁库容易,解锁的时候反而容易乱。我的建议是把恢复流程当成一次正式的发布操作,至少包含以下步骤:
- 业务侧确认所有写入任务已停止,不需要再保持只读。
- 恢复全局参数:
ALTER SYSTEM RESET default_transaction_read_only; SELECT pg_reload_conf(); - 恢复库级/角色级参数:对当初设置过的数据库和角色执行对应
RESET。 - 恢复
pg_hba.conf里临时加的reject规则,并 reload。 - 验证写入:开新连接执行
INSERT或CREATE TABLE,确认能成功。 - 通知应用团队恢复连接池和业务流量。
这里特别提醒一句:如果有从库在主库只读期间一直在提供服务,解锁主库前要跟 DBA 确认当前主备关系。别在主备状态没确认的情况下直接解只读,场景一复杂,很容易把复制搞乱。
4. 常见坑与问题排查实录
4.1 superuser 和对象 owner 绕过只读
这是最容易翻车的点。很多人以为把default_transaction_read_only打开后,全天下都不能写了。实际上,default_transaction_read_only对 superuser 也是生效的,因为它属于事务属性;但 superuser 可以在自己的会话里执行:
SET default_transaction_read_only = off;然后继续写。换句话说,这道锁防的是普通账号,防不住有心的管理员。如果你要做的是强合规场景,得配合更严格的手段,比如限制账号权限、控制管理员访问、或者在连接池层面对 superuser 连接做隔离。别天真地以为一个参数就能锁住全世界。
对象 owner 的坑也类似。默认建表的人对表有全部权限,当初如果权限体系不规范,很多应用账号本身就是 owner,权限回收不一定覆盖到位。所以长期方案一定要把“应用账号不应该是表 owner”这个原则立起来。
4.2 为什么 ALTER SYSTEM 之后还是能写
遇到“我明明设了只读,为什么还能写”的问题,从这三条查起:
- 查当前连接的配置来源:
SELECT current_setting('default_transaction_read_only');,如果显示 on,那就是新建连接问题;如果显示 off,看看是不是有ALTER DATABASE、ALTER ROLE、会话级SET覆盖了全局值。 - 查连接池:应用连的到底是数据库直连,还是 pgbouncer 代理。连接池里的连接如果不重连,可能一直拿着旧配置。
- 查执行用户:用业务账号去测,别拿 superuser 账号测,否则结果没有任何参考价值。
另外一个容易忽略的点:ALTER DATABASE设置只在“数据库默认参数”这一层生效,它会被会话级SET覆盖。如果应用在建立连接后自己执行了SET default_transaction_read_only = off,那库级配置也没用。遇到这种应用,要联合开发改代码,而不是单方面找数据库的问题。
4.3 连接池带来的“幽灵写入”
我处理过一起非常抓狂的事故:锁库窗口执行了ALTER SYSTEM SET default_transaction_read_only = on,也 reload 了,连 pg_stat_activity 都查了一遍没看出问题,结果业务还是报出某条 UPDATE 执行成功了。后来顺着日志查发现,写入来源是一条应用连接池里长期复用的连接,它是在锁库之前建立的,事务边界被应用框架控制得乱七八糟,锁库后它复用了旧的事务上下文,绕过了检查。
从那以后,我的操作习惯就改了:锁库前先和应用团队对齐连接池策略,要么全部重连,要么干脆停应用流量。数据库侧能做的只是“新建连接强制只读”,你把连接清掉或让连接池重新初始化,这一步无论如何省不了。
4.4 只读和复制、归档任务打架怎么办
很多环境不是单库,而是有流复制从库和 WAL 归档。主库只读后,业务写入停了,WAL 增长会变慢,从库压力也会下降,这本身没毛病。但要注意几点:
- 备份任务:如果备份工具在只读窗口里尝试执行
pg_start_backup(),可能因为写入备份标签等操作失败。建议提前把备份窗口调开。 - 主备切换:只读锁定通常只针对主库,如果此时发生 failover,原备库会被提升为主库并变为可写。原主库的只读状态在它降级后可能会随着配置被重新加载而解除,具体情况要看你当初是用哪一层做的锁定。
- 长事务导致的 vacuum 阻塞:只读窗口里如果还有长事务挂着,会影响 autovacuum 的推进,锁库时间特别长时要注意 vacuum 和事务 ID 回卷风险。
这里的原则是:只读锁定不是“把数据库冻住”,它只是不让业务写数据,后台的维护逻辑、复制协议、备份周期仍然需要单独评估。
4.5 和 SAP 系统混在一起时最容易踩的坑
前面说过,SAP 系统里的 message 实例、PAS 实例、AAS 实例都属于应用层,数据库实例是独立一层。如果你把 PostgreSQL 数据库实例设成只读,SAP 应用实例还开着,前端用户会看到一堆数据库连接错误、写入失败;反过来,你只把 SAP 应用停了,数据库实例继续可写,数据层面还是有可能被外部工具改动。这两件事必须当成两次变更来对待。
我在做某个 SAP 外围系统的数据迁移时,就吃过这个亏:数据库团队把 PG 库锁成只读,然后等应用团队停 batch job,结果两边没对齐,batch job 还在继续重试,数据库日志刷屏,最后不得不加班复盘。后来的流程固定成三个步骤:先由 SAP Basis 团队停相关作业和对话实例,再由应用团队确认没有活动写入,最后数据库团队才上只读锁定。顺序反了,谁都难受。
4.6 从 Oracle 转过来的操作习惯要改一改
Oracle 和 PostgreSQL 语法区别如果你没仔细研究过,很容易在只读锁定这个场景里出问题。举例来说,Oracle 里可以用ALTER SYSTEM ENABLE RESTRICTED SESSION来阻止新会话,PostgreSQL 没有这个命令,你得用pg_hba.conf加拒连规则,或者用pg_terminate_backend断连接;Oracle 里可以ALTER TABLESPACE users READ ONLY,PostgreSQL 没有表空间级只读开关,要做只能往实例参数、库参数或权限方向走。
我把这两个常用动作列成对照表,方便大家快速抓重点:
| 操作意图 | Oracle 常用命令 | PostgreSQL 实践 |
|---|---|---|
| 整库只读 | ALTER DATABASE OPEN READ ONLY; | ALTER SYSTEM SET default_transaction_read_only=on; SELECT pg_reload_conf(); |
| 阻止新会话 | ALTER SYSTEM ENABLE RESTRICTED SESSION; | 临时改pg_hba.conf+ reload |
| 断开已有会话 | 查 v$session 后 kill | 查 pg_stat_activity 后pg_terminate_backend(pid) |
| 表空间只读 | ALTER TABLESPACE tbs READ ONLY; | 没有直接开关,考虑库级/账号级只读 |
明白了这些差异,你在做数据库迁移或双轨运维时就能少走弯路。不是说 Oracle 的方法好或不好,而是换了个数据库,防线的位置和操作入口都要换一套思维。
5. 几个我踩过之后不想你再踩的建议
把 PostgreSQL 数据库实例锁成只读,听起来是个很小的操作,实际却是个系统工程。我个人的体会是:不管用什么参数,都要先问自己三个问题——这个锁是针对谁生效的?已有连接会不会漏?恢复流程有没有人负责?三个问题答不上来,就别急着执行。
再分享一个小技巧:执行只读锁定后,不要只查SHOW default_transaction_read_only,要主动用一个普通业务账号开新连接,真实执行一条 INSERT 试试。命令报错,才叫锁住;命令不报错,说明一定还有某个层级漏了。这个习惯帮我挡掉过好几次“假成功”的尴尬,成本很低,收益很高。
如果是多人协作的团队,建议把锁库和解锁做成带确认环节的操作单,每次变更都记录执行时间、执行人、恢复时间。听起来有点重,但数据库实例只读锁定往往是迁移、割接、合规审计的关键节点,留好记录,后续出问题你能少掉很多头发。