Redis缓存
什么是缓存
缓存就是数据交换的缓冲区,时临时存储数据的地方,一般读写性能比较高。比如CPU内部就有cache缓存,由于CPU运算的速度非常快,已经远远超过了内存的读写速度,以至于内存的读写速度降低了整体了性能,为了缓解这种速度差异矛盾,就再CPU内部增加了一块存储空间(缓存),用于存储CPU需要经常读写的数据,这样就不必要在内存中读写,使得CPU性能得到更好的发挥。因此才CPU的性能指标上缓存的大小也是很重要的一项。
在Web应用开发中,由于数据库需要从磁盘中加载数据,而磁盘IO的速度是比较慢的,因此数据库也需要缓存。当然在数据库内部也是有缓存的,但这还是不够快,因此就引入了其他的缓存中间件,如Redis、Memcache。在应用服务需要加载数据的时候,先去Redis中查询,如果没有命中再去数据中查询,这样就能使得系统整体的读写性能大大提高。
缓存作用模型
这里的客户端是指数据库的客户端,并不是与用户交互的应用客户端,一般来说就是后端应用服务层。当服务层需要查询数据时,先在Redis中查询,如果能查到就直接返回,如果没有再从数据库中查询,查到之后顺便把数据放入Redis中。
比如现在要根据id查询商铺的信息,大致流程如下:
这副流程图中还有很多的问题,藏着许多坑,接下来就来聊聊还存在什么问题。
缓存更新策略
缓存更新主要是解决缓存中的数据和数据库中的数据的一致性问题。
内存淘汰
Redis是基于内存的,数据都需要加载在内存中使用,但内存不像磁盘那样空间充足,所以Redis自己有一套内存淘汰机制,当内存不足时会自动淘汰部分数据,等下次查询时如果未命中就可以更新缓存,这样但这种方式存在很大的问题。
这种机制是无法控制的,淘汰的时候淘汰的是哪一部分数据、什么时候淘汰,也有可能内存比较大,很长一段时候都不会触发淘汰机制,这样就很长一段时间缓存和数据库数据不一致,所以一致性是比较差的。除非是静态数据,一般不会采用这种方式。
超时剔除
Redis里面添加数据的时候可以设置TTL过期时间,时间到期后会自动删除,等下次查询时再更新缓存。
这种方式数据一致性的强弱取决于过期时间TTL的长短,TTL长一点,一致性就差一点;TTL短一点,一致性就弱一点,但是如果过期时间过短,而数据的更新频率却比较低,这就会带来很多无效的缓存读写。这种方式数据一致性一般,维护起来也比较简单,适合数据变更不频繁的场景。
主动更新
主动更新,也就是再应用层编写业务逻辑,在修改数据库的同时,更新缓存。这种方式一致性是很高的,但维护成本是比较高的,还是隐藏着一些问题,到底是先更新数据库还是先更新缓存,看似简单顺序问题,却有很深的门道。
一般在高一致性需求场景下,就采用主动更新,并以超时剔除的作为兜底方案。也就是说,在正常情况下,修改数据库的同时更新缓存,但没有人能保证这种同时修改的原子性,如果更新缓存失败了,等过期时间一到,查询时就会从数据库中再写入新数据到缓存中,一段时间的数据不一致,但最终是一致的。
主动更新策略
缓存可以提升性能、缓解数据库压力,但是使用缓存也会导致数据不一致性的问题。有三种经典的缓存模式:
Cache Aside Pattern
旁路缓存模式,它的提出是为了尽可能地解决缓存与数据库的数据不一致问题。
Read/Write Through Pattern
读写穿透模式,服务端把缓存作为主要数据存储。应用程序跟数据库缓存交互,都是通过抽象缓存层完成的。
-
写的时候,先更新数据库,然后再删除缓存
可以理解为,缓存和数据库整合为一个服务,由这个服务来维护一致性。调用者调用服务,无需关心怎么实现的一致性。但现在没有这样直接拿来可以用的组件,只能靠自己去维护开发,成本比较高。
Write Behind Caching Pattern
这种方式也是由一个服务来维护一致性,但却又一个很大的不同Read/Write Through是同步更新缓存和数据的;Write Behind则是只更新缓存,不直接更新数据库,通过批量异步的方式来更新数据库,效率比较高。MySQL的InnoDB Buffer Pool机制就使用到这种模式
缓存问题
缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远都不会生效,这些请求会直接到数据库。就是穿过缓存,直接访问数据库。如果这样的请求非常多,就会直接把数据库搞崩。
常见的解决方案有两种:
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务直接宕机,导致大量的请求到达数据库,带来巨大压力。
解决方案:
- 给不同的key设置随机的过期时间,使得这些key的过期时间分布在某个时间段内,而不是同时过期。
- 利用Redis集群提高服务的可用性。
- 给缓存业务增加降级限流策略
- 添加多级缓存
缓存击穿
缓存击穿是指一个被高并发访问且缓存重建业务比较复杂的key突然失效了,这时大量的请求会在瞬间给数据给带来巨大的冲击。
有时候在缓存中存储的不是在数据库中直接存储的数据,而是需要从多个数据库中的表中进行查询, 在这段时间就会有很多请求到达数据库。
常见的解决方案有:
-
互斥锁 互斥锁使用Redis里的setnx 就可以简单的的实现 当setnx lock 1 返回1说明上锁成功,返回0说明上锁失败已经被其他线程上锁。 释放锁时直接del lock 即可。还有一个问题就是如果释放锁之前程序出现问题,那么就会出现无法释放锁的情况。所以要在这个锁上加个过期时间,确保这个锁能被释放。 这种方式可能可能会出现死锁,而且线程需要等待性能受影响比较大。 -
逻辑过期 向缓存中添加数据的时候不设置TTL,而是增加一个过期时间的字段。 这样当过期时间到了的时候,它还是会在缓存中,查询总是会命中,只不过在查询的时候会检查过期时间字段,如果发现过期了,先加锁然后开启一个线程去数据库中查询并写入缓存,然后再释放锁。如果发现已经被上锁了,就先返回已经过期的数据。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BZps6sNY-1646901981348)(C:\Users\Liuhd\Desktop\屏幕截图 2022-03-10 161756.png)]这种方式会有一定时间段的不一致性。 而是增加一个过期时间的字段。 这样当过期时间到了的时候,它还是会在缓存中,查询总是会命中,只不过在查询的时候会检查过期时间字段,如果发现过期了,先加锁然后开启一个线程去数据库中查询并写入缓存,然后再释放锁。如果发现已经被上锁了,就先返回已经过期的数据。
这种方式会有一定时间段的不一致性。
|