Redis 常见面试问题

单线程的redis为什么这么快

分析:这个问题其实是对redis内部机制的一个考察。其实根据博主的面试经验,很多人其实都不知道redis是单线程工作模型
回答:主要是以下三点

(一)纯内存操作
(二)单线程操作,避免了频繁的上下文切换。
(三)采用了非阻塞I/O多路复用机制,内部采用 epoll,epoll 中的读、写、关闭、连接都转化成了事件,然后利用 epoll 的多路复用特性,避免 IO 代价。

经营方式一

客户每送来一份快递,小曲就让一个快递员盯着,然后快递员开车去送快递。慢慢的小曲就发现了这种经营方式存在下述问题
几十个快递员基本上时间都花在了抢车上了,大部分快递员都处在闲置状态,谁抢到了车,谁就能去送快递
随着快递的增多,快递员也越来越多,小曲发现快递店里越来越挤,没办法雇佣新的快递员了
快递员之间的协调很花时间
综合上述缺点,小曲痛定思痛,提出了下面的经营方式

经营方式二

小曲只雇佣一个快递员。然后呢,客户送来的快递,小曲按送达地点标注好,然后依次放在一个地方。最后,那个快递员依次的去取快递,一次拿一个,然后开着车去送快递,送好了就回来拿下一个快递。

对比

上述两种经营方式对比,是不是明显觉得第二种,效率更高,更好呢。在上述比喻中:
每个快递员——————>每个线程went
每个快递——————–>每个socket(I/O流)
快递的送达地点————–>socket的不同状态
客户送快递请求————–>来自客户端的请求
小曲的经营方式————–>服务端运行的代码
一辆车———————->CPU的核数
于是我们有如下结论
1、经营方式一就是传统的并发模型,每个I/O流(快递)都有一个新的线程(快递员)管理。
2、经营方式二就是I/O多路复用。只有单个线程(一个快递员),通过跟踪每个I/O流的状态(每个快递的送达地点),来管理多个I/O流。
在这里插入图片描述
参照上图,简单来说,就是。我们的redis-client在操作的时候,会产生具有不同事件类型的socket。在服务端,有一段I/0多路复用程序,将其置入队列之中。然后,文件事件分派器,依次去队列中取,转发到不同的事件处理器中。
需要说明的是,这个I/O多路复用机制,redis还提供了select、epoll、evport、kqueue等多路复用函数库,大家可以自行去了解。

另外多路IO复用技术类似于拨开关,多个任务通过拨开关的方式共用一条线程,哪个任务需要了开关就拨到哪个任务,避免了CPU在不同的线程中切换,提高效率

如何应对缓存穿透和缓存雪崩问题

缓存穿透

即黑客故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上,从而数据库连接异常。

1.每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如 set -999 UNKNOWN 。然后设置一个过期时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效之前,都可以直接从缓存中取数据。
2. 提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的key。迅速判断出,请求所携带的Key是否合法有效。如果不合法,则直接返回。
3. 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试

缓存雪崩

即缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而导致数据库连接异常。

  1. 给缓存的失效时间,加上一个随机值,避免集体失效。
  2. Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
  3. 双缓存。我们有两个缓存,缓存A和缓存B。缓存A的失效时间为20分钟,缓存B不设失效时间。自己做缓存预热操作。

如何解决redis的并发竞争key问题

分析:这个问题大致就是,同时有多个子系统去set一个key。这个时候要注意什么呢?

  1. 如果对这个key操作,不要求顺序
    这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可,比较简单。
  2. 如果对这个key操作,要求顺序
    假设有一个key1,系统A需要将key1设置为valueA,系统B需要将key1设置为valueB,系统C需要将key1设置为valueC.
    期望按照key1的value值按照 valueA–>valueB–>valueC的顺序变化。这种时候我们在数据写入数据库的时候,需要保存一个时间戳。假设时间戳如下
系统A key 1 {valueA  3:00}
系统B key 1 {valueB  3:05}
系统C key 1 {valueC  3:10}

那么,假设这会系统B先抢到锁,将key1设置为{valueB 3:05}。接下来系统A抢到锁,发现自己的valueA的时间戳早于缓存中的时间戳,那就不做set操作了。以此类推。

redis主从复制

和Mysql主从复制的原因一样,Redis虽然读取写入的速度都特别快,但是也会产生读压力特别大的情况。为了分担读压力,Redis支持主从复制,Redis的主从结构可以采用一主多从或者级联结构,Redis主从复制可以根据是否是全量分为全量同步和增量同步。下图为级联结构。
在这里插入图片描述

全量同步

Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:

  • 从服务器连接主服务器,发送SYNC命令;
  • 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
  • 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
  • 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
  • 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
  • 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
    在这里插入图片描述
    完成上面几个步骤后就完成了从服务器数据初始化的所有操作,从服务器此时可以接收来自用户的读请求。

增量同步

Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。

Redis主从同步策略

主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求重新进行全量同步。

过期时间和过期删除机制

1. 设置过期时间

redis有四种命令可以用于设置键的生存时间和过期时间:

    EXPIRE <KEY> <TTL> : 将键的生存时间设为 ttl 秒
    PEXPIRE <KEY> <TTL> :将键的生存时间设为 ttl 毫秒
    EXPIREAT <KEY> <timestamp> :将键的过期时间设为 timestamp 所指定的秒数时间戳
    PEXPIREAT <KEY> <timestamp>: 将键的过期时间设为 timestamp 所指定的毫秒数时间戳.

2. 保存过期时间

那么redis里面对这些key的过期时间和生存时间的信息是怎么保存的呢??
答:在数据库结构redisDb中的expires字典中保存了数据库中所有键的过期时间,我们称expire这个字典为过期字典。
(1)过期字典是一个指针,指向键空间的某个键对象。
(2)过期字典的值是一个longlong类型的整数,这个整数保存了键所指向的数据库键的过期时间–一个毫秒级的 UNIX 时间戳。

下图是一个带过期字典的数据库例子:
在这里插入图片描述
过期字典是存储在redisDb这个结构里的:

typedef struct redisDb {
    ...
    
    dict *dict;     //数据库键空间,保存着数据库中所有键值对
    dict *expires      // 过期字典,保存着键的过期时间
    ...
} redisDb;

从以上结构中可以看到expire字典(过期字典)和dict字典(数据库键空间,保存着数据库中所有键值对)是并列的,由此可见expire字典的重要性。

3. 移除过期时间

PERSIST 命令可以移除一个键的过期时间:

127.0.0.1:6379> set message "hello"
OK
127.0.0.1:6379> expire message 60
(integer) 1
127.0.0.1:6379> ttl message
(integer) 54
127.0.0.1:6379> persist message
(integer) 1
127.0.0.1:6379> ttl message
(integer) -1

persist命令就是expire命令的反命令,这个函数在过期字典中查找给定的键,并从过期字典中移除。
比如在数据库当前状态(如上图所示),当给book这个key移除过期时间:

redis> persist book
(integer) 1

数据库将更新成如下状态:
在这里插入图片描述
可以从图中看到,当PERSIST book命令执行之后,过期字典中的 book 键消失了。

4. 计算并返回剩余生存时间

ttl命令以秒为单位返回指定键的剩余生存时间。pttl以毫秒返回。两个命令都是通过计算当前时间和过期时间的差值得到剩余生存期的。

127.0.0.1:6379> set minping shuxin
OK
127.0.0.1:6379> expire minping 60
(integer) 1
127.0.0.1:6379> ttl minping
(integer) 57
127.0.0.1:6379> ttl minping
(integer) 27
127.0.0.1:6379> pttl minping
(integer) 23839
127.0.0.1:6379>

redis源码为:

void ttlCommand(redisClient *c) {
    ttlGenericCommand(c, 0);
}
void pttlCommand(redisClient *c) {
    ttlGenericCommand(c, 1);
}
void ttlGenericCommand(redisClient *c, int output_ms) {
    long long expire, ttl = -1;
    /* 如果键不存在,返回-2 */
    if (lookupKeyRead(c->db,c->argv[1]) == NULL) {
        addReplyLongLong(c,-2);
        return;
    }
    
    /* 如果键存在*/
    /*如果没有设置生存时间,返回 -1, 否则返回实际剩余时间 */
    expire = getExpire(c->db,c->argv[1]);
    if (expire != -1) {
        /* 过期时间减去当前时间,就是键的剩余时间*/
        ttl = expire-mstime();
        if (ttl < 0) ttl = 0;
    }
    if (ttl == -1) {
        addReplyLongLong(c,-1);
    } else {
         /*将毫秒转化为秒*/
        addReplyLongLong(c,output_ms ? ttl : ((ttl+500)/1000));
    }
}

5. 过期键的删除策略

如果一个键是过期的,那它到了过期时间之后是不是马上就从内存中被删除呢??如果不是,那过期后到底什么时候被删除呢??

其实有三种不同的删除策略:
(1):立即删除。在设置键的过期时间时,创建一个回调事件,当过期时间达到时,由时间处理器自动执行键的删除操作
(2):惰性删除。键过期了就过期了,不管。每次从dict字典中按key取值时,先检查此key是否已经过期,如果过期了就删除它,并返回nil,如果没过期,就返回键值。
(3):定时删除。每隔一段时间,对expires字典进行检查,删除里面的过期键。
可以看到,第二种为被动删除,第一种和第三种为主动删除,且第一种实时性更高。下面对这三种删除策略进行具体分析。

5.1 立即删除

立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放。但是立即删除对cpu是最不友好的。因为删除操作会占用cpu的时间,如果刚好碰上了cpu很忙的时候,比如正在做交集或排序等计算的时候,就会给cpu造成额外的压力。

而且目前redis事件处理器对时间事件的处理方式–无序链表,查找一个key的时间复杂度为O(n),所以并不适合用来处理大量的时间事件。

5.2 惰性删除

惰性删除是指,某个键值过期后,此键值不会马上被删除,而是等到下次被使用的时候,才会被检查到过期,此时才能得到删除。所以惰性删除的缺点很明显:浪费内存。dict字典和expires字典都要保存这个键值的信息。

举个例子,对于一些按时间点来更新的数据,比如log日志,过期后在很长的一段时间内可能都得不到访问,这样在这段时间内就要拜拜浪费这么多内存来存log。这对于性能非常依赖于内存大小的redis来说,是比较致命的。

5.3 定时删除

从上面分析来看,立即删除会短时间内占用大量cpu,惰性删除会在一段时间内浪费内存,所以定时删除是一个折中的办法。
定时删除是:每隔一段时间执行一次删除操作,并通过限制删除操作执行的时长和频率,来减少删除操作对cpu的影响。另一方面定时删除也有效的减少了因惰性删除带来的内存浪费。

6. redis使用的策略

redis使用的过期键值删除策略是:惰性删除加上定时删除,两者配合使用。

Redis 和 Memcached 的区别

  • Redis 和 Memcache 都是将数据存放在内存中,都是内存数据库。不过 Memcache 还可用于缓存其他东西,例如图片、视频等等
  • Memcache 仅支持key-value结构的数据类型,Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,hash等数据结构的存储。
  • 存储数据安全– Memcache 挂掉后,数据没了; Redis 可以定期保存到磁盘(持久化)
  • Memcache 的单个value最大 1m,Redis 的单个value最大 512m
  • 灾难恢复– Memcache 挂掉后,数据不可恢复; Redis 数据丢失后可以通过 aof 恢复
  • Memcached 网络IO模型是多线程,非阻塞IO复用的网络模型,原型上接近于 nignx。而 Redis使用单线程的IO复用模型,自己封装了一个简单的 AeEvent 事件处理框架,主要实现类epoll,kqueue 和 select,更接近于Apache早期的模式。

参考:
https://www.cnblogs.com/zhuifeng-mayi/p/9301574.html(redis如何实现主从复制?以及数据同步机制?)
https://blog.csdn.net/middleware2018/article/details/80355418(关于redis,学会这8点就够了)
https://blog.csdn.net/wx1528159409/article/details/88358997 (为什么Redis这么快 & 单线程和多路IO复用技术)
https://www.jianshu.com/p/9352d20fb2e0 (redis的过期时间和过期删除机制)
https://zhuanlan.zhihu.com/p/364787132 (Redis的47连环炮,试试你能看住几个)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值