基于 Kafka Exactly-Once 语义保障微信群发消息不重复不丢失

基于 Kafka Exactly-Once 语义保障微信群发消息不重复不丢失

微信群发场景的可靠性要求

在企业服务中,通过 Kafka 异步触发微信群发任务(如通知、营销)时,必须确保:

  • 不丢失:每条待发消息至少被处理一次;
  • 不重复:即使消费者重启或重试,最终只发送一次。

Kafka 自 0.11 起支持 Exactly-Once Semantics (EOS),结合幂等生产者、事务与消费位移提交原子化,可满足该需求。

启用生产者幂等与事务

配置 KafkaProducer 支持事务:

java 复制代码
package wlkankan.cn.kafka.config;

import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class KafkaProducerConfig {

    @Bean
    public DefaultKafkaProducerFactory<String, String> kafkaProducerFactory() {
        Map<String, Object> props = new HashMap<>();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        
        // 启用幂等性(隐含 enable.idempotence=true)
        props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
        // 必须设置 transactional.id 才能使用事务
        props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "wecom-bulk-sender-tx");
        
        DefaultKafkaProducerFactory<String, String> factory = 
            new DefaultKafkaProducerFactory<>(props);
        factory.setTransactionIdPrefix("wecom-tx-"); // Spring Kafka 会自动追加后缀
        return factory;
    }
}

定义消息实体与发送服务

封装微信群发任务:

java 复制代码
package wlkankan.cn.model;

public class WeComMessage {
    private String msgId;      // 全局唯一ID,用于幂等
    private String content;
    private String receiver;
    // getters/setters
}

使用事务发送消息到 wecom.send.queue

java 复制代码
package wlkankan.cn.service;

import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class MessageQueueService {

    private final KafkaTemplate<String, String> kafkaTemplate;

    @Transactional
    public void enqueueWeComMessage(WeComMessage msg) {
        // 消息体序列化为 JSON
        String payload = JsonUtil.toJson(msg);
        // 发送至主题,key 使用 msgId 保证分区有序
        kafkaTemplate.send("wecom.send.queue", msg.getMsgId(), payload);
    }
}

消费者端:事务性监听与幂等发送

配置消费者开启事务并手动提交偏移:

java 复制代码
package wlkankan.cn.kafka.config;

import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.listener.ContainerProperties;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class KafkaConsumerConfig {

    @Bean
    public ConsumerFactory<String, String> consumerFactory() {
        Map<String, Object> props = new HashMap<>();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        props.put(ConsumerConfig.GROUP_ID_CONFIG, "wecom-sender-group");
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        // 必须关闭自动提交
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
        return new DefaultKafkaConsumerFactory<>(props);
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory(
            ConsumerFactory<String, String> consumerFactory,
            KafkaTemplate<String, String> kafkaTemplate) {
        ConcurrentKafkaListenerContainerFactory<String, String> factory =
            new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(consumerFactory);
        // 关键:启用 Exactly-Once 处理
        factory.setTransactionManager(new KafkaTransactionManager<>(kafkaTemplate.getProducerFactory()));
        factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
        return factory;
    }
}

幂等微信发送客户端

即使 Kafka 保证 Exactly-Once,仍需业务层幂等:

java 复制代码
package wlkankan.cn.wx.client;

import wlkankan.cn.dao.SentMessageRecordDao;
import wlkankan.cn.model.WeComMessage;

public class IdempotentWeComClient {

    private final SentMessageRecordDao recordDao;
    private final RawWeComApiClient apiClient;

    public void send(WeComMessage msg) {
        // 先查数据库是否已发送
        if (recordDao.existsByMsgId(msg.getMsgId())) {
            return; // 已发送,直接跳过
        }
        // 调用微信 API
        apiClient.sendMessage(msg.getReceiver(), msg.getContent());
        // 记录发送成功(事务内)
        recordDao.insert(msg.getMsgId(), System.currentTimeMillis());
    }
}

事务性消费者监听器

在同一个 Kafka 事务中完成"消费 + 微信发送 + 位移提交":

java 复制代码
package wlkankan.cn.listener;

import wlkankan.cn.model.WeComMessage;
import wlkankan.cn.wx.client.IdempotentWeComClient;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class WeComMessageListener {

    private final IdempotentWeComClient weComClient;

    @KafkaListener(topics = "wecom.send.queue")
    @Transactional
    public void onMessage(String payload) {
        WeComMessage msg = JsonUtil.fromJson(payload, WeComMessage.class);
        // 在 Kafka 事务上下文中执行发送
        weComClient.send(msg);
        // 若此处抛异常,Kafka 会回滚消费位移,消息重新投递
    }
}

数据库表结构支持幂等

sql 复制代码
CREATE TABLE sent_message_record (
    msg_id VARCHAR(64) PRIMARY KEY,
    sent_at BIGINT NOT NULL
);
-- msg_id 唯一索引天然防止重复插入

通过 Kafka 事务生产 + 事务消费 + 业务幂等 三层保障,微信群发消息在 Kafka 集群故障、消费者重启、网络抖动等场景下仍能严格实现 Exactly-Once 语义。所有核心组件位于 wlkankan.cn 包下,符合企业级高可靠消息系统设计规范。

相关推荐
洛豳枭薰16 小时前
消息队列关键问题描述
kafka·rabbitmq·rocketmq
lucky670716 小时前
Spring Boot集成Kafka:最佳实践与详细指南
spring boot·kafka·linq
Coder_Boy_16 小时前
基于Spring AI的分布式在线考试系统-事件处理架构实现方案
人工智能·spring boot·分布式·spring
袁煦丞 cpolar内网穿透实验室18 小时前
远程调试内网 Kafka 不再求运维!cpolar 内网穿透实验室第 791 个成功挑战
运维·分布式·kafka·远程工作·内网穿透·cpolar
岁岁种桃花儿18 小时前
CentOS7 彻底卸载所有JDK/JRE + 重新安装JDK8(实操完整版,解决kafka/jps报错)
java·开发语言·kafka
人间打气筒(Ada)18 小时前
GlusterFS实现KVM高可用及热迁移
分布式·虚拟化·kvm·高可用·glusterfs·热迁移
xu_yule18 小时前
Redis存储(15)Redis的应用_分布式锁_Lua脚本/Redlock算法
数据库·redis·分布式
難釋懷1 天前
分布式锁的原子性问题
分布式
ai_xiaogui1 天前
【开源前瞻】从“咸鱼”到“超级个体”:谈谈 Panelai 分布式子服务器管理系统的设计架构与 UI 演进
服务器·分布式·架构·分布式架构·panelai·开源面板·ai工具开发