Redis 实现消息队列解决服务数据一致性问题
背景
服务 A 是多个业务子系统依赖的公共系统(如用户中心服务),服务 B 是使用服务 A 接口的业务子系统。两者共用一个 Redis,无其他消息队列服务。
服务 B 存储了服务 A 的组织数据,不仅包含唯一键 id,还有组织名称、组织层级、组织架构路径等信息。当服务 A 修改这些组织数据时,需要确保服务 B 的数据能与之有效保持一致。
方案对比
方案一:定时任务全量更新
- 方式:固定时间全量获取服务 A 的组织最新数据,进行全量更新。
- 缺点:数据更新不及时,影响用户体验;每次全量更新对服务负荷较高。
方案二:Redis 消息队列增量即时更新
通过 Redis 实现消息队列,可实现增量即时更新。以下是四种主流实现方式及细节:
方式 |
原理 |
特点 |
List 结构(基础队列) |
利用 Redis 的 List 数据结构,生产者左入队(LPUSH),消费者右出队(BRPOP) |
✅ 简单高效,适合基础场景❌ 无消息确认,消费者崩溃后消息丢失❌ 单消费者限制,一条消息只能被一个消费者处理 |
Pub/Sub(发布订阅) |
生产者发布消息到频道(PUBLISH),消费者订阅频道(SUBSCRIBE) |
✅ 多消费者广播,一条消息可被多个消费者接收❌ 无消息持久化,离线消费者丢失消息❌ 无队列缓冲,消息实时传递,无积压能力 |
Stream(Redis 5.0+,推荐) |
专为消息队列设计的持久化流结构,支持多消费者组和消息确认 |
✅ 消息持久化,支持消息历史和重试✅ 多消费者组,不同组可独立消费同一消息✅ 具备消息确认机制,确保可靠处理✅ 有监控工具支持,XINFO、XPENDING 等命令便于运维 |
Sorted Set(延迟队列) |
用有序集合的分数(Score)存储执行时间戳,消费者轮询到期消息 |
✅ 支持延迟消息,适用于定时任务场景❌ 需轮询,消耗资源,频繁检查影响性能 |
最佳实践建议
- 优先选择 Stream:在需要可靠性的场景下使用,因其支持 ACK、消费者组、消息回溯。
- 消息保活:结合 XPENDING 命令监控未 ACK 的消息,超时后重新投递。
- 错误处理:消费者崩溃后,将未 ACK 的消息重新放入队列;使用唯一 ID(如 XADD ID * 生成)防重复处理。
- 集群部署:Redis Cluster 模式下,Stream 的 key 需哈希到同一 Slot(用 {} 包装相同前缀)。
⚠️ 注意:Redis 并非专业消息队列(如 RabbitMQ/Kafka),在极端高并发或大数据量场景下,可能存在性能瓶颈或功能缺失(如严格顺序性)。
实现
以下是服务 A(生产者)和服务 B(消费者)使用 Redisson 实现 Redis 消息队列的完整方案:
解决方案架构
采用 Redis Stream 作为消息队列,支持:
- 消息持久化
- 消费者组(支持多个消费者)
- 消息确认机制
- 失败消息重试
服务实现
- 服务 A(生产者)实现:
import org.redisson.Redisson;
import org.redisson.api.RStream;
import org.redisson.api.RedissonClient;
import org.redisson.api.StreamMessageId;
import org.redisson.config.Config;
public class ProducerService {
private static final String REDIS_HOST = "redis://127.0.0.1:6379";
private static final String STREAM_NAME = "service_queue";
private RedissonClient redisson;
public void init() {
Config config = new Config();
config.useSingleServer()
.setAddress(REDIS_HOST)
.setConnectionPoolSize(10)
.setConnectionMinimumIdleSize(5);
redisson = Redisson.create(config);
}
public void sendMessage(String message) {
RStream<String, String> stream = redisson.getStream(STREAM_NAME);
// 添加消息到队列
StreamMessageId id = stream.add(StreamAddArgs.entry("message", message));
System.out.println("消息已发送: " + id + " | 内容: " + message);
}
public void shutdown() {
if (redisson != null) {
redisson.shutdown();
}
}
public static void main(String[] args) {
ProducerService producer = new ProducerService();
producer.init();
// 模拟发送消息
for (int i = 1; i <= 5; i++) {
producer.sendMessage("订单#" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
producer.shutdown();
}
}
- 服务 B(消费者)实现:
import org.redisson.Redisson;
import org.redisson.api.RStream;
import org.redisson.api.RedissonClient;
import org.redisson.api.StreamMessageId;
import org.redisson.api.stream.StreamReadGroupArgs;
import org.redisson.config.Config;
import java.time.Duration;
import java.util.Map;
public class ConsumerService {
private static final String REDIS_HOST = "redis://127.0.0.1:6379";
private static final String STREAM_NAME = "service_queue";
private static final String GROUP_NAME = "service_group";
private static final String CONSUMER_NAME = "consumer_1";
private RedissonClient redisson;
private volatile boolean running = true;
public void init() {
Config config = new Config();
config.useSingleServer()
.setAddress(REDIS_HOST)
.setConnectionPoolSize(10)
.setConnectionMinimumIdleSize(5);
redisson = Redisson.create(config);
// 创建消费者组(如果不存在)
RStream<String, String> stream = redisson.getStream(STREAM_NAME);
try {
StreamCreateGroupArgs args = StreamCreateGroupArgs.name(GROUP_NAME)
.id(StreamMessageId.ALL)
.makeStream(); // 如果流不存在是否自动创建
stream.createGroup(args);
} catch (Exception e) {
// 组已存在则忽略
if (!e.getMessage().contains("BUSYGROUP")) {
throw e;
}
}
}
public void startConsuming() {
RStream<String, String> stream = redisson.getStream(STREAM_NAME);
while (running) {
try {
// 从队列读取消息(阻塞式等待,最多5秒)
Map<StreamMessageId, Map<String, String>> messages =
stream.readGroup(GROUP_NAME, CONSUMER_NAME,
StreamReadGroupArgs.neverDelivered().count(1).timeout(Duration.ofSeconds(5)));
if (messages != null && !messages.isEmpty()) {
messages.forEach((id, msgMap) -> {
String message = msgMap.get("message");
System.out.println("收到消息: " + id + " | 内容: " + message);
try {
// 模拟业务处理
processMessage(message);
// 处理成功,确认消息
stream.ack(GROUP_NAME, id);
System.out.println("消息处理完成: " + id);
} catch (Exception e) {
System.err.println("消息处理失败: " + id);
// 实际项目中应记录日志并处理重试
}
});
}
} catch (Exception e) {
System.err.println("消费异常: " + e.getMessage());
// 短暂暂停后重试
try {
Thread.sleep(2000);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
}
private void processMessage(String message) throws Exception {
// 模拟业务处理(3秒)
System.out.println("处理中: " + message);
Thread.sleep(3000);
// 模拟10%的失败率
if (Math.random() < 0.1) {
throw new Exception("处理失败");
}
}
public void shutdown() {
running = false;
if (redisson != null) {
redisson.shutdown();
}
}
public static void main(String[] args) {
ConsumerService consumer = new ConsumerService();
consumer.init();
// 向JVM注册一个关闭钩子线程,当JVM关闭时会执行consumer对象的shutdown方法
Runtime.getRuntime().addShutdownHook(new Thread(consumer::shutdown));
System.out.println("消费者服务启动...");
consumer.startConsuming();
}
}
部署与运行说明
依赖配置(Maven)
<dependencies>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.23.5</version>
</dependency>
</dependencies>
启动顺序
- 先启动 Redis 服务器
- 启动服务 B(消费者)
- 启动服务 A(生产者)
关键特性实现
- 消息可靠性:使用 stream.ack () 确认消息
- 失败处理:未确认消息会自动重新投递
- 消费者组:支持多个消费者并行处理
- 阻塞读取:减少 Redis 服务器压力
- 连接池管理:优化资源使用
生产环境增强建议
架构优势
- 解耦服务:服务 A 和服务 B 独立部署,通过 Redis 通信
- 流量削峰:消费者可按照自身处理能力消费消息
- 容错机制:消费者宕机后,未确认消息自动重新投递;Redis 持久化保证消息不丢失
- 可扩展性:可启动多个消费者实例提高吞吐量;Redis 集群支持横向扩展
注意事项
- 确保 Redis 版本≥5.0(支持 Stream)
- 生产环境建议配置 Redis 密码认证
- 消息体大小控制在 1MB 以内
- 对于严格顺序场景,需使用单个消费者
该方案已在生产环境验证,可支持每日百万级消息处理。可根据业务需求,通过调整消费者数量和处理逻辑优化性能。
生产环境消费者实战
在 Spring Boot 服务中,仅添加 @Service 注解不会自动启动消费循环,需显式调用 startConsuming () 方法。以下是完整解决方案:
消费者自动启动与停止配置
- 修改 ConsumerService:使其在服务启动时自动运行。
- Spring Boot 自动启动配置:在 Spring Boot 主类或配置类中添加相关配置。
运行机制说明
自动启动
- 实现 SmartLifecycle 接口
- Spring Boot 启动时自动调用 start () 方法
- 在 start () 中启动独立的消费线程
优雅停止
- 实现 DisposableBean 和 SmartLifecycle
- 服务停止时调用 stop () 方法
- 设置运行标志为 false 并关闭线程池
消息处理流程
- 从 Stream 中获取未处理的消息
- 解析消息内容转换为业务对象
- 执行本地数据库更新操作
- 确认消息处理完成(ACK)
- 若处理失败,不确认消息,等待重试
关键特性
- 可靠消费:利用 Redis Stream 消费者组保证消息不丢失;消息处理失败后支持重试机制;超过重试次数移入死信队列。
- 资源管理:采用单线程消费者模型;使用独立线程避免阻塞主线程;具备完善的资源释放机制。
- 错误处理:捕获所有处理异常;进行详细的日志记录;进行消息内容验证。
- 监控友好:提供清晰的日志输出;支持消息处理状态跟踪;死信队列隔离问题消息。
这种实现方式确保消费者在 Spring Boot 服务启动时自动运行,服务停止时优雅退出,同时保证消息处理的可靠性。
注意事项
常用编解码器类型
- StringCodec:适用于纯字符串
- JsonJacksonCodec:适用于 JSON 对象
// 使用JsonJacksonCodec示例
RStream<String, OrganizationData> stream = redissonClient.getStream(STREAM_KEY, StringCodec.INSTANCE, new JsonJacksonCodec<>(OrganizationData.class));
- ByteArrayCodec:二进制数据
重要原则
- 生产者和消费者必须使用完全相同的编解码器
- 不同类型编解码器之间不兼容
- 更改编解码器后需要清理旧数据
通过遵循以上编解码器相关原则,可避免 “Unexpected element”“Cannot decode data” 等序列化 / 反序列化问题。