Redis夺命连环18问-缓存穿透击穿雪崩数据一致性分布式锁
这里有四个问题:缓存穿透、缓存击穿、缓存雪崩以及缓存和数据库之间的一致性问题。接下来将分别解释它们的含义以及相应的解决方案。
- 缓存穿透 缓存穿透是指查询不存在的数据,导致请求直接穿透缓存层,访问数据库层。这种情况可能导致数据库负载增加。
解决方法:a. 使用布隆过滤器(Bloom Filter)判断请求的数据是否存在,这样可以在一定程度上过滤掉不存在的请求。b. 对查询结果为空的数据,也缓存一个空值或特定标识,并设置一个较短的过期时间。这样可以防止数据库持续收到不存在数据的查询请求。
- 不存在的数据--穿透到数据库--多个哈希函数映射设置10是否存在-布隆过滤器快速判断是否存在-避免重复查询不存在的也可以缓存较短的时间
布隆过滤器(Bloom Filter)是一种用于快速判断一个元素是否存在于一个集合中的数据结构。它利用了哈希函数的高效性和位操作的快速性能,在很小的内存空间下,可以高效地判断一个元素是否属于一个集合。
布隆过滤器由布隆在 1970 年提出,它的核心思想是利用多个哈希函数将一个元素映射为多个位置,并将这些位置上的比特位设置为 1。当需要查询某个元素是否在集合中时,对该元素进行哈希,然后查询对应的多个位置上的比特位是否都为 1,如果都为 1,则认为元素可能存在于集合中,如果有任意一个比特位为 0,则认为元素一定不存在于集合中。
布隆过滤器的主要优点是空间效率和查询时间的高效性,它可以在很小的内存空间下存储大量的元素,同时查询时间是常数级别的。它的缺点是存在误判率,即有可能将不属于集合的元素判断为属于集合,但可以通过调整哈希函数的个数和比特位数来降低误判率。
布隆过滤器的应用非常广泛,例如在网络爬虫、缓存系统、拼写检查器、垃圾邮件过滤等领域都有广泛的应用。
在数据库写入数据 x 后,把数据 x 标记在布隆过滤器时,数据 x 会被 3 个哈希函数分别计算出 3 个哈希值,然后在对这 3 个哈希值对 8 取模,假设取模的结果为 1、4、6,然后把位图数组的第 1、4、6 位置的值设置为 1。当应用要查询数据 x 是否数据库时,通过布隆过滤器只要查到位图数组的第 1、4、6 位置的值是否全为 1,只要有一个为 0,就认为数据 x 不在数据库中。
布隆过滤器由于是基于哈希函数实现查找的,高效查找的同时存在哈希冲突的可能性,比如数据 x 和数据 y 可能都落在第 1、4、6 位置,而事实上,可能数据库中并不存在数据 y,存在误判的情况。
所以,查询布隆过滤器说数据存在,并不一定证明数据库中存在这个数据,但是查询到数据不存在,数据库中一定就不存在这个数据
- 缓存击穿 缓存击穿是指一个热点数据(经常被访问的数据)在缓存中过期,大量请求同时访问数据库以重新获取该数据。这可能导致数据库压力过大。
解决方法:a. 对热点数据设置永不过期,或设置一个较长的过期时间,这样可以减少数据库的访问压力。b. 使用互斥锁(Mutex)机制,当缓存失效时,只允许一个请求访问数据库,其他请求等待。一旦数据被重新加载到缓存中,其他请求可以直接从缓存获取。
- 击穿--热点数据过期--大量请求击穿到数据库--数据不过期--互斥锁只允许一个线程-然后加载到缓存
- 缓存雪崩 缓存雪崩是指在某个时间点,大量缓存数据同时过期,导致大量请求直接访问数据库,可能引发数据库崩溃。或者redis宕机
解决方法:a. 对缓存数据设置不同的过期时间,避免同时过期。b. 使用分布式缓存,将缓存数据分散到多个缓存服务器上,降低单个服务器的负载。c. 为缓存数据添加版本号,使用双缓冲机制,确保缓存数据在更新时不会全部失效。
- 雪崩--大量缓存数据同时雪崩式过期--设置随机而不是固定过期时间--分布式缓存--双缓冲
分布式缓存可以在一定程度上缓解缓存雪崩问题,原因在于它将缓存数据分散在多个缓存服务器上。通过这种方式,分布式缓存提供了以下优势来应对缓存雪崩:
- 负载均衡:分布式缓存将数据存储在多个缓存服务器上,从而均衡了每个服务器的负载。当某个缓存节点出现问题时,整个系统仍然可以继续运行,降低了整个缓存系统的风险。
- 容错性:分布式缓存通常采用冗余机制(如主从复制或分片副本),确保当某个缓存节点出现故障时,其他节点可以顶替它继续提供服务。这有助于应对缓存雪崩,因为即使某些缓存数据过期,仍有其他缓存节点可以提供服务。
- 提高性能:分布式缓存可以水平扩展,增加缓存节点以提高整体性能。当缓存雪崩发生时,分布式缓存可以更好地分摊请求压力,降低单个服务器的负载。
- 避免同时过期:通过分布式缓存,您可以在不同的缓存节点上设置不同的过期时间,从而避免大量数据同时过期。此外,也可以在不同节点上对相同的数据设置稍微不同的过期时间,降低缓存雪崩的可能性。
综上所述,分布式缓存通过负载均衡、容错性、提高性能和避免同时过期等方面有助于缓解缓存雪崩问题。然而,需要注意的是,分布式缓存不能完全解决缓存雪崩问题。为了更有效地预防缓存雪崩,还需要结合其他策略,如设置不同的过期时间、使用双缓冲机制等。
双缓存是一种常用的缓存策略,它可以有效地避免缓存雪崩问题。缓存雪崩是指当缓存中大量的数据同时过期或者缓存服务器宕机时,大量的请求会涌入后端数据库,导致数据库负载骤增,甚至导致服务不可用。
双缓存的原理是将缓存分为两个部分,一个主缓存,一个备份缓存。主缓存用来缓存热点数据,备份缓存则是主缓存的备份。当主缓存中的数据过期或者失效时,程序会从备份缓存中读取数据,然后重新填充主缓存。这样,即使主缓存失效,备份缓存中的数据也可以继续提供服务,避免了因为缓存失效导致的服务宕机。
在双缓存的实现中,主缓存和备份缓存会定期进行数据同步,确保主缓存和备份缓存中的数据一致性。同步操作可以采用定时同步或者实时同步的方式,以保证数据的一致性。
因此,使用双缓存可以避免缓存失效时出现的雪崩效应,从而提高系统的可用性和稳定性。双缓存的缺点是需要消耗额外的内存空间来维护备份缓存,同时在同步操作时需要考虑数据一致性的问题,因此需要谨慎地设计和实现。
当业务线程访问不到「主 key 」的缓存数据时,就直接返回「备 key 」的缓存数据,然后在更新缓存的时候,同时更新「主 key 」和「备 key 」的数据。
双 key 策略的好处是,当主 key 过期了,有大量请求获取缓存数据的时候,直接返回备 key 的数据,这样可以快速响应请求。而不用因为 key 失效而导致大量请求被锁阻塞住(采用了互斥锁,仅一个请求来构建缓存),后续再通知后台线程,重新构建主 key 的数据。
缓存异常会面临的三个问题:缓存雪崩、击穿和穿透。
其中,缓存雪崩和缓存击穿主要原因是数据不在缓存中,而导致大量请求访问了数据库,数据库压力骤增,容易引发一系列连锁反应,导致系统奔溃。不过,一旦数据被重新加载回缓存,应用又可以从缓存快速读取数据,不再继续访问数据库,数据库的压力也会瞬间降下来。因此,缓存雪崩和缓存击穿应对的方案比较类似。
雪崩式大量数据同时过期--击穿是一个热点数据
消息队列-通知加载缓存
互斥锁
数据不过期-加一个随机数
而缓存穿透主要原因是数据既不在缓存也不在数据库中。因此,缓存穿透与缓存雪崩、击穿应对的方案不太一样。
Redis 故障宕机
针对 Redis 故障宕机而引发的缓存雪崩问题,常见的应对方法有下面这几种:
- 服务熔断或请求限流机制;
- 构建 Redis 缓存高可靠集群;
- 缓存与数据库一致性问题 缓存与数据库一致性问题是指在更新数据库时,缓存中的数据没有及时更新,导致用户获取到的是旧数据。
解决方法:a. 采用先更新数据库,再更新缓存的策略。在更新数据时,先删除对应的缓存,再更新数据库。当请求到来时,缓存中没有数据,就会从数据库中加载最新数据。b. 采用消息队列(如Kafka、RabbitMQ)进行异步更新,确保缓存和数据库之间的数据同步。c. 对于一些对实时性要求不高的应用,可以使用一定的延迟更新策略,
更新数据时,不更新缓存,而是删除缓存中的数据。然后,到读取数据时,发现缓存中没了数据之后,再从数据库中读取数据,更新到缓存中。
阿旺想的这个策略是有名字的,是叫 Cache Aside 策略,中文是叫旁路缓存策略。
无论先更新哪个都会不一致,因此需要先删除一个
- 先删除缓存,再更新数据库;
- 先更新数据库,再删除缓存。
先删除缓存---读写并发的时候-另外一个线程没有命中缓存-会导致直接访问数据库--然后数据库写入缓存--导致还是以前的数据
不一致
核心!!!!!!!!!!!!!
如果先更新数据库,再删除缓存,假设一开始没有缓存,同时两个线程,一个先更新数据库,而一个没有命中缓存就会读取数据库,然后再删除缓存--然后另外的线程又写入缓存
最后会先把以前的数据写入到缓存中
因为缓存的写入通常要远远快于数据库的写入,所以在实际中很难出现请求 B 已经更新了数据库并且删除了缓存,请求 A 才更新完缓存的情况。
而一旦请求 A 早于请求 B 删除缓存之前更新了缓存,那么接下来的请求就会因为缓存不命中而从数据库中重新读取数据,所以不会出现这种不一致的情况。
所以,「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的。
果我们的业务对缓存命中率有很高的要求,我们可以采用「更新数据库 + 更新缓存」的方案,因为更新缓存并不会出现缓存未命中的情况。
但是这个方案前面我们也分析过,在两个更新请求并发执行的时候,会出现数据不一致的问题,因为更新数据库和更新缓存这两个操作是独立的,而我们又没有对操作做任何并发控制,那么当两个线程并发更新它们的话,就会因为写入顺序的不同造成数据的不一致。
所以我们得增加一些手段来解决这个问题,这里提供两种做法:
在更新缓存前先加个分布式锁,保证同一时间只运行一个请求更新缓存,就会不会产生并发问题了,当然引入了锁后,对于写入的性能就会带来影响。
在更新完缓存时,给缓存加上较短的过期时间,这样即时出现缓存不一致的情况,缓存的数据也会很快过期,对业务还是能接受的。
对了,针对「先删除缓存,再更新数据库」方案在「读 + 写」并发请求而造成缓存不一致的解决办法是「延迟双删」。
绕晕了
使用分布式锁可以避免多个进程或线程同时对同一资源进行操作,从而保证数据的一致性。在数据库和缓存的一致性维护中,可以使用分布式锁来解决缓存和数据库之间的一致性问题。具体来说,可以按照以下步骤进行操作:
- 获取分布式锁:当需要更新数据库时,先获取一个分布式锁,确保只有一个进程或线程可以对数据库进行操作。
- 更新数据库:在获取到分布式锁后,可以更新数据库中的数据,并将结果返回给客户端。
- 更新缓存:更新数据库后,需要更新缓存中的数据,以保证缓存和数据库的一致性。如果更新缓存失败,可以记录日志或进行相应的处理。
- 释放分布式锁:在更新完数据库和缓存后,需要释放分布式锁,让其他进程或线程可以对资源进行操作。
通过使用分布式锁来控制对资源的访问,可以保证在同一时间内只有一个进程或线程对资源进行操作,从而避免了竞态条件和数据不一致等问题。需要注意的是,分布式锁的实现需要考虑锁的粒度和锁的过期时间等问题,以满足业务需求。同时,为了避免锁的粒度过大或过小,需要合理设计锁的名称和范围。
Redis虽然被称为单线程的,但是实际上并不完全是单线程的,而是采用了多种技术实现了多线程的效果,例如I/O多路复用、线程池等技术。
具体来说,Redis的单线程指的是Redis的主线程只会执行一个客户端请求,而不是并发地执行多个请求。但是,Redis采用了I/O多路复用技术,可以同时处理多个客户端的请求,从而实现了多个客户端同时访问的效果。此外,Redis还采用了线程池技术,将一些耗时的操作(如BGSAVE、BGREWRITEAOF等)放到线程池中执行,避免了主线程的阻塞,从而提高了Redis的性能和并发能力。
需要注意的是,虽然Redis采用了多种技术实现了多线程的效果,但是Redis的主线程仍然是单线程的,而且Redis中的操作都是原子性的,这意味着Redis的性能非常高,但是对于一些复杂的操作,Redis可能并不是最适合的解决方案。因此,在使用Redis时需要综合考虑业务需求、数据量、数据访问模式等多个因素,以选择最合适的解决方案。
Redis采用了线程池和I/O多路复用技术来提高并发性能和效率,具体来说:
- 线程池:Redis采用线程池技术来实现一些耗时的操作,例如BGSAVE、BGREWRITEAOF等,这些操作都是在主线程之外的线程池中执行的,避免了主线程的阻塞,提高了Redis的并发能力和性能。
- I/O多路复用:Redis采用I/O多路复用技术来实现多个客户端的并发访问,当有多个客户端连接到Redis时,Redis会通过select或epoll等系统调用来监听多个socket,然后在同一个线程中依次处理多个客户端的请求,从而实现了多个客户端的并发访问。
具体来说,Redis中的每个客户端连接都是一个socket,当有多个客户端连接到Redis时,Redis会通过select或epoll等系统调用来监听多个socket,然后在同一个线程中依次处理多个客户端的请求。这样,即使Redis的主线程只能处理一个请求,也可以通过I/O多路复用技术来实现多个客户端的并发访问。
需要注意的是,线程池和I/O多路复用技术可以提高Redis的并发性能和效率,但是在使用时需要根据实际业务需求和数据访问模式来选择合适的配置和参数,以达到最优的性能和效率。同时,需要注意线程池和I/O多路复用技术的实现原理和限制,以避免一些潜在的问题和风险。
epoll和select都是Linux下常用的I/O多路复用机制,它们的作用都是监听多个文件描述符,并在有数据可读或可写时通知程序进行读写操作。它们的区别主要有以下几点:
- 原理:select采用的是轮询机制,将要监听的文件描述符集合放在select函数的参数中,每次调用select函数后,内核会扫描所有的文件描述符,当有文件描述符就绪时,select函数会返回这些文件描述符。而epoll采用的是事件通知机制,将要监听的文件描述符加入到epoll对象中,内核只会通知发生了事件的文件描述符,从而减少了扫描的开销。
- 监听对象数量:select监听的文件描述符数量受限于系统内存,而epoll没有这个限制,可以监听大量的文件描述符。
- 文件描述符的设置:在使用select时,需要通过fd_set集合设置监听的文件描述符,而在使用epoll时,需要使用epoll_ctl函数向epoll对象中添加或删除监听的文件描述符。
- 线程安全性:select函数是线程不安全的,如果多个线程同时调用select函数会出现竞态条件,需要通过加锁等方式来保证线程安全性。而epoll函数是线程安全的,多个线程同时调用epoll函数不会出现竞态条件。
总的来说,epoll比select在效率和可扩展性方面都要优于select。但是在实际使用时,需要根据具体的场景来选择合适的I/O多路复用机制。
发生事件的文件描述符是指在I/O多路复用机制中被监听的文件描述符中,当这些文件描述符有数据可读或可写时,就会发生事件,此时内核会通知程序进行读写操作的文件描述符就是发生事件的文件描述符。
在select和epoll等I/O多路复用机制中,程序可以通过将要监听的文件描述符加入到select或epoll对象中,然后通过select或epoll函数来监听这些文件描述符,当有文件描述符就绪时,函数会返回这些文件描述符,程序可以进行相应的读写操作。
举个例子,如果程序要监听一个网络连接,当这个网络连接有数据可读或可写时,就会发生事件,此时内核会通知程序进行读写操作,这个网络连接就是发生事件的文件描述符。当有多个网络连接需要监听时,这些网络连接都可以加入到select或epoll对象中,程序只需要在函数返回的文件描述符集合中进行处理即可。
首先解决一致性,就必须实现先删除一个,不然肯定不一致
先删除缓存--并发请求数据库还是不一致--所以就是加分布式锁---只有一个线程可以更新数据库然后更新缓存--再释放锁
然后就是先更新数据库再删除缓存--
按时无论先删除哪个高并发下还是会有问题
其实不管是先操作数据库,还是先操作缓存,只要第二个操作失败都会出现数据一致的问题。
使用消息队列的重试机制可以有效解决数据库缓存一致性的问题,具体步骤如下:
- 在写入数据库的同时,将要更新的缓存信息封装成一个消息,发送到消息队列中。
- 消息队列中的消费者程序负责接收消息并更新缓存信息,如果更新成功,则从消息队列中删除该消息;如果更新失败,则将该消息重新放回到消息队列中,并设置重试次数和重试时间间隔。
- 在消息队列中设置重试次数和时间间隔,如果消息在一定时间内未能被消费者程序成功处理,则消息队列会自动重新发送该消息,直到达到重试次数或者消息被成功处理为止。
通过消息队列的重试机制,可以确保缓存信息和数据库信息的一致性,并且可以避免瞬时高并发对数据库造成的压力。同时,可以设置消息队列的重试次数和时间间隔,以便在出现故障或网络不稳定等情况时,能够尽快恢复正常的缓存更新流程,保证系统的可用性和稳定性。
需要注意的是,使用消息队列的重试机制可以提高系统的可靠性和稳定性,但是也需要考虑消息队列本身的可用性和稳定性,以及消息队列与数据库之间的一致性问题。因此,在实际应用中需要综合考
通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除
Binlog是MySQL的二进制日志,记录了数据库的变更操作,包括插入、更新和删除等操作。通过读取Binlog可以获得数据库的变更信息,从而实现数据库与缓存的一致性。
使用Binlog处理数据库与缓存的一致性,需要进行以下步骤:
- 在MySQL中开启Binlog功能,并配置Binlog格式和保存位置等参数。
- 编写程序读取MySQL的Binlog,解析出变更信息,并发送到消息队列中。
- 在消息队列中设置消费者程序,负责接收变更信息,并根据变更信息更新缓存。
- 在更新缓存时,需要注意缓存的生命周期,以避免缓存过期或失效等问题。
需要注意的是,在使用Binlog处理数据库与缓存的一致性时,需要考虑一些限制和风险,例如Binlog本身的性能开销、读取Binlog的延迟、Binlog中的数据格式和记录方式等问题。因此,在使用Binlog处理数据库与缓存的一致性时,需要综合考虑多个因素,并进行测试和验证,以确保系统的性能、可靠性和稳定性。
阿里巴巴的Canal中间件是一个基于数据库增量日志解析,实现了MySQL和Oracle数据库的增量订阅和消费。Canal可以监控MySQL或Oracle的Binlog日志,解析出数据库的增量变更数据,并将数据发送到消息队列中,以便消费者程序进行处理。
Canal中间件主要包括以下功能:
- 数据库增量日志解析:Canal可以解析MySQL和Oracle的增量日志,并提取出数据库的增量变更数据。
- 数据库增量订阅:Canal可以订阅MySQL和Oracle的增量日志,实时监控数据库的变化,并将变化数据发送到消息队列中。
- 数据库增量消费:Canal可以作为消息队列中的消费者程序,接收数据库的增量变更数据,并进行处理。
Canal中间件可以帮助开发人员实现数据库与缓存的实时同步和数据仓库的增量更新等功能,提高了系统的性能和可靠性。同时,Canal还支持多种消费者程序,例如Kafka、RocketMQ等,可以根据实际业务需求和数据访问模式选择合适的消息队列和配置参数,以达到最优的性能和效率。
需要注意的是,在使用Canal中间件时,需要考虑Binlog的开销、解析的延迟、消息队列的可用性和稳定性等问题,以及中间件本身的可靠性和性能问题,从而保证系统的可用性和稳定性。
#你觉得今年春招回暖了吗#