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

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


  • 首页

  • 归档

  • 搜索

硬核!一文看懂SpringMVC

发表于 2020-06-27

大家好,我是小菜,一个渴望在互联网行业做到蔡不菜的小菜。可柔可刚,点赞则柔,白嫖则刚!

死鬼~看完记得给我来个三连哦!

“
本文主要介绍 SprinMVC
如有需要,可以参考
如有帮助,不忘 点赞 ❥

创作不易,白嫖无义!

一丶SpringMVC概述

  • Spring 为展现层提供的基于 MVC 设计理念的优秀的Web 框架,是目前最主流的 MVC 框架之一
  • Spring3.0 后全面超越 Struts2,成为最优秀的 MVC 框架
  • Spring MVC 通过一套 MVC 注解,让 POJO 成为处理请求的控制器,而无须实现任何接口。
  • 支持 REST 风格的 URL 请求
  • 采用了松散耦合可插拔组件结构,比其他 MVC 框架更具扩展性和灵活性

二丶SpringMVC简单使用

1)在 web.xml 中配置 DispatcherServlet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
xml复制代码<!-- 配置 DispatcherServlet -->  
    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- 配置 DispatcherServlet 的一个初始化参数: 配置 SpringMVC 配置文件的位置和名称 -->
        <!-- 
            实际上也可以不通过 contextConfigLocation 来配置 SpringMVC 的配置文件, 而使用默认的.
            默认的配置文件为: /WEB-INF/<servlet-name>-servlet.xml
        -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

2)加入 Spring MVC 的配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
xml复制代码<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">

    <!-- 配置自定扫描的包 -->
    <context:component-scan base-package="cbuc.life.springmvc"></context:component-scan>

    <!-- 配置视图解析器: 如何把 handler 方法返回值解析为实际的物理视图 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/"></property>
        <property name="suffix" value=".jsp"></property>
    </bean>

</beans>

3)编写处理请求的处理器,并使用@Controller 注解标识为处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Controller  
public class HelloWorldController {
    /**
       1. 使用 @RequestMapping 注解来映射请求的 URL
       2. 返回值会通过视图解析器解析为实际的物理视图, 对于 InternalResourceViewResolver 视图解析器, 会做如下的解析:
          通过 prefix + returnVal + 后缀 这样的方式得到实际的物理视图, 然会做转发操作
          ==> /WEB-INF/views/success.jsp
     */
    @RequestMapping("/helloworld")
    public String hello(){
        System.out.println("hello world");
        return "success";
    }
}

4) 编写视图 JSP

在/WEB-INF/views/目录下创建一个succes.jsp

1
2
3
4
5
6
7
8
9
10
11
12
jsp复制代码<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>  
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
    <h1>成功跳转页面</h1>
</body>
</html>

5)将项目运行起来访问 :localhost:8080/hellowoorld

三丶使用 @RequestMapping 映射请求

  • Spring MVC 使用 @RequestMapping 注解为控制器指定可以处理哪些 URL 请求
  • 在控制器的类定义及方法定义处都可标注
    • 类定义:提供初步的请求映射信息。相对于 WEB 应用的根目录
    • 方法:提供进一步的细分映射信息。相对于类定义处的 URL。若类定义处未标注 @RequestMapping,则方法处标记的 URL 相对于WEB 应用的根目录
  • DispatcherServlet 截获请求后,就通过控制器上@RequestMapping 提供的映射信息确定请求所对应的处理 方法。

1)标准请求头

2)@RequestMapping

@RequestMapping 的value、method、params 及 heads 分别表示请求 URL、请求方法、请求参数及请求头的映射条件,他们之间是与的关系,联合使用多个条件可让请求映射更加精确化。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码/**  
     * 可以使用 params 和 headers 来更加精确的映射请求. params 和 headers 支持简单的表达式.
     * 
     * @return
     */
    @RequestMapping(value = "testParamsAndHeaders",
                    params = { "username","age!=10" },
                    headers = { "Accept-Language=en-US,zh;q=0.8" },
                    method = RequestMethod.POST)
    public String test() {
        System.out.println("test...");
        return "success";
    }

3)支持Ant 风格

  • ? :匹配文件名中的一个字符

/user/createUser?

匹配 /user/createUsera 或者 user/createUserb 等 URL

  • * :匹配文件名中的任意字符

/user/*/createUser

匹配 /user/aaa/createUser 或者 /user/bbb/createUser 等 URL

  • ** :匹配多层路径

/user/**/createUser

匹配 /user/createUser 或者 /user/aaa/bbb/createUser 等 URL

四丶@PathVariable

映射 URL 绑定的占位符

  • 带占位符的 URL 是 Spring3.0 新增的功能,该功能在 SpringMVC 向 REST 目标挺进发展过程中具有里程碑的意义
  • 通过@PathVariable可以将 URL 中占位符参数绑定到控制器处理方法的入参中:URL 中的 {xxx} 占位符可以通过@PathVariable("xxx") 绑定到操作方法的入参中。
1
2
3
4
5
6
7
8
java复制代码/**  
 * @PathVariable 可以来映射 URL 中的占位符到目标方法的参数中.
 */
@RequestMapping("/testPathVariable/{id}")
public String test(@PathVariable("id") Integer id) {
    System.out.println("id: " + id);
    return "success";
}

五丶REST风格

“
REST:即 Representational State Transfer。(资源)表现层状态转化。是目前最流行的一种互联网软件架构。它结构清晰、符合标准、易于理解、扩展方便, 所以正得到越来越多网站的采用

示例:

  • /order/1 HTTP GET :得到 id = 1 的 order 记录
  • /order/1 HTTP DELETE:删除 id = 1的 order 记录
  • /order/1 HTTP PUT:更新 id = 1的 order 记录
  • /order HTTP POST:新增 一条order记录

六丶@RequestParam 绑定请求参数值

  • 在处理方法入参处使用 @RequestParam 可以把请求参数传递给请求方法
  • value:参数名
  • required:是否必须;默认为 true,表示请求参数中必须包含对应的参数,若不存在,将抛出异常
1
2
3
4
5
6
7
8
9
10
11
java复制代码/**  
 * @RequestParam 来映射请求参数. value 值即请求参数的参数名 required 该参数是否必须. 默认为 true
 *               defaultValue 请求参数的默认值
 */
@RequestMapping(value = "/testRequestParam")
public String testRequestParam(
        @RequestParam(value = "username") String username,
        @RequestParam(value = "age", required = false, defaultValue = "0") int age) {
    System.out.println("testRequestParam, username: " + username + ", age: " + age);
    return "success";
}

七丶@RequestHeader 绑定请求报头的属性值

1
2
3
4
5
6
7
8
9
java复制代码/**  
 *   映射请求头信息 用法同 @RequestParam
 */
@RequestMapping("/testRequestHeader")
public String testRequestHeader(
        @RequestHeader(value = "Accept-Language") String al) {
    System.out.println("testRequestHeader, Accept-Language: " + al);
    return "success";
}

八丶@CookieValue 绑定请求中的 Cookie 值

1
2
3
4
5
6
7
8
java复制代码/**  
 * @CookieValue: 映射一个 Cookie 值. 属性同 @RequestParam
 */
@RequestMapping("/testCookieValue")
public String testCookieValue(@CookieValue("JSESSIONID") String sessionId) {
    System.out.println("testCookieValue: sessionId: " + sessionId);
    return "success";
}

九丶POJO 对象绑定请求参数值

1
2
3
4
5
6
7
8
9
java复制代码/**  
 * Spring MVC 会按请求参数名和 POJO 属性名进行自动匹配, 自动为该对象填充属性值。支持级联属性。
 * 如:dept.deptId、dept.address.tel 等
 */
@RequestMapping("/testPojo")
public String testPojo(User user) {
    System.out.println("testPojo: " + user);
    return "success";
}

十丶MVC 中Handler 方法可以接收的ServletAPI 类型的参数

  • HttpServletRequest
  • HttpServletResponse
  • HttpSession
  • Writer
  • java.security.Principal
  • Locale
  • InputStream
  • OutputStream
  • Reader

十一丶处理模型数据

1)ModelAndView

处理方法返回值类型为 ModelAndView时,方法体可通过该对象添加模型数据,ModelAndView中既包含视图信息,也包含模型数据信息。

2)Map 及 Model

入参为 org.springframework.ui.Model、org.springframework.ui.ModelMap 或 java.uti.Map 时,处理方法返回时,Map 中的数据会自动添加到模型中。

3)@SessionAttributes:

将模型中的某个属性暂存到HttpSession中,以便多个请求之间可以共享这个属性(从session域中获取)

  • 若希望在多个请求之间共用某个模型属性数据,则可以在 控制器类上标注一个 @SessionAttributes,Spring MVC 将在模型中对应的属性暂存到 HttpSession 中。
  • @SessionAttributes除了可以通过属性名指定需要放到会话中的属性外,还可以通过模型属性的对象类型指定哪些模型属性需要放到会话中

1)@SessionAttributes(types=User.class): 会将隐含模型中所有类型为 User.class 的属性添加到会话中

2)@SessionAttributes(value={“user1”, “user2”}):会将隐含模型中对象名为user1,user2 的属性添加到会话中

3)@SessionAttributes(types={User.class, Dept.class}):会将隐含模型中所有类型为 User.class,Dept.class 的属性添加到会话中

4)@SessionAttributes(value={“user1”, “user2”}, types={Dept.class}):会将隐含模型中对象名为user1,user2 的属性和所有类型为 Dept.class 的属性添加到会话中

4)@ModelAttribute

方法入参标注该注解后, 入参的对象就会放到数据模型中

十二丶@ModelAttribute

  • 在方法定义上使用 @ModelAttribute 注解:Spring MVC在调用目标处理方法前,会先逐个调用在方法级上标注了@ModelAttribute 的方法。
  • 在方法的入参前使用 @ModelAttribute 注解:
  • 可以从隐含对象中获取隐含的模型数据中获取对象,再将请求参数绑定到对象中,再传入入参
  • 将方法入参对象添加到模型中
示例:

十三丶视图和视图解析器

  • 请求处理方法执行完成后,最终返回一个 ModelAndView 对象。对于那些返回 String,View 或 ModeMap 等类型的处理方法,Spring MVC 也会在内部将它们装配成一个 ModelAndView 对象,它包含了逻辑名和模型对象的视图。
  • Spring MVC 借助视图解析器(ViewResolver)得到最终的视图对象(View),最终的视图可以是 JSP,也可能是 Excel、JFreeChart等各种表现形式的视图。
  • 对于最终究竟采取何种视图对象对模型数据进行渲染,处理器并不关心,处理器工作重点聚焦在生产模型数据的工 作上,从而实现 MVC 的充分解耦。

1)视图

我们只需要实现View这个接口就可以自定义视图

示例:
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Component  
public class HelloView implements View{
    @Override
    public String getContentType() {
        return "text/html";
    }
    @Override
    public void render(Map<String, ?> model, HttpServletRequest request,
            HttpServletResponse response) throws Exception {
        response.getWriter().print("hello view, time: " + new Date());
    }
}
1
2
3
4
5
java复制代码@RequestMapping("/testView")  
    public String testView(){
        System.out.println("testView");
        return "helloView"; //这里返回的就是我们自定义的视图
    }

2)视图解析器

  • SpringMVC 为逻辑视图名的解析提供了不同的策略,可以在 Spring WEB 上下文中配置一种或多种解析策略,并指定他们之间的先后顺序。每一种映射策略对应一个具体的视图解析器实现类。
  • 视图解析器的作用比较单一,将逻辑视图解析为一个具体的视图对象。
  • 所有的视图解析器都必须实现 ViewResolver 接口。
  • 程序员可以选择一种视图解析器或混用多种视图解析器。
  • 每个视图解析器都实现了Ordered接口并开放出一个 order 属性,可 以通过order 属性指定解析器的优先顺序,order 越小优先级越高。
  • SpringMVC 会按视图解析器顺序的优先顺序对逻辑视图名进行解析,直到解析成功并返回视图对象,否则将抛出 ServletException 异常

SpringMVC.xml中的配置:

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<!-- 配置视图解析器: 如何把 handler 方法返回值解析为实际的物理视图 -->  
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/views/"></property>
    <property name="suffix" value=".jsp"></property>
</bean>

<!-- 配置视图  BeanNameViewResolver 解析器: 使用视图的名字来解析视图 -->
<!-- 通过 order 属性来定义视图解析器的优先级, order 值越小优先级越高 -->
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
    <property name="order" value="100"></property>
</bean>

看完不赞,都是坏蛋

看完不赞,都是坏蛋

“
今天的你多努力一点,明天的你就能少说一句求人的话!

我是小菜,一个和你一起学习的男人。 💋

本文转载自: 掘金

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

消息疯狂堆积!RocketMQ出Bug了?

发表于 2020-06-26

前言

用过 MQ 的同学,可能会遇到过消息堆积的问题。而肥壕最近也踩上了这个坑,但是发现结果竟然是这么一个意料之外的原因而导致的。

正文

那一晚月黑风高,肥壕正准备踏上回家的路,突然收到告警短信轰炸!“MQ 消息堆积告警 [TOPIC: XXX] ”

肥壕心里“万只草泥马崩腾~” 第一反应是:“怎么肥事?刚下班就来搞事情???”

于是乎赶回公司赶紧打开电脑,登上 RocketMQ 后台查看(公司自己搭建的开源版RocketMQ)

握草 (キ`゚Д゚´)!!! 竟然堆积了3亿多条消息了???

要知道出现消息堆积无在乎这个问题:

生产者的生产速度 >> 消费者的处理速度

  1. 生产者的生产速度骤增,比如生产者的流量突然骤增
  2. 消费速度变慢,比如消费者实例 IO 阻塞严重或者宕机

擦了一下头上的冷汗😓…赶紧登上消费者服务器瞧瞧。

应用运行正常!服务器磁盘IO 正常!网络正常!

再去上去生产者的服务器,咦…流量也很正常!

什么???佛了😨 …生产者和消费者的应用都很正常,但是为什么消息会堆积怎么多呢?看着这堆积的数量越堆越多(要是这是我头发的数量那该多好啊),越发着急。

虽然说 RocketMQ 版能支持 10 亿级别的消息堆积,不会因为消息堆积导致性能明显下降,😰但是这堆积量很明显就是一个异常情况。

RocketMQ 有 BUG,没错这肯定是 RocketMQ 的锅!

本篇完…

哈哈言归正传,虽然肥壕拼爹不行,但至少不能坑爹😂

进入消费者的工程查看一下日志,emmm…没有发现报错,没有错误日志…看起来好像一切都很正常。

咦…不过这个消费的速度是不是有点慢???这不科学啊,消费者可是配置了3个结点的消费集群啊,按业务的需求量来说消费能力可是杠杠的呀。我再点开这个 TOPIC 的消费者信息

咦,这三个消费者的 ClientId 怎么会是一样呀?

以多年采坑经验的直接告诉我 “难道是因为 ClientId 的相同的问题,导致 broker 在分发消息的时候出现混乱,从而导致消息不能正常推送给消费者?” 因为生产者和消费者都表现正常,所以我猜测问题可能在于 Broker 这一块上。

基于这个推测,那么我们就需要解决这几个问题:

  1. 部署在不同的服务器上的两个消费者,为什么 ClientId 是相同的呢?
  2. ClientId 相同,会导致 broker 消息分发错误吗?

问题分析

为什么 ClientId 相同呢?我推测是因为 Docker 容器的问题。因为公司最近开始容器化阶段,而刚好消费者的项目也在第一批容器化阶段的列表上。

有了解过 Docker 的小伙伴都知道,当 Docker 进程启动时,会在主机上创建一个名为docker0的虚拟网桥。宿主机上的 Docker 容器会连接到这个虚拟网桥上。虚拟网桥的工作方式和物理交换机类似,这样主机上的所有容器就通过交换机连在了一个二层网络中。而 Docker 的网络模式一般有四种:

  • Host 模式
  • Container 模式
  • None 模式
  • Bridge 模式

对这几个模式不清楚的同学自行找度娘🤔

我们容器都是采用 Host 模式,所以容器的网络跟宿主机是完全一致的。

可以看到,这里第一个就是docker0网卡,默认的 ip 都是172.17.0.1。所以显而易见,ClientId 应该读取的都是docker0网卡的 IP,这就是能解释为什么多个消费端的 ClientId 都一致的问题了。

那么接下来就是 clientId 的究竟是在哪里设置呢?机智的我在 Github 的 Issues 搜索关键词 “Docker”,啪啦啪啦一搜,果然!还是有不少踩过次坑的志同道合之士,筛选了一番,找到一个比较靠谱的 open issue

可以看到,这个兄弟跟我的遇到的情况是一毛一样的,而他的结论跟我上面的推测也是大致相同(此时内心洋洋得意一番),他这里还提到 clientId 是在 ClientConfig 类中 buildMQClientId 方法中定义的。

源码探索

进入 ClientConfig 类,定位到 buildMQClientId 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public String buildMQClientId() {
StringBuilder sb = new StringBuilder();
sb.append(this.getClientIP());

sb.append("@");
sb.append(this.getInstanceName());
if (!UtilAll.isBlank(this.unitName)) {
sb.append("@");
sb.append(this.unitName);
}

return sb.toString();
}

通过这个相信大家都可以看出 clientId 的生成规则吧,就是 消费者客户端的IP + "@"+ 实例名称 ,很明显问题就出在获取客户端 IP 上。

我们再继续看一下它究竟是如何获取客户端 IP 的

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
java复制代码public class ClientConfig {
...
private String clientIP = RemotingUtil.getLocalAddress();
...
}

public static String getLocalAddress() {
try {
// Traversal Network interface to get the first non-loopback and non-private address
Enumeration<NetworkInterface> enumeration = NetworkInterface.getNetworkInterfaces();
ArrayList<String> ipv4Result = new ArrayList<String>();
ArrayList<String> ipv6Result = new ArrayList<String>();
while (enumeration.hasMoreElements()) {
final NetworkInterface networkInterface = enumeration.nextElement();
final Enumeration<InetAddress> en = networkInterface.getInetAddresses();
while (en.hasMoreElements()) {
final InetAddress address = en.nextElement();
if (!address.isLoopbackAddress()) {
if (address instanceof Inet6Address) {
ipv6Result.add(normalizeHostAddress(address));
} else {
ipv4Result.add(normalizeHostAddress(address));
}
}
}
}

// prefer ipv4
if (!ipv4Result.isEmpty()) {
for (String ip : ipv4Result) {
if (ip.startsWith("127.0") || ip.startsWith("192.168")) {
continue;
}

return ip;
}

return ipv4Result.get(ipv4Result.size() - 1);
} else if (!ipv6Result.isEmpty()) {
return ipv6Result.get(0);
}
//If failed to find,fall back to localhost
final InetAddress localHost = InetAddress.getLocalHost();
return normalizeHostAddress(localHost);
} catch (Exception e) {
log.error("Failed to obtain local address", e);
}

return null;
}

如果有操作过获取当前机器的 IP 的小伙伴,应该对RemotingUtil.getLocalAddress() 这个工具方法并不陌生~

简单说就是获取当前机器网卡 IP,但是由于容器的网络模式采用的是 host 模式,也就意味着各个容器和宿主机都是处于同一个网络下,所以容器中我们也可以看到 Docker - Server 所创建的docker 0网卡,所以它读取的也就是 docker 0网卡所默认的 IP 地址 172.17.0.1

(跟运维同学沟通了一下,目前由于是容器化的第一阶段,所以先采用简单模式部署,后面会慢慢替换成 k8s,每个 pod 都有自己的独立 IP ,到时网络会与宿主机和其他 pod 的相互隔离。emmm….k8s !听起来牛逼哄哄,恰好最近也在看这方面的书)

**这时候聪明的你可能会问 “不是还有一个实例名称的参数呢,这个又怎么会相同呢?” ** 别着急,我们继续往下看👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT");

public String getInstanceName() {
return instanceName;
}

public void setInstanceName(String instanceName) {
this.instanceName = instanceName;
}

public void changeInstanceNameToPID() {
if (this.instanceName.equals("DEFAULT")) {
this.instanceName = String.valueOf(UtilAll.getPid());
}
}

getInstanceName() 方法其实直接获取 instanceName这个参数值,但是这个参数值是什么时候赋值进去的呢?没错就是通过changeInstanceNameToPID()这个方法赋值的,在 consumer 在 start 的时候会调用此方法。

这个参数的逻辑很简单,在初始化的时候首先会获取环境变量rocketmq.client.name是否有值,如果没有就是用默认值DEFAULT。

然后 consumer 启动的时候会判断这参数值是否为DEFAULT,如果是的话就调用 UtilAll.getPid()。

1
2
3
4
5
6
7
8
9
java复制代码public static int getPid() {
RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();
String name = runtime.getName(); // format: "pid@hostname"
try {
return Integer.parseInt(name.substring(0, name.indexOf('@')));
} catch (Exception e) {
return -1;
}
}

通过方法名字我们就可以很清楚知道,这个方法其实获取进程号的。那…为什么获取的进程号都是一致的呢?

聪明的你可以已经知道答案了对吧🤨 !这里就不得不提 Docker 的 三大特性

  • cgroup
  • namespace
  • unionFS

没错,这里用的就是 namespace 技术啦。

Linux Namespace 是 Linux 内核提供的一个功能,可以实现系统资源的隔离,如:PID、User ID、Network 等。

由于都是使用相同的基础镜像,在最外层都是运行同样的 JAVA 工程,所以我们可以进去容器里面看,他们的进程号都是为 9

经过肥壕的一系列巧妙的推理和论证,在 Docker 容器 HOST 网络模式下, 会生成相同的 clientId !

到这里为止,我们算是解决了上文推测的第一个问题!

紧跟柯南肥壕的步伐,我们继续推理第二个问题: clientId 相同导致 Broker 分发消息错误?

Consumer 在负载均衡的时候应该是根据 clientId 作为客户端消费者的唯一标识,在消息下发的时候由于 clientId 的一致,导致负载分发错误。

那么我们下面就要去探究一下 Consumer 的负载均衡究竟是如何实现的。一开始我以为消费端的负载均衡都是在 Broker 处理的,由Broker 根据注册的 Consumer 把不同的 Queue 分配给不同的 Consumer。但是去看了一下源码上的 doc 描述文档和对源码进行一番的研究后,结果发现自己见识还是太少了(哈哈哈,应该有小伙伴跟我开始的想法是一样的吧)

先来补充一下 RocketMQ 的整体架构

image1.png

由于篇幅问题,这里我只讲解一下 Broker 和 consumer 之间的关系,其他的角色如果有不懂的可以看一下我之前写的 RocketMQ 介绍篇的文章

  1. Consumer 与 NameServer 集群中的其中一个节点(随机选择)建立长连接,定期从 NameServer 获取 Topic 路由信息。
  2. 根据获取 Topic 路由信息 与 Broker 建立长连接,且定时向 Broker 发送心跳。

Broker 接收心跳消息的时候,会把 Consumer 的信息保存到本地缓存变量 consumerTable。上图大致讲解了一下 consumerTable 的存储结构和内容,最主要的是它缓存了每个 consumer 的 clientId。

关于 Consumer 的消费模式,我直接引用源码的解释

在 RocketMQ 中,Consumer 端的两种消费模式(Push/Pull)都是基于拉模式来获取消息的,而在 Push 模式只是对 Pull 模式的一种封装,其本质实现为消息拉取线程在从服务器拉取到一批消息后,然后提交到消息消费线程池后,又“马不停蹄”的继续向服务器再次尝试拉取消息。如果未拉取到消息,则延迟一下又继续拉取。

在两种基于拉模式的消费方式(Push/Pull)中,均需要 Consumer 端在知道从 Broker 端的哪一个消息队列—队列中去获取消息。因此,有必要在 Consumer 端来做负载均衡,即 Broker 端中多个 MessageQueue 分配给同一个ConsumerGroup 中的哪些 Consumer 消费。

所以简单来说,不管是 Push 还是 Pull 模式,消息消费的控制权在 Consumer 上,所以 Consumer 的负载均衡实现是在 Consumer 的 Client 端上。

通过查看源码可以发现, RebalanceService 会完成负载均衡服务线程(每隔20s执行一次),RebalanceService 线程的run() 方法最终调用的是 RebalanceImpl 类的 rebalanceByTopic()方法,该方法是实现 Consumer 端负载均衡的核心。这里,rebalanceByTopic()方法会根据消费者通信类型为“广播模式”还是“集群模式”做不同的逻辑处理。这里主要来看下集群模式下的主要处理流程:

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
java复制代码private void rebalanceByTopic(final String topic, final boolean isOrder) {
switch (messageModel) {
case BROADCASTING: {
..... // 省略
}
case CLUSTERING: {
// 获取该Topic主题下的消息消费队列集合
Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
// 向 broker 获取消费者的clientId
List<String> cidAll = this.mQClientFactory.findConsumerIdList(topic, consumerGroup);
if (null == mqSet) {
if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
log.warn("doRebalance, {}, but the topic[{}] not exist.", consumerGroup, topic);
}
}

if (null == cidAll) {
log.warn("doRebalance, {} {}, get consumer id list failed", consumerGroup, topic);
}

if (mqSet != null && cidAll != null) {
List<MessageQueue> mqAll = new ArrayList<MessageQueue>();
mqAll.addAll(mqSet);

Collections.sort(mqAll);
Collections.sort(cidAll);
// 默认平均分配算法
AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy;

List<MessageQueue> allocateResult = null;
try {
allocateResult = strategy.allocate(
this.consumerGroup,
this.mQClientFactory.getClientId(),
mqAll,
cidAll);
} catch (Throwable e) {
log.error("AllocateMessageQueueStrategy.allocate Exception. allocateMessageQueueStrategyName={}", strategy.getName(),
e);
return;
}

Set<MessageQueue> allocateResultSet = new HashSet<MessageQueue>();
if (allocateResult != null) {
allocateResultSet.addAll(allocateResult);
}

boolean changed = this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder);
if (changed) {
log.info(
"rebalanced result changed. allocateMessageQueueStrategyName={}, group={}, topic={}, clientId={}, mqAllSize={}, cidAllSize={}, rebalanceResultSize={}, rebalanceResultSet={}",
strategy.getName(), consumerGroup, topic, this.mQClientFactory.getClientId(), mqSet.size(), cidAll.size(),
allocateResultSet.size(), allocateResultSet);
this.messageQueueChanged(topic, mqSet, allocateResultSet);
}
}
break;
}
default:
break;
}
}

(1) 从本地缓存变量 topicSubscribeInfoTable 中,获取该Topic主题下的消息消费队列集合(mqSet);

(2) 根据 topic 和 consumerGroup 为参数调用findConsumerIdList()方法向 Broker 端发送获取该消费组下 clientId 列表;

(3) 先对 Topic 下的消息消费队列、消费者Id排序,然后用消息队列分配策略算法(默认为:消息队列的平均分配算法),计算出待拉取的消息队列。这里的平均分配算法,类似于分页的算法,将所有 MessageQueue 排好序类似于记录,将所有消费端 Consumer 排好序类似页数,并求出每一页需要包含的平均 size 和每个页面记录的范围 range,最后遍历整个range 而计算出当前 Consumer 端应该分配到的记录(这里即为:MessageQueue)。

(4) 然后,调用updateProcessQueueTableInRebalance()方法,具体的做法是,先将分配到的消息队列集合(mqSet)与processQueueTable做一个过滤比对。

  • 上图中 processQueueTable 标注的红色部分,表示与分配到的消息队列集合 mqSet 互不包含。将这些队列设置Dropped 属性为 true,然后查看这些队列是否可以移除出 processQueueTable 缓存变量,这里具体执行removeUnnecessaryMessageQueue()方法,即每隔1s 查看是否可以获取当前消费处理队列的锁,拿到的话返回true。如果等待1s后,仍然拿不到当前消费处理队列的锁则返回false。如果返回true,则从 processQueueTable 缓存变量中移除对应的 Entry;
  • 上图中 processQueueTable 的绿色部分,表示与分配到的消息队列集合 mqSet 的交集。判断该 ProcessQueue 是否已经过期了,在Pull模式的不用管,如果是 Push 模式的,设置 Dropped 属性为 true,并且调用removeUnnecessaryMessageQueue()方法,像上面一样尝试移除 Entry;

消息消费队列在同一消费组不同消费者之间的负载均衡,其核心设计理念是在一个消息消费队列在同一时间只允许被同一消费组内的一个消费者消费,一个消息消费者能同时消费多个消息队列。

上面这部分内容是摘自RocketMQ 源码中 docs的文档,不知道你们看懂了没,反正我是看了好几遍才理解了🤔🤔🤔

其实看步骤3的图,负载均衡的实现原来也就一目了然了,简单说就是给不同的消费者分配数量相同的消费队列。而消费者都会生成 clientId 的唯一标识,但是根据我们上文的推理,在容器中并且是Host网络模式下会生成一致的 clientId。

Emmmm….到这里,想必大家都能猜到究竟是哪里出问题了吧。

没错!问题应该就出在步骤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
java复制代码@Override
public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll, List<String> cidAll) {
if (currentCID == null || currentCID.length() < 1) {
throw new IllegalArgumentException("currentCID is empty");
}
if (mqAll == null || mqAll.isEmpty()) {
throw new IllegalArgumentException("mqAll is null or mqAll empty");
}
if (cidAll == null || cidAll.isEmpty()) {
throw new IllegalArgumentException("cidAll is null or cidAll empty");
}

List<MessageQueue> result = new ArrayList<MessageQueue>();
if (!cidAll.contains(currentCID)) {
log.info("[BUG] ConsumerGroup: {} The consumerId: {} not in cidAll: {}",
consumerGroup,
currentCID,
cidAll);
return result;
}
// 当前clientId所在的下标
int index = cidAll.indexOf(currentCID);
int mod = mqAll.size() % cidAll.size();
int averageSize =
mqAll.size() <= cidAll.size() ? 1 : (mod > 0 && index < mod ? mqAll.size() / cidAll.size()
+ 1 : mqAll.size() / cidAll.size());
int startIndex = (mod > 0 && index < mod) ? index * averageSize : index * averageSize + mod;
int range = Math.min(averageSize, mqAll.size() - startIndex);
for (int i = 0; i < range; i++) {
result.add(mqAll.get((startIndex + i) % mqAll.size()));
}
return result;
}

上面的计算可以看起来有点绕,但是其实看懂了之后,说白就是计算当前 Consumer 所分配的消息队列,就好比上图步骤3中的图示

假设当前只有一个 consumer ,那我们的消费其实是完全正常的,因为当前 Topic 下所有的队列都会分配给当前的 consumer ,也不存在负载均衡的问题。

假设当前有两个 consumer,按照正常的计算方式结果应该是这样子的。但是因为cidAll是两个重复的 clientId,所以两个 consumer 获得的 index 都是0,自然他们分配的都是相同的 MessageQueue。这就能解释开头为什么能看到是有消费的日志,但是消费速度非常慢的原因了。

解决方法

  1. 解决负载均衡错误

罪魁祸首:clientId

经过一翻精彩的推论,大家应该知道导致 Consumer 负载均衡错误的根本原因就是Consumer 客户端生成的 clientId 一致,所以解决这个问题重点就是在于修改 clientId 的生成规则。上面简单地从源码分析了一下 clientId 的生成规则 ,我们可以通过手动设置 rocketmq.client.name 这个环境变量,生成自定义唯一的 clientId 。

肥壕这里在原来的 pid 后再加上了时间戳:

1
2
3
4
java复制代码@PostConstruct
public void init() {
System.setProperty("rocketmq.client.name", String.valueOf(UtilAll.getPid()) + "@" + System.currentTimeMillis());
}
  1. 解决消息堆积

终于解决了根本问题了!行吧,万事俱备只差上线,队列里头堆积的3亿多条消息还在等着消费呢。

(可谓是一时堆积一时爽,一直堆积一直爽😭)

刚上线了不久,emmm…效果显著,堆积的消息数量逐渐减少了。但是另外一个告警来了,mongodb 告警了!

握草。。。我差点忘记了,消费者对消息业务处理后后会写入mongodb,现在消费的流量入口突然骤增,mongodb反倒扛不住了。不过还好历史的消息不重要,是可以丢失的。于是肥壕果断去后台重置了一下消费点位,妥了现在消费正常了,mongodb也正常了。呼~有惊无险,差点又酿造了另外一起事故。

总结

  1. RocketMQ 的 consumer 客户端都会生成 clientId 唯一标识,clientId 的生成规则是客户端IP+客户端进程号
  2. Docker 容器部署如果网络模式使用 Host 模式,容器中的应用都会获取 Docker 网桥的默认IP
  3. RocketMQ 的 consumer 端负载均衡是在客户端实现的,consumer 客户端会缓存对应的 Topic 消费队列,默认采用消息队列的平均分配算法,如果 clientId 相同那么所有的客户端都会分配到相同的队列,导致消费异常。
  4. 对于消息堆积的处理,要做好全面的检查。不能被瞬间大流量的消费入口而影响其他业务,不然就像肥壕一样搞出另一起事故了(大家如果有更好的消息堆积处理方案欢迎留言提议)

这次是肥壕第一次写有关线上事故的文章,可能很多地方或者细节上比较粗糙,望各位广大猿友多体谅多提建议哈~

经历过的线上事故其实还不少,但是每次总结都是流于形式,希望从今以后能用文章的形式整理出来,一是有助于自己日后的总结复盘,也能给大家提供更多的采坑经验。

普通的改变,将改变普通

我是宅小年,一个在互联网低调前行的小青年

关注公众号「宅小年」,个人博客 📖 edisonz.cn,阅读更多分享文章

zhaixiaonian-code.png

本文转载自: 掘金

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

面试:在面试中关于List(ArrayList、Linked

发表于 2020-06-26

前言

在一开始基础面的时候,很多面试官可能会问List集合一些基础知识,比如:

  • ArrayList默认大小是多少,是如何扩容的?
  • ArrayList和LinkedList的底层数据结构是什么?
  • ArrayList和LinkedList的区别?分别用在什么场景?
  • 为什么说ArrayList查询快而增删慢?
  • Arrays.asList方法后的List可以扩容吗?
  • modCount在非线程安全集合中的作用?
  • ArrayList和LinkedList的区别、优缺点以及应用场景

ArrayList(1.8)

ArrayList是由动态再分配的Object[]数组作为底层结构,可设置null值,是非线程安全的。

ArrayList成员属性

1
2
3
4
5
6
7
8
9
10
11
复制代码//默认的空的数组,在构造方法初始化一个空数组的时候使用
private static final Object[] EMPTY_ELEMENTDATA = {};

//使用默认size大小的空数组实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

//ArrayList底层存储数据就是通过数组的形式,ArrayList长度就是数组的长度。
transient Object[] elementData;

//arrayList的大小
private int size;

那么ArrayList底层数据结构是什么呢?

很明显,使用动态再分配的Object[]数组作为ArrayList底层数据结构了,既然是使用数组实现的,那么数组特点就能说明为什么ArrayList查询快而增删慢?

因为数组是根据下标查询不需要比较,查询方式为:首地址+(元素长度*下标),基于这个位置读取相应的字节数就可以了,所以非常快;但是增删会带来元素的移动,增加数据会向后移动,删除数据会向前移动,导致其效率比较低。

ArrayList的构造方法

  • 带有初始化容量的构造方法
  • 无参构造方法
  • 参数为Collection类型的构造器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码//带有初始化容量的构造方法
public ArrayList(int initialCapacity) {
//参数大于0,elementData初始化为initialCapacity大小的数组
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
//参数小于0,elementData初始化为空数组
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
//参数小于0,抛出异常
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}

//无参构造方法
public ArrayList() {
//在1.7以后的版本,先构造方法中将elementData初始化为空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA
//当调用add方法添加第一个元素的时候,会进行扩容,扩容至大小为DEFAULT_CAPACITY=10
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

那么ArrayList默认大小是多少?

从无参构造方法中可以看出,一开始默认为一个空的实例elementData为上面的DEFAULTCAPACITY_EMPTY_ELEMENTDATA,当添加第一个元素的时候会进行扩容,扩容大小就是上面的默认容量DEFAULT_CAPACITY为10

ArrayList的Add方法

  • boolean add(E):默认直接在末尾添加元素
  • void add(int,E):在特定位置添加元素,也就是插入元素
  • boolean addAll(Collection<? extends E> c):添加集合
  • boolean addAll(int index, Collection<? extends E> c):在指定位置后添加集合
boolean add(E)
1
2
3
4
5
复制代码public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

通过ensureCapacityInternal方法为确定容量大小方法。在添加元素之前需要确定数组是否能容纳下,size是数组中元素个数,添加一个元素size+1。然后再数组末尾添加元素。

其中,ensureCapacityInternal方法包含了ArrayList扩容机制grow方法,当前容量无法容纳下数据时1.5倍扩容,进行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
复制代码private void ensureCapacityInternal(int minCapacity) {
//判断当前的数组是否为默认设置的空数据,是否取出最小容量
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//包括扩容机制grow方法
ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
//记录着集合的修改次数,也就每次add或者remove它的值都会加1
modCount++;

//当前容量容纳不下数据时(下标超过时),ArrayList扩容机制:扩容原来的1.5倍
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//ArrayList扩容机制:扩容原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}

ArrayList是如何扩容的?

根据当前的容量容纳不下新增数据时,ArrayList会调用grow进行扩容:

1
2
复制代码//相当于int newCapacity = oldCapacity + oldCapacity/2
int newCapacity = oldCapacity + (oldCapacity >> 1);

扩容原来的1.5倍。

void add(int,E)
1
2
3
4
5
6
7
8
9
10
复制代码public void add(int index, E element) {
//检查index也就是插入的位置是否合理,是否存在数组越界
rangeCheckForAdd(index);
//机制和boolean add(E)方法一样
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}

ArrayList的删除方法

  • **remove(int):**通过删除指定位置上的元素,
  • remove(Object):根据元素进行删除,
  • **clear():**将elementData中每个元素都赋值为null,等待垃圾回收将这个给回收掉,
  • **removeAll(collection c):**批量删除。
remove(int)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码public E remove(int index) {
//检查下标是否超出数组长度,造成数组越界
rangeCheck(index);

modCount++;
E oldValue = elementData(index);
//算出数组需要移动的元素数量
int numMoved = size - index - 1;
if (numMoved > 0)
//数组数据迁移,这样会导致删除数据时,效率会慢
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//将--size上的位置赋值为null,让gc(垃圾回收机制)更快的回收它。
elementData[--size] = null; // clear to let GC do its work
//返回删除的元素
return oldValue;
}

为什么说ArrayList删除元素效率低?

因为删除数据需要将数据后面的元素数据迁移到新增位置的后面,这样导致性能下降很多,效率低。

remove(Object)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码public boolean remove(Object o) {
//如果需要删除数据为null时,会让数据重新排序,将null数据迁移到数组尾端
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
//删除数据,并迁移数据
fastRemove(index);
return true;
}
} else {
//循环删除数组中object对象的值,也需要数据迁移
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}

可以看出,arrayList是可以存放null值。


LinkedList(1.8)

LinkedList是一个继承于AbstractSequentialList的双向链表。它也可以被当做堆栈、队列或双端队列进行使用,而且LinkedList也为非线程安全, jdk1.6使用的是一个带有 header节头结点的双向循环链表, 头结点不存储实际数据 ,在1.6之后,就变更使用两个节点first、last指向首尾节点。

LinkedList的主要属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码//链表节点的个数 
transient int size = 0;
//链表首节点
transient Node<E> first;
//链表尾节点
transient Node<E> last;
//Node节点内部类定义
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;

Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}

一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问

LinkedList构造方法

无参构造函数, 默认构造方法声明也不做,first和last节点会被默认初始化为null。

1
2
3
4
复制代码*
/** Constructs an empty list. \*/*

public LinkedList() {}

LinkedList插入

由于LinkedList由双向链表作为底层数据结构,因此其插入无非由三大种

  • 尾插: add(E e)、addLast(E e)、addAll(Collection<? extends E> c)
  • 头插: addFirst(E e)
  • 中插: add(int index, E element)

可以从源码看出,在链表首尾添加元素很高效,在中间添加元素比较低效,首先要找到插入位置的节点,在修改前后节点的指针。

尾插-add(E e)和addLast(E e)
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
复制代码//常用的添加元素方法
public boolean add(E e) {
//使用尾插法
linkLast(e);
return true;
}

//在链表尾部添加元素
public void addLast(E e) {
linkLast(e);
}

//在链表尾端添加元素
void linkLast(E e) {
//尾节点
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
//判断是否是第一个添加的元素
//如果是将新节点赋值给last
//如果不是把原首节点的prev设置为新节点
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
//将集合修改次数加1
modCount++;
}
头插-addFirst(E e)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码public void addFirst(E e) {
//在链表头插入指定元素
linkFirst(e);
}

private void linkFirst(E e) {
//获取头部元素,首节点
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
//链表头部为空,(也就是链表为空)
//插入元素为首节点元素
// 否则就更新原来的头元素的prev为新元素的地址引用
if (f == null)
last = newNode;
else
f.prev = newNode;
//
size++;
modCount++;
}
中插-add(int index, E element)

当index不为首尾的的时候,实际就在链表中间插入元素。

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
复制代码 // 作用:在指定位置添加元素
public void add(int index, E element) {
// 检查插入位置的索引的合理性
checkPositionIndex(index);

if (index == size)
// 插入的情况是尾部插入的情况:调用linkLast()。
linkLast(element);
else
// 插入的情况是非尾部插入的情况(中间插入):linkBefore
linkBefore(element, node(index));
}

private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}

void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev; // 得到插入位置元素的前继节点
final Node<E> newNode = new Node<>(pred, e, succ); // 创建新节点,其前继节点是succ的前节点,后接点是succ节点
succ.prev = newNode; // 更新插入位置(succ)的前置节点为新节点
if (pred == null)
// 如果pred为null,说明该节点插入在头节点之前,要重置first头节点
first = newNode;
else
// 如果pred不为null,那么直接将pred的后继指针指向newNode即可
pred.next = newNode;
size++;
modCount++;
}

LinkedList 删除

删除和插入一样,其实本质也是只有三大种方式,

  • 删除首节点:removeFirst()
  • 删除尾节点:removeLast()
  • 删除中间节点 :remove(Object o)、remove(int index)

在首尾节点删除很高效,删除中间元素比较低效要先找到节点位置,再修改前后指针指引。

删除中间节点-remove(int index)和remove(Object o)

remove(int index)和remove(Object o)都是使用删除指定节点的unlink删除元素

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
复制代码 public boolean remove(Object o) {
//因为LinkedList允许存在null,所以需要进行null判断
if (o == null) {
//从首节点开始遍历
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
//调用unlink方法删除指定节点
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}

//删除指定位置的节点,其实和上面的方法差不多
//通过node方法获得指定位置的节点,再通过unlink方法删除
public E remove(int index) {
checkElementIndex(index);

return unlink(node(index));
}

//删除指定节点
E unlink(Node<E> x) {
//获取x节点的元素,以及它上一个节点,和下一个节点
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
//如果x的上一个节点为null,说明是首节点,将x的下一个节点设置为新的首节点
//否则将x的上一节点设置为next,将x的上一节点设为null
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
//如果x的下一节点为null,说明是尾节点,将x的上一节点设置新的尾节点
//否则将x的上一节点设置x的上一节点,将x的下一节点设为null
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
//将x节点的元素值设为null,等待垃圾收集器收集
x.item = null;
//链表节点个数减1
size--;
//将集合修改次数加1
modCount++;
//返回删除节点的元素值
return element;
}
删除首节点-removeFirst()
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
复制代码//删除首节点
public E remove() {
return removeFirst();
}
//删除首节点
public E removeFirst() {
final Node<E> f = first;
//如果首节点为null,说明是空链表,抛出异常
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
//删除首节点
private E unlinkFirst(Node<E> f) {
//首节点的元素值
final E element = f.item;
//首节点的下一节点
final Node<E> next = f.next;
//将首节点的元素值和下一节点设为null,等待垃圾收集器收集
f.item = null;
f.next = null; // help GC
//将next设置为新的首节点
first = next;
//如果next为null,说明说明链表中只有一个节点,把last也设为null
//否则把next的上一节点设为null
if (next == null)
last = null;
else
next.prev = null;
//链表节点个数减1
size--;
//将集合修改次数加1
modCount++;
//返回删除节点的元素值
return element;
}
删除尾节点-removeLast()
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
复制代码    //删除尾节点
public E removeLast() {
final Node<E> l = last;
//如果首节点为null,说明是空链表,抛出异常
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
private E unlinkLast(Node<E> l) {
//尾节点的元素值
final E element = l.item;
//尾节点的上一节点
final Node<E> prev = l.prev;
//将尾节点的元素值和上一节点设为null,等待垃圾收集器收集
l.item = null;
l.prev = null; // help GC
//将prev设置新的尾节点
last = prev;
//如果prev为null,说明说明链表中只有一个节点,把first也设为null
//否则把prev的下一节点设为null
if (prev == null)
first = null;
else
prev.next = null;
//链表节点个数减1
size--;
//将集合修改次数加1
modCount++;
//返回删除节点的元素值
return element;
}

其他方法也是类似的,比如查询方法 LinkedList提供了get、getFirst、getLast等方法获取节点元素值。

modCount属性的作用?

modCount属性代表为结构性修改( 改变list的size大小、以其他方式改变他导致正在进行迭代时出现错误的结果)的次数,该属性被Iterator以及ListIterator的实现类所使用,且很多非线程安全使用modCount属性。

​ 初始化迭代器时会给这个modCount赋值,如果在遍历的过程中,一旦发现这个对象的modCount和迭代器存储的modCount不一样,Iterator或者ListIterator 将抛出ConcurrentModificationException异常,

这是jdk在面对迭代遍历的时候为了避免不确定性而采取的 fail-fast(快速失败)原则:

在线程不安全的集合中,如果使用迭代器的过程中,发现集合被修改,会抛出ConcurrentModificationExceptions错误,这就是fail-fast机制。对集合进行结构性修改时,modCount都会增加,在初始化迭代器时,modCount的值会赋给expectedModCount,在迭代的过程中,只要modCount改变了,int expectedModCount = modCount等式就不成立了,迭代器检测到这一点,就会抛出错误:urrentModificationExceptions。


总结

ArrayList和LinkedList的区别、优缺点以及应用场景

区别:

  • ArrayList是实现了基于动态数组的数据结构,LinkedList是基于链表结构。
  • 对于随机访问的get和set方法查询元素,ArrayList要优于LinkedList,因为LinkedList循环链表寻找元素。
  • 对于新增和删除操作add和remove,LinkedList比较高效,因为ArrayList要移动数据。

优缺点:

  • 对ArrayList和LinkedList而言,在末尾增加一个元素所花的开销都是固定的。对ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是 统一的,分配一个内部Entry对象。
  • 在ArrayList集合中添加或者删除一个元素时,当前的列表移动元素后面所有的元素都会被移动。而LinkedList集合中添加或者删除一个元素的开销是固定的。
  • LinkedList集合不支持 高效的随机随机访问(RandomAccess),因为可能产生二次项的行为。
  • ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间

应用场景:

ArrayList使用在查询比较多,但是插入和删除比较少的情况,而LinkedList用在查询比较少而插入删除比较多的情况

各位看官还可以吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!

欢迎扫码关注,原创技术文章第一时间推出

本文转载自: 掘金

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

MySQL的NOT EXISTS遭遇战 浅尝一下NOT EX

发表于 2020-06-25

浅尝一下NOT EXISTS

最近老婆在看视频学习MySQL,然后碰到了这样一道习题:有三个表,分别记录学生、课程,以及学生选修了什么课程的信息,问如何用NOT EXISTS找出选修了所有课程的学生。

为了避免想破脑袋编造一些尴尬的学生姓名和课程名,我简化了一下习题中的表的结构,只留下它们的ID列。建表语句如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码-- 学生表
CREATE TABLE `student` (
`id` INT NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
);

-- 课程表
CREATE TABLE `course` (
`id` INT NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
);

-- 选修关系
CREATE TABLE `elective` (
`student_id` INT NOT NULL,
`course_id` INT NOT NULL,
FOREIGN KEY (`student_id`) REFERENCES `student`(`id`),
FOREIGN KEY (`course_id`) REFERENCES `course`(`id`)
);

还需要给它们塞入一些示例数据

1
2
3
复制代码INSERT INTO `student` (`id`) VALUES (1), (2), (3), (4), (5);
INSERT INTO `course` (`id`) VALUES (1), (2);
INSERT INTO `elective` (`course_id`, `student_id`) VALUES (1, 1), (2, 1), (1, 2), (2, 3), (2, 5), (1, 5);

显然,只有id列的值为1和5的学生是选修了全部课程的。用NOT EXISTS写出来的SQL语句如下

1
2
3
4
5
6
7
8
9
10
复制代码SELECT * 
FROM `student`
WHERE NOT EXISTS (SELECT *
FROM `course`
WHERE NOT EXISTS (SELECT *
FROM `elective`
WHERE `student`.`id` =
`elective`.`student_id`
AND `course`.`id` =
`elective`.`course_id`));

在DBEaver中运行后的结果为

在DBEaver中执行的结果

正确地找出了两个选修了所有课程的学生的id。

如何理解双重NOT EXISTS

当第一次被请教这道习题的时候,我其实并不能理解NOT EXISTS的含义。直到后来去看EXISTS的文档,才顿悟了上面的SQL。

我的理解方法是将双重NOT EXISTS转换为三层循环。以上面的SQL为例,转述为人话就是:找出student表中所有的、没有任何一门course表中的课程是没有选修的、的学生——双重的 没有。

转换为三层循环大概长这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码for (const student of students) {
// 是否存在学生未选修的课程
let existSuchCourse = false;
for (const course of courses) {
let existSuchElective = false;
for (const elective of electives) {
if (elective.student_id === student.id && elective.course_id === course.id) {
existSuchElective = true;
break;
}
}
// 如果遍历完elective表的记录后,existSuchElective仍然为false,说明的确有一门课程是没有选修记录的
// 那么便意味着“存在至少一门课程,使得当前被遍历的学生与该课程没有选修关系”。
if (!existSuchElective) {
existSuchCourse = true;
break;
}
}
// 如果遍历完一圈后确实没有找到“未选修”的课程,说明这名学生全都选修了
if (!existSuchCourse) {
console.log(student);
}
}

NOT EXISTS的本质

即使不强行理解,也可以让MySQL明确告知双重NOT EXISTS是怎么运作的。用EXPLAIN解释上面的SQL的结果如下图所示

MySQL的EXPLAIN命令的文档中说明了如何解读执行计划

EXPLAIN returns a row of information for each table used in the SELECT statement. It lists the tables in the output in the order that MySQL would read them while processing the statement. This means that MySQL reads a row from the first table, then finds a matching row in the second table, and then in the third table, and so on. When all tables are processed, MySQL outputs the selected columns and backtracks through the table list until a table is found for which there are more matching rows. The next row is read from this table and the process continues with the next table.

以上面的EXPLAIN为例,MySQL从student表中读出一行,再从course表中读取一行,最后从elective表中读取一行,然后看看WHERE子句是否能够被满足。如果可以,就输出从student表中读出来的这行数据。上图第2和第3行的select_type都是DEPENDENT SUBQUERY,表示它们依赖于“外层”的查询上下文——elective的WHERE子句依赖于student和course中读出来的行。

似乎和方才的三重循环有异曲同工之妙呢。

后记

像NOT EXISTS这么“高阶”的功能我从未在业务代码中读过和使用过——别说NOT EXISTS,就算是EXISTS也是从未有之,甚至连子查询也极少。毕竟“正经的互联网公司”只是把MySQL当妹妹当一个具备复杂查询查询功能的key-value数据库来使用(笑

比起双重NOT EXISTS,我更可能凭直觉写出基于子查询的解决方法

1
2
3
4
5
6
复制代码SELECT * 
FROM `student`
WHERE `id` IN (SELECT `student_id`
FROM `elective`
GROUP BY `student_id`
HAVING( Count(0) ) = 2);

我甚至觉得会有人把数据库里的行读进内存然后用应用层代码来找出选修了全部课程的学生!

全文完。

阅读原文

本文转载自: 掘金

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

golang面试题:对已经关闭的的chan进行读写,会怎么样

发表于 2020-06-25


问题
–

对已经关闭的的 chan 进行读写,会怎么样?为什么?

怎么答

  • 读已经关闭的 chan 能一直读到东西,但是读到的内容根据通道内关闭前是否有元素而不同。
    • 如果 chan 关闭前,buffer 内有元素还未读 , 会正确读到 chan 内的值,且返回的第二个 bool 值(是否读成功)为 true。
    • 如果 chan 关闭前,buffer 内有元素已经被读完,chan 内无值,接下来所有接收的值都会非阻塞直接成功,返回 channel 元素的零值,但是第二个 bool 值一直为 false。
  • 写已经关闭的 chan 会 panic

举例

1. 写已经关闭的 chan

  • 注意这个 send on closed channel,待会会提到。
2. 读已经关闭的 chan


多问一句


1. 为什么写已经关闭的 chan 就会 panic 呢?

  • 当 c.closed != 0 则为通道关闭,此时执行写,源码提示直接 panic,输出的内容就是上面提到的 "send on closed channel"。

2. 为什么读已关闭的 chan 会一直能读到值?

  • c.closed != 0 && c.qcount == 0 指通道已经关闭,且缓存为空的情况下(已经读完了之前写到通道里的值)
  • 如果接收值的地址 ep 不为空
    • 那接收值将获得是一个该类型的零值
    • typedmemclr 会根据类型清理相应地址的内存
    • 这就解释了上面代码为什么关闭的 chan 会返回对应类型的零值

文章推荐:

  • 对未初始化的的 chan 进行读写,会怎么样?为什么?
  • golang 面试题:​reflect(反射包)如何获取字段 tag​?为什么 json 包不能导出私有变量的 tag?
  • golang 面试题:json 包变量不加 tag 会怎么样?
  • golang 面试题:怎么避免内存逃逸?
  • golang 面试题:简单聊聊内存逃逸?
  • golang 面试题:字符串转成 byte 数组,会发生内存拷贝吗?
  • golang 面试题:翻转含有中文、数字、英文字母的字符串
  • golang 面试题:拷贝大切片一定比小切片代价大吗?
  • golang 面试题:能说说 uintptr 和 unsafe.Pointer 的区别吗?
如果你想每天学习一个知识点?

本文转载自: 掘金

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

从0搭建属于自己的Jenkins持续集成平台

发表于 2020-06-24

前言

  Jenkins在日常工作中占据了一个非常重要的角色,帮助我们节省了大量用于构建的时间。有些公司有运维大哥对Jenkins进行维护,如果没有那只能自己动手了。俗话说的好自己动手丰衣足食,所以本文就从0开始搭建属于自己的Jenkins持续平台。主要包含,普通项目构建、流水线构建、多分支流水线构建并将构建结果辅以钉钉通知。

前期准备

  • centos7 服务器一台

确认是否能安装docker

 Docker要求CentOS系统的内核版本高于3.10.通过uname -r命令查看你当前的内核版本。

1
2
sh复制代码[root@CentOS ~]# uname -r  
3.10.0-1127.8.2.el7.x86_64

更改yum源为阿里云

备份旧源

1
shell复制代码mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup

下载最新的源

1
shell复制代码wget -O /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repo

生成缓存

1
shell复制代码yum makecache

更新

1
shell复制代码yum update

安装docker

官方安装文档

1
shell复制代码yum install -y yum-utils

添加docker源

1
2
3
shell复制代码yum-config-manager \  
    --add-repo \
    https://download.docker.com/linux/centos/docker-ce.repo

安装docker

1
shell复制代码yum install docker-ce

启动docker

1
shell复制代码systemctl start docker

更改docker镜像源

1
shell复制代码vim /etc/docker/daemon.json

加入阿里云源地址

1
2
3
text复制代码{  
    "registry-mirrors":["https://6kx4zyno.mirror.aliyuncs.com"]
}

重新读取配置

1
shell复制代码systemctl daemon-reload

重启docker

1
shell复制代码systemctl restart docker

安装jenkins

下载jenkins镜像

1
shell复制代码docker pull jenkins

启动jenkins

 设置端口为9090并映射jenkins_home到宿主机/home/jenkins_home。

1
shell复制代码docker run -d --name jenkins -p 9090:8080 -v /home/jenkins_home:/var/jenkins_home jenkins

 可以通过docker ps查看运行的容器。

1
2
3
4
text复制代码[root@CentOS home]# docker ps  
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS                               NAMES
ec6a4da6b83f        jenkins             "/bin/tini -- /usr/l…"   About a minute ago   Up About a minute   50000/tcp, 0.0.0.0:9090->8080/tcp   jenkins
[root@CentOS home]#

把玩jenkins docker镜像遇到的volume权限问题

 在运行启动jenkins的命令时,可能会出现jenkins无法启动情况。

1
2
3
4
text复制代码[root@CentOS home]# docker ps -a  
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                     PORTS               NAMES
b571f16dafbf        jenkins             "/bin/tini -- /usr/l…"   8 minutes ago       Exited (1) 8 minutes ago                       jenkins
[root@CentOS home]#

 可以通过docker logs 镜像名称查看启动日志。

1
2
3
4
text复制代码[root@CentOS home]# docker logs jenkins  
touch: cannot touch '/var/jenkins_home/copy_reference_file.log': Permission denied
Can not write to /var/jenkins_home/copy_reference_file.log. Wrong volume permissions?
[root@CentOS home]#

 查看输出的日志,如果出现 Permission denied 类似的错误。需要删除旧容器重新运行。

1
shell复制代码docker rm jenkins

 运行命令加入了-u 0重新运行。

1
shell复制代码docker run -d --name jenkins -p 9090:8080 -v /home/jenkins_home:/var/jenkins_home -u 0 jenkins

参考 https://blog.csdn.net/minicto/article/details/73539986

Jenkins初始化

 启动成功后输入 http://服务器:9090/

 如果无法访问,请检查一下防火墙端口是否开放,如果是云服务器还需要检查安全组设置

  首次启动jenkins需要输入密码,需要进入容器内获取密码。密码位于/var/jenkins_home/secrets/initialAdminPassword。

进入容器

1
shell复制代码docker exec -it jenkins /bin/bash

获取密码

1
shell复制代码cat /var/jenkins_home/secrets/initialAdminPassword
1
2
3
4
text复制代码[root@CentOS jenkins_home]# docker exec -it jenkins /bin/bash  
root@ec6a4da6b83f:/# cat /var/jenkins_home/secrets/initialAdminPassword
68eed23ad39541949972468e4f2ce1fd
root@ec6a4da6b83f:/#

  由于我们将/var/jenkins_home – 挂载到–> /home/jenkins_home所以也可以直接cat /home/jenkins_home/secrets/initialAdminPassword 获取密码。

  输入密码以后,安装需要的插件,在安装途中由于网络原因会出现有些插件安装失败,这个可以不用理会。

设置jenkins的默认登录账号和密码

处理插件安装失败

  进入jenkins的主页面右上角可能会出现一些报错信息,主要是提示jenkins 需要的某些插件没有安装,或者说jenkins版本太低了,插件无法使用这个时候我们需要先升级jenkins做一个升级。

自动升级

Jenkins提供了自动升级的方式

手动升级

 可以去Jenkins的官网下载好最新jar包上传到服务器,也可以使用wget命令。

1
2
3
shell复制代码wget http://jenkins新版本的下载地址  
#目前最新2.239
wget http://updates.jenkins-ci.org/download/war/2.239/jenkins.war

  Jenkins的更新主要是替换jenkins镜像里面的war包 ,我们可以把下载好的war包使用docker cp直接进行复制命令如下:

1
shell复制代码docker cp jenkins.war jenkins:/usr/share/jenkins

 重新启动Jenkins即可完成升级。

1
shell复制代码docker restart jenkins

更插件源

1
text复制代码https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json
  • 替换完源以后点击提交。
  • 然后进入插件管理页面将出错的插件重新安装。
  • 及时更新插件。

安装必要的插件

  • Localization: Chinese (Simplified) 1.0.14 汉化包 搜索关键字 chinese
  • Publish Over SSH 1.20.1 搜索关键字 ssh
  • DingTalk 钉钉通知 2.3.0

配置jenkins

全局工具配置

  主要配置 jdk、maven、git等常用环境。需要注意配置的别名,后续构建将会使用到。

配置jdk

  因为jenkins镜像自带jdk所以无需安装直接使用即可,进入Jenkins容器,使用java -verbose查看java安装路径。

1
shell复制代码docker exec -it jenkins /bin/bash
1
shell复制代码java -verbose

配置git

 进入容器内使用whereis git即可查询到git安装路径。

1
2
3
text复制代码root@6a9fbb129cbe:~# whereis git  
git: /usr/bin/git /usr/share/man/man1/git.1.gz
root@6a9fbb129cbe:~#

配置maven

 maven直接使用自动安装即可。

系统设置

配置服务器

点击新增即可添加服务器,主要配置:

  • Name 名称 - 构建的时候将会用到
  • Hostname 服务器地址
  • Username 用户名
  • Remote Directory 远程目录 - 上传文件的目录 默认配置根目录即可/。

点击高级进行其他参数配置

  • 如果需要使用密码登录,则选中Use password authentication, or use a different key 复选框即可,如下图所示。

  除了配置密码还可以配置端口Port,跳板机Jump Host的参数,可以根据实际情况配置。默认可以使用密码。

  配置完成以后点击Test Configuration按钮,如果配置正常会出现Success 反之出现错误信息,可以根据错误信息,调整配置参数。

配置钉钉

  钉钉主要用于构建通知,在配置前需要在钉钉群内,添加自定义机器人。

自由风格的软件项目

  以https://gitee.com/huangxunhui/jenkins_demo.git为例。

新建项目

设置项目简介

源码管理

  • 配置仓库地址。
  • 配置凭证-主要用于拉取代码。
  • 配置需要构建的分支。

添加凭证

  如果项目是开源,则可以跳过这一步。反之需要设置凭证,要不然将无法拉取代码进行构建。

构建触发器

  可以根据实际情况选择,案例采用轮询的方式进行构建。

构建

构建后操作

  • 将jar包发送到相应的服务器。

  • Source files jar包的路径。支持通配符匹配.
  • Remove prefix 移除前缀,一般jar包的路径都存在于**/target下,如果不移除,会在目标服务器上建立相应的目录结构。
  • Remote directory 远程目录。

注意的点, 在之前配置服务器时也配置了Remote directory,这时候部署的实际目录是,服务器设置的远程目录+现在配置的远程目录。

  • Exec command 执行脚本,主要用于将jar发送到目标服务器后,执行相应的启动脚本。

配置完成点击保存即可。

点击开始构建

发送钉钉通知


流水线

  流水线构建,将上述构建步骤代码化,方便调整。

项目创建

流水线编写

  由于配置步骤类似,前面简单的步骤可以参照,自由风格的软件项目。这里主要讲流水线如何编写。

注意右下角的流水线语法,后续会用上。

  我们可以点击右上角的下拉按钮,生成一个简单的流水线。比如说hello world。

1
2
3
4
5
6
7
8
9
10
11
12
13
groovy复制代码pipeline {  

    // 表示所有机器都能运行   
   agent any

   stages {
      stage('Hello') {
         steps {
            echo 'Hello World'
         }
      }
   }
}

  通过上面的pipeline可以知道,有一个Hello的步骤,这个步骤执行的是,输出hello world。依葫芦画瓢,一次完整的构建我们可以总结出如下几个步骤:拉取代码(checkout) -> 打包(build) -> 部署(deploy)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
groovy复制代码pipeline {  
   agent any

   stages {
      stage('checkout') {
         steps {

         }
      }

      stage('build') {
         steps {

         }
      }

    stage('deploy') {
         steps {

         }
      }
   }
}

  步骤梳理好了,这个时候就可以完善对应的步骤了,这就需要用到提到的,流水线语法。

将生成好的流水线脚本复制到对应的步骤即可。

注意:如果使用到maven需要将maven引入,tools相应的内容就是配置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
groovy复制代码pipeline {  
   agent any

    // 工具
    tools {
        maven 'maven'
        jdk 'jdk'
    }

   stages {
      stage('checkout') {
         steps {
            checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[url: 'https://gitee.com/huangxunhui/jenkins_demo.git']]])
         }
      }

      stage('build') {
         steps {
            sh 'mvn clean package -Dmaven.test.skip=true'            
         }
      }

    stage('deploy') {
         steps {
            sshPublisher(publishers: [sshPublisherDesc(configName: 'dev', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: '''cd /home/project 
nohup java -jar jenkins_demo.jar > nohup.out 2>&1 &''', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '/home/project', remoteDirectorySDF: false, removePrefix: '/target', sourceFiles: '**/target/jenkins_demo.jar')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
         }
      }

   }
}

配置完成点击应用即可。

构建测试

上面演示的是将流水线配置在jenkins内,其实我们还可以从SCM中获取,比如git。

我们可以建立一个仓库专门维护不同项目的构建脚本Jenkinsfile,也可以在每个项目下,建立对应的Jenkinsfile.

  注意的点:项目中的Jenkinsfile需要和配置的一致。比如说上面的配置,是扫描项目根目录下名字为Jenkinsfile的文件。

所以我们可以在jenkins_demo仓库内添加Jenkinsfile文件。

配置点击完成,即可。


多分支流水线

  在日常开发中,通常是基于git-flow进行开发的,前面两种都是基于单分支构建,如果每个分支都去配置,那将耗费大量时间。所以多分支流水线就是用来解决这个问题的。

创建项目

配置分支源

构建配置

扫描触发器

完成上述配置,点击应用即可。

编写jenkinsfile文件

  核心思想是,根据不同的分支使用不同的打包命令,发送到不同的服务器进行运行。

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
168
169
170
171
172
173
174
175
176
177
178
groovy复制代码pipeline {  
    // 指定集群 any 表示所有
    agent any

    // 工具
    tools {
        maven 'maven'
        jdk 'jdk'
    }

    // 定义常量
    environment {

        // 钉钉机器人编号
        rebootId = 'a3c07482-d031-47a6-8542-05ac56c5f17a'

        // 开始logo
        imageOfStart = 'https://www.easyicon.net/api/resizeApi.php?id=1229977&size=128'

        // 成功logo
        imageOfSuccess = 'https://www.easyicon.net/api/resizeApi.php?id=1194837&size=128'

        // 失败logo
        imageOfFailure = 'https://www.easyicon.net/api/resizeApi.php?id=1201052&size=128'

        // 不稳定logo
        imageOfUnstable = 'https://www.easyicon.net/api/resizeApi.php?id=1219854&size=128'

        // 终止logo
        imageOfAborted = 'https://www.easyicon.net/api/resizeApi.php?id=1183198&size=128'

        // 认证Id
        credentialsId = '98e9c197-f0ae-44c3-8f67-4ca0339028a8'

        // 仓库地址
        repositoryUrl = 'https://gitee.com/huangxunhui/jenkins_demo.git'

        // 打包命令 - 项目需要配置maven多环境
        mavenProd = 'mvn clean package -P prod -Dmaven.test.skip=true'
        mavenTest = 'mvn clean package -P test -Dmaven.test.skip=true'
        mavenDev = 'mvn clean package -P dev -Dmaven.test.skip=true'

        // 服务器名称 - 案例测试-全部部署到dev环境
        devServer = 'dev'
        testServer = 'dev'
        prodServer = 'dev'

        // sshPublisher 配置
        removePrefix = '/target'
        remoteDirectory = '/home/project/jenkins_demo'
        sourceFiles = '**/target/jenkins_demo.jar'

        execCommandProd = 'cd /home/project && ./manage.sh jenkins_demo/ restart'
        execCommandTest = 'cd /home/project && ./manage.sh jenkins_demo/ restart'
        execCommandDev = 'cd /home/project && ./manage.sh jenkins_demo/ restart'

    }

    stages {

        stage('开始构建通知'){
            steps {
                dingtalk (
                        robot: "${rebootId}",
                        type: 'LINK',
                        title: "${env.JOB_NAME}",
                        text: [
                            "开始构建-编号为#${BUILD_NUMBER}"
                        ],
                        messageUrl: "${env.BUILD_URL}",
                        picUrl: "${imageOfStart}"
                )
            }
        }

        stage('拉取代码'){
            steps {
                echo "拉取 ${BRANCH_NAME} 分支的代码。"
                checkout([$class: 'GitSCM', branches: [[name: "*/${BRANCH_NAME}"]], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: "${credentialsId}", url: "${repositoryUrl}"]]])
            }
        }

        stage('进行打包'){
            steps {
                script {
                    if (env.BRANCH_NAME == 'master') {
                        sh "${mavenProd}"
                    } else if (env.BRANCH_NAME == 'test') {
                        sh "${mavenTest}"
                    } else if (env.BRANCH_NAME == 'dev') {
                        sh "${mavenDev}"
                    } else {
                        sh "${mavenDev}"
                    }
                }
            }
        }

        stage('项目部署'){
            steps {
                script {
                    if (env.BRANCH_NAME == 'master') {
                        // 部署生产环境
                        sshPublisher(publishers: [sshPublisherDesc(configName: "${prodServer}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "${execCommandProd}", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory:  "${remoteDirectory}", remoteDirectorySDF: false, removePrefix: "${removePrefix}", sourceFiles: "${sourceFiles}")], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
                    } else if (env.BRANCH_NAME == 'test') {
                        // 部署测试环境
                        sshPublisher(publishers: [sshPublisherDesc(configName: "${testServer}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "${execCommandTest}", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory:  "${remoteDirectory}", remoteDirectorySDF: false, removePrefix: "${removePrefix}", sourceFiles: "${sourceFiles}")], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
                    } else if (env.BRANCH_NAME == 'dev') {
                        // 部署开发环境
                        sshPublisher(publishers: [sshPublisherDesc(configName: "${devServer}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "${execCommandDev}", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory:  "${remoteDirectory}", remoteDirectorySDF: false, removePrefix: "${removePrefix}", sourceFiles: "${sourceFiles}")], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
                    } else {
                        sshPublisher(publishers: [sshPublisherDesc(configName: "${devServer}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "${execCommandTest}", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory:  "${remoteDirectory}", remoteDirectorySDF: false, removePrefix: "${removePrefix}", sourceFiles: "${sourceFiles}")], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
                    }
                }
            }
        }
    }

    // 流水线结束通知
    post {

        // 成功通知
        success {
            dingtalk (
                    robot: "${rebootId}",
                    type: 'LINK',
                    title: "${env.JOB_NAME}",
                    text: [
                        "构建成功-编号为#${BUILD_NUMBER}"
                    ],
                    messageUrl: "${env.BUILD_URL}",
                    picUrl: "${imageOfSuccess}"
                )
        }

        // 失败通知
        failure {
            dingtalk (
                    robot: "${rebootId}",
                    type: 'LINK',
                    title: "${env.JOB_NAME}",
                    text: [
                        "构建失败-编号为#${BUILD_NUMBER}"
                    ],
                    messageUrl: "${env.BUILD_URL}",
                    picUrl: "${imageOfFailure}"
            )
        }

        // 构建不稳定通知
        unstable {
            dingtalk (
                    robot: "${rebootId}",
                    type: 'LINK',
                    title: "${env.JOB_NAME}",
                    text: [
                        "构建不稳定-编号为#${BUILD_NUMBER}"
                    ],
                    messageUrl: "${env.BUILD_URL}",
                    picUrl: "${imageOfUnstable}"
            )
        }

        // 构建终止通知
        aborted {
            dingtalk (
                    robot: "${rebootId}",
                    type: 'LINK',
                    title: "${env.JOB_NAME}",
                    text: [
                        "构建终止-编号为#${BUILD_NUMBER}"
                    ],
                    messageUrl: "${env.BUILD_URL}",
                    picUrl: "${imageOfAborted}"
            )
        }
    }
}

使用到的启动脚本manage.sh。

钉钉机器人插件使用文档

构建结果

结尾

  如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。

本文转载自: 掘金

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

Jetpack 新成员 Hilt 实践(一)启程过坑记

发表于 2020-06-24

前言

在之前的文章里面分别分析 Jetpack 新成员 App Startup 实践以及原理分析 和 Jetpack 新成员 Paging3 实践以及源码分析(一) 以及 Jetpack 新成员 Paging3 网络实践及原理分析(二) 如果没有看过可以点击下方地址前去查看:

  • Jetpack 最新成员 AndroidX App Startup 实践以及原理分析
  • Jetpack 成员 Paging3 数据实践以及源码分析(一)
  • Jetpack 成员 Paging3 网络实践及原理分析(二)
  • Jetpack 成员 Paging3 使用 RemoteMediator 实现加载网络分页数据并更新到数据库中(三)
  • 代码地址:https://github.com/hi-dhl/AndroidX-Jetpack-Practice

这篇文章主要来分析一下 Hilt,花了好几天时间梳理了一下 官方 Hilt 文档,Hilt 的知识点有点多,将会分为三篇文章结合实际案例来完成,每篇文章都会有详细的使用的案例。本篇文章的案例已经上传到了 GitHub:HiltSimple。

通过这篇文章你将学习到以下内容:

  • 为什么需要使用依赖注入库?
  • Hilt 是什么?
  • Hilt 常用注解含义?
  • 使用 Hilt 都有那些坑需要注意?
  • Hilt 如何和 Android 组件一起使用?
  • Hilt 如何和第三方组件一起使用?
  • Hilt 如何和 Jetpack 组件 ViewModule 一起使用?
  • Hilt 如何和 Jetpack 组件 Room 一起使用?

研究 Hilt 时遇到一些坑,有些坑在 Goggle 文档上也没有提到,我会在文中特别强调,并在 文末总结部分 进行汇总。

为什么需要使用依赖注入库

Hilt、Dagger、Koin 等等都是依赖注入库,Google 也在努力不断的完善依赖注入库从 Dagger 到 Dagger2 在到现在的 Hilt,因为依赖注入是面向对象设计中最好的架构模式之一,使用依赖注入库有以下优点:

  • 依赖注入库会自动释放不再使用的对象,减少资源的过度使用。
  • 在配置 scopes 范围内,可重用依赖项和创建的实例,提高代码的可重用性,减少了很多模板代码。
  • 代码变得更具可读性。
  • 易于构建对象。
  • 编写低耦合代码,更容易测试。

Hilt 是什么?

Hilt 是 Android 的依赖注入库,它减少了在项目中进行手动依赖,进行手动依赖注入需要您手动构造每个类及其依赖,依赖注入库的出现节省了 Android 开发者大量的时间。

Hilt 通过为项目中的每个 Android 类提供容器并自动管理它们的生命周期,提供了在应用程序中使用 DI 的标准方法。Hilt 是在 Dagger 的基础上进行构建,因为 Dagger 提供的编译时正确性、运行时性能、可伸缩性并且从 Android Studio 支持 Dagger 中获益。

Hilt 的实现要比 Dagger 简单得多,使用 Dagger 实现依赖注入,需要去编写 modules、components 等等。每次创建一个新的 android 组件,比如 Activity、Fragment 或Service,我们都需要手动将它们添加到各自的 modules 中,以便在需要的时候注入它们。

接下来我们开始从分析 Hilt 注解的含义出发,来了解如何在应用程序中使用 Hilt。

Hilt 常用注解的含义

Hilt 常用注解包含 @HiltAndroidApp、@AndroidEntryPoint、@Inject、@Module、@InstallIn、@Provides、@EntryPoint 等等。

@HiltAndroidApp

  1. 所有使用 Hilt 的 App 必须包含一个使用 @HiltAndroidApp 注解的 Application。
  2. @HiltAndroidApp 注解将会触发 Hilt 代码的生成,作为应用程序依赖项容器的基类。
  3. 生成的 Hilt 组件依附于 Application 的生命周期,它也是 App 的父组件,提供其他组件访问的依赖。
  4. 在 Application 中设置好 @HiltAndroidApp 之后,就可以使用 Hilt 提供的组件了,组件包含Application、Activity、Fragment、View、Service、BroadcastReceiver 等等。

@AndroidEntryPoint

Hilt 提供的 @AndroidEntryPoint 注解用于提供 Android 类的依赖(Activity、Fragment、View、Service、BroadcastReceiver)特殊的 Application 使用 @HiltAndroidApp 注解。

  • Activity:仅仅支持 ComponentActivity 的子类例如 FragmentActivity、AppCompatActivity 等等。
  • Fragment:仅仅支持继承 androidx.Fragment 的 Fragment
  • View
  • Service
  • BroadcastReceiver

坑:

  • 如果使用 @AndroidEntryPoint 在非 ComponentActivity 子类上注解,例如 Activity 则会抛出以下异常。
1
kotlin复制代码Activities annotated with @AndroidEntryPoint must be a subclass of androidx.activity.ComponentActivity. (e.g. FragmentActivity, AppCompatActivity, etc.)
  • 如果使用 @AndroidEntryPoint 注解 Android 类,必须在它依赖的 Android 类添加同样的注解,例如在 Fragment 中添加 @AndroidEntryPoint 注解,必须在 Fragment 依赖的 Activity 上也添加 @AndroidEntryPoint 注解 , 否则会抛出以下异常。
1
kotlin复制代码java.lang.IllegalStateException: Hilt Fragments must be attached to an @AndroidEntryPoint Activity. Found: class com.hi.dhl.hilt.MainActivity

@Inject

Hilt 需要知道如何从相应的组件中提供必要依赖的实例。使用 @Inject 注解来告诉 Hilt 如何提供该类的实例,它常用于构造函数、非私有字段、方法中。

注意:在构建时,Hilt 为 Android 类生成 Dagger 组件。然后 Dagger 遍历您的代码并执行以下步骤:

  • 构建并验证依赖关系,确保没有未满足的依赖关系。
  • 生成它在运行时用于创建实际对象及其依赖项的类。

@Module

常用于创建依赖类的对象(例如第三方库 OkHttp、Retrofit等等),使用 @Module 注解的类,需要使用 @InstallIn 注解指定 module 的范围。

1
2
3
4
5
less复制代码@Module
@InstallIn(ApplicationComponent::class)
// 这里使用了 ApplicationComponent,因此 NetworkModule 绑定到 Application 的生命周期。
object NetworkModule {
}

@InstallIn

使用 @Module 注入的类,需要使用 @InstallIn 注解指定 module 的范围,例如使用 @InstallIn(ActivityComponent::class) 注解的 module 会绑定到 activity 的生命周期上。

Hilt 提供了以下组件来绑定依赖与 对应的 Android 类的活动范围。

Hilt 提供的组件 对应的 Android 类的活动范围
ApplicationComponent Application
ActivityRetainedComponent ViewModel
ActivityComponent Activity
FragmentComponent Fragment
ViewComponent View
ViewWithFragmentComponent View annotated with @WithFragmentBindings
ServiceComponent Service

注意:Hilt 没有为 broadcast receivers 提供组件,因为 Hilt 直接从 ApplicationComponent 注入 broadcast receivers。

Hilt 会根据相应的 Android 类生命周期自动创建和销毁生成的组件类的实例,它们的对应关系如下表格所示。

Hilt 提供的组件 创建对应的生命周期 销毁对应的生命周期
ApplicationComponent Application#onCreate() Application#onDestroy()
ActivityRetainedComponent Activity#onCreate() Activity#onDestroy()
ActivityComponent Activity#onCreate() Activity#onDestroy()
FragmentComponent Fragment#onAttach() Fragment#onDestroy()
ViewComponent View#super() View destroyed
ViewWithFragmentComponent View#super() View destroyed
ServiceComponent Service#onCreate() Service#onDestroy()

@Provides

它常用于被 @Module 注解标记类的内部的方法,并提供依赖项对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
less复制代码@Module
@InstallIn(ApplicationComponent::class)
// 这里使用了 ApplicationComponent,因此 NetworkModule 绑定到 Application 的生命周期。
object NetworkModule {

/**
* @Provides 常用于被 @Module 注解标记类的内部的方法,并提供依赖项对象。
* @Singleton 提供单例
*/
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.build()
}
}

@EntryPoint

Hilt 支持最常见的 Android 类 Application、Activity、Fragment、View、Service、BroadcastReceiver 等等,但是您可能需要在Hilt 不支持的类中执行依赖注入,在这种情况下可以使用 @EntryPoint 注解进行创建,Hilt 会提供相应的依赖。

基本概念介绍完了之后,我们正式在项目中使用 Hilt。

如何使用 Hilt

首先需要添加 Hilt 依赖,Hilt 依赖添加方式相比于 Koin 太麻烦了,首先在 project 的 build.gradle 添加以下依赖。

1
2
3
4
5
6
7
arduino复制代码buildscript {
...
dependencies {
...
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
}
}

然后在 App 模块中的 build.gradle 文件中添加以下代码。

1
2
3
4
5
6
7
8
9
10
11
12
arduino复制代码...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
...
}

dependencies {
implementation "com.google.dagger:hilt-android:2.28-alpha"
kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}

坑:需要注意的是如果同时使用 hilt 和 data binding,Android Studio 的版本必须 >= 4.0

所以还没有升级的朋友们,尽快升级吧,升级到 Android Studio 4.0 也会遇到一些坑,不过好在这些坑现在都有相应的解决方案了。

Hilt 使用 Java 8 的功能,所以要在项目中启用 Java 8,需要在 App 模块的 build.gradle 文件中,添加以下代码

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

// For Kotlin projects
kotlinOptions {
jvmTarget = "1.8"
}
}

注意: 这里有一个坑,对于 Kotlin 项目,需要添加 kotlinOptions,这是 Google 文档 Dependency injection with Hilt 中没有提到的,否则使用 ViewModel 会编译不过,下文会有详细的讲解。

Hilt 依赖添加方式相比于 Koin 太麻烦了,使用 koin 只需要添加相应的依赖就可以使用了。

Application 是 App 的入口,所以所有使用 Hilt 的 App 必须包含一个使用 @HiltAndroidApp 注解的 Application

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码@HiltAndroidApp
class HiltApplication : Application() {
/**
* 1. 所有使用 Hilt 的 App 必须包含一个使用 @HiltAndroidApp 注解的 Application
* 2. @HiltAndroidApp 将会触发 Hilt 代码的生成,包括用作应用程序依赖项容器的基类
* 3. 生成的 Hilt 组件依附于 Application 的生命周期,它也是 App 的父组件,提供其他组件访问的依赖
* 4. 在 Application 中设置好 @HiltAndroidApp 之后,就可以使用 Hilt 提供的组件了,
* Hilt 提供的 @AndroidEntryPoint 注解用于提供 Android 类的依赖(Activity、Fragment、View、Service、BroadcastReceiver)等等
* Application 使用 @HiltAndroidApp 注解
*/
}
  1. @HiltAndroidApp 将会触发 Hilt 代码的生成,包括用作应用程序依赖容器的基类
  2. 生成的 Hilt 组件依附于 Application 的生命周期,它也是 App 的父组件,提供其他组件访问的依赖

准备工作都做完了,接下来我们来看几个例子,如何使用 Hilt 进行依赖注入。

如何使用 Hilt 进行依赖注入

我们先来看一个简单的例子,注入 HiltSimple 并在 Application 中调用它的 doSomething 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码class HiltSimple @Inject constructor() {
fun doSomething() {
Log.e(TAG, "----doSomething----")
}
}

@HiltAndroidApp
class HiltApplication : Application() {
@Inject
lateinit var mHiltSimple: HiltSimple

override fun onCreate() {
super.onCreate()
mHiltSimple.doSomething()
}
}

Hilt 需要知道如何从相应的组件中提供必要依赖的实例。使用 @Inject 注解来告诉 Hilt 如何提供该类的实例,@Inject 常用于构造函数、非私有字段、方法中。

Hilt 如何和 Android 组件一起使用

如果是 Hilt 支持的 Android 组件,直接使用 @AndroidEntryPoint 注解即可。

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
kotlin复制代码/**
*
* 为项目中的每个 Android 类生成一个 Hilt 组件,这些组件可以从它们各自的父类接收依赖项,
* 如果是抽象类则不能使用 @AndroidEntryPoint 注解
*
* 如果使用 @AndroidEntryPoint 注解 Android 类,还必须注解依赖于它的 Android 类,
* 例如 如果 注解 fragment 然后还必须注解 fragment 依赖的 Activity, 否则会抛出以下异常
* java.lang.IllegalStateException: Hilt Fragments must be attached to an @AndroidEntryPoint Activity. Found: class com.hi.dhl.hilt.MainActivity
*/
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 用到了 Fragment 1.2.0 中重要的更新
// 可以查看之前写的这篇文章 @see https://juejin.cn/post/6844904167685750798
supportFragmentManager.beginTransaction()
.add(R.id.container, HiltFragment::class.java, null)
.commit()
}
}

/**
* 如果 注解 fragment 然后还必须注解 fragment 依赖的 Activity, 否则会抛出以下异常
* java.lang.IllegalStateException: Hilt Fragments must be attached to an @AndroidEntryPoint Activity. Found: class com.hi.dhl.hilt.MainActivity
*/
@AndroidEntryPoint
class HiltFragment : Fragment() {

// 使用 @Inject 注解从组件中获取依赖
@Inject
lateinit var mHiltSimple: HiltSimple

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_hilt, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mHiltSimple.doSomething()
}
}
  • 如果是抽象类则不需要使用 @AndroidEntryPoint 注解。
  • @AndroidEntryPoint 注解 仅仅支持 ComponentActivity 的子类例如 FragmentActivity、AppCompatActivity 等等。
  • 如果使用 @AndroidEntryPoint 注解 Android 类,必须在它依赖的 Android 类添加同样的注解,例如在 Fragment 中添加 @AndroidEntryPoint 注解,必须在 Fragment 依赖的 Activity 上也添加 @AndroidEntryPoint 注解。

注意: 在 Activity 中添加 Fragment,用到了 Fragment 1.2.0 中重要的更新,可以查看之前写的这篇文章 [译][Google工程师] 详解 FragmentFactory 如何优雅使用 Koin 以及部分源码分析。

Hilt 如何和第三方组件一起使用

如果要在项目中注入第三方依赖,我们需要使用 @Module 注解,使用 @Module注解的普通类,在其中创建第三方依赖的对象。

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
kotlin复制代码@Module
@InstallIn(ApplicationComponent::class)
// 这里使用了 ApplicationComponent,因此 NetworkModule 绑定到 Application 的生命周期。
object NetworkModule {

/**
* @Provides 常用于被 @Module 注解标记类的内部的方法,并提供依赖项对象。
* @Singleton 提供单例
*/
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.build()
}

@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.client(okHttpClient)
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}

@Provides
@Singleton
fun provideGitHubService(retrofit: Retrofit): GitHubService {
return retrofit.create(GitHubService::class.java)
}
}
  • @Module 常用于创建依赖类的对象(例如第三方库 OkHttp、Retrofit等等)。
  • 使用 @Module 注入的类,需要使用 @InstallIn 注解指定 module 的范围,会绑定到 Android 类对应的生命周期上。
  • @Provides 常用于被 @Module 注解标记类的内部的方法,并提供依赖项对象。

Hilt 如何和 ViewModel 一起使用

在 App 模块中的 build.gradle 文件中添加以下代码。

1
2
arduino复制代码implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha01'
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha01'

注意: 这个是在 Google 文档上没有提到的,如果使用的是 kotlin 的话需要额外在 App 模块中的 build.gradle 文件中添加以下代码,否则调用 by viewModels() 会编译不过。

1
2
3
4
ini复制代码// For Kotlin projects
kotlinOptions {
jvmTarget = "1.8"
}

在 ViewModel 对象的构造函数中使用 @ViewModelInject 注解提供一个 ViewModel。

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
kotlin复制代码class HiltViewModel @ViewModelInject constructor(
) : ViewModel() {

/**
* 在 LifeCycle 2.2.0 之后,可以用更精简的方法来完成,使用 LiveData 协程构造方法 (coroutine builder)。
* liveData 协程构造方法提供了一个协程代码块,产生的是一个不可变的 LiveData,emit() 方法则用来更新 LiveData 的数据。
*
* 具体可以查看之前写的这篇文章 [https://juejin.cn/post/6844904193468137486#heading-10] 有详细介绍
*/
val mHitLiveData = liveData {
emit(" i am a ViewModelInject")
}
}

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

private val mHitViewModule: HiltViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

mHitViewModule.mHitLiveData.observe(this, Observer {
tvResult.setText(it)
})
}
}

在 HiltViewModel 里面使用了 LifeCycle 2.2.0 之后新增的方法,LiveData 协程构造方法提供了一个协程代码块,产生的是一个不可变的 LiveData,emit() 方法则用来更新 LiveData 的数据,具体可以查看之前写的这篇文章 Jetpack 成员 Paging3 实践以及源码分析(一) 里面有详细介绍。

Hilt 如何和 Room 一起使用

这里需要用到 @Module 注解,使用 @Module 注解的普通类,在其中提供 Room 的实例。

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
kotlin复制代码@Module
@InstallIn(ApplicationComponent::class)
// 这里使用了 ApplicationComponent,因此 NetworkModule 绑定到 Application 的生命周期。
object RoomModule {

/**
* @Provides 常用于被 @Module 注解标记类的内部的方法,并提供依赖项对象。
* @Singleton 提供单例
*/
@Provides
@Singleton
fun provideAppDataBase(application: Application): AppDataBase {
return Room
.databaseBuilder(application, AppDataBase::class.java, "dhl.db")
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build()
}

@Provides
@Singleton
fun providePersonDao(appDatabase: AppDataBase): PersonDao {
return appDatabase.personDao()
}
}

总结

全文到这里就结束了,本篇文章的案例已经全部上传到了 GitHub:HiltSimple。

这篇文章里面分别介绍了 @HiltAndroidApp、@AndroidEntryPoint、@Inject、@Module、@InstallIn、@Provides 的含义以及实战案例,下篇文章我们一起来分析一下 @EntryPoint,以及和其他 Jetpack 组件如何一起使用。

需要注意的是使用 Hilt 有三个需要注意的地方

  • 如果注解非 ComponentActivity 子类,例如 Activity 则会抛出以下异常。
1
kotlin复制代码Activities annotated with @AndroidEntryPoint must be a subclass of androidx.activity.ComponentActivity. (e.g. FragmentActivity, AppCompatActivity, etc.)
  • 如果使用 @AndroidEntryPoint 注解 Android 类,必须在它依赖的 Android 类添加同样的注解,例如在 Fragment 中添加 @AndroidEntryPoint 注解,必须在 Fragment 依赖的 Activity 上也添加 @AndroidEntryPoint 注解 , 否则会抛出以下异常。
1
kotlin复制代码java.lang.IllegalStateException: Hilt Fragments must be attached to an @AndroidEntryPoint Activity. Found: class com.hi.dhl.hilt.MainActivity
  • 需要注意的是如果同时使用 hilt 和 data binding,Android Studio 的版本必须 >= 4.0

所以还没有升级的朋友们,尽快升级吧,升级到 Android Studio 4.0 也会遇到一些坑,不过好在这些坑现在都有相应的解决方案了。

Hilt 如果和 ViewModel 一起使用有点需要注意

这个是在 Google 文档上没有提到的,如果使用的是 kotlin 语言的话,需要额外在 App 模块中的 build.gradle 文件中添加以下代码,否则调用 by viewModels() 会编译不过。

1
2
3
ini复制代码kotlinOptions {
jvmTarget = "1.8"
}

计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,可以前去查看:AndroidX-Jetpack-Practice, 如果这个仓库对你有帮助,请帮我点个赞,我会陆续完成更多 Jetpack 新成员的项目实践。

结语

致力于分享一系列 Android 系统源码、逆向分析、算法、翻译、Jetpack 源码相关的文章,正在努力写出更好的文章,如果这篇文章对你有帮助给个 star,一起来学习,期待与你一起成长。

算法

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

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

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

Android 10 源码系列

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

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

Android 应用系列

  • 如何在项目中封装 Kotlin + Android Databinding
  • 再见吧 buildSrc, 拥抱 Composing builds 提升 Android 编译速度
  • 为数不多的人知道的 Kotlin 技巧以及 原理解析
  • Jetpack 最新成员 AndroidX App Startup 实践以及原理分析
  • Jetpack 成员 Paging3 实践以及源码分析(一)
  • Jetpack 新成员 Paging3 网络实践及原理分析(二)

精选译文

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

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

工具系列

  • 为数不多的人知道的 AndroidStudio 快捷键(一)
  • 为数不多的人知道的 AndroidStudio 快捷键(二)
  • 关于 adb 命令你所需要知道的
  • 10分钟入门 Shell 脚本编程
  • 基于 Smali 文件 Android Studio 动态调试 APP
  • 解决在 Android Studio 3.2 找不到 Android Device Monitor 工具

本文转载自: 掘金

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

Elasticsearch系列---性能调优最佳实践

发表于 2020-06-24

概要

性能调优是系统架构里所有组件必不可少的话题,Elasticsearch也不例外,虽说Elasticsearch内的默认配置已经非常优秀,但这不表示它就是完美的,必要的一些实践我们还是需要了解一下。

开启慢查询日志

慢查询日志是性能诊断的重要利器,常规操作是设置慢查询的阀值,然后运维童鞋每天对慢日志进行例行巡查,有特别慢的查询,立即报备事件处理,其余的定期将慢日志的top n取出来进行优化。

慢日志的配置在elasticsearch 6.3.1版本下是通过命令配置的,读操作和写操作可以单独设置,阀值的定义可根据实际的需求和性能指标,有人觉得5秒慢,有人觉得3秒就不可接受,我们以3秒为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码PUT /_all/_settings
{
"index.search.slowlog.threshold.query.warn":"3s",
"index.search.slowlog.threshold.query.info":"2s",
"index.search.slowlog.threshold.query.debug":"1s",
"index.search.slowlog.threshold.query.trace":"500ms",

"index.search.slowlog.threshold.fetch.warn":"1s",
"index.search.slowlog.threshold.fetch.info":"800ms",
"index.search.slowlog.threshold.fetch.debug":"500ms",
"index.search.slowlog.threshold.fetch.trace":"200ms",

"index.indexing.slowlog.threshold.index.warn":"3s",
"index.indexing.slowlog.threshold.index.info":"2s",
"index.indexing.slowlog.threshold.index.debug":"1s",
"index.indexing.slowlog.threshold.index.trace":"500ms",
"index.indexing.slowlog.level":"info",
"index.indexing.slowlog.source":"1000"
}

这三段分别表示query查询、fetch查询和index写入三类操作的慢日志输出阀值,_all表示对所有索引生效,也可以针对具体的索引。

同时在log4j2.properties配置文件中增加如下配置:

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
复制代码# 查询操作慢日志输出
appender.index_search_slowlog_rolling.type = RollingFile
appender.index_search_slowlog_rolling.name = index_search_slowlog_rolling
appender.index_search_slowlog_rolling.fileName = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_index_search_slowlog.log
appender.index_search_slowlog_rolling.layout.type = PatternLayout
appender.index_search_slowlog_rolling.layout.pattern = [%d{ISO8601}][%-5p][%-25c] %.10000m%n
appender.index_search_slowlog_rolling.filePattern = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_index_search_slowlog-%d{yyyy-MM-dd}.log
appender.index_search_slowlog_rolling.policies.type = Policies
appender.index_search_slowlog_rolling.policies.time.type = TimeBasedTriggeringPolicy
appender.index_search_slowlog_rolling.policies.time.interval = 1
appender.index_search_slowlog_rolling.policies.time.modulate = true

logger.index_search_slowlog_rolling.name = index.search.slowlog
logger.index_search_slowlog_rolling.level = trace
logger.index_search_slowlog_rolling.appenderRef.index_search_slowlog_rolling.ref = index_search_slowlog_rolling
logger.index_search_slowlog_rolling.additivity = false

# 索引操作慢日志输出
appender.index_indexing_slowlog_rolling.type = RollingFile
appender.index_indexing_slowlog_rolling.name = index_indexing_slowlog_rolling
appender.index_indexing_slowlog_rolling.fileName = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_index_indexing_slowlog.log
appender.index_indexing_slowlog_rolling.layout.type = PatternLayout
appender.index_indexing_slowlog_rolling.layout.pattern = [%d{ISO8601}][%-5p][%-25c] %marker%.10000m%n
appender.index_indexing_slowlog_rolling.filePattern = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_index_indexing_slowlog-%d{yyyy-MM-dd}.log
appender.index_indexing_slowlog_rolling.policies.type = Policies
appender.index_indexing_slowlog_rolling.policies.time.type = TimeBasedTriggeringPolicy
appender.index_indexing_slowlog_rolling.policies.time.interval = 1
appender.index_indexing_slowlog_rolling.policies.time.modulate = true

logger.index_indexing_slowlog.name = index.indexing.slowlog.index
logger.index_indexing_slowlog.level = trace
logger.index_indexing_slowlog.appenderRef.index_indexing_slowlog_rolling.ref = index_indexing_slowlog_rolling
logger.index_indexing_slowlog.additivity = false

重启elasticsearch实例后,就能在/home/esuser/esdata/log目录中看到生成的两个日志文件了。

优化实践建议

基本使用规范

  1. 搜索结果不要返回过大的结果集

过大的结果集会占用大量的IO资源和带宽,速度肯定快不了,Elasticsearch是一个搜索引擎,最理想的搜索是精准查询或次精准查询,最关心的是排在前面的少数结果,而不是所有结果,优化搜索条件,控制搜索结果数量是高性能的前提。

如果真有大批量的数据查询,建议使用scroll api。

  1. 避免超大的document

http.max_context_length的默认值是100mb,如果你一次document写入时,document的内容不能超过100mb,否则es就会拒绝写入。虽然你可以修改此配置,但不建议这么做,es底层的lucene引擎还是有一个2gb的最大限制。

过大的document会占用非常多的资源,从任何方面考虑都不建议,如果业务需求真有非常大的内容,如对书的内容搜索,建议按章节、按段落进行拆分存储。

  1. 避免稀疏的数据

document的设计会从根本上影响索引的性能,稀疏数据是一个典型的不良设计,浪费存储空间,影响读写性能。

下面有一些document结构设计的建议:

  • 避免将没有任何关联性的数据写入同一个索引

没有关联性的数据,意味着数据结构也不相同,硬生生放在同一个索引,会导致index数据非常稀疏,建议是将这些数据放在不同的索引中。

  • 对document的结构进行统一规范化

document的结构、命名尽可能统一规范处理,同样是创建时间字段,避免有的叫timestamp,有的叫create_time,尽可能统一。

  • 对某些field禁用norms和doc_values

如果一个field不需要考虑其相关度分数,那么可以禁用norms,如果不需要对一个field进行排序或者聚合,那么可以禁用doc_values字段。

服务器层级

硬件资源是性能最硬核的部分,硬件好,起点就高。

  1. 用更快的硬件资源

在预算范围内,能用SSD固态硬盘就不要选用机械硬盘;

CPU主频、核数当然是强大到预算上限;

内存单机上限64GB,加机器加到没钱为止;

尽量使用本地存储系统,不要用NFS等网络存储,毕竟硬盘便宜。

  1. 给filesystem cache更多的内存

Elasticsearch的搜索严重依赖于底层的filesystem cache,如果所有的数据都能够存放在filesystem cache中,那么搜索基本上是秒级。

由于实际情况的限制,最佳的情况下,就是你的机器的内存,至少可以容纳你的总数据量的一半。

要达到最佳情况有两个办法:一个是砸钱,买更多机器,加更大内存;另一种是精简document数据,只把需要搜索的field放进es内,filesystem cache就能存下更多的document,可以提高内存的利用率。剩余的其他字段,可以放在redis/mysql/hbase/hapdoop做二级加载。

  1. 禁止swapping交换内存

将swapping禁止掉,如果es jvm内存交换到磁盘,再交换回内存,会造成大量磁盘IO,性能很差。

Elasticsearch层级

  1. index buffer

在高并发写入场景,我们可以将index buffer调大一些,indices.memory.index_buffer_size,这个可以调节大一些,这个值默认是jvm heap的10%,这个index buffer大小,是所有的shard公用的,这个值除以shard数量,算出来平均每个shard可以使用的内存大小,一般建议对于每个shard最多给512mb。

  1. 禁止_all field

_all field会将document中所有field的值都合并在一起进行索引,很占用磁盘空空间,实际上用处却不大,生产环境最好禁用_all field。

  1. 使用best_compression

_source field和其他field很占用磁盘空间,建议对其使用best_compression进行压缩。

  1. 用最小的最合适的数字类型

es支持4种数字类型:byte,short,integer,long。如果最小的类型就合适,那么就用最小的类型,节省磁盘空间。

  1. 禁用不需要的功能

对于需要进行聚合和排序的field,我们才建立正排索引;
对于需要进行检索的field,我们才建立倒排索引;
对于不关心doc分数的field,我们可以禁用掉norm;
对于不需要执行phrase query近似匹配的field,那么可以禁用位置这个属性;

  1. 不要用默认的动态string类型映射

默认的动态string类型映射会将string类型的field同时映射为text类型以及keyword类型,大多数情况我们只需要使用其中一种,剩下的都是浪费磁盘空间,例如,id field这种字段可能只需要keyword,而body field可能只需要text field。

所以是使用keyword和text在设计时就应该区分清楚,而不是全盘保存。

  1. 预热filesystem cache

如果我们重启了Elasticsearch,那么filesystem cache是空的,每次数据查询时再加载数据进filesystem cache,我们可以先对一些数据进行查询,提前将一些常用数据加载到内存,待真实客户使用时,可以直接使用内存数据,响应就很快了。

代码研发层级

  1. 多使用bulk做写入

我们使用Java作为客户端时,写入操作全部利用bulk api来完成。

  1. 使用多线程将数据写入
  2. document使用自动生成的id

手动给document设置一个id,那么es需要每次都去确认一下那个id是否存在,这个过程是比较耗费时间的。如果我们使用自动生成的id,那么es就可以跳过这个步骤,写入性能会更好。

对于关系型数据库中的表id,可以作为es document的一个field存入。

  1. 重视document结构设计

业务研发的重中之重,好的document结构会带来非常优秀的性能表现。

  1. 避免使用script脚本
  2. 充分利用缓存

时间查询时,不要使用now这种函数,应该在客户端把时间转换成规范的格式,再到Elasticsearch里查询,这样能提高缓存的使用率。

小结

本篇介绍了Elasticsearch性能调优的常见实践方法,从服务器、实例再到代码层级,可以作为参考,但性能调优没有约定俗成的方法,需要反复的验证,仅供参考,谢谢

专注Java高并发、分布式架构,更多技术干货分享与心得,请关注公众号:Java架构社区
可以扫左边二维码添加好友,邀请你加入Java架构社区微信群共同探讨技术

Java架构社区

本文转载自: 掘金

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

JVM内存模型(JMM)和内存区域,别再傻傻分不清楚 前言

发表于 2020-06-23

前言

在面试中,经常会遇到JVM内存相关的问题。但是实际上很多情况下,大家没有把内存模型和内存区域认真区分,实际上这是两个层面的东西。

JVM内存模型(JMM)

JMM的出现是为了解决在并发编程中的两个经典问题:线程之间如何通信及线程之间如何同步。

  • 这里的线程是指并发执行的活动实体
  • 通信是指线程之间以何种机制来交换信息

通信:在命令式编程中,线程之间的通信方式有两种

  • 共享内存(共享内存模型:线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信)
  • 消息传递(消息传递模型:线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信)

同步:同步是指程序用于控制不同线程之间操作发生相对顺序的机制

  • 在共享内存并发模型里,同步是显式进行的,程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行
    • 典型的共享内存通信方式就是通过共享对象进行通信
  • 在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的
    • 在java中典型的消息传递方式就是wait()和notify()

小结一下:

通信方式 通信隐显 同步隐显 程序员角度
共享内存 隐式通信 显示同步 显示指定
消息传递 显示通信 隐式同步 透明
  • Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。

Java内存模型的抽象结构

  • 从抽象的角度看,JMM定义了线程和主内存之间的抽象关系:
    • 线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。
    • 本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
  • Java线程之间的通信由Java内存模型(JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。
    • JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证。
  • 在Java中,所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。局部变量,方法定义参数(Formal Method Parameter)和异常处理器参数(Exception Handler Parameter)不会再线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。

Java内存模型相关规范:JSR 133: JavaTM Memory Model and Thread Specification Revision

Java内存区域(运行时数据区)

Java内存区域一般指的值运行时数据区。Java虚拟机在执行Java程序的过程中会把它管理的内存划分成若干个不同的数据区域。JDK 1.8对运行时数据区有调整,所以和之前的版本略有不同。

在JDK 1.8版本之前,如果你不知道JVM规范定义的运行时数据区和HotSpot JVM对应的实现的区别还问题不大,当然,大部分博客也是这么做的。但是在MetaSpace出现之后,就必须要搞清楚这两者的区别,不然很难理解区分方法区、永久带、元空间。

JVM规范定义的运行时数据区

参考:JDK8的JVM规范文档

针对文档的运行时数据区我自己翻译了一版,里面详细的介绍了各个运行时数据区:The Java Virtual Machine Specification, Java SE 8 Edition(Java虚拟机规范)-运行时数据区(翻译)

规范中定义了6个运行时数据区,The pc Register(程序计数器)、Java Virtual Machine Stacks(Java虚拟机栈)、Heap(堆)、Method Area(方法区)、Run-Time Constant Pool(运行时常量池)、Native Method Stacks(本地方法栈)。

  • 原文也说了Each run-time constant pool is allocated from the Java Virtual Machine's method area. 运行时常量池是在JVM的方法区中分配的。

所以,这就是我们常看到的图:

  • 值得注意的是:规范中定义的这些运行时数据区都是抽象的概念,并没有限制具体的实现。所以具体的虚拟机实现的这几个运行时数据区不一定是相互隔离的。例如:栈帧可能是分配在堆中的(如:CLDC HI)。

具体的Java虚拟机中的运行时数据区(以HotSpot VM为例)

HotSpot VM简介

HotSpot VM是Sun JDK和OpenJDK中所带的虚拟机,是目前使用范围最广的Java虚拟机。

在2006年的JavaOne大会上,Sun公司宣布最终会把Java开源,并在随后的一年,陆续将JDK的各个部分(其中当然也包括了HotSpot VM)在GPL协议下公开了源码,
并在此基础上建立了OpenJDK。这样,HotSpot VM便成为了Sun JDK和OpenJDK两个实现极度接近的JDK项目的共同虚拟机。

在2008年和2009年,Oracle公司分别收购了BEA公司和Sun公司,这样Oracle就同时拥有了两款优秀的Java虚拟机:JRockit VM和HotSpot VM。
Oracle公司宣布在不久的将来会完成这两款虚拟机的整合工作,使之优势互补。(这也是JDK 1.8中对HotSpot VM运行时数据区进行大调整的一个背景之一)

HotSpot VM 结构

HotSpot VM将虚拟机栈和本地方法栈合二为一:

在JDK 1.8之前的版本,图上的方法区在HotSpot VM中的实现就是PermGen(永久代),在其他JVM上不存在永久代。在JDK 1.8的时候,HotSpot VM将方法区的实现替换成了Meatspace(元空间)。所以在本质上,永久代和元空间都是方法区的实现,只是它们的实现细节不一样。

  • 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出
  • 移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代
  • 移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是Native Heap。譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。

永久代和元空间的一些区别

在JVM规范中定义:Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. 方法区逻辑上属于堆的一部分。(JDK1.7之前HotSpot VM的方法区物理上也属于了堆的一部分)

  • 永久代分配在堆中(在JVM中),元空间使用直接内存(在JVM之外)
  • 永久代溢出:java.lang.OutOfMemoryError: PermGen space,元空间溢出:java.lang.OutOfMemoryError: Metaspace
  • 永久代GC和老年代GC是捆绑到,其中一个满了,都会触发两个区域的GC;元空间中类和其元数据的生命周期与其对应的类加载器相同,当某个类加载器不再存活,GC会把对应的空间整个回收(每个类加载器有单独的存储空间),不需要扫描压缩耗时的操作。

本文转载自: 掘金

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

Spring事务隔离级别

发表于 2020-06-23

事务隔离级别

ISOLATION_DEFAULT

这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别。

ISOLATION_READ_UNCOMMITTED

这是事务最低的隔离级别,它充许令外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读。

ISOLATION_READ_COMMITTED

保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。

ISOLATION_REPEATABLE_READ

这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了避免不可重复读。

ISOLATION_SERIALIZABLE

这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,不可重复读外,还避免了幻像读。

什么是脏数据,脏读,不可重复读,幻觉读?

脏读

一个事务修改了一行数据但是没有提交,第二个事务可以读取到这行被修改的数据,如果第一个事务回滚,第二个事务获取到的数据就是脏读。
帮助记忆:写读

不可重复读

一个事务读取到一行数据,第二个事务修改了这行数据,第一个事务重新读取证行数据将得到不同的值。因此称为是不可重复读。
帮助记忆:读写读

幻读

一个事务按照一个where条件读取所有符合的数据,第二个事务插入了一行数据且恰好也满足这个where条件,第一个事务再以这个where条件重新获取将会获取额外多出来的这一行。
帮助记忆:where insert where

本文转载自: 掘金

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

1…800801802…956

开发者博客

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