Skip to content

Apache Kafka trong Hệ thống Ngân hàng

By Nhân Nguyễn on Apr 28, 2026

Apache Kafka trong Hệ thống Ngân hàng

Apache Kafka không còn là một công cụ “nice‑to‑have” – trong các hệ thống ngân hàng hiện đại, nó là xương sống của toàn bộ luồng sự kiện, từ giao dịch, thanh toán cho đến audit log và phòng chống gian lận. Tuy nhiên, triển khai Kafka trong môi trường đòi hỏi độ tin cậy tuyệt đối như ngân hàng lại đặt ra những bài toán khó về độ bền vững, thứ tự, khả năng mở rộng và an toàn dữ liệu.

Bài viết này sẽ dẫn dắt bạn qua một hành trình toàn diện: từ một sự cố thực tế tại X Bank, bạn sẽ hiểu được mô hình tư duy cốt lõi của Kafka, sau đó đi sâu vào cấu hình production‑grade với code Java Spring Boot. Cuối cùng, chúng ta sẽ điểm qua những anti‑pattern thường gặp và bộ câu hỏi phỏng vấn giúp bạn sẵn sàng cho mọi tình huống.


1. Tình huống thực tế tại X Bank

Hãy tưởng tượng hệ thống chuyển tiền của X Bank sử dụng Kafka để phát tán các sự kiện transaction.created, transaction.completed, transaction.failed. Một consumer group gồm 3 instance có nhiệm vụ ghi audit log từ những sự kiện này vào cơ sở dữ liệu.

Mọi thứ vận hành ổn định cho đến khi một consumer bất ngờ crash và khởi động lại. Ngay lập tức, toàn bộ group phải thực hiện rebalance – một khoảng “stop‑the‑world” kéo dài 30 giây. Trong thời gian đó, không một giao dịch nào được ghi nhận. Tồi tệ hơn, sau khi rebalance, consumer mới nhận lại một số message đã xử lý một phần, dẫn đến tình trạng ghi trùng audit log (double audit entry). Đồng thời, dù có tới 3 instance, chỉ có 2 thực sự làm việc (vì topic chỉ có 2 partition) – instance thứ ba ngồi chơi xơi nước. Đỉnh điểm, một số sự kiện bị mất hoàn toàn khi consumer crash giữa lúc poll message nhưng chưa kịp commit offset.

Từ sự cố này, 5 câu hỏi lớn đặt ra, cũng chính là trọng tâm chúng ta sẽ giải quyết:

  1. Phân phối partition: Tại sao 3 consumer mà chỉ 1 hoặc 2 hoạt động? Nếu tăng partition từ 2 lên 6 thì sao?
  2. Offset commit: Nếu dùng auto‑commit, offset sẽ được commit trước hay sau khi ghi audit log? Nếu crash ngay giữa chừng thì chuyện gì xảy ra?
  3. Rebalancing: Tại sao rebalance lại là “stop‑the‑world”? Làm sao để giảm downtime từ 30 giây xuống còn 5 giây?
  4. Exactly‑once delivery: Làm sao để một giao dịch chỉ được ghi audit một lần duy nhất bất chấp rebalance, crash hay network delay?
  5. Đảm bảo thứ tự theo tài khoản: Nếu 2 consumer cùng xử lý song song các sự kiện của một accountId, thứ tự ghi nhận có thể bị đảo lộn. Giải pháp?

2. Mô hình tư duy – Hiểu Kafka đúng cách

Trước khi lao vào code, bạn cần nắm thật vững một số khái niệm cốt lõi. Chúng sẽ là kim chỉ nam cho mọi quyết định thiết kế.

2.1 Kiến trúc cơ bản: Topic, Partition, Consumer Group

Một topic (ví dụ transaction‑events) là một dòng log được chia thành nhiều partition. Mỗi partition là một cấu trúc dữ liệu chỉ‑thêm‑vào (append‑only), với các message được đánh số tuần tự bởi offset (0, 1, 2, …). Quan trọng: thứ tự của message chỉ được đảm bảo trong cùng một partition, không bao giờ đảm bảo giữa các partition.

Consumer group là một nhóm các consumer cùng chia sẻ công việc đọc một topic. Nguyên tắc “vàng”: một partition chỉ được gán cho tối đa một consumer trong cùng một group tại một thời điểm. Như vậy, nếu topic có 2 partition mà group có 3 consumer, một consumer sẽ rảnh rỗi – đó chính là lý do instance thứ ba của X Bank “ngồi chơi”. Muốn tăng song song thực sự, bạn phải tăng số partition ít nhất bằng số consumer mong muốn.

Hai group khác nhau có thể đọc cùng topic với các offset hoàn toàn độc lập – group A có thể đang ở offset 100, trong khi group B mới bắt đầu từ offset 0.

2.2 Producer: Chọn mức xác nhận (acks)

Mức xác nhận quyết định độ an toàn của message:

  • acks=0 (fire‑and‑forget): Producer gửi xong là bỏ đi, không chờ broker phản hồi. Tốc độ cao nhất nhưng dễ mất message – cấm kỵ trong ngân hàng.
  • acks=1 (leader ack): Producer chờ leader của partition xác nhận. Nếu leader vừa nhận message nhưng chưa kịp nhân bản sang các follower thì gặp sự cố → message vẫn có thể mất. Rủi ro ở mức trung bình.
  • acks=all (hay acks=-1): Producer buộc phải chờ tất cả các bản sao trong danh sách In‑Sync Replica (ISR) xác nhận. Chậm hơn vì phải chờ nhiều network round‑trip, nhưng an toàn tuyệt đối. Đây là lựa chọn mặc định cho các hệ thống tài chính.

Để tăng cường thêm, broker có thể được cấu hình min.insync.replicas=2. Với acks=all, producer sẽ chỉ coi việc ghi là thành công khi có ít nhất 2 bản sao (bao gồm leader) đã lưu message – giúp chống mất dữ liệu kể cả khi một node chết ngay sau đó.

2.3 Consumer: At‑Most‑Once, At‑Least‑Once và Exactly‑Once

Đây là “bộ ba” delivery semantic mà bạn phải thuộc nằm lòng:

  • At‑Most‑Once: Offset được commit trước khi xử lý message. Nếu consumer crash sau khi commit nhưng trước khi xử lý xong → message bị mất hoàn toàn (offset đã nhảy qua, sẽ không bao giờ đọc lại).
  • At‑Least‑Once: Offset được commit sau khi xử lý message. Nếu consumer crash sau khi process nhưng trước khi commit → khi khởi động lại, cùng một message sẽ được xử lý lại. Đây là lựa chọn phổ biến hơn, nhưng đòi hỏi idempotency (tính toàn đẳng) ở phía xử lý để không gây hậu quả gấp đôi (như double payment).
  • Exactly‑Once: Đây không phải là “mỗi message được xử lý đúng một lần” mà là kết quả cuối cùng giống như message chỉ được xử lý một lần. Trong Kafka, nó đạt được nhờ kết hợp:
    • Idempotent Producer: Producer gắn một sequence number cho mỗi message trên mỗi partition. Broker tự động phát hiện và loại bỏ message trùng lặp nếu producer retry.
    • Transactional API: Cho phép nhóm thao tác produce message và commit offset trong cùng một transaction nguyên tử. Consumer chỉ đọc các message đã được commit với isolation.level=read_committed.

Hãy ghi nhớ: Exactly‑Once thực sự chỉ có nghĩa khi bạn có một pipeline “đầu cuối” xuyên suốt, từ producer, qua Kafka, đến consumer – và thường vẫn cần một cơ chế dedup bổ sung ở tầng ứng dụng.

2.4 Rebalancing – “Stop‑the‑world” là gì và cách giảm thiểu

Rebalance xảy ra khi:

  • Một consumer mới tham gia vào group.
  • Một consumer rời group (shutdown có kiểm soát).
  • Một consumer bị coi là chết (timeout session hoặc heartbeat).
  • Số lượng partition của topic thay đổi.
  • Người quản trị chủ động gọi rebalance.

Trong quá trình rebalance truyền thống (eager), tất cả consumer phải ngừng poll, thu hồi toàn bộ partition cũ, commit offset, chờ broker gán partition mới, rồi mới tiếp tục. Khoảng thời gian này dao động từ vài giây đến 30 giây – là lúc toàn bộ pipeline ngưng trệ, lag bắt đầu tích tụ. Với banking, điều này có nghĩa SLAs bị vi phạm.

Giải pháp hiện đại là Cooperative Rebalancing (KIP‑429). Thay vì thu hồi tất cả, chỉ những partition thực sự bị ảnh hưởng mới được gán lại. Những consumer không bị thay đổi partition vẫn tiếp tục xử lý bình thường. Kết quả: downtime rút ngắn từ 30 giây xuống còn vài giây. Để kích hoạt, bạn chỉ cần chỉ định:

partition.assignment.strategy = org.apache.kafka.clients.consumer.CooperativeStickyAssignor

Ngoài ra, tinh chỉnh các timeout rất quan trọng để tránh rebalance ngoài ý muốn:

  • max.poll.interval.ms (mặc định 5 phút): Thời gian tối đa giữa hai lần poll. Nếu consumer đang xử lý một batch quá lâu (ví dụ do DB chậm), nó có thể bị “kick” khỏi group. Hãy đảm bảo giá trị này lớn hơn thời gian xử lý tối đa của bạn.
  • session.timeout.ms (mặc định 10 giây) và heartbeat.interval.ms: Kiểm soát cơ chế phát hiện lỗi. Nếu heartbeat không đến đúng hạn, consumer bị coi là chết. Giảm session.timeout.ms giúp phát hiện lỗi nhanh hơn nhưng cũng tăng nguy cơ rebalance do network giật lag.

3. Triển khai Production‑Grade

Giờ là lúc viết code thực chiến. Các đoạn cấu hình dưới đây sử dụng Spring Kafka và nhắm đến độ tin cậy cao nhất.

3.1 Producer: Cấu hình Idempotent & ACKS=ALL

Producer ngân hàng cần: không mất message, không trùng khi retry, và đảm bảo message đến đúng partition dựa trên key.

@Configuration
public class KafkaProducerConfig {

    @Bean
    public ProducerFactory<String, TransactionEvent> producerFactory() {
        Map<String, Object> props = new HashMap<>();

        // Hai broker cơ bản
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-broker-1:9092,kafka-broker-2:9092");

        // Bắt buộc: chờ tất cả ISR xác nhận
        props.put(ProducerConfig.ACKS_CONFIG, "all");

        // Retry tối đa 3 lần
        props.put(ProducerConfig.RETRIES_CONFIG, 3);

        // Đảm bảo thứ tự khi retry (chỉ 1 request in‑flight)
        props.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 1);

        // Idempotent producer: brokers sẽ tự động loại bỏ message trùng hoá
        props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);

        // Transactional ID – cần cho exactly‑once xuyên suốt
        props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "x-bank-payment-producer");
        props.put(ProducerConfig.TRANSACTION_TIMEOUT_CONFIG, 60000);

        // Bảo vệ: acks=all chỉ thành công nếu ít nhất 2 replica đang sống
        props.put(ProducerConfig.MIN_INSYNC_REPLICAS_CONFIG, 2);

        // Hiệu năng: batch 16KB, chờ tối đa 10ms để gom batch, nén snappy
        props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
        props.put(ProducerConfig.LINGER_MS_CONFIG, 10);
        props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "snappy");
        props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 67108864); // 64 MB

        // Serializers
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);

        return new DefaultProducerFactory<>(props);
    }

    @Bean
    public KafkaTemplate<String, TransactionEvent> kafkaTemplate(
            ProducerFactory<String, TransactionEvent> factory) {
        KafkaTemplate<String, TransactionEvent> template = new KafkaTemplate<>(factory);
        template.setDefaultTopic("transaction-events");
        return template;
    }
}

Lưu ý: MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION=1 chưa hẳn là bắt buộc nếu đã bật idempotence (Kafka có thể xử lý nhiều request in‑flight nhưng vẫn duy trì thứ tự), nhưng với banking, an toàn vẫn là trên hết.

3.2 Consumer: Manual commit và Idempotency

Consumer phải tự commit offset sau khi xử lý thành công, đồng thời chống duplicate bằng cơ chế idempotency (ở đây dùng cache Redis như một ví dụ).

@Service
@RequiredArgsConstructor
public class TransactionAuditConsumer {

    private final AuditLogRepository auditRepository;
    private final IdempotencyCache idempotencyCache;
    private static final Logger log = LoggerFactory.getLogger(TransactionAuditConsumer.class);

    @KafkaListener(
        topics = "transaction-events",
        groupId = "audit-logger-group"
    )
    public void consumeTransactionEvent(
            @Payload TransactionEvent event,
            @Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition,
            @Header(KafkaHeaders.OFFSET) long offset,
            Acknowledgment ack) {

        String eventKey = event.getTransactionId() + "-" + offset;

        try {
            // Idempotency check: nếu đã xử lý rồi thì chỉ cần ack và return
            if (idempotencyCache.isProcessed(eventKey)) {
                log.warn("Event already processed: {} at offset {}", event.getTransactionId(), offset);
                ack.acknowledge();
                return;
            }

            // Business logic: ghi audit log
            AuditLogEntry audit = new AuditLogEntry();
            audit.setTransactionId(event.getTransactionId());
            audit.setEventType(event.getEventType());
            audit.setAccountId(event.getAccountId());
            audit.setAmount(event.getAmount());
            audit.setTimestamp(LocalDateTime.now());
            auditRepository.save(audit);

            // Đánh dấu đã xử lý trước khi commit offset
            idempotencyCache.markProcessed(eventKey);

            // Commit offset sau khi mọi thứ thành công
            ack.acknowledge();

            log.info("Audit logged for transaction: {} | partition: {} | offset: {}",
                     event.getTransactionId(), partition, offset);

        } catch (DataIntegrityException e) {
            // Trùng khóa trong DB, coi như đã xử lý
            log.warn("Audit already exists (likely duplicate): {}", event.getTransactionId());
            idempotencyCache.markProcessed(eventKey);
            ack.acknowledge();
        } catch (Exception e) {
            // Lỗi thực sự: không ack, message sẽ được retry
            log.error("Failed to process event: {}", event.getTransactionId(), e);
            throw new RuntimeException("Consumer failed, will retry", e);
        }
    }

    // Dead Letter Topic listener – xử lý message đã retry quá số lần
    @KafkaListener(topics = "transaction-events.dlt")
    public void handleDltMessage(@Payload TransactionEvent event) {
        log.error("Message moved to DLT after max retries: {}", event.getTransactionId());
        // Gửi cảnh báo, cần can thiệp thủ công
    }
}

Idempotency cache (Redis) nên có TTL phù hợp với thời gian commit offset. Ngoài ra, bạn có thể dùng ràng buộc unique trong chính bảng audit (ví dụ transaction_id + event_type) thay cho cache – vừa đơn giản vừa không lo cache bị mất. Tuy nhiên, cache giúp giảm tải cho DB.

3.3 Listener Container Factory – Manual commit & Cooperative Rebalancer

Cấu hình này là “bộ não” cho consumer group:

@Configuration
public class KafkaConsumerConfig {

    @Bean
    public ConsumerFactory<String, TransactionEvent> consumerFactory() {
        Map<String, Object> props = new HashMap<>();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-broker-1:9092,kafka-broker-2:9092");
        props.put(ConsumerConfig.GROUP_ID_CONFIG, "audit-logger-group");

        // Tắt auto‑commit – chúng ta sẽ commit thủ công
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);

        // Mỗi lần poll tối đa 500 record
        props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);

        // Sử dụng cooperative rebalancer để giảm downtime
        props.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,
                "org.apache.kafka.clients.consumer.CooperativeStickyAssignor");

        // Timeout: nếu xử lý lâu hơn 5 phút => rebalance
        props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 300000); // 5 phút

        // Heartbeat: session timeout 30s, gửi heartbeat mỗi 10s
        props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 30000);
        props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, 10000);

        // Nếu không tìm thấy offset đã commit => bắt đầu từ message sớm nhất
        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");

        // Chỉ đọc message đã commit (transactional producer)
        props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed");

        // Deserializers
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
        props.put(JsonDeserializer.VALUE_DEFAULT_TYPE, TransactionEvent.class.getName());
        props.put(JsonDeserializer.TRUSTED_PACKAGES, "*");

        return new DefaultConsumerFactory<>(props);
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory<String, TransactionEvent> kafkaListenerContainerFactory(
            ConsumerFactory<String, TransactionEvent> consumerFactory) {
        var factory = new ConcurrentKafkaListenerContainerFactory<String, TransactionEvent>();
        factory.setConsumerFactory(consumerFactory);
        factory.setConcurrency(3); // 3 threads – khớp với số partition

        // Error handler: retry 3 lần rồi đẩy vào DLT
        factory.setCommonErrorHandler(kafkaErrorHandler());
        return factory;
    }

    @Bean
    public CommonErrorHandler kafkaErrorHandler() {
        var backOff = new FixedBackOffPolicy();
        backOff.setBackOffPeriod(1000); // 1s giữa các lần retry

        DeadLetterPublishingRecoverer recoverer =
                new DeadLetterPublishingRecoverer(kafkaTemplate());
        var errorHandler = new DefaultErrorHandler(recoverer, backOff);
        errorHandler.setMaxFailures(3);
        return errorHandler;
    }
}

3.4 Transactional Producer‑Consumer: Exactly‑Once xuyên suốt

Để đạt được exactly‑once khi vừa ghi database vừa produce message (ví dụ trong outbox pattern hoặc SAGA), bạn có thể sử dụng Kafka transactions. Đoạn code dưới đây minh hoạ cách atomic giữa lưu transaction vào DB và gửi event.

@Service
@RequiredArgsConstructor
public class TransactionEventProducer {

    private final KafkaTemplate<String, TransactionEvent> kafkaTemplate;
    private final TransactionRepository transactionRepository;

    public void publishTransactionEvent(Transaction transaction) {
        try {
            // executeInTransaction tự động bắt đầu transaction Kafka
            kafkaTemplate.executeInTransaction(ops -> {
                // Bước 1: lưu transaction vào DB
                transactionRepository.save(transaction);

                // Bước 2: publish event với key = accountId (đảm bảo thứ tự)
                TransactionEvent event = new TransactionEvent();
                event.setTransactionId(transaction.getId());
                event.setEventType("TRANSACTION_CREATED");
                event.setAccountId(transaction.getAccountId());
                event.setAmount(transaction.getAmount());
                event.setTimestamp(LocalDateTime.now());

                ops.send("transaction-events", transaction.getAccountId(), event);
                return null;
            });
            log.info("Transaction published: {}", transaction.getId());
        } catch (Exception e) {
            // Nếu có lỗi, cả DB và Kafka message đều bị hủy
            log.error("Failed to publish transaction event", e);
            throw new RuntimeException("Transaction publishing failed", e);
        }
    }
}

Với cấu hình producer TRANSACTIONAL_ID_CONFIGisolation.level=read_committed ở consumer, bạn đã có một pipeline exactly‑once end‑to‑end.

3.5 Chiến lược Key = accountId để vừa có thứ tự, vừa có song song

Khi producer gửi message với key là accountId, hash của key này sẽ quyết định partition. Như vậy mọi sự kiện của cùng một tài khoản luôn rơi vào cùng một partition, đảm bảo xử lý tuần tự trong phạm vi tài khoản đó. Các tài khoản khác nhau sẽ được phân phối đều sang các partition khác, cho phép xử lý song song thực sự.

@Service
public class TransactionEventService {

    @KafkaListener(topics = "transaction-events", groupId = "payment-processor-group")
    public void processTransactionWithOrdering(
            @Payload TransactionEvent event,
            @Header(name = "kafka_receivedPartitionId") int partition,
            Acknowledgment ack) {

        log.info("Processing event for account: {} on partition: {}",
                 event.getAccountId(), partition);
        processPayment(event);
        ack.acknowledge();
    }
}

3.6 Xử lý backpressure: tự động pause/resume khi lag quá cao

Một consumer khôn ngoan nên biết tự bảo vệ mình khi lag (độ trễ) tăng vọt – ví dụ khi database downstream bị chậm.

@Service
@RequiredArgsConstructor
public class AdaptiveConsumer {

    private final KafkaListenerEndpointRegistry registry;
    private final MetricsService metricsService;

    @Scheduled(fixedRate = 5000)
    public void checkConsumerLag() {
        ConsumerGroupMetrics metrics = metricsService.getConsumerGroupMetrics("audit-logger-group");
        long lag = metrics.getTotalLag();

        if (lag > 100_000) {  // lag quá cao -> tạm dừng poll
            log.warn("Consumer lag high ({}), pausing consumption", lag);
            registry.getListenerContainer("audit-listener").pause();
        } else if (lag < 10_000) {
            log.info("Consumer lag low ({}), resuming consumption", lag);
            registry.getListenerContainer("audit-listener").resume();
        }
    }
}

4. Trade‑offs và Anti‑Patterns thường gặp

Ngay cả những kỹ sư giàu kinh nghiệm cũng có thể mắc phải những sai lầm sau nếu không để ý đến đặc thù của Kafka.

Anti‑Pattern #1: Auto‑commit trước khi xử lý

// ❌ SAI: auto‑commit = true, commit trước khi process
@KafkaListener(topics = "events")
public void wrong(Event e) {
    processDatabaseInsert(e); // lỗi ở đây -> mất dữ liệu vì offset đã nhảy
}

Hậu quả: Mất dữ liệu hoàn toàn. Cách duy nhất để sửa là kiểm tra lại toàn bộ history.
Cách đúng: Tắt auto‑commit, xử lý xong rồi mới ack.acknowledge().

Anti‑Pattern #2: Xử lý consumer mà không có idempotency

// ❌ Thiếu idempotency -> nếu message retry, payment bị duplicate
paymentRepository.save(payment);

Hậu quả: Double charge, double audit, khó đối soát.
Cách đúng: Sử dụng dedup key (cache hoặc DB unique constraint) trước khi thực hiện business logic.

Anti‑Pattern #3: Quá ít partition so với số consumer cần scale

Nếu topic chỉ có 2 partition nhưng bạn triển khai 10 consumer instance, chỉ 2 hoạt động – lãng phí tài nguyên. Số partition nên được thiết kế dựa trên mức độ song song kỳ vọng, đồng thời nhớ rằng việc tăng partition sau này tuy khả thi nhưng có thể gây rebalance không mong muốn.

Anti‑Pattern #4: Nhúng payload lớn vào message

Việc gửi file PDF 5MB qua Kafka sẽ làm nghẽn network, đầy ổ cứng broker, và khiến rebalance cực kỳ chậm (vì phải fetch những message nặng).
Giải pháp: Áp dụng Claim‑Check Pattern – lưu file lên S3, chỉ gửi URL qua Kafka. Consumer tự tải file khi cần.

Anti‑Pattern #5: Gọi đồng bộ database trong consumer không có timeout

Một câu SQL bị lock quá 5 phút có thể khiến max.poll.interval.ms trôi qua và consumer bị kick khỏi group, gây ra rebalance dây chuyền.
Cách đúng: Đặt timeout cho mọi thao tác blocking (ví dụ @Transactional(timeout=10)), hoặc chuyển xử lý nặng sang một thread pool riêng và giải phóng poll thread ngay.


5. Phỏng vấn Kafka: Những câu hỏi “tủ” và gợi ý trả lời

Khi phỏng vấn cho vị trí backend liên quan đến event‑driven hệ thống tài chính, gần như chắc chắn bạn sẽ gặp các câu hỏi về Kafka. Dưới đây là ba tầng kiến thức mà nhà tuyển dụng thường muốn kiểm tra.

Tầng 1: Hiểu bề mặt

Q: Tại sao partition lại quan trọng? Nếu consumer group có 3 member mà topic chỉ có 2 partition, điều gì xảy ra?
A: Partition là đơn vị song song tối thiểu. Mỗi partition chỉ gán được cho 1 consumer trong group. Với 3 consumer, 2 hoạt động, 1 rảnh. Muốn scale thì phải tăng partition. Quá nhiều partition có thể gây tăng overhead I/O và thời gian rebalance.

Q: Nếu auto‑commit=true và consumer crash lúc đang xử lý thì sao?
A: Auto‑commit chạy theo chu kỳ (mặc định 5 giây). Nếu crash sau khi broker đã commit offset nhưng consumer chưa xử lý xong → message đó bị mất. Nếu crash trước khi commit → message sẽ được gửi lại, nhưng có thể mất thời gian phát hiện. Trong banking, luôn dùng manual commit.

Tầng 2: Kỹ thuật sâu

Q: Thiết kế consumer group xử lý chuyển tiền với yêu cầu: không mất dữ liệu, thứ tự theo tài khoản, scale 100 account song song, không ghi duplicate audit.
A:

  • Producer: acks=all, idempotence=true, key là accountId.
  • Topic: ≥ 100 partition để đảm bảo đủ thread song song.
  • Consumer: manual commit, isolation.level=read_committed. Sau khi xử lý mới gọi ack.acknowledge().
  • Idempotency: dedup cache Redis hoặc DB unique constraint (transactionId + eventType).
  • Nếu xử lý lỗi sau vài lần retry -> gửi sang Dead Letter Topic.

Q: Rebalancing là gì và làm sao giảm thời gian chết?
A: Rebalance là quá trình gán lại partition khi có sự thay đổi thành viên trong group. Eager rebalance gây “stop‑the‑world” vì tất cả consumer đều bị thu hồi partition. Cooperative rebalance (KIP‑429) chỉ thu hồi partition bị ảnh hưởng, giảm downtime xuống vài giây. Thêm vào đó, cần tinh chỉnh max.poll.interval.ms, session.timeout.ms để tránh rebalance ngoài ý muốn.

Tầng 3: System Design

Q: Thiết kế hệ thống log tập trung cho 100 chi nhánh, 1 triệu message/giây, lưu trữ 1 năm, search real‑time, không ảnh hưởng API.
A:

  • Producer: Fire‑and‑forget từ các server (async, non‑blocking) vào Kafka.
  • Kafka cluster: 3 broker, 100+ partition, RF=3, acks=all.
  • Consumers: 3 pipeline song song:
    • Consumer A: batch insert vào DB audit (5 giây window).
    • Consumer B: real‑time index vào Elasticsearch.
    • Consumer C: archive lên S3 (nén và lưu dài hạn).
  • High availability: Khi DB chết, message vẫn nằm trong Kafka (retention 7 ngày), consumer sẽ catch up sau.
  • Trade‑off: Độ trễ audit log có thể chấp nhận vài giây. Elasticsearch cluster riêng biệt để tránh ảnh hưởng lẫn nhau.

6. Checklist thành thạo (chỉ cần trả lời không cần note)

Hãy tự kiểm tra – bạn đã sẵn sàng chưa?

  • Partition & Offset: Partition là sequence log có offset tăng dần. Mỗi partition có offset riêng, consumer lưu “committed offset” để biết đọc tiếp từ đâu. Lag là khoảng cách giữa offset mới nhất và offset đã commit.
  • Quy tắc Consumer Group: 1 partition → tối đa 1 consumer trong cùng group. Số consumer hoạt động ≤ số partition. Các group khác nhau có offset riêng. Strategies: RangeAssignor, RoundRobin, Sticky, CooperativeSticky.
  • Delivery semantics: At‑most‑once (commit trước, mất khi crash) – không dùng cho banking. At‑least‑once (commit sau, cần idempotency). Exactly‑once: producer idempotent + transactional + dedup.
  • Rebalancing: Trigger bởi join/leave/crash/thay đổi partition. Cooperative giảm downtime. Cần điều chỉnh timeout để tránh rebalance ngoài ý muốn.
  • Key‑based ordering: Message cùng key vào 1 partition → xử lý tuần tự. Các key khác nhau xử lý song song. Tận dụng cho kế toán per account.

Và đừng quên production checklist hàng ngày:

  • Producer: acks=all, idempotence, retries.
  • Consumer: manual commit, idempotency check.
  • Dead Letter Topic được cấu hình sẵn.
  • Số partition ≥ số consumer song song dự kiến.
  • max.poll.interval.ms > max processing time.
  • Giám sát: consumer lag, tần suất rebalance, error rate.
  • Test định kỳ: rebalance khi broker fail, consumer crash, partition thay đổi.

Lời kết

Apache Kafka là một cỗ máy mạnh mẽ nhưng cũng đầy cạm bẫy nếu ta không thực sự hiểu rõ mô hình của nó. Với những hệ thống ngân hàng – nơi mỗi message đều mang giá trị tài chính – việc nắm vững partition, delivery semantics, rebalancing và idempotency không chỉ là lý thuyết, mà là yêu cầu sống còn. Hy vọng qua bài viết này, bạn đã có một bức tranh toàn cảnh và sẵn sàng áp dụng vào dự án thực tế của mình.

Hãy kết nối

Nếu bạn quan tâm tới việc hợp tác, có câu hỏi về bài viết, hay chỉ đơn giản muốn chuyện trò về backend — cứ ping mình nhé.