先理解单写者约束
SQLite 很适合单机应用和中小规模读多写少服务。WAL 模式允许读者与写者更好地并发,但同一时刻仍只有一个写者。把 WAL 理解成无限写入并发,会在流量增加后遭遇 database is locked。更有效的优化往往是缩短事务、建立适当索引并避免在事务中等待网络。
在真正的业务边界开启事务
转账示例需要同时减少来源余额、增加目标余额,并确保来源余额足够。下面使用整数表示最小货币单位,且 amount 必须为正整数;连接以显式事务模式打开。BEGIN IMMEDIATE 尽早申请写锁,让竞争在业务开始前暴露。目标不存在时必须回滚,不能悄悄丢失金额。
import sqlite3
def transfer(db, source, target, amount):
if not isinstance(amount, int) or amount <= 0 or source == target:
raise ValueError('invalid transfer')
db.execute('BEGIN IMMEDIATE')
try:
changed = db.execute(
'UPDATE accounts SET balance=balance-? WHERE id=? AND balance>=?',
(amount, source, amount)).rowcount
if changed != 1:
raise ValueError('missing source or insufficient balance')
if db.execute('UPDATE accounts SET balance=balance+? WHERE id=?',
(amount, target)).rowcount != 1:
raise ValueError('missing target')
db.commit()
except Exception:
db.rollback()
raise
db = sqlite3.connect('example.db', isolation_level=None, timeout=3)忙等待不等于吞掉错误
timeout 或 busy_timeout 允许短暂等待锁释放,不会解决长期持锁的问题。对于可重试事务,应在事务整体回滚之后重试,并有总预算和次数限制。支付类业务还需要唯一业务单号去重;事务原子性不能防止客户端将同一业务提交两遍。
运行中的备份不要只复制主文件
WAL 模式下已提交数据可能仍位于 -wal 文件中。只复制 example.db 可能丢失近期提交;同时粗暴复制三个文件也未必产生一致快照。使用 SQLite backup API 更稳妥,让数据库引擎协调一致性。
with sqlite3.connect('example.db') as source:
with sqlite3.connect('backup.db') as destination:
source.backup(destination)
result = destination.execute('PRAGMA integrity_check').fetchone()[0]
assert result == 'ok'恢复演练才是备份的证明
integrity_check 验证结构完整性,但不能证明业务数据符合预期。把备份恢复到隔离目录,执行表数量、关键约束和样本查询,确认应用可以启动。备份要记录时间、大小和校验值,并避免把数据库直接放在 Web 根目录。含个人信息的数据库还需要加密和最小访问权限。
本文为技术原理与示例说明,不代表任何实际生产环境。示例运行前请在隔离环境验证,按具体版本与业务约束调整。
继续探索其他文章 →