专业的网站建设制作服务,服务公司商标,微信 公众号平台,百川网站转载自 扫盲#xff0c;为什么分布式一定要有Redis?
考虑到绝大部分写业务的程序员#xff0c;在实际开发中使用 Redis 的时候#xff0c;只会 Set Value 和 Get Value 两个操作#xff0c;对 Redis 整体缺乏一个认知。所以我斗胆以 Redis 为题材#xff0c;对 Redis …转载自 扫盲为什么分布式一定要有Redis?
考虑到绝大部分写业务的程序员在实际开发中使用 Redis 的时候只会 Set Value 和 Get Value 两个操作对 Redis 整体缺乏一个认知。所以我斗胆以 Redis 为题材对 Redis 常见问题做一个总结希望能够弥补大家的知识盲点。
本文围绕以下几点进行阐述 为什么使用 Redis 使用 Redis 有什么缺点 单线程的 Redis 为什么这么快 Redis 的数据类型以及每种数据类型的使用场景 Redis 的过期策略以及内存淘汰机制 Redis 和数据库双写一致性问题 如何应对缓存穿透和缓存雪崩问题 如何解决 Redis 的并发竞争 Key 问题 一、为什么使用 Redis
我觉得在项目中使用 Redis主要是从两个角度去考虑性能和并发。
当然Redis 还具备可以做分布式锁等其他功能但是如果只是为了分布式锁这些其他功能完全还有其他中间件如 ZooKpeer 等代替并不是非要使用 Redis。因此这个问题主要从性能和并发两个角度去答。点击这里查看Redis面试题汇总。
1性能
如下图所示我们在碰到需要执行耗时特别久且结果不频繁变动的 SQL就特别适合将运行结果放入缓存。这样后面的请求就去缓存中读取使得请求能够迅速响应。 题外话忽然想聊一下这个迅速响应的标准。根据交互效果的不同这个响应时间没有固定标准。
不过曾经有人这么告诉我在理想状态下我们的页面跳转需要在瞬间解决对于页内操作则需要在刹那间解决。
另外超过一弹指的耗时操作要有进度提示并且可以随时中止或取消这样才能给用户最好的体验。
那么瞬间、刹那、一弹指具体是多少时间呢
根据《摩诃僧祗律》记载
一刹那者为一念二十念为一瞬二十瞬为一弹指二十弹指为一罗预二十罗预为一须臾一日一夜有三十须臾。
那么经过周密的计算一瞬间为 0.36 秒、一刹那有 0.018 秒、一弹指长达 7.2 秒。
2并发
如下图所示在大并发的情况下所有的请求直接访问数据库数据库会出现连接异常。
这个时候就需要使用 Redis 做一个缓冲操作让请求先访问到 Redis而不是直接访问数据库。点击这里查看Redis面试题汇总。 二、使用 Redis 有什么缺点
大家用 Redis 这么久这个问题是必须要了解的基本上使用 Redis 都会碰到一些问题常见的也就几个。
回答主要是四个问题 缓存和数据库双写一致性问题 缓存雪崩问题 缓存击穿问题 缓存的并发竞争问题
这四个问题我个人觉得在项目中是常遇见的具体解决方案后文给出。 三、单线程的 Redis 为什么这么快
这个问题是对 Redis 内部机制的一个考察。根据我的面试经验很多人都不知道 Redis 是单线程工作模型。所以这个问题还是应该要复习一下的。
回答主要是以下三点 纯内存操作 单线程操作避免了频繁的上下文切换 采用了非阻塞 I/O 多路复用机制
题外话我们现在要仔细的说一说 I/O 多路复用机制因为这个说法实在是太通俗了通俗到一般人都不懂是什么意思。
打一个比方小曲在 S 城开了一家快递店负责同城快送服务。小曲因为资金限制雇佣了一批快递员然后小曲发现资金不够了只够买一辆车送快递。
1经营方式一
客户每送来一份快递小曲就让一个快递员盯着然后快递员开车去送快递。
慢慢的小曲就发现了这种经营方式存在下述问题 几十个快递员基本上时间都花在了抢车上了大部分快递员都处在闲置状态谁抢到了车谁就能去送快递。 随着快递的增多快递员也越来越多小曲发现快递店里越来越挤没办法雇佣新的快递员了。 快递员之间的协调很花时间。
综合上述缺点小曲痛定思痛提出了下面的经营方式。
2经营方式二
小曲只雇佣一个快递员。然后呢客户送来的快递小曲按送达地点标注好然后依次放在一个地方。
最后那个快递员依次的去取快递一次拿一个然后开着车去送快递送好了就回来拿下一个快递。
上述两种经营方式对比是不是明显觉得第二种效率更高更好呢
在上述比喻中 每个快递员→每个线程 每个快递→每个 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 等多路复用函数库大家可以自行去了解。 四、Redis 的数据类型以及每种数据类型的使用场景
是不是觉得这个问题很基础我也这么觉得。然而根据面试经验发现至少百分之八十的人答不上这个问题。
建议在项目中用到后再类比记忆体会更深不要硬记。基本上一个合格的程序员五种类型都会用到。
1String
这个没啥好说的最常规的 set/get 操作Value 可以是 String 也可以是数字。一般做一些复杂的计数功能的缓存。
2Hash
这里 Value 存放的是结构化的对象比较方便的就是操作其中的某个字段。
我在做单点登录的时候就是用这种数据结构存储用户信息以 CookieId 作为 Key设置 30 分钟为缓存过期时间能很好的模拟出类似 Session 的效果。
3List
使用 List 的数据结构可以做简单的消息队列的功能。另外还有一个就是可以利用 lrange 命令做基于 Redis 的分页功能性能极佳用户体验好。
4Set
因为 Set 堆放的是一堆不重复值的集合。所以可以做全局去重的功能。为什么不用 JVM 自带的 Set 进行去重
因为我们的系统一般都是集群部署使用 JVM 自带的 Set比较麻烦难道为了一个做一个全局去重再起一个公共服务太麻烦了。
另外就是利用交集、并集、差集等操作可以计算共同喜好全部的喜好自己独有的喜好等功能。
5Sorted Set
Sorted Set多了一个权重参数 Score集合中的元素能够按 Score 进行排列。
可以做排行榜应用取 TOP N 操作。Sorted Set 可以用来做延时任务。最后一个应用就是可以做范围查找。点击这里查看Redis面试题汇总。 五、Redis 的过期策略以及内存淘汰机制
这个问题相当重要到底 Redis 有没用到家这个问题就可以看出来。
比如你 Redis 只能存 5G 数据可是你写了 10G那会删 5G 的数据。怎么删的这个问题思考过么
还有你的数据已经设置了过期时间但是时间到了内存占用率还是比较高有思考过原因么?
回答Redis 采用的是定期删除惰性删除策略。
1为什么不用定时删除策略
定时删除用一个定时器来负责监视 Key过期则自动删除。虽然内存及时释放但是十分消耗 CPU 资源。
在大并发请求下CPU 要将时间应用在处理请求而不是删除 Key因此没有采用这一策略。
2定期删除惰性删除是如何工作
定期删除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-lruvolatile-random 和 volatile-ttl 策略的行为和 noeviction(不删除) 基本上一致。点击这里查看Redis面试题汇总。 六、Redis 和数据库双写一致性问题
一致性问题是分布式常见问题还可以再分为最终一致性和强一致性。数据库和缓存双写就必然会存在不一致的问题。
答这个问题先明白一个前提。就是如果对数据有强一致性要求不能放缓存。我们所做的一切只能保证最终一致性。
另外我们所做的方案从根本上来说只能说降低不一致发生的概率无法完全避免。因此有强一致性要求的数据不能放缓存。
回答首先采取正确更新策略先更新数据库再删缓存。其次因为可能存在删除缓存失败的问题提供一个补偿措施即可例如利用消息队列。 七、如何应对缓存穿透和缓存雪崩问题
这两个问题说句实在话一般中小型传统软件企业很难碰到这个问题。如果有大并发的项目流量有几百万左右。这两个问题一定要深刻考虑。
缓存穿透即黑客故意去请求缓存中不存在的数据导致所有的请求都怼到数据库上从而数据库连接异常。
缓存穿透解决方案 利用互斥锁缓存失效的时候先去获得锁得到锁了再去请求数据库。没得到锁则休眠一段时间重试。 采用异步更新策略无论 Key 是否取到值都直接返回。Value 值中维护一个缓存失效时间缓存如果过期异步起一个线程去读数据库更新缓存。需要做缓存预热(项目启动前先加载缓存)操作。 提供一个能迅速判断请求是否有效的拦截机制比如利用布隆过滤器内部维护一系列合法有效的 Key。迅速判断出请求所携带的 Key 是否合法有效。如果不合法则直接返回。
缓存雪崩即缓存同一时间大面积的失效这个时候又来了一波请求结果请求都怼到数据库上从而导致数据库连接异常。点击这里查看Redis面试题汇总。
缓存雪崩解决方案 给缓存的失效时间加上一个随机值避免集体失效。 使用互斥锁但是该方案吞吐量明显下降了。 双缓存。我们有两个缓存缓存 A 和缓存 B。缓存 A 的失效时间为 20 分钟缓存 B 不设失效时间。自己做缓存预热操作。 然后细分以下几个小点从缓存 A 读数据库有则直接返回A 没有数据直接从 B 读数据直接返回并且异步启动一个更新线程更新线程同时更新缓存 A 和缓存 B。 八、如何解决 Redis 的并发竞争 Key 问题
这个问题大致就是同时有多个子系统去 Set 一个 Key。这个时候大家思考过要注意什么呢
需要说明一下我提前百度了一下发现答案基本都是推荐用 Redis 事务机制。
我并不推荐使用 Redis 的事务机制。因为我们的生产环境基本都是 Redis 集群环境做了数据分片操作。
你一个事务中有涉及到多个 Key 操作的时候这多个 Key 不一定都存储在同一个 redis-server 上。因此Redis 的事务机制十分鸡肋。
1如果对这个 Key 操作不要求顺序
这种情况下准备一个分布式锁大家去抢锁抢到锁就做 set 操作即可比较简单。
2如果对这个 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 的常见问题做了一个总结。大部分是自己在工作中遇到以及之前面试别人的时候爱问的一些问题。
另外不推荐大家临时抱佛脚真正碰到一些有经验的工程师其实几下就能把你问懵。最后希望大家有所收获吧。