该篇笔记目录结构如下:
- 大key:定义、危害及消除方法。
- 热key:定义、危害及消除方法。
- 慢查询场景
- 缓存穿透和缓存雪崩:定义、危害及消除方法
1. 大key
1.1 大Key定义:
- string类型:value的字节数>10KB
- Hash/Set/Zset/list等复杂数据结构:元素个数>5000 或 总value字节数>10MB
1.2 大key危害:
先从Redis数据发到Redis服务器上处理过程来说
- 读取成本高
- 容易导致慢查询(过期、删除)
Redis单线程处理,见下图。当key为正常大小时,T时间内可以处理两条命令,但当Key变大时,T时间内就只够处理一条命令。可能会导致设置了过期时间的key到达过期时间被删除。 - 主从复制异常,服务阻塞
当我们删除一个大key的时候,会造成时间过长,会引发主从切换或者同步中断 Redis的工作线程是单线程的,如果一个command堵塞了,那所有请求都会超时主从复制:集群模式的一种。包含一个主数据库实例(master)与多个从数据库实例(slave)。客户端可对主数据库进行读写操作,对从数据库进行读操作,主数据库写入的数据会实时自动同步给从数据库。可以进行读写分离将读压力分担一部分到slave上。
业务侧使用大key的表现:请求Redis超时报错。业务设计的时候尽量不要用大key,
1.3 消除大key的方法:
-
string类型
-
拆分: 将大key拆分为小key。例如一个string拆分为多个string。
缺点:业务逻辑复杂需要去维护多个key and 解析字符串消耗时间 -
压缩:将value压缩后写入redis,读取时解压后再使用。
实际过程中怎么选择压缩算法:压缩算法可以是gzip、snappy、lz4等。通常情况下,一个压缩算法压缩率高则解压耗时就长,需要对实际数据测试后选择合适算法(根据场景去压力测试,写入100个压缩后的key,然后给它反解出来。解压一个key的时间=解压100个key需要时间/100),考虑到压缩时间和解压时间的平衡,如果是写少读多场景,则重点考虑解压时间。
通常情形下,压缩解决不了才拆分。
-
-
Hash/Set/Zset/list等集合类结构
- 拆分:可以用hash取余、位掩码的方式决定放在哪个key中
- 区分冷热:如榜单列表场景使用zset,只缓存前10页数据,后续数据走db
比较成熟的处理方式:区分冷热
绝大多数用户都能通过zset提供高速访问,同时也没有形成大key
2. 热key
2.1 热key定义
用户访问一个Key的QPS特别高,导致Server实例出现CPU负载突增或者不均的情况。热Key没有明确的标准,QPS超过500就有可能被识别为热Key。
场景:
Redis分区为多个slot,每个slot对应一个redis实例,虽然做了很多分片,但是所有请求都会访问第一台机器,全站流量都会访问它。
热key风险:
CPU负载高,速度慢,机器承受不住,网络拥塞。机器响应能力下降,所有请求实现不了,网站卡住。
2.2 解决热key的方法
-
设置Localcache
在访问Redis前,在业务服务侧设置Localcache,降低Redis的QPS。LocalCache中缓存过期或未命中,则从Redis中将数据更新到LocalCache。Golang的Bigcache就是这类LocalCache。LocalCache介绍:
MySQL数据量增大时为了提高访问速度使用了Redis,而本机LocalCache会比Redis更快,无需走网络缓存。LocalCache核心要做到:1. 让请求更快,从本机内存取 2. 让缓存相对有效缓存过期管理:用了LocalCache后Server上存的数据很有可能和Redis上存的数据不一致。所有需要让缓存的数据短暂有效,一旦过期需要从Redis更新。比如过期时间设置为2s,Redis压力会得到极大的降低。
-
拆分
实现:将key:value这一个热Key复制写入多份(例如key1:value, key2:value),访问的时候访问多个key,但value是同一个,以此将QPS分散到不同实例上,降低负载。
缺点:更新时需要更新多个Key,存在数据短暂不一致的风险。
关键点:更新key的时候要做很多工作保证所有slot数据一致
-
使用Redis代理的热Key承载能力
LocalCache可行但是需要每个程序写一个LocalCache,由代理实现该能力,代理具备后端路由热key发现能力,设计思想还是LocalCache,但是由Proxy来做。
3. 慢查询场景
4. 缓存穿透、雪崩、击穿
缓存穿透:
缓存和数据库中都没有的数据,可用户还是源源不断的发起请求,导致每次请求都会到数据库,从而压垮数据库。eg:Redis宕机,大量请求打到数据库上,数据库本身很慢。
比如客户查询一个根本不存在的东西,首先从Redis中查不到,然后会去数据库中查询,数据库中也查询不到,那么就不会将数据放入到缓存中,后面如果还有类似源源不断的请求,最后都会压到数据库来处理,从而给数据库造成巨大的压力。
缓存穿透的危害:
- 查询一个一定不存在的数据
由于通常不会缓存不存在数据,缓存不会命中,于是这类查询请求都会直接达到数据库,如果有系统bug或者人为攻击,那么容易导致数据库响应慢甚至宕机。 - 缓存过期时
在高并发场景下,一个热key如果过期,会有大量请求同时击穿至数据库,影响数据库性能和稳定。
同一时间有大量key集中过期时,也会导致大量请求落到数据库上,导致查询变慢,甚至出现数据库无法响应新的查询。
如何减少缓存穿透:
- 缓存空值:避免用户查询到不存在数据,下次用户查缓存直接返回空值。
- 布隆过滤器:通过布隆算法来告诉Key是否存在,存储合法Key。(该算法拥有超高的压缩率,只需极小的空间就能存储大量Key值。) 。对于缓存击穿,我们可以将查询的数据条件都哈希到一个足够大的布隆过滤器中,用户发送的请求会先被布隆过滤器拦截,一定不存在的数据就直接拦截返回了,从而避免下一步对数据库的压力。
- 针对缓存穿透的请求,可以在缓存层做拦截,防止直接访问底层存储系统。
缓存击穿
缓存中没有但数据库中有的数据,并且某一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间(一般是缓存时间到期),持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。导致请求直接穿透缓存,直接访问底层存储系统。
解决方案:
- 热点数据永不过期
- 互斥锁:简单来说就是在Redis中根据key获得的value值为空时,先锁上,然后从数据库加载,加载完毕,释放锁。若其他线程也在请求该key时,发现获取锁失败,则睡眠一段时间(比如100ms)后重试。
- 在缓存失效的时候,采用异步方式重新加载缓存,而不是同步方式。
缓存雪崩
缓存雪崩: 缓存中大批量数据到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
如何避免缓存雪崩:
- 将缓存失效时间分散开,比如在原有的失效时间基础上增加一个随机值。(eg: 不同key过期时间设置为10分1秒,10分23秒,10分8秒。单位秒部分就是随机时间。)
对于热点数据,过期时间尽量设置长,冷门数据可以相对设置过期时间短一些。 - 如果缓存数据库是分布式部署,将 热点数据均匀分布在不同的缓存数据库中。
- 热点数据永不过期