掌握这五种数据结构,你就能像使用瑞士军刀一样灵活运用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会返回所有元素,导致阻塞。 - 大集合运算 :
SINTER、SUNION等操作的时间复杂度为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类型,注意浮点数精度问题。
- 大集合操作 :
ZRANGE和ZREVRANGE可能拉取大量数据,分页使用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
💡 七、最佳实践与避坑指南
- 避免大Key:不要存储过大的String或List,建议拆分
- 合理设置过期时间:避免内存无限增长
- 监控热Key :使用
redis-cli --hotkeys监控 - 使用Pipeline减少网络开销 :批量操作时使用
executePipelined() - 使用Lua脚本保证原子性:复杂操作时使用Lua
- 避免过度使用Hash:字段过多时考虑使用JSON
- 定期清理过期数据 :使用
ZREMRANGEBYSCORE清理Sorted Set
🎯 八、总结
Redis的五种基本数据结构是通往高阶使用的基石。在Spring Boot项目中,通过RedisTemplate及其子类可以轻松集成。但要真正用好它们,必须深入理解每种结构的内在特性、命令的时间复杂度以及内存模型。
"Redis不是缓存,而是内存中的数据库。"
掌握这些数据结构,你就能像使用瑞士军刀一样灵活运用Redis,轻松应对各种业务挑战。
现在就行动:在下一个项目中,选择最适合的数据结构,让Redis成为你后端系统的得力助手!
点赞、收藏、转发 ,让更多开发者受益!也欢迎关注我的公众号 【卷毛的技术笔记】 ,每周一篇后端干货,从原理到实战,陪你一起进阶!