redis底层数据结构
Redis对外暴露了5种基本数据类型,但底层为了兼顾内存效率和操作性能,实际使用了多种物理数据结构。同一种上层类型在不同数据量下,底层类型还会自动切换。
1.String底层数据结构是SDS,简单动态字符串,可以在O(1)时间复杂度内获取长度,支持自动扩容避免缓冲区溢出,增长时支持空间预分配,截短时将多出的空间放到free里,之后无需重新分配,不以\\0判断字符串结束,可以安全存储图片,序列号对象
[图片]
2.List早期是ZipList+LinkedList,到3.2的QuickList,再到7.0用ListPack替代ZipList
小数据量用ZipList,它是一块连续内存,所有元素紧凑排列,非常省内存;大数据量用双向链表。但两者各有缺陷——ZipList修改效率低且有连锁更新风险,LinkedList小数据时每个节点都要存两个指针+一个数据指针(共24字节开销),指针比数据还大,内存浪费严重。
QuickList,统一了这两种结构。本质是双向链表+ZipList的混合体:外层用双向链表连接多个节点,每个节点内部是一个ZipList,保留了链表的灵活修改能力,又保持了节点内部的内存紧凑。
用ListPack替代了ZipList。ZipList的核心缺陷是连锁更新——每个元素记录前驱长度,一次插入可能触发后续所有元素的级联扩容。ListPack的解决方案是去掉前驱长度,改为在末尾记录自身长度,使每个元素的大小变化完全独立,从根本上消除了连锁更新。
3.Redis Hash的底层采用自适应编码策略:小数据用ZipList/ListPack数组节省内存,大数据用哈希表保证性能,两者之间通过阈值自动切换,且扩容时使用渐进式rehash避免阻塞主线程。
当HashTable需要扩容时,传统做法是一次性将所有数据迁移到新表:Redis是单线程模型,任何阻塞都会导致所有请求停滞
dict结构内部维护了两个哈希表ht[0]和ht[1]。当需要扩容时,先分配ht[1]。之后每次操作都会迁移ht[0]中的一个桶到ht[1]。rehash期间,新增操作只写入ht[1],查找先查ht[1]再查ht[0]。这样就把原本一次性迁移,分摊到了每次微秒级的操作中。
4.Redis Set的底层采用内容感知编码:当所有元素都是整数且数量较小时,用IntSet;一旦混入字符串或数量超512,自动升级为HashTable。Set的HashTable只用key不用value,value字段始终为NULL。对于纯整数的小集合,IntSet能节省90%以上的内存。
5.ZSet,小数据用ZipList(7.0后为ListPack),大数据量时使用跳表(SkipList)+ 哈希表(HashTable),跳表负责按score排序和范围查询,哈希表负责O(1)通过member查score。
跳表的核心设计是多层索引:最底层包含所有节点,高层是低层的快速通道。查找时从最高层开始,能前进就前进,不能前进就下降.
为什么选跳表而不是红黑树,主要有三个原因:一是范围查询友好,找到起点后沿最底层顺序遍历即可,红黑树需要复杂的中序遍历;二是实现简单,不需要旋转和变色;三是并发友好,局部修改易于加锁。
[图片]
[图片]
[图片]
[图片]
持久化:
Redis是内存数据库,数据全在内存中。一旦进程崩溃或机器断电,数据全部丢失。持久化就是将内存数据定期或实时地保存到磁盘,重启后恢复。
Redis提供RDB和AOF两种持久化机制:RDB是时间点快照,适合备份和灾难恢复;AOF是命令日志追加,数据安全性更高。Redis 4.0引入的混合持久化结合了两者的优点,是当前生产环境的推荐方案。
RDB
- 核心定位:全量快照,在指定的时间点,手动或自动把内存里所有的数据打包成一个紧凑的二进制文件(dump.rdb)。把内存中的所有数据都记录到磁盘RDB文件中,当Redis实例故障重启后,读取磁盘快照文件,恢复数据速度极快
手动save会阻塞主线程,用bgsave,去fork子进程异步执行,主线程继续服务
copy-on-write(COW):
主进程如果在写数据,触发COW,单独拷贝一份数据,让主进程在上面写,而子进程fork的是那一瞬间的数据
如果写入量大,COW会导致额外内存消耗,极端情况下可能翻倍。生产环境要预留足够内存。
AOF
- 核心定位:增量日志,记录每一个写命令在AOF文件
刷盘策略:何时将缓冲区数据刷到磁盘 - always:同步刷盘,写入内存数据后直接把命令写入AOF文件(优点是几乎零数据丢失;缺点是磁盘 IO 压力极大,严重拖慢 Redis 性能。)
- everysec:每秒刷盘,写内存后先把命令放入Aof缓冲区,后台线性每隔一秒写入AOF文件(优点是性能极高,且最多只会丢失 1 秒钟的数据(适合 99% 的生产环境)。)
- no:操作系统控制,写内存后把命令放Aof缓冲区,由操作系统决定何时将缓冲区内容写入AOF文件(Redis 性能最好,但一旦断电,可能会丢失几秒甚至几分钟的数据。)
bg re write aof命令让AOF文件执行重写功能 最少的命令达到相同效果
开启混合持久化
- 解法:在触发 AOF 重写时,子进程先把当前内存的全量数据以 RDB 格式写到文件前半段,然后把重写期间新产生的增量命令以 AOF 格式追加到后半段。
- 效果:重启时,先秒级加载前半段的 RDB(解决恢复慢的问题),再快速重放后半段的 AOF(解决数据丢失的问题)。既保证了极速恢复,又保证了极高的数据安全性。
分布式锁:
redis命令实现:set unique_key 机器码+线程ID NX EX TTL
- NX (互斥):只有钥匙不在门上(Key不存在)时,才能插进去。保证了同一时刻只有一个人能进。
- EX (防死锁):钥匙上绑了一个倒计时炸弹(TTL)。如果进去的人突发心脏病(服务宕机/网络断开)没出来,倒计时结束后炸弹爆炸,锁自动销毁,后面的人还能继续进。
- 机器码+线程ID (防误删):这是最关键的。假设你进去后业务卡住了,倒计时结束锁自动销毁了。这时别人拿到了新钥匙进去了。等你回过神来,想把锁删掉,如果不校验身份,你就会把别人的锁给删了! 所以,删锁前必须判断:“这锁是我刚才加的吗?”(通常配合 Lua 脚本保证判断和删除的原子性)
Redisson实现:
可重入:利用Hash结构,记录获取锁的线程和获取锁的次数,避免死锁问题 - 大 Key:密室的门牌号(锁名)。
- 小 Key (Field):你的身份证号(UUID + 线程ID)。
- Value:你开门的次数(重入计数器)。
- 加锁逻辑:你第一次进门,Hash 里写入 你的ID: 1;你再去开保险箱,Redisson 发现 Hash 里已经有你的 ID 了,于是把次数变成 你的ID: 2。
- 解锁逻辑:每退出一扇门,次数减 1。只有当次数减到 0 时,才真正把这把锁从 Redis 中删掉。这就完美避免了“自己把自己锁死”的问题。
看门狗机制:当没有指定锁的TTL时,默认会使用 - 默认 TTL 为 30 秒,后台定时 每隔 10 秒 检查业务是否完成。
- 若未完成 → 重置锁 TTL(续期至 30 秒),保证业务完成再释放锁
内存淘汰策略
8种
4针对设置了ttl的key->lru(最近最久未使用的,看最后访问时间),lfu(最近访问频率最低的),random,ttl(剩余时间最短的)
3针对所有key->lru(适合“热点数据明显”的场景,比如新闻首页缓存,经常看的新闻留下,几年没人看的旧新闻扔掉。),lfu,random
1noeviction:啥也不淘汰(适用场景:对数据安全性要求极高,宁可拒绝服务也不能丢数据的场景)
Redsi的key过期了怎么处理,是直接删除吗?
[图片]
Redis内存满了怎么处理?
[图片]
[图片]
[图片]
[图片]
[图片]
Redis为什么快?
[图片]
[图片]
Redis是单线程吗?
[图片]
[图片]
[图片]
[图片]
大key
[图片]
[图片]
热key
[图片]
redis和mysql数据一致性
先更新数据库再删除缓存
在极端并发下,它产生脏数据的概率远低于“先删缓存再更新数据库”。
如果先删缓存,因为写DB会慢很多,在缓存已经被删除,DB未更新成功之前这个间隙内,读请求就会把旧值 回填到缓存,造成脏数据;
而先更DB再删缓存,脏数据只在微秒级窗口内可能发生。
[图片]
缓存击穿雪崩穿透
[图片]
[图片]
[图片]
主从复制
[图片]
[图片]
[图片]
网硕互联帮助中心



评论前必须登录!
注册