1、Redis 集群如何选择数据库?
Redis 集群目前无法做数据库选择,默认在 0 数据库。
2、Redis 中的管道有什么用?
一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应,这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。
这就是管道(pipelining),是一种几十年来广泛使用的技术。例如许多 POP3 协议已经实现支持这个功能,大大加快了从服务器下载新邮件的过程。
3、怎么理解 Redis 事务?
事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行,事务在执行的过程中,不会
被其他客户端发送来的命令请求所打断。
事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
4、Redis 事务相关的命令有哪几个?
MULTI、EXEC、DISCARD、WATCH
5、Redis key 的过期时间和永久有效分别怎么设置?
EXPIRE 和 PERSIST 命令
6、Redis 如何做内存优化?
尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。
比如你的 web 系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的 key,而是应该把这个用户的所有信息存储到一张散列表里面。
7、Redis 回收进程如何工作的?
一个客户端运行了新的命令,添加了新的数据。
Redi 检查内存使用情况,如果大于 maxmemory 的限制, 则根据设定好的策略进行回收。一个新的命令被执行,等等。
所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下。如果一个命令的结果导致大量内存被使用(例如很大的集合的交集保存到一个新的键),不用多久内存限制就会被这个内存使用量超越。
8. 加锁机制
咱们来看上面那张图,现在某个客户端要加锁。如果该客户端面对的是一个 redis cluster 集群,他首先会根据 hash 节点选择一台机器。 这里注意,仅仅只是选择一台机器!这点很关键!紧接着,就会发送一段 lua 脚本到 redis 上,那段 lua 脚本如下所示:
为啥要用 lua 脚本呢?因为一大坨复杂的业务逻辑,可以通过封装在 lua 脚本中发送给 redis,保证这段复杂业务逻辑执行的 原子性。
那么,这段 lua 脚本是什么意思呢?这里 KEYS[1]代表的是你加锁的那个 key,比如说:RLock lock = redisson.getLock(“myLock”);这里你自己设置了加锁的那个锁 key 就是“myLock”。
ARGV[1]代表的就是锁 key 的默认生存时间,默认 30 秒。ARGV[2]代表的是加锁的客户端的 ID,类似于下面这样:8743c9c0-0795-4907-87fd-6c719a6b4586:1
给大家解释一下,第一段 if 判断语句,就是用“exists myLock”命令判断一下,如果你要加锁的那个锁 key 不存在的话,你就进行加锁。如何加锁呢?很简单, 用下面的命令:hset myLock
8743c9c0-0795-4907-87fd-6c719a6b4586:1 1,通过这个命令设置一个 hash 数据结构,这行命令执行后,会出现一个类似下面的数据结构:
上述就代表“8743c9c0-0795-4907-87fd-6c719a6b4586:1”这个客户端对“myLock”这个锁 key 完成了加锁。接着会执行“pexpire myLock 30000”命令,设置 myLock 这个锁 key 的 生存时间是 是 30 秒。好了,到此为止,ok,加锁完成了。
9. 锁互斥机制
那么在这个时候,如果客户端 2 来尝试加锁,执行了同样的一段 lua 脚本,会咋样呢?很简单,第一个 if 判断会执行“exists myLock”,发现 myLock 这个锁 key 已经存在了。接着第二个 if 判断,判断一下,myLock 锁 key 的 hash 数据结构中,是否包含客户端 2 的 ID,但是明显不是的,因为那里包含的是客户端 1 的 ID。
所以,客户端 2 会获取到 pttl myLock 返回的一个数字,这个数字代表了 myLock 这个锁 key的 剩余生存时间。比如还剩 15000 毫秒的生存时间。此时客户端 2 会进入一个 while 循环,不停的尝试加锁。
10.watch dog 自动延期机制
客户端 1 加锁的锁 key 默认生存时间才 30 秒,如果超过了 30 秒,客户端 1 还想一直持有这把锁,怎么办呢?
简单!只要客户端 1 一旦加锁成功,就会启动一个 watch dog 看门狗, 他是一个后台线程,会每隔 10 秒检查一下,如果客户端 1 还持有锁 key,那么就会不断的延长锁 key 的生存时间。
11. 可重入加锁机制
那如果客户端 1 都已经持有了这把锁了,结果可重入的加锁会怎么样呢?比如下面这种代码:
这时我们来分析一下上面那段 lua 脚本。 第一个 if 判断肯定不成立,“exists myLock”会显示锁key 已经存在了。 第二个 if 判断会成立,因为 myLock 的 hash 数据结构中包含的那个 ID,就是客户端 1 的那个 ID,也就是“8743c9c0-0795-4907-87fd-6c719a6b4586:1”
此时就会执行可重入加锁的逻辑,他会用:
incrby myLock 8743c9c0-0795-4907-87fd-6c71a6b4586:1 1 ,通过这个命令,对客户端 1的加锁次数,累加 1。此时 myLock 数据结构变为下面这样:
大家看到了吧,那个 myLock 的 hash 数据结构中的那个客户端 ID,就对应着加锁的次数
12. 释放锁机制
如果执行 lock.unlock(),就可以释放分布式锁,此时的业务逻辑也是非常简单的。其实说白了,就是每次都对 myLock 数据结构中的那个加锁次数减 1。如果发现加锁次数是 0 了,说明这个客户端已经不再持有锁了,此时就会用:“del myLock” 命令,从 redis 里删除这个 key。
然后呢,另外的客户端 2 就可以尝试完成加锁了。这就是所谓的 分布式锁的开源 Redisson 框架的实现机制。
一般我们在生产系统中,可以用 Redisson 框架提供的这个类库来基于 redis 进行分布式锁的加锁与释放锁。
13. 上述 Redis 分布式锁的缺点
其实上面那种方案最大的问题,就是如果你对某个 redis master 实例,写入了 myLock 这种锁key 的 value,此时会异步复制给对应的 master slave 实例。但是这个过程中一旦发生 redis master 宕机,主备切换,redis slave 变为了 redis master。接着就会导致,客户端 2 来尝试加锁的时候,在新的 redis master 上完成了加锁,而客户端 1也以为自己成功加了锁。此时就会导致多个客户端对一个分布式锁完成了加锁。这时系统在业务语义上一定会出现问题, 导致各种脏数据的产生。所以这个就是 redis cluster,或者是 redis master-slave 架构的主从异步复制导致的 redis 分布式锁的最大缺陷:在 在 redis master 实例宕机的时候,可能导致多个客户端同时完成加锁。
14. 使用过 Redis 分布式锁么,它是怎么实现的?
先拿 setnx 来争抢锁,抢到之后,再用 expire 给锁加一个过期时间防止锁忘记了释放。如果在 setnx 之后执行 expire 之前进程意外 crash 或者要重启维护了,那会怎么样?set 指令有非常复杂的参数,这个应该是可以同时把 setnx 和 expire 合成一条指令来用的!
15. 使用过 Redis 做异步队列么,你是怎么用的?有什么缺点?
一般使用 list 结构作为队列,rpush 生产消息,lpop 消费消息。当 lpop 没有消息的时候,要适当 sleep一会再重试。
缺点:
在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如 rabbitmq 等。
能不能生产一次消费多次呢?
使用 pub/sub 主题订阅者模式,可以实现 1:N 的消息队列。
16. 什么是缓存穿透?如何避免?什么是缓存雪崩?何如避免?
缓存穿透
一般的缓存系统,都是按照 key 去缓存查询,如果不存在对应的 value,就应该去后端系统查找(比如
DB)。一些恶意的请求会故意查询不存在的 key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。
如何避免?
1:对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该 key 对应的数据 insert 了之后清缓存。
2:对一定不存在的 key 进行过滤。可以把所有的可能存在的 key 放到一个大的 Bitmap 中,查询时通过该 bitmap 过滤。
缓存雪崩
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力。导致系统崩溃。
如何避免?
1:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线
程查询数据和写缓存,其他线程等待。
2:做二级缓存,A1 为原始缓存,A2 为拷贝缓存,A1 失效时,可以访问 A2,A1 缓存失效时间设置为
短期,A2 设置为长期
3:不同的 key,设置不同的过期时间,让缓存失效的时间点尽量均匀