尧图精选

C++工程实战:Redis List从原理到消息队列与时间线落地

🕒 发布时间:2026/10/2 15:02:41 📁 来源:尧图网络
刚接手一个C后端项目需要把一批异步任务从进程内队列挪到Redis里做统一调度。翻了半天资料发现聊Redis List的命令用法的文章一抓一大把但是真正以C为第一视角、从客户端选型到落地场景完整走一遍的却不多。过来人经验Redis List这个东西在C工程里用得好不好往往不取决于你记不记得住命令而在于你对底层数据结构理解到什么程度、客户端封装怎么选、以及遇到并发和故障时有没有预案。这篇内容就围绕“在C项目里老老实实用好Redis List”这件事展开。会讲清楚List在业务里适合做什么、不适合做什么C主流客户端库怎么选核心命令封装怎么写消息队列和时间线这两个高频场景怎么落地最后把我在实际项目里踩过的坑、排查过的线上问题一次聊完。适合正在用C做后端服务、队列、缓存、榜单功能的朋友尤其是那些Redis命令熟但不太确定C语境下该怎么下手的初中级工程师。1. Redis List到底适合干什么——场景判断与设计思路1.1 底层实现决定了你的使用边界先搞清楚Redis List在服务端到底长什么样。早期版本的List就是普通双向链表每个节点一个字符串对象插入删除确实O(1)但是内存碎片化严重、缓存命中率也差。后来官方把它重构成了quicklist结构——本质上是“双向链表 ziplist压缩列表”的混合体每个链表节点内部是一个或多个ziplist既保留了双端操作的超高效率又把内存压缩和局部性优化做到了极致。这个结构对C开发者有什么直接影响我给你翻译一下头部尾部操作LPUSH、RPUSH、LPOP、RPOP都是O(1)这是List最硬核的能力。中间随机访问LRANGE取中间段不是O(1)因为要从链表节点逐个遍历过去数据量大了之后延迟会明显上涨。每个ziplist的节点尺寸会动态调整Redis会限制单个ziplist的大小超出就拆分所以长度增长并不会让单次操作复杂度失控。List没有去重能力没有按分值排序能力这些是Set和ZSet的领地别硬拿List去干。很多人拿ZSet也能实现队列score放时间戳为什么还要用ListZSet是跳表实现的插入删除是O(logN)并且元素带分值天然支持范围查询。但List的双端进出是O(1)在纯粹的FIFO/LIFO队列场景里比ZSet更轻、更快、更省内存。所以选型判断很简单不需要排序、不需要去重、不需要消费组只要一个先进先出或者后进先出的通道List就是最优解。Redis还提供了阻塞版命令BLPOP和BRPOP没有数据的时候阻塞等待而不是轮询空转这让List在消息队列场景里的优势进一步放大后面会专门讲C里怎么正确使用这些阻塞命令。1.2 C业务里的典型应用场景与反模式结合我见过的工程实践Redis List在C服务里最值得用的场景有这么几类第一类是轻量消息队列。生产者LPUSH消费者BRPOP这是List最经典的玩法。任务量不大、不需要严格投递语义、不需要消费组分组直接用它比Kafka轻太多。我之前的电商订单服务里用户下单后需要做库存扣减、积分发放、短信通知这些操作串行执行太慢就丢到这个List队列里异步处理效果很稳定。第二类是时间线/Fresh Feed。每个用户一条List发动态就LPUSH拉取就LRANGE分页。List天然按插入顺序排列正好符合时间倒序展示的需求Facebook早期做Timeline就是这套思路。配一个LTRIM限制列表长度防止老数据无限膨胀。第三类是简单状态机/操作栈。比如撤回操作、浏览器前进后退用RPUSH记录状态、RPOP回退一次业务操作对应一次O(1)的弹出非常简单直观。反模式也需要列一下免得你走弯路不要把List当Set用需要去重、判断成员存在的场景应该用Set或直接用Redis的SISMEMBER。不要把List当排行榜用榜单要按分值排序选ZSet。不要让单个List无限增长千万级大Key会导致LRANGE阻塞、内存暴涨必须配合LTRIM或者定期裁剪。不要把List当持久化消息队列做丢消息敏感的业务Redis的持久化机制决定了一定会有极端故障下的数据丢失风险敏感消息请用Stream或独立消息中间件。2. C客户端选型与运行环境搭建2.1 hiredis、redis-plus-plus、cpp_redis怎么选C项目用Redis绕不开客户端选型。国内社区讨论最多的三款hiredis、redis-plus-plus、cpp_redis。我在不同项目里都用过直接说结论。hiredis是Redis官方维护的C语言客户端库。轻、快、稳定几乎所有C封装库最终都依赖它。但它给你的就是纯C接口需要自己管理redisContext连接、自己释放redisReply稍微不注意就内存泄漏或者把同一个reply释放两次直接崩溃。适合喜欢极简、可控、不介意被C API支配的团队。redis-plus-plus是C11封装的客户端库底层依赖hiredis。作者一直在维护接口设计非常现代支持STL容器、支持连接池、支持异步和发布订阅、支持Lua脚本调用。平时写业务代码直接创建Redis对象然后调用lpush、brpop、lrange等方法返回值和异常处理都很符合C习惯是我目前最推荐的生产级选择。cpp_redis是纯C实现不依赖hiredis走的是异步回调风格。早期概念很惊艳但是更新速度慢异步模型对初学者友好程度一般生产项目里见到的频率也越来越低了。不是不能用是没必要给自己找不自在。我整理了一张表方便你对比维度hiredisredis-plus-pluscpp_redis语言CC11C11底层依赖无hiredis无接口风格C API现代C / STL异步回调连接池需自研内置需自研异步支持不支持支持支持维护活跃度官方维护高低适用场景底层模块、嵌入式库业务服务首选轻量实验选取型逻辑其实很简单如果你在写一个对依赖深度敏感的基础组件比如要嵌入到自己的网络库里那用hiredis自己封一层RAII最干净如果你就是写业务服务别再考虑原始C API了直接用redis-plus-plus把精力省下来处理业务逻辑。2.2 vscode下C环境配置要点既然热词里大量出现vscode配置C/C环境的问题这里就顺带讲一下怎么把上述客户端库在vscode里跑起来。工作环境是Windows或者Linux都适用核心思路是两条——库得装好路径得配好。Linux下推荐直接源码编译安装。redis-plus-plus要求先装hiredis按顺序来git clone https://github.com/redis/hiredis.git cd hiredis make sudo make install git clone https://github.com/sewenew/redis-plus-plus.git cd redis-plus-plus mkdir build cd build cmake .. -DCMAKE_BUILD_TYPERelease make -j4 sudo make installWindows下最省事的方案是vcpkg。装好vcpkg之后一条命令把两个库都拉下来vcpkg install redis-plus-plus hiredis然后在CMakeLists.txt里这样配置find_package(redis REQUIRED) find_package(hiredis REQUIRED) target_link_libraries(your_app PRIVATE redis hiredis)vscode里需要特别注意includePath的配置。因为库安装路径在不同平台不一样单纯靠默认搜索路径经常会发现头文件找不到。在c_cpp_properties.json里把hiredis和redis的include目录加上{ configVersion: 4, name: Linux, includePath: [ ${workspaceFolder}/**, /usr/local/include, /usr/local/include/sw ] }这里有个常见误区——redis-plus-plus提供的头文件默认在/usr/local/include/sw/redis/目录下代码里写#include sw/redis/redis.hIndexing时头文件解析不到不是库没装好就是includePath少了一层/usr/local/include。vscode那套环境变量配置看着繁琐其实核心就这一件事配好了跳转和补全全通。Windows下还有个隐藏问题运行程序报“找不到redis.dll”或者编译器报链接错误多半是Visual C Redistributable运行库缺失或者版本不对。解决方式很简单去微软官网装最新的VC Redistributable然后再跑一遍。这个坑看着低级实际排查起来能卡一个下午。3. Redis List核心命令的C封装与实操3.1 LPUSH、RPUSH、LPOP、RPOP等基础命令的封装Core命令本身不复杂但在C工程里怎么封装得顺手、安全和可调试才是重点。先看redis-plus-plus的基础用法#include sw/redis/redis.h #include iostream using namespace sw::redis; int main() { // 连接Redis连接串格式tcp://host:port auto redis Redis(tcp://127.0.0.1:6379); // RPUSH从尾部依次插入三条数据 redis.rpush(queue:demo, {task_1, task_2, task_3}); // LLEN查看列表长度 auto len redis.llen(queue:demo); std::cout length *len std::endl; // LPOP从头部弹出一条 auto item redis.lpop(queue:demo); if (item) { std::cout popped: *item std::endl; } else { std::cout list is empty or key not exist std::endl; } // LRANGE取下标0到9之间的元素 std::vectorstd::string items; redis.lrange(queue:demo, 0, 9, std::back_inserter(items)); for (const auto s : items) { std::cout s std::endl; } return 0; }有几个细节我想特别强调。第一redis-plus-plus的取值命令返回的是Optional 这个设计是在模仿std::optional的语义。lpop一个不存在的key返回的Optional里没有值而不是返回空字符串。很多新手直接*item然后不解引用判空运行时就崩了。所有单值读取类命令取返回结果前一定要判空。第二lrange是把结果通过output iterator回填到容器里这里必须传std::back_inserter(items)不传的话函数不知道把元素塞到哪个容器里。这点和STL算法的习惯是一致的用顺手了很舒服。如果你不知道列表里大概有多少条建议先llen一下预估容量再调用lrange避免频繁vector扩容导致的内存拷贝。第三key命名。虽然和命令无关但C服务尤其是微服务架构下Redis key会有不同业务共用同个实例的情况。我习惯统一格式业务域:对象类型:对象ID比如order:task:1024后续排查线上问题时用通配符匹配和按前缀拆分都方便。hiredis的写法就不一样了纯C API直接拼命令字符串#include hiredis/hiredis.h redisContext *ctx redisConnect(127.0.0.1, 6379); if (ctx NULL || ctx-err) { printf(connect error: %s\n, ctx ? ctx-errstr : ctx is null); return -1; } redisReply *reply redisCommand(ctx, RPUSH queue:demo task_1 task_2); if (reply) { printf(pushed %lld\n, reply-integer); freeReplyObject(reply); } reply redisCommand(ctx, LRANGE queue:demo 0 -1); if (reply) { for (size_t i 0; i reply-elements; i) { printf(%s\n, reply-element[i]-str); } freeReplyObject(reply); } redisFree(ctx);hiredis这种写法最大的坑就是reply释放。Redis返回的一定要freeReplyObject漏一次内存泄漏释放两次直接崩溃。我见过一个线上C模块因为多线程里共享同一个reply对象一个线程释放另一个线程还在读直接c0000005访问违例。所以用hiredis务必建立“谁申请谁释放”的纪律。3.2 阻塞命令BLPOP与BRPOP的正确打开方式消息队列场景里消费端最常用的其实不是LPOP而是BRPOP和BLPOP。这两个命令在List为空时不会立刻返回而是阻塞住直到有新的元素push进来或者超过了指定的超时时间。语义上有差别BLPOP从头部弹出BRPOP从尾部弹出。用List做FIFO队列时生产者从尾部RPUSH消费者从头部LPOP/BRPOP两头互不冲突。redis-plus-plus里BRPOP的返回是一个Optionalpairstring, string分别对应弹出的key和valueauto item redis.brpop(queue:task, 30); // 阻塞最长30秒 if (item) { auto key item-first; auto value item-second; std::cout from key: key , value: value std::endl; }需要特别注意的是redis-plus-plus这个timeout参数的语义是普通阻塞不是无限阻塞。传0才是永久阻塞。生产代码里我一般不会传0因为如果Redis服务发生异常永久阻塞会让消费者线程卡死项目里我会配合心跳和重连机制来做自愈。阻塞命令在C工程里另一个容易踩的坑是——多个消费者同时BRPOP同一个keyRedis会保证一个元素只会被一个消费者取走不会出现争抢重复这一点是Redis服务端控制的不需要你额外加锁。但要小心的是连接池和线程池的组合。如果连接池大小小于并发消费者数剩下线程会排队等待获取空闲连接这本身没问题但如果你在持有连接的同时又去获取另一个连接就会死锁。我建议队列消费者的ThreadPool数量和连接池大小保持1:1的关系或者直接给每个消费者线程绑定独立连接省心很多。hiredis的使用也类似命令前加B即可但要注意阻塞期间这个TCP连接是被占用的不能同时做其他命令。如果你的服务是单连接复用模型一个地方BRPOP会把整个连接堵住其它请求全部排不上队。这种场景要么用独立连接伺候阻塞消费要么走redis-plus-plus的异步接口。4. 业务场景实战消息队列与时间线落地4.1 用Redis List实现轻量消息队列现在落到业务实战先做消息队列。需求背景订单服务里用户下单之后要触发库存扣减、优惠券核销、积分发放、消息推送这四个动作同步执行的话接口响应要加几十毫秒流量一大根本扛不住。解决办法就是通过Redis List异步化。生产者端就一行逻辑把任务序列化成字符串之后RPUSH。void ProduceTask(const std::string task_json) { auto redis Redis(tcp://127.0.0.1:6379); redis.rpush(order:async:task, task_json); }消费者端采用一个常驻线程池每个线程循环BRPOP#include sw/redis/redis.h #include atomic #include iostream #include thread #include vector using namespace sw::redis; std::atomicbool g_running{true}; bool ProcessTask(const std::string task_json) { // 解析并执行业务逻辑 // 返回true表示处理成功false表示失败 return true; } void ConsumerWorker(const std::string uri, int worker_id) { auto redis Redis(uri); while (g_running.load()) { try { auto result redis.brpop(order:async:task, 3); if (!result) { // 超时没数据继续循环 continue; } const std::string task result-second; bool ok ProcessTask(task); if (!ok) { // 简单重试把任务放回另外一个重试队列 redis.lpush(order:async:task:retry, task); } } catch (const Error e) { // 连接异常等错误简单退避后重试 std::cerr worker worker_id error: e.what() std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(200)); } } } int main() { std::vectorstd::thread workers; for (int i 0; i 4; i) { workers.emplace_back(ConsumerWorker, tcp://127.0.0.1:6379, i); } for (auto t : workers) { t.join(); } return 0; }这个例子很简单但已经包含了生产环境里最核心的几个要点第一BRPOP的超时时间不要设成0。设成3秒消费者最多等3秒就会回到循环顶部这样你有机会检查g_running标志实现优雅退出。设成0等于永久阻塞停机的时候线程根本喚不醒只能强行结束进程容易丢半截任务。第二处理失败要重新入队但别直接丢回原队列。我这里的方案是往retry队列里lpush这样主队列和重试队列是分离的不会因为某一条任务反复失败堵住后面所有消息。后续可以再起一个扫描器定期处理重试队列或者人工干预。第三BRPOP在多消费者场景下天然实现了负载均衡每条消息只会被一个worker取走。不需要你额外写分布式锁Redis内部已经保证了这个语义这也正是相比进程内队列的显著优势。Redis List消息队列的缺陷我也必须说清楚它没有消息确认机制。消费者BRPOP成功的那一刻这条消息就从列表里删除了如果业务处理过程中服务宕机消息就永久丢失。对于丢一条也无所谓的场景比如用户动态点赞通知、操作日志写入完全够用对账务、订单状态流转这种绝对不能丢的消息不要只依赖List要么消费完成后再写持久化存储要么用Redis Stream配合消费组换取更多可靠性控制。4.2 时间线/最新消息列表的实现思路消息队列之外Redis List在“时间线”场景的出场率也极高。这类业务形态非常简单用户发一条动态往他自己的Feed列表头部插一条拉取首页时按时间倒序取最近N条。用List简直是量身定做。发布动态的C实现void PublishPost(const std::string user_id, const std::string post_id) { auto redis Redis(tcp://127.0.0.1:6379); std::string feed_key feed: user_id; // 头部插入新动态 redis.lpush(feed_key, post_id); // 只保留最近500条防止列表无限膨胀 redis.ltrim(feed_key, 0, 499); }拉取动态列表实现滚动分页std::vectorstd::string FetchFeed(const std::string user_id, int page, int page_size) { auto redis Redis(tcp://127.0.0.1:6379); std::string feed_key feed: user_id; // 通过偏移量模拟游标分页 int start page * page_size; int end start page_size - 1; std::vectorstd::string post_ids; redis.lrange(feed_key, start, end, std::back_inserter(post_ids)); return post_ids; }这套写法的逻辑很顺畅头部push配LTRIM天然就是“最新N条”的数据结构。但有两个工程细节容易被忽略。一个是LTRIM的执行时机。每次push后立刻ltrim能控制列表长度但也多了一次命令往返。高并发场景下可以考虑批量处理比如攒到10条动态才执行一次ltrim或者依赖定期任务统一裁剪。Redis的pipeline在这里特别有用redis-plus-plus可以用auto pipe redis.pipeline();批量提交lpush和ltrim减少RTT。另一个是“粉丝时间线”场景——你关注的博主发了一条动态你要把这条动态搬进每个粉丝自己的feed里。假设百万粉丝那就要往百万个List里lpush这个开销是巨大的。业界通常的做法是拆分为“拉模式”和“推模式”混合大V发动态只写入自己的发件箱Pull模型普通用户发动态再推送给粉丝Push模型同时结合Read/Write操作平衡负载。List在这里只是存储载体真正的架构设计在你。4.3 简单排行榜场景与List的边界有时候业务要一个“最近的访问记录”“最近浏览的商品”这种记录数量级可控比如几百条也不需要分值排序只要按时间顺序去展示。用List同样很顺手。void RecordBrowseHistory(const std::string user_id, const std::string goods_id) { auto redis Redis(tcp://127.0.0.1:6379); std::string key browse:history: user_id; redis.lpush(key, goods_id); // 最多保留20条 redis.ltrim(key, 0, 19); }但你要是真的想实现一个排行榜比如“热销商品Top100”List就不合适了。它没分值概念没法直接按销量排序即使勉强把销量拼在value里每次变更都要把整个列表拉出来重建。ZSet才是榜单场景的正解score直接放销量ZREVRANGE取TopN复杂度O(logN)O(M)结构上就完胜。所以我的结论是List适合“顺序记录型”的榜单不适合“分值排名型”的榜单。做技术选型的时候先问问自己这张榜单到底需不需要“按某个分数排序”需要就选ZSet不需要就放心用List。5. 常见问题与排查技巧实录5.1 空列表消失、key过期与数据可见性Redis List有个很容易被忽视的行为当你把列表最后一个元素弹出来之后这个key会自动被删除顶层key直接不存在。这意味着你LINDEX或者LRANGE一个空列表得到的不是空结果而是“key不存在”的结果。如果业务逻辑里要区分“列表为空”和“列表不存在”这个差异会带来bug。比如展示一个用户的“已读消息列表”空列表应该提示“暂无消息”key不存在却被业务代码误判为“用户从未使用过该功能”从而走一套完全不同的逻辑。处理办法有两种一是程序里主动维护空列表的key比如业务初始化时rpush一个占位符再lpop掉二是读取时明确区分服务的返回语义不要用顶层key是否存在代替业务判断。另一个相关话题是过期时间。List本身可以设置EXPIRE很多人在创建队列或Feed的时候会在写入后调用expire设置一天的过期时间。但注意EXPIRE是针对整个key的只要设置了过期时间到期后整个列表连同所有数据一起消失。对队列而言这不是问题队列数据本来就是短期存在的对时间线而言就危险了曾经有大型活动页面的Feed列表因为设置了8小时过期凌晨过了时限用户直接看到一片空白。如果你希望列表长期存在但数据又不会无限增长正确组合是LTRIM控制长度 设置一个较长的过期时间作为兜底清理而不是指望过期时间来做数据淘汰。5.2 并发竞争、ABA问题与Lua脚本方案热词里出现了“aba问题c”这是C多线程并发编程里CAS操作的经典问题。在Redis List的语境下对应的场景就是多个生产者/消费者并发操作同一个列表时如何保证一系列操作的原子性。举个例子消费端期望通过“LPOP 业务处理 失败重新入队”来完成一次任务处理。但LPOP和后面的RPUSH不是原子操作——两个线程同时LPOP可能同时拿到同一任务的镜像不会Redis单命令本身是原子的LPOP不会被两个线程取到同一个值。真正的风险在复合操作比如你想实现“从队列A取一条通过某种规则决定要不要放到队列B”这需要多个命令配合多线程下就有竞态条件。解法很清晰用Lua脚本把这些命令封装成一个原子操作。Redis执行Lua脚本时期本身是原子的中间不会被其他命令插入。// Lua脚本原子地完成【从list取出一个元素 加入处理中列表】 const char* acquire_script R( local task redis.call(LPOP, KEYS[1]) if task then redis.call(RPUSH, KEYS[2], task) end return task ); auto redis Redis(tcp://127.0.0.1:6379); auto result redis.eval(acquire_script, {task:queue, task:processing}, {});redis-plus-plus的eval支持keys和argv参数Keys是std::initializer_liststd::string或者容器返回值是Optional具体类型看脚本返回什么。这种方案能让你把多个命令打包成一个原子单元也是我在生产环境里处理并发竞争的常规手段。多线程环境下还有一个藏在暗处的问题——连接对象的线程安全。redis-plus-plus的Redis对象本身是线程安全的内部会处理连接的获取和释放你可以在多个线程里共享同一个Redis对象。但如果你手动复用hiredis的redisContext做并发操作就要自己加锁或者创建连接池否则两个线程同时开头发命令响应完全会对不上。我在项目里遇到过现象很离奇线程A执行LPOP线程B执行RPUSH结果A拿到的返回值是RPUSH的结果。原因就是共享同一个hiredis连接没有做同步。这也是从hiredis往redis-plus-plus迁移的最大理由。5.3 vscode环境与链接调试实战前面讲了vscode配置基础这里再深入聊几个我在实际项目里被问过最多的编译问题。一个是#include sw/redis/redis.h头文件死活找不到另一个是链接阶段报unresolved external symbol。头文件找不到绝大多数是路径问题。Windows下vcpkg默认把头文件安装到了C:\vcpkg\installed\x64-windows\include但vscode的IntelliSense默认不会去搜这个目录。要么在c_cpp_properties.json里加includePath要么更粗暴一点直接把整个installed目录加进去。除此之外还有个小技巧在.vscode/settings.json里设置C_Cpp.default.includePath这样所有cpp文件共享一套配置。链接错误又是另一回事。很多人在vscode里点击运行用的是Code Runner或者tasks.json直接g编译这里必须手动指定链接库。g -stdc17 main.cpp -o app -lredis -lhiredis注意顺序——-l参数要放在源文件后面否则链接器扫描库的时候源文件的目标文件还没生成符号解析不到。这个坑极其隐蔽报错信息又晦涩我见过好几个同事在这里卡了一个晚上最后发现是-l的位置错了。如果你用的是CMake工具链则只要确保find_package和target_link_libraries配置正确即可。特别注意redis-plus-plus的CMake包名是redis不是redis-plus-plus写错一个字直接找不到包。这个命名是历史遗留记下来就好。5.4 大Key治理与慢查询排查Redis List最容易出现的性能杀手就是大Key。一个List里塞了几百万条未消费的任务LRANGE想要翻看中间的数据时服务端会花很长时间构建响应CPU飚高影响同一实例上的其他业务。治理方案有几板斧第一业务设计时就给List加长度上限。消费者处理不过来的时候生产者继续RPUSH只会让列表无限膨胀。生产端可以记录当前队列长度超过阈值直接丢弃任务或走降级逻辑。实现上就是在RPUSH之前LLEN判断一下或者RPUSH之后LTRIM掉超出部分。第二定期拆Key。把一个大List拆成多个分片Key比如task:queue:0到task:queue:9生产者和消费者都按hash规则选择分片。这样单个Key的数据量可控BRPOP多Key能力在这里就有用武之地。第三用MONITOR和SLOWLOG配合定位问题。线上Redis实例开启CONFIG SET slowlog-log-slower-than 10000记录执行超过10ms的命令再用SLOWLOG GET N查看。你会发现大部分慢命令都是LRANGE返回超大结果集或者一大堆阻塞命令在排队。我有个习惯每次上线涉及Redis List的代码都会在测试环境用100万条数据压一遍确认LRANGE在预期范围内大Key问题提前暴露而不是等线上告警了再查。压力测试脚本用hiredis批量pipeline写入几分钟就能造出几百万条数据完全模拟得了线上真实形态。最后再分享一点个人体会做了这么多Redis List的实践我最大的一个体会是List是一个看起来简单、用起来顺手、但深入之后到处是细节的数据结构。它不像ZSet那样自带排序语义让你一眼知道能干什么也不像Stream那样功能强大到可以直接对标消息中间件。List的精髓就在“双端操作O(1)”这八个字里一旦你把所有场景抽象成“从左边进、从右边出”这个模型它的用武之地会比你想象的多得多。我现在的习惯是新项目里遇到类似“通知列表”“操作日志”“简易任务队列”这类需求先想到Redis List但每一次都会追问自己三个问题数据丢失能不能容忍需不需要消费组列表会不会无限增长三个问题回答清楚了List到底合不合适也就一目了然了。如果哪天发现List满足不了我会再看看Stream和Kafka选型的事一步到位比后期重构舒服太多。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →