基础数据结构

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上面的

先就这些八股,后面有新的会加入。😚