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

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


  • 首页

  • 归档

  • 搜索

559 N 叉树的最大深度 DFS & BFS

发表于 2021-11-21

「这是我参与11月更文挑战的第 21 天,活动详情查看:2021最后一次更文挑战」。

题目描述

这是 LeetCode 上的 559. N 叉树的最大深度 ,难度为 简单。

Tag : 「DFS」、「BFS」

给定一个 N 叉树,找到其最大深度。

最大深度是指从根节点到最远叶子节点的最长路径上的节点总数。

N 叉树输入按层序遍历序列化表示,每组子节点由空值分隔(请参见示例)。

示例 1:

1
2
3
csharp复制代码输入:root = [1,null,3,2,4,null,5,6]

输出:3

示例 2:

1
2
3
csharp复制代码输入:root = [1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]

输出:5

提示:

  • 树的深度不会超过 1000 。
  • 树的节点数目位于 [0, 104][0, 10^4][0, 104] 之间。

DFS

根据题意,可以写出如下的 DFS 实现:从 rootrootroot 的所有子节点中的取最大深度,并在此基础上加一(统计 rootrootroot 节点)即是答案。

代码:

1
2
3
4
5
6
7
8
9
10
Java复制代码class Solution {
public int maxDepth(Node root) {
if (root == null) return 0;
int ans = 0;
for (Node node : root.children) {
ans = Math.max(ans, maxDepth(node));
}
return ans + 1;
}
}
  • 时间复杂度:O(n)O(n)O(n)
  • 空间复杂度:忽略递归带来的额外空间开销,复杂度为 O(1)O(1)O(1)

BFS

同理,可以使用 BFS 进行求解:其本质是对多叉树进行层序处理,当 BFS 过程结束,意味着达到最大层数(深度),所记录的最大层数(深度)即是答案。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Java复制代码class Solution {
public int maxDepth(Node root) {
if (root == null) return 0;
int ans = 0;
Deque<Node> d = new ArrayDeque<>();
d.addLast(root);
while (!d.isEmpty()) {
int size = d.size();
while (size-- > 0) {
Node t = d.pollFirst();
for (Node node : t.children) {
d.addLast(node);
}
}
ans++;
}
return ans;
}
}
  • 时间复杂度:O(n)O(n)O(n)
  • 空间复杂度:O(n)O(n)O(n)

最后

这是我们「刷穿 LeetCode」系列文章的第 No.559 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour…

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

本文转载自: 掘金

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

dart系列之 在dart中使用数字和字符串 简介 数字 字

发表于 2021-11-21

「这是我参与11月更文挑战的第20天,活动详情查看:2021最后一次更文挑战」

简介

要想熟悉一种语言,最简单的做法就是熟悉dart提供的各种核心库。dart为我们提供了包括dart:core,dart:async,dart:math,dart:convert,dart:html和dart:io这几种常用的库。

今天给大家介绍一下dart:core中的数字和字符串的使用。

数字

dart:core中定义了三种类型的数字,分别是num,int和double。

num是所有数字的总称。int和double都是继承自num,是num的子类。

事实上,dart:core中还有以一种数据类型叫做BigInt,BigInt是一种独立的数据类型,并不是num的子类:

1
typescript复制代码abstract class BigInt implements Comparable<BigInt>

数字中最常见的操作就是将字符串转换为数字,转换可以调用parse方法,先看下num中parse方法的定义:

1
2
3
4
5
6
typescript复制代码  static num parse(String input, [@deprecated num onError(String input)?]) {
num? result = tryParse(input);
if (result != null) return result;
if (onError == null) throw FormatException(input);
return onError(input);
}

传入的input可以是十进制、也可以是十六进制,如下所示:

1
2
3
java复制代码assert(int.parse('18') == 18);
assert(int.parse('0x05') == 5);
assert(double.parse('0.50') == 0.5);

num.parse会将对应的字符转换成为int或者double类型:

1
2
dart复制代码assert(num.parse('18') is int);
assert(num.parse('0.50') is double);

parse方法还可以传入字符串对应的基数,比如是十进制还是十六进制:

1
java复制代码assert(int.parse('11', radix: 16) == 17);

上面我们讲到了如何将字符串转换成为数字,下面是如何将数字转换成为字符串,num提供了toString()方法,可以方便的将int和double转换成为string。

1
2
3
java复制代码assert(18.toString() == '18');

assert(3.1415.toString() == '3.1415');

对于小数来说,可以使用toStringAsFixed来指定小数的位数:

1
java复制代码assert(3.1415.toStringAsFixed(2) == '3.14');

如果要使用科学记数法的话,可以使用toStringAsPrecision:

1
java复制代码assert(314.15.toStringAsPrecision(2) == '3.1e+2');

字符串

所有的字符串在dart中都是以UTF-16进行编码的,dart中的string定义了很多常用的并且非常有用的方法。

比如在字符串中进行查询:

1
2
3
4
5
6
7
java复制代码assert('www.flydean.com'.contains('flydean'));

assert('www.flydean.com'.startsWith('www'));

assert('www.flydean.com'.endsWith('com'));

assert('www.flydean.com'.indexOf('flydean') == 4);

从字符串中截取子串:

1
java复制代码assert('www.flydean.com'.substring(4, 11) == 'flydean');

将字符串按照特定字符进行截取:

1
2
ini复制代码var parts = 'www.flydean.com'.split('.');
assert(parts.length == 3);

那么dart中对应中文的支持是这么样的呢? 因为dart中所有的字符都是以UTF-16来表示的,如果一个UTF-16单元能够表示对应的字符,则中文使用起来也是没有问题的:

1
2
java复制代码  assert('你好吗?'.substring(1,2) == '好');
assert('你好吗?'[1] == '好');

但是有些字符使用一个UTF-16单元是表示不了的,这时候就需要用到 characters 包对特定的字符进行处理。

字符串转换为大写或者小写:

1
2
3
4
5
6
java复制代码assert('www.flydean.com'.toUpperCase() ==
'WWW.FLYDEAN.COM');

// Convert to lowercase.
assert('WWW.FLYDEAN.COM'.toLowerCase() ==
'www.flydean.com');

dart提供了 trim()方法,可以对字符串前后端的空格进行截取:

1
java复制代码assert('  www.flydean.com  '.trim() == 'www.flydean.com');

StringBuffer

除了显示的字符串来创建字符以外,dart还提供了StringBuffer类,通过StringBuffer类我们可以自由创建字符串:

1
2
3
4
5
6
7
dart复制代码var sb = StringBuffer();
sb
..write('www.flydean.com ')
..writeAll(['is', 'very', 'good'], ' ')
..write('.');

var fullString = sb.toString();

上面代码输出:”www.flydean.com is very good.”

其中writeAll() 将传入的字符数组以特定的连接符进行连接。

总结

以上就是dart中数字和字符串的介绍。

本文已收录于 www.flydean.com/14-dart-num…

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

本文转载自: 掘金

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

谷粒商城-分布式基础篇-环境搭建 1、写在前面 2、虚拟机环

发表于 2021-11-21

1、写在前面

既个人博客系统和Java虚拟机学习后,深感技术点过于零散,于是照着尚硅谷教程写了谷粒商城这个项目。谷粒商城是一个完整的大型分布式架构电商平台,这个项目将我目前学到的知识点,以及还未学到的知识点都串在了一起,可以说学完这个项目,企业级开发这一套流程你就打通了,当然,我才刚开始学高级篇,到底能不能打通,还得等我学完再看看。不过这个项目确实是我目前开发过最复杂的项目了,公司里的项目用到的技术栈都没谷粒商城多。


谷粒商城不是一个初学者项目,在博文中有些我认为初学者应该懂的东西就不展开来讲了,要是都展开来讲的话,那篇幅也太大了,而且对于会的人来说,过于啰嗦。真的有初学者看到这篇文章的话,可以转到尚筹网项目去学习,那个项目适合初学者。

2、虚拟机环境搭建

基础篇有用到诸如MySQL、Redis之类的软件,逐一安装太过于麻烦,这里使用Docker进行安装。

这里我依然选用在Ubuntu上安装Docker,因为之前写项目有现成的VMware虚拟机镜像可以用,所以很方便。诸位也可以把自己用过的镜像打包备份,不管是镜像损坏了恢复,还是复制一份给别的项目用,都是不错的。

安装完Docker后,还需要配一下镜像仓库,不然下载镜像很慢,请看这篇博客——《docker下载镜像太慢的解决方案》

2.1 安装MySQL

拉取MySQL镜像

1
shell复制代码docker pull mysql:5.7

启动MySQL

1
shell复制代码docker run -p 3306:3306 --name mysql -v /mydata/mysql/log:/var/log/mysql -v /mydata/mysql/data:/var/lib/mysql -v /mydata/mysql/conf:/etc/mysql -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7

记住MySQL的账号密码都为root

添加MySQL配置文件

1
shell复制代码vi /mydata/mysql/conf/my.cnf

my.cnf内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码[client]
default-character-set=utf8

[mysql]
default-character-set=utf8

[mysqld]
init_connect='SET collation_connection = utf8_unicode_ci'
init_connect='SET NAMES utf8'
character-set-server=utf8
collation-server=utf8_unicode_ci
skip-character-set-client-handshake
skip-name-resolve

2.2 安装Redis

拉取Redis镜像

1
shell复制代码docker pull redis

添加Redis配置文件

1
2
shell复制代码mkdir -p /mydata/redis/conf
touch /mydata/redis/conf/redis.conf

启动Redis

1
shell复制代码docker run -p 6379:6379 --name redis -v /mydata/redis/data:/data -v /mydata/redis/conf/redis.conf:/etc/redis/redis.conf -d redis redis-server /etc/redis/redis.conf

设置Redis持久化

1
shell复制代码vi /mydata/redis/conf/redis.conf

redis.conf内容如下:

1
bash复制代码appendonly yes

2.3 安装Nacos

拉取Nacos镜像

1
shell复制代码docker pull nacos/nacos-server

启动Nacos

1
shell复制代码docker run --env MODE=standalone --name nacos -d -p 8848:8848 nacos/nacos-server

2.4 设置容器自启动

1
2
3
shell复制代码docker update mysql --restart=always
docker update redis --restart=always
docker update nacos --restart=always

3、后端项目搭建

前置条件:

  • MAVEN
    • 设置阿里云镜像【用于更快速拉取jar包】
    • MAVEN设置JDK1.8
  • IDEA
    • 安装插件 lombok【减少代码编写】
    • 安装插件 Free Mybatis plugin【用于MyBatis接口跟xml文件跳转】
    • 安装插件Gitee【Gitee和GitHub自行选用】
    • 配置Gitee账号或GitHub账号【Gitee和GitHub自行选用】

博主用的码云,所以装了这个IDEA的Gitee插件,GitHub毕竟国外的,网络不好是一方面,顺带支持下国产软件。


3.1 初始化项目

在Gitee新建仓库如下:

新建仓库

截图的时候已经有叫gulimall的仓库了,所以建了gulimall2,这个自行更改。
创建完成后,复制仓库地址,在IDEA中新建项目进行拉取。

3.2 创建微服务模块

以商品服务为例,创建gulimall-product模块

创建模块

选择SpringBoot初始化,JDK选择1.8

SpringBoot初始化

填写模块信息,此处包名自行更改

模块信息

选择如下依赖

选择依赖

点击结束,创建成功

创建完毕

按照gulimall-product创建流程,创建其它几个模块:

  • gulimall-coupon
  • gulimall-member
  • gulimall-order
  • gulimall-ware

全部创建完毕后,项目结构如下图所示:

项目结构

3.3 Maven聚合

在gulimall的根目录下创建pom.xml文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.alageek.gulimall</groupId>
<artifactId>gulimall</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall</name>
<description>谷粒商城</description>
<packaging>pom</packaging>

<modules>
<module>gulimall-product</module>
<module>gulimall-order</module>
<module>gulimall-ware</module>
<module>gulimall-coupon</module>
<module>gulimall-member</module>
</modules>

</project>

在IDEA Maven工具中添加父工程:

添加父工程

添加完成后如下图所示:

Maven结构

3.4 修改gitignore文件

为了防止提交不相关的代码,在父工程的gitignore文件中添加如下代码:

1
2
3
4
5
6
7
8
9
bash复制代码.idea
**/.mvn
**/.gitignore
**/*.iml
**/HELP.md
**/mvnw
**/mvnw.cmd

**/target/

添加完后,可进行第一次代码提交,然后去Gitee看看提交是否成功

3.5 整合renren-fast项目

访问人人开源在Gitee的主页地址:gitee.com/renrenio

下载项目 renren-fast-vue 和 renren-fast,两个项目分别为人人开源的前端项目和后端项目,下载下来后需要把项目中的.git文件夹删除,不然提交代码就提到人人开源的项目去了,当然,也提不上去。

移动 renren-fast 文件夹到gulimall项目文件夹下,并且在父工程的pom文件中添加该模块:

1
xml复制代码<module>renren-fast</module>

添加后的pom文件如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.alageek.gulimall</groupId>
<artifactId>gulimall</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall</name>
<description>谷粒商城</description>
<packaging>pom</packaging>

<modules>
<module>gulimall-product</module>
<module>gulimall-order</module>
<module>gulimall-ware</module>
<module>gulimall-coupon</module>
<module>gulimall-member</module>
<module>renren-fast</module>
</modules>

</project>

3.6 数据库初始化

3.6.1 gulimall

用Navicat或者其它工具连接前文安装的MySQL数据库,创建五个数据库如下:

  • gulimall_pms(商品)
  • gulimall_oms(订单)
  • gulimall_sms(营销)
  • gulimall_ums(用户)
  • gulimall_wms(库存)

其中创建数据库时,字符集选择 utf8mb4

接下来需要获取尚硅谷官方提供的资料,其中有创建数据库表的SQL语句,获取方法如下:

  • 前往尚硅谷公众号,回复“谷粒商城”关键字,会有百度云分享链接返回【语句有错误,不推荐】
  • 或者直接访问我这个百度云分享链接 ,提取码:yyds【推荐】

解压后进入路径docs/代码/sql下,有如下几个文件:

SQL文件

根据上面创建的数据库,按名字打开文件,把SQL语句复制到Navicat中执行,生成对应的表结构


注1:直接执行文件可能会有编码问题,所以把文件打开后复制代码更为保险

注2:官方提供的SQL文件是有以下两个错误:

  • gulimall_pms.sql 中创建 pms_attr 语句中少了value_type字段,我提供的语句中以补全
  • gulimall_pms.sql 中创建 pms_spu_info 语句中所属分类id的字段名为 catalog_id,而其他表中所属分类id的字段名叫 catelog_id,这块我的代码里没有纠正,因为我发现这个错误的时候已经逆向工程生成代码好久了,而且也不是什么严重错误,所以就没改,诸位要是觉得别扭可以直接改掉。

3.6.2 renren-fast

创建 renren-fast 运行所需数据库,取名为 gulimall-admin,编码与前面保持一致,在 Navicat 中运行下列SQL语句:

renren-fast-sql

修改配置文件application-dev.yml中的数据库地址、数据库名,以及账号密码

4、前端项目搭建

前置条件:

  • Visual Studio Code
    • 安装插件 Auto Close Tag
    • 安装插件 Auto Rename Tag
    • 安装插件 Chinese (Simplified) (简体中文) Language Pack for Visual Studio Code
    • 安装插件 HTML CSS Support
    • 安装插件 HTML Snippets
    • 安装插件 JavaScript (ES6) code snippets
    • 安装插件 Live Server
    • 安装插件 open in browser
    • 安装插件 Vetur
  • node,js
    • 安装版本为10.16.3的node.js

node.js 不一致可能会导致项目编译不通过,博主不信邪,第一次安装的时候下了最新版,结果你们懂的

安装完 node 后,配置npm淘宝镜像,与前面Docker一样,也是为了更快:

1
shell复制代码npm config set registry http://registry.npm.taobao.org/

4.1 整合renren-fast-vue项目

用 VS Code 打开前文下载的 renren-fast-vue 项目,点击右下角打开命令行,执行如下命令:

npm install

显示如下即为安装成功:

npm install成功

4.2 测试

启动后端 renren-fast 项目,然后在 VS code 命令行执行下列命令:

1
shell复制代码npm run dev

前端项目启动后会自动在浏览器打开一个页面,没打开的自行访问地址http://localhost:8001/#/login,页面如下:

renren-fast-vue首页

观察验证码是否显示,以及后端服务是否有报错。

5、写在后面

5.1 项目资源

博主在码云的源码地址:谷粒商城-基础篇

项目在B站的教程链接:Java项目《谷粒商城》Java架构师 | 微服务 | 大型电商项目

5.2 博客跳转

谷粒商城的高级篇博主还没看,此处先留空


基础篇除了环境搭建,其它的其实就是crud了,而且这部分教程很大一部分在讲前端,后面教程其实直接把写好的前端代码拿来用了,诸位不想学前端的也可以跳过,这部分做个了解就行。
以下是教程对基础篇的一个总结:

基础篇总结

可以看到,真的是很基础的东西,都是尚硅谷前期课程以及项目中有的东西,当然,这些除了人人开源相关的东西,其他的都是要会的,基础都不打牢,后面进阶就不好弄了。


尚硅谷是真的良心,感谢尚硅谷的开源,让贫穷的学生有机会系统的学习,大家多去B站点点赞,有钱的捧个钱场,没钱的捧个人场。

本文转载自: 掘金

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

深入理解 volatile 关键字 前言 内存可见性与JMM

发表于 2021-11-21

前言

手写一个双重检验锁的单例设计模式相信大家分分钟就能解决,但有一个问题为什么我需要在类变量前面加一个 volatile 关键字,百度了下一发现它主要用来解决两个问题,即变量内存可见性以及禁止指令重排序,并发编程的三要素除了原子性之外它占了两个,为何它如此强大,一个小小的 volatile 所包含的知识点可能会远远超过你的想象。

内存可见性与JMM

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复制代码//程序清单 1.1
public class Test {
//注意我在这里没有加 volatile
private static boolean tag = false;

public static void main(String[] args) {

new Thread(()->{
System.out.println("子线程 begin");

while (!tag){

}

System.out.println("子线程 end");
}).start();

try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}

tag = true;

}

}

首先我们来看下上面的程序清单,我定义了一个 tag 默认值为 false ,开启了一个子线程如果 tag 的值为 false 那么 “子线程 end” 这句永远都没法输出出来,在主线程中 tag 的值被赋值为 true ,运行这段代码你会发现无论如何你运行多少遍 “子线程 end” 这句永远都没法打印出来,追究其原因呢是因为在 Java 中有一个Java内存模型的概念简称JMM。

线程、主内存、工作内存之间的关系.png

  • Java内存模型的主要目标是定义程序中各种变量的访问规则,即虚拟机中将变量从主内存中拿出以及将变量存储到内存中的底层细节。
  • JMM规定所有的变量存储在主内存中,每一个线程都拥有它们自己的工作内存,线程的工作内存保存了被该线程使用到的主内存变量的副本拷贝。
  • 线程对变量的所有操作都必须在线程内存的工作内存中处理,不能直接读取主内存中的变量,不同线程之间也无法访问对方工作内存中的变量,线程之间数据的传递需要在主内存中完成。
  • 这里的变量不包含局部变量和方法参数,因为这两者本身属于线程私有的不会共享自然也就不会出现竞争问题。

因为Java内存模型内存可见性的问题,在上述程序清单中尽管在主线程中将变量 tag 的值改为 true,但是有可能没有及时同步到主内存中,就算同步到主内存中了 thread 的工作内存也可能没有及时拿到主内存最新的数据拷贝,所以 thread 中使用还是之前拿到值为 false 拷贝,意料中的 “子线程 end” 并没有被打印出来。

JMM中8大原子性操作

从主内存中拿到数据并进行读写的操作可以拆分为JMM模型中的8大原子性操作:

  • read(读取):从主内存中读取数据
  • load(载入):将主内存读取到的数据写到工作内存
  • use(使用):从工作内存读取数据计算
  • assign(赋值):将计算好的值重新赋值到工作内存中
  • store(存储):将工作内存数据写入主内存
  • write(写入):将存入的数据变量值赋值给主内存中的共享变量
  • lock(锁定):将主内存变量加锁
  • unlock(解锁):将主内存变量解锁

JMM内存模型的8大原子操作.png

内存可见性问题解决

使用 volatile 关键字可以解决程序清单1.1中的内存可见性问题,在 tag 变量前面加上 volatile 修饰,使用 volatile 修饰的变量同时具备两种特性:

  • 保证此变量对所有线程可见,这里指当一个线程修改了这个变量的值那么这个修改对其它线程是立即可见的,比如说线程A修改了变量的值然后将新值回写到主内存中,线程B在线程A将新值回写到主内存中之后在从主内存中进行读取操作,新变量值才会对线程B可见。
  • 禁止指令重排优化,你写的java代码不一定会按照你写的顺序运行,JVM会根据一系列复杂的算法优化你写的代码的顺序,如果两行代码之间没有依赖关系那么这两行代码很可能会被“指令重排序”,即这两行代码的顺序是颠倒过来的,在单线程模式模型下指令重排没有任何问题,但是在并发场景下经常会出现稀奇古怪的问题,在很多场景比如说双重校验锁的单例设计模式中我们就很有必要进行避免指令重排。

解决问题了内存可见,就一定能拿到正确的值吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码//程序清单1.2
public class Test {

private static volatile int sum = 0;

public static void main(String[] args) {

for (int i = 0;i<20;i++){
Thread myThread = new Thread(()->{
for (int j =0;j<1000;j++){
accumulation();
}
});
myThread.start();
}

while (Thread.activeCount()>1){
Thread.yield();
}

System.out.println(sum+"");
}

static void accumulation(){
sum++;
}
}

看下上面的代码,我已经用 volatile 修饰了 sum 变量,按理说我解决了内存可见性问题,确保了 sum 变量在一个线程中被改变了值其它线程中都能拿到改变后的值,那么按照我代码中的写法最终的值应该是 20000 ,实际运行结果却是千奇百怪,有 20000 的也有 18951 的或是其它的值,为什么呢 ? 实际上我们写的java代码最终会被编译成 Class 文件,java程序最终执行的是它的字节码指令,使用 jclasslib 可以帮助我们查看字节码,这里我们看 accumulation 函数的字节码,如下程序清单1.3

1
2
3
4
5
6
js复制代码//程序清单1.3
0 getstatic #11 <Test.sum>
3 iconst_1
4 iadd
5 putstatic #11 <Test.sum>
8 return

通过 getstatic 指令将 sum 的值取到操作数栈的栈顶,volatile 可以保证在这一步中 sum 的值是正确的,但是在执行 iconst_1 、 iadd 指令即压栈和相加操作的时候其它线程很可能已经先一步执行了这两步指令将 sum 的值加大了所以也就导致了最终的值与我们所期望的值不一致的原因,通过这个例子你会发现 volatile 确实可以解决内存可见性问题但不表示使用它修饰的变量是线程安全的。

双重校验锁的单例设计模式为什么需要加 volatile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码/**
* @author by tengfei on 2021/11/16.
* @description:双重校验锁的单例设计模式 程序清单1.4
*/
public class Singleton {

private static volatile Singleton singleton;

private Singleton(){

}

public static Singleton getSingleton(){
if (singleton == null){
synchronized (Singleton.class){
if (singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}

我们来解决下在前言中提到过的单例设计模式加 volatile 的问题,synchronized 加锁的目的自然是为了在多线程的情况下避免 Singleton 对象被重复创建的情况,但问题就出在如果不加 volatile 那么 new 的操作会出现“半初始化”的问题,简单来讲就是你初始化的过程出现了bug,是失败的初始化,可以从字节码的角度来理解为什么会出现半初始化的情况。

1
2
3
4
5
6
7
8
9
10
java复制代码//程序清单1.5
10 monitorenter
11 getstatic #2 <Singleton.singleton>
14 ifnonnull 27 (+13)
17 new #3 <Singleton>
20 dup
21 invokespecial #4 <Singleton.<init>>
24 putstatic #2 <Singleton.singleton>
27 aload_0
28 monitorexit

核心在第 17、20、21、24 这四行指令当中

  1. 执行 new 指令通过 #3 符号引用去常量池中找到 Singleton 这样一个 Class 来创建一个 Singleton 对象,但此刻这个对象仅仅只是个壳子,还没有执行构造器函数。
  2. 执行 dup 指令分配内存空间。
  3. 执行 invokespecial 调用 init 方法,也就是执行构造函数。
  4. putstatic 给 singleton 变量赋值

执行 new 操作创建一个对象的流程大致就是这4步,问题出在第3、4两步,你可以发现 invokespecial 和 putstatic 这两个指令处理的都是符号引用,并没有直接的依赖关系,所以从JVM的角度来讲它两是没什么关联的,因此这里会出现指令重排的可能性,先执行第4步再执行第3步,顺序也就变成了 1 2 4 3,当执行完 4 还没 开始执行 3 的时候,另一个线程开始调用 getSingleton 方法,此刻因为 singleton 已经被赋值了,所以当程序直接拿过去用的时候因为还没执行 init 方法,对象内部假设在构造函数中定义了一些代码此刻还没被执行,所以就会出现空指针的情况,为了避免这种情况发生就需要加上 volatile 来禁止指令重排序。

JMM控制总线与总线嗅探

控制总线.png
首先我们来了解下什么是控制总线,控制总线用二进制信号对所有连接在系统总线上的部件的行为进行同步。

现代计算机CPU运算速度是极高的,从主内存中拿数据的操作往往跟不上CPU的运算速度从而造成CPU资源极大的浪费,为了避免这种浪费会在主内存和CPU之间加上一个速度能跟上CPU的高速缓存区,内存中的数据会copy到高速缓存区中,CPU直接跟高速缓存区打交道,这样做无疑可以提高CPU的效率,但是在多CPU的情况下就会出现缓存一致性的问题,当多个CPU的运算任务涉及到同一块内存区域的时候将可能导致各自的缓存数据不一致的情况,为了规避这种情况就需要定义一个“缓存一致性协议”。

缓存一致性协议规定了处理器在读写数据的时候需要遵循的一些规范,当CPU写数据的时候如果发现其它的CPU中也包含这个数据的副本拷贝那么会发出通知其它的CPU这个数据已经过时了是无效的,其它的CPU会把这个数据设置为无效,当它们需要这个数据的时候会从内存中重新读取。

每个处理器通过监听在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存中。(参考自其它文章,引用在文末参考资料中)

vloatile 的原子性

原子性指你的操作过程是不受外界干扰可以一次性操作完成的,在前面单例设计模式的例子中已经证明了 volatile 不是原子性的,它仅仅只是帮助我们实现了内存可见性以及禁止指令重排序,在程序清单1.2中我用 sum++ 的操作验证了如果在JVM中你的操作步骤是受干扰的那么 volatile 不能保证线程安全,但是针对一些原子性操作如程序清单1.1仅仅只是只是对 volatile 修饰的变量进行读写的操作是线程安全的,所以仅仅只是对 volatile 修饰的变量进行单纯的赋值是安全的,但是进行一些复合性的操作比如 i++ 或者 flag=!flag 的操作无法确保原子性自然也就会出现线程安全问题。

指令重排序问题

在 as-if-serial 语意的规则下(不管怎么重排序在单线程下程序执行的结果都是不可变的),编译器和处理器会为了提高程序执行的效率对指令进行重排,但是在并发的情况下会带来二义性按照不同的执行逻辑会带来不同的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码//程序代码 2-1 
public void test(){
int a = 1;
int b = 8;
System.out.println(a+b);
}

//2-2
public void test2() {

int a = 0;

int b = a;
}

如上代码 2-1 a 变量和 b 变量仅仅只是赋值最后相加输出的操作,所以从逻辑上而言两者之间没有任何依赖关系,这种情况下为了提高执行效率可能会对指令重排序,在 2-2 中 a 和 b 之间存在依赖关系,b 的值区别与 a 的值所以 2-2 不会发生重排,简单的归纳就是如果两个指令之间存在依赖关系那么就不会发生重排,下面的表格中归纳了在哪些情况下不会发生指令重排。

名称 代码示例 说明
写后读 a=1;b=a 写一个变量后在读这个变量
写后写 a=1;a=2 写一个变量后在写这个变量
读后写 a=b;b=1 读一个变量后在写这个变量

happens-before

可以使用 happens-before 关系来描述两个操作之间的执行顺序,由于这两个操作可以在一个线程之内,也可以是在不同线程之间。因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证,如果 A happens-before B 那么可以认定 A 对 B 可见,简单来讲 happens-before 描述的是程序间可见性的一种规则,具体的场景如下:

1、程序次序规则:在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变。

2、管程锁定规则:就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)

3、volatile变量规则:就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。

4、线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。

5、线程终止规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。也称线程join()规则。

6、线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。

7、传递性规则:这个简单的,就是happens-before原则具有传递性,即hb(A, B) , hb(B, C),那么hb(A, C)。

8、对象终结规则:这个也简单的,就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。

volatile特性的实现原理(何为内存屏障)

如果你看过JVM的源码(反正我没看),在HotSpot源码中针对 volatile 做了特殊处理,也就是发现当前变量是被 volatile 修饰的那么会在汇编指令前面就加上 lock 指令,该指令在执行过程中会生成相应的内存屏障,lock 指令是确保 volatile 实现可见性以及禁止指令重排的关键。

1、内存屏障会禁止指令重排序,即不会把前面的指令重排到内存屏障之后也不会把后面的指令排到内存屏障之前。

2、lock 指令同时会确保将当前处理器缓存行的数据立即写回系统内存,该操作本身会引起其它CPU缓存的数据失效,写回操作会通过总线来传递数据,CPU通过总线嗅探来检查自己缓存的值是不是过期了,如果发现过期了会强制重新从系统内存里把数据读到处理器缓存。

Java内存模型把内存屏障分为了以下几个类型:

屏障类型 指令示例 说明
LoadLoad Barriers Load1;LoadLoad;Load2 该屏障确保Load1数据的装载先于Load2及其后所有装载指令的的操作
StoreStore Barriers Store1;StoreStore;Store2 该屏障确保Store1立刻刷新数据到内存(使其对其他处理器可见)的操作先于Store2及其后所有存储指令的操作
LoadStore Barriers Load1;LoadStore;Store2 确保Load1的数据装载先于Store2及其后所有的存储指令刷新数据到内存的操作
StoreLoad Barriers Store1;StoreLoad;Load2 该屏障确保Store1立刻刷新数据到内存的操作先于Load2及其后所有装载装载指令的操作。它会使该屏障之前的所有内存访问指令(存储指令和访问指令)完成之后,才执行该屏障之后的内存访问指令

内存屏障.png

1、 volatile 的写操作是在指令前后分别插入指令。

2、 volatile 的读操作则是在指令后面插入两个指令。

3、 StoreLoad 屏障是一个全能型的屏障,它同时具有其他三个屏障的效果。所以执行该屏障开销会很大,因为它使处理器要把缓存中的数据全部刷新到内存中。

参考资料

1、阿里面试官没想到,一个Volatile我能跟他扯半个小时

2、Volatile如何保证线程可见性之总线锁、缓存一致性协议

3、volatile 关键字,你真的理解吗?

4、深入理解Java虚拟机

5、happens-before 规则

6、volatil和内存屏障

7、一文理解内存屏障

本文转载自: 掘金

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

喵星人宠物研究所 数据采集 数据分析 猫图生成

发表于 2021-11-21
  • 一起用代码吸猫!本文正在参与【喵星人征文活动】。

最近掘金开展了《用代码“吸猫”》的活动,这个活动发起了两个灵魂拷问:

  • 你有猫么?
  • 你羡慕有猫的人吗?

我的回答是,我没有猫,也不羡慕有猫的人。活动要求用代码来吸猫,看了这个活动发现,对猫感兴趣的小伙伴还真不少啊,有些群友也老是给我晒猫,不禁让我对喵星人产生了好奇。

想吸猫肯定得先买猫把?想买猫至少得先了解猫吧? 作为一个对猫完全不了解的人,今天就借着这个活动好好了解一下各种宠物猫。

这里我找到了一个专门交易猫猫的网站-猫猫交易网:www.maomijiaoyi.com/

其中的猫咪品种栏目列出了各种类型的宠物猫:
image-20211120115756303
我们可以采集一下其中的数据学习一下各种宠物猫的特点。

最终本文得到了如下效果:
image-20211121005132096.png

数据采集

首先我们爬取主页可以爬到的链接列表:

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
python复制代码 from lxml import etree
 import requests
 ​
 headers = {
     "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
 }
 url_base = "http://www.maomijiaoyi.com"
 session = requests.Session()
 ​
 # 访问猫咪品种入口页,获取各品种详情页的链接
 url = url_base+"/index.php?/pinzhongdaquan_5.html"
 res = session.get(url, headers=headers)
 html = etree.HTML(res.text)
 main_data = []
 for a_tag in html.xpath("//div[@class='pinzhong_left']/a"):
     url = url_base+a_tag.xpath("./@href")[0]
     pet_name, pet_price = None, None
     pet_name_tag = a_tag.xpath("./div[@class='pet_name']/text()")
     if pet_name_tag:
         pet_name = pet_name_tag[0].strip()
     pet_price_tag = a_tag.xpath("./div[@class='pet_price']/span/text()")
     if pet_price_tag:
         pet_price = pet_price_tag[0].strip()
     print(pet_name, pet_price, url)
     main_data.append((pet_name, pet_price, url))

打印结果如下:

image-20211120153713167

我想更多的了解猫就必须点进详情页,看看详细的属性:

image-20211120160204511

分别解析这三部分的数据,测试对这第一个链接解析:

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
python复制代码 pet_name, pet_price, url = main_data[0]
 res = session.get(url, headers=headers)
 html = etree.HTML(res.text)
 row = {}
 # 解析基本属性
 for text in html.xpath("//div[@class='details']//text()"):
     text = text.strip()
     if not text:
         continue
     if text.endswith(":"):
         key = text[:-1]
     else:
         row[key] = text
 row["参考价格"] = pet_price
 # 解析外观属性
 for shuxing in html.xpath("//div[@class='shuxing']/div"):
     name, v = shuxing.xpath("./div/text()")
     row[name.strip()] = v.strip()
 row["链接"] = url
 # 解析详细说明
 titles = html.xpath(
     "//div[@class='content']/div[@class='property_title']/div/text()")
 property_tags = html.xpath(
     "//div[@class='content']/div[@class='property_list']/div")
 for title, property_tag in zip(titles, property_tags):
     p_texts = []
     for p_tag in property_tag.xpath(".//p|.//div"):
         p_text = "".join([t.strip()
                           for t in p_tag.xpath(".//text()") if t.strip()])
         if p_text:
             p_texts.append(p_text)
     text = "\n".join(p_texts)
     row[title] = text
 row

可以看到前两部分的数据都非常顺利的解析出来:

image-20211120161536882

对于第三部分的数据也顺利的解析出来了:

image-20211120162228130

除了文本说明信息外,我们还需要保存图片。下面解析图片网址并下载:

1
2
3
4
5
6
7
8
9
python复制代码 img_urls = [
     url_base+url for url in html.xpath("//div[@class='big_img']/img/@src") if url]
 row["图片地址"] = img_urls
 ​
 for i, img_url in enumerate(img_urls, 1):
     with requests.get(img_url) as res:
         imgbytes = res.content
     with open(f"imgs/{pet_name}{i}.jpg","wb") as f:
         f.write(imgbytes)

可以看到几张图片都顺利的下载下来:

image-20211120164613370

那么我们可以整理一下网站的代码,将文本数据保存到Excel中,并将图片保存到文件中:

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
python复制代码 import pandas as pd
 from lxml import etree
 import requests
 ​
 headers = {
     "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
 }
 url_base = "http://www.maomijiaoyi.com"
 session = requests.Session()
 ​
 # 访问猫咪品种入口页,获取各品种详情页的链接
 url = url_base+"/index.php?/pinzhongdaquan_5.html"
 res = session.get(url, headers=headers)
 html = etree.HTML(res.text)
 main_data = []
 for a_tag in html.xpath("//div[@class='pinzhong_left']/a"):
     url = url_base+a_tag.xpath("./@href")[0]
     pet_name, pet_price = None, None
     pet_name_tag = a_tag.xpath("./div[@class='pet_name']/text()")
     if pet_name_tag:
         pet_name = pet_name_tag[0].strip()
     pet_price_tag = a_tag.xpath("./div[@class='pet_price']/span/text()")
     if pet_price_tag:
         pet_price = pet_price_tag[0].strip()
     main_data.append((pet_name, pet_price, url))
 data = []
 for pet_name, pet_price, url in main_data:
     res = session.get(url, headers=headers)
     html = etree.HTML(res.text)
     row = {}
     # 解析基本属性
     for text in html.xpath("//div[@class='details']//text()"):
         text = text.strip()
         if not text:
             continue
         if text.endswith(":"):
             key = text[:-1]
         else:
             row[key] = text
     row["参考价格"] = pet_price
     # 解析外观属性
     for shuxing in html.xpath("//div[@class='shuxing']/div"):
         name, v = shuxing.xpath("./div/text()")
         row[name.strip()] = v.strip()
     row["链接"] = url
     # 解析详细说明
     titles = html.xpath(
         "//div[@class='content']/div[@class='property_title']/div/text()")
     property_tags = html.xpath(
         "//div[@class='content']/div[@class='property_list']/div")
     for title, property_tag in zip(titles, property_tags):
         p_texts = []
         for p_tag in property_tag.xpath(".//p|.//div"):
             p_text = "".join([t.strip()
                               for t in p_tag.xpath(".//text()") if t.strip()])
             if p_text:
                 p_texts.append(p_text)
         text = "\n".join(p_texts)
         row[title] = text
     img_urls = [
         url_base+url for url in html.xpath("//div[@class='big_img']/img/@src") if url]
     row["图片地址"] = img_urls
     data.append(row)
     for i, img_url in enumerate(img_urls, 1):
         with requests.get(img_url) as res:
             imgbytes = res.content
         with open(f"imgs/{pet_name}{i}.jpg", "wb") as f:
             f.write(imgbytes)
 df = pd.DataFrame(data)
 df.to_excel("猫咪.xlsx", index=False)

爬取结果前几列如下:

image-20211120170733453

下载的各类猫的图片:

image-20211120170913997

有了上面的Excel数据,我们就可以分析处理了:

数据分析

首先读取Excel的数据:

1
2
3
python复制代码 import pandas as pd
 ​
 df = pd.read_excel("猫咪.xlsx")

观察数据发现,很多宠物猫存在多个别名,我们可以做一张关系图展示每种猫对应的别名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
python复制代码 from pyecharts import options as opts
 from pyecharts.charts import Graph
 ​
 links = []
 nodes = []
 nodes.append({"name": "猫", "symbolSize": 10})
 ​
 for name, alias in df[["中文学名", "别名"]].values:
     nodes.append({"name": name, "symbolSize": 10})
     links.append({"source": "猫", "target": name})
     for dest in alias.split(","):
         if name == dest:
             continue
         nodes.append({"name": dest, "symbolSize": 10})
         links.append({"source": name, "target": dest})
 c = (
     Graph(init_opts=opts.InitOpts(width="800px", height="800px"))
    .add("", nodes, links, repulsion=250,
          linestyle_opts=opts.LineStyleOpts(width=0.5, curve=0.3, opacity=0.7))
    .set_global_opts(title_opts=opts.TitleOpts(title="宠物猫的品种"))
 )
 c.render_notebook()

image-20211120194835315

鼠标指向中心点时可以查看主名称:

image-20211120195835647

宠物猫原产地分布:

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码 from pyecharts.charts import Bar
 data = df.原产地.value_counts()
 c = (
     Bar()
    .add_xaxis(data.index.to_list())
    .add_yaxis("", data.values.tolist())
    .set_global_opts(
         xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=15)),
         title_opts=opts.TitleOpts(title="宠物猫原产地分布")
    )
 )
 c.render_notebook()

image-20211120205233268

可以看到各种宠物猫主要分布在英国、美国和苏格兰。

那么在画个树形图展示各品种的猫分布的国家:

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
python复制代码 data = []
 tmp = df.groupby("原产地", as_index=False).agg(
     品种=("中文学名", ",".join), 品种数=("中文学名", "count"))
 for src, dest in tmp.values[:, :2]:
     dests = dest.split(",")
     children = []
     data.append({"value": len(dests), "name": src, "children": children})
     for dest in dests:
         children.append({"name": dest, "value": 1})
 c = (
     TreeMap(init_opts=opts.InitOpts(width='1280px', height='560px'))
    .add("", data,
          levels=[
              opts.TreeMapLevelsOpts(
                  treemap_itemstyle_opts=opts.TreeMapItemStyleOpts(
                      border_color="#555", border_width=1, gap_width=1
                  )
              ),
              opts.TreeMapLevelsOpts(
                  color_saturation=[0.3, 0.6],
                  treemap_itemstyle_opts=opts.TreeMapItemStyleOpts(
                      border_color_saturation=0.7, gap_width=5, border_width=10
                  ),
                  upper_label_opts=opts.LabelOpts(
                      is_show=True, position='insideTopLeft', vertical_align='top'
                  )
              ),
              opts.TreeMapLevelsOpts(
                  color_saturation=[0.3, 0.5],
                  treemap_itemstyle_opts=opts.TreeMapItemStyleOpts(
                      border_color_saturation=0.6, gap_width=1
                  ),
              ),
              opts.TreeMapLevelsOpts(color_saturation=[0.3, 0.5]),
          ])
    .set_global_opts(title_opts=opts.TitleOpts(title="宠物猫原产地分布"))
 )
 c.render_notebook()

image-20211120224628155

然后看下品种体型占比:

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
python复制代码 from pyecharts.charts import Pie
 ​
 c = (
     Pie()
    .add(
         "体型",
         df.体型.value_counts().reset_index().values.tolist(),
         radius=["40%", "55%"],
         label_opts=opts.LabelOpts(
             position="outside",
             formatter="{a|{a}}{abg|}\n{hr|}\n {b|{b}: }{c} {per|{d}%} ",
             background_color="#eee",
             border_color="#aaa",
             border_width=1,
             border_radius=4,
             rich={
                 "a": {"color": "#999", "lineHeight": 22, "align": "center"},
                 "abg": {
                     "backgroundColor": "#e3e3e3",
                     "width": "100%",
                     "align": "right",
                     "height": 22,
                     "borderRadius": [4, 4, 0, 0],
                },
                 "hr": {
                     "borderColor": "#aaa",
                     "width": "100%",
                     "borderWidth": 0.5,
                     "height": 0,
                },
                 "b": {"fontSize": 16, "lineHeight": 33},
                 "per": {
                     "color": "#eee",
                     "backgroundColor": "#334455",
                     "padding": [2, 4],
                     "borderRadius": 2,
                },
            },
        ),
    )
    .set_global_opts(
         title_opts=opts.TitleOpts(title="品种体型占比"),
    )
 )
 c.render_notebook()

image-20211120231306359

可以看到只有一种猫体型是最大的,即布偶猫。
下面我们找出价格最便宜和价格最贵的猫,目前认为最低价格最低的就是最便宜的品种,最高价格最高的就是最贵的品种:

1
2
3
4
5
6
7
8
python复制代码 tmp = df.参考价格.str.split("-", expand=True)
 tmp.columns = ["最低价格", "最高价格"]
 tmp.dropna(inplace=True)
 tmp = tmp.astype("int")
 cheap_cat = df.loc[tmp.index[tmp.最低价格 == tmp.最低价格.min()], "中文学名"].to_list()
 costly_cat = df.loc[tmp.index[tmp.最高价格 == tmp.最高价格.max()], "中文学名"].to_list()
 print("最便宜的品种有:", cheap_cat)
 print("最贵的品种有:", costly_cat)
1
2
css复制代码 最便宜的品种有: ['加菲猫', '金渐层', '银渐层', '橘猫']
 最贵的品种有: ['布偶猫', '缅因猫', '无毛猫']

对于数据集中的 [‘整体’, ‘毛发’, ‘颜色’, ‘头部’, ‘眼睛’, ‘耳朵’, ‘鼻子’, ‘尾巴’, ‘胸部’, ‘颈部’, ‘前驱’, ‘后驱’] 这些列都是对猫的描述文字,我们可以整体组合起来,给喵星人做个词云图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码 import stylecloud
 from IPython.display import Image
 text = ""
 for row in df[['整体', '毛发', '颜色', '头部', '眼睛', '耳朵',
                '鼻子', '尾巴', '胸部', '颈部', '前驱', '后驱']].values:
     for v in row:
         if pd.isna(v):
             continue
         text += v
 stylecloud.gen_stylecloud(text,
                           collocations=False,
                           font_path=r'C:\Windows\Fonts\msyhbd.ttc',
                           icon_name='fas fa-cat',
                           output_name='tmp.png')
 Image(filename='tmp.png')

image-20211120234638333

然后我们分别对性格特点和生活习性等做词云图。

性格特点词云图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码 import jieba
 import stylecloud
 from IPython.display import Image
 ​
 stopwords = ["主人", "它们", "毛猫", "不会", "性格特点", "猫咪"]
 words = df.性格特点.astype("str").apply(jieba.lcut).explode()
 words = words[words.apply(len) > 1]
 words = [word for word in words if word not in stopwords]
 stylecloud.gen_stylecloud(" ".join(words),
                           collocations=False,
                           font_path=r'C:\Windows\Fonts\msyhbd.ttc',
                           icon_name='fas fa-square',
                           output_name='tmp.png')
 Image(filename='tmp.png')

image-20211121000541560

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码 import jieba
 import stylecloud
 from IPython.display import Image
 ​
 stopwords = ["主人", "它们", "毛猫", "不会", "性格特点", "猫咪"]
 words = df.生活习性.astype("str").apply(jieba.lcut).explode()
 words = words[words.apply(len) > 1]
 words = [word for word in words if word not in stopwords]
 stylecloud.gen_stylecloud(" ".join(words),
                           collocations=False,
                           font_path=r'C:\Windows\Fonts\msyhbd.ttc',
                           icon_name='fas fa-square',
                           output_name='tmp.png')
 Image(filename='tmp.png')

image-20211121000607995

猫图生成

经过上面的分析,我们已经对猫有了一个基本的了解,接下来我们对各个品种的喵星人生成一张图。

该做一张什么图好呢?我认真想了一下就做一张思维导图。

首先生成分类文本:

1
2
3
4
python复制代码 for a, bs in df.中文学名.groupby(df.体型):
     print(a)
     for b in bs.values:
         print(f"\t{b}")
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
复制代码 中型
  加菲猫
  金渐层
  英短蓝猫
  英短蓝白
  英国短毛猫
  美国短毛猫
  苏格兰折耳猫
  银渐层
  异国短毛猫
  孟买猫
  暹罗猫
  孟加拉豹猫
 大型
  布偶猫
 小型
  缅因猫
  金吉拉猫
  无毛猫
  高地折耳猫
  曼基康矮脚猫
  波斯猫
  橘猫
  阿比西尼亚猫
  德文卷毛猫

此时我将其粘贴到思维导图中,然后经常一段时间的编辑得到:

image-20211121005132096.png

本文转载自: 掘金

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

Elasticsearch7——常用设置 一、配置方式 二、

发表于 2021-11-21

[这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战]


一、配置方式

Elasticsearch 提供了很好的默认值,并且只需要很少的配置。配置文件应包含特定节点的设置以及集群配置

  • 节点设置:node.name、paths
  • 集群配置:cluster.name、 network.host

Elasticsearch 有三个配置文件:

  • elasticsearch.yml 用于配置 Elasticsearch
  • jvm.options 用于配置 Elasticsearch JVM 设置
  • log4j2.properties 用于配置 Elasticsearch 日志记录

配置文件位于 config 目录中,其默认位置取决于安装方法。例如,通过docker安装的es配置文件位于/usr/share/elasticsearch/config中。配置文件还支持环境变量替换,如下所示。

1
2
bash复制代码node.name:    ${HOSTNAME}
network.host: ${ES_NETWORK_HOST}

环境变量的值必须是简单的字符串,使用逗号分隔的字符串提供 Elasticsearch 将解析为列表的值。

集群节点分为动态和静态两种,动态节点可以在运行时更新且支持临时、持久设置;而静态设置只能在未启动或关闭的节点上使用 elasticsearch.yml。

Elasticsearch 会按以下优先顺序应用设置:

  • 临时设置(Transient):临时设置会在第一次全集群重启后被移除
  • 持久化设置(Persistent):可以全集群重启时存活下来
  • elasticsearch.yml文件
  • 默认配置

临时或持久化设置是通过API实现的,具体类型是由字段决定的。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bash复制代码PUT /_cluster/settings
{
"persistent" : {
"discovery.zen.minimum_master_nodes" : 2
},
"transient" : {
"indices.store.throttle.max_bytes_per_sec" : "50mb"
}
}

{
"acknowledged" : true,
"persistent" : {
"discovery" : {
"zen" : {
"minimum_master_nodes" : "2"
}
}
},
"transient" : { }
}

二、重要的配置

  1. 路径设置

Elasticsearch 将您索引的数据写入索引并将数据流写入data 目录。Elasticsearch 将自己的应用程序日志写入一个logs目录,其中包含有关集群运行状况和操作的信息。例如:

1
2
3
javascript复制代码path:
data: /var/data/elasticsearch
logs: /var/log/elasticsearch

warn:不要修改数据目录中的任何内容或运行可能干扰其内容的进程。如果 Elasticsearch 以外的其他东西修改了数据目录的内容,那么 Elasticsearch 可能会失败,报告损坏或其他数据不一致,或者可能会在默默丢失一些数据的情况下正常工作。不要尝试对数据目录进行文件系统备份;没有支持的方法来恢复这样的备份。相反,使用 snapshot-restore安全地进行备份。不要在数据目录上运行病毒扫描程序。病毒扫描程序可能会阻止 Elasticsearch 正常工作,并且可能会修改数据目录的内容。数据目录不包含可执行文件,因此病毒扫描只会发现误报。

  1. 集群名称设置

当节点的cluster.name配置与集群中的所有其他节点相同时,才能加入集群。默认名称是elasticsearch,不应当在不同的环境中设置相同的集群名称,避免操作失误。

1
arduino复制代码cluster.name: "docker-cluster"
  1. 节点名称设置

节点名称用于描述节点,该名称会返回在许多响应中。

1
arduino复制代码node.name: "es02"
  1. 网络主机设置

默认情况下,Elasticsearch 只绑定到环回地址,例如127.0.0.1和 [::1];如果设置为环回地址,es为开发模式,不会进行引导检查。非环回地址设置:

1
makefile复制代码network.host: 192.168.1.10
  1. 集群发现设置

Elasticsearch将结合到现有的环回地址和扫描本地端口9300到9305,与同一台服务器上运行的其他节点连接。此行为提供了一种自动连接集群,而无需进行任何配置。

如果需要连接其它服务器的节点,需要通过discovery.seed_hosts设置其它可发现的节点,地址可以是 ipv4、ipv6以及域名。例如

1
2
3
4
5
ruby复制代码discovery.seed_hosts:
- 192.168.1.10:9300
- 192.168.1.11
- seeds.mydomain.com
- [0:0:0:0:0:ffff:c0a8:10c]:9301

当 Elasticsearch 集群初次启动时, 集群引导会在第一次选举中通过计票选择主节点;可以通过可以使用cluster.initial_master_nodes设置来设置此选举列表。在开发模式下,如果没有配置seed_hosts,这一步由节点自己自动执行。

1
makefile复制代码cluster.initial_master_nodes: es01,es02
  1. 堆大小设置

默认情况下,ES 会根据节点的角色和总内存自动设置 JVM 堆大小,大多数生产环境可以使用默认大小。

要覆盖默认堆大小,请使用Xms设置最小堆和使用Xmx设置最大堆,最小值和最大值必须相同;
设置Xms和Xmx不超过总内存的 50%;
设置堆大小可以配置jvm.options文件。

docker-compose下修改配置:

1
2
3
makefile复制代码es01:
environment:
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
  1. JVM 堆转储路径设置

默认情况下,Elasticsearch 将 JVM 配置为将内存不足异常时的堆转储到默认数据目录;可以修改jvm.options文件,设置参数-XX:HeapDumpPath。

  1. GC 日志设置

默认情况下,ES 会启用垃圾收集 (GC) 日志。ES 日志在jvm.options文件中配置并输出到默认位置。默认配置每 64 MB 轮换一次日志,最多可消耗 2 GB 的磁盘空间。例如:

1
2
3
4
5
6
7
ruby复制代码# 禁用日志
-Xlog:disable

-Xlog:all=warning:stderr:utctime,level,tags

# 配置目录、轮换日志的大小
-Xlog:gc*,gc+age=trace,safepoint:file=/opt/my-app/gc.log:utctime,pid,tags:filecount=32,filesize=64m
  1. 临时目录设置

默认情况下,ES 使用启动脚本在系统临时目录正下方创建的私有临时目录。在某些 Linux 发行版上,系统实用程序会清除/tmp最近未访问过的文件和目录。如果长时间不需要使用临时目录的功能,此行为会导致在 ES 运行时删除私有临时目录。如果随后使用需要此目录的功能,则删除私有临时目录会导致问题。

为了避免异常,需要设置临时目录权限,以便只有运行 Elasticsearch 的用户才能访问它。

  1. JVM致命错误日志设置

默认情况下,Elasticsearch 将 JVM 配置为将致命错误日志写入默认日志目录。可以修改jvm.options文件,设置-XX:ErrorFile修改文件路径。

  1. 集群备份

在灾难中,快照可以防止永久性数据丢失;备份集群的唯一可靠且受支持的方法是拍摄快照,不能通过复制其节点的数据目录来备份 Elasticsearch 集群。

三、参考文章

Important Elasticsearch configuration

本文转载自: 掘金

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

Gin 框架 自动添加 RequestId 介绍 安装 快

发表于 2021-11-21

这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战

介绍

通过一个完整例子,在 Gin 框架中,为每一个 API 自动添加 RequestId 。

我们将会使用 rk-boot 来启动 Gin 框架微服务。

请访问如下地址获取完整教程:

  • rkdocs.netlify.app/cn

安装

1
2
go复制代码go get github.com/rookie-ninja/rk-boot
go get github.com/rookie-ninja/rk-gin

快速开始

开启了 meta 中间件之后,每一个请求都会自动包含如下值。

Header 键 详情
X-Request-Id 中间件会自动生成请求 ID。
X-[Prefix]-App 服务名称。
X-[Prefix]-App-Version 服务版本。
X-[Prefix]-App-Unix-Time 当前服务的 Unix 时间。
X-[Prefix]-Request-Received-Time 接收到请求的时间戳。

1.创建 boot.yaml

为了验证,我们启动了 commonService,commonService 里包含了一系列常用 API,例如 /rk/v1/healthy。

1
2
3
4
5
6
7
8
9
10
yaml复制代码---
gin:
- name: greeter # Required
port: 8080 # Required
enabled: true # Required
commonService:
enabled: true # Optional, enable common service
interceptors:
meta:
enabled: true # Optional, enable meta middleware

2.创建 main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码// Copyright (c) 2021 rookie-ninja
//
// Use of this source code is governed by an Apache-style
// license that can be found in the LICENSE file.
package main

import (
"context"
"github.com/rookie-ninja/rk-boot"
_ "github.com/rookie-ninja/rk-gin/boot"
)

// Application entrance.
func main() {
// Create a new boot instance.
boot := rkboot.NewBoot()

// Bootstrap
boot.Bootstrap(context.Background())

// Wait for shutdown sig
boot.WaitForShutdownSig(context.Background())
}

3.启动 main.go

1
go复制代码$ go run main.go

4.验证

1
2
3
4
5
6
7
8
9
10
11
yaml复制代码$ curl -vs -X GET localhost:8080/rk/v1/healthy
...
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< X-Request-Id: 78c25a06-3b34-4ecb-b9dd-7197078873c7
< X-Rk-App-Name: rk-demo
< X-Rk-App-Unix-Time: 2021-11-21T00:24:49.662023+08:00
< X-Rk-App-Version: master-2c9c6fd
< X-Rk-Received-Time: 2021-11-21T00:24:49.662023+08:00
...
{"healthy":true}

覆盖 requestId

如果我们希望自定义 requestId,需要添加一个 Header。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码func Greeter(ctx *gin.Context) {
// Override request id
rkginctx.SetHeaderToClient(ctx, rkginctx.RequestIdKey, "request-id-override")
// We expect new request id attached to logger
rkginctx.GetLogger(ctx).Info("Received request")

ctx.JSON(http.StatusOK, &GreeterResponse{
Message: fmt.Sprintf("Hello %s!", ctx.Query("name")),
})
}

// Response.
type GreeterResponse struct {
Message string
}

RequestId 会被覆盖。

1
2
3
4
5
6
7
8
9
yaml复制代码$ curl -vs -X GET "localhost:8080/v1/greeter?name=rk-dev"
...
< X-Request-Id: request-id-override
< X-Rk-App-Name: rk-demo
< X-Rk-App-Unix-Time: 2021-11-21T00:37:16.514685+08:00
< X-Rk-App-Version: master-2c9c6fd
< X-Rk-Received-Time: 2021-11-21T00:37:16.514685+08:00
...
{"Message":"Hello rk-dev!"}

如果我们启动了日志中间件,那我们会看到如下的日志。

1
bash复制代码2021-11-21T00:39:04.605+0800    INFO    basic/main.go:54        Received request        {"requestId": "request-id-override"}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码------------------------------------------------------------------------
endTime=2021-11-21T00:39:04.605647+08:00
startTime=2021-11-21T00:39:04.605483+08:00
elapsedNano=164096
timezone=CST
ids={"eventId":"request-id-override","requestId":"request-id-override"}
app={"appName":"rk-demo","appVersion":"master-2c9c6fd","entryName":"greeter","entryType":"GinEntry"}
env={"arch":"amd64","az":"*","domain":"*","hostname":"lark.local","localIP":"192.168.101.5","os":"darwin","realm":"*","region":"*"}
payloads={"apiMethod":"GET","apiPath":"/v1/greeter","apiProtocol":"HTTP/1.1","apiQuery":"name=rk-dev","userAgent":"curl/7.64.1"}
error={}
counters={}
pairs={}
timing={}
remoteAddr=localhost:61967
operation=/v1/greeter
resCode=200
eventStatus=Ended
EOE

覆盖 header 前缀

1
2
3
4
5
6
7
8
yaml复制代码---
gin:
- name: greeter
...
interceptors:
meta:
enabled: true # Enable meta middleware
prefix: "Override" # Override prefix which formed as X-[Prefix]-xxx
1
2
3
4
5
6
7
8
9
yaml复制代码$ curl -vs -X GET localhost:8080/rk/v1/healthy
...
< X-Override-App-Name: rk-demo
< X-Override-App-Unix-Time: 2021-11-21T00:40:15.761993+08:00
< X-Override-App-Version: master-2c9c6fd
< X-Override-Received-Time: 2021-11-21T00:40:15.761993+08:00
< X-Request-Id: 1449deb5-464d-4b65-8430-985413c2671b
...
{"healthy":true}

本文转载自: 掘金

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

从JDK中学习设计模式——解释器模式

发表于 2021-11-21

这是我参与11月更文挑战的第21天,活动详情查看:2021最后一次更文挑战

概述

解释器模式(Interpreter Pattern):定义一个语言的文法,并且建立一个解释器来解释该语言中的句子,它是一种类行为型模式。

结构

解释器模式UML.png

  • Context(环境类):又称为上下文类,用于存储解释器之外的全局信息,通常它用来临时存储需要解释的语句。
  • AbstractExpression(抽象表达式):声明了抽象的解释操作,是所有终结符表达式和非终结符表达式的公共父类。
  • TerminalExpression(终结符表达式):抽象表达式的子类,实现了文法中与终结符相关的解释操作,句子中的每个终结符都是该类的实例。
  • NonterminalExpression(非终结符表达式):抽象表达式的子类,实现了文法中与非终结符相关的解释操作。在非终结符表达式中可以包含终结符表达式,也可以继续包含非终结符表达式,因此其解释操作一般通过递归来完成。

优点

  1. 修改语法规则只需修改相应的非终结表达式就可以了,若扩展语法,只要增加非终结符类就可以了,扩展方便。
  2. 在抽象语法树中每一个表达式节点类的实现方式都是相似的,实现文法较为容易。
  3. 增加新的解释表达式,只需增加一个新的终结符表达式或非终结符表达式,原有表达式类代码无须修改,符合开闭原则。

缺点

  1. 每一个语法都要产生一个非终结符表达式,语法规则比较复杂时,就可能产生大量的类文件,不利于维护。
  2. 由于使用了大量的循环和递归,执行效率较低。

应用场景

  1. 重复出现的问题。
  2. 将一个需要解释执行的语言中的句子表示为一个抽象语法树。
  3. 语言的文法较为简单,执行效率要求不高。

JDK 中的应用

JDK 中的解释器模式:

  • java.text.Normalizer
  • java.text.Format

本文转载自: 掘金

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

每日算法&面试题,算法特训十四天——第一天 导读 21天动态

发表于 2021-11-21

「这是我参与11月更文挑战的第20天,活动详情查看:2021最后一次更文挑战」

导读

在这里插入图片描述

肥友们为了更好的去帮助新同学适应算法和面试题,最近我们开始进行专项突击一步一步来。我们先来搞一下让大家最头疼的一类算法题,动态规划我们将进行为时21天的养成计划。还在等什么快来一起肥学进行动态规划21天挑战吧!!

21天动态规划入门

你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。

假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。

你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version
是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。

1
2
3
4
5
6
7
8
9
css复制代码示例 1:

输入:n = 5, bad = 4
输出:4
解释:
调用 isBadVersion(3) -> false
调用 isBadVersion(5) -> true
调用 isBadVersion(4) -> true
所以,4 是第一个错误的版本。
1
2
3
4
css复制代码示例 2:

输入:n = 1, bad = 1
输出:1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
css复制代码public class Solution extends VersionControl {
public int firstBadVersion(int n) {
int left = 1, right = n;
while (left < right) { // 循环直至区间左右端点相同
int mid = left + (right - left) / 2; // 防止计算时溢出
if (isBadVersion(mid)) {
right = mid; // 答案在区间 [left, mid] 中
} else {
left = mid + 1; // 答案在区间 [mid+1, right] 中
}
}
// 此时有 left == right,区间缩为一个点,即为答案
return left;
}
}

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(log n) 的算法。

1
2
3
4
css复制代码示例 1:

输入: nums = [1,3,5,6], target = 5
输出: 2
1
2
3
4
css复制代码示例 2:

输入: nums = [1,3,5,6], target = 2
输出: 1
1
2
3
4
java复制代码示例 3:

输入: nums = [1,3,5,6], target = 7
输出: 4
1
2
3
4
java复制代码示例 4:

输入: nums = [1,3,5,6], target = 0
输出: 0
1
2
3
4
java复制代码示例 5:

输入: nums = [1], target = 0
输出: 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码//主要思想还是二分查找
class Solution {
public int searchInsert(int[] nums, int target) {
int n = nums.length;
int left = 0, right = n - 1, ans = n;
while (left <= right) {
int mid = ((right - left) >> 1) + left;
if (target <= nums[mid]) {
ans = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
return ans;
}
}

面试题

3、HashMap 的扩容机制是怎样的? 一般情况下,当元素数量超过阈值时便会触发扩容。每次扩容的容量都是之前容量的 2 倍。
HashMap 的容量是有上限的,必须小于 1<<30,即 1073741824。如果容量超出了这个 数,则不再增长,且阈值会被设置为
Integer.MAX_VALUE。 JDK7 中的扩容机制 
空参数的构造函数:以默认容量、默认负载因子、默认阈值初始化数组。内部数组是空数 组。  有参构造函数:根据参数确定容量、负载因子、阈值等。
 第一次 put 时会初始化数组,其容量变为不小于指定容量的 2 的幂数,然后根据负载因 子确定阈值。  如果不是第一次扩容,则
新容量=旧容量 x 2 ,新阈值=新容量 x 负载因子 。 JDK8 的扩容机制  空参数的构造函数:实例化的 HashMap
默认内部数组是 null,即没有实例化。第一次 调用 put 方法时,则会开始第一次初始化扩容,长度为 16。 
有参构造函数:用于指定容量。会根据指定的正整数找到不小于指定容量的 2 的幂数, 将这个数设置赋值给阈值(threshold)。第一次调用
put 方法时,会将阈值赋值给容量, 然后让 阈值 = 容量 x 负载因子。  如果不是第一次扩容,则容量变为原来的 2
倍,阈值也变为原来的 2 倍。(容量和阈值 都变为原来的 2 倍时,负载因子还是不变)。 此外还有几个细节需要注意:  首次 put
时,先会触发扩容(算是初始化),然后存入数据,然后判断是否需要扩容;  不是首次
put,则不再初始化,直接存入数据,然后判断是否需要扩容;

本文转载自: 掘金

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

大数据Hive学习之旅第二篇 一、Hive 数据类型 二、D

发表于 2021-11-21

「这是我参与11月更文挑战的第21天,活动详情查看:2021最后一次更文挑战」。

一、Hive 数据类型

1、基本数据类型

image.png

对于 Hive 的 String 类型相当于数据库的 varchar 类型,该类型是一个可变的字符串,不过它不能声明其中最多能存储多少个字符,理论上它可以存储 2GB 的字符数。

2、集合数据类型

image.png

Hive 有三种复杂数据类型 ARRAY、MAP 和 STRUCT。ARRAY 和 MAP 与 Java 中的 Array和 Map 类似,而 STRUCT 与 C 语言中的 Struct 类似,它封装了一个命名字段集合,复杂数据类型允许任意层次的嵌套。

实操案例:

1)假设某表有如下一行,我们用 JSON 格式来表示其数据结构。在 Hive 下访问的格式为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
json复制代码{
"name": "songsong",
"friends": [
"bingbing",
"lili"
],
"children": {
"xiao song": 18,
"xiaoxiao song": 19
},
"address": {
"street": "hui long guan",
"city": "beijing"
}
}

2)基于上述数据结构,我们在 Hive 里创建对应的表,并导入数据。

创建本地测试文件 test.txt

1
2
txt复制代码songsong,bingbing_lili,xiao song:18_xiaoxiao song:19,hui long guan_beijing
yangyang,caicai_susu,xiao yang:18_xiaoxiao yang:19,chao yang_beijing

注意:MAP,STRUCT 和 ARRAY 里的元素间关系都可以用同一个字符表示,这里用”_“。

3)Hive 上创建测试表 test

1
2
3
4
5
6
7
8
9
10
hive复制代码create table test(
name string,
friends array<string>,
children map<string, int>,
address struct<street:string, city:string>
)
row format delimited fields terminated by ','
collection items terminated by '_'
map keys terminated by ':'
lines terminated by '\n';
1
2
3
4
5
txt复制代码字段解释:
row format delimited fields terminated by ',' -- 列分隔符
collection items terminated by '_' --MAP STRUCT 和 ARRAY 的分隔符(数据分割符号)
map keys terminated by ':' -- MAP 中的 key 与 value 的分隔符
lines terminated by '\n'; -- 行分隔符

4)导入文本数据到测试表

1
hive复制代码load data local inpath '/opt/module/hive-3.1.2/datas/test.txt' into table test;

5)访问三种集合列里的数据,以下分别是 ARRAY,MAP,STRUCT 的访问方式

1
2
3
4
5
hive复制代码hive (default)> select friends[1],children['xiao song'],address.city from test where name="songsong";
OK
_c0 _c1 city
lili 18 beijing
Time taken: 0.076 seconds, Fetched: 1 row(s)

3、类型转化

Hive 的原子数据类型是可以进行隐式转换的,类似于 Java 的类型转换,例如某表达式使用 INT 类型,TINYINT 会自动转换为 INT 类型,但是 Hive 不会进行反向转化,例如,某表达式使用 TINYINT 类型,INT 不会自动转换为 TINYINT 类型,它会返回错误,除非使用 CAST操作。

1)隐式类型转换规则如下

(1)任何整数类型都可以隐式地转换为一个范围更广的类型,如 TINYINT 可以转换成INT,INT 可以转换成 BIGINT。

(2)所有整数类型、FLOAT 和 STRING 类型都可以隐式地转换成 DOUBLE。

(3)TINYINT、SMALLINT、INT 都可以转换为 FLOAT。

(4)BOOLEAN 类型不可以转换为任何其它的类型。

2)可以使用 CAST 操作显示进行数据类型转换

例如 CAST(‘1’ AS INT)将把字符串’1’ 转换成整数 1;如果强制类型转换失败,如执行CAST(‘X’ AS INT),表达式返回空值 NULL。

二、DDL 数据定义

1、创建数据库

1
2
3
4
hive复制代码CREATE DATABASE [IF NOT EXISTS] database_name
[COMMENT database_comment]
[LOCATION hdfs_path]
[WITH DBPROPERTIES (property_name=property_value, ...)];
  • 创建一个数据库,数据库在 HDFS 上的默认存储路径是/user/hive/warehouse/*.db。
1
hive复制代码hive (default)> create database db_hive;
  • 避免要创建的数据库已经存在错误,增加 if not exists 判断。(标准写法)
1
2
3
4
hive复制代码hive (default)> create database db_hive;
FAILED: Execution Error, return code 1 from
org.apache.hadoop.hive.ql.exec.DDLTask. Database db_hive already exists
hive (default)> create database if not exists db_hive;
  • 创建一个数据库,指定数据库在 HDFS 上存放的位置
1
hive复制代码hive (default)> create database db_hive2 location '/db_hive2.db';

2、查询数据库

2.1、显示数据库

  • 显示数据库
1
hive复制代码hive (default)> show databases;
  • 过滤显示查询的数据库
1
hive复制代码hive> show databases like 'db_hive*';

2.2、查看数据库详情

  • 显示数据库信息
1
2
3
hive复制代码hive (default)> desc database db_hive;
db_name comment location owner_name owner_type parameters
db_hive hdfs://hadoop102:8020/user/hive/warehouse/db_hive.db moe USER
  • 显示数据库详细信息,extended
1
hive复制代码hive (default)> desc database extended db_hive;

2.3、切换当前数据库

1
hive复制代码hive (default)> use db_hive;

3、修改数据库

用户可以使用 ALTER DATABASE 命令为某个数据库的 DBPROPERTIES 设置键-值对属性值,来描述这个数据库的属性信息。

1
hive复制代码hive (default)> alter database db_hive set dbproperties('createtime'='20211120');

在 hive 中查看修改结果

1
2
3
hive复制代码hive (default)> desc database extended db_hive;
db_name comment location owner_name owner_type parameters
db_hive hdfs://hadoop102:8020/user/hive/warehouse/db_hive.db moe USER {createtime=20211120}

4、删除数据库

  • 删除空数据库
1
hive复制代码hive (default)> drop database db_hive;
  • 如果删除的数据库不存在,最好采用 if exists 判断数据库是否存在
1
2
3
hive复制代码hive> drop database db_hive;
FAILED: SemanticException [Error 10072]: Database does not exist: db_hive
hive> drop database if exists db_hive2;
  • 如果数据库不为空,可以采用 cascade 命令,强制删除
1
2
3
4
5
hive复制代码hive> drop database db_hive;
FAILED: Execution Error, return code 1 from
org.apache.hadoop.hive.ql.exec.DDLTask.
InvalidOperationException(message:Database db_hive is not empty. One or more tables exist.)
hive> drop database db_hive cascade;

5、创建表

建表语法

1
2
3
4
5
6
7
8
9
10
11
hive复制代码CREATE [EXTERNAL] TABLE [IF NOT EXISTS] table_name
[(col_name data_type [COMMENT col_comment], ...)]
[COMMENT table_comment]
[PARTITIONED BY (col_name data_type [COMMENT col_comment], ...)]
[CLUSTERED BY (col_name, col_name, ...)
[SORTED BY (col_name [ASC|DESC], ...)] INTO num_buckets BUCKETS]
[ROW FORMAT row_format]
[STORED AS file_format]
[LOCATION hdfs_path]
[TBLPROPERTIES (property_name=property_value, ...)]
[AS select_statement]

字段解释说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
txt复制代码(1)CREATE TABLE 创建一个指定名字的表。如果相同名字的表已经存在,则抛出异常;用户可以用 IF NOT EXISTS 选项来忽略这个异常。
(2)EXTERNAL 关键字可以让用户创建一个外部表,在建表的同时可以指定一个指向实际数据的路径(LOCATION),在删除表的时候,内部表的元数据和数据会被一起删除,
而外部表只删除元数据,不删除数据。
(3)COMMENT:为表和列添加注释。
(4)PARTITIONED BY 创建分区表
(5)CLUSTERED BY 创建分桶表
(6)SORTED BY 不常用,对桶中的一个或多个列另外排序
(7)ROW FORMAT DELIMITED [FIELDS TERMINATED BY char] [COLLECTION ITEMS TERMINATED BY char]
[MAP KEYS TERMINATED BY char] [LINES TERMINATED BY char]
| SERDE serde_name [WITH SERDEPROPERTIES (property_name=property_value,
property_name=property_value, ...)]
用户在建表的时候可以自定义 SerDe 或者使用自带的 SerDe。如果没有指定 ROW
FORMAT 或者 ROW FORMAT DELIMITED,将会使用自带的 SerDe。在建表的时候,用户还需
要为表指定列,用户在指定表的列的同时也会指定自定义的 SerDe,Hive 通过 SerDe 确定表
的具体的列的数据。
SerDe 是 Serialize/Deserilize 的简称, hive 使用 Serde 进行行对象的序列与反序列化。
(8)STORED AS 指定存储文件类型
常用的存储文件类型:SEQUENCEFILE(二进制序列文件)、TEXTFILE(文本)、RCFILE(列式存储格式文件)
如果文件数据是纯文本,可以使用STORED AS TEXTFILE。如果数据需要压缩,使用 STORED AS SEQUENCEFILE。
(9)LOCATION :指定表在 HDFS 上的存储位置。
(10)AS:后跟查询语句,根据查询结果创建表。
(11)LIKE 允许用户复制现有的表结构,但是不复制数据。

5.1、管理表

  • 理论

默认创建的表都是所谓的管理表,有时也被称为内部表。因为这种表,Hive 会(或多或少地)控制着数据的生命周期。Hive 默认情况下会将这些表的数据存储在由配置项hive.metastore.warehouse.dir(/user/hive/warehouse)所定义的目录的子目录下。当我们删除一个管理表时,Hive 也会删除这个表中数据。管理表不适合和其他工具共享数据。

  • 案例实操
+ 原始数据



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
txt复制代码1001	ss1
1002 ss2
1003 ss3
1004 ss4
1005 ss5
1006 ss6
1007 ss7
1008 ss8
1009 ss9
1010 ss10
1011 ss11
1012 ss12
1013 ss13
1014 ss14
1015 ss15
1016 ss16
+ 普通创建表
1
2
3
4
5
6
hive复制代码create table if not exists student(
id int, name string
)
row format delimited fields terminated by '\t'
stored as textfile
location '/user/hive/warehouse/student';
+ 根据查询结果创建表(查询的结果会添加到新创建的表中)
1
hive复制代码create table if not exists student2 as select id, name from student;
+ 根据已经存在的表结构创建表
1
hive复制代码create table if not exists student3 like student;
+ 查询表的类型
1
2
hive复制代码hive (default)> desc formatted student2;
Table Type: MANAGED_TABLE

5.2、外部表

  • 理论

因为表是外部表,所以 Hive 并非认为其完全拥有这份数据。删除该表并不会删除掉这份数据,不过描述表的元数据信息会被删除掉。

  • 管理表和外部表的使用场景

每天将收集到的网站日志定期流入 HDFS 文本文件。在外部表(原始日志表)的基础上做大量的统计分析,用到的中间表、结果表使用内部表存储,数据通过 SELECT+INSERT 进入内部表。

  • 案例实操

分别创建部门和员工外部表,并向表中导入数据。

+ 原始数据


dept:



1
2
3
4
txt复制代码10	ACCOUNTING	1700
20 RESEARCH 1800
30 SALES 1900
40 OPERATIONS 1700
emp:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
txt复制代码7369	SMITH	CLERK	7902	1980-12-17	800.00		20
7499 ALLEN SALESMAN 7698 1981-2-20 1600.00 300.00 30
7521 WARD SALESMAN 7698 1981-2-22 1250.00 500.00 30
7566 JONES MANAGER 7839 1981-4-2 2975.00 20
7654 MARTIN SALESMAN 7698 1981-9-28 1250.00 1400.00 30
7698 BLAKE MANAGER 7839 1981-5-1 2850.00 30
7782 CLARK MANAGER 7839 1981-6-9 2450.00 10
7788 SCOTT ANALYST 7566 1987-4-19 3000.00 20
7839 KING PRESIDENT 1981-11-17 5000.00 10
7844 TURNER SALESMAN 7698 1981-9-8 1500.00 0.00 30
7876 ADAMS CLERK 7788 1987-5-23 1100.00 20
7900 JAMES CLERK 7698 1981-12-3 950.00 30
7902 FORD ANALYST 7566 1981-12-3 3000.00 20
7934 MILLER CLERK 7782 1982-1-23 1300.00 10
+ 上传数据到 HDFS + 建表语句,创建外部表 创建部门表
1
2
3
4
5
6
hive复制代码create external table if not exists dept(
deptno int,
dname string,
loc int
)
row format delimited fields terminated by '\t';
创建员工表
1
2
3
4
5
6
7
8
9
10
hive复制代码create external table if not exists emp(
empno int,
ename string,
job string,
mgr int,
hiredate string,
sal double,
comm double,
deptno int)
row format delimited fields terminated by '\t';
+ 查看创建的表
1
hive复制代码hive (default)>show tables;
+ 查看表格式化数据
1
2
hive复制代码hive (default)> desc formatted dept;
Table Type: EXTERNAL_TABLE
+ 删除外部表
1
hive复制代码hive (default)> drop table dept;
外部表删除后,hdfs 中的数据还在,但是 metadata 中 dept 的元数据已被删除

5.3、管理表与外部表的相互转换

  • 查询表的类型
1
2
hive复制代码hive (default)> desc formatted student2;
Table Type: MANAGED_TABLE
  • 修改内部表 student2 为外部表
1
hive复制代码alter table student2 set tblproperties('EXTERNAL'='TRUE');
  • 查询表的类型
1
2
hive复制代码hive (default)> desc formatted student2;
Table Type: EXTERNAL_TABLE
  • 修改外部表 student2 为内部表
1
hive复制代码alter table student2 set tblproperties('EXTERNAL'='FALSE');
  • 查询表的类型
1
2
hive复制代码hive (default)> desc formatted student2;
Table Type: MANAGED_TABLE

注意:(‘EXTERNAL’=’TRUE’)和(‘EXTERNAL’=’FALSE’)为固定写法,区分大小写!

6、修改表

6.1、重命名表

  • 语法
1
hive复制代码ALTER TABLE table_name RENAME TO new_table_name
  • 实操案例
1
hive复制代码hive (default)> alter table dept1 rename to dept2;

6.2、增加、修改和删除表分区

  • 语法
+ 更新列



1
2
hive复制代码ALTER TABLE table_name CHANGE [COLUMN] col_old_name col_new_name
column_type [COMMENT col_comment] [FIRST|AFTER column_name]
+ 增加和替换列
1
2
hive复制代码ALTER TABLE table_name ADD|REPLACE COLUMNS (col_name data_type [COMMENT
col_comment], ...)
注:ADD 是代表新增一字段,字段位置在所有列后面(partition 列前), REPLACE 则是表示替换表中所有字段。
  • 实操案例
+ 查询表结构



1
hive复制代码hive> desc dept;
+ 添加列
1
hive复制代码hive (default)> alter table dept add columns(deptdesc string);
+ 查询表结构
1
hive复制代码hive> desc dept;
+ 更新列
1
hive复制代码hive (default)> alter table dept change column deptdesc desc string;
+ 查询表结构
1
hive复制代码hive> desc dept;
+ 替换列
1
hive复制代码hive (default)> alter table dept replace columns(deptno string, dname string, loc string);
+ 查询表结构
1
hive复制代码hive> desc dept;

6.3、增加/修改/替换列信息

7、删除表

1
hive复制代码hive (default)> drop table dept;

三、友情链接

大数据Hive学习之旅第一篇

本文转载自: 掘金

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

1…258259260…956

开发者博客

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