缓存一定要用redis吗
「缓存」,写程序的肯定都用过。
使用缓存的目的只有2个:提升速度、减轻数据库压力。
使用缓存虽然也很简单的,但还是有很多细节可以推敲。
经典的缓存更新策略
我就先摆一些经典的缓存模式,其实我们有时候都不知道它是一个模式的时候,都已经在用了。
一般有以下几种:
- Cache Aside
- Read/Write Through
- Write Behind
名字看上去很专业,但其实原理都比较简单,大家有时间可以去网上搜一下,我很多也是搜回来的,下面我只挑里面重点讲扼要。
Cache Aside
上面就是整个更新缓存的流程图,重点我们可以看下面的图更新缓存的方式:
- 先更新数据库
- 再把缓存的数据失效
缓存失效了后,等下次Application再请求数据的时候,就会回到第一张图,如此循环。
其实这个就是我们经常使用的更新缓存逻辑,但这里存在一个高并发情况下的问题。
- 缓存初始化为空,A请求数据,就去数据库查询到了数据,并准备放到缓存。即执行了上图的1、2,3还没执行。
- 此时B请求并发,更新数据库,并使缓存为空。也就是下图的1、2。
- 最后A请求执行步骤3,设置缓存并返回结果。这时候其实缓存里面的数据是「脏数据」,因为B的更新并没有读到缓存里。
然而高并发情况触发条件比较苛刻,所以如果对缓存再加上一个适当的TTL(Time To Live,过期时间),缓存数据的准确性要求不高,那问题就不大。
Read/Write Through
Read/Write Through 更新逻辑如下所示
咋看其实跟上面Cache Aside没什么区别,但架构上多了一个Cache的服务,Cache服务层充当写入、更新、读取缓存的服务。
整体数据流就是:应用 > Cache 服务 > 数据库。所以跟Cache Aside最大的区别在于:
- Cache Aside是直接应用跟数据库打交道,缓存的更新逻辑有应用自己完成。
- Read/Write Through 在应用和数据库之间多了一个“Cache 服务”,从而应用和数据库解耦。
- 解耦的好处在于,应用再也不需要关注缓存的更新逻辑,每次读取、更新数据都是跟Cache 服务交互;Cache 服务专注于缓存的更新以及数据库的同步即可。
Write Behind
Write Behind 更新逻辑如下所示
截取《缓存更新的套路》的一段话解释
Write Back套路,一句说就是,在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的I/O操作飞快无比(因为直接操作内存嘛 ),因为异步,write back还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的。
但是,其带来的问题是,数据不是强一致性的,而且可能会丢失,但软件设计没有完美,都是权衡取舍。
另外,Write Back实现逻辑比较复杂,因为他需要track有哪数据是被更新了的,需要刷到持久层上。操作系统的write back会在仅当这个cache需要失效的时候,才会被真正持久起来,比如,内存不够了,或是进程退出了等情况,这又叫lazy write。
如果中间件服务挂掉呢
其实可以看出,上面几个缓存使用策略,逻辑都是 Cache Aside 基础上强化改造。那我们回到 Cache Aside 的逻辑看,除了上面说的 “高并发” 问题,其实还有其他问题。
回忆一下,在Cache Aside的模式中,我们在更新缓存:
- 是先更新完数据库。
- 再直接把缓存失效;
- 待下次读取缓存数据的时候,再从数据库加载新数据;
- 加载到缓存中,再返回数据。
一般来说,程序部署标准都是用各种中间件,比如一个程序用了缓存,就用redis;数据库,就用mysql。那么一个完整的程序,就有程序应用实例、redis、数据库,当三个服务都正常时,就会提供正常响应,但如果redis或者数据库访问异常呢。
redis 异常
如果redis异常的话,一般程序会有2种表现:
- 程序没cache到redis异常,或者没有做处理,程序不返回数据直接报错。redis 属于数据缓存,缓存崩溃导致整体程序不可用,对于某些高可用场景来说未必是可以接受的;没了缓存性能、效率、吞吐量会降低,可以限流提供有限服务,但不能完全不提供。
- 程序cache到redis异常,并且跳过了redis,每一次的抓取数据都要访问DB,这个比上面的表现要好,因为起码对外仍然提供了服务;但如果没有有效的限流,高并发下就会造成「缓存击穿」,大量查询可能直接把数据库搞挂。
数据库异常
数据库异常本身就是比较大的故障,整体程序应该大部分都不能用。
但有时偶尔的网络抖动导致访问失败也是有可能的,那如果刚好缓存被设置为失效,又请求数据库失败的这种情况下,就会返回失败。
这种情况下,如果redis缓存还有旧数据,就可以暂时返回给用户。
真的要用中间件吗
理论上来说,redis和数据库作为中间件而言,一般都是具有较高可用性,比如腾讯云上的redis就达到99.5%。
但中间件终归是一种共享服务,除了本身的稳定性,还需要依赖网络的稳定性,相互请求也需要带宽。
回到「缓存」这个主题上来,要增加使用缓存的稳定性,还有一个可以考虑的点,就是是否真的需要使用redis。因为少一个外部依赖点,程序性能相对稳定性可控。
假如程序里面的缓存符合以下特性:
- 缓存数据不需要锁,多个实例不需要共享、统计。
- 缓存数据的量是可以统计到并预计到内存的使用量,要避免本地内存使用影响程序本身的服务。
- 缓存不需要考虑持久化,进程重启后可以随时重新获取。
符合以上特性,其实建议还是使用本地缓存,缺点就是增加了代码复杂度,要考虑过期、线程安全等;但对应也是有措施,可以用一些框架来缓解代码复杂度的问题。
本地缓存另外一个问题就是缓存更新,因为在分布式系统下,同一个应用的缓存就散落到各个实例上,如何主动统一刷新多态进程实例的缓存也是一个问题。