跳到主要内容
我的站点
首页 归档 分类 标签 案例 服务 关于 聊聊需求

SQLite 并发写入实测:WAL、busy_timeout 与 1000 次请求零丢失

把 SQLite 放上生产后,最常见的担心是「并发写入会不会丢数据」。这篇给出可复现的压测方法与本机实测数字:100 并发 1000 次请求,写入零丢失,P95 88ms。

SQLite 并发写入实测:WAL、busy_timeout 与 1000 次请求零丢失 封面

#背景

把 SQLite 用在正式站点上,被问得最多的一句是:「并发写会不会丢数据?」

这个担心不是空穴来风。SQLite 默认是 journal_mode=delete,写操作会锁整库;一旦有并发写入,后到的会立刻拿到 SQLITE_BUSY。如果你的代码用 try/catch 把异常吞掉(很多"访问统计"就是这种写法),表现就是静默丢数据——页面正常、日志干净、统计慢慢偏少,等到发现时已经无从对账。

与其争论"能不能扛",不如测出来。

#做法

#1. 开 WAL,并把等待时间交给数据库

php
// 连接建立后立刻执行(SQLite 专用)
PRAGMA journal_mode = WAL;        // 读写不再互斥:读不阻塞写,写不阻塞读
PRAGMA busy_timeout = 5000;       // 拿不到写锁时等 5 秒,而不是立刻抛 SQLITE_BUSY
PRAGMA synchronous = NORMAL;      // WAL 下的推荐值:崩溃安全性够用,写入快得多
PRAGMA foreign_keys = ON;

三个参数里,busy_timeout 是决定"丢不丢数据"的那一个。WAL 让读写不互斥,但写与写之间仍然串行;有了超时等待,突发并发只是"排队变慢",而不是"直接失败"。

#2. 压测要带真实写入

只压 GET 静态页没有意义——要压的是「一次请求写一次库」的路径(本站每次前台访问都会写一条访问记录)。压测脚本 tools/bench.php 用 curl_multi 实现,零依赖:

bash
# 8 个 worker 的开发服务器(模拟 PHP-FPM 的多进程)
PHP_CLI_SERVER_WORKERS=8 php -S 127.0.0.1:8000 -t public tools/serve-router.php

php tools/bench.php http://127.0.0.1:8000/services 50 800
php tools/bench.php http://127.0.0.1:8000/posts/xxx 100 1000

#3. 关键一步:核对写入条数

压测跑完,别只看延迟,去数一下库里的行数对不对:

bash
sqlite3 storage/database/vabora.sqlite 'select count(*) from v_visits;'

#结果

本机实测(PHP 8.5 + SQLite,8 worker,开启 gzip):

场景并发请求成功吞吐P50P95P99写入
首页20300300952/s18.3ms31.6ms39.3ms301/300 ✓
服务列表508008001026/s44.6ms61.6ms82.3ms801/800 ✓
文章详情100100010001295/s71.4ms88.3ms108.8ms1001/1000 ✓

(+1 是预热请求。写入数不少于请求数即为零丢失。)

100 并发下 P95 仍在 100ms 以内,没有一条 SQLITE_BUSY,没有一条统计数据丢失。对一台单机博客/企业官网来说,这个余量足够了。

#取舍

  • synchronous=NORMAL 而不是 FULL:WAL 下 NORMAL 在断电时最多丢失最近若干个事务,但不会损坏数据库文件。官网的访问统计丢几条无所谓,换来的是写入吞吐明显提升;如果记的是订单,请改回 FULL。
  • busy_timeout=5000 而不是更大:等 5 秒已经远超正常写入耗时(毫秒级)。真等到 5 秒,说明有长事务在卡锁,此时让请求失败并暴露问题,比无限等待更健康。
  • SQLite 的边界:单机、单写者场景非常合适;一旦要跨机器横向扩展、或者有持续的高频写入(每秒几百次以上),就换成 MySQL/PostgreSQL——php vabora database:switch 可以把数据搬过去,但架构该换的时候不要硬撑。
最后更新: 本文采用 CC BY-NC-SA 4.0 许可,转载请注明出处与作者。
related

相关文章

评论

还没有评论,来说点什么吧。

发表评论

评论需站长审核后展示 · 最多 1000 字