redis详述

本文围绕以下几点进行阐述:

· 为什么使用 Redis

· 使用 Redis 有什么缺点

· 单线程的 Redis 为什么这么快

· Redis 的数据类型,以及每种数据类型的使用场景

· Redis 的过期策略以及内存淘汰机制

· Redis 和数据库双写一致性问题

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

· 如何解决 Redis 的并发竞争 Key 问题

为什么使用Redis

我觉得在项目中使用 Redis,主要是从两个角度去考虑:性能和并发

扫描二维码关注公众号,回复: 1812545 查看本文章

当然,Redis 还具备可以做分布式锁等其他功能,但是如果只是为了分布式锁这些其他功能,完全还有其他中间件,如 Zookeeper等代替,并不是非要使用 Redis。因此,这个问题主要从性能和并发两个角度去答。

性能

如下图所示,我们在碰到需要执行耗时特别久,且结果不频繁变动的 SQL,就特别适合将运行结果放入缓存。这样,后面的请求就去缓存中读取,使得请求能够迅速响应。

 

并发

如下图所示,在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。

这个时候,就需要使用 Redis 做一个缓冲操作,让请求先访问到 Redis,而不是直接访问数据库。

 

使用 Redis 有什么缺点

大家用 Redis 这么久,这个问题是必须要了解的,基本上使用 Redis 都会碰到一些问题,常见的也就几个。

回答主要是四个问题:

1. 缓存和数据库双写一致性问题

2. 缓存雪崩问题

3. 缓存击穿问题

4. 缓存的并发竞争问题

这四个问题,我个人觉得在项目中是常遇见的,具体解决方案,后文给出。

单线程的 Redis 为什么这么快

这个问题是对 Redis 内部机制的一个考察。根据我的面试经验,很多人都不知道 Redis 是单线程工作模型。所以,这个问题还是应该要复习一下的。

回答主要是以下三点:

1. 纯内存操作

2. 单线程操作,避免了频繁的上下文切换

3. 采用了非阻塞 I/O 多路复用机制

题外话:我们现在要仔细的说一说 I/O 多路复用机制,因为这个说法实在是太通俗了,通俗到一般人都不懂是什么意思。

打一个比方:小曲在 S 城开了一家快递店,负责同城快送服务。小曲因为资金限制,雇佣了一批快递员,然后小曲发现资金不够了,只够买一辆车送快递。

1. 经营方式一

客户每送来一份快递,小曲就让一个快递员盯着,然后快递员开车去送快递。

慢慢的小曲就发现了这种经营方式存在下述问题:

几十个快递员基本上时间都花在了抢车上了,大部分快递员都处在闲置状态,谁抢到了车,谁就能去送快递。随着快递的增多,快递员也越来越多,小曲发现快递店里越来越挤,没办法雇佣新的快递员了。快递员之间的协调很花时间。

综合上述缺点,小曲痛定思痛,提出了下面的经营方式。

2. 经营方式二

小曲只雇佣一个快递员。然后呢,客户送来的快递,小曲按送达地点标注好,然后依次放在一个地方。

最后,那个快递员依次的去取快递,一次拿一个,然后开着车去送快递,送好了就回来拿下一个快递。

上述两种经营方式对比,是不是明显觉得第二种,效率更高,更好呢?

在上述比喻中:

每个快递员→每个线程

每个快递→每个 Socket(I/O 流)

快递的送达地点→Socket 的不同状态

客户送快递请求→来自客户端的请求

小曲的经营方式→服务端运行的代码

一辆车→CPU 的核数

于是我们有如下结论:

(1) 经营方式一就是传统的并发模型,每个 I/O 流(快递)都有一个新的线程(快递员)管理。

(2) 经营方式二就是 I/O 多路复用。只有单个线程(一个快递员),通过跟踪每个 I/O 流的状态(每个快递的送达地点),来管理多个 I/O 流。

下面类比到真实的 Redis 线程模型,如图所示:

 

简单来说,就是我们的 redis-client 在操作的时候,会产生具有不同事件类型的 Socket。

在服务端,有一段 I/O 多路复用程序,将其置入队列之中。然后,文件事件分派器,依次去队列中取,转发到不同的事件处理器中。

需要说明的是,这个 I/O 多路复用机制,Redis 还提供了 select、epoll、evport、kqueue 等多路复用函数库,大家可以自行去了解。

Redis 的数据类型,以及每种数据类型的使用场景

String

1. 最常规的 set/get 操作,Value 可以是 String 也可以是数字。

2. 一些复杂的计数功能的缓存。

3. 字符串和二进制的操作

Hash

1. Value 存放的是结构化的对象,比较方便的就是操作其中的某个字段。

List

1. 可以做简单的消息队列的功能。

2. 可以利用 lrange 命令,做基于 Redis 的分页功能,性能极佳。

Set

1. Set 堆放的是一堆不重复值的集合。所以可以做全局去重的功能。

2. 利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。

Sorted Set

1. Sorted Set多了一个权重参数 Score,集合中的元素能够按 Score 进行排列。可以做排行榜应用,取 TOP N 操作。

2. Sorted Set 可以用来做延时任务。

3. 最后一个应用就是可以做范围查找。

Redis 的过期策略以及内存淘汰机制

这个问题相当重要,到底 Redis 有没用到家,这个问题就可以看出来。

比如你 Redis 只能存 5G 数据,可是你写了 10G,那会删 5G 的数据。怎么删的,这个问题思考过么?

还有,你的数据已经设置了过期时间,但是时间到了,内存占用率还是比较高,有思考过原因么?

回答:Redis 采用的是定期删除+惰性删除策略。

为什么不用定时删除策略

定时删除,用一个定时器来负责监视 Key,过期则自动删除。虽然内存及时释放,但是十分消耗 CPU 资源。

在大并发请求下,CPU 要将时间应用在处理请求,而不是删除 Key,因此没有采用这一策略。

定期删除+惰性删除是如何工作

定期删除,Redis 默认每个 100ms 检查,是否有过期的 Key,有过期 Key 则删除。

需要说明的是,Redis 不是每个 100ms 将所有的 Key 检查一次,而是随机抽取进行检查(如果每隔 100ms,全部 Key 进行检查,Redis 岂不是卡死)。

因此,如果只采用定期删除策略,会导致很多 Key 到时间没有删除。于是,惰性删除派上用场。

也就是说在你获取某个 Key 的时候,Redis 会检查一下,这个 Key 如果设置了过期时间,那么是否过期了?如果过期了此时就会删除。

采用定期删除+惰性删除就没其他问题了么?

不是的,如果定期删除没删除 Key。然后你也没即时去请求 Key,也就是说惰性删除也没生效。这样,Redis的内存会越来越高。那么就应该采用内存淘汰机制。

redis.conf 中有一行配置:

# maxmemory-policy volatile-lru 

该配置就是配内存淘汰策略的:

1. noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。应该没人用吧。

2. allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。推荐使用,目前项目在用这种。

3. allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 Key。应该也没人用吧,你不删最少使用 Key,去随机删。

4. volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用。不推荐。

5. volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 Key。依然不推荐。

6. volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。不推荐。

PS:如果没有设置 expire 的 Key,不满足先决条件(prerequisites);那么 volatile-lru,volatile-random 和 volatile-ttl 策略的行为,和 noeviction(不删除) 基本上一致。

Redis 和数据库双写一致性问题

一致性问题是分布式常见问题,还可以再分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的问题。

答这个问题,先明白一个前提。就是如果对数据有强一致性要求,不能放缓存。我们所做的一切,只能保证最终一致性。

另外,我们所做的方案从根本上来说,只能说降低不一致发生的概率,无法完全避免。因此,有强一致性要求的数据,不能放缓存。

回答:首先,采取正确更新策略,先更新数据库,再删缓存。其次,因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。

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

这两个问题,说句实在话,一般中小型传统软件企业,很难碰到这个问题。如果有大并发的项目,流量有几百万左右。这两个问题一定要深刻考虑。

1. 缓存穿透,

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

2. 缓存穿透解决方案:

利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试。

采用异步更新策略,无论 Key 是否取到值,都直接返回。Value 值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。

提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的 Key。迅速判断出,请求所携带的 Key 是否合法有效。如果不合法,则直接返回。

3. 缓存雪崩,

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

4. 缓存雪崩解决方案:

给缓存的失效时间,加上一个随机值,避免集体失效。

使用互斥锁,但是该方案吞吐量明显下降了。

双缓存。我们有两个缓存,缓存 A 和缓存 B。缓存 A 的失效时间为 20 分钟,缓存 B 不设失效时间。自己做缓存预热操作。

然后细分以下几个小点:从缓存 A 读数据库,有则直接返回;A 没有数据,直接从 B 读数据,直接返回,并且异步启动一个更新线程,更新线程同时更新缓存 A 和缓存 B。

如何解决Redis的并发竞争Key问题

这个问题大致就是,同时有多个子系统去 Set 一个 Key。这个时候大家思考过要注意什么呢?

需要说明一下,我提前百度了一下,发现答案基本都是推荐用 Redis 事务机制。

我并不推荐使用 Redis 的事务机制。因为我们的生产环境,基本都是 Redis 集群环境,做了数据分片操作。

你一个事务中有涉及到多个 Key 操作的时候,这多个 Key 不一定都存储在同一个 redis-server 上。因此,Redis 的事务机制,十分鸡肋。

如果对这个 Key 操作,不要求顺序

这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做 set 操作即可,比较简单。

如果对这个 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 操作了,以此类推。

其他方法,比如利用队列,将 set 方法变成串行访问也可以。总之,灵活变通。

Redis的持久化

Redis提供了两种不同的持久化方法来将数据存储到硬盘里面。

一种方法叫快照,他可以将存在于某一时刻的所有数据都写入硬盘里面。

另一种方法叫只追加文件(AOF),他会在执行写命令时,将被执行的写命令复制到硬盘里面。

创建快照

1. 创建快照的方法

(1) 客户端bgsage命令。Redis会创建一个子进程负责将快照写入硬盘。父进程继续处理命令。

(2) 客户端save命令。Redis在快照创建完毕之前不再响应任何其他命令。

(3) 如果客户端设置了save配置选项,如save 60 1000 当条件满足时,redis会触发一次bgsave命令。

(4) Redis通过shutdown命令接收到关闭服务器的请求时,或者接受到term信号时,会执行save命令,阻塞所有客户端,save命令执行完后关闭服务器。

(5) 当一个redis服务器连接另一个redis服务器,并向对方发送sync命令来开始一次复制操作时候,如果主服务器目前没有在执行bgsave操作,或者主服务器并非刚刚执行完bgsave操作,那么主服务器就会执行bgsave命令。

2. 快照的特点

快照持久化只适用于那些即使丢失一部分数据,也不会造成问题的应用程序。

为了防止redis因为创建子进程而出现停顿,我们可以考虑关闭自动保存,转而通过手动发送bgsave或者save来进行持久化。手动发送bgsave一样会引起停顿,唯一不同的是,用户可以通过手动发送bgsave命令来控制停顿出现的时间。另一方面,虽然save会一直阻塞reids直到快照生产完毕,但是由于她不需要创建子进程,所以不会像bgsave一样因为创建子进程而导致reids停顿。并且因为没有子进程在争夺资源,所以save创建快照的速度会比bgsave创建快照的速度要来的更快一些。

AOF持久化

1. AOF的配置

AOF持久化可以通过设置appendonly yes配置选项来打开。appendfsync配置选项如下所示:

(1) always 每个reids写命令都要同步写入硬盘,这样会严重降低redis的速度,但是如果程序崩溃,损失最少。

(2) Everysec 每秒执行一次同步,显示地将多个命令同步到硬盘,这种模式下,redis的性能和不适用任何持久化特性时的性能相差无几。最多只会损失一秒内产生的数据。

(3) no 让操作系统来决定应该何时进行同步, 若系统崩溃,损失未知。 而且如果用户的硬盘处理写入操作的速度不够快的话,那么当缓冲区被等待写入硬盘的数据被填满,redis的写入操作将被阻塞,并导致redi处理命令请求的速度变慢。

2. 缺点

这种持久化,reids会不断的将被执行的写命令记录到AOF文件里面,所以随着reids不断运行,AOF文件的体积会不断增长,在极端情况下,体积不断增大的AOF文件甚至可能会用完硬盘的所有可用空间。

另外一个问题是,因为redis在重启之后需要通过重新执行AOF文件记录的所有写命令来还原数据,所以如果AOF体积非常大,那么还原操作执行的时间可能会特别长。

3. 解决

为了解决AOF文件体积不断增大的问题,用户可以向redis发送bgrewriteaof命令,这个命令会通过移除AOF文件中的冗余命令来重写AOF文件,使得其变得尽可能小。

Bgrewriteaof的工作原理和bgsave创建快照的工作原理非常相似,redis会创建一个子进程,然后子进程负责对AOF文件进行重写。因为AOF文件重写也需要用到子进程,所以快照持久化因为创建子进程导致的性能问题和内存占用问题,在AOF持久化中也同样存在。

更糟糕的是,如果不加以控制,AOF文件的体积可能会比快照文件的提交大好几倍,在进行AOF重写并删除旧AOF文件的时候,删除一个体积达到数十GB大的旧AOF文件会导致操作系统挂起数秒。

跟快照持久化可以通过设置save选项来自动执行bgsave一样,aof持久化也可以通过设置auto-aof-rewrite-percentage选项和auto-aof-rewrite-min-size选项来自动执行bgrewriteaof

4. 


猜你喜欢

转载自blog.csdn.net/wangzhanzheng/article/details/80847088
今日推荐