fix: retry precommit mysql lock conflicts

This commit is contained in:
2026-09-20 21:47:30 +08:00
parent 30a8081bd9
commit a1e6fcc07f
7 changed files with 170 additions and 8 deletions

View File

@@ -51,16 +51,56 @@ func Open(ctx context.Context, dsn string, maxOpen, maxIdle int) (*sql.DB, error
return db, nil
}
// WithTx does not replay callbacks: a failed commit may have succeeded remotely.
// Callers reconcile by stable operation identity before retrying an ambiguous write.
const transactionRetryLimit = 2
// 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;
// callers must reconcile such an ambiguous write by stable operation identity.
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 {
return err
}
if err := waitTransactionRetry(ctx, attempt); err != nil {
return err
}
}
return errors.New("transaction retry loop exhausted")
}
func withTxAttempt(ctx context.Context, db *sql.DB, fn func(*sql.Tx) error) (error, bool) {
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
if err != nil {
return err
return err, false
}
defer tx.Rollback()
if err := fn(tx); err != nil {
return err
return err, retryableTransactionError(err)
}
if err := tx.Commit(); err != nil {
return err, false
}
return nil, false
}
func retryableTransactionError(err error) bool {
var mysqlErr *driver.MySQLError
if !errors.As(err, &mysqlErr) {
return false
}
return mysqlErr.Number == 1205 || mysqlErr.Number == 1213
}
func waitTransactionRetry(ctx context.Context, attempt int) error {
delay := time.Duration(attempt+1) * 25 * time.Millisecond
timer := time.NewTimer(delay)
defer timer.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return nil
}
return tx.Commit()
}