一、缓存穿透,缓存击穿,缓存雪崩
1、缓存穿透
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的
并且出于容错考虑,从存储层查不到数据则不写入缓存,这将导致这个
不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大的时候,可能DB就
挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
解决方案
有很多种方法可以有效的解决缓存穿透的问题,最常见的就是布隆过滤器
将所有可能存在的数据hash到一个足够大的bitmap位示图中,一个一定不存在的数据会
被这个bitmap过滤掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单
粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数据不存在,还是系统故障)
仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过5min
2、缓存雪崩
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求
全部发到DB,DB瞬时压力过重雪崩。
解决方案
缓存失效时的雪崩效应对底层系统的冲击非常可怕,大多数系统设计者考虑用加锁
或者队列的方式保证缓存的单线程(进程)写,从而避免失效时大量的并发请求落到
底层存储系统上,这里分享一个简单方案:将缓存失效时间分散开,比如我们可以在原有失效
时间基础上增加一个随机值,比如1-5min随机,这样每一个缓存的过期时间的重复率就会降低
就很难引发集体失效的事件。
3、缓存击穿
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发的访问,是一种
非常热点数据,这个时候,需要考虑一个问题:缓存被击穿的问题,这个和缓存雪崩的区别
在于,缓存击穿针对某一key缓存,缓存雪崩是很多key
缓存在某个时间点过期的时候,恰好在这时间点对这个key有大量的并发请求过来,这些请求
发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把
后端DB压垮。
解决方案
1、使用互斥锁mutex key
常用的做法是:使用mutex,就是在缓存失效的时候,判断拿出来的值为空,不是立即去load db
而是先使用缓存工具的某些带成功操作返回值得操作,比如redis的setnx
去set一个mutex key。当操作返回成功时,在进行load db的操作,并回设缓存,否则,就重试
整个get缓存的方法。
setnx==set if not exists的缩写。也就是只有不存在的时候才设置,可以利用它来实现缓存击穿
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public String get(String key){
String value=redis.get(key);
if(value==null){
//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if(redis.setnx(key_mutex,1,3*60)==1){//代表设置成功
vlaue=db.get(key);
redis.set(key,value,expire_secs);
redls.del(key_mutex);
}else{//这个时候代表同时候的其他线程已经load db并回设到缓存了,这个时候,重试获取缓存值就行。
sleep(50);
get(key);//重试
}
}else{
return vlaue;
}
}
4、为什么要用redis缓存
Redis就是一个数据库,不过传统数据库不同是Redis数据是存在内存中。
Redis被广泛用于缓存方向,Redis也经常用来做分布式锁。
5、为什么要用Redis而不用map/guava做缓存?
缓存分本地缓存和分布式缓存。Java中使用自带的map或者guava实现的是本地的缓存,轻量快速,生命周期随着JVM的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
使用Redis或者memcached之类是分布式缓存,在多实例情况下,各实例共用一份缓存数据,缓存具有一致性。
缺点:Redis需要保持服务的高可用,整个程序架构比较复杂。
6、Redis和Memcached的区别
Redis支持更丰富的数据类型,也就意味着支持更复杂的应用场景
不仅仅是支持简单的key、value数据,同时还提供list,set,zset,hash等数据的存储。memached支持简单的数据类型,string
Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用,而Memecached把数据全部存在内存之中,重启就没了。
集群模式:Memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据。大师Redis是原生支持cluster模式的
Memcache是多线程,非阻塞IO复用的网络模型
Redis使用单线程的多路IO复用模型
7、Redis常见数据结构以及使用场景分析
String
常用命令:set,get,decr,incr,mget
String数据结构是简单的key-value,value可以是String,也可以是数字。
常规key-value缓存应用,常规计数,微博数,粉丝数。
Hash
常用命令:hget,hset,hgetall
hash是一个string类型的field和value的映射表
hash特别适合用来存储对象。可以用hash数据结构来存储用户信息,商品信息
1
2
3
4
5
6
7key=javatest12343
value={
'id':1,
'name':'vitor',
'age':12,
'locatoin':'shanghai'
}List
常用命令:lpush,rpush,lpop,lrange
list就是链表,Redis list的应用场景非常多,比如微博的关注列表,粉丝列表,消息列表等功能都是用Redis的list结构来实现
Redis list的实现是一个双向链表,既可以支持反向查找和遍历,但是带了额外的内存开销。
还可以利用list实现简单的高性能分页。
Set
常用命令:sadd,spop,smembers,sunion等
set对外提供的功能与list类似是一个列表功能。只不过set可以自动去重
Sorted Set
常用命令:zadd,zrange,zrem,zcard
和set相比,sorted set增加了一个权重参数score,使得集合的元素能够按照score进行排序。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息。
8、Redis设置过期时间
Redis中有个设置时间过期的功能,对存储在Redis数据库中的值可以设置一个过期时间。作为一个缓存数据库,这是非常实用的,如我们一般项目中的token或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统的数据库处理方式,一般都是自己判断过期,这样会严重影响项目性能。
所以,我们set key的时候,都可以给一个expire time过期时间,通过过期时间我们可以指定这个key可以存活的事件。如果假设你设置了一批key只能存活1个小时,那么接下来1个小时后,redis怎么删除key?
1、定期删除+惰性删除
定期删除:redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查是否过期,如果过期就删除。
这里是随机抽取的,因为随机抽取,假如redis存了几十万的key,每隔100ms就遍历所有的设置过期时间的key话,就会给cpu带来很大复杂
惰性删除:定期删除可能会导致很多过期key到了时间并没有被删除掉。就有了惰性删除。假如你的过期key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个key,才会被redis给删除掉。这就是所谓的惰性删除。
但是仅仅通过设置过期时间还是有问题的,如果定期删除漏掉了很多过期的key,然后也没有及时去查,也就没有惰性删除。如果大量过期key堆积在内存里,导致redis内存耗尽,redis内存的淘汰机制就出现了。
2、Redis内存淘汰机制
MySQL里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据?
Redis.conf中有相关注释
- Redis提供6中数据淘汰策略:
- volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集中任意选择数据进行淘汰
- allkeys-lru:当内存不足以容纳新写入数据的时候,在key空间中,移除最近最少使用的key(这是最常用的)
- allkey-radom:从数据集(server.db[i].dict)中任意选择数据淘汰
- volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
- allkeys-lfu:当内存不足容纳新写入数据,在key空间,移除最不经常使用的key
注:redis设置过期时间以及内存淘汰机制
3、Redis持久化机制(怎么保证Redis挂掉之后再重启数据可以进行恢复)
很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器,机器故障之后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置
Redis不同于Memcached重点是Redis可以持久化,支持两种不同持久化操作。
快照(Snapshotting RDB):
Redis可以通过创建快照来获取存储在内存里面的数据在某个时间点的副本。Redis创建快照后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提供Redis性能),还可以将快照留在原地以便重启服务器的时候使用。
1
2
3
4
5
6在900s之后,如果至少有1个key发生变化,redis就会自动触发BGSAVE命令创建快照
save 900 1
在300s之后,如果至少有10个key发生变化,redis就会自动触发BGSAVE命令
save 300 10
在60s之后如果有1w个key发生变化,redis就会自动触发BGSAVE命令
save 60 10000
追加文件(append-only file,AOF)
与快照持久化相比,AOF持久化的实时性更好,因此也成为主流的持久化方案。
默认情况下redis没有开启AOF方式持久化
1
appendonly yes
开启AOF持久化后每执行一条会更改redis中的数据的命令,redis就会将该命令写入硬盘中的AOF文件。AOF文件的保存位置和RDB文件位置相同都是通过dir参数设置
默认文件名为appendonly.aof
1
2
3
4
5
6每次有数据修改发生都会写入aof,这样会严重降低redis的速度
appendfsync always
每秒钟同步一次,显式的将多个写命令同步到硬盘
appendfsync everysec
让操作系统决定何时同步
appendfsync no为了兼顾数据和写入性能,用户可以考虑appendfsync everysec选项,让redis每秒同步一次AOF文件,redis性能几乎没收到任何影响。即使系统崩溃,用户也最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作,redis还会放慢自己的速度来适应硬盘的最大写入速度。
Redis4.0对于持久化机制的优化
4.0开始支持RDB和AOF的混合持久化(默认关闭,可以通过配置项aof-use-rdb-preamble开启)
混合持久化开启,AOF重写的时候就会直接把RDB的内容写到AOF文件开头。这样的好处可以结合RDB和AOF的优点,可以快速加载同时避免丢失过多的数据。
缺点是:AOF里面的RDB部分也不是AOF的压缩格式,可读性差。
AOF重写
AOF重写可以产生一个新的AOF文件,这个新的文件和原有的AOF文件所保存的数据库状态一样,但是体积更小
AOF重写是一个有歧义的名字,功能是通过读取数据库中的键值来实现。程序无需对现有的AOF文件进行任何读入,分析重写,
4、Redis事务
redis通过multi,exec,watch等命令来实现事务transaction功能。
事务提供了一种将多个命令请求打包,然后一次性,按顺序执行多个命令的机制。
5、解决Redis的并发竞争key的问题
所谓redis的并发竞争key的问题也就是多个系统同时对一个key进行操作,但是最后执行的顺序和我们期望的顺序不同。也就导致了结果的不同
解决方案:
分布式锁zookeeper和redis都可以实现分布式锁。如果不存在redis的并发竞争key问题及,不要使用分布式锁,这样影响性能。
基于zookeeper临时有序结点可以实现的分布式锁
大致思想:每个客户端对某方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序结点。
判断是否获取锁的方式很简单,只需要判断有序结点中序号最小的一个。当释放所得时候,只要将这个瞬时结点删除就行。同时也避免服务挂了导致锁无法释放,从而产生死锁问题。
什么是RedLock
特性:
- 安全性:互斥访问,也就是永远只有一个client能拿到锁
- 避免死锁:最终client都可能拿到锁,不会出现死锁的情况,原本锁住某资源的client crash或者出现了网络分区
- 容错性:只要大部分redis结点存活就可以正常提供服务。
- 本文作者: Victor Dan
- 本文链接: https://anonymousdq.github.io/victor.github.io/2019/02/01/缓存穿透,缓存击穿,缓存雪崩/
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
