feat: expose mysql transaction retry metrics

This commit is contained in:
2026-09-20 21:52:17 +08:00
parent a1e6fcc07f
commit ac8cc1260b
10 changed files with 55 additions and 4 deletions

View File

@@ -5,6 +5,7 @@ import (
"context"
"database/sql"
"errors"
"sync/atomic"
"time"
driver "github.com/go-sql-driver/mysql"
@@ -53,6 +54,24 @@ func Open(ctx context.Context, dsn string, maxOpen, maxIdle int) (*sql.DB, error
const transactionRetryLimit = 2
var transactionRetries atomic.Uint64
var transactionRetryExhausted atomic.Uint64
// TransactionStats 是本进程提交前事务重试的累计快照。
// 计数器只记录明确的 MySQL 锁等待超时或死锁回调失败,不记录提交回执不明。
type TransactionStats struct {
Retries uint64 // 已实际执行的重试次数。
Exhausted uint64 // 达到重试上限的事务次数。
}
// ReadTransactionStats 返回本进程事务重试计数,不访问数据库。
func ReadTransactionStats() TransactionStats {
return TransactionStats{
Retries: transactionRetries.Load(),
Exhausted: transactionRetryExhausted.Load(),
}
}
// WithTx retries only a callback failure caused by a MySQL deadlock or lock wait
// timeout before COMMIT. It never retries Begin/Commit errors because a commit
// response may be lost after the database has already applied the transaction;
@@ -61,11 +80,15 @@ func WithTx(ctx context.Context, db *sql.DB, fn func(*sql.Tx) error) error {
for attempt := 0; attempt <= transactionRetryLimit; attempt++ {
err, retryable := withTxAttempt(ctx, db, fn)
if err == nil || !retryable || attempt == transactionRetryLimit {
if retryable && attempt == transactionRetryLimit {
transactionRetryExhausted.Add(1)
}
return err
}
if err := waitTransactionRetry(ctx, attempt); err != nil {
return err
}
transactionRetries.Add(1)
}
return errors.New("transaction retry loop exhausted")
}