1. 从“键值对”到“十大数据类型”:Redis的进化之路
提到Redis,很多人的第一反应就是“缓存”。没错,它凭借其内存级的读写速度和简单的键值对模型,成为了缓存界的“扛把子”。但如果你对Redis的认知还停留在简单的set key value和get key,那可能就错过了它最强大的部分。Redis之所以能从一个简单的缓存中间件,进化成如今被广泛认可的“数据结构服务器”,其核心就在于它提供的丰富数据类型。这不仅仅是五个、八个,而是整整十种内置的数据结构类型。每一种类型都不是凭空设计,而是为了解决特定场景下的数据建模难题,将原本需要在应用层通过复杂代码和多次查询才能实现的功能,内化为了原子性的命令操作。
我见过不少项目,初期为了图省事,把所有数据都序列化成JSON字符串,然后一股脑地用String类型存进Redis。上线初期相安无事,随着业务增长,各种奇葩需求就来了:“给这个用户的积分排个名”、“统计一下最近一周登录过的用户”、“把这个商品列表按价格排序分页给我”……这时候,面对一堆字符串,要么在应用层做繁重的计算,拖慢整体性能;要么频繁与数据库交互,失去了缓存的意义。这就是没有用对数据类型的典型困境。
Redis的十大数据类型,就是十种应对特定场景的“武器”。理解它们,本质上是在学习如何根据数据的访问模式来选择合适的存储结构,从而用最少的资源,最高效地满足业务需求。这不仅仅是API调用的问题,更是一种数据建模和系统设计的思维。接下来,我就结合自己这些年踩过的坑和总结的经验,带你彻底搞懂这十种类型,看看它们各自在什么场景下能发挥出最大威力。
2. 基石类型:String(字符串)的深度解析与实战误区
String是Redis最基本的数据类型,也是其他所有类型的基石。但千万别小看它,“简单”往往意味着灵活和高效。一个Redis字符串value最多可以是512MB,这足以存储一张不小的图片序列化后的数据或一篇长文章。
### 2.1 不只是文本:String的二进制安全与多功能命令
String是二进制安全的。这意味着它可以存储任何数据,比如序列化的对象、图片的字节流,而不仅仅是文本。其核心命令SET、GET、DEL大家都很熟悉,但它的威力远不止于此。
- 原子计数器:这是String最经典的应用之一。
INCR、DECR、INCRBY这些命令是原子操作,在高并发场景下无需加锁即可实现精准计数,用于文章阅读量、用户点赞数、库存扣减等场景再合适不过。SET article:1001:views 0 INCR article:1001:views # 阅读量+1,并发安全 - 批量操作与过期管理:
MSET/MGET用于批量操作,减少网络开销。SETEX、PSETEX可以在设值的同时设置过期时间(秒/毫秒级),是实现缓存失效的标配。 - 位操作(BitMap):这是String类型一个隐藏的“大招”。通过
SETBIT、GETBIT、BITCOUNT、BITOP等命令,你可以把String当作一个巨大的位数组来操作。每个用户ID对应一个偏移量,只需一个比特位就能标记其状态(如是否签到、是否在线),极大地节省了内存。# 记录用户uid=1001在2023-10-01是否签到(假设该日期偏移量为365) SETBIT sign:2023:10:01 1001 1 # 检查是否签到 GETBIT sign:2023:10:01 1001 # 统计当天签到总人数 BITCOUNT sign:2023:10:01
### 2.2 实战避坑:滥用String与序列化陷阱
String虽然万能,但滥用就是最大的坑。
- 误区一:万物皆可JSON String。这是最常见的反模式。比如存储一个用户信息
{“name”: “张三”, “age”: 30},用String存储后,如果你想修改年龄,必须GET回来,在应用层反序列化、修改、再序列化、最后SET回去。这个过程涉及网络IO和序列化开销,且非原子性。更合适的做法是使用Hash类型。 - 误区二:忽略过期时间导致内存泄漏。用Redis作缓存,一定要设置合理的过期时间(TTL)。我曾经排查过一个线上问题,Redis内存持续增长直至写满触发淘汰,原因就是大量缓存键未设置TTL,变成了“永久缓存”。使用
EXPIRE命令或直接在SET时用EX/PX选项是必须养成的习惯。 - 误区三:大Key问题。如果一个String的value非常大(比如几百KB甚至上MB),在对其进行
GET、SET甚至过期删除时,会导致主线程阻塞,影响其他命令的执行。对于大文本或大对象,需要考虑是否应该拆分,或者使用其他更适合的结构。
String是入口,但绝非终点。当你发现自己在用String存储结构化数据或需要复杂操作时,就该考虑下面这些更专业的类型了。
3. 结构化数据存储首选:Hash(哈希)的精细化管理之道
当你的数据是一个对象(Object),拥有多个字段(Field)时,Hash类型就是为你量身定做的。它类似于编程语言中的Map<String, String>,适合存储一个实体的多个属性,比如用户、商品、配置项。
### 3.1 Hash的核心优势:原子操作与内存效率
与将整个对象序列化成String相比,Hash有两个压倒性优势:
- 原子性字段操作:你可以单独对某个字段进行
HSET、HGET、HINCRBY,而无需读写整个对象。这对于频繁更新部分字段的场景(如更新用户最后登录时间、增加商品库存)性能提升巨大,且是原子性的。 - 内存优化:Redis的Hash在元素较少时,采用一种称为
ziplist(压缩列表)的紧凑编码方式,比将每个字段分开存储成独立的String键要节省大量内存。这对于存储海量小对象(如用户会话、商品SKU属性)至关重要。
# 存储一个用户信息 HSET user:1001 name “张三” age 30 city “北京” # 单独获取年龄 HGET user:1001 age # 原子性地增加年龄 HINCRBY user:1001 age 1 # 获取所有字段和值 HGETALL user:1001### 3.2 场景对比与使用边界
Hash并非没有缺点。HGETALL命令在字段非常多时会返回一个巨大的回复,可能阻塞客户端。此时可以使用HSCAN进行渐进式遍历。另外,Hash不支持直接对内部的多个field进行范围查询或排序,这是它的设计边界。
那么,何时用String,何时用Hash?
- 用String:数据是一个不可分割的整体,读写总是以整个单位进行(如缓存一个完整的HTML页面、一个序列化的配置对象)。或者需要用到String特有的位图、计数器功能。
- 用Hash:数据是结构化的,你需要频繁地、独立地访问或修改其中的部分字段。典型场景就是各种“对象”的缓存:用户会话(Session)、商品信息、文章元数据等。
一个经验法则是:如果你在应用层代码里,经常需要从一个大JSON字符串中解析出某个特定字段,那么是时候改用Hash了。
4. 有序集合与列表:List与Sorted Set的排序艺术
当数据需要体现顺序时,List和Sorted Set就该登场了。它们都维护着元素的顺序,但实现方式和适用场景截然不同。
### 4.1 List(列表):灵活的双端队列与消息队列
Redis的List是一个双向链表,这意味着在头部和尾部进行插入删除操作的时间复杂度是O(1),非常高效。它的核心能力是序列和排队。
- 消息队列(简易版):利用
LPUSH(生产消息)和BRPOP(阻塞式消费消息)可以构建一个简单的消息队列。BRPOP在没有元素时会阻塞连接,避免了消费者轮询的空转消耗。# 生产者 LPUSH myqueue “task1” # 消费者(阻塞等待,超时时间5秒) BRPOP myqueue 5注意:这只是一个基础模型。对于需要ACK、重试、死信队列等复杂功能的场景,建议使用专业的消息中间件如RabbitMQ或Kafka。Redis Stream类型(后面会讲到)也是一个更强大的选择。
- 最新列表:
LPUSH+LTRIM是一个经典组合,可以轻松实现一个固定长度的最新动态列表,比如网站的最新100条评论。LPUSH latest:comments “评论C” LTRIM latest:comments 0 99 # 永远只保留最新的100条 - 实战坑点:List的索引访问(
LINDEX)效率是O(N),性能随列表长度线性下降,切忌用它来实现随机访问。它的优势在于头尾操作和范围获取(LRANGE)。
### 4.2 Sorted Set(有序集合):排行榜与范围查询的利器
如果说List是按插入顺序排序,那么Sorted Set就是按一个显式的分数(Score)来排序。它是Redis数据类型中最具特色的之一,结合了Set的去重性和按分数排序的能力。
- 核心数据结构:每个元素都是一个
成员(Member)- 分数(Score)对。成员唯一,分数用于排序(双精度浮点数)。分数可以相同,此时按成员的字典序排序。 - 排行榜实现:这是Sorted Set的“杀手级”应用。无论是游戏积分榜、商品销量榜,还是热门文章列表,都能轻松应对。
ZADD leaderboard 95 “Alice” 87 “Bob” 95 “Charlie” ZREVRANGE leaderboard 0 2 WITHSCORES # 获取前三名(降序) ZRANK leaderboard “Bob” # 获取Bob的排名(升序排名) - 范围查询(Range Query):你可以高效地获取分数在某个区间内的所有成员(
ZRANGEBYSCORE)。这可以用来实现诸如“查找价格在100到200之间的所有商品ID”、“获取昨天下午3点到5点所有在线用户”等功能。 - 集合运算:
ZUNIONSTORE、ZINTERSTORE可以对多个有序集合进行并集、交集计算,并将结果存为新集合。例如,可以计算同时出现在“一周热门”和“本月热门”两个榜单中的文章。
Sorted Set的内部实现是跳跃表(SkipList)和哈希表的结合,保证了范围查询和单点查询的高效。它的一个高级用法是,将时间戳作为分数,成员作为事件ID,这样就可以构建一个时间轴或延迟队列。
5. 去重与集合运算:Set的基础与高级玩法
Set(集合)是一个无序的、元素唯一的容器。它的基础应用是去重,但它的集合运算能力才是真正的价值所在。
### 5.1 基础去重与随机元素
- 去重:快速判断一个元素是否存在(
SISMEMBER),或存储一个不重复的集合,如文章的标签、用户的所有好友ID。SADD article:1001:tags “数据库” “缓存” “Redis” SISMEMBER article:1001:tags “缓存” # 返回1,表示存在 - 随机抽取:
SRANDMEMBER或SPOP(弹出)命令非常适合实现抽奖、随机推荐等场景。例如,从所有参与活动的用户中随机抽取10名幸运者。# 假设 sadd activity:users user1 user2 ... user10000 SRANDMEMBER activity:users 10 # 抽取10个不重复的用户,不删除 SPOP activity:users 10 # 抽取10个用户并从集合中移除
### 5.2 强大的集合运算:社交关系与数据过滤
Set的SINTER(交集)、SUNION(并集)、SDIFF(差集)命令,为复杂的数据关系查询提供了原子性的解决方案。
- 共同关注/好友:在社交网络中,查找A和B的共同好友,就是计算两个用户好友集合的交集。
SINTER friends:user:A friends:user:B - 兴趣标签推荐:计算拥有相似标签的用户。可以先找出与目标用户标签集合交集最大的其他用户。
- 数据过滤:假设有一个“黑名单IP”集合和一个“今日访问IP”集合,
SDIFF可以快速找出不在黑名单中的访问IP。
这些运算在服务端原子性完成,避免了在应用层进行多次查询和循环比对,性能极高。但需要注意,当参与运算的集合非常大时,这些命令可能会比较耗时,在阻塞Redis主线程。对于大数据集,可以考虑使用SSCAN迭代并结合客户端计算,或者将结果缓存起来。
6. 地理空间与基数统计:Geo与HyperLogLog的专项突破
Redis在后续版本中引入了更专门化的数据类型,用于解决特定领域的高频问题。
### 6.1 Geo(地理空间索引):附近的人与地点搜索
Geo本质上是使用Sorted Set(ZSET)的一种特殊封装,将二维的地理坐标(经纬度)通过Geohash算法编码成一维的分数(Score),从而利用ZSET的有序特性实现附近位置的查询。
- 核心命令:
GEOADD添加地理位置,GEODIST计算两点距离,GEORADIUS/GEORADIUSBYMEMBER查询指定半径内的元素。GEOADD restaurants 116.404 39.915 “全聚德” 116.408 39.920 “东来顺” GEORADIUS restaurants 116.405 39.915 5 km WITHDIST # 查找5公里内的餐厅,并返回距离 - 实现原理:理解其基于ZSET实现很重要。这意味着你可以用
ZREM删除一个地点,用ZRANGE查看所有地点(虽然没意义),但更重要的是,你可以利用ZSET的所有特性。例如,给每个地点附带一个“热度”分数,然后按距离和热度进行综合查询(这需要一些客户端计算)。 - 精度与性能:Geo的精度对于大多数LBS应用(如附近商家、打车)已经足够。它的性能远高于在关系数据库中用
ST_Distance_Sphere函数进行计算。但要注意,数据量极大(上千万)时,范围查询的复杂度是O(N+logM),仍需评估性能。
### 6.2 HyperLogLog(基数统计):海量数据去重计数
HyperLogLog是一种概率数据结构,用于估算一个集合中不重复元素的数量(基数)。它的最大优势是:占用空间极小且固定。一个HyperLogLog键只需要约12KB内存,就能以标准误差小于1%的精度,统计接近2^64个不同元素的基数!
- 典型场景:统计一个大型网站每日的独立访客数(UV)。如果使用Set来存储每个用户的ID,对于亿级用户量,内存消耗是灾难性的。而使用HyperLogLog,每天只需要12KB。
PFADD uv:2023-10-01 “user_id_1” “user_id_2” “user_id_1” # 添加元素,自动去重 PFCOUNT uv:2023-10-01 # 估算当天的UV PFMERGE uv:2023-10-week1 uv:2023-10-01 uv:2023-10-02 # 合并多天的数据,估算整周的UV - 重要限制:HyperLogLog只提供计数,无法获取具体的元素内容,也无法判断某个特定元素是否已经添加过。它只回答“大约有多少个不重复的元素”这个问题。对于需要精确去重或获取明细的场景,它不适用。
- 实战心得:
PFADD和PFCOUNT都是非常快的O(1)操作。合并多个HLL(PFMERGE)也是高效的。它通常用于替代那些“只需要一个大概数字”的Set场景,是节省内存的神器。
7. 位图与流:Bitmap与Stream的扩展应用
这两种类型可以看作是基础类型的威力加强版,分别扩展了String和List的能力边界。
### 7.1 Bitmap(位图):极致的空间利用
如前文在String类型中提到的,Bitmap是通过String类型的位操作命令实现的。但它解决的问题如此典型,以至于我们常常将其视为一个独立的数据类型。它的核心价值在于用最小的空间表示大量的布尔状态。
- 用户行为标记:除了签到,还可以用于记录用户是否阅读过某条消息、是否拥有某项权限、是否完成某个新手任务等。每个用户只需要一个比特位。
- 大数据量下的特征筛选:假设有1亿用户,需要筛选出“女性”且“活跃”的用户。可以创建两个Bitmap,一个标记性别,一个标记活跃状态。通过
BITOP命令对两个位图进行AND运算,得到的结果位图中,值为1的位对应的用户ID就是目标用户。这个操作的速度极快,且内存消耗极小(1亿用户约需12.5MB)。SETBIT gender:female 1001 1 SETBIT active:20231001 1001 1 BITOP AND result:target gender:female active:20231001 GETBIT result:target 1001 # 结果为1,表示用户1001符合条件 - 注意事项:Bitmap的偏移量(offset)是整数。如果你的用户ID不是连续的整数,需要建立一个从用户ID到偏移量的映射关系,这可能会增加一些复杂度。通常,可以使用用户ID的自增主键部分作为偏移量。
### 7.2 Stream(流):完善的消息队列与事件溯源
Redis 5.0引入的Stream类型,旨在弥补List作为消息队列时的功能缺失,提供了一个完整的、支持多消费者组的、可持久化的消息队列解决方案。
核心概念:
- 消息:Stream中的一条记录,包含一个唯一的ID(通常由时间戳-序列号组成)和多个键值对字段。
- 消费者组:允许多个消费者共同消费同一个Stream,组内消费者负载均衡,每条消息只会被组内的一个消费者处理。
- Pending List:已投递给消费者但尚未被确认(ACK)的消息列表,用于处理消费失败后的重试。
与List的对比:
特性 List (LPUSH/BRPOP) Stream 消息回溯 消费后即删除,无法重现 消息持久化,可重复读取 多消费者 一个消息只能被一个消费者获取 支持消费者组,组内竞争消费 确认机制 无,弹出即视为成功 有显式ACK机制,失败可重投 阻塞订阅 支持 支持,功能更丰富 功能完整性 简单 完整,支持消息ID范围查询、监控等 # 生产者添加消息 XADD mystream * user “Alice” action “login” # 创建消费者组 XGROUP CREATE mystream mygroup 0 # 消费者从组内读取消息 XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream > # 消费者确认消息处理完成 XACK mystream mygroup <message-id>适用场景:Stream非常适合需要可靠消息传递、顺序性、且希望用Redis统一技术栈的场景。例如,用户活动追踪(Event Sourcing)、微服务间的异步通信、日志收集等。它比List更可靠,但比Kafka、RabbitMQ等专业消息队列更轻量,功能上也有所取舍。
8. 概率去重与地理围栏:Bloom Filter的客户端实现与GEO进阶
除了内置类型,Redis还可以通过模块或客户端算法,支持更多高级数据结构。其中,布隆过滤器的应用尤为广泛。
### 8.1 Bloom Filter(布隆过滤器):存在性校验的守门员
Redis自身没有内置Bloom Filter,但可以通过RedisBloom模块或直接在客户端利用Bitmap实现。它的作用是:以极小的空间代价,快速判断一个元素“一定不存在”或“可能存在”于一个超大集合中。
- 工作原理:使用多个哈希函数,将一个元素映射到位数组(Bitmap)的多个位置上,并将这些位置置为1。查询时,如果该元素对应的所有位置都是1,则它“可能存在”;如果任何一个位置是0,则它“一定不存在”。
- 典型应用:
- 缓存穿透防护:在查询数据库前,先用Bloom Filter判断键是否存在。如果Bloom Filter说“不存在”,则直接返回空,避免对数据库的无效查询。因为Bloom Filter不会漏报(不存在的一定会判否),所以能有效拦截恶意的不存在Key请求。
- 推荐去重:在新闻推荐中,判断一篇新文章是否已经推荐给过某个用户。使用Bloom Filter可以快速过滤掉绝大部分已读文章,只在“可能存在”的情况下才去查询精确的已读记录库,大幅降低查询压力。
- 实现方式:
- 服务端模块:加载
RedisBloom模块后,可以使用BF.ADD、BF.EXISTS等命令,最为方便。 - 客户端算法+Bitmap:在应用层实现哈希和位运算,将Redis的Bitmap作为存储介质。这种方式更灵活,但需要自己维护。
- 服务端模块:加载
- 重要缺陷:Bloom Filter有误判率(False Positive),即可能将不存在的元素误判为存在。误判率可以通过增加位数组大小和使用更多哈希函数来降低,但无法消除。因此,它只适用于那些可以接受偶尔误判、但对“不存在”的判断要求绝对准确的场景。
### 8.2 GEO的进阶思考:距离计算与范围查询的代价
虽然Geo用起来很简单,但在设计大规模LBS系统时,还需要考虑更深层次的问题:
- 距离计算负载:
GEORADIUS命令在查询时,需要计算中心点与集合内每个点的距离。当集合内元素数量巨大(例如全球所有店铺)时,即使使用地理哈希预先筛选,计算量依然可观。常见的优化策略是分级索引:例如,先按国家、城市等大范围筛选出一个子集,再在这个子集上执行精确的Geo查询。 - 结果排序与分页:
GEORADIUS默认返回所有结果,如果范围内元素很多,返回的数据量会很大。虽然可以用COUNT选项限制,但分页是个难题,因为每次查询的范围是固定的,传统的LIMIT offset, count模式在这里不适用(除非配合SCAN)。一种做法是,先获取所有元素的ID和距离,在客户端进行排序和分页,但这会带来额外的网络和计算开销。 - 动态位置更新:对于移动对象(如车辆、外卖员),位置频繁更新。频繁调用
GEOADD更新坐标是可行的,但要注意这本质上是ZADD操作。如果对象数量极多,更新频率极高,可能会对Redis造成写入压力。需要根据业务容忍度,适当降低位置更新的频率。
理解这些底层细节,能帮助你在享受Redis Geo便利的同时,提前规避性能瓶颈,设计出更健壮的LBS服务。
9. 类型选择决策树与混合使用策略
面对十种类型,如何选择?我总结了一个简单的决策流程,可以帮你快速定位方向:
- 需要存储一个简单的值或计数器吗?->String
- 需要存储一个对象(多个字段)吗?->Hash
- 需要维护一个有序的序列,且经常从两端操作吗?->List
- 需要去重,或者做集合运算(交集、并集)吗?->Set
- 需要按某个分数排序,或者按分数范围查询吗?->Sorted Set
- 需要处理地理位置和附近搜索吗?->Geo
- 需要估算海量数据的唯一值数量吗?->HyperLogLog
- 需要记录大量的布尔状态(是/否)吗?->Bitmap
- 需要可靠的消息队列或多消费者流处理吗?->Stream
然而,真实的业务场景往往更复杂,混合使用多种类型才是高级玩法。例如:
- 场景:实现一个带点赞计数的文章评论列表,并按热度排序。
- 评论列表:使用
List存储每条评论的ID(LPUSH保证时间序)。 - 评论内容:使用
Hash存储评论ID到详细内容(作者、正文、时间)的映射。 - 点赞数:使用
String(计数器)或Hash中的字段,键名为comment:点赞数。 - 点赞用户记录(防重复点赞):使用
Set,键名为comment:liked_users,存储点赞用户ID。 - 评论热度榜:使用
Sorted Set,成员是评论ID,分数是点赞数或一个综合热度分(点赞数+时间衰减)。每当有点赞事件,更新对应评论ID的分数。
- 评论列表:使用
通过这种组合,你可以高效地实现列表分页获取、单条评论详情读取、点赞的原子操作和热度排序,所有操作都在Redis内完成,极大地减轻了数据库的压力。
10. 性能、内存与持久化:类型选择背后的工程考量
选择了正确的类型,并不意味着万事大吉。在工程实践中,你必须关注它们对性能和内存的影响。
### 10.1 内存编码的奥秘:ziplist, intset, skiplist
Redis为了节省内存,对小尺寸的数据结构采用了特殊的紧凑编码方式:
- Hash/List/ZSet元素较少时,会采用
ziplist(压缩列表)编码,将所有元素紧凑地存储在一起。 - Set元素较少且均为整数时,会采用
intset(整数集合)编码。 - 当元素数量或大小超过配置的阈值时,它们会转换为标准的
hashtable、linkedlist、skiplist等结构。
通过OBJECT ENCODING key命令可以查看一个键的内部编码。理解这一点很重要:盲目追求“小”可能适得其反。如果你为了利用ziplist而刻意将一个大Hash拆分成无数个tiny hash,反而会因为管理大量键的元数据而浪费更多内存。需要根据实际数据规模和访问模式,调整redis.conf中如hash-max-ziplist-entries等参数,找到最佳平衡点。
### 10.2 大Key与热Key的监控与治理
- 大Key:通常指value size过大(如>10KB的String, 元素数量>5000的Hash/Set/ZSet)的Key。大Key会导致
DEL命令阻塞、网络传输慢、内存分配不均等问题。可以使用redis-cli --bigkeys扫描,或通过MEMORY USAGE命令分析。治理方法包括:拆分(如将大Hash按字段前缀拆成多个小Hash)、压缩(客户端压缩value)、使用更适合的数据结构(如用HyperLogLog替代大Set做基数统计)。 - 热Key:指访问频率非常高的Key。热Key会造成单实例负载过高,成为性能瓶颈。监控可以通过
redis-cli --hotkeys(需开启maxmemory-policy为LFU)或分析慢查询日志。解决方案包括:本地缓存(如Guava Cache)、读写分离、使用Redis Cluster将热Key通过hash tag强制分配到独立slot等。
### 10.3 持久化与数据安全
数据类型的选择不影响RDB或AOF持久化机制,但会影响持久化文件的大小和恢复速度。例如,一个包含百万成员的Set,其AOF日志文件会记录大量的SADD命令。在极端情况下,考虑禁用某些重写成本高昂的命令。更重要的是,无论使用何种类型,都要理解SAVE/BGSAVE(RDB快照)和appendfsync(AOF刷盘策略)的配置,根据业务对数据安全性和性能的要求做出权衡。通常建议同时开启RDB和AOF,用RDB做冷备,用AOF保证数据完整性。
11. 从命令到设计:数据类型在真实架构中的角色
让我们看一个更综合的案例:设计一个简易的社交网络“关注/粉丝”与“动态推送”系统。
- 关系存储:
Set类型存储关注列表和粉丝列表:followings:user:{uid}和followers:user:{uid}。SADD/SREM用于添加/取消关注,SCARD用于获取数量,SINTER用于计算共同关注。
- 动态发布与推送(写扩散):
- 当用户发布一条动态时,除了存入数据库,我们执行:
LPUSH将动态ID写入自己的动态列表posts:user:{uid}。- 获取自己的所有粉丝ID(从
followers:user:{uid})。 - 对每个粉丝ID,执行
LPUSH dynamic:feed:{follower_uid},将动态ID推送到他们的个人Feed流中。
- 这里,每个用户的Feed流就是一个
List。这种“写扩散”模式,读性能极佳(直接LRANGE分页),但写操作成本高,适合粉丝数不多的场景(如普通社交)。
- 当用户发布一条动态时,除了存入数据库,我们执行:
- 动态拉取(读扩散):
- 对于粉丝数巨大的大V,采用“读扩散”。发布动态时,只存入一个全局的
Sorted Set中,分数为发布时间戳:ZADD global:posts <timestamp> <post_id>。 - 当用户查看Feed时,系统需要:
- 获取他的关注列表(
followings:user:{uid})。 - 对这些关注用户的ID,执行
ZUNIONSTORE,合并他们发布的动态(可以预先为每个用户维护一个Sorted Set:posts:user:{uid})。 - 从合并后的临时
Sorted Set中按分数倒序分页获取动态ID。
- 获取他的关注列表(
- 这种模式写轻读重,适合粉丝量巨大的场景,但需要更复杂的聚合逻辑,且可能用到
ZUNIONSTORE这种较重命令。
- 对于粉丝数巨大的大V,采用“读扩散”。发布动态时,只存入一个全局的
- 热点动态排行:
- 使用一个全局的
Sorted Set,如hot:posts,成员是动态ID,分数是热度值(点赞数权重 + 评论数权重 + 时间衰减)。每当有点赞、评论事件,就更新对应动态的分数。首页的热榜直接从这个ZSet中获取。
- 使用一个全局的
在这个案例中,Set、List、Sorted Set各司其职,共同构建了核心功能。选择“写扩散”还是“读扩散”,就是根据数据模型(用户粉丝量)对读写压力的权衡,这比单纯记住命令要重要得多。
理解Redis的十大数据类型,绝不是背诵API手册,而是学习一种用“数据结构服务器”的思维来建模和解决实际问题的能力。从简单的缓存键值,到复杂的关系运算、流处理、地理搜索,Redis提供了一套丰富而高效的原语。真正的功夫,在于如何根据你的数据特征和访问模式,像搭积木一样,灵活、混合地运用这些类型,构建出既快又省的内存数据层。下次当你设计一个功能时,不妨先问问自己:这个数据,在Redis里应该长什么样?