🧠 字节跳动面试: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.c 和 t_hash.c,那才是真正的宝藏代码。