缓存问题
一、缓存穿透
- 一般的缓存系统,都是按照
key
去缓存查询,如果不存在对应的value
,就应该去后端系统查找(比如DB
)。 - 缓存穿透是指在高并发下查询
key
不存在的数据,会穿过缓存查询数据库。导致数据库压力过大而宕机
解决方案
- 1)对查询结果为空的情况也进行缓存,缓存时间(
ttl
)设置短一点,或者该key
对应的数据insert
了之后清理缓存。- 问题:缓存太多空值占用了更多的空间
- 2)使用布隆过滤器。在缓存之前在加一层布隆过滤器,在查询的时候先去布隆过滤器查询
key
是否存在,如果不存在就直接返回,存在再查缓存和DB
。
- 布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机hash映射函数。
- 布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法。
- 布隆过滤器的原理是:当一个元素被加入集合时,通过
K
个hash
函数将这个元素映射成一个数组中的K
个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。
二、缓存雪崩
- 当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如
DB
)带来很大压力。 - 突然间大量的
key
失效了或redis
重启,大量访问数据库,数据库崩溃
解决方案
- 1)
key
的失效期分散开 不同的key
设置不同的有效期 - 2)设置二级缓存(数据不一定一致)
- 3)高可用(脏读)
三、缓存击穿
- 对于一些设置了过期时间的
key
,如果这些key
可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。 - 这个时候,需要考虑一个问题:缓存被击穿的问题,这个和缓存雪崩的区别在于这里针对某一
key
缓存,前者则是很多key
。 - 缓存在某个时间点过期的时候,恰好在这个时间点对这个
key
有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB
加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB
压垮。
解决方案
- 1)用分布式锁控制访问的线程
- 使用
redis
的setnx
互斥锁先进行判断,这样其他线程就处于等待状态,保证不会有大并发操作去操作数据库。
- 使用
- 2)不设超时时间,
volatile-lru
但会造成写一致问题- 当数据库数据发生更新时,缓存中的数据不会及时更新,这样会造成数据库中的数据与缓存中的数据的不一致,应用会从缓存中读取到脏数据。
- 可采用延时双删策略处理。
四、数据不一致
- 缓存和
DB
的数据不一致的根源 : 数据源不一样
保证数据的最终一致性(延时双删)
- 1)先更新数据库同时删除缓存项(
key
),等读的时候再填充缓存 - 2)2秒后再删除一次缓存项(
key
) - 3)设置缓存过期时间 Expired Time 比如 10秒 或1小时
- 4)将缓存删除失败记录到日志中,利用脚本提取失败记录再次删除(缓存失效期过长
7*24
)
五、数据并发竞争
- 这里的并发指的是多个
redis
的client
同时set
同一个key
引起的并发问题
第一种方案:分布式锁+时间戳
- 1、整体技术方案
- 主要是准备一个分布式锁,大家去抢锁,抢到锁就做
set
操作 - 加锁的目的实际上就是把并行读写改成串行读写的方式,从而来避免资源竞争。
- 主要是准备一个分布式锁,大家去抢锁,抢到锁就做
- 2、
Redis
分布式锁的实现- 主要用到的
redis
函数是setnx()
- 用
SETNX
实现分布式锁 - 时间戳
- 由于上面举的例子,要求
key
的操作需要顺序执行,所以需要保存一个时间戳判断set
顺序
- 主要用到的
第二种方案:利用消息队列
- 在并发量过大的情况下,可以通过消息中间件进行处理,把并行读写进行串行化。
- 把
Redis
的set
操作放在队列中使其串行化,必须的一个一个执行
Hot Key
- 当有大量的请求(几十万)访问某个
Redis
某个key
时,由于流量集中达到网络上限,从而导致这个redis
的服务器宕机。
造成缓存击穿,接下来对这个key
的访问将直接访问数据库造成数据库崩溃,或者访问数据库回填Redis
再访问Redis
,继续崩溃。
如何发现 hot key
- 1、预估热
key
,比如秒杀的商品、火爆的新闻等 - 2、在客户端进行统计,实现简单,加一行代码即可
- 3、如果是
Proxy
,比如Codis
,可以在Proxy
端收集 - 4、利用
Redis
自带的命令,monitor
、hotkeys
。但是执行缓慢(不要用) - 5、利用基于大数据领域的流式计算技术来进行实时数据访问次数的统计,比如
Storm
、Spark Streaming
、Flink
,这些技术都是可以的。发现热点数据后可以写到zookeeper
中
如何处理热Key
- 1、变分布式缓存为本地缓存
- 发现
hot key
后,把缓存数据取出后,直接加载到本地缓存中。 - 可以采用
Ehcache
、Guava Cache
都可以,这样系统在访问热key
数据时就可以直接访问自己的缓存了。(数据不要求时时一致)
- 发现
- 2、在每个
Redis
主节点上备份hot key
数据,这样在读取时可以采用随机读取的方式,将访问压力负载到每个Redis
上。 - 3、利用对热点数据访问的限流熔断保护措施
- 每个系统实例每秒最多请求缓存集群读操作不超过
400
次,一超过就可以熔断掉,不让请求缓存集群,直接返回一个空白信息,然后用户稍后会自行再次重新刷新页面之类的。(首页不行,系统友好性差)
- 每个系统实例每秒最多请求缓存集群读操作不超过
Big Key
Big Key指的是存储的值(Value)非常大
常见场景
- 热门话题下的讨论
- 大V的粉丝列表
- 序列化后的图片
- 没有及时处理的垃圾数据
Big Key的影响
-
Big Key
会大量占用内存,在集群中无法均衡 -
Redis
的性能下降,主从复制异常 - 在主动删除或过期删除时会操作时间过长而引起服务阻塞
如何发现Big Key
- 1、
redis-cli --bigkeys
命令。- 可以找到某个实例5种数据类型(
String、hash、list、set、zset
)的最大key
。 - 但如果
Redis
的key
比较多,执行该命令会比较慢
- 可以找到某个实例5种数据类型(
- 2、获取生产
Redis的
rdb`文件- 通过
rdbtools
分析rdb
生成csv
文件 - 再导入
MySQL
或其他数据库中进行分析统计,根据size_in_bytes
统计bigkey
- 通过
Big Key的处理
- 优化
big key
的原则就是string
减少字符串长度,list、hash、set、zset
等减少成员数。 - 1、
string
类型的big key
,尽量不要存入Redis
中,可以使用文档型数据库MongoDB
或缓存到CDN
上。
如果必须用Redis存储,最好单独存储,不要和其他的key一起存储。采用一主一从或多从。 - 2、单个简单的
key
存储的value
很大,可以尝试将对象分拆成几个key-value
, 使用mget获取值,这样
分拆的意义在于分拆单次操作的压力,将操作压力平摊到多次操作中,降低对redis的IO影响。 - 3、
hash, set,zset,list
中存储过多的元素,可以将这些元素分拆 - 4、删除
big key
时不要使用del
,因为del
是阻塞命令,删除时会影响性能。 - 5、使用
lazy delete
(unlink
命令)
分布式锁
watch
利用Watch实现Redis乐观锁
乐观锁基于
CAS(Compare And Swap)
思想(比较并替换),是不具有互斥性,不会产生锁等待而消耗资源,但是需要反复的重试,但也是因为重试的机制,能比较快的响应。因此我们可以利用redis
来实现乐观锁
- 具体思路如下
- 1、利用
redis·的
watch功能,监控这个
redisKey`的状态值 - 2、获取
redisKey
的值 - 3、创建
redis
事务 - 4、给这个key的值+1
- 5、然后去执行这个事务,如果key的值被修改过则回滚,key不加1