引言:
上一篇文章介绍了Redis的基本概念,了解了Redis 为什么快以及为什么使用单线程操作数据。Redis 受欢迎的另一个原因就是提供了丰富的数据结构,就像一套精密的‘瑞士军刀’,便于我们去解决各种各样的问题。灵活使用redis的数据结构,能让我们高效、优雅地解决业务问题。 今天,就带你深入Redis的“武器库”,看看这5把核心“军刀”如何助你披荆斩棘!
Redis 提供了 5 种基本数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。
其底层实现主要依赖这 8 种数据结构:简单动态字符串(SDS)、LinkedList(双向链表)、Dict(哈希表/字典)、SkipList(跳表)、Intset(整数集合)、ZipList(压缩列表)、QuickList(快速列表)。
我们要分清这两者的区别:
- 数据类型是编程方面的概念:是直接提供给开发者使用的
- 数据结构是底层实现的机制:是计算机层面存储数据的实现方式
基础数据结构
1. String (字符串):不只是“Hello World”
- 本质: 最基础的类型,可存文本、数字(整数/浮点数)、甚至二进制数据(如图片片段)。最多可以容纳的数据长度是
512M
。 - 底层: 简单动态字符串 (SDS),比C原生字符串更安全高效,支持动态扩容和预分配。
- Redis原生命令:
SET key value [EX seconds] # 设置键值,可设置过期时间 GET key # 获取值 INCR key # 原子递增 DECR key # 原子递减 APPEND key value # 追加内容
- 适用场景: 简单缓存、计数器、分布式锁、小对象存储。
实战案例:
原生命令:
# 实现文章阅读量统计 SET article:1001:views 0 INCR article:1001:views # 实现分布式锁 SETNX lock:order:1001 "1" EX 30
- Java代码片段 :
// 计数器 Long views = jedis.incr("article:1001:views"); // 分布式锁 (简化版) String result = jedis.set("lock:order:1001", "client1", "NX", "PX", 30000); if ("OK".equals(result)) { // 获取锁成功,执行业务 }
2. Hash (哈希表):存储“对象”的利器
- 本质: 键值对集合,特别适合存储对象(如用户信息、商品属性)。
- 底层: 压缩列表 (ziplist - 小数据时) 或 哈希表 (hashtable - 大数据时)。
- 优点:
- 结构化存储: 一个Key对应多个
field-value
,天然表示对象。 - 高效操作:
HGET
/HSET
操作单个字段,无需序列化/反序列化整个对象!HGETALL
获取全部。 - 节省网络: 更新用户姓名?只需
HSET user:1001 name "新名字"
,不用传整个用户对象。
- 结构化存储: 一个Key对应多个
常用命令:
HSET key field value # 设置字段值 HGET key field # 获取字段值 HGETALL key # 获取所有字段值 HINCRBY key field increment # 字段值递增
适用场景: 对象缓存(用户、商品、配置)、需要频繁修改部分字段的场景。
实战案例:
原生命令:
# 存储用户信息 HSET user:1001 name "张三" age 28 email "zhangsan@example.com" # 更新用户年龄 HINCRBY user:1001 age 1
Java代码片段:
// 存储用户信息 Map<String, String> userMap = new HashMap<>(); userMap.put("name", "张三"); userMap.put("age", "28"); userMap.put("email", "zhangsan@example.com"); jedis.hset("user:1001", userMap); // 只获取年龄 String age = jedis.hget("user:1001", "age"); // 只更新年龄 jedis.hset("user:1001", "age", "1");
3. List (列表):灵活的双端队列
本质: 有序、可重复元素的集合,按插入顺序排序。双向链表实现。
- 底层: 压缩列表 (ziplist) 或 双向链表 (linkedlist)。
- 特点:
- 队列:
LPUSH
(左入) +RPOP
(右出) = 先进先出 (FIFO) (消息队列、任务队列) - 栈:
LPUSH
(左入) +LPOP
(左出) = 后进先出 (LIFO) - 最新列表:
LPUSH
+LTRIM 0 99
= 保持最新的100条数据(最新消息、最新订单) - 阻塞操作:
BRPOP
/BLPOP
(没有元素时阻塞等待,实现简单消息队列)
- 队列:
- 常用命令:
LPUSH key value # 左侧插入 RPUSH key value # 右侧插入 LPOP key # 左侧弹出 RPOP key # 右侧弹出 BLPOP key timeout # 阻塞式左侧弹出
- 适用场景: 消息队列、最新列表(朋友圈、微博)、任务队列、记录操作日志。
实战案例:
原生命令:
# 实现消息队列 LPUSH message:queue "task1" RPOP message:queue # 实现最新文章列表 LPUSH news:latest "article:1001" LTRIM news:latest 0 9 # 只保留最新10条
- Java代码片段:
// 1. 消息队列 jedis.lpush("message:queue", "task1", "task2", "task3"); String task = jedis.rpop("message:queue"); // 消费者 (阻塞式) List<String> task = jedis.brpop(30, "message:queue"); // 阻塞30秒等待 if (task != null) { String taskData = task.get(1); // 实际数据在返回列表的第二个元素 // 处理任务... } // 2. 最新文章列表 jedis.lpush("news:latest", "article:1001", "article:1002", "article:1003"); jedis.ltrim("news:latest", 0, 9); // 只保留最新10条
4. Set (集合):排重与社交的基石
- 本质: 无序、唯一元素的集合。自动去重!
- 底层: 整数集合 (intset - 纯整数小集合) 或 哈希表 (hashtable)。
超能力:
- 去重:
SADD
自动过滤重复元素。 - 集合运算:
SINTER
(交集-共同好友)SUNION
(并集)SDIFF
(差集-可能认识的人)。 - 随机元素:
SRANDMEMBER
(抽奖)SPOP
(随机移除并返回 - 秒杀资格)。 - 成员存在性:
SISMEMBER
(快速判断用户是否在某个组/拥有标签)。
- 去重:
常用命令:
SADD key member # 添加元素 SREM key member # 移除元素 SISMEMBER key member # 判断元素是否存在 SINTER key1 key2 # 求交集
- 适用场景: 标签系统、社交关系(好友、关注)、唯一性检查(抽奖用户)、黑白名单、随机推荐。
实战案例:
原生命令:
# 文章标签系统 SADD article:1001:tags "Redis" "数据库" "缓存" # 共同好友 SINTER user:1001:friends user:1002:friends
- Java代码片段 (共同好友):
// 1. 标签系统 jedis.sadd("article:1001:tags", "Redis", "数据库", "缓存"); // 2. 检查标签 boolean isMember = jedis.sismember("article:1001:tags", "Redis"); System.out.println("是否包含Redis标签:" + isMember); // 输出:true // 3. 共同好友 jedis.sadd("user:1001:friends", "user1002", "user1003", "user1004"); jedis.sadd("user:1002:friends", "user1001", "user1003", "user1005"); Set<String> commonFriends = jedis.sinter("user:1001:friends", "user:1002:friends"); System.out.println("共同好友:" + commonFriends); // 输出:[user1003]
5. Sorted Set (ZSet - 有序集合):排行榜的王者
- 本质: Set的升级版!元素唯一,但每个元素关联一个
score
(分数)。元素按score
排序(从小到大)。分数可相同,相同分数按字典序排。 - 底层: 压缩列表 + 跳表 (skiplist)。跳表是实现高效范围查询的关键。
- 特点:
- 元素唯一
- 按分数排序
- 支持范围查询
- 常用命令:
ZADD key score member # 添加元素 ZRANGE key start stop [WITHSCORES] # 获取范围 ZREVRANK key member # 获取逆序排名 ZSCORE key member # 获取分数
- 适用场景:排行榜、延迟队列、带权重的元素存储
实战案例:
原生命令:
# 游戏排行榜 ZADD game:leaderboard 5000 "player1" 4500 "player2" # 获取TOP3 ZREVRANGE game:leaderboard 0 2 WITHSCORES
- Java代码片段:
// 1. 排行榜 jedis.zadd("game:leaderboard", 5000, "player1"); jedis.zadd("game:leaderboard", 4500, "player2"); jedis.zadd("game:leaderboard", 4800, "player3"); // 2. 获取TOP3 Set<String> topPlayers = jedis.zrevrange("game:leaderboard", 0, 2); System.out.println("TOP3玩家:" + topPlayers); // 输出:[player1, player3, player2] // 3. 获取玩家排名 Long rank = jedis.zrevrank("game:leaderboard", "player2"); System.out.println("player2排名:" + (rank + 1)); // 输出:3 // 4. 范围查询 Set<String> players = jedis.zrangeByScore("game:leaderboard", 4700, 5100); System.out.println("4700-5100分玩家:" + players); // 输出:[player3, player1]
高级数据结构应用
1. 位图(Bitmap)——极省空间的统计工具
特点:
- 基于String实现
- 极省空间
- 支持位运算
应用场景:用户签到、活跃用户统计、特征标记
实战案例:
# 用户签到
SETBIT sign:202405:user1 15 1 # 5月15日签到
# 统计当月签到天数
BITCOUNT sign:202405:user1
2. HyperLogLog——海量数据去重统计
特点:
- 固定12KB内存
- 误差率0.81%
- 支持合并
应用场景:UV统计、大规模去重计数
实战案例:
# 统计UV
PFADD uv:20240501 "user1" "user2" "user3"
PFCOUNT uv:20240501
3. 地理空间索引(GEO)——位置服务好帮手
特点:
- 基于Sorted Set实现
- 支持距离计算
- 支持范围查询
应用场景:附近的人、位置搜索、地理围栏
实战案例:
# 添加地理位置
GEOADD locations 116.404 39.915 "天安门" 121.474 31.230 "上海外滩"
# 查询5公里内的地点
GEORADIUS locations 116.404 39.915 5 km
数据结构选型指南
需求场景 | 推荐数据结构 | 原因 |
---|---|---|
简单键值存储 | String | 简单高效 |
对象存储 | Hash | 字段独立操作 |
消息队列 | List | 顺序访问 |
去重集合 | Set | 自动去重 |
排行榜 | Sorted Set | 自动排序 |
二值统计 | Bitmap | 极省空间 |
海量去重统计 | HyperLogLog | 固定内存消耗 |
地理位置服务 | GEO | 内置距离计算 |
性能优化建议
- 避免大Key:单个String不超过10KB,Hash字段不超过1000个
- 合理使用管道:减少网络往返时间
- 设置合适过期时间:避免内存浪费
- 使用SCAN替代KEYS:避免阻塞
- 考虑数据分片:超大集合可考虑分片存储
结语:Redis数据结构的力量
Redis通过精心设计的数据结构,在性能、功能和易用性之间取得了完美平衡。掌握这些数据结构,你就能:
- 构建高性能缓存系统
- 实现复杂的业务逻辑
- 处理海量数据统计
- 开发实时应用
记住:没有最好的数据结构,只有最适合的数据结构。根据你的具体业务场景,选择最合适的Redis数据结构,才能发挥Redis的最大威力。