考虑到绝大部分写业务的程序员,在实际开发中使用 Redis 的时候,只会 Set Value 和 Get Value 两个操做,对 Redis 总体缺少一个认知。java
因此我斗胆以 Redis 为题材,对 Redis 常见问题作一个总结,但愿可以弥补你们的知识盲点。python
为何使用Redis程序员
使用Redis 有什么缺点面试
单线程的Redis 为何这么快redis
Redis 的数据类型,以及每种数据类型的使用场景数据库
Redis 的过时策略以及内存淘汰机制缓存
Redis 和数据库双写一致性问题数据结构
如何应对缓存穿透和缓存雪崩问题并发
如何解决Redis 的并发竞争 Key 问题app
我以为在项目中使用 Redis,主要是从两个角度去考虑:性能和并发。固然,Redis 还具有能够作分布式锁等其余功能,可是若是只是为了分布式锁这些其余功能,彻底还有其余中间件,如 ZooKpeer 等代替,并非非要使用Redis。所以,这个问题主要从性能和并发两个角度去答。
性能
以下图所示,咱们在碰到须要执行耗时特别久,且结果不频繁变更的 SQL,就特别适合将运行结果放入缓存。这样,后面的请求就去缓存中读取,使得请求可以迅速响应。
题外话:突然想聊一下这个迅速响应的标准。根据交互效果的不一样,这个响应时间没有固定标准。
不过曾经有人这么告诉我:"在理想状态下,咱们的页面跳转须要在瞬间解决,对于页内操做则须要在刹那间解决。
另外,超过一弹指的耗时操做要有进度提示,而且能够随时停止或取消,这样才能给用户最好的体验。"
那么瞬间、刹那、一弹指具体是多少时间呢?
根据《摩诃僧祗律》记载:
一刹那者为一念,二十念为一瞬,二十瞬为一弹指,二十弹指为一罗预,二十罗预为一须臾,一日一晚上有三十须臾。
那么,通过周密的计算,一瞬间为 0.36 秒、一刹那有 0.018 秒、一弹指长达 7.2 秒。
并发
以下图所示,在大并发的状况下,全部的请求直接访问数据库,数据库会出现链接异常。
这个时候,就须要使用Redis 作一个缓冲操做,让请求先访问到Redis,而不是直接访问数据库。
你们用 Redis 这么久,这个问题是必需要了解的,基本上使用 Redis 都会碰到一些问题,常见的也就几个。
回答主要是四个问题:
缓存和数据库双写一致性问题
缓存雪崩问题
缓存击穿问题
缓存的并发竞争问题
这四个问题,我我的以为在项目中是常碰见的,具体解决方案,后文给出。
这个问题是对Redis内部机制的一个考察。根据个人面试经验,不少人都不知道Redis是单线程工做模型。因此,这个问题仍是应该要复习一下的。
回答主要是如下三点:
纯内存操做
单线程操做,避免了频繁的上下文切换
采用了非阻塞 I/O 多路复用机制
题外话:咱们如今要仔细的说一说 I/O 多路复用机制,由于这个说法实在是太通俗了,通俗到通常人都不懂是什么意思。
打一个比方:小曲在 S 城开了一家快递店,负责同城快送服务。小曲由于资金限制,雇佣了一批快递员,而后小曲发现资金不够了,只够买一辆车送快递。
经营方式一
客户每送来一份快递,小曲就让一个快递员盯着,而后快递员开车去送快递。
慢慢的小曲就发现了这种经营方式存在下述问题:
几十个快递员基本上时间都花在了抢车上了,大部分快递员都处在闲置状态,谁抢到了车,谁就能去送快递。
随着快递的增多,快递员也愈来愈多,小曲发现快递店里愈来愈挤,没办法雇佣新的快递员了。
快递员之间的协调很花时间。
综合上述缺点,小曲痛定思痛,提出了下面的经营方式。
经营方式二
小曲只雇佣一个快递员。而后呢,客户送来的快递,小曲按送达地点标注好,而后依次放在一个地方。
最后,那个快递员依次的去取快递,一次拿一个,而后开着车去送快递,送好了就回来拿下一个快递。
上述两种经营方式对比,是否是明显以为第二种,效率更高,更好呢?
在上述比喻中:
每一个快递员→每一个线程
每一个快递→每一个 Socket(I/O 流)
快递的送达地点→Socket 的不一样状态
客户送快递请求→来自客户端的请求
小曲的经营方式→服务端运行的代码
一辆车→CPU 的核数
因而咱们有以下结论:
经营方式一就是传统的并发模型,每一个 I/O 流(快递)都有一个新的线程(快递员)管理。
经营方式二就是 I/O 多路复用。只有单个线程(一个快递员),经过跟踪每一个 I/O 流的状态(每一个快递的送达地点),来管理多个 I/O 流。
下面类比到真实的Redis 线程模型,如图所示:
简单来讲,就是咱们的 redis-client 在操做的时候,会产生具备不一样事件类型的 Socket。
在服务端,有一段I/O 多路复用程序,将其置入队列之中。而后,文件事件分派器,依次去队列中取,转发到不一样的事件处理器中。
须要说明的是,这个 I/O 多路复用机制,Redis 还提供了 select、epoll、evport、kqueue 等多路复用函数库,你们能够自行去了解。
是否是以为这个问题很基础?我也这么以为。然而根据面试经验发现,至少百分之八十的人答不上这个问题。
建议,在项目中用到后,再类比记忆,体会更深,不要硬记。基本上,一个合格的程序员,五种类型都会用到。
String
这个没啥好说的,最常规的 set/get 操做,Value 能够是 String 也能够是数字。通常作一些复杂的计数功能的缓存。
Hash
这里 Value 存放的是结构化的对象,比较方便的就是操做其中的某个字段。
我在作单点登陆的时候,就是用这种数据结构存储用户信息,以 CookieId 做为 Key,设置 30 分钟为缓存过时时间,能很好的模拟出相似 Session 的效果。
List
使用 List 的数据结构,能够作简单的消息队列的功能。另外还有一个就是,能够利用 lrange 命令,作基于 Redis 的分页功能,性能极佳,用户体验好。
Set
由于 Set 堆放的是一堆不重复值的集合。因此能够作全局去重的功能。为何不用 JVM 自带的 Set 进行去重?
由于咱们的系统通常都是集群部署,使用 JVM 自带的 Set,比较麻烦,难道为了一个作一个全局去重,再起一个公共服务,太麻烦了。
另外,就是利用交集、并集、差集等操做,能够计算共同喜爱,所有的喜爱,本身独有的喜爱等功能。
Sorted Set
Sorted Set多了一个权重参数 Score,集合中的元素可以按 Score 进行排列。
能够作排行榜应用,取 TOP N 操做。Sorted Set 能够用来作延时任务。最后一个应用就是能够作范围查找。
这个问题至关重要,到底 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
该配置就是配内存淘汰策略的(什么,你没配过?好好检讨一下本身):
noeviction:当内存不足以容纳新写入数据时,新写入操做会报错。应该没人用吧。
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。推荐使用,目前项目在用这种。
allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 Key。应该也没人用吧,你不删最少使用 Key,去随机删。
volatile-lru:当内存不足以容纳新写入数据时,在设置了过时时间的键空间中,移除最近最少使用的 Key。这种状况通常是把Redis 既当缓存,又作持久化存储的时候才用。不推荐。
volatile-random:当内存不足以容纳新写入数据时,在设置了过时时间的键空间中,随机移除某个 Key。依然不推荐。
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过时时间的键空间中,有更早过时时间的 Key 优先移除。不推荐。
PS:若是没有设置 expire 的 Key,不知足先决条件(prerequisites);那么 volatile-lru,volatile-random 和 volatile-ttl 策略的行为,和 noeviction(不删除) 基本上一致。
一致性问题是分布式常见问题,还能够再分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的问题。
答这个问题,先明白一个前提。就是若是对数据有强一致性要求,不能放缓存。咱们所作的一切,只能保证最终一致性。
另外,咱们所作的方案从根本上来讲,只能说下降不一致发生的几率,没法彻底避免。所以,有强一致性要求的数据,不能放缓存。
回答:首先,采起正确更新策略,先更新数据库,再删缓存。其次,由于可能存在删除缓存失败的问题,提供一个补偿措施便可,例如利用消息队列。
这两个问题,说句实在话,通常中小型传统软件企业,很难碰到这个问题。若是有大并发的项目,流量有几百万左右。这两个问题必定要深入考虑。
缓存穿透,即黑客故意去请求缓存中不存在的数据,致使全部的请求都怼到数据库上,从而数据库链接异常。
缓存穿透解决方案:
利用互斥锁,缓存失效的时候,先去得到锁,获得锁了,再去请求数据库。没获得锁,则休眠一段时间重试。
采用异步更新策略,不管 Key 是否取到值,都直接返回。Value 值中维护一个缓存失效时间,缓存若是过时,异步起一个线程去读数据库,更新缓存。须要作缓存预热(项目启动前,先加载缓存)操做。
提供一个能迅速判断请求是否有效的拦截机制,好比,利用布隆过滤器,内部维护一系列合法有效的 Key。迅速判断出,请求所携带的 Key 是否合法有效。若是不合法,则直接返回。
缓存雪崩,即缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而致使数据库链接异常。
缓存雪崩解决方案:
给缓存的失效时间,加上一个随机值,避免集体失效。
使用互斥锁,可是该方案吞吐量明显降低了。
双缓存。咱们有两个缓存,缓存 A 和缓存 B。缓存 A 的失效时间为 20 分钟,缓存 B 不设失效时间。本身作缓存预热操做。
而后细分如下几个小点:从缓存 A 读数据库,有则直接返回;A 没有数据,直接从 B 读数据,直接返回,而且异步启动一个更新线程,更新线程同时更新缓存 A 和缓存 B。
这个问题大体就是,同时有多个子系统去 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 的常见问题作了一个总结。大部分是本身在工做中遇到,以及以前面试别人的时候,爱问的一些问题。
合理利用本身每一分每一秒的时间来学习提高本身,不要再用“ 没有时间 ”来掩饰本身思想上的懒惰!趁年轻,使劲拼,给将来的本身一个交代!
文章来源:
https://my.oschina.net/u/3967312/blog/2088188
做者:Java干货分享
-END-