面试高频问答(三):随便写写

浅浅的心里总结:

其实我感觉面试应该是不难的,一共就一个小时的面试时间。其实划分一下就还挺好的,半个小时基础知识回答时间,半个小时的手撕代码时间。前半个小时的时间好好做一下引导,超过半个小时还是很简单的(打个最简单的比方 智能指针->三个最常见的智能指针->着重介绍一下shareedptr->介绍一下为什么需要设计sharedptr->sharedptr会不会有线程安全的问题->weakptr又是怎么回事呢->sharedptr底层代码是怎么样子的呢->你会如何设计sharedptr)。那么到此,就是一个最简单的例子。

又或者(用过mysql吧->mysql有哪些数据引擎->为什么用innodb->它解决了什么问题?->那介绍一下他的事务隔离级别吧->介绍一下快照->那你是如何解决幻读的问题呢?->...)都有一个很明显的询问的流程,这也是大部分面试的时候打破沙锅问到底的一个整体流程,直到问到你不会为止。

至于实践怎么引导,那就得看语言的艺术。一般也就一些简单的几个方向,好好准备一下(是什么,为什么,怎么样。保证整个面试流程回答的专业知识的逻辑链条)。由于我还在学习的过程中,所以,我个人也是在不断学习专门深入一个方向进行仔细地研究了。

手撕代码的话,leetcode刷个300-400题就能应付大部分的面试手撕题了。

唉~真的好难哇。

C++篇

map为什么使用红黑树?他与其他的平衡二叉树有什么区别?(这个好好学一下,如果理解透了,很加分的!)

这是腾讯面试官问的,我又又又不知道。扎心了。。。。。我一下子忘记了平衡二叉树如果要平衡需要很大的消耗。下面详细介绍一下。

总结:红黑树在插入、删除的效率和查找性能之间取得了最佳平衡,同时实现相对简单,适合作为map的底层结构。而AVL树等其他平衡树因严格平衡或稳定性问题,未被C++标准库采用。这一设计选择反映了对动态操作频繁性的优化,以及对综合性能的权衡。

1. 红黑树与其他平衡二叉树的区别

(1)平衡条件的差异
  • 红黑树:通过颜色标记和路径约束(如确保任意路径的黑节点数相同)实现近似平衡,允许树的高度最多为2log(n+1)。这种宽松的平衡减少了插入和删除时的调整次数。(减少了多次的平衡操作,减少了大量的复杂的树的结构的调整时间)

  • AVL树:严格平衡,要求左右子树高度差不超过1,确保树的高度始终接近log(n)。这使得查找更快,但插入/删除可能需要更多旋转操作。(调整时间特别多,比较多的左旋右旋操作,那么如果在实际的应用场景,就会有很多的时间浪费)

(2)操作效率对比
  • 插入/删除:(主要的性能差异)

    • 红黑树通常需要O(1)次旋转(最多2次旋转完成插入,3次完成删除),调整频率更低。

    • AVL树可能需要O(log n)次旋转,且可能从插入点回溯到根节点调整平衡因子。

  • 查找

    • AVL树因更严格的平衡,查找效率略高(O(log n)的常数更小)。

    • 红黑树的查找效率稍逊,但差异在大多数场景中可以忽略。

(3)空间开销
  • 红黑树仅需1 bit存储颜色(如用布尔值表示),而AVL树需要存储平衡因子(通常占用2-3 bits)。对于大规模数据,红黑树的空间利用率更高。


2. 为什么选择红黑树?

(1)综合性能更优
  • map通常需要频繁的插入和删除(如动态维护键值对),红黑树在修改操作上的高效性更符合需求。

  • 虽然查找稍慢于AVL树,但实际应用中差距微小,而插入/删除的效率优势更为关键。

(2)实现复杂度与稳定性
  • 红黑树的调整操作(如颜色翻转和旋转)相对局部化,实现简单且稳定。

  • AVL树的严格平衡导致更复杂的调整逻辑,尤其在删除时可能需要多次回溯。

(3)避免极端退化
  • 相比非严格平衡结构,红黑树保证最坏情况下仍为O(log n)时间复杂度,适合标准库的高可靠性要求。


3. 其他平衡树的局限性

  • AVL树:严格平衡导致高维护成本,适合读多写少的场景(如数据库索引),但不符合map的典型使用模式。

  • B/B+树:更适合磁盘存储的大数据块场景(如数据库系统),内存中反而不如红黑树高效。

参考红黑树与AVL树对比-CSDN博客

可以延伸的知识点:详细介绍一下avl二叉树和红黑树是如何进行平衡调整的。

数据库篇

为什么要加这一篇章,因为暂时cpp问的东西大概我都熟悉了(?打个问号,下次不会了再收集hhh),但是数据库这一部分我是真的很难去回答(因为我只会用select、update语句哇!得专门重新抓一下基础知识了)。所以专门开这一篇章,好好从头开始有针对性的学习。前面的线程库暂时看了差不多了,以后有时间把源码拿出来分析一下。

redis有哪些数据结构?

这个问题,腾讯的面试官基本必问!我造了。。。老老实实学吧,世界上没有捷径!!!干就完了!!

常见的数据类型有五种:string、hash、list、set、zset

随着版本的更新增加了四种类型BitMap、Geo、HyperLogLop、Stream类型

在Redis中是怎么具体实现的?

String类型

用途
  1. 存储多样化数据

    • 字符串(文本、JSON、HTML

    • 整数(如计数器)

    • 浮点数(如实时统计值)

    • 二进制数据(如图片、序列化对象)

  2. 原子操作支持

    • INCR/DECR:自增/自减整数(如库存扣减)

    • INCRBY/DECRBY:按步长增减

    • APPEND:字符串追加

    • SETBIT/GETBIT:位操作(如用户签到、或者做一个布隆过滤器)


具体实现
1. Redis的String实现

Redis的String类型由两部分实现:

  • 整数编码(int
    当值为整数且范围在LONG_MINLONG_MAX时,直接存储为整数,避免字符串转换开销。

    set counter 100  // 内部存储为整数
    incr counter     // 直接操作整数,O(1)时间复杂度

  • 简单动态字符串(SDS,Simple Dynamic String)
    用于存储文本、二进制数据或大整数,核心优势如下:

    struct sdshdr {
        int len;     // 当前字符串长度(不含结尾'\0')
        int alloc;   // 分配的总容量(不含结尾'\0')
        char buf[];  // 实际数据(兼容C字符串函数)
    };
    • 与C字符串的关键区别

      特性C字符串 (char*)Redis SDS
      长度获取O(n)遍历到\0O(1)直接读取len字段
      二进制安全无法存储含\0的数据支持任意二进制数据
      内存预分配无,频繁修改需反复分配内存预分配策略减少内存分配次数
      惰性释放立即释放保留空间供未来使用
      自动扩容需手动处理自动按需扩展(最大1MB步长)

    为什么Redis不直接使用C++的std::string

    • C++的std::string虽支持存储\0,但其实现依赖具体标准库(如Copy-On-Write可能引发线程安全问题)。

    • SDS是专为Redis设计的通用结构,跨语言兼容避免依赖特定实现

2. 二进制安全示例

假设存储一个包含\0的二进制数据(如JPEG图片头0xFF 0xD8 0x00 0x0A):

SET image_data "\xff\xd8\x00\x0a..."  // SDS正确存储所有字节
GET image_data                       // 完整读取,不会因中间的0x00截断

应用场景
1. 缓存
  • JSON缓存(全量存储)

    SET user:1001 '{"name":"Alice", "age":30, "email":"alice@example.com"}'

    优点:单次读写,代码简单。
    缺点:更新部分字段需反序列化整个JSON。

  • 分字段存储(Hash优化)

    HMSET user:1001 name "Alice" age 30 email "alice@example.com"

    或通过MSET模拟分字段(需权衡键数量):

    MSET user:1001:name "Alice" user:1001:age 30 user:1001:email "alice@example.com"

    适用场景:频繁更新部分字段(如用户年龄)。

2. 计数器
  • 文章点赞数统计

    INCR article:1234:likes     // 原子自增,避免并发冲突

    高并发优化:结合管道(Pipeline)批量提交操作。

  • 限流器(每秒请求数限制)

    -- Lua脚本保证原子性
    local key = KEYS[1]
    local limit = tonumber(ARGV[1])
    local current = tonumber(redis.call('GET', key) or 0
    if current + 1 > limit then
        return 0
    else
        redis.call('INCR', key)
        redis.call('EXPIRE', key, 1)  -- 每秒重置
        return 1
    end

3. 分布式锁
  • 基础实现(SET + NX + EX)

    SET lock:order 123456 NX EX 30  # 设置锁,过期时间30秒

    关键参数

    • NX:仅当锁不存在时设置

    • EX:自动过期,防止死锁

  • 安全释放锁(Lua脚本)

    if redis.call("GET", KEYS[1]) == ARGV[1] then
        return redis.call("DEL", KEYS[1])
    else
        return 0
    end

    步骤

    1. 生成唯一客户端标识(如UUID + 线程ID)。

    2. 获取锁时设置该标识。

    3. 释放锁时校验标识,避免误删其他客户端的锁。

  • 锁续期(WatchDog机制)
    后台线程定期检测锁持有状态,若未完成操作则延长过期时间(Redisson库内置实现)。

List类型

用途
  1. 双向链表结构

    • 支持**头部(Left)尾部(Right)**的高效插入(LPUSH/RPUSH)与删除(LPOP/RPOP)。

    • 按索引访问(LINDEX)、范围截取(LRANGE)、长度查询(LLEN)。

  2. 动态数据集合

    • 存储有序元素集合,允许重复值。

    • 支持阻塞操作(BLPOP/BRPOP),实现生产消费模型的等待机制。


具体实现
1. Redis的List实现

Redis的List类型底层采用快速链表(QuickList),其核心设计结合了双向链表与**压缩列表(Ziplist)**的优势:

  • QuickList结构

    typedef struct quicklist {
        quicklistNode *head;          // 链表头节点
        quicklistNode *tail;          // 链表尾节点
        unsigned long count;          // 总元素数量
        unsigned long len;            // 节点数量
        int fill : 16;                // 单个ziplist的最大容量(由list-max-ziplist-size配置)
        unsigned int compress : 16;   // 节点压缩深度(由list-compress-depth配置)
    } 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;    // 编码方式(RAW=1, LZF=2)
        unsigned int container : 2;   // 数据容器类型(NONE=1, ZIPLIST=2)
        unsigned int recompress : 1;  // 是否被临时解压
    } quicklistNode;

  • 设计优势

    场景双向链表压缩列表(Ziplist)QuickList(混合结构)
    内存占用高(指针开销)低(连续内存,无指针)中等(平衡内存与操作效率)
    插入/删除效率O(1)(头尾操作)O(n)(需内存重排)头尾O(1),中间插入O(n)
    遍历效率O(n)O(n)O(n)
    适用场景频繁中间修改小数据、少修改综合高频头尾操作与小数据压缩存储
  • 配置调优

    • list-max-ziplist-size:控制单个ziplist的最大元素数量或内存大小(默认-2,即8 KB)。

    • list-compress-depth:压缩两端连续节点的数量(默认0,不压缩)。

2. 与C++ std::list的对比
  • 内存布局

    • C++ std::list:每个节点独立分配内存,含前后指针(64位系统每个指针占8字节)。

    • Redis QuickList:将多个元素打包进ziplist,减少指针开销(例如存储100个元素的ziplist仅需1个头尾指针)。

  • 性能差异

    • 头部插入10万次:

      Redis QuickList ≈ 2ms(内存连续,批量分配)  
      C++ std::list ≈ 15ms(频繁内存分配)  

应用场景
1. 消息队列
  • 基础队列模型

    • 生产者:LPUSH将任务插入队列头部。

    • 消费者:RPOP从队列尾部取出任务。

    LPUSH order:queue "task1"  # 生产者推送任务
    RPOP order:queue          # 消费者获取任务
  • 阻塞队列优化
    使用BRPOP避免消费者轮询空队列:

    BRPOP order:queue 30  # 阻塞30秒等待任务,超时返回nil
2. 最新消息列表
  • 固定长度存储
    通过LTRIM维护仅保留最近的N条数据:

    LPUSH news:latest "news3"  # 插入新消息
    LTRIM news:latest 0 9      # 仅保留前10条
  • 分页查询
    使用LRANGE实现分页:

    LRANGE articles 0 9   # 获取第1页(最新10条)
    LRANGE articles 10 19 # 获取第2页
3. 多消费者组(Pub/Sub替代方案)
  • 独立消费进度
    每个消费者组维护独立的索引位置:

    LPUSH log:stream "log1"      # 生产者插入日志
    LRANGE log:stream 0 -1       # 消费者A读取全部
    LTRIM log:stream 1 -1        # 消费者A处理完成后截断队列(需事务保证原子性)
4. 栈结构(LIFO)
  • 使用LPUSHLPOP模拟栈:

    LPUSH user:1001:history "pageA"  # 记录访问历史
    LPOP user:1001:history          # 后进先出

高级用法与限制
  1. 性能注意事项

    • 中间操作(如LINSERT:时间复杂度为O(n),避免在大列表中使用。

    • 超大元素(如10 MB的字符串):会导致ziplist退化为普通链表,内存碎片增加。

  2. 替代方案对比

    需求List类型Stream类型(更推荐)
    消息持久化需手动维护支持持久化、消费者组
    多消费者组需自行实现原生支持
    消息ACK确认不支持支持

总结

Redis List通过QuickList的混合结构,在内存效率与操作性能之间取得平衡,尤其适合高频头尾操作(如消息队列、最新动态)。其阻塞操作和原子性特性,使其成为轻量级实时系统的首选数据结构。但在需要消息持久化、多消费者组支持时,建议升级至Stream类型。

Hash类型

用途
  1. 结构化数据存储

    • 存储**字段-值(Field-Value)**的映射集合,类似编程语言中的字典或对象。

    • 支持对单个字段的原子操作(如HINCRBY),无需读取整个对象。

  2. 高效部分更新

    • 可单独修改某个字段,无需序列化/反序列化整个对象(如用户年龄更新)。

  3. 内存优化

    • 相比多个独立的String键,Hash通过共享键名减少内存开销(如user:1001:name vs. user:1001的字段)。


具体实现
1. Redis的Hash实现

Redis的Hash类型根据数据规模动态选择两种底层结构:

  • Ziplist(压缩列表)

    • 触发条件(由配置参数控制):

      • hash-max-ziplist-entries:字段数量 ≤ 512(默认)。

      • hash-max-ziplist-value:单个字段值大小 ≤ 64字节(默认)。

    • 内存布局

      // Ziplist内存结构(紧凑连续存储)
      [zlbytes][zltail][zllen][field1][value1][field2][value2]...[zlend]
      • 所有字段和值按插入顺序连续存储,无指针开销。

      • 字段和值作为相邻条目存储,查询时需遍历(时间复杂度O(n))。

  • Hashtable(哈希表)

    • 触发条件:字段数量或值大小超过ziplist阈值时自动转换。

    • 数据结构

      • 使用链地址法解决哈希冲突(类似Java HashMap)。

      • 渐进式Rehash:扩容时逐步迁移数据,避免长时间阻塞。

实现内存占用插入/删除效率查询效率(单字段)适用场景
Ziplist极低O(n)O(n)小数据、字段少且固定
Hashtable较高O(1)O(1)大数据、高频修改
2. 与String存储对象的对比

假设存储用户信息:

  • String方案

    SET user:1001:name "Alice" 
    SET user:1001:age "30"
    # 内存占用 = 2个键的元数据开销 + 键名字符串长度
  • Hash方案

    HSET user:1001 name "Alice" age 30
    # 内存占用 = 1个键的元数据 + 字段名的哈希表开销

内存节省示例(实测数据):

  • 存储100万个用户,每个用户10个字段:

    • String类型:≈ 1.2 GB

    • Hash类型(ziplist):≈ 600 MB


应用场景
1. 对象存储(用户、商品等)
  • 基础操作

    HMSET product:1001 name "Phone" price 599 stock 50  # 设置多个字段
    HGET product:1001 price                           # 获取单个字段
    HINCRBY product:1001 stock -1                     # 原子扣减库存

  • 部分更新优化
    仅修改必要字段,避免传输整个JSON:

    HSET user:1001 age 31  # 仅更新年龄字段
2. 购物车
  • 数据结构设计

    • Key:cart:用户ID

    • Field:商品ID

    • Value:商品数量

    HSET cart:1001 998 3   # 商品ID=998,数量=3
    HINCRBY cart:1001 998 -1  # 数量减1
  • 查询全部商品

    HGETALL cart:1001  # 返回所有商品ID和数量
3. 动态配置管理
  • 存储系统配置

    HSET config:global max_connections 5000 timeout 30
    HGET config:global max_connections  # 获取最大连接数
  • 热更新
    修改配置无需重启服务,直接更新Redis中的字段。

4. 聚合统计
  • 统计不同属性的数量

    HMSET event:clicks home 1200 detail 800 cart 500
    HINCRBY event:clicks home 50  # 首页点击量+50

高级用法与限制
  1. 批量操作与原子性

    • 使用HMSET/HMGET批量读写字段。

    • 复杂逻辑需结合Lua脚本保证原子性:

      -- 检查库存并扣减
      local stock = redis.call('HGET', KEYS[1], 'stock')
      if tonumber(stock) >= tonumber(ARGV[1]) then
          redis.call('HINCRBY', KEYS[1], 'stock', -ARGV[1])
          return 1
      else
          return 0
      end

  2. 大Hash问题

    • 内存碎片:Hashtable频繁增删会导致内存碎片(可定期重启或使用MEMORY PURGE)。

    • 查询效率HGETALL返回所有字段可能阻塞服务(改用HSCAN分页遍历)。

  3. 与其他数据结构对比

    需求Hash类型String + JSONZSet
    部分字段更新✅ 高效❌ 需读写整个JSON❌ 不适用
    按字段查询✅ O(1)❌ O(n)(需解析JSON)❌ 需按Score排序
    范围查询❌ 不支持❌ 不支持✅ 支持(ZRANGE)

总结

Redis Hash通过ziplist与hashtable的混合存储,在内存效率和操作性能之间取得平衡,尤其适合存储多字段对象和需要高频部分更新的场景。其原子操作和紧凑存储特性,使其成为替代String分键存储的优选方案。但在需要复杂查询(如范围搜索)时,需结合其他数据结构(如ZSet)或外部索引。

Set类型

用途
  1. 唯一元素集合

    • 存储不重复的无序元素集合(类似数学中的集合)。

    • 支持集合运算:交集(SINTER)、并集(SUNION)、差集(SDIFF)。

  2. 快速成员检测

    • 判断元素是否存在的时间复杂度为O(1),适合去重和存在性检查。

  3. 动态数据管理

    • 支持元素的增删(SADD/SREM),且操作保持原子性。


具体实现
1. Redis的Set实现

Redis的Set类型根据元素类型和规模动态选择两种底层结构:

  • typedef struct intset {
        uint32_t encoding;  // 编码方式(INTSET_ENC_INT16/32/64)
        uint32_t length;    // 元素数量
        int8_t contents[];  // 整数数组(有序存储)
    } intset;

    intset(整数集合)

    • 触发条件

      • 所有元素均为整数。

      • 元素数量 ≤ set-max-intset-entries(默认512)。

    • 内存布局

      • 优势:内存紧凑,无指针开销。

      • 劣势:插入非整数或元素超限时,需转换为hashtable。

  • Hashtable(哈希表)

    • 触发条件:元素包含非整数或数量超过intset阈值。

    • 数据结构

      • 仅存储键(元素),值为NULL(类似Hash类型中的键)。

      • 使用链地址法解决哈希冲突。

实现内存占用插入/删除效率查询效率适用场景
intset极低O(n)O(log n)小规模整数集合
Hashtable较高O(1)O(1)大规模或含非整数元素
2. 与List的对比
特性Set类型List类型
元素唯一性✅ 唯一❌ 允许重复
顺序性❌ 无序✅ 有序
成员检测O(1)(哈希表)O(n)(需遍历)
集合运算✅ 支持交并差❌ 不支持

应用场景
1. 标签系统(Tagging)
  • 用户兴趣标签

    SADD user:1001:tags "tech" "music" "sports"  # 添加标签
    SISMEMBER user:1001:tags "tech"             # 判断是否包含标签
  • 标签聚合统计

    SINTERSTORE tech_users user:tags:tech user:active  # 计算活跃用户中喜欢科技的交集

2. 共同好友/关注
  • 计算交集

    SINTER user:1001:friends user:1002:friends  # 用户1001和1002的共同好友
  • 推荐潜在好友

    SDIFF user:1002:friends user:1001:friends  # 推荐1002有但1001没有的好友
3. 唯一数据去重
  • IP黑名单过滤

    SADD blacklist:ips "192.168.1.1" "10.0.0.5"  # 添加黑名单IP
    SISMEMBER blacklist:ips "10.0.0.5"           # 检查IP是否被禁止

  • 实时去重统计

    SADD daily:uv "user1" "user2"    # 记录每日独立访客
    SCARD daily:uv                   # 统计当日UV

4. 随机抽奖
  • 抽取幸运用户

    SADD lottery:users "user1" "user2" "user3"  
    SRANDMEMBER lottery:users 5      # 抽取5名(允许重复)
    SPOP lottery:users 5            # 抽取并不放回(确保唯一)

5. 权限白名单
  • API访问控制

    SADD api:whitelist "token1" "token2"  # 添加合法Token
    SISMEMBER api:whitelist "token3"     # 验证请求合法性


高级用法与限制
  1. 集合运算的复杂度

    • SINTER/SUNION/SDIFF的时间复杂度为 O(N*M)(N为最小集合元素数,M为集合数量)。

    • 优化方案:对大集合使用SSCAN分批次处理,或预存结果到新Key中(如SINTERSTORE)。

  2. 大Key问题

    • 内存消耗:百万级元素的Set可能占用数百MB内存(Hashtable结构)。

    • 解决方案

      • 拆分多个Set(如按哈希分片)。

      • 使用HyperLogLog替代(仅需12 KB,但无法存储元素详情)。

  3. 与其他数据结构对比

    需求Set类型ZSet类型Hash类型
    元素唯一性❌(字段唯一)
    顺序性❌ 无序✅ 按分数排序❌ 无序
    范围查询❌ 不支持✅(ZRANGEBYSCORE)❌ 不支持
    关联值存储❌ 仅存储元素✅(元素+分数)✅(字段+值)

总结

Redis Set通过intset与hashtable的混合存储,在内存效率和操作性能之间灵活平衡,尤其适合去重、集合运算和快速存在性检测。其原子性和丰富的集合操作,使其成为社交关系、标签系统的理想选择。但在处理超大规模集合时,需警惕内存消耗和运算复杂度,必要时可结合分片或概率型数据结构(如HyperLogLog)优化。

ZSet类型

用途
  1. 有序唯一元素集合
    存储不重复元素,每个元素关联一个分数(score),按分数排序(默认升序)。

  2. 范围查询与排名
    支持按分数范围、排名范围、字典序范围快速查询元素。

  3. 带权重的数据管理
    以分数作为权重值,实现优先级队列、排行榜等场景。


具体实现

Redis的ZSet采用跳跃表(Skip List) + 哈希表(Hash Table) 的混合结构,根据数据规模动态优化存储:

  1. ziplist(压缩列表)
    触发条件

    • 元素数量 ≤ zset-max-ziplist-entries(默认128)。

    • 所有元素的长度 ≤ zset-max-ziplist-value(默认64字节)。
      内存布局

    // ziplist结构:连续内存块,按[元素1, 分数1, 元素2, 分数2...]存储

    优势:内存紧凑,无指针开销。
    劣势:插入/删除需重分配内存,效率低。

  2. skiplist + hashtable
    触发条件:元素数量或大小超过ziplist阈值。

    • 跳跃表(Skip List)

      • 按分数排序,支持O(log n)复杂度的插入、删除、范围查询。

      • 多层链表结构,加速查找过程。

    • 哈希表(Hash Table)

      • 存储元素到分数的映射,实现O(1)复杂度的分数查询。

    数据结构示例

    typedef struct zset {
        dict *dict;          // 哈希表,键=元素,值=分数
        zskiplist *zsl;      // 跳跃表,节点包含元素和分数
    } zset;

实现对比

结构内存占用插入/删除效率范围查询效率适用场景
ziplist极低O(n)O(n)小规模有序集合
skiplist较高O(log n)O(log n)大规模或大元素集合

应用场景
  1. 实时排行榜
    示例:游戏玩家得分排名

    ZADD leaderboard 1000 "PlayerA" 950 "PlayerB"  # 添加分数
    ZREVRANGE leaderboard 0 9 WITHSCORES          # 获取前10名
    ZRANK leaderboard "PlayerB"                   # 查询当前排名

  2. 延迟队列
    示例:定时任务调度

    ZADD tasks 1625097600 "send_email"    # 任务执行时间戳作为分数
    ZRANGEBYSCORE tasks 0 <current_time>  # 获取到期任务

  3. 热点数据统计
    示例:新闻点击量排行榜

    ZINCRBY news_clicks 1 "article_123"    # 点击量+1
    ZREVRANGE news_clicks 0 4              # 展示Top5热文

  4. 范围过滤与聚合
    示例:价格区间商品筛选

    ZADD products 299 "phone" 599 "laptop" 199 "earbuds"
    ZRANGEBYSCORE products 200 600          # 获取200-600元商品


高级用法与限制
  1. 分数相同处理

    • 元素按字典序排序(如"apple"排在"banana"前)。

  2. 大Key风险

    • 百万级元素的ZSet可能占用数百MB内存(skiplist结构)。

    • 优化方案:按业务维度拆分多个ZSet,或使用时间分片。

  3. 集合运算

    • 支持ZUNIONSTORE(并集)、ZINTERSTORE(交集),但复杂度较高(O(NK)+O(Mlog(M)))。

  4. 对比其他结构

    需求ZSet类型Set类型Hash类型
    唯一性❌(字段唯一)
    排序✅ 按分数❌ 无序❌ 无序
    范围查询✅(分数/字典序)
    关联值存储✅(元素+分数)❌ 仅元素✅(字段+值)

总结

Redis ZSet通过跳跃表+哈希表的混合结构,在保持元素唯一性的同时,提供高效的范围操作和精确查询。其核心优势在于有序性权重控制,适用于排行榜、延迟队列、时间轴等场景。需注意内存消耗问题,合理选择编码方式(ziplist/skiplist),避免大Key影响性能。

BitMap类型

用途

BitMap(位图)通过二进制位(bit)存储布尔值(0/1),常用于高效标记、统计大量二元状态数据(如用户签到、特征开关、活跃用户跟踪)。每个bit代表一个独立的状态位,支持快速位级操作与统计。


具体实现
  1. 底层结构

    • 基于Redis的String类型实现,底层为动态字节数组

    • 每个bit对应一个偏移量(offset),最大支持2^32 -1(约42亿)。

  2. 核心操作

    • SETBIT key offset 0/1:设置指定偏移量的bit值。

    • GETBIT key offset:获取偏移量的bit值(不存在时返回0)。

    • BITCOUNT key [start end]:统计范围内值为1的bit数。

    • BITOP命令(AND/OR/XOR/NOT):对多个BitMap进行位运算。

    • BITPOS key bit [start end]:查找第一个指定bit值的位置。

  3. 内存管理

    • 自动扩展:当设置的offset超过当前长度时,自动填充0扩展字节数组。

    • 稀疏性处理:未显式设置的bit默认视为0,但大范围稀疏offset可能浪费内存(需分片优化)。


应用场景
  1. 用户签到系统

    • 每日1bit标记签到状态,年签到仅需46字节。

    SETBIT user:1001:sign:2023 3 1  # 第4天签到(offset从0开始)
    BITCOUNT user:1001:sign:2023     # 统计总签到次数

  2. 活跃用户统计

    • 每日活跃用户存入一个BitMap,快速计算多日活跃/沉默用户。

    BITOP AND weekly_active user:day1 user:day2 ... user:day7  # 连续7天活跃用户
    BITCOUNT weekly_active  # 统计周活跃用户数

  3. 布隆过滤器(Bloom Filter)

    • 利用多个哈希函数映射元素到位图中,判断元素可能存在(所有位为1)或必定不存在(至少一位为0)。

    # 添加元素(哈希到多个offset)
    SETBIT bloom_filter 100 1  
    SETBIT bloom_filter 200 1  
    # 检查元素是否存在  
    GETBIT bloom_filter 100 && GETBIT bloom_filter 200

  4. 实时特征标记

    • 标记用户属性(如VIP状态、功能权限),支持批量查询。

    SETBIT features:dark_mode 1001 1  # 用户1001开启暗黑模式
    GETBIT features:dark_mode 1001    # 检查权限
  5. 大数据去重

    • 海量数据中快速判断元素是否已处理(如爬虫URL去重)。

    SETBIT features:dark_mode 1001 1  # 用户1001开启暗黑模式
    GETBIT features:dark_mode 1001    # 检查权限
    SETBIT crawled_urls 123456 1  # 标记URL哈希值
    GETBIT crawled_urls 123456    # 检查是否已爬取

高级技巧与限制
  • 内存优化

    • 分片存储:按范围拆分大BitMap(如user:id/1024)。

    • 压缩:Redis自动压缩连续为0的字节(redis.confactiverehashing配置)。

  • 性能注意

    • BITOP复杂度为O(N),大BitMap运算可能阻塞服务,建议异步或分片处理。

  • 替代方案

    • 超大规模数据:考虑结合HyperLogLog(基数统计)或Roaring Bitmaps(压缩位图库)。


总结

Redis BitMap以极低内存开销(1亿用户仅需12MB)实现高效二元状态存储与统计,适用于高并发标记、去重和批量位运算场景。需权衡稀疏数据内存消耗与大运算性能,结合分片和压缩策略可最大化其优势。

HyperLogLog类型(暂时不总结)

GEO类型(暂时不总结)

Stream类型

用途

Redis Stream 是专为消息队列和事件流处理设计的数据结构,提供持久化、有序、多消费者组支持,适用于以下场景:

  1. 消息队列:实现生产-消费模型,支持多消费者组竞争或协同处理消息。

  2. 事件溯源:按顺序记录事件(如用户操作、系统日志),支持回溯与审计。

  3. 实时数据管道:传输实时数据流(如传感器数据、点击流),供下游系统实时处理。


具体实现
  1. 底层结构

    • 基数树(Radix Tree):存储消息列表,每个节点包含多条消息,优化内存使用。

    • 消息ID:格式为 <时间戳>-<序号>(如 1650000000000-0),全局有序且可自定义。

    • 消费者组(Consumer Groups)

      • 每个组独立跟踪消费进度,组内消费者通过XREADGROUP竞争消息。

      • 维护Pending Entries List (PEL),记录已分配但未确认的消息。

  2. 核心操作

    • 生产消息

      XADD orders * user_id 1001 product "Book"  # *表示自动生成ID  

    • 消费消息

      XREADGROUP GROUP order_group consumer1 BLOCK 0 STREAMS orders >  

    • 确认与重试

      XACK orders order_group 1650000000000-0  # 确认消息处理完成  
      XCLAIM orders order_group consumer2 3600000 1650000000000-0  # 转移超时消息  

    • 数据保留

      XTRIM orders MAXLEN 1000  # 保留最近1000条消息  

  3. 内存管理

    • 自动分段存储:基数树按需分配内存,避免连续内存占用。

    • 过期策略:需手动修剪(XTRIMXADD MAXLEN),无自动过期。


应用场景
  1. 订单处理系统

    • 生产者:订单服务生成订单事件流。

    • 消费者组:库存服务、支付服务、通知服务并行处理不同任务。

    # 库存服务消费消息  
    XREADGROUP GROUP order_group inventory_service COUNT 10 STREAMS orders >  

  2. 实时日志收集

    • 收集多台服务器的日志事件,供分析服务实时消费。

    # 生产日志  
    XADD server_logs * host "web01" level "ERROR" message "Connection failed"  
    # 消费日志  
    XREAD STREAMS server_logs 0  # 从头读取历史日志  

  3. 用户行为追踪

    • 记录用户点击、浏览等行为,支持实时分析与漏斗计算。

    XADD user_clicks * user_id "1001" page "/product" action "view"  

  4. 微服务通信

    • 服务间通过Stream传递事件,实现解耦与异步通信。

    # 支付服务完成支付后触发事件  
    XADD payment_events * order_id "2001" status "success"  

高级特性与限制
  • 阻塞消费:支持BLOCK参数实现长轮询,减少空转开销。

  • 消息重试:通过XCLAIM接管超时未确认的消息,确保至少一次交付。

  • 性能瓶颈

    • 单Stream写入性能约10万/秒(依赖Redis实例配置)。

    • 消费者组过多可能增加内存与CPU开销。

  • 替代方案对比

    需求StreamPub/SubList(LPUSH/BRPOP)
    消息持久化✅ 支持❌ 临时消息✅ 支持
    多消费者组✅ 支持❌ 仅广播❌ 单消费者
    顺序保证✅ 严格有序✅ 有序但无持久化✅ 有序
    回溯历史消息✅ 支持❌ 不支持✅ 支持(需手动维护)

总结

Redis Stream 通过基数树与消费者组机制,实现了高吞吐、持久化的消息流处理,尤其适合需要严格顺序、多消费者协同及消息回溯的场景。其设计平衡了性能与功能,但需注意合理控制消息保留策略与消费者组规模,避免资源过度消耗。在微服务架构、实时数据分析等场景中,Stream是替代传统消息中间件的轻量级选择。

执行一条select,这个期间发生了什么?

这上面的图是我从小林coding上copy下来的(指路):执行一条 select 语句,期间发生了什么? | 小林coding

Mysql大致可以分为两层:

Server层:责建立连接、分析和执行 SQL。MySQL大多数的核心功能模块都在这实现,主要包括连接器,查询缓存、解析器、预处理器、优化器、执行器等。另外,所有的内置函数(如日期、时间、数学和加密函数等)和所有跨存储引擎的功能(如存储过程、触发器、视图等。)都在 Server 层实现。

存储引擎:主要负责数据的存储和提取。支持InnoDB、MyISAM(前两个着重复习)、Memory等多个存储引擎,不同的存储引擎共用一个 Server 层。现在最常用的存储引擎是InnoDB,从 MySQL 5.5 版本开始, InnoDB 成为了 MySQL 的默认存储引擎.。我们常说的索引数据结构,就是由存储引擎层实现的,不同的存储引擎支持的索引类型也不相同,比如 InnoDB 支持索引类型是 B+树 ,且是默认使用,也就是说在数据表中创建的主键索引和二级索引默认使用的是 B+ 树索引。

那么它是如何具体执行select语句呢(update、delete操作等同理):

第一步:连接器(Connector)

流程说明
  • 建立 TCP 连接

    • 客户端与 MySQL 服务端通过 TCP 三次握手 建立连接。

    • 默认端口为 3306,支持 SSL 加密传输。
  • 身份验证
    • 校验客户端提交的 用户名和密码,验证依据为 mysql.user 表中的凭证信息。

    • 若认证失败,返回 ERROR 1045 (28000): Access denied

  • 权限加载

    • 认证成功后,连接器会从 mysql.usermysql.db 等表中 加载该用户的权限,后续所有操作均基于此时读取的权限。

    • 权限变更需重连生效:修改用户权限后,已建立的连接需断开重连才能生效。


问题 1:空闲连接会一直占用吗?
  • 默认行为
    MySQL 通过 wait_timeout 参数控制空闲连接的超时时间,默认 8 小时。若连接在此期间无任何操作,服务端自动断开连接。

  • 手动干预

    • 管理员可通过 KILL <connection_id> 手动终止指定连接。

    • 客户端也可主动发送 COM_QUIT 命令断开连接。

问题 2:如何解决长连接占用问题?
  1. 定期断开长连接

    • 通过脚本或中间件(如 ProxySQL)周期性重置空闲连接。

    • 修改 wait_timeout 参数缩短超时时间:

      SET GLOBAL wait_timeout = 3600; -- 设置为 1 小时(单位:秒)
  2. 客户端主动重置连接

    • 客户端在执行完操作后,显式调用 mysql_close() 或连接池的 release() 方法释放连接。

  3. 使用连接池(推荐)

    • 优点:复用连接,避免频繁创建/销毁连接的开销。

    • 常用方案

      • 服务端:MySQL Connection PoolHikariCP(Java)、SQLAlchemy(Python)。

      • 客户端:配置连接池的最大空闲时间(idleTimeout)。

第二步:缓存查询(Query Cache)

  • 功能:缓存 SELECT 语句及其结果集(Key-Value 结构),若后续查询完全匹配则直接返回缓存。

  • 问题

    • 缓存失效频繁:表数据变更(INSERT/UPDATE/DELETE)会清空所有相关缓存。

    • 命中率低:适用于静态表(如配置表),生产环境中通常建议关闭。

  • 关闭缓存

    SET GLOBAL query_cache_type = OFF;

第三步:解析 SQL(Parser)

  1. 词法分析

    • 将 SQL 字符串拆分为 关键字、表名、列名 等原子单元(Token)。

    • 例如:SELECT id FROM users WHERE name='Alice' 解析为 [SELECT, id, FROM, users, WHERE, name, =, 'Alice']

  2. 语法分析

    • 基于语法规则生成 抽象语法树(AST),验证 SQL 结构合法性。

    • 若语法错误,返回 ERROR 1064 (42000): You have an error in your SQL syntax

第四步:执行 SQL(Executor)

  1. 预处理器

    • 检查表、列是否存在,解析 * 为实际列名,验证权限。

  2. 优化器(Optimizer)

    • 生成 执行计划,选择成本最低的方案(如索引选择、JOIN 顺序)。

    • 可通过 EXPLAIN 查看优化器决策:

      EXPLAIN SELECT id FROM users WHERE name='Alice';
  3. 执行器

    • 调用存储引擎接口执行计划,逐行校验权限。

    • 若开启慢查询日志(slow_query_log),超阈值的操作会被记录。

  4. 存储引擎(InnoDB/MyISAM,后面仔细介绍)

    • 执行器通过 Handler API 与存储引擎交互,读取或修改数据。

      •  预处理器:检查sql语句中的表或者字段是否存在、将select *的*扩展为表上的所有列。

      • 优化器:指定sql语句的执行计划,负责将sql查询语句的执行方案确定下来,如果有多个索引的时候,优化器会基于查询成本的考虑,决定使用那个索引(执行那个索引又是一个大的方向,类似什么主键查询,非主键查询,碰到锁了怎么办?等等)

      • 执行器:开始真正的查询,通过b+树的索引(主键、非主键)找到对应的记录。找到了对应的记录就发送给客户端,查找不到就返回查找不到的信息。

总结
  • 连接管理核心:通过 wait_timeout 控制空闲连接,结合连接池与客户端重置优化资源占用。

  • SQL 执行流程:连接 → 缓存(可选) → 解析 → 优化 → 执行 → 返回结果。

  • 性能关键点

    • 避免长连接占用内存(wait_timeout + 连接池)。

    • 优先关闭查询缓存,依赖索引和优化器提升查询效率。

mysql有哪些数据引擎?解决了哪些问题?

这个问题,我的回答方向就是。从mysql基础的几个数据引擎。然后往这个数据引擎解决了那些问题

InnoDB解决了数据一致性和可靠性问题,特别适合需要事务支持的应用;MyISAM则以其快速的读操作性能著称,适合那些读多于写的场景;而像Memory这样的引擎,则解决了对临时数据快速访问的需求。

事务隔离级别是怎么实现的?

事务的四大特性:原子性、一致性、隔离性、持久性

原子性:保证一个事务的所有操作是原子性的要么全部完成,要不错误了就回滚。

一致性:保证事务之间的交互是一致的。比如a给b转了200,那么a的账户是走了200元,b账户增加了200元。不会出现a账户减少了钱,而b没有增加。

隔离性:数据库允许多个并发事务同时对数据改写读取的能力,防止因为并发操作出现的数据不一致的问题。

持久性:保证你的操作结果能落盘,不会出现主机崩溃导致的数据丢失问题。

并行事务会出现哪些问题?

脏读:如果一个[事务]读取到了另一个[未提交事务]修改过的数据,就意味着发生了脏读。如果未提交事务发生了回滚,那么这样的话,会造成很严重的问题。

不可重复读:在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象。

幻读:在一个事务内多次查询某个符合查询条件的「记录数量」如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象。

事物的隔离级别

SQL标准提出了四种隔离级别来规避这些现象,隔离级别越高,性能效率就越低,这四个隔离级别如下:

  • 读未提交(read uncommitted),指一个事务还没提交时,它做的变更就能被基他事务看到;
  • 读提交(read committed),指一个事务提交之后,它做的变更才能被其他事务看到;
  • 可重复读(repeatable read),指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQLInnoDB 引擎的默认隔离级别;
  • 串行化(serializable);会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;

可重复读级别,完全解决了幻读嘛?

周六日再稍微改改吧~烦内!!!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值