注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

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
    收起阅读 »

    孩子是双眼皮还是单眼皮?来自贝叶斯算法的推测

    问题描述 最近家里有了宝宝,孩子他妈很希望孩子早日长出双眼皮,并因为他至今是单眼皮而有些担心。虽然我小时候也是单眼皮,后来才显现出双眼皮,但不排除孩子长大后仍是单眼皮的概率。为此我感到需要计算一下孩子是单眼皮基因的概率。 我家的情况是这样,宝爸宝妈、爷爷奶...
    继续阅读 »

    问题描述


    最近家里有了宝宝,孩子他妈很希望孩子早日长出双眼皮,并因为他至今是单眼皮而有些担心。虽然我小时候也是单眼皮,后来才显现出双眼皮,但不排除孩子长大后仍是单眼皮的概率。为此我感到需要计算一下孩子是单眼皮基因的概率。


    我家的情况是这样,宝爸宝妈、爷爷奶奶、姥姥姥爷都是双眼皮。


    查了一下资料,双眼皮是显性基因,因此除非宝爸宝妈都是杂合性基因且都贡献单眼皮片段,孩子才能是单眼皮。


    这里做一下假设,全部人群中有3/4是双眼皮,双眼皮人群中纯合基因有1/2。即



    其中S表示双眼皮,D表示单眼皮,C表示纯合基因,Z表示杂合基因。


    祖父辈人基因类型的后验概率


    父母双方的情况是对等的,因此只挑选其中一方进行计算。以父亲为例,爷爷奶奶可能的基因类型组合有:CC,CZ和ZZ。先验概率为:



    已知父亲是双眼皮,则爷爷奶奶是CC组合的后验概率为:



    类似地可以算出



    进行归一化后得到:



    这里之所以要进行归一化,是因为在计算过程中对概念进行了替换,我们利用了爷爷奶奶都是双眼皮的信息。


    父母基因类型的概率


    仍然以父亲为对象进行计算,其是纯合基因的概率为:



    是杂合基因的概率为:



    剩下1/20的概率是表现为单眼皮的概率,需要排除掉。也就是说,在观察到爷爷奶奶父亲都是双眼皮的情况下,父亲是纯合基因的概率为






































    ,是杂合基因的概率为







































    孩子双眼皮的概率


    综合以上,孩子是双眼皮的概率为:



    可见这个概率是很高的。


    压力测试


    在之前的计算里面,由于没有一般人群的统计数据,我们假设全部人群中有3/4是双眼皮,双眼皮人群中纯合基因有1/2。这里我们换一组数据,假设全部人群中有1/4是双眼皮,双眼皮人群中纯合基因有1/4,看看这样会对结果造成多大影响。


    这种情况下



    父母任意一方是纯合基因的概率为







































    ,是杂合基因的概率为









































    最终得到孩子是双眼皮的概率是



    可见概率依然非常高。“六个钱包”都是双眼皮是一个非常可靠的信号。


    作者:Kelly1024
    来源:mdnice.com/writing/f71210a4999f4bd2a674f9bead00551c
    收起阅读 »

    原来,我们的代码就是这样被污染的...

    最近 CR 了这样一段代码: 我们团队的变量命名规范是小写驼峰,但是这里可以看到,一个 http api 接口请求的工具函数的入参却是下划线。这是一个内部项目,前后端都是我们团队开发的。这个项目的代码随处都能看到这样的不符合规范的痕迹,而且屡禁不止。我不禁在...
    继续阅读 »

    最近 CR 了这样一段代码:


    插图1.png


    我们团队的变量命名规范是小写驼峰,但是这里可以看到,一个 http api 接口请求的工具函数的入参却是下划线。这是一个内部项目,前后端都是我们团队开发的。这个项目的代码随处都能看到这样的不符合规范的痕迹,而且屡禁不止。我不禁在想,为什么会这样?


    先探究一下这个问题的表面原因,让我们从底层开始:



    • DB 表结构按照团队规范,字段名是下划线分隔的

    • node 服务定义 entity 的时候,直接复用 DB 表结构字段名

    • service 模块在查询 DB 之后,直接回吐结果,所以 service 类返回的数据结构的变量名也是下划线的

    • node api 的 controller 通过调用 service 模块的函数,获取数据处理之后,也直接回吐下划线的变量名的数据结构

    • 因为 node api 的接口协议是下划线的,所以 web 页面请求 http api 的出入参也是下划线的,如上图所示

    • 因为 http api 返回的结果是下划线的,然后页面逻辑直接使用,最后,页面逻辑也出现了大量下划线


    这是一段挺长的链路,只要我们在后面的任意一个环节处理一下变量名的转换,都能避免这个问题,但是并没有。


    插图2.png


    为什么会这样?稍微探究一下这里的深层原因,也是挺有意思的。


    首先,项目启动时没有严格把控代码质量。这里的原因有很多,比如项目工期紧、主要以完成功能为主、内部项目不需要要求这么严格、项目启动时是直接 copy 另外一个项目的,那个项目也是这样写的等等。但是,这些统统都是借口!我是要负主要责任的。


    其次,没有开发同学想过要去优化这里的代码。参与这个项目的同学并不是不知道如何去解决这个问题,但就是没人想要去解决这个问题。大家更多的是选择“入乡随俗”,别人这么写,我也这么写吧。


    最后,Code Review 没有严抓。问题都是越早处理成本越小,如果在早期我们就开始严抓 Code Review 的话,说不定就能及时改善这个问题了。


    这个案例是一个非常真实的破窗效应案例,这里面有不少地方值得我们深思的。


    首先,它会污染整个项目。如果我们在项目的一开始就没有把控好代码质量,那我们的项目代码很快就会被污染。最开始的开发同学可能知道历史原因,自然不会觉得不自然。后续加入的维护者看到项目代码是这样子的,就会误以为这个项目的代码规范就是这样子的,于是也“入乡随俗”,最终整个项目的代码就被污染,破烂不堪。


    其次,它会污染依赖的项目。比如这里就是 node 项目先被污染,对外提供的 api 也受到污染,然后调用这些 api 的 web 项目也被污染了。很多时候,api 的接口协议都是由后端开发来定,如果碰到一些缺乏经验的后端开发,前端同学会收到一些很奇怪的接口协议,比如字段命名规范不统一,冗余字段,设计不合理等等,由于前端的话语权较弱的缘故,很可能会被动接受,如果处理不当,前端项目的代码就会受到污染了。


    最后,它会污染整个开发团队!初创的开发同学就不说了;后续的维护者看到代码是这样子的,不是错误认知这个项目的代码规范,就是错误认知团队的代码规范;更为糟糕的是,阅读项目源码的其他同学也会受到污染:“哦,原来别人也是这样写代码的”。噩梦由此而生...


    插图3.png


    破窗就像病毒一样,快速并疯狂地污染整个项目代码,然后传染给开发团队,最后扩散到其他团队。这个病毒传染的范围很广,速度很快,一不留神,整个团队就会沦陷。那我们应该怎样医治呢? 主要有 3 个方向:


    首先,增强个人免疫力。形成个人的编码风格,然后坚持它,这是治本的良方。良好的编码风格是不会损坏个人的编码效率的,反而会有助于提升个人的研发效率,主要体现在以下几个方面:



    • 减少低级错误

    • 提升代码的可阅读性,提升代码的可维护性,从而提升团队协作效率

    • 形成“肌肉记忆”,提升编码效率

    • 避免返工,主要体现在糟糕的设计问题和编码风格冲突问题


    其次,做好防护,避免传播。比如我们这个简单的案例,在整个链路的任何一个环节,都能轻松处理这个问题,这样就不会传播到后面的环节了。很多人碰到一些历史破窗代码时,可能会觉得修复这些破窗成本太大了,那就不要去修复,只需要保证自己写的新代码是良好的,也是一个不错的方案。已经中毒的人我们没有能力医治的时候,起码,我们可以认真做好防护,避免病毒进一步传播!


    最后,对症下药医治。只有千日做贼,那有千日防贼,我们总是需要想办法医治这个病,而治病的药方就是重构。这里就不深入讲了,重构也是个大学问,只需要知道,越早重构,成本越低,比如在 Code Review 的时候就要严抓这些问题,并跟踪修复情况。


    不知道,你的“免疫力”修炼得怎么样了?



    【讨论问题】


    除了这里的案例,还有很多其他类型的破窗,你都碰到了哪些?


    欢迎在评论区分享你的想法,一起讨论。


    作者:潜龙在渊灬
    来源:juejin.cn/post/7252888158828642360
    收起阅读 »

    为什么大部分人做不了架构师?

    腾小云导读成为架构师,是许多程序员的职业梦想。然而其中只有少数有着丰厚编码积累、超强自驱力和独到思维的程序员才能最终成为架构师。其实,日常工作中小到某个功能的开发,大到整个业务系统的设计,都可以看到架构设计的影子。《从0开始学架构》一书是颇受程序员欢迎的架构设...
    继续阅读 »

    腾小云导读

    成为架构师,是许多程序员的职业梦想。然而其中只有少数有着丰厚编码积累、超强自驱力和独到思维的程序员才能最终成为架构师。其实,日常工作中小到某个功能的开发,大到整个业务系统的设计,都可以看到架构设计的影子。《从0开始学架构》一书是颇受程序员欢迎的架构设计入门教程。接下来本文作者将提取该书籍之精髓,结合自身经验分享架构设计常见方法以及高可用、高性能、可扩散架构模式的实现思路,将架构设计思维“为我所用”、提升日常研效。希望对你有帮助~

    目录

    1 基本概念与设计方法

    2 高性能架构模式

    2.1 存储高性能

    2.2 计算高性能

    3 高可用架构模式

    3.1 理论方法

    3.2 存储高可用

    3.3 计算高可用

    4 可扩展架构模式

    5 总结

    之前本栏目《腾讯专家10年沉淀:后海量时代的架构设计》、《工作十年,在腾讯沉淀的高可用系统架构设计经验》两篇文章中,两位腾讯的开发者结合自身经历,分享了架构设计的实践经验。而本期,本栏目特邀腾讯云对《从0开始学架构》一书提取精髓,并结合亲身经验做分享。

    01、基本概念与设计方法

    在讲解架构思想之前,我们先统一介绍一下基本概念的含义,避免每个人对系统、框架、架构这些名词的理解不一致导致的误解。下面是《从0开始学架构》作者对每个名词的定义。其作用域仅限本文范畴,不用纠结其在其他上下文中的意义。

    系统:系统泛指由一群有关联的个体组成,根据某种规则运作,能完成个别元件不能单独完成的工作的群体。 子系统:子系统也是由一群有关联的个体所组成的系统,多半会是更大系统中的一部分。模块:从业务逻辑的角度来拆分系统后,得到的单元就是“模块”。划分模块的主要目的是职责分离。组件:从物理部署的角度来拆分系统后,得到的单元就是“组件”。划分组件的主要目的是单元复用。框架:是一整套开发规范,是提供基础功能的产品。架构:关注的是结构,是某一套开发规范下的具体落地方案,包括各个模块之间的组合关系以及它们协同起来完成功能的运作规则。
    由以上定义可见,所谓架构,是为了解决软件系统的某个复杂度带来的具体问题,将模块和组件以某种方式有机组合,基于某个具体的框架实现后的一种落地方案。

    而讨论架构时,往往只讨论到系统与子系统这个顶层的架构。

    可见,要进行架构选型,首先应该知道自己要解决的业务和系统复杂点在哪里,是作为秒杀系统有瞬间高并发,还是作为金融科技系统有极高的数据一致性和可用性要求等。

    一般来说,系统的复杂度来源有以下几个方面:

    高性能

    如果业务的访问频率或实时性要求较高,则会对系统提出高性能的要求。

    如果是单机系统,需要利用多进程、多线程技术。

    如果是集群系统,则还涉及任务拆分、分配与调度,多机器状态管理,机器间通信,当单机性能达到瓶颈后,即使继续加机器也无法继续提升性能,还是要针对单个子任务进行性能提升。

    高可用

    如果业务的可用性要求较高,也会带来高可用方面的复杂度。高可用又分为计算高可用和存储高可用。

    针对计算高可用,可以采用主备(冷备、温备、热备)、多主的方式来冗余计算能力,但会增加成本、可维护性方面的复杂度。

    针对存储高可用,同样是增加机器来冗余,但这也会带来多机器导致的数据不一致问题,如遇到延迟、中断、故障等情况。难点在于怎么减少数据不一致对业务的影响。

    既然主要解决思路是增加机器来做冗余,那么就涉及到了状态决策的问题。即如果判断当前主机的状态是正常还是异常,以及异常了要如何采取行动(比如切换哪台做主机)。

    对主机状态的判断,多采用机器信息采集或请求响应情况分析等手段,但又会产生采集信息这一条通信链路本身是否正常的问题,下文会具体展开讨论。事实上,状态决策本质上不可能做到完全正确。

    而对于决策方式,有以下几种方式:

    独裁式:存在一个独立的决策主体来收集信息并决定主机,这样的策略不会混乱,但这个主体本身存在单点问题。 协商式:两台备机通过事先指定的规则来协商决策出主机,规则虽然简单方便,但是如果两台备机之间的协商链路中断了,决策起来就会很困难,比如有网络延迟且机器未故障、网络中断且机器未故障、网络中断其机器已故障,多种情况需要处理。民主式:如果有多台备机,可以使用选举算法来投票出主机,比如 Paxos 就是一种选举算法,这种算法大多数都采取多数取胜的策略,算法本身较为复杂,且如果像协商式一样出现连接中断,就会脑裂,不同部分会各自决策出不同结果,需要规避。

    可扩展性

    众所周知在互联网行业只有变化才是永远不变的,而开发一个系统基本都不是一蹴而就的,那应该如何为系统的未来可能性进行设计来保持可扩展性呢?

    这里首先要明确的一个观点就是,在做系统设计时,既不可能完全不考虑可扩展性,也不可能每个设计点都考虑可扩展性,前者很明显,后者则是为了避免舍本逐末,为了扩展而扩展,实际上可能会为不存在的预测花费过多的精力。

    那么怎么考虑系统的未来可能性从而做出相应的可扩展性设计呢?这里作者给出了一个方法:只预测两年内可能的变化,不要试图预测五年乃至十年的变化。因为对于变化快的行业来说,预测两年已经足够远了,再多就可能计划赶不上变化。而对变化慢的行业,则预测的意义更是不大。

    要应对变化,主要是将变与不变分隔开来。

    这里可以针对业务,提炼变化层和稳定层,通过变化层将变化隔离。比如通过一个 DAO 服务来对接各种变化的存储载体,但是上层稳定的逻辑不用知晓当前采用何种存储,只需按照固定的接口访问 DAO 即可获取数据。

    也可以将一些实现细节剥离开来,提炼出抽象层,仅在实现层去封装变化。比如面对运营上经常变化的业务规则,可以提炼出一个规则引擎来实现核心的抽象逻辑,而具体的规则实现则可以按需增加。

    如果是面对一个旧系统的维护,接到了新的重复性需求,而旧系统并不支持较好的可扩展性,这时是否需要花费时间精力去重构呢?作者也提出了《重构》一书中提到的原则:事不过三,三则重构。

    简而言之,不要一开始就考虑复杂的做法去满足可扩展性,而是等到第三次遇到类似的实现时再来重构,重构的时候采取上述说的隔离或者封装的方案。

    这一原则对新系统开发也是适用的。总而言之就是,不要为难以预测的未来去过度设计,为明确的未来保留适量的可扩展性即可。

    低成本

    上面说的高性能、高可用都需要增加机器,带来的是成本的增加,而很多时候研发的预算是有限的。换句话说,低成本往往并不是架构设计的首要目标,而是设计架构时的约束限制。

    那如何在有限的成本下满足复杂性要求呢?往往只有“创新”才能达到低成本的目标。举几个例子:

    NoSQL 的出现是为解决关系型数据库应对高并发的问题。 全文搜索引擎的出现是为解决数据库 like 搜索效率的问题。Hadoop 的出现是为解决文件系统无法应对海量数据存储与计算的问题。Facebook 的 HipHop PHP 和 HHVM 的出现是为解决 PHP 运行低效问题。新浪微博引入 SSD Cache 做 L2 缓存是为解决 Redis 高成本、容量小、穿透 DB 的问题。Linkedin 引入 Kafka 是为解决海量事件问题。
    上述案例都是为了在不显著增加成本的前提下,实现系统的目标。

    这里还要说明的是,创造新技术的复杂度本身就是很高的,因此一般中小公司基本都是靠引入现有的成熟新技术来达到低成本的目标;而大公司才更有可能自己去创造新的技术来达到低成本的目标,因为大公司才有足够的资源、技术和时间去创造新技术。

    安全

    安全是一个研发人员很熟悉的目标,从整体来说,安全包含两方面:功能安全和架构安全。

    功能安全是为了“防小偷”,即避免系统因安全漏洞而被窃取数据,如 SQL 注入。常见的安全漏洞已经有很多框架支持,所以更建议利用现有框架的安全能力,来避免重复开发,也避免因自身考虑不够全面而遗漏。在此基础上,仍需持续攻防来完善自身的安全。

    架构安全是为了“防强盗”,即避免系统被暴力攻击导致系统故障,比如 DDOS 攻击。这里一方面只能通过防火墙集运营商或云服务商的大带宽和流量清洗的能力进行防范,另一方面也需要做好攻击发现与干预、恢复的能力。

    规模

    架构师在宣讲时往往会先说自己任职和设计过的大型公司的架构,这是因为当系统的规模达到一定程度后,复杂度会发生质的变化,即所谓量变引起质变。

    这个量,体现在访问量、功能数量及数据量上。

    访问量映射到对高性能的要求。功能数量需要视具体业务会带来不同的复杂度。而数据量带来的收集、加工、存储、分析方面的挑战,现有的方案基本都是基于 Google 的三篇大数据论文的理论:

    Google File System 是大数据文件存储的技术理论。Google Bigtable 是列式数据存储的技术理论。Google MapReduce 是大数据运算的技术理论。
    经过上面的分析可以看到,复杂度来源很多,想要一一应对,似乎会得到一个复杂无比的架构,但对于架构设计来说,其实刚开始设计时越简单越好,只要能解决问题,就可以从简单开始再慢慢去演化,对应的是下面三条原则:

    合适原则:不需要一开始就挑选业界领先的架构,它也许优秀,但可能不那么适合自己,比如有很多目前用不到的能力或者大大超出诉求从而增加很多成本。其实更需要考虑的是合理地将资源整合在一起发挥出最大功效,并能够快速落地。简单原则:有时候为了显示出自身的能力,往往会在一开始就将系统设计得非常复杂,复杂可能代表着先进,但更可能代表着“问题”,组件越多,就越可能出故障,越可能影响关联着的组件,定位问题也更加困难。其实只要能够解决诉求即可。演化原则:不要妄想一步到位,没有人可以准确预测未来所有发展,软件不像建筑,变化才是主题。架构的设计应该先满足业务需求,适当的预留扩展性,然后在未来的业务发展中再不断地迭代,保留有限的设计,修复缺陷,改正错误,去除无用部分。这也是重构、重写的价值所在。

    即使是 QQ、淘宝这种如今已经非常复杂的系统,刚开始时也只是一个简单的系统,甚至淘宝都是直接买来的系统,随着业务发展也只是先加服务器、引入一些组件解决性能问题,直到达到瓶颈才去重构重写,重新在新的复杂度要求下设计新的架构。

    明确了设计原则后,当面对一个具体的业务,我们可以按照如下步骤进行架构设计:

    识别复杂度:无论是新设计一个系统还是接手一个混乱的系统,第一步都是先将主要的复杂度问题列出来,然后根据业务、技术、团队等综合情况进行排序,优先解决当前面临的最主要的复杂度问题。复杂度的主要来源上文已经说过,可以按照经验或者排查法进行分析。方案对比:先看看业界是否有类似的业务,了解他们是怎么解决问题的,然后提出3~5个备选方案,不要只考虑做一个最优秀的方案,一个人的认知范围常常是有限的,逼自己多思考几个方案可以有效规避因为思维狭隘导致的局限性,当然也不要过多,不用给出非常详细的方案,太消耗精力。备选方案的差异要比较明显,才有扩宽思路和对比的价值。设计详细方案:当多个方案对比得出最终选择后,就可以对目标方案进行详细的设计,关键细节需要比较深入,如果方案本身很复杂,也可以采取分步骤、分阶段、分系统的实现方式来降低实现复杂度。当方案非常庞大的时候,可以汇集一个团队的智慧和经验来共同设计,防止因架构师的思维盲区导致问题。

    02、高性能架构模式

    2.1 存储高性能

    互联网业务大多是数据密集型的业务,其对性能的压力也常常来自于海量用户对数据的高频读写压力上。因此解决高性能问题,首先要解决数据读写的存储高性能问题。

    读写分离

    在大多数业务中,用户查询和修改数据的频率是不同的,甚至是差别很大的,大部分情况下都是读多写少的,因此可以将对数据的读和写操作分开对待,对压力更大的读操作提供额外的机器分担压力,这就是读写分离。

    读写分离的基本实现是搭建数据库的主从集群,根据需要提供一主一从或一主多从。

    注意是主从不是主备,从和备的差别在于从机是要干活的。

    通常在读多写少的情况下,主机负责读写操作,从机只负责读操作,负责帮主机分担读操作的压力。而数据会通过复制机制(如全同步、半同步、异步)同步到从机,每台服务器都有所有业务数据。

    既然有数据的同步,就一定存在复制延迟导致的从机数据不一致问题,针对这个问题有几种常见的解法,如:

    写操作后同一用户一段时间内的读操作都发给主机,避免数据还没同步到从机,但这个逻辑容易遗漏。读从机失败后再读一次主机,该方法只能解决新数据未同步的问题,无法解决旧数据修改的问题(不会读取失败),且二次读取主机会给主机带来负担,容易被针对性攻击。关键读写操作全部走主机,从机仅负责非关键链路的读,该方法是基于保障关键业务的思路。
    除了数据同步的问题之外,只要涉及主从机同时支持业务访问的,就一定需要制定请求分配的机制。上面说的几个问题解法也涉及了一些分配机制的细节。具体到分配机制的实现来说,有两种思路:

    程序代码封装:实现简单,可对业务定制化,但每个语言都要自己实现一次,且很难做到同步修改,因此适合小团队。中间件封装:独立出一套系统管理读写的分配,对业务透明,兼容 SQL 协议,业务服务器就无需做额外修改适配。需要支持多语言、完整的 SQL 语法,涉及很多细节,容易出 BUG,且本身是个单点,需要特别保障性能和可用性,因此适合大公司。
    分库分表

    除了高频访问的压力,当数据量大了以后,也会带来数据库存储方面的压力。此时就需要考虑分库分表的问题。分库分表既可以缓解访问的压力,也可以分散存储的压力。

    先说分库,所谓分库,就是指业务按照功能、模块、领域等不同,将数据分散存储到不同的数据库实例中。

    比如原本是一个 MySQL 数据库实例,在库中按照不同业务建了多张表,大体可以归类为 A、B 两个领域的数据。现在新建一个库,将原库中 A 领域的数据迁移到新的库中存储,还是按需建表,而 B 领域的数据继续留在原库中。

    分库一方面可以缓解访问和存储的压力,另一方面也可以增加抗风险能力,当一个库出问题后,另一个库中的数据并不会受到影响,而且还能分开管理权限。

    但分库也会带来一些问题:原本同一个库中的不同表可以方便地进行联表查询,分库后则会变得很复杂。由于数据在不同的库中,当要操作两个库中的数据时,无法使用事务操作,一致性也变得更难以保障。而且当增加备库来保障可用性的时候,成本是成倍增加的。

    基于以上问题,初创的业务并不建议在一开始就做这种拆分,会增加很多开发时的成本和复杂度,拖慢业务的节奏。

    再说分表。所谓分表,就是将原本存储在一张表里的数据,按照不同的维度,拆分成多张表来存储。

    按照诉求与业务的特性不同,可以采用垂直分表或水平分表的方式。

    垂直分表相当于垂直地给原表切了一刀,把不同的字段拆分到不同的子表中,这样拆分后,原本访问一张表可以获取的所有字段,现在则需要访问不同的表获取。

    垂直分表适合将表中某些不常用又占了大量空间的列(字段)拆分出去,可以提升访问常用字段的性能。

    但相应的,当真的需要的字段处于不同表中时,或者要新增记录存储所有字段数据时,要操作的表变多了。

    水平分表相当于横着给原表切了一刀,那么原表中的记录会被分散存储到不同的子表中,但是每张子表的字段都是全部字段。

    水平分表适合表的量级很大以至影响访问性能的场景,何时该拆分并没有绝对的指标,一般记录数超过千万时就需要警觉了。

    不同于垂直分表依然能访问到所有记录,水平分表后无法再在一张表中访问所有数据了,因此很多查询操作会受到影响,比如 join 操作就需要多次查询后合并结果,count 操作也需要计算多表的结果后相加,如果经常用到 count 的总数,可以额外维护一个总数表去更新,但也会带来数据一致性的问题。

    值得特别提出的是范围查询,原本的一张表可以通过范围查询到的数据,分表后也需要多次查询后合并数据,如果是业务经常用到的范围查询,那建议干脆就按照这种方式来分表,这也是分表的路由方式之一:范围路由。

    所谓路由方式是指:分表后当新插入记录时,如何判断该往哪张表插入。常用的插入方式有以下三种:

    范围路由:按照时间范围、ID 范围或者其他业务常用范围字段路由。这种方式在扩充新的表时比较方便,直接加表给新范围的数据插入即可,但是数量和冷热分布可能是不均匀的。 Hash 路由:根据 Hash 运算来路由新记录插入的表,这种方式需要提前就规划好分多少张表,才能决定 Hash 运算方式。但表数量其实很难预估,导致未来需要扩充新表时很麻烦,但数据在不同表中的分布是比较均匀的。配置路由:新增一个路由表来记录数据 id 和表 id 的映射,按照自定义的方式随时修改映射规则,设计简单,扩充新表也很方便。但每次操作表都需要额外操作一次路由表,其本身也成为了单点瓶颈

    无论是垂直分表还是水平分表,单表切分为多表后,新的表即使在同一个数据库服务器中,也可能带来可观的性能提升,如果性能能够满足业务要求,可以不拆分到多台数据库服务器,毕竟分库也会引入很多复杂性的问题;如果单表拆分为多表后,单台服务器依然无法满足性能要求,那就不得不再次进行业务分库的设计了。

    NoSQL 数据库

    上面发分库分表讨论的都是关系型数据库的优化方案,但关系型数据库也有其无法规避的缺点,比如无法直接存储某种结构化的数据、扩展表结构时会锁表影响线上性能、大数据场景下 I/O 较高、全文搜索的功能比较弱等。

    基于这些缺点,也有很多新的数据库框架被创造出来,解决其某方面的问题。

    比如以 Redis 为代表的的 KV 存储,可以解决无法存储结构化数据的问题;以 MongoDB 为代表的的文档数据库可以解决扩展表结构被强 Schema 约束的问题;以 HBase 为代表的的列式数据库可以解决大数据场景下的 I/O 问题;以 ES 为代表的的全文搜索引擎可以解决全文检索效率的问题等。

    这些数据库统称为 NoSQL 数据库,但 NoSQL 并不是全都不能写 SQL,而是 Not Only SQL 的意思。

    NoSQL 数据库除了聚焦于解决某方面的问题以外也会有其自身的缺点,比如 Redis 没有支持完整的 ACID 事务、列式存储在更新一条记录的多字段时性能较差等。因此并不是说使用了 NoSQL 就能一劳永逸,更多的是按需取用,解决业务面临的问题。

    关于 NoSQL 的更多了解,推荐大家可以看看《NoSQL 精粹》这本书。

    缓存

    如果 NoSQL 也解决不了业务的高性能诉求,那么或许你需要加点缓存

    缓存最直接的概念就是把常用的数据存在内存中,当业务请求来查询的时候直接从内存中拿出来,不用重新去数据库中按条件查询,也就省去了大量的磁盘 IO 时间。

    一般来说缓存都是通过 Key-Value 的方式存储在内存中,根据存储的位置,分为单机缓存和集中式缓存。单机缓存就是存在自身服务器所在的机器上,那么势必会有不同机器数据可能不一致,或者重复缓存的问题,要解决可以使用查询内容做路由来保障同一记录始终到同一台机器上查询缓存。集中式缓存则是所有服务器都去一个地方查缓存,会增加一些调用时间。

    缓存可以提升性能是很好理解的,但缓存同样有着它的问题需要应对或规避。数据时效性是最容易想到的问题,但也可以靠同时更新缓存的策略来保障数据的时效性,除此之外还有其他几个常见的问题。

    如果某条数据不存在,缓存中势必查不到对应的 KEY,从而就会请求数据库确认是否有新增加这条数据,如果始终没有这条数据,而客户端又反复频繁地查询这条数据,就会变相地对数据库造成很大的压力,换句话说,缓存失去了保护作用,请求穿透到了数据库,这称为缓存穿透。

    应对缓存穿透,最好的手段就是把“空值”这一情况也缓存下来,当客户端下次再查询时,发现缓存中说明了该数据是空值,则不会再问询数据库。但也要注意如果真的有对应数据写入了数据库,应当能及时清除”空值“缓存。

    为了保障缓存的数据及时更新,常常都会根据业务特性设置一个缓存过期时间,在缓存过期后,到再次生成期间,如果出现大量的查询,会导致请求都传递到数据库,而且会多次重复生成缓存,甚至可能拖垮整个系统,这就叫缓存雪崩,和缓存穿透的区别在于,穿透是面对空值的情况,而雪崩是由于缓存重新生成的间隔期大量请求产生的连锁效应。

    既然是缓存更新时重复生成所导致的问题,那么一种解法就是在缓存重新生成前给这个 KEY 加锁,加锁期间出现的请求都等待或返回默认值,而不去都尝试重新生成缓存。

    另一种方法是干脆不要由客户端请求来触发缓存更新,而是由后台脚本统一更新,同样可以规避重复请求导致的重复生成。但是这就失去了只缓存热点数据的能力,如果缓存因空间问题被清除了,也会因为后台没及时更新导致查不到缓存数据,这就会要求更复杂的后台更新策略,比如主动查询缓存有效性、缓存被删后通知后台主动更新等。

    虽说在有限的内存空间内最好缓存热点数据,但如果数据过热,比如微博的超级热搜,也会导致缓存服务器压力过大而崩溃,称之为缓存热点问题。

    可以复制多份缓存副本,来分散缓存服务器的单机压力,毕竟堆机器是最简单有效。此处也要注意,多个缓存副本不要设置相同的缓存过期时间,否则多处缓存同时过期,并同时更新,也容易引起缓存雪崩,应该设置一个时间范围内的随机值来更新缓存。

    2.2 计算高性能

    讲完存储高性能,再讲计算高性能,计算性能的优化可以先从单机性能优化开始,多进程、多线程、IO 多路复用、异步 IO 等都存在很多可以优化的地方,但基本系统或框架已经提供了基本的优化能力,只需使用即可。

    负载均衡

    如果单机的性能优化已经到了瓶颈,无法应对业务的增长,就会开始增加服务器,构建集群。对于计算来说,每一台服务器接到同样的输入,都应该返回同样的输出,当服务器从单台变成多台之后,就会面临请求来了要由哪一台服务器处理的问题,我们当然希望由当前比较空闲的服务器去处理新的请求,这里对请求任务的处理分配问题,就叫负载均衡。

    负载均衡的策略,从分类上来说,可以分为三类:

    DNS 负载均衡:通过 DNS 解析,来实现地理级别的均衡,其成本低,分配策略很简单,可以就近访问来提升访问速度,但 DNS 的缓存时间长,由于更新不及时所以无法快速调整,且控制权在各域名商下,且无法根据后端服务器的状态来决定分配策略。 硬件负载均衡:直接通过硬件设备来实现负载均衡,类似路由器路由,功能和性能都很强大,可以做到百万并发,也很稳定,支持安全防护能力,但是同样无法根据后端服务器状态进行策略调整,且价格昂贵。软件负载均衡:通过软件逻辑实现,比如 nginx,比较灵活,成本低,但是性能一般,功能也不如硬件强大。

    一般来说,DNS 负载均衡用于实现地理级别的负载均衡;硬件负载均衡用于实现集群级别的负载均衡;软件负载均衡用于实现机器级别的负载均衡。

    所以部署起来可以按照这三层去部署,第一层通过 DNS 将请求分发到北京、上海、深圳的机房;第二层通过硬件负载均衡将请求分发到当地三个集群中的一个;第三层通过软件策略将请求分发到具体的某台服务器去响应业务。

    就负载均衡算法来说,多是我们很熟悉的算法,如轮询、加权轮询、负载最低优先、性能最优优先、Hash 分配等,各有特点,按需采用即可。

    03、高可用架构模式

    3.1 理论方式

    CAP 与 BASE
    在说高可用之前,先来说说 CAP 理论,即:

    在一个分布式系统(指互相连接并共享数据的节点的集合)中,当涉及读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个,另外一个必须被牺牲。

    大家可能都知道 CAP 定理是什么,但大家可能不知道,CAP 定理的作者(Seth Gilbert & Nancy Lynch)其实并没有详细解释 CAP 三个单词的具体含义,目前大家熟悉的解释其实是另一个人(Robert Greiner)给出的。而且他还给出了两版有所差异的解释。

    书中第二版解释算是对第一版解释的加强,他要加强的点主要是:

    CAP 描述的分布式系统,是互相连结并共享数据的节点的集合。因为其实并不是所有的分布式系统都会互连和共享数据。CAP 理论是在涉及读写操作的场景下的理论,而不是分布式系统的所有功能。一致性只需要保障客户端读操作能读到最新的写操作结果,并不要求时时刻刻分布式系统的数据都是一致的,这是不现实的,只要保障客户读到的一致即可。可用性要求非故障的节点在合理的时间内能返回合理的响应,所谓合理是指非错误、非超时,即使数据不是最新的数据,也是合理的“旧数据”,是符合可用性的。分区容错性要求网络分区后系统能继续履行职责,不仅仅要求系统不宕机,还要求能发挥作用,能处理业务逻辑。比如接口直接返回错误其实也代表系统在运行,但却没有履行职责。
    在分布式系统下,P(分区容忍)是必须选择的,否则当分区后系统无法履行职责时,为了保障 C(一致性),就要拒绝写入数据,也就是不可用了

    在此基础上,其实我们能选择的只有 C+P 或者 A+P,根据业务特性来选择要优先保障一致性还是可用性。

    在选择保障策略时,有几个需要注意的点:

    CAP 关注的其实是数据的粒度,而不是整个系统的粒度,因此对于系统内的不同数据(对应不同子业务),其实是可以按照业务特性采取不同的 CAP 策略的。CAP 实际忽略了网络延迟,也就是允许数据复制过程中的短时间不一致,如果某些业务比如金融业务无法容忍这一点,那就只能对单个对象做单点写入,其他节点备份,无法做多点写入。但对于不同的对象,其实可以分库来实现分布式。当没有发生分区现象时,也就是不用考虑 P 时,上述限制就不存在,此时应该考虑如何保障 CA。当发生分区后,牺牲 CAP 的其中一个并不代表什么都不用做,而是应该为分区后的恢复 CA 做准备,比如记录分区期间的日志以供恢复时使用。

    随 CAP 的一个退而求其次,也更现实的追求,是 BASE 理论,即基本可用,保障核心业务的可用性;软状态,允许系统存在数据不一致的中间状态;最终一致性,一段时间后系统应该达到一致。

    FMEA 分析法

    要保障高可用,我们该怎么下手呢?俗话说知己知彼才能有的放矢,因此做高可用的前提是了解系统存在怎样的风险,并且还要识别出风险的优先级,先治理更可能发生的、影响更大的风险。说得简单,到底怎么做?业界其实已经提供了排查系统风险的基本方法论,即 FMEA(Failure mode and effects analysis)——故障模式与影响分析。

    FMEA 的基本思路是,面对初始的架构设计图,考虑假设其中某个部件发生故障,对系统会造成什么影响,进而判断架构是否需要优化。

    具体来说,需要画一张表,按照如下步骤逐个列出:

    功能点:列出业务流程中的每个功能点。故障模式:量化描述该功能可能发生怎样的故障,比如 MySQL 响应时间超过3秒。故障影响:量化描述该每个故障可能导致的影响,但不用非常精确,比如20%用户无法登录。严重程度:设定标准,给每个影响的严重程度打分。故障原因:对于每个故障,考虑有哪些原因导致该故障。故障概率:对于每个原因,考虑其发生的概率,不用精确,分档打分即可。风险程度:=严重程度 * 故障概率,据此就可以算出风险的处理优先级了,肯定是程度分数越高的越应该优先解决。已有措施、解决措施、后续规划:用于梳理现状,思考未来的改进方案等。
    基于上面这套方法论,我们可以有效地对系统的风险进行梳理,找出需要优先解决的风险点,从而提高系统的可用性。

    除了 FMEA,其实还有一种应用更广泛的风险分析和治理的理论,即 BCP——业务连续性计划,它是一套基于业务规律的规章流程,保障业务或组织在面对突发状况时其关键业务功能可以持续不中断。

    相比 FMEA,BCP 除了评估风险及重要程度,还要求详细地描述应对方案、残余风险、灾备恢复方案,并要求进行相应故障的培训和演习安排,尽最大努力保障业务连续性。

    知道风险在哪、优先治理何种风险之后,就可以着手优化架构。和高性能架构模式一样,高可用架构也可以从存储和计算两个方面来分析。

    3.2 存储高可用

    存储高可用的本质都是通过将数据复制到多个存储设备,通过数据冗余的方式来提高可用性。

    双机架构

    让我们先从简单的增加一台机器开始,即双机架构。

    当机器变成两台后,根据两台机器担任的角色不同,就会分成不同的策略,比如主备、主从、主主。

    主备复制的架构是指一台机器作为客户端访问的主机,另一台机器纯粹作为冗余备份用,当主机没有故障时,备机不会被客户端访问到,仅仅需要从主机同步数据。这种策略很简单,可以应对主机故障情况下的业务可用性问题,但在平常无法分担主机的读写压力,有点浪费。

    主从复制的架构和主备复制的差别在于,从机除了复制备份数据,还需要干活,即还需要承担一部分的客户端请求(一般是分担读操作)。当主机故障时,从机的读操作不会受到影响,但需要增加读操作的请求分发策略,且和主备不同,由于从机直接提供数据读,如果主从复制延迟大,数据不一致会对业务造成更明显的影响。

    对于主备和主从两种策略,如果主机故障,都需要让另一台机器变成主机,才能继续完整地提供服务,如果全靠人工干预来切换,会比较滞后和易错,最好是能够自动完成切换,这就涉及双机切换的策略。

    在考虑双机切换时,要考虑什么?首先是需要感知机器的状态,是两台机器直连传递互相的状态,还是都传递给第三方来仲裁?所谓状态要包含哪些内容才能定义一台主机是故障呢?是发现一次问题就切换还是多观察一会再切换?切换后如果主机恢复了是切换回来还是自动变备机呢?需不需要人工二次确认一下

    这些问题可能都得根据业务的特性来得出答案,此处仅给出三种常见的双机切换模式:

    互连式:两台机器直接连接传递信息,并根据传递的状态信息判断是否要切换主机,如果通道本身发生故障则无法判断是否要切换了,可以再增加一个通道构成双通道保障,不过也只是降低同时故障的概率。中介式:通过第三方中介来收集机器状态并执行策略,如果通道发生断连,中介可以直接切换其他机器作为主机,但这要求中介本身是高可用的,已经有比较成熟的开源解决方案如 zookeeper、keepalived。模拟式:备机模拟成客户端,向主机发送业务类似的读写请求,根据响应情况来判断主机的状态决定是否要切换主机,这样可以最真实地感受到客户端角度下的主机故障,但和互连式不同,能获取到的其他机器信息很少,容易出现判断偏差。
    最后一种双机架构是主主复制,和前面两种只有一主的策略不同,这次两台都是主机,客户端的请求可以达到任何一台主机,不存在切换主机的问题。但这对数据的设计就有了严格的要求,如果存在唯一 ID、严格的库存数量等数据,就无法适用,这种策略适合那些偏临时性、可丢失、可覆盖的数据场景。

    数据集群

    采用双机架构的前提是一台主机能够存储所有的业务数据并处理所有的业务请求,但机器的存储和处理能力是有上限的,在大数据场景下就需要多台服务器来构成数据集群。

    如果是因为处理能力达到瓶颈,此时可以增加从机帮主机分担压力,即一主多从,称为数据集中集群。这种集群方式需要任务分配算法将请求分散到不同机器上去,主要的问题在于数据需要复制到多台从机,数据的一致性保障会比一主一从更为复杂。且当主机故障时,多台从机协商新主机的策略也会变得复杂。这里有开源的 zookeeper ZAB 算法可以直接参考。

    如果是因为存储量级达到瓶颈,此时可以将数据分散存储到不同服务器,每台服务器负责存储一部分数据,同时也备份一部分数据,称为数据分散集群。数据分散集群同样需要做负载均衡,在数据分区的分配上,hadoop 采用独立服务器负责数据分区的分配,ES 集群通过选举一台服务器来做数据分区的分配。除了负载均衡,还需要支持扩缩容,此外由于数据是分散存储的,当部分服务器故障时,要能够将故障服务器的数据在其他服务器上恢复,并把原本分配到故障服务器的数据分配到其他正常的服务器上,即分区容错性。

    数据分区 

    数据集群可以在单台乃至多台服务器故障时依然保持业务可用,但如果因为地理级灾难导致整个集群都故障了(断网、火灾等),那整个服务就不可用了。面对这种情况,就需要基于不同地理位置做数据分区。

    做不同地理位置的数据分区,首先要根据业务特性制定分区规则,大多还是按照地理位置提供的服务去做数据分区,比如中国区主要存储中国用户的数据。

    既然分区是为了防灾,那么一个分区肯定不止存储自身的数据,还需要做数据备份。从数据备份的策略来说,主要有三种模式:

    集中式:存在一个总备份中心,所有的分区数据都往这个总中心备份,设计起来简单,各个分区间没有联系,不会互相影响,也很容易扩展新的分区。但总中心的成本较高,而且总中心如果出故障,就要全部重新备份。互备式:每个分区备份另一个分区的数据,可以形成一个备份环,或者按地理位置远近来搭对备份,这样可以直接利用已有的设备做数据备份。但设计较复杂,各个分区间需要联系,当扩展新分区时,需要修改原有的备份线路。独立式:每个分区配备自己的备份中心,一般设立在分区地理位置附近的城市,设计也简单,各个分区间不会影响,扩展新分区也容易。但是成本会很高,而且只能防范城市级的灾难。

    3.3 计算高可用

    从存储高可用的思路可以看出,高可用主要是通过增加机器冗余来实现备份,对计算高可用来说也是如此。通过增加机器,分担服务器的压力,并在单机发生故障的时候将请求分配到其他机器来保障业务可用性。

    因此计算高可用的复杂性也主要是在多机器下任务分配的问题,比如当任务来临(比如客户端请求到来)时,如何选择执行任务的服务器?如果任务执行失败,如何重新分配呢?这里又可以回到前文说过的负载均衡相关的解法上。

    计算服务器和存储服务器在多机器情况下的架构是类似的,也分为主备、主从和集群。

    主备架构下,备机仅仅用作冗余,平常不会接收到客户端请求,当主机故障时,备机才会升级为主机提供服务。备机分为冷备和温备。冷备是指备机只准备好程序包和配置文件,但实际平常并不会启动系统。温备是指备机的系统是持续启动的,只是不对外提供服务,从而可以随时切换主机。

    主从架构下,从机也要执行任务,由任务分配器按照预先定义的规则将任务分配给主机和从机。相比起主备,主从可以发挥一定的从机性能,避免成本空费,但任务的分配就变得复杂一些。

    集群架构又分为对称集群和非对称集群。

    对称集群也叫负载均衡集群,其中所有的服务器都是同等对待的,任务会均衡地分配到每台服务器。此时可以采用随机、轮询、Hash 等简单的分配机制,如果某台服务器故障,不再给它分配任务即可。

    非对称集群下不同的服务器有不同的角色,比如分为 master 和 slave。此时任务分配器需要有一定的规则将任务分配给不同角色的服务器,还需要有选举策略来在 master 故障时选择新的 master。这个选举策略的复杂度就丰俭由人了。

    异地多活

    讲存储高可用已经说过数据分区,计算高可用也有类似的高可用保障思路,归纳来说,它们都可以根据需要做异地多活,来提高整体的处理能力,并防范地区级的灾难。异地多活中的”异地“,就是指集群部署到不同的地理位置,“活”则强调集群是随时能提供服务的,不同于“备”还需要一个切换过程。

    按照规模,异地多活可以分为同城异区、跨城异地和跨国异地。显而易见,不同模式下能够应对的地区级故障是越来越高的,但同样的,距离越远,通信成本与延迟就越高,对通信通道可用性的挑战也越高。因此跨城异地已经不适合对数据一致性要求非常高的业务,而跨国异地往往是用来给不同国家的用户提供不同服务的。

    由于异地多活需要花费很高的成本,极大地增加系统复杂度,因此在设计异地多活架构时,可以不用强求为所有业务都做异地多活,可以优先为核心业务实现异地多活。尽量保障绝大部分用户的异地多活,对于没能保障的用户,通过挂公告、事后补偿、完善失败提示等措施进行安抚、提升体验。毕竟要做到100%可用性是不可能的,只能在能接受的成本下尽量逼近,所以当可用性达到一定瓶颈后,补偿手段的成本或许更低。

    在异地部署的情况下,数据一定会冗余存储,物理上就无法实现绝对的实时同步,且距离越远对数据一致性的挑战越大,虽然可以靠减少距离、搭建高速专用网络等方式来提高一致性,但也只是提高而已,因此大部分情况下, 只需考虑保障业务能接受范围下的最终一致性即可。

    在同步数据的时候,可以采用多种方式,比如通过消息队列同步、利用数据库自带的同步机制同步、通过换机房重试来解决同步延迟问题、通过 session id 让同一数据的请求都到同一机房从而不用同步等。

    可见,整个异地多活的设计步骤首先是对业务分级,挑选出核心业务做异地多活,然后对需要做异地多活的数据进行特征分析,考虑数据量、唯一性、实时性要求、可丢失性、可恢复性等,根据数据特性设计数据同步的方案。最后考虑各种异常情况下的处理手段,比如多通道同步、日志记录恢复、用户补偿等,此时可以借用前文所说的 FMEA 等方法进行分析。

    接口级故障

    前面讨论的都是较为宏观的服务器、分区级的故障发生时该怎么办,实际上在平常的开发中,还应该防微杜渐,从接口粒度的角度,来防范和应对接口级的故障。应对的核心思路依然是优先保障核心业务和绝大部分用户可用。

    对于接口级故障,有几个常用的方法:限流、排队、降级、熔断。其中限流和排队属于事前防范的措施,而降级和熔断属于接口真的故障后的处理手段。

    限流的目的在于控制接口的访问量,避免被高频访问冲垮。

    从限流维度来说,可以基于请求限流,即限制某个指标下、某个时间段内的请求数量,阈值的定义需要基于压测和线上情况来逐步调优。还可以基于资源限流,比如根据连接数、文件句柄、线程数等,这种维度更适合特殊的业务。

    实现限流常用的有时间窗算法和桶算法。

    时间窗算法分为固定时间窗和滑动时间窗。

    固定时间窗通过统计固定时间周期内的量级来决定限流,但存在一个临界点的问题,如果在两个时间窗的中间发生超大流量,而在两个时间窗内都各自没有超出限制,就会出现无法被限流拦截的接口故障。因此滑动时间窗采用了部分重叠的时间统计周期来解决临界点问题。

    桶算法分为漏桶和令牌桶。

    漏桶算法是将请求放入桶中,处理单元从桶里拿请求去进行处理,如果桶堆满了就丢弃掉新的请求,可以理解为桶下面有个漏斗将请求往处理单元流动,整个桶的容量是有限的。这种模式下流入的速率取决于请求的频率,当桶内有堆积的待处理请求时,流出速率是匀速的。漏桶算法适用于瞬时高并发的场景(如秒杀),处理可能慢一点,但可以缓存部分请求不丢弃。

    令牌桶算法是在桶内放令牌,令牌数是有限的,新的请求需要先到桶里拿到令牌才能被处理,拿不到就会被丢弃。和漏桶匀速流出处理不同,令牌桶还能通过控制放令牌的速率来控制接收新请求的频率,对于突发流量,可靠累计的令牌来处理,但是相对的处理速度也会突增。令牌桶算法适用于控制第三方服务访问速度的场景,防止压垮下游。

    除了限流,还有一种控制处理速度的方法就是排队。当新请求到来后先加入队列,出队端通过固定速度出队处理请求,避免处理单元压力过大。队列也有长度限制,其机制和漏桶算法差不多。

    如果真的事前防范真的被突破了,接口很可能或已经发生了故障,还能做什么呢?

    一种手段是熔断,即当处理量达到阈值,就主动停掉外部接口的访问能力,这其实也是一种防范措施,对外的表现虽然是接口访问故障,但系统内部得以被保护,不会引起更大的问题,待存量处理被消化完,或者外部请求减弱,或完成扩容后,再开放接口。熔断的设计主要是阈值,需要按照业务特点和统计数据制定。

    当接口故障后(无论是被动还是主动断开),最好能提供降级策略。降级是丢车保帅,放弃一下非核心业务,保障核心业务可用,或者最低程度能提供故障公告,让用户不要反复尝试请求来加重问题了。比起手动降级,更好的做法也是自动降级,需要具备检测和发现降级时机的机制。

    04、可扩展架构模式

    再回顾一遍互联网行业的金科玉律:只有变化才是不变的。在设计架构时,一开始就要抱着业务随时可能变动导致架构也要跟着变动的思想准备去设计,差别只在于变化的快慢而已。因此在设计架构时一定是要考虑可扩展性的。

    在思考怎样才是可扩展的时候,先想一想平常开发中什么情况下会觉得扩展性不好?大都是因为系统庞大、耦合严重、牵一发而动全身。因此对可扩展架构设计来说,基本的思想就是拆分。

    拆分也有多种指导思想,如果面向业务流程来谈拆分,就是分层架构;如果面向系统服务来谈拆分,就是 SOA、微服务架构;如果面向系统功能来拆分,就是微内核架构。

    分层架构

    分层架构是我们最熟悉的,因为互联网业务下,已经很少有纯单机的服务,因此至少都是 C/S 架构、B/S 架构,也就是至少也分为了客户端/浏览器和后台服务器这两层。如果进一步拆分,就会将后台服务基于职责进行自顶向下的划分,比如分为接入层、应用层、逻辑层、领域层等。

    分层的目的当然是为了让各个层次间的服务减少耦合,方便进行各自范畴下的优化,因此需要保证各层级间的差异是足够清晰、边界足够明显的,否则当要增加新功能的时候就会不知道该放到哪一层。各个层只处理本层逻辑,隔离关注点。

    额外需注意的是一旦确定了分层,请求就必须层层传递,不能跳层,这是为了避免架构混乱,增加维护和扩展的复杂度,比如为了方便直接跨层从接入层调用领域层查询数据,当需要进行统一的逻辑处理时,就无法切面处理所有请求了。

    SOA 架构

    SOA 架构更多出现在传统企业中,其主要解决的问题是企业中 IT 建设重复且效率低下,各部门自行接入独立的 IT 系统,彼此之间架构、协议都不同,为了让各个系统的服务能够协调工作,SOA 架构应运而生。

    其有三个关键概念:服务、ESB 和松耦合。

    服务是指各个业务功能,比如原本各部门原本的系统提供的服务,可大可小。由于各服务之间无法直接通信,因此需要 ESB,即企业服务总线进行对接,它将不同服务连接在一起,屏蔽各个服务的不同接口标准,类似计算机中的总线。松耦合是指各个服务的依赖需要尽量少,否则某个服务升级后整个系统无法使用就麻烦了。

    这里也可以看出,ESB 作为总线,为了对接各个服务,要处理各种不同的协议,其协议转换耗费了大量的性能,会成为整个系统的瓶颈。

    微服务

    微服务是近几年最耳熟能详的架构,其实它和 SOA 有一些相同之处,比如都是将各个服务拆分开来提供能力。但是和 SOA 也有一些本质的区别,微服务是没有 ESB 的,其通信协议是一致的,因此通信管道仅仅做消息的传递,不理解内容和格式,也就没有 ESB 的问题。而且为了快速交付、迭代,其服务的粒度会划分地更细,对自动化部署能力也就要求更高,否则部署成本太大,达不到轻量快速的目的。

    当然微服务虽然很火,但也不是解决所有问题的银弹,它也会有一些问题存在。如果服务划分的太细,那么互相之间的依赖关系就会变得特别复杂,服务数量、接口量、部署量过多,团队的效率可能大降,如果没有自动化支撑,交付效率会很低。由于调用链太长(多个服务),因此性能也会下降,问题定位会更困难,如果没有服务治理的能力,管理起来会很混乱,不知道每个服务的情况如何。

    因此如何拆分服务就成了每个使用微服务架构的团队的重要考量点。这里也提供一些拆分的思路:

    三个火枪手原则:考虑每三个人负责一个服务,互相可以形成稳定的人员备份,讨论起来也更容易得出结论,在此基础上考虑能负责多大的一个服务。基于业务逻辑拆分:最直观的就是按逻辑拆分,如果职责不清,就参考三个火枪手原则确定服务大小。基于稳定性拆分:按照服务的稳定性分为稳定服务和变动服务,稳定服务粒度可以粗一些,变动服务粒度可以细一些,目的是减少变动服务之间的影响,但总体数量依然要控制。基于可靠性拆分:按照可靠性排序,要求高的可以拆细一些,由前文可知,服务越简单,高可用方案就会越简单,成本也会越低。优先保障核心服务的高可用。基于性能拆分:类似可靠性,性能要求越高的,拆出来单独做高性能优化,可有效降低成本。
    微服务架构如果没有完善的基础设施保障服务治理,那么也会带来很多问题,降低效率,因此根据团队和业务的规模,可以按以下优先级进行基础设施的支持:

    优先支持服务发现、服务路由、服务容错(重试、流控、隔离),这些是微服务的基础。接着支持接口框架(统一的协议格式与规范)、API 网关(接入鉴权、权限控制、传输加密、请求路由等),可以提高开发效率。然后支持自动化部署、自动化测试能力,并搭建配置中心,可以提升测试和运维的效率。最后支持服务监控、服务跟踪、服务安全(接入安全、数据安全、传输安全、配置化安全策略等)的能力,可以进一步提高运维效率。
    微内核架构
    最后说说微内核架构,也叫插件化架构,顾名思义,是面向功能拆分的,通常包含核心系统和插件模块。在微内核架构中,核心系统需要支持插件的管理和链接,即如何加载插件,何时加载插件,插件如何新增和操作,插件如何和核心引擎通信等。

    举一个最常见的微内核架构的例子——规则引擎,在这个架构中,引擎是内核,负责解析规则,并将输入通过规则处理后得到输出。而各种规则则是插件,通常根据各种业务场景进行配置,存储到数据库中。

    05、总结

    人们通常把某项互联网业务的发展分为四个时期:初创期、发展期、竞争期和成熟期。

    在初创期通常求快,系统能买就买,能用开源就用开源,能用的就是好的,先要活下来;到了发展期开始堆功能和优化,要求能快速实现需求,并有余力对一些系统的问题进行优化,当优化到顶的时候就需要从架构层面来拆分优化了;进入竞争期后,经过发展期的快速迭代,可能会存在很多重复造轮子和混乱的交互,此时就需要通过平台化、服务化来解决一些公共的问题;最后到达成熟期后,主要在于补齐短板,优化弱项,保障系统的稳定。

    在整个发展的过程中,同一个功能的前后要求也是不同的,随着用户规模的增加,性能会越来越难保障,可用性问题的影响也会越来越大,因此复杂度就来了。

    对于架构师来说,首要的任务是从当前系统的一大堆纷繁复杂的问题中识别出真正要通过架构重构来解决的问题,集中力量快速突破,但整体来说,要徐徐图之,不要想着用重构来一次性解决所有问题。

    对项目中的问题做好分类,划分优先级,先易后难,才更容易通过较少的资源占用,较快地得到成果,提高士气。然后再循序渐进,每个阶段控制在 1~3 个月,稳步推进。

    当然,在这个过程中,免不了和上下游团队沟通协作,需要注意的是自己的目标和其他团队的目标可能是不同的,需要对重构的价值进行换位思考,让双方都可以合作共赢,才能借力前进。

    还是回到开头的那句话,架构设计的主要目的是为了解决软件系统复杂度带来的问题。首先找到主要矛盾在哪,做到有的放矢,然后再结合知识、经验进行设计,去解决面前的问题。

    祝各位开发者都成为一名合格的架构师。以上便是本次分享的全部内容,如果觉得内容有用,欢迎转发分享。

    -End-

    原创作者|Cloudox


    作者:腾讯云开发者
    链接:https://juejin.cn/post/7251779626682023994
    来源:稀土掘金

    收起阅读 »

    《Thinking In Java》作者:不要使用并发!

    前言 今天纯粹就是带你们来读读书的~ 最近除了工作,特地买回了自己很喜欢的作者新发售的一本书《On Java》,作者是我的老朋友布鲁斯·埃克尔,在Java领域很有名,你可能没听过他的名字,但极有可能听过他的另一本书《Thinking In Java》,我想很...
    继续阅读 »

    前言



    今天纯粹就是带你们来读读书的~


    最近除了工作,特地买回了自己很喜欢的作者新发售的一本书《On Java》,作者是我的老朋友布鲁斯·埃克尔,在Java领域很有名,你可能没听过他的名字,但极有可能听过他的另一本书《Thinking In Java》,我想很多Java工程师都读过这本书,可以说是Java编程思想的良心之作。


    虽然布鲁斯是我的老朋友,但我不得不吐槽一下,大概通读了一遍《On Java》之后,我心里大体认为是不如《Thinking In Java》的,可能和写小说一样,读者的要求高了,而作者的年纪大了。


    我认识布鲁斯很多年了,他是个比较幽默风趣的人,经常在书中直言不讳某编程语言的垃圾之处,同时又对该语言的未来做一点展望,算是一个很中肯且典型的直男程序猿。


    最后说一点,我认识他,他不认识我。





    正文



    我着重看了自己比较感兴趣的并发编程这一块,想知道这位大佬对于目前Java并发编程是否有新的看法和意见,不出我所料,他没讲什么重要的东西,但是好像又讲了,带着吐槽批判式的口吻,陈列了他喜欢和讨厌Java并发编程的地方。


    所以我把一些我觉得有意思的地方画出来,分享给大家,看看一个资深Java大佬对并发编程的理解。



    1、大佬的并发定律


    111.png



    其实看到作者研究出的这4条定律时,我还是挺意外的,第一句就点题了,不要使用并发。


    仔细想想好像也对……再琢磨一下咦有感觉……最后回忆一下这些年参与的项目……哇擦好有道理!


    接下来3条基本算是总纲了,后面的内容都是对这几条的说明。





    2、你已埋下的隐患


    222.png



    这里就是对2、3条的具体说明了,有些话我觉得略显啰嗦,我把对于程序员来讲比较重要的一句话画出来了。


    你很容易写出一个看起来运行正常但实际上有问题的并发程序。


    看到这句话的时候是不是已经开始默默打开自己的IDEA了,然后审视了一遍自己提交的代码?


    别看了,你埋的炸弹还少么,能看出花来吗。


    看清楚作者后面那句:你这个问题只有满足最罕见的条件时,才会将自己暴露出来。


    我可以这么说,在座绝大部分同行去了下一家公司干活,可能上一家公司的新同事才会在你毫不知情的时候默默踩到你埋的地雷然后被炸个粉碎,而你在新公司也正在踩别人的雷,出来混都是要还的。





    3、别否认你就是这种人


    333.png



    看到这里的时候,我忍不住亲了布鲁斯一口,他痛快的描述出了我一直以来在工作中说不清道不明的烦躁,因为你总会遇到这样的人,同时很难发现自己到底是不是这样的人。


    我在工作前3年其实如履薄冰,感觉自己什么都学了,但去了公司发现什么都不会,怀揣着自我否定一点点完成别人布置的任务,直到工作5年以后才有一种醍醐灌顶的感觉,理解了自己做的是什么,接下来要学习哪个方向,以前学到那么多东西究竟是怎么串联起来的,这是一种打通任督二脉的满足感。


    等到工作8年之后,才真正开始回头看Java语言,对以前烦厌欲呕的Java基础提起莫名的兴趣,同时喜欢看书,写案例,尝试阅读别人的源码等等,此时我才真正有自己一只腿迈进Java领域的意识。


    同时,在工作中会对许多能力一般但沟通较为偏执的同事产生抵触情绪,我有时会认为这是一种大人看小孩耍脾气的感觉,这个只有在工作多年之后才会产生,作者很准确的阐述出了我描绘不出的这种解释。


    同样的,我认为在这个成长的过程中,我一定也成为过别人心中眼高手低的人。


    我在这里能分享给大家的经验就是,在工作中多学习少争论,多和厉害的人走近一点,虚心把对方的东西都学过来,长此以往你会进步神速,这不是你在网上学习能得到的,一定是在工作中。





    4、高级Javaer都有过的想法


    444.png



    这里我为什么专门画出来,因为很多高级javaer一定有过类似的想法,就是发现了Java并不擅长做并发编程,是否可以用其他语言来完成,而Java只做他自己擅长的事。


    至少我以前就想过,可现实层面我认为是异想天开的,尤其是工作中,基本都是团队开发,这种想法就已经几乎被pass掉了,同时为了某一个领域的实现专门引入一门编程语言甚至体系,得不偿失,毕竟Java不擅长但却成熟,光是网上卖课郎告诉你的就有N种诸如《Java千亿级高并发解决方案》、《Java万亿级电商实战》等等这样的受用终生的鬼东西。


    而你辛辛苦苦跟着学完后,发现玛德用不上,就像你学了《九阴真经》后以为可以当武林盟主最终却进了铁匠铺,而铁匠铺老板还不想听你鬼扯只想让你每天加班多打几把武器。


    图片中我还画了个圈,我想不少人应该知道这门语言,还蛮有名的,就是国内不太火,这有编程历史因素在里面,其实还有一门语言也蛮适合的,而且这几年也挺火,我想你也猜到了,我觉得5年+的Java工程师都应该关注甚至学习一下。





    5、我和大佬不谋而合


    555.png



    这是接近尾声的部分了,也是这位作者熟悉的笔法,发泄完自己的情绪后又开始对Java的某新版本极尽赞美,典型的被PUA了。


    但不得不说,Java8我也认为是革命性的版本,在这个版本发布以前,作为Java工程师你甚至不会想到它敢做到这个地步,就像布鲁斯书中讲的,这是史诗般的魔法。


    你可以在Java8的版本里发现一些其他语言的影子,这没什么,天下语言一大抄,发展到一定程度,已经是避免不了的趋势了。


    重要的是,这个版本给Java上油了,为后续的版本提供了活力,而Java17作为官方长久支持版本的其中一个非常重要的版本,你可以发现有其他框架给它背书,比如SpringBoot3只支持Java17,而Jenkins也宣布在新版本放弃Java8并且该团队更推荐Java17,IDEA后续新版本可能也会放弃Java8,这明显就是小圈子,有利益的勾连,但对Java本身发展不是坏事。


    所以,Java8的核心技术点最应该学习,如果现在还一点不会,赶紧学吧,我认为这是后续版本的基础了,lambada表达式、stream流不必说了,是Java8版本的核心技术,CompletableFuture作为Java8并发编程中最大的改进要花时间好好学习,这也是本书作者所提到的,而且后面专门花了一个大章来讲CompletableFuture。


    作者虽然一直强调不要使用并发,但却对Java8的并发编程工具花了较大篇幅,我个人认为他更多的是一种见猎心喜,可是我们面试经常会问到这个工具类相关的东西,看一下大佬对该工具的理解还是很有用的。





    总结



    《On Java》这本书说实话,我觉得没有作者的《Thinking In Java》写得好,可能有多种原因导致。


    我说下我觉得不好的主要感受在哪里,一是有些地方翻译的不好,会给你带来困惑,二是作者给出的一些案例有自己的风格,而且例子我没觉得那么通俗易懂。


    但总体上还是值得一看,尤其是他穿插了很多和其他如C/C++、GO等语言的比较,还包含了自己对Java的理解,尤其是一些编程思想很直接,最后给出了林林总总有接近70条的编程指南,我认为对于初学者树立未来工作中的编程思想是很有用的。


    这位作者的文字中弥漫着一股浓烈的不推荐使用并发编程的味道,我觉得是他多年工作的心得,所以大家在往后的工作中不妨可以借鉴下大佬的思维。


    好了,我今天也就是带你读了下书,读的还开心吗。







    本人原创文章纯手打,觉得有一滴滴帮助的话就请点个赞和收藏吧~


    本人长期分享工作中的感悟、经验及实用案例,喜欢的话也可以进入个人主页关注一下哦~


    作者:程序员济癫
    来源:juejin.cn/post/7147523943321042980
    收起阅读 »

    个人支付项目,已稳定收款 100+

    对,没错,又趁着周末两天 + 几个工作日晚上熬夜开发了个支付项目出来,赞赏平台。我对这个项目的定位非常简单,就是一个买卖平台。平台内容由我来发布,免费内容大家只需注册即可观看,如需付费则支付相关费用方可查看。下面是项目运行首页下面是项目登录注册页下面是商品支付...
    继续阅读 »

    对,没错,又趁着周末两天 + 几个工作日晚上熬夜开发了个支付项目出来,赞赏平台。

    我对这个项目的定位非常简单,就是一个买卖平台。平台内容由我来发布,免费内容大家只需注册即可观看,如需付费则支付相关费用方可查看。

    下面是项目运行首页


    下面是项目登录注册页


    下面是商品支付页面


    虽然项目整体规模较小但也算是五脏俱全,有认证相关、有支付相关、也有分布式问题相关。对于没有做过个人项目特别是没做过支付项目的小伙伴来说,用来练手或者写在简历上都是未尝不可的。


    那下面我来具体项目中几个重要的业务点。


    1、网关认证


    以前我们开发项目要进行认证基本都是通过在服务中写个拦截器,然后配置拦截器拦截所有的请求,最终通过拦截器的逻辑进行认证。这中方法不是不可以,但我觉得不好,如果我们项目中有三个微服务以上,那么这个拦截器的认证逻辑就会存在于每个微服务中,这是我认为的不好的点。


    那我是怎么做的呢!


    对,在网关服务里做认证动作。将认证动作迁移,因为我的个人项目是通过网关进行请求转发,所以,所有的请求都会先进入网关,再进入各个具体的业务服务,那问题就好办了。我直接通过实现网关的 GlobalFilter 接口拦截所有的请求,通过实现该接口进行认证逻辑处理,完成本平台的认证、续约、限流等功能。


    下面来看看请求流程图


    2、支付逻辑


    支付功能可以说是本项目的重中之重,需要有非常强的健壮性。因为我是一位个人开发者,所以不能对接需要有营业执照的支付功能,最终我选择了支付宝的当面付这一个功能。


    当面付的好处很多,第一它不需要营业执照,第二对接也非常简单而且有支付宝封装的SDK,所以本人再对接的过程中没有费多少力气就把接口打通了。


    主要就是对接了当面付的三个接口:

    1. 获取支付二维码接口

    2. 支付成功的回调接口

    3. 订单状态回查接口


    当然,这三个接口的代码量也是很大的,所以本人为了通用就又对他做了一层封装,使得项目调用支付功能就更加简单了。如下就可以完成一个支付功能的完整逻辑:


    是不是很简单,如果需要代码的可以看文章末尾哦!


    下面我来介绍一下本项目中付费内容的整个业务流程。


    1、用户获取付费商品详情


    2、点击查看内容,这里就有两种结果了

    • 第一种结果:商品已支付,直接显示内容给用户观看

    • 第二种结果:商品未支付,提示用户付款查看


    3、当显示第二种结果时,如果用户点击付款,则进入后续流程


    4、服务器请求支付宝第三方,获取对应金额的支付二维码,并将返回的二维码和用户绑定生成一个未支付的订单,最终将这个待支付二维码返回给页面


    5、页面显示二维码之后,用户就需要进行扫码付款(打开支付宝APP扫码付款)


    6、用户付款成功之后,支付宝第三方会自动回调第四步我给支付宝的回调地址。回调接口的逻辑就是将订单状态改为已支付并做一些后续的流程操作。


    7、为了防止回调接口出问题,还写了一个定时任务,定时回查订单表中未支付订单的状态,循环请求支付宝询问支付支付成功并执行支付成功的相应回调逻辑。


    支付业务流程图


    3、手写分布式锁


    相信分布式锁大家都不陌生,无非就是向中间件中放入一个标志量,存在即表示已锁,反之则未锁执行相关逻辑。


    说都会说,但要真正自己手写而且做到高可用确是一个非常困难的问题。其中非常关键的一点就是如何解锁,如何做到业务执行完成百分之百解锁,那我再项目中是如何考虑的呢!


    我先来简单的说一下思路:


    1、定义一个分布式锁注解,用来标注那些方法需要分布式锁加持


    2、定义一个切面,逻辑就是给加上了分布式注解的方法进行增强


    3、增强的逻辑为:加锁、生成续约任务、执行业务逻辑、解锁


    4、另起一个延迟线程池,每隔一定时间遍历一次续约任务集合,判断任务是否需要进行续约(这个逻辑判断很多如:续约次数过多、业务已执行完毕、是否需要续约等等)


    具体业务流程如图(我画的比较多)


    当然,为了方便你们理解,我还出了相关视频,地址:



    http://www.bilibili.com/video/BV1jP…



    以上,就是赞赏平台项目中三个比较大的亮点,不论是写在简历上还是当作个人项目都是一个非常不错的选择,那我也把这个项目搭建起来了,地址如下:



    admire.j3code.cn



    需要项目代码 + 视频 + 详细文档的,我都放在这个平台上了,自取即可。


    我是J3code(三哥),咱们下回见

    作者:J3code
    链接:https://juejin.cn/post/7199820362954588197
    来源:稀土掘金
    收起阅读 »

    为了摸鱼,我开发了一个工具网站

      大家好,我是派大星,由于前段时间实习入职,所以把时间以及精力都放在熟悉公司业务以及从工作中提升自己的业务逻辑,空余时间也是放在了学习新技术上,到目前为止也是参与了公司3个项目的开发团队中,参与过程中犯过错,暴露出了很多的不足,丧失过信心,学生时期...
    继续阅读 »


      大家好,我是派大星,由于前段时间实习入职,所以把时间以及精力都放在熟悉公司业务以及从工作中提升自己的业务逻辑,空余时间也是放在了学习新技术上,到目前为止也是参与了公司3个项目的开发团队中,参与过程中犯过错,暴露出了很多的不足,丧失过信心,学生时期所带的傲气也是被一点一点的慢慢的打磨掉,正是因为这些,带给我的成长是巨大的。好了,闲言少叙,下面让我们进入今天的主题。


    创作背景


           因为”懒“得走路,所以发明了汽车飞机,因为”懒“得干苦力活,所以发明了机器帮助我们做,很早之前看到过这个梗,而我这次同样也是因为”懒“,所以才开发出了这个工具。这里先卖个关子,先不说这个工具的作用,容我向大家吐槽一下这一段苦逼的经历。在我实习刚入职不久,就迎来了自己第一个任务,由于自己对所参与的项目的业务并不太了解,所以只能先做一些类似测试的工作,比如就像这次,组长给了我一份Json 文件,当我打开文件后看到数据都是一些地区名称,但当我随手的将滚动条往下一拉,瞬间发现不对劲,因为这个小小的文件行数竟然达到了1w+❗❗❗❗


    不禁让我脊背发凉,但是这时我的担心还没达到最坏的地步,毕竟我还对具体的任务不了解。但当组长介绍任务内容,主要是让我将这些数据添加到数据库对应的表中,由于没有sql脚本,只有这个json 文件,需要手动去操作,而且给我定的任务周期是两天。听到这个时间时内心的慌张瞬间消失了,因为在之前我就了解过Navicat支持Json格式的文件直接导入数据,一个这么简单的任务给我两天时间,这不是非要让我带薪学习。


    当我接下任务自信打开Navicat的导入功能时发现了一个重要问题,虽然它支持字段映射,但是给的Json数据是省市区地址名称,里面包含着各种嵌套,说实话到想到这里我已经慌了,而且也测试了一下字段只能单个的批量导入,而且不支持嵌套的类型,突然就明白为什么 给我两天的时间。


    这时候心里只能默默祈祷已经有大神开发出了能处理这种数据的工具网站,但是经过一个小时的艰苦奋斗,但最终依旧是没有结果,网上有很多JsonSQl的工具网站,但是很多都支持简单支持一下生成创建表结构的语句,当场心如死灰,跑路的心都有了。但最终还是咬着牙 手动初始化数据,其过程中的“趣味” 实属无法用语言表达……

     上述就是这个工具的开发背景,也是怕以后再给我分配这么“有趣” 的任务。那么下面就给大家分享一下我自制的 Json转译SQL 工具,而且它也是一个完全免费的工具网站,同时这次也是将项目进行了 开源分享,大家也可以自己用现成的代码完成本地部署测试,感兴趣的同学可以自行拉取代码!



    开源地址:github.com/pdxjie/sql-…



    项目简介


    Sql-Translation (简称ST)是一个 Json转译SQL 工具,在同类工具的基础上增强了功能,为节省时间、提高工作效率而生。并且遵循 “轻页面、重逻辑” 的原则,由极简页面来处理复杂任务,且它不仅仅是一个项目,而是以“降低时间成本、提高效率”为目标的执行工具。


    技术选型

    前端:

    Vue
    AntDesignUI组件库
    MonacoEditor 编辑器
    sql-formatter SQL格式化

    后端:

    SpringBoot
    FastJson

    项目特点

    1.内置主键:JSON块如果包含id字段,在选择建表操作模式时内部会自动为id设置primary key
    2.支持JSON数据生成建表语句:按照内置语法编写JSON,支持生成创建表的SQL语句
    3.支持JSON数据生成更新语句:按照内置语法编写JSON,支持生成创更新的SQL语句,可配置单条件、多条件更新操作
    4.支持JSON数据生成插入语句:按照内置语法编写JSON,支持生成创插入的SQL语句,如果JSON中包含 多层 (children)子嵌套,可按照相关语法指定作为父级id的字段
    5.内置操作语法:该工具在选取不同的操作模式时,内置特定的使用语法规范
    6.支持字段替换:需转译的JSON中字段与对应的SQL字段不一致时可以选择字段替换
    7.界面友好:支持在线编辑JSON代码,支持代码高亮、语法校验、代码格式化、查找和替换、代码块折叠等,体验良好

    解决痛点

    下面就让我来给大家介绍一下Sql-Translation 可以解决哪些痛点问题:

    需要将大量JSON中的数据导入到数据库中,但是JSON中包含大量父子嵌套关系 ——> 可以使用本站

    在进行JSON数据导入数据库时,遇到JSON字段与数据库字段不一致需要替换字段时 ——> 可以使用本站

    根据Apifox工具来实现更新或新增接口(前提是对接口已经完成了设计工作),提供了Body体数据,而且不想手动编写SQL时 ——> 可以使用本站

    对上述三点进行进行举例说明(按照顺序):

    第一种情况:

    {
    "id": "320500000",
    "text": "苏州工业园区",
    "value": "320500000",
    "children": [
    {
    "id": "320505006",
    "text": "斜塘街道",
    "value": "320505006",
    "children": []
    },
    {
    "id": "320505007",
    "text": "娄葑街道",
    "value": "320505007",
    "children": []
    },
    ....
    ]
    }

    第二种情况:


    第三种情况


    以上内容就是该工具的简单介绍,由于该工具内置了部分语法功能,想要了解本工具全部工具以及想要动手操作的的同学请点击前往操作文档 ,该操作文档中包含了具体的语法介绍以及每种转换的具体示例数据 提供测试使用。


    地址传送门



    如果感兴趣的同学还希望可以到源码仓库给作者点个star⭐ 作为支持,非常感谢!


    作者:派同学
    链接:https://juejin.cn/post/7168285867160076295
    来源:稀土掘金
    收起阅读 »

    宽表为什么横行?

    宽表在BI业务中比比皆是,每次建设BI系统时首先要做的就是准备宽表。有时系统中的宽表可能会有上千个字段,经常因为“过宽”超过了数据库表字段数量限制还要再拆分。 为什么大家乐此不疲地造宽表呢?主要原因有两个。 一是为了提高查询性能。现代BI通常使用关系数据库作为...
    继续阅读 »

    宽表在BI业务中比比皆是,每次建设BI系统时首先要做的就是准备宽表。有时系统中的宽表可能会有上千个字段,经常因为“过宽”超过了数据库表字段数量限制还要再拆分。


    为什么大家乐此不疲地造宽表呢?主要原因有两个。


    一是为了提高查询性能。现代BI通常使用关系数据库作为后台,而SQL通常使用的HASH JOIN算法,在关联表数量和关联层级变多的时候,计算性能会急剧下降,有七八个表三四层级关联时就能观察到这个现象,而BI业务中的关联复杂度远远超过这个规模,直接使用SQL的JOIN就无法达到前端立等可取的查询需要了。为了避免关联带来的性能问题,就要先将关联消除,即将多表事先关联好采用单表存储(也就是宽表),再查询的时候就可以不用再关联,从而达到提升查询性能的目的。


    二是为了降低业务难度。因为多表关联尤其是复杂关联在BI前端很难表达和使用。如果采用自动关联(根据字段类型等信息匹配)当遇到同维字段(如一个表有2个以上地区字段)时会“晕掉”不知道该关联哪个,表间循环关联或自关联的情况也无法处理;如果将众多表开放给用户来自行选择关联,由于业务用户无法理解表间关系而几乎没有可用性;分步关联可以描述复杂的关联需求,但一旦前一步出错就要推倒重来。所以,无论采用何种方式,工程实现和用户使用都很麻烦。但是基于单表来做就会简单很多,业务用户使用时没有什么障碍,因此将多表组织成宽表就成了“自然而然”的事情。


    不过,凡事都有两面性,我们看到宽表好处而大量应用的同时,其缺点也不容忽视,有些缺点会对应用产生极大影响。下面来看一下。


    宽表的缺点


    数据冗余容量大


    宽表不符合范式要求,将多个表合并成一个表会存在大量冗余数据,冗余程度跟原表数据量和表间关系有关,通常如果存在多层外键表,其冗余程度会呈指数级上升。大量数据冗余不仅会带来存储上的压力(多个表组合出来的宽表数量可能非常多)造成数据库容量问题,在查询计算时由于大量冗余数据参与运算还会影响计算性能,导致虽然用了宽表但仍然查询很慢。


    数据错误


    由于宽表不符合三范式要求,数据存储时可能出现一致性错误(脏写)。比如同一个销售员在不同记录中可能存储了不同的性别,同一个供应商在不同记录中的所在地可能出现矛盾。基于这样的数据做分析结果显然不对,而这种错误非常隐蔽很难被发现。


    另外,如果构建的宽表不合理还会出现汇总错误。比如基于一对多的A表和B表构建宽表,如果A中有计算指标(如金额),在宽表中就会重复,基于重复的指标再汇总就会出现错误。


    灵活性差


    宽表本质上是一种按需建模的手段,根据业务需求来构建宽表(虽然理论上可以把所有表的组合都形成宽表,但这只存在于理论上,如果要实际操作会发现需要的存储空间大到完全无法接受的程度),这就出现了一个矛盾:BI系统建设的初衷主要是为了满足业务灵活查询的需要,即事先并不知道业务需求,有些查询是在业务开展过程中逐渐催生出来的,有些是业务用户临时起意的查询,这种灵活多变的需求采用宽表这种要事先加工的解决办法极为矛盾,想要获得宽表的好就得牺牲灵活性,可谓鱼与熊掌不可兼得。


    可用性问题


    除了以上问题,宽表由于字段过多还会引起可用性低的问题。一个事实表会对应多个维表,维表又有维表,而且表之间还可能存在自关联/循环关联的情况,这种结构在数据库系统中很常见,基于这些结构的表构建宽表,尤其要表达多个层级的时候,宽表字段数量会急剧增加,经常可能达到成百上千个(有的数据库表有字段数量限制,这时又要横向分表),试想一下,在用户接入界面如果出现上千个字段要怎么用?这就是宽表带来的可用性差的问题。


    总体来看,宽表的坏处在很多场景中经常要大于好处,那为什么宽表还大量横行呢?


    因为没办法。一直没有比宽表更好的方案来解决前面提到的查询性能和业务难度的问题。其实只要解决这两个问题,宽表就可以不用,由宽表产生的各类问题也就解决了。


    SPL+DQL消灭宽表


    借助开源集算器SPL可以完成这个目标。


    SPL(Structured Process Language)是一个开源结构化数据计算引擎,本身提供了不依赖数据库的强大计算能力,SPL内置了很多高性能算法,尤其是对关联运算做了优化,对不同的关联场景采用不同的手段,可以大幅提升关联性能,从而不用宽表也能实时关联以满足多维分析时效性的需要。同时,SPL还提供了高性能存储,配合高效算法可以进一步发挥性能优势。


    只有高性能还不够,SPL原生的计算语法不适合多维分析应用接入(生成SPL语句对BI系统改造较大)。目前大部分多维分析前端都是基于SQL开发的,但SQL体系(不用宽表时)在描述复杂关联计算上又很困难,基于这样的原因,SPL设计了专门的类SQL查询语法DQL(Dimensional Query Language)用于构建语义层。前端生成DQL语句,DQL Server将其转换成SPL语句,再基于SPL计算引擎和存储引擎完成查询返回给前端,实现全链路BI查询。需要注意的是,SPL只作为计算引擎存在,前端界面仍要由用户自行实现(或选用相应产品)。



    SPL:关联实现技术


    SPL如何不用宽表也能实现实时关联以满足性能要求的目标?


    在BI业务中绝大部分的JOIN都是等值JOIN,也就是关联条件为等式的 JOIN。SPL把等值关联分为外键关联和主键关联。外键关联是指用一个表的非主键字段,去关联另一个表的主键,前者称为事实表,后者称为维表,两个表是多对一的关系,比如订单表和客户表。主键关联是指用一个表的主键关联另一个表的主键或部分主键,比如客户表和 VIP 客户表(一对一)、订单表和订单明细表(一对多)。


    这两类 JOIN 都涉及到主键,如果充分利用这个特征采用不同的算法,就可以实现高性能的实时关联了。


    不过很遗憾,SQL 对 JOIN 的定义并不涉及主键,只是两个表做笛卡尔积后再按某种条件过滤。这个定义很简单也很宽泛,几乎可以描述一切。但是,如果严格按这个定义去实现 JOIN,理论上没办法在计算时利用主键的特征来提高性能,只能是工程上做些有限的优化,在情况较复杂时(表多且层次多)经常无效。


    SPL 改变了 JOIN 的定义,针对这两类 JOIN 分别处理,就可以利用主键的特征来减少运算量,从而提高计算性能。


    外键关联


    和SQL不同,SPL中明确地区分了维表和事实表。BI系统中的维表都通常不大,可以事先读入内存建立索引,这样在关联时可以少计算一半的HASH值。


    对于多层维表(维表还有维表的情况)还可以用外键地址化的技术做好预关联。即将维表(本表)的外键字段值转换成对应维表(外键表)记录的地址。这样被关联的维表数据可以直接用地址取出而不必再进行HASH值计算和比对,多层维表仅仅是多个按地址取值的时间,和单层维表时的关联性能基本相当。


    类似的,如果事实表也不大可以全部读入内存时,也可以通过预关联的方式解决事实表与维表的关联问题,提升关联效率。


    预关联可以在系统启动时一次性读入并做好,以后直接使用即可。


    当事实表较大无法全内存时,SPL 提供了外键序号化方法:将事实表中的外键字段值转换为维表对应记录的序号。关联计算时,用序号取出对应维表记录,这样可以获得和外键地址化类似的效果,同样能避免HASH值的计算和比对,大幅提升关联性能。


    主键关联


    有的事实表还有明细表,比如订单和订单明细,二者通过主键和部分主键进行关联,前者作为主表后者作为子表(还有通过全部主键关联的称为同维表,可以看做主子表的特例)。主子表都是事实表,涉及的数据量都比较大。


    SPL为此采用了有序归并方法:预先将外存表按照主键有序存储,关联时顺序取出数据做归并,不需要产生临时缓存,只用很小的内存就可以完成计算。而SQL采用的HASH分堆算法复杂度较高,不仅要计算HASH值进行对比,还会产生临时缓存的读写动作,运算性能很差。


    HASH 分堆技术实现并行困难,多线程要同时向某个分堆缓存数据,造成共享资源冲突;某个分堆关联时又会消费大量内存,无法实施较大的并行数量。而有序归则易于分段并行。数据有序时,子表就可以根据主表键值进行同步对齐分段以保证正确性,无需缓存,且因为占用内存很少可以采用较大的并行数,从而获得更高性能。


    预先排序的成本虽高,但是一次性做好即可,以后就总能使用归并算法实现 JOIN,性能可以提高很多。同时,SPL 也提供了在有追加数据时仍然保持数据整体有序的方案。


    对于主子表关联SPL还可以采用更有效的存储形式将主子表一体化存储,子表作为主表的集合字段,其取值是由与该主表数据相关的多条子表记录构成。这相当于预先实现了关联,再计算时直接取数计算即可,不需要比对,存储量也更少,性能更高。


    存储机制


    高性能离不开有效的存储。SPL也提供了列式存储,在BI计算中可以大幅降低数据读取量以提升读取效率。SPL列存采用了独有的倍增分段技术,相对传统列存分块并行方案要在很大数据量时(否则并行会受到限制)才会发挥优势不同,这个技术可以使SPL列存在数据量不很大时也能获得良好的并行分段效果,充分发挥并行优势。


    SPL还提供了针对数据类型的优化机制,可以显著提升多维分析中的切片运算性能。比如将枚举型维度转换成整数,在查询时将切片条件转换成布尔值构成的对位序列,在比较时就可以直接从序列指定位置取出切片判断结果。还有将多个标签维度(取值是或否的维度,这种维度在多维分析中大量存在)存储在一个整数字段中的标签位维度技术(一个整数字段可以存储16个标签),不仅大幅减少存储量,在计算时还可以针对多个标签同时做按位计算从而大幅提升计算性能。


    有了这些高效机制以后,我们就可以在BI分析中不再使用宽表,转而基于SPL存储和算法做实时关联,性能比宽表还更高(没有冗余数据读取量更小,更快)。


    不过,只有这些还不够,SPL原生语法还不适合BI前端直接访问,这就需要适合的语义转换技术,通过适合的方式将用户操作转换成SPL语法进行查询。


    这就需要DQL了。


    DQL:关联描述技术


    DQL是SPL之上的语义层构建工具,在这一层完成对于SPL数据关联关系的描述(建模)再为上层应用服务。即将SPL存储映射成DQL表,再基于表来描述数据关联关系。



    通过对数据表关系描述以后形成了一种以维度为中心的总线式结构(不同于E-R图中的网状结构),中间是维度,表与表之间不直接相关都通过维度过渡。



    基于这种结构下的关联查询(DQL语句)会很好表达。比如要根据订单表(orders)、客户表(customer)、销售员表(employee)以及城市表(city)查询:本年度华东的销售人员,在全国各销售区的销售额


    用SQL写起来是这样的:


    SELECT
    ct1.area,o.emp_id,sum(o.amount) somt
    FROM
    orders o
    JOIN customer c ON o.cus_id = c.cus_id
    JOIN city ct1 ON c.city_id = ct1.city_id
    JOIN employee e ON o.emp_id = e.emp_id
    JOIN city ct2 ON e.city_id = ct2.city_id
    WHERE
    ct2.area = 'east' AND year(o.order_date)= 2022
    GROUP BY
    ct1.area, o.emp_id

    多个表关联要JOIN多次,同一个地区表要反复关联两次才能查到销售员和客户的所在区域,对于这种情况BI前端表达起来会很吃力,如果将关联开放出来,用户又很难理解。


    那么DQL是怎么处理的呢?


    DQL写法:


    SELECT
    cus_id.city_id.area,emp_id,sum(amount) somt
    FROM
    orders
    WHERE
    emp_id.city_id.area == "east" AND year(order_date)== 2022
    BY
    cus_id.city_id.area,emp_id

    DQL不需要JOIN多个表,只基于orders单表查询就可以了,外键指向表的字段当成属性直接使用,有多少层都可以引用下去,很好表达。像查询客户所在地区通过cus_id.city_id.area一直写下去就可以了,这样就消除了关联,将多表关联查询转化成单表查询。


    更进一步,我们再基于DQL开发BI前端界面就很容易,比如可以做成这样:



    用树结构分多级表达多层维表关联,这样的多维分析页面不仅容易开发,普通业务用户使用时也很容易理解,这就是DQL的效力。


    总结一下,宽表的目的是为了解决BI查询性能和前端工程实现问题,而宽表会带来数据冗余和灵活性差等问题。通过SPL的实时关联技术与高效存储可以解决性能问题,而且性能比宽表更高,同时不存在数据冗余,存储空间也更小(压缩);DQL构建的语义层解决了多维分析前端工程的实现问题,让实时关联成为可能,,灵活性更高(不再局限于宽表的按需建模),界面也更容易实现,应用范围更广。


    SPL+DQL继承(超越)宽表的优点同时改善其缺点,这才是BI该有的样子。


    SPL资料


    收起阅读 »

    IDEA建议:不要在字段上使用@Autowire了!

    在使用IDEA写Spring相关的项目的时候,在字段上使用@Autowired注解时,总是会有一个波浪线提示:Field injection is not recommended. 纳尼?我天天用,咋就不建议了,今天就来一探究竟。 众所周知,在Spring里...
    继续阅读 »

    在使用IDEA写Spring相关的项目的时候,在字段上使用@Autowired注解时,总是会有一个波浪线提示:Field injection is not recommended. 纳尼?我天天用,咋就不建议了,今天就来一探究竟。



    众所周知,在Spring里面有三种可选的注入方式:构造器注入、Setter方法注入、Field注入,我们先来看下这三种注入方式的使用场景。


    构造器注入


    如下所示,使用构造器注入,可以将属性字段设置为final,在Aservice进行实例化时,BService对象必须得提前初始化完成,所以使用构造器注入,能够保证被注入的对象一定不为null。构造器注入适用于对象之间强依赖的场景,但是无法解决的循环依赖问题(因为必须互相依赖对方初始化完成,当然会产生冲突无法解决)。关于循环依赖,推荐阿里的一篇文章 一文详解Spring Bean循环依赖


    @Service
    public class AService {
       private final BService bService;

       @Autowired  //spring framework 4.3之后可以不用在构造方法上标注@Autowired
       public AService(BService bService) {
           this.bService = bService;
      }
    }

    Setter 方法注入


    使用Setter方法进行注入时,Spring会在执行默认的无参构造函数实例化Bean对象之后,调用Setter方法来注入依赖。使用Setter方法注入可以将 required 属性设置为 false,表示若注入的Bean对象不存在,直接跳过注入,不会报错。


    @Service
    public class AService {
       private  BService bService;

       @Autowired(required = false)
       public void setbService(BService bService) {
           this.bService = bService;
      }
    }

    Field注入


    一眼看去,Field注入简洁美观,被大家普遍大量使用。Spring容器会在对象实例化完成之后,通过反射设置需要注入的的字段。


    @Service
    public class AService {
       @Autowired
       private  BService bService;
    }

    为什么IDEA不推荐使用Field注入


    经查阅各方资料,我找到了如下几个比较重要的原因:



    • 可能导致空指针异常:如果创建对象不使用Spring容器,而是直接使用无参构造方法new一个对象,此时使用注入的对象会导致空指针。

    • 不能使用final修饰字段:不使用final修饰,会导致类的依赖可变,进而可能会导致一些不可预料的异常。通常情况下,此时可以使用构造方法注入来声明强制依赖的Bean,使用Setter方法注入来声明可选依赖的Bean。

    • 可能更容易违反单一职责原则:个人认为这点是一个很重要的原因。因为使用字段注入可以很轻松的在类上加入各种依赖,进而导致类的职责过多,但是往往开发者对此不能轻易察觉。而如果使用构造方法注入,当构造方法的参数过多时,就会提醒你,你该重构这个类了。

    • 不利于写单元测试:在单元测试中,使用Field注入,必须使用反射的方式来Mock依赖对象。


    那么替代方案是什么呢?其实上面已经提到了,当我们的类有强依赖于其他Bean时,使用构造方法注入;可选依赖时,使用Setter方法注入(需要自己处理可能出现的引用对象不存在的情况)。


    Spring官方的态度


    Spring 官方文档在依赖注入这一节其实没有讨论字段注入这种方式,重点比较了构造方法注入和Setter注入。可以看到Spring团队强推的还是构造方法注入


    构造方法注入还是Setter注入


    总结


    在Spring中使用依赖注入时,首选构造方法注入,虽然其无法解决循环依赖问题,但是当出现循环依赖时,首选应该考虑的是是否代码结构设计出现问题了,当然,也不排除必须要循环依赖的场景,此时字段注入也有用武之地。


    最后想说的是,平时在使用IDEA的过程中,可能会有一些下划线或飘黄提醒,如果多细心观察,可以学习到很多他人已经总结好的最佳实践经验,有助于自己代码功底的提升,共勉!


    参考文献:


    Spring 官方文档关于依赖注入: docs.spring.io/spring-fram…


    StackOverFlow关于避免使用字段注入的讨论:stackoverflow.com/questi

    ons/3…

    收起阅读 »

    怎么做登录(单点登录)功能?

    登陆是系统最基础的功能之一。这么长时间了,一直在写业务,这个基础功能反而没怎么好好研究,都忘差不多了。今天没事儿就来撸一下。 以目前在接触和学习的一个开源系统为例,来分析一下登陆该怎么做。代码的话我就直接从里面CV了。 简单上个图(有水印。因为穷所以没开会员...
    继续阅读 »

    登陆是系统最基础的功能之一。这么长时间了,一直在写业务,这个基础功能反而没怎么好好研究,都忘差不多了。今天没事儿就来撸一下。


    以目前在接触和学习的一个开源系统为例,来分析一下登陆该怎么做。代码的话我就直接从里面CV了。



    简单上个图(有水印。因为穷所以没开会员)


    怎么做登陆(单点登陆)?.png


    先分析下登陆要做啥



    首先,搞清楚要做什么。


    登陆了,系统就知道这是谁,他有什么权限,可以给他开放些什么业务功能,他能看到些什么菜单?。。。这是这个功能的目的和存在的意义。



    怎么落实?



    怎么实现它?用什么实现?




    我们的项目是Springboot + Vue前后端分离类型的。


    选择用token + redis 实现,权限的话用SpringSecurity来做。




    前后端分离避不开的一个问题就是单点登陆,单点登陆咱们有很多实现方式:CAS中央认证、JWT、token等,咱们这种方式其实本身就是基于token的一个单点登陆的实现方案。


    单点登陆我们改天整理一篇OAuth2.0的实现方式,今天不搞这个。



    上代码



    概念这个东西越说越玄。咱们直接上代码吧。



    接口:

    @PostMapping("/login")
    public AjaxResult login(@RequestBody LoginBody loginBody)
    {
       AjaxResult ajax = AjaxResult.success();
       // 生成令牌
       //用户名、密码、验证码、uuid
       String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                                         loginBody.getUuid());
       ajax.put(Constants.TOKEN, token);
       return ajax;
    }


    用户信息验证交给SpringSecurity



    /**
    * 登录验证
    */

    public String login(String username, String password, String code, String uuid)
    {
       // 验证码开关,顺便说一下,系统配置相关的开关之类都缓存在redis里,系统启动的时候加载进来的。这一块儿的代码就不贴出来了
       boolean captchaEnabled = configService.selectCaptchaEnabled();
       if (captchaEnabled)
      {
           //uuid是验证码的redis key,登陆页加载的时候验证码生成接口返回的
           validateCaptcha(username, code, uuid);
      }
       // 用户验证 -- SpringSecurity
       Authentication authentication = null;
       try
      {
           UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
           AuthenticationContextHolder.setContext(authenticationToken);
           // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername。
           //
           authentication = authenticationManager.authenticate(authenticationToken);
      }
       catch (Exception e)
      {
           if (e instanceof BadCredentialsException)
          {
               AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
               throw new UserPasswordNotMatchException();
          }
           else
          {
               AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
               throw new ServiceException(e.getMessage());
          }
      }
       finally
      {
           AuthenticationContextHolder.clearContext();
      }
       AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
       LoginUser loginUser = (LoginUser) authentication.getPrincipal();
       recordLoginInfo(loginUser.getUserId());
       // 生成token
       return tokenService.createToken(loginUser);
    }

    把校验验证码的部分贴出来,看看大概的逻辑(这个代码封装得太碎了。。。没全整出来)

    /**
    * 校验验证码
    */

    public void validateCaptcha(String username, String code, String uuid)
    {
       //uuid是验证码的redis key
       String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); //String CAPTCHA_CODE_KEY = "captcha_codes:";
       String captcha = redisCache.getCacheObject(verifyKey);
       redisCache.deleteObject(verifyKey);
       if (captcha == null)
      {
           AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
           throw new CaptchaExpireException();
      }
       if (!code.equalsIgnoreCase(captcha))
      {
           AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
           throw new CaptchaException();
      }
    }

    token生成部分


    这里,token



    /**
    * 创建令牌
    */

    public String createToken(LoginUser loginUser)
    {
       String token = IdUtils.fastUUID();
       loginUser.setToken(token);
       setUserAgent(loginUser);
       refreshToken(loginUser);

       Map<String, Object> claims = new HashMap<>();
       claims.put(Constants.LOGIN_USER_KEY, token);
       return createToken(claims);
    }

    刷新token

    /**
    * 刷新令牌
    */

    public void refreshToken(LoginUser loginUser)
    {
       loginUser.setLoginTime(System.currentTimeMillis());
       loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
       // 根据uuid将loginUser缓存
       String userKey = getTokenKey(loginUser.getToken());
       redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
    }

    验证token

    /**
    * 验证令牌
    */

    public void verifyToken(LoginUser loginUser)
    {
       long expireTime = loginUser.getExpireTime();
       long currentTime = System.currentTimeMillis();
       if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
      {
           refreshToken(loginUser);
      }
    }


    注意这里返回给前端的token其实用JWT加密了一下,SpringSecurity的过滤器里有进行解析。


    另外,鉴权时会刷新token有效期,看下面第二个代码块的注释。



    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception
    {
       //...无关的代码删了
       httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
    }

    @Component
    public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
    {
       @Autowired
       private TokenService tokenService;

       @Override
       protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
           throws ServletException, IOException
      {
           LoginUser loginUser = tokenService.getLoginUser(request);
           if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
          {
               //刷新token有效期
               tokenService.verifyToken(loginUser);
               UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
               authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
               SecurityContextHolder.getContext().setAuthentication(authenticationToken);
          }
           chain.doFilter(request, response);
      }
    }


    这个登陆方案里用了token + redis,还有JWT,其实用哪一种方案都可以独立实现,并且两种方案都可以用来做单点登陆。


    这里JWT只是起到个加密的作用,无它。


    作者:harhar
    来源:juejin.cn/post/7184266088652210231

    收起阅读 »

    项目使用redis做缓存,除了击穿,穿透,雪崩,我们还要考虑哪些!!!

    大家好,我是小趴菜,相信大家在项目中都是用过redis,比如用来做一个分布式缓存来提高程序的性能 当使用到了redis来做缓存,那么我们就必须要考虑几个问题,除了缓存击穿,缓存穿透,缓存雪崩,那么我们还需要考虑哪些问题呢? 高并发写 对于高并发情况下,比如直播...
    继续阅读 »

    大家好,我是小趴菜,相信大家在项目中都是用过redis,比如用来做一个分布式缓存来提高程序的性能


    当使用到了redis来做缓存,那么我们就必须要考虑几个问题,除了缓存击穿,缓存穿透,缓存雪崩,那么我们还需要考虑哪些问题呢?


    高并发写


    对于高并发情况下,比如直播下单,直播下单跟秒杀不一样,秒杀是有限定的库存,但是直播下单是可以一直下的,而且是下单越多越好的。比如说我们的库存有10万个,如果这个商品特别火,那么可能一瞬间流量就全都打过来了。虽然我们的库存是提前放到redis中,并不会去访问MySql,那么这时候所有的请求都会打到redis中。


    image.png


    表面看起来确实没问题,但是你有没有想过,即使你做了集群,但是访问的还是只有一个key,那么最终还是会落到同一台redis服务器上。这时候key所在的那台redid就会承载所有的请求,而集群其它机器根本就不会访问到,这时候你确定你的redis能扛住吗???如果这时候读的请求很多,你觉得你的redis能扛住吗?


    所以对于这种情况我们可以采用数据分片的解决方案,比如你有10万个库存,那么这时候可以搞10台redis服务器,每台redis服务器上放1万个库存,这时候我们可以通过用户的ID进行取模,然后将用户流量分摊到10台redis服务器上


    image.png


    所以对于热点数据来说,我们要做的就是将流量进行分摊,让多台redis分摊承载一部分流量,尤其是对于这种高并发写来讲


    高并发读


    使用redis做缓存可以说是我们项目中使用到的最多的了,可能由于平时访问量不高,所以我们的redis服务完全可以承载这么多用户的请求


    但是我们可以想一下,一次reids的读请求就是一次的网络IO,如果是1万次,10万次呢?那就是10万次的网络IO,这个问题我们在工作中是不得不考虑的。因为这个开销其实是很大的,如果访问量太大,redis很有可能就会出现一些问题


    image.png


    我们可以使用本地缓存+redis分布式缓存来解决这个问题,对于一些热点读数据,更新不大的数据,我们可以将数据保存在本地缓存中,比如Guava等工具类,当然本地缓存的过期时间要设置的短一点,比如5秒左右,这时候可以让大部分的请求都落在本地缓存,不用去访问redis


    如果这时候本地缓存没有,那么再去访问redis,然后将redis中的数据再放入本地缓存中即可


    加入了多级缓存,那么就会有相应的问题,比如多级缓存如何保证数据一致性


    总结


    没有完美的方案,只有最适合自己的方案,当你决定采用了某种技术方案的时候,那么势必会带来一些其它你需要考虑的问题,redis也一样,虽然我们使用它来做缓存可以提高我们程序的性能,但是在使用redis做缓存的时候,有些情况我们也是需要考虑到的,对于用户访问量不高来说,我们直接使用redis完全是够用的,但是我们可以假设一下,如果在高并发场景下,我们的方案是否能够支持我们的业务


    作者:我是小趴菜
    来源:juejin.cn/post/7264475859659079736
    收起阅读 »