浅浅的心里总结:
其实我感觉面试应该是不难的,一共就一个小时的面试时间。其实划分一下就还挺好的,半个小时基础知识回答时间,半个小时的手撕代码时间。前半个小时的时间好好做一下引导,超过半个小时还是很简单的(打个最简单的比方 智能指针->三个最常见的智能指针->着重介绍一下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二叉树和红黑树是如何进行平衡调整的。
数据库篇
为什么要加这一篇章,因为暂时cpp问的东西大概我都熟悉了(?打个问号,下次不会了再收集hhh),但是数据库这一部分我是真的很难去回答(因为我只会用select、update语句哇!得专门重新抓一下基础知识了)。所以专门开这一篇章,好好从头开始有针对性的学习。前面的线程库暂时看了差不多了,以后有时间把源码拿出来分析一下。
redis有哪些数据结构?
这个问题,腾讯的面试官基本必问!我造了。。。老老实实学吧,世界上没有捷径!!!干就完了!!
常见的数据类型有五种:string、hash、list、set、zset
随着版本的更新增加了四种类型BitMap、Geo、HyperLogLop、Stream类型
在Redis中是怎么具体实现的?
String类型
用途
-
存储多样化数据
-
字符串(文本、JSON、HTML)
-
整数(如计数器)
-
浮点数(如实时统计值)
-
二进制数据(如图片、序列化对象)
-
-
原子操作支持
-
INCR
/DECR
:自增/自减整数(如库存扣减) -
INCRBY
/DECRBY
:按步长增减 -
APPEND
:字符串追加 -
SETBIT
/GETBIT
:位操作(如用户签到、或者做一个布隆过滤器)
-
具体实现
1. Redis的String实现
Redis的String类型由两部分实现:
-
整数编码(
int
)
当值为整数且范围在LONG_MIN
到LONG_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)遍历到 \0
O(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
步骤:
-
生成唯一客户端标识(如UUID + 线程ID)。
-
获取锁时设置该标识。
-
释放锁时校验标识,避免误删其他客户端的锁。
-
-
锁续期(WatchDog机制)
后台线程定期检测锁持有状态,若未完成操作则延长过期时间(Redisson库内置实现)。
List类型
用途
-
双向链表结构
-
支持**头部(Left)和尾部(Right)**的高效插入(
LPUSH
/RPUSH
)与删除(LPOP
/RPOP
)。 -
按索引访问(
LINDEX
)、范围截取(LRANGE
)、长度查询(LLEN
)。
-
-
动态数据集合
-
存储有序元素集合,允许重复值。
-
支持阻塞操作(
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)
-
使用
LPUSH
和LPOP
模拟栈:LPUSH user:1001:history "pageA" # 记录访问历史 LPOP user:1001:history # 后进先出
高级用法与限制
-
性能注意事项:
-
中间操作(如
LINSERT
):时间复杂度为O(n),避免在大列表中使用。 -
超大元素(如10 MB的字符串):会导致ziplist退化为普通链表,内存碎片增加。
-
-
替代方案对比:
需求 List类型 Stream类型(更推荐) 消息持久化 需手动维护 支持持久化、消费者组 多消费者组 需自行实现 原生支持 消息ACK确认 不支持 支持
总结
Redis List通过QuickList的混合结构,在内存效率与操作性能之间取得平衡,尤其适合高频头尾操作(如消息队列、最新动态)。其阻塞操作和原子性特性,使其成为轻量级实时系统的首选数据结构。但在需要消息持久化、多消费者组支持时,建议升级至Stream类型。
Hash类型
用途
-
结构化数据存储
-
存储**字段-值(Field-Value)**的映射集合,类似编程语言中的字典或对象。
-
支持对单个字段的原子操作(如
HINCRBY
),无需读取整个对象。
-
-
高效部分更新
-
可单独修改某个字段,无需序列化/反序列化整个对象(如用户年龄更新)。
-
-
内存优化
-
相比多个独立的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
高级用法与限制
-
批量操作与原子性:
-
使用
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
-
-
大Hash问题:
-
内存碎片:Hashtable频繁增删会导致内存碎片(可定期重启或使用
MEMORY PURGE
)。 -
查询效率:
HGETALL
返回所有字段可能阻塞服务(改用HSCAN
分页遍历)。
-
-
与其他数据结构对比:
需求 Hash类型 String + JSON ZSet 部分字段更新 ✅ 高效 ❌ 需读写整个JSON ❌ 不适用 按字段查询 ✅ O(1) ❌ O(n)(需解析JSON) ❌ 需按Score排序 范围查询 ❌ 不支持 ❌ 不支持 ✅ 支持(ZRANGE)
总结
Redis Hash通过ziplist与hashtable的混合存储,在内存效率和操作性能之间取得平衡,尤其适合存储多字段对象和需要高频部分更新的场景。其原子操作和紧凑存储特性,使其成为替代String分键存储的优选方案。但在需要复杂查询(如范围搜索)时,需结合其他数据结构(如ZSet)或外部索引。
Set类型
用途
-
唯一元素集合
-
存储不重复的无序元素集合(类似数学中的集合)。
-
支持集合运算:交集(
SINTER
)、并集(SUNION
)、差集(SDIFF
)。
-
-
快速成员检测
-
判断元素是否存在的时间复杂度为O(1),适合去重和存在性检查。
-
-
动态数据管理
-
支持元素的增删(
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" # 验证请求合法性
高级用法与限制
-
集合运算的复杂度:
-
SINTER
/SUNION
/SDIFF
的时间复杂度为 O(N*M)(N为最小集合元素数,M为集合数量)。 -
优化方案:对大集合使用
SSCAN
分批次处理,或预存结果到新Key中(如SINTERSTORE
)。
-
-
大Key问题:
-
内存消耗:百万级元素的Set可能占用数百MB内存(Hashtable结构)。
-
解决方案:
-
拆分多个Set(如按哈希分片)。
-
使用
HyperLogLog
替代(仅需12 KB,但无法存储元素详情)。
-
-
-
与其他数据结构对比:
需求 Set类型 ZSet类型 Hash类型 元素唯一性 ✅ ✅ ❌(字段唯一) 顺序性 ❌ 无序 ✅ 按分数排序 ❌ 无序 范围查询 ❌ 不支持 ✅(ZRANGEBYSCORE) ❌ 不支持 关联值存储 ❌ 仅存储元素 ✅(元素+分数) ✅(字段+值)
总结
Redis Set通过intset与hashtable的混合存储,在内存效率和操作性能之间灵活平衡,尤其适合去重、集合运算和快速存在性检测。其原子性和丰富的集合操作,使其成为社交关系、标签系统的理想选择。但在处理超大规模集合时,需警惕内存消耗和运算复杂度,必要时可结合分片或概率型数据结构(如HyperLogLog)优化。
ZSet类型
用途
-
有序唯一元素集合
存储不重复元素,每个元素关联一个分数(score),按分数排序(默认升序)。 -
范围查询与排名
支持按分数范围、排名范围、字典序范围快速查询元素。 -
带权重的数据管理
以分数作为权重值,实现优先级队列、排行榜等场景。
具体实现
Redis的ZSet采用跳跃表(Skip List) + 哈希表(Hash Table) 的混合结构,根据数据规模动态优化存储:
-
ziplist(压缩列表)
触发条件:-
元素数量 ≤
zset-max-ziplist-entries
(默认128)。 -
所有元素的长度 ≤
zset-max-ziplist-value
(默认64字节)。
内存布局:
// ziplist结构:连续内存块,按[元素1, 分数1, 元素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) | 大规模或大元素集合 |
应用场景
-
实时排行榜
示例:游戏玩家得分排名ZADD leaderboard 1000 "PlayerA" 950 "PlayerB" # 添加分数 ZREVRANGE leaderboard 0 9 WITHSCORES # 获取前10名 ZRANK leaderboard "PlayerB" # 查询当前排名
-
延迟队列
示例:定时任务调度ZADD tasks 1625097600 "send_email" # 任务执行时间戳作为分数 ZRANGEBYSCORE tasks 0 <current_time> # 获取到期任务
-
热点数据统计
示例:新闻点击量排行榜ZINCRBY news_clicks 1 "article_123" # 点击量+1 ZREVRANGE news_clicks 0 4 # 展示Top5热文
-
范围过滤与聚合
示例:价格区间商品筛选ZADD products 299 "phone" 599 "laptop" 199 "earbuds" ZRANGEBYSCORE products 200 600 # 获取200-600元商品
高级用法与限制
-
分数相同处理:
-
元素按字典序排序(如
"apple"
排在"banana"
前)。
-
-
大Key风险:
-
百万级元素的ZSet可能占用数百MB内存(skiplist结构)。
-
优化方案:按业务维度拆分多个ZSet,或使用时间分片。
-
-
集合运算:
-
支持
ZUNIONSTORE
(并集)、ZINTERSTORE
(交集),但复杂度较高(O(NK)+O(Mlog(M)))。
-
-
对比其他结构:
需求 ZSet类型 Set类型 Hash类型 唯一性 ✅ ✅ ❌(字段唯一) 排序 ✅ 按分数 ❌ 无序 ❌ 无序 范围查询 ✅(分数/字典序) ❌ ❌ 关联值存储 ✅(元素+分数) ❌ 仅元素 ✅(字段+值)
总结
Redis ZSet通过跳跃表+哈希表的混合结构,在保持元素唯一性的同时,提供高效的范围操作和精确查询。其核心优势在于有序性和权重控制,适用于排行榜、延迟队列、时间轴等场景。需注意内存消耗问题,合理选择编码方式(ziplist/skiplist),避免大Key影响性能。
BitMap类型
用途
BitMap(位图)通过二进制位(bit)存储布尔值(0/1),常用于高效标记、统计大量二元状态数据(如用户签到、特征开关、活跃用户跟踪)。每个bit代表一个独立的状态位,支持快速位级操作与统计。
具体实现
-
底层结构
-
基于Redis的String类型实现,底层为动态字节数组。
-
每个bit对应一个偏移量(
offset
),最大支持2^32 -1
(约42亿)。
-
-
核心操作
-
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值的位置。
-
-
内存管理
-
自动扩展:当设置的
offset
超过当前长度时,自动填充0扩展字节数组。 -
稀疏性处理:未显式设置的bit默认视为0,但大范围稀疏offset可能浪费内存(需分片优化)。
-
应用场景
-
用户签到系统
-
每日1bit标记签到状态,年签到仅需46字节。
SETBIT user:1001:sign:2023 3 1 # 第4天签到(offset从0开始) BITCOUNT user:1001:sign:2023 # 统计总签到次数
-
-
活跃用户统计
-
每日活跃用户存入一个BitMap,快速计算多日活跃/沉默用户。
BITOP AND weekly_active user:day1 user:day2 ... user:day7 # 连续7天活跃用户 BITCOUNT weekly_active # 统计周活跃用户数
-
-
布隆过滤器(Bloom Filter)
-
利用多个哈希函数映射元素到位图中,判断元素可能存在(所有位为1)或必定不存在(至少一位为0)。
# 添加元素(哈希到多个offset) SETBIT bloom_filter 100 1 SETBIT bloom_filter 200 1 # 检查元素是否存在 GETBIT bloom_filter 100 && GETBIT bloom_filter 200
-
-
实时特征标记
-
标记用户属性(如VIP状态、功能权限),支持批量查询。
SETBIT features:dark_mode 1001 1 # 用户1001开启暗黑模式 GETBIT features:dark_mode 1001 # 检查权限
-
-
大数据去重
-
海量数据中快速判断元素是否已处理(如爬虫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.conf
中activerehashing
配置)。
-
-
性能注意:
-
BITOP
复杂度为O(N),大BitMap运算可能阻塞服务,建议异步或分片处理。
-
-
替代方案:
-
超大规模数据:考虑结合HyperLogLog(基数统计)或Roaring Bitmaps(压缩位图库)。
-
总结
Redis BitMap以极低内存开销(1亿用户仅需12MB)实现高效二元状态存储与统计,适用于高并发标记、去重和批量位运算场景。需权衡稀疏数据内存消耗与大运算性能,结合分片和压缩策略可最大化其优势。
HyperLogLog类型(暂时不总结)
GEO类型(暂时不总结)
Stream类型
用途
Redis Stream 是专为消息队列和事件流处理设计的数据结构,提供持久化、有序、多消费者组支持,适用于以下场景:
-
消息队列:实现生产-消费模型,支持多消费者组竞争或协同处理消息。
-
事件溯源:按顺序记录事件(如用户操作、系统日志),支持回溯与审计。
-
实时数据管道:传输实时数据流(如传感器数据、点击流),供下游系统实时处理。
具体实现
-
底层结构
-
基数树(Radix Tree):存储消息列表,每个节点包含多条消息,优化内存使用。
-
消息ID:格式为
<时间戳>-<序号>
(如1650000000000-0
),全局有序且可自定义。 -
消费者组(Consumer Groups):
-
每个组独立跟踪消费进度,组内消费者通过
XREADGROUP
竞争消息。 -
维护
Pending Entries List (PEL)
,记录已分配但未确认的消息。
-
-
-
核心操作
-
生产消息:
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条消息
-
-
内存管理
-
自动分段存储:基数树按需分配内存,避免连续内存占用。
-
过期策略:需手动修剪(
XTRIM
或XADD MAXLEN
),无自动过期。
-
应用场景
-
订单处理系统
-
生产者:订单服务生成订单事件流。
-
消费者组:库存服务、支付服务、通知服务并行处理不同任务。
# 库存服务消费消息 XREADGROUP GROUP order_group inventory_service COUNT 10 STREAMS orders >
-
-
实时日志收集
-
收集多台服务器的日志事件,供分析服务实时消费。
# 生产日志 XADD server_logs * host "web01" level "ERROR" message "Connection failed" # 消费日志 XREAD STREAMS server_logs 0 # 从头读取历史日志
-
-
用户行为追踪
-
记录用户点击、浏览等行为,支持实时分析与漏斗计算。
XADD user_clicks * user_id "1001" page "/product" action "view"
-
-
微服务通信
-
服务间通过Stream传递事件,实现解耦与异步通信。
# 支付服务完成支付后触发事件 XADD payment_events * order_id "2001" status "success"
-
高级特性与限制
-
阻塞消费:支持
BLOCK
参数实现长轮询,减少空转开销。 -
消息重试:通过
XCLAIM
接管超时未确认的消息,确保至少一次交付。 -
性能瓶颈:
-
单Stream写入性能约10万/秒(依赖Redis实例配置)。
-
消费者组过多可能增加内存与CPU开销。
-
-
替代方案对比:
需求 Stream Pub/Sub List(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.user
、mysql.db
等表中 加载该用户的权限,后续所有操作均基于此时读取的权限。 -
权限变更需重连生效:修改用户权限后,已建立的连接需断开重连才能生效。
-
问题 1:空闲连接会一直占用吗?
-
默认行为:
MySQL 通过wait_timeout
参数控制空闲连接的超时时间,默认 8 小时。若连接在此期间无任何操作,服务端自动断开连接。 -
手动干预:
-
管理员可通过
KILL <connection_id>
手动终止指定连接。 -
客户端也可主动发送
COM_QUIT
命令断开连接。
-
问题 2:如何解决长连接占用问题?
-
定期断开长连接
-
通过脚本或中间件(如 ProxySQL)周期性重置空闲连接。
-
修改
wait_timeout
参数缩短超时时间:SET GLOBAL wait_timeout = 3600; -- 设置为 1 小时(单位:秒)
-
-
客户端主动重置连接
-
客户端在执行完操作后,显式调用
mysql_close()
或连接池的release()
方法释放连接。
-
-
使用连接池(推荐)
-
优点:复用连接,避免频繁创建/销毁连接的开销。
-
常用方案:
-
服务端:
MySQL Connection Pool
、HikariCP
(Java)、SQLAlchemy
(Python)。 -
客户端:配置连接池的最大空闲时间(
idleTimeout
)。
-
-
第二步:缓存查询(Query Cache)
-
功能:缓存
SELECT
语句及其结果集(Key-Value 结构),若后续查询完全匹配则直接返回缓存。 -
问题:
-
缓存失效频繁:表数据变更(INSERT/UPDATE/DELETE)会清空所有相关缓存。
-
命中率低:适用于静态表(如配置表),生产环境中通常建议关闭。
-
-
关闭缓存:
SET GLOBAL query_cache_type = OFF;
第三步:解析 SQL(Parser)
-
词法分析
-
将 SQL 字符串拆分为 关键字、表名、列名 等原子单元(Token)。
-
例如:
SELECT id FROM users WHERE name='Alice'
解析为[SELECT, id, FROM, users, WHERE, name, =, 'Alice']
。
-
-
语法分析
-
基于语法规则生成 抽象语法树(AST),验证 SQL 结构合法性。
-
若语法错误,返回
ERROR 1064 (42000): You have an error in your SQL syntax
。
-
第四步:执行 SQL(Executor)
-
预处理器
-
检查表、列是否存在,解析
*
为实际列名,验证权限。
-
-
优化器(Optimizer)
-
生成 执行计划,选择成本最低的方案(如索引选择、JOIN 顺序)。
-
可通过
EXPLAIN
查看优化器决策:EXPLAIN SELECT id FROM users WHERE name='Alice';
-
-
执行器
-
调用存储引擎接口执行计划,逐行校验权限。
-
若开启慢查询日志(
slow_query_log
),超阈值的操作会被记录。
-
-
存储引擎(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);会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;
可重复读级别,完全解决了幻读嘛?
周六日再稍微改改吧~烦内!!!!!