注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

微博图床挂了!

一直担心的事情还是发生了。 作为hexo多年的使用者,微博图床一直是我的默认选项,hexo+typora+iPic更是我这几年写文章的黄金组合。而图床中,新浪图床一直都是我的默认选项,速度快、稳定同时支持大图片批量上传更是让其成为了众多图床工具的默认选项。虽然...
继续阅读 »

一直担心的事情还是发生了。


作为hexo多年的使用者,微博图床一直是我的默认选项,hexo+typora+iPic更是我这几年写文章的黄金组合。而图床中,新浪图床一直都是我的默认选项,速度快、稳定同时支持大图片批量上传更是让其成为了众多图床工具的默认选项。虽然今年早些的时候,部分如「ws1、ws2……」的域名就已经无法使用了,但通过某些手段还是可以让其存活的,而最近,所有调用的微博图床图片都无法加载并提示“403 Forbidden”了。




💡Tips:图片中出现的Tengine是淘宝在Nginx的基础上修改后开源的一款Web服务器,基本上,Tengine可以被看作一个更好的Nginx,或者是Nginx的超集,详情可参考👉淘宝Web服务器Tengine正式开源 - The Tengine Web Server



刚得知这个消息的时候,我的第一想法其实是非常生气的,毕竟自己这几年上千张图片都是用的微博图床,如今还没备份就被403了,可仔细一想,说到底还是把东西交在别人手里的下场,微博又不是慈善企业,也要控制成本,一直睁一只眼闭一只眼让大家免费用就算了,出了问题还是不太好怪到微博上来的。


那么有什么比较好的办法解决这个问题呢?


查遍了网上一堆复制/粘贴出来的文章,不是开启反向代理就是更改请求头,真正愿意从根本上解决问题的没几个。


如果不想将自己沉淀的博客、文章托管在印象笔记、notion、语雀这些在线平台的话,想要彻底解决这个问题最好的方式是:自建图床!


为了更好的解决问题,我们先弄明白,403是什么,以及我们存在微博上的图片究竟是如何被403的。


403


百度百科,对于403错误的解释很简单



403错误是一种在网站访问过程中,常见的错误提示,表示资源不可用。服务器理解客户的请求,但拒绝处理它,通常由于服务器上文件或目录的权限设置导致的WEB访问错误。



所以说到底是因为访问者无权访问服务器端所提供的资源。而微博图床出现403的原因主要在于微博开启了防盗链。


防盗链的原理很简单,站点在得知有请求时,会先判断请求头中的信息,如果请求头中有Referer信息,然后根据自己的规则来判断Referer头信息是否符合要求,Referer 信息是请求该图片的来源地址。


如果盗用网站是 https 的 协议,而图片链接是 http 的话,则从 https 向 http 发起的请求会因为安全性的规定,而不带 referer,从而实现防盗链的绕过。官方输出图片的时候,判断了来源(Referer),就是从哪个网站访问这个图片,如果是你的网站去加载这个图片,那么 Referer 就是你的网站地址;你的网址肯定没在官方的白名单内,(当然作为可操作性极强的浏览器来说 referer 是完全可以伪造一个官方的 URL 这样也也就也可以饶过限制🚫)所以就看不到图片了。



解决问题


解释完原理之后我们发现,其实只要想办法在自己的个人站点中设置好referer就可以解决这个问题,但说到底也只是治标不治本,真正解决这个问题就是想办法将图片迁移到自己的个人图床上。


现在的图床工具很多,iPic、uPic、PicGo等一堆工具既免费又开源,问题在于选择什么云存储服务作为自己的图床以及如何替换自己这上千张图片。



  1. 选择什么云存储服务

  2. 如何替换上千张图片


什么是OSS以及如何选择


「OSS」的英文全称是Object Storage Service,翻译成中文就是「对象存储服务」,官方一点解释就是对象存储是一种使用HTTP API存储和检索非结构化数据和元数据对象的工具。


白话文解释就是将系统所要用的文件上传到云硬盘上,该云硬盘提供了文件下载、上传等一列服务,这样的服务以及技术可以统称为OSS,业内提供OSS服务的厂商很多,知名常用且成规模的有阿里云、腾讯云、百度云、七牛云、又拍云等。


对于我们这些个人用户来说,这些云厂商提供的服务都是足够使用的,我们所要关心的便是成本💰。


笔者使用的是七牛云,它提供了10G的免费存储,基本已经够用了。


有人会考虑将GitHub/Gitee作为图床,并且这样的文章在中文互联网里广泛流传,因为很多人的个人站点都是托管在GitHub Pages上的,但是个人建议是不要这么做。


首先GitHub在国内的访问就很受限,很多场景都需要科学上网才能获得完整的浏览体验。再加上GitHub官方也不推荐将Git仓库存储大文件,GitHub建议仓库保持较小,理想情况下小于 1 GB,强烈建议小于 5 GB。


如何替换上千张图片


替换文章中的图片链接和“把大象放进冰箱里”步骤是差不多的



  1. 下载所有的微博图床的图片

  2. 上传所有的图片到自己的图床(xx云)

  3. 对文本文件执行replaceAll操作


考虑到我们需要迁移的文件数量较多,手动操作肯定是不太可行的,因此我们可以采用代码的方式写一个脚本完成上述操作。考虑到自己已经是一个成熟的Java工程师了,这个功能就干脆用Java写了。


为了减少代码量,精简代码结构,我这里引入了几个第三方库,当然不引入也行,如果不引入有一些繁琐而又简单的业务逻辑需要自己实现,有点浪费时间了。


整个脚本逻辑非常简单,流程如下:



获取博客文件夹下的Markdown文件


这里我们直接使用hutool这个三方库,它内置了很多非常实用的工具类,获取所有markdown文件也变得非常容易


/**
* 筛选出所有的markdown文件
*/

public static List<File> listAllMDFile() {
List<File> files = FileUtil.loopFiles(VAULT_PATH);
return files.stream()
.filter(Objects::nonNull)
.filter(File::isFile)
.filter(file -> StringUtils.endsWith(file.getName(), ".md"))
.collect(Collectors.toList());
}

获取文件中的所有包含微博图床的域名


通过Hutools内置的FileReader我们可以直接读取markdown文件的内容,因此我们只需要解析出文章里包含微博图床的链接即可。我们可以借助正则表达式快速获取一段文本内容里的所有url,然后做一下filter即可。


/**
* 获取一段文本内容里的所有url
*
* @param content 文本内容
* @return 所有的url
*/

public static List<String> getAllUrlsFromContent(String content) {
List<String> urls = new ArrayList<>();
Pattern pattern = Pattern.compile(
"\\b(((ht|f)tp(s?)\\:\\/\\/|~\\/|\\/)|www.)" + "(\\w+:\\w+@)?(([-\\w]+\\.)+(com|org|net|gov"
+ "|mil|biz|info|mobi|name|aero|jobs|museum" + "|travel|[a-z]{2}))(:[\\d]{1,5})?"
+ "(((\\/([-\\w~!$+|.,=]|%[a-f\\d]{2})+)+|\\/)+|\\?|#)?" + "((\\?([-\\w~!$+|.,*:]|%[a-f\\d{2}])+=?"
+ "([-\\w~!$+|.,*:=]|%[a-f\\d]{2})*)" + "(&(?:[-\\w~!$+|.,*:]|%[a-f\\d{2}])+=?"
+ "([-\\w~!$+|.,*:=]|%[a-f\\d]{2})*)*)*" + "(#([-\\w~!$+|.,*:=]|%[a-f\\d]{2})*)?\\b");
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
urls.add(matcher.group());
}
return urls;
}

下载图片


用Java下载文件的代码在互联网上属实是重复率最高的一批检索内容了,这里就直接贴出代码了。


public static void download(String urlString, String fileName) throws IOException {
File file = new File(fileName);
if (file.exists()) {
return;
}
URL url = null;
OutputStream os = null;
InputStream is = null;
try {
url = new URL(urlString);
URLConnection con = url.openConnection();
// 输入流
is = con.getInputStream();
// 1K的数据缓冲
byte[] bs = new byte[1024];
// 读取到的数据长度
int len;
// 输出的文件流
os = Files.newOutputStream(Paths.get(fileName));
// 开始读取
while ((len = is.read(bs)) != -1) {
os.write(bs, 0, len);
}
} finally {
if (os != null) {
os.close();
}
if (is != null) {
is.close();
}
}
}

上传图片


下载完图片后我们便要着手将下载下来的图片上传至我们自己的云存储服务了,这里直接给出七牛云上传图片的文档链接了,文档里写的非常详细,我就不赘述了👇


Java SDK_SDK 下载_对象存储 - 七牛开发者中心


全局处理


通过阅读代码的细节,我们可以发现,我们的方法粒度是单文件的,但事实上,我们可以先将所有的文件遍历一遍,统一进行图片的下载、上传与替换,这样可以节约点时间。


统一替换的逻辑也很简单,我们申明一个全局Map,


private static final Map<String, String> URL_MAP = Maps.newHashMap();

其中,key是旧的新浪图床的链接,value是新的自定义图床的链接。


我们将listAllMDFile这一步中所获取到的所有文件里的所有链接保存于此,下载时只需遍历这个Map的key即可获取到需要下载的图片链接。然后将上传后得到的新链接作为value存在到该Map中即可。


全文替换链接并更新文件


有了上述这些处理步骤,接下来一步就变的异常简单,只需要遍历每个文件,将匹配到全局Map中key的链接替换成Map中的value即可。


/**
* 替换所有的图片链接
*/

private static String replaceUrl(String content, Map<String, String> urlMap) {
for (Map.Entry<String, String> entry : urlMap.entrySet()) {
String oldUrl = entry.getKey();
String newUrl = entry.getValue();
if (StringUtils.isBlank(newUrl)) {
continue;
}
content = RegExUtils.replaceAll(content, oldUrl, newUrl);
}
return content;
}

我们借助commons-lang实现字符串匹配替换,借助Hutools实现文件的读取和写入。


files.forEach(file -> {
try {
FileReader fileReader = new FileReader(file.getPath());
String content = fileReader.readString();
String replaceContent = replaceUrl(content, URL_MAP);
FileWriter writer = new FileWriter(file.getPath());
writer.write(replaceContent);
} catch (Throwable e) {
log.error("write file error, errorMsg:{}", e.getMessage());
}
});

为了安全起见,最好把文件放在新的目录中,不要直接替换掉原来的文件,否则程序出现意外就麻烦了。


接下来我们只需要运行程序,静待备份结果跑完即可。


以上就是本文的全部内容了,希望对你有所帮助


作者:插猹的闰土
来源:juejin.cn/post/7189651446306963514
收起阅读 »

git merge 和 git rebase的区别

git rebase 让你的提交记录更加清晰可读 git rebase 的使用 rebase 翻译为变基,它的作用和 merge 很相似,用于把一个分支的修改合并到另外一个分支上。 如下图所示,下图介绍了经过 rebase 前后提交历史的变化情况。 现在我们...
继续阅读 »

git rebase 让你的提交记录更加清晰可读


git rebase 的使用


rebase 翻译为变基,它的作用和 merge 很相似,用于把一个分支的修改合并到另外一个分支上。


如下图所示,下图介绍了经过 rebase 前后提交历史的变化情况。



现在我们来用一个例子来解释一下上面的过程。


假设我们现在有2条分支,一个为 master\color{#2196F3}{master} ,一个为 feature/1\color{#2196F3}{feature/1},他们都基于初始的一个提交 add readme\color{#2196F3}{add \ readme} 进行检出分支,之后,master 分支增加了 3.js\color{red}{3.js},和 4.js\color{red}{4.js} 的文件,分别进行了2次提交,feature/1\color{#2196F3}{feature/1} 也增加了 1.js\color{red}{1.js}2.js\color{red}{2.js} 的文件,分别对应以下2条提交记录。


master\color{#2196F3}{master} 分支如下图:



feature/1\color{#2196F3}{feature/1} 分支如下图:



结合起来看是这样的:



此时,切换到 feature/1 分支下,执行 git rebase master ,成功之后,通过 log 查看记录。


如下图所示:可以看到先是逐个应用了 master 分支的更改,然后以 master\color{#2196F3}{master} 分支最后的提交作为基点,再逐个应用 feature/1\color{#2196F3}{feature/1} 的每个更改。



所以,我们的提交记录就会非常清晰,没有分叉,上面演示的是比较顺利的情况,但是大部分情况下,rebase 的过程中会产生冲突的,此时,就需要手动解决冲突,然后使用 git addgit rebase --continue 的方式来处理冲突,完成 rebase,如果不想要某次 rebase 的结果,那么需要使用 git rebase --skip 来跳过这次 rebase


git merge 和 git rebase 的区别


不同于 git rebase的是,git merge 在不是 fast-forward(快速合并)的情况下,会产生一条额外的合并记录,类似 Merge branch 'xxx' into 'xxx' 的一条提交信息。



另外,在解决冲突的时候,用 merge 只需要解决一次冲突即可,简单粗暴,而用 rebase 的时候 ,需要一次又一次的解决冲突。


git rebase 交互模式


在开发中,常会遇到在一个分支上产生了很多的无效的提交,这种情况下使用 rebase 的交互式模式可以把已经发生的多次提交压缩成一次提交,得到了一个干净的提交历史,例如某个分支的提交历史情况如下:



进入交互式模式的方法是执行:


git rebase -i <base-commit>

参数 base-commit 就是指明操作的基点提交对象,基于这个基点进行 rebase 的操作,对于上述提交历史的例子,我们要把最后的一个提交对象 (ac18084\color{#F19E38}{ac18084}) 之前的提交压缩成一次提交,我们需要执行的命令格式是


git rebase -i ac18084

此时会进入一个 vim 的交互式页面,编辑器列出的信息像下列这样。



想要合并这一堆更改,我们要使用 squash 策略进行合并,即把当前的 commit 和它的上一个 commit 内容进行合并, 大概可以表示为下面这样。


pick  ... ...
s ... ...
s ... ...
s ... ...

修改文件后 按下 : 然后 wq 保存退出,此时又会弹出一个编辑页面,这个页面是用来编辑提交的信息,修改为 feat: 更正,最后保存一下,接着使用 git branch 查看提交的 commit 信息,rebase 后的提交记录如下图所示,是不是清爽了很多? rebase 操作可以让我们的提交历史变得更加清晰。




特别注意,只能在自己使用的 feature 分支上进行 rebase 操作,不允许在集成分支上进行 rebase,因为这种操作会修改集成分支的历史记录。



rebase 的风险



patch:【假设本地分支为 dev1,c1 和 c2 是本地往 dev1 分支上做的两次提交】把 dev1 分支上的c1和 c2 “拆”下来,并临时保存成 c1' 和 c2'。git 里将其称为 patch



rebase\color{red}{rebase} 会将当前分支的新提交拆下来,保存成 patch\color{red}{patch},然后合并进其他分支新的 commit\color{red}{commit},最后将 patch\color{red}{patch} 接进当前分支。这是 rebase\color{red}{rebase} 对多条分支的操作。对于单条分支,rebase\color{red}{rebase} 还能够合并多个 commit\color{red}{commit} 单号,将多个提交合并成一个提交。


git rebase -i [commit id]命令能够合并(整改) commit id 之前的所有 commit\color{red}{commit} 单。加上-i选项能够提供一个交互界面,分阶段修改commit信息并 rebase\color{red}{rebase}


但这里就会出现一个问题:如果你合并多个单号时,一不小心合并多了,将别人的提交也合并了,此时你本地的 commit history\color{red}{commit \ history} 和远程仓库的 commit history\color{red}{commit \ history} 不一样了,无论你如何 push\color{red}{push},都无法推送你的代码了。如果你并不记得 rebase\color{red}{rebase} 之前的 HEAD\color{red}{HEAD} 指向的 commit\color{red}{commit}commit ID\color{red}{commit \ ID} 的话,git reflog\color{red}{git \ reflog} 都救不了你。


tips:  你可以 push\color{red}{push} 时带上 f\color{red}{-f} 参数,强制覆盖远程 commit history\color{red}{commit \ history},你这样做估计会被打,因为覆盖之后,团队的其他人的本地 commit history\color{red}{commit \ history} 就与远程的不一样了,都无法推送了。


因此,请保证仅仅对自己私有的提交单进行 rebase\color{red}{rebase} 操作,对于已经合并进远程仓库的历史提交单,不要使用 rebase\color{red}{rebase} 操作合并 commit\color{red}{commit} 单。


作者:d_motivation
来源:juejin.cn/post/7277089907974357052
收起阅读 »

第一次使用缓存,因为没预热,翻车了

缓存不预热会怎么样?我帮大家淌了路。缓存不预热会导致系统接口性能下降,数据库压力增加,更重要的是导致我写了两天的复盘文档,在复盘会上被骂出了翔。 悲惨的上线时刻 事情发生在几年前,我刚毕业时,第一次使用缓存内心很激动。需求场景是虚拟商品页面需要向用户透出库存状...
继续阅读 »

缓存不预热会怎么样?我帮大家淌了路。缓存不预热会导致系统接口性能下降,数据库压力增加,更重要的是导致我写了两天的复盘文档,在复盘会上被骂出了翔。


悲惨的上线时刻


事情发生在几年前,我刚毕业时,第一次使用缓存内心很激动。需求场景是虚拟商品页面需要向用户透出库存状态,提单时也需要校验库存状态是否可售卖。但是由于库存状态的计算包含较复杂的业务逻辑,耗时比较高,在500ms以上。如果要在商品页面透出库存状态那么商品页面耗时增加500ms,这几乎是无法忍受的事情。


如何实现呢?最合适的方案当然是缓存了,我当时设计的方案是如果缓存有库存状态直接读缓存,如果缓存查不到,则计算库存状态,然后加载进缓存,同时设定过期时间。何时写库存呢? 答案是过期后,cache miss时重新加载进缓存。 由于计算逻辑较复杂,库存扣减等用户写操作没有同步更新缓存,但是产品认可库存状态可以有几分钟的状态不一致。为什么呢?


因为仓库有冗余库存,就算库存状态不一致导致超卖,也能容忍。同时库存不足以后,需要运营补充库存,而补充库存的时间是肯定比较长的。虽然补充库存完成几分钟后,才变为可售卖的,产品也能接受。 梳理完缓存的读写方案,我就沉浸于学习Redis的过程。


第一次使用缓存,我把时间和精力都放在Redis存储结构,Redis命令,Redis为什么那么快等方面的关注。如饥似渴的学习Redis知识。


直到上线阶段我也没有意识到系统设计的缺陷。


代码写的很快,测试验证也没有问题。然而上线过程中,就开始噼里啪啦的报警,开始我并没有想到报警这事和我有关。直到有人问我,“XXX,你是不是在上线库存状态的需求?”。


我人麻了,”怎么了,啥事”,我颤抖的问


“商品页面耗时暴涨,赶紧回滚”。一个声音传来


“我草”,那一瞬间,我的血压上涌,手心发痒,心跳加速,头皮发麻,颤抖的手不知道怎么在发布系统点回滚,“我没回滚过啊,咋回滚啊?”


“有降级开关吗”? 一个声音传来。


"没写..."。我回答的时候觉得自己真是二笔,为啥没加降级啊。(这也是复盘被骂的重要原因)


那么如何对缓存进行预热呢?


如何预热缓存


灰度放量


灰度放量实际上并不是缓存预热的办法,但是确实能避免缓存雪崩的问题。例如这个需求场景中,如果我没有放开全量数据,而是选择放量1%的流量。这样系统的性能不会有较大的下降,并且逐步放量到100%。


虽然这个过程中,没有主动同步数据到缓存,但是通过控制放量的节奏,保证了初始化缓存过程中,不会出现较大的耗时波动。


例如新上线的缓存逻辑,可以考虑逐渐灰度放量。


扫描数据库刷缓存


如果缓存维度是商品维度或者用户维度,可以考虑扫描数据库,提前预热部分数据到缓存中。


开发成本较高。除了开发缓存部分的代码,还需要开发扫描全表的任务。为了控制缓存刷新的进度,还需要使用线程池增加并发,使用限流器限制并发。这个方案的开发成本较高。


通过数据平台刷缓存


这是比较好的方式,具体怎么实现呢?


数据平台如果支持将数据库离线数据同步到Hive,Hive数据同步到Kafka,我们就可以编写Hive SQL,建立ETL任务。把业务需要被刷新的数据同步到Kafka中,再消费Kafka,把数据写入到缓存中。在这个过程中通过数据平台控制并发度,通过Kafka 分片和消费线程并发度控制 缓存写入的速率。


这个方案开发逻辑包括ETL 任务,消费Kafka写入缓存。这两部分的开发工作量不大。并且相比扫描全表任务,ETL可以编写更加复杂的SQL,修改后立即上线,无需自己控制并发、控制限流。在多个方面ETL刷缓存效率更高。


但是这个方案需要公司级别支持 多个存储系统之间可以进行数据同步。例如mysql、kafka、hive等。


除了首次上线,是否还有其他场景需要预热缓存呢?


需要预热缓存的其他场景


如果Redis挂了,数据怎么办


刚才提到上线前,一定要进行缓存预热。还有一个场景:假设Redis挂了,怎么办?全量的缓存数据都没有了,全部请求同时打到数据库,怎么办。


除了首次上线需要预热缓存,实际上如果缓存数据丢失后,也需要预热缓存。所以预热缓存的任务一定要开发的,一方面是上线前预热缓存,同时也是为了保证缓存挂掉后,也能重新预热缓存。


假如有大量数据冷启动怎么办


假如促销场景,例如春节抢红包,平时非活跃用户会在某个时间点大量打开App,这也会导致大量cache miss,进而导致雪崩。 此时就需要提前预热缓存了。具体的办法,可以考虑使用ETL任务。离线加载大量数据到Kafka,然后再同步到缓存。


总结



  1. 一定要预热缓存,不然线上接口性能和数据库真的扛不住。

  2. 可以通过灰度放量,扫描全表、ETL数据同步等方式预热缓存

  3. Redis挂了,大量用户冷启动的促销场景等场景都需要提前预热缓存。


作者:他是程序员
来源:juejin.cn/post/7277461864349777972
收起阅读 »

王兴入局大模型!美团耗资21亿拿下光年之外100%股权

【新智元导读】 正式官宣!美团收购光年之外全部权益,斥资20.65亿。**** 官宣了!美团以20.65亿人民币收购光年之外。 就在刚刚,美团在港交所公告,已订立交易协议收购光年之外的全部权益。 总代价包括现金233,673,600美元;债务承担人民币366,...
继续阅读 »
【新智元导读】 正式官宣!美团收购光年之外全部权益,斥资20.65亿。****

官宣了!美团以20.65亿人民币收购光年之外。


就在刚刚,美团在港交所公告,已订立交易协议收购光年之外的全部权益。


总代价包括现金233,673,600美元;债务承担人民币366,924,000元;现金人民币1.00元。




于公告日期,光年之外的净现金总额约为285,035,563美元。转让协议交割完成后,美团将持有光年之外100%权益。


前几天,光年之外联合创始人王慧文因健康问题暂时离岗引发许多人的关注。


甚至,外界关心诸多的是他的停职对公司造成哪些影响。




美团在公告中对于并购的解释是,通过收购事项获得领先的AGI技术及人才,有机会加强其于快速增长的人工智能行业中的竞争力。


这次,美团出手,意味着光年之外在后续运营有了足够资金支持。


同时,对美团来说,大模型能对未来业务转型也将产生有利帮助。


美团拿下光年之外



其实,在外界看来,美团收购光年之外,就像是板凳钉钉的事。


从感性层面讲,王兴与王慧文是清华的室友,在创业路上并肩作战。王慧文入局大模型后,王兴紧接着应声跟进。


在大模型爆火后,美团CEO王兴也对此表示极大的关注,甚至,在3月份还投资光年之外。


当时,王兴表示「AI大模型让我既兴奋于即将创造出来的巨大生产力,又忧虑它未来对整个世界的冲击。老王和在创业路上同行近二十年,既然他决心拥抱这次大浪潮,那我必须支持。」




从理性层面讲,自2019年美团将战略升级为「零售+科技」后,不论是王兴本人,还是公司来讲,对AI也投入非常大的兴趣。


当前,大模型已经成为兵家必争之地,国内许多头部科技纷纷入局。


据「豹变」独家报道,美团做大模型,已经有2个多月,几乎是与王兴投资光年之外同步进行的。


据称,算法团队正积极扩招,甚至还在筹划成立单独的「平台部门」,帮助美团大模型通过具体的商业化形式落地。


对美团来讲,智能配送系统、外卖无人车等场景,都需要AI驱动。


收购光年之外后,美团能够将大模型的能力,与自家核心业务相结合,比如外卖、本地生活服务等等。




此外,还能够在客服、物流、产品体验等各种场景中实现应用,将大模型能力与场景深度融合。


美团方面表示,并购完成后,将支持光年团队继续在大模型领域进行探索和研究。


所以说,美团的未来还是值得期待的。


而前几日,王慧文病倒的消息,让外界猜测纷纷,比如融资不顺利,或团队组建困难。


有国内媒体澄清道,光年之外A轮融资已经完成一个月,融资到账实际金额远高于外部报道的2.3亿美元,网传的“融资不顺利”消息,属于谣言。


此次美团在港交所的公告,也证实了这一点。


而在人才组队上,光年之外也进展顺利。


在成立后不久,光年之外就以换股形式收购了一流科技,原核心技术团队被保留。


而在两个月内,光年之外的研发团队规模就已经在70人左右,团队在算法等领域,研发经验丰富。


这样一支已经组建成熟的团队,在当下的大模型之战中,无疑属于稀缺资源。


美团选择收购光年之外,显然也是经过深思熟虑。


VC平稳退出



光年之外在6月初刚刚完成了的这笔2.3亿美元的融资,由源码资本领投。


腾讯、五源资本和快手创始人宿华也参与了这次融资。


从港交所披露的信息来看,除了6月初的这轮融资,红杉中国也在前期对光年之外进行了投资。


当王慧文因病离开光年之外的领导岗位之后,这些前期投资的VC都因为这突发的黑天鹅事件,可能面临着投资打水漂的风险。


但是随着美团的出手收购,这些参与光年之外的投资至少能在一定程度上落袋为安。


不用再担心因为被投公司核心创始人离职给被投公司带来的巨大影响。


王慧文辞任董事



此前,大模型创业4个月,王慧文就因身体原因停职休养。


紧接着,美团在港交所公布,王慧文已提出辞去公司非执行董事、公司董事会提名委员会成员和公司授权代表职务,自6月26日起生效。




在王慧文卸任董事后,美团宣布,执行董事穆荣均已获委任为授权代表,自2023年6月26日起生效。


另外,提名委员会将由冷雪松先生和沈向洋博士组成,冷雪松继续担任提名委员会主席。


在另一份公告中,美团更新了董事会成员。王兴和穆荣均担任公司执行董事,沈南鹏为非执行董事,欧高敦、冷雪松、沈向洋是独立非执行董事。



参考资料:


www1.hkexnews.hk/listedco/li…


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

开发一个网站,用户密码你打算怎么存储

我们在开发网站或者APP时,首先要解决的问题,就是如何安全地传输和存储用户的密码。一些大公司的用户数据库泄露事件也时有发生,带来非常大的负面影响。因此,如何安全传输存储用户密码,是每位程序员必备的基础。本文将跟大家一起学习,如何安全传输存储用户的密码。 1....
继续阅读 »

我们在开发网站或者APP时,首先要解决的问题,就是如何安全地传输和存储用户的密码。一些大公司的用户数据库泄露事件也时有发生,带来非常大的负面影响。因此,如何安全传输存储用户密码,是每位程序员必备的基础。本文将跟大家一起学习,如何安全传输存储用户的密码。


image.png


1. 如何安全地传输用户的密码


要拒绝用户密码在网络上裸奔,我们很容易就想到使用https协议,那先来回顾下https相关知识吧~


1.1 https 协议


image.png



  • 「http的三大风险」


为什么要使用https协议呢?http它不香吗? 因为http是明文信息传输的。如果在茫茫的网络海洋,使用http协议,有以下三大风险:




  • 窃听/嗅探风险:第三方可以截获通信数据。

  • 数据篡改风险:第三方获取到通信数据后,会进行恶意修改。

  • 身份伪造风险:第三方可以冒充他人身份参与通信。



如果传输不重要的信息还好,但是传输用户密码这些敏感信息,那可不得了。所以一般都要使用https协议传输用户密码信息。



  • 「https 原理」


https原理是什么呢?为什么它能解决http的三大风险呢?



https = http + SSL/TLS, SSL/TLS 是传输层加密协议,它提供内容加密、身份认证、数据完整性校验,以解决数据传输的安全性问题。



为了加深https原理的理解,我们一起复习一下 一次完整https的请求流程吧~


image.png




  1. 客户端发起https请求

  2. 服务器必须要有一套数字证书,可以自己制作,也可以向权威机构申请。这套证书其实就是一对公私钥。

  3. 服务器将自己的数字证书(含有公钥、证书的颁发机构等)发送给客户端。

  4. 客户端收到服务器端的数字证书之后,会对其进行验证,主要验证公钥是否有效,比如颁发机构,过期时间等等。如果不通过,则弹出警告框。如果证书没问题,则生成一个密钥(对称加密算法的密钥,其实是一个随机值),并且用证书的公钥对这个随机值加密。

  5. 客户端会发起https中的第二个请求,将加密之后的客户端密钥(随机值)发送给服务器。

  6. 服务器接收到客户端发来的密钥之后,会用自己的私钥对其进行非对称解密,解密之后得到客户端密钥,然后用客户端密钥对返回数据进行对称加密,这样数据就变成了密文。

  7. 服务器将加密后的密文返回给客户端。

  8. 客户端收到服务器发返回的密文,用自己的密钥(客户端密钥)对其进行对称解密,得到服务器返回的数据。




  • 「https一定安全吗?」


https的数据传输过程,数据都是密文的,那么,使用了https协议传输密码信息,一定是安全的吗?其实不然




  • 比如,https 完全就是建立在证书可信的基础上的呢。但是如果遇到中间人伪造证书,一旦客户端通过验证,安全性顿时就没了哦!平时各种钓鱼不可描述的网站,很可能就是黑客在诱导用户安装它们的伪造证书!

  • 通过伪造证书,https也是可能被抓包的哦。



1.2 对称加密算法


既然使用了https协议传输用户密码,还是 「不一定安全」,那么,我们就给用户密码 「加密再传输」 呗~


加密算法有 「对称加密」「非对称加密」 两大类。用哪种类型的加密算法 「靠谱」 呢?



对称加密:加密和解密使用 「相同密钥」 的加密算法。



image.png
常用的对称加密算法主要有以下几种哈:


image.png
如果使用对称加密算法,需要考虑 「密钥如何给到对方」 ,如果密钥还是网络传输给对方,传输过程,被中间人拿到的话,也是有风险的哦。


1.3 非对称加密算法


再考虑一下非对称加密算法呢?



「非对称加密:」 非对称加密算法需要两个密钥(公开密钥和私有密钥)。公钥与私钥是成对存在的,如果用公钥对数据进行加密,只有对应的私钥才能解密。



image.png


常用的非对称加密算法主要有以下几种哈:


image.png



如果使用非对称加密算法,也需要考虑 「密钥公钥如何给到对方」 ,如果公钥还是网络传输给对方,传输过程,被中间人拿到的话,会有什么问题呢?「他们是不是可以伪造公钥,把伪造的公钥给客户端,然后,用自己的私钥等公钥加密的数据过来?」 大家可以思考下这个问题哈~



我们直接 「登录一下百度」 ,抓下接口请求,验证一发大厂是怎么加密的。可以发现有获取公钥接口,如下:


image.png
再看下登录接口,发现就是RSA算法,RSA就是 「非对称加密算法」 。其实百度前端是用了JavaScript库 「jsencrypt」 ,在github的star还挺多的。


image.png
因此,我们可以用 「https + 非对称加密算法(如RSA)」 传输用户密码~


2. 如何安全地存储你的密码?


假设密码已经安全到达服务端啦,那么,如何存储用户的密码呢?一定不能明文存储密码到数据库哦!可以用 「哈希摘要算法加密密码」 ,再保存到数据库。



哈希摘要算法:只能从明文生成一个对应的哈希值,不能反过来根据哈希值得到对应的明文。



2.1  MD5摘要算法保护你的密码


MD5 是一种非常经典的哈希摘要算法,被广泛应用于数据完整性校验、数据(消息)摘要、数据加密等。但是仅仅使用 MD5 对密码进行摘要,并不安全。我们看个例子,如下:


public class MD5Test {  
    public static void main(String[] args) {
        String password = "abc123456";
        System.out.println(DigestUtils.md5Hex(password));
    }
}

运行结果:
0659c7992e268962384eb17fafe88364


在MD5免费破解网站一输入,马上就可以看到原密码了。。。


image.png
试想一下,如果黑客构建一个超大的数据库,把所有20位数字以内的数字和字母组合的密码全部计算MD5哈希值出来,并且把密码和它们对应的哈希值存到里面去(这就是 「彩虹表」 )。在破解密码的时候,只需要查一下这个彩虹表就完事了。所以 「单单MD5对密码取哈希值存储」 ,已经不安全啦~


2.2  MD5+盐摘要算法保护用户的密码


那么,为什么不试一下MD5+盐呢?什么是 「加盐」



在密码学中,是指通过在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符,这种过程称之为“加盐”。



用户密码+盐之后,进行哈希散列,再保存到数据库。这样可以有效应对彩虹表破解法。但是呢,使用加盐,需要注意一下几点:




  • 不能在代码中写死盐,且盐需要有一定的长度(盐写死太简单的话,黑客可能注册几个账号反推出来)

  • 每一个密码都有独立的盐,并且盐要长一点,比如超过 20 位。(盐太短,加上原始密码太短,容易破解)

  • 最好是随机的值,并且是全球唯一的,意味着全球不可能有现成的彩虹表给你用。



2.3 提升密码存储安全的利器登场,Bcrypt


即使是加了盐,密码仍有可能被暴力破解。因此,我们可以采取更 「慢一点」 的算法,让黑客破解密码付出更大的代价,甚至迫使他们放弃。提升密码存储安全的利器~Bcrypt,可以闪亮登场啦。



实际上,Spring Security 已经废弃了 MessageDigestPasswordEncoder,推荐使用BCryptPasswordEncoder,也就是BCrypt来进行密码哈希。BCrypt 生而为保存密码设计的算法,相比 MD5 要慢很多。



看个例子对比一下吧:


public class BCryptTest {  

    public static void main(String[] args) {
        String password = "123456";
        long md5Begin = System.currentTimeMillis();
        DigestUtils.md5Hex(password);
        long md5End = System.currentTimeMillis();
        System.out.println("md5 time:"+(md5End - md5Begin));
        long bcrytBegin = System.currentTimeMillis();
        BCrypt.hashpw(password, BCrypt.gensalt(10));
        long bcrytEnd = System.currentTimeMillis();
        System.out.println("bcrypt Time:" + (bcrytEnd- bcrytBegin));
    }
}

运行结果:


md5 time:47


bcrypt Time:1597


粗略对比发现,BCrypt比MD5慢几十倍,黑客想暴力破解的话,就需要花费几十倍的代价。因此一般情况,建议使用Bcrypt来存储用户的密码


3. 总结



  • 因此,一般使用https 协议 + 非对称加密算法(如RSA)来传输用户密码,为了更加安全,可以在前端构造一下随机因子哦。

  • 使用BCrypt + 盐存储用户密码。

  • 在感知到暴力破解危害的时候,「开启短信验证、图形验证码、账号暂时锁定」 等防御机制来抵御暴力破解。


作者:小王和八蛋
来源:juejin.cn/post/7260140790546251831
收起阅读 »

揭秘外卖平台的附近公里设计

背景 相信大家都有点外卖的时候去按照附近公里排序的习惯,那附近的公里是怎么设计的呢?今天shigen带你一起揭秘。 分析 我们先明确一下需求,每个商家都有一个地址对吧,我们也有一个地址,我们点餐的时候,就是以我们自己所在的位置为圆心,向外辐射,这一圈上有一堆的...
继续阅读 »

背景


相信大家都有点外卖的时候去按照附近公里排序的习惯,那附近的公里是怎么设计的呢?今天shigen带你一起揭秘。


分析


我们先明确一下需求,每个商家都有一个地址对吧,我们也有一个地址,我们点餐的时候,就是以我们自己所在的位置为圆心,向外辐射,这一圈上有一堆的商家。类似我下方的图展示:



想到了位置,我们自然想到了卫星定位,想到了二维的坐标。那这个需求我们有什么好的设计方案吗?


redis的GEO地理位置坐标这个数据结构刚好能解决我们的需求。


GEO


GEO 是一种地理空间数据结构,它可以存储和处理地理位置信息。它以有序集合(Sorted Set)的形式存储地理位置的经度和纬度,以及与之关联的成员。


以下是 Redis GEO 的一些常见操作:



  1. GEOADD key longitude latitude member [longitude latitude member ...]:将一个或多个地理位置及其成员添加到指定的键中。 示例:GEOADD cities -122.4194 37.7749 "San Francisco" -74.0059 40.7128 "New York"

  2. GEODIST key member1 member2 [unit]:计算两个成员之间的距离。 示例:GEODIST cities "San Francisco" "New York" km

  3. GEOPOS key member [member ...]:获取一个或多个成员的经度和纬度。 示例:GEOPOS cities "San Francisco" "New York"

  4. GEORADIUS key longitude latitude radius unit [WITHCOORD] [WITHDIST] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]:根据给定的经纬度和半径,在指定范围内查找与给定位置相匹配的成员。 示例:GEORADIUS cities -122.4194 37.7749 100 km WITHDIST COUNT 5


Redis 的 GEO 功能可用于许多应用场景,例如:



  • 位置服务:可以存储城市、商店、用户等位置信息,并通过距离计算来查找附近的位置。

  • 地理围栏:可以存储地理围栏的边界信息,并检查给定的位置是否在围栏内。

  • 最短路径:可以将城市或节点作为地理位置,结合图算法,查找两个位置之间的最短路径。

  • 热点分析:可以根据位置信息生成热力图,统计热门区域或目标位置的访问频率。


Redis 的 GEO 功能提供了方便且高效的方式来存储和操作地理位置信息,使得处理地理空间数据变得更加简单和快速。



默默的说一句,redis在路径规划下边竟然也这么厉害!



好的,那我们就来开始实现吧。今天我的操作还是用代码来展示,毕竟经纬度在控制台输入可能会出错。


代码实现


今天的案例是将湖北省武汉市各个区的数据存储在redis中,并以我所在的位置计算离别的区距离,以及我最近10km内的区。数据来源



我的测试代码如下,其中的运行结果也在对应的注释上有显示。



因为代码图片的宽度过长,导致代码字体很小,在移动端可尝试横屏观看;在PC端可尝试右键在新标签页打开图片。




以上的代码案例也参考:Redis GEO 常用 RedisTemplate API(Java),感谢作者提供的代码案例支持。


总结


对于需要存储地理数据和需要进行地理计算的需求,可以尝试使用redis进行解决。当然,elasticsearch也提供了对应的数据类型支持。有机会的话,shigen也会逐一的展开分析讲解。感谢伙伴们的支持。


shigen一起,每天不一样!


作者:shigen01
来源:juejin.cn/post/7275595571733282853
收起阅读 »

揭秘 Google Cloud Next '23:生成式 AI 的探索之路与开发范式变革

戳这里了解更多 前言: 8 月底,谷歌以「AI 与云科技驱动创新」为题,举办了为期三天的 Google Cloud Next ’23 大会,展示了谷歌在基础架构、数据和 AI、Workspace 协作和信息安全解决方案等全系列产品不断创新的成果。 乍看之下似乎...
继续阅读 »


戳这里了解更多


前言:


8 月底,谷歌以「AI 与云科技驱动创新」为题,举办了为期三天的 Google Cloud Next ’23 大会,展示了谷歌在基础架构、数据和 AI、Workspace 协作和信息安全解决方案等全系列产品不断创新的成果。


乍看之下似乎又是一场「大而全」的行业大会,但全程看完之后会明显的感受到,本次大会的内容全部围绕住了一个重心 —— 「生成式 AI」。


生成式 AI 作为近一两年最热门的技术话题没有之一,大家的谈论早已经超出了技术的范畴。如何应用、如何融合、如何落地,各行各业都在探索生成式 AI 带来的可能性。但除了 ChatGPT 这类的聊天机器人,似乎还没有特别成功的落地工具或者应用,哪怕是技术本源所在的研发领域也如是。


谷歌这次,似乎给出了一个参考答案。


一、Google Next '23:生成式 AI 的探索之路


生成式 AI 与传统 AI 技术最根本的区别在于前者通过理解自然语言创建内容,而后者依赖的是编程语言,这是生成式 AI 技术的关键变革特征,也是以前从未有过的能力。并且生成式 AI 能够以文本、图像、视频、音频和代码的形式生成新内容,而传统的 AI 系统训练计算机对人类行为、商业结果等进行预测。


对于许多人来说,第一次切身感知到生成式 AI 技术就是通过 ChatGPT。作为一种人工智能聊天机器人,在 2022 年 11 月迅速风靡全球。


大部分人不知道的是,ChatGPT 在架构层使用的是 Transformer 这一语言处理架构,该架构实际上便是谷歌在 2017 年的论文《Attention Is All You Need》中提出的。


谷歌作为一家成立了 25 年的公司,曾经在搜索、邮箱等领域取得了很多成绩,但在 AI 领域却面临了一些质疑。此前有媒体表示“谷歌在人工智能领域没有‘秘密武器’,无法赢得这场竞争。”而今年 5 月份的 Google I/O 以及前几日的 Google Cloud Next '23,可能正是在某种程度上回击了这种言论。


正如 Alphabet 和谷歌首席执行官桑达尔·皮查伊 (Sundar Pichai) 在活动开幕式上表示:


在过去几年与企业领导者的交谈中,我听到了一个类似的主题。从桌面到移动,到云,再到现在的人工智能,他们需要的是一直走在技术突破前沿的合作伙伴。很多转变确实令人兴奋,但同时也会带来不确定性。向人工智能的转变无疑就是如此。


作为一家公司,我们已经为这一时刻准备了一段时间。在过去的七年里,我们采取了人工智能先行的方法,应用人工智能使我们的产品从根本上更加可用。我们相信,让人工智能为每个人带来帮助,是我们在未来十年完成使命的最重要方式。


先内部小规模测试,再面向大众开放成熟的能力。谷歌也许确实没有“秘密武器”,但可能重点在于并不需要“秘密”,准备好之后,拿出来大家正面比划一下。这次的大会中,谷歌便亮出了其武器:


1**、**Cloud TPU v5e


生成式 AI 带来许多先进的功能,并可广泛使用于各种应用,但不可否认的是更加迫切的需要更先进、更强大的基础架构,设计和构建计算基础设施的传统方法已不足以满足生成式 AI 和大语言模型 (LLM) 等新兴工作负载的需求。为了解决这个问题,谷歌推出了 Cloud TPU v5e,一款最新且最具成本效益的 TPU。


TPU 是专门为大型人工智能模型的训练和推理而设计的定制人工智能芯片。客户可以使用单个 Cloud TPU 平台来运作大规模 AI 训练和推理。根据大会公开信息展示,Cloud TPU v5e 可扩展到数万个芯片并针对效率进行了优化。与 Cloud TPU v4 相比,每美元的训练效率可提升 2 倍,每美元的推论效率可提升 2.5 倍。


2**、**Vertex AI


在 2021 年 Google I/O 大会中,谷歌推出了 Vertex AI 托管式机器学习平台,用来帮助开发者更轻松地构建、部署和维护其机器学习模型。在本次的大会上,则正式推出了 Vertex AI 的搜索和对话功能,并将 ML 模型数量增加到 100 多个,这些模型都依据不同任务和不同大小进行了优化,包括文本、聊天、图像、语音、软件代码等等。


为了进一步平衡用户使用大模型进行建模的灵活性,以及他们可以生成的场景与推理成本以及微调能力,谷歌还为 Vertex AI 带来了扩展功能和 Grounding 等新的功能和工具。


借助 Vertex AI 扩展功能,开发者可以将 Model Garden 模型库中的模型与实时数据、专有数据或第三方平台(如 CRM 系统或电子邮件)连接起来,从而提供即时信息、集成公司数据并代表用户采取行动。这为生成式 AI 应用程序开辟了无限的新可能性。


Grounding 则是适用于 Vertex AI 基础模型、搜索及对话(Search and Conversation)的一项服务,可以协助客户将回复纳入企业自身的数据中,以提供更准确的回复内容。这一功能的重点在于可以一定程度上避免现阶段 AI 的“胡言乱语”,从而规避一些风险或者问题。


3**、**Duet AI


在 5 月的 I/O 大会上,Google Cloud 推出了 Duet AI。官方将其描述为“一位重要的协作伙伴、教练、灵感来源,和生产力推进器”,比如将 Docs 大纲转换成 Slides 中的演示文档,根据表格中的数据生成对应的图表;或者把 Duet AI 当做一个创作型的工具,用它来撰写电子邮件、生成图像、做会议纪要、检查文章的语法错误等等。


但当时的 Duet AI 只能在 Workspace 中使用,这次则扩展到了 Google Cloud 和 BigQuery 中,并推出更多适用的 AI 功能。例如 BigQuery 中的 Duet AI 旨在通过生成完整的函数和代码块,让用户专注于逻辑结果。它还可以建议和编写 Python 代码和 SQL 查询。这将进一步发挥 Duet AI "编码专家、软件可靠性工程师、数据库专家、数据分析专家和网络安全顾问 "的作用。


数据是生成式 AI 的核心,不难看出谷歌这次的更新迭代正式为了帮助数据团队进一步提高生产力,协助组织发挥数据及 AI 的最大潜力。


二、一些后续思考:生成式 AI 带来的开发范式变革


从基建、到平台再到应用,草蛇灰线,伏脉千里。谷歌在生成式 AI 领域的探索,其实并不像大家所想的有些“掉队”,而是在另一个维度提前布局。


25 年来,谷歌不断投资数据中心和网络,现在已经拥有涵盖 38 个云区域的全球网络,根据官方所说,目标是在 2030 年完全实现全天候采用无碳能源维持运营。谷歌的 AI 基础架构也在业界占据很大的份额,有超过 70% 的生成式 AI 独角兽公司和超过一半获得融资的生成式 AI 初创公司,都是 Google Cloud 客户。


"我们从每一层开始。这是对整个堆栈的重新构想。"这是英伟达的黄仁勋在 Google Cloud Next '23 中传递的一个态度,"生成式人工智能正在彻底改变计算堆栈的每一层。我们两家公司(英伟达和谷歌)拥有世界上最有才华的两支计算科学团队,将为生成式人工智能重新发明云基础设施。"


开发者关注的,是如何借助生成式 AI 的能力&工具提效;企业关注的,是如何借助生成式 AI 来迭代业务产品抢占市场心智。但对谷歌这类“搞基建”的公司而言,关注堆栈的每一层、关注堆栈的整体结构,才有可能推进技术的发展,实现传统开发范式的变革。


今年年初,谷歌推出了 Security AI Workbench,这是业界首创的可扩展平台,由谷歌的新一代安全性大语言模型 Sec-PaLM 2 驱动,结合了谷歌独有的观测技术,能帮助开发者掌握不断变化的安全性威胁,并针对网络安全操作进行微调。


几周前,谷歌推出 Chronicle CyberShield,能解决数据孤岛的问题,也能集中管理安全性数据,并统一规划处理方式。


“我们正处于一个由人工智能推动的全新数字化转型时代,”Google Cloud 首席执行官库里安说,“这项技术已经在改善企业的运营方式以及人类之间的互动方式。它正在改变医生照顾病人的方式、人们沟通的方式,甚至我们在工作中的安全方式。而这仅仅是个开始。”


生成式 AI 通过 ChatGPT 类的工具产品,已经在艺术创作、代码生成等领域带来了未曾设想过的便利,随着基础设施的迭代演进,相信现阶段的开发范式变革,可能真的仅仅是个开始。


结语:


生成式 AI 兴起之后,业界纷纷提出“想象力等于生产力”之类的观点,并借助一些场景的应用为佐证。谷歌这次的大会发布,无论是对生成式 AI 技术的推动,还是开发工具&服务的迭代,都给了我们更多的信心与想象的方向。


无论是从 AI 最佳化的基础架构,到注入了生成式 AI 强大功能的数据分析和信息安全服务;还是从增加了更多新模型和工具的 Vertex AI 平台,到扩大了支持 Duet AI 的 Workspace 和 Google Cloud,这些技术都是难得的探索与尝试,这些演变或者变革都是迈向下一次重大演变的正确方向的垫脚石。


对于开发者这一最了解技术本质的人群而言,我们能做的就是拥抱变化与发展,与企业、社区、生态一起,持续探索与创新。在变革到来前,找到要去的方向;在变革到来后,找到自己的位置。


Tips:会后的配套学习资料,给你准备好了!


为了让中国的开发者们更好地 Get 新技术、新发展,Google Cloud 今年同样安排了会后的配套系列课程 —— 「Next ’23 中文精选课」。


今年的课程将聚焦 AI/ML、安全合规、数据库、数据分析、DevOps、应用程序开发等领域,解读最新技术发布与行业实践应用,解读 Next ’23 发布的 100 项创新成果 。


发布会中没来得及讲的、没讲完的,都在这次的课程中了👌


据官方的信息展示,这次的中文精选课不仅有技术干货,更给开发者提供了多种互动方式体验,以及一批 Google Cloud 官方周边礼品,旅行颈枕、无线鼠标、电竞游戏耳机、蓝牙音箱,甚至还有 Google 25 周年纪念版安卓小人!


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

很佩服的一个Google大佬,离职了。。

这两天,科技圈又有一个突发的爆款新闻相信不少同学都已经看到了。 那就是75岁的计算机科学家Geoffrey Hinton从谷歌离职了,从而引起了科技界的广泛关注和讨论。 而Hinton自己也证实了这一消息。 提到Geoffrey Hinton这个名字,对于...
继续阅读 »

这两天,科技圈又有一个突发的爆款新闻相信不少同学都已经看到了。


那就是75岁的计算机科学家Geoffrey Hinton从谷歌离职了,从而引起了科技界的广泛关注和讨论。



而Hinton自己也证实了这一消息。




提到Geoffrey Hinton这个名字,对于一些了解过AI人工智能和机器学习等领域的同学来说,应该挺熟悉的。


Hinton是一位享誉全球的人工智能专家,被誉为“神经网络之父”、“深度学习的鼻祖”、“人工智能教父”等等,在这个领域一直以来都是最受尊崇的泰斗之一。



作为人工智能领域的先驱,他的工作和成就也对该领域的后续发展产生了深远的影响。




其实算一下时间,距离Hinton 2013年加入谷歌,已经也有十个年头了。


据报道,Hinton在4月份其实就提出了离职,并于后来直接与谷歌CEO劈柴哥(Sundar Pichai)进行了交谈。


Hinton在接受媒体访谈时表示,他非常关注人工智能的风险,并表示对自己多年的工作和研究存在遗憾。


正当大家都在好奇Hinton离职原因的时候,Hinton自己却表示,这样一来可以更加自由地讨论人工智能的风险。





1947年,Geoffrey Hinton出生于英国温布尔登的一个知识分子家庭。



他的父亲Howard Everest Hinton是一个研究甲壳虫的英国昆虫学家,而母亲Margaret Clark则是一名教师。


除此之外,他的高曾祖父George Boole还是著名的逻辑学家,也是现代计算科学的基础布尔代数的发明人,而他的叔叔Colin Clark则是一个著名的经济学家。


如此看来,Hinton家庭里的很多成员都在学术和研究方面都颇有造诣。




Hinton主要从事神经网络和机器学习的研究,在AI领域做出过许多重要贡献,其中最著名的当属他在神经网络领域所做的研究工作。



他在20世纪80年代就已经开启了反向传播算法(Back Propagation, BP算法)的研究,并将其应用于神经网络模型的训练中。这一算法被广泛应用于语音识别、图像识别和自然语言处理等领域。



除此之外,Hinton还在卷积神经网络(Convolutional Neural Networks,CNN)、深度置信网络(Deep Belief Networks,DBN)、递归神经网络(Recursive Neural Networks,RNN)、胶囊网络(Capsule Network)等领域做出了重要贡献。




2013年,Hinton加入Google,同时把机器学习相关的很多技术带进了谷歌,同时融合到谷歌的多项业务之中。



2019年3月,ACM公布了2018年度的图灵奖得主。


图灵奖大家都知道,是计算机领域的国际最高奖项,也被誉为“计算机界的诺贝尔奖”。


而Hinton则与蒙特利尔大学计算机科学教授Yoshua Bengio和Meta首席AI科学家Yann LeCun一起因为研究神经网络而获得了该年度的图灵奖,以表彰他们在对应领域所做的杰出贡献。



除此之外,Hinton在他的学术生涯中发表了数百篇论文,这些论文中提出了许多重要的理论和方法,涵盖了人工智能、机器学习、神经网络、计算机视觉等多个领域。


而且他的论文被引用的次数也是惊人,这对于这些领域的研究和发展都产生了重要的影响。





除了自身在机器学习方面的造诣很高,Hinton同时也是一个优秀的老师。


Hinton带过很多大牛学生,其中不少都被像苹果、Google等这类硅谷科技巨头所挖走,在对应的公司里领导着人工智能相关领域的研究。


这其中最典型的就是Ilya Sutskever,他是Hinton的学生,同时他也是最近大名鼎鼎的OpenAI公司的联合创始人和首席科学家。



聊到这里,不得不感叹大佬们的创造力以及对这个领域所作出的贡献。


既然离开了谷歌,那也就意味着将开启一段新的旅程,也期待着大佬后续给大家带来更多精彩的故事。


好了,以上就是今天的文章内容,感谢大家的收看,我们下期见。


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

一个简单的TODO,原来这么好用

平常我们再开发的时候,遇到一些想要之后去编写的部分,或者说再开发某个模块的时候,突然被事情打断,暂时无法实现的代码,以后才会去修复的bug的时候,要如何精准快速的去定位到那个位置呢? 下面来介绍一个很多人会忽律的标记TODO TODO是一个特殊的标记,用于标识...
继续阅读 »

平常我们再开发的时候,遇到一些想要之后去编写的部分,或者说再开发某个模块的时候,突然被事情打断,暂时无法实现的代码,以后才会去修复的bug的时候,要如何精准快速的去定位到那个位置呢?


下面来介绍一个很多人会忽律的标记TODO


TODO是一个特殊的标记,用于标识需要实现但目前还未实现的功能。这是一个Javadoc的标签,因此它只能应用于类、接口和方法。


它可以帮助我们跟踪和管理开发中的待办事项。


使用方法


首先看一个最基本的使用方法


@RestController
public class TestController {

@GetMapping("/hello")
public String hello(){
//TODO do something
return "Hello World";
}
}

这里我们加上TODO。之后再需要去进行修改的时候。


直接去搜索就可以了


image-20230906195743692


除了这个方法,还有很多隐藏的方法


进入设置


image-20230906195949934


这里就可以自定义todo了


如果是团队协作的话,每个人可以自定义其他的todo类型。


也可以用自己喜欢的更加醒目的颜色


image-20230906200230765


同时也可以在idea中进行全局的todo查看


image-20230906200444351


除了这个之外,还有过滤器,可以进行自定义的todo类型


image-20230906200527489


阿里巴巴Java开发手册中对TODO的规范标注主要有以下两点:



  1. TODO:表示需要实现,但目前还未实现的功能。这个标记通常用于类、接口和方法中。

  2. FIXME:标记某代码是错误的,而且不能工作,需要及时纠正的情况。


最佳实践


编写一个代码模板


image-20230906201219291


image-20230906201810835


这样,就是一个最佳的实战了。


作者:小u
来源:juejin.cn/post/7276696131113959458
收起阅读 »

听说你会架构设计?来,弄一个打车系统

目录 引言 网约车系统 需求设计 概要设计 详细设计 体验优化 小结 1.引言 1.1 台风来袭 深圳上周受台风“苏拉”影响,从 9 月 1 日 12 时起在全市启动防台风和防汛一级应急响应。 对深圳打工人的具体影响为,当日从下午 4 点起全市...
继续阅读 »

目录




  1. 引言

  2. 网约车系统



    1. 需求设计

    2. 概要设计

    3. 详细设计

    4. 体验优化



  3. 小结



1.引言


1.1 台风来袭


深圳上周受台风“苏拉”影响,从 9 月 1 日 12 时起在全市启动防台风和防汛一级应急响应。


对深圳打工人的具体影响为,当日从下午 4 点起全市实行 “五停”:停工、停业、停市,当日已经停课、晚上 7 点后停运。


由于下午 4 点停市,于是大部分公司都早早下班。其中有赶点下班的,像这样:



有提前下班的,像这样:



还有像我们这样要居家远程办公的:



1.2 崩溃打车


下午 4 点左右,公交和地铁都人满为患。


于是快下班(居家办公)的时候就想着打个车回家,然而打开滴滴之后:



排队人数 142 位,这个排队人数和时长,让我的心一下就拔凉拔凉的。


根据历史经验,在雨天打上车的应答时间得往后推半个小时左右。更何况,这还是台风天气!


滴滴啊滴滴,你就不能提前准备一下嘛,这个等待时长,会让你损失很多订单分成的。


但反过来想,这种紧急预警,也不能完全怪打车平台,毕竟,车辆调度也是需要一定时间的。在这种大家争相逃命(bushi 的时候,周围的车辆估计也不太够用。


卷起来


等着也是等着,于是就回到公司继续看技术文章。这时我突然想到,经过这次车辆紧急调度之后,如果我是滴滴的开发工程师,需要怎么处理这种情况呢?


如果滴滴的面试官在我眼前,他又会怎么考量候选人的技术深度和产品思维呢?


2. 设计一个“网约车系统”


面试官:“滴滴打车用过是吧!看你简历里写道会架构设计是吧,如果让你设计一个网约车系统,你会从哪些方面考虑呢?”


2.1 需求分析


网约车系统(比如滴滴)的核心功能是把乘客的打车订单发送给附件的网约车司机,司机接单后,到上车点接送乘客,乘客下车后完成订单。


其中,司机通过平台约定的比例抽取分成(70%-80%不等),乘客可以根据第三方平台的信用值(比如支付宝)来开通免密支付,在下车后自动支付订单。用例图如下:



乘客和司机都有注册登录功能,分属于乘客用户模块和司机用户模块。网约车系统的另外核心功能是乘客打车,订单分配,以及司机送单。


2.2 概要设计


网约车系统是互联网+共享资源的一种模式,目的是要把车辆和乘客结合起来,节约已有资源的一种方式,通常是一辆网约车对多个用户。


所以对于乘客和司机来说,他们和系统的交互关系是不同的。比如一个人一天可能只打一次车,而一个司机一天得拉好几趟活。


故我们需要开发两个 APP 应用,分别给乘客和司机打车和接单,架构图如下:



1)乘客视角


如上所示:乘客在手机 App 注册成为用户后,可以选择出发地和目的地,进行打车。


打车请求通过负载均衡服务器,经过请求转发等一系列筛选,然后到达 HTTP 网关集群,再由网关集群进行业务校验,调用相应的微服务。


例如,乘客在手机上获取个人用户信息,收藏的地址信息等,可以将请求转发到用户系统。需要叫车时,将出发地、目的地、个人位置等信息发送至打车系统


2)司机视角


如上图所示:司机在手机 App 注册成为用户并开始接单后,打开手机的位置信息,通过 TCP 长连接定时将自己的位置信息发送给平台,同时也接收平台发布的订单消息。



司机 App 采用 TCP 长连接是因为要定时发送和接收系统消息,若采用 HTTP 推送:


一方面对实时性有影响,另一方面每次通信都得重新建立一次连接会有失体面(耗费资源)。​



司机 App:每 3~5 秒向平台发送一次当前的位置信息,包括车辆经纬度,车头朝向等。TCP 服务器集群相当于网关,只是以 TCP 长连接的方式向 App 提供接入服务,地理位置服务负责管理司机的位置信息。


3)订单接收


网关集群充当业务系统的注册中心,负责安全过滤,业务限流,请求转发等工作。


业务由一个个独立部署的网关服务器组成,当请求过多时,可以通过负载均衡服务器将流量压力分散到不同的网关服务器上。


当用户打车时,通过负载均衡服务器将请求打到某一个网关服务器上,网关首先会调用订单系统,为用户创建一个打车订单(订单状态为 “已创建”),并存库。


然后网关服务器调用打车系统,打车系统将用户信息、用户位置、出发地、目的地等数据封装到一个消息包中,发送到消息队列(比如 RabbitMQ),等待系统为用户订单分配司机。


4)订单分配


订单分配系统作为消息队列的消费者,会实时监听队列中的订单。当获取到新的订单消息时,订单分配系统会将订单状态修改为 “订单分配中”,并存库。


然后,订单分配系统将用户信息、用户位置、出发地、目的地等信息发送给订单推送 SDK


接着,订单推送 SDK 调用地理位置系统,获取司机的实时位置,再结合用户的上车点,选择最合适的司机进行派单,然后把订单消息发送到消息告警系统。这时,订单分配系统将订单状态修改为 “司机已接单” 状态。


订单消息通过专门的消息告警系统进行推送,通过 TCP 长连接将订单推送到匹配上的司机手机 App。


5)拒单和抢单


订单推送 SDK 在分配司机时,会考虑司机当前的订单是否完成。当分配到最合适的司机时,司机也可以根据自身情况选择 “拒单”,但是平台会记录下来评估司机的接单效率。


打车平台里,司机如果拒单太多,就可能在后续的一段时间里将分配订单的权重分数降低,影响自身的业绩。



订单分派逻辑也可以修改为允许附加的司机抢单,具体实现为:


当订单创建后,由订单推送 SDK 将订单消息推送到一定的地理位置范围内的司机 App,在范围内的司机接收到订单消息后可以抢单,抢单完成后,订单状态变为“已派单”。


2.3 详细设计


打车平台的详细设计,我们会关注网约车系统的一些核心功能,如:长连接管理、地址算法、体验优化等。


1)长连接的优势


除了网页上常用的 HTTP 短连接请求,比如:百度搜索一下,输入关键词就发起一个 HTTP 请求,这就是最常用的短连接。


但是大型 APP,尤其是涉及到消息推送的应用(如 QQ、微信、美团等应用),几乎都会搭建一套完整的 TCP 长连接通道。


一张图看懂长连接的优势:



图片来源:《美团点评移动网络优化实践》


通过上图,我们得出结论。相比短连接,长连接优势有三:




  1. 连接成功率高




  2. 网络延时低




  3. 收发消息稳定,不易丢失




2)长连接管理


前面说到了长连接的优势是实时性高,收发消息稳定,而打车系统里司机需要定期发送自身的位置信息,并实时接收订单数据,所以司机 App 采用 TCP 长连接的方式来接入系统。


和 HTTP 无状态连接不同的是,TCP 长连接是有状态的连接。所谓无状态,是指每次用户请求可以随意发送到某一台服务器上,且每台服务器的返回相同,用户不关心是哪台服务器处理的请求。



当然,现在 HTTP2.0 也可以是有状态的长连接,我们此处默认是 HTTP1.x 的情况。



而 TCP 长连接为了保证传输效率和实时性,服务器和用户的手机 App 需要保持长连接的状态,即有状态的连接。


所以司机 App 每次信息上报或消息推送时,都会通过一个特定的连接通道,司机 App 接收消息和发送消息的连接通道是固定不变的。


因此,司机端的 TCP 长连接需要进行专门管理,处理司机 App 和服务器的连接信息,架构图如下:



为了保证每次消息的接收和推送都能找到对应通道,我们需要维护一个司机 App 到 TCP 服务器的映射关系,可以用 Redis 进行保存。


当司机 App 第一次登录,或者和服务器断开连接(比如服务器宕机、用户切换网络、后台关闭手机 App 等),需要重连时,司机 App 会通过用户长连接管理系统重新申请一个服务器连接(可用地址存储在 Zookeeper 中),TCP 连接服务器后再刷新 Redis 的缓存。


3)地址算法


当乘客打车后,订单推送 SDK 会结合司机所在地理位置,结合一个地址算法,计算出最适合的司机进行派单。


目前,手机收集地理位置一般是收集经纬度信息。经度范围是东经 180 到西经 180,纬度范围是南纬 90 到北纬 90。


我们设定西经为负,南纬为负,所以地球上的经度范围就是[-180, 180],纬度范围就是[-90,90]。如果以本初子午线、赤道为界,地球可以分成4个部分。



根据这个原理,我们可以先将二维的空间经纬度编码成一个字符串,来唯一标识司机和乘客的位置信息。再通过 Redis 的 GeoHash 算法,来获取乘客附加的所有司机信息。


GeoHash 算法的原理是将乘客的经纬度换算成地址编码字符串,表示在某个矩形区域,通过这个算法可以快速找到同一个区域的所有司机


它的实现用到了跳表数据结构,具体实现为:


将某个市区的一块范围作为 GeoHash 的 key,这个市区范围内所有的司机存储到一个跳表中,当乘客的地理位置出现在这个市区范围时,获取该范围内所有的司机信息。然后进一步筛选出最近的司机信息,进行派单。


4)体验优化


1. 距离算法


作为线上派单,通过距离运算来分配订单效果一定会比较差,因为 Redis 计算的是两点之间的空间距离,但司机必须沿道路行驶过来,在复杂的城市路况下,也许几十米的空间距离行驶十几分钟也未可知。


所以,后续需综合行驶距离(而非空间距离)、司机车头朝向以及上车点进行路径规划,来计算区域内每个司机到达乘客的距离和时间。


更进一步,如果区域内有多个乘客和司机,就要考虑所有人的等待时间,以此来优化用户体验,节省派单时间,提升盈利额。



2. 订单优先级


如果打车订单频繁取消,可根据司机或乘客行为进行判责。判责后给乘客和司机计算信誉分,并告知用户信誉分会影响乘客和司机的使用体验,且关联到派单的优先级。


司机接单优先级

综合考虑司机的信誉分,投诉次数,司机的接单数等等,来给不同信誉分的司机分配不同的订单优先级。


乘客派单优先级

根据乘客的打车时间段,打车距离,上车点等信息,做成用户画像,以合理安排司机,或者适当杀熟(bushi。


PS:目前有些不良打车平台就是这么做的 🐶  甚至之前爆出某打车平台,会根据不同的手机系统,进行差异收费


4. 小结


4.1 网约车平台发展


目前,全球网约车市场已经达到了数千亿美元的规模,主要竞争者包括滴滴、Uber、Grab 等公司。在中国,滴滴作为最大的网约车平台已经占据了绝大部分市场份额。


网约车的核心商业逻辑比较简单,利益关联方主要为平台、司机、车辆、消费者。


平台分别对接司机、车辆【非必选项,有很多司机是带车上岗】和乘客,通过有效供需匹配赚取整个共享经济链省下的钱。


具体表现为:乘客和司机分别通过网约平台打车和接单,平台提供技术支持。乘客为打车服务付费,平台从交易金额中抽成(10%-30%不等)。



据全国网约车监管信息交互平台统计,截至 2023 年 2 月底,全国共有 303 家网约车平台公司取得网约车平台经营许可。


这些平台一部分是依靠高德打车、百度地图、美团打车为代表的网约车聚合平台;另一部分则是以滴滴出行、花小猪、T3 为代表的出行平台


4.2 网约车平台现状


随着出行的解封,网约车平台重现生机。


但由于部分网约车聚合平台的准入门槛太低,所以在过去一段时间里暴露出愈来愈多的问题。如车辆、司机合规率低,遇到安全事故,产生责任纠纷,乘客维权困难等等。


由于其特殊的模式,导致其与网约车运营商存在责任边界问题,一直游离在法律边缘。



但随着网约车聚合平台的监管不断落地,全国各地都出行了一定的监管条例。


比如某打车平台要求车辆将司机和乘客的沟通记录留档,除了司机与乘客的在线沟通记录必须保存以外,还需要一个语音电话或车载录音转换,留存一段时间备查。


有了这些人性化的监管条例和技术的不断创新,网约车平台或许会在未来的一段时间内,继续蓬勃发展。


后话


面试官:嗯,又专又红,全面发展!这小伙子不错,关注了~


作者:xin猿意码
来源:juejin.cn/post/7275211391102746684
收起阅读 »

马斯克的Twitter迎来严重危机,我国的超级App模式是否能拯救?

Meta公司近期推出的Threads 被网友戏称为“Twitter杀手”,该应用上线仅一天,用户就突破了3000 万人。外界普遍认为,这是推特上线17年来遭遇的最严峻危机。面对扎克伯格来势汹汹的挑战马斯克会如何快速组织反击? 前段时间闹得沸沸扬扬的“马扎大战”...
继续阅读 »

Meta公司近期推出的Threads 被网友戏称为“Twitter杀手”,该应用上线仅一天,用户就突破了3000 万人。外界普遍认为,这是推特上线17年来遭遇的最严峻危机。面对扎克伯格来势汹汹的挑战马斯克会如何快速组织反击?


前段时间闹得沸沸扬扬的“马扎大战”再出新剧情,继“笼斗”约架被马斯克妈妈及时叫停之后,马斯克在7月9日再次向扎克伯克打起嘴炮,这次不仅怒骂小扎是混蛋,还要公开和他比大小?!!此番马斯克的疯狂言论,让网友直呼他不是疯了就是账号被盗了。



互联网各路“吃瓜群众”对于大佬们宛如儿戏般的掐架喜闻乐见,摇旗呐喊!以至于很多人忘了这场闹剧始于一场商战:“马扎大战”开始之初,年轻的扎克伯格先发制人,率先挥出一记左钩拳——Threads,打得老马措手不及。


Threads 被网友戏称“Twitter杀手”,该应用上线仅一天,用户就突破了3000 万人。其中,不乏从推特中逃离的各界名流。舆论普遍认为,这是Twitter上线17年来遭遇的最严峻危机。



紧接着马斯克还以一记右勾拳,一封律师函向小扎发难,称Meta公司“非法盗用推特的商业秘密和其他知识产权的行为”。虽然Meta公司迅速回应,否认其团队成员中有Twitter的前雇员。但这样的回应似乎没有什么力度,Threads在功能、UI设计上均与Twitter相似,并在相关宣传中表示,Threads“具有良好的运营”,并称其为当前“一片混乱中的”Twitter的绝佳替代品。


社交平台之战的第一个回合,小扎向老马发起了猛烈的攻势。吃了一记闷拳的马斯克除了打嘴炮之外,会如何快速组织有效的反击?


会不会是老马嘴里的“非秘密武器”App X —App of Everything?


超级App或成为Twitter反击重拳


时间回溯到去年,在收购Twitter之前,马斯克就放出豪言即将创建一款他称之为“App X”的功能包罗万有的超级应用软件(Super App), 在他的愿景中,超级 “App X”就如同多功能瑞士军刀(Swiss Army Knife)般,能够包办用户日常生活大小事,包括:社交、购物、打车、支付等等。他希望这款App可以成为美国首个集食、衣、住、行功能于一身的平台。收购Twitter,似乎给了他改造实现这个超级App的起步可能。


马斯克坦言这是从微信的经营模式中汲取的灵感。微信一直被视为“超级应用程序”的代表,作为一体化平台,满足了用户的各种需求,包括即时通讯、社交、支付等等。在去年6月的推特全体员工大会上,马斯克就表示“我们还没有一个像微信那样优秀的应用,所以我的想法是为何不借鉴微信”。马斯克还在推特上写到“购买推特是创建App X的加速器,这是一个超级App(Everything App)。”


从他接手Twitter的任期开始,马斯克便加快推动超级 “App X”的发展步伐。对标于微信,除了社交功能之外,还将推出支付与电子商务。而获得监管许可是实现支付服务的重要第一步,支付也成了推特转型超级 “App X”的第一步,除了商业的必要性外,此举多少还有点宿命感。要知道,马斯克是从支付行业起家的,1999 年他投资 1200 万美元与Intuit前首席执行官 Bill Harris 共同创立了 X.com,而这家公司就是PayPal的前身。


据英国《金融时报》 1月份报道,Twitter 已经开始申请联邦和州监管许可。同时Twitter内部正在开发电子支付功能,未来更会整合其他金融服务,以实现超级App的终极目标。


但是,在亚洲“超级应用”巨头之外,几乎没有消息应用实现支付服务的先例,Whats App和Telegram 都未推出类似服务。老马领导下的Twitter,能不能成功?


添加了支付能力,也只不过是迈向“超级”的第一小步。挑战在于怎么把“everything”卷进来:衣食住行的数字服务、各行各业的商业场景。在微信世界,everything = 小程序。老马是否也要开发一套Twitter版小程序技术、缔造一个“Twitter小程序”宇宙?



“超级App”技术已实现普世化


事实上,马斯克并非“Super App ”技术理念在欧美的唯一拥趸。超级App的雄心壮志多年来早已成为美国公司管理层炫酷PPT展示中的常客了,甚至连沃尔玛都曾考虑过超级App的计划。


全球权威咨询机构Gartner发布的企业机构在2023年需要探索的十大战略技术趋势中也提到了超级应用。并预测,到2027年,全球50%以上的人口将成为多个超级应用的日活跃用户。


国外互联网巨头们开始对超级App技术趋之若鹜,但超级App的技术,是不是只有巨头才能拥有呢?


答案是否定的。互联网技术往往领先于企业应用5~7年,现在这个技术正在进入企业软件世界,任何行业的任何企业都可以拥有。


一种被称为“小程序容器”的技术,是构建超级App的核心,目前已经完全实现普及商用。背后推手是 FinClip,它作为当前市场上唯一独立小程序容器技术产品,致力于把制造超级App的技术带进各行各业,充当下一代企业数字化软件的技术底座。


超级App的技术实现,原理上是围绕一种内容载体,由三项技术共同组成:内容载体通常是某种形态的“轻巧应用”——读者最容易理解的,当然就是小程序,万事万物的数字场景,以小程序形态出现。马斯克大概率在把Twitter改造成他所谓的App X的过程中,要发展出一种类似的东西。反正在国内这就叫小程序,在W3C正在制定的标准里,这叫做Mini-App。我们就姑且依照大家容易理解的习惯,把这种“轻巧应用”称之为小程序吧。


围绕小程序,一个超级App需要在设备端实现“安全沙箱”+ “运行时”,负责把小程序从网上下载、关在一个安全隔离环境中,然后解释运行小程序内容;小程序内容的“镜像”(也就是代码包),则是发布在云端的小程序应用商店里,供超级App的用户在使用到某个商业场景或服务的时候,动态下载到设备端按需运行 – 随需随用且可以用完即弃。小程序应用商店负责了小程序的云端镜像“四态合一“(开发、测试、灰度、投产)的发布管理。


不仅仅这样,超级App本质上是一个庞大的数字生态平台,里面的小程序内容,并不是超级App的开发团队开发的,而是由第三方“进驻”和“上架”,所以,超级App还有一个非常重要的云端运营中心,负责引进和管理小程序化的数字内容生态。


超级App之所以“超级”,是因为它的生命周期(开发、测试、发版、运营),和运行在它里面的那些内容(也就是小程序)的生命周期完全独立,两者解耦,从而可运行“全世界”为其提供的内容、服务,让“全世界”为它提供“插件”而无需担心超级App本身的安全。第三方的内容无论是恶意的、有安全漏洞的或者其他什么潜在风险,并不能影响平台自身的安全稳定、以及平台上由其他人提供的内容安全保密。在建立了这样的安全与隔离机制的基础上,超级App才能实现所谓的“Economy of Scale”(规模效应),可以大开门户,放心让互联网上千行百业的企业、个人“注入插件”,产生丰富的、包罗万有的内容。


对于企业来说,拥有一个自己的超级App意味着什么呢?是超级丰富的业务场景、超级多元的合作生态、超级数量的内容开发者、以及超级敏捷的运营能力。相比传统的、封闭的、烟囱式的App,超级App实际上是帮助企业突破传统边界、建立安全开放策略、与合作伙伴实现数字化资源交换的技术手段,真正让一家企业具备平台化商业模式,加速数字化转型、增强与世界的在线连接、形成自己的网络效应。


超级App不是一个App -- Be A“world” platform


超级App+小程序,这不是互联网大平台的专利。对于传统企业来说,考虑打造自己的超级App动因至少有三:


首先,天下苦应用商店久矣。明明是纯粹企业内部一个商业决策行为,要发布某个功能或服务到自己的App上从而触达自己的客服服务自己的市场,这个发版却不得不经过不相干的第三方(App store们)批准。想象一下,你是一家银行,现在你计划在你的“数字信用卡”App里更新上架某个信用卡服务功能,你的IT完成了开发、测试,你的信用卡业主部门作了验收,你的合规、风控、法务部门通过内部的OA系统环环相扣、层层审批,现在流程到了苹果、谷歌… 排队等候审核,最后流程回到IT,服务器端一顿操作配合,正式开闸上线。你的这个信用卡服务功能,跟苹果谷歌们有一毛钱关系?但对不起,他们在你的审批流程里拥有终极话语权。


企业如果能够控制业务内容的技术实现粒度,通过自己的“服务商店”、“业务内容商店”去控制发布,让“宿主”App保持稳定,则苹果谷歌们也不用去操这个心你的App会不会每次更新都带来安全漏洞或者其他风险行为。


第二,成为一个“world platform”,企业应该有这样的“胸襟”和策略。虽然你可能不是腾讯不是推特不拥有世界级流量,这不妨碍你成为自己所在细分市场细分领域的商业世界里的平台,这里背后的思路是开放——开放平台,让全“世界”的伙伴成为我的生态,哪怕那个“世界”只存在于一个垂直领域。而这,就是数字化转型。讲那么多“数字化转型”理念,不如先落地一个技术平台作为载体,talk is cheap,show me the code。当你拥有一个在自己那个商业世界里的超级App和数以百千计的小程序的时候,你的企业已经数字化转型了。


第三,采用超级App是最有效的云化策略,把你和你的合作伙伴的内容作为小程序,挪到云端去,设备端只是加载运行和安全控制这些小程序内容的入口。在一个小小的手机上弹丸之地,“尺寸”限制了企业IT的生产力 – 无法挤进太大的团队让太多工程师同时开发生产,把一切挪到云上,那里的空间无限大,企业不再受限于“尺寸”,在云上你可以无上限的扩展技术团队,并行开发,互不认识互不打扰,为你供应无限量的内容。互联网大平台上动辄几百万个小程序是怎么来的?并行开发、快速迭代、低成本试错、无限量内容场景供应,这样的技术架构,是不是很值得企业借鉴?


做自己所在细分市场、产业宇宙里的“World Platform”吧,技术的发展已经让这一切唾手可得,也许在马斯克还在打“App of Everything”嘴炮的时候,你的超级App已经瓜熟蒂落、呱呱坠地。


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

iPhone 14 被用户吐槽电池老化

iOS
国内要闻 香港高校陆续拥抱 ChatGPT,暑期忙于筹备新学期变革 香港众多高校拥抱了 OpenAI 的聊天机器人 ChatGPT。香港科技大学率先引入ChatGPT。6 月 1 日它正式为学生与教职人员提供港科大版 ChatGPT,是香港首所为学生与教职工提...
继续阅读 »



国内要闻


香港高校陆续拥抱 ChatGPT,暑期忙于筹备新学期变革


香港众多高校拥抱了 OpenAI 的聊天机器人 ChatGPT。香港科技大学率先引入ChatGPT。6 月 1 日它正式为学生与教职人员提供港科大版 ChatGPT,是香港首所为学生与教职工提供 ChatGPT 的大学。香港中文大学、香港理工大学、香港浸会大学等高校也陆续推出使用 AI 工具的指引,共同希望师生批判性探索和谨慎使用 AI。除了在高等教育掀起热潮,AI 也将进入香港的初中课堂。香港教育局指出,ChatGPT 可以成为有价值的教育工具,但要留意抄袭的伦理问题,并期望所有公立中学尽快规划,于 2023/24 学年在“资讯和通讯科技课程”中安排 10 至 14 小时的 AI 课程教授。(奇客Solidot)


小鹏智驾灵魂人物吴新宙确认离职


小鹏汽车董事长何小鹏发文称,因家庭和多方面的原因,小鹏汽车自动驾驶中心副总裁吴新宙在 2022 年下半年表示要回到美国。在此后 10 个月时间里,小鹏汽车确立全新的工作模式,并在架构和组织上进行了提前优化和迭代。负责 XNGP 项目的李力耘博士将接手自动驾驶团队。


据业内人士透露,吴新宙或将担任英伟达“全球副总裁”这一级别的职位,直接向黄仁勋汇报,“是黄仁勋本人亲自出马,将吴新宙招至麾下。”届时,吴新宙将成为全球知名公司的最高等级华人高管,并继续在芯片等多个方面和小鹏汽车深度合作。(雷锋网)


微信要做“小绿书”?知情人士:小范围内测,优化视频号图文发布及呈现


据网传消息,微信正在灰度测试“小绿书”。从知情人士处了解到,这是一次非常小范围的内测,不是新功能,初衷就是为了更方便视频号创作者发布图文短内容,以及提高用户获得信息的效率。(36氪)


OPPO IoT 事业群负责人李开新离职,电视业务几近裁撤


OPPO IoT 事业群负责人李开新离职,可能导致其电视业务几近裁撤。OPPO IoT 部门最近两年变动不断,一直在探索新的产品线。虽然 OPPO 在 IoT 方面也尝试过其他小品类,但较为稳定的业务还是耳机和可穿戴设备。近期有报道称 OPPO 将裁撤电视业务,但 OPPO 方面表示电视业务目前运营正常。


百度千帆接入 LLaMA2 等 33 个大模型


8 月 2 日,百度智能云宣布千帆大模型平台完成新一轮升级,全面接入LLaMA2全系列、ChatGLM2、RWKV、MPT 等 33 个大模型,成为国内拥有大模型最多的平台,接入的模型经过千帆平台二次性能增强,模型推理成本可降低50%。同时,上线 103 个预置 Prompt 模板,覆盖对话、游戏、编程、写作十余个场景,并发布多款全新插件。


国际要闻


iPhone 14 被用户吐槽电池老化


据报道,不少 iPhone 14 系列机主在社交媒体吐槽,该系列出现了严重的电池老化问题。iPhone 14 系列于 2022 年 9 月上市发售,首批用户持有时间还不到一年。社交网站上不少用户留言反馈称手机电池健康已经低于 90%,最多的跌到 87%。苹果官方对“电池健康”的描述为:包含最大电池容量和峰值性能容量。一般在手机电池正常使用的情况下,完整充电次数达到 500 次,电池健康的最大容量低于 80% 则会影响手机峰值性能,保修期内的 iPhone 可以得到官方保修甚至更换。(IT之家)


消息称 OpenAI 正测试第三代图片生成模型


OpenAI 在去年 4 月推出了第二代 DALL-E“文生图”模型,该模型凭借过硬的实力吸引了业界广泛注意,据外媒表示,OpenAI 日前正在准备下一代 DALL-E AI 模型(DALL-E 3),目前该公司正在进行一系列 Alpha 测试,而部分用户已经提早接触到了该 AI 模型。(财联社)


韩国室温超导团队称论文存在缺陷


韩国一研究团队近日发布论文称实现了室温超导,在引起全球广泛关注的同时,也遭到了质疑。而该研究团队的成员表示,论文存在缺陷,系团队中的一名成员擅自发布,目前团队已要求下架论文。分析师郭明錤认为,常温常压超导体商业化的时程并没有任何能见度,但未来若能够顺利商业化,将对计算器与消费电子领域的产品设计有颠覆性的影响。即便是小如iPhone的行动装置,都能拥有与量子计算机匹敌的运算能力。(财联社)


消息称苹果 Vision Pro 开发者实验室冷清,开发者兴趣不大


苹果公司在 7 月份开始邀请开发者去 Vision Pro 的开发者实验室,这些实验室分布在库比蒂诺、伦敦、慕尼黑、上海、新加坡和东京等城市,但是目前看来,开发者对这些实验室并没有表现出很大的兴趣。据彭博社的 Mark Gurman 报道,这些开发者实验室“参与人数不多,只有少量的开发者”。


AI 打败 AI:谷歌研究团队利用 GPT-4 击败 AI-Guardian 审核系统


8 月 2 日消息,谷歌研究团队正在进行一项实验,他们使用 OpenAI 的 GPT-4 来攻破其他 AI 模型的安全防护措施,该团队目前已经攻破 AI-Guardian 审核系统,并分享了相关技术细节。谷歌 Deep Mind 的研究人员 Nicholas Carlini 在一篇题为“AI-Guardian 的 LLM 辅助开发”的论文中,探讨了使用 GPT-4“设计攻击方法、撰写攻击原理”的方案。据悉,GPT-4 会发出一系列错误的脚本和解释来欺骗 AI-Guardian ,论文中提到,GPT-4 可以让 AI-Guardian 认为“某人拿着枪的照片”是“某人拿着无害苹果的照片”,从而让 AI-Guardian 直接放行相关图片输入源。谷歌研究团队表示,通过 GPT-4 的帮助,他们成功地“破解”了 AI-Guardian 的防御,使该模型的精确值从 98% 的降低到仅 8%。(IT之家)


程序员专区


KubeSphere 3.4.0 发布


致力于打造以 Kubernetes 为内核的云原生分布式操作系统 KubeSphere 3.4.0 发布,该版本带来了值得大家关注的新功能以及增强:扩大对 Kubernetes 的支持范围,最新稳定性支持 1.26;重构告警策略架构,解耦为告警规则与规则组;提升集群别名展示权重,减少原集群名称不可修改导致的管理问题;升级 KubeEdge 组件到 v1.13 等。同时,还进行了多项修复、优化和增强,更进一步完善交互设计,并全面提升了用户体验。


Firefox 116 发布


浏览器 Firefox 116 正式发布,该版本新增加了编辑现有文本注释的可能性、用户可以从操作系统复制任何文件并将其粘贴到 Firefox 中,开发方面,Firefox 现在支持 CSP3 external hashes,添加了对 dirname 属性的支持。具体可查看发布说明:http://www.mozilla.org/en-US/firef…


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

“WWW” 仍然属于 URL 吗?它可以消失吗?

多年来,我们的地址栏上一直在进行着一场小小的较真战。也就是Google、Instagram和Facebook 等品牌。该群组已选择重定向 example.com 至 http://www.example.com。相反:GitHub...
继续阅读 »

多年来,我们的地址栏上一直在进行着一场小小的较真战。也就是GoogleInstagramFacebook 等品牌。该群组已选择重定向 example.com 至 http://www.example.com。相反:GitHubDuckDuckGoDiscord。该组织选择执行相反的操作并重定向 http://www.example.com 到 example.com



“WWW”属于 URL 吗?一些开发人员对此主题持有强烈的意见。在了解了一些历史之后,我们将探讨支持和反对它的论据。


WWW是什么?


WWW代表"World Wide Web",是上世纪80年代晚期的一个发明,引入了浏览器和网站。使用"WWW"的习惯源于给子域名命名的传统:


如果没有WWW会发生什么问题?


1. 向子域名泄露cookies


反对"没有WWW"的域名的批评者指出,在某些情况下,subdomain.example.com可以读取example.com设置的cookies。如果你是一个允许客户在你的域名上运营子域名的Web托管提供商,这可能是不希望看到的。


然而,这种行为只存在于Internet Explorer中。


RFC 6265标准化了浏览器对cookies的处理,并明确指出这种行为是错误的。


另一个潜在的泄露源是example.com设置的cookies的Domain值。如果Domain值明确设置为example.com,那么这些cookies也将被其子域名所访问。


总之,只要你不明确设置Domain值,而且你的用户不使用Internet Explorer,就不会发生cookie泄露。



2. DNS的困扰


有时,"没有WWW"的域名可能会使你的域名系统(DNS)设置复杂化。


当用户在浏览器的地址栏中输入example.com时,浏览器需要知道他们想访问的Web服务器的Internet协议(IP)地址。浏览器通过你的域名的域名服务器向其DNS服务器(通常间接通过用户的互联网服务提供商(ISP)的DNS服务器)请求IP地址。如果你的域名服务器配置为响应包含IP地址的A记录,那么"没有WWW"的域名将正常工作。


在某些情况下,你可能希望使用规范名称(CNAME)记录来代替为你的网站设置。这样的记录可以声明http://www.example.comexample123.somecdnprovider.com的别名,这会告诉用户的浏览器去查找example123.somecdnprovider.com的IP地址,并将HTTP请求发送到那里。


请注意,上面的示例使用了一个WWW子域名。对于example.com,不可能定义一个CNAME记录。根据RFC 1912,CNAME记录不能与其他记录共存。如果你尝试为example.com定义CNAME记录,example.com上的MX(邮件交换)记录将无法存在。因此,就不可能在@example.com上接收邮件


一些DNS提供商可以让你绕过这个限制。Cloudflare称其解决方案为CNAME解析。通过这种技术,域名管理员配置一个CNAME记录,但他们的域名服务器将暴露一个A记录。


例如,如果管理员为example.com配置了指向example123.somecdnprovider.com的CNAME记录,并且存在一个指向1.2.3.4example123.somecdnprovider.com的A记录,那么Cloudflare就会暴露一个指向1.2.3.4的example.com的A记录。


总之,虽然这个问题对希望使用CNAME记录的域名所有者来说是有效的,但现在有一些DNS提供商提供了合适的解决办法。


没有WWW的好处


大部分反对WWW的论点是实用性或外观方面的。"无WWW"的支持者认为example.comhttp://www.example.com更容易说和输入(对于不那么精通技术的用户可能更不容易混淆)。


反对WWW子域名的人还指出,去掉它会带来一种谦虚的性能优势。网站所有者可以通过这样做每个HTTP请求节省4个字节。虽然这些节省对于像Facebook这样的高流量网站可能会累积起来,但带宽通常并不是一种紧缺的资源。


有"WWW"的好处


支持WWW的一个实际论点适用于使用较新顶级域的情况。例如,http://www.example.miamiexample.miami无法立即被识别为Web地址。对于具有诸如.com这样的可识别顶级域的网站,这不是一个太大的问题。


对搜索引擎排名的影响


目前的共识是你的选择不会影响你的搜索引擎表现。如果你希望从一个URL迁移到另一个URL,你需要配置永久重定向(HTTP 301)而不是临时重定向(HTTP 302)。永久重定向确保你旧的URL的SEO价值转移到新的URL。


同时支持两者的技巧


网站通常会选择example.comhttp://www.example.com作为官方网站,并为另一个配置HTTP 301重定向。理论上,可以支持http://www.example.com和example.com两者。但实际上,成本可能会超过效益。


从技术角度来看,你需要验证你的技术栈是否能够处理。你的内容管理系统(CMS)或静态生成的网站需要将内部链接输出为相对URL以保留访问者的首选主机名。除非你可以将主机名配置为别名,否则你的分析工具可能会将流量分别记录在两个主机名上。


最后,你需要采取额外的措施来保护你的搜索引擎表现。谷歌将把URL的"WWW""非WWW"版本视为重复内容。为了在其搜索索引中去重复内容,谷歌将显示它认为用户更喜欢的那个版本——不论是好是坏。


为了在谷歌中保持对自己的控制,建议插入规范链接标签。首先,决定哪个主机名将成为官方(规范)主机名。


例如,如果你选择了www.example.com,则必须在 https://example.com/my-article里的 <head> 上的标记 中插入以下代码段:

    <link href="<https://www.example.com/my-article>" rel="canonical"> 

这个代码片段告诉谷歌"无WWW"变体代表着相同的内容。通常情况下,谷歌会在搜索结果中偏好你标记为规范的版本,也就是在这个例子中的"WWW"变体。


总结


对于是否在URL中加入"WWW",人们有不同的观点。下面是支持和反对的论点:


支持"WWW"的论点:

  1. 存在子域名的安全性问题:某些情况下,子域名可以读取主域名设置的cookies。虽然这个问题只存在于Internet Explorer浏览器中,并且已经被RFC 6265标准化修复,但仍有人认为使用"WWW"可以避免潜在的安全风险。
  2. DNS配置的复杂性:如果你的域名系统(DNS)配置为响应包含IP地址的A记录,那么"没有WWW"的域名将正常工作。但如果你想使用CNAME记录来设置规范名称,那么"没有WWW"的域名可能会导致一些限制,例如无法同时定义CNAME记录和MX(邮件交换)记录。
  3. 对搜索引擎排名的影响:对于使用较新顶级域的网站,使用"WWW"可以帮助识别网址,而不是依赖可识别的顶级域名。然而,目前的共识是选择是否使用"WWW"对搜索引擎表现没有直接影响。

支持去除"WWW"的论点:

  1. 实用性和外观:去除"WWW"可以使域名更简洁和易于输入,减少了用户可能混淆的机会。
  2. 节省字节:去除"WWW"可以每个HTTP请求节省4个字节。虽然这对于高流量网站来说可能是一个可累积的优势,但对于大多数网站来说,带宽通常不是一个紧缺的资源。

最佳实践:
一般来说,网站会选择将example.com或www.example.com作为官方网址,并对另一个进行重定向。你可以通过使用HTTP 301永久重定向来确保旧URL的SEO价值转移到新URL。同时,你还可以在页面的标签中插入规范链接标签,告诉搜索引擎两个URL代表相同的内容,以避免重复内容问题。


需要注意的是,在做决策时要考虑到技术栈的支持能力、DNS配置的限制和谷歌对搜索排名的处理方式。


本文同步我的技术文档


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

从《孤注一掷》出发,聊聊 SSL 证书的重要性

你去看《孤注一掷》了吗?相信最近大家的朋友圈和抖音都被爆火电影《孤注一掷》成功刷屏。取材于上万真实案例的《孤注一掷》揭露了缅甸诈骗园区残暴的统治,以及电信诈骗中系统性极强的诈骗技巧,引发了大量讨论。  图片来源于电影《孤注一掷》 这部电影除...
继续阅读 »

你去看《孤注一掷》了吗?相信最近大家的朋友圈和抖音都被爆火电影《孤注一掷》成功刷屏。取材于上万真实案例的《孤注一掷》揭露了缅甸诈骗园区残暴的统治,以及电信诈骗中系统性极强的诈骗技巧,引发了大量讨论。 


 图片来源于电影《孤注一掷》


这部电影除了让人后背发凉外,也不禁让人回忆起了曾经上网冲浪遇到的种种现象:看小说时性感荷官总在网页右下角在线发牌;看电影时网页左下角常常蹦出“在线老虎机”……这些让人烦不胜烦的广告弹窗之所以出现,要么是建站人员利欲熏心投放了非法广告,要么就是因为网站使用了不安全的 HTTP 协议而遭到了攻击,正常的网页内容被恶意篡改。


网站是电信诈骗、网络赌博等非法内容出现的重灾区,建站者和使用者都应该提高安全意识,特别是对建站者来说,保护通信安全才能更好的承担起建站责任。本文将从 HTTP 讲起,介绍 HTTPS 保护通信安全的原理,以及作为网络通信安全基石的 SSL 证书的重要性。


HTTP 协议


HTTP(Hyper Text Transfer Protocol)协议是超文本传输协议。它是从 WEB 服务器传输超文本标记语言(HTML)到本地浏览器的传送协议。HTTP 基于 TCP/IP 通信协议来传递数据,通信双方在 TCP 握手后即可开始互相传输 HTTP 数据包。具体过程如下图所示: 


 HTTP 建立流程


HTTP 协议中,请求和响应均以明文传输。如下图所示,在访问一个使用 HTTP 协议的网站时,通过抓包软件可以看到网站 HTTP 响应包中的完整 HTML 内容。




虽然 HTTP 明文传输的机制在性能上带来了优势,但同时也引入了安全问题:

  • 缺少数据机密性保护。HTTP 数据包内容以明文传输,攻击者可以轻松窃取会话内容。
  • 缺少数据完整性校验。通信内容以明文传输,数据内容可被攻击者轻易篡改,且双方缺少校验手段。
  • 缺少身份验证环节。攻击者可冒充通信对象,拦截真实的 HTTP 会话。

HTTP 劫持


作为划时代的互联网通信标准之一,HTTP 协议的出现为互联网的普及做出了不可磨灭的贡献。但正如上节谈到, HTTP 协议因为缺少加密、身份验证的过程导致很可能被恶意攻击,针对 HTTP 协议最常见的攻击就是 HTTP 劫持。


HTTP 劫持是一种典型的中间人攻击。HTTP 劫持是在使用者与其目的网络服务所建立的数据通道中,监视特定数据信息,当满足设定的条件时,就会在正常的数据流中插入精心设计的网络数据报文,目的是让用户端程序解析“错误”的数据,并以弹出新窗口的形式在使用者界面展示宣传性广告或直接显示某网站的内容。


下图是一种典型的 HTTP 劫持的流程。当客户端给服务端发送 HTTP 请求,图中发送请求为“梁安娜的电话号码是?”,恶意节点监听到该请求后将其放行给服务端,服务端返回正常 HTML 响应,关键返回内容本应该是“+86 130****1234”,恶意节点监听到该响应,并将关键返回内容篡改为泰国区电话“+66 6160 *88”,导致用户端程序展示出错误信息,这就是 HTTP 劫持的全流程。



 HTTP 劫持流程


例如,在某网站阅读某网络小说时,由于该网站使用了不安全的 HTTP 协议,攻击者可以篡改 HTTP 相应的内容,使网页上出现与原响应内容无关的广告,引导用户点击,可能将跳转进入网络诈骗或其他非法内容的页面。



 原网页



 HTTP 劫持后网页


HTTPS 工作原理


HTTPS 协议的提出正是为了解决 HTTP 带来的安全问题。HTTPS 协议(HyperText Transfer Protocol Secure,超文本传输安全协议),是一种通过计算机网络进行安全通信的传输协议。HTTPS 经由 HTTP 进行通信,但利用 SSL/TLS 来加密数据包。HTTPS 的开发主要是提供对网站服务器的身份认证,保护交换资料的隐私性与完整性。


TLS 握手是 HTTPS 工作原理的安全基础部分。TLS 传统的 RSA 握手流程如下所示:



 TLS 握手流程


TLS 握手流程主要可以分为以下四个部分:


第一次握手:客户端发送 Client Hello 消息。该消息包含:客户端支持的 SSL/TLS 协议版本(如 TLS v1.2 );用于后续生成会话密钥的客户端随机数 random_1;客户端支持的密码套件列表。


第二次握手:服务端收到 Client Hello 消息后,保存随机数 random_1,生成随机数 random_2,并发送以下消息。

  • 发送 Server Hello 消息。该消息包含:服务端确认的 SSL/TLS 协议版本(如果双方支持的版本不同,则关闭加密通信);用于后续生成会话密钥的服务端随机数 random_2;服务端确认使用的密码套件
  • 发送“Server Certificate”消息。该消息包含:服务端的 SSL 证书。SSL 证书又包含服务端的公钥、身份等信息。
  • 发送“Server Hello Done”消息。该消息表明 ServerHello 及其相关消息的结束。发送这个消息之后,服务端将会等待客户端发过来的响应。

第三次握手:客户端收到服务端证书后,首先验证服务端证书的正确性,校验服务端身份。若证书合法,客户端生成预主密钥,之后客户端根据(random_1, random_2, 预主密钥)生成会话密钥,并发送以下消息。

  • 发送“Client Key Exchange”消息,该消息为客户端生成的预主密钥,预主密钥会被服务端证书中的公钥加密后发送。
  • 发送“Change Cipher Spec”消息,表示之后数据都将用会话密钥进行加密。
  • 发送“Encrypted Handshake Message”消息,表示客户端的握手阶段已经结束。客户端会生成所有握手报文数据的摘要,并用会话密钥加密后发送给服务端,供服务端校验。

第四次握手:服务端收到客户端的消息后,利用自己的服务端证书私钥解密出预主密钥,并根据(random_1, random_2, 预主密钥)计算出会话密钥,之后发送以下消息。

  • 发送“Change Cipher Spec”消息,表示之后数据都将用会话密钥进行加密。
  • 发送“Encrypted Handshake Message”,表示服务端的握手阶段已经结束,同时服务端会生成所有握手报文数据的摘要,并用会话密钥加密后发送给客户端,供客户端校验。

根据 TLS 握手流程,可以看出它是如何解决 HTTP 协议缺陷,以及避免中间人攻击的:


1.规避窃听风险,攻击者无法获知通信内容


在客户端进行真正的 HTTPS 请求前,客户端与服务端都已经拥有了本次会话中用于加密的对称密钥,后续双方 HTTPS 会话中的内容均用该对称密钥加密,攻击者在无法获得该对称密钥的情况下,无法解密获得会话中内容的明文。即使攻击者获得了 TLS 握手中双方发送的所有明文信息,也无法从这些信息中恢复对称密钥,这是由大数质因子分解难题和有限域上的离散对数难题保证的。


2.规避篡改风险,攻击者无法篡改通信内容


在数据通信阶段,双端消息发送时会对原始消息做一次哈希,得到该消息的摘要后,与加密内容一起发送。对端接受到消息后,使用协商出来的对称加密密钥解密数据包,得到原始消息;接着也做一次相同的哈希算法得到摘要,对比发送过来的消息摘要和计算出的消息摘要是否一致,可以判断通信数据是否被篡改。


3.规避冒充风险,攻击者无法冒充身份参与通信


在 TLS 握手流程中的第二步“Server Hello”中,服务端将自己的服务端证书交付给客户端。客户端拿到 SSL 证书后,会对服务端证书进行一系列校验。以浏览器为例,校验服务端证书的过程为:

  • 验证证书绑定域名与当前域名是否匹配。
  • 验证证书是否过期,是否被吊销。
  • 查找操作系统中已内置的受信任的证书发布机构 CA(操作系统会内置有限数量的可信 CA),与服务端证书中的颁发者 CA 比对,验证证书是否为合法机构颁发。如果服务端证书不是授信 CA 颁发的证书,则浏览器会提示服务端证书不可信。
  • 验证服务端证书的完整性,客户端在授信 CA 列表中找到服务端证书的上级证书,后使用授信上级证书的公钥验证服务端证书中的签名哈希值。
  • 在确认服务端证书是由国际授信 CA 签发,且完整性未被破坏后,客户端信任服务端证书,也就确认了服务端的正确身份。

SSL 证书


正如上一节介绍,SSL 证书在 HTTPS 协议中扮演着至关重要的作用,即验证服务端身份,协助对称密钥协商。只有配置了 SSL 证书的网站才可以开启 HTTPS 协议。在浏览器中,使用 HTTP 的网站会被默认标记为“不安全”,而开启 HTTPS 的网站会显示表示安全的锁图标。



 使用 HTTP 协议的网站



 使用 HTTPS 协议的网站


从保护范围、验证强度和适用类型出发, SSL 证书会被分成不同的类型。只有了解类型之间的区别,才能根据实际情况选择更适合的证书类型,保障通信传输安全。


从保护范围分,SSL 证书可以分为单域名证书、通配符证书、多域名证书。

  • 单域名证书:单域名证书只保护一个域名,这些域名形如 http://www.test.com 等。
  • 通配符证书:通配符证书可以保护基本域和无限的子域。通配符 SSL 证书的公用名中带有星号 ,其中,星号表示具有相同基本域的任何有效子域。例如,。test.com 的通配符证书可用于保护 a.test.com、 b.test.com……
  • 多域名证书:多域证书可用于保护多个域或子域。包括完全唯一的域和具有不同顶级域的子域(本地/内部域除外)的组合。

从验证强度和适用类型进一步区分,SSL 证书可以分为 DV、OV、EV 证书。

  • DV(Domain Validated):域名验证型。在颁发该类型证书时,CA 机构仅验证申请者对域名的所有权。CA 机构会通过检查 WHOIS、DNS 的特定记录来确认资格。一般来说,DV 证书适用于博客、个人网站等不需要任何私密信息的网站。
  • OV(Organization Validated):组织验证型。OV 证书的颁发除了要验证域名所有权外,CA 还会额外验证申请企业的详细信息(名称、类型、地址)等。一般来说,OV 证书适用于中级商业组织。
  • EV(Extended Validation):扩展验证型。EV 证书的颁发除了 CA 对 DV 和 OV 证书所采取的所有身份验证步骤之外,还需要审查商业组织是否在真实运营、其实际地址,并致电以验证申请者的就业情况。一般来说,EV 证书适用于顶级商业组织。

结尾


随着互联网应用的普及,网络诈骗的方式也越发花样百出,让人防不胜防。


除了文内提到的网页环境,在软件应用、邮件、文档、物联网等领域同样存在恶意软件、钓鱼邮件、文档篡改、身份认证的问题。幸运的是,作为 PKI 体系下的优秀产品,证书体系同样在这些领域发挥着重要作用,软件签名证书、邮件签名证书、文档签名证书、私有证书等保护着各自领域的信息安全。


总有不法分子企图通过漏洞牟利,而证书体系在保护数据机密性、完整性、可用性以及身份验证场景上有着无可取代的地位,牢牢守护着用户信息,保障通信安全。


推荐活动


火山引擎域名与网站特惠活动来啦,欢迎访问火山引擎官网抢购!


5 折抢购 SSL 证书、1 元注册/转入域名、1 元升级 DNS 专业版、HTTPDNS 资源包 1 折起火热进行中……


此外,火山引擎已新推出:

  • 私有 CA(Private CA/PCA),通过私有证书灵活标识和保护企业内部资源和数据
  • 商标服务,专业、高效的商标注册管理服务平台
  • 私网解析 PrivateZone,灵活构建 VPC 内的私网域名系统
  • 公共解析PublicDNS,快速安全的递归DNS,永久免费
  • 域名委托购买服务,0元下单即可尝试获取心仪域名

关于火山引擎边缘云:
火山引擎边缘云,以云原生技术为基础底座,融合异构算力和边缘网络,构建在大规模边缘基础设施之上的云计算服务,形成以边缘位置的计算、网络、存储、安全、智能为核心能力的新一代分布式云计算解决方案。


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

一条SQL差点引发离职

排除一切不可能的,剩下的即使再不可能,那也是真相” 背景        最近组里的小伙伴在开发一个更新功能时踩了MySQL的一个类型转换的坑,差点造成线上故障。 本来是一个很简单的逻辑,就...
继续阅读 »

排除一切不可能的,剩下的即使再不可能,那也是真相”



背景


       最近组里的小伙伴在开发一个更新功能时踩了MySQL的一个类型转换的坑,差点造成线上故障。

本来是一个很简单的逻辑,就是根据唯一的id去更新对应的MySQL数据,代码简化后如下:


var updates []*model.Goods
for id, newGoods := range update {
 if err := model.GetDB().Model(&model.Goods{}).Where("id = ?", id).Updates(map[string]interface{}{
  "selling_price":  newGoods.SellingPrice,
  "sell_type":      newGoods.SellType,
  "status":         newGoods.Status,
  "category_id":    newGoods.CategoryID,
 }).Error; err != nil {
  return nil, err
 }
}

很明显,updates[]model.Goods\color{red}{updates []*model.Goods}本来应该是想声明为 map[string]model.Goods\color{red}{map[string]*model.Goods}类型的,然后key是唯一id。这样下面的更新逻辑才是对的,否则拿到的id其实是数组的下标。

但是code review由于跟着一堆代码一起评审了,并且这段更新很简单,同时测试的时候也测试过了(能测试通过也是“机缘巧合”),所以没有发现这段异常。

发到线上后,进行了灰度集群的测试,这个时候发现只要调用了这个接口,灰度集群的数据全部都变成了一样,回滚后正常。


分析


       回滚后在本地进行复现,由于本地环境是开启了SQL打印的,于是看到了这么一条SQL:很明显是拿数组的下标去比较了


update db_name set selling_price = xx,sell_type = xx where id = 0;

       由于我们的id是全部是通过uuid生成的,所以下意识的认为这条sql应该啥也不会更新才对,但是本地的确只执行了这条sql,没有别的sql,并且db中的数据全部都被修改了。

这个时候想起福尔摩斯的名言“排除一切不可能的,剩下的即使再不可能,那也是真相”\color{blue}{“排除一切不可能的,剩下的即使再不可能,那也是真相”} ,于是抱着试一试的心态直接拿这条sql去db控制台执行了一遍,发现果然所有的数据又都被修改了。

也就是 whereid=0\color{red}{where id = 0}  这个条件对于所有的记录都是恒为true,就会导致所有记录都被更新。在这个时候,想起曾经看到过MySQL对于不同类型的比较会有 【隐式转换】\color{red}{【隐式转换】},难道是这个原因导致的?


隐式转换规则


在MySQL官网找到了不同类型比较的规则:



最后一段的意思是:对于其他情况,将按照浮点(双精度)数进行比较。例如,字符串和数字的比较就按照浮点数规则进行比较。

也就是id会首先被转换成浮点数,然后再跟0进行比较。


MySQL字符转为浮点数时会按照如下规则进行:


1.如果字符串的第一个字符就是非数字的字符,那么转换结果就是0;

2.如果字符串以数字开头:

(1)如果字符串都是数字,转换结果就是整个字符串对应的数字;

(2)如果字符串中存在非数字,转换结果就是开头的那些数字对应的值;

举例说明:

"test" -> 0

"1test" -> 1

"12test12" -> 12

由于我们生成的uuid没有数字开头的字符串,于是都会转变成0。那么这条SQL就变成了:


update db_name set selling_price = xx,sell_type = xx where 0 = 0;

就恒为true了。

修复就很简单了,把取id的逻辑改成正确的就行。


为什么测试环境没有发现


       前面有提到这段代码在测试环境是测试通过了的,这是因为开发和测试同学的环境里都只有一条记录,每次更新他发现都能正常更新就认为是正常的了。同时由于逻辑太简单了,所以都没有重视这块的回归测试。

幸好在灰度集群就发现了这个问题,及时进行了回滚,如果发到了线上影响了用户数据,可能就一年白干了。


最后


代码无小事,事事需谨慎啊。一般致命问题往往是一行小小的修改导致的。


作者:云舒编程
来源:juejin.cn/post/7275550679790960640
收起阅读 »

请给系统加个【消息中心】功能,因为真的很简单

个人项目:社交支付项目(小老板) 作者:三哥,j3code.cn 项目文档:http://www.yuque.com/g/j3code/dv… 预览地址(未开发完):admire.j3code.cn/small-boss 内网穿透部署,第一次访问比较慢 ...
继续阅读 »

个人项目:社交支付项目(小老板)


作者:三哥,j3code.cn


项目文档:http://www.yuque.com/g/j3code/dv…


预览地址(未开发完):admire.j3code.cn/small-boss



  • 内网穿透部署,第一次访问比较慢



我相信,打开一个带有社交类型的网站,你或多或少都可以看到如下的界面:


1)消息提示


Snipaste_2023-08-27_13-41-36.jpg


2)消息列表


这样


Snipaste_2023-08-27_13-42-25.jpg


这样


Snipaste_2023-08-27_16-41-30.jpg


那,这就是我们今天要聊的【消息中心】。


1、设计


老规矩先来搞清楚消息中心的需求,再来代码实现。


我们知道在社交类项目中,有很多评论、点赞等数据的产生,而如果这些数据的产生不能让用户感知到,那你们想想这会带来什么影响?



用户A:太鸡肋了,发布的内容被人评论点赞了,我居然看不到,下次不用了...


用户B:还好没用这个系统...



所以,看到这些结果我们是不是能够意识到一个健全的社交功能,是不是少不了这种通知用户的机制啊!而这种机制我就把他定义为【消息中心】功能。


再来拆分一下这四个字:消息中心



  1. 消息

  2. 中心


消息:这个可以是由我们自己定义,如:把帖子被用户评论当作一条消息,把评论被用户点赞也可以当作一条消息,甚至系统发布的通知也是一条消息。


中心:这个就是字面意思,将上面所提到的所有消息,归拢到一个地方进行展示。


上面我们也提到消息基本就是这两种:



  • 用户对用户:用户消息

  • 平台对用户:系统消息


针对用户消息,就类似这样,用户 A 给用户 B 的一条评论进行了点赞,那这个点赞动作就会产生一条消息,并且通知到用户 B 的一个存储消息的地方,这里通常就指用户的收件箱。这个收件箱就是专门用来存储用户发给用户的消息,而这个点对点的模式是不是就是推送模式啊!(A 推送消息给 B)


接着针对系统消息,就类似这样,平台管理人员发布了一条通知,告诉大家平台有啥 XXX 活动。那这个活动通知肯定是要让平台的所有用户都知道把,所以这个通知就要存在一个发件箱中。这个发件箱就是专门存储平台的通知,所有用户都来这个发件箱中读取消息就行,而这个一对多的模式是不是就是拉取模式啊!(所有用户都来拉取平台消息)


这样一来,我们根据不同的消息场景就抽出了一个基本的消息推拉模型,模型图如下:



Snipaste_2023-08-27_14-27-25.jpg



Snipaste_2023-08-27_14-59-50.jpg


针对这两种模式,不知道大家有没有看出区别,好像乍一看没啥区别,都是发消息,读消息,对吧!


没错,确实都是一个发,一个读,但是两者的读写频率确实有着巨大的差异。先来看推模型,一个普通用户发表了一条帖子,然后获得了寥寥无几的评论和赞,这好似也没啥特别之处,对吧!那如果这个普通用户发表的帖子成为了热门帖子呢,也即该贴子获得了上万的评论和赞。那,你们想想是不是发消息的频率非常高,而该普通用户肯定是不可能一下子读取这么多消息的,所以是不是一个写多读少的场景。再来看看拉模型,如果你的平台用户人数寥寥无几,那倒没啥特别之处,但如果用户人数几万甚至几十万。那,每个用户都过来拉取系统消息是不是就是一个读频率非常高,而发消息频率非常低(系统消息肯定不会发的很快),所以这是不是一个读多写少的场景。


1.1 推:写多读少


针对这个模式,我们肯定是要将写这个动作交给性能更高的中间件来处理,而不是 MySQL,所以此时我们的 RocketMQ 就出来了。


当系统中产生了评论、点赞类的高频消息,那就无脑的丢给 MQ 吧,让其在消息中间件中呆会,等待消费者慢慢的将消息进行消费并发到各个用户的收件箱中,就类似下面这张图的流程:


Snipaste_2023-08-27_15-45-46.jpg


2.2 拉:读多写少


那对于这个模式,所实话,我觉得不用引入啥就可以实现,因为对于读多的话无非就是一个查,MySQL 肯定是能搞定的,即使你的用户几万、几十万都是 ok 的。


但咱们是不是可以这样想一下,一个系统的官方通知肯定是不多的,或者说几天或者几个星期一次,且一旦发送就不可更改。那是不是可以考虑缓存,让用户读取官方通知的时候走缓存,如果缓存没有再走 MySQL 这样应该是可以提高查询效率,提高响应速度。


具体流程如下图:


Snipaste_2023-08-27_15-57-21.jpg


2.3 表结构设计


基本的业务流程已经分析的差不多了,现在可以把表字段抽一下了,先根据上面分析的,看看我们需要那些表:



  1. 用户收件箱表

  2. 系统发件箱表


看似好像就这两张表,但是应该还有第三张表:



  1. 用户读取系统消息记录表



我们看到页面是不是每次有一条新的消息都会有一个小标点记录新消息数量,而第三张表就是为了这个作用而设计的。


具体原理如下:



  1. 首先运营人员发布的消息都是存储在第二张表中,这肯定是没错的

  2. 那用户每次过来拉取系统消息时,将最近拉取的一条消息写入到第三种表中

  3. 这样等用户下次再来拉取的时候,就可以根据第三张表的读取记录,来确定他有几条系统消息未查看了


可能有人会发出疑问:那用户的收件箱为啥不出一个用户读取记录表呢!


这个很简单,因为收件箱中的数据已经表示这个用户需要都这些个消息了,只是不知道那些是已读的那些是未读的,我们只需要再收件箱表中加一个字段,这个字段的作用就是记录最新一次读取的消息 ID 就行,等下次要读消息时,找到上传读取读取消息的记录ID,往后读新消息即可。



好,现在来看看具体的表字段:


1)用户收件箱表(sb_user_inbox)



  • id

  • 消息数据唯一 id:MQ唯一消息凭证

  • 消息类型:评论消息或者点赞消息

  • 帖子id:业务id

  • 业务数据id:业务id

  • 内容:消息内容

  • 业务数据类型:业务数据类型(商品评论、帖子、帖子一级评论、帖子二级评论)

  • 发起方的用户ID:用户 A 对用户 B 进行点赞,那这就是用户 A 的ID

  • 接收方的用户ID:用户 B 的 ID

  • 用户最新读取位置ID:用户最近一次读取记录的 ID


SQL


CREATE TABLE `sb_user_inbox` (
`id` bigint(20) NOT NULL,
`uuid` varchar(128) COLLATE utf8mb4_german2_ci NOT NULL COMMENT '消息数据唯一id',
`message_type` tinyint(1) NOT NULL COMMENT '消息类型',
`post_id` bigint(20) DEFAULT NULL COMMENT '帖子id',
`item_id` bigint(20) NOT NULL COMMENT '业务数据id',
`content` varchar(1000) COLLATE utf8mb4_german2_ci DEFAULT NULL COMMENT '内容',
`service_message_type` tinyint(1) NOT NULL COMMENT '业务数据类型',
`from_user_id` bigint(20) NOT NULL COMMENT '发起方的用户ID',
`to_user_id` bigint(20) NOT NULL COMMENT '接收方的用户ID',
`read_position_id` bigint(20) DEFAULT '0' COMMENT '用户最新读取位置ID',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `un01` (`uuid`),
UNIQUE KEY `un02` (`item_id`,`service_message_type`,`to_user_id`),
KEY `key` (`to_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci

可以看到,我加了很多业务相关的字段,这个主要是为了方便查询数据和展示数据。


2)系统发件箱表(sb_sys_outbox)



  • id

  • 内容


SQL


CREATE TABLE `sb_sys_outbox` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`content` varchar(2000) COLLATE utf8mb4_german2_ci NOT NULL COMMENT '内容',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci

这个表就非常简单了,没啥业务字段冗余。


3)用户读取系统消息记录表(sb_user_read_sys_outbox)



  • id

  • 系统收件箱数据读取id

  • 读取的用户id


SQL


CREATE TABLE `sb_user_read_sys_outbox` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`sys_outbox_id` bigint(20) NOT NULL COMMENT '系统收件箱数据读取id',
`user_id` bigint(20) NOT NULL COMMENT '读取的用户id',
PRIMARY KEY (`id`),
UNIQUE KEY `un` (`user_id`),
KEY `key` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci

ok,这是消息中心所有分析阶段了,下面就开始实操。


2、实现


先来引入引入一下 RocketMQ 的依赖


<!--rocketmq-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.1</version>
</dependency>

RocketMQ 的双主双从同步刷新集群搭建教程:blog.csdn.net/qq_40399646…


MQ 配置:


Snipaste_2023-08-27_16-26-09.jpg


2.1 生产者


先来实现生产者如何发送消息。


1)消息体对象:LikeAndCommentMessageDTO


位置:cn.j3code.config.dto.mq


@Data
public class LikeAndCommentMessageDTO {

/**
* 该消息的唯一id
* 业务方可以不设置,如果为空,代码会自动填充
*/

private String uuid;

/**
* 消息类型
*/

private UserCenterMessageTypeEnum messageType;

/**
* 冗余一个帖子id进来
*/

private Long postId;

/**
* 业务数据id
*/

private Long itemId;

/**
* 如果是评论消息,这个内容就是评论的内容
*/

private String content;

/**
* 业务数据类型
*/

private UserCenterServiceMessageTypeEnum serviceMessageType;

/**
* 发起方的用户ID
*/

private Long fromUserId;

/**
* 接收方的用户ID
*/

private Long toUserId;


/*
例子:
用户 A 发表了一个帖子,B 对这个帖子进行了点赞,那这个实体如下:
messageType = UserCenterMessageTypeEnum.LIKE
itemId = 帖子ID(对评论进行点赞,就是评论id,对评论进行回复,就是刚刚评论的id)
serviceMessageType = UserCenterServiceMessageTypeEnum.POST(这个就是说明 itemId 的 ID 是归于那个业务的,方便后续查询业务数据)
fromUserId = 用户B的ID
toUserId = 用户 A 的ID
*/

}

2)发送消息代码


位置:cn.j3code.community.mq.producer


@Slf4j
@Component
@AllArgsConstructor
public class LikeAndCommentMessageProducer {

private final RocketMQTemplate rocketMQTemplate;

/**
* 单个消息发送
*
* @param dto
*/

public void send(LikeAndCommentMessageDTO dto) {
if (Objects.isNull(dto.getUuid())) {
dto.setUuid(IdUtil.simpleUUID());
}
checkMessageDTO(dto);
Message<LikeAndCommentMessageDTO> message = MessageBuilder
.withPayload(dto)
.build();
rocketMQTemplate.send(RocketMQConstants.USER_MESSAGE_CENTER_TOPIC, message);
}

/**
* 批量消息发送
*
* @param dtos
*/

public void send(List<LikeAndCommentMessageDTO> dtos) {
/**
* 将 dtos 集合分割成 1MB 大小的集合
* MQ 批量推送的消息大小最大 1MB 左右
*/

ListSizeSplitUtil.split(1 * 1024 * 1024L, dtos).forEach(items -> {
List<Message<LikeAndCommentMessageDTO>> messageList = new ArrayList<>(items.size());
items.forEach(dto -> {
if (Objects.isNull(dto.getUuid())) {
dto.setUuid(IdUtil.simpleUUID());
}
checkMessageDTO(dto);
Message<LikeAndCommentMessageDTO> message = MessageBuilder
.withPayload(dto)
.build();
messageList.add(message);
});
rocketMQTemplate.syncSend(RocketMQConstants.USER_MESSAGE_CENTER_TOPIC, messageList);
});
}

private void checkMessageDTO(LikeAndCommentMessageDTO dto) {
AssertUtil.isTrue(Objects.isNull(dto.getMessageType()), "消息类型不为空!");
AssertUtil.isTrue(Objects.isNull(dto.getItemId()), "业务数据ID不为空!");
AssertUtil.isTrue(Objects.isNull(dto.getServiceMessageType()), "业务数据类型不为空!");
AssertUtil.isTrue(Objects.isNull(dto.getFromUserId()), "发起方用户ID不为空!");
AssertUtil.isTrue(Objects.isNull(dto.getToUserId()), "接收方用户ID不为空!");
}


/**
* 发送点赞消息
*
* @param messageType 消息类型
* @param serviceMessageType 业务类型
* @param itemToUserIdMap 业务ID对应的用户id
* @param saveLikeList 点赞数据
*/

public void sendLikeMQMessage(
UserCenterMessageTypeEnum messageType,
UserCenterServiceMessageTypeEnum serviceMessageType,
Map<Long, Long> itemToUserIdMap, List<Like> saveLikeList)
{
if (CollectionUtils.isEmpty(saveLikeList)) {
return;
}
List<LikeAndCommentMessageDTO> dtos = new ArrayList<>();
for (Like like : saveLikeList) {
LikeAndCommentMessageDTO messageDTO = new LikeAndCommentMessageDTO();
messageDTO.setItemId(like.getItemId());
messageDTO.setMessageType(messageType);
messageDTO.setServiceMessageType(serviceMessageType);
messageDTO.setFromUserId(like.getUserId());
messageDTO.setToUserId(itemToUserIdMap.get(like.getItemId()));
dtos.add(messageDTO);
}
try {
send(dtos);
} catch (Exception e) {
//错误处理
log.error("发送MQ消息失败!", e);
}
}
}

注意:这里我用了 MQ 批量发送消息的一个功能,但是他有一个限制就是每次只能发送 1MB 大小的数据。所以我需要做一个功能工具类将业务方丢过来的批量数据进行分割。


工具类:ListSizeSplitUtil


位置:cn.j3code.config.util


public class ListSizeSplitUtil {

private static Long maxByteSize;

/**
* 根据传进来的 byte 大小限制,将 list 分割成对应大小的 list 集合数据
*
* @param byteSize 每个 list 数据最大大小
* @param list 待分割集合
* @param <T>
* @return
*/

public static <T> List<List<T>> split(Long byteSize, List<T> list) {
if (Objects.isNull(list) || list.size() == 0) {
return new ArrayList<>();
}

if (byteSize <= 100) {
throw new RuntimeException("参数 byteSize 值不小于 100 bytes!");
}
ListSizeSplitUtil.maxByteSize = byteSize;


if (isSurpass(List.of(list.get(0)))) {
throw new RuntimeException("List 中,单个对象都大于 byteSize 的值,分割失败");
}

List<List<T>> result = new ArrayList<>();

List<T> itemList = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
itemList.add(list.get(i));

if (isSurpass(itemList)) {
i = i - 1;
itemList.remove(itemList.size() - 1);
result.add(new ArrayList<>(itemList));
itemList = new ArrayList<>();
}
}
result.add(new ArrayList<>(itemList));
return result;
}


private static <T> Boolean isSurpass(List<T> obj) {
// 字节(byte)
long objSize = RamUsageEstimator.sizeOfAll(obj.toArray());
return objSize >= ListSizeSplitUtil.maxByteSize;
}
}

至此呢,生产者的逻辑就算是完成了,每次有消息的时候就调用这个方法即可。


2.2 消费者


位置:cn.j3code.user.mq.consumer


@Slf4j
@Component
@AllArgsConstructor
@RocketMQMessageListener(topic = RocketMQConstants.USER_MESSAGE_CENTER_TOPIC,
consumerGroup = RocketMQConstants.GROUP,
messageModel = MessageModel.CLUSTERING,
consumeMode = ConsumeMode.CONCURRENTLY
)

public class LikeAndCommentMessageConsumer implements RocketMQListener<LikeAndCommentMessageDTO> {

private final UserInboxService userInboxService;

@Override
public void onMessage(LikeAndCommentMessageDTO message) {
userInboxService.saveMessage(message);
}
}

saveMessage 方法的逻辑就是将消息保存到 MySQL 中,至此消息的产生和存储就算完成了,下面来看看用户如何查看吧!


2.3 用户消息查看


对于用户查看普通的消息就是访问一下 MySQL,并且更新一下最新读取的字段值即可,我贴一下关键代码就行了,代码如下:


public IPage<UserMessageVO> page(UserMessagePageRequest request) {
// 获取消息
IPage<UserMessageVO> page = getBaseMapper().page(new Page<UserMessageVO>(request.getCurrent(), request.getSize()), request);

if (CollectionUtils.isEmpty(page.getRecords())) {
return page;
}
// 记录一下消息读取位置,默认进来就把全部消息读完了,类似掘金
if (request.getCurrent() == 1) {
if (Objects.isNull(page.getRecords().get(0).getReadPositionId()) ||
page.getRecords().get(0).getReadPositionId() == 0) {
UserInbox userInbox = new UserInbox();
userInbox.setId(page.getRecords().get(0).getId());
userInbox.setReadPositionId(userInbox.getId());
updateById(userInbox);
}
}
return page;
}

2.4 系统消息查看


对于系统消息的查看也是,只贴出关键代码,查询和更新读取记录逻辑,代码如下:


@Override
public IPage<SysOutboxVO> lookSysPage(SysOutboxPageRequest request) {
Page<SysOutbox> page = lambdaQuery()
.orderByDesc(SysOutbox::getId)
.page(new Page<>(request.getCurrent(), request.getSize()));
IPage<SysOutboxVO> outboxVOIPage = page.convert(userInboxConverter::converter);
if (CollectionUtils.isEmpty(outboxVOIPage.getRecords())) {
return outboxVOIPage;
}
// 记录一下消息读取位置,默认进来就把全部消息读完了,类似掘金
if (request.getCurrent() == 1) {
userReadSysOutboxService.updateReadLog(page.getRecords().get(0).getId(), SecurityUtil.getUserId());
}
return outboxVOIPage;
}

这里,可能有人会发现,没有按照上面分析的那用从缓存中读,是的。这里的实现我没有用到 Redis,这里我偷了一下懒,如果有拿到我代码的同学可以试着优化一下这个逻辑。


作者:J3code
来源:juejin.cn/post/7274922643453853735
收起阅读 »

从《孤注一掷》出发,聊聊 SSL 证书的重要性

你去看《孤注一掷》了吗?相信最近大家的朋友圈和抖音都被爆火电影《孤注一掷》成功刷屏。取材于上万真实案例的《孤注一掷》揭露了缅甸诈骗园区残暴的统治,以及电信诈骗中系统性极强的诈骗技巧,引发了大量讨论。 图片来源于电影《孤注一掷》 这部电影除了让人后背发凉外,也...
继续阅读 »

你去看《孤注一掷》了吗?相信最近大家的朋友圈和抖音都被爆火电影《孤注一掷》成功刷屏。取材于上万真实案例的《孤注一掷》揭露了缅甸诈骗园区残暴的统治,以及电信诈骗中系统性极强的诈骗技巧,引发了大量讨论。
image001.png
图片来源于电影《孤注一掷》


这部电影除了让人后背发凉外,也不禁让人回忆起了曾经上网冲浪遇到的种种现象:看小说时性感荷官总在网页右下角在线发牌;看电影时网页左下角常常蹦出“在线老虎机”……这些让人烦不胜烦的广告弹窗之所以出现,要么是建站人员利欲熏心投放了非法广告,要么就是因为网站使用了不安全的 HTTP 协议而遭到了攻击,正常的网页内容被恶意篡改。


网站是电信诈骗、网络赌博等非法内容出现的重灾区,建站者和使用者都应该提高安全意识,特别是对建站者来说,保护通信安全才能更好的承担起建站责任。本文将从 HTTP 讲起,介绍 HTTPS 保护通信安全的原理,以及作为网络通信安全基石的 SSL 证书的重要性。


HTTP 协议


HTTP(Hyper Text Transfer Protocol)协议是超文本传输协议。它是从 WEB 服务器传输超文本标记语言(HTML)到本地浏览器的传送协议。HTTP 基于 TCP/IP 通信协议来传递数据,通信双方在 TCP 握手后即可开始互相传输 HTTP 数据包。具体过程如下图所示:
image003.jpg
HTTP 建立流程


HTTP 协议中,请求和响应均以明文传输。如下图所示,在访问一个使用 HTTP 协议的网站时,通过抓包软件可以看到网站 HTTP 响应包中的完整 HTML 内容。


image005.png


虽然 HTTP 明文传输的机制在性能上带来了优势,但同时也引入了安全问题:



  • 缺少数据机密性保护。HTTP 数据包内容以明文传输,攻击者可以轻松窃取会话内容。

  • 缺少数据完整性校验。通信内容以明文传输,数据内容可被攻击者轻易篡改,且双方缺少校验手段。

  • 缺少身份验证环节。攻击者可冒充通信对象,拦截真实的 HTTP 会话。


HTTP 劫持


作为划时代的互联网通信标准之一,HTTP 协议的出现为互联网的普及做出了不可磨灭的贡献。但正如上节谈到, HTTP 协议因为缺少加密、身份验证的过程导致很可能被恶意攻击,针对 HTTP 协议最常见的攻击就是 HTTP 劫持。


HTTP 劫持是一种典型的中间人攻击。HTTP 劫持是在使用者与其目的网络服务所建立的数据通道中,监视特定数据信息,当满足设定的条件时,就会在正常的数据流中插入精心设计的网络数据报文,目的是让用户端程序解析“错误”的数据,并以弹出新窗口的形式在使用者界面展示宣传性广告或直接显示某网站的内容。


下图是一种典型的 HTTP 劫持的流程。当客户端给服务端发送 HTTP 请求,图中发送请求为“梁安娜的电话号码是?”,恶意节点监听到该请求后将其放行给服务端,服务端返回正常 HTML 响应,关键返回内容本应该是“+86 130****1234”,恶意节点监听到该响应,并将关键返回内容篡改为泰国区电话“+66 6160 *88”,导致用户端程序展示出错误信息,这就是 HTTP 劫持的全流程。


image007.jpg
HTTP 劫持流程


例如,在某网站阅读某网络小说时,由于该网站使用了不安全的 HTTP 协议,攻击者可以篡改 HTTP 相应的内容,使网页上出现与原响应内容无关的广告,引导用户点击,可能将跳转进入网络诈骗或其他非法内容的页面。


image009.png
原网页


image011.png
HTTP 劫持后网页


HTTPS 工作原理


HTTPS 协议的提出正是为了解决 HTTP 带来的安全问题。HTTPS 协议(HyperText Transfer Protocol Secure,超文本传输安全协议),是一种通过计算机网络进行安全通信的传输协议。HTTPS 经由 HTTP 进行通信,但利用 SSL/TLS 来加密数据包。HTTPS 的开发主要是提供对网站服务器的身份认证,保护交换资料的隐私性与完整性。


TLS 握手是 HTTPS 工作原理的安全基础部分。TLS 传统的 RSA 握手流程如下所示:


image013.jpg
TLS 握手流程


TLS 握手流程主要可以分为以下四个部分:


第一次握手:客户端发送 Client Hello 消息。该消息包含:客户端支持的 SSL/TLS 协议版本(如 TLS v1.2 );用于后续生成会话密钥的客户端随机数 random_1;客户端支持的密码套件列表。


第二次握手:服务端收到 Client Hello 消息后,保存随机数 random_1,生成随机数 random_2,并发送以下消息。



  • 发送 Server Hello 消息。该消息包含:服务端确认的 SSL/TLS 协议版本(如果双方支持的版本不同,则关闭加密通信);用于后续生成会话密钥的服务端随机数 random_2;服务端确认使用的密码套件

  • 发送“Server Certificate”消息。该消息包含:服务端的 SSL 证书。SSL 证书又包含服务端的公钥、身份等信息。

  • 发送“Server Hello Done”消息。该消息表明 ServerHello 及其相关消息的结束。发送这个消息之后,服务端将会等待客户端发过来的响应。


第三次握手:客户端收到服务端证书后,首先验证服务端证书的正确性,校验服务端身份。若证书合法,客户端生成预主密钥,之后客户端根据(random_1, random_2, 预主密钥)生成会话密钥,并发送以下消息。



  • 发送“Client Key Exchange”消息,该消息为客户端生成的预主密钥,预主密钥会被服务端证书中的公钥加密后发送。

  • 发送“Change Cipher Spec”消息,表示之后数据都将用会话密钥进行加密。

  • 发送“Encrypted Handshake Message”消息,表示客户端的握手阶段已经结束。客户端会生成所有握手报文数据的摘要,并用会话密钥加密后发送给服务端,供服务端校验。


第四次握手:服务端收到客户端的消息后,利用自己的服务端证书私钥解密出预主密钥,并根据(random_1, random_2, 预主密钥)计算出会话密钥,之后发送以下消息。



  • 发送“Change Cipher Spec”消息,表示之后数据都将用会话密钥进行加密。

  • 发送“Encrypted Handshake Message”,表示服务端的握手阶段已经结束,同时服务端会生成所有握手报文数据的摘要,并用会话密钥加密后发送给客户端,供客户端校验。


根据 TLS 握手流程,可以看出它是如何解决 HTTP 协议缺陷,以及避免中间人攻击的:


1.规避窃听风险,攻击者无法获知通信内容


在客户端进行真正的 HTTPS 请求前,客户端与服务端都已经拥有了本次会话中用于加密的对称密钥,后续双方 HTTPS 会话中的内容均用该对称密钥加密,攻击者在无法获得该对称密钥的情况下,无法解密获得会话中内容的明文。即使攻击者获得了 TLS 握手中双方发送的所有明文信息,也无法从这些信息中恢复对称密钥,这是由大数质因子分解难题和有限域上的离散对数难题保证的。


2.规避篡改风险,攻击者无法篡改通信内容


在数据通信阶段,双端消息发送时会对原始消息做一次哈希,得到该消息的摘要后,与加密内容一起发送。对端接受到消息后,使用协商出来的对称加密密钥解密数据包,得到原始消息;接着也做一次相同的哈希算法得到摘要,对比发送过来的消息摘要和计算出的消息摘要是否一致,可以判断通信数据是否被篡改。


3.规避冒充风险,攻击者无法冒充身份参与通信


在 TLS 握手流程中的第二步“Server Hello”中,服务端将自己的服务端证书交付给客户端。客户端拿到 SSL 证书后,会对服务端证书进行一系列校验。以浏览器为例,校验服务端证书的过程为:



  • 验证证书绑定域名与当前域名是否匹配。

  • 验证证书是否过期,是否被吊销。

  • 查找操作系统中已内置的受信任的证书发布机构 CA(操作系统会内置有限数量的可信 CA),与服务端证书中的颁发者 CA 比对,验证证书是否为合法机构颁发。如果服务端证书不是授信 CA 颁发的证书,则浏览器会提示服务端证书不可信。

  • 验证服务端证书的完整性,客户端在授信 CA 列表中找到服务端证书的上级证书,后使用授信上级证书的公钥验证服务端证书中的签名哈希值。

  • 在确认服务端证书是由国际授信 CA 签发,且完整性未被破坏后,客户端信任服务端证书,也就确认了服务端的正确身份。


SSL 证书


正如上一节介绍,SSL 证书在 HTTPS 协议中扮演着至关重要的作用,即验证服务端身份,协助对称密钥协商。只有配置了 SSL 证书的网站才可以开启 HTTPS 协议。在浏览器中,使用 HTTP 的网站会被默认标记为“不安全”,而开启 HTTPS 的网站会显示表示安全的锁图标。


image015.png
使用 HTTP 协议的网站


image028.gif
使用 HTTPS 协议的网站


从保护范围、验证强度和适用类型出发, SSL 证书会被分成不同的类型。只有了解类型之间的区别,才能根据实际情况选择更适合的证书类型,保障通信传输安全。


从保护范围分,SSL 证书可以分为单域名证书、通配符证书、多域名证书。



  • 单域名证书:单域名证书只保护一个域名,这些域名形如 http://www.test.com 等。

  • 通配符证书:通配符证书可以保护基本域和无限的子域。通配符 SSL 证书的公用名中带有星号 ,其中,星号表示具有相同基本域的任何有效子域。例如,。test.com 的通配符证书可用于保护 a.test.com、 b.test.com……

  • 多域名证书:多域证书可用于保护多个域或子域。包括完全唯一的域和具有不同顶级域的子域(本地/内部域除外)的组合。


从验证强度和适用类型进一步区分,SSL 证书可以分为 DV、OV、EV 证书。



  • DV(Domain Validated):域名验证型。在颁发该类型证书时,CA 机构仅验证申请者对域名的所有权。CA 机构会通过检查 WHOIS、DNS 的特定记录来确认资格。一般来说,DV 证书适用于博客、个人网站等不需要任何私密信息的网站。

  • OV(Organization Validated):组织验证型。OV 证书的颁发除了要验证域名所有权外,CA 还会额外验证申请企业的详细信息(名称、类型、地址)等。一般来说,OV 证书适用于中级商业组织。

  • EV(Extended Validation):扩展验证型。EV 证书的颁发除了 CA 对 DV 和 OV 证书所采取的所有身份验证步骤之外,还需要审查商业组织是否在真实运营、其实际地址,并致电以验证申请者的就业情况。一般来说,EV 证书适用于顶级商业组织。


结尾


随着互联网应用的普及,网络诈骗的方式也越发花样百出,让人防不胜防。


除了文内提到的网页环境,在软件应用、邮件、文档、物联网等领域同样存在恶意软件、钓鱼邮件、文档篡改、身份认证的问题。幸运的是,作为 PKI 体系下的优秀产品,证书体系同样在这些领域发挥着重要作用,软件签名证书、邮件签名证书、文档签名证书、私有证书等保护着各自领域的信息安全。


总有不法分子企图通过漏洞牟利,而证书体系在保护数据机密性、完整性、可用性以及身份验证场景上有着无可取代的地位,牢牢守护着用户信息,保障通信安全。


作者:火山引擎边缘云
来源:juejin.cn/post/7273685263841263672
收起阅读 »

使用 Vim 两年后的个人总结

为什么要使用 Vim 学习动机非常重要。并不是很多大牛程序员用 Vim 编程,你就应该去学习 Vim,如果你是这种心态,很大的概率,你会在几次尝试以后最终放弃,就像我曾经做过的一样。因为 Vim 的学习曲线很陡峭,没有强烈的学习动机很难坚持下来。 那我为什么后...
继续阅读 »

为什么要使用 Vim


学习动机非常重要。并不是很多大牛程序员用 Vim 编程,你就应该去学习 Vim,如果你是这种心态,很大的概率,你会在几次尝试以后最终放弃,就像我曾经做过的一样。因为 Vim 的学习曲线很陡峭,没有强烈的学习动机很难坚持下来。


那我为什么后来又重新开始学习 Vim,并在两年多后已经习惯、喜欢甚至离不开 Vim?原因很简单,我必须掌握 Vim。


我是一个很爱折腾的人,自己买过很多云服务器,也经常会在服务器上写一些程序,编辑器当然首选 Vim。日复一日,当有一天我实在无法忍受自己在服务端极其低效的编程体验后,我决定真正掌握 Vim。从那时候起,我开始刻意频繁练习,也终于有一天,我发现我完全存活了下来,并且喜欢上了 Vim。


我并不是说你一定要买个云服务器,然后在云服务器上写代码(其实现在你可以用 VSCode 的远程功能在服务器上写代码),我想表达的是,你一定要有足够的学习动机,这个学习动机往往来自于必要性,不管是工作上的必要性,还是自己业余项目上的必要性。也许,有强烈的炫耀动机可能也行。


当然了,当你真正喜欢上 Vim,你会有新的理解,比如 Vim 某种意义上代表了一些正向的价值,文章最后我会提到这一点。


关于 Normal 模式的最佳隐喻


《代码大全》(Code Complete)开头就讲了“软件构建的隐喻”,隐喻是非常好的方式,能够通过熟悉的事物帮我们建立正确的思维模型。关于 Vim 为什么要有 Normal 模式,我看过的最好的隐喻来自《Practical Vim》这本书,我摘录几个关键的段落:



Think of all of the things that painters do besides paint. They study their subject, adjust the lighting, and mix paints into new hues. And when it comes to applying paint to the canvas, who says they have to use brushes? A painter might switch to a palette knife to achieve a different texture or use a cotton swab to touch up the paint that's already been applied.




The painter does not rest with a brush on the canvas. And so it is with Vim. Normal mode is the natural resting state. The clue is in the name, really.




Just as painters spend a fraction of their time applying paint,programmers spend a fraction of their time composing code . More time is spent thinking, reading, and navigating from one part of a codebase to another. And when we do want to make a change, who says we have to switch to Insert mode? We can reformat existing code, duplicate it, move it around, or delete it. From Normal mode, we have many tools at our disposal.



作者把编程比喻成绘画,把 Normal 模式比喻成画家作画的间隙。就像画家要经常放下画笔,走远处看看,或用小刀、棉球等工具修改画作一样,程序员也不会一直输入代码(Insert 模式),程序员也需要思考,需要对程序做一些修改(不一定是插入内容),那么这个时候就应该进入 Normal 模式。Normal 模式让程序员休息、思考,同时提供了更多的工具,比如删除、复制、黏贴、跳转光标等等。每当写程序需要停顿思考的时候,就可以进入 Normal 模式。


一个最重要的模式


这里的模式,不是指“Normal”或“Insert”模式。而是我们在使用 Vim 组合快捷键时候的“操作模式”。这个最重要的模式如下:

Action = Operator + Motion

举一个例子,“删除当前到句尾的所有字符”的操作是d$d$ = d + $,其中的 d 即为 Operator,也即操作,$ 即为 Motion,也即操作的范围。这个模式在 Vim 中无处不在,再举一些例子:

  • dap,删除一整个段落;
  • yG,复制当前行到文件末尾所有内容;
  • cw, 修改当前单词(删除单词并进入 Insert 模式);

这是最基本的模式,也是 Vim 编辑器能高效编辑文本的基础,它把常用的 Operator 和 Motion 做了抽象,抽象成了一些简单字母,比如 d 代表删除操作,$代表句子末尾,而这些抽象符号又可以通过同一个公式组合使用,减轻了记忆负担。这是 Vim 非常优雅的地方。 不过有一个例外,如果你连续输入两个 Operator,就表示对当前行进行操作。比如 yy 表示复制当前行。


那 Vim 中有哪些常用的 Operator 呢,有以下这些:


至于 Motion,有更多,以下也是一些常用的:


当然还有更多,如果你感兴趣,可以在 Vim 的 Normal 模式下,输入以下命令查看完整的文档:

:h motion.txt

先存活下来


在成为 Vim 高手之前,我们的首要目标是先存活下来。这个目标其实并不难。


掌握基本的光标跳转,比如hlkj0^$ggG 等等,以及以上说的基本操作模式后,你大概率可以生存下来。当然知道不等于掌握,你需要频繁地练习把基本操作变成肌肉记忆。我一开始是跟着左耳朵耗子(在此纪念耗子叔)的文章《简明 VIM 练级攻略》练习,当时一旦有时间就打开文章,跟着内容逐条操作,一段时间后,我就真的存活下来了。


如果你也顺利存活了下来,在实际的开发过程中就已经可以使用 Vim 做一些编辑工作了,但可能总还是觉得哪儿哪儿不对劲,要完全行云流水还欠缺更多技巧。这个时候或许有必要去看看《Practival Vim》或者类似的书,更好地掌握 Vim 的设计理念以及许多细微的地方,同样配合不断的练习,我相信你迟早有一天会欣喜地发现自己在编码的时候几乎可以放弃鼠标了,这种喜悦或许类似于修仙小说中的破境。


恭喜你。


成为 Vim 高手的终极秘诀


其实没有秘诀。Vim 很快,但成为 Vim 高手是一个相对漫长的过程,在这个过程中你会掌握更多微妙的技能,比如如何更高效地使用 f{char} 命令更快地定位到某个字符。在生存下来以后,你唯一能做的就是每天使用 Vim。慢慢地, Vim 的使用会变成水和空气一样的自然存在,你从此离不开它。


如何每天使用 Vim 呢,以下是我的一些建议:

  • 把 Vim 变成日常开发工具。学习使用 Neovim,它提供了更好的插件和扩展机制,你如果愿意你甚至可以把 Neovim 配置成强大的 IDE。这里推荐一下掘金小册 Neovim 配置实战
  • 如果习惯使用 VSCode 或其他编辑器,可以安装相应的 Vim 插件
  • 如果你使用 Chrome 浏览器,你可以安装相应的 Vim 插件来提升浏览效率。
  • 平时习惯做笔记?那就使用一款支持 Vim 快捷键的笔记软件,比如我最喜欢的 Obsidian
  • 经常在 Cloud IDE 上写代码?建议使用一款支持 Vim 快捷键的 IDE,比如我常用的 Replit

总之,在我决定使用 Vim 提高编程效率以后,在任何编辑场景我都变得无法忍受没有 Vim 的存在,就是这么自然,它变成了我工作的一部分。Reddit 上有这样一条讨论,If using vim is a lifestyle/philosophy, what other products also fits into this lifestyle?,把 Vim 隐喻成一种生活方式/哲学确实很合适,Vim 和学习使用 Vim 隐含了一些有价值的东西,我相信大约有追求极致——更快更强的精神,坚持长期主义——忍受暂时痛苦,着眼长远的精神。或许也可以这么说,如果你有朝一日能成为 Vim 高手,你大概率也能做成其他许多困难的事。


少年们,加油。


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

把数据库里的未付款订单改成已付款,会发生什么

导言 不知道大家在网上购物的时候,有没有这样的念头,如果能把未付款的订单偷偷用一条SQL改成已付款,该多么美好啊。那么在实际开发过程中,我们应当如何保证数据库里的数据在保存后不会被偷偷更改? 大家好我是日暮与星辰之间,创作不易,如果觉得有用,求点赞,求收藏,...
继续阅读 »

导言


不知道大家在网上购物的时候,有没有这样的念头,如果能把未付款的订单偷偷用一条SQL改成已付款,该多么美好啊。那么在实际开发过程中,我们应当如何保证数据库里的数据在保存后不会被偷偷更改?



大家好我是日暮与星辰之间,创作不易,如果觉得有用,求点赞,求收藏,求转发,谢谢。



理论


在介绍具体的内容之间,先介绍MD5算法,简单的来说,MD5能把任意大小、长度的数据转换成固定长度的一串字符,经常玩大型游戏的朋友应该都注意到过,各种补丁包、端游客户端之类的大型文件一般都附有一个MD5值,用于确保你下载文件的完整性。那么在这里,我们可以借鉴其思想,对订单的某些属性进行加密计算,得出来一个 MD5值一并保存在数据库当中。从数据库取出数据后第一时间进行校验,如果有异常更改,那么及时抛出异常进行人工处理。


实现


道理我都懂,但是我要如何做呢,别急,且听我一一道来。


这种需求听起来并不强绑定于某个具体的业务需求,这就要用到了我们熟悉的鼎鼎有名的AOP(面向切面编程)来实现。


首先定义四个类型的注解作为AOP的切入点。@Sign@Validate都是作用在方法层面的,分别用于对方法的入参进行加签和验证方法的返回值的签名。@SignField用于注解关键的不容篡改的字段。@ValidateField用于注解保存计算后得出的签名值。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sign {
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Validate {
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SignField {
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidField {
}

以订单的实体为例 sn,amt,status,userId就是关键字段,绝不能允许有人在落单到数据库后对这些字段偷偷篡改。

public class Order {
@SignField
private String sn;
@SignField
private String amt;
@SignField
private int status;
@SignField
private int userId;
@ValidField
private String sign;
}

下面就到了重头戏的部分,如何通过AOP来进行实现。


1. 定义切入点

@Pointcut("execution(@com.example.demo.annotations.Sign * *(..))")
public void signPointCut() {

}

@Pointcut("execution(@com.example.demo.annotations.Validate * *(..))")
public void validatePointCut() {

}

2.环绕切入点

@Around("signPointCut()")
public Object signAround(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
for (Object o : args) {
System.out.println(o);
sign(o);
}
Object res = pjp.proceed(args);
return res;
}

@Around("validatePointCut()")
public Object validateAround(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
Object res = pjp.proceed(args);
valid(res);
return res;
}

3. 签名的实现

  • 获取需要签名字段
private Map<String, String> getSignMap(Object o) throws IllegalAccessException {
Map<String, String> fieldNameToValue = new HashMap<>();
for (Field f : o.getClass().getDeclaredFields()) {
System.out.println(f.getName());
for (Annotation annotation : f.getDeclaredAnnotations()) {
if (annotation.annotationType().equals(SignField.class)) {
String value = "";
f.setAccessible(true);
fieldNameToValue.put(f.getName(), f.get(o).toString());
}
}
}
return fieldNameToValue;
}
  • 计算出签名值,这里在属性名和属性值以外加入了我的昵称以防止他人猜测,同时使用了自定义的分隔符来加强密码强度。
private String getSign(Map<String, String> fieldNameToValue) {
List<String> names = new ArrayList<>(fieldNameToValue.keySet());
StringBuilder sb = new StringBuilder();
for (String name : names)
sb.append(name).append("@").append(fieldNameToValue.get(name));
System.out.println(sb.append("日暮与星辰之间").toString());
String signValue = DigestUtils.md5DigestAsHex(sb.toString().getBytes(StandardCharsets.UTF_8));
return signValue;
}

  • 找到保存签名的字段
private Field getValidateFiled(Object o) {
for (Field f : o.getClass().getDeclaredFields()) {
for (Annotation annotation : f.getDeclaredAnnotations()) {
if (annotation.annotationType().equals(ValidField.class)) {
return f;
}
}
}
return null;
}

  • 对保存签名的字段进行赋值
public void sign(Object o) throws IllegalAccessException {
Map<String, String> fieldNameToValue = getSignMap(o);
if (fieldNameToValue.isEmpty()) {
return;
}
Field validateField = getValidateFiled(o);
if (validateField == null)
return;
String signValue = getSign(fieldNameToValue);
validateField.setAccessible(true);
validateField.set(o, signValue);
}

  • 对从数据库中取出的对象进行验证
public void valid(Object o) throws IllegalAccessException {
Map<String, String> fieldNameToValue = getSignMap(o);
if (fieldNameToValue.isEmpty()) {
return;
}
Field validateField = getValidateFiled(o);
validateField.setAccessible(true);
String signValue = getSign(fieldNameToValue);
if (!Objects.equals(signValue, validateField.get(o))) {
throw new RuntimeException("数据非法");
}

}

使用示例


对将要保存到数据库的对象进行签名

@Sign
public Order save( Order order){
orderList.add(order);
return order;
}

验证从数据库中取出的对象是否合理

@Validate
public Order query(@ String sn){
return orderList.stream().filter(e -> e.getSn().equals(sn)).findFirst().orElse(null);
}

好文分享 ⬇️
从Offer收割机到延毕到失业再到大厂996,二零二二我的兵荒马乱 - 掘金


另类年终总结:在煤老板开的软件公司实习是怎样一种体验? - 掘金


第一次值守双十一,居然没有任何意外发生?! - 掘金


大厂996三个月,我曾迷失了生活的意义,努力找回中 - 掘金


阿里实习三个月,我学会了面试时讲好自己的项目,欢迎提问 - 掘金


迟到的苏州微软实习历险记 - 掘金


什么时候要用到本地缓存,比Redis还要快?怎么用? - 掘金


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

聊一聊过度设计!

  新手程序员在做设计时,因为缺乏经验,很容易写出欠设计的代码,但有一些经验的程序员,尤其是在刚学习过设计模式之后,很容易写出过度设计的代码,而这种代码比新手程序员的代码更可怕,过度设计的代码不仅写出来时的成本很高,后续维护的成本也高。因为相对于毫无设计的代码...
继续阅读 »

  新手程序员在做设计时,因为缺乏经验,很容易写出欠设计的代码,但有一些经验的程序员,尤其是在刚学习过设计模式之后,很容易写出过度设计的代码,而这种代码比新手程序员的代码更可怕,过度设计的代码不仅写出来时的成本很高,后续维护的成本也高。因为相对于毫无设计的代码,过度设计的代码有比较高的理解成本。说这么多,到底什么是过度设计?


什么是过度设计?


  为了解释清楚,我这里用个类比,假如你想拧一颗螺丝,正常的解决方案是找一把螺丝刀,这很合理对吧。 但是有些人就想:“我就要一个不止能拧螺丝的工具,我想要一个可以干各种事的工具!”,于是就花大价钱搞了把瑞士军刀。在你解决“拧螺丝”问题的时候,重心早已从解决问题转变为搞一个工具,这就是过度设计。

   再举个更技术的例子,假设你出去面试,面试官让你写一个程序,可以实现两个数的加减乘除,方法出入参都给你提供好了 int calc(int x, int y, char op),普通程序员可能会写出以下实现。

    public int calc(int x, int y, int op) {
if (op == '+') {
return x + y;
} else if (op == '-') {
return x - y;
} else if (op == '*') {
return x * y;
} else {
return x / y;
}
}

  而高级程序员会运用设计模式,写出这样的代码:

public interface Strategy {
int calc(int x, int y);
}

public class AddStrategy implements Strategy{
@Override
public int calc(int x, int y) {
return x + y;
}
}

public class MinusStrategy implements Strategy{
@Override
public int calc(int x, int y) {
return x - y;
}
}
/**
* 其他实现
*/
public class Main {
public int calc(int x, int y, int op) {
Strategy add = new AddStrategy();
Strategy minux = new MinusStrategy();
Strategy multi = new MultiStrategy();
Strategy div = new DivStrategy();
if (op == '+') {
return add.calc(x, y);
} else if (op == '-') {
return minux.calc(x, y);
} else if (op == '*') {
return multi.calc(x, y);
} else {
return div.calc(x, y);
}
}
}

  策略模式好处在于将计算(calc)和具体的实现(strategy)拆分,后续如果修改具体实现,也不需要改动计算的逻辑,而且之后也可以加各种新的计算,比如求模、次幂……,扩展性明显增强,很是牛x。 但光从代码量来看,复杂度也明显增加。回到我们原始的需求上来看,如果我们只是需要实现两个整数的加减乘除,这明显过度设计了。


过度设计的坏处


  个人总结过度设计有两大坏处,首先就是前期的设计和开发的成本问题。过度设计的方案,首先设计的过程就需要投入额外的时间成本,其次越复杂的方案实现成本也就越高、耗时越长,如果是在快速迭代的业务中,这些可能都会决定到业务的生死。其次即便是代码正常上线后,其复杂度也会导致后期的维护成本高,比如当你想将这些代码交接给别人时,别人也需要付出额外的学习成本。


  如果成本问题你都可以接受,接下来这个问题可能影响更大,那就是过度设计可能会影响到代码的灵活性,这点听起来和做设计的目的有些矛盾,做设计不就是为了提升代码的灵活性和扩展性吗!实际上很多过度设计的方案搞错了扩展点,导致该灵活的地方不灵活,不该灵活的地方瞎灵活。在机器学习领域,有个术语叫做“过拟合”,指的是算法模型在测试数据上表现完美,但在更广泛的数据上表现非常差,模式缺少通用性。 过度设计也会出现类似的现象,就是缺少通用性,在面对稍有差异的需求上时可能就需要伤筋动骨级别的改造了。


如何避免过度设计


  既然过度设计有着成本高和欠灵活的问题,那如何避免过度设计呢!我这里总结了几个方法,希望可以帮到大家。


充分理解问题本身


  在设计的过程中,要确保充分理解了真正的问题是什么,明确真正的需求是什么,这样才可以避免做出错误的设计。


保持简单


  过度设计毫无例外都是复杂的设计,很多时候未来有诸多的不确定性,如果过早的针对某个不确定的问题做出方案,很可能就白做了,等遇到真正问题的时候再去解决问题就行。


小步快跑


  不要一开始就想着做出完美的方案,很多时候优秀的方案不是设计出来的,而是逐渐演变出来的,一点点优化已有的设计方案比一开始就设计出一个完美的方案容易得多。


征求其他人的意见


  如果你不确定自己的方案是不是过度设计了,可以咨询下其他人的,尤其是比较资深的人,交叉验证可以快速让你确认问题。


总结


  其实在业务的快速迭代之下,很难判定当前的设计是欠设计还是过度设计,你当前设计了一个简单的方案,未来可能无法适应更复杂的业务需求,但如果你当前设计了一个复杂的方案,有可能会浪费时间……。 在面对类似这种不确定性的时候,我个人还是比较推崇大道至简的哲学,当前用最简单的方案,等需要复杂性扩展的时候再去重构代码。


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

Moshi:现代 Json 解析库全解析

json 解析框架,很容易想到 Gson、fastJson 等。而这些流行框架对 kotlin 的支持并不好,而Moshi 天生对 kotlin 友好。 前言 Gson 通过反射反序列化数据,Java 类默认有无参构造函数,对于默认参数能够很好的支持。对于 k...
继续阅读 »

json 解析框架,很容易想到 Gson、fastJson 等。而这些流行框架对 kotlin 的支持并不好,而Moshi 天生对 kotlin 友好。


前言


Gson 通过反射反序列化数据,Java 类默认有无参构造函数,对于默认参数能够很好的支持。对于 kotlin ,我们经常使用的 data class,其往往没有无参构造函数,Gson 便会通过 UnSafe 的方式创建实例,成员无法正常初始化默认值。为了勉强能用,只能将构造参数都加上默认值才行,不过这种兼容方式太过隐晦,有潜在的维护风险。


另外,Gson 无法支持 kotlin 空安全特性。定义为不可空且无默认值的字段,在没有该字段对应的 json 数据时会被赋值为 null,这可能导致使用时引发空指针问题。


Moshi


Moshi 是一个适用于 Android、Java 和 Kotlin 的现代 JSON 库。它可以轻松地将 JSON 解析为 Java 和 Kotlin 类。


val json: String = ...

val moshi: Moshi = Moshi.Builder().build()
val jsonAdapter: JsonAdapter = moshi.adapter()

val person = jsonAdapter.fromJson(json)

通过类型适配器 JsonAdapter 可以对数据类型 T 进行序列化/反序列化操作,即 toJsonfromJson 方法。


内置类型适配器


moshi 内置支持以下类型的类适配器:

  • 基本类型
  • Arrays, Collections, Lists, Sets, Maps
  • Strings
  • Enums


直接或间接由它们构成的自定义数据类型都可以直接解析。


反射 OR 代码生成


moshi 支持反射和代码生成两种方式进行 Json 解析。


反射的好处是无需对数据类做任何变动,可以解析 private 和 protected 成员,缺点是引入反射相关库,包体积增大2M多,且反射在性能上稍差。


代码生成的好处是速度更快,缺点是需要对数据类添加注解,无法处理 private 和 protected 成员,用于编译时生成代码,影响编译速度,且注解使用越来越多生成的代码也会越来越多。


反射方案依赖:


implementation("com.squareup.moshi:moshi-kotlin:1.14.0")

代码生成方案依赖(ksp):


plugins {
id("com.google.devtools.ksp").version("1.6.10-1.0.4") // Or latest version of KSP
}

dependencies {
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.14.0")
}

使用代码生成,需要使用注解 @JsonClass(generateAdapter = true) 修饰数据类:


@JsonClass(generateAdapter = true)
data class Person(
val name: String
)

使用反射时,需要添加 KotlinJsonAdapterFactoryMoshi.Builder


val moshi = Moshi.Builder()
.addLast(KotlinJsonAdapterFactory())
.build()

💡 注意:这里要使用 addLast 添加 KotlinJsonAdapterFactory,因为 Adapter 是按添加顺序排列和使用的,如果有自定义的 Adapter,为确保自定义的始终在前,建议通过 addLastKotlinJsonAdapterFactory 始终放在最后。


我们目前使用的是反射方案,主要考虑到侵入性低,数据类几乎无改动。


其实也可以两种方案都使用,Moshi 会优先使用代码生成的 Adapter,没有的话则走反射。


解析 JSON 数组


对于 json 数据:


[
{
"rank": "4",
"suit": "CLUBS"
},
{
"rank": "A",
"suit": "HEARTS"
}
]

解析:


String cardsJsonResponse = ...;
Type type = Types.newParameterizedType(List.class, Card.class);
JsonAdapter> adapter = moshi.adapter(type);
List cards = adapter.fromJson(cardsJsonResponse);

和 Gson 类似,为了运行时获取泛型信息,稍微麻烦点,可以定义扩展函数简化用法:


inline fun <reified T> Moshi.listAdapter(): JsonAdapter> {
val type = Types.newParameterizedType(List::class.java, T::class.java)
return adapter(type)
}

简化后:


String cardsJsonResponse = ...
val cards = moshi.listAdapter().fromJson(cardsJsonResponse)

自定义字段名


如果Json 中字段名和数据类中字段名不一致,或 json 中有空格,可以使用 @Json 注解修饰别名。


{
"username": "jesse",
"lucky number": 32
}

class Player {
val username: String
@Json(name = "lucky number") val luckyNumber: Int

...
}

忽略字段


使用 @Json(ignore = true) 可以忽略字段的解析,java 中的 @Transient 注解也可以。


class BlackjackHand(...) {
@Json(ignore = true)
var total: Int = 0

...
}

Java 支持


Moshi 同样支持 Java。需要注意的是,和 Gson 一样,Java 类需要有无参构造方法,否则成员变量的默认值无法生效。


public final class BlackjackHand {
private int total = -1;
...

public BlackjackHand(Card hidden_card, List visible_cards) {
...
}
}

如上,total 的默认值会为 0.


另外,和 Gson 不一样的是,Moshi 并不支持 JsonElement 这种中间产物,它只支持内置类型如 List、Map。


自定义 JsonAdapter


如果 json 的数据格式和我们想要的不一样,就需要我们自定义 JsonAdapter 来解析了。有意思的是,任何拥有 @Json@ToJson 注解的类都可以成为 Adapter,无需继承 JsonAdapter。


例如 json 格式:


{
"title": "Blackjack tournament",
"begin_date": "20151010",
"begin_time": "17:04"
}

目标数据类定义:


class Event(
val title: String,
val beginDateAndTime: String
)

我们希望 json 中日期 begin_date 和时间 begin_time 组成 beginDateAndTime 字段。moshi 支持我们在 json 和目标数据转换间定义一个中间类,json 和中间类转换后再转换为最终类型。


定义中间类型,本例中即和 json 匹配的数据类型:


class EventJson(
val title: String,
val begin_date: String,
val begin_time: String
)

定义 Adapter :


class EventJsonAdapter {
@FromJson
fun eventFromJson(eventJson: EventJson): Event {
return Event(
title = eventJson.title,
beginDateAndTime = "${eventJson.begin_date} ${eventJson.begin_time}"
)
}

@ToJson
fun eventToJson(event: Event): EventJson {
return EventJson(
title = event.title,
begin_date = event.beginDateAndTime.substring(0, 8),
begin_time = event.beginDateAndTime.substring(9, 14),
)
}
}

将 adapter 注册到 moshi:


val moshi = Moshi.Builder()
.add(EventJsonAdapter())
.build()

这样就可以使用 moshi 直接将 json 转换成 Event 了。本质是将 Json 和目标数据的相互转换加了个中间步骤,先转换为中间产物,再转为最终 Json 或数据实例。


@JsonQualifier:自定义字段类型解析


如下 json,color 为十六进制 rgb 格式的字符串:


{
"width": 1024,
"height": 768,
"color": "#ff0000"
}

数据类,color 为 Int 类型:


class Rectangle(
val width: Int,
val height: Int,
val color: Int
)

Json 中 color 字段类型是 String,数据类同名字段类型为 Int,除了上面介绍的自定义 JsonAdapter 外,还可以自定义同一数据的不同数据类型间的转换。


首先自定义注解:


@Retention(RUNTIME)
@JsonQualifier
annotation class HexColor

使用注解修饰字段:


class Rectangle(
val width: Int,
val height: Int,
@HexColor val color: Int
)

自定义 Adapter:


/** Converts strings like #ff0000 to the corresponding color ints.  */
class ColorAdapter {
@ToJson fun toJson(@HexColor rgb: Int): String {
return "#x".format(rgb)
}

@FromJson @HexColor fun fromJson(rgb: String): Int {
return rgb.substring(1).toInt(16)
}
}

通过这种方式,同一字段可以有不同的解析方式,可能不多见,但的确有用。


适配器组合


举个例子:


class UserKeynote(
val type: ResourceType,
val resource: KeynoteResource?
)

enum class ResourceType {
Image,
Text
}

sealed class KeynoteResource(open val id: Int)

data class Image(
override val id: Int,
val image: String
) : KeynoteResource(id)

data class Text(
override val id: Int,
val text: String
) : KeynoteResource(id)

UserKeynote 是目标类,其中的 KeynoteResource 可能是 ImageText ,具体是哪个需要根据 type 字段来决定。也就是说 UserKeynote 的解析需要 Image 或 Text 对应的 Adapter 来完成,具体是哪个取决于 type 的值。


显然自带的 Adapter 不能满足需求,需要自定义 Adapter。


先看下 Adapter 中签名要求(参见源码 AdapterMethodsFactory.java):


@FromJson


 R fromJson(JsonReader jsonReader) throws 

R fromJson(JsonReader jsonReader, JsonAdapter delegate, ) throws

R fromJson(T value) throws

@ToJson


 void toJson(JsonWriter writer, T value) throws 

void toJson(JsonWriter writer, T value, JsonAdapter delegate, ) throws

R toJson(T value) throws

前面分析了我们需要借助 Image 或 Text 对应的 Adapter,所以使用第二组函数签名:


class UserKeynoteAdapter {
private val namesOption = JsonReader.Options.of("type")

@FromJson
fun fromJson(
reader:
JsonReader,
imageJsonAdapter:
JsonAdapter<Image>,
textJsonAdapter:
JsonAdapter<Text>
)
: UserKeynote {
// copy 一份 reader,得到 type
val newReader = reader.peekJson()
newReader.beginObject()
var type: String? = null
while (newReader.hasNext()) {
if (newReader.selectName(namesOption) == 0) {
type = newReader.nextString()
}
newReader.skipName()
newReader.skipValue()
}
newReader.endObject()

// 根据 type 做解析
val resource = when (type) {
ResourceType.Image.name -> {
imageJsonAdapter.fromJson(reader)
}

ResourceType.Text.name -> {
textJsonAdapter.fromJson(reader)
}

else -> throw IllegalArgumentException("unknown type $type")
}
return UserKeynote(ResourceType.valueOf(type), resource)
}

@ToJson
fun toJson(
writer:
JsonWriter,
userKeynote:
UserKeynote,
imageJsonAdapter:
JsonAdapter<Image>,
textJsonAdapter:
JsonAdapter<Text>
)
{
when (userKeynote.resource) {
is Image -> imageJsonAdapter.toJson(writer, userKeynote.resource)
is Text -> textJsonAdapter.toJson(writer, userKeynote.resource)
null -> {}
}
}
}

函数接收一个 JsonReader / JsonWriter 以及若干 JsonAdapter,可以认为该 Adapter 由其他多个 Adapter 组合完成。这种委托的思路在 Moshi 中很常见,比如内置类型 List 的解析,便是委托给了 T 的适配器,并重复调用。


限制



  • 不要 Kotlin 类继承 Java 类

  • 不要 Java 类继承 Kotlin 类


这是官方强调不要做的,如果你那么做了,发现还没问题,不要侥幸,建议修改,毕竟有有维护风险,且会误导其他维护的人以为这样是可靠合理的。


作者:Aaron_Wang
来源:juejin.cn/post/7273516671575113743
收起阅读 »

面试官:如何防止重复提交订单?

这个问题,在电商领域的面试场景题下,应该算是妥妥的高频问题了,仅次于所谓的“秒杀场景如何实现”。 说个题外话,有段时间“秒杀场景如何实现”这个问题风靡一时,甚至在面试的时候,有些做财务领域、OA领域公司的面试官也都跟风问。 大有一种”无秒杀,不面试“的感觉了。...
继续阅读 »

这个问题,在电商领域的面试场景题下,应该算是妥妥的高频问题了,仅次于所谓的“秒杀场景如何实现”。


说个题外话,有段时间“秒杀场景如何实现”这个问题风靡一时,甚至在面试的时候,有些做财务领域、OA领域公司的面试官也都跟风问。


大有一种”无秒杀,不面试“的感觉了。


重复提交原因


其实原因无外乎两种:



  • 一种是由于用户在短时间内多次点击下单按钮,或浏览器刷新按钮导致。

  • 另一种则是由于Nginx或类似于SpringCloud Gateway的网关层,进行超时重试造成的。


常见解决方案


方案一:提交订单按钮置灰


这种解决方案在注册登录的场景下比较常见,当我们点击”发送验证码“按钮的时候,会进行手机短信验证码发送,且按钮就会有一分钟左右的置灰。


有些经验不太丰富的同学,通常会简单粗暴地把这个方案直接照搬过来。


但这种方案只能解决多次点击下单按钮的问题,对于Nginx或类似于SpringCloud Gateway的超时重试所导致的问题是无能为力的。


当然,这种方案也不是真的没有价值。它可以在高并发场景下,从浏览器端去拦住一部分请求,减少后端服务器的处理压力。


说到底,“下单防重”的问题是属于“接口幂等性”的问题范畴。



幂等性


接口幂等性是指:以相同的参数,对一个接口进行多次调用,所产生的结果和一次调用是完全相同的。


下面的情况就是幂等的:


student.setName("张三");

而这种情况就是非幂等的,因为每次调用,年龄都会增加一岁。


student.increaseAge(1);

现在我们的思路需要切换到幂等性的解决方案来。


同样是幂等性场景,“如何防止重复提交订单” 比 “如何防止订单重复支付” 的解决方案要难一些。


因为,后者在常规情况下,一个订单都是对应一笔支付单,所以orderID可以作为一个幂等性校验、防止订单重复支付的天然神器。


但这个方案在“如何防止重复提交订单”就不适用了,需要其他的解决方案,请继续看下文。


方案二:预生成全局唯一订单号


(1)后端新增一个接口,用于预生成一个“全局唯一订单号”,如:UUID 或 NanoID。


(2)进入创建订单页面时,前端请求该接口,获取该订单号。


(3)在提交订单时,请求参数里要带上这个预生成的“全局唯一订单号”,利用数据库的唯一索引特性,在插入订单记录时,如果该“全局唯一的订单号”重复,记录会插入失败。


btw:该“全局唯一订单号”不能代替数据库主键,在未分库分表场景下,主键还是用数据库自增ID比较好。



方案二


优点:彻底解决了重复下单的问题;


缺点:方案复杂,前后端都有开发工作量,还要新增接口,新增字段。


另外,网上还有同学说,要单独弄一个生成“全局唯一订单号”的服务,我觉得还是免了吧,这不是更麻烦了吗?


方案三:前端生成全局唯一订单号


这种方案是在借鉴了“方案二”的基础上,做了一些实现逻辑的简化。


(1)用户进入下页面时,前端程序自己生成一个“全局唯一订单号”。


(2)在提交订单时,请求参数里要带上这个预生成的“全局唯一订单号”,利用数据库的唯一索引特性,在插入订单记录时,如果该“全局唯一的订单号”重复,记录会插入失败。



方案三


优点:彻底解决了重复下单的问题,且技术方案做了一定简化;


缺点:前后端仍然都有开发工作量,且需要新增字段;


方案四:从订单业务的本质入手


先跟大家探讨一个概念,什么是订单?


其实,订单就是某个用户用特定的价格购买了某种商品,即:用户和商品的连接。


那么,“如何防止重复提交订单”,其实就是防止在短时间内,用户和商品进行多次连接。弄明白问题本质,接下来我们就着手制定技术方案了。


可以用 ”用户ID + 分隔符 + 商品ID“ 作为唯一标识,让持有相同标识的请求在短时间内不能重复下单,不就可以了吗?而且,Redis不正是做这种解决方案的利器吗?


Redis命令如下:


SET key value NX EX seconds


把”用户ID + 分隔符 + 商品ID“作为Redis key,并把”短时间所对应的秒数“设置为seconds,让它过期自动删除。


这样一来,整体业务步骤如下:


(1)在提交订单时,我们可以把”用户ID + 分隔符 + 商品ID“作为Redis key,并设置过期时间,让它可以到期自动删除。


(2)若Redis命令执行成功,则可以继续走下单的业务逻辑,执行不成功,直接返回给前端”下单失败“就可以了。



方案四


从上图来看,是不是实现方式越来越简单了?


优点:彻底解决了重复下单的问题,且在技术方案上,不需要前端参与,不需要添加接口,不需要添加字段;


缺点:综合比较而言,暂无明显缺点,如果硬要找缺点的话,可能强依赖于Redis勉强可以算上吧;


结语


在真正的生产环境下,我们最终选择了”方案四:从订单业务的本质入手“。


原因很简单,整体改动范围比较小,测试的回归范围也比较可控,且技术方案复杂度最低。


这样做技术选型的话,也比较符合百度一直倡导的”简单可依赖“原则。


作者:库森学长
来源:juejin.cn/post/7273024681631776829
收起阅读 »

聊聊Java中浮点丢失精度的事

在说这个之前,我们先看看十进制到二进制的转换过程 整数的十进制到二进制的转换过程 用白话说这个过程就是不断的除2,得到商继续除,直到商小于1为止,然后他每次结果的余数倒着排列出来就是它的二进制结果了,直接上图 说一下为什么倒着排列就是二进制结果哈 通俗点说就...
继续阅读 »

在说这个之前,我们先看看十进制到二进制的转换过程


整数的十进制到二进制的转换过程


用白话说这个过程就是不断的除2,得到商继续除,直到商小于1为止,然后他每次结果的余数倒着排列出来就是它的二进制结果了,直接上图


整数十进制转二进制.jpg
说一下为什么倒着排列就是二进制结果哈


通俗点说就是整数是一步一步除下来的,那回去不得一步一步乘上去?也就是说从上到下就是二进制从低位到高位的过程。


小数十进制到二进制的转换过程


小数的十进制到二进制的转换其实和整数类似,只不过算的方式变成了乘法,也就是用小数不断的乘2,然后得到的结果的整数部分拿出来,接着剩下的小数部分继续乘2,直到小数部分为0为止,直接上图~


小数十进制转二进制过程(不循环).jpg
二进制结果中的二分之一是转换后的,其实就是2的-1次方,-2次方。。。


当然了,小数转二进制的过程中,很多情况下都是无尽的,接着上图


小数十进制转二进制过程(循环).jpg
所以可以看到这样的循环下去是得不到二进制的结果的,所以计算机就要进行取舍。也就是IEEE 754规范


IEEE 754规范


IEEE 754规定了四种标识浮点数值的方式,单精确度(32位),双精确度(64位),延伸单精确度(43比特以上,很少用)和延伸双精确度(79比特以上,通常80位)


最常用的还是单精确度和双精确度,也就是对标的float和double。但是IEEE 754规范并没有解决精确标识小数的问题,只是提供了一种用近似值标识小数的方式。而且精确度不同近似值也会不同。# 为什么会精度丢失?教你看懂 IEEE-754!


下面有个例子来看一下丢失精度的问题,如0.1+0.2
0.1的64位二进制:0.00011001100110011001100110011001100110011001100110011001
0.2的64位二进制:0.00110011001100110011001100110011001100110011001100110011
二者相加的结果为:0.30000000000000004


那么如何解决精度问题呢?


BigDecimal


BigDecimal使用java.math包提供的,在涉及到金钱相关的计算的时候都需要使用它,而且其中提供了大量的方法,比如加减乘除都是可以直接调用的。


先看这个问题,BigDecimal中的比较问题


先看下面这个例子


public class ReferenceDemo {

public static void main(String[] args) {

BigDecimal bigDecimal1 = new BigDecimal(1);
BigDecimal bigDecimal2 = new BigDecimal(1);
System.out.println(bigDecimal1.equals(bigDecimal2));

BigDecimal bigDecimal3 = new BigDecimal(1);
BigDecimal bigDecimal4 = new BigDecimal(1.0);
System.out.println(bigDecimal3.equals(bigDecimal4));

BigDecimal bigDecimal5 = new BigDecimal("1");
BigDecimal bigDecimal6 = new BigDecimal("1.0");

System.out.println(bigDecimal5.equals(bigDecimal6));
}


}

结果为:


image.png
其中第二个例子和第三个例子的不同是需要聊一聊的。为什么会出现这种呢?下面是BigDecimal中的equals的源码。


public boolean equals(Object x) {
if (!(x instanceof BigDecimal))
return false;
BigDecimal xDec = (BigDecimal) x;
if (x == this)
return true;
//关键在这一行,比较了scale
if (scale != xDec.scale)
return false;
long s = this.intCompact;
long xs = xDec.intCompact;
if (s != INFLATED) {
if (xs == INFLATED)
xs = compactValFor(xDec.intVal);
return xs == s;
} else if (xs != INFLATED)
return xs == compactValFor(this.intVal);

return this.inflated().equals(xDec.inflated());
}

由上面的注释可以看到BigDecimal中有一个很关键的属性,就是scale,标度。标度是什么?
首先看一下BigDecimal的结构


public class BigDecimal extends Number implements Comparable<BigDecimal> {
/**
* The unscaled value of this BigDecimal, as returned by {@link
* #unscaledValue}.
*
* @serial
* @see #unscaledValue
*/

private final BigInteger intVal;

/**
* The scale of this BigDecimal, as returned by {@link #scale}.
*
* @serial
* @see #scale
*/

private final int scale; // Note: this may have any value, so
// calculations must be done in longs

/**
* If the absolute value of the significand of this BigDecimal is
* less than or equal to {@code Long.MAX_VALUE}, the value can be
* compactly stored in this field and used in computations.
*/

private final transient long intCompact;
}

我截取了几个关键字段,依次看一下:


intVal: 无标度值


scale: 标度


intCompact: 当intVal超过阈值(默认为Long.MAX_VALUE)时,进行压缩运算,结果存到这个字段上,用于后续计算。


注释中解释到,scale为0或者正数的时候代表数字小数点之后的位数,如果scale为负数,代表数字的无标度值需要乘10的该负数的绝对值的幂,即末尾有几个0


比如123.123这个数,他的intVal就是123123,scale就是3了


而二进制无法标识0.1,通过BigDecimal标识的话,它的intVal就是1,scale也是1。


接着看回上面的例子,传入的参数是字符串的bigDecimal5和bigDecimal6,为什么就返回了false。上图


image.png


他们的标度是不同的,所以直接返回了false,那么在看bigDecimal3和bigDecimal4的比较,为什么就返回了true呢,同样上图


image.png
可以看到他们的intVal和scale都是相等的,但是明明传入了不同的,有兴趣的可以取看看源码,找一些资料,对于1.0这个数,它本质上也是一个整数,经过一系列的运算他的intVal还是1,scale还是0,所以比较之后返回的是true。


这时候就能看出来equals方法的一些问题了,用equals涉及到scale的比较,实际的结果可能和预期不一样,所在BigDecimal的比较推荐用compareTo方法,如果返回0,代表相等


BigDecimal bigDecimal5 = new BigDecimal("1");
BigDecimal bigDecimal6 = new BigDecimal("1.0");

System.out.println(bigDecimal5.compareTo(bigDecimal6));

说到这里同时提一下,不要用传参为double的构造方法,同样会丢失精度,如果需要小数,需要传入字符串的小数来获取BigDecimal的实例对象。


说到这其实应该明白了他是怎么保证精度的了,其实关键点就是scale,这个标度贯穿了整个过程,加减乘除的运算都需要它来把控。上面说了其实2个参数最为关键,一个是无标度值,一个是标度,无标度值就是整数了,以加法为例子,不就可以变成整数的加法了吗,然后用scale控制小数点,说是这么说,实现过程还是很复杂的,有兴趣的可以自己查资料去学习。


除了用字符串代替double来表示BigDecimal的小数,其实也可以通过BigDecimal.valueOf()方法,它传入double之后可以和字符串一样的效果,为啥呢?上代码


public static BigDecimal valueOf(double val) {
// Reminder: a zero double returns '0.0', so we cannot fastpath
// to use the constant ZERO. This might be important enough to
// justify a factory approach, a cache, or a few private
// constants, later.
return new BigDecimal(Double.toString(val));
}

它把传入的double给toString了。。。。


作者:yulbo
来源:juejin.cn/post/7274692953058082877
收起阅读 »

谷歌是如何写技术文档的

Google软件工程文化的一个关键要素是使用设计文档来定义软件设计。这些相对非正式的文件由软件系统或应用程序的主要作者在开始编码项目之前创建。设计文档记录了高层次的实现策略和关键设计决策,重点强调在这些决策过程中考虑到的权衡。 作为软件工程师,我们的任务不仅仅...
继续阅读 »

Google软件工程文化的一个关键要素是使用设计文档来定义软件设计。这些相对非正式的文件由软件系统或应用程序的主要作者在开始编码项目之前创建。设计文档记录了高层次的实现策略和关键设计决策,重点强调在这些决策过程中考虑到的权衡。


作为软件工程师,我们的任务不仅仅是生成代码,而更多地是解决问题。像设计文档这样的非结构化文本可能是项目生命周期早期解决问题更好的工具,因为它可能更简洁易懂,并且以比代码更高层次的方式传达问题和解决方案。


除了原始软件设计文件外,设计文档还在以下方面发挥着作用:


在进行变更时及早识别出设计问题仍然较便宜。


在组织内达成对某个设计方案的共识。


确保考虑到跨领域关注点。


将资深工程师们掌握知识扩展到整个组织中去。


形成围绕设计决策建立起来的组织记忆基础。


作为技术人员投资组合中一份摘要性产物存在于其中。


设计文档的构成


设计文档是非正式的文件,因此其内容没有严格的指导方针。第一条规则是:以对特定项目最有意义的形式编写。


话虽如此,事实证明,某种结构已经被证明非常有用。


上下文和范围


本节为读者提供了新系统构建的大致概述以及实际正在构建的内容。这不是一份需求文档。保持简洁!目标是让读者迅速了解情况,但可以假设有一些先前的知识,并且可以链接到详细信息。本节应完全专注于客观背景事实。


目标和非目标


列出系统目标的简短项目列表,有时更重要的是列出非目标。请注意,非目标并不是否定性的目标,比如“系统不应崩溃”,而是明确选择不作为目标而合理可能成为目标的事项。一个很好的例子就是“ACID兼容性”;在设计数据库时,您肯定想知道是否将其作为一个目标或非目标。如果它是一个非目标,则仍然可以选择提供该功能的解决方案,前提是它不会引入阻碍实现这些目标的权衡考虑。


实际设计


这一部分应该以概述开始,然后进入细节。


image.png


设计文档是记录你在软件设计中所做的权衡的地方。专注于这些权衡,以产生具有长期价值的有用文档。也就是说,在给定上下文(事实)、目标和非目标(需求)的情况下,设计文档是提出解决方案并展示为什么特定解决方案最能满足这些目标的地方。


撰写文件而不是使用更正式的媒介之一的原因在于提供灵活性,以适当方式表达手头问题集。因此,并没有明确指导如何描述设计。


话虽如此,已经出现了一些最佳实践和重复主题,在大多数设计文档中都很合理:


系统上下文图


在许多文档中,系统上下文图非常有用。这样的图表将系统显示为更大的技术环境的一部分,并允许读者根据他们已经熟悉的环境来理解新设计。


image.png
一个系统上下文图的示例。


APIs


如果设计的系统暴露出一个API,那么草拟出该API通常是个好主意。然而,在大多数情况下,应该抵制将正式接口或数据定义复制粘贴到文档中的诱惑,因为这些定义通常冗长、包含不必要的细节,并且很快就会过时。相反,重点关注与设计及其权衡相关的部分。


数据存储


存储数据的系统可能需要讨论如何以及以什么样的形式进行存储。与对API的建议类似,出于同样的原因,应避免完全复制粘贴模式定义。而是专注于与设计及其权衡相关的部分。


代码和伪代码


设计文档很少包含代码或伪代码,除非描述了新颖的算法。在适当的情况下,可以链接到展示设计可实现性的原型。


约束程度


影响软件设计和设计文档形状的主要因素之一是解决方案空间的约束程度。


在极端情况下,有一个“全新软件项目”,我们只知道目标,解决方案可以是任何最合理的选择。这样的文档可能涉及范围广泛,但也需要快速定义一组规则,以便缩小到可管理的解决方案集。


另一种情况是系统中可能存在非常明确定义的解决方案,但如何将它们结合起来实现目标并不明显。这可能是一个难以更改且未设计为满足您期望功能需求的遗留系统,或者是需要在主机编程语言约束下运行的库设计。


在这种情况下,您可能能够相对容易地列举出所有可以做到的事情,但需要创造性地将这些事物组合起来实现目标。可能会有多个解决方案,并且没有一个真正很好,在识别了所有权衡后该文档应专注于选择最佳方式。


考虑的替代方案


本节列出了其他可能达到类似结果的设计方案。重点应放在每个设计方案所做的权衡以及这些权衡如何导致选择文档主题中所述设计的决策上。


尽管对于最终未被选中的解决方案可以简洁地进行描述,但是这一部分非常重要,因为它明确展示了根据项目目标而选择该解决方案为最佳选项,并且还说明了其他解决方案引入了不太理想的权衡,读者可能会对此产生疑问。


交叉关注点


这是您的组织可以确保始终考虑到安全、隐私和可观察性等特定的交叉关注点的地方。这些通常是相对简短的部分,解释设计如何影响相关问题以及如何解决这些问题。团队应该在他们自己的情况下标准化这些关注点。


由于其重要性,Google项目需要有专门的隐私设计文档,并且还有专门针对隐私和安全进行Review。尽管Review只要求在项目启动之前完成,但最佳实践是尽早与隐私和安全团队合作,以确保从一开始就将其纳入设计中。如果针对这些主题有专门文档,则中央设计文档当然可以引用它们而不详述。


设计文档的长度


设计文档应该足够详细,但又要短到忙碌的人实际上能读完。对于较大的项目来说,最佳页数似乎在10-20页左右。如果超过这个范围,可能需要将问题拆分成更易管理的子问题。还应注意到,完全可以编写一个1-3页的“迷你设计文档”。这对于敏捷项目中的增量改进或子任务尤其有帮助 - 你仍然按照长篇文档一样进行所有步骤,只是保持简洁,并专注于有限的问题集合。


何时不需要编写设计文档


编写设计文档是一种额外的工作量。是否要编写设计文档的决策取决于核心权衡,即组织共识在设计、文档、高级Review等方面的好处是否超过了创建文档所需的额外工作量。这个决策的核心在于解决设计问题是否模糊——因为问题复杂性或解决方案复杂性,或者两者都有。如果不模糊,则通过撰写文档来进行流程可能没有太大价值。


一个明确的指标表明可能不需要文档是那些实际上只是实施手册而非设计文档。如果一个文件基本上说“这就是我们将如何实现它”,而没有涉及权衡、替代方案和解释决策(或者解决方案显然意味着没有任何权衡),那么直接编写程序可能会更好。


最后,创建和Review设计文档所需的开销可能与原型制作和快速迭代不兼容。然而,大多数软件项目确实存在一系列已知问题。遵循敏捷方法论并不能成为对真正已知问题找到正确解决方案时间投入不足的借口。此外,原型制作本身可以是设计文档创建的一部分。“我尝试过,它有效”是选择一个设计方案的最佳论据之一。


设计文档的生命周期


设计文档的生命周期包括以下步骤:


创建和快速迭代
Review(可能需要多轮)
实施和迭代
维护和学习


创作和快速迭代


你撰写文档。有时与一组合著者共同完成。


这个阶段很快进入了一个快速迭代的时间,文档会与那些对问题领域最了解的同事(通常是同一个团队的人)分享,并通过他们提出的澄清问题和建议来推动文档达到第一个相对稳定版本。


虽然你肯定会找到一些工程师甚至团队更喜欢使用版本控制和代码Review工具来创建文档,但在谷歌,绝大多数设计文档都是在Google Docs中创建并广泛使用其协作功能。


Review


在Review阶段,设计文档会与比原始作者和紧密合作者更广泛的受众分享。Review可以增加很多价值,但也是一个危险的开销陷阱,所以要明智地对待。


Review可以采取多种形式:较轻量级的版本是将文档发送给(更广泛的)团队列表,让大家有机会看一下。然后主要通过文档中的评论线程进行讨论。在Review方面较重型的方式是正式的设计评审会议,在这些会议上作者通常通过专门制作的演示文稿向经验丰富、资深工程师们展示该文档内容。谷歌公司许多团队都定期安排了此类会议,并邀请工程师参加审核。自然而然地等待这样的会议可能会显著减慢开发过程。工程师可以通过直接寻求最关键反馈并不阻碍整体审核进度来缓解这个问题。


当谷歌还是一家较小的公司时,通常会将设计发送到一个中央邮件列表,高级工程师会在自己的闲暇时间进行Review。这可能是处理公司事务的好方法。其中一个好处是确立了相对统一的软件设计文化。但随着公司规模扩大到更庞大的工程团队,维持集中式方法变得不可行。


此类Review所添加的主要价值在于它们为组织的集体经验提供了融入设计的机会。最重要的是,在Review阶段可以确保考虑到横切关注点(如可观察性、安全性和隐私)等方面。Review的主要价值并非问题被发现本身,而是这些问题相对早期地在开发生命周期内被发现,并且修改仍然相对廉价。


实施和迭代


当事情进展到足够程度,有信心进一步Review不需要对设计进行重大更改时,就是开始实施的时候了。随着计划与现实的碰撞,不可避免地会出现缺陷、未解决的需求或者被证明错误的猜测,并且需要修改设计。强烈建议在这种情况下更新设计文档。作为一个经验法则:如果设计系统尚未发布,则绝对要更新文档。在实践中,我们人类很擅长忘记更新文件,并且由于其他实际原因,变更通常被隔离到新文件中。这最终导致了一个类似于美国宪法带有一堆修正案而不是一份连贯文档的状态。从原始文档链接到这些修正案可以极大地帮助那些试图通过设计文档考古学来理解目标系统的可怜未来维护程序员们。


维护和学习


当谷歌工程师面对一个之前没有接触过的系统时,他们经常会问:“设计文档在哪里?”虽然设计文档(像所有文档一样)随着时间推移往往与现实脱节,但它们通常是了解系统创建背后思考方式最容易入手的途径。


作为作者,请你给自己一个方便,并在一两年后重新阅读你自己的设计文档。你做得对了什么?你做错了什么?如果今天要做出不同决策,你会怎么选择?回答这些问题是作为一名工程师进步并改善软件设计技能的好方法。


结论


设计文档是在软件项目中解决最困难问题时获得清晰度和达成共识的好方法。它们可以节省金钱,因为避免了陷入编码死胡同而无法实现项目目标,并且可以通过前期调查来避免这种情况;但同时也需要花费时间和金钱进行创建和Review。所以,在选择项目时要明智!


在考虑撰写设计文档时,请思考以下几点:


您是否对正确的软件设计感到不确定,是否值得花费前期时间来获得确定性?


相关地,是否有必要让资深工程师参与其中,即使他们可能无法Review每个代码更改,在设计方面能提供帮助吗?


软件设计是否模糊甚至具有争议性,以至于在组织中达成共识将是有价值的?


我的团队是否有时会忘记在设计中考虑隐私、安全、日志记录或其他横切关注点?


组织中对遗留系统的高层次洞察力提供文档存在强烈需求吗?


如果您对以上3个或更多问题回答“是”,那么撰写一个设计文档很可能是开始下一个软件项目的好方法。


Reference


http://www.industrialempathy.com/posts/desig…


作者:dooocs
来源:juejin.cn/post/7272730352710418447
收起阅读 »

浅谈多人游戏原理和简单实现

一、我的游戏史 我最开始接触游戏要从一盘300游戏的光碟说起,那是家里买DVD送的,《魂斗罗》、《超级马里奥》天天玩。自从买回来后,我就经常和姐姐因为抢电视机使用权而大打出手。有次她把遥控器藏到了沙发的夹层里,被我妈一屁股做成了两半,我和我姐喜提一顿暴打。那顿...
继续阅读 »



一、我的游戏史


我最开始接触游戏要从一盘300游戏的光碟说起,那是家里买DVD送的,《魂斗罗》、《超级马里奥》天天玩。自从买回来后,我就经常和姐姐因为抢电视机使用权而大打出手。有次她把遥控器藏到了沙发的夹层里,被我妈一屁股做成了两半,我和我姐喜提一顿暴打。那顿是我挨得最狠的,以至于现在回想起来,屁股还条件反射的隐隐作痛。


后来我骗我妈说我要学习英语、练习打字以后成为祖国的栋梁之才!让她给我买台小霸王学习机(游戏机),在我一哭二闹三上吊胡搅蛮缠的攻势下,我妈妥协了。就此我接触到了FC游戏。现在还能记得我和朋友玩激龟快打,满屋子的小朋友在看的场景。经常有家长在我家门口喊他家小孩吃饭。那时候我们县城里面有商店卖游戏卡,小卡一张5块钱,一张传奇卡25-40块不等(所谓传奇卡就是角色扮演,带有存档的游戏),每天放学都要去商店去看看,有没有新的游戏卡,买不起,就看下封面过过瘾。我记得我省吃俭用一个多月,买了两张卡:《哪吒传奇》和《重装机兵》那是真的上瘾,没日没夜的玩。


再然后我接触到了手机游戏,记得那时候有个软件叫做冒泡游戏(我心目的中的Stream),里面好多游戏,太吸引我了。一个游戏一般都是几百KB,最大也就是几MB,不过那时候流量很贵,1块钱1MB,并且!一个月只有30Mb。我姑父是收手机的,我在他那里搞到了一部半智能手机,牌子我现在还记得:诺基亚N70,那时候我打开游戏就会显示一杯冒着热气的咖啡,我很喜欢这个图标,因为看见它意味着我的游戏快加载完成了,没想到,十几年后我们会再次相遇,哈哈哈哈。我当时玩了一款网游叫做:《幻想三国》,第一回接触网游简直惊呆了,里面好多人都是其他玩的家,这太有趣了。并且我能在我的手机上看到其他玩家,能够看到他们的行为动作,这太神奇了!!!我也一直思考这到底是怎么实现的!


最后是电脑游戏,单机:《侠盗飞车》、《植物大战僵尸》、《虐杀原型》;网游:《DNF》、《CF》、《LOL》、《梦幻西游》我都玩过。


不过那个疑问一直没有解决,也一值留在我心中 —— 在网络游戏中,是如何实时更新其他玩家行为的呢?


二、解惑


在我进入大学后,我选择了软件开发专业,真巧!再次遇到了那个冒着热气的咖啡图标,这时我才知道它叫做——Java。我很认真的去学,希望有一天能够做一款游戏!


参加工作后,我并没有如愿以偿,我成为了一名Java开发程序员,但是我在日常的开发的都是web应用,接触到大多是HTTP请求,它是种请求-响应协议模式。这个问题也还是想不明白,难道每当其他玩家做一个动作都需要发送一次HTTP请求?然后响应给其他玩家。这样未免效率也太低了吧,如果一个服务器中有几千几万人,那么服务器得承受多大压力呀!一定不是这样的!!!


直到我遇到了Websocket,这是一种长连接,而HTTP是一种短连接,顿时这个问题我就想明白了。在此二者的区别我就不过多赘述了。详细请看我的另一篇文章


知道了这个知识后,我终于能够大致明白了网络游戏的基本原理。原来网络游戏是由客户端服务器端组成的,客户端就是我们下载到电脑或者手机上的应用,而服务器端就是把其他玩家连接起来的中转站,还有一点需要说明的是,网络游戏是分房间的,这个房间就相当于一台服务器。首先,在玩家登陆客户端并选择房间建立长连接后,A玩家做出移动的动作,随即会把这个动作指令上传给服务器,然后服务器再将指令广播到房间中的其他玩家的客户端来操作A的角色,这样就可以实现实时更新其他玩家行为。




三、简单实现


客户端服务端在处理指令时,方法必须是配套的。比如说,有新的玩家连接到服务器,那么服务器就应当向其它客户端广播创建一个新角色的指令,客户端在接收到该指令后,执行客户端创建角色的方法。
为了方便演示,这里需要定义两个HTML来表示两个不同的客户端不同的玩家,这两套客户端代码除了玩家的信息不一样,其它完全一致!!!


3.1 客户端实现步骤


我在这里客户端使用HTML+JQ实现


客户端——1代码:


(1)创建画布

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas Game</title>
<style>
canvas {
border: 1px solid black;
}
</style>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<canvas id="gameCanvas" width="800" height="800"></canvas>
</body>
</html>

(2)设置1s60帧更新页面

const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function gameLoop() {
clearCanvas();
players.forEach(player => {
player.draw();
});
}
setInterval(gameLoop, 1000 / 60);
//清除画布方法
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}

(3)连接游戏服务器并处理指令


这里使用websocket链接游戏服务器

 //连接服务器
const websocket = new WebSocket("ws://192.168.31.136:7070/websocket?userId=" + userId + "&userName=" + userName);
//向服务器发送消息
function sendMessage(userId,keyCode){
const messageData = {
playerId: userId,
keyCode: keyCode
};
websocket.send(JSON.stringify(messageData));
}
//接收服务器消息,并根据不同的指令,做出不同的动作
websocket.onmessage = event => {
const data = JSON.parse(event.data);
// 处理服务器发送过来的消息
console.log('Received message:', data);
//创建游戏对象
if(data.type == 1){
console.log("玩家信息:" + data.players.length)
for (let i = 0; i < data.players.length; i++) {
console.log("玩家id:"+playerOfIds);
createPlayer(data.players[i].playerId,data.players[i].pointX, data.players[i].pointY, data.players[i].color);
}
}
//销毁游戏对象
if(data.type == 2){
console.log("玩家信息:" + data.players.length)
for (let i = 0; i < data.players.length; i++) {
destroyPlayer(data.players[i].playerId)
}
}
//移动游戏对象
if(data.type == 3){
console.log("移动;玩家信息:" + data.players.length)
for (let i = 0; i < data.players.length; i++) {
players.filter(player => player.id === data.players[i].playerId)[0].move(data.players[i].keyCode)
}
}
};

(4)创建玩家对象

//存放游戏对象
let players = [];
//playerId在此写死,正常情况下应该是用户登录获取的
const userId = "1"; // 用户的 id
const userName = "逆风笑"; // 用户的名称
//玩家对象
class Player {
constructor(id,x, y, color) {
this.id = id;
this.x = x;
this.y = y;
this.size = 30;
this.color = color;
}
//绘制游戏角色方法
draw() {
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, this.size, this.size);
}
//游戏角色移动方法
move(keyCode) {
switch (keyCode) {
case 37: // Left
this.x = Math.max(0, this.x - 10);
break;
case 38: // Up
this.y = Math.max(0, this.y - 10);
break;
case 39: // Right
this.x = Math.min(canvas.width - this.size, this.x + 10);
break;
case 40: // Down
this.y = Math.min(canvas.height - this.size, this.y + 10);
break;
}
this.draw();
}
}

(5)客户端创建角色方法

//创建游戏对象方法
function createPlayer(id,x, y, color) {
const player = new Player(id,x, y, color);
players.push(player);
playerOfIds.push(id);
return player;
}

(6)客户端销毁角色方法


在玩家推出客户端后,其它玩家的客户端应当销毁对应的角色。

//角色销毁
function destroyPlayer(playId){
players = players.filter(player => player.id !== playId);
}

客户端——2代码:


客户端2的代码只有玩家信息不一致:

  const userId = "2"; // 用户的 id
const userName = "逆风哭"; // 用户的名称

3.2 服务器端


服务器端使用Java+websocket来实现!


(1)引入依赖:

 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.3.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.3</version>
</dependency>

(2)创建服务器

@Component
@ServerEndpoint("/websocket")
@Slf4j
public class Server {
/**
* 服务器玩家池
* 解释:这里使用 ConcurrentHashMap为了保证线程安全,不会出现同一个玩家存在多条记录问题
* 使用 static fina修饰 是为了保证 playerPool 全局唯一
*/
private static final ConcurrentHashMap<String, Server> playerPool = new ConcurrentHashMap<>();
/**
* 存储玩家信息
*/
private static final ConcurrentHashMap<String, Player> playerInfo = new ConcurrentHashMap<>();
/**
* 已经被创建了的玩家id
*/
private static ConcurrentHashMap<String, Server> createdPlayer = new ConcurrentHashMap<>();

private Session session;

private Player player;

/**
* 连接成功后调用的方法
*/
@OnOpen
public void webSocketOpen(Session session) throws IOException {
Map<String, List<String>> requestParameterMap = session.getRequestParameterMap();
String userId = requestParameterMap.get("userId").get(0);
String userName = requestParameterMap.get("userName").get(0);
this.session = session;
if (!playerPool.containsKey(userId)) {
int locationX = getLocation(151);
int locationY = getLocation(151);
String color = PlayerColorEnum.getValueByCode(getLocation(1) + 1);
Player newPlayer = new Player(userId, userName, locationX, locationY,color,null);
playerPool.put(userId, this);
this.player = newPlayer;
//存放玩家信息
playerInfo.put(userId,newPlayer);
}
log.info("玩家:{}|{}连接了服务器", userId, userName);
// 创建游戏对象
this.createPlayer(userId);
}

/**
* 接收到消息调用的方法
*/
@OnMessage
public void onMessage(String message, Session session) throws IOException, InterruptedException {
log.info("用户:{},消息{}:",this.player.getPlayerId(),message);
PlayerDTO playerDTO = new PlayerDTO();
Player player = JSONObject.parseObject(message, Player.class);
List<Player> players = new ArrayList<>();
players.add(player);
playerDTO.setPlayers(players);
playerDTO.setType(OperationType.MOVE_OBJECT.getCode());
String returnMessage = JSONObject.toJSONString(playerDTO);
//广播所有玩家
for (String key : playerPool.keySet()) {
synchronized (session){
String playerId = playerPool.get(key).player.getPlayerId();
if(!playerId.equals(this.player.getPlayerId())){
playerPool.get(key).session.getBasicRemote().sendText(returnMessage);
}
}
}
}

/**
* 关闭连接调用方法
*/
@OnClose
public void onClose() throws IOException {
String playerId = this.player.getPlayerId();
log.info("玩家{}退出!", playerId);
Player playerBaseInfo = playerInfo.get(playerId);
//移除玩家
for (String key : playerPool.keySet()) {
playerPool.remove(playerId);
playerInfo.remove(playerId);
createdPlayer.remove(playerId);
}
//通知客户端销毁对象
destroyPlayer(playerBaseInfo);
}

/**
* 出现错误时调用的方法
*/
@OnError
public void onError(Throwable error) {
log.info("服务器错误,玩家id:{},原因:{}",this.player.getPlayerId(),error.getMessage());
}
/**
* 获取随即位置
* @param seed
* @return
*/
private int getLocation(Integer seed){
Random random = new Random();
return random.nextInt(seed);
}
}

websocket配置:

@Configuration
public class ServerConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}


(3)创建玩家对象


玩家对象:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Player {
/**
* 玩家id
*/
private String playerId;
/**
* 玩家名称
*/
private String playerName;
/**
* 玩家生成的x坐标
*/
private Integer pointX;
/**
* 玩家生成的y坐标
*/
private Integer pointY;
/**
* 玩家生成颜色
*/
private String color;
/**
* 玩家动作指令
*/
private Integer keyCode;
}

创建玩家对象返回给客户端DTO:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PlayerDTO {
private Integer type;
private List<Player> players;
}

玩家移动指令返回给客户端DTO:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PlayerMoveDTO {
private Integer type;
private List<Player> players;
}


(4)动作指令

public enum OperationType {
CREATE_OBJECT(1,"创建游戏对象"),
DESTROY_OBJECT(2,"销毁游戏对象"),
MOVE_OBJECT(3,"移动游戏对象"),
;
private Integer code;
private String value;

OperationType(Integer code, String value) {
this.code = code;
this.value = value;
}

public Integer getCode() {
return code;
}

public String getValue() {
return value;
}
}

(5)创建对象方法

  /**
* 创建对象方法
* @param playerId
* @throws IOException
*/
private void createPlayer(String playerId) throws IOException {
if (!createdPlayer.containsKey(playerId)) {
List<Player> players = new ArrayList<>();
for (String key : playerInfo.keySet()) {
Player playerBaseInfo = playerInfo.get(key);
players.add(playerBaseInfo);
}
PlayerDTO playerDTO = new PlayerDTO();
playerDTO.setType(OperationType.CREATE_OBJECT.getCode());
playerDTO.setPlayers(players);
String syncInfo = JSONObject.toJSONString(playerDTO);
for (String key :
playerPool.keySet()) {
playerPool.get(key).session.getBasicRemote().sendText(syncInfo);
}
// 存放
createdPlayer.put(playerId, this);
}
}

(6)销毁对象方法

   /**
* 销毁对象方法
* @param playerBaseInfo
* @throws IOException
*/
private void destroyPlayer(Player playerBaseInfo) throws IOException {
PlayerDTO playerDTO = new PlayerDTO();
playerDTO.setType(OperationType.DESTROY_OBJECT.getCode());
List<Player> players = new ArrayList<>();
players.add(playerBaseInfo);
playerDTO.setPlayers(players);
String syncInfo = JSONObject.toJSONString(playerDTO);
for (String key :
playerPool.keySet()) {
playerPool.get(key).session.getBasicRemote().sendText(syncInfo);
}
}

四、演示


4.1 客户端1登陆服务器




4.2 客户端2登陆服务器




4.3 客户端2移动




4.4 客户端1移动




4.5 客户端1退出



 完结撒花


完整代码传送门


五、总结


以上就是我对网络游戏如何实现玩家实时同步的理解与实现,我实现后心里也释然了,哈哈哈,真的好有趣!!!
我希望大家也是,不要失去好奇心,遇到自己感兴趣的事情,一定要多思考呀~


后来随着我经验的不断积累,我又去了解了一下Java作为游戏服务器的相关内容,发现Netty更适合做这个并且更容易入门,比如《我的世界》一些现有的服务器就是使用Netty实现的。有空也实现下,玩玩~


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

往往排查很久的问题,最后发现都非常简单。。。

之前线上发生了一个很诡异的异常,网上各种搜索、排查,都没有找到问题,给大家分享一下。 大概在 2 月份的时候,我们的某个应用整合了中间件的 kafka 客户端,发布到灰度和蓝节点进行观察,然后就发现线上某个 Topic 发生了大量的RetriableCommi...
继续阅读 »

之前线上发生了一个很诡异的异常,网上各种搜索、排查,都没有找到问题,给大家分享一下。


大概在 2 月份的时候,我们的某个应用整合了中间件的 kafka 客户端,发布到灰度和蓝节点进行观察,然后就发现线上某个 Topic 发生了大量的RetriableCommitException,并且集中在灰度机器上。

E20:21:59.770 RuntimeException  org.apache.kafka.clients.consumer.RetriableCommitFailedException  ERROR [Consumer clientId=xx-xx.4-0, groupId=xx-xx-consumer_[gray]] Offset commit with offsets {xx-xx-xx-callback-1=OffsetAndMetadata{offset=181894918, leaderEpoch=4, metadata=''}, xx-xx-xx-callback-0=OffsetAndMetadata{offset=181909228, leaderEpoch=5, metadata=''}} failed org.apache.kafka.clients.consumer.RetriableCommitFailedException: Offset commit failed with a retriable exception. You should retry committing the latest consumed offsets.
Caused by: org.apache.kafka.common.errors.TimeoutException: Failed to send request after 30000 ms.


排查


检查了这个 Topic 的流量流入、流出情况,发现并不是很高,至少和 QA 环境的压测流量对比,连零头都没有达到。


但是从发生异常的这个 Topic 的历史流量来看的话,发生问题的那几个时间点的流量又确实比平时高出了很多。



同时我们检查 Broker 集群的负载情况,发现那几个时间点的 CPU 负载也比平时也高出很多(也只是比平时高,整体并不算高)。



对Broker集群的日志排查,也没发现什么特殊的地方。


然后我们对这个应用在QA上进行了模拟,尝试复现,遗憾的是,尽管我们在QA上把生产流量放大到很多倍并尝试了多次,问题还是没能出现。


此时,我们把问题归于当时的网络环境,这个结论在当时其实是站不住脚的,如果那个时刻网络环境发生了抖动的话,其它应用为什么没有这类异常?


可能其它的服务实例网络情况是好的,只是发生问题的这个灰实例网络发生了问题。


那问题又来了,为什么这个实例的其它 Topic 没有报出异常,偏偏问题只出现在这个 Topic 呢?。。。。。。。。。


至此,陷入了僵局,无从下手的感觉。


从这个客户端的开发、测试到压测,如果有 bug 的话,不可能躲过前面那么多环节,偏偏爆发在了生产环境。


没办法了,我们再次进行了一次灰度发布,如果过了一夜没有事情发生,我们就把问题划分到环境问题,如果再次出现问题的话,那就只能把问题划分到我们实现的 Kafka 客户端的问题了。


果不其然,发布后的第二天凌晨1点多,又出现了大量的 RetriableCommitFailedException,只是这次换了个 Topic,并且异常的原因又多出了其它Caused by 。

org.apache.kafka.clients.consumer.RetriableCommitFailedException: Offset commit failed with a retriable exception. You should retry committing the latest consumed offsets.
Caused by: org.apache.kafka.common.errors.DisconnectException
...
...
E16:23:31.640 RuntimeException  org.apache.kafka.clients.consumer.RetriableCommitFailedException  ERROR 
...
...
org.apache.kafka.clients.consumer.RetriableCommitFailedException: Offset commit failed with a retriable exception. You should retry committing the latest consumed offsets.
Caused by: org.apache.kafka.common.errors.TimeoutException: The request timed out.

分析


这次出现的异常与之前异常的不同之处在于:

  1. 1. Topic 变了
  2. 2. 异常Cause变了

而与之前异常又有相同之处:

  1. 1. 只发生在灰度消费者组
  2. 2. 都是RetriableCommitFailedException

RetriableCommitFailedException 意思很明确了,可以重试提交的异常,网上搜了一圈后仅发现StackOverFlow上有一问题描述和我们的现象相似度很高,遗憾的是没人回复这个问题:StackOverFlow。


我们看下 RetriableCommitFailedException 这个异常和产生这个异常的调用层级关系。



除了产生异常的具体 Cause 不同,剩下的都是让我们再 retry,You should retry Commiting the lastest consumed offsets。



从调用层级上来看,我们可以得到几个关键的信息,commit 、 async。


再结合异常发生的实例,我们可以得到有用关键信息: 灰度、commit 、async。


在灰度消息的实现上,我们确实存在着管理位移和手动提交的实现。



看代码的第 62 行,如果当前批次消息经过 filter 的过滤后一条消息都不符合当前实例消费,那么我们就把当前批次进行手动异步提交位移。结合我们在生产的实际情况,在灰度实例上我们确实会把所有的消息都过滤掉,并异步提交位移。


为什么我们封装的客户端提交就会报大量的报错,而使用 spring-kafka 的没有呢?


我们看下Spring对提交位移这块的核心实现逻辑。



可以同步,也可以异步提交,具体那种提交方式就要看 this.containerProperties.isSyncCommits() 这个属性的配置了,然而我们一般也不会去配置这个东西,大部分都是在使用默认配置。



人家默认使用的是同步提交方式,而我们使用的是异步方式。


同步提交和异步提交有什么区别么?


先看下同步提交的实现:



只要遇到了不是不可恢复的异常外,在 timer 参数过期时间范围内重试到成功(这个方法的描述感觉不是很严谨的样子)。



我们在看下异步提交方式的核心实现:



我们不要被第 645 行的 RequestFuture future = sendOffsetCommitRequest(offsets) 所迷惑,它其实并不是发送位移提交的请求,它内部只是把当前请求包装好,放到 private final UnsentRequests unsent = new UnsentRequests(); 这个属性中,同时唤醒真正的发送线程来发送的。



这里不是重点,重点是如果我们的异步提交发生了异常,它只是简单的使用 RetriableCommitFailedException 给我们包装了一层。


重试呢?为什么异步发送产生了可重试异常它不给我们自动重试?


如果我们对多个异步提交进行重试的话,很大可能会导致位移覆盖,从而引发重复消费的问题。


正好,我们遇到的所有异常都是 RetriableCommitException 类型的,也就是说,我们把灰度位移提交的方式修改成同步可重试的提交方式,就可以解决我们遇到的问题了。


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

刚咬了一口馒头,服务器突然炸了!

首先,这个项目是完全使用Websocket开发,后端采用了基于Swoole开发的Hyperf框架,集成了Webscoket服务。 其次,这个项目是我们组第一次尝试使用Socket替换传统的HTTP API请求,前端使用了Vue3,前端同学定义了一套Socket...
继续阅读 »

首先,这个项目是完全使用Websocket开发,后端采用了基于Swoole开发的Hyperf框架,集成了Webscoket服务。
其次,这个项目是我们组第一次尝试使用Socket替换传统的HTTP API请求,前端使用了Vue3,前端同学定义了一套Socket管理器。


看着群里一群“小可爱”疯狂乱叫,我被吵的头都炸了,赶紧尝试定位问题。



  1. 查看是否存在Jenkins发版 -> 无

  2. 查看最新提交记录 -> 最后一次提交是下午,到晚上这段时间系统一直是稳定的

  3. 查看服务器资源,htop后,发现三台相关的云服务器资源都出现闲置状态

  4. 查看PolarDB后,既MySQL,连接池正常、吞吐量和锁正常

  5. 查看Redis,资源正常,无异常key

  6. 查看前端控制台,出现一些报错,但是这些报错经常会变化


  7. 查看前端测试环境、后端测试环境,程序全部正常

  8. 重启前端服务、后端服务、NGINX服务,好像没用,过了5分钟后,咦,好像可以访问了


就在我们组里的“小可爱”通知系统恢复正常后,20分钟不到,再一次处于无法打开的状态,沃焯!!!
完蛋了,找不出问题在哪里了,实在想不通问题究竟出在哪里。


我不服啊,我不理解啊!


咦,Nginx?对呀,我瞅瞅访问日志,于是我浏览了一下access.log,看起来貌似没什么问题,不过存在大量不同浏览器的访问记录,刷的非常快。


再瞅瞅error.log,好像哪里不太对


2023/04/20 23:15:35 [alert] 3348512#3348512: 768 worker_connections are not enough
2023/04/20 23:33:33 [alert] 3349854#3349854: *3492 open socket #735 left in connection 1013

这是什么?貌似是连接池问题,赶紧打开nginx.conf看一下配置


events {
worker_connections 666;
# multi_accept on;
}

???


运维小可爱,你特么在跟我开玩笑?虽然我们这个系统是给B端用的,还是我们自己组的不到100人,但是这连接池给的也太少了吧!


另外,前端为什么会开到 1000 个 WS 连接呢?后端为什么没有释放掉FD呢?


询问后,才知道,前端的Socket管理器,会在连接超时或者其它异常情况下,重新开启一个WS连接。


后端的心跳配置给了300秒


Constant::OPTION_OPEN_WEBSOCKET_PROTOCOL => true, // websocket 协议
Constant::OPTION_HEARTBEAT_CHECK_INTERVAL => 150,
Constant::OPTION_HEARTBEAT_IDLE_TIME => 300,

此时修改nginx.conf的配置,直接拉满!!!


worker_connections 655350;

重启Nginx,哇绰,好像可以访问了,但是每当解决一个问题,总会产生新的问题。


此时error.log中出现了新的报错:


2023/04/20 23:23:41 [crit] 3349767#3349767: accept4() failed (24: Too many open files)

这种就不怕了,貌似和Linux的文件句柄限制有关系,印象中是1024个。
至少有方向去排查,不是吗?而且这已经算是常规问题了,我只需小小的百度一下,哼哼~


拉满拉满!!


worker_rlimit_nofile 65535;

此时再次重启Nginx服务,系统恢复稳定,查看当前连接数:


netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

# 打印结果
TIME_WAIT 1175

FIN_WAIT1 52

SYN_RECV 1

FIN_WAIT2 9

ESTABLISHED 2033

经过多次查看,发现TIME_WAITESTABLISHED都在不断减少,最后完全降下来。


本次问题排查结束,问题得到了解决,但是关于socket的连接池管理器,仍然需要优化,及时释放socket无用的连接。


作者:兰陵笑笑生666
来源:juejin.cn/post/7224314619865923621
收起阅读 »

浅谈多人游戏原理和简单实现

一、我的游戏史 我最开始接触游戏要从一盘300游戏的光碟说起,那是家里买DVD送的,《魂斗罗》、《超级马里奥》天天玩。自从买回来后,我就经常和姐姐因为抢电视机使用权而大打出手。有次她把遥控器藏到了沙发的夹层里,被我妈一屁股做成了两半,我和我姐喜提一顿暴打。那顿...
继续阅读 »

在这里插入图片描述


一、我的游戏史


我最开始接触游戏要从一盘300游戏的光碟说起,那是家里买DVD送的,《魂斗罗》、《超级马里奥》天天玩。自从买回来后,我就经常和姐姐因为抢电视机使用权而大打出手。有次她把遥控器藏到了沙发的夹层里,被我妈一屁股做成了两半,我和我姐喜提一顿暴打。那顿是我挨得最狠的,以至于现在回想起来,屁股还条件反射的隐隐作痛。


后来我骗我妈说我要学习英语、练习打字以后成为祖国的栋梁之才!让她给我买台小霸王学习机(游戏机),在我一哭二闹三上吊胡搅蛮缠的攻势下,我妈妥协了。就此我接触到了FC游戏。现在还能记得我和朋友玩激龟快打,满屋子的小朋友在看的场景。经常有家长在我家门口喊他家小孩吃饭。那时候我们县城里面有商店卖游戏卡,小卡一张5块钱,一张传奇卡25-40块不等(所谓传奇卡就是角色扮演,带有存档的游戏),每天放学都要去商店去看看,有没有新的游戏卡,买不起,就看下封面过过瘾。我记得我省吃俭用一个多月,买了两张卡:《哪吒传奇》和《重装机兵》那是真的上瘾,没日没夜的玩。


再然后我接触到了手机游戏,记得那时候有个软件叫做冒泡游戏(我心目的中的Stream),里面好多游戏,太吸引我了。一个游戏一般都是几百KB,最大也就是几MB,不过那时候流量很贵,1块钱1MB,并且!一个月只有30Mb。我姑父是收手机的,我在他那里搞到了一部半智能手机,牌子我现在还记得:诺基亚N70,那时候我打开游戏就会显示一杯冒着热气的咖啡,我很喜欢这个图标,因为看见它意味着我的游戏快加载完成了,没想到,十几年后我们会再次相遇,哈哈哈哈。我当时玩了一款网游叫做:《幻想三国》,第一回接触网游简直惊呆了,里面好多人都是其他玩的家,这太有趣了。并且我能在我的手机上看到其他玩家,能够看到他们的行为动作,这太神奇了!!!我也一直思考这到底是怎么实现的!


最后是电脑游戏,单机:《侠盗飞车》、《植物大战僵尸》、《虐杀原型》;网游:《DNF》、《CF》、《LOL》、《梦幻西游》我都玩过。


不过那个疑问一直没有解决,也一值留在我心中 —— 在网络游戏中,是如何实时更新其他玩家行为的呢?


二、解惑


在我进入大学后,我选择了软件开发专业,真巧!再次遇到了那个冒着热气的咖啡图标,这时我才知道它叫做——Java。我很认真的去学,希望有一天能够做一款游戏!


参加工作后,我并没有如愿以偿,我成为了一名Java开发程序员,但是我在日常的开发的都是web应用,接触到大多是HTTP请求,它是种请求-响应协议模式。这个问题也还是想不明白,难道每当其他玩家做一个动作都需要发送一次HTTP请求?然后响应给其他玩家。这样未免效率也太低了吧,如果一个服务器中有几千几万人,那么服务器得承受多大压力呀!一定不是这样的!!!


直到我遇到了Websocket,这是一种长连接,而HTTP是一种短连接,顿时这个问题我就想明白了。在此二者的区别我就不过多赘述了。详细请看我的另一篇文章


知道了这个知识后,我终于能够大致明白了网络游戏的基本原理。原来网络游戏是由客户端服务器端组成的,客户端就是我们下载到电脑或者手机上的应用,而服务器端就是把其他玩家连接起来的中转站,还有一点需要说明的是,网络游戏是分房间的,这个房间就相当于一台服务器。首先,在玩家登陆客户端并选择房间建立长连接后,A玩家做出移动的动作,随即会把这个动作指令上传给服务器,然后服务器再将指令广播到房间中的其他玩家的客户端来操作A的角色,这样就可以实现实时更新其他玩家行为。


在这里插入图片描述


三、简单实现


客户端服务端在处理指令时,方法必须是配套的。比如说,有新的玩家连接到服务器,那么服务器就应当向其它客户端广播创建一个新角色的指令,客户端在接收到该指令后,执行客户端创建角色的方法。
为了方便演示,这里需要定义两个HTML来表示两个不同的客户端不同的玩家,这两套客户端代码除了玩家的信息不一样,其它完全一致!!!


3.1 客户端实现步骤


我在这里客户端使用HTML+JQ实现


客户端——1代码:


(1)创建画布


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas Game</title>
<style>
canvas {
border: 1px solid black;
}
</style>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<canvas id="gameCanvas" width="800" height="800"></canvas>
</body>
</html>

(2)设置1s60帧更新页面


const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function gameLoop() {
clearCanvas();
players.forEach(player => {
player.draw();
});
}
setInterval(gameLoop, 1000 / 60);
//清除画布方法
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}

(3)连接游戏服务器并处理指令


这里使用websocket链接游戏服务器


 //连接服务器
const websocket = new WebSocket("ws://192.168.31.136:7070/websocket?userId=" + userId + "&userName=" + userName);
//向服务器发送消息
function sendMessage(userId,keyCode){
const messageData = {
playerId: userId,
keyCode: keyCode
};
websocket.send(JSON.stringify(messageData));
}
//接收服务器消息,并根据不同的指令,做出不同的动作
websocket.onmessage = event => {
const data = JSON.parse(event.data);
// 处理服务器发送过来的消息
console.log('Received message:', data);
//创建游戏对象
if(data.type == 1){
console.log("玩家信息:" + data.players.length)
for (let i = 0; i < data.players.length; i++) {
console.log("玩家id:"+playerOfIds);
createPlayer(data.players[i].playerId,data.players[i].pointX, data.players[i].pointY, data.players[i].color);
}
}
//销毁游戏对象
if(data.type == 2){
console.log("玩家信息:" + data.players.length)
for (let i = 0; i < data.players.length; i++) {
destroyPlayer(data.players[i].playerId)
}
}
//移动游戏对象
if(data.type == 3){
console.log("移动;玩家信息:" + data.players.length)
for (let i = 0; i < data.players.length; i++) {
players.filter(player => player.id === data.players[i].playerId)[0].move(data.players[i].keyCode)
}
}
};

(4)创建玩家对象


//存放游戏对象
let players = [];
//playerId在此写死,正常情况下应该是用户登录获取的
const userId = "1"; // 用户的 id
const userName = "逆风笑"; // 用户的名称
//玩家对象
class Player {
constructor(id,x, y, color) {
this.id = id;
this.x = x;
this.y = y;
this.size = 30;
this.color = color;
}
//绘制游戏角色方法
draw() {
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, this.size, this.size);
}
//游戏角色移动方法
move(keyCode) {
switch (keyCode) {
case 37: // Left
this.x = Math.max(0, this.x - 10);
break;
case 38: // Up
this.y = Math.max(0, this.y - 10);
break;
case 39: // Right
this.x = Math.min(canvas.width - this.size, this.x + 10);
break;
case 40: // Down
this.y = Math.min(canvas.height - this.size, this.y + 10);
break;
}
this.draw();
}
}

(5)客户端创建角色方法


//创建游戏对象方法
function createPlayer(id,x, y, color) {
const player = new Player(id,x, y, color);
players.push(player);
playerOfIds.push(id);
return player;
}

(6)客户端销毁角色方法


在玩家推出客户端后,其它玩家的客户端应当销毁对应的角色。


//角色销毁
function destroyPlayer(playId){
players = players.filter(player => player.id !== playId);
}

客户端——2代码:


客户端2的代码只有玩家信息不一致:


  const userId = "2"; // 用户的 id
const userName = "逆风哭"; // 用户的名称

3.2 服务器端


服务器端使用Java+websocket来实现!


(1)引入依赖:


 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.3.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.3</version>
</dependency>

(2)创建服务器


@Component
@ServerEndpoint("/websocket")
@Slf4j
public class Server {
/**
* 服务器玩家池
* 解释:这里使用 ConcurrentHashMap为了保证线程安全,不会出现同一个玩家存在多条记录问题
* 使用 static fina修饰 是为了保证 playerPool 全局唯一
*/

private static final ConcurrentHashMap<String, Server> playerPool = new ConcurrentHashMap<>();
/**
* 存储玩家信息
*/

private static final ConcurrentHashMap<String, Player> playerInfo = new ConcurrentHashMap<>();
/**
* 已经被创建了的玩家id
*/

private static ConcurrentHashMap<String, Server> createdPlayer = new ConcurrentHashMap<>();

private Session session;

private Player player;

/**
* 连接成功后调用的方法
*/

@OnOpen
public void webSocketOpen(Session session) throws IOException {
Map<String, List<String>> requestParameterMap = session.getRequestParameterMap();
String userId = requestParameterMap.get("userId").get(0);
String userName = requestParameterMap.get("userName").get(0);
this.session = session;
if (!playerPool.containsKey(userId)) {
int locationX = getLocation(151);
int locationY = getLocation(151);
String color = PlayerColorEnum.getValueByCode(getLocation(1) + 1);
Player newPlayer = new Player(userId, userName, locationX, locationY,color,null);
playerPool.put(userId, this);
this.player = newPlayer;
//存放玩家信息
playerInfo.put(userId,newPlayer);
}
log.info("玩家:{}|{}连接了服务器", userId, userName);
// 创建游戏对象
this.createPlayer(userId);
}

/**
* 接收到消息调用的方法
*/

@OnMessage
public void onMessage(String message, Session session) throws IOException, InterruptedException {
log.info("用户:{},消息{}:",this.player.getPlayerId(),message);
PlayerDTO playerDTO = new PlayerDTO();
Player player = JSONObject.parseObject(message, Player.class);
List<Player> players = new ArrayList<>();
players.add(player);
playerDTO.setPlayers(players);
playerDTO.setType(OperationType.MOVE_OBJECT.getCode());
String returnMessage = JSONObject.toJSONString(playerDTO);
//广播所有玩家
for (String key : playerPool.keySet()) {
synchronized (session){
String playerId = playerPool.get(key).player.getPlayerId();
if(!playerId.equals(this.player.getPlayerId())){
playerPool.get(key).session.getBasicRemote().sendText(returnMessage);
}
}
}
}

/**
* 关闭连接调用方法
*/

@OnClose
public void onClose() throws IOException {
String playerId = this.player.getPlayerId();
log.info("玩家{}退出!", playerId);
Player playerBaseInfo = playerInfo.get(playerId);
//移除玩家
for (String key : playerPool.keySet()) {
playerPool.remove(playerId);
playerInfo.remove(playerId);
createdPlayer.remove(playerId);
}
//通知客户端销毁对象
destroyPlayer(playerBaseInfo);
}

/**
* 出现错误时调用的方法
*/

@OnError
public void onError(Throwable error) {
log.info("服务器错误,玩家id:{},原因:{}",this.player.getPlayerId(),error.getMessage());
}
/**
* 获取随即位置
* @param seed
* @return
*/

private int getLocation(Integer seed){
Random random = new Random();
return random.nextInt(seed);
}
}

websocket配置:


@Configuration
public class ServerConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}


(3)创建玩家对象


玩家对象:


@Data
@AllArgsConstructor
@NoArgsConstructor
public class Player {
/**
* 玩家id
*/

private String playerId;
/**
* 玩家名称
*/

private String playerName;
/**
* 玩家生成的x坐标
*/

private Integer pointX;
/**
* 玩家生成的y坐标
*/

private Integer pointY;
/**
* 玩家生成颜色
*/

private String color;
/**
* 玩家动作指令
*/

private Integer keyCode;
}

创建玩家对象返回给客户端DTO:


@Data
@AllArgsConstructor
@NoArgsConstructor
public class PlayerDTO {
private Integer type;
private List<Player> players;
}

玩家移动指令返回给客户端DTO:


@Data
@AllArgsConstructor
@NoArgsConstructor
public class PlayerMoveDTO {
private Integer type;
private List<Player> players;
}


(4)动作指令


public enum OperationType {
CREATE_OBJECT(1,"创建游戏对象"),
DESTROY_OBJECT(2,"销毁游戏对象"),
MOVE_OBJECT(3,"移动游戏对象"),
;
private Integer code;
private String value;

OperationType(Integer code, String value) {
this.code = code;
this.value = value;
}

public Integer getCode() {
return code;
}

public String getValue() {
return value;
}
}

(5)创建对象方法


  /**
* 创建对象方法
* @param playerId
* @throws IOException
*/

private void createPlayer(String playerId) throws IOException {
if (!createdPlayer.containsKey(playerId)) {
List<Player> players = new ArrayList<>();
for (String key : playerInfo.keySet()) {
Player playerBaseInfo = playerInfo.get(key);
players.add(playerBaseInfo);
}
PlayerDTO playerDTO = new PlayerDTO();
playerDTO.setType(OperationType.CREATE_OBJECT.getCode());
playerDTO.setPlayers(players);
String syncInfo = JSONObject.toJSONString(playerDTO);
for (String key :
playerPool.keySet()) {
playerPool.get(key).session.getBasicRemote().sendText(syncInfo);
}
// 存放
createdPlayer.put(playerId, this);
}
}

(6)销毁对象方法


   /**
* 销毁对象方法
* @param playerBaseInfo
* @throws IOException
*/

private void destroyPlayer(Player playerBaseInfo) throws IOException {
PlayerDTO playerDTO = new PlayerDTO();
playerDTO.setType(OperationType.DESTROY_OBJECT.getCode());
List<Player> players = new ArrayList<>();
players.add(playerBaseInfo);
playerDTO.setPlayers(players);
String syncInfo = JSONObject.toJSONString(playerDTO);
for (String key :
playerPool.keySet()) {
playerPool.get(key).session.getBasicRemote().sendText(syncInfo);
}
}

四、演示


4.1 客户端1登陆服务器


在这里插入图片描述


4.2 客户端2登陆服务器


在这里插入图片描述


4.3 客户端2移动


在这里插入图片描述


4.4 客户端1移动


在这里插入图片描述


4.5 客户端1退出


在这里插入图片描述
完结撒花


完整代码传送门


五、总结


以上就是我对网络游戏如何实现玩家实时同步的理解与实现,我实现后心里也释然了,哈哈哈,真的好有趣!!!
我希望大家也是,不要失去好奇心,遇到自己感兴趣的事情,一定要多思考呀~


后来随着我经验的不断积累,我又去了解了一下Java作为游戏服务器的相关内容,发现Netty更适合做这个并且更容易入门,比如《我的世界》一些现有的服务器就是使用Netty实现的。有空也实现下,玩玩~


作者:是江迪呀
来源:juejin.cn/post/7273429629398581282
收起阅读 »

向前兼容与向后兼容

2012年3月发布了Go 1.0,随着 Go 第一个版本发布的还有一份兼容性说明文档。该文档说明,Go 的未来版本会确保向后兼容性,不会破坏现有程序。 即用10年前Go 1.0写的代码,用10年后的Go 1.18版本,依然可以正常运行。即较高版本的程序能正常...
继续阅读 »

2012年3月发布了Go 1.0,随着 Go 第一个版本发布的还有一份兼容性说明文档。该文档说明,Go 的未来版本会确保向后兼容性,不会破坏现有程序。


即用10年前Go 1.0写的代码,用10年后的Go 1.18版本,依然可以正常运行。即较高版本的程序能正常处理较低版本程序的数据(代码)


反之则不然,如之前遇到过的这个问题[1]:在Mac上用Go 1.16可正常编译&运行的代码,在cvm服务器上Go 1.11版本,则编译不通过;


再如部署Spring Boot项目[2]时遇到的,在Mac上用Java 17开发并打的jar包,在cvm服务器上,用Java 8运行会报错




一般会认为向前兼容是向之前的版本兼容,这理解其实是错误的。


注意要把「前」「后」分别理解成「前进」和「后退」,不可以理解成「从前」和「以后」


线上项目开发中,向后(后退)兼容非常重要; 向后兼容就是新版本的Go/Java,可以保证之前用老版本写的程序依然可以正常使用




前 forward 未来拓展。


后 backward 兼容以前。







  • 向前兼容(Forward Compatibility):指老版本的软/硬件可以使用或运行新版本的软/硬件产生的数据。“Forward”一词在这里有“未来”的意思,其实翻译成“向未来”更明确一些,汉语中“向前”是指“从前”还是“之后”是有歧义的。是旧版本对新版本的兼容 (即向前 到底是以前还是前面?实际是前面





  • 向上兼容(Upward Compatibility):与向前兼容相同。









  • 向后兼容(Backward Compatibility):指新的版本的软/硬件可以使用或运行老版本的软/硬件产生的数据。是新版本对旧版本的兼容





  • 向下兼容(Downward Compatibility):与向后兼容相同。











软件的「向前兼容」和「向后兼容」如何区分?[3]


参考资料


[1]

这个问题: https://dashen.tech/2021/05/30/gvm-%E7%81%B5%E6%B4%BB%E7%9A%84Go%E7%89%88%E6%9C%AC%E7%AE%A1%E7%90%86%E5%B7%A5%E5%85%B7/#%E7%BC%98%E8%B5%B7

[2]

部署Spring Boot项目: https://dashen.tech/2022/02/01/%E9%83%A8%E7%BD%B2Spring-Boot%E9%A1%B9%E7%9B%AE/

[3]

软件的「向前兼容」和「向后兼容」如何区分?: https://www.zhihu.com/question/47239021



作者:fliter
来源:mdnice.com/writing/b8eb5fdae77f42e897ba69898a58e0d8
收起阅读 »

对负载均衡的全面理解

title: 对负载均衡的全面理解 date: 2021-07-10 21:41:24 tags: TCP/IP 对负载均衡服务(LBS)大名入行不多久就一直听闻,后来的工作中,也了解到 软件负载均衡器,如被合入Linux内核的章文嵩的LVS,还有...
继续阅读 »


title: 对负载均衡的全面理解 date: 2021-07-10 21:41:24 tags: TCP/IP





负载均衡服务(LBS)大名入行不多久就一直听闻,后来的工作中,也了解到 软件负载均衡器,如被合入Linux内核的章文嵩的LVS,还有以应用程序形式出现的HAProxy、KeepAlived,以及更熟悉的Nginx 等


也知道价格高昂的硬件负载均衡器如F5,A10 (甚至搬运过报废的F5)



















但长期以来,也有一些疑惑不解,比如





  • 常说的四层负载均衡是不是就是在传输层实现负载均衡?





  • 四层负载均衡中常听到的三角传输模式IP隧道模式NAT模式,有何区别?哪个性能最好?





  • 四层负载均衡性能好,那为何还有如nginx这样名气更大的七层负载均衡的出现?(Nginx也可以用来做四层代理)





  • 负载均衡与反向代理有何异同?





  • 转发和代理有何本质不同?




这是几年前记的笔记,显然存有谬误。





计算机网络中常见缩略词翻译及简明释要




通读 凤凰架构--负载均衡一章,可知





  • 四层负载均衡 主要工作在第二层和第三层,即 数据链路层和网络层 (通过改写 MAC 地址IP 地址 实现转发)​​​





  • “三角传输模式”(Direct Server Return,DSR),是作用于 数据链路层负载均衡,也称“单臂模式”(Single Legged Mode)或者“直接路由”(Direct Routing)。 通过修改请求数据帧中的 MAC 目标地址,让用户原本是发送给负载均衡器的请求的数据帧,被二层交换机根据新的 MAC 目标地址转发到服务器集群中对应的服务器(“真实服务器”)的网卡上。 效率高性能好,但有些场景不能满足










  • 网络层负载均衡:IP隧道模式,NAT模式


IP隧道模式:





NAT模式:







在流量压力比较大的时候,NAT 模式的负载均衡会带来较大的性能损失,比起直接路由和 IP 隧道模式,甚至会出现数量级上的下降






  • 四层负载均衡进行转发,只有一条TCP通道; 七层负载均衡只能进行代理,需要有两条TCP通道








  • 七层负载均衡器就属于反向代理中的一种;





  • 如果只论网络性能,七层均衡器肯定是无论如何比不过四层均衡器的;但其工作在应用层,可以感知应用层通信的具体内容,往往能够做出更明智的决策,玩出更多的花样来。









负载均衡的两大职责是“选择谁来处理用户请求”和“将用户请求转发过去”。上面讲的都是怎样将用户请求转发过去


至于选择哪台应用服务器来处理用户请求(翻牌子),则有很多算法,如下图就是F5的一些选择算法












B站:一次性讲清楚四层负载均衡中的NAT模式和IP隧道模式


Shadowsocks源码解读——什么是代理?什么是隧道?


NAT模式、路由模式、桥接模式的区别


VLAN是二层技术还是三层技术?


四层负载均衡详解


作者:fliter
来源:mdnice.com/writing/c5b54a9bdd78478a87c6d39e38572358
收起阅读 »

Apipost: 开发者们的瑞士军刀

在当今的数字化时代,数据流通是推动社会进步的关键因素之一。其中,API(应用编程接口)已经成为跨平台数据交互的标准。然而,API开发和管理并非易事,Apipost一体化研发协作赋能平台,支持从API设计到API调试再到API测试覆盖整个API生命周期的API管...
继续阅读 »

在当今的数字化时代,数据流通是推动社会进步的关键因素之一。其中,API(应用编程接口)已经成为跨平台数据交互的标准。然而,API开发和管理并非易事,Apipost一体化研发协作赋能平台,支持从API设计到API调试再到API测试覆盖整个API生命周期的API管理平台,一起来看看Apipost有什么不同吧。


一、Apipost是什么?


Apipost是一个专为开发者设计的API管理工具,提供了全面的API文档生成、调试、测试和分享功能。它的目标是帮助开发者简化API开发流程,提高工作效率。


二、如何使用Apipost?


安装:


进入官网下载安装或直接使用web端



使用:


可以从其他平台如postman导入脚本文件,或创建接口。



接口调试:


输入接口URL后点击发送即可模拟接口请求,上方为请求区,下方为响应区



生成接口文档:


点击分享文档即可生成标准的接口文档,可以将链接分享给需要查看接口的其他同事



一键压测


接口调试完成后可以在一键压测页面进行并发测试,看看接口在高并发情况下的运行情况



总结


Apipost作为一款专为开发者设计的API管理工具,凭借其强大的功能和易用性,已经在开发者社区中积累了良好的口碑。通过使用Apipost,开发者可以节省大量时间,专注于创新和打造卓越的产品。如果你正在寻找一款强大且易用的API管理工具,那么Apipost无疑是一个值得考虑的选择。


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

哇咔咔,体验了一把抖音的ChatGPT

说实在的,看到这里,让我忽然想吃过年时候蒸的豆包,不知道字节起这样一个名字有什么用意 最近一直在分享各种AIGC类的东西,感兴趣的可以看下主页历史干货。 你好,我是豆包 礼貌性的回复一句,你好,我是1点东西,我要开始使用你了。豆包是谁呢,可能有些朋友还不知道 ...
继续阅读 »


说实在的,看到这里,让我忽然想吃过年时候蒸的豆包,不知道字节起这样一个名字有什么用意


最近一直在分享各种AIGC类的东西,感兴趣的可以看下主页历史干货。


你好,我是豆包


礼貌性的回复一句,你好,我是1点东西,我要开始使用你了。豆包是谁呢,可能有些朋友还不知道


据悉,“豆包”的前身正是字节内部代号为“Grace”的AI项目。目前在AI浪潮下已经形成独立的AIGC产品供用户使用




刚进来可以看到经典的左右格局,左侧依然是历史问题记录区域,和其他国产GPT产品一样,有一些聚焦的功能模块。


不同的是,显的更加简洁大气,使用柔和不僵硬。毋庸置疑抖音的模型是基于字节产品多年数据沉淀最终服务于子节用户以及更好的发展。



可以看到回答问题响应很快,问题回答干脆不拖泥带水,同样在问题的最下方有点赞、复制、重新生成等功能。需要注意的是最下面有一个搜索功能,点击会跳转到今日头条进行搜索。


体验能力


左侧有英语小助手,先来看下英文能力怎么样




这能力杠杠的,学英语再也不是难事。接下来看全能写作助手体验。






接着问,测试上下文能力。




总体上还算总结的不错,我们问下小日本核废水排海事件




很明显,并不支持联网。




而且没有文生图功能




看下编程能力




编程能力也毫不逊色,最后可以问下GPT3.5都回答错误的问题。看看国产大模型咋样。




OK,今天的一个小分享暂时先到这里。上面的抖音的的申请体验链接:http://www.doubao.com/chat


最近涉猎于AIGC,总结了一些AI资料(实时更新),无套路分享给大家


1点东西AI资料地址



标签:

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

为啥count(*)会这么慢?

背景 本没想着写这篇文章的,因为我觉得这个东西大多数有经验的开发遇到过,肯定也了解过相关的原因,但最近我看到有几个关注的技术公众号在推送相关的文章。实在令我吃惊! 先上公众号文章的结论: count(*) :它会获取所有行的数据,不做任何处理,行数加1。 c...
继续阅读 »

背景


本没想着写这篇文章的,因为我觉得这个东西大多数有经验的开发遇到过,肯定也了解过相关的原因,但最近我看到有几个关注的技术公众号在推送相关的文章。实在令我吃惊!


先上公众号文章的结论:



  • count(*) :它会获取所有行的数据,不做任何处理,行数加1。

  • count(1):它会获取所有行的数据,每行固定值1,也是行数加1。

  • count(id):id代表主键,它需要从所有行的数据中解析出id字段,其中id肯定都不为NULL,行数加1。

  • count(普通索引列):它需要从所有行的数据中解析出普通索引列,然后判断是否为NULL,如果不是NULL,则行数+1。

  • count(未加索引列):它会全表扫描获取所有数据,解析中未加索引列,然后判断是否为NULL,如果不是NULL,则行数+1。


结论:count(*) ≈ count(1) > count(id) > count(普通索引列) > count(未加索引列)


我也不想卖关子了,以上结论纯属放屁。根本就是个人yy出来的东西,甚至不愿意去验证一下,哪怕看一眼执行计划,也得不出这么离谱的结论。


我不敢相信这是一篇被多个技术公众号转载的文章!


以下所有的内容均是基于,mysql 5.7 + InnoDB引擎, 进行的分析。


拓展:


MyISAM 如果没有查询条件,只是简单的统计表中数据总数,将会返回的超快,因为service层中获取到表信息中的总行数是准确的,而InnoDB只是一个估值。


实例


废话不多说,先看一个例子。


以下是一张表数据量有100w,表中字段相对较短,整体数据量不算大。


CREATE TABLE `hospital_statistics_data` (
`pk_id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`id` varchar(36) COLLATE utf8mb4_general_ci NOT NULL COMMENT '外键',
`hospital_code` varchar(36) COLLATE utf8mb4_general_ci NOT NULL COMMENT '医院编码',
`biz_type` tinyint NOT NULL COMMENT '1服务流程 2管理效果',
`item_code` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '考核项目编码',
`item_name` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '考核项目名称',
`item_value` varchar(36) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '考核结果',
`is_deleted` tinyint DEFAULT NULL COMMENT '是否删除 0否 1是',
`gmt_created` datetime DEFAULT NULL COMMENT '创建时间',
`gmt_modified` datetime DEFAULT NULL COMMENT 'gmt_modified',
`gmt_deleted` datetime(3) DEFAULT '9999-12-31 23:59:59.000' COMMENT '删除时间',
PRIMARY KEY (`pk_id`)
) DEFAULT CHARSET=utf8mb4 COMMENT='医院统计数据';

此表初始状态只有一个聚簇索引


以下分不同索引情况,看一下COUNT(*)的执行计划。


1)在只有一个聚簇索引的情况下看一下执行计划。


EXPLAIN select COUNT(*) from hospital_statistics_data;

结果:



关于执行计划的各个参数的含义,不在本文的讨论范围内,可自行了解。


这里只关注以下几个属性。



  1. type: 这里显示index,说明使用了索引。

  2. key:PRIMARY使用了主键索引。

  3. key_len: 索引长度8字节。


这里有很关键的一点:count(*)也会走索引,在当前情况下使用了聚簇索引。


好,再往下看。


2)存在一个非聚簇索引(二级索引)


给表添加一个hospital_code索引。


alter table hospital_statistics_data add index idx_hospital_code(hospital_code)

此时表中存在2个索引,主键 hospital_code


同样的,再执行一下:


EXPLAIN select COUNT(*) from hospital_statistics_data;

结果:



同样的,看一下 type、key和key_len三个字段。


是不是觉得有点“神奇”。


为何索引变成刚添加的idx_hospital_code了。


先别急着想结论,再看下面一种情况。


3)存在两个非聚簇索引(二级索引)


在上面的基础上,再添加一个二级索引。


alter table hospital_statistics_data add index idx_biz_type(biz_type)

此时表中存在3个索引,主键 、hospital_code 和 biz_type。


同样的,执行一下:


EXPLAIN select COUNT(*) from hospital_statistics_data;

结果:



是不是更困惑了,索引又..又又...变了.


变成新添加的idx_biz_type。


先不说为何会产生以上的变化,继续往下分析。


在以上3个索引的基础上,分别看一下,count(1)count(id)count(index)count(无索引)


这4种情况,与count(*)的执行计划有何区别。



  1. count(1)




  1. count(id)
    对于样例表来说是,主键是pk_id


image.png



  1. count(index)


这里选取biz_type索引字段。




  1. count(无索引)



小结:




  1. count(index) 会使用当前index指定的索引。




  2. count(无索引) 是全表扫描,未走索引。




  3. count(1) , count(*), count(id) 一样都会选择idx_biz_type索引




看到这,你还觉得那些千篇一律的公众号文章的结论正确吗?


必要知识点




  1. mysql 分为service层引擎层




  2. 所有的sql在执行前会经过service层的优化,优化分为很多类型,简单的来说可分为成本规则




  3. 执行计划所反映的是service层经过sql优化后,可能的执行过程。并非绝对(免得有些人说我只看执行计划过于片面)。绝大多数情况执行计划是可信的




  4. 索引类型分为聚簇索引非聚簇索引(二级索引)。其中数据都是挂在聚簇索引上的,非聚簇索引上只是记录的主键id。




  5. 抛开数据内存,只谈数据量,都是扯淡。什么500w就是极限,什么2个表以上的join都需要优化了,什么is null不会走索引等,纯纯的放屁。




  6. 相信一点,编写mysql代码的人比,看此文章的大部分人都要优秀。他们会尽可能在执行前,对我这样菜逼写的乱七八糟的sql进行优化。




原因分析


其实原因非常非常简单,上面也说了,service层会基于成本进行优化


并且,正常情况下,非聚簇索引所占有的内存要远远小于聚簇索引。所以问题来了,如果你是mysql的开发人员,你在执行count(*)查询的时候会使用那个索引?


我相信正常人都会使用非聚簇索引


那如果存在2个甚至多个非聚簇索引又该如何选择呢?


那肯定选择最短的,占用内存最小的一个呀,在回头看看上面的实例,还迷惑吗。


同样都是非聚簇索引。idx_hospital_codelen146字节;而idx_biz_typelen只有1。那还要选吗?


那为何count(*)走了索引,却还是很慢呢?


这里要明确一点,索引只是提升效率的一种方式,但不能完全的解决效率问题。count(*)有一个明显的缺陷,就是它要计算总数,那就意味着要遍历所有符合条件的数据,相当于一个计数器,在数据量足够大的情况下,即使使用非聚簇索引也无法优化太多。


官方文档:



InnoDBhandlesSELECT COUNT(*)andSELECT COUNT(1)operations in the same way. There is no performance difference.



简单的来说就是,InnoDB下 count(*) 等价于 count(1)


既然会自动走索引,那么上面那个所谓的速度排序还觉得对吗? count(*)的性能跟数据量有很大的关系,此外最好有一个字段长度较短的二级索引。


拓展:


另外,多说一下,关于网上说的那些索引失效的情况,大多都是片面的,我这里只说一点。量变才能引起质变,索引的失效取决于你圈定数据的范围,若你圈定的数据量占整体数据量的比例过高,则会放弃使用索引,反之则会优先使用索引。但是此规则并不是完美的,有时候可能与你预期的不同,也可以通过一些技巧强制使用索引,但这种方式少用。


举个栗子:


通过上面这个表hospital_statistics_data,我进行了如下查询:


select * from hospital_statistics_data where hospital_code is not null;

此时这个sql会使用到hospital_code的索引吗?


这里也不卖关子了,若hospital_code只有很少一部分数据是null值,那么将不会走索引,反之则走索引。


原因就2个字:回表


好比去买砂糖橘,如果你只买几斤,那么你随便挑筐里面好的就行。但是如果你要买一筐,我相信老板不会让你在里面一个个挑,而是一次给你一整筐,当然大家都不傻,都知道筐里里面肯定有那么几个坏果子。但是这样效率最高,而且对老板来说损失更小。


执行过程


摘抄自《从根上理解mysql》。我强烈推荐没有系统学过mysql的,看看这本书。


1.首先在server层维护一个count变量


2.server层向InnoDB引擎要第一条记录


3.InnoDB找到第一条二级索引记录,并返回给server层(注意:由于此时只是统计记录数量,所以并不需要回表)


4.由于COUNT函数的参数是*,MySQL会将*当作常数0处理。由于0并不是NULL,server层给count变量加1。


5.server层向InnoDB要下一条记录。


6.InnoDB通过二级索引记录的next_record属性找到下一条二级索引记录,并返回给server层。


7.server层继续给count变量加1。


8.重复上述过程,直到InnoDB向server层返回没记录可查的消息。


9.server层将最终的count变量的值发送到客户端。


总结


写完后还是心中挺郁闷的,现在能从公众号获取到的好文章越来越少了,现在已经是知识付费的时代了。


挺怀念刚工作的时候,那时候每天上午都花点时间看看公众号文章,现在全都是广告。哎!


不过也正常,谁也不能一直为爱发电。


学习还是建议多看看书籍,一般能成书的都不会太差。现在晚上能搜到的都

作者:微笑兔
来源:juejin.cn/post/7182491131651817531
是千篇一律的文章,对错不知。网上

收起阅读 »

Git 合并冲突不知道选哪个?试试开启 diff3 吧

iOS
导读:Git 早在 2008 年就提供 diff3,用于冲突展示时额外提供该区域的原始内容(两个分支公共祖先节点在此区域的内容),帮助更好的合并冲突。在 2022 年 Q1 发布的 Git 2.35 ,提供了一个新的选项 zdiff3,进一步优化了diff3 ...
继续阅读 »

导读:Git 早在 2008 年就提供 diff3,用于冲突展示时额外提供该区域的原始内容(两个分支公共祖先节点在此区域的内容),帮助更好的合并冲突。在 2022 年 Q1 发布的 Git 2.35 ,提供了一个新的选项 zdiff3,进一步优化了diff3 的展现。



Git 合并冲突,常见的展示形式分为 Current Change (ours, 当前分支的变更)和 Incoming Change (theirs, 目标分支的变更),两者针对的是同一区域的变化。



观察上面这个冲突示例,我们并不清楚两个分支各自都发生了什么变化,有两种可能:

  1. 两个分支同时增加了一行代码 "pkg": xxx
  2. 原先的提交记录里就有 "pkg": xxx ,只是两个分支同时修改了版本号

实际上这个例子,是第二种情况,两个分支都对 pkg 的版本做了改变。




这样的场景还有很多,如果不知道上下文,在解决冲突的时候容易束手束脚。


现在,我们可以使用 git 提供的 diff3 选项来调整合并冲突的展示效果



红框区域(|||||||=======)表示的就是改动前的上下文,确切的说, 当前分支 目标合并分支 的最近公共祖先节点在该区域的内容。


如何开启


冲突展示有两个选项 diff3merge(默认选项),可以通过以下方法进行配置



在 v2.35 新增了 zdiff3 选项,下文会提到

  • 对单个文件开启
git checkout --conflict=diff3 <文件名>
# 示例
git checkout --conflict=diff3 package.json
# 使用默认配置
git checkout --conflict=merge package.json
  • 项目配置
git config merge.conflictstyle diff3
# 删除配置
git config --unset merge.conflictstyle
# 使用默认配置
git config merge.conflictstyle merge
  • 全局配置
git config --global merge.conflictstyle diff3
# 删除配置
git config --global --unset merge.conflictstyle

示例展示


在同一位置添加代码行

<<<<<<< HEAD
import 'some_pkg';
||||||| merged common ancestor
=======
c
>>>>>>> merged-branch

如上示例,合并的公共祖先节点在该位置是空白,每个分支都在相同的位置添加代码行。


我们通常希望保留两者,并按照最有意义的顺序排序,也可能选择只保留其中一个。以下是一个冲突修复后的示例:

import 'some_pkg';
import 'some_pkg';

一方修改一方删除

<<<<<<< HEAD
||||||| merged common ancestor
console.log('调试信息')
=======
console.log('调试信息2')
>>>>>>> merged-branch

如上示例,一方把调试信息删除,而另一方修改了调试信息内容。对于这个示例,我们通常是选择删除而不保留修改。


为什么不是默认选项


经常需要知道祖先节点的内容来确保正确的合并,而 diff3 解决了这个痛点。同时,diff3 没有任何弊端(除了冲突区域行数变多🌝),没有理由不启用它。


那为什么 Git 不将 diff3 作为默认的合并冲突展示选项呢?


stackoverflow 上有人回答了这个问题,大概意思是说可能和 Unix diff 有关,早前默认的 Unix diff 不支持展示 3-way diff (待考证)。


之后的新版本也不方便调整默认值,否则会对用户造成困扰 — “合并冲突区域怎么多了一块内容?”。


zdiff3 (zealous diff3)


2022 年 Q1 ,Git 发布 v2.35,其中有个变化是冲突展示新增了 zdiff3 的配置选项。


zdiff3 基于 diff3 ,并对冲突块两侧的公共代码行做了压缩。


举个例子:




使用默认配置,合并冲突展示如下:

1
2
3
4
A
<<<<<<< ours
B
C
D
=======
X
C
Z
>>>>>>> theirs
E
7
8
9

使用 diff3 后,合并冲突展示如下:

1
2
3
4
<<<<<<
A
B
C
D
E
||||||
5
6
======
A
X
C
Z
E
>>>>>>
7
8
9

通过观察可以发现,冲突区域两侧有公共的代码行 A、E 。而这些代码行在默认配置下会被提取到外部。


而用了 zdiff3 之后,A、E 两行又将移到冲突之外。

1
2
3
4
A
<<<<<<
B
C
D
||||||
5
6
======
X
C
Z
>>>>>>
E
7
8
9

一句话总结 zdiff3 的优化:即展示公共祖先节点内容,又能够充分压缩冲突的公共部分。


最后


解决 Git 合并冲突是一个难题,diff3 并不是一个“银弹”,它只能帮助提供更多的信息,减少决策成本。


推荐读者尝试下 zdiff3 ,至少使用 diff3 ,并将其作为默认配置。


最后,如果看完本文有收获,欢迎一键三连(点赞、收藏、分享)🍻 ~


拓展阅读


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

Go 负责人说以后不会有 Go2 了

大家好,我是煎鱼。 最近 Go 核心团队负责人 @Russ Cox(下称:rsc)专门写了一篇文章《Backward Compatibility, Go 1.21, and Go 2》为 Go 这门编程语言的 Go1 兼容性增强和 Go2 的情况说明做诠释和宣...
继续阅读 »

大家好,我是煎鱼。


最近 Go 核心团队负责人 @Russ Cox(下称:rsc)专门写了一篇文章《Backward Compatibility, Go 1.21, and Go 2》为 Go 这门编程语言的 Go1 兼容性增强和 Go2 的情况说明做诠释和宣传。


今天希望能够帮助你获悉 Go 未来的规划、方向以及 rsc 的思考。


Go1 破坏兼容性的往事


新增结构体字段


第一个案例,比较经典。在 Go1 的时候,这段代码是可以正常运行的。如下演示代码:

// 脑子进煎鱼了
package main

import "net"

var myAddr = &net.TCPAddr{
net.IPv4(18, 26, 4, 9),
80,
}

但在 Go1.1,这段代码就跑不起来。必须要改成如下代码:

var myAddr = &net.TCPAddr{
IP: net.IPv4(18, 26, 4, 9),
Port: 80,
}

因为在当时的新版本中,对 net.TCPAddr 新增了 Zone 字段。原先的未声明值对应字段的方式就会出现一些问题。


后续在新版本的规范中,官方直接对标准库提交的代码增加了要求,赋值时必须声明字段名。以此避免该问题的产生。


改进排序/压缩的算法实现


第二个案例,Go1.6 时,官方修改了 Sort 的排序实现,使得运行速度提高了 10% 左右。以下是演示代码,将根据名称长度对颜色列表进行排序并输出结果:

colors := strings.Fields(
`black white red orange yellow green blue indigo violet`)
sort.Sort(ByLen(colors))
fmt.Println(colors)

一切听起来是那么的美好。


真实世界是改变排序算法通常会改变相等元素的排序方式。导致了 Go1.5 和 Go1.6 所输出的结果不一致:

Go 1.5:  [red blue green white black yellow orange indigo violet]
Go 1.6: [red blue white green black orange yellow indigo violet]

按照顺序排序后,结果集的差异点在于:


  • Go1.5 返回 green, white, black。
  • Go1.6 返回 white, green, black。

如果说程序依赖了结果集的输出顺序,这将是一个影响不小的兼容性破坏。


第三个案例,类似的还有在 Go1.8 中,官方改进了 compress/flate 的算法,达到了在 CPU 和 Memory 没有什么明显变化下,压缩后的结果集更小了。听起来是个很好的成果。


但实际上自己内部却翻车了,因为 Google 内部有一个需要可重现归档构建的项目,依赖了原有的算法。最后自己 fork 了一份来解决。


Go1.21 起增强兼容性(GODEBUG)


从上面的部分破坏兼容性示例来看,可以知道 Go 官方也不是刻意破坏的。但又存在必然要修改的各种原因和考量。


为此在 Go1.21 起,正式输出了 GODEBUG 的机制,相当于是开了个官方 “后门” 了。将其作为破坏性变更后的门把手。


允许设置 GODEBUG,来开关新功能特性。例如以下选项:

  • GODEBUG=asyncpreemptoff=1:禁用基于信号的 Goroutine 抢占,这偶尔会发现操作系统的错误。
  • GODEBUG=cgocheck=0:禁用运行时的 CGO 指针检查。
  • GODEBUG=cpu.<extension>=off:在运行时禁止使用某个特定的 CPU 扩展。

也会根据根据 go.mod 中的 Go 版本号来设置对应 GODEBUG,以提供版本所约定的 Go1 兼容性保障策略。


如果对这块感兴趣,可以查看《加大力度!Go 将会增强 Go1 向后兼容性》,有完整的增强兼容性的规范说明。


Go2 的情况和规划


Go 官方(via @rsc)正式回答了之前画的饼,也就是什么时候可以看到 Go2 的规范推出,打破 Go1 程序?


答案是永远不会。从与过去决裂、不再编译旧程序的意义上来说,Go 2 永远不会出现。从 Go 在 2017 年开始对 Go 1 进行重大修订的意义上来说,Go 2 已经发生了。


简而言之,透露出来的意思是:硬要说的话,Go2 已经套壳 Go1 上市了。


在未来规划上,不会出现破坏 Go1 程序的 Go2。工作方向会往将加倍努力保证兼容性的基础上,开展新的新工作。


总结


整体上 rsc 对破坏 Go1 兼容性做了很长时间规划的回溯和规划,释出了一大堆手段,例如:GODEBUG、go.mod 版本约束等。


从而引导了 Go2 直接可以借壳上的方向,也更好兑现了 Go1 兼容性保障的规范承诺。单从这方面来讲,还是非常的深思熟虑的。


也可能会有同学说,看 Go 现在这样,说不定下次就变了。这可能比较难,其实 rsc 才上任做团队负责人没几年,工作履历上和其他几位骨干大佬在 Google 已经有非常长年的在职经验了。



我目测一时半会是不会变的了。


想变,得等 Go 核心团队这一班子换了才有可能了。阻力也会很多,因为社区人多,一般会比较注重规范。



文章持续更新,可以微信搜【脑子进煎鱼了】阅读,本文 GitHub github.com/eddycjy/blo… 已收录,学习 Go 语言可以看 Go 学习地图和路线,欢迎 Star 催更。



Go 图书系列


推荐阅读


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

一个上午,我明白了,为什么总说人挣不到认知以外的钱

你好,我是刘卡卡,94年的大厂奶爸程序员,探索副业中 01 接下来,以我昨天上午的一段经历,讲述下为什么说我挣不到认知以外的钱 在昨天上班的路上,我在微信的订阅号推荐里面看到了下图 当时我的想法是:这东西阅读量好高噢,不过养老金和目前的我没什么关系。于是,我...
继续阅读 »

你好,我是刘卡卡,94年的大厂奶爸程序员,探索副业中


01


接下来,以我昨天上午的一段经历,讲述下为什么说我挣不到认知以外的钱


在昨天上班的路上,我在微信的订阅号推荐里面看到了下图



当时我的想法是:这东西阅读量好高噢,不过养老金和目前的我没什么关系。于是,我就划过去了。



(读者可以先停5s 思考下,假设是你看到这张图,会有什么想法)



02


当我坐上工位后,我看到我参加的社群里也人有发了上图,并附上了一段文字:


“养老金类型的公众号容易出爆文。


小白玩转职场这个号,篇篇10w+,而且这并不是一个做了很久的老号,而是今年5月才注册不久的号。 之前这个号刚做的时候是往职场方向发展,所以取名叫小白玩转职场,但是发了一阵后数据不是很好于是就换风格做了养老金的内容。


换到养老金赛道后就几乎篇篇10w+。 这些内容一般从官方网站找就好,选一些内容再加上自己想法稍微改下,或者直接通过Chatgpt辅助,写好标题就行”。


同时,文字下面有社群圈友留下评论说:“这是个好方向啊,虽然公众号文章已经同质化很严重了,但可以往视频号、带货等方向发展”。



读者可以先停5s 思考下,假设是你看到这段文字,会有什么想法。如果你不知道公众号赚钱的模式,那你大概率看不出这段话中的赚钱信息的



我想了想,对噢,确实可以挣到钱,于是将这则信息发到了程序员副业交流的微信群里。



然后,就有群友在交流:“他这是转载还是什么,不可能自己天天写吧”,“这种怎么冷启动呢,不会全靠搜索吧,“做他这种类型的公众号挺多吧,怎么做到每篇10w的”



有没有发现,这3个问题都是关注的怎么做的问题?怎么写的,怎么启动的,怎么每篇10w。


这就是我们大部分人的认知习惯,看到一个信息或别人赚钱的点子后,我们大部分人习惯去思考别人是如何做到的,是不是真的,自己可不可以做到。


可一般来说,我们当下的认知是有限的,大概率是想不出完整的答案的的,想不出来以后,然后就会觉得这个事情有点假,很难或者不适合。从而就错过这条信息了。



我当时觉得就觉得可能大部分群友会错过这则信息了,于是,在群里发了下面这段话


“分享一个点子后


首先去看下点子背后的商业变现机会,如带货/流量主/涨粉/等一系列


而后,才去考虑执行的问题,执行的话


1、首先肯定要对公众号流量主的项目流程进行熟悉


2、对标模仿,可以去把这个公众号的内容全看一看,看看别人为什么起号


3、做出差异化内容。”


发完后还有点小激动(嗯,我又秀了波自己的认知)。可到中午饭点,我发现我还是的认知太低了。


03


在中午吃饭时,我看到亦仁(生财有术的老大)发了这段内容



我被这段话震撼到了,我发现我现在的思考习惯,还是只停留在最表面的看山是山的地步。


我仅仅看到了这张图流量大,没有去思考它为什么这么大流量?



因为他通过精心制作的文章,为老年用户介绍了养老金的方方面面,所以,才会有流量


简单说,因为他满足了用户的需求



同样,我也没有思考还有没有其他产品可以满足用户的这个需求,我仅仅是停留在了视频号和公众号这两个产品砂锅。



只要满足用户需求,就不只有一个产品形态,对于养老金这个信息,我们可以做直播,做课程,做工具,做咨询,做1对1私聊的。这么看,就能有无数的可能



同时,我想到了要做差异化,但没有想到要通过关键字挖掘,去挖掘长尾词。



而亦仁,则直接就挖掘了百万关键字,并无偿分享了。



这才知道,什么叫做看山不是山了。


之前知道了要从“需求 流量 营销 变现”的角度去看待信息,也知道“需求为王”的概念。


可我看到这则信息时,还是没有考虑到需求这一层,更没有形成完整的闭环思路。


因此,以后要在看到这些信息时,去刻意练习“需求 流量 营销 变现”这个武器库,去关注他用什么产品,解决了用户什么需求,从哪里获取到的流量的,怎么做营销的,怎么做变现的。


04


于是,我就把这些思考过程也同样分享到了群里。


接着,下午我就看到有群友在自己的公众号发了篇和养老金相关的文章,虽然文章看上去很粗糙,但至少是起步了。


同时,我也建了个项目交流群,方便感兴趣的小伙伴交流进步(一群人走的更远)


不过我觉得在起步之前,也至少得花一两天时间去调研下,去评估这个需求有哪些地方自己可以切入进去,值不值得切入,能切入的话,怎么切入。


对了,可能你会关心我写了这么多,自己有没有做养老金相关的?


我暂时还没有,因为我目前关心的领域还在出海工具和个人IP上。


全文完结,如果对你有收获的话,关注公众号 刘卡卡 和我一起交流进步


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

👨‍💻 14 个最佳免费编程字体

我们整天都在使用代码编辑器、终端和其他开发工具,使用一种让眼睛舒服的字体可以大大改善我们的工作效率。 这篇文章汇总了 14 个免费的等宽编程字体,包括了每个字体的介绍、评价和下载链接。 😝 分享几个好玩的 VSCode 主题 🗂 让你的 VSCode 文件图...
继续阅读 »

我们整天都在使用代码编辑器、终端和其他开发工具,使用一种让眼睛舒服的字体可以大大改善我们的工作效率。
这篇文章汇总了 14 个免费的等宽编程字体,包括了每个字体的介绍、评价和下载链接。


😝 分享几个好玩的 VSCode 主题


🗂 让你的 VSCode 文件图标更好看的10个文件图标主题


🌈 冷门但好看的 VSCode 主题推荐


1. Fira Code




我曾使用 Monaco 字体超过十年的时间,直到我遇到了 Fira Code。这个字体在 Github 上面有超过 53,600 个 star,它这么受欢迎是有原因的。字体作者 Nikita Prokopov 在连字符(Ligature)上花了很多功夫,连字符可以把单独的字符合并成单一的逻辑标记。Fira Code 是我现在最喜欢的字体。




(Fira Code 中的连字符)


下载链接 • Github链接


2. IBM Plex Mono




Plex 系列字体是在 IBM 使用了 50 多年的 Helvetica 字体之后,被创建出来作为替代品的。它有着非常优雅的斜体字体,以及非常清晰易读的字形。美中不足的是,它没有包含连字符。


下载链接 • Github链接


3. Source Code Pro




Source Code Pro 是 Adobe 首先制作的开源字体之一。自2012年发布后,该字体大受欢迎,并被许多开发人员使用。它保留了 Source Sans 的设计特征和垂直比例,但改变了字形宽度,使其在所有粗细中保持一致。


下载链接 • Github链接


4. Monoid


如果你是那种讨厌水平滚动的人,这就是适合你的字体(因为这款字体比较细长)。它针对编程进行了优化,即使在低分辨率的显示器上也有 12px/9pt 的类似位图的清晰度。该字体还有一个名为 Monoisome 的 Font Awesome 集成。


下载链接 • Github链接




5. Hack


Hack 是所有字体中最可定制的之一,拥有1573个字形,你可以自行更改每一个字形的细节。此外,Powerline 字形也包含在其常规字体套件中。


下载链接 • Github链接




6. Iosevka


Iosevka 默认提供了苗条的字体形状:其字形宽度正好为1/2em。相比于其他的字体,你可以在同样的屏幕宽度下放置更多列的文字。它有两种宽度:普通和扩展。如果你希望字体间隔更大一点的话,就选择扩展版本的宽度。


下载链接 • Github链接




7. JetBrains Mono


IntelliJ、WebStorm 等诸多IDE背后的公司 —— JetBrains,在2020年出人意料地推出了自己的字体。他们的字体力求让代码行长度更符合开发人员的期望,使每个字母占据更多的像素。他们在保持字符的宽度标准的基础上最大化了小写字母的高度,从而实现这个目标。


下载链接 • Github链接




8. Fantasque Sans Mono


Fantasque Sans Mono 的设计以实用性为重点,它可以给你的代码增添一丝不一样的感觉。它手写风格的模糊感使其成为一个很酷的选择。


下载链接 • Github链接




9. Ubuntu Mono


这款字体是专门为了补充 Ubuntu 的语气而设计的。它拥有一种现代风格,并具有独特的 Ubuntu 品牌特性,传达出一种精准、可靠和自由的态度。如果你喜欢 Linux,但需要在 Windows 或 MacOS 上工作,这款字体将给你带来一点小小的慰藉和快乐。


下载链接 • 官网




10. Anonymous Pro


这种字体的出色之处在于,它特别区分了那些容易被误认为相同的字符,比如“0”(零)和“O”(大写字母O)。它是一个由四种固定宽度字体组成的字体族,特别针对编程人员而设计。


下载链接 • 官网




11. Inconsolata


这款字体可以作为微软的 Consolas 字体的开源替代。它是一种用于显示代码、终端等使用场景的等宽字体。它提供了连字符,能够给用户出色的编码体验。


下载链接 • GitHub链接




12. Victor Mono


这种字体简洁、清新且细长,具有较大的字母高度和清晰的标点符号,因此易读性强并且适合用于编程。它具有七种不同粗细和 Roman、Italic 和 Oblique 样式。它还提供可选的半连接草书斜体和编程符号连字符。


下载链接 • Github链接




13. Space Mono


这款字体专门为了标题和显示器排版而开发,它拥有几何板块的核心与新颖的近乎过度的合理化形式。它支持拉丁扩展字形集,可以用于英语和其他西欧语言的排版。


下载链接 • GitHub链接




14. Hasklig


在Source Code Pro的基础上,这款字体通过连字符来解决不合适的字符的问题,这也是排版师们一直以来使用的方式。底层代码保持不变——只有表现形式发生变化。


下载链接 • Github链接




哪个字体是你的最爱?


字体,就像颜色主题一样,是一个非常因人而异的话题。不同的开发者喜欢不同的字体。有些人喜欢连词,有些人不喜欢。有些人喜欢斜体字,有些人则讨厌。


希望这篇文章能帮助你找到喜欢的字体,给它一个机会,尝试使用几天,你会有不一样的感觉。



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

关于强制加班、人才培养、绩效考核的思考

来源于池老师星球里的一个提问,我也借此机会做一个小归纳,本来想直接贴问题截图的,想了想由于是池老师星球的里的提问,还是不要贴图片了。 大致问题描述:1.公司强制124加班,但是没那么多事情需要加班去做,如何让大家把这些时间利用起来去学习,提高团队能力2.作为研...
继续阅读 »

来源于池老师星球里的一个提问,我也借此机会做一个小归纳,本来想直接贴问题截图的,想了想由于是池老师星球的里的提问,还是不要贴图片了。


大致问题描述:

  • 1.公司强制124加班,但是没那么多事情需要加班去做,如何让大家把这些时间利用起来去学习,提高团队能力
  • 2.作为研发团队管理者绩效考核怎么设计,量化与非量化如何平衡

我的评论原文:


同样的情况我也有碰到,124强制加班,公司跟不上的意思猜测情况是“技术团队支撑业务需要绰绰有余,又或者是业务侧增速不够,总之就是没那么多工作需要加班去完成”


强制加班,至少公司看上去灯火通明的,很多时候是高管或老板要求的,有可能是一些对软件研发理解不足的老板他们需要安慰剂,也有可能是一些“政治原因”。这就不好揣测了,非心腹当然是没法知道,但此时尤为要注意做好管理工作,很多事情没法讲也讲不清楚,团队成员可能因此会对团队、公司失去信心。那么拥有健康的团队氛围,愿意帮助大家成长。规划有长期的团队目标,目标符合公司发展,符合团队成员成长需要,同时要具备一定的挑战性,具备这两点的团队这方面的问题会少很多。团队不可以长期处于磨洋工的状态,如果人心涣散,再聚极难。


研发团队怎么做人才培养:


有加班时间了才想到用这个时间帮助员工成长,之所以有这样的问题是不是平时做人才培养不到位,不够细致。比如项目空窗期的时候之前都是放任大家自由学习或“摸鱼“吗? 从团队管理者角度去看,大家自由学习不能算是好事,很有可能学完了就走了,毕竟你这里没啥挑战。


我的一些经验:


结合公司业务,比如toC 还是toB 去看公司下阶段的规模与增速,分析产研需要达到的能力,以此为基础去看行业内的标准与自己团队的落差,把落差放大一些 作为团队的长期目标,时间上至少是一个季度以上才够大。
这些目标的特点都是重要但不紧急,但具备一定的挑战性。既满足人才培养又能对应未来公司发展需要。


把这些目标作为OKR,分担到各个小组,各个小组再拆落实到个人,并至少最小以月为单位进行复核,协助他们分析解决碰到的阻力问题,同时很多一线同学向上管理做得不好,管理者需要时常主动了解情况,及时给予资源支持。


执行过程难免碰到阻力出现停滞,或速度不理想,那么配合KPI奖励或其他激励来提高成员的驱动力。
在完成的目标过程中,挑一些大里程碑收获拿出来做分享,做沉淀,结合业务做实际应用,大家也能感受到做这些事儿的实际意义,团队信任关系也会越牢固。


KPI设计权衡:


产研团队虽然很难量化指标,但是做一份大家都认可的KPI 是完全可行的。


员工自评+管理者补评只要达到双方的认可,保持公平,公开。


关于KPI第一是考虑清楚KPI是一个奖惩手段,奖惩要分开来看。不然他就单纯变成一把尺子,架在每个人都脖子上,为了KPI而KPI,黑魔法多了去了,比如池老师举过一个例子,为了提高pv,产品经理把一个页面拆成2个,得到2倍pv。


我个人倾向于把它的激励作用放大一些,惩罚效果降低。好的员工有荣辱心,反省能力强,反而是在出事时帮他一把作为激励效果很好。


展开挺复杂,我简单举2个例子表述我的理解:


开发侧好量化的软件交付质量:100分,那么出 bug,分测试阶段,线上阶段,等级从p1-p4 定级,定级之后要对应的扣分标准呢,主动或协助及时修复奖励多少分。这个比较典型,很好量化。


不好量化的沟通协作能力:


"能胜任跨部门沟通协调,高效协作 85~100分


能主导 1 V N 沟通协调 70~85分


能主导 1 V 1 沟通协调 60~70分


沟通能力一般 60分"


这可以用管理者+实例结合来做。员工自评写实例,在什么项目主导协作,这个大家都看得见,管理者再主观的做一些评价,稍微修正一下。


最后,以100为单位为每一项考核做好权重分配,假设考核一共分5项,软件交付质量,设置它的权重为25%,来控制它的影响幅度。


设置权重能很好的适应员工能力梯队,凸显他们各自的优点。


所有人都打完分了,不管是271,28的划分这对管理者都是个头疼的事情,这时候就需要花较多的时间综合考虑,绝对公平太难了,见仁见智了。


我见过的不好的例子就是:给A这个月打高分给B打低分,下个月给B打高分给A打低分,全年下来大家保个平安,上下串通一气,团队一片和谐。


一点题外话,我对设计流程,考核等工作内容有天然的兴趣,但不知道可以以此作为主要能力找工作吗?这会是什么样的工作呢?(我也去问下gpt)


GPT回复:


设计流程和考核等工作内容是非常重要的设计领域的核心能力,如果您对此有天然的兴趣,可以尝试在这方面深入学习和实践,成为一名设计项目经理、设计团队领导、设计流程咨询师等职业。


作为一名设计项目经理,您需要负责整个设计项目的流程管理、协调和监督,确保设计团队能够按时、按质完成设计任务。同时,您需要与客户和其他相关方保持良好的沟通,理解客户需求,并协调解决设计项目中的问题。


作为一名设计团队领导,您需要带领设计团队完成项目任务,并对团队成员进行管理和培训。您需要制定设计流程和考核标准,并监督团队成员按照标准完成任务。


作为一名设计流程咨询师,您需要帮助客户建立有效的设计流程,并优化设计流程中的各个环节,以提高设计效率和质量。您需要了解各种设计软件和工具,并根据客户需求推荐最佳的设计工具和流程。


需要注意的是,设计流程和考核等工作内容是设计领域的重要能力之一,但并不是唯一的能力。如果您想在设计领域发展,还需要具备良好的设计技能和创造力。


emm.....


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

技术主管是否需要什么段位的技术

今天来跟大家讨论一下技术主管需要什么样段位的技术? 首先我要说明的一点,技术主管前提一定是技术出身。对于那些完全不懂技术,但是又身兼技术主管或者总监的同学,我这里就不再赘述,毕竟这个已经超出我目前理解力的范围。比如阿里云的王坚博士,基本上不懂技术细节,但是依然...
继续阅读 »

今天来跟大家讨论一下技术主管需要什么样段位的技术?


首先我要说明的一点,技术主管前提一定是技术出身。对于那些完全不懂技术,但是又身兼技术主管或者总监的同学,我这里就不再赘述,毕竟这个已经超出我目前理解力的范围。比如阿里云的王坚博士,基本上不懂技术细节,但是依然是阿里云的CTO,一手缔造了阿里云。


那我们这里再详细讨论一下,作为一名技术主管,到底应该有什么样的一个技术的段位?或者换句话来说,你的主管的技术水平需要到达什么样的一个水位?


先说结论,作为一名技术主管,一定是整个团队的技术架构师。像其他的一些大家所讨论的条件我觉得都是次要的,比如说写代码的多少,对于技术深度的钻研多少,带的团队人数多少等等,最核心的是技术主管一定要把控整个团队整个业务技术发展的骨架。


为什么说掌控团队技术架构是最重要的?因为对于一个团队来说无非就两点,第一点就是业务价值,第二点就是技术价值。


对于业务价值来说,有各种各样的同学都可以去负责业务上面的一些导向和推进,比如说产品经理,比如说运营同学。技术主管可以在一定程度上去帮助业务成功,甚至是助力业务成功,但是一定要明白技术同学一定要有自己的主轴,就是你对于整个技术的把握。因为业务上的决策说到底技术主管是只能去影响而非去决策,否则就是你们整体业务同学太过拉胯,无法形成战术合力的目的。


对于一线开发同学来说,你只要完成一个接一个的技术项目即可。但是对于技术主管来说,你就要把握整体的技术发展脉络。要清晰的明白什么样的技术架构是和当前的业务匹配的,同时又具备未来业务发展的可扩展性。


那为什么不能把整个技术架构的设计交给某一个核心的骨干研发同学呢?


所以这里就要明白,对于名技术主管来说,未必一定要深刻的钻研技术本身,一定要把技术在业务上的价值发挥到最大。所以在一定程度上来说,可以让适当的同学参与或者主导整个技术架构的设计,但是作为主管必须要了解到所谓的技术投入的产出比是什么。但是如果不对技术架构有一个彻底的理解,如何能决定ROI?



也就是在技术方案的选型里面一定要有一个平衡,能够用最小的技术投入获取到最大的技术利益,而非深究于技术本身的实习方式。如果一名技术主管不了解技术的框架或者某一些主干流程,那么就根本谈不上怎么样去评估这投入的技术产出比。一旦一名技术主管无法衡量整个技术团队的投入产出比,那就意味着整个团队的管理都是在抓虾和浑水摸鱼的状态,这时候就看你团队同学是否自觉了。


出现了这种情况下的团队,可能换一头猪在主管的位置上,业务依然运行良好。如果在业务发展好的时候,可能一直能够顺利推动,你只要坐享其成就可以了,但是一旦到了要突破困难的时期,或者在业务走下行的时候,这个时候你技术上面的优势就一点就没有了。而且在这种情况下,如果你跳槽到其他公司,作为一名技术主管,对方的公司对你的要求也是非常高的,所以这个时候你如果都说不出来你的技术价值对于业务上面的贡献是什么那想当然,你可能大概率就凉凉了。


那问题又回到了什么样的水平才能到达架构师这个话题,可以出来另一篇文章来描述,但是整体上来说,架构的本质首先一定要明白,为的就是业务的增长。


其次,架构的设计其实就是建造一个软件体系的结构,使得具备清晰度,可维护性和可扩展性。另外要想做好架构,基本的基础知识也必不可少,比如说数据库选型、分布式缓存、分库分表、幂等、分布式锁、消息架构、异步架构等等。所以本身来说做好架构师本身难度就非常大,需要长期的积累,实现厚积而薄发。如何成为一名优秀的架构师可以看我的公众号的其他文章,这里就不再详细的介绍了。



第二点是技术主管需要对于技术细节有敏感度。很多人在问一名主管到底应该具备什么样的综合能力,能不能用一种更加形象的方式来概括,我认为就有一句话就可以概括了。技术主管应该是向战略轰炸机在平常的时候一直遨游在大气的最上层能够掌控整个全局,当到了必须要战斗的时候,可以快速的补充下去,定点打击。


我参加过一次TL培训课程,讲师是阿里云智能交付技术部总经理张瑞,他说他最喜欢的一句管理概括,就是“心有猛虎,细嗅蔷薇”,也就是技术主管在平常的时候会关注于更大的宏观战略或策略,也就是注重思考全局,但是在关键的时候一定要关注和落地实际的细节。


换句更加通俗的话来说,就是管理要像战略轰炸机,平常的时候飞在万丈高空巡视,当发生了战斗的时候,立即能够实现定点轰炸。



所以如果说架构上面的设计就是对于整个团队业务和技术骨架的把握,那么对于细节的敏感度就是对于解决问题的落地能力。


那怎么样能够保证你自己有一个技术细节的敏感度?


我认为必要的代码量是需要的,也就是说对于一个主管来说,不必要写太多低代码,但一定要保证一定的代码量,让自己能够最好的,最快的,最贴近实际的理解实际的业务项目。自己写一些代码,其实好处非常多,一方面能够去巩固和加深自己对技术的理解,另外一方面也能够通过代码去更加理解业务。


当然贴近技术的方式有很多种,不一定要全部靠写代码来完成,比如说做code review的方式来完成,做技术方案的评审来完成,这都是可以的。对我来说,我就会强迫自己在每一个迭代会写上一个需求,需求会涉及到各方各面的业务点。有前端的,有后端的,也有数据库设计的。


自己亲自参与写代码或者code review,会让自己更加贴近同学,能够感知到同学的痛点,而不至于只是在空谈说教。


总结


所以对于一个技术主管来说,我认为首要的就是具备架构设计的能力,其次就是要有代码细节的敏感度,对全局和对细节都要有很强大的把控能力。


当然再总结一下,这一套理论只是适用于基础的管理者,而非高层的CTO等,毕竟不同的层级要求的能力和影响力都是不一样的。


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

兄弟,王者荣耀的段位排行榜是通过Redis实现的?

在王者荣耀中,我们会打排位赛,而且大家最关注的往往都是你的段位,还有在好友中的排名。 作为程序员的你,思考过吗,这个段位排行榜是怎么实现的?了解它的实现原理,会不会对上分有所帮助? 看看我的排名,你就知道了,答案是否定的,哈哈。 一、排行榜设计方案 从技...
继续阅读 »

在王者荣耀中,我们会打排位赛,而且大家最关注的往往都是你的段位,还有在好友中的排名。


作为程序员的你,思考过吗,这个段位排行榜是怎么实现的?了解它的实现原理,会不会对上分有所帮助?



看看我的排名,你就知道了,答案是否定的,哈哈。




一、排行榜设计方案


从技术角度而言,我们可以根据排行榜的类型来选择不同技术方案来进行排行榜设计。


1、数据库直接排序


在低数据量场景中,用数据库直接排序做排行榜的,有很多。


举个栗子,比如要做一个程序员薪资排行榜,看看哪个城市的程序员最有钱。


根据某招聘网站的数据,2023年中国国内程序员的平均月薪为1.2万元,其中最高的是北京,达到了2.1万元,最低的是西安,只有0.7万元。


以下是几个主要城市的程序员平均月薪排行榜:



  1. 北京:2.1万元

  2. 上海:1.9万元

  3. 深圳:1.8万元

  4. 杭州:1.6万元

  5. 广州:1.5万元

  6. 成都:1.3万元

  7. 南京:1.2万元

  8. 武汉:1.1万元

  9. 西安:0.7万元


从这个榜单中可以看出,我拖了大家的后腿,抱歉了。



这个就可以用数据库来做,一共也没有多少个城市,来个百大,撑死了。


对于这种量级的数据,加好索引,用好top,都不会超过100ms,在请求量小、数据量小的情况下,用数据库做排行榜是完全没有问题的。


2、王者荣耀好友排行


这类榜单是根据自己好友数据来进行排行的,这类榜单不用将每位好友的数据都存储在数据库中,而是通过获取自己的好友列表,获取好友的实时分数,在客户端本地进行本地排序,展现出王者荣耀好友排行榜,因为向数据库拉取数据是需要时间的,比如一分钟拉取一次,因为并非实时拉取,这类榜单对数据库的压力还是较小的。



下面探索一下在Java中使用Redis实现高性能的排行榜是如何实现的?



二、Redis实现计数器


1、什么是计数器功能?


计数器是一种常见的功能,用于记录某种事件的发生次数。在应用中,计数器可以用来跟踪用户行为、统计点击次数、浏览次数等。


例如,您可以使用计数器来记录一篇文章被阅读的次数,或者统计某个产品被购买的次数。通过跟踪计数,您可以了解数据的变化趋势,从而做出更明智的决策。


2、Redis实现计数器的原理


Redis是一款高性能的内存数据库,提供了丰富的数据结构和命令,非常适合实现计数器功能。在Redis中,我们可以使用字符串数据类型以及相关的命令来实现计数器。


(1)使用INCR命令实现计数器


Redis的INCR命令是一个原子操作,用于将存储在键中的数字递增1。如果键不存在,将会创建并初始化为0,然后再执行递增操作。这使得我们可以轻松地实现计数器功能。


让我们通过Java代码来演示如何使用Redis的INCR命令实现计数器:


import redis.clients.jedis.Jedis;

public class CounterExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String articleId = "article:123";
String viewsKey = "views:" + articleId;

// 使用INCR命令递增计数
long views = jedis.incr(viewsKey);

System.out.println("Article views: " + views);

jedis.close();
}
}

在上面的代码中,我们使用了Jedis客户端库来连接Redis服务器,并使用INCR命令递增一个存储在views:article:123键中的计数器。每次执行该代码,计数器的值都会递增,并且我们可以轻松地获取到文章的浏览次数。


(2)使用INCRBY命令实现计数器


除了单次递增1,我们还可以使用INCRBY命令一次性增加指定的数量。这对于一些需要一次性增加较大数量的场景非常有用。


让我们继续使用上面的例子,但这次我们使用INCRBY命令来增加浏览次数:


import redis.clients.jedis.Jedis;

public class CounterExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String articleId = "article:123";
String viewsKey = "views:" + articleId;

// 使用INCRBY命令递增计数
long views = jedis.incrBy(viewsKey, 10); // 一次增加10

System.out.println("Article views: " + views);

jedis.close();
}
}

在上述代码中,我们使用了INCRBY命令将文章浏览次数一次性增加了10。这在统计需要一次性增加较多计数的场景中非常有用。


通过使用Redis的INCRINCRBY命令,我们可以轻松实现高性能的计数器功能。这些命令的原子性操作保证了计数的准确性,而且非常适用于需要频繁更新计数的场景。


三、通过Redis实现“王者荣耀”排行榜?


王者荣耀的排行榜是不是用Redis做的,我不得而知,但,我的项目中,排行榜确实是用Redis做的,这是实打实的。



看见了吗?掌握算法的男人,到哪里都是无敌的。




1、什么是排行榜功能?


排行榜是一种常见的功能,用于记录某种项目的排名情况,通常按照某种规则对项目进行排序。在社交媒体、游戏、电商等领域,排行榜功能广泛应用,可以增强用户的参与度和竞争性。例如,社交媒体平台可以通过排行榜展示最活跃的用户,游戏中可以展示玩家的分数排名等。


2、Redis实现排行榜的原理


在Redis中,我们可以使用有序集合(Sorted Set)数据结构来实现高效的排行榜功能。有序集合是一种键值对的集合,每个成员都与一个分数相关联,Redis会根据成员的分数进行排序。这使得我们能够轻松地实现排行榜功能。


(1)使用ZADD命令添加成员和分数


Redis的ZADD命令用于向有序集合中添加成员和对应的分数。如果成员已存在,可以更新其分数。让我们通过Java代码演示如何使用ZADD命令来添加成员和分数到排行榜:


import redis.clients.jedis.Jedis;

public class LeaderboardExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String leaderboardKey = "leaderboard";
String player1 = "PlayerA";
String player2 = "PlayerB";

// 使用ZADD命令添加成员和分数
jedis.zadd(leaderboardKey, 1000, player1);
jedis.zadd(leaderboardKey, 800, player2);

jedis.close();
}
}

在上述代码中,我们使用ZADD命令将PlayerAPlayerB作为成员添加到leaderboard有序集合中,并分别赋予分数。这样,我们就在排行榜中创建了两名玩家的记录。


(2)使用ZINCRBY命令更新成员分数


除了添加成员,我们还可以使用ZINCRBY命令更新已有成员的分数。这在实时更新排行榜中的分数非常有用。


让我们继续使用上面的例子,但这次我们将使用ZINCRBY命令来增加玩家的分数:


import redis.clients.jedis.Jedis;

public class LeaderboardExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String leaderboardKey = "leaderboard";
String player1 = "PlayerA";
String player2 = "PlayerB";

// 使用ZINCRBY命令更新成员分数
jedis.zincrby(leaderboardKey, 200, player1); // 增加200分

jedis.close();
}
}

在上述代码中,我们使用了ZINCRBY命令将PlayerA的分数增加了200分。这种方式可以用于记录玩家的得分、积分等变化,从而实时更新排行榜数据。


通过使用Redis的有序集合以及ZADDZINCRBY等命令,我们可以轻松实现高性能的排行榜功能。这些命令的原子性操作保证了排行的准确性和一致性,非常适用于需要频繁更新排行榜的场景。



我的最强百里,12-5-6,这都能输?肯定是哪里出问题了,服务器性能?




四、计数器与排行榜的性能优化


在本节中,我们将重点讨论如何在高并发场景下优化计数器和排行榜功能的性能。通过合理的策略和技巧,我们可以确保系统在处理大量数据和用户请求时依然保持高性能。


1、如何优化计数器的性能?


(1)使用Redis事务


在高并发场景下,多个用户可能同时对同一个计数器进行操作,这可能引发并发冲突。为了避免这种情况,可以使用Redis的事务来确保原子性操作。事务将一组命令包装在一个原子性的操作中,保证这些命令要么全部执行成功,要么全部不执行。


下面是一个示例,演示如何使用Redis事务进行计数器操作:


import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisException;

public class CounterOptimizationExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String counterKey = "view_count";
try {
// 开始事务
Transaction tx = jedis.multi();
// 对计数器执行加1操作
tx.incr(counterKey);
// 执行事务
tx.exec();
} catch (JedisException e) {
// 处理事务异常
e.printStackTrace();
} finally {
jedis.close();
}
}
}

在上述代码中,我们使用了Jedis客户端库,通过MULTI命令开启一个事务,然后在事务中执行INCR命令来增加计数器的值。最后,使用EXEC命令执行事务。如果在事务执行期间出现错误,我们可以通过捕获JedisException来处理异常。


(2)使用分布式锁


另一种优化计数器性能的方法是使用分布式锁。分布式锁可以确保在同一时刻只有一个线程能够对计数器进行操作,避免了并发冲突。这种机制可以保证计数器的更新是串行化的,从而避免了竞争条件。


以下是一个使用Redisson框架实现分布式锁的示例:


import org.redisson.Redisson;
import org.redisson.api.RLock;

public class CounterOptimizationWithLockExample {

public static void main(String[] args) {
Redisson redisson = Redisson.create();
RLock lock = redisson.getLock("counter_lock");

try {
lock.lock(); // 获取锁
// 执行计数器操作
} finally {
lock.unlock(); // 释放锁
redisson.shutdown();
}
}
}

在上述代码中,我们使用了Redisson框架来创建一个分布式锁。通过调用lock.lock()获取锁,然后执行计数器操作,最后通过lock.unlock()释放锁。这样可以保证在同一时间只有一个线程能够执行计数器操作。



2、如何优化排行榜的性能?


(1)分页查询


在排行榜中,通常会有大量的数据,如果一次性查询所有数据,可能会影响性能。为了解决这个问题,可以使用分页查询。将排行榜数据分成多个页,每次查询一小部分数据,以减轻数据库的负担。


以下是一个分页查询排行榜的示例:


import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;
import java.util.Set;

public class LeaderboardPaginationExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String leaderboardKey = "leaderboard";
int pageSize = 10; // 每页显示的数量
int pageIndex = 1; // 页码

// 获取指定页的排行榜数据
Set<Tuple> leaderboardPage = jedis.zrevrangeWithScores(leaderboardKey, (pageIndex - 1) * pageSize, pageIndex * pageSize - 1);

for (Tuple tuple : leaderboardPage) {
String member = tuple.getElement();
double score = tuple.getScore();
System.out.println("Member: " + member + ", Score: " + score);
}

jedis.close();
}
}

在上述代码中,我们使用zrevrangeWithScores命令来获取指定页的排行榜数据。通过计算起始索引和结束索引,我们可以实现分页查询功能。


(2)使用缓存


为了进一步提高排行榜的查询性能,可以将排行榜数据缓存起来,减少对数据库的访问。例如,可以使用Redis缓存最近的排行榜数据,定期更新缓存以保持数据的新鲜性。


以下是一个缓存排行榜数据的示例:


import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;
import java.util.Set;

public class LeaderboardCachingExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String leaderboardKey = "leaderboard";
String cacheKey = "cached_leaderboard";
int cacheExpiration = 300; // 缓存过期时间,单位:秒

// 尝试从缓存中获取排行榜数据
Set<Tuple> cachedLeaderboard = jedis.zrevrangeWithScores(cacheKey, 0, -1);

if (cachedLeaderboard.isEmpty()) {
// 如果缓存为空,从数据库获取数据并更新缓存
Set<Tuple> leaderboardData = jedis.zrevrangeWithScores(leaderboardKey, 0, -1);
jedis.zadd(cacheKey, leaderboardData);
jedis.expire(cacheKey, cacheExpiration);
cachedLeaderboard = leaderboardData;
}

for

(Tuple tuple : cachedLeaderboard) {
String member = tuple.getElement();
double score = tuple.getScore();
System.out.println("Member: " + member + ", Score: " + score);
}

jedis.close();
}
}

在上述代码中,我们首先尝试从缓存中获取排行榜数据。如果缓存为空,我们从数据库获取数据,并将数据存入缓存。使用expire命令来设置缓存的过期时间,以保持数据的新鲜性。


五、实际应用案例


在本节中,我们将通过两个实际的案例,展示如何使用Redis的计数器和排行榜功能来构建社交媒体点赞系统和游戏玩家排行榜系统。这些案例将帮助您更好地理解如何将Redis的功能应用于实际场景中。


1、社交媒体点赞系统案例


(1)问题背景


假设我们要构建一个社交媒体平台,用户可以在文章、照片等内容上点赞。我们希望能够统计每个内容的点赞数量,并实时显示最受欢迎的内容。


(2)系统架构



  • 每个内容的点赞数可以使用Redis的计数器功能进行维护。

  • 我们可以使用有序集合(Sorted Set)来维护内容的排名信息,将内容的点赞数作为分数。


(3)数据模型



  • 每个内容都有一个唯一的标识,如文章ID或照片ID。

  • 使用一个计数器来记录每个内容的点赞数。

  • 使用一个有序集合来记录内容的排名,以及与内容标识关联的分数。


(4)Redis操作步骤



  1. 用户点赞时,使用Redis的INCR命令增加对应内容的点赞数。

  2. 使用ZADD命令将内容的标识和点赞数作为分数添加到有序集合中。


Java代码示例


import redis.clients.jedis.Jedis;

public class SocialMediaLikeSystem {

private Jedis jedis;

public SocialMediaLikeSystem() {
jedis = new Jedis("localhost", 6379);
}

public void likeContent(String contentId) {
// 增加点赞数
jedis.incr("likes:" + contentId);

// 更新排名信息
jedis.zincrby("rankings", 1, contentId);
}

public long getLikes(String contentId) {
return Long.parseLong(jedis.get("likes:" + contentId));
}

public void showRankings() {
// 显示排名信息
System.out.println("Top content rankings:");
jedis.zrevrangeWithScores("rankings", 0, 4)
.forEach(tuple -> System.out.println(tuple.getElement() + ": " + tuple.getScore()));
}

public static void main(String[] args) {
SocialMediaLikeSystem system = new SocialMediaLikeSystem();
system.likeContent("post123");
system.likeContent("post456");
system.likeContent("post123");

System.out.println("Likes for post123: " + system.getLikes("post123"));
System.out.println("Likes for post456: " + system.getLikes("post456"));

system.showRankings();
}
}

在上述代码中,我们创建了一个名为SocialMediaLikeSystem的类来模拟社交媒体点赞系统。我们使用了Jedis客户端库来连接到Redis服务器,并实现了点赞、获取点赞数和展示排名的功能。每当用户点赞时,我们会使用INCR命令递增点赞数,并使用ZINCRBY命令更新有序集合中的排名信息。通过调用zrevrangeWithScores命令,我们可以获取到点赞数排名前几的内容。



2、游戏玩家排行榜案例


(1)问题背景


在一个多人在线游戏中,我们希望能够实时追踪和显示玩家的排行榜,以鼓励玩家参与并提升游戏的竞争性。


(2)系统架构



  • 每个玩家的得分可以使用Redis的计数器功能进行维护。

  • 我们可以使用有序集合来维护玩家的排名,将玩家的得分作为分数。


(3)数据模型



  • 每个玩家都有一个唯一的ID。

  • 使用一个计数器来记录每个玩家的得分。

  • 使用一个有序集合来记录玩家的排名,以及与玩家ID关联的得分。


(4)Redis操作步骤



  1. 玩家完成游戏时,使用Redis的ZINCRBY命令增加玩家的得分。

  2. 使用ZREVRANK命令获取玩家的排名。


(5)Java代码示例


import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;

import java.util.Set;

public class GameLeaderboard {

private Jedis jedis;

public GameLeaderboard() {
jedis = new Jedis("localhost", 6379);
}

public void updateScore(String playerId, double score) {
jedis.zincrby("leaderboard", score, playerId);
}

public Long getPlayerRank(String playerId) {
return jedis.zrevrank("leaderboard", playerId);
}

public Set<Tuple> getTopPlayers(int count) {
return jedis.zrevrangeWithScores("leaderboard", 0, count - 1);
}

public static void main(String[] args) {
GameLeaderboard leaderboard = new GameLeaderboard();
leaderboard.updateScore("player123", 1500);
leaderboard.updateScore("player456", 1800);
leaderboard.updateScore("player789", 1600);

Long rank = leaderboard.getPlayerRank("player456");
System.out.println("Rank of player456: " + (rank != null ? rank + 1 : "Not ranked"));

Set<Tuple> topPlayers = leaderboard.getTopPlayers(3);
System.out.println("Top players:");
topPlayers.forEach(tuple -> System.out.println(tuple.getElement() + ": " + tuple.getScore()));
}
}

在上述代码中,我们创建了一个名为GameLeaderboard的类来模拟游戏玩家排行榜系统。我们同样使用Jedis客户端库来连接到Redis服务器,并实现了更新玩家得分、获取玩家排名和获取排名前几名玩家的功能。使用zincrby命令可以更新玩家的得分,而zrevrank命令则用于


获取玩家的排名,注意排名从0开始计数。通过调用zrevrangeWithScores命令,我们可以获取到排名前几名玩家以及他们的得分。


六、总结与最佳实践


在本篇博客中,我们深入探讨了如何使用Redis构建高性能的计数器和排行榜功能。通过实际案例和详细的Java代码示例,我们了解了如何在实际应用中应用这些功能,提升系统性能和用户体验。让我们在这一节总结Redis在计数器和排行榜功能中的价值,并提供一些最佳实践指南。


1、Redis在计数器和排行榜中的价值


通过使用Redis的计数器和排行榜功能,我们可以实现以下价值:




  • 实时性和高性能:Redis的内存存储和优化的数据结构使得计数器和排行榜功能能够以极高的性能实现。这对于需要实时更新和查询数据的场景非常重要。




  • 用户参与度提升:在社交媒体和游戏等应用中,计数器和排行榜功能可以激励用户参与。通过显示点赞数量或排行榜,用户感受到了更强的互动性和竞争性,从而增加了用户参与度。




  • 数据统计和分析:通过统计计数和排行数据,我们可以获得有价值的数据洞察。这些数据可以用于分析用户行为、优化内容推荐等,从而指导业务决策。




2、最佳实践指南


以下是一些使用Redis构建计数器和排行榜功能的最佳实践指南:




  • 合适的数据结构选择:根据实际需求,选择合适的数据结构。计数器可以使用简单的String类型,而排行榜可以使用有序集合(Sorted Set)来存储数据。




  • 保证数据准确性:在高并发环境下,使用Redis的事务、管道和分布式锁来保证计数器和排行榜的数据准确性。避免并发写入导致的竞争条件。




  • 定期数据清理:定期清理不再需要的计数器和排行数据,以减小数据量和提高查询效率。可以使用ZREMRANGEBYRANK命令来移除排行榜中的过期数据。




  • 适度的缓存:对于排行榜数据,可以考虑添加适度的缓存以提高查询效率。但要注意平衡缓存的更新和数据的一致性。




通过遵循这些最佳实践,您可以更好地应用Redis的计数器和排行榜功能,为您的应

作者:哪吒编程
来源:juejin.cn/post/7271908000414351400
用程序带来更好的性能和用户体验。

收起阅读 »

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

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

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


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


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



神奇的天涯


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





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



历史人文类


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

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

人生箴言类


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

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

房产金融


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

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

故事连载小说类


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

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

玄学类


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

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

天涯的败落


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

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

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

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


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

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

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

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


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


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


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


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


插件cppcheck的安装


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


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


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


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


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

sudo apt-get install cppcheck

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

sudo yum install cppcheck

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

brew install cppcheck

CLion插件配置cppcheck路径


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


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



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


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


功能实测


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

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

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

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



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



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


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


系统话学习博文推荐


音视频入门基础

C++进阶

NDK学习入门

安卓camera应用开发

ffmpeg系列

Opengl入门进阶

webRTC


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


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

你知道什么是SaaS吗?

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

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


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


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

  2. SaaS系统的两大特征

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

  4. B2B2C

  5. SaaS系统的分类

  6. 如何SaaS化

  7. SaaS产品的核心组件

  8. SaaS多租户


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


1.1 PaaS


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


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


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


1.2 IaaS


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


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


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


1.3 SaaS


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


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


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


二、SaaS系统的两大特征



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

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


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


3.1 SaaS服务


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


3.2 传统软件


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


3.3 互联网应用供应商


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


四、B2B2C


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


五、SaaS系统的分类


5.1 业务型SaaS


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


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


5.2 效率型SaaS


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


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


5.3 混合型SaaS


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


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


六、如何SaaS化



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

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

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

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

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

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

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


七、SaaS产品的核心组件



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

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

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

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

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

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


八、SaaS多租户


8.1 多租户核心概念



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

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

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

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

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

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

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

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


8.2 三大模式


8.2.1 竖井隔离模式


image.png



  • 优势:



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

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

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



  • 劣势:



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

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

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

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


8.2.2 共享模式


image.png



  • 优势:



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

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



  • 劣势:



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

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


8.2.3 分域隔离模式


image.png


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



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

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

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

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


8.4 多租户系统应用架构图


image.png

收起阅读 »

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

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

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


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


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


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


传统I/O的读写过程


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


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


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


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


简化版读操作流程


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



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

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

详细的读写操作流程


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



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


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


应用程序


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


系统内核


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


外部存储


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


内核态

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

用户态


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

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

模式切换


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


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



内核缓冲区


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


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


用户缓冲区


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


磁盘缓冲区


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


PageCache

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

再说数据读写操作流程


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


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

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

瓶颈在哪里


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


那是什么地方耗时了呢?


数据拷贝


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


用户态和内核态的切换


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


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


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


什么是零拷贝


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


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


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


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


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

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

实现零拷贝方案


直接内存访问(DMA)


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




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


sendfile


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


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



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

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


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


共享内存


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



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


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


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


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


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


Java 实现零拷贝的方式


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


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


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


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


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

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

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



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



FileChannel.transferTo() 和 transferFrom()


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


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


transferTo()


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

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

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

只耗时0.536秒,快了一倍。



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



transferFrom()


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

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

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

执行时间:



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



Memory-Mapped Files


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


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

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

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

执行时间:



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



推荐阅读


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


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


RPC框架的核心到底是什么


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

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

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

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


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


从 Copilot 说起


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

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


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





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



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



开发工具 2.0 与现状


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


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




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

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

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

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

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


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


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




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


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


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




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




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


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


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


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


开发工具 2.0 的共同点


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


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



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




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




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




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




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



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

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

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


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



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



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


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


对开发者的影响


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


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


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




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


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




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


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


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

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

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


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



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



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


AI 正在吞噬软件


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


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


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



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



所以,你准备好了吗?


参考

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

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

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

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

写在前面


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


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


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


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


一、CodeGeex简介


① 来自官方的介绍



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



翻译过来大概是



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



GitHub地址


github.com/THUDM/CodeG…


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




③ 下载量

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

二、插件安装


① vscode




②idea


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




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


三、帮你写代码

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

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








四、帮你添加注释



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


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



操作方法:

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

② 鼠标右键选择Add Comment

③ 选择中文或者英文






这是没加注释的代码

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

这是CodeGeex帮加上的注释

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

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


五、帮你翻译成其他语言



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



操作方法:

① 选中需要转换的代码

② 鼠标右键选择Translation mode

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

④ 选择转换按钮进行转换






六 小结


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


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


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




本期内容到此就结束了


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


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

Java仿抽奖系统

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

Java仿抽奖系统


前言


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


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


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


代码实现


首先我们定义一些常量


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

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

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

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

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

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

       return prize;
  }
}

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


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


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

之后我们进行这样设计


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

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

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

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

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

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

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

   int prizeIndex = 0;
   double cumulativeWeight = 0;

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

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

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

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


image-20230821164510692


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


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


image-20230821164625143


就会不断的去输。



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

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


收起阅读 »

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

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

前言


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


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

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

1. 抽取公用方法


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


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


public class TianLuoExample {

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

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

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

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


public class TianLuoExample {

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

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

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

2. 抽工具类


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


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


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

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

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

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


public class PasswordServiceImpl implements PasswordService{

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

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

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


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

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

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

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

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

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

3. 反射


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


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

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


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

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

4.泛型


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


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

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

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


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

5. 继承与多态


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


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


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

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

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

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

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

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

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

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


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


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

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

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

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

public class DiscountOrder extends Order {
private double discountPercentage;

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

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

6.使用设计模式


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



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

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

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


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



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



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


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

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

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

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

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

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

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

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

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

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

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


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


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

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

abstract void brew();

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

abstract void addCondiments();
}

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

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

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

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

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


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


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


业务场景:


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


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

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

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


@Aspect
@Component
public class PermissionAspect {

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

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

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

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

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


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


业务场景:



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



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


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

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

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


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


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


最后


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

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

收起阅读 »

消息太大,kafka受不了

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

前言


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


业务背景


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


问题出现


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


处理问题


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



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


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


配置类


yml文件



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


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


扩展


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


总结


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


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

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

收起阅读 »

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

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

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


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


实际面临的问题


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




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




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




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




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


为什么是 Rainbond?


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


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


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


add_cluster


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


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


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


source_code_build


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


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


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


component_assembly


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


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


app_manage


如何使用?


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


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

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

收起阅读 »