
在使用 Go 的 database/sql 包迭代查询结果时,直接在循环中执行 UPDATE 会导致 SQLite 数据库锁冲突;正确做法是将 SELECT 和 UPDATE 操作统一置于显式事务中,从而避免锁竞争并提升批量更新效率。
在使用 go 的 `database/sql` 包迭代查询结果时,直接在循环中执行 update 会导致 sqlite 数据库锁冲突;正确做法是将 select 和 update 操作统一置于显式事务中,从而避免锁竞争并提升批量更新效率。
在 Go 中操作 SQLite(或其他支持事务的数据库)时,一个常见误区是:在未关闭 *sql.Rows 的情况下,尝试通过同一数据库连接(*sql.DB)执行写操作(如 db.Exec),这会触发“database is locked”错误——尤其在 mattn/go-sqlite3 驱动下,因 SQLite 的串行化事务模型要求读写操作需在同一事务上下文中协调。
*根本解法:使用显式事务(`sql.Tx`)统一管理读与写**
*sql.Tx 提供了隔离的执行环境,允许你在同一事务中安全地混合 Query(读)和 Exec(写),且所有操作共享同一个底层连接与锁上下文。修改原代码的关键步骤如下:
- 显式开启事务;
- 在事务对象上执行
Query获取结果集; - 遍历
rows,对每行计算新值并调用tx.Exec更新; - 最后统一提交(或回滚)事务。
✅ 正确示例代码:
tx, err := db.Begin()
if err != nil {
log.Fatal("无法开启事务:", err)
}
defer func() {
if err != nil {
tx.Rollback() // 出错时回滚
}
}()
rows, err := tx.Query("SELECT id, field1, field2 FROM table")
if err != nil {
log.Fatal("查询失败:", err)
}
defer rows.Close()
for rows.Next() {
var id, field1, field2 int
if err = rows.Scan(&id, &field1, &field2); err != nil {
log.Fatal("扫描行失败:", err)
}
// 根据业务逻辑动态计算新值(每行独立)
field1 += someFunc(id) // 建议传入 id 等上下文以增强可预测性
field2 += someOtherFunc(id)
// 在同一事务中执行更新
_, err = tx.Exec("UPDATE table SET field1 = ?, field2 = ? WHERE id = ?",
field1, field2, id)
if err != nil {
log.Fatal("更新失败:", err)
}
}
// 所有更新成功后提交事务
if err = tx.Commit(); err != nil {
log.Fatal("提交事务失败:", err)
}
⚠️ 重要注意事项:
- 不要使用
db.Exec或db.Query,必须全部使用tx.*方法; -
rows.Close()必须在事务提交/回滚前调用(但defer rows.Close()在tx.Commit()后仍安全,因*sql.Rows仅持有事务引用); - 若更新量极大(如数十万行),建议分批处理(例如每 1000 行
Commit()一次并新建事务),防止事务日志过大或长时间持锁; - SQLite 默认为
DEFERRED事务,若需更强一致性,可指定BEGIN IMMEDIATE(通过tx, _ := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})实现,但 SQLite 仅部分支持); -
someFunc()等外部函数应避免副作用、确保幂等性,便于调试与重试。
该方案不仅解决了锁冲突问题,还显著提升了性能:相比每次更新都建立新连接或等待锁释放,事务内执行将 I/O 和锁开销降至最低,是 database/sql 生态下符合标准、可移植且高效的实践方式。











