1、redis到底是单线程还是多线程?
Redis作为一个成熟的分布式缓存框架,由很多个模块组成,如网络请求模块、索引模块、存储模块、高可用集群支撑模块、数据操作模块等。
目前所说的Redis单线程,指的是"其网络IO和键值对读写是由一个线程完成的",也就是说,Redis中只有网络请求模块和数据操作模块是单线程的。而其他的如持久化存储模块、集群支撑模块等是多线程的。在提升I/O利用率这个方面上,Redis并没有采用多线程技术,而是选择了I/O多路复用技术。
Redis 6.0中的多线程,也只是针对处理网络请求过程采用了多线程,而数据的读写命令,仍然是单线程处理的。Redis 6.0 只有在网络请求的接收和解析,以及请求后的数据通过网络返回时,使用了多线程。而数据读写操作还是由单线程来完成的,所以,这样就不会出现并发问题了。
2、为什么网络操作模块和数据存储模块最初并没有使用多线程呢?
1、多线程的使用场景:
一个计算机程序在执行过程中,主要进行两个操作:读写操作和计算操作。其中读写操作主要是涉及到的就是I/O操作,其中包括网络I/O和磁盘I/O。计算操作主要涉及到CPU。
而多线程的目的,就是通过并发的方式来提升I/O的利用率和CPU的利用率。
而Redis的操作基本都是基于内存的,CPU资源根本就不是Redis的性能瓶颈,所以Redis不需要通过多线程技术来提升CPU利用率。
而提升I/O利用率,确实很有必要,但并不是只可以采用多线程技术这一条路。
2、多线程的弊端:
采用多线程可以帮助我们提升CPU和I/O的利用率,但是多线程带来的并发问题也给这些语言和框架带来了更多的复杂性。而且,多线程模型中,多个线程的互相切换也会带来一定的性能开销。
3、redis单线程,为什么还那么快?
1、命令执行基于内存操作,一条命令在内存中操作的时间是几十纳秒;
2、命令执行是单线程操作,没有线程切换开销;
3、基于I/O多路复用机制提升redis的I/O利用率;
4、高效的数据存储结构:全局hash表 以及多种高效数据结构,比如:跳表、压缩列表、链表等。
4、redis底层数据是如何用跳表来存储的?
Redis的ZSet集合和java中Set集合非常相似,都是唯一不重复的元素,不同的是Zset中有一个叫做权重的score,会进行有序的排序。
Redis使用跳表来存储Zset集合将有序链表使用分之思想改造成折半查找,【在链表上建索引】(有点和线段树相似的思想,空间换时间),可快速进行插入删除、查询。
5、redis 的key过期了为什么内存没有释放?
1、使用set命令的如果不设置过期时间,那么redis会自动擦除这个key的过期时间。所以在修改key的值时,需要注意它的过期时间。
2、redis对于过期 key的处理一般有 惰性删除和定时删除两种策略:
(1)惰性删除:当读写一个已经过期的 key 时,会触发惰性删除策略,判断 key 是否过期,如果过期了直接删除掉这个 key;(2)定时删除:由于惰性删除策略无法保证冷数据被及时删掉,所以 redis 会定期(默认每 100 ms) 主动淘汰一批己过期的 key,这里的一批只是部分过期 key,所以可能会出现部分 key 已经过期但还没有被清理掉的情况,导致内存并没有被释放。
6、redis 没有设置过期时间为什么被 redis 主动删除了?
当 redis 已用内存超过 maxmemory 限定时,触发主动清理策略,主动清理策略在 redis 4.0 之前一共实现了 6 种内存淘汰策略,在 4.0 之后,又增加了 2 种策略,总共 8 种:
针对设置了过期时间的 key 做处理:(1) volatile-ttl:在筛选时,会针对设置了过期时间的键值对,根据过期时间的先后进行删除,越早过期的越先被删除。(2) volatile-random:就像它的名称一样,在设置了过期时间的键值对中,进行随机删除。(3) volatile-lru:会使用 lru 算法筛选设置了过期时间的键值对删除。(4) volatile-lfu:会使用 lfu 算法筛选设置了过期时间的键值对删除。
针对所有的 key 做处理:(5) allkeys-random:从所有键值对中随机选择并删除数据。(6) allkeys-lru:使用 lru 算法在所有数据中进行筛选删除。(7) allkeys-lfu:使用 lfu 算法在所有数据中进行筛选删除。
不处理:(8) noeviction:不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息 "(error) OOM command not allowed when used memory",此时 redis 只响应读操作。
7、redis 淘汰算法 lru 和 lfu 的区别?
lru 算法 (Least Recently Used,最近最少使用):淘汰很久没被访问过的数据,以最近一次访问时间作为参考;lfu 算法 (Least Frequently Used,最不经常使用):淘汰最近一段时间被访问次数最少的数据,以次数作为参考;绝大多数情况我们都可以用 lru 策略,当存在大量的热点缓存数据时,lfu 可能更好点;
8、删除 key 的命令会阻塞 redis 吗?
首先需要搞清楚删除 key 的执行过程,对于不同的 key 删除的过程是不一样的,如果删除的是一个字符串那么时间复杂度为 O(1),
删除单个列表、集合、有序集合或哈希表类型的 key,时间复杂度为 O (M),M 为以上数据结构内的元素数量,当删除的是列表,集合或者是哈希表类型的时候会耗费比较多的时候所以会阻塞 redis, 即使删除的是字符串如果字符串对应的值比较大的时候其实也会耗费一定的时间所以也会阻塞 redis;时间复杂度:参考https://aijishu.com/a/1060000000008661
9、redis 主从,哨兵,集群架构优缺点比较?
在 redis 3.0 以前的版本要实现集群一般是借助哨兵 sentinel 工具来监控 master 节点的状态,如果master 节点异常,则会做主从切换,将某一台 slave 作为 master,哨兵的配置略微复杂,并且性能和高可用性等各方面表现一般,特别是在主从切换的瞬间存在访问瞬断的情况,而且哨兵模式只有一个主节点对外提供服务,没法支持很高的并发,且单个主节点内存也不宜设置得过大,否则会导致持久化文件过大,影响数据恢复或主从同步的效率;
redis 集群是一个由多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片特性。redis 集群不需要 sentine 哨兵也能完成节点移除和故障转移的功能。需要将每个节点设置成集群模式,这种集群模式没有中心节点,可水平扩展,据官方文档称可以线性扩展到上万个节点(官方推荐不超过 1000 个节点)。redis 集群的性能和高可用性均优于之前版本的哨兵模式,且集群配置非常简单。
10、 redis 集群数据 hash 分片算法是怎么回事?
redis cluster 将所有数据划分为 16384 个slots (槽位),每个节点负责其中一部分槽位。槽位的信息存储于每个节点中。当 redis cluster 的客户端来连接集群时,它也会得到一份集群的槽位配置信息并将其缓存在客户端本地。这样当客户端要查找某个 key 时,可以根据槽位定位算法定位到目标节点。
槽位定位算法:
cluster 默认会对 key 值使用 crc 16 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体槽位。HASH_SLOT = CRC 16 (key) mod 16384再根据槽位值和 redis 节点的对应关系就可以定位到 key 具体是落在哪个 redis 节点上的。
11、redis 执行命令竟然有死循环 bug ?
如果你想随机查看 redis 中的一个 key,redis 里有一个 RANDOMKEY 命令可以从 redis 中随机取出一个 key,这个命令可能导致 redis 死循环阻塞。前面的面试题讲过 redis 对于过期 key 的清理策略是定时删除与惰性删除两种方式结合来做的,而 RANDOMKEY 在随机拿出一个 key 后,首先会先检查这个 key 是否已过期,如果该 key 已经过期,那么 redis 会删除它,这个过程就是惰性删除。但清理完了还不能结束,redis 还要找出一个没过期的 key,返回给客户端。此时,redis则会继续随机拿出一个key,然后再判断它是否过期,直到找出一个没过期的 key 返回给客户端,这里就有一个问题了,如果此时 redis 中,有大量 key 已经过期,但还未来得及被清理掉,那这个循环就会持续很久才能结束,而且,这个耗时都花费在了清理过期 key 以及寻找不过期 key 上,导致的结果就是,RANDOMKEY 执行耗时变长,影响 redis 性能。以上流程,其实是在 master 上执行的。
如果在 slave 上执行 RANDOMEKY,那么问题会更严重。slave 自己是不会清理过期 key,当一个 key 要过期时,master 会先清理删除它,之后 master 向slave发送一个 DEL 命令,告知 slave 也删除这个key,以此达到主从库的数据一致性。假设 redis 中存在大量已过期还未被清理的 key,那在 slave 上执行 RANDOMKEY 时,就会发生以下问题:
slave 随机取出一个key,判断是否已过期。
key 已过期,但 slave 不会删除它,而是继续随机寻找不过期的 key。
由于大量 key 都已过期,那 slave 就会寻找不到符合条件的 key,此时就会陷入死循环。也就是说,在 slave 上执行 RANDOMKEY,有可能会造成整个 redis 实例卡死,这其实是 redis 的一个 bug,这个 bug 一直持续到 5.0 才被修复,修复的解决方案就是在 slave 中最多找一定的次数,无论是否能找到,都会退出循环。
12、redis 主从切换导致了缓存雪崩?
我们假设,slave 的机器时钟比 master 走得快很多,此时,redis master 里设置了过期时间的 key,从 slave 角度来看,可能会有很多在 master 里没过期的数据其实已经过期了。如果此时操作主从切换,把 slave 提升为新的 master。它成为 master 后,就会开始大量清理过期 key,此时就会导致以下结果:
master 大量清理过期 key,主线程可能会发生阻塞,无法及时处理客户端请求。
redis 中数据大量过期,引发缓存雪崩。当 master 与 slave 机器时钟严重不一致时,对业务的影响非常大。所以,我们一定要保证主从库的机器时钟一致性,避免发生这些问题。
13、 redis 持久化 rdb,aof 和混合持久化?
rdb 快照 (snapshot) 在默认情况下,redis 将内存数据库快照保存在名字为 dump.rdb 的二进制文件中。你可以对 redis 进行设置,让它在 N 秒内数据集至少有 M 个改动这一条件被满足时,自动保存一次数据集。比如说,以下设置会让 redis 在满足 60 秒内有至少有 1000 个键被改动这一条件时,自动保存一次数据集:save 60 1000;关闭 rdb 只需要将所有的 save 保存策略注释掉即可;还可以手动执行命令生成 rdb 快照,进入 redis 客户端执行命令 save 或 bgsave 可以生成 dump.rdb 文件,每次命令执行都会将所有 redis 内存快照到一个新的 rdb 文件里,并覆盖原有 rdb 快照文件。bgsave 的写时复制 (cow 机制)redis 借助操作系统提供的写时复制技术 (Copy-On-Write,COW),在生成快照的同时,依然可以正常处理写命令。简单来说 bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 rdb 文件,此时如果主线程对这些数据也都是读操作,那么主线程和 bgsave 子进程相互不影响。但是如果主线程要修改一块数据,那么这块数据就会被复制一份,生成该数据的副本,然后 bgsave 子进程会把这个副本数据写入 rdb 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。
saveredis save 命令执行一个同步保存操作,将当前 redis 实例的所有数据快照 (snapshort) 以 rdb 文件的方式保存到磁盘;
bgsavebgsave 执行后,会立刻返回 ok,redis 会 fork 一个子进程,原来的 redis 主进程继续执行后续操作,新 fork 的子进程负责将数据保存到磁盘,然后退出
区别:save 同步阻塞主进程,只有等 save 完后成,才能进行新操作bgsave 是 fork 的子进程,非阻塞,等执行完后会通知主进程,然后关闭子进程;
配置自动生成 rdb 文件后台使用的是 bgsave 方式;
快照功能并不是非常耐久:如果 redis 因为某些原因而造成故障停机,那么服务器将丢失最近写入、且仍未保存到快照中的那些数据,从 1.1 版本开始,redis 增加了一种完全耐久的持久化方式:aof 持久化,将修改的每一条指令记录写进文件 appendonly.aof 中(先写入操作系统缓存,每隔一段时间 fsync 到磁盘)你可以通过修改配置文件来打开 aof 功能:appendonly yes从现在开始,每当 redis 执行一个改变数据集的命令时(比如 SET),这个命令就会被追加到 aof 文件的末尾。这样的话,当 redis 重新启动时,程序就可以通过重新执行 aof 文件中的命令来达到重建数据集的目的,你可以配置 redis 多久才将数据 fsync 到磁盘一次。有三个选项:
appendfsync always:每次有新命令追加到 aof 文件时就执行一次fsync,非常慢,也非常安全;
appendfsync everysec:每秒 fsync 一次,足够快,并且在故障时只会丢失 1 秒钟的数据;
appendfsync no:从不 fsync,将数据交给操作系统来处理。更快,也更不安全的选择;推荐(并且也是默认)的措施为每秒 fsync 一次,这种 fsyc 策略可以兼顾速度和安全性;
aof 重写aof 文件里可能有太多没用指令,所以 aof 会定期根据内存的最新数据生成 aof 文件;例如执行下面这条命令 6 次:incr readCount;其实可以将其重写为:set readCount 6;redis 实际上是做了优化的,如下两个配置可以控制 aof 自动重写频率:
auto-aof-rewrite-min-size 64mb aof 文件至少要达到 64M 才会自动重写,文件太小恢复速度本来就很快,重写的意义不大;
auto-aof-rewrite-percentage 100 aof 文件自上一次重写后文件大小增长了100%,则再次触发重写;当然 aof 还可以手动重写,进入 redis 客户端执行命令 bgrewriteaof 重写 aof ;注意,aof 重写 redis 会 fork 出一个子进程去做 (与 bgsave 命令类似),不会对 redis 正常命令处理有太多影响;
生产环境可以都启动,redis启动时如果既有rdb又有aof文件则优先选择aof文件恢复数据,因为aof一般来说数据更全一点。
redis 4.0 混合持久化方式
redis 4.0 混合持久化重启 redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常使用 aof 日志重放,但是重放 aof 日志性能相对 rdb 来说要慢很多,这样在redis 实例很大的情况下,启动需要花费很长的时间。redis 4.0 为了解决这个问题,带来了一个新的持久化选项--混合持久化。通过如下配置可以开启混合持久化(必须先开启 aof):aof-use-rdb-preamble yes如果开启了混合持久化,aof 在重写时,不再是单纯将内存数据转换为 resp 命令写入 aof 文件,而是将重写这一刻之前的内存做 rdb 快照处理,并且将 rdb 快照内容和增量的 aof 修改内存数据的命令存在一起,都写入新的 aof 文件,新的文件一开始不叫 appendonly.aof,等到重写完新的 aof 文件才会进行改名,覆盖原有的 aof 文件,完成新旧两个 aof 文件的替换,于是在 redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 aof 日志就可以完全替代之前的 aof 全量文件重放,因此重启效率大幅得到提升,混合持久化 aof 文件结构如下:
14、redis 持久化策略一般如何设置?
如果对性能要求较高,在 master 最好不要做持久化,可以在某个slave 开启 aof 备份数据,策略设置为每秒同步一次即可。
15、主节点宕机导致数据全部丢失?
如果你的 redis 采用如下模式部署,就会发生数据丢失的问题:
master-slave + 哨兵部署实例;
master 没有开启数据持久化功能;
redis 进程使用 supervisor 管理,并配置为进程宕机,自动重启,如果此时 master 宕机,就会导致下面的问题:master 宕机,哨兵还未发起切换,此时 master 进程立即被 supervisor 自动拉起,但 master 没有开启任何数据持久化,启动后是一个空实例,此时 slave 为了与 master 保持一致,它会自动清空实例中的所有数据,slave也变成了一个空实例,在这个场景下,master/slave 的数据就全部丢失了。这时,业务应用在访问 redis 时,发现缓存中没有任何数据,就会把请求全部打到后端数据库上,这还会进一步引发缓存雪崩,对业务影响非常大,这种情况下我们一般不应该给 redis 主节点配置进程宕机马上自动重启策略,而应该等哨兵把某个 redis 从节点切换为主节点后再重启之前宕机的 redis 主节点让其变为 slave 节点。
16、redis 线上数据如何备份?
写 crontab 定时调度脚本,每小时都 copy 一份 rdb 或 aof 文件到另外一台机器中去,保留最近 48 小时的备份;
每天都保留一份当日的数据备份到一个目录中去,可以保留最近1个月的备份;
每次 copy 备份的时候,都把太旧的备份给删了;
17、redis 主从复制风暴?
如果 redis 主节点有很多从节点,在某一时刻如果所有从节点都同时连接主节点,那么主节点会同时把内存快照 rdb 发给多个从节点,这样会导致 redis 主节点压力非常大,这就是所谓的 redis 主从复制风暴问题,这种问题我们对 redis 主从架构做一些优化得以避免,比如可以做下面这种树形复制架构:
18、集群网络抖动导致频繁主从切换怎么处理?
真实世界的机房网络往往并不是风平浪静的,它们经常会发生各种各样的小问题。比如网络抖动就是非常常见的一种现象,突然之间部分连接变得不可访问,然后很快又恢复正常。为解决这种问题,redis cluter 提供了一种选项 cluster-node-timeout,表示当某个节点持续 timeout 的时间失联时,才可以认定该节点出现故障,需要进行主从切换,如果没有这个选项,网络抖动会导致主从频繁切换(数据的重新复制)。
19、redis 集群为什么至少需要三个 master 节点?
因为新 master 的选举需要大于半数的集群 master 节点同意才能选举成功,如果只有两个 master 节点,当其中一个挂了达不到选举新 master 的条件。
20、redis 集群为什么推荐奇数个节点?
因为新 master 的选举需要大于半数的集群 master 节点同意才能选举成功,奇数个 master 节点可以在满足选该条件的基础上节省一个节点,比如三个 master 节点和四个 master 节点的集群相比,大家如果都挂了一个 master 节点都能选举新 master 节点,如果都挂了两个master 节点都没法选举新naster节点了,所以奇数的 master 节点更多的是从节省机器资源角度出发说的。
21、redis 集群支持批量操作命令吗?
对于类似 mset,mget 这样的多个 key 的原生批量操作命令,redis 集群只支持所有 key 落在同一槽中的情况,如果有多个 key 一定要用 mset 命令在 redis 集群上操作,则可以在 key 的前面加上{XXX},这样参数数据分片 hash 计算的只会是大括号里的值,这样能确保不同的 key 能落到同一槽里去,示例如下:mset {user1}:1:name zhangsan {user1}:1:age 18假设 name 和 age 计算的 hash slot 值不一样,但是这条命令在集群下执行,redis 只会用大括号里的 user1 做 hash slot 计算,所以算出来的 slot 值肯定相同,最后都能落在同一 slot 。
22、lua 脚本能够在 redis 集群中执行吗?
redis 官方规定 lua 脚本如果想在 redis 集群里执行,需要 lua 脚本里操作的所有 redis key 落在集群的同一个节点上,这种的话我们可以给 lua 脚本的 key 前面加一个相同的 hash tag,就是{xxx},这样就能保证 lua 脚本里所有 key 落在相同的节点上了。
23、redis 主从切换导致分布式锁丢失的问题?
在 redis 的 master 节点上拿到了锁,但是这个加锁的 key 还没有同步到 slave 节点,master 故障,发生故障转移,slave 节点升级为 master 节点导致锁丢失,对当前的刚升级为 master 的节点可以加锁成功从而造成了分布式锁丢失的问题:
24、redlock 如何解决 redis 主从切换分布式锁丢失的问题?
Redlock 实现原理:
但是使用 redlock 会存在一定的问题,不推荐大家使用;
25、缓存击穿 (失效)?
正常的缓存流程:用户访问某个 web 应用,首先是访问 redis,如果 redis 存在数据那么直接返回缓存中的数据,如果 redis 没有数据那么会查询数据库,如果数据库中查询到值会将查询结果存储到 redis 中,并且将结果返回;
当缓存的 key 没有失效的时候可以将缓存中查到的 key 返回回去,当缓存的 key 失效了秒杀的请求就会直接打到数据库上,注意这里是某个热点的 key, 大量的用户请求去访问这个热点的 key, 它是击穿某一个热点的 key,缓存击穿的解决方案:集群:分布式锁,单体:互斥锁;
26、缓存穿透?
是指缓存和数据库中都没有数据,假如有用户请求这个热点商品,缓存和数据库中的数据都查询不到,把整个后端所有层全部穿透了,而且存在很多种情况导致缓存穿透问题,比如某个页面对应着某个商品的 id,此时如果有某个黑客利用工具在短时间内发送大量商品 id 不存在的请求,此时缓存中没有数据,数据库中也没有数据,每秒可能几十万的请求,把整个后端层面的击垮了,如何来解决这个问题呢?1. 数据库中查询不到缓存一个空值,例如存储一个空字符串,缓存可以扛住高并发,但是数据库不可以,当查询到数据库中的值不存在的时候此时设置到缓存中就会将不存在的商品设置到缓存中,此时缓存中就会存在大量不存在的商品;缓存穿透的解决方案:将数据库中查询不到的商品存储为空值将其存储到 redis 中;第二种是对参数的合法性进行校验,在判断参数不合法的时候直接 return 掉;第三种使用布隆过滤器;
27、缓存雪崩?
缓存雪崩指的是缓存的键值对同一时间大量失效,导致大量的请求打到数据库上,造成数据库响应不及时直接挂掉,这个时候访问的 web 应用没办法及时对外提供服务;缓存雪崩的解决方案:设置缓存的失效时间不要在同一个时间失效,在设置失效时间的时候随机初始化失效时间,这样就不会导致所有的缓存在同一时间内失效把所有的请求都打到数据库上,并且如果是集群部署我们可以将热点的 key 部署到不同的节点上,让热点缓存平均地分布在不同的 redis 节点上;第二个是设置定时任务来刷新缓存,在失效之前将缓存重新跑进去;
28、缓存穿透解决方案之布隆过滤器?
布隆过滤器:一种数据结构,是由一串很长的二进制向量组成,可以将其看成一个二进制数组。既然是二进制,那么里面存放的不是 0,就是1,但是初始默认值都是 0。布隆过滤器插入插入和更新的速度比较快,当要向布隆过滤器中添加一个元素 key 时,我们通过多个哈希函数,算出一个值,然后将这个值所在的方格置为1;
优点:因为使用一个数组来存储二进制串,所以占用的空间非常小,第二个是插入和查询的速度是非常快的(因为使用哈希函数计算哈希值然后映射到对应的下标);第三个点是安全性非常好,存储的二进制串不知道具体的意思是什么;
缺点:布隆过滤器很难做删除操作;第二个存在误判:本身不存在这个集合中,但是经过一系列的运算之后判断出这个数据存在于这个集合中,不同的数据计算的哈希值可能是相同的,这个是布隆过滤器很大的一个缺点(当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那么一定不存在);
布隆过滤器解决缓存穿透问题:布隆过滤器主要是判断这个数据是否存在 redis 中,正好与 redis 缓存的场景是一样的,redis 有一个客户端的工具类帮助我们实现了这个布隆过滤器;
29、基于 DCL 机制解决并发性热点缓存并发重建问题?
之前是冷门数据一下子变得热门了,缓存中不存在当前这些数据,如何解决呢?在代码层面第一个方法是可以加上 synchronized 锁,如果几万的请求同时访问后台数据库,因为是 synchronized 锁所以会排队执行,同一时刻只能够允许一个请求访问数据库,当第一个请求处理完成之后缓存中已经有当前这个商品的数据了,所以后面在队列中等待的几万请求就可以让它们查询缓存,所以实际上是先放一个请求过来,等到缓存重建完毕,在缓存这一层就可以查询到数据,直接在缓存中查询到数据就可以直接返回,就可以大大降低数据库的压力,这个锁其实叫做双重检测锁(DCL),查询是一次检测,中间是 synchronized 锁,这样就可以解决并发性热点缓存并发重建的问题,但是这样做是存在问题的,因为 synchronized 锁生效的点是在单节点上面,也即在 JVM 内部生效,而我们线上的系统都是以集群,分布式的方式部署,使用上面的方式重建的话只能够控制在当前这台机器上面,在所有的机器上都至少需要重建一次,重建还是较小的问题,但是这样做还存在一个小问题,假设当前的商品编号为 101,但是另外一个直播间推荐编号为 202 的商品,所以编号为 202 的商品也会并发重建,只要是编号为 101 的商品正在并发重建,所以编号为 202 的商品就会一直等待, 但是这样是完全没有必要的,因为编号为 101 的商品与编号为 202 的商品没有任何的关系,各自并发重建就可以了,这里其实是要针对于不同的商品 id 创建一个全局的对象,也即不同的商品 id 锁的对象是不一样的,所以需要根据不同的商品 id 维护一个全局的对象值,同样的商品锁相同的对象,不同的商品锁不同的对象,这里其实还可以使用另外一种方式来解决:分布式锁,这里使用分布式锁可以将上面遇到的所有问题全部解决,而使用 redis 就可以实现分布式锁,SETNX(SET if Not eXists)格式:setnx key value将 key 的值设为 value,当且仅当 key 不存在,若给定的 key 己经存在,则 SETNX 不做任何动作,可用版本:>= 1.0.0;
例如当前的部署的系统为:
使用分布式锁一般会使用到 redisson,很多公司实现分布式锁都会基于 redis 的一个客户端 redisson,也是类似于 Jredis 的一个 java 开发中操作 redis 的客户端程序,针对于上面的问题我们可以使用下面的分布式锁创建,可以理解为执行 redis 中的 setnx 命令:
这样当我们使用分布式锁之后,上面的问题就解决掉了,即使是垮了一台应用的多台机器,所有的请求被 nginx 分发到不同的 web 应用,最终这些请求都会到达 redis,redis 都会执行同一条命令,当设置的是同一个商品 id 那么就需要排队,如果设置的是不同的商品 id 那么就不需要排队,所以不会相互阻塞;
30、redis 分布式锁解决缓存与数据库双写不一致问题?
例如存在下面的系统,如果按照时间轴从上到下执行是没有问题的,但是线程 3 在查询完数据库与更新缓存之间的过程卡顿了一下,此时线程 2 将写数据库和更新缓存这两步操作完成了,此时缓存中的值为 10 而数据库中的值为 6 导致数据库与缓存的数据不一致,属于典型的缓存与数据库双写不一致的问题;如何解决这个问题呢?实际上有很多种解决方法,这里提供一种解决方案:分布式锁,可以发现实际上是并发读写相同的数据导致的,对其加一把锁就可以了,对于不同的线程,需要拿到分布式锁,谁先拿到锁谁就可以执行对应的业务逻辑,对于相同的数据同一把锁是不可以操作的,只有当我把锁释放掉了才可以尝试加锁,这样就可以解决并发安全的问题;
31、大促压力暴增导致分布式锁串行争用问题优化?
在二十八的例子中加上分布式锁存在性能问题,任何的分布式锁在开发完成之后是需要做优化的,而且分布式锁一般是可以做优化的,优化的手段一般有很多种,一般需要结合业务逻辑,对于比较大的公司的电商网站,大部分是读多写少的场景,分布式锁可以基于读写锁进行优化,redis 帮助我们提供了读写锁的实现,将读写锁分为读锁和写锁,在 redis 中操作 lockkey 的时候是同一把锁,如何使用这两把锁呢?例如对于两个线程如果他们都是读的操作,给他们加一把读锁就可以了,如果加上的读锁那么可以并行执行业务逻辑,可以发现如果两个线程执行的都是读的操作并发执行是没有任何安全问题的,在读数据的时候可以加上读锁,在写数据的时候加上写锁;所以如果加上的读锁那么可以让大家并发读取数据,如果加上的是写锁其他线程需要读那么就需要当前这个线程释放掉写锁再执行读操作那么就没有并发安全问题,这样就性能就比较高了,
下面的 redisson 加锁的逻辑:假设有多个线程针对于同一把锁执行 lock 方法,第一个线程会加锁成功,在它没有释放锁之前其他的线程都会等着这把锁释放再去加锁,第一个线程放进来执行业务逻辑,其余线程会等着第一个线程调用 unlock 方法释放锁,其他线程会尝试着抢锁,默认是非公平地抢锁,其他没有抢到锁的线程会等着,几万个请求过来并发重建缓存,第一个请求给它加到锁调用数据库重建缓存,然后调用 unlock 方法,其他的线程就可以抢锁,但是这几万个线程每次都这么来做嘛,等待着加锁重建缓存再解锁这是没有必要的,这里如何进行优化呢?可以调用 tryLock 方法,多个请求调用这个方法也是加锁,第一个线程放进去其他线程跟着等着,可以设置等待的秒数,等待的时间还没有执行完其他线程还没有拿到这把锁,这个方法的返回值我 false 这样不用让它去加锁解锁直接返回即可(分布式锁实际上就是让这些请求串行执行解决并发安全问题等待的请求不再等待了,直接查询缓存这样串行就变成并行执行了效率就变高了,这种方案需要确保第一个请求的重建缓存的时间小于这个时间,如果评估不好那么第一个线程的业务逻辑还没有执行完但是后面的线程直接不等了直接查询缓存,查询不到又会并发重建缓存,所以这种方案也不是绝对完美的,只要能够接受偶尔大量的并发重建缓存的问题那么就可以使用这种方案):
32、利用多级缓存架构解决 redis 线上缓存雪崩的问题?
如果请求非常多, 达到了 redis 都扛不住的情况,比如黑客攻击针对于同一个商品每秒发送几十万的请求,redis 都扛不住,即使是 redis 做集群,数据分片还是到达 redis 单个节点上面来,一个单节点 redis 最多扛几万的并发,每秒中几十万的请求可能将 redis 都会打垮掉,比如微博的新闻一旦发出来之后,微博有的时候就扛不住了宕机了,热点新闻导致微博瘫痪的事情,几十万的请求来查询一条消息,首先需要从缓存中查找,哪怕是从 redis 中查找也是扛不住的,因为这条消息是落在 redis 的某个节点上面的,扛不住请求就会宕机,redis 扛不住了前端应用就会一直等着或者时报错,缓存雪崩说的就是这个问题,系统大规模依赖缓存,如果没有做缓存挂了的 紧急措施 web 应用系统也会受到影响,导致用户端的大量报错,类似于这种缓存雪崩,缓存挂了的问题,有什么办法可以解决吗?可以使用多级缓存 ,在缓存的时候同步给我们的 jvm 进程缓存同样放一份缓存,这样当我们在查询数据的时候,redis 中有缓存,jvm 进程级别也会有缓存,可以先从缓存中查询。
33、在 centos 7 上 安装 redis?
因为在 centos 7 上使用压缩包的方式进行安装所以首先需要在官网下载 redis 对应的 linux 的相应版本的安装包,我下载的是 6.2.7 版本的 redis,下载完成之后可以参照官网 Install Redis from Source 的提示进行安装(根据下载的压缩包安装 redis),下载好 redis 压缩包之后需要使用远程文件传输工具(我使用的是 xftp 7 文件传输工具) 将压缩包上传到 centos 7 目录中,我是上传到/root/zhangyu/redis 目录下,使用 cd 命令进入到这个目录,按照官网的安装提示使用下面的命令进行安装:
为了方面后面修改 redis.conf 文件,我们可以将 redis.conf 文件复制一份到 /usr/local/bin/config 目录下(config 是后面使用 mkdir 创建的一个目录),我们通过 /usr/local/bin/ 目录下的 redis-server 文件通过拷贝的 redis.conf 文件启动 redis 服务端,由于 redis 默认是前台启动,所以我们需要修改 redis.conf 配置文件,使用 vim 编辑器编辑 redis.conf 文件,使用/daemonize 找到对应的行,将 daemonize 的 no 修改为 yes,这样在启动 redis 服务端的时候就不会占用页面以后台方式启动,使用 redis-server config/redis.conf 命令表示以 redis.conf 配置文件的方式启动 redis 服务端,启动之后我们使用命令:redis-cli -p 6379 进入客户端页面与服务端进行交互,使用命令:ps -ef | grep redis 查看启动的进程,如果成功启动服务端与客户端,使用上面的 ps 命令可以查看到 redis 服务端与客户端的进程信息,确保成功启动 redis 服务端与客户端:
34、redis 主从复制?
主从复制是指将一台 redis 服务器的数据,复制到其他的 redis 服务器。前者称为主节点(master/leader),后者称为从节点 (slave/follower);数据的复制是单向的,只能由主节点到从节点。Master 以写为主,Slave 以读为主。默认情况下,每台 redis 服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。主从复制的作用主要包括:
数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复,实际上是一种服务的冗余。
负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写 redis 数据时应用连接主节点,读 redis 数据时应用连接从节点),分担服务器负载;尤其是在读多写少的场景下,通过多个从节点分担读负载,可以大大提高 redis 服务器的并发量)。
高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是 redis 高可用的基础。一般来说,要将 redis 运用于工程项目中,只使用一台 redis 是万万不能的,原因如下:
从结构上,单个 redis 服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大;2.从容量上,单个 redis 服务器内存容量有限,就算一台 redis 服务器内存容量为 256 G,也不能将所有内存用作 redis 存储内存,一般来说,单台 redis 最大使用内存不应该超过 20 G;
环境配置:只配置从库,不用配置主库,redis 默认自己就是一个主库,打开四个命令行窗口,根据 redis.conf 复制三个配置文件:redis6380.conf,redis6381.conf,redis6382.conf:
使用 vim 编辑器编辑上面复制的三个配置文件,需要修改的信息有四个分别为:1. 端口;2. pid 名字;3. log 文件名字;4. dump.rdb 名字;
默认情况下 redis 的每一台都是主节点,一般情况下只配置从机即可,这里将 6379 作为主机,6380,6381,6382 作为从机,使用 redis-cli 进入到客户端,使用 SLAVEOF 命令配置当前从机的主机ip 地址和端口号:
真实的配置使用的是配置文件的方式进行配置,将下面replicaof 配置主机的ip 地址和端口号即可:
从机只能够读,不可以写,主机中的所有数据都会自动被从机保存,
测试:主机断开连接,从机依旧连接到主机,但是没有写操作,这个时候主机如果回来了,从机依旧可以直接获取到主机写的信息!如果使用命令行来配置的主从,这个时候如果重启了,就会变回主机!只要变为从机,立马就会从主机中获取值!复制原理slave 启动成功连接到 master 后会发送一个 sync 同步命令,master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master 将传送整个数据文件到 slave,并完成一次完全同步。全量复制:而 slave 服务在接收到数据库文件数据后,将其存盘并加载到内存中。增量复制:master 继续将新的所有收集到的修改命令依次传给 slave,完成同步但是只要是重新连接 master,一次完全同步(全量复制)将被自动执行。