[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
}

运行结果

image

posted on 2026-07-02 08:06  王景迁  阅读(6)  评论(0)    收藏  举报

导航