https://github.com/superleeyom/blogRSS feed of superleeyom's blog2024-04-24T05:23:55.354838+00:00python-feedgenhttps://github.com/superleeyom/blog/issues/582024香港银行卡办卡之旅2024-04-24T05:23:55.570595+00:00前言

因为公司的业务需要,需要办理一张境外的银行卡,恰好我自己一直挺想搞一张境外银行卡,经过相关同事的推荐以及小红书调研,最终决定办理一张招商永隆银行卡(实体银行,大多数同事都是选择的这一家)和一张众安银行卡(虚拟银行),下面就这次的办卡做下简单的记录。

所需要的资料

  • 1、身份证
  • 2、港澳通行证
    • 保证签注有效,长沙的出入境办事处基本上都支持自助续签
  • 3、往返的车票
    • 我是年后 2024.2.20 去的香港,长沙有直达香港西九龙的高铁,所以还算比较方便
    • 如果没有直达香港的高铁,可能需要在深圳北进行换乘,这个可以根据个人实际情况来
  • 4、人民币现金
    • 招商永隆卡需要账户里面有 1w 的港币,否则每个月会收取 100 港币的管理费,由于招商永隆的 ATM 支持直接存人民币现金,强烈建议直接带人民币过去(不超过 2w,否则过关可能会查),这样可以免去从国内跨境汇款到香港银行卡的手续费,存好后,后面就可以自己直接在招商永隆的 app 上自行进行换汇
    • ZA Bank 没有管理费的要求,所以也就不需要带现金
  • 5、手机下载招商永隆 app
    • 需要提前在招商永隆 app 里面填单进行预约,然后在预约成功后,会收到预约成功的短信(BK 码),3 个月之内去香港进行面核开户
      • image.png
  • 6、手机下载 ZA Bank app
    • 这个 App 在 App Store,好像需要外区 ID (我用的美区)才能下载,国区 ID 不行
    • ZA Bank 的开户必须要求人在香港,由于 ZA Bank 是虚拟银行,所以全程都是线上开户,相对比较简单
  • 7、香港数据漫游包
    • 因为要接收手机验证码,所以必须要开通数据漫游包,否则收不到验证码
    • 我用的中国联通的境外数据漫游包,25 块钱一天,无限流量,1g 的高速流量,超过后限速,基本上用一天绰绰有余,可以在中国联通的 app 客户端办理,去的当天办理好就行,快要到深圳的时候,手机打开漫游【数据蜂窝漫游选项】,过关的时候,就会自动切换成香港那边的运营商
    • image.png
    • 中国移动的也类似这样开通,这里不赘述
  • 8、开通微信搭车码
    • 微信小城程序搜【搭车码】
    • 在香港做地铁的时候,需要刷二维码,提前开通搭车码,方便坐地铁
  • 9、充电宝
    • 最好备一个充电宝,防止路上手机没电
  • 10、若干港币现金
    • 这个我觉得可带可不带,因为大部分的商店都支持微信支付和支付宝支付
    • 但是有些比较老的店,只支持现金,这个看个人需要

交通

长沙南 - 香港西九龙

  • 当日方案: 早去晚返高铁各有一趟
  • 隔日方案: 长沙南 - 深圳北 - 西九龙: 可选车次更灵活, 深圳北九龙车次很多, 不过需要隔日了

香港西九龙 - 招商永隆中环总行

  • 微信搜索「搭车码」小程序,直接乘坐香港地铁,建议提前开通下
  • 香港西九龙站出关后,根据导航达到佐敦站「荃湾线」,然后乘坐地铁,佐敦站=》中环站 B 出口
  • 中环站 B 出口后,步行 100 米就到了招商永隆银行

港卡

香港有很多的银行,比如汇丰、渣打、中银等,我因为自己个人业务需要,最终只办理了招商永隆卡和 ZA Bank 两张卡,就这两张卡做一个简介,其他银行可以自己按需办理。

招商永隆卡

  • 招商永隆是招商银行的香港分公司,在香港也算是一家老牌银行了,如果账户不足 1w 港币的话,需要每个月收取 100 的港币的管理费
  • 大陆招行卡转到香港招商永隆卡,国内要收手续费(金葵花免费,金卡5折手续费25 RMB 起)、电报费(100 RMB),香港永隆招行收 50 RMB 一笔,总计:150 RMB(金葵花)/ 175 RMB(金卡)
  • 香港招商永隆转大陆招行,招商永隆收 80 RMB 每笔,国内免费,总计:80 RMB
  • 港卡与港卡之间互转免费
  • 不支持绑定 Apple Pay
  • 支持绑定 WeChat Pay(香港钱包)

ZA Bank

  • 众安银行是一家虚拟银行,必须要求在港开卡,是香港第一虚拟银行
  • 全程线上开户,不需要任何管理费
  • 支持绑定 Apple Pay、WeChat Pay(香港钱包)、Google Pay
  • 这张卡比较适合用来支付一些海外的订阅服务
  • 该卡入金我试了下,跟招商永隆卡互转,不收取任何手续费,基本上是秒到账
  • 从内地或者境外银行转入,ZA Bank 这边跨境汇款入金不收取手续费,但是转出银行一般都是需要收费

其他

image.png

以上便是我这次去香港办卡的一些总结,这次应该是我第二次去香港了,第一次去是 2018 年的时候,去香港买 iPhone X,不得不感慨香港的物价是真的贵啊,基本上是内地的两到三倍。另外香港的交通也比较复杂,根据导航走,几次都迷失在高楼大厦之间。这次在香港逗留的时间比较短,行程比较匆忙,跟几个同事一起去的,大家在办完卡之后,简单吃了个烧鹅饭(54 港币一个人),当天下午五点便匆匆搭上回长沙的高铁,结束了这次香港之旅。

]]>
2024-02-25T06:46:12+00:00
https://github.com/superleeyom/blog/issues/572024年个人周报2024-04-24T05:23:55.664248+00:002024 年第 16 周
  • 本周日参加 2024 湘江半马,原计划是跑到 2 小时内,结果我还是高估自己了,最后差不多 2 小时 5 分完赛,最后 5 公里跑蹦了。
  • 周末考察了 3 家装修公司,只能说装修公司都是满满的套路,如果不做功课,很容易会被忽悠。

2024 年第 15 周

  • 本周跑步 3 次,下周末长沙湘江半马,随缘跑吧
  • Netflix 的《体能之巅:百人大挑战 2》上线了,感觉还不错,周末看了一集
  • 周六又去看了一家封窗,老板人还挺爽快的,聊得也还行,纳入考虑的范围之类
  • 最近一直在想装修的一些事情,现在在想到底是自装呢,还是找装修公司,也是比较头痛。

2024 年第 14 周

  • 感冒了一两周,总算恢复了
  • 清明节休息了三天,在家和老婆一起看了阮经天以前的老电影《艋舺》以及最新Netflix上映的韩剧《寄生兽:灰色部队》,都还不错,值得推荐
  • 最近在看封窗,最大的感受就是「乱」,定价乱,服务乱,标准乱,不做好功课,分分钟会被坑

2024 年第 12 周

  • 这周跑步 2 两次,这周工作太忙了
  • Netflix 版本的《三体》开播了,看了一集,感觉还不错,会继续追下去
  • 周末自己在家做饭:香煎鲈鱼、清炒空心菜、大蒜叶炒腊肠、干煸豆角、红豆粥等等
  • 周末这两天长沙温度很高,一到换季,总会感冒一下

2024 年第 11 周

  • 本周跑步 3 次,最近开始控制饮食了
  • 我老婆以前还蛮喜欢吃那种街边的炸淀粉肠,自从这周的 315 曝光了淀粉肠之后,我老婆就说以后再也不吃了
  • 最近老婆的奶奶身体不咋好,我们周末又回去了一次,老人在丧失了活动自理的能力后,真的是可怜

2024 年第 10 周

  • 本周跑步 4 次,天气放暖了,要继续开干了
  • 报名了 2024 年的湘江半马,4 月 21 日开赛,开始认真准备备赛了
  • 开始支持国产跑鞋了,入手了李宁的赤兔 7,目前穿了几天,感觉脚感还不错
  • 周末和老婆两个人又驱车回株洲了,乡下的生活就是放松

2024年第9周

  • 周末陪老婆回老家休息了两天,周末逃离城市回乡下充充电,短暂的放松一下
  • 周末看了最近大火的《周处除三害》,推荐一定要看非删减版的,陈桂林在灵修中心挨个爆头的戏,看的真过瘾呐
  • 最近读完了《蛤蟆先生去看心理医生》这本书,让我印象比较深刻的一句话:”所谓活得真实,就是真诚地回应当下的需求。这能打破从童年延续而来的因果循环,让真实的自我摆脱过去经历的束缚,在自由中成为真正的自己。“

2024年第8周

  • 本周继续开始锻炼了,周末健身房跑了2次,长沙最近天气是真的冷
  • 原本计划周末回老婆家的,但是最近冻雨,只能在长沙过元宵
  • 周末写了一篇博客:《2024香港银行卡办卡之旅》,记录下去香港办卡的过程
  • 开始使用笔记软件 Obsidian,个人感觉挺好用的,目前在 iPadOS 写作感觉很不错
  • 周六和老婆第一次去看新房子,有点小激动
  • 老婆的小红书粉丝一个星期居然已经超过了我的推特粉丝,有点不可思议

2024年第7周

  • 今年第一次去老婆家过年,也是我第一次去外面过年,非常不一样的体验,哈哈。
  • 今年也是老婆第一次来我家拜年,收到了不少的红包,哈哈。
  • 基本上整个春节都是摆烂状态,吃喝玩乐,准备收假,开始继续锻炼。
  • 辞别玉兔迎金龙,新的一年,继续加油!

2024年第 4 周

  • 长沙这周下大雪了,本周跑步 3 次,累计 14 公里。
  • 周末老婆加班,第一次开车接送她上下班,也是我第一次独立一个人在城市里面开车,感觉良好。
  • 周末炖了个萝卜排骨汤。
  • 最近看了三部韩国电影:《回家的路》(一部 2013 年的老片,根据真人事件改编,还不错,挺感人的)、《乌有之地》、《混凝土乌托邦》(这两部都是 Netflix 出的,马东石的演技撑起了整部剧,其他的一般般)。
  • 《太白金星有点烦》看完了,挺不错的,相当推荐。

2024年第3周

  • 上周没咋跑步,只周末跑步了2次。
  • 赶在年前给车子保养了一次,了解并学习了一下事故处理流程。
  • 周末天气非常的冷,看天气预报说周一会下雪,所以提前把车子从树下挪到了空旷的地方。
  • 周末宅家和媳妇一起看老剧《甄嬛传》,你还别说,还挺好看的,哈哈哈。
  • 周末做了两个菜:紫苏牛蛙和香煎鲈鱼。

2024年第2周

  • 上周跑了6次,累计25公里。
  • 周末做了几个菜:香菜牛肉、香煎鲈鱼、香煎鸡翅,其中香煎鲈鱼非常的成功,老婆非常喜欢吃,哈哈哈。
  • 买了个 CarPlay 便携屏,解决原车机不带 CarPlay 的痛点,但是拿到后发现了点小问题,机器的散热板有松动,又给退回去了,让老板重新发个过来,后面拿到后再看吧。

2024年第1周

  • 开启新的一年的个人周报啦!
  • 本周的天气终于不咋冷了,本周跑步3次,继续加油!
  • 最近在看韩剧《死期将至》,和老婆一起连刷了6集,看的真过瘾啦,强烈推荐。
  • 小说《太白金星有点烦》马亲王的文笔是真的牛逼,硬是把西游记,写成了当今职场社会现状,牛逼。
  • 周末去宜家逛了一下,看下宜家的家具设计,找找装修灵感,长沙的宜家是真的大啊,两层逛完估计得一两个小时。
  • 周六去医院复查了下身体,问题都不大,非常开心。
  • 我的 iPad Mini 6 被老婆征用了,重新用回了 iPad Pro。
]]>
2024-01-08T01:39:09+00:00
https://github.com/superleeyom/blog/issues/56Postman如何配置动态端口和IP2024-04-24T05:23:55.756050+00:00背景

起因是每次使用 Postman 调试服务接口的时候,如果服务一旦被重启,对应服务的ip和端口就会改变,就需要重新配置服务的ip和端口,非常的繁琐以及麻烦。所以就针对这个问题,做了一点研究,简单的记录下。

步骤

1、维护多套环境及相应的环境变量

实际开发中,我们一般会有多套环境,比如:开发环境dev、测试环境test、生产环境prod等等。每个环境,我们配置好对应的consul(注册中心)环境变量、对应服务名环境变量,如图所示:

image.png

2、维护collection目录层级以及脚本

建议以服务名为目录,在对应的目录层级,维护如下的脚本,动态的获取注册中心上对应的指定服务的ip和端口:

pm.sendRequest(pm.environment.get("consul_service") + "trade-assets-service",
function(err, res) {
    if (err) {
        console.log(err);
    } else {
        pm.environment.set("trade-assets-service", res.json()[0].ServiceAddress + ":" + res.json()[0].ServicePort);
    }
});

image.png 注: "trade-assets-service" 根据自己目录名变化

3、在对应的collection目录模拟request请求

维护好了动态获取脚本后,就可以正常的模拟对应的request请求,请求里面的ip和端口只需要填充对应的服务的占位符即可:

image.png

]]>
2023-12-14T11:51:00+00:00
https://github.com/superleeyom/blog/issues/552023上海之旅2024-04-24T05:23:55.843525+00:00
  • 初衷

  • 今年12月份我和女朋友马上就要领证了,想着我们俩自从疫情之后,就从来没有一起出去旅游过,所以女朋友提议,将原本用于求婚和购买钻石的预算,用来作为这次难忘之旅的资金,两人一拍即合,随即便踏上了上海魔都之旅。

  • 在做好一系列准备工作后,包括预订酒店、购买机票和火车票、制定详细攻略等,我们于11月18日周六上午9点从长沙出发,飞往了上海。顺利地,我们于10点45分抵达了上海。我们的运气真是不错,这个周末上海的天气异常宜人。而且正值冬季,感受到那抹暖暖的阳光,简直美不胜收。在这个季节里漫步在街头,仿佛是置身于一片宁静而温暖的画卷中,让人感到无比惬意。

  • 上海的地铁网络非常发达,整个旅途我们基本上都是乘坐地铁交通。而且我发现一个有趣的细节,就是上海的地铁站似乎没有像其他地方(比如长沙地铁)那样,工作人员拿个金属检测仪在乘客身上扫来扫去,这一点真的是好评啊。只需行李通过安检,就能完成整个安检流程,这样的效率和体验真的提升了不少。

  • 沪西老弄堂面馆

  • 一踏上上海的土地,我们迅速乘坐地铁前往第一站:沪西老弄堂面馆(静安寺店)。在B站上听到不少UP主推荐过这家面馆,加上我自己也是一位面食爱好者,自然是迫不及待地想要品尝一下上海的地道味道。抵达时恰好是饭点,因此排队的人群相对较多,我们排了差不多半个小时的样子才吃上。

  • 202311211608024

  • 202311211609220

  • 虽然价格相对于外面一般的面馆稍贵,人均消费在40多元,但我们决定尝尝他们家的招牌菜。我们点了一份大肠拌面、一份蛤蜊猪肝拌面,以及一份炸猪排,总共花费了93.5块。面的分量十分充足,浇头和面搅拌均匀后,让人垂涎欲滴。大肠、蛤蜊和猪肝的味道都带有一丝甜味,非常独特。每一口面都充满了风味,吃完后味蕾得到了极大的满足感。至于炸猪排,虽然相对普通,但搭配当地的辣酱油沾食,味道也颇为不错。总的来说,这次的用餐体验是物有所值的。

  • 202311211458811

  • 外滩 & 东方明珠

  • 从沪西老弄堂面馆吃完中饭后,我们搭乘地铁前往上海的著名景点:「外滩」,去看上海的地标:「东方明珠」。当天是周六,天气格外宜人,晴空万里,简直是理想的拍照日。外滩的人流相对较少,主要是一些观光游客和当地居民悠闲漫步。沿着黄埔江畔,走走停停,晒着冬日暖暖的阳光,好是惬意。我们还前往了万国建筑、和平饭店以及南京步行街,沿途漫步在上海的繁华之中,深刻感受上海这座城市的生活气息。

  • 202311211514212

  • 武康路

  • 武康路是我从关注的博主 hayami 那里得知的,这次来上海,当然不能错过这个著名的地方,去亲身感受一下武康大楼和武康路的魅力。武康路两旁是各种有着历史沉淀的精致老洋房,整条街道保持着干净整洁。沿途行走,感受到老洋房的宁静与历史沧桑。街道两边都是一些打扮精致的帅哥和美女在散步,沿着武康路一路走走逛逛,体验下上海的city walk文化。

  • 上海迪士尼乐园

  • 这次上海之行中,最让人难以忘怀的莫过于去上海迪士尼乐园,这简直是无法与其他任何经历相提并论。周日一大早,我们早早地离开了酒店,搭乘去往迪士尼的班车。上午9点,我们抵达了迪士尼乐园,或许是因为周日的天气宜人,游客络绎不绝。在此之前,我们在小红书上查阅攻略,得知周日可能相对人少,然而实际情况却并非如此,人潮涌动,倍感焦虑。不过好在女朋友一路耐心的开导,让我有了更多的期待。即便人再多,也无法减弱我们对迪士尼的期待与热情,哈哈哈。

  • 202311211601424

  • 在上午9:30左右入园后,我们首先体验的是「雷鸣山漂流」。这个项目相对来说比较温和,八个人一组,全程需要穿上雨衣,以防「湿身」。整个漂流过程中,最为刺激的部分莫过于穿越那个黑色的隧道,一片漆黑之中,突然响起的雷声,隧道内部还有一只巨大的恐龙在咆哮,营造出了一种紧张刺激的氛围。(ps:这个我网上找的图,当时没法带手机拍摄)

  • 202311211936597

  • 在完成「雷鸣山漂流」的体验后,我们前往观看了「风暴来临:杰克船长之惊天特技大冒险」舞台表演。这对我来说是在迪士尼见到的最为震撼的演出之一。演员们在游客进入剧场之前就开始与观众互动,引导大家积极参与。整个演出高潮迭起,而最令人难忘的时刻莫过于在一片雾气中,舞台前的视野模糊不清,然后幕布缓缓升起,揭开了整个实景海盗船的神秘面纱。这一刻,我不禁起了鸡皮疙瘩,视觉上的冲击感太过强烈。真心推荐这个项目,是迪士尼乐园中不可错过的精彩表演。

  • 202311211945349

  • 202311211947442

  • 第二个让我深感震撼的项目是「奇幻冬日巡演」。在这个表演中,迪士尼的各位朋友们都纷纷登场,包括米奇、米妮、唐老鸭、白雪公主、七个小矮人等童年时代的动画人物。他们欢快地与游客打招呼,沿着巡演路线边走边跳。看到这些可爱的动画人物真实地出现在眼前,仿佛时光倒流回到了那个纯真的童年时代,让我感到无比怀念。这一刻,迪士尼乐园成为了一个魔法般的世界,带领我重温了那段美好的童年回忆。

  • 后面我们还去体验了「创极速光轮」,这是一个相当刺激的项目,有点类似过山车的感觉。欣赏了「冰雪奇缘:欢唱盛会」表演,这是一场充满歌舞的节目,是个逛累了可以休息的好去处。

  • 最后,我们来到了城堡附近拍照,这里的景色非常出色,尤其是与另一半一同来迪士尼时,务必要在这里留下美好瞬间。城堡周围拍照真的特别出片,是个打卡必备的地方。这次在迪士尼的冒险之旅,不仅让我们体验了刺激的项目,还让我们在梦幻般的城堡前留下了珍贵的回忆。

  • 确实,迪士尼的物价确实有些离谱,火鸡腿85元一份,玉米35元一根,的确是比一般地方高出不少。在迪士尼游玩时,可以自备一些小零食和小面包,在排队等待的时候解决一下饥饿感。另外,如果想要吃饭,可以考虑到园区外的迪士尼小镇。那里的物价相对便宜一些,我们两个人晚餐在「大食代」,点了一份酸菜鱼和凤爪,总共花费100出头吧,味道还不错,而且能够吃饱。

  • 感受

  • 这次突发奇想的旅行对我而言太有意义了。自2017年以来,我一直没有好好出去玩过,这次和女朋友一起在上海度过的两天,让我收获了极大的快乐。我们手牵手漫步在上海的街头巷尾,品尝了当地特色美食;一同进入了迪士尼童话世界,体验了各种刺激的项目,观赏了震撼的表演,与可爱的卡通人物亲密接触。上海这座城市充满了独特的魅力,让我留下了美好的回忆,期待着再次踏上这片充满生机的土地。

  • ]]>
    2023-11-21T12:36:58+00:00
    https://github.com/superleeyom/blog/issues/54我的跑步数据流2024-04-24T05:23:55.929967+00:00
  • 前言

    • 现在大多数的健身类APP,都不愿意把自家的数据开放给普通的用户,作为一名程序员,同时也作为一名跑者,我一直在探索如何才能主动掌握自己的跑步数据,直到现在,我终于可以掌握自己的跑步数据流,今天就来分享下,我是如何管理自己的跑步数据的。
  • 设备

    • Smartisan T2 --> iPhone X --> Apple Watch Series 4
    • 我是2017年开始跑步的,那个时候就是用手机(Smartisan T2、iPhone X)记录跑步数据,直到2018年,我的朋友推荐我使用 Apple Watch,当时恰好发布最新款的 Apple Watch Series 4,在京东上首发购入了蜂窝版本的 Apple Watch Series 4,一直使至今。如今四五年过去了,这块手表除了续航,其他的一点问题都没有,对于习惯跑步健身的朋友,Apple Watch 简直是完美的搭档,如果你使用的是 iPhone,真的强烈建议购买一块。
  • 健身应用

    • 咕咚 --> Keep --> Nike Run Club --> Apple Watch 体能训练
    • 起初为了能激励自己能更好的坚持跑步,我开始尝试加入国内的跑步社区,我最初使用的是咕咚 APP记录我的跑步数据,后面发现咕咚 APP的广告越来越多,最终弃用,然后又转战 Keep,结果使用了一两年,Keep 也因为业务扩展,APP 内到处充斥着广告、短视频,无奈也最终弃用。后面经过推友的推荐,我开始尝试使用 Nike Run Club,非常的对我口味,无广告,简洁舒适,Apple Watch 也原生支持,可惜22年的时候,Nike关闭了中国大陆地区的 Nike Run Club 运营,最后,我干脆就直接就用 Apple Watch 原生的体能训练,来记录自己的跑步数据,原生自带,简洁干练,无广告,无干扰,唯一的缺点就是对于数据统计这块比较弱。
  • 数据流

    • 为了一直想补齐数据统计这块的缺陷,在偶然的机会下,了解到国外的一款健身APP:Strava,这个不就是我想要的跑步 APP 吗?无广告,开放的 API,丰富的数据统计分析(部分功能需要订阅),毫不犹豫在某宝上,开通了1年的会员(官方自带的订阅比较贵,建议上神奇的某宝,可以省不少钱)。
    • 这样,每次使用 Apple Watch 的原生体能训练跑完步后,数据会自动汇总到 Apple Health(健康)应用,然后 Strava 会自动读取健康应用里面的体能训练记录并上传,当然要实现这个自动化需要完成如下两个设置:
      • 1、在iPhone【系统设置-通用-后台App刷新】中给 Strava 开启后台刷新权限。PS:iPhone的低电量模式会关闭后台 APP 刷新权限
      • 2、在 Strava 应用内【应用程序、服务和设备-健康应用程序】,打开【自动上传】
      • iPhone设置和Strava设置
    • 只要数据上传到了 Strava,我们就可以借助 Strava 开放的 API,拿到我们自己的数据,再借助 running_page 项目上传到其他的平台,比如佳明、Nike Run Club 等等,将数据放到多个平台备份。
    • 所以我的整个的跑步数据流如下:
  • 总结

    • 以上便是我的整个的跑步数据流,所有的流程都是自动化执行,你要做的就是点开体能训练,开始锻炼就行。在大数据时代,将自己的数据掌握在自己的手里,心里会非常的踏实,也希望能给有相同需求的朋友带来一点启发,如有疑问,有问题欢迎和我交流。
  • ]]>
    2023-02-18T04:10:49+00:00
    https://github.com/superleeyom/blog/issues/532022年终总结之我的买房经历2024-04-24T05:23:56.007628+00:00
  • 1、前言

    • 其实我原本想偷懒不想写这篇文章的,但是后面想想,买房也是人生的一件大事,把这个整个过程记录下来,其实也是蛮有意义的,所以就有了今天的这篇文章,整篇文章记录了我整个买房的心路历程,也借此分享给那些正在考虑买房的朋友们,希望大家都能早日上车。
  • 2、为什么想要买房

    • 我曾在2021年离开深圳的时候,写了一篇《深漂5年随想》 的文章,里面曾写到:「我觉得我想离开深圳的最大的一个原因是自己对房价的绝望吧」,常年的租房生活,让我实在是受够了那种漂泊和居无定所的状态,我就特别渴望拥有了一套属于自己的房子。机缘巧合,我来到了号称省会房价洼地的城市:长沙,在这工作和生活一段时间后,慢慢的喜欢上了这座城市,所以脑袋里面就萌生了要在这里买房的想法。
  • 3、买房前期准备

    • 3.1、户口迁移

      • 要在长沙这边买房,首先你得要有购房资格,目前就说最常见的两种,更详细的可以见:《长沙购房资格》
        • 1、户籍不在长沙,只需要连续缴纳 2 年的长沙社保
        • 2、户籍转到长沙,在长沙工作,连续缴纳1年的长沙社保
      • 所以很明显我不符合条件1,只能靠条件2,所以在2021年10月国庆节的时候,我把自己的户口从邵阳老家,迁到了长沙。迁长沙户口其实比较简单,直接可以在线操作,通过 APP 我的长沙户口服务-自贸区人才引进落户,填写相关的信息,即可完成落户,预计等待 1周的时间,就可以收到新的户口页,原本的旧的户口页也就失效了。
    • 3.2、公积金迁移

      • 由于后期买房肯定需要公积金贷款,所以我就计划把深圳的公积金额度全部转移到长沙的公积金中心来,但是由于政策缘故,必须要在你工作的城市工作了6个月后,才能将公积金转移到另外一个城市,所以公积金的迁移,是在 2022年的3、4月份的时候操作的,这个步骤也可以直接在线操作,住建部推出的“全国住房公积金小程序”,手机上点一点,就可以轻松办理公积金转移接续,还是蛮方便的,具体的流程可以见:公积金转移接续,手机上就能办啦!(附操作指南),转移大概花了 10个工作日的样子,就完成了转移。
    • 3.3、与置业顾问的初次接触

      • 在2022年4月份的时候,我的一个大学同学过来长沙玩,从聊天中得知他在长沙买了房子,他的表哥刘哥就是他的置业顾问,恰好那个时候我也初步有打算买房的想法了,然后就加了刘哥的微信,两个人碰面聊了一两个小时,刚开始的时候,对于房地产这块算是一窍不通,通过和置业顾问的简单唠嗑后,我也算是对长沙房地产有了一个初步的了解,但是由于我购房资格最迟都要到2022年的10月份才有,所以刘哥的建议是要我再过几个月后再来,前期可以先明确自己要买的片区和楼盘,等到了大概九、十月份的时候再联系他,期间不懂的都可以问他。
    • 3.4、购房知识的学习

      • 初期我对房地产这块确实是啥都不懂,什么户型图,楼盘,真的是一窍不通,我当时还在推特上专门发了条推,结果一堆的热心的推友来建议:
      • 后面我也忘了是在哪里看到了(知乎?),有个专门做长沙房地产的公众号:春小楼,他写了很多针对小白用户买房的教程,专业术语介绍,楼盘测评,满满的全是干货,对于刚准备买房的小白用户,真的是太有用了,真的非常推荐这个公众号。
      • 另外除了公众号,可以在安居客 APP 上查看各种楼盘信息,每天可以没事的时候,随便看看,了解下楼盘信息,但是千万不要注册手机号,我之前就是因为不懂,注册了手机号,导致手机号被泄露,天天被一堆的房产中介骚扰,苦不堪言。
      • 这里对于长沙房市,可以再推荐一个小程序,叫做:长沙选房通,这上面可以了解长沙所有的楼盘信息,非常的实用。
    • 已上几件事情准备完毕后,基本上只需要静静等到购买资格到手了。
  • 4、开始准备买房

    • 在储备了部分的购房知识之后,时间也来到了2022年的10月份,这个时候我也有了购房资格了,恰好国家也出台了很多购房利好政策:公积金最高额度可以到70w和商贷利率降至4.1,这个时候我觉得这是最近几年最好的购房时机了。
    • 4.1、明确购房预算

      • 首先需要明确自己手里有多少的钱可以准备首付,自己的月银行流水是多少钱。打个比方说,你手里有50万来准备首付,最简单的算法是按首套三成来算,50万除以0.3,得出166万的房款总价。但是这个还不够,你还要把维修基金和契税也算在内,这两者按4万元去预估,也就是你的首付其实只有46万元。另一个方面你可以贷款的金额跟你的银行流水也有一定关系,流水需要覆盖你的月供2.2倍。这样算来,你可以接受的房款总额为153万元,按揭贷款总额为107万元,这样30年贷款月供为5517.31元,你的银行流水需要12000元以上,才能承受这个月供。
    • 4.2、明确购房需求

      • 刚需和改善是现在购房者买房的主要倾向,我给自己的定位就是刚需,主要的需求点如下:
        • 1、主要还是自住
        • 2、交通便利性,有地铁,上班交通方便,通勤时间在半小时以内
        • 3、增值以及流通性,考虑后期置换的可能
        • 4、最低要求三室户型
        • 5、居住环境安静,楼层考虑中间偏上
        • 6、周边有学校,考虑到小孩上学问题
      • 所以明确好自己的需求点非常重要,因为:在预算有限的情况下,真的没有十全十美的房子,带着自己的需求点,才能后续更好的挑选适合自己的房子。
    • 4.3、片区的选择

      • 由于我自己目前从事IT软件行业,因为河西有麓谷软件园,利好互联网行业,而且我现在上班的公司也在河西,所以我压根不考虑河东,只考虑河西的楼盘。河西这边目前两个区的就是岳麓区和望城区,我个人考虑的是岳麓区,退而求其次再考虑望城区。
      • 区域选择好后,进一步就是选择板块,通过我自己的分析,我刚开始初步考虑的几个板块如下:
        • 滨江,月亮岛,谷山,麓谷,市政府,梅溪湖,洋湖
      • 后面我觉得梅溪湖和洋湖离我目前上班的地方(湖南省金融中心)还是太远了,就不想考虑,进一步缩减范围:
        • 滨江,月亮岛,谷山,麓谷,市政府
    • 4.4、楼盘的选择

      • 在确定好片区后,基本上就可以根据自己的预算,选择适合自己的楼盘了,按照我自己的经验,通常选择楼盘需要考虑如下的几个点:
        • 1、开发商:在面临各种房地产暴雷事件后,选择国企&央企背景的房地产公司真的太重要了,即使暴雷了,好歹也有国家兜底,毕竟大部分的人买房都是掏空了家底,谁不愿意看到自己买的房子无法交付。
        • 2、地段:地段好,以后的房子升值空间大,房子流通性高,地段也是我们要考虑的一个点。
        • 3、户型:户型这个主要就是看自己喜欢了,我的计划是最低要3/4室2厅2卫,这样的话,基本上可以满足以后的三口之家,自己和老婆1个房间,小孩1个房间,要是以后有亲戚或者父母过来住1个房间,所以预算有限的情况下,尽量的考虑3~4室的户型。
        • 4、学区:考虑到以后小孩上学问题,居住的小区有小学和初中也是非常重要的点。
        • 5、交通:地铁一响,黄金万两,所以如果购买的房子有地铁,真的会极大的增加出行的便利性。
        • 6、周边配套:周边生活设施是否齐全,比如超市,商超,菜市场等等这些,不然买个菜都还的去很远的地方。
        • 7、价格:在明确了自己的购房预算后,基本上价格这个事情也就明确了,这里要注意点就是毛坯和精装的区别,目前大部分的楼盘都是精装,极少数的是毛坯,在长沙这边,房地产的精装的预算一般是2500的价格,但是听身边的同事反馈,实际交付的质量都不值2500,所以如果能选择毛坯,尽量还是选择毛坯吧,以后自己花点时间和精力装修。
        • 8、其他:其他的要考虑的点就是比如:交付时间、精装信息、塔楼还是板楼、楼层高度等等,这些点自己可以酌情考虑。
      • 我自己比较懒,实地去看的楼盘不多,目前粗略的总结了我目前看过的几个盘,通过Excel 汇总了下,有部分数据可能不准确,仅供参考:2022购房楼盘总结
      • 其实到最后我只考虑两个盘:中建麓江府和天健云麓府,由于中建麓江府需要认筹抽签,我当时打算是:如果抽中,就买中建麓江府,否则就买天健云麓府,结果认筹当天,中建麓江府的认筹人数没有超过房源,最后采取了按认筹顺序选房,当天和女朋友一起选到了我们还算满意的楼层,最终上车了中建麓江府,我总结了下我选择麓江府的几个主要原因:
        • 1、开发商是有国企和央企背景的中建和长沙轨道交通联合打造的,背景还算靠谱
        • 2、地段位于长沙二环内,滨江板块,靠着湖南省金融中心,离我上班的地方也挺近的
        • 3、118的户型非常不错,可以做四室,类板楼,18层的小高层
        • 4、小区自建幼儿园,小区门口紧挨着白马实验小学,初中长郡滨江中学也离的很近
        • 5、目前地铁已开通的有福元大桥西地铁站,已规划的有8号线岳华路站,过了福元大桥就可以去开福区中心北辰三角洲
        • 6、周边配套有凯德壹、渔人码头、湖南省金融中心、奥克斯广场,都已经非常成熟
        • 7、价格的话是 1.43w 的价格买的毛坯,说实话价格确实比较贵,但是还是那句话,一分钱一分货,没有十全十美的房子,你要想它各个方面都满足自己的要求,你就得承受它的高昂的价格
    • 4.5、贷款的选择

      • 楼盘选好了,楼层也确定了,剩下的就是要交首付款了,这里就涉及到一个贷款方式的选择:
        • 等额本金:逐期递减还款,总利息最少,但是每月月供都在变,越来越少
        • 等额本息:每期等额还款,总利息多
      • 我咨询了很多的朋友他们目前的还款方式,大部分的是选择等额本息,他们的建议是如果以后有提前还款的想法,就选择等额本金,否则就选择等额本息,考虑到通货膨胀以及我自己后期并无提前还款计划,结合我自身目前的需求,我最终选择了等额本息的还款方式,首付3.6成,一共61w,总贷款107w,贷款方式采用组合贷,70w的公积金,3.1的利率,37w的商贷,4.1的利率,30年月供4700~4800,总体在我的承受范围之类。
  • 5、买房后续

    • 基本上交完首付,剩下的就是签合同、公积金面签等一系列的繁琐的手续,需要准备的资料也是一大堆,我粗略整理了下如下的资料:
    • 合同现在都是线上签署,用的APP叫 长沙住房,大家签署的基本上都是一样的,如果不放心可以花点钱让专业的人审一下。
    • 再个就是首付款的收据一定要保管好,后期交房的时候,需要拿这个首付款的收据去换那个带税率的发票,所以这个不能搞丢了,后期一定要保管好。
    • 基本上这些搞完,公积金贷款放款后,就开始还贷了,30年的换贷生涯就开始了。
  • 6、总结

    • 我在 2022年11月13号的晚上,发了条推总结了自己当时买房后的感触:
    • 但是现在再仔细想想,我感觉一切都是值得的,对于我们这种农村娃来说,要想在大城市安家,确实挺难的,但是只要迈出第一步,后面的问题自然而然都会慢慢解决。房子是家的依托,虽然租房也可以使生活愉快,但从心底里不觉得是家,有一个自己的小窝,给身边人一个稳定的家,这就是我买房最大的动力吧。
    • 以上便是我整个2022年的买房经历,希望能帮助到大家!有问题欢迎和我交流!
  • ]]>
    2023-01-08T09:35:00+00:00
    https://github.com/superleeyom/blog/issues/522023年个人周报2024-04-24T05:23:56.104825+00:00
  • 2023年第52周

    • 跑步 3 次,结束了2023年这一年的训练!
    • 12.26 持证上岗了,这是今年最值得纪念的一天。
    • 元旦节陪老婆回家休息了3天,在家追Netflix的新剧《纸钞屋:柏林》
    • 2024 继续加油!
  • 2023年第51周

    • 本周跑步5次,还是健身房暖和,室外太冷了。
    • 读完《这样装修不超支、不被宰、不返工》,虽然本书出版时间比较久了,但是对于我这种装修小白来说,还是收获颇丰,了解了一些装修行业的基本知识,接下来准备看《装修常用数据手册》。
    • 周末做了个冬笋炒牛肉,嘎嘎下饭。
    • 周末沿着湘江边开车兜风,开到了东窑港,一路江景怡人啊。
    • 最近在追韩剧《甜蜜家园》第二季。
  • 2023年第50周

    • 这周事情太多了,只周末锻炼下。
    • 周一去给房子维权了,算是理解了马督工在《模范夫妻中国梦,毁在融创烂尾楼》那期节目里面说的:「最勤劳、最守法、最乐观的公民不配得到中国梦,其他所有人更不配」。
    • 这几天长沙天气非常的冷,外面的风吹的人直哆嗦,周末趁着附近的超市周年庆打折,和女朋友两个人大采购,周末自己在家做了一顿火锅,非常的安逸。
  • 2023年第49周

    • #训练日记 本周锻炼6次,冬天锻炼还是健身房要舒服,这周开始尝试在健身房进行力量训练了,里面的各种器材还在摸索。
    • 《鱿鱼游戏:真人挑战赛》追完了,没想到看上去最不起眼的人居然最后夺得了胜利,太厉害了。
    • 读完了《神们自己》,就是结局有点嘎然而止,让读者自己去想象,挺不错的一本科幻小说。
    • 和女朋友周六又去了一趟动物园,把嗨卡即将到期的权益抓紧时间用了。
  • 2023年第48周

    • #训练日记 最近天气变冷了,一直在健身房跑步,本周跑步6次,腿感觉恢复的差不多了,重新跑步的感觉真好!
    • 最近在看 Netflix 新出的综艺:《鱿鱼游戏:真人挑战赛》,感觉还不错,脑洞大开。
    • 周末陪女朋友又回了一趟攸县,顺便回家休息下和拿户口本。
  • 2023年第47周

    • #训练日记 本周主要是跑步(4次)+散步(3次),最近又开始去健身房了,冬天外面跑步太冷了。
    • 周六和女朋友去拍了结婚登记用的照片,拍的还挺好看的,哈哈。
    • 周日陪女朋友去讨薪。
    • 周日炖了个冬瓜排骨汤,非常好喝。
  • 2023年第46周

    • 这周主要是以散步为主,每天散步4公里
    • 这周最有意义的就是和女朋友一起去了一趟上海:2023上海之旅
  • 2023年第45周

    • 本周依然没有跑步,感觉脚好了不少了,这周主要依然是散步为主。
    • 最近在看阿西莫夫的科幻小说《神们自己》,同时由于马上要交房了,最近在补装修方面的知识,在看《装修做好三件事就够了》,填补下自己对装修领域的知识空白。
    • 计划12月份领证,周末陪女朋友回了趟老家拿户口本。
    • 趁着双十一,在闲鱼以125元的价格,续费了一年的微信读书会员,感觉还算比较实惠。
    • 看了大鹏的电影《第八个嫌疑人》,前面剧情还挺不错的,只是后面结尾也太唐突了,不怎么推荐。
  • 2023年第44周

    • #训练日记 本周没有跑步,右腿应该是颈膜炎,折腾了好一段时间了,周末去买了一瓶扶他林,感觉效果还可以,本周就是每天散步3km+。
    • 周六去了华谊电影小镇,都是一些现代化建筑,不过还挺适合拍照的。
    • 周日买了结婚的婚戒,花了快4k。
    • 《进击的巨人》终于完结啦🎉。
  • 2023年第43周

    • #训练日记 本周3次跑步,1次跳绳,2次散步,右腿的疼痛始终没有好,打算下周完整休息一周,不再进行剧烈运动。
    • 周六去了铜官窑古镇,我原本以为铜官窑没啥人去玩,结果晚上人还挺多挺热闹的,各种行进式的夜游表演、打铁花、以及烟花秀都出奇的好看啊,还顺带看了大型历史情景剧《天宠湖南》,感觉值回了票价。
    • 周天去了动趣王国,这个就简单随便逛了逛,里面就是一些可爱的小动物,适合带着小朋友来玩。
  • 2023年第42周

    • #训练日记 本周4次跑步,2次徒步,整体强度都比较小。
    • 长沙的嗨卡马上就要到期了,开启了特种兵模式,趁着周末,去道林古镇溜达了下,把该有的权益都给他用掉。
    • 终于把《鞋狗》读完了,现在在看东野圭吾的推理小说《假面山庄》。
    • 最近发现B站上线了《富豪谷底求翻身第二季》,赶紧追起来,看下大佬们是如何从100美元挣到100w美元的。
  • 2023年第41周

    • #训练日记 本周3次跑步,1次跳绳,1次徒步,最近不知道怎么回事,右腿的脚踝处一直隐隐作痛,不知道是不是扭到了,下周的强度会降低一些。
    • 这周看了那个西班牙的电影《无处逢生》,还不错,可以推荐看看。
  • 2023年第40周

    • #训练日记 国庆回家基本上没有锻炼,每天就是吃吃喝喝,负罪感爆棚啊,接下来要好好锻炼了。
    • 整个国庆假期没有出去玩,回家完成了一件人生大事:订婚,一切都比较顺利,感谢家人的支持。
  • 2023年第38周

    • #训练日记 本周跑步3次,跳绳3次,周五休息了1天,最近喜欢上了跳绳,专门重新在keep上买了一副竞速跳绳,我感觉跳绳的燃脂效率要比跑步高不少,摇个1000下,全身都是汗,继续坚持吧!
    • 周六看了王宝强导演的《八角笼中》,拍的挺好的,当年那个《士兵突击》中憨厚老实的许三多,如今已经是满脸沧桑的大叔了,哈哈哈。
    • 因为国庆要订婚,所以趁着周末和女朋友去五一广场那边买了几件新衣服。
  • 2023年第37周

    • 本周的训练日志,
      数据汇总:
    • 周末去复查了下身体,整体上已经没有什么大碍了,钱没了可以再挣,身体永远是最贵的奢侈品
    • 周六参加了业主组织的维权,长沙真的是买房送维权,各种减配低配,迫不得已,大家只能选择线下拉横幅去维护自己的合法权益,「爱哭的孩子有奶吃」、「按闹分配」,目前是长沙房产的常态了,真的是可笑啊。
  • 2023年第36周

    • 本周训练日志,
      数据汇总:
    • 周末回了趟女朋友家,去攸县县城逛了一下,现在的金价太贵了,已经快接近600的价格了,原计划是打算买三金的,最后被这价格劝退了,还是等价格再降一点再买。
  • 2023 年第 35 周

    • 本周训练日志,
      数据汇总:
    • 女朋友周末马上就要生日了,所以周末带她去株洲方特玩了一下,由于现在很多学校都已经开学了,园区里面基本上没有啥人,大部分的项目基本上都不用等待,其中最恐怖的项目要属:大摆锤和过山车,真的太恐怖了,那种失重感和眩晕感,心脏承受能力不行的话,感觉会原地去世。顺便提一下,我们两个大冤种来的时候没带吃的,就带了几瓶水,中午饿的要死,最终在园区两个人忍痛含泪吃了人均 40 一碗的米线,一共花了 81,这应该是我吃过的最贵的云南米线了。
  • 2023 年第 34 周

    • 本周训练日志,
      数据汇总:
    • 上周装的 360,这周女朋友就出事故了,倒车的时候,直接怼上了消防栓,后保险杠被撞瘪了一小块,然后周六趁着调试 360,简单的处理了一下
  • 2023 年第 32 周

    • 本周的训练日志,
      数据汇总:
    • 本周最有意思的事情就是跟女朋友去浏阳凤凰峡漂流,超级刺激,凤凰峡的落差是真的大,到最后第二个落差点,我跟女朋友最后还是翻船了,好在两个人都没有受伤,只是女朋友的一只拖鞋和打水仗的工具都丢了,女朋友吓得不轻,给整哭了,整体惊险又刺激,好好玩,哈哈哈。
  • 2023 年第31 周

    • 本周训练日志,本周进行了 5 次的跑步训练,最近天气真的是太热了,不管是室外还是室内,基本上跑完就是一身的汗,不知道是不是扭伤了,左腿的外侧脚踝有点痛,这两天暂时先休息下,练练上半身,暂时先不跑步。
      数据汇总:
    • 前面两周工作太忙了,都没来的记录,这周趁着周末,从长沙驱车2个小时,回攸县坪阳庙兜兜风,这里大片荷花都开了,景色非常的漂亮,安逸的很
    • 周末回家吃了粉蒸猪脚、卤鸡爪、小龙虾、花甲,太爽了,还是家里的菜好吃
  • 2023 年第 28 周

    • 本周训练日志,本周进行了 6 次跑步的训练,主要目标还是希望通过有氧来减脂,周末刷小红书的时候,无意建看到了一家特别平民的健身房:嘿哈猫,25 元/月,这也太实惠了,所以立即搜了下租房附近的地图,发现确实有两家,还离得挺近的,然后选了其中一家体验了下,感觉确实还挺不错的,里面的基础的健身设施都有,非常的棒,主要是这价格,还要啥自行车
      数据汇总
    • 周末去看了新的电影《封神》第一部,感觉特效制作精良,演员演技也都在线,非常值得一看
  • 2023年第28周

    • 本周训练日志
      数据汇总
    • 周末逛商场给女朋友买了一双粉色阿迪的板鞋,她还蛮喜欢的
    • 周末看了电影《蒙上你的眼:逃出巴塞罗那》,感觉不值得一看
    • 最近工作上实在是太忙了,时间如流水一样,一晃一周就过去了
  • 2023年第27周

    • 这周开始慢慢恢复运动了,搬去新的地方对跑步不友好,到处都是上下坡
    • 周六和女朋友一起做了个全身体检
    • 周日和大学同学小聚了一下
    • 最近工作感觉太忙了
  • 2023年第26周

    • 身体恢复的也差不多了,开始重新开始跑步了
    • 周末2天连续搬家,真的是累死人,然后和女朋友一起去吃潮汕牛肉火锅庆祝了一下
    • 和女朋友一起去看了电影《消失的她》,剧情还挺不错的,连续反转,值得一看
    • 最近在看的韩剧《恶鬼》,还挺不错的,推荐
    • 最近在读的书《鞋狗》
  • 2023年第24周

    • 本周训练日志:无,恢复身体中
    • 又要搬家了,这周末和女朋友一起去找房子,折腾了一天,总算把房子给定下来了,等到明年自己的房子交房后,就再也不用搬家了
    • 周六去医院挂了个号,整体的情况并不算太坏,悬着的心总算放下来了
  • 2023年第23周

    • 本周训练日志:由于做了个小手术,接下来的半个月,主要将以行走为主要的锻炼方式,暂时就先不跑步了,医生建议不能剧烈运动,
      数据汇总
    • 这周四去医院做了一个检测手术,期间发生的一件事情就是,由于手术做完后,需要平躺6个小时,6个小时后,我就忍不住立马起身去上厕所,由于起身太猛,导致突发性血压降低,瞬间一下子感觉天旋地转,全身冒冷汗,心里非常的难受,女朋友看到我那样子,吓的都哭了,好在马上躺下后,慢慢又恢复了正常。住院这两天,女朋友一直陪着我跑上跑下的,还担惊受怕,真的是苦了她了。
    • WWDC23 Apple Version Pro,划时代的产品,可惜这高昂的售价,一般人还真玩不起。iOS、macOS、iPadOS 都是无关痛痒的更新,我反倒最期待的,就是 watchOS 10,运动上增加了很多新的功能,可以和iPhone联动了,值得升级。
  • 2023年第22周

    • 本周训练日志:
      数据汇总
    • 周末陪女朋友去讨薪,希望她下周五能顺利讨回剩下的最后一个月工资
    • 周六提前收到了女朋友送的30岁的生日礼物:Apple Watch Ultra,真的超级超级喜欢!
    • 上次去电影院和女朋友看了那个《速度与激情10》,她觉得还挺好看的,所以陪她周末一起把《速度与激情》系列的第1部重温了一下
  • 2023年第21周

    • 本周训练日志:
      数据汇总
    • 为了不浪费CS嗨卡的权益,周末和女朋友去了松鼠谷,去看了电影《速度与激情10》
    • 孙燕姿:我的 AI
  • 2023年第20周

    • 本周训练日志:
      数据汇总
    • 周末陪女朋友回家看她奶奶,希望老人家身体快点好起来。
    • 开了2000多公里后,算是安全渡过了新手期,上高速,跑市区再也不害怕了
  • 2023年第19周

    • 本周训练日志:
      数据汇总
      • {:height 321, :width 716}
    • 给女朋友买了一只 Apple Pencil 2代笔给她画画用
    • 给车子装了一个一键升窗器,感觉还挺实用的,不然老是容易忘记关窗户
    • 周末和女朋友去石燕湖玩了下,走了一下那个玻璃桥,我原本以为我自己不咋恐高,真站上去,还是心里有点发慌的
    • 试穿了下女朋友给买的新鞋Nike 飞马40,感觉穿着非常舒服,日常都可以当成通勤的鞋子来穿了,上一双飞马还是37
  • 2023年第18周

    • 这些时间都没咋锻炼,处于一个停摆的阶段,一路都是吃吃吃
    • 5.1这段时间,带父母去见了女朋友的爸妈,商讨了下结婚的事情,一切还比较顺利
    • 我以前不咋吃榴莲,女朋友一直说想吃榴莲,我就尝试着尝了一下,发现味道其实还蛮不错的,很甜很软糯,就是价格比较贵
    • 周日陪女朋友去讨薪,打工人真难
    • 最近看了下腾讯视频的《漫长的季节》,还不错,国产悬疑剧
  • 2023年第16周

    • 本周训练日志:这周跑了3次,其中周六有幸参加了湘江半马,成绩是1小时50分钟完赛,好久没有跑长距离,跑的还挺累的,
      数据汇总
    • 周六终于给新车的车牌装上了,总算完整了
    • 周六跑完半马和女朋友去吃了顿好吃的,最近长沙又降温了,跑完喝上一碗热乎乎的海带龙骨汤,舒服的很
  • 2023年第15周

    • 本周训练日志:
      数据汇总
    • 这周给车子贴了车窗膜,前铲,黑化车标,前后差不多花了快2k,整体效果还不错,非常帅气!
    • 周六和女朋友一起去她闺蜜家蹭饭吃,顺便参观下人家的新家,为以后的装修做下参考
    • 周日和女朋友去她小区楼下,一家叫喜洋洋的新疆烧烤吃烤羊肉串,5块钱一串,吃的是真的真扎实,物美价廉
  • 2023年第14周

    • 本周训练日志,这周六参加了招行举行的一个小比赛,5.8公里,28’39完赛,平均配速4‘54,累的不行,领了一堆吃的喝的:
      数据汇总
    • 周五刚提完车,周六就在老司机的陪同下上高速了,最大的感受就是,不要惧怕速度快,变道超车要果断,不能跟大货车,尤其是跟大货车并线的时候要快速超车,不然有很大的压迫感,眼睛视线往前看,注意和前车保持距离,前车刹车灯亮起的时候,油门松掉,高速方向盘幅度不能太大,非常危险。
    • 周日回长沙的时候,走国道的时候,路面有个大坑,等我发现的时候,已经来不及了,底盘被磕了一下,新手第一血
  • 2023年第13周

    • 本周训练日志,又是摆烂的一周:
      数据汇总
      • {:height 259, :width 716}
    • 本周五终于定了本田的英仕派,混动次顶配,全款现金优惠3.78w,送三年6次保养,送了一些不值钱的赠品(膜,行车记录仪,脚垫),最终全款落地21.76w,原本计划是买雅阁的,但是雅阁的销售态度以及优惠幅度,让我觉得性价比不高,最后一番思索后,选了雅阁的兄弟英仕派。
    • 自从拿了驾照后,就没有摸过车,周六去小红书约了一个教练,花了200块钱,练了3个小时,心里没有那么慌了。
  • 2023年第12周

    • 本周训练日志,本周没咋锻炼,工作太忙了:
      数据汇总
    • 买了一张英国的电话卡 giffgaff,用来注册国外的一些服务,同时把微信换绑后,callkit也可以用了
    • 终于把《黑暗荣耀》看完了,期待第二季
    • 最近看到的一句关于AI相关的评论:“木匠没有被电动工具取代,会计师没有被电子表格取代,摄影师没有被数码相机/智能手机取代,开发人员不会被 LLM 取代。现阶段的 AI 只会帮助我们提升学习、生活和工作的效率,我们要利用好 AI 的能力。没必要感到过于焦虑,AI 不会取代你,会 AI 的人会取代你。”
  • 2023年第11周

    • 本周训练日志,跑步4次,:
      数据汇总
    • 周末长沙下了两天的雨,宅在家追Netflix新出的爽剧《暗黑荣耀》
    • 报名了2023湘江半程马拉松
    • 买了一个新的跑步软件 WorkOutDoors,这个软件是真的值,直接可以在手表上根据导入的路线,并进行实时导航,自定义太牛逼了,推荐!
  • 2023年第10周

    • 本周训练日志,跑步3次,hit 2次:
      数据汇总
    • 女朋友的手机太卡了,帮她把 iPhone Xr 换成了 iPhone 14 256g
    • 在推上看到几张适合 Apple 产品的壁纸
    • 换了新的手机壳,非常的喜欢
    • 又去岳麓区的一家东本店看车,由于湖北的东本最近大打折扣,所以最近准备先观望一下,不急着买先,万一这风吹到湖南来了,岂不是变大冤种了
    • 周末和许久未见的大学同学小聚了一下
  • 2023年第9周

    • 本周训练日志,跑步3次,hit1次,爬楼梯1次:
      数据汇总
    • 订阅了Strava一年的会员,花费了158 RMB
    • 周末用电饭煲做了了玉米海带排骨汤(用椰子水煮的,非常的清甜)、卤味鸡爪
    • 周末又去看了1家东风本田 4S 店看车,对于这种 4S 店的销售模式很反感,同时还去看了特斯拉 model 3,还不错,可惜预算不够
    • 买了一个壶铃,用来晚上练练力量
  • 2023年第8周

    • 本周训练日志,3次跑步,3次hit:
      数据汇总
    • 周末去看车,体验了下2023款的本田思域两厢版本,自从2020年拿了驾照后,就再也没有摸过车,第一次开车慌得不行
    • 模范出租车第二季上线了
    • 第一次玩Zwift,线上虚拟跑步,还挺有意思的
    • 最近跑步在听告五人的新专辑《带你飞》
  • 2023年第7周

    • 本周运动4次,3次跑步,总计16km,1次室内hit
    • ChatGPT 应用汇总及操作手册,非常不错的ChatGPT手册,可以玩一玩
    • 写了一篇关于我自己的跑步数据流的文章:我的跑步数据流
    • 和女朋友去米粉街逛了逛,吃了一家特别好吃的土鸭馆:钱粮湖土鸭馆(八一桥店),非常推荐
    • 周末炖了个汤:萝卜海带排骨汤
    • 最近在读《价值:我对投资的思考》
  • 2023年第6周

    • 本周跑步1次,总计5.2km
    • 让 Apple Music 播放豆瓣音乐 Top 250,里面的专辑质量都不错,慢慢听
    • 奈飞新出的综艺《体能之巅:百人大挑战》,超级好看,满屏的肌肉型男,荷尔蒙爆棚
    • 女朋友给买了两套新的运动装,我那穿了好几年的速干衣终于可以换了
  • 2023年第5周

    • 元宵节去株洲给女朋友的老爸一起过生日,听女朋友说,老丈人还是第一次过生日的时候收到蛋糕,虽然嘴上说不要买这些,但是心里其实乐开了花,作为子女,我们确实好像对自己的父母表达爱意的时候,都显得难为情。
    • 元宵节回长沙后和女朋友一起去德思勤逛了逛,去鲜花市场给她买了束粉红色的满天星,她卧室的那束紫色的满天星都干了。
    • 最近把《狂飙》看完了,虽然是主旋律的电视剧,但是演员把人物角色刻画的非常完美,由其是高启强这个角色,简直完美。
    • Netflix的《终末的女武神第二季》上线了,看了一两集,还不错,可能我就喜欢看这类的热血动漫。
    • 自从把 Logseq 的字体换成了霞鹜文楷后,我越来越喜欢写作了,真的太喜欢这个字体了,果然好看的字体让人写作的时候会心情愉悦。
  • 2023年第4周

    • 大年初一去看了电影《流浪地球2》,可能是我除夕晚上一晚都没有睡好,看的我想睡觉,主要还是看特效,剧情一般般吧
    • 大年初二呆在家里玩,给女朋友的家人准备礼物,大年初三去外婆家拜年
    • 大年初四驱车四个小时去女朋友家拜年,见了女朋友家大部分的亲戚,她的家人太热情了,不是在吃就是在吃的路上
    • 最近出的国产剧《狂飙》还不错,过年期间一直和女朋友在看
    • 初六回长沙上班
  • 2023年第2周

    • 本周跑步2次,总计14公里,最近开始恢复跑步了,很明显体能下降很多,心率偏高,同时薅了 2个月的strava的会员,发现strava在数据统计这块做的还蛮不错的,先试用两个月看看
    • 和女朋友一起过小年,自己整了个小火锅吃,味道还不错,海底捞的汤底
    • 最近在读《what if》、《要钱还是要生活》,挺好的两本书,值得阅读
    • 最近一直在追动画版本的《三体》,1月15号电视剧版本的《三体》上映了,好像口碑还不错?有空看看
  • 2023年第1周

    • 本周做了一个新菜:酸辣鸡胗,但是感觉还是没有处理好,腥味有点重
    • 这周就是给女朋友的家人提前挑选各种礼物,因为过年要去女朋友家见她的爸爸妈妈
    • 陪女朋友买了两件衣服,一件羽绒服,一件毛衣,她很喜欢
    • 总结了下自己的2022年的买房心路历程:2022年终总结之我的买房经历
    • 已经好久没有锻炼了,每天就是陪女朋友一起吃吃吃,体重暴增
  • ]]>
    2023-01-03T06:20:52+00:00
    https://github.com/superleeyom/blog/issues/51解决 IDEA 因为 Clash 代理问题引起的疑难杂症2024-04-24T05:23:56.275156+00:00前言

    最近我的 IDEA 因为 Clash 的问题,出现了各种奇奇怪的问题,就这些问题的解决做一个简单的记录。

    1、You have JVM property "https.proxyHost" set to "127.0.0.1"

    由于我在 Mac 上开了 Clash 代理软件,接管了系统代理,打开 IDEA 的 Appearance & Behavior --> System Settings --> HTTP Proxy 界面,提示 You have JVM property "https.proxyHost" set to "127.0.0.1",解决方案就是:移除掉 Java 自带的 http 和 socket 代理,采用系统代理,选择 Help -> Edit Custom VM Options,增加如下的配置:

    -Dhttp.proxyHost
    -Dhttp.proxyPort
    -Dhttps.proxyHost
    -Dhttps.proxyPort
    -DsocksProxyHost
    -DsocksProxyPort
    

    重启 IDEA 即可解决。

    2、刷新 Maven 项目依赖,Build 控制台报 status: 502 Bad Gateway

    因为公司有专门的 Maven 私服,而这个私服是需要通过代理才能访问,无法直接访问,这个只需要在 Maven 的 setting.xml 配置文件中,增加 HTTP 代理就行,让 Maven 强制走 Clash 代理,比如我的 Clash 的 HTTP 代理端口是 7890,则配置如下:

    <proxies>
        <proxy>
            <id>clash proxy</id>
            <active>true</active>
            <protocol>http</protocol>
            <host>127.0.0.1</host>
            <port>7890</port>
        </proxy>
    </proxies>
    

    3、Maven 依赖包导入错误:GC overhead limit exceeded

    该问题是于 IDEA 里为 Maven 的 importer 和 runner 设置的 JVM 最大堆内存(-Xmx)过小而导致的,只需要将 Maven 如下的两个地方设置堆内存设置大点就行: 然后重新刷新下,就不会报内存不足了。

    ]]>
    2022-09-28T05:48:01+00:00
    https://github.com/superleeyom/blog/issues/50[笔记]神的九十亿个名字2024-04-24T05:23:56.353097+00:00
  • 这个是科幻三巨头之一的阿瑟.克拉克的作品,整书是由多篇科幻短篇小说组成,故事有趣又引人思考,其中最有意思的一篇就是本书的名字,非常推荐。
  • 如果能穷尽所有字母,完成所有可能的排列组合,那么我们一定能找到那位至高神真正的名字。我们的功课就是要列举出所有的名字。
  • 无论是高达两千英尺、直插深谷的巍巍断崖,还是偏远山谷中如棋盘般纵横交错的梯田,都已无法再让他提起兴致。他倚靠在被山风打磨得光滑如镜的石壁上,愁眉苦脸地盯着远处的群峰。
  • 对她来说,安静就像一张雪白的画布,必须要用她自己的声音来填满。
  • 惊恐如涨潮的海水,渐渐淹没了我的心智,为了对抗神秘莫测的宇宙,每个人都会竖起的理智与逻辑的大坝,这会儿也被冲垮了。
  • 一座城市,或者类似的东西。你已经看到它的规模了,所以可以自行判断建造它的文明发达到了什么程度。我们已知的整个世界——包括海洋、大陆和山川——就像是一层薄雾,包裹着这个超出我们理解能力的世界。
  • 极目远眺,直至目光所及之处,是一片支离破碎的贫瘠荒原,布满山峦与沟壑、火山口与陨石坑。那些山脉峰峦高耸,直抵下垂的骄阳,仿佛火焰冲天的孤岛,正在黑暗之海中熊熊燃烧——在它们头顶,群星依然夺目,光芒却恒久不变。
  • 夕阳西下时天空的色彩,海浪拍打卵石海岸时的低吟,雨滴落地的轻柔,积雪无声的祝福……这些,还有上千种其他的馈赠,原本是属于他的合法遗产,但如今他只能从书籍和古老的影像记录中得知这一切。一想到这里,他的心中充满了被放逐的苦闷与哀伤。
  • ]]>
    2022-06-25T02:16:16+00:00
    https://github.com/superleeyom/blog/issues/49[笔记]盐糖脂:食品巨头是如何操纵我们的2024-04-24T05:23:56.436423+00:00

    盐糖脂:食品巨头是如何操纵我们的 作者:迈克尔·莫斯 时间:2022-04-02 09:45:26 数量:24个笔记

    • 第四章 到底是谷物还是糖?:

      • 他将肥胖病称为“文明的疾病”。通过研究,他发现人们进食的欲望由血液中的葡萄糖含量和大脑下丘脑所控制,而这两者都深受糖的影响。
    • 第五章 我想经常看到运尸袋:

      • “从解剖学上说,我们时常提起气味和口味,”他说道,“但是每个人基本都忽略了味道的第3个方面,那就是体感,或者说是触摸的感觉。这种感觉包括二氧化碳气泡的刺激,或者咬一口辣椒的刺激,抑或是奶油的柔滑感等等。而说到可口可乐,最好玩的事情就是每当你喝可口可乐时,其味道可以满足这3方面的感受。你可以感受到香草和柑橘的芳香以及所有的棕色香料香味,比如肉桂以及肉豆蔻等等。接着你还会感受到甜味,最后还会感受到磷酸和二氧化碳的刺激。在喝可口可乐时,你真的可以从多方面刺激你的味觉神经,给你带来完全不同的味觉享受。
      • 世界上80%的可乐被20%的“重度使用者”消费。
      • 食品销售的重要性绝对不亚于食品本身。
    • 第八章 液体黄金:

      • 乳制品企业不是普通的企业。它们不受自由市场经济的制约。自20世纪30年代起,联邦政府就认定牛奶对国民健康至关重要,因此,政府会努力确保乳制品企业的繁荣。为维持乳制品价格稳定,联邦政府会给予其必要的财政补贴,并使用纳税人的钱购买所有剩余乳制品。结果,乳制品企业不用担心与正规商业公司竞争销量。它们不需要担心乳制品因产量过剩而滞销,也不需要思考如何针对重度消费者采取相应的措施,它们甚至不需要应对其他食品生产商为促进消费所实施的市场营销战略。它们生产多少奶制品,政府就会购买多少奶制品。
    • 第九章 午餐时间你说了算:

      • 最难的事情是发明能够畅销的产品,如果你的产品畅销,你就能够弄清楚如何获得最佳成本
      • 人们对食品的需求主要围绕着吃饭时间或地方是否方便快捷,关注的是食物的味道、价值或营养,可能还包括一些很微妙的细节,比如,人们吃东西的方式、时间、原因、地点以及吃了什么。这是第一点。我们并不创造需求,而是一直去挖掘、寻找需求,直到我们找到客户所需。”
      • 万宝路之所以取得成功,并不是因为它是最聪明的香烟制造商,而是因为它能够以最快的速度和最积极的态度去发现消费者不断变化的弱点
      • 妈妈与孩子们对于生冷比萨截然相反的看法,与他们饮食模式的不同有很大关系。成年人用嘴吃东西,吃的是味道。相比之下,孩子们更倾向于用眼睛,至少第一眼是先根据外观来判断食物。
    • 第十章 政府传递的信息:

      • 人体内大量的脂肪并不是来源于脂肪本身。因为无论是薯片还是甜点,其大部分的脂肪实际上都是来源于另外两种主要的加工食品原料。实际上,饱和脂肪的最主要来源其实是奶酪及红肉,这也是医生们最为忧心的脂肪来源。
    • 第十一章 无糖、无脂肪、无买卖:

      • 拉夫了解到,营养学的新研究已经发现,人体的重量控制系统更加善于消耗固体食品产生的热量,而不是流质食品产生的热量。
    • 第十二章 人们热爱盐:

      • 食用的钠过多,超出了人体所需的10倍甚至20倍。这个量远远超过了身体的承受能力。大量的钠将人体组织中的液体转移到血液中,增加了血容量,促使心脏更为强劲地跳动,之后就会引起高血压。
      • 当我们食用加工食品时,血液会被大量的盐、糖和脂肪重重包围。但是饮食与吸毒的联系中最有趣的部分还是发生在大脑中。毒品和食品,尤其是盐、糖和脂肪含量较多的食品,在大脑中的反应非常相似。一旦摄入这两种物质,它们就会沿着同样的通道,利用神经线路直达大脑的快感区。当我们因为做了正确的事情而感觉良好时,该区域就会给我们提供反馈,或者视情况而定。有时候,大脑也会误导我们,让我们对事情做出错误的判断。
      • 如果食用过多的盐就可能与其他因素共同作用致人发病。在这方面,盐和“性爱、自主活动及加工食品中脂肪、碳水化合物和巧克力让人上瘾的特性”有着相似之处。
      • 饮食不是为了寻求快乐,而是为了避免痛苦。
    • 第十三章 消费者渴求同样绝妙的咸味:

      • 摄取过量的盐将导致高血压,而高血压是引发心脏病的危险因素。所以,减少钠的摄入可以降低患高血压和心脏病的风险。
    • 第十四章 我对公众深感抱歉:

      • “只有令人感觉良好的食品,人们才会愿意多买。虽然厂商为产品做广告,但它带来的差别是微不足道的。广告90%的目的是使消费者感觉良好,感觉良好的意思就是好吃”。
      • 人们对某种食物的偏爱会极大地受到他们所摄入的其他食物的影响。比如,你对糖果味道的偏爱,会因为喝可乐而改变。这意味着,甜味的极乐点是不固定的,它可能上升或下降。这取决于你吃的其他食物。
      • 其中一个关键特性是像巧克力一样入口即化的神奇的能力。“这就是所谓的消失的热量,”韦斯利说,“如果食物能快速融化,那么大脑就认为它没有热量,就像爆米花,你可以不停地吃下去。
      • 最近的研究表明,血糖升高会使人们的食欲大增。只要吃下能够引起血糖升高的食物,4个小时后,人就会食欲大增。吃完薯片一小时后,会想吃更多的薯片。
    • 后记 我们为廉价食品而着迷:

      • 在参观结束时,我马上意识到在我们有生之年,雀巢根本不可能将世界人民从肥胖病或加工食品所导致的其他疾病中拯救出来。人们在超市中购买的食品全都经过雀巢科学家们的完美设计,目的在于让人们过度消费产品。尽管这些科学家们掌握了先进的技术和食品科学的所有知识,但他们仍旧不可能拿出一个可行的解决方案。
      • 如果不打一场硬仗,这些公司是绝不会对这3种成分轻言放弃的。盐、糖和脂肪都是加工食品的基础,这些企业在确定产品配方时考虑的最重要的一个问题就是:多少含量的盐、糖和脂肪,才能让食品的诱惑力最大化。
      • 我们迷上了价格低廉的食品,这与我们热衷于使用廉价能源是一个道理,”前皮尔斯伯里总裁詹姆斯·贝克说道,“真正的问题在于价格的敏感性,而且十分不幸的是,富人与穷人之间的收入差距也在日益扩大。食用新鲜、健康的食物开销太大。因此,肥胖病的问题中隐藏了巨大的经济问题。它在那些资源少和知识匮乏的人身上表现得最为明显。
      • 他们试图改变自己的饮食习惯,结果又被商标上标着“健康”标签的产品蒙蔽,从而继续购买垃圾食品。这是食品公司一直采用的战术,极大地标榜食品中所含的某种健康成分,期待消费者会因此忽略其他成分。这是书中最古老的技巧,甚至可以追溯到20世纪20年代和30年代。
    ]]>
    2022-04-02T01:49:43+00:00
    https://github.com/superleeyom/blog/issues/48我为什么不能放弃跑步2024-04-24T05:23:56.529689+00:00

    "明明这么痛苦,这么难过,为什么就是不能放弃跑步?因为全身细胞都在蠢蠢欲动,想要感受强风迎面吹拂的滋味。"

    我非常喜欢《强风吹拂》里面的这句话,为此我将它写在了我的 running page 上,以此来激励自己一直坚持跑下去,当我问我自己,你为什么不能放弃跑步?我思索了一会儿,我将自己这几年跑步心路历程归纳为这几个关键字:痛苦、恐惧、和解,下面我来说说这几个关键字的含义。

    痛苦

    没错我以前是一个「胖子」,我在 2017 年的时候,体重一度达到了巅峰 93 kg,几乎任何一个人看见我,他们的第一句话都是「你怎么这么胖了?」,但是那个时候我的心态倒是蛮好的,整天乐呵呵的,该吃吃该喝喝的生活还是照样继续,并没有把朋友们的话放在心上。但是,你之前有多放肆,后面就有多痛苦,之后大体重带来的副作用也渐渐开始出现了:

    • 体能严重下降,爬楼梯喘不过气
    • 身材严重走形,真胖成个球
    • 衣服不好买,几乎只能穿 XXXL 型号的衣服
    • 心理上对自己越来越不自信
    • ....

    当我脱光衣服,面对着镜子里的自己,那简直是不忍直视,仿佛镜子里的自己对我说:「兄弟,我不想再胖下去了」,确实啊,不能再这样下去了!咔嚓...随着相机快门声的响起,我拍下了这张充满油腻的照片(PS: 这张照片,到现在我一直留在我的相册里,以此来警示现在的我),然后正式开始了减肥之旅,也正式开启了我的跑步之路。

    刚开始跑步的时候是痛苦的,首先是身体上的,由于身体的负重,跑个 3 公里,6~7 分配速,面红耳赤,喘不上气,迈不开步子,非常的痛苦,其次是心理上的,因为跑步是孤独的,所以你没有任何的鼓励,也没有任何的支持,你只能自己去控制自己的情绪,所以你的心理状态也是很绝望的。

    前面 1-3 个月非常痛苦,大部分的人都熬不过这段时间,因为我们会在这段时间给自己找各种借口理由,所以通常熬过初始阶段的 3 个月,基本上我们的身体会开始慢慢的适应这种运动的节奏,运动习惯一但养成,这将会是一个非常好的开端。

    中间也受过几次比较严重的伤,其中比较严重的一次,膝盖痛的走路都成问题,为此被迫休息一两个月,虽然后面陆陆续续的有些小的伤病,但是问题都不大,休息个一两天一般都自己恢复了。

    恐惧

    随着持续的锻炼,慢慢的也带来了正向的反馈,体重开始下降,精神状态发生了翻天覆地的变化,开始对跑步上瘾,开始享受多巴胺带来的快乐。但是另外一方面,我居然出现了一个「恐惧」心理,我特别害怕自己又胖回去了,以至于我疯狂的加大运动量,有段时间几乎每天都跑,压根不给身体恢复的时间。

    其实之所以会产生害怕的心理,归根结底还是对自己身材的一种焦虑,这种焦虑是害怕反弹。但是后面想明白了,大多数时候我们没必要对这种心理有太大负担,科学家提出了成功的减肥定义:「人刻意的减重 10% 的体重以上,并且至少保持一年以上」,如果能达到这个标准,加上合理的运动和饮食,也能继续保持现有的身材不反弹。

    和解

    其实我发现大部分喜欢跑步的人,或者说喜欢运动的人,他们身上大多有一个特点,就是不怎么「卷」,当我们处在一个焦虑倍增的大都市,大家都被迫面临各种压力,而跑步对我最大的影响是慢慢的拥有了一种可以对抗焦虑的力量,当我觉得自己非常的焦虑的时候,我会选择去跑个 20 公里的 LSD(Long Slow Distance,直译为长距离慢跑),20 公里都能跑完,还有什么事情解决不了,每个人都需要有自己的方式来对抗现实的压力。

    最近两年,我的心态发生了些许的变化,我开始对自己和解了,每天早上 5 公里的晨跑不再是机械式的任务,而是成为了生活的一部分,有时候没状态、想睡个懒觉,那就不跑了,现在的跑步,不再以减肥为目的,而是为了让自己保持一个比较好的精神状态。在 80% 的时间严格限制自己的情况下,剩下的 20% 的时间,我们可以做自己任何想做的事情,并且不需要有任何的心里负担。

    最后

    跑步其实是一件非常有意思的事情,由其是当你在城市里面跑步的时候,你会看到各种形形色色的人,有行色匆匆从地铁口进出的上班族,有背着书包穿着校服嬉戏打闹的学生,有坐在大排档喝酒撸串吹牛皮的中年大叔,有略显疲惫坐在公交车上刷手机的年轻人,每个角落都在上演着各种悲欢离合,自己也莫名的会被带入别人的生活里,不由自主的去揣测他们背后的人生故事。

    ]]>
    2022-03-27T08:34:13+00:00
    https://github.com/superleeyom/blog/issues/44[笔记]精力管理2024-04-24T05:23:56.616180+00:00
  • 精力管理 作者:吉姆·洛尔 托尼·施瓦茨  时间:2022-01-13 17:33:43 数量:14 个笔记

  • 第三章 高效表现有节奏——劳逸结合的平衡:

    • 恢复周期对创造力和亲密关系都同等重要。声音通过音符间的停顿空白组成了音乐,如同字母和空白组成了单词。就在工作的空白时段,爱、友谊、深度和维度得以延伸。如果没有恢复的时间,我们的生活将在失衡中变得一片混乱。
    • 我们越是忙碌,越会高看自己,认为自己对他人来说不可或缺。我们无法陪伴亲人朋友,不知疲倦,没日没夜,只管四处救火,不给自己留下喘息的时间。这就是现代社会的成功典型。
    • 我们必须在四个层面都保持健康波动的节奏,即「效能金字塔」的组成部分:体能,情感,思维和意志。
  • 第四章 体能精力——为身体添柴加火:

    • 从生理学的角度看,精力来源于氧气和血糖的化学反应。从实际生活来看,精力储备取决于我们的呼吸模式、进食的内容和时间、睡眠的长短和质量、白天间歇恢复的程度以及身体的健康程度。建立起体能消耗和恢复的节奏性平衡,能够确保精力储备保持在相对稳定的水平。
    • 肌肉若缺水 3%就会失去 10%的力量和 8%的速度。喝水不足也会损害大脑的注意力和协调能力。
    • 早餐是一天中最重要的一餐,我们之前说过,高质量的早餐不仅能够提高血糖水平,还可以强力推动新陈代谢。
    • 如果你摄入的 80%的食物都是健康而高效的,剩下的 20%可以是你喜欢的任何食物,只要份量控制得当。
  • 第五章 情感精力——把威胁转化为挑战:

    • 首先真诚地对该员工的良好表现给出正面评价,然后以讨论而非宣讲的形式提出批评意见——因为自己的看法或许并非完全准确,最后以鼓励结尾。
  • 第七章 意志精力——活出人生的意义:

    • 生命的终极意义是担起责任,找寻难题的答案,并且完成生命为每个人设定的任务。
  • 第八章 明确目标——知道什么最重要才能全情投入:

    • 人们可以被物质奖励或外部激励所驱使;但是,只有在自由选择并享受事物本身的情况下人们才会表现出更多热情,从中获得更多乐趣。
  • 第九章 正视现实——你的精力管理做得如何:

    • 「所有形式的上瘾都是有害的,」精神分析专家卡尔·荣格写道,「不论这种致幻剂是酒精、吗啡还是理想主义。」
    • 我们忽视或否认的一切,终将在我们的行为中体现出来。如果我们在成长过程中认为表达愤怒是不可取的,会损害个人形象,它就会以伪装后的其他形式出现,比如批判或挑剔,固执或心怀怨恨。
    • 邪恶的本质缺陷并非是罪恶本身,而是自我否认。
  • 第十章 付诸行动——积极仪式习惯的力量:

    • 人类行为只有 5%是受自我意识支配的。我们是习惯的造物,因而我们的行为有 95%都是自动反应或对于某种需求或紧急情况的应激反应。
  • ]]>
    2022-01-17T01:19:39+00:00
    https://github.com/superleeyom/blog/issues/432022年个人周报2024-04-24T05:23:56.697518+00:00
  • 2022年第52周

    • 2022年的最后一天,陪女朋友一起跨年,给她买了一个帽子,买了一堆好吃的,一起爬到楼顶,冒着小雨,放了好多烟花,拍了很多好看的照片,太开心了
    • 自从阳康后,已经快连续两周没有锻炼了,是时候开始捡起来了
    • 给女朋友做了几个新菜:鲫鱼豆腐汤、黄骨鱼豆腐汤、干煸四季豆
    • 不知不觉坚持写了1年的周报了,时间可过得真快啊!2022年的周报就到此圆满结束啦,2023继续记录生活!新年快乐!
  • 2022年第51周

    • 这周阳了,所有跑步暂停,主要症状就是发热,头晕,额头重,嗓子痛,咳嗽,申请了居家两天,目前已经基本上好的差不多了,抗原检测为阴性了
    • 这周主要是各种炖汤,炖了玉米排骨汤,鸡汤,冰糖雪梨
    • 和女朋友一起看完了《今际之国的闯关者》第二季,感觉没有第一季好看
    • 圣诞节女朋友给我买了一件优衣库的羽绒服,特别暖和
    • 和女朋友一起看了韩国电影《流感》
  • 2022年第50周

    • 本周跑步3次,总计15km
    • 身边的朋友和同事纷纷都阳了,女朋友也出现了感冒发烧的症状,周末在家照顾她,希望她能快点好起来,我目前的症状是嗓子疼,头有点晕,发烧的症状暂时没有,周末跑了两个地方想去测一下,检测点都关门了,所以现在到底阳了没,目前也不知道,希望我们都能早日康复
    • 恭喜阿根廷,恭喜梅老板!
    • 原本帮朋友做的一个小项目,都过去1年多了,原本以为黄了,却收到了朋友的打款,有点小开心!
  • 2022年第49周

    • 本周跑步三次,总计 15km,这几天长沙天气还不错,但是比较的冷
    • 疫情终于放开了,但是似乎大家都又变的异常的谨慎了,可能是身边阳了的人越来越多了吧
    • B站三体动画开播,看了一集,感觉还挺不错的,对原著的还原度还挺高的
    • 周末和女朋友的爸爸妈妈第一次视频电话,小小的紧张了一下
    • 周末女朋友教我玩滑板,这也是我第一次尝试玩了下滑板,还蛮好玩的,没有摔跤
    • 天冷周末给女朋友做了番茄金针菇牛肉,汤汁非常的鲜美,特别好喝
    • 第一次夹娃娃,花了20多个币,终于夹到了一个娃娃
  • 2022年第48周

    • 本周跑步2次,总计10km,都是在公司的健身房跑的,长沙的冬天真的好冷
    • 周末帮女朋友搬家,换了个比较大的房子,但是通勤时间多了点,用时间换空间,搬家第一餐,给她做了卤味鸡爪和玉米炖排骨,去鲜花市场,给她买了两盆满天星,一盆紫色,一盆白色
    • 把《克莱因壶》读完了,真的很难想象,这个还是1989年的一部科幻作品,作者的思维也太超前了,在虚拟与现实之间来回穿梭,推荐推荐,目前正在读《挽救计划》
  • 2022年第47周

    • 本周工作实在太忙了,本周只跑步1次,总计5km
    • 和女朋友周末把《阿凡达》第一部重温了一下,期待12.16号《阿凡达》第二部
    • 第一次自己染头发,和女朋友染得同一个发色,可能是没有漂白的缘故,实际效果并不明显
    • 白纸运动,勇敢的人民,为自己呐喊!突然想起B哥的一首歌里面的歌词:“人民不需要自由,这是最好的年代”,真的是讽刺啊
  • 2022年第46周

    • 本周跑步3次,总计20km,这周好像天气又变暖和了
    • 这周工作略饱和,周六加了半天的班,原本计划周日和女朋友去洋湖天街,因为女朋友周日要出差给取消掉了
    • 最近在读《克莱因壶》,特别好看,强推
    • 最近在看的剧是《万神殿》,剧情跟之前看的一部美剧《上载新生》有点类似,还挺不错的
    • 这周把房子的合同签了,然后剩下的就是等公积金贷款下来了
  • 2022年第45周

    • 这周没咋跑步,一直在忙买房的事情,跑步1次,总计5公里
    • 长沙这周的天气,瞬间从30度减到了15度,明显感觉到天气变冷了,恰好双十一买的714的衣服也到了
    • 周六晚上开盘,选到了自己想要的房子,买房子的事情终于确定了,发了条推记录了下当时的感受,没想到火了
    • 周日中午女朋友做的香辣鸡翅特别好吃,晚上和女朋友为了庆祝终于买房了,去吃了黄兴广场的金福潮汕鲜牛肉火锅,他们的家的肥牛卷非常好吃,吃了好多盘
    • 两个人相处真的需要多沟通,周四晚上因为自己太较真,惹得女朋友大哭了一场
  • 2022年第44周

    • 本周跑步4次,总计24公里,其中周六女朋友陪我去月湖公园跑了9公里(她骑自行车,我跑步)
    • 周末女朋友学会了一个新菜:泡椒牛肉,真的非常的好吃,特别下饭
    • 周末梳理了下最近这两周看的楼盘,发现买房确实也是个累人的活
    • 双十一又剁手了7件衣服,为国家的 gdp 做出了自己的贡献
    • 果不其然,岳阳君山马拉松还是延期了,然后把酒店火车票报名全部退掉了,希望12.4的湘江半马能如期举行(大概率也是取消),感觉今年年底感觉湖南地区无马可跑了
    • iPad Pro 11 寸太重了,给女朋友拿去刷剧了,自己在闲鱼淘了一台成色不错的 iPad mini 6,单手握持非常舒服,这就是我想要的 iPad 的形态,轻便不累手,要是加上高刷就完美了
    • 终于把网友的strava数据同步到了garmin,折腾了一两天才搞定,顺便给running page项目提交了一个pr,完善了下 strava 上传到 garmin 的脚本,目前已被合并
    • 这周把花呗和白条,以及那张用不到的招行分期卡都给注销掉了,最终只保留了一张招行的信用卡,后续的消费都统一走招行的信用卡和储蓄卡,这样子每月的消费支出就有一个统一的入口,方便月初或者月末掌握自己的财务收入状况,简化支出方式,总之就是,理性消费,尽量减少自身的负债。
  • 2022年第43周

    • 本周跑步2次,总计10km,长马取消了,希望岳马如期举办
    • 周末和女朋友一起去吃了金粒门的椰奶冻,特别的好吃
    • 给女朋友买了一个Apple Watch S8,督促她养成运动的习惯
    • 周六去看了四个楼盘,看房也是个体力活,看了一天累的不行
    • 帮一个网友手把手将strava的数据同步到garmin,存在一点小问题,还没有搞定,目前正在排查
    • 关于『代理』的不完全使用指北:一篇关于代理的文章,写的很不错,适合小白阅读
  • 2022 年第 42 周

    • 本周跑步4次,总计31km,状态还不错,继续备战全马
    • 周四是和女朋友的100天纪念日,当天特意早早的下班就过去陪她,带了她喜欢喝的椰子,两个人去搓了顿好的,特别开心的一天
    • 周六和女朋友一起去了动物园,长这么大还是第一次去动物园,近距离看到了狮子、老虎、大象、犀牛、大熊猫、河马等等好多动物,作为拍照工具人,给女朋友拍了很多漂亮的照片 ,晚上去吃了小龙坎火锅,小龙坎的味道整体上偏辣,感觉适合我俩的口味
    • 周天和女朋友一起去看了两个楼盘,其中一个楼盘我们两个人都还蛮满意的,价格的话,目前还在可承受的范围内,下午就去吃了味上家的新菜:臭豆腐烧猪脚,意外的特别好吃,特别喜欢
    • 最近在看奈飞的新剧《毒枭圣徒》,河正宇、黄晸珉、朴海秀,三大影帝,非常好看
    • 把奈飞的记录片《最凶监狱大揭秘》第六季看完了
  • 2022年第 41 周

    • 本周因为受伤休息了一周,伤口的结痂基本上都已经掉了,准备下周开始恢复锻炼了
    • 周末又是陪女朋友吃吃喝喝,女朋友做的:蜂蜜柚子茶,青椒皮蛋炒鸡蛋,炒面,都超级的好吃,我自己做的:玉米炖排骨(翻车了,炖的太久,汤都熬干了),大斌家串串火锅(还不错,第二次吃了),椰子(这个不错,超市里面买的,一个椰子 9.9,里面的椰水特别多,很清甜,纯天然无添加剂,真的爱了)
    • 时间过得真快,和女朋友在一起就 100 天了,这周花了点时间给女朋友准备了一个小礼物,希望她能喜欢
    • 最近一直在看各种楼盘信息,看的眼花缭乱,我的手机号被泄露后,一堆的中介打我的电话,真的被烦死
    • 原本打算周天去的动物园,因为核酸问题和女朋友身体不舒服,所以被迫终止了,只能下次再去了,长这么大,还真没去过一次动物园
    • 朋友圈被二十大刷屏了,因为开会,最近的梯子特别的不稳定,虽然不太关注政治,但是还是快点把会开完吧,代理一断,就跟断网一样,难受
    • 最近把代理软件从 clash for windows 换成了 clash x pro,使用 clash x pro 的 proxy-providers和 rule-providers,完美的解决了自定义规则被覆盖的问题,虽然 clash for windows 的前置 parse 也可以解决,但是 clash x pro 的内存/cpu占用率更低,还是蛮不错的
  • 2022 年第 40 周

    • 国庆七天,1-4 号回老家过节,顺便参加了一个发小的婚礼,期间得知另外一个发小的爷爷去世了,所以一个假期,同时参加了红白喜事,后面5-7 号,原本计划和女朋友去湘西凤凰那边玩几天,结果凤凰那边出现了疫情,最后在发车前的 20min,为了保住绿码,我们最终决定取消所有的高铁票,然后长沙市市内 3 天游
    • 国庆假期最后一天,骑电动车摔了一跤,把额头擦伤了,箱子也碎了,贼倒霉的一天
    • 国庆七天假期,本周暂无运动,完完整整的休息了一周
  • 2022年第 39 周

    • 本周跑步4 次,总计 19km,虽然长沙马拉松没报上,但是报了2022岳阳君山马拉松,希望能够如期举办,人生的第一个全程马拉松,非常的期待,同时接下来的一个月要开始认真的训练了!
    • 好久没用更新博客了,水了一篇文章:解决 IDEA 因为 Clash 代理问题引起的疑难杂症
    • Netflix 的纪录片《最凶监狱大揭秘》第六季上线了,很好看,质量很高,推荐
  • 2022 年第 38 周

    • 本周跑步 5 次,总距离 27km,最近有这么个需求,然后尝试了下,把 Apple Watch 自带的体能训练「跑步」数据导入 Strava,然后再把 Strava 的数据,借助 Running Page 上传到 garmin connect(国际区),以 garmin connect 作为载体,再将数据同步到 Nike Run Club(简称 NRC),目前发现数据确实正常导入到了 garmin connect,但是 NRC 同步的数据却没有距离、时间、心率,这个就非常的奇葩,不晓得问题出在了哪里,目前还在研究中,相关的讨论:issue
    • 一些适合 SwiftUI 初学者的教程 - by 肘子的Swift记事本
    • SwiftCourse:A repository to store PPTs for my students learning Swift. -by ᏞᎪᏦᏒ.enp92s0
    • 周末看了一部惊悚片《下坠》,还蛮不错的,小成本高质量,推荐
    • 播客:vol.409 科技乱炖:争先“捅破天”图个啥?
    • 周五晚上吃的烤肉应该是没有熟还是肉质有问题,周六早上我和女朋友两个人肚子都不舒服,我一大早上了四五次厕所,肚子都拉空了都
    • 周末和女朋友一起去爬了岳麓山,坐的观光车上去的,然后从南门口走路下来,下山途中吹着徐徐的秋风,好惬意啊,在南门口吃了烤串、糖油粑粑,味道还可以,价格不贵
  • 2022年第 37 周

    • 本周没咋锻炼,只跑步两次,总计10km
    • 周六女朋友下厨,做了一个红烧基围虾,真的特别好吃
    • 听了枫言枫语最新的一期节目:Vol. 76 苹果2022秋季发布会: 接着Ultra!
    • 本周 Adobe 收购了Figma,看看李奇对这件事的看法:屠龙者和恶龙成一家人了?!
    • Apple 终于发布了iOS 16 、watchOS 9 正式版,iPadOS、和 macOS 延迟到十月份发布,苹果还是太鸡贼,没有把所有功能都开放给老款 watch,部分高阶数据比如功率,垂直振幅等,在 watchOS 9 中,老watch 好像都没有
    • 下周马上就要过生日了,女朋友居然送了一把我种草很久的宁芝静电容键盘,真的是太喜欢了,那手感简直无敌了!
    • 和女朋友一起终于把双人成行的第一章给过关了,不容易
  • 2022年第36周

    • 本周跑步 4 次,总计 20km,自从开始从室内改室外跑步后,有氧适能瞬间提升了不少
    • 最近又开通了 Netflix,在追一部悬疑剧《莉亚的7重人生》,感觉还不错
    • 中秋假期回老家做了两天的农活,给家里帮忙收稻子,感觉比跑步累多了
    • 和女朋友一起去看了草莓音乐节,听了陈粒、五条人、万能青年旅店的现场,现场氛围很嗨
    • 【随机波动097】Make Love, Not War:“爱作为一种终极的互相承认,会终结战争,终结一切权力关系,导向平等与和平。”
  • 2022年第 35 周

    • 本周跑步4次,总距离25.8km,最近都是夜跑,天气非常的凉快了
    • 周末给女朋友做了四个菜,红烧鸡爪,紫苏黄鸭叫,手撕包菜,香葱煎蛋,非常的好吃
    • 周天和女朋友去了湖南省博物馆,第一次去,里面是真的好大,两层逛了两三个小时才看完了,去看了大名鼎鼎的马王堆历史文物
    • 电影《当男人恋爱时》,由黄政民、韩惠珍主演,只看了一半,下次接着看完
    • 赵雷的新专辑《署前街少年》
    • 期待下Apple 9.7 号的秋季新品发布会,一年一度的科技春晚,届时 iOS16 正式版也将要发布了
    • 2022长沙草莓音乐节的阵容调整了,增加了五条人,有点开心
  • 2022年第34周

    • 本周锻炼4次,跑步3次,总计20km,骑行1次,最近天气变凉快了,可以去室外跑步了
    • 网络通信 | HTTP(S)那些事儿,了解下http的那些事儿,文章写的非常不错!推荐
    • 周末陪女朋友过生日,第一次见她的朋友们,玩的很开心
    • 原本周日的长沙草莓音乐节延期了,延期到中秋节了,希望那个时候能凉快点
    • 回长沙一年后,体重蹭蹭往上涨,所以和女朋友定了一个对赌协议,以此来督促自己减肥,加油干!
  • 2022年第33周

    • 本周跑步3次,总计15km,继续健身房室内运动
    • 收听了最新一期枫言枫语的播客《Vol. 74 内核恐慌 × 枫言枫语: 如果不做程序员我们会做什么?》,我觉得我如果不做程序员了,如果不考收入的话,我想做的三个职业是:厨师、图书管理员、护林员
    • 报名了2022年长沙全程马拉松,希望能中签,人生的第一个全程马拉松要来了!
    • 周六因为身体原因去了一趟医院,结果说不上好也说不上坏,非常感动的是女朋友一直默默的陪在我身边,一整天都在医院跑上跑下的,也是她给了我勇气去面对这些结果,如果说星星是银河递给月亮的情书,那她则是世界赠予我的恩赐,一定要好好珍惜这个女孩
  • 2022年第32周

    • 本周3次跑步,总计15km,1次骑行,天气太热,只能室内运动
    • 周末和女朋友玩了Xbox游戏《双人成行》,出乎意料的好玩,画面制作精良,需要双人协作、互助、配合,好的游戏和设计真的值得付费,突然想要有把它通关的冲动
    • 周六去了长沙海底世界,各种奇奇怪怪的海洋生物,玩了一下午,夏天还是适合呆着在这种室内
    • 买了两张2022年长沙草莓音乐节的门票,阵容还算可以,和女朋友第一次去音乐节,有点期待
  • 2022年第31周

    • 本周依然继续室内锻炼,外面太热了,跑步机跑步3次,总计15km,室内单车2次,共计1h
    • 周末陪女朋友一起看了古天乐的《明日战记》,特效还行,就是剧情拉胯,时长太短了,不推荐
    • 周四陪女朋友过七夕节,周末就是吃吃喝喝:我家小院、酒拾烤肉、侯师傅热炒,长沙好吃的太多,体重是真的堪忧
    • 因为买了 Apple One,所以不打算续费 Spotify 了,音乐目前主用 Apple Music + YouTube Music 辅助,基本上能满足要求,Netflix 现在也看的少了,给女朋友刷剧用了,Disney+ 开了 1 年,也没咋看过,后续不打算续费了,Prime Video 白嫖的朋友的,YouTube Premium 倒是经常看,这个附赠了 YouTube Music ,后期考虑续费
    • 将 Nike Rub Club 彻底的切换到 Apple 自带的体能训练,配合 Apple Fitness+ 课程,使用体验非常的好,Apple 自带的体能训练就是在数据统计这块做的不行,比如不能按周、月、年的维度进行统计,若能完善下就非常好了
  • 2022年第30周

    • 本周跑步5次,总计23.83km,室内自行车3次,天气太热,开始在公司的健身房锻炼
    • 周日参加公司周年庆,第一次近距离看到了明星张震岳和吉克隽逸,Live House的现场氛围真的太嗨了!
    • 陪女朋友去看了沈腾和马丽的新电影《独行月球》,还不错,蛮搞笑的
    • 回村三天,二舅治好了我的精神内耗,短短11分钟,我们看完了二舅的一生:一个由苦难和命运支配的普通人,如何被时代打倒在地上,躺在地上缓了三年后,拄着拐又爬起来了。二舅治不好我们的精神内耗,我们只不过是在另一种“活着叙事”中,不断确认自己的位置。
  • 2022年第29周

    • 本周跑步3次,总计15.9km,动感单车2次,总计1h
    • 本周看到的不错的文章和播客:
    • Apple Fitness 里面的课程是真的相当的不错,教练非常的有激情,同时配套的音乐的律动完美适配,看得出每集的课程都是精心制作的,极力推荐!
    • 周六女朋友陪我去配了一副眼镜,之前一直想去配,结果一直拖到了现在
    • 周日和女朋友一起去溜冰,两个人都是第一次滑,但女朋友之前有基础,而我完全零基础,感觉站都站不稳,全程都是女朋友扶着我,最后还是摔了一跤,不过确实还是蛮好玩的,很刺激,哈哈
  • 2022年第28周

    • 本周感冒了,休息了一周,就跑了1次,总计5km
    • 《咒》这部电影应该是我这两年里看到的最恐怖的电影了,电影里面居然还带有心理暗示和视觉残留,把观众代入进去,胆小的朋友勿尝试,很吓人
    • 周董新专辑上线了,最喜欢《倒影》:
      • "你的倒影是我 帶不走的風景"
      • "就像居無定所 的雲還在旅行"
    • 周末陪女朋友去看了电影《人生大事》,很感动,「天上的每一颗星 都是爱过我们的人。」
    • 女朋友说我送给她的项链打结了,这怎么能行呢,然后我一顿捣鼓,居然还真被我给解开了,神奇
    • 历时一年,我完成了人生第一场官司,并拿回了租房押金:「他应该不敢再像之前那样轻视一个人说要走法律途径的严正告知了」,我只能说,博主好样的!文章很有参考意义!
    • 做空:什么是做空,做空有哪些策略,看完这篇文章你就知道了
  • 2022年第27周

  • 2022年第26周

    • 梳理下自己目前在订阅的流媒体服务,我发现我一年花在数字流媒体费用还挺多的,有些服务的使用频率很低,准备后面停掉一些不怎么常用的服务
    • 加入了 Apple One 家庭组,彻底被 Apple 生态给捆绑了,不得不说,Apple Fitness+ 的课程真不错,我原本以为国区 iCloud 无法共享美区 Apple One 的容量,结果鬼使神差的一顿操作,发现居然可以,真的很值
    • 上周跑步3次,总计15km,这周又是放肆吃喝的一周
    • 周末室友过生日,小刘推荐的「相信光」主题蛋糕买的实在太对了,效果拉满了
    • 第一次玩密室逃脱,结果没体会恐怖的气氛,反而给整成搞笑大会了,太有意思了,笑死我了
    • 小刘说她的电脑很多弹窗,很卡,想让我给她重装下系统,我打开她的电脑一看,我的天啊,各种弹窗,各种xx管家,一堆杀毒软件,鼠标都点不动,windows 上软件生态真的太糟糕了,分分钟给你捆绑安装各种流氓软件,突然觉得,对于不怎么会用电脑的女生来说,反而Mac才是最好的选择,最起码可以远离那些流氓软件,最后给她重装完系统后,又回到了最初的流畅
  • 2022年第25周

    • 本周跑步5次,总计25.7km,最近天气炎热,跑步需要及时补充水分
    • 《谐星聊天会》第三季开播了,感觉还不错,跑步听的第一期:博世01. 小狗打滚的时候都想什么呢?,一如既往的搞笑和解压,推荐
    • 《神的九十亿个名字》读书笔记整理:[笔记]神的九十亿个名字,科幻三巨头之一的阿瑟.克拉克的作品,值得阅读
    • 最近都没怎么看剧,奈飞又上线了《纸房子:韩国篇》,周末看了一两集,还不错,爽片
    • 最近开始读《金融的本质》,这本书我记得是在看代码家的博客时候关注到的,可以了解到很多金融相关的理论基础
    • 最近mac上的邮件.app又疯狂的占用cpu资源,基本上可以确认就是网易邮箱的原因,把网易邮箱的账号停用后,问题消失,只要一起用,邮件.app的cpu占有率就上来了,我怀疑是不是网易邮箱为了推广自家的邮箱客户端,故意这么搞的,类似的问题可以参考帖子:Mac 邮件占用大量CPU功耗影响很大哦
    • 周末晚上停电了,30多度的天气,一晚上没有睡着
    • 在使用路由器进行全局的科学代理后,我感觉整体感受非常的舒服,圈x打开的次数明显减少了
    • 之前戴AirPods Pro跑步的时候,能明显的感觉到有爆音的问题,我之前一直知道苹果有这个关于AirPods Pro的召回计划,但是一直没有时间去,就搁置了,直到6.12号预约了 Apple 长沙的直营店,从返厂维修检测,到6.23号拿到手,一共花了11天的时间,两只耳机都免费更换了,没想到用了2年,依旧还能免费换新,苹果的售后还是挺不错的
    • 周六晚上和小刘一起去吃了留香铁板烤肉,烤肉很好吃,就是天气比较热,辛苦小刘同学了,哈哈
  • 2022年第24周

    • 本周跑步4次,总计17.7km,其中有2天是跑的跑步机,跑步机和手机的数据差距还是蛮大的,而且跑步机跑起来特别的放不开,还是在户外跑要舒服一些
    • 周末和小刘去打了乒乓球,由于是新拍子,居然还给手指磨出泡了,不过还是蛮好玩的
    • 买了个新的玩具:Redmi 路由器 AX6S,解锁 SSH 安装 ShellClash,只要设备连接到WiFi,就可以实现科学上网,参考教程:红米 AX6S 性能远超想象,解锁 SSH 安装 ShellClash|刷 openwrt 教程
    • 周末做了两个新菜:香煎黄骨鱼、黄焖鸡,从家里带过来的半边鸡肉,由于是没有切好的,为了切成块,费了老大劲,香煎黄骨鱼放点紫苏叶,味道非常的鲜甜
    • 从家里带过来的鸡蛋,感觉太多了,一下子吃不了这么多,坏了好多个了都
    • 把克拉克的《神的九十亿个名字》看完了,感觉还不错,由很多的短篇科幻小说组成,故事有趣且引人思考,还是蛮推荐读一读的
  • 2022年第23周

    • “李佳琦悖论:一个岁静如果想完全不想触碰到政治禁区,他就必须了解所有的政治禁区。” by 噫~这世界
    • 将 NTC 的版本号回退到 6.29 的版本,这个版本的使用体验是最好的,支持动作分解,课程支持简体中文,最新版的是真的难用,可能跟最近 NTC 和 NRC 要放弃中国大陆市场有关吧
    • NTC 和 NRC 终于官宣退出中国大陆市场了,当代版的数字难民
    • WWDC 2022,Apple 又发布了最新的操作系统,其中最期待的是 iPadOS 16 ,终于可以外接扩展显示器了!
    • 本周跑步4次,距离19.2km,将房东的跑步机用起来了,实际跑步机记录的距离和NRC记录的距离相差了快2公里,anyway,跑了就行
    • 最近唐山打人事件闹得沸沸扬扬,其实我一直觉得,女生在变美变漂亮的同时,也需要花时间去「变强壮」,因为现在这个社会,对女生充满太多恶意了,一定要学会利用身体的框架和杠杆进行对抗,以此来保护好自己
    • 这周六给小刘做了红烧鸡爪和红烧鸡翅,照着教程来的,整体效果还不错,很好吃,没有翻车,然后下午陪小刘去蹦床公园玩了一趟,好久没去过了,没想到还挺好玩的,开心
    • 第一次玩字牌,还不是特别的熟练,感觉规则太多了,套用在编程上就是一系列的排列组合,配合运气,找到最优解
    • 播客:《那些退出大陆的海外服务们》《外包是省成本的银弹,还是麻烦的开始?》
  • 2022年第22周

    • 夏天来了,又到了年中购物节,某宝屯了三件T恤和一双鞋子
    • 端午节回家了一趟,回长沙然后带了90多个鸡蛋,我感觉够我吃一两个月的了
    • 准备开启一个计划:6.1-10.1,初始体重150,目标减重至135斤,最近对自己的状态很不满意,算是给自己定一个阶段性的小目标吧
    • 本周就跑了1次,总计5km,长沙这周的雨水天气实在是太多了,加上回了趟老家,所以这周几乎没有怎么锻炼
  • 2022年第21周

  • 2022年第20周

    • 本周跑步3次,总计15km,这两周跑步都比较少
    • 搬到新的地方去了,就在搬家前的一天,把原来的房子转租出去了,真的是有点幸运
    • 《爱死机》第三季上线,已缓存完毕,准备找个时间把它刷完
    • 好久没唱过歌了,周末和朋友们一起去ktv唱了三个小时,原本我以为没用李志的歌,发现还是有的,就是没有那么全
    • 周末做了三个菜:芹菜炒牛肉、红烧冬瓜、蒜苗炒腊肉
    • 和喜欢的人一起度过了一个快乐的520
    • 看了电影《坏蛋联盟》
  • 2022年第19周

    • 这周又是下了一周的雨,本周只跑了1次,才5km
    • 最近一直想把房子转租出去,看的人很多,实际真正要租的人却很少,可能是觉得太贵了?如果万一租不出去,就会损失一半的押金,不管了,下周准备搬家
    • 这周陪小红同学去吃了螺蛳粉,林科大的螺蛳粉是真的可以,特别好吃!
    • 许久不见的发小来长沙,一起吃了个饭
    • 最近略浮躁,周末把最新的《我们的蓝调》追完了
    • 最近在读《男人来自火星,女人来自金星2》
  • 2022年第18周

    • 五一劳动节第一天陪同学回了一趟母校,时光荏苒,依然是熟悉的环境,去当时生活的北门逛了很久,感觉还是老样子
    • 五一劳动节就是追剧,做饭,看书,睡觉,跑步
    • 五一最后的一天陪她去吃饭,给她买了一束小花,生活有些时候需要点仪式感
    • 周末的小龙虾🦞很成功,我居然忘记拍照片了,买了12斤的虾,20块一斤,老板一条龙服务,帮忙处理虾线和虾头,做了蒜蓉和香辣两种口味,四个人吃的非常满足,吃撑了,尤其是最后的汤用来下面条,真的是绝了
    • 这周跑步5次,总计32km
    • 又刷了一遍《无间道》,经典值得反复观看
  • 2022年第17周

    • 这周又是个多雨的周末,本周只跑步两次,总计12.2km,但是好在月底最后两天完成了4月100km的总量
    • 这两天跟前同事聊了下,了解到前东家裁了很多人,看来今年的大环境确实不是特别乐观
    • 《海螺电台》停更半年,终于又更新了,当时回长沙之前,听了《市井雄心2:我应该选择去哪个城市?》这期节目,感受非常的大
    • 5月份准备搬去新的地方了,第二个季度的第二个月了,从5月份开始,公司终于不需要晚上技术值班了
    • 这周工作上比较忙,需求都排满了
    • 最近在考虑帮我爸购买一只柯基犬
    • 暂时想到这些,最后,祝大家五一劳动节快乐!
  • 2022年第16周

    • 本周跑步 4 次,总计21公里
    • 这周六又和朋友一起去看《笑嘛脱口秀》了,非常的开心
    • Netflix 的新剧《我们的蓝调》真的特别好看,还有《我的解放日志》,最近特别喜欢看这类的生活剧,其实也会想,如果自己过上这样的生活,会是怎么样的呢?老是会把自己带入剧中的角色,韩国编剧真的太会讲故事了
    • English, code, write, speech, read,这个时代复利最高的五件事。 by @LuozhuZhang
    • 手机摔了,但是还好没碎,裸奔的代价😂
    • 最近老是无法控制自己的表达欲,虽然我很明白自己说的并无法改变些什么
    • 最近大家都在聊《四月之声》这个视频的事情,我自己也看了,其实并没有什么特别敏感的东西,只是底层上海人民被封后的真实声音,我真不知道zf在害怕什么,只是想起了李志的这首《春末的南方城市》,希望上海早日解封吧
  • 2022年第15周

    • 上周跑步5次,一共25.7KM,目前最大的感受是,夏天的状态确实要比冬天好太多了
    • 听了最新一期的《随机波动》播客 隔离来信:再没有“你的世界”和“我的世界”的区分,非常的感动,或许终结荒谬和悲伤的唯一方式,就是投入到荒谬和悲伤当中去
    • 周末两天又是连着下雨两天,宅在家里,把之前攒的一些剧全给看了,最近奈飞出的新剧《我们的蓝调》真的好好看,治愈系,强烈推荐
    • 最近无限循环 B哥的歌曲,《山阴路的夏天》、《关于郑州的记忆》、《忽然》、《黑色信封》….保持理智,相信未来
    • logseq 的iOS版本终于上架 App Store了,虽然功能没有桌面版全,但是借助 iCloud,基本上可以做到无缝切换了,但是有个缺陷是:iOS端无法进行复制
  • 2022 年第14周

    • 上周跑步次数4次,距离26km
    • 《进击的巨人最终季》part 2 完结,巨人这命名给我整不会了
    • 又看了奈飞新出的韩剧《明天》,还不错,我发现我越来越喜欢看韩剧了😂
    • 最近在读科幻小说《神的九十亿个名字》,克拉克,科幻三巨头之一,yyds
    • “ 我出神地看着她眼中的星光,又赶紧将目光转向真正的星空。如果说我的人生是一部电影,那前面已经放映过的都是黑白片,今天,在泰山之巅,画面突然变成彩色的了。” by 刘慈欣 《三体前传:球状闪电》
  • 2022年第13周

    • 今天推特居然被代码家关注了,有点开心
    • 「我身边很多朋友的“聪明”,是被大的系统狠锤了之后,迅速知道了自己的定位,从此收拢自己精神能量的给予范围。不再随便当某个庞然大物的精神股东,并懂得越来越具体地为自己和关心的人调整对未来的想象,且为之付出脚踏实地日久天长的实践,逐步实现自己的幸福。」-《我如何在 WFH 中重建生活秩序》 by hayami
    • 好多推友都在让我分享下我关注的 RSS 订阅源,索性写了个项目 my-feed-OPML,一劳永逸,每天自动定时同步我的 Feedly 到 Github 上,原理比较简单,就是利用 Feedly 开放的 API 接口,然后解析 opml 文件,写入 Github,100 多行代码搞定
    • 「最近收到的负面情绪越来越多,可能是股市的不景气,可能是最近的 bad news 太多,也可能是骆驼在疫情里坚持三年后迎来了最后一根稻草,总之从朋友嘴里大多数听到的都是抱怨。但越是在这种时刻,越要乐观。我们活在这个世界上其实就是在做多自己,做多这个世界,不是吗?如果悲观,相当于是做空自己。所以越在这种时刻,越要坚定地乐观,不仅心理上乐观,行动上也要乐观,不“空仓”,更不“做空”。」- 《消费决策成本与财富自由的关系》 by GeekPlux
    • 2021 年当我们聊前端部署时,我们在聊什么
    • 「互联网时代的信息质量,呈现两极化。信息的平均质量变得越来越差,但是你能从网上找到的最有用信息,质量正越来越好。这就好比一个商品极大丰富的市场,对普通消费者是不利的,因为他不知道怎么选择琳琅满目的商品;但对高水平消费者非常有利,因为他能找到最满足自己需要的商品。」by 科技爱好者周刊(第 201 期)
    • 「文中有一句也挺让人动容的:我始终认为,程序员写代码是一种创造活动,这是程序员职业的神圣之处。而且还是出自一位在计算机行业奋斗了三十年的”大龄程序员“。曾几何时我也怀揣这种想法,但经历几年职场的摸爬滚打和大环境不断有人发出认清现实的呼喊,我感觉自己也被侵染的认为抛弃理想谈论现实才是一种成熟,自以为的认清了现实,是不是反而是背道而驰呢?」by iOS摸鱼周报 第四十九期
    • 整理读书笔记[[[笔记]盐糖脂:食品巨头是如何操纵我们的]] #blog
    • 最近开始读阿瑟.克拉克的科幻小说《神的九十亿个名字》
    • 最近开始看动漫《文豪野犬》、韩剧《邪恶与疯狂》
  • 2022年第12周

    • 3·21 非常悲伤的一天,东方航空 MU5735 空难 ,愿逝者安息🕯️,你永远也不知道明天和意外哪个先来,珍惜眼前,祝大家幸福、开心、幸运每一天
    • 写了一篇博客《我为什么不能放弃跑步》
    • 博客支持 RSS 订阅,主页增加了 twitter、telegram 、RSS 链接
    • 采用 Feedly + RSSHub,回归了最原始的阅读方式,开始使用 RSS 订阅的方式去阅读自己关注的博客和 newsletter
    • 本周跑步 4 次,总计 27km
  • 2022年第11周

    • 这周 A 股经历了过山车般的刺激,在大家绝望的以为 A 股将要跌破 3000 点这个大关的时候,国家队出手救市了,大盘指数瞬间被拉起,3月份基金亏的钱总算回来一点了
    • 期待的 macOS Monterey 终于上线了通用控制,一大早把所有的设备都进行了升级,配合 iPadOS 15.4 ,体验非常的好,macOS 和 iPadOS 初步进行了打通
    • 周末刷了 3 集《浴血黑帮》第六季、刷了 1 集 《异星灾变》第二季,这两个剧我觉得都值得一看
    • 这周跑步 4 次,总距离 26.5 km
    • 听了最新的一期的科技乱炖节目《快要立不住的技术中立》,开源组件也被”政治化“,比如 react、nodejs 等等,因为俄乌战争,纷纷选择站队,原本纯粹的技术,也不再保持中立
  • 2022年第10周

    • 又把 Apple Music 续费了1年,主要原因还是 Spotify听李志的歌好多没有歌词,而AM基本上都有歌词
    • 这周把由金憓秀(信号中的女主角)的新剧《少年法庭》刷完了,感觉还不错,应该会有第二季
    • 好久没看电影了,周末刷了一部新电影《亚当计划》,不推荐,不怎么好看
    • 本周跑步3次,距离21.1km
    • 这周公司组织架构调整,又换了新的工位了
    • 这周去吃了一家非常好吃的店子,叫做《老许家鱼市》,在岳麓区,人均消费98/人,吃饭的旁边搭了个唱台,有小姐姐和小哥哥在上面唱歌,总之非常的推荐
  • 2022年第9周

    • 本周跑步3次,距离16km
    • 战争受伤害的最终是平民,fuck the war
    • 最近的状态好差,整个人都浑浑噩噩的,技术处于摆烂状态,书也不看,唉
    • 最近把推给卸载了,淡推一段时间,主要是推上全是关于俄乌战争,大家这段时间戾气太重了,太多负能量,厌烦了
  • 2022年第8周

    • 最近要淡推一段时间了,俄乌战争,丰县拐卖,字节程序员猝死,香港疫情,感觉推特上太多的负面情绪,大家都毫无保留在宣泄自己的情感,悲伤、难过、惋惜、愤怒,自己看着也闹心的……不禁想起B哥的一首歌,这个世界会好吗?
    • 本周跑步2次,总计22.5km,2月份是跑的少,一共才65.8km
  • 2022年第7周

  • 2022年第6周

  • 2022年第4周

    • 流媒体:
      • 谐星聊天会的这期《淦!我今年一定会谈恋爱的,淦!!!》推荐,搞笑又引发思考,里面问到一个问题:懂你的人和爱你的人,你选择哪个?
      • 每次听 B 哥的这首《家乡》2017 跨年版,都让我很震撼,由其是最后的和声的管弦乐响起的时候,鸡皮疙瘩都起来了,如果 B 哥还举办演唱会的话,有生之年一定要去听一场
      • Netflix 新剧《僵尸校园》、《模范出租车》
    • 编程:
      • git reset --soft HEAD^
        • --soft:撤销 commit,不撤销 git add . 操作,不删除改动的代码
        • --mixed :撤销 commit,撤销 git add . 操作,不删除改动的代码
        • --hard:撤销 commit,撤销 git add . 操作,删除改动的代码,危险操作
        • 如果进行了2次 commit,都想撤销,使用:HEAD~2
    • 数码:
      • Apple 终于在 iPadOS 15.4 和 macOS Monterey 12.3 中加入了Universal Control 功能,一直就很期待这个功能,终于要来了!
      • iOS 15.4 支持了戴口罩的面容ID解锁
    • 生活:
      • 这周又连着下了一周的雨,一次也没有跑,节后再说吧
      • 今天准备下班回家过年! [[2022-01-30]]
      • 一觉醒来,长沙又下雪了,天气贼冷,春节前的倒数第二个工作日,打开邮箱收到了公司发的邮件,噢?原来是有年终奖的啊!有点小开心 [[2022-01-29]]
  • 2022年第3周

    • 技术相关
    • 随感
      • 微软以 687 亿美元收购了暴雪,没想到暴雪这几年越来越拉胯了,但是好在没被腾讯收购,现在都 2022 年了,守望先锋 2 还没有出,怀念和大学同学打守望先锋的日子
      • 最近家里出了点事情,心情比较的烦躁和郁闷
      • 最近一周天天下雨,天气湿冷,我算是体会到长沙这糟糕的天气了
    • 流媒体
      • Netflix 纪录片《午夜亚洲》,讲述了日本东京、韩国首尔、印度孟买、泰国曼谷、台湾台北、菲律宾马尼拉 六个城市的午夜生活,拍的挺好的,我觉得应该来中国大陆拍一期
    • 阅读
      • 科幻小说:
        • 特德.姜的科幻短篇小说《巴比伦塔》【读完】
        • 阿西莫夫的科幻短篇小说《最后的问题》【读完】
        • 本土科幻作家何夕的《何夕科幻作品集》【进行中】
    • 跑步
      • 最近天天下雨,这周跑步两次,总计10.3km
      • 最近一段时间,我的Nike Run Club一直收不到朋友的cheer,咨询客服,各种折腾都不行,最后站在程序员的思路一想,是不是NRC的收件箱因为消息堆积,导致推送失败了,然后我尝试着把NRC的收件箱清空了,果然真的好了,能正常的收发朋友的Cheer了,果然是Nike的bug
  • 2022年第2周

    • 前言
      • 每周都要写工作周报,受到@yihong0618哥的博客的启发,今年开始尝试写「个人周报」,以周为单位,记录下每周自己都干了啥,通俗点理解就是「可公开」的流水账,算是对每周的一个回顾吧,我也不知道自己能坚持多久,哪天要是断了,也是正常,毕竟生活充满了太多了未知性,总之尽量坚持吧!
    • 编码人生
      • 修改 Mirror 源码:
      • IDEA 插件推荐:Restful Fast Request是一个基于SpringMVC的帮助你快速生成url和参数 的IDEA 插件,整体还不错,但是还是有缺陷的地方,比如如果URL是常量的话,无法识别的问题,已提相关的issues给开发者
      • 将自用的微信读书笔记导出项目优化了下,支持图书封面获取,过滤空的批注
      • 开始使用 Logseq 记录各种事项,我觉得这才是我想要的笔记软件啊,Notion感觉对我来说过于繁琐了
    • 流媒体
    • 写作阅读
      • 技术周刊分享
      • [[[笔记]精力管理]] 读书笔记整理
      • 正在读《盐糖脂:食品巨头如何操作我们的》
      • 周末读完两本科幻短篇小说:柳文扬《一日囚》、罗伯特·海因莱恩《你们这些还魂尸》
    • 运动健身
      • 本周状态感觉不太好,跑步3次,距离 15.3公里,跑的比较慢
  • ]]>
    2022-01-17T01:13:51+00:00
    https://github.com/superleeyom/blog/issues/42技术周刊分享2024-04-24T05:23:56.928340+00:00
  • 分享一些不错的免费开源的技术周刊(更多见:my-feed-OPML 项目,每天定时同步我 Feedly 上的订阅源):
  • ]]>
    2022-01-13T07:27:29+00:00
    https://github.com/superleeyom/blog/issues/40[译]如何阅读Apple开发文档2024-04-24T05:23:57.022958+00:00周末外面一直下雨,闲着也是没事,尝试花了几个小时,把 Hacking with Swift 的创始人 Paul Hudson 在2019 年写的一篇关于「How to read Apple’s developer documentation」的文章翻译成了中文,虽然距离这篇文章发表已经过去两三年了,但是里面的思想和方法却永远不会过时,希望能帮助大家,以下便是译文:

    作者:Paul Hudson,日期:2019年1月18日

    对于很多人来说,这篇文章看起来可能会比较奇怪,因为我们大多数人已经习惯了Apple的API文档的使用方式,并且也能快速的找到我们自己想要的东西。

    但是有一个有意思的事实是:去年很多朋友希望我写一些关于如何阅读Apple开发文档的文章,比如有:你是如何去查看iOS的API接口的?如何在这些开发文档中找到你想要的东西?如何深入了解这些文档或者接口的底层原理?

    你是不是曾经也希望有人帮助你去理解Apple的开发文档呢?其实并不只你一个人,有很多人的都有相同的苦恼。所以我希望这篇文章能对你有所帮助:我会尽力去解释它的整体结构,它有什么好的地方和不好的地方,以及我是如何使用Apple的开发者文档的。

    更重要的一点是,我将向你展示那些有经验的人是如何去搜寻相关资料,并且这些资料要比Apple的在线文档更有价值。

    “它是什么?” vs “你怎么用它?”

    任何的API文档应该有以下5种特性之一:

    1. 接口代码通常需要展示:方法名称和参数、属性名称和类型等,并带有一小段文字描述它的功能是什么,它是做什么的。
    2. API应该有用例指导的描述
    3. 示例代码应该多多使用这些API,以使得它们更加的有用
    4. 展示如何使用基础的API代码片段
    5. 需要有一套总结常见的问题的方法:比如:如何做 X,如何做 Y,以及如何做 Z,等等。

    通俗点讲,Apple第1点做的很不错,第2和第3点也做的很多,但是第4点做的相当少,第5点几乎就没有。

    所以,如果你正在寻找「如何使用Y去做X」的具体示例,你可以尝试从我的「swift的基础知识」教程学起,这也正是这篇教程的用途。

    理解 Apple 的文档所要解决的问题,将帮助你最大限度地利用它。它不是一个结构化的教程——它也不会向你介绍一系列的概念来帮助你实现一个目标,它只是作为苹果支持的数千个API 的参考指南。

    寻找类

    Apple的在线开发文档在:https://developer.apple.com/documentation/,虽然你有一个Xcode的本地离线版本,但是和我交流过的绝大多数的人都是使用的在线的版本,因为他们可以更容易的找到他们想要的东西。

    Apple绝大多数的文档都有接口描述,这也是你看的最多的。我想用一个实际的例子,所以请从在你的浏览器中打开https://developer.apple.com/documentation/ (那是所有苹果开发者文档的主页)。

    你会看到苹果所有的 API 都被分成了App Frameworks、Graphics 、Games等类别,并且你已经看到了一个重要的东西:所有深蓝色的文本都是可点击的,点击后它将带你进入特定框架的API文档。它使用相同的字体和大小,没有下划线,老实说,深蓝色链接和黑色文本之间没有太大区别,但你仍然需要留意这些链接,其中有很多,你将大量使用它们并进行深入研究。

    现在请从App Frameworks类别中选择UIKit,你会看到它的简要概述(为iOS创建用户界面),一个标记为「重要」的黄色大框,然后是一个类别列表。这些黄色的框框确实值得注意:尽管它们被频繁使用,但它们几乎总能阻止你犯一些基础的错误,从而在以后引发一些奇怪的问题。

    这个页面描述了UIKit的类别列表,这是也是大多数人通常会迷失的地方:他们想要学习一些类似于UIImage的东西,以至于他们必须查看整个列表,然后在合适的地方找到它。

    在这种情况下,你可能会查看「Resource Management」这个类别,因为它的副标题写着 「管理存储在主可执行文件之外的图像、字符串、storyboards 和 nib 文件」,这听起来好像很有希望。 但是,你会感到失望,你需要向下滚动页面到「Images and PDF」 部分才能找到 UIImage

    这就是为什么我交谈过的大多数人只使用他们最喜欢的搜索引擎,他们从搜索引擎中输入他们关心的类,只要它有一个像「UI」、「SK」或类似的前缀,通常搜索引擎的第一结果就是他们想要的。

    不要误解我的意思,我知道这种方法并不理想。但是当你要搜索一个类,你要么去搜索引擎搜索,要么去 https://developer.apple.com/documentation/ 查询,选择一个框架,选择一个类别,然后再选择一个类,很显然第一种方式会更快。

    重要的是:无论你选择哪种方法,结果都是一样的,最终都会出现在同一个地方,所以选择最适合你自己的搜索方式就行。现在,请你找到并选择UIImage

    阅读类的接口

    一旦你选择了你关心的类,页面就会有四个主要组成部分:概述、版本摘要、接口和关系。

    概述是我前面提到的「描述一个API应该做什么以及用例指导」,我要求你选择 UIImage,因为它是文本描述的比较好的一个例子。

    当这是我第一次使用的类时,尤其是最近才引入的类,我通常会阅读它的概述。 但是对于其他的类,任何我以前至少使用过一次的类,我会直接跳过它,并尝试找我想要的具体类容。 请记住一点,Apple 文档的设计目的并不是作为一种学习工具:当你有特定的目的时,它的效果才是最好的。

    如果你并不总是为你所选择的Apple 平台的最新版本进行开发,那么页面右侧的「版本摘要」侧边栏就非常重要了。在这种情况下,你会看到 iOS 2.0+, tvOS 9.0+和watchOS 2.0+,这告诉我们 UIImage 这个类何时在这三个操作系统上第一次使用,并且它仍然可用,如果它被弃用(不再可用),你会看到类似 iOS 2.0-9.0 的东西。

    这个页面上的真正内容,以及苹果开发框架中作为特定类主页的所有页面上的内容,都列在「主题」标题下。这将列出这个类支持的所有属性和方法,再细分为使用类别:「获取图像数据」,「获取图像大小和比例」等等。

    如果你选择的类有任何自定义初始化器,它们应该总是首先显示。UIImage有很多自定义初始化器,你会看到它们都被列为签名,只是描述它期望的参数的部分。所以,你会看到这样的:

    init?(named: String)
    init(imageLiteralResourceName: String)
    

    Tip:如果你看到的是Objective-C代码,确保你的语言是Swift。你可以在页面的右上角执行此操作,当重要的 iOS 测试版引入新的变化时,你也可以在此处启用 API 更改选项。

    请记住,初始化器被写为 init?而不是init, 是有可能失败的,因为init?返回一个可选的,以便在初始化失败时可以返回nil。

    在初始化器的正下方,你有时会看到一些比较特殊的用于创建类的实例方法。这些不是Swift意义上的初始化方式,但它们确实创建了类的实例。对于UIImage,你会看到这样的东西:

    class func animatedImageNamed(String, duration: TimeInterval) -> UIImage?
    

    class func意味着你可以调用UIImage.animatedImageNamed().

    在初始化器之后,事情变得不那么有组织性了:你会发现属性、方法和枚举全部混合在一起。虽然你可以通过滚动页面找到你要找的东西,但我可以大胆的说,大多数人只是Cmd+F在页面上找到一些文本!

    有以下三点需要注意:

    • 嵌套类型(类、结构和枚举)与属性和方法一起列出,这需要一点时间来适应。 例如,UIImage 包含嵌套的枚举 ReizingMode
    • 任何带有一条线的东西则说明被弃用了。 这意味着 Apple 打算在某个时候将其删除,因此你不应将其用于将来的代码,并且你应该开始重写已被已被弃用的代码。 (实际上,大多数 API 在很长一段时间内都处于“弃用”状态,—年复一年)
    • 一些非常复杂的类,比如UIViewController,会有额外的文档页面和它们的方法和属性混合在一起。你看他们旁边的页面图标,都会加上一个简单的英文标题,比如「定位内容相对于安全区」。

    在页面底部,你会找到对应的关系,它告诉你它继承自哪个类(在本例中,它直接来自 NSObject),以及它遵循的所有协议。 当你查看协议关系更复杂的 Swift 类型时,本节会更有帮助。

    阅读属性和方法页面

    你已经选择了一个框架和类,现在是时候查看一个特定的属性或方法了。 查找并选择此方法:

    class func animatedResizableImageNamed(String, capInsets: UIEdgeInsets, resizingMode: UIImage.ResizingMode, duration: TimeInterval) -> UIImage?
    

    你应该在创建专用图像对象类别中找到它。

    这不是一个复杂的方法,但它确实展示了这些页面的重要部分:

    • Apple 有几种不同的编写方法名称的方式。 前有class func animatedResizableImageNamed , 然后是方法页面标题中显示的表单(animatedResizableImageNamed(_:capInsets:resizingMode:duration:)),以及方法页面的声明部分中的表单。
    • 正如你在版本摘要中看到的(在右侧),此方法是在 iOS 6.0 中引入的。虽然主要的 UIImage 类从第一天就已经存在,但这种方法是在几年后引入的。
    • 方法声明的各个部分,颜色是紫色的都是可点击的。 不过要小心:如果你点击 UIImage.ResizingMode,你会去哪里取决于你点击的是UIImage还是ResizingMode。 (提示:你通常需要单击右侧的那个)
    • 你将看到每个参数的含义和返回值的简要说明。
    • 「Discussion」部分详细介绍了此方法的具体使用说明。 这是几乎是每个页面中最有用的部分,因为在这里你会看到诸如「不要调用此方法」或「当……时要小心」之类的内容。
    • 你可能会发现「See Also」部分,这里的方法列表与我们在前一页中使用的方法相同。

    UIImage是一个旧类,它没有太多改变,所以它的文档状态很好。但是一些较新的api,以及许多不像UIKit那样受欢迎的老API,仍然没有得到足够的文档支持。例如,来自SceneKitSCNAnimation,或来自UIKitUITextDragPreviewRenderer:都是在iOS 11中引入的,并且在发布 18 个月后,它们的文档中仍然包含「没有可用的概述」。

    当你看到「没有可用的概述」时,你的心会沉下去,但不要放弃:让我告诉你我接下来要做什么……

    查看代码

    尽管 Apple 的在线文档非常好,但你经常会遇到「没有可用的概述」,或者你发现没有足够的信息来回答你的问题。

    康威定律指出,「设计系统的架构受制于产生这些设计的组织的沟通结构」,也就是说,如果你以某种方式工作,你也会以类似的方式设计东西。

    Apple 在我们行业的独特地位使他们以一种相当不寻常的方式工作,这几乎可以肯定这与你自己公司的工作方式完全不同。他们有API审查讨论,试图研究在两种语言下API应该是什么样子,他们有专门的团队来制作文档和示例代码。

    但是他们获得示例代码的门槛非常高:通常需要非常好的代码才能获得示例代码,并且要经过多层审查,例如法律问题。虽然我可以在一个小时内输出一个项目,然后直接把它作为一篇文章实时发布,但 Apple 做同样的事情要花更长的时间,他们非常重视自己的形象。如果你曾经好奇为什么Swift 官方博客上很少有文章出现,现在你知道了!

    现在我说这些的原因是Apple 有一个被广泛使用的捷径:他们的工程师在他们的代码中留下注释的门槛似乎很低,这意味着你经常会在Xcode中找到有价值的信息。这些评论就像金粉一样:它们直接来自Apple 的开发者而不是他们的开发者发行团队,尽管我非常喜欢devpub,但很高兴直接从源头那里找到。

    还记得我之前提到 SceneKitSCNAnimation 在 Apple 的开发者网站上没有记录吗? 好吧,让我们看看 Xcode 可以显示什么:按 Shift+Cmd+O 调出 Open Quickly 菜单,确保右侧的 Swift 图标是彩色的而不是空心的,然后输入SCNAnimation

    你将看到列出的一些选项,但你正在寻找在 SCNAnimation.h 中定义的选项。 如果你不确定,最好选择 YourClassName.h 文件。

    无论如何,如果你打开SCNAnimation.h, Xcode会显示一个生成的SCNAnimation头文件的版本。因为原始版本是Objective-C,所以Xcode为Swift做了一个实时翻译,这就是 Open Quickly 框中带颜色的 Swift 标志的含义。

    现在,如果你按下 Command+F 并搜索「class SCAnimation」,你会发现:

    /**
     SCNAnimation represents an animation that targets a specific key path.
     */
    @available(iOS 11.0, *)
    open class SCNAnimation : NSObject, SCNAnimationProtocol, NSCopying, NSSecureCoding {  
        /*!
         Initializers
         */
    
        /**
         Loads and returns an animation loaded from the specified URL.
    
         @param animationUrl The url to load.
         */
        public /*not inherited*/ init(contentsOf animationUrl: URL)
    

    而这仅仅是开始。 是的,该类及其所有内部结构都有文档,包括使用说明、默认值等。 所有这些确实应该在在线文档中,但无论出于何种原因,它仍然没有出现,所以准备好查找代码可以作为一个有用的补充。

    最后的提示

    此时,你应该能够查找你喜欢的任何代码的在线文档,并查找头文件注释以获取额外的使用说明。

    但是,在你准备好面对所有 Apple 开发文档之前,你还需要了解两件事。

    首先,你会经常遇到标记为「已归档」、「旧版」或「已停用」的文档,即使是相对较新的文档。 当它真的很旧时,你会看到这样的消息:「这个文档可能不代表当前开发的最佳实践,下载和其他资源的链接可能不再有效」。

    尽管Apple 是世界上最大的公司之一,但它的工程和开发团队还没有达到人满为患的地步,他们不可能在更新所有内容的同时还涵盖新的 API。所以,你看到「归档」文档或类似文件时,请进行判断:如果它是Swift 的某个版本,至少你知道它是最近的,即使不是,你可能仍然会发现有很多有价值的信息。

    其次,Apple 还有一些特别有价值且非常出色的文档。 这些都列在 https://developer.apple.com 的页脚中,但主要的是人机交互界面指南。 这份文档这将带你了解苹果平台应用设计的各个部分,包括用图片来说明关键点,并提供大量具体建议。 尽管此文档是构建 iOS 应用程序时要考虑的非常重要的一个文档,但令人惊讶的是,似乎很少有开发人员阅读过它!

    接下来?

    我之前写过关于 Apple 文档问题的文章,虽然那里没有鼓励,但至少它可以帮助你在挣扎时感觉不那么孤单。

    幸运的是,我有很多可能更有用的材料:

    你认为阅读 Apple 文档最有效的方法是什么? 在 Twitter 上和我交流你的想法:@twostraws

    ]]>
    2022-01-08T09:18:20+00:00
    https://github.com/superleeyom/blog/issues/39我的2021年跑步报告2024-04-24T05:23:57.123670+00:00距离我上一次写博客已经三个月了,今天是2022年的第一天,原本想写篇博客总结回顾下2021年,但是想想还是算了,2021年过的并不是很顺利,也不太想用过多的文字来纪念它,过去了也就过去了,但是却特别想写写自己这一年来的跑步过程,看着各大平台都在出各种年度报告,我也给自己写个2021年的跑步报告,以此来激励自己2022年能继续跑下去。

    🏃跑步数据

    2021年的年度跑步数据如下所示:

    总距离 总跑步次数 时间 平均配速 平均心率 半马 10km+ 5km+ 晨跑 夜跑
    1026km 172次 97h37min 5‘42 150 2次 24次 136次 121次 38次

    其中每个月的具体数据如下:

    月份 距离 次数 平均配速 时间
    1月 81.8km 16次 6‘11 8h26min
    2月 20.6km 4次 5‘32 1h54min
    3月 102km 16次 5’09 8h45min
    4月 100.6km 15次 5‘15 8h48min
    5月 101.4km 16次 5’32 9h20min
    6月 69.6km 14次 5‘17 6h8min
    7月 90km 17次 5’46 8h41min
    8月 73.6km 16次 5‘28 6h43min
    9月 82km 14次 5‘56 8h6min
    10月 92.8km 11次 5’49 9h
    11月 104.7km 19次 6‘13 10h51min
    12月 106.5km 14次 6’07 10h51min

    我给自己每个月的目标是100km的跑量,其中只有5个月达成了目标,其他的月份都差了一点,2月份的跑量最低,是因为2月份我不小心意外受伤,下巴缝了六七针,被迫休息了一个月,6789四个月因为在准备换城市和换工作,所以整体的跑量相对少一点,虽然整体的数据跟预期的差了一点(1026km/1200km),但是也还算不错吧,好歹也有1026km的跑量,跟2020年对比要好上一点。

    更多跑步数据见我的跑步主页:https://running.leeyom.top

    ⏰跑步时间

    2021年我调整了作息时间,将跑步时间改到了早上,从汇总数据可以看出,晨跑的次数占大头。在深圳的时候,天气比较暖和,通常是早上6点起床,然后洗漱简单收拾一下,6点半出门,晨跑的距离比较短,一般也就5km左右,时间大概控制在30min左右,回到长沙后,由于长沙的天气变化莫测,将起床的时间调到了6点30,其他的保持不变,通常情况下跑一休一,有时候状态好,可能跑二休一,如果天天跑,久而久之就会有厌跑的情绪,要学会适当的休息,给身体恢复的时间。晚上一般是11点30前睡觉,也就是保持7个小时的睡眠时间,加上中午午休的1个小时,基本上能保持一整天精神状态的饱满。周末的话,通常会跑多一点,一般会跑个10km+的距离,总的来说,一周的平均跑步次数在3-4次,这个次数不多也不少。

    人类行为只有5%是受自我意识支配的。我们是习惯的造物,因而我们的行为有95%都是自动反应或对于某种需求或紧急情况的应激反应。

    跑步时间这个问题,我觉得还是得根据个人的情况来,比如你像我一样,晚上没有时间,那就早上跑,如果你早上起不来,那就晚上跑,如果你早晚都没有时间,中午休息时间,也可以动一下嘛,所以这个东西,不在于你有没有时间,而在于你想不想的问题,时间它就像海绵里的水,挤挤总是会有的。

    👟跑步装备

    整体如下:

    • 手表:Apple Watch Series 4
    • 腰包:Keep 跑步腰包
    • 耳机:AirPods 2
    • APP:Nike Run Club
    • 手机:iPhone 13
    • 跑鞋:Nike ZoomX Vaporfly Next% 2

    IMG_6488

    人们可以被物质奖励或外部激励所驱使;但是,只有在自由选择并享受事物本身的情况下人们才会表现出更多热情,从中获得更多乐趣。

    2021年买了一双新的跑鞋Nike Next% 2,虽然价格略贵,但是真的非常的喜欢,现在基本上就穿着这双鞋跑步,飞马37现在还在鞋架上,洗干净其实也还是能跑的。另外换了新的手机,从iPhone X 换到iPhone 13,也算是对自己年底的奖励,因为想多记录点生活,所以我通常跑步会带上手机,跑完后会随便拍几张,发到推上记录下自己跑步的碎片时光。Apple Watch 主要用来记录跑步的心率,AirPods 用来跑步的时候听播客和音乐,Nike Run Club 无广告,跟Apple Watch搭配,用来记录跑步的数据,腰包用的是keep家的,之前用过他们家的一代腰包,这个是改进款,感觉调节的松紧带反而没有一代的好用了。

    🎧跑步听点啥

    跑步确实挺无聊的,所以跑步的时候一般喜欢听播客和音乐来打发时间,2021年我主要在听这些播客和音乐:

    IMG_6424

    🤔跑步的意图

    2021年读了一本书叫做《精力管理》,对我启发很大,书中讲到人的精力由四个部分组成:体能,情感,思维和意志,其中体能是精力的底层基建,建立起体能消耗和恢复的节奏性平衡,能够确保精力储备保持在相对稳定的水平。我现在给自己的定位是,不再以减肥为目的,而是通过跑步,维持一个相对较好的精神状态,让精力维持在相对稳定的水平。跑步除了在精神层面带来的好处,在身体上也有很大的益处,2021年一整年有氧适能在都是高于平均,另外,回长沙后伙食变好了🐶,再个由于自己工种的缘故(长时间坐着),体重较之前略有提升,但是好在通过跑步,还能在可控范围内。

    作家村上春树在《当我谈跑步时,我在谈些什么》说到:“希望一人独处的念头,始终不变地存于心中。所以一天跑一个小时,来确保只属于自己的沉默的时间,对我的精神健康来说,成了具有重要意义的功课。”,确实啊,当代社会太浮躁了,我们总觉得自己很忙,我们越是忙碌,越会高看自己,认为自己对他人来说不可或缺。我们无法陪伴亲人朋友,不知疲倦,没日没夜,只管四处救火,不给自己留下喘息的时间,这就是现代社会的“成功典型”,真的,偶尔可以停下来,跑步1h,灵魂可以喘口气!

    🏁最后

    最后,祝大家2022年元旦快乐!新年快乐!

    ]]>
    2022-01-01T04:22:08+00:00
    https://github.com/superleeyom/blog/issues/38基于Github Issues的博客搭建2024-04-24T05:23:57.208808+00:00最新更新
    • 前端仓库 superleeyom.github.io 增加了一个 remove_running 分支,去掉了 running_page 的链接(我的那个跑步记录的入口,有些人用不到),想要去掉这个链接的朋友用这个分支就行 --2022-02-11

    前言

    应朋友@凯佬的要求,特意写一篇基于 Github Issues 博客的搭建教程,整体的过程非常简单,后端参考了 @yihong0618gitblog 项目,发布 issues,做数据备份,前端参考了@LoeiFyMirror 项目,用于做前端可视化界面,感谢二位大佬的开源精神,所有的服务全部免费(ps:如果你需要自定义域名,自定义域名需要自己付费购买),感谢 Github!

    利用 Github Actions 做数据备份

    首先需要 fork 我的项目 blog,也可以 fork @yihong0618gitblog 项目,都差不多,然后修改 generate_readme.yml文件,这个文件是触发自动备份的 CI/CD 配置文件,修改如下的地方:

    env:
      GITHUB_NAME: superleeyom
      GITHUB_EMAIL: xxx@qq.com
    

    改成你自己的 Github 用户名和邮箱,接着在你自己的这个 blog 仓库下,创建 Environment secrets环境变量 G_T

    这个G_T是 Github 的访问授权 Token,注意保密,不要泄漏,Token 的获取如下图,scope 如果不知道选啥,全部勾上:

    修改 main.py 脚本,修改你自己的定制化的 README.md 的 header:

    MD_HEAD = """**<p align="center">[Leeyom's Blog](https://blog.leeyom.top)</p>**
    **<p align="center">用于记录一些幼稚的想法和脑残的瞬间~</p>**
    ## 联系方式
    - Twitter:[@super_leeyom](https://twitter.com/super_leeyom)
    - Telegram:[@super_leeyom](https://t.me/super_leeyom)
    - Email:[leeyomwang@163.com](mailto:leeyomwang@163.com)
    - Blog:[https://blog.leeyom.top](https://blog.leeyom.top)
    """
    

    issues 列表,提前建好 labels 标签,后面一旦提交了新的 issues,或者你修改了 issuesREADME.md 会根据你给当前 issues 所打的 labels 标签进行分类,比如我建了如下的几个 labels,其中TODOTop,用于生成 TODO List 和置顶,比如你给这个 issues 加上了 Top 标签,那么他会出现在 README.md 置顶分类里面,这两个建议加上,剩下的按你自己的意愿加:

    如果你是 fork 我的项目,建议先把 backup 文件夹下里面的 md 文件删除!因为那是我的 blog 备份文件!

    有了这个项目,我们就可以通过 Github Actions,只要有 issues 发布或者修改,都会触发自动构建,备份issues生产 md 文件,然后刷新 README.md 文件。

    后期你要发布文章,只需要创建一个 issues,然后打好标签,点击发布即可,剩下的都是自动化构建,不需要人为参与。

    利用 Github Pages 做可视化界面

    • 首先你得先创建一个 github用户名.github.io的仓库,必须是公共仓库,比如我的:superleeyom.github.io,然后你把我这个仓库 superleeyom.github.io 的文件全部拷贝到你刚创建的仓库里面,删除 Archive文件夹,这个是我以前备份的 md 文件,对你没啥用!

    • 修改 docs 目录和根目录下的两个 CNAME 文件,里面的内容是你的自定义的域名,比如我的自定义域名是:blog.leeyom.top,如果没有自定义域名,默认填:github用户名.github.io

    • 修改 docs 目录下的 index.html 文件,比如我的:

      window.config = {
        organization: false,// 默认是 false,如果你的项目是属于 GitHub 组织 的,请设置为 true
        order: 'CREATED_AT',// 文章排序,以 创建时间 或者 更新时间,可选值 'UPDATED_AT','CREATED_AT'
        title: "Leeyom's Blog",// 博客标题
        user: 'superleeyom',// GitHub 用户名,必须
        repository: 'blog',// GitHub 项目名,指定文章内容来源 issues,必须
        authors: 'Leeyom',// 博客作者,以 ',' 分割,GitHub 用户名默认包含在内
        ignores: '',// 文章忽略的 issues ID
        hash: 'ghp_VkKID%Qnlgg$SXfIt!UmH&uCLCtHFU$XJHK^YmxvZy5sZWV5b20udG9w',// hash,必须
        perpage: 5,// 分页数量
      }
      

      其中关于 hash 的获取,参考:「获取 hash」

    • 去阿里云或者腾讯云,申请一个新的域名,将域名解析到你自己刚创建的 github 仓库 github用户名.github.io,不需要自定义域名的可忽略这一步:

    • github用户名.github.io 仓库下,将 page 的 source 指向到 docs 目录,指定你的自定义域名即可,若没有自定义域名,Custom domain 可以不用设置:

    • 最后改下你的 README.md,里面的内容是我自己瞎写的,换成你自己的,然后等个 5 分钟,访问你的域名,比如我的是:https://blog.leeyom.top(没有自定义域名默认的是:https://github用户名.github.io),看是否能正常访问,如果不能正常访问,请在当前 issues 下留言,我看到会回复。

    总结

    虽然 Github Issues 的定位就不是为博客而生的,这也注定了它有诸多不足之处,比如无法限制别人发 issue ,但是对于那些不想折腾,内容才是王道的程序员朋友来说,免费、Markdown、代码高亮、标签、评论、图床、备份、Github大厂背书,Github Issues 也不乏是个与自己和他人沟通的好地方。

    参考资料

    ]]>
    2021-09-06T14:44:53+00:00
    https://github.com/superleeyom/blog/issues/37深漂5年随想2024-04-24T05:23:57.328133+00:00有时候跟同事们一起吃饭,大家就会聊起深圳去与留的话题,从大家的言语中看出,大家当初来深圳的大多数是想看看外面的世界怎样,但是在经历了社会的毒打,996的煎熬,房价的绝望后,也感受到了大环境下沉重的压力,大家最后也只会在自嘲的笑声结束这个话题。

    当我自己真实面对这个选择的时候,自己内心有过纠结与彷惶。说实话,我还是挺喜欢深圳的,喜欢深圳的天气,一年9个月可以穿短袖短裤,喜欢深圳的大海,夏天可以去西涌海滩游泳冲浪,喜欢深圳的交通,去哪里都可以坐公交地铁,喜欢深圳的就业环境,工作机会多。

    可为啥想要离开深圳呢?

    我是2016年来到深圳的,今年2021年,刚好第5个年头了,我觉得我想离开深圳的最大的一个原因是自己对房价的绝望吧。目前福田区、宝安区、南山区,稍微像样点的小区,二手房价没有低于6w+的,买一套70平两房,总价420万,首付130万,还不算税,普通人估计掏空家里的6个钱包都不够首付。房价构成了阶级的壁垒,人生的难度都不一样,自然就有人要做出选择,能留在深圳扎根的,是社会的精英阶层,当然离开也并不可耻,它是一种生活方式的选择,在认清现实和理想后,选择一条合适自己的路走也不是不行。

    当我再次回忆深圳的这五年,发现自己的人生轨迹中也留下了一些痕迹:

    • 16年6月成为社会人后找到了第一份正式工作,爬了深圳最高峰梧桐山、七娘山。

    • 17年3月份开始跑步减肥一直坚持到现在,整个人发生了翻天覆地的变化。

    • 17年5月去现场看了老罗锤子科技2017年的春季新品发布会。

    • 17年7月份有幸去了一趟台湾,从台南到台北,感受了下祖国宝岛台湾的风土人情。

    • 18年12月16号,深圳国际马拉松日,完成了人生的第一个半马。

    • 19年10月带上了牙套,开始矫正之旅,到21年的7月,终于矫正完成。

    • 19年11月,跑了深圳龙华区微型公益马拉松比赛,顺利完赛,非常的激动和开心。

    • 20年花了4个半月,经历了科二和科三的两次挂科后终于拿到了驾照。

    • ........

    深圳的孤独是刻在骨子里的,在深圳,很多人耐得住工作的压力,却耐不住夜深人静的孤独。熟识的朋友一个个都离开了深圳,周末放假,独自在几平米的小单间,半夜醒来,手机上还播放着昨晚的音乐,合上手机听着老旧风扇的嗡嗡声却是愈加的孤独和疲惫。

    突然想起了非常喜欢的一首歌曲,4 Non Blondes 的《What's Up》,里面的歌词恰好就映射了我现在的心境:

    I try all the time, in this institution 我一直在尽力着,在这样一个笼牢里 And I pray, oh my god do I pray 我祈祷,我的上帝!我虔诚的祈祷! I pray every single day,For a revolution 我每天都在祈求,为了一个彻彻底底的改变 And so I cry sometimes,When I'm lying in bed 有时候我会躺在床上大哭 Just to get it all out,What's in my head 企图把脑袋清空 And I am,I am feeling a little peculiar 但是我总觉得会怪怪的 And so I wake in the morning,And I step outside 于是我早上醒来,走出门外 And I take a deep breath and I get real high 深深呼吸一口气,感觉很振奋 And I scream at the top of my lungs 我用力大声呼喊 What's going on? 他妈的,搞什么鬼

    深圳就像一座围城;

    里面的人想出去,外面的人拼命的想进来;

    或许几年后,我再回来看这篇文章;

    后悔?庆幸?开心?难过?

    管他呢!

    ]]>
    2021-08-18T02:04:58+00:00
    https://github.com/superleeyom/blog/issues/36对k8s中Service的理解2024-04-24T05:23:57.406223+00:00以下是我基于实际生产实践,对于k8s里关于Service的一些比较粗浅的理解,如果有些不对的地方,欢迎指出,这些理解基于腾讯云的容器服务TKE,之前一直对这个概念比较模糊,结合腾讯云TKE上的文档,后面请教了同事鲲鹏后,总算是有点清晰了,首先先明确Service的基本概念:

    用户在 Kubernetes 中可以部署各种容器,其中一部分是通过 HTTP、HTTPS 协议对外提供七层网络服务,另一部分是通过 TCP、UDP 协议提供四层网络服务。而 Kubernetes 定义的 Service 资源就是用来管理集群中四层网络的服务访问。

    Kubernetes 的 ServiceTypes 允许指定 Service 类型,默认为 ClusterIP 类型。ServiceTypes 的可取值如下:

    ClusterIP

    通过集群的内部 IP 暴露服务。当我们的服务只需要在集群内部被访问时,可以使用该类型。打个比方吧(实际生产不推荐这样做),比如你的一个服务调用另外一个服务,需要明确知道另外一个服务的ip,那这个时候,就可以为该被调用方Pod创建一个service,固定一个ip,此时这个ip只能是在这个集群内部访问,创建一个Service的时候,无论是那种的ServiceTypes都会生成一个虚拟ip,腾讯云TKE上叫服务ip。

    NodePort

    通过每个集群节点上的 IP 和静态端口(NodePort)暴露服务。NodePort 服务会路由到 ClusterIP 服务,该 ClusterIP 服务会自动创建。通过请求 <NodeIP>:<NodePort>,可从集群的外部访问该 NodePort 服务。

    假设现在有一个集群,集群内有四个节点,这个四个节点对外的节点ip分别是:10.21.2.10、10.21.2.11、10.21.2.12、10.21.2.13。假设现在为某个工作负载pod 创建了service,设置主机端口为30003,那这个时候,我们可以通过任意一个节点ip+主机端口号,就可以访问该pod上的服务,比如:10.21.2.10:30003、10.21.2.11:30003 都可以访问。这种的使用场景,比如说某个pod上的服务是属于网关类型的,需要将ip+端口号配置到nginx上进行反向代理,则可以考虑使用这种方式。

    LoadBalancer

    也叫负载均衡器,可以向公网或者内网暴露服务。负载均衡器可以路由到 NodePort 服务,或直接转发到处于 VPC-CNI 网络条件下的容器中。这种类型的使用场景,比如内网需要和公网打通的情况下,即可通过内网ip直接访问到公网后端的pod。创建完成后的服务在集群外可通过负载均衡域名或 IP + 服务端口 访问服务,集群内可通过服务名 + 服务端口 访问服务。

    腾讯云上内网和公网的打通是通过构建子网的方式,对应pod的service创建后,会生成一个ipv4地址,在内网直接ping该ipv4地址,是可以ping通的。LoadBalancer 就感觉有点像是 ClusterIP 和NodePort的超集,用这张图可以理解下:

    ]]>
    2021-07-25T00:52:01+00:00
    https://github.com/superleeyom/blog/issues/35Java空指针避坑指南2024-04-24T05:23:57.483918+00:00把阿里巴巴的《Java开发手册》里,关于 NPE异常总结了下,预防空指针异常,从你我做起!


    【强制】所有的 POJO 类属性必须使用包装数据类型。

    数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险。


    【强制】在使用 java.util.stream.Collectors 类的 toMap()方法转为 Map 集合时,一定要注意当 value 为 null 时会抛 NPE 异常。

    List<Pair<String, double>> pairArrayList = new ArrayList<>(2);
    pairArrayList.add(new Pair<>("version1", 8.3));
    pairArrayList.add(new Pair<>("version2", null));
    // 抛出 NullPointerException 异常
    pairArrayList.stream().collect(Collectors.toMap(Pair::getKey, Pair::getValue));
    
    

    因为 HashMap 的 merge 方法里,若 value 为 null,则会抛 NPE:

    @Override
    public V merge(K key, V value,BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
      if (value == null)
        // 抛出 NullPointerException 异常
        throw new NullPointerException();
      //....
    }
    

    这个问题java9已经修复,所以也可以尝试升级jdk,或者把value为null的过滤掉就行。另外注意:如果key相同也会抛异常 ,改成如下代码相同的key就不会报异常,新key的value会替换旧key的value:

    pairArrayList.stream().collect(Collectors.toMap(Pair::getKey, Pair::getValue,(v1, v2) -> v2))
    

    【强制】使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一 致、长度为 0 的空数组。

    List<String> list = new ArrayList<>(2);
    list.add("guan");
    list.add("bao");
    // array 的值为:["guan","bao",null,null]
    String[] array = list.toArray(new String[4]);
    

    如果循环遍历 array 的时候,不注意,就有可能抛 NPE:

    for (String s : array) {
      // 抛出 NullPointerException 异常
      System.out.println(s.length());
    }
    

    所以建议数组空间大小的 length 设置为0,动态创建与 list size 相同的数组,性能最好。

    String[] array = list.toArray(new String[0]);
    

    【强制】在使用 Collection 接口任何实现类的 addAll()方法时,都要对输入的集合参数进行 NPE 判断。

    List<String> list = new ArrayList<>(2);
    list.add("guan");
    list.add("bao");
    List<String> listSecond = null;
    // 抛出 NullPointerException 异常
    list.addAll(listSecond);
    

    观察 ArrayList 的 addAll 方法的源码:

    public Boolean addAll(Collection<? extends E> c) {
      // 若c为null,这里会抛NPE
      Object[] a = c.toArray();
      int numNew = a.length;
      ensureCapacityInternal(size + numNew);
      // Increments modCount
      System.arraycopy(a, 0, elementData, size, numNew);
      size += numNew;
      return numNew != 0;
    }
    

    所以正确的做法是,对输入参数做判空化处理:

    // CollUtil.emptyIfNull(List<T> set)方法是 hutool里面的对集合null设置为empty的方法,方法内实际为:
    // (null == set) ? Collections.emptyList() : set
    list.addAll(CollUtil.emptyIfNull(listSecond));
    

    【推荐】高度注意 Map 类集合 K/V 能不能存储 null 值的情况。


    【强制】三目运算符(condition? 表达式 1 : 表达式 2) 中,高度注意表达式 1 和 2 在类型对齐时,可能抛出因自动拆箱导致的 NPE 异常。

    Integer a = 1;
    Integer b = 2;
    Integer c = null;
    Boolean flag = false;
    // a*b的结果是int类型,那么c会强制拆箱成int类型,抛出NPE异常
    Integer result = (flag ? a * b : c);
    

    a*b 包含了算术运算,因此会触发自动拆箱过程(会调用 intValue 方法),此时a*b的结果是 int 类型,那么c会强制拆箱成 int 类型,以下两种场景会触发类型对齐的拆箱操作:

    1. 表达式 1 或表达式 2 的值只要有一个是原始类型。

    2. 表达式 1 或表达式 2 的值的类型不一致,会强制拆箱升级成表示范围更大的那个类型。


    【强制】当某一列的值全是 NULL 时,count(col)的返回结果为 0,但 sum(col)的返回结果为 NULL,因此使用 sum()时需注意 NPE 问题。

    可以使用如下方式来避免 sum 的 NPE 问题:SELECT IFNULL(SUM(column), 0) FROM table;


    【推荐】方法的返回值可以为 null,不强制返回空集合,或者空对象等,必须添加注释充分说 明什么情况下会返回 null 值。

    防止 NPE 是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回 null 的情况。


    【强制】当 switch 括号内的变量类型为 String 并且此变量为外部参数时,必须先进行 null 判断。

    public static void method(String param) {
        switch(param) {
            case "sth":
                System.out.println("it's sth");
                break;
            case "null":
                System.out.println("it's null");
                break;
            default:
                System.out.println("default");
        }
    }
    
    public static void main(String[] args) {
        // 会报NPE异常
        method(null);
    }
    

    【强制】字符串判断相等,常量一定要写在前面「这个手册上没有,我自己加的,哈哈」

    String str1 = null;
    String str2 = "hello world";
    // 错误的写法,会抛NPE
    str1.equals(str2);
    // 正确写法
    str2.equals(str1);
    
    

    其他的可能产生 NPE 的场景:

    1. 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE:
    public int f() {
      Integer a = null;
      // 抛出NPE异常
      return a;
    }
    

        另外这种情况也会自动拆箱产生NPE:

    Integer num = null;
    // 抛出NPE异常
    if(num > 1) {
      // do someing
    }
    
    1. 数据库的查询结果可能为 null:
    // 数据库查询用户信息
    User user = userMapper.getById(123);
    // user可能为null,抛出NPE异常
    String userName = user.getUserName();
    
    1. 远程调用返回对象时,一律要求进行空指针判断,防止 NPE:
    ApiResponse<UserInfoDTO> apiResponse = userFeign.getById(123);
    // user可能为null
    User user = apiResponse.getData();
    if(user!=null){
      String userName = user.getUserName();
    }
    
    1. 对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。

    2. 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。

    3. 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null,因为 List 里面也可以存 null。

    可以考虑使用 JDK8 的 Optional 类来防止 NPE 问题。


    ]]>
    2021-07-22T07:10:12+00:00
    https://github.com/superleeyom/blog/issues/34[笔记]半小时漫画经济学4:理财篇2024-04-24T05:23:57.570343+00:00《半小时漫画经济学4:理财篇》读书笔记

    在地铁上看完的,篇幅不长,算是一个科普理财的小册子,半小时到一个小时就可以看完,整书用通俗的话语解释那些投资和理财的专业名词,风趣幽默,挺有意思,摘抄了几句印象比较深的句子,记录一下。

    二、结构性存款:他们说我是保本理财的替身

    • 银行不管赚钱赔钱,只要理财产品一到期,都必须给大家保本金分利息,这种俗称打肿脸充胖子的行为,就叫—刚性兑付

    四、基金定投:听说没钱没时间的人都喜欢我

    • 有些基金就只照着指数配置股票,这就叫被动型基金,也叫指数基金。

    • 股票上涨时急着抛,下跌时却不撒手,这种行为叫作处置效应。

    • 国家政策、利率变动,甚至自然灾害,都有可能带偏经济大环境,这就是系统性风险。

    • 而且基金定投一般选的是指数基金,投资跟着指数走,降低了基金经理决策跑偏的风险。

    五、“宝宝”理财:虽然不懂我,但你或许在用

    • 货币基金是好东西,但如果它发展过速、不受约束,就可能会变成金融之癌,损害实体经济。

    • 要求限制原先的T+0提现。如果想在T+0的货币基金平台上提取超出规定限额的钱,提取时间就要变成T+N。

    • 限制T+0提现,既可防止挤兑风险,又能引导大家把鸡蛋放在不同的篮子里。

    六、年金险:给你稳稳的幸福

    • 投资这件事:事后诸葛亮,事前猪一样。

    七、股票:想和我一起感受乘风破浪的刺激吗

    • 散户也别泄气,机会还是有的。你可以掐准点,去网上摇号。如果运气好,就能中签。
      这就是传说中的打新股,简称“打新”。

    八、债券:欠债还钱,天经地义

    • 这种可以转变成股票的债券,就叫可转债。

    九、期货:征服这货需要一颗强大的心脏

    • 在金融市场上有这样一种投资方式,它原本是保证买卖正常运行,可最后却华丽转身,成为一种投机工具。 它既能让人一夜暴富,也能让人几代贫穷。

    • 期货不是货,而是一张合约,是按标准填写的远期交易合约。

    • 阿蛛觉得涨得差不多了,于是把期货卖了出去,这叫平仓

    ]]>
    2021-07-12T14:03:29+00:00
    https://github.com/superleeyom/blog/issues/33家常菜谱2024-04-24T05:23:57.650482+00:00简单记录下自己做过的一些好吃的菜!等哪天不记得咋做的时候,可以过来翻翻看,回忆下步骤。

    腐竹炒肉

    1. 猪肉切片备用
    2. 切少许姜蒜粒备用,辣椒切片备用
    3. 提前将腐竹泡水备用,泡发后切小条
    4. 锅中热油,下入姜蒜爆香
    5. 下入切好的猪肉,翻炒一会儿
    6. 加入适量的生抽,蚝油,少量胡椒粉,白糖翻炒
    7. 加入切好的辣椒翻炒一会儿
    8. 最后再下入泡发好的腐竹下锅翻炒,如果腐竹下锅比较早的话,容易炒散开,所以放最后面加入
    9. 最后加入适量的食用盐,即可出锅装盘

    宫保鸡丁

    1. 鸡胸肉切丁,加入少许料酒,生抽,蚝油,胡椒粉,淀粉,少量食用油腌制入味
    2. 切少许姜蒜粒备用
    3. 半根胡萝卜,半根黄瓜切丁备用
    4. 起锅烧油,姜蒜下锅爆香
    5. 下入腌制好的鸡胸肉丁翻炒
    6. 鸡胸肉变色后,下入少许豆瓣酱翻炒一会儿
    7. 胡萝卜和黄瓜丁下锅翻炒
    8. 加入花生米翻炒
    9. 最后加入少许白糖翻炒一会儿即可出锅装盘

    香菜炒牛肉

    1. 牛肉切条,加入适量的生抽、酱油、孜然粉、淀粉、少量油腌制一会儿
    2. 切辣椒、姜丝、蒜末备用
    3. 锅中热油,下入姜丝、辣椒、蒜末爆香
    4. 滑入牛肉,快速翻炒变色,加点少量料酒增香
    5. 牛肉变色后,加入香菜、鸡精快速翻炒几秒,即可出锅装盘

    青椒回锅肉

    1. 五花肉放入冷水中,加点料酒,煮沸后,冷却,切片备用
    2. 青椒、蒜苗、蒜头洗净切条备用
    3. 切片五花肉下锅,爆炒出油,肉片卷曲即可加入豆瓣酱、蒜头
    4. 炒至上色后,加入青椒
    5. 青椒炒至段生后,加入少许白糖、蒜苗提味,即可出锅装盘

    土豆黑椒牛肉

    1. 牛肉切条,加入适量的生抽、黑胡椒、淀粉、耗油、料酒腌制一会儿
    2. 切点小红椒、蒜头、姜丝备用
    3. 土豆切丝备用
    4. 锅中热油,放入姜丝、辣椒、蒜末爆香
    5. 滑入牛肉,快速翻炒,锅边淋入少许料酒增香
    6. 牛肉差不多后,加入土豆丝翻炒,土豆丝炒熟后,再撒入少许黑胡椒
    7. 加入少许盐,即可出锅装盘

    香菇板栗炖鸡

    1. 锅中下少许油,下板栗翻炒2分钟,然后盛出备用
    2. 锅中补一些油,下入姜蒜爆香
    3. 然后倒入切好的鸡块翻炒,加入酱油、耗油上色
    4. 加入板栗,香菇,翻炒均匀,然后加入水,盖盖子炖30分钟
    5. 炖的差不多后,加入少许盐、胡椒粉、白糖调味,即可出锅

    金针菇番茄牛肉

    1. 牛肉切薄片,加入少许生抽、料酒、白糖,鸡蛋清腌制
    2. 金针菇洗净,切掉根部备用,准备蒜头和姜片备用
    3. 番茄去皮,然后切碎,备用
    4. 锅中加入少许油,放入蒜头爆香,番茄下锅炒出汁水
    5. 加入少许切好的洋葱,继续翻炒
    6. 锅中加入适量的水
    7. 加入适量生抽,白糖,蚝油,转中小火把番茄和洋葱煮烂
    8. 加入金针菇,加入适量的盐调味
    9. 最后把腌制好的牛肉加入,等牛肉变色1分钟后即可出锅装盘
    ]]>
    2021-07-11T02:43:22+00:00
    https://github.com/superleeyom/blog/issues/32业务数据脱敏解决方案探究2024-04-24T05:23:57.738351+00:00业务数据脱敏解决方案探究

    使用背景

    • 在实际的业务场景中,业务开发团队需要针对公司安全部门需求,针对涉及客户安全数据或者一些商业性敏感数据,如身份证号、手机号、银行卡号、客户号等个人信息,都需要进行数据脱敏。

    • 搭建和生产环境一模一样的预发布环境,需要把生产环境的存量原文数据 加密后存储到预发环境。

    技术调研

    常见的脱敏算法

    目前常见的脱敏算法包括 AES 加密、K 匿名、加星、屏蔽、洗牌、全保留、格式保留、令牌化等,算法及其主要用途介绍如下:

    算法 主要用途
    K 匿名 通过如 K-匿名的算法对原始数据加入扰动和泛化,使得脱敏后的数据无法唯一对应回原数据,用于保留部分统计信息
    加星 最基础的脱敏方法,保留字段一部分信息的同时通过加星遮蔽局部信息
    屏蔽 用特殊字符如星号 * 作为掩码来将字段信息屏蔽掉
    洗牌 将目标字段在样本中洗牌,使得脱敏后的信息无法唯一对应回原始样本,常用于脱敏后作为测试样例或保留部分统计信息
    全保留 字段全保留,不脱敏,常用于字段无需脱敏的场景,如对主键不脱敏
    格式保留 保留字段的原格式的前提下将其中一部分信息进行随机化,达到脱敏的同时仍然可以提供一部分该类型的信息,常用于脱敏后作为测试样例
    令牌化 通过不可逆的算法将目标字段脱敏,但保留一份令牌使得可以在必要时还原
    AES 加密 一种对称加密算法,必要时可通过 AESKey 对脱敏结果进行还原

    Java生态下的脱敏方案

    目前 Java 生态环境下,数据脱敏中间件这块,做的最好的应该数 ShardingSphere,引用 ShardingSphere 官方文档关于数据脱敏模块的说明:

    数据脱敏模块属于ShardingSphere分布式治理这一核心功能下的子功能模块。它通过对用户输入的SQL进行解析,并依据用户提供的脱敏配置对SQL进行改写,从而实现对原文数据进行加密,并将原文数据(可选)及密文数据同时存储到底层数据库。在用户查询数据时,它又从数据库中取出密文数据,并对其解密,最终将解密后的原始数据返回给用户。

    更多关于 ShardingSphere 的数据脱敏原理,可以查阅官方文档

    那如果说,我只是想简单的做一些数据清洗和隐私脱敏,有什么的好的工具类吗?那可以使用 hutool 的 DesensitizedUtil 工具类就行,例如:

    String phone = "13488883888";
    String encryptPhone = DesensitizedUtil.mobilePhone(phone);
    // 打印:134****3888
    System.out.println(encryptPhone);
    

    技术演练

    在 ShardingSphere 的关于数据脱敏的场景分析里,针对已上线的历史数据,需要业务方自己进行清洗。那怎么清洗?简单说下自己的想法:

    1. 首先数据库对那些需要脱敏的列,新增额外的加密列,比如需要对 email进行脱敏,则新建额外的加密列 encrypt_email。

    2. 系统接入 sharding-jdbc,并配置好脱敏规则。

    3. 写个脚本,多线程同时遍历需要脱敏的历史数据,将明文列(email)取出,更新到加密列(encrypt_email),由于sharding-jdbc 会根据脱敏规则,对SQL进行解析、改写,最后加密列存储的其实是加密后的数据。

    这里将使用 ShardingSphere 的 sharding-jdbc 组件简单的演示如何进行数据脱敏。

    数据库结构

    user 表结构,其中 email 为明文列,encrypt_email 为密文列。

    CREATE TABLE `user` (
      `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
      `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '姓名',
      `age` int unsigned NOT NULL COMMENT '年龄',
      `email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '明文邮箱 ',
      `encrypt_email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '加密邮箱',
      PRIMARY KEY (`id`),
      UNIQUE KEY `uk_email` (`email`) USING BTREE COMMENT '邮箱唯一索引'
    ) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
    

    配置解析

    这里只展示 sharding-jdbc 的配置部分:

    spring:
      # sharding-jdbc配置
      shardingsphere:
        # 数据源配置
        datasource:
          name: ds
          ds:
            driver-class-name: com.mysql.cj.jdbc.Driver
            type: com.zaxxer.hikari.HikariDataSource
            jdbc-url: jdbc:mysql://127.0.0.1:3306/shardingsphere?useUnicode=true&useSSL=true&characterEncoding=utf8
            username: root
            password: '123456'
            max-total: 100
        encrypt:
          # 加密器配置
          encryptors:
            # 加密器的名字,这里设置为:email_encryptor
            email_encryptor:
              # 加密方式,内置MD5/AES 
              type: aes
              props:
                # 配置AES加密器的KEY属性
                aes.key.value: 123456abc
          # 脱敏表配置
          tables:
            # 脱敏表名
            user:
              columns:
                # 脱敏逻辑列,真实面向用户编写SQL
                email:
                  # 存储明文列
                  plainColumn: email
                  # 存储加密列
                  cipherColumn: encrypt_email
                  # 使用的加密器
                  encryptor: email_encryptor
        props:
          # 是否使用密文列查询
          query.with.cipher.column: true
          # 是否打印SQL,默认false
          sql.show: true
    

    由于使用 ShardingSphere 的数据源 datasource 后,无需再配置 Spring 自带的 datasource,另外遇到个小问题就是,如果按照 ShardingSphere 官方关于数据脱敏的 springboot配置(官网示例 properties 格式,实际也可以转成yml 格式):

    url: jdbc:mysql://127.0.0.1:3306/shardingsphere?useUnicode=true&useSSL=true&characterEncoding=utf8
    

    会报错 jdbcUrl is required with driverClassName,换成 jdbc-url 则正常启动:

    jdbc-url: jdbc:mysql://127.0.0.1:3306/shardingsphere?useUnicode=true&useSSL=true&characterEncoding=utf8
    

    实验对比

    插入一条数据:

    userService.save(new User("韩武江", 28, "hanwujiang@163.com"));
    

    观察控制台打印的两条log:

    # 逻辑SQL
    Logic SQL: INSERT INTO user  ( name,age,email )  VALUES  ( ?,?,? )
    # 实际SQL
    Actual SQL: ds ::: INSERT INTO user  ( name,age,encrypt_email, email )  VALUES  (?, ?, ?, ?) ::: [韩武江, 28, dad+OyyGMYILeBGwiWHU+tmzk4uJ7CCz4mB1va9Ya1M=, hanwujiang@163.com] 
    

    再观察数据库:

    加密列 encrypt_email 被 sharding-jdbc 加密存储。

    查询数据,并设置 query.with.cipher.column: true,开启密文列查询:

    User user = userService.getByEmail("jianglanhe@gmail.com");
    System.out.println(JSONUtil.toJsonStr(user));
    

    观察控制台打印的log:

    # 逻辑SQL
    Logic SQL: SELECT  id,name,age,email,encrypt_email  FROM user WHERE  email=?
    # 实际SQL
    Actual SQL: ds ::: SELECT  id,name,age,encrypt_email AS email,encrypt_email  FROM user WHERE  encrypt_email=? ::: [dad+OyyGMYILeBGwiWHU+tmzk4uJ7CCz4mB1va9Ya1M=] 
    # 打印结果
    {"encryptEmail":"dad+OyyGMYILeBGwiWHU+tmzk4uJ7CCz4mB1va9Ya1M=","name":"韩武江","id":13,"age":28,"email":"hanwujiang@163.com"} 
    

    若设置query.with.cipher.column: false,关闭密文列查询:

    观察控制台打印的log:

    # 逻辑SQL
    Logic SQL: SELECT  id,name,age,email,encrypt_email  FROM user WHERE  email=?
    # 实际SQL
    Actual SQL: ds ::: SELECT  id,name,age,email AS email,encrypt_email  FROM user WHERE  email=? ::: [hanwujiang@163.com] 
    # 打印结果
    {"encryptEmail":"dad+OyyGMYILeBGwiWHU+tmzk4uJ7CCz4mB1va9Ya1M=","name":"韩武江","id":13,"age":28,"email":"hanwujiang@163.com"} 
    

    我原本以为在开启密文查询的情况,实际SQL都 encrypt_email AS email了,最终返回实体里面的email应该是密文:

    SELECT
      id,
      `name`,
      age,
      encrypt_email AS email,
      encrypt_email 
    FROM
      `user` 
    WHERE
      encrypt_email = 'dad+OyyGMYILeBGwiWHU+tmzk4uJ7CCz4mB1va9Ya1M=';
    

    但是对比可以发现,开启密文列查询配置后,实际sharding-jdbc会在中间把密文解密后返回,最终返回实体里面的email应该是解密后的明文,密文存在的还是在密文列 encrypt_email 中。

    假如后期业务上线一段时间后,需要完全删除明文列,只保留密文列,在不改变代码的基础上是否可行?那直接在数据库层,把email列删除:

    然后修改 sharding-jdbc 配置,去掉明文列,不改写任何其他代码层面的东西:

    # 脱敏表配置
    tables:
      user:
        columns:
          email:
            # plainColumn: email
            cipherColumn: encrypt_email
            encryptor: email_encryptor
    

    然后查询数据:

    // 为了演示,这里改用id查询,如果用email查询的话,肯定为空
    User user = userService.getById(13);
    System.out.println(JSONUtil.toJsonStr(user));
    

    观察控制台,发现其实依然能正常执行,且不报错:

    # 逻辑SQL
    Logic SQL: SELECT id,name,age,email,encrypt_email FROM user WHERE id=? 
    # 实际SQL
    Actual SQL: ds ::: SELECT id,name,age,encrypt_email AS email,encrypt_email FROM user WHERE id=?  ::: [13]
    # 打印结果
    {"encryptEmail":"dad+OyyGMYILeBGwiWHU+tmzk4uJ7CCz4mB1va9Ya1M=","name":"韩武江","id":13,"age":28,"email":"dad+OyyGMYILeBGwiWHU+tmzk4uJ7CCz4mB1va9Ya1M="}
    

    至于为什么会正常运行,因为:

    因为有logicColumn存在,用户的编写SQL都面向这个虚拟列,Encrypt-JDBC就可以把这个逻辑列和底层数据表中的密文列进行映射转换

    假如你删除 email列,但是配置中还配置了 plainColumn: email,那代码执行的时候,则会报错:Cause: java.sql.SQLSyntaxErrorException: Unknown column 'email' in 'field list'

    总结

    我个人的感觉是 sharding-jdbc 确实做到了屏蔽底层对数据的脱敏处理 ,但是要接入 sharding-jdbc 的前提是,团队有制定严格的SQL规范 ,这样可能接入数据库中间件的时候,才会出现比较少的问题,对于一些老系统,动辄几百行的SQL,各种复杂函数,还是放弃接入的好,到时候只会是一步一个坑。

    另外如果想要满足文章开头的第二个需求,也就是把生产库的数据同步到预发布,同时要屏蔽部分敏感数据,大部分的云厂商,都有提供脱敏工具,比如我们自己在用的腾讯云的 DBbrain,就可以支持数据脱敏,但是实际使用还不是怎么完善,有待改进。

    示例代码:shardingsphere-encrypt-jdbc-demo

    ]]>
    2021-07-08T02:10:47+00:00
    https://github.com/superleeyom/blog/issues/31聊聊我整牙的那些事儿2024-04-24T05:23:57.829734+00:00聊聊我整牙的那些事儿

    矫正动机

    马上再过几天,就可以拆牙套了,想记录下自己这快两年整牙的辛酸史,顺便给想矫正牙齿的朋友分享点经验。因为我小时候,门牙就不太整齐,牙齿比较拥挤,并且家里当时条件也不太好,所以就没有去医院矫正,长大后,这也就成了我的一块心病,导致向人微笑的时候,常常抿嘴,不够自信。后面参加工作后,自己有了一定的积蓄后,我便开始准备着手矫正牙齿的事情,我在 19 年的国庆节向父母提了这个事情,他们当时还是挺支持我的,只是表示你要自己能受得了这份罪就去弄,不要后悔就行,就这样,我开始了我的牙齿矫正之路。

    前期准备

    在决定了准备矫正牙齿的时候,我所在的公司有位女同事,恰好也在带牙套,然后向她咨询了很多关于矫正相关的事情,比如医生的选择、矫正价格、矫正年龄一些疑问等等,了解了一些琐碎的细节后,我便开始了医院的选择,我去实际考察了几家医院,有私立的也有公立的,大概对比了下:

    医院类型 价格 医生流动性 对比
    公立医院 金属自锁25000+ 稳定 医生稳定,基本上会跟着走完整个治疗流程,价格透明,但是价格较贵
    私立医院 金属自锁15000+,可能存在乱收费现象 可能离职 医生流动性较高,主治医生可能会离职,价格较便宜,可能会有隐形消费

    最后经过评估,我最终还是选择去公立医院,贵就贵点吧,图个心安,最终选择的是「南方医科大学深圳口腔医院」,主治医生是林宝山医生,林医生人很 nice,没有给人太多的严肃感,就像朋友一样,言谈举止挺幽默的。第一天,首先先开始拍片,照了头部的 CT 扫描,林医生跟我大致跟我聊了下我目前牙齿的情况,问我目前的诉求,以及我的一些疑问,其实回想下,当时我关注的问题有如下几个:

    1. 我这年龄(当时26岁),现在矫正还是否来的及吗?

        牙齿矫正跟年龄其实没有太大的关系,只要牙周健康 ,年龄都没有太大的问题,四五十岁的人都有在矫正。

    1. 我是否需要拔牙?

        像我自己的话,经过医生的判断,最终需要拔掉了四颗智齿,上下左右各两颗,分了两次拔,一次两颗,拔牙那滋味,我依然清楚记得拔完牙齿后,我整个背都被汗水浸湿了,拔牙麻药消了后,真是生不如死,感觉咽口水都痛,第二天起来,脸都是肿的,总之矫正牙齿,就得做好拔牙的心里准备。

        放一张当时拔牙时留下的照片:

    1. 矫正大概需要花多长时间?

        一般来说,牙齿情况越复杂,所需时间越久。大部分矫正周期是 1~3 年,比较严重的骨性的,大概要 3年+,我这种由于只是牙齿不齐,医生大致说需要差不多两年(24 个月)时间,实际其实大概花了 21 个月,比预计的稍微要快点。

    1. 我该选择什么样的牙套?

        我记得回形针出了一期视频「如何科学地矫正牙齿」,里面有讲到牙套的选择,我选择的是金属自锁牙套,没有选择陶瓷和隐适美,主要是考虑价格太贵,且矫正周期太长了,最后经过医生的建议,选择带常见的金属自锁牙套。

    1. 一般多久复诊一次?

        通常一个月需要去医院复诊一次,每次复诊医生都需要调整钢丝的力度,来调整牙齿的位置。所以的话,如果要矫正,一定要选择在工作地或者上学的地方矫正,否则异地复诊是很麻烦的。

    开始矫正

    我是2019年10月20日正式戴上的牙套,刚开始戴上牙套的第一个月,异物感、口腔溃疡、托槽刮嘴、牙齿酸软无力、扎嘴、拔智齿 ,实在是太痛苦了,由于进食不太方便,整整喝了一个月的流食,那一个月瘦了好几斤。

    戴上牙套后,吃东西就是个麻烦事了,吃个面条或者青菜,牙套托槽上挂了一嘴的面条和青菜,像苹果、坚果这类比较硬的东西,只能暂时说拜拜了,再个就得经常刷牙,一日三餐,不是在刷牙,就是在刷牙的路上。

    矫正中期

    大概在适应了几个月后,牙齿基本上就能和牙套友好的和解了,口腔溃疡有了很大的缓解,我也开始习惯了牙套的生活。这个时候,突然疫情就爆发了,大家纷纷都戴上了口罩,所以啊,2020 年真是矫正牙齿的大好时机啊!大家都戴着口罩,人家也看不到你有没有戴牙套啊,就能偷偷摸摸的就完成了矫正,哈哈。

    矫正中期,我遇到主要三个头疼的问题:

    1. 吃东西的时候,如果不注意,咬到硬的东西,托槽掉了,这个时候,又得去医院处理,然后面临医生的拷问,所以那段时间,隔三差五的跑医院,最后吃任何东西我小心翼翼的,生怕咬掉托槽。

    2. 由于医生需要加力调整牙齿的移动,每次复诊完,牙齿都是酸软无力,啥也咬不动,那两三天,就只能吃流食。

    3. 调整咬合的时候,牙齿垫高后,咬合面积的减小,这个时候吃东西又是个麻烦事了,没办法,咀嚼不了,只能生吞。

    矫正后期

    到了矫正后期的话,基本上就是微调,比如调中线,调咬合,这个时候,牙齿基本上都已经排齐,托槽基本上也不会掉了,就感觉,牙套跟口腔已经融为一体了,已经完全适应了它的存在,自己也不太在意别人对自己牙齿的看法,总是露出这一口钢牙对别人傻笑。当然了,矫正后期,大家可能都会遇到一个问题,就是「牙套脸 」,造成牙套脸的原因如下:

    在牙齿矫正期间,饮食方面的改变,通常选择吃一些容易咀嚼的食物。这样会带来咀嚼肌运动减弱,长时间可能会造成咀嚼肌群萎缩。

    如果已经出现了所谓的「牙套脸」也不用太担心,一般咀嚼功能恢复后,面部的状态也会逐渐的恢复起来 ,但是也不一定百分百的恢复,毕竟还要考虑到年龄增长带来的变化。

    一些心得

    矫正牙齿我觉得是我人生中比较重要的一件事情 ,虽然过程确实挺痛苦的,但是能带来一口整齐的牙齿和灿烂的笑容,我觉得挺值的。人类的可塑性和适应能力是很强的,哪怕是根深蒂固的牙齿,依然借助外力也可以矫正过来。所以如果你有矫正的想法,现在就开始行动吧!因为:

    ]]>
    2021-07-05T16:05:13+00:00
    https://github.com/superleeyom/blog/issues/30我的跑步感悟2024-04-24T05:23:57.910849+00:00我的跑步感悟

    前言

    之前有看到 @yihong 发表的《程序员跑步指南》博客,突然也想写写最近这几年自己跑步的一些感悟,一直拖到现在,因为这只是我作为一名业余跑步爱好者的一些经验和感悟,这些建议可能缺乏专业性,毕竟每个人所处的环境,身体状况都不一样,所以仅供参考哈。

    个人概况

    距离 最好成绩
    5km 21m
    10km 45m
    半马 1h50m
    全马 暂时还没跑过

    今年是我今年跑步的第五个年头了,总跑量应该有 5000km+ 了吧,我是从 2017 年正式开始跑步,当时最重的时候应该有 90kg+,变胖带来的烦恼就是,每次去商场买衣服,都只能买xxxl号的,并且任何衣服上身后,那个肚子实在太明显了,但凡有段时间没见到我的人,见到我的第一句话都是:“你咋这么胖了”,所以至此开始,便下定决心开始减肥,开启了我的跑步之旅🏃。

    跑步的好处

    我真切感受到的好处,绝不骗人:

    • 减重 20kg,真是太爽了,现在吃啥都没负担

    • 释放多巴胺,调节情绪,享受内心短暂的平静,尤其是在 996 这种压抑环境下

    • 减掉多余脂肪,让身材变苗条,穿衣更好看,现在可以穿 M/L 的码了

    • 增强免疫力,提高身体素质,普通感冒不吃药也能自愈,脂肪肝也没有了

    • 增强心肺功能,上楼再也不喘了

    跑步时间安排

    其实也就两个选择,晨跑or夜跑,我自己的话,比较喜欢早上跑,所以晨跑居多,偶尔夜跑,一般尽量保持“跑一休一”的状态,也就是一周保持3到4次的跑步,每次大概在5km~10km,偶尔周末会跑10km以上,晨跑的话,一般早上5:30的闹钟,洗漱30分钟,然后6点出门前往跑步的地方。到底是晨跑好还是夜跑好,这个因人而异,选择自己合适的就好。

    防止受伤

    我在开始跑步前期,撒腿就跑,什么热身,拉伸都是不存在的,有点儿急功近利,那带来的自然就是伤痛了。所以不要嫌跑前热身/跑后拉伸的那15分钟浪费时间,这十多分钟,可以让我们远离伤痛。

    我比较推荐 Nike Training 上的 「跑者放松」和「跑者热身」两个课程,跑前和跑后可以跟着做一做。

    如何坚持跑步

    我就讲讲我自己吧,我有两个方法:

    • 『奖励机制』:比如周末有时候当天跑了10km,那今天就奖励自己去吃顿汉堡王,或者跑步坚持一年了,给自己奖励个 Apple Watch 手表等等,这种奖励能让我的跑步有更多的成就感。

    • 『目标制』:我目前给自己定了的是每个月100km的跑量,其实大概算一下,如果按每次跑5km~10km的距离的话,大概一个月有15次的运动机会,也就是一个月有一半时间是锻炼状态,一般时间是休息状态,我感觉这这种大多数人都能接受吧?不用为每天都跑,而造成心理上的压力,有时候完不成也没事,尽量朝着这个目标努力靠近就行。

    另外有这么种情况,有时候经常跑一个地方,跑多了会觉得无聊,至少我是这么觉得的,所以我的办法是,有时候在一个地方跑的无聊了,偶尔换个地方跑,今天操场不想跑,就去河道跑,或者干脆不设置目的地,跑到哪里算哪里,久而久之,方圆 10 公里基本上被我摸透了。

    跑步装备和软件

    跑步装备

    啥钱都可以省,跑鞋的钱咱不能省,毕竟脚是最先个地面接触的,一双好的跑鞋,能很大程度上减少膝盖受伤,同时也能提高跑步成绩。跑鞋的话,目前只穿过 Nike 的飞马系列,其他的品牌没穿过,就不多做啥评价了,不得不说,Nike 家的飞马系列,真的是日常训练的经典跑鞋,跑步小白,不知道买啥跑鞋,可以试试飞马系列,应该不会差多少,如果要跑竞速的话,可以考虑买 Next% 系列,我的下一双鞋就准备打算买 Nike ZoomX Vaporfly Next%,更多 Nike 的跑鞋可以参考网友整理的「Nike 跑鞋矩阵」。

    关于手表,有时候我的朋友问我:“最近想买个手表用来跑步,有啥推荐的吗?”,我的一般回答是:“你先坚持跑个一个月试试,如果能坚持下来,再考虑要不要买个手表。” 因为大部分人买手表都是一时冲动,买了后过几天,便束之高阁。买手表有用吗?肯定有用,监控心率,不用带手机,在线听音乐等等。我目前自己在用的是 Apple Watch 4 蜂窝版,除了续航,几乎没有啥太大的短板,使用 iPhone 的朋友可以考虑购买。

    跑步软件

    我跑步前期用的是「咕咚」和「keep」,但是越到后面,这两个 App 广告越来越多,卖货,短视频,直播,我就想跑个步,搞这么多乱七八糟的东西,之前舍不得换软件,是因为上面有太多的跑步数据舍不得,也是机缘巧合,在 V 站上遇到了 @yihong 在宣传他的开源项目 running_page,在 @yihong 的热心帮助下,终于把自己的跑步数据拿到手了,当时还写了篇文章:《咕咚和keep跑步数据导入Nike Run Club》记录了下,有这个需求的朋友可以试试看。

    后面就彻底换成 Nike Run Club,就凭它免费、无广告,这两点,足以让我用下去了,整体的这大半年使用下来,感觉还挺不错的,完美配合 Apple Watch。

    关于马拉松

    其实感觉如果能 10km 进 1 小时的话,我觉得半马应该没啥太大问题,当然如果有更高的追求,跑全马的话,可能就需要系统的训练,引用推友@gdp8 的话:

    成绩提升是个系统工程: 跑量、核心、减重、控制饮食、避免受伤等等,如果很难做到,自己跑得开心就好。

    我有幸跑过一次马拉松,哇,那现场氛围,太值了,大家一起奋力奔跑的感觉真好!如果可以,请一定要参加一场马拉松试试!不在乎成绩,只在乎过程!

    最后

    所以,你为啥要跑步?我的答案是:

    明明这么痛苦,这么难过,为什么就是不能放弃跑步?因为全身细胞都在蠢蠢欲动,想要感受强风迎面吹拂的滋味。––《强风吹拂》

    ]]>
    2021-06-30T14:59:17+00:00
    https://github.com/superleeyom/blog/issues/29为Docker Alpine添加中文字体2024-04-24T05:23:58.006349+00:00
  • 首先提前将需要安装的字体拷贝到 Jenkins 所在的机器上(/media/front/目录下)

    cd /media/front/
    ls
    simhei.ttf  simsun.ttc
    
  • 修改 Jenkins 的自动化配置,这里只展示核心的部分 shell 脚本片段:

    # 进入项目编译后的target目录
    cd /home/jenkins/data/soft/model/${MODEL_NAME}${BUILD_ID}/${MODEL_NAME}/target
    # 创建临时目录,并拷贝字体到临时目录
    mkdir front
    cp -r /media/front/* front/
    ...
    ...
    # 安装字体
    RUN apk add --update ttf-dejavu fontconfig \
        && rm -rf /var/cache/apk/* \
    WORKDIR /usr/share/fonts/
    COPY front/* /usr/share/fonts/
    WORKDIR /
    

    解析下 shell 脚本,其中安装字体那块,是构建 Docker 镜像的部分 Dockerfile 命令,下面解析这几句命令:

    RUN apk add --update ttf-dejavu fontconfig \
        && rm -rf /var/cache/apk/* \
    

    因为我们使用的基础镜像是FROM retail-harbor.aqara.com/retail/apline-jdk-iptables:v0.0.1,基于Linux 发行版Alpine,所以安装软件的指令是 apk,类似于 CentOS 的 yum,Ubuntu 的 apt-get

    由于安装字体需要安装软件fontconfig,所以需要执行apk add --update ttf-dejavu fontconfig,为了减少镜像的大小,需要删除安装后的缓存,执行rm -rf /var/cache/apk/*fontconfig安装完成后,会自动在/usr/share/下创建两个目录,分别是fontconfigfonts目录,接下来要做的就是把物理机的字体,拷贝到镜像的fronts目录中去。

    为了保持 Dockerfile 文件的可读性,可理解性,以及可维护性,建议将长的或复杂的 RUN 指令用反斜杠 \ 分割成多行,参考文档

    踩了一个坑就是,始终无法将本地的字体文件拷贝到镜像中去,镜像的定制实际上就是定制每一层所添加的配置、文件,每一个 RUN 的行为都会建立一层新的镜像,所以如果我没有指定工作目录 WORKDIR的话,实际拷贝的时候,是找不到 /usr/share/fonts/这个路径。

    使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录,更多有关WORKDIR 命令,参考文档

    WORKDIR /usr/share/fonts/
    COPY front/* /usr/share/fonts/
    # 拷贝完字体后,将工作目录切回根目录,因为接下来是要执行根目录下shell脚本entrypoint.sh
    WORKDIR /
    USER root
    ENTRYPOINT ["sh","entrypoint.sh" ]
    

    这里需要注意,COPY 这类指令中的源文件的路径都是相对路径,比如COPY front/* /usr/share/fonts/,这个 front目录就是相对路径,因为我们此时上下文路径就是项目编译后的target目录

    # 进入项目编译后的target目录
    cd /home/jenkins/data/soft/model/${MODEL_NAME}${BUILD_ID}/${MODEL_NAME}/target
    
  • ]]>
    2021-04-26T03:18:03+00:00
    https://github.com/superleeyom/blog/issues/28[笔记]指数基金投资指南2024-04-24T05:23:58.090575+00:00指数基金投资指南

    作者:银行螺丝钉;数量:33个笔记;时间:2021-04-19 22:17:00

    • 序言一:
      • 而且长期定投,天然就是长期持有的。股票市场的收益并不是均匀的,80%的收益出现在20%的时间里。只有长期持有下来,才能等到下一次牛市的出现。定投自带“长期持有”这个特征,也是有利于在股票市场盈利的。
    • 找到长期收益率最高的资产:
      • 能产生现金流的资产通常比不能产生现金流的资产长期收益率更高;能产生现金流的资产中,现金流越高,长期收益率更高。
    • 看收益,更要看风险:
      • 长期投资股票,不考虑任何策略,只是长期持有,平均可以获得9%~15%的年复合收益率;配合正确的策略,才可以获得20%左右的年复合收益率。
    • 投资者笔记:
      • 能够为我们“生钱”的就是资产,现金不是资产。
      • ·有的人看起来“富有”,但一旦停止工作,高收入也就戛然而止。
      • ·通过定期投资指数基金,业余投资者往往能够战胜大部分专业投资者。
    • 什么是指数:
      • 指数是一个选股规则,它的目的是按照某个规则挑选出一篮子股票,并反映这一篮子股票的平均价格走势。
    • 谁开发的股票指数:
      • 国内有三大指数系列。上海证券交易所(简称上交所)开发的上证系列指数,深圳证券交易所(简称深交所)开发的深证系列指数,以及中证指数有限公司开发的中证系列指数。
    • 指数基金是怎么来的:
      • 而指数基金的业绩跟基金经理的关系不大,主要取决于对应指数的表现。
    • “躺着赚钱”的指数基金:
      • 股神巴菲特也提到过,买指数基金就是买国运。只要相信国家能继续发展,指数基金就能长期上涨,我们就能分享国家经济增长的收益。
    • 最适合普通投资者的股票基金——指数基金:
      • 国内大多数的家庭,目前并没有配置多少股票资产。如果想退休后过上体面的生活,必须要配置一定的股票类资产。如果每个月配合工资来定投低估值的指数基金,实际上就是对现有五险一金的一个很好的补充。相当于自制了一个401(k)计划。
    • 行业指数基金:
      • 值得投资的行业,主要有两个,一个是天生赚钱更容易的行业,另一个是具有明显强周期性的行业。
    • 投资者笔记:
      • 投资的行业上区分,指数基金可以分为宽基和行业指数基金。
      • 常见宽基指数基金有:上证50、沪深300、中证500、创业板、红利、基本面、央视50、恒生、H股、上证50AH优选、纳斯达克100、标普500等。
      • 常见的行业指数基金有:必需消费行业的指数基金、医药行业的指数基金、可选消费行业的指数基金、养老产业的指数基金、银行业的指数基金、证券业的指数基金、保险行业的指数基金、金融行业的指数基金、地产行业的指数基金等。
    • 第4章 如何挑选适合投资的指数基金:
      • 用价值投资的理念挑选出值得投资的指数基金,再用定投的方式去投资它,这是我们投资指数基金的核心
    • 价值投资的理念:
      • 最常用的估值指标有哪些呢?主要是4个:市盈率、盈利收益率、市净率、股息率。
    • 盈利收益率法挑选指数基金:
      • ·当盈利收益率大于10%时,分批投资。
      • ·盈利收益率小于10%,但大于6.4%时,坚定持有已经买入的基金份额。
      • ·当盈利收益率小于6.4%时,分批卖出基金。
      • 目前适合盈利收益率的品种,国内主要是上证红利、中证红利、上证50、基本面50、上证50AH优选、央视50、恒生指数和恒生中国企业指数等。这几个品种的投资很简单,当盈利收益率大于10%时就可以投资,小于6.4%时就可以卖出。
    • 博格公式法挑选指数基金:
      • ·在股息率高的时候买入。
      • ·在市盈率处于历史较低位置时买入。(以上这两点往往是同时发生的。)
      • ·买入之后,耐心等待“均值回归”,即等待市盈率从低到高。
    • 指数基金估值方法小结:
      • 根据指数背后公司的盈利所处的状态,我们可以把指数分为4个类别,分别是:
      • (1)盈利稳定的指数。
      • (2)盈利呈高速增长态势的指数。
      • (3)盈利处于不稳定状态或呈周期性变化,但行业没有长期亏损记录的指数。
      • (4)长期亏损的指数。
    • 投资者笔记:
      • 格雷厄姆对价值投资有三个非常重要的理论,分别是价格与价值的关系、能力圈,以及安全边际。
      • 常见的估值指标有:市盈率、盈利收益率、市净率、股息率。
      • 挑选指数基金有两个策略:盈利收益率法和博格公式法。
      • 不同类型的指数基金需要使用不同的策略进行投资。比如盈利稳定的价值指数可以采用盈利收益率法来投资;成长指数可以使用博格公式法进行投资;周期指数可以使用博格公式的变种来投资;困境指数建议直接放弃。
    • 提高定投收益的5个小技巧:
      • (1)尽量选择费率比较低的场外和场内渠道。
      • (2)不要过于频繁交易,以免产生较高的费用。
      • (3)成立1年以上的指数基金,在分红税上会更有优势一些。
      • 在指数基金的低估区域,分红适合再投入;而当指数基金不低估的时候,分红可以投入到其他低估的品种上。这样就能把指数基金的分红充分利用起来,获得更高的长期收益。
    • 投资者笔记:
      • 定投是普通投资者买卖指数基金最合适的方式。
      • ·定投需要确定两个因素:定投的时间和定投的金额。
      • ·指数基金并不是任何时间都适合定投的,低估时定投才能获取更高的收益。
      • ·可以通过降低投资费用、正确处理基金分红、选择合适的定投频率、定期不定额等技巧,进一步提高定投指数基金的收益。
    • 投资者笔记:
      • 构建一个适合自己的详细定投计划分为四个步骤:梳理自己的现金流、挑选好基金、构建定投计划、定期检查优化。
      • ·制作定投计划表,并将计划表打印出来(尽量大一些),张贴在家中显眼的位置。
      • ·通过参考三个定投实例完善自己的定投计划:为父母构建养老定投计划、为自己构建加薪定投计划、为子女构建教育定投计划。
    • 做好家庭资产配置:
      • 家庭资产的配置,长期看应该以指数基金为主。指数基金在家庭资产中所占的比例,不应该低于“100-家庭成员平均年龄”%。例如家庭成员平均年龄是40岁,那最好配置60%左右的指数基金。剩余的40%,可以配置债券基金和货币基金。
    • 投资者笔记:
      • 家庭资产配置中需要根据资金的不同使用时间来投资不同的投资品种。货币基金适合打理随取随用的资金,债券基金适合打理中短期(1~3年)的资金,而指数基金适合打理长期(3年以上)的资金。
    • 看到上涨,定投下不去手怎么办:
      • 记住,“低估是一个区域,而不是一个点”。
    • 定投的“双核制”:
      • 定投指数基金其实一直都是一个“双核”制:靠工资、租金等收入提供稳定的现金流,靠指数基金来放大收益。
    • 有恒产者有恒心:
      • 有恒产者有恒心,无恒产者无恒心。持有资产,这是我们实现财务自由的必经之路;而拥有“长期持有资产”的信念,是走上财务自由的第一步。
    • 投资者笔记:
      • 认真工作,用双手创造价值,把自己打造成“获取稳定提升的现金流”的资产,这是我们的防御武器;再将现金流定投到低估值的指数基金上,依靠低估值的指数基金来放大收益,这是我们的进攻武器。这个“双核”制定投体系,是最适合大多数人的投资思路。
    ]]>
    2021-04-19T14:37:59+00:00
    https://github.com/superleeyom/blog/issues/27k8s实现Spring Cloud服务平滑升级解决方案2024-04-24T05:23:58.203310+00:00背景

    目前公司的服务是用 Spring Cloud 框架,且服务采用 k8s 进行部署,但是有新的服务需要升级的时候,虽然采用目前采用的滚动更新的方式,但是由于服务注册到 Eureka 上去的时候,会有30秒到1分钟左右不等的真空时间,这段时间会造成线上服务短时间的不能访问,所以在服务升级的时候,让服务能平滑升级达到用户无感的效果这是非常有必要的。

    原因分析

    在 Spring Cloud 的服务中,用户访问的一般都是网关(Gateway 或 Zuul),通过网关进行一次中转再去访问内部的服务,但是通过网关访问内部服务时需要一个过程,一般流程是这样的:服务启动好了后会先将自己注册信息(服务名->ip:端口)注册(上报)到 Eureka 注册中心,以便其他服务能访问到它,然后其他服务会定时访问(轮询 fetch 的默认时间间隔是 30s )注册中心以获取到 Eureka 中最新的服务注册列表。

    那么通过k8s按照滚动更新新的方式来更新服务的话,就可能出现这样的情况:

    在 T 时刻,serverA_1(老服务)已经 down 了,serverA_2(新服务)已经启动好,并已注册到了 eureka 中,但是对于 gateway 中缓存的注册列表中存在的仍是 serverA_1(老服务)的注册信息,那么此时用户去访问 serverA 就会报错的,因为serverA_1 所在的容器都已经 stop 了。

    解决办法

    1. Eureka参数优化

    Client端

    eureka:
      client:
        # 表示eureka client间隔多久去拉取服务注册信息,默认为30秒
        registryFetchIntervalSeconds: 5
    ribbon:
      # ribbon本地服务列表刷新间隔时间,默认为30秒
      ServerListRefreshInterval: 5000
    

    Server端

    eureka:
      server:
        # eureka server清理无效节点的时间间隔,默认60秒
        eviction-interval-timer-in-ms: 5000
        # eureka server刷新readCacheMap(二级缓存)的时间,默认时间30秒
        response-cache-update-interval-ms: 5000
    

    以上两个优化主要是缩短服务上线下线的时候,尽可能快的刷新 eureka client 端和 server 端服务注册列表的缓存。

    2. 网关开启重试机制

    因为我们用的是 zuul 网关,开启重试机制,防止在滚动更新的时候,由于网关层服务注册列表的缓存,将请求打到已下线的节点,zuul 请求失败后,会自动重试一次,重试其他可用节点,不至于直接报错给用户:

    ribbon:
      # 同一实例最大重试次数,不包括首次调用
      MaxAutoRetries: 0
      # 重试其他实例的最大重试次数,不包括首次所选的server
      MaxAutoRetriesNextServer: 1
      # 是否所有操作都进行重试
      OkToRetryOnAllOperations: false
    zuul:
      # 开启Zuul重试功能
      retryable: true
    

    关于 OkToRetryOnAllOperations 属性,默认值是 false,只有在请求是 GET 的时候会重试,如果设置为 true的话,这样设置之后所有的类型的方法(GET、POST、PUT、DELETE等)都会进行重试,server 端需要保证接口的幂等性,例如发生 read timeout 时,若接口不是幂等的,则可能会造成脏数据,这个是需要注意的点!

    3. 需要下线的服务主动从注册中心里移除

    利用k8s的容器回调 PreStop 钩子,在容器被stop终止之前,将需要被 down 掉的服务主动从注册中心进行移除,针对容器,有两种类型的回调处理程序可供实现:

    • Exec - 在容器的 cgroups 和名称空间中执行特定的命令,命令所消耗的资源计入容器的资源消耗。

      lifecycle:
        preStop:
          exec:
            command:
              - bash
              - -c                
              - 'curl -X "POST" "http://127.0.0.1:9401/ticket/actuator/service-registry?status=DOWN" -H "Content-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8";sleep 90'
      

      同时指定一下 k8s 优雅终止宽限期:terminationGracePeriodSeconds: 90,command 配置中添加了一个 sleep 时间,主要是作为服务停止的缓冲时间,解决可能有部分的请求存在未处理完成,就被停止的问题。这里采用的是 Eurek Client 自带的强制下线接口,这里需要注意的是,此方式需要服务引入spring-boot-starter-actuator组件,要求该服务对/actuator/service-registry加入白名单,同时基础镜像得安装 curl 命令才行。

    • HTTP - 对容器上的特定端点执行 HTTP 请求。

      lifecycle:
        preStop:
          httpGet:
      	path: /eureka/stop/client
      	port: 8080
      

      用 http 的方式,则需要我们在每个服务的里面,在代码层面将当前服务主动从注册中心进行移除:

      @RestController
      public class EurekaShutdownController {
      
          @Autowired
          private EurekaClient eurekaClient;
      
          @GetMapping("/eureka/stop/client")
          public ResultDto stopEurekaClient() {
              eurekaClient.shutdown();
              return new ResultDto(Consts.ErrCode.SUCCESS, "服务下线成功!");
          }
      }
      

      需要注意的是,如果该服务需有黑白名单,记得要把/eureka/stop/client加入白名单,如果有的服务有设置 context-path,注意需要加前缀,否则被拦截,就没有什么作用了。

    4. 延迟就绪探针首次探针时间

    在服务的 k8s 的 deployment 配置文件中添加 redainessProbe 和 livenessProbe,但是这两个有什么区别呢?

    • LivenessProbe(存活探针):存活探针主要作用是,用指定的方式进入容器检测容器中的应用是否正常运行,如果检测失败,则认为容器不健康,那么 Kubelet 将根据 Pod 中设置的 restartPolicy (重启策略)来判断,Pod 是否要进行重启操作,如果容器配置中没有配置 livenessProbe 存活探针,Kubelet 将认为存活探针探测一直为成功状态。

      livenessProbe:
          initialDelaySeconds: 35
          periodSeconds: 5
          timeoutSeconds: 10
          httpGet:
              scheme: HTTP
              port: 8081
              path: /actuator/health
      

      上面 Pod 中启动的容器是一个 SpringBoot 应用,其中引用了 Actuator 组件,提供了 /actuator/health 健康检查地址,存活探针可以使用 HTTPGet 方式向服务发起请求,请求 8081 端口的 /actuator/health 路径来进行存活判断。

    • ReadinessProbe(就绪探针):用于判断容器中应用是否启动完成,当探测成功后才使 Pod 对外提供网络访问,设置容器 Ready 状态为 true,如果探测失败,则设置容器的 Ready 状态为 false。对于被 Service 管理的 Pod,ServicePodEndPoint 的关联关系也将基于 Pod 是否为 Ready 状态进行设置,如果 Pod 运行过程中 Ready 状态变为 false,则系统自动从 Service 关联的 EndPoint 列表中移除,如果 Pod 恢复为 Ready 状态。将再会被加回 Endpoint 列表。通过这种机制就能防止将流量转发到不可用的 Pod 上

      readinessProbe:
          initialDelaySeconds: 30
          periodSeconds: 10
          httpGet:
              scheme: HTTP
              port: 8081
              path: /actuator/health
      

      periodSeconds 参数表示探针每隔多久检测一次,这里设置为 10s,参数 initialDelaySeconds 代表首次探针的延迟时间,这里的 30 就是指待 pod 启动好了后,再等待 30 秒再进行存活性检测,跟存活指针一样,使用 HTTPGet 方式向服务发起请求,请求 8081 端口(不同的服务端口可能不一样,按照实际端口进行修改)的 /actuator/health (如果有的服务有设置 context-path,注意需要加前缀)路径来进行存活判断,若请求成功,代表服务已就绪,这样配置的话就会达到新的服务启动好了30秒后 k8s 才会让旧服务 down 掉,而30秒后,经过优化 Eureka 配置后,基本上所有的服务都已经从 Eureka 获取到了新服务的注册信息了。

    这里在实际操作的时候,LivenessProbeinitialDelaySeconds 的值通常要大于 ReadinessProbeinitialDelaySeconds 的值,否则 pod 节点会起不起来,因为此时 pod 还没有就绪,存活指针就去探测的话,肯定是会失败的,这时候 k8s 会认为此 pod 已经不存活,就会把 pod 销毁重建。

    5. 优雅停机保证正在执行的业务操作不受影响

    首先先明确旧 Pod 是怎么下线的,如果是 linux 系统,会默认执行kill -15的命令,通知 web 应用停止,最后 Pod 删除。那什么叫优雅停机?他的作用是什么?简单说就是,在对应用进程发送停止指令之后,能保证正在执行的业务操作不受影响。应用接收到停止指令之后的步骤应该是,停止接收访问请求,等待已经接收到的请求处理完成,并能成功返回,这时才真正停止应用。SpringBoot 2.3 目前已支持了优雅停机,当使用server.shutdown=graceful启用时,在 web 容器关闭时,web 服务器将不再接收新请求,并将等待活动请求完成的缓冲期。但是我们公司使用的 SpringBoot 版本为 2.1.5.RELEASE,需要通过编写部分额外的代码去实现优雅停机,根据 web 容器的不同,有分为 tomcatundertow 的解决方案:

    tomcat 方案

    /**
     * 优雅关闭 Spring Boot tomcat
     */
    @Slf4j
    @Component
    public class GracefulShutdownTomcat implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {
        private volatile Connector connector;
        private final int waitTime = 30;
    
        @Override
        public void customize(Connector connector) {
            this.connector = connector;
        }
    
        @Override
        public void onApplicationEvent(ContextClosedEvent contextClosedEvent) {
            this.connector.pause();
            Executor executor = this.connector.getProtocolHandler().getExecutor();
            if (executor instanceof ThreadPoolExecutor) {
                try {
                    ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
                    threadPoolExecutor.shutdown();
                    if (!threadPoolExecutor.awaitTermination(waitTime, TimeUnit.SECONDS)) {
                        log.warn("Tomcat thread pool did not shut down gracefully within " + waitTime + " seconds. Proceeding with forceful shutdown");
                    }
                } catch (InterruptedException ex) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }
    
    @EnableDiscoveryClient
    @SpringBootApplication
    public class ShutdownApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(ShutdownApplication.class, args);
        }
    
        @Autowired
        private GracefulShutdownTomcat gracefulShutdownTomcat;
    
        @Bean
        public ServletWebServerFactory servletContainer() {
            TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
            tomcat.addConnectorCustomizers(gracefulShutdownTomcat);
            return tomcat;
        }
    }
    

    undertow方案

    /**
     * 优雅关闭 Spring Boot undertow
     */
    @Component
    public class GracefulShutdownUndertow implements ApplicationListener<ContextClosedEvent> {
    
        @Autowired
        private GracefulShutdownUndertowWrapper gracefulShutdownUndertowWrapper;
    
        @Autowired
        private ServletWebServerApplicationContext context;
    
        @Override
        public void onApplicationEvent(ContextClosedEvent contextClosedEvent) {
            gracefulShutdownUndertowWrapper.getGracefulShutdownHandler().shutdown();
            try {
                UndertowServletWebServer webServer = (UndertowServletWebServer)context.getWebServer();
                Field field = webServer.getClass().getDeclaredField("undertow");
                field.setAccessible(true);
                Undertow undertow = (Undertow) field.get(webServer);
                List<Undertow.ListenerInfo> listenerInfo = undertow.getListenerInfo();
                Undertow.ListenerInfo listener = listenerInfo.get(0);
                ConnectorStatistics connectorStatistics = listener.getConnectorStatistics();
                while (connectorStatistics.getActiveConnections() > 0){}
            } catch (Exception e) {
                // Application Shutdown
            }
        }
    }
    
    @Component
    public class GracefulShutdownUndertowWrapper implements HandlerWrapper {
        private GracefulShutdownHandler gracefulShutdownHandler;
        @Override
        public HttpHandler wrap(HttpHandler handler) {
            if(gracefulShutdownHandler == null) {
                this.gracefulShutdownHandler = new GracefulShutdownHandler(handler);
            }
            return gracefulShutdownHandler;
        }
        public GracefulShutdownHandler getGracefulShutdownHandler() {
            return gracefulShutdownHandler;
        }
    }
    
    public class UnipayProviderApplication {
        public static void main(String[] args) {
            SpringApplication.run(UnipayProviderApplication.class);
        }
        @Autowired
        private GracefulShutdownUndertowWrapper gracefulShutdownUndertowWrapper;
        @Bean
        public UndertowServletWebServerFactory servletWebServerFactory() {
            UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory();
            factory.addDeploymentInfoCustomizers(deploymentInfo -> deploymentInfo.addOuterHandlerChainWrapper(gracefulShutdownUndertowWrapper));
            factory.addBuilderCustomizers(builder -> builder.setServerOption(UndertowOptions.ENABLE_STATISTICS, true));
            return factory;
        }
    }
    

    ok,经过以上的优化后,基本上就能做到在用户无感知的情况下,进行滚动更新。

    参考资料

    ]]>
    2021-04-16T09:43:38+00:00
    https://github.com/superleeyom/blog/issues/26关于多表关联查询的优化思路2024-04-24T05:23:58.377206+00:00问题分析

    最近在帮同事优化一个慢查询,这张主表的数量在 100w+,它具体的问题就是,查询条件非常多,大约有 30 多个可选的查询条件,这些查询的字段分散在数据库的各个表中,导致 left join 的表特别多,大约 left join 七八张表,这种情况下分页查询,查询时间在 5~6 秒,非常的影响查询体验。

    -- 示例伪 sql
    select  o.id
            t1.name,
            t2.name,
            ...       
    from order o
        left join table_1 t1
        left join table_2 t2
        left join table_3 t3
        left join table_4 t4
        left join table_5 t5
        ...
    where o.id = 1323 
        and t1.id = 2323
        and t2.name = 'xxx'
        ...
    

    解决方案

    动态 left join

    通常情况下,用户使用的查询条件只会有两到三个,所以就可以根据用户实际的查询条件,动态的 left join 相关的表,比如 mybatis 里可以这样编写:

    <if test="(ticketWebQueryDto.snCode != null and ticketWebQueryDto.snCode != '')">
        left join ticket_product_detail tpd on t.ticket_id = tpd.ticket_id
    </if>
    <if test="ticketWebQueryDto.pickingUserName != null and ticketWebQueryDto.pickingUserName != ''">
        left join ticket_picking tp on tp.ticket_id = t.ticket_id
    </if>
    

    这样一来,left join 的表就可以减少不少。

    主查询只返回主表主键 id

    select id from xxx 直接使用 index 里面的值就返回结果的。但是一旦用了 select *,就会有其他列需要读取,这时在读完 index 以后还需要去读 data 才会返回结果。这两种处理方式性能差异非常大,特别是返回行数比较多,并且读数据需要 IO 的时候,可能会有几十上百倍的差异。主查询只返回主表 id 情况下,充分利用索引的优势,通常我们的主表存放的都是其他表的 id 字段,但是页面展示的都是 name,这时候如果我们为了省事,一次性将所需要的字段的 name select 出来,势必会降低查询效率,增加回表的次数,降低索引的命中率,所以大数据量情况下,将查询分散到应用层面,而非数据库层面,整体的效率会提升很大。

    -- 示例伪 sql
    select  o.id     
    from order o
        left join table_1 t1
        left join table_2 t2
        left join table_3 t3
        left join table_4 t4
        left join table_5 t5
        ...
    where o.id = 1323 
        and t1.id = 2323
        and t2.name = 'xxx'
        ...
    

    复杂查询拆解为多次单表查询

    核心思路就是,将多表关联查询,拆解为多个单表查询,然后在进行数据整合,由于是分页查询,所以主键 id 数量肯定是有限制的,通常是 10~20 个,所以在代码层面,我们批量查询主表(单表查询):

    // 以下代码均为伪代码,只讲解下思路
    // 分页联合查询,但是 select 的 column 只有主表的主键 id
    IPage<TicketWebListDto> page = ticketMapper.listByPage(pageParam, ticketWebQueryDto);
    List<BigInteger> ticketIdList = list.stream().map(TicketWebListDto::getTicketId).collect(Collectors.toList());
    // 批量单表查询主表 
    List<Ticket> ticketPageList = ticketService.getTicketPageListByTicketIds(ticketIdList);
    Map<BigInteger, Ticket> ticketMap = ticketPageList.stream().collect(Collectors.toMap(Ticket::getTicketId, ticket -> ticket));
    

    收集其他需要 left join 表的主键id,并进行多次单表查询:

    // 收集其他表的主键id
    List<String> workerIdList = new ArrayList<>(ticketPageList.size());
    List<String> providerIdList = new ArrayList<>(ticketPageList.size());
    List<String> customerAddressIdList = new ArrayList<>(ticketPageList.size());
    for (Ticket ticket : ticketPageList) {
        workerIdList.add(ticket.getWorkerId());
        providerIdList.add(ticket.getProviderId());
        customerAddressIdList.add(ticket.getCustomerAddressId());
    }
    
    // 单表查询1
    List<ProviderWorker> providerWorkerList = CollUtil.emptyIfNull(providerWorkerService.getWorkerInfoByWorkerIds(workerIdList));
    // 单表查询2
    List<ProviderObj> providerObjList =CollUtil.emptyIfNull(providerObjService.getProviderInfoByProviderIds(providerIdList));
    // 单表查询3
    List<CustomerInfoVO> addressList = CollUtil.emptyIfNull(customerRecvAddressService.getCustomerInfoByAddressIds(customerAddressIdList));
    

    将查询到的单表数据进行内存映射,构建k-v键值对,key 是单表主键id,value 就是我们查询到的数据,这一步的目的是为了接下来循环的构建返回前端视图层 VO 的时候,直接就可以从内存里面获取我们的单表数据:

    Map<String, ProviderWorker> workerMap = providerWorkerList.stream().collect(Collectors.toMap(ProviderWorker::getWorkerId, providerWorker -> providerWorker));
    Map<String, ProviderObj> providerObjMap = providerObjList.stream().collect(Collectors.toMap(ProviderObj::getProviderId, providerObj -> providerObj));
    Map<String, CustomerInfoVO> customerInfoVOMap = addressList.stream().collect(Collectors.toMap(CustomerInfoVO::getAddressId, customerInfoVO -> customerInfoVO));
    

    循环遍历,开始构建视图层 VO :

    // 构建视图层
    List<TicketWebListDto> list = page.getRecords();
    list.forEach(t ->{
    		
        // 从内存中获取构建的数据
        Ticket ticketPage = ticketMap.get(t.getTicketId());
        ProviderObj providerObj = ObjectUtil.defaultIfNull(providerObjMap.get(ticketPage.getProviderId()), new ProviderObj());
        ProviderWorker providerWorker = ObjectUtil.defaultIfNull(workerMap.get(ticketPage.getWorkerId()), new ProviderWorker());
        CustomerInfoVO customerInfoVO = ObjectUtil.defaultIfNull(customerInfoVOMap.get(ticketPage.getCustomerAddressId()), new CustomerInfoVO());
        
        // 设置value
        t.setProviderName(providerObj.getProviderName());
        t.setWorkerName(providerWorker.getWorkerName());
        t.setCustomerName(customerInfoVO.getName());
        // ...
    });
    return page;
    

    总结

    1. 在数据量比较大的情况下,多表关联查询,拆解为多个单表查询,可以提高整体的查询效率
    2. for 循环里面,不能有查询 DB 的操作,应该在 for 循环外批量从 DB 取出后,映射到内存中,然后 for 循环从内存中读取
    ]]>
    2021-03-26T11:26:27+00:00
    https://github.com/superleeyom/blog/issues/25[笔记]人生海海2024-04-24T05:23:58.461912+00:00《人生海海》这本书真的写的太棒了,麦家老师的文笔优美,平易近人,接地气,同时风趣幽默,最后看完本书后,鼻子酸酸的,满心的感受,上校的人生经历扑朔迷离,里面有一句话特别感动:

    世上只有一种英雄主义,就是在认清了生活真相后依然热爱生活。

    真的特别推荐阅读!

    人生海海

    作者:麦家;数量:39个笔记;时间:2021-03-20 11:23:07

    • 第一章:
      • 爷爷讲:“绰号是人脸上的疤,难看。但没绰号,像部队里的小战士,没职务,再好看也是没人看的,没斤量的。”
    • 第二章:
      • 有一次,我看到爷爷像发神经,在对一只狸花猫讲:“人世间就这样,池塘大了,水就深了,水深了,鱼就多了,大鱼小鱼,泥鳅黄鳝,乌龟王八,螃蟹龙虾,鲜的腥的,臊的臭的,什么货色都有。”
      • 总之,爷爷活成一个老埠头,你要改变他是很难的,不像我。我像三月里的桃树,一夜之间变成一幅画、一本诗,花枝招展,灿烂得连自己都认不得。
    • 第三章:
      • 但浓郁的香气会飞的,从锅铁里钻出,从窗洞里飘出,随风飘散,像春天的燕子在逼仄的弄堂里上下翻飞。香气驱散了空气里的污秽,像给空气撒了一层金,像闪闪金光点亮了人眼睛一样,拉长了人的鼻子。
      • 我本来是鼓足力气抱他的,反而被这个轻压垮了,哭了。
    • 第四章:
      • 这样不好的,人啊,心头一定要有个怕,有个躲。世间很大,天外有天,山外有山,不能太任着性子,该低头时要低头,该认错时要认错。”
      • 俗话讲不怕老只怕小,小鬼作恶老鬼哭。你不晓得,我早晓得,城里被这些小鬼搅翻了天,每天江面上都浮出无名死尸。这些小子心还没有长圆,做事没轻重,还是避一避好。”
      • 这天夜里十四岁的我第一次尝到了失眠的滋味,是一种夜色也有重量、形状和气味的滋味,像没睡在床铺上,是睡在黑色的空气上,睡在一堆目不暇接、纷乱和狂热的思绪里。这些思绪互相仇恨,穿着黑衣围攻我,让我虽然一动不动却累得不行,好像血液的流动需要齿轮转动才能带动。
    • 第五章:
      • 大人很怪的,平时总教育我们要诚实,讲真话,不能撒谎,自己却经常鬼话连篇。
    • 第六章:
      • 这是革命,革命不是请客吃饭,革命就是无情,就是斗争,就是撕开敌人的伪装,亮出他们丑恶的灵魂。
    • 第七章:
      • “他该难过的都难过了还有什么好难过的。”
      • 老保长曾经讲过,我母亲是只洞里猫,四十岁像十四岁一样没声响,一声响就脸红;父亲是老虎屁股摸不得,张口要骂娘,出手要打人;爷爷是半只喜鹊半只乌鸦,报喜报丧一肩挑。爷爷平常不骂人,骂人就是报丧,你会很难过的。爷爷这顿讥讽数落,洪水一样的,把表哥的心情彻底冲坏。我看他一言不发地离去,脚步沉重得要死,像只落汤鸡,鞋子里灌满泥淖。
      • 这哪是解围?这是雪上加霜,痛打落水狗。我更加羞愧,虽有一百个念头,有千言万语想讲,想骂人,想打人,想……却没有选择,只是一声不吭,缩着身子,垂落着头,灰溜溜地走了。我感到,背上负着一千斤目光,两条细腿撑不住,在打战。我第一次认识到,羞愧是有重量的。
      • 三四
    • 第八章:
      • 人言可畏,人心叵测。有些人的心是黑的,存心用来害人的,有些人的嘴是专门长来放屁造谣的。
      • 大多数蚊虫到寒露节气就要死掉,寒露寒露,蚊虫无路,指的就是这意思。但叮过人、吃过人血的蚊虫,精气足,头脑灵,变得聪明,到了寒露时节会寻个暖和的地方做窝,睡大觉,养精蓄锐。这样就可以熬过三九严寒,死不了,变成蚊虫精,来年继续作威作福。
    • 第九章:
      • 爷爷讲,我睡觉像死猪,雷都劈不醒,他睡觉像松鼠,掉一片树叶都会醒。
      • 门稀开一条缝,切进来一路月光,仿佛爷爷乘着月光走了;同时那个呜咽声也一同被月光照亮,满当当地挤拥在我心里:恐惧、好奇、刺激、紧张、混乱的感觉,在黑暗和呜咽声中左冲右突,起伏跌宕。
      • 这是我在村里最后一次见到他,月光下,他面色是那么苍白凄冷,神情是那样惊慌迷离,步履是那么沉重拖沓,腰杆是那么佝偻,耷拉的头垂得似乎要掉下来,整个人像团奄奄一息的炭火,和我印象中的他完全不是同个人——像白天和黑夜的不同,像活人和死鬼的不同,像清泉和污水的不同。
      • 我觉得,这一夜,像一道黑色的屏障,把我和过去彻底隔开,现在的我满脑子是疑问,是恐惧,是孤独,是无助,是冤屈,是被黑暗的谜团重重包围的样子,是天塌地陷的感觉。
    • 第十章:
      • 爷爷似乎很有感想,接着老保长的话讲:“是啊,老流氓,历史上杀人灭口的案例多,所以还是什么都不知晓的好。老古话讲得好,箱子里存的钱是越多越好,心里存的事是越少越好。”
      • 爷爷讲过,村子的一年四季,像人的一辈子,春天像少小孩子,看上去五颜六色,生龙活虎,朝气蓬勃,实际上好看不中用,开花不结果,馋死人(春天经常饿死人);夏天像大小伙子,热度高,精气旺,力(热)气日日长,蛇虫夜夜生,农忙双抢(结婚生子),手忙脚乱,累死人;秋天像精壮汉子,人到中年,成熟了,沉淀了,五谷丰登,六畜兴旺,天高云淡,不冷不热,爽死人;冬天像死老头子,寒气一团团冒,衣服一件件添,出门缩脖子,回家守床板,闷死人。
    • 第十一章:
      • 屋里一团黑,窗外更加黑,黑得发亮,有冲力的,洪水一样,排山倒海朝我扑来,把我吞没又抛起,抛起又摔下,摔下又托住,托住又跌落、吞没……什么叫骇人听闻?我那天就骇人听闻了。
      • 这注定是个不堪的夜晚,一个力败气衰的老头,一个世事不谙的少年,承受着世间最羞的辱、最沉的重。
    • 第十三章:
      • 这天我懂了一个新道理:人和兽之间,只隔着一团愤怒,像生死之间只隔着一层纸
    • 第十四章:
      • 年轻人容易心碎,老人容易嘴碎。
      • 人们爱听瞎话,不爱听真话,正如大家互相不叫名字,爱叫绰号一样。
      • 爷爷讲:“收音机里看不见人,玻璃柜里藏不了人。”意思是做人要亮身子,讲话要见芯子。
    • 第十五章:
      • 月光爬在墙上,久了,累了,都从墙上下来,匍匐在天井里,把灰白的地砖照得冒出冷气。
    • 第十六章:
      • 这个该死的下午,天地是雪白,可人是污黑的,坏人打好人,儿子骂老子,天理皇道塌下来,压得我窒息,心里眼前一团黑,恨不得哭死。
      • 对我好言好待十六年,却没有得到我一分钟的话别。爷爷讲过,一分钟的和好抵得过一辈子的仇恨,我和他正好反过来。
    • 第十七章:
      • 报纸上说,生活不是你活过的样子,而是你记住的样子。
    • 第十八章:
      • 报纸上说的,当一个人心怀悲悯时就不会去索取,悲悯是清空欲望的删除键。
      • 心有雷霆面若静湖,这是生命的厚度,是沧桑堆积起来的。
    • 第十九章:
      • 人像一枚硬币,有两面,遇到好的一面是你运气,遇到坏的一面是你晦气,如果两面都叫你遇到则不免要丧气叹气。
      • 她说:“世上没有如果,只有后果。”
    • 第二十章:
      • 世上只有一种英雄主义,就是在认清了生活真相后依然热爱生活。
      • 报纸上说,多数人说了一辈子话,只有临终遗言才有人听;如果临终遗言都没人听,这人差不多就白活了。
      • 报纸上说,生活是如此令人绝望,但人们兴高采烈地活着。
      • 没有完美的人生,不完美才是人生。
    ]]>
    2021-03-20T03:35:02+00:00
    https://github.com/superleeyom/blog/issues/24关于prometheus无法采集服务指标的问题总结2024-04-24T05:23:58.675864+00:00名词解析

    context-path

    对应的 Spring Boot 后台服务,如果增加了 server.servlet.context-path 配置,则会指定项目路径,是构成 url 地址的一部分,比如,在没有加此配置前,我们获取用户列表接口是这样访问:

    http://127.0.0.1:8090/user/list
    

    设定项目路径,server.servlet.context-path=demo,则用户访问接口路径变为:

    http://127.0.0.1:8090/demo/user/list
    

    prometheus

    中文名称叫「普罗米修斯」,普罗米修斯主要用于事件监控和警告,它可以和 Spring Boot 的子项目 Spring Boot Actuator 进行整合,它为应用提供了强大的监控能力,目前网上有很多的整合的示例,本文不在这里细讲了:

    起因

    在 SkyWalking 上监控到,有很多服务的普罗米修斯监控请求,出现了 404:

    后面经过排查,就是由于应用设置了 context-path的原因造成的,由于普罗米修斯监控站点走的是 http://${host}:${port}/actuator/prometheus这种 url,但是实际我们的服务都是加了context-path,也就是 http://${host}:${port}/${context-path}/actuator/prometheus,就导致普罗米修斯在 fetch 的时候,直接404,无法获取监控信息。

    解决方案

    由于 prometheus 是通过 Eureka 发现服务的,观察 prometheus 的配置文件 prometheus.yml

    scrape_configs:
      - job_name: 'eureka-prometheus'
        # 采集的路径
        metrics_path: '/actuator/prometheus'
        # eureka 注册中心地址
        eureka_sd_configs:
          - server: http://192.168.100.93:8761/eureka
    

    由于后台服务都是注册在 Eureka 上的,比如我们查看某个服务在 Eureka 上的注册信息,浏览器访问:http://192.168.100.93:8761/eureka/apps/${application-name},例如这个服务返回的注册信息:

    可以看出我们并没有将服务的指标路径(抓取路径)写入到 Eureka 的元数据(metadata) 中,所以 prometheus 最终发起的获取监控信息请求是http://ip:port+metrics_path:,比如:http://10.233.99.10:9425/actuator/prometheus,那假设这个服务没有设置 context-path,它肯定是可以正常返回监控信息:

    如果设置了 context-path,它最终依旧还是以 http://10.233.99.10:9425/actuator/prometheus 去访问,那肯定就会提示 404 了。

    加了server.servlet.context-path以后,抓取的路径就不再是 http://10.233.99.10:9425/actuator/prometheus了,而是变成了 http://10.233.99.10:9425/inventory/actuator/prometheus了。之前我们 prometheus.yml 文件里静态配置抓取目标的 metrics_path/actuator/prometheus,但是现在不能这样写了,因为加了应用上下文路径,而且每个服务都不一样,所以为了能够根据各服务动态自定义指标路径,需要如下处理:

    1. 在服务的application.yml文件里,增加如下的配置:

      eureka:
        instance:
          metadata-map:
            "prometheus.scrape": "true"
            "prometheus.path": "${server.servlet.context-path}/actuator/prometheus"
            "prometheus.port": "${server.port}" 
      

      prometheus 是通过 Eureka 发现服务的,因此只有将服务的指标路径(抓取地址)写到 Eureka 里,prometheus 才能拿到,换言之,只有服务在注册的时候,将自己暴露的端点(endpoint)以元数据的方式写到 Eureka 中, prometheus 才能正确的从目标抓取数据。

    2. 修改 prometheus.yml,去掉指定的metrics_path, 改为通过 Eureka 获取抓取目标:

      scrape_configs:
        - job_name: 'eureka-prometheus'
          eureka_sd_configs:
            - server: http://192.168.100.93:8761/eureka
          relabel_configs:
            - source_labels: [__meta_eureka_app_instance_metadata_prometheus_path]
              action: replace
              target_label: __metrics_path__
              regex: (.+)
      
    3. 重启对应的后台服务,不出意外,prometheus 就能正常的获取监控信息了。

    参考资料

    ]]>
    2021-03-12T10:32:04+00:00
    https://github.com/superleeyom/blog/issues/23[笔记]最好的告别2024-04-24T05:23:58.765262+00:00最好的告别:关于衰老与死亡,你必须知道的常识

    作者:阿图·葛文德;数量:23个笔记;时间:2021-03-10 10:12:36

    • 01 独立 活到100岁的代价:
      • 这一天,在雅典县展览会的正面看台,在几百个欢呼雀跃的人的注视下,他宣誓成为美国公民。但是,有一个美国人的习俗他没有接纳,那就是对待老人和病弱者的方式——让他们独自生活,或者把他们丢给一系列无名的设备,让他们在生命的最后日子同几乎只知道他们名字的医生、护士一起度过。这是同他的祖国印度最不相同的一点
    • 田园牧歌式的老年生活:
      • 健康专业人员有一个系统的标准来评估一个人的身体功能。如果在没有他人帮助的情况下不能如厕、进食、穿衣、洗浴、整容、下床、离开座椅、行走(所谓“八大日常生活活动”),那么,说明你缺少基本的生活自理能力。如果不能自行购物、做饭、清理房间、洗衣服、服药、打电话、独自旅行、处理财务(所谓“八大日常生活独立活动”),那么,你就缺少安全地独自生活的能力。
    • 活得久了,问题来了:
      • 历史的发展轨迹非常清楚:一旦人们拥有告别传统生活方式的足够的资源和机会,他们就会义无反顾地拥抱新生活。
    • 人如何衰老以及为什么会老:
      • 相比于平均值,寿命长短只有3%取决于父母的寿数,而高矮则90%取决于父母的身高。即便是基因相同的双胞胎,寿命差异也很大:典型的差距在15岁以上。
    • 03 依赖 我们为老做好准备了吗:
      • 高龄老人告诉我,他们最害怕的并不是死亡,而是那之前的种种状况——丧失听力、记忆力,失去最好的朋友和固有的生活方式。正如菲利克斯对我说的:“老年是一系列连续不断的丧失。”在小说《每个人》(Everyman)中,作家菲利普·罗思(Philip Roth)说得更加苦涩:“老年不是一场战斗,而是一场屠杀。”
    • 老了但对生活的要求不能仅仅是安全:
      • 在我们衰老脆弱、不再有能力保护自己的时候,如何使生活存在价值。
    • 什么时候可以考虑去老人院看一看:
      • 一旦衰老导致衰弱,似乎就没人可以活得快乐。
    • 有没有一个真正像家的“老年之家”:
      • 人类需求层次。这个理论经常被描述为一个金字塔。塔基是基本需求——生存的必需品(如食物、水、空气)和安全的必需品(如法律、秩序及稳定)。其上一个层次是爱的需求和归属感需求。再其上是成长的愿望——实现个人目标、掌握知识和技能、成就得到承认并获得奖励的机会。
      • 当“生命的脆弱性凸显出来”时,人们的日常生活目标和动机会彻底改变。至关紧要的是观念,而不是年龄。
    • 用两条狗、4只猫、100只鸟发起的革命:
      • 针对厌倦感,生物会体现出自发性;针对孤独感,生物能提供陪伴;针对无助感,生物会提供照顾其他生命的机会。
    • 修复健康,也需滋养心灵:
      • 为什么仅仅存在,仅仅有住、有吃、安全地活着,对于我们是空洞而无意义的?我们还需要什么才会觉得生命有价值?
      • 他认为,答案是:我们都追求一个超出我们自身的理由。对他来说,这是人类的一种内在需求。这个理由可大(家庭、国家、原则)可小(一项建筑工程、照顾一个宠物)。重要的是,在给这个理由赋予价值、将其视为值得为之牺牲之物的同时,我们赋予自己的生命以意义。
    • 生活中最好的事,就是能自己上厕所:
      • 自主的价值……在于它所产生的责任:自主使得我们每个人负责根据某种连贯的独特的个性感、信念感和兴趣,塑造自己的生活。它允许我们过自己的生活,而不是被生活所驱使,这样,我们每个人都能够在权利框架允许的范围内,成为他塑造的那个自己。
    • 战胜老年生活的无聊与无助:
      • 对疾病和老年的恐惧不仅仅是被迫忍受对种种丧失的恐惧,同样也是对孤独的恐惧。当人意识到生命的有限,他们就不再要求太多。他们不再寻求更多的财富,不再寻求更多的权力。他们只要求,在可能的情况下,被允许保留塑造自己在这个世界的生命故事的权利——根据自己的优先顺序作出选择,维持与他人的联系。
    • 大限来临该做什么:
      • 问题不是我们如何能够承担这个系统的开支,而是怎样建立一个系统,能够在人们生命终结之时,帮助他们实现其最重要的愿望。
    • 尽全力救治也许不是最正确的做法:
      • 只有不去努力活得更长,才能够活得更长。
    • 临终讨论专家的话术:
      • 接受个人的必死性、清楚了解医学的局限性和可能性,这是一个过程,而不是一种顿悟。
    • 选择可以信任的医生:
      • 他具有那种中西部人的特点,习惯在别人说完话后等一拍,确定别人真的说完了以后,自己才开始说话。
    • 三种医患关系:家长型、资讯型、解释型:
      • 人们寻求的首先是信息背后的意义,而不是信息本身。传递意义的最佳途径,他说,是告诉人们信息于你而言的意义。
    • 艰难的谈话如何开始:
      • 把今天过到最好、而不是为了未来牺牲现在
    • 08 勇气 最好的告别:
      • 在年老和患病的时候,人至少需要两种勇气。第一种勇气是面对人终有一死的事实的勇气——寻思真正应该害怕什么、可以希望什么的勇气。这种勇气已经够难了,我们有很多理由回避它。但是更令人却步的是第二种勇气——依照我们发现的事实采取行动的勇气。
    • 选择比风险计算更复杂:
      • 生命之所以有意义乃是因为那是一个故事。一个故事具有整体感,其弧度取决于那些有意义的时刻、那些发生了重要事情的时刻。逐刻评价人们的愉悦水平和痛苦水平忽视了人类存在的这一根本面向。表面看似幸福的生命可能是空虚的,而一个表面看似艰难的生活可能致力于一项伟大的事业。
    • 善终不是好死而是好好活到终点:
      • 我们在对待病人和老人方面最残酷的失败,是没有认识到,除了安全和长寿,他们还有优先考虑事项;建构个人故事的机会是维持人生意义的根本;通过改变每个人生命最后阶段的可能性这一方式,我们有机会重塑我们的养老机构、我们的文化和我们的对话。
    • 和父亲最后的对话:
      • 结尾不仅仅是对死者重要,也许,对于留下的人,甚至更重要
    ]]>
    2021-03-10T02:16:53+00:00
    https://github.com/superleeyom/blog/issues/22Maven中关于SNAPSHOT版本的总结2024-04-24T05:23:58.856975+00:00Maven中的SNAPSHOT版本

    假设有两个小组负责维护两个组件,example-serviceexample-ui,这两个组件不在同一个代码仓库,example-service 的版本号信息:

    <artifactId>example-service</artifactId>
    	<version>1.0</version>
    <packaging>jar</packaging>
    

    其中 example-ui 项目依赖于 example-service

    <dependency>
    	<groupId>com.xxx.yyy</groupId>
    	<artifactId>example-service</artifactId>
    	<version>1.0</version>
    </dependency>
    

    而这两个项目每天都会构建多次,我们知道,maven 的依赖管理是基于版本管理的,对于发布状态的 artifact,如果版本号相同,即使我们内部的镜像服务器上的组件比本地新,maven 也不会主动下载的。 假如 example-service 增加了一些新的功能,这时候就得升级 example-service 的版本号,然后 deploy 到 maven 私服上去,由于升级了 example-service 的版本号为 1.1,example-ui 由于是依赖方,开发阶段,它想要使用example-service的新功能,则要跟着把 example-service 的版本号到 1.1,如果example-service更新的很频繁,每次构建你都要升级 example-service 的版本,效率就非常低。

    那引入 SNAPSHOTRELEASE 版本控制,这两种版本是分别在不同的 maven 仓库,前者是快照版本,用于开发环境,后者是稳定正式版本,用于生产环境,那在开发阶段,我们需要将 example-service 的版本号改为:

    <artifactId>example-service</artifactId>
    	<version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    

    在该模块的版本号后加上 -SNAPSHOT 即可(注意这里必须是大写),然后 deploy 到私服,在 maven-snapshots 仓库下,version 列根据发布时间不同自动在 1.0 后面加上了当前时间,以此区别不同的快照版本:

    example-ui 项目里,引入 example-service 快照版本:

    <dependency>
    	<groupId>com.xxx.yyy</groupId>
    	<artifactId>example-service</artifactId>
    	<version>1.0-SNAPSHOT</version>
    </dependency>
    

    这样的话,每次 example-ui 构建时,会优先去远程仓库中查看是否有最新的 example-service-1.0-SNAPSHOT.jar,不需要频繁的去修改example-service 的版本号。等到两个组件要正式上线,example-service 的版本号改为:

    <artifactId>example-service</artifactId>
    <version>1.1-RELEASE</version>
    <packaging>jar</packaging>
    

    然后 deploy 到私服,example-ui 项目里,引入 example-service 正式升级版本:

    <dependency>
    	<groupId>com.xxx.yyy</groupId>
    	<artifactId>example-service</artifactId>
    	<version>1.1-RELEASE</version>
    </dependency>
    

    所以总的来说,对于 Maven 版本号,我们最好这样约定:

    1. 【强制】开发阶段版本号定义为 SNAPSHOT,发布后版本改为 RELEASE。
    2. 【强制】线上应用不要依赖 SNAPSHOT 版本(安全包除外);正式发布的类库必须先去中央仓库进行查证,使 RELEASE 版本号有延续性,版本号不允许覆盖升级。

    参考资料

    ]]>
    2021-02-24T09:25:10+00:00
    https://github.com/superleeyom/blog/issues/20为Git和Maven设置代理加速2024-04-24T05:23:58.967478+00:00Git

    由于 GFW 的缘故,有时候要去 Github 上克隆代码,半天 git clone 不下来,改过 host,设置过代理镜像,发现根本不管用,最后整来整去,花钱买个好点的梯子,设置好 Git 代理,要省不少事情。

    全局代理

    Git 设置全局代理(前提你已经买了比较好的梯子),根据代理协议的不同,在终端执行如下命令:

    # 代理协议是socket5,我这里监听端口是1086,实际改成你自己的监听端口
    git config --global http.proxy socks5://127.0.0.1:1086
    git config --global https.proxy socks5://127.0.0.1:1086
    # 代理协议是http,用这个,实际改成你自己的监听端口
    git config --global http.proxy http://127.0.0.1:1080
    git config --global https.proxy https://127.0.0.1:1080
    

    在哪里可以查看梯子的代理协议?比如我用的是 ClashX,截图如下:

    如果是 Shadowsocks 截图如下:

    部分代理

    我们大部分情况下,由于 GFW 的缘故,只需要对 Github 设置代理,国内的比如 Gitee 其实没有必要走代理,推荐这样设置,只针对 Github 设置部分代理:

    # 代理协议是socket5(推荐)
    git config --global http.https://github.com.proxy socks5://127.0.0.1:1086
    git config --global https.https://github.com.proxy socks5://127.0.0.1:1086
    # 代理协议是http
    git config --global http.https://github.com.proxy http://127.0.0.1:1080
    git config --global https.https://github.com.proxy http://127.0.0.1:1080
    

    取消代理

    取消 Git 的全局/部分代理:

    git config --global --unset http.proxy
    git config --global --unset https.proxy
    

    速度对比

    没有设置代理前,平均 6.00 KiB/s

    $ git clone https://github.com/mybatis/mybatis-3.git
    Cloning into 'mybatis-3'...
    remote: Enumerating objects: 3, done.
    remote: Counting objects: 100% (3/3), done.
    remote: Compressing objects: 100% (3/3), done.
    ^Cceiving objects:   0% (86/352273), 44.00 KiB | 6.00 KiB/s
    

    设置代理后,平均 6.90 MiB/s

    $ git clone https://github.com/mybatis/mybatis-3.git
    Cloning into 'mybatis-3'...
    remote: Enumerating objects: 3, done.
    remote: Counting objects: 100% (3/3), done.
    remote: Compressing objects: 100% (3/3), done.
    remote: Total 352273 (delta 0), reused 0 (delta 0), pack-reused 352270
    Receiving objects: 100% (352273/352273), 104.22 MiB | 6.90 MiB/s, done.
    Resolving deltas: 100% (302817/302817), done.
    

    没有对比就没有伤害,fuck GFW!!!

    Maven

    Maven 也是跟 Git 一样,拉取中央仓库的依赖时候,由于 GFW 的缘故,不设置代理的情况下,半天依赖是拉取不下来,通过设置 settings.xml ,配置代理也可以解决依赖下载速度过慢的问题:

    <proxies>
        <proxy>
            <id>ClashX</id>
            <active>true</active>
            <protocol>socks5</protocol>
            <host>127.0.0.1</host>
            <port>1086</port>
          	<!--不需要设置代理的ip或域名,多个用|分隔,比如公司自己搭建的maven私服镜像,阿里云镜像等-->
            <nonProxyHosts>172.16.xx.xx|maven.aliyun.com</nonProxyHosts>
        </proxy>
    </proxies>
    

    设置完毕后,依赖下载丝滑流畅😂,更加具体配置的可以参考 Maven 官方配置文档:Configuring a proxyfuck GFW!!!

    ]]>
    2021-02-04T01:31:45+00:00
    https://github.com/superleeyom/blog/issues/19GitHub Actions 实战之监控梯子流量2024-04-24T05:23:59.051087+00:00

    起因

    最近也开通了 Netflix,Netflix 其实挺费流量的,为了防止梯子的流量超标,所以打算借助 Github Actions + telegram 做一个简单的监控,整体的思路其实很简单,没啥太大的难度,就是模拟梯子服务网站的登录,然后爬取页面的流量汇总数据,然后每天 9:30 将流量的使用情况发送到 telegram,同时如果可使用的流量少于 20% 的时候,推送报警到 telegram,代码目前放到了 github 上 proxy-traffic-monitor,实现细节就不讲了,代码比较简单,直接看代码就行。

    开发环境

    • springboot 2.0+
    • jdk 1.8+

    准备工作

    1. 创建一个 telegram bot 🤖,如果不会创建的话,参见 telegram 的官方文档:Creating a new bot,或者直接谷歌搜下,一大堆的教程,保存 telegram bottoken,这个很重要。

    2. 创建好机器人🤖后,接下来就是要获取聊天id,也就是 chatId

      • 打开你创建的机器人,随便发点啥,比如发个:hello world

      • 浏览器输入:https://api.telegram.org/bot(这里加上你的token)/getUpdates,会返回如下示例:

        {
          "ok": true,
          "result": {
            "message_id": 3,
            "from": {
              "id": 1432925625,
              "is_bot": true,
              "first_name": "SuperLeeyom",
              "username": "SuperLeeyomBot"
            },
            "chat": {
              "id": 599877436,
              "first_name": "Leeyom",
              "username": "super_leeyom",
              "type": "private"
            },
            "date": 1612000615,
            "text": "这是一条神奇的消息~"
          }
        }
        

        取到 chat 下面的 id ,这个就是聊天 id 了,比如我这里的就是 599877436

      • 然后打开浏览器,输入:https://api.telegram.org/bot(这里加上你的token)/sendMessage?chat_id=(你的chatId)&text=这是一条神奇的消息~,不出意外你应该能收到一条消息,注意一定要是代理情况下你才能收到,毕竟 telegram 在国内无法使用的。

    3. 准备MonoCloudByWave这两家的代理的账号和密码,目前我使用时这两家的服务,还行吧,价格比较贵,但是比较稳定吧。

    如何使用

    • fork 项目proxy-traffic-monitor

      • 在项目的Settings-Secrets选项下,点击New repository secret,创建我们准备工作的几个工作常量,如果只用其中一家,另外一家的可以账号密码可设置为空:
        • BY_WAVE_USER_NAME:bywave 账号
        • BY_WAVE_PASSWORD:bywave 密码
        • MONO_CLOUD_USER_NAME:monoCloud 账号
        • MONO_CLOUD_PASSWORD:monoCloud 密码
        • TG_CHAT_ID:telegram 聊天 id
        • TG_TOKEN:telegram bot token
    • 目前有两个定时,分别是daily.ymlwarn.yml,前者是每天 9:30 点执行一次,汇总流量使用情况发送到 telegram,后者是每隔 2 个小时执行一次,监控可用流量的是否已经少于 20%,若少于 20% 会推送到telegram 进行预警,若要调整时间,可以修改这两个 yml 的 cron 表达式。

      • 我这里默认关闭warn.yml这个自动化任务了,因为我发现,ByWave 好像已经对对 github actions 的 ip做限制了,可能我测试的太频繁了吧😂,自己有需要的再打开这个注释吧

        on:
          workflow_dispatch:
        #  schedule:
        #    - cron: "0 */2 * * *"
        
      • ByWave 有防爬虫机制,所以定时任务太频繁,有可能会被限制 ip 地址,导致 github actions 自动化执行的时候,无法登录,如果被限制了,可以通过更换代理 ip 的方式:

        Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("xxx.xxx.xxx.xxx", 80));
        loginRequest.setProxy(proxy);
        
    • 如果喜欢,就点个 star 吧,以上就是这些了!Enjoy!

    声明

    本源码只用于学习和交流,禁止用于商业目的。

    ]]>
    2021-02-01T06:22:00+00:00
    https://github.com/superleeyom/blog/issues/18咕咚和keep跑步数据导入Nike Run Club2024-04-24T05:23:59.134580+00:00起初是 yihong 在v站推广他的开源跑步项目 running_page,我那天在v站刷帖无无意看到了,就点进去了解了下,发现确实挺不错的项目,可以抓取各个平台的跑步数据,汇总聚合在一起,生成一个精美的跑步主页。后面通过 twitter 联系上了 yihong,yihong 是个非常热情,乐于助人的人,在他的帮助下,我成功了拿到了咕咚和 keep 上的跑步数据,并且在他的安利下,加上本身实在是受不了国内运动软件上各种广告,正式从 keep 换到了 Nike Run Club(后面简称 nrc)。

    切换到 nrc 后,之前其实我有折腾过想把之前在咕咚和 keep 上的数据导入到 nrc,毕竟积累了好几千公里的跑量,放弃掉实在太可惜。后面通过 yihong 的提供的思路,可以尝试将咕咚、keep 的跑步数据导出 gpx,然后再把 gpx 导入到类似 Garmin Connect 等平台,然后在 nrc 上与佳明进行绑定,通过曲线救国,就可以将数据导入进 nrc。

    gpx 是一种 XML 格式,专门为应用软件设计的通用 GPS 数据格式,它可以用来描述路点、轨迹、路程,大部分的运动类软件都支持此类通用格式的导入。

    早在一个月前,我尝试如下的的步骤:

    1. 利用 running_page 项目,导出 gpx 数据
    2. 创建一个国区 garmin connect 的账号,将 gpx 数据一次性导入 Garmin Connect
    3. 在 nrc 上关联 Garmin,然后数据就会自动同步过来

    但是很遗憾并没有成功,后面我就没有在弄了。就在这两天,yihong 说他和另外一个网友,搞定了咕咚数据的抓取,所以又开始着手重新尝试。我仔细想了下,我当时的步骤是先创建 Garmin Connect 的账号,然后把 gpx 数据上传到佳明,最后再到 nrc 上关联 Garmin。是不是我的步骤不对?是不是 Garmin 是主动把数据推送给 Nike 的?所以在我没关联之前,就把数据上传了,没有触发推送?带着这些疑问,所以我又尝试了如下的步骤(最好全程都开启代理的情况下进行):

    1. 创建一个国区 garmin connect 的账号,非国区可能不太行,若已有账号不需要重复创建
    2. 在 nrc 上关联 Garmin,出现如下的界面说明绑定成功:
      • 关联成功
      • garmin
    3. 在 Garmin Connect 上传 gpx 数据
      • 上传 gpx 数据
      • 上传 gpx 数据
    4. 打开 nrc,然后刷新数据,同步的时候可能会费点时间,如果刷新后总里程数增加了,那么恭喜你,同步成功,由于缓存的缘故,nrc 的总里程会显示不对,最好退出重新登录几次。
      • 同步成功
      • 同步成功

    以上便是我整个同步过程的一些记录,如果导入后,没啥动静,建议在佳明那边删除掉已导入的 gpx 数据,在佳明那边解除 nike 绑定,然后再重新绑定,再重新导入。我觉得要想保证导入成功需要注意如下几点:

    1. 确保 gpx 数据的准确性
    2. 找个好的梯子,在全局代理环境下操作
    3. 注意操作顺序,绑定一定要在导入 gpx 数据之前
    4. 佳明账号选择国区:connect.garmin.cn

    由于在通过 running_page 项目生成 keep 和咕咚的 gpx 数据的时候,由于 keep 数据不完整性,实际生成的 gpx 文件不是很完整,丢失了差不多 1000 公里的数据,但是也无所谓了,能拿到 80% 的数据我已经很开心了,哈哈。

    最后贴下:

    ]]>
    2021-01-27T08:31:57+00:00
    https://github.com/superleeyom/blog/issues/17redis大key内存分析2024-04-24T05:23:59.216094+00:00最近 redis 的内存占用比较高,需要分析下哪些 key 内存占用率比较高,所以整理下分析的思路和笔记。

    bigkeys

    使用 redis 自带的查询工具:

    redis-cli -p 6379 -a 密码 --bigkeys
    

    比如我执行结果:

    [root@localhost ~]# redis-cli -p 6379 -a pd123456 --bigkeys
    
    # Scanning the entire keyspace to find biggest keys as well as
    # average sizes per key type.  You can use -i 0.1 to sleep 0.1 sec
    # per 100 SCAN commands (not usually needed).
    
    [00.00%] Biggest hash   found so far 'alarm:monitor:virtual.67481375665548lumi.light8.0.8107' with 1 fields
    [27.15%] Biggest hash   found so far 'alarmDefinitionCache' with 2 fields
    [28.14%] Biggest hash   found so far 'AIOT_DEVICE_INFO' with 62867 fields
    [45.33%] Biggest hash   found so far 'RESOURCE_LAST_TS' with 111030 fields
    [56.45%] Biggest set    found so far 'SMART_HOTEL_USER_CACHE' with 4 members
    [57.53%] Biggest zset   found so far '1h~keys' with 2 members
    
    -------- summary -------
    
    Sampled 128971 keys in the keyspace!
    Total key length in bytes is 7387660 (avg len 57.28)
    
    Biggest    set found 'SMART_HOTEL_USER_CACHE' has 4 members
    Biggest   hash found 'RESOURCE_LAST_TS' has 111030 fields
    Biggest   zset found '1h~keys' has 2 members
    
    0 strings with 0 bytes (00.00% of keys, avg size 0.00)
    0 lists with 0 items (00.00% of keys, avg size 0.00)
    1 sets with 4 members (00.00% of keys, avg size 4.00)
    128969 hashs with 337016 fields (100.00% of keys, avg size 2.61)
    1 zsets with 2 members (00.00% of keys, avg size 2.00)
    

    我们可以看到打印结果分为两部分,扫描过程部分,只显示了扫描到当前阶段里最大的 key。summary 部分给出了每种数据结构中最大的 Key 以及统计信息。

    redis-cli --bigkeys 的优点是可以在线扫描,不阻塞服务;缺点是信息较少,内容不够精确。扫描结果中只有 string 类型是以字节长度为衡量标准的。List、set、zset 等都是以元素个数作为衡量标准,只能看出来一个数据结构下有多少数据,看不出来到底占多少内存,看着数值大的并不一定有问题,也不一定占用空间很大,所以这个工具只能用来做大致分析。

    redis-rdb-tools

    那其实最好的办法就是离线分析,这里推荐一个工具:redis-rdb-tools,整体的思路就是,导出 redis 的 rdb 备份文件,生成内存报告,把所有 key 转换为 JSON,转存别的 DB 等,这里的 DB 就用 sqlite 就行。

    1. 先用 redis-cli 工具连上 Redis 执行 bgsave,备份完成后,将 rdb 文件下载到本地。

    2. 安装 redis-rdb-tools

      pip install rdbtools python-lzf
      

      或者:

      git clone https://github.com/sripathikrishnan/redis-rdb-tools
      cd redis-rdb-tools
      sudo python setup.py install
      

      若提示缺少组件,按照提示安装好即可。

    3. 若没有安装 sqlite,先安装 sqlite

    4. 然后生成内存快照:rdb -c memory dump.rdb > memory.csv,生成 CSV 格式的内存报告,这一步可能会比较久,我处理的 rdb 文件 2 个多 g,跑了有一二十分钟,生成的 CSV 文件有 1个g的大小。包含的列有:数据库 ID,数据类型,key,内存使用量(byte),编码。内存使用量包含 key、value 和其他值。

    5. 导入 memory.csvsqlite 数据库,数量量比较大的话,需要等一会儿。

      $ sqlite3 memory.db
      SQLite version 3.32.3 2020-06-18 14:16:19
      Enter ".help" for usage hints.
      sqlite> create table memory(database int,type varchar(128),key varchar(128),size_in_bytes int,encoding varchar(128),num_elements int,len_largest_element varchar(128));
      sqlite> .mode csv memory
      sqlite> .import memory.csv memory
      
    6. 查询内容占用最高的几个 key:

      sqlite> select key,size_in_bytes from memory order by size_in_bytes desc limit 11;
      key,size_in_bytes
      RESOURCE_LAST_TS,895383788
      AIOT_DEVICE_INFO,228425612
      DEVICE_STATUS_LAST_TS_ONLINE,47604980
      DEVICE_STATUS_LAST_TS_BIND,44006972
      RETAIL:TRADE:TRADE-ORDER-TEMP,22917444
      RETAIL:TRADE:TRADE-ORDER-MAPPER,3396012
      retail_biz_config_data:ticket_problem,750140
      areaTree,655416
      retail_biz_config_data:provider_product,486796
      

      经过内存分析,内存占用率排前十的key:

      • RESOURCE_LAST_TS 占用 853.9 MB

      • AIOT_DEVICE_INFO 占用 217.84 MB

      • DEVICE_STATUS_LAST_TS_ONLINE 占用 45.3996 MB

      • DEVICE_STATUS_LAST_TS_BIND 占用 41.9683 MB

      • RETAIL:TRADE:TRADE-ORDER-TEMP 占用 21.8558 MB

      • RETAIL:TRADE:TRADE-ORDER-MAPPER 占用 3.2387 MB

      • retail_biz_config_data:ticket_problem 占用 0.715389 MB

      • areaTree 占用 0.625053 MB

      • retail_biz_config_data:provider_product 占用 0.464245 MB

      • retail_biz_config_data:config_fault 占用 0.298893 MB

      找到了内存占用率比较高的 key 后,就可以去针对此 key 进行下一步的优化。

    一些总结

    1. 缩短键值对的存储长度:键值对的长度是和性能成反比的,因此在保证完整语义的同时,我们要尽量的缩短键值对的存储长度,必要时要对数据进行序列化和压缩再存储。
    2. 设置键值的过期时间:我们应该根据实际的业务情况,对键值设置合理的过期时间,这样 Redis 会帮你自动清除过期的键值对,以节约对内存的占用,以避免键值过多的堆积,频繁的触发内存淘汰策略。
    3. 禁用长耗时的查询命令:Redis 绝大多数读写命令的时间复杂度都在 O(1) 到 O(N) 之间,其中 O(1) 表示可以安全使用的,而 O(N) 就应该当心了,N 表示不确定,数据越大查询的速度可能会越慢。因为 Redis 只用一个线程来做数据查询,如果这些指令耗时很长,就会阻塞 Redis,造成大量延时。要避免 O(N) 命令对 Redis 造成的影响,可以从以下几个方面入手改造:
      • 决定禁止使用 keys 命令;

      • 避免一次查询所有的成员,要使用 scan 命令进行分批的,游标式的遍历;

      • 通过机制严格控制 Hash、Set、Sorted Set 等结构的数据大小;

      • 将排序、并集、交集等操作放在客户端执行,以减少 Redis 服务器运行压力;

      • 删除 (del) 一个大数据的时候,可能会需要很长时间,所以建议用异步删除的方式 unlink,它会启动一个新的线程来删除目标数据,而不阻塞 Redis 的主线程。

    参考资料

    ]]>
    2021-01-19T02:49:56+00:00
    https://github.com/superleeyom/blog/issues/16主流分布式id方案总结2024-04-24T05:23:59.300625+00:00在简单系统中,我们常常使用 db 的 id 自增方式来标识和保存数据,随着系统的复杂,数据的增多,分库分表成为了常见的方案,db 自增已无法满足要求。这时候全局唯一的 id 生成系统就派上了用场。当然这只是 id 生成其中的一种应用场景。

    1. 全局唯一的 id:无论怎样都不能重复,这是最基本的要求了
    2. 高性能:基础服务尽可能耗时少,如果能够本地生成最好
    3. 高可用:虽说很难实现 100% 的可用性,但是也要无限接近于 100% 的可用性
    4. 简单易用:能够拿来即用,接入方便,同时在系统设计和实现上要尽可能的简单

    现在就目前主流的分布式 id 生成方案做一个总结。

    UUID

    这个 jdk 自带的工具类就能实现:

     UUID.randomUUID().toString()
    

    MySQL 官方有明确的建议主键要尽量越短越好,36 个字符长度的 UUID 不符合要求,如果作为数据库主键,在 InnoDB 引擎下,UUID 的无序性可能会引起数据位置频繁变动,严重影响性能。

    数据库自增

    利用 MySQL 数据库的自增主键,来作为唯一的全局 id,专门用一张表来生成主键,表结构如下:

    CREATE TABLE `distributed_id` (
      `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
      `extra` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '额外字段',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci COMMENT='主键id生成';
    

    extra是额外字段,因为你要总要插入点什么,才能生成一个自增主键 id,这种方式存在问题是,每次生成一个id,都要请求MySQL,如果 MySQL 挂掉了,那么也就无法生成唯一 id 了,且 ID 发号性能瓶颈限制在单台 MySQL 的读写性能。

    数据库多主模式

    既然一台 MySQL 有风险,那么可以将 MySQL 扩展为两台,采用双主模式集群,设置每台 MySQL 的步长和起始值,其中一台 MySQL 的起始值为 1,步长为 2,一台的起始值为 2,步长为 2,配置如下:

    # mysql-1,这里需要注意,执行如下命令前,最好先删除「主键id生成表」,否则可能不生效
    # 最好连接 mysql 命令行界面执行,Navicat 等工具有时候会不生效
    set global auto_increment_offset = 1;
    set global auto_increment_increment = 2;
    
    # mysql-2
    set global auto_increment_offset = 2;
    set global auto_increment_increment = 2;
    
    # 查询是否修改成功
    SHOW global VARIABLES LIKE 'auto_inc%'; 
    

    通过如上的配置后,MySQL-1 生成:1、3、5、7 ... 系列主键 id,MySQL-2 生成 2、4、6、8 ... 系列的主键 id,即使其中一台 MySQL 挂了,另外一台 MySQL 还可以继续生成 id,然后专门搭建一个后台服务 distributedd-service 服务,暴露 http 接口,专门用于其他服务生成分布式 id。这里我写了一个简单示例:distributedd-service ,可以参考一下;

    但是这种还是有很大局限性,系统水平扩展比较困难,比如定义好了步长和机器台数之后,如果要添加机器该怎么做?首先,要调整新增加 MySQL 的起始值足够大,其次,之前两台的 MySQL 的步长肯定得修改,而且在修改步长的同时,很有可能会产生重复的 id,并且这种方式强依赖数据库,每次生成一个 id 都要请求数据库,数据库压力还是很大,每次获取 ID 都得读写一次数据库,只能靠堆机器来提高性能。

    号段模式

    那能不能批量获取呢?那就是号段模式,号段模式的意思就是,每次从数据库只获取一个号段,比如 (1,1000],然后分布式 id 服务 distributedd-service 在本地加载到内存中,然后采用自增的方式来生成 id,不需要每次都请求数据库。等这个号段使用完了,再去数据库申请一个新的号段。这种方式的好处就是解除了对数据库的强依赖,即使数据库挂了,分布式 id 服务 server 在本地还能支撑一段时间,另外为了提高分布式 id 服务的可用性,发号器服务也可以部署成集群,防止单点故障,但是也有个小缺陷的地方,就是假如分布式 id 服务重启了,就可能会丢失一部分的 id 号段。

    那关于号段模式的实践,目前已经有开源的方案,就是滴滴的tinyid

    滴滴TinyId

    DB 号段算法的数据库设计如下:

    CREATE TABLE `tiny_id_info` (
      `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
      `biz_type` varchar(64) NOT NULL COMMENT '业务类型,不同的业务id隔离',
      `max_id` int(11) NOT NULL COMMENT '当前最大可用id',
      `step` int(11) NOT NULL COMMENT '号段的长度,也就是步长',
      `version` int(11) NOT NULL COMMENT '乐观锁,每次更新都加上version,保证并发更新的准确性',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='db号段表';
    

    那么我们可以通过如下几个步骤来获取一个可用的号段:

    • A. 查询当前的 max_id 信息:select id, biz_type, max_id, step, version from tiny_id_info where biz_type='test';
    • B. 计算新的 max_id: new_max_id = max_id + step
    • C. 更新 DB 中的 max_id:update tiny_id_info set max_id=#{new_max_id} , verison=version+1 where id=#{id} and max_id=#{max_id} and version=#{version}
    • D. 如果更新成功,则可用号段获取成功,新的可用号段为(max_id, new_max_id]
    • E. 如果更新失败,则号段可能被其他线程获取,回到步骤 A,进行重试

    分布式 id 发号器服务 distributedd-service 对外提供 http 服务,用于生成 id,搭建发号器服务 server 集群,每个节点都相当于一个 id 号段,请求经过负载均衡,落到某个节点上,从事先加载好的号段中获取一个 id,如果号段还没有加载,或者已经用完,则向 db 再申请一个新的可用号段,读写数据库的频率从 1 减小到了 1/step ,多台 server 之间因为号段生成算法的原子性,而保证每台 server 上的可用号段不重,从而使 id 生成不重。其实这种在并发量不高的情况下,其实基本上已经满足大部分使用场景,但是如果并发量比较高的话,还是会存在一些问题:

    1. 使用 http 方式获取一个 id,存在网络开销,性能和可用性都不太好
    2. db 是一个单点,可能会有单点故障问题
    3. 当 id 用完时需要访问 db 加载新的号段,db 更新存在乐观锁,此时 id 生成耗时明显增加

    TinyId 给出了如下的优化方案:

    1. 双号段缓存:在号段使用到一定的程度,起一个线程,异步去加载下一个号段,保证内存中始终有可用的号段,则可避免性能波动。

    2. 多db支持:既然一个 db 可能单点故障,那就部署多台 db,原理跟上面讲的数据库多主模式一样,数据库 A 只生成奇数 id,数据库 B 只生成偶数 id。id 生成服务,随机向多个 db 申请号段。这时候,原本 DB 号段算法的数据库需要进行修改下:

      CREATE TABLE `tiny_id_info` (
        `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
        `biz_type` varchar(64) NOT NULL COMMENT '业务类型,不同的业务id隔离',
        `max_id` int(11) NOT NULL COMMENT '当前最大可用id',
        `step` int(11) NOT NULL COMMENT '号段的长度,也就是步长',
        `delta` int(4) NOT NULL COMMENT 'id每次的增量,理解为id的自增步长,类似auto_increment_increment',
        `remainder` int(4) NOT NULL COMMENT '代表余数',
        `version` int(11) NOT NULL COMMENT '乐观锁,每次更新都加上version,保证并发更新的准确性',
        PRIMARY KEY (`id`)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='db号段表';
      

      通过 delta 和 remainder 两个字段我们可以根据使用方的需求灵活设计 db 个数,使 db 能水平进行扩展,比如将AB两个 db 的 delta 都设置为 2,remainder A 设置为 0,B 设置为 1,则 A 则只生成偶数号段,B 则生成奇数号段。

    3. 增加tinyid-client:既然 http 会有网络开销,那就把 id 在本地,也就是客户端生成,就是说,id 生成服务 server 不直接生产id了,而是转为生产号段,id 生成的逻辑,转到本地,通过提供一个 tinyid-client sdk 组件,引入到其他服务里面,在本地构建双号段、id生成,如此 id 生成则变成纯本地操作,性能大大提升。

    雪花算法

    雪花算法(snowflake)是 twitter 开源的分布式 ID 生成算法,雪花算法的描述:指定机器 & 同一时刻 & 某一并发序列,是唯一的,据此可生成一个 64 bits 的唯一ID(long)。

    算法产生的是一个 long 型 64 比特位的值,由于一般分布式 id 均为正数(0 是代表正数,1 代表负数),第 1 位固定为 0,接下来是 41 位的毫秒单位的时间戳,我们可以计算下:

    2^41/1000*60*60*24*365 = 69年
    

    也就是这个时间戳可以使用 69 年不重复,这个对于大部分系统够用了。10 位的数据机器位,所以可以部署在 1024 个节点。12 位的序列,在毫秒的时间戳内计数。 支持每个节点每毫秒产生 4096 个 ID 序号,所以最大可以支持单节点差不多四百万的并发量。

    雪花算法要保证 id 不重复,最重要的就是要保证 workerid 不重复,也就是机器码不能重复。如果要部署的服务节点不多,直接可以通过 jvm 的启动参数方式传过来,应用启动的时候获取启动参数(workId 终端 ID 和 datacenterId 数据中心 ID),保证每个节点启动的时候传入不同的启动参数即可。

    java -jar xxx.jar -DworkerId=1 -DdatacenterId=123
    

    这里我用 Java 工具类库 Hutool 封装的工具类 IdUtil 生成唯一 id,伪代码如下:

    long workerid = System.getProperty("workerId ");
    long datacenterId = System.getProperty("datacenterId ");
    Snowflake snowflake = IdUtil.getSnowflake(workerid, datacenterId);
    long distributedId = snowflake.nextId();
    

    如果机器特别多,人工去为每台机器去指定一个机器 id,人力成本太大且容易出错,并且雪花算法,强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。所以一些大厂对 snowflake 进行了改造,比如百度的 uid-generator,美团的 Leaf 等开源方案。

    美团 Leaf

    目前美团 Leaf 有两种方案,一种也是基于号段模式,叫 Leaf-segment 数据库方案,他的原理其实跟滴滴的 tinyid 类似的,这里就不再复述了。另外一种是基于雪花算法的,叫 Leaf-snowflake 方案,Leaf-snowflake 方案完全沿用 snowflake 方案的 bit 位设计,即是 “1+41+10+12” 的方式组装 ID 号。Leaf-snowflake 方案使用 Zookeeper 持久顺序节点的特性自动对 snowflake 节点配置 workId

    1. 启动 Leaf-snowflake 服务,连接 Zookeeper,在 leaf_forever 父节点下检查自己是否已经注册过(是否有该顺序子节点)。
    2. 如果有注册过直接取回自己的 workerId( zk 顺序节点生成的 int 类型 ID 号),启动服务。
    3. 如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的 workerId 号,启动服务。

    除了每次会去 zk 拿数据以外,也会在本机文件系统上缓存一个 workerId 文件。当 ZooKeeper 出现问题,恰好机器出现问题需要重启时,能保证服务能够正常启动。

    另外关于时钟回拨问题,Leaf-snowflake 方案在服务启动阶段,就会去检测机器时间是否发生了大步长的回拨:

    更细节的问题,可以参考美团技术团队写的:《Leaf——美团点评分布式ID生成系统》

    百度 uid-generator

    百度的 UidGenerator 以组件(sdk)形式工作在应用项目中, 支持自定义 workerId 位数和初始化策略, 从而适用于 docker 等虚拟化环境下实例自动重启、漂移等场景。 在实现上, UidGenerator 通过借用未来时间来解决 sequence 天然存在的并发限制; 采用RingBuffer 来缓存已生成的 UID, 并行化 UID 的生产和消费, 同时对 CacheLine 补齐,避免了由 RingBuffer 带来的硬件级「伪共享」问题,最终单机 QPS 可达 600 万。

    相比于标准的雪花算法比特位的分布,百度的 UidGenerator 稍微有些不一样:

    • sign(1bit):固定 1bit 符号标识,即生成的 UID 为正数。
    • delta seconds (28 bits):当前时间,单位:秒。
    • worker id (22 bits):机器id,最多可支持约 420w 次机器启动,内置实现为在启动时由数据库分配,默认分配策略为用后即弃。
    • sequence (13 bits):每秒下的并发序列,13 bits 可支持每秒 8192 个并发。

    百度的 UidGenerator 默认提供的 workid 生成策略:应用在启动时会往数据库表 WORKER_NODE 表中去插入一条数据,数据插入成功后返回的该数据对应的自增唯一 id 就是该机器的 workId。

    DROP TABLE IF EXISTS WORKER_NODE;
    CREATE TABLE WORKER_NODE
    (
    ID BIGINT NOT NULL AUTO_INCREMENT COMMENT 'auto increment id',
    HOST_NAME VARCHAR(64) NOT NULL COMMENT 'host name',
    PORT VARCHAR(64) NOT NULL COMMENT 'port',
    TYPE INT NOT NULL COMMENT 'node type: ACTUAL or CONTAINER',
    LAUNCH_DATE DATE NOT NULL COMMENT 'launch date',
    MODIFIED TIMESTAMP NOT NULL COMMENT 'modified time',
    CREATED TIMESTAMP NOT NULL COMMENT 'created time',
    PRIMARY KEY(ID)
    )
     COMMENT='DB WorkerID Assigner for UID Generator',ENGINE = INNODB;
    

    但是百度的 uid-generator 项目已经基本上不维护了,但是思想还是挺棒的,如果可以的话,自己可以按照这个思路改造。

    其他方案

    除了上面主流的方案,还有一些其他的方案:

    • 使用 Redis 的自增原理,利用 Redis 中的 incr 命令来实现原子性的自增与返回,使用 setnx 保证并发更新的准确性。
    • MongoDB 的 ObjectId,其实它本质还是雪花算法。
    • 利用 zookeeper 的持久顺序节点特性。

    总结

    就目前这些方案来说,我个人更比较推荐:滴滴TinyId 和 美团 Leaf 两种开源方案,这两种方案已经经过了成熟的商业实践,适用于大部分的公司业务,我们自己公司目前使用的就是美团Leaf-segment数据库方案。

    参考文档

    ]]>
    2021-01-10T08:15:02+00:00
    https://github.com/superleeyom/blog/issues/15常用linux进程查询命令2024-04-24T05:23:59.402579+00:00ps 命令详解

    ps命令详解

    根据进程名查询进程信息

    ps  -ef | grep {processName}
    

    根据进程pid查询进程信息

    ps  -ef | grep {pid}
    

    根据端口查看对应进程信息

    Linux

    netstat -tunlp | grep {port}
    

    示例:

    # netstat -tunlp | grep 8080
    tcp6       0      0 :::8080                 :::*                    LISTEN      29150/java
    

    则 29150 为当前端口所对应的进程 pid

    MacOS

    lsof -i tcp:{port}
    

    查看进程pid占用端口情况

    Linux

    netstat -nap | grep {pid}
    

    MacOS

    lsof -p {pid}|grep LISTEN
    

    查询僵尸进程

    ps -A -ostat,ppid,pid,cmd | grep -e '^[Zz]'
    
    • -A 参数列出所有进程
    • -o 自定义输出字段 我们设定显示字段为 stat(状态), ppid(进程父id), pid(进程id),cmd(命令)这四个参数
    • 因为状态为 z 或者 Z 的进程为僵尸进程,所以我们使用 grep 抓取 stat 状态为 z 或者 Z 进程

    查看最消耗CPU和内存的进程

    # 查看最消耗CPU的进程
    ps -eo pid,ppid,%mem,%cpu,cmd --sort=-%cpu | head
    # 查看最消耗内存的进程
    ps -eo pid,ppid,%mem,%cpu,cmd --sort=-%mem | head
    
    ]]>
    2021-01-03T02:07:59+00:00
    https://github.com/superleeyom/blog/issues/14[笔记]失明症漫记2024-04-24T05:23:59.479668+00:00最近看完了诺贝尔文学奖获得者葡萄牙作家萨拉马戈的《失明症漫记》,比较有意思的是,整书没有感叹号,引号等标题符号,就只有逗号,和句号。所以刚开始看的时候,还有点不太适应,有点怎么说呢,叫断片?后面慢慢习惯后,才发现,可能作者的意图是想让读者更能站在角色得角度审视全书。

    这本书挺契合当前形势的,跟今年的武汉肺炎差不多,讲的是全世界的人都得了一种失明症,整个社会文明秩序崩塌的事情。

    整书让我比较印象深刻,当看到被关在精神病院的女人们,为了食物,答应那些盲人歹徒们的肮脏行为,心里非常气愤,在想女人们和他们的男人们为什么不奋起反抗,但是想想,在整个社会秩序文明崩塌的时候,人的本能,活下去才是唯一目的吧!在看到盲人中唯一能看的见的女人,医生的妻子,站起来反抗,拿着剪刀杀了盲人歹徒首领,真是看的让人拍手称快。书中非常让人心疼了就是医生的妻子了,全世界唯一一个能看得见的人,带着一群盲人,从恐怖的精神病院逃出来,在看到那种炼狱般的世界后,依然坚强的活着,给大家带来光明的希望。

    全书的最后那句:“我想我们没有失明,我想我们现在是盲人;能看得见的盲人;能看但又看不见的盲人。”,何尝不是啊?现如今,虽然我们生理上能看得见,但是灵魂和心里我们很多人都是盲人。如果有一天我们真的失明了,你有活下去的勇气吗?以下便是看书过程中一些比较印象深刻的句子:

    • 如果在实施任何行为之前我们都能预想到它的一切后果并认真加以考虑,先是眼前的后果,然后是可证明的后果,接着是可能的后果,进而是可以想象到的后果,那么我们根本就不会去做了,即使开始做了,思想也能立即让我们停下来。我们一切言行的好和坏的结果将分布在,假设以一种整饬均衡的形式,未来的每一天当中,包括那些因为我们已不在人世而无从证实也无法表示祝贺或请求原谅的永无止境的日子。有人会说,这就是人们常说的不朽。

    • 面对死神,我们最希望看到仇恨能失去力量和毒性。

    • 世界就是这样,真相往往以谎言为伪装达到其目的。

    • 到了把语言化为行动的时候,原来那么坚定的勇气开始消退,面对刺激鼻孔和眼睛的恶劣现实她开始崩溃。

    • 即便在最坏的不幸之中,也能找到足够的善让人耐心地承受此种不幸。

    • 甚至说法律从诞生那一天起就对所有人同等对待,而民主与特权水火不容。

    • 在我们被迫生活的这个地狱里,在我们自己打造的这个地狱中的地狱里,如果说廉耻二字还有一点意义的话,应当感谢那个有胆量进入鬣狗的巢穴杀死鬣狗的人

    • 穿袈裟的不一定是和尚,执权杖的不一定是国王,最好不要忘记这条真理。

    • 当焦急折磨着我们的时候,当肉体由于疼痛和痛苦不肯听从我们指挥的时候,就能看到我们自己渺小的兽性了

    • 正义的报复是人道主义的举动,如果受害者没有向残忍的家伙报复的权利,那就没有正义可言了

    • 没有答案,答案在最需要的时候总是不肯出现,而很多时候唯一可能的答案却是,你必须耐心等待。

    • 他们都成了没有性别的轮廓,成了边缘模糊的污渍,成了隐没在黑暗中的阴影。

    • 医生的妻子把手搭在作家的肩上,作家伸出两只手,摸到她的手,慢慢拉到自己唇边,您不要迷失,千万不要迷失,他说,这句话出人意料,寓意难明,好像是不经意说出来的。

    • 她会再生吗,戴墨镜的姑娘问;她不会,医生的妻子回答说,但活着的人们需要再生,从本身再生,而他们不肯;我们已经半死了,医生说;我们还半活着,妻子回答说。

    • 我想我们没有失明,我想我们现在是盲人;能看得见的盲人;能看但又看不见的盲人。

    ]]>
    2020-12-31T00:14:27+00:00
    https://github.com/superleeyom/blog/issues/13关于Redis缓存穿透、缓存雪崩、缓存击穿问题探究2024-04-24T05:23:59.562409+00:00缓存穿透

    拿一个不存在的 key 去查询数据,如果缓存里面查询不到,就会去数据库里面查询,如果有人恶意拿不存在的 key 疯狂请求,会把数据库压垮,这就是缓存穿透,下面用一段伪代码:

    List<String> cacheList = redis.get(key);
    if(CollUtil.isEmpty(cacheList)){
    	List<String> list = mysql.getList(key);
    	if(CollUtil.isNotEmpty(list)){
    		redis.set(key,list,3 * 60);
    	}
      return list;
    }
    return cacheList;
    

    通常来说,解决缓存穿透有两种方式:

    • 为不存在的 key 设置空值

      • 伪代码如下:

        List<String> cacheList = redis.get(key);
        if(CollUtil.isEmpty(cacheList)){
        	// 不管有没有在数据库中查询到数据,都给key设置值   
        	List<String> list = mysql.getList(key);
        	redis.set(key,list,3 * 60);
          return list;
        }
        return cacheList;
        
    • 使用布隆过滤器

    缓存雪崩

    在某个时间点,大批的 key 出现过期,导致所有的请求全部打到数据库上,把数据库压垮,这种就是缓存雪崩,通常解决缓存雪崩有如下的几种方案:

    • 永不过期:设置 key 永不过期,但是这种会占用服务器挺多内存;
    • 过期时间错开:比如这个 key 设置的过期时间是 5 分钟,那另外一个 key 设置的过期时间则为 7 分钟,把过期时间错开,防止在某个时间点同时失效
    • 多缓存结合:在数据库和 Redis 再加一层缓存,比如 Memcache,这样的话,缓存一旦过期,Memcache 里面还可以顶一会儿;
    • 采购第三方的 Redis 服务:现在很多云平台都有推出 Redis 服务,有单机的,集群的,可以根据自己的使用场景去采购,当然人家也帮你处理好了这些问题,有钱啥问题都能解决。

    缓存击穿

    当前的某个热点 key 缓存过期,同一时间,有大量的请求同时来访问这个 key,导致所有的请求都打到数据库上去了,把数据库压垮。那通常遇到这种问题的话,一般就是使用排斥锁,当然也有一种粗暴的办法,就是设置永不过期,但是这种粗暴方式,大多数情况下不适用。

    关于排斥锁,可以这样理解,第一个请求达到请求 key 发现缓存里面没有,允许它去数据库查询,同时加锁,这样第二个请求,第三个请求…都会被锁阻塞到当前,当第一个请求从数据库查询到数据后,将数据缓存到 Redis 中,然后释放锁,这样第二个,第三个请求...,就直接可以从缓存中拿数据,就不会再打到数据库,这样就减少了数据库的并发压力。

    String get(String key) {  
       String value = redis.get(key);  
       if (value  == null) {  
        if (redis.setnx(key_mutex, "1")) {  
            // 给锁设置一个过期时间,防止持有锁的人挂了,导致锁不能释放
            redis.expire(key_mutex, 3 * 60)  
            // 从DB中查询数据并缓存
            value = db.get(key);  
            redis.set(key, value);
          	// 释放锁
            redis.delete(key_mutex);
          	return value;
        } else {  
            //其他线程休息100毫秒后重试  
            Thread.sleep(100);  
            get(key);  
        }  
      }
      return value;
    }
    

    其实对于这些热点 key,最好还是有个独立的服务,去定时的刷新缓存,这样的话,很大的程度上可以避免这种问题。

    ]]>
    2020-12-23T11:45:12+00:00
    https://github.com/superleeyom/blog/issues/125分钟快速理解Redis的内存回收机制2024-04-24T05:23:59.635641+00:00设置键的生存时间
    • EXPIRE key seconds:用于设置秒级精度的生存时间,它可以让键在指定的秒数之后自动被移除
    • PEXPIRE key milliseconds:用于设置毫秒级精度的生存时间,它可以让键在指定的毫秒数之后自动被移除
    • EXPIREAT key timestamp:将键 key 的过期时间设置为 timestamp 所指定的秒数时间戳
    • PEXPIREAT key timestamp:将键 key 的过期时间设置为 timestamp 所指定的毫秒数时间戳

    虽然有多种不同单位和不同形式的设置命令,但实际上EXPIREPEXPIREEXPIREAT 三个命令都是使用PEXPIREAT命令来实现的:无论客户端执行的是以上四个命令中的哪一个,经过转换之后,最终的执行效果都和执行PEXPIREAT命令一样。

    在使用键过期功能时,组合使用 SET 命令和 EXPIRE/PEXIRE 命令的做法非常常见:

    • SET key value [EX seconds] [PX milliseconds]:在设置 key 的时候,同时设置过期时间,此命令等价于两条命令:

      SET key value
      EXPIRE key seconds
      

    使用带有 EX 选项或 PX 选项的 SET 命令除了可以减少命令的调用数量并提升程序的执行速度之外,更重要的是保证了操作的原子性,使得「为键设置值」和「为键设置生存时间」这两个操作可以一起执行。如果拆分成 SETEXPIRE 两条命令执行的话,如果 Redis 服务器在成功执行 SET 命令之后因为故障下线,导致 EXPIRE 命令没有被执行,那么 SET 命令设置的缓存就会一直存在,而不会因为过期而自动被移除。

    EXPIREAT/PEXPIREAT,还是 EXPIRE/PEXIRE,它们都只能对整个键进行设置,而无法对键中的某个元素进行设置,比如,用户只能对整个集合或者整个散列设置生存时间/过期时间,但是却无法为集合中的某个元素或者散列中的某个字段单独设置生存时间/过期时间,这也是目前 Redis 的自动过期功能的一个缺陷。

    移除过期时间

    能设置过期时间,自然也就能移除过期时间,PERSIST命令就是PEXPIREAT命令的反操作:PERSIST key命令在过期字典中查找给定的键,并解除键和值(过期时间)在过期字典中的关联。

    计算并返回剩余时间

    TTL 命令以秒为单位返回键的剩余生存时间,而PTTL命令则以毫秒为单位返回键的剩余生存时间:

    TTL key
    PTTL key
    

    Redis如何保存过期时间

    Redis 里面有一个过期字典 expires,专门存储 Redis key 的过期时间,一个键的 key,有两个指向,一个指向实际的 value,一个指向它的的过期时间,如下图所示

    判定一个键是否过期,主要分为两步:

    1. 检查给定键是否存在于过期字典:如果存在,那么取得键的过期时间。
    2. 检查当前 UNIX 时间戳是否大于键的过期时间:如果是的话,那么键已经过期;否则的话,键未过期。

    过期键删除策略

    • 定时删除:在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作。
      • 定时删除占用太多 CPU 时间,影响服务器的响应时间和吞吐量。因为在过期键比较多的情况下,删除过期键这一行为可能会占用相当一部分 CPU 时间。
    • 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
      • 惰性删除浪费太多内存,有内存泄漏的危险。因为当过期键一直没有访问将无法得到及时删除,从而导致内存不能及时释放。
    • 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
      • 定期删除策略是前两种策略的一种整合和折中,如果采用定期删除策略的话,服务器必须根据情况,合理地设置删除操作的执行时长和执行频率,否则到头来,还是会搞得跟「定时删除」和「惰性删除」一样。

    Redis 服务器实际使用的是惰性删除定期删除两种策略,其中惰性删除策略会调用 expireIfNeeded 函数对键进行检查:

    • 如果输入键已经过期,那么 expireIfNeeded 函数将输入键从数据库中删除
    • 如果输入键未过期,那么 expireIfNeeded 函数不做动作

    示意图如下:

    而 Redis 的定期删除策略,activeExpireCycle 函数就会被调用,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的 expires 过期字典中随机检查一部分键的过期时间,并删除其中的过期键,如下图所示:

    Redis内存回收机制

    Redis 的内存回收机制主要体现在以下两个方面:

    • 删除到达过期时间的键对象,就是上面说的过期键删除策略。

    • 内存使用达到maxmemory上限时触发内存溢出控制策略。

    当 Redis 所用内存达到 maxmemory 上限时会触发相应的溢出控制策略。 具体策略受 maxmemory-policy 参数控制,Redis 支持 6 种策略:

    • noeviction:默认策略,不会删除任何数据,拒绝所有写入操作并返 回客户端错误信息(error)OOM command not allowed when used memory,此时 Redis 只响应读操作。
    • volatile-lru:根据 LRU 算法删除设置了超时属性(expire)的键,直到腾出足够空间为止。如果没有可删除的键对象,回退到 noeviction 策略。
    • allkeys-lru:根据 LRU 算法删除键,不管数据有没有设置超时属性, 直到腾出足够空间为止。
    • allkeys-random:随机删除所有键,直到腾出足够空间为止。
    • volatile-random:随机删除过期键,直到腾出足够空间为止。
    • volatile-ttl:根据键值对象的 ttl 属性,删除最近将要过期数据。如果没有,回退到 noeviction 策略。

    内存溢出控制策略可以采用如下命令动态配置:

    config set maxmemory-policy {policy}
    

    当 Redis 一直工作在内存溢出(used_memory>maxmemory)的状态下且设置非 noeviction 策略时,会频繁地触发回收内存的操作,影响 Redis 服务器的性能。频繁执行回收内存成本很高,主要包括查找可回收键和删除键的开销,如果当前 Redis 有从节点,回收内存操作对应的删除命令会同步到从节点,导致写放大的问题。

    资料

    • 《Redis开发与运维》
    • 《Redis使用手册》
    • 《Redis设计与实现》
    ]]>
    2020-12-16T13:49:56+00:00
    https://github.com/superleeyom/blog/issues/11同一浏览器不同用户登录冲突问题探究2024-04-24T05:23:59.720422+00:00问题

    由于业务扩展问题,目前公司有 a 和 b 两个账号中心服务,分别对应的是运营端和服务商端,这两个账号系统的访问域名分别是a.aqara.cnb.aqara.cn,其中 b 账号中心由其他的团队负责开发,用户登录成功后,会返回用户的信息(userInfo)和访问令牌(token),前端会将他们缓存在客户端的 Cookies里面,由于共用同一个二级域名(.aqara.cn),前端Cookies 里面缓存的数据是共用的。

    就会存在这种问题:同一个浏览器,用户在标签 A 登录A用户,然后又重新打开标签页 B,登录用户 B,这样就会导致,第二个用户会把第一个用户的信息覆盖掉,但是此时用户无感知,Cookies 里面存储的令牌和用户信息就会被覆盖掉。这样的话假如请求的数据(比如查看个人信息)是基于 token 拿用户信息的话,由于后台的网关层,有把 token 作为键,用户信息作为 value,缓存用户用户信息,有时候就会导致 A 用户拿到 B 用户的数据。如果恰好 a 用户和 b 用户都有访问某接口的权限,就会造成,怎么我操作后,显示的操作人确实另外一个人的名字。

    解决方案

    • 简单粗暴

      • 修改 a 和 b 两个账号中心服务的访问域名,访问域名分别是a.aqara.cnb.aqara.com,由于是不同的二级域名,这样前端的 Cookies就是隔离开来的,相关之间不会有任何的影响。 这种得确认更换域名后,会不会影响其他的业务。
    • 服务端

      • 在用户登录的时候,返回用户的数据,如果不想被覆盖,只能换成不一样的,可以设置为用户名 (username)+sessionkey使每一个用户的 sessionkey 都不一样,但是由于 b 账号中心的登录接口不是我们掌控的,此方案不太好实施。

      • 用户登陆后分配一个临时标识(sid),所有的请求和响应均携带此标识,后台用来区分用户,实际就是将判断上移到应用层面。一边这个标识生成后会放到 redis,设置一定的有效时间。伪代码如下:

        String sid = request.getReuqestParam("sid");
        String token = request.getReuqestParam("token");
        String userId = redis.get("sid");
        if(StrUtil.isBlank(userId)){
        	throw new BizException("当前用户不存在");
        }
        String userIdFromToen = JwtUtil.parseToken("token");
        if(!userIdFromToen.equal(userId)){
        	throw new BizException("当前用户已被替换");
        }
        
      • 用户在登出后,把服务端要及时的缓存的用户信息给清除掉。

    • 前端:

      • 登陆成功后将 sid 后存储到本地,在每个需要验证的页面加上这个参数,当用户刷新页面时与本地存储的值进行比较,不符合就跳转登陆页(或弹出提示框,提醒当前用户已更换用户,是否继续执行此操作,用户刷新页面后,就会刷新当前用户的菜单权限,用户信息等等)。

      • 后登陆的用户会覆盖上一个用户的本地值,而 url 里的参数不会变所以会导致 URL 中获取的值和本地不一致,目前 qq 邮箱采用也是这种方式。

      • 前端伪代码:

        router.beforeEach((to, from, next) => {
            // 工具方法,获取url地址中sid的值
            let urlSid = getPop('sid');
            // 获取localStorege中sid的值
            let localSid = getLocalStorage('sid').sid
            console.log('url=%s,local=%s ', urlSid, localSid)
            // 先判断两个参数是否存在
            if (urlSid && localSid) {
              if (urlSid == localSid) {
                next()
              } else {
                let url = window.location.href.replace(/[\?,\#]\S*/g, '');
                window.location.href = url;
                next({
                  path: "/"
                });
              }
            } 
        })
        

    参考

    ]]>
    2020-12-13T03:36:15+00:00
    https://github.com/superleeyom/blog/issues/10GitHub Actions 实战之Chrome书签同步2024-04-24T05:23:59.797035+00:00之前对 GitHub Actions 不是特别熟悉,以为它适合于跑类似于脚本语言 Python,不太适合与 Java 这类需要借助于 JVM 的语言,恰好最近有一个简单的想法就是想把 Chrome 书签同步到 Github,并将书签生成 README.md 文件,就尝试下用 GitHub Actions 去构建 Java,实际验证了其实是可行的,GitHub Actions 完全可以跑 Java做一些自动化操作。

    什么是 GitHub Actions

    官网的定义就是:

    在 GitHub Actions 的仓库中自动化、自定义和执行软件开发工作流程。 您可以发现、创建和共享操作以执行您喜欢的任何作业(包括 CI/CD),并将操作合并到完全自定义的工作流程中。

    做 Java 的其实都知道 Jenkins,其实就是和 Jenkins差不多,用于自动化构建的,只不过 GitHub Actions基于 Github 平台。

    你只要在你的仓库下,创建.github/workflow目录,并在此目录下创建*.yml的文件,就可以开启 GitHub Actionsyml 文件主要用于配置自动化构建,这里我就拿我的这次实践的chrome_bookmarks_sync.yml示例:

    # 此 action 的名字
    name: ChromeBookmarksSyncApplication
    
    on:
      # 开启手动执行
      workflow_dispatch:
      # 触发条件,当有代码push到master分支的时候,就触发一次构建
      push:
        branches: [ master ]
      # 触发条件,当有pr发起的时候,就触发一次构建  
      pull_request:
        branches: [ master ]
    
    # 自定义的环境变量,实际需要换成你自己的
    env:
      GITHUB_NAME: superleeyom
      GITHUB_EMAIL: 635709492@qq.com
    
    # 任务
    jobs:
      build:
        # 设置系统环境
        runs-on: ubuntu-latest
        steps:
        # 检出代码
        - uses: actions/checkout@v2
        # 设置jdk版本号
        - name: Set up JDK 1.8
          uses: actions/setup-java@v1
          with:
            java-version: 1.8
    
        # 执行maven命令,进行编译,并执行脚本,生成 README.md
        - name: execute application
          run: mvn -B clean compile exec:java --file pom.xml
    
        # 提交代码
        - name: update README.md
          uses: github-actions-x/commit@v2.6
          with:
            github-token: ${{ secrets.G_TOKEN }}
            commit-message: ":memo: update README.md"
            files: README.md
            rebase: 'true'
            name: ${{ env.GITHUB_NAME }}
            email: ${{ env.GITHUB_EMAIL }}
    

    更多的 GitHub Actions用例,可以参考官方的文档

    实现思路

    其实思路很简单,首先使用 Chrome 插件「书签同步」,将书签信息(bookmark.json)上传到 Github 仓库,然后通过 github action 去读取书签数据,然后生成 README.md 文件。

    没法科学上传的前提下,可以通过CrxDL.COM去下载该插件,关键字搜索「书签同步」进行下载安装,设置流程的话,参考插件使用指南:

    • 登录Github,在 Settings->Personal access tokens->Generate new token 生成一个访问 token

    • 生成的 token 需要勾选 repo 权限,保存生成的 token

    • 点击插件 icon,依次输入用户名、凭据、仓库名、文件存放路径(在仓库提前创建好*.json文件)

    • 如果需要记住用户数据,需要打开 Remember Me 开关

    • 填写完用户数据后,便可以进行「上传」或「下载」操作

    自动化构建

    由于项目是用 Maven 构建的,所以我当时的想法是通过用 mvn clean package 命令,写个单元测试方法,去触发并执行 Java 类方法,后面经过试验发现是可行的,但是觉得此方法比较 low 啊,应该是还有其他方法的,后面经过查询资料,其实 Maven 是可以通过插件 exec-maven-plugin,运行 Java main 方法:

    <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>1.2.1</version>
        <configuration>
          	<!-- 指定main方法入口 -->
            <mainClass>com.bookmark.action.ChromeBookmarksSyncApplication</mainClass>
        </configuration>
    </plugin>
    

    对应的本地测试命令:mvn clean compile exec:java,实际的 github action 的 yml 文件里的写法有点区别:mvn -B clean compile exec:java --file pom.xml,需要指定 pom 文件。另外如果你想执行 mvn 命令的时候传递命令参数到 main 方法,可以这样:mvn clean compile exec:java -Dexec.args="arg0 arg1 arg2",这样在就可以接收到自定义参数了:

    public class ChromeBookmarksSyncApplication {
        public static void main(String[] args) {
          	// 打印:[arg0 arg1 arg2]
          	System.out.println("打印接收到的参数:"+JSONUtil.toJsonStr(args));
            GenerateReadmeUtil.generateReadme();
            System.exit(0);
        }
    }
    

    这样是不是我们可以在 yml 配置中自定义的参数,就可以通过 mvn 命令传递进来呢?对吧?

    文件路径问题

    关于文件读取和写入的路径问题,实际我们在本地测试的时候,对于 bookmark.jsonREADME.md应该取绝对路径,在GenerateReadmeUtil.java类中:

    private static final String BOOKMARK_JSON_PATH = "/Users/leeyom/workspace/github/chrome-bookmarks-sync/bookmark.json";
    private static final String README_PATH = "/Users/leeyom/workspace/github/chrome-bookmarks-sync/README.md";
    

    但是实际在 github action 中,取的是相对地址,如果取绝对地址,会报文件找不到的问题:

    private static final String BOOKMARK_JSON_PATH = "bookmark.json";
    private static final String README_PATH = "README.md";
    

    如何使用

    1. fork 仓库 chrome-bookmarks-sync仓库

    2. 修改chrome_bookmarks_sync.yml文件的环境变量:

      env:
        GITHUB_NAME: 改成你自己的github用户名
        GITHUB_EMAIL: 改成你自己的github邮箱
      

      设置 G_TOKEN常量,复制你创建的 github token,在该仓库下:Settings-->Secrets-->New repository secret,将此常量填入进去,变量名设置为G_TOKEN即可。

    3. 安装 Chrome 插件「书签同步」,依次输入用户名、凭据、仓库名、文件存放路径

    4. 填写完用户数据后,便可以进行「上传」或「下载」操作,然后借助 github action,就可以自动生成 README.md

    参考文档

    ]]>
    2020-12-07T02:12:13+00:00
    https://github.com/superleeyom/blog/issues/95分钟快速理解Redis的持久化2024-04-24T05:23:59.876918+00:00Redis 的持久化指的是把内存中存储的数据以文件形式存储到硬盘上,而服务器也可以根据这些文件在系统停机之后实施数据恢复,让服务器的数据库重新回到停机之前的状态。为了满足不同的持久化需求,Redis 提供了RDB持久化AOF持久化RDB-AOF混合持久化等多种持久化方式以供用户选择。如果用户有需要,也可以完全关闭持久化功能,让服务器处于无持久化状态

    RDB持久化

    RDB 的全称是 Redis DataBaseRDB持久化是 Redis 默认使用的持久化功能,通过创建以.rdb后缀结尾的二进制文件,该文件包含了服务器在各个数据库中存储的键值对数据等信息。Redis 提供了三种创建 RDB 文件的方法,分别是:手动执行SAVE命令、手动执行BGSAVE命令、通过配置选项自动创建等三种方式。

    那 RDB 文件的结构是咋样的呢?它由如下几部分组成:

    结构 解释
    RDB 文件标识符 文件最开头的部分为 RDB 文件标识符,这个标识符的内容为"REDIS"这5个字符。Redis 服务器在尝试载入 RDB 文件的时候,可以通过这个标识符快速地判断该文件是否为真正的 RDB 文件。
    版本号 版本号是一个字符串格式的数字,长度为4个字符。新版 Redis 服务器总是能够向下兼容旧版 Redis 服务器生成的 RDB 文件。比如,生成第9版 RDB 文件的 Redis 5.0 既能够正常读入由 Redis 4.0 生成的第8版 RDB 文件。
    设备附加信息 记录了生成 RDB 文件的 Redis 服务器及其所在平台的信息,比如服务器的版本号、宿主机器的架构、创建 RDB 文件时的时间戳、服务器占用的内存数量等。
    数据库数据 记录了 Redis 服务器存储的0个或任意多个数据库的数据,各个数据库的数据将按照数据库号码从小到大进行排列,每个数据库里面存放的是键值对数据。
    Lua 脚本缓存 如果 Redis 服务器启用了复制功能,那么服务器将在 RDB 文件的 Lua 脚本缓存部分保存所有已被缓存的 Lua 脚本。这样一来,从服务器在载入 RDB 文件完成数据同步之后,就可以继续执行主服务器发来的 EVALSHA 命令了。
    EOF 用于标识 RDB 正文内容的末尾,它的实际值为二进制值 0xFF。
    CRC64校验和 RDB文件的末尾是一个以无符号64位整数表示的 CRC64 校验和,用于校验 RDB 文件是否有出错或损坏的情况。

    Redis 服务器载入 RDB 文件的整体流程:打开 RDB 文件 --> 检查文件头 --> 检查版本号 --> 读取设备信息 --> 重建数据库 --> 重建脚本缓存 --> 对比校验和 --> 数据载入完毕。

    SAVE命令

    可以通过SAVE命令,以同步方式创建出一个记录了服务器当前所有数据库数据的 RDB 文件。SVAE命令是一个无参数命令,创建成功后,返回 OK 提示:

    redis> SAVE
    OK
    

    由于是同步方式进行创建的 RDB 文件,那在SAVE命令执行期间,Redis 服务器将阻塞,直到RDB文件创建完毕为止。如果 Redis 服务器在执行SAVE命令时已经拥有了相应的 RDB 文件,那么服务器将使用新创建的 RDB 文件代替已有的 RDB 文件。

    BGSAVE命令

    BGSAVE其实就是解决SAVE命令的阻塞问题的,它与SAVE命令的不同之处在于,BGSAVE命令是异步执行的,BGSAVE不会直接使用 Redis 服务器进程创建 RDB文件,而是使用子进程创建 RDB 文件。

    redis> BGSAVE
    Background saving started
    

    用户执行BGSAVE命令,Redis 的整个执行流程如下:

    1. 创建一个子进程
    2. 子进程执行SAVE命令,创建新的RDB文件
    3. RDB 文件创建完毕后,子进程退出,并通知Redis服务的主进程,新的 RDB 文件已创建完毕
    4. Redis 服务器进程使用新的 RDB 文件替换已有的 RDB 文件

    虽然说BGSAVE命令是异步执行的,Redis 服务器在BGSAVE命令执行期间仍然可以继续处理其他客户端发送的命令请求,不会阻塞服务器,但由于执行BGSAVE命令需要创建子进程,所以父进程占用的内存数量越大,创建子进程这一操作耗费的时间也会越长,因此 Redis 服务器在执行BGSAVE命令时,仍然可能会由于创建子进程而被短暂地阻塞。

    通过配置选项自动创建RDB文件

    除了手动执行BGSAVESAVE命令创建RDB文件外,Redis 还支持通过配置选项自动创建 RDB 文件:

    save <seconds> <changes>
    

    对于 secondschanges 参数,可以这么理解:如果服务器在 seconds 秒之内,对其包含的各个数据库总共执行了至少 changes 次修改,那么服务器将自动执行一次BGSAVE命令。

    当用户向 Redis 服务器提供多个 save 选项,只要满足其中任意一个选项,就会自动执行一次BGSAVE命令。

    save 6000 1
    save 6000 10
    save 6000 100
    

    为了避免因满足条件,而频繁触发执行BGSAVE命令,Redis 服务器在每次成功创建 RDB 文件之后,负责自动触发BGSAVE命令的时间计数器以及修改次数计数器都会被清零并重新开始计数。

    RDB 持久化的缺陷

    无论用户使用的是SAVE命令还是BGSAVE命令,停机时服务器丢失的数据量将取决于创建 RDB 文件的时间间隔:间隔越长,停机时丢失的数据也就越多。

    RDB 持久化是一种全量持久化操作,它在创建 RDB 文件时需要存储整个服务器包含的所有数据,并因此消耗大量计算资源和内存资源,所以用户是不太可能通过增大 RDB 文件的生成频率来保证数据安全的。

    从 RDB 持久化的特征来看,它更像是一种数据备份手段而非一种普通的数据持久化手段。为了解决 RDB 持久化在停机时可能会丢失大量数据这一问题,并提供一种真正符合用户预期的持久化功能,Redis 推出 AOF 持久化模式。

    AOF持久化

    与全量式的 RDB 持久化功能不同,AOF 提供的是增量式的持久化功能,这种持久化的核心原理在于:服务器每次执行完写命令之后,都会以协议文本的方式将被执行的命令追加到 AOF 文件的末尾。这样一来,服务器在停机之后,只要重新执行 AOF 文件中保存的 Redis 命令,就可以将数据库恢复至停机之前的状态,有点像 MySQL 的 binlog

    时间 事件 AOF 文件记录的命令
    T0 执行命令:SET K1 V1 SELECT 0
    SET K1 V1
    T1 执行命令:SET K2 V2 SELECT 0
    SET K1 V1
    SET K2 V3

    随着服务器不断地执行命令,被执行的命令也会不断地被保存到 AOF 文件中。其实在实际的 AOF 文件中,命令都是以 Redis 网络协议的方式保存的,比如:

    *2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n
    *3\r\n$3\r\nSET\r\n$2\r\nk1\r\n$2\r\nv1\r\n        
    *3\r\n$3\r\nSET\r\n$2\r\nk2\r\n$2\r\nv2\r\n
    

    Redis 服务器执行:

    • appendonly yes:开启 AOF 持久化功能,Redis 服务器在默认情况下将创建一个名为appendonly.aof的文件作为AOF 文件。

    • appendonly no:关闭 AOF 持久化功能。

    设置 AOF 文件冲洗频率

    为了提高程序的写入性能,现代化的操作系统通常会把针对硬盘的多次写操作优化为一次写操作。当程序调用 write 系统对文件进行写入时,系统并不会直接把数据写入硬盘,而是会先将数据写入位于内存的缓冲区中,等到指定的时限到达或者满足某些写入条件时,系统才会执行 flush 系统调用,将缓冲区中的数据冲洗至硬盘。

    Redis 向用户提供了appendfsync选项,以此来控制系统冲洗 AOF 文件的频率:

    appendfsync <value>
    

    通常有三个可选值,分别是:

    • always:每执行一个写命令,就对 AOF 文件执行一次冲洗操作。如果服务器停机,此时最多只会丢失一个命令的数据,但使用这种冲洗方式将使 Redis 服务器的性能降低至传统关系数据库的水平。
    • everysec:每隔 1s,就对 AOF 文件执行一次冲洗操作。服务器在停机时最多只会丢失 1s 之内产生的命令数据,这是一种兼顾性能和安全性的折中方案。
    • no:不主动对 AOF 文件执行冲洗操作,由操作系统决定何时对 AOF 进行冲洗。服务器在停机时将丢失系统最后一次冲洗 AOF 文件之后产生的所有命令数据,至于数据量的具体大小则取决于系统冲洗 AOF 文件的频率。

    所以 Redis 使用everysec作为appendfsync选项的默认值。除非有明确的需求,否则用户不应该随意修改appendfsync选项的值。

    AOF 重写

    随着服务器不断运行,被执行的命令将变得越来越多,而负责记录这些命令的 AOF 文件也会变得越来越大。与此同时,如果服务器曾经对相同的键执行过多次修改操作,那么 AOF 文件中还会出现多个冗余命令。

    SELECT 0
    SET msg "hello world! "
    SET msg "good morning! "
    SET msg "happy birthday! "
    SADD fruits "apple"
    SADD fruits "banana"
    SADD fruits "cherry"
    SADD fruits "dragon fruit"
    SREM fruits "dragon fruit"
    SADD fruits "durian"
    RPUSH job-queue 10086
    

    以上命令,重写后最终可以简化为:

    SELECT 0
    SET msg "happy birthday! "
    SADD fruits "apple" "banana" "cherry" "durian"
    RPUSH job-queue 10086
    

    为了减少冗余命令,让 AOF 文件保持“苗条”,并提供数据恢复操作的执行速度,Redis 提供了 AOF 重写功能BGREWRITEAOF命令,该命令能够生成一个全新的 AOF 文件,并且文件中只包含恢复当前数据库所需的尽可能少的命令。

    与 RDB 持久化的 BGSAVE命令一样,BGREWRITEAOF命令也是一个异步命令,Redis 服务器在接收到该命令之后会创建出一个子进程,由它扫描整个数据库并生成新的 AOF 文件。当新的 AOF 文件生成完毕,子进程就会退出并通知 Redis 服务器(父进程),然后 Redis 服务器就会使用新的 AOF 文件代替已有的 AOF 文件,借此完成整个重写操作。

    除了手动执行BGREWRITEAOF命令,也可以通过配置自动触发BGREWRITEAOF命令:

    • auto-aof-rewrite-min-size <value>:选项用于设置触发自动 AOF 文件重写所需的最小 AOF 文件体积,当 AOF 文件的体积大于给定值时,服务器将自动执行BGREWRITEAOF命令,该值的默认值是 64mb。

    • auto-aof-rewrite-percentage <value>:它控制的是触发自动 AOF 文件重写所需的文件体积增大比例。举个例子,如果此值设置为 100,表示如果当前 AOF 文件的体积比最后一次 AOF 文件重写之后的体积增大了一倍(100%),那么将自动执行一次BGREWRITEAOF命令。

    AOF 持久化的优缺点

    • 优点:

      • 与 RDB 持久化可能会丢失大量数据相比,AOF 持久化的安全性要高得多:通过使用everysec选项,用户可以将数据丢失的时间窗口限制在 1s 之内。
    • 缺点:

      • 为 AOF 文件存储的是协议文本,所以它的体积会比包含相同数据、二进制格式的 RDB 文件要大得多,并且生成 AOF 文件所需的时间也会比生成 RDB 文件所需的时间更长。
      • 因为 RDB 持久化可以直接通过 RDB 文件恢复数据库数据,而 AOF 持久化则需要通过执行 AOF 文件中保存的命令来恢复数据库(前者是直接的数据恢复操作,而后者则是间接的数据恢复操作),RDB 持久化的数据恢复速度将比 AOF 持久化的数据恢复速度快得多,并且数据库体积越大,这两者之间的差距就会越明显。
      • AOF 重写使用的BGREWRITEAOF命令与 RDB 持久化使用的BGSAVE命令一样都需要创建子进程,所以在数据库体积较大的情况下,进行 AOF 文件重写将占用大量资源,并导致服务器被短暂地阻塞。

    RDB-AOF 混合持久化

    由于 RDB 持久化和 AOF 持久化都有各自的优缺点,因此在很长一段时间里,如何选择合适的持久化方式成了很多 Redis 用户面临的一个难题。为了解决这个问题,Redis 从 4.0 版本开始引入 RDB-AOF 混合持久化模式,这种模式是基于 AOF 持久化模式构建而来的,如果用户打开了服务器的 AOF 持久化功能,并且将

    aof-use-rdb-preamble <value>
    

    设置为 yes,那么 Redis 服务器在执行 AOF 重写操作时,就会像执行BGSAVE命令那样,根据数据库当前的状态生成出相应的 RDB 数据,并将这些数据写入新建的 AOF 文件中,至于那些在 AOF 重写(BGREWRITEAOF)开始之后执行的 Redis 命令,则会继续以协议文本的方式追加到新 AOF 文件的末尾,即已有的 RDB 数据的后面。

    所以,开启了 RDB-AOF 混合持久化后,服务器生成的 AOF 文件将由两个部分组成,其中位于 AOF 文件开头的是 RDB 格式的数据,而跟在 RDB 数据后面的则是 AOF 格式的数据。

    结构
    RDB 数据
    AOF 数据

    当一个支持 RDB-AOF 混合持久化模式的 Redis 服务器启动并载入 AOF 文件时,它会检查 AOF 文件的开头是否包含了 RDB 格式的内容:

    • 如果包含,那么服务器就会先载入开头的 RDB 数据,然后再载入之后的 AOF 数据。
    • 如果 AOF 文件只包含 AOF 数据,那么服务器将直接载入 AOF 数据。

    所以为了避免全新的 RDB-AOF 混合持久化功能给传统的 AO F持久化功能使用者带来困惑,Redis 目前默认是没有打开 RDB-AOF 混合持久化功能的:aof-use-rdb-preamble no,如果要开启,需要用户手动设置 value 为 yes。

    无持久化

    即使用户没有显式地开启 RDB 持久化功能和 AOF 持久化功能,Redis 服务器也会默认使用以下配置进行 RDB 持久化:

    save 6010000
    save 300100
    save 3600 1
    

    如果用户想要彻底关闭这一默认的 RDB 持久化行为,让 Redis 服务器处于完全的无持久化状态,那么可以在服务器启动时向它提供以下配置选项:

    save ""
    

    这样一来,服务器将不会再进行默认的 RDB 持久化,从而使得服务器处于完全的无持久化状态中。处于这一状态的服务器在关机之后将丢失关机之前存储的所有数据,这种服务器可以用作单纯的内存缓存服务器。

    优雅的关闭 Redis 服务器

    如何优雅的关闭 Redis 服务器呢?那就是使用 SHUTDOWN命令,执行该命令,将执行如下动作:

    1. 停止处理客户端发送的命令请求。
    2. 如果服务器启用了 RDB 持久化功能,并且数据库距离最后一次成功创建 RDB 文件之后已经发生了改变,那么服务器将执行SAVE 命令,创建一个新的 RDB 文件。
    3. 如果服务器启用了 AOF 持久化功能或者 RDB-AOF 混合持久化功能,那么它将冲洗 AOF 文件,确保所有已执行的命令都被记录到了 AOF 文件中。
    4. 如果服务器既没有启用 RDB 持久化功能,也没有启用 AOF 持久化功能,那么服务器将略过这一步。
    5. 服务器进程退出。

    所以只要服务器启用了持久化功能,那么使用SHUTDOWN命令来关闭服务器就不会造成任何数据丢失。SHUTDOWN命令提供的 save 选项或者 nosave 选项,显式地指示服务器在关闭之前是否需要执行持久化操作:

    SHUTDOWN [save|nosave]
    

    如果用户给定的是 save 选项,那么无论服务器是否启用了持久化功能,服务器都会在关闭之前执行一次持久化操作。如果用户给定的是 nosave 选项,那么服务器将不执行持久化操作,直接关闭服务器。在这种情况下,如果服务器在关闭之前曾经修改过数据库,那么它将丢失那些尚未保存的数据。

    总结

    总的来说,在数据持久化这个问题上,Redis 4.0 及之后版本的使用者都应该优先使用 RDB-AOF 混合持久化;对于 Redis 4.0 之前版本的使用者,因为 RDB 持久化更接近传统意义上的数据备份功能,而 AOF 持久化则更接近于传统意义上的数据持久化功能,所以如果用户不知道自己具体应该使用哪种持久化功能,那么可以优先选用 AOF 持久化作为数据持久化手段,并将 RDB 持久化用作辅助的数据备份手段。

    ]]>
    2020-12-02T16:21:25+00:00
    https://github.com/superleeyom/blog/issues/8基于Docker实现nginx-keepalived双机热备机制2024-04-24T05:23:59.989917+00:00基本概念

    LVS

    LVS 是一个开源的软件,可以实现传输层四层负载均衡。LVS 是 Linux Virtual Server 的缩写,意思是 Linux 虚拟服务器。目前有三种 IP 负载均衡技术(VS/NAT、VS/TUN和VS/DR);八种调度算法:轮询、加权轮询、源地址散列、目标地址散列、最小连接数、加权最少连接数、最短期望延迟、最少队列调度。

    • NAT:

    • TUN:

    • DR:

    Keepalived

    Keepalived 是基于 vrrp 协议的一款高可用软件。Keepailived 有一台主服务器和多台备份服务器,在主服务器和备份服务器上面部署相同的服务配置,使用一个虚拟 IP 地址(Virtual IP,简称 VIP)对外提供服务,当主服务器出现故障时,虚拟 IP 地址会自动漂移到备份服务器,能够真正做到主服务器和备份服务器故障时 IP 瞬间无缝交接。

    整体流程

    • 创建基础镜像 centos_base,基础镜像里面安装了常用的工具,比如 vimwgetzlib 等,打包镜像的时候,为了方便,使用 dockercommit命令,但是实际推荐还是使用 Dockerfile 定制镜像,使用 docker commit 意味着所有对镜像的操作都是黑箱操作,生成的镜像也被称为黑箱镜像,官方并不推荐。

      # 拉取centos7镜像
      docker pull centos:7
      # 创建容器centos1
      docker run -itd --name centos1 centos:7
      # 进入容器centos1
      docker exec -it centos1 bash
      # 进入容器centos1后,安装常用的工具
      yum update
      yum install -y vim
      yum install -y wget
      yum install -y  gcc-c++  
      yum install -y pcre pcre-devel  
      yum install -y zlib zlib-devel  
      yum install -y  openssl-devel
      yum install -y popt-devel
      yum install -y initscripts
      yum install -y net-tools
      # 常用工具安装完成后,退出容器centos1,然后将该容器重新打包成新的镜像centos_base
      docker commit -a 'leeyom' -m 'centos with common tools' centos1 centos_base
      
    • 删除之前创建的 centos1 容器,重新以镜像 centos_base 为基础镜像创建容器centos_temp,并安装 keepalivednginx

      # 终止centos1容器
      docker container stop centos1
      # 删除centos1容器
      docker container rm centos1
      # 创建基础镜像容器centos_temp,使用privileged参数,表示容器内的root用户拥有真正的root权限
      # 容器内需要使用systemctl服务,需要加上/usr/sbin/init
      docker run -it --name centos_temp -d --privileged centos_base /usr/sbin/init
      # 进入centos_temp容器
      docker exec -it centos_temp bash
      
      # 安装nginx的依赖库
      rpm -Uvh http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm
      # 安装nginx
      yum install -y nginx
      # 启动nginx
      systemctl start nginx.service
      # 测试nginx是否安装成功,若安装成功会显示nginx的欢迎界面的html代码
      curl 172.17.0.2
      
      # 下载keepalived
      wget http://www.keepalived.org/software/keepalived-1.2.18.tar.gz
      # 解压keepalived安装包
      tar -zxvf keepalived-1.2.18.tar.gz -C /usr/local/
      # 安装keepalived依赖的插件openssl
      yum install -y openssl openssl-devel
      # 编译keepalived 
      cd  /usr/local/keepalived-1.2.18/ && ./configure --prefix=/usr/local/keepalived
      make && make install
      
      # 将keepalived安装成系统服务
      mkdir /etc/keepalived
      cp /usr/local/keepalived/etc/keepalived/keepalived.conf  /etc/keepalived/
      cp /usr/local/keepalived/etc/sysconfig/keepalived /etc/sysconfig/
      cp /usr/local/keepalived/sbin/keepalived  /usr/sbin/
      

      修改keepalived的配置文件 keepalived.conf ,同时设置 keepalived开机自启。

      # 备份配置文件
      cp /etc/keepalived/keepalived.conf /etc/keepalived/keepalived.conf.backup
      # 删除默认的配置文件,自己重新创建一个keepalived.conf文件
      cd /etc/keepalived/
      rm -f keepalived.conf
      vim keepalived.conf
      

      keepalived.conf文件的配置内容如下:

      vrrp_script chk_nginx {
      		# nginx心跳检测脚本
          script "/etc/keepalived/nginx_check.sh"
          interval 2
          weight -20
      }
      
      vrrp_instance VI_1 {
      		# 指定master
          state MASTER
          interface eth0
          # 路由id,所有服务器指定一致
          virtual_router_id 121
          # 当前容器ip地址
          mcast_src_ip 172.17.0.2
          priority 100
          nopreempt
          advert_int 1
          authentication {
              auth_type PASS
              auth_pass 1111
          }
      
          track_script {
              chk_nginx
          }
      
      		# 虚拟ip
          virtual_ipaddress {
              172.17.0.100
          }
      }  
      

      keepalived 是通过检测 keepalived 进程是否存在判断服务器是否宕机,如果 keepalived 进程在但是nginx进程不在了那么keepalived 是不会做主备切换,所以我们需要写个脚本来监控 nginx 进程是否存在,如果 nginx 不存在就将 keepalived 进程杀掉:

      # 在/etc/keepalived/目录下创建监控脚本
      vim nginx_check.sh
      # 脚本内容
      #!/bin/bash
      A=`ps -C nginx –no-header |wc -l`
      if [ $A -eq 0 ];then
          /usr/local/nginx/sbin/nginx
          sleep 2
          if [ `ps -C nginx --no-header |wc -l` -eq 0 ];then
              killall keepalived
          fi
      fi
      # 给脚本赋予执行权限
      chmod +x nginx_check.sh
      # keepalived开机自启
      systemctl enable keepalived.service
      chkconfig keepalived on
      systemctl start keepalived.service
      

      nginx设置为开机自启:

      systemctl enable nginx.service
      chkconfig nginx on
      

      检测虚拟 ip 是否成功,在宿主机内执行如下的命令:

      curl 172.17.0.100
      

      如果出现 nginx欢迎界面,则表示成功:

      <!DOCTYPE html>
      <html>
      <head>
      <title>Welcome to nginx!</title>
      <style>
          body {
              width: 35em;
              margin: 0 auto;
              font-family: Tahoma, Verdana, Arial, sans-serif;
          }
      </style>
      </head>
      <body>
      <h1>Welcome to nginx master !</h1>
      <p>If you see this page, the nginx web server is successfully installed and
      working. Further configuration is required.</p>
      
      <p>For online documentation and support please refer to
      <a href="http://nginx.org/">nginx.org</a>.<br/>
      Commercial support is available at
      <a href="http://nginx.com/">nginx.com</a>.</p>
      
      <p><em>Thank you for using nginx.</em></p>
      </body>
      </html>
      
    • centos_temp 容器重新打包成镜像centos_kn,然后利用这个新镜像再创建两个容器centos_mastercentos_slave,实现热备效果。

      # 创建新的镜像centos_kn
      docker commit -a 'leeyom' -m 'centos with keepalived nginx' centos_temp centos_kn
      # 删除容器centos_temp
      docker container stop centos_temp
      docker container rm centos_temp
      # 用centos_kn镜像创建主服务器容器centos_master
      docker run --privileged  -tid --name centos_master --restart=always  centos_kn /usr/sbin/init
      

      进入centos_master ,修改centos_master容器里面nginx 欢迎页的标题为:Welcome to nginx master,用于区分我们当前访问的是master容器的nginx

      vim /usr/share/nginx/html/index.html
      

      创建从服务器容器centos_slave,并进入容器,修改keepalived.conf 配置文件,主要是stateprioritymcast_src_ip三个参数的调整,其中master节点的priority值一定要比slave大才行。

      # 创建容器centos_slave
      docker run --privileged  -tid --name centos_slave --restart=always  centos_kn /usr/sbin/init
      # 进入容器
      docker exec -it centos_slave bash
      # 编辑keepalived.conf文件
      vim /etc/keepalived/keepalived.conf
      
      vrrp_script chk_nginx {
          script "/etc/keepalived/nginx_check.sh"
          interval 2
          weight -20
      }
      
      vrrp_instance VI_1 {
          state SLAVE
          interface eth0
          virtual_router_id 121
          mcast_src_ip 172.17.0.3
          priority 80
          nopreempt
          advert_int 1
          authentication {
              auth_type PASS
              auth_pass 1111
          }
      
          track_script {
              chk_nginx
          }
      
          virtual_ipaddress {
              172.17.0.100
          }
      }
      

      修改centos_slave容器里面nginx 欢迎页的标题为:Welcome to nginx slave,用于区分我们当前访问的是slave容器的nginx

      vim /usr/share/nginx/html/index.html
      

      修改完后,重新加载keepalived服务:

      systemctl daemon-reload
      systemctl restart keepalived.service
      
    • 开始测试:

      • 分别在宿主机,centos_mastercentos_slave中进行一下命令测试,如果nginx都显示为master的欢迎页面,说明配置是没啥问题的。

        curl 172.17.0.100
        
      • 关闭centos_master容器,模拟master机器故障,在centos_slave执行命令测试,如果nginx显示的欢迎页面由master切换到了slave,说明进行了故障转移,vip主机进行了漂移,主机挂掉后,备用机顶上。

        curl 172.17.0.100
        
      • 重新启动centos_master容器,再次执行命令测试,看nginx欢迎页面标题,slave切换到了master,如果切换成功,说明我们配置到此成功了。

        curl 172.17.0.100
        
    • 以上便是模拟的nginx-keepalived双机热备机制,到此,所有的验证和预期的一致,也达到我们借助docker为基础来实现了整套基于Nginx+Keepalived高可用的方案了。

    keepalived 服务命令

    • systemctl daemon-reload:重新加载
    • systemctl enable keepalived.service: 设置开机自动启动
    • systemctl disable keepalived.service :取消开机自动启动
    • systemctl start keepalived.service :启动
    • systemctl stop keepalived.service:停止
    • systemctl status keepalived.service :查看服务状态
    ]]>
    2020-11-23T03:03:49+00:00
    https://github.com/superleeyom/blog/issues/7[笔记]睡眠革命2024-04-24T05:24:00.082720+00:00睡眠革命:如何让你的睡眠更高效

    作者:尼克·利特尔黑尔斯;数量:13个笔记;时间:2020-11-22 11:18:11

    • [02] 走慢与走快——睡眠类型:
      • 把咖啡因当成高效的表现增强剂使用,而不是出于习惯去喝咖啡,并且一天的咖啡因摄入量不要超过400毫克。
    • [03] 90分钟睡眠法——睡眠周期:
      • R90”指的是以90分钟为一个周期,获得身体修复。“90”这个数字,并不是我从1—100中随意选择的。从临床上说,90分钟是一个人经历各个睡眠阶段所需的时间。这些睡眠阶段组成了一个睡眠周期。
      • 事实上,如果想提高睡眠修复的质量,那么设置固定时间的闹铃,正是我们能采取的最有效的方法。
    • [04] 热身与舒缓——睡眠前后的例行程序:
      • 在睡觉前提前关闭电脑、平板电脑、智能手机和电视机,能减少你暴露在这些设备发出的蓝光下的时间。
    • [05] 暂停片刻,该休息了!——日间小睡:
      • 即使你并没有真正进入睡眠状态也没有关系。重要的是,你能利用这段时间闭上眼睛、脱离这个世界片刻。能够睡着固然很棒,但徘徊在似睡非睡、似醒非醒的蒙眬、迷糊的状态中,同样也很迷人。这就像一个美好的白日梦,你并没有认真在想什么,你的大脑处于一片混沌中。
    • [06] 改造你的床铺——寝具套装:
      • 侧卧是我唯一推荐的睡姿,但也许你现在侧卧的方向并不正确。我所训练的那些运动员睡觉时,会采用胎儿的姿势、躺向相对不太重要的身体一侧。因为这是使用较少也较不敏感的一侧,换句话说,如果你习惯使用右手,你该向左侧睡,反之亦然。
    • [09] 与敌同眠——各种睡眠问题:
      • 多年以来,我们一直盲目地相信应当每晚睡足8小时,因此,“每晚只睡4个半小时就够了”会让大脑一时难以接受。但这样的安排反而更有益:比较一下,3个无缝衔接的睡眠周期——至少其中的有效睡眠占了不少的比例(还记得吗,如果没有获得充足睡眠,你的大脑会优先进入快速眼动睡眠阶段),和一段类似时长的睡眠——但断断续续地分布在8小时、且大多是浅睡眠,哪一种睡眠对你更有益呢?
      • 根据R90方案,我们把7天作为一个周期来观测睡眠状况,而不再着眼于一个晚上的睡眠状况。所以,如果在7天之后,那些睡眠问题仍然存在,可以再减少一个睡眠周期,让她在凌晨2点上床睡觉。这似乎难以置信,但你必须认识到,这并不是一个长期措施。这样做是为了有效地重置你的睡眠模式,并找到你的极限——你能保持多长时间的高效睡眠,然后逐步增加睡眠时间。
      • 在睡眠不佳时,不要总想着这一晚的睡眠是多么重要。我之所以提倡观察一周总共获得的睡眠周期并且推介24×7的修复计划,原因就在于此。把一晚的睡眠看得太重,是有欠公允的。
      • 失眠是过度清醒造成的。在过度清醒状态下,人脑会因过于兴奋而难以入睡。
      • 如果你睡不着,试着用别的方式休息一下。比如试着冥想,或者重温一下你运动生涯中最辉煌的一刻。你可以利用这些时间做些别的事情。”
      • 让他们重温自己的辉煌时刻,能帮助他们降低焦虑水平——焦虑有可能是他们失眠的原因。
      • 夜晚,人体会自然分泌褪黑素进入睡眠状态。如果你在夜间工作,就错过了睡眠冲动和睡眠需求同时达到高峰的睡眠时机,等早上回到家中时,太阳已经升起,你的睡眠压力非常之大,但睡眠冲动开始下降,你很难获得像夜间睡眠一样的高质量睡眠。
    • [10] 主队——性、伴侣和现代家庭:
      • 和谐的性爱能以一种愉快的方式有效地降低压力、焦虑和担忧,具有惊人的功效。它能让大脑聚焦于令人兴奋的自发行为,让人沉浸在那一刻中。它能让我们感到自己有人爱、有人需要并带来安全感。它是一种自然而然的锻炼方式——越频繁越好——而且性爱过后能让我们感到温馨、放松、幸福。此外,它似乎能让人快速进入梦乡,特别是对男性来说。
    ]]>
    2020-11-22T03:22:50+00:00
    https://github.com/superleeyom/blog/issues/6Hackintosh黑苹果折腾之旅2024-04-24T05:24:00.167979+00:00

    硬件配置

    其实我这台黑苹果,今年年初三月份的时候就装好了,周末趁着有空,把系统升级到了 macOS Big Sur ,在此总结下自己的整个的安装的一些心得。

    我这台黑苹果主机的整体配置清单如下:

    • cpu:intel i5 9400 散片 淘宝 1300元
    • 主板:华擎B365M-ITX/AC 淘宝 659元
    • 显卡:盈通 rx580 4g 2304sp 满血版 508元
    • 硬盘:海康威视 C2000 pro 256g SSD 京东 335 元 + 「自用剩余的闪迪」 SATA3 256g SSD
    • 内存条:宇瞻 DDR4 2666 16g*2 天猫 978元
    • 散热器:ID-COOLING IS-40X 淘宝 89元
    • 显卡延长线:傻瓜超人 ADT01 淘宝 99元(方便后期加显卡)
    • 电源:海盗船 sf450 金牌 + 定制线 淘宝 728元
    • 机箱:傻瓜超人 K55 + 定制铝侧板两块 淘宝 405 元
    • 无线网卡:BCM94360CS2 无线网卡+转接卡 淘宝 160元
    • 总计:5261 元

    由于我是第一次装机,一来就装 ITX 主机,各种懵逼,好在说明书也挺全的,从下午一直装到晚上凌晨三四点,才把机器点亮。

    组装初衷

    那为啥要组装一台黑苹果呢?而不直接买白的呢?这个嘛,迫于入了苹果生态的坑,加上老笔记本 MacBook Pro (Retina, 13-inch, Early 2015) 性能已经逐渐跟不上了,所以就萌生了组台 itx 黑苹果主机,那说到底,其实就是「穷」。

    黑苹果有优点也有缺点,优点是:

    • 省钱呀,性价比高,同样的配置,可能只要白苹果的1/3的价格
    • 可扩展性强,可以自己随意diy硬件

    缺点:

    • 如果硬件选的不好,比较折腾,没有合适的EFI的话,需要自己去摸索
    • 版本升级麻烦,每次大版本的升级的话,都需要重装
    • 需要一定软硬件基础

    装黑苹果,要想省事的话,去论坛,比如国内的:远景论坛黑果小兵,国外的:tonymacx86,按照别人已经有装成功过的硬件,并且对应的EFI也都有,那就对着采购一套相同的配件,尽可能选择免驱的硬件,基本上不会出大的问题,比如我的这个网卡BCM94360CS2 就是原笔记本的拆机原件。当然,你前提得懂一些基本的硬件和软件知识,否则还是花点钱,直接远程交给某宝来装。

    由于我自己装的这套配置,已经有成功的 (B365ITX-Hackintosh-OC) 例子,只要按照原作者给的提示,把对应的该删的删了,整体的安装过程,基本上很顺畅。整体的安装流程,参考的是黑果大神「黑果小兵」的教程「天逸510s Mini兼macOS BigSur安装教程」,镜像也是用的黑果小兵大神封装的「macOS BigSur 11.0.1 20B29 正式版」。

    遇到的问题

    目前主流的两到黑苹果引导方式:OpenCore(简称OC)和 Clover(简称四叶草),目前 Clover 逐渐被淘汰了,很多的驱动 Kext 都放弃适配 Clover,大家目前都开始使用 OC 做为黑苹果的首选引导方式。

    在安装过程中没遇到大的问题,就是通过「时间机器」恢复备份的时候,遇到一个比较坑爹的事情,我顺利的进入系统后,我打开系统自带的「迁移助理」,想恢复原有的备份,但是每次恢复到一半的时候,老是自动关机,重试几次,都是一样,很让人崩溃。最后,我不得不再次抹盘重装,在刚刚进入系统初始化的时候,系统提示是否要导入已有的备份,我从这里开始进行恢复,不等彻底登录进入系统后,通过「迁移助理」进行恢复,嚯,好家伙,顺顺利利的恢复成功了,你说这奇怪不奇怪。

    再个就是修改引导工具 OpenCore 的相关的配置的时候,不生效的问题。比如原作者的 EFI,启动的时候开启了啰嗦模式(-v),系统启动的时候,总会打印一大串的debug的代码,所以为了隐藏这个,得把 EFI 里 config.plistboot-args 属性里的 -v 去掉即可,但是发现去掉后,重启依旧不生效,后面通过找资料,最后发现,需要用工具 Hackinttools,清除 NVRAM 里这行配置的缓存(选中删除即可),否则不会生效。

    另外启动的时候,不想显示引导菜单的话,将 showpicker 改成 false,其他的话,目前没有遇到啥问题,由于大部分原作者已经调试好了,基本上就不需要调整了,感谢!

    完成情况

    目前这台黑苹果的整体的完整度在99%吧,唯一的缺点就是,睡眠的话,需要手动去点击系统左上角的「睡眠」选项,有时候系统无法自动进入睡眠。我自己也使用了大半年了,整体上和苹果的台式机 iMac、Mac Pro 基本上无任何的差别,也能配合 iPad、iPhone、Apple Watch 进行隔空投送、接力、随航、解锁 Mac等等一系列的功能。在会自己折腾的情况下,还是挺香的。所以如果你有差不多相同的配置,可以试试我这个 EFI。

    参考资料

    资源附件

    ]]>
    2020-11-15T08:15:07+00:00
    https://github.com/superleeyom/blog/issues/5nginx负载均衡原理之ip_hash哈希算法探究2024-04-24T05:24:00.259719+00:00关于 ip_hash nginx 官网是这样定义的:

    Specifies that a group should use a load balancing method where requests are distributed between servers based on client IP addresses. The first three octets of the client IPv4 address, or the entire IPv6 address, are used as a hashing key. The method ensures that requests from the same client will always be passed to the same server except when this server is unavailable. In the latter case client requests will be passed to another server. Most probably, it will always be the same server as well.

    翻译过来就是:

    指定组应使用负载平衡方法,其中根据客户端IP地址在服务器之间分配请求。 客户端IPv4地址的前三个八位位组或整个IPv6地址用作哈希密钥。 该方法确保了来自同一客户端的请求将始终传递到同一服务器,除非该服务器不可用。 在后一种情况下,客户端请求将传递到另一台服务器。 最有可能的是,它也将永远是同一台服务器。

    假设目前有三台服务器,采用 nginx 的ip_hash负载均衡策略,假设现在有三台 Tomcat 服务器,其对应的节点 index 分别是:0、1、2,此时有四个客户端访问 nginx,那根据哈希算法:hash(ip)%node_counts = index,最终得到的各个客户端的请求会落到如下的机器上(假设四个客户端ip的哈希值分别为:5、6、7、8):

    • ip:指的是客户端的 ip 地址的前三个八位位组,打个比方:138.23.324.13,那就是取138.23.324,所以如果同一个局域网内访问 nginx 的时候,如果机器的前三个八位一致,比如这两个ip:138.23.324.13138.23.324.14的请求最终只会落在同一台的节点上
    • node_counts:节点数量
    • index:节点的标识

    但是使用哈希算法会存在一些问题,比如要删除一个节点3,这时候用户的请求落地情况会变成这样:

    原本用户4是访问的节点3,这时候就会转为访问节点1,这时候在之前节点3上的用户会话就会丢失,增加一个节点也是同理,同样会存在用户会话丢失的情况。如何正确让一台服务器下线?那在 nginx 官网的文档中写道:如果需要临时删除其中一个服务器,则应该使用 down 参数标记它,以便保存当前客户机 IP 地址的散列。

    If one of the servers needs to be temporarily removed, it should be marked with the down parameter in order to preserve the current hashing of client IP addresses.

    示例:

    upstream backend {
        ip_hash;
    
        server backend1.example.com;
        server backend2.example.com;
        server backend3.example.com down;
        server backend4.example.com;
    }
    

    如果采用一致性哈希算法,出现以上问题的概率就会低很多。

    如上图,圆环上有 0-2^32-1 个节点,每个节点,按顺时针方向排列递增,用户的请求 ip,通过哈希算法,会落在圆环上的某个节点上。按顺时针方向,用户1、用户2会访问节点1,用户3、用户4会访问节点2,用户5、用户6会访问节点3,用户7访问到节点4。假如此时删除了节点3,那么原本的用户5和用户6的请求,则会落到节点4上,其他用户的访问均不会受到影响,只会有用户5和用户6的会话信息会丢失,不会造成全局的变动。

    另外还有一个优点,如果发现节点1访问量很大,负载高于其他节点,这就说明节点1存储的数据是热点数据。这时候,为了减少节点1的负载,我们可以在热点数据位置再加入一个node,用来分担热点数据的压力。

    ]]>
    2020-11-05T06:24:49+00:00
    https://github.com/superleeyom/blog/issues/4nginx基础指令及初始配置解析2024-04-24T05:24:00.344570+00:00nginx 常用命令
    • ./nginx -s stop :强制停止nginx

    • ./nginx -s quit: 优雅停止nginx,即处理完所有请求后再停止服务

    • ./nginx -t :检测配置文件是否有语法错误

    • ./nginx -v: 查看nginx的版本号

    • ./nginx -V :查看版本号和配置选项信息

    • ./nginx -c:设置配置文件(默认是:/etc/nginx/nginx.conf

    • ./nginx -s reload: 重新加载配置文件

    nginx docker 相关指令

    • 拉取 nginx 镜像:docker pull nginx
    • 启动 nginx 容器实例:docker run -itd --name nginx-demo -p 8080:80 nginx
      • -itd-t 选项让 Docker 分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上, -i 则让容器的标准输入保持打开,-d是后台运行
      • --name nginx-demo:指定容器实例名称nginx-demo
      • -p 8080:80:将本机 8080 端口映射为容器的 80 端口
    • 进入容器:docker exec -it nginx-demo bash
    • 终止容器:docker container stop nginx-demo
    • 删除容器:docker container rm nginx-demo
    • 启动已终止的容器:docker container start nginx-demo
    • 查询当前运行的容器:docker container ls
    • 查询所有的容器:docker container ls -a
    • 更多的docker指令见《Docker — 从入门到实践》

    nginx 默认配置文件解析

    # 设置worker进程的用户,指的linux中的用户,会涉及到nginx操作目录或文件的一些权限
    user  nginx;
    # worker进程工作数设置,一般来说CPU有几个,就设置几个
    worker_processes  1;
    
    # 设置日志级别,debug | info | notice | warn | error | crit | alert | emerg,错误级别从左到右越来越大
    error_log  /var/log/nginx/error.log warn;
    # 设置nginx进程 pid
    pid        /var/run/nginx.pid;
    
    # 设置工作模式
    events {
    	# 每个worker允许连接的客户最大连接数
    	worker_connections  1024;
    }
    
    # http 是指令块,针对http网络传输的一些指令配置
    http {
    	# include 引入外部配置,提高可读性,避免单个配置文件过大
    	include /etc/nginx/mime.types;
    	# 设置HTTP默认的 content-type
    	default_type  application/octet-stream;
    	# 设置日志格式,各项含义如下:
    	# $remote_addr:客户端ip
    	# $remote_user:远程客户端用户名,一般为:’-’
    	# $time_local:时间和时区
    	# $request:请求的url以及method
    	# $status:响应状态码
    	# $body_bytes_send:响应客户端内容字节数
    	# $http_referer:记录用户从哪个链接跳转过来的
    	# $http_user_agent:用户所使用的代理,一般来时都是浏览器
    	# $http_x_forwarded_for:通过代理服务器来记录客户端的ip
    	log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                          '$status $body_bytes_sent "$http_referer" '
                          '"$http_user_agent" "$http_x_forwarded_for"';
    
    	access_log  /var/log/nginx/access.log  main;
    
    	# sendfile 使用高效的文件传输,提升传输性能,启用后才能使用tcp_nopush,指当数据表累积到一定的大小后才发送,提高效率
    	sendfile        on;
    	#tcp_nopush     on;
    		
    	# 设置客户端与服务端请求的超时时间,保证客户端多次请求的时候不会重复建立新的连接,节约资源损耗
    	keepalive_timeout  65;
    		
    	# 开启gzip压缩功能,提高传输效率,节约带宽
    	#gzip  on;
    		
    	# include 引入外部配置,提高可读性,避免单个配置文件过大
    	include /etc/nginx/conf.d/*.conf;
    }
    

    root 与 alias

    假如服务器路径为:/home/leeyom/files/img/header.png

    • root 路径完全匹配访问:

      location /leeyom { 
        root /home 
      }
      

      用户访问的请求为:url:port/leeyom/files/img/header.png

    • alias 可以为你的路径做一个别名,对用户透明:

      location /hello { 
        alias /home/leeyom
      }
      

      用户访问的请求为:url:port/hello/files/img/header.png,相当于给 leeyom 目录做一个别名。

    location 的匹配规则

    • 空格:默认匹配,普通匹配

      location / { 
        root /home 
      }
      

      用户可以访问 home 目录下的所有文件。

    • =:精确匹配

      location = /leeyom/files/img/header.png { 
        root /home; 
      }
      

      用户只能访问此路径/home/leeyom/files/img/header.png下的header.png图片。

    • ~*:匹配正则表达式,不区分大小写

      location ~* \.(GIF|jpg|png|jpeg|gif) { 
        root /home; 
      }
      

      用户可以访问 home 目录下的只要后缀为GIF|jpg|png|jpeg|gif的文件,由于不区分大小写,如果访问的是 header.GIF图片,会重定向访问header.gif图片。

    • ~:匹配正则表达式,区分大小写

      location ~ \.(GIF|jpg|png|jpeg|gif) { 
        root /home; 
      }
      

      用户可以访问 home 目录下的只要后缀为GIF|jpg|png|jpeg|gif的文件。

    • ^~:以某个字符路径开头请求

      location = ^~ /leeyom/files/img { 
        root /home; 
      }
      

      用户只能访问此路径/home/leeyom/files/img/下的文件。

    nginx 跨域配置

    server 块里面增加:

    # 允许跨域请求的域,*代表允许所有的域
    add_header 'Access-Control-Allow-Origin' *;
    # 允许带上cookie请求
    add_header 'Access-Control-Allow-Credentials' 'true';
    # 允许请求的header,比如:Authorization,Content-Type,Accept,Origin,User-Agent 等
    add_header 'Access-Control-Allow-Headers' *;
    # 允许请求的方法,比如:GET、POST、PUT、DELETE
    add_header 'Access-Control-Allow-Methods' *;
    

    nginx 防盗链

    # 对源站点进行验证(白名单),多个域名用空格隔开
    valid_referers *.leeyom.com;
    # 非法访问则返回403
    if($invalid_referer){
    	return 403;  
    }
    

    nginx 搭建 Tomcat 集群简版配置

    upstream tomcats {
      server 192.168.1.174:8080;
      server 192.168.1.175:8080;
      server 192.168.1.176:8080;
    }
    server {
      listen 80;
      server_name www.tomcats.com;
      location / {
        proxy_pass: http://tomcats;
      }
    }
    

    访问www.tomcats.com,将以轮询方式,分别访问三台 Tomcat,当然也可以使用加权轮询,例如:

    server 192.168.1.174:8080 weight=1;
    server 192.168.1.175:8080 weight=2;
    server 192.168.1.176:8080 weight=5;
    

    weight的值越大,当前服务器的 Tomcat 被访问的几率越大。

    upstream 指令

    server 192.168.1.174:8080 max_conns=2;
    server 192.168.1.175:8080 max_conns=2;
    server 192.168.1.176:8080 max_conns=2;
    
    • max_conns:限制每台server的连接数,用于保护避免过载,可起到限流作用;
    server 192.168.1.174:8080 weight=1;
    server 192.168.1.175:8080 weight=2;
    server 192.168.1.176:8080 weight=5 slow_start=60s;
    
    • slow_start:缓慢启动,weight逐渐增大,使某台服务器慢慢加入集群,方便该服务器完成一些前置化的操作,该指令需要注意:
      • 只能在商业版中使用;
      • 该参数不能使用在hashrandom load balancing中;
      • 如果upstream中只有一台 server,则该参数无效;
    • down:标记服务节点不可用
    • backup:表示当前服务器节点是备用机, 只有在其他的服务器都宕机以后, 自己才会加入到集群中, 被用户访问到
      • backup参数不能使用在hashrandom load balancing中;
    • max_fails:表示失败几次,则标记 server 已宕机,踢出服务,默认值为1
    • fail_timeout:表示失败的重试时间,默认值 10s
      • 示例:max_fails=2 fail_timeout=15s :15 秒内,请求某一 server 失败达 2 次后,则认为此 server 已经宕机,随后再过 15 秒,这 15 秒内不会有新的请求到达刚宕机的节点,会请求到正常的运行的 server,15秒后会有新请求再次请求挂掉的 server,如果还是失败,重复之前的操作;

    Keepalived 提高吞吐量

    upstream tomcats {
      server 192.168.1.174:8080;
      server 192.168.1.175:8080;
      server 192.168.1.176:8080;
      # 设置长连接处理的数量
      keepalive 32;
    }
    server {
      listen 80;
      server_name www.tomcats.com;
      location / {
        proxy_pass: http://tomcats;
        # 设置长连接http的版本号
        proxy_http_version 1.1;
        # 清除 connection header 信息
        proxy_set_header Connection "";
      }
    }
    

    nginx的反向代理缓存

    # proxy_cache_path 设置缓存目录
    # keys_zone 设置共享内存以及占用空间大小
    # max_size 设置缓存大小
    # inactive 超过此时间则被清理
    # use_temp_path 临时目录,使用后会影响nginx性能
    proxy_cache_path /usr/local/nginx/upstream_cache keys_zone=mycache:5m max_size=1g inactive=1m use_temp_path=off;
    
    location / {
      proxy_pass http://tomcats;
      # 启用缓存,和keys_zone一致
      proxy_cache mycache;
      # 针对200和304状态码缓存时间为8小时
      proxy_cache_valid 200 304 8h;
    }
    

    配置ssl证书

    1. 安装 ssl 模块

    2. 将 ssl 证书*.crt和私钥*.key拷贝到/usr/local/nginx/conf目录中

    3. 新增 server 监控 443 端口:

      server{
        listen 443;
        server_name www.leeyom.me;
        # 开启ssl
        ssl on;
        # 配置ssl证书
        ssl_certificate yourdomain.com.crt;
        # 配置证书秘钥
        ssl_certificate_key yourdomain.com.key;
        # ssl会话cache
        ssl_session_cache shared:SSL:1m;
        # ssl会话超时时间
        ssl_session_timeout 5m;
        # 配置加密套件,写法遵循 openssl 标准
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers ECDHE-RSA-AES128-G
      }
      
    ]]>
    2020-10-20T01:39:07+00:00
    https://github.com/superleeyom/blog/issues/3白话解说之 BIO、NIO、AIO、异步阻塞的区别2024-04-24T05:24:00.436174+00:00白话解说

    首先明白这个几个简称的含义:

    • BIO(Blocking IO):同步阻塞
    • NIO(Non-Blocking IO):同步非阻塞
    • AIO(Asynchronous IO):异步非阻塞

    BIO、NIO、异步阻塞、AIO 这四者之间有啥区别呢?那就拿生活的实例来解释一下:

    • BIO:我去上厕所,这个时候坑位都满了,我必须等待坑位释放了,我才能上吧?!此时我啥都不干,站在厕所里盯着,过了一会有人出来了,我就赶紧蹲上去。
    • NIO:我去上厕所,这个时候坑位都满了,没关系,哥不急,我出去抽根烟,过会回来看看有没有空位,如果有我就蹲,如果没有我出去接着抽烟或者玩会手机。
    • 异步阻塞:我去上厕所,这个时候坑位都满了,没事我等着,等有了新的空位,让他通知我就行,通知了我,我就蹲上去。
    • AIO:我去上厕所,这个时候坑位都满了,没事,我一点也不急,我去厕所外面抽根烟再玩玩手机,等有新的坑位释放了,会有人通知我的,通知我了,我就蹲上去。

    从这个生活实例中能可以看得出来:

    • 同步就是我需要自己每隔一段时间,以轮训的方式去看看有没有空的坑位;
    • 异步则是有人拉完茅坑会通知你,通知你后,你再回去蹲;
    • 阻塞就是你在等待的过程中,你不去做任何的事情,就干等着;
    • 非阻塞就是你在等待的过程中,可以去做其他的事情,比如抽烟、玩手机等;

    总结就是:异步的优势显而易见,大大优化用户体验, 非阻塞使得系统资源开销远远小于阻塞模式,因为系统不需要创建新的进程(或线程),大大地节省了系统资源。

    专业解说

    发现一篇解说的很清楚的文章:《常见的IO模型有哪些?Java中的BIO、NIO、AIO 有啥区别?》

    ]]>
    2020-10-17T09:19:18+00:00
    https://github.com/superleeyom/blog/issues/2Java8函数式编程中比较实用的操作语法2024-04-24T05:24:00.518142+00:00java8-stream

    分组(一对多)

    假如有如下的一个数据结构:

    [
      {
        "userId": 1,
        "name": "王二狗",
        "className": "classA"
      },
      {
        "userId": 2,
        "name": "李老四",
        "className": "classA"
      },
      {
        "userId": 3,
        "name": "张翠花",
        "className": "classB"
      },
      {
        "userId": 4,
        "name": "李雷",
        "className": "classB"
      }
    ]
    

    需要将它按班级className进行分组,即如下的数据结构:

    {
      "classA": [
        {
          "userId": 1,
          "name": "王二狗",
          "className": "classA"
        },
        {
          "userId": 2,
          "name": "李老四",
          "className": "classA"
        }
      ],
      "classB": [
        {
          "userId": 3,
          "name": "张翠花",
          "className": "classB"
        },
        {
          "userId": 4,
          "name": "李雷",
          "className": "classB"
        }
      ]
    }
    

    学生实体类 Student.java

    @Data
    @AllArgsConstructor
    public class Student {
    
        private long userId;
    
        private String name;
    
        private String className;
    }
    

    采用Java8函数式编程 groupingBy 语法进行快速分组:

    Student s1 = new Student(1, "王二狗", "classA");
    Student s2 = new Student(2, "李老四", "classA");
    Student s3 = new Student(3, "张翠花", "classB");
    Student s4 = new Student(4, "李雷", "classB");
    List<Student> list = new ArrayList<>();
    list.add(s1);
    list.add(s2);
    list.add(s3);
    list.add(s4);
    Map<String, List<Student>> map = list.stream().collect(Collectors.groupingBy(Student::getClassName));
    System.out.println(JSONUtil.toJsonStr(map));
    

    分组(一对一)

    还是这个数据结构:

    [
      {
        "userId": 1,
        "name": "王二狗",
        "className": "classA"
      },
      {
        "userId": 2,
        "name": "李老四",
        "className": "classA"
      },
      {
        "userId": 3,
        "name": "张翠花",
        "className": "classB"
      },
      {
        "userId": 4,
        "name": "李雷",
        "className": "classB"
      }
    ]
    

    只不过要按用户的 userId 进行分组,分组后的数据格式如下:

    {
      "1": {
        "className": "classA",
        "userId": 1,
        "name": "王二狗"
      },
      "2": {
        "className": "classA",
        "userId": 2,
        "name": "李老四"
      },
      "3": {
        "className": "classB",
        "userId": 3,
        "name": "张翠花"
      },
      "4": {
        "className": "classB",
        "userId": 4,
        "name": "李雷"
      }
    }
    

    采用Java8函数式编程的 toMap 语法进行快速分组:

    Map<Long, Student> studentMap = list.stream().collect(Collectors.toMap(Student::getUserId, student -> student, (k1, k2) -> k1));
    

    对集合中重复的元素进行去重

    对于简单的集合,比如字符串,整型类的集合,采用 distinct 进行快速去重:

    List<String> strList = Arrays.asList("a", "b", "c", "c", "d");
    List<String> distinctList = strList.stream().distinct().collect(Collectors.toList());
    System.out.println(JSONUtil.toJsonStr(distinctList));
    

    对于集合元素是对象的,根据对象指定的属性进行去重:

    // 根据className去重
    List<Student> unique = list.stream().collect(Collectors.collectingAndThen(
        Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Student::getClassName))), ArrayList::new));
    System.out.println(JSONUtil.toJsonStr(unique));
    
    // 根据userId和className去重
    List<Student> unique2 = list.stream().collect(Collectors.collectingAndThen(
        Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(o -> o.getUserId() + ";" + o.getClassName()))), ArrayList::new));
    System.out.println(JSONUtil.toJsonStr(unique2));
    

    对集合元素进行快速排序

    Student s1 = new Student(1, "王二狗", "classA",10);
    Student s2 = new Student(2, "李老四", "classA",9);
    Student s3 = new Student(3, "张翠花", "classB",8);
    Student s4 = new Student(4, "李雷", "classB", 12);
    List<Student> list = new ArrayList<>();
    list.add(s1);
    list.add(s2);
    list.add(s3);
    list.add(s4);
    // 按照年龄进行升序
    list.sort(Comparator.comparing(Student::getAge));
    // 按照年龄进行降序
    list.sort(Comparator.comparing(Student::getAge).reversed());
    

    快速判断对象集合中是否存在指定的元素

    // 查找学生当中是否存在一个叫王二狗的同学
    boolean matchResult = list.stream().anyMatch(student -> "王二狗".equals(student.getName()));
    

    将List转变为逗号分隔的字符串

    List<String> cities = Arrays.asList("Milan", "London", "New York", "San Francisco");
    String citiesCommaSeparated = String.join(",", cities);
    System.out.println(citiesCommaSeparated);
    // 输出: Milan,London,New York,San Francisco
    

    参考文章

    ]]>
    2020-10-09T10:22:49+00:00
    https://github.com/superleeyom/blog/issues/1Java泛型的回顾之旅2024-04-24T05:24:00.608424+00:00“泛型”的字面意思就是广泛的类型。类、接口和方法代码可以应用于非常广泛的类型,代码与它们能够操作的数据类型不再绑定在一起,同一套代码可以用于多种数据类型,这样,不仅可以复用代码,降低耦合,而且可以提高代码的可读性和安全性。在实际开发中,经常会使用到泛型,但语法非常令人费解,而且容易混淆对于其中的一些细节的地方,所以翻书去回顾下。

    泛型类

    首先看一段简单代码:

    public class Pair<T> {
        T first;
        T second;
    
        public Pair(T first, T second) {
            this.first = first;
            this.second = second;
        }
    
        public T getFirst() {
            return first;
        }
    
        public T getSecond() {
            return second;
        }
    }
    

    这就是一个简单的泛型类,T 表示类型参数,泛型就是类型参数化,处理的数据类型不是固定的,而是可以作为参数传入。如下代码所示,对于构造方法 Pair(T first, T second) 既可以传 Integer 类型的参数,也可以传 String 类型的参数:

    Pair<Integer> minmax = new Pair<Integer>(1,100);
    Pair<String> kv = new Pair<String>("name", "老王");
    

    参数类型也可以多种,类 Pair 可以改成如下所示:

    public class Pair<U, V> {
        U first;
        V second;
    
        public Pair(U first, V second) {
            this.first = first;
            this.second = second;
        }
    
        public U getFirst() {
            return first;
        }
    
        public V getSecond() {
            return second;
        }
    }
    

    这样一来,构造方法 Pair(U first, V second) 就可以接收不同类型的参数了,既可以是 Integer,也可以是 String:

    Pair<String> kv = new Pair<String>(1, "老王");
    

    那假如我们不用泛型类,参数类型直接用 Oeject ,其实也可以满足基本的需求,将类 Pair 修改成如下的方式:

    public class Pair {
        Object first;
        Object second;
    
        public Pair(Object first, Object second) {
            this.first = first;
            this.second = second;
        }
    
        public Object getFirst() {
            return first;
        }
    
        public Object getSecond() {
            return second;
        }
    }
    
    Pair minmax = new Pair(1,100);
    Integer min = (Integer)minmax.getFirst();
    Integer max = (Integer)minmax.getSecond();
    

    但是这样会有什么坏处呢,假如我们在写代码的时候,不小心把类型弄错,但是编译器在编译的时候是不会有任何问题,在运行时候,就会抛类型转换异常:

    Pair minmax = new Pair("王二狗",100);
    Integer min = (Integer)minmax.getFirst();//其实这里已经类型转换异常,但是编译器编译的时候不会报错
    Integer max = (Integer)minmax.getSecond();
    

    但是如果使用了泛型的话,就可以避免这类错误,在编译期间,编译器就会报错:

    Pair<String, Integer> pair = new Pair<>("王二狗",1);
    Integer min = minmax.getFirst();//提示编译错误,String 类型,无法转换为 Integer 类型
    Integer max = minmax.getSecond();
    

    总结一下就是:Java 泛型是通过擦除实现的,对于泛型类,Java 编译器会将泛型代码转换为普通的非泛型代码,就像上面的普通 Pair 类代码及其使用代码一样,将类型参数 T 擦除,替换为 Object,插入必要的强制类型转换,泛型的两个好处就是:更好的安全性,更好的可读性。

    泛型接口

    接口也是可以泛型的,比如 Comparator 接口都是泛型的:

    public interface Comparator<T> {
        int compare(T o1, T o2);
    }
    

    那么在实现这两个类的时候,实现方法里面就得指定具体的参数类型:

    public class StringComparator implements Comparator<String> {
        @Override
        public int compare(String o1, String o2) {
            // 业务逻辑...
            return 0;
        }
    }
    

    泛型方法

    除了泛型类,方法也可以是泛型的,而且,一个方法是不是泛型的,与它所在的类是不是泛型没有什么关系。我们看个例子就知道了:

    public static < U, V > Pair<U, V> makePair( U first, V second ){
    	Pair<U, V> pair = new Pair<>( first, second );
    	return(pair);
    }
    

    参数类型的限定

    上界为指定的类

    如果没有参数类型的限定,那么类型参数 T 在擦除的时候,只能把它当作 Object,但 Java 支持限定这个参数的一个上界,也就是说,参数必须为给定的上界类型或其子类型,这个限定是通过 extends 关键字来表示的。例如,上面的 Pair 类,可以定义一个子类NumberPair,限定两个类型参数必须为 Number:

    public class NumberPair<U extends Number, V extends Number> extends Pair<U, V> {
        public NumberPair(U first, V second) {
            super(first, second);
        }
    }
    

    限定类型后,如果类型使用错误,编译器会提示。指定边界后,类型擦除时就不会转换为 Object 了,而是会转换为它的边界类型 Number,这也是容易理解的。

    上界为指定的接口

    上面看到上界是指定的类,当然了,上界也可以是指定的接口,那么类型 T,就必须实现该上界接口,如下所示:

    public static < T extends Comparable<T> > T max( T[] arr ){
    	T max = arr[0];
    	for ( int i = 1; i < arr.length; i++ ){
    		if ( arr[i].compareTo( max ) > 0 ){
    			max = arr[i];
    		}
    	}
    	return(max);
    }
    

    max 方法计算一个泛型数组中的最大值,计算最大值需要进行元素之间的比较,要求元素实现 Comparable 接口,所以给类型参数设置了一个上边界 Comparable, T 必须实现 Comparable 接口。

    上界为其他类型

    上界类型,除了类、接口,也可以是其他类型,举个例子:

    public class DynamicArray<E> {
    
        private static final int DEFAULT_CAPACITY = 10;
        private int size;
        private Object[] elementData;
    
        public DynamicArray() {
            this.elementData = new Object[DEFAULT_CAPACITY];
        }
    
        private void ensureCapacity(int minCapacity) {
            int oldCapacity = elementData.length;
            if (oldCapacity >= minCapacity) {
                return;
            }
            int newCapacity = oldCapacity * 2;
            if (newCapacity < minCapacity) {
                newCapacity = minCapacity;
            }
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
    
        public void add(E e) {
            ensureCapacity(size + 1);
            elementData[size++] = e;
        }
    
        public E get(int index) {
            return (E) elementData[index];
        }
    
        public int size() {
            return size;
        }
    
        public E set(int index, E element) {
            E oldValue = get(index);
            elementData[index] = element;
            return oldValue;
        }
    }
    
    public class DynamicArray<E> {
    	public < T extends E > void addAll( DynamicArray<T> c ){
    		for ( int i = 0; i < c.size; i++ ){
    			// 业务逻辑...
    		}
    	}
    }
    

    E 是 DynamicArray 的类型参数,T 是 addAll 的类型参数,T 的上界限定为 E。

    解析通配符

    有限定通配符

    <? extends E> 表示有限定通配符,匹配 E 或 E 的某个子类型,具体什么子类型是未知的,如:

    public class DynamicArray<E> {
    	public < T extends E > void addAll( DynamicArray<T> c ){
    		for ( int i = 0; i < c.size; i++ ){
    			// 业务逻辑...
    		}
    	}
    }
    

    改成有限定通配符方式:

    public class DynamicArray<E> {
    	public void addAll( DynamicArray<? extends E> c ){
    		for ( int i = 0; i < c.size; i++ ){
    			// 业务逻辑...
    		}
    	}
    }
    

    <T extends E><? extends E> 到底有什么关系?

    1. <T extends E> 用于定义类型参数,它声明了一个类型参数T,可放在泛型类定义中类名后面、泛型方法返回值前面
    2. <? extends E> 用于实例化类型参数,它用于实例化泛型变量中的类型参数,只是这个具体类型是未知的,只知道它是E或E的某个子类型。

    无限定通配符

    形如 DynamicArray<? > ,称为无限定通配符,举个例子,在 DynamicArray 中查找指定的元素:

    public static int indexOf(DynamicArray<?> arr, Object elm){
    	for (int i=0; i<arr.size(); i++){
    		if(arr.get(i).equals(elm)){
    			return i;
    		}
    	}
    	return -1;
    }
    

    无限定通配符,也可以改成类型参数,如下,两者写法是等价的:

    public static <T> int indexOf(DynamicArray<T> arr, Object elm){
    	for (int i=0; i<arr.size(); i++){
    		if(arr.get(i).equals(elm)){
    			return i;
    		}
    	}
    	return -1;
    }
    

    但是通配符形式是比较简洁,但是有一个重要的限制:只能读,不能写,如下所示:

    DynamicArray<Integer> ints = new DynamicArray<>();
    DynamicArray<? extends Number> numbers = ints;
    Integer a = 200;
    numbers.add(a);//错误!
    numbers.add((Number)a);//错误!
    numbers.add((Object)a);//错误!
    

    因为 ?问号就是表示类型安全无知, ? extends Number 表示是Number的某个子类型,但不知道具体子类型,如果允许写入,Java 就无法确保类型安全性,所以干脆禁止。现在我们再来看泛型方法到底应该用通配符的形式还是加类型参数。两者到底有什么关系?我们总结如下:

    1. 通配符形式都可以用类型参数的形式来替代,通配符能做的,用类型参数都能做。
    2. 通配符形式可以减少类型参数,形式上往往更为简单,可读性也更好,所以,能用通配符的就用通配符。
    3. 如果类型参数之间有依赖关系,或者返回值依赖类型参数,或者需要写操作,则只能用类型参数。
    4. 通配符形式和类型参数往往配合使用。

    超类型通配符

    <? super E> ,称为超类型通配符,表示E的某个父类型,它与 <? extends E> 正好相反,有了它,可以灵活的进行写入。我们给 DynamicArray 添加一个方法,将当前容器中的元素添加到传入的目标容器中:

    public void copyTo(DynamicArray<E> dest){
    	for (int i=0; i<size; i++){
    		dest.add(get(i));
    	}
    }
    
    DynamicArray<Integer> ints = new DynamicArray<Integer>();
    ints.add(100);
    ints.add(34);
    DynamicArray<Number> numbers = new DynamicArray<Number>();
    ints.copyTo(numbers);
    

    Integer 是 Number 的子类,将 Integer 对象拷贝入 Number 容器,这种用法应该是合情合理的,但 Java会 提示编译错误,理由我们之前也说过了,期望的参数类型是 DynamicArray<Number> , DynamicArray<Integer> 并不适用。这里使用超类型通配符就可以解决这个问题:

    public void copyTo(DynamicArray<? super E> dest){
    	for (int i=0; i<size; i++){
    		dest.add(get(i));
    	}
    }
    

    这样,编译器就不会报错了,所以总结一下:

    1. <? super E> 用于灵活写入或比较,使得对象可以写入父类型的容器,使得父类型的比较方法可以应用于子类对象,它不能被类型参数形式替代。
    2. <? ><? extends E> 用于灵活读取,使得方法可以读取E或E的任意子类型的容器对象,它们可以用类型参数的形式替代,但通配符形式更为简洁。

    局限性

    • 基本类型不能用于实例化类型参数
      • Pair<int> minmax = new Pair<int>(1,100); 是不支持的,解决方法是使用基本类型对应的包装类。
        • Pair<Integer> minmax = new Pair<Integer>(1,100);
    • 运行时类型信息不适用于泛型
      • Pair<Integer>.class 不支持
      • if(p1 instanceof Pair<Integer>) 不支持
        • if(p1 instanceof Pair<? >) 支持
    • 类型擦除可能会引发一些冲突
    • 不能通过类型参数创建对象
      • T elm = new T(); 不支持,但是可以借助反射机制实现:
    public static <T> T create(Class<T> type){
    	try {
    		return type.newInstance();
    	}
    	catch (Exception e) {
    		return null;
    	}
    }
    
    • 泛型类类型参数不能用于静态变量和方法,如下则是非法的:
    public class Singleton<T> {
    	private static T instance;
    	public synchronized static T getInstance(){
    		if(instance==null){
    			//创建实例
    		}
    		return instance;
    	}
    }
    
    • Java 不支持创建泛型数组
    • Java中还支持多个上界,多个上界之间以&分隔,类似这样:
      • T extends Base & Comparable & Serializable
      • Base 为上界类,Comparable 和 Serializable 为上界接口。如果有上界类,类应该放在第一个,当类型擦除时,会用第一个上界替换。
    ]]>
    2020-10-09T09:01:46+00:00