模拟 2000 并发的场景,从缓存中读数据,没有任何问题。而如果 redis 失效了呢?比如硬件升级、突然断电,这种怎么办?原本应该访问缓存的,结果缓存失效了,雪崩…
因为缓存失效,从而导致大量的请求怼到数据库。
显然,不能在生产环境出现这个问题。
一般,这么几种情况可能出现缓存失效:
上面这些问题,是不可避免的,或者无法全部避免。但是可以用一些方式,避免缓存雪崩。
缓存雪崩还是要针对出现的问题原因做解决,最终的目的是防止数据库崩溃导致全盘崩溃。主要方案有:
典型的有Semaphore信号量限流,JUC 中重要的并发编程工具类,可以理解为“手牌”。核心方法有:
使用的方式,在读取数据库之前,给一个 semaphore.acquire() 获取一个令牌,这样就可以控制只有一定数量的线程能够通过,执行数据库访问。访问完之后,再 semaphore.release() 释放一个令牌。
限流之后,等待时间太长了,于是就要有一个降级策略。比如告诉前台:网络不给力,请重试。
这时候,可以用 semaphore.tryAcquire(),或者设置超时时间。如果超时了,就返回一个错误,便可以触发降级了。
如果直接在代码里面写这些缓存、限流的代码,就也让业务代码变得复杂了。于是,就可以自定义组件,自定义注解,去扩展原来的功能。
(见后面的“高可用集群方案”)
比如,访问不存在的数据。针对高并发的场景,常用的读都会命中缓存。而如果去查不存在的数据呢?如果还照着原来的设计,就会不停的去查数据库。
这样,查询必然不存在的数据,请求透过 Redis,直击数据库,也会导致缓存雪崩。
思路是,查询之前先判断目标数据是否存在,不存在的直接忽略,将流量拦截于缓存和数据库之前。
如果把所有数据都放到内存,这肯定是不合理的。设计思路:
布隆过滤器是 1970 年由布隆提出的,实际上是一个很长的二进制数组和一系列 hash 函数。
可以用于检索一个元素是否在一个集合中。优点是空间效率(内存占用)和查询时间都比一般的算法要好得多,缺点是有一定的误识率和删除困难。
*二进制数组的构建过程
元素是否存在的过程:
换种方式表达,注意这儿的 bitmap 长度不是实际长度。比如 redis 中这么一个 bitmap 数组,长度有 40 亿~
利用 Redis 特性和命令:bitmaps(SETBIT 设置指定位置的值、GETBIT 获取值),可以理解为这是 Redis 自带的二进制数组特性。
在每次操作数据库之外,还需要维护这么个布隆数组。如果是第一次使用,还需要在启动的时候,把所有数据给初始化了。(在生产环境,只需要初始化一次,后续做数据维护即可)
在 Redis 里面,这么一个 bitmaps,长达 40 亿。按这种方式,出现重复的概率比较小。
而且,布隆过滤器并非拦截所有的请求,而是意在将缓存击穿控制在一定的数量。
单机的内存不够怎么办?比如,用户量 3 千万,内存需要 10 G,如何设计 Redis 的缓存架构?
故此,引出了分片存储的技术,一个集群的方案。而集群的难点不是部署,而是出问题了要怎么排查?
集群之后,不同的用户信息,数据就在不同的服务器上。而如果,某台服务器挂了,怎么办?那么,就也需要给 redis 集群做一个主从复制,给每个数据都有一个副本。
redis cluster 是 redis 的分布式集群解决方案,在 3.0 版本推出后有效地解决了 redis 分布式方面的需求。实现了数据在多个 redis 节点之间自动分片、故障自动转移、扩容机制等等。参考官方文档
问题2:对于客户端要怎么操作呢?现在服务多了,位置的算法也都是 redis 内部的,要连哪个?
问题3:如果客户端都不知道数据正确位置,发 1 万个错误服务器的请求,会影响性能吗?
问题4:redis 用着用着,满了,怎么办?
当我在执行查询的时候,通过注解 @Cacheable 把缓存加到了 redis,
数据修改了,怎么同步到 redis?
1、【不推荐】先更新数据库,再更新缓存;
2、【不推荐】先更新缓存,再更新数据库;
3、【不推荐】删除缓存,再更新数据库;
4、【推荐】先更新数据库,再删除缓存;
在多线程里面,一些看似可用的代码,却不是想要的效果。数据库里面数据更新了,可是缓存还是旧的值。可参考:究竟先操作缓存,还是数据库?、缓存与数据库不一致,咋办?
Spring 的注解 @CacheEvict 也是如此,方法执行结束,才会删除缓存;
数据库主从不一致怎么办?
1、直接忽略
如果业务可以接收,最推荐这种做法。任何脱离业务的架构设计都是耍流氓,绝大部分业务,例如:百度搜索,淘宝订单,QQ消息,58帖子都允许短时间不一致。
2、强制读主
3、选择性读主
有没有可能实现,只有这一段时间,可能读到从库脏数据的读请求读主,平时读从呢?可以利用一个缓存记录必须读主的数据。把写入数据分为这么 2 个步骤:
这样,在读数据的时候,先把“读哪个库,哪个表,哪个主键”的数据拼装成 key 到 cache 里去查询:
思路为:在从库同步完成之后,如果有旧数据入缓存,应该及时把这个旧数据淘汰掉。
实际上呢,即使引入缓存,也只有一个很小的时间间隔,可能读到旧数据。
换个方式表达,即:
具体的操作:
1、开启 mySql 的 binLog 信息,即修改 /etc/my.cnf…
# 开启binlog,并且把每一行都记录下来
binlog-format=ROW
2、阿里的 Canel-Server 服务,是一个单独启动的程序,可以监听多个数据库服务。包括要监控哪些表,都可以做到。
3、业务在 update 的时候不操作缓存了,取而代之引入 Canel 客户端,读取 Canel 服务端的 binLog 文件;
4、客户端依据不同的服务,采用不同的缓存策略。
这儿以用户信息更新为例,比如每隔 1 秒钟时间,用 canel 的 api 拿到数据库的操作事件(UPDATE/DELETE/INSERT…),依据事件进行 redis 的操作,把数据同步到 redis 里面去。
5、这样,把缓存的操作异步执行,业务部分就不需要关注这部分,只需要做好增删改查,其它的事情就交给架构来做!