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

Redis查漏补缺:Redis遗漏技术点和素养

时间:2023-03-11 21:03:31 科技观察

考虑到大部分写业务的程序员在实际开发中使用Redis,只有Setvalue和Getvalue两个操作,缺乏对Redis作为所有的。也恰好笔者有个同事下周要去培训Redis,所以笔者大胆的以Redis为主题,对Redis的常见问题进行了总结,希望能够扫清大家的知识盲点。本文主要关注以下几点:为什么要使用Redis使用Redis有什么缺点为什么单线程Redis那么快Redis数据类型,以及每种数据类型的使用场景Redis过期策略和内存淘汰机制一致性如何处理缓存穿透和缓存雪崩问题如何解决Redis并发竞争问题为什么使用Redis笔者认为在项目中使用Redis主要从性能和并发两个角度来考虑。当然,Redis也有其他功能可以用于分布式锁等功能,但是如果只是用于分布式锁等其他功能,还有其他中间件(如Zookpeer等)可以替代,以及没有必要使用Redis。所以这个问题主要从性能和并发两个角度来回答:1.性能如下图。当我们遇到需要执行很长时间且结果不经常变化的SQL时,将运行结果放入缓存中就特别适合。这样后续的请求都是从缓存中读取的,从而可以快速响应请求。题外话:突然想说说这个快速响应的标准——其实根据不同的交互效果,这个响应时间并没有固定的标准。但是,曾经有人跟我说:“在理想状态下,我们的页面跳转需要瞬间解决,页内操作需要瞬间解决。另外,超过一瞬间的耗时操作finger应该有进度提示,可以随时暂停或取消,给用户最好的体验。”那么一刹那、一刹那、弹指到底有多长呢?据《摩诃僧祗律》记载:一刹那为一念,二十念为一刹那,二十刹那为一刹那。弹指一算,二十念为一刹那。弹指一数余,拉二十个拉链一瞬间,一天一夜有三十个瞬间。那么仔细计算一下,瞬间是0.36秒,瞬间是0.018秒,弹指是7.2秒.2并发如下图所示,在大并发的情况下,所有的请求都直接访问数据库,数据库会出现连接异常,这时候就需要使用Redis做一个缓冲操作,这样request可以先访问Redis,而不是直接访问数据库。使用Redis有什么缺点?您已经使用Redis这么久了。这个问题必须搞清楚。基本上,你在使用Redis的时候都会遇到一些问题。常见的问题主要有四个方面:缓存和数据库双写一致性问题缓存雪崩问题Cache的击穿问题和缓存的并发竞争问题这四个问题,笔者个人认为在项目中比较常见,并且具体解决办法后面会给出。单线程Redis为什么这么快的问题其实是在考察Redis的内部机制。其实根据笔者的面试经验,很多人其实并不知道Redis是单线程的工作模型。因此,这个问题还是要重新审视的。主要有以下三点:纯内存操作单线程操作,避免频繁的上下文切换采用非阻塞的I/O多路复用机制现在我们来仔细谈谈I/O多路复用机制,因为这句话很常见,大多数人都不会不明白什么意思。打个比方:小曲在S市开了一家快递店,负责同城快递业务。由于经济拮据,小曲请了一批快递员,小曲发现资金不够,只够买辆车送快递。经营方式一:客户每次寄快递,小曲都会让快递员看管,然后快递员开车送快递。慢慢地,小曲发现这种运作方式存在着很多问题。几十个快递员基本上把时间都花在了抢车上,大部分快递员都闲着。谁抢到车,谁就能送快递。随着快递员的增加,快递员越来越多。小曲发现快递店越来越拥挤,又没有办法招到新的快递员。快递员之间的协调很费时间,而且大部分时间都花在了抢车上。优越的。基于以上缺点,小曲痛定思痛,提出了以下经营方式↓操作方式二:小曲只聘用一名快递员,客户寄来的快递,小曲根据派送地点进行标记,然后放在一个依次放置。***,快递员一个接一个去取快递,一个接一个,开车送快递,送完回来接下一个快递。对比以上两种操作方式,你是不是明显觉得第二种更高效更好?在上面的比喻中:每个快递员→每个线程每个快递→每个Socket(I/O流)快递寄送位置→Socket的不同状态,客户的快递请求→客户端请求,小趣的业务模式→运行在上面的代码服务器端→CPU核心数。所以我们有以下结论:第一种业务模式是传统的并发模型,每个I/O流(Express)由一个新的线程(Expresser)管理。第二种操作模式是I/O多路复用。只有一个线程(信使)通过跟踪每个I/O流的状态(每个信使投递到的地方)来管理多个I/O流。下面类比一下真实的Redis线程模型,如图:参考上图,简单来说,我们的Redis-client在运行时会产生不同事件类型的Socket。在服务器端,有一个I/O多路复用程序,将其放入队列中。然后文件事件分发器依次从队列中取出,转发给不同的事件处理器。需要注意的是,对于这种I/O多路复用机制,Redis还提供了Select、Epoll、Evport、Kqueue等多路复用函数库,大家可以自行学习。Redis数据类型及其各自的使用场景看到这个问题,是不是觉得很基础呢?其实笔者也是这么认为的。但是根据面试经验,至少80%的人都答不上这个问题。建议在项目中使用后,类比成记忆,这样体会更深,不要死记硬背。基本上一个合格的程序员都会用到这五种类型:1String这个没什么好说的,最常见的Set/Get操作,Value可以是String或者number,一般做一些复杂的计数函数缓存。2Hash这里Value存储的是结构化对象,操作其中一个字段比较方便。笔者在做单点登录的时候,用这个数据结构来存储用户信息,以CookieId为key,缓存过期时间设置为30分钟,可以很好的模拟类似Session的效果。3List使用了List的数据结构,可以进行简单的消息队列功能。另外就是可以使用Lrange命令实现基于Redis的分页功能,性能优异,用户体验好。4Set可以执行全局去重功能,因为Set堆叠了一个唯一值的集合。为什么不用JVM自带的Set去重呢?因为我们的系统一般都是集群部署的,所以使用JVM自带的Set比较麻烦。全局去重是否有必要开启一个公共服务?太麻烦了。此外,通过交、并、差等运算,可以计算出共同偏好、所有偏好、独特偏好等函数。5SortedSetSortedSet有一个权重参数Score,集合中的元素可以按照Score进行排列。可以作为排行榜应用,采取TOPN操作。此外,SortedSet还可以用于延迟任务。***一个应用程序是能够进行范围查找。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这个配置是配合内存淘汰策略的:Noeviction:当内存不足以容纳新写入的数据时,新的写操作会报错.没有人应该使用它;allkeys-lru:当内存不足以容纳新写入的数据时,在key空间中,移除最近最少使用的Key。目前推荐使用此类项目;allkeys-random:当内存不足以容纳新写入的数据时,在key空间中,随机取出一个key,不要再有人使用;volatile-lru:当内存不足以容纳新写入的数据时,移除设置过期时间的key空间中最近最少使用的Key。这种情况一般在同时使用缓存和持久化存储的情况下使用Redis。不建议;volatile-random:当内存不足以容纳新写入的数据时,从key空间中随机取出一个key,并设置过期时间。仍然不推荐;volatile-ttl:当内存不足以容纳新写入的数据时,在设置了过期时间的key空间中,先移除过期时间较早的key。不建议。PS:如果没有设置Expire的Key,则不满足先决条件(Prerequisites);那么Volatile-lru、Volatile-random和Volatile-ttl策略的行为与Noeviction(未删除)基本相同。Redis与数据库双写一致性问题一致性问题是常见的分布式问题,又可以分为最终一致性和强一致性。如果数据库和缓存是双写的,难免会出现不一致的情况。要回答这个问题,首先要明白一个前提:如果对数据有很强的一致性要求,就不能放缓存。我们所做的一切,只能保证最终的一致性。另外,我们所做的解决方案其实从根本上来说,只能说是降低了不一致的概率,并不能完全避免。因此,不能对一致性要求强的数据进行缓存。这里简单说一下:第一,采用正确的更新策略,先更新数据库,再删除缓存;第二,因为可能存在删除缓存失败的问题,所以提供一个补偿措施就可以了,比如使用消息队列。应对缓存穿透和缓存雪崩对于“如何应对缓存穿透和缓存雪崩”这两个问题,说实话,中小传统软件公司很难遇到。如果有百万级流量的大并发项目,这两个问题一定要深思熟虑:1.缓存穿透缓存穿透,即黑客故意请求缓存中不存在的数据,导致所有请求被阻塞。到数据库,所以数据库连接不正常。解决方案:使用mutex,缓存失效时,先获取锁,获取到锁,再请求数据库,如果没有获取到锁,sleep一会再试;采用异步更新策略,无论Key有没有取值,Both都直接返回。在Value值中维护缓存过期时间。如果缓存过期,将异步启动一个线程读取数据库并更新缓存。需要缓存预热操作(在项目启动前加载缓存);提供一种可以快速判断请求是否有效的拦截机制,比如使用布隆过滤器,内部维护一系列合法有效的Key,快速判断请求中携带的Key是否合法有效,直接返回如果他们不是。2Responsetocacheavalanche缓存雪崩,即缓存同时大面积失效。这时又来了一波请求,所有的请求都发到了数据库,导致数据库连接异常。解决方案:给缓存的失效时间加上一个随机值,避免集体失效;使用互斥量,但此解决方案的吞吐量显着降低;双缓存。我们有两个缓存,缓存A和缓存B。缓存A的过期时间是20分钟,而缓存B没有过期时间,缓存预热操作是自己进行的。再细分以下几个小点:a.从缓存A中读取数据库,直接返回;b.A没有数据,直接从B读取数据,直接返回,异步启动一个更新线程;C。更新线程同时更新CacheA和CacheB。Redis并发key竞争问题如何解决这个问题大致是有多个子系统同时设置一个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的顺序变化。这时候我们在向数据库写入数据的时候,需要保存一个时间戳。假设时间戳如下:SystemAKey1{ValueA3:00}SystemBKey1{ValueB3:05}SystemCKey1{ValueC3:10}那么,假设系统B先抢到锁,设Key1是{ValueB3:05}。接下来系统A抢到锁,发现自己的ValueA的时间戳早于缓存中的时间戳,所以不执行Set操作。等等。还可以使用其他方法,例如使用队列和将Set方法转换为串行访问。简而言之,要灵活。小结本文总结了Redis的常见问题。大部分是笔者在工作中遇到的,在采访别人的时候经常被问到的一些问题。希望每个人都能有所收获。笔者介绍的是中国平安研发工程师孤烟,目前负责规则云平台的架构设计和需求研发。毕业后一直从事Java开发,具有多年Web开发和架构设计实践经验。在MySQL性能优化、JVM调优、分布式领域有丰富经验。