为什么要使用缓存
- 言简意赅:速度快,好用
- 缓存数据存储于代码中,而代码运行在内存中,内存的读写性能远高于磁盘,缓存可以大大降低用户访问并发量带来的服务器读写压力
- 实际开发中,企业的数据量,少则几十万,多则几千万,这么大的数据量,如果没有缓存来作为避震器系统是几乎撑不住的,所以企业会大量运用缓存技术
- 但是缓存也会增加代码复杂度和运营成本
- 缓存的作用
1.降低后端负载
2.提高读写效率,降低响应时间 - 缓存的成本
1.数据一致性成本
2.代码维护成本
3.运维成本(一般采用服务器集群,需要多加机器,机器就是钱)
在这里护主要说redis缓存
图解
先从Redis中查询,如果没有查到,再去MySQL中查询,同时查询完毕之后,将查询到的数据也存入Redis,这样当下一个用户来进行查询的时候,就可以直接从Redis中获取到数据
商户缓存流程
- 代码思路:如果Redis缓存里有数据,那么直接返回,如果缓存中没有,则去查询数据库,然后存入Redis
缓存更新策略
- 缓存更新是Redis为了节约内存而设计出来的一个东西,主要是因为内存数据宝贵,当我们想Redis插入太多数据,此时就可能会导致缓存中数据过多,所以Redis会对部分数据进行更新,或者把它成为淘汰更合适
- 内存淘汰:Redis自动进行,当Redis内存大道我们设定的max-memery时,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式)
- 超时剔除:当我们给Redis设置了过期时间TTL之后,Redis会将超时的数据进行删除,方便我们继续使用缓存
- 主动更新:我们可以手动调用方法把缓存删除掉,通常用于解决缓存和数据库不一致问题
数据库和缓存不一致解决方案
由于我们的缓存数据源来自数据库,而数据库的数据是会发生变化的,因此,如果当数据库中数据发生变化,而缓存却没有同步,此时就会有一致性问题存在,其后果是
- 用户使用缓存中的过时数据,就会产生类似多线程数据安全问题,从而影响业务,产品口碑等
那么如何解决这个问题呢?有如下三种方式
- Cache Aside Pattern 人工编码方式 :缓存调用者在更新完数据库之后再去更新缓存,也称之为双写方案 (主要方案)
- Read/Write Through Pattern:缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题。但是维护这样一个服务很复杂,市面上也不容易找到这样的一个现成的服务,开发成本高
- Write Behind Caching Pattern:调用者只操作缓存,其他线程去异步处理数据库,最终实现一致性。但是维护这样的一个异步的任务很复杂,需要实时监控缓存中的数据更新,其他线程去异步更新数据库也可能不太及时,而且缓存服务器如果宕机,那么缓存的数据也就丢失了
综上所述,在企业的实际应用中,还是方案一最可靠,但是方案一的调用者该如何处理呢?
三个问题:
1. 删除缓存还是更新缓存?
2. 如何保证缓存与数据库的操作同时成功或者失败?
3. 先操作缓存还是先操作数据库?
- 删除缓存还是更新缓存?
- 如果采用方案一,假设我们每次操作完数据库之后,都去更新一下缓存,但是如果中间并没有人查询数据,那么这个更新动作只有最后一次是有效的,中间的更新动作意义不大,所以我们可以把缓存直接删除,等到有人再次查询时,再将缓存中的数据加载出来
对比删除缓存与更新缓存
更新缓存:每次更新数据库都需要更新缓存,无效写操作较多
删除缓存:更新数据库时让缓存失效,再次查询时更新缓存
- 如何保证缓存与数据库的操作同时成功/同时失败?
- 单体系统:将缓存与数据库操作放在同一个事务
- 分布式系统:利用TCC等分布式事务方案
- 先操作缓存还是先操作数据库?
-
我们来仔细分析一下这两种方式的线程安全问题
-
先删除缓存,再操作数据库
删除缓存的操作很快,但是更新数据库的操作相对较慢,如果此时有一个线程2刚好进来查询缓存,由于我们刚刚才删除缓存,所以线程2需要查询数据库,并写入缓存,但是我们更新数据库的操作还未完成,所以线程2查询到的数据是脏数据,出现线程安全问题
-
先操作数据库,再删除缓存
线程1在查询缓存的时候,缓存TTL刚好失效,需要查询数据库并写入缓存,这个操作耗时相对较短(相比较于上图来说),但是就在这么短的时间内,线程2进来了,更新数据库,删除缓存,但是线程1虽然查询完了数据(更新前的旧数据),但是还没来得及写入缓存,所以线程2的更新数据库与删除缓存,并没有影响到线程1的查询旧数据,写入缓存,造成线程安全问题
虽然这二者都存在线程安全问题,但是相对来说,后者出现线程安全问题的概率相对较低,所以我们最终采用后者先操作数据库,再删除缓存的方案