首页 / 资讯中心 / 文章详情

Redis构建支持程序:键值建模与四大结构实战

Redis构建支持程序:键值建模与四大结构实战 ★ FEATURED ARTICLE
1. 先搞清楚这套实验到底在练什么第一次接触“用 Redis 构建支持程序”这类实验题的人很容易陷入一个误区把 Redis 当成一个“更快的数据库”然后照着 MySQL 那套思路去建表、写 SQL、做关联查询。我当年第一次打开这类实验的作业说明时也是这么想的结果在第一个关卡就卡了快四十分钟——不是因为命令不会写而是因为脑子里没有建立起“键值存储”这套思维模型。所以我先把结论摆在前面这套实验的核心不是让你背命令而是让你完成一次数据建模思路的切换。你在关系型数据库里习惯的“表—行—列”在这里要全部翻译成“键—值—结构”。这个切换一旦完成后面的字符串缓存、哈希存对象、列表做队列、集合做去重全部都是水到渠成的。Redis 是一个内存型键值存储系统它把数据放在内存里所以读写速度是微秒级别的同时它提供了字符串、哈希、列表、集合、有序集合这几种数据结构每种结构都对应一批原子操作命令。它解决的问题很具体当你的程序需要频繁读取某些热点数据、需要在多个进程之间共享状态、需要做简单的计数排行或者任务排队时直接压到关系型数据库上会很吃力而 Redis 正好补上这一层。这套头歌实验就是围绕这些典型场景设计的从最简单的字符串读写一路做到用列表模拟队列、用哈希存对象、用集合做去重统计。适合谁来参考这份总结如果你是刚学完 Redis 基础命令、但一看到“请实现某功能”就不知道从哪个命令下手的人这篇内容对你最有用如果你已经在项目里用过 Redis 做缓存但只是停留在get/set层面想搞清楚哈希、列表、集合分别在什么场景下该用也能从这里拿到东西哪怕你完全没碰过 Redis只要能看懂一门编程语言的基本语法跟着走一遍也能把这套实验啃下来。下面我会按照“整体设计思路—核心结构拆解—具体实现步骤—踩坑排查”这个顺序展开中间穿插我在实际动手时遇到的问题和总结出来的技巧。所有命令和代码都是我实际跑通过的参数含义也会解释清楚方便你直接对着改。2. 整体设计与思路拆解2.1 为什么这套实验要用“支持程序”这个角度切入这套实验的题目里有个很关键的词——“支持程序”。这个词不是随便起的它点出了 Redis 在实际系统里的定位它不是主角是配角。你的业务逻辑、你的主数据存储还是由应用代码和关系型数据库承担Redis 站在它们旁边负责加速、负责解耦、负责扛住那些高频但轻量的操作。理解这个定位非常重要因为它直接决定了你在这套实验里该怎么设计数据结构。举个例子如果 Redis 是主存储那你就要考虑数据持久化、要考虑数据完整性、要考虑主从复制但如果 Redis 只是“支持程序”的辅助层那你设计的重点就变成了“怎么让读写更快”“怎么让多个模块之间通过一个共享的键来通信”“怎么用最少的命令完成一次操作”。我在做实验的时候就吃过这个亏。有一道题要求统计某个操作的执行次数我第一反应是在 Redis 里存一个完整的数据结构把每次操作的时间、发起者、结果都记下来然后再去算总数。结果命令写了一长串还没通过测试用例。后来回头看题意才明白题目要的就是一个计数器——INCR一条命令就够了。这就是“支持程序”思维用最小、最直接的数据结构去解决一个具体的小问题而不是在 Redis 里重建一个业务系统。所以这套实验的设计逻辑可以这么理解它把真实项目里 Redis 最常承担的几类职责拆成了一个个独立的小任务每个任务对应一种数据结构和一组命令。你把这几个任务做通了就等于把 Redis 在项目里的常见用法过了一遍。2.2 四种数据结构的选择逻辑实验里主要涉及字符串、哈希、列表、集合这四种结构有序集合在进阶题里会碰到。很多人学完之后还是不知道什么时候该用哪个我这里给一个我自己总结的判断顺序实测很好用数据结构一句话判断典型场景实验中的对应任务字符串 String存单个值或者做计数缓存 JSON、存 token、计数器、分布式锁缓存热点数据、统计访问次数哈希 Hash存一个对象的多个字段用户信息、商品属性、配置项集合存储结构化对象并按字段读取列表 List有顺序、要从两头进出消息队列、最新列表、操作日志模拟任务队列、取最新 N 条集合 Set要去重、要做交并差标签、关注关系、去重统计统计独立访客、求共同好友这个判断顺序的逻辑是先问“我要存的是一个值还是一组有名字的字段”如果是单个值就选字符串如果是一组字段聚在一起选哈希然后问“这些数据有没有顺序要求”有顺序且从两端操作就选列表最后问“要不要去重、要不要做集合运算”要就选集合。这里特别说一下哈希和字符串的区别这是新手最容易混淆的地方。你要存一个用户的信息用字符串的话就是把整个对象序列化成 JSON 存进去读出来再反序列化优点是简单缺点是如果你只想改其中一个字段比如只更新年龄你也得把整个对象读出来、改完、再整体写回去。用哈希的话你可以直接HSET user:1 age 25只动那一个字段其他字段不受影响。所以判断标准很简单如果这个对象会被频繁地按字段局部更新用哈希如果只是整体读整体写字符串更省事。2.3 键名设计的规范与理由这一点很多人忽略但在我看来是这套实验里最值得养成的习惯键名要设计不要随手起。实验里如果键名起得乱七八糟比如一会儿user1、一会儿u_1、一会儿1user你在调试的时候根本对不上号。我在实际项目里用的是冒号分隔的层级命名法比如user:1001:profile、order:2024:queue、cache:article:88。这种写法的好处有三个一是可读性强一眼能看出这个键属于哪个业务模块二是天然形成命名空间做批量扫描或者清理某个模块的缓存时可以用模式匹配三是避免不同模块之间的键名冲突。提示键名不要太长Redis 的键本身也占内存。user:1001:profile这种长度是合理的别写成application:module:user:profile:for:user:id:1001这种。一般控制在 60 字节以内够表达清楚就行。另外还有一个实验里容易忽略的点给键设置过期时间是必须养成的习惯。缓存类的键如果不设过期内存迟早被撑满而一旦内存满了Redis 的淘汰策略如果配置不当可能会把不该删的键删掉。实验里可能测试用例不会检查你有没有设过期时间但这个习惯在真实项目里能救命。常用的两个命令是EXPIRE key seconds和SET key value EX seconds后者一步到位我更推荐。3. 核心结构拆解与命令实操要点3.1 字符串不只是缓存更是原子计数器字符串是 Redis 最基础的结构但它的能力远不止SET和GET。这套实验里跟字符串相关的任务核心其实落在两个能力上带过期的缓存和原子自增计数。先说缓存。实验里常见的题型是“把某个查询结果缓存起来下次直接从 Redis 读”。实现起来就是两步SET cache:article:88 {\id\:88,\title\:\demo\,\views\:120} EX 300 GET cache:article:88EX 300表示 300 秒后自动删除。这里的 300 不是随便定的它取决于你的数据更新频率和能容忍的脏数据时长。如果这个文章内容一个小时才更新一次那把过期时间设成 600 秒甚至更长都没问题如果它是实时性很强的数据那就得缩短或者干脆在数据更新时主动DEL掉这个键。这就是所谓的“缓存失效策略”实验里不会细讲但实际项目里必须想清楚。再说计数。这里有个细节很多人不知道INCR是原子操作意思是即使有一百个客户端同时对一个键执行INCR结果也是准确的不会出现丢计数的情况。这一点在关系型数据库里是要靠事务或者行锁才能保证的而在 Redis 里一条命令就搞定了。所以实验里凡是遇到“统计次数”“记录访问量”这类题第一反应就该是INCR。INCR stats:page:view INCRBY stats:page:view 10 DECR stats:page:viewINCRBY可以指定步长DECR是自减。还有一个容易踩的坑INCR只能对能解析成整数的字符串用。如果你先SET counter abc再INCR counter会直接报错ERR value is not an integer or out of range。这个错我在实验里见过好几次根源就是前面某一步把非数字写进了同一个键。注意INCR的返回值是自增之后的新值不是旧值。很多人在写“先自增再判断是否超过阈值”的逻辑时会搞错这一点。3.2 哈希把对象拆开存按字段取哈希是我认为这套实验里最能体现“键值思维”的结构。它解决的就是“怎么在 Redis 里存一个对象”这个问题。基本命令是这样的HSET user:1001 name 张三 age 25 city 杭州 HGET user:1001 name HGETALL user:1001 HMGET user:1001 name age HDEL user:1001 city HINCRBY user:1001 age 1HSET现在支持一次设置多个字段这个语法比老版本的HMSET更清爽HMSET已经被标记为废弃了实验环境如果版本较新直接写HSET就好。HGETALL会返回所有字段和值格式是交替出现的所以在代码里取出来之后通常要转成字典或者映射结构。这里有个性能上的细节值得展开说HGETALL在字段很多的时候是有风险的。假设一个哈希里有十万个字段一次HGETALL会把整个哈希拉过来既占网络带宽又占客户端内存。这种情况下应该用HSCAN做游标式遍历或者干脆只取需要的字段用HMGET。实验里数据量小HGETALL随便用但要知道它在真实场景下的边界在哪。还有一个命令组合特别实用HSETNX只在字段不存在时才设置。这个在“初始化默认配置”的场景里很好用可以避免覆盖已有的值。我在做实验时遇到过一个题型要求把多个对象存起来然后按某个字段筛选。这时候如果每个对象是一个哈希键名是user:1001、user:1002这样你要遍历所有用户就得先知道有哪些键。这就引出了一个重要问题哈希本身不支持按字段值查询它只支持按键取整个哈希或者按字段名取值。所以如果你的需求里有“找出所有年龄大于 25 的用户”这种Redis 的哈希是做不到的除非你另外维护一个索引集合。这一点一定要想清楚别指望 Redis 承担关系型数据库的查询能力。3.3 列表队列和栈只差一个命令列表这个结构实验里最常考的就是“用它模拟队列”。理解列表的关键是搞清楚它的两端操作LPUSH queue:task task1 RPUSH queue:task task2 LPOP queue:task RPOP queue:task LRANGE queue:task 0 -1 LLEN queue:taskL是左R是右。LPUSH从左边塞RPOP从右边取这样就是先进先出也就是队列如果LPUSH进、LPOP出那就是先进后出也就是栈。所以队列和栈在 Redis 里根本不是两种数据结构只是命令组合不同这一点想通了会觉得很妙。实验里有个高频题型取最新 N 条记录。标准做法是LPUSH加LTRIMLPUSH feed:latest item_new LTRIM feed:latest 0 9 LRANGE feed:latest 0 -1LTRIM key 0 9的意思是只保留索引 0 到 9 这十个元素其余的裁掉。配合LPUSH每次往头部插入就实现了一个固定长度的最新列表。这个模式在“最新动态”“最近搜索”这类功能里用得非常多。注意列表的索引是从 0 开始的-1表示最后一个元素-2表示倒数第二个这个负索引的写法在LRANGE里很好用但别在LTRIM里写错范围否则会把数据裁没了。还有一个实验里的坑LPOP在列表为空的时候返回的是 nil如果你在代码里直接对这个返回值做类型转换或者字符串拼接很可能会抛异常。所以消费队列的时候一定要先判断返回值是否为空。另外LPOP支持带 count 参数的写法LPOP key 2一次弹出多个这个在批量消费时能减少往返次数。3.4 集合去重和交并差是它的主场集合的特性很纯粹元素无序、不重复、支持集合运算。实验里跟集合相关的题基本都围绕这三点。SADD tag:article:88 redis cache backend SMEMBERS tag:article:88 SISMEMBER tag:article:88 redis SCARD tag:article:88 SREM tag:article:88 cacheSADD的返回值很有意思它返回的是实际新添加的元素个数如果元素已经存在返回 0。这个特性可以直接用来做“判断是否首次出现”的逻辑比先查再插要省一次往返。集合运算才是它真正的价值所在SINTER tag:article:88 tag:article:99 SUNION tag:article:88 tag:article:99 SDIFF tag:article:88 tag:article:99SINTER求交集可以做“共同关注”“共同标签”SUNION求并集可以做“合并标签”SDIFF求差集可以做“推荐你可能感兴趣但还没关注的”。实验里如果出现“统计两个用户的共同好友”这种题答案就是SINTER。还有一个实战里很常用的场景是独立访客统计。如果用集合就是把每个访客的标识SADD进去最后SCARD拿总数天然去重。但这个做法有个问题访客量大的时候集合会非常大。所以真实项目里更多用 HyperLogLogPFADD/PFCOUNT它以极小的内存代价估算基数误差在 0.81% 左右。实验里如果数据量小用集合完全没问题但如果题目提到“海量数据”“内存敏感”就该想到 HyperLogLog 这个选项。这是我在做进阶题时的一个心得题目不会明说但关键词就是提示。4. 实操过程与关键环节实现4.1 环境与连接的第一步实验环境一般会预装好 Redis 服务你需要做的是确认服务在跑、然后连上去。这部分看起来简单但连接不上是新手最常见的第一个拦路虎。先确认服务状态redis-cli ping正常应该返回PONG。如果返回的是连接被拒绝说明服务没启动需要先启动服务。启动之后如果还是连不上检查端口默认是 6379实验环境有可能改过。进入交互式命令行之后有几点值得说第一redis-cli里是有命令提示和历史的用上下方向键可以翻之前敲过的命令这个在调试时非常省事。第二如果你要在命令行里执行一条命令但不进入交互模式可以直接redis-cli SET foo bar这在写脚本的时候很方便。第三千万不要在实验环境里随手执行FLUSHALL或FLUSHDB。这两个命令会清空所有数据如果实验的测试用例依赖前面步骤写入的数据一执行就全废了只能从头再来。我见过有人为了“清干净重做”直接FLUSHALL结果把前面几关的成果一起清了。正确的做法是用DEL精确删除你这次操作涉及的那几个键。4.2 分步实现从单键缓存到结构化存储我们拿一个典型的综合任务来走一遍假设要求实现“用户信息缓存 访问计数 最近访问记录”。第一步存用户信息。用户是一个对象有姓名、年龄、城市这些字段用哈希HSET user:1001 name 张三 age 25 city 杭州第二步记录这个用户的访问次数。这是一个独立的计数器用字符串的INCRINCR stats:user:visit:1001这里键名的设计有讲究我用stats:user:visit:1001而不是简简单单一个count原因是将来可能有多个用户的计数加上用户 ID 才能区分开而且前缀stats标识了这是统计数据方便后续统一处理。第三步记录最近访问的用户列表保留最近 10 条LPUSH recent:visitors 1001 LTRIM recent:visitors 0 9这三步做完三种结构各司其职哈希存对象字符串做计数列表做有序记录。这就是“支持程序”的典型形态——每个数据结构解决一个小问题组合起来支撑一个完整的功能。如果要读回来HGETALL user:1001 GET stats:user:visit:1001 LRANGE recent:visitors 0 -14.3 用代码调用 Redis 的完整示例实验里光在命令行敲是不够的通常要写代码。不同语言的客户端语法不一样但核心逻辑是一致的。这里用 Python 举例因为它最直观其他语言的思路完全一样。import redis r redis.Redis(hostlocalhost, port6379, db0, decode_responsesTrue) # 1. 写入用户信息哈希 r.hset(user:1001, mapping{ name: 张三, age: 25, city: 杭州 }) # 2. 访问计数字符串原子自增 visit_count r.incr(stats:user:visit:1001) print(f当前访问次数{visit_count}) # 3. 最近访问列表列表 裁剪 r.lpush(recent:visitors, 1001) r.ltrim(recent:visitors, 0, 9) # 4. 读取用户信息哈希 user_info r.hgetall(user:1001) print(f用户信息{user_info}) # 5. 读取最近访问列表 recent r.lrange(recent:visitors, 0, -1) print(f最近访问{recent})这里有几个参数和写法上的细节必须说明decode_responsesTrue这个参数非常关键。默认情况下 Python 的 redis 客户端返回的是 bytes 类型b\xe5\xbc\xa0\xe4\xb8\x89这种你在代码里做字符串比较或者拼接的时候会出各种问题。加上这个参数之后返回的就是普通字符串了。这是我在实验里踩过的坑当时打印出来一堆b...还以为数据写错了其实是编码问题。hset的mapping参数是一次设置多个字段的写法比连续调用多次要快因为它减少了网络往返。如果你的客户端版本比较老不支持mapping就退回到逐个字段设置或者用hmset。ltrim的第二个参数是结束索引0, 9保留前十个。注意这里的索引是包含两端的不是左闭右开。4.4 一个容易忽略的环节数据类型校验与异常处理实验的测试用例往往只验证正常路径但真实项目里你必须处理异常。这一段是我觉得比命令本身更值钱的部分。坑一对非数字执行INCR。前面提过如果键的值不是合法的整数INCR会报错。所以在做计数之前要么保证这个键只被计数操作写过要么在代码里做类型校验try: count r.incr(stats:counter) except redis.ResponseError as e: print(f自增失败原值不是整数{e}) r.set(stats:counter, 1) # 重置坑二对不存在的键做LPOP。返回 None如果后面直接拿去做字符串操作就会崩task r.lpop(queue:task) if task is None: print(队列为空等待新任务) else: process(task)坑三HGETALL返回空字典。键不存在时返回{}不会报错。所以你不能用“有没有报错”来判断键是否存在得用existsif r.exists(user:1001): info r.hgetall(user:1001) else: print(用户不存在)坑四类型错误。Redis 里一个键一旦确定了类型就不能用另一种类型的命令去操作它。比如user:1001是哈希你执行GET user:1001会报WRONGTYPE Operation against a key holding the wrong kind of value。这个错在实验里很常见通常是因为键名起得重复两个不同任务用了同一个键。解决办法就是我前面说的键名带业务前缀彻底避免冲突。4.5 把几个结构串起来的综合实现实验最后往往有一道综合题要求把多种结构组合使用。我给一个我实际写通过的完整例子实现一个简化的“文章热度统计”import redis r redis.Redis(hostlocalhost, port6379, db0, decode_responsesTrue) def view_article(article_id, user_id): # 1. 文章浏览量 1字符串原子自增 r.incr(fstats:article:view:{article_id}) # 2. 记录独立访客集合去重 r.sadd(farticle:visitors:{article_id}, user_id) # 3. 写入最近浏览列表只留最近20条 r.lpush(fuser:recent:{user_id}, article_id) r.ltrim(fuser:recent:{user_id}, 0, 19) # 4. 返回当前统计 total r.get(fstats:article:view:{article_id}) unique r.scard(farticle:visitors:{article_id}) return int(total), unique total, unique view_article(88, user:1001) print(f总浏览 {total}独立访客 {unique})这个例子里INCR解决总量、SADD解决去重、LPUSHLTRIM解决有序截断三种结构各自发挥所长没有任何一个是多余的。这就是我理解的“用 Redis 构建支持程序”的正确姿势不是把所有东西塞进一个大结构而是让每个结构做它最擅长的那一件事。有效性的判断上SCARD的取值范围必然小于等于INCR的结果如果出现独立访客数大于总浏览量那一定是逻辑写错了这个自检关系在做完实验后可以用来验证自己的实现是否正确。5. 常见问题与排查技巧实录5.1 高频报错速查实验过程中遇到的报错八成集中在这几个。我整理成表方便对号入座报错信息根本原因解决办法WRONGTYPE Operation against a key holding the wrong kind of value键已存在且类型不匹配换键名或者先DEL再重建检查键命名是否冲突ERR value is not an integer or out of range对非整数值执行INCR/DECR检查该键的历史写入必要时SET重置ERR wrong number of arguments命令参数个数不对对照文档数一遍参数特别是HSET多字段写法连接被拒绝Connection refused服务未启动或端口不对确认服务状态检查端口配置LPOP返回None后代码报错列表为空加空值判断HGETALL返回{}但以为没写入键不存在或键名拼错用EXISTS先确认再KEYS模糊匹配排查5.2 键名冲突是隐形杀手我把它单独拎出来说因为它的表现很迷惑命令写得完全正确语法也没问题但就是报WRONGTYPE。根源是你前一个任务和当前任务用了同一个键名而前一个任务往里面写的是另一种类型。排查方法很简单用TYPE命令看这个键到底是什么类型TYPE user:1001如果返回的是hash而你以为它是字符串问题就找到了。进一步可以用KEYS加模式匹配看这个命名空间下都有哪些键KEYS user:*注意KEYS在生产环境是禁用命令因为它会遍历所有键数据量大的时候会阻塞服务。实验环境数据少随便用但真实项目里要用SCAN替代。这个区别要知道别把实验里的习惯带到线上。治本的办法还是键名规范不同任务用不同的业务前缀别用test、demo、foo这种无意义的名字也别两个任务都叫mylist。5.3 集合运算结果的验证思路用SINTER、SUNION、SDIFF这类命令时最怕的是结果看着对但其实逻辑搞反了。比如求“共同关注”应该用交集而不是并集这两个搞混了结果会差很多。我的验证方法是手工构造小数据反推。比如我构造两个集合 A{1,2,3}、B{2,3,4}那么交集应该是 {2,3}并集应该是 {1,2,3,4}差集 A-B 应该是 {1}我先用SADD造出这两个集合然后逐个执行三个命令对照预期结果。确认命令行为符合预期之后再套用到题目要求的数据上。这个办法看着笨但能彻底排除“命令理解错误”这个变量剩下的问题就只可能是数据本身了。5.4 过期时间相关的问题设置过期时间的题常见错误有两个。第一个是把EXPIRE的参数单位搞错。EXPIRE key 300是 300 秒PEXPIRE key 300是 300 毫秒差了一千倍。实验里如果要求“5 分钟过期”写EXPIRE key 300别写成5那是 5 秒。我见过有人写成EXPIRE key 5然后疑惑为什么数据马上就没了。第二个是在SET之后单独EXPIRE存在时间间隙。如果两步之间程序崩了这个键就变成永久的了。所以优先用SET key value EX 300一条命令搞定原子操作没有间隙。还有一点对已经设置过过期时间的键再次SET如果不带EX参数过期时间会被清除变成永久键。这是个很容易忽略的坑实际项目里会导致缓存永远不过期内存慢慢被吃满。所以更新缓存值时记得把过期时间一起带上。5.5 实验数据被污染后怎么恢复如果前面的操作把某个键的值写乱了测试用例一直不通过最直接的办法是精确删除受影响的键然后重来DEL user:1001 DEL stats:user:visit:1001不要用FLUSHALL。理由前面说过它会连其他任务的成果一起清掉。如果实在不确定是哪个键出了问题可以用KEYS 前缀:*把所有可疑的键列出来逐个TYPE检查找出那个类型不对的或者值不对的。这个过程虽然慢但比推倒重来要快尤其是当你前面已经做了好几关的时候。另外一个思路是把整个实验的键命名空间规划好每个任务用独立前缀这样出问题的时候影响范围是隔离的删掉一个前缀下的所有键不影响其他任务。这个习惯从实验一开始就该建立。6. 我在动手过程中攒下的几条经验有些东西文档里不会写但实际动手时是真的省时间我挑几条最有用的说。先画数据模型再敲命令。我现在拿到一个题目不会直接开终端而是先在纸上写清楚要存几类数据、每类用什么结构、键名怎么起、有没有过期要求。这几个问题回答完了命令基本就是照着写几乎不会返工。当初我跳过这一步直接敲结果写到一半发现结构选错了前面写的全要改。多用redis-cli的交互模式做验证再落到代码里。命令行敲命令反馈是即时的你能立刻看到返回值长什么样、类型对不对。确认没问题之后再写进代码能避免“代码跑起来不知道错在哪”的困境。尤其是哈希和列表的返回值格式命令行看一遍比看文档描述要直观得多。把返回值类型当成一个检查点。SADD返回新增个数、INCR返回自增后的新值、LPOP可能返回空、HGETALL返回的是扁平列表某些客户端或字典这些返回值的类型和含义各不相同。每写完一个操作我都习惯打印一下返回值看看不对劲立刻就能发现。这个习惯帮我抓出过好几次“值写进去了但写错了键”的问题。键名带上前缀之后删除操作要格外小心。因为前缀相似很容易手滑删错。我在删除之前会先KEYS 前缀:*看一眼会波及哪些键确认范围对了再批量删。这个动作看起来多余但救过我一次——本来想删cache:user:*结果发现连cache:user:config也被包含在内了而那个是另一个任务要用的。最后说一个我认为最有价值的体会Redis 的命令都很简单难的是判断什么时候该用它、用哪种结构、以及数据该怎么组织。这套实验的价值不在于让你记住几十条命令而在于逼你把同一个需求在键值模型里重新想一遍。这个思维转换完成之后你再去看缓存、队列、计数器、排行榜这些常见功能脑子里会自然冒出“这个用字符串”“这个用列表”“这个用有序集合”那时候你的 Redis 才算是真正入门了。
阅读完成 · 觉得有帮助?
咨询建站