尧图精选

Redis五大核心数据类型详解:从选型到实战的完全指南

🕒 发布时间:2026/9/17 3:11:29 📁 来源:尧图网络
如果你刚接触Redis最先要弄明白的就是它的数据类型。我遇到过很多同学装好Redis就只会SET/GET把对象序列化成一个JSON字符串塞进去等要查某个字段的时候只能整个取出来再反序列化内存和性能都被浪费了不少。Redis虽然简单但它的value绝对不是只有字符串一种。官方把自己定位成数据结构服务器核心就在五种数据类型String、Hash、List、Set、ZSet。这篇文章我会从这五个到底是什么讲到项目里怎么选、怎么用、踩过哪些坑争取让你看完就能直接上手。1. 先搞懂Redis是“按key存value”的数据结构服务器1.1 Redis的全局结构一个相对地址的字典要理解Redis的数据类型先要看懂它的全局结构。你可以把Redis想象成一本巨大的字典每个词条都有一个key然后对应一个value。查询的时候你只需要知道keyRedis会根据哈希算法直接定位到对应的value整个过程接近O(1)。这里的key永远是一个字符串而value才是我们今天的主角。但要注意这个value不是简单的一串字符它可以是一个字符串、一个哈希结构、一个列表、一个集合或者一个有序集合。Redis之所以灵活是因为它把这五种数据结构做成了一等公民你不需要在客户端自己处理复杂的数据关系直接通过命令就能完成操作。我见过很多滥用Redis的方式把数组转成JSON字符串塞进String结果想取其中一个元素只能整个拿出来解析把用户对象序列化成一个大JSON结果每次改昵称都要覆盖整个字符串。这些问题不是Redis不行而是没有根据操作方式选择正确的数据类型。1.2 五大核心数据类型到底是什么这里先给一个快速概览后面每一类我都会展开讲类型内部元素是否有序是否去重典型用途String一个字节序列无无缓存、计数器、分布式锁Hash多个字段-值对无字段唯一对象缓存、用户信息List多个字符串有否消息队列、时间线Set多个字符串无是标签、去重、抽奖ZSet多个member-score对有按scoremember唯一排行榜、延迟队列看到这个表你应该能形成一个基本判断如果value本身是一个对象用Hash如果value是一串有序的数据用List如果value是多个去重元素的集合用Set如果还需要排序用ZSet。这不是绝对的但大概率不会错。1.3 选错类型的典型迹象与选型原则我通常会问自己三个问题这个value是单值还是多值多值里允不允许重复需不需要按顺序访问或排序这三个问题问完基本能锁定数据类型。另外如果发现自己天天在代码里循环遍历一个String里的JSON或者为了改某个字段把整个对象重新SET一次这就是选型错误。Redis的数据类型不是装饰品每种类型背后都有对应的数据结构和复杂度保证选对了很多问题自然就消失了。2. String字符串能原子自增的万能胶水型2.1 String不是普通字符串而是字节序列String是Redis里最基础、最常用的数据类型。你可能会觉得它就是字符串但在Redis内部它保存的其实是一个字节序列。这意味着它可以存字符串、整数、浮点数、二进制数据甚至经过序列化后的对象。不过正是因为它太普通很多人才会用顺手了之后把所有内容都往里塞。String能做的远不止缓存一个JSON它还提供了INCR、DECR、SETNX等原子操作这些才是它在生产环境里不可替代的原因。注意String的value最大是512MB虽然日常用不到这么大但你在设计缓存的时候心里要有这个边界。另外Redis的key也有命名规范我习惯用“业务:对象:ID”的格式比如user:info:10001看起来清晰排查问题也方便。2.2 6个高频命令覆盖90%场景用String你只需要掌握几个命令SET key value GET key MSET key1 value1 key2 value2 MGET key1 key2 INCR key SETNX key valueSETNX是“只在key不存在时设置”这是实现分布式锁的基石。INCR是可以对整数value进行原子自增的命令在高并发下不会出现超卖。这些命令看起来简单但组合起来非常有用。比如用户当天签到你可以用INCR key实现统计比如做接口限流你可以用INCR EXPIRE实现计数器比如热门文章点击量也是INCR。很多人以为String只能缓存静态数据其实它最适合做这种高频写、低频读的计数场景。2.3 实战演练商品库存扣减举个库存扣减的例子。没有经验的做法是先GET库存在代码里减一再SET回去。这在单机环境下没问题但并发稍微一上来就会因为多线程同时读到了旧值而超卖。正确的做法是直接用INCR或者DECRSET product:10086:stock 100 DECR product:10086:stockDECR是原子操作Redis的单线程命令执行机制保证了多个客户端同时访问时命令会排队执行不会出现两个请求都扣成同一个数字的问题。如果你还需要在扣减前判断库存是否足够可以用下面的Lua脚本或者先GET判断再DECR但要注意这个判断不是原子的严谨场景建议用Lua。这个案例告诉我们String的原子操作不是为了炫技而是解决实际问题。做计数器、限流器、库存、库存预扣优先想到String。2.4 底层SDS解决的两个经典问题String的底层结构叫SDSSimple Dynamic String简单动态字符串。传统C语言字符串用空字符判断结尾有一个经典问题如果字符串中间包含\0代表程序员熟悉的截断数据就会丢失。SDS记录了字符串的长度所以是二进制安全的可以存储图片、序列化对象的二进制内容这一点非常重要。另一个经典问题是字符串拼接。C语言字符串拼接前要手动分配内存否则会缓冲区溢出。SDS会预分配一部分空闲空间字符串变长时如果不是特别大会直接使用预分配空间减少内存分配次数。这也是为什么Redis执行很多次APPEND操作性能依然不错的底层原因。所以说面试时问SDS不是单纯考察你背过没背过而是想看你有没有理解Redis为什么适合做缓存、为什么能高效处理字符串操作。3. Hash哈希对象数据的最优解3.1 用String存对象有什么问题很多初学者会把用户对象转成JSON然后SET到Redis里。这样做不是完全不行但问题是当你只想改用户昵称时必须先GET出来反序列化成对象修改字段再序列化再SET回去。每次都要传输整条数据在高并发下CPU和内存开销都被放大了。Hash类型可以完美解决这个问题。Hash里的结构类似于Java的MapString, String一个field对应一个value。你可以把用户ID作为key把姓名、年龄、手机号这些作为field对单个字段进行修改Redis只更新对应的字段不会影响其他字段。3.2 常用命令与用户缓存实战HSET user:10001 name 张三 age 25 HGET user:10001 name HGETALL user:10001 HINCRBY user:10001 age 1 HDEL user:10001 phone比如用户资料缓存登录成功后把用户信息写入Hash。当用户修改头像时只需要HSET user:10001 avatar https://new-avatar.png这个操作只更新avatar字段其他字段不动。这一个细节在用户频繁更新部分字段的场景里性能差距是很明显的。不过也要注意Hash不适合保存过大的value。如果一个Hash里有几万个字段HGETALL会一次性返回大量数据容易阻塞网络和内存。设计时要考虑拆分或者尽量只读取需要的字段。3.3 底层编码切换和过期命中Hash底层有两种编码方式。当字段数量少且每个value较短时Redis用ziplist紧凑列表来节省内存当字段数量变多或者某个value变大时就会自动切换成hashtable。这个切换是Redis内部自动完成的你只需要知道小Hash很省内存大Hash会牺牲一些内存换取访问性能。在实际项目里我给Hash设置过期时间时踩过一个坑。Hash本身可以设置过期时间但单个field不能设置过期时间。比如你想让用户积分24小时后过期但用户名不过期Hash是做不到的只能给整个key设置EXPIRE。如果遇到这种需求要么拆key要么在field的value里自己存过期时间。4. List列表从消息队列到时间线4.1 双向链表模型命令组合是你自由定义的栈/队列List在Redis里的模型是一个双向链表或者用现代版本里的quicklist结构。它可以从左边压入、从右边弹出也可以从右到左取数据。命令组合起来可以实现栈、队列甚至阻塞队列。栈LPUSH LPOP先入后出队列LPUSH RPOP先入先出阻塞队列LPUSH BRPOP没有数据时一直等待这里的关键是理解左右两端。我把常用命令理一下LPUSH listkey value1 value2 RPUSH listkey value1 value2 LPOP listkey RPOP listkey LRANGE listkey 0 -1 LLEN listkey LTRIM listkey 0 994.2 朋友圈时间线案例List最常见的场景之一是“最新列表”。比如朋友圈每个用户有一条自己的发帖时间线。用户发一条动态我们用LPUSH把动态ID推送到列表头部展示时用LRANGE start stop分页取数据。LPUSH feed:10001 5001 LPUSH feed:10001 5002 LRANGE feed:10001 0 9这样就能拿到最新的10条动态ID再根据ID去数据库或缓存里查详情。还有一个很实用的命令是LTRIM它可以只保留列表里最新的N条比如只保留最近100条动态防止列表无限增长LTRIM feed:10001 0 99注意LIst按索引取中间元素的复杂度是O(N)所以不要把它当成数组来用。你要的是“头尾操作快”而不是“随机访问快”。4.3 阻塞队列BRPOP的正确用法和丢消息提醒List也经常做简单的消息队列。生产者用LPUSH消费者用BRPOP等待任务。BRPOP会一直阻塞直到队列里有数据。这样比轮询更省资源。但这里我要提醒一个坑BRPOP取出来的任务如果处理失败了任务就从队列里消失了没有“重新投递”机制。我做异步任务系统的时候会用BRPOP取出任务放进待处理集合处理完成后再从集合里删除。如果启动时发现待处理集合里有残留任务说明上次有任务处理到一半就崩了需要重新执行。这是一个简单的可靠投递方案但Redis List本身不是专业消息中间件复杂场景还是用Kafka或Redis Streams。另外如果你用BRPOP需要注意超时时间。设为0表示一直阻塞但客户端连接可能因为长时间空闲被服务端断开设一个合理的超时时间比如5秒超时后重新BRPOP体验会更好。5. Set集合无序去重和集合运算5.1 命令速查SADD、SREM、SISMEMBER、SINTERSet是不允许重复元素的无序集合它最大的价值是去重和集合关系运算。你可以把Set理解为Java里的HashSet。常用命令SADD user:1:tags tech design SREM user:1:tags design SISMEMBER user:1:tags tech SMEMBERS user:1:tags SCARD user:1:tags SINTER user:1:tags user:2:tags SUNION user:1:tags user:2:tags SDIFF user:1:tags user:2:tagsSINTER可以取两个集合的交集SUNION取并集SDIFF取差集。这几个命令是Set的灵魂因为它们在服务端完成集合运算不需要把数据拉到客户端处理。5.2 三个典型场景标签、共同好友、抽奖第一个场景是用户标签。一个用户可以有很多标签一个标签也可以对应很多用户。用Set存标签非常合适每次给用户加标签就是SADD判断用户是否有某个标签就是SISMEMBER瞬间返回。第二个场景是共同好友。每个人维护一个好友Set想看A和B的共同好友直接SINTER friends:A friends:B如果让你用数据库实现可能要两条SQL或者多表JOIN而Redis一条命令搞定。第三个场景是抽奖。先把所有参与用户放入Set然后随机抽取SADD lottery users SPOP lottery 1 # 随机弹出一个人并且从集合中移除 SRANDMEMBER lottery 1 # 随机抽一个人但不移除我用SPOP做过一次活动抽奖核心逻辑只有这两行命令比在数据库里做随机数查询简单太多了。5.3 intset和hashtable为什么小整数集合省内存Set底层也有编码切换。如果所有元素都是整数且数量不大Redis使用intset整数集合来存储它是一个有序的整数数组非常节省内存。当元素不是整数或者数量超过阈值就会转成hashtable。这一点在面试里经常被问到但在实际开发中我更多地用它来理解为什么小Set性能好。如果你要存一批ID列表类似“已读用户ID集合”用Set比用List更合理因为Set天然去重而且SISMEMBER判断是否已读是O(1)。有个坑SMEMBERS会返回集合里的所有元素如果集合很大比如几百万个ID这条命令会阻塞Redis服务。推荐用SSCAN迭代获取或者先SISMEMBER判断不要无脑SMEMBERS。6. ZSet有序集合排行榜与延迟队列的标配6.1 member和score唯一元素和可排序的权重ZSet可能是五大数据类型里最“聪明”的一个。它和Set一样元素唯一不同之处在于每个元素还关联了一个double类型的score。Redis按照score从小到大排序如果score相同再按member的字典序排列。这个结构天然适合“有权重、需要排序”的数据。分数、积分、时间戳、优先级都可以作为score。6.2 排行榜实现正序、倒序、按分数段取人排行榜是ZSet最经典的应用。比如直播间礼物排行榜每个用户送出礼物会增加对应分数ZINCRBY leaderboard:10001 100 user_1查看整个榜单前10名ZREVRANGE leaderboard:10001 0 9 WITHSCORES查看某个用户在榜单中的排名ZREVRANK leaderboard:10001 user_1ZREVRANGE是从大到小取因为榜单一般数值大在前。如果你需要正序排名用ZRANGE。如果你要按分数区间查询比如“分数在80到90之间的用户”用ZRANGEBYSCORE leaderboard 80 90 WITHSCORES这个操作在业务里非常有用比如筛选积分达到某个等级的用户。6.3 底层跳表哈希的双结构设计ZSet底层结合了哈希表和一个叫跳表的数据结构。哈希表负责快速定位member的score跳表负责按score排序和范围查询。跳表可以理解为一种层级式的链表用空间换时间插入、删除、查找的时间复杂度都是O(log N)。我在这儿不展开跳表的完整实现了但你要理解一个关键点ZSet不是像List那样只能在两端操作它在中间插入、删除、按区间取数据都非常高效。这就是我经常说“如果数据要排序并且要频繁更新ZSet是Redis里最合适的选择”的原因。6.4 用ZSet做一个简易延迟队列一个很实用的方案是延迟队列。把任务ID作为member任务执行时间戳作为score。投递任务时ZADD delay_queue 1699999999 task_123后台任务循环执行ZRANGEBYSCORE delay_queue 0 current_time LIMIT 0 1如果有返回说明有到了时间应该执行的任务然后ZREM把它移除执行真正的业务逻辑。这个方案简单可靠不需要引入额外的消息中间件。不过我要提醒如果任务量非常大或者你需要消息确认、重试、死信队列还是应该用专业消息中间件。ZSet延迟队列适合中小业务比如订单超时取消、定时刷新缓存。7. 综合实战会员模块的类型选型与常见排查7.1 从需求反推什么场景该用哪种类型在实际设计Redis缓存时我会先列需求再反推类型。这里用会员系统举例登录状态用String存tokenSETEX设置过期时间用户基本信息用Hash存字段支持单字段修改用户操作日志用List存最近的登录记录LTRIM截断用户标签/权限用Set存判断是否有某个权限用户积分排名用ZSet存按积分排序未读消息数或验证码次数用String INCR原子计数反过来如果用户积分许久才更新一次其实也可以直接放Hash字段。类型选择没有唯一标准但决策流程是固定的先看数据结构再看操作模式再考虑数据规模。7.2 一个会员系统的混合用法比如用户登录成功后我们会同时写多个keySETEX session:token:abc123 86400 user_10001 HSET user:10001 name 张三 level 3 SADD role:10001 vip ZINCRBY leaderboard:points 10 user_10001 LPUSH login_log:10001 2024-01-01 10:00:00 LTRIM login_log:10001 0 9这是非常典型的多类型混合用法。不同数据有不同的生命周期session短用户资料长排行榜一直累计日志只保留最近10条。如果全部用String那后面做权限判断、排行榜更新、日志截断都会非常别扭。7.3 常见错误与快速排查TYPE、OBJECT ENCODING和内存分析排查Redis数据类型相关问题时我一般按这几步来首先用TYPE key确认value类型。比如你以为存的是Hash结果返回string那就说明写入时用错了命令。然后用OBJECT ENCODING key查询内部编码。它会返回int、embstr、raw、ziplist、hashtable、quicklist、skiplist等结果。看到这个就能判断数据是否已经因为规模变大而切换了底层结构。如果Redis内存涨得异常我会用redis-cli --bigkeys扫描哪些key比较大。这个命令会遍历所有key输出占用空间最大的key类型。但是要注意在高峰期跑可能会造成延迟建议业务低峰期执行。还有一个常见的坑把对象序列化成JSON后存入String然后还想做范围查询或者字段更新根本做不到。遇到这种需求回想一下这篇文章换成Hash或者ZSet。7.4 一张表总结五大数据类型我最后再放一张总结表方便你收藏或贴在自己代码旁边类型典型命令底层结构核心优势一句话场景StringSET/GET/INCR/SETNXSDS二进制安全、原子计数缓存、计数器、锁HashHSET/HGET/HINCRBYziplist/hashtable对象局部更新用户信息、配置ListLPUSH/RPUSH/LPOP/BRPOPquicklist双向操作、阻塞读队列、时间线SetSADD/SISMEMBER/SINTERintset/hashtable去重、集合运算标签、共同好友ZSetZADD/ZINCRBY/ZRANGEskiplisthash按分数排序排行榜、延迟队列我个人在实际项目里体会最深的一点是Redis数据类型的学习不是背命令而是建立“数据结构匹配业务需求”的直觉。每当你准备把某个东西塞进Redis先停一秒问自己这个东西是字符串、对象、列表、集合还是要排序的集合想清楚了Redis的威力才能发挥出来。另外一个小技巧所有命令都可以通过官方文档查但生产环境里遇到问题先从TYPE开始排查往往比什么都不做直接重启要快得多。希望这篇文章能帮你少走弯路把五种类型真正用起来。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →