分析:博主以为在项目中使用redis,主要是从两个角度去考虑:性能和并发。固然,redis还具有能够作分布式锁等其余功能,可是若是只是为了分布式锁这些其余功能,彻底还有其余中间件(如zookpeer等)代替,并非非要使用redis。所以,这个问题主要从性能和并发两个角度去答。
回答:以下所示,分为两点
(一)性能
咱们在碰到须要执行耗时特别久,且结果不频繁变更的SQL,就特别适合将运行结果放入缓存。这样,后面的请求就去缓存中读取,使得请求可以迅速响应。html
(二)并发
在大并发的状况下,全部的请求直接访问数据库,数据库会出现链接异常。这个时候,就须要使用redis作一个缓冲操做,让请求先访问到redis,而不是直接访问数据库。node
疯狂创客圈 经典图书 : 《Netty Zookeeper Redis 高并发实战》 面试必备 + 面试必备 + 面试必备mysql
字符串String、字典Hash、列表List、集合Set、有序集合SortedSet。
若是你是Redis中高级用户,还须要加上下面几种数据结构HyperLogLog、Geo、Pub/Sub。
若是你说还玩过Redis Module,像BloomFilter,RedisSearch,Redis-ML,面试官得眼睛就开始发亮了。面试
先拿setnx来争抢锁,抢到以后,再用expire给锁加一个过时时间防止锁忘记了释放。
这时候对方会告诉你说你回答得不错,而后接着问若是在setnx以后执行expire以前进程意外crash或者要重启维护了,那会怎么样?
这时候你要给予惊讶的反馈:唉,是喔,这个锁就永远得不到释放了。紧接着你须要抓一抓本身得脑壳,故做思考片刻,好像接下来的结果是你主动思考出来的,而后回答:我记得set指令有很是复杂的参数,这个应该是能够同时把setnx和expire合成一条指令来用的!对方这时会显露笑容,内心开始默念:嗯,这小子还不错。redis
使用keys指令能够扫出指定模式的key列表。
对方接着追问:若是这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?
这个时候你要回答redis关键的一个特性:redis的单线程的。keys指令会致使线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候能够使用scan指令,scan指令能够无阻塞的提取出指定模式的key列表,可是会有必定的重复几率,在客户端作一次去重就能够了,可是总体所花费的时间会比直接用keys指令长。算法
通常使用list结构做为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。
若是对方追问可不能够不用sleep呢?list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来。
若是对方追问能不能生产一次消费屡次呢?使用pub/sub主题订阅者模式,能够实现1:N的消息队列。
若是对方追问pub/sub有什么缺点?在消费者下线的状况下,生产的消息会丢失,得使用专业的消息队列如rabbitmq等。
若是对方追问redis如何实现延时队列?我估计如今你很想把面试官一棒打死若是你手上有一根棒球棍的话,怎么问的这么详细。可是你很克制,而后神态自若的回答道:使用sortedset,拿时间戳做为score,消息内容做为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒以前的数据轮询进行处理。
到这里,面试官暗地里已经对你竖起了大拇指。可是他不知道的是此刻你却竖起了中指,在椅子背后。sql
若是大量的key过时时间设置的过于集中,到过时的那个时间点,redis可能会出现短暂的卡顿现象。通常须要在时间上加一个随机值,使得过时时间分散一些。数据库
bgsave作镜像全量持久化,aof作增量持久化。由于bgsave会耗费较长时间,不够实时,在停机的时候会致使大量丢失数据,因此须要aof来配合使用。在redis实例重启时,优先使用aof来恢复内存的状态,若是没有aof日志,就会使用rdb文件来恢复。
若是再问aof文件过大恢复时间过长怎么办?你告诉面试官,Redis会按期作aof重写,压缩aof文件日志大小。若是面试官不够满意,再拿出杀手锏答案,Redis4.0以后有了混合持久化的功能,将bgsave的全量和aof的增量作了融合处理,这样既保证了恢复的效率又兼顾了数据的安全性。这个功能甚至不少面试官都不知道,他们确定会对你另眼相看。
若是对方追问那若是忽然机器掉电会怎样?取决于aof日志sync属性的配置,若是不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。可是在高性能的要求下每次都sync是不现实的,通常都使用定时sync,好比1s1次,这个时候最多就会丢失1s的数据。编程
能够将屡次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用redis-benchmark进行压测的时候能够发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。api
从从同步。第一次同步时,主节点作一次bgsave,并同时将后续修改操做记录到内存buffer,待完成后将rdb文件全量同步到复制节点,复制节点接受完成后将rdb镜像加载到内存。加载完成后,再通知主节点将期间修改的操做记录同步到复制节点进行重放就完成了同步过程。
Redis Sentinal着眼于高可用,在master宕机时会自动将slave提高为master,继续提供服务。
Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。
这个问题,互联网公司必问,要是一我的连缓存都不太清楚,那确实比较尴尬。
只要问到缓存,上来第一个问题,确定是先问问你项目哪里用了缓存?为啥要用?不用行不行?若是用了之后可能会有什么不良的后果?
这就是看看你对缓存这个东西背后有没有思考,若是你就是傻乎乎的瞎用,无法给面试官一个合理的解答,那面试官对你印象确定不太好,以为你平时思考太少,就知道干活儿。
项目中缓存是如何使用的?
这个,须要结合本身项目的业务来。
为何要用缓存?
用缓存,主要有两个用途:高性能、高并发。
假设这么个场景,你有个操做,一个请求过来,吭哧吭哧你各类乱七八糟操做 mysql,半天查出来一个结果,耗时 600ms。可是这个结果可能接下来几个小时都不会变了,或者变了也能够不用当即反馈给用户。那么此时咋办?
缓存啊,折腾 600ms 查出来的结果,扔缓存里,一个 key 对应一个 value,下次再有人查,别走 mysql折腾 600ms 了,直接从缓存里,经过一个 key 查出来一个 value,2ms 搞定。性能提高 300 倍。
就是说对于一些须要复杂操做耗时查出来的结果,且肯定后面不怎么变化,可是有不少读请求,那么直接将查询出来的结果放在缓存中,后面直接读缓存就好。
因此要是你有个系统,高峰期一秒钟过来的请求有 1 万,那一个 mysql 单机绝对会死掉。你这个时候就只能上缓存,把不少数据放缓存,别放 mysql。缓存功能简单,说白了就是 key-value 式操做,单机支撑的并发量轻松一秒几万十几万,支撑高并发 so easy。单机承载并发量是 mysql 单机的几十倍。
缓存是走内存的,内存自然就支撑高并发。
用了缓存以后会有什么不良后果?
常见的缓存问题有如下几个:
缓存与数据库双写不一致 、缓存雪崩、缓存穿透、缓存并发竞争后面再详细说明。
这个是问 redis 的时候,最基本的问题吧,redis 最基本的一个内部原理和特色,就是 redis 其实是个单线程工做模型,你要是这个都不知道,那后面玩儿 redis 的时候,出了问题岂不是什么都不知道?
还有可能面试官会问问你 redis 和 memcached 的区别,可是 memcached 是早些年各大互联网公司经常使用的缓存方案,可是如今近几年基本都是 redis,没什么公司用 memcached 了。
redis 和 memcached 有啥区别?
redis 相比 memcached 来讲,拥有更多的数据结构,能支持更丰富的数据操做。若是须要缓存可以支持更复杂的结构和操做, redis 会是不错的选择。
在 redis3.x 版本中,便能支持 cluster 模式,而 memcached 没有原生的集群模式,须要依靠客户端来实现往集群中分片写入数据。
因为 redis 只使用单核,而 memcached 能够使用多核,因此平均每个核上 redis 在存储小数据时比memcached 性能更高。而在 100k 以上的数据中,memcached 性能要高于 redis。虽然 redis 最近也在存储大数据的性能上进行优化,可是比起 memcached,仍是稍有逊色。
redis 的线程模型
redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,因此 redis 才叫作单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,将产生事件的 socket 压入内存队列中,事件分派器根据 socket 上的事件类型来选择对应的事件处理器进行处理。
文件事件处理器的结构包含 4 个部分:
多个 socket 可能会并发产生不一样的操做,每一个操做对应不一样的文件事件,可是 IO 多路复用程序会监听多个 socket,会将产生事件的 socket 放入队列中排队,事件分派器每次从队列中取出一个 socket,根据 socket 的事件类型交给对应的事件处理器进行处理。
来看客户端与 redis 的一次通讯过程:
给缓存在 redis 里,而后每次读写缓存的时候,能够就操做 hash 里的某个字段。
hset person name bingohset person age 20hset person id 1 hget person nameperson = { "name": "bingo", "age": 20, "id": 1}
list
list 是有序列表,这个能够玩儿出不少花样。
好比能够经过 list 存储一些列表型的数据结构,相似粉丝列表、文章的评论列表之类的东西。
好比能够经过 lrange 命令,读取某个闭区间内的元素,能够基于 list 实现分页查询,这个是很棒的一个功能,基于 redis 实现简单的高性能分页,能够作相似微博那种下拉不断分页的东西,性能高,就一页一页走。
# 0 开始位置,-1 结束位置,结束位置为-1 时,表示列表的最后一个位置,即查看全部。lrange mylist 0 -1
好比能够搞个简单的消息队列,从 list 头怼进去,从 list 尾巴那里弄出来。
lpush mylist 1lpush mylist 2lpush mylist 3 4 5# 1rpop mylist
set
set 是无序集合,自动去重。
直接基于 set 将系统里须要去重的数据扔进去,自动就给去重了,若是你须要对一些数据进行快速的全局去重,你固然也能够基于 jvm 内存里的 HashSet 进行去重,可是若是你的某个系统部署在多台机器上呢?
得基于 redis 进行全局的 set 去重。
把两个大 V 的粉丝都放在两个 set 中,对两个 set 作交集。
#-------操做一个 set-------# 添加元素 sadd mySet 1# 查看所有元素 smembers mySet# 判断是否包含某个值 sismember mySet 3# 删除某个/些元素 srem mySet 1 srem mySet 2 4# 查看元素个数 scard mySet# 随机删除一个元素 spop mySet#-------操做多个 set-------# 将一个 set 的元素移动到另一个 setsmove yourSet mySet 2# 求两 set 的交集 sinter yourSet mySet# 求两 set 的并集 sunion yourSet mySet# 求在 yourSet 中而不在 mySet 中的元素 sdiff yourSet mySet
sorted set
sorted set 是排序的 set,去重但能够排序,写进去的时候给一个分数,自动根据分数排序。
zadd board 85 zhangsanzadd board 72 lisi zadd board 96 wangwuzadd board 63 zhaoliu# 获取排名前三的用户(默认是升序,因此须要 rev 改成降序) zrevrange board 0 3# 获取某用户的排名 zrank board zhaoliu
若是你连这个问题都不知道,上来就懵了,回答不出来,那线上你写代码的时候,想固然的认为写进 redis的数据就必定会存在,后面致使系统各类 bug,谁来负责?
常见的有两个问题:
(1)往 redis 写入的数据怎么没了?
可能有同窗会遇到,在生产环境的 redis 常常会丢掉一些数据,写进去了,过一下子可能就没了。个人天,同窗,你问这个问题就说明 redis 你就没用对啊。redis 是缓存,你给当存储了是吧?
啥叫缓存?用内存当缓存。内存是无限的吗,内存是很宝贵并且是有限的,磁盘是廉价并且是大量的。可能一台机器就几十个 G 的内存,可是能够有几个 T 的硬盘空间。redis 主要是基于内存来进行高性能、高并发的读写操做的。
那既然内存是有限的,好比 redis 就只能用 10G,你要是往里面写了 20G 的数据,会咋办?固然会干掉10G 的数据,而后就保留 10G 的数据了。那干掉哪些数据?保留哪些数据?固然是干掉不经常使用的数据,保留经常使用的数据了。
(2)数据明明过时了,怎么还占用着内存?
这是由 redis 的过时策略来决定。
redis 过时策略
redis 过时策略是:按期删除+惰性删除。
所谓按期删除,指的是 redis 默认是每隔 100ms 就随机抽取一些设置了过时时间的 key,检查其是否过时,若是过时就删除。
假设 redis 里放了 10w 个 key,都设置了过时时间,你每隔几百毫秒,就检查 10w 个 key,那 redis 基本上就死了,cpu 负载会很高的,消耗在你的检查过时 key 上了。注意,这里可不是每隔 100ms 就遍历全部的设置过时时间的 key,那样就是一场性能上的灾难。实际上 redis 是每隔 100ms 随机抽取一些key 来检查和删除的。
可是问题是,按期删除可能会致使不少过时 key 到了时间并无被删除掉,那咋整呢?因此就是惰性删除了。这就是说,在你获取某个 key 的时候,redis 会检查一下 ,这个 key 若是设置了过时时间那么是否过时了?若是过时了此时就会删除,不会给你返回任何东西。
获取 key 的时候,若是此时 key 已通过期,就删除,不会返回任何东西。
答案是:走内存淘汰机制。
内存淘汰机制
redis 内存淘汰机制有如下几个:
手写一个 LRU 算法
你能够现场手写最原始的 LRU 算法,那个代码量太大了,彷佛不太现实。
不求本身纯手工从底层开始打造出本身的 LRU,可是起码要知道如何利用已有的 JDK 数据结构实现一个Java 版的 LRU。
class LRUCache<K, V> extends LinkedHashMap<K, V> { private final int CACHE_SIZE; /*** 传递进来最多能缓存多少数据 ** @param cacheSize 缓存大小 */ public LRUCache(int cacheSize) { // true 表示让 linkedHashMap 按照访问顺序来进行排序,最近访问的放在头部,最老访问的 放在尾部。 super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true); CACHE_SIZE = cacheSize; } @Override protected Boolean removeEldestEntry(Map.Entry<K, V> eldest) { // 当 map 中的数据量大于指定的缓存个数的时候,就自动删除最老的数据。 return size() > CACHE_SIZE; }}
其实问这个问题,主要是考考你,redis 单机能承载多高并发?若是单机扛不住如何扩容扛更多的并发?redis 会不会挂?既然 redis 会挂那怎么保证 redis 是高可用的?
其实针对的都是项目中你确定要考虑的一些问题,若是你没考虑过,那确实你对生产系统中的问题思考太少。
若是你用 redis 缓存技术的话,确定要考虑如何用 redis 来加多台机器,保证 redis 是高并发的,还有就是如何让 redis 保证本身不是挂掉之后就直接死掉了,即 redis 高可用。
因为此节内容较多,所以,会分为两个小节进行讲解。- redis 主从架构 - redis 基于哨兵实现高可用redis 实现高并发主要依靠主从架构,一主多从,通常来讲,不少项目其实就足够了,单主用来写入数据,单机几万 QPS,多从用来查询数据,多个从实例能够提供每秒 10w 的 QPS。
若是想要在实现高并发的同时,容纳大量的数据,那么就须要 redis 集群,使用 redis 集群以后,能够提供每秒几十万的读写并发。
redis 高可用,若是是作主从架构部署,那么加上哨兵就能够了,就能够实现,任何一个实例宕机,能够进行主备切换。
redis 若是仅仅只是将数据缓存在内存里面,若是 redis 宕机了再重启,内存里的数据就所有都弄丢了啊。
你必须得用 redis 的持久化机制,将数据写入内存的同时,异步的慢慢的将数据写入磁盘文件里,进行持久化。
若是 redis 宕机重启,自动从磁盘上加载以前持久化的一些数据就能够了,也许会丢失少量数据,可是至少不会将全部数据都弄丢。
这个其实同样,针对的都是 redis 的生产环境可能遇到的一些问题,就是 redis 要是挂了再重启,内存里的数据不就全丢了?能不能重启的时候把数据给恢复了?
持久化主要是作灾难恢复、数据恢复,也能够归类到高可用的一个环节中去,好比你 redis 整个挂了,而后 redis 就不可用了,你要作的事情就是让 redis 变得可用,尽快变得可用。
重启 redis,尽快让它对外提供服务,若是没作数据备份,这时候 redis 启动了,也不可用啊,数据都没了。
极可能说,大量的请求过来,缓存所有没法命中,在 redis 里根本找不到数据,这个时候就死定了,出现缓存雪崩问题。全部请求没有在redis命中,就会去mysql数据库这种数据源头中去找,一会儿mysql承接高并发,而后就挂了…
若是你把 redis 持久化作好,备份和恢复方案作到企业级的程度,那么即便你的 redis 故障了,也能够经过备份数据,快速恢复,一旦恢复当即对外提供服务。
redis 持久化的两种方式
经过 RDB 或 AOF,均可以将 redis 内存中的数据给持久化到磁盘上面来,而后能够将这些数据备份到别的地方去,好比说阿里云等云服务。
若是 redis 挂了,服务器上的内存和磁盘上的数据都丢了,能够从云服务上拷贝回来以前的数据,放到指定的目录中,而后从新启动 redis,redis 就会自动根据持久化数据文件中的数据,去恢复内存中的数据,继续对外提供服务。
若是同时使用 RDB 和 AOF 两种持久化机制,那么在 redis 重启的时候,会使用 AOF 来从新构建数据,由于 AOF 中的数据更加完整。
RDB 优缺点
AOF 优缺点
RDB 和 AOF 到底该如何选择
你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就必定会有数据一致性的问题,那么你如何解决一致性问题?#### 面试题剖析试题剖析
通常来讲,若是容许缓存能够稍微的跟数据库偶尔有不一致的状况,也就是说若是你的系统不是严格要求“缓存+数据库” 必须保持一致性的话,最好不要作这个方案,即:读请求和写请求串行化,串到一个内存队列里去。
串行化能够保证必定不会出现不一致的状况,可是它也会致使系统的吞吐量大幅度下降,用比正常状况下多几倍的机器去支撑线上的一个请求。
Cache Aside Pattern
最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。- 读的时候,先读缓存,缓存没有的话,就读数据库,而后取出数据后放入缓存,同时返回响应。- 更新的时候,先更新数据库,而后再删除缓存。
为何是删除缓存,而不是更新缓存?
缘由很简单,不少时候,在复杂点的缓存场景,缓存不仅仅是数据库中直接取出来的值。
好比可能更新了某个表的一个字段,而后其对应的缓存,是须要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。
另外更新缓存的代价有时候是很高的。是否是说,每次修改数据库的时候,都必定要将其对应的缓存更新一份?也许有的场景是这样,可是对于比较复杂的缓存数据计算的场景,就不是这样了。若是你频繁修改一个缓存涉及的多个表,缓存也频繁更新。可是问题在于,这个缓存到底会不会被频繁访问到?
举个栗子,一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;可是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。实际上,若是你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就从新计算一次而已,开销大幅度下降。用到缓存才去算缓存。
其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都从新作复杂的计算,无论它会不会用到,而是让它到须要被使用的时候再从新计算。像 mybatis,hibernate,都有懒加载思想。查询一个部门,部门带了一个员工的 list,没有必要说每次查询部门,都里面的 1000 个员工的数据也同时查出来啊。80%的状况,查这个部门,就只是要访问这个部门的信息就能够了。先查部门,同时要访问里面的员工,那么这个时候只有在你要访问里面的员工的时候,才会去数据库里面查询1000个员工。
最初级的缓存不一致问题及解决方案
问题:先更新数据库,再删除缓存。若是删除缓存失败了,那么会致使数据库中是新数据,缓存中是旧数据,数据就出现了不一致。
解决思路:先删除缓存,再更新数据库。若是数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。由于读的时候缓存没有,因此去读了数据库中的旧数据,而后更新到缓存中。
比较复杂的数据不一致问题分析
数据发生了变动,先删除了缓存,而后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变动的程序完成了数据库的修改。完了,数据库和缓存中的数据不同了...
为何上亿流量高并发场景下,缓存会出现这个问题?
只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题。其实若是说你的并发量很低的话,特别是读并发很低,天天访问量就 1 万次,那么不多的状况下,会出现刚才描述的那种不一致的场景。可是问题是,若是天天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的状况。
解决方案以下:
更新数据的时候,根据数据的惟一标识,将操做路由以后,发送到一个 jvm 内部队列中。读取数据的时候,若是发现数据不在缓存中,那么将从新读取数据+更新缓存的操做,根据惟一标识路由以后,也发送同一个jvm 内部队列中。
一个队列对应一个工做线程,每一个工做线程串行拿到对应的操做,而后一条一条的执行。这样的话一个数据变动的操做,先删除缓存,而后再去更新数据库,可是还没完成更新。此时若是一个读请求过来,没有读到缓存,那么能够先将缓存更新的请求发送到队列中,此时会在队列中积压,而后同步等待缓存更新完成。
这里有一个优化点,一个队列中,其实多个更新缓存请求串在一块儿是没意义的,所以能够作过滤,若是发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操做进去了,直接等待前面的更新操做请求完成便可。
待那个队列对应的工做线程完成了上一个操做的数据库的修改以后,才会去执行下一个操做,也就是缓存更新的操做,此时会从数据库中读取最新的值,而后写入缓存中。
若是请求还在等待时间范围内,不断轮询发现能够取到值了,那么就直接返回;若是请求等待的时间超过必定时长,那么这一次直接从数据库中读取当前的旧值。
高并发的场景下,该解决方案要注意的问题:
(1)读请求长时阻塞
因为读请求进行了很是轻度的异步化,因此必定要注意读超时的问题,每一个读请求必须在超时时间范围内返回。该解决方案,最大的风险点在于说,可能数据更新很频繁,致使队列中积压了大量更新操做在里面,而后读请求会发生大量的超时,最后致使大量的请求直接走数据库。务必经过一些模拟真实的测试,看看更新数据的频率是怎样的。
另一点,由于一个队列中,可能会积压针对多个数据项的更新操做,所以须要根据本身的业务状况进行测试,可能须要部署多个服务,每一个服务分摊一些数据的更新操做。若是一个内存队列里竟然会挤压 100 个商品的库存修改操做,每隔库存修改操做要耗费 10ms 去完成,那么最后一个商品的读请求,可能等待 10 *100 = 1000ms = 1s 后,才能获得数据,这个时候就致使读请求的长时阻塞。
必定要作根据实际业务系统的运行状况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操做,可能会致使最后一个更新操做对应的读请求,会 hang 多少时间,若是读请求在 200ms 返回,若是你计算事后,哪怕是最繁忙的时候,积压 10 个更新操做,最多等待 200ms,那还能够的。
若是一个内存队列中可能积压的更新操做特别多,那么你就要加机器,让每一个机器上部署的服务实例处理更少的数据,那么每一个内存队列中积压的更新操做就会越少。
其实根据以前的项目经验,通常来讲,数据的写频率是很低的,所以实际上正常来讲,在队列中积压的更新操做应该是不多的。像这种针对读高并发、读缓存架构的项目,通常来讲写请求是很是少的,每秒的 QPS 能到几百就不错了。
咱们来实际粗略测算一下。
若是一秒有 500 的写操做,若是分红 5 个时间片,每 200ms 就 100 个写操做,放到 20 个内存队列中,每一个内存队列,可能就积压 5 个写操做。每一个写操做性能测试后,通常是在 20ms 左右就完成,那么针对每一个内存队列的数据的读请求,也就最多 hang 一下子,200ms 之内确定能返回了。
通过刚才简单的测算,咱们知道,单机支撑的写 QPS 在几百是没问题的,若是写 QPS 扩大了 10 倍,那么就扩容机器,扩容 10 倍的机器,每一个机器 20 个队列。
(2)读请求并发量太高
这里还必须作好压力测试,确保恰巧碰上上述状况的时候,还有一个风险,就是忽然间大量读请求会在几十 毫秒的延时 hang 在服务上,看服务能不能扛的住,须要多少机器才能扛住最大的极限状况的峰值。
可是由于并非全部的数据都在同一时间更新,缓存也不会同一时间失效,因此每次可能也就是少数数据的缓存失效了,而后那些数据对应的读请求过来,并发量应该也不会特别大。
(3)多服务实例部署的请求路由
可能这个服务部署了多个实例,那么必须保证说,执行数据更新操做,以及执行缓存更新操做的请求,都经过 Nginx 服务器路由到相同的服务实例上。
好比说,对同一个商品的读写请求,所有路由到同一台机器上。能够本身去作服务间的按照某个请求参数的hash 路由,也能够用 Nginx 的 hash 路由功能等等。
(4)热点商品的路由问题,致使请求的倾斜
万一某个商品的读写请求特别高,所有打到相同的机器的相同的队列里面去了,可能会形成某台机器的压力过大。就是说,由于只有在商品数据更新的时候才会清空缓存,而后才会致使读写并发,因此其实要根据业务系统去看,若是更新频率不是过高的话,这个问题的影响并非特别大,可是的确可能某些机器的负载会高一些。
缘由很简单,不少时候,在复杂点的缓存场景,缓存不仅仅是数据库中直接取出来的值。
好比可能更新了某个表的一个字段,而后其对应的缓存,是须要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。
另外更新缓存的代价有时候是很高的。是否是说,每次修改数据库的时候,都必定要将其对应的缓存更新一份?也许有的场景是这样,可是对于比较复杂的缓存数据计算的场景,就不是这样了。
若是你频繁修改一个缓存涉及的多个表,缓存也频繁更新。可是问题在于,这个缓存到底会不会被频繁访问到?
举个栗子,一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;可是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。
实际上,若是你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就从新计算一次而已,开销大幅度下降。用到缓存才去算缓存。
其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都从新作复杂的计算,无论它会不会用到,而是让它到须要被使用的时候再从新计算。像 mybatis,hibernate,都有懒加载思想。
查询一个部门,部门带了一个员工的 list,没有必要说每次查询部门,都里面的 1000 个员工的数据也同时查出来啊。80%的状况,查这个部门,就只是要访问这个部门的信息就能够了。先查部门,同时要访问里面的员工,那么这个时候只有在你要访问里面的员工的时候,才会去数据库里面查询1000个员工。
2)最初级的缓存不一致问题及解决方案
问题:先更新数据库,再删除缓存。若是删除缓存失败了,那么会致使数据库中是新数据,缓存中是旧数据,数据就出现了不一致。
解决思路:先删除缓存,再更新数据库。若是数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。由于读的时候缓存没有,因此去读了数据库中的旧数据,而后更新到缓存中。
3)比较复杂的数据不一致问题分析
数据发生了变动,先删除了缓存,而后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变动的程序完成了数据库的修改。
完了,数据库和缓存中的数据不同了…
这个也是线上很是常见的一个问题,就是多客户端同时并发写一个 key,可能原本应该先到的数据后到了,致使数据版本错了;或者是多客户端同时获取一个 key,修改值以后再写回去,只要顺序错了,数据就错了。
并且 redis 本身就有自然解决这个问题的 CAS 类的乐观锁方案。
####### 面试题剖析 面试题剖析
某个时刻,多个系统实例都去更新某个 key。能够基于 zookeeper 实现分布式锁。每一个系统经过zookeeper 获取分布式锁,确保同一时间,只能有一个系统实例在操做某个 key,别人都不容许读和写。
你要写入缓存的数据,都是从 mysql 里查出来的,都得写入 mysql 中,写入 mysql 中的时候必须保存一个时间戳,从 mysql 查出来的时候,时间戳也查出来。
每次要写以前,先判断一下当前这个 value 的时间戳是否比缓存里的 value 的时间戳要新。若是是的话,那么能够写,不然,就不能用旧的数据覆盖新的数据。
看看你了解不了解大家公司的 redis 生产集群的部署架构,若是你不了解,那么确实你就很失职了,你的redis 是主从架构?集群架构?用了哪一种集群方案?有没有作高可用保证?有没有开启持久化机制确保能够进行数据恢复?线上 redis 给几个 G 的内存?设置了哪些参数?压测后大家 redis 集群承载多少QPS?
兄弟,这些你必须是门儿清的,不然你确实是没好好思考过。
redis cluster,10 台机器,5 台机器部署了 redis 主实例,另外 5 台机器部署了 redis 的从实例, 每一个主实例挂了一个从实例,5 个节点对外提供读写服务,每一个节点的读写高峰 qps 可能能够达到每秒 5 万,5 台机器最可能是 25 万读写请求/s。
机器是什么配置?32G 内存+ 8 核 CPU + 1T 磁盘,可是分配给 redis 进程的是 10g 内存,通常线上生产环境,redis 的内存尽可能不要超过 10g,超过 10g 可能会有问题。
5 台机器对外提供读写,一共有 50g 内存。
由于每一个主实例都挂了一个从实例,因此是高可用的,任何一个主实例宕机,都会自动故障迁移,redis 从实例会自动变成主实例继续提供读写服务。
你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。
其实大型的公司,会有基础架构的 team 负责缓存集群的运维。
对于系统 A,假设天天高峰期每秒 5000 个请求,原本缓存在高峰期能够扛住每秒 4000 个请求,可是缓存机器意外发生了全盘宕机。缓存挂了,此时 1 秒 5000 个请求所有落数据库,数据库必然扛不住,它会报一下警,而后就挂了。此时,若是没有采用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,可是数据库立马又被新的流量给打死了。
缓存雪崩的事前事中过后的解决方案以下:
用户发送一个请求,系统 A 收到请求后,先查本地 ehcache 缓存,若是没查到再查 Redis。若是 ehcache 和 Redis 都没有,再查数据库,将数据库中的结果,写入 ehcache 和 Redis 中。
限流组件,能够设置每秒的请求,有多少能经过组件,剩余的未经过的请求,怎么办?走降级!能够返回一些默认的值,或者友情提示,或者空值。
好处:
对于系统A,假设一秒 5000 个请求,结果其中 4000 个请求是黑客发出的恶意攻击。
黑客发出的那 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。
举个栗子。数据库 id 是从 1 开始的,结果黑客发过来的请求 id 所有都是负数。这样的话,缓存中不会有,请求每次都“绕过缓存”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。
解决方式很简单,每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,好比 set -999 UNKNOWN
。而后设置一个过时时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效以前,均可以直接从缓存中取数据。这种方式虽然是简单,可是不优雅,可能会缓存过多的空值,更加优雅的方式就是:使用bitmap
缓存击穿,就是说某个 key 很是热点,访问很是频繁,处于集中式高并发访问的状况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
不一样场景下的解决方式可以下:
缓存击穿重点在“击” 就是某个或者是几个热点key穿透了缓存层 缓存穿透重点在“透”:大量的请求绕过了缓存层
简单的讲:就是多客户端同时并发写一个 key,可能原本应该先到的数据后到了,致使数据版本错了;或者是多客户端同时获取一个 key,修改值以后再写回去,只要顺序错了,数据就错了。
并且 Redis 本身就有自然解决这个问题的 CAS 类的乐观锁方案,使用版本号进行控制,cas的思想这里就不详细说了。
在 Redis cluster 架构下,每一个 Redis 要放开两个端口号,好比一个是 6379,另一个就是 加1w 的端口号,好比 16379。
16379 端口号是用来进行节点间通讯的,也就是 cluster bus 的东西,cluster bus 的通讯,用来进行故障检测、配置更新、故障转移受权。cluster bus 用了另一种二进制的协议, gossip
协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。
集群元数据的维护有两种方式:集中式、Gossip 协议。Redis cluster 节点间采用 gossip 协议进行通讯。
集中式是将集群元数据(节点信息、故障等等)几种存储在某个节点上。集中式元数据集中存储的一个典型表明,就是大数据领域的 storm
。它是分布式的大数据实时计算引擎,是集中式的元数据存储的结构,底层基于 zookeeper(分布式协调的中间件)对全部元数据进行存储维护。
Redis 维护集群元数据采用另外一个方式, gossip
协议,全部节点都持有一份元数据,不一样的节点若是出现了元数据的变动,就不断将元数据发送给其它的节点,让其它节点也进行元数据的变动。
集中式的好处在于,元数据的读取和更新,时效性很是好,一旦元数据出现了变动,就当即更新到集中式的存储中,其它节点读取的时候就能够感知到;很差在于,全部的元数据的更新压力所有集中在一个地方,可能会致使元数据的存储有压力。
gossip 好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续打到全部节点上去更新,下降了压力;很差在于,元数据的更新有延时,可能致使集群中的一些操做会有一些滞后。
ping
消息,同时其它几个节点接收到 ping
以后返回 pong
。gossip 协议包含多种消息,包含 ping
, pong
, meet
, fail
等等。
Redis-trib.rb add-node 1
其实内部就是发送了一个 gossip meet 消息给新加入的节点,通知那个节点去加入咱们的集群。
ping 时要携带一些元数据,若是很频繁,可能会加剧网络负担。
每一个节点每秒会执行 10 次 ping,每次会选择 5 个最久没有通讯的其它节点。固然若是发现某个节点通讯延时达到了 cluster_node_timeout / 2
,那么当即发送 ping,避免数据交换延时过长,落后的时间太长了。好比说,两个节点之间都 10 分钟没有交换数据了,那么整个集群处于严重的元数据不一致的状况,就会有问题。因此 cluster_node_timeout
能够调节,若是调得比较大,那么会下降 ping 的频率。
每次 ping,会带上本身节点的信息,还有就是带上 1/10 其它节点的信息,发送出去,进行交换。至少包含 3
个其它节点的信息,最多包含 总节点数减 2
个其它节点的信息。
来了一个 key,首先计算 hash 值,而后对节点数取模。而后打在不一样的 master 节点上。一旦某一个 master 节点宕机,全部请求过来,都会基于最新的剩余 master 节点数去取模,尝试去取数据。这会致使大部分的请求过来,所有没法拿到有效的缓存,致使大量的流量涌入数据库。
一致性 hash 算法将整个 hash 值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织,下一步将各个 master 节点(使用服务器的 ip 或主机名)进行 hash。这样就能肯定每一个节点在其哈希环上的位置。
来了一个 key,首先计算 hash 值,并肯定此数据在环上的位置,今后位置沿环顺时针“行走”,遇到的第一个 master 节点就是 key 所在位置。
在一致性哈希算法中,若是一个节点挂了,受影响的数据仅仅是此节点到环空间前一个节点(沿着逆时针方向行走遇到的第一个节点)之间的数据,其它不受影响。增长一个节点也同理。
燃鹅,一致性哈希算法在节点太少时,容易由于节点分布不均匀而形成缓存热点的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每个节点计算多个 hash,每一个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡。
Redis cluster 有固定的 16384
个 hash slot,对每一个 key
计算 CRC16
值,而后对 16384
取模,能够获取 key 对应的 hash slot。
Redis cluster 中每一个 master 都会持有部分 slot,好比有 3 个 master,那么可能每一个 master 持有 5000 多个 hash slot。hash slot 让 node 的增长和移除很简单,增长一个 master,就将其余 master 的 hash slot 移动部分过去,减小一个 master,就将它的 hash slot 移动到其余 master 上去。移动 hash slot 的成本是很是低的。客户端的 api,能够对指定的数据,让他们走同一个 hash slot,经过 hash tag
来实现。
任何一台机器宕机,另外两个节点,不影响的。由于 key 找的是 hash slot,不是机器。
Redis cluster 的高可用的原理,几乎跟哨兵是相似的。
若是一个节点认为另一个节点宕机,那么就是 pfail
,主观宕机。若是多个节点都认为另一个节点宕机了,那么就是 fail
,客观宕机,跟哨兵的原理几乎同样,sdown,odown。
在 cluster-node-timeout
内,某个节点一直没有返回 pong
,那么就被认为 pfail
。
若是一个节点认为某个节点 pfail
了,那么会在 gossip ping
消息中, ping
给其余节点,若是超过半数的节点都认为 pfail
了,那么就会变成 fail
。
对宕机的 master node,从其全部的 slave node 中,选择一个切换成 master node。
检查每一个 slave node 与 master node 断开链接的时间,若是超过了 cluster-node-timeout * cluster-slave-validity-factor
,那么就没有资格切换成 master
。
每一个从节点,都根据本身对 master 复制数据的 offset,来设置一个选举时间,offset 越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举。
全部的 master node 开始 slave 选举投票,给要进行选举的 slave 进行投票,若是大部分 master node (N/2 + 1)
都投票给了某个从节点,那么选举经过,那个从节点能够切换成 master。
从节点执行主备切换,从节点切换为主节点。
整个流程跟哨兵相比,很是相似,因此说,Redis cluster 功能强大,直接集成了 replication 和 sentinel 的功能。
要是系统严格要求 “缓存+数据库” 必须保持一致性的话,能够使用:读请求和写请求串行化,串到一个内存队列里去。串行化能够保证必定不会出现不一致的状况,可是它也会致使系统的吞吐量大幅度下降,用比正常状况下多几倍的机器去支撑线上的一个请求。另外一种方式就是:Cache Aside Pattern
为何是删除缓存,而不是更新缓存?
缘由很简单,不少时候,在复杂点的缓存场景,缓存不仅仅是数据库中直接取出来的值。
好比可能更新了某个表的一个字段,而后其对应的缓存,是须要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。
另外更新缓存的代价有时候是很高的。是否是说,每次修改数据库的时候,都必定要将其对应的缓存更新一份?也许有的场景是这样,可是对于比较复杂的缓存数据计算的场景,就不是这样了。若是你频繁修改一个缓存涉及的多个表,缓存也频繁更新。可是问题在于,这个缓存到底会不会被频繁访问到?
举个栗子,一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;可是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。实际上,若是你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就从新计算一次而已,开销大幅度下降。用到缓存才去算缓存。
其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都从新作复杂的计算,无论它会不会用到,而是让它到须要被使用的时候再从新计算。像 mybatis,hibernate,都有懒加载思想。查询一个部门,部门带了一个员工的 list,没有必要说每次查询部门,都把里面的 1000 个员工的数据也同时查出来啊。80% 的状况,查这个部门,就只是要访问这个部门的信息就能够了。先查部门,同时要访问里面的员工,那么这个时候只有在你要访问里面的员工的时候,才会去数据库里面查询 1000 个员工。
问题:先更新数据库,再删除缓存。若是删除缓存失败了,那么会致使数据库中是新数据,缓存中是旧数据,数据就出现了不一致。
解决思路:先删除缓存,再更新数据库。若是数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。由于读的时候缓存没有,因此去读了数据库中的旧数据,而后更新到缓存中。
数据发生了变动,先删除了缓存,而后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变动的程序完成了数据库的修改。完了,数据库和缓存中的数据不同了…
只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题。其实若是说你的并发量很低的话,特别是读并发很低,天天访问量就 1 万次,那么不多的状况下,会出现刚才描述的那种不一致的场景。可是问题是,若是天天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的状况。
疯狂创客圈 - Java高并发研习社群,为你们开启大厂之门