DSN必须加timezone=UTC和parseTime=true,否则time.Time字段会因时区不一致导致静默偏差;事务需显式commit/rollback,避免defer误用;批量写入须按Region分片,禁用单主键递增;HTTP/gRPC调用不可在事务内执行;需识别TiDB特有可重试错误码并重试。

DSN必须加 timezone=UTC 和 parseTime=true
不加这两个参数,time.Time 字段会出错——不是报错,而是静默偏差:插入 NOW() 或扫描 created_at 时,Golang 驱动按本地时区解析,TiDB 的 TSO 时间戳却是 UTC 纳秒级时间。结果就是时间字段偏移几小时,事务内判断失效、MVCC 版本错乱、甚至幻读。
正确写法:root:@tcp(127.0.0.1:4000)/test?timezone=UTC&parseTime=true
-
timezone=UTC强制驱动使用 UTC 解析所有时间类型,与 PD 授时对齐 -
parseTime=true让driver.ValueConverter把DATETIME/TIMESTAMP转成time.Time,否则 Scan 到 struct 会 panic 或返回空字符串 - 别写
loc=Local或loc=Asia%2FShanghai—— 这和timezone=UTC冲突,且 TiDB 不依赖客户端时区做计算
事务不能 defer rollback,必须显式判错后 commit/rollback
TiDB 的两阶段提交(2PC)耗时不稳定,Prepare 阶段可能卡在跨 Region 协调上。常见错误是写 defer tx.Rollback() 就完事,但一旦 tx.Commit() 失败,tx 仍处于 open 状态,连接不释放,还可能留下未清理的锁。
正确模式:
立即学习“go语言免费学习笔记(深入)”;
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
if tx != nil {
tx.Rollback()
}
}()
// ... 执行 SQL
err = tx.Commit()
if err != nil {
return err // 不再 defer rollback,这里已 rollback 过
}
tx = nil // 标记已提交,防止 defer 再 rollback- 事务开始后,
tx必须全程非 nil,直到明确Commit()成功或Rollback()完成 - 不要在事务内调用其他 service 方法并开启新
db.Begin()—— TiDB 不支持嵌套事务或 SAVEPOINT,第二次 Begin 会隐式提交前一个事务 - 所有
ctx必须带超时:ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second),避免长阻塞拖垮连接池
批量写入必须按 Region 分片,禁止单主键递增
TiDB 的数据按 Region 切分,默认 96MB/Region,由 PD 调度。如果用自增 id 或 UUID(v1/v4)作为主键,所有新行都写入同一个 Region,形成热点,吞吐骤降,PD 无法及时打散。
解决方案:
- 主键用
SHARD_ROW_ID_BITS分片:CREATE TABLE orders (id BIGINT PRIMARY KEY) SHARD_ROW_ID_BITS=4;→ 生成 16 个逻辑分片,打散写入 - 批量插入优先用多值语句:
INSERT INTO t(a,b) VALUES (?,?),(?,?),(?,?),而不是循环db.Exec - 如需按业务分片(如 user_id),用
consistenHash.GetNode(fmt.Sprintf("%d", userID))路由到对应 TiDB Server,再走本地事务 - 避免在事务中执行 HTTP 请求或调用 gRPC —— 网络延迟会拉长 2PC 时间,增加 Prepare 阶段失败概率
错误处理要识别 TiDB 特有可重试错误码
TiDB 在网络抖动、Region 切换、PD 选举期间会返回临时性错误,比如 ERROR 9005 (HY000): Region is unavailable 或 ERROR 8022 (HY000): Resolve lock timeout。这些不是 bug,而是分布式系统正常反馈,需要手动重试,而非直接上报 panic。
建议封装重试逻辑:
for i := 0; i < 3; i++ {
_, err := tx.ExecContext(ctx, "UPDATE accounts SET balance = ? WHERE id = ?", newBal, id)
if err == nil {
return nil
}
if tidb.IsRetryableError(err) { // 使用 github.com/pingcap/tidb/pkg/util/errors
time.Sleep(time.Millisecond * 100 * time.Duration(i+1))
continue
}
return err
}- 不要依赖
errors.Is(err, sql.ErrNoRows)判断业务逻辑 —— TiDB 的快照隔离下,SELECT ... FOR UPDATE可能返回空,但不代表数据不存在,只是当前快照没它 -
RowsAffected()在 TiDB 中默认返回匹配行数(clientFoundRows=true),不是影响行数;不加该 DSN 参数,永远是 0 - 监控重点不是 QPS,而是
tidb_tikvclient_region_err_total和tidb_session_transaction_duration_seconds—— 前者暴露调度问题,后者暴露事务卡点
实际部署时,最容易被忽略的是 PD 时钟同步精度。TiDB 要求所有节点(包括 Go 应用所在机器)与 PD 的时钟偏差 ≤500ms,否则 TSO 分配异常会导致事务拒绝或重复提交。别只盯着代码,先跑 ntpq -p 或 chronyc tracking 确认 NTP 同步状态。


















