目录
一、更新缓存
此处的更新缓存是指,Redis中原有的缓存被删除,通过了解这个,就可以知道,Redis中缓存的变化情况,有了这个概念,就可以比较好地使用Redis的缓存技术。
更新方式:
- 内存淘汰:到达设置的max-memery,触发淘汰机制,可以自己设置淘汰策略
- 超时剔除:设置过期时间TTL
- 主动更新:手动删除,通常用于解决缓存与数据库不一致的问题
1、有哪些内存淘汰策略?
独立的两种是noeviction和ttl,其余可以只记住三种:lru、lfu和random,可以分为六种,是因为剔除的时候还考虑是否配置了TTL。
- noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键(默认)。
- allkeys-lru:加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键。
- volatile-lru:加入键的时候,如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键。
- allkeys-random:加入键的时候如果过限,从所有key随机删除。
- volatile-random:加入键的时候如果过限,从过期键的集合中随机驱逐。
- volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键。
- volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键。
- allkeys-lfu:从所有键中驱逐使用频率最少的键。
2、Redis是如何做到超时剔除的?
一个键过期了,Redis是如何知道?
首先,我们暂时不考虑Redis是如何实现的,我们先来考虑,超时删除策略可以由哪些?
- 定时删除,超时立即删除(主动删除,并且是立即)
- 定期删除,每隔一段时间就去删除一部分过期的Key(主动删除,但是不是立即,也可以再细分,定期删除全部还是定期删除部分,如果定义部分,就靠算法与使用场景了)
- 惰性删除,获取的时候再去判断是否过期,判断过期再删除(被动删除,获取的时候才删除)
然后,我们在来看看,我们拥有哪些资源?CPU资源和内存资源。
这样,我们可以得出下面结论:
- 主动删除会消耗更多的CPU资源,但是可以更好利用内存资源,而其中定时删除需要比定期删除更多的CPU资源
- 被动删除会消耗更多的内存资源,但是不会额外消耗太多的CPU资源
对比于定时删除与惰性删除,定期删除无疑是一种”中庸之举“。
定时删除的实现,如果设置对某个时刻的过期键的删除的监听器,那么删除很有可能是O(n)
,并且经常需要占用比较大的CPU资源,在内存压力不大的时候,花费太多CPU资源在删除上,对服务器的响应时间是很不友好的,虽然Redis的性能上限是主要是内存与网络IO,但是不一定说CPU就没有影响。
惰性删除经常浪费了很多内存,很多过期的键依然存在内存中,造成了浪费。
现在可以说出Redis的使用的方式:定期删除+惰性删除 。
其中这个定期删除,使用的是,定期抽内存中抽取一部分出来进行判断是否过期然后删除,怎么抽取,一次抽取多少,抽取的频率,这些都需要Redis在内存资源与CPU资源之间进行取舍。而由于定期还会出现部分漏掉的过期Key,所以惰性删除为用户使用的最后一环提供了保障,这样用户就不会获取到已经过期的Key。
3、主动更新的时候,数据库与Redis缓存不一致怎么办?
你可能想说,同步更新不就好了?但是这个同步可没有原子性的保证!里面显然大有问题。
有一篇文章写得很好,这里就做一个推荐了,文章分析了各种可能会出现的问题,并给出解决方式 --> 如何保证数据库和缓存双写一致性? 。
二、有效的缓存
我们知道,在并发量大的情况下,普通查询关系数据库并不能满足我们对于并发量的需求,所以需要缓存来帮助我们快速获取到相应的数据,提高并发量,但是在一些情况下,未必能真的获取相应的数据,那么缓存也就失效了。
常见问题:
- 缓存穿透:大量请求的数据不在Redis中,只能被动查询数据库,导致数据库压力增大(本来就没有相应的Key)
- 缓存雪崩:key同时大量失效,或者redis服务器宕机,导致请求大量到达数据库(本来就有相应的Key,但是大量Key失效)
- 缓存击穿:热点key问题,高并发访问的key失效了,而重建业务复杂(本来就有相应的Key,但是少量key失效,而这些Key的并发访问量非常高)
1、缓存穿透
这个问题导致的后果是,大量查询关系数据库,导致并发量大大下降,并且如果是被攻击了,所发送的请求都是无效的,也就是说无效请求使得我们的并发量大大下降,这是不被允许发生的。另外,如果是有效请求,缓存也被穿透了,这种情况也是有的,比较常见于服务刚启动时,所以这种情况下,我们需要缓存预热,提前往缓存里面加载数据,等到用户访问的时候就不需要再去查询关系数据库。
解决方式一:缓存空对象
虽然关系数据库中没有该数据,但是我们可以尝试缓存一个空对象,使得下一次对应的缓存进来,我们也暂时可以不用查询关系数据库,如果关系数据库更新了,那么这个Key将会被删除,下一次就可以缓存真正的对象,也就不用担心影响用户的实际使用。
- 优点:实现简单。
- 缺点:如果真的是被攻击,从而发起大量无效请求,这些无效请求未必是有重复的,所以空对象未必能有效发挥作用。
解决方式二:布隆过滤器
假设我们可以通过不查询关系数据库就能知道是否存在该数据,那么这个问题将会迎刃而解。怎么样才能做到这一点呢?
如果关系数据库中所有的数据都被分别计算出一串“密文”,那么如果我们存储了这个“密文”,当有请求进来的时候,我们只需要判断请求对应的密文是否存在即可,“密文”就起到了一个中间映射的关系:请求—密文—真实数据,需要注意的是,这个成立的前提是,计算请求得出来的“密文”与计算数据得出来的密文是一致的。
但是“密文”也需要存储,也需要去查找,真的能比查询真实数据会更快吗?如果密文存储在关系数据库,显然并不会,如果存储在缓存数据库,将会花费大量的内存资源。
那么,有没有一种,花费的内存少,查询速度快的方式呢?
回看我们上方的讲述,“密文”是如何得出的?是被计算出来的,用来做映射的。可以达到这个要求,我们第一个会想到什么?没错,就是Hash映射!
下面举个应用例子。
我们使用一个bit数组来存储密文。初始状态如下,起始位的下标默认为0:
| 0 0 0 0 | 0 0 0 0 | 0 0 0 0 | 0 0 0 0 | 0 0 0 0 | 0 0 0 0 | 0 0 0 0 |
现在我们有创建第一条数据——数据A,我们对它做Hash运算,比如说对“表名+ID”做一个Hash运算,得出一个数字为5,那么可以得出以下的bit数组:
| 0 0 0 0 | 1 0 0 0 | 0 0 0 0 | 0 0 0 0 | 0 0 0 0 | 0 0 0 0 | 0 0 0 0 |
此时有一个请求进来,需要查询某个表的某个ID的数据,我们可以对“表名+ID”做一个Hash运算,假设此时得出一个数字6,而我们发现bit数组中对应下标的是0,这说明没有数据库中根本没有这个数据!我们就可以直接把这个请求打回,就不用再查询数据库。
当然,你会说,Hash冲突问题,假设一个无效的请求也能通过Hash运算得出了一个5,那怎么办?
这个问题是无法解决的,但是可以缓解,使用多个Hash算法算出多个值,比如说分别算出了2,3,4,那么当一个请求进来,分别算出2,4,7,就如果下标为7的位置是0,说明这个请求对应的数据是不存在的,通过将bit数组的长度增大与使用多个Hash算法,能减少Hash冲突的发生。
事实上,这些漏网之鱼的请求是可以被忍受的,尤其是考虑了其占据的内存大小与查找的速度。
我们先看看1MB的bit数组有多长:
1MB = 1024 KB = 1024 * 1024 B = 1024 * 1024 * 8 bit = 8,388,608 bit
只需要1MB的内存,就可以轻松排除相当多的无效请求了。而查找速度呢?我们知道这个是数组,数组的查找的时间复杂度是O(1),也就是说,我们只需要付出少量的代价就可以过滤相当多的无效请求,从这个角度来思考,这个解决方式是最常用。
2、缓存雪崩与缓存击穿
我把这两个放在一起讲,是因为,这两个问题发生的原因是类似的,就是Key失效了,想要解决这个问题,我们首先需要知道,为什么出现了这个问题,然后思考如何达成两个目的:不让Key在同一时间内大量失效以及不让热点Key失效,前者是解决缓存雪崩达成的目标,后者是解决缓存击穿达成的目标。
出现的原因
回看我们上面所说的更新缓存的内容,可以发现,缓存失效无非就四个原因,三个是因为缓存更新,一个是因为服务器宕机。
四个原因:
- 缓存的数据太多,部分Key被淘汰了
- 大量Key在同一时间超时
- 被使用者主动删除了
- 服务器宕机
分析出了问题,下面就可以开始解决问题了。
解决一:更换淘汰策略
回忆我们上面讲的八种缓存策略。
对于不让Key在同一时间内大量失效这个目的:
是无法通过缓存策略达成的,因为到达了缓存策略,就必然会淘汰部分Key,无论使用什么策略,都是一样的结果。
对于不让热点Key失效这个目的:
我们需要先定义什么是热点Key,最近使用过的Key?最近使用次数最多的Key?
对于前者,我们可以使用LRU策略,保存下来的就是最近使用过的Key,对于后者,我们可以使用LFU策略,保存下来的就是最近使用次数最多的Key。
另一方面,我们知道有些策略是可以设置是否只在配置了过期时间的Key中进行淘汰,所以,如果热点Key,不配置过期时间,策略也只选择了在配置了过期时间的Key中进行淘汰,那么就可以很大程度上保证热点Key不会失效。
解决二:配置随机过期时间或者不配置过期时间
前者可以解决缓存雪崩问题,后者可以解决缓存击穿问题。
缓存雪崩:
害怕大量Key同一时间过期?
那就随机过期时间,大大降低同一时间过期的可能性。
缓存击穿:
害怕热点Key过期?
那我干脆不设置过期时间,那样就不会过期了。
关于设置过期时间的特别的操作:
只能通过Redis设置过期时间?我们能不能自己设置过期时间,由自己管理是否过期?
当然可以!这个就是逻辑过期的操作。
我们在Key-Value中的Value中存储着过期时间,等到获取的时候,我们就可以自己判断是否要删除这个Key去使它过期,假设我们的业务需要动态判断Key是否应该过期,这种操作就相当有用。
解决三、建立良好的工作流
防止误删。
解决四、使用集群
害怕服务器宕机?那就使用Redis集群,多台服务器共同保障服务进行。
万一,Key真的失效了呢?
万一真的失效了,我们要做的是什么?当然是重构啦!
现在进来一个A请求,发现D Key并不存在,所以需要重构D Key。
在重构的过程中,又进来一个B请求,也发现D Key不存在,所以也去重构D Key。
这个时候就可以发现,为什么一个Key,我需要重构两次?当然不需要啦!只需要一次就好,我们很容易就可以想到使用锁来处理这个问题。
在分布式应用中,请求A与请求B可能并不在一台主机上,所以我们需要分布式锁,这样就可以完成这个任务:所有请求进来只重构一次,而不是一个请求重构一次。
不过,也需要注意,经常被访问的Key才更需要这个操作,很长一段时间内只访问一两次的Key,多少是有些多此一举的。