开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

TiDB 51 发版,打造更流畅的企业级数据库体验

发表于 2021-07-02

自 TiDB 5.0 发布以来,陆续在金融、互联网 & 新经济、物流等行业用户的生产环境得到应用,收获不少用户的积极评价:

TiDB 服务 58 金融、安居客等数仓报表的复杂读取与关联查询,在多表关联查询中,相比 4.0 版本性能最高提升达 90%;

经过网易互娱场景实测,与 4.0 相比 TiDB 5.0 整体性能表现更加稳定,没有出现明显的抖动;

TiDB 5.0 在汽车之家大数据 join 与聚合场景的应用中,MPP 体现出明显的优势,与 MySQL 相比总体效能提升 20 - 50 倍。

“用户的反馈激励我们不断前行,我们的使命是持续提升开发者和 DBA 的体验,让用户用得省心,用得顺手。” PingCAP 联合创始人兼 CTO 黄东旭说,“ TiDB 每一个版本的发布都立足于解决 DBA 的痛点。真实场景就是最好的架构师,从 5.0 版本开始 TiDB 缩短了发版周期,采用了更灵活、更敏捷的火车发版模型,每一个用户真实场景需求的输入,在两个月周期内就有可能成为下一个版本交付的功能。”

得益于大量用户真实应用场景的快速反馈,TiDB 5.1 提速发版,进一步打造更流畅的企业级数据库体验。TiDB 5.1 拥有更加稳定的响应延迟表现,更优的 MPP 性能与稳定性,更便捷的可运维性,开发者和 DBA 可以轻松地基于 TiDB 5.1 构建任意规模的关键业务应用。

TiDB 5.1 功能亮点和用户价值

  • 支持 ANSI SQL 99 标准的 Common Table Expression,用户可以写出更加简洁、更易维护的 SQL 代码,轻松应对复杂的业务逻辑,提高开发效率。
  • 进一步提升 MPP 性能和稳定性,帮助用户更快做出实时决策。5.1 通过支持 MPP 模式下的分区表以及新增的多个函数表达式和算子优化,实时分析性能提升一个数量级以上;通过增强的内存管理和负载平衡机制,让分析查询变得更快、更稳。
  • 在突发的大流量写入、集群扩缩容以及在线数据导入和备份等场景下,5.1 版本优化了数据库的长尾查询延迟的稳定性,应对不同的工作负载,延迟能够降低 20% - 70% 。尤其对于金融行业延迟敏感类型的关键业务应用,大幅提升了在高压力负载下的查询稳定性。
  • 支持列类型变更,与 MySQL 兼容度更高。5.1 新增 Stale Read 模式,在读写分离场景中通过打散读热点大幅提升读吞吐能力;引入新的系统表,实现在高并发事务场景中快速定位锁冲突;改进统计信息分析引擎,提升优化器选择索引的精准度,保障业务查询的效率和稳定性。
  • 面向大集群提供更加友好的运维体验,进一步降低 DBA 工作负荷。5.1 版本集群扩缩容和数据迁移速度提升 40%,改善了大规模集群运维的可靠性,降低大规模集群整体备份和恢复的耗时,通过优化 CDC 数据链路临时中断后的自动恢复机制,进一步提升数据同步链路的可靠性。

Common Table Expression 让 SQL 化繁为简

在金融交易类场景,由于业务的客观复杂性,有时候会写出长达 2000 行的单条 SQL 语句,其中包含大量的聚合和多层子查询嵌套,维护此类 SQL 堪称开发人员的噩梦。5.1 版本支持 ANSI SQL 99 标准的Common Table Expression(CTE)及递归的写法,极大提升开发人员和 DBA 编写复杂业务逻辑 SQL 的效率,增强代码的可维护性。

HTAP 实时分析能力再升级

进一步提升 MPP 的性能和稳定性

5.1 版本进一步增强TiFlash MPP 计算引擎的综合能力,帮助用户提升业务决策速度:

  • MPP 支持分区表,结合业务逻辑可优化海量数据分析查询所消耗的资源,提升查询速度;
  • 新增多个常用 SQL 函数支持,并优化算子使得查询能够更充分利用 MPP 来加速;
  • 提供便利的强制 MPP 模式开关,用户可自主决定是否开启 MPP 模式;
  • 通过优化集群负荷的分散与平衡机制,消除热点,提升系统“综合”承载能力;
  • 修复引擎内存使用问题,提供更加平稳流畅的使用体验。

提升高压力负载下查询分析的稳定性

在金融类业务场景下,技术人员每天会对数据进行高压力的跑批计算,生成最新的市场和营销分析报告,以辅助商业决策。跑批流程对连续性要求极高,无法容忍中间过程出错。针对该场景,5.1 版本优化了 TiDB 的请求重试机制和 TiKV 的请求处理机制,显著降低了在高负载下由于 TiFlash 同步数据不及时导致的 Region Unavailable 出错概率。

无缝集成 TiSpark

TiSpark 5.1 版本实现了对含有聚簇索引表的读写支持,不带来任何额外的性能开销,对用户完全透明,用户可以立刻迁移到新版 TiSpark 来体验与 TiDB 5.1 的无缝集成。

降低读写延迟抖动

在延迟敏感的应用场景下,当线上产生突发写流量、操作 TiDB 扩缩容、后台执行统计任务,以及在线数据导入和备份时,可能造成数据库的 P99 和 P999 百分位的延迟抖动,对长尾查询产生一定影响TiDB 5.1 加强了对磁盘读写链路的管理,限制后台任务对磁盘资源的使用,大幅降低上述场景对线上业务的干扰,改善读写链路的效率和稳定性。在 AWS EC2 r5b.4xlarge 实例挂载 EBS gp3 盘的环境下,通过 TPC-C 基准测试(10k WH)的实测结果:

  • 操作集群从 6 台 TiKV 缩到 3 台,P99 响应时间降低 20%,P999 响应时间降低 15%;
  • 执行在线导入 200GB 数据,P99 响应时间降低 71%,P999 响应时间降低 70%。

增强业务开发灵活性

支持列类型变更

在典型的 TiDB 应用场景中,经常借助 binlog 将多个 MySQL 上游数据汇聚到一个 TiDB 集群。原先 TiDB 不支持变更列类型的操作,如果上游 MySQL 修改表的字段类型会导致与 TiDB 数据同步的中断。5.1 版本新增对修改列类型 DDL 语句的支持,彻底解决上述问题并进一步提升MySQL 兼容性。

Stale Read

Stale Read 适用于读多写少并且能够容忍读到旧数据的场景。例如 Twitter 用户发出一条消息后,系统会产生成千上万甚至上亿次读取,并且新发出的消息在一定时间后被读到是可以容忍的。该场景给数据库带来相当大的读并发压力,可能会产生读热点,导致节点的负载分布不均,整体吞吐成为瓶颈。借助 Stale Read,用户可以指定一个过去的时间点从任意一个数据副本读取数据(不必从 leader 读取),从而显著分散节点的压力负载,使得整体读吞吐能力提升近一倍。

1
2
3
sql复制代码/* 例如:可以通过设置当前事务为查询 5 秒之前的数据状态来开启 Stale Read */
> SET TRANSACTION READ ONLY AS OF TIMESTAMP NOW() - INTERVAL 5 SECOND;
> SELECT * FROM T;

快速定位锁冲突 (实验特性)

业务开发需要很谨慎地处理数据库并发事务,一旦发生锁表会给线上业务带来巨大影响,而 DBA 需要快速定位锁表原因以保证业务能够恢复正常。TiDB 5.1 中新增 Lock View 系统表视图,可以快速定位到引起锁表的事务和相关 SQL 语句,从而提高锁冲突问题的处理效率。下面一个小例子展示如何使用 Lock View 快速定位发生锁表的事务和 SQL 语句。

更快更准的统计信息分析

随着业务数据持续不断的变更,表的统计信息也会变得陈旧,进而导致优化器执行计划准确度降低,使得查询变慢。DBA 通过执行 ANALYZE 操作,对表的统计信息进行重建。TiDB 5.1 对 ANALYZE 采样算法的性能进行了优化,生成统计信息的平均时间缩减为三分之一,同时新增一项新的统计数据类型,让优化器选择索引更加准确。

提升大集群运维和数据传输的可靠性

超多数量表的备份优化

优化超多数量表的备份,在 50k 张表的量级下,TiDB 集群全量备份时间降低到之前的30~40%。此外, 5.1 版本优化了备份模块的元信息文件组织形式(简称v2),启动 BR 时可以通过指定参数 “–backupmeta-version=2” 来启用 v2,从而减少单次写入量来降低内存消耗,有效避免低规格内存(≤8GB)环境下的异常退出。

提升大规模集群运维可靠性

TiDB 集群规模越大对生产集群扩缩容、硬件升级以及节点搬迁等日常运维操作的耗时就越久。TiDB 5.1 显著提升了扩缩容时数据迁移的性能,以下是两组测试结果:

  • 100 个节点规模下,完成集群所有数据跨数据中心迁移的耗时降低 20%;
  • 增加新节点或对某节点的数据进行迁移,耗时缩短约 40%。

优化内存使用

内存溢出(Out Of Memory)一直是困扰数据库行业的典型问题,5.1 版本针对 TiDB 的内存使用进行了一系列优化,从而降低 OOM 风险:

  • 无论数据量大小,窗口函数 row_number 将只占用固定大小内存;
  • 优化分区表的读取,占用更少内存;
  • 为存储层加入可配置的内存限制,当限制触发时,系统将释放部分缓存以降低内存占用;
  • TiFlash 写入的内存占用比上一版本降低 80%。

提升 CDC 同步链路可靠性

TiCDC 5.1 在无需人工干预的情况下提供同步链路的可靠性:当发生环境扰动或硬件故障时,TiCDC 可以保证同步持续进行;即使发生同步中断,TiCDC 也会根据实际情况自动进行重试。

最后,特别感谢小米、奇虎 360、知乎、爱奇艺、理想汽车、新浪、虎牙、小电、跨越速运、亿玛科技等公司和社区开发者们在 TiDB 5.1 版本的设计、开发和测试过程中做出的贡献,是你们一如既往的支持,帮助 TiDB 在实际场景中持续提升开发者和 DBA 的使用体验,让 TiDB 变得更加简单易用。

点击查看 TiDB 5.1 Release Notes,开启 TiDB 5.1 之旅。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何在二三线城市月薪过万(二)面试100人后的经验总结!看完

发表于 2021-07-02

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」

相信大家可能都看完了上篇文章了,也可能拿到了很多面试通知,接下来就是重中之重,面试!在楼主职场中也陆续面了大概100人左右,由此楼主总结了几点需要大家注意

温情提示:所有事项都不包括技术大牛,管理大牛,和bat大佬,请双手离开键盘,右上角退出。

1.仪表与沟通态度方面

  1. 一定要注意仪表,需要做到穿着合理,穿戴和理,仪表整洁。最起码需要理发,洗头发,洗脸,刮胡子。。。。(ps 穿的贼嘻哈 染个黄头发 我会考虑你是不是程序猿或者有多少心思放在写代码上)
  2. 精神面貌一定要注意,说话要自信,眼神不要迷离,态度散漫,提不起来精神。一定要表现出积极向上,肯为公司流血的态度(具体咋做入职了再说。。。)。
  3. 态度要温和,给人以容易沟通的印象,不要感觉自己很牛x,对面试官指点江山。(不建议与面试官发生冲突,如果不想在入职这个公司,那随你。。。。)
  4. 动作合理,保持着对面试官的尊重。

对于仪表和说话方式方面,可能有些人不是特别重视,但是这绝对是一个敲门砖,如果以上方面做得不好,可能整场面试总体基调就是这个人不行,你可能需要在技术方面多多挽回才能改变这种情况。对于二三线城市来说,如果薪资不高,技术要求可能不是特别特别高,重要的就是一个人是否听话,容易沟通,对工作态度是否端正,是否能很好融入团队(别因为你给我前端人干离职了),是否肯学习。如果在面试过程中,一个人跟我聊得特别来,情商很高,一起工作很舒服,那可能技术过得去就可以了。

2.技术面试

  1. 不要面试java,去介绍自己有c或者php经验。这样我会认为你的工作年限有问题。然后问你实际上从事java的工作时间。(除非你了解公司需要又会java又会php的)
  2. 在描述工作经验时简单的工作经验或者重复性高的项目经验(只负责增删改查,除非你没别的说的)需要高度总结一笔带过,别半个小时都说自己curd了什么什么模块,然后面试官忍无可忍打断了,因为再多也不如一句:我是xx人团队的leader 或是我为公司设计了xx(如代码规范,安全框架)。
  3. 面试过程中,可以引导面试官去问你准备的技能。如面试官问mysql优化,你可以回答使用索引,然后继续说索引使用了b+树从而使速度变快,如果我是面试官我会选择继续问 :请你说说b+树。。。 这时候就是你装x的时间段了。在面试中其实也就几个方面:java基础,spring全家桶,数据库优化。所以用这个方案,准备多一些东西,让面试节奏在自己掌握中,同时也能加深自己技术的深度。
  4. 在面试(简历中)过程中,一知半解的千万不要提。。。如在你上述过程中问题 面试官:请你说说b+树 你:emmmmmmmmmmmmmmm,我就是看过,没有深入了解。。。。
  5. 在介绍源码或者一些复杂逻辑中,面试官一般目的是想验证你是否看过和理解,而不是非要让你死记乱背,所以在这类问题上只需要说出你的理解的流程即可,而不是吭哧吭哧在哪背类名,然后还说不全。
  6. 不建议回避面试官问题,因为这样我就已经知道你不会了,我会抓住这个问题不放直到你你不会(谈薪资时候使用。。。),即使不会想绕过这个问题,你可以说:您那个方案我没接触过,但是我做过这个方案(以xxx方案代替过)。
  7. 千万不要知道就说掌握,了解就是精通,能问出来!!!

技术方面确实是会就是会,不会就是不会,但是掌握一些技巧,可以让你对面试的时间和走向更加得心应手。

hr面

  1. 在薪资方面,不能一下说死,给定一个范围,然后说按照情况在确定薪资,如加班情况,年终奖情况,如果面试的好,不就能坐地起价了。。。
  2. 在离职间隔方面,如果你离职的过于频繁,一定要给定一个合理理由,如长时间欠薪等,有特别情况,或者你的技术过硬,否则这是个减分项。(一家公司至少待1年,如果小于年,会评估招聘风险。)
  3. 强调自己的稳定性,如果是从一个城市到另一个城市,你可以说是女朋友在这,为了结婚,然后长期在这个城市。。。。你懂得。。
  4. 内卷绝招,能加班能出差!

对于公司来说,招聘的成本远远大于给你的薪资,招聘,面试,培训,交接。离职之后损失加在一起可能基本是你薪资的2倍还会多,所以对于不稳定的人员,即使比较合适,也会评估风险。所以表现出你要长时间呆的意愿是非常非常打动人的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

整理全网优秀的API接口设计及相关优秀的接口管理、在线文档生

发表于 2021-07-02

一、优秀的接口设计

在日常开发中,总会接触到各种接口。前后端数据传输接口,第三方业务平台接口。一个平台的前后端数据传输接口一般都会在内网环境下通信,而且会使用安全框架,所以安全性可以得到很好的保护。这篇文章重点讨论一下提供给第三方平台的业务接口应当如何设计?我们应该考虑哪些问题?

image-20210702094009859

主要从以上三个方面来设计一个安全的API接口。

1.1 安全性问题

安全性问题是一个接口必须要保证的规范。如果接口保证不了安全性,那么你的接口相当于直接暴露在公网环境中任人蹂躏。

1.1.1 调用接口的先决条件-token

获取token一般会涉及到几个参数appid,appkey,timestamp,nonce,sign。我们通过以上几个参数来获取调用系统的凭证。

appid和appkey可以直接通过平台线上申请,也可以线下直接颁发。appid是全局唯一的,每个appid将对应一个客户,appkey需要高度保密。

timestamp是时间戳,使用系统当前的unix时间戳。时间戳的目的就是为了减轻DOS攻击。防止请求被拦截后一直尝试请求接口。服务器端设置时间戳阀值,如果请求时间戳和服务器时间超过阀值,则响应失败。

nonce是随机值。随机值主要是为了增加sign的多变性,也可以保护接口的幂等性,相邻的两次请求nonce不允许重复,如果重复则认为是重复提交,响应失败。

sign是参数签名,将appkey,timestamp,nonce拼接起来进行md5加密(当然使用其他方式进行不可逆加密也没问题)。

token,使用参数appid,timestamp,nonce,sign来获取token,作为系统调用的唯一凭证。token可以设置一次有效(这样安全性更高),也可以设置时效性,这里推荐设置时效性。如果一次有效的话这个接口的请求频率可能会很高。token推荐加到请求头上,这样可以跟业务参数完全区分开来。

1.1.2 使用POST作为接口请求方式

一般调用接口最常用的两种方式就是GET和POST。两者的区别也很明显,GET请求会将参数暴露在浏览器URL中,而且对长度也有限制。为了更高的安全性,所有接口都采用POST方式请求。

1.1.3 客户端IP白名单

ip白名单是指将接口的访问权限对部分ip进行开放。这样就能避免其他ip进行访问攻击,设置ip白名单比较麻烦的一点就是当你的客户端进行迁移后,就需要重新联系服务提供者添加新的ip白名单。设置ip白名单的方式很多,除了传统的防火墙之外,spring cloud alibaba提供的组件sentinel也支持白名单设置。为了降低api的复杂度,推荐使用防火墙规则进行白名单设置。

1.1.4 单个接口针对ip限流

限流是为了更好的维护系统稳定性。使用redis进行接口调用次数统计,ip+接口地址作为key,访问次数作为value,每次请求value+1,设置过期时长来限制接口的调用频率。

1.5 记录接口请求日志

使用aop全局记录请求日志,快速定位异常请求位置,排查问题原因。

1.1.6 敏感数据脱敏

在接口调用过程中,可能会涉及到订单号等敏感数据,这类数据通常需要脱敏处理,最常用的方式就是加密。加密方式使用安全性比较高的RSA非对称加密。非对称加密算法有两个密钥,这两个密钥完全不同但又完全匹配。只有使用匹配的一对公钥和私钥,才能完成对明文的加密和解密过程。

1.2 幂等性问题

幂等性是指任意多次请求的执行结果和一次请求的执行结果所产生的影响相同。说的直白一点就是查询操作无论查询多少次都不会影响数据本身,因此查询操作本身就是幂等的。但是新增操作,每执行一次数据库就会发生变化,所以它是非幂等的。

幂等问题的解决有很多思路,这里讲一种比较严谨的。提供一个生成随机数的接口,随机数全局唯一。调用接口的时候带入随机数。第一次调用,业务处理成功后,将随机数作为key,操作结果作为value,存入redis,同时设置过期时长。第二次调用,查询redis,如果key存在,则证明是重复提交,直接返回错误。

1.3 数据规范问题

1.3.1 版本控制

一套成熟的API文档,一旦发布是不允许随意修改接口的。这时候如果想新增或者修改接口,就需要加入版本控制,版本号可以是整数类型,也可以是浮点数类型。一般接口地址都会带上版本号,http://ip:port//v1/list。

1.3.2 响应状态码规范

一个牛逼的API,还需要提供简单明了的响应值,根据状态码就可以大概知道问题所在。我们采用http的状态码进行数据封装,例如200表示请求成功,4xx表示客户端错误,5xx表示服务器内部发生错误。状态码设计参考如下:

分类 描述
1xx 信息,服务器收到请求,需要请求者继续执行操作
2xx 成功
3xx 重定向,需要进一步的操作以完成请求
4xx 客户端错误,请求包含语法错误或无法完成请求
5xx 服务端错误

状态码枚举类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码public enum CodeEnum {

// 根据业务需求进行添加
SUCCESS(200,"处理成功"),
ERROR_PATH(404,"请求地址错误"),
ERROR_SERVER(505,"服务器内部发生错误");

private int code;
private String message;

CodeEnum(int code, String message) {
this.code = code;
this.message = message;
}

public int getCode() {
return code;
}

public void setCode(int code) {
this.code = code;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}
}

1.3.3 统一响应数据格式

为了方便给客户端响应,响应数据会包含三个属性,状态码(code),信息描述(message),响应数据(data)。客户端根据状态码及信息描述可快速知道接口,如果状态码返回成功,再开始处理数据。

响应结果定义及常用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
java复制代码public class R implements Serializable {

private static final long serialVersionUID = 793034041048451317L;

private int code;
private String message;
private Object data = null;

public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}

public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}

public Object getData() {
return data;
}

/**
* 放入响应枚举
*/
public R fillCode(CodeEnum codeEnum){
this.setCode(codeEnum.getCode());
this.setMessage(codeEnum.getMessage());
return this;
}

/**
* 放入响应码及信息
*/
public R fillCode(int code, String message){
this.setCode(code);
this.setMessage(message);
return this;
}

/**
* 处理成功,放入自定义业务数据集合
*/
public R fillData(Object data) {
this.setCode(CodeEnum.SUCCESS.getCode());
this.setMessage(CodeEnum.SUCCESS.getMessage());
this.data = data;
return this;
}
}

1.4 接口设计总结

本篇文章从安全性、幂等性、数据规范等方面讨论了API设计规范。除此之外,一个好的API还少不了一个优秀的接口文档。接口文档的可读性非常重要,虽然很多程序员都不喜欢写文档,而且不喜欢别人不写文档。为了不增加程序员的压力,推荐使用swagger或其他接口管理工具,通过简单配置,就可以在开发中测试接口的连通性,上线后也可以生成离线文档用于管理API。

二、推荐些优秀的在线文档生成工具

一些优秀的在线文档、接口文档的生成工具,需满足如下条件:

  1. 必须是开源的
  2. 能够实时生成在线文档
  3. 支持全文搜索
  4. 支持在线调试功能
  5. 界面优美

在此推荐一些满足上述条件的在线接口文档工具。

2.1 Knife4j

gitee地址:gitee.com/xiaoym/knif… 开源协议:Apache-2.0 License

推荐指数:★★★★ 示例地址:swagger-bootstrap-ui.xiaominfo.com/doc.html

image-20210702093405442

Knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案,前身是swagger-bootstrap-ui,取名kni4j是希望她能像一把匕首一样小巧,轻量,并且功能强悍。

  • 优点:基于swagger生成实时在线文档,支持在线调试,全局参数、国际化、访问权限控制等,功能非常强大。
  • 缺点:界面有一点点丑,需要依赖额外的jar包
  • 个人建议:如果公司对ui要求不太高,可以使用这个文档生成工具,比较功能还是比较强大的。

2.2 smartdoc

gitee地址:gitee.com/smart-doc-t… 开源协议:Apache-2.0 License

用户:小米、科大讯飞、1加 推荐指数:★★★★

示例:

image-20210702093444426

smart-doc是一个java restful api文档生成工具,smart-doc颠覆了传统类似swagger这种大量采用注解侵入来生成文档的实现方法。smart-doc完全基于接口源码分析来生成接口文档,完全做到零注解侵入,只需要按照java标准注释的写就能得到一个标准的markdown接口文档。

  • 优点:基于接口源码分析生成接口文档,零注解侵入,支持html、pdf、markdown格式的文件导出。
  • 缺点:需要引入额外的jar包,不支持在线调试
  • 个人建议:如果实时生成文档,但是又不想打一些额外的注解,比如:使用swagger时需要打上@Api、@ApiModel等注解,就可以使用这个。

2.3 redoc

github地址:github.com/Redocly/red… 开源协议:MIT License

用户:docker、redocly 推荐指数:★★★☆

示例:

image-20210702093453581

redoc自己号称是一个最好的在线文档工具。它支持swagger接口数据,提供了多种生成文档的方式,非常容易部署。使用redoc-cli能够将您的文档捆绑到零依赖的 HTML文件中,响应式三面板设计,具有菜单/滚动同步。

  • 优点:非常方便生成文档,三面板设计
  • 缺点:不支持中文搜索,分为:普通版本 和 付费版本,普通版本不支持在线调试。另外UI交互个人感觉不适合国内大多数程序员的操作习惯。
  • 个人建议:如果想快速搭建一个基于swagger的文档,并且不要求在线调试功能,可以使用这个。

2.4 yapi

github地址:github.com/YMFE/yapi 开源协议:Apache-2.0 License

用户:腾讯、阿里、百度、京东等大厂 推荐指数:★★★★★

示例:

image-20210702093504332

yapi是去哪儿前端团队自主研发并开源的,主要支持以下功能:

  • 可视化接口管理
  • 数据mock
  • 自动化接口测试
  • 数据导入(包括swagger、har、postman、json、命令行)
  • 权限管理
  • 支持本地化部署
  • 支持插件
  • 支持二次开发
  • 优点:功能非常强大,支持权限管理、在线调试、接口自动化测试、插件开发等,BAT等大厂等在使用,说明功能很好。
  • 缺点:在线调试功能需要安装插件,用户体检稍微有点不好,主要是为了解决跨域问题,可能有安全性问题。不过要解决这个问题,可以自己实现一个插件,应该不难。
  • 个人建议:如果不考虑插件安全的安全性问题,这个在线文档工具还是非常好用的,可以说是一个神器,笔者在这里强烈推荐一下。

2.5 apidoc

github地址:github.com/apidoc/apid… 开源协议:MIT License

推荐指数:★★★★☆

示例:

image-20210702093549315

apidoc 是一个简单的 RESTful API 文档生成工具,它从代码注释中提取特定格式的内容生成文档。支持诸如 Go、Java、C++、Rust 等大部分开发语言,具体可使用 apidoc lang 命令行查看所有的支持列表。

apidoc 拥有以下特点:

  1. 跨平台,linux、windows、macOS 等都支持;
  2. 支持语言广泛,即使是不支持,也很方便扩展;
  3. 支持多个不同语言的多个项目生成一份文档;
  4. 输出模板可自定义;
  5. 根据文档生成 mock 数据;
  • 优点:基于代码注释生成在线文档,对代码的嵌入性比较小,支持多种语言,跨平台,也可自定义模板。支持搜索和在线调试功能。
  • 缺点:需要在注释中增加指定注解,如果代码参数或类型有修改,需要同步修改注解相关内容,有一定的维护工作量。
  • 个人建议:这种在线文档生成工具提供了另外一种思路,swagger是在代码中加注解,而apidoc是在注解中加数据,代码嵌入性更小,推荐使用。

2.6 showdoc(记得好像开始收费了)

github地址:github.com/star7th/sho… 开源协议:Apache Licence

用户:超过10000+互联网团队正在使用 推荐指数:★★★★☆

示例:

image-20210702093600033

ShowDoc就是一个非常适合IT团队的在线文档分享工具,它可以加快团队之间沟通的效率。

它都有些什么功能:

  1. 响应式网页设计,可将项目文档分享到电脑或移动设备查看。同时也可以将项目导出成word文件,以便离线浏览。
  2. 权限管理,ShowDoc上的项目有公开项目和私密项目两种。公开项目可供任何登录与非登录的用户访问,而私密项目则需要输入密码验证访问。密码由项目创建者设置。
  3. ShowDoc采用markdown编辑器,点击编辑器上方的按钮可方便地插入API接口模板和数据字典模板。
  4. ShowDoc为页面提供历史版本功能,你可以方便地把页面恢复到之前的版本。
  5. 支持文件导入,文件可以是postman的json文件、swagger的json文件、showdoc的markdown压缩包,系统会自动识别文件类型。
  • 优点:支持项目权限管理,多种格式文件导入,全文搜索等功能,使用起来还是非常方便的。并且既支持部署自己的服务器,也支持在线托管两种方式。
  • 缺点:不支持在线调试功能
  • 个人建议:如果不要求在线调试功能,这个在线文档工具值得使用。

2.7 Apizza(极客专属的接口协作管理工具)

官网地址:www.apizza.net/

2.7.1 主要功能

  1. api跨域调试量身定制的chrome插件,本地,在线接口,都可以调。
  2. 云端存储,企业安全版支持本地数据中心。
  3. 一键分享,与团队共享你的API文档。
  4. 支持Postman,Swagger格式 导入Postman/Swagger Json 生成文档。
  5. 导出离线文档,部署本地服务器。
  6. api Mock 根据文档自动生成返回结果,提供独立URL方便前端测试。
  7. 支持多种文档 http接口文档,markdown说明文档。

Apizza接口文档工具有一个很大不足的地方,那是Apizza个人免费版有人数限制,所有超过8人的团队如果想免费用,你是不用考虑Apizza的。如果你看到有文章或公众号上说Apizza是免费的,那简直是胡扯,他肯定没用过。当然如果你不缺钱,可以付费开通企业版。我们团队也是用了半年多Apizza,后来由于人员增加,Apizza里又无法再新添加新成员,迫使我们不得不放弃Apizza。

2.8 APIJSON

Gitee地址:gitee.com/Tencent/API…

推荐指数:★★★★

  • APIJSON 是一种专为 API 而生的 JSON 网络传输协议 以及 基于这套协议实现的 ORM 库。 为简单的增删改查、复杂的查询、简单的事务操作 提供了完全自动化的万能 API。
  • 能大幅降低开发和沟通成本,简化开发流程,缩短开发周期。
  • 适合中小型前后端分离的项目,尤其是 BaaS、Serverless、互联网创业项目和企业自用项目。
  • 通过万能的 API,前端可以定制任何数据、任何结构。
  • 大部分 HTTP 请求后端再也不用写接口了,更不用写文档了。
  • 前端再也不用和后端沟通接口或文档问题了。再也不会被文档各种错误坑了。
  • 后端再也不用为了兼容旧接口写新版接口和文档了。再也不会被前端随时随地没完没了地烦了。

2.8.1 特点功能

对于前端

  • 不用再向后端催接口、求文档
  • 数据和结构完全定制,要啥有啥
  • 看请求知结果,所求即所得
  • 可一次获取任何数据、任何结构
  • 能去除重复数据,节省流量提高速度

对于后端

  • 提供通用接口,大部分 API 不用再写
  • 自动生成文档,不用再编写和维护
  • 自动校验权限、自动管理版本、自动防 SQL 注入
  • 开放 API 无需划分版本,始终保持兼容
  • 支持增删改查、复杂查询、跨库连表、远程函数等

2.8.2 APIJSON 接口展示

img

2.9 其他一些接口管理神器

由于 API 在软件开发过程中如此关键,那么对 API 的管理就显得格外重要。通过 API 管理工具和平台能够大大简化 API 管理的难度和复杂度。下面列举了一些顶级 API 管理工具和平台,可供您参考。

2.9.1 API Umbrella

API Umbrella 是用于管理 API 和微服务的顶级开源工具之一。通过为不同的域授予不同的管理员权限,它可以使多个团队使用同一个 Umbrella。该平台还提供速率限制,API 密钥,缓存,实时分析和 Web 管理界面等功能。

2.9.2 Gravitee.io

Gravitee.io 是一个用于管理 API 的开源平台,这个工具是灵活的并且是轻量级的。它具有开箱即用的功能,例如速率限制,IP 过滤,跨域资源共享,即插即用选项,具有基于 OAuth2 和 JSON Web 令牌策略的开发者门户,负载平衡等。

但是,此 API 管理工具的主要功能是能够生成细粒度的报告以理解 API 的数据是如何使用的。

2.9.3 APIman.io

APIman.io 是由 Red Hat 引入的一个顶级 API 管理平台,这个平台在 GitHub 中可以找到,为后端开发人员提供了很多便利。这包括:

快速运行 具有可分离策略引擎的基于策略的治理 异步功能 增强的结算和分析选项 REST API 可用性的管理 限速,还有其他

2.9.4 WSO2 API 管理器

WSO2 API Manager 是一个完整的生命周期 API 管理平台,可以随时随地运行。可以在企业内部和私有云上执行 API 的分发和部署。除此之外,它还提供了一些其他的便利。其中一些是:

高度定制化 管理策略易用, 为 SOAP 或 RESTful API 设计和原型的可能性, 更好的访问控制和货币化设施等

2.9.5 Kong Enterprise

Kong 是一种广泛采用的开源微服务 API 工具,它使开发人员能够快速,轻松,安全地管理一切。它的企业版带有许多特性和功能,例如:

开源插件的可用性 一键式操作 通用语言基础架构功能 强大的可视化监控功能 常规软件运行状况检查 OAuth2.0 权限,以及 更广泛的社区支持

2.9.6 Tyk.io

Tyk.io 用 Go 编程语言编写,也是公认的开源 API 网关。

它带有开发者门户,详细的文档,用于 API 分析的仪表板,API 的速率限制,身份验证以及各种其他此类规范,可帮助组织专注于微服务环境和容器化。但是,其基于商业的服务仅适用于付费版本。

2.9.7 Fusio

Fusio 是另一个开源 API 管理工具,开发人员可以使用它从不同的数据类型创建和维护 REST API。它具有高效的生命周期管理功能,例如用于管理控制的后端仪表板,详细的文档,用于传入请求的 JSON 验证以及满足用户权限的范围处理。

而且,此 APIM 平台会自动生成 OAI 和 RAML 要求,并根据定义的架构创建自定义的客户端 SDK。

2.9.8 Apigility

Apigility 由 Zend 框架设计和维护,是考虑用于 API 管理的下一个开源框架。该平台创建并展示其代码的 JSON 表示形式。它还为他们提供了不同的版本控制选项,以及通过 OAuth2 进行身份验证的简便性和包含 API 蓝图的文档。

API 接口管理,这 15 种开源工具助你管理 API Apigility

2.9.9 SwaggerHub

SwaggerHub 被 40 多个组织考虑用于管理 API,它也是最好的开源 API 管理工具之一。

该平台为后端开发领域的设计人员和开发人员提供了广泛的选择。它为他们提供了强大而直观的编辑器,可在保持设计一致性的同时提供更高的效率和速度。

此外,它还提供了智能错误反馈,语法自动完成和多种样式验证器可用性的机会。

2.9.10 API Axle

在 Exicon 的支持下,API Axle 是另一种开源,简单且轻量级的代理,为开发人员提供了很多好处,例如:实时分析 强大的身份验证, 记录 API 流量以进行统计和报告, 易于创建和管理 API 密钥,以及 支持 REST API 设计以及 Go,PHP 和 Node.js 库的使用。

2.9.11 IBM Bluemix API

该 API 管理工具使开发人员可以使用 200 多种软件和中间件模式来为混合云构建可移植且兼容的应用程序。它还提供各种预先构建的服务和强大的机制,用于调节 API 访问,管理多个 API 版本,维持速率限制以及跟踪性能指标和所涉及的每个 API 的分析。

2.9.12 Repose

Repose 是一个开源的 RESTful 中间件平台,在不断变化的 API 市场中起着举足轻重的作用。该平台为组织提供了各种 API 处理功能,包括身份验证,API 验证,速率限制和 HTTP 请求日志记录。

该 API 管理平台旨在提供格式正确且经过验证的信任下游请求的下游服务。而且,它本质上具有高度可扩展性和可扩展性,这意味着开发人员可以根据不断增长的需求轻松地使用它。

2.9.13 SnapLogic 企业集成云

SnapLogic 是一个不错的集成平台即服务(iPaaS)工具,可帮助组织获取,维持和增长其客户群。其具备的特征是:

它是快速的,多点的,并具有可灵活满足面向批处理和实时应用程序数据集成需求的选项。它具有可扩展的体系结构,其运行方式类似于 Web 服务器,但也提供了拥抱多功能性的选项。它还带有创新的数据流解决方案,鼓励组织将著名的 SaaS 应用程序如 SugarCRM 和 Salesforce)添加到其传统流程中。

2.9.14 DreamFactory

DreamFactory API 管理平台是下一个项目要考虑的最好的免费开源工具之一,其受欢迎的原因如下:

它为开发人员提供了无需手动编写 API 即可进行移动应用程序开发的方法。

它使他们能够将任何 SQL / NoSQL 数据库,外部 HTTP / SOAP 服务或文件存储系统集成到 DreamFactory 环境中,并自动获得全面,灵活,完全文档化且随时可用的 REST API。除了访问用于分页,复杂过滤器,虚拟外键,相关表联接等的 API 参数之外,该平台还为 SQL 数据库提供了详细的 REST API。

DreamFactory API 管理平台的另一个独特功能是,它可以立即将 JSON 请求转换为 SOAP,反之亦然。此外,该平台还以易于管理的形式提供了高度安全的用户管理,SSO 身份验证,CORS,JSON Web 令牌,SAML 集成,API 端点上基于角色的访问控制,OAuth 和 LDAP。API 接口管理,这 15 种开源工具助你管理 API DreamFactory

2.9.15 3Scale

最后但并非最不重要的一点是,3Scale 是此 API 管理工具列表的补充。

API 管理工具由 Red Hat 拥有,它使大小型企业都可以通过以下功能轻松安全地管理其 API:

它采用了一个分布式的云层来集中 API 程序的控制。这样可以更轻松地控制分析,可访问性,开发人员工作流程,获利等。

由于它托管在分布式云托管层上,因此具有高度的灵活性和可扩展性。

3Scale API 的 OpenShift 集成功能使您能够以自动化且封闭的方式运行高性能应用程序。这个完整的生命周期 API 管理平台使开发人员可以随时计划,设计,应用,发布,管理,分析,优化和淘汰您的 API,以提供卓越的体验。

它具有通过 Web 或移动应用程序轻松共享组织数据,服务和内容的功能。最重要的是,3scale API 管理平台为您提供了将各种加密,身份验证和授权协议注入开发环境的机会。这使后端开发公司能够为其目标用户群提供适合他们的高度安全的移动应用程序体验。

上面共享的所有 API 管理工具都是开源的,有望成为技术堆栈的有益补充。但是,为了确保您选择最适合自己的业务应用程序的需求,我们接下来将介绍一些有关选择 API 管理工具的技巧。

三、第三方API接口测试问题反馈文档

  • 原文地址:蒲公英不是梦:第三方API接口测试问题反馈文档

站在第三方技术人员的角度去思考他们需要什么信息来辅助他们定位问题。

  • 文档说明解释了为什么发这份文档给他们
  • 问题反馈记录汇总记录了所有可能存在问题的接口,因为有时候处理接口并不是一次性就能完善的,需要不断的协调并进行修改,文档的目的也是为了记录我们处理接口问题的过程,做留档。
  • 接口地址用于说明我们测试这个接口的时候是用了这样的url,可以让第三方技术人员判断我们是不是测错接口了。
  • 测试人员用于第三方技术人员直接于测试人员联系并做出解释。
  • 测试时间记录的测试发生的时间,方便他们查找日志文档。
  • 请求方式、请求头部信息、请求参数可以让第三方技术人员快速判断测试人员是否按照接口要求进行测试,此外请求猜数也方便第三方技术人员自己测试进行问题复现。
  • 响应状态码则直接告诉他们接口有没有通。
  • 实际返回值和预期返回值可以让第三方技术人员进行对比我们想要得到什么样的数据。
  • 问题描述记录我们发现什么问题以及希望解决什么样的问题。

image-20210702094022710

image-20210702094029174

  • 个人网站:www.lovebetterworld.com/
  • 往后余生,只想分享一些干货,分享一些工作,学习当中的笔记、总结,并帮助需要帮助的任何人,关注我,大家一起来学习吧!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

深度掌握 Java Stream 流操作,让你的代码高出一个

发表于 2021-07-02

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」

概念

Stream将要处理的元素集合看作一种流,在流的过程中,借助Stream API对流中的元素进行操作,比如:筛选、排序、聚合等。

image-20210701194245361

Stream 的操作符大体上分为两种:中间操作符和终止操作符

中间操作符

对于数据流来说,中间操作符在执行制定处理程序后,数据流依然可以传递给下一级的操作符。

中间操作符包含8种(排除了parallel,sequential,这两个操作并不涉及到对数据流的加工操作):

  1. map(mapToInt,mapToLong,mapToDouble) 转换操作符,把比如A->B,这里默认提供了转int,long,double的操作符。
  2. flatmap(flatmapToInt,flatmapToLong,flatmapToDouble) 拍平操作比如把 int[]{2,3,4} 拍平 变成 2,3,4 也就是从原来的一个数据变成了3个数据,这里默认提供了拍平成int,long,double的操作符。
  3. limit 限流操作,比如数据流中有10个 我只要出前3个就可以使用。
  4. distint 去重操作,对重复元素去重,底层使用了equals方法。
  5. filter 过滤操作,把不想要的数据过滤。
  6. peek 挑出操作,如果想对数据进行某些操作,如:读取、编辑修改等。
  7. skip 跳过操作,跳过某些元素。
  8. sorted(unordered) 排序操作,对元素排序,前提是实现Comparable接口,当然也可以自定义比较器。

终止操作符

数据经过中间加工操作,就轮到终止操作符上场了;

终止操作符就是用来对数据进行收集或者消费的,数据到了终止操作这里就不会向下流动了,终止操作符只能使用一次。

  1. collect 收集操作,将所有数据收集起来,这个操作非常重要,官方的提供的Collectors 提供了非常多收集器,可以说Stream 的核心在于Collectors。
  2. count 统计操作,统计最终的数据个数。
  3. findFirst、findAny 查找操作,查找第一个、查找任何一个 返回的类型为Optional。
  4. noneMatch、allMatch、anyMatch 匹配操作,数据流中是否存在符合条件的元素 返回值为bool 值。
  5. min、max 最值操作,需要自定义比较器,返回数据流中最大最小的值。
  6. reduce 规约操作,将整个数据流的值规约为一个值,count、min、max底层就是使用reduce。
  7. forEach、forEachOrdered 遍历操作,这里就是对最终的数据进行消费了。
  8. toArray 数组操作,将数据流的元素转换成数组。

Stream的创建

1、通过 java.util.Collection.stream() 方法用集合创建流

1
2
3
4
5
java复制代码List<String> list = Arrays.asList("a", "b", "c");
// 创建一个顺序流
Stream<String> stream = list.stream();
// 创建一个并行流
Stream<String> parallelStream = list.parallelStream();

2、使用java.util.Arrays.stream(T[] array)方法用数组创建流

1
2
java复制代码int[] array={1,3,5,6,8};
IntStream stream = Arrays.stream(array);

3、使用Stream的静态方法:of()、iterate()、generate()

1
2
3
4
5
6
7
java复制代码Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);

Stream<Integer> stream2 = Stream.iterate(0, (x) -> x + 3).limit(4);
stream2.forEach(System.out::println); // 0 3 6 9

Stream<Double> stream3 = Stream.generate(Math::random).limit(3);
stream3.forEach(System.out::println);

输出结果:

1
2
3
4
5
6
7
8
vbnet复制代码3
6
9
0.8106623442686114
0.11554643727388458
0.1404645961428974

Process finished with exit code 0

stream和parallelStream的简单区分:

stream是顺序流,由主线程按顺序对流执行操作;
parallelStream是并行流,内部以多线程并行执行的方式对流进行操作,但前提是流中的数据处理没有顺序要求。

例如筛选集合中的奇数,两者的处理不同之处:

image-20210701230623951

Stream使用

遍历/匹配(foreach/find/match)

Stream也是支持类似集合的遍历和匹配元素的,只是Stream中的元素是以Optional类型存在的。Stream的遍历、匹配非常简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class StreamTest {

public static void main(String[] args) {
List<Integer> list = Arrays.asList(7, 6, 9, 3, 8, 2, 1);
// 遍历输出符合条件的元素
list.stream().filter(x -> x > 6).forEach(System.out::println);
// 匹配第一个
Optional<Integer> findFirst = list.stream().filter(x -> x > 6).findFirst();
// 匹配任意(适用于并行流)
Optional<Integer> findAny = list.parallelStream().filter(x -> x > 6).findAny();
// 是否包含符合特定条件的元素
boolean anyMatch = list.stream().anyMatch(x -> x < 6);
System.out.println("匹配第一个值:" + findFirst.get());
System.out.println("匹配任意一个值:" + findAny.get());
System.out.println("是否存在大于6的值:" + anyMatch);

}
}

输出结果:

1
2
3
4
5
6
7
8
vbnet复制代码7
9
8
匹配第一个值:7
匹配任意一个值:8
是否存在大于6的值:true

Process finished with exit code 0

筛选(filter)

筛选,是按照一定的规则校验流中的元素,将符合条件的元素提取到新的流中的操作。

筛选出Integer集合中大于7的元素,并打印出来

1
2
3
4
5
6
7
8
java复制代码public class StreamTest {

public static void main(String[] args) {
List<Integer> list = Arrays.asList(6, 7, 3, 8, 1, 2, 9);
Stream<Integer> stream = list.stream();
stream.filter(x -> x > 7).forEach(System.out::println);
}
}

输出结果:

1
2
3
4
vbnet复制代码8
9

Process finished with exit code 0

聚合(max/min/count)

max、min、count这些字眼你一定不陌生,没错,在mysql中我们常用它们进行数据统计。Java stream中也引入了这些概念和用法,极大地方便了我们对集合、数组的数据统计工作。

案例一:获取String集合中最长的元素。

1
2
3
4
5
6
7
8
java复制代码public class StreamTest {

public static void main(String[] args) {
List<String> list = Arrays.asList("adnm", "admmt", "pot", "xbangd", "weoujgsd");
Optional<String> max = list.stream().max(Comparator.comparing(String::length));
System.out.println("最长的字符串:" + max.get());
}
}

输出结果:

1
2
3
vbnet复制代码最长的字符串:weoujgsd

Process finished with exit code 0

案例二:获取Integer集合中的最大值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class StreamTest {

public static void main(String[] args) {
List<Integer> list = Arrays.asList(7, 6, 9, 4, 11, 6);
// 自然排序
Optional<Integer> max = list.stream().max(Integer::compareTo);
// 自定义排序
Optional<Integer> max2 = list.stream().max(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
});
System.out.println("自然排序的最大值:" + max.get());
System.out.println("自定义排序的最大值:" + max2.get());
}
}

输出结果:

1
2
3
4
vbnet复制代码自然排序的最大值:11
自定义排序的最大值:11

Process finished with exit code 0

案例三:计算Integer集合中大于6的元素的个数。

1
2
3
4
5
6
7
8
java复制代码public class StreamTest {

public static void main(String[] args) {
List<Integer> list = Arrays.asList(7, 6, 4, 8, 2, 11, 9);
long count = list.stream().filter(x -> x > 6).count();
System.out.println("list中大于6的元素个数:" + count);
}
}

输出结果:

1
2
3
vbnet复制代码list中大于6的元素个数:4

Process finished with exit code 0

映射(map/flatMap)

映射,可以将一个流的元素按照一定的映射规则映射到另一个流中。分为map和flatMap:

  • map:接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
  • flatMap:接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。

案例一:英文字符串数组的元素全部改为大写。整数数组每个元素+3。

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class StreamTest {

public static void main(String[] args) {
String[] strArr = { "abcd", "bcdd", "defde", "fTr" };
List<String> strList = Arrays.stream(strArr).map(String::toUpperCase).collect(Collectors.toList());
System.out.println("每个元素大写:" + strList);
List<Integer> intList = Arrays.asList(1, 3, 5, 7, 9, 11);
List<Integer> intListNew = intList.stream().map(x -> x + 3).collect(Collectors.toList());
System.out.println("每个元素+3:" + intListNew);
}
}

输出结果:

1
2
3
4
css复制代码每个元素大写:[ABCD, BCDD, DEFDE, FTR]
每个元素+3:[4, 6, 8, 10, 12, 14]

Process finished with exit code 0

案例二:将两个字符数组合并成一个新的字符数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class StreamTest {

public static void main(String[] args) {
List<String> list = Arrays.asList("m,k,l,a", "1,3,5,7");
List<String> listNew = list.stream().flatMap(s -> {
// 将每个元素转换成一个stream
String[] split = s.split(",");
Stream<String> s2 = Arrays.stream(split);
return s2;
}).collect(Collectors.toList());
System.out.println("处理前的集合:" + list);
System.out.println("处理后的集合:" + listNew);
}
}

输出结果:

1
2
3
4
css复制代码处理前的集合:[m,k,l,a, 1,3,5,7]
处理后的集合:[m, k, l, a, 1, 3, 5, 7]

Process finished with exit code 0

归约(reduce)

归约,也称缩减,顾名思义,是把一个流缩减成一个值,能实现对集合求和、求乘积和求最值操作。

案例一:求Integer集合的元素之和、乘积和最大值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public class StreamTest {

public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 3, 2, 8, 11, 4);
// 求和方式1
Optional<Integer> sum = list.stream().reduce(Integer::sum);
// 求和方式2
Optional<Integer> sum2 = list.stream().reduce(Integer::sum);
// 求和方式3
Integer sum3 = list.stream().reduce(0, Integer::sum);
// 求乘积
Optional<Integer> product = list.stream().reduce((x, y) -> x * y);
// 求最大值方式1
Optional<Integer> max = list.stream().reduce((x, y) -> x > y ? x : y);
// 求最大值写法2
Integer max2 = list.stream().reduce(1, Integer::max);
System.out.println("list求和:" + sum.get() + "," + sum2.get() + "," + sum3);
System.out.println("list求积:" + product.get());
System.out.println("list求和:" + max.get() + "," + max2);
}
}

输出结果:

1
2
3
4
5
vbnet复制代码list求和:29,29,29
list求积:2112
list求和:11,11

Process finished with exit code 0

归集(toList/toSet/toMap)

因为流不存储数据,那么在流中的数据完成处理后,需要将流中的数据重新归集到新的集合里。toList、toSet和toMap比较常用,另外还有toCollection、toConcurrentMap等复杂一些的用法。

下面用一个案例演示toList、toSet和toMap:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
java复制代码public class Person {

private String name; // 姓名
private int salary; // 薪资
private int age; // 年龄
private String sex; //性别
private String area; // 地区

// 构造方法
public Person(String name, int salary, int age,String sex,String area) {
this.name = name;
this.salary = salary;
this.age = age;
this.sex = sex;
this.area = area;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getSalary() {
return salary;
}

public void setSalary(int salary) {
this.salary = salary;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getSex() {
return sex;
}

public void setSex(String sex) {
this.sex = sex;
}

public String getArea() {
return area;
}

public void setArea(String area) {
this.area = area;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", salary=" + salary +
", age=" + age +
", sex='" + sex + '\'' +
", area='" + area + '\'' +
'}';
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class StreamTest {

public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 6, 3, 4, 6, 7, 9, 6, 20);
List<Integer> listNew = list.stream().filter(x -> x % 2 == 0).collect(Collectors.toList());
Set<Integer> set = list.stream().filter(x -> x % 2 == 0).collect(Collectors.toSet());
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, 23, "male", "New York"));
personList.add(new Person("Jack", 7000, 25, "male", "Washington"));
personList.add(new Person("Lily", 7800, 21, "female", "Washington"));
personList.add(new Person("Anni", 8200, 24, "female", "New York"));
Map<?, Person> map = personList.stream().filter(p -> p.getSalary() > 8000)
.collect(Collectors.toMap(Person::getName, p -> p));
System.out.println("toList:" + listNew);
System.out.println("toSet:" + set);
System.out.println("toMap:" + map);
}
}

输出结果:

1
2
3
4
5
ini复制代码toList:[6, 4, 6, 6, 20]
toSet:[4, 20, 6]
toMap:{Tom=Person{name='Tom', salary=8900, age=23, sex='male', area='New York'}, Anni=Person{name='Anni', salary=8200, age=24, sex='female', area='New York'}}

Process finished with exit code 0

统计(count/averaging)

Collectors提供了一系列用于数据统计的静态方法:

  • 计数:count
  • 平均值:averagingInt、averagingLong、averagingDouble
  • 最值:maxBy、minBy
  • 求和:summingInt、summingLong、summingDouble
  • 统计以上所有:summarizingInt、summarizingLong、summarizingDouble

案例:统计员工人数、平均工资、工资总额、最高工资。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码public class StreamTest {

public static void main(String[] args) {
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, 23, "male", "New York"));
personList.add(new Person("Jack", 7000, 25, "male", "Washington"));
personList.add(new Person("Lily", 7800, 21, "female", "Washington"));
// 求总数
long count = personList.size();
// 求平均工资
Double average = personList.stream().collect(Collectors.averagingDouble(Person::getSalary));
// 求最高工资
Optional<Integer> max = personList.stream().map(Person::getSalary).max(Integer::compare);
// 求工资之和
int sum = personList.stream().mapToInt(Person::getSalary).sum();
// 一次性统计所有信息
DoubleSummaryStatistics collect = personList.stream().collect(Collectors.summarizingDouble(Person::getSalary));
System.out.println("员工总数:" + count);
System.out.println("员工平均工资:" + average);
System.out.println("员工最高工资:" + max.get());
System.out.println("员工工资总和:" + sum);
System.out.println("员工工资所有统计:" + collect);
}
}

输出结果:

1
2
3
4
5
6
7
python复制代码员工总数:3
员工平均工资:7900.0
员工最高工资:8900
员工工资总和:23700
员工工资所有统计:DoubleSummaryStatistics{count=3, sum=23700.000000, min=7000.000000, average=7900.000000, max=8900.000000}

Process finished with exit code 0

分组(partitioningBy/groupingBy)

  • 分区:将stream按条件分为两个Map,比如员工按薪资是否高于8000分为两部分。
  • 分组:将集合分为多个Map,比如员工按性别分组。有单级分组和多级分组。

案例:将员工按薪资是否高于8000分为两部分;将员工按性别和地区分组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class StreamTest {

public static void main(String[] args) {
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, 23, "male", "Washington"));
personList.add(new Person("Jack", 7000, 25, "male", "Washington"));
personList.add(new Person("Lily", 7800, 21, "female", "New York"));
personList.add(new Person("Anni", 8200, 24, "female", "New York"));
// 将员工按薪资是否高于8000分组
Map<Boolean, List<Person>> part = personList.stream().collect(Collectors.partitioningBy(x -> x.getSalary() > 8000));
// 将员工按性别分组
Map<String, List<Person>> group = personList.stream().collect(Collectors.groupingBy(Person::getSex));
// 将员工先按性别分组,再按地区分组
Map<String, Map<String, List<Person>>> group2 = personList.stream().collect(Collectors.groupingBy(Person::getSex, Collectors.groupingBy(Person::getArea)));
System.out.println("员工按薪资是否大于8000分组情况:" + part);
System.out.println("员工按性别分组情况:" + group);
System.out.println("员工按性别、地区:" + group2);
}
}

输出结果:

1
2
3
4
5
css复制代码员工按薪资是否大于8000分组情况:{false=[Person{name='Jack', salary=7000, age=25, sex='male', area='Washington'}, Person{name='Lily', salary=7800, age=21, sex='female', area='New York'}], true=[Person{name='Tom', salary=8900, age=23, sex='male', area='Washington'}, Person{name='Anni', salary=8200, age=24, sex='female', area='New York'}]}
员工按性别分组情况:{female=[Person{name='Lily', salary=7800, age=21, sex='female', area='New York'}, Person{name='Anni', salary=8200, age=24, sex='female', area='New York'}], male=[Person{name='Tom', salary=8900, age=23, sex='male', area='Washington'}, Person{name='Jack', salary=7000, age=25, sex='male', area='Washington'}]}
员工按性别、地区:{female={New York=[Person{name='Lily', salary=7800, age=21, sex='female', area='New York'}, Person{name='Anni', salary=8200, age=24, sex='female', area='New York'}]}, male={Washington=[Person{name='Tom', salary=8900, age=23, sex='male', area='Washington'}, Person{name='Jack', salary=7000, age=25, sex='male', area='Washington'}]}}

Process finished with exit code 0

接合(joining)

joining可以将stream中的元素用特定的连接符(没有的话,则直接连接)连接成一个字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class StreamTest {

public static void main(String[] args) {
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, 23, "male", "New York"));
personList.add(new Person("Jack", 7000, 25, "male", "Washington"));
personList.add(new Person("Lily", 7800, 21, "female", "Washington"));
String names = personList.stream().map(Person::getName).collect(Collectors.joining(","));
System.out.println("所有员工的姓名:" + names);
List<String> list = Arrays.asList("A", "B", "C");
String string = list.stream().collect(Collectors.joining("-"));
System.out.println("拼接后的字符串:" + string);
}
}

输出结果:

1
2
3
4
css复制代码所有员工的姓名:Tom,Jack,Lily
拼接后的字符串:A-B-C

Process finished with exit code 0

排序(sorted)

sorted,中间操作。有两种排序:

  • sorted():自然排序,流中元素需实现Comparable接口
  • sorted(Comparator com):Comparator排序器自定义排序

案例:将员工按工资由高到低(工资一样则按年龄由大到小)排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
java复制代码public class StreamTest {

public static void main(String[] args) {
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Sherry", 9000, 24, "female", "New York"));
personList.add(new Person("Tom", 8900, 22, "male", "Washington"));
personList.add(new Person("Jack", 9000, 25, "male", "Washington"));
personList.add(new Person("Lily", 8800, 26, "male", "New York"));
personList.add(new Person("Alisa", 9000, 26, "female", "New York"));
// 按工资升序排序(自然排序)
List<String> newList = personList.stream().sorted(Comparator.comparing(Person::getSalary)).map(Person::getName)
.collect(Collectors.toList());
// 按工资倒序排序
List<String> newList2 = personList.stream().sorted(Comparator.comparing(Person::getSalary).reversed())
.map(Person::getName).collect(Collectors.toList());
// 先按工资再按年龄升序排序
List<String> newList3 = personList.stream()
.sorted(Comparator.comparing(Person::getSalary).thenComparing(Person::getAge)).map(Person::getName)
.collect(Collectors.toList());
// 先按工资再按年龄自定义排序(降序)
List<String> newList4 = personList.stream().sorted((p1, p2) -> {
if (p1.getSalary() == p2.getSalary()) {
return p2.getAge() - p1.getAge();
} else {
return p2.getSalary() - p1.getSalary();
}
}).map(Person::getName).collect(Collectors.toList());
System.out.println("按工资升序排序:" + newList);
System.out.println("按工资降序排序:" + newList2);
System.out.println("先按工资再按年龄升序排序:" + newList3);
System.out.println("先按工资再按年龄自定义降序排序:" + newList4);
}
}

输出结果:

1
2
3
4
5
6
css复制代码按工资升序排序:[Lily, Tom, Sherry, Jack, Alisa]
按工资降序排序:[Sherry, Jack, Alisa, Tom, Lily]
先按工资再按年龄升序排序:[Lily, Tom, Sherry, Jack, Alisa]
先按工资再按年龄自定义降序排序:[Alisa, Jack, Sherry, Tom, Lily]

Process finished with exit code 0

提取/组合

流也可以进行合并、去重、限制、跳过等操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class StreamTest {

public static void main(String[] args) {
String[] arr1 = { "a", "b", "c", "d" };
String[] arr2 = { "d", "e", "f", "g" };
Stream<String> stream1 = Stream.of(arr1);
Stream<String> stream2 = Stream.of(arr2);
// concat:合并两个流 distinct:去重
List<String> newList = Stream.concat(stream1, stream2).distinct().collect(Collectors.toList());
// limit:限制从流中获得前n个数据
List<Integer> collect = Stream.iterate(1, x -> x + 2).limit(10).collect(Collectors.toList());
// skip:跳过前n个数据
List<Integer> collect2 = Stream.iterate(1, x -> x + 2).skip(1).limit(5).collect(Collectors.toList());
System.out.println("流合并:" + newList);
System.out.println("limit:" + collect);
System.out.println("skip:" + collect2);
}
}

输出结果:

1
2
3
4
5
css复制代码流合并:[a, b, c, d, e, f, g]
limit:[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
skip:[3, 5, 7, 9, 11]

Process finished with exit code 0

分页操作

stream api 的强大之处还不仅仅是对集合进行各种组合操作,还支持分页操作。

例如,将如下的数组从小到大进行排序,排序完成之后,从第1行开始,查询10条数据出来,操作如下:

1
2
3
4
java复制代码//需要查询的数据
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5, 10, 6, 20, 30, 40, 50, 60, 100);
List<Integer> dataList = numbers.stream().sorted(Integer::compareTo).skip(0).limit(10).collect(Collectors.toList());
System.out.println(dataList.toString());

输出结果:

1
2
3
csharp复制代码[2, 2, 3, 3, 3, 5, 6, 7, 10, 20]

Process finished with exit code 0

并行操作

所谓并行,指的是多个任务在同一时间点发生,并由不同的cpu进行处理,不互相抢占资源;而并发,指的是多个任务在同一时间点内同时发生了,但由同一个cpu进行处理,互相抢占资源。

stream api 的并行操作和串行操作,只有一个方法区别,其他都一样,例如下面我们使用parallelStream来输出空字符串的数量:

1
2
3
4
java复制代码List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd", "", "jkl");
// 采用并行计算方法,获取空字符串的数量
long count = strings.parallelStream().filter(String::isEmpty).count();
System.out.println(count);

在实际使用的时候,并行操作不一定比串行操作快!对于简单操作,数量非常大,同时服务器是多核的话,建议使用Stream并行!反之,采用串行操作更可靠!

集合转Map操作

在实际的开发过程中,还有一个使用最频繁的操作就是,将集合元素中某个主键字段作为key,元素作为value,来实现集合转map的需求,这种需求在数据组装方面使用的非常多。

1
2
3
4
5
6
7
8
9
java复制代码public static void main(String[] args) {
List<Person> personList = new ArrayList<>();
personList.add(new Person("Tom",7000,25,"male","安徽"));
personList.add(new Person("Jack",8000,30,"female","北京"));
personList.add(new Person("Lucy",9000,40,"male","上海"));
personList.add(new Person("Airs",10000,40,"female","深圳"));
Map<Integer, Person> collect = personList.stream().collect(Collectors.toMap(Person::getAge, v -> v, (k1, k2) -> k1));
System.out.println(collect);
}

输出结果:

1
2
3
ini复制代码{40=Person{name='Lucy', salary=9000, age=40, sex='male', area='上海'}, 25=Person{name='Tom', salary=7000, age=25, sex='male', area='安徽'}, 30=Person{name='Jack', salary=8000, age=30, sex='female', area='北京'}}

Process finished with exit code 0

打开Collectors.toMap方法源码,一起来看看。

1
2
3
4
5
6
java复制代码public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction) {
return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}

从参数表可以看出:

  • 第一个参数:表示 key
  • 第二个参数:表示 value
  • 第三个参数:表示某种规则

上文中的Collectors.toMap(Person::getAge, v -> v, (k1,k2) -> k1),表达的意思就是将age的内容作为key,v -> v是表示将元素person作为value,其中(k1,k2) -> k1表示如果存在相同的key,将第一个匹配的元素作为内容,第二个舍弃!

结尾

本文主要,围绕 jdk stream api 操作,结合实际的日常开发需求,做了简单总结和分享。希望你也能跟着一起敲一遍加深印象,相信都能掌握这些操作符的初步用法;后续文章我会带大家一步步深入Stream。看完了,希望你能点个赞,哈哈。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

第三方API对接如何设计接口认证?

发表于 2021-07-02

一、前言

在与第三方系统做接口对接时,往往需要考虑接口的安全性问题,本文主要分享几个常见的系统之间做接口对接时的认证方案。

二、认证方案

例如订单下单后通过 延时任务 对接 物流系统 这种 异步 的场景,都是属于系统与系统之间的相互交互,不存在用户操作;所以认证时需要的不是用户凭证而是系统凭证,通常包括 app_id 与 app_secrect。

app_id与app_secrect由接口提供方提供

2.1. Baic认证

这是一种较为简单的认证方式,客户端通过明文(Base64编码格式)传输用户名和密码到服务端进行认证。

通过在 Header 中添加key为 Authorization,值为 Basic 用户名:密码的base64编码,例如app_id为和app_secrect都为 zlt,然后对 zlt:zlt 字符进行base64编码,最终传值为:

1
http复制代码Authorization: Basic emx0OnpsdA==

2.1.1. 优点

简单,被广泛支持。

2.1.2. 缺点

安全性较低,需要配合HTTPS来保证信息传输的安全

  1. 虽然用户名和密码使用了Base64编码,但是很容易就可以解码。
  2. 无法防止 重放攻击 与 中间人攻击。

2.2. Token认证

使用 Oauth2.0 中的 客户端模式 进行Token认证,流程如下图所示:

file

使用Basic认证的方式获取access_token之后,再通过token来请求业务接口

2.2.1. 优点

安全性相对 Baic认证 有所提升,每次接口调用时都使用临时颁发的 access_token 来代替 用户名和密码 减少凭证泄漏的机率。

2.2.2. 缺点

依然存在 Baic认证 的安全问题。

2.3. 动态签名

在每次接口调用时都需要传输以下参数:

  • app_id 应用id
  • time 当前时间戳
  • nonce 随机数
  • sign 签名

其中sign签名的生成方式为:使用参数中的
app_id + time + nonce 并在最后追加 app_secrect 的字符串进行md5加密,并全部转换成大写。

如果需要实现参数的防篡改,只需把接口所有的请求参数都作为签名的生成参数即可

2.3.1. 优点

安全性最高

  1. 服务端使用相同的方式生成签名进行对比认证,无需在网络上传输 app_secrect。
  2. 可以防止 中间人攻击。
  3. 通过 time 参数判断请求的时间差是否在合理范围内,可防止 重放攻击。
  4. 通过 nonce 参数进行幂等性判断。

2.3.2. 缺点

不适用于前端应用使用,js源码会暴露签名的方式与app_secrect

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

肥肥的主管和帅气的小饭饭讨论了一下午的ForkJoinPoo

发表于 2021-07-01

「本文已参与好文召集令活动,点击查看后端、大前端双赛道投稿,2万元奖池等你挑战!」

肥肥的主管: 小饭饭,了解ForkJoinPool吗

帅气的小饭饭: 了解啊,Caffeine中默认用到的处理线程池就是这个

肥肥的主管: 和ThreadPoolExecutor有什么区别吗?

帅气的小饭饭: (⊙o⊙)…,这个没了解

肥肥的主管: 有什么应用场景吗?能引入我们的游戏中吗?有什么弊端吗?

帅气的小饭饭: (⊙o⊙)…,我周末研究下

以上就是我,帅气的小饭饭被小肥肥刁难的日常了。

image-20210620115400862

image-20210620115400862

每次看到这个,总会邪恶的想到FuckXXX的。

其实好久前就知道这个FuckXXX了,比如Caffeine、Netty中默认提供的线程池,只是一直没有去研究它。

过了个周末,我李汉三终于搞懂了ForkJoinPool了,终于可以和小肥肥正面肝上了

image-20210620154701046

image-20210620154701046

看完该篇文章你大概可以了解到这些知识点:

  • ForkJoinPool是什么
  • 和ThreadPoolExecutor有什么区别
  • 应用场景是啥
  • 好处和弊端
  • 常见面试题

小肥肥: 周末不是研究了下ForkJoinPool吗?说说看是啥东西啊这?

*帅气的小饭饭: * 是啊是啊,可卖力了,现在已经研究透彻了,

ForkJoinPool是JDK7引入的线程池,核心思想是将大的任务拆分成多个小任务(即fork),然后在将多个小任务处理汇总到一个结果(即join),非常像MapReduce处理原理。

同时呢,它还提供基本的线程池功能,支持设置最大并发线程数,支持任务排队,支持线程池停止,支持线程池使用情况监控,也是AbstractExecutorService的子类,主要引入了“工作窃取”机制,在多CPU计算机上处理性能更佳。

为了让你看清楚这个过程,我还特地画了个流程图给你look look

image-20210620120440613

image-20210620120440613

小肥肥: 什么叫“工作窃取”机制啊,这么屌的

*帅气的小饭饭: *和它的名字一样啊,就是窃取啊,work-stealing(工作窃取),ForkJoinPool提供了一个更有效的利用线程的机制,当ThreadPoolExecutor还在用单个队列存放任务时,ForkJoinPool已经分配了与线程数相等的队列,当有任务加入线程池时,会被平均分配到对应的队列上,各线程进行正常工作,当有线程提前完成时,会从队列的末端“窃取”其他线程未执行完的任务,当任务量特别大时,CPU多的计算机会表现出更好的性能,就是这么强啊。

image-20210620123535386

image-20210620123535386

小肥肥: 我擦,好强,原理是啥啊

*帅气的小饭饭: *原理啊,我罗列下吧

  • ForkJoinPool 的每个工作线程都维护着一个工作队列(WorkQueue),这是一个双端队列(Deque),里面存放的对象是任务(ForkJoinTask)。
  • 每个工作线程在运行中产生新的任务,通常是因为调用了fork(),会放入工作队列的队尾,并且工作线程在处理自己的工作队列时,使用的是 LIFO 方式,也就是说每次从队尾取出任务来执行。
  • 每个工作线程在处理自己的工作队列同时,会尝试窃取一个任务,这个窃取行为或是来自于刚刚提交到 pool 的任务,或是来自于其他工作线程的工作队列,窃取的任务位于其他线程的工作队列的队首,也就是说工作线程在窃取其他工作线程的任务时,使用的是 FIFO 方式。
  • 在遇到 join() 时,如果需要 join 的任务尚未完成,则会先处理其他任务,并等待其完成。
  • 在既没有自己的任务,也没有可以窃取的任务时,进入休眠。

*小肥肥: *了解了,那除此之外还和ThreadPoolExecutor有什么区别吗

*帅气的小饭饭: *(⊙o⊙)…,最大的区别就是这个工作窃取机制了,其他的,我想想看哦,突然想到了我们游戏内的线程池模型

我们游戏内使用的ThreadPoolExecutor线程池内的对应线程池是有明确规定的,比如A线程只能处理一批玩家的数据,B线程只能处理另一批玩家的数据,这里用了取摩的思想,而ForkJoinPool 这种窃取思想,自身是并行计算,无法对对应线程进行明确的工作划分,单纯这点来说,其实就不适合用来做我们的线程模型的载体了。

_小肥肥:_嗯,也是,那它有啥应用场景呢

*帅气的小饭饭: *ForkJoinPool 最适合的是计算密集型的任务,如果存在 I/O,线程间同步,sleep() 等会造成线程长时间阻塞的情况时,最好配合使用 ManagedBlocker,或者不用。

为了让小肥肥看清楚这个应用场景,我打开了idea,怒写了以下代码【其实是从网上看到的例子】

方案一:平平无奇的for循环解决

1
java复制代码public interface Calculator {    /**     * 把传进来的所有numbers 做求和处理     *     * @param numbers     * @return 总和     */    long sumUp(long[] numbers);}

日常代码

1
java复制代码public class ForLoopCalculator implements Calculator {    @Override    public long sumUp(long[] numbers) {        long total = 0;        for (long i : numbers) {            total += i;        }        return total;    }}

执行类

1
java复制代码    public static void main(String[] args) {        long[] numbers = LongStream.rangeClosed(1, 10000000).toArray();        Instant start = Instant.now();        Calculator calculator = new ForLoopCalculator();        long result = calculator.sumUp(numbers);        Instant end = Instant.now();        System.out.println("耗时:" + Duration.between(start, end).toMillis() + "ms");        System.out.println("结果为:" + result);     }输出:耗时:10ms结果为:50000005000000

方案二:ExecutorService多线程方式实现

1
java复制代码public class ExecutorServiceCalculator implements Calculator {    private int parallism;    private ExecutorService pool;    public ExecutorServiceCalculator() {        parallism = Runtime.getRuntime().availableProcessors(); // CPU的核心数 默认就用cpu核心数了        pool = Executors.newFixedThreadPool(parallism);    }    //处理计算任务的线程    private static class SumTask implements Callable<Long> {        private long[] numbers;        private int from;        private int to;        public SumTask(long[] numbers, int from, int to) {            this.numbers = numbers;            this.from = from;            this.to = to;        }        @Override        public Long call() {            long total = 0;            for (int i = from; i <= to; i++) {                total += numbers[i];            }            return total;        }    }    @Override    public long sumUp(long[] numbers) {        List<Future<Long>> results = new ArrayList<>();        // 把任务分解为 n 份,交给 n 个线程处理   4核心 就等分成4份呗        // 然后把每一份都扔个一个SumTask线程 进行处理        int part = numbers.length / parallism;        for (int i = 0; i < parallism; i++) {            int from = i * part; //开始位置            int to = (i == parallism - 1) ? numbers.length - 1 : (i + 1) * part - 1; //结束位置            //扔给线程池计算            results.add(pool.submit(new SumTask(numbers, from, to)));        }        // 把每个线程的结果相加,得到最终结果 get()方法 是阻塞的        // 优化方案:可以采用CompletableFuture来优化  JDK1.8的新特性        long total = 0L;        for (Future<Long> f : results) {            try {                total += f.get();            } catch (Exception ignore) {            }        }        return total;    }}

执行类

1
java复制代码    public static void main(String[] args) {        long[] numbers = LongStream.rangeClosed(1, 10000000).toArray();        Instant start = Instant.now();        Calculator calculator = new ExecutorServiceCalculator();        long result = calculator.sumUp(numbers);        Instant end = Instant.now();        System.out.println("耗时:" + Duration.between(start, end).toMillis() + "ms");        System.out.println("结果为:" + result); // 打印结果500500    }输出:耗时:30ms结果为:50000005000000

方案三:采用ForkJoinPool(Fork/Join)

1
java复制代码public class ForkJoinCalculator implements Calculator {    private ForkJoinPool pool;    //执行任务RecursiveTask:有返回值  RecursiveAction:无返回值    private static class SumTask extends RecursiveTask<Long> {        private long[] numbers;        private int from;        private int to;        public SumTask(long[] numbers, int from, int to) {            this.numbers = numbers;            this.from = from;            this.to = to;        }        //此方法为ForkJoin的核心方法:对任务进行拆分  拆分的好坏决定了效率的高低        @Override        protected Long compute() {            // 当需要计算的数字个数小于6时,直接采用for loop方式计算结果            if (to - from < 6) {                long total = 0;                for (int i = from; i <= to; i++) {                    total += numbers[i];                }                return total;            } else { // 否则,把任务一分为二,递归拆分(注意此处有递归)到底拆分成多少分 需要根据具体情况而定                int middle = (from + to) / 2;                SumTask taskLeft = new SumTask(numbers, from, middle);                SumTask taskRight = new SumTask(numbers, middle + 1, to);                taskLeft.fork();                taskRight.fork();                return taskLeft.join() + taskRight.join();            }        }    }    public ForkJoinCalculator() {        // 也可以使用公用的线程池 ForkJoinPool.commonPool():        // pool = ForkJoinPool.commonPool()        pool = new ForkJoinPool();    }    @Override    public long sumUp(long[] numbers) {        Long result = pool.invoke(new SumTask(numbers, 0, numbers.length - 1));        pool.shutdown();        return result;    }}输出:耗时:390ms结果为:50000005000000

方案四:采用并行流

1
java复制代码    public static void main(String[] args) {        Instant start = Instant.now();        long result = LongStream.rangeClosed(0, 10000000L).parallel().reduce(0, Long::sum);        Instant end = Instant.now();        System.out.println("耗时:" + Duration.between(start, end).toMillis() + "ms");        System.out.println("结果为:" + result); // 打印结果500500    }输出:耗时:130ms结果为:50000005000000

并行流底层也是Fork/Join框架,只是任务拆分优化得很好。

耗时效率方面解释:Fork/Join 并行流等当计算的数字非常大的时候,优势才能体现出来。也就是说,如果你的计算比较小,或者不是CPU密集型的任务,不太建议使用并行处理。

小肥肥: 不明觉厉,看起来就是不适用,使用它的好处和弊端呢?

帅气的小饭饭: 好处就是对CPU的充分利用了,特别是在计算密集型的任务的时候,弊端就是需要对任务进行拆分,拆分的好坏也意味着任务的好坏,对开发人员来说,还是有点成本存在的。

小肥肥: 嗯,确实,我这边有份简历,有某初级游戏开发说用上了这个技术,你设计下相关面试题问问人家吧。

帅气的小饭饭: 好吧,那这边大概设计以下几道面试题吧

  • 什么是ForkJoin?
  • 说说看ForkJoin的内部原理?
  • 大概聊聊工作窃取机制?
  • 如何充分利用CPU,计算超大数组中所有整数的和?

这些面试题的答案基本上都在我们聊的话题中啦。

小肥肥: 挺好的,等着升资深开发吧你,周末还学习,对了,总结下,将刚刚的聊天记录总结下,然后后期做个分享。

帅气的小饭饭: 哦豁,可以啊,先加点钱啊,穷啊,我总结下吧

  • ForkJoinPool特别适合于“分而治之”算法的实现;
  • ForkJoinPool和ThreadPoolExecutor是互补的,不是谁替代谁的关系,二者适用的场景不同;
  • ForkJoinTask有两个核心方法——fork()和join(),有三个重要子类——RecursiveAction、RecursiveTask和CountedCompleter;
  • ForkjoinPool内部基于“工作窃取”算法实现;
  • 每个线程有自己的工作队列,它是一个双端队列,自己从队列头存取任务,其它线程从尾部窃取任务;
  • ForkJoinPool最适合于计算密集型任务,但也可以使用ManagedBlocker以便用于阻塞型任务;
  • RecursiveTask内部可以少调用一次fork(),利用当前线程处理,这是一种技巧;

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MySQL 常见面试题/知识点总结!(2021 最新版)

发表于 2021-07-01

相关阅读: 2.7w字!Java基础面试题/知识点总结!(2021 最新版)

这篇文章之前发过,不过,我最近对其进行了重构完善并且修复了很多小问题。所以,在再同步一下!

内容很硬!强烈建议小伙伴们花 10 分钟左右阅读一遍!

MySQL 基础

关系型数据库介绍

顾名思义,关系型数据库就是一种建立在关系模型的基础上的数据库。关系模型表明了数据库中所存储的数据之间的联系(一对一、一对多、多对多)。

关系型数据库中,我们的数据都被存放在了各种表中(比如用户表),表中的每一行就存放着一条数据(比如一个用户的信息)。

大部分关系型数据库都使用 SQL 来操作数据库中的数据。并且,大部分关系型数据库都支持事务的四大特性(ACID)。

有哪些常见的关系型数据库呢?

MySQL、PostgreSQL、Oracle、SQL Server、SQLite(微信本地的聊天记录的存储就是用的 SQLite) ……。

MySQL 介绍

MySQL 是一种关系型数据库,主要用于持久化存储我们的系统中的一些数据比如用户信息。

由于 MySQL 是开源免费并且比较成熟的数据库,因此,MySQL 被大量使用在各种系统中。任何人都可以在 GPL(General Public License) 的许可下下载并根据个性化的需要对其进行修改。MySQL 的默认端口号是3306。

存储引擎

存储引擎相关的命令

查看 MySQL 提供的所有存储引擎

1
sql复制代码mysql> show engines;

查看MySQL提供的所有存储引擎

从上图我们可以查看出 MySQL 当前默认的存储引擎是 InnoDB,并且在 5.7 版本所有的存储引擎中只有 InnoDB 是事务性存储引擎,也就是说只有 InnoDB 支持事务。

查看 MySQL 当前默认的存储引擎

我们也可以通过下面的命令查看默认的存储引擎。

1
sql复制代码mysql> show variables like '%storage_engine%';

查看表的存储引擎

1
sql复制代码show table status like "table_name" ;

查看表的存储引擎

MyISAM 和 InnoDB 的区别

MySQL 5.5 之前,MyISAM 引擎是 MySQL 的默认存储引擎,可谓是风光一时。

虽然,MyISAM 的性能还行,各种特性也还不错(比如全文索引、压缩、空间函数等)。但是,MyISAM 不支持事务和行级锁,而且最大的缺陷就是崩溃后无法安全恢复。

5.5 版本之后,MySQL 引入了 InnoDB(事务性数据库引擎),MySQL 5.5 版本后默认的存储引擎为 InnoDB。小伙子,一定要记好这个 InnoDB ,你每次使用 MySQL 数据库都是用的这个存储引擎吧?

言归正传!咱们下面还是来简单对比一下两者:

1.是否支持行级锁

MyISAM 只有表级锁(table-level locking),而 InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。

也就说,MyISAM 一锁就是锁住了整张表,这在并发写的情况下是多么滴憨憨啊!这也是为什么 InnoDB 在并发写的时候,性能更牛皮了!

2.是否支持事务

MyISAM 不提供事务支持。

InnoDB 提供事务支持,具有提交(commit)和回滚(rollback)事务的能力。

3.是否支持外键

MyISAM 不支持,而 InnoDB 支持。

🌈 拓展一下:

一般我们也是不建议在数据库层面使用外键的,应用层面可以解决。不过,这样会对数据的一致性造成威胁。具体要不要使用外键还是要根据你的项目来决定。

4.是否支持数据库异常崩溃后的安全恢复

MyISAM 不支持,而 InnoDB 支持。

使用 InnoDB 的数据库在异常崩溃后,数据库重新启动的时候会保证数据库恢复到崩溃前的状态。这个恢复的过程依赖于 redo log 。

🌈 拓展一下:

  • MySQL InnoDB 引擎使用 redo log(重做日志) 保证事务的持久性,使用 undo log(回滚日志) 来保证事务的原子性。
  • MySQL InnoDB 引擎通过 锁机制、MVCC 等手段来保证事务的隔离性( 默认支持的隔离级别是 REPEATABLE-READ )。
  • 保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。

5.是否支持 MVCC

MyISAM 不支持,而 InnoDB 支持。

讲真,这个对比有点废话,毕竟 MyISAM 连行级锁都不支持。

MVCC 可以看作是行级锁的一个升级,可以有效减少加锁操作,提供性能。

关于 MyISAM 和 InnoDB 的选择问题

大多数时候我们使用的都是 InnoDB 存储引擎,在某些读密集的情况下,使用 MyISAM 也是合适的。不过,前提是你的项目不介意 MyISAM 不支持事务、崩溃恢复等缺点(可是~我们一般都会介意啊!)。

《MySQL 高性能》上面有一句话这样写到:

不要轻易相信“MyISAM 比 InnoDB 快”之类的经验之谈,这个结论往往不是绝对的。在很多我们已知场景中,InnoDB 的速度都可以让 MyISAM 望尘莫及,尤其是用到了聚簇索引,或者需要访问的数据都可以放入内存的应用。

一般情况下我们选择 InnoDB 都是没有问题的,但是某些情况下你并不在乎可扩展能力和并发能力,也不需要事务支持,也不在乎崩溃后的安全恢复问题的话,选择 MyISAM 也是一个不错的选择。但是一般情况下,我们都是需要考虑到这些问题的。

因此,对于咱们日常开发的业务系统来说,你几乎找不到什么理由再使用 MyISAM 作为自己的 MySQL 数据库的存储引擎。

锁机制与 InnoDB 锁算法

MyISAM 和 InnoDB 存储引擎使用的锁:

  • MyISAM 采用表级锁(table-level locking)。
  • InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁

表级锁和行级锁对比:

  • 表级锁: MySQL 中锁定 粒度最大 的一种锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM 和 InnoDB 引擎都支持表级锁。
  • 行级锁: MySQL 中锁定 粒度最小 的一种锁,只针对当前操作的行进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。

InnoDB 存储引擎的锁的算法有三种:

  • Record lock:记录锁,单个行记录上的锁
  • Gap lock:间隙锁,锁定一个范围,不包括记录本身
  • Next-key lock:record+gap 临键锁,锁定一个范围,包含记录本身

查询缓存

执行查询语句的时候,会先查询缓存。不过,MySQL 8.0 版本后移除,因为这个功能不太实用

my.cnf 加入以下配置,重启 MySQL 开启查询缓存

1
2
properties复制代码query_cache_type=1
query_cache_size=600000

MySQL 执行以下命令也可以开启查询缓存

1
2
properties复制代码set global  query_cache_type=1;
set global query_cache_size=600000;

如上,开启查询缓存后在同样的查询条件以及数据情况下,会直接在缓存中返回结果。这里的查询条件包括查询本身、当前要查询的数据库、客户端协议版本号等一些可能影响结果的信息。因此任何两个查询在任何字符上的不同都会导致缓存不命中。此外,如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、MySQL 库中的系统表,其查询结果也不会被缓存。

缓存建立之后,MySQL 的查询缓存系统会跟踪查询中涉及的每张表,如果这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。

缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。 因此,开启查询缓存要谨慎,尤其对于写密集的应用来说更是如此。如果开启,要注意合理控制缓存空间大小,一般来说其大小设置为几十 MB 比较合适。此外,还可以通过 sql_cache 和 sql_no_cache 来控制某个查询语句是否需要缓存:

1
sql复制代码select sql_no_cache count(*) from usr;

事务

何为事务?

一言蔽之,事务是逻辑上的一组操作,要么都执行,要么都不执行。

可以简单举一个例子不?

事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账 1000 元,这个转账会涉及到两个关键操作就是:

  1. 将小明的余额减少 1000 元
  2. 将小红的余额增加 1000 元。

事务会把这两个操作就可以看成逻辑上的一个整体,这个整体包含的操作要么都成功,要么都要失败。

这样就不会出现小明余额减少而小红的余额却并没有增加的情况。

何为数据库事务?

数据库事务在我们日常开发中接触的最多了。如果你的项目属于单体架构的话,你接触到的往往就是数据库事务了。

平时,我们在谈论事务的时候,如果没有特指分布式事务,往往指的就是数据库事务。

那数据库事务有什么作用呢?

简单来说:数据库事务可以保证多个对数据库的操作(也就是 SQL 语句)构成一个逻辑上的整体。构成这个逻辑上的整体的这些数据库操作遵循:要么全部执行成功,要么全部不执行 。

1
2
3
4
5
6
sql复制代码# 开启一个事务
START TRANSACTION;
# 多条 SQL 语句
SQL1,SQL2...
## 提交事务
COMMIT;

另外,关系型数据库(例如:MySQL、SQL Server、Oracle 等)事务都有 ACID 特性:

事务的特性

何为 ACID 特性呢?

  1. 原子性(Atomicity) : 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  2. 一致性(Consistency): 执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;
  3. 隔离性(Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
  4. 持久性(Durabilily): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

数据事务的实现原理呢?

我们这里以 MySQL 的 InnoDB 引擎为例来简单说一下。

MySQL InnoDB 引擎使用 redo log(重做日志) 保证事务的持久性,使用 undo log(回滚日志) 来保证事务的原子性。

MySQL InnoDB 引擎通过 锁机制、MVCC 等手段来保证事务的隔离性( 默认支持的隔离级别是 REPEATABLE-READ )。

保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。

并发事务带来哪些问题?

在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题。

  • 脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
  • 丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务 1 读取某表中的数据 A=20,事务 2 也读取 A=20,事务 1 修改 A=A-1,事务 2 也修改 A=A-1,最终结果 A=19,事务 1 的修改被丢失。
  • 不可重复读(Unrepeatable read): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
  • 幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

不可重复读和幻读区别:

不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增或者删除比如多次读取一条记录发现记录增多或减少了。

事务隔离级别有哪些?

SQL 标准定义了四个隔离级别:

  • READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
  • READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
  • REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • SERIALIZABLE(可串行化): 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。

隔离级别 脏读 不可重复读 幻读
READ-UNCOMMITTED √ √ √
READ-COMMITTED × √ √
REPEATABLE-READ × × √
SERIALIZABLE × × ×

MySQL 的默认隔离级别是什么?

MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)。我们可以通过SELECT @@tx_isolation;命令来查看,MySQL 8.0 该命令改为SELECT @@transaction_isolation;

1
2
3
4
5
6
sql复制代码mysql> SELECT @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+

这里需要注意的是:与 SQL 标准不同的地方在于 InnoDB 存储引擎在 REPEATABLE-READ(可重读) 事务隔离级别下使用的是 Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以说 InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读) 已经可以完全保证事务的隔离性要求,即达到了 SQL 标准的 SERIALIZABLE(可串行化) 隔离级别。

🐛 问题更正:MySQL InnoDB 的 REPEATABLE-READ(可重读)并不保证避免幻读,需要应用使用加锁读来保证。而这个加锁度使用到的机制就是 Next-Key Locks。

因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是 READ-COMMITTED(读取提交内容) ,但是你要知道的是 InnoDB 存储引擎默认使用 REPEATABLE-READ(可重读) 并不会有任何性能损失。

InnoDB 存储引擎在 分布式事务 的情况下一般会用到 SERIALIZABLE(可串行化) 隔离级别。

🌈 拓展一下(以下内容摘自《MySQL 技术内幕:InnoDB 存储引擎(第 2 版)》7.7 章):

InnoDB 存储引擎提供了对 XA 事务的支持,并通过 XA 事务来支持分布式事务的实现。分布式事务指的是允许多个独立的事务资源(transactional resources)参与到一个全局的事务中。事务资源通常是关系型数据库系统,但也可以是其他类型的资源。全局事务要求在其中的所有参与的事务要么都提交,要么都回滚,这对于事务原有的 ACID 要求又有了提高。另外,在使用分布式事务时,InnoDB 存储引擎的事务隔离级别必须设置为 SERIALIZABLE。

后记

最后再推荐一个非常不错的 Java 教程类开源项目:JavaGuide 。我在大三开始准备秋招面试的时候,创建了 JavaGuide 这个项目。目前这个项目已经有 100k+的 star,相关阅读:《1049 天,100K!简单复盘!》 。

对于你学习 Java 以及准备 Java 方向的面试都很有帮助!正如作者说的那样,这是一份:涵盖大部分 Java 程序员所需要掌握的核心知识的 Java 学习+面试指南!

相关推荐:

  • 图解计算机基础!
  • 阿里ACM大佬开源的学习笔记!TQL!
  • 计算机优质书籍搜罗+学习路线推荐!

我是 Guide哥,拥抱开源,喜欢烹饪。开源项目 JavaGuide 作者,Github:Snailclimb - Overview 。未来几年,希望持续完善 JavaGuide,争取能够帮助更多学习 Java 的小伙伴!共勉!凎!点击查看我的2020年工作汇报!

参考

  • 《高性能 MySQL》
  • www.omnisci.com/technical-g…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

持续发烧,聊聊Dart语言的并发处理,能挑战Go不?

发表于 2021-07-01

前言

貌似关于Dart的文章没流量啊,就算在小编关怀上了首页,看得人还是很少的。

算了,今天持续发烧,再来写写如何使用 Dart 语言的并发操作。说起并发操作,玩 Go 的同学该笑了,这就是我们的看家本领啊。玩 PHP 的同学继续看看,表示我们光看不说话。

代码演示之前,我们先假设一个场景。假设我有一些漂亮妹妹,他们要出门旅行了,旅行的时候会发照片给我。在这里个过程中,代码需要做的事情:

  1. 接收请求
  2. 安排出行计划,同时出行哦,不能有先后之分
  3. 他们各自出行,可以发照片给我
  4. 返回结果

这个过程中,我关心的是他们能不能处理好自己的事情,因为我妹妹太多了,如果都让我帮他们,累死我也搞不定啊,我就干两件事,安排好出行计划,最后收照片。

代码演示一下吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
dart复制代码import 'dart:io';
import 'dart:isolate';

main() async {
print(DateTime.now().toString() + ' 开始');

//这是一个接收端口,可以理解成我的手机
ReceivePort receivePort = new ReceivePort();

//安排三个妹妹出去旅行,让她们牢记我的手机号
await Isolate.spawn(travelToBeijing, receivePort.sendPort);
await Isolate.spawn(travelToShanghai, receivePort.sendPort);
await Isolate.spawn(travelToGuangzhou, receivePort.sendPort);

//我就在手机上,等待他们的消息
receivePort.listen((message) {
print(message);
});

print(DateTime.now().toString() + ' 结束');
}

void travelToBeijing(SendPort sendPort) {
sleep(Duration(seconds: 3));
sendPort.send(DateTime.now().toString() + ' 我是妹妹1,我在北京了');
}

void travelToShanghai(SendPort sendPort) {
sleep(Duration(seconds: 3));
sendPort.send(DateTime.now().toString() + ' 我是妹妹2,我在上海了');
}

void travelToGuangzhou(SendPort sendPort) {
sleep(Duration(seconds: 3));
sendPort.send(DateTime.now().toString() + ' 我是妹妹3,我在广州了');
}

上面写了那么多,都是啥意思呢,我自己稍微解释下上面的代码。

Dart 里的并发,用到的是 Isolate 类。

Isolate 翻译过来即是 隔离区, 是 Dart 实现并发的重要类。它并发的原理,既不是多进程,也不是多线程,你说类似于 Go 的协程吧,也不像。

真的很难定义它,期待对操作系统研究更深的同学来布道,当然这不影响我们使用。

Isolate.spawn 来定义一个并发任务,接收两个参数,第一个是该任务的处理函数,第二个是该处理函数的所需要的参数

ReceivePort 翻译一下 接收端口, 也可以翻译成 接收器,用来接收各个任务回传的消息。

receivePort.listen 用来监听各任务的回传信息,代码里只是简单打印

执行它,得到打印的结果

1
2
3
4
5
bash复制代码2021-07-01 15:40:38.132122 开始
2021-07-01 15:40:38.295683 结束
2021-07-01 15:40:41.200316 我是妹妹1,我在北京了
2021-07-01 15:40:41.248851 我是妹妹2,我在上海了
2021-07-01 15:40:41.296737 我是妹妹3,我在广州了

注意看打印结果,妹妹们很乖,基本在同一时间给我回复了消息。总共时间差不多3秒钟,你可以添加更多任务,都会是3秒钟。

再次封装一下

实际使用的时候,我们可以再次封装,使用的同学不用去想 Isolate, ReceivePort 都是什么鬼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
dart复制代码import 'dart:io';
import 'dart:isolate';

class MultiTask {
static void run(
{List<TaskItem> taskList,
Function taskFunc,
Function onCompletedItem,
Function onCompletedAll}) async {
ReceivePort receivePort = new ReceivePort();

Map<String, Isolate> mapParam = {};
for (int i = 0; i < taskList.length; i++) {
TaskItem taskItem = taskList[i];
mapParam[taskItem.key] = await Isolate.spawn(
taskFunc, TaskMessage(receivePort.sendPort, taskItem));
}

List<TaskRes> taskResList = [];
receivePort.listen((message) async {
TaskRes taskRes = message as TaskRes;
if (null != onCompletedItem) await onCompletedItem(taskRes);
taskResList.add(taskRes);
mapParam[taskRes.key].kill();
if (taskResList.length == taskList.length) {
receivePort.close();
if (null != onCompletedAll) await onCompletedAll(taskResList);
}
});
}
}

class TaskMessage {
SendPort sendPort;
TaskItem taskItem;
TaskMessage(this.sendPort, this.taskItem);
}

class TaskItem {
String key;
String param;

TaskItem(this.key, this.param);
}

class TaskRes {
String key;
String res;

TaskRes(this.key, this.res);
}

使用的时候,这样来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
dart复制代码import 'dart:io';
import 'dart:isolate';
import 'MultiTask.dart';

main() async {
List<TaskItem> taskList = [
TaskItem('1', 'param1'),
TaskItem('2', 'param2'),
TaskItem('3', 'param1'),
TaskItem('4', 'param2'),
TaskItem('5', 'param1'),
TaskItem('6', 'param2'),
TaskItem('7', 'param1'),
TaskItem('8', 'param2'),
TaskItem('9', 'param1'),
TaskItem('0', 'param2'),
];

print(DateTime.now());

MultiTask.run(
taskList: taskList,
taskFunc: taskFunc,
onCompletedItem: (TaskRes taskRes) =>
print(DateTime.now().toString() + '_' + taskRes.res),
onCompletedAll: (List<TaskRes> taskResList) =>
print(DateTime.now().toString() + '_onCompletedAll'),
);
}

void taskFunc(TaskMessage taskMessage) async {
sleep(Duration(seconds: 3));

String res = 'onCompletedItem is ok';

taskMessage.sendPort.send(TaskRes(taskMessage.taskItem.key, res));
}

走起来吧

1
2
3
4
5
6
7
8
9
10
11
12
yaml复制代码2021-07-01 15:50:54.862973
2021-07-01 15:50:57.924675_onCompletedItem is ok
2021-07-01 15:50:57.954982_onCompletedItem is ok
2021-07-01 15:50:57.986042_onCompletedItem is ok
2021-07-01 15:50:58.021282_onCompletedItem is ok
2021-07-01 15:50:58.053387_onCompletedItem is ok
2021-07-01 15:50:58.088492_onCompletedItem is ok
2021-07-01 15:50:58.121968_onCompletedItem is ok
2021-07-01 15:50:58.157117_onCompletedItem is ok
2021-07-01 15:50:58.190835_onCompletedItem is ok
2021-07-01 15:50:58.229044_onCompletedItem is ok
2021-07-01 15:50:58.241011_onCompletedAll

可以看到,中间每一个任务完成的时间,都很接近,并发处理很完美

总结

当需要处理很多任务时,可以开辟多个隔离区,并发执行,提高效率。

Dart语言对并发的处理,还算人性化,理解起来没有难度,用起来也容易。

同学们,骚起来吧。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

TypeScript RPC 框架 TSRPC 介绍:不多写

发表于 2021-07-01

TSRPC 是什么

TSRPC 是一个 TypeScript 的 RPC 框架,适用于浏览器 Web 应用、WebSocket 实时应用、NodeJS 微服务等场景。

GitHub:github.com/k8w/tsrpc

中文文档:tsrpc.cn

视频教程:www.bilibili.com/video/BV1hM…

目前,大多数项目仍在使用传统的 Restful API 进行前后端通信,这存在一些痛点。

  1. 依赖文档进行协议定义,前后端联调常被低级错误困扰(如字段名大小写错误,字段类型错误等)。
  2. 一些框架虽然实现了协议定义规范,但需要引入 Decorator 或第三方 IDL 语言。
  3. 一些框架虽然实现了类型校验,但无法支持 TypeScript 的高级类型,例如业务中常见的 Union Type:
1
2
3
4
5
6
7
8
9
ts复制代码// 用户信息
interface UserInfo {
// 来源渠道
from: { type: '老用户邀请', fromUserId: string }
| { type: '推广链接', url: string }
| { type: '直接进入' },
// 注册时间
createTime: Date
}
  1. JSON 支持的类型有限,例如不支持 ArrayBuffer,实现文件上传会非常麻烦。
  2. 请求和响应都是明文,破解门槛太低,字符串加密方式有限且强度不够。
  3. 等等…

我们无法找到一个能完美解决这些问题的现成框架,于是我们全新设计和创造了 TSRPC 。

概览

一个名为 Hello 的协议,从定义、实现到浏览器调用。

协议定义

直接使用 type 或 interface 定义协议,无需 Decorator 和第三方 IDL 语言。

1
2
3
4
5
6
7
ts复制代码export interface ReqHello {
name: string;
}

export interface ResHello {
reply: string;
}

服务端实现

运行时自动校验类型,请求参数一定类型安全。

1
2
3
4
5
6
7
ts复制代码import { ApiCall } from "tsrpc";

export async function ApiHello(call: ApiCall<ReqHello, ResHello>) {
call.succ({
reply: 'Hello, ' + call.req.name
});
}

客户端调用

跨项目复用协议定义,全程代码提示,不需要接口文档。

1
2
3
4
ts复制代码let ret = await client.callApi('Hello', {
name: 'World'
});
console.log(ret); // { isSucc: true, res: { reply: 'Hello, World' } }

code-hint.gif

特性

TSRPC 具有一些前所未有的强大特性,给您带来极致的开发体验。

  • 🥤 原汁原味 TypeScript
    • 直接基于 TypeScript type 和 interface 定义协议
    • 无需额外注释,无需 Decorator,无需第三方 IDL 语言
  • 👓 自动类型检查
    • 在编译时刻和运行时刻,自动进行输入输出的类型检查
    • 总是类型安全,放心编写业务代码
  • 💾 二进制序列化
    • 比 JSON 更小的传输体积
    • 比 JSON 更多的数据类型:如 Date, ArrayBuffer, Uint8Array 等
    • 方便地实现二进制加密
  • 🔥 史上最强大的 TypeScript 序列化算法
    • 无需任何注解,直接实现将 TypeScript 源码中的类型定义序列化
    • 首个也是目前唯一支持 TypeScript 高级类型的二进制序列化算法,包括:
      • Union Type
      • Intersection Type
      • Pick Type
      • Partial Type
      • Indexed Access Types
      • 等等
  • ☎ 多协议
    • 同时支持 HTTP / WebSocket
  • 💻 多平台
    • NodeJS / 浏览器 / App / 小程序
  • ⚡️ 高性能
    • 单核单进程 5000+ QPS 吞吐量(测试于 Macbook Air M1, 2020)
    • 单元测试、压力测试、DevOps 方案齐备
    • 经过数个千万用户级项目验证

兼容性

完全可以在 Server 端使用 TSRPC,同时兼容传统前端。

  • 兼容 JSON 形式的 Restful API 调用
    • 可自行使用 XMLHttpRequest、fetch 或其它 AJAX 框架以 JSON 方式调用接口
  • 兼容纯 JavaScript 的项目使用
    • 可在纯 JavaScript 项目中使用 TSRPC Client,也能享受类型检查和序列化特性
  • 兼容 Internet Explorer 10 浏览器
    • 浏览器端兼容至 IE 10 ,Chrome 30

运行时类型检测的实现原理

众所周知,TypeScript 的类型检测仅发生在编译时刻,这是因为类型信息(如 type、interface)会在编译时刻被抹除。而 TSRPC 竟然能在运行时刻也检测这些被抹除的类型信息?
况且 TypeScript 编译器有大几 MB,而 TSRPC 才几十 KB……

其实,这是因为我们遵循 TypeScript 类型系统,独立实现了一套轻量的类型系统,可以在运行时完成类型检测,甚至是二进制序列化。它支持了绝大多数常用的 TypeScript 类型。

支持的类型清单

上手试试

使用 create-tsrpc-app 工具,可以快速创建 TSRPC 项目。

1
shell复制代码npx create-tsrpc-app@latest

创建过程是交互式的,在菜单上选择相应的配置,即可轻松创建包含前后端的 TSRPC 全栈应用项目。

create-tsrpc-app.gif

如果你选择创建 HTTP 短连接服务,则会创建一个留言板的演示项目;如果选择 WebSocket 长连接服务,则会创建一个实时聊天室的演示项目。

参考资料

GitHub:github.com/k8w/tsrpc

中文文档:tsrpc.cn

视频教程:www.bilibili.com/video/BV1hM…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

几十行代码,修复马赛克表情包|Python 主题月

发表于 2021-07-01

本文正在参加「Python主题月」,详情查看活动链接

是不是每次偷表情包时,都感觉模糊感, 想找一个清楚点

先观察

  1. 模糊的地方就是有些小黑色点
  2. 只需去掉小黑点,就清楚了

 先看下效果

原图 修复后
src=http___5b0988e595225.cdn.sohucs.com_images_20190705_099f2e9f3718465aa3ec54bd94410558.jpeg&refer=http___5b0988e595225.cdn.sohucs.jpeg repair-929ef834-c0ac-483f-81b4-9db25598d85a.png
WX20210513-112540@2x.png repair-9cd7d9ff-c567-4f66-be6a-4102b540a862.png

 是不是看着还不错

想思路

  1. 如何去掉小黑点,拿到每个像素点
  2. 将小黑点变成白色的就OK了

去操作

  1. 观察每个像素值,找到小黑点的范围
* 范围大概在 200 - 230
![image.png](https://gitee.com/songjianzaina/juejin_p8/raw/master/img/288cdfffc5f6aecbc3250464b5d4ef5bddf03cae6243c1f20ce609b6db4b38cd)
  1. 找到眼睛、嘴 的颜色,防止吧眼也变白了
* 大概在 20-140
![image.png](https://gitee.com/songjianzaina/juejin_p8/raw/master/img/b9cee24504856940a1fe828b4c7861802e7a2306791dae57fe737e93307cbfcd)
  1. 遍历每个像素点
* 200 - 230 颜色加几十,变白一些,不能超过255
* 20 - 140 颜色减几十,变黑一些,不能设置0,会太黑了
  1. 代码思路
* 读取图片
* 遍历像素点
* 更具修复程度算出,变白或变黑

上代码

 代码能力有限,写的不咋地,有兴趣可以研究一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
python复制代码# !/usr/bin/python3
import threading
import uuid
from tkinter import *
from tkinter import filedialog, messagebox

import cv2
from PIL import ImageTk, Image


class MY_GUI():

def __init__(self, init_window_name):
self.init_window_name = init_window_name
self.out_frame = None
self.in_frame = None

# 绘制窗口
def set_init_window(self):
self.init_window_name.title("图片处理") # 窗口名
self.init_window_name.geometry('950x681+10+10')

self.input_label = Label(self.init_window_name, text="路径:")
self.input_label.place(x=0, y=0, width=80, height=30)
self.input_path = Entry(self.init_window_name)
self.input_path.place(x=80, y=0, width=850, height=30)

self.init_data_label = Label(self.init_window_name, text="修复前")
self.init_data_label.place(x=0, y=60, width=80, height=30)
self.result_data_label = Label(self.init_window_name, text="修复后")
self.result_data_label.place(x=550, y=60, width=80, height=30)

self.in_img_button = Button(self.init_window_name, text="选择图片", bg="lightblue", width=10,
command=self.select_in_frame)
self.in_img_button.place(x=400, y=100, width=150, height=30)

self.degree_label = Label(self.init_window_name, text="修复程度(1-10)")
self.degree_label.place(x=400, y=150, width=150, height=30)
self.degree_input = Spinbox(self.init_window_name, from_=1, to=10) # 调用内部方法 加()为直接调用
self.degree_input.place(x=400, y=180, width=150, height=30)

self.repair_button = Button(self.init_window_name, text="修复", bg="lightblue", width=10,
command=self.access_pixels)
self.repair_button.place(x=400, y=230, width=150, height=30)
self.d_img_button = Button(self.init_window_name, text="保存", bg="lightblue", width=10,
command=self.d_img)
self.d_img_button.place(x=400, y=280, width=150, height=30)

def select_in_frame(self):
path = filedialog.askopenfilename()
if path is None or path == '':
messagebox.showinfo("提示", "请选择图片")
return
self.input_path.delete(0, END)
self.input_path.insert(0, path)
frame = cv2.imread(path)
self.in_frame = frame

# 这里就修复图片的代码了
def access_pixels(self):
if self.in_frame is None:
messagebox.showinfo("提示", "请选择图片")
return
degreeStr = self.degree_input.get()
if degreeStr is None or degreeStr == '':
degreeStr = '0'

degree = int(degreeStr)
if degree < 1:
degree = 1
elif degree > 10:
degree = 10

frame = self.in_frame.copy()

print(frame.shape) # shape内包含三个元素:按顺序为高、宽、通道数
height = frame.shape[0]
weight = frame.shape[1]
channels = frame.shape[2]

showImgHeight = int(height / (weight / 400))
# 这里是转换rgb 不然 保存下来会变色
b1, g1, r1 = cv2.split(self.in_frame.copy())
rgb_img1 = cv2.merge([r1, g1, b1])
im = Image.fromarray(cv2.resize(rgb_img1, (400, showImgHeight)))
imgtk = ImageTk.PhotoImage(image=im)

self.before2_img = Label(self.init_window_name, image=imgtk).place(x=0, y=90, width=400,
height=showImgHeight)
print("weight : %s, height : %s, channel : %s" % (weight, height, channels))
for row in range(height): # 遍历高
for col in range(weight): # 遍历宽
for c in range(channels): # 便利通道
pv = frame[row, col, c]
# 这里是判断是不是小黑点
if pv > 23 * (11 - degree):
# 这里就是计算增加白色 原颜色+修复程度*10,防止太白 颜色都一样
if (pv + degree * 10) > 255:
frame[row, col, c] = 255
else:
frame[row, col, c] = (pv + degree * 10)
# 这里是判断黑色,是不是需要更黑一些
if pv < degree * 15:
frame[row, col, c] = 0

self.out_frame = frame
b, g, r = cv2.split(self.out_frame.copy())
rgb_img = cv2.merge([r, g, b])
im2 = Image.fromarray(cv2.resize(rgb_img, (400, showImgHeight)))
imgtk2 = ImageTk.PhotoImage(image=im2)
self.img2 = Label(self.init_window_name, image=imgtk2).place(x=550, y=90, width=400, height=showImgHeight)
self.img2.place()

def d_img(self):
if self.out_frame is None:
messagebox.showinfo("提示", "请先修复图片")
return
path = filedialog.askdirectory()
if path is None or path == '':
messagebox.showinfo("提示", "请选择输出路径")
return
savepath = path + "/repair-" + str(uuid.uuid4()) + ".png"
cv2.imwrite(savepath, self.out_frame)


if __name__ == '__main__':
init_window = Tk()
ZMJ_PORTAL = MY_GUI(init_window)
ZMJ_PORTAL.set_init_window()
init_window.mainloop()

 代码差不多了,看看效果吧,有不懂的可以评论问

j1.png

j2.png

j3.png

 效果还行,不适用于颜色复杂的表情包, 简单的颜色还是可以的

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…624625626…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%