为什么需要缓存?缓存是什么?
在高并发的场景下,直接用数据库去抗是远远不够的,不管你的数据库是mysql还是oracle,不管用SSD还是磁盘阵列,不管采用怎样的分表分库都是很容易达到极限的,单机上千的QPS已经是一个比较的现实的天花板,而且成本太高了。
而且有些比较复杂的场景单纯的去采用数据库是不能实现要求的。
例如:
1。读多写少的场景,这种其实很尴尬的,数据库是力不从心 鞭长莫及。
比如文章流量:每日上亿的pv,每篇文章都会被大量的阅读,一次的insert的文章在短时间内会带来几百万甚至上千万的select。如果每次的阅读这种不善变的都直接干到数据库其实很傻的,数据库是为了记录数据不是为了这种简单查询场景设计的,这种场景下数据很容易撂挑子的。
2。需要复杂的计算得出的数据,数据库真的压力山大了。
比如排行榜:最新来访的排行,用数据库是可以实现的,大不了每次登陆或者刷新都来update一下数据么,可是当用户量过大的时候呢?看似update一个简单的数据因为量太大可能会导致整体数据库系统的瘫痪。
又比如附近的人:每个人都可能在移动这个update太频繁,mysql是可以通过插件实现这个功能,但是效率呢?
缓存就是为了应对这些场景应用而生的,诞生于高并发的互联网场景,为高速而生。也许它不能保证数据的100%的准确,也许它不能完全脱离数据库,但那些不是缓存要考虑的,它为了抗住高并发,有限的有损抗住流量洪峰,准确性可以异步慢慢操作。
缓存绝大多数存放在内存中(cpu的M1,M2,M3 cache其实也是缓存),内存的速度远不是硬盘能比的,硬盘就是SSD阵列也就是每秒百兆最快也就是G的级别。而内存已经达到数十G,双通道之后可以达到了上百G的级别。
缓存一次生产多次使用,避免每次读取都直接干到数据库。
如果说数据库主要是为了写,那么缓存主要是为了读,任务不同 场景不同。
缓存能够带来性能的大幅提升,以memcache为例,单台memcache服务器简单的key-value查询能够达到TPS 5千以上,而redis服务器的话可以再翻倍达到10万以上没有问题的。
基本架构:
每次的写入操作都是直接操作数据库,如果采用一些同步组件的话可以直接异步写入缓存。
而每次读取数据的操作一般是先读取缓存层,如果有就直接返回,没有话程序去读取数据库再写入到缓存然后再返回数据。其实这样会有些问题,后面再讲办法。
缓存使用中有哪些问题?
1.数据不一致
这个数据不一致主要是缓存和数据库不一致,前面讲基础架构里面有使用的方法,这种方法有可能会产生脏数据:
当年在趣头条前期的时候(那时候是爱魔豆),为了快速开发而设计的比较简单,导致后期整天遇到数据不一致的情况,白天处理校对数据,晚上开发需求,一个人搞的焦头烂额。
数据一致性有以下三个要求:
a。缓存不能读到脏数据。
b。缓存可能会读到过期数据,但要在可容忍时间内实现最终一致。
c。这个可容忍时间尽可能的小。
例如现有三个请求:写入数据A和C和读取数据的B。都对同一个数据进行读写操作。
- A先写入缓存再写数据库会怎样?
答:有可能缓存写完程序出错退出,数据库写入失败,此时B读取的缓存就是脏数据。
- A先写入数据库再写入缓存呢?
答:A先写,然后C继续写,接着C写入缓存,然后某种原因A写入缓存的步骤来晚了再去写入缓存,B去读缓存的时候就是A后面写入的脏数据了。
- A只管写数据库,然后删除缓存,等B来读的时候发现没缓存再去读取数据库同时写入缓存呢?
答:还是不行,因为这么操作流量大的时候可能都发现没缓存击都去读取数据库导致直接打垮数据库。或者B读取完毕的时候,C开始写数据库,然后C先同步缓存成功,然后B再去写入缓存,结果可想而知还是脏数据。
那怎么办?
- 最好的方式采用订阅监听binglog方式异步串行处理。
比如采用阿里开源的中间件canal可以完成订阅binlog日志的功能,我们cisg的企点部门也开发一款MySyn同步组件。
mysql主从时会在主机修改后写入binglog日志,然后同步到从服务器。这些组件伪装成从服务器,从收到binglog日志里面解析出原始的修改,然后再去从容的修改缓存。异步串行处理让复杂的事情变得简单了。!
局限性:
a。为了保证高效,这个最好不要做复杂的逻辑,比如聚合 连表 统计等等,不然的话影响性能。一般都是原表的复制或者是某些字段的复制。需要提前配置好,太不灵活,对于业务开发人员不够友好。
b。有一定的延迟性。这个其实还好的,一般都是毫秒级的同步。但是需要注意监控,出现问题最好及时人工处理。
- 缓存引入版本号。
设计表的时候增加版本号字段,每次update其他字段的时候增加set version=version+1,每次写入修改后,再去读取一把获取数据以及最新的version字段,再去修改缓存,如果发现缓存的中的版本比较高就不去修改缓存,如果现有缓存比较低就修改缓存。
局限性:
只有部分缓存支持,比如我厂的CKV等。而memcache和redis都不支持,如果采取读取缓存再去判断再去操作缓存,就变成非原子性的操作,一样的有不同步的风险。
- 写入缓存前加锁
先加锁(一定要带失效时间),再去查询数据库,再去写入缓存,然后删除锁。其他需要写入缓存的操作一定要先判断是否有锁,有锁就休眠一会再去重复刚才的动作。
局限性:
a。锁的失效时间一定要设置,还要设置的比较恰当,不然的话会出问题,推荐5-10s吧。
b。 只要是锁都是有风险的,都会影响点性能的。
综上所述 :
最好的还是监听binglog异步处理,其次如果采用缓存支持版本号可以使用版本号,再其次采用加锁。当然了如果要求不算太高可以先删缓存->写数据库->读取数据库->写缓存。Facebook选择了这个方案。
2。缓存穿透(查根本不存在的数据)。
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据,如果id是id是hash的更容易缓存不命中。这时的用户很可能是攻击者,攻击会导致数据库压力过大,很可能搞死数据库。
怎么去防范?
a。在接入层做好数据的校验,比如id要是正整数,否则返回错误。
b。每次读取数据库后不管是否读取到数据,均设置到缓存中,比如把null设置到缓存里同时要设置缓存失效时间的。这样在失效前下次再来请求不去查询数据库的。
c。很多教程只是说了前面两种,其实这是不够的,因为恶搞的人不傻,id最大才999999 但可能传过来的id是9999991,9999992,9999993。。。既合法又不重复其实没有的数据id,那要怎么办?你可能会想到一直维护一个maxId,每次update数据库都要维护一下,那就成本太高了。这就要涉及到频控(最好外加服务降级),比如ip的维度等等,不管是不是恶意都要控制到可靠的范围内,这样才能比较可靠。
3. 缓存击穿(高频次查刚失效的缓存)。
缓存击穿,就是说某个key非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个key在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
解决方法:
a。可以将高频次的热点数据设置为永远不过期(后面详细讲解),比如全局配置,挡在最前面即便没了缓存也是不应该查询数据库的,直接抛出错误返回。
b。基于Redis或者Zookeeper实现互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该key访问数据。
4. 缓存雪崩(缓存故障大批量甚至整体查询都去数据库)。
由于缓存服务器意外故障,也可能是网络问题也可能宕机,导致大量的请求直接落到数据库。由于数据库的并发能力并不能抗住大量并发请求,因此数据库直接瘫了。即便是重启灰度数据库也会立刻再次被打死,这被称为缓存雪崩。
解决方法:
1。集群!集群!集群!重要的事情说三遍,只是采用主从模式是不够的的。
2。即便是集群也不是不靠谱的,最好采用多级缓存比如ehcache缓存,或者服务在本机缓存等等(后面有举例)。
3。老生常谈:限流+服务降级。死一个服务总比全死强,丢掉一部分总比全丢强。
在 redis 中,实现 高可用 的技术主要包括 持久化、复制、哨兵 和 集群,下面简单说明它们的作用,以及解决了什么样的问题:
持久化:持久化是 最简单的 高可用方法。它的主要作用是 数据备份,即将数据存储在 硬盘,保证数据不会因进程退出而丢失。
复制:复制是高可用 redis 的基础,哨兵 和 集群 都是在 复制基础 上实现高可用的。复制主要实现了数据的多机备份以及对于读操作的负载均衡和简单的故障恢复。缺陷是故障恢复无法自动化、写操作无法负载均衡、存储能力受到单机的限制。
哨兵:在复制的基础上,哨兵实现了 自动化 的 故障恢复。缺陷是 写操作 无法 负载均衡,存储能力 受到 单机 的限制。
集群:通过集群,redis 解决了 写操作 无法 负载均衡 以及 存储能力 受到 单机限制 的问题,实现了较为 完善 的 高可用方案。
5. 缓存爆满(由于预估不足,内存设置不足,内存使用占满)。
redis的淘汰策略算法有6种:
volatile-lru:从设置了过期时间的数据集中,选择最近最久未使用的数据释放。
allkeys-lru:从数据集中(包括设置过期时间以及未设置过期时间的数据集中),选择最近最久未使用的数据释放。
volatile-random:从设置了过期时间的数据集中,随机选择一个数据进行释放。
allkeys-random:从数据集中(包括了设置过期时间以及未设置过期时间)随机选择一个数据进行入释放。
volatile-ttl:从设置了过期时间的数据集中,选择马上就要过期的数据进行释放操作。
noeviction:不删除任意数据(但redis还会根据引用计数器进行释放),这时如果内存不够时,会直接返回错误。
其实很多时候拿redis当mysql去用了,很多时候都是不失效的。这就导致了redis使用内存一直暴涨,如果内存设置的不够大那就有严重的问题了。
当年在趣头条的时候有天傍晚高峰期全线服务告警,没有一个服务正常的,顿时懵逼慌了神,不知道哪里的问题了。最后仔细分析才发现:redis满了!由于所有的服务基本都用了redis,导致全部写入不成功。
2019年初cisg企点webim也是出现类似的问题,webim的离线消息采用的redis的list结构作为存储。前期同事预估不足采购的redis太小(才2G),导致整体webim的离线消息不能正常存储,又一个惨痛运营事故!
解决预案(其实不能叫解决方法):
a。监控!监控!监控!其实即便不是这个问题也是需要对redis整体监控的,内存吃紧的时候尽早知晓!
b。集群也是要分开的。不是说一个集群大家一股脑的混在一起去用,一个爆掉全部爆掉。分开存储也是未来容易拆分迁移,可以细分。有些redis的key确实需要持久的可以一起存储,有些业务缓存有失效期可以一起存储,或者可以根据微服务模块的方式隔离存储。
c。随时准备好扩容,比如采用云redis集群。尽量不要自建集群,维护起来成本比较高。
6. 网卡打满(缓存故障大批量甚至整体查询都去数据库)。
这个估计真的没多少人遇到过了。又是一件自己惨痛的教训。
一些核心数据比如全局的设置等等,这些大部分的请求都是需要获取的。这样会造成流量过于集中,达到物理网卡上限,从而导致redis的服务整体不稳定了:延迟长,请求超时,最后整体服务瘫了。
又是在趣头条又是在高峰期,又是对外业务全部不正常了,一时半会不知道哪里的问题整个人汗都出来了。最后才发现网卡app的系统配置读取的太频繁,基本上每次请求都要来读取而且不是一次读取。
怎么发现热key :
a。凭借业务经验,进行预估哪些是热key
其实这个方法还是挺有可行性的。比如某商品在做秒杀,那这个商品的key就可以判断出是热key。缺点很明显,并非所有业务都能预估出哪些key是热key。
b。在客户端进行收集
这个方式就是在操作redis之前,加入一行代码进行数据统计。那么这个数据统计的方式有很多种,也可以是给外部的通讯系统发送一个通知信息。缺点就是对客户端代码造成入侵。
c。在Proxy层做收集
有些集群架构是下面这样的,Proxy可以是Twemproxy,是统一的入口。可以在Proxy层做收集上报,但是缺点很明显,并非所有的redis集群架构都有proxy。
d。用redis自带命令
(1)monitor命令,该命令可以实时抓取出redis服务器接收到的命令,然后写代码统计出热key是啥。当然,也有现成的分析工具可以给你使用,比如redis-faina。但是该命令在高并发的条件下,有内存增暴增的隐患,还会降低redis的性能。
(2)hotkeys参数,redis 4.0.3提供了redis-cli的热点key发现功能,执行redis-cli时加上–hotkeys选项即可。但是该参数在执行的时候,如果key比较多,执行起来比较慢。
e。自己抓包评估
redis客户端使用TCP协议与服务端进行交互,通信协议采用的是RESP。自己写程序监听端口,按照RESP协议规则解析数据,进行分析。缺点就是开发成本高,维护困难,有丢包可能性。
解决方法:
a。发现不对了赶紧做的是限流和服务降级,丢掉一部分请求先保证部分功能和请求的正常。
b。后期把这些key单独放到一个实例(库)里面,做好集群。
c。来个投机取巧的方式:每个业务服务器上部署一个redis做从机,做到缓存的分级。高频次访问的全局设置直接可以读本机的redis从机,因为这种全局配置肯定会有值哪怕默认值绝对不会为空。从机有问题或者读不到再去读取集群。
有人说这个方法太挫,比如扩容不便等,但是对于扩容这种低概率的事件,用这个方法最重要的解决缓存的主从同步,redis的主从同步帮你解决了.如果自己做一套内存缓存,那么你这个主从同步怎么办?做不好就是大量脏数据或者缓存击穿了.
d。其实最主要的是架构师的预估,对自身业务要心里有谱,就在设计的架构的时候就做好隔离,等到病了再想去吃药有点晚了。
总结 :