FEATURED · 精选文章

Redis List、ACL与阻塞队列:从数据组织到异步解耦的工程实践

发布时间 / 2026/9/15 11:45:13
来源 / 创域科博编辑部
栏目 / 资讯中心
Redis List、ACL与阻塞队列:从数据组织到异步解耦的工程实践 有人问我“Redis 到底学了能干嘛”我一般会反问一句你现在项目里的待办任务、最新动态、消息推送是不是都靠数据库硬扛如果是那这篇文章就正好是给你写的。Redis 的 List、ACL 和阻塞队列看着像是三个独立的知识点实际上拼在一起就是一套从数据组织、权限控制到异步解耦的完整方案。这篇内容适合后端开发、运维人员以及准备 Redis 相关面试的同学我会把命令背后的原理、生产环境里的落地方式还有我踩过的坑一起讲清楚。1. 三个主题为什么值得放在一起学1.1 Redis 里和“队列”沾边的三种思路Redis 能玩队列其实不止一种姿势。最常见的三种是 List、Stream 和 Pub/Sub。很多人一上来就纠结“该用哪个”结果选错方案后面越改越痛苦。List 是 Redis 最基础的数据结构底层是双向链表配合 LPUSH、RPOP 或者 BRPOP就能实现一个先进先出的队列。它的优势是简单、轻量、生态成熟绝大多数队列场景用它就够了。Stream 是 Redis 5.0 引入的专门消息队列支持消费组、消息持久化、ACK 确认功能上更像 Kafka 这类专业 MQ 的简化版。Pub/Sub 则是纯发布订阅模型消息不落盘消费者不在线消息就丢了。我的建议非常直接如果你的需求是“把任务塞进去消费者取走处理”List 就是首选。等哪天你需要消费者分组、消息回溯、确认机制了再考虑 Stream。Pub/Sub 只适合做实时通知这类允许丢失的场景别拿它当可靠队列用。1.2 把 List 当队列用最容易被忽视的边界条件List 做队列虽然简单但边界条件非常值得琢磨。第一个坑是“空队列轮询”。如果消费者用 RPOP 循环取数据队列为空时会瞬间上万次空请求打进 RedisCPU 白白烧掉不说还会拖慢其他命令。解决办法就是用 BRPOP 阻塞读取让消费者挂在那里等而不是反复来问。第二个坑是无脑 LPUSH 不做修剪。有些场景要的是“只保留最近 N 条”比如用户的操作记录、站内信列表。如果一直往左边塞数据右边的旧数据越堆越多内存迟早爆掉。这时候要配合 LTRIM 做裁剪比如每次 LPUSH 后执行LTRIM key 0 99列表就永远只留 100 条。第三个坑是消费者处理失败了消息直接丢。List 出队即删除没有 ACK 机制所以生产上要么自己包一层“处理失败重新入队”要么干脆引入 Stream 的 PEL待确认列表机制。我个人习惯是简单任务用 List 加 try-catch 重投核心链路直接上 Stream。1.3 ACL 不是安全工程师的专属话题很多开发者觉得 ACL 是运维和安全组的事自己只要有个全能账号能用就行。我刚开始也这么干所有服务共用一个默认账号权限天然全开。直到有一次排查线上问题时我发现自己能随手删掉别人业务里的缓存 key那一瞬间后背发凉。Redis 6.0 开始内置 ACL到了 7.x 已经相当成熟。ACL 的核心作用就一句话给不同的使用者分配不同的权限谁也不能越界。比如业务 A 只能读写cache:*前缀的 key只能执行 GET、SET 这类基础命令不能执行 KEYS、FLUSHALL、CONFIG 这些高危险命令。生产环境必须把“最小权限”当成默认准则。哪怕只是给内部服务开个账号也要把权限范围限制到它能碰到的 key 和命令。ACL 配置好之后不仅安全等级上去了排查问题也方便得多——你看 ACL LOG 就知道是谁在什么时间试图执行了什么违规命令。2. List 实战从数据结构到队列思维的转变2.1 List 的底层设计与使用边界要真正用好 List光会几个命令远远不够你得知道它内部是怎么回事。Redis 的 List 早期用压缩列表ziplist或者双向链表linkedlist实现后来 3.2 版本统一成了 quicklist。quicklist 可以理解成“把多个 ziplist 用双向指针串起来”每个节点内部是一段连续内存节点之间用指针连接。这样的好处很明显在列表较短时数据存在连续内存里CPU 缓存命中率高读写效率快列表变大后又能像链表那样在两端快速插入删除。所以 List 天生适合两端操作频繁的场景比如消息队列的 LPUSH BRPOP、最新动态的 LPUSH LRANGE。但 List 也有明确的边界它不支持随机访问。虽然可以用 LINDEX 按下标取元素但越往后越慢因为需要从头开始遍历。所以别拿 List 做需要频繁按下标查询的存储更别把它当数组用。我见过有人用 List 存一个业务表的数据动不动就 LINDEX 取中间某个值性能惨不忍睹后来换成哈希结构才好。2.2 队列场景的正确打开方式LPUSH BRPOP实际生产里我最常用的组合就是 LPUSH BRPOP。生产者用 LPUSH 把任务往队头塞消费者用 BRPOP 从队尾阻塞读取谁先进来谁先被消费完全符合 FIFO 的直觉。这里有个细节默认写法是 LPUSH RPOP但 RPOP 是非阻塞的队列为空时消费者会陷入疯狂轮询。改成 BRPOP 之后消费者线程会一直挂在那儿直到有数据可读或者超时。Redis 的阻塞命令不会占用 CPU而是通过事件循环里的注册回调被唤醒所以可以做到“挂机不烧电”。BRPOP 的超时参数单位是秒常用做法是设成 0永久阻塞或者一个合理的值。我一般给消费者设 30 秒的超时然后用 while 循环包起来超时后继续轮询。这样既能保证及时取到任务又能在 Redis 重启或者网络抖动时让消费者代码有机会做重连和日志记录。# 生产者 LPUSH task:queue task-001 # 消费者 BRPOP task:queue 302.3 时间线、排行榜、分页等高频用法除了队列List 还有三个高频场景值得单独拿出来说。第一个是时间线Timeline。比如一个社交 App 的首页 Feed 流每产生一条新内容就 LPUSH 到用户的 Timeline 列表再配合 LRANGE 做分页拉取。因为 List 天然按插入顺序排列所以 LPUSH LRANGE 就能非常自然地实现“最新在前、翻页浏览”的效果。网络上很多网友在搜索“list接口”、“模板里怎么填充”这类问题多半就是在类似场景里卡住了。第二个是排行榜。用 Sorted Set 做排行榜当然更专业但如果是“按照时间维度排序”的榜单List 甚至更合适。比如“最新上架的商品列表”LPUSH LTRIM 保留前 50 个每次查询直接 LRANGE 0 49性能极高。第三个是消息的分页消费。有些场景一次要拉一批任务而不是一条可以用LRANGE start stop先取出来处理完再批量删除。但要注意这种“先读后删”不是原子的多个消费者同时操作时可能重复消费必须自己做幂等。2.4 一个完整的任务队列案例我提供一个可以直接抄作业的简化版案例大家感受一下完整链路。需求用户注册后需要发一封欢迎邮件发邮件动作耗时且可能失败希望异步处理。生产者逻辑用户注册成功后调用某个服务把邮件任务的信息序列化成 JSON然后 LPUSH 到mail:queue。这里推荐把任务体设计得越完整越好比如收件人、邮件标题、模板编号全部塞进去消费者拿到后不需要再回查数据库。消费者逻辑用一个常驻进程循环执行 BRPOP拿到任务后反序列化调用邮件服务发送处理成功就继续下一条失败就记录日志并重新入队LPUSH 回原队列或者投递到延迟队列。为了防止异常导致消息永远卡在手里还要设置一个消费重试上限超过上限就进死信队列人工处理。# 任务入队 LPUSH mail:queue {to:userexample.com,template:welcome,tpl_id:1001} # 消费者批量查看队列长度 LLEN mail:queue # 消费者取出任务 BRPOP mail:queue 0这个案例里List 只是传输管道真正重要的是任务体设计和异常处理策略。很多初学者任务体只放一个 ID消费者拿到后还得查数据库结果数据库一抖整个队列就全卡死了。任务体里带全字段反而能降低下游依赖。3. ACL 权限控制的完整落地过程3.1 Redis 7 的 ACL 到底能管什么Redis 7 的 ACL 功能相比早期版本已经非常完善。它支持四个维度的权限控制用户管理可以创建多个用户每个用户有独立的密码和权限。命令权限可以指定用户能执行哪些命令比如get、set或者按类别授权比如read。键权限可以限制用户只能访问某些 key 模式比如~cache:*。发布订阅权限可以限制用户能订阅/发布的频道模式。这四层权限组合起来基本上能覆盖绝大多数生产场景。比如给一个专门做缓存的业务服务开账号就只允许它读写cache:*前缀的 key只能执行 GET、SET、DEL、EXPIRE 这些缓存相关命令其他一律禁止。3.2 生产环境配置一个最小权限账号假设我们要给一个店铺服务开一个 Redisson 客户端账号只允许它访问shop:*前缀的 key执行常规的读写命令。# 创建用户 shop_user设置密码限制 key 前缀和命令 ACL SETUSER shop_user on Shop2024 ~shop:* read write -admin # 查看用户权限 ACL GETUSER shop_user这里有几个容易踩的坑。第一~shop:*只匹配这个模式的 key如果 Redisson 内部的锁 key 用了redisson_lock这类前缀就会被 ACL 拦下来。所以配置前缀前一定要把客户端用到的所有 key 前缀都盘点一遍。第二read write是按类别授权生产环境不建议直接allcommands或者all。有网友在查“redis 7 前缀 acl 生产环境配置”时八成就是遇到了权限给了但客户端报 NOPERM 的错。第三密码里的特殊字符要注意。Redis 的 ACL 密码需要用来设置密码本身如果是大写字母加特殊符号命令行里最好用单引号包起来避免 shell 帮你做变量展开或者转义。3.3 ACL 文件与 Docker 部署的配置方式ACL 可以放在 redis.conf 里也可以独立成 aclfile。我强烈推荐独立 aclfile这样改权限不需要动主配置而且可以单独做版本管理和备份。# redis.conf 中启用 ACL 文件 aclfile /etc/redis/users.acl然后在 users.acl 里写规则user default on nopass ~* all user shop_user on Shop2024 ~shop:* read write -admin改完 aclfile 之后在 Redis 里执行ACL LOAD就能热加载不需要重启。反过来如果在命令行里通过 ACL SETUSER 修改了规则可以用ACL SAVE把当前规则保存到文件。Docker 部署时把 aclfile 挂载进去docker run -d \ -p 6379:6379 \ -v /myconf/redis.conf:/usr/local/etc/redis/redis.conf \ -v /myconf/users.acl:/etc/redis/users.acl \ redis:7.2 redis-server /usr/local/etc/redis/redis.confredis.conf 里一定要加上 aclfile 的路径否则容器内 Redis 不会自动加载这个文件。网上很多网友在用 docker 安装 redis 主从或者 Redis 7.2.4 的 ACL Docker 配置时遇到权限不生效绝大多数都是这个原因文件挂进去了配置里没启用等于白挂。3.4 排查 ACL 问题的实操经验ACL 配置完之后最常遇到的问题就是“客户端连上去了但执行命令报 NOPERM”。这时候别急着改配置先看 ACL LOG。# 在 redis-cli 里执行 ACL LOGACL LOG 会显示被拒绝的操作日志包括用户、命令、key 上下文。它会明确告诉你某个用户试图执行哪个命令访问哪个 key 被拦了。根据日志去调整规则效率比瞎猜高得多。还有一个常见误区是分不清网络设备的 ACL 和 Redis 的 ACL。网上很多人在搜“华三 ipv6 acl配置实验”“msr20-20怎么绑acl到端口上”“acl访问控制列表”这些是路由器/交换机上做包过滤的访问控制列表跟 Redis 的用户权限控制完全是两回事。如果你搜 ACL 搜出来的文章全是网络设备的配置教程那你得清醒一点那不是你要的东西。另外ACL 里有个很容易踩的细节用~*这种匹配规则不会匹配 Lua 脚本内部访问的 key。也就是说如果脚本里用了某个 key 而当前用户没有该 key 的权限脚本会执行失败。这个问题排查起来非常隐蔽因为单条命令测试时没问题一跑脚本就报错。我的习惯是脚本涉及的所有 key 前缀都在用户的 key 规则里提前放行。4. 阻塞队列从 Redis 命令到线程池选型4.1 BLPOP/BRPOP 是怎么实现阻塞等待的BRPOP 底层并不是“循环轮询”它靠的是 Redis 的事件驱动机制。当一个客户端执行 BRPOP 且队列为空时这个客户端会被挂起Redis 把它记录在对应 key 的阻塞客户端列表里。之后如果有 LPUSH 写入该 keyRedis 会主动把数据推给被阻塞的客户端并唤醒它继续执行。这整个过程对客户端来说是无感的看上去就是“阻塞了一会儿然后直接返回数据”。所以阻塞队列非常适合“消费者不知道任务什么时候来”的场景比如秒杀系统的下单请求、爬虫的任务调度、邮件发送这种异步处理。需要注意BRPOP 可以一次监听多个 key比如BRPOP queue1 queue2 0Redis 会从左到右检查一旦某个 key 有数据就立刻返回。这个特性在做消息路由时很实用比如高优先级队列优先消费。4.2 Redis 阻塞队列的可靠性边界我得说句掏心窝的话用 Redis List 做阻塞队列是在“不想引入重量级 MQ”的前提下最务实的选择但它有明确的可靠性边界。第一个边界是“消息可能丢”。消费者 BRPOP 拿到消息后进程崩溃消息就直接没了。因为没有持久化确认机制也没有消费位点。要解决只能在消费者端做本地持久化或者换 Stream。第二个边界是“阻塞连接占资源”。Redis 的单线程模型决定了它能处理的并发有限如果几千个客户端同时阻塞在几个 key 上虽然事件机制不会消耗大量 CPU但连接数本身就是内存和句柄开销。生产上我会控制消费者的数量而不是无脑扩容。网上有人在搜“线程池的阻塞队列选择”时容易把 Java 线程池里的队列选型和 Redis 的阻塞队列搞混我建议把这两件事分开理解线程池里的队列是 JVM 内部的调度工具Redis 的阻塞队列是跨进程的通信工具两者不是一个层面的东西。第三个边界是“积压后拉长延迟”。如果队列里的消息长期没人消费List 会一直堆积新消息排在后面延迟越来越高。这时候要用 LLEN 监控队列长度设置告警阈值比如超过 1 万条就通知值班人员。4.3 线程池的阻塞队列怎么选既然热词里反复出现“线程池的阻塞队列选择”这里就顺便把这块讲透。Java 并发编程里ThreadPoolExecutor 的核心参数之一就是阻塞队列它的选择直接影响任务的拒绝策略和执行顺序。ArrayBlockingQueue有界队列容量固定适合对任务堆积有硬性要求的场景能防止内存被耗尽。LinkedBlockingQueue默认无界也可以指定容量吞吐量较高但对任务堆积没限制如果消费者跟不上内存会一直涨。SynchronousQueue不存储任务生产者直接把任务交给消费者线程适合“希望任务尽快被处理”的场景配合CallerRunsPolicy很常见。PriorityBlockingQueue支持按优先级出队适合“重要任务先处理”的场景但要注意优先级翻转问题。我实际项目的选型经验是核心业务线程池尽量用有界队列比如ArrayBlockingQueue并且设置拒绝策略为记录日志并降级处理而不是直接抛异常。无界队列看起来省事真到流量洪峰时就是内存失控的开始。ThreadPoolExecutor executor new ThreadPoolExecutor( 8, 16, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue(1000), new ThreadPoolExecutor.CallerRunsPolicy() );4.4 阻塞队列与 Redis 链路的组合用法有时候Redis 阻塞队列和线程池并不是互斥的而是配合使用。比如消费者进程从 Redis BRPOP 拿到消息后并不直接执行耗时逻辑而是丢进线程池里异步处理让 Redis 的消费者连接尽快回到阻塞状态继续取下一条。这样做的核心好处是“把 Redis 操作和业务执行解耦”。Redis 的消费者连接数量是有限的如果每个连接处理一条消息要 5 秒那整个消费吞吐量就被卡死在“连接数除以 5 秒”的数值上。引入线程池后Redis 的连接只负责“取消息”业务逻辑交给工作线程并发处理吞吐量能提升一个量级。我用这个模式实现过一个发送站内信的服务Redis 队列作为缓冲消费者线程池大小调到 32单机每天能处理几十万条消息Redis 的连接数始终保持稳定一次没崩过。5. 高频问题与避坑速查5.1 常见网络搜索结果里的问题排查我从网络热词里挑了几个有代表性的问题整理成速查表覆盖网上常见困惑大家可以直接对照排查。问题现象可能原因解决思路Redis Desktop Manager 无法连接远程 Redis配置文件 bind 限制或 protected-mode 开启设置 bind 0.0.0.0 并关闭 protected-mode生产环境用 ACL 限制访问WSL 环境wsl --list --online连接超时网络连接问题微软远端仓库不可达代理或者直接换镜像站安装完 Redis 后优先用localhost:6379测试小程序downloadfile:fail create downloadtask:fail url not in domain list下载域名未在平台配置白名单在小程序管理后台把文件服务器域名加进 downloadFile 合法域名列表OSS 上传报put public object acl is not allowed存储桶策略禁止公写调整存储桶权限策略不要对 public 前缀的 key 做公共写权限java 获取两个 list 交集集合运算不熟悉用list1.retainAll(list2)可以直接求交集大数据量用 HashSet 加速Java EasyExcel 渲染嵌套 List 卡住模板变量设计不到位嵌套集合要用 List 泛型字段模板里写{?item.list}这种循环块Redis 序列化后值乱码序列化器不一致统一使用 Jackson 或 GenericJackson2JsonRedisSerializer别混用这些问题的共同点都是“思路清楚后实际操作一两分钟能解决”但如果没有排查方向很容易卡几小时。做开发就是这样很多时候瓶颈不是技术难度而是没见过这个坑。5.2 阻塞队列选型速查再把阻塞队列选型单独列一张表方便大家在技术方案评审时快速对号入座场景特点推荐方案理由跨服务异步任务、削峰填谷Redis List BRPOP轻量、部署简单适合中小规模需要消费组、ACK、消息回溯Redis Stream功能接近专业 MQ可靠性更高允许丢消息的实时通知Redis Pub/Sub延迟最低但无持久化JVM 内部任务并发调度ArrayBlockingQueue有界、可控防止内存爆掉高吞吐、大量短任务LinkedBlockingQueue默认无界但吞吐高需监控堆积追求最低调度延迟SynchronousQueue任务直达消费线程不做缓冲Redis 阻塞队列适合的是“能容忍少量消息丢失、又不想引入 Kafka/RabbitMQ 的重型依赖”这种场景。如果项目里消息一条都不能丢或者需要复杂的路由和分发策略该上专业 MQ 就上专业 MQ别硬扛。5.3 我踩过的三个坑最后分享三个我自己真实踩过的坑写出来希望帮大家节省时间。第一个是“消费者挂了Redis 里堆积了上百万条消息恢复后系统被冲垮”。当时没有队列积压告警一个消费者进程 OOM 挂了两小时恢复启动后BRPOP 疯狂消费积压数据下游数据库瞬间被压垮。从那以后我在所有消费端都加了消费限速每次最多连续取 100 条就 sleep 一秒同时 LLEN 告警阈值设为 5000。第二个是“ACL 配置没问题但 Redisson 报 WRONGPASS”。排查了很久才发现Redisson 连接池里有几个连接是配置前创建的配置 ACL 后这些旧连接还带着旧密码没有自动重连。解决办法是配置 ACLLOAD 后重启客户端或者用有优雅重连机制的客户端版本。第三个是“直接在生产环境执行 KEYS”。KEYS 命令的时间复杂度是 O(N)在 key 数量上百万时它会阻塞 Redis 单线程模型导致所有请求卡住几秒。这个命令要彻底禁用需要模糊查询就改用 SCAN或者用哈希结构把 key 打散。如果你也在做 Redis 队列或者 ACL 权限改造我建议从最小范围开始先给一个非核心业务配上最小权限账号再把一个任务链路切到 LPUSH BRPOP跑一周看监控数据再逐步推广。凡是涉及权限和消息的地方慢就是快稳就是快。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻