一、从"排队买奶茶"说起:为什么Redis列表适合做消息队列

想象一下你去奶茶店排队买饮料的场景。每个人进去之后都在排队,先到的人先被服务,后到的人排在后面等着。这个场景其实就是一个非常典型的"先进先出"模型,也就是我们常说的FIFO(First In, First Out)。

而Redis的列表(List)数据结构,天生就具备这种能力。你可以把列表想象成一个可以两头操作的队列:你可以从左边塞东西进去,也可以从右边塞进去;同样,你可以从左边取出来,也可以从右边取出来。这种灵活性让它在消息队列的场景中非常好用。

很多开发者第一次接触消息队列,可能觉得它是一个很复杂的东西,需要专门的中间件比如RabbitMQ、Kafka。但实际上,对于一些中小规模的消息处理场景,Redis列表完全可以胜任,而且部署简单、性能优秀。

二、Redis列表的基本操作:push和pop就像往盒子里放东西和取东西

Redis列表其实就是一个链表,支持从两端进行push和pop操作。下面我们把常用的命令理一遍。

2.1 往列表两端塞东西

往列表左边塞东西用LPUSH,往右边塞东西用RPUSH。这和你在生活中整理收纳盒的操作很像——你可以从上盖放进去,也可以从底下抽出来。

# 往my_queue这个列表的左边放入一个任务
LPUSH my_queue "任务A"

# 往my_queue这个列表的右边放入一个任务
RPUSH my_queue "任务B"

# 查看所有任务,0表示起始位置,-1表示最后一个
LRANGE my_queue 0 -1
# 返回: ["任务A", "任务B"]

2.2 从列表两端取东西

取东西的时候,如果从左边取用LPOP,从右边取用RPOP。这里有个关键的组合:如果你用RPUSH来放消息,用LPOP来取消息,那就是标准的先进先出(FIFO)队列。

# 从右边放入新消息,模拟生产者连续推送
RPUSH task_queue "消息1"
RPUSH task_queue "消息2"
RPUSH task_queue "消息3"

# 从左边取出最早的消息(先放进去的先被取出)
LPOP task_queue
# 返回: "消息1"

LPOP task_queue
# 返回: "消息2"

2.3 阻塞式获取:没有消息时不着急

在实际的消息队列场景中,消费者不会一直空转着检查有没有新消息。Redis提供了阻塞式的BLPOP和BRPOP命令,意思是:如果没有消息,就先挂着等,一旦有新消息就立刻返回。这就像你去取快递,如果快递员还没到,你就不用一直盯着门口,等快递到了自然会有人叫你。

# 阻塞式从左边取,最多等30秒,超时返回nil
BLPOP task_queue 30

# 也可以同时监听多个队列,哪个有消息就先取哪个
BRPOP task_queue urgent_queue 10

三、用Redis列表搭建一个简单的消息队列

下面我们用Python来演示一个完整的生产者-消费者模型。整个示例统一使用Python技术栈,依赖redis-py库。

3.1 安装依赖

pip install redis

3.2 生产者代码

生产者负责往队列里塞消息。

# 技术栈: Python + redis-py
# 文件: producer.py
# 功能: 消息生产者,负责往Redis列表中推送消息

import redis
import json
import time
import uuid

# 连接Redis,使用本地默认配置
r = redis.Redis(host='localhost', port=6379, db=0)

# 队列名称,全局统一
QUEUE_NAME = 'task_queue'


def produce_message(task_type, payload):
    """
    生产一条消息并推入队列
    task_type: 任务类型,比如'发邮件'、'发短信'
    payload: 任务的具体内容
    """
    message = {
        'id': str(uuid.uuid4()),       # 消息唯一ID
        'type': task_type,             # 任务类型
        'payload': payload,            # 任务数据
        'timestamp': time.time()       # 创建时间戳
    }
    # 使用RPUSH从右侧推入消息,保证FIFO顺序
    r.rpush(QUEUE_NAME, json.dumps(message))
    print(f"[生产者] 消息已入队: id={message['id']}, type={task_type}")
    return message


# 模拟生产多条消息
if __name__ == '__main__':
    # 批量生产10条不同类型的任务
    for i in range(10):
        task_types = ['发邮件', '发短信', '生成报表', '数据同步']
        task_type = task_types[i % len(task_types)]
        payload = {'order_id': f'ORD{1000 + i}', 'user': f'user_{i}'}
        produce_message(task_type, payload)
        time.sleep(0.5)  # 每隔0.5秒生产一条

    # 查看队列中消息总数
    queue_size = r.llen(QUEUE_NAME)
    print(f"[生产者] 当前队列中消息总数: {queue_size}")

3.3 消费者代码

消费者从队列中取消息并处理。

# 技术栈: Python + redis-py
# 文件: consumer.py
# 功能: 消息消费者,负责从Redis列表中取出并处理消息

import redis
import json
import time
import logging

# 配置日志格式
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s'
)
logger = logging.getLogger(__name__)

# 连接Redis
r = redis.Redis(host='localhost', port=6379, db=0)

# 队列名称,必须和生产者保持一致
QUEUE_NAME = 'task_queue'
# 阻塞超时时间,单位秒
BLOCK_TIMEOUT = 5


def process_message(message):
    """
    处理单条消息
    这里模拟不同的任务类型执行不同的逻辑
    """
    task_type = message['type']
    payload = message['payload']

    if task_type == '发邮件':
        # 模拟发邮件,耗时2秒
        time.sleep(2)
        logger.info(f"邮件已发送: to={payload.get('user')}")
    elif task_type == '发短信':
        # 模拟发短信,耗时1秒
        time.sleep(1)
        logger.info(f"短信已发送: to={payload.get('user')}")
    elif task_type == '生成报表':
        # 模拟生成报表,耗时3秒
        time.sleep(3)
        logger.info(f"报表已生成: order_id={payload.get('order_id')}")
    elif task_type == '数据同步':
        # 模拟数据同步,耗时1.5秒
        time.sleep(1.5)
        logger.info(f"数据已同步: order_id={payload.get('order_id')}")
    else:
        logger.warning(f"未知任务类型: {task_type}")


def consume_loop():
    """
    消费循环:不断从队列中取消息并处理
    使用BLPOP实现阻塞式消费,避免空转浪费CPU
    """
    logger.info(f"消费者启动,监听队列: {QUEUE_NAME}")

    while True:
        # BLPOP: 从左边阻塞式弹出
        # 参数1: 队列名称
        # 参数2: 超时时间(秒)
        result = r.blpop(QUEUE_NAME, timeout=BLOCK_TIMEOUT)

        # 如果超时了没有消息,result为None
        if result is None:
            logger.debug("暂时没有新消息,继续等待...")
            continue

        # result是一个元组: (队列名, 消息内容)
        queue_name, message_json = result
        message = json.loads(message_json)

        logger.info(f"消费消息: id={message['id']}, type={message['type']}")

        try:
            # 执行具体的业务逻辑
            process_message(message)
            logger.info(f"消息处理成功: id={message['id']}")
        except Exception as e:
            # 处理失败时记录错误
            logger.error(f"消息处理失败: id={message['id']}, error={e}")


if __name__ == '__main__':
    try:
        consume_loop()
    except KeyboardInterrupt:
        logger.info("消费者被手动停止")

3.4 多消费者并行消费

实际生产环境中,往往需要多个消费者并行消费,提高处理效率。Redis列表天然支持多个消费者同时从同一队列取消息。

# 技术栈: Python + redis-py
# 文件: multi_consumer.py
# 功能: 多消费者并行消费示例

import redis
import json
import time
import threading
import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(threadName)s] %(message)s'
)
logger = logging.getLogger(__name__)

r = redis.Redis(host='localhost', port=6379, db=0)
QUEUE_NAME = 'task_queue'


def worker(name):
    """
    每个工作线程独立消费消息
    线程名会体现在日志中,方便排查问题
    """
    logger.info(f"工作线程 {name} 启动")
    processed_count = 0

    while True:
        # 每个线程独立调用blpop,不会互相干扰
        result = r.blpop(QUEUE_NAME, timeout=10)

        if result is None:
            continue

        queue_name, message_json = result
        message = json.loads(message_json)
        processed_count += 1

        # 模拟处理耗时
        time.sleep(1)
        logger.info(
            f"线程{name} 处理完成 第{processed_count}条: "
            f"type={message['type']}"
        )


if __name__ == '__main__':
    # 创建3个消费者线程
    threads = []
    for i in range(3):
        t = threading.Thread(
            target=worker,
            args=(f'worker_{i}',),
            name=f'Worker-{i}'
        )
        t.daemon = True  # 设置为守护线程,主线程退出时自动结束
        t.start()
        threads.append(t)

    # 主线程保持运行,防止进程退出
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        logger.info("所有消费者已停止")

四、消息队列场景中的核心挑战

用一个Redis列表来做消息队列,跑起来确实很快,但一旦放到生产环境,就会遇到一些实际问题。下面这些是我们在实践中踩过坑的地方,也是大家在使用时最容易忽略的。

4.1 消息丢失问题

最典型的场景是:消费者取出了消息,但是在处理过程中程序崩了或者网络断了,这条消息就永远消失了。因为在Redis列表模型里,LPOP是"取走"操作,消息一旦被弹出就不再存在于队列中。

这个问题就像你去银行取钱,ATM机吐了钱出来,但你在钱到手之前突然停电了,这笔钱就"蒸发"了——银行不知道你已经取走了,但你手里也没有。

4.2 消息重复问题

如果我们为了防止消息丢失而做了重试机制,那又会引入新问题:一条消息可能被重复处理多次。比如消费者取到了消息,处理成功了,但是在给服务端回复"已处理"之前断网了。下次重连后,它会再次取到同样的消息。

在消息队列的术语里,这叫"至少一次投递"(at-least-once delivery)。要解决重复问题,通常需要在业务层面做幂等处理。

4.3 消息积压问题

当生产者的速度远快于消费者的处理速度时,队列会越堆越长。Redis列表是存储在内存中的,如果消息积压严重,内存会迅速被占满,最终导致Redis无法响应甚至崩溃。

五、优化实践:让消息队列更可靠

知道了问题在哪里,接下来我们来看看怎么解决。下面这些都是我们在实际项目中验证过的方案。

5.1 用两个队列实现可靠消费

思路很简单:用一个"就绪队列"和一个"处理中队列"。消费者取消息时,先从就绪队列取,同时把消息放到处理中队列。处理成功后从处理中队列移除。如果消费者挂了,可以通过检查处理中队列来找回丢失的消息。

# 技术栈: Python + redis-py
# 文件: reliable_consumer.py
# 功能: 基于双队列的可靠消息消费实现

import redis
import json
import time
import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s'
)
logger = logging.getLogger(__name__)

r = redis.Redis(host='localhost', port=6379, db=0)

# 就绪队列:存放待消费的消息
READY_QUEUE = 'task_queue:ready'
# 处理中队列:存放已经被取走但还没处理完的消息
PROCESSING_QUEUE = 'task_queue:processing'
# 消息超时时间,单位秒。超过这个时间还没处理完,就认为消费者挂了
MESSAGE_TIMEOUT = 60


def acquire_message():
    """
    原子性地从就绪队列获取消息并标记为处理中
    使用Lua脚本保证原子性,避免两个消费者同时取到同一条消息
    """
    lua_script = """
    -- 从就绪队列右边弹出消息,同时放入处理中队列的左边
    local msg = redis.call('RPOPLPUSH', KEYS[1], KEYS[2])

    if msg then
        -- 获取消息ID
        local message = cjson.decode(msg)

        -- 记录处理开始时间,方便后续超时检测
        redis.call('HSET', 'processing_info', message.id, tostring(tonumber(ARGV[1])))
    end

    return msg
    """

    # 执行Lua脚本
    # KEYS[1] = 就绪队列
    # KEYS[2] = 处理中队列
    # ARGV[1] = 当前时间戳
    result = r.eval(
        lua_script,
        2,
        READY_QUEUE,
        PROCESSING_QUEUE,
        str(time.time())
    )

    return result if result else None


def commit_message(message_id):
    """
    消息处理成功后,从处理中队列中移除
    表示这条消息已经被可靠地消费了
    """
    # 删除处理信息
    r.hdel('processing_info', message_id)


def recover_stale_messages():
    """
    恢复超时的消息:把处理中超时的消息放回就绪队列
    这个函数应该由一个定时任务定期调用
    """
    now = time.time()
    info = r.hgetall('processing_info')

    for msg_id, start_time_str in info.items():
        start_time = float(start_time_str)

        # 如果处理时间超过了超时阈值,就认为消费者挂了
        if now - start_time > MESSAGE_TIMEOUT:
            # 从处理中队列中找到对应的消息
            processing_msgs = r.lrange(PROCESSING_QUEUE, 0, -1)

            for msg_json in processing_msgs:
                message = json.loads(msg_json)
                if message['id'] == msg_id:
                    # 把消息放回就绪队列(用LPUSH放到左边,保持顺序)
                    r.lpush(READY_QUEUE, msg_json)
                    # 从处理中队列移除
                    r.lrem(PROCESSING_QUEUE, 0, msg_json)
                    # 删除处理信息
                    r.hdel('processing_info', msg_id)
                    logger.warning(
                        f"恢复超时消息: id={msg_id}, "
                        f"已处理时间={int(now - start_time)}秒"
                    )
                    break

    return len(info)


def safe_consume_loop():
    """
    可靠的消费循环
    每条消息都会被可靠地处理,不会丢失也不会无限重试
    """
    logger.info("可靠消费者启动")

    while True:
        # 原子性地获取消息
        result = acquire_message()

        if result is None:
            # 没有消息,短暂休眠后重试
            time.sleep(1)
            continue

        message = json.loads(result)
        logger.info(f"开始处理: id={message['id']}, type={message['type']}")

        try:
            # 模拟业务处理
            time.sleep(2)
            logger.info(f"业务处理完成: id={message['id']}")

            # 处理成功,标记为完成
            commit_message(message['id'])
            logger.info(f"消息已确认: id={message['id']}")

        except Exception as e:
            # 处理失败,不做任何操作
            # 消息仍然留在处理中队列
            # 等超时后会被自动恢复
            logger.error(f"处理失败,等待超时恢复: id={message['id']}, error={e}")


if __name__ == '__main__':
    try:
        safe_consume_loop()
    except KeyboardInterrupt:
        logger.info("可靠消费者已停止")

5.2 消息去重:用Redis的Set做幂等

为了防止消息被重复处理,我们可以用Redis的Set结构来记录已经处理过的消息ID。

# 技术栈: Python + redis-py
# 文件: dedup_consumer.py
# 功能: 带幂等检查的消息消费者

import redis
import json
import time
import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s'
)
logger = logging.getLogger(__name__)

r = redis.Redis(host='localhost', port=6379, db=0)
QUEUE_NAME = 'task_queue'

# 已处理消息ID的集合,用于去重
PROCESSED_SET = 'processed_messages'
# 去重记录的过期时间,单位秒(保留7天的记录)
DEDUP_EXPIRE_TIME = 7 * 24 * 3600


def is_duplicate(message_id):
    """
    检查消息是否已经被处理过
    使用Redis的SADD命令的原子性来同时检查和标记
    SADD返回1表示新增成功(即第一次处理),返回0表示已存在(即重复)
    """
    # 添加消息ID到去重集合
    added = r.sadd(PROCESSED_SET, message_id)

    # 设置整个集合的过期时间(简化处理,实际可以更精细)
    r.expire(PROCESSED_SET, DEDUP_EXPIRE_TIME)

    # added=1 表示是新消息,added=0 表示已处理过
    return added == 0


def consume_with_dedup():
    """
    带幂等检查的消费循环
    """
    logger.info("去重消费者启动")

    while True:
        result = r.blpop(QUEUE_NAME, timeout=5)

        if result is None:
            continue

        queue_name, message_json = result
        message = json.loads(message_json)

        # 幂等检查
        if is_duplicate(message['id']):
            logger.warning(f"消息重复,跳过处理: id={message['id']}")
            continue

        # 执行业务逻辑
        logger.info(f"处理消息: id={message['id']}, type={message['type']}")
        time.sleep(1)
        logger.info(f"处理完成: id={message['id']}")


if __name__ == '__main__':
    try:
        consume_with_dedup()
    except KeyboardInterrupt:
        logger.info("去重消费者已停止")

5.3 限制队列长度:防止内存溢出

生产者往队列里塞消息的速度如果不加控制,队列可能会无限增长。我们需要在入队时设置最大长度限制。

# 技术栈: Python + redis-py
# 文件: bounded_producer.py
# 功能: 带长度限制的队列生产者

import redis
import json
import time
import logging
import threading

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s'
)
logger = logging.getLogger(__name__)

r = redis.Redis(host='localhost', port=6379, db=0)
QUEUE_NAME = 'task_queue'

# 队列最大长度,超过这个值就拒绝入队
MAX_QUEUE_LENGTH = 10000


def safe_produce(message_json):
    """
    安全地往队列里添加消息
    如果队列已满,返回False表示入队失败
    """
    # 先检查队列当前长度
    current_length = r.llen(QUEUE_NAME)

    if current_length >= MAX_QUEUE_LENGTH:
        logger.warning(
            f"队列已满: 当前长度={current_length}, "
            f"最大长度={MAX_QUEUE_LENGTH}, 拒绝入队"
        )
        return False

    # 推入消息
    r.rpush(QUEUE_NAME, message_json)
    return True


# 更好的方案:用Lua脚本保证检查和推入的原子性
lua_enqueue = """
local queue_key = KEYS[1]
local max_length = tonumber(ARGV[1])
local message = ARGV[2]

-- 获取当前队列长度
local current_length = redis.call('LLEN', queue_key)

if current_length >= max_length then
    -- 队列已满
    return 0
end

-- 推入消息
redis.call('RPUSH', queue_key, message)

return 1
"""


def atomic_produce(message_json):
    """
    原子性地检查队列长度并推入消息
    使用Lua脚本避免并发问题
    """
    result = r.eval(lua_enqueue, 1, QUEUE_NAME, MAX_QUEUE_LENGTH, message_json)
    return result == 1


if __name__ == '__main__':
    # 模拟高并发场景下生产消息
    def producer_thread(thread_id):
        for i in range(100):
            msg = json.dumps({
                'id': f'thread_{thread_id}_msg_{i}',
                'type': '测试任务',
                'payload': {'thread': thread_id, 'index': i}
            })
            success = atomic_produce(msg)
            if not success:
                logger.warning(f"线程{thread_id} 第{i}条消息入队失败")
                break
            time.sleep(0.01)

    # 启动5个生产者线程
    threads = []
    for tid in range(5):
        t = threading.Thread(target=producer_thread, args=(tid,))
        t.start()
        threads.append(t)

    for t in threads:
        t.join()

    # 打印最终队列长度
    final_length = r.llen(QUEUE_NAME)
    logger.info(f"所有生产者完成后,队列长度: {final_length}")

5.4 延迟消息:用ZSet实现延迟队列

有时候我们需要消息延迟一段时间再被消费,比如用户下单后30分钟未支付就取消订单。Redis的列表本身不支持延迟,但可以结合ZSet(有序集合)来实现。

# 技术栈: Python + redis-py
# 文件: delayed_queue.py
# 功能: 基于ZSet的延迟消息队列实现

import redis
import json
import time
import threading
import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s'
)
logger = logging.getLogger(__name__)

r = redis.Redis(host='localhost', port=6379, db=0)

# 延迟消息存储在ZSet中,score是到期时间戳
DELAYED_ZSET = 'delayed_queue'
# 到期后移入的普通队列
READY_QUEUE = 'task_queue'


def schedule_delayed_message(message, delay_seconds):
    """
    将消息放入延迟队列
    message: 消息字典
    delay_seconds: 延迟多少秒后消息到期
    """
    message_json = json.dumps(message)
    # 计算到期时间戳(秒级)
    expire_at = time.time() + delay_seconds

    # 用ZADD把消息放入有序集合
    # score = 到期时间戳
    # member = 消息JSON字符串
    r.zadd(DELAYED_ZSET, {message_json: expire_at})
    logger.info(
        f"延迟消息已调度: id={message['id']}, "
        f"延迟={delay_seconds}秒, 到期时间={expire_at}"
    )


def promote_delayed_messages():
    """
    将到期的延迟消息移入就绪队列
    这个函数需要定时调用,比如每秒调用一次
    """
    now = time.time()

    # 查找所有score小于等于当前时间的消息
    # 即所有已经到期的消息
    expired_messages = r.zrangebyscore(
        DELAYED_ZSET,
        0,
        now,
        withscores=True
    )

    for msg_json, score in expired_messages:
        # 使用Lua脚本保证原子性:先从ZSet删除,再放入队列
        lua_promote = """
        local msg = ARGV[1]
        local score = tonumber(ARGV[2])

        -- 检查是否还在延迟队列中
        local member_score = redis.call('ZSCORE', KEYS[1], msg)
        if member_score == false then
            -- 已经被其他进程处理了,跳过
            return 0
        end

        -- 从延迟队列中移除
        redis.call('ZREM', KEYS[1], msg)

        -- 放入就绪队列
        redis.call('RPUSH', KEYS[2], msg)

        return 1
        """

        result = r.eval(lua_promote, 2, DELAYED_ZSET, READY_QUEUE, msg_json, str(score))

        if result == 1:
            msg = json.loads(msg_json)
            logger.info(f"延迟消息已激活: id={msg['id']}")

    return len(expired_messages)


def delay_queue_worker():
    """
    延迟队列工作线程
    定期检查并激活到期的延迟消息
    """
    logger.info("延迟队列工作线程启动")
    while True:
        promoted = promote_delayed_messages()
        if promoted > 0:
            logger.info(f"本轮激活了{promoted}条延迟消息")
        time.sleep(1)  # 每秒检查一次


if __name__ == '__main__':
    # 启动延迟队列工作线程
    worker = threading.Thread(target=delay_queue_worker, daemon=True)
    worker.start()

    # 模拟调度几条延迟消息
    for i in range(5):
        delay = (i + 1) * 10  # 10秒, 20秒, 30秒, 40秒, 50秒
        msg = {
            'id': f'delay_msg_{i}',
            'type': '取消订单',
            'payload': {'order_id': f'ORD_{1000 + i}'}
        }
        schedule_delayed_message(msg, delay)
        time.sleep(0.5)

    # 等待所有延迟消息到期
    time.sleep(60)
    logger.info("演示结束")

六、应用场景

6.1 订单处理系统

电商平台中,用户下单后会产生各种任务:扣库存、发通知、生成物流单、更新积分等。这些任务可以异步处理,用Redis列表做队列完全可以应对。

6.2 数据导出任务

用户点击"导出报表"按钮后,后台生成文件需要几十秒甚至几分钟。可以把导出请求放入队列,由后台worker慢慢处理,用户稍后去下载。

6.3 日志采集与处理

微服务产生的日志需要采集、解析、存储。用Redis列表做中转队列,前端服务负责写入,后端worker负责消费处理。

6.4 秒杀场景的任务削峰

秒杀活动瞬时涌入大量请求,直接打数据库肯定扛不住。把请求先放入Redis队列缓冲,后端按照数据库能承受的速率慢慢消费,起到"削峰填谷"的作用。

七、技术优缺点

7.1 优点

部署简单,运维成本低。 Redis本身就是一个轻量级的缓存服务,很多项目已经部署了Redis,直接用列表就能做消息队列,不需要额外搭建RabbitMQ或Kafka这样的重型中间件。

性能优秀,吞吐量高。 Redis是纯内存操作,单个实例的吞吐量可以达到每秒10万级。对于中小规模的消息处理,Redis列表完全够用。

API简洁,上手快。 Redis列表的核心操作就那几个命令:LPUSH、RPUSH、LPOP、RPOP、BLPOP。开发者不需要学习复杂的消息中间件概念,半天就能上手。

支持多种消费模式。 可以通过组合不同的命令,实现一对一消费、一对多消费、阻塞消费等模式,灵活性很高。

7.2 缺点

消息可靠性有限。 原生Redis列表没有消息确认机制,消费者取走消息后如果处理失败,消息就丢失了。需要自己实现可靠消费逻辑。

不支持消息回溯。 消息一旦被消费就没了,不能像Kafka那样按offset回退重新消费。这在数据对账、故障恢复场景下是个硬伤。

内存消耗大。 消息都存储在Redis内存中,如果消息量大或者积压严重,内存压力会非常大。而且Redis的持久化机制在极端情况下也可能导致数据丢失。

不适合超大规模场景。 当消息量达到每天上亿级别、或者需要跨机房高可用的时候,Redis列表就不够了,这时候应该选择Kafka、Pulsar等专业消息队列。

八、注意事项

8.1 队列命名要规范

建议给队列加上业务前缀和环境的区分,避免多个项目共用Redis时产生冲突。比如用prod:order:task_queue而不是简单的task_queue。

8.2 设置队列过期策略

如果队列长时间没有新消息,应该设置过期时间自动清理,避免僵尸队列占用内存。可以用Redis的EXPIRE命令给队列key设置TTL。

8.3 监控队列长度

一定要对队列长度做监控告警。当队列长度超过阈值时,应该及时通知运维人员,避免消息积压导致服务异常。

# 技术栈: Python + redis-py
# 文件: queue_monitor.py
# 功能: 队列长度监控示例

import redis
import time
import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s'
)
logger = logging.getLogger(__name__)

r = redis.Redis(host='localhost', port=6379, db=0)

# 需要监控的队列列表
WATCHED_QUEUES = [
    'task_queue:ready',
    'task_queue:processing',
    'delayed_queue'
]

# 告警阈值
WARN_THRESHOLD = 5000
CRITICAL_THRESHOLD = 10000


def monitor_queues():
    """
    定期检查队列长度
    超过阈值时发出警告
    """
    while True:
        for queue_name in WATCHED_QUEUES:
            length = r.llen(queue_name)

            if length >= CRITICAL_THRESHOLD:
                logger.critical(
                    f"[紧急] 队列 {queue_name} 长度: {length}, "
                    f"已超过紧急阈值 {CRITICAL_THRESHOLD}!"
                )
            elif length >= WARN_THRESHOLD:
                logger.warning(
                    f"[警告] 队列 {queue_name} 长度: {length}, "
                    f"已超过警告阈值 {WARN_THRESHOLD}"
                )

        # 每5秒检查一次
        time.sleep(5)


if __name__ == '__main__':
    try:
        monitor_queues()
    except KeyboardInterrupt:
        logger.info("监控已停止")

8.4 避免大Key

不要把单条消息做得太大。Redis列表的每个元素都是string类型,如果单条消息很大(比如几MB),会影响Redis的性能。建议单条消息控制在1KB以内,如果数据量大,可以把大对象存储在文件存储或数据库中,队列里只存引用ID。

8.5 生产者和消费者要解耦

消息的JSON格式要做好版本管理,新增字段时不要影响已有消费者。建议在消息体中包含版本号字段。

# 技术栈: Python + redis-py
# 文件: versioned_message.py
# 功能: 带版本管理的消息格式示例

import redis
import json
import time
import uuid

r = redis.Redis(host='localhost', port=6379, db=0)
QUEUE_NAME = 'task_queue'


def create_message_v2(task_type, payload):
    """
    创建带版本号的消息
    v2版本新增了priority字段和trace_id
    """
    message = {
        'version': 2,                    # 消息格式版本
        'id': str(uuid.uuid4()),          # 消息唯一ID
        'type': task_type,                # 任务类型
        'payload': payload,               # 任务数据
        'priority': 'normal',             # 优先级(v2新增)
        'trace_id': str(uuid.uuid4()),    # 链路追踪ID(v2新增)
        'timestamp': time.time(),         # 创建时间戳
    }
    r.rpush(QUEUE_NAME, json.dumps(message))
    return message


def create_message_v1(task_type, payload):
    """
    创建v1版本的消息(保持向后兼容)
    """
    message = {
        'version': 1,                    # 消息格式版本
        'id': str(uuid.uuid4()),          # 消息唯一ID
        'type': task_type,                # 任务类型
        'payload': payload,               # 任务数据
        'timestamp': time.time(),         # 创建时间戳
    }
    r.rpush(QUEUE_NAME, json.dumps(message))
    return message


def consume_versioned_message(message_json):
    """
    根据消息版本选择对应的处理逻辑
    """
    message = json.loads(message_json)
    version = message.get('version', 1)

    if version == 1:
        # v1版本处理逻辑
        print(f"处理v1消息: type={message['type']}")
    elif version == 2:
        # v2版本处理逻辑,可以获取priority和trace_id
        print(
            f"处理v2消息: type={message['type']}, "
            f"priority={message['priority']}, "
            f"trace_id={message['trace_id']}"
        )
    else:
        print(f"未知版本: {version}")


if __name__ == '__main__':
    # 生产一条v2消息
    msg = create_message_v2('发邮件', {'to': 'user@example.com'})
    consume_versioned_message(json.dumps(msg))

    # 生产一条v1消息
    msg = create_message_v1('发短信', {'to': '13800000000'})
    consume_versioned_message(json.dumps(msg))

九、文章总结

Redis列表在消息队列场景中的应用,本质上就是利用它"两端操作"的特性来实现FIFO的队列模型。对于中小规模的消息处理需求,它是一个简单高效的方案,不需要引入重型中间件,也不需要复杂的运维部署。

但在实际落地时,有几个关键点需要特别注意:第一,消息可靠性不能靠Redis列表本身保证,需要自己设计可靠消费机制,比如双队列加超时恢复;第二,消息去重必须做,用Redis的Set记录已处理的消息ID是最简单的方案;第三,队列长度必须做限制,防止内存溢出;第四,消息格式要有版本管理,方便后续扩展。

如果业务规模比较小、对可靠性要求不是特别高、团队对Redis比较熟悉,那用Redis列表做消息队列是一个非常务实的选择。但如果你的业务需要消息回溯、需要跨机房高可用、或者消息量非常大,那还是建议上专业的消息队列中间件。

说到底,技术选型没有银弹,关键是根据你的实际场景来权衡。Redis列表做消息队列,就像是骑自行车上班——对于大多数场景来说,它够快、够稳、够方便。但如果你需要日行千里,那还是得上火车或者飞机。