基础数据结构
String
常用命令
SET key value
GET key
INCR key
DECR KEY
APPEND key value
STRLEN key
底层数据结构
简单动态字符串
Hash
常用命令
HSET key fieldname value
HGET key fieldname
HGETALL key
HDEL key field name
HKEYS ket
HVALS key
底层数据结构
压缩列表+哈希表
Set
常用命令
SADD key member
SMEMBERS key
SREM key member
SINTER key1 key2
SUNION key1 key2
SDIFF key1 key2
底层数据结构
整数集合+哈希表
List
常用命令
LPUSH key value
RPUSH key value
LPOP key
RPOP key
LRANGE key start stop
LLEN key
底层数据结构
压缩列表+双向链表
Sorted Set
常用命令
ZADD key score member
ZRANGE key start stop
ZSCORE key member
ZRANK key member
ZREVRANK key member
ZREM key member
底层数据结构
压缩列表+哈希表+跳表
Bitmap
常用命令
SETBIT key offset value
GETBIT key offset
BITCOUNT key
BITOP operation destkey key1 key2
底层数据结构
字符串
缓存
缓存穿透
是指查询一个不存在的数据,由于查不到数据所以不会写入缓存,每次查找都会去数据库,会导致数据库挂掉
解决方案:
缓存空值
查找不到,直接设置一个null值到缓存里面,下次查找,直接返回null。
布隆过滤器
先初始化一个全为0的大数组,然后经过三次hash后,得到三个下标,唯一标识一个key,存在误判,一般情况,误判率为5%。
缓存击穿
设置了过期时间的key在某个时间点刚好过期,这个时候大量请求发送过来,发现缓存过期,就去访问数据库,数据库就会被压垮。
解决方案:
设置key永不过期
设置互斥锁
先不去加载数据库,用redis中的SETNX设置一个互斥锁,只允许一个线程去访问数据库。
缓存雪崩
大量key过期时间一样,同一时间失效,造成大量数据丢失。
解决方案:
设置不同的过期时间,可以在当前过期时间加上一些随机值,让他们不要同一时间过期。
与数据库的缓存一致性问题
延时双删
先删缓存 → 更新数据库 → 延时后再次删缓存。
写时更新缓存
更新数据库的同时,同步更新缓存。
更新缓存失败,需有回滚机制。
高并发写时,可能因时序问题导致缓存与数据库不一致
写时删除缓存
先更新数据库,再删缓存,删缓存失败时需重试机制,下次读取时再加载到缓存。
读写锁
读的时候添加共享锁,保证读读不互斥
写的时候添加排他锁,保证读写互斥,写写互斥。保证不会被污染数据,使用的是同一把锁。排他锁底层使用的是SETNX,保证当前时刻只有一个线程被锁住。
Canal+binlog实现
Canal读取binlog数据,再从Canal的客户端获取数据,更新缓存即可。
分布式锁
锁过期
引入看门狗机制,每隔一段时间去查看当前业务是否持有锁,增加锁的持有时间,执行完成之后,释放锁
可重入避免死锁
内部判断这个锁是不是当前线程持有的锁,是就统计计数加1,释放了的话就减1,直到计数为0,锁会被真正的释放,然后被其他线程获取,操作共享资源,是一个hash结构
误删锁问题
使用唯一标识确保只有持有锁的客服端才能释放锁
主从一致
存在一些问题
写入锁信息后崩溃了,数据没有同步到从节点,怎么解决😰
节点崩溃,从节点提升为新的主节点,新的主节点没有最新的锁信息😥
持久化💪
有两种记录数据机制
RDB
它是生成数据快照保存数据,定期将数据内的数据存进一个二进制文件dump.rdb,redis重启后,加载RDB文件中数据到内存
手动触发
通过SAVE和BGSAVE生成快照
自动触发
配置文件中的规则自动生成快照
在 redis.conf
配置,比如:
save 900 1 # 900秒(15分钟)内至少1个key被修改,就触发保存
save 300 10 # 300秒(5分钟)内至少10个key被修改,就触发保存
❌ 可能丢数据:如果 Redis 突然崩溃,上次快照之后的数据就没了(比如 5 分钟前的数据)。
AOF
redis每次将写操作保存到AOF中appendonly.aof,AOF中记录所有的写操作,每次重启后,重新执行AOF文件中的写操作恢复数据。在 redis.conf
配置:
appendonly yes # 开启AOF
appendfsync everysec # 每秒刷盘(推荐)
数据是先写到 内存缓冲区,再由操作系统决定何时写入磁盘。刷盘策略就是决定这个写入时期的。
AOF 刷盘策略(appendfsync
)
always
:每次执行写命令后,立即调用fsync()
强制写入磁盘。(最安全,但性能差)。everysec
⭐:后台线程 每秒调用一次fsync()
,把累积的写操作刷到磁盘。(平衡性能和数据安全)。no
🤣: Redis 不主动刷盘,完全依赖操作系统(最快,但可能丢数据)。
其中,fsync()
是 Linux/Unix 的系统调用,确保数据真正写入磁盘,而不是停留在内存缓存里。
✅ 数据更安全:最多丢 1 秒的数据(
everysec
配置)。
✅ 可读性强:AOF 是文本文件,可以手动修改(比如误删数据可以修复)。
混合模式(RDB+AOF)
定期用 RDB 存快照,两次 RDB 之间的数据用 AOF 记录。
aof-use-rdb-preamble yes # 开启混合模式
集群
主从复制
一般一主多从,主节点负责写数据,从节点读数据,主节点写入数据后,同步数据到从节点。在主节点故障时,从节点可以接管服务,确保系统的可用性。
同步流程
全量同步
从节点向主节点发送PSYNC命令,请求同步
主节点生成当前数据的RDB快照,发送给从节点
从节点接收到RDB文件,加载到内存中
主节点将RDB生成期间的写操作缓存到复制缓冲区起来,RDB同步后通过 TCP 连接发送给从节点
从节点应用这些写操作
示意图:
💞关键点:
复制偏移量:主节点和从节点都会维护一个复制偏移量,表示已同步的数据量。全局计数器,用于记录主节点和从节点之间已同步的数据量。
复制缓冲区:主节点会维护一个复制缓冲区,用于缓存最近的写操作。用于缓存主节点最近的写操作,以便在增量同步时发送给从节点。
💗心跳机制:主节点和从节点之间会定期发送心跳包,检测连接状态。
增量同步
从节点向主节点发送PSYNC命令,携带自己的复制偏移量
主节点检查从节点的复制偏移量,如果在复制缓冲区范围内,发送增量数据
从节点接受增量数据,应用到本地
哨兵模式
实现主从集群的自动故障恢复,里面就包含了对主从服务的监控、自动故障恢复、通知
实现
主节点故障,sentinel会将一个slave提升为master,故障恢复后以新的master为主
sentinel在集群发生故障转移时,将最新的信息推送给redis的客户端
问题
脑裂
由于网络抖动等原因,master、slave、sentinel处于不同的网络分区,sentinel心跳感知不到master,会选举一个新的master,客户端还不知道选举了一个新的master,会将数据发送到old master,new master无法同步数据,网络恢复后,sentinel又会将old master 降级为slave ,再从new master同步数据,这样旧的master数据就丢失了
解决
设置多个从节点,至少有一个从节点时,才能同步数据
设置主从复制和同步延迟时间,达不到要求就拒绝
分片集群
集群中有多个master,每个master有多个slave,master之间还能通过ping检测彼此的健康状态,客户端可以任意访问。
工作流程
客户端发送请求
redis计算键的哈希值,将请求转发到哈希槽的主节点
主节点处理
写操作:同步到主节点
读操作:计算后去找对应的主/从节点,然后直接返回数据给客户端
😠关键:
引入哈希槽概念,16384个哈希槽,每个节点绑定了一个哈希槽范围,key通过CRC16检验后取模决定放置位置
内存管理
数据过期
定期删除
定期随机检查一部分设置了过期时间的键,删除其中已经过期的键
参数HZ,默认10,控制执行频率。
参数maxmemory-samples,默认5,空值检查键数量。
惰性删除
客服端访问一个键时,redis检查该键是否过期。如果已经过期,立即删除,返回空值。未过期,正常返回键的值
数据淘汰
已经过期的数据或者是当前内存被占满了,就需要淘汰呀,怎么实现呢?接下来我告诉你鸭。
noeviction:不删除任何键,内存不住,返回错误
allkeys-lru:所有键中删除最近最少使用的键,询问内存超出时,可以说使用了这个策略,或者询问如何保证多少数据中哪些是热点数据,就说用了去淘汰。
volatile-lru:从设置了过期时间的键中删除最近最少使用的键
allkeys-random:所有键中随即删除键
volatile-random:从设置了过期时间里面随即删除键
volatile-ttl:设置了过期时间的键中删除剩余时间最短的键
其中,使用的LRU和LFU都是改良过的,那先介绍一下没改良前的底层原理。
LRU
底层数据结构
哈希表+双向链表(维护数据访问顺序,最近访问的数据放在链表头部。)
具体实现
数据访问时,移动到链表头部,不在就添加到链表头部
缓存空间不足时,就删除尾部最久未被访问的数据
具体优化:随机采样淘汰
LFU
底层数据结构
哈希表+计数器(记录每个键的访问频率)
具体实现
数据被访问时,增加其访问频率计数器,为了避免无限增长,对计数器进行衰减。
缓存空间不足时,淘汰访问频率最低数据。
具体优化:随机采样淘汰概率衰减和随机采样
快速单线程
纯内存操作,C语言实现
单线程模式,避免多线程的上下文切换,减少CPU浪费;避免了锁的竞争;还是原子性的操作
优化的数据结构,比如动态字符串,O(1) 长度获取;List/Hash 小数据时内存优化
避免系统调用,如 SET/GET 不经过内核缓冲区。同时还有持久化优化,不影响性能
协议简单,RESP是文本协议
最后是非阻塞的I/O,通过 epoll/kqueue 多路复用处理网络请求,高并发下依然高效
单个线程同时监听多个Socket,减少了系统资源消耗
实现
SELECT:支持的描述符数量有限,位掩码存储,遍历。性能较低
POLL:描述符数量没有限制,但是仍然需要拷贝和遍历数组,效率低
EPLOO:支持大量文件描述符,使用红黑树和链表管理文件描述符,但是只能在linux上面使用。在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能
KQUEUE:类似EPOLL,在macos上面的
先就这些八股,后面有新的会加入。😚
评论