Redis 常用数据结构与实战避坑指南

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 应用场景

  1. 缓存对象:缓存用户信息、商品详情等
  2. 计数器:文章阅读量、点赞数、库存数量
  3. 分布式锁:使用 SETNX 实现简单的分布式锁
  4. Session 共享:分布式系统中的 Session 存储
  5. 限流:基于时间窗口的访问频率控制

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 应用场景

  1. 存储对象:用户信息、商品信息、配置信息
  2. 购物车:用户 ID 作为 key,商品 ID 作为 field,数量作为 value
  3. 统计信息:网站访问统计、用户行为统计

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 应用场景

  1. 消息队列:简单的生产者-消费者模型
  2. 最新列表:微博最新动态、文章最新评论
  3. 排行榜:配合 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 应用场景

  1. 标签系统:文章标签、商品标签
  2. 社交关系:共同好友、共同关注
  3. 去重:用户签到、点赞、投票
  4. 抽奖系统:随机抽取中奖用户

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 应用场景

  1. 排行榜:游戏排行榜、热搜榜、销量排行
  2. 延迟队列:定时任务、延迟消息
  3. 优先级队列:任务调度系统
  4. 时间线:微博关注时间线

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 按时间戳排序

核心避坑要点

  1. 避免大 Key:单个 Key 不超过 10KB,集合类型不超过 5000 个元素
  2. 设置过期时间:防止内存泄漏,过期时间加随机值防止雪崩
  3. 使用连接池:避免频繁创建连接
  4. 批量操作:使用 Pipeline、MGET、MSET 减少网络开销
  5. 避免全量查询:使用 SCAN 代替 KEYS,分页查询大集合
  6. 监控慢查询:定期检查 SLOWLOG,优化慢查询
  7. 合理使用数据结构:根据场景选择最合适的数据结构

📚 参考资料


💡 提示:本文持续更新中,欢迎关注获取最新内容!
🔥 如果觉得有帮助,请点赞、收藏、关注三连支持!


标签Redis 数据结构 缓存 NoSQL 后端开发

相关推荐
少云清2 小时前
【接口测试】1_PyMySQL模块 _数据库操作应用场景
数据库·代码实现
spssau2 小时前
正交试验设计全解析:从正交表生成到极差与方差分析
数据库·算法·机器学习
山峰哥2 小时前
SQL性能瓶颈破局:Explain分析+实战优化全攻略
大数据·数据库·sql·oracle·性能优化
幺零九零零2 小时前
Redis容器了解Docker底层
数据库·redis·docker
Vic101012 小时前
【无标题】
java·数据库·分布式
特立独行的猫a2 小时前
QT开发鸿蒙PC应用:第一个Qt Widget应用入门
数据库·qt·harmonyos·鸿蒙pc·qtwidget
l1t2 小时前
sqlite递归查询指定搜索顺序的方法
数据库·sql·sqlite·dfs·递归·cte
盛世宏博北京2 小时前
守护千年文脉:图书馆古籍库房自动化环境治理(温湿度 + 消毒)技术方案
服务器·数据库·自动化·图书馆温湿度监控
「光与松果」2 小时前
Mongodb 日常维护命令集
数据库·mongodb