go中database/sql不产生死锁,但因事务未提交、rows未defer关闭、sql顺序混乱,会触发mysql/sqlite底层死锁或invalid connection等错误;修复核心是管住事务生命周期、查询结果集释放和sql执行顺序。

Go 的 database/sql 本身不产生“死锁”,但长连接场景下因资源未释放、事务未提交、SQL 执行顺序混乱,会触发 MySQL/SQLite 等底层数据库的死锁,或表现为 invalid connection、database is locked、Deadlock found when trying to get lock 等错误。修复核心不是“加锁控制连接”,而是管住三件事:事务生命周期、查询结果集释放、SQL 执行顺序。
MySQL 死锁:重试 + 统一更新顺序是刚需
MySQL InnoDB 检测到循环等待后会主动回滚一个事务,并返回错误码 1213(Deadlock found when trying to get lock)。你不该忽略它,也不该静默重试整个业务流程。
- 必须在应用层显式识别该错误:检查
err是否包含"1213"或用驱动提供的判断函数(如mysql.MySQLError类型断言) - 重试只针对事务体内部逻辑,且最多 3 次;每次重试前
time.Sleep随机抖动(如time.Millisecond * time.Duration(rand.Intn(50)+50)),避免雪崩重试 - 所有涉及多行或多表的 UPDATE/DELETE,强制按主键升序执行:例如
UPDATE users SET status=1 WHERE id IN (100, 50) ORDER BY id;应用层也应先对 ID 切片排序再拼 SQL - 避免在事务中调用外部 HTTP、写文件、复杂计算——这些会拉长锁持有时间,显著提高死锁概率
SQLite database is locked:根源在 rows.Close() 没 defer
SQLite 是文件锁模型,*sql.Rows 不关闭 → 底层连接无法归还 → 写锁不释放 → 后续任何写操作都卡在 database is locked。这不是并发高导致的,而是单 goroutine 写错位置就触发。
-
rows, err := db.Query(...)后必须立刻defer rows.Close(),哪怕你确定只查一条或无数据 - 禁止把
rows.Close()放在if rows.Next()块内,或放在tx.Commit()之后——一旦 panic 或提前 return,Close()就被跳过 - 即使使用
db.QueryRow(),其内部仍可能打开rows;若你用QueryRow().Scan()失败,也要注意错误是否来自未关闭的隐式资源(实际中较少,但Query场景必须严守) - 不要依赖
db.Close()来清理活跃查询;它只关闭空闲连接,对已发出但未 Close 的rows无效
invalid connection 报错:往往是死锁后连接被服务端踢掉
这个错误不是 Go 客户端主动断开,而是 MySQL 在事务因死锁被回滚后,又遇到网络超时、wait_timeout 触发、或客户端读包时连接已失效,最终在 readPacket() 阶段抛出 invalid connection。它是个“果”,不是“因”。
- 查日志重点看它前面有没有
Deadlock found或timeout相关输出;有,则优先解决死锁逻辑 - 确认 MySQL 的
innodb_lock_wait_timeout(默认 50 秒)和应用层context.WithTimeout是否匹配;若 DB 超时短于应用上下文,事务还没来得及重试就被 kill,容易连带报 invalid conn - 避免在事务中执行
db.Query后不消费全部结果(比如只取前 10 行就 break);未读完的rows会阻塞连接释放,间接导致后续获取连接失败 - 连接池参数要合理:
SetMaxOpenConns不宜过大(尤其 SQLite),SetMaxIdleConns建议设为与MaxOpenConns相同,减少空闲连接堆积
真正难处理的从来不是“怎么加锁”,而是“哪一行没 close”“哪条 SQL 没排序”“哪个 sleep 忘了加 jitter”。这些点不出现在架构图里,但压测一跑就全暴露。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











