做后端这几年,凡是遇到“最新列表”“待处理队列”“消息排队”这类需求,我第一反应都是Redis的List。在Redis里,List就是一个按插入顺序排列的字符串链表,端点插入、弹出都是O(1),配合LRANGE做分页也很顺手,再加阻塞版本命令还能当轻量队列用,算是日常开发里出现频率最高的数据结构之一。这篇文章就把我在项目里存取List的实践、踩过的坑和底层逻辑一起捋一遍,从命令语义到Java代码都有,正在用Redis做列表缓存或队列的开发者参考起来应该能少走不少弯路。
1. 先从底层看懂Redis List的存储设计
很多人用List用了很久,却说不清它底层到底是什么结构。这不影响日常写代码,但一旦遇到内存暴涨、性能抖动、版本升级前后表现不一致,你就得往底层看了。我习惯把底层机制先讲透,因为上层所有“为什么这么用”的问题,答案基本都在这里。
1.1 双向链表:早期实现的两难
Redis的List最早就是按双向链表实现的。每个节点除了保存数据之外,还需要记录前驱指针和后继指针。这个设计的优点很明确:只要拿到头节点或尾节点,插入和删除都是常数时间,不用像数组那样搬移数据。所以LPUSH、RPUSH、LPOP、RPOP这些命令天然适合用链表实现。
但缺点也肉眼可见:内存消耗偏高。64位系统上一个指针就占8字节,每个节点光前驱后继两个指针就是16字节,再加上节点对象本身的分配开销,以及每个节点单独malloc产生的外部碎片,实际花在“元数据”上的内存可能比数据本身还多。早年遇到过一种尴尬情况:往List里塞几百万个只有几个字节的小字符串,内存先撑不住了,数据量其实没多大。
为了解决这个问题,Redis在List元素不多且都是小整数或短字符串时,会换用压缩列表ziplist,它是一段连续内存,把多个元素紧凑地排在一起,极端情况下内存能省好几个量级。但压缩列表也有自己的命门:一旦中间插入或删除元素,后续元素的长度信息可能连锁更新,最坏场景下性能会出现明显毛刺。这才有了下一节要说的quicklist。
1.2 quicklist:空间与性能的折中
Redis 3.2版本引入了quicklist,本质上它是“双向链表 + 压缩列表”的混合体。quicklist本身还是链表,但每个链表节点内部装的不是单个元素,而是一个ziplist。你可以把它想象成一列火车,每节车厢里装了一堆货物,而不是一节车厢只放一件货。
这样做带来的好处非常实际。第一,指针数量大幅减少,原来每个元素都要维护前后指针,现在只需要每个“车厢”维护一组指针即可;第二,插入删除时,大部分操作被限制在某个ziplist内部,不需要频繁分配和释放链表节点;第三,quicklist提供了两个关键配置,让开发者可以自己做取舍:list-max-ziplist-size控制单个节点内最多存多少元素或多大字节数,list-compress-depth控制链表两端有多少个节点不压缩、中间的节点用LZF算法压缩。
我在实测中一般保持默认配置,但如果某个List会长期增长到很长,会把list-compress-depth设成2,只保留头部和尾部各两个节点不压缩,中间全部压缩,内存省得非常明显。需要注意的是,压缩和解压都有CPU成本,如果你的读取模式是随机访问中间段,压得太狠反而适得其反。
1.3 listpack:版本升级带来的潜变
Redis 7.0之后,内部实现逐渐从ziplist转向listpack。listpack的出发点和ziplist类似,也是紧凑连续内存,但它改变了节点长度信息的编码方式,从源头上规避了ziplist最让人头疼的级联更新问题。对业务开发者来说,这个变化完全透明,你的命令、API、数据操作方式都不需要改。
但理解这个演化对你有实际帮助。比如有人问我“为什么Redis从6升级到7之后,同样一批List数据内存少了?”答案往往就在编码结构的变化上。再比如排查问题的时候,用OBJECT ENCODING命令查看某个List键的底层编码,早点版本看到的是quicklist,新版本可能是listpack或quicklist(其中节点用listpack),知道这些才能准确判断数据到底以什么形态存在内存里,而不是对着监控图瞎猜。
2. List命令的完整操作语义
命令是存取的直接工具,但命令之间的细微差别才是坑最多的地方。LPUSH和RPUSH看似只是头尾方向不一样,实际决定了一个列表是LIFO还是FIFO;LRANGE和LPOP看起来都能拿数据,但一个不删除、一个删除,用在分页和队列里完全是两码事。下面按读写删弹四类分开讲。
2.1 写入命令:LPUSH、RPUSH、LSET、LINSERT怎么选
先说最常见的一组。LPUSH从头部写入,RPUSH从尾部写入。标准的生产者消费者队列,我会用RPUSH放入、LPOP取出,这样最早进来的元素最早被消费,符合FIFO语义;反过来LPUSH + RPOP就是栈,后进先出,比如实现“撤销记录”就合适。
LSET和LINSERT属于需要“找到位置”的写入。LSET key index value按索引直接改值,前提是你明确知道这个下标存在,否则会报错;LINSERT是在某个参考值的前面或后面插入。实际业务里我用的频率不高,但有两个典型场景会用到:一个是按位维护一个固定长度的评分列表,另一个是在历史操作列表中间插入一条更正记录。
选择逻辑其实很朴素:只要能通过Push方向解决的,绝不用LSET。因为List本身就不是为随机写设计的,按下标改值需要知道确切位置,而这个位置可能在你读出来之后又变了,并发下容易写错人。
2.2 读取命令:LRANGE做分页的正确姿势
读取最常用的是LRANGE key start stop,很多人第一次用会被闭区间坑到:LRANGE key 0 10返回的是11条,不是10条。这不是bug,而是start和stop都是包含在内的下标。
做分页时我会这样换算:第一页就是LRANGE key 0 9,第二页是10 19。要获取列表全部数据,用0 -1,负数是倒数第几个,-1就是最后一个,所以0 -1代表从头到尾。这个命令的时间复杂度是O(N),但N是返回的元素个数而不一定是整个列表长度,数据量可控的时候性能没问题。
还有一个阅读类命令LINDEX key index,按下标取单个元素,时间复杂度O(N),索引越界返回nil。它适合“根据上次游标拿下一个元素”的滑动场景,比如用户从头开始逐条刷列表时,可以用LINDEX记住当前下标。但频繁随机访问时,LINDEX性能不如LRANGE一次取一批,尽量别在循环里调用。
2.3 删除与裁剪:LREM、LTRIM的细节
删除命令LREM key count value,要注意它删的是“值等于value的元素”,而不是按下标删。count参数的正负控制方向:正数从头删,负数从尾删,0表示删掉所有匹配项。最容易踩的坑是:你想删“用户发布的第一条动态对应的那一个元素”,但这个值在列表里出现了多次,LREM会一次删掉多份。保证value全局唯一,或者干脆按index先LINDEX确认地址,再用LREM配合唯一值删,才会准。
LTRIM key start stop只保留区间内的元素,其余全部删除。它的价值被很多人低估了。比如只想要最近100条动态,执行LPUSH之后立刻跟一条LTRIM key 0 99,旧数据自动裁掉,List永远不会无限膨胀。这样既省内存,又省后续清理的麻烦。LTRIM虽然是O(N),但N是删除的元素数,通常可控。
2.4 阻塞弹出:BLPOP和BRPOP的正确用法
BLPOP是LPOP的阻塞版本,BRPOP是RPOP的阻塞版本。区别在于:如果列表为空,普通POP立即返回null,而阻塞版本会一直等待,直到有新的元素写入,或者达到客户端设定的超时时间。超时设为0表示无限期等待。
阻塞命令主要用来实现消费者模式的队列。试想一下,如果消费端用LPOP死循环轮询空列表,Redis单线程实例会被一堆无意义的空查询打死,CPU空转。改用BLPOP之后,消费者真正“挂起等消息”,队列有数据时立刻被唤醒,空转问题直接消失。但要注意两点:BLPOP设置了超时后,超时会返回null,代码必须处理null分支,不能默认一定有结果;其次,BLPOP在等待期间如果客户端断连,正在等待的连接会被强制断开,应用侧要做好重连。
另外一个容易被忽略的命令是BRPOPLPUSH及其替代者LMOVE,它们把“从A列表弹出元素”和“把元素压入B列表”两个动作原子化。我拿它做消息确认:消费者BRPOPLPUSH从队列弹出消息后,先压入“处理中”列表,处理成功后再LREM删掉,一旦应用崩溃,恢复后还能从“处理中”列表找回未完成的任务,这种可靠队列的雏形在生产里很实用。
3. 几个高频场景的落地参考
命令聊清楚了,接下来就是组合使用。List适用的场景比很多人想象中广,但也比很多人以为的“万能缓存”要窄。它擅长的是有序、追加式、带弹出的数据流,不是随机修改型的存储。这里我挑三个自己做过的高频场景,配合伪代码一起说。
3.1 最新动态列表
社区或资讯类App都会有“最近动态”需求,最朴素的方案是查数据库ORDER BY id DESC LIMIT 100,但一旦接口被频繁刷新,数据库压力很快上来。我在项目里用Redis List做了缓存:每当有新内容产生,就执行LPUSH feed:user:123 itemId,然后立刻LTRIM feed:user:123 0 99,把列表永远锁在100条以内。读接口直接LRANGE feed:user:123 0 99,毫秒级返回。
注意这里我只缓存内容ID,不缓存完整详情。为什么?如果直接把整个内容对象塞进List,几个字节的ID变成一个几KB的JSON,内存消耗直接爆炸,而且内容一旦编辑,缓存里还是旧值,一致性问题全来了。缓存ID、详情回源数据库按需取,这是用List做列表缓存时最重要的原则。
这套方案不适合需要翻很久以前数据的场景,因为LTRIM已经把历史数据丢掉了。真有“翻页超过100条”的需求,我会建议走数据库或ZSET,不要硬在List上做无限分页。List的定位是“最近N条”,不是“全量历史”。
3.2 轻量任务队列
如果你只是“触发一个动作,异步去执行”,完全不需要引入消息中间件,Redis List足够。生产者用RPUSH queue:tasks taskId,消费者多个实例用BRPOP queue:tasks超时0轮流取。BRPOP天然做了多消费者竞争,哪个实例先抢到就归谁处理,任务在多个消费者之间自动负载均衡。
但这里有个非常典型的坑:如果消费者处理失败,直接把消息丢了,业务上可能接受不了。我的习惯是把失败任务重新RPUSH回队列尾部,同时设置最大重试次数。还要防止“坏消息循环重试”打满队列,可以配合一个独立的“重试次数计数Key”,超过阈值就进死信列表,人工或者定时任务单独处理。顺带一提,BRPOP的空等连接在Redis里也占用连接数,消费端实例多时要留足最大连接,否则会阻塞其他Redis操作。
3.3 评论分页与列表缓存的边界
有人问我评论列表能不能用List缓存。我的答案是:能用,但有前提。评论属于“会删除、会插入中间”的数据,List天然不适合频繁删除中间元素。LREM按值删除,如果两个评论ID恰好相同(不可能,但假设value是用户ID就会),就会误删;按索引删除又先要遍历,成本高。
所以简单评论列表我还是首选ZSET,按评论时间戳做score,删除成员靠ZREM,天然避免重复和错删。List适合的是“只追加、不删除、不修改中间”的流水型数据,比如操作日志、活动流水、消息通知的未读列表。选错结构不会立刻报错,但会在数据膨胀之后给你上眼药。
3.4 场景选型对照速查
| 场景 | 推荐结构 | 原因 |
|---|---|---|
| 最近N条动态 | List + LTRIM | 天然倒序、控制长度容易 |
| 轻量任务队列 | List + BRPOP | FIFO清晰、多消费者竞争天然支持 |
| 评论/帖子列表 | ZSET | 需要按删除操作、按score准确移除 |
| 只追加流水 | List | 无删除需求、写读简单 |
| 栈式回退 | List + LPUSH + LPOP | LIFO语义正好匹配 |
| 可靠消息队列 | List + BRPOPLPUSH/LMOVE | 原子转移、支持确认与恢复 |
4. Java实战:Redis存取List的完整代码
命令再熟,落到代码里还是有一堆细节。我在Java生态里常用方式有两种:一种是直接用Jedis操作原生命令,适合非Spring项目或者想要细粒度控制的场景;另一种是Spring Data Redis,适合Spring Boot项目。两者的坑不太一样,尤其是序列化问题,能绊倒一大半人。
4.1 环境与版本选择
代码示例基于Redis 7.x、Jedis 4.x、Spring Boot 2.7.x。Redis版本不同,部分命令细节会变,但List的基础命令从2.0到现在基本没变过,放心使用。Jedis 4.x之后包名路径有调整,注意别引成了旧版本。Spring Boot的spring-boot-starter-data-redis会自动带上Lettuce连接工厂,直接用自动配置即可。
生产环境建议至少Redis 6.x以上,不仅性能更好,命令支持的完整性也更高。如果是云厂商提供的Redis,注意版本是否被强制砍掉过某些命令,比如有的云Redis为了安全会禁用KEYS,这跟List无关但排查问题时要知道。
4.2 用Jedis手写List存取
Jedis的API相当于把Redis命令直接翻译成了Java方法,写起来非常直观:
import redis.clients.jedis.Jedis; import java.util.List; public class RedisListDemo { public static void main(String[] args) { // 连接Redis,生产环境建议使用连接池 Jedis jedis = new Jedis("127.0.0.1", 6379); jedis.auth("yourPassword"); // 有密码才需要 String key = "queue:task"; // 1. 从尾部批量写入 jedis.rpush(key, "task-1", "task-2", "task-3"); // 2. 从头部写入 jedis.lpush(key, "task-0"); // 3. 读取全部 List<String> all = jedis.lrange(key, 0, -1); System.out.println("全部任务: " + all); // 4. 从头部弹出 String first = jedis.lpop(key); System.out.println("处理任务: " + first); // 5. 队列为空时代替轮询的阻塞弹出,5秒超时 List<String> blocked = jedis.blpop(5, key); if (blocked != null) { System.out.println("阻塞拿到任务: " + blocked); } else { System.out.println("等待超时,队列仍然为空"); } jedis.close(); } }这段代码把前面讲的命令基本都覆盖了,动手跑一遍就能感受到List的操作手感。需要注意:生产环境不要每次new Jedis,连接建立和销毁开销很大,要用JedisPool;blpop(5, key)返回的是一个List,第一个元素是key名,第二个才是value,解析时别取错位置。
4.3 Spring Data Redis里的正确姿势
Spring Boot项目我更推荐用StringRedisTemplate,而不是直接用RedisTemplate。原因下一节讲,先看代码:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import java.util.List; @Service public class TaskQueueService { @Autowired private StringRedisTemplate stringRedisTemplate; public void pushTask(String taskId) { stringRedisTemplate.opsForList() .rightPush("queue:task", taskId); } public void pushTasks(String... taskIds) { stringRedisTemplate.opsForList() .rightPushAll("queue:task", taskIds); } public List<String> getTasks(long start, long end) { return stringRedisTemplate.opsForList() .range("queue:task", start, end); } public String popTask() { return stringRedisTemplate.opsForList() .leftPop("queue:task"); } }StringRedisTemplate的操作方法和Redis命令一一对应:opsForList().rightPush是RPUSH,leftPush是LPUSH,range是LRANGE,leftPop和rightPop对应LPOP和RPOP,leftPop(K key, long timeout, TimeUnit unit)就是BLPOP。API命名非常直白,基本不会记混。
4.4 序列化踩坑记录
我见过太多人初次用RedisTemplate存List,存进去之后在redis-cli里看到一串形如\xAC\xED\x00\x05t...的乱码,这就是JDK序列化的结果。RedisTemplate默认使用JdkSerializationRedisSerializer,它在序列化对象时会写很多Java类型信息,存到Redis里占空间而且不可读。更麻烦的是,如果存入的对象没有实现Serializable,运行时会直接抛异常。
所以除非你明确要存对象且能接受额外开销,否则一律建议StringRedisTemplate,所有元素以字符串形式存储。入参是字符串ID,取出来是字符串ID,业务侧再根据自己的序列化需求处理。这样做的好处有三个:数据在Redis里可读可排查,跨语言跨平台兼容,迁移数据也不会有解析负担。
如果你确实要在RedisTemplate里存JSON对象,可以自定义一个GenericJackson2JsonRedisSerializer来替换默认序列化器。但我的经验是:List场景下能用字符串解决的就别折腾对象序列化,架构简单本身就是一种可靠。
5. 性能优化与经验教训
List性能虽好,但Redis是单线程模型,一个命令执行时间过长就会阻塞整个实例。所以对List的使用必须有一个意识:列表长度和元素大小是性能的两个隐形天花板。下面这些优化经验,每一个都是从我踩过的坑里总结出来的。
5.1 控制List长度是第一条铁律
List可以无限增长,但这不代表你该让它无限增长。当列表达到几十万上百万个元素时,问题开始集中爆发:LRANGE大范围读取耗时显著上升,LTRIM一次裁剪掉大量元素会让清理操作卡顿,列表占用的内存让maxmemory压力剧增,甚至触发内存淘汰策略,把不该淘汰的其他键淘汰掉。
我的做法是给每个List设定业务上限,并在每次写入后主动LTRIM。比如动态列表只留100条,消息通知只留500条,操作日志最多留1000条。从设计上就不给“长列表”生长的空间,避免后续救火。内存不是无限的,无界列表是生产事故的种子。
如果业务确实需要保留全量历史,那就别赖在Redis里,Redis只放热数据,历史数据定期异步迁移到数据库或对象存储。Redis List不是一个适合长期持久的存储系统,它更适合做短生命周期的高性能数据结构。
5.2 大Value是隐形杀手
有时候列表长度不大,但每个元素本身大得离谱。曾经排查过一个案例:业务方把整个订单对象序列化成JSON塞进List,单个元素20多KB,一晚上写入十万条,Redis内存直接涨了几个GB。更隐蔽的是,这么大的value不仅占内存,序列化和反序列化都要消耗CPU时间,网络传输也变慢,整个实例都被拖累。
所以在List里存储的元素,我强烈建议只放ID或短标识。比如feed流就存动态ID,任务队列就存任务ID,真正的业务详情放到另一个缓存Key或数据库里。如果实在要存对象,至少做压缩或者拆分成多个字段再组装,绝对不能把几KB的大JSON当List元素。
还需要关注单次写入的数量。RPUSH一次传几百上千个元素,虽然命令只是一次,但内部要连续复制这么多数据,同样会阻塞Redis。批量写入时建议控制每一批的大小,通常几百个以内为佳,避免单命令执行时间过长。
5.3 命令选择背后的性能思维
面试里常问List的时间复杂度,实际工作中更要心里有数。LPUSH、RPUSH、LPOP、RPOP、BLPOP这些端点操作是O(1),用起来毫无压力;LRANGE是O(N),N是返回条数,小范围没问题,别一把取全量;LINDEX是O(N),因为它要从头遍历,放到循环里就等于O(N^2),必须警惕;LREM同样是O(N),它要先遍历找匹配值,再删除,列表越长越慢。
我写代码前会先估算这个命令在高峰期会被调用多少次、每次会影响多大范围。比如一个每秒被调用上千次的接口,如果内部每次都LRANGE取出1000条再过滤,Redis实例基本就被这个命令喂饱了。换成只取前20条,或者把过滤逻辑放到业务侧合并处理,压力立刻降下来。
5.4 监控与超卖保护
List的监控不能只靠人看。我会在监控系统里对三个指标单独告警:每个List的LLEN长度、Redis实例的used_memory和instantaneous_ops_per_sec、慢查询日志SLOWLOG GET。一旦发现某个Key长度异常膨胀,马上定位是哪条业务逻辑在写入,检查是否存在死循环或消费停滞。
另一个容易被忽略的点是Redis的maxmemory策略。如果实例启用了allkeys-lru或类似策略,内存紧张时Redis可能会淘汰掉你的List键,缓存直接消失。想让List键即时消失都行,但你要知道这个行为,并让业务侧在读取时做好空值回源,别让缓存穿透打垮数据库。
6. 常见问题与排查心得
这部分我决定用问题实录的形式来写。每一个都是我在真实项目里碰到或帮别人排查过的,未必都发生在你身上,但一旦遇到,照着这个思路能省不少时间。
6.1 数据量一大,Redis就卡顿
现象:Redis命令平均耗时从1毫秒涨到几十毫秒,CPU跟着飙高,业务接口变慢。第一反应先执行SLOWLOG GET 10,看慢命令到底是什么。我排查结果多半是某个List被LRANGE取全量,比如前端为了展示,执行了一次LRANGE key 0 -1,而列表已经积累到几十万条。
解决分三步:限制接口读取范围,改为分页;对List执行LTRIM裁掉历史数据;如果历史数据还有用,异步迁移到数据库。这里要特别提醒:慢命令的产生往往不是Redis故障,而是客户端用错了姿势,先看慢日志再动Redis配置。
6.2 LREM误删了多条重复数据
现象:想删某用户的一条记录,执行LREM key 0 userId,结果把该用户所有记录全删了。这个问题的根源在于List允许重复值,而LREM是按值匹配的。解决办法是:存入时给每个元素加上唯一标识,比如userId + 时间戳,删除时用这个唯一值,保证不会误伤;或者改用ZSET,用ZREM member精准删除。
要是已经误删,只能在备份或数据库日志里恢复。这也提示我们,凡是对List做删除操作前,先想清楚这个value是否可能重复,重复就不能一次删。
6.3 BLPOP一直返回超时
现象:消费端用BLPOP等待任务,经常等到超时,但队列里明明有数据。这通常是因为多个消费者同时对同一个Key执行BLPOP,但Redis的阻塞命令唤醒逻辑是:队列有数据时,所有等待的消费者会被唤醒并竞争,只有一个能抢到;抢不到的会重新进入阻塞。如果消费者线程很多,或者其中一个超时时间设太短,就会出现“被唤醒但没抢到,然后超时”的现象。
解决思路是:给BLPOP设置一个合理超时时间,比如5秒或10秒,别设0无限等但是又手动中断;其次保证每个消费者实例在极长空闲时能及时重连,避免连接假死。还要注意,如果队列里消息积压严重但消费者总报超时,多半是消费速度跟不上生产速度,这时候把头加消费者数量不一定有用,要检查消费者的处理逻辑有没有阻塞点。
6.4 多实例同时消费,数据倾斜到同一Key
现象:多个应用实例同时消费同一个List,但总感觉某个实例闲、某个实例忙。其实这不是Redis List的毛病,而是消费策略的问题。BRPOP本身会公平竞争,但如果你在消费端对消息做了按业务ID取模、丢弃或延迟重试,某些业务ID就会被特定实例垄断。
数据倾斜更常见的场景是“热点Key”,所有写入都打到同一个Key,单实例的写入性能就到顶了。List不像Hash或Set那么容易拆Key,但你可以按业务维度拆分成多个Key,比如queue:task:1、queue:task:2,生产者按业务ID或随机数落桶,消费者订阅多个Key。拆Key虽然增加了代码复杂度,但在高并发写入场景下是绕不开的方案。
6.5 快速排查速查表
| 现象 | 大概率原因 | 优先排查手段 |
|---|---|---|
| 命令变慢、CPU飙高 | 长列表被LRANGE全量读取 | SLOWLOG GET |
| 内存暴涨 | List元素过大或列表无界增长 | LLEN、MEMORY USAGE |
| 数据在客户端显示乱码 | RedisTemplate JDK序列化 | TYPE + OBJECT ENCODING |
| 消费超时但队列有数据 | 消费者线程竞争/超时设置不当 | 观察实例数和BLPOP超时参数 |
| List键突然消失 | maxmemory淘汰策略触发 | 检查maxmemory-policy |
| 写入后查不到数据 | 写到了不同key或选错了库 | 确认连接DB序号和Key名 |
7. 一点私货:我踩过几次坑之后的习惯
List这个数据结构太常见了,常见到很多人不把它当回事,但恰恰是这种“简单结构”,在实际生产里最容易因为使用姿势不当而出事。我自己刚开始给系统设计队列的时候,也干过把完整对象塞进List、结果Redis内存报警的事。后来养成了一个习惯:任何List键,都先把“元素最大长度”和“列表最大条数”两个值写在代码注释里,写操作时自动带上约束。
另外不建议把List当作多数据类型混存的垃圾桶。同一个List键里,一会儿存字符串ID、一会儿存JSON对象,业务维护起来非常痛苦,排错也难。一个Key只存一种含义、一种格式的数据,出了任何问题都好定位。
还有一个小技巧:在调试阶段,多用OBJECT ENCODING和MEMORY USAGE这两个命令看数据实际存储状态,不要只盯着业务返回结果。它们能帮你快速判断到底是编码问题、序列化问题还是单纯数据量太大。工具用得越顺手,排查效率就越高。
如果你刚开始接触Redis的List,我建议不要只停留在跑通命令,而是把每个命令的复杂度、在每个场景中的语义都过一遍,这样在你真正遇到“这个问题还有很多别的方式能实现,但为什么偏偏选List”的时刻,你的选择才会足够笃定。