前传
在之前王哥的辅助之下,小明的简历成功被内推进到了王哥所在公司。由于一面就是王哥自己,所以简单聊聊了便过去了。接下来,二面的面试官来了。
二面面试官看上去比较年轻的消瘦,戴着一副眼镜,头发比较稀疏,看上去像是有十多年经验的样子,两人在一处安静的地方坐了下来,开始了第二轮面试。
面试官:嗯嗯,你好,请先简单自我介绍一下自己吧。
小林:嗯嗯,你好,我是XXXX,之前在XXX(此处省略200字介绍)
面试官点了点头,一副迷之微笑的表情,然后低头过了一遍简历的内容。直到看见了 “负责过电商系统的秒杀项目后端开发模块,并且对于缓存有过较为深入的研究”这么一行字眼。
面试官:我对你这上边写的秒杀项目和缓存研究有些感兴趣,想深入了解一下其中的细节点。
小林:嗯嗯,好的。
面试官:先从简单问起吧,在秒杀业务场景中,你在下单的时候是如何防止库存超卖发生的呢?
小林:嗯嗯,首先在活动开始之前,我们会进行一次商品库存的预热处理,将数据库的信息加载到redis中,然后扣除库存的时候会在redis里面进行处理,最后当库存为0的时候,会触发一个方法去触发关闭前端秒杀活动的开关。
每次当有用户提交下单请求的时候,会先将请求通过mq来进行削峰,在进行扣除库存的时候,会先更新redis里面的库存量,最后再统一更新db。
面试官:为啥不直接更新db呢?
小林:像秒杀这种典型的高并发场景,直接对db层进行写操作对数据库的访问压力实在是太大了,并发量过大容易压垮数据库。
面试官:嗯嗯,那为什么要把库存存储在redis中呢?
小林:具体有两个原因,首先第一点, redis的并发承载能力足以应付我上家公司的秒场景所需。还有一点非常重要的就是,redis是单线程模型,在做库存减1操作的时候不会出现数据竞争导致商品超卖的情况发生。
面试官似乎对小林说的这个redis单线程模型感到一些兴趣了,于是又接着深入展开了对于缓存部分的提问。
面试官:等等,既然你说了redis的并发承载能力强,但是又说是单线程模型,能解释下为什么单线程模型下redis也能有较好的性能承载能力呢?
小林在面试之前正好准备了这方面的知识点内容,于是便拿出了自己以前所学习过的知识内容点进行了讲解。
小林:redis采用了非阻塞网络IO模型,适合用于快速地操作逻辑。所谓的非阻塞网络io模型,这有点类似于java里面的nio。当有多个请求发送到服务端的时候,实际上会有一个文件事件处理器同时监听多个套接字,并且根据套接字目前执行的任务来关联不同的事件处理器。
这些不同的套接字用于给事件处理器将其分发给不同的逻辑程序处理,事件处理器只需要将它们做绑定即可。这些处理事件可能会并发地出现,但是io多路复用程序是会将所有产生的套接字都存入一个 有序且同步的队列中(单线程的核心点),最后redis会有逐一地对这个队列中的元素进行处理。
这里就是为啥单线程的原因。在一开始学习这块知识点的时候,为了更好地深入理解,我去用了nio程序来做比对。
不同的套接字事件对应的处理器也听类似的,例如说accept,read,write等事件,应对不同连接的时候处理逻辑也不同。
我当时是结合了实际业务来进行设计的,由于商品有多种,因此对于商品的库存数目采用了key-value的结构,按照商品的id作为key,库存作为value存储。对于库存的减少是采用了decr指令操作,这条指令实际上是一条原子性操作,之所以原子性操作是因为redis的单线程特性。
面试官:嗯嗯,既然上边你解释到了redis是单线程模型,那么在使用redis的时候需要注意些什么吗?或者说对于redis的存储有什么优化技巧可以讲解下吗?
这时候小林想起了之前王哥发给他的一份面试笔记,上边记录了很多关于redis的知识点,其中就有提及到过这一点。
小林:嗯嗯,在redis6.0之前,redis还是处于一个单线程的状态,我们就拿单线程版本的redis来说吧。
拒绝bigkey的出现场景首先key的值不宜设置地过大,尽量保证简洁明了,减少对于内存的占用。通常来说,当一个单独存储的value值大于10kb的时候就会被认为是bigkey了。
实际上在redis的内部实现中, 对于 set key "some string" 这样的指令而言,底层的c语言会自己构建一个称为SDS的结构体(类似java里面的类对象)进行存储。这个结构体包含了下边信息:
struct sdshdr { int len; int free; char buf[]; }
内部没有直接采用c语言自带的字符串,好处有以下几点:减少原先繁琐的内存扩增问题。(会根据初始化的值,提前给出更多的空间,避免出现空间溢出问题)
通过空间预分配机制来减少内存重分配问题。(其实内存重分配是一个非常复杂的过程,需要惊动到os操作系统层面的修改, 其中涉及到了非常复杂的操作,因此sds在初始化过程中尽量帮我们把这块给优化处理) 针对bigkey而言,其实还有很多点可以注意;
1.对于hash,list,set,zset这类数据结构而言,尽量不要让其数目超过5000个。假设我们存储了一个大小为100万元素的zset数据结构到redis中,并且设计了1小时过期的机制,那么在元素到期时候触发 了删除操作,这将会对redis自身造成堵塞。
2.如何避免上述在删除过程中的堵塞情况?首先应该从根源上避免这类设计的存在,如果实际线上数据库存在这类数据信息,那么可以结合redis自身提供的机制 异步 删除机制 (需要redis4.0之后才具有)
3.bigkey是如何产生的?常见的产生bigkey场景:1)例如一些社交类产品,粉丝列表,为了减少对于db的访问,会根据注册用户的id来绑定相关的list结构存储粉丝信息,假若遇到了某些明星,大v,那么如果这个list没有做过相关的调优处理就很容易转换为一个bigkey。2)假设用list来存储用户缓存信息,当访问量增加的时候也很容易产生bigkey。3)将相关的数据存储到redis的一些复杂数据结构中(list,set相关类型)的时候,需要考虑,是否每个存储项的字段都有必要存入,如果是无关必要的字段则可以忽略掉。
4.如何对bigkey做优化处理?如果线上已经有存在这种情况的话,不建议直接暴力删除,最好是通过一些拆解手段来做平滑过滤。例如说一个list拆解为多个list1,list2,list3,如果是个map的话也是可以拆解为多个小map,另外提取元素的时候不要随意用hgetall这类占用网络带宽资源的指令。
面试官:嗯嗯,那你们之前的项目组里面会有做一些禁止命令的设置吗?防止某些不安全指令在线上环境产生造成危害。
小林:额,这个我就不是很清楚了,应该是要有的,平时没有太过注意。
面试官:嗯嗯,其实我们这边的生产环境是会精致实用keys,flushall,flushdb这类命令的,主要是通过rename机制来禁用掉它。
ps:可以借助redis内部的rename机制关闭危险指令的使用
通过修改redis.conf中的SECURITY项,在里头新增以下几行,即可实现对危险指令的禁用 rename-command FLUSHALL "" rename-command FLUSHDB "" rename-command CONFIG "" rename-command KEYS "" 对于命令的查询推荐通过scan来替代keys
面试官:看来你对于redis还是有些研究的啊。那么你能讲解下更深一些层次的缓存吗,例如说cpu层面的缓存管理机制?
小林此时突然脑袋一片空白,在面试之前并没有对操作系统底层的知识做过复习,一下子懵逼了。小林:这块我不是太了解。。面试官:哦,那我简单和你讲解下吧。
cpu内部的其实经常会需要用到内存中的数据做运算和读写操作,但是cpu的计算性能和内存的计算性能差距非常巨大,针对这类密集型计算的物件,后续人类发明了“缓存”的概念。早期的时候人们只发明了一级缓存,后来又增加了L2,L3级别的缓存。
将缓存分为了L1,L2,L3,其速度值大小为L3<L2<L1,当cpu需要获取数据的时候会先从自己的寄存器中提取数据,然后再从L1中查询,L1都能查询的缓存数据若没有命中,则会返回到L2查询,如果L2也查询不到就会追溯到L3查询,通常情况下L3中能够命中80%的数据信息。L3如果没有命中数据则会到内存里面查询。
为什么要分这么多级的缓存?因为不同级别的缓存速度差异都巨大,运算越快的缓存制作难度越高,成本也越高。
为什么不把java程序存储到cpu的多级缓存中呢?别逗了,cpu的多级缓存是数据内核层面的东西,java存储的数据是属于jvm虚拟机层面的玩意,两者根本不在一个层面上,而且cpu的多级缓存存储空间相对于内存而言也非常小。
在面对这么多级的缓存数据中,如何保证查询数据的正确和有效呢?于是便有了一个叫做MESI的缓存一致性协议。
听了面试官的简单介绍后,小林似乎发现自己还是有部分知识体系存在不足之处,连上露出了尴尬而不失礼貌的微笑。
面试官: 好吧,那我们继续。你可以讲解下之前自己在秒杀项目中使用到的缓存设计方案吗?例如说一些机器的部署方面?
小林:emmmm,让我思考一下先(脑海中疯狂回忆以前学过的笔记内容点和工作接触点) (一两分钟后....) 嗯呢,我思绪准备好了。之前的项目中采用的是sentinel架构来进行设计redis存储。
sentinel一共部署了三台机器,一台作为主机,另外两台作为从机器,每台机器上边都设立了一个哨兵的角色,当主节点出现异常的时候会有从节点来顶替。
以前在预发布环境曾经遇到过一个问题,当时是做容灾模拟演练,当日模拟中将redis主节点和从节点之间的网络做了截断操作,导致从节点的机器一直没有和主节点的机器进行网络通信,于是此时便从从机器中选定了一台机器作为主节点,在主从做切换期间redis服务曾经出现过异常中断服务的情况。
面试官:嗯嗯,那么请你讲解下redis主从切换过程中可能会遇到哪些生产的异常问题呢?
小林:嗯嗯,我大概知道那么几个常见的场景吧。例如说在一些结合redis用的分布式锁,在这一时刻可能会失效,假设说秒杀活动的高峰期,主节点挂了,那么分布式锁就会失效,可能会引发后续一连串可怕的事情发生,因此对于接口的压测和限流是非常重要的。
emm,还有的话例如说一些知识在redis中存储并没有实际落入db做持久化的数据也会丢失,假设一些购物车中存放的数据,可能会在主从切换中的那段时间里面突然发现 "加入购物车" 失效了!
面试官:嗯嗯,那么你对于主从切换中的选举原理了解吗?可以简单介绍下吗?
小林:emmm,这块并不是特别了解。
ps:关于redis的sentinel架构采用到的raft选举算法考点 https://blog.csdn.net/sanwenyublog/article/details/53385616
面试官:好吧。那你在做redis设计的时候主要的目的还是为了防止请求进入到db层面,在这方面还有哪些细节点也需要注意到的吗?
小林 : 需要注意一下缓存的过期时间,假设某些热点数据是同时存入到redis的话,那么它们的过期时间最好是能够做成随机值,防止出现时间到达后缓存大面积失效,导致缓存击穿的情况大规模发生。
面试官:嗯嗯,关于缓存的模块大概就先问到这里吧。你还有什么需要问我吗?
看来这次面试似乎小林在面试官前的表现已经达到了入职的技术要求,距离成功上岸似乎还差一步之遥的感觉。
小林:嗯嗯,请问我还有机会吗?
面试官:你先回家等下通知吧,我们这边和hr商量一下再做决定。