在 Linux 环境下使用 SQLite3 数据库时,经常会遇到一个令人困惑的错误:明明数据库文件本身有写权限,却仍然提示 “attempt to write a readonly database”。本文基于 SQLite 官方文档,深入解析这个错误的根本原因以及 SQLite3 的临时文件机制。
问题背景
在使用 SmartTotem GUI 工具时,遇到了以下场景:
环境配置:
- 数据库文件:
/tools/Ansys/SmartTotemGUI/v1/.smarttotem_config_by_admin.db - 文件权限:755(所有者可读写执行)
- 所有者:当前用户账号
操作流程:
- 启动
smarttotem_gui.py - 在 User Configurations 中双击某个条目
- 程序尝试修改数据库
错误信息:
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 模式数据库,前提是满足以下至少一个条件:
- ✅
-shm和-wal文件已存在且可读 - ✅ 父目录有写权限(可以创建
-shm和-wal) - ✅ 使用
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
总结
关键要点:
- 🔑 SQLite3 需要在父目录创建临时文件
- 📝 主要临时文件:
-journal(DELETE 模式)-wal和-shm(WAL 模式)
- ⚠️ “readonly database” 错误通常是父目录权限问题
- ✅ 解决方法:
chmod 755 parent_directory/ - 🚀 高并发场景推荐使用 WAL 模式
权限检查清单:
- 数据库文件可读写 (600 或 644)
- 父目录可写 (700 或 755)
- 用户在正确的组中(如果使用组权限)
- 无 SELinux/AppArmor 限制
- 磁盘空间充足
参考资源:
掌握 SQLite3 的临时文件机制和权限要求,可以快速定位和解决 “readonly database” 错误,确保应用程序的数据库操作稳定可靠。