Redis究竟有几种数据结构?分别有什么特点?

张开发
2026/4/12 11:35:39 15 分钟阅读

分享文章

Redis究竟有几种数据结构?分别有什么特点?
Redis有多少种数据结构大多数人的回答是5种String、List、Hash、Set、Sorted Set。这个答案放在Redis 3.x的时代没问题但到了Redis 7.x实际可用的数据结构已经有10种。除了上面5种基础类型还有Bitmap、HyperLogLog、GEO、Stream以及Module扩展类型。这10种数据结构大厂在线上系统里并不是只用其中两三种。京东的购物车用Hash微博的转评赞用String美团的排行榜用Sorted Set外卖的附近商家搜索用GEO用户签到用Bitmap。每种数据结构被选中背后都有具体的业务原因。下面先从大厂的真实使用场景出发看看它们各自用了什么数据结构、为什么这么选然后系统梳理Redis全部10种数据结构的特点和选型方法末尾附了一张可以直接拿来用的选型速查表。大厂怎么用Redis数据结构京东到家购物车Hash京东到家的购物车系统用的是Redis的Hash。购物车的数据有一个明显的特点一个用户在一个门店下可能会加好几件商品每件商品又有商品ID、数量、价格等多个属性。这种「一个Key下面挂多个字段」的结构和Hash天然匹配。京东到家最初的Key设计是以用户标识作为Key每个门店的商品作为Hash的Field。用户打开购物车时一次HGETALL就能把这个用户在某个门店的所有商品拉出来。修改某件商品的数量时用HSET只更新对应的Field不需要把整个购物车的数据取出来再序列化写回去。京东购物车Hash结构示意图这里有个容易踩的坑。京东到家后来发现有些用户在多个门店都加了大量商品导致单个Hash的Field数量过多形成了大Key。大Key的问题HGETALL的时间复杂度是O(n)Field越多阻塞时间越长严重时会影响Redis其他请求的响应。京东云技术团队对这个问题的解决方案是拆Key把原来的userPin作为Key改成userPin_storeId按门店维度把一个大Hash拆成多个小Hash。每个Hash只存一个门店下的商品Field数量可控。来源京东云技术团队《浅析Redis大Key》 https://juejin.cn/post/7295694519184441353为什么用Hash而不是String如果用String存购物车常见做法是把整个购物车序列化成JSON字符串存进去。问题在于每次改一件商品的数量都得先GET整个JSON反序列化修改再序列化最后SET回去。高并发下还要处理并发写入的覆盖问题。Hash天然支持字段级的读写HSET只改一个Field不影响其他Field操作粒度更细、性能更好。用Hash存购物车这种多字段对象时Key的粒度要控制好。一个Hash里Field太多就是大Key读写性能都会下降。按业务维度拆Key是标准做法。微博转评赞计数String微博每条内容下面的转发数、评论数、点赞数背后用的是Redis的String。计数场景的数据特点是值只有一个数字操作只有加1、减1、读取。Redis的String类型有一个INCR命令单线程模型下天然是原子操作不需要加锁。每条微博的转发数、评论数、点赞数各用一个独立的Key存储。一条热门微博的点赞数可能每秒几万次写入INCR在Redis单实例上每秒可以执行10万次以上扛得住这个量级。微博的关注关系也用到了Redis。用户A关注了哪些人最初用Redis的原生Set存储每个元素是被关注者的用户ID。这个方案功能上没问题但内存开销大。微博后来自研了一个叫LongSet的数据结构把用户ID当成long类型直接存储去掉了Set底层哈希表里每个entry的指针和元数据开销内存占用降了一个量级。微博的信息流关注的人发的最新内容用的是Sorted Set以时间戳作为分数排序。用户刷新首页时用ZREVRANGEBYSCORE按时间倒序取最新的内容ID列表。来源微博技术团队陈波《新浪微博Redis优化历程》 https://www.slidestalk.com/u65/redis25919为什么计数用String而不是Hash假设把一条微博的转发数、评论数、点赞数都放到一个Hash里三个计数器共享一个Key。看起来更整齐但在Redis Cluster环境下三个计数器放在一个Key里意味着它们一定落在同一个节点上。如果某条微博突然成为热点三个计数器的写入压力全部集中在一个节点。拆成三个独立的String Key有机会分散到不同的节点上负载更均衡。单实例模式下差别不大但在集群规模大的时候这个设计的收益很明显。美团排行榜和最新列表Sorted Set与List美团的商家排行榜、销量榜这类场景用的是Redis的Sorted Set。排行榜的数据有两个核心需求每个元素有一个可比较的分数需要按分数排序后取前N名。Sorted Set每个元素都关联一个分数score内部按分数自动排序。获取排名前10的商家一条ZREVRANGE就能拿到时间复杂度O(logN M)N是集合大小M是返回的元素数。更新排名时商家每成交一单ZINCRBY给这个商家的分数加1。Sorted Set会自动维护排序位置不需要应用层排序。美团还把List用在了「最新列表」的场景上。比如最新的订单列表、最新的评价列表这类数据的特点是只需要最近的N条不需要排序。用LPUSH往头部插入新数据用LTRIM保持列表长度不超过N两条命令组合就实现了固定长度的滑动窗口。美团的统一KV存储系统叫Squirrel底层基于Redis Cluster构建日均访问量达到万亿级别。来源美团技术博客《美团万亿级KV存储架构与实践》 https://tech.meituan.com/2020/07/01/kv-squirrel-cellar.html 来源美团技术博客《缓存那些事》 https://tech.meituan.com/2017/03/17/cache-about.htmlSorted Set和List在「取最新N条」的场景下都能用怎么选关键看是否需要按分数排序。如果只需要按插入时间取最近的N条List就够了LPUSH LTRIM组合效率高。如果需要按某个维度比如销量、热度排序用Sorted Set。淘宝秒杀库存扣减String Lua秒杀场景的核心挑战是库存扣减。短时间内几十万请求同时抢同一个商品库存数字必须精确不能超卖。阿里的做法是用Redis的String存库存数量用DECR做原子扣减。DECR是原子操作不存在两个请求同时读到同一个值然后各自减1的问题。实际场景中库存扣减通常不是只执行一个DECR还需要先判断库存是否大于0。「判断 扣减」这两步如果分开执行中间可能被其他请求插入导致超卖。解决方案是用Lua脚本把两步操作打包成一个原子操作Redis会把整个Lua脚本的执行当成一个不可分割的命令来处理。-- 库存扣减Lua脚本 local stock tonumber(redis.call(GET, KEYS[1])) if stock 0 then redis.call(DECR, KEYS[1]) return 1 end return 0分布式锁也是String的典型应用。SET命令带上NX不存在才设置和EX设置过期时间参数一条命令实现加锁。这个在阿里的秒杀系统里用来做请求去重和资源互斥。来源阿里云开发者社区《电商秒杀系统架构实战》 https://developer.aliyun.com/article/1702168外卖和打车的附近搜索GEO美团外卖展示附近3公里内的商家滴滴匹配附近的可用车辆这类位置服务场景用的是Redis的GEO。GEO的使用方式是用GEOADD把商家或司机的经纬度写入Redis用GEOSEARCHRedis 6.2替代了老的GEORADIUS命令按距离范围查询附近的元素。GEOADD merchants:beijing 116.397128 39.916527 shop_001 GEOSEARCH merchants:beijing FROMLONLAT 116.40 39.92 BYRADIUS 3 km ASC COUNT 20GEO底层并不是一种独立的数据结构它基于Sorted Set实现。Redis把二维经纬度通过GeoHash算法编码成一个一维的52位整数作为Sorted Set的分数存储。GeoHash的编码原理是交替对经度和纬度做二分把二维平面递归地划分成越来越小的格子。距离相近的点GeoHash值也相近存在边界情况除外这使得范围查询可以利用Sorted Set的有序性来高效完成。GEO有一个限制它只支持二维平面上的距离计算不考虑海拔。对于外卖和打车场景足够了但如果需要三维空间距离需要应用层自己计算。日活统计和用户签到Bitmap与HyperLogLog统计每天有多少用户登录了系统日活或者记录用户本月哪几天签到了这类场景适合用Bitmap。Bitmap本质上是String类型的按位操作。用SETBIT把用户ID对应的位设成1表示该用户今天活跃。统计日活就是BITCOUNT数一下有多少个位是1。1亿用户的日活统计只需要约12MB内存1亿bit ≈ 12.5MBBITCOUNT在这个数据量下的执行时间在毫秒级。用户签到场景类似。以sign:{userId}:{yearMonth}作为Key一个月最多31天用31个bit就能表示一个用户一个月的签到记录。GETBIT查某天是否签到BITCOUNT统计本月签到天数。如果不需要精确数字只需要一个大致的去重统计结果可以用HyperLogLog。HyperLogLog是一种概率数据结构无论统计多少个不同元素固定占用12KB内存标准误差0.81%。用PFADD添加元素PFCOUNT获取去重后的近似数量。Bitmap和HyperLogLog的选择边界需要知道「某个用户今天有没有活跃」用Bitmap它保留了每个用户的状态信息只需要知道「今天总共有多少活跃用户」且允许0.81%的误差用HyperLogLog它只保留一个聚合结果无法查询单个用户的状态。Redis全部数据结构速查Redis 7.2的源码定义了7种对象类型String、List、Set、Sorted Set、Hash、Stream、Module。加上基于String实现的Bitmap和HyperLogLog、基于Sorted Set实现的GEO一共10种可用的数据结构。Redis数据结构全景图数据结构底层编码典型命令适用场景大厂案例StringRAW / EMBSTR / INTGET SET INCR DECR SETNX缓存、计数器、分布式锁、库存微博转评赞计数、淘宝秒杀库存ListQUICKLIST / LISTPACKLPUSH RPUSH LPOP RPOP LRANGE最新列表、消息队列简易美团最新订单列表HashLISTPACK / HTHSET HGET HGETALL HINCRBY对象缓存、购物车、用户信息京东到家购物车SetINTSET / LISTPACK / HTSADD SREM SISMEMBER SINTER标签、去重、交集计算微博共同关注Sorted SetLISTPACK / SKIPLISTZADD ZRANGE ZREVRANGE ZINCRBY排行榜、延迟队列、信息流美团排行榜、微博信息流Bitmap基于StringSETBIT GETBIT BITCOUNT BITOP签到、日活统计、布隆过滤器用户签到打卡、日活统计HyperLogLog基于StringPFADD PFCOUNT PFMERGEUV统计、去重计数允许误差页面独立访客统计GEO基于Sorted SetGEOADD GEOSEARCH GEODIST附近的人/商家、距离计算美团外卖附近商家、打车匹配StreamRadix树 LISTPACKXADD XREAD XGROUP XACK消息队列支持消费者组轻量级事件流处理上面这张表覆盖了日常开发中最常用的9种数据结构。Module类型比较特殊它是Redis的扩展机制允许通过加载模块来定义全新的数据类型比如RedisJSON、RediSearch、RedisTimeSeries等。Module不是Redis内置的数据结构需要单独安装对应的模块这里不展开。底层编码与自动转换Redis对同一种数据类型会根据数据量的大小自动选择不同的底层编码方式。数据量小的时候用内存紧凑的编码如listpack、intset数据量大了自动转换为性能更好的编码如哈希表、跳表。这个转换对使用者是透明的不需要手动干预。以Redis 7.2源码中的默认阈值为参考HashField数量不超过512且每个Field和Value的长度不超过64字节时用listpack编码超过后转为哈希表Set所有元素都是整数且数量不超过512时用intset编码包含非整数元素但数量不超过128且元素长度不超过64字节时用listpack编码超过后转为哈希表Sorted Set元素数量不超过128且Value长度不超过64字节时用listpack编码超过后转为跳表 哈希表List在Redis 7.2中统一使用quicklist编码quicklist内部由listpack节点组成的双向链表构成以上阈值来自Redis 7.2源码的默认配置值可通过CONFIG SET命令动态调整。这些编码转换阈值在生产环境中一般不需要改动。但如果遇到内存敏感的场景比如有几千万个小Hash可以适当调大listpack的阈值让更多的Hash使用内存更紧凑的编码。反过来如果Hash的Field数量经常超过阈值导致频繁转换可以在Key设计时就控制好每个Hash的Field数量。数据结构选型决策Redis数据结构选型决策图按业务场景选数据结构可以参考这张对照表业务场景推荐数据结构选型理由缓存单个值字符串、数字、序列化对象String操作简单GET/SET即可缓存多字段对象用户信息、商品详情、购物车Hash字段级读写不需要整体序列化反序列化计数器点赞数、浏览量、库存StringINCR/DECR原子操作天然并发安全排行榜销量榜、积分榜Sorted Set自动按分数排序ZREVRANGE取前N名最新列表最新订单、最新评论只取最近N条ListLPUSH LTRIM固定长度滑动窗口信息流/时间线需要按时间排序、支持分页Sorted Set时间戳作分数ZRANGEBYSCORE范围查询去重集合标签、已读列表、共同好友SetSADD自动去重SINTER求交集大规模去重计数UV、独立IP允许误差HyperLogLog12KB固定内存0.81%标准误差二值状态记录签到、日活、功能开关Bitmap1亿用户只需12MB按位操作地理位置查询附近商家、附近的人GEO基于GeoHash支持半径查询和距离计算消息队列需要消费者组、消息确认StreamXGROUP XACK支持多消费者和消息回溯分布式锁StringSET NX EX组合一条命令加锁限流滑动窗口Sorted Set时间戳作分数ZRANGEBYSCORE ZCARD统计窗口内请求数有几个常见的选型纠结点值得说一下。缓存对象用String还是Hash如果对象是一个整体每次都是整体读写比如缓存一段HTML、一个配置文件用String更合适一次GET拿到所有数据。如果对象有多个字段经常只读写其中几个字段比如用户信息里只改昵称用Hash更合适省掉整体序列化反序列化的开销。两者不是非此即彼的关系同一个系统里可以根据访问模式分别使用。消息队列用Stream还是专业MQStream适合轻量级的消息场景比如系统内部的事件通知、状态变更同步。它的优势是不需要额外部署中间件Redis本身就能用。劣势是功能上和Kafka、RocketMQ相比有差距没有事务消息、没有消息过滤、集群模式下的可靠性也不如专业MQ。数据量大、可靠性要求高的场景还是用专业的消息队列。Set和Sorted Set怎么选Set是无序的、Sorted Set是有序的这是最直观的区别。如果只需要去重和集合运算交集、并集、差集用Set。如果还需要按某个维度排序用Sorted Set。Sorted Set每个元素多存了一个8字节的分数内存开销比Set大一些不需要排序的场景没必要用它。小结数据结构选型这件事工作越久越觉得它不是一个技术问题而是一个对业务数据理解程度的问题。同一个「缓存用户信息」的需求一个人用String把JSON整体丢进去另一个人用Hash按字段存都能跑通。差别在高并发场景下才显现出来前者每次改昵称都要全量读写后者只改一个Field。这个差别不是Redis的问题是对「这个数据会怎么被访问」的理解深度不同。从上面这些大厂案例里能看到一个共性它们选数据结构的依据不是「哪个功能多」而是「数据的访问模式是什么」。京东的购物车选Hash是因为购物车需要字段级读写。微博的计数器选String是因为计数只需要原子递增。美团的排行榜选Sorted Set是因为排行榜需要按分数排序。每一个选择都指向同一个判断标准数据怎么写、怎么读、并发有多高这三个问题的答案决定了该用哪种数据结构。Redis给了我们10种数据结构但日常开发中真正高频使用的就那么五六种。与其把每种数据结构的命令都背一遍不如把业务数据的访问模式想清楚答案自然就出来了。

更多文章