首页 > 其他分享 >这就叫“面试造火箭,工作拧螺丝!”

这就叫“面试造火箭,工作拧螺丝!”

时间:2024-06-24 12:54:28浏览次数:16  
标签:面试官 这个 面试 Redis 业务 火箭 螺丝 消息 save

你好呀,我是歪歪。

我想再讨论一下上次的这篇文章《哎,被这个叫做at least once的玩意坑麻了》

因为有些朋友看完之后再评论区给出了自己的思考,也有朋友和我私聊,分享了自己的看法,我觉得有些想法很好,所以我决定一鱼两吃,再聊聊这个问题。

假设,我们是一场面试,面试官给你抛出了这样一个问题:

如果一个消费队列由于某些原因,对于某个消息发起了两次。导致一样的数据落库两条,请问你会怎么处理这个问题?

这题你一拿到手上,应该就立马能分析出是在问如何实现一个幂等机制。

想着这玩意我熟啊,张口就能给出方案:

业务消息 = select(业务唯一流水号);
if(业务消息 == null){
    save(业务消息);
}

面试官一听,提示道:你这个方案在多线程的情况下会不会有什么问题呢?

于是你的小脑瓜子立刻开始转了起来:先查询,再判断,最后保存。

如果两个线程同时过来,都查不到数据,那么就能都走到保存的逻辑里面去,确实拦不住。

于是你扣了一下脑壳,想起了你上家公司针对这个问题,就是在数据库的表结构里面,对业务唯一流水号做了唯一索引,所以不会出现重复插入的情况。

然后你给出了“加唯一索引”的方案,准备绝杀这个问题。

没想到面试官非常不懂事,还在继续追问:我想尽量不要让程序抛出异常,还有没有其他的方案呢?

Redis

你抱着自己的左手,边啃指甲边思考:唯一索引是数据库帮我们保证的逻辑,现在面试官这个老登不想让我用数据库来做这件事情。那就必须要控制在并发的场景下,只有一个请求能抵达数据库。

锁!这不就是锁干的事儿吗?

于是你飞快的又想到了一个方法:

flag = redis(业务唯一流水号,过期时间);
if(flag){
    save(业务消息);    
}

可以利用业务唯一流水号结合 Redis 来做一个锁,加锁成功的请求才能走到 save 逻辑中。

这样就能解决并发场景下,多个请求穿透到 save 逻辑这一步的问题。

面试官听到你这个方案之后,立马就启动了追问技能:如果放 Redis 成功了,但是还没来得及 save,服务重启了。

这个请求理论上是应该能再次发起的,但是由于 Redis 锁的存在,导致不会走到 save 的逻辑去,怎么办呢?

于是你又扣了一下脑壳,想起你在上家公司的时候,好像也遇到过这个情况。

当时的解决方案就是人工介入,分析了一波数据,确认了这个消息确实应该被继续处理,于是你找 DBA 帮忙删除了 Redis 对应的 key,流程就通了。

然而这个回答面试官并不满意:人工就显得不优雅了,要不再想想?

你又抱着自己的右手,边啃指甲边思考:这个老登考虑的确实挺多的,感觉应该在一个很厉害的团队,我得加把劲儿,再想想。

现在要人工介入的原因,是因为我们把第二次的请求拦截住并丢弃了。

如果不丢弃,那么理论上在“过期时间”到了,锁被释放后,第二次的请求拿到锁,就能接着往下走。

所以,这里需要在 Redis 这里加一个加锁失败则等待的逻辑:

flag = redis(业务唯一流水号,过期时间,获取不到则等待);
if(flag){
    save(业务消息);    
}

但是你一看这个逻辑又不对了:由于有锁等待的逻辑,那么如果两个请求过来,还是有可能会都放入到 Redis 里面,flag 都会为 true,那么 save 方法还是会走两遍。

所以,还得在获取锁成功之后加上一个查询数据库的逻辑:

flag = redis(业务唯一流水号,过期时间,获取不到则等待);
if(flag){
    业务消息 = select(业务唯一流水号);
    if(业务消息 == null){
        save(业务消息);    
    }
}else{
    //等待结束后还是未获取到锁,发送预警
    monitor(预警信息);
}

第一层的 Redis 相当于让请求排队,确保只有一个请求进来。

第二层的 select 才是真正的防止重复的业务逻辑。

同时,如果等待结束后还是未获取到锁,出现这种低概率情况,就预警出来,人工兜底嘛,一旦人工介入,那就是能解决任何问题。

你心想这波应该是稳了,应该是可以换题了。

然而面试官并不打算在这个回合上轻易放过你:这个方案确实是可以解决这个问题,但是在技术实现上引入了 Redis 框架,如果我不使用 Redis,单纯的靠 MySQL 呢?

回到 MySQL

听到这个问题的时候你觉得不对啊,最开始的时候不就是说了“加唯一索引”就可以解决这个问题吗?

于是面试官补充了一下描述:

最开始的加唯一索引是基于业务表来做的,如果出现问题就让其抛出主键冲突异常,这个方案确实是可以实现需求。但是我现在想让你给我设计一个通用的技术组件,不需要基于某个具体的业务场景去设计。我想听听你的思路。

拿到新的题目,你开始觉得这是***难,看着面试官求知的眼神,你又开始怀疑:这个老登不会是来套方案的吧?

看着自己已经被咬秃了的左右大拇指指甲,感觉自己的灵感和指甲一样都光秃秃的。

开始后悔前面几个回合咬得太快了,原以为可以秒杀这个面试,没想到面试官还在缠斗。你动了使用必杀技来结束战斗的念想。

于是从帽子的缝隙中插进入一根指甲已经秃了的手指,在差不多秃了的头顶,用指腹画圈,给自己头皮按摩,医生说这样的有助于毛囊发育,你想着头发还会长出来,就思如泉涌,这就是必杀技。

你陷入了思考,Redis 在前面的方案中是为了防止有多条数据穿透到 save 方法中去,如果不让用 Redis。MySQL 怎么实现类似的效果呢?

也加锁吗?for update?

业务消息 = select(业务唯一流水号);//select *** for update
if(业务消息 == null){
    save(业务消息);    
}

这玩意一看上去就是性能就拉胯了,为了解决这个偶发的问题,牺牲了接口的性能,这个路线就走的有点远了。

而且这个上锁的逻辑隐藏的有点深,容易留下后患,面试官肯定不会满意的。

那还有什么办法,能把 MySQL 当作锁来用,确保并发情况下只有一个请求能穿过这个锁呢?

那还是得靠唯一索引的约束才行。

但是这个唯一索引面试官不让用业务表的,那就只能直接搞个“消息消费记录表”,里面有个“消息唯一标识”的字段,这个字段是唯一索引。

这张表面试官问起来,我就说这张表是完全独立于业务的存在,只是为了解决消息幂等这个存粹的技术问题而出现的,基于它,我们就可以设计出一个通用的技术组件,这样应该说的过去。

表有了,技术方法大概的雏形就有了。

然而你还不能开始答题,现在思路还不是特别清晰,你要把方案捋清楚了再张口。

在不知不觉间,你的指腹已经摩擦的有点麻木了,于是你换了一个手,穿过帽子,接着按摩着自己的头顶。

这个表我怎么用呢?

if(保存数据到消息消费记录表){//出现主键冲突就返回false
    save(业务消息);    
}

先校验,再保存,非原子性,这样肯定不行啊,

我们想想一个场景,如果保存数据到消息消费记录表成功,还没来得及 save(业务消息) ,服务重启了,怎么办?

所以为了保证原子性,我们可以加入事务,把这两步绑定到一起:

开启事务;
if(保存数据到消息消费记录表){//出现主键冲突就返回false
    save(业务消息);    
}
提交事务;

这样,如果保存数据到消息消费记录表成功,还没来得及 save(扣款信息) ,服务重启,事务回滚,消息消费记录表就不会真的插入成功。

而 MQ 没有收到这个消息的回执,也会再次进行投递。

由于消息消费记录表里没有这个数据,所以会再次进行消费。

现在你觉得似乎没啥问题了,刚想给面试官说你这个思路,但是立马又想到了另外一个问题:通过引入事务来解决了“非原子性”的问题,但是事务这玩意,一般来说,大家都是能不使用事务的地方就尽量不使用事务,通过最终一致性来保证数据的完整性。

这个老登肯定会在这个地方继续穷追猛打的,我先预判了他,想想这个问题怎么解决。

我们可以在消息消费记录表里面再引入一个“状态”字段,这个字段有两个取值:消费中、消费完成。

同时把唯一索引改成“消息唯一标识+状态”。

首先,MQ 发起请求,数据往消息消费记录表插的时候,状态直接就是“消费中”。

如果插入成功,则说明是第一次消费,进入到业务逻辑中去。

  • 如果业务逻辑执行成功,则更新消息消费记录表对应数据为“消费完成”。
  • 如果业务逻辑执行失败,则删除消息消费记录表对应数据,把消息仍回 MQ,等待下次重试。

如果插入失败,则说明是重复消费,直接扔掉。

画成流程图上大概是这样的:

顺便提一嘴,上面这个流程图我是用这个网站直接生成的,我觉得这个网站画图还挺舒服的:

https://excalidraw.com/

你感觉这波应该稳了,于是给面试官说出了自己的方案,并在白字上画了流程图。

面试官拿着你的流程图,看了一眼,立马就看出了一个问题:如果一个消息插入失败,你的逻辑是扔掉。那假设这条消息的状态是消费中,业务逻辑执行失败,是不是应该重新消费才对呢?

于是你立马反映过来,如果插入失败,则说明是重复消费,还需要判断数据的状态。

  • 如果状态是“消费成功”,则说明重复请求,直接返回成功
  • 如果状态是“消费中”,则说明还未处理完成,为了确保成功,需要把请求再次仍回到 MQ。

修改了流程图:

面试官拿着这个流程图,微微一笑:

倘若我业务执行完之后,状态更新之前,服务挂了,阁下又该如何应对?

巧了,这个问题上一篇文章的评论区也提到了:

所以,还需要针对长时间在“消费中”的数据进行一个监控,人工兜底一下。

此外,为了防止“消费完成”的数据量过多,还应该对于这个状态的数据做一个定时清理的任务。

终于,你看到了面试官脸上那一闪而过的满意表情,在你觉得面试官应该会放过你了的时候,他又提出了另外的问题:

你这个通用组件理论上确实是可行的。

但是,这张表放在哪个库的哪个表里呢?

是统一放在一个库里呢还是就放在业务服务的库里呢?

统一放一个库的话太大了怎么办呢是不是要按日期分表?

万一跟业务库用的数据库不是一个数据库产品那事务不生效咋办呢?

放在业务库里的话万一业务服务连好几个库那我具体放哪一个呢?

是不是所有业务库我都得加这么一张表强制绑架他们的数据库?

...

这一部分问题,也来自上一篇文章评论区。

听到这些问题,你开始觉得这个面试官是在胡搅蛮缠,一气之下,准备拿回简历,结束面试。

但是手上动作稍微大了一点,一不小心掀起了自己的帽子,漏出了“资深的发型”。

面试官也愣住了,看着你“资深的发型”,当即就握住了你的手:你就是我要找的人才。不面了,就你了,明天来报道!

入职

入职之后你第一件事情就是看看这个公司的代码。

当你看第一个接口的时候,发现根本没有做幂等。

当你看第二个接口的时候,发现就是靠业务表的唯一索引做的幂等。

当你看第三个接口的时候,Redis 的方案跃然纸上。

突然一个哥们气喘吁吁的跑过来找昨天面试你的老登,说:快,又出问题了,帮忙删除一个 Redis key。

于是,你抽过去准备看一下怎么操作。

不经意间看到了老登正在写一个文档,题目叫做《一种分布式系统中数据唯一性的消息幂等保障策略》。

老登看到你过来了,说:正好,你来写这个文档,我已经把名字给你想好了,你就按照这个写,把你昨天的思路写清楚,到时候我去汇报。

你兴奋的问:汇报过了之后我们要按照这个方案落地吗?

老登说:不不不,落地干啥啊,多麻烦啊,方案汇报嘛,体现一下我们在技术方面的时刻,在领导面前去刷个脸,所以你要多用一些高大上的词,越晦涩难懂越好。哦,对了,我顺便教教你怎么“删除 Redis key”,以后就让他们找你了。这帮老登,大半夜的,老是给我打电话。

标签:面试官,这个,面试,Redis,业务,火箭,螺丝,消息,save
From: https://www.cnblogs.com/thisiswhy/p/18264822

相关文章

  • 计算机网络面试基础知识基础篇
    TCP/IP网络模型有哪几层?问大家,为什么要有TCP/IP网络模型?对于同一台设备上的进程间通信,有很多种方式,比如有管道、消息队列、共享内存、信号等方式,而对于不同设备上的进程间通信,就需要网络通信,而设备是多样性的,所以要兼容多种多样的设备,就协商出了一套通用的网络协议。这个......
  • Kubernetes面试整理-解释Etcd在Kubernetes中的作用,包括如何管理配置数据和状态信息
    etcd是一个分布式的键值存储系统,在Kubernetes中起着至关重要的作用。它主要用于存储集群的所有配置数据和状态信息,确保这些数据的一致性和高可用性。具体来说,etcd在Kubernetes中的作用如下:etcd的作用● 配置存储:etcd存储Kubernetes集群的所有配置信息,包括节点......
  • java 并发编程面试(1)
    一、单例模式的DCL为啥要加volatile?避免指令重排,获取到未初始化完成的对象。单例模式的懒汉模式确保线程安全的机制DCLpublicclassMyTest{privatestaticMyTestmyTest;publicstaticMyTestgetInstance(){if(myTest==null){//check......
  • 测试面试题
    冒烟测试是什么意思呀?冒烟测试(SmokeTesting)是一种初步的测试,主要是用来验证软件的基本功能是否正常运行。就像在买一个电器之前,先插电看它是否能启动一样,冒烟测试会检查软件最重要的功能是否工作正常,如果基本功能有问题,测试就不会继续深入。你们公司的项目流程是什么呀?每......
  • Linux gdb lldb面试题及参考答案(万字长文)
    什么是GDB?简述其主要功能。GDB(GNUDebugger)是GNU项目的一部分,是一个功能强大的源代码级别的调试器,主要用于C、C++和其他多种编程语言的程序调试。GDB提供了丰富的功能来帮助开发者理解程序内部的工作方式,诊断并修复代码中的错误。其主要功能包括但不限于:启动程序:可以在GDB......
  • 【气动学】三级火箭发射弹道主动段仿真(三次点火达到目标轨道)【含Matlab源码 4711期】
    ⛄一、获取代码方式获取代码方式1:完整代码已上传我的资源:【气动学】基于matlab三级火箭发射弹道主动段仿真(三次点火达到目标轨道)【含Matlab源码4711期】点击上面蓝色字体,直接付费下载,即可。获取代码方式2:付费专栏Matlab物理应用(初级版)备注:点击上面蓝色字体付费专栏......
  • 【面试宝典】28道Java集合高频题库整理(附答案背诵版)
    常见的集合有哪些?常见的Java集合可以分为两大类:Collection和Map。Collection接口下主要有以下几种实现:List:有序的集合,其中的元素可以重复。ArrayList:基于动态数组实现,查询速度快,但在中间插入和删除元素时速度较慢。LinkedList:基于双向链表实现,插入和删除速......
  • 面试官:告诉我为什么static和transient关键字修饰的变量不能被序列化?
    一、写在开头在上一篇学习序列化的文章中我们提出了这样的一个问题:“如果在我的对象中,有些变量并不想被序列化应该怎么办呢?”当时给的回答是:不想被序列化的变量我们可以使用transient或static关键字修饰;transient关键字的作用是阻止实例中那些用此关键字修饰的的变量序列化;当......
  • 【2024最新精简版】分布式事物面试篇
    文章目录在你的项目中哪些模块使用了分布式事务控制?能否举例说明?说一说SeatAT模式的工作原理?说一说SeatXA模式的工作原理?说一说SeatTCC模式的工作原理?什么是TCC模式的业务悬挂和空回滚?如何解决业务悬挂和空回滚?更多相关内容可查看在你的项目......
  • 【2024最新精简版】线程安全/多线程 面试篇
    文章目录一.线程基础线程和进程什么是进程什么是线程并发与并行的区别创建线程继承Thread类实现Runable接口实现Callable接口使用线程池线程状态等待唤醒机制等待方法唤醒方法二.线程池线程池作用创建线程池线程池任务调度流程阻塞队列BlockingQueue线程池拒绝策......