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

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


  • 首页

  • 归档

  • 搜索

Swagger30官方starter诞生,可以扔掉那些野生

发表于 2020-10-02

原创:猿逻辑,欢迎分享,转载请保留出处。

Swagger是研发的好帮手,可以减少前后端的很多沟通成本。甚至在一些比较高级的公司,还能减少和测试人员的沟通成本。所以只要一个项目采用了SpringBoot框架,Swagger几乎是必选的组件。

可惜的是,Swagger只是一个工具。集成的时候,需要修改pom文件,增加两个jar包。还要做配置,运气不好的项目还需要修改WebMvcConfigurer中的内容。

在SpringBoot中,只要比较麻烦的事情,就可以写一个starter组件来解决。鉴于Swagger的这诸多原因,出现了许多野生的starter。看着项目中引入的,千奇百怪的jar包,洁癖的人心里总有一些不自然的。

我们来看一下,Swagger3.0在SpringBoot中是如何使用的。

在pom.xml追加starter依赖。

1
2
3
4
5
xml复制代码<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>

在浏览器中访问:

1
bash复制代码http://localhost:8080/swagger-ui/

即可拥有了文档功能(是的,不是swagger-ui.html了)。完了?完了。就是这么简单。有人说需要在主类上加入@EnableOpenApi注解,但其实是不需要的。

有哪些改变?

可以看到,Swagger3在SpringBoot中的配置,简单了不是一点点。更重要的是io.springfox这样的包名,看起来就高大上,让人不由自主的产生信任的感觉。

Swagger在3.0中做了如下的事:

  • 去掉了啰嗦的pom依赖,包括springfox-swagger2
  • 干掉了@EnableSwagger2注解,零配置
  • 去掉了不少依赖,比如guava,更清爽

其实,所有的事情都是在AutoConfig文件里做的,就像其他starter做的事情一样。从源码中,我们发现swagger和ui组件默认都是开启的。

springfox.documentation.enabled配置,可以一键关掉它。springfox.documentation.swagger-ui.enabled参数,可以控制ui的展示。

从Swagger的依赖中,我们看到了一个比较有意思的概念:openAPI。这玩意,竟然也有Specification了。可见,文档不仅仅在老掉牙的项目类公司,在互联网中也是痛点。

1
bash复制代码https://swagger.io/specification/

文章很长,我们暂且称specification为文档的文档 O_O。

关于认证

当然,变化也是有的。如果你的项目中用到了Spring Security这种权限控制组件,不要忘了添加白名单。类似于下面这种。

1
2
3
4
5
6
7
8
9
10
java复制代码String[] SWAGGER_WHITELIST = {
"/swagger-ui.html",
"/swagger-ui/*",
"/swagger-resources/**",
"/v2/api-docs",
"/v3/api-docs",
"/webjars/**"
};
httpSecurity.cors()
.antMatchers(SWAGGER_WHITELIST).permitAll()

背后的swagger地址,你访问v2也成,访问v3也成。反正我导入yapi、rap2这种API管理平台,都行得通。

集成到是变得简单了,但ApiOperation这种注解,还是一如既往的丑啊。

有时候,我们使用了JWT这样的认证方式,就需要在请求的时候,在Header构造一个token。

Swagger支持两种方式。

第一种,通过全局的Auth认证配置。

如上图,点击右上角的Auth按钮,可弹出对话框。

这个时候,你就需要搞一个SwaggerConfig文件了。下面是完整代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@Configuration
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.build()
.securitySchemes(security());
}
private List<SecurityScheme> security() {
ApiKey apiKey = new ApiKey("Authorization", "Authorization", "header");
return Collections.singletonList(apiKey);
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("mbye api")
.description("nothing here")
.version("1.0")
.build();
}
}

另外一种,就是在每次请求的时候,都需要手动输入一个token。类似于下面这种:

配置如下:

1
2
3
4
5
6
7
8
java复制代码private List<RequestParameter> globalRequestParameters() {
RequestParameterBuilder parameterBuilder = new RequestParameterBuilder()
.in(ParameterType.HEADER)
.name("Authorization")
.required(false)
.query(param -> param.model(model -> model.scalarModel(ScalarType.STRING)));
return Collections.singletonList(parameterBuilder.build());
}

使用下面的代码用起来就可以了。

1
java复制代码.globalRequestParameters(globalRequestParameters());

End

总之,整体感觉还是很不错的。可能是我的错觉,我觉得页面也流畅了不少。但由于新版本还是比较新,有不少细小的bug。比如Auth页面成功了,但在curl的请求参数里并没有值。

不过,瑕不掩瑜,swagger3还是值得一试。更何况,它的改动代价,几乎没有。

​

本文转载自: 掘金

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

MySql 不香了?为什么放弃MySql选择NewSql?

发表于 2020-10-01

最近与同行科技交流,经常被问到分库分表与分布式数据库如何选择,网上也有很多关于中间件+传统关系数据库(分库分表)与NewSQL分布式数据库的文章,但有些观点与判断是我觉得是偏激的,脱离环境去评价方案好坏其实有失公允。

本文通过对两种模式关键特性实现原理对比,希望可以尽可能客观、中立的阐明各自真实的优缺点以及适用场景。

NewSQL数据库先进在哪儿?

首先关于“中间件+关系数据库分库分表”算不算NewSQL分布式数据库问题,国外有篇论文pavlo-newsql-sigmodrec,如果根据该文中的分类,Spanner、TiDB、OB算是第一种新架构型,Sharding-Sphere、Mycat、DRDS等中间件方案算是第二种(文中还有第三种云数据库,本文暂不详细介绍)。基于中间件(包括SDK和Proxy两种形式)+传统关系数据库(分库分表)模式是不是分布式架构?我觉得是的,因为存储确实也分布式了,也能实现横向扩展。但是不是”伪”分布式数据库?从架构先进性来看,这么说也有一定道理。”伪”主要体现在中间件层与底层DB重复的SQL解析与执行计划生成、存储引擎基于B+Tree等,这在分布式数据库架构中实际上冗余低效的。为了避免引起真伪分布式数据库的口水战,本文中NewSQL数据库特指这种新架构NewSQL数据库。

NewSQL数据库相比中间件+分库分表的先进在哪儿?画一个简单的架构对比图:

  1. 传统数据库面向磁盘设计,基于内存的存储管理及并发控制,不如NewSQL数据库那般高效利用。
  2. 中间件模式SQL解析、执行计划优化等在中间件与数据库中重复工作,效率相比较低;
  3. NewSQL数据库的分布式事务相比于XA进行了优化,性能更高;
  4. 新架构NewSQL数据库存储设计即为基于paxos(或Raft)协议的多副本,相比于传统数据库主从模式(半同步转异步后也存在丢数问题),在实现了真正的高可用、高可靠(RTO<30s,RPO=0)
  5. NewSQL数据库天生支持数据分片,数据的迁移、扩容都是自动化的,大大减轻了DBA的工作,同时对应用透明,无需在SQL指定分库分表键。

这些大多也是NewSQL数据库产品主要宣传的点,不过这些看起来很美好的功能是否真的如此?接下来针对以上几点分别阐述下的我的理解。

分布式事务

这是把双刃剑。

CAP限制

想想更早些出现的NoSQL数据库为何不支持分布式事务(最新版的mongoDB等也开始支持了),是缺乏理论与实践支撑吗?并不是,原因是CAP定理依然是分布式数据库头上的紧箍咒,在保证强一致的同时必然会牺牲可用性A或分区容忍性P。为什么大部分NoSQL不提供分布式事务?

那么NewSQL数据库突破CAP定理限制了吗?并没有。NewSQL数据库的鼻主Google Spanner(目前绝大部分分布式数据库都是按照Spanner架构设计的)提供了一致性和大于5个9的可用性,宣称是一个“实际上是CA”的,其真正的含义是系统处于 CA 状态的概率非常高,由于网络分区导致的服务停用的概率非常小,究其真正原因是其打造私有全球网保证了不会出现网络中断引发的网络分区,另外就是其高效的运维队伍,这也是cloud spanner的卖点。详细可见CAP提出者Eric Brewer写的《Spanner, TrueTime 和CAP理论》。

推荐一篇关于分布式系统有趣的文章,站在巨人的分布式肩膀上,其中提到:分布式系统中,您可以知道工作在哪里,或者您可以知道工作何时完成,但您无法同时了解两者;两阶段协议本质上是反可用性协议。

完备性:

两阶段提交协议是否严格支持ACID,各种异常场景是不是都可以覆盖?2PC在commit阶段发送异常,其实跟最大努力一阶段提交类似也会有部分可见问题,严格讲一段时间内并不能保证A原子性和C一致性(待故障恢复后recovery机制可以保证最终的A和C)。完备的分布式事务支持并不是一件简单的事情,需要可以应对网络以及各种硬件包括网卡、磁盘、CPU、内存、电源等各类异常,通过严格的测试。之前跟某友商交流,他们甚至说目前已知的NewSQL在分布式事务支持上都是不完整的,他们都有案例跑不过,圈内人士这么笃定,也说明了分布式事务的支持完整程度其实是层次不齐的。

但分布式事务又是这些NewSQL数据库的一个非常重要的底层机制,跨资源的DML、DDL等都依赖其实现,如果这块的性能、完备性打折扣,上层跨分片SQL执行的正确性会受到很大影响。

性能

传统关系数据库也支持分布式事务XA,但为何很少有高并发场景下用呢?因为XA的基础两阶段提交协议存在网络开销大,阻塞时间长、死锁等问题,这也导致了其实际上很少大规模用在基于传统关系数据库的OLTP系统中。NewSQL数据库的分布式事务实现也仍然多基于两阶段提交协议,例如google percolator分布式事务模型, 采用原子钟+MVCC+ Snapshot Isolation(SI),这种方式通过TSO(Timestamp Oracle)保证了全局一致性,通过MVCC避免了锁,另外通过primary lock和secondary lock将提交的一部分转为异步,相比XA确实提高了分布式事务的性能。

SI是乐观锁,在热点数据场景,可能会大量的提交失败。另外SI的隔离级别与RR并非完全相同,它不会有幻想读,但会有写倾斜。

但不管如何优化,相比于1PC,2PC多出来的GID获取、网络开销、prepare日志持久化还是会带来很大的性能损失,尤其是跨节点的数量比较多时会更加显著,例如在银行场景做个批量扣款,一个文件可能上W个账户,这样的场景无论怎么做还是吞吐都不会很高。

Spanner给出的分布式事务测试数据

虽然NewSQL分布式数据库产品都宣传完备支持分布式事务,但这并不是说应用可以完全不用关心数据拆分,这些数据库的最佳实践中仍然会写到,应用的大部分场景尽可能避免分布式事务。

既然强一致事务付出的性能代价太大,我们可以反思下是否真的需要这种强一致的分布式事务?尤其是在做微服务拆分后,很多系统也不太可能放在一个统一的数据库中。尝试将一致性要求弱化,便是柔性事务,放弃ACID(Atomicity,Consistency, Isolation, Durability),转投BASE(Basically Available,Soft state,Eventually consistent),例如Saga、TCC、可靠消息保证最终一致等模型,对于大规模高并发OLTP场景,我个人更建议使用柔性事务而非强一致的分布式事务。关于柔性事务,笔者之前也写过一个技术组件,最近几年也涌现出了一些新的模型与框架(例如阿里刚开源的Fescar),限于篇幅不再赘述,有空再单独写篇文章。

解决分布式事务是否只能用两阶段提交协议?oceanbase1.0中通过updateserver避免分布式事务的思路很有启发性 ,不过2.0版后也变成了2PC。业界分布式事务也并非只有两阶段提交这一解,也有其它方案its-time-to-move-on-from-two-phase(如果打不开,国内有翻译版www.jdon.com/51588)

HA与异地多活

主从模式并不是最优的方式,就算是半同步复制,在极端情况下(半同步转异步)也存在丢数问题,目前业界公认更好的方案是基于paxos分布式一致性协议或者其它类paxos如raft方式,Google Spanner、TiDB、cockcoachDB、OB都采用了这种方式,基于Paxos协议的多副本存储,遵循过半写原则,支持自动选主,解决了数据的高可靠,缩短了failover时间,提高了可用性,特别是减少了运维的工作量,这种方案技术上已经很成熟,也是NewSQL数据库底层的标配。当然这种方式其实也可以用在传统关系数据库,阿里、微信团队等也有将MySQL存储改造支持paxos多副本的,MySQL也推出了官方版MySQL Group Cluster,预计不远的未来主从模式可能就成为历史了。

分布式一致性算法本身并不难,但具体在工程实践时,需要考虑很多异常并做很多优化,实现一个生产级可靠成熟的一致性协议并不容易。例如实际使用时必须转化实现为multi-paxos或multi-raft,需要通过batch、异步等方式减少网络、磁盘IO等开销。

需要注意的是很多NewSQL数据库厂商宣传基于paxos或raft协议可以实现【异地多活】,这个实际上是有前提的,那就是异地之间网络延迟不能太高。以银行“两地三中心”为例,异地之间多相隔数千里,延时达到数十毫秒,如果要多活,那便需异地副本也参与数据库日志过半确认,这样高的延时几乎没有OLTP系统可以接受的。

**数据库层面做异地多活是个美好的愿景,但距离导致的延时目前并没有好的方案。**之前跟蚂蚁团队交流,蚂蚁异地多活的方案是在应用层通过MQ同步双写交易信息,异地DC将交易信息保存在分布式缓存中,一旦发生异地切换,数据库同步中间件会告之数据延迟时间,应用从缓存中读取交易信息,将这段时间内涉及到的业务对象例如用户、账户进行黑名单管理,等数据同步追上之后再将这些业务对象从黑名单中剔除。由于双写的不是所有数据库操作日志而只是交易信息,数据延迟只影响一段时间内数据,这是目前我觉得比较靠谱的异地度多活方案。

另外有些系统进行了单元化改造,这在paxos选主时也要结合考虑进去,这也是目前很多NewSQL数据库欠缺的功能。

Scale横向扩展与分片机制

paxos算法解决了高可用、高可靠问题,并没有解决Scale横向扩展的问题,所以分片是必须支持的。NewSQL数据库都是天生内置分片机制的,而且会根据每个分片的数据负载(磁盘使用率、写入速度等)自动识别热点,然后进行分片的分裂、数据迁移、合并,这些过程应用是无感知的,这省去了DBA的很多运维工作量。以TiDB为例,它将数据切成region,如果region到64M时,数据自动进行迁移。

分库分表模式下需要应用设计之初就要明确各表的拆分键、拆分方式(range、取模、一致性哈希或者自定义路由表)、路由规则、拆分库表数量、扩容方式等。相比NewSQL数据库,这种模式给应用带来了很大侵入和复杂度,这对大多数系统来说也是一大挑战。

分库分表模式也能做到在线扩容,基本思路是通过异步复制先追加数据,然后设置只读完成路由切换,最后放开写操作,当然这些需要中间件与数据库端配合一起才能完成。

**这里有个问题是NewSQL数据库统一的内置分片策略(例如tidb基于range)可能并不是最高效的,因为与领域模型中的划分要素并不一致,这导致的后果是很多交易会产生分布式事务。**举个例子,银行核心业务系统是以客户为维度,也就是说客户表、该客户的账户表、流水表在绝大部分场景下是一起写的,但如果按照各表主键range进行分片,这个交易并不能在一个分片上完成,这在高频OLTP系统中会带来性能问题。

分布式SQL支持

常见的单分片SQL,这两者都能很好支持。NewSQL数据库由于定位与目标是一个通用的数据库,所以支持的SQL会更完整,包括跨分片的join、聚合等复杂SQL。中间件模式多面向应用需求设计,不过大部分也支持带拆分键SQL、库表遍历、单库join、聚合、排序、分页等。但对跨库的join以及聚合支持就不够了。NewSQL数据库一般并不支持存储过程、视图、外键等功能,而中间件模式底层就是传统关系数据库,这些功能如果只是涉及单库是比较容易支持的。NewSQL数据库往往选择兼容MySQL或者PostgreSQL协议,所以SQL支持仅局限于这两种,中间件例如驱动模式往往只需做简单的SQL解析、计算路由、SQL重写,所以可以支持更多种类的数据库SQL。

SQL支持的差异主要在于分布式SQL执行计划生成器,由于NewSQL数据库具有底层数据的分布、统计信息,因此可以做CBO,生成的执行计划效率更高,而中间件模式下没有这些信息,往往只能基于规则RBO(Rule-Based-Opimization),这也是为什么中间件模式一般并不支持跨库join,因为实现了效率也往往并不高,还不如交给应用去做。

这里也可以看出中间件+分库分表模式的架构风格体现出的是一种妥协、平衡,它是一个面向应用型的设计;而NewSQL数据库则要求更高、“大包大揽”,它是一个通用底层技术软件,因此后者的复杂度、技术门槛也高很多。

存储引擎

传统关系数据库的存储引擎设计都是面向磁盘的,大多都基于B+树。B+树通过降低树的高度减少随机读、进而减少磁盘寻道次数,提高读的性能,但大量的随机写会导致树的分裂,从而带来随机写,导致写性能下降。NewSQL的底层存储引擎则多采用LSM,相比B+树LSM将对磁盘的随机写变成顺序写,大大提高了写的性能。不过LSM的的读由于需要合并数据性能比B+树差,一般来说LSM更适合应在写大于读的场景。当然这只是单纯数据结构角度的对比,在数据库实际实现时还会通过SSD、缓冲、bloom filter等方式优化读写性能,所以读性能基本不会下降太多。NewSQL数据由于多副本、分布式事务等开销,相比单机关系数据库SQL的响应时间并不占优,但由于集群的弹性扩展,整体QPS提升还是很明显的,这也是NewSQL数据库厂商说分布式数据库更看重的是吞吐,而不是单笔SQL响应时间的原因。

成熟度与生态

分布式数据库是个新型通用底层软件,准确的衡量与评价需要一个多维度的测试模型,需包括发展现状、使用情况、社区生态、监控运维、周边配套工具、功能满足度、DBA人才、SQL兼容性、性能测试、高可用测试、在线扩容、分布式事务、隔离级别、在线DDL等等,虽然NewSQL数据库发展经过了一定时间检验,但多集中在互联网以及传统企业非核心交易系统中,目前还处于快速迭代、规模使用不断优化完善的阶段。相比而言,传统关系数据库则经过了多年的发展,通过完整的评测,在成熟度、功能、性能、周边生态、风险把控、相关人才积累等多方面都具有明显优势,同时对已建系统的兼容性也更好。对于互联网公司,数据量的增长压力以及追求新技术的基因会更倾向于尝试NewSQL数据库,不用再考虑库表拆分、应用改造、扩容、事务一致性等问题怎么看都是非常吸引人的方案。对于传统企业例如银行这种风险意识较高的行业来说,NewSQL数据库则可能在未来一段时间内仍处于探索、审慎试点的阶段。基于中间件+分库分表模式架构简单,技术门槛更低,虽然没有NewSQL数据库功能全面,但大部分场景最核心的诉求也就是拆分后SQL的正确路由,而此功能中间件模式应对还是绰绰有余的,可以说在大多数OLTP场景是够用的。

限于篇幅,其它特性例如在线DDL、数据迁移、运维工具等特性就不在本文展开对比。
总结
==

如果看完以上内容,您还不知道选哪种模式,那么结合以下几个问题,先思考下NewSQL数据库解决的点对于自身是不是真正的痛点:

  • 强一致事务是否必须在数据库层解决?
  • 数据的增长速度是否不可预估的?
  • 扩容的频率是否已超出了自身运维能力?
  • 相比响应时间更看重吞吐?
  • 是否必须做到对应用完全透明?
  • 是否有熟悉NewSQL数据库的DBA团队?

如果以上有2到3个是肯定的,那么你可以考虑用NewSQL数据库了,虽然前期可能需要一定的学习成本,但它是数据库的发展方向,未来收益也会更高,尤其是互联网行业,随着数据量的突飞猛进,分库分表带来的痛苦会与日俱增。当然选择NewSQL数据库你也要做好承担一定风险的准备。如果你还未做出抉择,不妨再想想下面几个问题:

  • 最终一致性是否可以满足实际场景?
  • 数据未来几年的总量是否可以预估?
  • 扩容、DDL等操作是否有系统维护窗口?
  • 对响应时间是否比吞吐更敏感?
  • 是否需要兼容已有的关系数据库系统?
  • 是否已有传统数据库DBA人才的积累?
  • 是否可容忍分库分表对应用的侵入?

如果这些问题有多数是肯定的,那还是分库分表吧。在软件领域很少有完美的解决方案,NewSQL数据库也不是数据分布式架构的银弹。相比而言分库分表是一个代价更低、风险更小的方案,它最大程度复用传统关系数据库生态,通过中间件也可以满足分库分表后的绝大多数功能,定制化能力更强。在当前NewSQL数据库还未完全成熟的阶段,分库分表可以说是一个上限低但下限高的方案,尤其传统行业的核心系统,如果你仍然打算把数据库当做一个黑盒产品来用,踏踏实实用好分库分表会被认为是个稳妥的选择。

最后

感谢大家看到最后,如文章有不足,欢迎大家在评论区支持,给予意见。如果觉得我的文章对你有帮助,那就给我一个赞同吧。

也欢迎大家关注我的公众号:程序员麦冬,每天都会分享java相关技术文章或行业资讯。欢迎大家关注和转发文章。

本文转载自: 掘金

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

在 React 中用 Threejs 实现 Web VR

发表于 2020-09-30

随着 Three.js 的更新迭代,实现 Web(支持PC 和 手机) 端 VR 全景看房已不再是难事。接下来让我们一起动手在 React 中去实践一下。笔者使用最新版 Three.js - v0.120.1 并且结合 React 和 Next.js 开发,可以更好地与生产项目相结合。

在线效果图 PC 和手机均可,或者手机扫码下方二维码。

VR 看房 Demo

网上虽然有一些关于 Web VR 看房的文章,但多数由于某些原因导致项目不能直接跑起来,或者不能直接应用到项目中。例如:有的是 Three.js 的版本比较老旧,有的是在HTML中通过 CDN 的方式引入Three.js。

需求分析及拆解

  • React 开发环境
  • Three.js 插件
  • VR 看房

开发环境

项目初始化

  • Next.js

用 Next.js 脚手架快手搭建项目:

--example blog-starter-typescript 指定项目模板,完整模板地址

1
2
sh复制代码npx create-next-app --example blog-starter-typescript vr-demo
cd vr-demo

安装 Three.js

three.js 包是老版本的,已经被废弃,大家要安装three

1
sh复制代码npm i three

代码实现

上面我们已经准备了开发环境,并且安装的相关依赖。接下来就是重点,实现 VR 全景看房。

全景

所为全景就是我们告别传统 2D 平面图片的形式,以 3D 的效果,把观察者置身其中,实现仿若其中的效果。

  • 优点:身临其境,效果更逼真,效果更佳。
  • 缺点:素材的准备,开发难度均较大。

素材准备

3D 效果其实就是立体效果,一个正方体有6个面,假设我们深处一个房间,把房间的6个面都用照片贴上,这样我们在房间无论看向何方,其实看到的都是贴在墙面的各个照片。

所以,我们选择一个立方体,然后在立方体的6个面贴上房间的6个面对应的照片,然后假想我们身处立方体中,此时我们在立方体中转身或者抬头、低头就是看到的全景效果。

接下来我们去用代码来实现:

Three.js 需要了解的核心知识点

用Three.js去创建3d世界,我们可以理解为拍摄一场舞台剧,需要舞台-场景(scene)、相机(camera)、导演-渲染器(renderer)、任务-网格(mesh)和光源-(light)。

  • 场景 scene:可以理解为舞台,一切都需要放在舞台上才可以进行拍摄。
  • 相机(camera):相当于摄像机或者人的眼睛,可以记录一切。
  • 渲染器(renderer):相当于导演或者控制器,一切指令都需要他来操控。
  • 网格(mesh):在计算机里,3D世界是由点组成的,无数的面拼接成各种形状的物体。这种模型叫做网格模型。一条线是两个点组成,一个面是3个点组成,一个物体由多个3点组成的面组成。
  • 光源(light):没有光是什么都看不到的,添加光源才可以看见效果。

Coding

  1. 创建场景 - Scene
1
2
3
ts复制代码import * as THREE from 'three';

scene = new THREE.Scene();
  1. 创建相机 - Camera

相机相当于人眼,有摄像机才可以看见场景里面的一切物体和光源。常用的相机是正交摄像机和透视摄像机

  • 正交摄像机是一个矩形可视区域,物体只有在这个区域内才是可见的物体无论距离摄像机是远或事近,物体都会被渲染成一个大小。一般应用场景是2.5d游戏如跳一跳、机械模型
  • 透视摄像机是最常用的摄像机类型,模拟人眼的视觉,近大远小(透视)。Fov表示的是视角,Fov越大,表示眼睛睁得越大,离得越远,看得更多。如果是需要模拟现实,基本都是用这个相机
1
2
3
4
5
6
7
8
9
10
11
12
13
ts复制代码// 创建相机
const w = window.innerWidth
const h = window.innerHeight

camera = new THREE.PerspectiveCamera(75, w / h, 0.1, 1000); // 透视相机

camera.position.set(0, -0, 0); // 设置相机位置
camera.lookPoint = {}; // 观察点
camera.lookPoint.x = 0;
camera.lookPoint.y = 0;
camera.lookPoint.z = -1;

camera.lookAt(camera.lookPoint.x, camera.lookPoint.y, camera.lookPoint.z);
  1. 创建渲染器 - Renderer
1
2
3
4
ts复制代码// 创造渲染器
renderer = new THREE.WebGLRenderer();
renderer.setSize(w, h);
app.appendChild(renderer.domElement);
  1. 创建网格- Mesh

而网格(mesh)由几何体(geometry)和材质(material)构成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ts复制代码const baseUrl = '/static/images/vr/';

// 立方纹理
const textureLoader = new THREE.CubeTextureLoader().setPath(baseUrl);

const arr = ['f.jpg', 'b.jpg', 'u.jpg', 'd.jpg', 'l.jpg', 'r.jpg']; // 6张纹理图依次贴在立方体的前、后、上、下、左、右的顺序放置的
const texture = textureLoader.load(arr); // 新版API

// 几何体
const geometry = new THREE.BoxGeometry(50, 50, 50);

// 材质
const material = new THREE.MeshPhongMaterial({
envMap: texture,
side: THREE.DoubleSide,
});

// 创建网格
const cube = new THREE.Mesh(geometry, material);

cube.position.set(-0, -0, -0);
scene.add(cube); // 加入场景
  1. 创建光源
1
2
3
ts复制代码//环境光
const ambient = new THREE.AmbientLight(0xffffff);
scene.add(ambient);
  1. 相机位置及朝向计算
1
2
3
4
5
6
7
8
9
10
11
12
13
ts复制代码const update = () => {
lat = Math.max(-85, Math.min(85, lat));
phi = THREE.Math.degToRad(90 - lat);
theta = THREE.Math.degToRad(lon);

target.x = 500 * Math.sin(phi) * Math.cos(theta);
target.y = 500 * Math.cos(phi);
target.z = 500 * Math.sin(phi) * Math.sin(theta);

camera.lookAt(target);

renderer.render(scene, camera);
};
  1. 动画渲染函数

用requestAnimationFrame 实现动画效果,优势:

  • 可以开启GPU渲染加速
  • 渲染无卡顿
1
2
3
4
5
ts复制代码const animate = () => {
renderer.render(scene, camera);
requestAnimationFrame(animate);
update();
};
  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
ts复制代码
const handleTouchStart = (e) => {
e.preventDefault();

interactiveFlag = true;

startX = e.touches[0].pageX;
startY = e.touches[0].pageY;
startLon = lon;
startLat = lat;
};

const handleTouchMove = (e) => {
e.preventDefault();
lon = (startX - e.touches[0].pageX) * 0.2 + startLon;
lat = (e.touches[0].pageY - startY) * 0.2 + startLat;
update();
};

const handleTouchEnd = (e) => {
e.preventDefault();

interactiveFlag = false;
};

总结

其实,从代码量来看,在react 中用 Three.js 实现 Web VR 全景看房并不复杂,关键是要具备一定的立体思维。

更多的 Three.js 和 Next.js 的使用请参考官网文档,

项目源码

完整的项目代码已经公布在 github ,方便大家调试并运行:

github.com/AlexShan200…

本文转载自: 掘金

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

【SpringBoot DB 系列】Jooq 之新增记录使用

发表于 2020-09-30

【SpringBoot DB 系列】Jooq 之新增记录使用姿势

接下来我们开始进入 jooq 的增删改查的使用姿势系列,本篇将主要介绍如何利用 jooq 来实现添加数据

I. 项目搭建

本项目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发

1. 项目依赖

关于如何创建一个 SpringBoot 的项目工程,不再本文的描述范围内,如有兴趣可以到文末的个人站点获取

在这个示例工程中,我们的选用 h2dabase 作为数据库(方便有兴趣的小伙伴直接获取工程源码之后,直接测试体验),因此对应的 pom 核心依赖如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xml复制代码<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jooq</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
</dependencies>

2. 数据库初始化

我们借助jooq-codegen-maven插件来自动生成数据库相关的代码,对这一段逻辑感兴趣的小伙伴可以参考博文:【DB 系列】Jooq 代码自动生成

后文中使用的表结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sql复制代码DROP TABLE IF EXISTS poet;

CREATE TABLE poet (
`id` int NOT NULL,
`name` varchar(20) NOT NULL default '',
CONSTRAINT pk_t_poet PRIMARY KEY (ID)
);

DROP TABLE IF EXISTS poetry;
CREATE TABLE poetry (
`id` int NOT NULL,
`poet_id` int NOT NULL default '0',
`title` varchar(128) not null default '',
`content` varchar(128) not null default '',
CONSTRAINT pk_t_poetry PRIMARY KEY (ID)
);

3. 配置文件

h2database 的连接配置如 application.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ini复制代码#Database Configuration
spring.datasource.url=jdbc:h2:~/h2-jooq-poet
spring.datasource.username=test
spring.datasource.password=
spring.datasource.driverClassName=org.h2.Driver


#jOOQ Configuration
spring.jooq.sql-dialect=H2


spring.datasource.initialization-mode=never
spring.datasource.continueOnError=true


##h2 web console设置
spring.datasource.platform=h2
#进行该配置后,h2 web consloe就可以在远程访问了。否则只能在本机访问。
spring.h2.console.settings.web-allow-others=true
#进行该配置,你就可以通过YOUR_URL/h2访问h2 web consloe
spring.h2.console.path=/h2
#进行该配置,程序开启时就会启动h2 web consloe
spring.h2.console.enabled=true

II. 新增记录

接下来我们进入正式的数据插入的使用姿势介绍,一般来说新增数据会区分单个和批量两种方式,下面我们分别进行介绍

1. Record 实体类新增方式

在 jooq 中,借助自动生成的 Record 类来实现新增是最简单的 case,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码
private static final PoetTB table = PoetTB.POET;
@Autowired
private DSLContext dsl;

/**
* 新增记录
*
* @param id
* @param name
* @return
*/
public boolean save(int id, String name) {
PoetPO record = dsl.newRecord(table);
record.setId(id);
record.setName(name);
return record.insert() > 0;
}

注意:

  • 实体类的创建方式:PoetPO record = dsl.newRecord(table);,不要直接 new 一个对象出来使用

2. 链式写法

下面介绍的这种写法和 sql 非常相似,也是我个人用的比较多的方式,特点就是一目了然

1
2
3
java复制代码public boolean save2(int id, String name) {
return dsl.insertInto(table).set(table.ID, id).set(table.NAME, name).execute() > 0;
}

3. InsertQuery 方式

上面两种写法比较常见,而直接使用 InsertQuery 的方式,在实际的业务开发中可能并没有上面的优雅,但某些特殊场景下还是很有用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码/**
* 不使用自动生成的代码来原生插入数据
*
* @param id
* @param name
* @return
*/
public boolean save3(int id, String name) {
// 当不使用自动生成的对象时,table可以用 DSL.table()指定,列可以用 DSL.field()指定
InsertQuery insertQuery = dsl.insertQuery(DSL.table("poet"));
insertQuery.addValue(DSL.field("id", Integer.class), id);
insertQuery.addValue(DSL.field("name", String.class), name);
return insertQuery.execute() > 0;
}

注意一下上面的用法,InsertQuery本身的使用没有什么值得说到的,重点在上面的实现中,并没有利用自动生成的代码,如

  • table: DSL.table(表名)
  • field: DSL.field(列名,类型)

通过上面的的 case,我们可以知道在不自动生成 DB 对应的代码前提下,如何进行数据库的操作

4. Record 实体批量保存

借助dsl.batchInsert来批量添加实体,属于最基础的使用姿势了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码private PoetPO bo2po(PoetBO bo) {
PoetPO po = dsl.newRecord(table);
po.setId(bo.getId());
po.setName(bo.getName());
return po;
}

/**
* 通过Record执行批量添加
*
* @param list
* @return
*/
public boolean batchSave(List<PoetBO> list) {
List<PoetPO> poList = list.stream().map(this::bo2po).collect(Collectors.toList());
int[] ans = dsl.batchInsert(poList).execute();
System.out.println(JSON.toJSONString(ans));
return true;
}

5. 链式批量保存

同样是类 sql 的链式插入方式,需要注意一下与前面的单条记录的链式插入的区别,下面这种写法和 sql 的批量插入的写法及其相似

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码/**
* 类sql写法,批量添加
*
* @param list
* @return
*/
public boolean batchSave2(List<PoetBO> list) {
InsertValuesStep2<PoetPO, Integer, String> step = dsl.insertInto(table).columns(table.ID, table.NAME);
for (PoetBO bo : list) {
step.values(bo.getId(), bo.getName());
}
return step.execute() > 0;
}

6. InsertQuery 批量保存

上面介绍了 InsetQuery 的单条插入方式,下面的批量写法基本上没有太大的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码/**
* 不基于自动生成的代码,来批量添加数据
*
* @param list
* @return
*/
public boolean batchSave3(List<PoetBO> list) {
InsertQuery insertQuery = dsl.insertQuery(DSL.table("poet"));
for (PoetBO bo : list) {
insertQuery.addValue(DSL.field("id", Integer.class), bo.getId());
insertQuery.addValue(DSL.field("name", String.class), bo.getName());
insertQuery.newRecord();
}

return insertQuery.execute() > 0;
}

7. 测试 case

接下来测试一下上面的 6 个方法执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public void test() {
this.save(11, "一灰");
this.save2(12, "一灰灰");
this.save3(13, "一灰灰Blog");


this.batchSave(Arrays.asList(new PoetBO(14, "yh"), new PoetBO(15, "yhh")));
this.batchSave2(Arrays.asList(new PoetBO(16, "yihui"), new PoetBO(17, "yihuihui")));
this.batchSave3(Arrays.asList(new PoetBO(18, "YiHui"), new PoetBO(19, "YiHuiBlog")));

RecordMapper<PoetPO, PoetBO> mapper =
dsl.configuration().recordMapperProvider().provide(table.recordType(), PoetBO.class);
List<PoetBO> result = dsl.selectFrom(table).fetch().map(mapper);
System.out.println(result);
}

输出结果如下

1
2
csharp复制代码[1,1]
[PoetBO (1, 李白), PoetBO (2, 艾可翁), PoetBO (11, 一灰), PoetBO (12, 一灰灰), PoetBO (13, 一灰灰Blog), PoetBO (14, yh), PoetBO (15, yhh), PoetBO (16, yihui), PoetBO (17, yihuihui), PoetBO (18, YiHui), PoetBO (19, YiHuiBlog)]

II. 其他

0. 项目

系列博文

  • 【SpringBoot DB 系列】Jooq 代码自动生成
  • 【SpringBoot DB 系列】Jooq 初体验

项目源码

  • 工程:github.com/liuyueyi/sp…
  • 项目源码:github.com/liuyueyi/sp…

1. 一灰灰 Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

  • 一灰灰 Blog 个人博客 blog.hhui.top
  • 一灰灰 Blog-Spring 专题博客 spring.hhui.top

一灰灰blog

本文转载自: 掘金

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

【业务分享】使用 vue-quill-editor 自定义图

发表于 2020-09-29

2022 年 2 月 14 日更新

  • 掘金的 markdown 解析规则可能改了,两年前的文章样式全崩了,现已修复

前言

近期接到的众多需求里,有一个需求挺有意思,为了完成它,头发又掉了不少…

废话不多说,先来介绍一下需求吧:要求用户在管理后台能编写微信公众号格式的文章,编写过程中要求保持格式,插入的图片这种,在第二行插入的绝不能在第三行显示…

  • 图片来源于网络,如有侵权请私信删除

具体可以参考 135 微信文章编辑

分析


讲真,这种需求之前还真没搞过,查了很多富文本编辑器之后,发现都比较难搞,而且很多富文本编辑器,demo 都出问题…

后来发现了 vue-quill-editor 非常不错,功能多且强大,但是 vue-quill-editor 上传图片是将图片转为 base64 编码,所以就用到了他的 自定义上传 的功能,我们可以利用他的自定义上传功能调取 element 的上传组件,完成我们的需求

接下来就是图片存在哪了,直接扔到 OSS 上,然后 数据库 存 URL,让前端加载去吧…

正常上传图片流程:

  1. 用户发送上传 Policy 请求到应用服务器
  2. 应用服务器返回上传 Policy 和签名给用户
  3. 用户直接上传数据到 OSS

But !!

由于公司没做内网穿透,设置上传回调就成了问题…

具体参考:help.aliyun.com/document_de…

其实倒也是有办法解决,但当时时间很紧迫,为了抓紧时间上线项目,只能变通一下咯,于是上面提到的流程变成了:

  1. 客户端上传图片至应用服务器
  2. 应用服务器直传 OSS
  3. 返回链接给客户端

没错,变成了服务端上传,这么搞其实有一些 风险 的,但考虑到时间紧,上线后后台又只有一个人管理,上传频率也不是很高,影响不是很大…

最终决定,使用 vue-quill-editor 的自定义上传调用 element upload,element upload 上传图片至服务端,服务端使用 Go 的 web 框架 Gin 转存至 OSS 上,成功上传后将图片地址返回给 element upload,element upload 再将地址回传给 vue-quill-editor

开发

确定好方案后,开始施工啦!

  • vue-quill-editor 安装与自定义上传
  • npm install vue-quill-editor --save
1
2
3
4
5
js复制代码import VueQuillEditor from 'vue-quill-editor'
Vue.use(VueQuillEditor);

import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'

前端逻辑

  • html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
html复制代码 <quill-editor
v-model="content"
:options="editorOption"
ref="QuillEditor">
</quill-editor>
<el-upload
:data=""
:multiple=""
:show-file-list=""
:on-success=""
class=""
drag
:http-request="uploadImg"
>
</el-upload>
  • el-upload 参数可以根据场景自己填写,关键点在于 http-request, 选中图片后会直接调用此事件
  • vue-quill-editor 各种配置
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
js复制代码   const toolbarOptions = [
['bold', 'italic', 'underline', 'strike'], // toggled buttons
['blockquote', 'code-block'],

[{'header': 1}, {'header': 2}], // custom button values
[{'list': 'ordered'}, {'list': 'bullet'}],
[{'script': 'sub'}, {'script': 'super'}], // superscript/subscript
[{'indent': '-1'}, {'indent': '+1'}], // outdent/indent
[{'direction': 'rtl'}], // text direction

[{'size': ['small', false, 'large', 'huge']}], // custom dropdown
[{'header': [1, 2, 3, 4, 5, 6, false]}],

[{'color': []}, {'background': []}], // dropdown with defaults from theme
[{'font': []}],
[{'align': []}],
['link', 'image', 'video'],
['clean'] // remove formatting button
]

export default {
data () {
return {
content: '',
editorOption: {
modules: {
toolbar: {
container: toolbarOptions, // 工具栏
handlers: {
'image': function (value) {
if (value) {
// 这里最重要
// 在编辑器中点击图片 icon 会触发此事件
// 需自己写方法触发 <el-upload>.click()
// 使用 <el-upload> 完成上传,在回调到
} else {
this.quill.format('image', false);
}
}
}
}
}
}
}
}
}
  • 由于我们使用 http-request 自定义了上传事件,上传完成后就可以将图片插入到 vue-quill-editor 中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
javascript复制代码uploadImg(res) {
axios.post('服务端地址', res.file, {
headers: {"content-type": "multipart/form-data"}
}).then(res => {
let quill = this.$refs.QuillEditor.quill
// 如果上传成功, 获取光标所在位置, 插入图片,res 为服务器返回的图片链接地址
if (res) {
let length = quill.getSelection().index;
quill.insertEmbed(length, 'image', res)
// 调整光标到最后
quill.setSelection(length + 1)
} else {
// fail...
}
})
}

后端逻辑

我们使用 Gin 来获取前端上传的图片并转存至 OSS 上

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
golang复制代码func UploadToOss(c *gin.Context) (url string, err error)  {
file, err := c.FormFile("file")
if err != nil {
return "", err
}

fileHandle, err := file.Open() //打开上传文件
if err != nil {
return "", err
}
defer fileHandle.Close()
fileByte, err := ioutil.ReadAll(fileHandle) //获取上传文件字节流
if err != nil {
return "", err
}

url, err = uploadOss(file.Filename, fileByte)
return url, err
}

func uploadOss(fileName string, fileByte []byte) (url string, err error) {
// oss 配置
endpoint := ""
accessKeyId := ""
accessKeySecret := ""
bucketName := ""
domain := ""

// 创建 OSS Client 实例
client, err := oss.New(endpoint, accessKeyId, accessKeySecret)
if err != nil {
return url,err
}

// 获取存储空间
bucket, err := client.Bucket(bucketName)
if err != nil {
return url,err
}

// 随机数防止文件重复
rand.Seed(time.Now().Unix())
randNum := rand.Int()
fileName = cast.ToString(randNum) + fileName

//上传阿里云路径
yunFileTmpPath := filepath.Join("OSS 存储路径", "可选参数") + "/" + fileName

// 上传Byte数组
err = bucket.PutObject(yunFileTmpPath, bytes.NewReader([]byte(fileByte)))
if err != nil {
return url,err
}
return domain + "/" + yunFileTmpPath ,nil
}

总结

第一次写文章,上述如有不清晰,不正确等信息还请多指教。

代码只写了必要逻辑,大家可以结合自己的实际场景处理结果。

本文转载自: 掘金

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

看完了MySQL小册,我为何强烈推荐?

发表于 2020-09-29

本文有福利相送,作为我鸽了这么久的歉意。

朋友们我又回来了,距离上次发文应该过了一个月了,期间有很多个人的琐事,导致我的文章不能正常的更新,给大家说声抱歉。

从上周开始吧,我开始看我在掘金很久以前买的一本小册:《从根上理解Mysql是怎么运行的》,本着买了会一半的原则,它一直在我的掘金里面吃灰。

直到最近,看到一些Java交流群里MVCC这个名词在我的时间线上反复出现,我知道这是提示我去好好梳理一下Mysql相关的知识了。

于是我花了一周的时间把这本小册从头到尾的好好看了一遍,有些章节可能反复看了四五遍,自认这本小册里的知识我搞懂了1/3,真的很惭愧,居然还不到一半,因为这个号称从根上理解Mysql的小册涉及到了很多概念,短短一周真的很难全部梳理清楚。

总体上来说,是一本干货满满的性价比好书,从行文内容上也可以看出作者的功底,值得二刷三刷。(这本书貌似准备在机工出版了,已经说了半年了,估计就一个月之内了~)

我不是来写软文,也不是搞推荐,很纯粹的分享一下最近看这本书的感受,这本书很适合会CRUD但对Mysq不甚了解的同学。

简单画一个导图给大家看一下这本书都讲了什么:

我将小册讲诉的所有内容分为七部分,接下来我将根据导图的各个部分说说我的看法。

InnoDB

大家如果查看过这本小册的目录,可以发现其前四章都是关于一些基础内容的,比如启动Mysql-Server,使用Mysql-Client进行连接和更改字符集什么的,所以这部分我并没有标在导图上,有一个大概了解就可以。

在这些基础之后,就开始了小册第一个硬菜:InnoDB原理的探究。

InnoDB作为我们最常用的Mysql存储引擎,作者对它的笔墨也是最多的,因为只有它支持事务,支持锁,所以在这部分讲述的很多内容都和后面的部分相呼应。

作者从一条记录说起,给我们娓娓道来了InnoDB内部的数据管理方式:

一条数据库表记录会以一种叫记录格式的方式存储,以5.7为例列举了MySQL中四种行格式的不同组成结构及其特性。

记录格式说完之后,又要说说记录格式放在哪了?记录格式放在一个名为页的数据结构上,MySQL中有很多不同的页用以不同的用处。

每个页是16KB大小,MySQL在读取数据或者持久化数据的时候都是以页为基本单位,也就是说一次性最小会读取或持久化16KB数据。

每次需要写入数据时,都会向对应的数据页申请内存,直至填满之后会申请下一个页,如果是删除数据则会标记为删除,但是数据被删除并不会移除此页,而是被删除的数据组成一个垃圾链表,等待新的数据来将其覆盖。

那么页说完了,页又从属于谁呢?这时又引入了表空间的概念。

一个表对应一个表空间文件(表名.ibd),这个表空间文件里就是实际存放了数据的文件,数据记录被页管理,页又被表空间管理,这是一个层层递进的从属关系。

但是因为用表空间直接管理页可以跨度太大(一个表空间对应所有表数据,但是一个页才16KB,数据稍微大一点,就是一个表空间对应上万甚至几十万的页),所以表空间里面又进行分层:区和组。

64个页对应一个区(64*16=1MB),256个区又对应一个组(256MB),这样划分之后一个逻辑从属关系就变成了:记录->数据页->区->组->表空间。

同时为了加快加载速度,几乎所有系统都会用到的缓存就被引入了-缓存池。

缓存池的引入大大提高了数据页查询的效率,毕竟磁盘IO太太太慢了,缓存池会在MySQL启动时申请一块内存作为数据页的缓存池(数据库优化的时候可以谈谈这个思路),同时根据算法来较好的控制缓存池里的数据(最常访问啊什么的,这块蛮复杂的)。

至此,InnoDB的基础原理部分基本就是结束了,这部分我看下来最复杂的就是表空间部分,因为涉及到诸如索引之类的东西,可以说表空间这节是整本小册最难的,我看小册评论区也都是如此想法。

索引

看小册的时候本来就是抱着看索引的想法去的,但是没想到索引的篇幅很短,只有两章:索引原理和索引用法。

其实看小册的时候,我一般都是对照着看的,看完作者写的再去网上看看别人写的,两者结合起来去理解。

索引这块也是如此,但是作者写的真是蛮通俗易懂的,索引就是在行记录的基础上进行主键排序,因为主键是有序的所以用主键作为节点构成一颗B+树,这颗B+树的叶子节点就是所有的记录,非叶子节点是一种名叫目录树的东西,可以理解为对叶子节点进行分组用的,因为是B+树,所以使用二分查找可以很快的查找到对应节点。

这种非叶子节点直接是行记录数据的索引被称为聚簇索引,这种索引会在InnoDB中自动生成,且因为它叶子节点是所有的记录,所以它也是很占空间的东西。

我们手动进行生成的索引叫做二级索引/辅助索引,这种索引生成后也会构成一个B+树,但是此树叶子节点并不记录行数据,而是记录对应行数据的主键,当你用二级索引拿到主键后,还要用主键重新搜索一次数据,这个过程叫做回表。

如果创建二级索引时指定了多个列,这种索引叫做覆盖索引。

除了这些作者还顺带讲了MyISAM的索引,它没有聚簇索引都是二级索引,但二级索引和InnoDB的又有些不同,MyISAM的索引中记录的不是主键而是行号,可以通过对行号直接找到对应数据,这样就少了一次回表过程。


懂了索引之后,再去看索引用法的注意事项就简单多了~,比如:

  1. 索引列的类型尽量小(减少建立索引耗费的空间)
  2. 删除表中的重复和冗余索引(索引列只会用到一个索引)
  3. 数据库主键自增(避免主键无序,这样会频繁打乱B+树的节点,因为B+树依靠主键排序)
  4. where 条件尽量索引字段的顺序和创建联合索引时字段顺序一致(因为覆盖索引创建是按照多个索引字段值从左到右进行排序的)
  5. 模糊匹配指定左前缀可以用到索引(因为索引值排序时是按照从左到右,所以如果左侧值指定了也可以用到索引缩小范围)

还有很多其他的注意事项,这块网上基本说的都够了,我这里不再赘述,避嫌。

单表的访问方式

单表的访问方式,咋一听很难理解的一个名词,其实是MySQL的设计者给不同的查询设立了不同的叫法,比如用主键查询速度非常快,这种叫const,意为常量级查询,复杂度可以忽略掉。

这些都是一些知道了解的东西,因为算是一种规定,同时这部分是Explain的铺垫,Explain会用到这里面的知识。

Explain

Explain的知识网上博客讲的也很清楚了,因为都是一些概念性的知识,我以前学MySQL的时候看的周阳的课,里面也有讲到Explain相关知识。

它主要是列出了一条查询语句都走了哪些索引,影响了多少行数,个人觉得其实效用不大,因为一条SQL交给执行器去执行之后,会有优化器进行优化,优化过的SQL可能已经和原来你编写的不一样了,需要了解优化器优化过程才能看懂这个。

这种情况你可以使用optimizer_trace去查看SQL的执行过程来知道优化器都做了哪些事。

也可以用其他方式比如:show status like '%last_query_cost%'

这条语句可以查询上条查询语句使用了多少成本,在SQL优化器对一个语句进行优化时会尝试计算不同方案花费了多少成本,最终使用最少成本的那个优化方案,我觉得这种方式可以比较直观的对比自己编写的两个SQL的性能差距。

子查询优化

在优化器优化这块,小册主要讲了优化器进行优化的一些方案,不过主要讲的还是子查询的优化方案。

事务

InnoDB基础,索引,事务和锁,可以说是看完这本小册必须要了解的四大块了。

事务里的概念很多,ACID就不说了,既然是讲原理的书,那作者的笔墨就主要用在MySQL用了什么方案来保证事务和事务的回滚。

MySQL使用redo日志来记录事务执行中的语句,用undo日志记录语句执行前记录的值,同时每个事务都有一个全局ID。

redo日志可以保证系统哪怕崩溃了,重启回来之后还能根据里面的日志恢复到原样,undo则可以保证需要回滚时有对应的记录,undo还有一个作用,就是用来做MVCC。

MVCC(Mutil-Version Concurrency Control)-多版本并发控制,它是MySQL中用来解决并发读写的问题的一种方案。

在高并发的程序中,数据库往往会同时开启多个事务,执行过程中多个事务交替执行,这就可能会出现事务并发问题:脏写,脏读,不可重复读和幻读。

这里我们再来复习一下这四个概念:

  1. 脏写:一个事务修改了另一个未提交事务修改过的数据
  2. 脏读:一个事务读到了另一个事务未提交修改过的数据
  3. 不可重复读:一个事务能读到此事务开启后其他事务提交修改的值
  4. 幻读:一个事务中两条同样的查询条件进行查询时,后读到的数据比前一次多

这四个问题都可以通过锁来解决,但是这样的话性能必然是会下降的,所以在MySQL中选择这样做:

脏写一般是由锁来完成的,也就是一个时间点只有一个事务对一条记录进行写,避免脏写。

脏读,不可重复读,幻读则可以由MVCC来控制,增强MySQL的并发性能,其原理是通过undo日志维护了一个版本链,版本链每个版本上面都有一个事务id来标识这个版本是由哪个事务修改的,当前事务进行查询时会根据当前事务id进行计算可以看到这个版本链上的哪些记录,从而去避免脏读,不可重复读和幻读。

网上有些解读MVCC的时候说这是版本链+乐观锁,看了小册之后我在网上翻到了MySQL中关于这块的源码,和小册上面讲诉的一样,是通过计算(或者说判断)来决定能看到哪个版本,乐观锁一般是比较新值和旧值,实际情况和乐观锁不太相像。

事务这块看完之后,发现数据库常问的MVCC也没有那么神秘,可以说是一句话就能说清的事。而且后来我忽然想起来seata这个分布式事务的AT模式,它的原理实现其实就和MySQL里面的事务原理几乎是一样的。

锁

有了前面的铺垫之后,看锁这一章会简单许多,但是小册上面的锁这一章仅仅是介绍了锁的分类,也就是说平常MySQL在用锁的时候一般会用到哪些锁,他们分别有什么特性,并没有去深入的去分析两个事务同时修改一条记录的情况下锁是怎么做的。

但是并不是说作者没写,而是写在公众号里面了,,,关注公众号之后可以去看有三篇加锁实战分析的文章。

锁其实可以通过两个维度来划分:粒度和特性。(当然这是我自己的理解)

  • 粒度:根据影响的范围可以分为行级和表级。
  • 特性:根据锁的特性可以分为独占或共享。

两个维度组合一下一般就是四种:行级独占锁、行级共享锁、表级独占锁和表级共享锁。

除了这几种锁的具体实现,还介绍了锁在内存中的结构,虽然我前面说了这章很重要,但我自己也没有太过于细看,因为都是概念性需要记忆的东西。

其实这章结合公众号的加锁实战分析的文章是比较好的,这章就像是锁实战的先行知识,不然你直接去看锁实战,各种概念一上来恐怕给人搞得头都大了。

小结

这篇文章给我写的非常尴尬,因为有很多想多写出来的知识又不能写(文章内容最多只能由30%知识来源于小册),所以我本来是想从逻辑上详细给大家讲讲这本小册的内容,但是又得避嫌(侵权啊,抄袭啊),最后就感觉有点虎头蛇尾了(极力推荐查看我的导图)。

不过像高频率的一些知识点,我还是写到的了,比如索引原理和事务原理(不过有点简化),大部分开发应该是都没有这个知识深度,如果能好好记忆下里面的内容,面试的时候其实可以往深了说,能把一点说深总比你说一堆浅显易懂的知识点来的有用。

总体来说这本小册还是比较推荐的,是一本深挖原理的书,有些知识点比如bin log这种偏应用层的没有讲到,其他的基本上都是很详细了,用一下小册优惠码应该不到20块钱,性价比很高。

文章开头也说了,本文有一点点福利,给大家抽三个小册六折码(非赞助,都是我之前参加征文活动攒的),鉴于上次抽奖的教训(因为没有私信,迟迟联系不上中奖者),这次打算转移到公号去抽奖。

  • 只需关注我的同名公众号(和耳朵),回复抽奖就有专属的抽奖连接,十月一日中午十二点开奖。
  • 还有我的MySQL思维导图文件,回复MySQL关键字就能获取脑图。

好了,今天的文章就先到这,欢迎大家点赞支持,我是和耳朵,知识输出,共同成长。

本文转载自: 掘金

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

zuihou-admin-cloud 微服务开发平台 功能点

发表于 2020-09-28

本项目是基于SpringCloud + Vue 的 SaaS型微服务开发平台,其中的SaaS功能通过插件方式提供, 禁用时,变成普通的开发框架, 启用时,有3种租户模式选择. 具备用户管理、资源权限管理、网关统一鉴权、XSS防跨站攻击、自动代码生成、多存储系统、分布式事务、分布式定时任务等多个模块,支持多业务系统并行开发,支持多服务并行开发,可以作为后端服务的开发脚手架。代码简洁,架构清晰,非常适合学习和实际项目中使用。核心技术采用Nacos、Fegin、Ribbon、Zuul、Hystrix、JWT Token、Mybatis、SpringBoot、Seata、Sentinel、RabbitMQ、FastDFS等主要框架和中间件。

希望能努力打造一套 基础开发框架 - 分布式微服务架构 - 持续集成 - 自动化部署 - 系统监测 的解决方案。

另本项目旨在实现基础能力,不涉及具体业务,项目中很多配置都严格按照开发+生产级动态切换的设计思路,所以很多配置比较绕,需要对框架有一定的了解才能理解,努力达到:开发环境效率第一、生产环境的安全&性能第一!。

租户模式:

她既是一套SaaS基础开发平台,也是一套非SaaS基础开发平台,一套代码,实现4种租户模式,只需要对配置文件做一个改动,既能实现4中模式的

租户模式 描述 优点 缺点 分布式事务
NONE(非租户模式) 没有租户 简单、适合独立系统 缺少租户系统的便利性 支持
COLUMN(字段模式) 租户共用一个数据库,在业务表中增加字段来区分 简单、不复杂、开发无感知 数据隔离性差、安全性差、数据备份和恢复困难 支持
SCHEMA(独立schema) 每个租户独立一个 数据库(schema),执行sql时,动态在表名前增加schema 简单、开发无感知、数据隔离性好 配置文件中必须配置数据库的root账号、不支持复杂sql和 sql嵌套自定义函数 支持
DATASOURCE(独立数据源) 每个租户独立一个 数据库(数据源),执行代码时,动态切换数据源 可独立部署数据库,数据隔离性好、扩展性高、故障影响小 相对复杂、开发需要注意切换数据源时的事务问题、需要较多的数据库 支持

功能点介绍:

服务鉴权:

通过JWT的方式来加强服务之间调度的权限验证,保证内部服务的安全性。

监控:

利用Spring Boot Admin 来监控各个独立Service的运行状态;利用turbine来实时查看接口的运行状态和调用频率;通过Zipkin来查看各个服务之间的调用链等。

数据权限:

利用基于Mybatis的DataScopeInterceptor拦截器实现了简单的数据权限

SaaS的无感解决方案:

使用Mybatis拦截器实现对所有SQL的拦截,COLUMN模式动态拼接租户编码,SCHEMA模式修改默认的Schema,DATASOURCE模式动态切换数据源,从而实现多租户数据隔离的目的。并且支持禁用租户模式。

二级缓存:

采用J2Cache操作缓存,第一级缓存使用内存(Caffeine),第二级缓存使用 Redis。 由于大量的缓存读取会导致 L2 的网络成为整个系统的瓶颈,因此 L1 的目标是降低对 L2 的读取次数。 该缓存框架主要用于集群环境中。单机也可使用,用于避免应用重启导致的缓存冷启动后对后端业务的冲击。

优雅的Bean转换:

采用Dozer组件来对 DTO、DO、PO等对象的优化转换

前后端统一表单验证:

严谨的表单验证通常需要 前端+后端同时验证, 但传统的项目,均只能前后端各做一次检验, 后期规则变更,又得前后端同时修改。 故在hibernate-validator的基础上封装了zuihou-validator-starter起步依赖,提供一个通用接口,可以获取需要校验表单的规则,然后前端使用后端返回的规则, 以后若规则改变,只需要后端修改即可。

防跨站脚本攻击(XSS):

  • 通过过滤器对所有请求中的 表单参数 进行过滤
  • 通过Json反序列化器实现对所有 application/json 类型的参数 进行过滤

当前用户信息注入器:

通过注解实现用户身份注入

在线API:

由于原生swagger-ui某些功能支持不够友好,故采用了国内开源的 knife4j ,并制作了stater,方便springboot用户使用。

代码生成器:

基于Mybatis-plus-generator自定义了一套代码生成器, 通过配置数据库字段的注释,自动生成枚举类、数据字典注解、SaveDTO、UpdateDTO、表单验证规则注解、Swagger注解等。

定时任务调度器:

基于xxl-jobs进行了功能增强。(如:指定时间发送任务、执行器和调度器合并项目、多数据源)

大文件/断点/分片续传:

前端采用webupload.js、后端采用NIO实现了大文件断点分片续传,启动Eureka、Zuul、File服务后,直接打开docs/chunkUploadDemo/demo.html即可进行测试。 经测试,本地限制堆栈最大内存128M启动File服务,5分钟内能成功上传4.6G+的大文件,正式服耗时则会受到用户带宽和服务器带宽的影响,时间比较长。

分布式事务:

集成了阿里的分布式事务中间件:seata,以 高效 并且对业务 0侵入 的方式,解决 微服务 场景下面临的分布式事务问题。

灰度发布:

为了解决频繁的服务更新上线,版本回退,快速迭代,公司内部协同开发,本项目采用修改ribbon的负载均衡策略来实现来灰度发布。

关联数据自动注入:

用于解决跨表、跨库、跨服务分页数据的属性或单个对象的属性 回显关联数据之痛, 支持对静态数据属性(数据字典)、动态主键数据进行自动注入。

架构图

系统一览

本文转载自: 掘金

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

Kotlin StateFlow 搜索功能的实践 DB +

发表于 2020-09-27

前言

在之前分享过一篇文章 Google 推荐在 MVVM 架构中使用 Kotlin Flow ,在这篇文章中分析了如何在 MVVM 架构中使用 Kotlin Flow,以及 Kotlin Flow 为我们解决了以下问题:

  • LiveData 是一个生命周期感知组件,最好在 View 和 ViewModel 层中使用它,如果在 Repositories 或者 DataSource 中使用会有几个问题
+ 它不支持线程切换,其次不支持背压,也就是在一段时间内**发送**数据的速度 > **接受**数据的速度,LiveData 无法正确的处理这些请求
+ 使用 LiveData 的最大问题是所有数据转换都将在主线程上完成
  • RxJava 虽然支持线程切换和背压,但是 RxJava 那么多傻傻分不清楚的操作符,实际上在项目中常用的可能只有几个例如 Observable 、 Flowable 、 Single 等等,如果我们不去了解背后的原理,造成内存泄露是很正常的事,大家可以从 StackOverflow 上查看一下,有很多因为 RxJava 造成内存泄露的例子
  • RxJava 入门的门槛很高,学习过的朋友们,我相信能够体会到从入门到放弃是什么感觉
  • 解决回调地狱的问题

而相对于以上的不足,Flow 有以下优点:

  • Flow 支持线程切换、背压
  • Flow 入门的门槛很低,没有那么多傻傻分不清楚的操作符
  • 简单的数据转换与操作符,如 map 等等
  • Flow 是对 Kotlin 协程的扩展,让我们可以像运行同步代码一样运行异步代码,使得代码更加简洁,提高了代码的可读性
  • 易于做单元测试

而这篇文章主要来分析一下 PokemonGo 搜索功能的实践,主要包含以下几个方面的内容:

  • Kotlin Flow 是什么?以及如何使用?
  • 如何区分末端操作符还是中间操作符?
  • Kotlin Channel 是什么?以及如何使用?
  • Kotlin Channel 都有那几种类型?
  • BroadcastChannels 是什么?以及如何在项目中使用?
  • StateFlow 是什么?以及如何在项目中使用?
  • Kotlin 常用操作符 debounce 、filter 、flatMapLatest 、 distinctUntilChanged 解析?

之前有很多朋友跟我反馈,如何使用 Flow 实现搜索功能,所以我在 PokemonGo 项目中增加了两种搜索场景,分别演示 BroadcastChannels 和 StateFlow 的用法。

  • 使用 ConflatedBroadcastChannel 实现 DB 搜索
  • 使用 StateFlow 实现 NetWork 搜索

在分析这两种实现方式之前,需要先了解几个基本概念, Flow 和 Channel 是什么,以及常用的操作符 debounce 、filter 、flatMapLatest 、 distinctUntilChanged 等等的使用,Flow 和 Channel 是一个比较大的概念,后面我会花好几篇文章来分析它们,本文只会概述它们之间的区别。

Kotlin Flow 是什么

先来看看 Kotlin 官方文档是如何介绍 Flow

将上面这段话,简单的总结一下:

  • Flow 是非阻塞的,以挂起的方式执行,只有遇到末端操作符,才会触发所有操作的执行
  • 所有操作都在相同的代码块内顺序执行
  • 发射出来的值都是顺序执行的,只有在某一时刻结束(遇到 末端操作符 或者出现异常)
  • map , filter , take , zip 等等是中间操作符,collect , collectLatest , single , reduce , toList 等等末端操作符
  • 中间操作符构建了一个待执行的调用链,如下图所示:

不阻塞,以挂起的方式执行 :也就是协程作用域被挂起, 当前线程中协程作用域之外的代码不会阻塞

接下来我们来看一段示例:

1
2
3
4
5
6
7
kotlin复制代码suspend fun printValue() = flow<Int> {
for (index in 1..10) {
emit(index)
}
}.map { it -> it * it } // map, filter, take, zip 等等是中间操作符
.filter { it -> it > 5 }
.toList() // 只有遇到末端操作符 collect, collectLatest,single, reduce, toList 等等才会触发所有操作的执行
  • 遇到中间操作符,并不会执行任何操作,也不会挂起函数本身,这些操作符构建了一个待执行的调用链
  • 末端操作符是可挂起函数,遇到末端操作符会触发所有操作的执行

如何区分末端操作符还是中间操作符

区分末端操作符还是中间操作符,可以按照是否是挂起函数来区分,我个人觉得按照挂起函数来区分,方便去记忆上面提到的 Flow 的几个特点,当然也可以按照其他方式来区分,我们一起来分析一下源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码// 中间操作符是 Flow 的扩展函数,它们最后都是通过 emit 来发射数据
public inline fun <T> Flow<T>.filter(crossinline predicate: suspend (T) -> Boolean): Flow<T> = transform { value ->
if (predicate(value)) return@transform emit(value)
}

// 末端操作符是一个挂起函数
// 末端操作符无论是 collectLatest,single, reduce, toList 最后都是调用 collect
public suspend fun <T> Flow<T>.toList(destination: MutableList<T> = ArrayList()): List<T> = toCollection(destination)

public suspend fun <T, C : MutableCollection<in T>> Flow<T>.toCollection(destination: C): C {
collect { value ->
destination.add(value)
}
return destination
}
  • 中间操作符是 Flow 的扩展函数,它们最后都是通过 emit 来发射数据
  • 末端操作符是一个挂起函数
  • 末端操作符无论是 collectLatest , single , reduce , toList 最后都是调用 collect

Kotlin Channel 是什么

来看看 Kotlin 官方文档是如何介绍 Channel

Channel

将上面这段话,简单的总结一下:

  • Channel 是非阻塞的,它用于发送方 (SendChannel) 和接收方 (ReceiveChannel) 之间通信
  • Channel 实现了 SendChannel 和 ReceiveChannel 接口,所以既可以发送数据又可以接受数据
  • Channel 和 Java 中的 BlockingQueue 类似,不同之处在于 BlockingQueue 是阻塞的,而 Channel 是挂起的
  • 发送方 (SendChannel) 和接收方 (ReceiveChannel) 之间通过缓冲区进行同步的,如下图所示:

  • 通过发送方 (SendChannel) 将数据发送到缓冲区
  • 通过接收方 (ReceiveChannel) 从缓冲区获取数据
  • 发送方 (SendChannel) 和 接收方 (ReceiveChannel) 之间有一个通道,也就是缓冲区
  • 缓冲区的作用帮我们同步发送方 (SendChannel) 和 接收方 (ReceiveChannel) 发送和接受的数据,也就意味着多个协程可以向同一个 channel 发送数据, 一个 channel 的数据也可以被多个协程接收

我们来实现一个简易的消息发送和接受的例子:

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
kotlin复制代码val channel = Channel<Int>()
// 接受消息
suspend fun receiveEvent() {
coroutineScope {
while (!channel.isClosedForReceive) {

// receive()方法异步获取元素,如果缓冲区是空,receive() 调用者将被挂起,直到一个新值被发送到缓冲区
// receive() 是一个挂起函数,用于同步发送方和接收方的一种机制
channel.receive()

// poll()方法同步获取一个元素,如果缓冲区是空的,则返回null
// channel.poll()
}
}
}

// 发送消息
suspend fun postEvent() {
coroutineScope {
if (!channel.isClosedForSend) {
(1..10).forEach {

// 如果缓冲区没有满,则立即添加元素,
// 如果缓冲区满了调用者会被挂起
// send() 是一个挂起函数,用于同步发送方和接收方的一种机制
channel.send(it)

// offer():如果缓冲区存在并且没有满立即向缓冲区添加一个元素
// 如果添加成功会返回true, 失败会返回 false
// channel.offer(it)
}
}
}
}

正如你所看到的 发送 和 接受 都有两个方法,分别来分析一下他们的区别。

send() 和 offer() 的区别:

  • send(element: E) :如果缓冲区没有满,则立即添加元素, 如果缓冲区满了调用者会被挂起,send() 方法是一个挂起函数,用于同步发送方和接收方的一种机制
  • offer(element: E): Boolean :如果缓冲区存在并且没有满立即向缓冲区添加一个元素,添加成功会返回 true, 失败会返回 false

receive() 和 poll() 的区别:

  • receive(): E :异步获取元素,如果缓冲区是空时调用者会被挂起,直到一个新值被发送到缓冲区,receive() 方法是一个挂起函数,用于同步发送方和接收方的一种机制
  • poll(): E?:用于同步获取一个元素,如果缓冲区是空的,则返回 null

Flow 与 Channel 的区别:

  • Flow :中间操作符 (map , filter 等等) 会构建了一个待执行的调用链,只有遇到末端操作符 (collect , toList 等等) 才会触发所有操作的执行,所以 Flow 也被称为冷数据流
  • Channel :发送方 (SendChannel) 发送数据,并不依赖于接受方(ReceiveChannel),所以 Channel 也被称为热数据流

Channel 的不同类型

Channel 对应着有四种不同的类型:

  • RendezvousChannel :这是默认的类型,大小为 0 的缓冲区,只有当 send() 方法和 receive() 方法都调用的时候,元素才会从发送方传输到接收方,否则将会被挂起
  • LinkedListChannel :通过 Channel.Factory.CONFLATED 会创建一个容量无限的缓冲区 (受限于内存的大小) ,send() 方法远不会挂起,offer() 方法始终返回 true
  • ConflatedChannel :最多缓冲一个元素,新元素会覆盖掉旧元素,只会接收最后发送的元素,之前的元素都会丢失,send() 方法永远不会挂起,offer() 方法始终返回 true
  • ArrayChannel :通过 Channel.Factory.BUFFERED 或者 指定大小 会创建一个固定容量的数组缓冲区,send() 方法仅在缓冲区满时挂起,receive() 方法仅在缓冲区为空时挂起

创建四种不同类型 channel 的方式:

1
2
3
4
5
6
7
kotlin复制代码val rendezvousChannel = Channel<Int>()
val linkedListChannel = Channel<Int>(Channel.Factory.UNLIMITED)
val conflatedChannel = Channel<Int>(Channel.Factory.CONFLATED)

// 指定数字 或者 通过 Channel.Factory.BUFFERED 都会创建创建一个固定容量的数组缓冲区
val bufferedChannel = Channel<Int>(Channel.Factory.BUFFERED) // 创建默认容量的数组缓冲区
val arrayChannel = Channel<Int>(30) // 创建指定容量的数组缓冲区

上面创建 channel 的方式,对应的源码:

1
2
3
4
5
6
7
8
scss复制代码public fun <E> Channel(capacity: Int = RENDEZVOUS): Channel<E> =
when (capacity) {
RENDEZVOUS -> RendezvousChannel()
UNLIMITED -> LinkedListChannel()
CONFLATED -> ConflatedChannel()
BUFFERED -> ArrayChannel(CHANNEL_DEFAULT_CAPACITY)
else -> ArrayChannel(capacity)
}

BroadcastChannels 是什么

来看看 Kotlin 官方文档是如何介绍 BroadcastChannels

  • BroadcastChannels 是非阻塞的,它用于发送方 (SendChannel) 和接收方 (ReceiveChannel) 之间通信
  • BroadcastChannels 实现了 SendChannel 接口,所以只可以发送数据
  • BroadcastChannels 提供了 openSubscription 方法,会返回一个新的 ReceiveChannel,可以从缓冲区获取数据
  • 通过 BroadcastChannels 发送的数据,所有接收方 (ReceiveChannel) 都会收到,如下图所示

BroadcastChannels 是一个接口,而它的子类有 ConflatedBroadcastChannel、ArrayBroadcastChannel,这里主要介绍一下 ConflatedBroadcastChannel,ConflatedBroadcastChannel 重写了 openSubscription 方法。

1
2
3
4
5
kotlin复制代码public override fun openSubscription(): ReceiveChannel<E> {
val subscriber = Subscriber(this)
...... // 省略很多无关的代码
return subscriber
}
  • openSubscription 方法返回一个 ReceiveChannel 作为接受者
  • 在 openSubscription 方法内,创建了一个 Subscriber 的实例

Subscriber 其实是 ConflatedBroadcastChannel 的内部类,它实现了 ReceiveChannel 接口。

1
2
3
csharp复制代码private class Subscriber<E>(
private val broadcastChannel: ConflatedBroadcastChannel<E>
) : ConflatedChannel<E>(), ReceiveChannel<E>

正如你所见 Subscriber 继承 ConflatedChannel 同时实现了 ReceiveChannel 接口,而 ConflatedChannel 在上文介绍过了,最多缓冲一个元素,新元素会覆盖掉旧元素,只会接收最后发送的元素,之前的元素都会丢失,所以 ConflatedBroadcastChannel 适合用来实现搜索相关的功能,因为用户只对最后一次搜索结果感兴趣。

注意: StateFlow 将会取代 ConflatedBroadcastChannel 下文有介绍

使用 ConflatedBroadcastChannel 实现 DB 搜索

我在 PokemonGo 项目中增加了两种搜索场景,分别通过 BroadcastChannels 和 StateFlow 来实现,通过 ConflatedBroadcastChannel 实现 DB 搜索,只需要两步

1.在 Activity 中监听 ConflatedBroadcastChannel 的变化
src/main/java/com/hi/dhl/pokemon/ui/main/MainActivity.kt

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码// searchView 是一个 AppCompatEditText,当然你可以使用 androidx.appcompat.widget.SearchView,或者其他
searchView.addTextChangedListener {
val result = it.toString()
// 调用 queryParamterForDb 方法过滤用户的输入,并查询数据库
mViewModel.queryParamterForDb(result)
}

// 监听查询结果
mViewModel.searchResultForDb.observe(this, Observer {
mPokemonAdapter.submitData(lifecycle, it)
})
  • 接受用户输入的数据,并调用 queryParamterForDb 方法过滤用户的输入,然后查询数据库
  • 通过 searchResultForDb.observe 方法监听查询结果

2. 在 MainViewModel 中实现 queryParamterForDb 方法
src/main/java/com/hi/dhl/pokemon/ui/main/MainViewModel.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码// 根据关键词搜索
fun queryParamterForDb(paramter: String) = mChanncel.offer(paramter)

// 使用 ConflatedBroadcastChannel 进行搜索
val searchResultForDb = mChanncel.asFlow()
// 避免在单位时间内,快输入造成大量的请求
.debounce(200)
// 避免重复的搜索请求。假设正在搜索 dhl,用户删除了 l 然后输入 l。最后的结果还是 dhl。它就不会再执行搜索查询 dhl
// distinctUntilChanged 对于 StateFlow 任何实例是没有效果的
.distinctUntilChanged()
.flatMapLatest { search -> // 只显示最后一次搜索的结果,忽略之前的请求
pokemonRepository.fetchPokemonByParameter(search).cachedIn(viewModelScope)
}
.catch { throwable ->
// 异常捕获
}.asLiveData()
  • 通过 mChanncel.offer 发送数据
  • 通过 mChanncel.asFlow() 方法,将 Channel 转换为 Flow 并调用 debounce 、 distinctUntilChanged 、 flatMapLatest 过掉用户的输入数据,这些操作符在后文会详细分析
  • 最后查询数据库,返回结果,项目中使用的是通过 Paging3 查询本地数据库,关于如何实现可以查看另外一篇文章 Jetpack 成员 Paging3 数据实践以及源码分析(一)

重点: 在 Kotlin coroutines library (1.3.6) 版本中增加了一个新类 StateFlow,它的设计和 ConflatedBroadcastChannel 相同,将来计划完全取代 ConflatedBroadcastChannel

StateFlow 是什么

在前面的内容提到了很多次 StateFlow,那么 StateFlow 是什么,它与 Flows 和 Channels 有什么关系呢,来看看 Kotlin 官方文档是如何介绍 StateFlow

将上面这段话,简单的总结一下:

  • StateFlow 实现了 Flow 接口,它仅仅表示一种可读的状态,它的值是不变的,用于外部调用
1
2
3
csharp复制代码public interface StateFlow<out T> : Flow<T> {
public val value: T // val 关键字表示不可变的
}
  • StateFlow 提供了一个可变的版本 MutableStateFlow,它的值是可变的,用于内部调用
1
2
3
csharp复制代码public interface MutableStateFlow<T> : StateFlow<T> {
public override var value: T // var 表示可变的
}
  • StateFlow 与 Flow 的不同之处在于,StateFlow 仅仅表示一种状态,不依赖于特定的上下文,而 Flow 操作执行是在 CoroutineScope 内的,换句话说 StateFlow 不需要在协程的作用域内,它也可以执行

刚才我们提到 StateFlow 的出现是为了取代 ConflatedBroadcastChannel,那么它与 ConflatedBroadcastChannel 有什么不同之处:

  • StateFlow 实现更加简单,不需要实现所有 Channel API,而 ConflatedBroadcastChannel 在其内部封装了 ConflatedChannel 和 BroadcastChannels
  • StateFlow 内部有个变量 value,无论任何时候都可以安全的访问
  • StateFlow 实现读写分离,StateFlow 用来读而 MutableStateFlow 用来写
  • StateFlow 内部使用 Any.equals 来比较新值与旧值,和 distinctUntilChanged 方式相同,所以在 StateFlow 上应用 distinctUntilChanged 是没有效果的

StateFlow 源码:

1
kotlin复制代码if (oldState == newState) return // 如果值没有改变,不会做任何事

distinctUntilChanged 源码

1
2
kotlin复制代码public fun <T, K> Flow<T>.distinctUntilChangedBy(keySelector: (T) -> K): Flow<T> =
distinctUntilChangedBy(keySelector = keySelector, areEquivalent = { old, new -> old == new })

使用 StateFlow 实现 NetWork 搜索

StateFlow 和 ConflatedBroadcastChannel 一样,实现搜索功能只需要两步

1.在 Activity 中监听 ConflatedBroadcastChannel 的变化
src/main/java/com/hi/dhl/pokemon/ui/main/MainActivity.kt

1
2
3
4
5
6
7
8
9
10
kotlin复制代码// searchView 是一个 AppCompatEditText,当然你可以使用 androidx.appcompat.widget.SearchView 或者其他
searchView.addTextChangedListener {
val result = it.toString()
// 调用 queryParamterForNetWork 方法过滤用户的输入,并查询网络
mViewModel.queryParamterForNetWork(result)
}

mViewModel.searchResultMockNetWork.observe(this, Observer {
// 网络搜索回调监听
})
  • 接受用户输入的数据,并调用 queryParamterForNetWork 方法过滤用户的输入,通过网络查询关键字
  • 通过 searchResultMockNetWork.observe 方法监听查询结果

2. 在 MainViewModel 中实现 queryParamterForNetWork 方法
src/main/java/com/hi/dhl/pokemon/ui/main/MainViewModel.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kotlin复制代码// 根据关键词搜索
fun queryParamterForNetWork(paramter: String) {
_stateFlow.value = paramter
}

// 因为没有合适的搜索接口,在这里模拟进行网络搜索
val searchResultMockNetWork =
// 避免在单位时间内,快输入造成大量的请求
stateFlow.debounce(200)
.filter { result ->
if (result.isEmpty()) { // 过滤掉空字符串等等无效输入
return@filter false
} else {
return@filter true
}
}
.flatMapLatest { // 只显示最后一次搜索的结果,忽略之前的请求
// 网络请求,这里替换自己的实现即可
}
.catch { throwable ->
// 异常捕获
}
.asLiveData()
  • 通过 _stateFlow.value 更新数据
  • 调用 debounce 、filter 、flatMapLatest 等等操作符过滤掉无效的请求

常用操作符解析

在 PokemonGo 项目中使用 debounce 、filter 、flatMapLatest 、 distinctUntilChanged 等等操作符,一起来详细的分析一下这些操作符的含义,以及如何使用。

debounce

debounce 也叫做防抖动函数,当用户在很短的时间内输入 “d”,”dh”,”dhl”,但是用户可能只对 “dhl” 的搜索结果感兴趣,因此我们必须舍弃 “d”,”dh” 过滤掉不需要的请求,针对于这个情况,我们可以使用 debounce 函数,在指定时间内出现多个字符串,debounce 始终只会发出最后一个字符串,我们来看个例子。

1
2
3
4
5
6
7
8
9
scss复制代码val result = flow {
emit("h")
emit("i")
emit("d")
delay(90)
emit("dh")
emit("dhl")
}.debounce(200).toList()
println(result) // 最后输出:dhl

filter

filter 操作符用于过滤不需要的字符串,在 PokemonGo 项目中只过滤了空字符串,我们来看个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码val result = flow {
emit("h")
emit("i")
emit("d")
delay(90)
emit("dh")
emit("dhl")
}.filter { result ->
if (!result.equals("dhl")) {
return@filter false
} else {
return@filter true
}
}.toList()
println(result) // 最后输出:dhl

flatMapLatest

flatMapLatest 避免向用户展示不需要的结果,只提供最后一个搜索查询(最新)的结果,例如,正在查询 “dh”,然后用户输入 “dhl”, 这个时候用户对 “dh” 的结果不感兴趣,可能只对 “dhl” 的结果感兴趣,这个时候可以使用 flatMapLatest,我们来看个例子。

1
2
3
4
5
6
7
8
9
10
11
scss复制代码flow {
emit("dh")
emit("dhl")
}.flatMapLatest { value ->

flow<String> {
delay(100)
println("collected $value") // 最后输出 collected dhl
}

}.collect()

注意: flatMapLatest 在 Kotlin coroutines library (1.3.20) 以下版本使用会出现以下错误。

1
sql复制代码IllegalStateException crash: call to 'resume' before 'invoke' with coroutine

Kotlin 团队在 Kotlin coroutines library (1.3.20) 以上修复了这个问题,如果出现这个问题,将版本升级到 1.3.20 以上即可 issues 地址。

DistinctUntilChanged

  • distinctUntilChanged 操作符用来过滤掉重复的请求,只有当前值与最后一个值不同时才将其发出,我们来看个例子。
1
2
3
4
5
6
7
8
9
10
11
scss复制代码val result = flow {
emit("d")
emit("d")
emit("d")
emit("d")
emit("dhl")
emit("dhl")
emit("dhl")
emit("dhl")
}.distinctUntilChanged().toList()
println(result) // 输出 [d, dhl]
  • StateFlow 内部已经实现了类似于 distinctUntilChanged 操作符的功能,因此 distinctUntilChanged 应用在 StateFlow 上是没有效果的

我们一起来分析 distinctUntilChanged 操作符源码是如何实现的

1
2
3
4
5
kotlin复制代码public fun <T> Flow<T>.distinctUntilChanged(): Flow<T> =
when (this) {
is StateFlow<*> -> this
else -> distinctUntilChangedBy { it }
}
  • distinctUntilChanged 是 Flow 的扩展函数
  • 如果当前对象是 StateFlow,直接返回调用者本身
  • 如果不是 StateFlow 就会调用 distinctUntilChangedBy 方法
1
2
kotlin复制代码public fun <T, K> Flow<T>.distinctUntilChangedBy(keySelector: (T) -> K): Flow<T> =
distinctUntilChangedBy(keySelector = keySelector, areEquivalent = { old, new -> old == new })

最后会调用 areEquivalent 方法进行比较,会过滤掉所有相同值的

全文到这里就结束了,效果图如下所示,如果效果图无法查看,请点击这里查看 效果图

文章中提到的 PokemonGo(神奇宝贝) 是基于 Jetpack + MVVM + Data Mapper + Repository + Paging3 + App Startup + Hilt + Kotlin Flow + Motionlayout + Coil 等等技术综合实战项目,点击这里前往查看

参考文献

  • kotlinx.coroutines.channels
  • kotlinx.coroutines.flow
  • Implementing Search Filter using Kotlin
  • Going deep on Flows & Channels
  • Implement Instant Search Using Kotlin Flow Operators

结语

应小伙伴们的建议,公众号开通了:ByteCode , 方便查看一系列的文章。致力于分享一系列 Android 系统源码、逆向分析、算法、译文、Kotlin、Jetpack 源码相关的文章,如果这篇文章对你有帮助给个 star,欢迎一起来学习,在技术的道路上一起前进。

正在建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,仓库持续更新中,可以前去查看:AndroidX-Jetpack-Practice, 如果这个仓库对你有帮助,请仓库右上角帮我点个赞。

算法

由于 LeetCode 的题库庞大,每个分类都能筛选出数百道题,由于每个人的精力有限,不可能刷完所有题目,因此我按照经典类型题目去分类、和题目的难易程度去排序。

  • 数据结构: 数组、栈、队列、字符串、链表、树……
  • 算法: 查找算法、搜索算法、位运算、排序、数学、……

每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路、时间复杂度和空间复杂度,如果你同我一样喜欢算法、LeetCode,可以关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin,一起来学习,期待与你一起成长。

Android 10 源码系列

正在写一系列的 Android 10 源码分析的文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,如果你同我一样喜欢研究 Android 源码,可以关注我 GitHub 上的 Android10-Source-Analysis,文章都会同步到这个仓库。

  • 0xA01 Android 10 源码分析:APK 是如何生成的
  • 0xA02 Android 10 源码分析:APK 的安装流程
  • 0xA03 Android 10 源码分析:APK 加载流程之资源加载
  • 0xA04 Android 10 源码分析:APK 加载流程之资源加载(二)
  • 0xA05 Android 10 源码分析:Dialog 加载绘制流程以及在 Kotlin、DataBinding 中的使用
  • 0xA06 Android 10 源码分析:WindowManager 视图绑定以及体系结构
  • 0xA07 Android 10 源码分析:Window 的类型 以及 三维视图层级分析
  • 更多……

Android 应用系列

  • 为数不多的人知道的 AndroidStudio 快捷键(一)
  • 为数不多的人知道的 AndroidStudio 快捷键(二)
  • 再见吧 buildSrc, 拥抱 Composing builds 提升 Android 编译速度
  • 为数不多的人知道的 Kotlin 技巧以及 原理解析
  • Jetpack 最新成员 AndroidX App Startup 实践以及原理分析
  • Jetpack 成员 Paging3 实践以及源码分析(一)
  • Jetpack 新成员 Paging3 网络实践及原理分析(二)
  • Jetpack 新成员 Hilt 实践(一)启程过坑记
  • Jetpack 新成员 Hilt 实践之 App Startup(二)进阶篇
  • Jetpack 新成员 Hilt 与 Dagger 大不同(三)落地篇
  • 全方面分析 Hilt 和 Koin 性能
  • 神奇宝贝(PokemonGo) 眼前一亮的 Jetpack + MVVM 极简实战
  • Google 推荐在项目中使用 sealed 和 RemoteMediator
  • Kotlin Sealed 是什么?为什么 Google 都用

精选译文

目前正在整理和翻译一系列精选国外的技术文章,不仅仅是翻译,很多优秀的英文技术文章提供了很好思路和方法,每篇文章都会有译者思考部分,对原文的更加深入的解读,可以关注我 GitHub 上的 Technical-Article-Translation,文章都会同步到这个仓库。

  • [译][Google工程师] 刚刚发布了 Fragment 的新特性 “Fragment 间传递数据的新方式” 以及源码分析
  • [译][Google工程师] 详解 FragmentFactory 如何优雅使用 Koin 以及部分源码分析
  • [译][2.4K Start] 放弃 Dagger 拥抱 Koin
  • [译][5k+] Kotlin 的性能优化那些事
  • [译] 解密 RxJava 的异常处理机制
  • [译][1.4K+ Star] Kotlin 新秀 Coil VS Glide and Picasso
  • 更多……

本文转载自: 掘金

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

万字长文!你还敢说你看不懂阿里开源Java神器Arthas?

发表于 2020-09-26

大家好,我是爱吃鱼的程序员,一个渴望在互联网行业做到C位的程序员。可柔可刚,点赞则柔,白嫖则刚!看完记得给我来个三连哦!欢迎私信!

java排查神器Arthas -日常用法分析

在这里插入图片描述

前言

有时候线上出现问题,我们需要迫切的找寻解决方法,加日志再上线?查看修改的功能是否成功上线?现在有了Arthas一切都是那么简单。
我们看下官网的描述:
Arthas 是Alibaba开源的Java诊断工具,深受开发者喜爱。

  1. 当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:
  2. 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
  3. 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
  4. 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
  5. 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
  6. 是否有一个全局视角来查看系统的运行状况?
  7. 有什么办法可以监控到JVM的实时运行状态?
  8. 怎么快速定位应用的热点,生成火焰图?
    Arthas支持JDK 6+,支持Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 Tab 自动补全功能,进一步方便进行问题的定位和诊断。

学习

推荐 按照官方文档结合在线教程。本文档记录一下日常的用法,便于快速查找。

  1. 官方文档
  2. 在线教程
  3. ognl表达式

日常用法

wget https://arthas.aliyun.com/arthas-boot.jar 快速下载

  1. 监控方法调用 monitor/watch/trace相关

请注意,这些命令,都通过字节码增强技术来实现的,会在指定类的方法中插入一些切面来实现数据统计和观测,因此在线上、预发使用时,请尽量明确需要观测的类、方法以及条件,诊断结束要执行 stop 或将增强过的类执行 reset 命令。

  • monitor——方法执行监控
  • watch——方法执行数据观测
  • trace——方法内部调用路径,并输出方法路径上的每个节点上耗时
  • stack——输出当前方法被调用的调用路径
  • tt——方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测

watch

让你能方便的观察到指定方法的调用情况。能观察到的范围为:返回值、抛出异常、入参,通过编写 OGNL 表达式进行对应变量的查看。

参数:

class-pattern 类名表达式匹配

method-pattern 方法名表达式匹配

express 观察表达式

condition-express 条件表达式

-b 在方法调用之前观察

-e 在方法异常之后观察

-s 在方法返回之后观察

-f 在方法结束之后(正常返回和异常返回)观察 默认开启

-E 开启正则表达式匹配,默认为通配符匹配

-x 指定输出结果的属性遍历深度,默认为 1

-n 执行的次数

特别说明:

  • watch 命令定义了4个观察事件点,即 -b 方法调用前,-e 方法异常后,-s 方法返回后,-f 方法结束后
  • 4个观察事件点 -b、-e、-s 默认关闭,-f 默认打开,当指定观察点被打开后,在相应事件点会对观察表达式进行求值并输出
  • 这里要注意方法入参和方法出参的区别,有可能在中间被修改导致前后不一致,除了 -b 事件点 params 代表方法入参外,其余事件都代表方法出参
  • 当使用 -b 时,由于观察事件点是在方法调用前,此时返回值或异常均不存在
  • -x 代表输出结果的深度 ,默认为 1

使用参考

  • 常规用法
    watch class method {params,returnObj} -x 1 '#cost>200'

观察表达式 { } 可以包裹结果集 , 分割。输出的表达式变量定义
params 代表参数数组 ,returnObj代表返回值
过滤耗时大于200ms的

  • 特殊用法

1.调用第一个参数的方法或属性

watch class method "{params[0].length()}" -x 1

watch class method "{params[0].{ #this.length()}}" -x 1

2.按照条件过滤

watch class method "{params[0].{?#this.length()>7}}" -x 1

字符串长度 > 7 才会输出 params[0] 的值

3.过滤后统计

1
kotlin复制代码`watch class  method  "{params[0].{?#this.length()>9}.size()}" -x 1`

trace

trace 命令能主动搜索 class-pattern/method-pattern 对应的方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路。

参数说明

class-pattern 类名表达式匹配

method-pattern 方法名表达式匹配

condition-express 条件表达式

-E 开启正则表达式匹配,默认为通配符匹配

-n 命令执行次数

#cost 方法执行耗时

使用参考

1.常规用法

trace class method '#cost>100' -n 1
过滤大于100ms的调用链,只输出一次

2.特殊用法

trace命令只会trace匹配到的函数里的子调用,并不会向下trace多层。因为trace是代价比较贵的,多层trace可能会导致最终要trace的类和函数非常多。

动态trace

3.3.0 版本后支持。
打开终端1,trace run函数,可以看到打印出 listenerId: 1:

1
2
3
4
5
6
bash复制代码[arthas@59161]$ trace demo.MathGame run
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 112 ms, listenerId: 1
`---ts=2020-07-09 16:48:11;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@3d4eac69
`---[1.389634ms] demo.MathGame:run()
`---[0.123934ms] demo.MathGame:primeFactors() #24 [throws Exception]

现在想要深入子函数primeFactors,可以打开一个新终端2,使用telnet localhost 3658连接上arthas,再trace primeFactors时,指定listenerId。

再查看终端1,可以发现trace的结果增加了一层,打印了primeFactors函数里的内容

1
2
3
4
5
bash复制代码`---ts=2020-07-09 16:49:29;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@3d4eac69
`---[0.492551ms] demo.MathGame:run()
`---[0.113929ms] demo.MathGame:primeFactors() #24 [throws Exception]
`---[0.061462ms] demo.MathGame:primeFactors()
`---[0.001018ms] throw:java.lang.IllegalArgumentException() #46

通过指定listenerId的方式动态trace,可以不断深入。另外 watch/tt/monitor等命令也支持类似的功能。

tt

方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测。

watch 虽然很方便和灵活,但需要提前想清楚观察表达式的拼写,这对排查问题而言要求太高,因为很多时候我们并不清楚问题出自于何方,只能靠蛛丝马迹进行猜测。

这个时候如果能记录下当时方法调用的所有入参和返回值、抛出的异常会对整个问题的思考与判断非常有帮助。

于是乎,TimeTunnel 命令就诞生了。

参数说明:

-n 执行次数

#cost 过滤耗时

-t tt 命令有很多个主参数,-t 就是其中之一。这个参数的表明希望记录下类 *Test 的 print 方法的每次执行情况。

-s 可以跟条件表达式 进行筛选

-l 列出所有的记录

记录说明

表格字段 字段解释
INDEX 时间片段记录编号,每一个编号代表着一次调用,后续tt还有很多命令都是基于此编号指定记录操作,非常重要。
TIMESTAMP 方法执行的本机时间,记录了这个时间片段所发生的本机时间
COST(ms) 方法执行的耗时
IS-RET 方法是否以正常返回的形式结束
IS-EXP 方法是否以抛异常的形式结束
OBJECT 执行对象的hashCode(),注意,曾经有人误认为是对象在JVM中的内存地址,但很遗憾他不是。但他能帮助你简单的标记当前执行方法的类实体
CLASS 执行的类名
METHOD 执行的方法名

使用参考

tt -t class method '#cost>100'

1
2
3
4
5
6
7
bash复制代码 INDEX   TIMESTAMP            COST(ms)  IS-RET  IS-EXP   OBJECT         CLASS                          METHOD
-------------------------------------------------------------------------------------------------------------------------------------
1000 2018-12-04 11:15:38 1.096236 false true 0x4b67cf4d MathGame primeFactors
1001 2018-12-04 11:15:39 0.191848 false true 0x4b67cf4d MathGame primeFactors
1002 2018-12-04 11:15:40 0.069523 false true 0x4b67cf4d MathGame primeFactors
1003 2018-12-04 11:15:41 0.186073 false true 0x4b67cf4d MathGame primeFactors
1004 2018-12-04 11:15:42 17.76437 true false 0x4b67cf4d MathGame

查看调用信息
对于具体一个时间片的信息而言,你可以通过 -i 参数后边跟着对应的 INDEX 编号查看到他的详细信息。
tt -i 1003

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bash复制代码$ tt -i 1003
INDEX 1003
GMT-CREATE 2018-12-04 11:15:41
COST(ms) 0.186073
OBJECT 0x4b67cf4d
CLASS demo.MathGame
METHOD primeFactors
IS-RETURN false
IS-EXCEPTION true
PARAMETERS[0] @Integer[-564322413]
THROW-EXCEPTION java.lang.IllegalArgumentException: number is: -564322413, need >= 2
at demo.MathGame.primeFactors(MathGame.java:46)
at demo.MathGame.run(MathGame.java:24)
at demo.MathGame.main(MathGame.java:16)

Affect(row-cnt:1) cost in 11 ms.

tt -l 查看我们现有的记录

1
2
3
4
5
6
7
8
9
10
11
bash复制代码$ tt -l
INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD
-------------------------------------------------------------------------------------------------------------------------------------
1000 2018-12-04 11:15:38 1.096236 false true 0x4b67cf4d MathGame primeFactors
1001 2018-12-04 11:15:39 0.191848 false true 0x4b67cf4d MathGame primeFactors
1002 2018-12-04 11:15:40 0.069523 false true 0x4b67cf4d MathGame primeFactors
1003 2018-12-04 11:15:41 0.186073 false true 0x4b67cf4d MathGame primeFactors
1004 2018-12-04 11:15:42 17.76437 true false 0x4b67cf4d MathGame primeFactors
9
1005 2018-12-04 11:15:43 0.4776 false true 0x4b67cf4d MathGame primeFactors
Affect(row-cnt:6) cost in 4 ms.

我需要筛选出 primeFactors 方法的调用信息
tt -s

1
2
3
4
5
6
7
8
9
10
11
bash复制代码$ tt -s 'method.name=="primeFactors"'
INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD
-------------------------------------------------------------------------------------------------------------------------------------
1000 2018-12-04 11:15:38 1.096236 false true 0x4b67cf4d MathGame primeFactors
1001 2018-12-04 11:15:39 0.191848 false true 0x4b67cf4d MathGame primeFactors
1002 2018-12-04 11:15:40 0.069523 false true 0x4b67cf4d MathGame primeFactors
1003 2018-12-04 11:15:41 0.186073 false true 0x4b67cf4d MathGame primeFactors
1004 2018-12-04 11:15:42 17.76437 true false 0x4b67cf4d MathGame primeFactors
9
1005 2018-12-04 11:15:43 0.4776 false true 0x4b67cf4d MathGame primeFactors
Affect(row-cnt:6) cost in 607 ms.

stack

很多时候我们都知道一个方法被执行,但这个方法被执行的路径非常多,或者你根本就不知道这个方法是从那里被执行了,此时你需要的是 stack 命令。

class-pattern 类名表达式匹配

method-pattern 方法名表达式匹配

condition-express 条件表达式

-E 开启正则表达式匹配,默认为通配符匹配

-n 执行次数限制

使用参考

stack demo.MathGame primeFactors ’#cost>5‘
输出调用链,按照执行时间过滤

stack demo.MathGame primeFactors 'params[0]<0' -n 2
按照条件表达式进行过滤

monitor

参数说明

方法拥有一个命名参数 [c:],意思是统计周期(cycle of output),拥有一个整型的参数值

参数名称 参数说明
class-pattern 类名表达式匹配
method-pattern 方法名表达式匹配
condition-express 条件表达式
-E 开启正则表达式匹配,默认为通配符匹配
-c 统计周期,默认值为120秒
-b 在方法调用之前计算condition-express

监控的维度说明

监控项 说明
timestamp 时间戳
class Java类
method 方法(构造方法、普通方法)
total 调用次数
success 成功次数
fail 失败次数
rt 平均RT
fail-rate 失败率

使用参考

monitor -c 5 demo.MathGame primeFactors
每5秒统计一次

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
bash复制代码$ monitor -c 5 demo.MathGame primeFactors
Press Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 94 ms.
timestamp class method total success fail avg-rt(ms) fail-rate
-----------------------------------------------------------------------------------------------
2018-12-03 19:06:38 demo.MathGame primeFactors 5 1 4 1.15 80.00%

timestamp class method total success fail avg-rt(ms) fail-rate
-----------------------------------------------------------------------------------------------
2018-12-03 19:06:43 demo.MathGame primeFactors 5 3 2 42.29 40.00%

timestamp class method total success fail avg-rt(ms) fail-rate
-----------------------------------------------------------------------------------------------
2018-12-03 19:06:48 demo.MathGame primeFactors 5 3 2 67.92 40.00%

timestamp class method total success fail avg-rt(ms) fail-rate
-----------------------------------------------------------------------------------------------
2018-12-03 19:06:53 demo.MathGame primeFactors 5 2 3 0.25 60.00%

timestamp class method total success fail avg-rt(ms) fail-rate
-----------------------------------------------------------------------------------------------
2018-12-03 19:06:58 demo.MathGame primeFactors 1 1 0 0.45 0.00%

timestamp class method total success fail avg-rt(ms) fail-rate
-----------------------------------------------------------------------------------------------
2018-12-03 19:07:03 demo.MathGame primeFactors 2 2 0 3182.72 0.00%

计算条件表达式过滤统计结果(方法执行完毕之后)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bash复制代码monitor -c 5 demo.MathGame primeFactors "params[0] <= 2"
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 19 ms, listenerId: 5
timestamp class method total success fail avg-rt(ms) fail-rate
-----------------------------------------------------------------------------------------------
2020-09-02 09:42:36 demo.MathGame primeFactors 5 3 2 0.09 40.00%

timestamp class method total success fail avg-rt(ms) fail-rate
----------------------------------------------------------------------------------------------
2020-09-02 09:42:41 demo.MathGame primeFactors 5 2 3 0.11 60.00%

timestamp class method total success fail avg-rt(ms) fail-rate
----------------------------------------------------------------------------------------------
2020-09-02 09:42:46 demo.MathGame primeFactors 5 1 4 0.06 80.00%

timestamp class method total success fail avg-rt(ms) fail-rate
----------------------------------------------------------------------------------------------
2020-09-02 09:42:51 demo.MathGame primeFactors 5 1 4 0.12 80.00%

timestamp class method total success fail avg-rt(ms) fail-rate
----------------------------------------------------------------------------------------------
2020-09-02 09:42:56 demo.MathGame primeFactors 5 3 2 0.15 40.00%

2.jvm相关

  • dashboard——当前系统的实时数据面板
  • thread——查看当前 JVM 的线程堆栈信息
  • jvm——查看当前 JVM 的信息
  • sysprop——查看和修改JVM的系统属性
  • sysenv——查看JVM的环境变量
  • vmoption——查看和修改JVM里诊断相关的option
  • perfcounter——查看当前 JVM 的Perf Counter信息
  • logger——查看和修改logger
  • getstatic——查看类的静态属性
  • ognl——执行ognl表达式
  • mbean——查看 Mbean 的信息
  • heapdump——dump java heap, 类似jmap命令的heap dump功能

dashboard

当前系统的实时数据面板,按 ctrl+c 退出。

命令

dashboard -i 5000 -n 10

-i 刷新实时数据的时间间隔 (ms),默认5000ms
-n 刷新数据的次数

数据说明

  • ID: Java级别的线程ID,注意这个ID不能跟jstack中的nativeID一一对应。
  • NAME: 线程名
  • GROUP: 线程组名
  • PRIORITY: 线程优先级, 1~10之间的数字,越大表示优先级越高
  • STATE: 线程的状态
  • CPU%: 线程的cpu使用率。比如采样间隔1000ms,某个线程的增量cpu时间为100ms,则cpu使用率=100/1000=10%
  • DELTA_TIME: 上次采样之后线程运行增量CPU时间,数据格式为秒
  • TIME: 线程运行总CPU时间,数据格式为分:秒
  • INTERRUPTED: 线程当前的中断位状态
  • DAEMON: 是否是daemon线程

在这里插入图片描述

thread

查看当前线程信息,查看线程的堆栈

参数名称 参数说明
id 线程id
-n 指定最忙的前N个线程并打印堆栈
-b 找出当前阻塞其他线程的线程
-i 指定cpu使用率统计的采样间隔,单位为毫秒,默认值为200
–all 显示所有匹配的线程
–state 查看指定状态的线程

常用操作

thread -b, 找出当前阻塞其他线程的线程
有时候我们发现应用卡住了, 通常是由于某个线程拿住了某个锁, 并且其他线程都在等待这把锁造成的。 为了排查这类问题, arthas提供了thread -b, 一键找出那个罪魁祸首

thread -i 1000 : 统计最近1000ms内的线程CPU时间。

thread -n 3 -i 1000 : 列出1000ms内最忙的3个线程栈

thread --state WAITING 列出等待态的线程

jvm

查看当前JVM信息

使用参考

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
bash复制代码$ jvm
RUNTIME
--------------------------------------------------------------------------------------------------------------
MACHINE-NAME 37@ff267334bb65
JVM-START-TIME 2020-07-23 07:50:36
MANAGEMENT-SPEC-VERSION 1.2
SPEC-NAME Java Virtual Machine Specification
SPEC-VENDOR Oracle Corporation
SPEC-VERSION 1.8
VM-NAME Java HotSpot(TM) 64-Bit Server VM
VM-VENDOR Oracle Corporation
VM-VERSION 25.201-b09
INPUT-ARGUMENTS []
CLASS-PATH demo-arthas-spring-boot.jar
BOOT-CLASS-PATH /usr/lib/jvm/java-8-oracle/jre/lib/resources.jar:/usr/lib/jvm/java-8-oracle/j
re/lib/rt.jar:/usr/lib/jvm/java-8-oracle/jre/lib/sunrsasign.jar:/usr/lib/jvm/
java-8-oracle/jre/lib/jsse.jar:/usr/lib/jvm/java-8-oracle/jre/lib/jce.jar:/us
r/lib/jvm/java-8-oracle/jre/lib/charsets.jar:/usr/lib/jvm/java-8-oracle/jre/l
ib/jfr.jar:/usr/lib/jvm/java-8-oracle/jre/classes
LIBRARY-PATH /usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib

--------------------------------------------------------------------------------------------------------------
CLASS-LOADING
--------------------------------------------------------------------------------------------------------------
LOADED-CLASS-COUNT 7529
TOTAL-LOADED-CLASS-COUNT 7529
UNLOADED-CLASS-COUNT 0
IS-VERBOSE false

--------------------------------------------------------------------------------------------------------------
COMPILATION
--------------------------------------------------------------------------------------------------------------
NAME HotSpot 64-Bit Tiered Compilers
TOTAL-COMPILE-TIME 14921(ms)

--------------------------------------------------------------------------------------------------------------
GARBAGE-COLLECTORS
--------------------------------------------------------------------------------------------------------------
PS Scavenge name : PS Scavenge
[count/time (ms)] collectionCount : 7
collectionTime : 68

PS MarkSweep name : PS MarkSweep
[count/time (ms)] collectionCount : 1
collectionTime : 47

--------------------------------------------------------------------------------------------------------------
MEMORY-MANAGERS
--------------------------------------------------------------------------------------------------------------
CodeCacheManager Code Cache

Metaspace Manager Metaspace
Compressed Class Space

Copy Eden Space
Survivor Space

MarkSweepCompact Eden Space
Survivor Space
Tenured Gen

--------------------------------------------------------------------------------------------------------------
MEMORY
--------------------------------------------------------------------------------------------------------------
HEAP-MEMORY-USAGE init : 268435456(256.0 MiB)
[memory in bytes] used : 18039504(17.2 MiB)
committed : 181403648(173.0 MiB)
max : 3817865216(3.6 GiB)

NO-HEAP-MEMORY-USAGE init : 2555904(2.4 MiB)
[memory in bytes] used : 33926216(32.4 MiB)
committed : 35176448(33.5 MiB)
max : -1(-1 B)

--------------------------------------------------------------------------------------------------------------
OPERATING-SYSTEM
--------------------------------------------------------------------------------------------------------------
OS Linux
ARCH amd64
PROCESSORS-COUNT 3
LOAD-AVERAGE 29.53
VERSION 4.15.0-52-generic

--------------------------------------------------------------------------------------------------------------
THREAD
--------------------------------------------------------------------------------------------------------------
COUNT 30
DAEMON-COUNT 24
PEAK-COUNT 31
STARTED-COUNT 36
DEADLOCK-COUNT 0

--------------------------------------------------------------------------------------------------------------
FILE-DESCRIPTOR
--------------------------------------------------------------------------------------------------------------
MAX-FILE-DESCRIPTOR-COUNT 1048576
OPEN-FILE-DESCRIPTOR-COUNT 100
Affect(row-cnt:0) cost in 88 ms.

THREAD相关

  • COUNT: JVM当前活跃的线程数
  • DAEMON-COUNT: JVM当前活跃的守护线程数
  • PEAK-COUNT: 从JVM启动开始曾经活着的最大线程数
  • STARTED-COUNT: 从JVM启动开始总共启动过的线程次数
  • DEADLOCK-COUNT: JVM当前死锁的线程数

文件描述符相关

  • MAX-FILE-DESCRIPTOR-COUNT:JVM进程最大可以打开的文件描述符数
  • OPEN-FILE-DESCRIPTOR-COUNT:JVM当前打开的文件描述符数

heapdump

dump java heap, 类似jmap命令的heap dump功能。

使用参考

dump到指定文件

1
2
3
bash复制代码[arthas@58205]$ heapdump /tmp/dump.hprof
Dumping heap to /tmp/dump.hprof...
Heap dump file created

只dump live对象

1
2
3
bash复制代码[arthas@58205]$ heapdump --live /tmp/dump.hprof
Dumping heap to /tmp/dump.hprof...
Heap dump file created

dump到临时文件

1
2
3
bash复制代码[arthas@58205]$ heapdump
Dumping heap to /var/folders/my/wy7c9w9j5732xbkcyt1mb4g40000gp/T/heapdump2019-09-03-16-385121018449645518991.hprof...
Heap dump file created

sysprop 和 sysenv

sysprop 查看和修改当前JVM的系统属性(System Property)
sysenv 查看当前JVM的环境属性(System Environment Variables)

sysprop 和 sysenv使用方式类似

使用参考

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
bash复制代码$ sysprop
KEY VALUE
-------------------------------------------------------------------------------------------------------------------------------------
java.runtime.name Java(TM) SE Runtime Environment
sun.boot.library.path /Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib
java.vm.version 25.51-b03
user.country.format CN
gopherProxySet false
java.vm.vendor Oracle Corporation
java.vendor.url http://java.oracle.com/
path.separator :
java.vm.name Java HotSpot(TM) 64-Bit Server VM
file.encoding.pkg sun.io
user.country US
sun.java.launcher SUN_STANDARD
sun.os.patch.level unknown
java.vm.specification.name Java Virtual Machine Specification
user.dir /private/var/tmp
java.runtime.version 1.8.0_51-b16
java.awt.graphicsenv sun.awt.CGraphicsEnvironment
java.endorsed.dirs /Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/jre/lib/endors
ed
os.arch x86_64
java.io.tmpdir /var/folders/2c/tbxwzs4s4sbcvh7frbcc7n000000gn/T/
line.separator

java.vm.specification.vendor Oracle Corporation
os.name Mac OS X
sun.jnu.encoding UTF-8
java.library.path /Users/wangtao/Library/Java/Extensions:/Library/Java/Extensions:/Network/Libra
ry/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:.
sun.nio.ch.bugLevel
java.specification.name Java Platform API Specification
java.class.version 52.0
sun.management.compiler HotSpot 64-Bit Tiered Compilers
os.version 10.12.6
user.home /Users/wangtao
user.timezone Asia/Shanghai
java.awt.printerjob sun.lwawt.macosx.CPrinterJob
file.encoding UTF-8
java.specification.version 1.8
user.name wangtao
java.class.path .
java.vm.specification.version 1.8
sun.arch.data.model 64

查看单个属性
支持通过TAB键自动补全

1
2
bash复制代码$ sysprop java.version
java.version=1.8.0_51

修改单个属性

1
2
3
4
5
bash复制代码$ sysprop user.country
user.country=US
$ sysprop user.country CN
Successfully changed the system property.
user.country=CN

logger

查看logger信息,更新logger level

使用参考

查看所有logger信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bash复制代码[arthas@2062]$ logger
name ROOT
class ch.qos.logback.classic.Logger
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
level INFO
effectiveLevel INFO
additivity true
codeSource file:/Users/hengyunabc/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar
appenders name CONSOLE
class ch.qos.logback.core.ConsoleAppender
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
target System.out
name APPLICATION
class ch.qos.logback.core.rolling.RollingFileAppender
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
file app.log
name ASYNC
class ch.qos.logback.classic.AsyncAppender
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
appenderRef [APPLICATION]

从appenders的信息里,可以看到

  • CONSOLE logger的target是System.out
  • APPLICATION logger是RollingFileAppender,它的file是app.log
  • ASYNC它的appenderRef是APPLICATION,即异步输出到文件里

查看指定的logger信息

按名字查找

1
2
3
4
5
6
7
8
9
bash复制代码[arthas@2062]$ logger -n org.springframework.web
name org.springframework.web
class ch.qos.logback.classic.Logger
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
level null
effectiveLevel INFO
additivity true
codeSource file:/Users/hengyunabc/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar

指定classloader查找

注意hashcode是变化的,需要先查看当前的ClassLoader信息,提取对应ClassLoader的hashcode。

如果你使用-c,你需要手动输入hashcode:-c <hashcode>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bash复制代码[arthas@2062]$ logger -c 2a139a55
name ROOT
class ch.qos.logback.classic.Logger
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
level DEBUG
effectiveLevel DEBUG
additivity true
codeSource file:/Users/hengyunabc/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar
appenders name CONSOLE
class ch.qos.logback.core.ConsoleAppender
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
target System.out
name APPLICATION
class ch.qos.logback.core.rolling.RollingFileAppender
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
file app.log
name ASYNC
class ch.qos.logback.classic.AsyncAppender
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
appenderRef [APPLICATION]

更新 logger level

1
2
bash复制代码[arthas@2062]$ logger --name ROOT --level debug
update logger level success.

指定classloader更新 logger level

默认情况下,logger命令会在SystemClassloader下执行,如果应用是传统的war应用,或者spring boot fat jar启动的应用,那么需要指定classloader。

1
bash复制代码[arthas@2062]$ logger -c 2a139a55 --name ROOT --level debug
  1. class/classloader相关

jad

反编译指定已加载类的源码

jad 命令将 JVM 中实际运行的 class 的 byte code 反编译成 java 代码,便于你理解业务逻辑;

  • 在 Arthas Console 上,反编译出来的源码是带语法高亮的,阅读更方便
  • 当然,反编译出来的 java 代码可能会存在语法错误,但不影响你进行阅读理解

使用参考

编译java.lang.String

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
bash复制代码$ jad java.lang.String

ClassLoader:

Location:

/*
* Decompiled with CFR 0_132.
*/
package java.lang;

import java.io.ObjectStreamField;
...
public final class String
implements Serializable,
Comparable<String>,
CharSequence {
private final char[] value;
private int hash;
private static final long serialVersionUID = -6849794470754667710L;
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator();

public String(byte[] arrby, int n, int n2) {
String.checkBounds(arrby, n, n2);
this.value = StringCoding.decode(arrby, n, n2);
}
...

反编译时只显示源代码

默认情况下,反编译结果里会带有ClassLoader信息,通过--source-only选项,可以只打印源代码。方便和mc/redefine命令结合使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bash复制代码$ jad --source-only demo.MathGame
/*
* Decompiled with CFR 0_132.
*/
package demo;

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

public class MathGame {
private static Random random = new Random();
public int illegalArgumentCount = 0;
...

反编译指定的函数

$ jad demo.MathGame main

4.后台执行

arthas中的后台异步任务,使用了仿linux系统任务相关的命令。

1.使用&在后台执行任务
比如希望执行后台执行trace命令,那么调用下面命令

trace Test t &
这时命令在后台执行,可以在console中继续执行其他命令。

2.通过jobs查看任务
如果希望查看当前有哪些arthas任务在执行,可以执行jobs命令,执行结果如下

kill job-id 可以杀死指定任务

1
2
3
4
5
6
7
bash复制代码$ jobs
[10]*
Stopped watch com.taobao.container.Test test "params[0].{? #this.name == null }" -x 2
execution count : 19
start time : Fri Sep 22 09:59:55 CST 2017
timeout date : Sat Sep 23 09:59:55 CST 2017
session : 3648e874-5e69-473f-9eed-7f89660b079b (current)

可以看到目前有一个后台任务在执行。

  • job id是10, * 表示此job是当前session创建
  • 状态是Stopped
  • execution count是执行次数,从启动开始已经执行了19次
  • timeout date是超时的时间,到这个时间,任务将会自动超时退出

任务转到前台执行
fg <job-id> 可以把对应的任务转到前台继续执行。
bg <job-id>可以把对应的任务在后台继续执行

任务输出重定向

可通过>或者>>将任务输出结果输出到指定的文件中,可以和&一起使用,实现arthas命令的后台异步任务

$ trace Test t >> test.out &

今天的你多努力一点,明天的C位就是你!
一起学习成为 C位程序员。💋
微信公众号已开启,【C位程序员】,没关注的同学们记得关注哦!

本文转载自: 掘金

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

中间件系列之ElasticSearch-3-Java API

发表于 2020-09-26

在ES的官方文档中有详细的说明,现整理如下,推荐使用 Java High Level REST Client进行操作。

1.准备工作

引入依赖

以最新的版本为例

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.6.2</version>
</dependency>

初始化

填写自己的ES服务端IP即可,如果是集群,则填写多个。

1
2
3
4
java复制代码RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("localhost", 9200, "http"),
new HttpHost("localhost", 9201, "http")));

使用完毕后,记得关闭

1
go复制代码client.close();

2.利用RestClient进行基本操作

准备工作

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

需要注意,当前版本,即2.2.6版的spring-boot-starter-data-elasticsearch,集成的ES客户端版本为6.8.7。

如果与实际ES版本不符合,需要在pom.xml中手动指定ES版本,以我使用的ES 7.6.1版本为例

1
2
3
xml复制代码 <properties>
<elasticsearch.version>7.6.1</elasticsearch.version>
</properties>

注入client

1
2
3
4
5
6
7
8
9
java复制代码@Configuration
public class ESConfig {
@Bean
public RestHighLevelClient restHighLevelClient(){
return new RestHighLevelClient(
RestClient.builder(
new HttpHost("127.0.0.1", 9200, "http")));
}
}

创建Index

1
2
3
4
5
6
7
8
java复制代码@Test
void createIndex() throws IOException {
// 1、创建索引请求
CreateIndexRequest request = new CreateIndexRequest("user1");
// 2、客户端执行请求 IndicesClient,请求后获得响应
CreateIndexResponse createIndexResponse = client.indices().create(request, RequestOptions.DEFAULT);
System.out.println(createIndexResponse);
}

删除Index

1
2
3
4
5
6
7
java复制代码    @Test
void testDeleteIndex() throws IOException {
DeleteIndexRequest request = new DeleteIndexRequest("user");
// 删除
AcknowledgedResponse delete = client.indices().delete(request, RequestOptions.DEFAULT);
System.out.println(delete.isAcknowledged());
}

判断Index存在

1
2
3
4
5
6
java复制代码 @Test
void testExistIndex() throws IOException {
GetIndexRequest request = new GetIndexRequest("user");
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
System.out.println(exists);
}

创建Document

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@Test
void testAddDocument() throws IOException {
//创建对象
User user = new User("张三", 3);
//创建请求
IndexRequest request = new IndexRequest("user");
//规则 put /user/_doc/1
request.id("1");
//设置超时时间
request.timeout(TimeValue.timeValueSeconds(1));
//request.timeout("1s");
//将我们的数据放入请求 json
request.source(JSON.toJSONString(user), XContentType.JSON);
//客户端发送请求 , 获取响应的结果
IndexResponse indexResponse = client.index(request,
RequestOptions.DEFAULT);
System.out.println(indexResponse.toString()); //打印文档的内容
System.out.println(indexResponse.status()); // 对应我们命令返回的状态 ,第一次创建,返回CREATED
}

判断Document存在

1
2
3
4
5
6
java复制代码    @Test
void testIsExists() throws IOException {
GetRequest getRequest = new GetRequest("user", "1");
boolean exists = client.exists(getRequest, RequestOptions.DEFAULT);
System.out.println(exists);
}

获取Document

1
2
3
4
5
6
7
8
java复制代码 @Test
void testGetDocument() throws IOException {
GetRequest getRequest = new GetRequest("user", "1");
GetResponse getResponse = client.get(getRequest,
RequestOptions.DEFAULT);
System.out.println(getResponse.getSourceAsString()); // 打印文档的内容
System.out.println(getResponse); // 返回的全部内容是和命令式一样的
}

更新Document

1
2
3
4
5
6
7
8
9
10
java复制代码 @Test
void testUpdateRequest() throws IOException {
UpdateRequest updateRequest = new UpdateRequest("user", "1");
updateRequest.timeout("1s");
User user = new User("张三", 18);
updateRequest.doc(JSON.toJSONString(user), XContentType.JSON);
UpdateResponse updateResponse = client.update(updateRequest,
RequestOptions.DEFAULT);
System.out.println(updateResponse.status());
}

删除Document

1
2
3
4
5
6
7
java复制代码 @Test
void testDeleteRequest() throws IOException {
DeleteRequest request = new DeleteRequest("user","1");
request.timeout("1s");
DeleteResponse deleteResponse = client.delete(request,
RequestOptions.DEFAULT);
System.out.println(deleteResponse.status());

批量插入Document

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码 @Test
void testBulkRequest() throws IOException {
BulkRequest bulkRequest = new BulkRequest();
bulkRequest.timeout("10s");
ArrayList<User> userList = new ArrayList<User>();
userList.add(new User("张三", 3));
userList.add(new User("李四", 3));
userList.add(new User("王五", 3));
for (int i = 0; i < userList.size(); i++) {
// 批量更新和批量删除,就在这里修改对应的请求就可以了
bulkRequest.add(new IndexRequest("user").id("" + (i + 1))
.source(JSON.toJSONString(userList.get(i)), XContentType.JSON));
}
BulkResponse bulkResponse = client.bulk(bulkRequest,
RequestOptions.DEFAULT);
System.out.println(bulkResponse.hasFailures()); // 是否失败,返回fals代表成功
}

查询

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
java复制代码 	/*
SearchRequest 搜索请求
SearchSourceBuilder 条件构造
HighlightBuilder 构建高亮
TermQueryBuilder 精确查询
MatchAllQueryBuilder
xxxQueryBuilder 与命令行时的查询类型一一对应*/
@Test
void testSearch() throws IOException {
SearchRequest searchRequest = new SearchRequest("user");
// 构建搜索条件
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 查询条件,我们可以使用 QueryBuilders 工具来实现
// QueryBuilders.termQuery 精确
// QueryBuilders.matchAllQuery() 匹配所有
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("name",
"张三");
//MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery();
sourceBuilder.query(termQueryBuilder);
sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));

searchRequest.source(sourceBuilder);
SearchResponse searchResponse = client.search(searchRequest,
RequestOptions.DEFAULT);
System.out.println(JSON.toJSONString(searchResponse.getHits()));
System.out.println("=================================");
for (SearchHit documentFields : searchResponse.getHits().getHits()) {
System.out.println(documentFields.getSourceAsMap());
}
}

本文转载自: 掘金

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

1…777778779…956

开发者博客

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