|

Long-running Transactions: O Que Acontece no Banco Enquanto Sua Transação Não Fecha

O artigo anterior mostrou como transações longas esgotam o connection pool. Mas esse é apenas o dano visível — o que a aplicação sente diretamente. Dentro do banco de dados, uma transação longa causa um conjunto separado de problemas que continuam existindo mesmo depois que o connection pool é dimensionado corretamente, mesmo com PgBouncer, mesmo com infraestrutura robusta.

Uma transação aberta há 30 segundos está silenciosamente acumulando locks, bloqueando operações de manutenção, impedindo o banco de liberar espaço em disco, e potencialmente degradando a performance de queries completamente não relacionadas.

Este artigo entra no banco de dados e explica o que acontece lá dentro.


O que uma Transação “Aberta” Significa para o Banco

Quando uma transação começa, o banco de dados precisa garantir que ela veja um snapshot consistente dos dados — o estado do banco no momento em que ela começou, não o estado atual. Isso é o isolamento do ACID, e ele tem custo.

MVCC: o mecanismo por trás do isolamento

PostgreSQL (e a maioria dos bancos modernos) implementa isolamento via MVCC — Multi-Version Concurrency Control. Em vez de bloquear linhas para leitura, o banco mantém múltiplas versões de cada linha. Cada transação vê as versões que existiam quando ela começou.

Linha original:    | id=1 | status='PENDING'  | xmin=100 | xmax=null |
Após UPDATE        | id=1 | status='PENDING'  | xmin=100 | xmax=150  |  <- versão antiga
por transação 150: | id=1 | status='CONFIRMED'| xmin=150 | xmax=null |  <- versão nova

xmin é o ID da transação que criou a versão. xmax é o ID da transação que a deletou ou atualizou. Uma transação com ID 120 que lê essa linha vê a versão com xmin=100 (existia quando ela começou) e ignora a versão com xmin=150 (criada depois).

O problema: enquanto a transação 120 estiver aberta, o banco não pode descartar a versão antiga da linha. Ela precisa existir para que a transação 120 possa vê-la se precisar. Se a transação 120 ficar aberta por horas, o banco acumula versões antigas de todas as linhas que foram modificadas durante esse tempo — por qualquer transação, não apenas pela 120.

Isso é chamado de table bloat no PostgreSQL, e é um dos efeitos mais graves de transações longas.


Os Problemas em Detalhe

1. Lock Contention

Toda operação de escrita (INSERT, UPDATE, DELETE) adquire locks nas linhas afetadas. Esses locks são mantidos até o fim da transação. Uma transação longa segura locks por todo o seu tempo de vida.

java

@Transactional
public void updateProductPrices(List<Long> productIds, BigDecimal discount) {
    for (Long id : productIds) {
        Product product = productRepository.findById(id).orElseThrow();
        product.setPrice(product.getPrice().multiply(discount));
        productRepository.save(product); // lock na linha, mantido até commit
    }

    // I/O externo: conexão + locks presos por todo esse tempo
    pricingAuditService.notifyPriceChange(productIds); // HTTP externo, 2 segundos
    catalogService.invalidateCache(productIds);        // HTTP externo, 1 segundo

} // Locks liberados apenas aqui, 3+ segundos depois

Qualquer outra operação que precise atualizar um desses produtos — outro request, um job de background, um processo de importação — fica esperando. Em tabelas com alto volume de escrita, um único método como esse pode criar uma cadeia de bloqueios.

No PostgreSQL, você pode ver quem está bloqueando quem:

sql

SELECT
    blocked.pid                    AS blocked_pid,
    blocked.query                  AS blocked_query,
    now() - blocked.query_start    AS blocked_duration,
    blocking.pid                   AS blocking_pid,
    blocking.query                 AS blocking_query,
    now() - blocking.query_start   AS blocking_duration
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
    ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE cardinality(pg_blocking_pids(blocked.pid)) > 0;

2. Vacuum Bloat no PostgreSQL

O MVCC descrito acima cria versões antigas de linhas que precisam ser limpas periodicamente. Esse processo é o VACUUM — uma operação de manutenção que marca versões antigas como espaço reutilizável.

O VACUUM não pode remover uma versão de linha enquanto qualquer transação ativa puder precisar vê-la. Uma transação longa com ID 100 que começou há 1 hora impede que o VACUUM remova todas as versões criadas por transações com IDs entre 100 e o atual — potencialmente milhões de versões acumuladas durante essa hora.

sql

-- PostgreSQL: transações antigas que estão impedindo o VACUUM
SELECT
    pid,
    usename,
    application_name,
    now() - xact_start AS transaction_age,
    now() - state_change AS state_age,
    state,
    query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start ASC
LIMIT 10;

O resultado do bloat acumulado:

Tabelas crescem fisicamente mesmo sem dados novos. Uma tabela de 10GB pode crescer para 30GB com bloat acumulado — e queries de sequential scan ficam 3x mais lentas porque leem mais páginas físicas.

Índices também sofrem bloat. Versões antigas de entradas de índice acumulam, aumentando o tamanho dos índices e degradando a performance de index scans.

VACUUM FULL é necessário para recuperar o espaço, e ele bloqueia a tabela inteiramente durante a execução. Em tabelas grandes, isso pode levar horas — criando uma janela de manutenção dolorosa que existe porque transações longas não foram tratadas antes.

3. Transaction ID Wraparound (PostgreSQL)

O PostgreSQL usa IDs de transação de 32 bits — um contador que vai de 0 a aproximadamente 4 bilhões. Quando chega no limite, ele volta para zero (wraparound). O banco usa aritmética modular para comparar IDs: uma transação “anterior” ou “posterior” é determinada pela posição relativa no ciclo.

O problema: se o VACUUM não consegue limpar versões antigas porque há transações longas no caminho, o banco pode se aproximar do limite do contador sem conseguir avançar. PostgreSQL tem alarmes para isso:

WARNING:  database "mydb" must be vacuumed within 177009986 transactions
HINT:  To avoid a database shutdown, execute a database-wide VACUUM in "mydb".

E se o VACUUM não for executado a tempo, o banco entra em modo somente leitura para se proteger — uma das falhas mais graves possíveis em um sistema de produção.

Transações longas que acumulam bloat são um fator de risco significativo para este cenário.

4. Replication Lag em Réplicas de Leitura

Em setups com streaming replication (PostgreSQL) ou replication binlog (MySQL), réplicas precisam aplicar as mudanças do primário na ordem correta. Uma transação longa no primário que não commitou ainda não pode ser aplicada na réplica — porque o estado final não é conhecido.

Em PostgreSQL com replication slots, a réplica fica esperando. O replication_lag cresce. Se a réplica é usada para queries de leitura — um padrão comum para escalar reads — queries de leitura na réplica ficam cada vez mais desatualizadas.

sql

-- PostgreSQL: lag das réplicas
SELECT
    client_addr,
    state,
    sent_lsn,
    write_lsn,
    flush_lsn,
    replay_lsn,
    sent_lsn - replay_lsn AS replication_lag_bytes
FROM pg_stat_replication
ORDER BY replication_lag_bytes DESC;

Com replication_lag_bytes crescendo, a réplica serve dados cada vez mais antigos — e em um sistema que roteia leituras para a réplica após uma escrita, você começa a ver inconsistências aparentes: “acabei de criar o pedido mas ele não aparece na listagem”.

5. Snapshot Isolation e Anomalias de Leitura

Com nível de isolamento REPEATABLE READ ou SERIALIZABLE (padrão do Hibernate em alguns cenários), uma transação longa que lê dados frequentemente pode tomar decisões baseadas em dados cada vez mais desatualizados.

java

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void processInventoryAdjustment(Long productId) {

    // Lê o estoque — snapshot capturado aqui
    Product product = productRepository.findById(productId).orElseThrow();
    int currentStock = product.getStockQuantity(); // ex: 100 unidades

    // Procesamento lento: 30 segundos de cálculo
    int adjustment = inventoryCalculator.calculate(productId); // 30 segundos

    // Durante esses 30 segundos, 80 unidades foram vendidas por outras transações
    // currentStock ainda é 100 — o snapshot não viu essas vendas

    product.setStockQuantity(currentStock + adjustment); // baseado em dado obsoleto
    productRepository.save(product); // sobrescreve as 80 vendas

}

Isso não é um bug do banco — é o isolamento funcionando como projetado. O bug é manter uma transação aberta por 30 segundos enquanto decisões críticas são tomadas com base no snapshot inicial.


Identificando Transações Longas

Monitoramento contínuo no PostgreSQL

sql

-- Transações abertas há mais de 10 segundos
SELECT
    pid,
    usename,
    application_name,
    client_addr,
    now() - xact_start                AS transaction_age,
    now() - state_change              AS current_state_age,
    state,
    left(query, 100)                  AS current_query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
  AND now() - xact_start > interval '10 seconds'
  AND state != 'idle'
ORDER BY transaction_age DESC;

sql

-- Conexões idle in transaction: transação aberta mas sem query rodando
-- Particularmente perigoso: ninguém sabe que está segurando locks
SELECT
    pid,
    usename,
    now() - xact_start AS idle_in_transaction_for,
    left(query, 100)   AS last_query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY idle_in_transaction_for DESC;

idle in transaction é o sinal mais preocupante. Significa que o código abriu uma transação, executou alguma query, e depois parou — esperando por I/O externo, por input do usuário, por um lock, ou simplesmente travado. A transação está aberta, os locks estão mantidos, e o VACUUM não pode avançar.

MySQL/MariaDB

sql

-- Transações longas no InnoDB
SELECT
    r.trx_id,
    r.trx_started,
    TIMESTAMPDIFF(SECOND, r.trx_started, NOW()) AS age_seconds,
    r.trx_state,
    r.trx_query,
    r.trx_rows_locked,
    r.trx_rows_modified
FROM information_schema.innodb_trx r
WHERE TIMESTAMPDIFF(SECOND, r.trx_started, NOW()) > 10
ORDER BY age_seconds DESC;

Via Hibernate Statistics em Java

java

@Component
public class TransactionMonitor {

    @Scheduled(fixedDelay = 30_000) // a cada 30 segundos
    public void logSlowTransactions() {
        EntityManagerFactory emf = entityManagerFactory
            .unwrap(SessionFactory.class);

        Statistics stats = emf.getStatistics();

        if (stats.isStatisticsEnabled()) {
            long transactions = stats.getTransactionCount();
            long successful = stats.getSuccessfulTransactionCount();
            long failed = transactions - successful;

            log.info("Transactions: total={}, successful={}, failed={}",
                transactions, successful, failed);
        }
    }
}

Para monitoramento mais granular por transação individual, integre com o Micrometer e exporte métricas de duração de transação:

java

@Aspect
@Component
public class TransactionTimingAspect {

    private final MeterRegistry meterRegistry;

    @Around("@annotation(transactional)")
    public Object timeTransaction(ProceedingJoinPoint pjp,
                                  Transactional transactional) throws Throwable {
        String methodName = pjp.getSignature().toShortString();
        Timer.Sample sample = Timer.start(meterRegistry);

        try {
            Object result = pjp.proceed();
            sample.stop(Timer.builder("transaction.duration")
                .tag("method", methodName)
                .tag("outcome", "success")
                .register(meterRegistry));
            return result;
        } catch (Exception e) {
            sample.stop(Timer.builder("transaction.duration")
                .tag("method", methodName)
                .tag("outcome", "failure")
                .register(meterRegistry));
            throw e;
        }
    }
}

Isso expõe transaction.duration no Prometheus/Grafana — configure alertas para transações acima de 5 segundos.


As Soluções

1. Reduzir o escopo da transação ao mínimo necessário

A regra fundamental: uma transação deve conter apenas as operações de banco de dados necessárias para garantir consistência. Nada mais.

java

// Antes: transação engloba tudo, incluindo I/O externo e computação pesada
@Transactional
public OrderConfirmation confirmOrder(Long orderId) {

    Order order = orderRepository.findById(orderId).orElseThrow();

    // Computação pesada — não precisa de transação
    PricingResult pricing = pricingEngine.calculate(order); // 500ms de CPU

    // I/O externo — não deve estar na transação
    StockResult stock = inventoryService.checkAndReserve(order); // HTTP, 200ms

    order.setStatus("CONFIRMED");
    order.setTotal(pricing.getTotal());
    orderRepository.save(order);

    // I/O externo — não deve estar na transação
    notificationService.sendConfirmation(order); // HTTP, 300ms

    return new OrderConfirmation(order, pricing, stock);
}

// Depois: transação apenas onde necessário
public OrderConfirmation confirmOrder(Long orderId) {

    // Fora da transação: leitura para computação
    Order order = orderRepository.findById(orderId).orElseThrow();

    // Fora da transação: computação e I/O
    PricingResult pricing = pricingEngine.calculate(order);
    StockResult stock = inventoryService.checkAndReserve(order);

    // Transação curta: apenas a escrita que precisa ser atômica
    Order confirmed = persistConfirmation(order.getId(), pricing.getTotal());

    // Fora da transação: notificação
    notificationService.sendConfirmation(confirmed);

    return new OrderConfirmation(confirmed, pricing, stock);
}

@Transactional
protected Order persistConfirmation(Long orderId, BigDecimal total) {
    Order order = orderRepository.findById(orderId).orElseThrow();
    order.setStatus("CONFIRMED");
    order.setTotal(total);
    return orderRepository.save(order);
    // Transação commita imediatamente ao sair deste método
}

2. Decompor transações longas em transações menores

Quando uma operação precisa modificar muitos registros — uma importação em lote, uma migração de dados, um processo de recalculo — a tentação é fazer tudo em uma transação para garantir atomicidade. Mas uma transação que processa 100.000 registros mantém locks e acumula bloat por toda a sua duração.

A alternativa é processar em lotes com commits intermediários:

java

@Service
public class PriceRecalculationService {

    private static final int BATCH_SIZE = 500;

    // NÃO faça: uma transação para tudo
    @Transactional // Abre uma transação gigante
    public void recalculateAllPrices() {
        List<Product> products = productRepository.findAll(); // potencialmente milhões
        for (Product product : products) {
            product.setPrice(pricingEngine.calculate(product));
            productRepository.save(product); // locks acumulando
        }
    } // Commit depois de processar tudo — locks presos por horas

    // Faça: commits em lote
    public void recalculateAllPricesInBatches() {
        Long lastId = 0L;

        while (true) {
            // Cada chamada é uma transação separada e curta
            List<Long> batch = processBatch(lastId, BATCH_SIZE);

            if (batch.isEmpty()) break;

            lastId = batch.get(batch.size() - 1);

            log.info("Processed batch up to id={}", lastId);
        }
    }

    @Transactional
    protected List<Long> processBatch(Long afterId, int batchSize) {
        List<Product> batch = productRepository
            .findByIdGreaterThanOrderById(afterId, PageRequest.of(0, batchSize));

        batch.forEach(product ->
            product.setPrice(pricingEngine.calculate(product))
        );

        productRepository.saveAll(batch);

        return batch.stream()
            .map(Product::getId)
            .collect(toList());
    } // Transação commita aqui — locks liberados, VACUUM pode avançar
}

A perda de atomicidade total é o trade-off. Se o processo falhar no meio, parte dos dados foi atualizada e parte não. Para operações de recalculo, isso frequentemente é aceitável — reprocesse os registros não atualizados. Para operações onde atomicidade total é mandatória, avalie se operações de compensação (saga pattern) são viáveis.

3. Timeout de transação como rede de segurança

Configure timeouts em múltiplas camadas para garantir que nenhuma transação fique aberta indefinidamente, independente do que o código faça:

java

// Por método
@Transactional(timeout = 5) // 5 segundos
public void updateProduct(Long id, ProductUpdateDTO dto) {
    // Se demorar mais que 5s, TransactionTimedOutException
}

// Via annotation customizada para consistência
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Transactional(timeout = 5, rollbackFor = Exception.class)
public @interface ShortTransaction {}

@ShortTransaction
public void updateInventory(Long productId, int delta) {
    // ...
}

properties

# Timeout global: fallback para métodos sem timeout explícito
spring.transaction.default-timeout=30

No banco, configure o timeout de transação como segunda linha de defesa:

sql

-- PostgreSQL: mata conexões idle in transaction após 60 segundos
ALTER SYSTEM SET idle_in_transaction_session_timeout = '60s';
SELECT pg_reload_conf();

-- MySQL: timeout de lock wait (padrão é 50 segundos, reduza)
SET GLOBAL innodb_lock_wait_timeout = 10;

idle_in_transaction_session_timeout é particularmente valioso: mata automaticamente conexões que estão em estado idle in transaction — o sinal mais perigoso de transação abandonada.

4. SELECT FOR UPDATE com escopo mínimo

Quando você precisa de locks explícitos para coordenação, minimize o escopo do lock:

java

// Ruim: lock em toda a transação, incluindo partes que não precisam
@Transactional
public void processPayment(Long orderId) {
    Order order = orderRepository.findById(orderId).orElseThrow(); // lock implícito
    PaymentResult result = paymentGateway.charge(order); // 1-2 segundos
    order.setPaymentStatus(result.getStatus());
    orderRepository.save(order);
}

// Melhor: lock apenas quando necessário para a escrita
@Transactional
public void processPayment(Long orderId) {
    // Lê sem lock para a validação inicial
    Order order = orderRepository.findById(orderId).orElseThrow();
    validateOrderForPayment(order);

    // Resultado do pagamento obtido fora da transação
    PaymentResult result = chargeExternally(order); // fora da @Transactional

    // Lock apenas para a escrita final, que é rápida
    persistPaymentResult(orderId, result);
}

@Transactional
protected void persistPaymentResult(Long orderId, PaymentResult result) {
    // SELECT FOR UPDATE: lock explícito, mas escopo mínimo
    Order order = orderRepository.findByIdForUpdate(orderId).orElseThrow();
    order.setPaymentStatus(result.getStatus());
    orderRepository.save(order);
} // Lock liberado imediatamente

java

// Repository
public interface OrderRepository extends JpaRepository<Order, Long> {

    @Lock(LockModeType.PESSIMISTIC_WRITE) // SELECT FOR UPDATE
    @Query("SELECT o FROM Order o WHERE o.id = :id")
    Optional<Order> findByIdForUpdate(@Param("id") Long id);
}

5. Locks otimistas para alta contenção

Para tabelas com muito mais leitura do que escrita, locks otimistas eliminam a necessidade de locks pessimistas — e portanto eliminam a contenção associada a transações longas que seguram locks:

java

@Entity
public class Product {

    @Id
    @GeneratedValue
    private Long id;

    private int stockQuantity;

    @Version // Versão para lock otimista
    private Long version;
}

java

@Transactional
public void decrementStock(Long productId, int quantity) {
    Product product = productRepository.findById(productId).orElseThrow();

    if (product.getStockQuantity() < quantity) {
        throw new InsufficientStockException();
    }

    product.setStockQuantity(product.getStockQuantity() - quantity);
    // Se outra transação modificou 'version' desde a leitura,
    // Hibernate lança OptimisticLockException — sem deadlock
    productRepository.save(product);
}

O SQL gerado inclui a versão no WHERE:

sql

UPDATE product
SET stock_quantity = ?, version = ?
WHERE id = ? AND version = ?  -- falha se version mudou

Com lock otimista, transações longas não bloqueiam outras transações — a contenção se manifesta como OptimisticLockException que você trata com retry, em vez de lock contention silenciosa que trava tudo.

6. Processamento assíncrono para operações longas inerentemente

Algumas operações são longas por natureza e não podem ser divididas de forma simples. Para essas, o padrão correto é não usar transação longa — é não ter transação durante o processamento, e usar eventos ou mensagens para garantir consistência eventual:

java

@Service
public class BulkImportService {

    // NÃO: transação gigante durante todo o processamento
    @Transactional
    public void importProducts(List<ProductCSVRow> rows) {
        rows.forEach(this::processRow); // potencialmente horas
    }

    // SIM: persiste o job, processa assincronamente em lotes
    public ImportJob startImport(List<ProductCSVRow> rows) {
        // Transação curta: apenas salvar o job
        ImportJob job = importJobRepository.save(new ImportJob(rows));

        // Processamento assíncrono: sem transação longa, sem locks prolongados
        eventPublisher.publishEvent(new ImportStartedEvent(job.getId()));

        return job;
    }
}

@Component
public class ImportJobProcessor {

    @Async
    @EventListener
    public void processImport(ImportStartedEvent event) {
        ImportJob job = importJobRepository.findById(event.getJobId())
            .orElseThrow();

        List<ProductCSVRow> rows = job.getRows();
        int processed = 0;

        // Processa em lotes de 100, cada lote com sua própria transação
        for (List<ProductCSVRow> batch : partition(rows, 100)) {
            try {
                processBatch(batch);
                processed += batch.size();
                updateJobProgress(event.getJobId(), processed); // commit intermediário
            } catch (Exception e) {
                markBatchFailed(event.getJobId(), batch, e);
            }
        }
    }

    @Transactional
    protected void processBatch(List<ProductCSVRow> batch) {
        batch.forEach(row -> {
            Product product = mapToProduct(row);
            productRepository.save(product);
        });
    } // Commit aqui: transação de ~100ms, não de horas
}

Configuração de Referência

properties

# Spring: timeout global de transação
spring.transaction.default-timeout=30

# JPA: propriedades de lock
spring.jpa.properties.jakarta.persistence.lock.timeout=5000  # lock wait timeout em ms
spring.jpa.properties.jakarta.persistence.query.timeout=10000 # query timeout em ms

sql

-- PostgreSQL: configurações recomendadas para produção
-- Mata conexões idle in transaction após 60 segundos
ALTER SYSTEM SET idle_in_transaction_session_timeout = '60s';

-- Mata queries que rodam por mais de 30 segundos
ALTER SYSTEM SET statement_timeout = '30s';

-- Log de queries lentas (queries acima de 1 segundo)
ALTER SYSTEM SET log_min_duration_statement = '1000';

-- Log de lock waits acima de 500ms
ALTER SYSTEM SET log_lock_waits = on;
ALTER SYSTEM SET deadlock_timeout = '500ms';

-- Aplica as mudanças
SELECT pg_reload_conf();

sql

-- MySQL: configurações equivalentes
SET GLOBAL innodb_lock_wait_timeout = 10;        -- timeout de lock em segundos
SET GLOBAL wait_timeout = 60;                    -- fecha conexões ociosas após 60s
SET GLOBAL interactive_timeout = 60;
SET GLOBAL long_query_time = 1;                  -- log queries acima de 1s
SET GLOBAL slow_query_log = ON;

A Relação com os Problemas Anteriores

Os problemas desta série são camadas de um mesmo fenômeno. N+1 queries aumentam o tempo que uma transação fica com a conexão aberta. Chatty queries multiplicam o tempo de uso. Connection pool exhaustion é o sintoma visível quando conexões ficam presas. E transações longas são frequentemente a causa raiz de todos os anteriores — quando uma transação demora, ela força as conexões a ficarem presas, o que esgota o pool, o que faz requests esperarem, o que piora a latência, o que cria mais transações longas esperando por conexão disponível.

O ciclo se quebra no mesmo lugar onde começa: reduzindo o escopo das transações ao mínimo necessário para garantir consistência, e movendo tudo o que não é operação de banco para fora do contexto transacional.


Conclusão

Uma transação @Transactional é um contrato com o banco de dados: “enquanto este método executar, mantenha todos os meus dados consistentes”. O banco honra esse contrato com locks, snapshots e versões de linha — e paga o custo de mantê-los enquanto a transação durar.

O desenvolvedor que adiciona @Transactional sem pensar no escopo está transferindo esse custo para o banco, para outros usuários que precisam das mesmas linhas, e para as operações de manutenção que não podem avançar enquanto a transação está viva. Em tabelas com alto volume, o custo se acumula de formas que não aparecem no código — bloat em disco, replication lag, vacuum inability, e eventualmente o cenário de transaction ID wraparound que força o banco a entrar em modo somente leitura.

A prática que evita tudo isso é a mesma: transação deve ser um invólucro fino ao redor apenas das escritas que precisam ser atômicas. Computação antes. I/O externo fora. Leitura para validação fora. A transação abre, escreve, commita — em milissegundos, não em segundos.


Próximo da série: Dirty Checking Overhead — o custo que o Hibernate paga para detectar mudanças em entidades gerenciadas, e como ele escala de forma não óbvia com o número de objetos na sessão.

Posts Similares

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *