字节跳动面试:Redis 数据结构有哪些?分别怎么实现的?

🧠 字节跳动面试:Redis 数据结构有哪些?分别怎么实现的?

"Redis 支持哪些数据结构?" 当面试官在问这题时,我的脑子只闪过一句话: 「"这题......不就是字符串、列表、哈希、集合、有序集合嘛~"」

结果------面试官微微一笑。 这笑容,就像你答完小学数学题,却被要求推导出牛顿第二定律一样😅。 后来我才明白,这题真正考的不是"记忆力",而是**「底层原理 + 实战思维」**。

今天我们就像老朋友一样,聊聊这道"Redis 数据结构的灵魂拷问"背后的那些技术点,顺便上点代码,看看 Go 和 Java 里分别该怎么玩。


⚙️ 一、Redis 常见数据结构概览

类型 说明 底层实现
String 字符串(可以是数字、文本、二进制) SDS(简单动态字符串)
List 链表/队列 双端链表(quicklist)
Hash 键值对集合 ziplist / hashtable
Set 无序集合 intset / hashtable
ZSet(Sorted Set) 有序集合 skiplist + hashtable

看到这表别急着打哈欠😴,下面才是灵魂部分👇。


🧩 二、String ------ SDS 动态字符串结构

Redis 的 String 并不是简单的 C 字符串,而是自己造了一个叫 「SDS(Simple Dynamic String)」 的轮子。

它长这样:

c 复制代码
struct sdshdr {
    int len;    // 实际使用长度
    int free;   // 剩余可用空间
    char buf[]; // 数据内容
};

SDS 的优势是什么?✅

  • 支持二进制安全("\0" 不会截断)
  • 支持动态扩容(append 时自动扩容)
  • 减少内存碎片

🕵‍♂ 实战场景: 比如计数器、缓存字符串、JSON 数据、Session token 等,全部走 String。

👉 Java 实现示例

typescript 复制代码
public class RedisString {
    private StringBuilder data = new StringBuilder();

    public void append(String s) {
        data.append(s);
    }

    public String get() {
        return data.toString();
    }

    public static void main(String[] args) {
        RedisString s = new RedisString();
        s.append("hello");
        s.append(" world");
        System.out.println(s.get());
    }
}

👉 Go 实现示例

go 复制代码
type RedisString struct {
 buf []byte
}

func (s *RedisString) Append(str string) {
 s.buf = append(s.buf, str...)
}

func (s *RedisString) Get() string {
 return string(s.buf)
}

🤔 看出来了吗?这其实就是一个"能扩容的字节数组"。


📜 三、List ------ quicklist 双端链表

面试官常问:"Redis 的 List 是用什么实现的?" 很多人回答"链表",但其实是**「quicklist」**------它是"链表 + 压缩列表(ziplist)"的混合体!

每个节点存的是一小段连续内存(ziplist),再用双向链表串起来。 优点:兼顾随机访问和内存利用率。

🕵‍♂ 实战场景: 消息队列、时间序列数据、评论列表。

👉 Java 模拟实现

typescript 复制代码
import java.util.LinkedList;

public class RedisList {
    private LinkedList<String> list = new LinkedList<>();

    public void lpush(String value) {
        list.addFirst(value);
    }

    public String rpop() {
        return list.pollLast();
    }

    public static void main(String[] args) {
        RedisList q = new RedisList();
        q.lpush("task1");
        q.lpush("task2");
        System.out.println(q.rpop()); // task1
    }
}

👉 Go 模拟实现

go 复制代码
type RedisList struct {
 data []string
}

func (r *RedisList) LPush(v string) {
 r.data = append([]string{v}, r.data...)
}

func (r *RedisList) RPop() string {
 if len(r.data) == 0 {
  return ""
 }
 v := r.data[len(r.data)-1]
 r.data = r.data[:len(r.data)-1]
 return v
}

🧱 四、Hash ------ ziplist / hashtable

Redis 会根据数据量选择底层结构:

  • 小数据量:「ziplist」(紧凑存储)
  • 大数据量:「hashtable」 (类似 dict

这其实就是"空间换时间"的经典思路。

🕵‍♂ 实战场景: 用户信息(user:1:name=Tom)、商品属性缓存、对象缓存。

Java 代码示例

arduino 复制代码
import java.util.HashMap;
public class RedisHash {
    private HashMap<String, String> map = new HashMap<>();

    public void set(String key, String value) {
        map.put(key, value);
    }

    public String get(String key) {
        return map.get(key);
    }
}

Go 代码示例

go 复制代码
type RedisHash struct {
 data map[string]string
}

func (h *RedisHash) Set(k, v string) {
 if h.data == nil {
  h.data = make(map[string]string)
 }
 h.data[k] = v
}

func (h *RedisHash) Get(k string) string {
 return h.data[k]
}

🔢 五、Set ------ intset / hashtable

Set 是无序集合,底层实现分两种:

  • 全是整数:intset
  • 否则:hashtable

🕵‍♂ 实战场景: 点赞去重、黑名单、用户标签、抽奖池。

简单示例(Go)

go 复制代码
type RedisSet struct {
 data map[string]struct{}
}

func (s *RedisSet) Add(v string) {
 if s.data == nil {
  s.data = make(map[string]struct{})
 }
 s.data[v] = struct{}{}
}

func (s *RedisSet) Contains(v string) bool {
 _, ok := s.data[v]
 return ok
}

📈 六、ZSet ------ skiplist + hashtable

来了,Redis 里最有"算法气质"的结构 ------ 有序集合 ZSet。

它的核心是 「跳表(skiplist)+ 哈希表」

  • 哈希表用于快速查找成员
  • 跳表用于按分数排序和范围查询

比如排行榜、延迟任务调度、推荐系统排序,都离不开它。

Java 伪代码

arduino 复制代码
class ZSet {
    TreeMap<Double, String> scoreMap = new TreeMap<>();
    HashMap<String, Double> valueMap = new HashMap<>();

    void add(String member, double score) {
        scoreMap.put(score, member);
        valueMap.put(member, score);
    }

    String range(double min, double max) {
        return scoreMap.subMap(min, max).toString();
    }
}

Go 伪实现

go 复制代码
type ZSet struct {
 scores map[string]float64
}

func (z *ZSet) Add(member string, score float64) {
 if z.scores == nil {
  z.scores = make(map[string]float64)
 }
 z.scores[member] = score
}

func (z *ZSet) Range(min, max float64) []string {
 var res []string
 for k, v := range z.scores {
  if v >= min && v <= max {
   res = append(res, k)
  }
 }
 return res
}

💡 七、延伸思考:为什么 Redis 不直接用 ArrayList / HashMap?

这也是很多人忽略的"面试加分点"👇 Redis 设计的核心目标是:「内存效率 + 性能极致」。 它所有的数据结构都经过"空间压缩 + CPU 缓存友好"优化。

比如:

  • SDS 避免频繁 malloc/free
  • ziplist 减少指针开销
  • skiplist 用概率结构平衡时间复杂度和实现复杂度

这就是为什么 Redis 在做缓存、计数器、排行榜时,总是那么丝滑。


🧭 八、总结:从背诵到理解

📖 背:String / List / Hash / Set / ZSet 🧠 懂:SDS / quicklist / ziplist / skiplist 💪 用:计数器 / 队列 / 缓存对象 / 去重 / 排行榜

其实这道"Redis 数据结构有哪些"的问题,考的不是背诵,而是你能否从底层结构,联想到它在真实业务里的**「取舍与应用」**。

当你能说出:

"ZSet 是用跳表实现的,可以支持排行榜和延迟队列" 或者 "Hash 小时是 ziplist,大时是 hashtable,用于节省内存"

面试官那抹"神秘的微笑",就会变成赞许的点头😉。


「结尾彩蛋🎁」 如果你想继续深入 Redis 的实现,可以去读下它的 src/t_zset.ct_hash.c,那才是真正的宝藏代码。


相关推荐
9ilk3 小时前
【仿RabbitMQ的发布订阅式消息队列】--- 介绍
linux·笔记·分布式·后端·rabbitmq
Tech有道3 小时前
滴滴面试题:一道“轮询算法”的面试题,让我意识到自己太天真了
后端·面试
golang学习记3 小时前
Go 1.25 Flight Recorder:线上偶发问题的“时间回放”利器
后端
ZZHHWW4 小时前
Redis 主从复制详解
后端
ZZHHWW4 小时前
Redis 集群模式详解(上篇)
后端
EMQX4 小时前
技术实践:在基于 RISC-V 的 ESP32 上运行 MQTT over QUIC
后端
程序员蜗牛4 小时前
Java泛型里的T、E、K、V都是些啥玩意儿?
后端
CoderLemon4 小时前
一次因缺失索引引发的线上锁超时事故
后端
ZZHHWW4 小时前
Redis 集群模式详解(下篇)
后端