JavaGuide知识点整理——Redis面试题总结(上)

Redis基础

简单来说redis就是一个使用C语言开发的数据库。不过与传统数据库不同的是Redis的数据是存在内存中的,也就是说它是内存数据库,所以读写速度非常快,因为Redis被广泛用于缓存方向。
另外Redis除了可以做缓存外,还经常被用来做分布式锁,甚至消息队列。
Redis提供了多种数据类型来支持不同的业务场景。Redis还支持事务,持久化,lua脚本,多种集群方案。

分布式缓存常见的技术选型方案有哪些?

分布式缓存的话,使用较多的是memcached和Redis。现在最主流的是Redis。
Memcached是分布式缓存刚兴起那会比较常用的。后来随着Redis发展,Redis使用率更高了。
分布式缓存主要解决的是单机缓存的容量受服务器限制并且无法保持通讯信息的问题。因为本地缓存只有在当前服务里有效。比如部署两个相同的服务器,他们之间的缓存是无法共同的。

Redis和Memcached的区别和共同点

共同点:

  1. 都是基于内存的数据库,一般都被当做缓存使用
  2. 都有过期策略
  3. 两者的性能都非常高

区别:

  1. Redis支持更丰富的数据类型。Memcached只支持K/V对。而Redis支持K/V对,list,set,zset,hash等数据结构的存储。
  2. Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用.Memcached把数据都存在内存中了。
  3. Redis有灾难恢复机制。这也是因为Redis可以持久化。
  4. Redis在服务器内存使用完后,可以将不用的数据放到磁盘上。但是Memcached在服务器内存使用完后会直接报异常
  5. Memcached没有原生的集群模式,需要依靠客户端来实现集群中分片写入数据。Redis目前是原生支持cluster模式的。
  6. Memcached是多线程,非阻塞IO复用的网络模型,而Redis使用的是单线程的多了IO复用模型(Redis6.0引入了多线程IO)
  7. Redis支持发布订阅模型,lua脚本,事务等功能,而Memcached不支持。
  8. Memcached过期数据的删除策略只用了惰性删除。而Redis同时使用了惰性删除与定期删除。

缓存数据的处理流程是怎样的?

简单来说如下:

  1. 用户请求的数据在缓存中有,就直接返回
  2. 缓存中不存在就去查看数据库是否存在
  3. 数据库中存在就更新缓存中的数据
  4. 数据库中不存在直接返回空

为什么要用Redis(为什么要用缓存)?

主要从高性能和高并发两点考虑的。
高性能:
数据库查询是磁盘IO,效率没有从内存中查询快。如果用户访问的数据属于高频且不会经常改变,放在缓存中可以提升访问速度。
高并发:
MySQL的OPS在1w左右,而Redis缓存之后很容易达到10W+。由此可见直接操作缓存能够承受的请求数量远远大于直接访问数据库的。

Redis除了做缓存还能做什么?

  • 分布式锁:通过Redis来做分布式锁是比较常见的方式。通常情况下我们基于Redisson来实现分布式锁(这个锁采用看门狗概念防止宕机死锁)。
  • 限流:一般通过Redis+Lua脚本的方式来实现限流。相关阅读:《我司用了 6 年的 Redis 分布式限流器,可以说是非常厉害了!》
  • 消息队列:Redis自带的list数据结构可以作为一个简单的队列使用。Redis中增加的Stream类型的数据结构更加适合做消息队列。它比较类似于kafka,有主题和消费组的概念,支持消息吃计划以及ACK机制。
  • 复杂业务场景:比如sorted set实现排行榜,实现地理位置距离转换等。

Redis如何实现消息对列?

Redis5.0新增了一个数据结构Stream可以用来做消息对列,其支持:

  • 发布/订阅模式
  • 按照消费组进行消费
  • 消息持久化(RDB和AOF)

不过和专业的消息队列比较,还是有欠缺的地方。比如消息丢失和堆积问题不好解决。还是建议选择市面上成熟的消息队列,比如RocketMQ,Kafka等。

Redis数据结构

Redis常用的数据结构有哪些?

  • 5种基础数据结构:String,List,Set,ZSet,Hash
  • 3种特殊数据结构:HyperLogLogs(基数统计).BitMap(位存储),Geospatial(地理位置)

简单说一下每种类型都有哪些操作:

  • String:
    SET key value 设置指定 key 的值
    SETNX key value 只有在 key 不存在时设置 key 的值
    GET key 获取指定 key 的值
    MSET key1 value1 key2 value2 … 设置一个或多个指定 key 的值
    MGET key1 key2 ... 获取一个或多个指定 key 的值
    STRLEN key 返回 key 所储存的字符串值的长度
    INCR key 将 key 中储存的数字值增一
    DECR key 将 key 中储存的数字值减一
    EXISTS key 判断指定 key 是否存在
    DEL key(通用) 删除指定的 key
    EXPIRE key seconds(通用) 给指定 key 设置过期时间

  • List:
    RPUSH key value1 value2 ... 在指定列表的尾部(右边)添加一个或多个元素
    LPUSH key value1 value2 ... 在指定列表的头部(左边)添加一个或多个元素
    LSET key index value 将指定列表索引 index 位置的值设置为 value
    LPOP key 移除并获取指定列表的第一个元素(最左边)
    RPOP key 移除并获取指定列表的最后一个元素(最右边)
    LLEN key 获取列表元素数量
    LRANGE key start end 获取列表 start 和 end 之间 的元素

  • Hash:
    HSET key field value 设置指定哈希表中指定字段的值
    HSETNX key field value 只有指定字段不存在时设置指定字段的值
    HMSET key field1 value1 field2 value2 ... 同时将一个或多个 field-value (域-值)对设置到指定哈希表中
    HGET key field 获取指定哈希表中指定字段的值
    HMGET key field1 field2 ... 获取指定哈希表中一个或者多个指定字段的值
    HGETALL key 获取指定哈希表中所有的键值对
    HEXISTS key field 查看指定哈希表中指定的字段是否存在
    HDEL key field1 field2 ... 删除一个或多个哈希表字段
    HLEN key 获取指定哈希表中字段的数量

  • Set:
    SADD key member1 member2 ... 向指定集合添加一个或多个元素
    SMEMBERS key 获取指定集合中的所有元素
    SCARD key 获取指定集合的元素数量
    SISMEMBER key member 判断指定元素是否在指定集合中
    SINTER key1 key2 ... 获取给定所有集合的交集
    SINTERSTORE destination key1 key2 ... 将给定所有集合的交集存储在 destination 中
    SUNION key1 key2 ... 获取给定所有集合的并集
    SUNIONSTORE destination key1 key2 ... 将给定所有集合的并集存储在 destination 中
    SDIFF key1 key2 ... 获取给定所有集合的差集
    SDIFFSTORE destination key1 key2 ... 将给定所有集合的差集存储在 destination 中
    SPOP key count 随机移除并获取指定集合中一个或多个元素
    SRANDMEMBER key count 随机获取指定集合中指定数量的元素

  • ZSet:
    ZADD key score1 member1 score2 member2 ... 向指定有序集合添加一个或多个元素
    ZCARD KEY 获取指定有序集合的元素数量
    ZSCORE key member 获取指定有序集合中指定元素的 score 值
    ZINTERSTORE destination numkeys key1 key2 ... 将给定所有有序集合的交集存储在 destination 中,对相同元素对应的 score 值进行 SUM 聚合操作,numkeys 为集合数量
    ZUNIONSTORE destination numkeys key1 key2 ... 求并集,其它和 ZINTERSTORE 类似
    ZDIFF destination numkeys key1 key2 ... 求差集,其它和 ZINTERSTORE 类似
    ZRANGE key start end 获取指定有序集合 start 和 end 之间的元素(score 从低到高)
    ZREVRANGE key start end 获取指定有序集合 start 和 end 之间的元素(score 从高到底)
    ZREVRANK key member 获取指定有序集合中指定元素的排名(score 从大到小排序)

三种特殊类型用法:

  • Bitmap:
    SETBIT key offset value 设置指定 offset 位置的值
    GETBIT key offset 获取指定 offset 位置的值
    BITCOUNT key start end 获取 start 和 end 之前值为 1 的元素个数
    BITOP operation destkey key1 key2 ... 对一个或多个 Bitmap 进行运算,可用运算符有 AND, OR, XOR 以及 NOT

  • HyperLogLog:
    PFADD key element1 element2 ... 添加一个或多个元素到 HyperLogLog 中
    PFCOUNT key1 key2 获取一个或者多个 HyperLogLog 的唯一计数。
    PFMERGE destkey sourcekey1 sourcekey2 ... 将多个 HyperLogLog 合并到 destkey 中,destkey 会结合多个源,算出对应的唯一计数。

  • Geospatial:
    GEOADD key longitude1 latitude1 member1 ... 添加一个或多个元素对应的经纬度信息到 GEO 中
    GEOPOS key member1 member2 ... 返回给定元素的经纬度信息
    GEODIST key member1 member2 M/KM/FT/MI 返回两个给定元素之间的距离
    GEORADIUS key longitude latitude radius distance 获取指定位置附近 distance 范围内的其他元素,支持 ASC(由近到远)、DESC(由远到近)、Count(数量) 等参数
    GEORADIUSBYMEMBER key member radius distance 类似于 GEORADIUS 命令,只是参照的中心点是 GEO 中的元素

String的应用场景有哪些?

  • 常规数据(比如session,token,序列化后的对象)的缓存。
  • 计数。比如用户单位时间的请求数,页面单位时间的访问数。
  • 分布式锁

String还是hash存储对象数据更好呢?

  • String存储的是序列化后的对象数据,存放的是整个对象。HashS是对对象的每个字段单独存储,可以获取部分字段的信息。也可以修改或者添加部分字段,节省网络流量。如果对象中某些字段需要经常变动或者经常单纯查询某个属性,Hash比较合适。
  • String存储相对来说更节省内存,缓存相同数量的对象数据,String消耗的内存是Hash的一般,并且存储具有多重嵌套对象的时候也很方便。如果系统对性能和资源消耗非常敏感的话,String就非常合适。

打个比方,购物车信息建议用Hash存储。用户id为key,商品id为field,商品数量为value。因为购物车中的商品会频繁改动。绝大多数情况我们建议用String存储对象数据。

使用Redis实现一个排行榜

Redis中zset经常被用在各种排行榜的场景,比如直播间礼物排行榜,朋友圈的微信步数排行榜,王者中的段位排行榜等。因为这个数据自带一些命令:zrange(从小到大排序),zrevrange(从大到小排序),zrevrank(按照自定元素排名)

使用Set实现抽奖需要用到什么命令?

  • SPOP key count:随机移除并获取指定集合中一个或者多个元素。适合不允许重复中奖的场景。
  • SRANFMEMBER key count:随机获取指定集合中指定数量的元素,适合允许重复中奖的场景

使用Bitmap统计活跃用户做法

使用日期作为key,用户id为offset,,如果当日活跃过就设置为1.统计的时候用BITOP获取一段时间内的活跃度或者BICOUNT获得某日总活跃度:


Redis线程模型

Redis单线程模型

Redis是基于Reactor模式来设计开发了自己的一套高效的事件处理模型(Netty的线程模型也是基于Reactor模式)。这套事件处理模型对应的是Redis中的文件事件处理器。由于文件事件处理器是单线程方式运行的,所以我们一般都说Redis是单线程模型。
既然是单线程,那么怎么监听大量的客户端连接呢?
Redis通过IO多路复用程序来监听来自客户端的大量连接(或者说是监听多个socket)。它会将感兴趣的时间以及类型注册到内核中并监听每个事件是否发生。
这样的好处非常明显:IO多路复用技术的使用让Redis不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗。
另外Redis服务器是一个事件驱动程序,服务器需要处理两类事件:

  1. 文件事件
  2. 时间事件

时间事件不需要多花时间了解,我们接触最多的还是文件事件(客户端进行读取写入等到左,涉及一系列网络通信)。

文件事件处理器主要包括四个部分:

  • 多个socket
  • IO多路复用程序
  • 文件事件分派器(将socket关联到相应的事件处理器)
  • 事件处理器(连接应答处理器,命令请求处理器,命令回复处理器)


Redis6.0之前为什么都是单线程模式?

虽说Redis是单线程模型,但是4.0之后就加入了对多线程的支持。不过4.0增加的多线程主要是针对一些大的键值对的删除操作命令,使这些命令异步处理。大体来说6.0之前还是单线程的。为什么要这样呢?主要原因有三个:

  1. 单线程编程容易并且更容易维护
  2. Redis的性能瓶颈不在CPU上,主要在内存和网络
  3. 多线程会存在死锁,线程上下文切换等问题,甚至会影响性能

Redis6.0为何引入了多线程?

Redis6.0引入多线程主要是为了提高网络IO读写性能。因为这个也算是性能瓶颈之一(内存和网络)。
虽然6.0引入了多线程,但是只是在网络数据的读写这类耗时操作上使用了。执行命令仍然是单线程顺序执行。
并且6.0的多线程默认是禁用的,如需要开启要修改配置文件:

io-threads-do-reads yes

开启多线程后还需要配置线程数,否则也是不生效的,同样是修改配置文件:

io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程

Redis内存管理

Redis给缓存数据设置过期时间用处

因为内存是有效的,如果缓存中的所有数据都一致保存的话,耗费性能且没必要。Redis子弟了给缓存数据设置国企时间的功能,除了字符串类型有自己独有设置过期时间的命令setex之外,其他方法都需要依靠expire命令来设置过期时间。另外presist命令可以移除一个键的过期时间。

过期时间除了缓解内存的消耗,还可以针对业务需求有实际的效果。比如验证码的有效期是五分钟,如果是传统数据库我们还要自己判断过期时间,更麻烦且性能还差。

Redis是如何判断数据是否过期的?

Redis通过一个叫做过期字典来保存数据的过期时间。过期字典的键指向Redis数据库中的key,值是log类型的证书,保存的key的过期时间。

过期的数据删除策略

有两个策略:

  • 惰性删除:使用的时候做检查,对CPU友好,但是可能造成大量的过期key没有删除,对内存不友好
  • 定期删除:每隔一段时间抽取key执行删除过期key操作。并且通过限制删除操作执行的时长和频率来减少对CPU时间的影响。

Redis采用的是定期删除+惰性删除

Redis内存淘汰机制

  1. volatile-lru:从设置过期时间的数据集中挑选最近最少使用的数据淘汰
  2. volatile-ttl:从设置过期时间的数据集中挑选将要过期的数据淘汰
  3. volatile-random:从设置过期时间的数据集中随机淘汰
  4. allkeys-lru:移除最近最少使用的key
  5. allkeys-random:随机淘汰
  6. no-eviction:不淘汰,内存满了直接报错

4.0版本后增加两个策略:
7.volatile-lfu:从设置过期时间的数据集中挑选最不经常用的淘汰

  1. allkeys-lfu:挑选最不经常用的key淘汰

Redis持久化机制

持久化其实是保证挂掉重启后可以进行数据恢复的保证。Redis相比于Memcached最大的优点也是支持持久化。Redis有两种持久化方式:RDB快照,AOF文件追加。

RDB持久化

创建快照获取存储在内存里的数据的某个时间点上的副本。Redis是默认开启这种持久化方式的,我们可以设置快照的频率。

save 900 1           #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

save 300 10          #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

save 60 10000        #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

AOF持久化

AOF其实就是redis的每一条命令都追加到文件中。恢复的时候从头执行一次就行。但是Redis默认是不开启AOF的。可以通过appendonly参数开启:

appendonly yes

这个原理类似MySQL的日志记录。每一个命令存到缓存文件中,然后根据配置同步到硬盘的文件中。Redis有三种不同的AOP持久化方式:

appendfsync always    #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec  #每秒钟同步一次,显式地将多个写命令同步到硬盘
appendfsync no        #让操作系统决定何时进行同步

AOF重写

其实这个功能不会对原有文件有任何的读取,分析或者写入的操作。而是根据读取数据库中的键值对来实现的。
比如对key1 这个键 设置值1,然后改成2,然后改成lsj,然后改成ll。正常的aof会有四条命令。但是其实有效的只有key1 = ll这一个。所以重写后会清理很多无用的东西。
重写本质就是读取当前kv.然后读取过程中创建线程记录读取时候的命令,在读取完后追加读取过程中的命令作为一个新的AOF文件替换旧的,这就是AOF重写。

Redis4.0对持久化机制优化

Redis4.0开始支持AOF和RDB混合持久化。
AOF重写的时候直接把RDB的内容写到AOF文件开头,既快速又避免丢失过多数据。缺点就是AOF文件不再是AOF格式,可读性差。

本篇笔记就记到这里,如果稍微帮到你了记得点个喜欢点个关注。也祝大家工作顺顺利利,中秋快乐哟~!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章