分布式微服务系统架构第167集:从零到能跑kafka-redis实战

加群联系作者vx:xiaoda0423

仓库地址:webvueblog.github.io/JavaPlusDoc...

1024bat.cn/

github.com/webVueBlog/...

webvueblog.github.io/JavaPlusDoc...

点击勘误issues,哪吒感谢大家的阅读

1. Redis 一次性拉取数据可不可行?

取决于你要拉的数据量和 Redis 的内存/网络情况。

  • 小数据量(KB ~ MB 级) :一次性拉取完全没问题,Redis 单线程响应极快,网络带宽也能支撑。

  • 大数据量(几十 MB ~ GB 级) :一次性拉取会有几个风险:

    1. 阻塞 Redis:Redis 是单线程处理命令的,大 key 或大批量数据一次取出会长时间占用 CPU,阻塞其他请求。
    2. 内存和网络开销大:一次性把很多数据打包返回,服务器需要序列化成 TCP 包,客户端要解包,容易卡顿甚至 OOM。
    3. 客户端反序列化耗时 :比如 jedisTemplate.opsForValue().get("bigKey") 返回 1GB 数据,客户端 JVM 可能直接炸掉。

所以:一次拉取所有数据不是最佳实践,更推荐分批次(分页、游标)拉取。


2. 推荐的安全拉取方式

🔹 使用 SCAN 游标遍历

比 KEYS 命令更安全,避免阻塞。

scss 复制代码
// 每次扫描 500 条
Cursor<byte[]> cursor = redisTemplate.getConnectionFactory()
        .getConnection()
        .scan(ScanOptions.scanOptions().match("prefix:*").count(500).build());

while (cursor.hasNext()) {
    String key = new String(cursor.next());
    String value = redisTemplate.opsForValue().get(key);
    // 处理 value ...
}
  • 优势:渐进式遍历,不会阻塞,内存友好。
  • 劣势:遍历完成需要时间,非实时。

🔹 分批分页拉取(LIST、ZSET、HASH 常见)

例如从 Redis List 分页:

ini 复制代码
int pageSize = 500;
long start = 0;
long end = start + pageSize - 1;

while (true) {
    List<String> items = redisTemplate.opsForList().range("mylist", start, end);
    if (items == null || items.isEmpty()) break;

    // 处理 items
    start += pageSize;
    end += pageSize;
}

这样不会一次性把整个 list 拉回来。


🔹 消息流式处理(推荐大数据场景)

如果你是做日志/订单/IoT 场景,Redis Streams 更合适:

less 复制代码
List<MapRecord<String, Object, Object>> messages =
        redisTemplate.opsForStream().read(
            Consumer.from("group1", "consumer1"),
            StreamReadOptions.empty().count(100),
            StreamOffset.create("mystream", ReadOffset.lastConsumed())
        );

一次只取少量数据,边消费边 ack,天然适合大规模并发。


3. 多节点 Redis 下的扩展

  • 主从复制 (Master-Slave) :读写分离,可以从节点拉取大批量数据,减轻主节点压力。
  • 哨兵 (Sentinel) :自动主备切换,保证高可用。
  • 集群模式 (Cluster) :数据分片分布在多个节点,单 key 按哈希 slot 落在一个节点上。拉全量数据要跨节点遍历。

👉 在 分布式场景 下,建议:

  • SCAN + pipeline 遍历多个节点,减少 RTT。
  • 数据特别大时,考虑 Kafka / ClickHouse / ElasticSearch 等更合适的 OLAP 系统,不要把 Redis 当大数据库用。

4. 总结

  • Redis 可以一次拉,但只适合小数据。
  • 大数据要 分页 / 游标 / 流式 处理,否则可能卡死 Redis 或拖垮 JVM。
  • 多节点扩展要结合 Cluster / Sentinel / Master-Slave,并避免单点压力。

按顺序照着做,一次就能通。

0. 你会得到什么

  • 本地一键起 Kafka + 可视化控制台
  • 用命令行创建/生产/消费消息
  • Java(Spring Boot)最小可用的生产者/消费者示例
  • 常用生产级配置(幂等、重试、DLT、事务/准严格一次)
  • 物联网/高并发场景的落地建议与排错清单

1. 本地一键起 Kafka(最省事的 Compose)

适合入门实验:使用 Confluent 社区镜像(带 ZooKeeper,配置简单)

新建 docker-compose.yml

yaml 复制代码
version: '3.8'
services:
  zookeeper:
    image: confluentinc/cp-zookeeper:7.4.0
    container_name: zookeeper
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000
    ports: ["2181:2181"]

  broker:
    image: confluentinc/cp-kafka:7.4.0
    container_name: broker
    depends_on: [zookeeper]
    ports: ["9092:9092","29092:29092"]
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_INTERNAL:PLAINTEXT
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,PLAINTEXT_INTERNAL://broker:29092
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT_INTERNAL
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1

  kafka-ui:
    image: provectuslabs/kafka-ui:v0.7.2
    container_name: kafka-ui
    depends_on: [broker]
    ports: ["8080:8080"]
    environment:
      KAFKA_CLUSTERS_0_NAME: local
      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: broker:29092

启动:

复制代码
docker compose up -d

打开可视化控制台:http://localhost:8080(可建/看 Topic、消费组、消息)


2. 5 分钟命令行上手

创建一个 3 分区 Topic(本地副本数只能 1):

css 复制代码
docker exec -it broker kafka-topics --create \
  --topic demo.orders --partitions 3 --replication-factor 1 \
  --bootstrap-server localhost:9092

查看:

css 复制代码
docker exec -it broker kafka-topics --describe \
  --topic demo.orders --bootstrap-server localhost:9092

开一个生产者:

bash 复制代码
docker exec -it broker kafka-console-producer \
  --topic demo.orders --bootstrap-server localhost:9092
# 然后键盘输入几行文本回车发送

开一个消费者(从头消费):

css 复制代码
docker exec -it broker kafka-console-consumer \
  --topic demo.orders --from-beginning --bootstrap-server localhost:9092

3. 最小可用 Spring Boot Demo

3.1 依赖(Maven)

xml 复制代码
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>3.2.5</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
  </dependency>
  <dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
  </dependency>
</dependencies>

3.2 application.yml

yaml 复制代码
spring:
  kafka:
    bootstrap-servers: localhost:9092

    producer:
      acks: all
      retries: 10
      enable-idempotence: true
      compression-type: lz4
      batch-size: 32768
      linger-ms: 5
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer

    consumer:
      group-id: demo-consumer
      auto-offset-reset: earliest
      enable-auto-commit: false
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      properties:
        isolation.level: read_committed

    listener:
      ack-mode: MANUAL         # 手动提交位移
      concurrency: 3           # 并发线程数 = 分区数起步

3.3 生产者

less 复制代码
@RestController
@RequiredArgsConstructor
public class OrderController {
  private final KafkaTemplate<String, String> kafka;

  @PostMapping("/send")
  public String send(@RequestParam String orderId) {
    // 按 key 分区,保证同一订单顺序
    kafka.send("demo.orders", orderId, "NEW_ORDER:" + orderId);
    return "ok";
  }
}

3.4 消费者(带手动提交 + 简易重试 → DLT)

typescript 复制代码
@Configuration
@RequiredArgsConstructor
class KafkaErrorHandlingConfig {

  private final KafkaTemplate<String, String> kafkaTemplate;

  @Bean
  DefaultErrorHandler errorHandler() {
    // 3 次快速重试,失败则发往 DLT:demo.orders.DLT
    DeadLetterPublishingRecoverer recoverer =
        new DeadLetterPublishingRecoverer(kafkaTemplate,
          (rec, ex) -> new TopicPartition(rec.topic() + ".DLT", rec.partition()));

    var handler = new DefaultErrorHandler(recoverer, new FixedBackOff(1000L, 3));
    // 反序列化等无需重试的异常可直接跳过
    handler.addNotRetryableExceptions(IllegalArgumentException.class);
    return handler;
  }
}

@Component
@RequiredArgsConstructor
class OrderConsumer {
  private final AcknowledgmentNop ackNop = new AcknowledgmentNop();

  @KafkaListener(topics = "demo.orders", containerFactory = "kafkaListenerContainerFactory")
  public void onMessage(ConsumerRecord<String, String> rec, Acknowledgment ack) {
    try {
      // 业务处理
      System.out.println("consume: key=" + rec.key() + " value=" + rec.value());
      // 成功后提交位移
      ack.acknowledge();
    } catch (Exception e) {
      // 交给 DefaultErrorHandler(会按策略重试/投递DLT)
      throw e;
    }
  }
}

提示

  • 真正生产里,DLT 消费者会记录异常原因(从 headers 取 kafka_dlt-exception-class 等),落库/告警。
  • listener.concurrency 通常设置为 <= 分区数(1 个分区同一时刻只会被一个线程消费,保证分区内有序)。

3.5 事务(生产端 & 消费端"读已提交")

适合"入库 & 发消息要么都成功,要么都失败"的场景(如订单创建 + 发送事件)

typescript 复制代码
@Configuration
class TxConfig {

  @Bean
  public ProducerFactory<String, String> txProducerFactory(
      ObjectProvider<ProducerFactoryCustomizer<String, String>> customizers) {

    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);
    props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);

    DefaultKafkaProducerFactory<String, String> pf = new DefaultKafkaProducerFactory<>(props);
    pf.setTransactionIdPrefix("demo-tx-"); // 开启事务
    return pf;
  }

  @Bean
  public KafkaTemplate<String, String> txKafkaTemplate(ProducerFactory<String, String> pf) {
    return new KafkaTemplate<>(pf);
  }
}

@Service
@RequiredArgsConstructor
class OrderService {
  private final KafkaTemplate<String, String> kafka;

  @Transactional // 这里是 Spring 事务(若含数据库,需用同一个事务边界协调)
  public void createOrderAndPublish(String orderId) {
    // db.save(order) ...
    kafka.executeInTransaction(kt -> {
      kt.send("demo.orders", orderId, "CREATED:" + orderId);
      return true;
    });
  }
}

4. 你能立刻用的"生产级"关键点

  1. 分区与键
  • deviceId / orderId 作为 key → 保证同一实体顺序。
  • 并发 = 消费者线程数 ≈ 分区数(高吞吐可 12、24、48... 逐级扩)。
  1. 可靠投递
  • 生产者:acks=all + enable.idempotence=true + retries>0
  • linger.ms + batch.size + compression=lz4/zstd 提升吞吐、降带宽。
  1. "至少一次"+ 幂等消费
  • 消费端关闭自动提交,业务成功后手动 ack
  • 幂等:为每条消息使用唯一业务键(如 (deviceId, eventId))落库时唯一约束去重。
  1. 死信与告警
  • 消费失败重试 N 次后 → topic.DLT;DLT 消费者写异常表、飞书/钉钉/邮件告警。
  1. Schema 兼容
  • 生产环境推荐 Avro/Protobuf + Schema Registry(向后兼容)。
  • 主题命名建议:{domain}.{entity}.{event}.v{n}(例如 device.telemetry.up.v1)。
  1. 保留与压缩
  • 时间保留:retention.ms
  • 状态类主题用 日志压缩cleanup.policy=compact),仅保留每个 key 的最新值。
  1. 监控
  • 指标:消费者 Lag、吞吐、失败率、重试/死信量。
  • 组合:kafka-exporter + Prometheus + Grafana,或使用 kafka-ui 观察。
  1. 安全(可选)
  • 云/公网:开启 SASL/SCRAM + TLS,限制公网入口,仅允许内网/专线访问。

5. IoT/高并发场景落地(贴合你的常用场景)

  • 主题划分

    • 遥测上报:device.telemetry.up.v1(key=deviceId,分区内有序)
    • 设备状态(压缩):device.state.v1(cleanup=compact)
    • 命令下发回执:device.command.ack.v1
  • 网关 → Kafka

    • Netty 接入后,解析为标准事件(JSON/Avro),按 deviceId 作为 key 发 Kafka。
    • 避免"每设备一分区",而是"按哈希范围/地域/组织分区",分区数阶段性扩(如 24 → 48 → 96)。
  • 消费侧

    • 异常数据写 DLT + 审计表;
    • 入仓(ClickHouse/OLAP)用 Kafka Connect Sink 插件或自写消费者批量入库。
  • 扩容建议

    • 吞吐↑ → 先加分区再加实例;
    • "热键"设备(超高频)可路由到专用主题或专用分区。

6. 常见排错清单

  • 本地连不上 :确认 bootstrap-servers=localhost:9092;容器端口已映射;Windows 上建议 WSL2 或直接 Docker Desktop。
  • 消费者没有消息 :检查 group.id 是否相同;是否 from-beginning;是否已经提交过位移。
  • 顺序乱了:确认是否同一个 key;是否多分区并发消费。
  • 积压(Lag)增长:提高并发(分区/线程)、优化消费处理(异步批量)、加机器。
  • 序列化异常:先用字符串跑通,再切 Avro/Protobuf;Schema 变更要向后兼容。

7. 下一步你可以做的 3 件小事

  1. 把上面的 Compose 起起来,在 kafka-ui 创建/查看消息
  2. 跑通 Spring Boot 的 /send?orderId=10001,看消费者日志
  3. 故意抛异常,观察 DLT 里的消息和异常头(headers)

怎么跑(超简)

  1. 起 Kafka + UI
bash 复制代码
cd kafka-labs
docker compose up -d
# 打开 http://localhost:8080
  1. 订单场景(事务 + 幂等 + DLT)
bash 复制代码
cd orders-app
mvn spring-boot:run
# 发订单
curl -X POST "http://localhost:8081/orders?orderId=10001&amount=88.8"
  • 主题:demo.orders(失败自动重试→投递 demo.orders.DLT
  • 消费端手动提交位移,保序:同一 orderId 为 key
  1. IoT 场景(遥测上报 + 压缩状态流)
bash 复制代码
cd ../iot-app
mvn spring-boot:run
# 上报遥测
curl -X POST "http://localhost:8082/telemetry?deviceId=D001&temp=36.6&voltage=53.2"
  • 遥测主题:device.telemetry.up.v1
  • 状态主题(log compaction):device.state.v1

项目亮点

  • Docker 一键起 Kafka + Kafka UI(可视化建 topic、看消息、看消费组)
  • Spring Boot 3 + spring-kafka,生产者启用幂等、批量、压缩
  • 订单服务:事务性发送(executeInTransaction),失败自动重试 + DLT 落地
  • IoT 服务:按 deviceId 分区保序,状态主题已预留压缩使用场景
  • 消费端统一异常处理:DefaultErrorHandler + DeadLetterPublishingRecoverer
  • 配置都在 application.yml,开箱即用
相关推荐
whitepure几秒前
万字详解Java泛型
后端
whitepure2 分钟前
万字详解Java注解
java·后端
探索java5 分钟前
Tomcat Context的核心机制
java·后端·tomcat
纪莫12 分钟前
Kafka如何保证「消息不丢失」,「顺序传输」,「不重复消费」,以及为什么会发生重平衡(reblanace)
java·分布式·后端·中间件·kafka·队列
阿杆40 分钟前
零成本 Redis 实战:用Amazon免费套餐练手 + 缓存优化
redis·后端
舒一笑1 小时前
如何优雅统计知识库文件个数与子集下不同文件夹文件个数
后端·mysql·程序员
IT果果日记1 小时前
flink+dolphinscheduler+dinky打造自动化数仓平台
大数据·后端·flink
Java技术小馆1 小时前
InheritableThreadLoca90%开发者踩过的坑
后端·面试·github
寒士obj2 小时前
Spring容器Bean的创建流程
java·后端·spring
诗和远方14939562327342 小时前
iOS 异常捕获原理详解
面试