注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

天涯论坛倒闭,我给天涯续一秒

时代抛弃你,连句招呼都不会打 "时代抛弃你,甚至连句招呼都不会打",成立差不多23年,承载无数人青春回忆的社区就这样悄无声息的落幕了。在那个没有微博抖音的年代,天涯可谓是神一般的存在,当时还没有网络实名制,因此内容包罗万象五花八门,各路大神层出不穷,这里有:盗...
继续阅读 »

时代抛弃你,连句招呼都不会打


"时代抛弃你,甚至连句招呼都不会打",成立差不多23年,承载无数人青春回忆的社区就这样悄无声息的落幕了。在那个没有微博抖音的年代,天涯可谓是神一般的存在,当时还没有网络实名制,因此内容包罗万象五花八门,各路大神层出不穷,这里有:盗走麻花腾qq的黑客大神、高深莫测的民生探讨、波诡云谲的国际形势分析、最前沿最野的明星八卦、惊悚刺激的怪力乱神、脑洞大开的奇人异事 等等,让人眼花缭乱。甚至还有教你在家里养一只活生生的灵宠(见下文玄学类) 


今年4月初,天涯官微发布公告,因技术升级和数据重构,暂时无法访问。可直到现在,网站还是打不开。虽然后来,官微略带戏谑和无奈地表示:“我会回来的”。但其糟糕的财务状况预示着,这次很可能真是,咫尺天涯,永不再见了。 



神奇的天涯


当时还在读大一时候就接触到了 天涯,还记得特别喜欢逛的板块是 "莲蓬鬼话"、"天涯国际"。莲蓬鬼话老用户都知道,主要是一些真真假假的怪力乱神的惊险刺激的事情,比如 有名的双鱼玉佩,还有一些擦边的玩意,比如《风雪漫千山人之大欲》,懂得都懂,这些都在 pdf里面自取😁;天涯国际主要是各路大佬分析国际局势,每每看完总有种感觉 "原来在下一盘大棋",还有各种人生经验 比如kk大神对房产的预测,现在看到貌似还是挺准的。还有教你在家里养一只活生生的灵宠,神奇吧。 总共200+篇,这里先做下简单介绍





关注公众号,回复 「天涯」 海量社区经典文章双手奉上,感受一下昔日论坛的繁华



历史人文类


功底深厚,博古通今,引人入胜,实打实的的拓宽的你的知识面

  • (长篇)女性秘史◆那些风华绝代、风情万种的女人,为你打开女人的所有秘密.pdf
  • 办公室实用暴力美学——用《资治通鉴》的智慧打造职场金饭碗.pdf
  • 《二战秘史》——纵论二战全史——邀你一起与真相贴身肉搏.pdf
  • 不被理解的mzd(密码是123).zip
  • 地缘看世界——欧洲部分-温骏轩.pdf
  • 宝钗比黛玉大八岁!重解红楼全部诗词!血泪文字逐段解释!所有谜团完整公开!.pdf
  • 现代金融经济的眼重看历史-谁是谁非任评说.pdf
  • 蒋介石为什么失掉大陆:1945——1949-flp713.pdf

人生箴言类


开挂一般的人生,有的应该是体制内大佬闲来灌水,那时上网还无需实名

  • 职业如何规划?大城市,小城市,如何抉择?我来说说我的个人经历和思考-鸟树下睡懒觉的猪.pdf
  • kk所有内容合集(506页).pdf
  • 一个潜水多年的体制内的生意人来实际谈谈老百姓该怎么办?.pdf
  • 三年挣850万,你也可以复制!现在新书已出版,书名《我把一切告诉你》.pdf
  • 互联网“裁员”大潮将起:离开的不只是马云 可能还有你.pdf
  • 大鹏金翅明王合集.pdf
  • 解密社会中升官发财的经济学规律-屠龙有术.pdf

房产金融


上帝视角,感觉有的可能是参与制定的人

  • 从身边最简单的经济现象判断房价走势-招招是道.pdf
  • 大道至简,金融战并不复杂。道理和在县城开一个赌场一样。容我慢慢道来-战略定力.pdf
  • 沉浮房产十余载,谈房市心得.pdf
  • 现代金融的本质以及房价-curgdbd.pdf
  • 对当前房地产形势的判断和对一些现象的解释-loujinjing.pdf
  • 中国VS美国:决定世界命运的博弈 -不要二分法 .pdf
  • 大江论经济-大江宁静.pdf
  • 形势转变中,未来哪些行业有前景.pdf
  • 把握经济大势和个人财运必须读懂钱-现代金钱的魔幻之力.pdf
  • 烽烟四起,中美对决.pdf
  • 赚未来十年的钱-王海滨.pdf
  • 一个炒房人的终极预测——调控将撤底失败.pdf

故事连载小说类


小说爱好者的天堂,精彩绝伦不容错过

  • 人之大欲,那些房中术-风雪漫千山.pdf
  • 冒死记录中国神秘事件(真全本).pdf 五星推荐非常精彩
  • 六相十年浩劫中的灵异往事,颍水尸媾,太湖獭淫,开封鬼谷,山东杀坑-御风楼主人.pdf
  • 《内参记者》一名“非传统”记者颠覆你三观的采访实录-有骨难画.pdf
  • 中国式骗局大全-我是骗子他祖宗.pdf
  • 我是一名警察,说说我多年来破案遇到的灵异事件.pdf
  • 一个十年检察官所经历的无数奇葩案件.pdf
  • 宜昌鬼事 (三峡地区巫鬼轶事记录整理).pdf
  • 南韩往事——华人黑帮回忆录.pdf
  • 惊悚灵异《青囊尸衣》(斑竹推荐)-鲁班尺.pdf
  • 李幺傻江湖故事之《戚绝书》(那些湮没在岁月深处的江湖往事)-我是骗子他祖宗.pdf
  • 闲来8一下自己幽暗的成长经历-风雪漫千山.pdf
  • 阴阳眼(1976年江汉轶事).pdf
  • 民调局异闻录-儿东水寿.pdf
  • 我当道士那些年.pdf
  • 目睹殡仪馆之奇闻怪事.pdf

玄学类


怪力乱神,玄之又玄,虽然已经要求建国后不许成精了

  • 请块所谓的“开光”玉,不如养活的灵宠!.pdf
  • 写在脸上的风水-禅海商道.pdf
  • 谶纬、民谣、推背图-大江宁静.pdf
  • 拨开迷雾看未来.pdf
  • 改过命的玄教弟子帮你断别你的网名吉凶-大雨小水.pdf

天涯的败落


内容社区赚钱,首先还是得有人气,这是互联网商业模式的基础。天涯在PC互联网时代,依靠第一节说的几点因素,持续快速的吸引到用户,互联网热潮,吸引了大量的资本进入,作为有超高流量的天涯社区,自然也获得了资本的青睐。营收这块,主要分为两个部分:网络广告营销业务和互联网增值业务收入。广告的话,最大的广告主是百度,百度在2015年前5个月为天涯社区贡献了476万元,占总收入的比重达11.24%;百度在2014年为天涯社区贡献收入1328万元,占比12.76%。广告收入严重依赖于流量,天涯为了获得广告营收,大幅在社区内植入广告位,影响了用户体验,很有竭泽而渔的感觉。 但是在进入移动互联网时代没跟上时代步伐, 

2010年底,智能手机的出货量开始超过PC,另外,移动互联网走的是深度垂直创新,天涯还是大而全的综合社区模式,加上运营也不是很高明,一两个没工资的版主,肯定打不过别人公司化的运作,可以看到在细分领域被逐步蚕食:

  • 新闻娱乐,被**「微博、抖音」**抢走;
  • 职场天地,被**「Boss直聘」**抢走;
  • 跳蚤市场,被**「闲鱼、转转」**抢走;
  • 音乐交友,被**「网易云、qq音乐」**抢走;
  • 女性兴趣,被**「小红书」**抢走,等等

强如百度在移动互联网没占到优势,一直蛰伏到现在,在BAT中名存实亡,何况天涯,所以也能理解吧。"海内存知己,天涯若比邻",来到2023年,恐怕只剩物是人非,变成一个被遗忘的角落,一段被尘封的回忆罢了,期待天涯能够度过难关再度重来吧。


作者:Android茶话会
链接:https://juejin.cn/post/7245594280303165497
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

介绍一款CPP代码bug检测神器

最近使用C++开发的比较多,对于C++开发来说,内存管理是一个绕不开的永恒话题,因此在使用C++特别是指针时经常是慎之又慎, 最怕一不小心就给自己挖了一个坑,刚好最近发现了一个特别好用的C++静态代码分析利器,借助这个强大的分析工具,我们可以很好地将一些空指针...
继续阅读 »

最近使用C++开发的比较多,对于C++开发来说,内存管理是一个绕不开的永恒话题,因此在使用C++特别是指针时经常是慎之又慎,
最怕一不小心就给自己挖了一个坑,刚好最近发现了一个特别好用的C++静态代码分析利器,借助这个强大的分析工具,我们可以很好地将一些空指针,
数组越界等一些常见的bug扼杀在萌芽阶段,正所谓独乐了不如众乐乐,特将这个利器分享给大家。


这个利器就是cppcheck,它的官网是:cppcheck.sourceforge.io/


同时它还提供了在线分析的功能:cppcheck.sourceforge.io/demo/


在这个在线分析工具中,我们只需要将我们需要检测的代码拷贝粘贴到输入框,然后点击Check按钮即可进行代码分析。


当然啦,这个在线分析还是有很多不足的,比如最多只支持1024个字符,无法在线分析多文件,大项目等,如果要分析长文件,甚至是大项目,那就得安装本地使用啦,
下面我们就以CLion为例,简单介绍下cppcheck的安装和使用。


插件cppcheck的安装


首先这个强大的分析工具是有一个CLion插件的,它的连接是:plugins.jetbrains.com/plugin/8143…


我们可以直接在这个地址上进行在线安装,也可以在CLion的插件市场中搜索cppcheck进行安装。


需要注意的是这个插件仅仅是为了在CLion中自动帮我们分析项目代码,它是不包含cppcheck功能的,也就是要让这个插件正常工作,我们还得
手动安装cppcheck,然后在CLion配置好cppcheck的可执行文件的路径才行。


关于这个cppcheck核心功能的安装官网已经说得很清楚,也就是一句命令行的事情。


比如Debian系统可以通过一下命令安装:

sudo apt-get install cppcheck

Fedora的系统可以通过以下命令安装:

sudo yum install cppcheck

至于Mac系统,那肯定就是用神器包管理工具Homebrew进行安装啦:

brew install cppcheck

CLion插件配置cppcheck路径


安装好cppcheck核心工具包和CLion的cppcheck插件工具之后,我们只需要在CLion中配置一下cppcheck工具包的安装路径就可以正常使用啦。


以笔者的Mac系统的CLion为例子,打开CLion后,点击CLion-> Settings -> Other Settings -> Cppcheck Configuration



在弹出框中设置好cppcheck安装包的绝对路径即可。


如果你是使用Homebrew安装的话可以通过命令brew info cppcheck查找到cppcheck安装的具体路径。


功能实测


为了检测cppcheck这个分析工具的功能,我们新建了一个工程,输入以下代码:

void foo(int x)
{
int buf[10];
if (x == 1000)
buf[x] = 0; // <- ERROR
}

int main() {
int y[1];
y[2] = 1;
return 0;
}

当我们没有安装cppcheck插件时,它是这样子的,看起来没什么问题:



当我们安装了cppcheck插件之后,对于可能会发生潜在的空指针、数组越界、除数为0等等可能导致bug的地方会有高亮提示,
给人一看就有问题的感觉:



当然啦,cppcheck的功能远比这个这个例子所展示的强大,更多惊喜欢迎大家使用体验。


工具是智慧的延伸,在开发的过程选择适合你的工具,可以让我们的工作事半功倍,同行的你如果有好的开发辅助工具欢迎留言分享...


系统话学习博文推荐


音视频入门基础

C++进阶

NDK学习入门

安卓camera应用开发

ffmpeg系列

Opengl入门进阶

webRTC


关注我,一起进步,有全量音视频开发进阶路径、资料、踩坑记等你来学习...


作者:思想觉悟
链接:https://juejin.cn/post/7270208019388350524
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

你知道什么是SaaS吗?

天天听SaaS,相信大家都知道什么叫SaaS系统!这不?领导安排下来了任务,说要去做SaaS系统,作为小白的我赶紧去看看什么是SaaS,大概收集整理(并非原创)了这部分内容,分享给大家。相信大家看了也会有很多收获。 本文从以下几个方面对SaaS系统召开介绍...
继续阅读 »

天天听SaaS,相信大家都知道什么叫SaaS系统!这不?领导安排下来了任务,说要去做SaaS系统,作为小白的我赶紧去看看什么是SaaS,大概收集整理(并非原创)了这部分内容,分享给大家。相信大家看了也会有很多收获。


  本文从以下几个方面对SaaS系统召开介绍:


  1. 云服务架构的三个概念

  2. SaaS系统的两大特征

  3. SaaS服务与传统服务、互联网服务的区别

  4. B2B2C

  5. SaaS系统的分类

  6. 如何SaaS化

  7. SaaS产品的核心组件

  8. SaaS多租户


一、云服务架构的三个概念


1.1 PaaS


英文就是 Platform-as-a-Service(平台即服务)


PaaS,某些时候也叫做中间件。就是把客户采用提供的开发语言和工具(例如Java,python, .Net等)开发的或收购的应用程序部署到供应商的云计算基础设施上去。
客户不需要管理或控制底层的云基础设施,包括网络、服务器、操作系统、存储等,但客户能控制部署的应用程序,也可能控制运行应用程序的托管环境配置。


PaaS 在网上提供各种开发和分发应用的解决方案,比如虚拟服务器和特定的操作系统。底层的平台3/4帮你铺建好了,你只需要开发自己的上层应用。这即节省了你在硬件上的费用,也让各类应用的开发更加便捷,不同的工作互相打通也变得容易,因为在同一平台上遵循的是同样的编程语言、协议和底层代码。


1.2 IaaS


英文就是 Infrastructure-as-a-Service(基础设施即服务)


IaaS 提供给消费者的服务是对所有计算基础设施的利用,包括处理 CPU、内存、存储、网络和其它基本的计算资源,用户能够部署和运行任意软件,包括操作系统和应用程序。
消费者不管理或控制任何云计算基础设施,但能控制操作系统的选择、存储空间、部署的应用,也有可能获得有限制的网络组件(例如路由器、防火墙、负载均衡器等)的控制。


IaaS 会提供场外服务器,存储和网络硬件,你可以租用。节省了维护成本和办公场地,公司可以在任何时候利用这些硬件来运行其应用。我们最熟悉的IaaS服务是我们服务器托管业务,多数的IDC都提供这样的服务,用户自己不想要再采购价格昂贵的服务器和磁盘阵列了,所有的硬件都由 IaaS 提供,你还能获得品质更高的网络资源。


1.3 SaaS


英文就是 Software-as-a-Service(软件即服务)


SaaS提供给客户的服务是运行在云计算基础设施上的应用程序,用户可以在各种设备上通过客户端界面访问,如浏览器。
消费者不需要管理或控制任何云计算基础设施,包括网络、服务器、操作系统、存储等等。


SaaS 与我们普通使用者联系可能是最直接的,简单地说任何一个远程服务器上的应用都可以通过网络来运行,就是SaaS了。国内的互联网巨头竭力推荐的 SaaS 应用想必大家已经耳熟能详了,比如阿里的钉钉,腾讯的企业微信,这些软件里面应用平台上的可供使用的各类SaaS小软件数不胜数,从OA,到ERP到CRM等等,涵盖了企业运行所需的几乎所用应用。


二、SaaS系统的两大特征



  1. 部署在供应商的服务器上,而不是部署在甲方的服务器上。

  2. 订购模式,服务商提供大量功能供客户选择,客户可以选择自己需要的进行组合,支付所需的价格,并支持按服务时间付费。


三、SaaS服务与传统服务、互联网服务的区别


3.1 SaaS服务


介于传统与互联网之间,通过租用的方式提供服务,服务部署在云端,任何用户通过注册后进行订购后获得需要的服务,可以理解成服务器及软件归供应商所有,用户通过付费获得使用权
image.png


3.2 传统软件


出售软件及配套设备,将软件部署在客户服务器或客户指定云服务器,出售的软件系统及运维服务为盈利来
image.png


3.3 互联网应用供应商


服务器部署在云端,所有用户可以通过客户端注册进行使用,广告及付费增值服务作为盈利来源
image.png


四、B2B2C


SaaS作为租户系统,需要为租户(C端)提供注册、购买、业务系统的入口,还得为B端(运营/运维)提供租户管理、流量监控、服务状态监控运维入口


五、SaaS系统的分类


5.1 业务型SaaS


定义:为客户的赚钱业务提供工具以及服务的SaaS,直面的是用户的生意,例如有赞微盟等电商SaaS以及销售CRM工具,为B2B2C企业;


架构以及商业模式:在产品的成长期阶段,为了扩充业务规模和体量,业务SaaS产品会拓展为“多场景+多行业”的产品模式,为不同行业或者不同场景提供适应的解决方案,例如做电商独立站的有赞,后期发展为“商城、零售、美业、教育”多行业的解决方案进行售卖。
image.png


5.2 效率型SaaS


定义:为客户效率提升工具的SaaS,如项目管理工具、Zoom等会议工具,提升办公或者生产效率,为B2B企业;


架构以及商业模式:不同于业务型的SaaS,效率SaaS思考得更多的是企业内存在一个大共性的效率的问题,不同的企业对于CRM销售系统的需求是不一样的,但都需要一个协同办公的产品来提升协作效率。对于效率类SaaS来说,从哪来到哪去是非常清晰的,就是要解决优化或者解决一个流程上的问题。
image.png


5.3 混合型SaaS


定义:即兼顾企业业务和效率效用SaaS,例如近几年在私域流量上大做文章的企业微信,其本身就是一个办公协同工具,但为企业提供了一整套的私域管理能力,实现业务的提升,同时也支持第三方服务。


架构以及商业模式:混合SaaS是业务和效率SaaS的结合体,负责企业业务以及企业管理流程的某类场景上的降本增效;因混合SaaS核心业务的使用场景是清晰且通用的,非核心业务是近似于锦上添花的存在,所以在中台产品架构上更接近为“1+X”组合方式——即1个核心业务+X个非核心功能,两者在产品层级上是属于同一层级的。
image.png


六、如何SaaS化



  1. 进行云化部署,性能升级,能够支持更大规模的用户访问

  2. 用户系统改造,支持2C用户登录(手机号一键登录、小程序登录、短信验证码登录)

  3. 网关服务,限流,接口防篡改等等

  4. 租户系统开发,包含租户基础信息管理、租户绑定资源(订购的功能)、租户服务期限等等

  5. 客户端改造(通常SaaS系统主要提供WEB端服务),页面权限控制,根据租户系统用户资源提供用户已购买的模块或页面

  6. 官网开发,功能报价单,功能试用、用户选购及支付

  7. 服务端接口数据权限改造、租户级别数据权限


七、SaaS产品的核心组件



  1. 安全组件:在SaaS产品中,系统安全永远是第一位需要考虑的事情

  2. 数据隔离组件:安全组件解决了用户数据安全可靠的问题,但数据往往还需要解决隐私问题,各企业之间的数据必须相互不可见,即相互隔离。

  3. 可配置组件:SaaS产品在设计之初就考虑了大多数通用的功能,让租户开箱即用,但任然有为数不少的租户需要定制服务自身业务需求的配置项,如UI布局、主题、标识(Logo)等信息

  4. 可扩展组件:SaaS产品应该具备水平扩展的能力。如通过网络负载均衡其和容器技术,在多个服务器上部署多个软件运行示例并提供相同的软件服务,以此实现水平扩展SaaS产品的整体服务性能

  5. 0停机时间升级产品:实现在不重启原有应用程序的情况下,完成应用程序的升级修复工作

  6. 多租户组件:SaaS产品需要同时容纳多个租户的数据,同时还需要保证各租户之间的数据不会相互干扰,保证租户中的用户能够按期望索引到正确的数据


八、SaaS多租户


8.1 多租户核心概念



  • 租户:一般指一个企业客户或个人客户,租户之间数据与行为是隔离的

  • 用户:在某个租户内的具体使用者,可以通过使用账户名、密码等登录信息,登录到SaaS系统使用软件服务

  • 组织:如果租户是一个企业客户,通常会拥有自己的组织架构

  • 员工:是指组织内部具体的某位员工。

  • 解决方案:为了解决客户的某类型业务问题,SaaS服务商将产品与服务组合在一起,为商家提供整体的打包方案。

  • 产品能力:指的是SaaS服务商对客户售卖的产品应用,特指能够帮助客户实现端到端场景解决方案闭环的能力。

  • 资源域:用来运行1个或多个产品应用的一套云资源环境

  • 云资源:SaaS产品一般都部署在各种云平台上,例如阿里云、腾讯云、华为云等。对这些云平台提供的计算、存储、网络、容器等资源,抽象为云资源。


8.2 三大模式


8.2.1 竖井隔离模式


image.png



  • 优势:



  1. 满足强隔离需求:一些客户为了系统和数据的安全性,可能提出非常严格的隔离需求,期望软件产品能够部署在一套完全独立的环境中,不和其他租户的应用实例、数据放在一起。

  2. 计费逻辑简单:SaaS服务商需要针对租户使用资源进行计费,对于复杂的业务场景,计算、存储、网络资源间的关系同样也会非常复杂,计费模型是很有挑战的,但在竖井模式下,计费模型相对来说是比较简单的。

  3. 降低故障影响面:因为每个客户的系统都部署在自己的环境中,如果其中一个环境出现故障,并不会影响其他客户使用软件服务。



  • 劣势:



  1. 规模化问题:由于租户的SaaS环境是独立的,所以每入驻一个租户,就需要创建和运营一套SaaS环境,如果只是少量的租户,还可能可以管理,但如果是成千上万的租户,管理和运营这些环境将会是非常大的挑战。

  2. 成本问题:每个租户都有独立的环境,花费在单个客户上的成本将非常高,会大幅削弱SaaS软件服务的盈利能力。

  3. 敏捷迭代问题:SaaS模式的一个优势是能够快速响应市场需求,迭代产品功能。但竖井隔离策略会阻碍这种敏捷迭代能力,因为更新、管理、支撑这些租户的SaaS环境,会变得非常复杂和低效。

  4. 统一管理与监控:在同一套环境中,对部署的基础设施进行管理与监控,是较为简单的。但每个租户都有独立的环境,在这种非中心化的模式下,对每个租户的基础设施进行管理与监控,同样也是非常复杂、困难的。


8.2.2 共享模式


image.png



  • 优势:



  1. 高效管理:在共享策略下,能够集中化地管理、运营所有租户,管理效率非常高。同时,对基础设施配置管理、监控,也将更加容易。相比竖井策略,产品的迭代更新会更快。

  2. 成本低:SaaS服务商的成本结构中,很大一块是基础设施的成本。在共享模型下,服务商可以根据租户们的实际资源负载情况,动态伸缩系统,这样基础设施的利用率将非常高。



  • 劣势:



  1. 租户相互影响:由于所有租户共享一套资源,当其中一个租户大量占用机器资源,其他租户的使用体验很可能受到影响,在这种场景下,需要在技术架构上设计一些限制措施(限流、降级、服务器隔离等),让影响面可控。

  2. 租户计费困难:在竖井模型下,非常容易统计租户的资源消耗。然而,在共享模型下,由于所有租户共享一套资源,需要投入更多的精力统计单个租户的合理费用。


8.2.3 分域隔离模式


image.png


8.3 多租户系统需要具备的能力



  1. 多个租户支持共享一套云资源,如计算、存储、网络资源等。单个租户也可以独占一套云资源。

  2. 多个租户间能够实现数据与行为的隔离,能够对租户进行分权分域控制。

  3. 租户内部能够支持基于组织架构的管理,可以对产品能力进行授权和管理。

  4. 不同的产品能力可以根据客户需求,支持运行在不同的云资源上。


8.4 多租户系统应用架构图


image.png

收起阅读 »

搞明白什么是零拷贝,就是这么简单

我们总会在各种地方看到零拷贝,那零拷贝到底是个什么东西。 接下来,让我们来理一理啊。 拷贝说的是计算机里的 I/O 操作,也就是数据的读写操作。计算机可是一个复杂的家伙,包括软件和硬件两大部分,软件主要指操作系统、驱动程序和应用程序。硬件那就多了,CPU、内存...
继续阅读 »

我们总会在各种地方看到零拷贝,那零拷贝到底是个什么东西。


接下来,让我们来理一理啊。


拷贝说的是计算机里的 I/O 操作,也就是数据的读写操作。计算机可是一个复杂的家伙,包括软件和硬件两大部分,软件主要指操作系统、驱动程序和应用程序。硬件那就多了,CPU、内存、硬盘等等一大堆东西。


这么复杂的设备要进行读写操作,其中繁琐和复杂程度可想而知。


传统I/O的读写过程


如果要了解零拷贝,那就必须要知道一般情况下,计算机是如何读写数据的,我把这种情况称为传统 I/O。


数据读写的发起者是计算机中的应用程序,比如我们常用的浏览器、办公软件、音视频软件等。


而数据的来源呢,一般是硬盘、外部存储设备或者是网络套接字(也就是网络上的数据通过网口+网卡的处理)。


过程本来是很复杂的,所以大学课程里要通过《操作系统》、《计算机组成原理》来专门讲计算机的软硬件。


简化版读操作流程


那么细的没办法讲来,所以,我们把这个读写过程简化一下,忽略大多数细节,只讲流程。



上图是应用程序进行一次读操作的过程。

  1. 应用程序先发起读操作,准备读取数据了;
  2. 内核将数据从硬盘或外部存储读取到内核缓冲区;
  3. 内核将数据从内核缓冲区拷贝到用户缓冲区;
  4. 应用程序读取用户缓冲区的数据进行处理加工;

详细的读写操作流程


下面是一个更详细的 I/O 读写过程。这个图可好用极了,我会借助这个图来厘清 I/O 操作的一些基础但非常重要的概念。



先看一下这个图,上面红粉色部分是读操作,下面蓝色部分是写操作。


如果一下子看着有点儿迷糊的话,没关系,看看下面几个概念就清楚了。


应用程序


就是安装在操作系统上的各种应用。


系统内核


系统内核是一些列计算机的核心资源的集合,不仅包括CPU、总线这些硬件设备,也包括进程管理、文件管理、内存管理、设备驱动、系统调用等一些列功能。


外部存储


外部存储就是指硬盘、U盘等外部存储介质。


内核态

  • 内核态是操作系统内核运行的模式,当操作系统内核执行特权指令时,处于内核态。
  • 在内核态下,操作系统内核拥有最高权限,可以访问计算机的所有硬件资源和敏感数据,执行特权指令,控制系统的整体运行。
  • 内核态提供了操作系统管理和控制计算机硬件的能力,它负责处理系统调用、中断、硬件异常等核心任务。

用户态


这里的用户可以理解为应用程序,这个用户是对于计算机的内核而言的,对于内核来说,系统上的各种应用程序会发出指令来调用内核的资源,这时候,应用程序就是内核的用户。

  • 用户态是应用程序运行的模式,当应用程序执行普通的指令时,处于用户态。
  • 在用户态下,应用程序只能访问自己的内存空间和受限的硬件资源,无法直接访问操作系统的敏感数据或控制计算机的硬件设备。
  • 用户态提供了一种安全的运行环境,确保应用程序之间相互隔离,防止恶意程序对系统造成影响。

模式切换


计算机为了安全性考虑,区分了内核态和用户态,应用程序不能直接调用内核资源,必须要切换到内核态之后,让内核来调用,内核调用完资源,再返回给应用程序,这个时候,系统在切换会用户态,应用程序在用户态下才能处理数据。


上述过程其实一次读和一次写都分别发生了两次模式切换。



内核缓冲区


内核缓冲区指内存中专门用来给内核直接使用的内存空间。可以把它理解为应用程序和外部存储进行数据交互的一个中间介质。


应用程序想要读外部数据,要从这里读。应用程序想要写入外部存储,要通过内核缓冲区。


用户缓冲区


用户缓冲区可以理解为应用程序可以直接读写的内存空间。因为应用程序没法直接到内核读写数据, 所以应用程序想要处理数据,必须先通过用户缓冲区。


磁盘缓冲区


磁盘缓冲区是计算机内存中用于暂存从磁盘读取的数据或将数据写入磁盘之前的临时存储区域。它是一种优化磁盘 I/O 操作的机制,通过利用内存的快速访问速度,减少对慢速磁盘的频繁访问,提高数据读取和写入的性能和效率。


PageCache

  • PageCache 是 Linux 内核对文件系统进行缓存的一种机制。它使用空闲内存来缓存从文件系统读取的数据块,加速文件的读取和写入操作。
  • 当应用程序或进程读取文件时,数据会首先从文件系统读取到 PageCache 中。如果之后再次读取相同的数据,就可以直接从 PageCache 中获取,避免了再次访问文件系统。
  • 同样,当应用程序或进程将数据写入文件时,数据会先暂存到 PageCache 中,然后由 Linux 内核异步地将数据写入磁盘,从而提高写入操作的效率。

再说数据读写操作流程


上面弄明白了这几个概念后,再回过头看一下那个流程图,是不是就清楚多了。


读操作
  1. 首先应用程序向内核发起读请求,这时候进行一次模式切换了,从用户态切换到内核态;
  2. 内核向外部存储或网络套接字发起读操作;
  3. 将数据写入磁盘缓冲区;
  4. 系统内核将数据从磁盘缓冲区拷贝到内核缓冲区,顺便再将一份(或者一部分)拷贝到 PageCache;
  5. 内核将数据拷贝到用户缓冲区,供应用程序处理。此时又进行一次模态切换,从内核态切换回用户态;

写操作
  1. 应用程序向内核发起写请求,这时候进行一次模式切换了,从用户态切换到内核态;
  2. 内核将要写入的数据从用户缓冲区拷贝到 PageCache,同时将数据拷贝到内核缓冲区;
  3. 然后内核将数据写入到磁盘缓冲区,从而写入磁盘,或者直接写入网络套接字。

瓶颈在哪里


但是传统I/O有它的瓶颈,这才是零拷贝技术出现的缘由。瓶颈是啥呢,当然是性能问题,太慢了。尤其是在高并发场景下,I/O性能经常会卡脖子。


那是什么地方耗时了呢?


数据拷贝


在传统 I/O 中,数据的传输通常涉及多次数据拷贝。数据需要从应用程序的用户缓冲区复制到内核缓冲区,然后再从内核缓冲区复制到设备或网络缓冲区。这些数据拷贝过程导致了多次内存访问和数据复制,消耗了大量的 CPU 时间和内存带宽。


用户态和内核态的切换


由于数据要经过内核缓冲区,导致数据在用户态和内核态之间来回切换,切换过程中会有上下文的切换,如此一来,大大增加了处理数据的复杂性和时间开销。


每一次操作耗费的时间虽然很小,但是当并发量高了以后,积少成多,也是不小的开销。所以要提高性能、减少开销就要从以上两个问题下手了。


这时候,零拷贝技术就出来解决问题了。


什么是零拷贝


问题出来数据拷贝和模态切换上。


但既然是 I/O 操作,不可能没有数据拷贝的,只能减少拷贝的次数,还有就是尽量将数据存储在离应用程序(用户缓冲区)更近的地方。


而区分用户态和内核态有其他更重要的原因,不可能单纯为了 I/O 效率就改变这种设计吧。那也只能尽量减少切换的次数。


零拷贝的理想状态就是操作数据不用拷贝,但是显示情况下并不一定真的就是一次复制操作都没有,而是尽量减少拷贝操作的次数。


要实现零拷贝,应该从下面这三个方面入手:

  1. 尽量减少数据在各个存储区域的复制操作,例如从磁盘缓冲区到内核缓冲区等;
  2. 尽量减少用户态和内核态的切换次数及上下文切换;
  3. 使用一些优化手段,例如对需要操作的数据先缓存起来,内核中的 PageCache 就是这个作用;

实现零拷贝方案


直接内存访问(DMA)


DMA 是一种硬件特性,允许外设(如网络适配器、磁盘控制器等)直接访问系统内存,而无需通过 CPU 的介入。在数据传输时,DMA 可以直接将数据从内存传输到外设,或者从外设传输数据到内存,避免了数据在用户态和内核态之间的多次拷贝。




如上图所示,内核将数据读取的大部分数据读取操作都交个了 DMA 控制器,而空出来的资源就可以去处理其他的任务了。


sendfile


一些操作系统(例如 Linux)提供了特殊的系统调用,如 sendfile,在网络传输文件时实现零拷贝。通过 sendfile,应用程序可以直接将文件数据从文件系统传输到网络套接字或者目标文件,而无需经过用户缓冲区和内核缓冲区。


如果不用sendfile,如果将A文件写入B文件。



  1. 需要先将A文件的数据拷贝到内核缓冲区,再从内核缓冲区拷贝到用户缓冲区;

  2. 然后内核再将用户缓冲区的数据拷贝到内核缓冲区,之后才能写入到B文件;


而用了sendfile,用户缓冲区和内核缓冲区的拷贝都不用了,节省了一大部分的开销。


共享内存


使用共享内存技术,应用程序和内核可以共享同一块内存区域,避免在用户态和内核态之间进行数据拷贝。应用程序可以直接将数据写入共享内存,然后内核可以直接从共享内存中读取数据进行传输,或者反之。



通过共享一块儿内存区域,实现数据的共享。就像程序中的引用对象一样,实际上就是一个指针、一个地址。


内存映射文件(Memory-mapped Files)


内存映射文件直接将磁盘文件映射到应用程序的地址空间,使得应用程序可以直接在内存中读取和写入文件数据,这样一来,对映射内容的修改就是直接的反应到实际的文件中。


当文件数据需要传输时,内核可以直接从内存映射区域读取数据进行传输,避免了数据在用户态和内核态之间的额外拷贝。


虽然看上去感觉和共享内存没什么差别,但是两者的实现方式完全不同,一个是共享地址,一个是映射文件内容。


Java 实现零拷贝的方式


Java 标准的 IO 库是没有零拷贝方式的实现的,标准IO就相当于上面所说的传统模式。只是在 Java 推出的 NIO 中,才包含了一套新的 I/O 类,如 ByteBufferChannel,它们可以在一定程度上实现零拷贝。


ByteBuffer:可以直接操作字节数据,避免了数据在用户态和内核态之间的复制。


Channel:支持直接将数据从文件通道或网络通道传输到另一个通道,实现文件和网络的零拷贝传输。


借助这两种对象,结合 NIO 中的API,我们就能在 Java 中实现零拷贝了。


首先我们先用传统 IO 写一个方法,用来和后面的 NIO 作对比,这个程序的目的很简单,就是将一个100M左右的PDF文件从一个目录拷贝到另一个目录。

public static void ioCopy() {
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);
try (FileInputStream fis = new FileInputStream(sourceFile);
FileOutputStream fos = new FileOutputStream(targetFile)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
}
System.out.println("传输 " + formatFileSize(sourceFile.length()) + " 字节到目标文件");
} catch (IOException e) {
e.printStackTrace();
}
}

下面是这个拷贝程序的执行结果,109.92M,耗时1.29秒。



传输 109.92 M 字节到目标文件
耗时: 1.290 秒



FileChannel.transferTo() 和 transferFrom()


FileChannel 是一个用于文件读写、映射和操作的通道,同时它在并发环境下是线程安全的,基于 FileInputStream、FileOutputStream 或者 RandomAccessFile 的 getChannel() 方法可以创建并打开一个文件通道。FileChannel 定义了 transferFrom() 和 transferTo() 两个抽象方法,它通过在通道和通道之间建立连接实现数据传输的。


这两个方法首选用 sendfile 方式,只要当前操作系统支持,就用 sendfile,例如Linux或MacOS。如果系统不支持,例如windows,则采用内存映射文件的方式实现。


transferTo()


下面是一个 transferTo 的例子,仍然是拷贝那个100M左右的 PDF,我的系统是 MacOS。

public static void nioTransferTo() {
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);
try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel();
FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) {
long transferredBytes = sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);

System.out.println("传输 " + formatFileSize(transferredBytes) + " 字节到目标文件");
}
} catch (IOException e) {
e.printStackTrace();
}
}

只耗时0.536秒,快了一倍。



传输 109.92 M 字节到目标文件
耗时: 0.536 秒



transferFrom()


下面是一个 transferFrom 的例子,仍然是拷贝那个100M左右的 PDF,我的系统是 MacOS。

public static void nioTransferFrom() {
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);

try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel();
FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) {
long transferredBytes = targetChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
System.out.println("传输 " + formatFileSize(transferredBytes) + " 字节到目标文件");
}
} catch (IOException e) {
e.printStackTrace();
}
}

执行时间:



传输 109.92 M 字节到目标文件
耗时: 0.603 秒



Memory-Mapped Files


Java 的 NIO 也支持内存映射文件(Memory-mapped Files),通过 FileChannel.map() 实现。


下面是一个 FileChannel.map()的例子,仍然是拷贝那个100M左右的 PDF,我的系统是 MacOS。

    public static void nioMap(){
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);

try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel();
FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) {
long fileSize = sourceChannel.size();
MappedByteBuffer buffer = sourceChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileSize);
targetChannel.write(buffer);
System.out.println("传输 " + formatFileSize(fileSize) + " 字节到目标文件");
}
} catch (IOException e) {
e.printStackTrace();
}
}

执行时间:



传输 109.92 M 字节到目标文件
耗时: 0.663 秒



推荐阅读


我的第一个 Chrome 插件上线了,欢迎试用!


前端同事最讨厌的后端行为,看看你中了没有


RPC框架的核心到底是什么


作者:古时的风筝
链接:https://juejin.cn/post/7270519061207908352
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

开发工具 2.0 的时代已经来临

AI 正在变革软件工程:开发工具 2.0 时代 生成式 AI 的爆发已经开始改变了很多行业的工作方式,但对于软件工程来说,转型才刚刚开始。 从 Copilot 说起 Github Copilot 的成功引发了一场 AI 编程工具的浪潮,《Research: q...
继续阅读 »

AI 正在变革软件工程:开发工具 2.0 时代


生成式 AI 的爆发已经开始改变了很多行业的工作方式,但对于软件工程来说,转型才刚刚开始。


从 Copilot 说起


Github Copilot 的成功引发了一场 AI 编程工具的浪潮,《Research: quantifying GitHub Copilot’s impact on developer productivity and happiness》这份报告研究了 Copilot 对开发者效率和幸福感的提升,如下

  • 使用 GitHub Copilot 的开发人员比不使用 GitHub Copilot 的开发人员完成任务的速度快 55%
  • 使用 GitHub Copilot 的小组完成任务的比例为 78%,而没有使用 Copilot 的小组为 70%
  • 88% 的使用者认为自己生产力提高了
  • 96% 的使用者认为自己处理重复性的工作更快了
  • 88% 的使用者认为自己可以更加专注于更喜欢的工作上了


原文地址:github.blog/2022-09-07-…





从数据上来看,Copilot 已经是非常成功了,我们会认为这已经是一个大的变革,但是当我们把眼光放到整个软件工程行业的时候,才发现 Copilot 可能只是 AI 改变软件工程师工作方式的开端。



我曾经写了一篇 Copilot 的体验文章,有兴趣可以看看 # 与 AI 结对编程,好搭档 Copilot



开发工具 2.0 与现状


红衫资本在《Developer Tools 2.0》中定义了”开发工具 2.0“ :通过 AI 改变软件创造方式的工具。


还整理了一张图用以展示现有的开发工具在不同的软件研发阶段的应用。




这图本质上是一个表格,每一行从左到右代表了软件在当前市场的占用水平,分为

  • Incumbents:当前主流使用的标准工具
  • Challengers:挑战者,一些加入了 AI 特性的创新型工具
  • Dev Tools 2.0:通过 AI 改变软件创造方式的工具

列的话从上到下代表了软件开发的各个阶段,或者说生命周期,分别为

  • Deployment:部署阶段,包括 CI/CD、云、监控等
  • Implementation:实现阶段,包括 code review 工具、文档工具、代码编写维护工具等
  • Setup:配置阶段,包括 IDE、终端、ISSUE 记录工具等

接下来我们从上往下来分析。


Deployment 所属区域中,软件还是集中在 Incumbents(主流) 和 Challengers(挑战者) 中,这里可以看到很多熟悉的产品,比如 Datadog、Grafana、Aws、Jenkins 等。


但 Deployment 目前还没有 Dev Tools 2.0 的工具




Implementation 中,目前已有很多 Dev Tools 2.0 了,比如 AI code review 工具 Codeball、DEBUG 和对话工具 ChatGPT、AI 文档工具 Mintlify、以及 AI 代码补全工具 Copilot 和 Tabnine。


注意看细分的 write docs(文档编写) 和 write & maintain code (代码编写维护)中,在主流中这些都是人力维护,这说明当前的软件工程已经处于一个分水岭了:从人工到 AI。


对比 Deployment 的话,Implementation 的 2.0 工具可谓是百花齐放。




最后就是 Setup 了,目前只有 Cursor (一款集成了 ChatGPT 4 的代码编辑器)被完全定义为 Dev Tools 2.0




这里比较意外的是 warp 和 fig 居然没有被定义为 2.0 工具,因为我前段时间刚试用了 warp 终端,有兴趣的可以看看我发的视频


其实回顾一下红衫资本对 Dev Tools 2.0 的定义就能理解了:通过 AI 改变软件创造方式的工具。


warp 和 fig 只是带了 AI 的特性,还没有改变软件的创造规则,所以就被列入了 challenger 里。


从目前世面上的工具来看,AI 已经有了巨大的机会改变软件工程,并且这是一个关于“谁”,而不是“是与否”的问题。


开发工具 2.0 的共同点


再再再次啰嗦一下红衫资本对 Dev Tools 2.0 的定义:通过 AI 改变软件创造方式的工具。


我考察了 5 个图中被归类为 2.0 的软件,看看它们是如何改变软件的创作方式的



首先是 Cursor,我们可以用自然语言来写新的代码、维护既有代码,从这点来看它是超越了 Copilot (这不是指下一代 Copilot X )。




然后是 Codeball,它主要是用 AI 来自动执行 code review,它可以为每一个 PR 进行评分(检查代码规范、Bug 等)并自动合并,大量节省功能特性因 PR 被 Block 的时间,而且用机器代替人做检查也能避免 Review 成为形式主义的流程。




ChatGPT 此处就不做演示了,直接看一下 Grit 吧。虽然下面展示的动图只是将代码片段的优化,但 Grit 给自己的定位是通过 AI 自动化完成整个项目的代码迁移和升级,比如从 JavaScript 到 TypeScript、自动处理技术债等




最后就是 Adrenaline 了,它是一个 AI Debuger(调试器?),我输入了一段会导致 NullPointerException 的代码,但是因为服务器请求的数量太多无法运行。所以我直接在对话框里问了一句:Is there anything wrong with this code?(这段代码有问题吗?)。Adrenaline 不仅回答了会出问题,还详细分析了这段代码的功能




再来对比一下这几个场景下传统的处理方式



基于以上工具的特点,我们也可以畅想一下 Deployment 2.0 工具的特点

  1. 首先肯定是通过自然语言进行交互,比如:帮我在阿里云上部署一下 xxx 项目;也可以说帮我创建一个项目,这项目叫熔岩巨兽,需要使用到 mysql、redis,需要一个公网域名等…
  2. 然后是能够自动分析并配置项目的依赖,比如:部署 xxx 项目需要 mysql 数据库、redis 缓存
  3. 如果能够为我使用最优(成本、性能等多方面)的解决方案更好

其实随着云平台的成熟、容器化的普及,我相信这样的 Deployment 2.0 工具肯定不会太遥远。


事实上在写这篇文章的时候我就发现了 Github 上的一个项目叫 Aquarium,它已经初步基于 AI 的能力实现了部署,它给 AI 输入了以下的前提提示:



你现在控制着一个Ubuntu Linux服务器。你的目标是运行一个Minecraft服务器。不要回答任何批判、问题或解释。你会发出命令,我会回应当前的终端输出。 回答一个要给服务器的Linux命令。



然后向 AI 输入要执行的部署,比如:”Your goal is to run a minecraft server“。


接着 AI 就会不断的输出命令,Aquarium 负责在程序执行命令并将执行结果返回给 AI,,不断重复这个过程直到部署结束。


对开发者的影响


作为一名软件开发者,我们经常会自嘲为 CV 工程师,CV 代表了 ctrl + cctral + v ,即复制粘贴工程师。


这是因为大多数的代码都是通过搜索引擎查询获得,开发者可以直接复制、粘贴、运行,如果运行失败就把错误信息放进搜索引擎再次搜索,接着又复制、粘贴、运行……


但基于开发工具 2.0,这个流程就产生了变化:搜索、寻找答案、检查答案的过程变成了询问、检查答案,直接省去了最费时间的寻找答案的过程。




还有就是开发模式的改变,以前是理解上游的需求并手写代码,而现在是理解上游的需求并用自然语言描述需求,由 AI 写代码。


也就是说在代码上的关注会降低,需要将更多的注意力集中在需求上




也许你发现了,其实可以直接从产品到 AI,因为程序员极有可能是在重复的描述产品需求。


这个问题其实可以更大胆一点假设:如果 AI 可以根据输入直接获得期望的输出,那么老板可以直接对接 AI 了,80% 的业务人员都不需要。


既然已经谈到了对”人“的影响,那不如就接着说两点吧

  • 这些工具会让高级开发者的技能经验价值打折扣,高级和初级的编码能力会趋于拟合,因为每个人都拥有一个收集了全人类知识集的 AI 助手
  • 会编程的人多了,但是适合以编程为工作的人少了

很多开发者对此产生焦虑,其实也不必,因为这是时代的趋势,淹没的也不止你一个,浪潮之下顺势而为指不定也是一个机遇。


如果光看软件工具 2.0,它给软件工程带来的是一次转型,是一次人效的变革,目前来看还没有达到对软件工程的颠覆,那什么时候会被颠覆呢?



有一天有一个这样的游戏出现了,每个人在里面都是独一无二的,系统会为每个人的每个行为动态生成接下来的剧情走向,也就是说这个游戏的代码是在动态生成,并且是为每一个人动态生成。这个游戏的内存、存储空间等硬件条件也是动态在增加。 这就是地球 Online



短期来看,AI 还不会代替程序员,但会替代不会用 AI 的程序员。


AI 正在吞噬软件


最后就用两位大佬的话来结束本文吧。


原 Netscape(网景公司)创始人 Marc Andreessen 说过一句经典的话:软件正在吞噬世界。


人工智能领域知名科学家 Andrej Karpathy 在 2017 年为上面的话做了补充:软件(1.0)正在吞噬世界,现在人工智能(软件2.0)正在吞噬软件



Software (1.0) is eating the world, and now AI (Software 2.0) is eating software.



所以,你准备好了吗?


参考

  1. http://www.sequoiacap.com/article/ai-…
  2. karpathy.medium.com/software-2-…
  3. github.blog/2022-09-07-…
  4. github.com/fafrd/aquar…

作者:vran
链接:https://juejin.cn/post/7215608036394647612
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

继copilot之后,又一款免费帮你写代码的插件

写在前面 在之前的文章中推荐过一款你写注释,它就能帮你写代码的插件copilot。 copilot写代码的能力没得说,但是呢copilot试用没几天之后就收费了。 按理说这么好用,又可以提高效率的工具,收点费也理所当然 但是秉承白嫖一时爽,一直白嫖一直爽的原则...
继续阅读 »

写在前面


在之前的文章中推荐过一款你写注释,它就能帮你写代码的插件copilot。


copilot写代码的能力没得说,但是呢copilot试用没几天之后就收费了。


按理说这么好用,又可以提高效率的工具,收点费也理所当然


但是秉承白嫖一时爽,一直白嫖一直爽的原则(主要是我穷穷穷),又发现了一款可以平替的插件CodeGeex


一、CodeGeex简介


① 来自官方的介绍



CodeGeeX is a powerful intelligent programming assistant based on LLMs. It provides functions such as code generation/completion, comment generation, code translation, and AI-based chat, helping developers significantly improve their work efficiency. CodeGeeX supports multiple programming languages.



翻译过来大概是



CodeGeeX是一个功能强大的基于llm的智能编程助手。它提供了代码生成/完成、注释生成、代码翻译和基于ai的聊天等功能,帮助开发人员显著提高工作效率。CodeGeeX支持多种编程语言。



GitHub地址


github.com/THUDM/CodeG…


目前在GitHub上 2.6k star 最近更新是2周前




③ 下载量

  • vscode 目前已有129k下载量
  • idea 目前已有58.7k 下载量

二、插件安装


① vscode




②idea


注: idea低版本的搜不到这个插件,小编用的是2023.01 这个版本的




安装完成后,注册一个账号即可使用


三、帮你写代码

① 我们只需要输入注释回车,它就可以根据注释帮你写代码

② tab接受一行代码 ctrl+space 接受一个单词








四、帮你添加注释



有时候,我们拿到同事没有写注释的代码,或者翻看一周前自己写的代码时。


这写得啥,完全看不懂啊,这时候就可以依靠它来帮我们的代码添加注释了



操作方法:

① 选中需要添加注释的代码

② 鼠标右键选择Add Comment

③ 选择中文或者英文






这是没加注释的代码

public class test02 {
   public static void main(String[] args) {
       int count=0;
       for(int i=101;i<200;i+=2) {
           boolean flag=true;
           for(int j=2;j<=Math.sqrt(i);j++) {
               if(i%j==0) {
                   flag=false;
                   break;
              }
          }
           if(flag==true) {
               count++;
               System.out.println(i);
          }
      }
       System.out.println(count);
  }
}

这是CodeGeex帮加上的注释

public class test02 {
   //主方法,用于执行循环
   public static void main(String[] args) {
       //定义一个变量count,初始值为0
       int count=0;
       //循环,每次循环,计算101到200之间的值,并判断是否是因子
       for(int i=101;i<200;i+=2) {
           //定义一个变量flag,初始值为true
           boolean flag=true;
           //循环,每次循环,计算i的值,并判断是否是因子
           for(int j=2;j<=Math.sqrt(i);j++) {
               //如果i的值不是因子,则flag设置为false,并跳出循环
               if(i%j==0) {
                   flag=false;
                   break;
              }
          }
           //如果flag为true,则count加1,并打印出i的值
           if(flag==true) {
               count++;
               System.out.println(i);
          }
      }
       //打印出count的值
       System.out.println(count);
  }
}

基本上每一行都加上了注释,这还怕看不懂别人写的代码


五、帮你翻译成其他语言



除了上面功能外,CodeGeeX 还可以将一种语言的代码转换成其他语言的代码



操作方法:

① 选中需要转换的代码

② 鼠标右键选择Translation mode

③ 在弹出的侧边栏中选择需要转换成的语言,例如C++、 C#、Javascript 、java、Go、Python、C 等等

④ 选择转换按钮进行转换






六 小结


试用了一下,CodeGeeX 还是可以基本可以满足需求的,日常开发中提高效率是没得说了


作为我这样的穷逼,完全可以用来平替copilot,能白嫖一天是一天~


也不用当心哪天不能用了,等用不了了再找其他的呗




本期内容到此就结束了


希望对你有所帮助,我们下期再见~ (●'◡'●)


作者:xiezhr
链接:https://juejin.cn/post/7270114845402382375
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Java仿抽奖系统

Java仿抽奖系统 前言 今天也是刚看完最近挺火的电影《孤注一掷》,也是亲眼的看到了,一个完整的家庭,是如何因为赌,而导致分崩离析,最后导致走向破碎的。 一旦涉及到电子的东西,很多东西都是变得可以控制的。这个作为程序员的我们是最清楚的,同时现在的反诈宣传,做的...
继续阅读 »

Java仿抽奖系统


前言


今天也是刚看完最近挺火的电影《孤注一掷》,也是亲眼的看到了,一个完整的家庭,是如何因为赌,而导致分崩离析,最后导致走向破碎的。


一旦涉及到电子的东西,很多东西都是变得可以控制的。这个作为程序员的我们是最清楚的,同时现在的反诈宣传,做的也是非常的到位,当时剧中哪位女警说的话,影响也非常的深刻。人都有贪心和不甘心,这也就是赌能真正抓住人的东西


好了不说那么多了,下面看一个简易的程序的代码实现


代码实现


首先我们定义一些常量


private static final int PRIZE_LEVELS = 4; // 奖品级别数量
private static final int[] PRIZE_AMOUNTS = {1, 10, 100, 1000}; // 奖品金额
private static final double[] WINNING_RATES = {10, 0, 0, 0}; // 中奖率

public static void main(String[] args) {
       // 设定中奖率
       double winningRate = 0.1;

       // 抽奖
       int prize = drawLottery(winningRate);

       // 发放奖品
       if (prize > 0) {
           System.out.println("恭喜你中奖了!奖金:" + prize + "元");
      } else {
           System.out.println("很遗憾,未中奖");
      }
  }

   // 抽奖方法
   private static int drawLottery(double winningRate) {
       Random random = new Random();
       int prize = 0;

       // 根据奖品级别逐级判断中奖
       for (int i = 0; i < PRIZE_LEVELS; i++) {
           // 生成0到1之间的随机数,判断是否中奖
           if (random.nextDouble() < winningRate * WINNING_RATES[i]) {
               prize = PRIZE_AMOUNTS[i];
               break;
          }
      }

       return prize;
  }
}

一个简单的抽奖程序。我们根据这个进行一些修改,更加的客观真实,我们加上已经有的金额和权重,让他更像是真正的赌。


我们加入权重以及自己的现金


private static double[] WEIGHTS;
// 自己的现金余额
static int cashBalance = 1000;

之后我们进行这样设计


   public static void main(String[] args) {
       // 计算权重
       calculateWeights();

       // 自己的现金余额
       int cashBalance = 1000;

       // 抽奖一次
       drawLottery(cashBalance);
  }

public static void calculateWeights() {
   WEIGHTS = new double[WINNING_RATES.length];
   double totalWeight = 0;

   // 计算总权重
   for (double rate : WINNING_RATES) {
       totalWeight += rate;
  }

   // 计算每个奖品级别的权重
   for (int i = 0; i < WEIGHTS.length; i++) {
       WEIGHTS[i] = WINNING_RATES[i] / totalWeight;
  }
}

public static void drawLottery() {
   Random random = new Random();
   double randomValue = random.nextDouble();

   int prizeIndex = 0;
   double cumulativeWeight = 0;

   // 根据随机值选择对应的奖品级别
   for (int i = 0; i < WEIGHTS.length; i++) {
       cumulativeWeight += WEIGHTS[i];
       if (randomValue <= cumulativeWeight) {
           prizeIndex = i;
           break;
      }
  }

   // 判断是否中奖
   if (random.nextDouble() <= WINNING_RATES[prizeIndex]) {
       int prizeAmount = PRIZE_AMOUNTS[prizeIndex];
       System.out.println("恭喜您中奖了!获得奖金:" + prizeAmount + "元");
       cashBalance += prizeAmount;
  } else {
       System.out.println("很遗憾,未中奖。");
  }

   // 更新现金余额
   cashBalance -= COST_PER_DRAW;
   System.out.println("抽奖后的现金余额:" + cashBalance + "元");
}

可以看出,我们这里规定的是20元抽奖一次,最高能达到1000元。


image-20230821164510692


运行一次后发现从原来的升值到了5000,


可是当你一旦陷入进去的话,只要我们稍微修改一下中奖率


image-20230821164625143


就会不断的去输。



赌博有害健康,需要我们每个人去

作者:小u
来源:juejin.cn/post/7270173541457723452
制止


收起阅读 »

优化重复冗余代码的8种方式

前言 大家好,我是田螺。好久不见啦~ 日常开发中,我们经常会遇到一些重复代码。大家都知道重复代码不好,它主要有这些缺点:可维护性差、可读性差、增加错误风险等等。最近呢,我优化了一些系统中的重复代码,用了好几种的方式。感觉挺有用的,所以本文给大家讲讲优化重复代码...
继续阅读 »

前言


大家好,我是田螺。好久不见啦~


日常开发中,我们经常会遇到一些重复代码。大家都知道重复代码不好,它主要有这些缺点:可维护性差、可读性差、增加错误风险等等。最近呢,我优化了一些系统中的重复代码,用了好几种的方式。感觉挺有用的,所以本文给大家讲讲优化重复代码的几种方式。

  • 抽取公用方法
  • 抽个工具类
  • 反射
  • 泛型
  • 继承和多态
  • 设计模式
  • 函数式
  • AOP

1. 抽取公用方法


抽取公用方法,是最常用的代码去重方法~


比如这个例子,分别遍历names列表,然后各自转化为大写和小写打印出来:


public class TianLuoExample {

public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "TianLuo");

System.out.println("Uppercase Names:");
for (String name : names) {
String uppercaseName = name.toUpperCase();
System.out.println(uppercaseName);
}

System.out.println("Lowercase Names:");
for (String name : names) {
String lowercaseName = name.toLowerCase();
System.out.println(lowercaseName);
}
}
}

显然,都是遍历names过程,代码是重复的,只不过转化大小写不一样。我们可以抽个公用方法processNames,优化成这样:


public class TianLuoExample {

public static void processNames(List<String> names, Function<String, String> nameProcessor, String processType) {
System.out.println(processType + " Names:");
for (String name : names) {
String processedName = nameProcessor.apply(name);
System.out.println(processedName);
}
}

public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "TianLuo");

processNames(names, String::toUpperCase, "Uppercase");
processNames(names, String::toLowerCase, "Lowercase");
}
}

2. 抽工具类


我们优化重复代码,抽一个公用方法后,如果发现这个方法有更多共性,就可以把公用方法升级为一个工具类。比如这样的业务场景:我们注册的时候,修改邮箱,重置密码等,都需要校验邮箱


实现注册功能时,用户会填邮箱,需要验证邮箱格式


public class RegisterServiceImpl implements RegisterService{
private static final String EMAIL_REGEX =
"^[A-Za-z0-9+_.-]+@(.+)$";

public boolean registerUser(UserInfoReq userInfo) {
String email = userInfo.getEmail();
Pattern pattern = Pattern.compile(EMAIL_REGEX);
Matcher emailMatcher = pattern.matcher(email);
if (!emailMatcher.matches()) {
System.out.println("Invalid email address.");
return false;
}

// 进行其他用户注册逻辑,比如保存用户信息到数据库等
// 返回注册结果
return true;
}
}

密码重置流程中,通常会向用户提供一个链接或验证码,并且需要发送到用户的电子邮件地址。在这种情况下,也需要验证邮箱格式合法性


public class PasswordServiceImpl implements PasswordService{

private static final String EMAIL_REGEX =
"^[A-Za-z0-9+_.-]+@(.+)$";

public void resetPassword(PasswordInfo passwordInfo) {
Pattern pattern = Pattern.compile(EMAIL_REGEX);
Matcher emailMatcher = pattern.matcher(passwordInfo.getEmail());
if (!emailMatcher.matches()) {
System.out.println("Invalid email address.");
return false;
}
//发送通知修改密码
sendReSetPasswordNotify();
}
}

我们可以抽取个校验邮箱的方法出来,又因为校验邮箱的功能在不同的类中,因此,我们可以抽个校验邮箱的工具类


public class EmailValidatorUtil {
private static final String EMAIL_REGEX =
"^[A-Za-z0-9+_.-]+@(.+)$";

private static final Pattern pattern = Pattern.compile(EMAIL_REGEX);

public static boolean isValid(String email) {
Matcher matcher = pattern.matcher(email);
return matcher.matches();
}
}

//注册的代码可以简化为这样啦
public class RegisterServiceImpl implements RegisterService{

public boolean registerUser(UserInfoReq userInfo) {
if (!EmailValidatorUtil.isValid(userInfo.getEmail())) {
System.out.println("Invalid email address.");
return false;
}

// 进行其他用户注册逻辑,比如保存用户信息到数据库等
// 返回注册结果
return true;
}
}

3. 反射


我们日常开发中,经常需要进行PO、DTO和VO的转化。所以大家经常看到类似的代码:


    //DTO 转VO
public UserInfoVO convert(UserInfoDTO userInfoDTO) {
UserInfoVO userInfoVO = new UserInfoVO();
userInfoVO.setUserName(userInfoDTO.getUserName());
userInfoVO.setAge(userInfoDTO.getAge());
return userInfoVO;
}
//PO 转DTO
public UserInfoDTO convert(UserInfoPO userInfoPO) {
UserInfoDTO userInfoDTO = new UserInfoDTO();
userInfoDTO.setUserName(userInfoPO.getUserName());
userInfoDTO.setAge(userInfoPO.getAge());
return userInfoDTO;
}

我们可以使用BeanUtils.copyProperties() 去除重复代码BeanUtils.copyProperties()底层就是使用了反射


    public UserInfoVO convert(UserInfoDTO userInfoDTO) {
UserInfoVO userInfoVO = new UserInfoVO();
BeanUtils.copyProperties(userInfoDTO, userInfoVO);
return userInfoVO;
}

public UserInfoDTO convert(UserInfoPO userInfoPO) {
UserInfoDTO userInfoDTO = new UserInfoDTO();
BeanUtils.copyProperties(userInfoPO,userInfoDTO);
return userInfoDTO;
}

4.泛型


泛型是如何去除重复代码的呢?给大家看个例子,我有个转账明细和转账余额对比的业务需求,有两个类似这样的方法:


private void getAndUpdateBalanceResultMap(String key, Map<String, List> compareResultListMap,
List balanceDTOs
) {
List<TransferBalanceDTO> tempList = compareResultListMap.getOrDefault(key, new ArrayList<>());
tempList.addAll(balanceDTOs);
compareResultListMap.put(key, tempList);
}

private void getAndUpdateDetailResultMap(String key, Map<String, List> compareResultListMap,
List detailDTOS
) {
List<TransferDetailDTO> tempList = compareResultListMap.getOrDefault(key, new ArrayList<>());
tempList.addAll(detailDTOS);
compareResultListMap.put(key, tempList);
}

这两块代码,流程功能看着很像,但是就是不能直接合并抽取一个公用方法,因为类型不一致。单纯类型不一样的话,我们可以结合泛型处理,因为泛型的本质就是参数化类型.优化为这样:


private  void getAndUpdateResultMap(String key, Map<String, List> compareResultListMap, List accountingDTOS) {
List tempList = compareResultListMap.getOrDefault(key, new ArrayList<>());
tempList.addAll(accountingDTOS);
compareResultListMap.put(key, tempList);
}

5. 继承与多态


假设你正在开发一个电子商务平台,需要处理不同类型的订单,例如普通订单和折扣订单。每种订单都有一些共同的属性(如订单号、购买商品列表)和方法(如计算总价、生成订单报告),但折扣订单还有特定的属性和方法


在没有使用继承和多态的话,会写出类似这样的代码:


//普通订单
public class Order {
private String orderNumber;
private List products;

public Order(String orderNumber, List products) {
this.orderNumber = orderNumber;
this.products = products;
}

public double calculateTotalPrice() {
double total = 0;
for (Product product : products) {
total += product.getPrice();
}
return total;
}

public String generateOrderReport() {
return "Order Report for " + orderNumber + ": Total Price = $" + calculateTotalPrice();
}
}

//折扣订单
public class DiscountOrder {
private String orderNumber;
private List products;
private double discountPercentage;

public DiscountOrder(String orderNumber, List products, double discountPercentage) {
this.orderNumber = orderNumber;
this.products = products;
this.discountPercentage = discountPercentage;
}

public double calculateTotalPrice() {
double total = 0;
for (Product product : products) {
total += product.getPrice();
}
return total - (total * discountPercentage / 100);
}
public String generateOrderReport() {
return "Order Report for " + orderNumber + ": Total Price = $" + calculateTotalPrice();
}
}

显然,看到在OrderDiscountOrder类中,generateOrderReport() 方法的代码是完全相同的。calculateTotalPrice()则是有一点点区别,但也大相径庭。


我们可以使用继承和多态去除重复代码,让DiscountOrder去继承Order,代码如下:


public class Order {
private String orderNumber;
private List products;

public Order(String orderNumber, List products) {
this.orderNumber = orderNumber;
this.products = products;
}

public double calculateTotalPrice() {
double total = 0;
for (Product product : products) {
total += product.getPrice();
}
return total;
}

public String generateOrderReport() {
return "Order Report for " + orderNumber + ": Total Price = $" + calculateTotalPrice();
}
}

public class DiscountOrder extends Order {
private double discountPercentage;

public DiscountOrder(String orderNumber, List products, double discountPercentage) {
super(orderNumber, products);
this.discountPercentage = discountPercentage;
}

@Override
public double calculateTotalPrice()
{
double total = super.calculateTotalPrice();
return total - (total * discountPercentage / 100);
}
}

6.使用设计模式


很多设计模式可以减少重复代码、提高代码的可读性、可扩展性.比如:



  • 工厂模式: 通过工厂模式,你可以将对象的创建和使用分开,从而减少重复的创建代码

  • 策略模式: 策略模式定义了一族算法,将它们封装成独立的类,并使它们可以互相替换。通过使用策略模式,你可以减少在代码中重复使用相同的逻辑

  • 模板方法模式:模板方法模式定义了一个算法的骨架,将一些步骤延迟到子类中实现。这有助于避免在不同类中重复编写相似的代码


我给大家举个例子,模板方法是如何去除重复代码的吧,业务场景:



假设你正在开发一个咖啡和茶的制作流程,制作过程中的热水和添加物质的步骤是相同的,但是具体的饮品制作步骤是不同的



如果没有使用模板方法模式,实现是酱紫的:


public class Coffee {
public void prepareCoffee() {
boilWater();
brewCoffeeGrinds();
pourInCup();
addCondiments();
}

private void boilWater() {
System.out.println("Boiling water");
}

private void brewCoffeeGrinds() {
System.out.println("Brewing coffee grinds");
}

private void pourInCup() {
System.out.println("Pouring into cup");
}

private void addCondiments() {
System.out.println("Adding sugar and milk");
}
}

public class Tea {
public void prepareTea() {
boilWater();
steepTeaBag();
pourInCup();
addLemon();
}

private void boilWater() {
System.out.println("Boiling water");
}

private void steepTeaBag() {
System.out.println("Steeping the tea bag");
}

private void pourInCup() {
System.out.println("Pouring into cup");
}

private void addLemon() {
System.out.println("Adding lemon");
}
}

这个代码例子,我们可以发现,烧水和倒入杯子的步骤代码,在CoffeeTea类中是重复的。


使用模板方法模式,代码可以优化成这样:


abstract class Beverage {
public final void prepareBeverage() {
boilWater();
brew();
pourInCup();
addCondiments();
}

private void boilWater() {
System.out.println("Boiling water");
}

abstract void brew();

private void pourInCup() {
System.out.println("Pouring into cup");
}

abstract void addCondiments();
}

class Coffee extends Beverage {
@Override
void brew() {
System.out.println("Brewing coffee grinds");
}

@Override
void addCondiments() {
System.out.println("Adding sugar and milk");
}
}

class Tea extends Beverage {
@Override
void brew() {
System.out.println("Steeping the tea bag");
}

@Override
void addCondiments() {
System.out.println("Adding lemon");
}
}

在这个例子中,我们创建了一个抽象类Beverage,其中定义了制作饮品的模板方法 prepareBeverage()。这个方法包含了烧水、倒入杯子等共同的步骤,而将制作过程中的特定步骤 brew() 和 addCondiments() 延迟到子类中实现。这样,我们避免了在每个具体的饮品类中重复编写相同的烧水和倒入杯子的代码,提高了代码的可维护性和重用性。


7.自定义注解(或者说AOP面向切面)


使用 AOP框架可以在不同地方插入通用的逻辑,从而减少代码重复。


业务场景:


假设你正在开发一个Web应用程序,需要对不同的Controller方法进行权限检查。每个Controller方法都需要进行类似的权限验证,但是重复的代码会导致代码的冗余和维护困难


public class MyController {
public void viewData() {
if (!User.hasPermission("read")) {
throw new SecurityException("Insufficient permission to access this resource.");
}
// Method implementation
}

public void modifyData() {
if (!User.hasPermission("write")) {
throw new SecurityException("Insufficient permission to access this resource.");
}
// Method implementation
}
}

你可以看到在每个需要权限校验的方法中都需要重复编写相同的权限校验逻辑,即出现了重复代码.我们使用自定义注解的方式能够将权限校验逻辑集中管理,通过切面来处理,消除重复代码.如下:


@Aspect
@Component
public class PermissionAspect {

@Before("@annotation(requiresPermission)")
public void checkPermission(RequiresPermission requiresPermission) {
String permission = requiresPermission.value();

if (!User.hasPermission(permission)) {
throw new SecurityException("Insufficient permission to access this resource.");
}
}
}

public class MyController {
@RequiresPermission("read")
public void viewData() {
// Method implementation
}

@RequiresPermission("write")
public void modifyData() {
// Method implementation
}
}

就这样,不管多少个Controller方法需要进行权限检查,你只需在方法上添加相应的注解即可。权限检查的逻辑在切面中集中管理,避免了在每个Controller方法中重复编写相同的权限验证代码。这大大提高了代码的可读性、可维护性,并避免了代码冗余。


8.函数式接口和Lambda表达式


业务场景:



假设你正在开发一个应用程序,需要根据不同的条件来过滤一组数据。每次过滤的逻辑都可能会有些微的不同,但基本的流程是相似的。



没有使用函数式接口和Lambda表达式的情况:


public class DataFilter {
public List<Integer> filterPositiveNumbers(List numbers) {
List<Integer> result = new ArrayList<>();
for (Integer number : numbers) {
if (number > 0) {
result.add(number);
}
}
return result;
}

public List<Integer> filterEvenNumbers(List numbers) {
List<Integer> result = new ArrayList<>();
for (Integer number : numbers) {
if (number % 2 == 0) {
result.add(number);
}
}
return result;
}
}

在这个例子中,我们有两个不同的方法来过滤一组数据,但是基本的循环和条件判断逻辑是重复的,我们可以使用使用函数式接口和Lambda表达式,去除重复代码,如下:


public class DataFilter {
public List<Integer> filterNumbers(List numbers, Predicate predicate) {
List<Integer> result = new ArrayList<>();
for (Integer number : numbers) {
if (predicate.test(number)) {
result.add(number);
}
}
return result;
}
}


我们将过滤的核心逻辑抽象出来。该方法接受一个 Predicate函数式接口作为参数,以便根据不同的条件来过滤数据。然后,我们可以使用Lambda表达式来传递具体的条件,这样最终也达到去除重复代码的效果啦.


最后


我是捡田螺的小男孩,大家如果觉得看了本文有帮助的话,麻烦给个三连(点赞、分享、转发)支持一下哈。最近我在工作中,用了其中的几种方式,去优化重复代码。下一篇文章,我打算出一篇后端思维系列的文章,基于业务代码,手把手教大家去除重复代码哈。一起加油~~

作者:捡田螺的小男孩
来源:juejin.cn/post/7270026656663322685

收起阅读 »

消息太大,kafka受不了

前言 上周在进行自测的时候,kafka抛出一个RecordTooLargeException异常,从名字我们可以直接看出是消息太大了,导致发不出去而抛出异常,那么怎么应该怎么解决这个问题呢,其实很简单,要么将消息拆分得小一点,要么调节kafka层面的参数,依然...
继续阅读 »

前言


上周在进行自测的时候,kafka抛出一个RecordTooLargeException异常,从名字我们可以直接看出是消息太大了,导致发不出去而抛出异常,那么怎么应该怎么解决这个问题呢,其实很简单,要么将消息拆分得小一点,要么调节kafka层面的参数,依然它抛出这个异常,那么就证明超过了某个参数的阈值,由此我们可以有两种方式来处理这个问题,但是一切还要从我们的业务背景和数据结构去看这个问题。


业务背景


我们这边会将数据写入文件,通过FTP的方式,没产生数据,就往FTP里面追加,而这些数据都是需要保证不丢失的,由于业务的发展,我这边需要专门去处理这些文件,然后通过kafka投递给下游系统,所以自然需要解析文件,还得一条一条的解析后发送。


问题出现


一开始我看到文件都比较小,所以处理方式是只有这个文件的数据全部解析完成并成功投递kafka,那么我这边才记录这个文件处理成功,但是处理了很多个大文件过后,发现数据条数对不上,看日志是RecordTooLargeException异常,因为上面的处理方式是文件处理完成并全部投递到kafka才记录文件解析完成,所以这是有问题的,一个大文件可能有即使上百万条数据,难免会遇到很大的数据,所以只要一条没解析成功,那么后面的数据就不去解析了,这个文件就不算解析成功,所以应该要设计容错,并对数据进行监控和补偿。


处理问题


在得知是某些数据过大的问题,我就DEBUG去看源码,在kafka生产端的KafkaProducer类中,发现问题出在下面这方法中。



ensureValidRecordSize方法就是对消息的大小进行判断的,参数size就是我们所发送的消息的字节数,maxRequestSize就是允许消息的最大字节,因为没有进行设置,所以这个值使用的是默认值,默认为1M,所以就应该将maxRequestSize这个参数进行重新设置。


因为我们使用的是SpringBoot开发,于是通过yml方式配置,但是发现spring-kafka没提示这个属性,于是只有写一个Kafka的配置类,然后再读取yml文件内容进行配置


配置类


yml文件



通过上面的配置后,我们看到我将max.request.size参数的值设置为10M,这需要根据实际情况来,因为我在处理的过程中发现像比较大的数据行也只有个别。


如果在实际使用过程中数据比较大,那么可能需要拆分数据,不过如果数据不能拆分,那么我们应该考虑消息压缩方式,将数据压缩后再发送,然后在消费者进行解压,不过这种压缩是我们自己实现的,并不是kafka层面的压缩,kafka本身也提供了压缩功能,有兴趣可以了解一下。


扩展


上面设置了max.request.size参数,我们在上面的截图代码中看到第二个判断中有一个参数totalMemorySize,这个值是缓冲区大小,我们发送的消息并不会马上发送kafka服务端,而是会先放在内存缓冲区,然后kafka通过一个线程去取,然后发送,可通过buffer.memory设置,这个值的默认值为32M,所以我们在设置max.request.size的时候也要考虑一下这个值。


总结


有必要对kafka进行比较深一点的学习,这样在出现问题的时候能够快速定位,并且合理解决,当然,在业务处理的时候要充分考虑可能出现的问题,做好容错和相应的补偿方案。


今天的分享就到这里,感谢你的观

作者:刘牌
来源:juejin.cn/post/7269745800178286627
看,我们下期见

收起阅读 »

开发者不需要成为 K8s 专家!!!

之前有一篇文章 “扯淡的DevOps,我们开发者根本不想做运维!” 得到了许多开发者的共鸣,每一个开发人员,都希望能够抛却运维工作,更专注于自己开发的代码,将创意转化为令人惊叹的应用。然而事不尽如人意,到了云原生时代,开发者的运维工作似乎并没有减少,而是变成了...
继续阅读 »

之前有一篇文章 “扯淡的DevOps,我们开发者根本不想做运维!” 得到了许多开发者的共鸣,每一个开发人员,都希望能够抛却运维工作,更专注于自己开发的代码,将创意转化为令人惊叹的应用。然而事不尽如人意,到了云原生时代,开发者的运维工作似乎并没有减少,而是变成了在 K8s 上的应用部署和管理。


对运维人员来说,只需要维护好底层的 K8s,便可以在弹性、便捷性上得到巨大提升。然而 K8s 对于我们开发者而言还是太复杂了,我们还需要学习如何打包镜像以及 K8s 相关知识。许多时间都浪费在了应用部署上,我们真的需要成为 K8s 专家吗?我只是想部署一个应用至于那么复杂吗?你是否曾想过,能否有平台或方法,让我们不必成为 K8s 专家,甚至都不需要懂 K8s 就能部署好你的应用,轻松管理应用?


实际面临的问题


对于我们开发者而言,总会遇到以下不同的场景,也许是公司层面的问题、又或是业务层面的问题,也许现在用传统部署方式很简单,但随着业务增长,又不得不迁移。而面对这些问题,我们也要发出自己的声音。




  • 身处小公司,没有专门的运维。需要程序员自己负责写 Dockerfile + YAML + Kustomize 然后部署到 k8s 上面。除了工作量以外,还面临 K8s 自身的复杂性,对于多套业务,Dockerfie、Yaml、CI、CD 脚本占据了绝大部分的工作量。不写这些行不行?




  • 公司内的微服务越来越复杂,在写代码的基础上还得考虑各个服务之间的通信、依赖和部署问题,毕竟除了我们开发者以外,运维人员也不会比你更熟悉微服务之间的复杂依赖。也许已经开始尝试 Helm ,但是编写一个完整的 Chart 包依然是如此复杂,还可能面临格式问题、配置解耦不完全导致的换个环境无法部署问题。时间全写 Yaml 了。不额外编写 Helm Chart,直接复制应用行不行?




  • 在大型企业内部,正处于在传统应用迁移到云环境的十字路口。面对多种集群的需求、现有应用的平稳迁移、甚至一些公共的模块的复用如何做都将成为我们需要解决的问题。不要每次都重新开发,把现有的应用或模块积累下来行不行?




在这些场景下,我们大量的时间都消耗在额外的 Dockerfile、Yaml、Helm Chart 这些编写上了。K8s 很好,但似乎没解决我们开发者的问题,我们开发者用 K8s 反而变得更加复杂。不说需要额外编写的这些文件或脚本,单单是掌握 K8s 的知识就需要耗费大量时间和精力。
这些问题真的绕不过去吗?我觉得不是。来了解下 Rainbond 这个不需要懂 K8s 的云原生应用管理平台吧。谁说只有成为 K8s 专家后,才能管理好你的应用?


为什么是 Rainbond?


Rainbond 是一个不需要懂 K8s 的应用管理平台。不用在服务器上进行繁琐操作,也不用深入了解 K8s 的相关知识。Rainbond 遵循“以应用为中心”的设计理念。在这里只有你的业务模块和应用。每一个业务模块都可以从你的代码仓库直接部署并运行,你不是 K8s 专家也可以管理应用的全生命周期。同时利用 Rainbond 的模块化拼装能力,你的业务可以灵活沉淀为独立的应用模块,这些模块可以随意组合、无限拼装,最终构建出多样化的应用系统。


1. 不懂 K8s 部署 K8s 行不行?


行! 对于很多初学者或者开发人员来说,如果公司内部已经搭建好了可用的 K8s 平台,那么这一步不会是需要担心的问题。但是对于一些独立开发者而言,却很难有这样的环境,而 Rainbond 就提供了这样的解决方案,在 Linux 服务器上,只需要先运行一个 Docker 容器,访问到 Rainbond 控制台后,再输入服务器的 IP 地址,即可快速部署一套完整的 K8s 集群。


add_cluster


如果这还是太复杂,那么可以尝试使用 Rainbond 的快速安装,只需要一个容器和 5 分钟时间,就能为你启动一个带 K8s 集群的平台,而你在平台上部署的业务也都会部署到这个集群中。


2. 不想或不会写 Dockerfile、Yaml 等文件能不能部署应用?


能! Rainbond 支持自动识别各类开发语言,不论你是使用哪种开发语言,如Java、Python、Golang、NodeJS、Dockerfile、Php、.NetCore等,通过简单的向导式流程,无需配置或少量配置,Rainbond 都能够将它们识别并自动打包为容器镜像,并将你的业务快速部署到 K8s 集群中进行高效管理。你不再需要编写任何与代码无关的文件。只需要提供你的代码仓库地址即可。


source_code_build


3. 各类业务系统如何拼装?


在 Rainbond 中,不同的业务程序可以通过简单的连线方式进行快速编排。如果你需要前端项目依赖后端,只需打开编排模式,将它们连接起来,便能迅速建立依赖关系,实现模块化的拼装。这为你的应用架构带来了极大的灵活性,无需复杂的配置和操作,即可快速构建复杂的应用系统。


同时如果你已经实现了完整的业务程序,它可能包含多个微服务模块,你还可以将其发布到本地的组件库实现模块化的积累。下次部署时可以直接即点即用,且部署后可以与你其他应用程序再次进行拼装。实现无限拼装组合的能力。


component_assembly


4. 不会 K8s 能不能管理部署好的应用?


没问题! Rainbond 提供了面向应用的全生命周期管理运维,不需要学习 Kubectl 命令,也不需要知道 K8s 内复杂的概念,即可在页面上一键管理应用内各个业务模块的批量启动、关闭、构建、更新、回滚等关键操作,同时还支持应用故障时自动恢复,以及应用自动伸缩等功能。同时还支持应用 http 和 tcp 策略的配置,以及相应的证书管理。


app_manage


如何使用?


在 Linux 终端执行以下命令, 5 分钟之后,打开浏览器,输入 http://<你的IP>:7070 ,即可访问 Rainbond 的 UI 了。


curl -o install.sh https://get.rainbond.com && bash ./install.sh

作者:Rainbond开源
来源:juejin.cn/post/7268539925086519353

收起阅读 »

Amazon SageMaker 让机器学习轻松“云上见”!

最近,“上云探索实验室”活动 正在如火如荼进行,亚马逊云、“大厂”背景加持,来看看它们有什么新鲜技术/产品? 本篇带来对 shouAmazon SageMaker 的认识~ 闻名不如一见 首先,开宗明义,shouAmazon SageMaker 是什么? 从...
继续阅读 »

最近,“上云探索实验室”活动 正在如火如荼进行,亚马逊云、“大厂”背景加持,来看看它们有什么新鲜技术/产品?


本篇带来对 shouAmazon SageMaker 的认识~


闻名不如一见


首先,开宗明义,shouAmazon SageMaker 是什么?




官网我们可以了解到:Amazon SageMaker 是一项帮助开发人员快速构建、训练和部署机器学习 (ML) 模型的托管服务。


其实,类比于:就像咱们开发人员经常把代码部署 Github Page 上,自动构建、运行我们的代码服务。


当下,我们可以把 AIGC 领域火爆的模型共享平台 —— Hugging Face 中的模型放到 Amazon SageMaker 中去部署运行、进行微调等~


如果你还不了解 HuggingFace?那抓紧快上车!




就这张小黄脸,目前已经估值 20 亿美元,它拥有 30K+ 的模型,5K+ 的数据集,以及各式各样的 Demo,用于构建、训练最先进的 NLP (自然语言处理)模型。


是的,如果你:


1、不想关心硬件、软件和基础架构等方面的问题


2、想简化操作机器学习模型的开发流程


3、想灵活选择使用自己的算法和框架以满足不同业务需求


就可以 把目光投向 Amazon SageMaker,用它的云服务来部署你想要用的 HuggingFace 模型等~




百思不如一试


Amazon SageMaker 可以全流程的帮助我们构建机器学习模型,这样真的会省下很多心力(少掉几根头发)~


具体实践中,我们知道在数据预处理过程中,在训练模型之前,需要做一系列操作,比如:缺失值处理、数据归一化和特征选择等,Amazon SageMaker 提供了很好的预处理和转换数据的工具,助力快速完成这些工作。


在模型选择环节,Amazon SageMaker 提供多种内置的机器学习算法和框架,你可以根据数据集和任务类型选择合适的模型。


还有,提高模型性能是我们需要特别关注的,Amazon SageMaker 让你可以指定调优的目标和约束条件,系统会自动搜索最优的参数组合,这就很智能、很舒服。


模型训练完后,Amazon SageMaker 也自带易用的模型部署和监控功能;


一整套下来,训练模型感觉就像呼吸一样自然~


官网教程写的很清晰,还有很多视频讲解:# Amazon SageMaker - 入门手册


这里不赘述,仅看实战中代码表示,感受一二:


如何在Amazon SageMaker 上部署Hugging Face 模型?




1、首先,在 Amazon SageMaker 中创建一个 Notebook 实例。可以使用以下代码在 Amazon SageMaker 中创建 Notebook 实例:

import sagemaker from sagemaker
import get_execution_role

role = get_execution_role()

sess = sagemaker.Session()

# 创建 Notebook 实例
notebook_instance_name = 'YOUR_NOTEBOOK_INSTANCE_NAME'
instance_type = 'ml.t2.medium'
sagemaker_session = sagemaker.Session()
sagemaker_session.create_notebook_instance(notebook_instance_name=notebook_instance_name,instance_type=instance_type,role=role)


2、其次,下载 Hugging Face 模型,你可以使用以下代码下载 Hugging Face 模型:

!pip install transformers

from transformers import AutoTokenizer, AutoModelForSequenceClassification

# 下载 Hugging Face 模型
model_name = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(model_name)

3、创建一个推理规范,以指定部署的模型。可以参照以下代码创建推理规范:

from sagemaker.predictor import Predictor
from sagemaker.serializers import JSONSerializer
from sagemaker.deserializers import JSONDeserializer
from sagemaker.tensorflow.serving import Model

# 创建推理规范
class HuggingFacePredictor(Predictor):
def __init__(self, endpoint_name, sagemaker_session):
super().__init__(endpoint_name, sagemaker_session, serializer=JSONSerializer(),
deserializer=JSONDeserializer())

model_name = "huggingface"
model_data = "s3://YOUR_S3_BUCKET/YOUR_MODEL.tar.gz"
entry_point = "inference.py"
instance_type = "ml.m5.xlarge"
role = sagemaker.get_execution_role()

model = Model(model_data=model_data,
image_uri="763104351884.dkr.ecr.us-east-1.amazonaws.com/huggingface-pytorch-inference:1.6.0-transformers4.0.0",
role=role,
predictor_cls=HuggingFacePredictor,
entry_point=entry_point)

predictor = model.deploy(initial_instance_count=1, instance_type=instance_type)


4、测试部署模型

data = {"text": "I love using Amazon SageMaker!"}
response = predictor.predict(data)

print(response)

过程就是,创建 NoteBook => 下模型 => 指定模型、设定推理脚本 => 测试部署,足够简洁~


人人都能上云,人人都能训练机器模型~


不知道大家发现没有,其实现在的编程开发都逐渐在“云”化,不仅是机器学习,普通开发也是;类似低代码平台,开发不再是一点点复制代码、拼凑代码、修改代码,更多是通过自动化平台“点点点”就能构建自己想要的服务了!拥抱“云”平台,省心又省力,也在拥抱未来~


防守不如亮剑


目前市面上同类型的产品也有一些,比如:


1、Google Cloud AI Platform:Google提供的全托管的机器学习平台,可以帮助用户构建、训练和部署机器学习模型。


2、Microsoft Azure Machine Learning:微软提供的一款全托管的机器学习平台,可以帮助用户构建、训练和部署机器学习模型。


3、IBM Watson Studio:IBM提供的一款机器学习工具和服务平台,可以帮助用户构建、训练和部署机器学习模型。此外它还提供了数据可视化、模型解释和协作等功能。


它们与Amazon SageMaker 类似,具有全托管和自动化的特点,同时提供了多种算法和框架供用户选择。但尺有所长、寸有所短,咱们不妨用表格来一眼对比它们各自的优缺点:



可以看到,Amazon SageMaker 的配置更简单;


至于谈到:“需掌握 AWS 服务”,其实它也好上手,类比于阿里云,AWS 是亚马逊云服务,全球顶流、百万客户、加速敏捷,即使不用来开发机器模型,也建议体验、上手其它云服务产品~具体参见aws.amazon.com




活动福利环节


刚好,最近正在进行“上云探索实验室”活动;6月27-28日亚马逊云科技中国峰会上海现场,展区5楼【开发者会客厅】有互动活动:


现场为开发者提供了部分 Amazon codewhisperer 账号,开发者可以到现场直接进行代码体验,参与体验问卷回复可获得社区定制周边礼品一份。


同时开发者也可于现场进行专业版账号注册,完成注册即可获得定制周边礼品一份或免费 Serverlesspresso 咖啡券一张。


欢迎大家注册~~


话不多少,先冲为敬~




作者:掘金安东尼
链接:https://juejin.cn/post/7244526264618680380
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

总是跳转到国内版(cn.bing.com)?New Bing使用全攻略

你是否想要使用强大的(被削后大嘘)New Bing? 你是否已经获得了New Bing的使用资格? 你是否在访问www.bing.com/new时提示页面不存在? 你是否在访问www.bing.com时总是重定向到cn.bing.com而使用不了New Bin...
继续阅读 »

你是否想要使用强大的(被削后大嘘)New Bing?


你是否已经获得了New Bing的使用资格?


你是否在访问www.bing.com/new时提示页面不存在?


你是否在访问www.bing.com时总是重定向到cn.bing.com而使用不了New Bing?


New Bing的使用本来就不需要依赖科学上网,看完下面的教程,不论你卡在哪一步,你都可以成功使用New Bing。


3.13更新


根据大量评论反馈,微软似乎已经让本文中的方法:“修改请求头X-Forwarded-For来伪装请求地址”的方法失效了。现在微软已经可以检测到请求的源IP了,使用科学上网(代理)的方法依然可用,使用前需清空cookie,否则还是国内特供版Bing。没有办法科学上网的朋友,目前暂时帮不上忙了,如果未来有新方法,我仍然会更新在文档中。


一、加入New Bing的候选名单


现在的情况是:访问 http://www.bing.com/new,会自动跳转到cn.bing.com
如果你是Chrome或者Edge浏览器(下面以Edge举例,Chrome也同理)可以通过以下扩展的方式,修改请求Header来防止被重定向。 


打开浏览器的扩展,找到 管理扩展 按钮,在打开的页面左侧找到 获取 Microsoft Edge 扩展 并打开。
搜索 ModHeader ,安装下面这个扩展。




接着在已有扩展中,找到这个扩展并点击打开。 


打开扩展后就会弹出下面这个弹窗,点击 FILTER,选择 Request URL filter (在Chrome中是 URL filter

  


填写下面三个内容(分别是:X-Forwarded-For8.8.8.8.*://www.bing.com/.*)并确保都勾选。 



设置好后,再次访问 http://www.bing.com/new ,登录自己的微软账号,点击加入候选名单即可。

  


静静等待Microsoft Bing发来的 “你已中奖”的邮件,或者你微软的邮箱不是你常用的邮箱,时不时重新访问一下 http://www.bing.com/new 也可以看到你是否已经 “中奖”。


2023.3.6更新


在这里,可能会有一些朋友遇到重定向次数过多,请清除Cookie的问题,地址栏会有很多相同的zh-CN“后缀”,可以尝试以下方法:

  1. 点击Request URL filters一行的右方加号,并添加一个 .*://cn.bing.com/.* 。这时候Request URL filters中,同时存在两个筛选规则,包括www与cn两个
  2. 清除bing相关网站的cookie。在设置->cookie与网站权限->管理和删除cookie->查看所有网站cookie->右上方搜索bing,然后删除所有相关的条目
  3. 如果还有登录账号时遇到类似的问题。可以先按上一步Cookie,然后关闭扩展,在cn.bing.com中登录账号,再开启扩展访问正常版本试试。

二、下载Edge Dev 目前普通版Edge也可以了


获得资格后,首先需要解决的问题应该是下载Edge的DEV版本,在除了dev版本的Edge以外的任何浏览器中,均不能使用带有 Chat 功能的 New Bing。


通过下面这个链接,下载dev版本的Edge
http://www.microsoft.com/en-us/edge


不要直接点页面中大大的下载按钮(那个是普通版),找到下面这个部分并打开我框住的链接。 


在打开的页面也不要直接下载,往下找到下面这个图片的部分:



确保Edge图标上有 DEV 字样,点击右侧的下载适合自己电脑的版本(macOS、Windows、Linux)


三、访问New Bing


在你走完上面的流程后,访问 http://www.bing.com 即可看到上面的导航栏有Chat字样。

点击后,开始你的New Bing使用之旅吧。 



四、手机访问New Bing


现在(更新时间2023.2.28)微软已经将New Bing带上了手机。


现在有了更方面的访问途径,使用手机的Bing App使用New Bing的Chat功能。
但国内的应用商城应该是名叫“微软必应”的阉割版,我这里找到了微软官方的下载地址:Microsoft Bing


不过我并不是通过这种方式下载的APP,我在谷歌的应用商城下载的Bing APP,如果上述官网的地址下载的APP在账号已拥有测试资格的情况下仍没有Chat功能,请自行尝试谷歌商店下载。


五、结语


有问题可以评论中询问我,请确保你清楚地描述出你遇到的问题和尝试过的方法。New Bing、ChatGPT还有本文的作者我,都需要你具备基本的提问题的能力。我有看到部分评论(CSDN)的朋友仅简单问了一句话,我根本无从得知你的问题的现状,自然也无法解决你的问题。请你清楚地思考完下面几个问题:

  1. 你进行到哪一步卡住了?
  2. 你尝试了哪些方法?
  3. 你是否完整阅读了本文?

如果你的问题描述不清楚,恕我拒不回答。


另外如果有帮到你成功访问到New Bing,还请不要吝啬你的点赞和收藏 ^_^


作者:西木鹿亚
链接:https://juejin.cn/post/7202531472720592951
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Git stash 存储本地修改

前言 我们在开发的过程中经常会遇到以下的一些场景:当我们在 dev 分支开发时,可能需要临时切换到 master 拉分支修复线上 bug 或者修改其他分支代码,此时对于 dev 分支的修改,最常见的处理方式是将代码提交到远程后再切到对应的分支进行开发,但是如果...
继续阅读 »

前言


我们在开发的过程中经常会遇到以下的一些场景:

  • 当我们在 dev 分支开发时,可能需要临时切换到 master 拉分支修复线上 bug 或者修改其他分支代码,此时对于 dev 分支的修改,最常见的处理方式是将代码提交到远程后再切到对应的分支进行开发,但是如果 dev 分支的修改不足以进行一次 commit(功能开发不完整、还有 bug 未解决等各种原因),或者觉得提交代码的步骤过多,此时 dev 的修改就不好处理

  • 在开发阶段可能某个分支需要修改特定的配置文件才能运行,而其他分支不需要,那么当我们在这个分支和其他分支来回切换的时候,就需要反复的修改、回滚对应的配置文件,这种操作也是比较低效且麻烦的


而我们通过 Git 提供的 stash 存储操作,可以将当前分支的修改或者开发常用的配置文件存储至暂存区并生成一条存储记录,在需要使用时通过存储记录的索引直接取出,无需额外提交或单独保存,可以有效的解决以上的问题


Git stash 使用流程


1. 存储修改:我们可以使用 git stash 对 dev 的修改进行存储,存储修改后会生成一条存储记录




2. 查看存储记录:通过 git stash list 查看存储记录列表,存储记录的格式为:

stash@{索引值}:WIP on [分支名]: [最近的一次 commitId + 提交信息]



3. 多次存储记录相同:如果多次存储修改的过程中没有进行过 commit 提交,存储记录除了 索引值 之外将会完全相同,此时我们就无法快速辨识存储记录对应的修改内容




4. 存储记录标识


为了解决存储记录无法辨识问题,存储修改时可以用 git stash -m '标识内容' 对存储记录进行标识




此时我们再查看存储记录列表,就可以看到存储记录的标识,此时存储记录的格式为:

stash@{索引值}:on [分支名]: [标识内容]



5. 恢复存储:当我们在其他分支完成开发再回到 dev 分支时,就可以通过 git stash apply index 将指定的存储记录恢复至工作区,index 是存储记录的索引,未指定则恢复第一条存储记录




6. 删除存储


对于不再需要的存储记录,可以通过 git stash drop index 删除指定的存储记录,此时我们执行 git stash drop 删除第一条记录后再使用 git stash list 查看存储记录就已经少了一条了




如果所有的存储记录都不需要,可以使用 git stash clear 清除所有存储记录




Git Stash 命令


查看存储记录


查看存储记录列表

git stash list

查看 最近一次 存储记录的具体修改内容,即修改了哪些文件

git stash show

查看 指定索引 存储记录的具体修改内容

git stash show index
git stash show stash@{index}

存储修改


直接存储修改

git stash

存储修改,并添加备注

git stash -m '备注内容'

恢复存储记录



恢复存储记录的修改内容



恢复 最近一次 的存储记录

git stash apply

恢复 指定索引 的存储记录

git stash apply index
git stash apply stash@{index}

删除存储记录



对不需要的存储记录进行删除,可以删除部分或全部

  • 删除 最近一次 的存储记录
git stash drop
  • 删除 指定索引 的存储记录
git stash drop index
git stash drop stash@{index}
  • 删除所有的暂存修改
git stash clear

恢复并删除存储记录



恢复存储记录的同时删除对应的存储记录

  • 恢复并删除 最近一次 的存储记录
git stash pop
  • 恢复并删除 指定索引 的存储记录
git stash pop index
git stash pop stash@{index}

作者:蒋绾心
链接:https://juejin.cn/post/7269691627598479375
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

💢可恶!终究还是逃不过这些网站吗?

Documatic http://www.documatic.com/ Documatic 是一个高效的搜索引擎工具,旨在帮助开发人员轻松搜索他们的代码库以查找特定的代码片段,函数,方法和其他相关信息。这个工具旨在为开发人员节省宝贵的时间并增加生产力,可以在几...
继续阅读 »

Documatic


http://www.documatic.com/


Documatic 是一个高效的搜索引擎工具,旨在帮助开发人员轻松搜索他们的代码库以查找特定的代码片段,函数,方法和其他相关信息。这个工具旨在为开发人员节省宝贵的时间并增加生产力,可以在几秒钟内快速提供准确和相关的搜索结果。Documatic 是一个代码搜索工具,具有自然语言查询功能,可以简化新手和专家开发人员的代码库搜索。输入查询后,Documatic 会快速从代码库中获取相关的代码块,使您更容易找到所需的信息。 



Transform.tools


transform.tools/


Transform.tools 是一个网站,可以转换大多数内容,如 HTML 到 JSX,JavaScript 到 JSON,CSS 到 JS 对象等等。当我需要转换任何内容时,它真的节省了我的时间。 




Convertio


convertio.co/


Convertio - 在线轻松转换文件。超过 309 种不同的文档,图像,电子表格,电子书,档案,演示文稿,音频和视频格式。比如 PNG 到 JPEG,SVG 到 PNG,PNG 到 ICO 等等。 



Removebg


http://www.remove.bg/


Removebg 是一个令人惊叹的工具,可以轻松地删除任何图像的背景。RemoveBG 可以立即检测图像的主题并删除背景,留下透明的 PNG 图像,您可以轻松地在项目中使用。无论您是否从事平面设计,图片编辑或涉及图像的任何其他项目,我使用过这个工具太多次,我甚至不记得了。




Imglarger


imglarger.com/


Imglarger 允许您将图像放大高达 800%,并增强照片而不损失质量,这对摄影师和图像处理者特别有用。它是一个一体化的 AI 工具包,可以增强和放大图像。增加图像分辨率而不损失质量。




Code Beautify


codebeautify.org/


Code Beautify 是一个在线代码美化和格式化工具,允许您美化源代码。除了此功能外,它还支持一些转换器,如图像到 base64,不仅如此,它还有如下图像所示的大量功能:




Vercel


vercel.com/


Vercel 是前端开发人员的平台,为创新者提供构建即时 Web 应用程序所需的速度和可靠性。它是一个云平台,自动化开发和部署流程来构建无服务器 Web 应用程序。它提供诸如无服务器功能,静态站点托管,持续部署,自定义域名和 SSL 以及团队协作等功能。它有免费层和付费计划以获得更高级功能,并被许多流行的网站和 Web 应用程序使用。




作者:前端小蜗
链接:https://juejin.cn/post/7248440645688819769
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

微信(群)接入ChatGPT,MJ聊天机器人Bot

前言 微信接入ChatGPT机器人还是挺有必要的,不用被墙,可以直接问它问题,还可以接入微信群等一些实用的功能。 注意:微信接入ChatGPT机器人Bot,微信账号极其容易被封,请谨慎接入 注意:微信接入ChatGPT机器人Bot,微信账号极其容易被封,...
继续阅读 »

前言


微信接入ChatGPT机器人还是挺有必要的,不用被墙,可以直接问它问题,还可以接入微信群等一些实用的功能。



注意:微信接入ChatGPT机器人Bot,微信账号极其容易被封,请谨慎接入




注意:微信接入ChatGPT机器人Bot,微信账号极其容易被封,请谨慎接入




注意:微信接入ChatGPT机器人Bot,微信账号极其容易被封,请谨慎接入



首先你需要一个 OpenAI 的账号并且创建一个可用的 api key,这里不做过多介绍,有任何问题可以加博客首页公告处微信群进行沟通。


相关的聊天机器人Bot GitHub上有非常多的项目,不仅支持接入ChatGPT,还支持接入MJ画图等一些其他功能。


本篇介绍两个项目(我用的第一个 chatgpt-on-wechat 项目):


chatgpt-on-wechat 项目最新版支持如下功能:

  • 多端部署: 有多种部署方式可选择且功能完备,目前已支持个人微信,微信公众号和企业微信应用等部署方式
  • 基础对话: 私聊及群聊的消息智能回复,支持多轮会话上下文记忆,支持 GPT-3,GPT-3.5,GPT-4模型
  • 语音识别: 可识别语音消息,通过文字或语音回复,支持 azure, baidu, google, openai等多种语音模型
  • 图片生成: 支持图片生成 和 图生图(如照片修复),可选择 Dell-E, stable diffusion, replicate模型
  • 丰富插件: 支持个性化插件扩展,已实现多角色切换、文字冒险、敏感词过滤、聊天记录总结等插件
  • Tool工具: 与操作系统和互联网交互,支持最新信息搜索、数学计算、天气和资讯查询、网页总结,基于 chatgpt-tool-hub 实现

支持 Linux、MacOS、Windows 系统(可在Linux服务器上长期运行),同时需安装 Python。



建议Python版本在 3.7.1~3.9.X 之间,推荐3.8版本,3.10及以上版本在 MacOS 可用,其他系统上不确定能否正常运行。


注意:Docker 或 Railway 部署无需安装python环境和下载源码



Windows、Linux、Mac本地部署


本地部署请参考官方文档,按照文档一步一步操作即可。


注意要安装相对应的环境,例如 Node、Python等,这里不做过多介绍,建议大家用 Docker 方式安装,无需关心环境问题,一个命令直接部署。


环境变量

# config.json文件内容示例
{
"open_ai_api_key": "YOUR API KEY", # 填入上面创建的 OpenAI API KEY
"model": "gpt-3.5-turbo", # 模型名称。当use_azure_chatgpt为true时,其名称为Azure上model deployment名称
"proxy": "127.0.0.1:7890", # 代理客户端的ip和端口
"single_chat_prefix": ["bot", "@bot"], # 私聊时文本需要包含该前缀才能触发机器人回复
"single_chat_reply_prefix": "[bot] ", # 私聊时自动回复的前缀,用于区分真人
"group_chat_prefix": ["@bot"], # 群聊时包含该前缀则会触发机器人回复
"group_name_white_list": ["ChatGPT测试群", "ChatGPT测试群2"], # 开启自动回复的群名称列表
"group_chat_in_one_session": ["ChatGPT测试群"], # 支持会话上下文共享的群名称
"image_create_prefix": ["画", "看", "找"], # 开启图片回复的前缀
"conversation_max_tokens": 1000, # 支持上下文记忆的最多字符数
"speech_recognition": false, # 是否开启语音识别
"group_speech_recognition": false, # 是否开启群组语音识别
"use_azure_chatgpt": false, # 是否使用Azure ChatGPT service代替openai ChatGPT service. 当设置为true时需要设置 open_ai_api_base,如 https://xxx.openai.azure.com/
"azure_deployment_id": "", # 采用Azure ChatGPT时,模型部署名称
"character_desc": "你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。", # 人格描述
# 订阅消息,公众号和企业微信channel中请填写,当被订阅时会自动回复,可使用特殊占位符。目前支持的占位符有{trigger_prefix},在程序中它会自动替换成bot的触发词。
"subscribe_msg": "感谢您的关注!\n这里是ChatGPT,可以自由对话。\n支持语音对话。\n支持图片输出,画字开头的消息将按要求创作图片。\n支持角色扮演和文字冒险等丰富插件。\n输入{trigger_prefix}#help 查看详细指令。"
}

配置说明:


1.个人聊天

  • 个人聊天中,需要以 "bot"或"@bot" 为开头的内容触发机器人,对应配置项 single_chat_prefix (如果不需要以前缀触发可以填写 "single_chat_prefix": [""])
    • 机器人回复的内容会以 "[bot] " 作为前缀, 以区分真人,对应的配置项为 single_chat_reply_prefix (如果不需要前缀可以填写 "single_chat_reply_prefix": "")

    2.群组聊天

  • 群组聊天中,群名称需配置在 group_name_white_list 中才能开启群聊自动回复。如果想对所有群聊生效,可以直接填写 "group_name_white_list": ["ALL_GROUP"]
    • 默认只要被人 @ 就会触发机器人自动回复;另外群聊天中只要检测到以 "@bot" 开头的内容,同样会自动回复(方便自己触发),这对应配置项 group_chat_prefix
    • 可选配置: group_name_keyword_white_list配置项支持模糊匹配群名称,group_chat_keyword配置项则支持模糊匹配群消息内容,用法与上述两个配置项相同。(Contributed by evolay)
    • group_chat_in_one_session:使群聊共享一个会话上下文,配置 ["ALL_GROUP"] 则作用于所有群聊

    3.语音识别

  • 添加 "speech_recognition": true 将开启语音识别,默认使用openai的whisper模型识别为文字,同时以文字回复,该参数仅支持私聊 (注意由于语音消息无法匹配前缀,一旦开启将对所有语音自动回复,支持语音触发画图);
    • 添加 "group_speech_recognition": true 将开启群组语音识别,默认使用openai的whisper模型识别为文字,同时以文字回复,参数仅支持群聊 (会匹配group_chat_prefix和group_chat_keyword, 支持语音触发画图);
    • 添加 "voice_reply_voice": true 将开启语音回复语音(同时作用于私聊和群聊),但是需要配置对应语音合成平台的key,由于itchat协议的限制,只能发送语音mp3文件,若使用wechaty则回复的是微信语音。

    4.其他配置

  • model: 模型名称,目前支持 gpt-3.5-turbotext-davinci-003gpt-4gpt-4-32k (其中gpt-4 api暂未完全开放,申请通过后可使用)
    • temperature,frequency_penalty,presence_penalty: Chat API接口参数,详情参考OpenAI官方文档。
    • proxy:由于目前 openai 接口国内无法访问,需配置代理客户端的地址,详情参考 #351
    • 对于图像生成,在满足个人或群组触发条件外,还需要额外的关键词前缀来触发,对应配置 image_create_prefix
    • 关于OpenAI对话及图片接口的参数配置(内容自由度、回复字数限制、图片大小等),可以参考 对话接口 和 图像接口 文档,在config.py中检查哪些参数在本项目中是可配置的。
    • conversation_max_tokens:表示能够记忆的上下文最大字数(一问一答为一组对话,如果累积的对话字数超出限制,就会优先移除最早的一组对话)
    • rate_limit_chatgptrate_limit_dalle:每分钟最高问答速率、画图速率,超速后排队按序处理。
    • clear_memory_commands: 对话内指令,主动清空前文记忆,字符串数组可自定义指令别名。
    • hot_reload: 程序退出后,暂存微信扫码状态,默认关闭。
    • character_desc 配置中保存着你对机器人说的一段话,他会记住这段话并作为他的设定,你可以为他定制任何人格 (关于会话上下文的更多内容参考该 issue)
    • subscribe_msg:订阅消息,公众号和企业微信channel中请填写,当被订阅时会自动回复, 可使用特殊占位符。目前支持的占位符有{trigger_prefix},在程序中它会自动替换成bot的触发词。

    本说明文档可能会未及时更新,当前所有可选的配置项均在该config.py中列出。


    Railway部署



    Railway 每月提供5刀和最多500小时的免费额度,目前大部分账号已无法免费部署


    1. 进入 Railway
    2. 点击 Deploy Now 按钮。
    3. 设置环境变量来重载程序运行的参数,例如open_ai_api_keycharacter_desc

    Docker方式搭建


    如果想一直跑起来这个项目,建议在自己服务器上搭建,如果在自己本地电脑上搭建,电脑关机后就用不了啦,下面演示的是在我服务器上搭建,和在本地搭建步骤是一样的。


    环境准备

    1. 域名、服务器购买
    2. 服务器环境搭建,需要系统安装docker、docker-compose
    3. docker、docker-compose安装:blog.fanjunyang.zone/archives/de…

    创建相关目录


    我自己放在服务器中 /root/docker_data/wechat_bot 文件夹下面

    mkdir -p /root/docker_data/wechat_bot
    cd /root/docker_data/wechat_bot

    创建yml文件


    /root/docker_data/wechat_bot文件夹下面新建docker-compose.yml文件如下:

    version: '2.0'
    services:
    chatgpt-on-wechat:
    image: zhayujie/chatgpt-on-wechat
    container_name: chatgpt-on-wechat
    security_opt:
    - seccomp:unconfined
    environment:
    OPEN_AI_API_KEY: 'YOUR API KEY'
    MODEL: 'gpt-3.5-turbo'
    PROXY: ''
    SINGLE_CHAT_PREFIX: '["bot", "@bot"]'
    SINGLE_CHAT_REPLY_PREFIX: '"[bot] "'
    GROUP_CHAT_PREFIX: '["@bot"]'
    GROUP_NAME_WHITE_LIST: '["ChatGPT测试群", "ChatGPT测试群2"]'
    IMAGE_CREATE_PREFIX: '["画", "看", "找"]'
    CONVERSATION_MAX_TOKENS: 1000
    SPEECH_RECOGNITION: 'False'
    CHARACTER_DESC: '你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。'
    EXPIRES_IN_SECONDS: 3600
    USE_LINKAI: 'False'
    LINKAI_API_KEY: ''
    LINKAI_APP_CODE: ''

    运行yml文件


    进入/root/docker_data/wechat_bot文件夹下面,运行命令:docker-compose up -d


    或者在任意文件夹下面,运行命令:docker-compose -f /root/docker_data/wechat_bot/docker-compose.yml up -d


    然后服务就跑起来了,运行 sudo docker ps 能查看到 NAMES 为 chatgpt-on-wechat 的容器即表示运行成功。


    使用


    运行以下命令可查看容器运行日志,微信扫描日志中的二维码登录后即可使用:

    sudo docker logs -f chatgpt-on-wechat

    插件使用:

    如果需要在docker容器中修改插件配置,可通过挂载的方式完成,将 插件配置文件 重命名为 config.json,放置于 docker-compose.yml 相同目录下,并在 docker-compose.yml 中的 chatgpt-on-wechat 部分下添加 volumes 映射:

    volumes:
    - ./config.json:/app/plugins/config.json

    作者:Junyang
    链接:https://juejin.cn/post/7259249904777855013
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    工作6年了日期时间格式化还在写YYYY疯狂给队友埋雷

    前言 哈喽小伙伴们好久不见,今天来个有意思的雷,看你有没有埋过。 正文 不多说废话,公司最近来了个外地回来的小伙伴,在广州工作过6年,也是一名挺有经验的开发。 他提交的代码被小组长发现有问题,给打回了,原因是里面日期格式化的用法有问题,用的Simpl...
    继续阅读 »

    前言



    哈喽小伙伴们好久不见,今天来个有意思的雷,看你有没有埋过。



    正文



    不多说废话,公司最近来了个外地回来的小伙伴,在广州工作过6年,也是一名挺有经验的开发。




    他提交的代码被小组长发现有问题,给打回了,原因是里面日期格式化的用法有问题,用的SimpleDateFormat,但不知道是手误还是什么原因,格式用了YYYY-MM-dd。




    这种写法埋了一个不大不小的雷。




    用一段测试代码就可以展示出来问题



    1.jpg



    打印结果如下:



    2.jpg



    很明显,使用YYYY时,2023年变成了2024年,在正常情况下可能没问题,但是在跨年的时候大概率就会有问题了。




    原因比较简单,与小写的yyyy不同,大写的YYYY表示一个基于周的年份。它是根据周计算的年份,而不是基于日历的年份。通常情况下,两者的结果是相同的,但在跨年的第一周或最后一周可能会有差异。




    比如我如果换成2023-12-30又不会有问题了



    3.jpg



    另外,Hutool工具类本身是对Java一些工具的封装,DateUtil里面也有用到SimpleDateFormat,因此也会存在类似的问题。



    4.jpg



    避免这个问题的方法也十分简单,要有公用的格式类,所有使用日期格式的地方都引用这个类,这个类中就定义好yyyy-MM-dd想给的格式即可,这样就不会出现有人手误给大家埋雷了。



    总结




    1. 日期时间格式统一使用yyyy小写;

    2. 日期格式要规定大家都引用定义好的工具类,避免有人手误打错。




    最后再回头想一想,这种小问题并不会马上暴露出来,倘若没有被发现,到了明年元旦,刚好跨年的时候,是不是就要坑死一堆人

    作者:程序员济癫
    来源:juejin.cn/post/7269013062677823528
    了。


    收起阅读 »

    228欢乐马事件,希望大家都能平安健

    iOS
    我这个人体质很奇怪,总能遇到一些奇怪的事。比如六年前半夜打车回家,差点被出租车司机拉到深山老林。比如两年前去福州出差,差点永远回不了家。比如十点从实验室下班,被人半路拦住。比如这次,被人冒充 (在我心里这事和前几件同样恶劣) 不过幸好每次都是化险为夷...
    继续阅读 »

    我这个人体质很奇怪,总能遇到一些奇怪的事。

    • 比如六年前半夜打车回家,差点被出租车司机拉到深山老林。
    • 比如两年前去福州出差,差点永远回不了家。
    • 比如十点从实验室下班,被人半路拦住。
    • 比如这次,被人冒充 (在我心里这事和前几件同样恶劣)

    不过幸好每次都是化险为夷,所以我顺顺利利活到现在。




    事情起因是这样的:


    去年朋友B突然告诉我:发现了你的闲鱼账号。


    :我没有闲鱼啊?


    他给我截图,那个人卖掘金的周边,名字叫LolitaAnn


    因为我遇到太多离谱的事,再加上看的一堆被冒充的新闻,所以我第一反应是:这人也在冒充我


    当时朋友F 说我太敏感了,他觉得只是巧合。


    但我觉得不是巧合,因为LolitaAnn是我自己造的词。




    把我的沸点搬到小红书


    又是某一天, 朋友H告诉我:你的小红书上热门了。


    :?我没有小红书啊?


    然后他们给我看,有个人的小红书完全照搬我的沸点。


    为此我又下载小红书和他对线。起初正常交涉,但是让他删掉,他直接不回复我了,最后还是投诉他,被小红书官方删掉的。




    现在想了想,ip一样,极有可能是一个人干的。




    闲鱼再次被挖出来


    今年,有人在掘金群里说我卖周边。


    我跑到那个群解释,说我被人冒充了。


    群友很热心,都去举报那个人的昵称。昵称被举报下去了。


    但是几天之后:




    看到有人提醒我,它名字又改回来了。


    当时以为是热心群友,后来知道就是它冒充我,现在回想起来一阵恶寒。


    把名字改回去之后还在群里跟炫耀一样,心里想着:我改回来了,你们不知道是我吧。




    冒充我的人被揪出来了


    2.28的时候, 朋友C突然给我发了一段聊天记录。


    是它在群里说 它的咸鱼什么掘金周边都有。结果打开一看,闲鱼是我的名字和头像。




    事情到今天终于知道是谁冒充我了


    虽然Smile只是它的微信小号之一,都没实名认证。但是我还是知道了一些强哥的信息。


    发现是谁冒充我,我第一反应必然是喷一顿。


    刚在群里被我骂完,它脑子也不太好使,马上跑去改了自己掘金和闲鱼的名字。这不就是自爆了? 证明咸鱼和掘金都是他的号。


    奔跑姐妹兄弟(原名一只小小黄鸭) ←点击链接即可鞭尸。






    牵扯出一堆小号


    本来我以为事情已经结束了,我就去群里吐槽他。结果一堆认识他的掘友们给我说它还有别的掘金号。因为它和不同掘友用不同掘金号认识的。所以掘友们给我提供了一堆。我就挨个搜。


    直到我看到了这两条:




    因为我搜欢乐马出来上万个同名账号, 再加上他说自己有脚本,当时我以为都是他的小号。


    后来掘友们提醒我,欢乐马是微信默认生成的,所以有一堆欢乐马,不一定是他的小号。


    但是我确信他有很多小号,比如:




    比如咸鱼卖了六七个掘金鼠标垫,卖了掘金的国行switch……

    • 你们有没有想过为什么fixbug不许助攻了

    • 你们有没有想过为什么矿石贬值了,兑换商店越来越贵了?

    • 你们有没有想过为什么掘金活动必得奖励越来越少了?


    有这种操作在,普通用户根本没办法玩吧。


    所以最后,我就把这个事交给官方了。




    处理结果


    所幸官方很给力,都处理了,感谢各位官方大大。



    本次事件,共涉及主要近期活跃UserID 4个,相关小号570个。 





    我再叨叨几句

    • 卖周边可以, 你别冒充别人卖吧,又不是见不得光,做你自己不丢人。

    • 开小号可以,你别一开开一堆,逼得普通玩家拿不到福利吧。

    • 先成人后成才,不指望能为国家做多大贡献,起码别做蛀虫吧。

    • 又不是没生活,专注点自己的东西,别老偷别人沸点。

    • 我以后改名了嗷。叫Ann的八百万个,别碰瓷你爹了。


    作者:Ann
    链接:https://juejin.cn/post/7206249542751404069
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    LangChain 是 LLM 交响乐的指挥家

    本文分享 LangChain 框架中一个引人入胜的概念 Prompt Templates。如果热衷于语言模型并且热爱 Python,那么将会大饱口福。 深入了解 LangChain 的世界,这个框架在我作为开发者的旅程中改变了游戏规则。 LangChain 是...
    继续阅读 »

    本文分享 LangChain 框架中一个引人入胜的概念 Prompt Templates。如果热衷于语言模型并且热爱 Python,那么将会大饱口福。


    深入了解 LangChain 的世界,这个框架在我作为开发者的旅程中改变了游戏规则。


    LangChain 是一个框架,它一直是我作为开发者旅途中的规则改变者。 LangChain 是一个独特的工具,它利用大语言模型(LLMs)的力量为各种使用案例构建应用程序。Harrison Chase 的这个创意于 2022 年 10 月作为开源项目首次亮相。从那时起,它就成为 GitHub 宇宙中一颗闪亮的明星,拥有高达 42,000 颗星,并有超过 800 名开发者的贡献。


    LangChain 就像一位大师,指挥着 OpenAI 和 HuggingFace Hub 等 LLM 模型以及 Google、Wikipedia、Notion 和 Wolfram 等外部资源的管弦乐队。它提供了一组抽象(链和代理)和工具(提示模板、内存、文档加载器、输出解析器),充当文本输入和输出之间的桥梁。这些模型和组件链接到管道中,这让开发人员能够轻而易举地快速构建健壮的应用程序原型。本质上,LangChain 是 LLM 交响乐的指挥家。


    LangChain 的真正优势在于它的七个关键模块:

    1. 模型:这些是构成应用程序主干的封闭或开源 LLM
    2. 提示:这些是接受用户输入和输出解析器的模板,这些解析器格式化 LLM 模型的输出。
    3. 索引:该模块准备和构建数据,以便 LLM 模型可以有效地与它们交互。
    4. 记忆:这为链或代理提供了短期和长期记忆的能力,使它们能够记住以前与用户的交互。
    5. :这是一种在单个管道(或“链”)中组合多个组件或其他链的方法。
    6. 代理人:根据输入决定使用可用工具/数据采取的行动方案。
    7. 回调:这些是在 LLM 运行期间的特定点触发以执行的函数。

    GitHub:python.langchain.com/


    什么是提示模板?


    在语言模型的世界中,提示是一段文本,指示模型生成特定类型的响应。顾名思义,提示模板是生成此类提示的可重复方法。它本质上是一个文本字符串,可以接收来自最终用户的一组参数并相应地生成提示。


    提示模板可以包含语言模型的说明、一组用于指导模型响应的少量示例以及模型的问题。下面是一个简单的例子:

    from langchain import PromptTemplate

    template = """
    I want you to act as a naming consultant for new companies.
    What is a good name for a company that makes {product}?
    """

    prompt = PromptTemplate(
    input_variables=["product"],
    template=template,
    )

    prompt.format(product="colorful socks")

    在此示例中,提示模板要求语言模型为生产特定产品的公司建议名称。product 是一个变量,可以替换为任何产品名称。


    创建提示模板


    在 LangChain 中创建提示模板非常简单。可以使用该类创建简单的硬编码提示 PromptTemplate。这些模板可以采用任意数量的输入变量,并且可以格式化以生成提示。以下是如何创建一个没有输入变量、一个输入变量和多个输入变量的提示模板:

    from langchain import PromptTemplate

    # No Input Variable 无输入变量
    no_input_prompt = PromptTemplate(input_variables=[], template="Tell me a joke.")
    print(no_input_prompt.format())

    # One Input Variable 一个输入变量
    one_input_prompt = PromptTemplate(input_variables=["adjective"], template="Tell me a {adjective} joke.")
    print(one_input_prompt.format(adjective="funny"))

    # Multiple Input Variables 多个输入变量
    multiple_input_prompt = PromptTemplate(
    input_variables=["adjective", "content"],
    template="Tell me a {adjective} joke about {content}."
    )
    print(multiple_input_prompt.format(adjective="funny", content="chickens"))

    总结


    总之,LangChain 中的提示模板是为语言模型生成动态提示的强大工具。它们提供了对提示的灵活性和控制,能够有效地指导模型的响应。无论是为特定任务创建语言模型还是探索语言模型的功能,提示模板都可以改变游戏规则。


    作者:天行无忌
    链接:https://juejin.cn/post/7247810665242083383
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    AI孙燕姿翻唱爆火,多亏这个开源项目!广西老表带头打造,上手指南已出

    明敏 发自 凹非寺 量子位 | 公众号 QbitAI AI孙燕姿这么快翻唱了这么多首歌,到底是咋实现的? 关键在于一个开源项目。 最近,这波AI翻唱趋势大火,不仅是AI孙燕姿唱的歌越来越多,AI歌手的范围也在扩大,就连制作教程都层出不穷了。 而如果在各大教程...
    继续阅读 »
    明敏 发自 凹非寺 量子位 | 公众号 QbitAI

    AI孙燕姿这么快翻唱了这么多首歌,到底是咋实现的?


    关键在于一个开源项目




    最近,这波AI翻唱趋势大火,不仅是AI孙燕姿唱的歌越来越多,AI歌手的范围也在扩大,就连制作教程都层出不穷了。


    而如果在各大教程中溜达一圈后就会发现,其中的关键秘诀,还是要靠一个名为so-vits-svc的开源项目。



    它提供了一种音色替换的办法,项目在今年3月发布。


    贡献成员应该大部分都来自国内,其中贡献量最高的还是一位玩明日方舟的广西老表。




    如今,项目已经停止更新了,但是星标数量还在蹭蹭上涨,目前已经到了8.4k。


    所以它到底实现了哪些技术能引爆这波趋势?


    一起来看。


    多亏了一个开源项目


    这个项目名叫SoftVC VITS Singing Voice Conversion(歌声转换)。


    它提供了一种音色转换算法,采用SoftVC内容编码器提取源音频语音特征,然后将矢量直接输入VITS,中间不转换成文本,从而保留了音高和语调。


    此外,还将声码器改为NSF HiFiGAN,可以解决声音中断的问题。


    具体分为以下几步:

    • 预训练模型
    • 准备数据集
    • 预处理
    • 训练
    • 推理

    其中,预训练模型这步是关键之一,因为项目本身不提供任何音色的音频训练模型,所以如果你想要做一个新的AI歌手出来,需要自己训练模型。


    而预训练模型的第一步,是准备干声,也就是无音乐的纯人声。


    很多博主使用的工具都是UVR_v5.5.0


    推特博主@歸藏介绍说,在处理前最好把声音格式转成WAV格式,因为So-VITS-SVC 4.0只认这个格式,方便后面处理。


    想要效果好一些,需要处理两次背景音,每次的设置不同,能最大限度提高干声质量。


    得到处理好的音频后,需要进行一些预处理操作。


    比如音频太长容易爆显存,需要对音频切片,推荐5-15秒或者再长一点也OK。


    然后要重新采样到44100Hz和单声道,并自动将数据集划分为训练集和验证集,生成配置文件。再生成Hubert和f0。


    接下来就能开始训练和推理了。


    具体的步骤可以移步GitHub项目页查看(指路文末)。


    值得一提的是,这个项目在今年3月上线,目前贡献者有25位。从贡献用户的简介来看,很多应该都来自国内。


    据说项目刚上线时也有不少漏洞并且需要编程,但是后面几乎每一天都有人在更新和修补,现在的使用门槛已经降低了不少。


    目前项目已经停止更新了,但还是有一些开发者创建了新的分支,比如有人做出了支持实时转换的客户端。




    项目贡献量最多的一位开发者是Miuzarte,从简介地址判断应该来自广西。




    随着想要上手使用的人越来越多,也有不少博主推出了上手难度更低、更详细的食用指南。


    歸藏推荐的方法是使用整合包来推理(使用模型)和训练,还有B站的Jack-Cui展示了Windows下的步骤指南(http://www.bilibili.com/read/cv2237…


    需要注意的是,模型训练对显卡要求还是比较高的,显存小于6G容易出现各类问题。


    Jack-Cui建议使用N卡,他用RTX 2060 S,训练自己的模型大概用了14个小时


    训练数据也同样关键,越多高质量音频,就意味着最后效果可以越好。


    还是会担心版权问题


    值得一提的是,在so-vits-svc的项目主页上,着重强调了版权问题。



    警告:请自行解决数据集的授权问题。因使用未经授权的数据集进行培训而产生的任何问题及其一切后果,由您自行承担责任。存储库及其维护者、svc开发团队,与生成结果无关!





    这和AI画画爆火时有点相似。


    因为AI生成内容的最初数据取材于人类作品,在版权方面的争论不绝于耳。


    而且随着AI作品盛行,已经有版权方出手下架平台上的视频了。


    据了解,一首AI合成的《Heart on My Sleeve》在油管和Tik Tok上爆火,它合成了Drake和Weekend演唱的版本。


    但随后,Drake和Weekend的唱片公司环球音乐将这个视频从平台上下架了,并在声明里向潜在的仿冒者发问,“是要站在艺术家、粉丝和人类创造性表达的一边,还是站在Deepfake、欺诈和拒付艺术家赔偿的一边?”


    此外,歌手Drake也在ins上对AI合成翻唱歌曲表达了不满。


    而另一边,也有人选择拥抱这项技术。


    加拿大歌手Grimes表示,她愿意让别人使用自己的声音合成歌曲,但是要给她一半版权费。


    GitHub地址:

    github.com/svc-develop…


    参考链接:

    [1]mp.weixin.qq.com/s/bXD1u6ysY…

    [2]http://www.vulture.com/article/ai-…


    —  —


    作者:量子位
    链接:https://juejin.cn/post/7231738442274439225
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    记录用前端代替后端生成zip的过程,速度快了 57 倍!!!

    业务场景: 产品有个功能是设置主题。类似手机自动切换壁纸,以及其他功能颜色,icon,字体等。 管理员需要在后端管理系统多次下载不同主题,(至于要干啥就不说了...),主题中可能有 30 ~ 100个高清壁纸, icon 等。现在每次下载主题(31张高清图片)...
    继续阅读 »

    业务场景:


    产品有个功能是设置主题。类似手机自动切换壁纸,以及其他功能颜色,icon,字体等。


    管理员需要在后端管理系统多次下载不同主题,(至于要干啥就不说了...),主题中可能有 30 ~ 100个高清壁纸, icon 等。现在每次下载主题(31张高清图片)至少需要 10s。有什么方法能够优化下。



    因为代码不具备可复用性,因此部分代码直接省略,思路为主


    原始逻辑


      public async getZip(themeId: string, res: any) {
    const theme = await this.model.findById(themeId); // 从数据库

    // 这里需要借用一个服务器上的主题模板文件夹 template/,

    /*
    theme = {
    wallpapers: [
    { url: 'https://亚马逊云.com/1.jpg', ... },
    ...
    ]
    }
    */


    // for 循环遍历 theme.wallpapers , 并通过 fetch 请求 url,将其写进 template/static/wallpapers 文件夹中
    theme.wallpapers.map((item) => {
    const response = await fetch(item.url);
    const buffer = new Uint8Array(await response.arrayBuffer());
    await fs.writeFile(`template/wallpapers/${fileName}`, buffer);
    })

    // ... 还有其他一些处理

    // 将 template 压缩成 zip 文件,发送给前端
    }

    思考 ing ...


    1 利用图片可以被浏览器缓存


    当一次下载主题从请求亚马逊云的图片数据,这步没有问题。 但是当重复下载的时候,之前下载过的图片又会再次下载,操作人员每次都需要等个十几秒,这就不太友好了。这部分时间花费还是挺多的。


    可以利用下浏览器能够将图片缓存到 disk cache 中的特点,将这部分的代码逻辑放到前端完成,因为还需要对压缩包中的文件做一些处理,因此需要借助下 jszip 这个库。


    看下改后的代码



    onDownload () {
    // 请求拿到 theme 数据
    const theme = api.getTheme()
    const template = api.getTemplate() // Blob

    const zip = new JSZip()
    await zip.loadAsync(getTmplResp) // 读取 template.zip 文件数据

    console.time('handle images')
    const wallpaperList = theme.wallpapers
    for (const wallpaper of wallpaperList) {
    const response = await fetch(wallpaper.url) // 请求图片数据
    const buffer = new Uint8Array(await response.arrayBuffer())
    const fileName = wallpaper.url.split('/').pop()
    zip.file(`static/wallpapers/${fileName}`, buffer, { binary: true }) // 写进压缩包
    }
    console.timeEnd('handle images') // 统计用时

    // 还需要读取 template.zip 中的 config.json, 然后修改,重新保存到 template.zip 中
    ...

    // 导出 template.zip
    zip.generateAsync({ type: 'base64' }).then(
    (base64) => {
    const link = document.createElement('a')
    link.href = 'data:application/zip;base64,' + base64
    link.download = 'template.zip'
    link.target = '_blank'
    link.style.display = 'none'

    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
    },
    (err) => {
    console.log('打包失败', err)
    }
    )
    }

    优化完成


    当第一次下载时,handle images 步骤耗时 20 - 21 s,流程和后端差不多。


    当第二次下载时,handle images 步骤耗时 0.35s - 0.45 s。会直接读取 disk cache 中的图片数据,50 ms 内就完成了。


    速度快了 57 倍有余!!!, 你还能想到其他优化方式吗?继续往后看 🍒


    第一次请求各个图片耗时
    image.png


    第二次请求各个图片耗时
    image.png


    2 并发请求


    我们都知道,浏览器会为每个域名维持 6 个 TCP 链接(再拓展还有域名分片知识),我们是否可以利用这个特点做些什么?


    答案是:并发上传


    通过上面的代码,可以看到,每个图片请求都是串行的,一个图片请求完了再进行下一个图片请求。我们一次请求 4 个图片,这样就更快了。


    首先写一个能够管理并发任务的类


    export class TaskQueue {
    public queue: {
    task: <T>() => Promise<T>
    resolve: (value: unknown) => void
    reject: (reason?: any) => void
    }[]
    public runningCount: number // 正在执行的任务数量
    public tasksResloved?: (value: unknown) => void
    public tasksRejected?: (reason?: any) => void

    public constructor(public maxConcurrency: number = 4) { // 最多同时执行 4 个任务
    this.queue = [] // 任务队列
    this.runningCount = 0
    }

    // 添加任务
    public addTask(task) {
    return new Promise((resolve, reject) => {
    this.queue.push({ task, resolve, reject })
    })
    }

    // 执行
    public run() {
    return new Promise((resoved, rejected) => {
    this.tasksResloved = resoved
    this.tasksRejected = rejected
    this.nextTask()
    })
    }

    private nextTask() {
    if (this.queue.length === 0 && this.runningCount === 0) {
    this.tasksResloved?.('done')
    return
    }

    // 如果任务队列中还有任务, 并且没有到最大执行任务数,就继续取出任务执行
    while (this.queue.length > 0 && this.runningCount < this.maxConcurrency) {
    const { task, resolve, reject } = this.queue.shift()
    this.runningCount++
    task()
    .then((res) => {
    this.runningCount--
    resolve(res)
    this.nextTask()
    })
    .catch((e) => {
    this.runningCount--
    reject(e)
    this.nextTask()
    })
    }
    }
    }


    改造代码


    onDownload () {
    // 请求拿到 theme 数据
    const theme = api.getTheme()
    const template = api.getTemplate() // Blob

    const zip = new JSZip()
    await zip.loadAsync(getTmplResp) // 读取 template.zip 文件数据

    console.time('handle images')
    const wallpaperList = theme.wallpapers

    // 注释之前的逻辑
    // for (const wallpaper of wallpaperList) {
    // const response = await fetch(wallpaper.url)
    // const buffer = new Uint8Array(await response.arrayBuffer())
    // const fileName = wallpaper.url.split('/').pop()
    // zip.file(`static/wallpapers/${fileName}`, buffer, { binary: true })
    // }

    const taskQueue = new TaskQueue() // 新建任务队列,默认同时执行 4 个
    for (const wallpaper of wallpaperList) {
    taskQueue
    .addTask(() => fetch(wallpaper.url)) // 添加任务
    .then(async (res) => { // 任务执行完后的回调
    const buffer = new Uint8Array(await (res as Response).arrayBuffer())
    const fileName = wallpaper.url.split('/').pop()
    zip.file(`static/wallpapers/${fileName}`, buffer, { binary: true })
    })
    .catch((e) => console.log('壁纸获取失败', e))
    }
    await taskQueue.run() // 等待所有图片都拿到
    console.timeEnd('handle images') // 统计用时

    // 还需要读取 template.zip 中的 config.json, 然后修改,重新保存到 template.zip 中
    ...

    // 导出 template.zip
    zip.generateAsync({ type: 'base64' }).then(
    (base64) => {
    const link = document.createElement('a')
    link.href = 'data:application/zip;base64,' + base64
    link.download = 'template.zip'
    link.target = '_blank'
    link.style.display = 'none'

    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
    },
    (err) => {
    console.log('打包失败', err)
    }
    )
    }

    大功告成!


    当第一次下载时,handle images 步骤耗时 7 s,速度是之前的 3 倍。


    当第二次下载时,handle images 步骤耗时 0.25s,速度是之前的 1.4 - 1.8


    3 更多的可能


    越来越感觉到计算机网络的重要性, 还有未实现的优化方式:



    1. 域名分片,更多的并发(也有劣势 ,比如 每个域都需要额外的 DNS 查找成本以及建立每个 TCP 连接的开销, TCP 慢启动带宽利用不足)

    2. 升级 HTTP2 这不是靠前端一人能够完成的


    如果学到了新知识,麻烦点个
    作者:marh
    来源:juejin.cn/post/7267418197746270271
    👍 和 ⭐

    收起阅读 »

    只改了五行代码接口吞吐量提升了10多倍

    背景 公司的一个ToB系统,因为客户使用的也不多,没啥并发要求,就一直没有经过压测。这两天来了一个“大客户”,对并发量提出了要求:核心接口与几个重点使用场景单节点吞吐量要满足最低500/s的要求。 当时一想,500/s吞吐量还不简单。Tomcat按照100个线...
    继续阅读 »

    背景


    公司的一个ToB系统,因为客户使用的也不多,没啥并发要求,就一直没有经过压测。这两天来了一个“大客户”,对并发量提出了要求:核心接口与几个重点使用场景单节点吞吐量要满足最低500/s的要求。


    当时一想,500/s吞吐量还不简单。Tomcat按照100个线程,那就是单线程1S内处理5个请求,200ms处理一个请求即可。这个没有问题,平时接口响应时间大部分都100ms左右,还不是分分钟满足的事情。


    然而压测一开,100 的并发,吞吐量居然只有 50 ...


    image.png


    而且再一查,100的并发,CPU使用率居然接近 80% ...




    从上图可以看到几个重要的信息。


    最小值: 表示我们非并发场景单次接口响应时长。还不足100ms。挺好!


    最大值: 并发场景下,由于各种锁或者其他串行操作,导致部分请求等待时长增加,接口整体响应时间变长。5秒钟。有点过分了!!!


    再一看百分位,大部分的请求响应时间都在4s。无语了!!!


    所以 1s钟的 吞吐量 单节点只有 50 。距离 500 差了10倍。 难受!!!!


    分析过程


    定位“慢”原因



    这里暂时先忽略 CPU 占用率高的问题



    首先平均响应时间这么慢,肯定是有阻塞。先确定阻塞位置。重点检查几处:



    • 锁 (同步锁、分布式锁、数据库锁)

    • 耗时操作 (链接耗时、SQL耗时)


    结合这些先配置耗时埋点。



    1. 接口响应时长统计。超过500ms打印告警日志。

    2. 接口内部远程调用耗时统计。200ms打印告警日志。

    3. Redis访问耗时。超过10ms打印告警日志。

    4. SQL执行耗时。超过100ms打印告警日志。


    上述配置生效后,通过日志排查到接口存在慢SQL。具体SQL类似与这种:


    <!-- 主要类似与库存扣减 每次-1 type 只有有限的几种且该表一共就几条数据(一种一条记录)-->
    <!-- 压测时可以认为 type = 1 是写死的 -->
    update table set field = field - 1 where type = 1 and filed > 1;

    上述SQL相当于并发操作同一条数据,肯定存在锁等待。日志显示此处的等待耗时占接口总耗时 80% 以上。


    二话不说先改为敬。因为是压测环境,直接先改为异步执行,确认一下效果。实际解决方案,感兴趣的可以参考另外一篇文章:大量请求同时修改数据库表一条记录时应该如何设计


    PS:当时心里是这么想的: 妥了,大功告成。就是这里的问题!绝壁是这个原因!优化一下就解决了。当然,如果这么简单就没有必要写这篇文章了...


    优化后的效果:


    image.png


    嗯...


    emm...


    好! 这个优化还是很明显的,提升提升了近2倍。




    此时已经感觉到有些不对了,慢SQL已经解决了(异步了~ 随便吧~ 你执行 10s我也不管了),虽然对吞吐量的提升没有预期的效果。但是数据是不会骗人的。


    最大值: 已经从 5s -> 2s


    百分位值: 4s -> 1s


    这已经是很大的提升了。


    继续定位“慢”的原因


    通过第一阶段的“优化”,我们距离目标近了很多。废话不多说,继续下一步的排查。


    我们继续看日志,此时日志出现类似下边这种情况:


    2023-01-04 15:17:05:347 INFO **.**.**.***.50 [TID: 1s22s72s8ws9w00] **********************
    2023-01-04 15:17:05:348 INFO **.**.**.***.21 [TID: 1s22s72s8ws9w00] **********************
    2023-01-04 15:17:05:350 INFO **.**.**.***.47 [TID: 1s22s72s8ws9w00] **********************

    2023-01-04 15:17:05:465 INFO **.**.**.***.234 [TID: 1s22s72s8ws9w00] **********************
    2023-01-04 15:17:05:467 INFO **.**.**.***.123 [TID: 1s22s72s8ws9w00] **********************

    2023-01-04 15:17:05:581 INFO **.**.**.***.451 [TID: 1s22s72s8ws9w00] **********************

    2023-01-04 15:17:05:702 INFO **.**.**.***.72 [TID: 1s22s72s8ws9w00] **********************

    前三行info日志没有问题,间隔很小。第4 ~ 第5,第6 ~ 第7,第7 ~ 第8 很明显有百毫秒的耗时。检查代码发现,这部分没有任何耗时操作。那么这段时间干什么了呢?



    1. 发生了线程切换,换其他线程执行其他任务了。(线程太多了)

    2. 日志打印太多了,压测5分钟日志量500M。(记得日志打印太多是有很大影响的)

    3. STW。(但是日志还在输出,所以前两种可能性很高,而且一般不会停顿百毫秒)


    按照这三个思路做了以下操作:


    首先,提升日志打印级别到DEBUG。emm... 提升不大,好像增加了10左右。


    然后,拆线程 @Async 注解使用线程池,控制代码线程池数量(之前存在3个线程池,统一配置的核心线程数为100)结合业务,服务总核心线程数控制在50以内,同步增加阻塞最大大小。结果还可以,提升了50,接近200了。


    最后,观察JVM的GC日志,发现YGC频次4/s,没有FGC。1分钟内GC时间不到1s,很明显不是GC问题,不过发现JVM内存太小只有512M,直接给了4G。吞吐量没啥提升,YGC频次降低为2秒1次。


    唉,一顿操作猛如虎。


    PS:其实中间还对数据库参数一通瞎搞,这里不多说了。




    其实也不是没有收获,至少在减少服务线程数量后还是有一定收获的。另外,已经关注到了另外一个点:CPU使用率,减少了线程数量后,CPU的使用率并没有明显的下降,这里是很有问题的,当时认为CPU的使用率主要与开启的线程数量有关,之前线程多,CPU使用率较高可以理解。但是,在砍掉了一大半的线程后,依然居高不下这就很奇怪了。


    此时关注的重点开始从代码“慢”方向转移到“CPU高”方向。


    定位CPU使用率高的原因


    CPU的使用率高,通常与线程数相关肯定是没有问题的。当时对居高不下的原因考虑可能有以下两点:



    1. 有额外的线程存在。

    2. 代码有部分CPU密集操作。


    然后继续一顿操作:



    1. 观察服务活跃线程数。

    2. 观察有无CPU占用率较高线程。


    在观察过程中发现,没有明显CPU占用较高线程。所有线程基本都在10%以内。类似于下图,不过有很多线程。


    image.png


    没有很高就证明大家都很正常,只是多而已...


    此时没有下一步的排查思路了。当时想着,算了打印一下堆栈看看吧,看看到底干了啥~


    在看的过程中发现这段日志:


    "http-nio-6071-exec-9" #82 daemon prio=5 os_prio=0 tid=0x00007fea9aed1000 nid=0x62 runnable [0x00007fe934cf4000]
    java.lang.Thread.State: RUNNABLE
    at org.springframework.core.annotation.AnnotationUtils.getValue(AnnotationUtils.java:1058)
    at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory$AspectJAnnotation.resolveExpression(AbstractAspectJAdvisorFactory.java:216)
    at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory$AspectJAnnotation.<init>(AbstractAspectJAdvisorFactory.java:197)
    at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory.findAnnotation(AbstractAspectJAdvisorFactory.java:147)
    at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(AbstractAspectJAdvisorFactory.java:135)
    at org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory.getAdvice(ReflectiveAspectJAdvisorFactory.java:244)
    at org.springframework.aop.aspectj.annotation.InstantiationModelAwarePointcutAdvisorImpl.instantiateAdvice(InstantiationModelAwarePointcutAdvisorImpl.java:149)
    at org.springframework.aop.aspectj.annotation.InstantiationModelAwarePointcutAdvisorImpl.<init>(InstantiationModelAwarePointcutAdvisorImpl.java:113)
    at org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory.getAdvisor(ReflectiveAspectJAdvisorFactory.java:213)
    at org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory.getAdvisors(ReflectiveAspectJAdvisorFactory.java:144)
    at org.springframework.aop.aspectj.annotation.BeanFactoryAspectJAdvisorsBuilder.buildAspectJAdvisors(BeanFactoryAspectJAdvisorsBuilder.java:149)
    at org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator.findCandidateAdvisors(AnnotationAwareAspectJAutoProxyCreator.java:95)
    at org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator.shouldSkip(AspectJAwareAdvisorAutoProxyCreator.java:101)
    at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.wrapIfNecessary(AbstractAutoProxyCreator.java:333)
    at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.postProcessAfterInitialization(AbstractAutoProxyCreator.java:291)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:455)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1808)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:353)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:233)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1282)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1243)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:494)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:349)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:342)
    at cn.hutool.extra.spring.SpringUtil.getBean(SpringUtil.java:117)
    ......
    ......

    上边的堆栈发现了一个点: 在执行getBean的时候,执行了createBean方法。我们都知道Spring托管的Bean都是提前实例化好放在IOC容器中的。createBean要做的事情有很多,比如Bean的初始化,依赖注入其他类,而且中间还有一些前后置处理器执行、代理检查等等,总之是一个耗时方法,所以都是在程序启动时去扫描,加载,完成Bean的初始化。


    而我们在运行程序线程堆栈中发现了这个操作。而且通过检索发现竟然有近200处。


    通过堆栈信息很快定位到执行位置:


    <!--BeanUtils 是 hutool 工具类。也是从IOC容器获取Bean 等价于 @Autowired 注解 -->
    RedisTool redisTool = BeanUtils.getBean(RedisMaster.class);

    而RedisMaster类


    @Component
    @Scope("prototype")
    public class RedisMaster implements IRedisTool {
    // ......
    }

    没错就是用了多例。而且使用的地方是Redis(系统使用Jedis客户端,Jedis并非线程安全,每次使用都需要新的实例),接口对Redis的使用还是比较频繁的,一个接口得有10次左右获取Redis数据。也就是说执行10次左右的createBean逻辑 ...


    叹气!!!


    赶紧改代码,直接使用万能的 new 。


    在看结果之前还有一点需要提一下,由于系统有大量统计耗时的操作。实现方式是通过:


    long start = System.currentTimeMillis();
    // ......
    long end = System.currentTimeMillis();
    long runTime = start - end;


    或者Hutool提供的StopWatch:


    这里感谢一下huoger 同学的评论,当时还误以为该方式能够降低性能的影响,但是实际上也只是一层封装。底层使用的是 System.nanoTime()。


    StopWatch watch = new StopWatch();
    watch.start();
    // ......
    watch.stop();
    System.out.println(watch.getTotalTimeMillis());

    而这种在并发量高的情况下,对性能影响还是比较大的,特别在服务器使用了一些特定时钟的情况下。这里就不多说,感兴趣的可以自行搜索一下。





    最终结果:



    image.png





    排查涉及的命令如下:



    查询服务进程CPU情况: top –Hp pid


    查询JVM GC相关参数:jstat -gc pid 2000 (对 pid [进程号] 每隔 2s 输出一次日志)


    打印当前堆栈信息: jstack -l pid >> stack.log


    总结


    结果是好的,过程是曲折的。总的来说还是知识的欠缺,文章看起来还算顺畅,但都是事后诸葛亮,不对,应该是事后臭皮匠。基本都是边查资料边分析边操作,前后花费了4天时间,尝试了很多。



    • Mysql : Buffer Pool 、Change Buffer 、Redo Log 大小、双一配置...

    • 代码 : 异步执行,线程池参数调整,tomcat 配置,Druid连接池配置...

    • JVM : 内存大小,分配,垃圾收集器都想换...


    总归一通瞎搞,能想到的都试试。


    后续还需要多了解一些性能优化知识,至少要做到排查思路清晰,不瞎搞。




    最后5行代码有哪些:



    1. new Redis实例:1

    2. 耗时统计:3

    3. SQL异步执行 @Async: 1(上图最终的结果是包含该部分的,时间原因未对SQL进行处理,后续会考虑Redis原子操作+定时同步数据库方式来进行,避免同时操数据库)


    TODO


    问题虽然解决了。但是原理还不清楚,需要继续深挖。



    为什么createBean对性能影响这么大?



    如果影响这么大,Spring为什么还要有多例?


    首先非并发场景速度还是很快的。这个毋庸置疑。毕竟接口响应时间不足50ms。


    所以问题一定出在,并发createBean同一对象的锁等待场景。根据堆栈日志,翻了一下Spring源码,果然发现这里出现了同步锁。相信锁肯定不止一处。


    image.png


    org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean


    image.png



    System.currentTimeMillis并发度多少才会对性能产生影响,影响有多大?



    很多公司(包括大厂)在业务代码中,还是会频繁的使用System.currentTimeMillis获取时间戳。比如:时间字段赋值场景。所以,性能影响肯定会有,但是影响的门槛是不是很高。



    继续学习性能优化知识




    • 吞吐量与什么有关?


    首先,接口响应时长。直接影响因素还是接口响应时长,响应时间越短,吞吐量越高。一个接口响应时间100ms,那么1s就能处理10次。


    其次,线程数。现在都是多线程环境,如果同时10个线程处理请求,那么吞吐量又能增加10倍。当然由于CPU资源有限,所以线程数也会受限。理论上,在 CPU 资源利用率较低的场景,调大tomcat线程数,以及并发数,能够有效的提升吞吐量。


    最后,高性能代码。无论接口响应时长,还是 CPU 资源利用率,都依赖于我们的代码,要做高性能的方案设计,以及高性能的代码实现,任重而道远。



    • CPU使用率的高低与哪些因素有关?


    CPU使用率的高低,本质还是由线程数,以及CPU使用时间决定的。


    假如一台10核的机器,运行一个单线程的应用程序。正常这个单线程的应用程序会交给一个CPU核心去运行,此时占用率就是10%。而现在应用程序都是多线程的,因此一个应用程序可能需要全部的CPU核心来执行,此时就会达到100%。


    此外,以单线程应用程序为例,大部分情况下,我们还涉及到访问Redis/Mysql、RPC请求等一些阻塞等待操作,那么CPU就不是时刻在工作的。所以阻塞等待的时间越长,CPU利用率也会越低。也正是因为如此,为了充分的利用CPU资源,多线程也就应运而生(一个线程虽然阻塞了,但是CPU别闲着,赶紧去运行其他的线程)。



    • 一个服务线程数在多少比较合适(算上Tomcat,最终的线程数量是226),执行过程中发现即使tomcat线程数量是100,活跃线程数也很少超过50,整个压测过程基本维持在20
      作者:FishBones
      来源:juejin.cn/post/7185479136599769125
      左右。

    收起阅读 »

    大佬都在用的 IDE 主题,21k Star!

    大家好,我是 Zhero! 作为程序员,在我们的日常工作中,会花费大量时间在编码和阅读代码上。为了减轻眼睛的疲劳并提高工作效率,选择一个优秀的代码编辑器主题非常重要。今天我向大家推荐一款备受欢迎的 JetBrains IDE 主题插件 - Dracula。它采...
    继续阅读 »

    大家好,我是 Zhero!


    作为程序员,在我们的日常工作中,会花费大量时间在编码和阅读代码上。为了减轻眼睛的疲劳并提高工作效率,选择一个优秀的代码编辑器主题非常重要。今天我向大家推荐一款备受欢迎的 JetBrains IDE 主题插件 - Dracula。它采用深色调和高对比度的设计风格,成为黑暗系编程主题的杰出代表。Dracula 主题的界面清晰简洁,代码高亮显示明确鲜明,使得代码结构更加清晰易读。使用 Dracula 主题不仅能减少眼睛的疲劳,还能让我们更专注于代码的编写和理解。如果你正在寻找一个优秀的代码编辑器主题,不妨给 Dracula 一试,相信它会给你带来全新的编程体验。


    来源



    Dracula 主题源自于一种热门的色彩风格,也被称为“Dracula”。它最初由 Zeno Rocha 在 TextMate 编辑器上设计和实现。随着其受欢迎程度的不断增加,Dracula Color Scheme 成为一个跨平台的开源项目,并得到了许多编辑器和 IDE 的支持。


    JetBrains 公司注意到了 Dracula 这种深色调和高对比度的设计,并将其引入了他们的 IDE 产品线。现在,IntelliJ IDEA、PyCharm、WebStorm 等 JetBrains 的 IDE 都提供了官方支持的 Dracula 主题插件。这款黑暗炫彩的主题受到了广大程序员的喜爱,成为了他们工作中常用的选择之一。无论是在日常编码还是阅读代码时,Dracula 主题都能为程序员带来舒适的使用体验。


    设计风格



    Dracula 主题的设计具有以下魅力:

  • 深邃紫罗兰色基调: Dracula 的主题采用深邃的紫罗兰色作为基调,给人一种神秘而吸引人的感觉。
    1. 高对比度的前景和背景: Dracula 主题使用高对比度的前景和背景色,使得代码内容的层次分明,易于阅读和理解。
    2. 强调重要内容的语法高亮: Dracula 主题使用明亮的绿色进行语法高亮,能够清晰地强调代码中的重要部分,帮助程序员更好地理解代码逻辑。
    3. FLAT 扁平化设计风格: Dracula 主题采用简洁大方的 FLAT 扁平化设计风格,界面整洁清晰,让代码更加突出。
    4. 黑客文化与美学融合: Dracula 主题融合了黑客文化中的深色基调和对于对比度和视觉冲击的美学追求。它既展现了黑客式的科技感,又兼具艺术家般的美学气质。

    通过这些设计特征,Dracula 主题确保了代码的可读性,提供了令人愉悦的编程体验,并为开发者们带来了独特的视觉享受。


    优点


    Dracula 主题在技术上具有以下优势:

  • 中性深色背景的精心调配: Dracula 主题采用中性深色背景,软化了强光对眼睛的刺激,减轻了长时间工作导致的眼睛疲劳问题。
    • 明暗分明的前景和背景: Dracula 主题使用明暗分明的前景和背景色,使得代码的视觉层次感强,识别度高,提高了代码的可读性和理解效率。
    • 温暖色菜单栏和标识色边框: Dracula 主题在菜单栏和标识色边框上采用温暖色,增加了页面元素的识别度,帮助用户更好地找到所需功能。
    • 强调重要内容的明亮色彩: Dracula 主题使用明亮的色彩来突出重要的内容,提高了可关注点的辨识度,使开发者能够更快速地定位和理解关键代码部分。
    • 条件颜色支持: Dracula 主题提供了适应不同环境光照条件的条件颜色支持,确保在不同的工作环境中都能有良好的显示效果。

    Dracula 主题带来的用户体验提升包括:

  • 减轻眼睛疲劳问题: 通过精心调配的色彩和对比度,Dracula 主题可以减轻长时间工作导致的眼睛疲劳问题。
    • 提高代码可读性和理解效率: 明暗分明的视觉层次感和明亮色彩的使用使得代码更易于阅读和理解,提高了开发者的工作效率。
    • 丰富的语法色彩增强趣味性: Dracula 主题提供丰富的语法色彩,使得编程过程更具趣味性和乐趣,激发开发者的工作热情。
    • 酷炫的外观满足个性化追求: Dracula 主题具有独特的外观设计,满足技术宅对个性化的追求,让开发环境更具魅力和个性。
    • 对色弱用户友好: Dracula 主题经过精心设计,在保证美观的同时也考虑到了色弱用户的需求,不会造成视觉障碍。

    正因为这些优势,Dracula 主题备受码农的青睐。它极大地提升了 JetBrains IDE 的美观性和可用性,无论是初学者还是老手,都能够享受到 Dracula 主题带来的舒适的用户体验。


    支持产品


    到目前为止,Dracula 主题已经广泛支持341+款应用程序,涵盖了各个平台和工具。除了 JetBrains IDE,Dracula 还适用于许多其他知名的应用程序,其中包括但不限于以下几个:

    • Notepad++: Dracula 主题为 Notepad++ 提供了独特的外观,使得文本编辑器更加美观和舒适。
    • iTerm: Dracula 主题为 iTerm 终端应用程序带来了独特的配色方案,提升了终端界面的可视性和使用体验。
    • Visual Studio Code: Dracula 主题为 Visual Studio Code 提供了一套酷炫且易于辨识的代码颜色方案,让开发者能够更好地编写和调试代码。
    • Vim: Dracula 主题为 Vim 编辑器提供了一种简洁而又优雅的配色方案,使得代码在终端中的显示更加清晰明了。
    • Terminal.app: Dracula 主题为 macOS 上的 Terminal.app 终端应用程序提供了一种时尚和易于识别的配色方案,提升了终端的可用性和美观性。
    • Zsh: Dracula 主题兼容 Zsh 终端的配色方案,使得命令行界面更加美观和个性化。

    这些应用程序只是 Dracula 主题所支持的众多应用程序中的一部分,它们的加入使得 Dracula 主题在各个平台和工具上都能够提供一致的视觉体验,满足开发者对于美观和可用性的追求。



    查看更多支持产品:



    draculatheme.com



    使用


    下面我用 IDEA 实例给大家展示一下如何使用吧!



    1. 前往插件市场,搜索Dracula,点击安装




    1. 前往 Preferences > Appearance & Behavior > Appearance,从下拉菜单中选择Dracula



    1. 前往 Preferences > Editor > Color Scheme,从下拉菜单中选择Dracula



    通过上述步骤,您可以启用Dracula主题啦!


    总结


    Dracula 主题为 JetBrains IDE 带来了卓越的高对比度黑暗风格,本文我为大家介绍一下它的优点。如果你还没有尝试过这款插件,快去试试吧!


    作者:程序员Zhero
    链接:https://juejin.cn/post/7267442470663979069
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    为什么我在公司里访问不了家里的电脑?

    本文为掘金社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究! 上篇文章「为什么我们家里的IP都是192.168开头的?」提到,因为IPv4地址有限,最大42亿个。为了更好的利用这有限的IP数量,网络分为局域网和广域网,将IP分为了私有I...
    继续阅读 »

    本文为掘金社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!



    上篇文章「为什么我们家里的IP都是192.168开头的?」提到,因为IPv4地址有限,最大42亿个。为了更好的利用这有限的IP数量,网络分为局域网和广域网,将IP分为了私有IP和公网IP,一个局域网里的N多台机器都可以共用一个公网IP,从而大大增加了"可用IP数量"。


    收发数据就像收发快递


    当我们需要发送网络包的时候,在IP层,需要填入源IP地址,和目的IP地址,也就是对应快递的发货地址和收货地址。


    IP报头里含有发送和接收IP地址


    但是我们家里的局域网内,基本上都用192.168.xx.xx这样的私有IP


    如果我们在发送网络包的时候,这么填。对方在回数据包的时候该怎么回?毕竟千家万户人用的都是192.168.0.1,网络怎么知道该发给谁?


    所以肯定需要将这个192.168.xx私有IP转换成公有IP


    因此在上篇文章最后,留了这么个问题。局域网内用的是私有IP,公网用的都是公有IP。一个局域网里的私有IP想访问局域网外的公有IP,必然要做个IP转换,这是在哪里做的转换呢?


    私有IP和公有IP在哪进行转换


    答案是NAT设备,全称Network Address Translation,网络地址转换。基本上家用路由器都支持这功能。


    我们来聊下它是怎么工作的。


    NAT的工作原理


    为了简单,我们假设你很富,你家里分到了一个公网IP地址 20.20.20.20,对应配到了你家自带NAT功能的家用路由器上,你家里需要上网的设备有很多,比如你的手机,电脑都需要上网,他们构成了一个局域网,用的都是私有IP,比如192.168.xx。其中你在电脑上执行ifconfig命令,发现家里的电脑IP是192.168.30.5。 你要访问的公网IP地址是30.30.30.30


    于是就有下面这样一张图


    内网IP访问公网IP


    当你准备发送数据包的时候,你的电脑内核协议栈就会构造一个IP数据包。这个IP数据包报头里的发送端IP地址填的就是192.168.30.5接收端IP地址就是30.30.30.30。将数据包发到NAT路由器中。


    此时NAT路由器会将IP数据包里的源IP地址修改一下,私有IP地址192.168.30.5改写为公网IP地址20.20.20.20,这叫SNATSource Network Address Translation,源地址转换)。并且还会在NAT路由器内部留下一条 192.168.30.5 -> 20.20.20.20的映射记录,这个信息会在后面用到。之后IP数据包经过公网里各个路由器的转发,发到了接收端30.30.30.30,到这里发送流程结束。


    SNAT


    如果接收端处理完数据了,需要发一个响应给你的电脑,那就需要将发送端IP地址填上自己的30.30.30.30,将接收端地址填为你的公网IP地址20.20.20.20,发往NAT路由器。NAT路由器收到公网来的消息之后,会检查下自己之前留下的映射信息,发现之前留下了这么一条 192.168.30.5 -> 20.20.20.20记录,就会将这个数据包的目的IP地址修改一下,变成内网IP地址192.168.30.5, 这也叫DNATDestination Network Address Translation,目的地址转换)。 之后将其转发给你的电脑上。


    DNAT


    整个过程下来,NAT悄悄的改了IP数据包的发送和接收端IP地址,但对真正的发送方和接收方来说,他们却对这件事情,一无所知


    这就是NAT的工作原理。




    NAPT的原理


    到这里,相信大家都有一个很大的疑问。


    局域网里并不只有一台机器,局域网内 每台机器都在NAT下留下的映射信息都会是 192.168.xx.xx -> 20.20.20.20,发送消息是没啥事,但接收消息的时候就不知道该回给谁了。


    NAT的问题


    这问题相当致命,因此实际上大部分时候不会使用普通的NAT


    那怎么办呢?


    问题出在我们没办法区分内网里的多个网络连接。


    于是乎。


    我们可以加入其他信息去区分内网里的各个网络连接,很自然就能想到端口。


    但IP数据包(网络层)本身是没有端口信息的。常见的传输层协议TCP和UDP数据报文里才有端口的信息。


    TCP报头有端口号


    UDP报头也有端口号


    于是流程就变成了下面这样子。


    当你准备发送数据包的时候,你的电脑内核协议栈就会先构造一个TCP或者UDP数据报头,里面写入端口号,比如发送端口是5000,接收端口是3000,然后在这个基础上,加入IP数据报头,填入发送端和接收端的IP地址。


    那数据包长这样。


    数据包的构成


    假设,发送端IP地址填的就是192.168.30.5接收端IP地址就是30.30.30.30


    将数据包发到NAT路由器中。


    此时NAT路由器会将IP数据包里的源IP地址和端口号修改一下,从192.168.30.5:5000改写成20.20.20.20:6000。并且还会在NAT路由器内部留下一条 192.168.30.5:5000 -> 20.20.20.20:6000的映射记录。之后数据包经过公网里各个路由器的转发,发到了接收端30.30.30.30:3000,到这里发送流程结束。


    NAPT发送数据


    接收端响应时,就会在数据包里填入发送端地址是30.30.30.30:3000,将接收端20.20.20.20:6000,发往NAT路由器。NAT路由器发现下自己之前留下过这么一条 192.168.30.5:5000 -> 20.20.20.20:6000的记录,就会将这个数据包的目的IP地址和端口修改一下,变回原来的192.168.30.5:5000。 之后将其转发给你的电脑上。


    NAPT接收数据


    如果局域网内有多个设备,他们就会映射到不同的公网端口上,毕竟端口最大可达65535,完全够用。这样大家都可以相安无事。


    像这种同时转换IP和端口的技术,就是NAPT(Network Address Port Transfer , 网络地址端口转换 )。


    看到这里,问题就来了。


    那这么说只有用到端口的网络协议才能被NAT识别出来并转发?


    但这怎么解释ping命令?ping基于ICMP协议,而ICMP协议报文里并不带端口信息。我依然可以正常的ping通公网机器并收到回包。


    ping报头


    事实上针对ICMP协议,NAT路由器做了特殊处理。ping报文头里有个Identifier的信息,它其实指的是放出ping命令的进程id


    对NAT路由器来说,这个Identifier的作用就跟端口一样。


    另外,当我们去抓包的时候,就会发现有两个Identifier,一个后面带个BE(Big Endian),另一个带个LE(Little Endian)


    其实他们都是同一个数值,只不过大小端不同,读出来的值不一样。就好像同样的数字345,反着读就成了543。这是为了兼容不同操作系统(比如linux和Windows)下大小端不同的情况。


    1667783441963


    内网穿透是什么


    看到这里,我们大概也发现了。使用了NAT上网的话,前提得内网机器主动请求公网IP,这样NAT才能将内网的IP端口转成外网IP端口


    反过来公网的机器想主动请求内网机器,就会被拦在NAT路由器上,此时由于NAT路由器并没有任何相关的IP端口的映射记录,因此也就不会转发数据给内网里的任何一台机器。


    举个现实中的场景就是,你在你家里的电脑上启动了一个HTTP服务,地址是192.168.30.5:5000,此时你在公司办公室里想通过手机去访问一下,却发现访问不了。


    那问题就来了,有没有办法让外网机器访问到内网的服务?


    有。


    大家应该听过一句话叫,"没有什么是加中间层不能解决的,如果有,那就再加一层"。


    放在这里,依然适用。


    说到底,因为NAT的存在,我们只能从内网主动发起连接,否则NAT设备不会记录相应的映射关系,没有映射关系也就不能转发数据。


    所以我们就在公网上加一台服务器x,并暴露一个访问域名,再让内网的服务主动连接服务器x,这样NAT路由器上就有对应的映射关系。接着,所有人都去访问服务器x,服务器x将数据转发给内网机器,再原路返回响应,这样数据就都通了。这就是所谓的内网穿透


    像上面提到的服务器x,你也不需要自己去搭,已经有很多现成的方案,花钱就完事了,比如花某壳。


    内网穿透


    到这里,我们就可以回答文章标题的问题。


    为什么我在公司里访问不了家里的电脑?


    那是因为家里的电脑在局域网内,局域网和广域网之间有个NAT路由器。由于NAT路由器的存在,外网服务无法主动连通局域网内的电脑。


    两个内网的聊天软件如何建立通讯


    好了,问题就叒来了。


    我家机子是在我们小区的局域网里,班花家的机子也是在她们小区的局域网里。都在局域网里,且NAT只能从内网连到外网,那我电脑上登录的QQ是怎么和班花电脑里的QQ连上的呢?


    两个局域网内的服务无法直接连通


    上面这个问法其实是存在个误解,误以为两个qq客户端应用是直接建立连接的。


    然而实际上并不是,两个qq客户端之间还隔了一个服务器。


    聊天软件会主动与公网服务器建立连接


    也就是说,两个在内网的客户端登录qq时都会主动向公网的聊天服务器建立连接,这时两方的NAT路由器中都会记录有相应的映射关系。当在其中一个qq上发送消息时,数据会先到服务器,再通过服务器转发到另外一个客户端上。反过来也一样,通过这个方式让两台内网的机子进行数据传输。


    两个内网的应用如何直接建立连接


    上面的情况,是两个客户端通过第三方服务器进行通讯,但有些场景就是要抛开第三端,直接进行两端通信,比如P2P下载,这种该怎么办呢?


    这种情况下,其实也还是离不开第三方服务器的帮助。


    假设还是A和B两个局域网内的机子,A内网对应的NAT设备叫NAT_A,B内网里的NAT设备叫NAT_B,和一个第三方服务器server


    流程如下。


    step1和2: A主动去连server,此时A对应的NAT_A就会留下A的内网地址和外网地址的映射关系,server也拿到了A对应的外网IP地址和端口。


    step3和4: B的操作和A一样,主动连第三方server,NAT_B内留下B的内网地址和外网地址的映射关系,然后server也拿到了B对应的外网IP地址和端口。


    step5和step6以及step7: 重点来了。此时server发消息给A,让A主动发UDP消息到B的外网IP地址和端口。此时NAT_B收到这个A的UDP数据包时,这时候根据NAT_B的设置不同,导致这时候有可能NAT_B能直接转发数据到B,那此时A和B就通了。但也有可能不通,直接丢包,不过丢包没关系,这个操作的目的是给NAT_A上留下有关B的映射关系


    step8和step9以及step10: 跟step5一样熟悉的配方,此时server再发消息给B,让B主动发UDP消息到A的外网IP地址和端口。NAT_B上也留下了关于A到映射关系,这时候由于之前NAT_A上有过关于B的映射关系,此时NAT_A就能正常接受B的数据包,并将其转发给A。到这里A和B就能正常进行数据通信了。这就是所谓的NAT打洞


    step11: 注意,之前我们都是用的UDP数据包,目的只是为了在两个局域网的NAT上打个洞出来,实际上大部分应用用的都是TCP连接,所以,这时候我们还需要在A主动向B发起TCP连接。到此,我们就完成了两端之间的通信。


    NAT打洞


    这里估计大家会有疑惑。


    端口已经被udp用过了,TCP再用,那岂不是端口重复占用(address already in use)?

    其实并不会,端口重复占用的报错常见于两个TCP连接在不使用SO_REUSEADDR的情况下,重复使用了某个IP端口。而UDP和TCP之间却不会报这个错。之所以会有这个错,主要是因为在一个linux内核中,内核收到网络数据时,会通过五元组(传输协议,源IP,目的IP,源端口,目的端口)去唯一确定数据接受者。当五元组都一模一样的时候,内核就不知道该把数据发给谁。而UDP和TCP之间"传输协议"不同,因此五元组也不同,所以也就不会有上面的问题。五元组


    NAPT还分为好多种类型,上面的nat打洞方案,都能成功吗?

    关于NAPT,确实还细分为好几种类型,比如完全锥形NAT和限制型NAT啥的,但这并不是本文的重点。所以我就略过了。我们现在常见的都是锥形NAT。上面的打洞方案适用于大部分场景,这其中包括限制最多的端口受限锥形NAT


    1668247032737


    总结



    • IPV4地址有限,但通过NAT路由器,可以使得整个内网N多台机器,对外只使用一个公网IP,大大节省了IP资源。

    • 内网机子主动连接公网IP,中间的NAT会将内网机子的内网IP转换为公网IP,从而实现内网和外网的数据交互。

    • 普通的NAT技术,只会修改网络包中的发送端和接收端IP地址,当内网设备较多时,将有可能导致冲突。因此一般都会使用NAPT技术,同时修改发送端和接收端的IP地址和端口

    • 由于NAT的存在,公网IP是无法访问内网服务的,但通过内网穿透技术,就可以让公网IP访问内网服务。一波操作下来,就可以在公司的网络里访问家里的电脑。


    最后留个问题,有了NAT之后,原本并不富裕的IPv4地址突然就变得非常够用了。


    那我们为什么还需要IPv6?


    另外IPv6号称地址多到每粒沙子都能拥有自己的IP地址,那我们还需要NAT吗?


    最后


    最近原创更文的阅读量稳步下跌,思前想后,夜里辗转反侧。


    我有个不成熟的请求。





    离开广东好长时间了,好久没人叫我靓仔了。


    大家可以在评论区里,叫我一靓仔吗?


    最近评论区里叫我diao毛的兄弟越来越多了。


    so emo. 哪有什么diao毛,在你面前的,不过是一个漂泊在外,思念故乡的可怜打工人而已。


    所以。


    我这么善良质朴的愿望,能被满足吗?


    别说了,一起在知识的海洋里呛水吧


    作者:小白debug
    来源:juejin.cn/post/7170850066473680927
    收起阅读 »

    一个月后,我们又从 MySQL 双主切换成了主 - 从

    一、遇到的坑 一个月前,我们在测试环境部署了一套 MySQL 高可用架构,也就是 MySQL 双主 + Keepalived 的模式。 在这一个月遇到了很多坑: 因为两个 MySQL 节点都可以写入,极其容易造成主键重复,进而导致主从同步失败。 同步失败后,...
    继续阅读 »

    一、遇到的坑


    一个月前,我们在测试环境部署了一套 MySQL 高可用架构,也就是 MySQL 双主 + Keepalived 的模式。


    在这一个月遇到了很多坑



    • 因为两个 MySQL 节点都可以写入,极其容易造成主键重复,进而导致主从同步失败。

    • 同步失败后,Slave_SQL_Thread 线程就停了,除非解决了同步的错误,才能继续进行同步。

    • 同步失败的错误,不会只有一条记录有问题,往往是一大片的同步问题。

    • 两个节点互相缺少对方的数据。

    • 主从的同步延迟,切换到新主库后,数据不是最新。

    • 当出现不一致时,无法确定以哪个库为准。


    造成上面问题的主要原因就是因为两个节点都支持写入 + 双主可以随时切换。


    解决这种问题的方案有 改进自增主键的步长(影响未评估),使用 GTID 方案(未验证)。即使这样,双主同步的风险还是有,而且不同步后,如何处理是个大难题。


    那么回到我们最初的想法:为什么会选择双主?


    最开始的目的就是为了高可用。双主就是说有一台 MySQL 节点挂了,另外一台能够顶上,对于用户来说是无感的,给运维人员一定的缓冲时间来排查 MySQL 故障。另外老的主节点恢复后,不用改配置就能立即成为从节点。


    经过这一个月的 MySQL 双主模式的试运行,最后我们还是决定切换到 MySQL 主 - 从模式。


    双主模式就是两个节点即是主节点也是从节点,那我们现在切换到一主一从模式,就可以认为是降级。接下来我们聊聊双主换成主从的思路和步骤。


    二、双主降为主从


    双主模式


    双主模式的原理图如下:



    两个主节点,都安装了 KeepAlived 高可用组件,对外提供了一个 VIP,只有一个节点接管 VIP,客户端访问的请求都是到这个 VIP,另外一个节点处于待机状态。


    主从模式


    和双主不一样的地方如下,从节点是只读的。



    一主一从是主从模式中的一种,具有以下特点:



    • 一个主节点,一个从节点,主节点提供给客户端访问,从节点只通过主节点的 binlog 进行数据同步。

    • 从节点是只读的。从节点可以作为只读节点提供类似报表查询等耗时读操作。

    • 主节点宕机后,从节点成为主节点,也是高可用的一种方案。


    相对于双主的高可用方案,不同之处如下:



    • 主从切换需要用脚本将从库设置为可读可写。

    • 主从切换后,需要将从库设置为不同步老主库。

    • 主从切换后,老的主库恢复后,需要人工设置为只读,且开启同步新主库的功能。


    这样来看,主从模式在异常情况下,多了些人工操作。


    在异常情况下,主从切换一般是这样处理的:通过脚本监测主节点是否宕机,如果主库宕机了,则从库自动切换为新的主库,待老主库恢复后,就作为从库同步新主库数据,新主库上的 Keepalived 接管 VIP。


    目前改为主从模式有两种方式:



    • 简单方式:人工切换模式,主节点故障后需要人工切换主从。

    • 复杂方式:高可用方式,主节点故障后,主从自动切换,读写分离自动切换。


    本篇只涉及简单方式,复杂方式的原理和配置步骤放到下篇专门讲解。


    三、改为主从的简单方式


    简单方式的主从切换流程如下:



    和双主模式的主从切换的区别是,从节点是只读的,Keepalived 没有启动,需要人工操作主从切换和启动 Keepalived。


    修改配置的步骤如下


    ① 为了避免从节点上的 Keepalived 自动接管 VIP 的情况出现,将从节点的 Keepalived 停止,如果遇到主节点故障,则需要人工干预来进行主从切换。从节点切换为主节点后,重新启动从节点 Keepalived。


    systemctl status keepalived

    ② 保留主节点的 Keepalived,保证 MySQL 的连接信息都不需要变。


    ③ 主节点 node1 停用 MySQL 的同步线程。


    STOP SLAVE

    ④ 从节点 node2 设置 MySQL 为只读模式。


    # 修改 my.cnf 文件read_only = 1

    ⑤ 移除主节点 node1 同步 node2 MySQL 的权限。


    ⑥ 从节点 node1 的开机启动项中移除 keepalived 服务自启动。


    # 修改启动项配置sudo vim /etc/rc.local# 移除以下脚本systemctl start keepalived

    四、总结


    双主高可用的坑确实比较多,没有 MySQL 的硬核知识真的很难搞定。笔者在这一个月的实践中,深刻体会到了双主同步的难点所在,最后还是选择了一主一从的模式。


    另外因为最开始的配置都是双主模式下的,所以要修改一些配置,来改为主从模式。因项目时间比较紧,目前采取的是非高可用的主从模式。


    作者:优雅程序员阿鑫
    来源:juejin.cn/post/7136841690802814989
    收起阅读 »

    都用HTTPS了,还能被查出浏览记录?

    大家好,我卡颂。 最近,群里一个刚入职的小伙因为用公司电脑访问奇怪的网站,被约谈了。他很困惑 —— 访问的都是HTTPS的网站,公司咋知道他访问了啥? 实际上,由于网络通信有很多层,即使加密通信,仍有很多途径暴露你的访问地址,比如: DNS查询:通常DN...
    继续阅读 »

    大家好,我卡颂。


    最近,群里一个刚入职的小伙因为用公司电脑访问奇怪的网站,被约谈了。他很困惑 —— 访问的都是HTTPS的网站,公司咋知道他访问了啥?



    实际上,由于网络通信有很多层,即使加密通信,仍有很多途径暴露你的访问地址,比如:




    • DNS查询:通常DNS查询是不会加密的,所以,能看到你DNS查询的观察者(比如运营商)是可以推断出访问的网站




    • IP地址:如果一个网站的IP地址是独一无二的,那么只需看到目标 IP地址,就能推断出用户正在访问哪个网站。当然,这种方式对于多网站共享同一个IP地址(比如CDN)的情况不好使




    • 流量分析:当访问一些网站的特定页面,可能导致特定大小和顺序的数据包,这种模式可能被用来识别访问的网站




    • cookies或其他存储:如果你的浏览器有某个网站的cookies,显然这代表你曾访问过该网站,其他存储信息(比如localStorage)同理




    除此之外,还有很多方式可以直接、间接知道你的网站访问情况。


    本文将聚焦在HTTPS协议本身,聊聊只考虑HTTPS协议的情况下,你的隐私是如何泄露的。


    欢迎围观朋友圈、加入人类高质量前端交流群,带飞


    HTTPS简介


    我们每天访问的网站大部分是基于HTTPS协议的,简单来说,HTTPS = HTTP + TLS,其中:




    • HTTP是一种应用层协议,用于在互联网上传输超文本(比如网页内容)。由于HTTP是明文传递,所以并不安全




    • TLS是一种安全协议。TLS在传输层对数据进行加密,确保任何敏感信息在两端(比如客户端和服务器)之间安全传输,不被第三方窃取或篡改




    所以理论上,结合了HTTPTLS特性的HTTPS,在数据传输过程是被加密的。但是,TLS建立连接的过程却不一定是加密的。


    TLS的握手机制


    当我们通过TLS传递加密的HTTP信息之前,需要先建立TLS连接,比如:




    • 当用户首次访问一个HTTPS网站,浏览器开始查询网站服务器时,会发生TLS连接




    • 当页面请求API时,会发生TLS连接




    建立连接的过程被称为TLS握手,根据TLS版本不同,握手的步骤会有所区别。



    但总体来说,TLS握手是为了达到三个目的:




    1. 协商协议和加密套件:通信的两端确认接下来使用的TLS版本及加密套件




    2. 验证省份:为了防止“中间人”攻击,握手过程中,服务器会向客户端发送其证书,包含服务器公钥和证书授权中心(即CA)签名的身份信息。客户端可以使用这些信息验证服务器的身份




    3. 生成会话密钥:生成用于加密接下来数据传输的密钥




    TLS握手机制的缺点


    虽然TLS握手机制会建立安全的通信,但在握手初期,数据却是明文发送的,这就造成隐私泄漏的风险。


    在握手初期,客户端、服务端会依次发送、接收对方的打招呼信息。首先,客户端会向服务端打招呼(发送client hello信息),该消息包含:




    • 客户端支持的TLS版本




    • 支持的加密套件




    • 一串称为客户端随机数client random)的随机字节




    • SNI等一些服务器信息




    服务端接收到上述消息后,会向客户端打招呼(发送server hello消息),再回传一些信息。


    其中,SNIServer Name Indication,服务器名称指示)就包含了用户访问的网站域名。


    那么,握手过程为什么要包含SNI呢?


    这是因为,当多个网站托管在一台服务器上并共享一个IP地址,且每个网站都有自己的SSL证书时,那就没法通过IP地址判断客户端是想和哪个网站建立TLS连接,此时就需要域名信息辅助判断。


    打个比方,快递员送货上门时,如果快递单只有收货的小区地址(IP地址),没有具体的门牌号(域名),那就没法将快递送到正确的客户手上(与正确的网站建立TLS连接)。


    所以,SNI作为TLS的扩展,会在TLS握手时附带上域名信息。由于打招呼的过程是明文发送的,所以在建立HTTPS连接的过程中,中间人就能知道你访问的域名信息。


    企业内部防火墙的访问控制和安全策略,就是通过分析SNI信息完成的。



    虽然防火墙可能已经有授信的证书,但可以先分析SNI,根据域名情况再判断要不要进行深度检查,而不是对所有流量都进行深度检查



    那么,这种情况下该如何保护个人隐私呢?


    Encrypted ClientHello


    Encrypted ClientHelloECH)是TLS1.3的一个扩展,用于加密Client Hello消息中的SNI等信息。


    当用户访问一个启用ECH的服务器时,网管无法通过观察SNI来窥探域名信息。只有目标服务器才能解密ECH中的SNI,从而保护了用户的隐私。



    当然,对于授信的防火墙还是不行,但可以增加检查的成本



    开启ECH需要同时满足:




    • 服务器支持TLSECH扩展




    • 客户端支持ECH




    比如,cloudflare SNI测试页支持ECH扩展,当你的浏览器不支持ECH时,访问该网站sni会返回plaintext



    对于chrome,在chrome://flags/#encrypted-client-hello中,配置ECH支持:



    再访问上述网站,sni如果返回encrypted则代表支持ECH


    总结


    虽然HTTPS连接本身是加密的,但在建立HTTPS的过程中(TLS握手),是有数据明文传输的,其中SNI中包含了服务器的域名信息。


    虽然SNI信息的本意是解决同一IP下部署多个网站,每个网站对应不同的SSL证书,但也会泄漏访问的网站地址


    ECH通过对TLS握手过程中的敏感信息(主要是SNI)进行加密,为用户提供了更强的隐私保护。


    作者:魔术师卡颂
    来源:juejin.cn/post/7264753569834958908
    收起阅读 »

    鹅厂组长,北漂 10 年,有房有车,做了一个违背祖宗的决定

    前几天是 10 月 24 日,有关注股票的同学,相信大家都过了一个非常难忘的程序员节吧。在此,先祝各位朋友们身体健康,股票基金少亏点,最重要的是不被毕业。 抱歉,当了回标题党,不过在做这个决定之前确实纠结了很久,权衡了各种利弊,就我个人而言,不比「3Q 大战」...
    继续阅读 »

    前几天是 10 月 24 日,有关注股票的同学,相信大家都过了一个非常难忘的程序员节吧。在此,先祝各位朋友们身体健康,股票基金少亏点,最重要的是不被毕业。


    抱歉,当了回标题党,不过在做这个决定之前确实纠结了很久,权衡了各种利弊,就我个人而言,不比「3Q 大战」时腾讯做的「艰难的决定」来的轻松。如今距离这个决定过去了快 3 个月,我也还在适应着这个决定带来的变化。


    按照工作汇报的习惯,先说结论:



    在北漂整整 10 年后,我回老家合肥上班了



    做出这个决定的唯一原因:



    没有北京户口,积分落户陪跑了三年,目测 45 岁之前落不上



    户口搞不定,意味着孩子将来在北京只能考高职,这断然是不能接受的;所以一开始是打算在北京读几年小学后再回老家,我也能多赚点钱,两全其美。


    因为我是一个人在北京,如果在北京上小学,就得让我老婆或者让我父母过来。可是我老婆的职业在北京很难就业,我父母年龄大了,北京人生地不熟的,而且那 P 点大的房子,住的也憋屈。而将来一定是要回去读书的,这相当于他们陪着我在北京折腾了。


    或者我继续在北京打工赚钱,老婆孩子仍然在老家?之前的 6 年基本都是我老婆在教育和陪伴孩子,我除了逢年过节,每个月回去一到两趟。孩子天生过敏体质,经常要往医院跑,生病时我也帮不上忙,所以时常被抱怨”丧偶式育儿“,我也只能跟渣男一样说些”多喝热水“之类的废话。今年由于那啥,有整整 4 个多月没回家了,孩子都差点”笑问客从何处来“了。。。


    5月中旬,积分落户截止,看到贴吧上网友晒出的分数和排名,预计今年的分数线是 105.4,而实际分数线是 105.42,比去年的 100.88 多了 4.54 分。而一般人的年自然增长分数是 4 分,这意味着如果没有特殊加分,永远赶不上分数线的增长。我今年的分数是 90.8,排名 60000 左右,每年 6000 个名额,即使没有人弯道超车,落户也得 10 年后了,孩子都上高一了,不能在初二之前搞到户口,就表示和大学说拜拜了。


    经过我的一番仔细的测算,甚至用了杠杆原理和人品守恒定理等复杂公式,最终得到了如下结论:



    我这辈子与北京户口无缘了



    所以,思前想后,在没有户口的前提下,无论是老婆孩子来北京,还是继续之前的异地,都不是好的解决方案。既然将来孩子一定是在合肥高考,为了减少不必要的折腾,那就只剩唯一的选择了,我回合肥上班,兼顾下家里。


    看上去是个挺自然的选择,但是:



    我在腾讯是组长,团队 20 余人;回去是普通工程师,工资比腾讯打骨折



    不得不说,合肥真的是互联网洼地,就没几个公司招人,更别说薪资匹配和管理岗位了。因此,回合肥意味着我要放弃”高薪“和来之不易的”管理“职位,从头开始,加上合肥这互联网环境,基本是给我的职业生涯判了死刑。所以在 5 月底之前就没考虑过这个选项,甚至 3 月份时还买了个显示器和 1.6m * 0.8m 的大桌子,在北京继续大干一场,而在之前的 10 年里,我都是用笔记本干活的,从未用过外接显示器。


    5 月初,脉脉开始频繁传出毕业的事,我所在的部门因为是盈利的,没有毕业的风险。但是营收压力巨大,作为底层的管理者,每天需要处理非常非常多的来自上级、下级以及甲方的繁杂事务,上半年几乎都是凌晨 1 点之后才能睡觉。所以,回去当个普通工程师,每天干完手里的活就跑路,貌似也不是那么不能接受。毕竟自己也当过几年 leader 了,leader 对自己而言也没那么神秘,况且我这还是主动激流勇退,又不是被撸下来的。好吧,也只能这样安慰自己了,中年人,要学会跟自己和解。后面有空时,我分享下作为 leader 和普通工程师所看到的不一样的东西。


    在艰难地说服自己接受之后,剩下的就是走各种流程了:

    1. 5月底,联系在合肥工作的同学帮忙内推;6月初,通过面试。我就找了一家,其他家估计性价比不行,也不想继续面了
    2. 6月底告诉总监,7月中旬告诉团队,陆续约或被约吃散伙饭
    3. 7月29日,下午办完离职手续,晚上坐卧铺离开北京
    4. 8月1日,到新公司报道

    7 月份时,我还干了一件大事,耗时两整天,历经 1200 公里,不惧烈日与暴雨,把我的本田 125 踏板摩托车从北京骑到了合肥,没有拍视频,只能用高德的导航记录作为证据了:




    这是导航中断的地方,晚上能见度不行,在山东花了 70 大洋,随便找了个宾馆住下了,第二天早上出发时拍的,发现居然是水泊梁山附近,差点落草为寇:




    骑车这两天,路上发生了挺多有意思的事,以后有时间再分享。到家那天,是我的结婚 10 周年纪念日,我没有提前说我要回来,更没说骑着摩托车回来,当我告诉孩子他妈时,问她我是不是很牛逼,得到的答复是:



    我觉得你是傻逼



    言归正传,在离开北京前几天,我找团队里的同学都聊了聊,对我的选择,非常鲜明的形成了两个派系:

    1. 未婚 || 工作 5 年以内的:不理解,为啥放弃管理岗位,未来本可以有更好的发展的,太可惜了,打骨折的降薪更不能接受

    2. 已婚 || 工作 5 年以上的:理解,支持,甚至羡慕;既然迟早都要回去,那就早点回,多陪陪家人,年龄大了更不好回;降薪很正常,跟房价也同步,不能既要又要

    确实,不同的人生阶段有着不同的想法,我现在是第 2 阶段,需要兼顾家庭和工作了,不能像之前那样把工作当成唯一爱好了。


    在家上班的日子挺好的,现在加班不多,就是稍微有点远,单趟得 1 个小时左右。晚上和周末可以陪孩子玩玩,虽然他不喜欢跟我玩🐶。哦,对了,我还有个重要任务 - 做饭和洗碗。真的是悔不当初啊,我就不应该说会做饭的,更不应该把饭做的那么好吃,现在变成我工作以外的最重要的业务了。。。


    比较难受的是,现在公司的机器配置一般,M1 的 MBP,16G 内存,512G 硬盘,2K 显示器。除了 CPU 还行,内存和硬盘,都是快 10 年前的配置了,就这还得用上 3 年,想想就头疼,省钱省在刀刃上了,属于是。作为对比,腾讯的机器配置是:



    M1 Pro MBP,32G 内存 + 1T SSD + 4K 显示器


    客户端开发,再额外配置一台 27寸的 iMac(i9 + 32G内存 + 1T SSD)



    由奢入俭难,在习惯了高配置机器后,现在的机器总觉得速度不行,即使很多时候,它和高配机没有区别。作为开发,尤其是客户端开发,AndroidStudio/Xcode 都是内存大户,16G 实在是捉襟见肘,非常影响搬砖效率。公司不允许用自己的电脑,否则我就自己买台 64G 内存的 MBP 干活用了。不过,换个角度,编译时间变长,公司提供了带薪摸鱼的机会,也可以算是个福利🐶


    另外,比较失落的就是每个月发工资的日子了,比之前少了太多了,说没感觉是不可能的,还在努力适应中。不过这都是小事,毕竟年底发年终奖时,会更加失落,hhhh😭😭😭😭


    先写这么多吧,后面有时间的话,再分享一些有意思的事吧,工作上的或生活上的。


    遥想去年码农节时,我还在考虑把房子从昌平换到海淀,好让孩子能有个“海淀学籍”,当时还做了点笔记:




    没想到,一年后的我回合肥了,更想不到一年后的腾讯,股价竟然从 500 跌到 206 了(10月28日,200.8 了)。真的是世事难料,大家保重身体,好好活着,多陪陪家人,一起静待春暖花开💪🏻💪🏻


    作者:野生的码农
    链接:https://juejin.cn/post/7159837250585362469
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    Hook神器—Frida安装

    什么是Frida Frida is Greasemonkey for native apps, or, put in more technical terms, it’s a dynamic code instrumentation toolkit. It ...
    继续阅读 »

    什么是Frida



    Frida is Greasemonkey for native apps, or, put in more technical terms, it’s a dynamic code instrumentation toolkit. It lets you inject snippets of JavaScript into native apps that run on Windows, Mac, Linux, iOS and Android. Frida is an open source software.



    frida 是平台原生 appGreasemonkey ,说的专业一点,就是一种动态插桩工具,可以插入一些代码到原生 app 的内存空间去,(动态地监视和修改其行为),这些原生平台可以是WinMacLinuxAndroid 或者 iOS 。而且 frida 还是开源的。


    Frida安装


    Python3安装


    安装Frida之前需要电脑有Python环境的,Python3的安装可以参考这篇文章。Python安装之后,还要检查是否安装了pip3,如果没有安装的话,需要自行查找安装的方法。


    安装Frida

    1.安装frida

    pip3 install frida

    2.安装frida-tools

    pip3 install frida-tools

    3.安装objection

    pip3 install objection

    执行完以上命令就完成了frida的安装,上面的命令安装的都是最新版本的


    安装frida-server


    安装frida-server之前需要知道Android手机的cpu架构,命令如下

    adb shell getprop ro.product.cpu.abi 

    还要知道电脑安装的frida的版本,frida-server的版本要与电脑端的frida版本相同,查看电脑端的frida版本的命令如下,

    frida --version

    知道了Android手机的cpu架构和frida的版本,到github下载相应版本的frida-server,github地址点击这里




    测试是否安装成功


    启动frida-server

    1. 将下载的frida-server压缩包解压
    2. 解压后的文件push到手机
    3. 启动frida-server服务

    上面步骤的对应命令如下

    $ adb root 
    $ adb push frida-server-16.0.8-android-arm /data/local/tmp
    $ adb shell
    $ su
    $ cd /data/local/tmp
    $ chmod 777 /data/local/tmp/frida-server-16.0.8-android-arm
    $ ./frida-server-16.0.8-android-arm

    端口映射


    启动frida-server之后还要进行端口映射,否则电脑无法连接到手机

    adb forward tcp:27042 tcp:27042
    adb forward tcp:27043 tcp:27043

    查看进程


    上面的步骤都完成后,就可以执行下面的命令,获取手机当前的进程信息

    frida-ps -U

    如果能看到你手机的进程信息,如图




    则说明你的frida和frida-server安装配置成功。


    作者:平行绳
    链接:https://juejin.cn/post/7229883377142104125
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    教你如何实现接口防刷

    教你如何实现接口防刷 前言 我们在浏览网站后台的时候,假如我们频繁请求,那么网站会提示 “请勿重复提交” 的字样,那么这个功能究竟有什么用呢,又是如何实现的呢? 其实这就是接口防刷的一种处理方式,通过在一定时间内限制同一用户对同一个接口的请求次数,其目的是为了...
    继续阅读 »

    教你如何实现接口防刷


    前言


    我们在浏览网站后台的时候,假如我们频繁请求,那么网站会提示 “请勿重复提交” 的字样,那么这个功能究竟有什么用呢,又是如何实现的呢?


    其实这就是接口防刷的一种处理方式,通过在一定时间内限制同一用户对同一个接口的请求次数,其目的是为了防止恶意访问导致服务器和数据库的压力增大,也可以防止用户重复提交。


    思路分析


    接口防刷有很多种实现思路,例如:拦截器/AOP+Redis、拦截器/AOP+本地缓存、前端限制等等很多种实现思路,在这里我们来讲一下 拦截器+Redis 的实现方式。


    其原理就是 在接口请求前由拦截器拦截下来,然后去 redis 中查询是否已经存在请求了,如果不存在则将请求缓存,若已经存在则返回异常。具体可以参考下图




    具体实现



    注:以下代码中的 AjaxResult 为统一返回对象,这里就不贴出代码了,大家可以根据自己的业务场景来编写。



    编写 RedisUtils

    import com.apply.core.exception.MyRedidsException;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Component;
    import org.springframework.util.CollectionUtils;

    import java.util.Collections;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    import java.util.concurrent.TimeUnit;

    /**
    * Redis工具类
    */
    @Component
    public class RedisUtils {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /****************** common start ****************/
    /**
    * 指定缓存失效时间
    *
    * @param key 键
    * @param time 时间(秒)
    * @return
    */
    public boolean expire(String key, long time) {
    try {
    if (time > 0) {
    redisTemplate.expire(key, time, TimeUnit.SECONDS);
    }
    return true;
    } catch (Exception e) {
    e.printStackTrace();
    return false;
    }
    }

    /**
    * 根据key 获取过期时间
    *
    * @param key 键 不能为null
    * @return 时间(秒) 返回0代表为永久有效
    */
    public long getExpire(String key) {
    return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
    * 判断key是否存在
    *
    * @param key 键
    * @return true 存在 false不存在
    */
    public boolean hasKey(String key) {
    try {
    return redisTemplate.hasKey(key);
    } catch (Exception e) {
    e.printStackTrace();
    return false;
    }
    }

    /**
    * 删除缓存
    *
    * @param key 可以传一个值 或多个
    */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
    if (key != null && key.length > 0) {
    if (key.length == 1) {
    redisTemplate.delete(key[0]);
    } else {
    redisTemplate.delete(CollectionUtils.arrayToList(key));
    }
    }
    }
    /****************** common end ****************/


    /****************** String start ****************/

    /**
    * 普通缓存获取
    *
    * @param key 键
    * @return 值
    */
    public Object get(String key) {
    return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
    * 普通缓存放入
    *
    * @param key 键
    * @param value 值
    * @return true成功 false失败
    */
    public boolean set(String key, Object value) {
    try {
    redisTemplate.opsForValue().set(key, value);
    return true;
    } catch (Exception e) {
    e.printStackTrace();
    return false;
    }
    }

    /**
    * 普通缓存放入并设置时间
    *
    * @param key 键
    * @param value 值
    * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
    * @return true成功 false 失败
    */
    public boolean set(String key, Object value, long time) {
    try {
    if (time > 0) {
    redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
    } else {
    set(key, value);
    }
    return true;
    } catch (Exception e) {
    e.printStackTrace();
    return false;
    }
    }

    /**
    * 递增
    *
    * @param key 键
    * @param delta 要增加几(大于0)
    * @return
    */
    public long incr(String key, long delta) {
    if (delta < 0) {
    throw new MyRedidsException("递增因子必须大于0");
    }
    return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
    * 递减
    *
    * @param key 键
    * @param delta 要减少几(小于0)
    * @return
    */
    public long decr(String key, long delta) {
    if (delta < 0) {
    throw new MyRedidsException("递减因子必须大于0");
    }
    return redisTemplate.opsForValue().increment(key, -delta);
    }
    /****************** String end ****************/
    }

    定义Interceptor

    import com.alibaba.fastjson.JSON;
    import com.apply.common.utils.redis.RedisUtils;
    import com.apply.common.validator.annotation.AccessLimit;
    import com.apply.core.http.AjaxResult;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.web.method.HandlerMethod;
    import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

    import javax.servlet.ServletOutputStream;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.Objects;

    /**
    * @author Bummon
    * @description 重复请求拦截
    * @date 2023-08-10 14:14
    */
    @Component
    public class RepeatRequestIntercept extends HandlerInterceptorAdapter {

    @Autowired
    private RedisUtils redisUtils;

    /**
    * 限定时间 单位:秒
    */
    private final int seconds = 1;

    /**
    * 限定请求次数
    */
    private final int max = 1;

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    //判断请求是否为方法的请求
    if (handler instanceof HandlerMethod) {
    String key = request.getRemoteAddr() + "-" + request.getMethod() + "-" + request.getRequestURL();
    Object requestCountObj = redisUtils.get(key);
    if (Objects.isNull(requestCountObj)) {
    //若为空则为第一次请求
    redisUtils.set(key, 1, seconds);
    } else {
    response.setContentType("application/json;charset=utf-8");
    ServletOutputStream os = response.getOutputStream();
    AjaxResult<Void> result = AjaxResult.error(100, "请求已提交,请勿重复请求");
    String jsonString = JSON.toJSONString(result);
    os.write(jsonString.getBytes());
    os.flush();
    os.close();
    return false;
    }
    }
    return true;
    }

    }

    然后我们 将拦截器注册到容器中

    import com.apply.common.validator.intercept.RepeatRequestIntercept;
    import com.apply.core.base.entity.Constants;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.CorsRegistry;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

    /**
    * @author Bummon
    * @description
    * @date 2023-08-10 14:17
    */
    @Configuration
    public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private RepeatRequestIntercept repeatRequestIntercept;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(repeatRequestIntercept);
    }
    }

    我们再来编写一个接口用于测试

    import com.apply.common.validator.annotation.AccessLimit;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    /**
    * @author Bummon
    * @description
    * @date 2023-08-10 14:35
    */
    @RestController
    public class TestController {

    @GetMapping("/test")
    public String test(){
    return "SUCCESS";
    }

    }

    最后我们来看一下结果是否符合我们的预期:


    1秒内的第一次请求:




    1秒内的第二次请求:




    确实已经达到了我们的预期,但是如果我们对特定接口进行拦截,或对不同接口的限定拦截时间和次数不同的话,这种实现方式无法满足我们的需求,所以我们要提出改进。


    改进


    我们可以去写一个自定义的注解,并将 secondsmax 设置为该注解的属性,再在拦截器中判断请求的方法是否包含该注解,如果包含则执行拦截方法,如果不包含则直接返回。


    自定义注解 RequestLimit

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;

    /**
    * @author Bummon
    * @description 幂等性注解
    * @date 2023-08-10 15:10
    */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface RequestLimit {

    /**
    * 限定时间
    */
    int seconds() default 1;

    /**
    * 限定请求次数
    */
    int max() default 1;

    }

    改进 RepeatRequestIntercept

    /**
    * @author Bummon
    * @description 重复请求拦截
    * @date 2023-08-10 15:14
    */
    @Component
    public class RepeatRequestIntercept extends HandlerInterceptorAdapter {

    @Autowired
    private RedisUtils redisUtils;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    //判断请求是否为方法的请求
    if (handler instanceof HandlerMethod) {
    HandlerMethod hm = (HandlerMethod) handler;
    //获取方法中是否有幂等性注解
    RequestLimit anno = hm.getMethodAnnotation(RequestLimit.class);
    //若注解为空则直接返回
    if (Objects.isNull(anno)) {
    return true;
    }
    int seconds = anno.seconds();
    int max = anno.max();
    String key = request.getRemoteAddr() + "-" + request.getMethod() + "-" + request.getRequestURL();
    Object requestCountObj = redisUtils.get(key);
    if (Objects.isNull(requestCountObj)) {
    //若为空则为第一次请求
    redisUtils.set(key, 1, seconds);
    } else {
    //限定时间内的第n次请求
    int requestCount = Integer.parseInt(requestCountObj.toString());
    //判断是否超过最大限定请求次数
    if (requestCount < max) {
    //未超过则请求次数+1
    redisUtils.incr(key, 1);
    } else {
    //否则拒绝请求并返回信息
    refuse(response);
    return false;
    }
    }
    }
    return true;
    }

    /**
    * @param response
    * @date 2023-08-10 15:25
    * @author Bummon
    * @description 拒绝请求并返回结果
    */
    private void refuse(HttpServletResponse response) throws IOException {
    response.setContentType("application/json;charset=utf-8");
    ServletOutputStream os = response.getOutputStream();
    AjaxResult<Void> result = AjaxResult.error(100, "请求已提交,请勿重复请求");
    String jsonString = JSON.toJSONString(result);
    os.write(jsonString.getBytes());
    os.flush();
    os.close();
    }

    }

    这样我们就可以实现我们的需求了。


    作者:Bummon
    链接:https://juejin.cn/post/7265565809823760441
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    你大脑中的画面,现在可以高清还原了

    前言 AI 直接把你脑中的创意画出来的时刻,已经到来了。 本文转载自机器之心 仅用于学术分享,若侵权请联系删除 欢迎关注公众号CV技术指南,专注于计算机视觉的技术总结、最新技术跟踪、经典论文解读、CV招聘信息。 CV各大方向专栏与各个部署框架最全教程整理 【...
    继续阅读 »

    前言 AI 直接把你脑中的创意画出来的时刻,已经到来了。



    本文转载自机器之心


    仅用于学术分享,若侵权请联系删除


    欢迎关注公众号CV技术指南,专注于计算机视觉的技术总结、最新技术跟踪、经典论文解读、CV招聘信息。


    CV各大方向专栏与各个部署框架最全教程整理


    【CV技术指南】CV全栈指导班、基础入门班、论文指导班 全面上线!!


    近几年,图像生成领域取得了巨大的进步,尤其是文本到图像生成方面取得了重大突破:只要我们用文本描述自己的想法,AI 就能生成新奇又逼真的图像。


    但其实我们可以更进一步 —— 将头脑中的想法转化为文本这一步可以省去,直接通过脑活动(如 EEG(脑电图)记录)来控制图像的生成创作。


    这种「思维到图像」的生成方式有着广阔的应用前景。例如,它能极大提高艺术创作的效率,并帮助人们捕捉稍纵即逝的灵感;它也有可能将人们夜晚的梦境进行可视化;它甚至可能用于心理治疗,帮助自闭症儿童和语言障碍患者。


    最近,来自清华大学深圳国际研究生院、腾讯 AI Lab 和鹏城实验室的研究者们联合发表了一篇「思维到图像」的研究论文,利用预训练的文本到图像模型(比如 Stable Diffusion)强大的生成能力,直接从脑电图信号生成了高质量的图像。



    论文地址:arxiv.org/pdf/2306.16…


    项目地址:github.com/bbaaii/Drea…


    方法概述


    近期一些相关研究(例如 MinD-Vis)尝试基于 fMRI(功能性磁共振成像信号)来重建视觉信息。他们已经证明了利用脑活动重建高质量结果的可行性。然而,这些方法与理想中使用脑信号进行快捷、高效的创作还差得太远,这主要有两点原因:


    首先,fMRI 设备不便携,并且需要专业人员操作,因此捕捉 fMRI 信号很困难;


    其次,fMRI 数据采集的成本较高,这在实际的艺术创作中会很大程度地阻碍该方法的使用。


    相比之下,EEG 是一种无创、低成本的脑电活动记录方法,并且现在市面上已经有获得 EEG 信号的便携商用产品。


    但实现「思维到图像」的生成还面临两个主要挑战:


    1)EEG 信号通过非侵入式的方法来捕捉,因此它本质上是有噪声的。此外,EEG 数据有限,个体差异不容忽视。那么,如何从如此多的约束条件下的脑电信号中获得有效且稳健的语义表征呢?


    2)由于使用了 CLIP 并在大量文本 - 图像对上进行训练,Stable Diffusion 中的文本和图像空间对齐良好。然而,EEG 信号具有其自身的特点,其空间与文本和图像大不相同。如何在有限且带有噪声的 EEG - 图像对上对齐 EEG、文本和图像空间?


    为了解决第一个挑战,该研究提出,使用大量的 EEG 数据来训练 EEG 表征,而不是仅用罕见的 EEG 图像对。该研究采用掩码信号建模的方法,根据上下文线索预测缺失的 token。


    不同于将输入视为二维图像并屏蔽空间信息的 MAE 和 MinD-Vis,该研究考虑了 EEG 信号的时间特性,并深入挖掘人类大脑时序变化背后的语义。该研究随机屏蔽了一部分 token,然后在时间域内重建这些被屏蔽的 token。通过这种方式,预训练的编码器能够对不同个体和不同脑活动的 EEG 数据进行深入理解。


    对于第二个挑战,先前的解决方法通常直接对 Stable Diffusion 模型进行微调,使用少量噪声数据对进行训练。然而,仅通过最终的图像重构损失对 SD 进行端到端微调,很难学习到脑信号(例如 EEG 和 fMRI)与文本空间之间的准确对齐。因此,研究团队提出采用额外的 CLIP 监督,帮助实现 EEG、文本和图像空间的对齐。


    具体而言,SD 本身使用 CLIP 的文本编码器来生成文本嵌入,这与之前阶段的掩码预训练 EEG 嵌入非常不同。利用 CLIP 的图像编码器提取丰富的图像嵌入,这些嵌入与 CLIP 的文本嵌入很好地对齐。然后,这些 CLIP 图像嵌入被用于进一步优化 EEG 嵌入表征。因此,经过改进的 EEG 特征嵌入可以与 CLIP 的图像和文本嵌入很好地对齐,并更适合于 SD 图像生成,从而提高生成图像的质量。


    基于以上两个精心设计的方案,该研究提出了新方法 DreamDiffusion。DreamDiffusion 能够从脑电图(EEG)信号中生成高质量且逼真的图像。



    具体来说,DreamDiffusion 主要由三个部分组成:


    1)掩码信号预训练,以实现有效和稳健的 EEG 编码器;


    2)使用预训练的 Stable Diffusion 和有限的 EEG 图像对进行微调;


    3)使用 CLIP 编码器,对齐 EEG、文本和图像空间。


    首先,研究人员利用带有大量噪声的 EEG 数据,采用掩码信号建模,训练 EEG 编码器,提取上下文知识。然后,得到的 EEG 编码器通过交叉注意力机制被用来为 Stable Diffusion 提供条件特征。



    为了增强 EEG 特征与 Stable Diffusion 的兼容性,研究人员进一步通过在微调过程中减少 EEG 嵌入与 CLIP 图像嵌入之间的距离,进一步对齐了 EEG、文本和图像的嵌入空间。


    实验与分析


    与 Brain2Image 对比


    研究人员将本文方法与 Brain2Image 进行比较。Brain2Image 采用传统的生成模型,即变分自编码器(VAE)和生成对抗网络(GAN),用于实现从 EEG 到图像的转换。然而,Brain2Image 仅提供了少数类别的结果,并没有提供参考实现。


    鉴于此,该研究对 Brain2Image 论文中展示的几个类别(即飞机、南瓜灯和熊猫)进行了定性比较。为确保比较公平,研究人员采用了与 Brain2Image 论文中所述相同的评估策略,并在下图 5 中展示了不同方法生成的结果。


    下图第一行展示了 Brain2Image 生成的结果,最后一行是研究人员提出的方法 DreamDiffusion 生成的。可以看到 DreamDiffusion 生成的图像质量明显高于 Brain2Image 生成的图像,这也验证了本文方法的有效性。



    消融实验


    预训练的作用:为了证明大规模 EEG 数据预训练的有效性,该研究使用未经训练的编码器来训练多个模型进行验证。其中一个模型与完整模型相同,而另一个模型只有两层的 EEG 编码层,以避免数据过拟合。在训练过程中,这两个模型分别进行了有 / 无 CLIP 监督的训练,结果如表 1 中 Model 列的 1 到 4 所示。可以看到,没有经过预训练的模型准确性有所降低。



    mask ratio:本文还研究了用 EEG 数据确定 MSM 预训练的最佳掩码比。如表 1 中的 Model 列的 5 到 7 所示,过高或过低的掩码比会对模型性能都会产生不利影响。当掩码比为 0.75 达到最高的整体准确率。这一发现至关重要,因为这表明,与通常使用低掩码比的自然语言处理不同,在对 EEG 进行 MSM 时,高掩码比是一个较好的选择。


    CLIP 对齐:该方法的关键之一是通过 CLIP 编码器将 EEG 表征与图像对齐。该研究进行实验验证了这种方法的有效性,结果如表 1 所示。可以观察到,当没有使用 CLIP 监督时,模型的性能明显下降。实际上,如图 6 右下角所示,即使在没有预训练的情况下,使用 CLIP 对齐 EEG 特征仍然可以得到合理的结果,这凸显了 CLIP 监督在该方法中的重要性。



    欢迎关注公众号CV技术指南,专注于计算机视觉的技术总结、最新技术跟踪、经典论文解读、CV招聘信息。


    计算机视觉入门1v3辅导班


    【技术文档】《从零搭建pytorch模型教程》122页PDF下载


    QQ交流群:470899183。群内有大佬负责解答大家的日常学习、科研、代码问题。


    其它文章


    中科院自动化所发布FastSAM | 精度相当,速度提升50倍!!!


    大核卷积网络是比 Transformer 更好的教师吗?ConvNets 对 ConvNets 蒸馏奇效


    MaskFormer:将语义分割和实例分割作为同一任务进行训练


    CVPR 2023 VAND Workshop Challenge零样本异常检测冠军方案


    视觉魔法师:开启语义分割的奇幻之旅


    沈春华团队最新 | SegViTv2对SegViT进行全面升级,让基于ViT的分割模型更轻更强


    刷新20项代码任务SOTA,Salesforce提出新型基础LLM系列编码器-解码器Code T5+


    可能95%的人还在犯的PyTorch错误


    从DDPM到GLIDE:基于扩散模型的图像生成算法进展


    CVPR最佳论文颁给自动驾驶大模型!中国团队第一单位,近10年三大视觉顶会首例


    最新轻量化Backbone | FalconNet汇聚所有轻量化模块的优点,成就最强最轻Backbone


    ReID专栏(二)多尺度设计与应用


    ReID专栏(一) 任务与数据集概述


    libtorch教程(三)简单模型搭建


    libtorch教程(二)张量的常规操作


    libtorch教程(一)开发环境搭建:VS+libtorch和Qt+libtorch


    NeRF与三维重建专栏(三)nerf_pl源码部分解读与colmap、cuda算子使用


    NeRF与三维重建专栏(二)NeRF原文解读与体渲染物理模型


    NeRF与三维重建专栏(一)领域背景、难点与数据集介绍


    异常检测专栏(三)传统的异常检测算法——上


    异常检测专栏(二):评价指标及常用数据集


    异常检测专栏(一)异常检测概述


    BEV专栏(二)从BEVFormer看BEV流程(下篇)


    BEV专栏(一)从BEVFormer深入探究BEV流程(上篇)


    可见光遥感图像目标检测(三)文字场景检测之Arbitrary


    可见光遥感目标检测(二)主要难点与研究方法概述


    可见光遥感目标检测(一)任务概要介绍


    TensorRT教程(三)TensorRT的安装教程


    TensorRT教程(二)TensorRT进阶介绍


    TensorRT教程(一)初次介绍TensorRT


    AI最全资料汇总 | 基础入门、技术前沿、工业应用、部署框架、实战教程学习


    计算机视觉入门1v3辅导班


    计算机视觉交流群


    聊聊计算机视觉入门


    作者:CV技术指南
    链接:https://juejin.cn/post/7251875626905763901
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    阿里又又发布了一个“AI神器”

    历史回顾 上一次阿里发布通义千问,犹在昨天,结果,阿里又发布了一件AI神器,该神器实用性极强,据说背后依然采用阿里的通义千问大模型。 不了解的可以看下我的历史文章 阿里的通义千问,震惊到我了 最近一直在整理AIGC方面的东西分享给大家,当然编程也不会落下,欢迎...
    继续阅读 »

    历史回顾


    上一次阿里发布通义千问,犹在昨天,结果,阿里又发布了一件AI神器,该神器实用性极强,据说背后依然采用阿里的通义千问大模型。


    不了解的可以看下我的历史文章 阿里的通义千问,震惊到我了


    最近一直在整理AIGC方面的东西分享给大家,当然编程也不会落下,欢迎关注,让我们在AI的道路上越走越远


    他来了,讯飞星火迈着矫健的步伐向我们走来了


    免费搭建个人stable-diffusion绘画(非本地、干货教程)


    阿里给“打工”朋友送上“节日礼物”




    六一儿童节当天,阿里就给所有“打工”的大朋友送上了一份“节日礼物”


    6月1日上午,阿里云发布了面向音视频内容的AI新品“通义听悟”,并正式公测



    【通义听悟】 推荐给你~ tingwu.aliyun.com/u/14xZ00303… 工作学习AI助手,依托大模型,为每一个人提供全新的音视频体验。点击链接立即注册,公测期免费体验。



    通义千问、通义听悟 这哥俩现在所处环境不同,定位不同,功能不同 但依赖大模型是相同的


    这是阿里通义大模型最新的应用进展,也是国内首个开放公测的大模型应用产品。


    根据阿里云智能CTO周靖人介绍,“通义听悟”是依托通义千问大模型和音视频AI模型的AI助手,可进行实时语音识别,实现对话的实时记录、多语言翻译、发言总结、提取PPT、内容规整等。


    对我们打工人有什么用


    会议神器




    当领导在上面夸夸其谈的时候,你的会议纪要可谓是错乱无章,这会儿通义听悟就上线了,你只需要录音




    或者我们本地上传




    支持区分多人对话,然后开始转写


    值得一提的是, “听悟”可以根据AI转写,提取这场说话内容的关键词,给出全文摘要。




    视频总结神器




    不同于传统的实时会议速记转写,如今面向C端提供视频转写服务的应用尚在少数。而如今的通义听悟,则从纯粹的音频转写,延伸到了音视频领域,融合了十多项AI新功能。


    “通义听悟”我个人认为最大的实用功能是:可以设置插件,无论看视频、看直播,还是开会,点开听悟插件,就能实现音视频的实时转录和翻译。




    其实看到这里,可以感受到,这不只是说对打工人的福利,也是对于学生党的福利,比如我们上课,课后复盘总结




    最后再提一点阿里的生态,他们将数据存储和阿里云盘打通 这点是值得表扬的,在阿里云盘中,用户可以一键转写云盘中的文件,在云盘内在线播放视频时,能够实时生成字幕。


    还能帮我们什么


    通义听悟未来还有更多基于大模型的功能上线。比如,对视频中出现的PPT,AI能够基于通义千问大模型做到一键提取,而用户也能向AI助手针对多个音视频内容进行提问、让听悟概括特定段落等等。


    值得注意的是,听悟目前针对一些细分场景中提供了不同的部署形态,如浏览器插件。在Chrome安装听悟插件后,听悟在无字幕视频中就可以实时生成双语悬浮字幕。二转写结果可下载为字幕文件,方便新媒体从业者视频后期制作




    通义千问Chrome插件示意图,近期该功能将上线,可以持续关注 我后续给大家做详细介绍,不过我们可以先感受下




    钉钉的在线会议模块“钉闪记”,同样集成了听悟。在会议结束后,钉闪记所能够输出的也不再是纯粹的速记,而是包含重点摘要的完整文档,可以有效地提升公司内部工作效率。甚至,在开会时,AI可以代为记录会议、整理要点。


    未来一段时间还将在夸克APP、阿里云盘等端口提供服务


    总结


    这一番体验下来总体的效果还是可以的




    从通义听悟中可以看出,国内大模型厂商除了在底层大模型搭建上快马加鞭外,AI应用也已经成为他们必须抓住的机遇——AI音视频转写、内容理解等功能,背后意味着通用能力,厂商们可以覆盖包括开会、上课、培训、面试、直播、看视频、听播客等音视频场景,嵌入到不同的应用软件当中。


    今天的分享就到这里,我们的AI绘画系列正在慢慢搭建,对AI有兴趣的可以关注公众号(微信公众号搜索 1点东西) ,我们会持续输出AIGC类好玩的工具和想法,立志让每个人都能感受AI,利用AI找寻更多可能性


    作者:1点东西
    链接:https://juejin.cn/post/7239962972508209212
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    糟了,生产环境数据竟然不一致,人麻了!

    大家好,我是冰河~~ 今天发现Mysql的主从数据库没有同步 先上Master库: mysql>show processlist; 查看下进程是否Sleep太多。发现很正常。 show master status; 也正常。 mysql> sh...
    继续阅读 »

    大家好,我是冰河~~


    今天发现Mysql的主从数据库没有同步


    先上Master库:


    mysql>show processlist;

    查看下进程是否Sleep太多。发现很正常。


    show master status;

    也正常。


    mysql> show master status;
    +-------------------+----------+--------------+-------------------------------+
    | File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
    +-------------------+----------+--------------+-------------------------------+
    | mysqld-bin.000001 | 3260 | | mysql,test,information_schema |
    +-------------------+----------+--------------+-------------------------------+
    1 row in set (0.00 sec)

    再到Slave上查看


    mysql> show slave status\G                                                

    Slave_IO_Running: Yes
    Slave_SQL_Running: No

    可见是Slave不同步


    解决方案


    下面介绍两种解决方法


    方法一:忽略错误后,继续同步


    该方法适用于主从库数据相差不大,或者要求数据可以不完全统一的情况,数据要求不严格的情况


    解决:


    stop slave;

    #表示跳过一步错误,后面的数字可变
    set global sql_slave_skip_counter =1;
    start slave;

    之后再用mysql> show slave status\G 查看


    mysql> show slave status\G
    Slave_IO_Running: Yes
    Slave_SQL_Running: Yes

    ok,现在主从同步状态正常了。。。


    方式二:重新做主从,完全同步


    该方法适用于主从库数据相差较大,或者要求数据完全统一的情况


    解决步骤如下:


    1.先进入主库,进行锁表,防止数据写入


    使用命令:


    mysql> flush tables with read lock;

    注意:该处是锁定为只读状态,语句不区分大小写


    2.进行数据备份


    #把数据备份到mysql.bak.sql文件


    mysqldump -uroot -p -hlocalhost > mysql.bak.sql

    这里注意一点:数据库备份一定要定期进行,可以用shell脚本或者python脚本,都比较方便,确保数据万无一失。


    3.查看master 状态


    mysql> show master status;
    +-------------------+----------+--------------+-------------------------------+
    | File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
    +-------------------+----------+--------------+-------------------------------+
    | mysqld-bin.000001 | 3260 | | mysql,test,information_schema |
    +-------------------+----------+--------------+-------------------------------+
    1 row in set (0.00 sec)

    4.把mysql备份文件传到从库机器,进行数据恢复


    scp mysql.bak.sql root@192.168.128.101:/tmp/

    5.停止从库的状态


    mysql> stop slave;

    6.然后到从库执行mysql命令,导入数据备份


    mysql> source /tmp/mysql.bak.sql

    7.设置从库同步,注意该处的同步点,就是主库show master status信息里的| File| Position两项


    change master to master_host = '192.168.128.100', master_user = 'rsync',  master_port=3306, master_password='', master_log_file =  'mysqld-bin.000001', master_log_pos=3260;

    8.重新开启从同步


    mysql> start slave;

    9.查看同步状态


    mysql> show slave status\G  

    Slave_IO_Running: Yes
    Slave_SQL_Running: Yes

    10.回到主库并执行如下命令解除表锁定。


    UNLOCK TABLES;

    好了,今天就到这儿吧,小伙伴们点赞、收藏、评论,一键三连走起呀,我是冰河,我们下期见~

    作者:冰_河
    来源:juejin.cn/post/7221858081495203897
    ~

    收起阅读 »

    某用户说他付钱了订单状态未修改

    背景 某项目中有一个收费业务,生成订单,订单状态为待支付。在移动端上打开该功能时可查看到待支付的订单,然后用户可以对待支付的订单进行支付。但偶尔出现客户反馈支付后订单还是待支付的状态,导致用户无法继续使用接下来的功能,导致用户的体验行特别差。重新梳理一遍,排查...
    继续阅读 »



    背景


    某项目中有一个收费业务,生成订单,订单状态为待支付。在移动端上打开该功能时可查看到待支付的订单,然后用户可以对待支付的订单进行支付。但偶尔出现客户反馈支付后订单还是待支付的状态,导致用户无法继续使用接下来的功能,导致用户的体验行特别差。重新梳理一遍,排查该问题。


    涉及服务:



    • server1 主服务业务,提供所有业务的服务;

    • m-server:提供移动端 H5 视图和部分业务功能,部分功能业务都直接请求 server1;


    总体架构


    2022-09-02-10-11-16-image.png


    总体流程:



    • 用户打开移动端应用时,由 m-server 提供页面视图;

    • 移动端相关业务数据由 server1 提供,即用户请求时,会到 m- server 再由其转发到 server1 服务上;

    • 相关支付业务由 m-server 服务与微信支付交互,支付完成后再由 m-server 与 server1 交互,同步订单的状态;


    详细支付时序图:


    支付流程的时序图,可以参考微信的官网:pay.weixin.qq.com/wiki/doc/ap…


    支付流程:



    • 用户点击支付时,向 m-server 发起支付,然后生成订单

    • 向微信支付发起生成预付单

    • 点击微信支付的支付,此时会与微信支付进行验证支付授权权限

    • 微信返回支付授权,然后用户输入密码,确认支付,向微信支付服务提交授权

    • 微信支付返回支付结果给用户,并发微信消息提醒,同时会向 m-server 异步通知支付结果

    • m-server 接收到支付结果将同步给 server ,然后server 变更订单状态结果

    • m-server 显示最后结果给用户,如支付成功的订单详情


    订单状态同步设计:


    2022-09-02-10-34-24-image.png


    订单状态流程:



    • server1 生成订单并记录到 db 中

    • m-server 从 server 中获取到订单的列表

    • m-server 接收到微信支付成功时,就会告知 server 支付成功,然后由 server 将订单状态修改为已支付


    问题分析


    已支付成功了,但订单状态却还是未支付成功?


    首先,订单的状态由待支付到支付成功,必须是由微信支付服务异步通知 m-server 支付成功,然后再由 m-server 通知 server1 去修改订单状态。


    所以,无论 m-server 还是 server1 服务在支付期间发生抖动都可能导致支付成功的信息成功通知给 server1 ,从而导致订单状态修改失败。还有一种可能性,微信支付服务可能没有异步通知。毕竟是第三方发起通知,所以也可能发生未通知情况。


    优化方案


    为了保证订单状态最终结果状态一致性,需要增加服务高可用,且可以支持自动重新发送订单状态变更的请求,及时重发重试。


    详细设计


    2022-09-02-10-45-00-image.png



    • m-server 确认支付时也将订单信息进行存储,状态为待支付

    • m-server 接收到微信支付成功通知后,就转发告知server1 服务

    • server1 修改订单状态进行响应,m-server 接收到响应进行删除或者修改订单状态(可按需进行),m-server 这里订单信息没有用了就也可以删除

    • 同时开启一个异步轮询 m-server 存储的订单信息,对于订单状态是待支付的,进行重发重试。这个过程需要先和微信支付服务确认确实是已支付,然后再将信息重新发送 server1,告知将订单状态调整为已支付


    // 定时轮询订单信息状态
    func notifyAuto() {
    // 异步定时监控订单状态的变化
    var changeOrder = func() {
    result := orderService.getUnPayOrder(0) // 获取当前未支付状态的订单
    if len(result) > 0 {
    for v := range result {
    status := wechat.getOrderStatus(v.orderId)
    if status == 1 { // 订单在微信上时已支付的,需重新调用server 修改订单状态
    orderService.sendOrderFinish(v.orderId)
    }
    }
    }
    }

    go func() {
    time.AfterFunc(time.Minute*10, changeOrder)
    }()
    }
    复制代码
    作者:小雄Ya
    来源:juejin.cn/post/7138609603356901413
    >
    收起阅读 »

    1.0 除 0 没抛出错误,我差点被输送社会

    简言 在项目中,我使用了错误的除法,除零没有抛出错误,程序运行正常,但是数据异常。组长说要把我输送社会,老板说还可以挽留一下,我们来复盘一下这次错误。 先让我们来试一试 public class TestDouble { public static v...
    继续阅读 »

    简言


    在项目中,我使用了错误的除法,除零没有抛出错误,程序运行正常,但是数据异常。组长说要把我输送社会,老板说还可以挽留一下,我们来复盘一下这次错误。


    先让我们来试一试


    public class TestDouble {   
    public static void main(String[] args) {
    System.out.println(1.0 / 0);
    }
    }

    你认为的我认为的它应该会抛出 ArithmeticException 异常


    但是它现在输出了 Infinity



    为什么呢?


    Double 数据类型支持无穷大


    还有其他类型支持吗?


    有,还有 Float


    下面我们来查看 Double 源码,可以看到


    /** 
    * 一个常数,保持类型的正无穷大
    */

    public static final double POSITIVE_INFINITY = 1.0 / 0.0;
    /**
    * 一个常数,保持类型的负无穷大
    */

    public static final double NEGATIVE_INFINITY = -1.0 / 0.0;
    /**
    * 一个常数,非数值类型
    */

    public static final double NaN = 0.0d / 0.0;

    下面来试验下 0.0/0 与 -1.0/0


    Double 正无穷 = 1.0 / 0;
    Double 负无穷 = -1.0 / 0;
    System.out.println("正无穷:" + 正无穷);
    System.out.println("负无穷:" + 负无穷);
    Double 非数值 = 0.0 / 0;
    System.out.println("非数值 0.0/0 ->" + 非数值);

    输出:


    正无穷:Infinity
    负无穷:-Infinity
    非数值 0.0/0 ->NaN

    对无穷大进行运算


    下面来测试对 Float 类型与 Doubloe 类型无穷大进行运算


    public static void testFloatInfinity() {
    Float infFloat = Float.POSITIVE_INFINITY;
    Double infDouble = Double.POSITIVE_INFINITY;
    System.out.println("infFloat + 5 = " + (infFloat + 5));
    System.out.println("infFloat - infDouble = " + (infFloat - infDouble));
    System.out.println("infFloat * -1 = " + (infFloat * -1));
    }

    输出:


    infFloat + 5 = InfinityinfFloat - infDouble = NaNinfFloat * -1 = -Infinity

    可以注意到 1,3 行运算符合我们的预计结果


    ps: Infinity- Infinity 的结果不是数字类型


    对这些值进行判断


    public static void checkFloatInfinity() {   
    Double 正无穷 = 1.0 / 0;
    Double 负无穷 = -1.0 / 0;
    Double 非数值 = 0.0 / 0;
    System.out.println("判断正无穷: " + Double.isInfinite(正无穷));
    System.out.println("判断负无穷: " + (Double.NEGATIVE_INFINITY == 负无穷));
    System.out.println("判断非数值(==): " + (Double.NaN == 非数值));
    System.out.println("判断非数值(isNaN): " + Double.isNaN(非数值));
    }

    输出:


    判断正无穷: true
    判断负无穷: true
    判断非数值(==): false
    判断非数值(isNaN): true


    ps: 判断 NaN 不要使用 ==


    作者:程序员鱼丸
    来源:juejin.cn/post/7135621128818524174

    收起阅读 »

    刚来公司就接了一个不发版直接改代码的需求

    前言 前几天突然接到一个技术需求,想要做一个功能。前端有一个表单,在页面上可以直接写 java 代码,写完后就能保存到数据库,并且这个代码实时生效。这岂非是不用发版就可以随时改代码了吗?而且有bug也不怕,随时改。 适用场景:代码逻辑需要经常变动的业务。 核...
    继续阅读 »

    前言


    前几天突然接到一个技术需求,想要做一个功能。前端有一个表单,在页面上可以直接写 java 代码,写完后就能保存到数据库,并且这个代码实时生效。这岂非是不用发版就可以随时改代码了吗?而且有bug也不怕,随时改。



    适用场景:代码逻辑需要经常变动的业务。


    核心思想



    • 页面改动 java 代码字符串

    • java 代码字符串编译成 class

    • 动态加载到 jvm



    实现重点


    JDK 提供了一个工具包 javax.tools 让使用者可以用简易的 API 进行编译。



    这些工具包的使用步骤:



    1. 获取一个 javax.tools.JavaCompiler 实例。

    2. 基于 Java 文件对象初始化一个编译任务 CompilationTask 实例。

    3. 因为JVM 里面的 Class 是基于 ClassLoader 隔离的,所以编译成功之后可以通过自定义的类加载器加载对应的类实例

    4. 使用反射 API 进行实例化和后续的调用。


    1. 代码编译


    这一步需要将 java 文件编译成 class,其实平常的开发过程中,我们的代码编译都是由 IDEA、Maven 等工具完成。


    内置的 SimpleJavaFileObject 是面向源码文件的,而我们的是源码字符串,所以需要实现 JavaFileObject 接口自定义一个 JavaFileObject。


    public class CharSequenceJavaFileObject extends SimpleJavaFileObject {

    public static final String CLASS_EXTENSION = ".class";

    public static final String JAVA_EXTENSION = ".java";

    private static URI fromClassName(String className) {
    try {
    return new URI(className);
    } catch (URISyntaxException e) {
    throw new IllegalArgumentException(className, e);
    }
    }

    private ByteArrayOutputStream byteCode;
    private final CharSequence sourceCode;

    public CharSequenceJavaFileObject(String className, CharSequence sourceCode) {
    super(fromClassName(className + JAVA_EXTENSION), Kind.SOURCE);
    this.sourceCode = sourceCode;
    }

    public CharSequenceJavaFileObject(String fullClassName, Kind kind) {
    super(fromClassName(fullClassName), kind);
    this.sourceCode = null;
    }

    public CharSequenceJavaFileObject(URI uri, Kind kind) {
    super(uri, kind);
    this.sourceCode = null;
    }

    @Override
    public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
    return sourceCode;
    }

    @Override
    public InputStream openInputStream() {
    return new ByteArrayInputStream(getByteCode());
    }

    // 注意这个方法是编译结果回调的OutputStream,回调成功后就能通过下面的getByteCode()方法获取目标类编译后的字节码字节数组
    @Override
    public OutputStream openOutputStream() {
    return byteCode = new ByteArrayOutputStream();
    }

    public byte[] getByteCode() {
    return byteCode.toByteArray();
    }
    }

    如果编译成功之后,直接通过 CharSequenceJavaFileObject#getByteCode()方法即可获取目标类编译后的字节码对应的字节数组(二进制内容)


    2. 实现 ClassLoader


    因为JVM 里面的 Class 是基于 ClassLoader 隔离的,所以编译成功之后得通过自定义的类加载器加载对应的类实例,否则是加载不了的,因为同一个类只会加载一次。


    主要关注 findClass 方法


    public class JdkDynamicCompileClassLoader extends ClassLoader {

    public static final String CLASS_EXTENSION = ".class";

    private final static Map<String, JavaFileObject> javaFileObjectMap = new ConcurrentHashMap<>();

    public JdkDynamicCompileClassLoader(ClassLoader parentClassLoader) {
    super(parentClassLoader);
    }


    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
    JavaFileObject javaFileObject = javaFileObjectMap.get(name);
    if (null != javaFileObject) {
    CharSequenceJavaFileObject charSequenceJavaFileObject = (CharSequenceJavaFileObject) javaFileObject;
    byte[] byteCode = charSequenceJavaFileObject.getByteCode();
    return defineClass(name, byteCode, 0, byteCode.length);
    }
    return super.findClass(name);
    }

    @Override
    public InputStream getResourceAsStream(String name) {
    if (name.endsWith(CLASS_EXTENSION)) {
    String qualifiedClassName = name.substring(0, name.length() - CLASS_EXTENSION.length()).replace('/', '.');
    CharSequenceJavaFileObject javaFileObject = (CharSequenceJavaFileObject) javaFileObjectMap.get(qualifiedClassName);
    if (null != javaFileObject && null != javaFileObject.getByteCode()) {
    return new ByteArrayInputStream(javaFileObject.getByteCode());
    }
    }
    return super.getResourceAsStream(name);
    }

    /**
    * 暂时存放编译的源文件对象,key为全类名的别名(非URI模式),如club.throwable.compile.HelloService
    */

    void addJavaFileObject(String qualifiedClassName, JavaFileObject javaFileObject) {
    javaFileObjectMap.put(qualifiedClassName, javaFileObject);
    }

    Collection<JavaFileObject> listJavaFileObject() {
    return Collections.unmodifiableCollection(javaFileObjectMap.values());
    }
    }

    3. 封装了上面的 ClassLoader 和 JavaFileObject


    public class JdkDynamicCompileJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {

    private final JdkDynamicCompileClassLoader classLoader;
    private final Map<URI, JavaFileObject> javaFileObjectMap = new ConcurrentHashMap<>();

    public JdkDynamicCompileJavaFileManager(JavaFileManager fileManager, JdkDynamicCompileClassLoader classLoader) {
    super(fileManager);
    this.classLoader = classLoader;
    }

    private static URI fromLocation(Location location, String packageName, String relativeName) {
    try {
    return new URI(location.getName() + '/' + packageName + '/' + relativeName);
    } catch (URISyntaxException e) {
    throw new IllegalArgumentException(e);
    }
    }

    @Override
    public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException {
    JavaFileObject javaFileObject = javaFileObjectMap.get(fromLocation(location, packageName, relativeName));
    if (null != javaFileObject) {
    return javaFileObject;
    }
    return super.getFileForInput(location, packageName, relativeName);
    }

    /**
    * 这里是编译器返回的同(源)Java文件对象,替换为CharSequenceJavaFileObject实现
    */

    @Override
    public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
    JavaFileObject javaFileObject = new CharSequenceJavaFileObject(className, kind);
    classLoader.addJavaFileObject(className, javaFileObject);
    return javaFileObject;
    }

    /**
    * 这里覆盖原来的类加载器
    */

    @Override
    public ClassLoader getClassLoader(Location location) {
    return classLoader;
    }

    @Override
    public String inferBinaryName(Location location, JavaFileObject file) {
    if (file instanceof CharSequenceJavaFileObject) {
    return file.getName();
    }
    return super.inferBinaryName(location, file);
    }

    @Override
    public Iterable<JavaFileObject> list(Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse) throws IOException {
    Iterable<JavaFileObject> superResult = super.list(location, packageName, kinds, recurse);
    List<JavaFileObject> result = new ArrayList<>();
    // 这里要区分编译的Location以及编译的Kind
    if (location == StandardLocation.CLASS_PATH && kinds.contains(JavaFileObject.Kind.CLASS)) {
    // .class文件以及classPath下
    for (JavaFileObject file : javaFileObjectMap.values()) {
    if (file.getKind() == JavaFileObject.Kind.CLASS && file.getName().startsWith(packageName)) {
    result.add(file);
    }
    }
    // 这里需要额外添加类加载器加载的所有Java文件对象
    result.addAll(classLoader.listJavaFileObject());
    } else if (location == StandardLocation.SOURCE_PATH && kinds.contains(JavaFileObject.Kind.SOURCE)) {
    // .java文件以及编译路径下
    for (JavaFileObject file : javaFileObjectMap.values()) {
    if (file.getKind() == JavaFileObject.Kind.SOURCE && file.getName().startsWith(packageName)) {
    result.add(file);
    }
    }
    }
    for (JavaFileObject javaFileObject : superResult) {
    result.add(javaFileObject);
    }
    return result;
    }

    /**
    * 自定义方法,用于添加和缓存待编译的源文件对象
    */

    public void addJavaFileObject(Location location, String packageName, String relativeName, JavaFileObject javaFileObject) {
    javaFileObjectMap.put(fromLocation(location, packageName, relativeName), javaFileObject);
    }
    }

    4. 使用 JavaCompiler 编译并反射生成实例对象


    public final class JdkCompiler {

    static DiagnosticCollector<JavaFileObject> DIAGNOSTIC_COLLECTOR = new DiagnosticCollector<>();

    @SuppressWarnings("unchecked")
    public static <T> T compile(String packageName,
    String className,
    String sourceCode) throws Exception {
    // 获取系统编译器实例
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    // 设置编译参数
    List<String> options = new ArrayList<>();
    options.add("-source");
    options.add("1.8");
    options.add("-target");
    options.add("1.8");
    // 获取标准的Java文件管理器实例
    StandardJavaFileManager manager = compiler.getStandardFileManager(DIAGNOSTIC_COLLECTOR, null, null);
    // 初始化自定义类加载器
    JdkDynamicCompileClassLoader classLoader = new JdkDynamicCompileClassLoader(Thread.currentThread().getContextClassLoader());

    // 初始化自定义Java文件管理器实例
    JdkDynamicCompileJavaFileManager fileManager = new JdkDynamicCompileJavaFileManager(manager, classLoader);
    String qualifiedName = packageName + "." + className;
    // 构建Java源文件实例
    CharSequenceJavaFileObject javaFileObject = new CharSequenceJavaFileObject(className, sourceCode);
    // 添加Java源文件实例到自定义Java文件管理器实例中
    fileManager.addJavaFileObject(
    StandardLocation.SOURCE_PATH,
    packageName,
    className + CharSequenceJavaFileObject.JAVA_EXTENSION,
    javaFileObject
    );
    // 初始化一个编译任务实例
    JavaCompiler.CompilationTask compilationTask = compiler.getTask(
    null,
    fileManager,
    DIAGNOSTIC_COLLECTOR,
    options,
    null,
    Collections.singletonList(javaFileObject)
    );
    Boolean result = compilationTask.call();
    System.out.println(String.format("编译[%s]结果:%s", qualifiedName, result));
    Class<?> klass = classLoader.loadClass(qualifiedName);
    return (T) klass.getDeclaredConstructor().newInstance();
    }
    }

    完成上面工具的搭建之后。我们可以接入数据库的操作了。数据库层面省略,只展示 service 层


    service 层:


    public class JavaService {

    public Object saveAndGetObject(String packageName,String className,String javaContent) throws Exception {
    Object object = JdkCompiler.compile(packageName, className, javaContent);
    return object;
    }

    }

    测试:


    public class TestService {

    public static void main(String[] args) throws Exception {
    test();
    }

    static String content="package cn.mmc;\n" +
    "\n" +
    "public class SayHello {\n" +
    " \n" +
    " public void say(){\n" +
    " System.out.println(\"11111111111\");\n" +
    " }\n" +
    "}";

    static String content2="package cn.mmc;\n" +
    "\n" +
    "public class SayHello {\n" +
    " \n" +
    " public void say(){\n" +
    " System.out.println(\"22222222222222\");\n" +
    " }\n" +
    "}";

    public static void test() throws Exception {
    JavaService javaService = new JavaService();
    Object sayHello = javaService.saveAndGetObject("cn.mmc", "SayHello", content);
    sayHello.getClass().getMethod("say").invoke(sayHello);

    Object sayHello2 = javaService.saveAndGetObject("cn.mmc", "SayHello", content2);
    sayHello2.getClass().getMethod("say").invoke(sayHello2);
    }
    }

    我们在启动应用时,更换了代码文件内存,然后直接反射调用对象的方法。执行结果:


    可以看到,新的代码已经生效!!!



    注意,直接开放修改代码虽然方便,但是一定要做好安全防护


    作者:女友在高考
    来源:juejin.cn/post/7134155429147312141

    收起阅读 »

    项目中前端如何实现无感刷新 token!

    前一阵遇到了一个问题,线上平台有时会出现用户正在使用的时候,突然要用户去进行登录,这样会造成很不好的用户体验,但是当时一直没有好的思路因此搁置了下来;通过零散时间查询资料以及思考,最终解决了这个问题,接下来跟大家分享一下! 环境请求采用的 Axios V1.3...
    继续阅读 »

    前一阵遇到了一个问题,线上平台有时会出现用户正在使用的时候,突然要用户去进行登录,这样会造成很不好的用户体验,但是当时一直没有好的思路因此搁置了下来;通过零散时间查询资料以及思考,最终解决了这个问题,接下来跟大家分享一下!


    环境

    1. 请求采用的 Axios V1.3.2。
    2. 平台的采用的 JWT(JSON Web Tokens) 进行用户登录鉴权。
      (拓展:JWT 是一种认证机制,让后台知道该请求是来自于受信的客户端;更详细的可以自行查询相关资料)

    问题现象


    线上用户在使用的时候,偶尔会出现突然跳转到登录页面,需要重新登录的现象。


    原因

    1. 突然跳转到登录页面,是由于当前的 token 过期,导致请求失败;在 axios 的响应拦截axiosInstance.interceptors.response.use中处理失败请求返回的状态码 401,此时得知token失效,因此跳转到登录页面,让用户重新进行登录。
    2. 平台目前的逻辑是在 token 未过期内,用户登录平台可直接进入首页,无需进行登录操作;因此就存在该现象:用户打开平台,由于此时 token 未过期,用户直接进入到了首页,进行其他操作。但是在用户操作的过程中,token 突然失效了,此时就会出现突然跳转到登录页面,严重影响用户的体验感!
      注:目前线上项目中存在数据大屏,一些实时数据的显示;因此存在用户长时间停留在大屏页面,不进行操作,查看实时数据的情况

    切入点

    1. 怎样及时的、在用户感知不到的情况下更新token
    2. 当 token 失效的情况下,出错的请求可能不仅只有一个;当失效的 token 更新后,怎样将多个失败的请求,重新发送?

    操作流程


    好了!经过了一番分析后,我们找到了问题的所在,并且确定了切入点;那么接下来让我们实操,将问题解决掉。

    前要:

    1、我们仅从前端的角度去处理。

    2、后端提供了两个重要的参数:accessToken(用于请求头中,进行鉴权,存在有效期);refreshToken(刷新令牌,用于更新过期的 accessToken,相对于 accessToken 而言,它的有效期更长)。


    1、处理 axios 响应拦截


    注:在我实际的项目中,accessToken 过期后端返回的 statusCode 值为 401,需要在axiosInstance.interceptors.response.useerror回调中进行逻辑处理

    // 响应拦截
    axiosInstance.interceptors.response.use(
    (response) => {
    return response;
    },
    (error) => {
    let {
    data, config
    } = error.response;
    return new Promise((resolve, reject) => {
    /**
    * 判断当前请求失败
    * 是否由 toekn 失效导致的
    */
    if (data.statusCode === 401) {
    /**
    * refreshToken 为封装的有关更新 token 的相关操作
    */
    refreshToken(() => {
    resolve(axiosInstance(config));
    });
    } else {
    reject(error.response);
    }
    })
    }
    )


    1. 我们通过判断statusCode来确定,是否当前请求失败是由token过期导致的;

    2. 使用 Promise 处理将失败的请求,将由于 token 过期导致的失败请求存储起来(存储的是请求回调函数,resolve 状态)。理由:后续我们更新了 token 后,可以将存储的失败请求重新发起,以此来达到用户无感的体验


    补充:


    现象:在我过了几天登录平台的时候发现,refreshToken过期了,但是没有跳转到登录界面
    原因

    1、当refreshToken过期失效后,后端返回的状态码也是 401

    2、发起的更新token的请求采用的也是处理后的axios,因此响应失败的拦截,对更新请求同样适用

    问题:

    这样会造成,当refreshToken过期后,会出现停留在首页,无法跳转到登录页面。

    解决方法

    针对这种现象,我们需要完善一下axios中响应拦截的逻辑

    axiosInstance.interceptors.response.use(
    (response) => {
    return response;
    },
    (error) => {
    let {
    data, config
    } = error.response;
    return new Promise((resolve, reject) => {
    /**
    * 判断当前请求失败
    * 是否由 toekn 失效导致的
    */
    if (
    data.statusCode === 401 &&
    config.url !== '/api/token/refreshToken'
    ) {
    refreshToken(() => {
    resolve(axiosInstance(config));
    });
    } else if (
    data.statusCode === 401 &&
    config.url === '/api/token/refreshToken'
    ) {
    /**
    * 后端 更新 refreshToken 失效后
    * 返回的状态码, 401
    */
    window.location.href = `${HOME_PAGE}/login`;
    } else {
    reject(error.response);
    }
    })
    }
    )

    2、封装 refreshToken 逻辑


    要点:



    1. 存储由于token过期导致的失败的请求。
    2. 更新本地以及axios中头部的token
    3. 当 refreshToken 刷新令牌也过期后,让用户重新登录
    // 存储由于 token 过期导致 失败的请求
    let expiredRequestArr: any[] = [];

    /**
    * 存储当前因为 token 失效导致发送失败的请求
    */
    const saveErrorRequest = (expiredRequest: () => any) => {
    expiredRequestArr.push(expiredRequest);
    }

    // 避免频繁发送更新
    let firstRequre = true;
    /**
    * 利用 refreshToken 更新当前使用的 token
    */
    const updateTokenByRefreshToken = () => {
    firstRequre = false;
    axiosInstance.post(
    '更新 token 的请求',
    ).then(res => {
    let {
    refreshToken, accessToken
    } = res.data;
    // 更新本地的token
    localStorage.setItem('accessToken', accessToken);
    // 更新请求头中的 token
    setAxiosHeader(accessToken);
    localStorage.setItem('refreshToken', refreshToken);

    /**
    * 当获取了最新的 refreshToken, accessToken 后
    * 重新发起之前失败的请求
    */
    expiredRequestArr.forEach(request => {
    request();
    })
    expiredRequestArr = [];
    }).catch(err => {
    console.log('刷新 token 失败err', err);
    /**
    * 此时 refreshToken 也已经失效了
    * 返回登录页,让用户重新进行登录操作
    */
    window.location.href = `${HOME_PAGE}/login`;
    })
    }

    /**
    * 更新当前已过期的 token
    * @param expiredRequest 回调函数,返回由token过期导致失败的请求
    */
    export const refreshToken = (expiredRequest: () => any) => {
    saveErrorRequest(expiredRequest);
    if (firstRequre) {
    updateTokenByRefreshToken();
    }
    }

    补充:


    问题:

    1、怎么能保证当更新token后,在处理存储的过期请求时,此时没有过期请求还在存呢?;万一此时还在expiredRequestArr推失败的请求呢?

    解决方法
    我们需要调整一下更新 token的逻辑,确保当前由于过期失败的请求都接收到了,再更新token然后重新发起请求。


    最终结果:

    // refreshToken.ts

    /**
    * 功能:
    * 用于实现无感刷新 token
    */
    import { axiosInstance, setAxiosHeader } from "@/axios"
    import { CLIENT_ID, HOME_PAGE } from "@/systemInfo"

    // 存储由于 token 过期导致 失败的请求
    let expiredRequestArr: any[] = [];

    /**
    * 存储当前因为 token 失效导致发送失败的请求
    */
    const saveErrorRequest = (expiredRequest: () => any) => {
    expiredRequestArr.push(expiredRequest);
    }

    /**
    * 执行当前存储的由于过期导致失败的请求
    */
    const againRequest = () => {
    expiredRequestArr.forEach(request => {
    request();
    })
    clearExpiredRequest();
    }

    /**
    * 清空当前存储的过期请求
    */
    export const clearExpiredRequest = () => {
    expiredRequestArr = [];
    }

    /**
    * 利用 refreshToken 更新当前使用的 token
    */
    const updateTokenByRefreshToken = () => {
    axiosInstance.post(
    '更新请求url',
    {
    clientId: CLIENT_ID,
    userName: localStorage.getItem('userName')
    },
    {
    headers: {
    'Content-Type': 'application/json;charset=utf-8',
    'Authorization': 'bearer ' + localStorage.getItem("refreshToken")
    }
    }
    ).then(res => {
    let {
    refreshToken, accessToken
    } = res.data;
    // 更新本地的token
    localStorage.setItem('accessToken', accessToken);
    localStorage.setItem('refreshToken', refreshToken);
    setAxiosHeader(accessToken);
    /**
    * 当获取了最新的 refreshToken, accessToken 后
    * 重新发起之前失败的请求
    */
    againRequest();
    }).catch(err => {
    /**
    * 此时 refreshToken 也已经失效了
    * 返回登录页,让用户重新进行登录操作
    */
    window.location.href = `${HOME_PAGE}/login`;
    })
    }

    let timer: any = null;
    /**
    * 更新当前已过期的 token
    * @param expiredRequest 回调函数,返回过期的请求
    */
    export const refreshToken = (expiredRequest: () => any) => {
    saveErrorRequest(expiredRequest);
    // 保证再发起更新时,已经没有了过期请求要进行存储
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
    updateTokenByRefreshToken();
    }, 500);
    }
    // 响应拦截 区分登录前
    axiosInstance.interceptors.response.use(
    (response) => {
    return response;
    },
    (error) => {
    let {
    data, config
    } = error.response;
    return new Promise((resolve, reject) => {
    /**
    * 判断当前请求失败
    * 是否由 toekn 失效导致的
    */
    if (
    data.statusCode === 401 &&
    config.url !== '/api/token/refreshToken'
    ) {
    refreshToken(() => {
    resolve(axiosInstance(config));
    });
    } else if (
    data.statusCode === 401 &&
    config.url === '/api/token/refreshToken'
    ) {
    /**
    * 后端 更新 refreshToken 失效后
    * 返回的状态码, 401
    */
    clearExpiredRequest();
    window.location.href = `${HOME_PAGE}/login`;
    } else {
    reject(error.response);
    }
    })
    }
    )

    补充


    感谢很多朋友提出了很多更好的方法;我写这篇文章主要是为了分享一下,恰好这种问题推到了我(前端工程师)身上,我是怎样处理的;虽然有可能在一些朋友看来很低级,但它确是我实际工作中碰到的问题,每一个问题的出现解决后都对自身是一种成长,通过分享的方式来巩固自己,也希望能对他人有一些帮助!


    总结


    经过一波分析以及操作,我们最终实现了实际项目中的无感刷新token,最主要的是有效避免了:用户在平台操作过程中突然要退出登录的现象(尤其是当用户进行信息填写,突然要重新登录,之前填写的信息全部作废,是很容易让人发狂的)。

    其实回顾一下,技术上并没有什么难点,只是思路上自己是否能够想通、自洽。人是一棵会思想的芦苇,我们要有自己的思想,面对问题,有自己的思考。

    希望我们能在技术的路上走的越来越远,与君共勉!!!


    作者:fly_dream
    链接:https://juejin.cn/post/7254572706536734781
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    假如面试官让你讲一下新版雪花算法

    这是一篇由 Seata 的官网上一篇叫做“关于新版雪花算法的答疑”的文章引发的思考。 seata.io/zh-cn/blog/… 还是有点意思的,结合自己的理解和代码,加上画几张图来拆解一下 Seata 里面的“改良版雪花算法”。 虽然是在 Seata ...
    继续阅读 »

    这是一篇由 Seata 的官网上一篇叫做“关于新版雪花算法的答疑”的文章引发的思考。



    seata.io/zh-cn/blog/…



    image.png


    还是有点意思的,结合自己的理解和代码,加上画几张图来拆解一下 Seata 里面的“改良版雪花算法”。


    虽然是在 Seata 里面看到的,但是本篇文章的内容和 Seata 框架没有太多关系,反而和数据库的基础知识有关。


    所以,即使你不了解 Seata 框架,也不影响你阅读。


    当你理解了这个类的工作原理之后,你完全可以把这个只有 100 多行的类搬运到你的项目里面,然后就变成你的了。


    dddd(懂的都懂)!



    先说问题



    雪花算法的使用场景:


    如果你的项目中涉及到需要一个全局唯一的流水号,比如订单号、流水号之类的,又或者在分库分表的情况下,需要一个全局唯一的主键 ID 的时候,就需要一个算法能生成出这样“全局唯一”的数据。


    一般来说,我们除了“全局唯一”这个基本属性之外,还会要求生成出来的 ID 具有“递增趋势”,这样的好处是能减少 MySQL 数据页分裂的情况,从而减少数据库的 IO 压力,提升服务的性能。


    此外,在当前的市场环境下,不管你是啥服务,张口就是高并发,我们也会要求这个算法必须得是“高性能”的。


    雪花算法,就是一个能生产全局唯一的、递增趋势的、高性能的分布式 ID 生成算法。


    关于雪花算法的解析,网上相关的文章比雪花还多,这个玩意,应该是“面试八股文”中重点考察模块,分布式领域中的高频考题之一,如果是你的盲区的话,可自行去了解一下。


    比如一个经典的面试题就是:


    雪花算法最大的缺点是什么?


    背过题的小伙伴应该能立马答出来:时钟敏感


    因为在雪花算法中,由于要生成单调递增的 ID,因此它利用了时间的单调递增性,所以是强依赖于系统时间的。


    如果系统时间出现了回拨,那么生成的 ID 就可能会重复。


    而“时间回拨”这个现象,是有可能出现的,不管是人为的还是非人为的。


    当你回答出这个问题之后,面试官一般会问一句:


    那如果真的出现了这种情况,应该怎么办呢?


    很简单,正常来说只要不是不是有人手贱或者出于泄愤的目的进行干扰,系统的时间漂移是一个在毫秒级别的极短的时间。


    所以可以在获取 ID 的时候,记录一下当前的时间戳。然后在下一次过来获取的时候,对比一下当前时间戳和上次记录的时间戳,如果发现当前时间戳小于上次记录的时间戳,所以出现了时钟回拨现象,对外抛出异常,本次 ID 获取失败。


    理论上当前时间戳会很快的追赶上上次记录的时间戳。


    但是,你可能也注意到了,“对外抛出异常,本次 ID 获取失败”,意味着这段时间内你的服务对外是不可使用的。


    比如,你的订单号中的某个部分是由这个 ID 组成的,此时由于 ID 生成不了,你的订单号就生成不了,从而导致下单失败。


    再比如,在 Seata 里面,如果是使用数据库作为 TC 集群的存储工具,那么这段时间内该 TC 就是处于不可用状态。


    你可以简单的理解为:基础组件的错误导致服务不可用。



    再看代码



    基于前面说的问题,Seata 才提出了“改良版雪花算法”。



    seata.io/zh-cn/blog/…



    图片


    在介绍改良版之前,我们先把 Seata 的源码拉下来,瞅一眼。


    在源码中,有一个叫做 IdWorker 的类:



    io.seata.common.util.IdWorker



    再来看一下它的提交记录:


    图片


    2020 年 5 月 4 日第一次提交,从提交时的信息可以看出来,这是把分布式 ID 的生成策略修改为 snowflake,即雪花算法。


    同时我们也能在代码中找到前面提到的“对外抛出异常,本次 ID 获取失败”相关代码,即 nextId  方法,它的比较方式就是用当前时间戳和上次获取到的时间戳做对比:



    io.seata.common.util.IdWorker#nextId



    图片


    这个类的最后一次提交是 2020 年 12 月 15 日:


    图片


    这一次提交对于 IdWorker 这个类进行了大刀阔斧的改进,可以看到变化的部分非常的多:


    图片


    我们重点关注刚刚提到的 nextId 方法:


    图片


    整个方法从代码行数上来看都可以直观的看到变化,至少没有看到抛出异常了。


    这段代码到底是怎么起作用的呢?


    首先,我们得理解 Seata 的改良思路,搞明白思路了,再说代码就好理解一点。


    在前面提到的文章中 Seata 也说明了它的核心思路:


    图片


    原版的雪花算法 64 位 ID 是分配这样的:


    图片


    可以看到,时间戳是在最前面的,因为雪花算法利用了时间的单调递增的特性。


    所以,如果前面的时间戳一旦出现“回退”的情况,即打破了“时间的单调递增”这个前提条件,也就打破了它的底层设计。


    它能怎么办?


    它只能给你抛出异常,开始摆烂了。


    可以看到节点 ID 长度为二进制的 10 位,也就是说最多可以服务于 1024 台机器,所以你看 Seata 最开始提交的版本里面,有一个在 1024 里面随机的动作。


    因为算法规定了,节点 ID 最多就是 2 的 10 次方,所以这里的 1024 这个值就是这样来的:


    图片


    包括后面有大佬觉得用这个随机算法一点都不优雅,就把这部分改成了基于 IP 去获取:


    图片


    看起来有点复杂,但是我们仔细去分析最后一行:



    return ((ipAddressByteArray[ipAddressByteArray.length - 2] & 0B11) << Byte.SIZE) + (ipAddressByteArray[ipAddressByteArray.length - 1] & 0xFF);



    变量 & 0B11 运算之后的最大值就是 0B11 即 3。


    Byte.SIZE = 8。


    所以,3 << 8,对应二进制 1100000000,对应十进制 768。


    变量 & 0xFF 运算之后的最大值就是 0xFF 即 255。


    768+255=1023,取值范围都还是在 [0,1023] 之间。


    然后你再看现在最新的算法里面,官方的老哥们认为获取 IP 的方式不够好:


    图片


    所以又把这个地方从使用 IP 地址改成了获取 Mac 地址。


    图片


    最后一行是这样的:



    return ((mac[4] & 0B11) << 8) | (mac[5] & 0xFF);



    那么理论上的最大值就是 768 | 255 ,算出来还是 1023。


    所以不管你怎么玩出花儿来,这个地方搞出来的数的取值范围就只能是 [0,1023] 之间。


    别问,问就是规定里面说了,算法里面只给节点 ID 留了 10 位长度。


    最后,就是这个 12 位长度的序列号了:


    图片


    这个玩意没啥说的,就是一个单纯的、递增的序列号而已。


    既然 Seata 号称是改良版,那么具体体现在什么地方呢?


    简单到你无法想象:


    图片


    是的,仅仅是把时间戳和节点 ID 换个位置就搞定了。


    然后每个节点的时间戳是在 IdWorker 初始化的时候就设置完成了,具体体现到代码上是这样的:



    io.seata.common.util.IdWorker#initTimestampAndSequence



    图片


    在获取 ID 的过程中,只有在初始化的时候获取过一次系统时间,之后和它就再也没有关系了。


    所以,Seata 的分布式 ID 生成器,不再依赖于时间。


    然后,你再想想另外一个问题:


    由于序列号只有 12 位,它的取值范围就是 [0,4095]。如果我们序列号就是生成到了 4096 导致溢出了,怎么办呢?



    很简单,序列号重新归 0,溢出的这一位加到时间戳上,让时间戳 +1。


    那你再进一步想想,如果让时间戳 +1 了,那么岂不是会导致一种“超前消费”的情况出现,导致时间戳和系统时间不一致了?


    朋友,慌啥啊,不一致就不一致呗,反正我们现在也不依赖于系统时间了。


    然后,你想想,如果出现“超前消费”,意味着什么?


    意味着在当前这个毫秒下,4096 个序列号不够用了。


    4096/ms,约 400w/s。


    你啥场景啊,怎么牛逼?


    (哦,原来是面试场景啊,那懂了~)


    另外,官网还抛出了另外一个问题:这样持续不断的"超前消费"会不会使得生成器内的时间戳大大超前于系统的时间戳,从而在重启时造成 ID 重复?


    你想想,理论上确实是有可能的。假设我时间戳都“超前消费”到一个月以后了。


    那么在这期间,你服务发生重启时我会重新获取一次系统时间戳,导致出现“时间回溯”的情况。


    理论上确实有可能。



    但是实际上...


    看看官方的回复:


    图片



    别问,问就是不可能,就算出现了,最先崩的也不是我这个地方。



    说了这么多其实就记住住这个图,就完事了:


    图片


    那么问题又来了:


    改良版的算法是单调递增的吗?



    在单节点里面,它肯定是单调递增的,但是如果是多个节点呢?


    在多个节点的情况下,单独看某个节点的 ID 是单调递增的,但是多个节点下并不是全局单调递增。


    因为节点 ID 在时间戳之前,所以节点 ID 大的,生成的 ID 一定大于节点 ID 小的,不管时间上谁先谁后。



    这一点我们也可以通过代码验证一下,代码的意思是三个节点,每个节点各自生成 5 个 ID:


    图片


    从输出来看,一眼望去,生成的 ID 似乎是乱序的,至少在全局的角度下,肯定不是单调递增的:


    但是我们把输出按照节点 ID 进行排序,就变成了这样,单节点内严格按照单调递增,没毛病:


    图片


    而在原版的雪花算法中,时间戳在高位,并且始终以系统时钟为准,每次生成的时候都会严格和系统时间进行对比,确保没有发生时间回溯,这样可以保证早生成的 ID 一定小于晚生成的 ID ,只有当 2 个节点恰好在同一时间戳生成 ID 时,2 个 ID 的大小才由节点 ID 决定。


    这样看来,Seata 的改进算法是不是错的?


    先别急,继续往下看。



    分析一波



    分析之前,先抛出官方的回答:


    图片


    先来一个八股文热身:


    请问为什么不建议使用 UUID 作为数据库的主键 ID ?


    就是为了避免触发 MySQL 的页分裂从而影响服务性能。


    比如当前主键索引的情况是这样的:


    图片


    如果来了一个  433,那么直接追加在当前最后一个记录 432 之后即可。


    图片


    但是如果我们要插入一个 20 怎么办呢?


    那么数据页 10 里面已经放满了数据,所以会触发页分裂,变成这样:


    图片


    进而导致上层数据页的分裂,最终变成这样的一个东西:


    图片


    上面的我们可以看出页分裂伴随着数据移动,所以我们应该尽量避免。


    理想的情况下,应该是把一页数据塞满之后,再新建另外一个数据页,这样 B+ tree 的最底层的双向链表永远是尾部增长,不会出现上面画图的那种情况:在中间的某个节点发生分裂。


    那么 Seata 的改良版的雪花算法在不具备“全局的单调递增性”的情况下,是怎么达到减少数据库的页分裂的目的的呢?


    我们还是假设有三个节点,我用 A,B,C 代替,在数值上 A < B < C,采用的是改良版的雪花算法,在初始化的情况下是这样的。


    图片


    假设此时,A 节点申请了一个流水号,那么基于前面的分析,它一定是排在 A-seq1 之后,B-seq1 之前的。


    但是这个时候数据页里面的数据满了,怎么办?


    分裂呗:


    图片


    又来了 A-seq3 怎么办?


    问题不大,还放的下:


    图片


    好,这个时候数据页 7 满了,但是又来了 A-seq4,怎么办?


    只有继续分裂了:


    图片


    看到这个分裂的时候,你有没有嗦出一丝味道,是不是有点意思了?


    因为在节点 A 上生成的任何 ID 都一定小于在节点 B 上生成的任何 ID,节点 B 和节点 C 同理。


    在这个范围内,所有的 ID 都是单调递增的:


    图片


    而这样的范围最多有多少个?


    是不是有多少个节点,就有多少个?


    那么最多有多少个节点?


    图片


    2 的 10 次方,1024 个节点。


    所以官方的文章中有这样的一句话:



    新版算法从全局角度来看,ID 是无序的,但对于每一个 workerId,它生成的 ID 都是严格单调递增的,又因为 workerId 是有限的,所以最多可划分出 1024 个子序列,每个子序列都是单调递增的。



    经过前面的分析,每个子序列总是单调递增的,所以每个子序列在有限次的分裂之后,最终都会达到稳态。


    或者用一个数学上的说法:该算法是收敛的。


    再或者,放个图看看:


    图片


    我想说作者画的时候尽力了,至于你看懂看不懂的,就看天意了。


    页分裂


    前面写的所有内容,你都能在官网上前面提到的两个文章中找到对应的部分。


    但是关于页分裂部分,官方并没有进行详细说明。本来也是这样的,人家只是给你说自己的算法,没有必要延伸的太远。


    “刚才说到了页分裂”,以面试官的嘴脸怎么可能放过你,“展开讲讲?”


    链接已放,自行展开:



    mysql.taobao.org/monthly/202…



    还是先搞个图:


    图片


    问,在上面的这个 B+ tree 中,如果我要插入 9,应该怎么办?


    因为数据页中已经没有位置了,所以肯定要触发页分裂。


    会变成这样:


    图片


    这种页分裂方式叫做插入点(insert point)分裂。


    其实在 InnoDB 中最常用的是另外一种分裂方式,中间点(mid point)分裂。


    如果采用中间点(mid point)分裂,上面的图就会变成这样:


    图片


    即把将原数据页面中的 50% 数据移动到新页面,这种才是普遍的分裂方法。


    这种分裂方法使两个数据页的空闲率都是 50%,如果之后的数据在这两个数据页上的插入是随机的话,那么就可以很好地利用空闲空间。


    但是,如果后续数据插入不是随机,而是递增的呢?


    比如我插入 10 和 11。


    插入 10 之后是这样的:


    图片


    插入 11 的时候又要分页了,采用中间点(mid point)分裂就变成了这样:


    图片


    你看,如果是在递增的情况下,采用中间点(mid point)分裂,数据页 8 和 20 的空间利用率只有 50%。


    因为数据的填充和分裂的永远是右侧页面,左侧页面的利用率只有 50%。


    所以,插入点(insert point)分裂是为了优化中间点(mid point)分裂的问题而产生的。


    InnoDB 在每个数据页上专门有一个叫做 PAGE_LAST_INSERT 的字段,记录了上次插入位置,用来判断当前插入是是否是递增或者是递减的。


    如果是递减的,数据页则会向左分裂,然后移动上一页的部分数据过去。


    如果判定为递增插入,就在当前点进行插入点分裂。


    比如还是这个图:


    图片


    上次插入的是记录 8,本次插入 9,判断为递增插入,所以采用插入点分裂,所以才有了上面这个图片。


    好,那么问题就来了,请听题:


    假设出现了这种情况,阁下又该如何应对?


    图片


    在上面这个图的情况下,我要插入 10 和 9:


    当插入 10 的时候,按 InnoDB 遍历 B+ tree 的方法会定位到记录 8,此时这个页面的 PAGE_LAST_INSERT 还是 8。所以会被判断为递增插入,在插入点分裂:


    图片


    同理插入 9 也是这样的:


    图片


    最终导致 9、10、11 都独自占据一个 page,空间利用率极低。


    问题的根本原因在于每次都定位到记录 8(end of page),并且都判定为递增模式。


    哦豁,你说这怎么办?


    答案就藏在这一节开始的时候放的链接中:


    图片


    前面所画的图都是在没有并发的情况下展开的。


    但是在这个部分里面,牵扯到了更为复杂的并发操作,同时也侧面解释了为什么 InnoDB 在同一时刻只能有有一个结构调整(SMO)进行。


    这里面学问就大了去了,有兴趣的可以去了解一下 InnoDB 在 B+ tree 并发控制上的限制,然后再看看 Polar index 的破局之道。


    反正我是学不动了。


    哦,对了。前面说了这么多,还只是聊了页分裂的情况。


    有分裂,就肯定有合并。


    那么什么时候会触发页合并呢?


    页合并会对我们前面探讨的 Seata 的改良版雪花算法带来什么影响呢?


    别问了,别问了,学不动了,学不动了。


    作者:essenceNow
    来源:juejin.cn/post/7265641370495451148
    收起阅读 »

    代码中被植入了恶意删除操作,太狠了!

    背景 在交接的代码中做手脚进行删库等操作,之前只是网上听说的段子,没想到上周还真遇到了,并且亲自参与帮忙解决。 事情是这样的,一老板接手了一套系统,可能因为双方在交接时出现了什么不愉快的事情,对方不提供源代码,只是把生产环境的服务器打了一个镜像给到对方。 对方...
    继续阅读 »

    背景


    在交接的代码中做手脚进行删库等操作,之前只是网上听说的段子,没想到上周还真遇到了,并且亲自参与帮忙解决。


    事情是这样的,一老板接手了一套系统,可能因为双方在交接时出现了什么不愉快的事情,对方不提供源代码,只是把生产环境的服务器打了一个镜像给到对方。


    对方拿到镜像恢复之后,系统起来怎么也无法正常处理业务,于是就找到我帮忙看是什么原因。经过排查,原来交接的人在镜像中做了多处手脚,多处删除核心数据及jar包操作。下面来给大家细细分析排查过程。


    排查过程


    由于只提供了镜像文件,导致到底启动哪些服务都是问题。好在是Linux操作系统,镜像恢复之后,通过history命令可以查看曾经执行了哪些命令,能够找到都需要启动哪些服务。但服务启动之后,业务无法正常处理,很多业务都处于中间态。


    原本系统是可以正常跑业务的,打个镜像之后再恢复就不可以了?这就奇怪了。于是对项目(jar包或war)文件进行排查,查看它们的修改时间。


    在文件的修改时间上还真找到了一些问题,发现在打镜像的两个小时前,项目中一个多个项目底层依赖的jar包被修改过,另外还有两个class文件被修改过。


    于是,就对它们进行了重点排查。首先反编译了那两个被修改过的class文件,在代码中找到了可疑的地方。


    可疑代码


    在两个被修改的类中都有上述代码。最开始没太留意这段代码,但直觉告诉我不太对,一个查询业务里面怎么可能出现删除操作呢?这太不符合常理了。


    于是仔细阅读上述代码,发现上述红框中的代码无论何时执行最终的结果都是id=1。你是否看出来了?问题就出在三目表达式上,无论id是否为null,id被赋的值都是1。看到这里,也感慨对方是用心了。为了隐藏这个目的,前面写了那么多无用的代码。


    但只有这个还不是什么问题,毕竟如果只是删除id为1的值,也只是删除了一条记录,影响范围应该有限。


    紧接着反编译了被修改的jar包,依次去找上述删除方法的底层实现,看到如下代码:


    删除操作


    原来前面传递的id=1是为了配合where条件语句啊,当id=1被传递进来之后,就形成了where 1=1的条件语句。这个大家在mybatis中拼接多条件语句时经常用到。结果就是一旦执行了上述业务逻辑,就会触发删除T_QUART_DATA全表数据的操作。


    T_QUART_DATA表中是用于存储触发定时任务的表达式,到这里也就明白了,为啥前面的业务跑不起来,全部是中间态了。因为一旦在业务逻辑中触发开关,把定时任务的cron表达式全部删除,十多个定时任务全部歇菜,业务也就跑步起来了。


    找到了问题的根源,解决起来就不是啥事了,由于没有源代码,稍微费劲的是只能把原项目整个反编译出来,然后将改修改地方进行了修改。


    又起波折


    本以为到此问题已经解决完毕了,没想到第二天又出现问题了,项目又跑不起来了。经过多方排查和定位,感觉还有定时任务再进行暗箱操作。


    于是通过Linux的crontab命令查看是否有定时任务在执行,执行crontab -ecrontab -l,还真看到有三个定时任务在执行。跟踪到定时任务执行的脚本中,而且明目张胆的起名deleteXXX:


    删除脚本


    而在具体的脚本中,有如下执行操作:


    删除核心依赖包


    这下找到为什么项目中第二天为啥跑不起来了,原来Linux的定时任务将核心依赖包删除了,并且还会去重启服务。


    为了搞破坏,真是煞费苦心啊。还好的是这个jar包在前一天已经反编译出来了,也算有了备份。


    小结


    原本以为程序员在代码中进行删库操作或做一些其他小手脚只是网络上的段子,大多数人出于职业操守或个人品质是不会做的。没想到这还真遇到了,而且对方为了隐藏删除操作,还做了一些小伪装,真的是煞费苦心啊。如果有这样的能力和心思,用在写出更优秀的代码或系统上或许更好。


    当然,不知道他们在交接的过程中到底发生了什么,竟然用这样的方式对待昔日合作的伙伴。之所以写这篇文章,是想让大家学习如何排查代码问题的过程,毕竟用到了不少知识点和技能,但这并不是教大家如何去做手脚。无论怎样,最起码的职业操守还是要有

    作者:程序新视界
    来源:juejin.cn/post/7140066341469290532
    的,这点不接受反驳。

    收起阅读 »

    万能的异步处理方案

    异步处理通用方案 前言 良好的系统设计必须要做到开闭原则,随着业务的不断迭代更新,核心代码也会被不断改动,出错的概率也会大大增加。但是大部分增加的功能都是在扩展原有的功能,既要保证性能又要保证质量,我们往往都会使用异步线程池来处理,然而却增加了很多不确定性因素...
    继续阅读 »

    异步处理通用方案


    前言


    良好的系统设计必须要做到开闭原则,随着业务的不断迭代更新,核心代码也会被不断改动,出错的概率也会大大增加。但是大部分增加的功能都是在扩展原有的功能,既要保证性能又要保证质量,我们往往都会使用异步线程池来处理,然而却增加了很多不确定性因素。 由此我设计了一套通用的异步处理SDK,可以很轻松的实现各种异步处理


    目的


    通过异步处理不仅能够保证方法能够得到有效的执行而且不影响主流程


    更重要的是各种兜底方法保证数据不丢失,从而达到最终一致性\color{red}最终一致性


    优点


    无侵入设计,独立数据库,独立定时任务,独立消息队列,独立人工执行界面(统一登录认证)


    使用spring事务事件机制,即使异步策略解析失败也不会影响业务


    如果你的方法正在运行事务,会等事务提交后再处理事件


    就算事务提交了,异步策略解析失败了,我们还有兜底方案执行(除非数据库有问题,消息队列有问题,方法有bug)


    组件


    kafka 消息队列


    xxl job 定时任务


    mysql 数据库


    spring 切面


    vue 界面


    设计模式


    策略


    模板方法


    动态代理


    流程图


    image.png


    数据库脚本


    CREATE TABLE `async_req` (
    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    `application_name` varchar(100) NOT NULL DEFAULT '' COMMENT '应用名称',
    `sign` varchar(50) NOT NULL DEFAULT '' COMMENT '方法签名',
    `class_name` varchar(200) NOT NULL DEFAULT '' COMMENT '全路径类名称',
    `method_name` varchar(100) NOT NULL DEFAULT '' COMMENT '方法名称',
    `async_type` varchar(50) NOT NULL DEFAULT '' COMMENT '异步策略类型',
    `exec_status` tinyint NOT NULL DEFAULT '0' COMMENT '执行状态 0:初始化 1:执行失败 2:执行成功',
    `exec_count` int NOT NULL DEFAULT '0' COMMENT '执行次数',
    `param_json` longtext COMMENT '请求参数',
    `remark` varchar(200) NOT NULL DEFAULT '' COMMENT '业务描述',
    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (`id`) USING BTREE,
    KEY `idx_applocation_name` (`application_name`) USING BTREE,
    KEY `idx_exec_status` (`exec_status`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='异步处理请求';

    CREATE TABLE `async_log` (
    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    `async_id` bigint NOT NULL DEFAULT '0' COMMENT '异步请求ID',
    `error_data` longtext COMMENT '执行错误信息',
    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    PRIMARY KEY (`id`) USING BTREE,
    KEY `idx_async_id` (`async_id`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='异步处理日志';

    异步策略


    image.png


    安全级别


    image.png


    执行状态


    image.png


    流程图


    image.png


    image.png
    image.png

    apollo 配置


    # 开关:默认关闭
    scm.async.enabled=true

    # 数据源 druid
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://127.0.0.1:3306/fc_async?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true
    spring.datasource.username=user
    spring.datasource.password=xxxx
    spring.datasource.filters=config
    spring.datasource.connectionProperties=config.decrypt=true;config.decrypt.key=yyy
    #静态地址
    spring.resources.add-mappings=true
    spring.resources.static-locations=classpath:/static/


    # 以下配置都有默认值
    # 核心线程数
    async.executor.thread.corePoolSize=10
    # 最大线程数
    async.executor.thread.maxPoolSize=50
    # 队列容量
    async.executor.thread.queueCapacity=10000
    # 活跃时间
    async.executor.thread.keepAliveSeconds=600

    # 执行成功是否删除记录:默认删除
    scm.async.exec.deleted=true

    # 自定义队列名称前缀:默认应用名称
    scm.async.topic=应用名称

    # 重试执行次数:默认5次
    scm.async.exec.count=5

    # 重试最大查询数量
    scm.async.retry.limit=100

    # 补偿最大查询数量
    scm.async.comp.limit=100

    用法


    1,异步开关
    scm.async.enabled=true

    2,在需要异步执行的方法加注解 (必须是spring代理方法)
    @AsyncExec(type = AsyncExecEnum.SAVE_ASYNC, remark = "数据字典")

    3,人工处理地址
    http://localhost:8004/async/index.html

    注意


    1,应用名称
    spring.application.name

    2,队列名称
    ${scm.async.topic:${spring.application.name}}_async_queue
    自定义topic:scm.async.topic=xxx

    3,自己业务要做幂等

    4,一个应用公用一个队列
    自产自消

    5,定时任务
    异步重试定时任务(2分钟重试一次,可配置重试次数)
    异步补偿定时任务(一小时补偿一次,创建时间在一小时之前的)

    效果展示


    image.png


    image.png


    作者:三火哥
    来源:juejin.cn/post/7266087843239084090
    收起阅读 »

    AI欣赏-街头少女🔥🔥🔥

    描述 💯💯💯 你更喜欢哪一位选手呢? 自己制作的一些AI绘画关键词,一是为了保存下来,二是分享给大家看看,可以收集一些意见和建议、想法等等。所以欢迎大家踊跃发言! 本次文章的描述主题如下:一个女孩在街头大笑,湿透的,辫子,写实风格的,电影级别的,HDR的。Lo...
    继续阅读 »

    描述


    💯💯💯 你更喜欢哪一位选手呢?


    自己制作的一些AI绘画关键词,一是为了保存下来,二是分享给大家看看,可以收集一些意见和建议、想法等等。所以欢迎大家踊跃发言!


    本次文章的描述主题如下:一个女孩在街头大笑,湿透的,辫子,写实风格的,电影级别的,HDR的。

    • Lora:无

    • Embeddings:ng_deepnegative_v1_75t [1a3e]


    Prompt



    a young woman, street, laughing, ponytails, (hdr:1.3), (muted colors:1.2), dramatic, complex background, cinematic, filmic, (rutkowski, artstation:0.8), soaking wet,



    Negative Prompt



    (nsfw:2),Multiple people,easynegative,(worst quality:2),(low quality:2),lowres,(monochrome:1.4),(grayscale:1.4),big head,severed legs,short legs,missing legs,acnes,skin blemishes,age spot,backlight,(ugly:1.4),(duplicate:1.4),(morbid:1.2),(mutilated:1.2),mutated hands,(poorly drawn hands:1.4),blurry, (bad anatomy:1.4),(bad proportions:1.4),(disfigured:1.4),(unclear eyes:1.4),bad hands, bad tooth,missing fingers,extra digit,bad body,NG_DeepNegative_V1_75T,glans,EasyNegative:0.5,gross proportions.short arm,(missing arms:1.4),missing thighs,missing calf,mutation,duplicate,morbid,mutilated,poorly drawn cloth,strange finger,bad finger,(mutated hands and fingers:1.4),(text:1.4), bad-artist, bad_prompt_version2, bad-hands-5, bad-image-v2-39000,



    基础配置




    生成图的效果展示


    选手1




    选手2




    选手3




    选手4




    选手5




    选手6




    选手7




    选手8




    选手9




    投票


    🌺 请开始诸位的投票吧!评论区见!!


    作者:襄垣
    链接:https://juejin.cn/post/7223267912727298103
    来源:稀土掘金
    收起阅读 »

    iPhone两秒出图,目前已知的最快移动端Stable Diffusion模型来了

    Stable Diffusion (SD)是当前最热门的文本到图像(text to image)生成扩散模型。尽管其强大的图像生成能力令人震撼,一个明显的不足是需要的计算资源巨大,推理速度很慢:以 SD-v1.5 为例,即使用半精度存储,其模型大小也有 1.7...
    继续阅读 »

    Stable Diffusion (SD)是当前最热门的文本到图像(text to image)生成扩散模型。尽管其强大的图像生成能力令人震撼,一个明显的不足是需要的计算资源巨大,推理速度很慢:以 SD-v1.5 为例,即使用半精度存储,其模型大小也有 1.7GB,近 10 亿参数,端上推理时间往往要接近 2min。


    为了解决推理速度问题,学术界与业界已经开始对 SD 加速的研究,主要集中于两条路线:(1)减少推理步数,这条路线又可以分为两条子路线,一是通过提出更好的 noise scheduler 来减少步数,代表作是 DDIM [1],PNDM [2],DPM [3] 等;二是通过渐进式蒸馏(Progressive Distillation)来减少步数,代表作是 Progressive Distillation [4] 和 w-conditioning [5] 等。(2)工程技巧优化,代表作是 Qualcomm 通过 int8 量化 + 全栈式优化实现 SD-v1.5 在安卓手机上 15s 出图 [6],Google 通过端上 GPU 优化将 SD-v1.4 在三星手机上加速到 12s [7]。


    尽管这些工作取得了长足的进步,但仍然不够快。


    近日,Snap 研究院推出最新高性能 Stable Diffusion 模型,通过对网络结构、训练流程、损失函数全方位进行优化,在 iPhone 14 Pro 上实现 2 秒出图(512x512),且比 SD-v1.5 取得更好的 CLIP score。这是目前已知最快的端上 Stable Diffusion 模型!



    论文地址:arxiv.org/pdf/2306.00…
    Webpage: snap-research.github.io/SnapFusion


    核心方法


    Stable Diffusion 模型分为三部分:VAE encoder/decoder, text encoder, UNet,其中 UNet 无论是参数量还是计算量,都占绝对的大头,因此 SnapFusion 主要是对 UNet 进行优化。具体分为两部分:(1)UNet 结构上的优化:通过分析原有 UNet 的速度瓶颈,本文提出一套 UNet 结构自动评估、进化流程,得到了更为高效的 UNet 结构(称为 Efficient UNet)。(2)推理步数上的优化:众所周知,扩散模型在推理时是一个迭代的去噪过程,迭代的步数越多,生成图片的质量越高,但时间代价也随着迭代步数线性增加。为了减少步数并维持图片质量,我们提出一种 CFG-aware 蒸馏损失函数,在训练过程中显式考虑 CFG (Classifier-Free Guidance)的作用,这一损失函数被证明是提升 CLIP score 的关键!


    下表是 SD-v1.5 与 SnapFusion 模型的概况对比,可见速度提升来源于 UNet 和 VAE decoder 两个部分,UNet 部分是大头。UNet 部分的改进有两方面,一是单次 latency 下降(1700ms -> 230ms,7.4x 加速),这是通过提出的 Efficient UNet 结构得到的;二是 Inference steps 降低(50 -> 8,6.25x 加速),这是通过提出的 CFG-aware Distillation 得到的。VAE decoder 的加速是通过结构化剪枝实现。




    下面着重介绍 Efficient UNet 的设计和 CFG-aware Distillation 损失函数的设计。


    (1)Efficient UNet


    我们通过分析 UNet 中的 Cross-Attention 和 ResNet 模块,定位速度瓶颈在于 Cross-Attention 模块(尤其是第一个 Downsample 阶段的 Cross-Attention),如下图所示。这个问题的根源是因为 attention 模块的复杂度跟特征图的 spatial size 成平方关系,在第一个 Downsample 阶段,特征图的 spatial size 仍然较大,导致计算复杂度高。




    为了优化 UNet 结构,我们提出一套 UNet 结构自动评估、进化流程:先对 UNet 进行鲁棒性训练(Robust Training),在训练中随机 drop 一些模块,以此来测试出每个模块对性能的真实影响,从而构建一个 “对 CLIP score 的影响 vs. latency” 的查找表;然后根据该查找表,优先去除对 CLIP score 影响不大同时又很耗时的模块。这一套流程是在线自动进行,完成之后,我们就得到了一个全新的 UNet 结构,称为 Efficient UNet。相比原版 UNet,实现 7.4x 加速且性能不降。


    (2)CFG-aware Step Distillation


    CFG(Classifier-Free Guidance)是 SD 推理阶段的必备技巧,可以大幅提升图片质量,非常关键!尽管已有工作对扩散模型进行步数蒸馏(Step Distillation)来加速 [4],但是它们没有在蒸馏训练中把 CFG 纳入优化目标,也就是说,蒸馏损失函数并不知道后面会用到 CFG。这一点根据我们的观察,在步数少的时候会严重影响 CLIP score。


    为了解决这个问题,我们提出在计算蒸馏损失函数之前,先让 teacher 和 student 模型都进行 CFG,这样损失函数是在经过 CFG 之后的特征上计算,从而显式地考虑了不同 CFG scale 的影响。实验中我们发现,完全使用 CFG-aware Distillation 尽管可以提高 CLIP score, 但 FID 也明显变差。我们进而提出了一个随机采样方案来混合原来的 Step Distillation 损失函数和 CFG-aware Distillation 损失函数,实现了二者的优势共存,既显著提高了 CLIP score,同时 FID 也没有变差。这一步骤,实现进一步推理阶段加速 6.25 倍,实现总加速约 46 倍。


    除了以上两个主要贡献,文中还有对 VAE decoder 的剪枝加速以及蒸馏流程上的精心设计,具体内容请参考论文。


    实验结果


    SnapFusion 对标 SD-v1.5 text to image 功能,目标是实现推理时间大幅缩减并维持图像质量不降,最能说明这一点的是下图:




    该图是在 MS COCO’14 验证集上随机选取 30K caption-image pairs 测算 CLIP score 和 FID。CLIP score 衡量图片与文本的语义吻合程度,越大越好;FID 衡量生成图片与真实图片之间的分布距离(一般被认为是生成图片多样性的度量),越小越好。图中不同的点是使用不同的 CFG scale 获得,每一个 CFG scale 对应一个数据点。从图中可见,我们的方法(红线)可以达到跟 SD-v1.5(蓝线)同样的最低 FID,同时,我们方法的 CLIP score 更好。值得注意的是,SD-v1.5 需要 1.4min 生成一张图片,而 SnapFusion 仅需要 1.84s,这也是目前我们已知最快的移动端 Stable Diffusion 模型!


    下面是一些 SnapFusion 生成的样本:




    更多样本请参考文章附录。


    除了这些主要结果,文中也展示了众多烧蚀分析(Ablation Study)实验,希望能为高效 SD 模型的研发提供参考经验:


    (1)之前 Step Distillation 的工作通常采用渐进式方案 [4, 5],但我们发现,在 SD 模型上渐进式蒸馏并没有比直接蒸馏更有优势,且过程繁琐,因此我们在文中采用的是直接蒸馏方案。




    (2)CFG 虽然可以大幅提升图像质量,但代价是推理成本翻倍。今年 CVPR’23 Award Candidate 的 On Distillation 一文 [5] 提出 w-conditioning,将 CFG 参数作为 UNet 的输入进行蒸馏(得到的模型叫做 w-conditioned UNet),从而在推理时省却 CFG 这一步,实现推理成本减半。但是我们发现,这样做其实会造成图片质量下降,CLIP score 降低(如下图中,四条 w-conditioned 线 CLIP score 均未超过 0.30, 劣于 SD-v1.5)。而我们的方法则可以减少步数,同时将 CLIP score 提高,得益于所提出的 CFG-aware 蒸馏损失函数!尤其值得主要的是,下图中绿线(w-conditioned, 16 steps)与橙线(Ours,8 steps)的推理代价是一样的,但明显橙线更优,说明我们的技术路线比 w-conditioning [5] 在蒸馏 CFG guided SD 模型上更为有效。




    (3)既有 Step Distillation 的工作 [4, 5] 没有将原有的损失函数和蒸馏损失函数加在一起,熟悉图像分类知识蒸馏的朋友应该知道,这种设计直觉上来说是欠优的。于是我们提出把原有的损失函数加入到训练中,如下图所示,确实有效(小幅降低 FID)。




    总结与未来工作


    本文提出 SnapFusion,一种移动端高性能 Stable Diffusion 模型。SnapFusion 有两点核心贡献:(1)通过对现有 UNet 的逐层分析,定位速度瓶颈,提出一种新的高效 UNet 结构(Efficient UNet),可以等效替换原 Stable Diffusion 中的 UNet,实现 7.4x 加速;(2)对推理阶段的迭代步数进行优化,提出一种全新的步数蒸馏方案(CFG-aware Step Distillation),减少步数的同时可显著提升 CLIP score,实现 6.25x 加速。总体来说,SnapFusion 在 iPhone 14 Pro 上实现 2 秒内出图,这是目前已知最快的移动端 Stable Diffusion 模型。


    未来工作:


    1.SD 模型在多种图像生成场景中都可以使用,本文囿于时间,目前只关注了 text to image 这个核心任务,后期将跟进其他任务(如 inpainting,ControlNet 等等)。




    1. 本文主要关注速度上的提升,并未对模型存储进行优化。我们相信所提出的 Efficient UNet 仍然具备压缩的空间,结合其他的高性能优化方法(如剪枝,量化),有望缩小存储,并将时间降低到 1 秒以内,离端上实时 SD 更进一步。




    参考文献


    [1] Denoising Diffusion Implicit Models, ICLR’21


    [2] Pseudo Numerical Methods for Diffusion Models on Manifolds, ICLR’22


    [3] DPM-Solver: A Fast ODE Solver for Diffusion Probabilistic Model Sampling in Around 10 Steps, NeurIPS’22


    [4] Progressive Distillation for Fast Sampling of Diffusion Models, ICLR’22


    [5] On Distillation of Guided Diffusion Models, CVPR’23


    [6] http://www.qualcomm.com/news/onq/20…


    [7] Speed Is All You Need: On-Device Acceleration of Large Diffusion Models via GPU-Aware Optimizations, CVPR’23 Workshop


    作者:机器之心
    链接:https://juejin.cn/post/7244452476191850557
    来源:稀土掘金
    收起阅读 »

    使用脚本更新 macOS 壁纸,让你每天看到不同的美景🖼️

    在macOS系统中,我们可以轻松地更换桌面壁纸。但是,如果你每天都想要一张新的壁纸,手动更换就会变得十分繁琐。幸运的是,我们可以使用bash脚本和unsplash API自动更新壁纸。 步骤1:获取unsplash API密钥 首先,你需要注册一个unspla...
    继续阅读 »

    在macOS系统中,我们可以轻松地更换桌面壁纸。但是,如果你每天都想要一张新的壁纸,手动更换就会变得十分繁琐。幸运的是,我们可以使用bash脚本和unsplash API自动更新壁纸。


    步骤1:获取unsplash API密钥


    首先,你需要注册一个unsplash账户,并申请一个API密钥。这个API密钥将允许你通过编程方式访问unsplash图片库。


    步骤2:编写bash脚本


    创建一个新的文本文件,然后在其中添加以下代码:

    #!/bin/bash

    # set the unsplash API access key
    access_key="YOUR_UNSPLASH_API_ACCESS_KEY"

    # define the query to search for wallpaper images
    query="nature"

    # search for a random wallpaper image
    result=$(/usr/bin/curl -s -H "Authorization: Client-ID $access_key" "https://api.unsplash.com/photos/random?query=$query")

    # extract the image URL from the JSON response
    image_url=$(echo "$result" | /opt/homebrew/bin/jq -r '.urls.full')

    # download the image
    /usr/bin/curl -s "$image_url" > ~/Pictures/wallpaper.jpg

    # set the image as the desktop wallpaper
    osascript -e "tell application \"Finder\" to set desktop picture to POSIX file \"$HOME/Pictures/wallpaper.jpg\""

    这段代码会使用unsplash API搜索与“nature”相关的随机图片,并将其下载到“~/Pictures/wallpaper.jpg”文件中。然后,它会使用AppleScript将下载的图片设置为桌面壁纸。


    步骤3:运行bash脚本


    将文件保存为“update-wallpaper.sh”,然后打开终端并导航到该文件所在的目录。运行以下命令以使脚本可执行:

    chmod +x update-wallpaper.sh

    现在,你可以通过在终端中输入以下命令来运行脚本:

    ./update-wallpaper.sh

    步骤4:设置定时任务

    脚本依赖:curl、jq、bash、unsplash,使用 which 获取路径,然后替换脚本里的curl和jq。

    which curl
    which jq

    你可以将该脚本设置为定时任务,以便每天自动更新壁纸。打开“终端”并输入以下命令以编辑cron定时任务:

    crontab -e

    然后,添加以下行:

    0 9 * * * /path/to/update-wallpaper.sh

    这将在每天上午9点运行该脚本。

    现在,你可以坐下来,放松一下,让你的macOS自动更新壁纸。享受吧!

    作者:FreeCultureBoy
    链接:https://juejin.cn/post/7226301946839089211
    来源:稀土掘金

    收起阅读 »

    完全免费白嫖 GPT-4 的终极方案!

    GPT-4 目前是世界上最强的多模态大模型,能力甩 GPT-3.5 好几条街。 大家都希望早日用上 GPT-4,不过目前体验 GPT-4 的渠道非常有限,要么就是开通 ChatGPT 尊贵的 Plus 会员,即使你开了会员,也是有限制的,每 3 小时只能发送 ...
    继续阅读 »


    GPT-4 目前是世界上最强的多模态大模型,能力甩 GPT-3.5 好几条街。


    大家都希望早日用上 GPT-4,不过目前体验 GPT-4 的渠道非常有限,要么就是开通 ChatGPT 尊贵的 Plus 会员,即使你开了会员,也是有限制的,每 3 小时只能发送 25 条消息。。。


    要么就去 OpenAI 官网申请 GPT-4 的 API,但是目前申请到 API 的小伙伴非常少,你以为申请到 API 就可以用了吗?GPT-4 的 API 价格超级无敌贵,是 GPT-3.5 价格的 30 倍,你敢用吗?😄


    然而,但是,既然我写了这篇文章,肯定是要告诉那一个惊天大幂幂的!


    现在完全免费白嫖 GPT-4 的机会来了,不仅可以白嫖,还可以直接作为 API 来调用!


    不仅能够作为 API 调用,我还接入了公众号给大家白嫖,你说气人不气人?



    下面言归正传,开始手把手教大家如何免费白嫖 GPT-4


    gpt4free-ts 介绍


    GPT4Free 大家应该都知道吧?它上线几周就在 GitHub 上揽收了接近 4w 的 Star。原因就在于其提供了对 GPT-4 及 GPT-3.5 免费且几乎无限制的访问。该项目通过对各种调用了 OpenAI API 网站的第三方 API 进行逆向工程,达到使任何人都可以免费访问该流行 AI 模型的目的。


    这就相当于什么?假设地主家有一个粮仓,你往他家的粮仓偷偷插了一根管子,不停地向外抽米,他全然不知,所以你也不用交钱,一切费用由地主承担


    现在接入 GPT-4 的第三方网站就相当于那个地主,懂了吧?


    但是这个项目并没有封装 API,而且目前也不太能用了。


    作为开发者,我们想要的肯定是 API 啊!这就要提到今天的主角了:gpt4free-ts


    这个项目是用 TypeScript 写的,相当于 GPT4Free 的 TypeScript 版本,但是更方便部署,而且封装了 API,简直就是开发者的福音,就他了!


    这个项目向多个地主家的粮仓插了管子,其中最强大的地主就是 forefront.ai,这个地主家的粮仓里就包含了 GPT-4 这个香饽饽,而且还有 Claude,就嫖他了!


    除了 forefront 之外,它接的粮仓还挺多的。。



    大批量注册临时邮箱


    forefront 的 GPT-4 模型是有限制的,每个账号每 3 小时内只能发送 5 条消息


    所以接下来需要用到一个非常神奇的服务叫 RapidAPI你可以通过这个 API 来获取无穷无尽的临时邮箱,然后再用这些无穷无尽的临时邮箱去注册无穷无尽的 forefront 账号。


    这么一说,你是不是就悟了?哈哈哈


    首先你需要在这里注册一个账号并登录:rapidapi.com/calvinlovel…


    然后需要在 Pricing 页面开启订阅:



    一般情况下订阅免费套餐即可,一天可以调用 100 次。


    如果你有更高的需求,可以考虑订阅更高级的套餐(比如你的用户数量特别多)。


    订阅完了之后,你就能看到 API Key 了。这个 Key 我们后面会用到。



    Sealos 云操作系统介绍


    单机操作系统大家应该都知道吧?Windows、macOS、Linux 这些都属于单机操作系统,为什么叫单机操作系统呢?因为他的内存啊,CPU 啊,都在一台机器上,你不可能用其他机器的内存和 CPU。


    那么什么是云操作系统呢?就是把一群机器的 CPU 和内存看成一个整体,然后给用户提供一个交互界面,用户可以通过这个交互界面来操作所有的资源。


    懂 K8s 的玩家可能要说了:这个我懂,K8s 就可以!


    如果我们的目标愿景是一个云操作系统,K8s 充其量只能是这个云操作系统的内核,就像 Linux 内核一样。完整的云操作系统需要一个像 Windows 和 Ubuntu 操作系统那样的交互界面,也就是操作系统发行版


    对于云操作系统来说,Sealos 就是那个发行版。



    链接:cloud.sealos.io




    有人可能会把云操作系统理解成“Web 界面”,但其实不是,Sealos 云操作系统完全是类似于 Windows 和 macOS 桌面的那种逻辑,并不是 Web 界面。我只需要点几下鼠标,一个应用就装好了,老夫并不知道什么容器什么 K8s。


    数据库也一样,小鼠标一点,一个分布式数据库就装好了。


    我知道,这时候云原生玩家要坐不住了,您别着急,看到桌面上的终端了没?



    终端只是这个云操作系统中的一个 App 而已。同理,容器管理界面仍然可以作为云操作系统的 App,我管你是 Kubernetes Dashboard、Rancher、KubeSphere 还是 Kuboard,都可以作为 App 装在这个云操作系统中。这时候对于云原生专家而言,仍然可以命令行咔咔秀操作,也可以通过各种管理界面来管理容器。


    云操作系统嘛,就是要什么人都能用才行,不管你是什么角色,都能在这个操作系统里找到你想要的 App 去完成你的使命


    安装 gpt4free-ts


    接下来才是这篇文章的重头戏。


    我要教大家如何在 Sealos 中点几下鼠标就能安装一个 gpt4free-ts 集群


    没错,就是 gpt4free-ts 集群。


    什么叫集群?就是说我要运行一群 gpt4free-ts 实例,然后前面加一个负载均衡作为对外的 API 入口。


    下面的步骤非常简单,楼下的老奶奶都会,是真的,当时我就在楼下看她操作


    首先进入 Sealos 云操作系统的界面:**cloud.sealos.io**。


    然后打开桌面上的应用管理 App:



    点击「新建应用」:



    在启动参数中,按照以下方式进行设置:

    1.应用名称随便写,比如 gpt4free。
    2.镜像名称是:xiangsx/gpt4free-ts:latest
    CPU 和内存需要根据应用的实际情况来填写。这个应用运行之后默认会启动两个 Chrome 浏览器来模拟登录 forefront,每次对话会从里面取一个账号来使用,次数用完了会自动注册新账号(因为每个账号每 3 小时只能发送 5 条信息)。我们可以通过环境变量来修改启动的浏览器数量,所以需要根据你的浏览器数量来确定 CPU 和内存。 我自己把浏览器数量设置为 3,所以需要的内存和 CPU 比较多(后面会告诉你怎么设置环境变量)。
    3.实例数根据自己的实际需求填写,我需要接入公众号,粉丝比较多,一个实例才 3 个账号(因为我一个实例跑了 3 个浏览器),根本不够用,所以我开了 3 个实例。
    4.容器暴露端口指定为 3000。
    5.打开外网访问。



    继续往下,展开高级设置,点击「编辑环境变量」:



    填入以下环境变量:

    rapid_api_key=<rapid_api_key>
    DEBUG=0
    POOL_SIZE=3


    ⚠️注意:请将 <rapid_api_key> 替换为你自己的 key。



    其中 POOL_SIZE 就是浏览器数量,每个浏览器会登录一个 forefront 账号。你可以根据自己的需要调整浏览器数量,并根据浏览器数量调整 CPU 和内存。如果你不知道怎么调整合适,建议无脑跟着本文操作。



    继续,点击「新增存储卷」:



    容量只需 1G,挂载路径设置为 /usr/src/app/run




    这个存储的作用是为了保存已登录的账号。已经注册的账号 3 个小时以后还可以重新使用,不用再浪费邮箱去注册新账号。



    最终点击右上角的「部署应用」,即可完成部署:



    最终要等待所有的实例都处于 Running 状态,才算是启动成功了。



    点击右边的复制按钮,便可复制 API 的外网地址:



    我们来测一下这个 API:



    完美!打完收工!


    接入微信公众号


    什么?你想将这个 API 接入自己的公众号?换个形式吧!直接来看直播吧,我们会通过直播活动手把手教你如何将 GPT-4 免费接入公众号、网页等各种前端。


    活动链接:forum.laf.run/d/684


    当然,直播过程不会直接教你如何接入公众号,而是“授你🫵以渔”,告诉你如何使用 Laf 来通过各种姿势调用这个 API,最终你也接入公众号也罢,网页前端也罢,那都不是事儿~

    作者:米开朗基杨
    链接:https://juejin.cn/post/7241790368949190693
    来源:稀土掘金
    收起阅读 »

    雷军写的代码上热搜了!

    就在昨天,「雷军写的代码」相关话题先后上了一波热搜和热榜。 出于好奇,第一时间点进去围观了一波。 原来雷总马上要在8月14日举办他的2023年度演讲了,并且也放出了对应的演讲海报。 这个海报可以说暗藏玄机,放大后仔细一看,好家伙,密密麻麻全都是代码这是...
    继续阅读 »

    就在昨天,「雷军写的代码」相关话题先后上了一波热搜和热榜。




    出于好奇,第一时间点进去围观了一波。



    原来雷总马上要在8月14日举办他的2023年度演讲了,并且也放出了对应的演讲海报。



    这个海报可以说暗藏玄机,放大后仔细一看,好家伙,密密麻麻全都是代码这是!



    看一下代码细节,都是类似MOVJMPPUSHLOOP等这些指令。


    这不就是自己当年上学时学得瑟瑟发抖痛哭涕零的汇编语言嘛。。



    这一瞬间就让我想起了当年微博上的这张图:



    早在十几年前,微博上就曾流传过雷军早年所写的一段完整的汇编代码。



    当时雷军也转发过,并表示这个程序的第一个版本是他1989年写的,怀念当初写程序的快乐时光。



    在网上经常会看到关于雷军代码水平到底如何的讨论帖子。


    可以说,看看雷军早年写的这段代码相信心里基本就有答案了。


    在早年那个机器硬件水平和性能都十分受限的年代,为了满足某些需求,可以说对编码的程序员提出了非常苛刻的要求。


    开发者往往只能使用非常底层的编程语言去实现某些程序,同时还需要把代码优化做到极致,这本就非常考验程序员的基本功和编程底子。


    各方面信息都显示,作为一个程序员来说,雷军不仅仅是合格,说他是非常厉害的大神那也丝毫没有毛病。


    就像这次热搜话题下稚晖君大佬的一篇动态所言,雷军作为老一代程序员的代表和创业楷模,确实值得敬佩。



    文章的最后,我们也找到了当年雷军所写的这段汇编代码的完整版,这里也分享给大家。


    咳咳,前方高能!!!


    作者:CodeSheep
    链接:https://juejin.cn/post/7265679390242537512
    来源:稀土掘金
    收起阅读 »

    吐槽大会,来瞧瞧资深老前端写的垃圾代码

    忍无可忍,不吐不快。 本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。 知道了什么是烂代码,才能写出好代码。 别说什么代码和人有一个能跑就行的话,玩笑归玩笑。 人都有菜的时候,写出垃圾代码无可厚非,但是工作几年了...
    继续阅读 »

    忍无可忍,不吐不快。


    本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。


    知道了什么是烂代码,才能写出好代码。


    别说什么代码和人有一个能跑就行的话,玩笑归玩笑。


    人都有菜的时候,写出垃圾代码无可厚非,但是工作几年了还是写垃圾代码,有点说不过去。


    我认为的垃圾代码,不是你写不出深度优先、广度优先的算法,也不是你写不出发布订阅、策略模式的 if else


    我认为垃圾代码都有一个共性:看了反胃。就是你看了天然的不舒服,看了就头疼的代码,比如排版极丑,没有空格、没有换行、没有缩进。

    优秀的代码就是简洁清晰,不能用策略模式优化 if else 没关系,多几行代码影响不大,你只要清晰,就是好代码。


    有了差代码、好代码的基本标准,那我们也废话不多说,来盘盘这些资深前端的代码,到底差在哪里。


    ---------------------------------------------更新------------------------------------------------


    集中回答一下评论区的问题:


    1、项目是原来某大厂的项目,是当时的外包团队写的,整体的代码是还行的。eslintcommitlint 之类的也是有的,只不过这些都是可以禁用的,作用全靠个人自觉。


    2、后来项目被我司收购,原来的人也转为我司员工,去支撑其他项目了。这个项目代码也就换成我们这批新来的维护了。很多代码是三几年前留下的确实没错,谁年轻的时候没写过烂代码,这是可以理解的。只不过同为三年前的代码,为啥有人写的简单清晰,拆分清楚,有的就这么脏乱差呢?


    3、文中提到的很多代码其实是最近一年写的,写这些功能的前端都是六七年经验的,当时因为项目团队刚组建,没有前端,抽调这些前端过来支援,写的东西真的非常非常不规范,这是让人难以理解的,他们在原来的项目组(核心产品,前端基建和规范都很厉害)也是这么写代码的吗?


    4、关于团队规范,现在的团队是刚成立没多久的,都是些年轻人,领导也不是专业前端,基本属于无前端 leader 的状态,我倒是很想推规范,但是我不在其位,不好去推行这些东西,提了建议,领导是希望你简单写写业务就行(说白了我不是 leader 说啥都没用)。而且我们这些年轻前端大家都默认打开 eslint,自觉性都很高,所以暂时都安于现状,代码 review 做过几次,都没太大的毛病,也就没继续做了。


    5、评论区还有人说想看看我写的代码,我前面文章有,我有几个开源项目,感兴趣可以自己去看。项目上的代码因为涉及公司机密,没法展示。


    6、本文就是篇吐槽,也不针对任何人,要是有人看了不舒服,没错,说的就是你。要是没事,大家看了乐呵乐呵就行。轻喷~


    文件命名千奇百怪


    同一个功能,都是他写的,三个文件夹三种奇葩命名法,最重要的是第三个,有人能知道它这个文件夹是什么功能吗?这完全不知道写的什么玩意儿,每次我来看代码都得重新理一遍逻辑。




    组件职责不清


    还是刚才那个组件,这个 components 文件夹应该是放各个组件的,但是他又在底下放一个 RecordDialog.jsx 作为 components 的根组件,那 components 作为文件名有什么意义呢?




    条件渲染逻辑置于底层


    这里其实他写了几个渲染函数,根据客户和需求的不同条件性地渲染不同内容,但是判断条件应该放在函数外,和函数调用放在一起,根据不同条件调用不同渲染函数,而不是函数内就写条件,这里就导致逻辑内置,过于分散,不利于维护。违反了我们常说的高内聚、低耦合的编程理念。




    滥用、乱用 TS


    项目是三四年前的项目了,主要是类组件,资深前端们是属于内部借调来支援维护的,发现项目连个 TS 都没有,不像话,赶紧把 TS 配上。配上了,写了个 tsx 后缀,然后全是无类型或者 anyscript。甚至于完全忽视 TSESlint 的代码检查,代码里留下一堆红色报错。忍无可忍,我自己加了类型。




    留下大量无用注释代码和报错代码


    感受下这个注释代码量,我每次做需求都删,但是几十个工程,真的删不过来。






    丑陋的、隐患的、无效的、混乱的 css


    丑陋的:没有空格,没有换行,没有缩进


    隐患的:大量覆盖组件库原有样式的 css 代码,不写类名实现 namespace


    无效的:大量注释的 css 代码,各种写了类名不写样式的垃圾类名


    混乱的:从全局、局部、其他组件各种引入 css 文件,导致样式代码过于耦合




    一个文件 6 个槽点


    槽点1:代码的空行格式混乱,影响代码阅读


    槽点2:空函数,写了函数名不写函数体,但是还调用了!


    槽点3:函数参数过多不优化


    槽点4:链式调用过长,属性可能存在 undefined 或者 null 的情况,代码容易崩,导致线上白屏


    槽点5:双循环嵌套,影响性能,可以替换为 reduce 处理


    槽点6:参数、变量、函数的命名随意而不语义化,完全不知道有何作用,且不使用小驼峰命名




    变态的链式取值和赋值


    都懒得说了,各位观众自己看吧。




    代码拆分不合理或者不拆分导致代码行数超标


    能写出这么多行数的代码的绝对是人才。


    尽管说后来维护的人加了一些代码,但是其初始代码最起码也有近 2000 行。




    这是某个功能的数据流文件,使用 Dva 维护本身代码量就比 Redux 少很多了,还是能一个文件写出这么多。导致到现在根本不敢拆出去,没人敢动。




    杂七杂八的无用 js、md、txt 文件


    在我治理之前,充斥着非常多的无用的、散乱的 md 文档,只有一个函数却没有调用的 js 文件,还有一堆测试用的 html 文件。


    实在受不了干脆建个文件夹放一块,看起来也要舒服多了。




    less、scss 混用


    这是最奇葩的。




    特殊变量重命名


    这是真大佬,整个项目基建都是他搭的。写的东西也非常牛,bug 很少。但是大佬有一点个人癖好,例如喜欢给 window 重命名为 G。这虽然算不上大缺点,但真心不建议大家这么做,window 是特殊意义变量,还请别重命名。

    const G = window;
    const doc = G.document;

    混乱的 import


    规范的 import 顺序,应该是框架、组件等第三方库优先,其次是内部的组件库、包等,然后是一些工具函数,辅助函数等文件,其次再是样式文件。乱七八糟的引入顺序看着都烦,还有这个奇葩的引入方式,直接去 lib 文件夹下引入组件,也是够奇葩了。


    总而言之,css 文件一般是最后引入,不能阻塞 js 的优先引入。




    写在最后


    就先吐槽这么多吧,这些都是平时开发过程中容易犯的错误。希望大家引以为戒,不然小心被刀。


    要想保持一个好的编码习惯,写代码的时候就得时刻告诉自己,你的代码后面是会有人来看的,不想被骂就写干净点。


    我觉得什么算法、设计模式都是次要的,代码清晰,数据流向清晰,变量名起好,基本 80% 不会太差。


    不过说这么多,成事在人。


    不过我写了一篇讲解如何写出简洁清晰代码的文章,我看不仅是一年前端需要学习,六七年的老前端也需要看看。

    作者:北岛贰
    链接:https://juejin.cn/post/7265505732158472249
    来源:稀土掘金
    收起阅读 »

    接口测试神器:ApiKit

    想给大家分享一款技术人必备的接口测试神器:ApiKit,应该是我目前用过,算得上良心的接口工具1.背景 作为互联网行业技术从业者,接口调试是必不可少的一项技能,通常我们都会选择使用 Postman 这类工具来进行接口调试,在接口调试方面 Postman 做的确...
    继续阅读 »

    想给大家分享一款技术人必备的接口测试神器:ApiKit,应该是我目前用过,算得上良心的接口工具

    1.背景


    作为互联网行业技术从业者,接口调试是必不可少的一项技能,通常我们都会选择使用 Postman 这类工具来进行接口调试,在接口调试方面 Postman 做的确实非常出色。


    但是在整个软件开发过程中,接口调试只是其中的一部分,还有很多事情 Postman 是无法完成的,或者无法高效完成,比如:接口文档定义、Mock 数据、接口自动化测试等等。


    今天给大家推荐的一款神器: ApiKit=API 管理 + Mock + 自动化测试 + 异常监控 + 团队协作

    1.1聊一聊接口管理的现状


    对于接口管理的现状来说,目前行业大部分采取的解决方案有如下几种:

    1.使用 **Swagger**管理接口文档。 

    2. 使用 Postman 调试接口。

    3.使用 **RAP或Easy Mock**来进行 **Mock**数据。 

    4. 使用 JMeter 做接口自动化测试。


    而上述的接口管理手段,咋一看,貌似没有什么问题,但仔细分析,不难发现,当中存在的问题还真不少,比如要维护不同工具,并且这些工具之间数据一致性非常困难、非常低效。这里不仅仅是工作量的问题,更大的问题是多个系统之间数据不一致,导致协作低效,频繁出问题,开发人员、测试人员工作起来也痛苦不堪。


    设想一下这样的一个协作流程(官方示例):



    1. 开发人员在Swagger定义好文档后,接口调试的时候还需要去 Postman 再定义一遍。 2. 前端开发Mock 数据的时候又要去RAPEasy Mock定义一遍,手动设置好 Mock 规则。 3. 测试人员需要去 JMeter定义一遍。 4. 前端根据 RAPEasy Mock定义 Mock 出来的数据开发完,后端根据 Swagger定义的接口文档开发完,各自测试测试通过了,本以为可以马上上线,结果一对接发现各种问题:原来开发过程中接口变更,只修改了 Swagger,但是没有及时同步修改 RAPEasy Mock。 5. 同样,测试在 JMeter 写好的测试用例,真正运行的时候也会发现各种不一致。 6. 时间久了,各种不一致会越来越严重。

    ApiKit介绍

    官方对ApiKit定位是,API 管理 + Mock + 自动化测试 + 异常监控 + 团队协作 

    结合 API 设计、文档管理、自动化测试、监控、研发管理和团队协作的一站式 API 生产平台,从个人开发者到跨国企业用户,Apikit 帮助全球超过50万开发者和10万家企业更快、更好且更安全地开发和使用 API 

    概括来讲,ApiKit 常用功能分为五类: 

    1.智能且强大的 Mock 前端团队可以在 API 还没开发完成的情况下,借助 Mock API 实现预对接,加速开发进程。测试团队可以通过 Mock API 解耦不必要的系统,完成集成测试 

    2.快速生成和管理所有 API 文档 无论您使用什么语言开发,Apikit 都可以帮您统一规范地管理起来,并提供强大的文档管理、协作、测试、分享功能 

    3. 零代码自动化测试 Apikit 提供了 API 测试功能,支持自动生成测试数据,能够通过Javascript 对请求报文、返回结果等进行加解密、签名等处理;提供强大、易用的企业级 API 自动化测试解决方案,5分钟快速上手,提高 95% 以上回归测试效率,人人皆可使用的“零代码”自动化测试平台; 

    4. 领先的 API 团队协作功能 无论您使用什么语言开发,Apikit 都可以帮您统一规范地管理起来,并提供强大的文档管理、协作、测试、分享功能 

    5.还有更多的 Devops 功能 API 异常监控,对接CI/CD、DevOps 平台,支持主流IM ,也可通过自由拓展。 

    ApiKit 小试牛刀 

    接下来,带着大家,简单体验一下ApiKit的使用。

    Apikit 有三种客户端,你可以依据自己的情况选择。三种客户端的数据是共用的,因此你可以随时切换不同的客户端。

    我们推荐使用新推出的 Apikit PC 客户端,PC端拥有线上产品所有的功能,并且针对本地测试、自动化测试以及使用体验等方面进行了强化,可以提供最佳的使用感受。 



    我们建议对本地测试有需求的用户使用PC端,可满足更多本地测试需求。



    发起 API 测试


    进入 API 文档详情页,点击上方 测试 标签,进入 API 测试页,系统会根据API文档自动生成测试界面并且填充测试数据。




    填写请求参数


    首先填写好请求参数。



    请求头部


    您可以输入或导入请求头部。批量导入的数据格式为 key : value ,一行一条header信息,如:

    Connection: keep-alive
    Content-Encoding: gzip
    Content-Type: application/json
    Date: Mon, 30 Dec 2019 20:49:45 GMT

    请****求体


    请求体提供了五种类型:

    1. Form-data(表单)

    2. JSON

    3. XML

    4. Raw(自定义文本类型数据)

    5. Binary(字节流、文件参数)


    产品中提供了的 JSON 和 XML 编辑器,当您已经在 API 文档中定义好 API 的请求数据结构时,只需要在测试界面填写各个字段的值,系统会自动转换为相应的 JSON 和 XML 结构的请求数据。


    Query 参数


    Query参数指的是地址栏中跟在问号?后面的参数,如以下地址中的 user_name 参数:

    /user/login?user_name=jackliu

    批量导入的数据格式为 ?key=value ,通过&分隔多个参数,如:

    api.eolinker.com/user/login?user_name=jackliu&user_password=hello

    REST参数

    REST参数指的是地址栏被斜杠/分隔的参数,如以下地址中的user_name、user_password参数。

    /user/login/{user_name}/{user_password}

    注意,只需要在URL中使用 {} 将REST参数括起来,下方的请求参数名中不需要使用 {} 。


    处理脚本


    脚本分为 前置脚本后置脚本 两种,分别对应 API 请求前 和 返回数据后 的两个阶段。您可以通过编写 Javascript 代码,在 API 前置脚本中改变请求参数,或者是在 API 后置脚本中改变返回结果。


    脚本常用于以下几种情况:

    1.API 请求前对请求参数进行复制、加解密等操作,比如进行Body进行整体签名 

    2. API 返回结果后对结果进行解密等


    发起的API请求会依次经过以下流程。其中如果您没有编写相应的API脚本,则会略过API脚本处理阶段。



    管****理 Cookie


    当您测试需要 Cookie 的 API 时,可以先进行一次 API 登录或者在 Cookie 管理里添加所需的 Cookie 信息,系统会自动将 Cookie 储存起来,下次测试其他相同域名的 API 时会自动传递 Cookie 请求参数。




    查看测试结果


    填写好请求参数后,点击测试按钮即可得到测试报告,报告包括以下内容:

    1.返回头部 

    2. 返回内容 

    3.实际请求头部 

    4. 实际请求内容 

    5.请求时间分析




    快速生成mock


    在高级mock页面,选择添加为mock,可快速生成mock。



    将测试用例请求参数和返回参数自动带到mock的请求报文和响应报文中。



    ApiKit 更多特性


    新建 API 文档



    团队协作,API分享



    高级mock



    创建自动化测试



    API 异常警告



    环境管理



    前后置脚本



    创建项目



    APIHub




    关于 ApiKit 的更多功能,值得你来尝试体验!


    传送门:


    http://www.eolink.com/?utm_source…


    小结


    虽然 ApiKit 目前有些功能还并不完善,但整的来说,ApiKit 还是不错的,也为接口开发调试测试提供了一种效率更佳的的解决方案。

    作者:CV_coder
    链接:https://juejin.cn/post/7237024604962766909
    来源:稀土掘金
    收起阅读 »

    Mac效率神器Alfred Workflows

    Alfred 是 Mac 上一款著名的效率应用,强大的功能和众多的扩展能让你在实际操作中大幅提升工作效率, 这里简单介绍下 Alfred 的工作流 背景&效果展示 作为一名程序员经常会遇到时间戳转时间、时间转时间戳的情况,以前都会打开网页在线工具进行...
    继续阅读 »

    Alfred 是 Mac 上一款著名的效率应用,强大的功能和众多的扩展能让你在实际操作中大幅提升工作效率,
    这里简单介绍下 Alfred 的工作流



    背景&效果展示


    作为一名程序员经常会遇到时间戳转时间、时间转时间戳的情况,以前都会打开网页在线工具进行转换。
    每次打开浏览器找到网址,然后复制内容进行转换都需要 5秒以上的时间。
    有什么快捷的方式能帮助我们快速的进行这个操作吗,这里我想到的 Alfred 的工作流。
    Alfred工作流可以直接脚本开发,下面是工作流开发完的效果。


    1)首先唤起Alfred输入框,这里看自己设置的快捷键了
    2)输入这里工作流对应的keyword (tm或tmt)然后空格输入需要转换的内容
    3)回车键将转换之后的内容复制到剪切板


    这个简单的工作流可以实现linux类型的时间戳转换成 yyyy-MM-dd HH:mm:ss 类型的时间字符串,
    也可以将 yyyy-MM-dd HH:mm:ss 类型的时间字符串转换成时间。
    转换完的内容会自动放到剪切板里面,可以直接使用 command + v 进行粘贴,也可以使用Alfred的历史剪切板进行复制。


    工作流开发


    1、创建空的工作流


    为了简单快捷这里使用的是python开发的这个功能。
    开发这个工作流首先需要再 Alfred 面板上创建一个空的工作流
    Alfred -> Preferences -> Workflows -> 左下角的 + -> Blank Workflows
    如下图: 



    2、添加流程节点


    这里使用的是 Script Filter类型的节点,可以支持keyword触发。
    节点配置如下,keyword 为触发的命令关键字。下面是运行命令的配置 Script输入框中写需要运行的命令,
    这里使用 {query} 方式将转换的内容传递给python脚本。 



    复制到剪切板的节点如下,创建好节点用线连接就行。 



    3、python脚本开发


    脚本的位置放到当前工作流的根目录就行,这样不行写绝对路径,也方便工作流的导出。
    1. 打开工作流根目录:右键选中工作流,点 open in find 或者 open in terminal 打开工作流根目录:右键选中工作流,点 open in find 或者 open in terminal 


    2.Alfred官方提供了一个python类库方便开发工作流。在工作流根目录执行命令

    pip install --target=. Alfred-Workflow

    3.创建脚本 timestamp_2_time.py,然后开发对应的代码即可 。代码里面有注释大家可以看下

    # -*- coding: utf-8 -*-
    import sys
    from datetime import datetime
    from workflow import Workflow, ICON_CLOCK # 导包

    def main(wf):
    query = wf.args[0] # 获取传入的参数,这里能获取到需要转换的呢绒
    if not query:
    return
    # 时间戳转时间字符串的方法
    d = datetime.fromtimestamp(int(query) / 1000)
    str1 = d.strftime("%Y-%m-%d %H:%M:%S")
    '''
    调用框架的方法添加运行的结果
    可选参数是标题、副标题,arg是下一个节点的入参,icon是这个item展示的图标
    如果有多个结果可以放多个,然后通过上下键选择
    '''
    wf.add_item(title=query, subtitle=str1, arg=str1, valid=True, icon=ICON_CLOCK)
    # 展示结果内容list
    wf.send_feedback()


    if __name__ == '__main__':
    '''构造 Workflow 对象,运行完退出
    '''
    wf = Workflow()
    sys.exit(wf.run(main))

    通过这几行简单的代码实现了时间戳转换成时间的小功能,
    相比于以前的使用网页的形式,这个工作流可以将时间缩短到1秒,每次为你省下 4 秒钟的时间 😂😂😂


    debug


    开发的时候可能会遇到bug,可以通过下图方式打开运行日志查问题。 



    开发好的工作流要使用直接导入就行


    tmwf.tar
    下载之后 tar -xvf tmwf.tar 解压导入就行。
    这里需要注意下,我本地的python路径是 /usr/local/bin/python 大家需要换成自己的python路径。

    作者:程序员大鹏
    链接:https://juejin.cn/post/7252541723149238330
    来源:稀土掘金
    收起阅读 »

    rpc比http好吗,缪论?

    是什么,如何理解 RPC(Remote Procedure Call) 直译就是远程过程调用 HTTP(HyperText Transfer Protorl) 直译就是超文本传输协议 RPC和HTTP都是 请求-响应协议,但是因为出现的时机、设计理念、约定协议...
    继续阅读 »

    是什么,如何理解


    RPC(Remote Procedure Call) 直译就是远程过程调用


    HTTP(HyperText Transfer Protorl) 直译就是超文本传输协议


    RPC和HTTP都是 请求-响应协议,但是因为出现的时机、设计理念、约定协议、效率、应用范围、使用规则等不同,所以是不同的名字,本质都是为了分布式系统间的通信而生,是一种应用层通信(请求-响应)协议(从OSI网络模型来看)。

    RPC是 Bruce Jay Nelson 在1981年创造的术语,HTTP是在1990年左右产生的(可以参看维基百科)


    RPC协议 和 RPC,到底叫什么?RPC协议=RPC


    HTTP协议、HTTP,到底叫什么?HTTP协议=HTTP


    RPC|HTTP只是大家的简称

    1、HTTP协议不仅仅只有协议,还有超文本,传输,以及很多功能(比如编解码、面试经常背的各种参数的作用)

    2、RPC协议也不仅仅只有协议,还有 编解码,服务注册发现,负载均衡等


    RPC协议本质上定义了一种通信的流程,而具体的实现技术是没有约束的,每一种RPC框架都有自己的实现方式,我认为HTTP也是RPC的一种实现方式


    协议直白来讲是一种约定,rpc和http都是为了服务器间的通信而生,都需要制定一套标准协议来进行通信。不过HTTP比较火,是一个全世界的统一约定,使用比较广泛。但通用也意味着冗余,所以后来又产生了很多RPC框架(自定义协议,具备优秀的性能等)


    我们可以自定义RPC请求/响应 包含的消息头和消息体结构,自定义编解码方式,自定义网络通信方式,只要clientserver消息的发送和解析能对应即可,这些问题确认下来,一个RPC框架就设计出来了


    下面先从请求过程看一下RPC和HTTP都会经历哪些阶段,然后再分阶段去做对比

    一次请求的过程



    从请求链路可以看到,最核心的只有三层:编解码、协议、网络通信


    下面会从这3个角度去对比HTTP和RPC


    HTTP VS RPC自定义协议


    HTTP和RPC 2个关键词不具备可比较性,因为RPC包含了HTTP。


    但是RPC自定义协议(thrift, protobuf, dubbo, kitex-thrift等) 是RPC的具体实现,HTTP也是RPC的具体实现,它们是具备可比较性的


    编解码(序列化)



    • 序列化: 指将程序运行过程中的动态内存数据(java的class、go的struct)转化为硬盘中静态二进制数据的过程,以方便网络传输。

    • 反序列化:指将硬盘中静态二进制数据转化为程序运行过程中的动态内存数据的过程,以方便程序计算。


    HTTP/1.1 一般用json


    自定义RPC协议 一般用 thrift、protobuf


    kitex序列化协议


    协议层


    编码之后,数据转换成字节流,但是RPC通信时,每次请求发送的数据大小不是固定的,那么为了区分消息的边界,避免粘包、半包等现象,我们需要定义一种协议,来使得接收方能够正确地读出不定长的内容。简单点说,通信协议就是约定客户端和服务器端传输什么数据,以及如何解析数据。


    可参考

    1、kitex:概览,传输协议

    2、dubbo:triple 协议,概览


    可以思考一下 序列化、传输协议、网络通信的关系,下面以kitex为例进行分析


    kitex codec 接口定义kitex thrift 序列化实现kitex ttheader协议,kitex 发送请求核心代码


    可以发现 Encode中,先根据message构造出header,写入out,然后再把data(实际的业务数据)写到out。

    encode函数完全遵守 ttheader协议去构造数据。

    最后再把out通过网络库发送出去

    网络通信层

    网络通信层主要提供一个易用的网络库,封装了操作系统提供的socket api。


    HTTP的长连接和TCP长连接不是一个东西,需要注意下,TCP Keepalive是操作系统实现的功能,并不是TCP协议的一部分,需要在操作系统下进行相关配置(只能保证网络没问题,不能代表服务没问题)


    其中 HTTP2 拥有多路复用、优先级控制、头部压缩等优势


    可以参考


    kitex:连接类型


    RPC自定义协议 和 HTTP的使用场景


    公司内部的微服务,对客户端提供的服务 适合用RPC,更好的性能


    对外服务、单体服务、为前端提供的服务适合用HTTP


    我的思考


    rpc在编解码、协议层、网络通信 都比HTTP有更大的优势,那为啥不把HTTP换成RPC呢

    1、人的认知,HTTP已经深入人心(或者说生态好,通用性强),几乎所有的机器、浏览器和语言默认都会支持。但是自定义RPC协议 可能很多人都没听过(比如kitex、dubbo等),还让别人支持,根本不可能。

    • 需要建设全局的DNS等等,HTTP链路中的组件都需要换成 自定义的那一套,成本极高。
    • 但是公司内部可以搞成一套,可以极大提高性能,何乐而不为。

    • 我见过的案例是很多时候并没有深入思考为什么用,而是大家都这么用,我也这么用。

    2、浏览器只支持 http协议。而且浏览器不支持自定义编解码的解析
          为啥大家面向浏览器/前端 不用自定义编解码?
         http不仅可以传输json、还可以传输二进制、图片等。所以协议层可以用http,编解码用protobuf/thrift也是可行的。
    • 公司内部实际案例:服务端和客户端交互时,为了提高性能,采用protobuf编解码数据,使用http协议传输数据。

    • 但是每次请求/响应数据都是不可读的。服务端会把protobuf编码前的数据转为json,用于打印log/存储,方便排查问题。

    3、RPC框架 可以自定义负载均衡,重试机制,高可用,流量控制等策略。这些是HTTP不能支持的
    • 我理解是协议层用的http,但是内部的运行机制还是自定义的。http只是定义了传输数据的格式。举个例子:http的流量控制其实用的是 tcp的滑动窗口,http协议本身不具备这些功能。但是rpc是可以自己加这些功能的。这些功能必然有数据传输,这个传输协议用的http。

    作者:cli
    链接:https://juejin.cn/post/7264454873588449336
    来源:稀土掘金

    收起阅读 »

    SpringBoot获取不到用户真实IP怎么办

    今天周六,Binvin来总结一下上周开发过程中遇到的一个小问题,项目部署后发现服务端无法获取到客户端真实的IP地址,这是怎么回事呢?给我都整懵逼了,经过短暂的思考,我发现了问题的真凶,那就是我们使用了Nginx作的请求转发,这才导致了获取不到客户端真实的IP地...
    继续阅读 »
    今天周六,Binvin来总结一下上周开发过程中遇到的一个小问题,项目部署后发现服务端无法获取到客户端真实的IP地址,这是怎么回事呢?给我都整懵逼了,经过短暂的思考,我发现了问题的真凶,那就是我们使用了Nginx作的请求转发,这才导致了获取不到客户端真实的IP地址,害,看看我是怎么解决的吧!

    问题原因

    客户端请求数据时走的是Nginx反向代理,默认情况下客户端的真实IP地址会被其过滤,使得SpringBoot程序无法直接获得真实的客户端IP地址,获取到的都是Nginx的IP地址。

    解决方案:

    通过更改Nginx配置文件将客户端真实的IP地址加到请求头中,这样就能正常获取到客户端的IP地址了,下面我一步步带你看看如何配置和获取。
    修改Nginx配置文件

    #这个参数设置了HTTP请求头的Host字段,host表示请求的Host头,也就是请求的域名。通过这个设置,Nginx会将请求的Host头信息传递给后端服务。
    proxy_set_header Host $host;
    #这个参数设置了HTTP请求头的X−Real−IP字段,remote_addr表示客户端的IP地址。通过这个设置,Nginx会将客户端的真实IP地址传递给后端服务
    proxy_set_header X-Real-IP $remote_addr;
    #这个参数设置了HTTP请求头的 X-Forwarded-For字段,"X-Forwarded-For"是一个标准的HTTP请求头,用于表示HTTP请求经过的代理服务器链路信息,proxy_add_x_forwarded_for表示添加额外的服务器链路信息。
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    修改后我的nginx.conf中的server如下所示

    server {
    listen 443 ssl;
    server_name xxx.com;

    ssl_certificate "ssl证书pem文件";
    ssl_certificate_key "ssl证书key文件";
    ssl_session_timeout 5m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
    ssl_prefer_server_ciphers on;

    location / {
    root 前端html文件目录;
    index index.html index.htm;
    }

    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
    root html;
    }
    # 关键在下面这个配置,上面的配置自己根据情况而定就行
    location /hello{
    proxy_pass http://127.0.0.1:8090;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
    }

    SpringBoot代码实现

    第一种方式:在代码中直接通过X-Forwarded-For获取到真实IP地址

    @Slf4j
    public class CommonUtil {
    /**
    * <p> 获取当前请求客户端的IP地址 </p>
    *
    * @param request 请求信息
    * @return ip地址
    **/
    public static String getIp(HttpServletRequest request) {
    if (request == null) {
    return null;
    }
    String unknown = "unknown";
    // 使用X-Forwarded-For就能获取到客户端真实IP地址
    String ip = request.getHeader("X-Forwarded-For");
    log.info("X-Forwarded-For:" + ip);
    if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
    ip = request.getHeader("Proxy-Client-IP");
    log.info("Proxy-Client-IP:" + ip);
    }
    if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
    ip = request.getHeader("WL-Proxy-Client-IP");
    log.info("WL-Proxy-Client-IP:" + ip);
    }
    if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
    ip = request.getHeader("HTTP_X_FORWARDED_FOR");
    log.info("HTTP_X_FORWARDED_FOR:" + ip);
    }
    if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
    ip = request.getHeader("HTTP_X_FORWARDED");
    log.info("HTTP_X_FORWARDED:" + ip);
    }
    if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
    ip = request.getHeader("HTTP_X_CLUSTER_CLIENT_IP");
    log.info("HTTP_X_CLUSTER_CLIENT_IP:" + ip);
    }
    if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
    ip = request.getHeader("HTTP_CLIENT_IP");
    log.info("HTTP_CLIENT_IP:" + ip);
    }
    if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
    ip = request.getHeader("HTTP_FORWARDED_FOR");
    log.info("HTTP_FORWARDED_FOR:" + ip);
    }
    if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
    ip = request.getHeader("HTTP_FORWARDED");
    log.info("HTTP_FORWARDED:" + ip);
    }
    if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
    ip = request.getHeader("HTTP_VIA");
    log.info("HTTP_VIA:" + ip);
    }
    if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
    ip = request.getHeader("REMOTE_ADDR");
    log.info("REMOTE_ADDR:" + ip);
    }
    if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
    ip = request.getRemoteAddr();
    log.info("getRemoteAddr:" + ip);
    }
    return ip;
    }

    第二种方式:在application.yml文件中加以下配置,直接通过request.getRemoteAddr()并可以获取到真实IP

    server:
    port: 8090
    tomcat:
    #Nginx转发 获取客户端真实IP配置
    remoteip:
    remote-ip-header: X-Real-IP
    protocol-header: X-Forwarded-Proto

    作者:BivinCode
    链接:https://juejin.cn/post/7266040474321027124
    来源:稀土掘金

    收起阅读 »

    69.9K Star,最强开源内网穿透工具:frp

    作为一名开发者,有很多场景需要用到内网穿透,比如:我们在接入一些大平台做第三方应用时,在本地开发微信公众号工具的时候需要让微信平台能否访问到本地提供的接口。除此之外,还有很多其他场景,也会用到,比如:把放在家里的NAS或服务器暴露到公网上,这样在外面的时候也可...
    继续阅读 »

    作为一名开发者,有很多场景需要用到内网穿透,比如:我们在接入一些大平台做第三方应用时,在本地开发微信公众号工具的时候需要让微信平台能否访问到本地提供的接口。除此之外,还有很多其他场景,也会用到,比如:把放在家里的NAS或服务器暴露到公网上,这样在外面的时候也可以随时随地的访问。


    说到内网传统,TJ君第一个想到的是国内最早的一款知名软件:花生壳。但是今天不是要推荐它,而是要推荐一个更牛的开源项目:frp!该项目目前已经收获了69.9 K Star,在GitHub上获得了极大的认可!


    下载安装

    frp目前已经提供了大部分操作系统的支持版本,通过这个链接:github.com/fatedier/fr… 就可以下载到适合你使用的安装。


    以Windows的包为例,解压后可以获得这些内容:


    frps是服务端程序,frpc是客户端程序。ini文件就是对应的配置文件。


    首发 blog.didispace.com/tj-opensour…,转载请注明出处


    暴露内网服务


    内网穿透的玩法有很多,这里列举一个比较常见的例子。


    比如:我要暴露一个只有自己能访问到服务器。那么可以这样配置:


    配置 frps.ini,并启动服务端 frps

    [common]
    bind_port = 7000

    在需要暴露到外网的机器上部署 frpc,配置如下:

    [common]
    server_addr = x.x.x.x
    server_port = 7000

    [secret_ssh]
    type = stcp
    # 只有 sk 一致的用户才能访问到此服务
    sk = abcdefg
    local_ip = 127.0.0.1
    local_port = 22

    在想要访问内网服务的机器上也部署 frpc,配置如下:

    [common]
    server_addr = x.x.x.x
    server_port = 7000

    [secret_ssh_visitor]
    type = stcp
    # stcp 的访问者
    role = visitor
    # 要访问的 stcp 代理的名字
    server_name = secret_ssh
    sk = abcdefg
    # 绑定本地端口用于访问 SSH 服务
    bind_addr = 127.0.0.1
    bind_port = 6000

    把frpc也都启动起来之后,通过 SSH 就可以访问内网机器了

    ssh -oPort=6000 test@127.0.0.1

    其他支持


    除了上面的玩法之外,frp还有很多玩法,比如:

    1、自定义域名访问内网的 Web 服务

    2、转发 DNS 查询请求

    3、转发 Unix 域套接

    4、对外提供简单的文件访问服务

    5、为本地 HTTP 服务启用 HTTPS

    6、点对点内网穿透


    篇幅有限,具体如何配置这里就不多说了,有需要的读者可以直接查看官方文档,均有详细的服务端客户端配置案例。


    最后,奉上相关链接:


    开源地址:github.com/fatedier/fr…
    文档地址:gofrp.org/docs/

    作者:程序猿DD
    链接:https://juejin.cn/post/7263283712224395321
    来源:稀土掘金
    收起阅读 »

    你的野心距离成功,就差一个机会

    这几天,深圳的几个达友张罗着找时间线下聚聚。 这两天在看《狂飙》。 自从开始写公众号后,业余时间不是在码字,就是在思考怎么码字。 这么热门的片子,加一起才看 6 集。 里面的张颂文,演技的确赞。 不过东哥想的是,演技这么好,怎么出道20年才火...
    继续阅读 »

    这几天,深圳的几个达友张罗着找时间线下聚聚。





    这两天在看《狂飙》。


    自从开始写公众号后,业余时间不是在码字,就是在思考怎么码字。


    这么热门的片子,加一起才看 6 集。


    里面的张颂文,演技的确赞。


    不过东哥想的是,演技这么好,怎么出道20年才火起来?


    他缺的,是什么?







    人怎么能成功?简单地说



    成功 = 能力 × 机会



    能力是修炼内功,让自己变得更专业,成为头部的专家,是自己应该搞定的事情。


    机会这事儿,就朦胧多了。


    酒香不怕巷子深,是物质稀缺时代的事情。


    整个镇子只有一家酿酒,巷子再深都有酒鬼登门。


    而当前世界已经进入到了产能过剩、需求不足的时代。


    国民总时间恒定,大家在存量的池子里杀的头破血流。


    没机会展现的才华,只能被风沙埋没。


    机会,越来越重要。


    你与成功之间,可能就差一个机会。


    比如张颂文,没机会拿到好剧本好角色,就只能是个小演员。


    46 岁,买不起房子,没存款,感觉自己好失败。


    每天都被拒绝,甚至侮辱地拒绝,让他滚蛋。


    所以我们不能只死磕自己苦练内功,坐等被赏识被发现。


    傻傻的,感动自己。


    要为自己的野心,创造机会。


    怎么创造呢?




    去机会更多的地方。 在水多的地方打井,选鱼多的地方捞鱼。


    最关键的选择,一个是城市,一个是赛道。


    做金融,就到上海和深圳,搞互联网,就去北京和深圳。


    东哥做香港保险,一个很大的优势就是所在的城市。


    从福田站到香港西九龙,高铁15分钟。


    市中心到市中心。


    赛道选择上,做和钱近的工作,做可积累的工作。


    能和人打交道,就别和机器打交道;能做销售,就别做售后。


    销售看到的都是机会和钱,售后看到的都是负面问题。




    向上链接,寻找大节点,利用好高能级的关系


    人和人能量密度不同,大节点就是个人崛起的发动机。


    达叔曾谈到过,在他写公众号的过程中,曾被欧神、医业观察的星哥、凯叔药械升职记的凯叔推荐。


    三次推荐,引来了大批流量,成就了达叔的崛起。


    在职场,就是要发现身边的强者,做深度绑定,成为强者权力结构件的一部分。


    多花心思,多花钱,努力走进领导的小圈子,成为他身边的人。


    利用他的势能,实现职业和财富的崛起。


    就像《人民的名义》里面,高育良提到汉大帮的时候说



    主观上说,我从没想过把人民赋予的权力向任何一位学生私相授受,但客观上也许私相授受了。
    做了这么多年的法学教授,教了这么多年书,学生少不了,对自己学生呢,谁都不可能没感情,用人时就难免有偏爱。






    进入不了核心圈子,就注定是个边缘人物,最后沦为炮灰。


    如果在食物、资源匮乏的时候,你不坐在桌子旁,大概率就得躺在桌子上。


    需要注意的是,链接的能级差不能太大。


    基层员工,就别总想着去链接总经理董事长。


    除非你是王思聪,他是王健林。


    县官不如现管,链接好那个直接自己升职加薪的人,最有价值。




    多和人互动,无论是线上还是线下。


    人和人之间的互动永远是这个世界的最核心的算法,剩下全是工具。


    职场中,多和实权派互动,和业务部门互动,和给公司赚钱最多的部门互动。


    最忌讳的,就是整天对着电脑研究计算模型。


    上班一天,接触的人不过办公室里这三五个,和固定的几个窗口,加一起超不过10个人。


    能深入沟通和交流的,只有个位数。


    除非你能成为公司最顶级的技术大牛,非你不可的那种,否则随时都可能被干掉。


    东哥在自己的在每一篇文章后,都会附上了自己的个人微信,也是希望能和大家链接。


    短视频时代,能潜下心来阅读文字本身就很可贵。


    尤其还是东哥这种枯燥乏味谈赚钱的文章。


    但有趣的是,很多人加微信以后一声不吱,甚至我主动询问也不回应。


    这样的链接很难有实际价值。


    只有沟通彼此的链接才会有价值,无论这个价值是信息、资源还是项目。


    没有互动,彼此就仅仅是微信系统里面的几个号码。




    多尝试,多拓展。机会是干出来的,不是想出来的。


    就像猫王和村上春树的故事。


    猫王是二十世纪收入最高的歌星,在卖唱及作明星之前是一位货车司机,每月的收入只数百美元。


    工余之暇,他去试唱,被唱片公司相中,然后一举成名。


    每年的收入以千万美元计。


    村上春树则是在看棒球比赛的时候,突然想要写本小说。


    比赛一结束,他就立马赶到文具店买了笔和纸,开始创作他的第一本小说《且听风吟》。


    人生有无数种可能。当前的状态,不代表永远。


    状态之外,要不断向外拓展,比如猫王的试镜,村上春树的小说。


    只有不断地试,才会发现身上其他可能。


    否则就只能,守着安静的沙漠 等待花开,看着别人的快乐默默感慨。


    尤其自媒体,轻资产,风险可控,是时代给予我们的拓展机会。


    多想想,自己有什么东西,可以不断打磨放大?


    写公众号这几个月,和各路大 V 沟通,大开眼界。


    医生讲医疗,房 V 讲房产都是常规操作。


    搞心理学的做星座内容,看桃花运、分析财运,居然也有不少客户。


    擅长做饭的可以做美食博主,身材好的可以做服装博主。


    你擅长的是什么,能打出什么机会?




    用无限游戏的思维方式,多推销自己。


    无论做什么,总有一双眼睛在背后默默看着你。


    合作的过程就是展现自己的机会。


    工作中做汇报做提案,与同事沟通方案,最高任务是什么?


    有人会说是把事情搞定,所以应该对事不对人。


    对项目推进有利的,就要据理力争。


    实际上,这样的工作是低效的,有时候甚至会起反作用。


    同样的事情,不同的人沟通,常会有完全相反的结果。


    对事不对人,现实中不存在。


    理念和现实冲突时,错的永远是理念,不会是现实


    沟通的最高任务,不是推销自己对项目的主张,而是推销自己这个人


    从这个角度想,我们就不会局限于一城一地之得失,一朝一夕之荣辱。


    而会站在一个更高层面,整个职业生涯的高度,来看待当下的问题。


    项目成败的因素有很多,不一定是我们可以控制的。


    但项目可以失败,人不可以失败


    每一次沟通,都要展现自己足够靠谱,足够专业,是更长远合作的好伙伴。


    所以要利用好每一次沟通,甚至是每一次刁难。


    都是在给自己种善因。




    资源也好,技术也罢,多分享,多展现。


    不要藏着掖着,害怕教会徒弟饿死师傅。


    太阳下面没有新鲜事,你能找到,别人也能找到。


    真正的强者,你打压不了。


    与其彼此竞争拼个头破血流,不如投资强者。


    在他发展壮大的必然道路上,助他一臂之力。


    也助以后的自己一臂之力。


    你能成就多少强者,就有多大能量。


    就像春秋时期齐桓公,想拜鲍叔牙为相,同时还要宰了对手公子纠的谋士管仲。


    谁知鲍叔牙对他说:“我的才能只能让齐国平安,如果您要称霸天下,一定得拜管仲为相。”


    齐桓公从之,尽释前嫌,拜管仲为国相,终成王霸之业,鲍叔牙也成就了自己千古美名。




    结合多动手和多分享的思路,打造自己的产品。


    就像伟大的程序员 Linus 说的



    Talk is cheap. Show me the code.
    被扯没用的,给我看代码。



    产品是创作能力的背书,是自控能力的表现,是你最大的筹码。


    自己的作品,可以理解为一种社交货币。


    打磨作品就是往里钱,在将来的某个时刻,可以兑换它。


    陌生人介绍的时候,就可以直接把作品丢出来。


    有了作品,就有了各种合作的可能。


    东哥这么小一个号,都有人过来谈合作,有时会一些意外的机会。




    最后一点,给生活增加一些随机性。


    平时晚上在家读书写作充电。下班时,朋友邀请参加一个陌生的聚会,去不去?


    马上出发。


    这就是生活中的随机性,说不定在聚会过程中会有新的收获。


    一个漂亮的妞做女朋友,或一个合作的机会赚点钱。


    这就是《黑天鹅》里面提到的正面黑天鹅


    努力把自己暴露在可能发生正向意外的环境里。


    用时间上微小的损失,换正面黑天鹅出现的可能,进而突破路径依赖。


    就像这几天,深圳的几个达友张罗着找时间线下聚聚。


    这群人里面有做医疗的,做芯片贸易的,做生物的,还有东哥是卖保险的。


    彼此之间可能交流什么,完全未知。


    但也就是因为这种未知,才能让自己突破日常的局限,也就更值钱。




    供给过剩的时代,无数人在存量的池子里杀得头破血流。


    给自己多创造一些机会,杀出来的可能就大一些

    • 去鱼多的地方捞鱼,挤到机会多的地方;
    • 向上链接,发现强者,进入到强者的核心圈子;
    • 多和人互动,不要沉迷于技术;
    • 基于自身优势多对外拓展,寻找机会;
    • 无限游戏思维,办事儿的时候笑面挑战,推销自己;
    • 多分享,尤其挖掘并投资潜力股;
    • 打造自己的作品,积累社交货币;
    • 给生活增加一些随机性;


    避免像张颂文,满腹才华,却走了漫长的二十年。


    你距离成功,也许只缺一个机会。


    那就造一个出来。



    作者:jetorz
    来源:mdnice.com/writing/39186f5978a84668b24442c684d01fa3
    收起阅读 »