1.什么是缓存?
缓存是数据进行交换的缓冲区,它的读写速度比较快
2.缓存的优缺点?
优点:
1)降低后端负载
不需要从数据库查找,进行磁盘IO操作,可以直接从缓存获取数据
2)提高读写速率,降低反应时间
缓存的读写性能比较高,在用户两比较大的时候,优势很很明显
缺点:
1)数据一致性成本
使用缓存会造成数据的不一致(数据库的数据更新,但是缓存还未更新)
2)代码维护成本
缓存可能会引起许多诸如缓存击穿、缓存穿透、缓存雪崩的问题,代码维护成本较高
3)运维成本
为了解决缓存雪崩等问题需要搭建集群,运维成本高
3.添加商户缓存
<redis缓存模型>
<添加商户缓存流程>
4.缓存更新策略
1)内存淘汰
内存不足的时候,删除一些缓存中的部分数据
一致性差,有可能有些数据一直不被删除,维护成本最低
2)超时剔除
缓存中的数据带有TTL(Time to live),生存时间到了就被删除
一致性一般,在TTL设置时间如果长了,在该时间段内,数据库发生更新,也会出现数据不一致,维护成本低
3)主动更新
当数据库发生更新时,也同时更新缓存中的数据
一致性好(不是完全保证一致,程序可能发生运行意外),维护成本高(代码维护)
如何使用?
对于一致性要求比较低的业务,使用内存淘汰机制。比如店铺类型的查询缓存(店铺类型一般更新比较少)
对于一致性要求比较高的业务,使用主动更新+超时剔除(万一程序发生异常,主动更新失败可以兜底)。比如店铺详情查询的缓存
5.主动更新策略深入
<操作缓存与数据库需要考虑的三个问题>
1)删除缓存还是更新缓存?
如果每次更新数据库就更新缓存,假设更新了100次,但是这期间没有任何查询操作,那么实际上这期间对缓存的更新是无效的
正确的做法是每次更新数据库就删除缓存,后面有查询操作的时候才更新缓存
2)如何保证缓存与数据库的操作同时成功同时失败?
a.在单体系统的情况下,对缓存与数据库的操作是在一个项目中,或者一个方法中,那么将他们放在一个事务中即可
b.但如果是在分布式系统下,对缓存和数据库的操作可能是两个不同的服务,需要用到分布式事务方案如TCC等
3)先操作数据库还是缓存?->线程安全问题
a.先操作缓存再操作数据库
问题:一个线程在操作缓存与操作数据库期间,另一个线程恰好过来查询缓存并进行写入操作,这样一来,缓存保存的是旧数据,而数据库是最新的记录,从而出现数据不一致
发生概率:因为更新数据库操作是比较费时的,所以很有可能在删除缓存与更新数据库期间发生这种线程不安全情况
b.先操作数据库再操作缓存
问题:A线程在查询缓存的时候,刚好缓存失效,而在查询数据库与写入缓存期间,B线程过来进行更新数据库与删除缓存的操作,A线程再进行写入缓存的操作,这样一来,缓存中保存的是旧数据,而数据库保存最新数据,从而出现数据不一致
发生概率:一方面,A线程进行查询缓存操作时缓存得恰好失效,另一方面,因为写入缓存的速度很快,在如此快的时间内完成更新数据库(耗时)、删除缓存的操作,发生的概率是不高的
<综上,缓存更新的最佳实践方案>
1)低一致性需求:使用redis自带的内存淘汰机制
2)高一致性需求:使用主动更新策略并以超时剔除策略作为兜底
3)读写操作
读操作:
缓存命中直接返回
缓存未命中查询数据库并写入缓存,设定超时时间
写操作:
先更新数据库再删除缓存(删除缓存速度快,保证了操作数据库与删除缓存期间不会有线程穿插)
要确保数据库与缓存操作的原子性
6.缓存引发的三个问题
1)缓存穿透
用户请求的数据在缓存与数据库中都不存在,这样缓存永远不会生效,请求都会打到数据库,给数据库造成较大压力
解决方案:
a.缓存空对象
当请求的数据在缓存与数据库都查不到的时候,在缓存中保存一个对应的空对象,这样后面查询同样的数据时缓存就会生效,不会打到数据库
优点:实现简单,维护方便
缺点:额外的内存消耗
可能造成短期的不一致(如果缓存保存了空对象后,数据库更新了这个上次查询不到的数据,就会出现不一致)
如何解决这里出现的短期不一致问题?
可以为缓存的空对象设置一个TTL,可是在TTL期间还是会出现不一致?
->数据库数据插入的时候更新缓存
b.布隆过滤
优点:内存占用较少
缺点:实现复杂、存在误判的可能(所以我们一般采用“缓存空对象”的方法)
<解决商铺查询的缓存穿透问题的基本流程>
c.增强id的复杂度,避免被猜测到id规律,同时做好数据的基础格式的校验
d.加强用户权限校验
e.做好热点参数的限流
2)缓存雪崩
同一个时段内,大量缓存的键值对同时失效或者redis直接宕机了,导致大量请求到达数据库,带来巨大压力
解决方案 :
a.给不同的key的TTL添加随机值
b.利用redis集群提高服务的可用性
c.给缓存业务添加降级限流策略
d.给业务添加多级缓存
3)缓存击穿(热点Key问题)
一个被高并发访问或者缓存业务重建复杂的key突然失效,这时候在较长的数据库查询与缓存重建期间,无数的请求都会打到数据库,带来巨大压力
解决方案:
a.互斥锁
一个线程发现缓存未命中后,获得互斥锁,查询数据库并进行缓存重建,缓存重建期间,如果有其它线程查询缓存发现未命中,这个时候因为没有拿到互斥锁,所以不会查询数据库与重建缓存,而是休眠一会再查询缓存,直到缓存命中
(可用性低,但一致性好。线程需要等待,但是查到的是最新的数据,与数据库一致)
b.逻辑过期
维护一个逻辑过期时间(不是TTL)
一个线程查询缓存发现逻辑过期时间已过期,获得互斥锁,接着会创建新的线程进行数据库查询与缓存重建以及逻辑过期时间的更新,在释放互斥锁之前,其它线程查询缓存未命中时,获取互斥锁失败后会直接返回过期数据。
(可用性高,但一致性较差。线程不需要等待就能查到,但是查到的可能不是最新的数据,与数据库可能不一致)
<利用互斥锁解决商铺缓存击穿问题的基本流程>
如何实现互斥效果?-> 利用redis中的setnx命令
setnx命令与普通的set命令不同,只有执行命令的key不存在的时候才可以设置成功,否则设置失败,利用setnx命令的这一特性,我们可以实现互斥效果
<基于逻辑过期解决商铺缓存击穿问题的基本流程>
问题:逻辑过期时间这个属性并不在Shop类中,更改Shop类显然不是一个明智的方案,那应该怎么办?
在utils包中加入一个RedisData类,该类具备了逻辑过期时间属性,也包含一个Object data对象(也就是Shop对象)