考虑到大部分写业务的程序员,在实际开发中使用Redis时,只有SetValue和GetValue这两个操作,对Redis整体知识缺乏。一个认知。所以冒昧的以Redis为主题,对Redis的常见问题做一个总结,希望能够弥补大家的知识盲点。本文主要关注以下几点:为什么要使用Redis使用Redis有什么缺点为什么单线程Redis那么快Redis数据类型以及每种数据类型的使用场景Redis过期策略和内存淘汰机制Redis与数据库双写一致性如何处理缓存穿透和缓存雪崩问题如何解决Redis的并发竞争关键问题为什么要使用Redis我认为在项目中使用Redis主要从性能和并发两个角度来考虑。当然Redis也有分布式锁等其他功能,但是如果只是做分布式锁等功能,还有其他的中间件,比如Zookeeper等,而不是使用Redis。所以这个问题主要从性能和并发两个角度来回答。性能如下图所示。当我们遇到执行时间较长且结果变化不频繁的SQL时,特别适合将运行结果放入缓存。这样后续的请求都是从缓存中读取的,从而可以快速响应请求。题外话:突然想说一下这个快速反应的标准。根据交互效果,这个响应时间没有固定的标准。但是,曾经有人跟我说:“在理想状态下,我们的页面跳转需要瞬间解决,页内操作需要瞬间解决。另外,超过弹指一挥间的耗时操作手指应该有进度提醒,可以随时暂停或取消,给用户最好的体验。”那么一刹那、一刹那、弹指到底是多少时间呢?据《摩诃僧祗律》记载:一念为一念,二十念为一念,二十念为弹指一圈,弹指二十为一圈,二十分钟为一念,一念有三十念。一天一夜。那么仔细一算,一瞬间是0.36秒,一瞬间是0.018秒,一根手指有7.2秒那么长。并发如下图所示。在大并发的情况下,所有请求都直接访问数据库,数据库会出现连接异常。这时候就需要用Redis做一个缓冲操作,让请求先访问Redis,而不是直接访问数据库。使用Redis有什么缺点?您已经使用Redis这么久了。这个问题必须搞清楚。基本上在使用Redis的时候都会遇到一些问题,常见的就那么几个。回答主要是四个问题:缓存和数据库双写一致性问题缓存雪崩问题缓存击穿问题缓存并发竞争问题。我个人认为这四个问题在项目中经常会遇到。具体的解决办法稍后会给出。单线程Redis为什么这么快的问题,是对Redis内部机制的考察。根据我的面试经验,很多人并不知道Redis是单线程的工作模型。因此,这个问题还是要重新审视的。答案主要是以下三点:纯内存操作单线程操作,避免频繁的上下文切换非阻塞I/O多路复用机制题外话:我们现在要详细说说I/O多路复用机制,因为这个名词是这样的常见的是大多数人不明白这意味着什么。打个比方:小曲在S市开了一家快递店,负责同城快递服务。由于经济拮据,小曲请了一批快递员,小曲发现资金不够,只够买辆车送快递。经营方式1、客户每次寄快递,小曲都会让快递员看管,然后快递员开车送快递。慢慢地,小曲发现了这种运作模式存在以下问题:几十个快递员基本上都在抢车,大部分快递员都闲着。谁抢到车,我就可以发快递。随着快递量的增加,快递员也越来越多。小曲发现快递店越来越拥挤,又没有办法招到新的快递员。快递员之间的协调需要时间。基于以上不足,小曲痛定思痛,提出了以下经营方式。经营方式二小趣只雇用一名快递员。然后,客户寄来的快递,小曲会根据派送地点进行标记,然后依次放在一个地方。***,快递员一个接一个去取快递,一个接一个,然后开车送快递,送完回来取下一个快递。对比以上两种经营方式,你是不是明显觉得第二种更高效更好?在上面的类比中:每个快递员→每个线程和每个快递员→每个Socket(I/O流)投递位置→Socket的不同状态,客户投递请求→客户端请求,小趣的业务方法→服务端运行的代码是一辆车→CPU核数。所以我们有以下结论:第一种运行方式是传统的并发模型,每个I/O流(express)由一个新的线程(express)管理。第二种操作模式是I/O多路复用。只有一个线程(一个信使)通过跟踪每个I/O流的状态(每个信使投递到的地方)来管理多个I/O流。下面类比一下真实的Redis线程模型,如图:简单的说,我们的redis-client在运行的时候,会产生不同事件类型的Sockets。在服务器端,有一个I/O多路复用器将其放入队列中。然后,文件事件分发器依次从队列中取出,转发给不同的事件处理器。需要注意的是,对于这种I/O多路复用机制,Redis也提供了select、epoll、evport、kqueue等多路复用函数库,大家可以自行了解。是不是觉得这道题很基础,关于Redis的数据类型以及每种数据类型的使用场景?我想是这样。但是根据面试经验,至少80%的人都答不上这个问题。建议在项目中使用后,可以对照记忆,加深体会,不要死记硬背。基本上,合格的程序员会使用所有五种类型。String就没什么好说的了,最常见的set/get操作,Value可以是String也可以是数字。一般做一些复杂的计数函数的缓存。这里的HashValue存储的是结构化对象,操作其中一个字段比较方便。我在做单点登录的时候,就是用这个数据结构来存储用户信息的,以CookieId为Key,缓存过期时间设置为30分钟,可以很好的模拟一个类似session的效果。List使用了List的数据结构,可以进行简单的消息队列功能。另外就是可以使用lrange命令做基于redis的分页功能,性能优异,用户体验好。Set是因为Set堆叠了一组唯一值。这样就可以做全局去重的功能了。为什么不用JVM自带的Set去重呢?因为我们的系统一般都是集群部署的,所以使用JVM自带的Set比较麻烦。做一个全局去重,开一个公共服务是不是太麻烦了。此外,通过交、并、差等运算,可以计算出共同偏好、所有偏好、独特偏好等函数。SortedSetSortedSet多了一个权重参数Score,集合中的元素可以按照Score进行排列。可以作为排行榜应用,采取TOPN操作。SortedSet可用于延迟任务。***一个应用程序是能够进行范围查找。Redis的过期策略和内存淘汰机制的问题很重要。家里有没有用Redis,从这个issue就可以看出。比如你的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与数据库双写一致性问题一致性问题是常见的分布式问题,又可以分为最终一致性和强一致性。如果数据库和缓存是双写的,难免会出现不一致的情况。要回答这个问题,首先要明白一个前提。即如果对数据有很强的一致性要求,则不能缓存。我们所做的一切,只能保证最终的一致性。另外,我们所做的解决方案只能降低不一致的概率,并不能完全避免。因此,不能对一致性要求强的数据进行缓存。答:首先,采用正确的更新策略,先更新数据库,再删除缓存。其次,因为可能存在删除缓存失败的问题,所以提供一个补偿措施就可以了,比如使用消息队列。如何应对缓存穿透和缓存雪崩这两个问题,说实话,中小的传统软件公司很难遇到这个问题。如果有大的并发项目,流量上百万。这两个问题必须深入考虑。缓存穿透是指黑客故意请求缓存中不存在的数据,导致所有请求都发送到数据库,导致数据库连接异常。缓存穿透解决方案:使用互斥锁,当缓存失效时,先获取锁,获取锁后再请求数据库。如果没有拿到锁,它会休眠一段时间,然后重试。采用异步更新策略,无论Key是否获取到值,都会直接返回。在Value值中维护缓存过期时间。如果缓存过期,则异步启动一个线程来读取数据库并更新缓存。需要进行缓存预热(在项目启动前加载缓存)操作。提供一种能够快速判断请求是否有效的拦截机制,例如使用Bloomfilter在内部维护一系列合法有效的key。快速判断请求中携带的Key是否合法有效。如果无效,直接返回。缓存雪崩,即缓存同时大面积失效。这时,又是一波请求。结果请求都发到数据库了,导致数据库连接异常。缓存雪崩解决方案:给缓存过期时间加一个随机值,避免集体失效。使用互斥量,但此解决方案的吞吐量会显着下降。双缓冲。我们有两个缓存,缓存A和缓存B,缓存A的过期时间是20分钟,缓存B的过期时间没有设置。自己做缓存预热操作。然后再细分以下几个小点:从缓存A中读取数据库,直接返回;如果A没有数据,直接从B读取数据,直接返回,异步启动一个更新线程,更新线程同时更新缓存A和缓存B。Redis并发竞争Keys的问题如何解决这个问题大致就是有多个子系统同时设置一个Key。这个时候,你想过要注意什么吗?需要说明一下,我提前百度了一下,发现答案基本都是推荐使用Redis事务机制。我不推荐使用Redis的事务机制。因为我们的生产环境基本是Redis集群环境,所以进行了数据分片操作。当你在一个事务中涉及到多个Key操作时,多个Key不一定存放在同一个redis-server上。所以Redis的事务机制很鸡肋。如果key操作不需要顺序,这种情况下,准备一个分布式锁,大家都可以抢到锁,抢到锁后做set操作就可以了,比较简单。如果对这个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的常见问题。大部分是我在工作中遇到的,以及之前面试别人的时候喜欢问的一些问题。另外暂时不建议大家急着去佛脚下。如果你真的遇到一些有经验的工程师,其实随便点几下就能糊涂了。***,希望大家有所收获。
