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

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


  • 首页

  • 归档

  • 搜索

Growth Hacking:移动端 ABTest 在支付宝

发表于 2019-07-03
导语

近年来,随着互联网的快速发展,Growth-Hacking 已经是一个很普遍的概念。Growth-Hacking 的目的就是使用更小更灵活的成本通过数据驱动来挖掘产品增长的奥秘。同时在 AARRR 这个模型中,打造成一个不断优良循环的流程,需要从数据分析中发现产品功能、运营策略与转化之间的相关性,思考他们之间因果关系。

One accurate measurement is worth more than a thousand expert opinions

– Admiral Grace Hopper

如何衡量思考、创新想法的正确性?数据是最好的衡量标准,这就因此要求我们需要运用一些工具。而 AB 测试就是一个快速试错,用户影响尽可能小,通过数据科学决策的工具,它是 Growth-Hacking 最基础也是最重要的工具之一。

自 2000 年谷歌工程师将 ABTest 应用在互联网产品以来,A/B 测试在国内外越来越普及,逐渐成为互联网数据驱动产品的重要体现,通过 A/B 测试来驱动产品设计迭代优化。

什么是 ABTest

A/B 测试以数据驱动为导向,可以实现灵活的流量切分,使得同一产品的不同版本能同时在线,通过记录和分析用户对不同版本产生的行为数据,得到效果对比,最大程度地保证结果的科学性和准确性,从而帮助人们进行科学的产品决策。

ABTest 的主要组成部分

下图是一个通用的架构设计:

整个架构包含以下部分:

  • AB 测试管理平台:实验操作门户,提供创建、修改、关闭实验等,并且提供报表查看。
  • 配置数据库:实验配置数据,不仅仅局限于普通的关系型数据库,也可能有缓存数据库等。
  • 分流服务:根据实验配置数据,实验具体的分流逻辑,这个一般集成在各个业务平台或者业务服务器。
  • SDK:提供通用的解析分流逻辑,一般集成于客户端,前端。
  • 数据采集:分流结果日志,用户行为日志的实时采集。
  • 数据分析:实时和离线数据分析,通过一定的数据分析算法做出科学决策。

ABTest 的统计学原理

从 A/B 测试的试验原理来看,它是统计学上假设检验(显著性检验)的一种形式:假设检验中的参数检验是先对总体的参数提出某种假设,然后利用样本数据判断假设是否成立的过程。

逻辑上运用反证法,统计上依据小概率思想:

  • 小概率思想是指小概率事件(显著性水平 p < 0.05)在一次试验中基本上不会发生。
  • 反证法是指先提出假设,再用适当的统计方法确定假设成立的可能性大小;如可能性小,则认为假设不成立。

具体到对比试验,就是假设测试版本的总体参数(优化指标均值)等于对照版本的总体参数,然后利用这两个版本的样本数据来判断这个假设是否成立。

检验假设的基础概念

  • 原假设:又称零假设,H0,通常我们都是假设对比实验中的两组统计量的统计值一样,即实验组的均值等于对照组的均值。
  • 备择假设:也作对立假设,即否定原假设;实验组均值不等于对照组均值。
  • 双侧检验与单侧检验:如果备择假设没有确定的方向即”≠”,则为双侧假设。如果有特定的方向,包含“>” 或 “<”的则为单侧检验。
  • 检验统计量:在检验假设时所使用的统计量称为检验统计量,比如样本组的均值。
  • 接受域:使原假设接受的那些样本 (X1,X2,…,Xn) 所在的区域。
  • 否定域:使原假设被否定的样本构成的区域。
  • 简单假设与复杂假设:不论是原假设或者备择假设,只包含一个参数则为简单假设,否则为复杂假设。

两类型错误

  • 第 I 类错误(弃真错误):原假设为真时拒绝原假设;第 I 类错误的概率记为 α(alpha)。
  • 第 II 类错误(取伪错误):原假设为假时未拒绝原假设。第 II 类错误的概率记为 β(Beta)。
真实情况\实际决策 接受 H0 拒绝H0
H0为真 正确判决 第一类错误
H1为真 第二类错误 正确判决
  • 功效函数:设 R 表示一个检验的拒绝区域,

显著性水平和统计功效

  • 显著性水平:显著性水平是指在原假设为真时而被拒绝的概率或者风险,也就是发生类型一错误的概率α。通常在 AB 测试中,我们设置显著性水平为 0.05,当求得的 p-value 即 p<=0.05,那么拒绝原假设;p>0.05,那么不能拒绝原假设。
  • 统计功效:统计功效,简单说就是真理能被发现的可能性。就像胰岛素能降低血糖这事是真实存在的,但人类能发现它的概率是多少?如果统计功效是 0.8,就是说人类有 80% 的概率能发现它。它的数学定义可用一个公式来概括,统计功效=1-β。

p-value 计算

AB 测试中 p-value 的计算和区间估计的计算是相对应的,这个里面我们就利用双样本 Z 检验,简单说明一下 p-value 的计算公式:

  • 均值类指标:

  • UV 点击率/转化率等比率类指标:

根据 Z 值求 p 值,其实就是求标准正态分布的积分,求(z,∞)的N(0,1)的积分*2.

mPaaS ABTest

蚂蚁金服内部也有浓厚的实验文化,ABTest 实验平台已累计运行上万个实验:从支付宝客户端样式实验、交互实验到后端算法、策略实验都有着丰富的案例积累,在此过程中 ABTest 平台也得到了深度锤炼和能力积累,越来越简单易用、成熟稳定、权威可信。

目前,ABTest 平台已完成产品化改造,并入驻到 mPaaS 产品体系中,为 mPaaS 的智能化能力构建提供了有力的支撑。

实验模型

在进一步介绍 ABTest 架构之前有必要讨论一下 ABTest 的实验模型:

流量的隔离和复用

同一批用户如果同时参与多个实验,实验场景之间如果是相关的,两个实验之间的结果就可能相互影响,导致实验结论不正确。因此我们需要让同一个用户在同一段时间只能参与一个实验,从而避免多个实验导致的影响,称之为流量的隔离。

随着线上实验的增多,流量隔离避免不了流量不足以及流量饥饿的问题,这就对流量的复用提出了要求。

Google的层域架构

在 Google 2010 年发布的《Overlapping Experiment Infrastructure: More, Better, Faster Experimentation》 这篇文章中,主要介绍了 Google 的交叠实验架构,目的是能够提供一个 Better & More Faster 的实验平台。

关于 Google 提出的交叠实验架构,其核心是层域模型:

通过上图我们可以了解层域模型的基础结构了,这类结构不仅能够兼容单层实验和多变量实验,同时更具灵活性,可以对系统参数进行多种组合划分。

因为层域之间的复杂关系,这样设计虽然增加了灵活性但是同时使得业务方使用成本也大大增高,实际业务中主要还是基于单层实验,当整个链路中,每个分支都是相互独立,而每个分支的子集划分也比较清晰,所以对域的需求并不是很强。

ABTest 的领域模型

mPaaS ABTest 的领域模型:

mPaaS 上的 ABTest 通过“实验室”实现了一个分层,即每个“实验室”都拥有独立的 100% 流量。由于实际业务的一个链路并没有很多的参数并且参数间是否独立也是比较明确的,所以 ABTest 弱化了域这个概念。

  • 实验室

100% 的流量入口。支持设置参数的默认值兜底。业务上相互独立的每个业务应该拥有一个独立的实验室。

  • 实验

每个实验室下可以创建多个实验,实验间的流量是互斥的,达到流量隔离的目的。同时实验级别支持圈人定向条件,支持定向实验。

  • 实验版本

一个试验下可以创建多个实验版本,实验版本上绑定实验参数的实验值。每个版本的的流量分配通过和 10000 个 bucket 间的映射关系来分配。

ABTest 架构

从架构上来讲 ABTest 分为接入层、核心能力层和底层依赖层,下面重点介绍下接入层和核心能力层。

接入层

接入层解决业务系统如何接入 ABTest 组件的问题,互联网业务的 ABTest 通常分为两类:客户端实验和服务端实验,接入层对这两类型实验都提供了支持。

客户端实验

客户端实验通常是针对客户端的样式、交互流程进行实验,从而帮助产品和研发团队进行更好的迭代和优化。

实验配置通过客户端动态配置服务(Remote Config)触达到客户端,客户端业务通过从本地 local cache 中取变量的方式,拿到分流结果对应的实验变量及值,做对应的差异化渲染,达到对不同的用户提供不同服务、体验的目地,同时动态配置(Remote Config)服务会记录分流日志,回传服务端。

H5 容器、小程序框架集成 Remote Config,通过 Hybrid API 暴露服务给 H5、小程序,这样客户端 Native、H5、小程序三大开发框架都具备了 ABTest 的能力。

服务端实验

服务端实验通常是对服务端策略、算法做实验,是 AI 能力迭代的基础。

性能、接入难度是服务端业务系统接入 ABTest 比较关心的问题。ABTest 将分流服务抽离出独立SDK,将实验配置信息保存在内存中,集成进宿主系统后可以提供本地的分流服务,避免了网络开销。同时SDK提供了简单易用的接口,降低了使用的难度。

下图是分流SDK和ABTest Console、宿主系统之间的关系:

集成分流 SDK 有一定的开发成本,需要业务系统接入。mPaaS 平台在各个流量入口组件中预置了分流 SDK 提供分流服务,简化了 ABTest 的接入工作:

网关服务 MGS 将分流参数在请求上下文中透传到业务系统中,业务系统可以直接使用 ;
发布服务 MDS 的 H5 离线包管理平台可以直接对一个 H5 应用的不同版本做 ABTest;
智能投放 MCDP 可以支持对不同广告投放效果做 ABTest。

核心能力

关于 mPaaS ABTest 的核心能力,它已经达到了一个通用 ABTest 系统所应有的标准,主要分为两部分:

实验能力

实验能力主要包括实验管理、指标定义、圈人定向、分流服务、灰度推全。我们围绕“实验生命周期 & 科学流量分隔”着重展开:

  • 一次实验的生命周期包含“创建实验、功能和链路验证、正式运行、逐步放量、全量推全”五个阶段。而在实验状态转换过程中,流量的分配是否科学非常重要。
  • 在一个实验室下,每个用户通过 Hash 算法被绑定到 0~9999 号桶上,而实验版本保存对桶号的映射
  • ABTest 通过将流量划分为空闲桶、回收桶、使用桶三种状态,在流量分配过程中优先使用空闲桶,其次使用回收桶。在回收桶也分配完之后,整个实验室的流量已经使用完毕,需要同实验室下其他实验推全或者下线后,使用桶被回收后才能新建实验。ABTest系统在实验生命周期流转过程中科学的分配、回收流量(即改变桶的状态)保证了流量分配的科学性,降低了对用户的打扰。

分析能力

分析能力包括实时的分流 PV/UV 统计,T+1 的实验显著性报表以及多维分析和对比分析。

实验分流数据分为客户端分流埋点和服务端 ABTest SDK 分流日志两种,分别通过日志网关和 flume 收集到 HDFS 中。实验效果统计通过 mPaaS 客户端 SDK 自带的自定义事件埋点通过日志网关和 flume 回流到 HDFS。

  • 实时计算链路

数据通过 Kafka 导入 Kepler,kepler 任务进行分流日志和业务转化日志的双流 join,和实验 PUV 统计,最终将计算结果转储至 HBase,ABTest Console 展现结果。

  • 离线计算链路

数据通过 flume 导入 HDFS,由离线计算平台进行离线指标的计算和 ABTest 元数据和结果的同步,数据回流Hbase, ABTest console 展现结果。离线链路用于计算利己显著性报表、自定义指标已经留存等计算量较大的场景。

  • 即时分析链路

离线链路还会将预处理的部分数据回流到 LDAP 系统 Explorer,ABTest Console 利用 Explorer 做更为灵活的多维分析和漏斗分析

mPaas指标计算体系

针对 mPaaS 的客户端用户行为采取自定义事件进行埋点,用户只需要通过关联特定的自定义事件即可自动产生 T+1 的实验统计效果数据。以每个用户为独立的实验个体,这里面我们认为两个用户之间的行为是独立的,而用户在实验期间的每次行为是有相关性的,通过实验期间所有的用户行为进行统计分析。

对于除 sum 和 count 类指标我们都会进行区间估计(计算指标统计的置信区间,实验方案间对比的绝对差置信区间和相对差置信区间),以及基于检验假设计算 p-value 给出显著性统计结论。

  • 全局指标

实验期间进入各个方案的人数(累计 UV)和次数(累计 PV)作为基础的实验分流指标;另外一部分全局指标为留存类指标,以用户第一次进入实验开始,在第二天活跃则记为次日留存,依次类推,我们可以计算 2,3,…,7 日留存等。

  • 简单型指标

简单型指标由单个自定义事件构成,在用户配置一个自定义事件之后,会自动生成对应的实验指标:包括 PV 总数(事件触发次数),UV 总数(事件触发总用户数),UV 转化率(实验用户中触发了该自定义事件的用户占比),均值(触发总次数/进入实验方案的用户数)。

  • 复合型指标

复合型指标是为了计算多个自定义事件组成的相关统计效果,在基础的集合运算中包含交并差除,那么我们在复合型指标中也支持这四种运算。这里面我们以两个自定义事件 E1,E2 为例:

并(E1+E2):表示用户只要发生了 E1 或者 E2 即认为该用户触发了(E1+E2)事件,那么事件总触发次数即为触发E1总次数+触发E2总次数,其余相关指标计算就可以看做是一个简单型指标来进行处理。

交(E1*E2):表示用户既触发了 E1,也触发了 E2,这个里面我们可以基于单个 session 来看,这样事件触发总次数就是同时触发了E1和E2的总会话数也可以默认为 1(目前 mPaaS 直接认为 1,即相交的计算我们主要查看 UV 的转化),其他相关指标计算就可以看做是一个简单性指标来进行处理。

差(E1-E2):表示用户触发了 E1 没有触发 E2,事件总触发次数则是满足该条件的用户触发 E1 的总次数,其余相关指标计算就可以看做是一个简单型指标来处理。

除(E1/E2):这个我们成为转化类事件,最简单的例子就是 E1 事件表示某个按钮的点击,E2 事件表示某个按钮的曝光,那么 pv 转化率就是 sum(E1)/sum(E2) 即 pv-ctr,同样的 UV 转化率均值等口径也就清晰了。同样以曝光点击为例,在实际 pv-ctr 的方差计算时我们发现一个用户多次曝光之间其实是有相关性的,那么在计算实际方差的时候我们利用泰勒展开和 E1,E2 的协相关系数,对方差公式进行了优化:

假设 x1,x2,…xn 为每个用户点击次数,y1,y2,…,yn 为每个用户曝光次数,则

以上,是移动端 ABTest 的具体技术架构解析以及在支付宝内如何落地实践的总结。如果对 ABTest 感兴趣,大家可以进一步关注 mPaaS 后续文章及产品迭代更新。

往期阅读

《开篇 | 蚂蚁金服 mPaaS 服务端核心组件体系概述》

《蚂蚁金服 mPaaS 服务端核心组件:亿级并发下的移动端到端网络接入架构解析》

《mPaaS 核心组件:支付宝如何为移动端产品构建舆情分析体系?》

《mPaaS 服务端核心组件:移动分析服务 MAS 架构解析》

《蚂蚁金服面对亿级并发场景的组件体系设计》

《自动化日志收集及分析在支付宝 App 内的演进》

关注我们公众号,获得第一手 mPaaS 技术实践干货

QRCode

钉钉群:通过钉钉搜索群号“23124039”

期待你的加入~

本文转载自: 掘金

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

MySQL延迟问题和数据刷盘策略

发表于 2019-07-03

一、MySQL复制流程

官方文档流程图如下:

1、绝对的延时,相对的同步

2、纯写操作,线上标准配置下,从库压力大于主库,最起码从库有relaylog的写入。

二、MySQL延迟问题分析

1、主库DML请求频繁

原因:主库并发写入数据,而从库为单线程应用日志,很容易造成relaylog堆积,产生延迟。

解决思路:做sharding,打散写请求。考虑升级到MySQL 5.7+,开启基于逻辑时钟的并行复制。

2、主库执行大事务

原因:类似主库花费很长时间更新了一张大表,在主从库配置相近的情况下,从库也需要花几乎同样的时间更新这张大表,此时从库延迟开始堆积,后续的events无法更新。

解决思路:拆分大事务,及时提交。

3、主库对大表执行DDL语句

原因:DDL未开始执行,被阻塞,检查到位点不变;DDL正在执行,单线程应用导致延迟增加,位点不变。

解决思路:找到被阻塞DDL或是写操作的查询,干掉该查询,让DDL正常在从库上执行;业务低峰期执行,尽量使用支持Online DDL的高版本MySQL。

4、主从实例配置不一致

原因:硬件上:主库实例服务器使用SSD,而从库实例服务器使用普通SAS盘、cpu主频不一致等;配置上:如RAID卡写策略不一致,OS内核参数设置不一致,MySQL落盘策略(innodb_flush_log_at_trx_commit和sync_binlog等)不一致等

解决思路:尽量统一DB机器的配置(包括硬件及选项参数);甚至对于某些OLAP业务,从库实例硬件配置高于主库等。

5、从库自身压力过大

原因:从库执行大量select请求,或业务大部分select请求被路由到从库实例上,甚至大量OLAP业务,或者从库正在备份等,此时可能造成cpu负载过高,io利用率过高等,导致SQL Thread应用过慢。

解决思路:建立更多从库,打散读请求,降低现有从库实例的压力。

也可以调整innodb_flush_log_at_trx_commit=0和sync_binlog=0刷盘参数来缓解IO压力来降低主从延迟。

三、大促期间CPU过高问题

现象:

高并发导致CPU负载过高,处理请求时间拉长,逐步积压,最终导致服务不可用;大量的慢SQL导致CPU负载过高。

解决思路:

基本上是禁止或是慎重考虑数据库主从切换,这个解决不了根本问题,需要研发配合根治SQL问题,也可以服务降级,容器的话可以动态扩容CPU;和业务协商启动pt-kill查杀只读慢SQL;查看是否可以通过增加一般索引或是联合索引来解决慢SQL问题,但此时要考虑DDL对数据库影响。

四、InnoDB刷盘策略

MySQL的innodb_flush_method这个参数控制着innodb数据文件及redo log的打开、刷写模式,对于这个参数,文档上是这样描述的:
有三个值:fdatasync(默认),O_DSYNC,O_DIRECT
默认是fdatasync,调用fsync()去刷数据文件与redo log的buffer
为O_DSYNC时,innodb会使用O_SYNC方式打开和刷写redo log,使用fsync()刷写数据文件
为O_DIRECT时,innodb使用O_DIRECT打开数据文件,使用fsync()刷写数据文件跟redo log
首先文件的写操作包括三步:open,write,flush
上面最常提到的fsync(int fd)函数,该函数作用是flush时将与fd文件描述符所指文件有关的buffer刷写到磁盘,并且flush完元数据信息(比如修改日期、创建日期等)才算flush成功。
使用O_DSYNC方式打开redo文件表示当write日志时,数据都write到磁盘,并且元数据也需要更新,才返回成功。
O_DIRECT则表示我们的write操作是从MySQL innodb buffer里直接向磁盘上写。

这三种模式写数据方式具体如下:

fdatasync模式:写数据时,write这一步并不需要真正写到磁盘才算完成(可能写入到操作系统buffer中就会返回完成),真正完成是flush操作,buffer交给操作系统去flush,并且文件的元数据信息也都需要更新到磁盘。
O_DSYNC模式:写日志操作是在write这步完成,而数据文件的写入是在flush这步通过fsync完成
O_DIRECT模式:数据文件的写入操作是直接从mysql innodb buffer到磁盘的,并不用通过操作系统的缓冲,而真正的完成也是在flush这步,日志还是要经过OS缓冲。

1、在类unix操作系统中,文件的打开方式为O_DIRECT会最小化缓冲对io的影响,该文件的io是直接在用户空间的buffer上操作的,并且io操作是同步的,因此不管是read()系统调用还是write()系统调用,数据都保证是从磁盘上读取的;所以IO方面压力最小,对于CPU处理压力上也最小,对物理内存的占用也最小;但是由于没有操作系统缓冲的作用,对于数据写入磁盘的速度会降低明显(表现为写入响应时间的拉长),但不会明显造成整体SQL请求量的降低(这有赖于足够大的innodb_buffer_pool_size)。

2、O_DSYNC方式表示以同步io的方式打开文件,任何写操作都将阻塞到数据写入物理磁盘后才返回。这就造成CPU等待加长,SQL请求吞吐能力降低,insert时间拉长。

3、fsync(int filedes)函数只对由文件描述符filedes指定的单一文件起作用,并且等待写磁盘操作结束,然后返回。fdatasync(int filedes)函数类似于fsync,但它只影响文件的数据部分。而除数据外,fsync还会同步更新文件的元信息到磁盘。

O_DSYNC对CPU的压力最大,datasync次之,O_DIRECT最小;整体SQL语句处理性能和响应时间看,O_DSYNC较差;O_DIRECT在SQL吞吐能力上较好(仅次于datasync模式),但响应时间却是最长的。

默认datasync模式,整体表现较好,因为充分利用了操作系统buffer和innodb_buffer_pool的处理性能,但带来的负面效果是free内存降低过快,最后导致页交换频繁,磁盘IO压力大,这会严重影响大并发量数据写入的稳定性。

本文转载自: 掘金

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

记一次关于 Mysql 中 text 类型和索引问题引起的慢

发表于 2019-07-03

最近有用户反馈产品有些页面加载比较慢,刚好我在学习 Mysql 相关知识,所以先从 Mysql 慢查询日志开始定位:

step1:通过慢查询日志定位具体 SQL

首先通过 SHOW VARIABLES like 查看当前 Mysql 服务器关于慢查询的具体配置信息:

1
2
3
4
5
复制代码slow_query_log = ON                  # 慢查询日志处于开启状态,所以可以直接查询
slow_query_type = 1 # 根据运行时间将 SQL 语句中记录到 slow log 中,而不考虑逻辑 IO 次数
long_query_time = 5.000000 # 凡是超过 5 秒以上的 SQL 都会记录到 slow log 中
log_output = TABLE # slow log 记录到 mysql.slow_log 表中
log_queries_not_using_indexes = OFF # 没有使用索引的 SQL 不会记录到 slow_log 中,刚好我们只关心查询时间慢的 SQL

确认了 Mysql 服务器对慢查询的配置满足需求,我们不需要再修改任何配置,直接抓取对应时间点的慢查询日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码-- 查看7月1日从9点半到10点半的slow log,并找出每条慢查询SQL的最长查询时间以及查询次数,并按照查询时间排序
SELECT
db,
start_time,
max(query_time) AS max_query_time,
CONVERT (sql_text USING utf8) AS sqlText, -- sql_text 是 blob 类型,我们需要 CONVERT 到 varchar 来识别具体 SQL
count(1) AS count
FROM
mysql.slow_log
WHERE
start_time > "2019-07-01 09:30:00.000000"
AND start_time < "2019-07-01 10:30:00.000000"
GROUP BY
sql_text
ORDER BY
max_query_time DESC

最终我们找到了服务器上四条不同的 slow log sql,最长查询时间分别是 9秒,8秒,7秒,6秒:

image

step2:使用 explain 分析 SQL 执行计划

刚好上周末写了一篇 使用 explain 优化你的 mysql 性能,可以直接上手,先对第一条 SQL 作分析:

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
复制代码mysql> EXPLAIN SELECT
t.*, p.id AS projectId
FROM
table_extract t
LEFT JOIN data_connection dc ON dc.id = t.data_connection_id
LEFT JOIN project p ON p.id = dc.project_id
WHERE
p.id IN (
700201361,
700201360,
700201359,
700201358,
700201357,
700201356,
700201354,
700201353,
700201351,
700201350,
700201347
);
+----+-------------+-------+------------+--------+---------------------------------------------------------+---------+---------+------------------------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+--------+---------------------------------------------------------+---------+---------+------------------------------+------+----------+-------------+
| 1 | SIMPLE | t | NULL | ALL | NULL | NULL | NULL | NULL | 2159 | 100.00 | NULL |
| 1 | SIMPLE | dc | NULL | eq_ref | PRIMARY,index_data_connection_project_id,idx_project_id | PRIMARY | 4 | youdata.t.data_connection_id | 1 | 100.00 | Using where |
| 1 | SIMPLE | p | NULL | eq_ref | PRIMARY | PRIMARY | 4 | youdata.dc.project_id | 1 | 100.00 | Using index |
+----+-------------+-------+------------+--------+---------------------------------------------------------+---------+---------+------------------------------+------+----------+-------------+
3 行于数据集 (0.05 秒)

通过上述输出结果没发现什么大的问题,两次关联查询都使用了 type = eq_ref,并且都使用了索引,只是对于 table_extract 这张表的查询数据库走了全表扫描,这个确实没办法,我们需要获取该表中除了索引以外的其它字段,但是这张表的数据量也只有rows=2159行,所以理论上也不会有问题,所以这条 SQL 通过 explain 没有发现什么大问题,后面会继续分析。

接下来再看第二条 SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码mysql> EXPLAIN SELECT
date(create_time) AS days,
count(create_time) AS dayView
FROM
resource_operation_record
WHERE
resource_type IN ('NEW_REPORT', 'COCKPIT')
AND `action` = 'VIEW'
AND resource_id = 4539
AND create_time > '2019-06-25 00:00:00'
AND create_time < '2019-07-01 09:45:19'
GROUP BY
days;
+----+-------------+---------------------------+------------+------+-----------------+------+---------+------+---------+----------+----------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------------------------+------------+------+-----------------+------+---------+------+---------+----------+----------------------------------------------+
| 1 | SIMPLE | resource_operation_record | NULL | ALL | resource_id_idx | NULL | NULL | NULL | 1729523 | 0.02 | Using where; Using temporary; Using filesort |
+----+-------------+---------------------------+------------+------+-----------------+------+---------+------+---------+----------+----------------------------------------------+
1 行于数据集 (0.05 秒)

首先 possible_keys 字段告诉我们可能用到的索引 resource_id_idx,可是为什么 key 字段里没有真正用到索引呢?这应该是 Mysql 优化器认为使用索引对该查询优化空间不大,或者说可能会使性能更差。加上 Extra 字段里还有 Using filesort,Using temporary,在将近 rows = 200万 的数据里进行全表扫描,查询时间超过 5 秒再正常不过了。所以我们查看一下索引信息来定位一下为什么没有使用 resource_id_idx 索引:

1
2
3
4
5
6
7
8
9
复制代码mysql> show index from resource_operation_record;
+---------------------------+------------+-----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+---------------------------+------------+-----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| resource_operation_record | 0 | PRIMARY | 1 | id | A | 1646744 | NULL | NULL | | BTREE | | |
| resource_operation_record | 1 | creator_id_idx | 1 | creator_id | A | 1169 | NULL | NULL | YES | BTREE | | |
| resource_operation_record | 1 | resource_id_idx | 1 | resource_id | A | 4228 | NULL | NULL | YES | BTREE | | |
+---------------------------+------------+-----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
3 行于数据集 (0.04 秒)

resource_operation_record 这张表上一共三个索引,id 是自增主键,这个可以先不用管。对于其它两个索引 creator_id_idx 和 resource_id_idx,首先看到 Cardinality 这个值和 id 聚集索引差距好大,Cardinality 这个值表示索引中不重复的预估值,该值很关键,它和表中总行数比值越接近 1 越好,而且优化器会根据该值选择是否使用索引优化,关于 InnoDB 索引和 Cardinality 相关内容可以看 InnoDB 存储引擎的索引和算法学习 这篇文章。resource_id_idx 的可选择太小了,比例只有 0.0025,看来优化器不选择该索引是正常的,所以我们大部分情况下要相信 Mysql 优化器。我们也可以使用 force index(resource_id_idx) 强制使用索引来观察效果:

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
复制代码 SELECT
date(create_time) AS days,
count(create_time) AS dayView
FROM
resource_operation_record
force index(resource_id_idx) -- 使用 force index 强制使用索引
WHERE
resource_id = 4539
AND resource_type IN ('NEW_REPORT', 'COCKPIT')
AND `action` = 'VIEW'
AND create_time > '2019-06-25 00:00:00'
AND create_time < '2019-07-01 09:45:19'
GROUP BY
days;
+------------+---------+
| days | dayView |
+------------+---------+
| 2019-06-28 | 29 |
| 2019-06-29 | 2 |
| 2019-06-30 | 2 |
| 2019-07-01 | 5 |
+------------+---------+
4 行于数据集 (1.67 秒)
-- 查询要 1.67 秒,相同情况下,我不使用 force index 要 1.61 秒,比使用索引还要快,当然这个不同时间点查询也有关系
-- 总之,使用索引确实没有多大提升

再观察上述查询,我们发现 select 和 where 条件中用到了 create_time,而且这个 create_time 是数据插入的时间,理论上不会有太多重复的,尝试在 create_time 上创建索引:

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
复制代码-- 新建索引
ALTER TABLE `resource_operation_record` ADD INDEX `create_time_idx` USING BTREE (`create_time`);

-- 查看索引信息
show index from resource_operation_record;
+---------------------------+------------+-----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+---------------------------+------------+-----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| resource_operation_record | 0 | PRIMARY | 1 | id | A | 1739371 | NULL | NULL | | BTREE | | |
| resource_operation_record | 1 | creator_id_idx | 1 | creator_id | A | 1002 | NULL | NULL | YES | BTREE | | |
| resource_operation_record | 1 | resource_id_idx | 1 | resource_id | A | 6988 | NULL | NULL | YES | BTREE | | |
| resource_operation_record | 1 | create_time_idx | 1 | create_time | A | 1246230 | NULL | NULL | YES | BTREE | | |
+---------------------------+------------+-----------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
4 行于数据集 (0.25 秒)

-- 查看执行计划
mysql> EXPLAIN SELECT
date(create_time) AS days,
count(create_time) AS dayView
FROM
resource_operation_record
WHERE
resource_type IN ('NEW_REPORT', 'COCKPIT')
AND `action` = 'VIEW'
AND resource_id = 4539
AND create_time > '2019-06-25 00:00:00'
AND create_time < '2019-07-01 09:45:19'
GROUP BY
days;

+----+-------------+---------------------------+------------+-------+---------------------------------+-----------------+---------+------+--------+----------+---------------------------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------------------------+------------+-------+---------------------------------+-----------------+---------+------+--------+----------+---------------------------------------------------------------------+
| 1 | SIMPLE | resource_operation_record | NULL | range | resource_id_idx,create_time_idx | create_time_idx | 5 | NULL | 210240 | 0.20 | Using index condition; Using where; Using temporary; Using filesort |
+----+-------------+---------------------------+------------+-------+---------------------------------+-----------------+---------+------+--------+----------+---------------------------------------------------------------------+
1 行于数据集 (0.19 秒)

建立索引的基础上我们再查看执行计划和索引信息,create_time_idx 的 Cardinality 变为 1246230,选择性大于 0.7,优化器自然会选择该索引,果然 explain 出来的结果是 type = range,使用了范围索引查询,并且 extra 里增加了 Using index condition,表示会先条件过滤索引,过滤完索引后找到所有符合索引条件的数据行,随后用 WHERE 子句中的其他条件去过滤这些数据行。

最后执行查询看一下优化后的效果,相比以前 1.7 秒,速度提升了 5 倍左右,这个优化到此为止,因为这个表是对用户访问记录的统计,后面可以考虑针对时间分区进行优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码SELECT
date(create_time) AS days,
count(create_time) AS dayView
FROM
resource_operation_record
WHERE
resource_id = 4539
AND resource_type IN ('NEW_REPORT', 'COCKPIT')
AND `action` = 'VIEW'
AND create_time > '2019-06-25 00:00:00'
AND create_time < '2019-07-01 09:45:19'
GROUP BY
days;
4 行于数据集 (0.35 秒)

接下来还有两个慢查询 SQL,这两个慢查询 SQL 和第一个 SQL 一样通过 explain 输出结果看不出什么效果,所以接下来我们通过 profile 查看这三个 SQL 性能

step3:使用 show profile 继续定位

可以通过文章 学习如何统计 Mysql 服务器状态信息 来了解如何使用 SHOW STATUS,SHOW ENGINE INNODB STATUS,SHOW PROCESSLIST,SHOW PROFILE 来查看 Mysql 服务器状态信息。

Mysql 5.1 版本开始支持 SHOW PROFILE 功能,它可以高精度的记录每个查询语句在运行过程中各个操作的执行时间,这个功能可能会影响 Mysql 查询性能,所以默认情况下是关闭的,由于我们临时定位问题,可以短暂开启该功能:

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
复制代码-- 开启 profiling 功能
mysql> SET global profiling = ON;
Query OK, 0 rows affected, 1 warning (0.00 sec)

-- 执行第三条慢查询 SQL
SELECT
t.id AS id,
t. NAME AS NAME,
data_connection_id AS dataConnectionId,
QUERY,
init_sql AS initSql,
t.produced AS produced,
t.creator_id AS creatorId,
t.create_time AS createTime,
u.nick AS creatorName
FROM
custom_table AS t
LEFT JOIN bigviz_user AS u ON t.creator_id = u.id
WHERE
t.data_connection_id = 20;
800 行于数据集 (1.1 秒)

-- 根据 show profiles 找到对应的 Query_Id = 8,对应执行时间为 1.1 秒
show profiles

-- 具体查看每一步的耗时情况
mysql> show profile for query 8;
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| starting | 0.000222 |
| checking permissions | 0.000030 |
| checking permissions | 0.000027 |
| Opening tables | 0.000049 |
| init | 0.000062 |
| System lock | 0.000035 |
| optimizing | 0.000037 |
| statistics | 0.000063 |
| preparing | 0.000048 |
| executing | 0.000025 |
| Sending data | 1.101708 |
| end | 0.000090 |
| query end | 0.000034 |
| closing tables | 0.000088 |
| freeing items | 0.000055 |
| logging slow query | 0.000030 |
| Opening tables | 0.000159 |
| System lock | 0.000100 |
| cleaning up | 0.000041 |
+----------------------+----------+
19 rows in set, 1 warning (0.00 sec)

通过 show profile 返回数据可以发现,基本上所有的时间都花在了 “Sending data” 上,我们查看 Mysql 官方文档对 “Sending data” 的说明:

1
2
3
复制代码The thread is reading and processing rows for a SELECT statement, and sending data to the client. 
Because operations occurring during this state tend to perform large amounts of disk access (reads),
it is often the longest-running state over the lifetime of a given query.

也就是说 “Sending data” 并不是单纯的发送数据,而是包括“收集 + 发送数据”,这个阶段一般是 query 中最耗时的阶段,那么为什么这个只有 800 行的查询会耗时这么久呢,难道这 800 行中平均每行数据量都很大?所以看一下该表定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码mysql> show create table custom_table\G;
*************************** 1. row ***************************
Table: custom_table
Create Table: CREATE TABLE `custom_table` (
`name` varchar(2000) DEFAULT NULL,
`produced` varchar(255) DEFAULT 'UserDefinedSQL',
`query` longtext,
`project_id` int(11) DEFAULT NULL,
`data_connection_id` int(11) DEFAULT NULL,
`creator_id` int(11) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`modifier_id` int(11) DEFAULT NULL,
`modify_time` datetime DEFAULT NULL,
`id` int(11) NOT NULL AUTO_INCREMENT,
`init_sql` text COMMENT '初始化sql',
`rely_list` text COMMENT '表依赖关系',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=27975 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

上述表结构定义中发现有三个字段 query,init_sql,rely_list 都是 text 类型字段,而且 query 字段还是 longtext,从我们怀疑出发,试着在 select 查询中去掉 init_sql 和 query 的查询后再观察结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码SELECT
t.id AS id,
t. NAME AS NAME,
data_connection_id AS dataConnectionId,
t.produced AS produced,
t.creator_id AS creatorId,
t.create_time AS createTime,
u.nick AS creatorName
FROM
custom_table AS t
LEFT JOIN bigviz_user AS u ON t.creator_id = u.id
WHERE
t.data_connection_id = 20;
800 行于数据集 (0.04 秒)

天哪,从 1 秒左右变成了 0.04 秒,完全不是一个数量级的,看来 text 类型的字段对整个查询影响太大了,我们先不急追究为什么,先看如何在当前业务上优化查询,由于考虑到业务场景是前端获取 custom_table 在某个 data_connection_id 下的列表,会返回 query,init_sql 这两个字段,这个两个字段用户确实会用到,但是只有在用户点击某个 custom_table 进行编辑或者查看详情时才会用到,那我们为什么不考虑延迟获取呢?只有当用户需要查看详情时再根据主键 ID 去获取对应的信息,这个时候属于 const 查询且只有一行数据,代价非常小。

而对于第一个慢查询 SQL,直接使用 select * 去查询,这个表里面包好了多个 text 字段,而该业务需求其实只需要 id 和 used_memory(biginit) 两个字段,所以我们优化成只选择其中两个字段进行查询。

对于第四个慢查询 SQL,对应的表结构里面也包含了一个 mediumtext 字段,前端界面上用户需要根据该字段里的文本信息进行搜索,但是该场景使用很少,只有当用户切换到对应的”按照字段名称搜索“时才会用到该字段,默认情况下不会用到该字段,所以我们可以在用户切换到对应的搜索时再返回该字段,默认情况下不返回即可。

针对以上三个慢查询 SQL 我们在不改变表结构的情况下,通过修改业务处理逻辑都成功解决了问题,下面是对于 Mysql 在使用 text 和 blob 类型时以及索引查询时的一些优化建议:

step4: 如何优化 Mysql 中 text 和 blob 类型:

什么是行溢出数据?
  • InnoDB 会将一些大对象数据存放在数据页之外的 BLOB 页中,然后在查询时根据指针去对应的 BLOB 页中查询。
  • 要不要将数据放在 BLOB 页中,取决于当前页中是否可以存放下至少两行数据,对于默认是 16 KB 大小的页,这个阈值长度是 8098,大于该值的会存放在 BLOB 页中。
  • BLOB 不只存放 text 和 blob 类型,varchar 类型的数据也有可能被存放在 BLOB 页中,而 blob 类型和 text 类型的数据也有可能不被存放在 BLOB 页中。
  • 对于 Compact 和 Redundant 行存储格式存放的数据,采用的是部分行溢出存储,前 768 字节还是会存放在当前数据页中的。
  • 对于 Compressed 和 Dynamic 行存储格式存放的数据,采用的完全行溢出存储,只用 20 个字节存放指针,其余所有数据都放在行溢出数据中。
为什么要尽量少使用 text 和 blob 类型?
  • 首先对于 text 和 blob 类型,在遇到使用临时表的情况时,无法使用内存临时表,只能在磁盘上创建临时表。
  • 对于行溢出数据,InnoDB 一次只会为一个列分配一页的空间,但是当该列超过 32 个页后会一次性分配 64 个页面,存储空间有一定的浪费。
  • 行溢出数据禁用了自适应哈希索引,如果作为 where 条件时必须完整的比较整个列。
  • 对于 text 和 blob 字段进行排序时,只能使用部分前缀进行排序,默认是 1024 字节,可以通过 max_sort_length 进行设置。
  • 数据量太大,会导致 InnoDB 每个数据页中存放的行数减少,从而影响对页面的缓存。
  • 如果存放在行溢出数据中,每次会根据指针去对应的溢出页进行查询,增加页面访问次数,而且每次查询都是随机 IO,text 字段越多查询次数越多。
如何优化查询?
  • 如果有许多大字段,可以考虑合并这些字段到一个字段,存储一个大的 200kb 比存储 20 个 10kb 更高效,检查随机页面访问次数。
  • 查询时尽量避免对大字段查询,尤其是获取列表时,杜绝使用 select * 查询。
  • 可以考虑将大字段专门放在另外一张表中,只有在需要时再关联查询,增加 InnoDB 的当前表缓存命中率。
  • 如果只需要获取大字段的部分数据,可以使用 SUBSTRING( ) 函数,这样可以避免使用磁盘临时表。
  • 如果必须使用到磁盘临时表,可以考虑将磁盘临时表指向在基于内存的文件系统中,可以通过修改 tmpdir 参数实现。
  • 必要时可以考虑对大字段进行压缩后再存储到表中。
  • 尽量不要使用大字段作为 where 中的查询条件。

step5: 如何正确使用索引

  • 创建索引时尽量选择 Cardinality 值比较大的字段,你可以通过 explain 观察自己创建的索引到底有没有被使用
  • order by 中的排序的列如果建了索引,则可以使用直接索引进行排序,优化性能
  • 在使用索引时对应的索引列必须独立,不能是表达式的一部分也不能是函数的参数,否则不能使用索引:
1
2
复制代码-- 虽然 id 上建立了索引,但是无法使用索引优化
select id from user where id + 1 =5;
  • 当服务器出现多个列做 AND 操作查询时,通常需要建了一个多列索引,而不是多个独立的单列索引
  • 当不需要考虑排序和分组时,将选择性最高的列放在前面通常是最好的,因为可以很快的过滤出需要的行
  • 如果索引包含了需要查询的所有字段值,那么就是可以使用覆盖索引查询,只需要读取索引,极大地减少了数据访问量,在 EXPLAIN 分析的 Extra 字段中可以看到 “Using index” 信息
  • 如果查询中某个列是范围查询,那么其右边的所有列将无法使用索引优化,索引尽量将范围条件放在右边或者使用多个等值条件来代替范围查询
  • 查询时尽量不要返回多余的列,第一可以减少网络流量,第二增加使用覆盖索引的可能性
  • 多列索引时只有当索引的列和 ORDER BY 子句的顺序完全一致且所有列的排序方向一致时才能使用索引做排序
  • 不要创建冗余的索引,Mysql 不仅需要单独维护索引列,并且在优化器查询时也需要逐个索引进行过滤,会影响性能,下面是创建冗余索引的几个例子:
1
2
复制代码- 创建了索引(A,B)再创建索引(A),那后者便是冗余索引
- 创建索引扩展为(A,ID),其中 ID 是主键,对于 InnoDB 来说主键已经包含在二级索引中了,所以这也是冗余的
  • 有一些索引可能服务器永远都不会用到,建议考虑删除,在 percona 版本或 marida 中可以通过 information_schea.index_statistics 查看得到索引的使用情况,在官方版本中 可以使用 performance_schema.table_io_waits_summary_by_index_usage 查看索引使用情况

参考文献

  • 使用 explain 优化你的 mysql 性能
  • 学习如何统计 Mysql 服务器状态信息
  • InnoDB 存储引擎的索引和算法学习

本文转载自: 掘金

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

ARP协议

发表于 2019-07-02

ARP地址解析协议(IP地址—>MAC地址)

地址解析协议,即ARP(Address Resolution Protocol),是根据IP地址获取物理地址的一个TCP/IP协议。主机与主机之间的通信在物理上实质是网卡与网卡之间的通信,而网卡只认识MAC地址,所以要想实现主机与主机之间的通信,需要知道对方IP地址所对应的MAC地址,完成这一过程的协议就是ARP协议。在具体的网络传输过程中,使用地址解析协议,可根据网络层IP数据包包头中的IP地址信息解析出目标硬件地址(MAC地址)信息,以保证通信的顺利进行。

ARP工作原理

每台主机或路由器都有一个ARP缓存表,用来保存IP地址与MAC地址的对应关系。

以主机A(192.168.1.5)向主机B(192.168.1.1)发送数据为例。当发送数据时,主机A会在自己的ARP缓存表中寻找是否有目标IP地址。如果找到了,也就知道了目标MAC地址,直接把目标MAC地址写入帧里面发送就可以了;如果在ARP缓存表中没有找到目标IP地址,主机A就会在网络上发送一个广播arp request,请求包中包含了A主机的ip地址和mac地址。网络上其他主机并不响应ARP询问,直接丢弃,只有主机B接收到这个帧时,才以单播方式向主机A做出回应arp reply,并带上自己的ip和mac地址,而B主机收到A的请求包时也会将A主机的IP与MAC对应关系保存在自己的缓存区。A收到B的回应包后便可得知B的MAC地址,将其存入ARP缓存。此后A再向B发送数据时,就可以直接从缓存表中查找B的地址了,然后直接把数据发送给B。由于B在接收A的请求时也保存了A的地址信息,因此B要向A发送数据也可以直接从缓存表中查找。

ARP缓存表设置了生存时间TTL,在一段时间内(一般15到20分钟,跟操作系统有关)如果表中的某一行没有使用,就会被删除,这样可以大大减少ARP缓存表的长度,加快查询速度。

ARP封装

可以对ARP报文有个直观的认识,深入的话可以结合WireShark抓包学习。

在这里插入图片描述

广播请求:

在这里插入图片描述

回应:

在这里插入图片描述

补充一个以太网帧封装:

在这里插入图片描述

再补充一个ARP协议在TCP/IP协议中的位置:

在这里插入图片描述

arp命令

可通过如下命令查看高速缓存中的所有项目:

1
复制代码arp -a

其他选项:

1
2
3
4
5
复制代码-a 显示所有接口的当前ARP缓存表;

-v 显示详细信息;

-n 以数字地址形式显示;

RARP协议(MAC地址—>IP地址)

反向地址转换协议,RARP(Reverse Address Resolution Protocol),就是将局域网中某个主机的物理地址转换为IP地址。

MAC地址

MAC(Medium/Media Access Control)地址,用来表示互联网上每一个站点的标识符,采用十六进制数表示,共六个字节(48位)。其中,前三个字节是由IEEE的注册管理机构RA负责给不同厂家分配的代码(高位24位),也称为“编制上唯一的标识符”(Organizationally Unique Identifier),后三个字节(低位24位)由各厂家自行指派给生产的适配器接口,称为扩展标识符(唯一性)。一个地址块可以生成224个不同的地址。MAC地址实际上就是适配器地址或适配器标识符EUI-48。

关注微信公众号,每天进步一点点!

本文转载自: 掘金

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

采坑系列之--dubbo异步调用传递性导致嵌套调用返回nul

发表于 2019-07-01

一、现象

有三个应用serviceA,serviceB,serviceC,在确保消费没有错乱的前提下(都只有单个服务提供者),期望其调用关系为

serviceA dubbo消费serviceB 配置为异步消费async=”true”,配置如下:

1
2
3
4
5
复制代码// serviceA 异步消费serviceB 配置如下
<dubbo:reference id="serviceB" interface="cn.ServiceB"
version="1.0.0" check="false">
<dubbo:method name="reRunJob" async="true"/>
</dubbo:reference>
1
2
3
4
5
复制代码// serviceB 同步消费serviceC配置如下
<dubbo:reference id="serviceC" interface="cn.ServiceC"
version="1.0.0" check="false">
<dubbo:method name="xxx"/>
</dubbo:reference>

然而,上面配置后,实际调用关系变为下图

如上所述,由于B->C 由于dubbo异步配置的传递性,导致变为了异步调用,结果返回了null,导致期望的同步调用结果异常,但是B第二次调用C会正常返回

二、寻找问题根源–源码

1. 我们的排查思路

现象是由于dubbo异步调用,然后服务提供者内部又有dubbo嵌套调用,==所以我们需要找出dubbo的内部嵌套调用是否存在异步传递性==,那么既然是传递,就需要上下文环境,进而,我们想到了dubbo中的上下文环境==RpcContext==,我们推测是由此类传递了异步参数

2. 预备知识:RpcContext简介

RpcContext 是一个 ThreadLocal 的临时状态记录器,当接收到 RPC 请求,或发起 RPC 请求时,RpcContext 的状态都会变化。

比如:A调B,B再调C,则B机器上,在B调C之前,==RpcContext记录的是A调B的信息==,在B调C之后,RpcContext记录的是B调C的信息。

我们知道dubbo的方法调用,都是由invoker代理调用的,我们找到AbstractInvoker,查看底层的invoke方法,源码如下:

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
复制代码public Result invoke(Invocation inv) throws RpcException {
··· 省略无关代码

// 这里的代码在此处对我们的serviceB-->serviceC时,会取出RpcContext,语句上面的RpcContext知识储备,我们知道,这里的context存的是serviceA->serviceB的上下文,也就是说是异步的,到这里谜团解开
Map<String, String> context = RpcContext.getContext().getAttachments();
if (context != null) {
invocation.addAttachmentsIfAbsent(context);
}
// 注意此处代码,如果提供者方法配置了异步参数
if (getUrl().getMethodParameter(invocation.getMethodName(), Constants.ASYNC_KEY, false)){
// 会将异步参数值设置到当前调用对象中 invocation.setAttachment(Constants.ASYNC_KEY, Boolean.TRUE.toString());
}
// 最后再将异步参数存入上下文
RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);


try {
return doInvoke(invocation);
} catch (InvocationTargetException e) { ···
} catch (RpcException e) {
···
} catch (Throwable e) {
···
}
}

3. 上面还有个小问题,serviceB第二次调用serviceC,会正常返回,这又是为什么呢?

这里涉及到一个Filter ConsumerContextFilter 源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码@Activate(group = Constants.CONSUMER, order = -10000)
public class ConsumerContextFilter implements Filter {

public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
RpcContext.getContext()
.setInvoker(invoker)
.setInvocation(invocation)
.setLocalAddress(NetUtils.getLocalHost(), 0)
.setRemoteAddress(invoker.getUrl().getHost(),
invoker.getUrl().getPort());
if (invocation instanceof RpcInvocation) {
((RpcInvocation)invocation).setInvoker(invoker);
}
try {
return invoker.invoke(invocation);
} finally {
// ①注意这里 ,进行了上下文清理
RpcContext.getContext().clearAttachments();
}
}

}

==如上代码,serviceB第一次调用serviceC结束时,在consumer的filter chain中,有一个ConsumerContextFilter,在调用结束后会执行RpcContext.getContext().clearAttachments()方法,清除RpcContext中的信息,也就清除了async标识==,所以,第二次调用serviceC,就正常==同步==调用了,至此,我们的疑问得到解释

解决方法

分析了问题产生的原因后,在不修改dubbo源码的情况,可以有一下几种处理方式。

  1. 将serviceB改为同步调用,如果业务上确实需要异步调用,有以下2种处理方式
  2. serviceB的方法无需返回值,可采用oneway的方式(在消费者端配置dubbo:method中return=”false”)
  3. 有返回值,并且需要异步,最简单的方式为在实现中使用线程池执行业务。
  4. 增加一个Provider端的Filter,保证在filter链的结尾,在执行方法前,清除attachment中的async标志。也可达到同样的效果

本文转载自: 掘金

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

最全的 JVM 面试知识点(三):垃圾收集器

发表于 2019-06-29

在涉及 Java 相关的面试中,面试官经常会让讲讲 Java 中的垃圾收集相关的理解和常见的分类。可见,光就应付面试而言,JVM 的垃圾收集也对每一位 Java 开发者很重要。除此之外,对于我们了解和解决 Java 应用的性能时,也很有帮助。

在上一篇介绍了 Java 虚拟机内存的垃圾收集算法。本章将会介绍 Java 中常用的垃圾收集器及其特性。

本文的主要内容:

  • 基本概念
    • 串行、并行和并发
    • JVM 垃圾收集中的串行、并行和并发
  • 串行垃圾回收器
    • Serial
    • Serial Old
  • 并行垃圾回收器
    • ParNew
    • Parallel
    • Parallel Old
    • CMS
    • G1(Garbage First)
  • 小结

基本概念

在介绍具体的垃圾回收器之前,我们先了解几个基本概念。

串行、并行和并发

计算机系统的信息交换有两种方式:并行数据传输方式和串行数据传输方式。

  • 串行:计算机中的串行是用 Serial 表示。A 和 B 两个任务运行在一个 CPU 线程上,在 A 任务执行完之前不可以执行 B。即,在整个程序的运行过程中,仅存在一个运行上下文,即一个调用栈一个堆。程序会按顺序执行每个指令。
  • 并行:并行性指两个或两个以上事件或活动在同一时刻发生。在多道程序环境下,并行性使多个程序同一时刻可在不同 CPU 上同时执行。比如,A 和 B 两个任务可以同时运行在不同的 CPU 线程上,效率较高,但受限于 CPU 线程数,如果任务数量超过了 CPU 线程数,那么每个线程上的任务仍然是顺序执行的。
  • 并发:并发指多个线程在宏观(相对于较长的时间区间而言)上表现为同时执行,而实际上是轮流穿插着执行,并发的实质是一个物理 CPU 在若干道程序之间多路复用,其目的是提高有限物理资源的运行效率。 并发与并行串行并不是互斥的概念,如果是在一个CPU线程上启用并发,那么自然就还是串行的,而如果在多个线程上启用并发,那么程序的执行就可以是既并发又并行的。

举个简单的例子,写代码和听音乐这两件事:

  • 我们在编码的时候为了保证注意力集中,其他什么也不干。在茶歇的时候才听音乐。
  • 我们在编码的时候,既可以写代码,也可以听音乐,这两件事同时进行。
  • 我们会经常写一段时间代码就歇一下听音乐,这两件事穿插着进行。

读者可以思考上面所述的串行、并行和并发。

JVM 垃圾收集中的串行、并行和并发

在 JVM 垃圾收集器中也涉及到如上的三个概念。

  • 串行(Serial):使用单线程进行垃圾回收的回收器。
  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。

在了解了这些概念之后,我们开始具体介绍常用的垃圾收集器。

串行垃圾回收器

如上所述,串行回收器是指使用单线程进行垃圾回收的回收器,每次回收时串行回收器只有一个工作线程,对于并发能力较弱的计算机来说,串行回收器的专注性和独占性往往有更好的表现。串行回收器可以在新生代和老年代使用,根据作用的堆空间不同,分为新生代串行回收器和老年代串行回收器。

Serial

Serial收集器是最古老的收集器,它的缺点是当Serial收集器想进行垃圾回收的时候,必须暂停用户的所有进程,即 STW(服务暂停)。到现在为止,它依然是虚拟机运行在 client 模式下的默认新生代收集器。

参数控制:-XX:+UseSerialGC 使用串行收集器。

Serial Old

Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。

UseSerialGC:开启此参数使用 Serial & Serial Old 搜集器(client 模式默认值)。

并行垃圾回收器

并行回收器是在串行回收器的基础上做了改进,它可以使用多个线程同时进行垃圾回收,对于计算能力强的计算机来说,可以有效的缩短垃圾回收所需的实际时间。

ParNew

ParNew 收集器是一个工作在新生代的垃圾收集器,它只是简单的将串行收集器多线程化,它的回收策略和算法和串行回收器一样。新生代并行,老年代串行;新生代复制算法、老年代标记-整理。

参数控制:-XX:+UseParNewGC 使用 ParNew 收集器;-XX:ParallelGCThreads 限制线程数量
除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。

Parallel

Parallel 是采用复制算法的多线程新生代垃圾回收器,Parallel 收集器更关注系统的吞吐量。所谓吞吐量就是 CPU 用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间/(运行用户代码时间 + 垃圾收集时间)。

停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能够提升用户的体验;

而高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;新生代复制算法、老年代标记-整理

参数控制:

  • -XX:MaxGCPauseMillis 设置最大垃圾收集停顿时间
  • -XX:GCTimeRatio 设置吞吐量的大小(默认是99)
  • -XX:+UseAdaptiveSeizPolicy 打开自适应模式,当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量

Parallel Old

Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,采用多线程和标记-整理算法,也是比较关注吞吐量。在注重吞吐量及 CPU 资源敏感的场合,都可以优先考虑 Parallel 加 Parallel Old 收集器。

参数控制:-XX:+UseParallelOldGC 使用 Parallel Old 收集器;-XX:ParallelGCThreads 限制线程数量。

CMS 垃圾回收器

CMS(Concurrent Mark Sweep) 并发标记请除,它使用的是标记-清除法,工作在老年代,主要关注系统的停顿时间。

CMS 并不是独占的回收器,也就是说,CMS 回收的过程中应用程序仍然在不停的工作,又会有新的垃圾不断的产生,所以在使用CMS的过程中应该确保应用程序的内存足够可用,CMS不会等到应用程序饱和的时候才去回收垃圾,而是在某一阀值(默认为68)的时候开始回收,也就是说当老年代的空间使用率达到68%的时候会执行CMS。如果内存使用率增长很快,在CMS执行过程中,已经出现了内存不足的情况,此时,CMS回收就会失败,虚拟机将启动老年代 Serial 进行垃圾回收,这会导致应用程序中断,直到垃圾回收完成后才会正常工作,这个过程GC的停顿时间可能较长,所以阀值的设置要根据实际情况设置。

  • 初始标记:暂停所有的其他线程,并记录下直接与root相连的对象,速度很快;
  • 并发标记:同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
  • 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
  • 并发清除:开启用户线程,同时GC线程开始对未标记的区域做清扫。

主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:

  • 对CPU资源敏感;
  • 无法处理浮动垃圾;
  • 使用的 标记-清除 算法会导致收集结束时会有大量空间碎片产生。

标记清除法产生的内存碎片问题,CMS 提供提供了一些优化设置,可以设置完成 CMS 之后进行一次碎片整理,也可以设置进行多少次 CMS 回收后进行碎片整理。

参数控制:

  • -XX:+UserConcMarkSweepGC 使用 CMS 垃圾清理器
  • -XX:CMSInitatingPermOccupancyFraction 设置阀值
  • -XX:ConcGCThreads 限制线程数量
  • -XX:+UseCMSCompactAtFullCollection 设置完成 CMS 之后进行一次碎片整理
  • -XX:CMSFullGCsBeforeCompaction 设置进行多少次 CMS 回收后进行碎片整理

G1(Garbage First)

G1(Garbage First) 垃圾收集器是当今垃圾回收技术最前沿的成果之一。早在 JDK7 就已加入 JVM 的收集器大家庭中,成为 HotSpot 重点发展的垃圾回收技术。

G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region。包括:Eden、Survivor、Old 和 Humongous。

其中,Humongous 是特殊的 Old 类型,回收空闲巨型分区,专门放置大型对象。这样的划分方式意味着不需要一个连续的内存空间管理对象。G1 将空间分为多个区域,优先回收垃圾最多的区域。一个对象和它内部所引用的对象可能不在同一个 Region 中,那么当垃圾回收时,是否需要扫描整个堆内存才能完整地进行一次可达性分析?

当然不是,每个 Region 都有一个 Remembered Set(已记忆集合),用于记录本区域中所有对象引用的对象所在的区域,从而在进行可达性分析时,只要在 GC Roots 中再加上 Remembered Set 即可防止对所有堆内存的遍历。

同 CMS 垃圾回收器一样,G1 也是关注最小时延的垃圾回收器,也同样适合大尺寸堆内存的垃圾收集,官方也推荐使用 G1 来代替选择 CMS。G1 最大的特点是引入分区的思路,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至 CMS 的众多缺陷。

G1收集器的运作大致分为以下几个步骤:

  • 初始标记:初始标记阶段仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS 的值,让下一个阶段用户程序并发运行时,能在正确可用的 Region 中创建新对象,这一阶段需要停顿线程,但是耗时很短。
  • 并发标记:并发标记阶段是从 GC Root 开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
  • 最终标记:而最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程 Remenbered Set Logs 里面,最终标记阶段需要把Remembered Set Logs 的数据合并到 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这一阶段需要停顿线程,但是可并行执行。
  • 筛选回收:最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。

G1 能充分利用多 CPU、多核环境下的硬件优势,使用多 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿的时间,部分其他收集器原本需要停顿 Java 线程执行的GC动作,G1 收集器仍然可以通过并发的方式让 Java 程序继续执行。

此外,与其他收集器一样,分代概念在G1中依然得以保留。虽然 G1 可以不需其他收集器配合就能独立管理整个 GC 堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次 GC 的旧对象以获取更好的收集效果。

空间整合:与 CMS 的 标记-清理 算法不同,G1 从整体看来是基于 标记-整理 算法实现的收集器,从局部(两个 Region 之间)上看是基于 复制 算法实现,无论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。

可预测的停顿:这是 G1 相对于 CMS 的另外一大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒,这几乎已经是实时 Java(RTSJ)的垃圾收集器特征了。

参数控制:-XX:+UseG1GC。

小结

本文介绍了常见的7种不同分代的收集器:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1;而它们所处区域,则表明其是属于新生代收集器还是老年代收集器:

  • 新生代收集器:Serial、ParNew、Parallel Scavenge;
  • 老年代收集器:Serial Old、Parallel Old、CMS;
  • 整堆收集器:G1;

根据收集的区域(年轻代或年老代)和收集器自身的特性,可以有如下组合:
Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel/Serial Old、Parallel/Parallel Old、G1。

ZGC 来了。ZGC 是 JDK11 中要发布的最新垃圾收集器。完全没有分代的概念,官方给出 ZGC 的优点是无碎片,时间可控,超大堆。读者可以尝试了解和使用一下 ZGC 。

推荐阅读

最全的 JVM 面试知识点系列文章

订阅最新文章,欢迎关注我的公众号

微信公众号

参考

  1. 从串行到并行,从并行到分布式
  2. 详解 JVM Garbage First(G1) 垃圾收集器

本文转载自: 掘金

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

伴随 P5js 入坑创意编程

发表于 2019-06-28

上一篇文章:填坑!完结娱乐圈明星关系图谱 发布后,古柳印象里过往留下的坑貌似只剩下 图像检索(一):因缘际会与前瞻 的后续实践代码(原文里给了参考代码链接)和在豆瓣Top250电影海报上的尝试效果了。

一想到所有坑都被填了(如果还有啥是我不记得的,请千万不要提醒我),就觉得真是业界良心,倍感轻松。

于是古柳某日点开 图像检索(一):因缘际会与前瞻一文,回顾“佳作”之余,也找了下里面清华美院向帆老师的作品集网站 zeelab Projects。

PS:如果你没看过这个演讲,推荐一看,古柳至今难忘:【一席】向帆:如果把每年的春晚都像蚊香一样卷起来的话,它就是这样的

而在这些作品中,古柳更中意且也想实现下类似网页展示效果的是:AwardPuzzel - zeelab 。下面援引下“官方”介绍,建议去网页体验一下:

AwardPuzzel 是一个全国美展油画类获奖画作的数据视觉化作品,收录了美展第六届至第十二届的2276幅获奖作品,通过动态交互的方式呈现了中国油画30年间的艺术历程、形态、色彩、尺寸和地区之间的变化和关系以及中国油画大师们的艺术思路。
本作品可以被当作研究工具为研究者和评论家使用,亦可作艺术作品欣赏。
我们希望通过这个平台分享我们的视角,也希望使用者通过自己的浏览和观察得到自己的结论。
全国美展是中国美术界最重要事件,每五年举办一次,第六届是1984年举办,第十二届为2014年举办。

虽然古柳不怎么会前端,但自从接触爬虫以来,右键“审查元素”,查看网页源代码的习惯还是有的。

于是不看不知道,一看又引出了后续的诸多故事,借用书上的一句话:“那日也是合该有事”,且听古柳慢慢道来……

点开网页源代码后,找到数据展示和交互的区域对应的代码自然是不难的。这里为了展示方便,特地丢到 Carbon 里,重点突出下这段代码。

可以看到 HTML 里主要用了 canvas 标签,这也没什么,古柳反正不懂 canvas,睁眼瞎罢了,也看不出什么名堂。但是却发现标签里的 data-processing-soucres 属性对应的 .pde 文件,特别与众不同,“闻所未闻,见所未见”,并且想起当初也曾各种搜罗,希冀能复现向帆老师的春晚或美展油画项目,虽不了了之,但对 processing 这一能实现各种艺术创意的编程语言有了印象。

于是谷歌了下 “HTML+Canvas+Processing” 等关键词,意外地发现:基于 Java 的 Processing 语言的家谱中,还有对应 JavaScript 和 Python 版本,前者即:P5.js,而这不禁使古柳看到了能在网页中复现上述效果的希望。

说起来,之前古柳压根一丁点都没听说过 P5.js,搜了下对应的中文资料也不算多,更偏爱看视频学习的我,看到万能的B站上有人搬运了油管上Daniel Shiffman 的教学视频(1-12节),于是立马刷了下,p5.js 基础教程 1-7,并全部跟着敲了遍代码,虽然无字幕,但还蛮好啃的,有很多针对初学编程的知识讲解。(原始链接:Code! Programming with p5.js - YouTube)

习得新技能后,立马用明星关系图谱的图片简单粗暴的拼了下照片墙,虽然离美展油画的效果差了十万八千里,但也算是卖出了第一步。


其实以前就没少拼照片墙,想来大家也都见过爬取微信好友然后拼图的文章,但古柳还是安利下这篇旧文,里面的图片绝对值得一看(如果你看完觉得也不咋地,那……也就随它去吧):Python PIL库实现的照片墙成果图分享。

再就是几天前,看到 @爱可可-爱生活 老师的这则微博:Processing 创作的生成艺术 via:おかず,配图漂亮就不说了,重点是带着 Processing 关键词,于是就埋下了想用 P5.js 实现一波的念头。

幸运地找到了作品的出处:Generative Art #146 via:おかず,欣喜地发现附有 Processing 实现代码,而且该系列有更多不错的作品,遂萌发了想将其所有作品用 P5.js 实现一波并开源的想法。

当然因为目前 P5.js 不够熟练,JavaScript / ES6 之类也只是入门,难免有所担心和顾虑。但在复现这个作品时发现 Processing 和 P5.js 真的很像,很多函数接口官方设计成统一的,极大降低了门槛。

上图就是古柳用 P5.js 复现的效果,虽然还有些小问题,代码也不一定最规范,但先行分享,后续再优化哈!可在此网址体验作品生成效果:editor.p5js.org/DesertsX/sk…

欢迎关注,“牛衣古柳”哈!

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
复制代码let particles;
const n = 120;

function setup() {
createCanvas(900, 900);
// pixelDensity(2);
colorMode(HSB, 360, 100, 100, 100);
rectMode(CENTER);
newParticles();
}

function draw() {
for (let i in particles) {
let p = particles[i];
p.run();
if (p.isDead()) {
particles.splice(i, 1);
}
}
}

function forms() {
for (let j = 0; j < n; j++) {
let x = random(width), y = random(height);
let s = random(20, 100);
let hs = s / 2;
let c = getCol();
noStroke();
fill(c);
if (random(1) > 0.5) {
for (let i = -s / 2; i < s / 2; i++) {
particles.push(new Particle(x + i, y - hs, c));
particles.push(new Particle(x + i, y + hs, c));
particles.push(new Particle(x - hs, y + i, c));
particles.push(new Particle(x + hs, y + i, c));
}
square(x, y, s);
} else {
for (let a = 0; a < TAU; a += TAU / 360) {
particles.push(new Particle(x + hs * cos(a), y + hs * sin(a), c));
}
circle(x, y, s);
}
}
}
function newParticles() {
// particles = new ArrayList<Particle>();
particles = new Array();
background("#FCFCF0");
forms();
noiseSeed(parseInt(random(100000)));
}
// function mousePressed() {
// newParticles();
// }
function keyPressed() {
// 还没生效
if (keyCode === 's') {
saveFrame("123.png");
}
}
function getCol() {
let colors = ["#e4572e", "#29335c", "#f3a712", "#a8c686", "#669bbc", "#efc2f0"];
//let colors = ["#880D1E", "#DD2D4A", "#F26A8D", "#F49CBB", "#CBEEF3"];
let idx = parseInt(random(colors.length));
// console.log(idx + colors[idx]);
return colors[idx];
}
class Particle {
constructor(x, y, col) {
this.pos = createVector(x, y);
this.step = 1;
this.angle = random(10);
this.lifeSpan = 100;
this.noiseScale = 800;
this.noiseStrength = 90;
this.col = col;
}
show() {
noStroke();
// fill(this.col, this.lifeSpan);
fill(this.col);
circle(this.pos.x, this.pos.y, 0.5);
}
move() {
this.angle = noise(this.pos.x / this.noiseScale, this.pos.y / this.noiseScale) * this.noiseStrength;
this.pos.x += cos(this.angle) * this.step;
this.pos.y += sin(this.angle) * this.step;
this.lifeSpan -= 0.1;
}

isDead() {
return (this.lifeSpan < 0.0)
}
run() {
this.show();
this.move();
}
}

本文转载自: 掘金

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

Jetpack LiveData入门指南

发表于 2019-06-28

又到周末好时光,茫茫人海中,与你在掘金相遇,好幸运~请君赏阅本文,相处不易,开门见山,不扯皮。本文讲的是Jetpack系列第三个架构组件LiveData,LiveData是Lifecycle-aware 组件的一个应用,这意味着LiveData遵守Activity、Fragment和Service等组件的生命周期,在它们生命周期处于活跃状态(CREATED和RESUMED)才进行更新Views。

使用LiveData步骤

  1. 创建持有某种类型的LiveData对象。通常在ViewModel类来实现该对象。
  2. 定义一个具有onChanged()方法的Observer对象,当LiveData持有数据变化是回调该方法。通常在UI控制器类中实现创建该Observer对象,如Activity或Fragment。
  3. 通过使用observe()方法,将上述的LiveData对象和Observer对象关联在一起。这样Observer对象就与LiveData产生了订阅关系,当LiveData数据发生变化时通知,而在Observer更新数据,所以Observer通常是Activity和Fragment。

三个步骤就定义了使用LiveData的方式,从步骤可以看出,使用了观察者模式,当LiveData对象持有数据发生变化,会通知对它订阅的所有处于活跃状态的订阅者。而这些订阅者通常是UI控制器,如Activity或Fragment,以能在被通知时,自动去更新Views。

创建LiveData对象

LiveData可以包装任何数据,包括集合对象。LiveData通常存储在ViewModel中,并通过getter方法获得。示例:

1
2
3
4
5
6
7
8
9
复制代码class NameViewModel : ViewModel() {

// Create a LiveData with a String
val currentName: MutableLiveData<String> by lazy {
MutableLiveData<String>()
}

// Rest of the ViewModel...
}

为什么是ViewModel持有LiveData而不是Activity或者Fragment中呢?

  • 这样导致Activity或Fragment代码臃肿,Activity或Fragment一般用来展示数据而不是持有数据。
  • 将LiveData解耦,不和特定的Activity或Fragment绑定在一起。

创建 观察LiveData 的对象

有了数据源之后,总需要有观察者来观察数据源,不然数据源就失去了存在的意义。

那么在哪里观察数据源呢?

在大多数情况下,在应用组件的onCreate()方法中访问LiveData是个合适的时机。这样可以确保系统不在Activity或Fragment的onResume()方法进行多余的调用;另外这样也确保Activity和Fragment尽早有数据可以进行显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码class NameActivity : AppCompatActivity() {

private lateinit var model: NameViewModel

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// Other code to setup the activity...

// Get the ViewModel.
model = ViewModelProviders.of(this).get(NameViewModel::class.java)


// Create the observer which updates the UI.
val nameObserver = Observer<String> { newName ->
// Update the UI, in this case, a TextView.
nameTextView.text = newName
}

// Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
model.currentName.observe(this, nameObserver)
}
}

在讲nameObserver对象传给observe()方法后,存储在LiveData最近的值以参数的形式立即传递到onChange()方法中。当然,如果此时LiveData没有存储值的话,onChange()方法不会被调用。

更新 LiveData 对象

LiveData本身没有提供公共方法更新值。如果需要修改LiveData的数据的话,可以通过MutableLiveData来暴露共有方法setValue()和postValue()。通常在在ViewModel中使用MutableLiveData,而MutableLiveData暴露不可变的LiveData给Observer。与Observer建立关系后,通过修改LiveData的值从而更新Observer中的视图。

1
2
3
4
复制代码button.setOnClickListener {
val anotherName = "GitCode"
model.currentName.setValue(anotherName)
}

当单击button时,字符串GitCode会存储到LiveData中,nameTextView的文本也会更新为GitCode。这里通过button的点击来给LiveData设置值,也可以网络或者本地数据库获取数据方式来设置值。

扩展 LiveData

可以通过下面的栗子来看看如何扩展LiveData。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码class StockLiveData(symbol: String) : LiveData<BigDecimal>() {
private val stockManager = StockManager(symbol)

private val listener = { price: BigDecimal ->
value = price
}

override fun onActive() {
stockManager.requestPriceUpdates(listener)
}

override fun onInactive() {
stockManager.removeUpdates(listener)
}
}

首先建立一个StockLiveData并继承自LiveData,并重写两个重要方法。

  • onActivite() 当有活跃状态的订阅者订阅LiveData时会回调该方法。意味着需要在这里监听数据的变化。
  • onInactive() 当没有活跃状态的订阅者订阅LiveData时会回调该方法。此时没有必要保持StockManage服务象的连接。
  • setValue() 注意到value=price这里是调用了setValue(price)方法,通过该方法更新LiveData的值,进而通知处于活跃状态的订阅者。

LiveData会认为订阅者的生命周期处于STARTED或RESUMED状态时,该订阅者是活跃的。

那么如何使用StockLiveData呢?

1
2
3
4
5
6
7
复制代码override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val myPriceListener: LiveData<BigDecimal> = ...
myPriceListener.observe(this, Observer<BigDecimal> { price: BigDecimal? ->
// Update the UI.
})
}

以Fragment作LifecycleOwner的实例传递到observer()方法中,这样就将Observer绑定到拥有生命周期的拥有者。由于LiveData可以在多个Activity、Fragment和Service中使用,所以可以创建单例模式。

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
复制代码class StockLiveData(symbol: String) : LiveData<BigDecimal>() {
private val stockManager: StockManager = StockManager(symbol)

private val listener = { price: BigDecimal ->
value = price
}

override fun onActive() {
stockManager.requestPriceUpdates(listener)
}

override fun onInactive() {
stockManager.removeUpdates(listener)
}

companion object {
private lateinit var sInstance: StockLiveData

@MainThread
fun get(symbol: String): StockLiveData {
sInstance = if (::sInstance.isInitialized) sInstance else StockLiveData(symbol)
return sInstance
}
}
}

那么在Fragment可以这样使用:

1
2
3
4
5
6
7
8
复制代码class MyFragment : Fragment() {

override fun onActivityCreated(savedInstanceState: Bundle?) {
StockLiveData.get(symbol).observe(this, Observer<BigDecimal> { price: BigDecimal? ->
// Update the UI.
})

}

转换 LiveData

有时候在把数据分发给Observer前,转换存储在LiveData中的值,或者返回一个 基于已有值的LiveData对象 的另外一个LiveData对象。这时候就需要用到 Transformations类来处理了。

使用Transformations.map()方法可以改变其下游的结果:

1
2
3
4
复制代码LiveData<User> userLiveData = ...;
LiveData<String> userName = Transformations.map(userLiveData, user -> {
user.name + " " + user.lastName
});

使用Transformations.switchMap()同样可以改变下游的结果,但传递给switchMap()的函数必须返回一个LiveData对象。

1
2
3
4
5
复制代码private fun getUser(id: String): LiveData<User> {
...
}
val userId: LiveData<String> = ...
val user = Transformations.switchMap(userId) { id -> getUser(id) }

这种转换方式都惰性的,也就是只有Observer来订阅数据的时候,才会进行转换。当在ViewModel需要一个Lifecycle对象,或许这种转换会是很好的解决方案。

合并多个LiveData 源

MediatorLiveData是LiveData的子类,它主要用途是用来合并多个LiveData源。当其中一个源数据发生变化是,都会回调订阅MediatorLiveData的观察者的onChanged()方法。例如我们在实际开发中,我们的数据源要么来自服务器,要么来自本地数据库。这里就考虑使用MediatorLiveData。

更多信息请参阅官网

总结

LiveData的入门使用来说还是相对简单的,等到后面讲完Jetpack系列文章,再以一个综合的Demo示例Jetpack涉及到的一些知识点。光看文档,都可以感觉到Android 对设计模式,和MVP模式、MVVM模式设计理念使用得淋漓尽致。所以建议各位同学在代码方面的编写一定要有大局观念,代码规范的还是要有,方便别人就是方便自己。不要为应付功能实现而代码臃肿,后续又不重新架构,一直积累成垃圾码。

Welcom to visit my github

本文是Jetpack系列文章第三篇

第一篇:
Jetpack:你还在findViewById么?

第二篇:Jetpack:你如何管理Activity和Fragment的生命周期?

本文转载自: 掘金

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

Java容器(一)—CurrentHashMap详解(JDK

发表于 2019-06-28

摘要

在涉及到Java多线程开发时,如果我们使用HashMap可能会导致死锁问题,使用HashTable效率又不高。而ConcurrentHashMap既可以保持同步也可以提高并发效率,所以这个时候ConcurrentHashmap是我们最好的选择。

为什么使用ConcurrentHashMap

  • 在多线程环境中使用HashMap的put方法有可能导致程序死循环,因为多线程可能会导致HashMap形成环形链表,即链表的一个节点的next节点永不为null,就会产生死循环。这时,CPU的利用率接近100%,所以并发情况下不能使用HashMap。
  • HashTable通过使用synchronized保证线程安全,但在线程竞争激烈的情况下效率低下。因为当一个线程访问HashTable的同步方法时,其他线程只能阻塞等待占用线程操作完毕。
  • ConcurrentHashMap使用分段锁的思想,对于不同的数据段使用不同的锁,可以支持多个线程同时访问不同的数据段,这样线程之间就不存在锁竞争,从而提高了并发效率。

简介

在阅读ConcurrentHashMap的源码时,有一段相关描述。

The primary design goal of this hash table is to maintain concurrent readability(typically method get(), but also iterators and related methods) while minimizing update contention. Secondary goals are to keep space consumption about the same or better than java.util.HashMap, and to support high initial insertion rates on an empty table by many threads.

大致意思就是:
ConcurrentHashMap的主要设计目的是保持并发的可读性(通常是指的get()方法的使用,同时也包括迭代器和相关方法),同时最小化更新征用(即在进行插入操作或者扩容时也可以保持其他数据段的访问)。第二个目标就是在空间利用方面保持与HashMap一致或者更好,并且支持多线程在空表的初始插入速率。

Java7与Java8中的ConcurrentHashMap:

在ConcurrentHashMap中主要通过锁分段技术实现上述目标。

在Java7中,ConcurrentHashMap由Segment数组结构和HashEntry数组组成。Segment是一种可重入锁,是一种数组和链表的结构,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构。正是通过Segment分段锁,ConcurrentHashMap实现了高效率的并发。

在Java8中,ConcurrentHashMap去除了Segment分段锁的数据结构,主要是基于CAS操作保证保证数据的获取以及使用synchronized关键字对相应数据段加锁实现了主要功能,这进一步提高了并发性。同时同时为了提高哈希碰撞下的寻址性能,Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(long(N)))。

Java8中ConcurrentHashMap的结构

在Java8中,ConcurrentHashMap弃用了Segment类,但是保留了Segment属性,用于序列化。目前ConcurrentHashMap采用Node类作为基本的存储单元,每个键值对(key-value)都存储在一个Node中。同时Node也有一些子类,TreeNodes用于树结构中(当链表长度大于8时转化为红黑树);TreeBins用于维护TreeNodes。当链表转树时,用于封装TreeNode。也就是说,ConcurrentHashMap的红黑树存放的是TreeBin,而不是treeNode;ForwordingNodes是一个重要的结构,它用于ConcurrentHashMap扩容时,是一个标志节点,内部有一个指向nextTable的属性,同时也提供了查找的方法;

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
复制代码static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val; //使用了volatile属性
volatile Node<K,V> next; //使用了volatile属性

Node(int hash, K key, V val) {
this.hash = hash;
this.key = key;
this.val = val;
}
Node(int hash, K key, V val, Node<K,V> next) {
this(hash, key, val);
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return val; }
public final int hashCode() { return key.hashCode() ^ val.hashCode(); }
public final String toString() {...}
public final V setValue(V value) {...}

public final boolean equals(Object o) {...}

/**
* Virtualized support for map.get(); overridden in subclasses.
*/
Node<K,V> find(int h, Object k) {...}
}

处理Node之外,Node的一个子类ForwardingNodes也是一个重要的结构,它主要作为一个标记,在处理并发时起着关键作用,有了ForwardingNodes,也是ConcurrentHashMap有了分段的特性,提高了并发效率。

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
复制代码 /**
* A node inserted at head of bins during transfer operations.
*/
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
//hash值默认为MOVED(-1)
super(MOVED, null, null);
this.nextTable = tab;
}

Node<K,V> find(int h, Object k) {
// loop to avoid arbitrarily deep recursion on forwarding nodes
//在nextTable中查找,nextTable可以看做是当前hash表的一个副本
outer: for (Node<K,V>[] tab = nextTable;;) {
Node<K,V> e; int n;
if (k == null || tab == null || (n = tab.length) == 0 ||
(e = tabAt(tab, (n - 1) & h)) == null)
return null;
for (;;) {
int eh; K ek;
if ((eh = e.hash) == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
if (eh < 0) {
if (e instanceof ForwardingNode) {
tab = ((ForwardingNode<K,V>)e).nextTable;
continue outer;
}
else
return e.find(h, k);
}
if ((e = e.next) == null)
return null;
}
}
}
}

ConcurrentHashMap中的原子操作

在ConcurrentHashMap中通过原子操作查找元素、替换元素和设置元素。这些原子操作起着非常关键的作用,你可以在所有ConcurrentHashMap的基本功能中看到它们的身影。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码 static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectAcquire(tab, ((long)i << ASHIFT) + ABASE);
}

static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSetObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectRelease(tab, ((long)i << ASHIFT) + ABASE, v);
}

ConcurrentHashMap的功能实现

1.ConcurrentHashMap初始化
在介绍初始化之前先介绍一个重要的参数sizeCtl,含义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码 /**
* Table initialization and resizing control. When negative,the
* table is being initialized or resized: -1 for initialization,
* else -(1 + the number of active resizing threads). Otherwise,
* when table is null, holds the initial table size to use upon
* creation, or 0 for default. After initialization, holds the
* next element count value upon which to resize the table.
* Hash表的初始化和调整大小的控制标志。为负数,Hash表正在初始化或者扩容;
* (-1表示正在初始化,-N表示有N-1个线程在进行扩容)
* 否则,当表为null时,保存创建时使用的初始化大小或者默认0;
* 初始化以后保存下一个调整大小的尺寸。
*/
private transient volatile int sizeCtl;

这个参数起到一个控制标志的作用,在ConcurrentHashMap初始化和扩容都有用到。
ConcurrentHashMap构造函数只是设置了一些参数,并没有对Hash表进行初始化。当在从插入元素时,才会初始化Hash表。在开始初始化的时候,首先判断sizeCtl的值,如果sizeCtl < 0,说明有线程在初始化,当前线程便放弃初始化操作。否则,将SIZECTL设置为-1,Hash表进行初始化。初始化成功以后,将sizeCtl的值设置为当前的容量值。

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
复制代码 /**
* Initializes table, using the size recorded in sizeCtl.
*/
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
//sizeCtl小于0,正在初始化
if ((sc = sizeCtl) < 0)
//调用yield()函数,使线程让出CPU资源
Thread.yield(); // lost initialization race; just spin
//设置SIZECTL为-1,表示正在初始化
else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2); //n-(1/4)n,即默认的容量(n * loadFactor)
}
} finally {
sizeCtl = sc; //重新设置sizeCtl
}
break;
}
}
return tab;
}

2.确定元素在Hash表的索引

通过hash算法可以将元素分散到哈希桶中。在ConcurrentHashMap中通过如下方法确定数组索引:
第一步:

1
2
3
复制代码static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}

第二步:(length-1) & (h ^ (h >>> 16)) & HASH_BITS);

ConcurrentHashMap的put方法

  • 如果key或者value为null,则抛出空指针异常;
  • 如果table为null或者table的长度为0,则初始化table,调用initTable()方法。
  • 计算当前键值的索引位置,如果Hash表中当前节点为null,则将元素直接插入。(注意,这里使用的就是前面锁说的CAS操作)
  • 如果当前位置的节点元素的hash值为-1,说明这是一个ForwaringNodes节点,即正在进行扩容。那么当前线程加入扩容。
  • 当前节点不为null,对当前节点加锁,将元素插入到当前节点。在Java8中,当节点长度大于8时,就将节点转为树的结构。
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
复制代码	/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
//数据不合法,抛出异常
if (key == null || value == null) throw new NullPointerException();
//计算索引的第一步,传入键值的hash值
int hash = spread(key.hashCode());
int binCount = 0; //保存当前节点的长度
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); //初始化Hash表
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//利用CAS操作将元素插入到Hash表中
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // no lock when adding to empty bin(插入null的节点,无需加锁)
}
else if ((fh = f.hash) == MOVED) //f.hash == -1
//正在扩容,当前线程加入扩容
tab = helpTransfer(tab, f);
else if (onlyIfAbsent && fh == hash && // check first node
((fk = f.key) == key || fk != null && key.equals(fk)) &&
(fv = f.val) != null)
return fv;
else {
V oldVal = null;
//当前节点加锁
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
//插入的元素键值的hash值有节点中元素的hash值相同,替换当前元素的值
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
//替换当前元素的值
e.val = value;
break;
}
Node<K,V> pred = e;
//没有相同的值,直接插入到节点中
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value);
break;
}
}
}
//节点为树
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
//替换旧值
p.val = value;
}
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
if (binCount != 0) {
//如果节点长度大于8,转化为树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}

ConcurrentHashMap的扩容机制

当ConcurrentHashMap中元素的数量达到cap * loadFactor时,就需要进行扩容。扩容主要通过transfer()方法进行,当有线程进行put操作时,如果正在进行扩容,可以通过helpTransfer()方法加入扩容。也就是说,ConcurrentHashMap支持多线程扩容,多个线程处理不同的节点。

  • 开始扩容,首先计算步长,也就是每个线程分配到的扩容的节点数(默认是16)。这个值是根据当前容量和CPU的数量来计算(stride = (NCPU > 1) ? (n >>> 3) / NCPU : n),最小是16。
  • 接下来初始化临时的Hash表nextTable,如果nextTable为null,初始化nextTable长度为原来的2倍;
  • 通过计算出的步长开始遍历Hash表,其中坐标是通过一个原子操作(compareAndSetInt)记录。通过一个while循环,如果在一个线程的步长内便跳过此节点。否则转下一步;
  • 如果当前节点为空,之间将此节点在旧的Hash表中设置为一个ForwardingNodes节点,表示这个节点已经被处理过了。
  • 如果当前节点元素的hash值为MOVED(f.hash == -1),表示这是一个ForwardingNodes节点,则直接跳过。否则,开始重新处理节点;
  • 对当前节点进行加锁,在这一步的扩容操作中,重新计算元素位置的操作与HashMap中是一样的,即当前元素键值的hash与长度进行&操作,如果结果为0则保持位置不变,为1位置就是i+n。其中进行处理的元素是最后一个符合条件的元素,所以扩容后可能是一种倒序,但在Hash表中这种顺序也没有太大的影响。
  • 最后如果是链表结构直接获得高位与低位的新链表节点,如果是树结构,同样计算高位与低位的节点,但是需要根据节点的长度进行判断是否需要转化为树的结构。
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
复制代码 /**
* Moves and/or copies the nodes in each bin to new table. See
* above for explanation.
*/
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//根据长度和CPU的数量计算步长,最小是16
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
//初始化新的Hash表,长度为原来的2倍
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n;
}
int nextn = nextTab.length;
//初始化ForwardingNodes节点
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true; //是否跨过节点的标记
boolean finishing = false; // to ensure sweep before committing nextTab
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
//根据步长判断是否需要跨过节点
while (advance) {
int nextIndex, nextBound;
//到达没有处理的节点下标
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
//所有节点都已经接收处理
i = -1;
advance = false;
}
else if (U.compareAndSetInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
//更新下表transferIndex,在步长的范围内都忽略
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
//所有节点都被接收处理或者已经处理完毕
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
//处理完毕
if (finishing) {
nextTable = null;
table = nextTab;
//更新sizeCtl
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//判断所有节点是否全部被处理
if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
//如果节点为null,直接标记为已接收处理
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
//键值的hash为-1,表示这是一个ForwardingNodes节点,已经被处理
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
//对当前节点进行加锁
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) {
//索引位置是否改变的标志
int runBit = fh & n;
Node<K,V> lastRun = f; //最后一个元素
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
//重新计算更新直到最后一个元素
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
//runBit = 0,保持位置不变
if (runBit == 0) {
ln = lastRun;
hn = null;
}
//runBit = 1,位置时i+n
else {
hn = lastRun;
ln = null;
}
//重新遍历节点元素
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
//构建低位(位置不变)新的链表
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
//构建高位(i+n)新的链表
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
//将新的链表设置到新的Hash表中相应的位置
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
//将原来的Hash表中相应位置的节点设置为ForwardingNodes节点
setTabAt(tab, i, fwd);
advance = true;
}
//如果节点是树的结构
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
//同样的方式计算新的索引位置
if ((h & n) == 0) {
//构建新的链表结构
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
//构建新的链表结构
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
//判断是否需要转化为树
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
//判断是否需要转化为树
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
//将新的链表设置到新的Hash表中相应的位置
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
//将原来的Hash表中相应位置的节点设置为ForwardingNodes节点
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}

ConcurrentHashMap的get方法

ConcurrentHashMap的get方法就是从Hash表中读取数据,而且与扩容不冲突。该方法没有同步锁。

  • 通过键值的hash计算索引位置,如果满足条件,直接返回对应的值;
  • 如果相应节点的hash值小于0 ,即该节点在进行扩容,直接在调用ForwardingNodes节点的find方法进行查找。
  • 否则,遍历当前节点直到找到对应的元素。
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
复制代码/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code key.equals(k)},
* then this method returns {@code v}; otherwise it returns
* {@code null}. (There can be at most one such mapping.)
*
* @throws NullPointerException if the specified key is null
*/
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
//满足条件直接返回对应的值
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//e.hash<0,正在扩容
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
//遍历当前节点
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}

总结

ConcurrentHashMap就介绍到这里,以上主要是对ConcurrentHashMap中经常使用到的特性进行分析,如果对其他内容感兴趣,可以阅读相应的源码。

本文转载自: 掘金

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

MySQL性能优化(七)-- 慢查询

发表于 2019-06-28

1.慢查询的用途

它能记录下所有执行超过long_query_time时间的SQL语句,帮我们找到执行慢的SQL,方便我们对这些SQL进行优化。

2.查看是否开启慢查询

show variables like ‘slow_query%’;

img

slow_query_log = off,表示没有开启慢查询

slow_query_log_file 表示慢查询日志存放的目录

3.开启慢查询(需要的时候才开启,因为很耗性能,建议使用即时性的)

方式一:(即时性的,重启mysql之后失效,常用的)

set global slow_query_log=1; 或者 set global slow_query_log=ON;

开启之后 我们会发现 /var/lib/mysql下已经存在 localhost-slow.log了,未开启的时候默认是不存在的。

方式二:(永久性的)

在/etc/my.cfg文件中的[mysqld]中加入:

1
2
复制代码slow_query_log=ON
slow_query_log_file=/var/lib/mysql/localhost-slow.log

4.设置慢查询记录的时间

查询慢查询记录的时间:show variables like ‘long_query%’,默认是10秒钟,意思是大于10秒才算慢查询。

img

我们现在设置慢查询记录时间为1秒:set long_query_time=1;

img

5.执行select count(1) from order o where o.user_id in (select u.id where users);

因为我们开启了慢查询,且设置了超过1秒钟的就为慢查询,此sql执行了24秒,所以属于慢查询。

我们在日志中查看:

more /var/lib/mysql/localhost-slow.log,

img

我们可以看到查询的时间,用户,花费的时间,使用的数据库,执行的sql语句等信息。在生产上我们就可以使用这种方式来查看 执行慢的sql。

6.查询慢查询的次数:show status like ‘slow_queries’;

img

在我们重新执行刚刚的查询sql后,查询慢查询的次数会变为8

img

当然,用 more /var/lib/mysql/localhost-slow.log 也是可以看到详细结果的。

在生产中,我们会分析查询频率高的,且是慢查询的sql,并不是每一条查询慢的sql都需要分析。

7.慢查询日志分析工具Mysqldumpslow

由于在生产上会有很多慢查询,所以采用上述的方法查看慢查询sql会很麻烦,还好MySQL提供了慢查询日志分析工具Mysqldumpslow。

其功能是, 统计不同慢sql的出现次数(Count),执行最长时间(Time),累计总耗费时间(Time),等待锁的时间(Lock),发送给客户端的行总数(Rows),扫描的行总数(Rows)

(1)查询Mysqldumpslow的帮助信息,随便进入一个文件夹下,执行:mysqldumpslow –help

查看mysqldumpslow命令安装在哪个目录:whereis mysqldumpslow

img

说明:

  • -s,是order的顺序,主要有c(按query次数排序)、t(按查询时间排序)、l(按lock的时间排序)、r (按返回的记录数排序)和 at、al、ar,前面加了a的代表平均数
  • -t,是top n的意思,即为返回前面多少条的数据
  • -g,后边可以写一个正则匹配模式,大小写不敏感的
  • -r:倒序

(2)案例:取出耗时最长的两条sql

格式:mysqldumpslow -s t -t 2 慢日志文件

mysqldumpslow -s t -t 2 /var/lib/mysql/localhost-slow.log

img

参数分析:

  • 出现次数(Count),
  • 执行最长时间(Time),
  • 累计总耗费时间(Time),
  • 等待锁的时间(Lock),
  • 发送给客户端的行总数(Rows),
  • 扫描的行总数(Rows),
  • 用户以及sql语句本身(抽象了一下格式, 比如 limit 1, 20 用 limit N,N 表示).

(3)案例:取出查询次数最多,且使用了in关键字的1条sql

mysqldumpslow -s c -t 1 -g ‘in’ /var/lib/mysql/localhost-slow.log

img

这种方式更加方便,更加快捷!

8.show profile

用途:用于分析当前会话中语句执行的资源消耗情况

(1)查看是否开启profile,mysql默认是不开启的,因为开启很耗性能

show variables like ‘profiling%’;

img

(2)开启profile(会话级别的,关闭当前会话就会恢复原来的关闭状态)

set profiling=1; 或者 set profiling=ON;

(3)关闭profile

set profiling=0; 或者 set profiling=OFF;

(4)显示当前执行的语句和时间

show profiles;

img

(5)显示当前查询语句执行的时间和系统资源消耗

show profile cpu,block io for query 4;(分析show profiles中query_id等于4的sql所占的CPU资源和IO操作)

或者直接 : show profile for query 4;

img

欢迎关注我的公众号,第一时间接收最新文章~ 搜索公众号: 码咖 或者 扫描下方二维码:
img

本文转载自: 掘金

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

1…866867868…956

开发者博客

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