Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
As transações agrupam múltiplas operações numa unidade atómica. Ou todas as operações são concluídas com êxito (commit) ou nenhuma delas produz efeitos (rollback). Este artigo aborda como usar transações com o go-mssqldb driver, incluindo níveis de isolamento, tratamento de erros e padrões para aplicações de produção.
Os exemplos deste artigo são executados na base de dados de exemplo AdventureWorks2025. Exemplos orientados à escrita têm como alvo HumanResources.Department, Production.ProductCategory, Production.ProductSubcategory, e Production.ProductInventory.
Iniciar uma transação
Usa db.BeginTx para iniciar uma transação. Os pins devolvidos *sql.Tx garantem uma única ligação do pool durante a vida útil da transação:
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
Chame defer tx.Rollback() sempre imediatamente após BeginTx. Se Commit() for executado com êxito, a chamada diferida a Rollback() não faz nada. Se ocorrer algum erro antes de Commit(), a instrução adiada Rollback() garante que a transação não permaneça aberta nem devolva a ligação ao pool sem efetuar o commit.
Níveis de isolamento
O SQL Server suporta vários níveis de isolamento que controlam como as transações concorrentes interagem. Defina o nível de isolamento em sql.TxOptions:
tx, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelReadCommitted,
})
Comparação de níveis de isolamento
| Nível de isolamento | Leituras sujas | Leituras não repetíveis | Phantom lê | Impacto no desempenho | Utilizar quando |
|---|---|---|---|---|---|
sql.LevelReadUncommitted |
Sim | Sim | Sim | Despesas gerais mais baixas | Contagens aproximadas, painéis de monitorização. A precisão dos dados não é crítica. |
sql.LevelReadCommitted |
No | Sim | Sim | Predefinição. Bom para a maioria das cargas de trabalho. | Cargas de trabalho OLTP gerais. O ponto de partida padrão e recomendado. |
sql.LevelRepeatableRead |
No | No | Sim | Moderado. Segura as fechaduras por mais tempo. | Leituras que devem apresentar valores consistentes nas mesmas linhas durante a transação. |
sql.LevelSerializable |
No | No | No | Mais alto. Os bloqueios de intervalo impedem inserções concorrentes. | Transações financeiras, gestão de inventário, qualquer situação em que as leituras fantasma sejam inaceitáveis. |
sql.LevelSnapshot |
No | No | No | Usa versionamento por linhas em tempdb. Sem bloqueios. |
Cargas de trabalho com utilização intensiva de leitura que exigem consistência num determinado momento, sem bloquear os processos de escrita. |
Observação
sql.LevelSnapshot requer que o isolamento de snapshots esteja ativado na base de dados: ALTER DATABASE AdventureWorks2025 SET ALLOW_SNAPSHOT_ISOLATION ON.
Exemplo: Ler Commit vs. serializável
Especifique o nível de isolamento em 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,
})
Roteio de somente leitura
O driver não suporta sql.TxOptions.ReadOnly. Se passares ReadOnly: true, BeginTx devolve um erro.
Para o encaminhamento de apenas leitura do AlwaysOn, defina applicationintent=ReadOnly na cadeia de ligação ao abrir a ligação:
sqlserver://listener.example.com?database=AdventureWorks2025&applicationintent=ReadOnly
Isto é uma configuração ao nível da ligação. Pode encaminhar sessões elegíveis para um secundário legível, mas não torna uma transação existente apenas de leitura. Use uma ligação dedicada de apenas leitura ou credenciais de privilégio mínimo para cargas de trabalho que não devem gravar dados.
Tratamento de erros em transações
Lide com os erros em transações com cuidado. Quando qualquer instrução falha, a transação deve ser revertida na íntegra. Não tente continuar com outras afirmações após um erro:
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
}
Se o procedimento em lote ou armazenado for executado com SET XACT_ABORT ON, trate qualquer erro de instrução como terminal para a transação. Recua imediatamente e não tente mais afirmações ou Commit(). As versões atuais dos drivers detetam transações abortadas pelo servidor e devolvem um erro em vez de permitir um commit parcial silencioso.
Guardar pontos
Os pontos de restauro criam pontos intermédios de reversão dentro de uma transação. O SQL Server suporta pontos de gravação nativos. Como o pacote database/sql do Go não expõe diretamente os pontos de restauro, execute-os como SQL em bruto através da transação:
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()
}
Observação
SAVE TRANSACTION <name> cria o ponto de restauro.
ROLLBACK TRANSACTION <name> Reverte até esse ponto de restauro sem terminar a transação externa.
ROLLBACK sem nome reverte a transação inteira.
Tratamento de interbloqueios
O SQL Server resolve os deadlocks terminando uma das transações concorrentes (a vítima do deadlock) e devolve o erro 1205. A transação terminada é automaticamente revertida pelo servidor.
Detetar e repetir impasses
Verifique o erro 1205 e tente novamente toda a transação com um pequeno atraso:
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)
}
Use o invólucro de repetição em caso de impasse
Passe uma função de transação para o invólucro de repetição:
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
})
Reduzir impasses
| Strategy | Como ajuda |
|---|---|
| Aceder a tabelas numa ordem consistente | Quando todas as transações bloqueiam a Tabela A antes da Tabela B, não podem ocorrer esperas circulares. |
| Mantenha as transações curtas | Transações mais curtas mantêm bloqueios por menos tempo, reduzindo a janela para conflitos. |
| Use o nível de isolamento suficiente mais baixo |
ReadCommitted tem menos bloqueios do que Serializable. |
| Adicionar índices apropriados | As atualizações direcionadas ao índice bloqueiam menos linhas do que as análises de tabelas. |
| Evite a interação do utilizador durante as transações | Nunca espere pela entrada do utilizador entre BeginTx e Commit. |
Voltar a tentar é a abordagem correta no código da aplicação, mas interbloqueios repetidos na mesma consulta indicam um problema de conceção. Use o grafo de deadlock do SQL Server (capturado através de Eventos Estendidos ou da sessão de saúde do sistema) para identificar as instruções concorrentes e os tipos de bloqueio, aplicando depois as estratégias na tabela anterior. Para uma explicação detalhada da análise e prevenção de interbloqueios, consulte o guia sobre interbloqueios.
Transações e fixação de ligação
Uma transação fixa uma única ligação do pool até Commit() que ou Rollback() seja chamada. Durante este tempo, nenhuma outra goroutine pode usar essa conexão.
Implicações:
- Transações de longa duração reduzem o tamanho efetivo do pool. Se tiveres
MaxOpenConns=25e 20 transações abertas, só 5 ligações estão disponíveis para outras tarefas. - Um
Rollback()esquecido deixa a ligação aberta permanentemente. - O cancelamento de contexto no contexto da transação reverte a transação e devolve a ligação ao 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()
Transações simultâneas
Cada goroutine deve criar a sua própria transação. Nunca partilhe um(a) *sql.Tx entre goroutines, porque *sql.Tx não é seguro para utilização concorrente:
// 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()
Transações distribuídas
O go-mssqldb driver não suporta transações distribuídas (transações XA ou System.Transactions equivalentes). Se precisar de coordenar trabalho em várias bases de dados:
- Utilize o padrão saga com ações compensatórias.
- Consolide as operações numa única base de dados sempre que possível.
- Utilize servidores ligados com
BEGIN DISTRIBUTED TRANSACTIONno Transact-SQL (T-SQL) se ambas as bases de dados forem do SQL Server.
Lista de verificação de transações
| Area | Recommendation |
|---|---|
| Segurança da reversão | Sempre defer tx.Rollback() imediatamente após BeginTx. |
| Nível de isolamento | Comece por ReadCommitted (o padrão). Escalar só quando for necessário. |
| Deadlocks | Envolva o código transacional num ciclo de repetição. Aceder às tabelas numa ordem consistente. |
| Duração | Mantenha as transações o mais curtas possível. Defina prazos de contexto. |
| Concurrency | Nunca partilhem um *sql.Tx entre as rotinas. |
| Guardar pontos | Uso SAVE TRANSACTION e ROLLBACK TRANSACTION <name> para recuo parcial. |
| Impacto da piscina | Monitorize db.Stats().InUse para detetar fugas de ligação causadas por transações não confirmadas. |