应用场景
秒杀场景
注意:目前基于 Redis Standalone 模式的秒杀场景经过优化已经支持很高的并发能力,所以暂时不作更加深入的研究。
详细实现请参考示例https://gitee.com/dexterleslie/demonstration/tree/master/demo-redis/demo-flash-sale
示例中分别实现基于数据库的秒杀和基于 Redis 的秒杀,性能测试结果说明基于 Redis 秒杀性能高于基于数据库的秒杀。
基于 Redis 秒杀性能优化点如下:
- 基于消息队列的异步订单创建
- 基于 Redis 的库存余量判断/扣减和用户重复下单判断
- 基于 Redis 的库存余量不足后无效请求拦截
- todo 实现基于 JVM 的库存余量不足后无效请求拦截
- todo 实现基于 Redis 集群的秒杀
- todo 多级缓存:浏览器缓存、openresty 缓存、JVM 进程缓存、Redis 缓存
- todo 单个超热商品秒杀场景
- todo 双 11 场景高并发每秒 10w 订单生成方案
- todo 怎么使用 JMeter 大规模压测 10w QPS
示例总结:
- 使用 Lua 脚本进行库存余量判断、用户重复下单判断、库存余量扣减、用户重复下单位标记不能充分利用 Redis 集群的并发能力,在海量请求秒杀单个热点商品的情况下,此方案无法支撑。
分布式锁
应用缓存
缓存数据不一致
缓存更新策略
分类
Redis的缓存更新策略主要有以下几种:
一、内存淘汰策略
这是Redis自动进行的一种策略,当Redis内存达到设定的max-memory时,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式)。这种机制在一定程度上可以保证数据的一致性,但当内存不足时,它淘汰的是哪一部分数据是不确定的,因此这种一致性是比较差的。不过,其维护成本较低。
二、主动更新策略
对于高一致性需求,可以采用主动更新策略,并以超时剔除作为兜底方案。主动更新策略主要有以下三种模式:
- Cache Aside Pattern(缓存分离模式):
- 由缓存的调用者在更新数据库的同时更新缓存,但更常见的做法是更新数据库后删除缓存,然后在需要时重新从数据库加载数据到缓存。
- 这种方式需要开发者自己编写业务逻辑来保证数据库和缓存的一致性。
- 适用于读多写少的场景。
- Read/Write Through Pattern(读/写直通模式):
- 缓存与数据库整合成为一个服务,由服务来维护一致性。
- 调用者调用该服务时无需关心数据一致性问题。
- 优点在于整合的服务保证了数据的一致性,但缺点在于维护和开放成本高。
- Write Behind Caching Pattern(写回缓存模式):
- 调用者只操作缓存,由其他线程异步地将缓存数据持久化到数据库,保证最终一致。
- 这种方式可以提高效率,因为多次对缓存的更新可以合并为一次对数据库的更新。
- 但缺点在于无法保证一致性,且维护成本高。
三、超时剔除策略
为Redis中的缓存数据设置过期时间(TTL),当数据超时后,Redis会自动将其删除。下次用户查询未命中的缓存时,会重新从数据库加载数据到缓存,从而确保一致性。
- 一致性的强弱取决于设置的过期时间的长短。
- 维护成本相对较低,只需在原来设置缓存的逻辑上添加一个过期时间即可。
综上所述,Redis的缓存更新策略包括内存淘汰、主动更新(包括Cache Aside Pattern、Read/Write Through Pattern和Write Behind Caching Pattern)以及超时剔除。在实际应用中,可以根据业务场景和需求选择合适的策略。例如,对于低一致性需求可以采用内存淘汰策略;对于高一致性需求则可以采用主动更新策略,并以超时剔除作为兜底方案。
Cache Aside Pattern(缓存分离模式)
更新缓存还是删除缓存呢?
在Cache Aside Pattern中,关于更新缓存还是删除缓存的选择,通常推荐的做法是删除缓存而不是更新缓存。以下是详细的解释和原因:
一、删除缓存的优势
- 避免数据不一致:
- 当更新数据库时,如果直接更新缓存,可能会因为并发访问或更新延迟导致缓存和数据库中的数据不一致。
- 删除缓存则可以在下次访问时从数据库中重新加载最新数据,确保数据的一致性。
- 简化缓存逻辑:
- 更新缓存需要处理复杂的同步逻辑,以确保缓存和数据库中的数据保持一致。
- 删除缓存则简化了这一逻辑,因为下次访问时会自动从数据库中加载最新数据。
- 提高缓存利用率:
- 更新缓存可能会导致缓存中存储一些不再被访问的旧数据,浪费缓存空间。
- 删除缓存则可以在需要时重新加载最新数据,提高缓存的利用率。
二、更新缓存的潜在问题
- 并发访问问题:
- 在高并发场景下,多个请求可能同时更新数据库和缓存,导致数据不一致。
- 这种情况需要使用额外的同步机制(如分布式锁)来保证一致性,增加了实现的复杂性和性能开销。
- 更新延迟:
- 更新缓存可能需要一些时间,特别是在缓存数据较大或网络延迟较高的情况下。
- 这可能导致在更新缓存期间读取到旧数据,影响数据的一致性。
- 缓存失效风险:
- 如果更新缓存失败(例如由于网络问题或缓存服务故障),则可能导致缓存中的数据长时间无法更新,成为脏数据。
- 删除缓存则不存在这个问题,因为下次访问时会自动从数据库中加载最新数据。
三、实践中的选择
- 先更新数据库,再删除缓存:
- 这是推荐的实践方式。先更新数据库可以确保数据的持久性和一致性;再删除缓存则可以在下次访问时重新加载最新数据。
- 在实现时,需要注意处理并发访问和异常情况,以确保数据的一致性。
- 使用延时双删策略:
- 在更新数据库后,先删除缓存;然后等待一段时间(根据业务读取数据的平均耗时来确定),再次删除缓存。
- 这样可以进一步减少在数据库和缓存主从同步过程中读取到旧缓存数据的风险。但需要注意延时时间的合理设置和性能影响。
综上所述,在Cache Aside Pattern中,通常推荐删除缓存而不是更新缓存。这可以简化缓存逻辑、避免数据不一致、提高缓存利用率,并减少潜在的并发访问问题和更新延迟风险。在实践时,需要注意处理并发访问和异常情况,以确保数据的一致性。
先删除缓存再更新数据库还是先更新数据库再删除缓存呢?
在Cache Aside Pattern(旁路缓存模式)中,操作的顺序通常是先更新数据库,再删除缓存。以下是详细的解释:
一、Cache Aside Pattern的原理
- 读操作:当应用程序需要从数据库中获取数据时,它首先检查缓存是否已有。如果缓存中没有,应用程序从数据库中读取数据,并将其放入缓存。
- 写操作:当应用程序对数据进行写入操作时,它首先更新数据库中的数据,然后使缓存中的相关数据失效(即删除缓存)。
二、先更新数据库再删除缓存的原因
- 确保数据一致性:
- 如果先删除缓存,再更新数据库,中间可能会有短暂的时间窗口,在这个窗口内,缓存已失效,但数据库尚未更新。如果在这个时间段内有读取请求,这些请求将从数据库获取旧数据,而应用程序可能期望的是新数据,从而导致数据不一致。
- 先更新数据库可以确保数据已成功持久化。如果在更新数据库的过程中发生错误,缓存仍然保持原有的数据,避免缓存中存在无效或部分更新的数据。
- 避免竞态条件:
- 在高并发的系统中,多个进程或线程可能同时操作缓存和数据库。先更新数据库再删除缓存可以减少竞态条件的发生,因为数据库更新是一个原子操作,而删除缓存的操作相对简单且快速。
- 简化操作:
- 更新缓存需要处理缓存中的数据结构,这可能比简单地删除缓存更复杂。删除缓存后,可以避免复杂的同步逻辑,简化系统的维护。
三、实现时的注意事项
- 使用互斥锁:在更新数据库与删除缓存之间,可能有其他读取请求触发从数据库读取旧数据并重新填充缓存,导致缓存中存储的是旧数据。使用互斥锁可以防止这种情况的发生。
- 版本控制:在缓存数据中维护版本号,每次更新时增加版本号,确保读取时获取的是最新版本的数据。
- 事务机制:使用数据库事务确保更新操作的原子性。
- 重试机制:在删除缓存失败时,实施重试逻辑,确保缓存最终被更新或删除。
综上所述,在Cache Aside Pattern中,先更新数据库再删除缓存是确保数据一致性和系统稳定性的推荐做法。
MySQL 和 Redis 缓存数据不一致问题
对于热点数据(经常被查询,但不经常被修改的数据),我们可以将其放入 redis 缓存中,以增加查询效率,但需要保证从 redis 中读取的数据与数据库中存储的数据最终是一致的。针对一致性的问题进行了汇总总结。
MySQL 和 Redis 缓存不一致情况如下(以下操作方案都属于 Cache Aside Pattern):
https://blog.csdn.net/sanylove/article/details/127849015
先更新数据库,再更新缓存:「请求 A 」先把数据库的数据更新为1,然后在更新缓存之前,「请求 B 」再将数据库的数据更新为2,紧接着把缓存数据更新为2,然后「请求 A 」才更新缓存数据为1。
先更新缓存,再更新数据库:「请求 A 」先将缓存的数据更新为 1,然后在更新数据库前,「请求 B 」将缓存的数据更新为 2,紧接着把数据库更新为 2,然后「请求 A 」才将数据库的数据为1。
先删除缓存,再更新数据库:「请求 A 」先将缓存的数据删除,然后在更新数据库前,「请求 B 」来读取数据,但是没有在缓存中命中,所以「请求 B 」会去数据库读取数据,并更新到缓存中去,然后「请求 A 」才将数据库的数据。所以,先删除缓存,再更新数据库,在「读 + 写」并发的时候,还是会出现缓存和数据库的数据不一致的问题。
先更新数据库,再删除缓存:「请求 A 」去读取数据,但是未在缓存中命中,去数据库读取数据,但是在数据库读取数据之后还没有更新缓存数据之前,「请求 B 」去更新数据库数据,然后删除缓存数据,然后「请求 A 」才更新缓存数据。
参考示例重现此场景
https://gitee.com/dexterleslie/demonstration/tree/master/demo-redis/demo-redis-cache-inconsistency
从上面的理论上分析,先更新数据库,再删除缓存也是会出现数据不一致性的问题,但是在实际中,这个问题出现的概率并不高。因为缓存的写入通常要远远快于数据库的写入,所以在实际中很难出现请求 B 已经更新了数据库并且删除了缓存,请求 A 才更新完缓存的情况。所以,「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的。
从上面我们也知道「先更新数据库,再删除缓存」这属于两个操作,那么就会出现更新数据库成功,删除缓存失败的状态。如果出现这种状态,修改的数据是要过一段时间才生效,这个还是在我们加入过期时间的前提下。
那么怎么确保两个操作都能成功呢?其实解决方案有两种:重试机制和订阅 MySQL binlog,再操作缓存。
MySQL 和 Redis 缓存数据不一致情况解决方案如下:
对于「先删除缓存,再更新数据库」这种「读 + 写」并发请求而造成缓存不一致的解决办法:延迟双删。
延迟双删实现的伪代码如下:
#删除缓存 redis.delKey(X) #更新数据库 db.update(X) #睡眠 Thread.sleep(N) #再删除缓存 redis.delKey(X)所以,不管是「先更新数据库,再更新缓存」,还是「先更新缓存,再更新数据库」,这两个方案都存在并发问题。即当两个请求并发更新同一条数据的时候,可能会出现缓存和数据库中的数据不一致的现象。
我们通过分析可以知道「先更新数据库,再更新缓存」和「先更新缓存,再更新数据库」(即两个更新)在并发的时候,出现数据不一致问题。主要是因为更新数据库和更新缓存这两个操作是独立的,而我们又没有对操作做任何并发控制,那么当两个线程并发更新它们的话,就会因为写入顺序的不同造成数据的不一致。所以,我们可以对这两个操作进行控制,方法如下:
1、在更新缓存前先加个分布式锁,保证同一时间只运行一个请求更新缓存,就会不会产生并发问题了,当然引入了锁后,对于写入的性能就会带来影响。
2、在更新完缓存时,给缓存加上较短的过期时间,这样即时出现缓存不一致的情况,缓存的数据也会很快过期,对业务还是能接受的。
「先更新数据库,再删除缓存」不一致问题可以在缓存中加入过期时间,这样就算出现了缓存和数据库不一致问题,但最终是一致的。或者使用读写锁并发控制机制解决数据不一致问题。
解决方案的实现请参考示例
https://gitee.com/dexterleslie/demonstration/tree/master/demo-redis/demo-redis-cache-inconsistency
缓存穿透
https://baijiahao.baidu.com/s?id=1730541502423010481&wfr=spider&for=pc
缓存穿透是指查询一个缓存中和数据库中都不存在的数据,导致每次查询这条数据都会透过缓存,直接查库,最后返回空。当用户使用这条不存在的数据疯狂发起查询请求的时候,对数据库造成的压力就非常大,甚至可能直接挂掉。
解决缓存穿透的方法一般有两种:
- 缓存空对象。优点:实现简单,维护方便。缺点:额外的内存消耗(为缓存加入 TTL 以解决此问题),可能造成短期的数据不一致。
- 使用布隆过滤器。优点:内存占用较少,不存储多余的 key 到缓存中。缺点:实现复杂,存在误判可能(布隆过滤器在判断元素是否存在时,存在一种特定的误判情况:它可能会误判不存在的数据为存在,但绝不会误判实际存在的数据为不存在)。
详细用法请参考示例https://gitee.com/dexterleslie/demonstration/tree/master/demo-redis/demo-redis-cache-penetration
缓存击穿
https://baijiahao.baidu.com/s?id=1730541502423010481&wfr=spider&for=pc
缓存击穿是指当缓存中某个热点数据过期了,在该热点数据重新载入缓存之前,有大量的查询请求穿过缓存,直接查询数据库。这种情况会导致数据库压力瞬间骤增,造成大量请求阻塞,甚至直接挂掉。
解决缓存击穿的方法有两种:
- 设置 key 逻辑过期时间。Redis 中的 key 不设置 TTL,逻辑过期时间在 value 中设置,读取数据时判断逻辑过期时间是否过期,是则重新加载热点数据到缓存中(使用锁控制并发只能有一条线程加载热点数据到缓存,其他线程暂时返回当前过期数据),否则直接返回数据。(在高并发热点数据情况下,线程不需要等待,性能较好)
- 使用分布式锁,保证同一时刻只能有一个查询请求重新加载热点数据到缓存中,这样,其他的线程只需等待该线程运行完毕即可重新从Redis中获取数据或者等待一会儿后依旧无法获取锁则返回提示。(在高并发热点数据情况下,线程需要等待,性能受到影响)
详细用法请参考示例https://gitee.com/dexterleslie/demonstration/tree/master/demo-redis/demo-redis-cache-breakdown
缓存雪崩
https://baijiahao.baidu.com/s?id=1730541502423010481&wfr=spider&for=pc
缓存雪崩是指当缓存中有大量的key在同一时刻过期,或者Redis直接宕机了,导致大量的查询请求全部到达数据库,造成数据库查询压力骤增,甚至直接挂掉。
针对第一种大量key同时过期的情况,解决起来比较简单,只需要将每个key的过期时间打散即可,使它们的失效点尽可能均匀分布。
针对第二种redis发生故障的情况,部署redis时可以使用redis的几种高可用方案部署
实现简单所以暂时不做实验。