导言

追忆四年前:一段关于我被外企CTO用登录注册吊打的不堪往事

是的,诸位没有看错,这篇文章的要叙述的并不是我吊打面试官,而是一段我被面试官吊打的陈年往事,这段苦楚的记忆在我脑海中长久不衰,也是一个我心里曾屡次不肯面临的现实,各位看官可以准备好一小把瓜子,听我将这则旧事缓缓道来~

写这篇文章的缘由是由于年前有不少小伙伴主张我写个2022年终总结,但我自己比较排挤写总结这类的,不过在脑海里回想近几年的技术生涯时,忽然想起了这起风趣的作业,因而将其稍做同享,期望诸君认真读完本文之后,也可以给咱们带来一些考虑!

其实我与诸位一样,几年前的我排挤、甚至讨厌学习,究竟常识从脑子过一遍后,一点也留不住的感觉我并不喜爱。但也是由于这次阅历,才让我改过自新,从一个不爱考虑的“小码农”,变成了现在的“码农Plus”,好戏开场!

由于近期作业较忙的原因,其实这篇文章,是从上一年终究一天,也便是20221230日开端,一向写到现在才完结的文章,标题也被我从原本的「回忆三年前」改成了「回忆四年前」,哈哈哈~

一、“被吊打作业”的来龙去脉

在正式谈及这次“被吊打作业”之前,首要来聊一聊此次作业的来龙去脉,作业的起因源自于我太过自傲,刚结业的那段时刻,本故事的主人公,也便是我,通过一些特别手法,成功入职了一家从事教育软件开发的小企业。当然,你要问我什么样的特别手法能让刚结业的我,无需面试就进入了一家软件企业,那便是大名鼎鼎的面试秘法——走后门

由于我的一位亲戚,在这家企业担任等级不算低的“高管”,因而我靠常人不能及的手法成功入职,没错!俺是一个妥妥的联系户,也正式由于这个原因,所以入职后的作业使命并不算重,饭点前、下班前,冲在榜首个的永久是我,究竟实力摆在这,不嚣张点简直对不住我的身份,哈哈哈~

总归在入职榜首份作业的韶光中,我大致算整个研制部门中最轻松的那个,大致与沸点摸鱼区那群家伙的作业量相似[狗头],由于作业轻松,所以给了我不少摸鱼学习时刻,也正是通过这些时刻,我在忙完作业之余的时刻内,自身也额定学习了不少Java技术。

其时我的技术大致是什么水平呢?这儿我从招聘软件上将我开端的简历信息摘过来了,如下:

追忆四年前:一段关于我被外企CTO用登录注册吊打的不堪往事

2019年的时分,我其时的简历是这样的,凡是我开端听到过的干流技术,底子上都去做了相关学习,并且一半以上在开发中都用过,由于其时的研制模式归于低代码定制开发,因而核心渠道的功用代码中,涉及到的新技术也蛮多,所以开端我有入神之自傲,自以为技术到达了 很牛逼 的程度。

正是由于其时这份简历,给予了自己极大的自傲,再加上成也萧何败也萧何,由于联系户的原因,我入职额定轻松,但也正由于是联系户,所以极大程度上约束了自己的生长空间,也便是抹不开面子去提涨薪,因而终究我做出了一个决议:“大丈夫生于天地之间,岂能郁郁久居人下”

没错,其时的我毅然决然的“提桶跑路”了!提出辞去职务之后,在诸多的劝止中,头也不回的卷铺盖走人,没有其他原因,完全归咎于个人对自己技术的自傲!其时跑回了老家玩了一段时刻后,想着男儿膝下有黄金,是时分该有一番作为了!接着我去到了间隔老家最近的省会城市,从此踏上了额定自傲的面试之旅(上份作业不在老家的省份,这也是离任原因之一,玩心重,朋友都不在身边~)

从这段回忆中,咱们应该可以感受出我其时的心态,用一个词去描述特别恰当,也便是“年少轻狂”,沉浸在自己的认知中,换其时的心思,假如非要找一个字来描述的话,那便是“我技术很屌”!哈哈哈,现在想起来感觉有些许天真,但开端的我确实便是这个心态,因自以为的技术飞速提升,造就了其时心里十分膨胀的我。

二、“被吊打作业”的正戏开场

追忆四年前:一段关于我被外企CTO用登录注册吊打的不堪往事

2019年国庆后的某个下午,阳光明媚,昂首望去,天空万里无云,也正是在这个时刻点上,一位身着白色T恤的帅小伙,正在卖力的蹬动双腿:
追忆四年前:一段关于我被外企CTO用登录注册吊打的不堪往事

别看了,蹬车的不是这双腿,而是我那长达一米四的大长腿~!其时盛行骑同享单车,在我约好面试后,就按约好的时刻骑车赶往面试现场,通过近半小时的不懈努力,我成功在约好时刻前赶到了,首要接待我的是一位人事小姐姐,在走完面试前的一些流程后,随即就喊来了一个技术老哥。

担任榜首轮面试我的老哥,在简略看完我的简历后体现的很有兴趣,大约同我聊了有四五十分钟的时刻,当然,这并不是本次的主题内容,所以按下快进键:

  • 先简略问了一些关于Java根底的内容,如面向对象、集合、多线程、特性….
  • 接着问了最近做的两个项目,全体的事务内容、核心技术、个人担任的技术作业….
  • 然后又问了一些JVM相关的常识,如内存区域、废物回收、类加载机制、即时编译…..
  • 接着又聊了一些常用开源框架,如Spring事务原理、MVC作业流程、AOP的应用场景…
  • 然后又谈了一些关于散布式体系的技术,如散布式体系的一些处理计划、常用的中间件技术….
  • 终究探讨了一下MySQL的常识,也便是索引、事务、锁的原理,以及SQL优化、功用优化….

榜首轮技术面试中,在我的记忆中答复的还算不错,包含其时担任一面的技术老哥似乎也挺满意,在聊完技术过了一瞬间之后,他就带着我的简历去了人事部,紧接着又是开端的那位人事小姐姐,出来领着我往一个房间走去……

由于其时房间内还在说话,所以我在门口稍等了一小会儿,但没过多久,我就被喊了进去,进门后映入我眼帘的,是一位梳着成功人士发型、穿戴黑衬衫、打着小领带、并且面容较为帅气的男人,从面相上看应该在27、28岁左右,其时给我的榜首形象并不是油腻,而是一位很注重形象、并且风貌气度不凡的老哥,他!便是本文的主角人物!

后来我从人事经理口中得知,他便是这个分公司的技术总担任人,也可以被称之为CTO、技术总监啥的,当然,在这儿我形象最深入的并非是他的长相和气质,而是那张超大规模的真皮沙发!究竟我进门听到的榜首句,便是他朝我说:“别紧张,过来这儿坐”!我其时坐在真皮沙发上的榜首感觉便是:真软!

三、“被吊打作业”的来龙去脉

  • 前奏:先是简略的攀谈,通过一番问寒问暖之后,总算正戏上演了!
  • 技术总监:看了一下你的简历还不错呀,跟我聊聊你最近做过的这个项目吧。
  • :叭啦叭啦叭…..一顿介绍。
  • 技术总监:说说你在这个项目中,首要担任哪块开发呢?
  • :个人参与了该项意图不少核心功用开发,如整个渠道的用户模块,办理、身份、权限等…..
  • 技术总监:OK,那咱们聊一聊登录注册吧,这个是你担任的对嘛?
  • :是的。

3.1、榜首问:登录、注册的事务规划

技术总监:你先跟我说说,你们这个项意图注册、登录界面是怎样样的呢?

注册、登录界面都迥然不同,与一些干流网站相似,注册界面会要求用户输入「手机号/邮箱、昵称、暗码、二次承认暗码、验证码」等底子信息,等这些根底信息填写完结后,用户就可以点击按钮注册账号了。而关于登录流程的规划,相较来说就更为简略,只需求用户输入「邮箱/昵称/手机号」中的任一信息,然后填写暗码点击登录即可。

技术总监:那假如用户登录时,忘记了暗码怎样办呢?

关于这点是无需忧虑的,由于在登录页面上,供给了找回暗码的入口,前面注册时必需求填写邮箱或手机号的,用户可以通过「手机验证码或邮箱验证码」的方法找回暗码。

技术总监:嗯呢,那用户登录一次之后,第2次登录时还需求从头输入暗码吗?

这个要看详细状况来差异,由于在登录的时分,供给了一个「记住暗码」的选项,假如用户登录时勾选了该选项,这时在浏览器宣布的「登录恳求」中,除开底子的用户登录信息外,还会额定传递一个标识。

在后端判别用户输入的「用户名、暗码」正确后,假如恳求中存在该标识,则会生成一个Cookie信息,将「用户名、暗码」保存在Cookie中回来,客户端在收到该信息后,会主动把Cookie存储在浏览器的本地缓存中,所以当用户第2次登录时,防止了再次重复输入「用户名、暗码」的作业。

技术总监:那你以为这种方法存在什么问题么?

有两个隐患,一方面由于保存的「用户名、暗码」存在浏览器的本地记载中,所以假如在本地找到了对应的Cookie记载,用户暗码是有被盗取的风险。一起,假如并非用户本人操作电脑时,其他人通过「开发者东西」把input元素的类型从password类型修正成text这种,仍旧有或许形成用户暗码泄露。

技术总监:可以的,逻辑思维能力还不错。

哈哈哈,没有的,首要是关于这块事务比较了解,并且登录注册的事务不算太复杂(这个时分我还没有意识到问题的严重性)。

技术总监:再问问你登录的规划哈。
技术总监:用户注册时填好了一部分信息,但由于有事走开了,终究电脑没电关机,用户重启电脑后,再次翻开注册界面,需求重填信息吗?

在咱们其时的项目中,假如呈现这种状况,由于电脑现已重启了,因而用户前次填写的信息会丢失,需求用户从头从头填写注册信息。

技术总监:嗯呢,那你有没有好的方法处理这个问题呢?

「静心苦思:不对劲,这小子很不对劲,这种问题怎样也问?让我想想该怎样答复。」

在用户填写数据的时分,前端可以通过「光标移出作业」来获取用户其时填写的数据,接着将其保存在本地的Cookie中,假如用户点击了「注册」按钮后,则主动去删去Cookie中的信息,究竟提交注册后这些保存的信息就失去了作用。
但假如用户写到一半,电脑忽然没电关机了,重启后再次翻开注册页面,那这儿又可以在「页面加载作业」中,从本地Cookie中将原本保存的数据读出来,然后赋值给对应的文本框即可,然后防止用户重复填写数据。

技术总监:很棒呀,这个主意很不错!
技术总监:那假定有两个用户在一起注册,并且输入的用户名相同,一起提交注册会呈现什么状况呢?

首要这种状况呈现的几率比中彩票都小,一起就算呈现了也不要紧,由于不同地段的网速必定有差距,所以两个注册恳求到达服务器的机遇也不同,一起在规划数据库的用户表时,对用户名加了仅有索引,所以两个用户一起注册时,就算输入的用户名相同,也只会有一个注册成功,并不会呈现用户名重复的状况。

技术总监:你们项目除开通过注册账号登录外,还有没有什么其他方法呢?

还有第三方联合登录的完结,首要便是QQ、微信这两种社交账号的联合登录,是通过腾讯自身供给的API来完结的,假如用户挑选这种第三方登录,会直接去调用腾讯的登录API。用户扫码登录成功后,会触发咱们渠道登录成功的回调接口,为其主动在渠道注册一个账号,终究完结第三方账号的联合登录。

技术总监:好的,那咱们再聊点其他的。

3.2、第二问:注册时的灵敏词检测

技术总监:你在做注册事务的时分,有没有考虑过,如若用户填写的「昵称/用户名」包含灵敏信息怎样办呢?比方填写的昵称存在传达色情、违反政策规矩、存在侮辱性意义等状况。

「缄默沉静下来考虑了几十秒,心里OSWC,我还真没想过这块问题」

关于这块问题,其时在开发时并未考虑完全,由于这个渠道归于定制化开发的,所以用户注册量也不算太大,因而在规划时也没往这块多想。

技术总监:不要紧,那假定现在我让你去处理这个问题,你会怎样下手呢?

「其时的我,由于做的都是一些简略的CRUD/增修改查项目,所以被问的时分,脑袋有些断线,心思维的是:明明我都说没做过了,你偏偏还得往这块问,这纯属是在存心刁难我胖虎啊!」

但没方法,究竟人家都问了,所以其时硬着头皮随意扯,其时的答复大致是这样的:首要我会在数据库里规划一张表,或许在后端里边创立一个Map、Set这类的容器,专门用来存储「违规灵敏词」,当用户注册时,在填写好「昵称」后,前端采用Ajax异步恳求的方法,将用户输入的「昵称」发给后端进行灵敏词检测。

后端收到前端发送的Ajax恳求后,拿着用户的昵称去和「违规灵敏词」进行匹配,假如用户输入的昵称中包含灵敏词,那就让前端显示一下「昵称违规,请从头输入」,反之则通过验证,答应用户正常注册。

技术总监:思路不错嘛,那假如用户的昵称有七个字,但其间有两个字组成的词语归于灵敏词,请问怎样检测出来呢?

首要必定需求先把用户输入的昵称分隔,然后再进行灵敏词检测,但由于个人未处理过该问题,所以现在不清楚详细的做法(其实详细计划是可以凭借ElasticSearch对用户输入的昵称做分词处理,然后再对分词后的成果进行灵敏词检测,或许可以通过DFA算法的方法进行灵敏词检测)。

技术总监:不要紧,已然你没详细做过,那咱们先越过这个论题。

「我心中长呼一口气,总算越过这个该死的问题了,再问下去都遭不住了!但没想到,我以为的完毕却仅仅仅仅开端!」

技术总监:假如有人通过机器手法,如爬虫技术对渠道进行账号的批量注册怎样办?

这点不用忧虑啊,由于前面说过的,在注册时用户必需求填写「手机号、或邮箱地址」,然后后端会先向对应的手机号或邮箱发送「验证码」,用户必需求输入正确的「验证码」之后,才干持续注册的,而手机号也好、邮箱也罢,底子上同一个人不会有太多个,所以通过「验证码」的方法,可以有用阻挠机器批量注册。

也包含这个渠道其实还支撑第三方账号注册,也便是通过QQ、微信的方法方便注册,这种账号和「手机号、邮箱」相似,都具有必定的稀缺性,同一个人不会有太多的账号,所以依据这类稀缺性账号完结注册功用,都能有用的防止机器批量注册。

技术总监:好的,那一个手机号或许邮箱答应注册多个账号么?

这个是不可的,由于在后端有做仅有性判别,一个「手机号、邮箱」注册一次之后,就无法再使用它进行二次注册了。

技术总监:嗯呢,好的。

3.3、第三问:爬虫歹意调用短信接口做轰炸

技术总监:你有触摸过、或许听说过短信轰炸嘛?

这个之前触摸过,比方当你在网上和一个人起了争执,并且对方通过一些手法得到了你的手机号,他就可以拿着你的手机号,放到一些轰炸渠道上去,然后这个渠道就会频繁的给你发送一些废物短信,以此来完结轰炸、打扰的作用。

技术总监:你说的很对,所谓的短信轰炸便是这么回事,但我想问你个事啊。
技术总监:你前面说过:用户在注册时不是可以挑选手机号注册么?
技术总监:假定有人通过逆向剖析,调试出了你们「发送短信验证码」的接口,接着用爬虫技术批量调用该接口轰炸别人怎样办?

「心里OS:他*的,早知道说没听过短信轰炸了,我是真嘴欠啊,但后边仔细一想,如同不用忧虑这个问题!」

咳咳,关于这个问题嘛,其实也不用忧虑,由于当用户点击了「发送验证码」的按钮之后,首要会弹出来一个「滑块验证码」,只要当用户通过「滑块验证」之后,才会颁布一个调用接口的「数字签名」,假如不具有这个签名,直接调用「发短信验证码」的接口时就会回来「权限缺乏」的提示。

而作为一个正常人,通过「滑块验证」天然不成问题,所以当一个“人类用户”注册时,必定是先拿到签名再调用「发短信」接口,假如呈现未带着「数字签名」的恳求,天然无法通过调用前的校验,因而通过这种滑块验证码的方法,就能有用防止爬虫的暴力调用问题。

其实开端身为一个CRUD仔的我,在被问到这个问题之前,一向并不了解为什么要在发送短信之前,增加「滑块验证码」这步反人类操作,究竟一个简略的滑块,就连三岁小孩都能通过,因而开端在开发程序时,思来想去都不能了解这步操作!

技术总监:嗯呢,那假如对方通过Selenium这种主动化技术,通过了你们渠道的「滑块验证」,又或许说对方又调试出了「数字签名」的生成接口,然后得到了签名,仍旧可以正常调用「短信」接口怎样办?

当我听到这个发问的时分,我很想答复一句:我!不!知!道!我仅仅一个天天摸鱼的螺丝仔,这不是纯属刁难人么!但不懂两个字决不能从我口中说出,因而其时随意间就扯了起来!

这个其时没有考虑到,究竟前面跟您说过的,这个渠道归于定制化程序,上线后边临的用户量并不算大,因而也没有考虑规划反爬虫机制,但您所说的这个问题也很好处理,关于一些较为“宝贵”的接口资源,比方现在所说到的短信接口,由于每条宣布的短信都需求付费,所以通常状况下都会做调用约束,比方约束十分钟内只答应调用三次这类的。

技术总监:拿你所说的十分钟调用三次为例,假如一个人在第九分钟调用了三次,接着又在第十一分钟调用了三次,这样做是不是打破了调用约束呢?你以为是否有更好的计划代替呢?

其时答复的是:听您这么说,确实是存在必定的漏洞,然后让调用约束被打破,但这块没有去详细了解和触摸过,所以并不清楚是否有更好的计划处理此问题。

关于这个问题,其时确实没有触摸过,现在想来,他想听到的答案应该是“高并发状况下的限流计划”,而我答复的限流算法,归于最底子的计数器限流计划,除此之外还有时刻窗口限流、令牌桶限流、漏桶限流这三种计划,下面临这几种常见的限流计划翻开聊聊。
一起,关于「发短信」这类“宝贵性”接口,也应该做好接口的安全性规划,比方做好接口的防篡改、防重放,以及通过数字签名完结接口调用的高鉴权等方法。

3.3.1、计数器限流计划

计数器计划归于限流算法中最简略、并且完结难度最低的算法,比方以前面的事例来说,规矩了「短信」接口的调用频率,不答应在十分钟内超出三次

这时完结起来就很简略,在「短信」接口的类中,创立一个Map<String,AtomicInteger>类型的容器即可,其间Key存储用户ID,而Value则存储一个原子计数器,每当一个用户调用一次短信接口后,就将容器中对应的计数器加一,一起敞开一个定时使命,每十分钟对计数器做归零重置。

当然,上述这种做法在用户量较大的状况下,显着会对程序形成较大的功用损耗,假定有100W用户,那就需求维护100W个计数器,这会使得内存占用率直线飙升,一起还需求创立100W个定时器,来别离维护每个用户的调用计数器。

更好一些的做法是凭借中间件完结,比方依据Redis缓存中间件来完结,将用户ID规划成Key,而Value则是计数器,并且创立每个Key时将过期时刻指定为10s,这样就能充分使用资源,不会形成太大的资源与功用开销,伪逻辑如下:

@Autowired
private StringRedisTemplate redis;
@RequestMapping("/sendSmsVerification")
public ResultVO sendSmsVerification(String sign, String userId){
    // 用 SMS_ 拼接用户ID作为Key
    String userIdSMS = "SMS_" + userId;
    // 先通过前面生成的Key去Redis中进行查询
    String value = redis.opsForValue().get(userIdSMS);
    // 假如现在现已到达了调用次数约束
    if ("3".equals(value)) {
        return new ResultVO(200, "短信调用次数已达上限,请在十分钟后重试...");
    }
    // 假如该用户的Key在Redis中不存在,阐明是榜首次调用短信接口
    if ("".equals(value)) {
        // 初次调用短信接口时,则在Redis中创立一个计数器
        redis.opsForValue().set(lockKey, 1, 10, TimeUnit.SECONDS);
    } 
    // 假如该用户的Key在Redis中存在,阐明并非榜首次调用短信接口
    else {
        // 此刻则通过Redis的incr指令,把对应的计数器加一
        redis.opsForValue().increment(key);
    }
    // 省略其他事务代码......
}

这段限流代码并不算特别复杂,全体下来无非仍是前面说的那几步:

  • ①先通过用户ID拼接得到Key,然后去Redis中进行查询。
  • ②假如查询出的成果为3,阐明现在已到达了调用约束,则直接回来调用已达上限。
  • ③假如查询出的成果为空,则阐明用户是榜首次调用短信接口,此刻则在Redis中创立计数器。
  • ④假如查询出的value和上面两条都不匹配,则对Redis中的计数器加一。

这种计数器限流算法完结起来尤为简略,但前面也聊过它所存在的问题:临界问题,假如在两个时刻单位的临界处调用,比方在第9:59秒调用了三次,接着又在第10:01秒调用了三次,那仍旧会发生“超出调用上限”的状况,究竟以十分钟作为单位,第9、10分钟归于一个时刻单位内,这时就超出了调用上限,调用次数到达6次。

3.3.2、时刻窗口限流计划

时刻窗口限流计划被提出的首要意图,便是为了处理传统的计数器计划存在的临界问题,它的演化前身为TCP协议的滑动窗口,假如关于TCP协议较为了解的小伙伴,听到这个词汇相信必定不生疏,如若对这块内容并不了解的小伙伴也不要紧,可参考之前文章中聊过的《TCP粘包、半包问题-滑动窗口》。

限流计划中的时刻窗算法,首要可被分为固定窗口限流、滑动窗口限流两种计划,而前面聊到的计数器计划,实际上便是一种特别的固定窗口限流计划,在前面的比方中,时刻窗口巨细为10min,速率约束为3次,这种计划存在显着的临界约束问题。

下面要点聊一聊滑动时刻窗口,这种计划是处理临界问题而被提出的,但关于滑动窗口的概念有些不好了解,所以先上一副逻辑图,如下:

追忆四年前:一段关于我被外企CTO用登录注册吊打的不堪往事

在上图中,整个用虚红线圈出来的代表一个时刻窗口,以上述比方来说,一个窗口的巨细为600s/10min,并且每个窗口被分为了三个单位,每个单位巨细是200s,这也就意味着每过200s,窗口会向后滑动一个单位,这个动作也可以被称之为向后滑动一格,现在的窗口散布如下:

  • 榜首格:0~200s
  • 第二格:201~400s
  • 第三格:401~600s

差异出来的每个格子,都具有各自独立的计数器,比方在第138s时发生了一次接口调用,此刻榜首格的计数器就会+1,仍是以之前的比方来说:

9:59秒调用了三次,接着又在第10:01秒调用了三次。

将这儿的分钟转换为详细秒数,也便是在第599s调用了三次,第601s调用了三次,此刻来看,每其时刻曩昔200s,窗口就会向后滑动一格,这也就意味着整个窗口会变成图中的下面的样子,此刻的窗口散布为:

  • 榜首格:201~400s
  • 第二格:401~600s
  • 第三格:601~800s

当第599s调用了三次「短信」接口后,第二格的计数器会累加到3,此刻再当第601s测验调用「短信」接口时,就会检测出已到达调用上限,此刻就会回绝用户的调用,以此来处理传统计数器计划的临界问题。

Why?Why?Why?有些小伙伴或许到这儿就有些晕了,第601s是怎样检测出调用超额的呐?由于现在的时刻窗口规模是201~800s,而将整个时刻窗口内的计数器求和,就会得到调用总次数为3,因而成功检测出了第601s的调用上限。

当呈现调用到达上限时,必须跟着时刻推移、窗口不断向后滑动,这样整个窗口的计数器总和才会下降,因而用户才干持续调用,通过这种方法就能操控一个时刻段的绝对限流。

但滑动窗口限流计划就不存在临界问题吗?答案是No,仍旧存在,Why?来看下图:

追忆四年前:一段关于我被外企CTO用登录注册吊打的不堪往事

看上图中给出的事例,由于现在的时刻窗口巨细是600s,而199s~203s显着处于同一个时刻窗口规模内,但跟着窗口向后滑动,这儿仍旧会呈现临界问题,也便是在一个窗口规模内,相同会呈现打破调用次数上限的状况,那这种状况下又该怎样处理呢?其实答案很简略,把一个窗口的格子单位调小即可。

比方直接将每一格的单位巨细从200s调整为1s,此刻每过一秒钟,窗口就会向后滑动一格,等到100s秒过后,窗口会向后滑动100格,此刻窗口的区间规模是101~700s,这就将199~203s这个规模包含了进去,因而上述状况天然就不会呈现!

通过上述剖析由此可以得出一条准则:当滑动窗口的格子差异的单位越小,整个窗口中的格子数量会越多,滑动窗口的向后移动就越平滑,限流的统计就会越准确

3.3.3、令牌桶限流计划

前面简略聊完了时刻窗口限流计划后,接着再来聊一聊大名鼎鼎的令牌桶限流计划,令牌桶算法是一种相似于“池化”思维的产物,算法的大体进程如下:

追忆四年前:一段关于我被外企CTO用登录注册吊打的不堪往事

  • ①初始化令牌桶并设置最大令牌数,当桶内的令牌到达阈值时,新增加的令牌会被回绝或丢掉。
  • ②依据限流巨细,启动一条线程,并依照必定速率向令牌桶中不断增加新的令牌。
  • ③任何处于「限流规模」内的恳求,都需求先获取到一个可用令牌,然后才会被处理。
  • ④当一个恳求获取到可用令牌后,才会真实履行事务逻辑,履行完结后会将此令牌从桶内移除。
  • ⑤令牌桶除开有最大令牌数外,也会有最小令牌数,当桶内令牌数小于最小阈值时,处理完恳求并不会移除令牌,而是会将令牌还给令牌桶。

关于令牌桶限流算法,了解起来并没有前面的滑动时刻窗口复杂,但仅有要注意的是:当桶内的令牌被一个恳求获取后,此刻并不会立马从桶内移除,该令牌会仍旧停留在桶内,只不过该令牌的状况会从可用状况变为不可用状况,也便是其他恳求无法再获取该令牌,真实移除令牌的作业,会在事务逻辑履行完结之后才触发。

3.3.4、漏桶限流计划

漏桶限流和令牌桶限流都归于桶类型的算法,但漏桶算法更相似于MQ音讯行列,其算法的履行示意图如下:

追忆四年前:一段关于我被外企CTO用登录注册吊打的不堪往事

想要了解漏桶算法,咱们先来看看日常日子中的漏斗,比方现在我要用漏斗来给摩托车加油:
追忆四年前:一段关于我被外企CTO用登录注册吊打的不堪往事

倒油时,咱们可以用瓶子,也可以用桶子,也可以用加油枪…..,这也就意味着:漏斗上方的进油速率并不固定,但不论上方的进油速率怎样,下方的漏斗出口,其速率确实固定的,不论上方进油多快,都不能影响下方的出油速率。

了解了日常日子中的漏斗后,接着再来看看前面的漏桶限流算法,恳求会从漏桶上方进入,而服务端则只会依照固定速率去处理恳求。此刻考虑一个问题:当恳求进入的速率大于恳求处理的速率,会发生什么状况呢

此刻仍旧回到用漏斗给摩托车加油的比方中,假如漏斗上方的倒油速度比较快,而由于漏斗的结构原因,下方的出口跟不上进油速度,此刻漏斗中的油量会直线上升,直到超出漏斗的最大容量时,再进入漏斗的汽油会溢出。

而限流中的漏桶算法相同如此,恳求进入的速率大于恳求处理的速率时,多出来的恳求会被放入桶中等候,当桶内阻塞等候的恳求超过最大约束后,后续进入的恳求会被丢掉或回绝。

从上述的讲解中,诸位应该可以显着感受到漏桶算法的特色,即:宽进严出,该算法中不会约束恳求进入的速率,但会约束恳求处理的速率,一些对安稳性要求较高的体系,就可以采用该算法对体系进行限流。当然,假如了解MQ的小伙伴也能感受出:漏桶算法和MQ的削峰填谷有着异曲同工之妙,当体系峰值流量较高时,会将恳求写入到MQ中,然后再由详细的事务服务,依照固定的速率拉取MQ中的音讯进行处理

3.3.5、高并发限流算法小结

在前面共计提到了计数器、滑动窗口、令牌桶、漏桶这四种惯例的限流计划,但要记住:并不存在一种适用于任何场景的限流算法,依据事务的需求不同,体系的重视面不同,应当采用不同的限流计划,没有所谓的最好!终究简略说一些成熟的限流完结:

  • Guava中的RateLimiter东西类:依据令牌桶完结的限流组件,并且对其进行了预热拓宽。
  • Sentinel中的匀速排队限流策略:依据漏桶思维的限流策略,内部采用行列进行完结。
  • Nginxlimit_req_zone限流模块:依据漏桶思维的限流模块,完结网关层的限流操控。
  • ........

3.4、第四问:API接口的幂等性问题

技术总监:接下来咱们再聊聊其他方面的可以吧?
技术总监:以现在的技术来说,任何用户在使用网络时,难免会存在推迟是不是?

对的,这点我深有体会,尤其是在春节回老家的时分,由于山区的网络覆盖并不全面,所以在拜访一个网站时,加载的速度会特其他慢。

技术总监:嗯呢,已然你也说了这个问题,那我再问你一个问题。
技术总监:假如一个用户在注册时,网络比较卡顿,所以提交注册后迟迟没有反应,因而他又接连点击了屡次「注册」按钮,此刻会发生什么状况呢?

「我沉思顷刻答复道」:假如没有做任何约束,理论上会向服务端宣布屡次恳求,假如数据库的表结构规划不合理,那么还会呈现同一用户的注册信息,在用户表中被刺进屡次。

技术总监:说的不错,那请问你们其时是怎样处理呢?

咱们其时处理计划比较简略,首要在前端做了必定约束,也便是当用户初次点击了「注册」按钮后,「注册」按钮就会变成灰色,也便是用户再次点击时,并不会再次发送Post恳求向后端提交表单数据。

技术总监:那假如用户看点击注册按钮后迟迟没反应,按F5改写或浏览器的撤退键,接着再次点了「注册」按钮怎样办?

「心里一颤,没想过啊!硬着头皮解释道」:关于此问题,我在做登录注册时并未考虑周全,未对这个问题进行考虑。

但其完结在想来,处理的思维也比较简略,除开在原本将按钮变灰的根底上,再加上一个「重定向页面」即可,比方信息提交后就跳转下述这个界面:

追忆四年前:一段关于我被外企CTO用登录注册吊打的不堪往事

这样做的好处在于:重定向操作发生后,当用户再次改写网页,或许通过浏览器的回退键,回到原本的界面时,之前表单中填写的信息并不会保存。这样做的好处在于:用户想要再次点击注册按钮,就只能再次从头输入信息。

在用户网络比较卡顿的状况下,做了上述规划后,就只会呈现两种状况:

  • ①用户前次点击「注册」按钮提交的Post恳求发送失利,服务端并未处理前次的注册恳求。
  • ②用户前次点击「注册」按钮提交的Post恳求发送成功,在用户再次填写信息的进程中,服务端将前次的注册恳求成功处理,用户再次提交注册时,体系会直接提示去通过手机号登录。

总归参加了这个「重定向页面」后,都能保障在短时刻内,用户无法再次重复提交参数相同的注册恳求。

技术总监:那假如有人通过PostMan之类的东西,模仿注册参数屡次调用注册接口呢?

这个实际上也不需求忧虑的,由于在数据库的表规划中,咱们对「邮箱/昵称/手机号」这些特别字段也加了仅有索引,就算特别状况下形成重复恳求呈现,由于表结构中有仅有性字段,所以关于相同注册参数的恳求,在用户表中仍旧只会成功刺进一条数据。

技术总监:这种计划也可以,但你还有没有什么其他更好的计划呢?

其时项目是这么做的,所以并未再去对其他计划进行研究。

技术总监:没事,你等面试完毕之后可以再研究一下。

3.4.1、接口幂等性规划的最佳完结

尽管其时并未答复出更好的计划,但后续自己也去了解过「接口幂等性与防重规划」,这儿做简略总结。

发生幂等问题的底子原因

总的来说,在软件体系中呈现幂等问题的原因无非四个:

  • ①用户重复提交:一般是指用户填写好表单信息后,由于呼应较慢,然后屡次点击提交按钮。
  • ②不合法调用:指第三方通过逆向手法调试到了接口地址,然后通过爬虫或接口东西屡次调用。
  • ③失利重试:指散布式项目中,被调用方呈现超时或异常时,触发了调用方的重试补偿机制。
  • ④重复音讯:通常指引入MQ的项目,关于同一个音讯,生产者屡次发送,或消费者重复消费。
会呈现幂等问题的操作

作为开发者的咱们都知道,任何一个软件,不论事务多么复杂,其背面的本质仍旧是增修改查,关于删、查操作而言,天然具有幂等性,因而需求考虑幂等性规划的就只要增、改这两种,Why

由于查询、删去操作,就算呈现屡次也并不影响全体数据的一致性,比方查询“张三”的年纪,同一时刻内不论查多少次,得到的成果都是相同的。而删操作相同如此,如删去姓名为“张三”的用户数据,就算同一时刻内呈现了十个这样的恳求,终究成果都是“张三”这条数据不见了。

多个层面处理幂等问题的计划
  • 前端:
    • ①按钮变灰/或变为Load状况:防止用户点击屡次按钮,形成多个重复恳求呈现。
    • ②重定向页面:防止用户通过改写/回退的方法,形成多个重复恳求呈现。
  • 后端:
    • ①仅有Key计划:先依据事务参数,从中选出或核算出一个大局仅有Key
      • 仅有Key的核算计划:
        • 选用恳求参数中的某个特别值,如手机号、订单号…作为Key
        • 通过Hash函数来对一切参数进行哈希核算,得到一个Key
        • 非注册的场景,可以使用其时用户ID+方针方法名作为Key
        • …..(这儿只要能得到一个与事务相关的仅有Key即可)。
      • 得到仅有Key之后,通过set nx px指令向Redis刺进数据:
        • 成功:代表前面没有重复的恳求,其时恳求可以履行。
        • 失利:代表前面有相同恳求现已刺进过了,其时恳求需求被丢掉。
    • ②防重表计划:使用事务的仅有ID,如订单号作为仅有索引,操作之前先刺进防重表。
    • ③状况机计划:在表上多加一个状况字段,关于update操作加上状况判别,如订单表:
      • 将「待付款」改为「待发货」:update ...,status = 2 where status = 1;
      • 这样就算呈现多个修正恳求,由于榜首个恳求改成功后,状况变为2,其他恳求都会失利。
    • Token计划:内容较多,后边聊。
  • 数据库:
    • 乐观锁计划:额定规划一个version版别字段,但这种计划只适用于update操作。
    • 仅有索引:关于数据的要害字段加上仅有索引,如手机号,防止重复数据屡次刺进。

上面依据不同的层面,给出了多种幂等问题的处理计划,但有些计划只适用于特别的场景,如状况机、乐观锁、防重表等计划,假如要规划一套处理幂等问题的通用计划,挑选如下:

  • 甲、前端重定向页面防重 + 后端仅有Key去重 + 数据库仅有索引兜底。
  • 乙、前端按钮变灰防重 + 后端Token去重 + 数据库仅有索引兜底。

通过上述这两套组合计划,任选其一都可以打造出一套处理幂等问题的通用策略,但其间仅有没翻开讲解的则是Token计划,这种方法到底是怎样完结的呢?下面翻开聊一聊,示意图如下:

追忆四年前:一段关于我被外企CTO用登录注册吊打的不堪往事

  • ①当用户进入一个表单时,前端通过Ajax异步调用后端供给的Token获取接口。
  • ②后端生成一个大局仅有性的Token放入Redis中,可以是UUID、SnowflakeID....
  • ③后端将生成的Token回来给前端,前端先将其保存在一个变量或Cookie中。
  • ④用户填写好表单数据后,在Post恳求的头部带着Token值,接着与表单数据一起发给后端。
  • ⑤后端先获取头部的Token值,并测验去Redis中删去该Token,即del [token_value]
  • ⑥后端依据删去指令的履行成果,进行下一步判别:
    • 假如成功删去:表示现在恳求是榜首次调用接口,答应履行详细的事务逻辑。
    • 假如删去失利:表示该Token之前现已删过了,其时恳求归于重复恳求,应当被丢掉。

上述便是前面所说的Token计划,整个进程会呈现两个恳求,榜首个恳求是异步获取Token,第二个恳求则是详细的事务恳求,终究会依据事务恳求上带着的Token值,以此作为重复恳求的判别条件,然后防止一起处理多个重复的恳求。

3.5、第五问:用户账号的兼并问题

技术总监:你之前说过,你们项目注册时,可以选用「邮箱/手机号/第三方账号」进行登录是吧?

对的,用户可以通过这三种方法来注册并登录渠道。

技术总监:那一个用户通过手机号注册后,能否绑定第三方账号呢?

这个是支撑的,在用户的个人中心里,用户可以挑选绑定第三方账号,绑定第三方账号后,后续用户也可以直接通过第三方账号登录。

技术总监:那假定用户先通过微信进行第三方登录,按你们渠道的规矩,会主动为其注册一个账号。
技术总监:接着该用户又用手机号注册了,此刻同一个人在你们渠道,是不是有了两个账号?

是的,通过微信登录时,假如之前这个微信没有绑定过渠道账号,会为其主动创立一个账号。用户通过手机号进行注册,相同又会生成一个账号。

技术总监:嗯呢,那我想问一下,假如这个用户有一次通过手机号登录,接着想要绑定那个微信,这样可以吗?

我听到这个问题,榜首反应是想答复:“可以”!
但转念一想发现了端倪,假如能绑定同一个微信,岂不是一个微信对应两个渠道账号了?假定该用户下次挑选通过微信扫码登录,扫码成功之后,到底要登入哪个账号呢?

「我理清思路答复道」:这是不可以的,由于这样绑定之后,一个微信号会与两个渠道账号发生映射联系,下次用户挑选用该微信号登录时,就会呈现问题,无法确定要登入哪个账号。

技术总监:已然你能想明白这个问题,那我想问问你有没有什么好的处理计划呢?

「我听到这个问题后,陷入了缄默沉静…..」

3.5.1、站在现在的视点再次看待此问题

其实这个问题自身并不是技术问题,而是一个事务问题,所以想要处理此问题,就无法完全依靠程序自己完结,此刻必须介入人工进行处理,而这个问题在现在的各大渠道都有处理计划,大体归为下述五类:

  • ①挑选第三方登录时,需求用户通过手机号先创立一个渠道账号。
  • ②兼并多账号的权力交给用户自己。
  • ③当用户测验绑定一个「已绑微信」时,提示用户找办理员申诉。
  • ④答应同一个第三方账号对应多个渠道账号,扫码登录时,挑选登录哪个账号的权力交给用户。
  • ⑤用户想要绑定一个「已绑微信」时,提示用户先去解除该微信与其他账号的绑定联系。

榜首种做法在各大银行的手机APP中比较常见,当你挑选通过第三方账号登录手机银行时,假如是榜首次登录,微信登录成功后会跳转注册界面,要求你先通过手机号创立一个账号,接着银行APP会主动将其时「手机号、微信」发生绑定联系,后续可以两者中的任一方法登录。

第二种做法我在简书见过,当多个账号之间存在冲突时,将兼并账号的权力交给用户自己,当用户挑选保存某个账号时,其他账号都会被毁掉,包含其他账号在渠道上的一切数据也会完全丢失。

第三种做法我在一些小的自建站见过,其实这是触发了渠道的「不知道操作」的补偿机制,由于用户在测验绑定一个「已绑微信」,这种操作在程序后台无法识别,所以直接给出一致的提示,即:“请联系办理员进行申诉”,申诉后会由渠道办理员,介入修正后台数据库进行处理。

第四种做法在游戏的用户办理中比较常见,以广为人知的「王者荣耀」举例阐明,在登录界面可以挑选通过微信登录游戏,而微信登录成功之后,会呈现下述这个界面:

追忆四年前:一段关于我被外企CTO用登录注册吊打的不堪往事

在这类游戏中,玩家可以自行挑选分区,同一个微信账号支撑在多个分区创立账号,这也就意味着一个第三方账号,可以与多个渠道账号存在关联联系,当用户下次通过该微信账号登录时,用户可以自行挑选详细的分区(详细要登录的渠道账号)。

第五种做法归于最常见的做法,明确规矩一个第三方账号,只能与一个渠道账号存在绑定联系,当一个账号测验绑定第三方账号时,假如检测到对应的第三方账号存在其他的绑定联系,就直接提示用户:“该第三方账号已被其他账号绑定,请手动解除绑定后重试”!

3.6、第六问:登录的夺命五连问

技术总监:用户登录成功之后,第二天再次翻开网站需求从头登录吗?

假如用户登录成功之后,第二天再次翻开网站无需再次登录,但「免登录」存在时效约束,一般状况下为7天,也便是间隔用户前次登录的时刻超出七天后,用户再次拜访网站就需求再次登录。

完结的大体原理:通过JWT完结,用户登录成功之后,后端往Redis中存储一个时效七天的refresh Token(Key=userID,value=refreshToken),接着会向前端颁布一个时效较短的access Token,前端会将其存储浏览器本地,在后续每次客户端拜访其时网站时,都会带着这个access Token完结鉴权。

颁布给前端的access Token时效为何比refresh Token要短呢?
有些事务对权限比较灵敏,为了Token防止被盗用,access Token天然是有用期越短越安全。

时效较短的access Token过期了怎样办?
当一个客户端带着过期的access Token来恳求时,服务端可以通过该Token解析出时刻戳和用户信息,效验时刻戳没有问题后,接着通过用户信息中的userID去查Redis,假如可以查询到对应的refresh Token,此刻就可以从头签发一个access Token回来给前端,前端将之前的Token替换成新的后,再次恳求服务端资源。
这个进程会不断循环,循环往复之,直至服务端Redis中的refresh Token过期为止(过期后需求用户从头登录)。

技术总监:用户登录成功之后,其他的子体系怎样得知该用户登录了?

由于不同的子体系都有权限操控,一个用户在主站登录成功之后,服务端会向客户端颁布Token,客户端可以通过该Token在主站域名下“活泼”,但当客户端测验拜访其他不同域名的子体系时,由于浏览器的本地数据(缓存、Cookies等)是按域名差异存储的,所以拜访其他子体系时并不会带着前面主站颁布的Token,终究客户端的拜访会遭到回绝。

现现在事务线更加复杂,因而都会引入散布式概念拆分出不同的子体系,并且不同的事务子体系会采用不同的域名布置,所以想要确保「用户一次登录,全线都能拜访」的功用,就需求完结单点登录功用。在咱们项目中,其时通过OAuth2.0整合JWT完结了SSO认证服务,然后终究完结了单点登录的功用。

简略概述OAuth2.0 + JWT + SSO完结单点登录的原理,如下图:

追忆四年前:一段关于我被外企CTO用登录注册吊打的不堪往事

条件:
①当用户在拜访恣意子体系没有带着Token(Ticket)时,都会被重定向到独立布置的SSO认证中心。
②假如对应的用户在SSO服务中找不到登录凭据,终究会跳转登录页面,要求用户进行登录操作。

一次完好的单点登录进程:

  • ①用户未带着Ticket拜访A体系的某个页面,被重定向到SSO服务。
  • ②用户未带着登录凭据拜访SSO认证中心,被重定向到登录页面。
  • ③用户完结登录操作,在SSO域的Cookie中植入各种凭据,并再带着Code重定向到A体系的回调接口。
  • ④用户带着Code拜访A体系,ASSO恳求验证Code,有用则为A域颁布Ticket,偏重定向到原网页。
  • ⑤用户带着Ticket拜访A体系的原网页,ASSO恳求校验Ticket,有用则履行详细的事务逻辑。
  • ⑥用户拜访B体系的某个页面(此刻无法带着A域的Ticket),被重定向到SSO服务。
  • ⑦用户带着SSO-Cookie拜访SSO,该用户的登录凭据校验成功,带着Code重定向到B体系的回调。
  • ⑧用户带着Code拜访B体系,BSSO恳求验证Code,有用则为B域颁布Ticket,偏重定向到原网页。
  • ⑨用户带着Ticket拜访B体系的原网页,BSSO恳求校验Ticket,有用则履行详细的事务逻辑。

为什么可以通过CodeTicket呢?使用OAuth2.0的四种授权方法之一:授权码来完结。
为什么要用CodeTicket呢?Code是一次性的,下降Ticket被盗用的风险。

技术总监:用户仿制一个登录后才干拜访的链接,然后粘贴到另一个页面上会怎样?

这要分状况,假如用户仿制链接之后,粘贴在同一个浏览器的其他页面,此刻该用户是可以正常拜访的。但假如用户仿制链接粘贴到其他浏览器上,在其他浏览器未登录过的状况下,本次拜访都会遭到回绝。
这是由于后端都对用户做了权限操控,假如未登录账号的客户端,在咱们渠道归于游客等级,而登录了账号的客户端,则归于正常用户的等级,不同的用户等级对应不同人物,不同人物则又对应不同权限,以此来完结权限的精准操控。

这儿背面的完结原理就不过多啰嗦了,其时的项目是采用Shrio权限框架完结的,一切的权限、人物、用户的映射联系,都存储在数据库的五张权限表之中(有兴趣的可以自行去了解)。

技术总监:用户点击登录之后把其时页面关了会发生什么?

「思索顷刻后不自傲道」:额….,应该会登录成功吧。

技术总监:确定会登录成功么?

「陷入缄默沉静…..」(心里:我擦,这纯属刁难人啊,那个吃饱没事干的人,会点了登录就关网页!?!!)

站在现在的视点考虑:
定论:是否会登录成功要分实际状况来决议,看用户封闭的是其时网页,仍是其时的浏览器。
用户封闭了其时网页,成果是会登录成功。用户封闭了其时浏览器,成果是不用定登录成功。

原理剖析:
封闭其时网页:由于用户点击登录按钮之后,登录(账号、暗码)的恳求现已发往了服务器,所以服务端处理完登录恳求后,终究会回来一个Token或登录凭据,此刻由于浏览器进程还在,这也就意味着浏览器自带的网络进程并未消失,所以登录效验成功之后的操作,如在Cookie中植入Token、各类凭据等操作仍旧能正常完结,所以理论上会登录成功。
封闭其时浏览器:这种状况下,用户点击登录按钮后,仍旧会向服务器宣布登录恳求,但由于浏览器现已被封闭了,所以相应的网络进程也会消失,终究就会呈现一种特别现象:「当服务端处理完登录恳求后,向客户端回来呼应成果时,由于客户端的网络进程现已毁掉,所以浏览器无法接纳呼应成果,也就天然无法在Cookie中植入各种登录凭据,终究成果就不用定登录成功」。

疑问解答:
为什么封闭浏览器之后无法接纳服务端的呼应成果?
由于HTTP/HTTPS协议的底层是TCP协议,TCP是一种双向通讯的网络协议,当通讯的一端呈现毛病时,两头之间的网络数据就无法正常传输,期间TCP的发送方会屡次从头发送数据包,但由于接纳端的网络进程已毁掉,所以无法收到呼应成果。
为什么封闭浏览器的成果是不用定登录成功?
由于存在不安稳要素,究竟大多数进程在退出时,采纳的方法都是高雅封闭,也便是会先处理完现在正在履行的使命后,才会真实将一切后台进程退出(也便是咱们封闭一个程序之后,电脑管家都会提示你XX软件还有残留进程可整理的原因)。
假如封闭浏览器之后,网络进程没有立马毁掉,在这期间或许会正常收到服务端的呼应成果,终究就会登录成功。
但假如服务端的呼应时刻比较慢,或许用户安装了电脑管家之类的程序,在进程退出后或许会主动整理残留进程,这种状况下就会完全毁掉网络进程,此刻成果便是登录失利。

技术总监:用户点击登录之后把网线拔了,你以为成果是怎样样的?

「其时的心境:……………………………….」
「其时的心里:我去你大爷的,你*&#…~-/!,前面的点登录按钮后关页面就够离谱了,你现在又整一个拔网线…,你怎样不问我用户点击登录之后,地球就爆炸了会怎样呢???」

「我的答复」:不知道!(其时到这儿心态都被问出一点问题了)

以现在的常识储备进行理性考虑:
定论:详细要看用户拔网线的机遇,成果仍旧或许是登录成功或登录失利。
假如用户在呼应成果回来之后拔了网线,成果是登录成功。但如若呼应回来之前拔了网线,成果是失利。

原理剖析:
这个问题其实和上一个问题相似,但实际状况又存在很大差异,由于不论什么时分拔网线,本质上浏览器的网络进程都不会消失,问题在于网络传输链路出了问题。
关于接纳到呼应成果之后才拔网线的状况,了解起来也比较简略,究竟呼应成果都拿到了,剩下的作业天然也能进行,终究成果就必然是登录成功。但此刻要点要阐明的另一种状况,也便是:为什么在呼应成果回来之前,拔掉网线的成果是登录失利?
想要明白这个问题,本质上与核算机网络的根底脱不了干系,众所周知的一点,现现在的互联网是由一个个局域网组成的(不了解的小伙伴回去看《计网根底:漫谈核算机网络》),由于IP归于宝贵性资源,所以并不是每台网络设备都具有公网IP,而恰恰远间隔的网络通讯需求公网IP,此刻又该怎样办呢?那也便是多台网络设备同享一个公网IP,这些同享一个公网IP的多台机器,会组成一个小的局域网(假如了解比较困难,可以这样了解:插同一个路由器网线、连同一个路由器WiFi的设备,都可以看成是一个局域网内的设备)。
有了上述常识的简略储备后,接着再回到问题自身进行探讨,当用户的浏览器宣布登录恳求,并且服务端将用户的登录恳求处理完结后,经一系列处理睬发生一个数据报文,该报文的方针地址便是宣布登录恳求的那台机器(实际上是那台机器地点的局域网的公网IP),接着呼应报文会先来到机器地点的局域网,但此刻问题来了!
呼应报文现已抵达了局域网,不过此刻用户的电脑网线被拔,也便是对应设备会退出这个局域网,那么局域网的路由器在“派送数据报文”时,就无法找到详细的派送方针,但此刻用户电脑上的浏览器网络进程仍旧存在,仅仅由于传输链路呈现毛病,所以无法接纳到呼应成果,终究导致登录失利。
这种状况就相当于买快递,原本你写的是收货地址A,当快递送到A小区的菜鸟驿站时,成果你搬迁搬去了B小区,这时A小区的驿站派送员,就无法依据收货地址将快递送货上门。
当然,还有一种特别状况,也便是用户把网线拔了之后,又立马插上去了,这时理论上仍是会登录成功的,由于HTTP底层的TCP协议,是一种可靠性传输协议,在传输失利的状况下会有重发机制。

3.7、第七问:令人窒息的多IP并发操作

技术总监:一个账号在多台电脑上一起点击登录按钮,终究会呈现什么状况呢?

「吸收前面的经验,听到这个问题的我,榜首反应便是这儿面绝对有诈!」

「通过一番考虑后,答复道」:应该都会登录成功。

技术总监:哦?也便是你们的项目中,并未约束多IP登录,或许做同端互斥对吗?

「我仔细想了想,如同确实没做,于是答复道」:在咱们的项目中确实没做这些。

技术总监:那假定一个账号在两个IP上登录了,一起修正昵称会发生什么变化?

有一个IP上修正的昵称,会被另外一个IP上的昵称代替掉。由于就算两个IP一起修正、一起提交,终究到数据库履行update句子时,都会被串行化,由于两个事务并发修正同一行数据时,需求先获取行锁资源,这也就意味着这两个修正操作终究都有前后之分,前一个IP修正的昵称总会被后一个IP修正的昵称覆盖掉。

技术总监:嗯呢,那在不约束多IP登录的状况下,你有什么好的方法成果这个问题吗?

「仔细推导一番后,答复道」:可以参加一个中间状况,也便是在用户表中多规划一个状况字段,0代表正常状况,1代表审核状况,当用户的信息发生变化后,对应的用户记载都会被改成「审核中状况」,而履行句子时只答应修正正常状况的用户记载,伪SQL如下:

-- 之前的SQL句子
update zz_user set user_name = "竹子爱熊猫", ... where user_id = 888;
-- 优化后的SQL句子
update 
    zz_user 
set 
    user_name = "竹子爱熊猫", status = 1, ... 
where 
    user_id = 888 and status = 0;

通过这样的手法,在榜首个IP修正成功之后,第二个IP就无法满足SQL句子的履行条件,终究就无法真实修正用户数据。

技术总监:很不错的思路,确实可以处理我所提出的问题。
技术总监:假如现在有一个签到领积分的功用,两个不同IP的同一账号一起签到,会不会领到双倍积分?

假如没有做任何约束方法,这种状况下应该会领到双倍积分,但条件是两个IP是以绝对手法进行一起操作的才行,也便是服务端中同一时刻内,两条线程并行处理两个IP的签到恳求。

技术总监:嗯呢,那假如你项目中有订单功用,一个IP删去订单,一个IP结算订单,两个操作同一时刻内进行,成果是什么呢?

会呈现问题,导致一个账号上的订单数据错乱。

技术总监:那你以为该怎样处理此问题呢?

其时的我听到这个问题,心里的榜首主意:得加锁,但又转念一想,似乎发现了不对劲,由于加锁只能让并行操作串行化,但终究两个事务操作总会履行的,这儿加锁之后只会呈现两种状况:
①删去订单的恳求先获取锁,先删掉了订单,结算订单的恳求无法履行结算事务(由于订单都没了)。
②结算订单的恳求先拿到锁,用户付钱结算了订单之后,删去订单的恳求获取到了锁,然后把用户现已付钱的订单删了(这显着更不合理,用户估量能举起四十米的大刀…)。

「由于其时的我没做过并发处理,就只懂一些简略的多线程理论,于是又陷入了缄默沉静…..」

站在现在的视点出发,再次看待此问题,处理计划为:状况机!啥意思呢?其实和之前「并发修正昵称」的计划差不多,单独的靠加锁无法处理此问题,问题并不在这上面,这相同是个事务逻辑的问题,应该在订单表上面也规划一个status状况字段。

订单表的状况字段,可选状况如下:

  • 0:待结算(待支付)。
  • 1:待发货。
  • 2:待收货。
  • 3:已签收。
  • .....
  • 9:已毁掉。

有了上述这个状况机字段后,再回过头来看「删去订单、结算订单」这两个事务操作,本质上都是履行update操作,删去是将状况改为9,结算是将状况改为1,所以SQL句子只需求新增一个条件即可:

-- 删去订单(只答应删去待结算、已签收的订单)
update zz_order set status = 9,... where status = 0 or status = 3;
-- 结算订单(只答应结算待支付的订单)
update zz_order set status = 1,... where status = 0;

也便是直接通过状况字段约束其他并发操作,不论「删去订单、结算订单」谁先履行,另一个操作都无法持续履行。有人或许会疑问:有了状况机之后,就不需求加锁了吗?

其实这种状况下,加不加锁就无所谓了,由于MySQL-InnoDB自身有行锁机制,多个事务并发修正同一条数据,都会被串行化履行,因而在后端加锁,仅仅将恳求串行化的作业提前罢了,这反而会影响全体的功用。

「其实到这儿还并未完毕,后来这位面试官还与我聊了许多,但由于时刻较为长远,就只能回忆起一些形象比较深入的发问了~」

四、这段难忘阅历给我带来的感悟

或许看到这儿,咱们很感兴趣的一点是:后来的我怎样样了?其实这次面试之后,其时的我不气馁是不或许的,甚至那时的我被冲击的有些严重,自以为不可一世的我,成果死在了“最简略的登录注册”上….

完毕了这次面试后,我并未再持续投递简历,但人总得吃饭是不?于是乎,我又使出了另一种大名鼎鼎的面试秘法 —— 朋友内推,在第二天以满意的薪酬,成功入职了另一家公司~

当然,其实后来这家外企的人事也在后边一周的周一,给我打来了入职邀约的电话,但由于我现已入职了朋友公司,所以用「暂时有事,不方便曩昔入职」的理由回绝了(我原本以为自己必定凉了,究竟三四天都没有给我通知,但后边转念一想,究竟这是外企的分公司,或许是入职批阅流程比较长)。

不过令人出乎意料的是:在当天的下午,该外企的人事总监又打来了一个电话约请我入职,说他们技术总监比较看好我,感觉我很具有培育价值….,并且这回的入职邀约中,或许以为我前次回绝是薪资不满意,还额定在我报价的根底上加了1.5K的工资(这对其时的我来说,尽管不算特别多,但每个月多出1.5K也是一笔不菲的收入),不过终究仍是由于多方面的原因回绝了,哈哈(其实早两天打给我说不定真的会曩昔~)。

尽管这次面试带给我的冲击很大,但从中我的收成也不小,其实总的来说也算自取其祸,究竟其时的我确实很自豪,而这位CTO则用我其时以为“最简略的事务”,将我虐的遍体鳞伤,从这段阅历中我想明白了一个道理:谦善戒骄才是真实的大佬应有的美德

当然,说了这么多的进程,终究也来聊聊这段阅历带给我的感悟,扪心自问,其实这位面试官也是人生中的一位“贵人”,从他身上我看到了许多之前并不知晓的道理。

4.1、千万不要诉苦自己仅仅个CRUD的螺丝仔

在现在的开发环境中,许多人都会诉苦作业:“天天都是担任事务的增修改查,这种日子什么时分是个头啊,不想一向再做CRUD的螺丝仔了”!拥有这种心态的人不在少数,谁的心里都有个梦,起初的我也并不破例,「架构师CTO、技术总监、技术专家…..」,面临这一个个高大上的职位,曾经的我也神往过,时常幻想着什么时分我也能成为这样的人啊,这头衔说出去就倍有面子…..

但等到了这些职位的时分,你会发现每天的作业仍是和事务打交道,泡泡茶畅谈未来技术?用技术在项目中指点江山?沉沦在技术中做架构选型?其实这些都不存在,每天其实仍旧在围绕着事务兜兜转转,「高职技术人」和「普通开发者」之间,仅有差异便是把敲简略的事务代码这项作业,换成了其他更为艰难的使命。

当然,话再说回主题,已然现在无法在项目中用到各种新技术,现在的CRUD无法给自己带来技术生长,那咱们要做的便是:在有限的空间内做到“无限”的开展,其实就算最普通的事务也能玩出不同把戏,事务的增修改查想要做好也并不简略,比方怎样才干让代码更整洁、能否让程序拓宽性更好?怎样才干让代码跑的更快…..,动才干改变,诉苦再多也改变不了自身。

4.2、紧记谦善戒骄,人外有人天外有天

这个道理应该是本次阅历中,带给我感悟最深的一条,作为技术人觉得自己牛可以,但千万不要自豪,也不要在面试中、搭档攀谈中、群聊讨论中…..表露出来,由于永久会有人比你更厉害,不要为了虚荣心去故意“攀比”,不然终究倒霉的仍是自己,举个很常见的事例:

假如当过面试官的小伙伴应该都遇到过一种状况,也便是候选者在面试时有些故意装逼,这样的候选者在面试时,往往都会遭到面试官的无情打压,最简略的做法便是连环炮问法,从根底入门问源码原理,从API调用问到操作体系完结…..直到终究被问到哑口无言。

拥有自傲是功德,但千万不要自傲过头,紧记谦善戒骄,由于人外有人天外有天。比方我,原以为自己的技术已达巅峰造极,但通过这次面试后,发现自己了解的一些东西都是浮于外表的假象,看似驾轻就熟,实际上仅仅上层特性的搬砖工,学习和学会,压根是两码事!

4.3、再牛的技术也永久是为事务所服务

IT开发行业,其实有不少人抱着做纯技术开发的念想,至少我遇到过的不在少数,不想去重复做单纯的事务开发,但也请紧记:技术驱动事务,但技术也永久是为事务供给服务。
当然,想做纯技术开发也并非不可,但国内这样的人很少,或许说国内这样的岗位比较少,除开少数中间件开发、开源技术研制、根底渠道开发等作业外,大多数岗位都需求和事务打交道,所以在学习新技术时也万万不要忘了事务,等你吃透某一行业的事务时,或许给你带来的好处会胜过技术的收益。

4.4、终究的结语

到这儿,本篇内容也就完毕啦,其实我开端写技术文章的初衷,也便是单纯的想同享技术常识,由于我自身在学习的途中,遇到了许多不如意的作业,例如:

  • 看到标题特别合适自己,但点进去发现内容底子不是自己想要的….
  • 找到一篇优质文章后,仔细阅览收益颇多,但想要寻找后续时,发现压根没有…..
  • 阅览一些经典的技术书本时,总会碰到一些很难了解,或许很拗口的段落….
  • 有时发现某篇文章,名字特别吸引人,成果点进去发现是引流广告….
  • ……

正是由于上面的一些原因,促使我动了写技术文章的想法,迄今为止,个人发布的一切文章,底子上都已连载形式呈现,力求完好、优质、易懂且内容对得住标题!从2019年开端动笔,到现在转瞬已逝三个年头,但却只完结了大约4个专栏:

  • 《JVM成神路》
  • 《深入并发编程》
  • 《程序员的网络编程》
  • 《全解MySQL数据库》

四个专栏看起来并不多,更文总共才几十篇,但累计码字已有近百万,使用作业之余的空闲韶光,写出这些连载专栏,协助了不少小伙伴生长,也收成了许多小伙伴的称誉。本想着在《数据库》、《网络》两个专栏更完后,回头去写《中间件》或《散布式》专栏:

追忆四年前:一段关于我被外企CTO用登录注册吊打的不堪往事

究竟这两个专栏,是早已欠给咱们要还的债,但正在《全解MySQL数据库》专栏接近尾声,计划朝着新专栏动笔时,船长大佬找到了我:
追忆四年前:一段关于我被外企CTO用登录注册吊打的不堪往事

考虑一再,我说在年后有主意写,但在年前会先更完之前断掉的《网络》专栏,但在此之后,自己脑海里一向在想,要写什么体裁的小册合适呢?写技术类型吧,自以为水平不够,思索一段时刻后,结合现在大环境下的氛围,终究决心写实用型体裁的内容,现在规划如下(或许会调整):
追忆四年前:一段关于我被外企CTO用登录注册吊打的不堪往事

估计大约会在四月份左右上线,名字应该叫《求职的神机妙算》,全体内容倾向于代码之外的实用软技术,定价也不会太高,估量在小几十上下。为此,在终究的尾巴上,也提前给自己做个小小的宣传,有主意的小伙伴到时分可以重视一下(但假如想着分钱不花,主打的便是一个陪伴,也是完全OK的,哈哈哈~)