← Recipes

阅读中文版本

Write with an Atomic Kafka Produce-and-Commit

Use transactions so a database commit and a Kafka produce either both happen or neither does, and set a stable transaction id for fencing.

~50 minkafkatransactionsreliability

Task

Wrap a database write and a Kafka produce in one atomic transaction to avoid duplicate or lost events.

When to use this

The database commit succeeds but the produce fails, so downstream never learns about the change and the system drifts out of sync.

Prerequisites

  • A Kafka cluster with transactions enabled
  • A transactional database adapter

Steps

1. Configure a stable transactional.id so restarts fence previous zombie producers.

Expected output: Only one live producer instance can write.

2. Begin a transaction, write to the database, and produce the event within it.

Expected output: Both effects are part of one unit.

3. Commit the transaction; on any error, abort so neither effect is visible.

Expected output: Failures leave no partial state.

4. Keep the consumer isolation level read_committed so it never sees aborted writes.

kafka-console-consumer --topic orders --property isolation.level=read_committed

Expected output: Downstream only sees committed events.

Troubleshooting

Problem

Transaction stays open too long

Solution

Set transaction.timeout.ms generously but bounded, and keep the unit small.

Problem

Zombie producer from an old instance

Solution

Reuse the same transactional.id; Kafka fences the older instance automatically.

Limitations

  • Transactions add latency and require broker support; they are unsuitable for very high-throughput best-effort streams.
  • A transaction guarantees atomicity across Kafka and a store only if that store participates in the same protocol.

Context

Wrap a database write and a Kafka produce in one atomic transaction to avoid duplicate or lost events. The database commit succeeds but the produce fails, so downstream never learns about the change and the system drifts out of sync.

Steps

  1. Configure a stable transactional.id so restarts fence previous zombie producers. Expected: Only one live producer instance can write.
  2. Begin a transaction, write to the database, and produce the event within it. Expected: Both effects are part of one unit.
  3. Commit the transaction; on any error, abort so neither effect is visible. Expected: Failures leave no partial state.
  4. Keep the consumer isolation level read_committed so it never sees aborted writes. Expected: Downstream only sees committed events.
await producer.transaction(async (tx) => {
  await db.query('INSERT INTO orders ...');
  await tx.send({ topic: 'orders', messages: [event] });
});
// abort on throw leaves neither effect visible

Verify

  • Only one live producer instance can write.
  • Both effects are part of one unit.
  • Failures leave no partial state.
  • Downstream only sees committed events.

Limits

  • Transactions add latency and require broker support; they are unsuitable for very high-throughput best-effort streams.
  • A transaction guarantees atomicity across Kafka and a store only if that store participates in the same protocol.