Redis五种数据结构实战指南:如何用Spring Boot玩转缓存、队列、排行榜

掌握这五种数据结构,你就能像使用瑞士军刀一样灵活运用Redis,轻松应对各种业务挑战!

在很多开发者眼中,Redis只是一个"缓存"。但事实上,Redis之所以能成为后端系统的瑞士军刀,正是因为它提供了远超键值对的丰富数据结构。每种数据结构都对应着一种或几种特定问题的解决方案:

  • String:最简单的键值对,却能实现计数器、分布式锁
  • List:双向链表,天然支持队列、栈
  • Hash:对象存储,节省内存,方便操作单个字段
  • Set:无序且唯一,轻松搞定交集、并集,如共同好友
  • Sorted Set:有序集合,排行榜、延时队列的最佳选择

理解这些数据结构,并能在Spring Boot中灵活运用,是每个后端开发者的必备技能。


🌟 为什么数据结构是Redis的灵魂?

Redis的核心竞争力不在于它是缓存,而在于它提供了丰富的数据结构 。这些数据结构使得Redis不仅仅是一个简单的键值存储,而是一个多功能的内存数据平台

表格

数据结构 本质 优势 适用场景
String 键值对 最简单,最通用 缓存、计数器、分布式锁
List 双向链表 两端操作O(1) 队列、栈、最新列表
Hash 字段-值映射 节省内存,支持字段级更新 对象存储、购物车
Set 无序唯一集合 自动去重,支持集合运算 标签、共同好友、抽奖
Sorted Set 有序唯一集合 按分数排序,范围查询快 排行榜、延时队列、优先级队列

🔥 一、String:最基础,却最不简单

📌 内部结构

Redis内部使用SDS(简单动态字符串)存储,支持二进制安全,可以存储文本、图片、序列化对象等。最大容量512MB。

🛠️ Spring Boot实战

场景一:缓存用户基本信息

java 复制代码
@Service
public class UserService {
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    @Autowired
    private UserMapper userMapper;
    
    private static final String USER_KEY_PREFIX = "user:";
    
    public User getUserById(Long id) {
        String key = USER_KEY_PREFIX + id;
        String json = redisTemplate.opsForValue().get(key);
        if (json != null) {
            return JSON.parseObject(json, User.class);
        }
        
        User user = userMapper.selectById(id);
        if (user != null) {
            redisTemplate.opsForValue()
                .set(key, JSON.toJSONString(user), 30, TimeUnit.MINUTES);
        }
        return user;
    }
    
    public void updateUser(User user) {
        userMapper.updateById(user);
        // 更新后删除缓存,保证一致性
        redisTemplate.delete(USER_KEY_PREFIX + user.getId());
    }
}

场景二:分布式ID生成器

java 复制代码
@Component
public class IdGenerator {
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    public long nextId(String key) {
        return redisTemplate.opsForValue().increment(key);
    }
}

场景三:限流计数器

java 复制代码
public boolean rateLimit(String userId) {
    String key = "rate:limit:" + userId;
    Long count = redisTemplate.opsForValue().increment(key);
    if (count == 1) {
        redisTemplate.expire(key, 60, TimeUnit.SECONDS);
    }
    return count <= 100; // 每分钟最多100次
}

⚠️ 注意事项

  • 大Key问题:String过大(如存储大文本)会导致网络拥塞和慢查询,建议拆分。
  • 内存占用:SDS会预分配空间,大量短小字符串可能造成内存浪费。
  • 编码优化 :可用object encoding key查看编码(embstr/raw)。

📦 二、List:消息队列与栈的天然选择

📌 内部结构

Redis 3.2之后统一使用quicklist(双向链表+压缩列表混合),兼顾内存和性能。

🛠️ Spring Boot实战

场景一:简单消息队列

java 复制代码
// 生产者
redisTemplate.opsForList().leftPush("order:queue", orderId);

// 消费者(使用@Scheduled定期拉取)
@Scheduled(fixedDelay = 500)
public void consume() {
    String orderId = redisTemplate.opsForList().rightPop("order:queue");
    if (orderId != null) {
        processOrder(orderId);
    }
}

场景二:最新消息列表

java 复制代码
public void addFeed(Long userId, String feedId) {
    String key = "feed:user:" + userId;
    redisTemplate.opsForList().leftPush(key, feedId);
    redisTemplate.opsForList().trim(key, 0, 99); // 只保留前100条
}

⚠️ 注意事项

  • 队列堵塞:使用阻塞弹出时,务必设置超时时间。
  • 无ACK机制:消费者取出消息后若崩溃,消息会丢失。
  • 大列表 :避免LRANGE一次拉取过多元素,应分页或使用迭代器。

🧾 三、Hash:对象的完美归宿

📌 内部结构

当字段较少时使用压缩列表(ziplist),字段较多时转为哈希表(hashtable)。

🛠️ Spring Boot实战

场景一:存储购物车

java 复制代码
@Service
public class CartService {
    @Autowired
    private RedisTemplate redisTemplate;
    
    private static final String CART_KEY_PREFIX = "cart:";
    
    public void addItem(Long userId, Long skuId, Integer quantity) {
        String key = CART_KEY_PREFIX + userId;
        redisTemplate.opsForHash().increment(key, skuId.toString(), quantity);
    }
    
    public Map<Object, Object> getCart(Long userId) {
        String key = CART_KEY_PREFIX + userId;
        return redisTemplate.opsForHash().entries(key);
    }
    
    public void removeItem(Long userId, Long skuId) {
        String key = CART_KEY_PREFIX + userId;
        redisTemplate.opsForHash().delete(key, skuId.toString());
    }
}

场景二:存储对象字段(节省内存)

java 复制代码
// 存储用户信息
redisTemplate.opsForHash().put("user:1001", "name", "张三");
redisTemplate.opsForHash().put("user:1001", "age", "25");
redisTemplate.opsForHash().put("user:1001", "level", "3");

⚠️ 注意事项

  • 避免HGETALL :当Hash字段很多时(如上万),HGETALL会阻塞Redis。
  • 字段数量控制:Hash适合存储对象属性,但不宜存储大量字段。
  • 内存优化:字段少时使用ziplist编码,字段多时转为hashtable。

🔗 四、Set:标签与社交关系的利器

📌 内部结构

当元素均为整数且数量较少时使用整数集合(intset),否则使用哈希表。

🛠️ Spring Boot实战

场景一:共同好友/关注

java 复制代码
public Set<String> commonFollows(Long userIdA, Long userIdB) {
    String keyA = "follow:user:" + userIdA;
    String keyB = "follow:user:" + userIdB;
    return redisTemplate.opsForSet().intersect(keyA, keyB);
}

场景二:抽奖池

java 复制代码
public void joinLottery(Long userId, String activityId) {
    String key = "lottery:" + activityId;
    redisTemplate.opsForSet().add(key, userId.toString());
}

public String drawWinner(String activityId) {
    String key = "lottery:" + activityId;
    return redisTemplate.opsForSet().pop(key);
}

场景三:标签系统

java 复制代码
// 为文章添加标签
public void addTags(Long articleId, List<String> tags) {
    for (String tag : tags) {
        String key = "tag:" + tag;
        redisTemplate.opsForSet().add(key, articleId.toString());
    }
}

// 根据多个标签获取文章交集
public Set<String> getArticlesByTags(List<String> tags) {
    List<String> keys = tags.stream()
        .map(t -> "tag:" + t)
        .collect(Collectors.toList());
    return redisTemplate.opsForSet().intersect(keys);
}

⚠️ 注意事项

  • SMEMBERS危险 :当Set很大时,SMEMBERS会返回所有元素,导致阻塞。
  • 大集合运算SINTERSUNION等操作的时间复杂度为O(N),集合过大时需谨慎。
  • 内存优化:当元素都是整数时,intset编码节省内存。

🏆 五、Sorted Set:排行榜的王者

📌 内部结构

使用跳表(skiplist)和哈希表的组合,既支持按分数排序,又支持快速查找。

🛠️ Spring Boot实战

场景一:实时排行榜

java 复制代码
// 增加用户积分
public void addScore(Long userId, Integer score) {
    String key = "rank:score";
    redisTemplate.opsForZSet().incrementScore(key, userId.toString(), score);
}

// 获取Top 10
public Set<String> getTop10() {
    String key = "rank:score";
    return redisTemplate.opsForZSet().reverseRange(key, 0, 9);
}

// 获取用户排名
public Long getRank(Long userId) {
    String key = "rank:score";
    Long rank = redisTemplate.opsForZSet().reverseRank(key, userId.toString());
    return rank != null ? rank + 1 : null;
}

场景二:延时队列

java 复制代码
// 添加延时任务
public void addDelayedTask(String taskId, long delayMs) {
    long executeTime = System.currentTimeMillis() + delayMs;
    redisTemplate.opsForZSet().add("delay:queue", taskId, executeTime);
}

// 消费者扫描
@Scheduled(fixedDelay = 1000)
public void processDelayedTasks() {
    long now = System.currentTimeMillis();
    Set<String> tasks = redisTemplate.opsForZSet().rangeByScore("delay:queue", 0, now);
    for (String taskId : tasks) {
        Double removed = redisTemplate.opsForZSet().remove("delay:queue", taskId);
        if (removed != null && removed > 0) {
            handleTask(taskId);
        }
    }
}

场景三:带权重的消息队列

java 复制代码
public void sendMsg(String msg, int priority) {
    redisTemplate.opsForZSet().add("priority:queue", msg, priority);
}

public void consume() {
    Set<String> msgs = redisTemplate.opsForZSet().reverseRangeByScore(
        "priority:queue", 0, Integer.MAX_VALUE, 0, 1);
    // 处理...
}

⚠️ 注意事项

  • 分数精度:分数使用double类型,注意浮点数精度问题。
  • 大集合操作ZRANGEZREVRANGE可能拉取大量数据,分页使用limit参数。
  • 内存占用:跳表结构内存开销高于哈希,元素很多时考虑定期清理。

📊 六、五种数据结构对比与选型指南

数据结构 存储特点 适用场景 性能特点 Spring Boot API
String 二进制安全字符串 缓存、计数器、分布式锁 读写最快,适合单值操作 opsForValue()
List 双向链表 队列、栈、最新列表 两端操作O(1),中间操作慢 opsForList()
Hash 字段-值映射 对象存储、购物车 节省内存,适合频繁修改个别字段 opsForHash()
Set 无序唯一集合 标签、共同好友、抽奖 集合运算高效,去重 opsForSet()
Sorted Set 有序唯一集合 排行榜、延时队列、优先级队列 按分数排序,范围查询快 opsForZSet()

🧭 选型建议

  • 单纯缓存对象:String(JSON)或Hash(字段频繁更新)
  • 队列场景:List(简单)或Stream(可靠,Redis 5.0+)
  • 集合运算:Set(无序)或Sorted Set(有序)
  • 需要按分数排序:Sorted Set

💡 七、最佳实践与避坑指南

  1. 避免大Key:不要存储过大的String或List,建议拆分
  2. 合理设置过期时间:避免内存无限增长
  3. 监控热Key :使用redis-cli --hotkeys监控
  4. 使用Pipeline减少网络开销 :批量操作时使用executePipelined()
  5. 使用Lua脚本保证原子性:复杂操作时使用Lua
  6. 避免过度使用Hash:字段过多时考虑使用JSON
  7. 定期清理过期数据 :使用ZREMRANGEBYSCORE清理Sorted Set

🎯 八、总结

Redis的五种基本数据结构是通往高阶使用的基石。在Spring Boot项目中,通过RedisTemplate及其子类可以轻松集成。但要真正用好它们,必须深入理解每种结构的内在特性、命令的时间复杂度以及内存模型。

"Redis不是缓存,而是内存中的数据库。"

掌握这些数据结构,你就能像使用瑞士军刀一样灵活运用Redis,轻松应对各种业务挑战。

现在就行动:在下一个项目中,选择最适合的数据结构,让Redis成为你后端系统的得力助手!


点赞、收藏、转发 ,让更多开发者受益!也欢迎关注我的公众号 【卷毛的技术笔记】 ,每周一篇后端干货,从原理到实战,陪你一起进阶!

相关推荐
学以智用2 小时前
.NET Core 部署上线完整教程(Windows IIS / Linux / Docker)
后端·.net
共享家95272 小时前
实现简化的高性能并发内存池
开发语言·数据结构·c++·后端
黄昏恋慕黎明2 小时前
spring的IOC与DI
java·后端·spring
用户608186527902 小时前
WPF 命令 ICommand 从原理到实战
后端
liuqun03192 小时前
go进阶之gc
开发语言·后端·golang
李小狼lee2 小时前
以一个简单案例来讲解RAG
后端
程序员清风2 小时前
OpenAI创始人学AI的底层逻辑,普通人照着做就能上手!
java·后端·面试
元俭2 小时前
【Eino 框架入门】用 JSONL 实现会话持久化
后端
Memory_荒年2 小时前
Netty面试终极指南:从“Hello World”到源码深处
java·后端