Redis面试系列

137 阅读8分钟

Redis基础

Memcache和Redis的区别?

  • Memcahce
    • 支持简单数据类型
    • 不支持数据持久化
    • 不支持主从
    • 不支持分片
  • Redis
    • 数据类型丰富
    • 支持数据磁盘持久化存储
    • 支持主从
    • 支持分片

为什么Redis这么快?

  • 10W+QPS(每秒查询次数10W+)
  • 完全基于内存,绝大多数请求时纯粹的内存操作,执行效率高
    • 数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
  • 数据结构简单,对数据操作也简单
    • 与关系型数据库相比,Redis不需要去预定义表结构以及关联其他表查询,性能高于关系型数据库
  • 采用单线程,单线程也能处理高并发请求,向多核也可以启动多实例
    • 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU
    • 不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
  • 使用多路I/O复用模型,非阻塞IO
    • 利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,等待IO流进入,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒 ,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作
    • 这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量。

Redis的数据类型

  • String
  • Hash
  • List
  • Set
  • Sorted Set
  • HyperLogLog
    • 用于计数
  • Geo
    • 用于支持存储地理位置信息
    • 指定位置添加经纬度(多个)
    • geoadd key longitude latitude member [key longitude latitude member ...]
    • 查询指定Key的经纬度信息(多个)
    • geopos key member [member ...]
    • 计算两个位置间的距离,可指定单位m米、km千米、mi英里、ft英寸
    • geodist key member1 member2 [unit]
    • 计算指定经纬度位置指定半径范围内的其他位置信息,可指定结果返回个数,距离排序,位置的经纬度,距离,Hash值,半径的单位
    • 可以通过STORE将查询的结果集存储到其他key中,注意命令行不能填写WITH****这些选项
    • georadius key longitude latitude radius m|km|mi|ft [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
    • 计算指定member位置指定半径范围内的其他位置信息,可指定结果返回个数,距离排序,位置的经纬度,距离,Hash值,半径的单位
    • georadiusbymember key longitude latitude radius m|km|mi|ft [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
    • 查询指定位置的geoHash值
    • geohash key member

Redis求交集、并集、差集

  • Sorted Set (Zset)
  • zadd [集合A] [...value]
  • zadd [集合B] [...value]
  • sinter A B - 交集
  • sunion A B - 并集
  • sdiff A B C - BC与A的差集

Redis底层数据类型基础

  • 简单动态字符串
  • 链表
  • 字典
  • 跳跃表
  • 整数集合
  • 压缩列表
  • 对象

从海量数据中查询某一固定前缀Key?

  • keys Patterns
  • 会一次性返回所有匹配的key列表,匹配的数量过大会导致服务卡顿,可能带来内存的消耗和阻塞Redis服务器
  • scan cursor [MATCH pattern] [COUNT count]
  • 游标匹配获取key集合,程序中遍历分批次获取。

如何实现Redis分布式锁?

  • setnx+expire实现的分布式锁
class RedisLock{
    private Redis redis;
    // 获取分布式锁
    public boolean lock(String key,String requestId){
        // 设置key,成功返回1,失败返回0
        Integer flag = redis.setnx(key,requestId);
        // 获取到锁,避免死锁设置过期时间
        if(flag == 1){
            redis.expire(key,10);
            return true;
        }
        return false;
    }
    // 释放分布式锁
    public boolean unLock(String key,String requestId){
        // 先判断是否是自己设置的锁,再进行删除
        Long flag = redis.exec("if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end",key,requestId);
        return flag == 1;
    }
    public static void main(String[] args){
        // 加锁
        if (redisLock.lock("lock","requestId")){
            // 独占资源的业务逻辑
            doMyWork();
            // 解锁
            redisLock.unLock("lock","requestId");
        }
    }
}

以上使用setnxexpire实现的分布式锁有以下的缺陷:
redis的每一条命令都是原子性操作,但是上述setnxexpire分开执行,原子性已不复存在,此时setnx执行完毕后程序发生异常,key未设置过期时间,则将产生key的死锁,使得其他抢占的线程无法获取锁执行下面的业务逻辑;

  • 使用set key value [expiration EX seconds|PX milliseconds] [NX|XX]进行加锁的原子性操作
set lockKey requestId EX 10 NX
# EX 10 - 设置key的过期时间10秒
# NX - 表示key已存在时,返回nil,否则设置lockKey对应的requestId后返回OK

大量的Key同时过期的注意事项

  • 集中过期,由于清除大量的key很耗时,会出现短暂的卡顿现象
  • 解决方案:在设置key的过期时间时,给每个Key加上随机值

如何实现异步队列

  • 使用List数据类型,rpush生产消息,lpop消费消息
    • 缺点:
        1. 没有等待队列有值就直接消费,并非事件驱动模式
        1. 只能提供给一个消费者消费
    • 弥补 1:
      • 可以通过在应用层引入Sleep机制去调用lpop重试
      • 使用blpop阻塞队列直到有消息返回或者超时
  • pub/sub主题订阅模式
    • 发送者(publish)发送消息,订阅者(subscribe)接收消息
    • 订阅者可以订阅任意数量的频道
    • 事件驱动模式,不需要主动去请求队列询问是否有值
    • 订阅同一个topic的多个消费者可以实时接收到消息
    • 缺点:
      • 消息的发布是无状态的,无法保证可达
      • 某个消费者在发布消息时下线后重新上线时获取不到消息的
    • 弥补:使用RabbitMQ等保证消息可靠性的消息中间件

Redis如何做持久化

  • RDB(快照)持久化:保存某个时间点的全量数据快照
    • save:阻塞Redis的服务器进程,直到RDB文件被创建完毕

    • bgsave:Fork出一个子进程来创建RDB文件,不阻塞服务器进程

      • 检查是否已开启AOF持久化,没开启才可进行bgsave
      • 系统调用fork():创建进程,实现Copy-On-Write
    • lastsave: 查看最近一次保存操作的时间戳

    • 自动化触发RDB持久化的方式:

      • 根据redis.conf配置里的save m n定时触发(用的是bgsave)
      • 主从复制时,主节点自动触发
      • 执行Debug Reload
      • 执行shutdown且没有开启AOF持久化
    • 缺点:

      • 内存数据的全量同步,数据量大会由于IO而严重影响性能
      • 可能会因为Redis挂掉而丢失从当前至最近一次快照期间的数据
  • AOF持久化:保存写状态
    • 记录下除了查询以外的所有变更数据库状态的指令
    • 以append的形式追加保存到AOF文件中(增量)
    • 实战操作:
      • 1.修改redis.conf配置
        • appendonly yes 开启AOF增量备份模式
        • appendfilename "appendonly.aof" 指定AOF文件名
        • appendfsync eveysec 每秒进行一次备份
  • RDB和AOF的优缺点
    • RDB
      • 优点:全量数据快照,文件小,恢复快
      • 缺点:无法保存最近一次快照之后的数据
    • AOF
      • 优点:可读性高,适合保存增量数据,数据不易丢失
      • 缺点:文件体积大,恢复时间长
  • RDB-AOF混合持久化方式
    • bgsave做镜像全量持久化,AOF做增量持久化

使用Pipeline的好处

  • Pipeline和Linux的管道类似
  • Redis基于请求/响应模型,单个请求处理需要一一应答
  • Pipeline批量执行指令,节省多次IO往返的时间
  • 有顺序依赖的指令建议分批执行

Redis的同步机制

  • 主从同步原理
    • 全同步过程
      • salve发送sync命令到master
      • master启动一个后台进程,将Redis中的数据快照保存到文件中
      • master将保存的数据快照期间接收到的写命令缓存起来
      • master完成写文件操作后,将该文件发送给salve
      • 使用新的AOF文件替换掉旧的AOF文件
      • master将这期间收集的增量写命令发送给salve
    • 增量同步机制
      • master接收到用户的操作指令,判断是否需要同步到salve(查询操作除外的其他操作)
      • 将操作记录追加到AOF文件
      • 将操作同步到salve:1. 对齐主从库;2. 往响应缓存写入指令
      • 将缓存中的数据发送给salve
    • 缺点
      • 主从模式下,只有一个master节点和多个salve节点,master节点负责写操作,salve负责读操作
      • 当master节点宕机后,redis无法对外提供服务
      • 没有达到高可用
  • Redis Sentinel 哨兵
    • 解决主从同步master宕机后的主从切换问题
      • 监控:检查主从服务器是否运行正常
      • 提醒:通过API向管理员或者其他应用程序发送故障通知
      • 自动故障迁移:主从切换
      • 留言协议Gossip

Redis的集群原理

  • 分片:按照某种规则区划分数据,分散存储在多个节点上