缓存设计
更新: 5/2/2025 字数: 0 字 时长: 0 分钟
当服务器的流量过大时,我们往往会使用缓存的方式来减缓数据库压力,一般会使用Redis(分布式缓存),而Redis也有自己的问题(三大问题),因此会又会采取本地缓存(单机缓存)与其互补,让本地缓存预先再抗住一部分的流量
在一般的系统设计中,我们会预先对流量进行一系列的处理(OpenRestry,Sentinel,Guava RateLimter),之后才会到达我们的系统,这时我们一般就会认为这一次访问是正常的访问了,但在高并发的场景下(比如秒杀场景),我们仍然要想法设法的来避免大量流量导致的服务异常
技术选型
本地缓存:
- Guava Cache:Google Guava 包下内置的缓存方案,方便集成,但是性能一般
- Caffeine:Google 开源的本地缓存方案,集成难度相对Caffeine高一点(就一点点,可以忽略),但性能更好,且功能更加强大
- EhCache:功能丰富,支持多种储存层,与Hibernate的集成较好,但配置难度更高(使用xml配置),性能相对Caffeine也差一点
分布式缓存:
- Redis:目前市面上最常见的分布式缓存方案,将数据存储于内存中,有着可谓恐怖的读写性能,天生支持分布式,插件功能丰富(布隆过滤器/向量数据库)
- Memcached:一个比较老的缓存方案,也是将数据存储在内存中,但种种方面相对于Redis略显过时,一般都是老项目在用
我个人会偏向使用Caffeine+Redis作为一个分布式项目的缓存方案
读写设计
读
一般是层层递进,先去本地缓存去读,读不到进入分布式缓存,最后再去数据库读,若在某一层读到,则立即返回,同时异步的向前面的层去更新缓存
写
写的顺序一般是和读的顺序相反,用户持久化一个数据到数据库后,我们就立刻返回给用户一个结果,然后异步的去将持久化的数据更新到缓存中
刷新设计
由于Caffeine和Redis都是基于内存,且热点数据具有较强的时效性,因此我们应该对数据设置一个缓存时间,让缓存的数据有一个自己的生命周期,同时为了防止缓存雪崩,我们往往会对分布式缓存和本地缓存设置一个时间差(往往本地缓存更新较快,缓存有效期一分钟左右),这样就能最小的防止问题的发生
刷新机制
- 本地缓存:
- 主动刷新:请求接口会传入一个版本号,如果版本号大于本地缓存的版本号,则说明失效,此时需要从分布式缓存中获取数据
- 被动刷新: 本地缓存过期,自动拉取刷新
- 分布式缓存:
- 主动刷新: 业务驱动缓存刷新(比如出现了新的持久化数据或持久化数据发生更新),这时需要我们主动的去刷新数据
- 被动刷新: 当数据过期时,自动拉取数据到分布式缓存中
数据一致性
缓存的引入自然会导致数据的缓存一致性问题,这个一致性是缓存和数据库之间的数据一致性问题,在一些系统中,我们对一致性的要求可能不会那么高(比如论坛,视频网站,几秒甚至几分钟的一个一致性问题是可以容忍的),而对于秒杀/支付一系列涉及到金钱或是安全的系统,则需要较高的一致性,这个一致性需要我们用代码去实现与保护(比如真正支付的时候都去校验一下数据库中的内容)
分布式锁
涉及到分布式上锁的问题,就不得不提到分布式锁。在分布式场景下,如果我们希望对一个资源进行上锁,我们就可以采用分布式锁的方式为其上锁。
由于Redis的高性能,我们往往会选择Redis作为我们的分布式锁的实现方案,一种简单的实现方案是基于Lua脚本来进行实现。
实现思路
一个简单的代码实现思路:
为一个资源赋予一个id,使其作为分布式锁的key,在当前线程试图获取到资源时,先检测这个key是否存在,若key存在 则表示资源已被上锁,我们可以使用一个while循环来让这个线程短暂的自旋一下,等待锁被释放;若key不存在 则为这个key赋予一个value(使用一个全局唯一的随机值,比如uuid等),同时加上一个最大时间,防止死锁。当线程自行完后,使用lua脚本实现一个原子操作,操作具体内容为检测当前key对应的value和线程持有的value是否一致,若一致,则主动删除key,不 然操作失败(认为为恶意操作)
使用Redisson自带的分布式锁:
这个时候肯定就会有小伙伴问了,有没有什么包帮我们集成分布式锁呢?
有的,兄弟,有的
Redisson自带分布式锁的一个实现方案,我们可以基于Redisson来实现分布式锁
延迟双删
在高并发场景下存在这样一种情况:用户A更新原本资源的内容,原资源内容过期,用户B访问该资源三间事近乎同时发生
那么就会有一种很眉笔的排列顺序出现
- 用户B访问资源,发现资源过期,从数据库中拉取资源
- 从数据库中拉取到旧资源
- 用户A修改资源,数据库资源修改为新资源
- 将缓存更新为3中的新资源
- 将缓存更新为2中的旧资源
就在这一系列惊为天人的操作下,脏读发生了
为了解决这种问题,有人提出了延迟双删的解决方案:当资源发生修改操作时,先将原资源的缓存删除,然后再修改数据库,修改完数据库后等待1s的时间(认为1s旧资源拉取缓存的操作已经完成),然后再次删除一次缓存,这样就能保证数据库资源和Redis资源始终保持一致