当前位置: 首页 > 科技观察

分布式Redis数据库复习精讲

时间:2023-03-21 21:00:09 科技观察

简介为什么要写这篇文章?博主的《分布式之消息队列复习精讲》得到了大家的一致好评,感慨良多,心存诚意,想着再发一篇关于精修的文章。但是还是要说明一下,复习精讲的文章,更多的是在为面试做准备。在真正的开发过程中,还是脚踏实地,循序渐进,不要投机取巧。考虑到绝大多数写业务的程序员,在实际开发中使用redis时,只有setvalue和getvalue这两个操作,对redis整体缺乏了解。也恰好博主的一个同事下周要去培训redis,所以博主大胆的以redis为主题,对redis的常见问题做一个总结,希望能够弥补大家的知识盲点。复习要点?本文主要关注以下几点:1、为什么要用redis2,用redis有什么缺点?3、为什么单线程redis这么快?4、redis的数据类型及各数据类型的使用场景5、redis的过期策略和内存淘汰机制6、redis与数据库双写一致性问题7、如何处理缓存穿透和缓存雪崩问题8、如何处理解决redis并发竞争问题正文1.为什么使用redis分析:博主认为在项目中使用Redis主要从性能和并发两个角度考虑。当然redis也有分布式锁等其他功能,但是如果只是做分布式锁等功能,还有其他中间件(比如zoopeer等)代替,也不一定非得用redis。所以这个问题主要从性能和并发两个角度来回答。答:如下图,分为两点(1)性能题外话:突然想说一下快速响应这个标准。事实上,根据交互效果,这个响应时间并没有固定的标准。但是曾经有人跟我说:“在理想状态下,我们的页面跳转需要瞬间解决,页内操作需要瞬间解决。另外,超过弹指的耗时操作应该有进度提示,可以随时暂停或取消,给用户最好的体验。”那么一刹那、一刹那、弹指到底是多少时间呢?根据《摩诃僧祗律》,一念为一念,二十念为一念,二十念为弹指,二十弹指为一散,二十卢比为一念,一天有三十念。和一个晚上。那么仔细计算一下,一瞬间是0.36秒,一瞬间是0.018秒。弹指间长达7.2秒。(2)并发如下图所示,在大并发的情况下,所有的请求都直接访问数据库,数据库会出现连接异常。这时候就需要使用redis做一个缓冲操作,让请求先访问redis,而不是直接访问数据库。2、使用redis的缺点分析:大家用redis这么久了,这个问题想必都明白了。基本上使用redis都会遇到一些问题,常见的就那么几个。答:主要有四个问题:(1)缓存和数据库双写一致性问题(2)缓存雪崩问题(3)缓存击穿问题(4)缓存并发竞争问题这四个问题,我个人感觉项目在其中,比较常见的,后面给出具体的解决方法。3、单线程redis为什么这么快的分析:这道题其实是在考察redis的内部机制。其实根据博主的面试经验,很多人其实并不知道redis是单线程的工作模型。因此,这个问题还是要重新审视的。答:主要有以下三点(1)纯内存操作(2)单线程操作,避免频繁的上下文切换(3)采用非阻塞I/O多路复用机制题外话:我们现在得好好谈谈了I/O多路复用机制,因为这个说法太普通了,以至于大多数人不明白它是什么意思。博主打个比方:小曲在S市开了一家快递店,负责同城快递服务。由于经济拮据,小曲请了一批快递员,小曲发现资金不够,只够买辆车送快递。经营方式1、客户每次寄快递,小曲都会让快递员看管,然后快递员开车送快递。慢慢的小曲发现了这种运作方式存在以下问题。几十个快递员基本上把时间都花在了抢车上。大多数快递员都闲着。谁抢到车,谁就能送快递。随着快递员的增加,快递员越来越多。小曲发现快递店越来越拥挤,也没有办法雇用新的快递员。快递员之间的协调需要时间。结合以上不足,小曲痛定思痛,提出了以下操作模式。经营方式二小趣只雇用一名快递员。然后,客户寄来的快递,小曲会根据派送地点进行标记,然后依次放在一个地方。***,快递员一个接一个去取快递,一个接一个,然后开车送快递,送完回来取下一个快递。对比以上两种操作方式,你是不是明显觉得第二种更高效更好?在上面的比喻中:每个信使————>每个线程,每个信使——————>每个信使的socket(I/O流)投递位置————>socket的不同状态客户发送快递请求——————>客户端请求的操作方法——————>服务端运行的代码是一辆车————————————>号码那么CPU的内核数是我们有以下结论:1.第一种操作模式是传统的并发模型,其中每个I/O流(express)由一个新线程(express)管理。2.第二种操作模式是I/O多路复用。只有一个线程(信使)通过跟踪每个I/O流的状态(每个信使投递到的地方)来管理多个I/O流。下面类比真实的redis线程模型,如图,参考上图,简单来说就是。当我们的redis-client运行时,它会生成不同事件类型的套接字。在服务器端,有一个I/0多路复用程序,将其放入队列中。然后,文件事件分发器依次从队列中取出,转发给不同的事件处理器。需要注意的是,对于这种I/O多路复用机制,redis也提供了select、epoll、evport、kqueue等多路复用函数库,大家可以自行了解。4、redis的数据类型,以及每种数据类型的使用场景分析:你是不是觉得这道题很基础,其实我也这么觉得。但是根据面试经验,至少80%的人都答不上这个问题。建议在项目中使用后,可以对照记忆,加深体会,不要死记硬背。基本上,合格的程序员会使用所有五种类型。答:一共有五种类型(1)String其实没啥好说的,最常见的set/get操作,值可以是String也可以是数字。一般做一些复杂的计数函数的缓存。(2)hash这里的值是一个结构化对象,操作其中一个字段比较方便。博主在做单点登录的时候,用这个数据结构来存储用户信息,以cookieId为key,缓存过期时间设置为30分钟,可以很好的模拟出类似session的效果。(3)list使用了List的数据结构,可以完成简单消息队列的功能。另外就是可以使用lrange命令做基于redis的分页功能,性能优异,用户体验好。(4)set因为set堆叠了一个唯一值的集合。这样就可以做全局去重的功能了。为什么不用JVM自带的Set去重呢?因为我们的系统一般都是集群部署的,所以使用JVM自带的Set比较麻烦。做一个全局去重,开一个公共服务是不是太麻烦了。此外,通过交、并、差等运算,可以计算出共同偏好、所有偏好、独特偏好等函数。(5)sortedsetsortedset有一个额外的权重参数score,集合中的元素可以根据score进行排列。可以作为排行榜应用,采取TOPN操作。另外,参考另一篇文章?,指出sortedset可以用于延迟任务。***一个应用程序是能够进行范围查找。5、redis过期策略和内存淘汰机制分析:这个问题其实很重要。家里有没有用redis,从这个问题就可以看出。比如你的redis只能存储5G的数据,但是你写了10G,那么5G的数据就会被删除。你是怎么删除的?你想过这个问题吗?还有就是你的数据已经设置了过期时间,但是到时候内存使用率还是比较高的。你想过原因吗?答:Redis采用的是定时删除+惰性删除的策略。为什么不用定时删除策略呢?定时删除,使用定时器监控key,超时自动删除。虽然及时释放了内存,但是却消耗了大量的CPU资源。在大并发请求下,CPU应该花时间处理请求而不是删除key,所以没有采用这种策略。定时删除+惰性删除如何操作?定时删除,redis默认每100ms检查一次,是否有过期的key,如果有过期的key会被删除。需要注意的是,redis并不是每100ms检查一次所有的key,而是随机抽取检查一次(如果每100ms检查一次所有的key,redis就不会卡了)。因此,如果只采用定时删除策略,到时候很多键都不会被删除。因此,惰性删除就派上用场了。也就是说,当你拿到一个key的时候,如果设置了过期时间,redis会检查这个key是否已经过期?如果过期,此时将被删除。定时删除+惰性删除就没有其他问题了吗?不会,如果定期删除不会删除key。然后你没有立即请求key,也就是说懒删除没有生效。这样redis的内存就会越来越高。那么就要采用内存淘汰机制。在redis.conf中,有一行配置#maxmemory-policyvolatile-lru。这个配置自带内存淘汰策略(什么,你没配置?自己反省一下)1)noeviction:当内存不足以容纳新写入的数据时,Newwrite操作会报错。应该是没有用的。2)allkeys-lru:当内存不足以容纳新写入的数据时,在key空间中,移除最近最少使用的key。推荐使用,目前项目正在使用这个。3)allkeys-random:当内存不足以容纳新写入的数据时,从key空间中随机取出一个key。没有人应该使用它。不删的话,至少用Key,随便删。4)volatile-lru:当内存不足以容纳新写入的数据时,移除key空间中设置过期时间的最近最少使用的key。在这种情况下,redis一般既作为缓存,又作为持久化存储。不推荐5)volatile-random:当内存不足以容纳新写入的数据时,从key空间中随机移除一个key,并设置过期时间。仍然不推荐6)volatile-ttl:当内存不足以容纳新写入的数据时,在设置了过期时间的key空间中,先移除过期时间较早的key。PS不推荐:如果没有设置expirekey,则不满足前提条件;那么volatile-lru、volatile-random和volatile-ttl策略的行为与noeviction(不删除)基本相同。6、redis与数据库双写一致性问题分析:一致性问题是常见的分布式问题,又可以分为最终一致性和强一致性。如果数据库和缓存是双写的,难免会出现不一致的情况。要回答这个问题,首先要明白一个前提。即如果对数据有很强的一致性要求,则不能缓存。我们所做的一切,只能保证最终的一致性。另外,我们所做的解决方案其实从根本上来说,只能说是降低了不一致的概率,并不能完全避免。因此,不能对一致性要求强的数据进行缓存。答:《分布式之数据库和缓存双写一致性方案解析》分析的很详细,这里简单说一下。首先,采用正确的更新策略,先更新数据库,再删除缓存。其次,因为可能存在删除缓存失败的问题,所以提供一个补偿措施就可以了,比如使用消息队列。7、缓存穿透和缓存雪崩问题如何处理分析:说实话,中小型传统软件公司很难遇到这两个问题。如果有大的并发项目,流量上百万。这两个问题必须深入考虑。答:缓存穿透如下图,即黑客故意请求缓存中不存在的数据,导致所有请求都发往数据库,导致数据库连接异常。解决方法:(1)使用互斥量,当缓存失效时,先获取锁,获取锁后再请求数据库。如果没有拿到锁,会sleep一段时间再试(2)采用异步更新策略,不管key有没有值,直接返回。value值中维护了一个缓存过期时间。如果缓存过期,则异步启动一个线程来读取数据库并更新缓存。需要进行缓存预热(在项目启动前加载缓存)操作。(3)提供能够快速判断请求是否有效的拦截机制,例如使用Bloomfilter在内部维护一系列合法有效的key。快速判断请求中携带的Key是否合法有效。如果无效,直接返回。缓存雪崩,即缓存同时大面积失效。这时,又是一波请求。结果请求都发到数据库了,导致数据库连接异常。解决方法:(1)给缓存的失效时间加上一个随机值,避免集体失效。(2)使用了互斥锁,但是程序的吞吐量明显下降。(3)双缓冲。我们有两个缓存,缓存A和缓存B,缓存A的过期时间是20分钟,缓存B的过期时间没有设置。自己做缓存预热操作。然后再细分以下几个小点我从缓存A读取数据库,直接返回IIA没有数据,直接从B读取数据,直接返回,异步启动一个更新线程。III更新线程同时更新缓存A和缓存B。8、redis并发key竞争问题如何解决分析:这个问题大致是有多个子系统同时设置一个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的值会按照valueA-->valueB-->valueC的顺序变化。这时候我们在向数据库写入数据的时候,需要保存一个时间戳。假设时间戳如下SystemAkey1{valueA3:00}SystemBkey1{valueB3:05}SystemCkey1{valueC3:10}那么,假设系统B会先抢到锁,将key1设置为{valueB3:05}。接下来系统A抢到了锁,发现自己的valueA的时间戳早于缓存中的时间戳,所以不执行set操作。等等。也可以使用其他方法,例如使用队列和将set方法更改为串行访问。简而言之,要灵活。小结本文总结了redis的常见问题。大多是博主在工作中遇到的一些问题,之前采访别人的时候,也爱问。另外暂时不建议大家急着去佛脚下。如果你真的遇到一些有经验的工程师,其实随便点几下就能糊涂了。***,希望大家有所收获。系列回顾《分布式之数据库和缓存双写一致性方案解析》《分布式之缓存击穿》《分布式之延时任务方案解析》《分布式之消息队列复习精讲》

猜你喜欢