8.2 Redis核心数据结构

面试重要程度:⭐⭐⭐⭐⭐

常见提问方式: Redis底层实现、跳跃表原理、压缩列表优化

预计阅读时间:45分钟

🎯 String、Hash、List、Set、ZSet底层实现

String类型底层实现

面试官: "Redis的String类型底层是如何实现的?"

必答要点:

SDS(Simple Dynamic String)结构:

// Redis 6.0+ SDS结构
struct sdshdr8 {
    uint8_t len;        // 已使用长度
    uint8_t alloc;      // 总分配长度
    unsigned char flags; // 类型标识
    char buf[];         // 字符数组
};

// SDS相比C字符串的优势:
/**
 * 1. O(1)获取字符串长度(len字段)
 * 2. 杜绝缓冲区溢出(alloc字段控制)
 * 3. 减少修改字符串时的内存重分配次数
 * 4. 二进制安全(可存储任意数据)
 * 5. 兼容部分C字符串函数
 */

String类型编码方式:

/**
 * String类型的三种编码
 */
public class RedisStringEncoding {
    
    /**
     * 1. REDIS_ENCODING_INT
     * 条件:字符串是整数且在long范围内
     * 优势:节省内存,整数运算更快
     */
    public void intEncoding() {
        // SET counter 100
        // 存储为long类型,占用8字节
        jedis.set("counter", "100");
        jedis.incr("counter");  // 直接进行整数运算
    }
    
    /**
     * 2. REDIS_ENCODING_EMBSTR  
     * 条件:字符串长度 <= 44字节(Redis 6.0)
     * 优势:RedisObject和SDS在连续内存中,减少内存碎片
     */
    public void embstrEncoding() {
        // SET name "张三"
        // RedisObject和SDS一次分配,提高缓存局部性
        jedis.set("name", "张三");
    }
    
    /**
     * 3. REDIS_ENCODING_RAW
     * 条件:字符串长度 > 44字节
     * 特点:RedisObject和SDS分别分配内存
     */
    public void rawEncoding() {
        // SET description "很长的描述信息..."
        String longStr = "很长的描述信息".repeat(20);
        jedis.set("description", longStr);
    }
}

Hash类型底层实现

Hash类型的两种编码:

/**
 * Hash类型编码转换
 */
public class RedisHashEncoding {
    
    /**
     * 1. REDIS_ENCODING_ZIPLIST(压缩列表)
     * 条件:
     * - 所有键值对的键和值字符串长度都小于64字节
     * - 键值对数量少于512个
     */
    public void ziplistEncoding() {
        // HSET user:1 name "张三" age "25" city "北京"
        Map<String, String> user = new HashMap<>();
        user.put("name", "张三");
        user.put("age", "25");
        user.put("city", "北京");
        jedis.hmset("user:1", user);
        
        // 内存布局紧凑,节省空间
        // 但查找时间复杂度为O(n)
    }
    
    /**
     * 2. REDIS_ENCODING_HT(哈希表)
     * 条件:不满足ziplist条件时自动转换
     */
    public void hashtableEncoding() {
        // 当键值对数量或长度超过阈值时转换
        Map<String, String> largeUser = new HashMap<>();
        for (int i = 0; i < 600; i++) {
            largeUser.put("field" + i, "value" + i);
        }
        jedis.hmset("user:large", largeUser);
        
        // 查找时间复杂度为O(1)
        // 但内存开销较大
    }
}

List类型底层实现

List类型编码演进:

/**
 * List类型编码(Redis版本演进)
 */
public class RedisListEncoding {
    
    /**
     * Redis 3.0之前:ZIPLIST + LINKEDLIST
     */
    public void oldEncoding() {
        // 小数据量:ZIPLIST(压缩列表)
        // 大数据量:LINKEDLIST(双向链表)
        
        // 问题:两种数据结构差异大,维护复杂
    }
    
    /**
     * Redis 3.2+:QUICKLIST
     * 结合了ziplist和linkedlist的优点
     */
    public void quicklistEncoding() {
        // LPUSH mylist "a" "b" "c"
        jedis.lpush("mylist", "a", "b", "c");
        
        /**
         * QuickList结构:
         * - 由多个ziplist节点组成的双向链表
         * - 每个节点是一个ziplist
         * - 平衡了内存使用和操作效率
         */
    }
}

QuickList结构详解:

// QuickList节点结构
typedef struct quicklistNode {
    struct quicklistNode *prev;    // 前驱节点
    struct quicklistNode *next;    // 后继节点
    unsigned char *zl;             // ziplist指针
    unsigned int sz;               // ziplist字节数
    unsigned int count : 16;       // ziplist中元素个数
    unsigned int encoding : 2;     // 编码方式
    unsigned int container : 2;    // 容器类型
    unsigned int recompress : 1;   // 是否压缩
    unsigned int attempted_compress : 1; // 压缩尝试标志
    unsigned int extra : 10;       // 预留字段
} quicklistNode;

// QuickList主结构
typedef struct quicklist {
    quicklistNode *head;           // 头节点
    quicklistNode *tail;           // 尾节点
    unsigned long count;           // 总元素数量
    unsigned long len;             // 节点数量
    int fill : QL_FILL_BITS;      // 每个节点的填充因子
    unsigned int compress : QL_COMP_BITS; // 压缩深度
} quicklist;

Set类型底层实现

/**
 * Set类型的两种编码
 */
public class RedisSetEncoding {
    
    /**
     * 1. REDIS_ENCODING_INTSET(整数集合)
     * 条件:
     * - 所有元素都是整数
     * - 元素数量不超过512个
     */
    public void intsetEncoding() {
        // SADD numbers 1 2 3 4 5
        jedis.sadd("numbers", "1", "2", "3", "4", "5");
        
        /**
         * IntSet特点:
         * - 有序存储(便于二分查找)
         * - 根据整数范围选择编码(int16/int32/int64)
         * - 内存紧凑,查找效率O(log n)
         */
    }
    
    /**
     * 2. REDIS_ENCODING_HT(哈希表)
     * 条件:不满足intset条件时转换
     */
    public void hashtableEncoding() {
        // SADD tags "java" "redis" "mysql" "spring"
        jedis.sadd("tags", "java", "redis", "mysql", "spring");
        
        // 查找、插入、删除都是O(1)
        // 但内存开销较大
    }
}

ZSet类型底层实现

面试官: "有序集合ZSet是如何实现的?跳跃表的原理是什么?"

ZSet的两种编码:

/**
 * ZSet类型编码
 */
public class RedisZSetEncoding {
    
    /**
     * 1. REDIS_ENCODING_ZIPLIST
     * 条件:
     * - 元素数量少于128个
     * - 所有member长度小于64字节
     */
    public void ziplistEncoding() {
        // ZADD leaderboard 100 "player1" 200 "player2"
        Map<String, Double> scores = new HashMap<>();
        scores.put("player1", 100.0);
        scores.put("player2", 200.0);
        jedis.zadd("leaderboard", scores);
        
        // 在ziplist中按score排序存储
        // member和score紧挨着存储
    }
    
    /**
     * 2. REDIS_ENCODING_SKIPLIST + HASH
     * 条件:不满足ziplist条件时转换
     * 同时使用跳跃表和哈希表
     */
    public void skiplistEncoding() {
        Map<String, Double> largeScores = new HashMap<>();
        for (int i = 0; i < 200; i++) {
            largeScores.put("player" + i, Math.random() * 1000);
        }
        jedis.zadd("large_leaderboard", largeScores);
        
        /**
         * 数据结构组合:
         * - SkipList:支持范围查询,O(log n)
         * - HashMap:支持O(1)的member查找
         */
    }
}

🚀 跳跃表与压缩列表深度解析

跳跃表(Skip List)原理

跳跃表结构:

// 跳跃表节点
typedef struct zskiplistNode {
    sds ele;                        // member对象
    double score;                   // 分值
    struct zskiplistNode *backward; // 后退指针
    struct zskiplistLevel {
        struct zskiplistNode *forward; // 前进指针
        unsigned long span;            // 跨度
    } level[];                      // 层级数组
} zskiplistNode;

// 跳跃表主结构
typedef struct zskiplist {
    struct zskiplistNode *header, *tail; // 头尾指针
    unsigned long length;                 // 节点数量
    int level;                           // 最大层数
} zskiplist;

跳跃表查找过程:

/**
 * 跳跃表查找算法模拟
 */
public class SkipListSearch {
    
    /**
     * 查找score=60的元素
     * 
     * 层级结构示例:
     * L3: 1 ---------> 50 ---------> 100
     * L2: 1 -> 20 ---> 50 -> 70 ---> 100
     * L1: 1 -> 20 -> 30 -> 50 -> 60 -> 70 -> 80 -> 100
     */
    public void searchProcess() {
        // 从最高层开始查找
        // L3: 1 -> 50,50 < 60,继续向前
        // L3: 50 -> 100,100 > 60,下降到L2
        
        // L2: 50 -> 70,70 > 60,下降到L1
        
        // L1: 50 -> 60,找到目标
        
        /**
         * 时间复杂度分析:
         * - 平均情况:O(log n)
         * - 最坏情况:O(n)(退化为链表)
         * - 空间复杂度:O(n)
         */
    }
}

跳跃表vs其他数据结构:

/**
 * 数据结构对比
 */
public class DataStructureComparison {
    
    public void comparison() {
        /**
         * 跳跃表 vs 平衡二叉树:
         * 
         * 跳跃表优势:
         * 1. 实现简单,无需复杂的旋转操作
         * 2. 范围查询效率高(有序链表特性)
         * 3. 并发友好,锁粒度小
         * 4. 内存局部性好
         * 
         * 平衡二叉树优势:
         * 1. 最坏情况性能保证O(log n)
         * 2. 内存使用更少(无冗余指针)
         */
        
        /**
         * Redis选择跳跃表的原因:
         * 1. 实现简单,代码维护成本低
         * 2. 支持高效的范围查询(ZRANGE命令)
         * 3. 平均性能接近平衡树
         * 4. 适合内存数据库的场景
         */
    }
}

压缩列表(ZipList)原理

压缩列表结构:

// ZipList整体结构
// <zlbytes><zltail><zllen><entry1><entry2>...<entryN><zlend>

typedef struct zlentry {
    unsigned int prevrawlensize; // 前一个entry长度所需字节数
    unsigned int prevrawlen;     // 前一个entry的长度
    unsigned int lensize;        // 当前entry长度所需字节数
    unsigned int len;            // 当前entry的长度
    unsigned int headersize;     // header大小
    unsigned char encoding;      // 编码类型
    unsigned char *p;           // 指向entry的指针
} zlentry;

压缩列表优化原理:

/**
 * 压缩列表内存优化
 */
public class ZipListOptimization {
    
    public void memoryOptimization() {
        /**
         * 内存节省策略:
         * 
         * 1. 紧凑存储:
         * - 连续内存分配,无内存碎片
         * - 无指针开销(普通链表每个节点需要前后指针)
         * 
         * 2. 变长编码:
         * - 根据数据大小选择最小的存储方式
         * - 整数:1/2/4/8字节编码
         * - 字符串:根据长度选择1/2/5字节长度前缀
         * 
         * 3. 特殊编码:
         * - 小整数直接编码在type字段中
         * - 短字符串优化存储
         */
    }
    
    public void cascadeUpdate() {
        /**
         * 连锁更新问题:
         * 
         * 场景:插入一个元素导致后续元素的prevlen字段需要扩展
         * 
         * 例如:
         * - 原来prevlen用1字节存储(< 254)
         * - 插入大元素后,prevlen需要5字节存储
         * - 可能引发连锁反应,多个元素需要重新编码
         * 
         * 解决:
         * - Redis会尽量避免这种情况
         * - 当ziplist过大时转换为其他数据结构
         */
    }
}

🔧 Redis内存优化实战

数据结构选择策略

/**
 * Redis数据结构选择指南
 */
@Service
public class RedisOptimizationService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 小对象存储优化
     */
    public void smallObjectOptimization() {
        // ✅ 使用Hash存储小对象(利用ziplist编码)
        Map<String, String> user = new HashMap<>();
        user.put("id", "1001");
        user.put("name", "张三");
        user.put("age", "25");
        redisTemplate.opsForHash().putAll("user:1001", user);
        
        // ❌ 避免为每个字段单独设置key
        // redisTemplate.opsForValue().set("user:1001:id", "1001");
        // redisTemplate.opsForValue().set("user:1001:name", "张三");
        // redisTemplate.opsForValue().set("user:1001:age", "25");
    }
    
    /**
     * 列表数据优化
     */
    public void listOptimization() {
        // ✅ 小列表使用List(quicklist编码)
        List<String> tags = Arrays.asList("java", "redis", "spring");
        redisTemplate.opsForList().leftPushAll("user:1001:tags", tags);
        
        // ✅ 大列表考虑分片
        for (int i = 0; i < 10000; i++) {
            int shardId = i % 10;  // 分成10个分片
            redisTemplate.opsForList().leftPush("large_list:" + shardId, "item" + i);
        }
    }
    
    /**
     * 有序集合优化
     */
    public void zsetOptimization() {
        // ✅ 小数据量使用ZSet(ziplist编码)
        ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
        zset.add("small_rank", "player1", 100);
        zset.add("small_rank", "player2", 200);
        
        // ✅ 大数据量考虑分桶
        long userId = 1001L;
        int bucket = (int) (userId % 100);  // 分成100个桶
        zset.add("rank_bucket:" + bucket, "user:" + userId, 500);
    }
}

内存使用监控

/**
 * Redis内存监控
 */
@Component
public class RedisMemoryMonitor {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 获取内存使用情况
     */
    public void memoryInfo() {
        Properties info = redisTemplate.getConnectionFactory()
            .getConnection().info("memory");
        
        /**
         * 关键指标:
         * used_memory: 已使用内存
         * used_memory_human: 人类可读格式
         * used_memory_rss: 系统分配的物理内存
         * used_memory_peak: 内存使用峰值
         * mem_fragmentation_ratio: 内存碎片率
         */
        
        String usedMemory = info.getProperty("used_memory_human");
        String fragmentation = info.getProperty("mem_fragmentation_ratio");
        
        System.out.println("已使用内存: " + usedMemory);
        System.out.println("内存碎片率: " + fragmentation);
    }
    
    /**
     * 分析key的内存使用
     */
    public void analyzeKeyMemory() {
        // 使用MEMORY USAGE命令(Redis 4.0+)
        RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
        
        String[] keys = {"user:1001", "large_list:0", "small_rank"};
        for (String key : keys) {
            Long memoryUsage = connection.memoryUsage(key.getBytes());
            System.out.println(key + " 内存使用: " + memoryUsage + " bytes");
        }
    }
}

💡 面试回答要点

标准回答模板

数据结构底层实现:

"Redis的五种数据类型都有多种底层编码实现:

String类型:int、embstr、raw三种编码,根据内容和长度自动选择
Hash类型:ziplist和hashtable,小数据用ziplist节省内存
List类型:使用quicklist,结合了ziplist和linkedlist的优点
Set类型:intset和hashtable,整数集合用intset优化
ZSet类型:ziplist和skiplist+hash,跳跃表支持高效范围查询

这种设计在保证功能的同时,针对不同场景进行了内存和性能优化。"

跳跃表原理:

"跳跃表是一种概率性数据结构,通过多层索引实现O(log n)的查找效率。
相比平衡二叉树,跳跃表实现简单,支持高效的范围查询,
内存局部性好,适合Redis的内存数据库场景。
Redis在ZSet中使用跳跃表配合哈希表,
既支持按score范围查询,又支持按member快速查找。"

本节核心要点:

  • ✅ 五种数据类型的底层编码实现
  • ✅ 跳跃表和压缩列表的原理和优化
  • ✅ 内存使用优化策略和监控方法
  • ✅ 数据结构选择的最佳实践

总结: 深入理解Redis数据结构的底层实现,有助于在实际项目中做出正确的数据结构选择,优化内存使用和查询性能

#java秋招面试#
Java面试圣经 文章被收录于专栏

Java面试圣经

全部评论
坐标南京,OD岗位多多,欢迎来聊
点赞 回复 分享
发布于 08-23 15:17 贵州

相关推荐

08-07 22:08
吉林大学 golang
内推__免笔试:同学,瞅瞅我司,医疗独角兽,校招刚开,名额有限,先到先得,我的主页最新动态,绿灯直达,免笔试~
查看7道真题和解析
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务