Streams Fundamentals
Before Redis 5.0, developers had to choose between the speed of Pub/Sub (which had no persistence) and the reliability of Lists (which were not designed for complex consumption). Enter Redis Streams.
A Stream is an append-only log data structure, similar to Apache Kafka, but implemented with the in-memory speed of Redis.
1. The Append-Only Log
At its core, a Stream is just an infinite list of messages.
- Ordered: Messages are stored in the order they are received.
- Immutable: Once a message is written, it (generally) doesn’t change.
- Persistent: Unlike Pub/Sub, messages are stored on disk (via RDB/AOF) and survive server restarts.
1.1 Anatomy of a Message
Every message in a stream consists of:
- ID: A unique identifier, typically a timestamp (e.g.,
1623456789123-0). - Fields: One or more key-value pairs (e.g.,
sensor_id="A1",temp="22.5").
2. Basic Commands
Adding Data (XADD)
The XADD command appends a new entry to the stream. You almost always use * to let Redis auto-generate the ID.
XADD mystream * sensor_id "A1" temp "22.5"
# Returns: "1623456789123-0"
Reading History (XRANGE)
Since a stream is persistent, you can query it by time range.
# Read from beginning (-) to end (+)
XRANGE mystream - +
# Read just the first 2 entries
XRANGE mystream - + COUNT 2
3. Interactive: Stream Internals
Visualize how XADD appends data to the log and how Redis efficiently stores it internally.
Append-Only Log
Internal Structure (Radix Tree + Listpacks)
4. Under the Hood: Radix Trees & Listpacks
Redis Streams are not implemented as simple linked lists. To save memory, they use a hybrid data structure called a Radix Tree (or Rax).
4.1 The Rax (Radix Tree)
A Radix Tree is a compressed prefix tree. Instead of storing every ID explicitly, it shares common prefixes.
- Example: IDs
1623456789000-0and1623456789000-1share the prefix1623456789000. - Efficiency: This drastically reduces the memory overhead for storing timestamps.
4.2 Listpacks
Inside each node of the Radix Tree, Redis doesn’t store a single message. Instead, it stores a Listpack (a tightly packed list of binary data) containing multiple messages (typically 50-100).
- Pointer Chasing: This reduces the number of pointers Redis needs to follow.
- CPU Cache: Packed data is friendlier to CPU caches, improving iteration speed.
[!NOTE] This hybrid approach allows Redis Streams to store millions of messages with significantly less memory than a standard Linked List or Hash.
5. Code Examples: Producer
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// XADD mystream * sensor_id A1 temp 22.5
id, err := rdb.XAdd(ctx, &redis.XAddArgs{
Stream: "mystream",
Values: map[string]interface{}{
"sensor_id": "A1",
"temp": 22.5,
},
}).Result()
if err != nil {
panic(err)
}
fmt.Println("Produced message:", id)
}
6. Summary
Redis Streams provide a high-performance, persistent log. By using XADD and XRANGE, you can treat Redis as a lightweight time-series database or an event store. However, reading raw ranges is hard to scale. In the next chapter, we’ll see how Consumer Groups solve this.