在本地进行批量读写测试时,发现:
- 读取完全正常,无任何异常
- 批量写入(如并发评论/资料更新)时频繁触发锁库,导致写入失败
- 错误表现为
database is locked
,这是 SQLite 的写锁独占机制在起作用
第一轮优化:启用 WAL 模式
通过 DeepSeek 建议,首先尝试 Write-Ahead Logging (WAL) 模式:
db, err := sql.Open("sqlite", "file:data.db?_journal_mode=WAL")
效果:
- 相比默认的
DELETE
日志模式,WAL 允许读写并发(读不阻塞写,写不阻塞读) - 但多个写入仍然会互相阻塞,高并发下依然会出现锁库
第二轮优化:批量事务合并
DeepSeek 进一步建议:
- 合并多个写入为单个事务,减少锁竞争
- 按阈值或定时触发写入(如每 100 条评论或每 5 秒提交一次)
实现代码示例:
// 使用事务批量写入
func batchUpdate(updates []Update) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback() // 出错时回滚
for _, u := range updates {
_, err = tx.Exec("UPDATE books SET views = views + ? WHERE id = ?", u.Views, u.ID)
if err != nil {
return err
}
}
return tx.Commit() // 统一提交
}
效果:
- 减少了锁冲突频率,但极端并发下仍会偶发锁库
终极方案:内存缓存 + 异步持久化
最终采纳的解决方案:
- 内存缓存热点数据(如书籍信息、用户资料)
- 写入先进入内存队列,异步批量落盘
- 高频更新操作(如点赞、浏览计数)暂存内存
- 通过定时任务或阈值触发数据库写入
- 关键数据绕过缓存直写数据库
实现架构:
客户端请求 → 内存缓存(读写) → 定期同步 → SQLite
↓
关键数据直写
代码示例:
var (
cache = make(map[int]Book) // 简易内存缓存
mu sync.RWMutex
)
// 读取优先走缓存
func GetBook(id int) (Book, error) {
mu.RLock()
defer mu.RUnlock()
if book, ok := cache[id]; ok {
return book, nil
}
// 缓存未命中时查数据库
return fetchFromDB(id)
}
// 写入先更新缓存,异步持久化
func UpdateViews(id int) {
mu.Lock()
defer mu.Unlock()
cache[id].Views++ // 内存更新
// 触发异步落盘(实际可合并更多操作)
go func() {
_, err := db.Exec("UPDATE books SET views = views + 1 WHERE id = ?", id)
if err != nil {
log.Printf("异步写入失败: %v", err)
}
}()
}
优化结果
- 日志监控显示:
database is locked
错误完全消失 - 吞吐量提升:并发写入请求处理速度提高 10 倍+
- 代价:极端情况下可能丢失少量非关键数据(如浏览计数),但对业务无影响
经验总结
- SQLite 的并发瓶颈主要在写入锁竞争,WAL 模式治标不治本
- 内存缓存 + 异步持久化是小型项目的高效解决方案
- 关键数据仍需保证实时持久化
- 未来可考虑改用 Redis + SQLite 组合,进一步分离读写负载
通过这次优化,深刻体会到:没有完美的技术方案,只有适合场景的权衡。AI 的建议提供了关键方向,但最终仍需结合实际测试调整。
使用Go 写的吗,SQLite 适合少写 多读的场景,例如企业官网,小说章节存储(只有作者是写入 其他人都是读取)之类。个人建议 可以考虑 使用 pgsql 代替 sqlite。
SQLite 批量写操作可以用 Go 封装一个队列 需要的写的内容 直接推送到这个队列里面,然后只通过队列进行批量的写操作。
我使用 SQLite 存业务日志的,可以实现 每秒 3w条左右的落盘速度。
我之前写过一个博文 是 PHP 批量写入 SQLite 的 不过里面的思路可以参考一下 https://www.dbkuaizi.com/archives/154.html