加群联系作者vx:xiaoda0423
仓库地址:webvueblog.github.io/JavaPlusDoc...
webvueblog.github.io/JavaPlusDoc...
点击勘误issues,哪吒感谢大家的阅读
1. Redis 一次性拉取数据可不可行?
取决于你要拉的数据量和 Redis 的内存/网络情况。
-
小数据量(KB ~ MB 级) :一次性拉取完全没问题,Redis 单线程响应极快,网络带宽也能支撑。
-
大数据量(几十 MB ~ GB 级) :一次性拉取会有几个风险:
- 阻塞 Redis:Redis 是单线程处理命令的,大 key 或大批量数据一次取出会长时间占用 CPU,阻塞其他请求。
- 内存和网络开销大:一次性把很多数据打包返回,服务器需要序列化成 TCP 包,客户端要解包,容易卡顿甚至 OOM。
- 客户端反序列化耗时 :比如
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. 你能立刻用的"生产级"关键点
- 分区与键
- 以
deviceId
/orderId
作为 key → 保证同一实体顺序。 - 并发 = 消费者线程数 ≈ 分区数(高吞吐可 12、24、48... 逐级扩)。
- 可靠投递
- 生产者:
acks=all + enable.idempotence=true + retries>0
。 linger.ms + batch.size + compression=lz4/zstd
提升吞吐、降带宽。
- "至少一次"+ 幂等消费
- 消费端关闭自动提交,业务成功后手动
ack
。 - 幂等:为每条消息使用唯一业务键(如
(deviceId, eventId)
)落库时唯一约束去重。
- 死信与告警
- 消费失败重试 N 次后 →
topic.DLT
;DLT 消费者写异常表、飞书/钉钉/邮件告警。
- Schema 兼容
- 生产环境推荐 Avro/Protobuf + Schema Registry(向后兼容)。
- 主题命名建议:
{domain}.{entity}.{event}.v{n}
(例如device.telemetry.up.v1
)。
- 保留与压缩
- 时间保留:
retention.ms
; - 状态类主题用 日志压缩 (
cleanup.policy=compact
),仅保留每个 key 的最新值。
- 监控
- 指标:消费者 Lag、吞吐、失败率、重试/死信量。
- 组合:
kafka-exporter + Prometheus + Grafana
,或使用kafka-ui
观察。
- 安全(可选)
- 云/公网:开启
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 件小事
- 把上面的 Compose 起起来,在
kafka-ui
创建/查看消息 - 跑通 Spring Boot 的
/send?orderId=10001
,看消费者日志 - 故意抛异常,观察 DLT 里的消息和异常头(headers)
怎么跑(超简)
- 起 Kafka + UI
bash
cd kafka-labs
docker compose up -d
# 打开 http://localhost:8080
- 订单场景(事务 + 幂等 + 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
- 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
,开箱即用