文章内容输出来源:拉勾教育Java高薪训练营;
缓存穿透
缓存穿透: 在高并发下查询key不存在的数据,会穿过缓去存查询数据库。导致数据库压力过大而宕机。
解决方案:
-
对查询结果为空的情况也进行缓存,缓存时间(ttl)设置短一点,或者该key对应的数据insert了之后清理缓存。
缺点:缓存太多空值占用了更多的空间 -
使用布隆过滤器。在缓存之前在加一层布隆过滤器,在查询的时候先去布隆过滤器查询 key 是否存在,如果不存在就直接返回,存在再查缓存和DB。
布隆过滤器原理: 当一个元素被加入集合时,将这个元素通过n次Hash函数结果映射成一个数组中的n个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。总之布隆过滤器是一个很大二进制的位数组,数组里面只存0和1。
举例说明:
初始无数据的布隆过滤器为一个二进制的位数组,如下:
假设添加一个元素为x,对该元素进行3次hash计算得出结果:
hash0[x] = 10;
hash1[x] = 5;
hash2[x] = 16;
那么接下来hash值作为,布隆过滤器数组的下标,将值由0改为1
添加操作:
存入 将每次的hash值作为下标 ,位数组的下标改为1 数据 n次hash计算 位数组查询操作:
查询 将每次的hash值作为下标 ,在位数组找对应下标是否都为1,则可能存在 数据 n次hash计算 位数组为什么会存在误判?
举例说明:
假设对已经在布隆过滤器中存储了元素X和Y,对应的n次hash坐标如下
hash0[x] = 10; hash1[x] = 5; hash2[x] = 16;
hash1[y] = 11; hash2[y] = 10; hash3[y] = 22;
此时来了一个元素Z,需要对Z进行判断是否存在与布隆过滤器中,对Z进行3次hash计算
hash1[z] = 5; hash2[z] = 22; hash3[z] = 11;
得到的hash值恰好在x和y的hash中,此时布隆过滤器会认为元素z也在数组中存在,这是就会造成误判
所以如果布隆过滤器判断该元素存在,那么该元素大概率存在,如果布隆过滤器判断该元素不存在,那么该元素则一定不存在。
缓存雪崩
缓存雪崩: 某一个时间段内,缓存大量失效或者缓存服务器挂掉(重启)时,导致大量请求直接去访问数据库,导致数据库崩溃。
解决方案:
- 随机过期时间,key的失效期分散开,不同的key设置不同的有效期
- 设置二级缓存,加一层本地缓存(例如Guava Cache、ECache等),采用本地缓存+分布式缓存redis的方式。
缺点:数据不一定一致 - 高可用(有可能存在脏读)
缺点:读从库时,有可能存在脏读
缓存击穿
缓存击穿: 缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般会从数据库中加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮
解决方案:
- 用分布式锁控制访问的线程,使用redis的setnx互斥锁先进行判断,这样其他线程就处于等待状态,保证不会有大并发操作去操作数据库。
- 不设超时时间,采用volatile-lru淘汰策略
缺点:会造成写一致问题,当数据库数据发生更新时,缓存中的数据不会及时更新,这样会造成数据库中的数据与缓存中的数据的不一致,应用会从缓存中读取到脏数据。可采用延时双删策略处理。
缓存一致性问题
缓存不一致: 由于缓存和数据库不属于同一个数据源,本质上非原子操作,所以是无法保证强一致性的,只能去实现最终一致性。
解决方案:
- 延时双删:先更新数据库同时删除缓存,等2秒后再删除一次缓存,等到读的时候在回写到缓存。
- 利用工具(canal)将数据库的binlog日志采集发送到MQ中,然后通过ACK机制确认处理删除缓存
Hot key(热点key)
当有大量的请求(几十万)访问某个Redis某个key时,由于流量集中达到网络上限,从而导致这个redis的服务器宕机。造成缓存击穿,接下来对这个key的访问将直接访问数据库造成数据库崩溃,或者访问数据库回填Redis再访问Redis,继续崩溃
如何发现热点key:
- 预估热key,比如秒杀的商品、火爆的新闻等
- 在客户端进行统计,实现简单,加一行代码即可
- 如果是Proxy,比如Codis,可以在Proxy端收集
- 利用Redis自带的命令,monitor、hotkeys。但是执行缓慢(不要用)
- 利用基于大数据领域的流式计算技术来进行实时数据访问次数的统计,比如 Storm、Spark、Streaming、Flink,这些技术都是可以的。发现热点数据后可以写到zookeeper中
解决方案:
- 变分布式缓存为本地缓存,发现热key后,把缓存数据取出后,直接加载到本地缓存中。可以采用Ehcache、Guava Cache都可以,这样系统在访问热key数据时就可以直接访问自己的缓存了。(数据不要求时时一致)
- 在每个Redis主节点上备份热key数据,这样在读取时可以采用随机读取的方式,将访问压力负载到每个Redis上。
- 利用对热点数据访问的限流熔断保护措施,每个系统实例每秒最多请求缓存集群读操作不超过 400 次,一超过就可以熔断掉,不让请求缓存集群,直接返回一个空白信息,然后用户稍后会自行再次重新刷新页面之类的。(首页不行,系统友好性差)通过系统层自己直接加限流熔断保护措施,可以很好的保护后面的缓存集群.
Big key(大 key)
大key指的是存储的值(Value)非常大。
- 大key会大量占用内存,在集群中无法均衡
- Redis的性能下降,主从复制异常
- 在主动删除或过期删除时会操作时间过长而引起服务阻塞
如何发现Big key
- redis-cli --bigkeys命令。可以找到某个实例5种数据类型(String、hash、list、set、zset)的最大
key。但如果Redis 的key比较多,执行该命令会比较慢。 - 获取生产Redis的rdb文件,通过rdbtools分析rdb生成csv文件,再导入MySQL或其他数据库中进行分析统计,根据size_in_bytes统计big key
解决方案:
- string类型的big key,尽量不要存入Redis中,可以使用文档型数据库MongoDB或缓存到CDN上。如果必须用Redis存储,最好单独存储,不要和其他key一起存储。采用一主一从或多从。
- 单个简单key存储的value很大,可以尝试将对象分拆成几个key-value, 使用mget获取值,这样分拆的意义在于分拆单次操作的压力,将操作压力平摊到多次操作中,降低对redis的IO影响。
- hash, set,zset,list 中存储过多的元素,可以将这些元素分拆。
以hash类型举例来说,对于field过多的场景,可以根据field进行hash取模,生成一个新的key,例如原
来的
hash_key:{filed1:value, filed2:value, filed3:value …},可以hash取模后形成如下
key:value形式
hash_key:1:{filed1:value}
hash_key:2:{filed2:value}
hash_key:3:{filed3:value}
…
取模后,将原先单个key分成多个key,每个key filed个数为原先的1/N
- 删除大key时不要使用del,因为del是阻塞命令,删除时会影响性能。
- 使用 lazy delete (unlink命令)
删除指定的key(s),若key不存在则该key被跳过。但是,相比DEL会产生阻塞,该命令会在另一个线程中回收内存,因此它是非阻塞的。 这也是该命令名字的由来:仅将keys从key空间中删除,真正的数据删除会在后续异步操作。
写在最后
工作几年,一直都没有去体系化的学习,很多东西没有复杂的工作场景经验,去年综合几家机构,最后还是决定报了拉勾的高薪训练营,在这里也是实实在在的学习到了很多,学完掌握程度也比之前深了很多,而且还有定期的内推,多了更多的机会,真的对我有了很大的帮助提升。