1. 有哪些数据类型?底层分别使用什么数据结构?

1
2
3
4
5
6
7
8
type def struct redisObject {
    // 类型
    unsigned type
    // 编码方式
    unsigned encoding
    // 指向底层数据结构的指针
    void *ptr
} robj
  • String:
    • int: 底层使用long类型存储
    • raw: 简单动态字符串SDS(含总长和剩余长度信息),字符长度大于39 byte
    • embstr: 字符串长度<= 39 byte,redisObject和sdshdr结构在一块连续的内存中,浮点数也使用这种结构存储
  • List
    • ziplist: 当元素个数小于512且元素长度小于64字节时使用
    • linkedlist
  • Hash
    • ziplist: 当元素个数小于512且元素长度小于64字节时使用
    • hashtable
  • Set
    • intset: 当元素个数小于512且元素全是整数时使用
    • hashtable
  • SortedSet
    • ziplist: 当元素个数小于128且元素长度小于64字节时使用
    • skiplist: 底层实际使用了skiplist和dict两种结构,且他们会通过指针来共享成员和分数

2. 持久化方式有哪些?

AOF(Append Only File)

AOF是写后日志,已追加的方式记录的是每一条写命令

三种写回策略

  • Always: 每个写命令执行完后立即将日志写回磁盘
  • Everysec: 将日志写入文件缓存,每秒执行fsyncl落盘
  • No: 操作系统决定格式将缓冲区的内存写会磁盘

AOF重写机制 fork一个子进程bgrewriteaof,逐一将每个键值对用一条命令记录并写入临时文件,完成后会通过信号通知主进程,主进程会将AOF重新缓冲区的内容写入临时文件中,这时临时文件保存的内容和数据库一致,对临时文件改名,原子的覆盖 现有的AOF文件

RDB(Redis DataBase)

fork一个子进程将全量的数据以二进制的方式写入RDB文件

Redis4.0推出了混合使用AOF和RDB的方式,在两个RBD快照之间使用AOF日志记录期间的所有命令,等下一次做全量快照时,可以清空AOF日志

3. 主从同步是怎么样的?

初始同步

  • 从库发送`psync ? -1命令申请全量复制
  • 主库执行bgsave生成RBD文件,发送给从库加载数据
  • 主库将期间新的写入操作replication buffer中的数据发送给从库

增量同步

主库有一个repl_backlog_buffer环形缓冲区,主库依据自己的master_repl_offset和从库的slave_repl_offset之间的操作发给从库

4. 如何保证高可用?

通过哨兵机制保证高可用,哨兵负责监控节点、选主和通知

  • 监控:哨兵会周期性的给主/从节点发ping命令,如果规定时间内没有响应,对于从库将其标记为下线状态,对于主库则进行切换主库流程
  • 选主:
    • 客观下线:当有quronum个哨兵判断主库下线,才判断为客观下线
    • 筛选打分:
      • 去除已下线和总是断连的从节点
      • 选择复制进度最高的节点
      • 如果多个选ID最小的
  • 通知:
    • 新主库选好后,会给所有从库发信主库连接信息,随之从库会设置主库,重新从新主库同步数据
    • 另外也会在switch-master频道里面发送主库信息,客户端可以通过订阅收到消息

5. 哨兵集群是如何工作的?

  • 相互发现:通过pub/sub主库的__sentinel:hello__频道来相互发现,随之建立网络连接
  • 发现从库:通过INFO命令获取从库信息
  • Leader选举:当哨兵拿到quronum个主观下线后,就将主库标记为客观下线,随后发起选举命令,如果获得>=N/2+1且>=quronum张票,则选为Leader(注:如果此轮没有产生Leader,会过段时间重试;每个哨兵的定时器执行周期都会加上一个小小的随机时间偏移,目的是让每个哨兵执行上述操作的时间能稍微错开些,也是为了避免它们都同时判定主库下线,同时选举Leader)

6. Cluster分片集群是如何工作的?

  • 集群分为16384(2^14 == 2KB)个hash slot哈希槽,key通过CRC算法得到一个16bit的值,再对16384取模,确定落在哪个槽中

  • 创建cluster时会自动给每个实例分配16384/N个槽,也可以手动指定

  • 实例之间通过相互传递消息获取最新哈希槽分布信息,

  • 客户端和集群连接时,会获得哈希槽对应节点的分布信息,并缓存在本地,之后如果分布信息变更后,客户端把命名发送给了旧的节点,旧的节点会将新节点的地址信息告知客户端,其中有 两种状态:

    • 哈希槽已经迁移完毕,旧节点会返回MOVED信息,客户端的缓存也会更新
    1
    2
    
    GET hello:key
    (error) MOVED 13320 172.16.19.5:6379
    
    • 哈希槽正在迁移,旧节点会返回ASK信息
    1
    2
    
    GET hello:key
    (error) ASK 13320 172.16.19.5:6379
    

7. 事务的实现?

通过结合WATCHMULTIEXEC命令实现事务,执行EXEC时会检查WATCH的键是否有变更,如有则取消事务

原子性:如果执行事务命令队列中的某个命令出错,仍会继续执行下去,无法回滚

8. 有哪些耗时操作?

  • 全量查询和聚合等复杂操作
  • bigkey删除:释放大量内存很耗时 (用专门负责删除的子线程来处理)
  • AOF写回磁盘 (频率设置为everysec后用专门负责AOF日志落盘的子线程来处理)

9. 有哪些淘汰缓存策略?

不进行淘汰:noeviction

对所有数据进行淘汰

  • allkeys-lru
  • allkeys-random
  • allkeys-lfu

对设置了过期时间(无论有没有到期)的数据进行淘汰

  • volatile-random: 随机删除
  • volatile-ttl: 越早过期的数据越先被删除
  • volatile-lru: 每个RedisObject有一个lru字段,记录最新访问时间,淘汰数据时会随机选取N个数据作为一个候选集合,将lru字段最小的数据淘汰
  • volatile-lfu: 在lru的基础上为每个数据统计访问次数,优先淘汰次数低的;范围次数相同的情况下淘汰lru更小的

10. 有哪些缓存使用模式?

  • Cache Aside只读缓存:
    • 读操作命中缓存则直接返回,否则从后端数据库加载到缓存中再返回
    • 写操作直接更新数据库然后删除缓存
  • Read/Write Throught 同步直写
    • 写操作会更新缓存和数据库,然后给客户端返回
  • Read/Write Write Back 异步写回
    • 写操作先更新缓存,等到缓存要淘汰时将它们写回数据库

如果需要给写请求加速,选择读写缓存;如果写请求很少选择只读缓存

11. 有哪些缓存异常

缓存雪崩: 大量请求无法在Redis中进行处理,导致数据库层压力激增

通常因为大量数据同时过期,可以给数据过期时间加一个小的随机数

可通过服务降级来应对

缓存击穿:热点请求没有在缓存中处理

通常发生在热点数据过期时,可以对热点数据不设置过期时间

缓存穿透:访问的数据既不在redis中也不在数据库中

解决方案:a. redis缓存一个缺省值 b. 查询数据库时先通过查询布隆过滤器快速判断数据是否存在

12. 分布式锁实现?

单个redis

1
2
3
SET lock_key unique_value NX PX 10000
DO SOMETHING
DEL lock_key

使用SETNX和DEL, 为了DEL可能异常,需要给key设置过期时间

多个redis节点 Redlock

分为三步:

  • 客户端获取当前时间
  • 按顺序依次想N个redis示例执行加锁操作,加锁操作设置超时时间,超时则认为该示例加锁失败
  • 计算加锁总耗时

如果获取了N/2+1把锁且总耗时没有超过锁的有效时间则认为加锁成功 - 哈希槽正在迁移,旧节点会返回ASK信息

```redis
GET hello:key
(error) ASK 13320 172.16.19.5:6379
```