同一台 Linux 机器上跑着两个进程,都要读写同一份数据,这是很多运维和开发都会遇到的场景。直觉做法是让两个进程各自打开同一个数据库文件,结果往往是要么报 database is locked,要么读到半截数据。问题不在于 Linux 不允许共享,而在于共享的粒度和锁的机制没选对。本文按三条可行路线讲清楚:走数据库服务、用 SQLite 的 WAL 模式、以及加一层连接池,并给出验证与排错方法。
先分清:共享的是文件、连接还是事务
两个进程共享数据库,实际有三层含义,混在一起谈容易开错药方。
- 共享数据文件:两个进程直接操作同一个
.db或数据目录。除了 SQLite 这种嵌入式引擎,MySQL、PostgreSQL 都不允许第二个进程碰它的数据目录,强行打开只会损坏文件。 - 共享连接:两个进程各自建立到数据库服务的连接,共用同一个账号、同一张表。这是最常见的正确形态。
- 共享事务:一个事务跨越两个进程。除非用分布式事务或外部协调,否则不要这么设计,锁等待和回滚代价都很高。
结论先行:如果两个进程都需要长期读写,优先把它们变成数据库服务的两个客户端;只有在轻量、单机、低并发的场景,才让两个进程直接共享一个 SQLite 文件。
路线一:走数据库服务,两个进程各建连接
MySQL/MariaDB 与 PostgreSQL 都是单服务进程(或单实例)模型,数据文件只归服务进程所有,客户端通过 TCP 或 Unix socket 访问。两个业务进程共享数据库,本质是共享同一个服务实例的访问权限。
以 MariaDB 为例,给两个进程分配同一个库、彼此可区分的账号:
sudo mysql -e "CREATE DATABASE shared_db CHARACTER SET utf8mb4;"
# 两个进程各用一个独立账号,便于审计与限权
sudo mysql -e "CREATE USER 'app_a'@'localhost' IDENTIFIED BY '密码A';"
sudo mysql -e "CREATE USER 'app_b'@'localhost' IDENTIFIED BY '密码B';"
sudo mysql -e "GRANT SELECT,INSERT,UPDATE ON shared_db.* TO 'app_a'@'localhost';"
sudo mysql -e "GRANT SELECT,INSERT,UPDATE ON shared_db.* TO 'app_b'@'localhost';"
sudo mysql -e "FLUSH PRIVILEGES;"
用独立账号而不是共用 root,好处是出问题时能从审计日志直接定位是哪个进程写坏了数据,权限也能按最小化收敛。PostgreSQL 对应的是 CREATE ROLE 加 GRANT,并在 pg_hba.conf 里限定连接来源。
并发写入的冲突交给数据库自己的锁与隔离级别处理。默认 REPEATABLE READ(MySQL)或 READ COMMITTED(PostgreSQL)已经能避免脏读;如果两个进程会更新同一行,用行锁或乐观锁版本号,而不是在应用层加文件锁。
路线二:共享 SQLite 文件,必须开 WAL
SQLite 没有服务进程,两个进程共享的就是那个 .db 文件本身。默认的回滚日志(rollback journal)模式下,写操作会独占整个库,另一个进程一写就报 database is locked。要支持多进程并发,第一步是切到 WAL 模式:
# 只需执行一次,模式会持久记录在库文件里
sqlite3 /var/lib/myapp/app.db "PRAGMA journal_mode=WAL;"
# 每个进程连接后设置忙等超时,避免瞬时锁冲突直接失败
sqlite3 /var/lib/myapp/app.db "PRAGMA busy_timeout=5000;"
WAL 的价值在于读写可以并行:读进程读的是快照,不会被写进程阻塞,写进程写的是追加日志。但要注意三点。
- 写写仍然串行:同一时刻只有一个写事务。把事务切小、切短,锁等待自然下降。
- WAL 文件必须本地:
-wal和-shm依赖共享内存映射,把数据库放在 NFS 或其他网络文件系统上,锁语义不可靠,容易损坏。 - 需要定期 checkpoint:长时间只写不合并会让
-wal无限增长,SQLite 默认自动触发,写入密集时可手动PRAGMA wal_checkpoint(TRUNCATE);。
文件权限也要放开到两个进程的属主都可读写,别忘了 SQLite 需要在工作目录创建临时文件,目录权限同样重要。
路线三:连接数吃紧时,加一层连接池
如果两个进程内部还各自开线程,连接数会成倍膨胀,压垮数据库的 max_connections。这时在进程与服务端之间放一个连接池,进程共享的是池里的物理连接。单机轻量场景推荐 PgBouncer(PostgreSQL)或 ProxySQL(MySQL);应用内则用 SQLAlchemy 的 QueuePool、HikariCP 这类进程内池。
# PgBouncer 最小配置:限制后端总连接数,前端按需排队
[databases]
shared_db = host=127.0.0.1 port=5432 dbname=shared_db
[pgbouncer]
pool_mode = transaction max_client_conn = 200 default_pool_size = 20
pool_mode=transaction 按事务粒度复用连接,能显著压低后端连接占用,代价是会话级变量和临时表不再可靠,迁移时要检查这类用法。
验证是否真的共享成功
不要靠日志猜,直接开两个终端模拟两个进程交叉读写:
# 终端 1:进程 A 写入
sqlite3 /var/lib/myapp/app.db "INSERT INTO t(v) VALUES('from-A');"
# 终端 2:进程 B 立刻读取,能看到 A 的数据即共享生效
sqlite3 /var/lib/myapp/app.db "SELECT * FROM t ORDER BY id DESC LIMIT 3;"
数据库服务端则用连接视图确认两个进程确实各占一条连接:MySQL 执行 SELECT user, host, db, command FROM information_schema.processlist;,PostgreSQL 查 SELECT usename, application_name, state FROM pg_stat_activity;。看到两个不同账号的连接同时存在,说明共享路径打通。
常见问题
问:已经开了 WAL,偶尔还是报 database is locked?
答:多半是没设 busy_timeout,或存在一个长事务把写锁占住了。先查是否有进程开了事务忘记提交,再把超时设到几秒,并缩短事务。
问:两个进程在容器里,共享宿主机的 SQLite 文件行不行?
答:同一台宿主机、通过 bind mount 映射到本地磁盘可以;如果映射的是 NFS 卷,回到前面说的坑,换成本地盘或改走数据库服务。
问:能不能让两个进程共用一个数据库连接?
答:跨进程共享同一条物理连接没有可靠办法,连接状态、事务上下文和游标都属于单个进程。要共享的是数据与服务端权限,不是连接句柄本身。
问:读写冲突很多,怎么判断该不该上数据库服务?
答:看锁等待时间。SQLite 下用 PRAGMA busy_timeout 配合重试计数,如果重试频繁或等待超过几十毫秒成为常态,就该把数据挪进 MySQL/PostgreSQL,让服务端的行锁来接管并发。