Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Las transacciones agrupan múltiples operaciones en una unidad atómica. O bien todas las operaciones tienen éxito (se comprometen) o ninguna de ellas entra en vigor (rollback). Este artículo explica cómo utilizar las transacciones con el go-mssqldb controlador, incluyendo niveles de aislamiento, manejo de errores y patrones para aplicaciones de producción.
Los ejemplos de este artículo se aplican a la base de datos de ejemplo AdventureWorks2025 . Los ejemplos orientados a escritura tienen como objetivo HumanResources.Department, Production.ProductCategory, Production.ProductSubcategory, y Production.ProductInventory.
Iniciar una transacción
Úsalo db.BeginTx para iniciar una transacción. El valor devuelto *sql.Tx mantiene asignada una única conexión del grupo durante toda la transacción:
tx, err := db.BeginTx(ctx, nil) // nil uses the default isolation level
if err != nil {
return err
}
defer tx.Rollback() // No-op if tx.Commit() succeeds first.
_, err = tx.ExecContext(ctx, "INSERT INTO HumanResources.Department (Name, GroupName) VALUES (@p1, @p2)",
sql.Named("p1", name),
sql.Named("p2", groupName))
if err != nil {
return err
}
return tx.Commit()
Important
Llama siempre a defer tx.Rollback() inmediatamente después de BeginTx. Si Commit() tiene éxito, el diferido Rollback() es un no-op. Si se produce algún error antes de Commit(), la llamada diferida a Rollback() garantiza que la transacción no permanezca abierta y que la conexión no se devuelva al pool sin confirmar.
Niveles de aislamiento
SQL Server soporta varios niveles de aislamiento que controlan cómo interactúan las transacciones concurrentes. Establezca el nivel de aislamiento en sql.TxOptions:
tx, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelReadCommitted,
})
Comparación de niveles de aislamiento
| Nivel de aislamiento | Lecturas sucias | Lecturas no repetibles | Lecturas fantasma | Impacto sobre el rendimiento | Se utiliza cuando |
|---|---|---|---|---|---|
sql.LevelReadUncommitted |
Sí | Sí | Sí | Menor coste fijo | Recuentos aproximados, paneles de monitorización. La precisión de los datos no es fundamental. |
sql.LevelReadCommitted |
No | Sí | Sí | Predeterminado. Es bueno para la mayoría de las cargas de trabajo. | Cargas de trabajo generales de OLTP. El punto de partida por defecto y recomendado. |
sql.LevelRepeatableRead |
No | No | Sí | Moderado. Mantiene los bloqueos durante más tiempo. | Lecturas que deben mostrar valores consistentes para las mismas filas dentro de la transacción. |
sql.LevelSerializable |
No | No | No | Máximo. Los bloqueos de intervalo bloquean las inserciones concurrentes. | Transacciones financieras, gestión de inventarios, cualquier entorno en el que las lecturas fantasma sean inaceptables. |
sql.LevelSnapshot |
No | No | No | Utiliza el versionado por filas en tempdb. Sin bloqueos. |
Cargas de trabajo con mucha lectura que necesitan coherencia en un momento en el tiempo sin bloquear a los escritores. |
Note
sql.LevelSnapshot requiere habilitar el aislamiento de instantáneas en la base de datos: ALTER DATABASE AdventureWorks2025 SET ALLOW_SNAPSHOT_ISOLATION ON.
Ejemplo: lectura confirmada frente a serializable
Especifica el nivel de aislamiento en sql.TxOptions:
// Read Committed (default) - suitable for most operations.
tx1, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelReadCommitted,
})
// Serializable - prevents phantom reads in financial calculations.
tx2, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelSerializable,
})
Enrutamiento de solo lectura
El controlador no soporta sql.TxOptions.ReadOnly. Si apruebas ReadOnly: true, BeginTx devuelve un error.
Para el enrutamiento de solo lectura de AlwaysOn, establece applicationintent=ReadOnly en la cadena de conexión al abrir la conexión:
sqlserver://listener.example.com?database=AdventureWorks2025&applicationintent=ReadOnly
Esto es un ajuste a nivel de conexión. Puede enrutar las sesiones elegibles a una secundaria legible, pero no convierte una transacción existente en solo lectura. Utiliza una conexión dedicada de solo lectura o credenciales de mínimo privilegio para cargas de trabajo que no deben escribir datos.
Gestión de errores en transacciones
Gestiona cuidadosamente los errores dentro de las transacciones. Cuando falla alguna sentencia, la transacción debe deshacerse por completo. No intentes continuar con otras afirmaciones tras un error:
func createCategory(ctx context.Context, db *sql.DB, category Category) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin transaction: %w", err)
}
defer tx.Rollback()
var categoryId int64
err = tx.QueryRowContext(ctx, `
INSERT INTO Production.ProductCategory (Name)
OUTPUT INSERTED.ProductCategoryID
VALUES (@name)`,
sql.Named("name", category.Name)).Scan(&categoryId)
if err != nil {
return fmt.Errorf("insert category: %w", err)
}
// Insert subcategories under the new category.
for _, sub := range category.Subcategories {
_, err = tx.ExecContext(ctx, `
INSERT INTO Production.ProductSubcategory (ProductCategoryID, Name)
VALUES (@catId, @name)`,
sql.Named("catId", categoryId),
sql.Named("name", sub.Name))
if err != nil {
return fmt.Errorf("insert subcategory %s: %w", sub.Name, err)
}
}
if err = tx.Commit(); err != nil {
return fmt.Errorf("commit category: %w", err)
}
return nil
}
Si el procedimiento por lotes o almacenado se ejecuta con SET XACT_ABORT ON, trata cualquier error de sentencia como terminal para la transacción. Revierte inmediatamente y no intentes más instrucciones ni Commit(). Las versiones actuales de controladores detectan transacciones abortadas por el servidor y devuelven un error en lugar de permitir un commit parcial silencioso.
Puntos de retorno
Los puntos de recuperación crean puntos intermedios de reversión dentro de una transacción. SQL Server soporta puntos de guardado de forma nativa. Como el paquete database/sql de Go no expone directamente los puntos de restauración, ejecútalos como SQL sin procesar mediante la transacción:
func createCategoryWithOptionalSubcategory(ctx context.Context, db *sql.DB, category Category) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
err = tx.QueryRowContext(ctx,
"INSERT INTO Production.ProductCategory (Name) OUTPUT INSERTED.ProductCategoryID VALUES (@p1)",
sql.Named("p1", category.Name)).Scan(&category.Id)
if err != nil {
return err
}
// Try to add a subcategory. If it fails, roll back only the subcategory part.
_, err = tx.ExecContext(ctx, "SAVE TRANSACTION AddSubcategory")
if err != nil {
return err
}
_, err = tx.ExecContext(ctx,
"INSERT INTO Production.ProductSubcategory (ProductCategoryID, Name) VALUES (@p1, @p2)",
sql.Named("p1", category.Id),
sql.Named("p2", category.DefaultSubcategory))
if err != nil {
// Roll back only the subcategory; the category insert is preserved.
_, rbErr := tx.ExecContext(ctx, "ROLLBACK TRANSACTION AddSubcategory")
if rbErr != nil {
return fmt.Errorf("rollback savepoint: %w (original: %w)", rbErr, err)
}
log.Printf("Subcategory %q failed, proceeding without it: %v",
category.DefaultSubcategory, err)
}
return tx.Commit()
}
Note
SAVE TRANSACTION <name> Crea el punto de guardado.
ROLLBACK TRANSACTION <name> revierte hasta ese punto de guardado sin finalizar la transacción externa.
ROLLBACK sin nombre revierte toda la transacción.
Manejo de bloqueo
SQL Server resuelve bloqueos terminando una de las transacciones competidoras (la víctima del bloqueo) y devolviendo el error 1205. La transacción terminada se revierte automáticamente por el servidor.
Detectar y reintentar interbloqueos
Revisa el error 1205 y vuelve a intentar toda la transacción con un breve retraso:
import (
"errors"
"fmt"
"time"
"github.com/microsoft/go-mssqldb"
)
func isDeadlock(err error) bool {
var mssqlErr mssql.Error
return errors.As(err, &mssqlErr) && mssqlErr.Number == 1205
}
func withDeadlockRetry(ctx context.Context, db *sql.DB, maxRetries int,
fn func(ctx context.Context, tx *sql.Tx) error) error {
for attempt := 0; attempt < maxRetries; attempt++ {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
err = fn(ctx, tx)
if err != nil {
tx.Rollback()
if isDeadlock(err) && attempt < maxRetries-1 {
// Wait briefly before retrying.
delay := time.Duration(attempt+1) * 50 * time.Millisecond
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(delay):
}
continue
}
return err
}
if err = tx.Commit(); err != nil {
if isDeadlock(err) && attempt < maxRetries-1 {
delay := time.Duration(attempt+1) * 50 * time.Millisecond
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(delay):
}
continue
}
return err
}
return nil
}
return fmt.Errorf("transaction failed after %d deadlock retries", maxRetries)
}
Usa el envoltorio de reintento ante interbloqueos
Pasa una función de transacción al envoltorio de reintento:
err := withDeadlockRetry(ctx, db, 3, func(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx,
"UPDATE Production.ProductInventory SET Quantity = Quantity - @qty WHERE ProductID = @pid AND LocationID = @lid",
sql.Named("qty", orderQty),
sql.Named("pid", productId),
sql.Named("lid", locationId))
return err
})
Reducir los bloqueos
| Strategy | Cómo ayuda |
|---|---|
| Acceder a las tablas en un orden coherente | Cuando todas las transacciones bloquean la Tabla A antes que la Tabla B, no pueden producirse esperas circulares. |
| Mantén las transacciones cortas | Las transacciones más cortas mantienen los bloqueos menos tiempo, reduciendo la ventana de conflicto. |
| Utiliza el nivel de aislamiento suficiente más bajo |
ReadCommitted tiene menos cerraduras que Serializable. |
| Añadir índices apropiados | Las actualizaciones orientadas al índice bloquean menos filas que los escaneos de tablas. |
| Evita la interacción del usuario durante las transacciones | Nunca esperes la entrada del usuario entre BeginTx y Commit. |
Reintentar es la respuesta correcta en el código de la aplicación, pero los bloqueos repetidos en la misma consulta indican un problema de diseño. Utiliza el grafo de bloqueo de SQL Server (capturado mediante Eventos Extendidos o la sesión de salud del sistema) para identificar las sentencias y tipos de bloqueo en competencia, y luego aplica las estrategias de la tabla anterior. Para una guía completa sobre el análisis y prevención de bloqueos, consulta la guía de bloqueos.
Transacciones y anclaje de conexiones
Una transacción fija una única conexión del pool hasta que Commit() o Rollback() es llamada. Durante este tiempo, ninguna otra gorutina puede usar esa conexión.
Implicaciones:
- Las transacciones de larga duración reducen el tamaño efectivo del grupo. Si tienes
MaxOpenConns=25y 20 transacciones abiertas, solo hay 5 conexiones disponibles para otras tareas. - Olvidarse de
Rollback()provoca una fuga permanente de la conexión. - La cancelación de contexto en el contexto de la transacción reverte la transacción y devuelve la conexión al pool.
// Set a deadline to prevent transactions from running indefinitely.
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
Transacciones concurrentes
Cada goroutine debe crear su propia transacción. Nunca compartas un *sql.Tx entre goroutines porque *sql.Tx no es seguro para el uso concurrente:
// CORRECT: Each goroutine gets its own transaction.
var g errgroup.Group
for _, item := range items {
item := item
g.Go(func() error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
_, err = tx.ExecContext(ctx,
"UPDATE Production.ProductInventory SET Quantity = Quantity - 1 WHERE ProductID = @p1 AND LocationID = @p2",
sql.Named("p1", item.ProductId),
sql.Named("p2", item.LocationId))
if err != nil {
return err
}
return tx.Commit()
})
}
return g.Wait()
Transacciones distribuidas
El go-mssqldb controlador no soporta transacciones distribuidas (transacciones XA o System.Transactions equivalentes). Si necesitas coordinar trabajo en varias bases de datos:
- Usa un patrón saga con acciones compensatorias.
- Consolidar operaciones en una única base de datos siempre que sea posible.
- Utiliza servidores vinculados con
BEGIN DISTRIBUTED TRANSACTIONde Transact-SQL (T-SQL) si ambas bases de datos son SQL Server.
Lista de verificación de transacciones
| Area | Recommendation |
|---|---|
| Seguridad de reversión | Siempre defer tx.Rollback() inmediatamente después de BeginTx. |
| Nivel de aislamiento | Empieza con ReadCommitted (el valor por defecto). Escalar solo cuando sea necesario. |
| Interbloqueos | Envuelva el código transaccional en un bucle de reintento. Accede a las tablas en un orden consistente. |
| Duration | Mantén las transacciones lo más cortas posible. Establece plazos contextuales. |
| Concurrency | Nunca compartas un *sql.Tx entre goroutines. |
| Puntos de retorno | Uso SAVE TRANSACTION y ROLLBACK TRANSACTION <name> para retroceso parcial. |
| Impacto de la piscina | Supervisa db.Stats().InUse para detectar fugas de conexiones causadas por transacciones sin confirmar. |