Redis缓存使用问题
1数据一致性
分析一下几种方案:
1:先更新缓存,再更新数据库
2:先更新数据库,在更新缓存
3:先删除缓存,后更新数据库
4:想更新数据库,后删除缓存
1.1 新增数据:
如果是新增数据,数据会直接写入数据库中,不用对缓存做任何操作,此时,缓存中本身没有新增的数据,而数据库的值是最新的,此时,数据库和缓存的数据是一致的。
1.2 更新数据
1.2.1 先更新缓存,再更新数据库
这个方案一般不考虑。原因是更新缓存成功,更新数据库出现异常,导致缓存数据与数据库中数据完全不一致,而且我们很难察觉,因为缓存中的数据一直都存在。
1.2.2 先更新数据库,在更新缓存
这个方案一般不考虑。原因是更新数据库成功,更新缓存出现异常,导致缓存数据与数据库中数据完全不一致。同时还有一下几个问题:
1)并发问题
同时请求A和B进行更新操作,那么会出现
- 线程A更新数据库
- 线程B更新数据库
- 线程B更新缓存
- 线程A更新缓存
这就出现了问题,线程A应该比线程B更新缓存早才对,但是因为网络问题,B却比A更新了缓存,这就导致了脏数据,因此不考虑
2)业务场景问题
如果是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致,数据压根没有督导,缓存就会被频繁的更新,浪费性能。
1.3 删除缓存类
1.3.3 先删除缓存,后更新DB
该方案也会出问题,具体出现的原因如下。
1、此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)
2、请求 A 会先删除 Redis 中的数据,然后去数据库进行更新操作;
3、此时请求 B 看到 Redis 中的数据时空的,会去数据库中查询该值,补录到 Redis 中;
4、但是此时请求 A 并没有更新成功,或者事务还未提交,请求B去数据库查询得到旧值;
5、那么这时候就会产生数据库和 Redis 数据不一致的问题。
如何解决呢?其实最简单的解决办法就是延时双删的策略。就是
(1)先淘汰缓存
(2)再写数据库
(3)休眠1秒,再次淘汰缓存
redis.delete(key)
db.update(key)
Thread.sleep(N)
redis.delete(key)
这么做可以将N秒内所造成的缓存脏数据再次删除。
那么该休眠几秒如何确定?
针对上面情形,应该评估自己的项目读数据业务逻辑的耗时,然后写数据的休眠时间则在读数据业务逻辑耗时基础上加几百毫秒即可。
但是上述的保证事务提交完以后再进行删除缓存还有一个问题,就是如果你使用的是 Mysql 的读写分离的架构的话,那么其实主从同步之间也会有时间差。
此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)
请求 A 更新操作,删除了
Redis,
请求主库进行更新操作,主库与从库进行同步数据的操作,
请 B 查询操作,发现 Redis中没有数据,
去从库中拿去数据,此时同步数据还未完成,拿到的数据是旧数据。
此时的解决办法有两个:
1、还是使用双删延时策略。只是,睡眠时间修改为在主从同步的延时时间基础上,加几百ms。
2、就是如果是对 Redis
进行填充数据的查询数据库操作,那么就强制将其指向主库进行查询。
继续深入,采用这种同步淘汰策略,吞吐量降低怎么办?
那就将第二次删除作为异步的。自己起一个线程,异步删除。这样,写的请求就不用沉睡一段时间后了,再
返回。这么做,加大吞吐量。
继续深入,第二次删除,如果删除失败怎么办?
所以,我们引出了,下面的第四种策略,先更新数据库,再删缓存。
4、先更新DB,后删除缓存
这种方式,被称为Cache Aside Pattern,读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据
后放入缓存,同时返回响应。更新的时候,先更新数据库,然后再删除缓存。
1.4 方案如何选择的问题
一般线上,更多的偏向使用删除缓存类操作。
2 缓存穿透和击穿
2.1 缓存穿透
缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,于是这个请求就可以随意访问数据库。缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义。
造成缓存穿透的基本原因有两个:
第一:自身的业务代码和数据出现问题。比如,我们数据库的 id 都是1开始自增上去的,如发起为id值为 -1
的数据或 id 为特别大不存在的数据。如果不对参数做校验,数据库id都是大于0的,我一直用小于0的参数去
请求你,每次都能绕开Redis直接打到数据库,数据库也查不到,每次都这样,并发高点就容易崩掉了。
第二:一些恶意攻击,爬虫等造成大量空命中。
下面是解决方案:
1:缓存空对象
当存储层不命中时,到数据库查发现也没有命中,那么仍然将空对象保留到缓存层中,之后访问该数据将会从缓存层中获取,这样保护了后端数据源。
但是同样叶会出现问题:
第一,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是攻击,问题更严重),比较
有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
第二,缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为
5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利
用消前面所说的数据一致性方案处理。
2:布隆过滤器拦截
在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截。例如:一个推荐系统有4
亿个用户id,每个小时算法工程师会根据每个用户之前历史行为计算出推荐数据放到存储层中,但是最新的用
户由于没有历史行为,就会发生缓存穿透的行为,为此可以将所有推荐数据的用户做成布隆过滤器。如果布隆过
滤器认为该用户id不存在,那么就不会访问存储层,在一定程度保护了存储层。# Redis缓存使用问题
1数据一致性
分析一下几种方案:
1:先更新缓存,再更新数据库
2:先更新数据库,在更新缓存
3:先删除缓存,后更新数据库
4:想更新数据库,后删除缓存
1.1 新增数据:
如果是新增数据,数据会直接写入数据库中,不用对缓存做任何操作,此时,缓存中本身没有新增的数据,而数据库的值是最新的,此时,数据库和缓存的数据是一致的。
1.2 更新数据
1.2.1 先更新缓存,再更新数据库
这个方案一般不考虑。原因是更新缓存成功,更新数据库出现异常,导致缓存数据与数据库中数据完全不一致,而且我们很难察觉,因为缓存中的数据一直都存在。
1.2.2 先更新数据库,在更新缓存
这个方案一般不考虑。原因是更新数据库成功,更新缓存出现异常,导致缓存数据与数据库中数据完全不一致。同时还有一下几个问题:
1)并发问题
同时请求A和B进行更新操作,那么会出现
- 线程A更新数据库
- 线程B更新数据库
- 线程B更新缓存
- 线程A更新缓存
这就出现了问题,线程A应该比线程B更新缓存早才对,但是因为网络问题,B却比A更新了缓存,这就导致了脏数据,因此不考虑
2)业务场景问题
如果是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致,数据压根没有督导,缓存就会被频繁的更新,浪费性能。
1.3 删除缓存类
1.3.3 先删除缓存,后更新DB
该方案也会出问题,具体出现的原因如下。
1、此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)
2、请求 A 会先删除 Redis 中的数据,然后去数据库进行更新操作;
3、此时请求 B 看到 Redis 中的数据时空的,会去数据库中查询该值,补录到 Redis 中;
4、但是此时请求 A 并没有更新成功,或者事务还未提交,请求B去数据库查询得到旧值;
5、那么这时候就会产生数据库和 Redis 数据不一致的问题。
如何解决呢?其实最简单的解决办法就是延时双删的策略。就是
(1)先淘汰缓存
(2)再写数据库
(3)休眠1秒,再次淘汰缓存
redis.delete(key)
db.update(key)
Thread.sleep(N)
redis.delete(key)
这么做可以将N秒内所造成的缓存脏数据再次删除。
那么该休眠几秒如何确定?
针对上面情形,应该评估自己的项目读数据业务逻辑的耗时,然后写数据的休眠时间则在读数据业务逻辑耗时基础上加几百毫秒即可。
但是上述的保证事务提交完以后再进行删除缓存还有一个问题,就是如果你使用的是 Mysql 的读写分离的架构的话,那么其实主从同步之间也会有时间差。
此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)
请求 A 更新操作,删除了
Redis,
请求主库进行更新操作,主库与从库进行同步数据的操作,
请 B 查询操作,发现 Redis中没有数据,
去从库中拿去数据,此时同步数据还未完成,拿到的数据是旧数据。
此时的解决办法有两个:
1、还是使用双删延时策略。只是,睡眠时间修改为在主从同步的延时时间基础上,加几百ms。
2、就是如果是对 Redis
进行填充数据的查询数据库操作,那么就强制将其指向主库进行查询。
继续深入,采用这种同步淘汰策略,吞吐量降低怎么办?
那就将第二次删除作为异步的。自己起一个线程,异步删除。这样,写的请求就不用沉睡一段时间后了,再
返回。这么做,加大吞吐量。
继续深入,第二次删除,如果删除失败怎么办?
所以,我们引出了,下面的第四种策略,先更新数据库,再删缓存。
4、先更新DB,后删除缓存
这种方式,被称为Cache Aside Pattern,读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据
后放入缓存,同时返回响应。更新的时候,先更新数据库,然后再删除缓存。
1.4 方案如何选择的问题
一般线上,更多的偏向使用删除缓存类操作。
2 缓存穿透和击穿
2.1 缓存穿透
缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,于是这个请求就可以随意访问数据库。缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义。
造成缓存穿透的基本原因有两个:
第一:自身的业务代码和数据出现问题。比如,我们数据库的 id 都是1开始自增上去的,如发起为id值为 -1
的数据或 id 为特别大不存在的数据。如果不对参数做校验,数据库id都是大于0的,我一直用小于0的参数去
请求你,每次都能绕开Redis直接打到数据库,数据库也查不到,每次都这样,并发高点就容易崩掉了。
第二:一些恶意攻击,爬虫等造成大量空命中。
下面是解决方案:
1:缓存空对象
当存储层不命中时,到数据库查发现也没有命中,那么仍然将空对象保留到缓存层中,之后访问该数据将会从缓存层中获取,这样保护了后端数据源。
但是同样叶会出现问题:
第一,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是攻击,问题更严重),比较
有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
第二,缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为
5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利
用消前面所说的数据一致性方案处理。
2:布隆过滤器拦截
在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截。例如:一个推荐系统有4
亿个用户id,每个小时算法工程师会根据每个用户之前历史行为计算出推荐数据放到存储层中,但是最新的用
户由于没有历史行为,就会发生缓存穿透的行为,为此可以将所有推荐数据的用户做成布隆过滤器。如果布隆过
滤器认为该用户id不存在,那么就不会访问存储层,在一定程度保护了存储层。
2.2 缓存击穿
缓存击穿是指一个keyg非常热点,再不停的扛着大并发,大并发几种对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就被穿破缓存,直接请求数据库。
缓存击穿:设置热点数据永不过期或者加互斥锁就ok了。
使用互斥锁(mutex key)
业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是
立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache
的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个
get缓存的方法。
永远不过期
这里的“永远不过期”包含两层意思:
(1) 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。
(2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现
要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期
从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能
访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。
4热点Key
在Redis中,访问频率高的key称为热点key。
产生原因和危害
原因
热点问题产生的原因大致有以下两种:
用户消费的数据远大于生产的数据(热卖商品、热点新闻、热点评论、明星直播)。
在日常工作生活中一些突发的事件,例如:双十一期间某些热门商品的降价促销,当这其中的某一件商品被
数万次点击浏览或者购买时,会形成一个较大的需求量,这种情况下就会造成热点问题。同理,被大量刊
发、浏览的热点新闻、热点评论、明星直播等,这些典型的读多写少的场景也会产生热点问题。
请求分片集中,超过单Server的性能极限。在服务端读数据进行访问时,往往会对数据进行分片切分,此过
程中会在某一主机Server上对相应的Key进行访问,当访问超过Server极限时,就会导致热点Key问题的产
生。
危害
1、流量集中,达到物理网卡上限。
2、请求过多,缓存分片服务被打垮。
3、DB击穿,引起业务雪崩。发现热点key
4.1预估发现
针对业务提前预估出访问频繁的热点key,例如秒杀商品业务中,秒杀的商品都是热点key。
当然并非所有的业务都容易预估出热点key,可能出现漏掉或者预估错误的情况。
客户端发现
客户端其实是距离key"最近"的地方,因为Redis命令就是从客户端发出的,以Jedis为例,可以在核心命令入口,使用这个Google Guava中的AtomicLongMap进行记录,如下所示。
使用客户端进行热点key的统计非常容易实现,但是同时问题也非常多:
(1) 无法预知key的个数,存在内存泄露的危险。
(2) 对于客户端代码有侵入,各个语言的客户端都需要维护此逻辑,维护成本较高。
(3) 规模化汇总实现比较复杂。
Redis发现
monitor命令
monitor命令可以监控到Redis执行的所有命令,利用monitor的结果就可以统计出一段时间内的热点key排
行榜,命令排行榜,客户端分布等数据
此种方法会有两个问题:
1、monitor命令在高并发条件下,内存暴增同时会影响Redis的性能,所以此种方法适合在短时间内使用。
2、只能统计一个Redis节点的热点key,对于Redis集群需要进行汇总统计。
4.2 解决热点key
使用二级缓存:可以使用 guava-cache或hcache,发现热点key之后,将这些热点key加载到JVM中作为本地缓存。访问这些key时直接从本地缓存获取即可,不会直接访问到redis层了,有效的保护了缓存服务器。
key分散将热点key分散为多个子key,然后存储到缓存集群的不同机器上,这些子key对应的value都和热点key是一样的。当通过热点key去查询数据时,通过某种hash算法随机选择一个子key,然后再去访问缓存机器,将热点分散到了多个子key上