在 Linux 环境下使用 SQLite3 数据库时,经常会遇到一个令人困惑的错误:明明数据库文件本身有写权限,却仍然提示 “attempt to write a readonly database”。本文基于 SQLite 官方文档,深入解析这个错误的根本原因以及 SQLite3 的临时文件机制。

问题背景

在使用 SmartTotem GUI 工具时,遇到了以下场景:

环境配置

  • 数据库文件:/tools/Ansys/SmartTotemGUI/v1/.smarttotem_config_by_admin.db
  • 文件权限:755(所有者可读写执行)
  • 所有者:当前用户账号

操作流程

  1. 启动 smarttotem_gui.py
  2. 在 User Configurations 中双击某个条目
  3. 程序尝试修改数据库

错误信息

1
Failed to copy configuration to public database: attempt to write a readonly database

问题根源

经过排查发现,数据库文件本身的权限并不是唯一需要考虑的因素。SQLite3 在写入数据库时,需要在数据库文件所在的父目录创建临时文件,这些临时文件用于保证数据完整性和事务原子性。

为什么父目录需要写权限?

即使数据库文件本身可写,如果父目录没有写权限,SQLite3 无法创建临时文件,就会报错:

attempt to write a readonly database


SQLite3 临时文件详解

根据 SQLite 官方文档,SQLite3 使用 9 种临时文件 来保证数据完整性和性能优化。其中与写操作最相关的有以下几类:

1. Rollback Journal(回滚日志)

文件命名<database-name>-journal

用途

  • 实现原子提交(Atomic Commit)和回滚(Rollback)能力
  • 存储事务开始前的原始数据
  • 崩溃恢复时用于恢复数据库到一致状态

生命周期

  • 事务开始时创建
  • 事务提交或回滚时删除
  • 如果程序崩溃,日志文件会保留在磁盘上(称为 “hot journal”)
  • 下次打开数据库时自动用于恢复

为何重要

  • 没有 rollback journal,SQLite 无法回滚未完成的事务
  • 断电或崩溃时,没有日志文件会导致整个数据库损坏

示例

1
2
/path/to/mydata.db          # 原数据库
/path/to/mydata.db-journal  # 回滚日志

2. Write-Ahead Log(WAL)文件

文件命名<database-name>-wal

用途

  • WAL 模式下替代 rollback journal
  • 将修改先写入 WAL 文件,而不是直接写入主数据库
  • 允许读写并发:读取主数据库的同时,写入可以追加到 WAL 文件

生命周期

  • 第一个连接打开数据库时创建
  • 最后一个连接关闭时删除
  • 如果进程异常退出,WAL 文件会保留,下次打开时自动清理

性能优势

  • 大幅提高并发性能
  • 减少磁盘 I/O(批量写入)
  • 多个读取者可以同时访问数据库

示例

1
2
/path/to/mydata.db      # 原数据库
/path/to/mydata.db-wal  # WAL 文件

3. Shared-Memory(共享内存)文件

文件命名<database-name>-shm

用途

  • 仅在 WAL 模式下使用
  • 作为 WAL 文件的索引
  • 多个进程之间共享信息,协调锁和变更跟踪

生命周期

  • 与 WAL 文件同时创建和删除
  • 在 WAL 恢复期间会根据 WAL 内容重建

无持久内容

  • 文件本身不包含任何持久化数据
  • 仅用于进程间通信

示例

1
2
3
/path/to/mydata.db      # 原数据库
/path/to/mydata.db-wal  # WAL 文件
/path/to/mydata.db-shm  # 共享内存文件

4. 其他临时文件

SQLite3 还会创建以下临时文件(不在父目录):

文件类型 用途 位置
Super-Journal 多数据库事务的原子提交 主数据库同目录
Statement Journal 单个 SQL 语句的回滚 临时目录
TEMP Database 临时表和索引 临时目录
Transient Indices 临时索引(ORDER BY, GROUP BY) 内存或临时目录
VACUUM 临时数据库 VACUUM 命令使用 临时目录

SQLite3 的 Journal Mode

SQLite3 支持多种日志模式,通过 PRAGMA journal_mode 设置:

常见模式对比

模式 日志文件 行为 适用场景
DELETE -journal 事务结束时删除日志文件(默认) 通用场景
PERSIST -journal 事务结束时清空日志头(不删除文件) 高频事务,避免文件创建开销
WAL -wal, -shm 使用 WAL 模式,支持读写并发 高并发读写场景
MEMORY 日志存储在内存中 临时数据,崩溃后允许丢失
OFF 完全不使用日志 ⚠️ 危险!崩溃必定损坏数据库

查看和设置 Journal Mode

1
2
3
4
5
6
7
8
-- 查看当前模式
PRAGMA journal_mode;

-- 切换到 WAL 模式
PRAGMA journal_mode=WAL;

-- 切换回 DELETE 模式
PRAGMA journal_mode=DELETE;

完整的权限要求

为了让 SQLite3 正常工作,需要满足以下权限要求:

文件权限

文件/目录 最小权限 说明
数据库文件 rw- (600) 读写数据库
父目录 rwx (700) 创建临时文件(journal, WAL, shm)
-journal / -wal / -shm rw- (600) SQLite 自动创建,继承 umask

检查脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/bin/bash
DB_FILE="/tools/Ansys/SmartTotemGUI/v1/.smarttotem_config_by_admin.db"
DB_DIR=$(dirname "$DB_FILE")

echo "=== SQLite3 权限检查 ==="
echo ""
echo "数据库文件:"
ls -lh "$DB_FILE"
echo ""
echo "父目录权限:"
ls -ldh "$DB_DIR"
echo ""
echo "临时文件(如果存在):"
ls -lh "${DB_FILE}-journal" 2>/dev/null || echo "  (无 journal 文件)"
ls -lh "${DB_FILE}-wal" 2>/dev/null || echo "  (无 WAL 文件)"
ls -lh "${DB_FILE}-shm" 2>/dev/null || echo "  (无 SHM 文件)"
echo ""
echo "当前用户:"
id
echo ""
echo "写入测试:"
if touch "${DB_DIR}/.test_write" 2>/dev/null; then
    echo "  ✓ 父目录可写"
    rm -f "${DB_DIR}/.test_write"
else
    echo "  ✗ 父目录不可写 - 这是问题所在!"
fi

解决方案

方案 1:修改父目录权限(推荐)

1
2
3
4
5
6
# 给父目录添加写权限
chmod 755 /tools/Ansys/SmartTotemGUI/v1/

# 验证
ls -ld /tools/Ansys/SmartTotemGUI/v1/
# 输出应该是:drwxr-xr-x

[!IMPORTANT] 这是最简单、最安全的解决方案。755 权限允许所有者写入,其他用户只读。

方案 2:使用组权限

如果多个用户需要访问:

1
2
3
4
5
6
7
8
9
10
11
12
# 创建专用组
sudo groupadd smarttotem_users

# 将用户加入组
sudo usermod -a -G smarttotem_users your_username

# 修改目录所有权和权限
sudo chgrp smarttotem_users /tools/Ansys/SmartTotemGUI/v1/
sudo chmod 775 /tools/Ansys/SmartTotemGUI/v1/

# 设置默认组权限(新文件自动继承)
sudo chmod g+s /tools/Ansys/SmartTotemGUI/v1/

方案 3:移动数据库到用户目录

1
2
3
4
5
6
# 复制数据库到用户主目录
cp /tools/Ansys/SmartTotemGUI/v1/.smarttotem_config_by_admin.db \
   ~/.smarttotem_config.db

# 修改程序配置,指向新位置
# (具体方法取决于 smarttotem_gui.py 的配置方式)

方案 4:使用符号链接(不推荐)

1
2
3
4
5
6
7
# 在用户目录创建配置
mkdir -p ~/.smarttotem/
cp .smarttotem_config_by_admin.db ~/.smarttotem/config.db

# 创建符号链接
ln -s ~/.smarttotem/config.db \
      /tools/Ansys/SmartTotemGUI/v1/.smarttotem_config_by_admin.db

[!WARNING] 符号链接方案可能导致权限混乱,不建议使用。


WAL 模式的只读数据库

在 WAL 模式下,只读数据库有特殊要求(SQLite 3.22.0+):

只读访问条件

可以读取只读 WAL 模式数据库,前提是满足以下至少一个条件

  1. -shm-wal 文件已存在且可读
  2. ✅ 父目录有写权限(可以创建 -shm-wal
  3. ✅ 使用 immutable 参数打开连接:
    1
    2
    
    import sqlite3
    conn = sqlite3.connect('file:/path/to/db.db?immutable=1', uri=True)
    

最佳实践

[!TIP] 如果要将 SQLite 数据库烧录到只读介质(如 CD-ROM),应先转换为 DELETE 模式:

1
PRAGMA journal_mode=DELETE;

排查流程

当遇到 “readonly database” 错误时:

graph TD
    A[遇到 'readonly database' 错误] --> B{检查数据库文件权限}
    B -->|无写权限| C[chmod 644 database.db]
    B -->|有写权限| D{检查父目录权限}
    
    D -->|无写权限| E[chmod 755 parent_dir/]
    D -->|有写权限| F{检查临时文件}
    
    F -->|存在 .db-journal| G[检查 journal 文件权限]
    F -->|存在 .db-wal/.db-shm| H[检查 WAL/SHM 文件权限]
    F -->|不存在临时文件| I{检查磁盘空间}
    
    G --> J[chmod 644 database.db-journal]
    H --> K[chmod 644 database.db-wal/shm]
    I -->|磁盘满| L[清理磁盘空间]
    I -->|磁盘正常| M{检查 SELinux/AppArmor}
    
    C --> N[重试操作]
    E --> N
    J --> N
    K --> N
    L --> N
    M --> N

实战案例:SmartTotem GUI

问题复现

1
2
3
4
5
6
$ ls -l /tools/Ansys/SmartTotemGUI/v1/.smarttotem_config_by_admin.db
-rwxr-xr-x 1 user group 32768 Jan 27 16:00 .smarttotem_config_by_admin.db

$ ls -ld /tools/Ansys/SmartTotemGUI/v1/
dr-xr-xr-x 5 admin admin 4096 Jan 27 15:00 /tools/Ansys/SmartTotemGUI/v1/
                                              ^^^ 注意这里!父目录没有写权限

解决过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1. 确认问题
$ touch /tools/Ansys/SmartTotemGUI/v1/.test
touch: cannot touch '/tools/Ansys/SmartTotemGUI/v1/.test': Permission denied

# 2. 修复权限(需要管理员权限)
$ sudo chmod 755 /tools/Ansys/SmartTotemGUI/v1/

# 3. 验证
$ ls -ld /tools/Ansys/SmartTotemGUI/v1/
drwxr-xr-x 5 admin admin 4096 Jan 27 15:00 /tools/Ansys/SmartTotemGUI/v1/
^^^
现在所有者可以写入了

# 4. 重新运行程序
$ python smarttotem_gui.py
# 双击配置条目 -> 成功!

最佳实践建议

1. 目录权限规划

应用程序配置目录

1
2
3
4
/opt/app/config/          # 755 (drwxr-xr-x)
├── app.db                # 644 (-rw-r--r--)
├── app.db-wal            # 644 (自动创建)
└── app.db-shm            # 644 (自动创建)

用户配置目录

1
2
~/.config/app/            # 700 (drwx------)
└── user.db               # 600 (-rw-------)

2. 使用 WAL 模式

对于多用户场景,启用 WAL 模式:

1
2
3
4
5
import sqlite3

conn = sqlite3.connect('database.db')
conn.execute('PRAGMA journal_mode=WAL')
conn.close()

优势

  • ✅ 提升并发性能
  • ✅ 减少锁竞争
  • ✅ 更好的崩溃恢复

3. 权限继承设置

使用 setgid 位确保新文件继承组权限:

1
sudo chmod g+s /shared/database/

4. 定期检查和清理

1
2
3
4
5
# 清理孤立的 journal 文件(在确认数据库未使用时)
find /path/to/databases -name "*.db-journal" -mtime +7 -delete

# 检查 WAL 文件大小(过大会影响性能)
find /path/to/databases -name "*.db-wal" -size +100M -ls

总结

关键要点

  1. 🔑 SQLite3 需要在父目录创建临时文件
  2. 📝 主要临时文件:
    • -journal (DELETE 模式)
    • -wal-shm (WAL 模式)
  3. ⚠️ “readonly database” 错误通常是父目录权限问题
  4. ✅ 解决方法:chmod 755 parent_directory/
  5. 🚀 高并发场景推荐使用 WAL 模式

权限检查清单

  • 数据库文件可读写 (600 或 644)
  • 父目录可写 (700 或 755)
  • 用户在正确的组中(如果使用组权限)
  • 无 SELinux/AppArmor 限制
  • 磁盘空间充足

参考资源

掌握 SQLite3 的临时文件机制和权限要求,可以快速定位和解决 “readonly database” 错误,确保应用程序的数据库操作稳定可靠。