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

Redis缓存总结:淘汰机制、缓存雪崩、数据不一致...._0

时间:2023-03-12 22:59:58 科技观察

https://github.com/Ccww-lx/JavaCommunity在实际工作项目中,缓存已经成为高并发高性能架构的关键组成部分,那么Redis为什么可以用作缓存呢?首先,可以作为缓存的两大特点:在分层体系中,内存/CPU访问性能好,缓存数据饱和,有良好的数据淘汰机制。由于Redis天生就具备这两个特点,所以Redis是基于内存操作的。并且有完善的数据淘汰机制,非常适合作为缓存组件。其中,基于内存运算,容量可达32-96GB,运算时间平均100ns,运算效率高。而且,还有很多数据淘汰机制。Redis4.0之后有8种类型,这使得Redis适合作为缓存的很多场景。为什么Redis缓存需要数据淘汰机制?8种数据淘汰机制是什么?数据淘汰机制Redis缓存是基于内存实现的,其缓存容量是有限的。当缓存满了,Redis应该怎么处理呢?当缓存满了的时候,Redis需要一个缓存数据淘汰机制,通过一定的淘汰规则来选择和删除一些数据,从而实现缓存服务的复用。那么Redis是采用什么淘汰策略来刷删数据的呢?Redis4.0之后,Redis的缓存淘汰策略有6+2种,包括三类:无数据淘汰,无数据淘汰,缓存满时,Redis不提供服务,直接返回错误。在设置过期时间的键值对volatile-random中,随机删除设置过期时间的键值对中的volatile-ttl,在设置过期时间的键值对中,根据过期时间的顺序,过期时间越早的越先被删除。volatile-lru,基于LRU(LeastRecentlyUsed)算法过滤设置过期时间的key-value对,最近最少使用原则过滤数据volatile-lfu,使用LFU(LeastFrequentlyUsed)算法选择key-valuepairswithanexpirationtime是的,使用频率最低的键值对来过滤数据。在所有键值对中,allkeys-random,从所有键值对中随机选取并删除数据allkeys-lru,使用LRU算法在所有数据中进行过滤allkeys-lfu,使用LFU算法在所有数据中进行过滤注意:LRU(LeastRecentlyUsed)算法,LRU维护一个双向链表,链表的头和尾代表MRU端和LRU端,分别代表最近使用的数据和最近最少使用的数据。LRU算法在实际实现时,需要使用一个链表来管理所有的缓存数据,这会带来额外的空间开销。而且,在访问数据时,需要将数据移动到链表上的MRU端。如果访问的数据量很大,会带来大量的链表移动操作,耗费时间,降低Redis缓存性能。其中LRU和LFU是根据Redis对象结构体的lru和refcount属性实现的redisObject:typedefstructredisObject{unsignedtype:4;unsignedencoding:4;//最后一次访问对象的时间unsignedlru:LRU_BITS;/*LRUtime(relativetogloballru_clock)or*LFUdata(leastsignificant8bitsfrequency//referencecount*andmostsignificant16bitsaccesstime).*/intrefcount;void*ptr;}robj;Redis的LRU会使用redisObject的lru记录上次被访问的时间,随机选择maxmemory-samples配置的参数个数作为候选集,其中lru属性值最小的数据被选择淘汰。在实际项目中,如何选择数据剔除机制?首选allkeys-lru算法,将最近访问的数据保留在缓存中,提高应用程序的访问性能。顶层数据使用volatile-lru算法,顶层数据不设置缓存过期时间,其他数据设置过期时间,基于LRU规则进行过滤。了解了Redis缓存淘汰机制后,Redis作为缓存有几种模式呢?Redis缓存模式Redis缓存模式根据是否接收写请求可以分为只读缓存和读写缓存:只读缓存:只处理读操作,所有更新操作都在数据库中,这样就有没有数据丢失的风险。CacheAside模式读写缓存,所有的读写操作都在缓存中进行,如果出现宕机故障,会导致数据丢失。数据缓存和回写到数据库有同步和异步两种:同步:访问性能低,更注重保证数据可靠性Read-Throug模式Write-Through模式异步:存在数据丢失风险,这侧重于提供低延迟的AccessWrite-BehindmodeCacheAsidemode查询数据先从缓存中读取数据,如果缓存中不存在,则从数据库中读取数据,拿到数据后更新到缓存缓存中,但是更新数据操作,它会先去更新数据库中的数据,然后再去使缓存中的数据失效。而CacheAside模式会存在并发风险:读操作未命中缓存,再查询数据库取数据。数据已查询但未放入缓存。同时更新写入操作使缓存失效,然后读取操作检索查询到的数据。加载缓存,导致缓存有脏数据。Read/Write-Throug方式的查询数据和更新数据都直接访问缓存服务,缓存服务同步更新数据到数据库。脏数据概率低,但对缓存的依赖性强,对缓存服务的稳定性要求很高,但同步更新会导致性能不佳。WriteBehind模式查询数据和更新数据都是直接访问缓存服务,但是缓存服务采用异步的方式更新数据到数据库(通过异步任务),速度快,效率高,但是数据的一致性比较差,并且有可能出现数据丢失的情况,实现逻辑也比较复杂。在实际项目开发中,根据实际业务场景需求选择缓存模式。了解了上面的内容之后,为什么我们的应用中需要使用redis缓存呢?在应用中使用Redis缓存可以提高系统性能和并发,主要体现在高性能:基于内存查询,KV结构,逻辑运算简单高并发:Mysql每秒只能支持2000左右的请求,而Redis可以轻松每秒超过1W。让80%以上的查询进入缓存,不到20%的查询进入数据库,可以大大提高系统吞吐量。虽然使用Redis缓存可以大大提高系统的性能,但是使用缓存会带来一些问题,比如缓存和双向数据库不一致,缓存雪崩等,这些问题怎么解决呢?使用缓存时的常见问题使用缓存,会出现一些问题,主要体现在:缓存与数据库双写不一致缓存雪崩:Redis缓存无法处理大量应用请求,中转至数据库层造成对数据库的压力数据库层激增;缓存穿透:访问数据在Redis缓存和数据库中不存在,导致大量访问穿透缓存直接转移到数据库,造成数据库层压力激增;缓存崩溃:缓存无法处理高频热点数据,导致直接高频访问数据库。层压急剧增加;缓存和数据库数据不一致。Read-onlycache(CacheAsidemode)对于只读缓存(CacheAsidemode),所有的读操作都发生在缓存中,数据不一致只发生在删除和修改操作上(新增操作不会,因为增加只会在数据库中处理),当发生删除操作时,缓存会将数据标记为无效并更新数据库。因此,在更新数据库和删除缓存值的过程中,无论这两个操作的执行顺序如何,只要有一个操作失败,就会出现数据不一致的情况。得出结论,当没有并发时,使用重试机制(消息队列),当有高并发时,使用延迟双删除(第一次删除后,休眠一定时间,然后删除),如如下:操作的顺序高吗?并发潜在问题现象对策先删除缓存,再更新数据库否删除缓存成功,更新数据库失败读取数据库旧值重试机制(数据库更新)先更新数据库,再删除缓存否数据库更新成功,则cachedeletionfailedtoreadtheoldvalue缓存重试机制(cachedeletion)先删除缓存,再更新数据库。缓存删除后,数据库还没有更新,有并发的读请求。并发读请求读取数据库的旧值并更新到缓存,导致后续读请求读取旧值延迟doubledelete()。先更新数据库,再删除缓存。数据库更新成功,从缓存中读取的旧值没有被删除。双删伪代码:redis.delKey(X)db.update(X)Thread.sleep(N)redis.delKey(X)读写缓存(Read/Write-Throug,WriteBehindmode)用于读写缓存,写操作都发生在缓存中,然后更新数据库,只要有一次操作失败,就会出现数据不一致。得出结论,当没有并发时,使用重试机制(消息队列),当并发高时,使用分布式锁。具体如下:操作顺序高吗?并发潜在问题现象解决方案先更新缓存,再更新数据库。否。缓存更新成功。如果数据库更新失败,会从缓存中读取最新的值,短期影响不大。重试机制(数据库更新)先更新数据库,再更新缓存没有数据库更新成功,缓存更新失败会从缓存中读取旧值重试机制(缓存删除)先更新数据库,再更新缓存write+read并发线程A先更新数据库,然后线程B读取数据,线程A更新缓存后,B会命中缓存。在读取旧值之前,A更新了缓存,这对业务有短期影响。先更新缓存,再更新数据库。Write+read并发线程A先更新缓存成功,然后线程B读取数据,此时线程B命中缓存,读取最新值后返回。线程A成功更新数据库后,线程B将命中缓存。如果读取到最新的值,则不会影响业务。先更新数据库,再更新缓存。B同时更新同一条数据。更新数据库的顺序是先A再B,但是更新缓存的顺序是先B再A,这样会导致数据库和缓存不一致。数据库和缓存不一致的写操作加上分布式锁先更新缓存。,然后更新数据库write+write并发线程A和线程B同时更新同一条数据,更新缓存的顺序是先A再B,但是更新数据库的顺序是先B再B然后A,这也将导致数据库和缓存的不一致写操作加分布式锁缓存雪崩缓存雪崩,由于缓存中大量数据同时过期或者缓存宕机,大量的应用请求无法在Redis缓存中处理,以及然后发送到数据库层会导致数据库层的压力急剧增加,严重的会导致数据库宕机。缓存中的大量数据会同时过期,导致大量请求无法处理。解决方案:数据预热,大并发访问前手动加载触发器缓存不同的key可以避免用户请求时先查询数据库设置不同的过期时间,这样缓存失效的时间点尽可能统一复制缓存,以及原始缓存过期时间设置为短期,副本缓存设置为长期服务降级。当缓存雪崩发生时,针对不同的数据采用不同的降级方案。比如非核心数据直接返回预定义信息,空值或者报错信息是缓存宕机,解决方法是:在业务系统中实现服务熔断或者请求限流机制,防止大量导致数据库宕机的访问次数。查询数据的结果,如果在缓存中找不到key对应的值,就必须去数据库重新查询,然后返回空(相当于两次无用的查询)。当有大量的访问请求,绕过缓存,直接去查数据库,数据库层的压力会急剧增加,严重的时候会导致数据库宕机。对于缓存穿透,解决方案:Cachenullordefaultvalues。当查询返回的数据为空时,空的结果也会被缓存,其过期时间会设置得比较短。下次访问会直接从缓存中取值,避免大量请求发送到数据库处理,导致数据库出现问题。布隆过滤器(BloomFilter),将所有可能的查询数据键散列成一个足够大的位图。查询的时候,先去BloomFilter检查key是否存在。如果不存在就直接返回,如果存在再去查询缓存。无需查询缓存中的数据库,避免因数据库层压力激增而宕机。Cachebreakdown缓存击穿,对于一个非常频繁访问的热点数据过期,导致访问无法在缓存中处理,会导致大量直接请求数据库,增加数据库层的压力,严重的会导致数据库宕机。对于缓存击穿,解决方法:不设置过期时间,访问特别频繁的热点数据不设置过期时间。总结在大多数业务场景中,Redis缓存都是作为只读缓存使用的。对于只读缓存,优先采用先更新数据库再删除缓存的方式来保证数据的一致性。其中缓存雪崩、缓存穿透、缓存击穿是三大问题的成因和解决方案。解决问题的方法是缓存雪崩。大量数据过期,缓存宕机。数据预热设置不同。Fuse限流机制缓存穿透数据在数据库和缓存中没有cachenull或默认值。设置过期时间