Can Redis Replace MQ? Building a Reliable Delayed Queue with Keyspace Events and Redisson
This article compares two Redis‑based delayed‑queue approaches—listening to key‑space expiration events and using Redisson’s built‑in delayed queue—detailing their principles, Spring Boot demos, pitfalls, and why Redisson offers a more reliable solution for distributed applications.
Background
For a small project that needed delayed tasks, the author first considered using a message queue such as RabbitMQ or RocketMQ but avoided adding MQ complexity. Since Redis was already in use, the author explored using Redis to implement a delayed queue.
Listening to Expired Keys
The first solution relies on Redis key‑space notifications. When a key expires, Redis publishes an event that can be listened to.
1. Redis Pub/Sub
Redis implements a publish/subscribe model similar to MQ, where a channel is analogous to a topic.
The channel concept works like an MQ topic: producers send messages to a channel and consumers subscribe to receive them.
2. Keyspace notifications
Redis provides default channels that publish events about key changes. Two main prefixes are used:
Prefix __keyspace@<db>__: listens to events related to a specific key. Example: a consumer listening to __keyspace@0__:sanyou receives messages when the key sanyou is deleted or modified.
Prefix __keyevent@<db>__: listens to a specific event type, such as expiration. Example: listening to __keyevent@0__:expired receives a message when any key in DB 0 expires.
The DB number corresponds to Redis's logical databases (0‑15).
3. Delayed queue implementation principle
When a key expires, Redis publishes an event to __keyevent@<db>__:expired . By listening to this channel, a service can detect the expired key and treat it as a delayed task.
The implementation consists of two steps:
Set a delayed task by creating a key whose TTL equals the delay.
Listen to the __keyevent@<db>__:expired channel and process the expired key.
4. Demo
A Spring Boot demo shows how to use the above principle.
pom.xml dependencies:
<code><dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.5.RELEASE</version>
</dependency></code>Configuration class:
<code>@Configuration
public class RedisConfiguration {
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
@Bean
public KeyExpirationEventMessageListener redisKeyExpirationListener(RedisMessageListenerContainer container) {
return new KeyExpirationEventMessageListener(container);
}
}</code>The listener receives messages from __keyevent@*__:expired (the asterisk means all databases).
When an expired key event is received, Spring publishes a RedisKeyExpiredEvent which can be handled:
<code>@Component
public class MyRedisKeyExpiredEventListener implements ApplicationListener<RedisKeyExpiredEvent> {
@Override
public void onApplicationEvent(RedisKeyExpiredEvent event) {
byte[] body = event.getSource();
System.out.println("Got delayed message: " + new String(body));
}
}</code>Running the application and setting a key manually:
<code>set sanyou task
expire sanyou 5</code>Expected output after 5 seconds: "Got delayed message: sanyou". However, the message may not appear because Redis only publishes the expiration event after the key is actually removed, which depends on the lazy or periodic eviction policies.
Key expiration strategies:
Lazy deletion – the key is removed only when accessed after expiration.
Periodic deletion – a background task scans and removes expired keys.
If neither strategy removes the key promptly, the expiration event is delayed, causing the delayed‑queue to be unreliable.
5. Pitfalls
Additional issues include frequent message loss (no persistence in Redis Pub/Sub), broadcast‑only consumption, and receiving events for all keys when listening to __keyevent@<db>__: prefixes.
These drawbacks make the key‑space notification method unstable for delayed queues.
Redisson Implementation of Delayed Queue
Redisson, a Redis client library, provides a built‑in delayed queue that overcomes many of the previous pitfalls.
1. Demo
pom.xml dependency:
<code><dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.1</version>
</dependency></code>RedissonDelayQueue class:
<code>@Component
@Slf4j
public class RedissonDelayQueue {
private RedissonClient redissonClient;
private RDelayedQueue<String> delayQueue;
private RBlockingQueue<String> blockingQueue;
@PostConstruct
public void init() {
initDelayQueue();
startDelayQueueConsumer();
}
private void initDelayQueue() {
Config config = new Config();
SingleServerConfig serverConfig = config.useSingleServer();
serverConfig.setAddress("redis://localhost:6379");
redissonClient = Redisson.create(config);
blockingQueue = redissonClient.getBlockingQueue("SANYOU");
delayQueue = redissonClient.getDelayedQueue(blockingQueue);
}
private void startDelayQueueConsumer() {
new Thread(() -> {
while (true) {
try {
String task = blockingQueue.take();
log.info("Received delayed task:{}", task);
} catch (Exception e) {
e.printStackTrace();
}
}
}, "SANYOU-Consumer").start();
}
public void offerTask(String task, long seconds) {
log.info("Add delayed task:{} delay:{}s", task, seconds);
delayQueue.offer(task, seconds, TimeUnit.SECONDS);
}
}</code>Controller to add tasks:
<code>@RestController
public class RedissonDelayQueueController {
@Resource
private RedissonDelayQueue redissonDelayQueue;
@GetMapping("/add")
public void addTask(@RequestParam("task") String task) {
redissonDelayQueue.offerTask(task, 5);
}
}</code>Test by visiting:
http://localhost:8080/add?task=sanyou
After 5 seconds the consumer prints the task.
2. Implementation principle
Redisson uses several internal Redis keys:
redisson_delay_queue_timeout:SANYOU – a sorted set storing all delayed tasks with their absolute execution timestamps.
redisson_delay_queue:SANYOU – a list (mostly unused).
SANYOU – the target list where tasks become available for consumption.
redisson_delay_queue_channel:SANYOU – a channel used to notify clients about the next task to execute.
When a task is added, its execution timestamp is stored in the sorted set. A client‑side Lua script atomically moves tasks whose timestamps have arrived from the sorted set to the target list and publishes the next earliest timestamp to the channel. Clients listening to the channel schedule a new Lua script execution for the remaining delay, ensuring timely delivery.
On application startup Redisson runs a one‑time client task to move any already‑expired entries to the target list, preventing missed executions after a restart.
3. Comparison with the first scheme
Redisson eliminates the initial scheme’s delay issues, greatly reduces message loss thanks to Redis persistence, avoids broadcast‑only consumption because all consumers pull from the same target list, and removes reliance on Redis key‑event channels.
Overall, Redisson provides a more reliable and production‑ready delayed‑queue solution.
Sanyou's Java Diary
Passionate about technology, though not great at solving problems; eager to share, never tire of learning!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.