缓存大杀器-redis
缓存模型
redis是我们最常使用的缓存组件,使用缓存能够大大降低用户访问并发量带来的服务器读写压力。
常见的缓存模型如下:
在客户端请求数据库之前先查询缓存,如果缓存数据存在,则直接返回;如果缓存数据不存在,再查询数据库,然后将数据写入redis缓存中,最后返回数据给客户端。
通过缓存中间件可以有效缓解数据库并发访问下产生的压力
缓存更新
缓存更新是redis为了节约内存而设计出来的一个东西,主要是因为内存数据宝贵,当我们向redis插入太多数据,此时就可能会导致缓存中的数据过多,所以redis会对部分数据进行更新,或者把他叫为淘汰更合适。
-
内存淘汰:redis自动进行,当redis内存达到咱们设定的max-memery的时候,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式)
-
超时剔除:当我们给redis设置了过期时间ttl之后,redis会将超时的数据进行删除,方便咱们继续使用缓存
-
主动更新:我们可以手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题
双写一致问题
由于我们的缓存的数据源来自于数据库,而数据库的数据是会发生变化的,因此,如果当数据库中数据发生变化,而缓存却没有同步,此时就会有一致性问题存在。
缓存数据与数据库数据不一致,使得用户使用的是缓存中的过时数据,而不是数据库中的实时数据,这就会给我们的程序带来灾难性的业务问题。
为了确保数据库和缓存数据的一致性,我们每次操作数据库后都需要操作缓存,那么这就带来了一系列问题?
-
操作缓存时是删除缓存还是更新缓存?
-
更新缓存:每次更新数据库都更新缓存,无效写操作较多
-
删除缓存:更新数据库时让缓存失效,查询时再更新缓存
-
-
如何保证缓存与数据库的操作的同时成功或失败?
-
单体系统,将缓存与数据库操作放在一个事务
-
分布式系统,利用TCC等分布式事务方案
-
-
先操作缓存还是先操作数据库?
-
先删除缓存,再操作数据库
-
先操作数据库,再删除缓存
-
在多线程并发执行的环境下,当你使用a方案先删除缓存,再来操作数据库可能会出现如下情况:
线程1想要更新数据,会先删除缓存中存在的数据,此时发生线程调度,切换到线程2。
线程2想要获取数据,先查询缓存中的数据,当缓存为命中时(缓存中数据被线程1删除),线程2会接着去查询数据库,并将数据库中还未发生修改的旧数据写入缓存(缓存中依然是数据库中的旧数据),并返回给线程2。
此时又发生线程调度,将cpu交由线程1,线程1会接着执行上次未完成的任务,进行数据库更新操作,使得数据库中存储新的数据。
经过我们上面的分析,我们发现缓存与数据库中的出现了不一致的情况。
当前b方案先操作数据库,再来删除缓存在多线程并发访问的条件下也有可能出现问题:
线程1查询缓存时,缓存正好失效(缓存未命中),此时线程1会去查询数据库数据,当准备将查询的数据库数据写入缓存时恰好发生线程调度,切换到线程2。
线程2正好更新数据库数据,并执行了删除缓存操作。
此时又发生线程调度,将cpu交由线程1,线程1会接着执行上次未完成的任务,将上次线程1查询到的数据库数据写入缓存。
也就是说,无论是a方案,还是b方案都有可能存在数据库数据和缓存数据的不一致性,这时候我们可以通过给缓存数据添加过期时间来确保数据库数据和缓存数据的最终一致性(双写最终一致)。
尽管上述两种方案都可能存在短暂的数据不一致情况,但是我们还是推荐使用b方案,先操作数据库再来操作缓存。
这是因为相比于方案a而言,方案b是在极端场景下才会出现一致性问题;方案b需要同时满足缓存恰好失效,线程切换,和写入缓存前被打断这三个条件才会出现一致性问题。(更新数据库的耗时(如MySQL事务提交)远长于删除缓存的耗时(如Redis的
DEL
命令,约1-10微秒),这也就是说删除缓存的操作几乎不会被其他线程打断)
缓存穿透问题
缓存穿透 :缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库,给数据库造成大量压力。
常见的解决方案有两种:
缓存雪崩问题
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
缓存击穿问题
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案有两种:
- 通过互斥锁限制访问实现缓存重建,从而避免数据库的访问压力过大,但是这种互斥会导致查询其他线程的阻塞,从而影响性能。
- 热点key问题是由过期时间导致的,那么我们可以不真正设置过期时间,而是通过逻辑过期来判断key的有效与否的手段来解决问题。
采用逻辑过期方案时,当我们获取互斥锁成功后,后续缓存重建的逻辑是在新的线程中进行,然后在原线程中直接返回过期数据;当获取互斥锁失败后,直接返回过期数据。通过这种异步的构建提高程序的效率,缺点在于缓存重建完成前,返回的都是脏数据。