#背景
把 SQLite 用在正式站点上,被问得最多的一句是:「并发写会不会丢数据?」
这个担心不是空穴来风。SQLite 默认是 journal_mode=delete,写操作会锁整库;一旦有并发写入,后到的会立刻拿到 SQLITE_BUSY。如果你的代码用 try/catch 把异常吞掉(很多"访问统计"就是这种写法),表现就是静默丢数据——页面正常、日志干净、统计慢慢偏少,等到发现时已经无从对账。
与其争论"能不能扛",不如测出来。
#做法
#1. 开 WAL,并把等待时间交给数据库
// 连接建立后立刻执行(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 实现,零依赖:
# 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. 关键一步:核对写入条数
压测跑完,别只看延迟,去数一下库里的行数对不对:
sqlite3 storage/database/vabora.sqlite 'select count(*) from v_visits;'#结果
本机实测(PHP 8.5 + SQLite,8 worker,开启 gzip):
| 场景 | 并发 | 请求 | 成功 | 吞吐 | P50 | P95 | P99 | 写入 |
|---|---|---|---|---|---|---|---|---|
| 首页 | 20 | 300 | 300 | 952/s | 18.3ms | 31.6ms | 39.3ms | 301/300 ✓ |
| 服务列表 | 50 | 800 | 800 | 1026/s | 44.6ms | 61.6ms | 82.3ms | 801/800 ✓ |
| 文章详情 | 100 | 1000 | 1000 | 1295/s | 71.4ms | 88.3ms | 108.8ms | 1001/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可以把数据搬过去,但架构该换的时候不要硬撑。
评论
还没有评论,来说点什么吧。