云寒YUNHAN / FIELD NOTES
← 返回文章目录
数据与存储 ·

SQLite 的可靠性,从事务边界开始

WAL 不是多写者魔法。理解锁、短事务和一致性备份,才能用好这个轻量数据库。

先理解单写者约束

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 根目录。含个人信息的数据库还需要加密和最小访问权限。

本文为技术原理与示例说明,不代表任何实际生产环境。示例运行前请在隔离环境验证,按具体版本与业务约束调整。
继续探索其他文章 →