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 ejecutan con 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 *sql.Tx devuelto reserva una única conexión del grupo durante toda la duración de 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() se ejecuta correctamente, el Rollback() diferido no realiza ninguna operación. Si se produce algún error antes de Commit(), la operación diferida Rollback() garantiza que la transacción no permanezca abierta y devuelve la conexión al grupo en un estado 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 ver valores consistentes para las mismas filas dentro de la transacción. |
sql.LevelSerializable |
No | No | No | Máximo. Los bloqueos de rango impiden las inserciones simultáneas. | 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 gran volumen de lecturas que necesitan consistencia en un momento dado 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: Read committed 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 redirigir las sesiones elegibles a un servidor secundario legible, pero no convierte una transacción existente en de 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 cualquier instrucción, la transacción debe revertirse 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 lote o el procedimiento almacenado se ejecuta con SET XACT_ABORT ON, trata cualquier error de instrucción como terminal para la transacción. Reverte inmediatamente y no intentes ejecutar 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. Dado que el paquete database/sql de Go no expone los puntos de guardado directamente, ejecútalos como SQL sin formato a través de 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.
Administración de interbloqueos
SQL Server resuelve bloqueos terminando una de las transacciones competidoras (la víctima del bloqueo) y devolviendo el error 1205. El servidor revierte automáticamente la transacción finalizada.
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)
}
Utiliza el envoltorio de reintento de interbloqueo
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 durante menos tiempo, lo que reduce la ventana de posibles conflictos. |
| 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 bloquea una única conexión del grupo hasta que se llama a Commit() o Rollback(). Durante este tiempo, ninguna otra goroutine puede utilizar 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 del contexto de la transacción revierte la transacción y devuelve la conexión al grupo.
// 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, ya que *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. |