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

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


  • 首页

  • 归档

  • 搜索

带查询条件的分页列表缓存策略 目标:快速根据查询条件拿到分页

发表于 2021-05-21

目标:快速根据查询条件拿到分页的list数据

使用应用情景如下:

查询条件可分为以下几类:

  • type status 等 可选值为有限集合,使用 “=” 来查询
  • title name等 输入值不确定,使用 “like” 来查询

分页:

  • pageNumber
  • pageSize

数据存储在mysql

首先确定缓存是否是最佳的方案

可用做缓存key生成条件的属性为3种:

  1. 枚举类值 type
  2. 模糊搜索类值 title
  3. 分页参数 pageNumber pageSize

缓存的value:

  1. id
  2. 整个Record对象

如用以上3种可选属性排列组合,无论选取哪几种,都会面临缓存频繁清除和缓存命中率低的问题,如直接拼接全部查询条件+分页参数作为key,则

  • 数据库记录增,删,改,都需要失效缓存,缓存清除的频率非常高;
  • 因为有用户输入字段,一旦涉及到用户输入文本,那么该字段的输入值差异会比较大,导致缓存中的key数量非常多,缓存的命中率会很低

如不使用分页参数,则要缓存条件匹配的所有记录,数量的压力是一方面,另一方面,当数据库记录发生变化时,不管是增删改哪一种,每一个缓存要判断改动数据是否命中,然后更新,更新逻辑十分复杂

所以结论是,此种情况下,要提升接口响应速度,缓存方案不可行

目前成熟解决方案:搜索引擎(Elasticsearch)

Elasticsearch(ES)是一个基于 Lucene 构建的开源分布式搜索分析引擎,可以近实时的索引、检索数据。具备高可靠、易使用、社区活跃等特点,在全文检索、日志分析、监控分析等场景具有广泛应用。

方案:数据库中的数据可同步存入到Elasticsearch中,当更新或者删除数据时,同时更新Elasticsearch中数据,列表查询时可从Es中查询

未命名文件.png

列表缓存是否一无是处

具体问题具体分析

场景:无查询条件,只要求分页的list数据的快速查询
则一种可用设计方案如下:

redsi.png

缓存实现选择redis, 缓存分两块:

  1. 全部数据id的缓存,使用zset, key为数据id, score为用来排序的字段(id,rank,时间戳等)
  2. 对象的缓存,可序列化后使用String类型

分页查询时,首先在zset中查出当前要获取页的所有id,再根据这一批id去获取相应的Record,如有Record不在缓存中,再去数据库中使用where id in 查出这些数据,然后更新缓存

对象的缓存的list getById 都可以使用

需要注意的是zset中必须有全部id,并且score要同步数据库中被选中排序的那个字段的变化

缓存设计的三个要点:

  1. 初始化
  2. 更新
  3. 清除

从这三个点来考虑上述缓存设计:

初始化:

ids Record
可进行缓存预热,在服务启动前,就把全部ids加载到缓存中,或在第一次list查询时加载 同样,可把score比较大的一批预热进缓存,这些数据在最前面,属于热数据,或在第一次获取不到时加载

更新:

ids Record
数据新增or删除or用作score的字段变化时 数据更新时

清除

ids Record
不清除 数据删除时

通过以上分析,可以看出这种设计命中率比较高,缓存更新策略也比较简单明了

本地缓存 vs 集中式缓存

cache对比.png

案例分析:服务有多个节点,使用spring cache做本地缓存,获取数据的方法上使用@Cacheable来缓存数据,有一个单独的方法来清理缓存,该方法使用@CacheEvict注解,不做实际操作,专门用来清缓存,调用的时机在数据保存的时候,看起来好像没有问题,在数据更新的时候清理掉缓存

那么问题在哪儿呢?

该服务有多个节点,保存数据的请求只会打到某一个节点上,也就是说,只有收到保存请求的节点会调用清理缓存的方法,而其他节点并没有清缓存,所以导致了几个节点的缓存不一致

本文转载自: 掘金

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

第一章 发送HTTP请求 第一章 发送HTTP请求 HTTP

发表于 2021-05-21

第一章 发送HTTP请求

本主题介绍如何发送HTTP请求(如POST或GET)和处理响应。

HTTP请求简介

可以创建%Net.HttpRequest的实例来发送各种HTTP请求并接收响应。此对象相当于Web浏览器,可以使用它发出多个请求。它会自动发送正确的cookie,并根据需要设置Referer标头。

要创建HTTP请求,请使用以下常规流程:

  1. 创建%Net.HttpRequest的实例。
  2. 设置此实例的属性以指示要与之通信的Web服务器。基本属性如下:
  • 服务器指定Web服务器的IP地址或计算机名称。默认值为localhost。

注意:不要将http://或https://作为服务器值的一部分。这将导致错误#6059:无法打开到服务器http:/的TCP/IP套接字。

  1. 可以选择设置HTTP请求的其他属性和调用方法,如指定其他HTTP请求属性中所述。
  2. 然后,通过调用%Net.HttpRequest实例的get()方法或其他方法来发送HTTP请求,如“发送HTTP请求”中所述。

可以从实例发出多个请求,它将自动处理cookie和Referer标头。

注意:如果创建此HTTP请求是为了与生产出站适配器(EnsLib.HTTP.Outbound Adapter)一起使用,那么请改用该适配器的方法来发送请求。

  1. 如果需要,使用%Net.HttpRequest的同一实例发送其他HTTP请求。默认情况下,InterSystems IRIS使TCP/IP套接字保持打开状态,以便可以重复使用套接字,而无需关闭和重新打开它。

以下是一个简单的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码/// w ##class(PHA.TEST.HTTP).Get()
ClassMethod Get()
{
set request=##class(%Net.HttpRequest).%New()
set request.Server="tools.ietf.org"
set request.Https=1
set request.SSLConfiguration="yx"
set status=request.Get("/html/rfc7158")
d $System.Status.DisplayError(status)

s response = request.HttpResponse
s stream = response.Data
q stream.Read()
}

提供身份验证

如果目标服务器需要登录凭据,则HTTP请求可以包括提供凭据的HTTP Authorization标头。

如果使用的是代理服务器,还可以指定代理服务器的登录凭据;为此,请设置ProxyAuthorization属性

使用HTTP 1.0时对请求进行身份验证

对于HTTP 1.0,要验证HTTP请求,请设置%Net.HttpRequest实例的用户名和密码属性。然后,该实例使用基本访问身份验证基于该用户名和密码创建HTTP Authorization标头(RFC 2617)。此%Net.HttpRequest发送的任何后续请求都将包括此头。

重要提示:请确保还使用SSL。在基本身份验证中,凭据以base-64编码形式发送,因此易于读取。

在使用HTTP 1.1时对请求进行身份验证

对于HTTP 1.1,要验证HTTP请求,在大多数情况下,只需设置%Net.HttpRequest实例的用户名和密码属性。当%Net.HttpRequest的实例收到401 HTTP状态代码和WWW-Authenticate标头时,它会尝试使用包含支持的身份验证方案的Authorization标头进行响应。使用为IRIS支持和配置的第一个方案。默认情况下,它按以下顺序考虑这些身份验证方案:

  1. 协商(SPNEGO和Kerberos,根据RFC 4559和RFC 4178)
  2. NTLM(NT LAN Manager身份验证协议)
  3. 基本认证(RFC 2617中描述的基本接入认证)

重要:如果有可能使用基本身份验证,请确保也使用SSL(参见“使用SSL进行连接”)。
在基本身份验证中,凭据以base-64编码的形式发送,因此很容易读取。

在Windows上,如果没有指定Username属性,IRIS可以使用当前登录上下文。
具体来说,如果服务器使用401状态码和用于SPNEGO、Kerberos或NTLM的WWW-Authenticate头响应,那么IRIS将使用当前操作系统用户名和密码创建Authorization头。

具体情况与HTTP 1.0不同,如下所示:

  1. 如果认证成功,IRIS更新%Net的CurrentAuthenticationScheme属性。
    HttpRequest实例来指示它在最近的身份验证中使用的身份验证方案。
  2. 如果尝试获取方案的身份验证句柄或令牌失败,IRIS会将基础错误保存到%Net.HttpRequest实例的AuthenticationErrors属性中。此属性的值为$List,其中每一项都具有格式scheme ERROR: message

仅HTTP 1.1支持协商和NTLM,因为这些方案需要多次往返,而HTTP 1.0要求在每个请求/响应对之后关闭连接。

Variations

如果知道服务器允许的一个或多个身份验证方案,则可以通过包括Authorization标头来绕过服务器的初始往返行程,该标头包含所选方案的服务器的初始令牌。为此,请设置%Net.HttpRequest实例的InitiateAuthentication属性。对于此属性的值,请指定服务器允许的单个授权方案的名称。使用下列值之一(区分大小写):

  • Negotiate
  • NTLM
  • Basic

如果要自定义要使用的身份验证方案(或更改其考虑顺序),请设置%Net.HttpRequest实例的AuthenticationSchemes。对于此属性的值,请指定以逗号分隔的身份验证方案名称列表(使用上一个列表中给出的准确值)。

直接指定授权标头

对于HTTP 1.0或HTTP 1.1(如果适用于场景),可以直接指定HTTP Authorization标头。具体地说,可以将Authorization属性设置为等于正在请求的资源的用户代理所需的身份验证信息。

如果指定Authorization属性,则忽略用户名和密码属性。

启用HTTP身份验证的日志记录

要启用HTTP身份验证的日志记录,请在终端中输入以下内容:

1
2
3
4
java复制代码 set $namespace="%SYS"
kill ^ISCLOG
set ^%ISCLOG=2
set ^%ISCLOG("Category","HttpRequest")=5

日志条目将写入^ISCLOG global中.。要将日志写入文件(以提高可读性),请输入以下内容(仍在%SYS命名空间内):

1
java复制代码 do ##class(%OAuth2.Utils).DisplayLog("filename")

其中,filename是要创建的文件的名称。该目录必须已存在。如果该文件已经存在,它将被覆盖。

要停止日志记录,请输入以下内容(仍在%SYS命名空间内):

1
2
java复制代码 set ^%ISCLOG=0
set ^%ISCLOG("Category","HttpRequest")=0

指定其他HTTP请求属性

在发送HTTP请求之前(请参阅发送HTTP请求),可以指定其属性,如以下各节所述:

可以为%Net.HttpRequest的所有属性指定默认值,如最后列出的部分中所指定。

Location属性

Location属性指定从Web服务器请求的资源。如果设置此属性,则在调用Get(), Head(), Post(), 或 Put()方法时,可以省略location参数。

例如,假设正在向url http://machine_name/test/index.html发送一个HTTP请求

在这种情况下,将使用下列值:

%Net.HttpRequest的示例属性

Properties Value
Server machine_name
Location test/index.html

指定Internet媒体类型(Media Type)和字符编码(Character Encoding)

可以使用以下属性指定%Net.HttpRequest实例及其响应中的Internet媒体类型(也称为MIME类型)和字符编码:

  • Content-Type指定Content-Type标头,该标头指定请求正文的Internet媒体类型。默认类型为None。

可能的值包括application/json、application/pdf、application/postscript、image/jpeg、image/png、multipart/form-data、text/html、text/plan、text/xml等等

  • ContentCharset属性控制请求的任何内容(例如,text/html或text/xml)类型时所需的字符集。如果不指定此属性,InterSystems IRIS将使用InterSystems IRIS服务器的默认编码。

注意:如果设置此属性,则必须首先设置ContentType属性。

  • NoDefaultContentCharset属性控制在未设置ContentCharset属性的情况下是否包括文本类型内容的显式字符集。默认情况下,此属性为False。

如果此属性为true,则如果有文本类型的内容,并且没有设置ContentCharset属性,则内容类型中不包括任何字符集;这意味着字符集iso-8859-1用于消息输出。

  • WriteRawMode属性影响实体正文(如果包含)。它控制请求正文的写入方式。默认情况下,此属性为False,并且InterSystems IRIS以请求标头中指定的编码写入正文。如果此属性为true,则InterSystems IRIS以原始模式写入正文(不执行字符集转换)。
  • ReadRawMode属性控制如何读取响应正文。默认情况下,此属性为False,并且InterSystems IRIS假定正文在响应标头中指定的字符集中。如果此属性为true,InterSystems IRIS将以原始模式读取正文(不执行字符集转换)。

使用代理服务器

可以通过代理服务器发送HTTP请求。要设置此设置,请指定HTTP请求的以下属性:

  • ProxyServer指定要使用的代理服务器的主机名。如果此属性不为空,则将HTTP请求定向到此计算机。
  • ProxyPort指定代理服务器上要连接到的端口。
  • ProxyAuthorization指定Proxy-Authorization标头,如果用户代理必须使用代理验证其自身,则必须设置该标头。对于该值,请使用正在请求的资源的用户代理所需的身份验证信息。
  • ProxyHTTPS控制HTTP请求是针对HTTPS页面还是针对普通HTTP页面。如果未指定代理服务器,则忽略此属性。此属性将目标系统上的默认端口更改为代理端口443。
  • ProxyTunes指定是否通过代理建立到目标HTTP服务器的隧道。如果为true,则请求使用HTTP CONNECT命令建立隧道。代理服务器的地址取自ProxyServer和ProxyPort属性。如果ProxyHttps为true,则隧道建立后,系统间IRIS将协商SSL连接。在这种情况下,由于隧道与目标系统建立直接连接,因此将忽略https属性。

使用SSL进行连接

%Net.HttpRequest类支持SSL连接。要通过SSL发送请求,请执行以下操作:

  1. 将SSLConfiguration属性设置为要使用的已激活SSL/TLS配置的名称。
  2. 还要执行以下操作之一,具体取决于是否使用代理服务器:
  • 如果未使用代理服务器,请将https属性设置为true。
  • 如果使用的是代理服务器,请将ProxyHTTPS属性设置为true。

在这种情况下,要使用到代理服务器本身的SSL连接,请将https属性设置为true。

请注意,当使用到给定服务器的SSL连接时,该服务器上的默认端口假定为443(HTTPS端口)。例如,如果没有使用代理服务器,并且https为true,则会将Default Port属性更改为443。

服务器身份检查

默认情况下,当%Net.HttpRequest实例连接到SSL/TLS安全的Web服务器时,它会检查证书服务器名称是否与用于连接到服务器的DNS名称匹配。如果这些名称不匹配,则不允许连接。此默认行为可防止“中间人”攻击,在RFC 2818的3.1节中进行了描述;另请参阅RFC 2595的2.4节。

若要禁用此检查,请将SSLCheckServerIdentity属性设置为0。

HTTPVersion、Timeout、WriteTimeout和FollowRedirect属性

%Net.HttpRequest还提供以下属性:

HTTPVersion指定请求页面时使用的HTTP版本。默认值是"HTTP/1.1"。你也可以使用“HTTP/1.0”。

Timeout指定等待web服务器响应的时间,以秒为单位。
缺省值是30秒。

WriteTimeout指定等待Web服务器完成写入的时间(以秒为单位)。默认情况下,它将无限期等待。可接受的最小值为2秒。

FollowRedirect指定是否自动跟踪来自Web服务器的重定向请求(由300-399范围内的HTTP状态代码发出信号)。如果使用的是GET或HEAD,则默认值为TRUE;否则为FALSE。

指定HTTP请求的默认值

可以为%Net.HttpRequest的所有属性指定默认值。

  • 要指定适用于所有名称空间的默认值,请设置全局节 ^%SYS("HttpRequest","propname"),其中“PropName”是属性的名称。
  • 要为一个名称空间指定默认值,请转到该名称空间并设置节点^SYS("HttpRequest","propname")

(^%SYS全局设置会影响整个安装,^SYS全局设置会影响当前命名空间。)

例如,要为所有名称空间指定默认代理服务器,请设置全局节^%SYS("HttpRequest","ProxyServer")

本文转载自: 掘金

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

美团二面:Redis与MySQL双写一致性如何保证?

发表于 2021-05-21

前言

四月份的时候,有位朋友去美团面试,他说被问到Redis与MySQL双写一致性如何保证? 这道题其实就是在问缓存和数据库在双写场景下,一致性是如何保证的?本文将跟大家一起来探讨如何回答这个问题。

  • 公众号:捡田螺的小男孩
  • github地址,感谢每一颗star

谈谈一致性

一致性就是数据保持一致,在分布式系统中,可以理解为多个节点中数据的值是一致的。

  • 强一致性:这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大
  • 弱一致性:这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态
  • 最终一致性:最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来,是因为它是弱一致性中非常推崇的一种一致性模型,也是业界在大型分布式系统的数据一致性上比较推崇的模型

三个经典的缓存模式

缓存可以提升性能、缓解数据库压力,但是使用缓存也会导致数据不一致性的问题。一般我们是如何使用缓存呢?有三种经典的缓存模式:

  • Cache-Aside Pattern
  • Read-Through/Write through
  • Write behind

Cache-Aside Pattern

Cache-Aside Pattern,即旁路缓存模式,它的提出是为了尽可能地解决缓存与数据库的数据不一致问题。

Cache-Aside读流程

Cache-Aside Pattern的读请求流程如下:

Cache-Aside读请求

  1. 读的时候,先读缓存,缓存命中的话,直接返回数据
  2. 缓存没有命中的话,就去读数据库,从数据库取出数据,放入缓存后,同时返回响应。

Cache-Aside 写流程

Cache-Aside Pattern的写请求流程如下:

Cache-Aside写请求

更新的时候,先更新数据库,然后再删除缓存。

Read-Through/Write-Through(读写穿透)

Read/Write Through模式中,服务端把缓存作为主要数据存储。应用程序跟数据库缓存交互,都是通过抽象缓存层完成的。

Read-Through

Read-Through的简要流程如下

Read Through简要流程

  1. 从缓存读取数据,读到直接返回
  2. 如果读取不到的话,从数据库加载,写入缓存后,再返回响应。

这个简要流程是不是跟Cache-Aside很像呢?其实Read-Through就是多了一层Cache-Provider,流程如下:

Read-Through流程

Read-Through实际只是在Cache-Aside之上进行了一层封装,它会让程序代码变得更简洁,同时也减少数据源上的负载。

Write-Through

Write-Through模式下,当发生写请求时,也是由缓存抽象层完成数据源和缓存数据的更新,流程如下:
Write-Through流程

Write behind (异步缓存写入)

Write behind跟Read-Through/Write-Through有相似的地方,都是由Cache Provider来负责缓存和数据库的读写。它两又有个很大的不同:Read/Write Through是同步更新缓存和数据的,Write Behind则是只更新缓存,不直接更新数据库,通过批量异步的方式来更新数据库。

Write behind流程

这种方式下,缓存和数据库的一致性不强,对一致性要求高的系统要谨慎使用。但是它适合频繁写的场景,MySQL的InnoDB Buffer Pool机制就使用到这种模式。

操作缓存的时候,删除缓存呢,还是更新缓存?

一般业务场景,我们使用的就是Cache-Aside模式。
有些小伙伴可能会问, Cache-Aside在写入请求的时候,为什么是删除缓存而不是更新缓存呢?

Cache-Aside写入流程

我们在操作缓存的时候,到底应该删除缓存还是更新缓存呢?我们先来看个例子:

  1. 线程A先发起一个写操作,第一步先更新数据库
  2. 线程B再发起一个写操作,第二步更新了数据库
  3. 由于网络等原因,线程B先更新了缓存
  4. 线程A更新缓存。

这时候,缓存保存的是A的数据(老数据),数据库保存的是B的数据(新数据),数据不一致了,脏数据出现啦。如果是删除缓存取代更新缓存则不会出现这个脏数据问题。

更新缓存相对于删除缓存,还有两点劣势:

  • 如果你写入的缓存值,是经过复杂计算才得到的话。更新缓存频率高的话,就浪费性能啦。
  • 在写数据库场景多,读数据场景少的情况下,数据很多时候还没被读取到,又被更新了,这也浪费了性能呢(实际上,写多的场景,用缓存也不是很划算了)

双写的情况下,先操作数据库还是先操作缓存?

Cache-Aside缓存模式中,有些小伙伴还是有疑问,在写入请求的时候,为什么是先操作数据库呢?为什么不先操作缓存呢?

假设有A、B两个请求,请求A做更新操作,请求B做查询读取操作。
image.png

  1. 线程A发起一个写操作,第一步del cache
  2. 此时线程B发起一个读操作,cache miss
  3. 线程B继续读DB,读出来一个老数据
  4. 然后线程B把老数据设置入cache
  5. 线程A写入DB最新的数据

酱紫就有问题啦,缓存和数据库的数据不一致了。缓存保存的是老数据,数据库保存的是新数据。因此,Cache-Aside缓存模式,选择了先操作数据库而不是先操作缓存。

缓存延时双删

有些小伙伴可能会说,不一定要先操作数据库呀,采用缓存延时双删策略就好啦?什么是延时双删呢?

image.png

  1. 先删除缓存
  2. 再更新数据库
  3. 休眠一会(比如1秒),再次删除缓存。

这个休眠一会,一般多久呢?都是1秒?

这个休眠时间 = 读业务逻辑数据的耗时 + 几百毫秒。 为了确保读请求结束,写请求可以删除读请求可能带来的缓存脏数据。

删除缓存重试机制

不管是延时双删还是Cache-Aside的先操作数据库再删除缓存,如果第二步的删除缓存失败呢,删除失败会导致脏数据哦~

删除失败就多删除几次呀,保证删除缓存成功呀~ 所以可以引入删除缓存重试机制

image.png

  1. 写请求更新数据库
  2. 缓存因为某些原因,删除失败
  3. 把删除失败的key放到消息队列
  4. 消费消息队列的消息,获取要删除的key
  5. 重试删除缓存操作

读取biglog异步删除缓存

重试删除缓存机制还可以,就是会造成好多业务代码入侵。其实,还可以通过数据库的binlog来异步淘汰key。

image.png

以mysql为例 可以使用阿里的canal将binlog日志采集发送到MQ队列里面,然后通过ACK机制确认处理这条更新消息,删除缓存,保证数据缓存一致性

参考与感谢

  • 并发环境下,先操作数据库还是先操作缓存?
  • 高并发场景下,到底先更新缓存还是先更新数据库?
  • 两难!先更新数据库再删缓存?还是先删缓存再更新数据库?
  • 3种缓存读写策略都不了解?面试很难让你通过啊兄弟

本文转载自: 掘金

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

Netty源码深度解析-EventLoop(2)EventL

发表于 2021-05-20

导读

原创文章,转载请注明出处。

本文源码地址:netty-source-code-analysis

本文所使用的netty版本4.1.6.Final:带注释的netty源码

EventLoop在netty中发挥着驱动引擎的作用,本文我们以NioEventLoop为例分析一下EventLoop的工作原理。

1 EventLoop线程的创建时机

还记得我们在“服务端启动”和“客户端启动”这两篇文章中都有一个重要操作吗?就是将Channel注册到EventLoop上。我们看AbstractChannel的register方法,其中有eventLoop.execute调用,是不是很熟悉。大多数情况下(并非绝对),这里就是EventLoop线程开始的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
arduino复制代码public final void register(EventLoop eventLoop, final ChannelPromise promise) {
if (eventLoop.inEventLoop()) {
register0(promise);
} else {
try {
eventLoop.execute(new Runnable() {
@Override
public void run() {
register0(promise);
}
});
} catch (Throwable t) {

}
}
}

咱们跟进去execute方法,这个方法的实现在SingleThreadEventExecutor中,我们看到这里有一个startThread方法,看名字就知道,这里是线程真正开始的地方,一起来看看吧。

后面的!addTaskWakesUp && wakesUpForTask(task)是怎么回事呢?

EventLoop的实现中,有的EventLoop实现会阻塞在任务队列上,对于这样的EventLoop唤醒方法是向任务队列中添加一个比较特殊的任务,这样的EventLoop中addTaskWakesUp为ture。

而有的EventLoop比如NioEventLoop不会阻塞在任务队列上,但是会阻塞在selector上,对于这样的EventLoop通过调用wakeup方法唤醒,这样的EventLoop中addTaskWakesUp为false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码 public void execute(Runnable task) {
boolean inEventLoop = inEventLoop();
if (inEventLoop) {
addTask(task);
} else {
startThread();
addTask(task);
if (isShutdown() && removeTask(task)) {
reject();
}
}

if (!addTaskWakesUp && wakesUpForTask(task)) {
wakeup(inEventLoop);
}
}

我们看看SingleThreadEventExecutor中的wakeup方法,这里通过向队列中添加一个特殊的task来唤醒EventLoop线程。

1
2
3
4
5
typescript复制代码protected void wakeup(boolean inEventLoop) {
if (!inEventLoop || STATE_UPDATER.get(this) == ST_SHUTTING_DOWN) {
taskQueue.offer(WAKEUP_TASK);
}
}

而NioEventLoop中覆盖了这个wakeup方法,通过调用selector.wakeup方法来唤醒EventLoop线程,因为wakeup是个重量级操作,所以netty用了一个AtomicBoolean类型的wakenUp变量来减少wakeup的次数,如果已经被wakeup了,就不再调用selector.wakeup。

1
2
3
4
5
typescript复制代码protected void wakeup(boolean inEventLoop) {
if (!inEventLoop && wakenUp.compareAndSet(false, true)) {
selector.wakeup();
}
}

startThread方法中首先对EventLoop的状态做了判断,如果为ST_NOT_STARTED(未开始)状态,才调用doStartThread方法,接着跟下去看doStartThread方法。

1
2
3
4
5
6
7
java复制代码private void startThread() {
if (STATE_UPDATER.get(this) == ST_NOT_STARTED) {
if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
doStartThread();
}
}
}

doStartThread接着调用了SingleThreadEventExecutor.this.run()方法,这个run方法是抽象的,在这里没有实现。我们重点关注NioEventLoop,所以我们去看NioEventLoop中run方法的实现。

我们前面已经讲过了这个executor是ThreadPerTaskExecutor,所以这里调用execute方法会创建出一个新的线程,这个线程就是EventLoop线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码private void doStartThread() {
assert thread == null;
executor.execute(new Runnable() {
@Override
public void run() {
try {
SingleThreadEventExecutor.this.run();
} catch (Throwable t) {
} finally {

}
}
});
}

2 EventLoop线程的工作内容

接下来我们要分析的逻辑中有很多关于wakenup的magic操作,我也看不懂,非常难以理解,很多操作我觉得是没有必要的。这些地方在后来的版本中经过一次比较大的重构,逻辑更加清晰了,感兴趣的同学可以看一下这次github上的代码提交,Clean Up NioEventLoop。下面贴出的代码中已经删除了很多难以理解的,又不影响我们理解整个EvenLoop主体逻辑的代码。

run方法中是一个死循环,这就是EventLoop线程的主要逻辑内容了。

每次循环之前要先调用一下selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())方法判断下一步的动作,默认实现在DefaultSelectStrategy中。这里如果hasTasks为true也就是说taskQueue中有任务要执行的话,会调用一下selectSupplier.get(),这个selectSupplier是NioEventLoop中的selectNowSupplier属性,get逻辑非常简单,就是调用一下非阻塞的selectNow方法。

selectStrategy.calculateStrategy这里的整体逻辑就是,如果当前有任务要执行,就立即调用selectNow返回一个>=0的值,这将导致run方法直接跳出switch去执行下面的逻辑。否则就返回SelectStrategy#SELECT,这样run方法将进入select(wakenUp.getAndSet(false))执行。

1
2
3
4
5
6
7
8
9
10
java复制代码public int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception {
return hasTasks ? selectSupplier.get() : SelectStrategy.SELECT;
}

private final IntSupplier selectNowSupplier = new IntSupplier() {
@Override
public int get() throws Exception {
return selectNow();
}
};

咱们接着看run方法,run方法中的三个重要操作:

  • select(wakenUp.getAndSet(false))
  • processSelectedKeys()
  • runAllTasks()

EventLoop的一生都在为这3件事奔波,咱们一起来看一下。

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
java复制代码 protected void run() {
for (;;) {
try {
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.SELECT:
//select操作
select(wakenUp.getAndSet(false));

default:
// fallthrough
}

cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
try {
//处理Channel事件
processSelectedKeys();
} finally {
// Ensure we always run tasks.
//处理队列中的任务
runAllTasks();
}
} else {
final long ioStartTime = System.nanoTime();
try {
processSelectedKeys();
} finally {
// Ensure we always run tasks.
final long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}
} catch (Throwable t) {

}

}
}

2.1 select

select中也有一个死循环操作,在循环之前首先计算出一个selectDeadLineNanos,这是select操作的最迟返回时间,是当前时间+下一个定时任务距离现在的时间。

deleayNanos方法会到定时任务队列(EventLoop的创建这篇文章中的scheduledTaskQueue)查看队首的任务距离现在还有多久,如果没有定时任务的话,默认返回1秒。

timeoutMillis即select操作的超时时间,至于这里为什么加上500微秒,我也觉得很奇怪,没有理解,咱们暂且不去管它,接着往下看。

如果发现timeoutMillis<=0说明现在有定时任务要执行了,立即调用非阻塞的selector.selectNow方法,并且跳出循环。

咱们接着往下看是阻塞式的selector.select操作,如果阻塞期间有任务加入,会调用wakeupy方法,这个select操作会立即返回。

接下来的代码咱们只关注 rebuildSelector,这是为了修复jdk的空轮询bug而设计的,默认如果发生了512次空轮询就重建selector。
select循环跳出的条件大致有以下几种情况:

  • 有定时任务到期了
  • selector.select(timeoutMillis)操作返回非0值
  • 往EventLoop中添加任务时唤醒了阻塞的selector.select(timeoutMillis)操作
  • 出现了512次空轮询
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
java复制代码private void select(boolean oldWakenUp) throws IOException {
Selector selector = this.selector;
try {
int selectCnt = 0;
long currentTimeNanos = System.nanoTime();
//select操作的最迟返回时间
long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
for (;;) {
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
if (timeoutMillis <= 0) {
if (selectCnt == 0) {
selector.selectNow();
selectCnt = 1;
}
break;
}

int selectedKeys = selector.select(timeoutMillis);
selectCnt ++;

if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {

break;
}

if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {

} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
//修复jdk空轮询bug
rebuildSelector();
selector = this.selector;

selector.selectNow();
selectCnt = 1;
break;
}

currentTimeNanos = time;
}

} catch (CancelledKeyException e) {

}
}

2.2 processSelectedKeys

在跳出select循环之后又回到了run方法,咱们接着看run方法剩余的逻辑,首先把callcedlledKeys设置为0,并且把needsToSelectAgain设置为false,这两个是控制对已经取消的SelectionKey进行清理的变量,每次调用selector.select或者selector.selectNow方法都会导致selector驱逐出已经被取消的Selectionkey,代码执行到这里,因为刚刚执行过select或者selectNow方法,所以此时肯定不存在被取消的Selectionkey(SelectionKey的取消操作必须被EventLoop线程执行)。

接着判断ioRatio变量的值,ioRatio是表示EventLoop处理io事件的时间比例,默认值为50。这个只能大致控制处理io事件的时间和处理异步任务的时间比例,并非绝对值。

这里如果ioRatio等于100,就先处理所有的io事件,再处理所有的任务。
如果ioRatio不等于100,就先处理所有的io事件,再处理异步任务,此时处理异步任务会有一个超时时间,是根据处理io事件所消耗的时间和ioRatio计算出来的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
try {
//处理Channel事件
processSelectedKeys();
} finally {
// Ensure we always run tasks.
//处理队列中的任务
runAllTasks();
}
} else {
final long ioStartTime = System.nanoTime();
try {
processSelectedKeys();
} finally {
// Ensure we always run tasks.
final long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}

我们先去看processSelectedKeys方法,这里首先判断selectedKeys是否为null,如果不为null说明selectedKeys被netty优化过,咱们直接去看第1个分支,也就是优化过的分支。

1
2
3
4
5
6
7
scss复制代码private void processSelectedKeys() {
if (selectedKeys != null) {
processSelectedKeysOptimized(selectedKeys.flip());
} else {
processSelectedKeysPlain(selector.selectedKeys());
}
}

processSelectedKeysOptimized方法遍历SelectionKey数组,对每个SelectionKey调用processSelectedKey(k, (AbstractNioChannel) a)方法进行处理。

如果发现被取消的key过多,默认超过256,则清空数组中的剩余元素,重新select,重新select时selector会驱逐已经取消的key。

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
java复制代码private void processSelectedKeysOptimized(SelectionKey[] selectedKeys) {
for (int i = 0;; i ++) {
final SelectionKey k = selectedKeys[i];
//key == null说明已经遍历完了
if (k == null) {
break;
}
//遍历过的元素设置为null
selectedKeys[i] = null;

final Object a = k.attachment();

if (a instanceof AbstractNioChannel) {
//处理SelectionKey
processSelectedKey(k, (AbstractNioChannel) a);
} else {

}
//如果发现被取消的Key过多,默认超过256,则清空数组中的剩余元素,重新select
if (needsToSelectAgain) {
//清空数组剩余的元素
for (;;) {
i++;
if (selectedKeys[i] == null) {
break;
}
selectedKeys[i] = null;
}
//重新select,则selector会驱逐出已经取消的key
selectAgain();

selectedKeys = this.selectedKeys.flip();
i = -1;
}
}
}

接下来咱们关注一下processSelectedKey方法,这里首先判断一下Key是否已经取消了,如果已经取消了,则调用unsafe.close关闭Channel。接着往下首先判断是否发生了OP_CONNECT事件,还记得NioSocketChannel.doConnect方法吗,如果SocketChannel的connect方法返回false,还要继续调用SocketChannel的finishConnect方法`才能真正完全连接,这里咱们在在“客户端的启动流程”这篇文章中已经讲过了,这里不再展开。

接下来是判断是否发生了OP_WRITE事件,OP_WRITE事件表明TCP缓冲区有空间可以写数据,此时调用unsafe.forceFlush方法将AbstractUnsafe#outboundBuffer中的数据写入到TCP缓冲区中。

最后判断如果发生了OP_READ或者OP_ACCEPT事件,表明Channel可读或者有新连接接入,此时调用unsafe.read方法读取数据。那么有人有疑问了,OP_ACCEPT事件为什么也能调用unsafe.read呢,此时并没有数据可以读取啊,这里就是一个特殊之处了,专为AbstractNioMessageChannel而设计,而NioServerSocketChannel也是AbstractNioMessageChannel的子类,感兴趣的同学去看一下NioMessageUnsafe#read方法和NioServerSocketChannel#doReadMessages方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
//key已经取消了
if (!k.isValid()) {
unsafe.close(unsafe.voidPromise());
return;
}
try {
//在调用SocketChannel的connect方法返回false时,这里需要处理OP_CONNECT事件,在unsafe.finishConnect()方法中调用SocketChannel的finishConnect方法
//参考:io.netty.channel.socket.nio.NioSocketChannel.doConnect
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
// remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
// See https://github.com/netty/netty/issues/924
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
unsafe.finishConnect();
}
//这个表示当前tcp缓冲区可写
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
//将未写入缓冲区的数据flush到缓冲区
ch.unsafe().forceFlush();
}
//OP_ACCEPT专为`AbstractNioMessageChannel`而设计
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}

至此run方法中,processSelectedKeys咱们已经分析完了,接下来看另外两个调用,runAllTasks()和runAllTasks(long timeoutNanos),这两个方法主体逻辑差不多,只不过其中一个带有超时时间控制。

2.3 runAllTasks

咱们先看runAllTasks()方法,这里首先调用fetchFromScheduledTaskQueue()从scheduledTaskQueue中将到期的定时任务拉到taskQueue队列中,再调用runAllTasksFrom(taskQueue)将taskQueue队列中的所有任务执行完毕,最后调用afterRunningAllTasks()方法执行完所有tailTasks队列中的任务。这里用到了咱们在“EventLoop的构造”这篇中提到的3个重要的队列。

咱们还没说过tailTasks这个队列有什么用,其实我也还没发现它有什么用,有人说它可以用来统计run方法每次循环的时间。好吧,反正它不是重点,不必纠结。

fetchFromScheduledTaskQueue()、runAllTasksFrom(taskQueue)和afterRunningAllTasks()这3个方法比较简单,咱们不再展开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码protected boolean runAllTasks() {
assert inEventLoop();
boolean fetchedAll;
boolean ranAtLeastOne = false;

do {
//从`scheduledTaskQueue`中将到期的定时任务拉到`taskQueue`队列中
fetchedAll = fetchFromScheduledTaskQueue();
//将taskQueue中的所有任务执行完毕
if (runAllTasksFrom(taskQueue)) {
ranAtLeastOne = true;
}
} while (!fetchedAll); // keep on processing until we fetched all scheduled tasks.

if (ranAtLeastOne) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
}
//将tailTasks中的所有任务执行完毕
afterRunningAllTasks();
return ranAtLeastOne;
}

接着看runAllTasks(long timeoutNanos)方法,这个方法咱们不多展开讲了,与runAllTasks()的区别就在于多了一个超时时间,Netty这里对超时的判断做了一些优化,因为System.nanoTime是一个很重的操作,所以这里并不是每执行一个任务就判断一下是否超时,而是每执行64个任务判断一下是否超时。

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
java复制代码protected boolean runAllTasks(long timeoutNanos) {
//从scheduledTaskQueue将所有到期的任务拉到taskQueue中
fetchFromScheduledTaskQueue();
Runnable task = pollTask();
if (task == null) {
afterRunningAllTasks();
return false;
}
//ScheduledFutureTask.nanoTime()是一个从ScheduledFutureTask的类加载开始的一个相对时间
final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;
long runTasks = 0;
long lastExecutionTime;
//遍历taskQueue执行任务,因为natoTime操作很重,所以每64次任务判断一次是否超过了超时时间
for (; ; ) {
safeExecute(task);

runTasks++;

if ((runTasks & 0x3F) == 0) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
if (lastExecutionTime >= deadline) {
break;
}
}

task = pollTask();
if (task == null) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
break;
}
}
//执行所有tailTasks中的任务
afterRunningAllTasks();
this.lastExecutionTime = lastExecutionTime;
return true;
}

3 总结

EventLoop的一生就是一个死循环,这个死循环中每次循环它干了3件事。

  • select:选出有兴趣事件发生的Channel
  • processSelectedKeys:处理Channel上发生的io事件
  • runAllTasks:执行异步任务,包括scheduledTaskQueue、taskQueue和tailTasks这3个队列中的任务。

关于作者

王建新,转转架构部资深Java工程师,主要负责服务治理、RPC框架、分布式调用跟踪、监控系统等。爱技术、爱学习,欢迎联系交流。

本文转载自: 掘金

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

一篇让你熟练掌握Google Guava包(全网最全) Go

发表于 2021-05-20

Google Guava

guava开源库的地址:github.com/google/guav…

概述

工具类 就是封装平常用的方法,不需要你重复造轮子,节省开发人员时间,提高工作效率。谷歌作为大公司,当然会从日常的工作中提取中很多高效率的方法出来。所以就诞生了guava。

guava的优点:

  • 高效设计良好的API,被Google的开发者设计,实现和使用
  • 遵循高效的java语法实践
  • 使代码更刻度,简洁,简单
  • 节约时间,资源,提高生产力

guava的核心库:

  • 集合 [collections]
  • 缓存 [caching]
  • 原生类型支持 [primitives support]
  • 并发库 [concurrency libraries]
  • 通用注解 [common annotations]
  • 字符串处理 [string processing]
  • I/O 等等。
    在这里插入图片描述

guava的使用

引入gradle依赖(引入Jar包)

1
java复制代码compile 'com.google.guava:guava:26.0-jre'
1
2
3
4
5
java复制代码        <dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>

1.集合的创建

1.1

1
2
3
4
5
6
7
8
9
java复制代码// 普通Collection的创建
List<String> list = Lists.newArrayList();
Set<String> set = Sets.newHashSet();
Map<String, String> map = Maps.newHashMap();

// 不变Collection的创建
ImmutableList<String> iList = ImmutableList.of("a", "b", "c");
ImmutableSet<String> iSet = ImmutableSet.of("e1", "e2");
ImmutableMap<String, String> iMap = ImmutableMap.of("k1", "v1", "k2", "v2");

创建不可变集合 先理解什么是immutable(不可变)对象

  • 在多线程操作下,是线程安全的
  • 所有不可变集合会比可变集合更有效的利用资源
  • 中途不可改变
1
java复制代码ImmutableList<String> immutableList = ImmutableList.of("1","2","3","4");

这声明了一个不可变的List集合,List中有数据1,2,3,4。类中的 操作集合的方法(譬如add, set, sort, replace等)都被声明过期,并且抛出异常。 而没用guava之前是需要声明并且加各种包裹集合才能实现这个功能

1
2
3
4
5
java复制代码  // add 方法
@Deprecated @Override
public final void add(int index, E element) {
throw new UnsupportedOperationException();
}

1.2
当我们需要一个map中包含key为String类型,value为List类型的时候,以前我们是这样写的

1
2
3
4
5
6
java复制代码Map<String,List<Integer>> map = new HashMap<String,List<Integer>>();
List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
map.put("aa", list);
System.out.println(map.get("aa"));//[1, 2]

现在

1
2
3
4
java复制代码Multimap<String,Integer> map = ArrayListMultimap.create();		
map.put("aa", 1);
map.put("aa", 2);
System.out.println(map.get("aa")); //[1, 2]

1.3

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码MultiSet: 无序+可重复   count()方法获取单词的次数  增强了可读性+操作简单
创建方式: Multiset<String> set = HashMultiset.create();

Multimap: key-value key可以重复
创建方式: Multimap<String, String> teachers = ArrayListMultimap.create();

BiMap: 双向Map(Bidirectional Map) 键与值都不能重复
创建方式: BiMap<String, String> biMap = HashBiMap.create();

Table: 双键的Map Map--> Table-->rowKey+columnKey+value //和sql中的联合主键有点像
创建方式: Table<String, String, Integer> tables = HashBasedTable.create();

...等等(guava中还有很多java里面没有给出的集合类型)

2.特色工具

字符串连接器Joiner
连接多个字符串并追加到StringBuilder

1
2
3
4
5
6
java复制代码        StringBuilder stringBuilder = new StringBuilder("hello");
// 字符串连接器,以|为分隔符,同时去掉null元素
Joiner joiner1 = Joiner.on("|").skipNulls();
// 构成一个字符串foo|bar|baz并添加到stringBuilder
stringBuilder = joiner1.appendTo(stringBuilder, "foo", "bar", null, "baz");
System.out.println(stringBuilder); // hellofoo|bar|baz

连接List元素并写到文件流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码        FileWriter fileWriter = null;
try{
fileWriter = new FileWriter(new File("/home/gzx/Documents/tmp.txt"));
}
catch(Exception e){
System.out.println(e.getMessage());
}
List<Date> dateList = new ArrayList<Date>();
dateList.add(new Date());
dateList.add(null);
dateList.add(new Date());
// 构造连接器:如果有null元素,替换为no string
Joiner joiner2 = Joiner.on("#").useForNull("no string");
try{
// 将list的元素的tostring()写到fileWriter,是否覆盖取决于fileWriter的打开方式,默认是覆盖,若有true,则是追加
joiner2.appendTo(fileWriter, dateList);
// 必须添加close(),否则不会写文件
fileWriter.close();
}
catch(IOException e){
System.out.println(e.getMessage());
}

字符串分割器Splitter

将字符串分割为Iterable

1
2
3
4
5
6
7
java复制代码        // 分割符为|,并去掉得到元素的前后空白
Splitter sp = Splitter.on("|").trimResults();
String str = "hello | world | your | Name ";
Iterable<String> ss = sp.split(str);
for(String it : ss){
System.out.println(it);
}

结果为:
hello
world
your
Name

字符串工具类Strings

1
2
3
4
5
6
7
8
java复制代码        System.out.println(Strings.isNullOrEmpty("")); // true
System.out.println(Strings.isNullOrEmpty(null)); // true
System.out.println(Strings.isNullOrEmpty("hello")); // false
// 将null转化为""
System.out.println(Strings.nullToEmpty(null)); // ""

// 从尾部不断补充T只到总共8个字符,如果源字符串已经达到或操作,则原样返回。类似的有padStart
System.out.println(Strings.padEnd("hello", 8, 'T')); // helloTTT

字符匹配器CharMatcher
空白一一替换

1
2
3
4
java复制代码        // 空白回车换行对应换成一个#,一对一换
String stringWithLinebreaks = "hello world\r\r\ryou are here\n\ntake it\t\t\teasy";
String s6 = CharMatcher.BREAKING_WHITESPACE.replaceFrom(stringWithLinebreaks,'#');
System.out.println(s6); // hello#world###you#are#here##take#it###easy

连续空白缩成一个字符

1
2
3
4
java复制代码        // 将所有连在一起的空白回车换行字符换成一个#,倒塌
String tabString = " hello \n\t\tworld you\r\nare here ";
String tabRet = CharMatcher.WHITESPACE.collapseFrom(tabString, '#');
System.out.println(tabRet); // #hello#world#you#are#here#

去掉前后空白和缩成一个字符

1
2
3
java复制代码// 在前面的基础上去掉字符串的前后空白,并将空白换成一个#
String trimRet = CharMatcher.WHITESPACE.trimAndCollapseFrom(tabString, '#');
System.out.println(trimRet);// hello#world#you#are#here

保留数字

1
2
3
4
java复制代码        String letterAndNumber = "1234abcdABCD56789";
// 保留数字
String number = CharMatcher.JAVA_DIGIT.retainFrom(letterAndNumber);
System.out.println(number);// 123456789

3.将集合转换为特定规则的字符串

3.1以前我们将list转换为特定规则的字符串是这样写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码//use java
List<String> list = new ArrayList<String>();
list.add("aa");
list.add("bb");
list.add("cc");
String str = "";
for(int i=0; i<list.size(); i++){
str = str + "-" +list.get(i);
}
//str 为-aa-bb-cc

//use guava
List<String> list = new ArrayList<String>();
list.add("aa");
list.add("bb");
list.add("cc");
String result = Joiner.on("-").join(list);
//result为 aa-bb-cc

3.2把map集合转换为特定规则的字符串

1
2
3
4
5
java复制代码Map<String, Integer> map = Maps.newHashMap();
map.put("xiaoming", 12);
map.put("xiaohong",13);
String result = Joiner.on(",").withKeyValueSeparator("=").join(map);
// result为 xiaoming=12,xiaohong=13

4.将String转换为特定的集合

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码//use java
List<String> list = new ArrayList<String>();
String a = "1-2-3-4-5-6";
String[] strs = a.split("-");
for(int i=0; i<strs.length; i++){
list.add(strs[i]);
}

//use guava
String str = "1-2-3-4-5-6";
List<String> list = Splitter.on("-").splitToList(str);
//list为 [1, 2, 3, 4, 5, 6]

guava还可以使用 omitEmptyStrings().trimResults() 去除空串与空格

1
2
3
java复制代码String str = "1-2-3-4-  5-  6   ";  
List<String> list = Splitter.on("-").omitEmptyStrings().trimResults().splitToList(str);
System.out.println(list);

将String转换为map

1
2
java复制代码String str = "xiaoming=11,xiaohong=23";
Map<String,String> map = Splitter.on(",").withKeyValueSeparator("=").split(str);

5.guava还支持多个字符切割,或者特定的正则分隔

1
2
java复制代码String input = "aa.dd,,ff,,.";
List<String> result = Splitter.onPattern("[.|,]").omitEmptyStrings().splitToList(input);

关于字符串的操作 都是在Splitter这个类上进行的

1
2
3
4
5
6
7
8
9
10
java复制代码// 判断匹配结果
boolean result = CharMatcher.inRange('a', 'z').or(CharMatcher.inRange('A', 'Z')).matches('K'); //true

// 保留数字文本 CharMatcher.digit() 已过时 retain 保留
//String s1 = CharMatcher.digit().retainFrom("abc 123 efg"); //123
String s1 = CharMatcher.inRange('0', '9').retainFrom("abc 123 efg"); // 123

// 删除数字文本 remove 删除
// String s2 = CharMatcher.digit().removeFrom("abc 123 efg"); //abc efg
String s2 = CharMatcher.inRange('0', '9').removeFrom("abc 123 efg"); // abc efg

6. 集合的过滤

我们对于集合的过滤,思路就是迭代,然后再具体对每一个数判断,这样的代码放在程序中,难免会显得很臃肿,虽然功能都有,但是很不好看。

guava写法

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
java复制代码import com.google.common.base.*;
import com.google.common.collect.*;
import com.google.common.collect.Maps;
import org.junit.jupiter.api.Test;

import java.util.*;

/**
* @author: xingkong
* @date: 2020/11/18 10:26
* @description:
*/
public class Test8 {

@Test
public void Test1(){
//按照条件过滤
ImmutableList<String> names = ImmutableList.of("begin", "code", "Guava", "Java");
Iterable<String> fitered = Iterables.filter(names, Predicates.or(Predicates.equalTo("Guava"), Predicates.equalTo("Java")));
System.out.println(fitered);
// [Guava, Java]

//自定义过滤条件 使用自定义回调方法对Map的每个Value进行操作
ImmutableMap<String, Integer> m = ImmutableMap.of("begin", 12, "code", 15);
// Function<F, T> F表示apply()方法input的类型,T表示apply()方法返回类型
Map<String, Integer> m2 = Maps.transformValues(m, input -> {
if(input > 12){
return input;
}else{
return input + 1;
}
});
System.out.println(m2);
//{begin=13, code=15}
}
}

set的交集, 并集, 差集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码HashSet setA = newHashSet(1, 2, 3, 4, 5);  
HashSet setB = newHashSet(4, 5, 6, 7, 8);

SetView union = Sets.union(setA, setB);
System.out.println("union:");
for (Integer integer : union)
System.out.println(integer); //union 并集:12345867

SetView difference = Sets.difference(setA, setB);
System.out.println("difference:");
for (Integer integer : difference)
System.out.println(integer); //difference 差集:123

SetView intersection = Sets.intersection(setA, setB);
System.out.println("intersection:");
for (Integer integer : intersection)
System.out.println(integer); //intersection 交集:45

map的交集,并集,差集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码HashMap<String, Integer> mapA = Maps.newHashMap();
mapA.put("a", 1);mapA.put("b", 2);mapA.put("c", 3);

HashMap<String, Integer> mapB = Maps.newHashMap();
mapB.put("b", 20);mapB.put("c", 3);mapB.put("d", 4);

MapDifference differenceMap = Maps.difference(mapA, mapB);
differenceMap.areEqual();
Map entriesDiffering = differenceMap.entriesDiffering();
Map entriesOnlyLeft = differenceMap.entriesOnlyOnLeft();
Map entriesOnlyRight = differenceMap.entriesOnlyOnRight();
Map entriesInCommon = differenceMap.entriesInCommon();

System.out.println(entriesDiffering); // {b=(2, 20)}
System.out.println(entriesOnlyLeft); // {a=1}
System.out.println(entriesOnlyRight); // {d=4}
System.out.println(entriesInCommon); // {c=3}

7.检查参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码//use java
if(list!=null && list.size()>0)
'''
if(str!=null && str.length()>0)
'''
if(str !=null && !str.isEmpty())

//use guava
if(!Strings.isNullOrEmpty(str))

//use java
if (count <= 0) {
throw new IllegalArgumentException("must be positive: " + count);
}

//use guava
Preconditions.checkArgument(count > 0, "must be positive: %s", count);

免去了很多麻烦!并且会使你的代码看上去更好看。而不是代码里面充斥着 !=null, !=””

==检查是否为空,不仅仅是字符串类型,其他类型的判断,全部都封装在 Preconditions类里,里面的方法全为静态==

其中的一个方法的源码

1
2
3
4
5
6
7
java复制代码@CanIgnoreReturnValue
public static <T> T checkNotNull(T reference) {
if (reference == null) {
throw new NullPointerException();
}
return reference;
}

在这里插入图片描述

8. MoreObjects

这个方法是在Objects过期后官方推荐使用的替代品,该类最大的好处就是不用大量的重写 ==toString==,用一种很优雅的方式实现重写,或者在某个场景定制使用。

1
2
3
4
java复制代码Person person = new Person("aa",11);
String str = MoreObjects.toStringHelper("Person").add("age", person.getAge()).toString();
System.out.println(str);
//输出Person{age=11}

9.强大的Ordering排序器

排序器[Ordering]是Guava流畅风格比较器[Comparator]的实现,它可以用来为构建复杂的比较器,以完成集合排序的功能。

1
2
3
4
5
6
7
8
9
java复制代码natural()	对可排序类型做自然排序,如数字按大小,日期按先后排序
usingToString() 按对象的字符串形式做字典排序[lexicographical ordering]
from(Comparator) 把给定的Comparator转化为排序器
reverse() 获取语义相反的排序器
nullsFirst() 使用当前排序器,但额外把null值排到最前面。
nullsLast() 使用当前排序器,但额外把null值排到最后面。
compound(Comparator) 合成另一个比较器,以处理当前排序器中的相等情况。
lexicographical() 基于处理类型T的排序器,返回该类型的可迭代对象Iterable<T>的排序器。
onResultOf(Function) 对集合中元素调用Function,再按返回值用当前排序器排序。

示例

1
2
3
4
5
6
7
8
9
java复制代码Person person = new Person("aa",14);  //String name  ,Integer age
Person ps = new Person("bb",13);
Ordering<Person> byOrdering = Ordering.natural().nullsFirst().onResultOf(new Function<Person,String>(){
public String apply(Person person){
return person.age.toString();
}
});
byOrdering.compare(person, ps);
System.out.println(byOrdering.compare(person, ps)); //1 person的年龄比ps大 所以输出1

10.计算中间代码的运行时间

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
java复制代码import com.google.common.base.Stopwatch;

import java.util.concurrent.TimeUnit;

/**
* @author: xingkong
* @date: 2020/11/18 20:16
* @description:
*/
public class Test9 {
public static void main(String[] args) throws InterruptedException {
// 创建stopwatch并开始计时
Stopwatch stopwatch = Stopwatch.createStarted();
Thread.sleep(1980);

// 以秒打印从计时开始至现在的所用时间,向下取整
System.out.println(stopwatch.elapsed(TimeUnit.SECONDS)); // 1

// 停止计时
stopwatch.stop();
System.out.println(stopwatch.elapsed(TimeUnit.SECONDS)); // 1

// 再次计时
stopwatch.start();
Thread.sleep(100);
System.out.println(stopwatch.elapsed(TimeUnit.SECONDS)); // 2

// 重置并开始
stopwatch.reset().start();
Thread.sleep(1030);

// 检查是否运行
System.out.println(stopwatch.isRunning()); // true
long millis = stopwatch.elapsed(TimeUnit.MILLISECONDS); // 1034
System.out.println(millis);

// 打印
System.out.println(stopwatch.toString()); // 1.034 s

}
}

11.文件操作

**以前我们写文件读取的时候要定义缓冲区,各种条件判断,%#@#

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码File file = new File("test.txt");
List<String> list = null;
try {
list = Files.readLines(file, Charsets.UTF_8);
} catch (Exception e) {
}

Files.copy(from,to); //复制文件
Files.deleteDirectoryContents(File directory); //删除文件夹下的内容(包括文件与子文件夹)
Files.deleteRecursively(File file); //删除文件或者文件夹
Files.move(File from, File to); //移动文件
URL url = Resources.getResource("abc.xml"); //获取classpath根下的abc.xml文件url

12.guava缓存

==guava的缓存设计的比较巧妙,可以很精巧的使用。guava缓存创建分为两种,一种是CacheLoader,另一种则是callback方式==

CacheLoader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码LoadingCache<String,String> cahceBuilder=CacheBuilder
.newBuilder()
.build(new CacheLoader<String, String>(){
@Override
public String load(String key) throws Exception {
String strProValue="hello "+key+"!";
return strProValue;
}
});
System.out.println(cahceBuilder.apply("begincode")); //hello begincode!
System.out.println(cahceBuilder.get("begincode")); //hello begincode!
System.out.println(cahceBuilder.get("wen")); //hello wen!
System.out.println(cahceBuilder.apply("wen")); //hello wen!
System.out.println(cahceBuilder.apply("da"));//hello da!
cahceBuilder.put("begin", "code");
System.out.println(cahceBuilder.get("begin")); //code

api中已经把apply声明为过期,声明中推荐使用get方法获取值

关注公众号“程序员面试之道”

回复“面试”获取面试一整套大礼包!!!

本公众号分享自己从程序员小白到经历春招秋招斩获10几个offer的面试笔试经验,其中包括【Java】、【操作系统】、【计算机网络】、【设计模式】、【数据结构与算法】、【大厂面经】、【数据库】期待你加入!!!

1.计算机网络—-三次握手四次挥手

2.梦想成真—–项目自我介绍

3.你们要的设计模式来了

4.震惊!来看《这份程序员面试手册》!!!

5.一字一句教你面试“个人简介”

6.接近30场面试分享

本文转载自: 掘金

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

什么是Ribbon现在就带你研究!

发表于 2021-05-20

什么是Ribbon

Ribbon是Netflix发布的负载均衡器。属于SpringCloud组件之一,用于实现客户端负载均衡功能。

服务器端负载均衡

所谓服务器端负载均衡,⽐如Nginx、F5这些,请求到达服务器之后由这些负载均衡
器根据⼀定的算法将请求路由到⽬标服务器处理。

客户端负载均衡

所谓客户端负载均衡,⽐如我们要说的Ribbon,服务消费者客户端会有⼀个服务器
地址列表,调⽤⽅在请求前通过⼀定的负载均衡算法选择⼀个服务器进⾏访问,负
载均衡算法的执⾏是在请求客户端进⾏。

Ribbon的执行源码

ribbon的负载均衡是通过@LoadBalanced启动的,首先看一下LoadBalanced注解,关注一下提示被注解标记的RestTemplate bean会被配置使用LoadBalancerClient,所以我们就得到了的,主要的执行过程是在LoadBalancerClient对象中。

1
2
3
4
5
6
7
php复制代码/**
* Annotation to mark a RestTemplate bean to be configured to use a LoadBalancerClient
* @author Spencer Gibb
*/
...
public @interface LoadBalanced {
}

LoadBalancerClient

这边我把注释删除了,可以看到execute方法肯定是执行方法。

1
2
3
4
5
6
7
8
9
java复制代码public interface LoadBalancerClient extends ServiceInstanceChooser {


<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;

<T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;

URI reconstructURI(ServiceInstance instance, URI original);
}

现在思考executor方法什么时候执行的?

查看引用,是在一个Interceptor方法中调用了。

image.png

RibbonLoadBalancerClient.execute

接下来看具体的execute方法

1
2
3
4
5
6
7
8
9
10
11
scss复制代码public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
//获取一个loadbalancer对象
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
//获取服务
Server server = getServer(loadBalancer);
//封装成一个RibbonServer对象
RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
serviceId), serverIntrospector(serviceId).getMetadata(server));
//执行
return execute(serviceId, ribbonServer, request);
}
getLoadBalancer方法

从一个SpringClientFactory对象获取

1
2
3
typescript复制代码protected ILoadBalancer getLoadBalancer(String serviceId) {
return this.clientFactory.getLoadBalancer(serviceId);
}
getServer方法

执行loadBalancer.chooseServer方法

1
2
3
4
5
6
kotlin复制代码protected Server getServer(ILoadBalancer loadBalancer) {
if (loadBalancer == null) {
return null;
}
return loadBalancer.chooseServer("default"); // TODO: better handling of key
}
RibbonLoadBalancer.chooseServer方法

主要看else的逻辑,执行父类的chooseServer方法

1
2
3
4
5
6
7
8
9
kotlin复制代码public Server chooseServer(Object key) {
//主要判断是否是多个分区 这是正对于aws的
if (ENABLED.get() && this.getLoadBalancerStats().getAvailableZones().size() > 1) {
...
} else {
logger.debug("Zone aware logic disabled or there is only one zone");
return super.chooseServer(key);
}
}
BaseLoadBalancer.chooseServer方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kotlin复制代码public Server chooseServer(Object key) {
//计数器,用于记录请求次数
if (this.counter == null) {
this.counter = this.createCounter();
}
this.counter.increment();
//负载均衡策略
if (this.rule == null) {
return null;
} else {
try {
return this.rule.choose(key);
} catch (Exception var3) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", new Object[]{this.name, key, var3});
return null;
}
}
}
PredicateBasedRule.choose方法

主要获取server的逻辑,从loadbalancer对象中获取所有的server集合

1
2
3
4
5
6
7
8
9
10
11
scss复制代码public Server choose(Object key) {
//获取loadbalancer对象
ILoadBalancer lb = getLoadBalancer();
//获取server对象 选择一个实例在过滤之后执行负载均衡
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}

以上就是ribbon的主要执行流程,接下来是具体实现细节的解答。

初始化疑问

首先在解决疑问前,要先了解ribbon的配置类,一般提前设置属性都是在初始化配置完成的。1.LoadBalancerAutoConfiguration 2.RibbonAutoConfiguration 3.RibbonClientConfiguration

疑问1.interceptor什么时候设置的?用来拦截什么对象?

这个问题是负载均衡相关的,首先定位到LoadBalancerAutoConfiguration类,具体看如下方法

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
java复制代码public class LoadBalancerAutoConfiguration {
//负载均衡
@LoadBalanced
//注入所有RestTemplate对象
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();

@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
//遍历所有restTemplate
return () -> restTemplateCustomizers.ifAvailable(customizers -> {
for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
//为每个restTemplate执行RestTemplateCustomizer.customize方法
//此处的customize方法就是下面的lambda表达式,就是设置拦截器
for (RestTemplateCustomizer customizer : customizers) {
customizer.customize(restTemplate);
}
}
});
}

...

@Configuration
@ConditionalOnClass(RetryTemplate.class)
public static class RetryInterceptorAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public RetryLoadBalancerInterceptor ribbonInterceptor(
LoadBalancerClient loadBalancerClient, LoadBalancerRetryProperties properties,
LoadBalancerRequestFactory requestFactory,
LoadBalancedRetryFactory loadBalancedRetryFactory) {
return new RetryLoadBalancerInterceptor(loadBalancerClient, properties,
requestFactory, loadBalancedRetryFactory);
}

@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
final RetryLoadBalancerInterceptor loadBalancerInterceptor) {
//此处用了函数式编程,本方法是为restTemplate设置拦截器
return restTemplate -> {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
};
}
}
}

所以我们知道了是在LoadBalancerAutoConfiguration类设置了拦截器,拦截restTemplate对象。

疑问2.SpringClientFactory什么时候设置的?

如下所示,SpringClientFactory是在RibbonAutoConfiguration自动配置类初始化的

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

....

@Bean
public SpringClientFactory springClientFactory() {
SpringClientFactory factory = new SpringClientFactory();
factory.setConfigurations(this.configurations);
return factory;
}
...
}

疑问3.LoadBalancer什么时候初始化的?Rule什么时候初始化的?

这个问题实例化就得干活儿了,涉及到需要具体实现,所以我们猜测是跟生成RibbonClient的时候有关。所以找到了RibbonClientConfiguration类,我只保留了重要代码,其他的…代替,

具体如下

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
kotlin复制代码public class RibbonClientConfiguration {

...

//设置负载均衡策略rule
@Bean
@ConditionalOnMissingBean
public IRule ribbonRule(IClientConfig config) {
//如果配置文件配置了,就生成配置的
if (this.propertiesFactory.isSet(IRule.class, name)) {
return this.propertiesFactory.get(IRule.class, config, name);
}
//默认分区隔离负载均衡策略
ZoneAvoidanceRule rule = new ZoneAvoidanceRule();
rule.initWithNiwsConfig(config);
return rule;
}

//同rule
@Bean
@ConditionalOnMissingBean
public IPing ribbonPing(IClientConfig config) {
if (this.propertiesFactory.isSet(IPing.class, name)) {
return this.propertiesFactory.get(IPing.class, config, name);
}
return new DummyPing();
}

//生成serverList 此时是个空对象
@Bean
@ConditionalOnMissingBean
@SuppressWarnings("unchecked")
public ServerList<Server> ribbonServerList(IClientConfig config) {
if (this.propertiesFactory.isSet(ServerList.class, name)) {
return this.propertiesFactory.get(ServerList.class, config, name);
}
ConfigurationBasedServerList serverList = new ConfigurationBasedServerList();
serverList.initWithNiwsConfig(config);
return serverList;
}

@Bean
@ConditionalOnMissingBean
public ServerListUpdater ribbonServerListUpdater(IClientConfig config) {
return new PollingServerListUpdater(config);
}

//生成loadBalancer对象
@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
//如果配置文件配置,则走配置文件
if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
return this.propertiesFactory.get(ILoadBalancer.class, config, name);
}
//生成默认分区LoadBalancer
return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
serverListFilter, serverListUpdater);
}
}

可以看出RibbonClientConfiguration生成了ribbon所需的所有组件,组成了ribbon服务。

疑问4.serverList什么时候加载的?

可以看到RibbonClientConfiguration生成了空的serverList对象,然后再创建loadbalancer时将这个空对象传入了。所以我们关注一下LoadBalancer初始化过程

首先调用了父类构造

1
2
3
4
5
scss复制代码public ZoneAwareLoadBalancer(IClientConfig clientConfig, IRule rule,
IPing ping, ServerList<T> serverList, ServerListFilter<T> filter,
ServerListUpdater serverListUpdater) {
super(clientConfig, rule, ping, serverList, filter, serverListUpdater);
}

进入父类构造,属性赋值的部分就不关注了,主要关注restOfInit方法

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码 public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping,
ServerList<T> serverList, ServerListFilter<T> filter,
ServerListUpdater serverListUpdater) {
super(clientConfig, rule, ping);
this.serverListImpl = serverList;
this.filter = filter;
this.serverListUpdater = serverListUpdater;
if (filter instanceof AbstractServerListFilter) {
((AbstractServerListFilter) filter).setLoadBalancerStats(getLoadBalancerStats());
}
restOfInit(clientConfig);
}
DynamicServerListLoadBalancer.restOfInit方法

进入restOfInit方法,主要看enableAndInitLearnNewServersFeature以及updateListOfServers方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码void restOfInit(IClientConfig clientConfig) {
boolean primeConnection = this.isEnablePrimingConnections();
// turn this off to avoid duplicated asynchronous priming done in BaseLoadBalancer.setServerList()
this.setEnablePrimingConnections(false);
//定时任务刷新服务器缓存列表
enableAndInitLearnNewServersFeature();
//因为上述方法是延迟至性的,所以立即获取一次服务器列表
updateListOfServers();
if (primeConnection && this.getPrimeConnections() != null) {
this.getPrimeConnections()
.primeConnections(getReachableServers());
}
this.setEnablePrimingConnections(primeConnection);
LOGGER.info("DynamicServerListLoadBalancer for client {} initialized: {}", clientConfig.getClientName(), this.toString());
}

DynamicServerListLoadBalancer.enableAndInitLearnNewServersFeature方法

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
java复制代码public void enableAndInitLearnNewServersFeature() {
LOGGER.info("Using serverListUpdater {}", serverListUpdater.getClass().getSimpleName());
serverListUpdater.start(updateAction);
}
//start方法
public synchronized void start(final UpdateAction updateAction) {
if (isActive.compareAndSet(false, true)) {
//新建runnable对象
final Runnable wrapperRunnable = new Runnable() {
@Override
public void run() {
if (!isActive.get()) {
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
}
return;
}
try {
//更新
updateAction.doUpdate();
lastUpdated = System.currentTimeMillis();
} catch (Exception e) {
logger.warn("Failed one update cycle", e);
}
}
};
//定时任务
scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
wrapperRunnable,
initialDelayMs,
refreshIntervalMs,
TimeUnit.MILLISECONDS
);
} else {
logger.info("Already active, no-op");
}
}

这边在看一下updateAction对象以及doUpdate方法,

1
2
3
4
5
6
java复制代码protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
@Override
public void doUpdate() {
updateListOfServers();
}
};
DynamicServerListLoadBalancer.updateListOfServers方法

这边就是更新服务列表的方法

1
2
3
4
5
6
7
8
9
scss复制代码 public void updateListOfServers() {
List<T> servers = new ArrayList<T>();
if (serverListImpl != null) {
//获取server集合
servers = serverListImpl.getUpdatedListOfServers();
...
}
updateAllServerList(servers);
}

这边serverListImpl就是具体的注册中心客户端实现,用来向注册中心发送请求 获取服务列表

image.png

以上就是ribbon的启动以及执行流程

本文转载自: 掘金

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

数据库改造,由Oracle换为MySQL遇到的坑

发表于 2021-05-20

前言

前段时间自行安装MySQL8.0数据库,为数据库去Oracle做准备。现在公司正规划搭建MySQL集群。于是挑选两个应用,进行改造测试,积累下经验。

迁移工具

测试时,使用powerdesinger进行表结构转换,使用Navicat进行数据导入。
生产环境数据量较大,会由数据组同事选用其他工具进行迁移,到时再行补充。

应用改造

添加mysql8.0驱动包

使用mysql-connector-java-8.0.15.jar,如果是maven管理,直接添加依赖:

1
2
3
4
5
6
7
xml复制代码                <!--MySql 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
<scope>runtime</scope>
</dependency>

修改数据源配置

1
2
3
4
ini复制代码spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://host:ip/database?useUnicode=true&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=password

对象改造

  • 如果使用的hibernate,将配置文件中主键自增的序列删除掉,在mysql中将主键设置为自增;或者为序列创建对应函数。使用mysql集群的话,主键的生成方式还会有改动。
  • 逐条sql检查,特别是mybatis中拼接的sql语句,包括:
    1. 主键修改:序列sequence删除,数据库中主键改为自增;或创建对应自增函数
    2. 对涉及mysql关键字的字段进行处理,使用``标识
    3. 日期格式处理
    4. rownum条件查询改为limit条件查询

问题汇总

问题:本地远程连接mysql数据库,报10060登录异常

  • 出现该问题可能的原因:
    1、网络不通;
    2、服务未启动;
    3、防火墙未关闭;
    4、服务器上防火墙端口未开放;
    5、端口未被监听;
    6、权限不足。
    我这里是排查发现测试数据库服务器上3306端口未开放原因。
  • 解决方法:
1
2
3
4
css复制代码sudo vim /etc/sysconfig/iptables
-A INPUT -p tcp -m state --state NEW -m tcp --dport 3306 -j ACCEPT
sudo service iptables restart
sudo iptables -L -n

问题:Navicat 连接MySQL8出现2059错误

  • 原因:mysql8之前的版本中加密规则是mysql_native_password,而在mysql8之后,加密规则是caching_sha2_password
  • 解决办法:更改加密规则
1
2
3
4
5
php复制代码mysql -uroot -ppassword #登录
use mysql; #选择数据库
ALTER USER 'root'@'%' IDENTIFIED BY 'password' PASSWORD EXPIRE NEVER; #更改加密方式
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'password'; #更新用户密码
FLUSH PRIVILEGES; #刷新权限

问题:使用Navicat迁移数据报错 –> [Err] [Dtf] 1426 - Too-big precision 7 specified for ‘TIME_CREATE’. Maximum is 6.

  • 原因:oracle的DATE类型是7位,而mysql的时间类型最多6位,所以无法导入。
  • 解决方法:将oracle库中的DATE改为TIMESTAMP,同时长度改成6 (一定要把类型和长度都修改后才保存),即可导入。

问题:数据迁移中varchar字段插入报错 –> Data too long for column ‘DESIGNER’ at row 1

  • 原因:Oracle与mysql采用不同的编码集,导致即使相同的字符,存储长度要求也不一样(需进一步深入了解)
  • 解决办法:需要在msyql扩展字段长度

问题:SpringBoot连接mysql报错–> Unknown system variable ‘query_cache_size’

  • 原因:使用mysql驱动jar包版本过低,不兼容mysql8.0
  • 解决:使用mysql-connector-java-8.0.15.jar,驱动名换为com.mysql.cj.jdbc.Driver

问题:更改为mysql后,应用前端页面查询中文显示乱码

  • 原因:从数据库、服务器、页面三个维度排查编码格式
  1. MySQL数据库编码格式排查
  2. 服务器编码格式排查
  3. 前端页面编码格式排查
    最终发现,是在Navicat连接中,设置了编码格式为utf-8,导致导入的数据在Navicat中看到是正常的,但数据库中是乱码,查询结果也是乱码。这个真的查了好久,没注意到时工具的原因。。
  • 解决办法:重新设置编码格式,导入数据,显示正常

参考

mysql关键字表

本文转载自: 掘金

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

6种常见分布式唯一ID生成策略及它们的优缺点对比 简单分析一

发表于 2021-05-20

全局唯一的 ID 几乎是所有系统都会遇到的刚需。这个 id 在搜索, 存储数据, 加快检索速度 等等很多方面都有着重要的意义。有多种策略来获取这个全局唯一的id,针对常见的几种场景,我在这里进行简单的总结和对比。

简单分析一下需求

所谓全局唯一的 id 其实往往对应是生成唯一记录标识的业务需求。

这个 id 常常是数据库的主键,数据库上会建立聚集索引(cluster index),即在物理存储上以这个字段排序。这个记录标识上的查询,往往又有分页或者排序的业务需求。所以往往要有一个time字段,并且在time字段上建立普通索引(non-cluster index)。

普通索引存储的是实际记录的指针,其访问效率会比聚集索引慢,如果记录标识在生成时能够基本按照时间有序,则可以省去这个time字段的索引查询。

这就引出了记录标识生成的两大核心需求:

  • 全局唯一
  • 趋势有序

常见生成策略的优缺点对比

方法一: 用数据库的 auto_increment 来生成

优点:

  • 此方法使用数据库原有的功能,所以相对简单
  • 能够保证唯一性
  • 能够保证递增性
  • id 之间的步长是固定且可自定义的

缺点:

  • 可用性难以保证:数据库常见架构是 一主多从 + 读写分离,生成自增ID是写请求 主库挂了就玩不转了
  • 扩展性差,性能有上限:因为写入是单点,数据库主库的写性能决定ID的生成性能上限,并且 难以扩展

改进方案:

  • 冗余主库,避免写入单点
  • 数据水平切分,保证各主库生成的ID不重复

6种常见分布式唯一ID生成策略及它们的优缺点对比

方法一改进方案的结构图

如上图所述,由1个写库变成3个写库,每个写库设置不同的 auto_increment 初始值,以及相同的增长步长,以保证每个数据库生成的ID是不同的(上图中DB 01生成0,3,6,9…,DB 02生成1,4,7,10,DB 03生成2,5,8,11…)

改进后的架构保证了可用性,但缺点是

  • 丧失了ID生成的“绝对递增性”:先访问DB 01生成0,3,再访问DB 02生成1,可能导致在非常短的时间内,ID生成不是绝对递增的(这个问题不大,目标是趋势递增,不是绝对递增
  • 数据库的写压力依然很大,每次生成ID都要访问数据库

为了解决这些问题,引出了以下方法:

方法二:单点批量ID生成服务

分布式系统之所以难,很重要的原因之一是“没有一个全局时钟,难以保证绝对的时序”,要想保证绝对的时序,还是只能使用单点服务,用本地时钟保证“绝对时序”。

数据库写压力大,是因为每次生成ID都访问了数据库,可以使用批量的方式降低数据库写压力。

6种常见分布式唯一ID生成策略及它们的优缺点对比

方法二的结构图

如上图所述,数据库使用双master保证可用性,数据库中只存储当前ID的最大值,例如4。

ID生成服务假设每次批量拉取5个ID,服务访问数据库,将当前ID的最大值修改为4,这样应用访问ID生成服务索要ID,ID生成服务不需要每次访问数据库,就能依次派发0,1,2,3,4这些ID了。

当ID发完后,再将ID的最大值修改为11,就能再次派发6,7,8,9,10,11这些ID了,于是数据库的压力就降低到原来的1/6。

优点:

  • 保证了ID生成的绝对递增有序
  • 大大地降低了数据库的压力,ID生成可以做到每秒生成几万几十万个

缺点:

  • 服务仍然是单点
  • 如果服务挂了,服务重启起来之后,继续生成ID可能会不连续,中间出现空洞(服务内存是保存着0,1,2,3,4,数据库中max-id是4,分配到3时,服务重启了,下次会从5开始分配,3和4就成了空洞,不过这个问题也不大)
  • 虽然每秒可以生成几万几十万个ID,但毕竟还是有性能上限,无法进行水平扩展

改进方案

  • 单点服务的常用高可用优化方案是“备用服务”,也叫“影子服务”,所以我们能用以下方法优化上述缺点:

6种常见分布式唯一ID生成策略及它们的优缺点对比

方法二改进方案的结构图

如上图,对外提供的服务是主服务,有一个影子服务时刻处于备用状态,当主服务挂了的时候影子服务顶上。这个切换的过程对调用方是透明的,可以自动完成,常用的技术是 vip+keepalived。另外,id generate service 也可以进行水平扩展,以解决上述缺点,但会引发一致性问题。

方法三:uuid / guid

不管是通过数据库,还是通过服务来生成ID,业务方Application都需要进行一次远程调用,比较耗时。uuid是一种常见的本地生成ID的方法。

1
ini复制代码UUID uuid = UUID.randomUUID();

优点:

  • 本地生成ID,不需要进行远程调用,时延低
  • 扩展性好,基本可以认为没有性能上限

缺点:

  • 无法保证趋势递增
  • uuid过长,往往用字符串表示,作为主键建立索引查询效率低,常见优化方案为“转化为两个uint64整数存储”或者“折半存储”(折半后不能保证唯一性)

方法四:取当前毫秒数

uuid是一个本地算法,生成性能高,但无法保证趋势递增,且作为字符串ID检索效率低,有没有一种能保证递增的本地算法呢?- 取当前毫秒数是一种常见方案。(搜索公众号Java知音,回复“2021”,送你一份Java面试题宝典)

优点:

  • 本地生成ID,不需要进行远程调用,时延低
  • 生成的ID趋势递增
  • 生成的ID是整数,建立索引后查询效率高

缺点:

  • 如果并发量超过1000,会生成重复的ID
  • 这个缺点要了命了,不能保证ID的唯一性。当然,使用微秒可以降低冲突概率,但每秒最多只能生成1000000个ID,再多的话就一定会冲突了,所以使用微秒并不从根本上解决问题。

方法五:使用 Redis 来生成 id

当使用数据库来生成ID性能不够要求的时候,我们可以尝试使用Redis来生成ID。这主要依赖于Redis是单线程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作 INCR 和 INCRBY 来实现。

(搜索公众号Java知音,回复“2021”,送你一份Java面试题宝典)

优点:

  • 依赖于数据库,灵活方便,且性能优于数据库。
  • 数字ID天然排序,对分页或者需要排序的结果很有帮助。

缺点:

  • 如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。
  • 需要编码和配置的工作量比较大。

方法六:Twitter 开源的 Snowflake 算法

snowflake 是 twitter 开源的分布式ID生成算法,其核心思想为,一个long型的ID:

  • 41 bit 作为毫秒数 - 41位的长度可以使用69年
  • 10 bit 作为机器编号 (5个bit是数据中心,5个bit的机器ID) - 10位的长度最多支持部署1024个节点
  • 12 bit 作为毫秒内序列号 - 12位的计数顺序号支持每个节点每毫秒产生4096个ID序号

6种常见分布式唯一ID生成策略及它们的优缺点对比

Snowflake图示

算法单机每秒内理论上最多可以生成1000*(2^12),也就是400W的ID,完全能满足业务的需求。

该算法 java 版本的实现代码如下:

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
java复制代码package com;

public class SnowflakeIdGenerator {
//================================================Algorithm's Parameter=============================================
// 系统开始时间截 (UTC 2017-06-28 00:00:00)
private final long startTime = 1498608000000L;
// 机器id所占的位数
private final long workerIdBits = 5L;
// 数据标识id所占的位数
private final long dataCenterIdBits = 5L;
// 支持的最大机器id(十进制),结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
// -1L 左移 5位 (worker id 所占位数) 即 5位二进制所能获得的最大十进制数 - 31
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 支持的最大数据标识id - 31
private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits);
// 序列在id中占的位数
private final long sequenceBits = 12L;
// 机器ID 左移位数 - 12 (即末 sequence 所占用的位数)
private final long workerIdMoveBits = sequenceBits;
// 数据标识id 左移位数 - 17(12+5)
private final long dataCenterIdMoveBits = sequenceBits + workerIdBits;
// 时间截向 左移位数 - 22(5+5+12)
private final long timestampMoveBits = sequenceBits + workerIdBits + dataCenterIdBits;
// 生成序列的掩码(12位所对应的最大整数值),这里为4095 (0b111111111111=0xfff=4095)
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
//=================================================Works's Parameter================================================
/**
* 工作机器ID(0~31)
*/
private long workerId;
/**
* 数据中心ID(0~31)
*/
private long dataCenterId;
/**
* 毫秒内序列(0~4095)
*/
private long sequence = 0L;
/**
* 上次生成ID的时间截
*/
private long lastTimestamp = -1L;
//===============================================Constructors=======================================================
/**
* 构造函数
*
* @param workerId 工作ID (0~31)
* @param dataCenterId 数据中心ID (0~31)
*/
public SnowflakeIdGenerator(long workerId, long dataCenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("Worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
throw new IllegalArgumentException(String.format("DataCenter Id can't be greater than %d or less than 0", maxDataCenterId));
}
this.workerId = workerId;
this.dataCenterId = dataCenterId;
}
// ==================================================Methods========================================================
// 线程安全的获得下一个 ID 的方法
public synchronized long nextId() {
long timestamp = currentTime();
//如果当前时间小于上一次ID生成的时间戳: 说明系统时钟回退过 - 这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
//毫秒内序列溢出 即 序列 > 4095
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = blockTillNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - startTime) << timestampMoveBits) //
| (dataCenterId << dataCenterIdMoveBits) //
| (workerId << workerIdMoveBits) //
| sequence;
}
// 阻塞到下一个毫秒 即 直到获得新的时间戳
protected long blockTillNextMillis(long lastTimestamp) {
long timestamp = currentTime();
while (timestamp <= lastTimestamp) {
timestamp = currentTime();
}
return timestamp;
}
// 获得以毫秒为单位的当前时间
protected long currentTime() {
return System.currentTimeMillis();
}
//====================================================Test Case=====================================================
public static void main(String[] args) {
SnowflakeIdGenerator idWorker = new SnowflakeIdGenerator(0, 0);
for (int i = 0; i < 100; i++) {
long id = idWorker.nextId();
//System.out.println(Long.toBinaryString(id));
System.out.println(id);
}
}
}

本文转载自: 掘金

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

springboot项目编写测试用例

发表于 2021-05-20

前期准备

idea默认快捷键ctrl+shift+t 通过Create New Test生成测试用例,如果没有出现再安装插件JUnitGenerator V2.0

maven

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
js复制代码<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>

<!-- 测试代码覆盖率 在父级POM引入-->
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.3</version>
</dependency>
<!-- 测试代码覆盖率 在父级POM引入-->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.3</version>
<configuration>
<includes>
<include>com/**/*</include>
</includes>
</configuration>
<executions>
<execution>
<id>pre-test</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>post-test</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
<!--在启动模块添加-->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.3</version>
<executions>
<execution>
<id>report-aggregate</id>
<phase>verify</phase>
<goals>
<goal>report-aggregate</goal>
</goals>
</execution>
</executions>
</plugin>

编写测试用例

controller端

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
js复制代码import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.context.WebApplicationContext;
····

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@WebAppConfiguration
@ActiveProfiles("dev")
public class ControllerTest {

private static final String URL = "/demo";

@Autowired
private WebApplicationContext context;
private MockMvc mockMvc;

@Before
public void setUp() throws Exception {
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
}


@Test
public void importAll() throws Exception {
//传文件夹路径
String content = mockMvc
.perform(MockMvcRequestBuilders.post(URL + "/importAll").param("file","D://测试文件夹").contentType(MediaType.APPLICATION_JSON_UTF8))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.success").value(true))
.andReturn().getResponse().getContentAsString();
Assert.assertNotNull(content);
}

//上传文件
@Test(expected = FileNotFoundException.class)
public void readExcel() throws Exception {
String uploadFilePath = "D://测试.xlsx";
File uploadFile = new File(uploadFilePath);
String fileName = uploadFile.getName();
MockMultipartFile file = new MockMultipartFile("uploadFile", fileName, MediaType.TEXT_PLAIN_VALUE, new FileInputStream(uploadFile));
String content = mockMvc.perform(MockMvcRequestBuilders.fileUpload(URL + "/readExcel").file(file))
.andDo(print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.failed").value(false))
.andReturn().getResponse().getContentAsString();
Assert.assertNotNull(content);
}
}

@Test
public void find() throws Exception {
ArrayList<Long> longs = new ArrayList<>();
longs.add(1L);
longs.add(2L);
HashMap<Object, Object> map = Maps.newHashMap();
map.put("idList",longs);
String json = JsonUtils.toJsonString(map);
String content = mockMvc
.perform(MockMvcRequestBuilders.post(URL + "/find").content(json).contentType(MediaType.APPLICATION_JSON_UTF8))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.success").value(true))
.andReturn().getResponse().getContentAsString();
Assert.assertNotNull(content);
}

//下载文件
@Test(expected = Exception.class)
public void download() throws Exception {
Long projectId = 1L;
String excelSheetName = "Sheet1";
String filePath = "D://测试.xlsx"; //下载文件路径
mockMvc.perform(MockMvcRequestBuilders.get(URL+"/download/"+projectId))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(result -> {
result.getResponse().setCharacterEncoding("UTF-8");
MockHttpServletResponse contentResponse = result.getResponse();
InputStream contentInStream = new ByteArrayInputStream(
contentResponse.getContentAsByteArray());
XSSFWorkbook resultExcel = new XSSFWorkbook(contentInStream);
//Assert.assertEquals("multipart/form-data", contentResponse.getContentType());
XSSFSheet sheet = resultExcel.getSheet(excelSheetName);
Assert.assertNotNull(sheet);
File file = new File(filePath);
OutputStream out = new FileOutputStream(file);
resultExcel.write(out);
resultExcel.close();
Assert.assertTrue(file.exists());
});
}

@Test
public void userAreaList() throws Exception{
Cookie cookie = new Cookie("www.baidu.com","xxxxxxxxxxxxxxxxxxxxx");
cookie.setPath("/");
cookie.setMaxAge(7);
String content = mockMvc
.perform(MockMvcRequestBuilders.get(URL + "/areaList").cookie(cookie))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.failed").value(true))
.andReturn().getResponse().getContentAsString();
Assert.assertNotNull(content);
}
}

service端

service端Test同controller放在同一主test包下,均在启动model所在test包下

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
js复制代码@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class DemoServiceImplTest {

@MockBean
private DemoMapper demoMapper;
@Resource
private DemoServiceImpl demoImpl;

@Test
public void listByParam() {
DataVo map = new DataVo();
map.setCityName("南昌");
//Mockito需要放在service调用前,模拟dao层返回数据,即new ArrayList<>()相当于原地TP
Mockito.when(demoMapper.listByParam(map)).thenReturn(new ArrayList<>());
List<DemoData> param = demoImpl.listByParam(map);
Assert.assertNotNull(param);
}

@Test
public void listByCityId() {
List<DemoData> param = DemoServiceImpl.listByCityId(1L, 1);
Assert.assertNotNull(param);
}
}

生成覆盖率

执行mvn -test或者mvn verify

本文转载自: 掘金

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

GO语言平均薪资为什么比Java高?

发表于 2021-05-20

Go 语言开发者的薪资也水涨船高。据职友集统计,中国 Go 语言开发工程师的平均月工资为 21.8k 。从全球范围来看,Go 语言的开发者的薪酬也是位列前茅,结合其并不高的声量和使用率,可以说是「闷声赚大钱」了。

你是否有以下这些疑问?

为什么GO语言平均薪资这么高?

我是新手小白选择GO语言合适吗?

我是CRUD程序员,现在转战GO语言能不能拿高薪?

GO语言前景是怎么样的?

我想通过学习GO语言进入字节跳动能行吗?

推荐大家观看这个视频:

Golang100集:带你7天搞定Go语言,附:清华大牛对GO语言前景分析;

本文转载自: 掘金

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

1…664665666…956

开发者博客

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