redis常见问题的总结

这篇文章希望对redis整体做一个总结。

一、使用redis的原因
1、redis 的响应速度快,将一些不频换变动的查询数据存入redis能够提供更快的响应速度。
2、关系型数据库的数据存储在硬盘,在高并发环境下I/O较高,并发能力弱,一般我们使用redis 做缓冲代替直接访问数据库,能够提高redis的并发量。
3、Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系,适合用于分布式锁。
4、redis的数据格式灵活,能够在设置值的时候添加有效期。

二、redis响应快的原因
1、纯内存访问,Redis 将所有数据放在内存中,非数据同步正常工作时,是不需要从磁盘读取数据的。(存储在内存也决定了数据相对于存储硬盘更易丢失 ,所以有了 RDB 持久化 和 AOF 持久化。内存相对硬盘造价更高,也决定了redis不能无限扩容,故有了过期策略和内存淘汰机制)

2、redis是单线程的,避免了频繁的上下文切换。(因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了)

3、采用了非阻塞 I/O 多路复用机制。(Redis-client 在操作的时候,会产生具有不同事件类型的 Socket。在服务端,有一段 I/O 多路复用程序,将其置入队列之中。然后,文件事件分派器,依次去队列中取,转发到不同的事件处理器中。 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态,来同时管理多个I/O流,提高了的提高服务器的吞吐能力。)

4、数据结构简单,操作节省时间。(redis支持这 String 整数,浮点数或者字符串、Hash 散列表、Set 集合、Zset 有序集合、List 列表 这五种数据类型,同时也决定了没办法想关系性数据库一样直接在数据库对数据进行复杂的操作)

三、redis五种常用场景介绍

String 整数,浮点数或者字符串:
最常规的 set/get 操作,Value 可以是 String 也可以是数字。一般做一些复杂的计数功能的缓存。

Hash 散列表
这里 Value 存放的是结构化的对象,比较方便的就是操作其中的某个字段。能够通过这种数据结构存储用户信息,以 CookieId 作为 Key,设置 30 分钟为缓存过期时间,能很好的模拟出类似 Session 的效果。

List 列表
使用 List 的数据结构,可以做简单的消息队列的功能。另外,可以利用 lrange 命令,做基于 Redis 的分页功能,性能极佳,用户体验好。

Set 集合
因为 Set 堆放的是一堆不重复值的集合。所以可以做全局去重的功能。我们的系统一般都是集群部署,使用 JVM 自带的 Set 比较麻烦。另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。

Zset 有序集合
Sorted Set 多了一个权重参数 Score,集合中的元素能够按 Score 进行排列。可以做排行榜应用,取 TOP N 操作。Sorted Set 可以用来做延时任务。

四、Redis 的过期删除策略

1、定时删除策略
定时删除,用一个定时器来负责监视 Key,过期则自动删除。虽然内存及时释放,但是十分消耗 CPU 资源。在大并发请求下,CPU 要将时间应用在处理请求,而不是删除 Key,因此没有采用这一策略。

2、惰性删除策略

放任键过期不管,但是每次从键空间中获取键是,都检查取得的键的过期时间,如果过期的话,删除即可;惰性操作对于CPU来说是友好的,过期键只有在程序读取时判断是否过期才删除掉,而且也只会删除这一个过期键,但是对于内存来说是不友好的,如果多个键都已经过期了,而这些键又恰好没有被访问,那么这部分的内存就都不会被释放出来;

3、定期删除策略:
每隔一段时间,程序就对数据库进行一次检查,删除掉过期键;定期删除是上面两种方案的折中方案,每隔一段时间来删除过期键,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响,除此之外,还有效的减少内存的浪费;但是该策略的难点在于间隔时长,这个需要根据自身业务情况来进行设置;

3.1 如何配置定期删除执行时间间隔

redis的定时任务默认是每秒执行10次,如果要修改这个值,可以在redis.conf中修改hz的值。
redis.conf中,hz默认设为10,提高它的值将会占用更多的cpu,当然相应的redis将会更快的处理同时到期的许多key,以及更精确的去处理超时。 hz的取值范围是1~500,通常不建议超过100,只有在请求延时非常低的情况下可以将值提升到100。

# The range is between 1 and 500, however a value over 100 is usually not
# a good idea. Most users should use the default of 10 and raise this up to
# 100 only in environments where very low latency is required.
hz 10

3.2 单线程的redis,如何知道要运行定时任务

redis是单线程的,线程不但要处理定时任务,还要处理客户端请求,线程不能阻塞在定时任务或处理客户端请求上,那么,redis是如何知道何时该运行定时任务的呢?

Redis 的定时任务会记录在一个称为最小堆的数据结构中。这个堆中,最快要执行的任务排在堆的最上方。在每个循环周期,Redis 都会将最小堆里面已经到点的任务立即进行处理。处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是接下来处理客户端请求的最大时长,若达到了该时长,则暂时不处理客户端请求而去运行定时任务

定期删除+惰性删除
定期删除,Redis 默认每个 100ms 检查,有过期 Key 则删除。需要说明的是,Redis 不是每个 100ms 将所有的 Key 检查一次,而是随机抽取进行检查。如果只采用定期删除策略,会导致很多 Key 到时间没有删除。于是,惰性删除派上用场。

五、内存淘汰机制

1、内存淘汰策略:

5.1.1、noeviction
当内存不足以容纳新写入数据时,新写入操作会报错。
5.1.2、allkeys-lru
当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。(redis 4.0 以前推荐使用)
5.1.3、allkeys-random
当内存不足以容纳新写入数据时,在键空间中,随机移除某个 Key。(不推荐)
5.1.4、volatile-lru
当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用。(不推荐)
5.1.5、volatile-random
当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 Key。(依然不推荐)
5.1.6、volatile-ttl
当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。(不推荐)
5.1.7、volatile-lfu
在设置了过期时间的key中使用LFU算法淘汰key。在设置了过期时间的键空间中,key的最近被访问的频率进行淘汰,很少被访问的优先被淘汰,被访问的多的则被留下来。(不推荐)

5.1.8、allkeys-lfu
在所有的key中使用LFU算法淘汰数据,在键空间中,ey的最近被访问的频率进行淘汰,很少被访问的优先被淘汰,被访问的多的则被留下来。(Redis4.0 出现, 推荐)

2、更改内存淘汰策略
5.2.1、通过更改redis.conf更改内存淘汰策略

# maxmemory-policy noeviction

# LRU, LFU and minimal TTL algorithms are not precise algorithms but approximated
# algorithms (in order to save memory), so you can tune it for speed or
# accuracy. For default Redis will check five keys and pick the one that was
# used less recently, you can change the sample size using the following
# configuration directive.
#
# The default of 5 produces good enough results. 10 Approximates very closely
# true LRU but costs more CPU. 3 is faster but not very accurate.
#
# maxmemory-samples 5

5.2.2、通过命令行更改redis淘汰策略

[root@localhost src]# ./redis-cli -p 7000 -c 
127.0.0.1:7000> config get maxmemory-policy
1) "maxmemory-policy"
2) "noeviction"
127.0.0.1:7000> config set maxmemory-policy allkeys-lfu
OK
127.0.0.1:7000> config get maxmemory-policy
1) "maxmemory-policy"
2) "allkeys-lfu"
127.0.0.1:7000> 

3、采用近视算法的原因
之所以采用近似算法,可以提高性能,不用每一次都去排序计算概率。

六、redis的持久化方式

RDB持久化
原理是将Reids在内存中的数据库记录定时dump到磁盘上的RDB持久化
优点:
1、保存了 Redis 在各个时间点上的数据集,能恢复到各个时间节点上。 这种文件非常适合用于进行备份: 比如说,你可以在最近的 24 小时内,每小时备份一次 RDB 文件,并且在每个月的每一天,也备份一个 RDB 文件。 这样的话,即使遇上问题,也可以随时将数据集还原到不同的版本。

2、数据丢失后更容易恢复。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。

3、性能最大化。对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。

4、RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

缺点:
1、RDB可能会造成数据丢失的可能性大于AOF,一旦发生故障停机, 可能会丢失好几分钟的数据。
2、可能造成redis停止处理客户端数据。fork() 可能会非常耗时,造成服务器在某某毫秒内停止处理客户端; 如果数据集非常巨大,并且 CPU 时间非常紧张的话,那么这种停止时间甚至可能会长达整整一秒。

save 900 1    # 900 秒内有至少有 1 个键被改动 ,触发保存
save 300 10   # 300 秒内有至少有 10 个键被改动,触发保存
save 60 10000 # 60 秒内有至少有 1000 个键被改动,触发保存

AOF持久化
原理是将Reids的操作日志以追加的方式写入文件
优点:
1、能够同过采用配置宕机之后丢失更少的数据。Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。每修同步是是同步完成的,效率比较低。

2、 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。
3、AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。 导出(export) AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。

缺点:
1、对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

开启:
开启AOF持久化

修改redis.conf配置文件,redis 默认开启的.

appendonly yes
# appendfsync always(每修改同步)
appendfsync everysec(每秒同步)
# appendfsync no(每修改同步)

命令

config set appendonly yes

RDB与AOF如何选择:
1、小孩才做选择,大人全都要。应该同时使用两种持久化方式。
2、如果可以忍受几分钟数据缺失可以采用RDB

六、缓存和数据库双写一致性问题
一致性问题还可以再分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的问题。前提是如果对数据有强一致性要求,不能放缓存。我们所做的一切,只能保证最终一致性。

解决方案:
1、首先需要按照比较好的一个方案,对数据进行更新。先更新数据库,再删缓存。这种情况下也不是没有可能造成数据不一致了,如果下面这种情况:
(1)缓存刚好失效
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存
造成这种情况只有 必须步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5),这种情况发生概率太小,也是目前比较合适的一种处理方案。如果出现删除缓存失败的情况,可以添加一个异步队列对缓存进行更新。

七、缓存雪崩问题

1、使用互斥锁,但是该方案吞吐量明显下降了。
2、 给缓存的失效时间,加上一个随机值,避免集体失效,这个方案是工作更为常用方式。

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

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

八、缓存穿透问题

指查询一个不存在的数据时候,由于缓存是不命中时,出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次。请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
  
解决方案:
1、利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试。这个会大大降低并发量,一般不会采用。
2、采用异步更新策略,无论 Key 是否取到值,都直接返回。Value 值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。

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

九、缓存的并发竞争问题

多客户端同时并发写一个key,可能本来应该先到的数据后到了,导致数据缓存错误。如:客户端A一个key的值修改为valueA,客户端B修改为改为valueB。原本先改为valueA再改为valueB,但valueB却先到了,然后被改成了valueA。

解决方案:
1、首先使用分布式锁,确保同一时间,只能有一个系统实例在操作某个key。
2、key的值时,将value值携带时间(如valueB 02292317)。 要先判断这值的时间戳是否比缓存里的值的时间戳更靠后,如果是旧数据就不要更新了

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