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

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


  • 首页

  • 归档

  • 搜索

字节面试官叫我手写HashMap,我两分钟就给他整出来了!!

发表于 2021-06-08

前言

今天看面经看得到大厂面试题,说实话HashMap感觉真的很了解,源码看了也很多遍了,相信大部分小伙伴都能有这个程度。但是,突然给你来这么一手,还是有点懵圈!所以,今天给各位小伙伴整理一下,帮助各位掌握!

正常来说,面试时遇到手写HahsMap,基本上都是要求实现get、put方法,我就稍微全面那么一点,再加一个remove方法。

JDK7、8HashMap的get()、put()方法流程

JDK7、8HashMap扩容详解

重点掌握

手写LRU、LFU,让面试官对你刮目相看!!!

HashMap在JDK7、JDK8中不安全的地方到底在哪里???

好了好了,话不多说,我们开始吧。


撸起袖子开始造

实现数组 + 链表

众所周知,HashMap无论是JDK7还是JDK8,底层都是数组 + 链表,只是JDK8多了一个红黑树,按道理讲不会还有手写红黑树的吧,😰。

既然是数组 + 链表,那我就实现一个链表

参考JDK8源码

在这里插入图片描述

我们就没必要那么高级,🤭

1
2
3
4
5
6
7
8
9
java复制代码static class Node {
int key, value; //保存该节点的Key、Value
Node next; //指向下一个节点

public Node(int key, int value) {
this.key = key;
this.value = value;
}
}

链表有了,就再来个数组(面试过程基本上不要求扩容,所以我们就直接给数组定义一个差不多的值就OK了)

1
2
java复制代码private final int CAPACITY = 10000;
Node[] nodes = new Node[CAPACITY];

实现获取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
java复制代码public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}


final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}

图示讲解

在这里插入图片描述

实现

因为int为基本数据类型,所以我们用==Integer.hashCode(int value)==

而Integer.hashCode(int value),返回的其实就是你传入的值

1
2
3
java复制代码public static int hashCode(int value) {
return value;
}
1
2
3
4
5
6
7
python复制代码private int getIndex(int key) {
int hash = Integer.hashCode(key);
//高16位异或低16位
hash ^= (hash >>> 16);
//与数组长度取模,得到对应的索引下标
return hash % CAPACITY;
}

实现get方法

流程很简单

  1. 获取到传入的 key 的对应的索引下标
  2. 拿到对应下标对应的链表首节点
  3. 非空判断
  4. 如果链表首节点的Key是目标Key,那么直接返回对应的Value值;如果不是,那么对链表进行遍历获取,如果遍历完成都没有去返回Value值,那么说明HashMap没有这个数据,那么就返回-1.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public int get(int key) {
int idx = getIndex(key);
Node now = nodes[idx];

if (now != null) {
if (now.key == key) {
return now.value;
} else {
while (now != null) {
if (now.key == key) {
return now.value;
}
now = now.next;
}
}
}

return -1;
}

参考源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
python复制代码final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {

//如果是首节点,那么直接返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
//红黑树判断不用管
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//遍历获取
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//如果还没有就返回null值
return null;
}

实现put方法

流程介绍

注意:我们需要保存前一个节点,这样如果put的是一个新键值对的话,我们可以获取到链表的最后一个不为null的节点

  1. 获取Key对应的索引值
  2. 非空判断,如果为空,说明该索引对应的链表为空,可直接创建新节点添加
  3. 不为空则循环遍历,遍历过程更新 prev ,如果遍历过程中找到则返回value值
  4. 如果遍历完成还没有返回,说明没有该节点可以添加,那么根据 prev 是否为null进行添加;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码public void put(int key, int value) {
int idx = getIndex(key);
Node now = nodes[idx], tmp = now;

if (tmp != null) {
Node prev = null;
while (tmp != null) {
if (tmp.key == key) {
tmp.value = value;
return;
}
prev = tmp;
tmp = tmp.next;
}
tmp = prev;
}

Node node = new Node(key, value);
if (tmp != null) {
tmp.next = node;
} else {
nodes[idx] = node;
}
}

实现remove方法

大致流程跟get方法差不多,区别就是我们我们需要==保存需要删除节点的前一个节点==

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
python复制代码 public void remove(int key) {
//得到索引
int idx = getIndex(key);
//拿到首节点
Node now = nodes[idx];

//非空判断
if (now != null) {
//保存前节点
Node prev = null;
//遍历查找
while (now != null) {
//如果找到
if (now.key == key) {
//这里有两种情况
//1. 如果要删除的节点是首节点,那么直接让当前数组下标对应的首节点位为其下一个节点
//2. 如果不是,那么让前一个节点的下一个节点指向当前要删除节点的下一个节点就实现了删除效果
if (prev != null) {
prev.next = now.next;
}else {
nodes[idx] = now.next;
}
//不管是怎么删除的,都让当前节点的下一个节点为null,方便垃圾挥手(加分点哦)
now.next = null;
return;
}
//如果没找到,让前节点指向当前节点,当前节点指向其下一个节点
prev = now;
now = now.next;
}
}
}

测试一下

1
2
3
4
5
6
7
8
9
10
java复制代码public static void main(String[] args) {
MyHashMap map = new MyHashMap();
map.put(1,1);
map.put(2,2);
map.put(1,40);
map.put(2,200);

System.out.println(map.get(1));
System.out.println(map.get(2));
}

在这里插入图片描述


完整代码

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
java复制代码public class MyHashMap {

static class Node {
int key, value;
Node next;

public Node(int key, int value) {
this.key = key;
this.value = value;
}
}

private final int CAPACITY = 10000;
Node[] nodes = new Node[CAPACITY];

public void put(int key, int value) {
int idx = getIndex(key);
Node now = nodes[idx], tmp = now;
if (tmp != null) {
Node prev = null;
while (tmp != null) {
if (tmp.key == key) {
tmp.value = value;
return;
}
prev = tmp;
tmp = tmp.next;
}
tmp = prev;
}

Node node = new Node(key, value);
if (tmp != null) {
tmp.next = node;
} else {
nodes[idx] = node;
}
}

public int get(int key) {
int idx = getIndex(key);
Node now = nodes[idx];

if (now != null) {
if (now.key == key) {
return now.value;
} else {
while (now != null) {
if (now.key == key) {
return now.value;
}
now = now.next;
}
}
}

return -1;
}

public void remove(int key) {
int idx = getIndex(key);
Node now = nodes[idx];

if (now != null) {
Node prev = null;
while (now != null) {
if (now.key == key) {
if (prev != null) {
prev.next = now.next;
}else {
nodes[idx] = now.next;
}
now.next = null;
return;
}
prev = now;
now = now.next;
}
}
}

private int getIndex(int key) {
int hash = Integer.hashCode(key);
hash ^= (hash >>> 16);
return hash % CAPACITY;
}

}

CSDN独家福利降临!!!

最近CSDN有个独家出品的活动,也就是下面的《Java的全栈知识图谱》,路线规划的非常详细,尺寸 是870mm x 560mm 小伙伴们可以按照上面的流程进行系统的学习,不要像我当初一样没人带自己随便找本书乱学,系统的有规律的学习,它的基础才是最扎实的,在我们这行,《基础不牢,地动山摇》尤其明显。

最后,如果有兴趣的小伙伴们可以酌情购买,为自己的未来铺好道路!!!

在这里插入图片描述


最后

我是 Code皮皮虾,一个热爱分享知识的 皮皮虾爱好者,未来的日子里会不断更新出对大家有益的博文,期待大家的关注!!!

创作不易,如果这篇博文对各位有帮助,希望各位小伙伴可以==一键三连哦!==,感谢支持,我们下次再见~

分享大纲

大厂面试题专栏

Java从入门到入坟学习路线目录索引

开源爬虫实例教程目录索引

更多精彩内容分享,请点击 Hello World (●’◡’●)


在这里插入图片描述

本文转载自: 掘金

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

助力碳中和,EMQ 与 SAP 共同构建绿色 IoT 解决方

发表于 2021-06-08

背景

在气候保护全球合作的大背景下,中国政府积极推动2030“碳达峰”与 2060“碳中和”。“双碳”战略的达成将对中国经济社会发展带来深刻影响。相关行业企业面临转型升级的重大挑战,特别是能源电力、工业制造、交通等高碳排放的行业,必须通过更加精细、智能的数字化管理手段,对碳足迹加以把控,以实现在产业链各环节中的逐步脱碳。

为助力“碳中和”的早日实现和相关企业的竞争力重塑,SAP 与 EMQ 达成合作,双方将利用各自在智慧企业转型与物联网数据接入领域的技术优势,共同构建面向全产业链的碳排放数字化平台。借助数字化与物联网技术帮助企业进行能耗排放监控管理、产业转型升级和新能源产业数字化转型,为工业节能减排、风光发电清洁能源生产、新能源车联网等领域提供数字化解决方案。

该方案通过 EMQ X IIoT 套件规则引擎对接 SAP BTP 平台 Event Mesh 组件,传输碳中和相关数据到 SAP BTP 平台进行分析。

EMQ X IIoT 套件

EMQ X IIoT 套件是一套接入与流数据预处理的工业物联网基础套件,套件包含多协议全网络设备接入能力的 EMQ X Neuron 和千万并发连接、百万数据吞吐的 EMQ X Enterprise 等组件。可依靠强大的规则引擎传输数据到各种数据平台,实现一站式的数据提取、筛选、转换和处理,与其它各类平台进行完美适配集成。同时支持可弹性扩展的集群模式,企业可以随着业务增长在客户无感知的情况下拓展接入规模,通过增加集群节点扩展业务上限。EMQ X IIoT 套件 灵活集成 SAP BTP 平台进行数据传输,提供可靠的碳达峰/碳中和源数据。

SAP BTP 平台

SAP BTP 是打造智慧企业的平台,借助此平台,客户能够集成和扩展所有 SAP 和第三方的应用,化数据为价值,从而提升敏捷性、实现业务价值并推动持续创新。 SAP BTP 囊括了 SAP 的所有技术组合,例如 SAP HANA(内存计算平台)、SAP Analytics Cloud(分析云)、SAP Integration 套件 (集成套件)和 SAP Extension 套件(扩展套件)。SAP BTP 平台可以把 EMQ 采集的能耗数据转化成有价值的业务成果,更好地为碳达峰/碳中和提供有利的数据调整支撑。

SAP & EMQ 碳中和集成方案

在碳中和集成方案中,EMQ 提供了对工业能耗数据、生产制造过程数据、排放数据以及新能源生产等数据的实时采集接入,通过灵活的规则引擎把接入数据按业务需求进行过滤,处理,计算,通过高速实时的数据桥接,把有价值的数据通过 SAP Event Mesh 进一步分发到相应的消费组件,例如通过 SAP Analytics Cloud 或者 SAP HANA Cloud 把数据转化成碳中和分析数据,从而为企业与监管部门提供从生产制造到碳排量、碳足迹分析的一站式服务。

通过 EMQ X IIoT 套件采集数据后,利用 SAP BTP 平台的数据存储,数据处理和报表展现等能力,快速上线碳达峰/碳中和智慧解决方案。

1.png

方案架构图


有关 SAP 账号权限、SAP BTP、Event Mesh 、SAC、EMQ 规则引擎使用等概念及要点可见官方文档:

  • SAP 账号权限介绍
  • SAP Event Mesh 介绍
  • SAC 介绍
  • EMQ 规则引擎

1.启用 Cloud Foundry 环境,作为服务的运行容器,创建一个 Space

2.png

2.订阅 Event Mesh 服务,并在 Space 里面创建 Event Mesh 服务实例

3.png

3.创建 Service Key, 创建 Queue, 基于 Service Key 生成 Token,使用 Token 进行 EMQ 的连接
4.png

4.配置 EMQ X 规则引擎,把数据输出到 SAP Event Mesh,目前是 EMQ 是通过 post API 的方式连接到 SAP Event Mesh,在 EMQ 下一个大版本里面,将更好与 SAP Event Mesh 集成,可以直接添加 SAP Event Mesh 资源,更加方便快捷。

5.png

6.png

5.查看 SAP Event Mesh 里面的数据,需要使用 Event Mesh 的 REST API 消费进行查询。

7.png

6.存储在 SAP HANA Cloud 的能耗数据,可以供 SAP Analytics Cloud、S4/HANA,自开发 App 等应用消费展示。
8.png

未来展望

随着 SAP 与 EMQ 深度合作,利用 EMQ 在 IoT 领域数据接入、预处理和分类存储等能力优势,SAP 在业务层把数据转化成对碳中和价值分析数据,通过对转化为标准碳排放的数据分析,在工业、运输能耗等领域进行碳排放管理,将对碳排放治理起到关键指导性价值。

“双碳战略”作为未来长期的战略方向,既是全球社会性的问题,也是经济效益问题。SAP 与 EMQ 的碳中和深度集成方案,不仅可以帮助企业完成转型升级,节约企业成本,还能通过带动绿色经济,提高全民的生活质量和身体健康,为数字化、绿色智能化建设技术赋能。

关于 SAP

SAP 于1972年在德国创立,是全球商业 软件市场的领导厂商,SAP 起源于Systems Applications and Products in Data Processing。SAP 既是公司名称,又是其产品——企业管理解决方案的软件名称。SAP 是目前全世界排名第一的 ERP 软件。另有,计算机用语 SAP,同时也是 Stable Abstractions Principle(稳定抽象原则)的简称。SAP 为全球第三大(根据市值排名)独立软件制造商。在全球120多个国家拥有10万+的企业客户,并在包括 欧洲、美洲、中东及亚太地区的50个国家雇用52000多名员工。

版权声明: 本文为 EMQ 原创,转载请注明出处。

原文链接:www.emqx.com/zh/blog/emq…

本文转载自: 掘金

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

基于Java (spring-boot)和微信小程序的校园闲

发表于 2021-06-08
  1. 总体功能图

1用户端

(1)用户信息模块 用户注册登录、个人信息维护

(2)闲置信息模块 闲置信息的发布、查询

(3)留言模块 实时留言功能

(4)关注用户 实时了解关注用户动态

image.png

2管理端

1
2
3
4
5
6
7
复制代码(1)用户端功能

(2)用户信息管理 用户信息管理

(3)发布信息管理 发布信息管理

(4)数据统计模块 统计系统信息及交易信息
  1. 技术选型与架构

前端采用的微信小程序,后端采用spring-boot,spring-data-jpa,数据库采用Mysql,图表采用的百度Echarts和微信小程序结合,评论采用二级评论,微信朋友圈图片发布样式(多图片上传),未读消息红点气泡展示。

软件:IDEA mysql 微信开发者工具

  1. 系统架构

image.png

  1. 数据库表设计

image.png

5 效果图

5.1 用户模块

个人中心运行效果图如图5-1所示。

image.png

图5-1 个人中心效果图

5.2 闲置信息模块

闲置发布运行页面如图5-2所示。(微信朋友圈图片发布样式)

image.png

图5-2 闲置发布效果图

闲置列表和闲置详情运行效果图如图5-3、5-4所示。

image.png

图5-3 闲置列表效果图

image.png

图5-4 闲置详情效果图

5.3 评论模块(二级评论)

评论列表和消息通知运行效果图如图5-5、5-6所示。

image.png

图5-5评论列表效果图 (未读消息红点气泡展示)

image.png

图5-6 消息通知效果图

5.4 关注模块

关注管理页面和关注人闲置列表页面运行效果图如图5-7、5-8所示。

image.png

图5-7 关注管理页面效果图

image.png

图5-8 关注人闲置列表效果图

5.5 统计模块

概况统计和商品类别统计运行效果图如图5-10所示。

image.png

图5-10 商品类别统计效果图

5.6 广告及活动模块(仅做banner详情展示,可忽略)

广告及活动页面运行效果图如图5-11所示。

image.png

图5-11 广告及活动页面效果图

小程序介绍视频(当时参加比赛要求录得):v.qq.com/x/page/a067…

以此来给各位做相关内容时,提供一点思路和架构设计,若有不对,请多包涵和指正。


个人github:github.com/Gaojiajie

不喜欢篮球的摄影师不是一个好程序员

本文转载自: 掘金

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

【20216小白向】使用腾讯云轻量服务器快速搭建一个属于自

发表于 2021-06-08

前言
前几天有同学来问我如何搭建一个跟我一样的博客,于是特意写了这个玩转云服务器系列专题,来帮助一些刚入门的同学.

购买云服务器和域名
我这里使用的是腾讯云的轻量应用服务器.

腾讯云的学生优惠用来购买一台服务器很合算:点击进入腾讯云学生云服务器

或者购买轻量应用服务器Lighthouse

最近的618活动也有很多不错的机型:2021年618云上Go

需要注意的是,域名购买后如果要在国内的机器解析,域名必须要备案.

如果你不想备案,不想等待备案的时间,想要购买域名后直接部署博客,可以选择购买腾讯云香港的轻量服务器这类的海外机器,速度也很快.

安装环境
如果你喜欢图形化界面,可以选择宝塔面板

选择适合你系统的安装指令,在ssh中执行即可

image.png
如果你更喜欢命令行,更在乎安全,你可以选择军哥的lnmp一键安装脚本或者OneinStack自动安装脚本

image.png

image.png
以下我用宝塔面板做演示

安装成功后如图:

image.png
需要注意的安全组设置问题

我们可与看到,宝塔面板使用了8888端口,而一些云服务器厂家可能并不会默认开启这个端口,这就需要我们到安全组中放行这个端口,我们才可以访问

到腾讯云的后台安全组中放行8888端口,这里我直接放行了所有端口.不放心的同学可以仅放行8888端口

image.png
点击外网地址进入宝塔面板后,如果你不想登录宝塔账号以防隐私泄露,可以看我的下面这篇文章

www.wangfuchao.com/645/
进入面板后,根据自己机器的配置选择适合的环境 1核2g的机器按照下图即可,如果内存高,可以把mysql换成5.7版本

image.png
安装时间随机器配置有所不同

域名解析
我是在腾讯云购买的域名,在dnspod进行解析操作

这里我将主域名解析到我的腾讯云服务器ip

image.png
@代表主域名,记录值填写服务器公网ip,选择A类型.

建立网站
这时,我们就可以开始搭建网站了

首先我们去wordpress官网下载程序安装包到自己的电脑上,因为我用腾讯云北京的服务器wget下载太慢了…所以我们迂回一下,先下载到本地电脑上,然后再上传到自己的服务器上

image.png
下载完成后,我们打开宝塔面板.

点击网站→添加站点

域名填写自己刚才解析的域名,数据库选择MySQL,php版本选择刚才安装的php7.4,点击提交

image.png
之后我们进入网站根目录,将里面的文件都删除,然后上传wordpress的程序包,并解压它,然后将解压的文件都放到网站的根目录

最后如图所示:

image.png
之后我们点击网站,点击我们的网站网址,选择伪静态,在下拉栏里选择wordpress并保存(我这里伪静态是Apache的,跟nginx的不一样,同学们忽略即可)

image.png
如果你想让网站https访问,就是地址栏那里出现一个小锁,就需要点击SSL,申请一个免费证书.

image.png
访问网站
到这里就全部完成了

我们输入网站的网址,进行最后一步的程序安装配置,顺着往下走就行了

选择简体中文

填写数据库名、用户名和密码,别的不用管

image.png
填写你的站点信息

image.png
好了,属于自己的一个博客就搭建好了.

你可能发现你的博客样式跟我的不一样,这是主题的不同.

这里我推荐一款免费开源的主题,也挺好看的,地址是github.com/Licoy/wordp…
原文链接:www.wangfuchao.com/1374/

本文转载自: 掘金

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

如何优雅的使用easy excel

发表于 2021-06-08

问题痛点:一般都过easy excel 实现导入功能,不同的业务需要写不同的listener,这显然不够优雅,那么就让我们讨论下如何优雅的使用easy excel吧,优雅永不过时!

1、首先引入easy excel pom配置

1
2
3
4
5
6
7
xml复制代码<!--easy excel start-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.2.6</version>
</dependency>
<!--easy excel end-->

2、定义自己的function函数(不定义的话,可以使用java8 的consumer函数,但由于我们导入一般为list,使用consumer使用起来不够优雅)

1
2
3
4
5
6
7
8
9
10
java复制代码/**
* @author :Nickels
* @date :2020/7/27
* @desc :
*/
@FunctionalInterface
public interface ExcelConsumer<E> {

void excute(List<E> e);
}

3、编写listener

①listener基本根据git官网照抄的,唯一不同的是我们是以我们自定义的function作为一个变量

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
csharp复制代码/**
* @author :Nickels
* @date :2020/7/27
* @desc :
*/
// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
public class ExcelListener<T> extends AnalysisEventListener<T> {

/**
* 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 1000;
List<T> list = new ArrayList<T>();
/**
* 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。这里我们传入自己的function作为参数
*/
ExcelConsumer consumer;

public ExcelListener() {
}

/**
* 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
*/
public ExcelListener(ExcelConsumer consumer) {
this.consumer = consumer;
}


@Override
public void invoke(T t, AnalysisContext analysisContext) {
list.add(t);
if (list.size() > BATCH_COUNT){
//超出界限值,保存数据库,避免oom
//执行函数
consumer.excute(list);

// 存储完成清理 list
list.clear();

}
}

@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
// 这里也要保存数据,确保最后遗留的数据也存储到数据库
consumer.excute(list);
}

}

4、自定义导入/导出模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
less复制代码/**
* @author :Nickels
* @date :2020/7/27
*/
@Getter
@Setter
public class SendAddressTemplate {

@ExcelProperty(value = "公司名称",index = 0)
private String companyName;

@ExcelProperty(value = "姓名",index = 1)
private String name;

@ExcelProperty(value = "电话",index = 2)
private String phone;

@ExcelProperty(value = "详细地址",index = 3)
private String addressDetail;

@ExcelProperty(value = "备注",index = 4)
private String remak;

}

5、导入使用

1
2
3
4
scss复制代码EasyExcel.read(file.getInputStream(), SendAddressTemplate.class,new ExcelListener(p->{
//保存逻辑(可封装一个普通的sevice)p-为我们执行函数时传入的list
//sendAddressService.importExcel(p);
})).sheet().doRead();

本文转载自: 掘金

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

SpringBoot整合Flowable工作流-2(代码整合

发表于 2021-06-08
  1. 前言

上一篇博客【SpringBoot整合Flowable工作流-1(画流程定义) 】介绍用 Flowable-ui 画了一个简单的流程图。


这篇博客将介绍代码整合部分,主要内容有:【发布流程定义】、【开启流程任务】、【获取用户任务】、【用户审批任务】、【添加审批意见】、【获取流程图】、【获取我的待办任务】、【获取我发起的流程】、【我审批过的流程】…

  1. 代码添加依赖

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter</artifactId>
<version>6.4.2</version>
</dependency>
  1. 创建数据库

创建数据库可以使用通过 Flowable 提供的 sql 实现,也可以通过程序自动创建数据库实现

3.1 Flowable 提供的 sql

下载文件 Flowable 相关的资源,进入 flowable.com/open-source…,然后点击 【Download Flowable v6.x.x】,下载下来是一个压缩包,解压后会看到如下目录结构

1
2
3
4
python复制代码└─database                                   # 数据库文件
└─create
└─all
└─flowable.mysql.all.create.sql

找到 $/database/create/database/create/flowable.mysql.all.create.sql 文件,导入mysql数据库即可

3.2 应用程序自动创建数据库(推荐)

需要在 jdbc 的 url 中添加一个参数值,nullCatalogMeansCurrent=true

如下:

1
ini复制代码jdbc:mysql://127.0.0.1:3306/flowable?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false&nullCatalogMeansCurrent=true
  1. 代码部分

4.1 常用的几个Service类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码/** 运行时Service(用于运行时流程实例、流程变量、流程节点) */
@Autowired
private RuntimeService runtimeService;
/** 资源存储Service(用于模型、流程定义) */
@Autowired
private RepositoryService repositoryService;
/** 流程引擎Service */
@Qualifier("processEngine")
@Autowired
private ProcessEngine processEngine;
/** 任务Service(用于运行时的用户任务、审批日志) */
@Autowired
private TaskService taskService;
@Autowired
protected ManagementService managementService;
/** 历史Service(用于历史记录,可以找到历史的流程实例、流程节点) */
@Autowired
protected HistoryService historyService;

4.2 流程定义相关代码

4.2.1 发布流程定义

上一篇博客【SpringBoot整合Flowable工作流-1(画流程定义) 】画好了流程,然后下载下来是一个 “请假流程1.bpmn20.xml” 的xml文件,下载就可以通过代码把这个流程发布到流程定义中了。

代码如下

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
java复制代码 @Override
public boolean importProcessDefinition(MultipartFile file) {
try {
Deployment deployment = repositoryService.createDeployment()
// .key()
// .name(name)
// .category(category)
// .tenantId()
// 通过压缩包的形式一次行多个发布
// .addZipInputStream()
// 通过InputStream的形式发布
.addInputStream(file.getOriginalFilename(), file.getInputStream())
// 通过存放在classpath目录下的文件进行发布
// .addClasspathResource("p1.bpmn20.xml")
// 通过xml字符串的形式
// .addString()
.deploy();
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().deploymentId(deployment.getId()).singleResult();
if (processDefinition == null) {
return false;
}
} catch (Exception e) {
throw new BusinessException("导入流程定义失败:" + e.getMessage());
}
return true;
}

基于 SpringBoot 发布流程定义,还有一种巧妙的形式,那就是在 resources 目录下建立一个文件夹 processes ,然后把对应的流程文件发到这个文件夹下即可,启动 SpringBoot 项目的时候,通过观察日志就会发现该流程就自动发布了。(不推荐)

1
2
3
4
5
6
7
8
css复制代码workflow-server
└─src
└─main
├─java
└─resources
├─mapper
└─processes
└─请假流程1.bpmn20.xml

4.2.2 查询流程定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码// 创建 ProcessDefinitionQuery 
ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();
// 根据流程定义ID
// processDefinitionQuery.processDefinitionId(requestDTO.getId());
// 根据流程定义Key
// processDefinitionQuery.processDefinitionKeyLike("%" + requestDTO.getKey().trim() + "%");
// 根据流程定义名称
// processDefinitionQuery.processDefinitionNameLike("%" + requestDTO.getName().trim() + "%");
//
// 获取总数
long count = processDefinitionQuery.count();
// 获取单个
ProcessDefinition processDefinition = processDefinitionQuery.singleResult();
// 获取列表
List<ProcessDefinition> processDefinitions = processDefinitionQuery.list();
// 获取分页
List<ProcessDefinition> processDefinitions = processDefinitionQuery.listPage(0, 10)

4.2.3 获取流程定义xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public String getXmlResource(String id) {
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionId(id).singleResult();
InputStream inputStream = repositoryService.getResourceAsStream(processDefinition.getDeploymentId(),
processDefinition.getResourceName());
try {
return IOUtils.toString(inputStream, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new BusinessException("获取资源失败:" + e.getMessage());
} finally {
try {
IOUtils.close(inputStream, null);
} catch (IOException ignored) {
}
}
}

4.2.4 获取流程定义图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码public String getDiagramImageResource(String id) {
// 理论上我用这种形式也是行的,但是我获取出来会有乱码,我也比较奇怪,所以换了通过 bpmnModel 的方式
// ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionId(id).singleResult();
// InputStream inputStream = repositoryService.getResourceAsStream(processDefinition.getDeploymentId(),
// processDefinition.getDiagramResourceName());
// 获取bpmnModel对象
BpmnModel bpmnModel = repositoryService.getBpmnModel(id);
// 生成图片流
ProcessEngineConfiguration configuration = processEngine.getProcessEngineConfiguration();
ProcessDiagramGenerator diagramGenerator = configuration.getProcessDiagramGenerator();
InputStream inputStream = diagramGenerator.generateDiagram(bpmnModel, "png", Collections.emptyList(),
Collections.emptyList(), "宋体", "宋体", "宋体",
this.getClass().getClassLoader(), 1.0, true);
try {
return "data:image/png;base64," + Base64.encode(inputStream);
} catch (Exception e) {
throw new BusinessException("获取资源失败:" + e.getMessage());
} finally {
try {
IOUtils.close(inputStream, null);
} catch (IOException ignored) {
}
}
}

4.2.5 删除流程定义

1
2
3
4
5
6
java复制代码public void deleteByIds(List<String> ids) {
List<ProcessDefinition> processDefinitions = repositoryService.createProcessDefinitionQuery().processDefinitionIds(new HashSet<>(ids)).list();
// repositoryService.deleteDeployment(String deploymentId, boolean cascade)
// 删除给定的部署和级联删除流程实例、历史流程实例和作业。
processDefinitions.forEach(v -> repositoryService.deleteDeployment(v.getDeploymentId(), true));
}

4.3 流程实例相关代码

4.3.1 添加流程实例审批意见

起到类似于记录流程的操作记录的作用,我这里是自己封装了一层,我封装了自己的业务用户ID、用户名、执行类型、意见内容…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@Data
@ApiModel("流程实例-审批意见请求参数")
public class ProcessInstanceCommentRequestDTO implements Serializable {

@ApiModelProperty(value = "流程定义Key")
private String processInstanceId;
@ApiModelProperty(value = "任务ID(缺省)")
private String taskId;
@ApiModelProperty(value = "类型 CommentEntity: event/comment")
private String type;

@ApiModelProperty(value = "用户ID")
private String userId;
@ApiModelProperty(value = "用户昵称")
private String nickname;
@ApiModelProperty(value = "执行类型")
private String executeType;
@ApiModelProperty(value = "执行类型(参考ExecuteTypeEnum)SUBMIT-提交;YES-同意;NO-拒绝;STOP-流程终止;DELETE-流程删除")
private String executeTypeValue;
@ApiModelProperty(value = "内容")
private String content;
@ApiModelProperty(value = "额外携带的内容")
private String ext;
}

添加流程实例审批意见

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码// 添加流程实例审批记录
public void addProcessInstanceComment(ProcessInstanceCommentRequestDTO requestDTO) {
CommentWrapper wrapper = new CommentWrapper();
wrapper.setUserId(requestDTO.getUserId());
wrapper.setNickname(requestDTO.getNickname());
wrapper.setExecuteType(requestDTO.getExecuteType());
wrapper.setExecuteTypeValue(requestDTO.getExecuteTypeValue());
wrapper.setContent(requestDTO.getContent());
wrapper.setExt(requestDTO.getExt());
String message = JSON.toJSONString(wrapper);
// 使用 taskService 添加一条审批意见
taskService.addComment(requestDTO.getTaskId(), requestDTO.getProcessInstanceId(), requestDTO.getType(), message);
}

4.3.2 启动流程实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Data
@ApiModel("流程实例-启动请求参数")
public class ProcessInstanceStartRequestDTO implements Serializable {

@ApiModelProperty(value = "流程定义Key")
@NotEmpty(message = "流程定义Key 不可以为空")
private String processDefinitionKey;
@ApiModelProperty(value = "流程实例名称")
@NotEmpty(message = "流程实例名称 不可以为空")
private String name;
@ApiModelProperty(value = "项目ID")
@NotEmpty(message = "项目ID 不可以为空")
private String communityId;
@ApiModelProperty(value = "全局变量")
private Map<String, Object> variables;

}

启动流程实例

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
java复制代码@Transactional(rollbackFor = Exception.class)
public ProcessInstanceStartResponseDTO startProcessInstance(ProcessInstanceStartRequestDTO requestDTO) {
ValidatorUtils.validate(requestDTO);
//
ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();
processDefinitionQuery.processDefinitionKey(requestDTO.getProcessDefinitionKey());
ProcessDefinition processDefinition = processDefinitionQuery.latestVersion().singleResult();
AssertUtils.notEmpty(processDefinition, "找不到流程定义");
// 启动流程
ProcessInstanceBuilder builder = runtimeService.createProcessInstanceBuilder();
builder.processDefinitionKey(requestDTO.getProcessDefinitionKey());
builder.name(requestDTO.getName());
// variables("name", "value")
// 可以添加流程过程的变量,比如这里我添加了我不少业务变量进来,方面流程流转的时候处理业务
builder.variables(requestDTO.getVariables());
builder.variable(VAR_COMMUNITY_ID, VAR_COMMUNITY_ID_EQ + requestDTO.getCommunityId());
builder.variable(VAR_PROCESS_INSTANCE_NAME, VAR_PROCESS_INSTANCE_NAME_EQ + requestDTO.getName());
builder.variable(VAR_CREATE_USER_ID, VAR_CREATE_USER_ID_EQ + SecurityUser.getUserId());
builder.variable(VAR_CREATE_USER_NICKNAME, VAR_CREATE_USER_NICKNAME_EQ + SecurityUser.get().getName());
builder.variable(VAR_PROCESS_DEFINITION_NAME, VAR_PROCESS_DEFINITION_NAME_EQ + processDefinition.getName());
builder.transientVariables(requestDTO.getTransientVariables());
// builder.tenantId("101");
ProcessInstance processInstance = builder.start();
// 添加审批意见
ProcessInstanceCommentRequestDTO commentRequestDTO = new ProcessInstanceCommentRequestDTO();
commentRequestDTO.setProcessInstanceId(processInstance.getProcessInstanceId());
commentRequestDTO.setTaskId(null);
commentRequestDTO.setUserId(SecurityUser.getUserId());
commentRequestDTO.setNickname(SecurityUser.get().getName());
commentRequestDTO.setExecuteType(ExecuteTypeEnum.SUBMIT.name());
commentRequestDTO.setExecuteTypeValue(ExecuteTypeEnum.SUBMIT.getValue());
commentRequestDTO.setContent(requestDTO.getName() + " 提交流程");
commentRequestDTO.setExt("");
this.addProcessInstanceComment(commentRequestDTO);
// 构建 ResponseDTO
ProcessInstanceStartResponseDTO responseDTO = new ProcessInstanceStartResponseDTO();
responseDTO.setProcessInstanceId(processInstance.getProcessInstanceId());
responseDTO.setProcessDefinitionId(processInstance.getProcessDefinitionId());
responseDTO.setProcessDefinitionKey(processInstance.getProcessDefinitionKey());
responseDTO.setProcessDefinitionName(processInstance.getProcessDefinitionName());
responseDTO.setName(processInstance.getName());
responseDTO.setBusinessKey(processInstance.getBusinessKey());
responseDTO.setDescription(processInstance.getDescription());
//
return responseDTO;
}

4.3.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
java复制代码public String getProcessImage(String processInstanceId) {
// 1.获取当前的流程实例
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
String processDefinitionId;
List<String> activeActivityIds = new ArrayList<>();
List<String> highLightedFlows = new ArrayList<>();
// 2.获取所有的历史轨迹线对象
List<HistoricActivityInstance> historicActivityInstances = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(processInstanceId).activityType(BpmnXMLConstants.ELEMENT_SEQUENCE_FLOW).list();
historicActivityInstances.forEach(historicActivityInstance -> highLightedFlows.add(historicActivityInstance.getActivityId()));
// 3. 获取流程定义id和高亮的节点id
if (processInstance != null) {
// 3.1 正在运行的流程实例
processDefinitionId = processInstance.getProcessDefinitionId();
activeActivityIds = runtimeService.getActiveActivityIds(processInstanceId);
} else {
// 3.2 已经结束的流程实例
HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery()
.processInstanceId(processInstanceId).singleResult();
processDefinitionId = historicProcessInstance.getProcessDefinitionId();
// 3.3 获取结束节点列表
List<HistoricActivityInstance> historicEnds = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(processInstanceId).activityType(BpmnXMLConstants.ELEMENT_EVENT_END).list();
List<String> finalActiveActivityIds = activeActivityIds;
historicEnds.forEach(historicActivityInstance -> finalActiveActivityIds.add(historicActivityInstance.getActivityId()));
}
// 4. 获取bpmnModel对象
BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId);
// 5. 生成图片流
ProcessEngineConfiguration configuration = processEngine.getProcessEngineConfiguration();
ProcessDiagramGenerator diagramGenerator = configuration.getProcessDiagramGenerator();
InputStream inputStream = diagramGenerator.generateDiagram(bpmnModel, "png", activeActivityIds,
highLightedFlows, "宋体", "宋体", "宋体",
this.getClass().getClassLoader(), 1.0, true);
// 6. 转化成Base64网络传输
return "data:image/png;base64," + Base64.encode(inputStream);
}

4.3.4 获取流程审批意见

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码@Override
public List<ProcessInstanceCommentResponseDTO> findProcessInstanceCommentList(String processInstanceId) {
List<Comment> processInstanceComments = taskService.getProcessInstanceComments(processInstanceId);
List<ProcessInstanceCommentResponseDTO> list = processInstanceComments.stream()
.map(this::convertComment)
.filter(Objects::nonNull).collect(Collectors.toList());
return list;
}

private ProcessInstanceCommentResponseDTO convertComment(Comment v) {
String fullMessage = v.getFullMessage();
if (StringUtils.startsWith(fullMessage, "{")) {
CommentWrapper wrapper = JSON.parseObject(fullMessage, CommentWrapper.class);
ProcessInstanceCommentResponseDTO responseDTO = new ProcessInstanceCommentResponseDTO();
responseDTO.setProcessInstanceId(v.getProcessInstanceId());
responseDTO.setType(v.getType());
responseDTO.setTaskId(v.getTaskId());
responseDTO.setTime(v.getTime());
responseDTO.setUserId(wrapper.getUserId());
responseDTO.setNickname(wrapper.getNickname());
responseDTO.setExecuteType(wrapper.getExecuteType());
responseDTO.setExecuteTypeValue(wrapper.getExecuteTypeValue());
responseDTO.setContent(wrapper.getContent());
responseDTO.setExt(wrapper.getExt());
return responseDTO;
}
return null;
}

4.3.5 流程实例执行下一步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@Data
@ApiModel("流程实例-执行下一步请求参数")
public class ProcessInstanceExecuteNextStepRequestDTO implements Serializable {

@ApiModelProperty(value = "流程实例ID")
@NotEmpty(message = "流程实例ID 不可以为空")
private String processInstanceId;
@ApiModelProperty(value = "任务ID")
private String taskId;
@ApiModelProperty(value = "ExecuteTypeEnum 执行类型")
@NotEmpty(message = "执行类型 不可以为空")
private String executeType;
@ApiModelProperty(value = "审批意见")
@NotEmpty(message = "审批意见 不可以为空")
private String commentContent;
@ApiModelProperty(value = "变量参数")
private HashMap<String, Object> variables;

}
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复制代码@Transactional(rollbackFor = Exception.class)
public void executeNextStep(ProcessInstanceExecuteNextStepRequestDTO requestDTO) {
ValidatorUtils.validate(requestDTO);
//
if (ExecuteTypeEnum.of(requestDTO.getExecuteType()).isNone()) {
throw new BusinessException("未知执行状态");
}
TaskQuery taskQuery = taskService.createTaskQuery();
taskQuery.taskId(requestDTO.getTaskId());
Task task = taskQuery.singleResult();
AssertUtils.notEmpty(task, "找不到任务");
//
// 添加审批意见
ProcessInstanceCommentRequestDTO commentRequestDTO = new ProcessInstanceCommentRequestDTO();
commentRequestDTO.setProcessInstanceId(task.getProcessInstanceId());
commentRequestDTO.setTaskId(task.getId());
// commentRequestDTO.setType("event");
commentRequestDTO.setUserId(SecurityUser.getUserId());
commentRequestDTO.setNickname(SecurityUser.get().getName());
commentRequestDTO.setExecuteType(requestDTO.getExecuteType());
commentRequestDTO.setExecuteTypeValue(ExecuteTypeEnum.of(requestDTO.getExecuteType()).getValue());
commentRequestDTO.setContent(task.getName() + ":" + requestDTO.getCommentContent());
commentRequestDTO.setExt("");
//
this.addProcessInstanceComment(commentRequestDTO);
// 处理流程审批
HashMap<String, Object> variables = requestDTO.getVariables();
if (variables == null) {
variables = new HashMap<>(8);
}
// 这里会put一个执行变量executeType,也就是流程定义xml中的那个变量名称,这个变量决定了流程再走向
variables.put(WorkflowConstants.EXECUTE_TYPE, requestDTO.getExecuteType());
// 添加执行人
variables.put("_execute_user_id=" + SecurityUser.getUserId(), "_execute_user_id=" + SecurityUser.getUserId());
taskService.complete(task.getId(), variables);
}

4.3.6 终止流程实例

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Data
@ApiModel("流程实例-分页请求参数")
public class ProcessInstanceStopRequestDTO implements Serializable {

@ApiModelProperty(value = "流程实例ID")
@NotEmpty(message = "流程实例ID 不可以为空")
private String processInstanceId;
@ApiModelProperty(value = "审批意见")
@NotEmpty(message = "审批意见 不可以为空")
private String commentContent;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码public void stopProcessInstance(ProcessInstanceStopRequestDTO requestDTO) {
ValidatorUtils.validate(requestDTO);
// 修改流转执行状态
runtimeService.setVariable(requestDTO.getProcessInstanceId(), WorkflowConstants.EXECUTE_TYPE, ExecuteTypeEnum.STOP.name());
//
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
.processInstanceId(requestDTO.getProcessInstanceId()).singleResult();
// 添加一条审批记录
ProcessInstanceCommentRequestDTO commentRequestDTO = new ProcessInstanceCommentRequestDTO();
commentRequestDTO.setProcessInstanceId(processInstance.getProcessInstanceId());
commentRequestDTO.setTaskId(null);
commentRequestDTO.setUserId(SecurityUser.getUserId());
commentRequestDTO.setNickname(SecurityUser.get().getName());
commentRequestDTO.setExecuteType(ExecuteTypeEnum.STOP.name());
commentRequestDTO.setExecuteTypeValue(ExecuteTypeEnum.STOP.getValue());
commentRequestDTO.setContent(StringUtils.defaultString(requestDTO.getCommentContent(), "终止流程"));
commentRequestDTO.setExt("");
this.addProcessInstanceComment(commentRequestDTO);
/// 执行终止
List<Execution> executions = runtimeService.createExecutionQuery().parentId(requestDTO.getProcessInstanceId()).list();
List<String> executionIds = executions.stream().map(v -> v.getId()).collect(Collectors.toList());
// 获取流程结束点
BpmnModel bpmnModel = repositoryService.getBpmnModel(processInstance.getProcessDefinitionId());
Process process = bpmnModel.getMainProcess();
List<EndEvent> endNodes = process.findFlowElementsOfType(EndEvent.class);
String endId = endNodes.get(endNodes.size() - 1).getId();
// 执行跳转
runtimeService.createChangeActivityStateBuilder()
.moveExecutionsToSingleActivityId(executionIds, endId)
.changeState();
}

4.3.7 批量获取流程实例变量列表

这个方法是自定义实现的,因为 Flowable 没有对应的根据流程实例ID列表获取批量的流程变量

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
java复制代码/**
* 实例映射 Flowable 的 act_hi_varinst 表
*/
@Data
public class HistoryVariable implements Serializable {

public static final HistoryVariable EMPTY = new HistoryVariable();

private String id;
private String rev;
private String processInstanceId;
private String executionId;
private String taskId;
private String name;
private String varType;
private String scopeId;
private String subScopeId;
private String scopeType;
private String bytearrayId;
private Double doubleValue;
private Long longValue;
private String text;
private String text2;
private Date createTime;
private Date lastUpdatedTime;

/*
act_hi_varinst 字段列表:
ID_
REV_
PROC_INST_ID_
EXECUTION_ID_
TASK_ID_
NAME_
VAR_TYPE_
SCOPE_ID_
SUB_SCOPE_ID_
SCOPE_TYPE_
BYTEARRAY_ID_
DOUBLE_
LONG_
TEXT_
TEXT2_
CREATE_TIME_
LAST_UPDATED_TIME_
*/
}

Service

1
2
3
4
5
6
7
8
9
java复制代码@Override
public List<HistoryVariable> findHistoryVariableList(Collection<String> processInstanceIds) {
if (CollectionUtils.isEmpty(processInstanceIds)) {
return Collections.emptyList();
}
QueryWrapper<HistoryVariable> ew = new QueryWrapper<>();
ew.in("t.PROC_INST_ID_", processInstanceIds);
return this.baseMapper.findHistoryVariableList(ew);
}

Dao

1
java复制代码List<HistoryVariable> findHistoryVariableList(@Param("ew") QueryWrapper<HistoryVariable> ew);

xml

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
xml复制代码<select id="findHistoryVariableList" resultType="cn.leadersheep.xz.workflow.server.entity.flowable.HistoryVariable">
SELECT
t.ID_ AS id,
t.REV_ AS rev,
t.PROC_INST_ID_ AS process_instance_id,
t.EXECUTION_ID_ AS execution_id,
t.TASK_ID_ AS task_id,
t.NAME_ AS name,
t.VAR_TYPE_ AS var_type,
t.SCOPE_ID_ AS scope_id,
t.SUB_SCOPE_ID_ AS sub_scope_id,
t.SCOPE_TYPE_ AS scope_type,
t.BYTEARRAY_ID_ AS bytearray_id,
t.DOUBLE_ AS double_value,
t.LONG_ AS long_value,
t.TEXT_ AS text,
t.TEXT2_ AS text2,
t.CREATE_TIME_ AS create_time,
t.LAST_UPDATED_TIME_ AS last_update_time

FROM act_hi_varinst t
<where>
${ew.sqlSegment}
</where>
</select>

4.3.8 获取流程实例分页

获取流程实例分页,因为这里涉及到数据权限过滤、以及待办、已办、历史记录、我发起等等,逻辑还是挺复杂的,因此简单通过Flowable的API可能不是一个很好的选择了,因此这里自定义实现,通过查找Flowable相关的数据库表找出符合记录


1.如果流程实例还在进行中数据是保存在 act_ru_* 这几张表中,如果流程实例结束了数据是保存在 act_hi_* 这几张表中,因此查询的时候需要根据不用场景查询不用的表;
2.流程定义图中定义的分配的用户组,保存在 act_ru_identitylink 表中;

流程实例-分页请求参数

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel("流程实例-分页请求参数")
public class ProcessInstancePageRequestDTO extends PageParamsEntity {

@ApiModelProperty(value = "项目ID")
private String communityId;
@ApiModelProperty(value = "查找范围:MY_TODO-我的待办;MY_DONE-我的已办;MY_SCOPE-我的范围;MY_CREATE-我的创建;")
private String searchScope;
@ApiModelProperty(value = "任务名称")
private String processInstanceName;

}

流程定义-分页响应结果

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
java复制代码@Data
@ApiModel("流程定义-分页响应结果")
public class ProcessInstancePageResponseDTO implements Serializable {

@ApiModelProperty(value = "项目ID")
private String communityId;
@ApiModelProperty(value = "项目名称")
private String communityName;
@ApiModelProperty(value = "流程实例ID")
private String processInstanceId;
@ApiModelProperty(value = "流程实例名称")
private String processInstanceName;
@ApiModelProperty(value = "流程定义ID")
private String processDefinitionId;
@ApiModelProperty(value = "流程定义名称")
private String processDefinitionName;
@ApiModelProperty(value = "任务ID")
private String taskId;
@ApiModelProperty(value = "任务名称")
private String taskName;
@ApiModelProperty(value = "开始时间")
private Date startTime;
@ApiModelProperty(value = "结束时间")
private Date endTime;
@ApiModelProperty(value = "持续时间")
private String duration;
@ApiModelProperty(value = "创建人ID")
private String createId;
@ApiModelProperty(value = "创建人名称")
private String createName;

}

Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
java复制代码public PageDTO<ProcessInstancePageResponseDTO> findPage(ProcessInstancePageRequestDTO requestDTO) {
// 在这里实现数据过滤
String filterSql = StringUtils.defaultString(processFilterSQL("TEXT_", requestDTO.getCommunityId(), true), "");
filterSql = filterSql.replace("('", "('" + WorkflowConstants.VAR_COMMUNITY_ID_EQ);
filterSql = filterSql.replace(", '", ", '" + WorkflowConstants.VAR_COMMUNITY_ID_EQ);
// TEXT_ IN ('_community_id=1545645315843546', '_community_id=15456453158436521')
// System.out.println("filterSQL = " + filterSQL);
//
PageDTO<ProcessInstancePageResponseDTO> page = this.buildPage(requestDTO);
if ("MY_TODO".equals(requestDTO.getSearchScope())) {
// 我的待办
QueryWrapper<ProcessInstancePageResponseDTO> ew = buildEmptyQueryWrapper();
ew.like(StringUtils.isNotEmpty(requestDTO.getProcessInstanceName()), "t4.NAME_", requestDTO.getProcessInstanceName());
ew.eq("1", 1);
ew.apply(CoreUtil.isNotEmpty(filterSql), "t3." + filterSql);
ew.orderByDesc("t.CREATE_TIME_");
ew.groupBy("t.PROC_INST_ID_");
//
if (!AuthHelper.isRoleSuperAdmin(SecurityUser.current().getRoleCodeSet())) {
// 不是超级管理员,需要过滤
ew.and(and -> and.in("t2.GROUP_ID_", SecurityUser.current().getRoleCodeSet()).or(or -> {
or.eq("t2.USER_ID_", SecurityUser.getUserId());
}));
}
page = this.baseMapper.findMyTodo(page, ew);
} else if ("MY_DONE".equals(requestDTO.getSearchScope())) {
// 我的已办
QueryWrapper<ProcessInstancePageResponseDTO> ew = buildEmptyQueryWrapper();
ew.like(StringUtils.isNotEmpty(requestDTO.getProcessInstanceName()), "t.NAME_", requestDTO.getProcessInstanceName());
ew.eq("t2.TEXT_", "_execute_user_id=" + SecurityUser.getUserId());
ew.apply(filterSql.length() > 0, "t3." + filterSql);
ew.orderByDesc("t.START_TIME_");
ew.groupBy("t.ID_");
//
page = this.baseMapper.findMyDonePage(page, ew);
} else if ("MY_SCOPE".equals(requestDTO.getSearchScope())) {
// 我的范围
QueryWrapper<ProcessInstancePageResponseDTO> ew = buildEmptyQueryWrapper();
ew.like(StringUtils.isNotEmpty(requestDTO.getProcessInstanceName()), "t.NAME_", requestDTO.getProcessInstanceName());
ew.eq("1", 1);
ew.apply(filterSql.length() > 0, "t4." + filterSql);
ew.orderByDesc("t.START_TIME_");
ew.groupBy("t.ID_");
//
if (!AuthHelper.isRoleSuperAdmin(SecurityUser.current().getRoleCodeSet())) {
// 不是超级管理员,需要过滤
ew.and(and -> and.in("t3.GROUP_ID_", SecurityUser.current().getRoleCodeSet()).or(or -> {
or.eq("t3.USER_ID_", SecurityUser.getUserId());
}));
}
page = this.baseMapper.findMyScopePage(page, ew);
} else if ("MY_CREATE".equals(requestDTO.getSearchScope())) {
// 我发起的
QueryWrapper<ProcessInstancePageResponseDTO> ew = buildEmptyQueryWrapper();
ew.like(StringUtils.isNotEmpty(requestDTO.getProcessInstanceName()), "t.NAME_", requestDTO.getProcessInstanceName());
ew.eq("t2.TEXT_", "_create_user_id=" + SecurityUser.getUserId());
ew.apply(filterSql.length() > 0, "t3." + filterSql);
ew.orderByDesc("t.START_TIME_");
ew.groupBy("t.ID_");
//
page = this.baseMapper.findMyDonePage(page, ew);
}
//
if (CollectionUtils.isNotEmpty(page.getRecords())) {
// 填充其他属性
List<String> processInstanceIds = page.getRecords().stream().map(v -> v.getProcessInstanceId()).distinct().collect(Collectors.toList());
List<HistoryVariable> variableList = this.findHistoryVariableList(processInstanceIds);
Map<String, HistoryVariable> variableMap = variableList.stream()
.collect(Collectors.toMap(v -> v.getProcessInstanceId() + "_" + v.getName(), v -> v));
page.getRecords().forEach(v -> {
String prefix = v.getProcessInstanceId() + "_";
//
HistoryVariable variable = variableMap.getOrDefault(prefix + VAR_COMMUNITY_ID, HistoryVariable.EMPTY);
String text = Optional.ofNullable(variable.getText()).map(t -> t.replace(VAR_COMMUNITY_ID_EQ, "")).orElse(null);
v.setCommunityId(text);
//
// variable = variableMap.getOrDefault(prefix + VAR_PROCESS_INSTANCE_NAME, HistoryVariable.EMPTY);
// text = Optional.ofNullable(variable.getText()).map(t -> t.replace(VAR_PROCESS_INSTANCE_NAME_EQ, "")).orElse(null);
// v.setProcessInstanceName(text);
//
variable = variableMap.getOrDefault(prefix + VAR_CREATE_USER_ID, HistoryVariable.EMPTY);
text = Optional.ofNullable(variable.getText()).map(t -> t.replace(VAR_CREATE_USER_ID_EQ, "")).orElse(null);
v.setCreateId(text);
//
variable = variableMap.getOrDefault(prefix + VAR_CREATE_USER_NICKNAME, HistoryVariable.EMPTY);
text = Optional.ofNullable(variable.getText()).map(t -> t.replace(VAR_CREATE_USER_NICKNAME_EQ, "")).orElse(null);
v.setCreateName(text);
//
variable = variableMap.getOrDefault(prefix + VAR_PROCESS_DEFINITION_NAME, HistoryVariable.EMPTY);
text = Optional.ofNullable(variable.getText()).map(t -> t.replace(VAR_PROCESS_DEFINITION_NAME_EQ, "")).orElse(null);
v.setProcessDefinitionName(text);
});
sysOrganizationRemote.fillOrganization(page.getRecords(), v -> v.getCommunityId(), (v, c) -> v.setCommunityName(c.getName()));
}
return page;
}

Dao

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
java复制代码/**
* 获取我的待办任务
*
* @author houyu for.houyu@qq.com <br>
* @param page 分页
* @param ew 参数包装器
* @return PageDTO<ProcessInstancePageResponseDTO>
*/
PageDTO<ProcessInstancePageResponseDTO> findMyTodo(@Param("page") PageDTO<ProcessInstancePageResponseDTO> page, @Param("ew") QueryWrapper<ProcessInstancePageResponseDTO> ew);

/**
* 获取我的范围内的任务
*
* @author houyu for.houyu@qq.com <br>
* @param page 分页
* @param ew 参数包装器
* @return PageDTO<ProcessInstancePageResponseDTO>
*/
PageDTO<ProcessInstancePageResponseDTO> findMyScopePage(@Param("page") PageDTO<ProcessInstancePageResponseDTO> page, @Param("ew") QueryWrapper<ProcessInstancePageResponseDTO> ew);

/**
* 获取我的已办(我的发起 / 我审批的)
*
* @author houyu for.houyu@qq.com <br>
* @param page 分页
* @param ew 参数包装器
* @return PageDTO<ProcessInstancePageResponseDTO>
*/
PageDTO<ProcessInstancePageResponseDTO> findMyDonePage(@Param("page") PageDTO<ProcessInstancePageResponseDTO> page, @Param("ew") QueryWrapper<ProcessInstancePageResponseDTO> ew);

Xml

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
xml复制代码<select id="findMyTodo" resultType="cn.leadersheep.xz.workflow.client.dto.flowable.response.ProcessInstancePageResponseDTO">
SELECT
t.ID_ AS task_id,
t.PROC_INST_ID_ AS process_instance_id,
t4.NAME_ AS process_instance_name,
t.NAME_ AS task_name,
t.PROC_DEF_ID_ as process_definition_id,
t.CREATE_TIME_ as start_time,
NULL as end_time

FROM act_ru_task t
LEFT JOIN act_ru_identitylink t2 ON t2.TASK_ID_ = t.ID_
LEFT JOIN act_ru_variable t3 ON t3.PROC_INST_ID_ = t.PROC_INST_ID_
LEFT JOIN act_hi_procinst t4 ON t4.PROC_INST_ID_ = t.PROC_INST_ID_
<where>
${ew.sqlSegment}
</where>
</select>

<select id="findMyScopePage" resultType="cn.leadersheep.xz.workflow.client.dto.flowable.response.ProcessInstancePageResponseDTO">
SELECT
t.PROC_INST_ID_ AS process_instance_id,
t.NAME_ AS process_instance_name,
t.PROC_DEF_ID_ as process_definition_id,
t.START_TIME_ as start_time,
t.END_TIME_ as end_time

FROM act_hi_procinst t
LEFT JOIN act_hi_taskinst t2 ON t2.PROC_INST_ID_ = t.ID_
LEFT JOIN act_hi_identitylink t3 ON t3.TASK_ID_ = t2.ID_
LEFT JOIN act_hi_varinst t4 ON t4.PROC_INST_ID_ = t.ID_
<where>
${ew.sqlSegment}
</where>
</select>

<select id="findMyDonePage" resultType="cn.leadersheep.xz.workflow.client.dto.flowable.response.ProcessInstancePageResponseDTO">
SELECT
t.PROC_INST_ID_ AS process_instance_id,
t.NAME_ AS process_instance_name,
t.PROC_DEF_ID_ as process_definition_id,
t.START_TIME_ as start_time,
t.END_TIME_ as end_time

FROM act_hi_procinst t
LEFT JOIN act_hi_varinst t2 ON t2.PROC_INST_ID_ = t.ID_
LEFT JOIN act_hi_varinst t3 ON t3.PROC_INST_ID_ = t.ID_
<where>
${ew.sqlSegment}
</where>
</select>
  1. Linux部署流程图文字乱码

简单描述一下我遇到的情况,我当时是在Windows上开发的,设置流程图的字体为宋体,没有出现乱码的情况,但是我部署到Linux服务器上查看流程图的时候文字出现了乱码,然后我大概能猜到是因为缺少字体的问题(因为之前有了解过activiti流程图乱码是缺少字体的问题),所以我这次也是按照相同套路给Linux添加宋体字体就解决了。

5.1 复制Windows上的字体

进入目录:C:\Windows\Fonts 复制 “宋体 常规”到桌面上备用

5.2 Linux找出 java 位置

执行命令

1
复制代码whereis java

我的是在 /var/lib/jdk/jdk1.8.0_211/bin/java

5.3 复制字体文件到 jre/lib/fonts

找出了java的位置,jre 的位置在 java 的前几级目录下

1
2
3
4
5
bash复制代码java:
/var/lib/jdk/jdk1.8.0_211/bin/java

jre 字体目录(复制到这里):
/var/lib/jdk/jdk1.8.0_211/jre/lib/fonts

5.4 复制字体文件到系统字体目录中(/usr/share/fonts)

这个目录可能不存在,如果不存在则自己创建出来

1
bash复制代码/usr/share/fonts

5.5 重启系统

1
复制代码reboot

重启系统之后,启动应用程序,即不会出现乱码的情况了,祝你好运~


上一篇博客:SpringBoot整合Flowable工作流-1(画流程定义)

基于 flowable-spring-boot-starter 整合的代码基本完成,但是感觉还是少了一点东西,流程一步一步执行下去了,什么时候执行完?现在到什么环节了?貌似我们都不太清楚,执行完了业务要怎么操作,这就要介绍一下 flowable 全局事件监听器了,下一篇博客将介绍 flowable 全局事件监听器,结合监听器实现业务的通知业务。


  • 公众号:IT加载中(it_loading)
  • CSDN:blog.csdn.net/JinglongSou…
  • 博客:ihouyu.cn/
  • 邮箱:for.houyu@qq.com

程序员[ 后宇 ],是一个关注编程,热爱技术的Java后端开发者,热衷于 [ Java后端 ],[ 数据爬虫领域 ]。不定期分享 I T 技能和干货!!欢迎关注 “IT加载中”,一个只出 干货 和 实战 的公众号。

本文转载自: 掘金

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

ArrayMap跟HashMap区别

发表于 2021-06-08

Hash碰撞的解决方式

  • 提起存储键值对,首先想到的是Map集合,但是对于hash算法导致的hash碰撞,一般有两种解决方式: 链表法跟开放地址法, 对于Android应用开发来说,正好对应着HashMap跟ArrayMap的解决方式

ArrayMap: 开放地址法

  • 笔者在使用Retrofit请求网络时由于需要将数据封装成map集合发送给后端,但是一般都是提交数据量很小的内容,因此使用ArrayMap替换HashMap已节约内存空间.这里通过源码分析两者之间的差别
  1. 首先查看构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ini复制代码 public ArrayMap() { 
this(0, false);
}
public ArrayMap(int capacity) {
this(capacity, false);
}
/**
* identityHashCode: 是否使用系统的System.identityHashCode(key),跟对象存储位置地址值相关 ,
* false时使用对象的hashCode,如果重写就使用对象的hashCode方法(String 重写了,所以map键常用的字符串是一致的),未重写则使用object的hashcode
* 同系统的调用同一个本地方法,结果值一致
*/
public ArrayMap(int capacity, boolean identityHashCode) {
mIdentityHashCode = identityHashCode;

// 这是同HashMap不同的地方,如果当前没有设置大小默认为0,hashMap默认数组为16,节约空间了吧
if (capacity < 0) {
mHashes = EMPTY_IMMUTABLE_INTS;
mArray = EmptyArray.OBJECT;
} else if (capacity == 0) {
mHashes = EmptyArray.INT;
mArray = EmptyArray.OBJECT;
} else {
// 初始化值,根据提供的大小,但同HashMap会改变(注意并非指定多少即为多大空间数组)
allocArrays(capacity);
}
mSize = 0;
}
  1. 搞懂几个数据含义
  • ArrayMap是由两个数组构成的 mHashes 和 mArray
  • mHashes : 由键的Hash值根据大小有序的数组 mHashes [key1.hash , key2.hash , ….],(key1.hash跟key2.hash完全可以相等,这个没关系的)
  • mArray: 键值对依次排列的数组 mArray[key1, value1, key2, value2 ….]
  1. 查看 put()方法
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
scss复制代码    @Override
public V put(K key, V value) {
final int osize = mSize;
final int hash;
int index;
if (key == null) { //key是空,则通过indexOfNull查找对应的index,支持存储null键
hash = 0;
index = indexOfNull();
} else { //否则 通过key查找下标index
hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode(); //构造函数中指定使用哪种hash值
index = indexOf(key, hash); //稍后解释(根据hash值及key找到对应的下标index ,存在为正,不然为 ~位置 一个负数值)
}
if (index >= 0) { //找到了当前存储的key键在mArray中替换成新值value并返回旧值
index = (index<<1) + 1;
final V old = (V)mArray[index];
mArray[index] = value;
return old;
}

index = ~index; //将indexOf中找到的~index在次~即为(~~index) == index一个正值,即为数据将要插入的位置
if (osize >= mHashes.length) { //判断是否需要扩容大小,扩容为默认 4 ->8 -> 12 -> 18(不足向上取,比如初始7个数据则数组大小为8),扩容后赋值并回收原有数组
final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))
: (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);

if (DEBUG) Log.d(TAG, "put: grow from " + mHashes.length + " to " + n);
//将原来的数组赋值保存后在使用allocArrays(n)对原来数组根据新大小n进行扩容
final int[] ohashes = mHashes;
final Object[] oarray = mArray;
allocArrays(n); //具体的扩容函数,创建一个新的数据,可能存在缓存数组,稍后讲解

if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) { //对于两个线程同时扩容报错,同HashMap一致是非线程安全
throw new ConcurrentModificationException();
}

if (mHashes.length > 0) {
//将上方保存的原有数据拷贝到新数组中,注意下标从0开始及大小原有数组长度
if (DEBUG) Log.d(TAG, "put: copy 0-" + osize + " to 0");
System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
System.arraycopy(oarray, 0, mArray, 0, oarray.length);
}
//释放原有数组,已经在上方拷贝过了,这里涉及到缓存数据
//查看是否数据还有利用价值,其后会讲到,大致是对数据量为4或者8做一个缓存以便其后还会利用到
//如果数据量超过8就就没有必要占用内存去缓存它了
freeArrays(ohashes, oarray, osize);
}

if (index < osize) { //index是指当前数据key的hash值下标即key应该插入数组的位置不是最后一位则移动他将插入位置及其后的所有数据腾出位置给其插入使用
if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (osize-index)
+ " to " + (index+1));
//注意: 将mHashes的index 位置以后的向后移动一位,同时mArray也是同时向后移动2位,给key.hash,跟(key,value)插入让出位置
System.arraycopy(mHashes, index, mHashes, index + 1, osize - index);
System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
}

if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
if (osize != mSize || index >= mHashes.length) { //再次判断是否线程安全
throw new ConcurrentModificationException();
}
}
mHashes[index] = hash; //在特定位置插入mHashes,及mArray数组上
mArray[index<<1] = key;
mArray[(index<<1)+1] = value;
mSize++; //数据值+1
return null;
}
  1. 初始化数组函数 allocArrays(capacity)用于数组的初始化及缓存数据
  • 看这个缓存函数需要搞懂一个数组扩容大小改变关系(在put函数中BASE_SIZE = 4)
1
2
ini复制代码final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))
: (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);
  • 默认值为 4 ,第一次扩容为 4 * 2 = 8 , 以后每次扩容为增加当前值得 1/2 倍.即下次为 12 ->18 …
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
ini复制代码    private void allocArrays(final int size) {
if (mHashes == EMPTY_IMMUTABLE_INTS) {
throw new UnsupportedOperationException("ArrayMap is immutable");
}
//设置缓存用的,当数据量由24 降为 8时不用再次重新创建加载,而是直接使用mTwiceBaseCache缓存的数组即可
if (size == (BASE_SIZE*2)) {
synchronized (ArrayMap.class) {
if (mTwiceBaseCache != null) { //缓存 8数据存在
final Object[] array = mTwiceBaseCache;
mArray = array;
mTwiceBaseCache = (Object[])array[0];
mHashes = (int[])array[1];
array[0] = array[1] = null;
mTwiceBaseCacheSize--;
if (DEBUG) Log.d(TAG, "Retrieving 2x cache " + mHashes
+ " now have " + mTwiceBaseCacheSize + " entries");
return;
}
}
} else if (size == BASE_SIZE) {
synchronized (ArrayMap.class) {
if (mBaseCache != null) {
final Object[] array = mBaseCache;
mArray = array;
mBaseCache = (Object[])array[0];
mHashes = (int[])array[1];
array[0] = array[1] = null;
mBaseCacheSize--;
if (DEBUG) Log.d(TAG, "Retrieving 1x cache " + mHashes
+ " now have " + mBaseCacheSize + " entries");
return;
}
}
}

mHashes = new int[size];
mArray = new Object[size<<1];
}
  1. 对于查找indexOf(key,hash)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
arduino复制代码
int indexOf(Object key, int hash) {
final int N = mSize;

// Important fast case: if nothing is in here, nothing to look for.
if (N == 0) {
return ~0;
}
// 使用二分查找到当前hash值在mHashes数组中的下标,如果不存在返回(~当前需要插入的位置)后续在~即为正在需要插入的位置 (~~A == A)
int index = binarySearchHashes(mHashes, N, hash);

if (index < 0) { //如果没有找到就返回一个负值
return index;
}

//如果当前index下标的key同查找的一致就返回
if (key.equals(mArray[index<<1])) {
return index;
}

// 先向上查找相同hash值是否key一致,否则向下查找
int end;
for (end = index + 1; end < N && mHashes[end] == hash; end++) {
if (key.equals(mArray[end << 1])) return end;
}

for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
if (key.equals(mArray[i << 1])) return i;
}

// 如果都没有找到,将返回需要插入位置的负值,注意这里是相同hash值的最后一个其后添加数据
return ~end;
}
  1. remove 方法:
  • 在某种条件下,会重新分配内存,保证分配给ArrayMap的内存在合理区间,减少对内存的占用。但是如果每次remove都重新分配空间,会浪费大量的时间。
  • 因此在此处,Android使用的是用空间换时间的方式,以避免效率低下。无论从任何角度,频繁的分配回收内存一定会耗费时间的。
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
ini复制代码public V removeAt(int index) {
final Object old = mArray[(index << 1) + 1];
final int osize = mSize; //msize是当前存储实际数据大小,mHashes.length为当前申请的数组大小
final int nsize;
if (osize <= 1) {
// 当实际数据为1时,即移除以后恢复成默认值null
if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to 0");
final int[] ohashes = mHashes;
final Object[] oarray = mArray;
mHashes = EmptyArray.INT;
mArray = EmptyArray.OBJECT;
freeArrays(ohashes, oarray, osize);
nsize = 0;
} else {
nsize = osize - 1;
// 如果当前数组大于 8并且实际数据量小于 数组长度的 1/3 则重新分配内存空间
if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) {
//重新分配后实际数组的大小
final int n = osize > (BASE_SIZE*2) ? (osize + (osize>>1)) : (BASE_SIZE*2);

if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to " + n);
//备份数据以便后期复制使用
final int[] ohashes = mHashes;
final Object[] oarray = mArray;
allocArrays(n); //重新分配数据,注意里面会用到已经缓存的4和8的数据

if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) { //判断是否线程安全
throw new ConcurrentModificationException();
}
//移除index上的数据分两个部分移除,首先复制0--index大小的数据就是到 (index -1)处

if (index > 0) {
if (DEBUG) Log.d(TAG, "remove: copy from 0-" + index + " to 0");
System.arraycopy(ohashes, 0, mHashes, 0, index);
System.arraycopy(oarray, 0, mArray, 0, index << 1);
}
// 再次从index + 1 处移动nsize - index的数据复制到新数组中
if (index < nsize) {
if (DEBUG) Log.d(TAG, "remove: copy from " + (index+1) + "-" + nsize
+ " to " + index);
System.arraycopy(ohashes, index + 1, mHashes, index, nsize - index);
System.arraycopy(oarray, (index + 1) << 1, mArray, index << 1,
(nsize - index) << 1);
}
} else { //不用重新分配内存,直接可用的则直接移动其后的数据,并将最后一位设置成默认null
if (index < nsize) {
if (DEBUG) Log.d(TAG, "remove: move " + (index+1) + "-" + nsize
+ " to " + index);
System.arraycopy(mHashes, index + 1, mHashes, index, nsize - index);
System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1,
(nsize - index) << 1);
}
mArray[nsize << 1] = null;
mArray[(nsize << 1) + 1] = null;
}
}
if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
throw new ConcurrentModificationException();
}
mSize = nsize; //减少一位并返回删除的旧值
return (V)old;
}
  1. 重点关注缓存 freeArrays(final int[] hashes, final Object[] array, final int size) , 结合 allocArrays(int size)同看
    • 注意参数的内容 : hashes 跟 array的长度关系是 1/ 2 , 切记切记啊
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
ini复制代码    private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
if (hashes.length == (BASE_SIZE*2)) { //如果当前缓存的大小为8
synchronized (ArrayMap.class) {
if (mTwiceBaseCacheSize < CACHE_SIZE) {
array[0] = mTwiceBaseCache; //第一次的array[0] = null, 但是其后的将会是一个链表结构这一次缓存中的array[0] ->指向的是上一次缓存的整体数字,但是array的中长度依然是 16 ,最多缓存 10个
array[1] = hashes;
for (int i=(size<<1)-1; i>=2; i--) {
array[i] = null;
}
mTwiceBaseCache = array;
mTwiceBaseCacheSize++;
if (DEBUG) Log.d(TAG, "Storing 2x cache " + array
+ " now have " + mTwiceBaseCacheSize + " entries");
}
}
} else if (hashes.length == BASE_SIZE) { //同上所述不过缓存的数据为4,array的总长度为8,最多缓存10个
synchronized (ArrayMap.class) {
if (mBaseCacheSize < CACHE_SIZE) {
array[0] = mBaseCache;
array[1] = hashes;
for (int i=(size<<1)-1; i>=2; i--) {
array[i] = null;
}
mBaseCache = array;
mBaseCacheSize++;
if (DEBUG) Log.d(TAG, "Storing 1x cache " + array
+ " now have " + mBaseCacheSize + " entries");
}
}
}
}
  1. 再次查看 allocArrays
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
ini复制代码
private void allocArrays(final int size) {
if (mHashes == EMPTY_IMMUTABLE_INTS) {
throw new UnsupportedOperationException("ArrayMap is immutable");
}
if (size == (BASE_SIZE*2)) { //8数据同下
synchronized (ArrayMap.class) {
if (mTwiceBaseCache != null) {
final Object[] array = mTwiceBaseCache;
mArray = array;
mTwiceBaseCache = (Object[])array[0];
mHashes = (int[])array[1];
array[0] = array[1] = null;
mTwiceBaseCacheSize--;
if (DEBUG) Log.d(TAG, "Retrieving 2x cache " + mHashes
+ " now have " + mTwiceBaseCacheSize + " entries");
return;
}
}
} else if (size == BASE_SIZE) { //如果当前缓存的有数据,则array是一个大小为8的链式结构,加入缓存了多次,但是被复用以后其被赋值给mArray的时候依然是一个大小为8且在后续会被重新赋值的,所以不用担心链式多层结构问题
synchronized (ArrayMap.class) {
if (mBaseCache != null) {
final Object[] array = mBaseCache;
mArray = array;
mBaseCache = (Object[])array[0]; //将下一层缓存的数据重新赋值给4个缓存
mHashes = (int[])array[1]; //当前为4的数组赋值给mHashes
array[0] = array[1] = null;
mBaseCacheSize--; //缓存池中数量减1
if (DEBUG) Log.d(TAG, "Retrieving 1x cache " + mHashes
+ " now have " + mBaseCacheSize + " entries");
return;
}
}
}

mHashes = new int[size];
mArray = new Object[size<<1];
}
  • 缓存数据图如下
    image
  1. 特别解释,为何最大缓存数据是10层:
  • 没有注意几个数据情况 :几个缓存数据及他们的长度均是静态成员变量,也就是他们是跟类一致而跟对象无关,所以是所有的ArrayMap对象共用的,不管你创建多少ArrayMap都将共用这最多10个缓存池,相当于线程池一样儿的
1
2
3
4
dart复制代码static Object[] mBaseCache; 
static int mBaseCacheSize;
static Object[] mTwiceBaseCache;
static int mTwiceBaseCacheSize;

SparseArray

  • 同ArrayMap一样是由两个数组构成的,不同的是他并不是map集合
1
2
3
4
5
6
7
8
9
10
java复制代码//成员变量分析
public class SparseArray<E> implements Cloneable {
private static final Object DELETED = new Object(); //数组中的数据如果被删除,可能仅仅标记value为DELETED并不会每次都重新移动数组,太消耗时间了,同时可以直接在Deleted位置上put数据覆盖之
private boolean mGarbage = false; //是否需要垃圾回收,即value中存在DELETED,但并不一定会被回收的,在delete(key)时被标记为Deleted
private int[] mKeys; //一个key对应一个value,且key是int型不可重复,多在从0-N有序列使用,比如RecycleView中的viewType就是有序且不可重复的,缓存即使用SparseArray
private Object[] mValues;
private int mSize;
public SparseArray() { //默认keys和values数组长度为10
this(10);
}
  • 存储put
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
ini复制代码public void put(int key, E value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//如果找到i则i对应的value数组位置修改值
if (i >= 0) {
mValues[i] = value;
} else {
i = ~i; //没有找到,这个只是根据二分查找到第一个大于i的位置插入这里,其后值向后移动

if (i < mSize && mValues[i] == DELETED) { //如果当前位置被标记为Deleted删除标记,表示该位置可用
mKeys[i] = key;
mValues[i] = value;
return;
}
//有Deleted则mGarbage = true,数组是可以靠移动而不是扩容就能容纳该put新值的
if (mGarbage && mSize >= mKeys.length) {
gc(); //新gc将deleted标志都删除掉

// Search again because indices may have changed.
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
//如果位置没有Deleted,且数组已经满了,则二倍的扩容
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
  • put步骤:
    1. 如果当前i位置根据二分查找找到了,则替换原来值为value新值
    2. 如果当前i不存在,则找到第一个大于i的位置标示插入位置,此时有两种情况,1该位置设置为Deleted则直接插入即可,否则判断是否已满且有被标记Deleted的位置,通过GC移动数组,将deleted位置处删除则可正常的插入了
    3. 如果数组满了,且没有Deleted标志,则需要2倍的扩容数组后在插入啦!
  • 这里的gc比较有意思,可以看一下,使用双指针移动删除数组中被标记为Deleted
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ini复制代码private void gc() {
// Log.e("SparseArray", "gc start with " + mSize);
int n = mSize;
int o = 0;
int[] keys = mKeys;
Object[] values = mValues;
for (int i = 0; i < n; i++) {
Object val = values[i];
if (val != DELETED) {
if (i != o) {
keys[o] = keys[i];
values[o] = val;
values[i] = null;
}
o++;
}
}
mGarbage = false;
mSize = o;
// Log.e("SparseArray", "gc end with " + mSize);
}
  • get和delete函数都很简单,这里只贴出来,标记为Deleted即可,不用真正的去移动数组,提高效率
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ini复制代码public E get(int key, E valueIfKeyNotFound) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

if (i < 0 || mValues[i] == DELETED) {
return valueIfKeyNotFound;
} else {
return (E) mValues[i];
}
}

/**
* Removes the mapping from the specified key, if there was any.
*/
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage = true;
}
}
}

HashMap

  • 对于大数据量的存储还是推荐使用HashMap,因为ArrayMap如果大量数据插入时需要时间复杂度O(n),而使用hashMap最坏时间复杂度O(logn,都在一个红黑树下),平均时间复杂度O(1),主要看数据量来换算是否已时间换取空间对于很多算法选择无非就是时间跟空间两者的平衡
  1. 数据结构为:HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体;
* 根据使用场景设置初始容量及负载因子
* hashMap实现了Map接口,使用键值对进行映射,map中不允许出现重复的键(key)
* Map接口分为两类:TreeMap跟HashMap
    + TreeMap保存了对象的排列次序,hashMap不能
    + HashMap可以有空的键值对(null-null),是非线程安全的,但是可以调用collections的静态方法synchronizeMap()实现
* HashMap中使用键对象来计算hashcode值
* HashMap比较快,因为是使用唯一的键来获取对象
  1. 源码解析:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
ini复制代码final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
HashMap.Node<K, V>[] tab;
HashMap.Node<K, V> p;
int n, i;
if ((tab = table) == null || (n = tab.length) == 0) //列表为空或者长度为0 初始化一个HashMap
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) //当前位置上没有数据new一个链表
tab[i] = newNode(hash, key, value, null);
else { //当前数组Hash位置上面已经存在了值
HashMap.Node<K, V> e;
K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) //有相同的hash键,直接替换
e = p;
else if (p instanceof HashMap.TreeNode) //如果当前是红黑树使用红黑树的添加
e = ((HashMap.TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
else { //当前为链表结构的添加
for (int binCount = 0; ; ++binCount) { //计算当前链表的长度
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null); //放到链表尾部
/**
* 当前链表从0开始遍历一直到 7,所以长度大于等于 8转化为红黑树存储,如果remove时树的数据量=<6 ,红黑树会转化成链表结构存储
*
* 解析:因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。
* 链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
* 选择6和8,中间有个差值7可以有效防止链表和树频繁转换,而导致的不停转化树与链表导致效率低下的问题;
*/
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash); //如果
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount; //记录变化次数,防止在多线程中迭代器取值导致错误,fail-fill检查机制
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
1. 通过源码我们可以得出:


    1. 当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,根据hash值得到元素在数组中的下标,(hash值计算 = 32位hash的 hash值 ^ 高16位后再 & 上 length -1 )


        1. 如果数组该位置已经存放有其他元素,则在这个位置上以链表的形式存放,新加入的放在链头,最后加入的放在链尾,如果链表中有相应的key则替换value值为最新的并返回旧值;
        2. 如果该位置上没有其他元素,就直接将该位置放在此数组中的该位置上;
        3. 我们希望HashMap里面的元素位置尽量的分布均匀写,使得每个位置上的元素只有一个,这样当使用hash算法得到这个位置的时候,马上就可以知道对应位置的元素就是我们要的,不用再遍历链表,这样就大大优化了查询效率;
        4. 计算hash值的方法hash(int h),理论上是对数组长度取模运算 % ,但是消耗比较大,源代码调用为:
    
1
2
3
4
5
6
7
8
9
10
11
12
13
arduino复制代码 /**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
return h & (length-1); //HashMap底层数组长度总是2的n次方,每次扩容为2倍
}

//Map数组初始化取值:
//当 length 总是 2 的 n 次方时,h& (length-1)运算等价于对 length 取模,也就是 h%length,但是 & 比 % 具有更高的效率。
// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1; //每次增加2倍,所以总长度一定为2的次方
1. 为何hash码初始化为2的次方数探讨: ![image](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/6fc0e5abc3e400693f5806e392b29b3043e7c4730e12797509d0a40dac44aaa8) 分析: 1. 当它们和 15-1(1110)“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8 和 9 会被放到数组中的同一个位置上形成链表,那么查询的时候就需要遍历这个链 表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为 15 的时候,hash 值会与 15-1(1110)进行“与”,那么最后一位永远是 0,而 0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率! 2. 而当数组长度为16时,即为2的n次方时,2n-1 得到的二进制数的每个位上的值都为 1,这使得在低位上&时,得到的和原 hash 的低位相同,加之 hash(int h)方法对 key 的 hashCode 的进一步优化,加入了高位计算,就使得只有相同的 hash 值的两个值才会被放到数组中的同一个位置上形成链表。 3. 当数组长度为 2 的 n 次幂的时候,不同的 key 算得得 index 相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了 * **总结:** 根据上面 put 方法的源代码可以看出,当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但key不会覆盖。如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部——具体说明继续看 addEntry() 方法的说明。 2. HashMap的扩容机制 1. 什么时候扩容: 达到阈值,数组大小 \* 加载因子默认16\* 0.75 = 12个 2. JDK 1.7没有引入红黑树,使用数组加上链表,JDK1.8引入红黑树,使用数组+链表+红黑树存储,当链表值大于等于8转化为红黑树,当remove时红黑树大小小于等于6时会转化为链表,设置一个过渡值7防止不停地put,remove导致不听转化而使得效率低下;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
	scss复制代码if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD) //6
tab[index] = loHead.untreeify(map); //将红黑树转化成链表
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}

```#### 归纳HashMap


1. **简单地说,HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据 hash 算法来决定其在数组中的存储位置,在根据 equals 方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry 时,也会根据 hash 算法找到其在数组中的存储位置,再根据 equals 方法从该位置上的链表中取出该Entry。**
2. 初始化HashMap默认值为16, 初始化时可以指定initial capacity,若不是2的次方,HashMap将选取第一个大于initial capacity 的2n次方值作为其初始长度 ;
3. 初始化负载因子为0.75,如果超过16 \* 0.75 = 12个数据就会将数组扩容为2倍 长度为32 , 并后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,如果我们已经预知 HashMap 中元素的个数,那么预设元素的个数能够有效的提高 HashMap 的性能。 如果负载印在越大,对空间利用越充分,但是会降低查询效率,如果负载因子越小,则散列表过于稀疏,对空间造成浪费(时间<-> 空间转换: 0.75最佳)
4. 多线程中的检测机制:Fail-Fast,通过标记modCount(使用volatile修饰,保证线程间修改的可见性) 域 修改次数,在迭代初始化时候赋值,以后每次next的时候都会判断是否相等
scss复制代码HashIterator() { expectedModCount = modCount; if (size > 0) { // advance to first entry Entry[] t = table; while (index < t.length && (next = t[index++]) == null) ; } } final Entry<K,V> nextEntry() { if (modCount != expectedModCount) throw new ConcurrentModificationException();
1
2
3
4
5
6
7
5. 建议使用concurrent HashMap在多线程中
6. Map的遍历,


1. 使用entrySet获取键值对的Entry集合,只需要遍历一次
2. 使用keySet先遍历所有的键,在根据键调取get(key),需要遍历两次
3. JDK1,8以上新增forEach()遍历,先遍历hash表,如果是链表结构遍历后再遍历下一个hash值的链表
ini复制代码public final void forEach(Consumer<? super Map.Entry<K,V>> action) { Node<K,V>[] tab; if (action == null) throw new NullPointerException(); if (size > 0 && (tab = table) != null) { int mc = modCount; // Android-changed: Detect changes to modCount early. for (int i = 0; (i < tab.length && modCount == mc); ++i) { for (Node<K,V> e = tab[i]; e != null; e = e.next) action.accept(e); } if (modCount != mc) throw new ConcurrentModificationException(); } } Map map = new HashMap();
iter
1
2
3
4
5
  while (iter.hasNext()) {
  Map.Entry entry = (Map.Entry) iter.next();
  Object key = entry.getKey();
  Object val = entry.getValue();
  }

复制代码

1
2
3
4
5
6
7


#### HashMap头添加扩容导致死循环分析


* 在JDK1.7之前HashMap的扩容是从头部添加元素,导致在多线程环境下put操作使得Entry链表形成环形数据结构,则next节点永不为null,导致死循环获取Entry
* 主要是在put元素HashMap扩容阶段:JDK1.7的扩容函数为:

ini复制代码void transfer(Entry[] newTable) {
Entry[] src = table; //旧的hash表
int newCapacity = newTable.length;
//下面这段代码的意思是:
// 从OldTable里摘一个元素出来,然后放到NewTable中
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next; //注意添加顺序,是由头部开始添加,这个地方会导致循环的产生,加入线程A执行完毕挂起了,此时e = 3 , next = 7 , 3.next = 7的
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}

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

* 分析,如果对于线程A , B分别对HashMap扩容
1. 对于上方的扩容函数当两个线程A,B运行到Entry<K,V> next = e.next, 时
2. 对于 旧Hash表中的3 ->7线程A此时 e =3, e.next = next = 7 , 3指向的是 7 运行到这个时候A挂起了,
3. 这个时候B开始正常运行扩容 这个时候还是 3->7 添加到新表时先添加3,在3的前面添加7 此时运行完成以后是 7->3,即7.next = 3 , 3.next = null ,这个时候还是正常的操作
4. 但是,还记得我们上面的线程A是3->7的时候挂起了,如果此时开始运行下面代码 e.next = newTable[i]; e = 3 ,而此时newTable[i]已经被线程B修改成7了,因此3.next = 7这一步替换了3步中的3.next = null,happened before原则满足 ,将3重新添加到链表头结点处,e重新赋值为7 ,还记得3中的 7.next = 3,注意这里并没有修改7.next的值哦,因此put进入死循环中
5. 为了防止这个JDK1.8优化了从尾部添加元素,安全性无法保证多线程会被覆盖!


### Hashtable(默认容量 11 ,加载因子0.75)


* 概述


和HashMap一样儿,Hashtable也是一个散列表,它存储的内容是键值对,implements Map<>
* 成员变量的含义:table, count, threshold, loadFactor, modCount


+ table是一个Entry[]数组类型,Entry就是一个单向链表,而key-value 键值对就是存储在entry中
+ count是Hashtable的大小,是保存键值对的数量
+ threshold为判断是否需要扩容 = 容量 \* 加载因子
+ loadFactor 加载因子
+ modCount用来实现fail-fast同步机制
* put方法


1. 判断value 是否为null,为空抛出异常;
2. 计算key的hash值获得key在table数组的位置index,如果table[index]元素不为空,进行迭代,如果遇到相同的key,则直接替换,并返回旧的value
3. 否则将其插入到table[index]位置上

ini复制代码public synchronized V put(K key, V value) {
// Make sure the value is not null确保value不为null
if (value == null) {
throw new NullPointerException();
}

    // Makes sure the key is not already in the hashtable.
    //确保key不在hashtable中
    //首先,通过hash方法计算key的哈希值,并计算得出index值,确定其在table[]中的位置
    //其次,迭代index索引位置的链表,如果该位置处的链表存在相同的key,则替换value,返回旧的value
    Entry tab[] = table;
    int hash = hash(key);
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            V old = e.value;
            e.value = value;
            return old;
        }
    }

    modCount++;
    if (count >= threshold) {
        // Rehash the table if the threshold is exceeded
        //如果超过阀值,就进行rehash操作
        rehash();

        tab = table;
        hash = hash(key);
        index = (hash & 0x7FFFFFFF) % tab.length;
    }

    // Creates the new entry.
    //将值插入,返回的为null
    Entry<K,V> e = tab[index];
    // 创建新的Entry节点,并将新的Entry插入Hashtable的index位置,并设置e为新的Entry的下一个元素
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
    return null;
}
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


#### Hashtable与HashMap的比较


1. HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过`synchronized` 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);


1. 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它 ;
2. **对Null key 和Null value的支持:** HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。
3. **初始容量大小和每次扩充容量大小的不同 :** ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小。
4. **底层数据结构:** JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。


##### HashMap jdk1.8红黑树


* 为何在1.8以后使用红黑树替换链表优化hash碰撞,首先链表容易维护但不利于查找O(n),红黑树是一种特殊的二叉树,其利于查找(二分查找法O(logn)),但相对应的维护比较复杂(需要左旋,右旋及更改节点颜色等)
* 因此对于数量小于等于6的hash碰撞使用链表存储,对于数量大于等于8的hash碰撞优化成使用红黑树存储,增删改查比较快捷,空出数字7是为了频繁的在6-8之间增加删除而导致不停的变化数据起到一个缓冲作用
+ 选择红黑树是为了解决二叉查找树的缺陷,二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。而红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,引入红黑树就是为了查找数据快,解决链表查询深度的问题,我们知道红黑树属于平衡二叉树,但是为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当长度大于8的时候,会使用红黑树,链表长度低于6,就把红黑树转回链表,因为根本不需要引入红黑树,引入反而会慢。


###### 红黑树


* \*\*在红黑树中的hash值一致,他是根据comporeTo()来比较数值的:\*\*比如对于常见的String键来说,相同的hash值是根据native compareTo()比较两个字符串差值来计算出红黑树的大小,因此红黑树中不存在相同数值的数据,一旦相同就表示是存储的相同的键(equals方法相同)需要替换值了
* 简单说一下什么是红黑树:**特点**
1. 根节点为黑色
2. 子节点跟他的父节点不能同时是红色:即红色节点的子节点都是黑色节点
3. 从根节点到任何一个叶子结点其经过的黑色节点个数相同
4. 所有空叶子结点都是黑色的,这里的叶子结点指的是空结点,常用 NIL 表示,一般可以省略,但在用到的时候需要补全它
+ **注意:由于特性2,3可确保没有一条路径会比其他路径长出俩倍,因此它是一个相对平衡的二叉树**,相对于普通二叉树可能退化成链表更优秀,同时相对于完全二叉树(最大跟最小最多差1且最深的节点都位于左边),平衡二叉树(左子树跟右子树深度差的绝对值不能超过1,且左右子树也是平衡二叉树)需要转化步骤更少,通过更少的更改数据既能达到相对平衡的效果,对于Hash冲突解决最优,无非还是空间换取时间操作
* 左旋跟右旋的理解
![image](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/510b513ce85b647f09aa339cb62a62464c7a5792859443a0f120b2b5ec07c5eb)
* 分析:
1. **对L进行左旋就是将L的右节点设置成x的父节点,即将L变成一个左节点==>左旋中的左是指将旋转的节点变成一个左节点**
2. **对P进行右旋就是将P的左节点设置成p的父节点,即将P变成一个右节点==>右旋中的右是指将旋转的节点变成一个右节点**


###### 查找 时间复杂度O(logN)


* 对于红黑树的查找来说
![image](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/dc4ec82a7e701df79e592c02c5caba8cbfdd94ca287d540803a05d787806cb0c)


###### 插入


* 注意:
1. 红黑树插入节点均是最底层的叶子节点处
2. 插入节点颜色为红色,不为黑色是因为根据特性3,任何一个叶子节点经过黑色节点个数相同,如果是黑色一定会更改其中个数导致不相同而需要重新左旋右旋更改颜色等多余操作,如果是红色则有可能不需要做任何操作也达到平衡了
* 假设: 待插入节点为N,其父节点为P, 其祖父节点为G,U是N的叔叔节点(P的兄弟节点),则插入红黑色有下面几种情况:
1. N是根节点,即红黑树的第一个节点
2. N的父节点P为黑色
3. P是红色的不是根节点(根节点一定是黑色),他的兄弟节点U也是红色的
4. P为红色,而U为黑色
* 对于以上1,2,3情况来说,他们都不涉及旋转,只涉及颜色翻转而已
1. 对于情况1 , 2 并不影响红黑树的性质即不会破坏平衡,直接插入即可
2. 对于3来说,P,U均为红色,直接变色即可,P和U变成黑色,G变成红色,如果G是根节点,直接变黑,否则则将G作为插入节点,递归向上检查是否造成不平衡
* 对于情况4来说,**P为红色,U为黑色**,这种分成四种情况:
1. P是G的左孩子,若N是P的左孩子,则将祖父节点G右旋即可
2. P是G的左孩子,若N是P的右孩子,则将P先左旋转(左转后情况如1),然后再将祖父节点G右旋转
3. P是G的右孩子,若N是P的右孩子,则将祖父节点G左旋转即可
4. P是G的右孩子,若N是P的左孩子,则先将P右旋转(右旋转后情况如3),然后在将祖父节点G左旋转
* 由于添加只能是叶子节点,所以只能是这四种情况,注意黑色U可能是Nul节点的


###### 删除


* 对于删除操作来说,只能是一下四种情况:
1. 删除节点的左,右子树都不为Nul;
2. 删除节点的左子树为Nul,右子树非空;
3. 删除节点的右子树为Nul,左子树非空;
4. 删除节点的左,右子树均为Nul;
在TreeMap源码中可以查看以上四种情况分析: **对于1来说,删除节点最终还是删除非空的一个叶子节点**

ini复制代码 private void deleteEntry(TreeMapEntry<K,V> p) {
modCount++;
size–;

// If strictly internal, copy successor's element to p and then make p
// point to successor.
if (p.left != null && p.right != null) {  //对应第一种情况,左,右子树均不为Nul
    TreeMapEntry<K,V> s = successor(p);
    p.key = s.key;
    p.value = s.value;
    p = s;
} // 没有返回,将第一种情况转化成了 2,3,4 种情况

// Start fixup at replacement node, if it exists.
TreeMapEntry<K,V> replacement = (p.left != null ? p.left : p.right);

if (replacement != null) { //对应于2,3中情况,左,右子树有一个为Nul
    // Link replacement to parent
    replacement.parent = p.parent;
    if (p.parent == null) 
        root = replacement;
    else if (p == p.parent.left)
        p.parent.left  = replacement;
    else
        p.parent.right = replacement;

    // Null out links so they are OK to use by fixAfterDeletion.
    p.left = p.right = p.parent = null;

    // Fix replacement
    if (p.color == BLACK)
        fixAfterDeletion(replacement);
} else if (p.parent == null) { // return if we are the only node.
    root = null;
} else { //  第四种情况,左右子树均为Nul
    if (p.color == BLACK)
        fixAfterDeletion(p);

    if (p.parent != null) {
        if (p == p.parent.left)
            p.parent.left = null;
        else if (p == p.parent.right)
            p.parent.right = null;
        p.parent = null;
    }
}

}

* 以上可以至源码中查看算法实现细节,如有不当之处,欢迎给予评论指正!


**本文转载自:** [掘金](https://juejin.cn/post/6971252098369323022)

*[开发者博客 – 和开发相关的 这里全都有](https://dev.newban.cn/)*

Redis集群搭建及原理 一、哨兵模式 二、Redis集群

发表于 2021-06-08

一、哨兵模式

在 redis3.0之前,redis使用的哨兵架构,它借助 sentinel 工具来监控 master 节点的状态;如果 master 节点异常,则会做主从切换,将一台 slave 作为 master。

哨兵模式的缺点:

(1)当master挂掉的时候,sentinel 会选举出来一个 master,选举的时候是没有办法去访问Redis的,会存在访问瞬断的情况;若是在电商网站大促的时候master给挂掉了,几秒钟损失好多订单数据;

(2)哨兵模式,对外只有master节点可以写,slave节点只能用于读。尽管Redis单节点最多支持10W的QPS,但是在电商大促的时候,写数据的压力全部在master上。

(3)Redis的单节点内存不能设置过大,若数据过大在主从同步将会很慢;在节点启动的时候,时间特别长;(从节点上有主节点的所有数据)

二、Redis集群

1、Redis集群的介绍

Redis集群是一个由多个主从节点群组成的分布式服务集群,它具有复制、高可用和分片特性。Redis集群不需要sentinel哨兵也能完成节点移除和故障转移的功能。需要将每个节点设置成集群模式,这种集群模式没有中心节点,可水平扩展,据官方文档称可以线性扩展到上万个节点(官方推荐不超过1000个节点)。redis集群的性能和高可用性均优于之前版本的哨兵模式,且集群配置非常简单。

2、Redis集群的优点:

(1)Redis集群有多个master,可以减小访问瞬断问题的影响;

若集群中有一个master挂了,正好需要向这个master写数据,这个操作需要等待一下;但是向其他master节点写数据是不受影响的。

(2)Redis集群有多个master,可以提供更高的并发量;

(3)Redis集群可以分片存储,这样就可以存储更多的数据;

3、Redis集群的搭建

Redis的集群搭建最少需要3个master节点,我们这里搭建3个master,每个下面挂一个slave节点,总共6个Redis节点;(3台机器,每台机器一主一从)

第1台机器: 192.168.1.1 8001端口 8002端口

第2台机器: 192.168.1.2 8001端口 8002端口

第3台机器: 192.168.1.3 8001端口 8002端口

第一步:创建文件夹

1
bash复制代码mkdir -p /usr1/redis/redis-cluster/8001 /usr1/redis/redis-cluster/8002

第二步:将redis安装目录下的 redis.conf 文件分别拷贝到8001目录下

1
bash复制代码cp /usr1/redis-5.0.3/redis.conf /usr1/redis/redis-cluster/8001

第三步:修改redis.conf文件以下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
yaml复制代码port 8001 daemonize yes
pidfile "/var/run/redis\_8001.pid" #指定数据文件存放位置,必须要指定不同的目录位置,不然会丢失数据 dir /usr1/redis/redis-cluster/8001/ #启动集群模式
cluster\-enabled yes

#集群节点信息文件,这里800x最好和port对应上
cluster\-config-file nodes-8001.conf

# 节点离线的超时时间
cluster\-node-timeout 5000 #去掉bind绑定访问ip信息
#bind 127.0.0.1 #关闭保护模式
protected\-mode no

#启动AOF文件
appendonly yes

#如果要设置密码需要增加如下配置:
#设置redis访问密码
requirepass redis\-pw

#设置集群节点间访问密码,跟上面一致
masterauth redis\-pw

第四步,把上面修改好的配置文件拷贝到8002文件夹下,并将8001修改为8002:

1
2
3
4
bash复制代码cp /usr1/redis/redis-cluster/8001/redis.conf /usr1/redis/redis-cluster/8002 cd /usr1/redis/redis-cluster/8002/ vim redis.conf

#批量修改字符串
:%s/8001/8002/g

第五步,将本机(192.168.1.1)机器上的文件拷贝到另外两台机器上

1
2
3
4
ruby复制代码scp /usr1/redis/redis-cluster/8001/redis.conf  root@192.168.1.2:/usr1/redis/redis-cluster/8001/
scp /usr1/redis/redis-cluster/8002/redis.conf root@192.168.1.2:/usr1/redis/redis-cluster/8002/
scp /usr1/redis/redis-cluster/8001/redis.conf root@192.168.1.3:/usr1/redis/redis-cluster/8001/
scp /usr1/redis/redis-cluster/8002/redis.conf root@192.168.1.3:/usr1/redis/redis-cluster/8002/

第六步,分别启动这6个redis实例,然后检查是否启动成功

1
2
bash复制代码/usr1/redis/redis-5.0.3/src/redis-server /usr1/redis/redis-cluster/8001/redis.conf /usr1/redis/redis-5.0.3/src/redis-server /usr1/redis/redis-cluster/8002/redis.conf   
ps -ef | grep redis

第七步,使用 redis-cli 创建整个 redis 集群(redis5.0版本之前使用的ruby脚本 redis-trib.rb)

运行以上命令,完成搭建

1
css复制代码/usr1/redis/redis-5.0.3/src/redis-cli -a redis-pw --cluster create --cluster-replicas 1 192.168.1.1:8001 192.168.1.1:8002 192.168.1.2:8001 192.168.1.2:8002 192.168.1.3:8001 192.168.1.3:8002

说明:

-a :密码;

–cluster-replicas 1:表示1个master下挂1个slave; –cluster-replicas 2:表示1个master下挂2个slave。

扩展:

查看帮助命令: src/redis‐cli –cluster help

1
2
3
4
5
6
sql复制代码create:创建一个集群环境host1:port1 ... hostN:portN
call:可以执行redis命令
add\-node:将一个节点添加到集群里,第一个参数为新节点的ip:port,第二个参数为集群中任意一个已经存在的节点的ip:port
del\-node:移除一个节点
reshard:重新分片
check:检查集群状态

第八步,验证集群

(1)连接任意一个客户端

1
bash复制代码/usr1/redis/redis-5.0.3/src/redis-cli -a redis-pw -c -h 192.168.1.1 -p 8001

说明:‐a表示服务端密码;‐c表示集群模式;-h指定ip地址;-p表示端口号

(2)查看集群的信息: cluster info

(3) 查看节点列表: cluster nodes slave 对应的 master 从上面也可以看到;

从上面可以看到 slave挂在哪个 master 下面;

在 /usr1/redis/redis-cluster/8001/nodes-8001.conf 文件中存储了节点信息;

(4)进行数据操作验证;

(5)关闭集群则需要逐个进行关闭,使用命令:

1
2
css复制代码/usr/local/redis‐5.0.3/src/redis‐cli ‐a redis-pw ‐c ‐h 192.168.1.1 ‐p 8001 shutdown /usr/local/redis‐5.0.3/src/redis‐cli ‐a redis-pw ‐c ‐h 192.168.1.1 ‐p 8002 shutdown
......

注意:在创建集群的时候,需要把所有节点机器上的防火关闭,保证 Redis的服务端口和集群节点通信的 gossip 端口能通;

systemctl stop firewalld # 临时关闭防火墙

systemctl disable firewalld # 禁止开机启动

4、Redis集群原理分析

  Redis Cluster 将所有数据划分为 16384 个 slots(槽位),每个节点负责其中一部分槽位。槽位的信息存储于每个节点中。只有master节点会被分配槽位,slave节点不会分配槽位。

当Redis Cluster 的客户端来连接集群时,它也会得到一份集群的槽位配置信息,并将其缓存在客户端本地。这样当客户端要查找某个 key 时,可以直接定位到目标节点。同时因为槽位的信息可能会存在客户端与服务器不一致的情况,还需要纠正机制来实现槽位信息的校验调整。

槽位定位算法

Cluster 默认会对 key 值使用 crc16 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体槽位。

HASH_SLOT = CRC16(key) % 16384

跳转重定位

  当客户端向一个节点发出了指令,首先当前节点会计算指令的 key 得到槽位信息,判断计算的槽位是否归当前节点所管理;若槽位不归当前节点管理,这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,告诉客户端去连这个节点去获取数据。客户端收到指令后除了跳转到正确的节点上去操作,还会同步更新纠正本地的槽位映射表缓存,后续所有 key 将使用新的槽位映射表。

Redis集群节点之间的通信机制

维护集群的元数据有集中式和 gossip两种方式,Redis 的集群节点之间的通信采取 gossip 协议进行通信

(1)集中式:

优点:元数据的更新和读取,时效性非常好,一旦元数据出现变更立即就会更新到集中式的存储中,其他节点读取的时候立即就可以立即感知到;

缺点:所有的元数据的更新压力全部集中在一个地方,可能导致元数据的存储压力。zookeeper使用该方式

(2)gossip:

gossip协议包含多种消息,包括ping,pong,meet,fail等等。。

优点:元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力;

缺点:元数据更新有延时可能导致集群的一些操作会有一些滞后。

每个节点都有一个专门用于节点间通信的端口,就是自己提供服务的端口号+10000,比如7001,那么用于节点间通信的就是17001端口。 每个节点每隔一段时间都会往另外几个节点发送ping消息,同时其他几点接收到ping消息之后返回pong消息。

网络抖动

  网络抖动就是非常常见的一种现象,突然之间部分连接变得不可访问,然后很快又恢复正常。

为解决这种问题,Redis Cluster 提供了一种选项 cluster-­node-­timeout ,表示当某个节点失联的时间超过了配置的 timeout时,才可以认定该节点出现故障,需要进行主从切换。如果没有这个选项,网络抖动会导致主从频繁切换 (数据的重新复制)。

Redis集群选举原理

当 slave 发现自己的 master 变为 fail 状态时,便尝试进行 FailOver,以期成为新的 master。由于挂掉的 master 有多个 slave,所以这些 slave 要去竞争成为 master 节点,过程如下:

(1)slave1,slave2都 发现自己连接的 master 状态变为 Fail;

(2)它们将自己记录的集群 currentEpoch(选举周期) 加1,并使用 gossip 协议去广播 FailOver_auth_request 信息;

(3)其他节点接收到slave1、salve2的消息(只有master响应),判断请求的合法性,并给 slave1 或 slave2 发送 FailOver_auth_ack(对每个 epoch 只发一次ack); 在一个选举周期中,一个master只会响应第一个给它发消息的slave;

(4)slave1 收集返回的 FailOver_auth_ack,它收到超过半数的 master 的 ack 后变成新 master; (这也是集群为什么至少需要3个master的原因,如果只有两个master,其中一个挂了之后,只剩下一个主节点是不能选举成功的)

(5)新的master节点去广播 Pong 消息通知其他集群节点,不需要再去选举了。

从节点并不是在主节点一进入 FAIL 状态就马上尝试发起选举,而是有一定延迟,一定的延迟确保我们等待FAIL状态在集群中传播,slave如果立即尝试选举,其它masters或许尚未意识到FAIL状态,可能会拒绝投票。

延迟计算公式:DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms

SLAVE_RANK:表示此slave已经从master复制数据的总量的rank。Rank越小代表已复制的数据越新。这种方式下,持有最新数据的slave将会首先发起选举(理论上)

Redis集群为什么至少需要三个master节点,并且推荐节点数为奇数?

因为新master的选举需要大于半数的集群master节点同意才能选举成功,如果只有两个master节点,当其中一个挂了,是达不到选举新master的条件的。

奇数个master节点可以在满足选举该条件的基础上节省一个节点,比如三个master节点和四个master节点的集群相比,大家如果都挂了一个master节点都能选举新master节点,如果都挂了两个master节点都没法选举新master节点了,所以奇数的master节点更多的是从节省机器资源角度出发说的。

集群是否完整才能对外提供服务

当redis.conf的配置cluster-require-full-coverage为no时,表示当负责一个插槽的主库下线且没有相应的从库进行故障恢复时,集群仍然可用,如果为yes则集群不可用。

5、Java 操作 Redis 集群

方式一(Jedis):

(1)引入jedis的maven坐标

1
2
3
4
5
bash复制代码<dependency\>
<groupId\>redis.clients</groupId\>
<artifactId\>jedis</artifactId\>
<version\>2.9.0</version\>
</dependency\>

(2)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
csharp复制代码public class JedisClusterTest { public static void main(String\[\] args) throws IOException {

JedisPoolConfig config \= new JedisPoolConfig();
config.setMaxTotal(20);
config.setMaxIdle(10);
config.setMinIdle(5);

Set<HostAndPort> jedisClusterNode = new HashSet<HostAndPort>();
jedisClusterNode.add(new HostAndPort("192.168.1.1", 8001));
jedisClusterNode.add(new HostAndPort("192.168.1.1", 8002));
jedisClusterNode.add(new HostAndPort("192.168.1.2", 8001));
jedisClusterNode.add(new HostAndPort("192.168.1.2", 8002));
jedisClusterNode.add(new HostAndPort("192.168.1.3", 8001));
jedisClusterNode.add(new HostAndPort("192.168.1.3", 8002));
JedisCluster jedisCluster \= null; try { //connectionTimeout:指的是连接一个url的连接等待时间 //soTimeout:指的是连接上一个url,获取response的返回等待时间
jedisCluster = new JedisCluster(jedisClusterNode, 6000, 5000, 10, "redis-pw", config);
System.out.println(jedisCluster.set("name", "zhangsan"));
System.out.println(jedisCluster.get("name"));
} catch (Exception e) {
e.printStackTrace();
} finally { if (jedisCluster != null) {
jedisCluster.close();
}
}
}
}

方式二:SpringBoot 整合 Redis

(1)引入maven坐标

1
2
3
4
5
6
7
8
9
bash复制代码<dependency\>
<groupId\>org.springframework.boot</groupId\>
<artifactId\>spring‐boot‐starter‐data‐redis</artifactId\>
</dependency\>

<dependency\>
<groupId\>org.apache.commons</groupId\>
<artifactId\>commons‐pool2</artifactId\>
</dependency\>

(2)配置文件

1
2
3
4
5
6
7
8
9
yaml复制代码server:
port: 8080 spring:
redis:
database: 0 timeout: 3000 password: redis\-pw
cluster:
nodes: 192.168.0.61:8001,192.168.0.62:8002,192.168.0.63:8003,192.168.0.61:8004,192.168.0.62:8005,192.168.0.6
3:8006 lettuce:
pool:
max‐idle: 50 min‐idle: 10 max‐active: 100 max‐wait: 1000

(3)java代码,如下:

1
2
3
4
5
6
7
8
9
kotlin复制代码@RestController public class TestController { private static final Logger logger = LoggerFactory.getLogger(TestController.class);

@Autowired private StringRedisTemplate stringRedisTemplate;

@RequestMapping("/test\_cluster") public void testCluster() throws InterruptedException {
stringRedisTemplate.opsForValue().set("user:name", "wangwu");
System.out.println(stringRedisTemplate.opsForValue().get("user:name"));
}
}

作者:风止雨歇
链接:www.cnblogs.com/yufeng218/p…
来源:cnblogs

本文转载自: 掘金

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

Java多线程有哪几种实现方式、如何实现|Java 开发实战

发表于 2021-06-08

这是我参与更文挑战的第8天,活动详情查看: 更文挑战

本文正在参加「Java主题月 - Java 开发实战」,详情查看 活动链接


相关文章

Java多线程:Java多线程


前言

多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理” 。——以上摘自百度百科

一、生活中多线程的例子
  • 城市发展:
    • 乡村小道:可以理解为典型的单线程,当车流量较少时,可以正常同行,不会影响效率。
      在这里插入图片描述
    • 高速公路:当车流量大了之后,乡村小路开始变得拥堵,人们为了解决这个问题,就拓宽马路,增加车道,抽象的理解为,多线程同时工作。
      在这里插入图片描述
  • 实际生活中例子数不胜数,比如:吃饭时变玩手机边吃饭,边上厕所边玩手机。
二、进程和线程
  • 程序:
+ 程序是一个指令序列。
+ 程序是静态的。
  • 进程:
+ 进程就是执行程序的一次性过程。**相当于一整条高速公路**。
+ 一个线程可以包含多个线程,当然,最少有一个线程,不然这个进程毫无意义。
  • 线程:
+ 线程相当于独立的执行路径。**相当于高速公路的每一条车道。**
+ 在程序执行中,即使你没有创建线程,也会有**默认的线程**,如:main,gc等。
+ **main() 函数** 被称为主线程,是整个程序的入口。
+ 在一个线程当中,如果有多个进程,具体的调度是无法人为干预的,是由**cpu来调度的**。
+ 多个线程对同一个资源进行操作时,可能会发生错误,需要加入**并发控制**。

上面都是一些理论的知识,有些枯燥,理解记忆即可。

三、继承Tread类

实现方式:

  • 继承Tread类
  • 重写run方法
  • 创建实例调用start()方法

实现代码案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码/**
* 多线程的实现方式一 :继承Tread类,并重写run方法,创建实例调用start方法
*/
public class TestTread extends Thread{

//idea中快速重现父类方法的快捷键是 ctrl + o
@Override
public void run() {
System.out.println("我是一个线程呀~~~~");
}

public static void main(String[] args) {
//实例化线程实现类
TestTread testTread = new TestTread();
//使用start()方法启动该线程,无需调用run方法,因为在线程中他会默认调用run()方法,也就是启动后会自动执行run()方法
testTread.start();
}
}

执行结果如下:
在这里插入图片描述

四、实现Runnable接口

  • 实现Runnable接口
  • 重写run()方法
  • 创建Thread时作为参数传入
  • 用start方法

代码实现案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码/**
* 多线程的实现方式二 :实现Runnable接口,并重写run方法,创建Thread时作为参数传入,调用start方法
*/
public class TestRunnable implements Runnable{
public static void main(String[] args) {
//分开写
TestRunnable testRunnable = new TestRunnable();
Thread tread = new Thread(testRunnable);
tread.start();

//简写
new Thread(new TestRunnable()).start();
}
//和Thread一样,需要重写run()方法
@Override
public void run() {
System.out.println("我也是一个线程呀~~~");
}
}

执行结果如下:
在这里插入图片描述

五、实现Callable接口

  • 实现Callable接口
  • 重写call方法
  • 必须定义返回值
  • 抛出异常
  • ExecutorService :不展开讲,后面的文章会详细介绍
+ shutdown():停止接收新任务,原来的任务继续执行
+ shutdownNow():停止接收新任务,原来的任务停止执行
+ awaitTermination(long timeOut, TimeUnit unit):当前线程阻塞

代码实现案例:

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
java复制代码/**
* 多线程的实现方式三:实现Callable接口,重写call方法
* 需要有返回值
*/
public class TestCallable implements Callable<String> {

public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable testCallable = new TestCallable();

//创建执行服务,这是相当于线程池,本篇文章不展开讲,后面会详细介绍这几个参数和使用方法
ExecutorService service = Executors.newFixedThreadPool(1);

//执行
Future<String> result = service.submit(testCallable);

//可获取线程的返回值
String result1 = result.get();
System.out.println("我是线程的返回值:"+result1);

//停止线程
service.shutdownNow();
}

@Override
public String call() throws Exception {
System.out.println("我当然也是一个线程啦啦啦啦德玛西亚~~~");
return "我是一个线程";
}
}

执行结果如下:
在这里插入图片描述

六、总结

  • 线程只是实现Runnable或实现Callable接口,还可以继承其他类。
  • 这种方式下,多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
  • 但是编程稍微复杂,如果需要访问当前线程,必须调用Thread.currentThread()方法。下一篇详解。
  • 继承Thread类的线程类不能再继承其他父类(Java单继承决定)。
  • 注:一般推荐采用实现接口的方式来创建多线程,因为单继承多实现,提高复用性!

路漫漫其修远兮,吾必将上下求索~
如果你认为i博主写的不错!写作不易,请点赞、关注、评论给博主一个鼓励吧转载请注明出处哦

本文转载自: 掘金

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

mysql删除重复数据,仅保留一条

发表于 2021-06-08

这是我参与更文挑战的第7天,活动详情查看: 更文挑战

原创:Telami’s Blog,欢迎分享,转载请保留出处。

今天在公司加班,此为背景。

加班原因是上线,解决线上数据库存在重复数据的问题,发现了程序的bug,很好解决,有点问题的是,修正线上的重复数据。

线上库有6个表存在重复数据,其中2个表比较大,一个96万+、一个30万+,因为之前处理过相同的问题,就直接拿来了上次的Python去重脚本,脚本很简单,就是连接数据库,查出来重复数据,循环删除。

emmmm,但是这个效率嘛,实在是太低了,1秒一条,重复数据大约2万+,预估时间大约在8个小时左右。。。

盲目依靠前人的东西,而不去自己思考是有问题的!总去想之前怎么可以,现在怎么不行了,这也是有问题的!

我发现,最近确实状态不太对,失去了探索和求知的欲望,今天算是一个警醒,颇有迷途知返的感觉。

言归正传,下面详细介绍去重步骤。

1
2
3
4
5
6
7
8
9
10
11
12
13
mysql复制代码CREATE TABLE `animal` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;


INSERT INTO `pilipa_dds`.`student` (`id`, `name`, `age`) VALUES ('1', 'cat', '12');
INSERT INTO `pilipa_dds`.`student` (`id`, `name`, `age`) VALUES ('2', 'dog', '13');
INSERT INTO `pilipa_dds`.`student` (`id`, `name`, `age`) VALUES ('3', 'camel', '25');
INSERT INTO `pilipa_dds`.`student` (`id`, `name`, `age`) VALUES ('4', 'cat', '32');
INSERT INTO `pilipa_dds`.`student` (`id`, `name`, `age`) VALUES ('5', 'dog', '42');

目标:我们要去掉name相同的数据。

先看看哪些数据重复了

1
2
3
4
5
mysql复制代码select name,count(1) from student GROUP BY name having count(1) > 1;

name count(1)
cat 2
dog 2

name为cat和dog的数据重复了,每个重复的数据有两条;

Select * From 表 Where 重复字段 In (Select 重复字段 From 表 Group By 重复字段 Having Count(1)>1)

删除全部重复数据,一条不留

直接删除会报错

1
2
3
mysql复制代码DELETE FROM student WHERE NAME IN ( SELECT NAME FROM student GROUP BY NAME HAVING count( 1 ) > 1)

1093 - You can't specify target table 'student' for update in FROM clause, Time: 0.016000s

原因是:更新这个表的同时又查询了这个表,查询这个表的同时又去更新了这个表,可以理解为死锁。mysql不支持这种更新查询同一张表的操作

解决办法:把要更新的几列数据查询出来做为一个第三方表,然后筛选更新。

1
mysql复制代码DELETE FROM student WHERE NAME IN (select t.name from ( SELECT NAME FROM student GROUP BY NAME HAVING count( 1 ) > 1)t)

删除表中删除重复数据,仅保留一条

在删除之前,我们可以先查一下,我们要删除的重复数据是啥样的

1
mysql复制代码SELECT * FROM student WHERE id NOT IN ( SELECT t.id FROM ( SELECT MIN(id) AS id FROM student GROUP BY `name` ) t )

啥意思呢,就是先通过name分组,查出id最小的数据,这些数据就是我们要留下的火种,那么再查询出id不在这里面的,就是我们要删除的重复数据。

开始删除重复数据,仅留一条

很简单,刚才的select换成delete即可

1
mysql复制代码delete FROM student WHERE id NOT IN ( SELECT t.id FROM ( SELECT MIN(id) AS id FROM student GROUP BY `name` ) t )

90万+的表执行起来超级快。

All done 👏👏👏👏~

本文转载自: 掘金

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

1…648649650…956

开发者博客

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