缓存架构设计方案
第一版设计方案:应用从redis获取缓存数据,写数据走MySQL通道。定时将mysql数据同步至Redis
存在问题:
1、缓存利用率低
redis中的大部分数据,访问频率很低。定时同步过程中,有很大的资源浪费。
2、缓存与数据库数据不一致
redis与数据库经常容易出现数据不一致的情况,只有定时同步后,数据才能保证一致。
问题一解决方案:
1、取消mysql定时同步策略,应用读数据完全走redis通道,redis不存在数据,再到mysql中查询数据并更新。redis中存在数据,均为热点数据。
2、缓存设置有效时间,超过时间后,自动清除缓存。redis自动可将非热点数据清除,保证redis中的缓存数据高效性。
由此得出第二版设计方案:
第二版设计方案未解决问题2,即缓存与数据库数据不一致问题。为解决缓存不一致问题,最先想到的解决方案为,更新数据库同时更新缓存数据。但是,更新数据库与更新缓存两个动作不可能处于同一个事务中。因此两个动作必有先后。当并发读写时,均可能出现redis与mysql长时间数据不一致的问题。分析过程如下:
1、先更新mysql,后更新redis
假设缓存数据X,线程1为t1,线程2位t2
t1 mysql:x=1
t2 mysql:x=2
t2 redis:x=2
t1 redis:x=1
结果:mysql中数据与redis中数据不一致
2、先更新redis,后更新mysql
t1 redis:x=1
t2 redis:x=2
t2 mysql:x=2
t1 mysql:x=1
结果:mysql中数据与redis中数据不一致
因此,在高并发场景下,不管redis与mysql的更新次序,均可能出现mysql与redis数据长期不一致的结果。采用如下替代方案,更新mysql数据时,不更新redis中的缓存,直接删除该缓存。应用在缓存中找不到数据后,直接到DB中查询,并同步至redis中。
1、先更新DB、后删除缓存
假设缓存数据X,线程1为t1,线程2位t2,缓存中最初不存在x,mysql中x=0
t2 获取mysql:x=0
t1 mysql:x=1
t1 删除x
t2 设置redis:x=0
结果:mysql中数据与redis中数据不一致。
发生条件:1、缓存中最初无x;2、读写并发;3、写数据穿插于读数据步骤之间。条件3发生概率基本为0,因为数据库写数据通常比读数据时间长。
2、先删除缓存、后更新DB
t1 删除x
t2 获取x,缓存找不到x,取mysql:x,
t2 设置redis:x=0
t1 mysql:x=1
结果:mysql中数据与redis中数据不一致
由上述分析发现,优先选择第一种方式:先更新DB、后删除缓存。但是还遗留一个问题,更新DB、删除缓存中第二个步骤可能不成功。
解决方案:mysql数据库记录需要删除缓存,采用异步线程方式,删除缓存。但是,这种方式会导致数据库的压力过大。采用MQ替代数据库,记录需要删除的缓存(第一次未删除缓存成功,再写MQ)。但是mq保存也可能出现失败。为什么不直接放在内存中。因为,进程重启,内存数据就丢失了。
由此,第三版设计图如下:
采用监听binlog推送数据至mq方式,可以减小应用代码书写量。第四版设计图如下
但是第四版设计方案对应canal、mq的可用性要求高,只要一个出问题,会导致redis不能更新。
缓存常见问题及解决方案
1、缓存穿透
缓存穿透是指查询一个根本不存在的数据,缓存层和持久层都不会命中。
解决方案:缓存中的key值value可设置为不存在,不存在与空不一样。应用查询到该key值,就不需要访问数据库。
2、缓存击穿
缓存中一个热点key值失效后,同一时间点,大量并发请求发生。此时,有大量请求访问数据库。可能导致数据库崩溃。
解决方案:
1、分布式锁:每个key值,只能有一个线程,能够刷新缓存最新数据。
2、缓存无失效时间,通过程序自动清除无效缓存。解决麻烦,不建议使用。
3、缓存雪崩
如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成缓存雪崩。
解决方案:
1、加锁排队,限流:同一时间内,处理缓存更新的最大线程数预先设定好。
2、缓存层高可用:redis采用cluster模式,建议结合1使用。
3、二级缓存