[AI生成] 基于Redis List实现消息队列
// Package main —— Redis 消息队列演示
//
// 基于 Redis List 数据结构实现消息队列,核心思路:
// - 生产者通过 RPUSH 将消息追加到 List 尾部
// - 消费者通过 BLPOP 从 List 头部阻塞式取出消息
// - RPUSH + BLPOP 天然形成 FIFO(先进先出)队列
// - BLPOP 是阻塞命令,队列为空时挂起等待,无需客户端轮询,节省 CPU
// - 多个消费者同时 BLPOP 同一条队列时,Redis 保证每条消息只被一个消费者取走
//
// 运行方式:go run ./redis-message-queue/
package main
import (
"context"
"fmt"
"os"
"sync"
"time"
"github.com/redis/go-redis/v9"
)
// Queue 基于 Redis List 的消息队列
//
// 底层使用 Redis 的 List 类型,一个 key 对应一条队列。
// 写入端用 RPUSH 追加到队尾,读取端用 BLPOP 阻塞式从队头取出。
type Queue struct {
client *redis.Client // Redis 客户端连接
key string // 队列在 Redis 中对应的 key 名称
}
// NewQueue 创建消息队列实例
//
// - client: 已连接的 Redis 客户端
// - key: 队列在 Redis 中的 key,不同业务使用不同 key 即可实现队列隔离
func NewQueue(client *redis.Client, key string) *Queue {
return &Queue{client: client, key: key}
}
// Push 向队列尾部追加一条消息(RPUSH)
//
// RPUSH 将消息追加到 List 的右侧(队尾),返回操作后队列的总长度。
// 当 key 不存在时,Redis 会自动创建一个空 List 再追加。
//
// 返回值:
// - int64: 当前队列长度
// - error: 操作失败时返回错误(如网络断开)
func (q *Queue) Push(ctx context.Context, msg string) (int64, error) {
return q.client.RPush(ctx, q.key, msg).Result()
}
// Pop 阻塞式从队列头部取出一条消息(BLPOP)
//
// BLPOP 从 List 的左侧(队头)弹出第一个元素。
// 如果队列为空,会阻塞等待直到有消息到达或超过 timeout 时间。
// 超时后返回 redis.Nil 错误,调用方可通过判断 err == redis.Nil 来区分"超时"和"真正错误"。
//
// 参数:
// - timeout: 最长阻塞等待时间。0 表示永久阻塞,不推荐在生产中使用
//
// 返回值:
// - string: 取出的消息内容
// - error: 超时时为 redis.Nil,其他错误(如网络断开)为具体 error
func (q *Queue) Pop(ctx context.Context, timeout time.Duration) (string, error) {
// BLPOP 返回 [key, value] 两个元素的数组,[0] 是队列名,[1] 是消息体
results, err := q.client.BLPop(ctx, timeout, q.key).Result()
if err != nil {
return "", err
}
return results[1], nil
}
// Len 返回队列当前长度(LLEN)
//
// 返回值:
// - int64: 队列中待消费的消息数量
// - error: 操作失败时返回错误
func (q *Queue) Len(ctx context.Context) (int64, error) {
return q.client.LLen(ctx, q.key).Result()
}
// Clear 清空队列(DEL)
//
// 直接删除 Redis 中的 key,下次 Push 时会自动重建。
// 注意:DEL 不可逆,清空前请确保没有未消费的重要消息。
func (q *Queue) Clear(ctx context.Context) error {
return q.client.Del(ctx, q.key).Err()
}
// main 程序入口:连接 Redis → 运行 8 个测试用例 → 输出通过/失败统计
func main() {
// 创建可取消的 context,用于控制所有 Redis 操作的生命周期
ctx := context.Background()
// 连接本地 Redis(默认端口 6379,无密码)
client := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
defer client.Close() // 程序退出时关闭连接,释放资源
// 发送 PING 命令验证连接是否正常
if err := client.Ping(ctx).Err(); err != nil {
fmt.Printf("failed to connect Redis: %v\n", err)
os.Exit(1)
}
fmt.Println("Redis connected")
// 记录通过和失败的测试用例数量
passed, failed := 0, 0
// run 是一个辅助闭包,用于执行单个测试用例并统计结果
// 每个测试函数返回 error:nil 表示通过,非 nil 表示失败(携带失败原因)
run := func(name string, fn func() error) {
if err := fn(); err != nil {
failed++
fmt.Printf(" FAIL %s: %v\n", name, err)
} else {
passed++
fmt.Printf(" PASS %s\n", name)
}
}
// ============================================================
// 8 个测试用例,覆盖消息队列的核心场景
// ============================================================
run("PushAndPop", func() error { return testPushAndPop(ctx, client) })
run("PopTimeout", func() error { return testPopTimeout(ctx, client) })
run("MultipleMessages", func() error { return testMultipleMessages(ctx, client) })
run("FIFOOrder", func() error { return testFIFOOrder(ctx, client) })
run("ConcurrentProducers", func() error { return testConcurrentProducers(ctx, client) })
run("ConcurrentConsumers", func() error { return testConcurrentConsumers(ctx, client) })
run("Clear", func() error { return testClear(ctx, client) })
run("MultipleQueues", func() error { return testMultipleQueues(ctx, client) })
fmt.Printf("\n%d passed, %d failed\n", passed, failed)
}
// testPushAndPop 测试基本 Push 和 Pop 操作
//
// 场景:Push 一条消息 → Pop 出来 → 验证队列为空
// 验证点:
// 1. Push 返回的队列长度是否正确(首次 Push 后应为 1)
// 2. Pop 取出的消息内容是否与 Push 的一致
// 3. Pop 后队列长度是否归零
func testPushAndPop(ctx context.Context, client *redis.Client) error {
q := NewQueue(client, "test:push-pop")
q.Clear(ctx) // 清理可能残留的旧数据
// 写入一条消息
n, err := q.Push(ctx, "hello")
if err != nil {
return fmt.Errorf("Push: %w", err)
}
if n != 1 {
return fmt.Errorf("expected length 1, got %d", n)
}
// 阻塞取出消息(最多等 1 秒)
msg, err := q.Pop(ctx, time.Second)
if err != nil {
return fmt.Errorf("Pop: %w", err)
}
if msg != "hello" {
return fmt.Errorf("expected 'hello', got '%s'", msg)
}
// 验证队列为空
length, _ := q.Len(ctx)
if length != 0 {
return fmt.Errorf("expected length 0, got %d", length)
}
return nil
}
// testPopTimeout 测试空队列 Pop 超时
//
// 场景:队列为空时调用 Pop,应超时返回 redis.Nil
// 验证点:空队列阻塞 Pop 后返回 redis.Nil,而非真正的错误
func testPopTimeout(ctx context.Context, client *redis.Client) error {
q := NewQueue(client, "test:pop-timeout")
q.Clear(ctx)
// 空队列 Pop,指定 1 秒超时(go-redis 最小超时粒度为 1s)
_, err := q.Pop(ctx, time.Second)
if err != redis.Nil {
return fmt.Errorf("expected redis.Nil on timeout, got %v", err)
}
return nil
}
// testMultipleMessages 测试多条消息的 Push 和 Pop
//
// 场景:连续 Push 5 条消息 → 验证队列长度 → 逐条 Pop 全部消费 → 验证队列为空
// 验证点:
// 1. 中间状态:Push 5 条后队列长度应为 5
// 2. 最终状态:全部 Pop 后队列长度应为 0
func testMultipleMessages(ctx context.Context, client *redis.Client) error {
q := NewQueue(client, "test:multi-msg")
q.Clear(ctx)
// 连续 Push 5 条消息
for i := 0; i < 5; i++ {
q.Push(ctx, "msg")
}
// 验证中间状态:队列长度应为 5
length, _ := q.Len(ctx)
if length != 5 {
return fmt.Errorf("expected length 5, got %d", length)
}
// 逐条 Pop 消费
for i := 0; i < 5; i++ {
msg, err := q.Pop(ctx, time.Second)
if err != nil {
return fmt.Errorf("Pop: %w", err)
}
if msg != "msg" {
return fmt.Errorf("expected 'msg', got '%s'", msg)
}
}
// 验证最终状态:队列应为空
length, _ = q.Len(ctx)
if length != 0 {
return fmt.Errorf("expected length 0, got %d", length)
}
return nil
}
// testFIFOOrder 测试消息的先进先出顺序
//
// 场景:按顺序 Push a→b→c→d→e,Pop 时应按相同顺序取出
// 验证点:RPUSH + BLPOP 天然形成 FIFO,先写入的消息先被消费
func testFIFOOrder(ctx context.Context, client *redis.Client) error {
q := NewQueue(client, "test:fifo")
q.Clear(ctx)
// 按顺序写入 5 条消息
messages := []string{"a", "b", "c", "d", "e"}
for _, m := range messages {
q.Push(ctx, m)
}
// 按顺序取出,验证 FIFO
for _, expected := range messages {
got, err := q.Pop(ctx, time.Second)
if err != nil {
return fmt.Errorf("Pop: %w", err)
}
if got != expected {
return fmt.Errorf("expected '%s', got '%s'", expected, got)
}
}
return nil
}
// testConcurrentProducers 测试多生产者并发 Push
//
// 场景:5 个 goroutine 各自 Push 20 条消息,共 100 条
// 验证点:Redis 单线程处理命令,RPUSH 天然原子,并发 Push 不会丢消息
// 100 条消息全部写入后,队列长度应恰好为 100
func testConcurrentProducers(ctx context.Context, client *redis.Client) error {
q := NewQueue(client, "test:concurrent-producers")
q.Clear(ctx)
var wg sync.WaitGroup
total := 100 // 总消息数
goroutines := 5 // 生产者数量
perGoroutine := total / goroutines // 每个生产者写入 20 条
for g := 0; g < goroutines; g++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < perGoroutine; i++ {
q.Push(ctx, "msg")
}
}()
}
wg.Wait() // 等待所有生产者完成
// 验证:并发写入后队列长度应等于 total
length, _ := q.Len(ctx)
if int(length) != total {
return fmt.Errorf("expected %d messages, got %d", total, length)
}
return nil
}
// testConcurrentConsumers 测试多消费者并发 Pop
//
// 场景:预先 Push 200 条消息 → 4 个 goroutine 并发消费 → 验证消费总数
// 验证点:BLPOP 的原子性保证每条消息只被一个消费者取走,不会重复消费
// 4 个消费者同时竞争,最终消费总数应等于 200
func testConcurrentConsumers(ctx context.Context, client *redis.Client) error {
q := NewQueue(client, "test:concurrent-consumers")
q.Clear(ctx)
// 预先写入 200 条消息
total := 200
for i := 0; i < total; i++ {
q.Push(ctx, "msg")
}
var consumed int // 已消费消息计数
var mu sync.Mutex // 保护 consumed 的互斥锁
var wg sync.WaitGroup
goroutines := 4 // 消费者数量
for g := 0; g < goroutines; g++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
// 阻塞 Pop,超时 1 秒后退出循环
_, err := q.Pop(ctx, time.Second)
if err == redis.Nil {
return // 队列为空,超时退出
}
if err != nil {
return // 发生错误,退出
}
mu.Lock()
consumed++
mu.Unlock()
}
}()
}
wg.Wait() // 等待所有消费者退出
// 验证:消费总数应等于 total
if consumed != total {
return fmt.Errorf("expected %d consumed, got %d", total, consumed)
}
return nil
}
// testClear 测试队列清空操作
//
// 场景:Push 10 条消息 → 验证长度 → Clear → 验证队列为空
// 验证点:Clear 后队列长度归零,且再次 Push 可以正常工作
func testClear(ctx context.Context, client *redis.Client) error {
q := NewQueue(client, "test:clear")
q.Clear(ctx)
// 写入 10 条消息
for i := 0; i < 10; i++ {
q.Push(ctx, "msg")
}
// 验证清空前长度
length, _ := q.Len(ctx)
if length != 10 {
return fmt.Errorf("expected length 10, got %d", length)
}
// 清空队列
q.Clear(ctx)
// 验证清空后长度
length, _ = q.Len(ctx)
if length != 0 {
return fmt.Errorf("expected length 0 after clear, got %d", length)
}
return nil
}
// testMultipleQueues 测试多队列隔离
//
// 场景:创建两条独立队列 q1 和 q2,分别写入不同消息,验证互不干扰
// 验证点:不同 key 的队列是完全隔离的,q1 的 Pop 不会取出 q2 的消息
func testMultipleQueues(ctx context.Context, client *redis.Client) error {
q1 := NewQueue(client, "test:q1")
q2 := NewQueue(client, "test:q2")
q1.Clear(ctx)
q2.Clear(ctx)
// 分别向两条队列写入不同消息
q1.Push(ctx, "from-q1")
q2.Push(ctx, "from-q2")
// 分别从两条队列取出消息,验证内容正确
msg1, _ := q1.Pop(ctx, time.Second)
msg2, _ := q2.Pop(ctx, time.Second)
if msg1 != "from-q1" {
return fmt.Errorf("expected 'from-q1', got '%s'", msg1)
}
if msg2 != "from-q2" {
return fmt.Errorf("expected 'from-q2', got '%s'", msg2)
}
return nil
}
运行结果

浙公网安备 33010602011771号