<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Zer0e&#39;s Blog</title>
  
  
  <link href="/atom.xml" rel="self"/>
  
  <link href="https://blog.zer0e.com/"/>
  <updated>2026-04-06T11:38:10.477Z</updated>
  <id>https://blog.zer0e.com/</id>
  
  <author>
    <name>Zer0e</name>
    
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>记一次小米售后</title>
    <link href="https://blog.zer0e.com/2026/03/07/202603-mi-after-sales/"/>
    <id>https://blog.zer0e.com/2026/03/07/202603-mi-after-sales/</id>
    <published>2026-03-07T14:14:24.000Z</published>
    <updated>2026-04-06T11:38:10.477Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>说实话，我也没想到我会在博客上分享生活琐事，只是突然觉得，偶尔分享分享不那么重要的私事好像也没什么。</p><h1 id="叙旧"><a href="#叙旧" class="headerlink" title="叙旧"></a>叙旧</h1><p>事情的起因是我的小米平板5pro，21年买的，那时候米子刚重新开了平板产品线，那时候幻想的挺好的，我要用平板用来学习、看书，偶尔再刷刷视频。有句名言叫做买前生产力，买后爱奇艺，对此我不能说嗤之以鼻吧，算是有点认可。<br>我刚工作第二个月，我用我第一个月的工资，买了一部，那时候售价2800左右吧，还挺开心的，算是工作之后第一次买东西给自己。不知不觉竟然用了4年多了，还挺耐用。<br>平板也逐步成为了我睡前看看番剧的工具，真正沦落到爱奇艺工具hhh。不过就是系统偶尔有点小毛病，不过都用米了，还要啥自行车？物理设备上，自从哪一次平板从床上跌落之后，上音量键就坏了，那时候我也没管，因为我可以用下音量键再配合触控来调节音量。应该是去年年底，我发现我的下音量键也坏了，彻底按不了了，有一说一完全可以通过下拉任务栏调节音量，而且从操作时间上没什么不同，但是我就是觉得膈应。因此我决定维修下平板。  </p><h1 id="经过"><a href="#经过" class="headerlink" title="经过"></a>经过</h1><p>春节前正好有一天年假得用完，正好约的那天线下维修，结果店里打电话过来说没有备件，说能不能等两天，我说可能得过年后了，第一次维修先告一段落。过年的时候，我决定提前几天回杭州，正好约维修。<br>维修当天，我是下午一点去线下店，店里当时有两个老哥也是维修的，师傅说要可能要一个小时往上了，说可以稍晚再过来取，我说没问题我现场等，结果等了差不多2.5小时才修完了。维修前还告诉我需要撕掉钢化膜，我说没问题。除去前面等待的几个人，整个维修过程大概要1个小时左右，整个维修过程中能听到师傅在打电话请教其他人。<br>维修完成后，当场我检测了音量键，确实修复完成了，我也没在意，一共付款70块钱，顺带27块买了条数据线，我就回去了。回到家后，打开屏幕和设置，结果发现屏幕出现了一些褶皱，我打开了CIT，查看了白屏测试，确实非常明显。我第一时间拨打了线下店的电话，询问怎么回事，师傅给我答复是正常的，使用时正常磨损，但问题是我不记得我之前有啊，他说可以再回去线下店看看，我又回去了，结果现场查看后他还是说是日常使用磨损，我那时陷入疑惑，难道之前真的有吗？因为我日常都使用的黑夜模式，这种模式下根本看不出来。<br>然后就又回家了。晚上睡前看视频时，倒是不怎么影响，因为大部分视频都不可能有纯白背景，但是进入设置后十分明显，我越想越不对，结果网上搜了一下，屏幕褶皱是什么情况，结果翻到一篇帖子，说苹果维修由于师傅开屏器温度设置不对，导致LCD屏出现褶皱。我开始观察我平板的褶皱，还真发现蹊跷，褶皱上下是对齐的，说明这真的是线下店开屏的问题。  </p><h1 id="再次维修"><a href="#再次维修" class="headerlink" title="再次维修"></a>再次维修</h1><p>于是，我决定再次维修，但这次我决定走线上维修，原因有二，一是我也理解线下店打工人的不易，二是线下店毕竟是真人PK我比较害怕。所以我拍摄了相关照片，将平板寄修给小米浙江维修中心。于是便开始了我的维权之路。<br>维修中心1号师傅检测后，确定是有类似问题，需要换屏，但是他要价800多，我说太贵了，这是你们维修的问题为啥要我承担费用，他说他们维修中心和网点是不同的网点，网点的问题他们不承担的，我觉得他说的有道理。过程中我问他你看褶皱是不是开屏温度过高导致的，他说他没法判断，还说可以打九折，我说我不接受，平板先放他们那，我走线上客服看看，师傅说可以，系统上显示的就是客户不认可检测结果。<br>由于我的售后维修都是小米商城的线上申请，我直接找客服，说了相关的事情，第二天寄修中心客服又给我打电话了，说可以7折换屏，我说我不同意。同时客服说会让线下店找我沟通。第三天傍晚，线下店之前的师傅确实电话我了，说可以5折换屏，我说不行，我的诉求就是免费换屏，你们的责任为啥要我承担。由于那时候是周五傍晚，赶去一个饭局，就没多聊。<br>结束后我还和姚老师吐槽，看看最后会不会免费给我修，身为一个十年米粉，没想到小米的售后还挺扯。不过线上客服确实干事，有问题就会解决。但是维权周期实在太久了，如果急用的话真的会气死。姚老师还评判我，说我这种十年米粉喂了狗的观点就是典型的渣女思维，DS说，这种隐含着一种“我长期支持你、付出感情和金钱，你就必须永远让我满意，否则你就是辜负我”的逻辑。我思考了下，确实没错，但是如果这次小米没给我一个满意的结果，真的挺掉粉的，虽然我又买了一个全新的平板，真的像舔狗一样。<br>周一一到，由于我周末又去催线上客服了，线下店再次打电话给我，说可以免费维修，并表示了道歉。我登录商城查看维修单，发现平板已经维修完成了。<br>至此，我的这次维修之路终于结束了。  </p><h1 id="感想"><a href="#感想" class="headerlink" title="感想"></a>感想</h1><p>如果提前知道这个维修会出这么多事，我宁愿不维修。说实话，我知道对线下店的师傅会有影响，甚至维修的费用都可能是他们自掏腰包，但是我还是决定维护自己的权益，说实话，我已经好几年没干这种事了，这种事是我高中会干的事，高中取快递的时候发现快递站点对快递是扔的，我直接投诉了，现在回想起来，还挺好笑的，明明不关我事，但我还是去做了，那时候觉得捍卫自己权益的事情好酷啊。类似的事情还有很多，比如投诉国家电网、给校长投诉高中老师等等。<br>我已经低调了很多年，原因是我明白了很多事情不是非黑即白，打工后明白了打工人的不容易等等。但有些事情涉及自己并且退一步越想越气时，我还是会出手去解决这件事，去正常维护自己的权益。  </p><h1 id="后续"><a href="#后续" class="headerlink" title="后续"></a>后续</h1><p>4月6日，我看到了一个关于小米售后的视频<a href="https://www.bilibili.com/video/BV1zLSSBFEhz?vd_source=abd010b6322f253e6138556b61403f04">新机不幸中奖了…带大家体验一下小米线上售后</a><br>真的也是感触颇深，但我知道终究会解决的。这无关普通人还是UP主，这取决于你是否懂得去争取。只是时效问题，这真的是我想吐槽小米的一点。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;说实话，我也没想到我会在博客上分享生活琐事，只是突然觉得，偶尔分享分享不那么重要的私事好像也没什么。&lt;/p&gt;
&lt;h1 id=&quot;叙旧&quot;&gt;&lt;a 
      
    
    </summary>
    
    
      <category term="生活琐事" scheme="https://blog.zer0e.com/tags/%E7%94%9F%E6%B4%BB%E7%90%90%E4%BA%8B/"/>
    
  </entry>
  
  <entry>
    <title>2025年末碎碎念</title>
    <link href="https://blog.zer0e.com/2025/12/13/2025-end/"/>
    <id>https://blog.zer0e.com/2025/12/13/2025-end/</id>
    <published>2025-12-13T06:20:27.000Z</published>
    <updated>2026-01-08T08:14:25.106Z</updated>
    
    <content type="html"><![CDATA[<h1 id="流水账"><a href="#流水账" class="headerlink" title="流水账"></a>流水账</h1><p>转眼间又到了这个时候，2025年最后一个月，人生又过了一年！这一年好像发生了很多，又好像十分平淡。又或许每天都在行尸走肉当中。总是想提笔写点什么，但又不知道如何下笔。心中好像有很多想法，但这些想法又好像一团浆糊，黏在脑子里，说不出写不出，只能影响思考。但我还是认为应该留下点什么，在2025年的年底回忆这一年，反思过往。  </p><p>那就来倒叙吧。先说说最近。11、12月比较重要的一件事是软考，只能说可惜。我其实知道自己会没过，所以成绩出来的一周我都没查，就在今天，我决定面对自己的失败。客观题44分，仅仅只差一分，案例与论文没有任何问题。很难受，说实话。我感觉自己对这个考试有点魔怔了，我想从这个考试中来肯定自己。但我想到姚老师和我说的一句话，只有无能的人才从需要通过各种考试肯定自己。是的，我认同，但我也难以接受。人作为社会性动物，来自外部的肯定对人自身是有积极作用的。你说这个证书对我有用吗，没有任何实质性的作用。那我为什么要考这个考试呢？因为我还是想从外部证明自己不比其他人差。  </p><p>所以，我认了，我承认现阶段的自己还是太狂了，一个本科毕业刚4年的小屁孩，就想冲击国内计算机最高的考试，是的，我有点膨胀了。从另一方面来看，我去年到现在考了三次，第一次只靠自己刷题，客观题差8分。第二次开始我看了点课程，但是上半年很忙，结果不仅客观题还是差8分，案例还差了3分。这一次，客观题差1分，案例超过了及格线7分。虽然今年下半年难度偏低，但整体来看自己是在进步的，运气稍差一点，但我也要承认运气也是实力的一部分。虽然明年上半年我不一定会参加考试，下次多花点时间学习，争取下次考试考过。  </p><p>聊完了软考，再聊聊生活。最近买了一个新耳机，索尼XM5，1465拿下，听音乐很赞，但是我开心不起来。不知道从什么时候开始，我开始感受不到生活上的一些快乐，买东西？打游戏？又或者出去吃饭？我感觉很难填满心中的某些空缺。我时常会想，我是不是抑郁了？事实上可能确实有点。消费只是为了让自己感到快乐这句话一点没错。我经常晚上睡觉前会刷刷购物软件，但是我也不买，有一段时间，我每晚都刷运动相机，想买，但是理性的思维让我停下了消费。最后我还是放弃了 。回到耳机，这个也是自己非常想买，线下也尝试了，最后还是拿下了。刚收到还是比较激动，后面就平常心了。回想起来，我已经好几次进行了小额度的冲动消费，眼部按摩仪、除螨仪等等。。。你说这些东西有用吧，好像又没用。但买了就买了，整体我还是满意的。    </p><p>11月份，我正式入职一周年，我的感受只有一个，那就是终于熬了一年。拿了个钥匙扣，平平无奇。我不想在开头就吐槽工作，这块就留到最后吧。这个月很忙，感觉很多事，伴随着大大小小的感冒。流感太多，身体太差。<br>再聊聊游戏，11月最遗憾的还是崩铁，陪了一整年的翁星故事终于落下帷幕，有遗憾，有美好，但我觉得编剧老师说的对，和这个故事一起成长的自己，才是那个最值得珍惜的东西。还有配套的音乐，应该是近一个月我最喜欢的音乐之一。  </p><p>10月，因为工作的原因，我回了趟老东家。回去一趟，心情还是比较复杂，以专家的title再次去交流。回过头看，我当初离开，也是年少轻狂，觉得自己牛逼的不行，内部的混沌不适合我。因此我毅然决然离开。依结果来看，工作的状态也没有多大改变，又或许工作本身就是这样。工作只是赚钱的手段之一，并不能在工作中寻找自我。我想我也逐渐懂得这个道理了。    </p><p>国庆假期，因为有天值班，没回家，当然也是借口，想在宿舍躺尸。6号去了五云山爬山，累得半死。感觉身体越来越差，还是得多锻炼。5号骑着我的电动车在良渚附近转了30多km，很爽很自由，我很喜欢那种风吹在自己身上的感觉，当然冬天除外。一直想去考摩托车驾照，可惜杭州禁摩，不然我真的想整一辆去旅游，摩旅，好像也不错。  </p><p>9月，买了个骑行支架，本来以为能把手机架在电动车拍点VLOG，结果发现我想多了，抖得不成样子，那个时候也在考虑运动相机，但是我最终还是放弃了，用手机都不愿意拍，那么我也不相信运动相机会拍。国庆前还和几个兄弟吃了一个日式烤肉？等了2小时，好像那就那样，让我吃第二次也不好说。9月还有一件事，就是认识了半年的同事离职了，从4月份共事到9月正好半年，他离职去某大厂了估计，追求他所谓的提升。但说实话我知道他只是对现在的老板不满而已，边缘下属+傻逼老板，buff实在是叠满了。他离职前我还专门和他聊过，外面也不一定好，你所谓的提升都只是表象，提升了五六千块又能咋样？不过有些事没有自己体会是不会懂的，去年离职后，我感觉自己又悟了不少。后面又陆陆续续和他聊天，他说现在天天加班到晚上9点，相当于增加的工资就是他的加班费了。他作为本地人其实钱拿少点早点下班就行了，没必要一直卷，我说他是没吃过苦的，他说是的，从第一家公司到之前为止，他基本上没有高强度上班，也祝他能找到平衡吧。<br>9月初血月，熬了个小夜，拍了照片，好像也就那样，等待的时候日常刷手机，刷到一句话，”去罗马的路很多，可以搭飞机，可以绕到北极，但也可以不去罗马。“，确实令我思考许久，但不去罗马不相当我放弃了目标吗？又或者这只是人生中的一个小插曲，不值一提呢？我想我还得思考几年。<br>9月4号，生日，今年生日我也写了一篇文章，碎碎念，那时候整个人还是比较消极的，当然现在也是消极的，只不过有些东西通顺了不少，但还有些东西还得继续思考。  </p><p>8月，遇到一个工作上的难题，我排查了两个月，才最终解决。不得不说，当去解决难题并且解决了的感觉确实很爽，之前为什么喜欢编程，因为编程就是一个个问题的集合，当我完成编码看着他们运行起来，那感觉确实很开心。运维也是一样，不过运维解决问题基本上是系统要有问题了，压力相对大点。8月还出了一件事，就是封禁了443端口，导致跨境网站都用不了，有人说是临时工，但我觉得应该也是在测试什么导致手滑配置错误了。这件事之后我才发现原来源码已经泄漏了，大家都在研究，真的好家伙，我也偷偷下载了，不过除了一些文档外，代码我啥都没看。<br>这个月团队还去了团建，青岛，滨海城市，感觉和泉州差不多，吃了海鲜，自驾了环海公路，第一次坐了绿皮火车过夜，基本整夜无眠，回来第一次坐了商务座，好像也没啥。团建没和老板行动都是开心的，总体来说还不错。  </p><p>7月，星铁送我的纪念册到了，不过直到写这篇文章的时候，我才发现我包装都没拆，米哈游每年都会送点东西，这点还不错。这个月还和小伙伴一起去了西湖的宝石山，爬山确实累，不过还是得多锻炼，今年又胖了不少，现在已经60多kg了。。。这个月还请小伙伴吃了泉州菜，宋小瑾这家店真的见证了它，从我们小镇上的一个小餐馆，逐步开成了连锁，这个老板也是牛逼。<br>7月13日，那天很晚的时候有一个私人号码给我打电话，说他是警察，怀疑我被诈骗了，让我不要转账，我啥操作都没呢，他说我支付宝有消息送到他们系统里。那天我确实尝试把银行卡绑定到paypal，但是无法支付。随后，公司的应急响应中心也给我发消息，说公安推送我有被诈骗的风险，说实话，我害怕的不是我的钱被骗，而是我做了什么操作，后台都看的清清楚楚，美名其曰反诈，我背后一阵发凉。<br>7月初，和帅哥一起去了趟义乌，看了小商品，帅哥想做网上玩偶、盲盒，问我有没有兴趣，基本没啥成本，找代发就行，只要会运营账号，我说可以，一起去义乌看看有没有渠道，还让他请了两顿饭。后面又找了两个小伙伴，揍了1w块钱，每个人搞一个平台的店铺，把货铺开，尝试运营账号，由于他老婆是视频剪辑，因此视频基本都是他老婆剪，我们发到平台上。结果做了两个月，他要结婚买房装修啥的太忙了，就没做了，还有9千多块在我的账户上。也许哪一天又重启了，但说实话这些钱在我这压力也挺大。我也不好去投资啥的。  </p><p>6月，去吃了一家还行的新疆料理，离职的那位同事一起带吃，确实还行，但是感觉羊肉太油腻了。6月和帅哥还一起做了一个爬虫项目，本来我是友情支持，但帅哥非得给我钱，好像给了我几千块，我也坳不过收了，虽然交付后账号都被封了，但老板也没说啥，本来帅哥还想靠着这条路致富，结果出身未捷身先死。也算是一个人生的小插曲，大起大落。<br>这个月还看了一本书，叫停止精神内耗，这本书的很多观点我都认同。”因为追求太多，我们的目光总是投向遥不可及的未来，却看不到身边的美好。年轻时，不懂珍惜，将时间都花在虚无的追求上，随着年龄的增长，这些东西变得毫无实用价值买新的焦虑又会产生，我们要把目光投向未来，看到问题的本质，珍惜当下。“、”我们之所以痛苦，是因为追求过多。我们不该因为失去什么而感到不幸，学会选择，有所放弃。有些东西永远无法拥有，有些人最终会从熟悉到陌生。如果不愿面对现实，只会错过当前的幸福。“<br>类似的观点还有很多，如果真的有人能看到这的话，我建议你去看看这本书。  </p><p>5月底，端午节回了趟老家，老爹又要出国，想着回家看看，家里人也挺不容易的，当初为何毕业后想工作赚钱，其实也有这个理由，养活自己。后面其实老爹老妈都聊过，他们都知道我的想法（想出来赚钱），我的大学生活还是比较拮据的，没有特别多的活动，我最大的开销就是买那台笔记本，花了7000块钱，其中还有几千块是我高中的时候攒下来的。这次回去，老家又改变了不少，各种旅游设施都建好了，老家被拆的房子也被改造成休息点了，住了十几年的房子，想想也挺感慨的。<br>这个月底，还参加了上半年的软考，难度比下半年高很多，结果也是没通过，不提也罢。劳动节假期，和大学的好兄弟一起去了青山湖骑行了30km，感觉很爽，还是第一次尝试骑行那么远，花了3个多小时好像，没啥特别累的感觉，路上看了很多风景，产生用无线电呼叫附近的火腿，但是没有得到回应。假期还发生了一件令我很不爽的工作上的事，老板真的傻逼，对外软弱，对内重拳出击，也因此我和北区团队结下了梁子，这时也在我心里种下了一枚离职的种子。    </p><p>4月底，由于团队合并，老板让我去新团队，因此去了趟广州，认识了另一个团队的大佬们，餐桌上有说有笑，现在回忆起来也是真够搞笑的，在那时吃饭的成员中，两个转岗，一个离职，哈哈哈，那时候大家不知道是怎么想的。也正是这次的广州出差之行，让我知道老板是怎么样的人，让我和另一个同学住一间双人间，自己却住豪华酒店，啧啧啧。据另一个同学说，他出差的时候想住近一点，结果2个人住的总额超了单人标准，老板还让他们付额外的钱。。。令人震惊。广州，挺好的一个城市，和北京的同事去了广州塔，拍了一些照片，和他聊了聊，北京买房生娃感觉他也挺不容易的。这次还是我第一次坐飞机，挺普通的出行方式，但愿不要出现意外情况。<br>这个月，拿到了我的无线电执照，第一次参加了无线电点名，这里是BG5FFU，很激动，有时候觉得有一群志同道合的网友一起喜欢一件事感觉也很不错，只可惜，我好久没有打开过我的无线电手台了。写下这段话的时候，我把我的手台重新插上了电源，看看能否收到来自其他火腿的信息。  </p><p>3月下旬，去了趟颈椎科推拿，感觉脖子很僵硬，好久没去了，最近好像又是之前的感觉，脖子很酸，找个时间需要再去一趟。这个月还参加了公司的培训，入职半年后第一次参加了新人营，没有什么特别大的归属感对于公司，我感觉地位很低，也被人看不起。姚老师说，自己应该被自己定义，而不是通过其他人评价你来获得。也没问题，但这个社会是群居的，很难逃过别人的评价，但只是因为某一个点不达标就被贬低，这社会真的病了。<br>这个月还去考了无线电执照，学习了半个月的系统知识，看了好几款手台，最终以96分通过了考试，也算顺利。以无线电为契机，我也学习了很多知识、工具、软件等等，这个圈子很小众，但是很多人都很和善。   </p><p>2月，春节后回杭州，处于不想上班的氛围中，偶尔请朋友或者朋友请我吃饭，这个月好像很平淡，帮客户解决了很多问题，我想我那时候应该是迷茫的。  </p><p>1月底，春节会老家，一大群亲戚去了古城上徒步，拍了很多照片，回看的时候感觉还在昨天，前面提到端午节的时候，说老家的旅游越办越好了，其实我每次回老家都有这样的感觉，我感觉大半年甚至一整年才回去一趟让我跟不上，很多昔日的地点当我再踏上时他却是另一番景色，物是人非。我时常在想原理家乡的我，为何在杭州，以后如何发展，如何照顾老人。今年老妈搬到了新家，装修的很好，她说是为了将来的我结婚用的，我说还早，她总会反驳我说隔壁谁谁谁小孩都有了，你还在单身。其实这个话题我也想聊聊，但说到底无非觉得比较自由，加上自己还是比较懒的，除了工作外只想睡觉打游戏，谈恋爱结婚在我想法中优先级是比较低的。  </p><h1 id="碎碎念"><a href="#碎碎念" class="headerlink" title="碎碎念"></a>碎碎念</h1><p>磨了好久，终于把流水账磨完了，有一说一，拖延症还是比较严重，是个坏习惯，总是把一些不那么重要的事向后推，宁愿去玩游戏、刷视频或者看书。不过还是主要不知道怎么去总结这一年的事情。发生了好多但又好像没有可以刻印在人生轨迹中的重大事件，一切都很平淡。姚老师说，牛马才会反思总结，我四分之三认可吧，不可否认，的确牛马总是喜欢反思自己的问题，试图从自身去解释整个世界，也许是被这个社会PUA惯了，从小的惯性思维。但还有四分之一也不能说否认吧，我觉得人生还是每隔一段时间需要回看下发生了什么，其实我也是在写这段话的时候才意识到，好多发生的事我的记忆好模糊，因为他们可能是一些微不足道的小事，或者说没有在人生中留下浓墨重彩，它们带给我的是那段时间的感动。也许其实每件发生的事对于人来说都是微不足道的呢？记忆总有一天会忘却，唯有文字可以记录。我为什么搭建这个博客，理由我已经忘了，也许是为了炫技？又或者真的想记录知识？翻看我最初的博客，当初我好像只是想记录知识而已。但是不知从何时起，我不爱记录知识了，曾经喜欢探索的心，似乎也随着工作慢慢磨平了。<br>今年工作上的事，一般，黄神总是push我跳槽，我能感受到他的焦虑，他和我经历有点像，但他学历比我高，我时常安慰他，不要太过焦虑，只是这个社会的原因。但实际上我好像也很焦虑，我不知道在焦虑什么，也许是因为工作，又或者是钱，又或者是其他的事。姚老师总是批评我，内心需要安定一点，我觉得他批评的对，但这也是当代年轻人的现状，姚老师人生洒脱，内心平静，有时候我也羡慕他，他总是反说我也可以，的确，寻找内心的宁静说简单也简单，但说实话很挺难。当我有足够的资金去支撑我的生活吗？未必，那时候又或者有新的想法。<br>工作上的事我也不想吐槽了，除了老板和值班工作以外都好，有时候忙碌，有时候空闲，不难，说实话有点过于安逸了。这个团队目前问题挺大的，不晓得今年这个团队会咋样，不然又得回去干迁移了，感觉挺没劲的，该准备还是准备起来，给自己留条后路吧。<br>今年最遗憾的事，还是没能通过软考，一分之差，不过也没啥好遗憾的，也怪自己没好好学习。不过这个失败也让我感觉今年好像没什么技术上的提升。明年看看要不要接着努力吧。<br>年轻人总是迷茫地过着日子，没错，我也不例外，我至今没找到自己想做的，只是浑浑噩噩混着，好吧，也不算，最起码有些事情也是感兴趣的。新的一年希望能把技术博客重新写起来，回归下本心，静下心来学习学习，顺带看看其他机会。希望新的一年，一切顺利，也祝所有家人朋友能找到属于自己的宁静，身体健康，发大财。  </p><p>写于2025.12.13，更新于2026.1.8.</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;流水账&quot;&gt;&lt;a href=&quot;#流水账&quot; class=&quot;headerlink&quot; title=&quot;流水账&quot;&gt;&lt;/a&gt;流水账&lt;/h1&gt;&lt;p&gt;转眼间又到了这个时候，2025年最后一个月，人生又过了一年！这一年好像发生了很多，又好像十分平淡。又或许每天都在行尸走肉当中。总是想
      
    
    </summary>
    
    
      <category term="随想" scheme="https://blog.zer0e.com/tags/%E9%9A%8F%E6%83%B3/"/>
    
  </entry>
  
  <entry>
    <title>写于2025年生日前</title>
    <link href="https://blog.zer0e.com/2025/09/03/2025-birthday/"/>
    <id>https://blog.zer0e.com/2025/09/03/2025-birthday/</id>
    <published>2025-09-03T14:14:24.000Z</published>
    <updated>2025-09-03T14:20:56.095Z</updated>
    
    <content type="html"><![CDATA[<h1 id="回望"><a href="#回望" class="headerlink" title="回望"></a>回望</h1><p>又是一年过去，2025年又过去了8个月。去年生日前我也写过一篇文章，那时候离职超两个月，心态有点差，和朋友去外面走了走，聊了聊天，虽然也是苦闷但也不至于特别丧。<br>十月份过后，我入职了新公司，以全新的起点、全新的挑战去面对没听说过的岗位，那时候还是比较期待的。入职后马上就开始熟悉工作，技术上从云网络入手，逐步扩散到云上的大部分产品，那段时间学习还是比较快的。后面跟随一位大佬做客户迁移工作，现在想想有做的好的地方，也有不好的地方，但总之都顺利完成任务了。  </p><p>今年4月，组织架构调整，另一个团队合并到我所在的团队，可能老板认为我是比较不错的，因此被他拉着去做另一份他的重点工作。随后便开始接触故障应急、客户稳定性、故障演练等等一些工作，现在回想，也许老板那时候只想多拉点人来干这块吧，很苦，尤其是应急值班，一有大故障那真的要命，不仅如此，有夜班oncalll还睡不踏实，真的非常难受。  </p><p>今年5月，我开始跟随两位大佬做客户稳定性相关的工作，但说是客户稳定性，但其实我能做的有限，能和客户交流的有限，而且比较有局限性，不是从客户架构的全局视角出发，而是从某个点切入分析问题。负责对客交流架构还是交给另一位大佬讲述，我负责打下手。仔细想想，我也就让客户将业务监控接入了工单系统，有问题可以第一时间排查，也没有特别有亮点的地方。  </p><p>另一块客户稳定性工作是故障演练，这块我自认为研究的还是比较深入的，当然我指的不是理论方面，而是从实际故障注入的角度来说，包括开源的chaosblade工具，也深入了代码。故障演练这块给几个客户去现场讲述发展与体系，也演示了具体效果，还给一些客户进行了培训。总之这块收获颇丰。</p><h1 id="现状"><a href="#现状" class="headerlink" title="现状"></a>现状</h1><p>最近有位同事离职了。现在团队可以分为两块业务A和B，后面合并过来的是B业务。他就属于B业务的一员。至于离职原因我认为还是因为和我的老板不对付，并且值班压力大。说实话我挺理解他的，他属于边缘人，现在要求越来越多，他也担心哪天出了问题。但我说实话完全不是事，出问题再说呗，上次心理老师问我，你觉得如果领导责备会对你有什么影响吗？是走人还是说绩效不好？说实话我都不在意。<br>所以我觉得能过就过吧，而且他属于裸辞，说实话，经历过去年那段时期，我对裸辞还是比较谨慎的。如今工作更难找了，但是我也理解他所处的位置，不走的话更难受。<br>目前团队现状就是老板过于傻x，整天整细节和微操，他不参活事照样干的顺利。至于我个人成长只能说还行，也有提升但是不接触代码后感觉少了什么，虽然偶尔也会写写脚本，但感觉还是差了点啥。<br>故障应急值班是真的累，睡不踏实，周末有时候也不敢出门，这是目前最难受的一点。</p><h1 id="未来"><a href="#未来" class="headerlink" title="未来"></a>未来</h1><p>也和几位大佬聊了一下，不太推荐我在现在这个岗位，技术上还不错，还年轻应该出去多拼拼。其实道理我也都明白，但是总感觉还能带给我什么，除了领导过于傻x以外，并且值班压力大，其他真的也能自由学到很多东西，这也是让我很纠结的一个点。不过今天就是我入职的296天了，马上就一年了，机会还是要多看看，看能否去一个更好的地方。<br>今年是我27岁，马上就奔3了，一没事业，二没家庭，感觉还是在漂泊，不知道方向何在，但总是默默勉励自己还可以，未来会好起来的。</p><h1 id="题外话"><a href="#题外话" class="headerlink" title="题外话"></a>题外话</h1><p>之前去上海的那个朋友从大厂离职了，家里人介绍后去了家国企，不得不感叹还是得关系硬啊，可惜家里没权没关系，都得自己拼自己博机会，只是有时候想，这打工何时才是个头啊。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;回望&quot;&gt;&lt;a href=&quot;#回望&quot; class=&quot;headerlink&quot; title=&quot;回望&quot;&gt;&lt;/a&gt;回望&lt;/h1&gt;&lt;p&gt;又是一年过去，2025年又过去了8个月。去年生日前我也写过一篇文章，那时候离职超两个月，心态有点差，和朋友去外面走了走，聊了聊天，虽然也是苦
      
    
    </summary>
    
    
      <category term="随想" scheme="https://blog.zer0e.com/tags/%E9%9A%8F%E6%83%B3/"/>
    
  </entry>
  
  <entry>
    <title>秒杀系统设计</title>
    <link href="https://blog.zer0e.com/2025/03/15/seckill-system-design/"/>
    <id>https://blog.zer0e.com/2025/03/15/seckill-system-design/</id>
    <published>2025-03-15T07:53:23.000Z</published>
    <updated>2025-03-15T10:39:34.389Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>如何设计一个秒杀系统，想必大家一定在互联网上看了各种文章，本文结合内部系统的一些实践还有内部分享，总结了一些系统设计要点，供没有相关经验的同学参考。</p><h1 id="什么是秒杀"><a href="#什么是秒杀" class="headerlink" title="什么是秒杀"></a>什么是秒杀</h1><p>在电商领域，存在着典型的秒杀业务场景，简单来说就是一件商品的购买人数远远大于这件商品的库存，而且这件商品在很短的时间内就会被抢购一空。即特定SKU的瞬时访问需求呈现指数级增长，而可售库存量级则维持极低水平，最终形成供不应求的”库存击穿”现象。</p><p>比如每年的618、双11等业务场景，就是典型的秒杀业务场景。例如2023年双11期间，热门3C单品的每秒订单峰值达12万笔，库存释放后平均3.2秒即告售罄。</p><p>秒杀活动的目的是拉新和促活。<br>拉新：通过实施”锚定商品”策略，选取高认知度、强引流属性的商品设置超低折扣，利用价格敏感型消费者的逐利心理，构建用户转化漏斗。最终形成”流量磁铁”效应。<br>促活：为主站引流，进而带动高客单价商品的销售。</p><h1 id="秒杀的特点"><a href="#秒杀的特点" class="headerlink" title="秒杀的特点"></a>秒杀的特点</h1><ul><li><p>业务特点</p><ul><li><p>限时限量限价</p></li><li><p>活动预热</p></li><li><p>持续时间短</p></li></ul></li><li><p>技术特点</p><ul><li><p>瞬时流量突增</p></li><li><p>读多写少</p></li><li><p>数据一致性要求高</p></li></ul></li></ul><h1 id="秒杀系统的挑战"><a href="#秒杀系统的挑战" class="headerlink" title="秒杀系统的挑战"></a>秒杀系统的挑战</h1><h2 id="1-巨大的瞬时流量"><a href="#1-巨大的瞬时流量" class="headerlink" title="1. 巨大的瞬时流量"></a>1. 巨大的瞬时流量</h2><p>作为网站运营体系中的补充性营销手段，秒杀活动具有显著的突发性特征：其业务周期通常以分钟级计算，却在极短时间内会产生指数级增长的并发访问请求。若将此类活动与常规业务系统进行混合部署，将面临双重风险：其一，突发流量可能引发服务器资源过载，导致正常业务响应延迟；其二，更严重的情况下可能造成系统级联故障，致使整体服务平台瘫痪。因此，构建针对瞬时流量洪峰的防御体系，需要从多维度建立系统化的应对策略。</p><h2 id="2-热点问题"><a href="#2-热点问题" class="headerlink" title="2. 热点问题"></a>2. 热点问题</h2><p>在分布式系统架构中，热点问题始终是高并发场景的核心挑战，主要表现为两种形态：操作型热点（如高频接口调用）和数据型热点（如特定商品的库存访问）。对热点的有效识别与治理直接关系到系统稳定性。</p><ul><li>异常热点请求的资源侵占效应：即使恶意请求仅占全局流量的百万分之一量级，若未建立有效的请求过滤机制，这类无效操作可能占据90%以上的计算资源，形成严重的资源错配现象。这不仅导致基础设施成本的无谓损耗，更会挤压正常业务请求的处理能力。</li><li>合法热点请求的优化必要性：对于经过验证的有效热点请求，系统架构需要建立分级处理机制。通过热点预判、动态缓存策略、读写分离架构等手段，实现服务资源的精准调度。例如，针对秒杀商品的库存访问，可采用缓存替代传统数据库操作。</li><li>热点扩散的连锁风险防控：未受控的热点可能引发次生问题，如缓存击穿导致的数据库雪崩，或分布式锁竞争引发的服务阻塞。这要求系统设计时需建立热点监控体系，实现毫秒级的热点识别与动态降级能力。</li></ul><table><thead><tr><th>热点操作</th><th>热点数据</th></tr></thead><tbody><tr><td>零点刷新、零点下单、零点添加购物车都属于热点操作</td><td>对于秒杀活动，大家抢购的都是同一个商品，所以这个商品直接就被推到了热点的位置，不管你是用的数据库，还是分布式缓存”都无法支持几十万、上百万对同一个key的读写</td></tr><tr><td>热点操作是用户的行为，无法约束，但可以做一些限制，比如用户频繁刷新页面提示并阻止</td><td>热点数据的处理有三步，一是热点识别，二是热点隔离，三是热点优化</td></tr></tbody></table><h2 id="3-超卖问题"><a href="#3-超卖问题" class="headerlink" title="3. 超卖问题"></a>3. 超卖问题</h2><p>秒杀开始时，同时会有很多用户请求秒杀接口，如果查询库存和扣减库存这两步操作如果不能保证原子操作。一定会导致超卖问题，造成资损。<br>如何防止商品超卖造成资损，是需要我们重点考虑的。</p><h2 id="4-接口防刷问题"><a href="#4-接口防刷问题" class="headerlink" title="4. 接口防刷问题"></a>4. 接口防刷问题</h2><p>黄牛党能够利用机器下单、人肉抢单，将秒杀商品瞬间抢到手，然后高价格售出赚取差价。大规模的批量机器下单，还会对网站的流量带来压力，产生类DDOS攻击，甚至能够造成网站瘫痪。<br>如何防正这类软件的重复无效请求，防止不断发起的请求也是需要我们针对性考虑的。</p><h1 id="秒杀系统架构原则"><a href="#秒杀系统架构原则" class="headerlink" title="秒杀系统架构原则"></a>秒杀系统架构原则</h1><h2 id="1-尽量将请求拦截在上游"><a href="#1-尽量将请求拦截在上游" class="headerlink" title="1. 尽量将请求拦截在上游"></a>1. 尽量将请求拦截在上游</h2><p>尽量把请求拦截在上游，逐层过滤减少请求，让请求在秒杀系统可承受范围之内。</p><p>我们将请求进行分层，从用户侧到系统分别经过DNS、CDN、WEB网关、WEB服务、中间件、DB。</p><h2 id="2-数据要尽量少"><a href="#2-数据要尽量少" class="headerlink" title="2. 数据要尽量少"></a>2. 数据要尽量少</h2><p>一是用户请求的数据能少就少，包括上传给系统的数据和系统返回给用户的数据。二是要求系统依赖的数据能少就少，包括系统完成某些业务逻辑需要读取和保存的数据，这些数据一般是和后台服务以及数据库打交道的。数据库本身容易成为一个瓶颈，数据越简单、越小则越好。</p><h2 id="3-请求数要尽量少"><a href="#3-请求数要尽量少" class="headerlink" title="3. 请求数要尽量少"></a>3. 请求数要尽量少</h2><p>当用户请求的网页内容完成基础加载后，浏览器的页面渲染流程实际上还会触发一系列次级资源请求。这些被统称为”附加请求”的资源不仅包含基础的CSS样式表和JavaScript脚本文件，还涉及页面中的各类图像素材、异步数据请求（如Ajax技术实现的动态内容加载）等关键元素。</p><p>从网页性能优化的角度考虑，应当尽可能压缩这类附加请求的总数量。其根本原因在于每个网络请求的发起都会产生多维度的性能损耗：首先在连接建立阶段需要完成完整的TCP三次握手协议，若涉及HTTPS加密通信还会增加TLS协商的额外开销；其次，受限于浏览器对同域名并发请求数的限制（通常为6-8个），以及部分资源（特别是JavaScript脚本）因可能修改DOM结构而必须采用的串行加载机制，请求队列的堆积会显著延长关键渲染路径；再者，当资源分布在多个不同域名时（如使用CDN加速或第三方服务），每个新域名都需要经历完整的DNS解析过程，包括递归查询、缓存验证等环节，这在网络状况欠佳时可能产生数百毫秒级的延迟。</p><p>因此，通过减少附加请求总量，可以有效规避上述各环节的潜在性能瓶颈，从而显著降低网络传输开销、提升资源加载效率，并最终改善终端用户的体验感知。</p><h2 id="4-请求路径要尽量短"><a href="#4-请求路径要尽量短" class="headerlink" title="4. 请求路径要尽量短"></a>4. 请求路径要尽量短</h2><p>请求路径特指客户端请求从发起到最终数据返回所经历的系统节点拓扑。从接入层网关到业务逻辑服务，再到数据库集群，每个节点都会引入特定处理时延。缩短调用链路不仅能提升系统可用性（减少单点故障概率），更能产生双重性能增益：其一降低序列化开销（每经过一个RPC节点至少触发两次编解码操作，常见协议如JSON序列化的CPU耗时可达毫秒级），其二压缩网络传输时间（跨机房调用通常产生5-15ms额外延迟）。</p><p>典型优化策略包括但不限于：对业务强耦合的服务实施”物理部署聚合”，通过JVM内方法调用替代跨进程通信，此举可消除协议转换开销（如Dubbo协议头128字节的无效负载）</p><h2 id="5-依赖要尽量少"><a href="#5-依赖要尽量少" class="headerlink" title="5. 依赖要尽量少"></a>5. 依赖要尽量少</h2><p>系统依赖可划分为强依赖（服务不可降级）与弱依赖（具备柔性处理能力）两类，其中强依赖构成系统稳定性关键路径。科学的依赖治理需建立三维管控体系：首先实施”服务分级认证”，构建0-3级金字塔型架构（0级：直接影响核心交易链路；1级：重要业务支撑系统；2级：非关键业务系统；3级：辅助型系统），并遵循”同级或向下兼容”原则——0级系统仅允许依赖同级或更高级服务；其次建立”熔断防护机制”，通过Hystrix等框架实现故障服务的快速隔离，当1级依赖系统（如优惠券服务）响应成功率低于阈值时，自动触发服务降级并返回托底数据；最后采用”异步化改造”，将非必要同步依赖转换为消息队列驱动的异步处理模式。以支付系统为例，其作为0级系统对接的风控服务应保持同级，而日志采集等辅助功能需通过Kafka异步解耦，确保核心链路不受边缘服务故障影响。</p><h1 id="系统设计"><a href="#系统设计" class="headerlink" title="系统设计"></a>系统设计</h1><h2 id="DNS处理"><a href="#DNS处理" class="headerlink" title="DNS处理"></a>DNS处理</h2><p>DNS 层可以做一些和网络相关的防攻击措施，例如CNAME-WAF，这层我们无法写业务，但是可以拦截一些攻击请求。</p><h2 id="前端设计"><a href="#前端设计" class="headerlink" title="前端设计"></a>前端设计</h2><h3 id="前端页面动静分离"><a href="#前端页面动静分离" class="headerlink" title="前端页面动静分离"></a>前端页面动静分离</h3><p>动静分离的首要目的是将动态页面改造成适合缓存的静态页面。这个如今的新系统大多数都能采用这种设计。下面我来介绍几种常用的手段。</p><h4 id="数据拆分"><a href="#数据拆分" class="headerlink" title="数据拆分"></a>数据拆分</h4><ul><li>用户：用户身份信息包括登录状态以及登录画像等，相关要素可以单独拆分出来，通过动态请求进行获取；与之相关的广告推荐，如用户偏好、地域偏好等，同样可以通过异步方式进行加载。</li><li>时间：秒杀时间是由服务端统一管控的，可以通过动态请求进行获取</li></ul><h4 id="数据静态缓存"><a href="#数据静态缓存" class="headerlink" title="数据静态缓存"></a>数据静态缓存</h4><p>三种方式：1、浏览器；2、CDN；3、服务端。这个没什么好说的，都是基础了。</p><h4 id="数据整合"><a href="#数据整合" class="headerlink" title="数据整合"></a>数据整合</h4><p>这里介绍两种方案。</p><ul><li>ESI方案：Web代理服务器上请求动态数据，并将动态数据插入到静态页面中，用户看到页面时已经是一个完整的页面。这种方式对服务端性能要求高，但用户体验较好</li><li>CSI方案：Web代理服务器上只返回静态页面，前端单独发起一个异步JS请求动态数据。这种方式对服务端性能友好，但用户体验稍差。</li></ul><h3 id="接口url动态化"><a href="#接口url动态化" class="headerlink" title="接口url动态化"></a>接口url动态化</h3><p>为了避免有程序访问经验的人通过下单页面直接访问秒杀后台接口来秒杀货品，我们需要将秒杀的url实现动态化，即使是开发整个系统的人都无法在秒杀开始前知道秒杀的url。具体的做法就是通过hash一串随机字符作为秒杀的url，然后前端访问后台获取具体的url，后台校验通过之后才可以继续秒杀。</p><h3 id="前端页面拦截处理"><a href="#前端页面拦截处理" class="headerlink" title="前端页面拦截处理"></a>前端页面拦截处理</h3><p>在用户点击秒杀按钮之后，可以先进行用户行为判断，这样可以拦截掉很多刷接口的秒杀器。</p><h4 id="验证码、拼图、答题方式"><a href="#验证码、拼图、答题方式" class="headerlink" title="验证码、拼图、答题方式"></a>验证码、拼图、答题方式</h4><p>在发起秒杀后端请求之前，先进行验证码、拼图、答题判断，验证成功才可以继续后续流程。</p><h4 id="限制请求频次方式"><a href="#限制请求频次方式" class="headerlink" title="限制请求频次方式"></a>限制请求频次方式</h4><p>限制用户点击秒杀按钮的频率，强制用户等待。这属于前端限流，用户在秒杀按钮点击以后发起请求，那么在接下来的5秒是无法点击(通过设置按钮为disable)。这一小举措开发起来成本很小，但是很有效。</p><h2 id="接入风控"><a href="#接入风控" class="headerlink" title="接入风控"></a>接入风控</h2><p>对于秒杀器等机器秒杀行为，需要在上游做拦截处理，防止它们超高频的请求对秒杀系统造成巨大的流量压力。秒杀系统需要接入营销风控系统，对羊毛党、黄牛党等营销作弊行为进行风险监控、预警、识别、过滤、阻断。这样秒杀系统的压力就转移到风控系统了，减少了自身压力。</p><h2 id="服务端设计"><a href="#服务端设计" class="headerlink" title="服务端设计"></a>服务端设计</h2><p>秒杀系统高可用设计，有隔离、削峰限流、缓存热点处理、熔断降级、扩容等一系列措施。</p><h3 id="隔离设计"><a href="#隔离设计" class="headerlink" title="隔离设计"></a>隔离设计</h3><p>通过秒杀流量的隔离，我们能够把巨大瞬时流量的影响范围控制在隔离的秒杀环境里。</p><ul><li>业务隔离：把秒杀做成一种营销活动，卖家要参加秒杀这种营销活动需要单独报名，从技术上来说，卖家报名后对我们来说就有了已知热点，因此可以提前做好预热。</li><li>系统隔离：系统隔离更多的是运行时的隔离，可以通过分组部署的方式和另外99%分开。秒杀可以申请单独的域名，目的也是让请求落到不同的集群中。</li><li>数据隔离：秒杀所调用的数据大部分都是热点数据，比如会启用单独的Cache集群或者MySQL数据库来放热点数据，目的也是不想0.01%的数据有机会影响99.99%数据。</li></ul><h3 id="削峰限流"><a href="#削峰限流" class="headerlink" title="削峰限流"></a>削峰限流</h3><p>限流是一种有损技术削峰。<br>验证码、拼图、答题以及异步化消息队列可以归为无损削峰。</p><h4 id="限流"><a href="#限流" class="headerlink" title="限流"></a>限流</h4><p>限流是系统自我保护的最直接手段，再厉害的系统，总有所能承载的能力上限，一旦流量突破这个上限，就会引起实例宕机，进而发生系统雪崩，带来灾难性后果。<br>可以使用Sentinel、Hystrix、Ratelimiter等做限流。</p><h4 id="削峰"><a href="#削峰" class="headerlink" title="削峰"></a>削峰</h4><p>产品方式的削峰：验证码、拼图、答题。</p><p>技术方式的削峰：异步化消息队列。</p><h3 id="热点处理"><a href="#热点处理" class="headerlink" title="热点处理"></a>热点处理</h3><p>热点数据分为“静态热点数据”和“动态热点数据”。</p><p>静态热点数据是能够提前预测的热点数据例如，我们可以通过卖家报名的方式提前筛选出来，通过报名系统对这些热点商品进行打标。另外，我们还可以通过大数据分析来提前发现热点商品，比如我们分析历史成交记录用户的购物车记录，来发现哪些商品可能更热门、更好卖，这些都是可以提前分析出来的热点。</p><p>动态热点数据不能被提前预测到的，系统在运行过程中临时产生的热点例如，卖家在抖音上做了广告，然后商品一下就火了，导致它在短时间内被大量购买。缓存热点数据</p><p>优化热点数据最有效的办法就是缓存热点数据，如果热点数据做了动静分离，那么可以长期缓存静态数据。但是，缓存热点数据更多的是“临时”缓存，即不管是静态数据还是动态数据，都用业个队列短暂地缓存数秒钟，由于队列长度有限，可以采用LRU淘汰算法替换。</p><h4 id="限制热点数据"><a href="#限制热点数据" class="headerlink" title="限制热点数据"></a>限制热点数据</h4><p>限制更多的是一种保护机制，限制的办法也有很多，例如对被访问商品的ID做一致性Hash，然后根据Hash做分桶，每个分桶设置一个处理队列，这样可以把热点商品限制在一个请求队列里，防止因某些热点商品占用太多的服务器资源，而使其他请求始终得不到服务器的处理资源。</p><h3 id="限购"><a href="#限购" class="headerlink" title="限购"></a>限购</h3><p>限购之于库存，就像秒杀之于下单，前者都是后者的过滤网和保护伞。<br>限购的主要功能就是做商品的限制性购买，一般限制的维度主要包括两方面。</p><h4 id="商品维度限购"><a href="#商品维度限购" class="headerlink" title="商品维度限购"></a>商品维度限购</h4><p>最基本的限制就是商品活动库存的限制，即每次参加秒杀活动的商品投放量。如果再细分，还可以支持针对不同地区做投放的场景，比如我只想在北京、上海、广州、深圳这些一线城市投放，那么就只有收货地址是这些城市的用户才能参与抢购，而且各地区库存量是隔离的，互不影响。</p><h4 id="个人维度限购"><a href="#个人维度限购" class="headerlink" title="个人维度限购"></a>个人维度限购</h4><p>以个人维度来做限制，这里不单单指同一用户ID，还会从同一手机号、同一收货地址、同一设备IP等维度来做限制。比如限制同一手机号每天只能下1单，每单只能购买1件，并且一个月内只能购买2件等。个人维度的限购，体现了秒杀的公平性。</p><h3 id="减库存"><a href="#减库存" class="headerlink" title="减库存"></a>减库存</h3><p>减库存一般有三种方式：下单减库存，付款减库存，预扣库存。</p><p>而秒杀这个场景，应该采用哪种方案比较好呢？</p><table><thead><tr><th>名称</th><th>下单减库存</th><th>付款减库存</th><th>预扣库存</th></tr></thead><tbody><tr><td>定义</td><td>即当买家下单后，在商品的总库存中减去买家购买数量</td><td>即买家下单后，并不立即减库存，而是等到有用户付款后才真正减库存，否则库存一直保留给其他买家</td><td>买家下单后，库存为其保留一定的时间(如10分钟)，超过这个时间，库存将会自动释放，释放后其他买家就可以继续购买。</td></tr><tr><td>问题</td><td>竞争对手通过恶意下单的方式将该卖家的商品全部下单，让这款商品的库存减为零，但是不付款，从而影响卖家正常的商品销售。</td><td>“付款减库存”因为下单时不会减库存，所以也就可能出现下单成功数远远超过真正库存数的情况，会导致很多买家下单成功但是付不了款，买家的购物体验自然比较差</td><td>针对恶意下单这种情况，虽然把有效的付款时间设置为10分钟，但是恶意买家完全可以在10分钟后再次下单，或者采用一次下单很多件的方式把库存减完。</td></tr></tbody></table><p>由于参加秒杀的商品，一般都是“抢到就是赚到”，所以成功下单后却不付款的情况比较少，再加上卖家对秒杀商品的库存有严格限制，所以秒杀商品采用“下单减库存”更加合理。<br>库存超卖的问题主要是由两个原因引起的，一个是查询和扣减不是原子操作，另一个是并发请求无序。</p><h4 id="库存不能扣减为负数"><a href="#库存不能扣减为负数" class="headerlink" title="库存不能扣减为负数"></a>库存不能扣减为负数</h4><p>要保证大并发请求时库存数据不能扣减为负数，有以下几个方案：</p><ul><li>在应用程序中通过事务来判断，即保证减后库存不能为负数，否则就回滚</li><li>直接设置数据库的字段数据为无符号整数，这样减后库存字段值小于零时会直接执行SQL语句来报错</li><li>使用CASE WHEN判断语句，例如这样的SQL语句：<br><code>UPDATE item SET inventory = CASE WHEN inventory &gt;= xxx THEN inventory-xxx ELSE inventory END</code></li></ul><h4 id="缓存中减库存"><a href="#缓存中减库存" class="headerlink" title="缓存中减库存"></a>缓存中减库存</h4><p>秒杀商品的减库存逻辑非常单一，比如没有复杂的SKU库存和总库存这种联动关系的，可以把秒杀商品减库存直接放到缓存系统中实现，也就是直接在缓存中减库存。</p><h4 id="数据库中减库存"><a href="#数据库中减库存" class="headerlink" title="数据库中减库存"></a>数据库中减库存</h4><p>如果有比较复杂的减库存逻辑，或者需要使用事务，还是必须在数据库中完成减库存。由于 MySQL存储数据的特点，同一数据在数据库里肯定是一行存储（MySQL），因此会有大量线程来竞争InnoDB行锁，而并发度越高时等待线程会越多，TPS会下降，响应时间（RT）会上升，数据库的吞吐量就会严重受影响。</p><p>解决并发锁的问题，有两种办法：</p><ul><li>应用层做排队: 按照商品维度设置队列顺序执行，这样能减少同一台机器对数据库同一行记录进行操作的并发度，同时也能控制单个商品占用数据库连接的数量，防止热点商品占用太多的数据库连接。</li><li>数据库层做排队: 应用层只能做到单机的排队，但是应用机器数本身很多，这种排队方式控制并发的能力仍然有限，所以如果能在数据库层做全局排队是最理想的。阿里的数据库团队开发了针对这种MySQL的InnoDB层上的补丁程序(patch），可以在数据库层上对单行记录做到并发排队。</li></ul><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>秒杀系统的设计本质是一场高并发场景下的“攻防战”，既要保障极致的性能与稳定性，又要兼顾业务公平性与数据一致性。本文从秒杀的业务与技术特点出发，系统性地梳理了四大核心挑战——瞬时流量、热点问题、超卖风险与接口防刷，并提出了五大架构原则：<strong>请求拦截上游化、数据精简、请求收敛、链路缩短、依赖解耦</strong>。</p><p>在系统设计层面，通过动静分离、动态URL、前端拦截与风控接入，将无效请求层层过滤；借助业务隔离、削峰限流、热点缓存与预扣库存策略，精准控制核心资源；最终结合异步队列、分布式锁与原子化操作，保障库存与交易的最终一致性。</p><p>秒杀系统的设计没有“银弹”，需根据业务规模动态权衡技术方案。但核心逻辑始终不变：<strong>化瞬时洪峰为涓涓细流，以最小代价支撑最大并发</strong>。希望本文总结的实践经验，能为初涉高并发系统设计的开发者提供清晰的解题思路，在面对类似场景时，快速构建出可靠、弹性、高效的秒杀架构。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;如何设计一个秒杀系统，想必大家一定在互联网上看了各种文章，本文结合内部系统的一些实践还有内部分享，总结了一些系统设计要点，供没有相关经验的同
      
    
    </summary>
    
    
      <category term="编程" scheme="https://blog.zer0e.com/tags/%E7%BC%96%E7%A8%8B/"/>
    
      <category term="架构" scheme="https://blog.zer0e.com/tags/%E6%9E%B6%E6%9E%84/"/>
    
  </entry>
  
  <entry>
    <title>某客户云上迁移实践</title>
    <link href="https://blog.zer0e.com/2025/03/02/2025-migrate-vpc/"/>
    <id>https://blog.zer0e.com/2025/03/02/2025-migrate-vpc/</id>
    <published>2025-03-02T09:53:23.000Z</published>
    <updated>2025-03-15T10:17:39.842Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>本文详细阐述某企业遗留系统云端迁移的技术实现过程，文中不涉及敏感信息与可视化资料，仅作技术方案参考。</p><h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>某企业客户部署的n台经典网络实例需实施跨可用区迁移，客户技术团队在2024年度三次自主迁移尝试均告失败。该业务系统自201x年上线以来累计服务千万级活跃用户，每周三至周日高峰时段并发用户达数十万量级。系统架构包含18台ECS+x个RDS-MySQL+x个Redis+x个MongoDB+x个RabbitMQ的复杂组合。</p><p>由于系统历经多次技术团队更迭，现存系统存在三大核心痛点：</p><ol><li>源代码及部署文档遗失</li><li>服务调用关系不明晰</li><li>关键组件单点部署风险<br>日常运维仅能维持基础功能运行（曾出现服务器重启导致部分业务模块失效的情况）。客户原定2025年2月启动系统重构计划，但因业务连续性要求无法等待，故紧急启动迁移项目。在获得有限权限（控制台只读账号+OS级访问）后，技术团队通过逆向工程完成架构梳理并制定迁移方案。</li></ol><h1 id="网络拓扑现状"><a href="#网络拓扑现状" class="headerlink" title="网络拓扑现状"></a>网络拓扑现状</h1><h2 id="现网环境分析"><a href="#现网环境分析" class="headerlink" title="现网环境分析"></a>现网环境分析</h2><p>客户现有两个VPC网络：</p><ul><li>业务VPC：172.16.0.0/16</li><li>迁移专用VPC：10.0.0.0/8<br>三台经典网络实例（10.25.63.135、10.27.10.158、10.29.112.12）需保持与业务VPC互通。</li></ul><h2 id="技术障碍突破"><a href="#技术障碍突破" class="headerlink" title="技术障碍突破"></a>技术障碍突破</h2><p>检测发现存在Classlink配置冲突问题：若保留经典网络连接通道，将导致其他ECS无法通过迁移计划进行迁移，影响迁移前后服务的互通。通过为期7天的流量捕获分析：</p><ul><li>2台实例无业务流量交互</li><li>1台实例仅存在TCP握手空连接<br>进一步端口扫描确认该流量源于业务VPC访问废弃Nginx端口，故所有Classlink均可安全拆除。</li></ul><h1 id="业务系统剖析"><a href="#业务系统剖析" class="headerlink" title="业务系统剖析"></a>业务系统剖析</h1><h2 id="服务组成"><a href="#服务组成" class="headerlink" title="服务组成"></a>服务组成</h2><ul><li>Windows集群（4节点）：具备完整文档，独立迁移</li><li>Linux集群（14节点）：承载核心业务，无部署文档</li></ul><h2 id="技术栈特征"><a href="#技术栈特征" class="headerlink" title="技术栈特征"></a>技术栈特征</h2><ul><li>应用层：SpringBoot+SpringCloud单体架构，无CI/CD流程</li><li>中间件：自建ZK集群+单节点Memcache/MySQL/Nginx/Apollo</li><li>云服务：混合使用RDS/Redis等托管服务</li><li>历史债务：大量硬编码数据库连接（IP/域名）、公网IP监听等技术债</li></ul><h1 id="迁移实践"><a href="#迁移实践" class="headerlink" title="迁移实践"></a>迁移实践</h1><p>采用分批次渐进式迁移策略，每周一二低峰时段迁移2-3台实例，整体方案分为网络重构、数据库迁移、应用迁移三阶段实施。</p><h2 id="网络架构改造"><a href="#网络架构改造" class="headerlink" title="网络架构改造"></a>网络架构改造</h2><ol><li>解除无效Classlink连接</li><li>创建跨VPC对等连接满足跳板机需求</li><li>在迁移VPC中规划与原有ECS网段不重叠的24位子网</li><li>将业务VPC与迁移VPC的路由配置完成。</li></ol><h2 id="数据库迁移"><a href="#数据库迁移" class="headerlink" title="数据库迁移"></a>数据库迁移</h2><p>经典网络数据库迁移至VPC网络的实施流程主要包含两个核心环节，每个环节均需通过控制台可视化操作完成且业务无感。具体实施规范如下：</p><p>1.安全组变更。</p><p>执行安全组规则分离操作，实现经典网络与VPC网络的白名单隔离管理。系统会自动将原经典网络已授权的IP地址段同步至VPC白名单策略组。特别建议在VPC白名单中预置10.0.0.0/8全量地址段，该配置前瞻性解决后续网络架构中网络负载均衡（NLB）组件采用FULLNAT模式时，可能因源地址转换导致真实客户端IP被过滤的问题，确保业务流量无阻断风险。</p><p>2.开启临时混访。</p><p>网络类型切换时需勾选”保留经典网络地址”选项，该操作将生成专属于VPC网络的访问终端节点，形成经典网络与VPC网络双地址并存的混合访问模式。若未保留经典地址，将直接导致未迁移业务系统失联。</p><p>网络切换完成后，需选取任意业务ECS实例执行网络诊断：分别对经典网络连接地址和VPC连接地址执行dig命令解析，完整记录两个终端对应的实际IP地址。此操作获取的IP映射关系，为后续解决应用程序硬编码连接地址的改造工作提供基础数据支撑，确保业务平滑迁移过渡。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">dig classic-db.example.com +short</span><br><span class="line">10.141.20.223</span><br><span class="line">dig vpc-db.example.com +short</span><br><span class="line">10.1.0.79</span><br></pre></td></tr></table></figure><h2 id="应用迁移攻坚"><a href="#应用迁移攻坚" class="headerlink" title="应用迁移攻坚"></a>应用迁移攻坚</h2><p>ECS迁移应用上发现有3个已知问题，分别为写死固定字符串连接、写死数据库固定IP、写死公网IP监听，针对这3个问题分别制定了3种应对策略，在ECS迁移启动后首先完成这3类问题处理，然后再进行后续程序启动</p><h3 id="技术债务应对方案"><a href="#技术债务应对方案" class="headerlink" title="技术债务应对方案"></a>技术债务应对方案</h3><table><thead><tr><th align="left">问题类型</th><th align="left">解决方案</th><th align="center">技术实现</th></tr></thead><tbody><tr><td align="left">域名依赖</td><td align="left">Hosts劫持</td><td align="center">建立域名-专网IP映射表</td></tr><tr><td align="left">IP直连</td><td align="left">NLB代理</td><td align="center">创建IP保留型网络负载均衡</td></tr><tr><td align="left">公网绑定</td><td align="left">双网卡路由</td><td align="center">EIP+辅助网卡绑定</td></tr></tbody></table><h3 id="1-域名硬编码连接优化"><a href="#1-域名硬编码连接优化" class="headerlink" title="1.域名硬编码连接优化"></a>1.域名硬编码连接优化</h3><p><strong>问题特征</strong>：应用程序采用硬编码数据库域名连接方式，但实际存在不可变连接字符串约束</p><p><strong>技术方案</strong>：</p><ol><li>建立全网域名-IP映射表：系统化梳理所有RDS实例的经典网络域名与专有网络IP对应关系</li><li>实施本地DNS覆盖：通过修改ECS实例的/etc/hosts文件，建立”经典域名→专有IP”的静态解析规则，保持应用层代码零改造</li><li>设计过渡期运维机制：针对数据库跨可用区迁移导致的IP变更风险，建立人工巡检与hosts文件更新流程。同步规划系统重构计划，要求新建ECS必须采用动态域名解析标准方案</li></ol><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">经典架构：应用代码-&gt; 经典域名 -&gt; 经典IP(10.172.X.X)</span><br><span class="line">过渡架构：应用代码-&gt; 经典域名 -&gt; hosts解析 -&gt; 专有IP(10.X.X.X)</span><br></pre></td></tr></table></figure><h3 id="2-IP直连架构改造"><a href="#2-IP直连架构改造" class="headerlink" title="2.IP直连架构改造"></a>2.IP直连架构改造</h3><p><strong>问题特征</strong>：应用程序直接硬编码数据库IP地址，无法通过常规DNS方案解决</p><p><strong>创新方案</strong>：</p><ol><li>构建NLB代理层：通过OpenAPI创建指定保留IP的NLB实例，严格保持原应用配置的IP地址不变</li><li>配置透明转发规则：建立NLB监听器与RDS专有网络IP的绑定关系，白名单放通NLB固定出口IP</li><li>实现流量无损切换：业务流量经NLB全量转发至新数据库节点，网络拓扑变更对应用完全透明</li></ol><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">传统架构：ECS -&gt; RDS(10.172.X.X)</span><br><span class="line">迁移架构：ECS -&gt; NLB(10.172.X.X) -&gt; RDS(10.X.X.X)</span><br></pre></td></tr></table></figure><h3 id="3-公网IP绑定重构"><a href="#3-公网IP绑定重构" class="headerlink" title="3. 公网IP绑定重构"></a>3. 公网IP绑定重构</h3><p><strong>问题特征</strong>：应用代码写死监听公网IP地址，在ECS迁移到VPC网络后，eth1公网网卡将会被删除，原公网IP将映射到内网eth0网卡，不在直接在系统内显示。应用不能监听公网IP，需要修改为监听内网IP。目前应用已不具备能力修改，需要使用其他方案解决此问题。</p><p><strong>改造方案</strong>：</p><ol><li>公网IP弹性化改造：将原有固定公网IP转换为弹性公网IP(EIP)</li><li>辅助网卡部署：为ECS挂载第二块虚拟网卡，采用”EIP直通绑定”模式实现公网IP可视化</li><li>智能路由配置：精细化定义双网卡路由策略<ul><li>eth0(主网卡)：承载10.0.0.0/8及172.16.0.0/16内网流量</li><li>eth1(辅助网卡)：配置默认路由，处理公网出入流量</li></ul></li></ol><h3 id="4-应用启动"><a href="#4-应用启动" class="headerlink" title="4.应用启动"></a>4.应用启动</h3><p><strong>实施规范</strong>：</p><ol><li><strong>分级启动机制</strong>：<ul><li>基础层：优先启动Nginx、Redis等开源中间件，完成健康状态校验</li><li>支撑层：依次启动基础服务、核心组件、API网关</li><li>业务层：最终启动控制台等前端服务</li></ul></li><li><strong>立体化排障体系</strong>：<ul><li>有日志服务：实时监控error级别日志，结合代码反编译精准定位异常</li><li>无日志服务：采用tcpdump抓包分析+strace系统调用追踪的复合诊断模式</li><li>业务验证：完整执行核心功能测试用例，数据一致性校验通过率需达100%</li></ul></li><li><strong>观测期管理</strong>：<ul><li>建立3小时黄金观察期，监控QPS、错误率、响应延时等12项核心指标</li><li>配置业务流量灰度放量机制，按10%、30%、50%、100%阶梯递增</li><li>达成零客户投诉且系统指标平稳运行后，方可启动下一节点迁移</li></ul></li></ol><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>本次迁移不仅完成基础设施升级，更帮助客户重建了系统架构图谱。项目结束后，客户技术团队已着手推进以下改进：</p><ul><li>建立配置管理中心</li><li>实施CICD流水线</li><li>构建双活容灾架构</li></ul><p>此次实践验证了对传统系统改造的技术可行性，为同类项目提供了有价值的参考范式。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;本文详细阐述某企业遗留系统云端迁移的技术实现过程，文中不涉及敏感信息与可视化资料，仅作技术方案参考。&lt;/p&gt;
&lt;h1 id=&quot;背景&quot;&gt;&lt;a 
      
    
    </summary>
    
    
      <category term="编程" scheme="https://blog.zer0e.com/tags/%E7%BC%96%E7%A8%8B/"/>
    
      <category term="架构" scheme="https://blog.zer0e.com/tags/%E6%9E%B6%E6%9E%84/"/>
    
  </entry>
  
  <entry>
    <title>2024年度总结</title>
    <link href="https://blog.zer0e.com/2024/12/31/2024-summary/"/>
    <id>https://blog.zer0e.com/2024/12/31/2024-summary/</id>
    <published>2024-12-31T14:00:00.000Z</published>
    <updated>2024-12-31T15:23:45.941Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>2024年又是弹指一挥间，当我回看23年的总结时，总是还觉得还在昨天。去年在总结中写到今年的展望，说到今年想换个环境继续提升自己，完成了一半的目标吧，环境换了，但是提升打个问号。提笔忘字，每当到了要写点什么时，都不知道怎么去描写现在的心情和想法。</p><h1 id="正文"><a href="#正文" class="headerlink" title="正文"></a>正文</h1><p>站在26岁的十字路口，我也开始思考之后的道路。</p><h2 id="一季度"><a href="#一季度" class="headerlink" title="一季度"></a>一季度</h2><p>一月份我记得很清楚的很清楚的一件事就是签了器官捐赠，也是受到一个游戏的影响，为社会做点贡献。同样在一月还获得了绝区零的内测资格，大概体验了一下，不是特别对胃口，也提了一些意见与建议，最后给了几十块钱酬劳好像哈哈哈。公测之后玩了一个版本后就放弃了。</p><p>二月份的时候去了趟上海，参加崩三的活动，去南京路还有黄浦江边走了走，当天回程时还差点买不到票。然后就是春节，回了老家，不得不说老家的旅游业越办越好了。春节期间还拜访了老师，和朋友聚了餐，放假总是时间飞快。</p><p>三月份去办理了护照，但是后面还是没有办理签证，出趟国太费钱也太累了，最终还是选择不去。然后和大学舍友约了个饭，聊了聊之后的规划，以及目前的环境，其实从那时候开始就打算要离职了。约饭那天周五，还有人打电话给我，虽然我没接，但是种子已经埋下了。</p><h2 id="二季度"><a href="#二季度" class="headerlink" title="二季度"></a>二季度</h2><p>四月份开始自己做饭了，买了柴米油盐，还有锅刀啥的，第一顿做的是西红柿炒蛋加火腿肠，结果太咸了哈哈哈。二季度一开始领导要开始搞内部框架大统一，说实话我觉得不现实，但是上头要求就得做，底下的人苦不堪言。四月底的时候和领导聊了聊，表达了自己会离职的想法，领导让我好好考虑考虑，也不断的和我沟通，毕竟合格的牛马不能轻易放走。</p><p>五月份的劳动节假期听了星铁的演唱会，并且去线下的星铁land活动，但我个人不喜欢coser，因此大多数时间都在看场景和节目。五月中旬我便不参与开发和评审任务了，那段时间真的特别轻松，想着离职后的规划，觉得后续会好起来的。这段时间也看了一些机会，但是没有去投递。</p><p>同时，我还去尝试了正骨复位、针灸、拔罐，去治疗我的颈椎，这几天又不好了，肯定是上班上的。</p><p>六月初，我的离职日到了，那天早上我还在和用户沟通，记忆尤新，下午的时候和其他组的领导聊了聊，反馈了目前大组里的现状。结束后去办理了离职手续，和组里的同事告别之后，焕哥正好说要送我一程，开着车请我吃了顿饭。那天真的五味杂陈，既有离职的兴奋，又有对未来的迷茫，还有对遇到一群这么好的导师、领导、同事而我却离开的惋惜。</p><p>离职之后几天，开始请之前的几个比较好的同事吃饭，聊了很多，但更多的是对康子的吐槽，还有对未来的迷茫。</p><p>随后我便开始休整，不紧不慢的修改简历，六月中下旬开始投递，第一个挂我的是好像是字节，后面六月份好像挂了两三家，那时候觉得还行，积累经验。熟不知后续经历让我更加迷茫。</p><p>在我离职后不久，还有几个同事陆续离职，也许是受不了当前的环境，也许是受够了日夜的加班，我也祝福他们能找到自己的路。</p><h2 id="三季度"><a href="#三季度" class="headerlink" title="三季度"></a>三季度</h2><p>七月份开始，我便开始高强度的找工作，上午起床学习，下午面试，晚上复盘加修改简历，但全都了无音讯，这时候其实我有点慌了，我没想到同事口中很强的我竟然一个offer全都没有，尽管面试不断，但全都没有得到反馈。</p><p>八月份，得知一个四月份离职的同事找到了一份不错的工作，我既高兴也不解，他和我住附近，有时候还会帮他喂猫咪。他选择离开杭州前往上海，但令我不解的是平常感觉一般的他却能上岸不错的工作，使我非常疑惑，但我也祝福他。随后又开始了一波新的面试，但依旧草草收场。</p><p>八月中旬，为了缓解苦闷，我去了上海参加了原神的嘉年华，特意买的还是vip票，当我检票的时候已经十点多了估计，vip检票口一个人都没有，那个小哥还问我游戏多少级了哈哈哈。</p><p>八月下旬开始看软考相关的东西，但是也是属于三天打鱼两天晒网，每天就看一点，也没有看的多么仔细。</p><p>随后九月到了自己的生日，买了蛋糕当了晚饭。那天和帅哥他们出来逛了湘湖，并在那天喝了一杯伏特加，第一次喝酒头晕，那天洗完澡后马上就睡着了。</p><p>九月还去了西溪湿地逛了逛，剩下时间基本都是学习、面试、打游戏。那段时间自己确实比较焦虑，因为离职后面试三个月了一点反馈都没有，自己的自信心有点受到打击了。</p><p>九月底和林子哥他们去了崩三联动的肯德基，吃了联动套餐，又拿了一堆周边回来。同时离职的事老妈也知道了，但她没有催我去找工作，二是让我国庆回家休息休息，没钱了可以和她说。父母永远是自己后盾这句话真的难以言表，真的感谢父母。</p><h2 id="四季度"><a href="#四季度" class="headerlink" title="四季度"></a>四季度</h2><p>十月份国庆回了趟老家，老家的房子终于快装修好了，调整了家里的网络，拍了拍照片，帮忙进行了一些收尾。国庆结束后尽管老妈让我多呆几天，但是因为自己拉不下面子，没回来杭州的话估计别人就知道没工作了，总之还是回来了。</p><p>国庆后有好消息就是有两个offer，但是一个是od直接pass了，最后选择了目前这边。确认offer之后便开始玩游戏了，早上做点软考题目，下午和晚上玩游戏打炉石，当然面试也同步在进行。</p><p>十一月份，准备搬家，从萧山搬到西湖区，我的物品已经一辆小面装不下了，下次搬家得约中面才行了。同时医保被冻结，看病得花自己钱，肉疼。</p><p>十一月中旬，开始上班。阿里云这边确实不是很卷，但是有时候需要回个消息，开始了解云上网络原理、架构，但说实话接触不到底层实现。</p><p>月底去了植物园逛了逛，用现金买了实体票，和林子哥帅哥卿哥吃了个饭，深夜食堂，味道还不错。</p><p>十二月，也基本都在忙工作的事，有时候周末也得加加班，也逐渐了另外两个同事熟悉了，会聊一些其他话题。他们说康子太苦了，加班到凌晨还没有调休，我也只能哈哈一笑。来了两周的同事今年的最后一天离职了，他觉得学不到什么去了一家初创公司，先不论他的决定是否正确，但我也尊重他的选择，祝他前程似锦。</p><h1 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h1><p>赶在零点前写到这里，好久没用机械键盘打这么字，打的我手腕疼。</p><p>站在26岁的十字路口，也开始思考之后的道路。每次看到朋友圈结婚的，当官的，当老师放假的，我便不禁思考我未来的道路，仍然一片迷茫。我想做的东西究竟是什么，经过今年五个月没工作时间的思考，我又再次迷茫。另一方面，我钱也不多，三年的康子工作我并没有攒下多少钱，甚至不足以我去继续深造。随着年龄的增大，我是否还有机会改变目前的现状仍是未知数。但另一方面也不能完全相信出国读了书回来能remake重生，未来的事真的说不准。</p><p>另外目前这边的工作主要是技术上提升较小，全都得靠自己摸索学习，尽管大佬很多，但帮助有限。明年的我是否会另外寻找工作目前也还是未知数，但如果工作内容持续是这样的话，那么我估计离职也不远了。</p><p>希望明年的我运气能好点，不仅是找工作，各种方面都希望能好起来。另外也希望自己和家人能健健康康活着，我觉得这才是最关键的事情。永远健康，永远开心，后面才是考虑其他的。</p><p>加油吧，未来的我，今年的我只能到这里了，也感谢今年的我，确实努力过了，奈何运气差一些。相信明年都会好起来的。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;2024年又是弹指一挥间，当我回看23年的总结时，总是还觉得还在昨天。去年在总结中写到今年的展望，说到今年想换个环境继续提升自己，完成了一半
      
    
    </summary>
    
    
      <category term="随想" scheme="https://blog.zer0e.com/tags/%E9%9A%8F%E6%83%B3/"/>
    
  </entry>
  
  <entry>
    <title>游戏邮件系统设计</title>
    <link href="https://blog.zer0e.com/2024/10/31/game-email-architecture/"/>
    <id>https://blog.zer0e.com/2024/10/31/game-email-architecture/</id>
    <published>2024-10-31T07:14:24.000Z</published>
    <updated>2024-10-31T07:13:34.980Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>昨天睡前突发奇想，想着要不设计什么系统来思考思考，正好前段时间看到一篇关于站内信的设计文章，里面也有不少可以琢磨的。但是又没想法要设计啥系统，便在各个游戏里琢磨，想着o神这个邮件系统应该也有不少思考，因此决定设计一个游戏邮件系统。注意这个游戏的邮件系统只针对管理员向用户发送的邮件系统，用户间不存在邮件交互。</p><p>在这里也先叠个甲，本人非游戏从业中，只是从一个后端开发，架构师的角度去思考这个问题，结合业务的话我也只能进行一定猜测，本文的所编写的任何东西不构成生产建议或实践！欢迎友好交流的同学一起讨论和学习。</p><h1 id="正文"><a href="#正文" class="headerlink" title="正文"></a>正文</h1><h2 id="需求明确"><a href="#需求明确" class="headerlink" title="需求明确"></a>需求明确</h2><p>首先我们需要明确设计的目标，我列了以下几个点，当然还有一些没有罗列，我们就按照简单的需求来设计。</p><ul><li>管理员(运营)可以向用户发送邮件。</li><li>收到的邮件主体包括标题，接收时间，发送人，正文，附件。</li><li>用户间不可以相互发送邮件。</li><li>运营可以根据用户条件并且指定时间发送邮件。</li><li>用户接收群发邮件时接收时间可以为上线时间或者运营确定的时间。</li><li>邮件可以有过期时间，到达过期时间后，邮件将被删除。</li></ul><p>需求还是比较简单并且清晰的。</p><p>其中值得一提的是，最后一个点我认为是出于性能的考虑，可以防止大量的数据同时写入。也可以排除掉不活跃的用户。</p><p>其他的一些需求，比如收藏邮件，其实和主体并没有什么冲突，只需要加字段即可实现。</p><p>接下来就讲讲具体的设计思路。</p><h2 id="设计思路"><a href="#设计思路" class="headerlink" title="设计思路"></a>设计思路</h2><p>说是设计，其实这个系统只要把字段逻辑理清楚，尤其是数据如何存储，比如分表如何分，字段设计完成后，其他如上层开发就简单一些。</p><h3 id="字段设计"><a href="#字段设计" class="headerlink" title="字段设计"></a>字段设计</h3><p>我一直觉得实体类设计是比较简单的一环，尤其是当业务清楚的时候。</p><p>先给出第一版设计</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Attachment</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Long itemId;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Integer num;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SystemEmail</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Long id;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> String sender;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 标题</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> String subject;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 是否为确定的接收时间</span></span><br><span class="line"><span class="comment">     * true 用户邮件的接收时间为系统邮件指定的时间</span></span><br><span class="line"><span class="comment">     * false 用户邮件的接收时间为拉取系统邮件的时间</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> Boolean timeExacted;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 接收时间</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> LocalDateTime receivedTime;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 主体</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> String content;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 附件列表</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> List&lt;Attachment&gt; attachments;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 邮件过期时间</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> LocalDateTime expiredTime;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 用户id列表</span></span><br><span class="line"><span class="comment">     * 当用户id在列表中时，可以拉取</span></span><br><span class="line"><span class="comment">     * 否则需要检测条件是否满足</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> List&lt;Long&gt; uidList;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 邮件发送条件</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> List&lt;String&gt; conditions;</span><br><span class="line">    </span><br><span class="line">    </span><br><span class="line">    <span class="keyword">private</span> LocalDateTime createTime;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">private</span> Long createUserId;</span><br><span class="line">    </span><br><span class="line">     <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 是否删除</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> Integer deleted;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserEmail</span> &#123;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * The Id.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> Long id;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 用户id</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> Long uid;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 发送人</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> String sender;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 标题</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> String subject;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 接收时间</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> LocalDateTime receivedTime;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 主体</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> String content;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 附件列表</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> List&lt;Attachment&gt; attachments;</span><br><span class="line">    </span><br><span class="line">     <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 邮件过期时间</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> LocalDateTime expiredTime;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 系统邮件来源</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> Long systemEmailId;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 记录创建时间</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> LocalDateTime createTime;</span><br><span class="line">    </span><br><span class="line">     <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 是否删除</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> Integer deleted;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里先谈谈我的设计思路。</p><p>首先所有的邮件是由系统去创建的，并且在数据库存储上，不可能是系统发送邮件后就对所有用户写入邮件数据，所以必然应该有一张或者多张表存储系统邮件信息，然后再根据一个触发条件（一般是登录）去将系统邮件的内容写入到个人邮件数据中。因此用户邮件<code>UserEmail</code>和系统邮件<code>SystemEmail</code>应该有一个关联id。</p><p>此外系统邮件应该有个字段自由控制接收时间，这里我命名的是<code>timeExacted</code>，意思是时间是否确定。</p><p>那其实数据结构字段就差不多了。</p><h3 id="数据表设计"><a href="#数据表设计" class="headerlink" title="数据表设计"></a>数据表设计</h3><p>既然实体类已经设计完成，那其实表的设计就按照实体类即可，但是其实还需要考虑挺多点。</p><p>首先，老生常谈的问题，分库分表。当然，对于这个系统而言，我们可以只考虑分表的情况，底层的性能我们可以先不考虑，先从业务上进行分表的设计。</p><h4 id="分库分表"><a href="#分库分表" class="headerlink" title="分库分表"></a>分库分表</h4><p>为什么要分表？防止单表的数据量达到一定量级总而造成性能问题。首先用户邮件的设计上，我们采用<code>uid</code>字段去进行区分，对于单表来说只需要简单的sql查询即可完成个人邮件检索。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> t_user_email <span class="keyword">where</span> uid <span class="operator">=</span> <span class="number">1000</span> <span class="keyword">order</span> <span class="keyword">by</span> receviedTime <span class="keyword">desc</span>;</span><br></pre></td></tr></table></figure><p>但是考虑到用户的量级上，假设一个服务器上有1亿个活跃账号，每个账号有100封邮件，那么此时用户邮件表中就有100亿条数据，这个量级是十分恐怖的。尽管有邮件过期机制，但是长时间运营的游戏必然会有很多永久邮件存储在用户上。</p><p>因此必须要分表。那么怎么分呢？以阿里的规范来说，单表超过500w条记录或者2GB时，才进行分库分表，我们先以这个标准去进行分表，假设单表500w，100亿条数据我让AI回答了一下，它说需要2000个表才可以存的下。那我们就以两倍进行预估，那么就需要分4000个表。</p><p>假设mysql能抗住这么大的一个并发，那也得分4000个表去存储，一般情况下是根据uid进行hash取模，然后落到不同的表上。</p><p>但实际情况下，单库分4000个表是不现实的，因为单库根本扛不住这么多的流量，这就有点脱裤子放屁了。所以应该是先分库然后再分表，这里的4000最接近4096，因此可以先进行模64分库，然后用相同字段div 64后模64进行分表，这样会稍微合理一些。</p><p>但随着时代的变迁，分库分表会将成为历史。据我所知，马哈鱼的游戏基本上都是用的阿里的PolarDB分布式数据库，无需去关心分库分表。</p><h3 id="系统设计"><a href="#系统设计" class="headerlink" title="系统设计"></a>系统设计</h3><p>这个主要就是业务上的一些实现了，如果说不再考虑分库分表的话，其实业务系统上的就比较简单了。向用户发送邮件的流程应该如下</p><ol><li>运营向系统邮件表写入邮件内容。写入完成后，邮件系统发送通知到主进程。</li><li>一些内部流转邮件，例如签到奖励，可以直接写入用户邮件表中。</li><li>用户此时正在游戏中，主系统收到用户邮件通知，或者登陆进游戏后触发邮件拉取检查。</li><li>此时邮件系统将系统邮件写入用户邮件表。用户收到邮件到达通知。</li><li>用户收到邮件。</li><li>阅读和领取操作。</li></ol><p>其中有个比较关键的点就是邮件拉取。比如拉取的时间范围应该是什么？如何去判断是否已经拉取过？</p><p>首先拉取范围，应该是用户上次登录时间到此时的所有邮件。当然邮件有过期时间，可以加上条件过滤掉，其实不会有那么多。</p><p>是否拉取过那就很简单了，判断systemEmailId是否有就好了。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>写到这里其实逻辑差不多清楚了，但是我没有选择去实现这个。业务上的逻辑其实还是得深入研究。本文主要还是从表的设计入手去分析如何实现。顺带讲了讲分库分表的事，虽然我认为它一定会退出历史舞台。</p><p>当然也还有很多东西没讲到，比如过期删除的逻辑，条件判断逻辑等等。</p><p>其实有些东西只有当你确确实实要去用代码去实现时才能发现问题所在。</p><p>说实话写到后面其实自己差不多已经倦了，所以打算结束这篇思考。花了两三天陆陆续续完成这篇文章。总的来说还是思考了不少东西，但也让我意识到自己可能对于代码实现不是那么有激情了，也许是年纪大了。</p><p>后面应该就去新公司那边了，看看能不能学习到一些新的东西。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;昨天睡前突发奇想，想着要不设计什么系统来思考思考，正好前段时间看到一篇关于站内信的设计文章，里面也有不少可以琢磨的。但是又没想法要设计啥系统
      
    
    </summary>
    
    
      <category term="编程" scheme="https://blog.zer0e.com/tags/%E7%BC%96%E7%A8%8B/"/>
    
      <category term="随想" scheme="https://blog.zer0e.com/tags/%E9%9A%8F%E6%83%B3/"/>
    
      <category term="架构" scheme="https://blog.zer0e.com/tags/%E6%9E%B6%E6%9E%84/"/>
    
  </entry>
  
  <entry>
    <title>10月随笔</title>
    <link href="https://blog.zer0e.com/2024/10/14/2024-october/"/>
    <id>https://blog.zer0e.com/2024/10/14/2024-october/</id>
    <published>2024-10-14T02:14:24.000Z</published>
    <updated>2024-10-14T02:59:08.293Z</updated>
    
    <content type="html"><![CDATA[<h1 id="随笔"><a href="#随笔" class="headerlink" title="随笔"></a>随笔</h1><p>现在是10月中旬，距离我上次写文章已经过去一个多月了，时间太快了。</p><p>这一个月我都干了些啥呢？整个9月份我就面试了诚云和菜鸟两家公司，诚云面试了4面，加上8月份还面了2面，一共有6面，虽然中途换过部门。菜鸟国庆前两面，国庆后一面。</p><p>至于结果嘛，诚云是面试通过了但是没有HC，一直就挂着。菜鸟的话大概率HR面寄了，我说离职原因是学不到东西，但是HR说做这么多东西和学不到东西是冲突的，她觉得很矛盾。我也觉得她说话很矛盾，她不理解我，我不理解她。</p><p>细想下来，好像9月份做的事情很少，还有的话就是看了看软考视频，感觉也没认真学下去。</p><p>9月10月又是中秋又是国庆，所以也浪费了不少时间，不管是面试的还是个人的。</p><p>还有就是9月份老妈知道我离职了，她也说慢慢找，没有钱可以和她说，和我爸说的一样，父母还是关心孩子的，希望儿女不要有那么大的压力。</p><p>国庆久违的回了一次家，10天，一点东西都没学，都在陪老妈收拾东西搬家，看电视。有时候觉得这种日子也挺不错的，但我知道总是要工作的，在国庆假期后第一天我就回杭州了。</p><p>接下来我也不知道咋办了，焦虑焦虑还是焦虑，打开招聘软件焦虑，因为感觉没有合适的岗位，睡觉焦虑，因为想着面试经历还有后面的不确定性。</p><p>但是面试还得继续，我实在不想去OD，不管菜鸟有没有offer，虽然十月过半，但也要继续修改投递简历面试，人生还是要积极。</p><p>好在虽然股市亏损了，但是没投入多少，还在可控范围内。存款也还有不少，最起码在杭州一年半载是没啥问题的。</p><p>继续加油吧。</p><h1 id="后话"><a href="#后话" class="headerlink" title="后话"></a>后话</h1><p>昨天玩了一部游戏，感觉对相亲也没那么排斥了，但是说实话我遇不到那么好的女生(哭)，到头来只是宅男的幻想罢了。但是游戏里有些观点我还是比较认同的，比如恋爱观，相处观等等。其实我还是对自己未来的不确定性而担忧，没法承担另一个人的责任，主观上一直在抗拒。但也有人和我说应该两个人相互扶持，我也认同，但这个社会过于浮躁了，并且对于男方太过于苛责，渣男渣女太多，导致很多人不敢恋爱。哈哈哈，总之不知道什么时候才能遇到自己喜欢的另一半呢。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;随笔&quot;&gt;&lt;a href=&quot;#随笔&quot; class=&quot;headerlink&quot; title=&quot;随笔&quot;&gt;&lt;/a&gt;随笔&lt;/h1&gt;&lt;p&gt;现在是10月中旬，距离我上次写文章已经过去一个多月了，时间太快了。&lt;/p&gt;
&lt;p&gt;这一个月我都干了些啥呢？整个9月份我就面试了诚云和菜鸟两家
      
    
    </summary>
    
    
      <category term="随想" scheme="https://blog.zer0e.com/tags/%E9%9A%8F%E6%83%B3/"/>
    
  </entry>
  
  <entry>
    <title>写于8月最后一天</title>
    <link href="https://blog.zer0e.com/2024/08/31/2024-end-of-august/"/>
    <id>https://blog.zer0e.com/2024/08/31/2024-end-of-august/</id>
    <published>2024-08-31T11:14:24.000Z</published>
    <updated>2024-08-31T13:38:05.985Z</updated>
    
    <content type="html"><![CDATA[<h1 id="随笔"><a href="#随笔" class="headerlink" title="随笔"></a>随笔</h1><p>今天是8月最后一天，总觉得我应该写点什么，但是提笔忘字，哦不对，应该说手放键盘，却不知写下何物。</p><p>今天早上五点半起床去爬了山，结果下山的时候低血糖，还好山不是很高，救回一条狗命。</p><p>又去浙大玉泉校区看了看，可望不可即的感觉，门口有来送孩子上学的父母，她父母应该为他们的女儿或者儿子感到骄傲吧。高考真的是决定人的一生，我确实感觉到了，很多单位招聘只看第一学历，因为不管研究生或者留学生，其实钱能到位就好说，因此他们只相信相对公平的高考，相信被高考录取的学校就是你的实力。说实话，我打心里不认同，因为我认识第一学历一般，但是凭借自己努力考上研究生或者入职某个好单位的朋友，但是在这个社会上又不得不唯第一学历论，感觉很悲哀。</p><p>上周末把boss上的简历给隐藏了，这周没有消息来打扰了。这周面试了华为od的流程，包括两面技术面，一面主管面。周三的时候阿里云的技术服务经理岗也面了一面，不过感觉是刷KPI的，应该不太行。</p><p>华为od应该是八九不离十可以给offer的，整体面试难度偏低，说实话，只要你是目标院校，并且有点基础，代码也还可以，那么轻轻松松，没有网上说的那么难。又回到了上面说的学校论了，不过od看的是你的最高学历，目标院校笔试只要150，而听说非目标需要300往上。</p><p>差不多给了20k，其中17k基本工资，3k绩效，绩效正常的话2个月年终，社保公积金基数为基本工资，公积金12%比例。大概算下来就是30%左右的涨幅，算计的真好。每个月最后一个周六需要加班，工资翻倍，差不多那天能有一千来块的收入。</p><p>我纠结到底去不去，一旦去了外包我就没有回头路了。说是这么说，但我知道过去其实也是打杂，不知道能不能学习到什么，而且压力不知道咋样。朋友们也都在和我说，外包的地位那不是一般的低，也许华为里还好，因为od是当正式员工用的。但是技术提升我觉得不会特别高。</p><p>我一直认为工资可以稍微低一些，但是技术得提升。但是现在就挺矛盾，一是面不到好的公司，二是去中小型公司技术其实很难再提升了，三是工资也比原来低很多。</p><p>所以我现在纠结是不是应该先去od混着，然后顺便也继续面试，也是填补空窗期。虽然刚开始简历上不会写这段经历，但是可以继续尝试。但我也知道只要去了就会开始忙起来了，又开始无暇顾及面试，我是知道的。</p><p>其次我也担心一旦我去od超过一定时间，我简历上肯定会写上这份经历，外包经历肯定对简历有影响，一是面试机会少，二是影响面试官印象。但如果只是单纯赚钱的话，我觉得可以去，20k其实也不低了。一个月20不知道领先了多少人，真的我认为网络社区上高工资的人太多了，搞得人人都很焦虑，中国月入过万的人才多少？</p><p>有时候当你问出问题的那一刻，其实心里早就有了答案。你向其他人询问也只是为了坚定你的想法。</p><p>未来的路还很长，不必太过焦虑。这句话其实很多人都和我说过。我们对自己的未来感到迷茫恰恰是因为不甘平凡而已。我也经常这样安慰自己，但是说实话还是想多赚钱。人是矛盾的动物。</p><p>不知不觉胡言乱语一千多字，不知道写给谁看，只是想记录下此时的心情，想和自己和解，仅此而已。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;随笔&quot;&gt;&lt;a href=&quot;#随笔&quot; class=&quot;headerlink&quot; title=&quot;随笔&quot;&gt;&lt;/a&gt;随笔&lt;/h1&gt;&lt;p&gt;今天是8月最后一天，总觉得我应该写点什么，但是提笔忘字，哦不对，应该说手放键盘，却不知写下何物。&lt;/p&gt;
&lt;p&gt;今天早上五点半起床去爬了山，
      
    
    </summary>
    
    
      <category term="随想" scheme="https://blog.zer0e.com/tags/%E9%9A%8F%E6%83%B3/"/>
    
  </entry>
  
  <entry>
    <title>2024面试复盘13</title>
    <link href="https://blog.zer0e.com/2024/08/15/2024-08-15-replay/"/>
    <id>https://blog.zer0e.com/2024/08/15/2024-08-15-replay/</id>
    <published>2024-08-15T12:00:00.000Z</published>
    <updated>2024-08-16T08:54:36.870Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>今天zoom面试，亏我还那么期待，感觉好像面试官水平不咋样啊，面试完后一个小时直接发拒绝邮件了，我真是醉了，我感觉大部分回答的还可以吧，除了小部分不懂的问题，不会又是刷KPI的吧。艹！</p><p>全程基本都是八股文，有数据库相关，有java相关，我印象比较深的是一个explain的filesort没答上来，然后es的一个match和match_phrase好像答反了。不至于就挂掉啊，真奇怪。难道是我跟面试官说话的态度不好吗？好像有一两次我觉得他说的不对，反驳了一下。</p><p>算了，就不应该抱有期待的，就这样吧，复盘一下。</p><h1 id="复盘"><a href="#复盘" class="headerlink" title="复盘"></a>复盘</h1><h2 id="为什么用xxl-job？-scheduler和xxl-job"><a href="#为什么用xxl-job？-scheduler和xxl-job" class="headerlink" title="为什么用xxl-job？@scheduler和xxl-job"></a>为什么用xxl-job？@scheduler和xxl-job</h2><p>高可用部署+任务失败重试+多种调度方式+动态任务配置.</p><p><code>@Scheduled</code> 注解是 Spring 的一部分，集成简单，适合在 Spring 环境中进行定时任务调度。适合单个应用程序内部的定时任务管理，不支持分布式任务调度。</p><p>XXL-JOB 是一个分布式任务调度平台，提供了更加全面和强大的任务调度功能，适用于大规模分布式系统的任务调度需求。支持分布式部署，可以在多个服务器或实例上运行调度任务，适用于分布式系统和大规模应用。支持多种调度方式，包括简单任务、分片任务、定时任务等。</p><table><thead><tr><th>特性</th><th><code>@Scheduled</code></th><th>XXL-JOB</th></tr></thead><tbody><tr><td><strong>适用场景</strong></td><td>单个 Spring 应用程序内的定时任务</td><td>大规模分布式系统的任务调度</td></tr><tr><td><strong>任务调度方式</strong></td><td>基于 Cron 表达式、固定延迟、固定频率</td><td>支持简单任务、分片任务、定时任务等</td></tr><tr><td><strong>分布式支持</strong></td><td>不支持</td><td>支持</td></tr><tr><td><strong>管理界面</strong></td><td>不提供</td><td>提供图形化管理界面</td></tr><tr><td><strong>集成方式</strong></td><td>Spring 注解配置</td><td>需要集成客户端库并配置管理平台</td></tr><tr><td><strong>容错性</strong></td><td>低（单机环境下）</td><td>高（支持高可用部署和容错机制）</td></tr></tbody></table><h2 id="xxl-job如何控制单一任务只有一次执行"><a href="#xxl-job如何控制单一任务只有一次执行" class="headerlink" title="xxl-job如何控制单一任务只有一次执行"></a>xxl-job如何控制单一任务只有一次执行</h2><p>在任务开始执行前，任务执行器会尝试获取任务的锁。通常使用数据库中的记录或分布式锁机制来确保任务只有一个实例在执行。</p><p>任务执行器会在任务执行前更新任务状态，例如在数据库中标记任务为“正在执行”。这有助于防止任务被重复下发或执行。</p><h2 id="数据库的表锁和行锁"><a href="#数据库的表锁和行锁" class="headerlink" title="数据库的表锁和行锁"></a>数据库的表锁和行锁</h2><p>表锁是指对整个表施加锁定，确保在锁定期间，其他事务无法对该表进行修改或读取操作（具体行为取决于锁的类型）。表锁会锁定整个表，导致在锁定期间，其他事务无法访问该表。这可能会导致较大的并发访问瓶颈。</p><p>行锁是指对数据库表中的特定行施加锁定。行锁可以更细粒度地控制对表中数据的访问，通常在高并发环境中使用。行锁只锁定表中的特定行，允许其他事务访问表中的其他行。这可以提高并发性能和资源利用率。</p><table><thead><tr><th>特性</th><th>表锁</th><th>行锁</th></tr></thead><tbody><tr><td><strong>粒度</strong></td><td>锁定整个表</td><td>锁定特定行</td></tr><tr><td><strong>并发</strong></td><td>可能导致高并发访问瓶颈</td><td>提高并发性能</td></tr><tr><td><strong>实现复杂度</strong></td><td>简单易用</td><td>较复杂</td></tr><tr><td><strong>性能影响</strong></td><td>性能可能较差，尤其在高并发环境下</td><td>性能较好，适合高并发环境</td></tr></tbody></table><h4 id="行锁膨胀"><a href="#行锁膨胀" class="headerlink" title="行锁膨胀"></a>行锁膨胀</h4><p>当数据库中的锁定行数量非常多时，某些数据库系统可能会自动升级为表锁，以简化锁管理和避免过多的锁开销。这种情况被称为<strong>锁膨胀</strong>。</p><p>操作涉及整个表时，例如执行一个需要对整个表进行锁定的查询或更新，数据库系统可能会选择表锁以保证操作的完整性。例如在事务中执行了一条没有索引条件的查询，引发全表扫描，行锁 膨胀为表锁。</p><p>当行锁锁定的数据没有主键或唯一索引时，数据库系统可能会自动将行锁膨胀为表锁。</p><h2 id="数据库的死锁？回滚事务如何处理？"><a href="#数据库的死锁？回滚事务如何处理？" class="headerlink" title="数据库的死锁？回滚事务如何处理？"></a>数据库的死锁？回滚事务如何处理？</h2><p>看录像之后，发现答得不好。</p><h3 id="表级锁死锁"><a href="#表级锁死锁" class="headerlink" title="表级锁死锁"></a>表级锁死锁</h3><p>假设有两个事务，分别在操作两个表：表A和表B。<br>事务1试图先锁定表A，然后锁定表B。<br>而事务2试图先锁定表B，然后锁定表A。<br>用户A和用户B加锁的顺序如下：</p><ul><li>用户A–》表1（表锁）–》表2（表锁）</li><li>用户B–》表2（表锁）–》表1（表锁）</li></ul><p>如果这两个事务同时执行，它们将会相互等待对方释放锁，从而导致死锁。</p><p>这种死锁比较常见，是由于程序的BUG产生的，除了调整的程序的逻辑没有其它的办法。</p><ol><li>仔细分析程序的逻辑，对于数据库的多表操作时，尽量按照相同的顺序进行处理，</li><li>尽量避免同时锁定两个资源，如操作A和B两张表时，总是按先A后B的顺序处理， 必须同时锁定两个资源时，要保证在任何时刻都应该按照相同的顺序来锁定资源。</li></ol><h3 id="行级锁死锁"><a href="#行级锁死锁" class="headerlink" title="行级锁死锁"></a>行级锁死锁</h3><p><strong>原因1</strong>：</p><p>如果在事务中执行了一条没有索引条件的查询，引发全表扫描，行锁 膨胀 为表锁（ 或者等价于 表级锁）</p><p>多个这样的 锁表事务 执行后，就很容易产生死锁和阻塞，最终应用系统会越来越慢，发生阻塞或 死锁。</p><p><strong>解决方案</strong></p><ol><li>SQL语句中不要使用太复杂的关联多表的查询；</li><li>使用explain“执行计划”对SQL语句进行分析，对于有全表扫描和全表锁定的SQL语句，建立相应的索引进行优化。</li></ol><p><strong>原因2：</strong></p><p>两个事务分别想拿到对方持有的锁，互相等待，于是产生死锁</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Session_1执行：<span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> account <span class="keyword">where</span> id<span class="operator">=</span><span class="number">1</span> <span class="keyword">for</span> <span class="keyword">update</span>;</span><br><span class="line">Session_2执行：<span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> account <span class="keyword">where</span> id<span class="operator">=</span><span class="number">2</span> <span class="keyword">for</span> <span class="keyword">update</span>;</span><br><span class="line">Session_1执行：<span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> account <span class="keyword">where</span> id<span class="operator">=</span><span class="number">2</span> <span class="keyword">for</span> <span class="keyword">update</span>;</span><br><span class="line">Session_2执行：<span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> account <span class="keyword">where</span> id<span class="operator">=</span><span class="number">1</span> <span class="keyword">for</span> <span class="keyword">update</span>;</span><br></pre></td></tr></table></figure><p>这种情况下，mysql会自动检测死锁并回滚事务，按照AI的回答，mysql会自动分析回滚等待时间最长，回滚代价小，数据影响少的事务进行回滚。</p><h2 id="xxl任务执行失败时的处理"><a href="#xxl任务执行失败时的处理" class="headerlink" title="xxl任务执行失败时的处理"></a>xxl任务执行失败时的处理</h2><ul><li>“故障转移”发生在调度阶段，在执行器集群部署时，如果某一台执行器发生故障，该策略支持自动进行Failover切换到一台正常的执行器机器并且完成调度请求流程。</li><li>“失败重试”发生在”调度 + 执行”两个阶段，支持通过自定义任务失败重试次数，当任务失败时将会按照预设的失败重试次数主动进行重试；</li></ul><h2 id="mysql事务隔离级别？项目中使用的是什么？"><a href="#mysql事务隔离级别？项目中使用的是什么？" class="headerlink" title="mysql事务隔离级别？项目中使用的是什么？"></a>mysql事务隔离级别？项目中使用的是什么？</h2><ul><li><strong>READ-UNCOMMITTED(读取未提交)</strong> ：最低的隔离级别，允许读取尚未提交的数据变更，可能会导致脏读、幻读或不可重复读。</li><li><strong>READ-COMMITTED(读取已提交)</strong> ：允许读取并发事务已经提交的数据，可以阻止脏读，但是幻读或不可重复读仍有可能发生。</li><li><strong>REPEATABLE-READ(可重复读)</strong> ：对同一字段的多次读取结果都是一致的，除非数据是被本身事务自己所修改，可以阻止脏读和不可重复读，但幻读仍有可能发生。</li><li><strong>SERIALIZABLE(可串行化)</strong> ：最高的隔离级别，完全服从 ACID 的隔离级别。所有的事务依次逐个执行，这样事务之间就完全不可能产生干扰，也就是说，该级别可以防止脏读、不可重复读以及幻读。</li></ul><h3 id="mysql为什么使用RR级别？"><a href="#mysql为什么使用RR级别？" class="headerlink" title="mysql为什么使用RR级别？"></a>mysql为什么使用RR级别？</h3><p>Mysql在5.0这个版本以前，binlog只支持<code>STATEMENT</code>这种格式！</p><p>而这种格式在读已提交(Read Commited)这个隔离级别下主从复制是有bug的，因此Mysql将可重复读(Repeatable Read)作为默认的隔离级别</p><p>假设主节点两个事务A, B，并且隔离级别为RC，事务A先执行了删除操作，但是未提交事务；事务B执行了插入操作，先提交了事务。最后事务A提交事务。</p><p>此时主节点上查询应该是有数据的才对，因为delete早于insert。</p><p>但是由于提交时间不一样，所以bin log记录的顺序是先insert然后delete，导致从节点上无数据。</p><p>解决方案有两种！</p><ol><li>隔离级别设为可重复读(Repeatable Read),在该隔离级别下引入间隙锁。当Session 1执行delete语句时，会锁住间隙。那么，Ssession 2执行插入语句就会阻塞住！</li><li>将binglog的格式修改为row格式，此时是基于行的复制，自然就不会出现sql执行顺序不一样的问题！奈何这个格式在mysql5.1版本开始才引入。因此由于历史原因，mysql将默认的隔离级别设为可重复读(Repeatable Read)，保证主从复制不出问题！</li></ol><h3 id="项目中选了哪个隔离级别？为什么？"><a href="#项目中选了哪个隔离级别？为什么？" class="headerlink" title="项目中选了哪个隔离级别？为什么？"></a><strong>项目中选了哪个隔离级别？为什么？</strong></h3><p>项目中是不用读未提交(Read UnCommitted)和串行化(Serializable)两个隔离级别，原因有二</p><p>采用读未提交(Read UnCommitted),一个事务读到另一个事务未提交读数据，从逻辑上都说不过去！</p><p>采用串行化(Serializable)，每个次读操作都会加锁，快照读失效，一般是使用mysql自带分布式事务功能时才使用该隔离级别！</p><p>那为什么很多互联网大厂为选<strong>读已提交</strong>(Read Commited)作为事务隔离级别？</p><p><strong>原因1：在RR隔离级别下，存在间隙锁，导致出现死锁的几率比RC大的多！</strong></p><p>假设表数据如下，并执行sql</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="operator">+</span><span class="comment">----+-------+</span></span><br><span class="line"><span class="operator">|</span> id <span class="operator">|</span> type  <span class="operator">|</span></span><br><span class="line"><span class="operator">+</span><span class="comment">----+-------+</span></span><br><span class="line"><span class="operator">|</span>  <span class="number">1</span> <span class="operator">|</span>  red  <span class="operator">|</span></span><br><span class="line"><span class="operator">|</span>  <span class="number">2</span> <span class="operator">|</span> white <span class="operator">|</span></span><br><span class="line"><span class="operator">|</span>  <span class="number">5</span> <span class="operator">|</span>  red  <span class="operator">|</span></span><br><span class="line"><span class="operator">|</span>  <span class="number">7</span> <span class="operator">|</span> white <span class="operator">|</span></span><br><span class="line"><span class="operator">+</span><span class="comment">----+-------+</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> test <span class="keyword">where</span> id <span class="operator">&lt;</span><span class="number">3</span> <span class="keyword">for</span> <span class="keyword">update</span>;</span><br></pre></td></tr></table></figure><p>在RR隔离级别下，存在间隙锁，可以锁住(2,5)这个间隙，防止其他事务插入数据！<br>而在RC隔离级别下，不存在间隙锁，其他事务是可以插入数据！</p><p><strong>原因2：在RR隔离级别下，条件列未命中索引会锁表！而在RC隔离级别下，只锁行</strong>。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">update</span> test <span class="keyword">set</span> type <span class="operator">=</span> <span class="string">&#x27;blue&#x27;</span> <span class="keyword">where</span> type <span class="operator">=</span> <span class="string">&#x27;white&#x27;</span>; </span><br></pre></td></tr></table></figure><p>在RC隔离级别下，其先走聚簇索引，进行全部扫描</p><p>但在实际中，MySQL做了优化，发现不满足后，会调用unlock_row方法，把不满足条件的记录放锁。</p><p>然而，在RR隔离级别下，走聚簇索引，进行全部扫描，最后会将整个表锁上。</p><p><strong>原因3：在RC隔离级别下，半一致性读(semi-consistent)特性增加了update操作的并发性</strong></p><p>在5.1.15的时候，innodb引入了一个概念叫做“semi-consistent”，减少了更新同一行记录时的冲突，减少锁等待。<br>所谓半一致性读就是，一个update语句，如果读到一行已经加锁的记录，此时InnoDB返回记录最近提交的版本，由MySQL上层判断此版本是否满足update的where条件。若满足(需要更新)，则MySQL会重新发起一次读操作，此时会读取行的最新版本(并加锁)！</p><p>假设有两个事务，事务1和事务2！<br>事务1执行:</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">update</span> test <span class="keyword">set</span> type <span class="operator">=</span> <span class="string">&#x27;blue&#x27;</span> <span class="keyword">where</span> type <span class="operator">=</span> <span class="string">&#x27;red&#x27;</span>; </span><br></pre></td></tr></table></figure><p>先不Commit事务！</p><p>与此同时事务2执行:</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">update</span> test <span class="keyword">set</span> type <span class="operator">=</span> <span class="string">&#x27;blue&#x27;</span> <span class="keyword">where</span> type <span class="operator">=</span> <span class="string">&#x27;white&#x27;</span>; </span><br></pre></td></tr></table></figure><p>事务 2尝试加锁的时候，发现行上已经存在锁，InnoDB会开启semi-consistent read，返回最新的committed版本(1,red),(2，white),(5,red),(7,white)。MySQL会重新发起一次读操作，此时会读取行的最新版本(并加锁)!<br>而在RR隔离级别下，事务2只能等待！</p><h3 id="在RC级别下，不可重复读问题需要解决么？"><a href="#在RC级别下，不可重复读问题需要解决么？" class="headerlink" title="在RC级别下，不可重复读问题需要解决么？"></a>在RC级别下，不可重复读问题需要解决么？</h3><p>不用解决，这个问题是可以接受的！毕竟你数据都已经提交了，读出来本身就没有太大问题！Oracle的默认隔离级别就是RC。</p><h2 id="可重复读实现原理"><a href="#可重复读实现原理" class="headerlink" title="可重复读实现原理"></a>可重复读实现原理</h2><p><code>可重复读（Repeatable Read）</code> 是 MySQL 的事务隔离级别之一，旨在保证在一个事务中多次读取相同数据时的结果一致性。为了实现这一点，MySQL 使用了 <strong>多版本并发控制（MVCC, Multi-Version Concurrency Control）</strong> 机制。</p><p>它是通过在每个数据行上维护多个版本的数据来实现的。当一个事务要对数据库中的数据进行修改时，MVCC 会为该事务创建一个数据快照，而不是直接修改实际的数据行。</p><p><code>MVCC</code> 的实现依赖于：<strong>隐藏字段、Read View、undo log</strong>。在内部实现中，<code>InnoDB</code> 通过数据行的 <code>DB_TRX_ID</code> 和 <code>Read View</code> 来判断数据的可见性，如不可见，则通过数据行的 <code>DB_ROLL_PTR</code> 找到 <code>undo log</code> 中的历史版本。每个事务读到的数据版本可能是不一样的，在同一个事务中，用户只能看到该事务创建 <code>Read View</code> 之前已经提交的修改和该事务本身做的修改</p><h3 id="隐藏字段"><a href="#隐藏字段" class="headerlink" title="隐藏字段"></a>隐藏字段</h3><p>在内部，<code>InnoDB</code> 存储引擎为每行数据添加了三个 <a href="https://dev.mysql.com/doc/refman/5.7/en/innodb-multi-versioning.html">隐藏字段</a>：</p><ul><li><code>DB_TRX_ID（6字节）</code>：表示最后一次插入或更新该行的事务 id。此外，<code>delete</code> 操作在内部被视为更新，只不过会在记录头 <code>Record header</code> 中的 <code>deleted_flag</code> 字段将其标记为已删除</li><li><code>DB_ROLL_PTR（7字节）</code> 回滚指针，指向该行的 <code>undo log</code> 。如果该行未被更新，则为空</li><li><code>DB_ROW_ID（6字节）</code>：如果没有设置主键且该表没有唯一非空索引时，<code>InnoDB</code> 会使用该 id 来生成聚簇索引。</li></ul><h3 id="ReadView"><a href="#ReadView" class="headerlink" title="ReadView"></a>ReadView</h3><p><a href="https://github.com/facebook/mysql-8.0/blob/8.0/storage/innobase/include/read0types.h#L298">Read View</a> 主要是用来做可见性判断，里面保存了 “当前对本事务不可见的其他活跃事务”</p><p>主要有以下字段：</p><ul><li><code>m_low_limit_id</code>：目前出现过的最大的事务 ID+1，即下一个将被分配的事务 ID。大于等于这个 ID 的数据版本均不可见</li><li><code>m_up_limit_id</code>：活跃事务列表 <code>m_ids</code> 中最小的事务 ID，如果 <code>m_ids</code> 为空，则 <code>m_up_limit_id</code> 为 <code>m_low_limit_id</code>。小于这个 ID 的数据版本均可见</li><li><code>m_ids</code>：<code>Read View</code> 创建时其他未提交的活跃事务 ID 列表。创建 <code>Read View</code>时，将当前未提交事务 ID 记录下来，后续即使它们修改了记录行的值，对于当前事务也是不可见的。<code>m_ids</code> 不包括当前事务自己和已提交的事务（正在内存中）</li><li><code>m_creator_trx_id</code>：创建该 <code>Read View</code> 的事务 ID</li></ul><h3 id="undo-log"><a href="#undo-log" class="headerlink" title="undo log"></a>undo log</h3><p><code>undo log</code> 主要有两个作用：</p><ul><li>当事务回滚时用于将数据恢复到修改前的样子</li><li>另一个作用是 <code>MVCC</code> ，当读取记录时，若该记录被其他事务占用或当前版本对该事务不可见，则可以通过 <code>undo log</code> 读取之前的版本数据，以此实现非锁定读</li></ul><p><strong>在 <code>InnoDB</code> 存储引擎中 <code>undo log</code> 分为两种：<code>insert undo log</code> 和 <code>update undo log</code>：</strong></p><ol><li><code>insert undo log</code>：指在 <code>insert</code> 操作中产生的 <code>undo log</code>。因为 <code>insert</code> 操作的记录只对事务本身可见，对其他事务不可见，故该 <code>undo log</code> 可以在事务提交后直接删除。不需要进行 <code>purge</code> 操作</li><li><code>update undo log</code>：<code>update</code> 或 <code>delete</code> 操作中产生的 <code>undo log</code>。该 <code>undo log</code>可能需要提供 <code>MVCC</code> 机制，因此不能在事务提交时就进行删除。提交时放入 <code>undo log</code> 链表，等待 <code>purge线程</code> 进行最后的删除.</li></ol><h3 id="数据可见性算法"><a href="#数据可见性算法" class="headerlink" title="数据可见性算法"></a>数据可见性算法</h3><p>在 <code>InnoDB</code> 存储引擎中，创建一个新事务后，执行每个 <code>select</code> 语句前，都会创建一个快照（Read View），<strong>快照中保存了当前数据库系统中正处于活跃（没有 commit）的事务的 ID 号</strong>。其实简单的说保存的是系统中当前不应该被本事务看到的其他事务 ID 列表（即 m_ids）。当用户在这个事务中要读取某个记录行的时候，<code>InnoDB</code> 会将该记录行的 <code>DB_TRX_ID</code> 与 <code>Read View</code> 中的一些变量及当前事务 ID 进行比较，判断是否满足可见性条件。</p><h3 id="RC-和-RR-隔离级别下-MVCC-的差异"><a href="#RC-和-RR-隔离级别下-MVCC-的差异" class="headerlink" title="RC 和 RR 隔离级别下 MVCC 的差异"></a>RC 和 RR 隔离级别下 MVCC 的差异</h3><p>在事务隔离级别 <code>RC</code> 和 <code>RR</code> （InnoDB 存储引擎的默认事务隔离级别）下，<code>InnoDB</code> 存储引擎使用 <code>MVCC</code>（非锁定一致性读），但它们生成 <code>Read View</code> 的时机却不同</p><ul><li>在 RC 隔离级别下的 <strong><code>每次select</code></strong> 查询前都生成一个<code>Read View</code> (m_ids 列表)</li><li>在 RR 隔离级别下只在事务开始后 <strong><code>第一次select</code></strong> 数据前生成一个<code>Read View</code>（m_ids 列表）</li></ul><h2 id="声明式事务标记在private方法上"><a href="#声明式事务标记在private方法上" class="headerlink" title="声明式事务标记在private方法上"></a>声明式事务标记在private方法上</h2><p>无法生效，因为声明式事务是基于AOP实现的。Spring 生成的代理对象仅能拦截 <code>public</code> 方法，因为代理对象是 <code>public</code> 方法的拦截器，而无法直接拦截 <code>private</code> 方法。因此，<code>private</code> 方法上的 <code>@Transactional</code> 注解不会被 Spring 事务管理器识别和应用。</p><h2 id="explain分析有哪些指标"><a href="#explain分析有哪些指标" class="headerlink" title="explain分析有哪些指标"></a>explain分析有哪些指标</h2><p><strong><code>id</code></strong></p><ul><li><strong>描述</strong>：查询的唯一标识符，通常用于标识查询中的不同 SELECT 或 UNION 子句的执行顺序。</li><li><strong>意义</strong>：在复杂查询中，<code>id</code> 指标帮助你理解查询的执行顺序和逻辑结构。</li></ul><p> <strong><code>select_type</code></strong></p><ul><li><code>SIMPLE</code>：简单的查询，不包括子查询或 UNION。</li><li><code>PRIMARY</code>：最外层的 SELECT 查询。</li><li><code>UNION</code>：UNION 中的 SELECT 查询。</li><li><code>SUBQUERY</code>：子查询中的 SELECT 查询。</li><li><code>DERIVED</code>：派生表（子查询中的 SELECT）中的查询。</li></ul><p><strong><code>table</code></strong></p><p>显示查询涉及的表。</p><p><strong><code>type</code></strong></p><ul><li><code>system</code>：表只有一行，最优。</li><li><code>const</code>：用于匹配常量，效率很高。</li><li><code>eq_ref</code>：通过唯一索引进行匹配，效率高。</li><li><code>ref</code>：非唯一索引匹配，效率较高。</li><li><code>range</code>：范围查询（使用索引进行范围查找）。</li><li><code>index</code>：全索引扫描，效率低于使用索引的其他方式。</li><li><code>ALL</code>：全表扫描，最差，性能较低。</li></ul><p><strong><code>possible_keys</code></strong></p><p>列出查询中可能用到的索引。</p><p><strong><code>key</code></strong></p><p>实际使用的索引。了解查询中实际使用了哪个索引。与 <code>possible_keys</code> 对比，确定是否选择了最优的索引。</p><p><strong><code>key_len</code></strong></p><p>表示实际使用的索引的长度（字节数）。通过分析 <code>key_len</code>，可以了解使用的索引的有效性。较长的 <code>key_len</code> 可能意味着索引覆盖了更多的列。</p><p><strong><code>ref</code></strong></p><p>显示哪些列或常量用于索引查找。</p><p><strong><code>rows</code></strong></p><p>表示 MySQL 估算需要读取的行数。估算的行数可以帮助你了解查询的复杂性。较高的行数可能意味着查询会扫描大量数据，从而影响性能。</p><p><strong><code>filtered</code></strong></p><p>表示在读取的行中，多少百分比的行被过滤掉（即匹配条件的行）。</p><p><strong><code>extra</code></strong></p><ul><li><code>Using index</code>：查询只使用了索引，没有访问表数据，通常意味着索引覆盖了查询。</li><li><code>Using where</code>：查询使用了 WHERE 子句来过滤结果。</li><li><code>Using temporary</code>：查询使用了临时表，通常意味着查询需要复杂的处理，可能会影响性能。</li><li><code>Using filesort</code>：查询需要额外的排序操作，通常意味着性能较差。</li></ul><h2 id="extra的file-sort是什么情况？如何优化"><a href="#extra的file-sort是什么情况？如何优化" class="headerlink" title="extra的file sort是什么情况？如何优化"></a>extra的file sort是什么情况？如何优化</h2><p>当 <code>Using filesort</code> 出现时，意味着 MySQL 必须对结果集进行排序，但不能使用索引中的数据直接进行排序。因此，MySQL 使用临时文件来完成排序过程。</p><p><code>Using filesort</code> 发生在以下情况：</p><ol><li><strong>无法使用索引排序</strong>：如果查询中需要对结果集进行排序，但所用的索引不能满足排序需求，MySQL 会使用额外的排序步骤。即使使用了索引，排序顺序可能与查询中的排序要求不匹配。</li><li><strong>多个排序条件</strong>：当查询中有多个排序条件时，索引可能无法完全满足所有排序要求，从而需要额外的排序操作。</li><li><strong><code>ORDER BY</code> 不匹配索引顺序</strong>：即使有索引，如果 <code>ORDER BY</code> 子句中的排序顺序与索引的顺序不匹配，也会触发文件排序。</li></ol><p><strong>如何优化?</strong></p><p><strong>使用合适的索引</strong>：确保你的 <code>ORDER BY</code> 子句中的字段有适当的索引。如果排序字段与 WHERE 子句中的字段有共同的索引，那么 MySQL 可能会使用这个索引进行排序。</p><p><strong>优化查询</strong>: 如果排序不是必需的，可以考虑删除 <code>ORDER BY</code> 子句或优化排序条件，以减少对排序操作的需求。尽量简化排序字段。例如，如果只需对一个字段进行排序，确保只在该字段上创建索引，而不是包括多个字段。</p><p><strong>使用索引覆盖查询</strong>: 如果查询只涉及到索引中的字段，MySQL 可以使用覆盖索引来避免回表操作。在这种情况下，排序可以直接通过索引完成，无需额外的文件排序。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> column1, column2 <span class="keyword">FROM</span> <span class="keyword">table</span></span><br><span class="line"><span class="keyword">WHERE</span> column1 <span class="operator">=</span> <span class="string">&#x27;value&#x27;</span></span><br><span class="line"><span class="keyword">ORDER</span> <span class="keyword">BY</span> column2;</span><br></pre></td></tr></table></figure><p>这里的 column1 和 column2 都应该在索引中，以便可以使用索引直接排序。</p><p><strong>调整配置参数</strong>: **调整 <code>sort_buffer_size</code>**：增加 <code>sort_buffer_size</code> 配置参数的值可以提高排序操作的效率，减少文件排序的频率。这是 MySQL 在排序操作中使用的内存缓冲区的大小。</p><h2 id="事务提交后，如何执行一些后置处理？如何知道事务已经提交？"><a href="#事务提交后，如何执行一些后置处理？如何知道事务已经提交？" class="headerlink" title="事务提交后，如何执行一些后置处理？如何知道事务已经提交？"></a>事务提交后，如何执行一些后置处理？如何知道事务已经提交？</h2><p>不查不知道，一查吓一跳，原来有这么多方法。</p><h3 id="使用-TransactionSynchronizationManager-和-TransactionSynchronization"><a href="#使用-TransactionSynchronizationManager-和-TransactionSynchronization" class="headerlink" title="使用 TransactionSynchronizationManager 和 TransactionSynchronization"></a>使用 <code>TransactionSynchronizationManager</code> 和 <code>TransactionSynchronization</code></h3><p>Spring 提供了 <code>TransactionSynchronizationManager</code> 和 <code>TransactionSynchronization</code> 机制，可以在事务提交后执行自定义逻辑。这是一种非常灵活的方法，可以在事务提交后注册回调方法。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> org.springframework.transaction.support.TransactionSynchronization;</span><br><span class="line"><span class="keyword">import</span> org.springframework.transaction.support.TransactionSynchronizationManager;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyTransactionSynchronization</span> <span class="keyword">implements</span> <span class="title class_">TransactionSynchronization</span> &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">beforeCommit</span><span class="params">(<span class="type">boolean</span> readOnly)</span> &#123;</span><br><span class="line">        <span class="comment">// Do nothing</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">afterCommit</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// 执行事务提交后的处理逻辑</span></span><br><span class="line">        System.out.println(<span class="string">&quot;事务已经提交&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">afterCompletion</span><span class="params">(<span class="type">int</span> status)</span> &#123;</span><br><span class="line">        <span class="comment">// 事务完成后的处理</span></span><br><span class="line">        <span class="keyword">if</span> (status == STATUS_COMMITTED) &#123;</span><br><span class="line">            System.out.println(<span class="string">&quot;事务提交完成&quot;</span>);</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (status == STATUS_ROLLED_BACK) &#123;</span><br><span class="line">            System.out.println(<span class="string">&quot;事务回滚&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">beforeCompletion</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// Do nothing</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">suspend</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// Do nothing</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">resume</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// Do nothing</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在需要执行后置处理的地方注册 <code>TransactionSynchronization</code>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> org.springframework.transaction.support.TransactionSynchronizationManager;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyService</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">myTransactionalMethod</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// 业务逻辑</span></span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 注册 TransactionSynchronization</span></span><br><span class="line">        TransactionSynchronizationManager.registerSynchronization(<span class="keyword">new</span> <span class="title class_">MyTransactionSynchronization</span>());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="使用-Transactional-注解的-afterCommit-和-afterRollback-属性"><a href="#使用-Transactional-注解的-afterCommit-和-afterRollback-属性" class="headerlink" title="使用 @Transactional 注解的 afterCommit 和 afterRollback 属性"></a><strong>使用 <code>@Transactional</code> 注解的 <code>afterCommit</code> 和 <code>afterRollback</code> 属性</strong></h3><p>还真没用过。。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> org.springframework.transaction.annotation.Transactional;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyTransactionalService</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Transactional</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">myTransactionalMethod</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// 业务逻辑</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Transactional(afterCommit = &quot;postCommit&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">myTransactionalMethodWithCallback</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// 业务逻辑</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">postCommit</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// 事务提交后的处理逻辑</span></span><br><span class="line">        System.out.println(<span class="string">&quot;事务提交后处理&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="使用-ApplicationListener-监听事务事件"><a href="#使用-ApplicationListener-监听事务事件" class="headerlink" title="使用 ApplicationListener 监听事务事件"></a>使用 <code>ApplicationListener</code> 监听事务事件</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> org.springframework.context.ApplicationListener;</span><br><span class="line"><span class="keyword">import</span> org.springframework.transaction.event.TransactionPhase;</span><br><span class="line"><span class="keyword">import</span> org.springframework.transaction.event.TransactionalEventListener;</span><br><span class="line"><span class="keyword">import</span> org.springframework.transaction.event.TransactionPhase;</span><br><span class="line"><span class="keyword">import</span> org.springframework.transaction.support.TransactionSynchronizationManager;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyTransactionEventListener</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">handleAfterCommit</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// 事务提交后的处理逻辑</span></span><br><span class="line">        System.out.println(<span class="string">&quot;事务已经提交，执行后置处理&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">handleAfterRollback</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// 事务回滚后的处理逻辑</span></span><br><span class="line">        System.out.println(<span class="string">&quot;事务回滚&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注册监听器</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="keyword">public</span> MyTransactionEventListener <span class="title function_">myTransactionEventListener</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">MyTransactionEventListener</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="使用-TransactionCallback"><a href="#使用-TransactionCallback" class="headerlink" title="使用 TransactionCallback"></a>使用 <code>TransactionCallback</code></h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> org.springframework.transaction.PlatformTransactionManager;</span><br><span class="line"><span class="keyword">import</span> org.springframework.transaction.TransactionStatus;</span><br><span class="line"><span class="keyword">import</span> org.springframework.transaction.support.TransactionCallback;</span><br><span class="line"><span class="keyword">import</span> org.springframework.transaction.support.TransactionCallbackWithoutResult;</span><br><span class="line"><span class="keyword">import</span> org.springframework.transaction.support.TransactionTemplate;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyService</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> TransactionTemplate transactionTemplate;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">MyService</span><span class="params">(PlatformTransactionManager transactionManager)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.transactionTemplate = <span class="keyword">new</span> <span class="title class_">TransactionTemplate</span>(transactionManager);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">myTransactionalMethod</span><span class="params">()</span> &#123;</span><br><span class="line">        transactionTemplate.execute(<span class="keyword">new</span> <span class="title class_">TransactionCallbackWithoutResult</span>() &#123;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">doInTransactionWithoutResult</span><span class="params">(TransactionStatus status)</span> &#123;</span><br><span class="line">                <span class="comment">// 业务逻辑</span></span><br><span class="line">                </span><br><span class="line">                <span class="comment">// 事务提交后的处理</span></span><br><span class="line">                System.out.println(<span class="string">&quot;事务已经提交，执行后置处理&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="深度分页查询优化"><a href="#深度分页查询优化" class="headerlink" title="深度分页查询优化"></a>深度分页查询优化</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"># MySQL 在无法利用索引的情况下跳过<span class="number">1000000</span>条记录后，再获取<span class="number">10</span>条记录</span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> t_order <span class="keyword">ORDER</span> <span class="keyword">BY</span> id LIMIT <span class="number">1000000</span>, <span class="number">10</span></span><br></pre></td></tr></table></figure><h3 id="基于游标的分页"><a href="#基于游标的分页" class="headerlink" title="基于游标的分页"></a>基于游标的分页</h3><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">table</span> <span class="keyword">WHERE</span> id <span class="operator">&gt;</span> ? <span class="keyword">ORDER</span> <span class="keyword">BY</span> id LIMIT <span class="number">10</span>;</span><br></pre></td></tr></table></figure><p>在第一次查询中，<code>?</code> 是初始的游标值（例如，0），在后续查询中，它是上一次查询的最后一条记录的 <code>id</code>。</p><p>这种优化方式限制比较大，且一般项目的 ID 也没办法保证完全连续。</p><h3 id="子查询"><a href="#子查询" class="headerlink" title="子查询"></a>子查询</h3><p>阿里巴巴《Java 开发手册》中也有对应的描述：</p><blockquote><p>利用延迟关联或者子查询优化超多分页场景。</p></blockquote><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"># 通过子查询来获取 id 的起始值，把 limit <span class="number">1000000</span> 的条件转移到子查询</span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> t_order <span class="keyword">WHERE</span> id <span class="operator">&gt;=</span> (<span class="keyword">SELECT</span> id <span class="keyword">FROM</span> t_order limit <span class="number">1000000</span>, <span class="number">1</span>) LIMIT <span class="number">10</span>;</span><br></pre></td></tr></table></figure><p>不过，子查询的结果会产生一张新表，会影响性能，应该尽量避免大量使用子查询。并且，这种方法只适用于 ID 是正序的。在复杂分页场景，往往需要通过过滤条件，筛选到符合条件的 ID，此时的 ID 是离散且不连续的。</p><h3 id="延迟关联"><a href="#延迟关联" class="headerlink" title="延迟关联"></a>延迟关联</h3><p>延迟关联的优化思路，跟子查询的优化思路其实是一样的：都是把条件转移到主键索引树，减少回表的次数。不同点是，延迟关联使用了 INNER JOIN（内连接） 包含子查询。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> t1.<span class="operator">*</span> <span class="keyword">FROM</span> t_order t1</span><br><span class="line"><span class="keyword">INNER</span> <span class="keyword">JOIN</span> (<span class="keyword">SELECT</span> id <span class="keyword">FROM</span> t_order limit <span class="number">1000000</span>, <span class="number">10</span>) t2</span><br><span class="line"><span class="keyword">ON</span> t1.id <span class="operator">=</span> t2.id;</span><br></pre></td></tr></table></figure><p>除了使用 INNER JOIN 之外，还可以使用逗号连接子查询</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> t1.<span class="operator">*</span> <span class="keyword">FROM</span> t_order t1,</span><br><span class="line">(<span class="keyword">SELECT</span> id <span class="keyword">FROM</span> t_order limit <span class="number">1000000</span>, <span class="number">10</span>) t2</span><br><span class="line"><span class="keyword">WHERE</span> t1.id <span class="operator">=</span> t2.id;</span><br></pre></td></tr></table></figure><h3 id="覆盖索引"><a href="#覆盖索引" class="headerlink" title="覆盖索引"></a>覆盖索引</h3><p>索引中已经包含了所有需要获取的字段的查询方式称为覆盖索引。</p><p><strong>避免 InnoDB 表进行索引的二次查询，也就是回表操作:</strong> InnoDB 是以聚集索引的顺序来存储的，对于 InnoDB 来说，二级索引在叶子节点中所保存的是行的主键信息，如果是用二级索引查询数据的话，在查找到相应的键值后，还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中，二级索引的键值中可以获取所有的数据，避免了对主键的二次查询（回表），减少了 IO 操作，提升了查询效率。</p><p><strong>可以把随机 IO 变成顺序 IO 加快查询效率:</strong> 由于覆盖索引是按键值的顺序存储的，对于 IO 密集型的范围查找来说，对比随机从磁盘读取每一行的数据 IO 要少的多，因此利用覆盖索引在访问时也可以把磁盘的随机读取的 IO 转变成索引查找的顺序 IO。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"># 如果只需要查询 id, code, type 这三列，可建立 code 和 type 的覆盖索引</span><br><span class="line"><span class="keyword">SELECT</span> id, code, type <span class="keyword">FROM</span> t_order</span><br><span class="line"><span class="keyword">ORDER</span> <span class="keyword">BY</span> code</span><br><span class="line">LIMIT <span class="number">1000000</span>, <span class="number">10</span>;</span><br></pre></td></tr></table></figure><p>不过，当查询的结果集占表的总行数的很大一部分时，可能就不会走索引了，自动转换为全表扫描。当然了，也可以通过 <code>FORCE INDEX</code> 来强制查询优化器走索引，但这种提升效果一般不明显。</p><h2 id="什么是倒排索引"><a href="#什么是倒排索引" class="headerlink" title="什么是倒排索引"></a>什么是倒排索引</h2><p>倒排索引 也被称作反向索引（inverted index），是用于提高数据检索速度的一种数据结构，空间消耗比较大。倒排索引首先将检索文档进行分词得到多个词语/词条，然后将词语和文档 ID 建立关联，从而提高检索效率。</p><p>倒排索引使用 词语/词条（Term） 来作为索引关键字，并同时记录了哪些 文档（Document） 中有这个词语。</p><h2 id="match-query和term-query"><a href="#match-query和term-query" class="headerlink" title="match query和term query"></a>match query和term query</h2><p>说实话没用过。。。</p><p><code>match</code> 查询用于全文搜索，通常用于检索文档中包含某个单词或短语的文档。<code>match</code> 查询会经过分词（Tokenization）过程，将查询词分解成多个词项，然后进行搜索。这意味着 <code>match</code> 查询适合处理用户输入的自然语言文本。</p><p><code>term</code> 查询用于精确匹配，通常用于检索字段中确切匹配的值。这意味着 <code>term</code> 查询不会经过分析或分词过程，适合用于关键词、标识符或固定的值。</p><h2 id="使用redis分布式锁如何解决主从同步问题"><a href="#使用redis分布式锁如何解决主从同步问题" class="headerlink" title="使用redis分布式锁如何解决主从同步问题"></a>使用redis分布式锁如何解决主从同步问题</h2><p><strong>确保写操作在主节点</strong>：所有的分布式锁操作（包括获取和释放锁）都应该在主节点上执行。这样可以保证在任何时候，锁的状态都是最新的。</p><p><strong>避免在从节点上执行锁操作</strong>：在设计系统时，确保所有的锁操作请求都不会被重定向到从节点。这可以通过客户端库来实现，或者在应用层面进行控制。</p><p><strong>使用 RedLock 算法</strong>：RedLock 是一种设计用于 Redis 的分布式锁获取算法，它通过在多个 Redis 节点上尝试获取锁来提高锁的安全性。如果 Redis 集群使用了主从复制，RedLock 算法可以确保锁的安全性。</p><p><strong>监控主从复制延迟</strong>：监控主从复制的延迟情况，如果延迟过高，可能需要考虑优化复制性能或重新评估锁的超时时间。</p><p>redission中也进行了一些处理</p><ol><li><strong>多节点锁获取</strong>：Redisson 通过在所有的 Redis 服务器上尝试获取锁来实现这一点。即使在主从复制的环境中，这样也能确保锁的安全性</li><li><strong>自动检测和配置主从节点</strong>：Redisson 提供了自动检测和配置主从节点的功能，这有助于设置主从复制并保持数据一致性</li></ol><h2 id="redis分布式锁保证安全性？不会被其他线程操作？"><a href="#redis分布式锁保证安全性？不会被其他线程操作？" class="headerlink" title="redis分布式锁保证安全性？不会被其他线程操作？"></a>redis分布式锁保证安全性？不会被其他线程操作？</h2><ul><li><strong>使用 <code>SET</code> 命令的 <code>NX</code> 选项</strong>：这确保了只有当 key 不存在时，才能设置成功，从而避免了多个客户端同时获取锁。</li><li><strong>设置合理的超时时间</strong>：使用 <code>EX</code> 或 <code>PX</code> 选项设置超时时间，确保即使客户端出现问题，锁也能在一定时间后自动释放。</li></ul><p>分布式锁的值可以设置成UUID或者服务标识+线程标识，只要确保唯一。释放锁时，需要先判断现值与获取锁时的标识是否一致。</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> redis.call(<span class="string">&#x27;get&#x27;</span>, KEYS[<span class="number">1</span>]) == ARGV[<span class="number">1</span>] <span class="keyword">then</span> <span class="keyword">return</span> redis.call(<span class="string">&#x27;del&#x27;</span>, KEYS[<span class="number">1</span>]) <span class="keyword">else</span> <span class="keyword">return</span> <span class="number">0</span> <span class="keyword">end</span></span><br></pre></td></tr></table></figure><h2 id="mybatis缓存机制"><a href="#mybatis缓存机制" class="headerlink" title="mybatis缓存机制"></a>mybatis缓存机制</h2><p>MyBatis 提供了两种缓存机制：<strong>一级缓存</strong>（本地缓存）和 <strong>二级缓存</strong>（全局缓存）。</p><p>一级缓存是 MyBatis 的本地缓存，每次 SQL 查询会在当前 <code>SqlSession</code> 中缓存查询结果。一级缓存的生命周期与 <code>SqlSession</code> 绑定，即 <code>SqlSession</code> 在关闭时缓存会被清空。</p><p><strong>作用范围</strong>：每个 <code>SqlSession</code> 具有自己的一级缓存，缓存的数据只能在当前 <code>SqlSession</code> 内部访问。</p><p><strong>自动生效</strong>：MyBatis 默认启用一级缓存，不需要额外配置。</p><p><strong>缓存清空</strong>：在以下情况下一级缓存会被清空：</p><ul><li><code>SqlSession</code> 的 <code>commit()</code> 或 <code>rollback()</code> 操作。</li><li>执行了 <code>clearCache()</code> 方法。</li><li>执行了增、删、改操作（因为这些操作可能影响到缓存的有效性）。</li></ul><p>二级缓存是 MyBatis 的全局缓存，缓存的生命周期与 <code>SqlSessionFactory</code> 绑定，跨多个 <code>SqlSession</code> 实例共享。适用于多个 <code>SqlSession</code> 使用相同的数据源的情况。</p><p><strong>作用范围</strong>：多个 <code>SqlSession</code> 可以共享二级缓存，缓存的数据可以被不同的 <code>SqlSession</code> 访问。</p><p><strong>需要配置</strong>：二级缓存需要在 MyBatis 配置文件中启用，并为每个 <code>Mapper</code> 映射器配置缓存。</p><p><strong>缓存清空</strong>：当执行增、删、改操作时，相关的缓存会被清空。</p><p><strong>脏数据问题</strong>：二级缓存可能导致脏数据问题，因为多个 SqlSession 可能共享相同的缓存数据。如果一个 SqlSession 更新了数据但没有及时清空或更新缓存，其他 SqlSession 可能会读取到旧的数据。</p><h3 id="缓存失效条件"><a href="#缓存失效条件" class="headerlink" title="缓存失效条件"></a>缓存失效条件</h3><ul><li><strong>一级缓存</strong>：在 SqlSession 执行插入、更新或删除操作后，默认会清空一级缓存。</li><li><strong>二级缓存</strong>：在执行插入、更新或删除操作后，如果配置了相应的映射语句的 <code>flushCache</code> 属性为 <code>true</code>，则会清空涉及的命名空间的二级缓存。</li></ul><h2 id="mybatis什么时候读取会产生脏数据"><a href="#mybatis什么时候读取会产生脏数据" class="headerlink" title="mybatis什么时候读取会产生脏数据"></a>mybatis什么时候读取会产生脏数据</h2><p><strong>多会话并发操作</strong>：在多线程或多会话环境中，如果一个会话（SqlSession）读取了数据并将其缓存，而另一个会话对相同的数据进行了修改并提交了事务，那么第一个会话的缓存就会变成脏数据。</p><p><strong>缓存未设置为只读</strong>：MyBatis的二级缓存默认是只读的（<code>readOnly</code>属性为<code>true</code>），这意味着缓存的数据不会被修改。如果错误地将缓存设置为可写（<code>readOnly</code>属性为<code>false</code>），那么缓存的数据可能会被修改，导致脏数据。</p><p><strong>事务管理不当</strong>: 事务的隔离级别可能不够高，导致其他事务能看到未提交的数据。MyBatis 的一级缓存和二级缓存都受事务管理的影响，事务的隔离级别可能导致缓存中的数据与数据库中的实际数据不一致。</p><p><strong>缓存键不准确</strong>：如果缓存键的生成逻辑不正确，可能会导致不同的数据请求被错误地映射到同一个缓存键，从而覆盖彼此的数据，产生脏数据。</p><p>从AI回答中挑了几个可能原因。</p><h2 id="sql监控插件如何实现"><a href="#sql监控插件如何实现" class="headerlink" title="sql监控插件如何实现"></a>sql监控插件如何实现</h2><h3 id="使用-MyBatis-自定义拦截器"><a href="#使用-MyBatis-自定义拦截器" class="headerlink" title="使用 MyBatis 自定义拦截器"></a>使用 MyBatis 自定义拦截器</h3><p>MyBatis 提供了拦截器机制，可以拦截执行 SQL 的关键点，例如 <code>Executor</code>、<code>StatementHandler</code>、<code>ResultSetHandler</code> 等。通过自定义拦截器，可以在 SQL 执行前后记录时间，并计算执行耗时。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> org.apache.ibatis.executor.Executor;</span><br><span class="line"><span class="keyword">import</span> org.apache.ibatis.mapping.MappedStatement;</span><br><span class="line"><span class="keyword">import</span> org.apache.ibatis.plugin.*;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.util.Properties;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Intercepts(&#123;</span></span><br><span class="line"><span class="meta">        @Signature(type = Executor.class, method = &quot;query&quot;, args = &#123;MappedStatement.class, Object.class&#125;),</span></span><br><span class="line"><span class="meta">        @Signature(type = Executor.class, method = &quot;update&quot;, args = &#123;MappedStatement.class, Object.class&#125;)</span></span><br><span class="line"><span class="meta">&#125;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SqlTimeInterceptor</span> <span class="keyword">implements</span> <span class="title class_">Interceptor</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Object <span class="title function_">intercept</span><span class="params">(Invocation invocation)</span> <span class="keyword">throws</span> Throwable &#123;</span><br><span class="line">        <span class="type">long</span> <span class="variable">startTime</span> <span class="operator">=</span> System.currentTimeMillis();</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> invocation.proceed();  <span class="comment">// 执行SQL</span></span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            <span class="type">long</span> <span class="variable">endTime</span> <span class="operator">=</span> System.currentTimeMillis();</span><br><span class="line">            <span class="type">long</span> <span class="variable">executionTime</span> <span class="operator">=</span> endTime - startTime;</span><br><span class="line"></span><br><span class="line">            <span class="type">MappedStatement</span> <span class="variable">mappedStatement</span> <span class="operator">=</span> (MappedStatement) invocation.getArgs()[<span class="number">0</span>];</span><br><span class="line">            <span class="type">String</span> <span class="variable">sqlId</span> <span class="operator">=</span> mappedStatement.getId();</span><br><span class="line"></span><br><span class="line">            System.out.println(<span class="string">&quot;SQL [&quot;</span> + sqlId + <span class="string">&quot;] 执行耗时: &quot;</span> + executionTime + <span class="string">&quot; ms&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Object <span class="title function_">plugin</span><span class="params">(Object target)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> Plugin.wrap(target, <span class="built_in">this</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setProperties</span><span class="params">(Properties properties)</span> &#123;</span><br><span class="line">        <span class="comment">// 可配置属性</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后注册这个拦截器</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> org.apache.ibatis.session.SqlSessionFactory;</span><br><span class="line"><span class="keyword">import</span> org.mybatis.spring.boot.autoconfigure.MybatisProperties;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Bean;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Configuration;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MybatisConfig</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> MybatisProperties mybatisProperties;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> SqlSessionFactory <span class="title function_">sqlSessionFactory</span><span class="params">(javax.sql.DataSource dataSource)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        org.apache.ibatis.session.<span class="type">Configuration</span> <span class="variable">configuration</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">org</span>.apache.ibatis.session.Configuration();</span><br><span class="line">        configuration.setDataSource(dataSource);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 注册自定义拦截器</span></span><br><span class="line">        <span class="type">SqlTimeInterceptor</span> <span class="variable">sqlTimeInterceptor</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SqlTimeInterceptor</span>();</span><br><span class="line">        configuration.addInterceptor(sqlTimeInterceptor);</span><br><span class="line"></span><br><span class="line">        org.mybatis.spring.<span class="type">SqlSessionFactoryBean</span> <span class="variable">sessionFactoryBean</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">org</span>.mybatis.spring.SqlSessionFactoryBean();</span><br><span class="line">        sessionFactoryBean.setDataSource(dataSource);</span><br><span class="line">        sessionFactoryBean.setConfiguration(configuration);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> sessionFactoryBean.getObject();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="使用-MyBatis-Plus-内置功能"><a href="#使用-MyBatis-Plus-内置功能" class="headerlink" title="使用 MyBatis-Plus 内置功能"></a>使用 MyBatis-Plus 内置功能</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Bean;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Configuration;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MybatisPlusConfig</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> PerformanceInterceptor <span class="title function_">performanceInterceptor</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="type">PerformanceInterceptor</span> <span class="variable">performanceInterceptor</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">PerformanceInterceptor</span>();</span><br><span class="line">        performanceInterceptor.setMaxTime(<span class="number">1000</span>); <span class="comment">// 设置SQL最大执行时间，超过将抛出异常</span></span><br><span class="line">        performanceInterceptor.setFormat(<span class="literal">true</span>);  <span class="comment">// 格式化SQL输出</span></span><br><span class="line">        <span class="keyword">return</span> performanceInterceptor;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="mq如何保证顺序性"><a href="#mq如何保证顺序性" class="headerlink" title="mq如何保证顺序性"></a>mq如何保证顺序性</h2><p>Kafka通过分区（partition）来保证消息的顺序性。在Kafka中，消息在每个分区内是按顺序存储的，消费者在读取消息时也是按照这个顺序读取的。只要消息被写入同一个分区，Kafka就能保证这些消息在消费时是按照生产的顺序被处理的。</p><p>如果希望保证某些消息的顺序性（例如，某个用户的所有操作日志必须按顺序处理），可以使用消息键（key）来控制消息的分区选择。Kafka根据消息键对分区数取模（hashing），将具有相同键的消息放入同一个分区，从而保证了这些消息在分区内的顺序。</p><p>消费者组中的每个消费者消费一个或多个分区，从而保证了这些消息在单个消费者内的顺序。</p><p>在RabbitMQ中，消息在队列中是按顺序存储的，消费者从队列中消费消息时，消息的消费顺序与生产顺序一致。因此，使用一个队列并确保一个消费者处理队列中的消息时，可以保证消息的顺序性。</p><p>如果多个消费者同时从同一个队列中消费，RabbitMQ会以轮询方式分配消息，这可能导致顺序被打乱。因此，如果需要严格的顺序，应该使用单个消费者。</p><p><strong>通过设计不同的队列来处理顺序性。</strong>例如，如果有多种类型的消息需要保持顺序，可以为每种类型创建单独的队列，并保证每个队列由一个消费者处理。</p><h2 id="队列能有多少消费者"><a href="#队列能有多少消费者" class="headerlink" title="队列能有多少消费者"></a>队列能有多少消费者</h2><p>这个问题我面试之后挺气的。面试官说我topic和队列搞混了，他应该认为分区是队列的概念。查了一下，其实当成队列也没毛病，是我孤陋寡闻了。</p><p>可以简单理解成kafka分区就是队列。</p><h3 id="kafka"><a href="#kafka" class="headerlink" title="kafka"></a>kafka</h3><p><strong>单个消费者组内的消费者数量</strong>：受限于<code>topic</code>的分区数量，最多可以有与分区数相同的消费者并行消费。如果消费者数量超过分区数，多余的消费者将处于空闲状态。</p><p><strong>多个消费者组</strong>：Kafka允许同一个<code>topic</code>有多个消费者组，每个消费者组的消费者数与分区数的关系相同。多个消费者组可以独立并行消费同一个<code>topic</code>，从而允许无限制数量的消费者整体上并行消费。</p><h3 id="rabbitmq"><a href="#rabbitmq" class="headerlink" title="rabbitmq"></a>rabbitmq</h3><p><strong>理论上，RabbitMQ能支持的消费者数量没有固定的上限</strong>。但实际可支持的消费者数量取决于服务器的资源（CPU、内存、网络带宽）、RabbitMQ的配置以及消息处理的速度。</p><h2 id="线程池如何设置核心线程数和最大线程数"><a href="#线程池如何设置核心线程数和最大线程数" class="headerlink" title="线程池如何设置核心线程数和最大线程数"></a>线程池如何设置核心线程数和最大线程数</h2><p>核心线程数：</p><ol><li>CPU密集型任务：<code>核心线程数 = CPU核心数 + 1</code></li><li>I/O密集型任务：<code>核心线程数 = CPU核心数 * 2</code> 或 <code>核心线程数 = CPU核心数 * (1 + 任务等待时间/任务执行时间)</code>。</li></ol><p>最大线程数：</p><p>最大线程数是线程池中允许存在的最大线程数。当任务数超过核心线程数并且队列已满时，线程池会创建新线程，直到达到最大线程数。如果线程数达到了最大值，新的任务将被拒绝。最大线程数应该足够大，以应对高峰负载时的需求，但也不宜过大，以免系统资源耗尽导致性能下降。</p><p>对于大多数I/O密集型应用，<code>最大线程数</code>通常设置为<code>核心线程数</code>的2倍或3倍。</p><p>对于CPU密集型应用，<code>最大线程数</code>可以设置为<code>核心线程数</code>或略大一些。</p><h2 id="在一个线程池中，核心线程数为3，最大线程数为10，此时有五个任务提交，会有多少个线程产生"><a href="#在一个线程池中，核心线程数为3，最大线程数为10，此时有五个任务提交，会有多少个线程产生" class="headerlink" title="在一个线程池中，核心线程数为3，最大线程数为10，此时有五个任务提交，会有多少个线程产生"></a>在一个线程池中，核心线程数为3，最大线程数为10，此时有五个任务提交，会有多少个线程产生</h2><p>这题原来我答错了。要复习一下线程池的处理逻辑。</p><ol><li>如果当前运行的线程数小于核心线程数，那么就会新建一个线程来执行任务。</li><li>如果当前运行的线程数等于或大于核心线程数，但是小于最大线程数，那么就把该任务放入到任务队列里等待执行。</li><li>如果向任务队列投放任务失败（任务队列已经满了），但是当前运行的线程数是小于最大线程数的，就新建一个线程来执行任务。</li><li>如果当前运行的线程数已经等同于最大线程数了，新建线程将会使当前运行的线程超出最大线程数，那么当前任务会被拒绝，拒绝策略会调用<code>RejectedExecutionHandler.rejectedExecution()</code>方法。</li></ol><p>gpt给我的解答是：</p><h3 id="情景1：使用无界队列（如LinkedBlockingQueue）"><a href="#情景1：使用无界队列（如LinkedBlockingQueue）" class="headerlink" title="情景1：使用无界队列（如LinkedBlockingQueue）"></a>情景1：使用无界队列（如<code>LinkedBlockingQueue</code>）</h3><ul><li><strong>核心线程数（3）</strong>：首先，线程池会尝试使用核心线程来处理任务。如果核心线程数未达到上限，那么新任务会直接创建新线程来执行，直到核心线程数达到3。</li><li><strong>最大线程数（10）</strong>：如果所有核心线程都在工作，而队列没有容量限制，那么新任务会被放入队列中，而不会创建新的线程。</li><li><strong>五个任务提交的处理方式</strong>：<ol><li>线程池会创建3个核心线程来处理前3个任务。</li><li>剩余的两个任务会被放入队列，等待空闲线程来处理。</li></ol></li><li><strong>最终结果</strong>：线程池将会有3个活跃线程在处理任务。</li></ul><h3 id="情景2：使用直接提交策略（如SynchronousQueue）"><a href="#情景2：使用直接提交策略（如SynchronousQueue）" class="headerlink" title="情景2：使用直接提交策略（如SynchronousQueue）"></a>情景2：使用直接提交策略（如<code>SynchronousQueue</code>）</h3><ul><li><strong>直接提交策略</strong>：使用<code>SynchronousQueue</code>时，每个任务必须被立即处理或执行，如果没有空闲线程来处理新任务，则会创建新的线程，直到达到最大线程数。</li><li><strong>五个任务提交的处理方式</strong>：<ol><li>线程池会创建3个核心线程来处理前3个任务。</li><li>剩余的两个任务由于没有空闲线程可用，线程池会创建2个非核心线程（达到最大线程数5）来处理这两个任务。</li></ol></li><li><strong>最终结果</strong>：线程池将会有5个活跃线程在处理任务。</li></ul><p>我对比了一下，gpt说的没有错。SynchronousQueue是没有容量的。所以会直接创建线程去处理。</p><h1 id="后话"><a href="#后话" class="headerlink" title="后话"></a>后话</h1><p>复盘下来，好像确实有比较多的问题答得不好，比如mybatis相关，还有最后一个线程池相关我也记错了。</p><p>在此向两位面试官道歉，虽然面试过程中并没有特别不愉快，但是一个小时后就发拒绝邮件使我变得暴躁，把原因都归结于你们的不专业上，但是复盘之后发现是自己水平不够，很多问题即使有文档帮助依旧回答的不够好。在此郑重道歉。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;今天zoom面试，亏我还那么期待，感觉好像面试官水平不咋样啊，面试完后一个小时直接发拒绝邮件了，我真是醉了，我感觉大部分回答的还可以吧，除了
      
    
    </summary>
    
    
      <category term="编程" scheme="https://blog.zer0e.com/tags/%E7%BC%96%E7%A8%8B/"/>
    
      <category term="随想" scheme="https://blog.zer0e.com/tags/%E9%9A%8F%E6%83%B3/"/>
    
      <category term="面经" scheme="https://blog.zer0e.com/tags/%E9%9D%A2%E7%BB%8F/"/>
    
  </entry>
  
  <entry>
    <title>2024面试复盘12</title>
    <link href="https://blog.zer0e.com/2024/08/14/2024-08-14-replay/"/>
    <id>https://blog.zer0e.com/2024/08/14/2024-08-14-replay/</id>
    <published>2024-08-14T12:00:00.000Z</published>
    <updated>2024-08-14T12:28:21.626Z</updated>
    
    <content type="html"><![CDATA[<h2 id="bin-log-redo-log-undo-log"><a href="#bin-log-redo-log-undo-log" class="headerlink" title="bin log, redo log, undo log"></a>bin log, redo log, undo log</h2><p>MySQL 日志 主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。其中，比较重要的还要属二进制日志 binlog（归档日志）和事务日志 redo log（重做日志）和 undo log（回滚日志）。</p><h3 id="redo-log"><a href="#redo-log" class="headerlink" title="redo log"></a>redo log</h3><p>redo log（重做日志）是 InnoDB 存储引擎独有的，它让 MySQL 拥有了崩溃恢复能力。</p><p>比如 MySQL 实例挂了或宕机了，重启时，InnoDB 存储引擎会使用 redo log 恢复数据，保证数据的持久性与完整性。</p><p>MySQL 中数据是以页为单位，你查询一条记录，会从硬盘把一页的数据加载出来，加载出来的数据叫数据页，会放入到 <code>Buffer Pool</code> 中。</p><p>后续的查询都是先从 <code>Buffer Pool</code> 中找，没有命中再去硬盘加载，减少硬盘 IO 开销，提升性能。</p><p>更新表数据的时候，也是如此，发现 <code>Buffer Pool</code> 里存在要更新的数据，就直接在 <code>Buffer Pool</code> 里更新。</p><p>然后会把“在某个数据页上做了什么修改”记录到重做日志缓存（<code>redo log buffer</code>）里，接着刷盘到 redo log 文件里。</p><p>理想情况，事务一提交就会进行刷盘操作，但实际上，刷盘的时机是根据策略来进行的。</p><p>InnoDB 将 redo log 刷到磁盘上有几种情况：</p><ol><li>事务提交：当事务提交时，log buffer 里的 redo log 会被刷新到磁盘（可以通过<code>innodb_flush_log_at_trx_commit</code>参数控制，后文会提到）。</li><li>log buffer 空间不足时：log buffer 中缓存的 redo log 已经占满了 log buffer 总容量的大约一半左右，就需要把这些日志刷新到磁盘上。</li><li>事务日志缓冲区满：InnoDB 使用一个事务日志缓冲区（transaction log buffer）来暂时存储事务的重做日志条目。当缓冲区满时，会触发日志的刷新，将日志写入磁盘。</li><li>Checkpoint（检查点）：InnoDB 定期会执行检查点操作，将内存中的脏数据（已修改但尚未写入磁盘的数据）刷新到磁盘，并且会将相应的重做日志一同刷新，以确保数据的一致性。</li><li>后台刷新线程：InnoDB 启动了一个后台线程，负责周期性（每隔 1 秒）地将脏页（已修改但尚未写入磁盘的数据页）刷新到磁盘，并将相关的重做日志一同刷新。</li><li>正常关闭服务器：MySQL 关闭的时候，redo log 都会刷入到磁盘里去。</li></ol><p>总之，InnoDB 在多种情况下会刷新重做日志，以保证数据的持久性和一致性。</p><p>我们要注意设置正确的刷盘策略<code>innodb_flush_log_at_trx_commit</code> 。根据 MySQL 配置的刷盘策略的不同，MySQL 宕机之后可能会存在轻微的数据丢失问题。</p><p><code>innodb_flush_log_at_trx_commit</code> 的值有 3 种，也就是共有 3 种刷盘策略：</p><ul><li><strong>0</strong>：设置为 0 的时候，表示每次事务提交时不进行刷盘操作。这种方式性能最高，但是也最不安全，因为如果 MySQL 挂了或宕机了，可能会丢失最近 1 秒内的事务。</li><li><strong>1</strong>：设置为 1 的时候，表示每次事务提交时都将进行刷盘操作。这种方式性能最低，但是也最安全，因为只要事务提交成功，redo log 记录就一定在磁盘里，不会有任何数据丢失。</li><li><strong>2</strong>：设置为 2 的时候，表示每次事务提交时都只把 log buffer 里的 redo log 内容写入 page cache（文件系统缓存）。page cache 是专门用来缓存文件的，这里被缓存的文件就是 redo log 文件。这种方式的性能和安全性都介于前两者中间。</li></ul><p>刷盘策略<code>innodb_flush_log_at_trx_commit</code> 的默认值为 1，设置为 1 的时候才不会丢失任何数据。为了保证事务的持久性，我们必须将其设置为 1。</p><p>另外，InnoDB 存储引擎有一个后台线程，每隔<code>1</code> 秒，就会把 <code>redo log buffer</code> 中的内容写到文件系统缓存（<code>page cache</code>），然后调用 <code>fsync</code> 刷盘。</p><h4 id="一个没有提交事务的-redo-log-记录，也可能会刷盘。"><a href="#一个没有提交事务的-redo-log-记录，也可能会刷盘。" class="headerlink" title="一个没有提交事务的 redo log 记录，也可能会刷盘。"></a>一个没有提交事务的 redo log 记录，也可能会刷盘。</h4><p>因为在事务执行过程 redo log 记录是会写入<code>redo log buffer</code> 中，这些 redo log 记录会被后台线程刷盘。</p><h4 id="日志文件组"><a href="#日志文件组" class="headerlink" title="日志文件组"></a>日志文件组</h4><p>硬盘上存储的 redo log 日志文件不只一个，而是以一个<strong>日志文件组</strong>的形式出现的，每个的<code>redo</code>日志文件大小都是一样的。</p><p>比如可以配置为一组<code>4</code>个文件，每个文件的大小是 <code>1GB</code>，整个 redo log 日志文件组可以记录<code>4G</code>的内容。</p><p>在这个<strong>日志文件组</strong>中还有两个重要的属性，分别是 <code>write pos、checkpoint</code></p><ul><li><strong>write pos</strong> 是当前记录的位置，一边写一边后移</li><li><strong>checkpoint</strong> 是当前要擦除的位置，也是往后推移</li></ul><p>每次刷盘 redo log 记录到<strong>日志文件组</strong>中，<code>write pos</code> 位置就会后移更新。</p><p>每次 MySQL 加载<strong>日志文件组</strong>恢复数据时，会清空加载过的 redo log 记录，并把 <code>checkpoint</code> 后移更新。</p><p><code>write pos</code> 和 <code>checkpoint</code> 之间的还空着的部分可以用来写入新的 redo log 记录。</p><p>如果 <code>write pos</code> 追上 <code>checkpoint</code> ，表示<strong>日志文件组</strong>满了，这时候不能再写入新的 redo log 记录，MySQL 得停下来，清空一些记录，把 <code>checkpoint</code> 推进一下。</p><p>在 MySQL 8.0.30 之前可以通过 <code>innodb_log_files_in_group</code> 和 <code>innodb_log_file_size</code> 配置日志文件组的文件数和文件大小，但在 MySQL 8.0.30 及之后的版本中，这两个变量已被废弃，即使被指定也是用来计算 <code>innodb_redo_log_capacity</code> 的值。而日志文件组的文件数则固定为 32，文件大小则为 <code>innodb_redo_log_capacity / 32</code> 。</p><h3 id="bin-log"><a href="#bin-log" class="headerlink" title="bin log"></a>bin log</h3><p>redo log 它是物理日志，记录内容是“在某个数据页上做了什么修改”，属于 InnoDB 存储引擎。</p><p>而 binlog 是逻辑日志，记录内容是语句的原始逻辑，类似于“给 ID=2 这一行的 c 字段加 1”，属于<code>MySQL Server</code> 层。</p><p>不管用什么存储引擎，只要发生了表数据更新，都会产生 binlog 日志。</p><p>可以说 MySQL 数据库的<strong>数据备份、主备、主主、主从</strong>都离不开 binlog，需要依靠 binlog 来同步数据，保证数据一致性。</p><p>binlog 会记录所有涉及更新数据的逻辑操作，并且是顺序写。</p><p>binlog 日志有三种格式，可以通过<code>binlog_format</code>参数指定。</p><ul><li><strong>statement</strong></li><li><strong>row</strong></li><li><strong>mixed</strong></li></ul><p>指定<code>statement</code>，记录的内容是<code>SQL</code>语句原文，比如执行一条<code>update T set update_time=now() where id=1</code>，记录的内容如下。</p><p>同步数据时，会执行记录的<code>SQL</code>语句，但是有个问题，<code>update_time=now()</code>这里会获取当前系统时间，直接执行会导致与原库的数据不一致。</p><p>为了解决这种问题，我们需要指定为<code>row</code>，记录的内容不再是简单的<code>SQL</code>语句了，还包含操作的具体数据。</p><p><code>row</code>格式记录的内容看不到详细信息，要通过<code>mysqlbinlog</code>工具解析出来。</p><p><code>update_time=now()</code>变成了具体的时间<code>update_time=1627112756247</code>，条件后面的@1、@2、@3 都是该行数据第 1 个~3 个字段的原始值（<strong>假设这张表只有 3 个字段</strong>）。</p><p>这样就能保证同步数据的一致性，通常情况下都是指定为<code>row</code>，这样可以为数据库的恢复与同步带来更好的可靠性。</p><p>但是这种格式，需要更大的容量来记录，比较占用空间，恢复与同步时会更消耗 IO 资源，影响执行速度。</p><p>所以就有了一种折中的方案，指定为<code>mixed</code>，记录的内容是前两者的混合。</p><p>MySQL 会判断这条<code>SQL</code>语句是否可能引起数据不一致，如果是，就用<code>row</code>格式，否则就用<code>statement</code>格式。</p><h4 id="写入机制"><a href="#写入机制" class="headerlink" title="写入机制"></a>写入机制</h4><p>binlog 的写入时机也非常简单，事务执行过程中，先把日志写到<code>binlog cache</code>，事务提交的时候，再把<code>binlog cache</code>写到 binlog 文件中。</p><p>因为一个事务的 binlog 不能被拆开，无论这个事务多大，也要确保一次性写入，所以系统会给每个线程分配一个块内存作为<code>binlog cache</code>。</p><p>我们可以通过<code>binlog_cache_size</code>参数控制单个线程 binlog cache 大小，如果存储内容超过了这个参数，就要暂存到磁盘（<code>Swap</code>）。</p><p><code>write</code>和<code>fsync</code>的时机，可以由参数<code>sync_binlog</code>控制，默认是<code>1</code>。</p><p>为<code>0</code>的时候，表示每次提交事务都只<code>write</code>，由系统自行判断什么时候执行<code>fsync</code>。</p><p>虽然性能得到提升，但是机器宕机，<code>page cache</code>里面的 binlog 会丢失。</p><p>为了安全起见，可以设置为<code>1</code>，表示每次提交事务都会执行<code>fsync</code>，就如同 <strong>redo log 日志刷盘流程</strong> 一样。</p><p>最后还有一种折中方式，可以设置为<code>N(N&gt;1)</code>，表示每次提交事务都<code>write</code>，但累积<code>N</code>个事务后才<code>fsync</code>。</p><p>在出现 IO 瓶颈的场景里，将<code>sync_binlog</code>设置成一个比较大的值，可以提升性能。</p><p>同样的，如果机器宕机，会丢失最近<code>N</code>个事务的 binlog 日志。</p><h4 id="两阶段提交"><a href="#两阶段提交" class="headerlink" title="两阶段提交"></a>两阶段提交</h4><p>redo log（重做日志）让 InnoDB 存储引擎拥有了崩溃恢复能力。</p><p>binlog（归档日志）保证了 MySQL 集群架构的数据一致性。</p><p>虽然它们都属于持久化的保证，但是侧重点不同。</p><p>在执行更新语句过程，会记录 redo log 与 binlog 两块日志，以基本的事务为单位，redo log 在事务执行过程中可以不断写入，而 binlog 只有在提交事务时才写入，所以 redo log 与 binlog 的写入时机不一样。</p><p>回到正题，redo log 与 binlog 两份日志之间的逻辑不一致，会出现什么问题？</p><p>我们以<code>update</code>语句为例，假设<code>id=2</code>的记录，字段<code>c</code>值是<code>0</code>，把字段<code>c</code>值更新成<code>1</code>，<code>SQL</code>语句为<code>update T set c=1 where id=2</code>。</p><p>假设执行过程中写完 redo log 日志后，binlog 日志写期间发生了异常，会出现什么情况呢？</p><p>由于 binlog 没写完就异常，这时候 binlog 里面没有对应的修改记录。因此，之后用 binlog 日志恢复数据时，就会少这一次更新，恢复出来的这一行<code>c</code>值是<code>0</code>，而原库因为 redo log 日志恢复，这一行<code>c</code>值是<code>1</code>，最终数据不一致。</p><p>为了解决两份日志之间的逻辑一致问题，InnoDB 存储引擎使用<strong>两阶段提交</strong>方案。</p><p>原理很简单，将 redo log 的写入拆成了两个步骤<code>prepare</code>和<code>commit</code>，这就是<strong>两阶段提交</strong>。</p><p>使用<strong>两阶段提交</strong>后，写入 binlog 时发生异常也不会有影响，因为 MySQL 根据 redo log 日志恢复数据时，发现 redo log 还处于<code>prepare</code>阶段，并且没有对应 binlog 日志，就会回滚该事务。</p><h3 id="undo-log"><a href="#undo-log" class="headerlink" title="undo log"></a>undo log</h3><p>每一个事务对数据的修改都会被记录到 undo log ，当执行事务过程中出现错误或者需要执行回滚操作的话，MySQL 可以利用 undo log 将数据恢复到事务开始之前的状态。</p><p>undo log 属于逻辑日志，记录的是 SQL 语句，比如说事务执行一条 DELETE 语句，那 undo log 就会记录一条相对应的 INSERT 语句。同时，undo log 的信息也会被记录到 redo log 中，因为 undo log 也要实现持久性保护。并且，undo-log 本身是会被删除清理的，例如 INSERT 操作，在事务提交之后就可以清除掉了；UPDATE/DELETE 操作在事务提交不会立即删除，会加入 history list，由后台线程 purge 进行清理。</p><p>undo log 是采用 segment（段）的方式来记录的，每个 undo 操作在记录的时候占用一个 <strong>undo log segment</strong>（undo 日志段），undo log segment 包含在 <strong>rollback segment</strong>（回滚段）中。事务开始时，需要为其分配一个 rollback segment。每个 rollback segment 有 1024 个 undo log segment，这有助于管理多个并发事务的回滚需求。</p><p>通常情况下， <strong>rollback segment header</strong>（通常在回滚段的第一个页）负责管理 rollback segment。rollback segment header 是 rollback segment 的一部分，通常在回滚段的第一个页。<strong>history list</strong> 是 rollback segment header 的一部分，它的主要作用是记录所有已经提交但还没有被清理（purge）的事务的 undo log。这个列表使得 purge 线程能够找到并清理那些不再需要的 undo log 记录。</p><p>另外，<code>MVCC</code> 的实现依赖于：<strong>隐藏字段、Read View、undo log</strong>。在内部实现中，InnoDB 通过数据行的 <code>DB_TRX_ID</code> 和 <code>Read View</code> 来判断数据的可见性，如不可见，则通过数据行的 <code>DB_ROLL_PTR</code> 找到 undo log 中的历史版本。每个事务读到的数据版本可能是不一样的，在同一个事务中，用户只能看到该事务创建 <code>Read View</code> 之前已经提交的修改和该事务本身做的修改。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>MySQL InnoDB 引擎使用 <strong>redo log(重做日志)</strong> 保证事务的<strong>持久性</strong>，使用 <strong>undo log(回滚日志)</strong> 来保证事务的<strong>原子性</strong>。</p><p>MySQL 数据库的<strong>数据备份、主备、主主、主从</strong>都离不开 binlog，需要依靠 binlog 来同步数据，保证数据一致性。</p><h2 id="和-equals的区别"><a href="#和-equals的区别" class="headerlink" title="== 和 equals的区别"></a>== 和 equals的区别</h2><ul><li>对于基本数据类型来说，<code>==</code> 比较的是值。</li><li>对于引用数据类型来说，<code>==</code> 比较的是对象的内存地址。</li></ul><p><strong><code>equals()</code></strong> 不能用于判断基本数据类型的变量，只能用来判断两个对象是否相等。<code>equals()</code>方法存在于<code>Object</code>类中，而<code>Object</code>类是所有类的直接或间接父类，因此所有的类都有<code>equals()</code>方法。</p><p><code>equals()</code> 方法存在两种使用情况：</p><ul><li><strong>类没有重写 <code>equals()</code>方法</strong>：通过<code>equals()</code>比较该类的两个对象时，等价于通过“==”比较这两个对象，使用的默认是 <code>Object</code>类<code>equals()</code>方法。</li><li><strong>类重写了 <code>equals()</code>方法</strong>：一般我们都重写 <code>equals()</code>方法来比较两个对象中的属性是否相等；若它们的属性相等，则返回 true(即，认为这两个对象相等)。</li></ul><h2 id="为什么重写-equals-时必须重写-hashCode-方法？"><a href="#为什么重写-equals-时必须重写-hashCode-方法？" class="headerlink" title="为什么重写 equals() 时必须重写 hashCode() 方法？"></a>为什么重写 equals() 时必须重写 hashCode() 方法？</h2><p>因为两个相等的对象的 <code>hashCode</code> 值必须是相等。也就是说如果 <code>equals</code> 方法判断两个对象是相等的，那这两个对象的 <code>hashCode</code> 值也要相等。</p><p>如果重写 <code>equals()</code> 时没有重写 <code>hashCode()</code> 方法的话就可能会导致 <code>equals</code> 方法判断是相等的两个对象，<code>hashCode</code> 值却不相等。</p><h2 id="索引是怎么使用？失效时机？"><a href="#索引是怎么使用？失效时机？" class="headerlink" title="索引是怎么使用？失效时机？"></a>索引是怎么使用？失效时机？</h2><p>索引是一种用于加速数据库表中数据检索的结构。它类似于书籍中的目录，通过对数据的关键列建立索引，可以快速定位所需的数据行，而无需全表扫描。</p><p><strong>类型</strong>：</p><ul><li><strong>B-Tree 索引</strong>：最常见的索引类型，适用于大多数查询操作。</li><li><strong>Hash 索引</strong>：基于哈希表，适用于精确匹配查询。</li><li><strong>Full-Text 索引</strong>：用于全文搜索。</li><li><strong>R-Tree 索引</strong>：用于空间数据类型（如 GIS 数据）的查询。</li></ul><p><strong>优点</strong>：</p><ul><li>使用索引可以大大加快数据的检索速度（大大减少检索的数据量）, 减少 IO 次数，这也是创建索引的最主要的原因。</li><li>通过创建唯一性索引，可以保证数据库表中每一行数据的唯一性。</li></ul><p><strong>缺点</strong>：</p><ul><li>创建索引和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候，如果数据有索引，那么索引也需要动态的修改，会降低 SQL 执行效率。</li><li>索引需要使用物理文件存储，也会耗费一定空间。</li></ul><p>但是，<strong>使用索引一定能提高查询性能吗?</strong></p><p>大多数情况下，索引查询都是比全表扫描要快的。但是如果数据库的数据量不大，那么使用索引也不一定能够带来很大提升。</p><h3 id="索引失效场景"><a href="#索引失效场景" class="headerlink" title="索引失效场景"></a>索引失效场景</h3><p>创建了组合索引，但查询条件未遵守最左匹配原则;</p><p>在索引列上进行计算、函数、类型转换等操作;</p><p>以 % 开头的 LIKE 查询比如 <code>LIKE &#39;%abc&#39;;</code>;</p><p>查询条件中使用 OR，且 OR 的前后条件中有一个列没有索引，涉及的索引都不会被使用到;</p><p>IN 的取值范围较大时会导致索引失效，走全表扫描(NOT IN 和 IN 的失效场景相同);</p><p>发生隐式转换；</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> users <span class="keyword">WHERE</span> username <span class="operator">=</span> <span class="number">123</span>;  <span class="comment">-- 假设 username 是字符串类型，索引失效</span></span><br></pre></td></tr></table></figure><p>查询条件使用 <code>!=</code> 或 <code>&lt;&gt;</code></p><p>使用 <code>IS NULL</code> 或 <code>IS NOT NULL</code></p><p>排序操作（ORDER BY）中混合使用 ASC 和 DESC</p><p>表中的数据量很少</p><h2 id="mysql中的锁"><a href="#mysql中的锁" class="headerlink" title="mysql中的锁"></a>mysql中的锁</h2><h3 id="乐观锁与悲观锁"><a href="#乐观锁与悲观锁" class="headerlink" title="乐观锁与悲观锁"></a>乐观锁与悲观锁</h3><p><strong>乐观锁</strong> 基于乐观的假设，认为多个事务并发修改数据时不会发生冲突。它通常不直接使用数据库的锁机制，而是通过版本号或时间戳等机制来实现。</p><p>在数据库表中添加一个 <code>version</code> 字段，表示数据的版本。当事务读取数据时，也会读取这个 <code>version</code>。在提交更新时，事务会检查当前数据库中的 <code>version</code> 是否与自己读取时的一致。如果一致，才会更新数据，并将 <code>version</code> 加1；如果不一致，表示有其他事务已经修改了该数据，当前事务需要进行重试或处理冲突。</p><p><strong>悲观锁</strong> 采取悲观的态度，假设并发修改数据时会发生冲突。因此，在操作数据之前，会先锁住数据，以防止其他事务对其进行修改。悲观锁通常通过数据库的锁机制来实现。    </p><p>使用 <code>SELECT ... FOR UPDATE</code> 或 <code>LOCK IN SHARE MODE</code> 语句显式地对数据行进行加锁。在使用 <code>FOR UPDATE</code> 时，该行数据会被锁住，其他事务在同一行上执行 <code>SELECT FOR UPDATE</code> 或更新操作时会被阻塞，直到锁被释放。</p><p>在事务中执行更新、删除操作时，MySQL 会自动为相关数据行加锁。</p><h3 id="表级锁和行级锁"><a href="#表级锁和行级锁" class="headerlink" title="表级锁和行级锁"></a>表级锁和行级锁</h3><p><strong>表级锁：</strong> MySQL 中锁定粒度最大的一种锁（全局锁除外），是针对非索引字段加的锁，对当前操作的整张表加锁，实现简单，资源消耗也比较少，加锁快，不会出现死锁。不过，触发锁冲突的概率最高，高并发下效率极低。表级锁和存储引擎无关，MyISAM 和 InnoDB 引擎都支持表级锁。</p><p><strong>行级锁：</strong> MySQL 中锁定粒度最小的一种锁，是 <strong>针对索引字段加的锁</strong> ，只针对当前操作的行记录进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小，并发度高，但加锁的开销也最大，加锁慢，会出现死锁。行级锁和存储引擎有关，是在存储引擎层面实现的。</p><p>InnoDB 的行锁是针对索引字段加的锁，表级锁是针对非索引字段加的锁。当我们执行 <code>UPDATE</code>、<code>DELETE</code> 语句时，如果 <code>WHERE</code>条件中字段没有命中唯一索引或者索引失效的话，就会导致扫描全表对表中的所有行记录进行加锁。这个在我们日常工作开发中经常会遇到，一定要多多注意！！</p><h3 id="行锁有哪些"><a href="#行锁有哪些" class="headerlink" title="行锁有哪些"></a>行锁有哪些</h3><p><strong>记录锁（Record Lock）</strong>：也被称为记录锁，属于单个行记录上的锁。</p><p><strong>间隙锁（Gap Lock）</strong>：锁定一个范围，不包括记录本身。</p><p><strong>临键锁（Next-Key Lock）</strong>：Record Lock+Gap Lock，锁定一个范围，包含记录本身，主要目的是为了解决幻读问题（MySQL 事务部分提到过）。记录锁只能锁住已经存在的记录，为了避免插入新记录，需要依赖间隙锁。</p><p>在 InnoDB 默认的隔离级别 REPEATABLE-READ 下，行锁默认使用的是 Next-Key Lock。但是，如果操作的索引是唯一索引或主键，InnoDB 会对 Next-Key Lock 进行优化，将其降级为 Record Lock，即仅锁住索引本身，而不是范围。</p><h3 id="共享锁和排他锁"><a href="#共享锁和排他锁" class="headerlink" title="共享锁和排他锁"></a>共享锁和排他锁</h3><p>不论是表级锁还是行级锁，都存在共享锁（Share Lock，S 锁）和排他锁（Exclusive Lock，X 锁）这两类：</p><ul><li><strong>共享锁（S 锁）</strong>：又称读锁，事务在读取记录的时候获取共享锁，允许多个事务同时获取（锁兼容）。</li><li><strong>排他锁（X 锁）</strong>：又称写锁/独占锁，事务在修改记录的时候获取排他锁，不允许多个事务同时获取。如果一个记录已经被加了排他锁，那其他事务不能再对这条事务加任何类型的锁（锁不兼容）。</li></ul><table><thead><tr><th align="left"></th><th align="left">S 锁</th><th>X 锁</th></tr></thead><tbody><tr><td align="left">S 锁</td><td align="left">不冲突</td><td>冲突</td></tr><tr><td align="left">X 锁</td><td align="left">冲突</td><td>冲突</td></tr></tbody></table><p>由于 MVCC 的存在，对于一般的 <code>SELECT</code> 语句，InnoDB 不会加任何锁。不过， 你可以通过以下语句显式加共享锁或排他锁。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"># 共享锁 可以在 MySQL <span class="number">5.7</span> 和 MySQL <span class="number">8.0</span> 中使用</span><br><span class="line"><span class="keyword">SELECT</span> ... LOCK <span class="keyword">IN</span> SHARE MODE;</span><br><span class="line"># 共享锁 可以在 MySQL <span class="number">8.0</span> 中使用</span><br><span class="line"><span class="keyword">SELECT</span> ... <span class="keyword">FOR</span> SHARE;</span><br><span class="line"># 排他锁</span><br><span class="line"><span class="keyword">SELECT</span> ... <span class="keyword">FOR</span> <span class="keyword">UPDATE</span>;</span><br></pre></td></tr></table></figure><h3 id="意向锁"><a href="#意向锁" class="headerlink" title="意向锁"></a>意向锁</h3><p>如果需要用到表锁的话，如何判断表中的记录没有行锁呢，一行一行遍历肯定是不行，性能太差。我们需要用到一个叫做意向锁的东东来快速判断是否可以对某个表使用表锁。</p><p>意向锁是表级锁，共有两种：</p><ul><li><strong>意向共享锁（Intention Shared Lock，IS 锁）</strong>：事务有意向对表中的某些记录加共享锁（S 锁），加共享锁前必须先取得该表的 IS 锁。</li><li><strong>意向排他锁（Intention Exclusive Lock，IX 锁）</strong>：事务有意向对表中的某些记录加排他锁（X 锁），加排他锁之前必须先取得该表的 IX 锁。</li></ul><p><strong>意向锁是由数据引擎自己维护的，用户无法手动操作意向锁，在为数据行加共享/排他锁之前，InooDB 会先获取该数据行所在在数据表的对应意向锁。</strong></p><p>意向锁之间是互相兼容的。</p><table><thead><tr><th></th><th>IS 锁</th><th>IX 锁</th></tr></thead><tbody><tr><td>IS 锁</td><td>兼容</td><td>兼容</td></tr><tr><td>IX 锁</td><td>兼容</td><td>兼容</td></tr></tbody></table><p>意向锁和共享锁和排它锁互斥（这里指的是表级别的共享锁和排他锁，意向锁不会与行级的共享锁和排他锁互斥）。</p><table><thead><tr><th></th><th>IS 锁</th><th>IX 锁</th></tr></thead><tbody><tr><td>S 锁</td><td>兼容</td><td>互斥</td></tr><tr><td>X 锁</td><td>互斥</td><td>互斥</td></tr></tbody></table><h3 id="快照读和当前读"><a href="#快照读和当前读" class="headerlink" title="快照读和当前读"></a>快照读和当前读</h3><p><strong>快照读</strong>（一致性非锁定读）就是单纯的 <code>SELECT</code> 语句，但不包括下面这两类 <code>SELECT</code> 语句。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> ... <span class="keyword">FOR</span> <span class="keyword">UPDATE</span></span><br><span class="line"># 共享锁 可以在 MySQL <span class="number">5.7</span> 和 MySQL <span class="number">8.0</span> 中使用</span><br><span class="line"><span class="keyword">SELECT</span> ... LOCK <span class="keyword">IN</span> SHARE MODE;</span><br><span class="line"># 共享锁 可以在 MySQL <span class="number">8.0</span> 中使用</span><br><span class="line"><span class="keyword">SELECT</span> ... <span class="keyword">FOR</span> SHARE;</span><br></pre></td></tr></table></figure><p>快照即记录的历史版本，每行记录可能存在多个历史版本（多版本技术）。</p><p>快照读的情况下，如果读取的记录正在执行 UPDATE/DELETE 操作，读取操作不会因此去等待记录上 X 锁的释放，而是会去读取行的一个快照。</p><p>只有在事务隔离级别 RC(读取已提交) 和 RR（可重读）下，InnoDB 才会使用一致性非锁定读：</p><ul><li>在 RC 级别下，对于快照数据，一致性非锁定读总是读取被锁定行的最新一份快照数据。</li><li>在 RR 级别下，对于快照数据，一致性非锁定读总是读取本事务开始时的行数据版本。</li></ul><p>快照读比较适合对于数据一致性要求不是特别高且追求极致性能的业务场景。</p><p><strong>当前读</strong> （一致性锁定读）就是给行记录加 X 锁或 S 锁。</p><p>当前读的一些常见 SQL 语句类型如下：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"># 对读的记录加一个X锁</span><br><span class="line">SELECT...FOR <span class="keyword">UPDATE</span></span><br><span class="line"># 对读的记录加一个S锁</span><br><span class="line">SELECT...LOCK <span class="keyword">IN</span> SHARE MODE</span><br><span class="line"># 对读的记录加一个S锁</span><br><span class="line">SELECT...FOR SHARE</span><br><span class="line"># 对修改的记录加一个X锁</span><br><span class="line">INSERT...</span><br><span class="line">UPDATE...</span><br><span class="line">DELETE...</span><br></pre></td></tr></table></figure><h3 id="自增锁"><a href="#自增锁" class="headerlink" title="自增锁"></a>自增锁</h3><p>关系型数据库设计表的时候，通常会有一列作为自增主键。InnoDB 中的自增主键会涉及一种比较特殊的表级锁— <strong>自增锁（AUTO-INC Locks）</strong> 。</p><p>更准确点来说，不仅仅是自增主键，<code>AUTO_INCREMENT</code>的列都会涉及到自增锁，毕竟非主键也可以设置自增长。</p><p>如果一个事务正在插入数据到有自增列的表时，会先获取自增锁，拿不到就可能会被阻塞住。这里的阻塞行为只是自增锁行为的其中一种，可以理解为自增锁就是一个接口，其具体的实现有多种。具体的配置项为 <code>innodb_autoinc_lock_mode</code> （MySQL 5.1.22 引入），可以选择的值如下</p><table><thead><tr><th align="left">innodb_autoinc_lock_mode</th><th align="left">介绍</th></tr></thead><tbody><tr><td align="left">0</td><td align="left">传统模式</td></tr><tr><td align="left">1</td><td align="left">连续模式（MySQL 8.0 之前默认）</td></tr><tr><td align="left">2</td><td align="left">交错模式(MySQL 8.0 之后默认)</td></tr></tbody></table><p>交错模式下，所有的“INSERT-LIKE”语句（所有的插入语句，包括：<code>INSERT</code>、<code>REPLACE</code>、<code>INSERT…SELECT</code>、<code>REPLACE…SELECT</code>、<code>LOAD DATA</code>等）都不使用表级锁，使用的是轻量级互斥锁实现，多条插入语句可以并发执行，速度更快，扩展性也更好。</p><p>不过，如果你的 MySQL 数据库有主从同步需求并且 Binlog 存储格式为 Statement 的话，不要将 InnoDB 自增锁模式设置为交叉模式，不然会有数据不一致性问题。这是因为并发情况下插入语句的执行顺序就无法得到保障。</p><h2 id="zookeeper为什么可以实现强一致性的分布式锁"><a href="#zookeeper为什么可以实现强一致性的分布式锁" class="headerlink" title="zookeeper为什么可以实现强一致性的分布式锁"></a>zookeeper为什么可以实现强一致性的分布式锁</h2><p>ZooKeeper 提供了强顺序一致性，这意味着客户端对同一个 ZooKeeper 节点的更新操作是按照顺序执行的，并且所有客户端都能看到相同的更新顺序。通过这种顺序一致性，ZooKeeper 可以确保所有客户端在同一时间看到的数据状态是一致的。</p><p>ZooKeeper 提供了临时顺序节点的功能。客户端在创建一个临时顺序节点时，会获得一个全局唯一且递增的序号，这个节点在客户端会话结束时（比如客户端崩溃或与 ZooKeeper 的连接断开）会自动删除。利用这一特性，可以实现分布式锁。</p><p><strong>避免死锁与竞争</strong></p><ul><li><strong>自动删除节点</strong>：由于使用的是临时顺序节点，当客户端因故障与 ZooKeeper 断开连接时，ZooKeeper 会自动删除该客户端创建的节点，避免锁资源被永久占用，从而防止死锁的发生。</li><li><strong>竞争公平性</strong>：因为所有节点都是按顺序创建的，锁的获取是基于节点的顺序号，先创建的节点先获得锁，这保证了锁的公平性，不会因为客户端的网络延迟等原因导致锁的争夺不公平。</li></ul><p>ZooKeeper 使用的是 <strong>Zab（ZooKeeper Atomic Broadcast）协议</strong> 作为其核心的分布式一致性算法。</p><p>Zab 协议在 ZooKeeper 中主要用于以下两个目的：</p><ul><li><strong>Leader 选举</strong>：在 ZooKeeper 集群中选出一个主节点（Leader），其余节点作为从节点（Followers）。所有写请求都通过 Leader 处理，再由 Leader 将更新广播给 Followers。</li><li><strong>原子广播（Atomic Broadcast）</strong>：确保所有节点以相同的顺序应用相同的更新，保持数据的一致性。</li></ul><h3 id="Zab-协议的核心流程"><a href="#Zab-协议的核心流程" class="headerlink" title="Zab 协议的核心流程"></a><strong>Zab 协议的核心流程</strong></h3><ol><li><strong>Leader 选举</strong>：当 ZooKeeper 集群启动或现任 Leader 出现故障时，Zab 协议会启动 Leader 选举过程。集群中的所有节点会根据各自的投票信息选出一个新的 Leader。选举过程中，节点会根据各自的 <code>ZXID</code>（事务 ID）来决定投票，<code>ZXID</code> 更大的节点具有更高的优先级。</li><li><strong>同步数据</strong>：当新的 Leader 被选出后，Leader 会与 Followers 同步数据，确保所有节点的状态一致。这通常涉及将 Followers 的数据状态更新到最新的事务版本。</li><li><strong>原子广播</strong>：在广播模式下，所有的写请求都被发送到 Leader 处理。Leader 将这些写请求封装成事务提案（Proposal），并将其以原子广播的方式发送给所有 Followers。所有 Followers 都必须对提案进行确认（Ack），一旦 Leader 收到了半数以上节点的确认，它就会将事务提交，并通知所有节点应用该事务。</li><li><strong>保证一致性</strong>：Zab 协议通过严格的顺序更新和多节点确认机制，确保即使在节点崩溃或网络分区等情况下，ZooKeeper 集群仍然能够保证数据的一致性和可用性。</li></ol><h2 id="缓存击穿，穿透，雪崩"><a href="#缓存击穿，穿透，雪崩" class="headerlink" title="缓存击穿，穿透，雪崩"></a>缓存击穿，穿透，雪崩</h2><h3 id="缓存击穿"><a href="#缓存击穿" class="headerlink" title="缓存击穿"></a>缓存击穿</h3><p>缓存击穿中，请求的 key 对应的是 <strong>热点数据</strong> ，该数据 <strong>存在于数据库中，但不存在于缓存中（通常是因为缓存中的那份数据已经过期）</strong> 。这就可能会导致瞬时大量的请求直接打到了数据库上，对数据库造成了巨大的压力，可能直接就被这么多请求弄宕机了。</p><p>举个例子：秒杀进行过程中，缓存中的某个秒杀商品的数据突然过期，这就导致瞬时大量对该商品的请求直接落到数据库上，对数据库造成了巨大的压力。</p><p><strong>永不过期</strong>（不推荐）：设置热点数据永不过期或者过期时间比较长。</p><p><strong>提前预热</strong>（推荐）：针对热点数据提前预热，将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。</p><p><strong>加锁</strong>（看情况）：在缓存失效后，通过设置互斥锁确保只有一个请求去查询数据库并更新缓存。</p><h3 id="缓存穿透"><a href="#缓存穿透" class="headerlink" title="缓存穿透"></a>缓存穿透</h3><p>缓存穿透说简单点就是大量请求的 key 是不合理的，<strong>根本不存在于缓存中，也不存在于数据库中</strong> 。这就导致这些请求直接到了数据库上，根本没有经过缓存这一层，对数据库造成了巨大的压力，可能直接就被这么多请求弄宕机了。</p><p>解决方法如下</p><p><strong>缓存无效 key</strong></p><p>如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间，具体命令如下：<code>SET key value EX 10086</code> 。这种方式可以解决请求的 key 变化不频繁的情况，如果黑客恶意攻击，每次构建不同的请求 key，会导致 Redis 中缓存大量无效的 key 。很明显，这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话，尽量将无效的 key 的过期时间设置短一点比如 1 分钟。</p><p><strong>布隆过滤器</strong></p><p>布隆过滤器是一个非常神奇的数据结构，通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们可以把它看作由二进制向量（或者说位数组）和一系列随机映射函数（哈希函数）两部分组成的数据结构。相比于我们平时常用的 List、Map、Set 等数据结构，它占用空间更少并且效率更高，但是缺点是其返回的结果是概率性的，而不是非常准确的。</p><p>具体是这样做的：把所有可能存在的请求的值都存放在布隆过滤器中，当用户请求过来，先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话，直接返回请求参数错误信息给客户端，存在的话才会走下面的流程。</p><p><strong>接口限流</strong></p><p>根据用户或者 IP 对接口进行限流，对于异常频繁的访问行为，还可以采取黑名单机制，例如将异常 IP 列入黑名单。</p><p>后面提到的缓存击穿和雪崩都可以配合接口限流来解决，毕竟这些问题的关键都是有很多请求落到了数据库上造成数据库压力过大。</p><h3 id="缓存穿透和缓存击穿有什么区别"><a href="#缓存穿透和缓存击穿有什么区别" class="headerlink" title="缓存穿透和缓存击穿有什么区别"></a>缓存穿透和缓存击穿有什么区别</h3><p>缓存穿透中，请求的 key 既不存在于缓存中，也不存在于数据库中。</p><p>缓存击穿中，请求的 key 对应的是 <strong>热点数据</strong> ，该数据 <strong>存在于数据库中，但不存在于缓存中（通常是因为缓存中的那份数据已经过期）</strong> </p><h3 id="缓存雪崩"><a href="#缓存雪崩" class="headerlink" title="缓存雪崩"></a>缓存雪崩</h3><p><strong>缓存在同一时间大面积的失效，导致大量的请求都直接落到了数据库上，对数据库造成了巨大的压力。</strong> 这就好比雪崩一样，摧枯拉朽之势，数据库的压力可想而知，可能直接就被这么多请求弄宕机了。</p><p><strong>有哪些解决办法</strong>？</p><p><strong>针对 Redis 服务不可用的情况：</strong></p><ol><li><strong>Redis 集群</strong>：采用 Redis 集群，避免单机出现问题整个缓存服务都没办法使用。Redis Cluster 和 Redis Sentinel 是两种最常用的 Redis 集群实现方案，详细介绍可以参考：<a href="https://javaguide.cn/database/redis/redis-cluster.html">Redis 集群详解(付费)open in new window</a>。</li><li><strong>多级缓存</strong>：设置多级缓存，例如本地缓存+Redis 缓存的二级缓存组合，当 Redis 缓存出现问题时，还可以从本地缓存中获取到部分数据。</li></ol><p><strong>针对大量缓存同时失效的情况：</strong></p><ol><li><strong>设置随机失效时间</strong>（可选）：为缓存设置随机的失效时间，例如在固定过期时间的基础上加上一个随机值，这样可以避免大量缓存同时到期，从而减少缓存雪崩的风险。</li><li><strong>提前预热</strong>（推荐）：针对热点数据提前预热，将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。</li><li><strong>持久缓存策略</strong>（看情况）：虽然一般不推荐设置缓存永不过期，但对于某些关键性和变化不频繁的数据，可以考虑这种策略。</li></ol><h2 id="聚簇索引与非聚簇索引"><a href="#聚簇索引与非聚簇索引" class="headerlink" title="聚簇索引与非聚簇索引"></a>聚簇索引与非聚簇索引</h2><p>聚簇索引（Clustered Index）即索引结构和数据一起存放的索引，并不是一种单独的索引类型。InnoDB 中的主键索引就属于聚簇索引。</p><p>在 MySQL 中，InnoDB 引擎的表的 <code>.ibd</code>文件就包含了该表的索引和数据，对于 InnoDB 引擎表来说，该表的索引(B+树)的每个非叶子节点存储索引，叶子节点存储索引和索引对应的数据。</p><p><strong>优点</strong>：</p><ul><li><strong>查询速度非常快</strong>：聚簇索引的查询速度非常的快，因为整个 B+树本身就是一颗多叉平衡树，叶子节点也都是有序的，定位到索引的节点，就相当于定位到了数据。相比于非聚簇索引， 聚簇索引少了一次读取数据的 IO 操作。</li><li><strong>对排序查找和范围查找优化</strong>：聚簇索引对于主键的排序查找和范围查找速度非常快。</li></ul><p><strong>缺点</strong>：</p><ul><li><strong>依赖于有序的数据</strong>：因为 B+树是多路平衡树，如果索引的数据不是有序的，那么就需要在插入时排序，如果数据是整型还好，否则类似于字符串或 UUID 这种又长又难比较的数据，插入或查找的速度肯定比较慢。</li><li><strong>更新代价大</strong>：如果对索引列的数据被修改时，那么对应的索引也将会被修改，而且聚簇索引的叶子节点还存放着数据，修改代价肯定是较大的，所以对于主键索引来说，主键一般都是不可被修改的。</li></ul><p>非聚簇索引(Non-Clustered Index)即索引结构和数据分开存放的索引，并不是一种单独的索引类型。二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎，不管主键还是非主键，使用的都是非聚簇索引。</p><p>非聚簇索引的叶子节点并不一定存放数据的指针，因为二级索引的叶子节点就存放的是主键，根据主键再回表查数据。</p><p><strong>优点</strong>：</p><p>更新代价比聚簇索引要小 。非聚簇索引的更新代价就没有聚簇索引那么大了，非聚簇索引的叶子节点是不存放数据的。</p><p><strong>缺点</strong>：</p><ul><li><strong>依赖于有序的数据</strong>:跟聚簇索引一样，非聚簇索引也依赖于有序的数据</li><li><strong>可能会二次查询(回表)</strong>:这应该是非聚簇索引最大的缺点了。 当查到索引对应的指针或主键后，可能还需要根据指针或主键再到数据文件或表中查询。</li></ul><p><strong>非聚簇索引不一定回表查询。</strong></p><p>用户准备使用 SQL 查询用户名，而用户名字段正好建立了索引。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> name <span class="keyword">FROM</span> <span class="keyword">table</span> <span class="keyword">WHERE</span> name<span class="operator">=</span><span class="string">&#x27;guang19&#x27;</span>;</span><br></pre></td></tr></table></figure><p>那么这个索引的 key 本身就是 name，查到对应的 name 直接返回就行了，无需回表查询。</p><h2 id="什么是回表"><a href="#什么是回表" class="headerlink" title="什么是回表"></a>什么是回表</h2><p>它描述了在使用非聚簇索引（或辅助索引）进行查询时，需要再通过主键索引（或聚簇索引）去获取完整的行数据的过程。</p><h2 id="redis键过长会有什么影响？"><a href="#redis键过长会有什么影响？" class="headerlink" title="redis键过长会有什么影响？"></a>redis键过长会有什么影响？</h2><p>懵了一下，故意刁难人吧，哪个正常人会把key弄得很长？</p><ol><li><p>内存占用增加</p></li><li><p>网络带宽消耗增加</p></li><li><p>查询性能下降。在 Redis 中，键的比较是通过逐字节比较的方式进行的，因此键越长，比较操作的时间就越长。在高并发和大量键的情况下，这种额外的比较时间会影响查询性能。</p></li><li><p>持久化和数据加载时间增加</p></li><li><p>键空间效率低。Redis 使用的是字典数据结构（hash table）来存储键值对，过长的键可能导致 hash 冲突的概率增加，进而影响 Redis 的哈希性能。</p></li></ol><h2 id="redis中hash结构有哪些使用场景"><a href="#redis中hash结构有哪些使用场景" class="headerlink" title="redis中hash结构有哪些使用场景"></a>redis中hash结构有哪些使用场景</h2><ol><li>存储用户信息。用户的基本信息（如用户名、邮箱、年龄等）可以通过哈希结构存储在一个键中:</li></ol><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">user<span class="punctuation">:</span><span class="number">1001</span> -&gt; <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Alice&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;email&quot;</span><span class="punctuation">:</span> <span class="string">&quot;alice@example.com&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;age&quot;</span><span class="punctuation">:</span> <span class="string">&quot;30&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><ol start="2"><li>会话管理。例如存储session。</li></ol><h2 id="SpringCloud组件有哪些"><a href="#SpringCloud组件有哪些" class="headerlink" title="SpringCloud组件有哪些"></a>SpringCloud组件有哪些</h2><ul><li>Spring Cloud Config</li><li>Spring Cloud Netflix：Spring Cloud Netflix是对Netflix开发的一套分布式服务框架的封装，包括服务的发现和注册，负载均衡、断路器、REST客户端、请求路由等。它是Spring Cloud的一部分。包括 Eureka、Ribbon、Hystrix、Zuul、Gateway。</li><li>Spring Cloud Gateway</li><li>Spring Cloud OpenFeign</li><li>Spring Cloud Sleuth： 分布式追踪工具，用于在微服务架构中跟踪请求的流转路径，支持与 Zipkin、Jaeger 等分布式追踪系统集成。</li><li>Spring Cloud Bus：用于将消息总线连接到多个分布式系统的节点，通常与 Spring Cloud Config 结合使用，实现配置的动态刷新。</li></ul><p>国内常用的SpringCloudAlibaba组件有：</p><ul><li>Nacos: 提供服务发现、配置管理、动态 DNS 和服务健康检查的功能。Nacos 是 Spring Cloud Alibaba 中的核心组件，用于替代 Spring Cloud Netflix 的 Eureka 和 Spring Cloud Config。</li><li>Sentinel: 提供流量控制、熔断降级、系统负载保护等功能。Sentinel 是一个高可用保护的流量管理工具，能够对微服务进行全面的保护。</li><li>RocketMQ: 分布式消息中间件，支持高吞吐量、低延迟和高可用的消息传递。Spring Cloud Alibaba 提供了对 RocketMQ 的集成，方便微服务之间的消息通信。</li><li>Dubbo: 高性能的 RPC 框架，支持微服务间的远程调用。Dubbo 提供了服务治理、服务监控等功能，并与 Spring Cloud 集成，实现微服务的无缝对接。</li><li>Seata: 分布式事务管理框架，支持微服务架构中的分布式事务，能够在多服务、多数据库的场景下保证事务的最终一致性。</li></ul><p>在阿里云上商用的组件还有：</p><ul><li>Alibaba Cloud OSS: 阿里云的对象存储服务，提供大规模、高可用的云存储解决方案。Spring Cloud Alibaba 提供了与 OSS 的集成，使开发者能够方便地在微服务中使用对象存储。</li><li>Alibaba Cloud SMS: 阿里云的短信服务，支持通过 API 发送短信验证码、通知等。Spring Cloud Alibaba 集成了短信服务，方便在微服务中使用短信功能。</li><li>Alibaba Cloud ACM (Application Configuration Management): 应用配置管理服务，类似于 Nacos 的配置管理功能，但更加侧重于企业级应用的配置管理。Spring Cloud Alibaba 集成了 ACM，提供了配置管理的高级功能。</li><li>Alibaba Cloud SchedulerX: 分布式任务调度平台，支持高可用、高并发的任务调度。SchedulerX 可以用于定时任务、分布式任务和调度任务的管理。</li><li>Alibaba Cloud MNS (Message Notification Service): 消息通知服务，支持大规模的异步消息通知。Spring Cloud Alibaba 提供了对 MNS 的集成，使开发者能够轻松实现消息通知功能。</li><li>Alibaba Cloud ARMS (Application Real-Time Monitoring Service): 应用实时监控服务，提供了对分布式系统的全方位监控，包括性能监控、日志分析、报警等。Spring Cloud Alibaba 集成了 ARMS，支持对微服务的实时监控。</li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;bin-log-redo-log-undo-log&quot;&gt;&lt;a href=&quot;#bin-log-redo-log-undo-log&quot; class=&quot;headerlink&quot; title=&quot;bin log, redo log, undo log&quot;&gt;&lt;/a&gt;bin log, 
      
    
    </summary>
    
    
      <category term="编程" scheme="https://blog.zer0e.com/tags/%E7%BC%96%E7%A8%8B/"/>
    
      <category term="随想" scheme="https://blog.zer0e.com/tags/%E9%9A%8F%E6%83%B3/"/>
    
      <category term="面经" scheme="https://blog.zer0e.com/tags/%E9%9D%A2%E7%BB%8F/"/>
    
  </entry>
  
  <entry>
    <title>2024面试复盘11</title>
    <link href="https://blog.zer0e.com/2024/08/12/2024-08-12-replay/"/>
    <id>https://blog.zer0e.com/2024/08/12/2024-08-12-replay/</id>
    <published>2024-08-12T12:00:00.000Z</published>
    <updated>2024-08-14T11:08:22.733Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>一周没写复盘了。上周主要是滴滴的一面，一个小公司的一面，还有个网易的线下面试。网易那个其实都在问简历的东西，方向不是很匹配，大概是凉了。今天是小公司的二面，可能面试官觉得之前做的东西不是业务开发，所以感觉机会也小。这次把两次面试一起复盘了。</p><h1 id="复盘"><a href="#复盘" class="headerlink" title="复盘"></a>复盘</h1><h2 id="TCP和UDP"><a href="#TCP和UDP" class="headerlink" title="TCP和UDP"></a>TCP和UDP</h2><table><thead><tr><th>TCP</th><th>UDP</th><th></th></tr></thead><tbody><tr><td>是否面向连接</td><td>是</td><td>否</td></tr><tr><td>是否可靠</td><td>是</td><td>否</td></tr><tr><td>是否有状态</td><td>是</td><td>否</td></tr><tr><td>传输效率</td><td>较慢</td><td>较快</td></tr><tr><td>传输形式</td><td>字节流</td><td>数据报文段</td></tr><tr><td>首部开销</td><td>20 ～ 60 bytes</td><td>8 bytes</td></tr><tr><td>是否提供广播或多播服务</td><td>否</td><td>是</td></tr></tbody></table><p><strong>UDP 一般用于即时通信</strong>，比如：语音、 视频、直播等等。这些场景对传输数据的准确性要求不是特别高，比如你看视频即使少个一两帧，实际给人的感觉区别也不大。</p><p><strong>TCP 用于对传输准确性要求特别高的场景</strong>，比如文件传输、发送和接收邮件、远程登录等等。</p><h2 id="ES作用？为什么不用mysql？mysql模糊查询？"><a href="#ES作用？为什么不用mysql？mysql模糊查询？" class="headerlink" title="ES作用？为什么不用mysql？mysql模糊查询？"></a>ES作用？为什么不用mysql？mysql模糊查询？</h2><p>ElasticSearch 是一个开源的 分布式、RESTful 搜索和分析引擎，可以用来解决使用数据库进行模糊搜索时存在的性能问题，适用于所有类型的数据，包括文本、数字、地理空间、结构化和非结构化数据。</p><p>Elasticsearch 使用倒排索引来支持快速的全文搜索和复杂的查询操作。这使得 ES 能够高效地处理大量的非结构化数据（如日志）。ES 能够对多个字段进行模糊搜索、词语匹配、词干提取等复杂查询，并且性能优越。</p><p>MySQL 的主要索引结构是 B-Tree 索引，适合处理结构化数据和简单查询。但对于全文搜索、复杂查询和模糊匹配，性能较差。</p><p>虽然 MySQL 也支持全文索引（如 InnoDB 和 MyISAM 引擎），但其搜索功能和性能不如 Elasticsearch 强大，特别是在处理大量日志数据时。</p><p>mysql模糊查询有三种情况：</p><p>前缀匹配 (<code>LIKE &#39;keyword%&#39;</code>)：查找以 <code>keyword</code> 开头的记录，这种情况下如果 <code>column_name</code> 有索引，MySQL 可以有效利用索引。</p><p>后缀匹配 (<code>LIKE &#39;%keyword&#39;</code>)：查找以 <code>keyword</code> 结尾的记录，通常索引无法生效。</p><p>全匹配 (<code>LIKE &#39;%keyword%&#39;</code>)：查找包含 <code>keyword</code> 的记录，索引也通常无法生效。</p><p>对于 MySQL 的 InnoDB 或 MyISAM 引擎，如果要进行更高效的模糊查询，尤其是针对大文本数据，可以使用全文索引。全文索引适用于自然语言文本的查找，但它主要适用于针对整词的搜索，而不是部分匹配。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> my_table <span class="keyword">WHERE</span> <span class="keyword">MATCH</span>(column_name) AGAINST(<span class="string">&#x27;keyword&#x27;</span>);</span><br></pre></td></tr></table></figure><h2 id="字典树，KMP，AC自动机"><a href="#字典树，KMP，AC自动机" class="headerlink" title="字典树，KMP，AC自动机"></a>字典树，KMP，AC自动机</h2><p>聊到mysql搜索的时候面试官突然说到这个，好久了，这里复习一下。</p><p>字典树（Trie），也称为前缀树或单词查找树，是一种用于存储字符串集的数据结构，特别适合处理字符串前缀相关的问题。字典树的结构类似于一棵多叉树，每个节点代表字符串中的一个字符，通过节点之间的链接表示字符串的前缀关系。</p><h3 id="字典树的基本特性"><a href="#字典树的基本特性" class="headerlink" title="字典树的基本特性"></a>字典树的基本特性</h3><ol><li><strong>根节点不包含字符</strong>，它仅用于表示一个空字符串。</li><li><strong>每个节点都表示一个字符</strong>，通过路径从根节点到某个节点，可以组成一个字符串。</li><li><strong>每个节点的所有子节点表示不同的字符</strong>，即所有子节点不重复。</li><li><strong>路径上的每个节点都对应一个前缀</strong>，从根节点到某个节点的路径表示一个字符串的前缀或完整字符串。</li></ol><p>假设我们有一组单词集：<code>[&quot;cat&quot;, &quot;cap&quot;, &quot;bat&quot;, &quot;bad&quot;]</code>，它的字典树可能是这样的：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">     root</span><br><span class="line">     /  \</span><br><span class="line">    c    b</span><br><span class="line">   / \   / \</span><br><span class="line">  a   a a   a</span><br><span class="line"> /   /   \   \</span><br><span class="line">t   p     d   t</span><br></pre></td></tr></table></figure><h3 id="字典树的优缺点"><a href="#字典树的优缺点" class="headerlink" title="字典树的优缺点"></a>字典树的优缺点</h3><ul><li><strong>优点</strong>：<ul><li><strong>高效的前缀查找</strong>：对于前缀匹配和字符串查找，字典树可以在 O(m) 时间复杂度内完成操作，其中 m 是字符串的长度。</li><li><strong>结构清晰</strong>：字典树的结构非常直观，特别适合处理字符串。</li></ul></li><li><strong>缺点</strong>：<ul><li><strong>空间消耗大</strong>：由于每个节点都需要存储多个子节点的引用，字典树的空间消耗较大，尤其在字符集较大的情况下。</li><li><strong>不适合处理变长字符串</strong>：对于非常长的字符串，字典树的深度会很大，导致操作效率下降。</li></ul></li></ul><h3 id="KMP"><a href="#KMP" class="headerlink" title="KMP"></a>KMP</h3><p>Knuth-Morris-Pratt 算法，用于在一个字符串中快速查找另一个字符串（即模式匹配问题）。查找单个模式的时间复杂度是 O(n)，其中 n 是主字符串的长度。</p><p><strong>KMP算法</strong>：主要用于单个模式字符串在一个主字符串中的查找，适用于模式匹配问题。</p><p><strong>字典树</strong>：用于存储和快速查询多个字符串，特别适合处理前缀查询、多模式匹配、自动补全等问题。</p><h3 id="AC自动机"><a href="#AC自动机" class="headerlink" title="AC自动机"></a>AC自动机</h3><p>AC自动机，全称 <strong>Aho-Corasick 自动机</strong>，是一种多模式字符串匹配算法。它结合了 <strong>字典树（Trie）</strong> 和 <strong>KMP算法</strong> 的思想，用于在一个文本中同时查找多个模式字符串。AC自动机在网络安全、文本检索、DNA序列分析等领域有广泛应用。</p><p>AC自动机的构建和使用分为以下几个步骤：</p><ol><li>构建字典树（Trie）把所有模式字符串插入到字典树中，构建一棵多叉树。</li><li>构建失败指针（Failure Pointer）在字典树的基础上，构建每个节点的失败指针。失败指针指向当前匹配失败时，应该转移到的下一个状态（即节点）。如果某个状态下的字符匹配失败，则通过失败指针跳转到另一个状态继续匹配，直到匹配成功或回到根节点。</li><li>匹配过程。在文本中进行匹配时，从根节点开始，逐个字符遍历文本。如果当前字符匹配成功，则进入下一个状态。如果匹配失败，则通过失败指针进行状态转移，继续匹配。</li></ol><p>说实话这次面试官是个算法工程师，所以对算法比较在行。说实话我对算法不是很熟悉。</p><h2 id="HTTP和RPC"><a href="#HTTP和RPC" class="headerlink" title="HTTP和RPC"></a>HTTP和RPC</h2><p>HTTP （超文本传输协议）是一种<strong>应用层协议</strong>。</p><p><strong>使用场景</strong>：</p><ul><li><strong>Web 浏览器与服务器之间的通信</strong>：加载网页、提交表单、下载文件等。</li><li><strong>RESTful API</strong>：使用 HTTP 协议进行跨平台、跨语言的服务调用。</li></ul><p>RPC 是一种<strong>分布式计算协议</strong>，它使得一个程序可以像调用本地函数一样，调用位于远程服务器上的函数或过程。RPC 的目的是隐藏网络通信的细节，让开发者可以专注于业务逻辑。</p><table><thead><tr><th>特性</th><th>HTTP</th><th>RPC</th></tr></thead><tbody><tr><td><strong>层次</strong></td><td>应用层协议</td><td>分布式计算协议</td></tr><tr><td><strong>通信模型</strong></td><td>请求-响应</td><td>请求-响应（远程调用）</td></tr><tr><td><strong>数据格式</strong></td><td>通常为纯文本（如 JSON、HTML）</td><td>通常为二进制或自定义协议格式</td></tr><tr><td><strong>状态性</strong></td><td>无状态</td><td>通常有状态</td></tr><tr><td><strong>透明性</strong></td><td>明显的请求和响应分离</td><td>远程调用看起来像本地调用</td></tr><tr><td><strong>适用场景</strong></td><td>Web 开发、API 调用</td><td>微服务通信、分布式系统</td></tr></tbody></table><p>HTTP 更适合于 Web 应用和简单的 API 调用，而 RPC 更适用于复杂的分布式系统和微服务架构。</p><h2 id="redis作用"><a href="#redis作用" class="headerlink" title="redis作用"></a>redis作用</h2><p>这个不再多说了。之前复习过好几次了。</p><p>其他问题都是项目介绍，就不展开了。</p><h2 id="MongoDB用过吗？存什么数据？"><a href="#MongoDB用过吗？存什么数据？" class="headerlink" title="MongoDB用过吗？存什么数据？"></a>MongoDB用过吗？存什么数据？</h2><p>MongoDB 是一个基于 <strong>分布式文件存储</strong> 的开源 NoSQL 数据库系统，由 <strong>C++</strong> 编写的。MongoDB 提供了 <strong>面向文档</strong> 的存储方式，操作起来比较简单和容易，支持“<strong>无模式</strong>”的数据建模，可以存储比较复杂的数据类型，是一款非常流行的 <strong>文档类型数据库</strong> 。</p><h3 id="适合存储的数据类型"><a href="#适合存储的数据类型" class="headerlink" title="适合存储的数据类型"></a><strong>适合存储的数据类型</strong></h3><ul><li><strong>非结构化数据</strong>：MongoDB 使用 BSON（Binary JSON）格式来存储数据，能够处理复杂的嵌套结构和多样化的数据类型，包括文档、数组、嵌套对象等。</li><li><strong>动态模式的数据</strong>：对于数据模型经常变化的应用，MongoDB 允许文档之间的字段不一致，使得模式可以灵活调整。</li><li><strong>大规模的数据</strong>：MongoDB 支持水平扩展（sharding），能够处理大规模的数据存储需求。</li></ul><p>例如存储和管理不同类型的内容（如文章、评论、用户信息）以及内容的元数据。</p><p><strong>灵活的数据模型</strong>：不同的内容可能有不同的字段和结构，如文章有标题、正文、作者等字段，评论有评论者、内容、时间等字段。MongoDB 的文档模型允许每个内容类型有不同的结构。</p><p><strong>嵌套文档</strong>：可以在一个文档中嵌套评论，避免了复杂的联接操作。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;_id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;article_123&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;title&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Introduction to MongoDB&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;author&quot;</span><span class="punctuation">:</span> <span class="string">&quot;John Doe&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;content&quot;</span><span class="punctuation">:</span> <span class="string">&quot;MongoDB is a NoSQL database...&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;comments&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;user&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Alice&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;comment&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Great article!&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;date&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2024-08-10&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;user&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Bob&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;comment&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Very informative.&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;date&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2024-08-11&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>又比如存储和处理来自大量设备的数据，如传感器数据、设备状态和日志。</p><p><strong>高吞吐量和可扩展性</strong>：支持大规模数据写入，适合物联网场景中的实时数据采集。</p><p><strong>时间序列数据</strong>：能够处理时间序列数据，支持高效的插入和查询操作。</p><h2 id="mysql数据结构"><a href="#mysql数据结构" class="headerlink" title="mysql数据结构"></a>mysql数据结构</h2><p>BTree 索引：MySQL 里默认和最常用的索引类型。只有叶子节点存储 value，非叶子节点只有指针和 key。存储引擎 MyISAM 和 InnoDB 实现 BTree 索引都是使用 B+Tree，但二者实现方式不一样（前面已经介绍了）。</p><p>哈希索引：类似键值对的形式，一次即可定位。</p><p>RTree 索引：一般不会使用，仅支持 geometry 数据类型，优势在于范围查找，效率较低，通常使用搜索引擎如 ElasticSearch 代替。</p><p>全文索引：对文本的内容进行分词，进行搜索。目前只有 <code>CHAR</code>、<code>VARCHAR</code> ，<code>TEXT</code> 列上可以创建全文索引。一般不会使用，效率较低，通常使用搜索引擎如 ElasticSearch 代替。</p><h3 id="B-树-amp-B-树两者有何异同呢？"><a href="#B-树-amp-B-树两者有何异同呢？" class="headerlink" title="B 树&amp; B+树两者有何异同呢？"></a><strong>B 树&amp; B+树两者有何异同呢？</strong></h3><ul><li>B 树的所有节点既存放键(key) 也存放数据(data)，而 B+树只有叶子节点存放 key 和 data，其他内节点只存放 key。</li><li>B 树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点。</li><li>B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找，可能还没有到达叶子节点，检索就结束了。而 B+树的检索效率就很稳定了，任何查找都是从根节点到叶子节点的过程，叶子节点的顺序检索很明显。</li><li>在 B 树中进行范围查询时，首先找到要查找的下限，然后对 B 树进行中序遍历，直到找到查找的上限；而 B+树的范围查询，只需要对链表进行遍历即可。</li></ul><p>B+树与 B 树相比，具备更少的 IO 次数、更稳定的查询效率和更适于范围查询这些优势。</p><h3 id="B-树为什么快"><a href="#B-树为什么快" class="headerlink" title="B+树为什么快"></a>B+树为什么快</h3><ol><li><strong>树的高度较低</strong>。B+树是一种自平衡的多路查找树，所有的叶子节点都在同一层。这意味着从根节点到任一叶子节点的路径长度（即树的高度）是相同的。</li><li><strong>顺序存储叶子节点</strong>。B+树的所有数据都存储在叶子节点中，且所有叶子节点通过指针连接在一起形成一个有序链表。当需要进行范围查询（如查找某个区间内的所有数据）时，B+树可以从找到的起始叶子节点开始，顺序扫描后续的叶子节点，大大提高了查询效率。</li><li><strong>分裂与合并减少磁盘I/O</strong>。在B+树中，节点分裂和合并的操作次数相对较少，因为每个节点可以包含多个元素。当插入或删除元素时，不会频繁触发树的重平衡操作，这减少了磁盘I/O操作的频率。由于树的高度较低，搜索过程中的磁盘I/O次数较少，而磁盘I/O通常是数据库系统中的瓶颈，因此 B+ 树能够显著提高查找速度。</li><li><strong>所有键值在叶子节点上</strong>。在 B+ 树中，所有实际数据（键值对）都存储在叶子节点上，中间节点只存储键值用于指引搜索路径。这意味着每次查找最终都会访问到叶子节点，避免了不必要的键值访问，简化了搜索路径。</li></ol><h3 id="前缀查找与全文检索"><a href="#前缀查找与全文检索" class="headerlink" title="前缀查找与全文检索"></a>前缀查找与全文检索</h3><p><strong>前缀查找</strong>和<strong>全文检索</strong>是两种不同的查询方式。前缀查找通常是基于<strong>B+树索引</strong>（如 BTREE 索引）的查询方式，通过匹配字符串的前缀来进行快速定位。例如，<code>WHERE column LIKE &#39;abc%&#39;</code> 这样的查询就是典型的前缀查找。</p><p>全文检索（Full-Text Search）是一种基于全文索引的查询方式，专门用于查找大文本中的关键词。MySQL 提供的 FULLTEXT 索引可以支持自然语言模式和布尔模式下的全文检索。</p><p>前缀查找通常比全文检索快，因为它只需在索引中定位前缀即可，而全文检索需要更复杂的计算和匹配操作。不过，这也取决于数据的规模和具体的查询条件。</p><p>全文检索更灵活，可以查找文本中的任意位置，而前缀查找只能匹配字符串的开头部分。</p><p>如果你只需要匹配字符串的前缀（如用户名查找），前缀查找更合适且更快；但如果你需要在大文本中查找关键词（如文章搜索），全文检索则更适合，尽管速度可能略慢。</p><h2 id="mysql调优"><a href="#mysql调优" class="headerlink" title="mysql调优"></a>mysql调优</h2><p>这个其实需要具体问题具体分析，前面也有一些复盘提过这个。</p><h2 id="Redis线程模型"><a href="#Redis线程模型" class="headerlink" title="Redis线程模型"></a>Redis线程模型</h2><p>对于读写命令来说，Redis 一直是单线程模型。不过，在 Redis 4.0 版本之后引入了多线程来执行一些大键值对的异步删除操作， Redis 6.0 版本之后引入了多线程来处理网络请求（提高网络 IO 读写性能）。</p><p><strong>Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型</strong> （Netty 的线程模型也基于 Reactor 模式，Reactor 模式不愧是高性能 IO 的基石），这套事件处理模型对应的是 Redis 中的文件事件处理器（file event handler）。由于文件事件处理器（file event handler）是单线程方式运行的，所以我们一般都说 Redis 是单线程模型。</p><p><strong>既然是单线程，那怎么监听大量的客户端连接呢？</strong></p><p>Redis 通过 <strong>IO 多路复用程序</strong> 来监听来自客户端的大量连接（或者说是监听多个 socket），它会将感兴趣的事件及类型（读、写）注册到内核中并监听每个事件是否发生。</p><p>这样的好处非常明显：<strong>I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接，降低了资源的消耗</strong>（和 NIO 中的 <code>Selector</code> 组件很像）。</p><p>虽然说 Redis 是单线程模型，但实际上，<strong>Redis 在 4.0 之后的版本中就已经加入了对多线程的支持。</strong></p><p>不过，Redis 4.0 增加的多线程主要是针对一些大键值对的删除操作的命令，使用这些命令就会使用主线程之外的其他线程来“异步处理”，从而减少对主线程的影响。</p><p>为此，Redis 4.0 之后新增了几个异步命令：</p><ul><li><code>UNLINK</code>：可以看作是 <code>DEL</code> 命令的异步版本。</li><li><code>FLUSHALL ASYNC</code>：用于清空所有数据库的所有键，不限于当前 <code>SELECT</code> 的数据库。</li><li><code>FLUSHDB ASYNC</code>：用于清空当前 <code>SELECT</code> 数据库中的所有键。</li></ul><p><strong>Redis6.0 引入多线程主要是为了提高网络 IO 读写性能</strong>，因为这个算是 Redis 中的一个性能瓶颈（Redis 的瓶颈主要受限于内存和网络）。</p><p>虽然，Redis6.0 引入了多线程，但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了，执行命令仍然是单线程顺序执行。</p><h2 id="Redis如何做主从同步"><a href="#Redis如何做主从同步" class="headerlink" title="Redis如何做主从同步"></a>Redis如何做主从同步</h2><ol><li>当从节点启动并配置为从主节点复制数据时，从节点会向主节点发送 <code>PSYNC</code> 命令，表示希望与主节点同步。</li><li>如果从节点是第一次与主节点同步，主节点会执行全量同步。如果从节点曾经同步过，但与主节点的连接中断了一段时间，主节点会尝试进行增量同步（如果从节点的复制偏移量与主节点匹配）。</li><li>主节点生成当前内存数据的快照（RDB 文件），并将该快照发送给从节点。从节点接收快照文件并将其加载到内存中，完成数据同步。在快照传输的同时，主节点会继续记录新的写操作，并在快照传输完成后将这些增量操作也发送给从节点。</li><li>当主节点判断从节点可以进行增量同步时，主节点只将从节点缺失的那部分命令发送给从节点。从节点执行这些命令，从而使自身的数据与主节点保持一致。</li><li>从节点与主节点保持长连接，主节点会实时将所有写命令传播给从节点，使得从节点的数据保持与主节点同步。</li></ol><h2 id="mq的选型"><a href="#mq的选型" class="headerlink" title="mq的选型"></a>mq的选型</h2><p>首先要明确业务需求，包括但不限于以下几点：</p><p>消息的吞吐量：预估系统每秒钟需要处理的消息数量。<br>消息的延迟要求：消息传递的实时性要求如何，是否可以容忍延迟。<br>消息持久化：是否需要消息的持久化存储，以便系统故障时能够恢复。<br>消息的顺序性：消息是否需要保证严格的顺序性。<br>消息的可靠性：是否需要确保消息不丢失、不重复。<br>可扩展性：系统未来可能需要的扩展能力。<br>与现有技术栈的兼容性：是否与现有的技术栈兼容，或者团队是否擅长使用。</p><h3 id="Apache-Kafka"><a href="#Apache-Kafka" class="headerlink" title="Apache Kafka"></a>Apache Kafka</h3><p><strong>特点</strong>：</p><ul><li>高吞吐量，支持大规模数据传输。</li><li>适合处理流式数据，如日志、事件数据的实时处理。</li><li>数据持久化存储，支持消息重放。</li><li>强大的分区机制，支持消息的水平扩展。</li><li>有时会存在消息延迟，尤其是在高吞吐场景下。</li></ul><p><strong>适用场景</strong>：</p><ul><li>实时数据处理、日志收集和分析、事件驱动架构、流处理等。</li><li>大数据相关的应用场景。</li></ul><h3 id="RabbitMQ"><a href="#RabbitMQ" class="headerlink" title="RabbitMQ"></a>RabbitMQ</h3><p><strong>特点</strong>：</p><ul><li>支持丰富的消息路由规则（如发布/订阅、点对点、路由键）。</li><li>高可靠性，支持消息确认机制、持久化、镜像队列等。</li><li>支持复杂的消息路由和协议（如 AMQP、STOMP、MQTT）。</li><li>吞吐量较 Kafka 低，但延迟较低。</li></ul><p><strong>适用场景</strong>：</p><ul><li>金融系统、订单处理、需要复杂路由逻辑的场景。</li><li>高可靠性、高一致性要求的应用。</li></ul><h2 id="kafka如何实现负载均衡"><a href="#kafka如何实现负载均衡" class="headerlink" title="kafka如何实现负载均衡"></a>kafka如何实现负载均衡</h2><p>这个我可能理解错了，我以为说的是部署如何实现负载均衡。其实应该是使用上的消息负载均衡。</p><h3 id="主题（Topic）与分区（Partition）"><a href="#主题（Topic）与分区（Partition）" class="headerlink" title="主题（Topic）与分区（Partition）"></a><strong>主题（Topic）与分区（Partition）</strong></h3><ul><li><strong>分区的概念</strong>：Kafka 将每个主题（Topic）分成多个分区（Partition），每个分区都是一个有序的消息队列。消息会被分发到不同的分区中，每个分区只能被一个消费者组中的一个消费者消费。</li><li><strong>负载均衡的基础</strong>：分区是实现负载均衡的基本单位。Kafka 通过将消息分配到不同的分区来实现消息的分布，而消费者组内的消费者则通过消费不同的分区来实现负载均衡。</li></ul><h3 id="生产者端的负载均衡"><a href="#生产者端的负载均衡" class="headerlink" title="生产者端的负载均衡"></a><strong>生产者端的负载均衡</strong></h3><ul><li>分区选择策略：生产者在发送消息时，需要决定消息发送到哪个分区。Kafka 提供了几种分区选择策略：<ul><li><strong>轮询策略（Round-Robin）</strong>：消息被轮询地发送到每一个分区。这种方式简单而均匀。</li><li><strong>基于键的分区策略</strong>：如果消息携带了一个键，Kafka 会根据这个键的哈希值来选择分区。这样可以保证相同键的消息总是被发送到相同的分区，实现消息的有序性。</li><li><strong>自定义分区器</strong>：用户可以实现自己的分区器逻辑，控制消息如何分布到不同的分区。</li></ul></li></ul><h3 id="消费者端的负载均衡"><a href="#消费者端的负载均衡" class="headerlink" title="消费者端的负载均衡"></a><strong>消费者端的负载均衡</strong></h3><ul><li><strong>消费者组</strong>：Kafka 通过消费者组实现消费负载的自动均衡。每个消费者组由多个消费者实例组成，每个消费者实例负责消费一部分分区。<ul><li><strong>消费者组的分区分配</strong>：Kafka 会自动将分区分配给消费者组中的消费者。默认的分配策略是均匀分配（Range Assignor）或轮询分配（RoundRobin Assignor）。如果消费者的数量发生变化（如新消费者加入或消费者退出），Kafka 会重新平衡分区分配，以确保每个消费者分配到的分区数量大致相等。</li></ul></li><li><strong>再平衡（Rebalance）</strong>：当消费者组中的消费者发生变化时（如新增消费者、消费者故障退出等），Kafka 会触发再平衡操作。再平衡过程会重新分配分区，以确保新的消费者组中的每个消费者都能公平地消费分区。再平衡过程中，消费者可能会暂停消费一段时间，直到新的分配完成。</li></ul><h2 id="kafka场景题"><a href="#kafka场景题" class="headerlink" title="kafka场景题"></a>kafka场景题</h2><p>同一个topic，两个不同的消费者组，一个消费到5，第二个消费者组没有消费者，此时第二个消费者组进来一个消费者，此时应该消费什么？</p><p>没遇到过这种场景，蒙了一下，还蒙错了。不同消费者组其实是相互独立的。</p><h3 id="不同消费者组的消费行为"><a href="#不同消费者组的消费行为" class="headerlink" title="不同消费者组的消费行为"></a><strong>不同消费者组的消费行为</strong></h3><ul><li><strong>相互独立</strong>：不同的消费者组是相互独立的，每个消费者组都会消费主题中的所有消息。也就是说，Kafka 会为每个消费者组维护独立的消费偏移量（offset），每个消费者组会从自己的偏移量开始消费消息。</li><li><strong>重复消费</strong>：由于不同的消费者组独立消费消息，每个消费者组中的消费者实例都会消费主题中的全部消息。因此，同一个主题的不同消费者组会<strong>重复消费</strong>这些消息。</li></ul><p>假设有一个主题 <code>TopicA</code>，它有 2 个分区，存在两个消费者组 <code>Group1</code> 和 <code>Group2</code>，每个消费者组中各有 2 个消费者实例。</p><ul><li><strong>Group1</strong> 的两个消费者实例分别消费 <code>TopicA</code> 的两个分区中的消息。</li><li><strong>Group2</strong> 的两个消费者实例也会分别消费 <code>TopicA</code> 的两个分区中的消息。</li></ul><h2 id="算法1：topK问题"><a href="#算法1：topK问题" class="headerlink" title="算法1：topK问题"></a>算法1：topK问题</h2><p>之前写过文章。就回答了三种算法。</p><h3 id="排序"><a href="#排序" class="headerlink" title="排序"></a>排序</h3><p>快排时间复杂度O(nlogn)</p><h3 id="堆"><a href="#堆" class="headerlink" title="堆"></a>堆</h3><p>使用一个大小为 K 的最小堆来维护当前遇到的前 K 个最大元素。</p><p>时间复杂度O(N log K)。其中 <code>N</code> 是元素的总数，<code>K</code> 是需要找出的最大元素数量。</p><h3 id="快速选择法"><a href="#快速选择法" class="headerlink" title="快速选择法"></a>快速选择法</h3><p>基于快速排序的思想，使用分治法找到前 K 个元素。使用快速排序的分区（partition）方法，每次将集合划分成两部分，一部分比基准元素小，另一部分比基准元素大。判断基准元素的位置是否为第 K 大元素的位置，如果是，则前 K 个元素已经找到，否则继续递归处理相应的部分。</p><p>平均 <code>O(N)</code>，最坏 <code>O(N^2)</code>，平均情况下，快速选择法可以在线性时间内找到前 K 个元素，但在极端情况下，时间复杂度会退化为 <code>O(N^2)</code>。</p><h3 id="桶排序法"><a href="#桶排序法" class="headerlink" title="桶排序法"></a>桶排序法</h3><p>将元素分到不同的桶中，根据需要的 K 值只处理最靠近的几个桶。</p><ol><li><p>确定桶的数量和范围。</p></li><li><p>将元素分配到相应的桶中。</p></li><li><p>对包含最大元素的桶进行排序，然后找出前 K 大的元素。</p></li></ol><p><code>O(N)</code>（分配到桶的操作）+ <code>O(K log K)</code>（在目标桶中排序）。</p><h2 id="算法2：两个栈实现队列"><a href="#算法2：两个栈实现队列" class="headerlink" title="算法2：两个栈实现队列"></a>算法2：两个栈实现队列</h2><p><strong>主要思路</strong>：</p><ul><li>将元素推入栈 <code>stack1</code> 中；</li><li>当需要出队列时，如果 <code>stack2</code> 为空，就将 <code>stack1</code> 中的所有元素逐个弹出，并推入 <code>stack2</code>，这样 <code>stack2</code> 中的元素顺序就是先进先出的顺序；</li><li>从 <code>stack2</code> 中弹出的元素就是队列的出队元素。</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> java.util.Stack;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyQueue</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> Stack&lt;Integer&gt; stack1;</span><br><span class="line">    <span class="keyword">private</span> Stack&lt;Integer&gt; stack2;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">MyQueue</span><span class="params">()</span> &#123;</span><br><span class="line">        stack1 = <span class="keyword">new</span> <span class="title class_">Stack</span>&lt;&gt;();</span><br><span class="line">        stack2 = <span class="keyword">new</span> <span class="title class_">Stack</span>&lt;&gt;();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 入队操作</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">push</span><span class="params">(<span class="type">int</span> x)</span> &#123;</span><br><span class="line">        stack1.push(x);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 出队操作</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">int</span> <span class="title function_">pop</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (stack2.isEmpty()) &#123;</span><br><span class="line">            <span class="keyword">while</span> (!stack1.isEmpty()) &#123;</span><br><span class="line">                stack2.push(stack1.pop());</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> stack2.pop();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 获取队列头部元素</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">int</span> <span class="title function_">peek</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (stack2.isEmpty()) &#123;</span><br><span class="line">            <span class="keyword">while</span> (!stack1.isEmpty()) &#123;</span><br><span class="line">                stack2.push(stack1.pop());</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> stack2.peek();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 判断队列是否为空</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">empty</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> stack1.isEmpty() &amp;&amp; stack2.isEmpty();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>面试的时候查了一下，结果又被csdn坑了，说要有一个额外变量记录最后一个元素。其实不需要的。</p><h1 id="后话"><a href="#后话" class="headerlink" title="后话"></a>后话</h1><p>说实话，有点麻木了。今天面试感觉也一般。不知道路在何方，感觉做的东西太小众了，然后会的东西太杂。导致其实很多东西没那么专精，但咋说呢，我觉得我应该还算可以吧。但就是没有啥收获。也许我该把项目经历改改。</p><p>就挺离谱的。环境好像真的一般般，都在内卷，面试候选人贼多，要选上也挺难的。</p><p>然后现在是越来越没动力了，一座在电脑前打开文档就犯困。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;一周没写复盘了。上周主要是滴滴的一面，一个小公司的一面，还有个网易的线下面试。网易那个其实都在问简历的东西，方向不是很匹配，大概是凉了。今天
      
    
    </summary>
    
    
      <category term="编程" scheme="https://blog.zer0e.com/tags/%E7%BC%96%E7%A8%8B/"/>
    
      <category term="随想" scheme="https://blog.zer0e.com/tags/%E9%9A%8F%E6%83%B3/"/>
    
      <category term="面经" scheme="https://blog.zer0e.com/tags/%E9%9D%A2%E7%BB%8F/"/>
    
  </entry>
  
  <entry>
    <title>2024面试复盘10</title>
    <link href="https://blog.zer0e.com/2024/08/05/2024-08-05-replay/"/>
    <id>https://blog.zer0e.com/2024/08/05/2024-08-05-replay/</id>
    <published>2024-08-05T12:00:00.000Z</published>
    <updated>2024-08-05T14:42:21.243Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>滴滴面试，面试官人很亲切。除了netty相关，其他都回答的七七八八，奈何人家就是用netty去做业务的，估计也是凉凉。</p><h1 id="复盘"><a href="#复盘" class="headerlink" title="复盘"></a>复盘</h1><h2 id="分布式锁场景-怎么实现"><a href="#分布式锁场景-怎么实现" class="headerlink" title="分布式锁场景?怎么实现"></a>分布式锁场景?怎么实现</h2><p>复盘过好多次了，就是redis去实现。</p><p>场景：</p><p>分布式锁是一种用于在分布式系统中协调对共享资源访问的技术，确保在多个节点同时访问某一资源时不会出现竞争和数据不一致问题。</p><p><strong>分布式缓存一致性</strong>：当多个应用实例需要更新同一个缓存数据时，使用分布式锁可以确保只有一个实例可以进行更新操作，避免缓存不一致问题。</p><p><strong>任务调度</strong>：在分布式任务调度系统中，为了防止同一个任务被多个节点重复执行，可以使用分布式锁来确保任务只会被一个节点执行。</p><p><strong>限流控制</strong>：在高并发系统中，使用分布式锁可以限制同时处理的请求数量，避免系统过载。</p><p><strong>分布式事务</strong>：在分布式事务中，使用分布式锁可以确保多个节点在执行跨数据库或跨服务的事务时，能够按照预期的顺序进行，避免数据不一致。</p><p><strong>资源协调</strong>：在分布式系统中，多个节点可能需要访问共享资源（如文件、数据库记录等）。使用分布式锁可以确保每次只有一个节点能访问这些资源，避免竞争条件。</p><h2 id="锁的自动续期"><a href="#锁的自动续期" class="headerlink" title="锁的自动续期"></a>锁的自动续期</h2><p>watchdog大部分回答出来了，后续再复习下。</p><h2 id="redisson中如何解决主从同步问题"><a href="#redisson中如何解决主从同步问题" class="headerlink" title="redisson中如何解决主从同步问题"></a>redisson中如何解决主从同步问题</h2><p>在使用 Redisson 实现分布式锁时，如果 Redis 处于主从模式（master-slave replication），可能会遇到主从同步问题。主从同步问题主要体现在以下几个方面：</p><ol><li><strong>主节点崩溃和故障转移</strong>：当主节点崩溃时，从节点会接管成为新的主节点。在这个过程中，可能会出现锁状态丢失或不一致的问题。</li><li><strong>数据复制延迟</strong>：主节点和从节点之间的数据复制存在延迟，在这种情况下，从节点上的锁状态可能会滞后，导致数据不一致。</li></ol><h3 id="redLock"><a href="#redLock" class="headerlink" title="redLock"></a>redLock</h3><p>其实就是类似 redLock，通过将所有节点视为主节点。</p><blockquote><p>Redlock 需要部署 N （N &gt;= 2n+1）个独立的 Redis 实例，且实例之间没有任何的联系。也就是说，只要一半以上的 Redis 实例加锁成功，那么 Redlock 依然可以正常运行。</p><p>使用独立实例是为了避免 Redis 异步复制导致锁丢失。</p></blockquote><p>Redlock 加锁失败有两种情况：</p><ul><li>加锁成功的实例数量未超过半数。</li><li>加锁过程花费时间超过锁的有效时间。</li></ul><h3 id="强制主节点读取锁"><a href="#强制主节点读取锁" class="headerlink" title="强制主节点读取锁"></a>强制主节点读取锁</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">config.useMasterSlaveServers()</span><br><span class="line">      .setMasterAddress(<span class="string">&quot;redis://127.0.0.1:6379&quot;</span>)</span><br><span class="line">      .addSlaveAddress(<span class="string">&quot;redis://127.0.0.1:6380&quot;</span>, <span class="string">&quot;redis://127.0.0.1:6381&quot;</span>)</span><br><span class="line">      .setReadMode(ReadMode.MASTER);</span><br></pre></td></tr></table></figure><h2 id="其他方式实现分布式锁"><a href="#其他方式实现分布式锁" class="headerlink" title="其他方式实现分布式锁"></a>其他方式实现分布式锁</h2><p>除了数据库方式还有其他方式？</p><p>基于Zookeeper：</p><ol><li><p>在zookeeper中创建一个锁节点</p></li><li><p>检查在锁节点的兄弟节点中是否自己创建的节点是最小的。如果是，说明获取到了锁；否则，监听前一个节点的删除事件。</p></li><li><p>删除自己创建的锁节点，释放锁。</p></li></ol><p>基于etcd:</p><ol><li>在 etcd 中创建一个带有租约的键，作为锁的标识。</li><li>通过原子操作（如 <code>PUT</code> 请求）确保只有一个客户端能够创建该键。</li><li>保持租约的有效性，避免锁过期。</li><li>删除创建的键，释放锁。</li></ol><p>当然这两种方式比较少用。</p><h2 id="布隆过滤器？实现？设计的hash值和函数有什么要求？"><a href="#布隆过滤器？实现？设计的hash值和函数有什么要求？" class="headerlink" title="布隆过滤器？实现？设计的hash值和函数有什么要求？"></a>布隆过滤器？实现？设计的hash值和函数有什么要求？</h2><p>布隆过滤器（Bloom Filter）是一种高效的概率数据结构，用于判断一个元素是否存在于一个集合中。它有很高的空间效率和查询效率，但有一定的误判率，即可能会误认为某个不在集合中的元素存在于集合中。布隆过滤器不存储实际的元素，只存储元素的哈希值。</p><p>布隆过滤器的基本原理</p><ol><li><strong>初始化</strong>：创建一个位数组，初始时所有位都设为0。</li><li><strong>添加元素</strong>：使用k个不同的哈希函数将元素映射到位数组的k个位置上，并将这些位置的值设为1。</li><li><strong>查询元素</strong>：使用同样的k个哈希函数将查询元素映射到位数组的k个位置上。如果所有这些位置上的值都是1，则认为该元素可能在集合中；如果有任意一个位置上的值为0，则可以确定该元素不在集合中。</li></ol><p><strong>优点</strong>：</p><ul><li>空间效率高：相比于直接存储元素集合，布隆过滤器使用的空间要少得多。</li><li>插入和查询速度快：时间复杂度为O(k)，k是哈希函数的数量。</li></ul><p><strong>缺点</strong>：</p><ul><li>存在误判率：查询结果为“存在”时，可能是误判，但查询结果为“不存在”时一定是准确的。</li><li>无法删除元素：标准布隆过滤器不支持删除操作，删除元素可能会影响其他元素的存在判断。</li></ul><p>下面实现代码是GPT写的</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> java.util.BitSet;</span><br><span class="line"><span class="keyword">import</span> java.util.Random;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">BloomFilter</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> BitSet bitSet;</span><br><span class="line">    <span class="keyword">private</span> <span class="type">int</span> size;</span><br><span class="line">    <span class="keyword">private</span> <span class="type">int</span>[] hashSeeds;</span><br><span class="line">    <span class="keyword">private</span> Random random;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">BloomFilter</span><span class="params">(<span class="type">int</span> bitSetSize, <span class="type">int</span> hashCount)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.size = bitSetSize;</span><br><span class="line">        <span class="built_in">this</span>.bitSet = <span class="keyword">new</span> <span class="title class_">BitSet</span>(size);</span><br><span class="line">        <span class="built_in">this</span>.hashSeeds = <span class="keyword">new</span> <span class="title class_">int</span>[hashCount];</span><br><span class="line">        <span class="built_in">this</span>.random = <span class="keyword">new</span> <span class="title class_">Random</span>();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Initialize hash seeds with random values</span></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; hashCount; i++) &#123;</span><br><span class="line">            hashSeeds[i] = random.nextInt();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Simple hash function</span></span><br><span class="line">    <span class="keyword">private</span> <span class="type">int</span> <span class="title function_">hash</span><span class="params">(String value, <span class="type">int</span> seed)</span> &#123;</span><br><span class="line">        <span class="type">int</span> <span class="variable">result</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">char</span> c : value.toCharArray()) &#123;</span><br><span class="line">            result = result * seed + c;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> (size - <span class="number">1</span>) &amp; result; <span class="comment">// Modulo size to ensure it is within the range</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Add an element to the Bloom filter</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">add</span><span class="params">(String value)</span> &#123;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> seed : hashSeeds) &#123;</span><br><span class="line">            <span class="type">int</span> <span class="variable">hash</span> <span class="operator">=</span> hash(value, seed);</span><br><span class="line">            bitSet.set(hash);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Check if an element might be in the Bloom filter</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">mightContain</span><span class="params">(String value)</span> &#123;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> seed : hashSeeds) &#123;</span><br><span class="line">            <span class="type">int</span> <span class="variable">hash</span> <span class="operator">=</span> hash(value, seed);</span><br><span class="line">            <span class="keyword">if</span> (!bitSet.get(hash)) &#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        <span class="type">BloomFilter</span> <span class="variable">bloomFilter</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">BloomFilter</span>(<span class="number">1024</span>, <span class="number">3</span>);</span><br><span class="line">        bloomFilter.add(<span class="string">&quot;hello&quot;</span>);</span><br><span class="line">        bloomFilter.add(<span class="string">&quot;world&quot;</span>);</span><br><span class="line"></span><br><span class="line">        System.out.println(bloomFilter.mightContain(<span class="string">&quot;hello&quot;</span>)); <span class="comment">// True</span></span><br><span class="line">        System.out.println(bloomFilter.mightContain(<span class="string">&quot;world&quot;</span>)); <span class="comment">// True</span></span><br><span class="line">        System.out.println(bloomFilter.mightContain(<span class="string">&quot;bloom&quot;</span>)); <span class="comment">// False</span></span><br><span class="line">        System.out.println(bloomFilter.mightContain(<span class="string">&quot;filter&quot;</span>)); <span class="comment">// False</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Netty线程模型"><a href="#Netty线程模型" class="headerlink" title="Netty线程模型"></a>Netty线程模型</h2><p>Netty 基于 NIO （NIO 是一种同步非阻塞的 I/O 模型，在 Java 1.4 中引入了 NIO），使用 Netty 可以极大地简化 TCP 和 UDP 套接字服务器等网络编程，并且性能以及安全性等很多方面都非常优秀。</p><p>NIO 是一种同步非阻塞的 I/O 模型，于 Java 1.4 中引入，对应 java.nio包，提供了 Channel , Selector，Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking，不单纯是 New。它支持面向缓冲的，基于通道的 I/O 操作方法。 NIO 提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。对于高负载、高并发的（网络）应用，应使用 NIO 的非阻塞模式来开发</p><p>netty不用 NIO 主要是因为 NIO 的编程模型复杂而且存在一些 BUG，并且对编程功底要求比较高。而且，NIO 在面对断连重连、包丢失、粘包等问题时处理过程非常复杂。Netty 的出现正是为了解决这些问题，更多关于 Netty 的特点可以看下面的内容。</p><p><strong>Reactor 是一种经典的线程模型，Reactor 模式基于事件驱动，特别适合处理海量的 I/O 事件。</strong></p><h3 id="单线程-Reactor"><a href="#单线程-Reactor" class="headerlink" title="单线程 Reactor"></a>单线程 Reactor</h3><p>所有的 IO 操作都由同一个 NIO 线程处理。</p><p><strong>单线程 Reactor 的优点是对系统资源消耗特别小，但是，没办法支撑大量请求的应用场景并且处理请求的时间可能非常慢</strong>。</p><h3 id="多线程-Reactor"><a href="#多线程-Reactor" class="headerlink" title="多线程 Reactor"></a>多线程 Reactor</h3><p>一个线程负责接受请求,一组 NIO 线程处理 IO 操作。</p><p>大部分场景下多线程 Reactor 模型是没有问题的，但是在一些并发连接数比较多（如百万并发）的场景下，一个线程负责接受客户端请求就存在性能问题了。</p><p>为了解决这些问题，演进出了主从多线程 Reactor 模型。</p><h3 id="主从多线程-Reactor"><a href="#主从多线程-Reactor" class="headerlink" title="主从多线程 Reactor"></a>主从多线程 Reactor</h3><p>一组 NIO 线程负责接受请求，一组 NIO 线程处理 IO 操作。</p><p>在 Netty 主要靠 <code>NioEventLoopGroup</code> 线程池来实现具体的线程模型的 。</p><p>我们实现服务端的时候，一般会初始化两个线程组：</p><ol><li>bossGroup :接收连接。</li><li>workerGroup ：负责具体的处理，交由对应的 Handler 处理。</li></ol><p>从一个 主线程 NIO 线程池中选择一个线程作为 Acceptor 线程，绑定监听端口，接收客户端连接的连接，其他线程负责后续的接入认证等工作。连接建立完成后，Sub NIO 线程池负责具体处理 I/O 读写。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 1.bossGroup 用于接收连接，workerGroup 用于具体的处理</span></span><br><span class="line"><span class="type">EventLoopGroup</span> <span class="variable">bossGroup</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">NioEventLoopGroup</span>(<span class="number">1</span>);</span><br><span class="line"><span class="type">EventLoopGroup</span> <span class="variable">workerGroup</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">NioEventLoopGroup</span>();</span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="comment">//2.创建服务端启动引导/辅助类：ServerBootstrap</span></span><br><span class="line">    <span class="type">ServerBootstrap</span> <span class="variable">b</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ServerBootstrap</span>();</span><br><span class="line">    <span class="comment">//3.给引导类配置两大线程组,确定了线程模型</span></span><br><span class="line">    b.group(bossGroup, workerGroup)</span><br><span class="line">            <span class="comment">// (非必备)打印日志</span></span><br><span class="line">            .handler(<span class="keyword">new</span> <span class="title class_">LoggingHandler</span>(LogLevel.INFO))</span><br><span class="line">            <span class="comment">// 4.指定 IO 模型</span></span><br><span class="line">            .channel(NioServerSocketChannel.class)</span><br><span class="line">            .childHandler(<span class="keyword">new</span> <span class="title class_">ChannelInitializer</span>&lt;SocketChannel&gt;() &#123;</span><br><span class="line">                <span class="meta">@Override</span></span><br><span class="line">                <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">initChannel</span><span class="params">(SocketChannel ch)</span> &#123;</span><br><span class="line">                    <span class="type">ChannelPipeline</span> <span class="variable">p</span> <span class="operator">=</span> ch.pipeline();</span><br><span class="line">                    <span class="comment">//5.可以自定义客户端消息的业务处理逻辑</span></span><br><span class="line">                    p.addLast(<span class="keyword">new</span> <span class="title class_">HelloServerHandler</span>());</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;);</span><br><span class="line">    <span class="comment">// 6.绑定端口,调用 sync 方法阻塞知道绑定完成</span></span><br><span class="line">    <span class="type">ChannelFuture</span> <span class="variable">f</span> <span class="operator">=</span> b.bind(port).sync();</span><br><span class="line">    <span class="comment">// 7.阻塞等待直到服务器Channel关闭(closeFuture()方法获取Channel 的CloseFuture对象,然后调用sync()方法)</span></span><br><span class="line">    f.channel().closeFuture().sync();</span><br><span class="line">&#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">    <span class="comment">//8.优雅关闭相关线程组资源</span></span><br><span class="line">    bossGroup.shutdownGracefully();</span><br><span class="line">    workerGroup.shutdownGracefully();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>具体流程如下：</p><p>1、首先你创建了两个 NioEventLoopGroup 对象实例：bossGroup 和 workerGroup。</p><ul><li>bossGroup : 用于处理客户端的 TCP 连接请求。</li><li>workerGroup ： 负责每一条连接的具体读写数据的处理逻辑，真正负责 I/O 读写操作，交由对应的 Handler 处理。</li></ul><p>这里要注意使用 NioEventLoopGroup 类的无参构造函数设置线程数量的默认值就是 CPU 核心数 *2 。</p><p>2、接下来 我们创建了一个服务端启动引导/辅助类： ServerBootstrap，这个类将引导我们进行服务端的启动工作。</p><p>3、通过 group() 方法给引导类 ServerBootstrap 配置两大线程组，确定了线程模型。</p><p>4、通过channel()方法给引导类 ServerBootstrap指定了 IO 模型为NIO</p><ul><li>NioServerSocketChannel ：指定服务端的 IO 模型为 NIO，与 BIO 编程模型中的ServerSocket对应 </li><li>NioSocketChannel : 指定客户端的 IO 模型为 NIO， 与 BIO 编程模型中的Socket对应 </li></ul><p>5、通过 childHandler()方法给引导类创建一个ChannelInitializer ，然后指定了服务端消息的业务处理逻辑 HelloServerHandler 对象。</p><p>6、调用 ServerBootstrap 类的 bind()方法绑定端口。</p><h3 id="TCP-粘包-拆包"><a href="#TCP-粘包-拆包" class="headerlink" title="TCP 粘包/拆包"></a>TCP 粘包/拆包</h3><p>1.使用 Netty 自带的解码器</p><ul><li>LineBasedFrameDecoder : 发送端发送数据包的时候，每个数据包之间以换行符作为分隔，LineBasedFrameDecoder 的工作原理是它依次遍历 ByteBuf 中的可读字节，判断是否有换行符，然后进行相应的截取。</li><li>DelimiterBasedFrameDecoder : 可以自定义分隔符解码器，实际上是一种特殊的 DelimiterBasedFrameDecoder 解码器。</li><li>FixedLengthFrameDecoder: 固定长度解码器，它能够按照指定的长度对消息进行相应的拆包。如果不够指定的长度，则空格补全</li><li>LengthFieldBasedFrameDecoder：长度域解码器，它能够根据发送的数据中消息长度相关参数（比如长度域偏移量 lengthFieldOffset）来进行拆包。</li></ul><p>Netty其他知识后面再梳理下。</p><h2 id="Java锁的实现方式"><a href="#Java锁的实现方式" class="headerlink" title="Java锁的实现方式"></a>Java锁的实现方式</h2><p>一个是synchronized另一个是ReentrantLock(AQS).</p><p>还有个乐观锁(AtomicInteger).</p><p>CAS 是一种原子操作，用于确保在多线程环境中，某个值在更新时不会被其他线程修改。Java的 <code>java.util.concurrent.atomic</code> 包提供了一些基于 CAS 的类，这些类实现了乐观锁机制。</p><p>例如<code>AtomicInteger</code>, <code>AtomicReference</code>, <code>AtomicStampedReference</code></p><ul><li><p><code>AtomicInteger</code>就不说了。</p></li><li><p><code>AtomicReference</code> 是用于原子操作引用类型的类。它允许原子地更新对象引用，适用于需要保证对象的线程安全的场景。例如<code>AtomicReference&lt;String&gt; reference = new AtomicReference&lt;&gt;(&quot;Initial&quot;);</code></p></li><li><p><code>AtomicStampedReference</code> 提供了对引用类型的原子操作，并结合了一个整数戳（通常是版本号），用于解决ABA问题。这个我还没用过。</p></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">AtomicStampedReference&lt;String&gt; stampedReference =</span><br><span class="line">            <span class="keyword">new</span> <span class="title class_">AtomicStampedReference</span>&lt;&gt;(<span class="string">&quot;Initial&quot;</span>, <span class="number">0</span>);</span><br><span class="line"><span class="type">int</span>[] stamp = <span class="keyword">new</span> <span class="title class_">int</span>[<span class="number">1</span>];</span><br><span class="line">        <span class="type">String</span> <span class="variable">oldValue</span> <span class="operator">=</span> stampedReference.get(stamp);</span><br><span class="line">        stampedReference.set(newValue, stamp[<span class="number">0</span>] + <span class="number">1</span>);</span><br></pre></td></tr></table></figure><h2 id="限流了解吗"><a href="#限流了解吗" class="headerlink" title="限流了解吗"></a>限流了解吗</h2><h3 id="令牌桶算法（Token-Bucket-Algorithm）"><a href="#令牌桶算法（Token-Bucket-Algorithm）" class="headerlink" title="令牌桶算法（Token Bucket Algorithm）"></a>令牌桶算法（Token Bucket Algorithm）</h3><p>令牌桶算法使用一个“桶”来控制数据的流入速率。</p><p>桶里有令牌，令牌以固定速率生成。</p><p>每次请求需要获取一个令牌，令牌桶中的令牌数量不足时，请求会被拒绝或等待。</p><p>令牌桶算法允许突发流量，但总流量受限于桶的大小和令牌生成速率。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> java.util.concurrent.Semaphore;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TokenBucket</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Semaphore tokens;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">long</span> refillInterval;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> maxTokens;</span><br><span class="line">    <span class="keyword">private</span> <span class="type">long</span> lastRefillTime;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">TokenBucket</span><span class="params">(<span class="type">int</span> maxTokens, <span class="type">long</span> refillInterval)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.maxTokens = maxTokens;</span><br><span class="line">        <span class="built_in">this</span>.refillInterval = refillInterval;</span><br><span class="line">        <span class="built_in">this</span>.tokens = <span class="keyword">new</span> <span class="title class_">Semaphore</span>(maxTokens, <span class="literal">true</span>);</span><br><span class="line">        <span class="built_in">this</span>.lastRefillTime = System.currentTimeMillis();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="type">boolean</span> <span class="title function_">acquire</span><span class="params">()</span> &#123;</span><br><span class="line">        refill();</span><br><span class="line">        <span class="keyword">return</span> tokens.tryAcquire();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">refill</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="type">long</span> <span class="variable">now</span> <span class="operator">=</span> System.currentTimeMillis();</span><br><span class="line">        <span class="type">long</span> <span class="variable">elapsed</span> <span class="operator">=</span> now - lastRefillTime;</span><br><span class="line">        <span class="keyword">if</span> (elapsed &gt;= refillInterval) &#123;</span><br><span class="line">            <span class="type">int</span> <span class="variable">newTokens</span> <span class="operator">=</span> (<span class="type">int</span>) (elapsed / refillInterval);</span><br><span class="line">            <span class="keyword">if</span> (newTokens &gt; <span class="number">0</span>) &#123;</span><br><span class="line">                <span class="type">int</span> <span class="variable">availableTokens</span> <span class="operator">=</span> Math.min(newTokens, maxTokens - tokens.availablePermits());</span><br><span class="line">                tokens.release(availableTokens);</span><br><span class="line">                lastRefillTime = now;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="漏桶算法（Leaky-Bucket-Algorithm）"><a href="#漏桶算法（Leaky-Bucket-Algorithm）" class="headerlink" title="漏桶算法（Leaky Bucket Algorithm）"></a>漏桶算法（Leaky Bucket Algorithm）</h3><p>漏桶算法通过一个“桶”来控制请求流量。</p><p>请求以任意速度到达桶中，但桶中的请求以固定速率“漏出”。</p><p>当桶满时，多余的请求会被丢弃。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> java.util.concurrent.BlockingQueue;</span><br><span class="line"><span class="keyword">import</span> java.util.concurrent.LinkedBlockingQueue;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">LeakyBucket</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> BlockingQueue&lt;Object&gt; bucket;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">long</span> leakRate;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">LeakyBucket</span><span class="params">(<span class="type">int</span> capacity, <span class="type">long</span> leakRate)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.bucket = <span class="keyword">new</span> <span class="title class_">LinkedBlockingQueue</span>&lt;&gt;(capacity);</span><br><span class="line">        <span class="built_in">this</span>.leakRate = leakRate;</span><br><span class="line">        startLeak();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">add</span><span class="params">(Object item)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> bucket.offer(item);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">startLeak</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">new</span> <span class="title class_">Thread</span>(() -&gt; &#123;</span><br><span class="line">            <span class="keyword">while</span> (<span class="literal">true</span>) &#123;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    Thread.sleep(leakRate);</span><br><span class="line">                    bucket.poll();</span><br><span class="line">                &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">                    Thread.currentThread().interrupt();</span><br><span class="line">                    <span class="keyword">break</span>;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;).start();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="滑动窗口算法（Sliding-Window-Algorithm）"><a href="#滑动窗口算法（Sliding-Window-Algorithm）" class="headerlink" title="滑动窗口算法（Sliding Window Algorithm）"></a>滑动窗口算法（Sliding Window Algorithm）</h3><p>滑动窗口算法维护一个时间窗口，在该窗口内控制请求数量。</p><p>当请求超出窗口内的最大允许数时，新的请求会被拒绝。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> java.util.LinkedList;</span><br><span class="line"><span class="keyword">import</span> java.util.Queue;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SlidingWindow</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> maxRequests;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">long</span> windowSize;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Queue&lt;Long&gt; requests = <span class="keyword">new</span> <span class="title class_">LinkedList</span>&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">SlidingWindow</span><span class="params">(<span class="type">int</span> maxRequests, <span class="type">long</span> windowSize)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.maxRequests = maxRequests;</span><br><span class="line">        <span class="built_in">this</span>.windowSize = windowSize;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="type">boolean</span> <span class="title function_">allowRequest</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="type">long</span> <span class="variable">now</span> <span class="operator">=</span> System.currentTimeMillis();</span><br><span class="line">        <span class="keyword">while</span> (!requests.isEmpty() &amp;&amp; now - requests.peek() &gt; windowSize) &#123;</span><br><span class="line">            requests.poll();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (requests.size() &lt; maxRequests) &#123;</span><br><span class="line">            requests.add(now);</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="固定窗口计数器（Fixed-Window-Counter）"><a href="#固定窗口计数器（Fixed-Window-Counter）" class="headerlink" title="固定窗口计数器（Fixed Window Counter）"></a>固定窗口计数器（Fixed Window Counter）</h3><p>固定窗口计数器维护一个固定时间窗口内的请求计数。</p><p>每个窗口开始时计数器被重置，新的请求会增加计数器。</p><p>当计数器超出最大允许请求数时，新的请求会被拒绝。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> java.util.concurrent.atomic.AtomicInteger;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">FixedWindowCounter</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> maxRequests;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">long</span> windowSize;</span><br><span class="line">    <span class="keyword">private</span> <span class="type">long</span> windowStart;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">AtomicInteger</span> <span class="variable">requestCount</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">AtomicInteger</span>();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">FixedWindowCounter</span><span class="params">(<span class="type">int</span> maxRequests, <span class="type">long</span> windowSize)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.maxRequests = maxRequests;</span><br><span class="line">        <span class="built_in">this</span>.windowSize = windowSize;</span><br><span class="line">        <span class="built_in">this</span>.windowStart = System.currentTimeMillis();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="type">boolean</span> <span class="title function_">allowRequest</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="type">long</span> <span class="variable">now</span> <span class="operator">=</span> System.currentTimeMillis();</span><br><span class="line">        <span class="keyword">if</span> (now - windowStart &gt; windowSize) &#123;</span><br><span class="line">            windowStart = now;</span><br><span class="line">            requestCount.set(<span class="number">0</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (requestCount.incrementAndGet() &lt;= maxRequests) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="伪代码实现令牌桶-ratelimiter-？"><a href="#伪代码实现令牌桶-ratelimiter-？" class="headerlink" title="伪代码实现令牌桶(ratelimiter)？"></a>伪代码实现令牌桶(ratelimiter)？</h2><p>刚开始我回答用另一个线程或者定时任务去做。</p><p>其实实现就是基于上次获取的时间戳来做。</p><p>上面已经写了一下。</p><p>说实话，挺有意思，没想到用这种方式来考察代码。</p><h2 id="后话"><a href="#后话" class="headerlink" title="后话"></a>后话</h2><p>今天是8月5号，昨天4号的时候刚好是我离职两月。说焦虑吧，其实早上起床还有晚上睡觉前挺焦虑的，随着面试逐渐增多，我开始怀疑自己，但好像认真做事的时候，比如写东西的时候或者学习的时候感觉也还好，我可能只是觉得空窗太久之后不太好找工作，又或者觉得努力了一段时间没有任何收获，急需一颗糖补充下信心？</p><p>稍微统计了一下我从6月下旬开始面试的次数，一共12家公司，16场面试，说实话，我没想到这么难，如果说刚开始是自身原因，那么后续其实都是客观因素了，有面试刷KPI的，有业务不匹配的，有薪酬不合适的。如果后面实在不行，我只能继续试试测试开发岗了，但说实话我真不想做测试开发了。再不行考虑下回家睡大觉。</p><p>今天小谢和我说毕业之后第一份工作很重要，因为除非你运气爆棚或者受到赏识，否则你后续都是以这份工作为基础，我觉得说的很对，我好像被锁死上限了。</p><p>他也一直和我交流人生，心理上的，比如人生漫漫，不以物喜，不以己悲啊之类的，我觉得说的都挺有道理的。我是一个接受能力比较开放的人，一些观点我其实都是持保留意见，我不反对也不认可，我认为一个观点只有当亲身经历过，才会明白其中的云云。我想这就是人生的意义所在吧，亲身经历与感受各种心情或事物？应该是吧。</p><p>有时候我觉得他对人生看的比我透彻，我也羡慕他的豁达。他总是和我说总有办法的，我想也是，只要活着，未来谁又说的准呢？</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;滴滴面试，面试官人很亲切。除了netty相关，其他都回答的七七八八，奈何人家就是用netty去做业务的，估计也是凉凉。&lt;/p&gt;
&lt;h1 id
      
    
    </summary>
    
    
      <category term="编程" scheme="https://blog.zer0e.com/tags/%E7%BC%96%E7%A8%8B/"/>
    
      <category term="随想" scheme="https://blog.zer0e.com/tags/%E9%9A%8F%E6%83%B3/"/>
    
      <category term="面经" scheme="https://blog.zer0e.com/tags/%E9%9D%A2%E7%BB%8F/"/>
    
  </entry>
  
  <entry>
    <title>2024面试复盘9</title>
    <link href="https://blog.zer0e.com/2024/08/02/2024-08-02-replay/"/>
    <id>https://blog.zer0e.com/2024/08/02/2024-08-02-replay/</id>
    <published>2024-08-02T12:00:00.000Z</published>
    <updated>2024-08-02T13:05:28.762Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>今天经历了一场长时间面试，接近快两小时，晚上又经历了一场，把我干蒙了，尤其是晚上那场，一共问了三个问题，我都没答上来。寄。。。</p><h1 id="复盘"><a href="#复盘" class="headerlink" title="复盘"></a>复盘</h1><h2 id="JAVA代码加载执行过程"><a href="#JAVA代码加载执行过程" class="headerlink" title="JAVA代码加载执行过程"></a>JAVA代码加载执行过程</h2><p>涉及到类加载过程，根本记不住，就算文档在旁边我也答不上来。</p><p>Java代码的加载和执行过程涉及多个步骤，包括编译、类加载、链接、初始化和运行。</p><h3 id="1-编译阶段"><a href="#1-编译阶段" class="headerlink" title="1. 编译阶段"></a>1. 编译阶段</h3><p>Java源代码文件（.java）通过Java编译器（javac）编译成字节码文件（.class）。字节码是一种中间表示形式，它可以在Java虚拟机（JVM）上运行。</p><h3 id="2-类加载阶段"><a href="#2-类加载阶段" class="headerlink" title="2. 类加载阶段"></a>2. 类加载阶段</h3><p>类加载阶段是将类的字节码加载到JVM内存中的过程。类加载器（ClassLoader）负责加载类。</p><ul><li><strong>启动类加载器（Bootstrap ClassLoader）</strong>：加载核心Java类库，如<code>java.lang</code>、<code>java.util</code>等，通常从JVM的运行时环境（rt.jar）中加载。</li><li><strong>扩展类加载器（Extension ClassLoader）</strong>：加载扩展类库，通常从<code>lib/ext</code>目录中加载。</li><li><strong>应用类加载器（Application ClassLoader）</strong>：加载应用程序的类，通常从类路径（classpath）中加载。</li></ul><h3 id="3-链接阶段"><a href="#3-链接阶段" class="headerlink" title="3. 链接阶段"></a>3. 链接阶段</h3><p>链接阶段将类的字节码合并到JVM中，并为其分配内存。链接阶段包括三个子阶段：</p><ul><li><strong>验证（Verification）</strong>：确保字节码符合JVM的规范，并且没有安全问题。</li><li><strong>准备（Preparation）</strong>：为类的静态变量分配内存，并初始化为默认值。</li><li><strong>解析（Resolution）</strong>：将常量池中的符号引用替换为直接引用。</li></ul><h3 id="4-初始化阶段"><a href="#4-初始化阶段" class="headerlink" title="4. 初始化阶段"></a>4. 初始化阶段</h3><p>初始化阶段是执行类构造器方法 <code>&lt;clinit&gt;</code> 的过程。类构造器方法 <code>&lt;clinit&gt;</code> 是由编译器自动生成的，它包含类静态变量的赋值和静态代码块的执行。</p><h3 id="5-执行阶段"><a href="#5-执行阶段" class="headerlink" title="5. 执行阶段"></a>5. 执行阶段</h3><p>在执行阶段，JVM开始执行应用程序的<code>main</code>方法。整个应用程序的执行过程由JVM的执行引擎来处理。执行引擎包括以下几个部分：</p><ul><li><strong>解释器（Interpreter）</strong>：逐行解释执行字节码。</li><li><strong>即时编译器（Just-In-Time Compiler, JIT）</strong>：将热点代码（执行频繁的代码）编译成本地机器码，以提高执行效率。</li><li><strong>垃圾收集器（Garbage Collector, GC）</strong>：管理内存，回收不再使用的对象。</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Main</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;Hello, World!&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>假设我们有上述简单的Java代码，以下是详细的加载和执行过程</p><p><strong>编译</strong>：</p><ul><li>使用 <code>javac Main.java</code> 编译器将 <code>Main.java</code> 编译为 <code>Main.class</code> 字节码文件。</li></ul><p><strong>类加载</strong>：</p><ul><li>JVM启动时，启动类加载器首先加载核心类库。</li><li>应用类加载器加载 <code>Main</code> 类的字节码，将其放入方法区，并创建一个 <code>Class</code> 对象表示 <code>Main</code> 类。</li></ul><p><strong>链接</strong>：</p><ul><li><strong>验证</strong>：JVM验证 <code>Main.class</code> 字节码是否合法。</li><li><strong>准备</strong>：为 <code>Main</code> 类的静态变量分配内存并初始化为默认值（此例中没有静态变量）。</li><li><strong>解析</strong>：将常量池中的符号引用解析为直接引用。</li></ul><p><strong>初始化</strong>：</p><ul><li>执行 <code>Main</code> 类的静态初始化代码（如有），包括静态变量的初始化和静态代码块（此例中没有静态初始化代码）。</li></ul><p><strong>执行</strong>：</p><ul><li>JVM调用 <code>Main</code> 类的 <code>main</code> 方法。</li><li>解释器解释执行 <code>main</code> 方法的字节码，将 <code>Hello, World!</code> 打印到控制台。</li><li>JIT编译器在执行频繁的代码时将其编译为本地机器码，以提高执行效率。</li></ul><h2 id="JAVA中能否加载相同类名-自定义java-lang-String能否加载？"><a href="#JAVA中能否加载相同类名-自定义java-lang-String能否加载？" class="headerlink" title="JAVA中能否加载相同类名?自定义java.lang.String能否加载？"></a>JAVA中能否加载相同类名?自定义java.lang.String能否加载？</h2><p>在Java中，可以加载具有相同类名的类，但需要通过不同的类加载器来实现。每个类加载器都有其独立的命名空间，因此可以加载同名类而不冲突。这种机制在实现Java应用程序中的模块化、插件化和动态加载时非常有用。</p><p><strong>类加载器</strong>：负责加载类文件到JVM内存中。Java中的类加载器包括引导类加载器（Bootstrap ClassLoader）、扩展类加载器（Extension ClassLoader）和应用类加载器（Application ClassLoader）。还可以自定义类加载器。</p><p><strong>命名空间</strong>：每个类加载器都有自己的命名空间，包含其加载的所有类。不同类加载器的命名空间是独立的，这意味着两个不同类加载器可以加载同名的类而不冲突。</p><p>简单来说就是使用不同的类加载器来加载类。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ClassLoaderTest</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">// Load MyClass from dir1</span></span><br><span class="line">            <span class="type">CustomClassLoader</span> <span class="variable">classLoader1</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">CustomClassLoader</span>(<span class="string">&quot;bin/dir1/&quot;</span>);</span><br><span class="line">            Class&lt;?&gt; class1 = classLoader1.loadClass(<span class="string">&quot;com.example.MyClass&quot;</span>);</span><br><span class="line">            <span class="type">Object</span> <span class="variable">obj1</span> <span class="operator">=</span> class1.newInstance();</span><br><span class="line">            class1.getMethod(<span class="string">&quot;print&quot;</span>).invoke(obj1);</span><br><span class="line"></span><br><span class="line">            <span class="comment">// Load MyClass from dir2</span></span><br><span class="line">            <span class="type">CustomClassLoader</span> <span class="variable">classLoader2</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">CustomClassLoader</span>(<span class="string">&quot;bin/dir2/&quot;</span>);</span><br><span class="line">            Class&lt;?&gt; class2 = classLoader2.loadClass(<span class="string">&quot;com.example.MyClass&quot;</span>);</span><br><span class="line">            <span class="type">Object</span> <span class="variable">obj2</span> <span class="operator">=</span> class2.newInstance();</span><br><span class="line">            class2.getMethod(<span class="string">&quot;print&quot;</span>).invoke(obj2);</span><br><span class="line"></span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在Java中，自定义 <code>java.lang.String</code> 类并尝试加载它是非常困难的，因为 <code>java.lang.String</code> 是由引导类加载器（Bootstrap ClassLoader）加载的核心类。引导类加载器是JVM的一部分，用于加载核心Java类库，它有最高的优先级并且不允许由其他类加载器覆盖这些核心类。</p><p><strong>引导类加载器的优先级</strong>：引导类加载器首先加载核心类库，包括 <code>java.lang.String</code>。由于它是最先被加载的类加载器，并且在JVM启动时就已经加载了核心类，其他类加载器不能覆盖或重新定义这些类。</p><p><strong>双亲委派模型</strong>：Java的类加载器遵循双亲委派模型，即当一个类加载器加载类时，它首先委派给其父类加载器。如果父类加载器已经加载了这个类，子类加载器就不会再加载它。由于 <code>java.lang.String</code> 是由引导类加载器加载的，所有其他类加载器在加载 <code>java.lang.String</code> 时都会被委派给引导类加载器，从而无法加载自定义的 <code>java.lang.String</code>。</p><p>虽然理论上可以通过一些非常规手段尝试加载自定义的 <code>java.lang.String</code> 类，但在标准JVM实现中，这样的操作通常会失败或导致异常。以下是一些可能的方式和其局限性：</p><ol><li><strong>通过自定义类加载器</strong>：即使你编写了一个自定义类加载器并尝试加载自定义的 <code>java.lang.String</code>，由于双亲委派模型，最终还是会委派给引导类加载器。</li><li><strong>修改引导类路径</strong>：可以尝试将自定义的 <code>java.lang.String</code> 放在引导类路径上，但这需要修改JVM的启动参数，且一般来说这不被推荐，因为它会破坏JVM的稳定性和安全性。</li></ol><h2 id="mysql也能存储json，为什么mongo比mysql快"><a href="#mysql也能存储json，为什么mongo比mysql快" class="headerlink" title="mysql也能存储json，为什么mongo比mysql快"></a>mysql也能存储json，为什么mongo比mysql快</h2><h3 id="1-数据模型和存储机制"><a href="#1-数据模型和存储机制" class="headerlink" title="1. 数据模型和存储机制"></a>1. 数据模型和存储机制</h3><p><strong>MongoDB</strong>：</p><ul><li>MongoDB 是一个文档数据库，设计上天然支持 JSON（实际上是 BSON，Binary JSON）。</li><li>数据以文档的形式存储，每个文档是一个键-值对的集合，可以嵌套和具有多样的结构。</li><li>MongoDB 的文档模型更接近 JSON 的原始结构，因此存储和读取 JSON 数据时更加高效。</li></ul><p><strong>MySQL</strong>：</p><ul><li>MySQL 是一个关系型数据库，虽然它支持 JSON 数据类型，但其设计主要针对结构化数据。</li><li>MySQL 在存储 JSON 数据时，通常将其作为字符串处理，需要额外的解析和转换。</li><li>对 JSON 数据的查询和操作需要额外的步骤，如解析 JSON 字符串，这会影响性能。</li></ul><h3 id="2-索引和查询优化"><a href="#2-索引和查询优化" class="headerlink" title="2. 索引和查询优化"></a>2. 索引和查询优化</h3><p><strong>MongoDB</strong>：</p><ul><li>MongoDB 支持在文档中的嵌套字段上创建索引，这使得对嵌套 JSON 结构的查询更高效。</li><li>MongoDB 的查询语言和引擎专为文档结构设计，能够直接对 BSON 结构进行操作，无需额外的解析步骤。</li><li>由于设计上的优势，MongoDB 在进行复杂的嵌套查询时性能更好。</li></ul><p><strong>MySQL</strong>：</p><ul><li>MySQL 也支持对 JSON 字段创建索引，但其索引机制和查询优化主要为关系型数据设计。</li><li>查询 JSON 数据时，MySQL 需要先解析 JSON 字符串，然后再执行查询，增加了额外的开销。</li><li>对嵌套 JSON 结构的查询在 MySQL 中较为复杂，可能需要使用函数和额外的步骤来解析和提取数据。</li></ul><h3 id="3-数据更新和写入"><a href="#3-数据更新和写入" class="headerlink" title="3. 数据更新和写入"></a>3. 数据更新和写入</h3><p><strong>MongoDB</strong>：</p><ul><li>MongoDB 的写入操作设计上更加灵活，支持文档的部分更新。</li><li>对于 JSON 数据的更新，MongoDB 能够直接修改文档中的嵌套字段，而无需整体替换。</li><li>这种灵活性使得对 JSON 数据的写入和更新操作更高效。</li></ul><p><strong>MySQL</strong>：</p><ul><li>MySQL 对 JSON 数据的更新通常需要解析和重新构建整个 JSON 字符串，尤其是对于嵌套结构。</li><li>这种操作增加了写入和更新的开销，导致性能下降。</li></ul><h3 id="4-并发处理和扩展性"><a href="#4-并发处理和扩展性" class="headerlink" title="4. 并发处理和扩展性"></a>4. 并发处理和扩展性</h3><p><strong>MongoDB</strong>：</p><ul><li>MongoDB 的架构设计为分布式存储和高并发处理进行了优化。</li><li>通过分片和复制集机制，MongoDB 能够轻松扩展以应对大规模的数据量和高并发请求。</li></ul><p><strong>MySQL</strong>：</p><ul><li>MySQL 也支持分布式和集群架构，但其扩展性主要针对关系型数据。</li><li>对于大规模 JSON 数据的处理，MySQL 的性能和扩展性可能不如 MongoDB。</li></ul><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>这次真的一问三不知，另一场两小时面试基础问题比较多，周末整理下看看再发一篇。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;今天经历了一场长时间面试，接近快两小时，晚上又经历了一场，把我干蒙了，尤其是晚上那场，一共问了三个问题，我都没答上来。寄。。。&lt;/p&gt;
&lt;h
      
    
    </summary>
    
    
      <category term="编程" scheme="https://blog.zer0e.com/tags/%E7%BC%96%E7%A8%8B/"/>
    
      <category term="随想" scheme="https://blog.zer0e.com/tags/%E9%9A%8F%E6%83%B3/"/>
    
      <category term="面经" scheme="https://blog.zer0e.com/tags/%E9%9D%A2%E7%BB%8F/"/>
    
  </entry>
  
  <entry>
    <title>2024面试复盘8</title>
    <link href="https://blog.zer0e.com/2024/08/01/2024-08-01-replay/"/>
    <id>https://blog.zer0e.com/2024/08/01/2024-08-01-replay/</id>
    <published>2024-08-01T11:00:00.000Z</published>
    <updated>2024-08-01T11:25:44.249Z</updated>
    
    <content type="html"><![CDATA[<h1 id="复盘"><a href="#复盘" class="headerlink" title="复盘"></a>复盘</h1><h2 id="反射是什么-怎么用"><a href="#反射是什么-怎么用" class="headerlink" title="反射是什么 怎么用"></a>反射是什么 怎么用</h2><p>刚开始我愣了一下，在大学的时候我其实也写过关于反射的<a href="https://re0.top/2020/03/22/java-reflection/">文章</a>，结果忘得差不多了。</p><p>其实就是借助class对象获取类信息的方式称作反射。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//1</span></span><br><span class="line"><span class="type">Class</span> <span class="variable">c1</span> <span class="operator">=</span> Cat.class; <span class="comment">//任何类都有隐含的静态成员class用于获取Class对象</span></span><br><span class="line"><span class="comment">//2</span></span><br><span class="line"><span class="type">Class</span> <span class="variable">c2</span> <span class="operator">=</span> cat.getClass();<span class="comment">//实例有一个getClass方法获取Class对象</span></span><br><span class="line"><span class="comment">//3</span></span><br><span class="line"><span class="type">Class</span> <span class="variable">c3</span> <span class="operator">=</span> Class.forName(<span class="string">&quot;Cat&quot;</span>);</span><br></pre></td></tr></table></figure><blockquote><p>如果使用getMethods则可以获取所有方法。但是getMethod和getMethods获取的是public方法，如果需要获得私有方法，则使用getDeclaredMethod方法。</p></blockquote><p>获取成员则是使用<code>getField/getDeclaredField</code>.</p><p>获取构造器是<code>getConstructor/getDeclaredConstructor</code></p><h2 id="Java中的常用集合-底层结构"><a href="#Java中的常用集合-底层结构" class="headerlink" title="Java中的常用集合 底层结构"></a>Java中的常用集合 底层结构</h2><p>Java 集合，也叫作容器，主要是由两大接口派生而来：一个是 <code>Collection</code>接口，主要用于存放单一元素；另一个是 <code>Map</code> 接口，主要用于存放键值对。对于<code>Collection</code> 接口，下面又有三个主要的子接口：<code>List</code>、<code>Set</code> 、 <code>Queue</code>。</p><p><code>ArrayList</code>底层是<code>Object[]</code> 数组。动态进行扩容。</p><p><code>TreeSet</code>底层是红黑树。</p><h2 id="用redis实现延迟队列"><a href="#用redis实现延迟队列" class="headerlink" title="用redis实现延迟队列"></a>用redis实现延迟队列</h2><p>使用sortedSet来实现</p><p><strong>添加任务</strong>：使用<code>ZADD</code>命令将任务添加到有序集合中，分数为任务的执行时间。</p><p><strong>消费任务</strong>：使用<code>ZRANGEBYSCORE</code>命令获取到期的任务，并使用<code>ZREM</code>命令从有序集合中删除这些任务。</p><h2 id="CAP理论"><a href="#CAP理论" class="headerlink" title="CAP理论"></a>CAP理论</h2><p>一致性（Consistency）、可用性（Availability）和分区容忍性（Partition Tolerance）。</p><p>根据CAP定理，在一个分布式系统中，最多只能同时满足这三个属性中的两个。</p><p><strong>一致性（Consistency）</strong>：</p><ul><li>在一个分布式系统中，一致性指的是所有节点在同一时间看到的数据是相同的。换句话说，每次读操作都能够返回最近一次写操作的结果。</li></ul><p><strong>可用性（Availability）</strong>：</p><ul><li>可用性指的是系统在任何时间都是可用的，并且每个请求都能够得到非错误的响应（不保证返回最新的数据）。即使某些节点发生故障，系统仍然能够响应请求。</li></ul><p><strong>分区容忍性（Partition Tolerance）</strong>：</p><ul><li>分区容忍性指的是系统能够继续操作，即使网络分区（Partition）发生了。网络分区是指网络中的一些节点无法与其他节点通信，但系统仍然能够正常工作。</li></ul><p><strong>CA（Consistency and Availability）</strong>：</p><ul><li>系统在正常操作时保证一致性和可用性，但在网络分区时无法容忍。</li><li>例子：传统的关系型数据库（如单一的SQL数据库）。</li></ul><p><strong>CP（Consistency and Partition Tolerance）</strong>：</p><ul><li>系统在网络分区时保证一致性，但可能会牺牲部分可用性。</li><li>例子：一些分布式数据库（如HBase、MongoDB在强一致性配置下）。</li></ul><p><strong>AP（Availability and Partition Tolerance）</strong>：</p><ul><li>系统在网络分区时保证可用性，但可能无法保证强一致性。</li><li>例子：一些NoSQL数据库（如Cassandra、DynamoDB）。</li></ul><h2 id="分布式锁实现"><a href="#分布式锁实现" class="headerlink" title="分布式锁实现"></a>分布式锁实现</h2><p>有redis分布式锁和mysql的悲观锁。</p><p>使用<code>SET</code>命令以<code>NX</code>和<code>EX</code>选项来设置锁键。</p><p>使用<code>DEL</code>命令来释放锁。</p><p>使用一个唯一的值（通常是UUID）来标识锁持有者，以防止误删他人的锁。</p><h3 id="NX-选项"><a href="#NX-选项" class="headerlink" title="NX 选项"></a><code>NX</code> 选项</h3><ul><li><strong>含义</strong>：<code>NX</code>选项表示”只在键不存在时设置”（Not Exists）。</li><li><strong>功能</strong>：当<code>NX</code>选项指定时，<code>SET</code>命令只会在键不存在的情况下设置键值。如果键已经存在，<code>SET</code>命令不会执行任何操作。</li><li><strong>用途</strong>：常用于分布式锁的实现，确保只有一个客户端能够成功设置锁，防止多个客户端同时获得锁。</li></ul><h3 id="EX-选项"><a href="#EX-选项" class="headerlink" title="EX 选项"></a><code>EX</code> 选项</h3><ul><li><strong>含义</strong>：<code>EX</code>选项表示”设置键的过期时间”（Expire）。</li><li><strong>功能</strong>：当<code>EX</code>选项指定时，<code>SET</code>命令会在设置键值的同时，为键设置一个过期时间（以秒为单位）。键在过期时间到达后将自动删除。</li><li><strong>用途</strong>：用于设置键的自动过期，防止因持有锁或设置值过长时间而导致系统资源泄漏。对于分布式锁，设置锁的过期时间非常重要，以确保锁不会无限期地占用，导致系统无法恢复。</li></ul><h2 id="死锁与避免"><a href="#死锁与避免" class="headerlink" title="死锁与避免"></a>死锁与避免</h2><p>在计算机系统中，死锁的发生通常需要满足以下四个条件，这四个条件被称为死锁的必要条件：</p><ol><li><strong>互斥（Mutual Exclusion）</strong>：资源不能被多个进程（或线程）同时占用，即资源在某一时刻只能被一个进程（或线程）使用。</li><li><strong>占有并等待（Hold and Wait）</strong>：进程（或线程）已经持有一个或多个资源，并且正在等待其他资源，而这些资源被其他进程（或线程）持有。</li><li><strong>非抢占（No Preemption）</strong>：已经分配给进程（或线程）的资源在被释放之前不能被抢占，资源只能在进程（或线程）自愿释放后才能被其他进程（或线程）获取。</li><li><strong>循环等待（Circular Wait）</strong>：存在一个进程（或线程）集合，其中每个进程（或线程）都在等待下一个进程（或线程）释放资源，形成一个循环链。</li></ol><p><strong>预防</strong>就是破坏四个条件。</p><p><strong>避免</strong>可以使用<strong>银行家算法</strong>：在资源请求时，通过检查是否能保证系统处于安全状态来决定是否分配资源。只有在分配资源后系统仍然处于安全状态时，才允许该资源分配。</p><p><strong>检测</strong>使用资源分配图（Resource Allocation Graph）或其他检测机制来检测系统中是否存在死锁。</p><p><strong>恢复</strong>：</p><ul><li><strong>终止进程</strong>：选择一个或多个进程终止，以释放资源。</li><li><strong>资源抢占</strong>：从某些进程中抢占资源，分配给其他进程，以打破死锁循环。</li></ul><h2 id="git切换分支？-复制某个commit？"><a href="#git切换分支？-复制某个commit？" class="headerlink" title="git切换分支？ 复制某个commit？"></a>git切换分支？ 复制某个commit？</h2><p>平常git用的不多。只回答上第一个。</p><p>使用<code>git checkout</code>来切换分支。</p><p>如果你想从某个特定的提交创建一个新的分支：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">git checkout -b &lt;new-branch-name&gt; &lt;commit-hash&gt;</span><br><span class="line">或者</span><br><span class="line">git switch -c &lt;new-branch-name&gt; &lt;commit-hash&gt;</span><br></pre></td></tr></table></figure><p>如果你想将某个提交的更改应用到当前分支，可以使用<code>git cherry-pick</code>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git cherry-pick &lt;commit-hash&gt;</span><br></pre></td></tr></table></figure><h2 id="maven怎么解决依赖冲突"><a href="#maven怎么解决依赖冲突" class="headerlink" title="maven怎么解决依赖冲突"></a>maven怎么解决依赖冲突</h2><p>使用<code>mvn dependency:tree</code>查看冲突依赖，然后在pom文件中使用<code>&lt;exclusions&gt;</code>排除依赖。</p><p>也可以在<code>pom.xml</code>文件中，通过<code>&lt;dependencyManagement&gt;</code>标签来强制使用特定版本的依赖。</p><p>使用<code>mvn dependency:analyze</code>这个命令可以识别未使用和未声明的依赖。</p><h2 id="对称加密和非对称加密算法？有什么优缺点"><a href="#对称加密和非对称加密算法？有什么优缺点" class="headerlink" title="对称加密和非对称加密算法？有什么优缺点"></a>对称加密和非对称加密算法？有什么优缺点</h2><p>对称加密是指加密和解密使用相同的密钥。常见的对称加密算法有AES（Advanced Encryption Standard）、DES（Data Encryption Standard）和3DES（Triple DES）。</p><p><strong>优点</strong></p><ol><li><strong>速度快</strong>：对称加密算法通常比非对称加密算法快，因为它们的计算复杂度较低，适合处理大量数据。</li><li><strong>效率高</strong>：对称加密适用于需要高性能加密的场景，如大数据传输、磁盘加密等。</li><li><strong>资源消耗少</strong>：对称加密的计算资源消耗通常较低，因此适合在资源有限的环境中使用。</li></ol><p><strong>缺点</strong></p><ol><li><p><strong>密钥管理困难</strong>：对称加密需要确保密钥的安全传输和存储，密钥的分发和管理是一个挑战。</p></li><li><p><strong>不适合分布式系统</strong>：在大规模分布式系统中，密钥的管理和分发变得更加复杂，需要额外的机制来确保安全性。</p></li><li><p><strong>密钥泄露风险</strong>：一旦密钥泄露，所有用该密钥加密的数据都可能被解密，因此密钥的保护至关重要。</p></li></ol><p>非对称加密（或称公钥加密）使用一对密钥：公钥和私钥。公钥用于加密，私钥用于解密。常见的非对称加密算法有RSA（Rivest–Shamir–Adleman）、ECC（Elliptic Curve Cryptography）和DSA（Digital Signature Algorithm）。</p><p><strong>优点</strong></p><ol><li><strong>密钥管理简化</strong>：非对称加密解决了密钥分发的问题。公钥可以公开，而私钥保持秘密。</li><li><strong>数字签名</strong>：非对称加密支持数字签名，可以验证数据的真实性和完整性。</li><li><strong>加密密钥交换</strong>：常用于安全的密钥交换，例如在HTTPS中使用非对称加密来安全地交换对称加密密钥。</li></ol><p><strong>缺点</strong></p><ol><li><strong>速度较慢</strong>：非对称加密通常比对称加密慢，因为它需要更复杂的数学运算，适合加密小量数据。</li><li><strong>资源消耗大</strong>：计算和存储资源消耗较高，因此不适合大规模数据的加密。</li><li><strong>计算复杂性</strong>：非对称加密的算法复杂度较高，实施和优化可能较为复杂。</li></ol><h2 id="spring框架有什么设计模式？aop用的是什么？"><a href="#spring框架有什么设计模式？aop用的是什么？" class="headerlink" title="spring框架有什么设计模式？aop用的是什么？"></a>spring框架有什么设计模式？aop用的是什么？</h2><p><strong>单例模式</strong>：通过配置Bean的作用域为<code>singleton</code>（默认值），Spring在启动时创建唯一的Bean实例。</p><p><strong>代理模式</strong>：Spring使用代理模式来实现AOP（面向切面编程）。通过动态代理或CGLIB字节码生成，Spring可以在运行时创建代理对象，并在方法调用前后插入额外的逻辑（如事务管理、日志记录等）。</p><p>模板方法模式： Spring的<code>JdbcTemplate</code>、<code>JmsTemplate</code>和<code>HibernateTemplate</code>等类使用模板方法模式来简化常见的操作。模板方法模式定义了操作的算法框架，将某些步骤的实现延迟到子类。</p><p><strong>工厂模式</strong>：Spring使用工厂模式来创建Bean实例。Bean的创建由Spring容器负责，开发者只需要定义Bean的配置，Spring容器在运行时根据配置创建和管理Bean。</p><p><strong>策略模式</strong>： Spring的许多组件使用策略模式来定义算法的变体，而不需要修改客户端代码。例如，Spring的<code>TransactionManager</code>接口允许不同的事务策略实现，如<code>DataSourceTransactionManager</code>、<code>JpaTransactionManager</code>等。</p><p><strong>观察者模式</strong>：Spring的事件发布和监听机制使用了观察者模式。Spring允许应用程序通过事件发布和监听来实现松耦合的组件交互。<code>ApplicationEvent</code>和<code>ApplicationListener</code>接口，<code>ApplicationContext</code>作为事件发布者，监听器作为观察者。</p><p><strong>装饰器模式</strong>：Spring AOP的一个实现，特别是<code>ProxyFactoryBean</code>，使用了装饰器模式来增强对象的功能，而不需要改变其结构。通过在代理对象上添加额外的功能来装饰原始对象。Spring AOP中的切面（Aspect）和建议（Advice）可以看作是对目标对象的装饰。</p><h2 id="算法题"><a href="#算法题" class="headerlink" title="算法题"></a>算法题</h2><p>输入一个字符串n1, 由数字与”,”分隔，输出这个序列中第二大的数字，以字符串返回。</p><p>还算比较简单，就是先切分字符串，然后用<code>Integer.valueOf</code>变成数字，然后用两个变量记录最大和第二大的数字。</p><p>面试时写的时候忘记考虑没有第二大的数字了。现在让GPT又写了一下才恍然大悟。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 找到最大的和第二大的数字</span></span><br><span class="line"><span class="type">Integer</span> <span class="variable">largest</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line"><span class="type">Integer</span> <span class="variable">secondLargest</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (Integer number : uniqueNumbers) &#123;</span><br><span class="line">    <span class="keyword">if</span> (largest == <span class="literal">null</span> || number &gt; largest) &#123;</span><br><span class="line">        secondLargest = largest;</span><br><span class="line">        largest = number;</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (secondLargest == <span class="literal">null</span> || (number &gt; secondLargest &amp;&amp; number &lt; largest)) &#123;</span><br><span class="line">        secondLargest = number;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> (secondLargest != <span class="literal">null</span>) ? String.valueOf(secondLargest) : <span class="literal">null</span>;</span><br></pre></td></tr></table></figure><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>没想到都是八股文，措手不及，其中最后一个aop用的是什么设计模式我竟然转不过脑子来。</p><p>也有一些没答出来，比如<code>chery-pick</code>，反射也答得不好，sortedSet如何删除也没答出来。</p><p>包括代码编写只面试了半个小时，不知道咋说，准备下一场其他面试吧。。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;复盘&quot;&gt;&lt;a href=&quot;#复盘&quot; class=&quot;headerlink&quot; title=&quot;复盘&quot;&gt;&lt;/a&gt;复盘&lt;/h1&gt;&lt;h2 id=&quot;反射是什么-怎么用&quot;&gt;&lt;a href=&quot;#反射是什么-怎么用&quot; class=&quot;headerlink&quot; title=&quot;反射是什么 怎
      
    
    </summary>
    
    
      <category term="编程" scheme="https://blog.zer0e.com/tags/%E7%BC%96%E7%A8%8B/"/>
    
      <category term="随想" scheme="https://blog.zer0e.com/tags/%E9%9A%8F%E6%83%B3/"/>
    
      <category term="面经" scheme="https://blog.zer0e.com/tags/%E9%9D%A2%E7%BB%8F/"/>
    
  </entry>
  
  <entry>
    <title>2024面试复盘7</title>
    <link href="https://blog.zer0e.com/2024/07/30/2024-07-30-replay/"/>
    <id>https://blog.zer0e.com/2024/07/30/2024-07-30-replay/</id>
    <published>2024-07-30T12:00:00.000Z</published>
    <updated>2024-07-31T08:34:45.169Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>今天面试不咋顺利。</p><p>面试完后得知一个同事去了百度，我既替他高兴，但我自己也很难受。我面试了好多家都不咋顺利，他一说他base只有18，我就知道原来我要价太高了。</p><p>还是挺难受的，我觉得我并不比他差。但也只能接受，有时候面试就是看运气还有眼缘的。</p><h1 id="复盘"><a href="#复盘" class="headerlink" title="复盘"></a>复盘</h1><h2 id="算法题"><a href="#算法题" class="headerlink" title="算法题"></a>算法题</h2><p>算法题是最后问的，我挪到前面来。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">Flight</span> &#123;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 出发城市</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> String cityFrom;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 到达城市</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> String cityTo;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 航班价格</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">int</span> price;</span><br><span class="line">    <span class="comment">/*</span></span><br><span class="line"><span class="comment">     * 航班号</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> String flightNo;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">Flight</span><span class="params">(String cityFrom, String cityTo, <span class="type">int</span> price, String flightNo)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.cityFrom = cityFrom;</span><br><span class="line">        <span class="built_in">this</span>.cityTo = cityTo;</span><br><span class="line">        <span class="built_in">this</span>.price = price;</span><br><span class="line">        <span class="built_in">this</span>.flightNo = flightNo;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">toString</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;Flight&#123;&quot;</span> +</span><br><span class="line">                <span class="string">&quot;cityFrom=&#x27;&quot;</span> + cityFrom + <span class="string">&#x27;\&#x27;&#x27;</span> +</span><br><span class="line">                <span class="string">&quot;, cityTo=&#x27;&quot;</span> + cityTo + <span class="string">&#x27;\&#x27;&#x27;</span> +</span><br><span class="line">                <span class="string">&quot;, price=&quot;</span> + price +</span><br><span class="line">                <span class="string">&quot;, flightNo=&#x27;&quot;</span> + flightNo + <span class="string">&#x27;\&#x27;&#x27;</span> +</span><br><span class="line">                <span class="string">&#x27;&#125;&#x27;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 找出 fromCity 至 toCity 最便宜的航班组合并返回</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> flights</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> fromCity</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> toCity</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> List&lt;Flight&gt; <span class="title function_">searchLowestPriceFlightCombination</span><span class="params">(</span></span><br><span class="line"><span class="params">        Set&lt;Flight&gt; flights,</span></span><br><span class="line"><span class="params">        String fromCity,</span></span><br><span class="line"><span class="params">        String toCity)</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// TODO</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>面试官就发了一道这种题目。</p><p>我就写成了直达做法，但实际上面试官是要考察包括中转的情况。有点尴尬。。我就说怎么这么简单。其实应该用BFS（广度优先）去做的。</p><p>也是请教了GPT.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> List&lt;Flight&gt; <span class="title function_">searchLowestPriceFlightCombination</span><span class="params">(</span></span><br><span class="line"><span class="params">            Set&lt;Flight&gt; flights,</span></span><br><span class="line"><span class="params">            String fromCity,</span></span><br><span class="line"><span class="params">            String toCity)</span> &#123;</span><br><span class="line"></span><br><span class="line">        Map&lt;String, List&lt;Flight&gt;&gt; graph = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">        <span class="keyword">for</span> (Flight flight : flights) &#123;</span><br><span class="line">            graph.computeIfAbsent(flight.cityFrom, k -&gt; <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;()).add(flight);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Queue to hold the paths and their corresponding total price</span></span><br><span class="line">        Queue&lt;List&lt;Flight&gt;&gt; queue = <span class="keyword">new</span> <span class="title class_">LinkedList</span>&lt;&gt;();</span><br><span class="line">        <span class="comment">// Map to hold the minimum cost to reach each city</span></span><br><span class="line">        Map&lt;String, Integer&gt; minCost = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">        minCost.put(fromCity, <span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Start with the fromCity with an empty path</span></span><br><span class="line">        queue.add(<span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;());</span><br><span class="line"></span><br><span class="line">        List&lt;Flight&gt; result = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;();</span><br><span class="line">        <span class="type">int</span> <span class="variable">minTotalCost</span> <span class="operator">=</span> Integer.MAX_VALUE;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">while</span> (!queue.isEmpty()) &#123;</span><br><span class="line">            List&lt;Flight&gt; currentPath = queue.poll();</span><br><span class="line">            <span class="type">String</span> <span class="variable">lastCity</span> <span class="operator">=</span> currentPath.isEmpty() ? fromCity : currentPath.get(currentPath.size() - <span class="number">1</span>).cityTo;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> (lastCity.equals(toCity)) &#123;</span><br><span class="line">                <span class="type">int</span> <span class="variable">currentCost</span> <span class="operator">=</span> currentPath.stream().mapToInt(f -&gt; f.price).sum();</span><br><span class="line">                <span class="keyword">if</span> (currentCost &lt; minTotalCost) &#123;</span><br><span class="line">                    minTotalCost = currentCost;</span><br><span class="line">                    result = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;(currentPath);</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="keyword">continue</span>;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">for</span> (Flight nextFlight : graph.getOrDefault(lastCity, <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;())) &#123;</span><br><span class="line">                <span class="type">int</span> <span class="variable">newCost</span> <span class="operator">=</span> currentPath.stream().mapToInt(f -&gt; f.price).sum() + nextFlight.price;</span><br><span class="line"></span><br><span class="line">                <span class="keyword">if</span> (newCost &lt; minCost.getOrDefault(nextFlight.cityTo, Integer.MAX_VALUE)) &#123;</span><br><span class="line">                    minCost.put(nextFlight.cityTo, newCost);</span><br><span class="line">                    List&lt;Flight&gt; newPath = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;(currentPath);</span><br><span class="line">                    newPath.add(nextFlight);</span><br><span class="line">                    queue.add(newPath);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> result;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>实际上可以使用笛卡斯特拉算法（Dijkstra）来实现。其实就是改成优先级队列去排序。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> List&lt;Flight&gt; <span class="title function_">searchLowestPriceFlightCombination</span><span class="params">(</span></span><br><span class="line"><span class="params">            Set&lt;Flight&gt; flights,</span></span><br><span class="line"><span class="params">            String fromCity,</span></span><br><span class="line"><span class="params">            String toCity)</span> &#123;</span><br><span class="line"></span><br><span class="line">        Map&lt;String, List&lt;Flight&gt;&gt; graph = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">      <span class="keyword">for</span> (Flight flight : flights) &#123;</span><br><span class="line">         graph.computeIfAbsent(flight.cityFrom, k -&gt; <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;()).add(flight);</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="comment">// PriorityQueue to hold the paths and sort by the total price (cost)</span></span><br><span class="line">      PriorityQueue&lt;List&lt;Flight&gt;&gt; queue = <span class="keyword">new</span> <span class="title class_">PriorityQueue</span>&lt;&gt;(Comparator.comparingInt(path -&gt; path.stream().mapToInt(f -&gt; f.price).sum()));</span><br><span class="line">      <span class="comment">// Map to hold the minimum cost to reach each city</span></span><br><span class="line">      Map&lt;String, Integer&gt; minCost = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">      minCost.put(fromCity, <span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">      <span class="comment">// Start with the fromCity with an empty path</span></span><br><span class="line">      queue.add(<span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;());</span><br><span class="line"></span><br><span class="line">      <span class="keyword">while</span> (!queue.isEmpty()) &#123;</span><br><span class="line">         List&lt;Flight&gt; currentPath = queue.poll();</span><br><span class="line">         <span class="type">String</span> <span class="variable">lastCity</span> <span class="operator">=</span> currentPath.isEmpty() ? fromCity : currentPath.get(currentPath.size() - <span class="number">1</span>).cityTo;</span><br><span class="line"></span><br><span class="line">         <span class="keyword">if</span> (lastCity.equals(toCity)) &#123;</span><br><span class="line">            <span class="keyword">return</span> currentPath;</span><br><span class="line">         &#125;</span><br><span class="line"></span><br><span class="line">         <span class="keyword">for</span> (Flight nextFlight : graph.getOrDefault(lastCity, <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;())) &#123;</span><br><span class="line">            <span class="type">int</span> <span class="variable">newCost</span> <span class="operator">=</span> currentPath.stream().mapToInt(f -&gt; f.price).sum() + nextFlight.price;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> (newCost &lt; minCost.getOrDefault(nextFlight.cityTo, Integer.MAX_VALUE)) &#123;</span><br><span class="line">               minCost.put(nextFlight.cityTo, newCost);</span><br><span class="line">               List&lt;Flight&gt; newPath = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;(currentPath);</span><br><span class="line">               newPath.add(nextFlight);</span><br><span class="line">               queue.add(newPath);</span><br><span class="line">            &#125;</span><br><span class="line">         &#125;</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;();</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>看了一下，好像仅仅是队列的区别。</p><p>GPT的回答：</p><p><strong>BFS</strong> 适用于无权图或所有边权重相等的图，简单高效。</p><p><strong>Dijkstra</strong> 适用于加权图，特别是边权重不等的图，更加通用但稍微复杂。</p><h2 id="项目介绍"><a href="#项目介绍" class="headerlink" title="项目介绍"></a>项目介绍</h2><p>问了下项目做啥的。</p><p>还问了代码规模。仔细想了想好像核心代码不过百行，哈哈哈。。。</p><h2 id="项目里用了哪些java相关技术"><a href="#项目里用了哪些java相关技术" class="headerlink" title="项目里用了哪些java相关技术"></a>项目里用了哪些java相关技术</h2><p>spring redis rabbitmq…</p><h2 id="Java异步框架？"><a href="#Java异步框架？" class="headerlink" title="Java异步框架？"></a>Java异步框架？</h2><p>之前了解过vert.x框架。jdk自带的有CompletableFuture，还有经常听到的Netty。</p><h2 id="如何理解异步编程，阻塞非阻塞，同步非同步"><a href="#如何理解异步编程，阻塞非阻塞，同步非同步" class="headerlink" title="如何理解异步编程，阻塞非阻塞，同步非同步"></a>如何理解异步编程，阻塞非阻塞，同步非同步</h2><p>异步编程是一种编程方式，它允许程序在执行某些操作（如I/O操作、网络请求或长时间运行的计算任务）时，不阻塞当前线程，从而可以同时处理其他任务。这种方式可以显著提高程序的性能和响应能力，尤其是在需要处理大量并发任务时</p><p><strong>同步 vs 异步</strong>:</p><ul><li><strong>同步</strong>: 当一个任务开始时，必须等待其完成后才能继续执行后续任务。执行是按顺序进行的，任务之间存在依赖性。</li><li><strong>异步</strong>: 当一个任务开始时，可以继续执行其他任务而不必等待该任务完成。当该任务完成时，会通过回调、消息或其他机制通知主程序。</li></ul><p><strong>阻塞 vs 非阻塞</strong>:</p><ul><li><strong>阻塞</strong>: 在等待某个操作（如I/O操作）完成之前，线程被暂停，无法执行其他操作。</li><li><strong>非阻塞</strong>: 线程在等待操作完成时仍然可以继续执行其他操作。</li></ul><h3 id="异步编程的机制"><a href="#异步编程的机制" class="headerlink" title="异步编程的机制"></a>异步编程的机制</h3><ol><li><strong>回调（Callback）</strong>: 传递一个函数作为参数，当异步操作完成时调用该函数。例如，JavaScript中的异步函数经常使用回调。</li><li><strong>Promise/Future</strong>: 表示一个将来可能会完成的操作，并允许你在操作完成时获取结果。Java的 <code>CompletableFuture</code> 和 JavaScript 的 <code>Promise</code> 是典型例子。</li><li><strong>Async/Await</strong>: 提供一种更简洁和可读的方式来编写异步代码，通过将异步操作的结果等待并返回。JavaScript和C#中都有这种机制。</li></ol><h2 id="Java-NIO"><a href="#Java-NIO" class="headerlink" title="Java NIO"></a>Java NIO</h2><p>Java NIO（New Input/Output）是Java 1.4中引入的一组API，也叫做Non-blocking I/O。用于替代传统的Java I/O。它提供了一种更高效的I/O操作方式，特别适合处理大量并发连接和大数据量的读写操作。NIO主要通过非阻塞I/O和缓冲区来提高I/O操作的效率和性能。</p><p><strong>主要概念和组件</strong></p><ol><li><strong>缓冲区（Buffer）</strong>: 一个线性数组，用于存储数据。NIO中的所有数据都是用缓冲区处理的。常见的缓冲区类型有 <code>ByteBuffer</code>、<code>CharBuffer</code>、<code>IntBuffer</code> 等。</li><li><strong>通道（Channel）</strong>: 一个比传统的 <code>InputStream</code> 和 <code>OutputStream</code> 更高效的I/O抽象。通道可以异步地读写数据。常见的通道有 <code>FileChannel</code>、<code>SocketChannel</code>、<code>ServerSocketChannel</code> 和 <code>DatagramChannel</code>。</li><li><strong>选择器（Selector）</strong>: 一个对象，可以检测一个或多个通道的状态（如是否准备好读、写等）。选择器使得单个线程可以管理多个通道，从而实现高效的非阻塞I/O操作。</li><li><strong>选择键（SelectionKey）</strong>: 选择器和通道之间的连接器。当通道准备就绪时，选择键会被选择器选择并返回。</li></ol><p><strong>工作原理</strong></p><p>NIO通过以下几个核心组件协同工作来实现非阻塞I/O：</p><ol><li><strong>通道（Channel）和缓冲区（Buffer）</strong>:<ul><li>数据读写都是通过缓冲区进行的。通道读取数据到缓冲区，或者将缓冲区中的数据写入通道。</li><li>缓冲区有几个重要的属性：容量（capacity）、位置（position）和限制（limit），用于控制数据读写的范围和进度。</li></ul></li><li><strong>非阻塞模式</strong>:<ul><li>通道可以配置为非阻塞模式，这意味着I/O操作（如读写）可以立即返回，而不会阻塞当前线程。如果操作不能立即完成，它会返回零或负数，而不是阻塞等待。</li></ul></li><li><strong>选择器（Selector）</strong>:<ul><li>选择器允许一个线程管理多个通道。通过注册通道到选择器并在通道准备好执行某些操作时被通知，选择器使得服务器可以有效地处理大量并发连接。</li></ul></li></ol><h2 id="select-和-epoll"><a href="#select-和-epoll" class="headerlink" title="select 和 epoll"></a>select 和 epoll</h2><p><code>select</code> 和 <code>epoll</code> 是两种不同的I/O多路复用机制，用于在一个线程中管理多个I/O操作。它们都用于监视一组文件描述符（如网络套接字）并等待其中的一个或多个准备好进行I/O操作。</p><p><code>select</code> 是一个较早期的I/O多路复用机制，几乎在所有Unix-like系统上都可以使用，包括Linux和BSD。它通过一个固定大小的文件描述符集合来检测哪些描述符准备好进行I/O操作。</p><p><strong>特点</strong>：</p><ol><li><strong>简单且广泛支持</strong>：几乎所有的Unix-like系统都支持<code>select</code>，因此具有很好的兼容性。</li><li><strong>固定大小的描述符集合</strong>：<code>select</code> 使用一个固定大小的数组来存储文件描述符，在Linux上通常限制为1024个文件描述符。</li><li><strong>性能问题</strong>：当文件描述符数量很大时，<code>select</code>的性能会下降，因为它每次都需要扫描整个描述符集合。</li></ol><p><code>epoll</code> 是Linux特有的I/O多路复用机制，是<code>select</code>和<code>poll</code>的改进版本，旨在提高大规模并发连接的性能。<code>epoll</code> 使用事件驱动的机制，适用于处理大量的文件描述符。</p><p><strong>特点</strong>：</p><ol><li><strong>高效性</strong>：<code>epoll</code> 在处理大量文件描述符时效率更高，因为它只在文件描述符状态发生变化时才进行处理，而不是扫描整个描述符集合。</li><li><strong>动态大小的描述符集合</strong>：<code>epoll</code> 支持动态大小的文件描述符集合，没有固定的限制。</li><li><strong>边缘触发和水平触发</strong>：<code>epoll</code> 提供两种工作模式，边缘触发（edge-triggered, ET）和水平触发（level-triggered, LT），其中边缘触发模式适用于高性能场景。</li></ol><p><strong>适用场景</strong></p><ul><li><code>select</code>：适用于需要广泛兼容性的应用程序，或文件描述符数量较少的场景。</li><li><code>epoll</code>：适用于Linux系统上需要处理大量并发连接的高性能服务器应用程序。</li></ul><h2 id="多线程例子"><a href="#多线程例子" class="headerlink" title="多线程例子"></a>多线程例子</h2><p>用线程池管理线程。</p><h2 id="线程池的参数"><a href="#线程池的参数" class="headerlink" title="线程池的参数"></a>线程池的参数</h2><p>核心线程数？QPS依据？</p><p>CPU密集型任务一般为核心数+1，IO密集型任务一般2N-4N，具体根据系统负载，QPS等依据灵活调整。</p><p>其他参数之前复盘都有写过。</p><h2 id="线程安全的工具"><a href="#线程安全的工具" class="headerlink" title="线程安全的工具"></a>线程安全的工具</h2><p>synchronized，java.util.concurrent中的AQS。</p><p>其中AQS的核心原理之前也看过，这里再让GPT总结下：</p><p>AQS 的核心原理是基于一个 FIFO（先入先出）等待队列来管理线程的获取和释放锁的操作。它通过内置的 <code>state</code> 变量以及一些低层的 CAS 操作和锁条件变量实现高效的线程同步。</p><p><strong>State 变量</strong>:</p><ul><li><code>state</code> 是一个 <code>int</code> 类型的变量，表示共享资源的状态。其含义取决于具体的同步器实现。例如，在 <code>ReentrantLock</code> 中，<code>state</code> 表示锁的持有计数；在 <code>CountDownLatch</code> 中，<code>state</code> 表示倒计时计数。</li><li>访问和修改 <code>state</code> 需要通过 AQS 提供的 <code>getState</code>、<code>setState</code> 和 <code>compareAndSetState</code> 方法，这些方法保证了对 <code>state</code> 的原子操作。</li></ul><p><strong>FIFO 队列</strong>:</p><ul><li>AQS 使用一个双向链表来实现等待队列，每个节点（Node）表示一个等待线程。节点中保存了线程的引用及其等待状态。</li><li>等待队列中的线程会被阻塞，当锁资源可用时，线程会被唤醒并重新竞争锁。</li></ul><p><strong>独占锁与共享锁</strong>:</p><ul><li><strong>独占模式（Exclusive Mode）</strong>：一个线程独占资源。例如，<code>ReentrantLock</code> 就是独占模式。</li><li><strong>共享模式（Shared Mode）</strong>：多个线程可以共享资源。例如，<code>Semaphore</code> 和 <code>CountDownLatch</code> 是共享模式。</li></ul><h2 id="ThreadLocal用过吗"><a href="#ThreadLocal用过吗" class="headerlink" title="ThreadLocal用过吗"></a>ThreadLocal用过吗</h2><p>讲了MDC的使用。</p><h2 id="父子线程ThreadLocal"><a href="#父子线程ThreadLocal" class="headerlink" title="父子线程ThreadLocal"></a>父子线程ThreadLocal</h2><p>可以使用 <code>InheritableThreadLocal</code> 。但在线程池中，由于线程会被重用，<code>InheritableThreadLocal</code> 的值可能会被意外共享，导致不正确的行为。</p><p>可以使用阿里开源的<strong>transmittableThreadLocal</strong></p><p><strong>TransmittableThreadLocal 的特点和优势</strong></p><ol><li><strong>线程池支持</strong>：<ul><li>解决了 <code>InheritableThreadLocal</code> 在线程池中使用时可能出现的变量污染问题。<code>TransmittableThreadLocal</code> 可以在任务提交到线程池时传递上下文，并在任务执行结束后恢复原始上下文。</li></ul></li><li><strong>上下文一致性</strong>：<ul><li>确保在线程池中使用时，上下文信息能够正确地传递到子线程，即使线程被复用也不会污染其他任务的上下文。</li></ul></li><li><strong>扩展性</strong>：<ul><li>提供了 <code>TtlRunnable</code> 和 <code>TtlCallable</code> 类，用于包装任务，确保上下文的正确传递。</li></ul></li></ol><h2 id="ThreadLocal的底层实现"><a href="#ThreadLocal的底层实现" class="headerlink" title="ThreadLocal的底层实现"></a>ThreadLocal的底层实现</h2><p><strong>ThreadLocalMap</strong>:</p><ul><li>每个线程中有一个 <code>ThreadLocal.ThreadLocalMap</code> 实例来存储线程局部变量。<code>ThreadLocalMap</code> 是一个专门为线程局部变量设计的内存映射表，主要通过线程的 <code>ThreadLocal</code> 对象作为键，通过 <code>ThreadLocalMap</code> 存储实际的值。</li></ul><p><strong>ThreadLocalMap 结构</strong>:</p><ul><li><p><code>ThreadLocalMap</code></p><p> 是一个数组，每个元素是一个 <code>ThreadLocalMap.Entry</code></p><p> 对象。Entry类中包含两个字段：</p><ul><li><code>ThreadLocal</code> 对象的引用（作为键）</li><li>线程局部变量的值（作为值）</li></ul></li></ul><h2 id="最新的JDK有什么特性吗"><a href="#最新的JDK有什么特性吗" class="headerlink" title="最新的JDK有什么特性吗"></a>最新的JDK有什么特性吗</h2><p>回答了虚拟线程。追问有什么优势。线程间切换会快一点。</p><h2 id="G1特性"><a href="#G1特性" class="headerlink" title="G1特性"></a>G1特性</h2><p><strong>分代收集</strong>:</p><ul><li>G1 是基于分代收集的，即堆内存被划分为多个区域（Region）。这些区域可以属于年轻代（Young Generation）、老年代（Old Generation）或永久代（Metaspace）。G1 通过动态调整这些区域的大小和数量来优化内存回收。</li></ul><p><strong>区域化堆内存</strong>:</p><ul><li>堆内存被划分成多个相等大小的区域（Region），这些区域用于不同的目的（年轻代、老年代、和空闲区域）。这种区域化使得 G1 可以根据需求进行灵活的内存管理。</li></ul><p><strong>增量式收集</strong>:</p><ul><li>G1 采用增量式的垃圾回收策略。它将堆分成多个区域，并按需回收这些区域，而不是一次性回收整个年轻代或老年代。这种方式减少了垃圾回收的暂停时间，降低了应用程序的停顿时间。</li></ul><p><strong>并行和并发收集</strong>:</p><ul><li>G1 支持多线程并行和并发回收。这意味着垃圾回收工作可以并行执行，从而减少了垃圾回收的总停顿时间。G1 的并发标记阶段（Concurrent Marking）减少了应用程序的停顿时间。</li></ul><p><strong>预测性停顿时间</strong>:</p><ul><li>G1 设计目标之一是提供预测性停顿时间。G1 可以通过配置参数（如 <code>-XX:MaxGCPauseMillis</code>）来控制垃圾回收的最大停顿时间。这使得 G1 可以在一定范围内保证垃圾回收的停顿时间不会超出指定的阈值。</li></ul><p><strong>回收优先级</strong>:</p><ul><li>G1 使用了回收优先级策略，它会首先回收那些垃圾最多的区域，从而提高垃圾回收效率。G1 会评估每个区域的回收收益，以决定哪个区域最值得回收。</li></ul><p><strong>混合回收</strong>:</p><ul><li>G1 在进行年轻代垃圾回收时，可以同时回收老年代的部分区域。这种混合回收机制有助于减少老年代中的垃圾量，从而减轻后续的老年代回收负担。</li></ul><p><strong>全堆回收</strong>:</p><ul><li>在需要全堆回收的情况下，G1 会执行一次全堆回收（Full GC）。G1 的全堆回收也会尽可能地减少停顿时间，并且会回收年轻代和老年代的所有垃圾。</li></ul><p><strong>自适应调整</strong>:</p><ul><li>G1 可以自适应调整堆的区域划分，以优化垃圾回收性能。它会根据堆的使用情况和垃圾回收的需求来动态调整区域的大小和数量。</li></ul><p>要使用 G1 垃圾回收器，需要在 JVM 启动时指定 <code>-XX:+UseG1GC</code>。此外，还有一些配置参数可以用来调整 G1 的行为：</p><ul><li><code>-XX:MaxGCPauseMillis=&lt;n&gt;</code>: 目标最大垃圾回收停顿时间（毫秒）。</li><li><code>-XX:G1HeapRegionSize=&lt;size&gt;</code>: 指定 G1 区域的大小（如 1m、2m、4m、8m）。</li><li><code>-XX:ParallelGCThreads=&lt;n&gt;</code>: 设置并行垃圾回收线程的数量。</li><li><code>-XX:ConcGCThreads=&lt;n&gt;</code>: 设置并发标记阶段的线程数量。</li><li><code>-XX:G1ReservePercent=&lt;n&gt;</code>: 设置 G1 在堆中保留的区域百分比，用于防止频繁的 Full GC。</li></ul><h2 id="java的内存分区"><a href="#java的内存分区" class="headerlink" title="java的内存分区"></a>java的内存分区</h2><h3 id="1-程序计数器-Program-Counter-Register"><a href="#1-程序计数器-Program-Counter-Register" class="headerlink" title="1. 程序计数器 (Program Counter Register)"></a>1. 程序计数器 (Program Counter Register)</h3><ul><li><strong>功能</strong>: 程序计数器（PC 寄存器）是一个指向当前执行线程的字节码指令的指针。每个线程都有一个独立的程序计数器。</li><li><strong>用途</strong>: 在多线程环境下，用于跟踪线程的执行位置。它不参与垃圾回收。</li></ul><h3 id="2-虚拟机栈-Java-Stack"><a href="#2-虚拟机栈-Java-Stack" class="headerlink" title="2. 虚拟机栈 (Java Stack)"></a>2. 虚拟机栈 (Java Stack)</h3><ul><li><strong>功能</strong>: 虚拟机栈用于管理线程的局部变量、操作数栈、动态链接、方法返回地址等。每个线程都有自己的虚拟机栈。</li><li><strong>用途</strong>: 支持方法调用和返回。每个方法在调用时会创建一个栈帧（Stack Frame），存储方法的局部变量、操作数栈等信息。</li><li><strong>大小</strong>: 可通过 <code>-Xss</code> 参数设置栈大小。</li></ul><h3 id="3-本地方法栈-Native-Method-Stack"><a href="#3-本地方法栈-Native-Method-Stack" class="headerlink" title="3. 本地方法栈 (Native Method Stack)"></a>3. 本地方法栈 (Native Method Stack)</h3><ul><li><strong>功能</strong>: 本地方法栈用于支持 native 方法的执行，它与虚拟机栈类似，但专门用于处理 native 方法的调用。</li><li><strong>用途</strong>: 用于与本地方法（由 Java Native Interface, JNI 提供）进行交互。</li><li><strong>大小</strong>: 可通过 <code>-Xss</code> 参数设置栈大小。</li></ul><h3 id="4-堆内存-Heap"><a href="#4-堆内存-Heap" class="headerlink" title="4. 堆内存 (Heap)"></a>4. 堆内存 (Heap)</h3><ul><li><strong>功能</strong>: 堆是 JVM 中最大的一块内存区域，用于存储所有的对象实例和数组。垃圾回收器主要在堆上进行垃圾回收。</li><li><strong>分区:</strong> <ul><li><strong>年轻代 (Young Generation)</strong> 包含新创建的对象，分为三个部分：<ul><li><strong>Eden 区</strong>: 新生对象首先被分配到 Eden 区。</li><li><strong>From Survivor 区</strong>: 对象经过一次或多次垃圾回收后，从 Eden 区晋升到 From Survivor 区。</li><li><strong>To Survivor 区</strong>: 另一个 Survivor 区，用于交换对象。</li></ul></li><li><strong>老年代 (Old Generation)</strong>: 包含经过多次垃圾回收仍然存活的对象。长期存活的对象最终会被晋升到老年代。</li><li><strong>永久代 (PermGen) / 元空间 (Metaspace)</strong><ul><li><strong>PermGen</strong>: Java 8 之前用于存储类元数据和常量池。</li><li><strong>Metaspace</strong>: 从 Java 8 开始取代 PermGen，用于存储类的元数据，动态生成的类和其他元数据。Metaspace 存储在本地内存中，而不是堆中。</li></ul></li></ul></li><li><strong>用途</strong>: 支持对象的动态分配和垃圾回收。</li><li><strong>大小</strong>: 可以通过 <code>-Xmx</code> 和 <code>-Xms</code> 参数设置堆的最大值和初始值，<code>-XX:MaxPermSize</code> 用于设置 PermGen 大小（Java 7 及之前）。</li></ul><h3 id="5-运行时常量池-Runtime-Constant-Pool"><a href="#5-运行时常量池-Runtime-Constant-Pool" class="headerlink" title="5. 运行时常量池 (Runtime Constant Pool)"></a>5. 运行时常量池 (Runtime Constant Pool)</h3><ul><li><strong>功能</strong>: 运行时常量池是方法区的一部分，用于存储类、字段、方法、字符串等常量。</li><li><strong>用途</strong>: 支持类的常量值和字符串常量。</li><li><strong>大小</strong>: 由 JVM 管理，通常不需要手动设置。</li></ul><h3 id="6-直接内存-Direct-Memory"><a href="#6-直接内存-Direct-Memory" class="headerlink" title="6. 直接内存 (Direct Memory)"></a>6. 直接内存 (Direct Memory)</h3><ul><li><strong>功能</strong>: 直接内存是 JVM 之外的内存区域，用于存储与 I/O 操作相关的数据，如 NIO 的缓冲区。</li><li><strong>用途</strong>: 提供与操作系统的直接交互，减少内存复制，提高性能。</li><li><strong>大小</strong>: 可以通过 <code>-XX:MaxDirectMemorySize</code> 参数设置最大直接内存大小。</li></ul><h2 id="堆外内存"><a href="#堆外内存" class="headerlink" title="堆外内存"></a>堆外内存</h2><p>堆外内存（Off-Heap Memory）指的是不属于 Java 堆内存的一块内存区域。与堆内存不同，堆外内存是由应用程序直接管理的，并且不受到 Java 垃圾回收器的管理。堆外内存通常用于存储需要大量内存或需要频繁访问的高性能数据，例如大缓存、直接内存缓冲区等。</p><p>Java NIO 提供了 <code>ByteBuffer</code> 类，可以通过 <code>ByteBuffer.allocateDirect()</code> 方法分配直接内存。这种方法分配的内存不受 Java 堆管理，并且可以直接用于 I/O 操作。</p><h2 id="字节码技术"><a href="#字节码技术" class="headerlink" title="字节码技术"></a>字节码技术</h2><p>JVMTI？JVMTI 全程 JVM Tool Interface，它是Java虚拟机定义的一个开发和监控JVM使用的程序接口(programing interface),通过该接口可以探查JVM内部的一些运行状态，甚至控制JVM应用程序的执行。</p><p>修改字节码可以使用ASM库或 Javassist，还有动态代理中使用cglib也是一种字节码技术，但底层依旧使用的ASM库。</p><h2 id="平常会写单侧-UT-吗"><a href="#平常会写单侧-UT-吗" class="headerlink" title="平常会写单侧(UT)吗"></a>平常会写单侧(UT)吗</h2><p>junit，Mockito</p><h2 id="SPI机制"><a href="#SPI机制" class="headerlink" title="SPI机制"></a>SPI机制</h2><p>SPI 即 Service Provider Interface ，字面意思就是：“服务提供者的接口”，我的理解是：专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。</p><p>SPI 将服务接口和具体的服务实现分离开来，将服务调用方和服务实现者解耦，能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。</p><p>很多框架都使用了 Java 的 SPI 机制，比如：Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。</p><p>通过 SPI 机制能够大大地提高接口设计的灵活性，但是 SPI 机制也存在一些缺点，比如：</p><ul><li>需要遍历加载所有的实现类，不能做到按需加载，这样效率还是相对较低的。</li><li>当多个 <code>ServiceLoader</code> 同时 <code>load</code> 时，会有并发问题。</li></ul><h2 id="Redis用过哪些数据结构"><a href="#Redis用过哪些数据结构" class="headerlink" title="Redis用过哪些数据结构"></a>Redis用过哪些数据结构</h2><h3 id="1-字符串-String"><a href="#1-字符串-String" class="headerlink" title="1.字符串 (String)"></a>1.<strong>字符串 (String)</strong></h3><ul><li><strong>描述</strong>: 最基本的数据类型，可以存储任意形式的数据，例如文本、数字、二进制数据等。</li><li>操作<ul><li><code>SET key value</code>: 设置键的值。</li><li><code>GET key</code>: 获取键的值。</li><li><code>INCR key</code>: 对键的值进行递增操作。</li><li><code>APPEND key value</code>: 在现有值的末尾追加字符串。</li></ul></li></ul><h3 id="2-哈希-Hash"><a href="#2-哈希-Hash" class="headerlink" title="2. 哈希 (Hash)"></a>2. <strong>哈希 (Hash)</strong></h3><ul><li><strong>描述</strong>: 存储键值对的集合，适合存储对象的属性数据。</li><li>操作<ul><li><code>HSET key field value</code>: 设置哈希表中的字段值。</li><li><code>HGET key field</code>: 获取哈希表中指定字段的值。</li><li><code>HGETALL key</code>: 获取哈希表中所有字段及其值。</li><li><code>HDEL key field</code>: 删除哈希表中的指定字段。</li></ul></li></ul><h3 id="3-列表-List"><a href="#3-列表-List" class="headerlink" title="3. 列表 (List)"></a>3. <strong>列表 (List)</strong></h3><ul><li><strong>描述</strong>: 有序的字符串集合，可以在头部和尾部添加元素，适合用作队列或栈。</li><li>操作<ul><li><code>LPUSH key value</code>: 将值插入到列表的左侧。</li><li><code>RPUSH key value</code>: 将值插入到列表的右侧。</li><li><code>LPOP key</code>: 移除并获取列表的左侧第一个元素。</li><li><code>RPOP key</code>: 移除并获取列表的右侧第一个元素。</li><li><code>LRANGE key start stop</code>: 获取列表中指定范围的元素。</li></ul></li></ul><h3 id="4-集合-Set"><a href="#4-集合-Set" class="headerlink" title="4. 集合 (Set)"></a>4. <strong>集合 (Set)</strong></h3><ul><li><strong>描述</strong>: 无序的字符串集合，集合中的元素是唯一的。</li><li>操作<ul><li><code>SADD key member</code>: 向集合添加元素。</li><li><code>SREM key member</code>: 从集合中移除元素。</li><li><code>SMEMBERS key</code>: 获取集合中的所有元素。</li><li><code>SISMEMBER key member</code>: 检查元素是否存在于集合中。</li></ul></li></ul><h3 id="5-有序集合-Sorted-Set"><a href="#5-有序集合-Sorted-Set" class="headerlink" title="5. 有序集合 (Sorted Set)"></a>5. <strong>有序集合 (Sorted Set)</strong></h3><ul><li><strong>描述</strong>: 类似于集合，但每个元素都关联一个浮点数的分数。元素按分数排序，并且元素是唯一的。</li><li>操作<ul><li><code>ZADD key score member</code>: 向有序集合添加元素和分数。</li><li><code>ZRANGE key start stop [WITHSCORES]</code>: 获取有序集合中指定范围的元素。</li><li><code>ZREM key member</code>: 从有序集合中移除元素。</li><li><code>ZSCORE key member</code>: 获取元素的分数。</li></ul></li></ul><h3 id="6-位图-Bitmap"><a href="#6-位图-Bitmap" class="headerlink" title="6. 位图 (Bitmap)"></a>6. <strong>位图 (Bitmap)</strong></h3><ul><li><strong>描述</strong>: 用于处理大量的二进制位，通常用于统计、标记等。</li><li>操作<ul><li><code>SETBIT key offset value</code>: 设置位图中指定偏移量的位。</li><li><code>GETBIT key offset</code>: 获取位图中指定偏移量的位。</li><li><code>BITCOUNT key [start end]</code>: 计算位图中设置为1的位的数量。</li></ul></li></ul><h3 id="7-超日志-HyperLogLog"><a href="#7-超日志-HyperLogLog" class="headerlink" title="7. 超日志 (HyperLogLog)"></a>7. <strong>超日志 (HyperLogLog)</strong></h3><ul><li><strong>描述</strong>: 用于估算唯一元素的数量，具有固定的内存占用，不受数据量大小的影响。</li><li>操作<ul><li><code>PFADD key element [element ...]</code>: 将元素添加到 HyperLogLog 中。</li><li><code>PFCOUNT key [key ...]</code>: 获取 HyperLogLog 的基数估算值。</li></ul></li></ul><h3 id="8-地理位置-Geospatial"><a href="#8-地理位置-Geospatial" class="headerlink" title="8. 地理位置 (Geospatial)"></a>8. <strong>地理位置 (Geospatial)</strong></h3><ul><li><strong>描述</strong>: 用于存储地理位置数据和进行地理空间操作。</li><li>操作<ul><li><code>GEOADD key longitude latitude member</code>: 添加地理位置数据。</li><li><code>GEOPOS key member [member ...]</code>: 获取地理位置的坐标。</li><li><code>GEORADIUS key longitude latitude radius unit [WITHDIST|WITHCOORD|WITHHASH]</code>: 查询地理位置数据。</li></ul></li></ul><h3 id="9-流-Stream"><a href="#9-流-Stream" class="headerlink" title="9. 流 (Stream)"></a>9. <strong>流 (Stream)</strong></h3><ul><li><strong>描述</strong>: 用于处理日志数据和消息队列，支持高效的消息流处理。</li><li>操作<ul><li><code>XADD key id field value [field value ...]</code>: 向流中添加一条记录。</li><li><code>XREAD [BLOCK milliseconds] [COUNT count] STREAMS key [key ...]</code>: 从流中读取数据。</li><li><code>XDEL key id [id ...]</code>: 从流中删除一条记录。</li></ul></li></ul><h3 id="10-事务-Transaction"><a href="#10-事务-Transaction" class="headerlink" title="10. 事务 (Transaction)"></a>10. <strong>事务 (Transaction)</strong></h3><ul><li><strong>描述</strong>: 允许将多个 Redis 命令打包成一个事务并一起执行，保证事务的原子性。</li><li>操作<ul><li><code>MULTI</code>: 开始事务。</li><li><code>EXEC</code>: 执行事务中的所有命令。</li><li><code>DISCARD</code>: 放弃事务，清除事务中的所有命令。</li></ul></li></ul><h3 id="11-发布-订阅-Pub-Sub"><a href="#11-发布-订阅-Pub-Sub" class="headerlink" title="11. 发布/订阅 (Pub/Sub)"></a>11. <strong>发布/订阅 (Pub/Sub)</strong></h3><ul><li><strong>描述</strong>: 实现消息的发布和订阅，允许消息在不同客户端之间传递。</li><li>操作<ul><li><code>PUBLISH channel message</code>: 向频道发布消息。</li><li><code>SUBSCRIBE channel [channel ...]</code>: 订阅频道，接收消息。</li><li><code>UNSUBSCRIBE [channel [channel ...]]</code>: 取消订阅频道。</li></ul></li></ul><h2 id="sorted-set底层的数据结构"><a href="#sorted-set底层的数据结构" class="headerlink" title="sorted set底层的数据结构"></a>sorted set底层的数据结构</h2><p>我回答了最大堆最小堆，笑死。</p><p>实际上是调表和哈希表。</p><h4 id="跳表-Skip-List"><a href="#跳表-Skip-List" class="headerlink" title="跳表 (Skip List)"></a><strong>跳表 (Skip List)</strong></h4><ul><li><strong>描述</strong>: 跳表是一种基于概率的数据结构，它是一种带有多级索引的链表，用于在有序序列中快速查找、插入和删除元素。跳表可以视为一种分层的链表，每一层都是一个有序的链表。</li><li><strong>作用</strong>: 在 Redis 的有序集合中，跳表用于存储集合中的元素及其分数，并支持快速的排序操作。</li><li>优势<ul><li><strong>平均时间复杂度</strong>: 跳表的查找、插入和删除操作平均时间复杂度为 O(log N)，其中 N 是跳表中的元素数量。</li><li><strong>空间复杂度</strong>: 跳表的空间复杂度为 O(N)，在存储多层索引的情况下略高于线性链表。</li></ul></li></ul><h2 id="反问：岗位职责，技术团队架构"><a href="#反问：岗位职责，技术团队架构" class="headerlink" title="反问：岗位职责，技术团队架构"></a>反问：岗位职责，技术团队架构</h2><p>人家做BFF的，其实就是聚合层。负责前后端的对接，将后端多个服务的数据整合返回给前端。</p><h1 id="后话"><a href="#后话" class="headerlink" title="后话"></a>后话</h1><p>复盘完，今天是7月31号，是我离职的第57天，心中苦闷的心情又增加了，很难受。一个是对未来的迷茫，另一个是对自己找工作的苦恼。有人问我后悔当初离职吗，我想我既不后悔也有点后悔，一是每当想到原来的工作我就难以接受，后悔也是源自于自己找工作一个月（实际上应该是3周左右）没有收获的后悔。但是综合来说我并没有后悔当初选择离职，就是头铁哈哈哈哈。</p><p>26年人生至今令我感到后悔的事情其实屈指可数，我一直觉得既然做出了选择那么就为自己的选择买单，这是一个成年人应该有的责任，而不是事后去后悔。</p><p>与之相对的，是我在6月11号写的离职感悟，那时候我写到自己的压力还可以，但是我并不知道后面会发生什么，也没经历过离职，也不知道原来重新找工作真的很难很累，更不知道直接离职再找工作是多么艰辛，不过有一说一，让我在职去寻找工作那岂不是更难。</p><p>现在压力逐渐上来了，虽然我并不是特别缺钱，但是一是长时间没工作不太习惯（但是挺爽，也会写点代码），二是很多HR也都会提前问是不是离职了，离职原因是啥，三是听前同事说某某公司又降薪了，其实也是侧面反馈环境不太好，岗位越来越少。</p><p>心态已经有点不正了，我也总怀疑从上班的时候我就已经抑郁了，如今很可能加重了。但是人生不应该只有上班，前领导也劝我要不去考个研，实在不行明年可以再回来，我没回复他，我知道我不能像某个同事那样，出去了然后再二进宫，我拉不下脸。即便很难我也会默默撑下去。</p><p>机会总会有的，我相信。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;今天面试不咋顺利。&lt;/p&gt;
&lt;p&gt;面试完后得知一个同事去了百度，我既替他高兴，但我自己也很难受。我面试了好多家都不咋顺利，他一说他base只
      
    
    </summary>
    
    
      <category term="编程" scheme="https://blog.zer0e.com/tags/%E7%BC%96%E7%A8%8B/"/>
    
      <category term="随想" scheme="https://blog.zer0e.com/tags/%E9%9A%8F%E6%83%B3/"/>
    
      <category term="面经" scheme="https://blog.zer0e.com/tags/%E9%9D%A2%E7%BB%8F/"/>
    
  </entry>
  
  <entry>
    <title>【架构之路12】es-k8s搭建</title>
    <link href="https://blog.zer0e.com/2024/07/27/devops12/"/>
    <id>https://blog.zer0e.com/2024/07/27/devops12/</id>
    <published>2024-07-27T06:30:00.000Z</published>
    <updated>2024-07-27T06:47:16.018Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>遥想一年前，搭建es时还需要先挂载卷然后hung住容器拷贝配置文件，后面是自己做了镜像。</p><p>再到后面切换到k8s后，有了configMap搭建稍微简单了一些。</p><p>再到现在，由于要使用es做数据分析，决定搭建一个es节点测试使用。</p><p>翻看文档发现官方其实针对k8s有<a href="https://www.elastic.co/guide/en/cloud-on-k8s/current/k8s-deploy-eck.html">ECK</a>来辅助在k8s上搭建es集群。有点类似之前文章提到的tidb operator。</p><p>今天来快速搭建一个es和kibana.</p><h1 id="正文"><a href="#正文" class="headerlink" title="正文"></a>正文</h1><h2 id="准备"><a href="#准备" class="headerlink" title="准备"></a>准备</h2><p>分别创建CRDs和RBAC.</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">kubectl</span> <span class="string">create</span> <span class="string">-f</span> <span class="string">https://download.elastic.co/downloads/eck/2.13.0/crds.yaml</span></span><br><span class="line"><span class="string">kubectl</span> <span class="string">apply</span> <span class="string">-f</span> <span class="string">https://download.elastic.co/downloads/eck/2.13.0/operator.yaml</span></span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">customresourcedefinition.apiextensions.k8s.io/agents.agent.k8s.elastic.co created</span><br><span class="line">customresourcedefinition.apiextensions.k8s.io/apmservers.apm.k8s.elastic.co created</span><br><span class="line">customresourcedefinition.apiextensions.k8s.io/beats.beat.k8s.elastic.co created</span><br><span class="line">customresourcedefinition.apiextensions.k8s.io/elasticmapsservers.maps.k8s.elastic.co created</span><br><span class="line">customresourcedefinition.apiextensions.k8s.io/elasticsearches.elasticsearch.k8s.elastic.co created</span><br><span class="line">customresourcedefinition.apiextensions.k8s.io/enterprisesearches.enterprisesearch.k8s.elastic.co created</span><br><span class="line">customresourcedefinition.apiextensions.k8s.io/kibanas.kibana.k8s.elastic.co created</span><br><span class="line">customresourcedefinition.apiextensions.k8s.io/logstashes.logstash.k8s.elastic.co created</span><br></pre></td></tr></table></figure><h2 id="es搭建"><a href="#es搭建" class="headerlink" title="es搭建"></a>es搭建</h2><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">elasticsearch.k8s.elastic.co/v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Elasticsearch</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">es</span></span><br><span class="line">  <span class="attr">namespace:</span> <span class="string">elastic-system</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">version:</span> <span class="number">8.14</span><span class="number">.3</span></span><br><span class="line">  <span class="attr">nodeSets:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">default</span></span><br><span class="line">    <span class="attr">count:</span> <span class="number">1</span></span><br><span class="line">    <span class="attr">config:</span></span><br><span class="line">      <span class="attr">node.store.allow_mmap:</span> <span class="literal">false</span></span><br><span class="line">    <span class="attr">volumeClaimTemplates:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">metadata:</span></span><br><span class="line">        <span class="attr">name:</span> <span class="string">elasticsearch-data</span></span><br><span class="line">      <span class="attr">spec:</span></span><br><span class="line">        <span class="attr">accessModes:</span></span><br><span class="line">          <span class="bullet">-</span> <span class="string">ReadWriteOnce</span></span><br><span class="line">        <span class="attr">resources:</span></span><br><span class="line">          <span class="attr">requests:</span></span><br><span class="line">            <span class="attr">storage:</span> <span class="string">20Gi</span></span><br><span class="line">        <span class="attr">storageClassName:</span> <span class="string">csi-rbd-sc</span></span><br></pre></td></tr></table></figure><p>定义也十分简单，之后apply即可。</p><p>定义一个service</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">kind:</span> <span class="string">Service</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">es-http</span></span><br><span class="line">  <span class="attr">namespace:</span> <span class="string">elastic-system</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">ports:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">http</span></span><br><span class="line">      <span class="attr">protocol:</span> <span class="string">TCP</span></span><br><span class="line">      <span class="attr">port:</span> <span class="number">9200</span></span><br><span class="line">      <span class="attr">targetPort:</span> <span class="number">9200</span></span><br><span class="line">  <span class="attr">selector:</span></span><br><span class="line">    <span class="attr">common.k8s.elastic.co/type:</span> <span class="string">elasticsearch</span></span><br><span class="line">    <span class="attr">elasticsearch.k8s.elastic.co/cluster-name:</span> <span class="string">es</span></span><br><span class="line">  <span class="attr">type:</span> <span class="string">LoadBalancer</span></span><br></pre></td></tr></table></figure><p>获取下账号密码</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">kubectl get secret es-es-elastic-user -n elastic-system -o go-template=&#x27;&#123;&#123;.data.elastic | base64decode&#125;&#125;&#x27;</span><br></pre></td></tr></table></figure><p>访问下应该是正常的。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;es-es-default-0&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;cluster_name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;es&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;cluster_uuid&quot;</span><span class="punctuation">:</span> <span class="string">&quot;sxMRMy15QACkzxpSPeTlzA&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;number&quot;</span><span class="punctuation">:</span> <span class="string">&quot;8.14.3&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;build_flavor&quot;</span><span class="punctuation">:</span> <span class="string">&quot;default&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;build_type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;docker&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;build_hash&quot;</span><span class="punctuation">:</span> <span class="string">&quot;d55f984299e0e88dee72ebd8255f7ff130859ad0&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;build_date&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2024-07-07T22:04:49.882652950Z&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;build_snapshot&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;lucene_version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;9.10.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;minimum_wire_compatibility_version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;7.17.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;minimum_index_compatibility_version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;7.0.0&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;tagline&quot;</span><span class="punctuation">:</span> <span class="string">&quot;You Know, for Search&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h2 id="kibana"><a href="#kibana" class="headerlink" title="kibana"></a>kibana</h2><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">kibana.k8s.elastic.co/v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Kibana</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">kibana</span></span><br><span class="line">  <span class="attr">namespace:</span> <span class="string">elastic-system</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">version:</span> <span class="number">8.14</span><span class="number">.3</span></span><br><span class="line">  <span class="attr">count:</span> <span class="number">1</span></span><br><span class="line">  <span class="attr">elasticsearchRef:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">es</span></span><br><span class="line">  <span class="attr">http:</span></span><br><span class="line">    <span class="attr">service:</span></span><br><span class="line">      <span class="attr">spec:</span></span><br><span class="line">        <span class="attr">type:</span> <span class="string">LoadBalancer</span></span><br><span class="line">        <span class="attr">ports:</span></span><br><span class="line">          <span class="bullet">-</span> <span class="attr">port:</span> <span class="number">5601</span></span><br><span class="line">            <span class="attr">targetPort:</span> <span class="number">5601</span></span><br><span class="line">    <span class="attr">tls:</span></span><br><span class="line">      <span class="attr">selfSignedCertificate:</span></span><br><span class="line">        <span class="attr">disabled:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><p>这个没什么难度，直接就起来了。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>使用ECK十分快速的在k8s上启动了一个es集群，如果后续需要扩容的话，直接修改副本的数量即可，十分简单。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;遥想一年前，搭建es时还需要先挂载卷然后hung住容器拷贝配置文件，后面是自己做了镜像。&lt;/p&gt;
&lt;p&gt;再到后面切换到k8s后，有了conf
      
    
    </summary>
    
    
      <category term="编程" scheme="https://blog.zer0e.com/tags/%E7%BC%96%E7%A8%8B/"/>
    
      <category term="架构" scheme="https://blog.zer0e.com/tags/%E6%9E%B6%E6%9E%84/"/>
    
      <category term="运维" scheme="https://blog.zer0e.com/tags/%E8%BF%90%E7%BB%B4/"/>
    
      <category term="从0.5开始的运维架构之路" scheme="https://blog.zer0e.com/tags/%E4%BB%8E0-5%E5%BC%80%E5%A7%8B%E7%9A%84%E8%BF%90%E7%BB%B4%E6%9E%B6%E6%9E%84%E4%B9%8B%E8%B7%AF/"/>
    
  </entry>
  
  <entry>
    <title>【架构之路11】sa-token框架上手体验</title>
    <link href="https://blog.zer0e.com/2024/07/26/devops11/"/>
    <id>https://blog.zer0e.com/2024/07/26/devops11/</id>
    <published>2024-07-26T06:30:00.000Z</published>
    <updated>2024-07-26T06:34:07.093Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>看见了一个权限框架Sa-Token，以快速上手，轻量为优点，快速完成登录认证，权限认证等功能。</p><p>这篇文章来上手体验下这个框架，并讲讲分布式情况下如何使用。</p><h1 id="正文"><a href="#正文" class="headerlink" title="正文"></a>正文</h1><h2 id="创建项目"><a href="#创建项目" class="headerlink" title="创建项目"></a>创建项目</h2><p>创建一个springboot3项目吧。</p><p>pom文件如下</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependencies</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-data-redis<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-web<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.projectlombok<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>lombok<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">optional</span>&gt;</span>true<span class="tag">&lt;/<span class="name">optional</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="comment">&lt;!--        这里需要使用springboot3的依赖--&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>cn.dev33<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>sa-token-spring-boot3-starter<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.38.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-test<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">scope</span>&gt;</span>test<span class="tag">&lt;/<span class="name">scope</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependencies</span>&gt;</span></span><br></pre></td></tr></table></figure><p>我们用0配置来编写这个项目</p><h2 id="登录认证"><a href="#登录认证" class="headerlink" title="登录认证"></a>登录认证</h2><p>编写对应的service和controller</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">doLogin</span><span class="params">(String username, String password)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (<span class="string">&quot;zer0e&quot;</span>.equals(username)) &#123;</span><br><span class="line">            <span class="comment">// 这里是用户id</span></span><br><span class="line">            StpUtil.login(<span class="number">1</span>);</span><br><span class="line">        &#125;<span class="keyword">else</span> &#123;</span><br><span class="line">            StpUtil.login(<span class="number">2</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserController</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> UserService userService;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/login&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">doLogin</span><span class="params">(<span class="meta">@RequestParam</span> String username,</span></span><br><span class="line"><span class="params">                          <span class="meta">@RequestParam</span> String password)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> userService.doLogin(username, password);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/info&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Object <span class="title function_">info</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> StpUtil.getTokenInfo();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里由于没有接入数据库，所以service中没有校验用户名密码，问题不大。</p><p>login接口使用get是为了演示。</p><p>此时先访问<code>http://127.0.0.1:8080/login?username=zer0e&amp;password=zer0e</code></p><p>然后访问</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">http://127.0.0.1:8080/info</span><br><span class="line">&#123;</span><br><span class="line">    &quot;tokenName&quot;: &quot;satoken&quot;,</span><br><span class="line">    &quot;tokenValue&quot;: &quot;deb45ea8-3498-4837-88bf-9dba5e2e6b7e&quot;,</span><br><span class="line">    &quot;isLogin&quot;: true,</span><br><span class="line">    &quot;loginId&quot;: &quot;1&quot;,</span><br><span class="line">    &quot;loginType&quot;: &quot;login&quot;,</span><br><span class="line">    &quot;tokenTimeout&quot;: 2591993,</span><br><span class="line">    &quot;sessionTimeout&quot;: 2591993,</span><br><span class="line">    &quot;tokenSessionTimeout&quot;: -2,</span><br><span class="line">    &quot;tokenActiveTimeout&quot;: -1,</span><br><span class="line">    &quot;loginDevice&quot;: &quot;default-device&quot;,</span><br><span class="line">    &quot;tag&quot;: null</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以发现正常登录，并且获取登录信息也是正常的。</p><h2 id="权限和角色"><a href="#权限和角色" class="headerlink" title="权限和角色"></a>权限和角色</h2><p>光登录还不行，像spring security框架我们可以在登录时获取角色和权限以达到检验的目的。</p><p>sa-token也可以做到。</p><p>只要我们注册一个实现<code>StpInterface</code>接口的bean 再加上一点配置即可！</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">StpInterfaceImpl</span> <span class="keyword">implements</span> <span class="title class_">StpInterface</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> List&lt;String&gt; <span class="title function_">getPermissionList</span><span class="params">(Object loginId, String loginType)</span> &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;获取权限列表：&quot;</span> + loginId);</span><br><span class="line">        <span class="keyword">if</span> (loginId != <span class="literal">null</span> &amp;&amp; loginId.equals(<span class="number">1</span>)) &#123;</span><br><span class="line">            <span class="keyword">return</span> Arrays.asList(<span class="string">&quot;read&quot;</span>, <span class="string">&quot;write&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> List.of(<span class="string">&quot;read&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> List&lt;String&gt; <span class="title function_">getRoleList</span><span class="params">(Object loginId, String loginType)</span> &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;获取角色列表：&quot;</span> + loginId);</span><br><span class="line">        <span class="keyword">if</span> (loginId != <span class="literal">null</span> &amp;&amp; loginId.equals(<span class="number">1</span>)) &#123;</span><br><span class="line">            <span class="keyword">return</span> List.of(<span class="string">&quot;admin&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> List.of(<span class="string">&quot;user&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SaTokenConfigure</span> <span class="keyword">implements</span> <span class="title class_">WebMvcConfigurer</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">addInterceptors</span><span class="params">(InterceptorRegistry registry)</span> &#123;</span><br><span class="line">        <span class="comment">// 注册 Sa-Token 拦截器，打开注解式鉴权功能 </span></span><br><span class="line">        registry.addInterceptor(<span class="keyword">new</span> <span class="title class_">SaInterceptor</span>()).addPathPatterns(<span class="string">&quot;/**&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们把SaInterceptor注册到所有路由上。</p><p>这里为了方便都是采用硬编码，实际情况下是去数据库查询，这里不再赘述。</p><p>增加三个权限相关的接口</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping(&quot;/admin&quot;)</span></span><br><span class="line"><span class="meta">@SaCheckRole(&quot;admin&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Boolean <span class="title function_">admin</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@GetMapping(&quot;/read&quot;)</span></span><br><span class="line"><span class="meta">@SaCheckPermission(&quot;read&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Boolean <span class="title function_">read</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@GetMapping(&quot;/write&quot;)</span></span><br><span class="line"><span class="meta">@SaCheckPermission(&quot;write&quot;)</span></span><br><span class="line"><span class="keyword">public</span> Boolean <span class="title function_">write</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>先登录zer0e，然后访问<code>/admin</code> <code>/write</code> <code>/read</code>均可以访问。</p><p>然后登录其他用户，发现除了<code>/read</code>，其他接口都是500。因为我们没有做全局异常处理，所以抛出了500异常，问题不大。</p><p>如果我们需要在service层做权限校验，那么我们必须引入aop依赖</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>cn.dev33<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>sa-token-spring-aop<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.38.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="分布式环境"><a href="#分布式环境" class="headerlink" title="分布式环境"></a>分布式环境</h2><p>默认情况下，sa框架使用内存进行token存储，对于分布式环境下不太友好，好在sa提供了redis集成，我们加上即可。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>cn.dev33<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>sa-token-redis<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.38.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="comment">&lt;!-- 也可以选择jackson序列化</span></span><br><span class="line"><span class="comment">&lt;dependency&gt;</span></span><br><span class="line"><span class="comment">    &lt;groupId&gt;cn.dev33&lt;/groupId&gt;</span></span><br><span class="line"><span class="comment">    &lt;artifactId&gt;sa-token-redis-jackson&lt;/artifactId&gt;</span></span><br><span class="line"><span class="comment">    &lt;version&gt;1.38.0&lt;/version&gt;</span></span><br><span class="line"><span class="comment">&lt;/dependency&gt;</span></span><br><span class="line"><span class="comment">--!&gt;</span></span><br></pre></td></tr></table></figure><p>由于我们创建项目时就引入了redis，因此我们这里直接进行配置即可。</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">application:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">sa-token-demo</span></span><br><span class="line">  <span class="attr">data:</span></span><br><span class="line">    <span class="attr">redis:</span></span><br><span class="line">      <span class="attr">host:</span> <span class="string">localhost</span></span><br><span class="line">      <span class="attr">port:</span> <span class="number">6379</span></span><br></pre></td></tr></table></figure><p>基本上不用动东西，此时再次登录后发现token会存储在redis中，重启项目后原先token也可以使用。</p><h2 id="角色权限缓存"><a href="#角色权限缓存" class="headerlink" title="角色权限缓存"></a>角色权限缓存</h2><p>从刚才的权限例子我们可以知道，每次访问接口时，sa框架都会去获取用户id所对应的角色或权限（取决于你做了什么校验）。这对数据库的压力是比较大的，因此必须做缓存。</p><p>看了官方文档，其实是可以把一部分数据一起缓存的。这里我直接上代码。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> List&lt;String&gt; <span class="title function_">getRoleList</span><span class="params">(Object loginId, String loginType)</span> &#123;</span><br><span class="line">    <span class="type">SaSession</span> <span class="variable">session</span> <span class="operator">=</span> StpUtil.getSessionByLoginId(loginId);</span><br><span class="line">    <span class="keyword">return</span> session.get(<span class="string">&quot;roles&quot;</span>, () -&gt; &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;从数据库获取角色列表：&quot;</span> + loginId); </span><br><span class="line">        <span class="keyword">if</span> (loginId != <span class="literal">null</span> &amp;&amp; loginId.equals(<span class="number">1</span>)) &#123;</span><br><span class="line">            <span class="keyword">return</span> List.of(<span class="string">&quot;admin&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> List.of(<span class="string">&quot;user&quot;</span>); </span><br><span class="line">    &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>SaSession对象其实就是一个缓存，我们不必自己对接redisTemplate。至于权限也是一样的，但是这里官方做了一个解释，大概意思就是不要直接缓存账号的权限，而是通过获取角色再获取权限，我觉得说的也没毛病，因为你一旦角色和权限的对应关系更改后，权限缓存需要大面积失效，而且还不知道要失效哪些。</p><blockquote><h5 id="疑问：为什么不直接缓存-账号id-gt-权限列表-的关系，而是-账号id-gt-角色id-gt-权限列表-？"><a href="#疑问：为什么不直接缓存-账号id-gt-权限列表-的关系，而是-账号id-gt-角色id-gt-权限列表-？" class="headerlink" title="疑问：为什么不直接缓存 [账号id-&gt;权限列表\]的关系，而是 [账号id -&gt; 角色id -&gt; 权限列表]？"></a>疑问：为什么不直接缓存 <code>[账号id-&gt;权限列表\]</code>的关系，而是 <code>[账号id -&gt; 角色id -&gt; 权限列表]</code>？</h5><p>答：<code>[账号id-&gt;权限列表]</code>的缓存方式虽然更加直接粗暴，却有一个严重的问题：</p><ul><li>通常我们系统的权限架构是RBAC模型：权限与用户没有直接的关系，而是：用户拥有指定的角色，角色再拥有指定的权限</li><li>而这种’拥有关系’是动态的，是可以随时修改的，一旦我们修改了它们的对应关系，便要同步修改或清除对应的缓存数据</li></ul><p>现在假设如下业务场景：我们系统中有十万个账号属于同一个角色，当我们变动这个角色的权限时，难道我们要同时清除这十万个账号的缓存信息吗？ 这显然是一个不合理的操作，同一时间缓存大量清除容易引起Redis的缓存雪崩</p><p>而当我们采用 <code>[账号id -&gt; 角色id -&gt; 权限列表]</code> 的缓存模型时，则只需要清除或修改 <code>[角色id -&gt; 权限列表]</code> 一条缓存即可</p><p>一言以蔽之：权限的缓存模型需要跟着权限模型走，角色缓存亦然</p></blockquote><p>因此官网的做法是获取用户的角色，然后以角色id为key获取权限缓存，这样权限有更改时，我们只要失效对应的角色id的key即可，即<code>roleSession.clear()</code>。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> List&lt;String&gt; <span class="title function_">getPermissionList</span><span class="params">(Object loginId, String loginType)</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 1. 声明权限码集合</span></span><br><span class="line">    List&lt;String&gt; permissionList = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;();</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 2. 遍历角色列表，查询拥有的权限码 </span></span><br><span class="line">    <span class="keyword">for</span> (String roleId : getRoleList(loginId, loginType)) &#123;</span><br><span class="line">        <span class="type">SaSession</span> <span class="variable">roleSession</span> <span class="operator">=</span> SaSessionCustomUtil.getSessionById(<span class="string">&quot;role-&quot;</span> + roleId);</span><br><span class="line">        List&lt;String&gt; list = roleSession.get(<span class="string">&quot;Permission_List&quot;</span>, () -&gt; &#123;</span><br><span class="line">            <span class="keyword">return</span> ...;     <span class="comment">// 从数据库查询这个角色所拥有的权限列表  </span></span><br><span class="line">        &#125;);</span><br><span class="line">        permissionList.addAll(list);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 3. 返回权限码集合</span></span><br><span class="line">    <span class="keyword">return</span> permissionList;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>sa-token相对于spring security轻量了不少，使用上很简单，如果要配置权限校验的路径的话可以在<code>new SaInterceptor</code>中来指定。</p><p>此外它还有很多功能，比如踢人下线，多端登录等功能。甚至可以完全放弃session模式，改用jwt去存储token。</p><p>从今天的学习可以发现入门真的很快。如果要快速开发，那么sa-token是一个不错的选择。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;看见了一个权限框架Sa-Token，以快速上手，轻量为优点，快速完成登录认证，权限认证等功能。&lt;/p&gt;
&lt;p&gt;这篇文章来上手体验下这个框架，
      
    
    </summary>
    
    
      <category term="编程" scheme="https://blog.zer0e.com/tags/%E7%BC%96%E7%A8%8B/"/>
    
      <category term="架构" scheme="https://blog.zer0e.com/tags/%E6%9E%B6%E6%9E%84/"/>
    
      <category term="运维" scheme="https://blog.zer0e.com/tags/%E8%BF%90%E7%BB%B4/"/>
    
      <category term="从0.5开始的运维架构之路" scheme="https://blog.zer0e.com/tags/%E4%BB%8E0-5%E5%BC%80%E5%A7%8B%E7%9A%84%E8%BF%90%E7%BB%B4%E6%9E%B6%E6%9E%84%E4%B9%8B%E8%B7%AF/"/>
    
  </entry>
  
  <entry>
    <title>【架构之路10】sentinel限流上手</title>
    <link href="https://blog.zer0e.com/2024/07/25/devops10/"/>
    <id>https://blog.zer0e.com/2024/07/25/devops10/</id>
    <published>2024-07-25T13:30:00.000Z</published>
    <updated>2024-07-26T05:07:30.090Z</updated>
    
    <content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>这篇讲讲sentinel限流。</p><p>之前我们内部系统其实没有限流这个概念，我个人尝试过在网关层面直接做请求限制，没有在服务层尝试过。</p><h1 id="正文"><a href="#正文" class="headerlink" title="正文"></a>正文</h1><p>sentinel是阿里的产品，目的是为了限制服务的流量来达到保护应用的目的。</p><h2 id="sentinel功能"><a href="#sentinel功能" class="headerlink" title="sentinel功能"></a>sentinel功能</h2><h3 id="流量控制"><a href="#流量控制" class="headerlink" title="流量控制"></a>流量控制</h3><p>流量控制有以下几个角度:</p><ul><li>资源的调用关系，例如资源的调用链路，资源和资源之间的关系；</li><li>运行指标，例如 QPS、线程池、系统负载等；</li><li>控制的效果，例如直接限流、冷启动、排队等。</li></ul><h3 id="熔断降级"><a href="#熔断降级" class="headerlink" title="熔断降级"></a>熔断降级</h3><p>当调用链路中某个资源出现不稳定，例如，表现为 timeout，异常比例升高的时候，则对这个资源的调用进行限制，并让请求快速失败，避免影响到其它的资源，最终产生雪崩的效果。</p><p>Hystrix通过<a href="https://github.com/Netflix/Hystrix/wiki/How-it-Works#benefits-of-thread-pools">线程池</a>的方式，来对依赖(在sentinel的概念中对应资源)进行了隔离。这样做的好处是资源和资源之间做到了最彻底的隔离。缺点是除了增加了线程切换的成本，还需要预先给各个资源做线程池大小的分配。</p><p>Sentinel则是：</p><ul><li>通过并发线程数进行限制</li></ul><p>和资源池隔离的方法不同，Sentinel 通过限制资源并发线程的数量，来减少不稳定资源对其它资源的影响。这样不但没有线程切换的损耗，也不需要您预先分配线程池的大小。当某个资源出现不稳定的情况下，例如响应时间变长，对资源的直接影响就是会造成线程数的逐步堆积。当线程数在特定资源上堆积到一定的数量之后，对该资源的新请求就会被拒绝。堆积的线程完成任务后才开始继续接收请求。</p><ul><li>通过响应时间对资源进行降级</li></ul><p>除了对并发线程数进行控制以外，Sentinel 还可以通过响应时间来快速降级不稳定的资源。当依赖的资源出现响应时间过长后，所有对该资源的访问都会被直接拒绝，直到过了指定的时间窗口之后才重新恢复。</p><h3 id="系统负载保护"><a href="#系统负载保护" class="headerlink" title="系统负载保护"></a>系统负载保护</h3><blockquote><p>Sentinel 同时提供<a href="https://sentinelguard.io/zh-cn/docs/system-adaptive-protection.html">系统维度的自适应保护能力</a>。防止雪崩，是系统防护中重要的一环。当系统负载较高的时候，如果还持续让请求进入，可能会导致系统崩溃，无法响应。在集群环境下，网络负载均衡会把本应这台机器承载的流量转发到其它的机器上去。如果这个时候其它的机器也处在一个边缘状态的时候，这个增加的流量就会导致这台机器也崩溃，最后导致整个集群不可用。</p><p>针对这个情况，Sentinel 提供了对应的保护机制，让系统的入口流量和系统的负载达到一个平衡，保证系统在能力范围之内处理最多的请求。</p></blockquote><p>简单来说就是可以根据系统的某些指标，如CPU使用情况，RT，QPS等数据限制请求。</p><h2 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h2><p>sentinel的使用也是十分简单，sentinel提供dashboard服务供限流服务接入，我们可以通过dashboard快速下发规则给应用，实现界面化管理。</p><p>先<a href="https://github.com/alibaba/Sentinel/releases">下载dashboard</a>，启动</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.8.jar</span><br></pre></td></tr></table></figure><p>这里采用springCloudAlibaba快速接入。</p><p>pom文件依赖如下</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">properties</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">java.version</span>&gt;</span>1.8<span class="tag">&lt;/<span class="name">java.version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">project.build.sourceEncoding</span>&gt;</span>UTF-8<span class="tag">&lt;/<span class="name">project.build.sourceEncoding</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">project.reporting.outputEncoding</span>&gt;</span>UTF-8<span class="tag">&lt;/<span class="name">project.reporting.outputEncoding</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">spring-boot.version</span>&gt;</span>2.6.13<span class="tag">&lt;/<span class="name">spring-boot.version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">spring-cloud-alibaba.version</span>&gt;</span>2021.0.5.0<span class="tag">&lt;/<span class="name">spring-cloud-alibaba.version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">properties</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependencies</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-web<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-alibaba-sentinel<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.projectlombok<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>lombok<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">optional</span>&gt;</span>true<span class="tag">&lt;/<span class="name">optional</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-test<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">scope</span>&gt;</span>test<span class="tag">&lt;/<span class="name">scope</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependencies</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependencyManagement</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependencies</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-dependencies<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">version</span>&gt;</span>$&#123;spring-boot.version&#125;<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">type</span>&gt;</span>pom<span class="tag">&lt;/<span class="name">type</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">scope</span>&gt;</span>import<span class="tag">&lt;/<span class="name">scope</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-alibaba-dependencies<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">version</span>&gt;</span>$&#123;spring-cloud-alibaba.version&#125;<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">type</span>&gt;</span>pom<span class="tag">&lt;/<span class="name">type</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">scope</span>&gt;</span>import<span class="tag">&lt;/<span class="name">scope</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependencies</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependencyManagement</span>&gt;</span></span><br></pre></td></tr></table></figure><p>使用<code>start.aliyun.com</code>构建的springboot程序会自动生成demo文件，这里我们手动写下。</p><h3 id="流控"><a href="#流控" class="headerlink" title="流控"></a>流控</h3><p>先写配置，用于在触发sentinel流控时显示的返回。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SentinelConfig</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> BlockExceptionHandler <span class="title function_">sentinelBlockExceptionHandler</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> (request, response, e) -&gt; &#123;</span><br><span class="line">            response.setStatus(<span class="number">429</span>);</span><br><span class="line"></span><br><span class="line">            <span class="type">PrintWriter</span> <span class="variable">out</span> <span class="operator">=</span> response.getWriter();</span><br><span class="line">            out.print(<span class="string">&quot;Oops, blocked by Sentinel: &quot;</span> + e.getClass().getSimpleName());</span><br><span class="line">            out.flush();</span><br><span class="line">            out.close();</span><br><span class="line">        &#125;;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>定义服务</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserService</span> &#123;</span><br><span class="line">    <span class="meta">@SentinelResource(value = &quot;UserService#getUserNameById&quot;, defaultFallback = &quot;getUserFallback&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">getUserNameById</span><span class="params">(Integer id)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;default user&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    </span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">getUserFallback</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;get user fall back&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里我们讲讲<code>@SentinelResource</code>注解。</p><p>注意这个注解也采用aop，所以private方法不支持。</p><p>这个注解用于定义资源，并提供可选的异常处理和 fallback 配置项。</p><blockquote><p><code>@SentinelResource</code> 注解包含以下属性：</p><ul><li><code>value</code>：资源名称，必需项（不能为空）</li><li><code>entryType</code>：entry 类型，可选项（默认为 <code>EntryType.OUT</code>）</li><li><code>blockHandler</code> / <code>blockHandlerClass</code>: <code>blockHandler</code> 对应处理 <code>BlockException</code> 的函数名称，可选项。blockHandler 函数访问范围需要是 <code>public</code>，返回类型需要与原方法相匹配，参数类型需要和原方法相匹配并且最后加一个额外的参数，类型为 <code>BlockException</code>。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数，则可以指定 <code>blockHandlerClass</code> 为对应的类的 <code>Class</code> 对象，注意对应的函数必需为 static 函数，否则无法解析。</li><li><code>fallback</code>：fallback 函数名称，可选项，用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常（除了exceptionsToIgnore里面排除掉的异常类型）进行处理。fallback 函数签名和位置要求：<ul><li>返回值类型必须与原函数返回值类型一致；</li><li>方法参数列表需要和原函数一致，或者可以额外多一个 <code>Throwable</code> 类型的参数用于接收对应的异常。</li><li>fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数，则可以指定 <code>fallbackClass</code> 为对应的类的 <code>Class</code> 对象，注意对应的函数必需为 static 函数，否则无法解析。</li></ul></li><li><code>defaultFallback</code>（since 1.6.0）：默认的 fallback 函数名称，可选项，通常用于通用的 fallback 逻辑（即可以用于很多服务或方法）。默认 fallback 函数可以针对所以类型的异常（除了exceptionsToIgnore里面排除掉的异常类型）进行处理。若同时配置了 fallback 和 defaultFallback，则只有 fallback 会生效。defaultFallback 函数签名要求：<ul><li>返回值类型必须与原函数返回值类型一致；</li><li>方法参数列表需要为空，或者可以额外多一个 <code>Throwable</code> 类型的参数用于接收对应的异常。</li><li>defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数，则可以指定 <code>fallbackClass</code> 为对应的类的 <code>Class</code> 对象，注意对应的函数必需为 static 函数，否则无法解析。</li></ul></li><li><code>exceptionsToIgnore</code>（since 1.6.0）：用于指定哪些异常被排除掉，不会计入异常统计中，也不会进入 fallback 逻辑中，而是会原样抛出。</li></ul><p>若 blockHandler 和 fallback 都进行了配置，则被限流降级而抛出 <code>BlockException</code> 时只会进入 <code>blockHandler</code> 处理逻辑。若未配置 <code>blockHandler</code>、<code>fallback</code> 和 <code>defaultFallback</code>，则被限流降级时会将 <code>BlockException</code> <strong>直接抛出</strong>。</p></blockquote><p>其实核心参数就两个，value和defaultFallback，一个用于定义资源，另一个配置默认的失败方法。</p><p>注意这里getUserFallBack的方法参数为空或者接收一个Throwable参数。</p><p>接下来写controller</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/user&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserController</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> UserService userService;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/get&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">getUserById</span><span class="params">(<span class="meta">@RequestParam</span> Integer id)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> userService.getUserNameById(id);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>很简单的一个接口，我们调用看看。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">http://127.0.0.1:8081/user/get?id=1</span><br><span class="line">default user</span><br></pre></td></tr></table></figure><p>打开dashboard，账号密码都是sentinel。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://127.0.0.1:8080</span><br></pre></td></tr></table></figure><p>在左侧可以发现我们的应用，并且在实时监控中可以看到我们调用的接口和对应的资源。</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">sentinel_spring_web_context</span></span><br><span class="line">    <span class="string">/user/get</span></span><br><span class="line">        <span class="string">UserService#getUserNameById</span></span><br></pre></td></tr></table></figure><p>我们可以对/user/get做限流，也可以对UserService#getUserNameById限流，因为他们都属于sentinel的资源。</p><p>这里我们对/user/get限流，qps单机阈值限制为1。新增完成后，多次访问接口，会出现<code>Oops, blocked by Sentinel: FlowException</code>的错误提示。</p><p>对UserService#getUserNameById做qps为1的限流，可以发现限流请求默认返回了get user fall back。</p><h3 id="熔断降级-1"><a href="#熔断降级-1" class="headerlink" title="熔断降级"></a>熔断降级</h3><p>在上面服务的基础上增加一个服务方法和接口。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@SentinelResource(value = &quot;UserService#getUsers&quot;, defaultFallback = &quot;getUserFallback&quot;)</span></span><br><span class="line"><span class="keyword">public</span> String <span class="title function_">getUsers</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        Thread.sleep(<span class="number">1000</span>);</span><br><span class="line">    &#125;<span class="keyword">catch</span> (Exception ignored) &#123;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="string">&quot;all users&quot;</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping(&quot;/get-all&quot;)</span></span><br><span class="line"><span class="keyword">public</span> String <span class="title function_">getAllUser</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> userService.getUsers();</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>在这个接口上我们等待了一秒延迟。</p><p>我们在控制台对这个资源进行降级。为了能更直观看出来，最大RT填为500，最小请求数为1，比例阈值0.5(这个无所谓)，熔断时间10s。</p><p>保存之后我们请求接口，第一次请求成功，然后由于请求时间大于500ms，直接触发了熔断降级，此时再次请求就会执行默认的fallback方法。</p><h3 id="集群限流"><a href="#集群限流" class="headerlink" title="集群限流"></a>集群限流</h3><p>这个好理解，就是一组服务的最大请求量。配置起来也简单。</p><p>先copy一份idea的启动文件，然后加上vmoptions：<code>-Dserver.port=8082</code>再启动一个服务，在dashboard上就能看见应用为2/2.</p><p>选择集群流控，添加token-server，这个服务的作用是用于接收其他客户端的请求，判断请求是否通过。</p><p>然后将另一个服务变为token-client，填写最大qps即可完成集群流控。</p><p>当然我们在配置普通流控规则时，也可以勾选是否集群，选择均摊模式，填写阈值即可应用流控规则。原理也是请求前向token-server获取令牌实现集群限流。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>今天试用了一下sentinel，很多年前在我还在大学的时候就初步使用过。</p><p>今天遇到的坑是defaultFallback不生效，我在源码中调试了很久，包括源码是如何寻找到方法，最后调试才发现<code>name.equals(method.getName())</code>判断不通过，原来我在SentinelResource定义的是getUserFallback，而编写的方法是getUserFallBack，大小写错了！崩溃！</p><p>其他的话问题不是很大，还有一个就是规则的持久化了，这个的话得配合nacos做。官方demo和网上的案例我也看了，难度较小，只需要知道流控的规则json如何编写，然后引入对应的包监听nacos数据即可。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h1&gt;&lt;p&gt;这篇讲讲sentinel限流。&lt;/p&gt;
&lt;p&gt;之前我们内部系统其实没有限流这个概念，我个人尝试过在网关层面直接做请求限制，没有在服务层尝试过
      
    
    </summary>
    
    
      <category term="编程" scheme="https://blog.zer0e.com/tags/%E7%BC%96%E7%A8%8B/"/>
    
      <category term="架构" scheme="https://blog.zer0e.com/tags/%E6%9E%B6%E6%9E%84/"/>
    
      <category term="运维" scheme="https://blog.zer0e.com/tags/%E8%BF%90%E7%BB%B4/"/>
    
      <category term="从0.5开始的运维架构之路" scheme="https://blog.zer0e.com/tags/%E4%BB%8E0-5%E5%BC%80%E5%A7%8B%E7%9A%84%E8%BF%90%E7%BB%B4%E6%9E%B6%E6%9E%84%E4%B9%8B%E8%B7%AF/"/>
    
  </entry>
  
  <entry>
    <title>【架构之路9】flink-cdc学习与使用</title>
    <link href="https://blog.zer0e.com/2024/07/24/devops9/"/>
    <id>https://blog.zer0e.com/2024/07/24/devops9/</id>
    <published>2024-07-24T10:30:00.000Z</published>
    <updated>2024-07-24T14:00:19.986Z</updated>
    
    <content type="html"><![CDATA[<h1 id="随便聊聊"><a href="#随便聊聊" class="headerlink" title="随便聊聊"></a>随便聊聊</h1><p>今天是离职的50天，面试不咋顺利，昨晚也没睡好，有个离职的同事去日本玩了，不知道他是什么考虑。脱离工作久了舒服是确实舒服，但是找工作真的挺难的，不仅是投递，面试，复习，复盘，一环扣一环，而且还很累。</p><p>本来挺有把握的一场面试，结果大晚上查了一下发现面试不通过，也不知道啥原因，有一说一虽然我不一定会去，不过得知面试失效的消息打击还是比较大的，不知道是不是薪酬说高了。</p><p>离职还没有让家里人知道，把面试失败的消息跟老姐说后，她送了我一句话，此处不留爷，自有留爷处。我觉得说的太好了。</p><p>来看今天的flink-cdc的使用吧。</p><h1 id="正文"><a href="#正文" class="headerlink" title="正文"></a>正文</h1><h2 id="flink-cdc介绍"><a href="#flink-cdc介绍" class="headerlink" title="flink-cdc介绍"></a>flink-cdc介绍</h2><p>CDC 是变更数据捕获（Change Data Capture）技术的缩写，它可以将源数据库（Source）的增量变动记录，同步到一个或多个数据目的（Sink）。在同步过程中，还可以对数据进行一定的处理，例如分组（GROUP BY）、多表的关联（JOIN）等。</p><p>一般情况下，我们使用CDC将数据库增量数据同步到不同的数据源中，比如ES, ClickHouse之类的。</p><p>通常来讲，CDC 分为<strong>主动查询</strong>和<strong>事件接收</strong>两种技术实现模式。</p><p>对于主动查询而言，通常会在数据源表的某个字段中，保存上次更新的时间戳或版本号等信息，然后下游通过不断的查询和与上次的记录做对比，来确定数据是否有变动，是否需要同步。这种方式优点是不涉及数据库底层特性，实现比较通用；缺点是要对业务表做改造，且实时性不高，不能确保跟踪到所有的变更记录，且持续的频繁查询对数据库的压力较大。</p><p>事件接收模式可以通过触发器（Trigger）或者日志（例如 Transaction log、Binary log、Write-ahead log 等）来实现。当数据源表发生变动时，会通过附加在表上的触发器或者 binlog 等途径，将操作记录下来。下游可以通过数据库底层的协议，订阅并消费这些事件，然后对数据库变动记录做重放，从而实现同步。这种方式的优点是实时性高，可以精确捕捉上游的各种变动；缺点是部署数据库的事件接收和解析器（例如 Debezium、Canal 等），有一定的学习和运维成本，对一些冷门的数据库支持不够。</p><p>Flink-cdc是由apache托管的开源项目，使用监听bin-log的方式获取增量数据，并且支持常用的数据库，例如mysql，mongodb，tidb等等。</p><h2 id="环境搭建"><a href="#环境搭建" class="headerlink" title="环境搭建"></a>环境搭建</h2><p>这里用docker容器快速启动一个mysql。</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">mysql:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">&#x27;mysql:8.0&#x27;</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&#x27;MYSQL_ROOT_PASSWORD=root&#x27;</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&#x27;3306:3306&#x27;</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">mysql:/var/lib/mysql</span></span><br><span class="line"></span><br><span class="line"><span class="attr">volumes:</span></span><br><span class="line">  <span class="attr">mysql:</span></span><br></pre></td></tr></table></figure><p>容器启动后需要做点准备工作，修改mysql的时区。因为默认情况下cdc应用和mysql时区不一致的话，<del>可能会导致数据监听有延迟。</del>任务会一直在报错，无法正常监听。具体报错可以在webUI里看到。感兴趣可以不改mysql时区然后启动下flink-cdc试试。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">mysql&gt; show variables like &#x27;%time_zone%&#x27;;</span><br><span class="line">+------------------+--------+</span><br><span class="line">| Variable_name    | Value  |</span><br><span class="line">+------------------+--------+</span><br><span class="line">| system_time_zone | UTC    |</span><br><span class="line">| time_zone        | SYSTEM |</span><br><span class="line">+------------------+--------+</span><br><span class="line">2 rows in set (0.00 sec)</span><br><span class="line"></span><br><span class="line">mysql&gt; set persist time_zone=&#x27;+8:00&#x27;;</span><br><span class="line">Query OK, 0 rows affected (0.00 sec)</span><br><span class="line"></span><br><span class="line">mysql&gt; show variables like &#x27;%time_zone%&#x27;;</span><br><span class="line">+------------------+--------+</span><br><span class="line">| Variable_name    | Value  |</span><br><span class="line">+------------------+--------+</span><br><span class="line">| system_time_zone | UTC    |</span><br><span class="line">| time_zone        | +08:00 |</span><br><span class="line">+------------------+--------+</span><br><span class="line">2 rows in set (0.00 sec)</span><br></pre></td></tr></table></figure><p>创建一张测试表，再加点数据。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">create table</span> test.t_user</span><br><span class="line">(</span><br><span class="line">    id          <span class="type">int</span> auto_increment</span><br><span class="line">        <span class="keyword">primary key</span>,</span><br><span class="line">    name        <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">not null</span>,</span><br><span class="line">    description <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">null</span></span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="keyword">INSERT INTO</span> test.t_user (id, name, description) <span class="keyword">VALUES</span> (<span class="number">1</span>, <span class="string">&#x27;alice&#x27;</span>, <span class="string">&#x27;test&#x27;</span>);</span><br><span class="line"><span class="keyword">INSERT INTO</span> test.t_user (id, name, description) <span class="keyword">VALUES</span> (<span class="number">2</span>, <span class="string">&#x27;bob&#x27;</span>, <span class="string">&#x27;fuck&#x27;</span>);</span><br><span class="line"><span class="keyword">INSERT INTO</span> test.t_user (id, name, description) <span class="keyword">VALUES</span> (<span class="number">3</span>, <span class="string">&#x27;c&#x27;</span>, <span class="string">&#x27;moon&#x27;</span>);</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h2><p>Talk is cheap. Show me the code.</p><p>这里我们采用maven构建一个java程序，使用flink-cdc监听mysql变化。</p><p>pom文件依赖如下</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">project</span> <span class="attr">xmlns</span>=<span class="string">&quot;http://maven.apache.org/POM/4.0.0&quot;</span> <span class="attr">xmlns:xsi</span>=<span class="string">&quot;http://www.w3.org/2001/XMLSchema-instance&quot;</span></span></span><br><span class="line"><span class="tag">         <span class="attr">xsi:schemaLocation</span>=<span class="string">&quot;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">modelVersion</span>&gt;</span>4.0.0<span class="tag">&lt;/<span class="name">modelVersion</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.github.zer0e<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>flink-cdc-demo<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>0.0.1-SNAPSHOT<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependencies</span>&gt;</span></span><br><span class="line">        <span class="comment">&lt;!--    flink 基础包--&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.flink<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>flink-connector-base<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.18.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="comment">&lt;!--flink mysql连接器--&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.ververica<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>flink-connector-mysql-cdc<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">version</span>&gt;</span>3.0.1<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="comment">&lt;!--flink data stream支持 --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.flink<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>flink-streaming-java<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.18.1<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="comment">&lt;!--flink java客户端--&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.flink<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>flink-clients<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.18.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="comment">&lt;!--web界面支持--&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.flink<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>flink-runtime-web<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.18.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="comment">&lt;!--用于读写批处理和流水表--&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.flink<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>flink-table-runtime<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.18.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">&lt;!--    日志--&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>ch.qos.logback<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>logback-classic<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.5.6<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.slf4j<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>slf4j-api<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.0.13<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.projectlombok<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>lombok<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.18.32<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependencies</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">build</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">plugins</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">plugin</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.maven.plugins<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>maven-compiler-plugin<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">configuration</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="name">source</span>&gt;</span>8<span class="tag">&lt;/<span class="name">source</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="name">target</span>&gt;</span>8<span class="tag">&lt;/<span class="name">target</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;/<span class="name">configuration</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;/<span class="name">plugin</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">plugins</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">build</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">project</span>&gt;</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>在resource目录下新建logback.xml，否则日志会有debug级别。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version=<span class="string">&quot;1.0&quot;</span> encoding=<span class="string">&quot;UTF-8&quot;</span>?&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">configuration</span> <span class="attr">debug</span>=<span class="string">&quot;false&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">appender</span> <span class="attr">name</span>=<span class="string">&quot;STDOUT&quot;</span> <span class="attr">class</span>=<span class="string">&quot;ch.qos.logback.core.ConsoleAppender&quot;</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">encoder</span> <span class="attr">class</span>=<span class="string">&quot;ch.qos.logback.classic.encoder.PatternLayoutEncoder&quot;</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">pattern</span>&gt;</span>%d&#123;yyyy-MM-dd HH:mm:ss.SSS&#125; [%thread] %-5level %logger&#123;50&#125; - %msg%n<span class="tag">&lt;/<span class="name">pattern</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">encoder</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">appender</span>&gt;</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">root</span> <span class="attr">level</span>=<span class="string">&quot;INFO&quot;</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">appender-ref</span> <span class="attr">ref</span>=<span class="string">&quot;STDOUT&quot;</span>/&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">root</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">configuration</span>&gt;</span></span><br></pre></td></tr></table></figure><p>然后我们创建出一个MySink作为监听数据的处理。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.github.zer0e;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.apache.flink.streaming.api.functions.sink.RichSinkFunction;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MySink</span> <span class="keyword">extends</span> <span class="title class_">RichSinkFunction</span>&lt;String&gt; &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">invoke</span><span class="params">(String value, Context context)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;mysql cdc: &quot;</span> + value);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>主程序入口</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.github.zer0e;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.ververica.cdc.connectors.mysql.source.MySqlSource;</span><br><span class="line"><span class="keyword">import</span> com.ververica.cdc.debezium.JsonDebeziumDeserializationSchema;</span><br><span class="line"><span class="keyword">import</span> org.apache.flink.api.common.eventtime.WatermarkStrategy;</span><br><span class="line"><span class="keyword">import</span> org.apache.flink.configuration.Configuration;</span><br><span class="line"><span class="keyword">import</span> org.apache.flink.configuration.RestOptions;</span><br><span class="line"><span class="keyword">import</span> org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CdcExample</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        MySqlSource&lt;String&gt; source = MySqlSource.&lt;String&gt;builder()</span><br><span class="line">                .hostname(<span class="string">&quot;localhost&quot;</span>)</span><br><span class="line">                .port(<span class="number">3306</span>)</span><br><span class="line">                .databaseList(<span class="string">&quot;test&quot;</span>)</span><br><span class="line">                .tableList(<span class="string">&quot;test.t_user&quot;</span>)</span><br><span class="line">                .username(<span class="string">&quot;root&quot;</span>)</span><br><span class="line">                .password(<span class="string">&quot;root&quot;</span>)</span><br><span class="line">                .deserializer(<span class="keyword">new</span> <span class="title class_">JsonDebeziumDeserializationSchema</span>())</span><br><span class="line">                .includeSchemaChanges(<span class="literal">true</span>)</span><br><span class="line">                .build();</span><br><span class="line"></span><br><span class="line">        <span class="type">Configuration</span> <span class="variable">configuration</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Configuration</span>();</span><br><span class="line">        configuration.setInteger(RestOptions.PORT, <span class="number">8081</span>);</span><br><span class="line">        <span class="type">StreamExecutionEnvironment</span> <span class="variable">executionEnvironment</span> <span class="operator">=</span> StreamExecutionEnvironment.getExecutionEnvironment(configuration);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 检查点的作用是当作业失败后，回到前一个检查点开始</span></span><br><span class="line">        executionEnvironment.enableCheckpointing(<span class="number">5000</span>);</span><br><span class="line"></span><br><span class="line">        executionEnvironment.fromSource(source, WatermarkStrategy.&lt;String&gt;noWatermarks(),</span><br><span class="line">                <span class="string">&quot;Mysql&quot;</span>).addSink(<span class="keyword">new</span> <span class="title class_">MySink</span>());</span><br><span class="line">        executionEnvironment.execute();</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>启动！顺利的话，启动后命令行就能输出如下的东西</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">mysql cdc: &#123;&quot;before&quot;:null,&quot;after&quot;:&#123;&quot;id&quot;:3,&quot;name&quot;:&quot;c&quot;,&quot;description&quot;:&quot;moon1&quot;&#125;,&quot;source&quot;:&#123;&quot;version&quot;:&quot;1.9.7.Final&quot;,&quot;connector&quot;:&quot;mysql&quot;,&quot;name&quot;:&quot;mysql_binlog_source&quot;,&quot;ts_ms&quot;:0,&quot;snapshot&quot;:&quot;false&quot;,&quot;db&quot;:&quot;test&quot;,&quot;sequence&quot;:null,&quot;table&quot;:&quot;t_user&quot;,&quot;server_id&quot;:0,&quot;gtid&quot;:null,&quot;file&quot;:&quot;&quot;,&quot;pos&quot;:0,&quot;row&quot;:0,&quot;thread&quot;:null,&quot;query&quot;:null&#125;,&quot;op&quot;:&quot;r&quot;,&quot;ts_ms&quot;:1721806935503,&quot;transaction&quot;:null&#125;</span><br><span class="line">mysql cdc: &#123;&quot;before&quot;:null,&quot;after&quot;:&#123;&quot;id&quot;:2,&quot;name&quot;:&quot;bob&quot;,&quot;description&quot;:&quot;fuck&quot;&#125;,&quot;source&quot;:&#123;&quot;version&quot;:&quot;1.9.7.Final&quot;,&quot;connector&quot;:&quot;mysql&quot;,&quot;name&quot;:&quot;mysql_binlog_source&quot;,&quot;ts_ms&quot;:0,&quot;snapshot&quot;:&quot;false&quot;,&quot;db&quot;:&quot;test&quot;,&quot;sequence&quot;:null,&quot;table&quot;:&quot;t_user&quot;,&quot;server_id&quot;:0,&quot;gtid&quot;:null,&quot;file&quot;:&quot;&quot;,&quot;pos&quot;:0,&quot;row&quot;:0,&quot;thread&quot;:null,&quot;query&quot;:null&#125;,&quot;op&quot;:&quot;r&quot;,&quot;ts_ms&quot;:1721806935503,&quot;transaction&quot;:null&#125;</span><br><span class="line">mysql cdc: &#123;&quot;before&quot;:null,&quot;after&quot;:&#123;&quot;id&quot;:1,&quot;name&quot;:&quot;alice&quot;,&quot;description&quot;:&quot;test&quot;&#125;,&quot;source&quot;:&#123;&quot;version&quot;:&quot;1.9.7.Final&quot;,&quot;connector&quot;:&quot;mysql&quot;,&quot;name&quot;:&quot;mysql_binlog_source&quot;,&quot;ts_ms&quot;:0,&quot;snapshot&quot;:&quot;false&quot;,&quot;db&quot;:&quot;test&quot;,&quot;sequence&quot;:null,&quot;table&quot;:&quot;t_user&quot;,&quot;server_id&quot;:0,&quot;gtid&quot;:null,&quot;file&quot;:&quot;&quot;,&quot;pos&quot;:0,&quot;row&quot;:0,&quot;thread&quot;:null,&quot;query&quot;:null&#125;,&quot;op&quot;:&quot;r&quot;,&quot;ts_ms&quot;:1721806935501,&quot;transaction&quot;:null&#125;</span><br></pre></td></tr></table></figure><p>这里主要是关于op字段，其实是对应数据库的增删改查，即c(create), d(delete), u(update), r(read)。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">mysql cdc: &#123;&quot;before&quot;:null,&quot;after&quot;:&#123;&quot;id&quot;:4,&quot;name&quot;:&quot;d&quot;,&quot;description&quot;:&quot;today&quot;&#125;,&quot;source&quot;:&#123;&quot;version&quot;:&quot;1.9.7.Final&quot;,&quot;connector&quot;:&quot;mysql&quot;,&quot;name&quot;:&quot;mysql_binlog_source&quot;,&quot;ts_ms&quot;:1721807345000,&quot;snapshot&quot;:&quot;false&quot;,&quot;db&quot;:&quot;test&quot;,&quot;sequence&quot;:null,&quot;table&quot;:&quot;t_user&quot;,&quot;server_id&quot;:1,&quot;gtid&quot;:null,&quot;file&quot;:&quot;binlog.000004&quot;,&quot;pos&quot;:2490,&quot;row&quot;:0,&quot;thread&quot;:21,&quot;query&quot;:null&#125;,&quot;op&quot;:&quot;c&quot;,&quot;ts_ms&quot;:1721807345604,&quot;transaction&quot;:null&#125;</span><br><span class="line"></span><br><span class="line">mysql cdc: &#123;&quot;before&quot;:&#123;&quot;id&quot;:4,&quot;name&quot;:&quot;d&quot;,&quot;description&quot;:&quot;today&quot;&#125;,&quot;after&quot;:null,&quot;source&quot;:&#123;&quot;version&quot;:&quot;1.9.7.Final&quot;,&quot;connector&quot;:&quot;mysql&quot;,&quot;name&quot;:&quot;mysql_binlog_source&quot;,&quot;ts_ms&quot;:1721807364000,&quot;snapshot&quot;:&quot;false&quot;,&quot;db&quot;:&quot;test&quot;,&quot;sequence&quot;:null,&quot;table&quot;:&quot;t_user&quot;,&quot;server_id&quot;:1,&quot;gtid&quot;:null,&quot;file&quot;:&quot;binlog.000004&quot;,&quot;pos&quot;:2788,&quot;row&quot;:0,&quot;thread&quot;:21,&quot;query&quot;:null&#125;,&quot;op&quot;:&quot;d&quot;,&quot;ts_ms&quot;:1721807364426,&quot;transaction&quot;:null&#125;</span><br><span class="line"></span><br><span class="line">mysql cdc: &#123;&quot;before&quot;:&#123;&quot;id&quot;:3,&quot;name&quot;:&quot;c&quot;,&quot;description&quot;:&quot;moon1&quot;&#125;,&quot;after&quot;:&#123;&quot;id&quot;:3,&quot;name&quot;:&quot;c&quot;,&quot;description&quot;:&quot;moon&quot;&#125;,&quot;source&quot;:&#123;&quot;version&quot;:&quot;1.9.7.Final&quot;,&quot;connector&quot;:&quot;mysql&quot;,&quot;name&quot;:&quot;mysql_binlog_source&quot;,&quot;ts_ms&quot;:1721807381000,&quot;snapshot&quot;:&quot;false&quot;,&quot;db&quot;:&quot;test&quot;,&quot;sequence&quot;:null,&quot;table&quot;:&quot;t_user&quot;,&quot;server_id&quot;:1,&quot;gtid&quot;:null,&quot;file&quot;:&quot;binlog.000004&quot;,&quot;pos&quot;:3095,&quot;row&quot;:0,&quot;thread&quot;:21,&quot;query&quot;:null&#125;,&quot;op&quot;:&quot;u&quot;,&quot;ts_ms&quot;:1721807381851,&quot;transaction&quot;:null&#125;</span><br></pre></td></tr></table></figure><p>其他的没什么好说的，如果不想读取这么多json数据的话，可以创建实体类映射下。</p><p>但是个人尝试下来，其实要自己做解析的话会比较困难，建议还是拿到json之后，根据表名称做下parse会相对简单。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@ToString</span></span><br><span class="line"><span class="meta">@AllArgsConstructor</span></span><br><span class="line"><span class="meta">@NoArgsConstructor</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Message</span>&lt;T&gt; &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> T before;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> T after;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Source source;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> String op;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@JsonProperty(&quot;ts_ms&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> Long timestamp;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="meta">@Data</span></span><br><span class="line">    <span class="meta">@ToString</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">Source</span> &#123;</span><br><span class="line">        <span class="keyword">private</span> String db;</span><br><span class="line">        <span class="keyword">private</span> String table;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MySink</span> <span class="keyword">extends</span> <span class="title class_">RichSinkFunction</span>&lt;String&gt; &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">ObjectMapper</span> <span class="variable">objectMapper</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ObjectMapper</span>();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">static</span> &#123;</span><br><span class="line">        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, <span class="literal">false</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">invoke</span><span class="params">(String value, Context context)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;mysql cdc: &quot;</span> + value);</span><br><span class="line"></span><br><span class="line">        Message&lt;User&gt; userMessage = objectMapper.readValue(value, <span class="keyword">new</span> <span class="title class_">TypeReference</span>&lt;Message&lt;User&gt;&gt;() &#123;</span><br><span class="line">        &#125;);</span><br><span class="line"></span><br><span class="line">        System.out.println(<span class="string">&quot;mysql cdc2: &quot;</span> + userMessage);</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>简单搞了一个demo，实际搞下来我还看了能否自己解析原始数据，比较困难。</p><p>其次是每次重启后都会读取到所有源数据，虽然不影响，但是我不知道这个能否关闭？</p><p>之后其实就是对接es等其他数据源了，这个业务写多了都会。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;随便聊聊&quot;&gt;&lt;a href=&quot;#随便聊聊&quot; class=&quot;headerlink&quot; title=&quot;随便聊聊&quot;&gt;&lt;/a&gt;随便聊聊&lt;/h1&gt;&lt;p&gt;今天是离职的50天，面试不咋顺利，昨晚也没睡好，有个离职的同事去日本玩了，不知道他是什么考虑。脱离工作久了舒服是确实舒服，但
      
    
    </summary>
    
    
      <category term="编程" scheme="https://blog.zer0e.com/tags/%E7%BC%96%E7%A8%8B/"/>
    
      <category term="架构" scheme="https://blog.zer0e.com/tags/%E6%9E%B6%E6%9E%84/"/>
    
      <category term="运维" scheme="https://blog.zer0e.com/tags/%E8%BF%90%E7%BB%B4/"/>
    
      <category term="从0.5开始的运维架构之路" scheme="https://blog.zer0e.com/tags/%E4%BB%8E0-5%E5%BC%80%E5%A7%8B%E7%9A%84%E8%BF%90%E7%BB%B4%E6%9E%B6%E6%9E%84%E4%B9%8B%E8%B7%AF/"/>
    
  </entry>
  
</feed>
