Redis之所以能在众多内存数据库中脱颖而出,靠的不只是快,更是它把常用数据结构直接做成了可以远程操作的存储类型。开发者不需要把数据从数据库读出来再在应用层组装,而是直接让Redis帮你完成排序、去重、计数这些操作。Redis的基本数据类型共五种:String、Hash、List、Set和ZSet,理解它们的特性和适用场景,是用好Redis的第一步。

一、String字符串:最简单也最全能的类型
String是Redis中最基础的数据类型,但它能存的远不止字符串。一个String类型的key可以存储字符串、整数、浮点数,甚至是二进制数据(比如序列化后的对象、图片字节流),单个值最大支持512MB。这个上限在日常业务中几乎不会成为瓶颈,但需要注意如果真的往里塞大对象,网络传输和反序列化的开销会很明显。
String的常用命令比较直观:SET设置值、GET取值、INCR和DECR做原子自增自减、SETNX实现不存在才写入的语义,这个语义正是分布式锁的第一种实现方式。原子性是关键:多个客户端同时执行INCR,Redis保证每次操作都是串行执行的,不会出现并发丢失更新,这一点比自己写代码加锁要省心得多。
SET user:1:name "zhangsan" GET user:1:name INCR article:1001:views # 阅读量原子自增 SETNX lock:order:2001 1 # 不存在才能设置,可用来做简易分布式锁 SETEX session:abc 1800 "uid=42" # 设置值同时指定30分钟过期
典型应用场景包括:常规缓存(缓存用户信息、配置项)、计数器(点赞数、浏览量、限流计数)、分布式锁、全局唯一ID生成(用INCR配合日期拼接)。存对象时通常把对象序列化成JSON字符串再存入,取出来反序列化,简单粗暴但有效。
二、Hash哈希:把对象拆成字段来存
Hash类型相当于一个String的集合,它把一个key下面的内容组织成field-value对的形式。用Hash存用户对象时,可以把name、age、city分别作为field存储。这样做最大的好处是可以单独读写某个字段,而不需要把整个对象序列化反序列化一遍。假设用户对象有几十个字段,你只想更新一个昵称,用String存JSON就得整个读出来改完再写回去,用Hash只需一条HSET user:1 name "lisi"就完成了。
常用命令包括:HSET设置字段、HGET获取字段、HGETALL获取所有字段、HINCRBY对字段值自增、HEXISTS判断字段是否存在。需要注意Hash中没有嵌套结构,field的值只能是字符串,不能在Hash里再套一个Hash,复杂对象还是得靠序列化。
HSET user:1 name "zhangsan" age 28 city "beijing" HGET user:1 age HGETALL user:1 HINCRBY user:1 login_count 1
底层实现上,Hash在小数据量时使用listpack(早期版本是ziplist)这种紧凑的连续内存结构,节省内存开销;当元素数量或单个值大小超过阈值时,会转换为Hashtable。这也是Redis在内存占用和查询性能之间做的经典权衡。应用场景主要是对象缓存、购物车(用户id为key,商品id为field,数量为value)等需要局部读写的场合。
三、List列表:有序可重复的双端队列
List是一个双向链表结构,元素有序且可以重复,支持从两端压入和弹出元素。这个特性让它天然适合做消息队列的简化版:生产者用LPUSH往队头塞数据,消费者用BRPOP阻塞式地从队尾取数据,形成先进先出的消费模型。BRPOP的阻塞特性意味着消费者在没有消息时不会空转轮询,可以挂起等待,省CPU。
常用命令有:LPUSH和RPUSH分别从左、右两端插入元素,LPOP和RPOP从两端弹出,LRANGE按索引范围查询,LINDEX取指定位置元素。两端操作的时间复杂度都是O(1),但中间位置的访问是O(N),所以不要把List当数组随机访问用。
LPUSH task:queue "send_email:1001" LPUSH task:queue "send_sms:1002" BRPOP task:queue 5 # 阻塞最多5秒等待消息 LRANGE task:queue 0 -1 # 查看队列中所有任务
底层实现上,List统一使用quicklist(快速列表),它是一种以listpack为节点的双向链表,兼顾了内存紧凑性和两端操作的高效性。应用场景包括简单消息队列、最新消息列表(朋友圈动态、微博时间线)、操作日志截断等。不过要提醒一句,用List做消息队列有硬伤:没有ACK机制,消息弹出后消费失败就丢了,且无法多消费者按主题广播,严肃的队列需求还是应该用专业的消息中间件。
四、Set集合:无序去重的利器
Set是一个无序、元素唯一的集合,底层在元素都是整数且数量不多时使用intset整数集合,否则使用Hashtable。它的核心价值在于去重和集合运算。往Set里塞一万个元素,重复插入不会产生任何效果,判断某个元素是否存在是O(1)的操作,这比在数据库里建唯一索引再查询要轻量得多。
更强大的是集合间的交并差运算:SINTER求交集、SUNION求并集、SDIFF求差集。比如共同好友功能,把每个用户的好友存成一个Set,两个人的共同好友就是一条SINTER命令的事。抽奖场景也很常见,用SPOP随机弹出元素实现随机抽取且不重复中奖。
SADD user:1:friends "u2" "u3" "u4" SADD user:2:friends "u3" "u4" "u5" SINTER user:1:friends user:2:friends # 共同好友: u3 u4 SISMEMBER user:1:friends "u2" # 判断是否是好友 SPOP lottery:pool # 随机弹出一名中奖者
典型场景包括:标签系统(用户打标签、按标签圈人)、共同好友、抽奖、签到去重、黑名单白名单校验。需要注意集合是无序的,不要依赖元素的插入顺序。
五、ZSet有序集合:排行榜的标配
ZSet在Set的基础上给每个元素关联了一个double类型的分数(score),并按分数有序排列。它是Redis五种基本类型中功能最丰富的一种,既能像Set一样去重,又能按分数范围高效查询。排行榜类的需求几乎就是为ZSet量身定做的:把用户id作为成员,分数作为积分,实时排名一条ZREVRANGE命令就能拿到。
常用命令:ZADD添加成员并指定分数、ZSCORE查分数、ZRANGE按分数升序取区间、ZREVRANGE降序取区间、ZRANK和ZREVRANK查成员排名、ZINCRBY给成员加分。如果需要排名并列显示,分数可以拼上时间戳或其它辅助字段做加权处理。
ZADD rank:game 100 "player:1" 250 "player:2" 180 "player:3" ZINCRBY rank:game 50 "player:1" # player:1 加50分 ZREVRANGE rank:game 0 2 WITHSCORES # 前三名及分数 ZREVRANK rank:game "player:2" # player:2 的排名 ZRANGEBYSCORE rank:game 150 200 # 分数在150到200之间的成员
底层实现方面,ZSet在小数据量时使用listpack,数据量增大后转为skiplist(跳表)加Hashtable的组合:跳表负责有序范围查询,Hashtable负责按成员快速定位分数。跳表是一种概率平衡的多层链表,实现比红黑树简单得多,范围查询性能也更友好,这是Redis作者选择跳表的重要原因。应用场景涵盖排行榜、延迟队列(score存时间戳,定时扫描到期任务)、滑动窗口限流、按权重排序的各类业务。
选型思路与常见误区
面对一个业务需求,怎么选类型?可以先问自己三个问题:数据需不需要去重?需不需要有序?需不需要按字段局部访问?不需要去重且是简单键值对就用String;是对象且经常改单个字段就用Hash;需要队列语义或按插入顺序访问就用List;只需要去重和集合运算就用Set;去重同时还要排序就用ZSet。
常见误区有两个。一是习惯性把所有东西都序列化成JSON塞进String,功能上没问题,但每次修改都要整存整取,在高频更新场景下浪费严重,这类数据换Hash往往更合适。二是把List当数组频繁访问中间元素,或把Set当有序结构使用,都是对类型特性理解不到位导致的性能问题。真正用好Redis的关键,在于把业务操作语义和Redis提供的数据结构能力对齐,让计算尽量靠近数据,这样才能发挥出它作为数据结构服务器的全部价值。