Redis 常用数据结构与实战避坑指南 🚀
本文全面介绍 Redis 的五大核心数据结构及其使用场景,并总结实战中常见的坑点和最佳实践。
📋 目录
- 一、String(字符串)
- [1.1 基本操作](#1.1 基本操作)
- [1.2 应用场景](#1.2 应用场景)
- [1.3 常见坑点](#1.3 常见坑点)
- 二、Hash(哈希)
- [2.1 基本操作](#2.1 基本操作)
- [2.2 应用场景](#2.2 应用场景)
- [2.3 常见坑点](#2.3 常见坑点)
- 三、List(列表)
- [3.1 基本操作](#3.1 基本操作)
- [3.2 应用场景](#3.2 应用场景)
- [3.3 常见坑点](#3.3 常见坑点)
- 四、Set(集合)
- [4.1 基本操作](#4.1 基本操作)
- [4.2 应用场景](#4.2 应用场景)
- [4.3 常见坑点](#4.3 常见坑点)
- [五、Sorted Set(有序集合)](#五、Sorted Set(有序集合))
- [5.1 基本操作](#5.1 基本操作)
- [5.2 应用场景](#5.2 应用场景)
- [5.3 常见坑点](#5.3 常见坑点)
- [六、Redis 使用最佳实践](#六、Redis 使用最佳实践)
- 七、总结

一、String(字符串)
String 是 Redis 最基本的数据类型,一个 key 对应一个 value。String 类型是二进制安全的,可以包含任何数据,比如图片或者序列化的对象。
1.1 基本操作
bash
# 设置值
SET key value
# 获取值
GET key
# 设置值并指定过期时间(秒)
SETEX key seconds value
# 批量设置
MSET key1 value1 key2 value2
# 批量获取
MGET key1 key2
# 自增
INCR key
# 自减
DECR key
# 追加字符串
APPEND key value
# 获取字符串长度
STRLEN key
Java 示例:
java
import redis.clients.jedis.Jedis;
public class StringExample {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
// 设置值
jedis.set("user:1001:name", "张三");
// 获取值
String name = jedis.get("user:1001:name");
System.out.println(name); // 输出:张三
// 设置带过期时间的值(10秒)
jedis.setex("verification:code", 10, "123456");
// 计数器
jedis.set("page:views", "0");
jedis.incr("page:views"); // 1
jedis.incrBy("page:views", 10); // 11
jedis.close();
}
}
1.2 应用场景
- 缓存对象:缓存用户信息、商品详情等
- 计数器:文章阅读量、点赞数、库存数量
- 分布式锁:使用 SETNX 实现简单的分布式锁
- Session 共享:分布式系统中的 Session 存储
- 限流:基于时间窗口的访问频率控制
1.3 常见坑点
⚠️ 坑点 1:大 Key 问题
java
// ❌ 错误示例:存储大对象
jedis.set("user:profile", largeJsonString); // 超过 10MB
// ✅ 正确做法:拆分存储或使用 Hash
jedis.hset("user:1001", "name", "张三");
jedis.hset("user:1001", "age", "25");
jedis.hset("user:1001", "email", "zhangsan@example.com");
影响:大 Key 会导致网络阻塞、慢查询、内存碎片等问题。
⚠️ 坑点 2:缓存穿透
java
// ❌ 问题代码:查询不存在的数据
String user = jedis.get("user:9999");
if (user == null) {
// 每次都会查询数据库
user = database.query("SELECT * FROM users WHERE id = 9999");
}
// ✅ 解决方案:缓存空值
String user = jedis.get("user:9999");
if (user == null) {
user = database.query("SELECT * FROM users WHERE id = 9999");
if (user == null) {
// 缓存空值,设置较短过期时间
jedis.setex("user:9999", 60, "NULL");
} else {
jedis.setex("user:9999", 3600, user);
}
}
⚠️ 坑点 3:缓存雪崩
java
// ❌ 问题:大量 key 同时过期
for (int i = 0; i < 10000; i++) {
jedis.setex("product:" + i, 3600, productData);
}
// ✅ 解决方案:过期时间加随机值
Random random = new Random();
for (int i = 0; i < 10000; i++) {
int expireTime = 3600 + random.nextInt(300); // 3600-3900秒
jedis.setex("product:" + i, expireTime, productData);
}
二、Hash(哈希)
Hash 是一个 string 类型的 field 和 value 的映射表,特别适合存储对象。
2.1 基本操作
bash
# 设置单个字段
HSET key field value
# 获取单个字段
HGET key field
# 批量设置
HMSET key field1 value1 field2 value2
# 批量获取
HMGET key field1 field2
# 获取所有字段和值
HGETALL key
# 删除字段
HDEL key field
# 判断字段是否存在
HEXISTS key field
# 获取所有字段名
HKEYS key
# 获取所有值
HVALS key
# 字段值自增
HINCRBY key field increment
Java 示例:
java
import redis.clients.jedis.Jedis;
import java.util.HashMap;
import java.util.Map;
public class HashExample {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
// 存储用户信息
Map<String, String> user = new HashMap<>();
user.put("name", "李四");
user.put("age", "28");
user.put("email", "lisi@example.com");
user.put("phone", "13800138000");
jedis.hmset("user:1002", user);
// 获取单个字段
String name = jedis.hget("user:1002", "name");
System.out.println(name); // 输出:李四
// 获取所有字段
Map<String, String> userData = jedis.hgetAll("user:1002");
System.out.println(userData);
// 字段自增(如:购物车商品数量)
jedis.hset("cart:1002", "product:101", "1");
jedis.hincrBy("cart:1002", "product:101", 2); // 数量变为 3
jedis.close();
}
}
2.2 应用场景
- 存储对象:用户信息、商品信息、配置信息
- 购物车:用户 ID 作为 key,商品 ID 作为 field,数量作为 value
- 统计信息:网站访问统计、用户行为统计
2.3 常见坑点
⚠️ 坑点 1:HGETALL 性能问题
java
// ❌ 危险操作:对大 Hash 使用 HGETALL
Map<String, String> allData = jedis.hgetAll("large:hash"); // 可能有上万个字段
// ✅ 正确做法:使用 HSCAN 分批获取
ScanParams scanParams = new ScanParams().count(100);
String cursor = "0";
do {
ScanResult<Map.Entry<String, String>> scanResult =
jedis.hscan("large:hash", cursor, scanParams);
List<Map.Entry<String, String>> entries = scanResult.getResult();
// 处理这批数据
cursor = scanResult.getCursor();
} while (!cursor.equals("0"));
⚠️ 坑点 2:Hash 过大导致内存问题
java
// ❌ 问题:单个 Hash 存储过多字段
for (int i = 0; i < 1000000; i++) {
jedis.hset("all:users", "user:" + i, userData);
}
// ✅ 解决方案:分片存储
int shardSize = 1000;
for (int i = 0; i < 1000000; i++) {
int shardId = i / shardSize;
jedis.hset("users:shard:" + shardId, "user:" + i, userData);
}
三、List(列表)
List 是简单的字符串列表,按照插入顺序排序。可以在列表的头部或尾部添加元素。
3.1 基本操作
bash
# 左侧插入
LPUSH key value1 value2
# 右侧插入
RPUSH key value1 value2
# 左侧弹出
LPOP key
# 右侧弹出
RPOP key
# 阻塞式左侧弹出
BLPOP key timeout
# 获取列表长度
LLEN key
# 获取指定范围元素
LRANGE key start stop
# 获取指定索引元素
LINDEX key index
# 修剪列表
LTRIM key start stop
Java 示例:
java
import redis.clients.jedis.Jedis;
import java.util.List;
public class ListExample {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
// 消息队列示例
jedis.rpush("message:queue", "消息1", "消息2", "消息3");
// 消费消息
String message = jedis.lpop("message:queue");
System.out.println(message); // 输出:消息1
// 获取列表所有元素
List<String> messages = jedis.lrange("message:queue", 0, -1);
System.out.println(messages); // [消息2, 消息3]
// 最新动态列表(保留最新 100 条)
jedis.lpush("user:1001:feeds", "发布了新动态");
jedis.ltrim("user:1001:feeds", 0, 99); // 只保留前 100 条
// 阻塞式消费(用于消息队列)
List<String> result = jedis.blpop(5, "task:queue"); // 5秒超时
jedis.close();
}
}
3.2 应用场景
- 消息队列:简单的生产者-消费者模型
- 最新列表:微博最新动态、文章最新评论
- 排行榜:配合 LPUSH + LTRIM 实现固定长度排行榜
3.3 常见坑点
⚠️ 坑点 1:LRANGE 大范围查询
java
// ❌ 危险操作:查询大列表全部数据
List<String> allItems = jedis.lrange("large:list", 0, -1); // 可能有百万条数据
// ✅ 正确做法:分页查询
int pageSize = 100;
int page = 1;
List<String> pageItems = jedis.lrange("large:list",
(page - 1) * pageSize, page * pageSize - 1);
⚠️ 坑点 2:List 作为消息队列的局限性
java
// ❌ 问题:List 不支持消息确认机制
String message = jedis.lpop("queue");
// 如果这里处理失败,消息就丢失了
processMessage(message);
// ✅ 解决方案:使用 RPOPLPUSH 实现可靠队列
String message = jedis.rpoplpush("queue", "queue:processing");
try {
processMessage(message);
jedis.lrem("queue:processing", 1, message); // 处理成功,删除
} catch (Exception e) {
// 处理失败,消息仍在 processing 队列中,可以重试
}
⚠️ 坑点 3:BLPOP 死锁问题
java
// ❌ 问题:多个消费者可能导致死锁
// 消费者 1
jedis.blpop(0, "queue"); // 永久阻塞
// ✅ 建议:设置合理的超时时间
jedis.blpop(5, "queue"); // 5秒超时
四、Set(集合)
Set 是 string 类型的无序集合,集合成员是唯一的。
4.1 基本操作
bash
# 添加元素
SADD key member1 member2
# 获取所有元素
SMEMBERS key
# 判断元素是否存在
SISMEMBER key member
# 删除元素
SREM key member
# 获取集合元素个数
SCARD key
# 随机获取元素
SRANDMEMBER key count
# 弹出随机元素
SPOP key
# 集合运算
SINTER key1 key2 # 交集
SUNION key1 key2 # 并集
SDIFF key1 key2 # 差集
Java 示例:
java
import redis.clients.jedis.Jedis;
import java.util.Set;
public class SetExample {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
// 标签系统
jedis.sadd("article:1001:tags", "Java", "Redis", "数据库");
jedis.sadd("article:1002:tags", "Python", "Redis", "机器学习");
// 查找共同标签(交集)
Set<String> commonTags = jedis.sinter("article:1001:tags", "article:1002:tags");
System.out.println(commonTags); // [Redis]
// 点赞去重
jedis.sadd("post:2001:likes", "user:1001", "user:1002");
boolean isLiked = jedis.sismember("post:2001:likes", "user:1001");
System.out.println(isLiked); // true
// 抽奖系统
jedis.sadd("lottery:participants", "user1", "user2", "user3", "user4");
String winner = jedis.spop("lottery:participants"); // 随机抽取一个
System.out.println("中奖用户:" + winner);
// 共同好友
jedis.sadd("user:1001:friends", "user:2001", "user:2002", "user:2003");
jedis.sadd("user:1002:friends", "user:2002", "user:2003", "user:2004");
Set<String> mutualFriends = jedis.sinter("user:1001:friends", "user:1002:friends");
System.out.println("共同好友:" + mutualFriends);
jedis.close();
}
}
4.2 应用场景
- 标签系统:文章标签、商品标签
- 社交关系:共同好友、共同关注
- 去重:用户签到、点赞、投票
- 抽奖系统:随机抽取中奖用户
4.3 常见坑点
⚠️ 坑点 1:SMEMBERS 性能问题
java
// ❌ 危险操作:对大集合使用 SMEMBERS
Set<String> allMembers = jedis.smembers("large:set"); // 可能有百万个元素
// ✅ 正确做法:使用 SSCAN
ScanParams scanParams = new ScanParams().count(100);
String cursor = "0";
do {
ScanResult<String> scanResult = jedis.sscan("large:set", cursor, scanParams);
Set<String> members = new HashSet<>(scanResult.getResult());
// 处理这批数据
cursor = scanResult.getCursor();
} while (!cursor.equals("0"));
⚠️ 坑点 2:集合运算阻塞
java
// ❌ 问题:大集合运算会阻塞 Redis
Set<String> result = jedis.sinter("set1", "set2"); // 两个集合各有百万元素
// ✅ 解决方案:使用 SINTERSTORE 异步计算
jedis.sinterstore("result:set", "set1", "set2");
// 后续异步获取结果
五、Sorted Set(有序集合)
Sorted Set 是 Set 的升级版,每个元素都会关联一个 double 类型的分数,Redis 通过分数为集合中的成员进行排序。
5.1 基本操作
bash
# 添加元素
ZADD key score member
# 获取指定范围元素(按分数)
ZRANGE key start stop [WITHSCORES]
# 获取指定范围元素(按分数倒序)
ZREVRANGE key start stop [WITHSCORES]
# 获取元素分数
ZSCORE key member
# 获取元素排名
ZRANK key member
# 删除元素
ZREM key member
# 获取集合元素个数
ZCARD key
# 增加元素分数
ZINCRBY key increment member
# 按分数范围获取
ZRANGEBYSCORE key min max
# 按分数范围删除
ZREMRANGEBYSCORE key min max
Java 示例:
java
import redis.clients.jedis.Jedis;
import redis.clients.jedis.resps.Tuple;
import java.util.List;
public class ZSetExample {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
// 排行榜系统
jedis.zadd("game:leaderboard", 1500, "player1");
jedis.zadd("game:leaderboard", 2300, "player2");
jedis.zadd("game:leaderboard", 1800, "player3");
jedis.zadd("game:leaderboard", 2100, "player4");
// 获取前 3 名(倒序)
List<Tuple> top3 = jedis.zrevrangeWithScores("game:leaderboard", 0, 2);
System.out.println("排行榜前三名:");
int rank = 1;
for (Tuple tuple : top3) {
System.out.println(rank++ + ". " + tuple.getElement() +
" - 分数:" + tuple.getScore());
}
// 获取某个玩家的排名
Long playerRank = jedis.zrevrank("game:leaderboard", "player3");
System.out.println("player3 排名:" + (playerRank + 1));
// 增加分数
jedis.zincrby("game:leaderboard", 500, "player1");
// 延迟队列(按时间戳排序)
long timestamp = System.currentTimeMillis() + 60000; // 1分钟后执行
jedis.zadd("delay:queue", timestamp, "task:1001");
// 获取到期任务
long now = System.currentTimeMillis();
List<Tuple> tasks = jedis.zrangeByScoreWithScores("delay:queue", 0, now);
for (Tuple task : tasks) {
System.out.println("执行任务:" + task.getElement());
jedis.zrem("delay:queue", task.getElement());
}
jedis.close();
}
}
5.2 应用场景
- 排行榜:游戏排行榜、热搜榜、销量排行
- 延迟队列:定时任务、延迟消息
- 优先级队列:任务调度系统
- 时间线:微博关注时间线
5.3 常见坑点
⚠️ 坑点 1:分数精度问题
java
// ⚠️ 注意:分数是 double 类型,存在精度问题
jedis.zadd("scores", 0.1 + 0.2, "item1"); // 可能不等于 0.3
// ✅ 建议:使用整数分数
jedis.zadd("scores", 100, "item1"); // 使用整数避免精度问题
⚠️ 坑点 2:ZRANGE 大范围查询
java
// ❌ 危险操作:查询大有序集合全部数据
List<String> allMembers = jedis.zrange("large:zset", 0, -1);
// ✅ 正确做法:分页查询
int pageSize = 100;
int page = 1;
List<String> pageMembers = jedis.zrange("large:zset",
(page - 1) * pageSize, page * pageSize - 1);
⚠️ 坑点 3:排行榜数据过期问题
java
// ❌ 问题:排行榜数据一直累积
jedis.zadd("daily:leaderboard", score, player);
// ✅ 解决方案:定期清理或使用日期作为 key
String today = LocalDate.now().toString();
jedis.zadd("leaderboard:" + today, score, player);
jedis.expire("leaderboard:" + today, 86400 * 7); // 保留 7 天
六、Redis 使用最佳实践
1. Key 命名规范
java
// ✅ 推荐的命名规范
// 格式:业务模块:对象类型:对象ID:属性
jedis.set("user:profile:1001:name", "张三");
jedis.set("order:detail:20231226001", orderJson);
jedis.hset("product:info:5001", "price", "99.99");
// ❌ 避免的命名方式
jedis.set("u1001", "张三"); // 不清晰
jedis.set("user_profile_1001_name", "张三"); // 使用下划线不如冒号清晰
2. 设置合理的过期时间
java
// ✅ 始终设置过期时间,避免内存泄漏
jedis.setex("session:1001", 1800, sessionData); // 30分钟
jedis.expire("cache:product:1001", 3600); // 1小时
// ❌ 避免永久存储临时数据
jedis.set("temp:data", value); // 没有设置过期时间
3. 使用连接池
java
// ✅ 使用连接池
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(100);
config.setMaxIdle(20);
config.setMinIdle(10);
config.setTestOnBorrow(true);
JedisPool jedisPool = new JedisPool(config, "localhost", 6379);
try (Jedis jedis = jedisPool.getResource()) {
jedis.set("key", "value");
}
// ❌ 避免每次创建新连接
Jedis jedis = new Jedis("localhost", 6379); // 性能差
4. 批量操作优化
java
// ✅ 使用 Pipeline 批量操作
Pipeline pipeline = jedis.pipelined();
for (int i = 0; i < 1000; i++) {
pipeline.set("key:" + i, "value:" + i);
}
pipeline.sync();
// ❌ 避免循环单次操作
for (int i = 0; i < 1000; i++) {
jedis.set("key:" + i, "value:" + i); // 1000次网络往返
}
5. 监控关键指标
bash
# 查看内存使用
INFO memory
# 查看慢查询
SLOWLOG GET 10
# 查看客户端连接
CLIENT LIST
# 查看键空间统计
INFO keyspace
七、总结
数据结构选择指南
| 场景 | 推荐数据结构 | 原因 |
|---|---|---|
| 缓存对象 | String / Hash | String 简单,Hash 节省内存 |
| 计数器 | String (INCR) | 原子操作,性能高 |
| 消息队列 | List / Stream | List 简单,Stream 功能强大 |
| 排行榜 | Sorted Set | 自动排序,查询高效 |
| 去重 | Set | 自动去重,集合运算 |
| 标签系统 | Set | 支持交并差运算 |
| 延迟队列 | Sorted Set | 按时间戳排序 |
核心避坑要点
- 避免大 Key:单个 Key 不超过 10KB,集合类型不超过 5000 个元素
- 设置过期时间:防止内存泄漏,过期时间加随机值防止雪崩
- 使用连接池:避免频繁创建连接
- 批量操作:使用 Pipeline、MGET、MSET 减少网络开销
- 避免全量查询:使用 SCAN 代替 KEYS,分页查询大集合
- 监控慢查询:定期检查 SLOWLOG,优化慢查询
- 合理使用数据结构:根据场景选择最合适的数据结构
📚 参考资料
💡 提示:本文持续更新中,欢迎关注获取最新内容!
🔥 如果觉得有帮助,请点赞、收藏、关注三连支持!
标签 :Redis 数据结构 缓存 NoSQL 后端开发