Redis数据结构深度解析:5种基本类型+3种高级类型完全掌握!

难度:⭐⭐⭐ | 适合人群:想深入理解Redis的开发者


💥 开场:一次技术选型的争论

时间: 周三下午技术评审会
地点: 会议室
事件: 新项目技术选型

产品经理: "我们要做一个电商系统,需要商品列表、购物车、排行榜..."

我: "这些都存MySQL吧。"

哈吉米: "等等,用MySQL做缓存?性能能行吗?"

我: "那用什么?" 🤔

南北绿豆: "Redis啊!专业的缓存中间件。"

我: "Redis不就是个Key-Value数据库吗?能做这么多功能?"

阿西噶阿西: "Redis可不是简单的Key-Value!它支持5种基本数据类型,每种都有不同的使用场景。"

我: "5种?都是什么?" 😯

哈吉米: "来,我给你讲讲..."


🎯 第一问:为什么Redis这么快?

四大原因

南北绿豆: "Redis快的原因有四个。"

1. 内存存储

MySQL(磁盘)
    读取速度:毫秒级(ms)
    
Redis(内存)
    读取速度:微秒级(μs)
    
速度差距:1000倍!

2. 单线程模型

多线程数据库:
    线程1、线程2、线程3...
    ├─ 需要加锁
    ├─ 上下文切换
    └─ 竞争资源

Redis单线程:
    一个线程处理所有请求
    ├─ 不需要加锁
    ├─ 不需要切换
    └─ 顺序执行

注意: Redis 6.0引入了多线程,但只用于网络IO,核心数据处理还是单线程


3. 高效的数据结构

不是简单的哈希表!

每种数据类型都有多种底层实现:
- String → SDS(简单动态字符串)
- List → quicklist(快速列表)
- Hash → hashtable/ziplist
- Set → hashtable/intset
- ZSet → skiplist(跳表)

根据数据量自动选择最优实现

4. IO多路复用

传统IO:
    一个请求 = 一个线程
    10000个请求 = 10000个线程(爆炸)

IO多路复用(epoll):
    一个线程处理10000个请求
    监听所有连接,有数据就处理

📦 第二问:String字符串

基本使用

# 设置值
SET name "张三"

# 获取值
GET name
# 输出:"张三"

# 设置过期时间(秒)
SET session:123 "user-data" EX 3600

# 不存在才设置(分布式锁)
SET lock:order:123 "locked" NX EX 10

# 自增
SET counter 0
INCR counter
# 输出:1
INCR counter
# 输出:2

# 自减
DECR counter
# 输出:1

使用场景

场景1:缓存对象

@Service
public class UserService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    public User getUser(Long userId) {
        String key = "user:" + userId;
        
        // 1. 从Redis获取
        String json = redisTemplate.opsForValue().get(key);
        if (json != null) {
            return JSON.parseObject(json, User.class);
        }
        
        // 2. 从数据库获取
        User user = userDao.findById(userId);
        
        // 3. 存入Redis(过期时间1小时)
        redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 
            1, TimeUnit.HOURS);
        
        return user;
    }
}

场景2:计数器

@Service
public class ArticleService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    /**
     * 文章点赞
     */
    public void like(Long articleId) {
        String key = "article:like:" + articleId;
        redisTemplate.opsForValue().increment(key);  // 自增
    }
    
    /**
     * 获取点赞数
     */
    public Long getLikeCount(Long articleId) {
        String key = "article:like:" + articleId;
        String count = redisTemplate.opsForValue().get(key);
        return count == null ? 0 : Long.parseLong(count);
    }
}

场景3:分布式锁(简易版)

public boolean tryLock(String lockKey, String lockValue, long expireTime) {
    
    // SET key value NX EX seconds
    Boolean result = redisTemplate.opsForValue().setIfAbsent(
        lockKey, 
        lockValue, 
        expireTime, 
        TimeUnit.SECONDS
    );
    
    return Boolean.TRUE.equals(result);
}

// 使用
if (tryLock("lock:order:123", UUID.randomUUID().toString(), 10)) {
    try {
        // 执行业务逻辑
    } finally {
        // 释放锁
        redisTemplate.delete("lock:order:123");
    }
}

📋 第三问:List列表

基本使用

# 左侧插入
LPUSH list1 "a" "b" "c"
# 列表:["c", "b", "a"]

# 右侧插入
RPUSH list1 "d" "e"
# 列表:["c", "b", "a", "d", "e"]

# 左侧弹出
LPOP list1
# 输出:"c"
# 列表:["b", "a", "d", "e"]

# 右侧弹出
RPOP list1
# 输出:"e"
# 列表:["b", "a", "d"]

# 获取范围
LRANGE list1 0 -1
# 输出:["b", "a", "d"]

# 获取长度
LLEN list1
# 输出:3

使用场景

场景1:消息队列(简易版)

/**
 * 生产者
 */
public void sendMessage(String message) {
    redisTemplate.opsForList().rightPush("queue:message", message);
    System.out.println("发送消息:" + message);
}

/**
 * 消费者
 */
public void consumeMessage() {
    while (true) {
        // 阻塞式左侧弹出(没有消息会等待)
        String message = redisTemplate.opsForList().leftPop(
            "queue:message", 
            5, 
            TimeUnit.SECONDS
        );
        
        if (message != null) {
            System.out.println("消费消息:" + message);
            // 处理消息
        }
    }
}

场景2:文章列表(时间线)

@Service
public class ArticleService {
    
    /**
     * 发布文章
     */
    public void publishArticle(Long userId, Article article) {
        // 保存到数据库
        articleDao.save(article);
        
        // 添加到时间线(最新的在前面)
        String key = "timeline:user:" + userId;
        redisTemplate.opsForList().leftPush(key, article.getId().toString());
        
        // 只保留最新的100篇
        redisTemplate.opsForList().trim(key, 0, 99);
    }
    
    /**
     * 获取时间线
     */
    public List<Article> getTimeline(Long userId, int page, int size) {
        String key = "timeline:user:" + userId;
        
        // 分页获取文章ID
        int start = page * size;
        int end = start + size - 1;
        List<String> articleIds = redisTemplate.opsForList().range(key, start, end);
        
        // 根据ID批量查询文章
        return articleDao.findByIds(articleIds);
    }
}

🗂️ 第四问:Hash哈希

基本使用

# 设置字段
HSET user:1 name "张三"
HSET user:1 age "25"
HSET user:1 email "zhangsan@example.com"

# 批量设置
HMSET user:1 name "张三" age "25" email "zhangsan@example.com"

# 获取字段
HGET user:1 name
# 输出:"张三"

# 获取所有字段
HGETALL user:1
# 输出:
# 1) "name"
# 2) "张三"
# 3) "age"
# 4) "25"
# 5) "email"
# 6) "zhangsan@example.com"

# 删除字段
HDEL user:1 email

# 字段自增
HINCRBY user:1 age 1

使用场景

场景1:对象存储

阿西噶阿西: "存储对象时,Hash比String更节省内存!"

对比:

// 方式1:String存储(JSON)
String key = "user:1";
String json = "{\"name\":\"张三\",\"age\":25,\"email\":\"...\"}";
redisTemplate.opsForValue().set(key, json);
// 缺点:修改一个字段要重新序列化整个对象

// 方式2:Hash存储(推荐)
String key = "user:1";
Map<String, String> user = new HashMap<>();
user.put("name", "张三");
user.put("age", "25");
user.put("email", "zhangsan@example.com");
redisTemplate.opsForHash().putAll(key, user);
// 优点:可以只修改一个字段

修改字段:

// String方式:要重新设置整个JSON
String json = redisTemplate.opsForValue().get("user:1");
User user = JSON.parseObject(json, User.class);
user.setAge(26);
redisTemplate.opsForValue().set("user:1", JSON.toJSONString(user));

// Hash方式:直接修改字段
redisTemplate.opsForHash().put("user:1", "age", "26");

场景2:购物车

@Service
public class CartService {
    
    /**
     * 添加商品到购物车
     */
    public void addToCart(Long userId, Long productId, Integer quantity) {
        String key = "cart:" + userId;
        
        // Hash的key是商品ID,value是数量
        redisTemplate.opsForHash().put(key, 
            productId.toString(), 
            quantity.toString());
    }
    
    /**
     * 修改商品数量
     */
    public void updateQuantity(Long userId, Long productId, Integer quantity) {
        String key = "cart:" + userId;
        redisTemplate.opsForHash().put(key, productId.toString(), quantity.toString());
    }
    
    /**
     * 删除商品
     */
    public void removeFromCart(Long userId, Long productId) {
        String key = "cart:" + userId;
        redisTemplate.opsForHash().delete(key, productId.toString());
    }
    
    /**
     * 获取购物车
     */
    public Map<Long, Integer> getCart(Long userId) {
        String key = "cart:" + userId;
        Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
        
        Map<Long, Integer> cart = new HashMap<>();
        for (Map.Entry<Object, Object> entry : entries.entrySet()) {
            cart.put(Long.parseLong(entry.getKey().toString()), 
                    Integer.parseInt(entry.getValue().toString()));
        }
        return cart;
    }
    
    /**
     * 获取购物车商品数量
     */
    public Integer getCartSize(Long userId) {
        String key = "cart:" + userId;
        return redisTemplate.opsForHash().size(key).intValue();
    }
}

🎲 第五问:Set集合

基本使用

# 添加元素
SADD tags:article:1 "Java" "Redis" "Spring"

# 查看所有元素
SMEMBERS tags:article:1
# 输出:["Java", "Redis", "Spring"]

# 判断元素是否存在
SISMEMBER tags:article:1 "Java"
# 输出:1(存在)

# 删除元素
SREM tags:article:1 "Redis"

# 获取元素数量
SCARD tags:article:1
# 输出:2

# 集合运算
SADD set1 "a" "b" "c"
SADD set2 "b" "c" "d"

# 交集
SINTER set1 set2
# 输出:["b", "c"]

# 并集
SUNION set1 set2
# 输出:["a", "b", "c", "d"]

# 差集
SDIFF set1 set2
# 输出:["a"]

使用场景

场景1:标签系统

@Service
public class TagService {
    
    /**
     * 给文章添加标签
     */
    public void addTags(Long articleId, String... tags) {
        String key = "article:tags:" + articleId;
        redisTemplate.opsForSet().add(key, tags);
    }
    
    /**
     * 获取文章的所有标签
     */
    public Set<String> getTags(Long articleId) {
        String key = "article:tags:" + articleId;
        return redisTemplate.opsForSet().members(key);
    }
    
    /**
     * 查找相似文章(共同标签最多的)
     */
    public List<Long> findSimilarArticles(Long articleId, int limit) {
        String key = "article:tags:" + articleId;
        
        // 获取当前文章的标签
        Set<String> tags = redisTemplate.opsForSet().members(key);
        
        // 遍历其他文章,计算共同标签数
        // 返回共同标签最多的文章
        // ...
    }
}

场景2:共同关注

@Service
public class FollowService {
    
    /**
     * 关注用户
     */
    public void follow(Long userId, Long followUserId) {
        String key = "user:follow:" + userId;
        redisTemplate.opsForSet().add(key, followUserId.toString());
    }
    
    /**
     * 获取共同关注
     */
    public Set<String> getCommonFollows(Long userId1, Long userId2) {
        String key1 = "user:follow:" + userId1;
        String key2 = "user:follow:" + userId2;
        
        // 求交集
        return redisTemplate.opsForSet().intersect(key1, key2);
    }
    
    /**
     * 可能认识的人(朋友的朋友)
     */
    public Set<String> getMayKnow(Long userId) {
        String key = "user:follow:" + userId;
        
        // 获取我关注的人
        Set<String> myFollows = redisTemplate.opsForSet().members(key);
        
        Set<String> mayKnow = new HashSet<>();
        
        // 遍历我关注的人,获取他们关注的人
        for (String followId : myFollows) {
            String followKey = "user:follow:" + followId;
            Set<String> theirFollows = redisTemplate.opsForSet().members(followKey);
            
            // 求差集(他们关注的 - 我关注的 - 我自己)
            theirFollows.removeAll(myFollows);
            theirFollows.remove(userId.toString());
            
            mayKnow.addAll(theirFollows);
        }
        
        return mayKnow;
    }
}

🏆 第六问:ZSet有序集合

基本使用

# 添加元素(score member)
ZADD rank 100 "张三"
ZADD rank 95 "李四"
ZADD rank 98 "王五"

# 获取排名(从高到低)
ZREVRANGE rank 0 -1 WITHSCORES
# 输出:
# 1) "张三"
# 2) "100"
# 3) "王五"
# 4) "98"
# 5) "李四"
# 6) "95"

# 获取排名(从低到高)
ZRANGE rank 0 -1 WITHSCORES

# 获取指定元素的分数
ZSCORE rank "张三"
# 输出:"100"

# 获取指定元素的排名
ZREVRANK rank "张三"
# 输出:0(第一名,从0开始)

# 增加分数
ZINCRBY rank 5 "李四"
# 李四的分数变成100

# 获取分数范围内的元素
ZRANGEBYSCORE rank 95 100

# 删除元素
ZREM rank "李四"

使用场景

场景1:排行榜

@Service
public class RankService {
    
    /**
     * 更新用户积分
     */
    public void updateScore(Long userId, Integer score) {
        String key = "rank:score";
        redisTemplate.opsForZSet().add(key, userId.toString(), score);
    }
    
    /**
     * 增加积分
     */
    public void incrScore(Long userId, Integer delta) {
        String key = "rank:score";
        redisTemplate.opsForZSet().incrementScore(key, userId.toString(), delta);
    }
    
    /**
     * 获取排行榜(前10名)
     */
    public List<RankVO> getTopRank(int top) {
        String key = "rank:score";
        
        // 获取分数最高的N个(倒序)
        Set<ZSetOperations.TypedTuple<String>> tuples = 
            redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, top - 1);
        
        List<RankVO> result = new ArrayList<>();
        int rank = 1;
        
        for (ZSetOperations.TypedTuple<String> tuple : tuples) {
            RankVO vo = new RankVO();
            vo.setRank(rank++);
            vo.setUserId(Long.parseLong(tuple.getValue()));
            vo.setScore(tuple.getScore().intValue());
            result.add(vo);
        }
        
        return result;
    }
    
    /**
     * 获取用户排名
     */
    public Integer getUserRank(Long userId) {
        String key = "rank:score";
        
        // 获取排名(倒序,分数高的在前)
        Long rank = redisTemplate.opsForZSet().reverseRank(key, userId.toString());
        
        return rank == null ? null : rank.intValue() + 1;  // 排名从1开始
    }
    
    /**
     * 获取用户积分
     */
    public Integer getUserScore(Long userId) {
        String key = "rank:score";
        Double score = redisTemplate.opsForZSet().score(key, userId.toString());
        return score == null ? 0 : score.intValue();
    }
}

场景2:延迟队列

@Service
public class DelayQueueService {
    
    /**
     * 添加延迟任务
     */
    public void addDelayTask(String taskId, long delaySeconds) {
        String key = "delay:queue";
        
        // 分数是执行时间戳
        long executeTime = System.currentTimeMillis() + delaySeconds * 1000;
        
        redisTemplate.opsForZSet().add(key, taskId, executeTime);
        System.out.println("添加延迟任务:" + taskId + "," + delaySeconds + "秒后执行");
    }
    
    /**
     * 消费延迟任务
     */
    public void consumeDelayTask() {
        String key = "delay:queue";
        
        while (true) {
            long now = System.currentTimeMillis();
            
            // 获取分数小于当前时间的任务(到期的任务)
            Set<String> tasks = redisTemplate.opsForZSet().rangeByScore(key, 0, now, 0, 1);
            
            if (tasks.isEmpty()) {
                try {
                    Thread.sleep(1000);  // 没有任务,等待1秒
                } catch (InterruptedException e) {
                    break;
                }
                continue;
            }
            
            String taskId = tasks.iterator().next();
            
            // 删除任务(使用ZREM,避免重复消费)
            Long removed = redisTemplate.opsForZSet().remove(key, taskId);
            
            if (removed > 0) {
                System.out.println("执行延迟任务:" + taskId);
                // 处理任务
                processTask(taskId);
            }
        }
    }
    
    private void processTask(String taskId) {
        // 任务处理逻辑
        System.out.println("任务处理完成:" + taskId);
    }
}

测试:

// 添加任务
delayQueueService.addDelayTask("task1", 5);   // 5秒后执行
delayQueueService.addDelayTask("task2", 10);  // 10秒后执行

// 启动消费者
delayQueueService.consumeDelayTask();

// 输出:
// 添加延迟任务:task1,5秒后执行
// 添加延迟任务:task2,10秒后执行
// (5秒后)执行延迟任务:task1
// 任务处理完成:task1
// (再过5秒)执行延迟任务:task2
// 任务处理完成:task2

🎨 第七问:高级数据类型

Bitmap(位图)

使用场景: 签到、统计

/**
 * 用户签到
 */
public void signIn(Long userId, LocalDate date) {
    String key = "sign:" + userId + ":" + date.getYear() + ":" + date.getMonthValue();
    int day = date.getDayOfMonth();
    
    // 设置第day位为1
    redisTemplate.opsForValue().setBit(key, day - 1, true);
}

/**
 * 检查是否签到
 */
public boolean hasSignIn(Long userId, LocalDate date) {
    String key = "sign:" + userId + ":" + date.getYear() + ":" + date.getMonthValue();
    int day = date.getDayOfMonth();
    
    return Boolean.TRUE.equals(redisTemplate.opsForValue().getBit(key, day - 1));
}

/**
 * 获取本月签到次数
 */
public long getSignCount(Long userId, int year, int month) {
    String key = "sign:" + userId + ":" + year + ":" + month;
    
    // 统计1的个数
    return redisTemplate.execute((RedisCallback<Long>) connection -> 
        connection.bitCount(key.getBytes()));
}

HyperLogLog(基数统计)

使用场景: UV统计(不精确,但省内存)

/**
 * 记录用户访问
 */
public void recordVisit(String pageId, Long userId) {
    String key = "uv:" + pageId;
    redisTemplate.opsForHyperLogLog().add(key, userId.toString());
}

/**
 * 获取UV(独立访客数)
 */
public Long getUV(String pageId) {
    String key = "uv:" + pageId;
    return redisTemplate.opsForHyperLogLog().size(key);
}

优势:

  • 12KB内存统计2^64个不同元素
  • 误差率0.81%
  • 适合大数据量场景

GEO(地理位置)

使用场景: 附近的人、外卖配送

/**
 * 添加位置
 */
public void addLocation(Long userId, double longitude, double latitude) {
    String key = "location:users";
    
    // 添加地理位置
    redisTemplate.opsForGeo().add(key, 
        new Point(longitude, latitude), 
        userId.toString());
}

/**
 * 获取附近的人
 */
public List<Long> getNearbyUsers(double longitude, double latitude, double radius) {
    String key = "location:users";
    
    // 以指定坐标为中心,查找半径范围内的用户
    GeoResults<RedisGeoCommands.GeoLocation<String>> results = 
        redisTemplate.opsForGeo().radius(key,
            new Circle(new Point(longitude, latitude), 
                new Distance(radius, Metrics.KILOMETERS)),
            RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
                .includeDistance()  // 包含距离
                .sortAscending()    // 按距离升序
                .limit(10));        // 最多10个
    
    List<Long> userIds = new ArrayList<>();
    for (GeoResult<RedisGeoCommands.GeoLocation<String>> result : results) {
        userIds.add(Long.parseLong(result.getContent().getName()));
    }
    
    return userIds;
}

📊 第八问:数据类型选择指南

对比表格

数据类型使用场景优点缺点
String缓存对象、计数器、Session简单易用修改整个值
List消息队列、时间线、列表有序、支持阻塞查询慢
Hash对象存储、购物车节省内存、字段操作不支持过期单个字段
Set标签、去重、集合运算去重、集合操作无序
ZSet排行榜、延迟队列有序、范围查询内存占用高
Bitmap签到、在线状态极省内存只能存0/1
HyperLogLogUV统计超省内存有误差
GEO附近的人、外卖配送地理位置查询功能单一

选择决策树

你的需求是?
│
├─ 简单的Key-Value缓存
│  └─ 选择:String
│
├─ 存储对象,需要修改单个字段
│  └─ 选择:Hash
│
├─ 消息队列、列表
│  └─ 选择:List
│
├─ 去重、标签、集合运算
│  └─ 选择:Set
│
├─ 排行榜、有序数据
│  └─ 选择:ZSet
│
├─ 签到、布尔值统计
│  └─ 选择:Bitmap
│
├─ 大数据量UV统计
│  └─ 选择:HyperLogLog
│
└─ 地理位置相关
   └─ 选择:GEO

💡 知识点总结

Redis数据结构核心要点

5种基本类型

  1. String - 字符串(最基础)
  2. List - 列表(有序可重复)
  3. Hash - 哈希(对象存储)
  4. Set - 集合(去重)
  5. ZSet - 有序集合(排序)

3种高级类型

  1. Bitmap - 位图(省内存)
  2. HyperLogLog - 基数统计(省内存)
  3. GEO - 地理位置(LBS)

实战场景

  • 缓存:String、Hash
  • 排行榜:ZSet
  • 消息队列:List
  • 标签系统:Set
  • 签到:Bitmap
  • UV统计:HyperLogLog
  • 附近的人:GEO

选择原则

  • 根据业务场景选择
  • 考虑内存占用
  • 考虑操作复杂度

底层实现

  • 每种类型有多种底层实现
  • 自动选择最优实现
  • 这是Redis高性能的秘密

记忆口诀

String最简单,缓存和计数。
List做队列,时间线排序。
Hash存对象,购物车最优。
Set去重复,标签和集合。
ZSet有顺序,排行榜首选。
Bitmap省内存,签到用它行。
HyperLogLog统计UVGEO附近找的人。

🤔 常见面试题

Q1: Redis有哪些数据类型?

A:

基本类型(5种):
1. String - 字符串
2. List - 列表
3. Hash - 哈希
4. Set - 集合
5. ZSet - 有序集合

高级类型(3种):
1. Bitmap - 位图
2. HyperLogLog - 基数统计
3. GEO - 地理位置

Q2: ZSet的底层实现是什么?

A:

ZSet底层使用两种数据结构:

1. 跳表(skiplist)
   - 用于范围查询
   - 时间复杂度O(logN)
   
2. 哈希表(hashtable)
   - 用于通过member快速查分数
   - 时间复杂度O(1)

两种结构同时维护,空间换时间。

Q3: 什么场景用Hash,什么场景用String?

A:

String:
- 简单的Key-Value
- 整个对象很少修改
- 不需要单独操作字段

Hash:
- 对象有多个字段
- 需要频繁修改单个字段
- 节省内存

例如:
用户基本信息 → Hash(name、age等字段可单独修改)
用户Token → String(整体存储,很少修改)

💬 写在最后

从String到ZSet,我们全面学习了Redis的8种数据结构:

  • 💎 掌握了5种基本类型的使用
  • 🚀 了解了3种高级类型的场景
  • 💻 完成了多个实战案例
  • 📊 学会了如何选择合适的类型

这篇文章,希望能让你对Redis数据结构有清晰的认识!

如果这篇文章对你有帮助,请:

  • 👍 点赞支持
  • ⭐ 收藏备用
  • 🔄 转发分享
  • 💬 评论交流

感谢阅读,下一篇我们聊Redis持久化! 👋