缓存
1、缓存穿透?
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
解决方案:
- 接口校验:在正常业务流程中可能会存在少量访问不存在 key 的情况,但是一般不会出现大量的情况,所以这种场景最大的可能性是遭受了非法攻击。可以在最外层先做一层校验:用户鉴权、数据合法性校验等,例如商品查询中,商品的ID是正整数,则可以直接对非正整数直接过滤等等。
- 缓存空值:当访问缓存和DB都没有查询到值时,可以将空值写进缓存,但是设置较短的过期时间,该时间需要根据产品业务特性来设置。
- 布隆过滤器:使用布隆过滤器快速判断数据是否存在,避免从数据库中查询数据是否存在,减轻数据库压力。 基于布隆过滤器的快速检测特性,即使发生缓存穿透了,大量请求只会查询Redis和布隆过滤器,而不会积压到数据库,也就不会影响数据库的正常运行。
2、缓存击穿?
缓存击穿是指缓存中没有,但数据库中有的数据(一般是热点数据缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大, 造成过大压力。
解决方案:
- 加互斥锁:在并发的多个请求中,只有第一个请求线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,直接走缓存。
- 热点数据不过期:直接将缓存设置为不过期,然后由定时任务去异步加载数据,更新缓存。这种方式适用于比较极端的场景,例如流量特别特别大的场景,使用时需要考虑业务能接受数据不一致的时间,还有就是异常情况的处理,不要到时候缓存刷新不上,一直是脏数据,那就凉了。
3、缓存雪崩?
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:
- 过期时间打散:既然是大量缓存集中失效,那最容易想到就是让他们不集中生效。可以给缓存的过期时间加上一个随机值时间,使得每个 key 的过期时间分布开来,不会集中在同一时刻失效。
- 热点数据不过期:该方式和缓存击穿一样,也是要着重考虑刷新的时间间隔和数据异常如何处理的情况。
- 加互斥锁:该方式和缓存击穿一样,按 key 维度加锁,对于同一个 key,只允许一个线程去计算,其他线程原地阻塞等待第一个线程的计算结果,然后直接走缓存即可。
4、缓存预热?
缓存预热是指系统上线后,提前将相关的缓存数据加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据。
如果不进行预热,那么Redis初始状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。
解决方案:
- 数据量不大的时候,工程启动的时候进行加载缓存动作;
- 数据量大的时候,设置一个定时任务脚本,进行缓存的刷新;
- 数据量太大的时候,优先保证热点数据进行提前加载到缓存。
5、缓存降级?
缓存降级是指缓存失效或缓存服务器挂掉的情况下,不去访问数据库,直接返回默认数据或访问服务的内存数据。降级一般是有损的操作,所以尽量减少降级对于业务的影响程度。
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
- 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
- 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
- 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
- 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
6、缓存一致性具体是什么意思?
缓存数据的一致性,这里的“一致性”包含了两种情况:
- 缓存中有数据,那么,缓存的数据值需要和数据库中的值相同;
- 缓存中本身没有数据,那么,数据库中的值必须是最新值。
不符合这两种情况的,就属于缓存和数据库的数据不一致问题了。
7、非并发下,数据不一致的发生及解决?
在更新数据库和删除缓存值的过程中,无论这两个操作的执行顺序谁先谁后,只要有一个操作失败了,就会导致客户端读取到旧值。
7.1 先删除缓存值,后更新数据库值
缓存删除成功,但是数据库更新失败,导致请求再次访问缓存时,发现缓存缺失,再读数据库时,从数据库中读到旧值
如下:
应用要把数据X的值从10更新为3,先在Redis缓存中删除了X的缓存值,但是更新数据库却失败了。如果此时有其他并发的请求访问X,会发现Redis中缓存缺失,紧接着,请求就会访问数据库,读到的却是旧值10。
7.2 先更新数据库值,后删除缓存。
数据库更新成功,但是缓存删除失败,导致请求再次访问缓存时,发现缓存命中,并从缓存中读取到旧值。
如下:
应用要把数据X的值从10更新为3,先成功更新了数据库,然后在Redis缓存中删除X的缓存,但是这个操作却失败了,这个时候,数据库中X的新值为3,Redis中的X的缓存值为10,这肯定是不一致的。如果刚好此时有其他客户端也发送请求访问X,会先在Redis中查询,该客户端会发现缓存命中,但是读到的却是旧值10。
总结:
解决:重试机制
- 可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如Kafka消息队列),当应用没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。
- 如果能够成功地删除或更新,我们就要把这些值从消息队列中去除,以免重复操作。
- 否则的话,我们还需要再次进行重试。如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。
8、并发下,数据不一致问题的发生及解决?
8.1 先删除缓存值,后更新数据库值
缓存删除后,尚未更新数据库,此时有并发读请求,并发请求从数据库读到旧值,并且更新到缓存,导致后续请求都读取旧值。
例如:
解决方案: 延迟双删,在线程A更新完数据库值以后,我们可以让它先sleep一小段时间,再进行一次缓存删除操作。
原因分析:
- sheep原因:线程A sleep的这段时间中,线程B能够先从数据库读取数据,再把缺失的数据写入缓存,避免空数据,然后线程A再进行删除。(线程A sleep的时间,需要大于线程B读取数据+写入缓存的时间)
- 再删缓存原因: 为了避免删除缓存后,还没更新数据库,读请求来读到旧数据存于缓存,导致以后都是旧数据,则在读请求结束后,写请求可以删除读请求造成的旧数据。
8.2 先更新数据库值,后删除缓存值。
数据库更新成功后,尚未删除缓存,此时有并发读请求,并发请求从缓存中读到旧值。
例如:
解决方案:等待缓存删除完成期间会有不一致数据短暂存在。
原因分析:
- 在这种情况下,如果其他线程并发读缓存的请求不多,那么,就不会有很多请求读取到旧值。
- 而且线程A一般也会很快删除缓存值,这样一来,其他线程再次读取时,就会发生缓存缺失,进而从数据库中读取最新值。所以,这种情况对业务的影响较小。
9、缓存和数据库不一致总结
缓存和数据库的数据不一致一般是由两个原因导致的
- 删除缓存值或更新数据库失败而导致数据不一致,可以使用重试机制确保删除或更新操作成功。
- 在删除缓存值、更新数据库的这两步操作中,有其他线程的并发读操作,导致其他线程读取到旧值,应对方案是延迟双删。
在大多数业务场景下,我们会把Redis作为只读缓存使用。针对只读缓存来说,我们既可以先删除缓存值再更新数据库,也可以先更新数据库再删除缓存。建议是,优先使用先更新数据库再删除缓存的方法,原因主要有两个:
- 先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力;
- 如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置。
注意:
使用先更新数据库再删除缓存时,如果业务层要求必须读取一致的数据,那我们可以在更新数据库时,暂缓客户端暂存并发读请求,等数据库更新完、缓存值删除后,再读取数据,从而保证数据一致性。
图表总结:
10、Redis缓存有哪些淘汰策略? ⚡
Redis缓存共存在8种淘汰机制,我们可以按照是否会进行数据淘汰把它们分成两类:
- 不进行数据淘汰的策略,只有noeviction这一种。
- 会进行淘汰的7种其他策略。
会进行淘汰的7种策略,我们可以再进一步根据淘汰候选数据集的范围把它们分成两类:
- 从设置了过期时间的数据集中选择性移除;
- 从全局的数据集中选择性移除。
设置了过期时间的数据集中选择性移除:
- volatile-ttl在筛选时,会针对设置了过期时间的键值对,根据过期时间的先后进行删除,越早过期的越先被删除。
- volatile-random就像它的名称一样,在设置了过期时间的键值对中,进行随机删除。
- volatile-lru会使用LRU算法筛选设置了过期时间的键值对。
- volatile-lfu会使用LFU算法选择设置了过期时间的键值对。
从全局的数据集中选择性移除:
- allkeys-random策略,从所有键值对中随机选择并删除数据;
- allkeys-lru策略,使用LRU算法在所有数据中进行筛选。
- allkeys-lfu策略,使用LFU算法在所有数据中进行筛选。
如图:
使用建议:
- 优先使用allkeys-lru策略。这样,可以充分利用LRU这一经典缓存算法的优势,把最近最常访问的数据留在缓存中,提升应用的访问性能。因此,如果你的业务数据中有明显的冷热数据区分,我建议你使用allkeys-lru策略。
- 如果业务应用中的数据访问频率相差不大,没有明显的冷热数据区分,建议使用allkeys-random策略,随机选择淘汰的数据就行。
- 如果你的业务中有置顶的需求,比如置顶新闻、置顶视频,那么,可以使用volatile-lru策略,同时不给这些置顶数据设置过期时间。这样一来,这些需要置顶的数据一直不会被删除,而其他数据会在过期时根据LRU规则进行筛选。
11、过期键的删除策略
Redis 使用的是惰性删除和定期删除相结合的过期删除策略。
- 惰性删除:设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。
- 优点:对CPU友好。
- 缺点:对内存不友好。
- 定时删除:设置某个key的过期时间同时,我们创建一个定时器,让定时器在该过期时间到来时,立即执行对其进行删除的操作。
- 优点:对内存友好。
- 缺点:对CPU不友好。
- 定期删除: 每隔一段时间,就对一些Key进行检查,删除里面过期的key。
- 优点:限制删除操作执行的时长和频率来减少删除操作对CPU的影响,也能有效释放过期键占用的内存。
- 缺点:难以确定删除操作执行的时长和频率。
并发
1、项目的并发访问问题
我们在使用Redis时,不可避免地会遇到并发访问的问题,比如说如果多个用户同时下单,就会对缓存在Redis中的商品库存并发更新。一旦有了并发写操作,数据就会被修改,如果我们没有对并发写请求做好控制,就可能导致数据被改错,影响到业务的正常使用(例如库存数据错误,导致下单异常)。
2、如何保证并发访问的正确性?
为了保证并发访问的正确性,Redis提供了两种方法,分别是加锁和原子操作。
- 加锁: 加锁是一种常用的方法,在读取数据前,客户端需要先获得锁,否则就无法进行操作。当一个客户端获得锁后,就会一直持有这把锁,直到客户端完成数据更新,才释放这把锁。
- 缺陷:
- 加锁操作多,会降低系统的并发访问性能
- Redis客户端要加锁时,需要用到分布式锁,而分布式锁实现复杂,需要用额外的存储系统来提供加解锁操作。
- 原子操作: 原子操作是指执行过程保持原子性的操作,而且原子操作执行时并不需要再加锁,实现了无锁操作。
- 优点:能保证并发控制,还能减少对系统并发性能的影响。
- Redis的原子操作采用了两种方法:
- 把多个操作在Redis中实现成一个操作,也就是单命令操作;
- 把多个操作写到一个Lua脚本中,以原子性方式执行单个Lua脚本。