06 队列与异步处理
🎯 学习要点
- 列表队列与阻塞消费
- Stream 消费者组与重试
- 延迟队列与定时任务
- 发布订阅通知
📖 名词解释
队列:先进先出消息结构,常用 List 实现。阻塞消费:消费者在无消息时等待直至超时或有消息到来。Stream:Redis 的消息流结构,支持多消费者与确认机制。消费者组:Stream 的分组消费模式,组内多个消费者共享消息。确认(ACK):消费者处理完消息后进行确认,避免重复投递。死信:未能成功处理或超时未确认的消息,需转入专用队列处理。延迟队列:按时间延迟执行的队列,常用 ZSet 的分数存时间戳。发布订阅:即发即收的消息模型,不保证持久化与重试。
🧭 学习方案
- 用 List 实现一个最小订单队列,生产与阻塞消费,处理超时与空队列。
- 用 Stream 创建消费者组,生产消息并实现确认,模拟失败重试。
- 用 ZSet 实现延迟取消订单任务,编写到期扫描流程并加去重。
- 使用发布订阅完成群聊通知或系统广播,理解其非持久化的特点。
🗺️ 应用场景说明
- 订单处理:下单后入队,后台异步扣库存与发货,提升接口响应速度。
- 通知中心:用户操作后推送消息到多个消费者,例如短信与站内信并行。
- 定时任务:30 分钟未支付自动取消,用 ZSet 保证到期执行。
⚠️ 注意事项
📥 列表队列
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.data.redis.core.RedisTemplate;
import java.time.Duration;
@Service
public class ListQueue {
@Autowired
RedisTemplate<String, String> tpl;
public void produce(String id) {
tpl.opsForList().leftPush("queue:orders", id);
}
public String consume() {
return tpl.opsForList().rightPop("queue:orders", Duration.ofSeconds(2));
}
}
通俗说明
- 生产者“左推”,消费者“右取”,保证先入先出,像排队取号。
- 阻塞消费就像“等消息”,超时返回空,需要业务层做重试或补偿。
- 队列消息要考虑幂等处理,防止重复消费导致重复扣减或发送。
详细解释
- 写入
LPUSH,读取 BRPOP 可阻塞等待消息到来并设置超时。 - 幂等通过业务 ID 去重或状态机控制,保证重复消息不会重复执行。
- 长队列建议分桶或限长,避免形成大 Key 影响性能与扫描。
速记口诀
👥 Stream 消费者组
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.StreamOffset;
import java.util.Map;
@Service
public class StreamGroup {
@Autowired
RedisTemplate<String, String> tpl;
public void init() {
try {
tpl.opsForStream().createGroup("stream:orders", ReadOffset.from("0-0"), "g1");
} catch (Exception ignored) {
}
}
public void produce() {
tpl.opsForStream().add(MapRecord.create("stream:orders", Map.of("id", "O1001")));
}
public Object consume() {
return tpl.opsForStream().read(Consumer.from("g1", "c1"), StreamOffset.create("stream:orders", ReadOffset.lastConsumed()));
}
}
通俗说明
- 创建消费者组后,消息会分配到组内消费者,处理完要确认(ACK)。
- 读取偏移要根据场景选择:从未读、最后已消费处继续、或指定 ID。
详细解释
- 初始化组后用
lastConsumed 继续消费,避免重复;处理完成要 ACK。 - 失败消息进入待处理队列(PENDING),需按需转移或重试。
- 组内分配提升并发处理能力,但要注意公平性与负载均衡。
⏰ 延迟队列(ZSet)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.Set;
@Service
public class DelayQueueZSet {
@Autowired
RedisTemplate<String, String> tpl;
public void schedule(String orderId, long delayMs) {
long at = System.currentTimeMillis() + delayMs;
tpl.opsForZSet().add("delay:orders", orderId, at);
}
public void scanDue() {
Set<String> due = tpl.opsForZSet().rangeByScore("delay:orders", 0, System.currentTimeMillis());
if (due != null) {
for (String id : due) {
Long removed = tpl.opsForZSet().remove("delay:orders", id);
if (removed != null && removed > 0) {
process(id);
}
}
}
}
private void process(String id) {
String s = id;
}
}
通俗说明
- 用时间戳作为分数,实现“到期执行”的延迟任务。
- 扫描时先查询再删除,配合去重避免重复处理。
详细解释
- 分数即为触发时间戳,按
rangeByScore 查到期项,再 remove 去重处理。 - 并发扫描需加互斥或原子删除,避免多实例重复处理同一任务。
- 适合到期取消订单、超时关闭会话等场景。
📣 发布订阅(Spring)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.listener.ChannelTopic;
@Configuration
public class PubSubConfig {
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory factory) {
RedisMessageListenerContainer c = new RedisMessageListenerContainer();
c.setConnectionFactory(factory);
c.addMessageListener(new MessageListenerAdapter((MessageListener) (message, pattern) -> {}), new ChannelTopic("chat:family"));
return c;
}
interface MessageListener {
void onMessage(byte[] message, byte[] pattern);
}
}
通俗说明
- 发送端通过频道发布消息,订阅者即时接收;没有历史消息回放。
- 适合系统通知与群聊提醒,不适合可靠投递。
详细解释
- 发布订阅是“即发即收”,离线不会补发,适合作轻量通知。
- 监听回调里尽量做轻逻辑,复杂流程交给队列或任务系统。
- 频道命名需规范,避免与其他业务冲突。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.data.redis.core.RedisTemplate;
@Component
public class PubSubSender {
@Autowired
RedisTemplate<String, String> tpl;
public void send() {
tpl.convertAndSend("chat:family", "晚饭开饭");
}
}
通俗说明
- 发送端通过频道发布消息,订阅者即时接收;没有历史消息回放。
- 适合系统通知与群聊提醒,不适合可靠投递。
详细解释
- 发送用
convertAndSend(channel, msg);订阅端需在线才能收到消息。 - 不保证持久化与重试,重要消息用队列或事务型存储保障可靠性。
- 适合作公告、群聊提醒等轻量场景。
🧰 Redisson 队列与延迟队列
原理
RQueue 作为共享队列供多实例读写;RBlockingQueue 支持阻塞获取以降低空轮询开销。RDelayedQueue 绑定目标队列并保存到期时间,到期后将元素投递回目标队列,无需人工扫描。
import org.redisson.api.RedissonClient;
import org.redisson.api.RQueue;
import org.redisson.api.RDelayedQueue;
import org.redisson.api.RBlockingQueue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class RedissonQueues {
@Autowired
RedissonClient client;
public void pushNow(String item) {
RQueue<String> q = client.getQueue("rqueue:orders");
q.add(item);
}
public void delay(String item, long delay, TimeUnit unit) {
RQueue<String> q = client.getQueue("rqueue:orders");
RDelayedQueue<String> dq = client.getDelayedQueue(q);
dq.offer(item, delay, unit);
}
public String poll() {
RQueue<String> q = client.getQueue("rqueue:orders");
return q.poll();
}
public String takeBlocking() throws InterruptedException {
RBlockingQueue<String> bq = client.getBlockingQueue("rqueue:orders");
return bq.take();
}
public void cleanup() {
RQueue<String> q = client.getQueue("rqueue:orders");
RDelayedQueue<String> dq = client.getDelayedQueue(q);
dq.destroy();
}
}
通俗说明
- Redisson 提供分布式队列、阻塞队列与延迟队列,延迟到期自动投递回目标队列,消费更简单。。
- 适用于订单异步处理、到期任务调度与生产者/消费者解耦,无需手写扫描逻辑。
详细解释
RDelayedQueue 到期后将元素投递回目标 RQueue,消费者直接从队列处理。- 时间单位与精度由
offer(item, delay, unit) 决定,适合大量延时任务。 - 无需写扫描代码,降低并发安全与重复处理的复杂度。
使用建议
- 普通队列:短任务用 poll 轮询配合退避;常驻消费者用 takeBlocking 。
- 延迟队列:应用关闭或切换队列名时执行 cleanup() 清理结构,避免泄露。
- 幂等与重试:消费端基于业务唯一 ID 做去重,失败重试要防止重复执行。
🔍 小结
- 列表队列简单易用,适合少量生产者与消费者;阻塞消费配合超时处理。
- Stream 支持消费者组与确认机制,适合可靠传递与重试场景。
- 延迟队列使用 ZSet 按时间执行,扫描时需加并发安全处理避免重复执行。
- 发布订阅用于广播消息,无法持久化与可靠投递,适合通知类场景。