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

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


  • 首页

  • 归档

  • 搜索

Web框架Gin | Gin 中间件

发表于 2021-08-14

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

中间件 middleware 在 Golang 中是一个很重要的概念,与 Java 中的拦截器类似,常用于提高应用程序的扩展能力,留出更多的扩展空间,比如:日志记录、故障处理等功能。

在 Gin 的整个实现中,中间件是 Gin 的精髓。一个个中间件组成了一条中间件链,对 HTTP Request 请求进行拦截处理,实现了代码的解耦和分离,并且中间件之间相互无感知,每个中间件只需要处理自己需要处理的事情即可。

Gin 中间件概述

在最初的示例中,我们是直接通过 gin.Default() 来初始化 gin 对象,其中它包含了一个自带默认中间件的 *Engine。

其中,Default() 函数会默认绑定两个已经准备好的中间件,它们就是 Logger 和 Recovery,帮助我们打印日志输出和 painc 处理。

1
2
3
4
5
6
7
go复制代码// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}

从上面 Default()函数中,我们可以看到 Gin 中间件是通过 Use 方法设置的,它是一个可变参数,可同时设置多个中间件。

1
2
3
4
5
6
7
8
9
go复制代码// Use attaches a global middleware to the router. ie. the middleware attached though Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware.
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}

看到这里,很容易理解到,Gin 的中间件实际上就是 Gin 定义的一个函数 func,该 func 的返回值类型为 HandlerFunc。

中间件实现

中间件主要用于完成一些通用的功能,便于功能的扩充、统一实现,类似于 filter。

从上面的介绍中,要实现一个中间件,只需满足以下两点:

  • 它是一个函数
  • 函数返回值的类型必须是 HandlerFunc

示例:

实现计算每次请求执行花费的时间。

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
go复制代码package main

import (
"fmt"
"time"

"github.com/gin-gonic/gin"
)

func main() {
// 初始化gin对象
route := gin.Default()

// 中间件注册
route.Use(costTimeMiddleware())

// 设置一个get请求,其URL为/hello,并实现简单的响应
route.GET("/hello", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "hello world!",
})
})

// 启动服务
route.Run()
}

func costTimeMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 请求前获取当前时间
nowTime := time.Now()

// 请求处理
c.Next()

// 请求处理完获取花费的时间
costTime := time.Since(nowTime)

requestURL := c.Request.URL.String()
fmt.Printf("the request URL %s cost %v\n", requestURL, costTime)
}
}

其中,c.Next() 只允许在中间件函数中使用,用于挂起请求业务逻辑处理。为了更好的理解中间件函数的执行,可以将中间件函数分为三层:

gin中间件函数执行流程

根据中间件注册的位置不同,分为:

  • 全局中间件:所有请求都经过该中间件。例如,上述示例中就属于全局中间件。
  • 局部中间件:只针对注册的路由生效。如果将中间件 costTimeMiddleware 注册在如下位置,则属于局部中间件,只对请求 /hello 生效。
1
2
3
4
5
go复制代码route.GET("/hello", costTimeMiddleware(), func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "hello world!",
})
})

常见中间件

可参考:contrib,里面包含众多常用的中间件。

本文转载自: 掘金

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

Redis实战 5种Redis数据类型详解

发表于 2021-08-14

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

Redis是目前非常主流的KV数据库,它因高性能的读写能力而著称,其实还有另外一个优势,就是Redis提供了更加丰富的数据类型,这使得Redis有着更加广泛的使用场景。那Redis提供给用户的有哪些数据类型呢?主要有:string(字符串)、List(列表)、Set(集合)、Hash(哈希)、Zset(有序集合)、HyperLogLogs(计算基数用的一种数据结构)、Streams(Redis 5.0提供一种建模日志用的全新数据结构)。

需要注意的是这里说的数据类型是指Redis值的数据类型,而Redis键的类型总是string。

本文主要详解一下前5种,也就是最常用的5种数据类型。剩下两种可上Redis官网(redis.io)自行了解下。另外,Redis已经是目前Java程序员面试必问内容,而 “Redis有哪些数据类型?” 更是面试官张口就来的基础问题。如果连这第一问都过不了,那基本上Redis这块已经凉凉了。

string | 字符串类型

redis的字符串类型,可以存储字符串、整数或者浮点数。如果存储的是整数或者浮点数,还能执行自增或者自减操作。

并且redis的string类型是二进制安全的,它可以包含任何数据,比如一个序列化的对象、一个图片字节流等。不过存储大小是由上限的-512M

这里解释下二进制安全的含义:简单的来说,就是字符串不是根据某种特殊的标志位来(C语言的\0)解析的,无论输入的是什么,总能保证输出是处理的原始输入而不是根据某种特殊格式来处理。

redis是怎么实现string类型的二进制安全的呢?

答案是Sds (Simple Dynamic String,简单动态字符串),Redis底层定义了自己的一种数据结构。(简单了解下)

1
2
3
4
5
6
7
8
9
10
11
12
13
c复制代码typedef char *sds;

struct sdshdr {

// buf 已占用长度
int len;

// buf 剩余可用长度
int free;

// 实际保存字符串数据的地方
char buf[];
};

操作字符串的一些命令

基础set、get、del命令及示例

get keyname 获取存储在给定键中的值

set keyname value 设置存储唉给定键中的值

del keyname 删除存储在给定键中的值(通用命令,适用于所有类型)

1
2
3
4
5
6
7
8
9
ruby复制代码127.0.0.1:6379> set happy today
OK
127.0.0.1:6379> get happy
"today"
127.0.0.1:6379> del happy
(integer) 1
127.0.0.1:6379> get happy
(nil)
127.0.0.1:6379>

自增和自减命令

incr keyname 将键存储的值加1

decr kename 将键存储的是减1

incrby keyname amount 将键存储的值加上整数amount

decrby keyname amount 将键存储的值减去整数amount

incrbyfloat keyname amount 将键存储的值加上浮点数amount

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ruby复制代码127.0.0.1:6379> set number 1
OK
127.0.0.1:6379> get number
"1"
127.0.0.1:6379> incr number
(integer) 2
127.0.0.1:6379> get number
"2"
127.0.0.1:6379> decr number
(integer) 1
127.0.0.1:6379> get number
"1"
127.0.0.1:6379> incrby number 3
(integer) 4
127.0.0.1:6379> get number
"4"
127.0.0.1:6379> decrby number 2
(integer) 2
127.0.0.1:6379> get number
"2"
127.0.0.1:6379> incrbyfloat number 1.23
"3.23"
127.0.0.1:6379> get number
"3.23"

子串和二进制位命令

append keyname value 追加value值到指定字符串末尾

getrange keyname start end 获取start到end范围的所有字符组成的子串,包括start和end在内

setrange keyname offset value 从偏移量 offset 开始, 用 value 参数覆写(overwrite)键 keyname 储存的字符串值。

getbit keyname offset 对 keyname 所储存的字符串值,获取指定偏移量上的位(bit)。

setbit keyname offset value 对 keyname 所储存的字符串值,设置或清除指定偏移量上的位(bit)。

注意redis的索引以0为开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ruby复制代码127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> append hello ,java
(integer) 10
127.0.0.1:6379> get hello
"world,java"
127.0.0.1:6379> getrange hello 2 5
"rld,"
127.0.0.1:6379> setrange hello 6 redis
(integer) 11
127.0.0.1:6379> get hello
"world,redis"
127.0.0.1:6379>
127.0.0.1:6379> setbit bitstr 100 1
(integer) 0
127.0.0.1:6379> getbit bitstr 100
(integer) 1
127.0.0.1:6379> get bitstr
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\b"
127.0.0.1:6379>

其他几个重要的命令

setnx key value 只在键 key 不存在的情况下, 将键 key 的值设置为 value;若键 key 已经存在, 则 SETNX 命令不做任何动作。

setex key seconds value 将键 key 的值设置为 value , 并将键 key 的生存时间设置为 seconds 秒钟。如果键 key 已经存在, 那么 SETEX 命令将覆盖已有的值。

说明一下: - SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。命令在设置成功时返回 1 , 设置失败时返回 0 。 - SETEX命令相当于SET key value 和 EXPIRE key seconds # 设置生存时间两条命令的效果,但是SETEX是一个原子操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ruby复制代码127.0.0.1:6379> exists mark
(integer) 0
127.0.0.1:6379> setnx mark abcd
(integer) 1
127.0.0.1:6379> setnx mark defg
(integer) 0
127.0.0.1:6379> get mark
"abcd"
127.0.0.1:6379> setex cachekey 20 ak98
OK
127.0.0.1:6379> get cachekey
"ak98"
127.0.0.1:6379> ttl cachekey
(integer) 2

List | 列表类型

Redis的列表类型和许多程序语言中的列表类型类似,可以有序地存储多个字符串。

支持从列表的左端和右端推入或弹出元素。Redis列表的底层实现是压缩列表(redis内容自己实现的数据结构)和双端链表。看下图

image.png

图片来自《Redis 设计与实现》

列表操作命令详解

lpush key value [value...]

将一个或者多个value值插入列表的表头。如果 key 不存在,会创建一个空列表并执行 LPUSH 操作。当 key 存在但不是列表类型时,返回一个错误。

执行 LPUSH 命令后,会返回列表的长度。

1
2
3
4
5
6
7
8
9
10
ruby复制代码127.0.0.1:6379> lpush listkey a
(integer) 1
127.0.0.1:6379> lpush listkey a b c
(integer) 4
127.0.0.1:6379> lrange listkey 0 -1
1) "c"
2) "b"
3) "a"
4) "a"
127.0.0.1:6379>
  • list类型可以加入重复的元素,这个和后面要说的set(集合类型)不同。
  • lrange listkey 0 -1 是获取整个列表的内容
  • 类似地rpush命令是从列表右端加入元素

LPOP key

从列表的左端弹出一个值,并返回被弹出的值

1
2
3
4
5
6
7
8
9
10
11
12
ruby复制代码127.0.0.1:6379> lrange listkey 0 -1
1) "c"
2) "b"
3) "a"
4) "a"
127.0.0.1:6379> lpop listkey
"c"
127.0.0.1:6379> lrange listkey 0 -1
1) "b"
2) "a"
3) "a"
127.0.0.1:6379>

lrange key start end

获取列表key在给定start到end范围上的所有元素值。

0表示第一个元素,-1表示最后一个元素。

1
2
3
4
5
6
7
8
ruby复制代码127.0.0.1:6379> lrange listkey 0 -1
1) "b"
2) "a"
3) "a"
127.0.0.1:6379> lrange listkey 0 1
1) "b"
2) "a"
127.0.0.1:6379>

lindex key index

获取列表在给定index位置上的单个元素值。

可以是-1,代表最后一个元素,-2表示倒数第二个元素,以此类推。

1
2
3
4
5
6
7
8
9
10
11
ruby复制代码127.0.0.1:6379> lrange listkey 0 -1
1) "b"
2) "a"
3) "a"
127.0.0.1:6379> lindex listkey 0
"b"
127.0.0.1:6379> lindex listkey -1
"a"
127.0.0.1:6379> lindex listkey 3
(nil)
127.0.0.1:6379>

blpop key [key …] timeout

blpop 是阻塞式的弹出命令,它是lpop key 命令的阻塞版本。当给定列表内没有任何元素可供弹出的时候,连接将被 blpop 命令阻塞,直到等待超时或发现可弹出元素为止。

当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的头元素。

因此可以分两种情况讨论,一种是至少有一个key存在且是非空列表,则blpop命令不会阻塞,另外是blpop命令中的列表是空列表,此时会在超时时间内阻塞。

先看下非阻塞的场景,返回值是第一个非空列表名和被弹出元素。

1
2
3
4
5
6
7
8
ruby复制代码127.0.0.1:6379> lpush list1 hello java
(integer) 2
127.0.0.1:6379> lpush list2 hello redis
(integer) 2
127.0.0.1:6379> blpop list2 list1 list3 0
1) "list2"
2) "redis"
127.0.0.1:6379>

阻塞的场景,在执行了blpop book1 book2 300 命令后会一直阻塞住。

1
2
3
4
5
6
ruby复制代码127.0.0.1:6379> exists book1
(integer) 0
127.0.0.1:6379> exists book2
(integer) 0
127.0.0.1:6379> blpop book1 book2 300
阻塞在这里了

这个时候,我们如果在开另外一个redis客户端,执行如下lpush命令往book1列表中推入一个元素。

1
2
3
makefile复制代码127.0.0.1:6379> lpush book1 springboot
(integer) 1
127.0.0.1:6379>

此时,再回到原来阻塞的客户端,已经弹出了元素。

1
2
3
4
5
6
7
8
9
ruby复制代码127.0.0.1:6379> exists book1
(integer) 0
127.0.0.1:6379> exists book2
(integer) 0
127.0.0.1:6379> blpop book1 book2 300
1) "book1"
2) "springboot"
(237.45s)
127.0.0.1:6379>

通过利用Redis列表类型的阻塞式命令的特性,我们最容易想到的就是可以用它实现一个简易版的消息队列。

set | 集合类型

Redis的集合以无序的方式存储多个不同的元素。这里要注意的是无序和不同。

除了对集合能快速执行添加、删除、检查一个元素是否在集合中之外,还可以对多个集合执行交集、并集和差集运算。

底层实现概述

Redis的集合类型底层实现主要是通过一种叫做字典的数据结构。不过Redis为了追求极致的性能,会根据存储的值是否是整数,选择一种intset的数据结构。当满足一定条件后,会切换成字典的实现。

image.png

这里大概解释下字典: 其实是由一集键值对(key-value pairs)组成, 各个键值对的键各不相同, 程序可以添加新的键值对到字典中, 或者基于键进行查找、更新或删除等操作。

Redis的set(集合)在使用字典数据结构保存数据时,将元素保存到字典的键里面, 而字典的值则统一设为 NULL 。

集合类型操作命令详解

sadd key member [member...]

将一个或者多个元素添加到集合key中,已存在于集合中的元素将被忽略。返回新添加的元素数量,不包括忽略的元素。

srem key member [member...]

移除集合中的一个或多个元素,不存在的元素将被忽略。返回被成功移除的元素数量。

sismember key meber

检查元素member是否存在于集合key中。如果是返回1,不是或者key不存在,返回0。

scard key 返回集合包含的元素数量

spop key 随机移除集合中的一个元素,并返回被移除元素。

smembers 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
ruby复制代码127.0.0.1:6379> sadd set1 java spring redis
(integer) 3
127.0.0.1:6379> smembers set1
1) "redis"
2) "spring"
3) "java"
127.0.0.1:6379> scard set1
(integer) 3
127.0.0.1:6379> srem set1 spring
(integer) 1
127.0.0.1:6379> sismember set1 spring
(integer) 0
127.0.0.1:6379> smembers set1
1) "redis"
2) "java"
127.0.0.1:6379> sadd set1 mysql spring
(integer) 2
127.0.0.1:6379> spop set1
"redis"
127.0.0.1:6379> smembers set1
1) "mysql"
2) "spring"
3) "java"
127.0.0.1:6379>

下面是一些用于处理多个集合的一些命令

sdiff key [key...] 返回存在于第一个集合,但不存在于其他集合中的元素(数学上的差集运算)

sinter key [key...] 返回同时存在于所有集合中的元素(数学上的交集运算) sunion key [key...] 返回至少存在于一个集合中的元素(数学上的并集运算)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ruby复制代码127.0.0.1:6379> smembers set1
1) "mysql"
2) "spring"
3) "java"
127.0.0.1:6379> smembers set2
1) "mysql"
2) "springboot"
3) "redis"
127.0.0.1:6379> sdiff set1 set2
1) "java"
2) "spring"
127.0.0.1:6379> sinter set1 set2
1) "mysql"
127.0.0.1:6379> sunion set1 set2
1) "mysql"
2) "springboot"
3) "java"
4) "spring"
5) "redis"
127.0.0.1:6379>

hash | 散列表(哈希表)

Redis的hash类型其实就是一个缩减版的redis。它存储的是键值对,将多个键值对存储到一个redis键里面。

底层实现概述

hash类型的底层主要也是基于字典这种数据结构来实现的。

image.png
redis内部在实现hash数据类型的时候是使用了两种数据结构。在创建一个空的hash表时,默认使用的是ziplist的数据结构,满足一定条件后会转成字典的形式。

散列表操作命令详解

hmget hash-key key [key...] 从散列表里面获取一个或多个键的值

hmset hash-key key value [key value...] 为散列表里面的一个或多个键设置值 hdel hash-key key [key...] 删除散列表里面的一个或多个键值对,返回删除成功的键值对的数量

hlen hash-key 返回散列表包含的键值对的数量

hexists hash-key key 检查给定的键是否存在于散列表中

hkeys hash-key 获取散列包含的所有键

hvals hash-key 获取散列包含的所有值

hgetall hash-key 获取散列包含的所有键值对

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ruby复制代码127.0.0.1:6379> hmset hash1 username tom email 123@123 year 12
OK
127.0.0.1:6379> hmget hash1 email
1) "123@123"
127.0.0.1:6379> hlen hash1
(integer) 3
127.0.0.1:6379> hdel hash1 year
(integer) 1
127.0.0.1:6379> hexists hash1 year
(integer) 0
127.0.0.1:6379> hkeys hash1
1) "username"
2) "email"
127.0.0.1:6379> hvals hash1
1) "tom"
2) "123@123"
127.0.0.1:6379> hgetall hash1
1) "username"
2) "tom"
3) "email"
4) "123@123"
127.0.0.1:6379>

zset | 有序集合

有序集合相比较于集合,多个有序两个字,我们知道set集合类型存储的元素是无序的,那Redis有序集合是怎么保证有序的?使用分值,有序集合里存储这成员与分值之间的映射,并提供了分值处理命令,以及根据分值的大小有序地获取成员或分值的命令。

底层实现概述

Redis有序集合的实现使用了一种叫跳跃表的数据结构(简称跳表,可自行查阅),同时也使用到了前面提到的压缩列表。也是满足一定条件的话,会自行转换。

image.png

有序集合操作命令详解

zadd z-key score memer [score member...] 将带有给定分值的成员添加到有序集合里面

zrem z-key member [member...] 从有序集合里面移除给定的成员,并返回被移除成员的数量

zcard z-key 返回有序集合包含的成员数量

zincrby z-key increment member 将member成员的分值加上increment

zcount z-key min max 返回分值介于min和max之间的成员数量

zrank z-key member 返回成员member在有序集合中的排名

zscore z-key member 返回成员member的分值

zrange z-key start stop [withscores] 返回有序集合中排名介于start和stop之间的成员,如果给定了可选的withscores选项,name命令会将成员的分值也一并返回。

zrevrank z-key member 返回有序集合里成员member的排名,成员按照分值从大到小排列。

zrevrange z-key start stop 返回有序集合给定排名范围内的成员,成员按照分值从大到小排列。

zrangebyscore z-key min max 返回有序集合中分值介于min和max之间的所有成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ruby复制代码127.0.0.1:6379> zadd zset1 10 a 12 b 1 c 3 d 20 e
(integer) 5
127.0.0.1:6379> zcard zset1
(integer) 5
127.0.0.1:6379> zcount zset1 2 10
(integer) 2
127.0.0.1:6379> zrank zset1 d
(integer) 1
127.0.0.1:6379> zscore zset1 e
"20"
127.0.0.1:6379> zrange zset1 3 5
1) "b"
2) "e"
127.0.0.1:6379> zrevrank zset1 d
(integer) 3
127.0.0.1:6379> zrevrange zset1 3 5
1) "d"
2) "c"
127.0.0.1:6379> zrangebyscore zset1 5 10
1) "a"
127.0.0.1:6379>

本文转载自: 掘金

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

Netty 源码分析系列(九)Netty 程序引导类

发表于 2021-08-14

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

前言

今天是七夕,首先祝大家七夕快乐吧。废话不多说,进入正题。

程序引导类(Bootstrap)可以理解为是一个程序的入口程序,在 Java 程序中,就是一个包含 main 方法的程序类。在 Netty 中,引导程序还包含一系列的配置项。本篇文章我们就来介绍 Netty 的引导程序。

引导程序类

引导程序类是一种引导程序,使 Netty 程序可以很容易地引导一个 Channel。在 Netty 中,承担引导程序的是 AbstractBootStrap抽象类。

引导程序类都在io.netty.bootstrap包下。AbstractBootStrap抽象类有两个子类:Bootstrap和ServerBootstrap,分别用于引导客户端程序及服务的程序。

下图展示了引导程序类的关系:

image-20210813220201575

AbstractBootStrap 抽象类

从上图可以看出,AbstractBootStrap抽象类实现了Cloneable接口。那么为什么需要实现Cloneable接口呢?

在 Netty 中经常需要创建多个具有类似配置或者完全相同配置的Channel。为了支持这种模式而又不避免为每个Channel都创建并配置一个新的引导类实例,因此AbstractBootStrap被标记为了Cloneable。在一个已经配置完成的引导实例上调用clone()方法将返回另一个可以立即使用的引导类实例。

这种方式只会创建引导类实例的EventLoopGroup的一个浅拷贝,所以,EventLoopGroup将在所有克隆的Channel实例之间共享。这是可以接受的,毕竟这些克隆的Channel的生命周期都很短暂,例如,一个典型的场景是创建一个Channel以进行一次HTTP请求。

以下是AbstractBootStrap的核心源码:

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复制代码public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C extends Channel> implements Cloneable {

volatile EventLoopGroup group;
private volatile ChannelFactory<? extends C> channelFactory;
private volatile SocketAddress localAddress;
private final Map<ChannelOption<?>, Object> options = new LinkedHashMap();
private final Map<AttributeKey<?>, Object> attrs = new ConcurrentHashMap();
private volatile ChannelHandler handler;

AbstractBootstrap() {
//禁止从其他程序包扩展
}

AbstractBootstrap(AbstractBootstrap<B, C> bootstrap) {
this.group = bootstrap.group;
this.channelFactory = bootstrap.channelFactory;
this.handler = bootstrap.handler;
this.localAddress = bootstrap.localAddress;
synchronized(bootstrap.options) {
this.options.putAll(bootstrap.options);
}

this.attrs.putAll(bootstrap.attrs);
}

//...
}

从上述源码可以看出,子类型B是其父类型的一个类型参数,因此可以返回到运行时实例的引用以支持方法的链式调用。从私有变量可以看出,AbstractBootStrap所要管理的启动配置包括EventLoopGroup、SocketAddress、Channel配置、ChannelHandler等信息。

AbstractBootStrap是禁止被除io.netty.bootstrap包外其他程序所扩展的,因此可以看到AbstractBootStrap默认构造方法被设置为了包内可见。

Bootstrap 类

BootStrap 类是AbstractBootStrap抽象类的子类之一,主要是用于客户端或者使用了无连接协议的应用程序。

以下是BootStrap 类的核心源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
java复制代码public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel> {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(Bootstrap.class);
private static final AddressResolverGroup<?> DEFAULT_RESOLVER;
private final BootstrapConfig config = new BootstrapConfig(this);
private volatile AddressResolverGroup<SocketAddress> resolver;
private volatile SocketAddress remoteAddress;

public Bootstrap() {
this.resolver = DEFAULT_RESOLVER;
}

private Bootstrap(Bootstrap bootstrap) {
super(bootstrap);
this.resolver = DEFAULT_RESOLVER;
this.resolver = bootstrap.resolver;
this.remoteAddress = bootstrap.remoteAddress;
}

public Bootstrap resolver(AddressResolverGroup<?> resolver) {
this.resolver = resolver == null ? DEFAULT_RESOLVER : resolver;
return this;
}

public Bootstrap remoteAddress(SocketAddress remoteAddress) {
this.remoteAddress = remoteAddress;
return this;
}

public Bootstrap remoteAddress(String inetHost, int inetPort) {
this.remoteAddress = InetSocketAddress.createUnresolved(inetHost, inetPort);
return this;
}

public Bootstrap remoteAddress(InetAddress inetHost, int inetPort) {
this.remoteAddress = new InetSocketAddress(inetHost, inetPort);
return this;
}

public ChannelFuture connect() {
this.validate();
SocketAddress remoteAddress = this.remoteAddress;
if (remoteAddress == null) {
throw new IllegalStateException("remoteAddress not set");
} else {
return this.doResolveAndConnect(remoteAddress, this.config.localAddress());
}
}

public ChannelFuture connect(String inetHost, int inetPort) {
return this.connect(InetSocketAddress.createUnresolved(inetHost, inetPort));
}

public ChannelFuture connect(InetAddress inetHost, int inetPort) {
return this.connect(new InetSocketAddress(inetHost, inetPort));
}

public ChannelFuture connect(SocketAddress remoteAddress) {
ObjectUtil.checkNotNull(remoteAddress, "remoteAddress");
this.validate();
return this.doResolveAndConnect(remoteAddress, this.config.localAddress());
}

public ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) {
ObjectUtil.checkNotNull(remoteAddress, "remoteAddress");
this.validate();
return this.doResolveAndConnect(remoteAddress, localAddress);
}

//...

public Bootstrap validate() {
super.validate();
if (this.config.handler() == null) {
throw new IllegalStateException("handler not set");
} else {
return this;
}
}

public Bootstrap clone() {
return new Bootstrap(this);
}

public Bootstrap clone(EventLoopGroup group) {
Bootstrap bs = new Bootstrap(this);
bs.group = group;
return bs;
}

public final BootstrapConfig config() {
return this.config;
}

final SocketAddress remoteAddress() {
return this.remoteAddress;
}

final AddressResolverGroup<?> resolver() {
return this.resolver;
}

...
}

上述方法主要分为以下几类:

  • group:设置用于处理Channel所有事件的EventLoopGroup。
  • channel:指定了 Channel的实现类。
  • localAddress:指定 Channel应该绑定到的本地地址。如果没有指定,则有操作系统创建一个随机的地址。或者,也可以通过bind()或者connect()方法指定localAddress。
  • option:设置ChannelOption,其将被应用到每个新创建的Channel的ChannelConfig。这些选项将会通过bind()或者connect()方法设置到Channel,配置的顺序与调用先后没有关系。这个方法在Channel已经被创建后再次调用就不会再起任何效果了。支持什么样的ChannelOption取决于所使用的Channel类型。
  • attr:指定新创建的Channel的属性值。这些属性值是通过bind()或者connect()方法设置到Channel。配置的顺序取决于调用的先后顺序。这个方法在Channel已经被创建后再次调用就不会再起任何效果了。
  • handler:设置被添加到ChannelPipeline以接收事件通知的ChannelHandler。
  • remoteAddress:设置远程地址。也可以通过connect()方法来指定它。
  • clone:创建一个当前Bootstrap的克隆,其具有和原始的Bootstrap相同的设置信息。
  • connect:用于连接到远程节点并返回一个ChannelFuture,并将会在连接操作完成后接收到通知。
  • bind:绑定Channel并返回一个ChannelFuture,其将会在绑定操作完成之后接收到通知,在那之后必须调用Channel。

BootStrap类中许多方法都继承自AbstractBootstrap类。

ServerBootstrap 类

ServerBootstrap 类是AbstractBootStrap抽象类的子类之一,主要是用于引导服务器程序。

以下是 ServerBootstrap 类的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
java复制代码public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerChannel> {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(ServerBootstrap.class);
private final Map<ChannelOption<?>, Object> childOptions = new LinkedHashMap();
private final Map<AttributeKey<?>, Object> childAttrs = new ConcurrentHashMap();
private final ServerBootstrapConfig config = new ServerBootstrapConfig(this);
private volatile EventLoopGroup childGroup;
private volatile ChannelHandler childHandler;

public ServerBootstrap() {
}

private ServerBootstrap(ServerBootstrap bootstrap) {
super(bootstrap);
this.childGroup = bootstrap.childGroup;
this.childHandler = bootstrap.childHandler;
synchronized(bootstrap.childOptions) {
this.childOptions.putAll(bootstrap.childOptions);
}

this.childAttrs.putAll(bootstrap.childAttrs);
}

public ServerBootstrap group(EventLoopGroup group) {
return this.group(group, group);
}

public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
super.group(parentGroup);
if (this.childGroup != null) {
throw new IllegalStateException("childGroup set already");
} else {
this.childGroup = (EventLoopGroup)ObjectUtil.checkNotNull(childGroup, "childGroup");
return this;
}
}

public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value) {
ObjectUtil.checkNotNull(childOption, "childOption");
synchronized(this.childOptions) {
if (value == null) {
this.childOptions.remove(childOption);
} else {
this.childOptions.put(childOption, value);
}

return this;
}
}

public <T> ServerBootstrap childAttr(AttributeKey<T> childKey, T value) {
ObjectUtil.checkNotNull(childKey, "childKey");
if (value == null) {
this.childAttrs.remove(childKey);
} else {
this.childAttrs.put(childKey, value);
}

return this;
}

public ServerBootstrap childHandler(ChannelHandler childHandler) {
this.childHandler = (ChannelHandler)ObjectUtil.checkNotNull(childHandler, "childHandler");
return this;
}

//...

public ServerBootstrap clone() {
return new ServerBootstrap(this);
}

/** @deprecated */
@Deprecated
public EventLoopGroup childGroup() {
return this.childGroup;
}

final ChannelHandler childHandler() {
return this.childHandler;
}

final Map<ChannelOption<?>, Object> childOptions() {
synchronized(this.childOptions) {
return copiedMap(this.childOptions);
}
}

final Map<AttributeKey<?>, Object> childAttrs() {
return copiedMap(this.childAttrs);
}

public final ServerBootstrapConfig config() {
return this.config;
}

//...
}

上述方法主要分为以下几类:

  • group:设置ServerBootstrap要用的EventLoopGroup。这个EventLoopGroup将用于ServerChannel和被接受的子Channel的 I/O 处理。
  • channel:设置将要被实例化的ServerChannel类。
  • localAddress:指定 ServerChannel应该绑定到的本地地址。如果没有指定,则有操作系统创建一个随机的地址。或者,也可以通过bind()方法指定localAddress。
  • option:指定要应用到新创建的ServerChannel的ChannelConfig的ChannelOption。这些选项将会通过bind()设置到Channel,在bind()方法被调用之后,设置或者改变ChannelOption将不会有任何效果。支持什么样的ChannelOption取决于所使用的Channel类型。
  • childOption:指定当子Channel被接受时,应用到子Channel的ChannelConfig的ChannelOption。所支持的ChannelOption取决于所使用的Channel类型。
  • attr:指定ServerChannel上的属性值。这些属性值是通过bind()或者connect()方法设置到Channel。在bind()方法被调用之后它们将不会有任何效果。
  • childAttr:将属性设置给已经被接受的子Channel。之后再次调用将不会有任何效果。
  • handler:设置被添加到ServerChannel的ChannelPipeline中的ChannelHandler。
  • childHandler:设置将被添加到已被接受的子Channel的ChannelPipeline中的ChannelHandler。
  • clone:克隆一个设置好原始的ServerBootstrap相同的ServerBootstrap。
  • bind:绑定ServerChannel并返回一个ChannelFuture,其将会在绑定操作完成之后接收到通知(带着成功或者失败的结果)。

ServerBootstrap类中许多方法都继承自AbstractBootstrap类。

引导服务器

为了能更好的理解引导程序,下面就以 Echo 协议的服务器的代码为例。核心代码如下:

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复制代码// 多线程事件循环器
EventLoopGroup bossGroup = new NioEventLoopGroup(); // boss
EventLoopGroup workerGroup = new NioEventLoopGroup(); // worker

try {
// 启动NIO服务的引导程序类
ServerBootstrap b = new ServerBootstrap();

b.group(bossGroup, workerGroup) // 设置EventLoopGroup
.channel(NioServerSocketChannel.class) // 指明新的Channel的类型
.childHandler(new EchoServerHandler()) // 指定ChannelHandler
.option(ChannelOption.SO_BACKLOG, 128) // 设置的ServerChannel的一些选项
.childOption(ChannelOption.SO_KEEPALIVE, true); // 设置的ServerChannel的子Channel的选项

// 绑定端口,开始接收进来的连接
ChannelFuture f = b.bind(port).sync();

System.out.println("EchoServer已启动,端口:" + port);

// 等待服务器 socket 关闭 。
// 在这个例子中,这不会发生,但你可以优雅地关闭你的服务器。
f.channel().closeFuture().sync();
} finally {

// 优雅的关闭
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}

引导 Netty 服务器主要分为几下几个步骤。

1、实例化引导程序类

在上述代码中,首先是需要实例化引导程序类。由于是服务器端的程序,所以,实例化了一个ServerBootstrap。

2、设置 EventLoopGroup

设置ServerBootstrap的EventLoopGroup。上述服务器使用了两个NioEventLoopGroup,一个代表boss线程组,一个代表work线程组。

boss线程主要是接收客户端的请求,并将请求转发给work线程处理。

boss线程是轻量的,不会处理耗时的任务,因此可以承受高并发的请求。而真实的 I/O 操作都是由work线程在执行。

NioEventLoopGroup是支持多线程的,因此可以执行线程池的大小。如果没有指定,则 Netty 会指定一个默认的线程池大小,核心代码如下:

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复制代码    private static final int DEFAULT_EVENT_LOOP_THREADS;

static {
// 默认 EventLoopGroup 的线程数
DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
"io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));

if (logger.isDebugEnabled()) {
logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
}
}

public NioEventLoopGroup() {
// 如果不指定, nThreads = 0
this(0);
}

/**
* @see MultithreadEventExecutorGroup#MultithreadEventExecutorGroup(int, Executor, Object...)
*/
protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
// 如果不指定(nThreads = 0),默认值是 DEFAULT_EVENT_LOOP_THREADS
super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}

从上述源码可以看出,如果NioEventLoopGroup实例化时,没有指定线程数,则最终默认值是DEFAULT_EVENT_LOOP_THREADS,而默认值DEFAULT_EVENT_LOOP_THREADS是根据当前机子的CPU 处理的个数乘以 2 得出的。

3、指定 Channel 类型

channel()方法用于指定ServerBootstrap的Channel类型。在本例中,使用的是NioServerSocketChannel类型,代表了服务器是一个基于ServerSocketChannel的实现,使用基于 NIO 选择器的实现来接受新连接。

4、指定 ChannelHandler

childHandler用于指定ChannelHandler,以便处理Channel的请求。上述例子中,指定的是自定义的EchoServerHandler。

5、设置 Channel 选项

option和childOption方法,分别用于设置ServerChannel及ServerChannel的子Channel的选项。这些选项定义在ChannelOption类中,包含以下常量:

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
java复制代码public class ChannelOption<T> extends AbstractConstant<ChannelOption<T>> {

public static final ChannelOption<ByteBufAllocator> ALLOCATOR = valueOf("ALLOCATOR");
public static final ChannelOption<RecvByteBufAllocator> RCVBUF_ALLOCATOR = valueOf("RCVBUF_ALLOCATOR");
public static final ChannelOption<MessageSizeEstimator> MESSAGE_SIZE_ESTIMATOR = valueOf("MESSAGE_SIZE_ESTIMATOR");

public static final ChannelOption<Integer> CONNECT_TIMEOUT_MILLIS = valueOf("CONNECT_TIMEOUT_MILLIS");
/**
* @deprecated Use {@link MaxMessagesRecvByteBufAllocator}
* and {@link MaxMessagesRecvByteBufAllocator#maxMessagesPerRead(int)}.
*/
@Deprecated
public static final ChannelOption<Integer> MAX_MESSAGES_PER_READ = valueOf("MAX_MESSAGES_PER_READ");
public static final ChannelOption<Integer> WRITE_SPIN_COUNT = valueOf("WRITE_SPIN_COUNT");
/**
* @deprecated Use {@link #WRITE_BUFFER_WATER_MARK}
*/
@Deprecated
public static final ChannelOption<Integer> WRITE_BUFFER_HIGH_WATER_MARK = valueOf("WRITE_BUFFER_HIGH_WATER_MARK");
/**
* @deprecated Use {@link #WRITE_BUFFER_WATER_MARK}
*/
@Deprecated
public static final ChannelOption<Integer> WRITE_BUFFER_LOW_WATER_MARK = valueOf("WRITE_BUFFER_LOW_WATER_MARK");
public static final ChannelOption<WriteBufferWaterMark> WRITE_BUFFER_WATER_MARK =
valueOf("WRITE_BUFFER_WATER_MARK");

public static final ChannelOption<Boolean> ALLOW_HALF_CLOSURE = valueOf("ALLOW_HALF_CLOSURE");
public static final ChannelOption<Boolean> AUTO_READ = valueOf("AUTO_READ");

/**
* If {@code true} then the {@link Channel} is closed automatically and immediately on write failure.
* The default value is {@code true}.
*/
public static final ChannelOption<Boolean> AUTO_CLOSE = valueOf("AUTO_CLOSE");

public static final ChannelOption<Boolean> SO_BROADCAST = valueOf("SO_BROADCAST");
public static final ChannelOption<Boolean> SO_KEEPALIVE = valueOf("SO_KEEPALIVE");
public static final ChannelOption<Integer> SO_SNDBUF = valueOf("SO_SNDBUF");
public static final ChannelOption<Integer> SO_RCVBUF = valueOf("SO_RCVBUF");
public static final ChannelOption<Boolean> SO_REUSEADDR = valueOf("SO_REUSEADDR");
public static final ChannelOption<Integer> SO_LINGER = valueOf("SO_LINGER");
public static final ChannelOption<Integer> SO_BACKLOG = valueOf("SO_BACKLOG");
public static final ChannelOption<Integer> SO_TIMEOUT = valueOf("SO_TIMEOUT");

public static final ChannelOption<Integer> IP_TOS = valueOf("IP_TOS");
public static final ChannelOption<InetAddress> IP_MULTICAST_ADDR = valueOf("IP_MULTICAST_ADDR");
public static final ChannelOption<NetworkInterface> IP_MULTICAST_IF = valueOf("IP_MULTICAST_IF");
public static final ChannelOption<Integer> IP_MULTICAST_TTL = valueOf("IP_MULTICAST_TTL");
public static final ChannelOption<Boolean> IP_MULTICAST_LOOP_DISABLED = valueOf("IP_MULTICAST_LOOP_DISABLED");

public static final ChannelOption<Boolean> TCP_NODELAY = valueOf("TCP_NODELAY");

@Deprecated
public static final ChannelOption<Boolean> DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION =
valueOf("DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION");

public static final ChannelOption<Boolean> SINGLE_EVENTEXECUTOR_PER_GROUP =
valueOf("SINGLE_EVENTEXECUTOR_PER_GROUP");

}

6、绑定端口启动服务

bind()方法用于绑定端口,会创建一个Channel而后启动服务。

绑定成功后,返回一个ChannelFuture,以代表是一个异步的操作。在上述的例子里,使用的是sync()方法,以同步的方式来获取服务启动的结果。

引导客户端

为了能更好的理解引导程序,下面就以 Echo 协议的客户端的代码为例。核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
java复制代码// 配置客户端
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new EchoClientHandler());

// 连接到服务器
ChannelFuture f = b.connect(hostName, portNumber).sync();

Channel channel = f.channel();
ByteBuffer writeBuffer = ByteBuffer.allocate(32);
try (BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))) {
String userInput;
while ((userInput = stdIn.readLine()) != null) {
writeBuffer.put(userInput.getBytes());
writeBuffer.flip();
writeBuffer.rewind();

// 转为ByteBuf
ByteBuf buf = Unpooled.copiedBuffer(writeBuffer);

// 写消息到管道
channel.writeAndFlush(buf);

// 清理缓冲区
writeBuffer.clear();
}
} catch (UnknownHostException e) {
System.err.println("不明主机,主机名为: " + hostName);
System.exit(1);
} catch (IOException e) {
System.err.println("不能从主机中获取I/O,主机名为:" + hostName);
System.exit(1);
}
} finally {

// 优雅的关闭
group.shutdownGracefully();
}

引导 Netty 客户端主要分为几下几个步骤。

1、实例化引导程序类

在上述代码中,首先是需要实例化引导程序类。由于是客户端的程序,所以,实例化了一个Bootstrap。

2、设置 EventLoopGroup

设置Bootstrap的EventLoopGroup。不同于服务器,客户端只需要使用了一个NioEventLoopGroup。

3、指定 Channel 类型

channel()方法用于指定Bootstrap的Channel类型。在本例中,由于是客户端使用,使用的是NioSocketChannel类型,代表了客户端是一个基于SocketChannel的实现,使用基于 NIO 选择器的实现来发起连接请求。

4、设置 Channel 选项

option用于设置Channel的选项。这些选项定义在ChannelOption类中。

5、指定 ChannelHandler

Handler用于设置处理服务端请求的ChannelHandler。上述例子中,指定的是自定义的EchoClientHandler。

6、连接到服务器

connect()方法用于连接到指定的服务器的Channel。

连接成功后,返回一个ChannelFuture,以代表是一个异步的操作。在上述的例子里,使用的是sync()方法,以同步的方式来获取服务启动的结果。

总结

通过上述对引导程序类的介绍,相信大家对于服务端和客户端的引导程序以及一些配置项的都有了一定的了解。通过看源码,我们就能更加清楚的知道 Netty 服务端和客户端的启动的过程。下节我们来分析Netty的线程模型。

结尾

我是一个正在被打击还在努力前进的码农。如果文章对你有帮助,记得点赞、关注哟,谢谢!

本文转载自: 掘金

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

k8s 简单集群搭建(1213)

发表于 2021-08-14

注意:本人使用centos7系统进行搭建

一、基本环境准备

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
bash复制代码#禁用**iptables**和**firewalld**服务

#关闭firewalld服务
systemctl stop firewalld
systemctl disable firewalld
#关闭iptables服务
systemctl stop iptables
systemctl disable iptables

# 关闭 swap
swapoff -a && sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab

#禁用**selinux**
sed -i 's/enforcing/disabled/' /etc/selinux/config

# 将桥接的IPv4流量传递到iptables的链

vim /etc/sysctl.d/k8s.conf
#增加如下内容
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1

# 生效命令
sysctl --system

二、docker环境准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bash复制代码wget https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo -O /etc/yum.repos.d/docker-ce.repo
yum list docker-ce --showduplicates | sort -r
#安装指定版本
yum install docker-ce-20.10.7

# 添加阿里云 yum 源, 可从阿里云容器镜像管理中复制镜像加速地址
cat <<EOF > /etc/docker/daemon.json
{
"registry-mirrors": [
"https://registry.docker-cn.com",
"http://hub-mirror.c.163.com",
"https://docker.mirrors.ustc.edu.cn"
]
}
EOF

#启动docker
systemctl enable docker && systemctl start docker

三、安装 kubeadm、kubelet 和 kubectl

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码cat > /etc/yum.repos.d/kubernetes.repo << EOF
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=0
repo_gpgcheck=0
gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
EOF

yum install -y kubelet-1.21.3 kubeadm-1.21.3 kubectl-1.21.3
systemctl enable kubelet

四、单机部署

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
bash复制代码# 设置hostname
hostnamectl set-hostname master
cat >> /etc/hosts << EOF
192.168.137.200 master
EOF

# 初始化 Master
kubeadm init \
--apiserver-advertise-address=192.168.137.200 \
--image-repository registry.aliyuncs.com/google_containers \
--kubernetes-version v1.21.3 \
--service-cidr=10.96.0.0/12 \
--pod-network-cidr=10.244.0.0/16

#可能遇到coredns镜像下载失败 用下面方式解决
docker pull registry.aliyuncs.com/google_containers/coredns:1.8.0
docker tag registry.aliyuncs.com/google_containers/coredns:1.8.0 registry.aliyuncs.com/google_containers/coredns:v1.8.0
docker rmi registry.aliyuncs.com/google_containers/coredns:1.8.0

# 为其他非root用户创建执行权限
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

#root 用户,则可以运行:
export KUBECONFIG=/etc/kubernetes/admin.conf

# 去除污点
kubectl describe node master | grep Taints
kubectl taint nodes master node-role.kubernetes.io/master-

# 部署CNI网络插件
kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
# 查看运行状态
kubectl get pods -n kube-system

image.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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
bash复制代码设置 hosts

# 设置主机名
hostnamectl set-hostname master # node1 / node2
hostname

# 配置 hosts(只在master执行)
cat >> /etc/hosts << EOF
192.168.137.200 master
192.168.137.201 node1
192.168.137.202 node2
EOF
#初始化 Master

kubeadm init \
--apiserver-advertise-address=192.168.137.200 \
--image-repository registry.aliyuncs.com/google_containers \
--kubernetes-version v1.21.3 \
--service-cidr=10.96.0.0/12 \
--pod-network-cidr=10.244.0.0/16

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

#root 用户,则可以运行:
export KUBECONFIG=/etc/kubernetes/admin.conf

kubectl get nodes
#初始化 Node

kubeadm join 192.168.137.200:6443 --token 0bgtnj.6xl01261s02wqc7z \
--discovery-token-ca-cert-hash sha256:6553e9758f5eb18d81e793e936fffb7a168c943a6429de0834e1946033ce6f80
#可能出现的错误
[ERROR FileContent--proc-sys-net-ipv4-ip_forward]: /proc/sys/net/ipv4/ip_forward contents are not set to 1

kubeadm reset
echo 1 > /proc/sys/net/ipv4/ip_forward
#部署 CNI 网络插件(Master)

kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
# 查看运行状态
kubectl get pods -n kube-system

ipvs配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bash复制代码# 安装ipvs
yum -y install ipvsadm ipset

# 永久生效
cat > /etc/sysconfig/modules/ipvs.modules <<EOF
modprobe -- ip_vs
modprobe -- ip_vs_rr
modprobe -- ip_vs_wrr
modprobe -- ip_vs_sh
modprobe -- nf_conntrack_ipv4
EOF

# 为脚本文件添加执行权限
chmod +x /etc/sysconfig/modules/ipvs.modules
# 执行脚本文件
/bin/bash /etc/sysconfig/modules/ipvs.modules
# 查看对应的模块是否加载成功
lsmod | grep -e ip_vs -e nf_conntrack_ipv4

#修改kube-proxy配置 如下图
kubectl edit configmap kube-proxy -n kube-system

image.png

删除原有的 kube-proxy

image.png

1
2
3
4
5
6
7
perl复制代码#查看pod
kubectl get pod -n kube-system
#删除pod
kubectl delete pod kube-proxy-* -n kube-system
#查看新pod
kubectl get pod -n kube-system
#查看日志看是否有ipvs

image.png

dashboard UI监控使用

github地址 github.com/kubernetes/…

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
yaml复制代码#下载
wget https://raw.githubusercontent.com/kubernetes/dashboard/v2.3.1/aio/deploy/recommended.yaml

#编辑文件开放端口进行访问
kind: Service
apiVersion: v1
metadata:
labels:
k8s-app: kubernetes-dashboard
name: kubernetes-dashboard
namespace: kubernetes-dashboard
spec:
type: NodePort
ports:
- port: 443
targetPort: 8443
nodePort: 31443
selector:
k8s-app: kubernetes-dashboard

#设置超级用户 roleRef 按照如下修改
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: kubernetes-dashboard-minimal
namespace: kube-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: kubernetes-dashboard
namespace: kube-system

---

#启动
kubectl apply -f recommended.yaml

#获取token

kubectl -n kubernetes-dashboard describe secret $(kubectl -n kubernetes-dashboard get secret | grep admin-user | awk '{print $1}')

#会显示token字符串然后进行登陆

image.png

简单springboot应用部署

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
yaml复制代码vim k8s-springboot.yaml

apiVersion: v1
kind: Namespace
metadata:
name: k8s-springboot
---
apiVersion: v1
kind: Service
metadata:
name: springboot-demo-nodeport
namespace: k8s-springboot
spec:
type: NodePort
ports:
- port: 8080
targetPort: 8080
nodePort: 30001
selector:
app: springboot-demo
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: springboot-demo
namespace: k8s-springboot
spec:
selector:
matchLabels:
app: springboot-demo
replicas: 1
template:
metadata:
labels:
app: springboot-demo
spec:
containers:
- name: springboot-demo
image: huzhihui/springboot:1.0.0
ports:
- containerPort: 8080

# 部署
kubectl apply -f k8s-springboot.yaml

#查看部署pod状态
kubectl get pods --all-namespaces

image.png

image.png

使用proxy访问服务

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
ini复制代码#使用kubectl proxy命令就可以使API server监听在本地的8001端口上:
$ kubectl proxy
Starting to serve on 127.0.0.1:8001

#如果想通过其它主机访问就需要指定监听的地址:
$ kubectl proxy --port=8009
Starting to serve on 127.0.0.1:8009

$ kubectl proxy --port=8009
Starting to serve on 127.0.0.1:8009

#此时通过curl访问会出现未认证的提示:
$ curl -X GET -L http://master:8009/
<h3>Unauthorized</h3>

#设置API server接收所有主机的请求:
$ kubectl proxy --address='0.0.0.0' --accept-hosts='^*$' --port=8009
Starting to serve on [::]:8009

#访问正常:
$ curl -X GET -L http://k8s-master:8009/
{
"paths": [
"/api",
"/api/v1",
...
]
}
#访问方式:
curl http://[k8s-master]:8009/api/v1/namespaces/[namespace-name]/services/[service-name]/proxy

#若要服务一直后台运行则用
nohup kubectl proxy --address='0.0.0.0' --accept-hosts='^*$' --port=8009 2>&1 &

ingress使用

插件地址 kubernetes.github.io/ingress-ngi…

1
2
3
4
bash复制代码#安装插件
wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.48.1/deploy/static/provider/cloud/deploy.yaml
#编辑文件修改如下版本,因为镜像在国内拉取不到,所以换了一下
vim deploy.yaml

image.png

运行 kubectl apply -f deploy.yaml 只要下面的controller成功了就可以了,另外两个先不用管

image.png

编写自己的ingress,之前我已经创建了springboot-demo-nodeport的service了,所以我直接使用
k8s-springboot-ingress.yaml文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yaml复制代码apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: k8s-springboot-ingress
namespace: k8s-springboot
spec:
rules:
- host: k8s-springboot.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: springboot-demo-nodeport
port:
number: 8080

查看ingress 命令为: kubectl get ingress --all-namespaces

image.png

编辑hosts文件

1
2
3
bash复制代码vim /etc/hosts
#增加下面这行 自己主机的IP
192.168.137.200 k8s-springboot.com

访问 curl http://k8s-springboot.com

image.png

至此简单的使用就完了,我也刚刚学习,还有很多不懂,还希望大佬们指教

本文转载自: 掘金

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

图的存储结构 邻接矩阵、邻接表、十字链表、邻接多重表、边集数

发表于 2021-08-14

7.4 图的存储结构

图的存储结构相较线性表与树来说就更加复杂了。首先,我们口头上说的“顶点的位置”或“邻接点的位置”只是一个相对的概念。其实从图的逻辑结构定义来看,图上任何一个顶点都可被看成是第一个顶点,任一顶点的邻接点之间也不存在次序关系。比如图7-4-1中的四张图,仔细观察发现,它们其实是同一个图,只不过顶点的位置不同,就造成了表象上不太一样的感觉。

图7-4-1

也正由于图的结构比较复杂,任意两个顶点之间都可能存在联系,因此无法以数据元素在内存中的物理位置来表示元素之间的关系,也就是说,图不可能用简单的顺序存储结构来表示。而多重链表的方式,即以一个数据域和多个指针域组成的结点表示图中的一个顶点,尽管可以实现图结构,但其实在树中,我们也已经讨论过,这是有问题的。如果各个顶点的度数相差很大,按度数最大的顶点设计结点结构会造成很多存储单元的浪费,而若按每个顶点自己的度数设计不同的顶点结构,又带来操作的不便。因此,对于图来说,如何对它实现物理存储是个难题,不过我们的前辈们已经解决了,现在我们来看前辈们提供的五种不同的存储结构。

7.4.1 邻接矩阵

考虑到图是由顶点和边或弧两部分组成。合在一起比较困难,那就很自然地考虑到分两个结构来分别存储。顶点不分大小、主次,所以用一个一维数组来存储是很不错的选择。而边或弧由于是顶点与顶点之间的关系,一维搞不定,那就考虑用一个二维数组来存储。于是我们的邻接矩阵的方案就诞生了。

图的邻接矩阵(Adjacency Matrix)存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。

设图G有n个顶点,则邻接矩阵是一个n×n的方阵,定义为:

我们来看一个实例,图7-4-2的左图就是一个无向图。

图7-4-2

我们可以设置两个数组,顶点数组为vertex[4]={v0, v1, v2, v3},边数组arc[4][4]为图7-4-2右图这样的一个矩阵。简单解释一下,对于矩阵的主对角线的值,即arc[0][0]、arc[1][1]、arc[2][2]、arc[3][3],全为0是因为不存在顶点到自身的边,比如v0到v0。arc[0][1]=1是因为v0到v1的边存在,而arc[1][3]=0是因为v1到v3的边不存在。并且由于是无向图,v1到v3的边不存在,意味着v3到v1的边也不存在。所以无向图的边数组是一个对称矩阵。

嗯?对称矩阵是什么?忘记了不要紧,复习一下。所谓对称矩阵就是n阶矩阵的元满足aij=aji,(0≤i,j≤n)。即从矩阵的左上角到右下角的主对角线为轴,右上角的元与左下角相对应的元全都是相等的。

有了这个矩阵,我们就可以很容易地知道图中的信息。

1.我们要判定任意两顶点是否有边无边就非常容易了。

2.我们要知道某个顶点的度,其实就是这个顶点vi在邻接矩阵中第i行(或第i列)的元素之和。比如顶点v1的度就是1+0+1+0=2。

3.求顶点vi的所有邻接点就是将矩阵中第i行元素扫描一遍,arc[i][j]为1就是邻接点。

我们再来看一个有向图样例,如图7-4-3所示的左图。

图7-4-3

顶点数组为vertex[4]={v0, v1, v2, v3},弧数组arc[4][4]为图7-4-3右图这样的一个矩阵。主对角线上数值依然为0。但因为是有向图,所以此矩阵并不对称,比如由v1到v0有弧,得到arc[1][0]=1,而v0到v1没有弧,因此arc[0][1]=0。

有向图讲究入度与出度,顶点v1的入度为1,正好是第v1列各数之和。顶点v1的出度为2,即第v1行的各数之和。

与无向图同样的办法,判断顶点vi到vj是否存在弧,只需要查找矩阵中arc[i][j]是否为1即可。要求vi的所有邻接点就是将矩阵第i行元素扫描一遍,查找arc[i][j]为1的顶点。

在图的术语中,我们提到了网的概念,也就是每条边上带有权的图叫做网。那么这些权值就需要存下来,如何处理这个矩阵来适应这个需求呢?我们有办法。

设图G是网图,有n个顶点,则邻接矩阵是一个n×n的方阵,定义为:

这里wij表示(vi,vj)或<vi,vj>上的权值。∞表示一个计算机允许的、大于所有边上权值的值,也就是一个不可能的极限值。有同学会问,为什么不是0呢?原因在于权值wij大多数情况下是正值,但个别时候可能就是0,甚至有可能是负值。因此必须要用一个不可能的值来代表不存在。如图7-4-4左图就是一个有向网图,右图就是它的邻接矩阵。

图7-4-4

那么邻接矩阵是如何实现图的创建的呢?我们先来看看图的邻接矩阵存储的结构,代码如下。

1
2
3
4
5
6
7
8
9
10
c复制代码    typedef char VertexType;                  /* 顶点类型应由用户定义 */
typedef int EdgeType; /* 边上的权值类型应由用户定义 */
#define MAXVEX 100 /* 最大顶点数,应由用户定义 */
#define INFINITY 65535 /* 用65535来代表∞ */
typedef struct
{
VertexType vexs[MAXVEX]; /* 顶点表 */
EdgeType arc[MAXVEX][MAXVEX]; /* 邻接矩阵,可看作边表 */
int numVertexes, numEdges; /* 图中当前的顶点数和边数 */
}MGraph;

有了这个结构定义,我们构造一个图,其实就是给顶点表和边表输入数据的过程。我们来看看无向网图的创建代码。

从代码中也可以得到,n个顶点和e条边的无向网图的创建,时间复杂度为O(n+n2+e),其中对邻接矩阵G.arc的初始化耗费了O(n2)的时间。

7.4.2 邻接表

邻接矩阵是不错的一种图存储结构,但是我们也发现,对于边数相对顶点较少的图,这种结构是存在对存储空间的极大浪费的。比如说,如果我们要处理图7-4-5这样的稀疏有向图,邻接矩阵中除了arc[1][0]有权值外,没有其他弧,其实这些存储空间都浪费掉了。

图7-4-5

因此我们考虑另外一种存储结构方式。回忆我们在线性表时谈到,顺序存储结构就存在预先分配内存可能造成存储空间浪费的问题,于是引出了链式存储的结构。同样的,我们也可以考虑对边或弧使用链式存储的方式来避免空间浪费的问题。

再回忆我们在树中谈存储结构时,讲到了一种孩子表示法,将结点存入数组,并对结点的孩子进行链式存储,不管有多少孩子,也不会存在空间浪费问题。这个思路同样适用于图的存储。我们把这种数组与链表相结合的存储方法称为邻接表(Adjacency List) 。

邻接表的处理办法是这样。

1.图中顶点用一个一维数组存储,当然,顶点也可以用单链表来存储,不过数组可以较容易地读取顶点信息,更加方便。另外,对于顶点数组中,每个数据元素还需要存储指向第一个邻接点的指针,以便于查找该顶点的边信息。

2.图中每个顶点vi的所有邻接点构成一个线性表,由于邻接点的个数不定,所以用单链表存储,无向图称为顶点vi的边表,有向图则称为顶点vi作为弧尾的出边表。

例如图7-4-6所示的就是一个无向图的邻接表结构。

图7-4-6

从图中我们知道,顶点表的各个结点由data和firstedge两个域表示,data是数据域,存储顶点的信息,firstedge是指针域,指向边表的第一个结点,即此顶点的第一个邻接点。边表结点由adjvex和next两个域组成。adjvex是邻接点域,存储某顶点的邻接点在顶点表中的下标,next则存储指向边表中下一个结点的指针。比如v1顶点与v0、v2互为邻接点,则在v1的边表中,adjvex分别为v0的0和v2的2。

这样的结构,对于我们要获得图的相关信息也是很方便的。比如我们要想知道某个顶点的度,就去查找这个顶点的边表中结点的个数。若要判断顶点vi到vj是否存在边,只需要测试顶点vi的边表中adjvex是否存在结点vj的下标j就行了。若求顶点的所有邻接点,其实就是对此顶点的边表进行遍历,得到的adjvex域对应的顶点就是邻接点。

若是有向图,邻接表结构是类似的,比如图7-4-7中第一幅图的邻接表就是第二幅图。但要注意的是有向图由于有方向,我们是以顶点为弧尾来存储边表的,这样很容易就可以得到每个顶点的出度。但也有时为了便于确定顶点的入度或以顶点为弧头的弧,我们可以建立一个有向图的逆邻接表,即对每个顶点vi都建立一个链接为vi为弧头的表。如图7-4-7的第三幅图所示。

图7-4-7

此时我们很容易就可以算出某个顶点的入度或出度是多少,判断两顶点是否存在弧也很容易实现。

对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值信息即可,如图7-4-8所示。

图7-4-8

有了这些结构的图,下面关于结点定义的代码就很好理解了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
arduino复制代码    typedef char VertexType;       /* 顶点类型应由用户定义 */
typedef int EdgeType; /* 边上的权值类型应由用户定义 */

typedef struct EdgeNode /* 边表结点 */
{
int adjvex; /* 邻接点域,存储该顶点对应的下标 */
EdgeType weight; /* 用于存储权值,对于非网图可以不需要 */
struct EdgeNode *next; /* 链域,指向下一个邻接点 */
}EdgeNode;

typedef struct VertexNode /* 顶点表结点 */
{
VertexType data; /* 顶点域,存储顶点信息*/
EdgeNode *firstedge; /* 边表头指针 */
}VertexNode, AdjList[MAXVEX];

typedef struct
{
AdjList adjList;
int numVertexes,numEdges; /* 图中当前顶点数和边数 */
}GraphAdjList;

对于邻接表的创建,也就是顺理成章之事。无向图的邻接表创建代码如下。

这里加粗代码,是应用了我们在单链表创建中讲解到的头插法(3),由于对于无向图,一条边对应都是两个顶点,所以在循环中,一次就针对i和j分别进行了插入。本算法的时间复杂度,对于n个顶点e条边来说,很容易得出是O(n+e)。

7.4.3 十字链表

记得看过一个创意,我非常喜欢。说的是在美国,晚上需要保安通过视频监控对如商场超市、码头仓库、办公写字楼等场所进行安保工作,如图7-4-9所示。值夜班代价总是比较大的,所以人员成本很高。我们国家的一位老兄在国内经常和美国的朋友视频聊天,但总为白天黑夜的时差苦恼,突然灵感一来,想到一个绝妙的点子。他创建一家公司,承接美国客户的视频监控任务,因为美国的黑夜就是中国的白天,利用互联网,他的员工白天上班就可以监控到美国仓库夜间的实际情况,如果发生了像火灾、偷盗这样的突发事件,及时电话到美国当地相关人员处理。由于利用了时差和人员成本的优势,这位老兄发了大财。这个创意让我们知道,充分利用现有的资源,正向思维、逆向思维、整合思维可以创造更大价值。

图7-4-9

那么对于有向图来说,邻接表是有缺陷的。关心了出度问题,想了解入度就必须要遍历整个图才能知道,反之,逆邻接表解决了入度却不了解出度的情况。有没有可能把邻接表与逆邻接表结合起来呢?答案是肯定的,就是把它们整合在一起。这就是我们现在要讲的有向图的一种存储方法:十字链表(Orthogonal List)。

我们重新定义顶点表结点结构如表7-4-1所示。

表7-4-1

其中firstin表示入边表头指针,指向该顶点的入边表中第一个结点,firstout表示出边表头指针,指向该顶点的出边表中的第一个结点。

重新定义的边表结点结构如表7-4-2所示。

表7-4-2

其中tailvex是指弧起点在顶点表的下标,headvex是指弧终点在顶点表中的下标,headlink是指入边表指针域,指向终点相同的下一条边,taillink是指边表指针域,指向起点相同的下一条边。如果是网,还可以再增加一个weight域来存储权值。

比如图7-4-10,顶点依然是存入一个一维数组{v0,v1,v2,v3},实线箭头指针的图示完全与图7-4-7的邻接表相同。就以顶点v0来说,firstout指向的是出边表中的第一个结点v3。所以v0边表结点的headvex=3,而tailvex其实就是当前顶点v0的下标0,由于v0只有一个出边顶点,所以headlink和taillink都是空。

图7-4-10

我们重点需要来解释虚线箭头的含义,它其实就是此图的逆邻接表的表示。对于v0来说,它有两个顶点v1和v2的入边。因此v0的firstin指向顶点v1的边表结点中headvex为0的结点,如图7-4-10右图中的①。接着由入边结点的headlink指向下一个入边顶点v2,如图中的②。对于顶点v1,它有一个入边顶点v2,所以它的firstin指向顶点v2的边表结点中headvex为1的结点,如图中的③。顶点v2和v3也是同样有一个入边顶点,如图中④和⑤。

十字链表的好处就是因为把邻接表和逆邻接表整合在了一起,这样既容易找到以vi为尾的弧,也容易找到以vi为头的弧,因而容易求得顶点的出度和入度。而且它除了结构复杂一点外,其实创建图算法的时间复杂度是和邻接表相同的,因此,在有向图的应用中,十字链表是非常好的数据结构模型。

7.4.4 邻接多重表

讲了有向图的优化存储结构,对于无向图的邻接表,有没有问题呢?如果我们在无向图的应用中,关注的重点是顶点,那么邻接表是不错的选择,但如果我们更关注边的操作,比如对已访问过的边做标记,删除某一条边等操作,那就意味着,需要找到这条边的两个边表结点进行操作,这其实还是比较麻烦的。比如图7-4-11,若要删除左图的(v0,v2)这条边,需要对邻接表结构中右边表的阴影两个结点进行删除操作,显然这是比较烦琐的。

图7-4-11

因此,我们也仿照十字链表的方式,对边表结点的结构进行一些改造,也许就可以避免刚才提到的问题。

重新定义的边表结点结构如表7-4-3所示。

表7-4-3

其中ivex和jvex是与某条边依附的两个顶点在顶点表中下标。ilink指向依附顶点ivex的下一条边,jlink指向依附顶点jvex的下一条边。这就是邻接多重表结构。

我们来看结构示意图的绘制过程,理解了它是如何连线的,也就理解邻接多重表构造原理了。如图7-4-12所示,左图告诉我们它有4个顶点和5条边,显然,我们就应该先将4个顶点和5条边的边表结点画出来。由于是无向图,所以ivex是0、jvex是1还是反过来都是无所谓的,不过为了绘图方便,都将ivex值设置得与一旁的顶点下标相同。

图7-4-12

我们开始连线,如图7-4-13。首先连线的①②③④就是将顶点的firstedge指向一条边,顶点下标要与ivex的值相同,这很好理解。接着,由于顶点v0的(v0,v1)边的邻边有(v0,v3)和(v0,v2)。因此⑤⑥的连线就是满足指向下一条依附于顶点v0的边的目标,注意ilink指向的结点的jvex一定要和它本身的ivex的值相同。同样的道理,连线⑦就是指(v1,v0)这条边,它是相当于顶点v1指向(v1,v2)边后的下一条。v2有三条边依附,所以在③之后就有了⑧⑨。连线⑩的就是顶点v3在连线④之后的下一条边。左图一共有5条边,所以右图有10条连线,完全符合预期。

图7-4-13

到这里,大家应该可以明白,邻接多重表与邻接表的差别,仅仅是在于同一条边在邻接表中用两个结点表示,而在邻接多重表中只有一个结点。这样对边的操作就方便多了,若要删除左图的(v0,v2)这条边,只需要将右图的⑥⑨的链接指向改为∧即可。由于各种基本操作的实现也和邻接表是相似的,这里我们就不讲解代码了。

7.4.5 边集数组

边集数组是由两个一维数组构成。一个是存储顶点的信息;另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、终点下标(end)和权(weight)组成,如图7-4-14所示。显然边集数组关注的是边的集合,在边集数组中要查找一个顶点的度需要扫描整个边数组,效率并不高。因此它更适合对边依次进行处理的操作,而不适合对顶点相关的操作。关于边集数组的应用我们将在本章7.6.2节的克鲁斯卡尔(Kruskal)算法中有介绍,这里就不再详述了。

图7-4-14

定义的边数组结构如表7-4-4所示。

表7-4-4

其中begin是存储起点下标,end是存储终点下标,weight是存储权值。

本文转载自: 掘金

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

实战小技巧5:驼峰与下划线互转

发表于 2021-08-14

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

实战小技巧5:驼峰与下划线互转

每天一个实战小技巧:驼峰与下划线划转

这个考题非常实用,特别是对于我们这些号称只需要CURD的后端开发来说,驼峰与下划线互转,这不是属于日常任务么;一般来讲db中的列名,要求是下划线格式(why? 阿里的数据库规范是这么定义的,就我感觉驼峰也没毛病),而java实体命名则是驼峰格式,所以它们之间的互转,就必然存在一个驼峰与下划线的互转

今天我们就来看一下,这两个的互转支持方式

1. Gauva

一般来讲遇到这种普适性的问题,大部分都是有现成的工具类可以来直接使用的;在java生态中,说到好用的工具百宝箱,guava可以说是排列靠前的

接下来我们看一下如何使用Gauva来实现我们的目的

1
2
3
4
5
6
7
java复制代码// 驼峰转下划线
String ans = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, "helloWorld");
System.out.println(ans);

// 下划线转驼峰
String ans2 = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, "hello_world");
System.out.println(ans2);

在这里主要使用的是CaseFormat来实现互转,guava的CaseFormat还提供了其他几种方式

上面这个虽然可以实现互转,但是如果我们有一个字符串为 helloWorld_Case

将其他转换输出结果如下:

  • 下划线:hello_world__case
  • 驼峰:helloworldCase

这种输出,和标准的驼峰/下划线不太一样了(当然原因是由于输入也不标准)

2. Hutool

除了上面的guava,hutool的使用也非常广,其中包含很多工具类,其StrUtil也提供了下划线与驼峰的互转支持

1
2
3
4
java复制代码String ans = StrUtil.toCamelCase("hello_world");
System.out.println(ans);
String ans2 = StrUtil.toUnderlineCase("helloWorld");
System.out.println(ans2);

同样的我们再来看一下特殊的case

1
2
java复制代码System.out.println(StrUtil.toCamelCase("helloWorld_Case"));
System.out.println(StrUtil.toUnderlineCase("helloWorld_Case"));

输出结果如下

  • 驼峰:helloworldCase
  • 下划线: hello_world_case

相比较上面的guava的场景,下划线这个貌似还行

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
java复制代码private static final char UNDER_LINE = '_';

/**
* 下划线转驼峰
*
* @param name
* @return
*/
public static String toCamelCase(String name) {
if (null == name || name.length() == 0) {
return null;
}

int length = name.length();
StringBuilder sb = new StringBuilder(length);
boolean underLineNextChar = false;

for (int i = 0; i < length; ++i) {
char c = name.charAt(i);
if (c == UNDER_LINE) {
underLineNextChar = true;
} else if (underLineNextChar) {
sb.append(Character.toUpperCase(c));
underLineNextChar = false;
} else {
sb.append(c);
}
}

return sb.toString();
}

驼峰转下划线

  • 关键点:大写的,则前位补一个下划线,当前字符转小写(如果前面已经是一个下划线了,那前面不补,直接转小写即可)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public static String toUnderCase(String name) {
if (name == null) {
return null;
}

int len = name.length();
StringBuilder res = new StringBuilder(len + 2);
char pre = 0;
for (int i = 0; i < len; i++) {
char ch = name.charAt(i);
if (Character.isUpperCase(ch)) {
if (pre != UNDER_LINE) {
res.append(UNDER_LINE);
}
res.append(Character.toLowerCase(ch));
} else {
res.append(ch);
}
pre = ch;
}
return res.toString();
}

再次测试helloWorld_Case,输出如下

  • 驼峰:helloWorldCase
  • 下划线: hello_world_case

系列博文

  • 实战小技巧1:字符串占位替换-JDK版
  • 实战小技巧2:数组与list互转
  • 实战小技巧3:字符串与容器互转
  • 实战小技巧4:优雅的实现字符串拼接

II. 其他

1. 一灰灰Blog: liuyueyi.github.io/hexblog

一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

2. 声明

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

  • 微博地址: 小灰灰Blog
  • QQ: 一灰灰/3302797840
  • 微信公众号: 一灰灰blog

本文转载自: 掘金

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

💖每天拿出20分钟,带你入门涨薪3k的ElasticSear

发表于 2021-08-14

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

🌈往期回顾

**感谢阅读,希望能对你有所帮助,博文若有瑕疵请在评论区留言或在主页个人介绍中添加我私聊我,感谢每一位小伙伴不吝赐教。我是XiaoLin,既会写bug也会唱rap的男孩**


**今天是特殊的一天,祝大家有情人的终成眷属,没情人的涨薪20k**
  • 🌹史上最全的后端必备Linux常用命令汇总(超全面!超详细!)收藏这一篇就够了!🌹
  • 🌈MySQL真的就CRUD吗?✨来看看2k和12k之间的差距(下)
  • 🌈MySQL真的就CRUD吗?✨来看看2k和12k之间的差距(上)

一、ElasticSearch简介

1.1、什么是全文检索

全文检索是计算机程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置。当用户查询时根据建立的索引查找,类似于通过字典的检索字表查字的过程。


检索: 索(建立索引) 检:(检索索引)


全文检索(Full-Text Retrieval(检索))以文本作为检索对象,找出含有指定词汇的文本。**全面、准确和快速是衡量全文检索系统的关键指标。**
全文检索的特点:
  1. 只处理文本。
  2. 不处理语义。
  3. 搜索时英文不区分大小写。
  4. 结果列表有相关度排序。

1.2、什么是ElasticSearch

ElasticSearch 简称ES ,**是基于Apache Lucene构建的开源搜索引擎,是当前流行的企业级搜索引擎**。Lucene本身就可以被认为迄今为止性能最好的一款开源搜索引擎工具包,但是lucene的API相对复杂,需要深厚的搜索理论。很难集成到实际的应用中去。**但是ES是采用java语言编写,提供了简单易用的RestFul API,开发者可以使用其简单的RestFul API,开发相关的搜索功能,从而避免lucene的复杂性**。

1.3、ElasticSearch 的诞生

多年前,一个叫做Shay Banon的刚结婚不久的失业开发者,由于妻子要去伦敦学习厨师,他便跟着也去了。在他找工作的过程中,为了给妻子构建一个食谱的搜索引擎,他开始构建一个早期版本的Lucene。


直接基于Lucene工作会比较困难,所以Shay开始抽象Lucene代码以便Java程序员可以在应用中添加搜索功能。他发布了他的第一个开源项目,叫做“Compass”。


后来Shay找到一份工作,这份工作处在高性能和内存数据网格的分布式环境中,因此高性能的、实时的、分布式的搜索引擎也是理所当然需要的。然后他决定重写Compass库使其成为一个独立的服务叫做Elasticsearch。


第一个公开版本出现在2010年2月,在那之后Elasticsearch已经成为Github上最受欢迎的项目之一,代码贡献者超过300人。一家主营Elasticsearch的公司就此成立,他们一边提供商业支持一边开发新功能,不过Elasticsearch将永远开源且对所有人可用。
Shay的妻子依旧等待着她的食谱搜索……

1.4、ElasticSearch应用场景

ES主要以轻量级JSON作为数据存储格式,这点与MongoDB有点类似。同时也支持地理位置查询 ,还方便地理位置和文本混合查询 。 以及在统计、日志类数据存储和分析、可视化这方面是引领者。国内外的使用场景为:
  1. 国外: Wikipedia(维基百科)使用ES提供全文搜索并高亮关键字、StackOverflow(IT问答网站)结合全文搜索与地理位置查询、Github使用Elasticsearch检索1300亿行的代码。
  2. 国内:百度(在云分析、网盟、预测、文库、钱包、风控等业务上都应用了ES,单集群每天导入30TB+数据, 总共每天60TB+)、新浪 、阿里巴巴、腾讯等公司均有对ES的使用。

二、ElasticSearch安装

2.1、环境准备

  1. centos7
  2. jdk(1.8以上)
  3. ElasticSearch6.8.0

2.2、下载ElasticSearch

我们可以从官网下载ElasticSearch
1
shell复制代码wget http://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.8.0.tar.gz

2.3、安装JDK

2.3.1、下载JDK

1
2
shell复制代码# 默认位置是 /usr/java/jdk1.8.0_171-amd64*/
rpm -ivh jdk-8u181-linux-x64.rpm

2.3.2、配置环境变量

1
shell复制代码vim /etc/profile
在这个配置文件的末尾加入:
1
2
shell复制代码export JAVA_HOME=/usr/java/jdk1.8.0_171-amd64
export PATH=$PATH:$JAVA_HOME/bin

2.3.3、重载系统配置

1
shell复制代码source /etc/profile

2.4、ElasticSearch安装(Linux)

2.4.1、添加新的用户并且赋予权限

1
2
3
4
5
6
7
8
9
10
11
shell复制代码# 在linux系统中创建新的组
groupadd es

# 创建新的用户xialin并将es用户放入es组中
useradd xiaolin -g es

# 修改es用户密码
passwd xiaolin

# 赋予权限(给xiaolin赋予/usr文件夹下所有权限)
chown -R xiaolin /usr

2.4.2、解压

1
shell复制代码tar -zxvf elasticsearch-6.4.1.tar.gz

2.4.3、了解目录结构

image-20210422101835207

  • bin 可执行的二进制文件的目录
  • config 配置文件的目录
  • lib 运行时依赖的库
  • logs 运行时日志文件
  • modules 运行时依赖的模块
  • plugins 可以安装官方以及第三方插件

2.4.4、启动服务

进入bin目录中启动ES服务

1
shell复制代码./elasticsearch

image-20210422102014689

2.4.5、测试

默认web服务端口9200,真正的java端口(tcp端口)9300,任何身份都可以访问

1
2
shell复制代码# (curl相当于模拟浏览器,检测es是否安装成功且默认不允许远程链接)
curl http://localhost:9200

image-20210422102134635

2.4.6、开启远程连接

注意:ES服务默认启动是受保护的,只允许本地客户端连接,如果想要通过远程客户端访问,必须开启远程连接

我们只需要

三、ElasticSearch基本概念

image-20200701163807755

3.1、接近实时(NRT Near Real Time )

**Elasticsearch是一个接近实时的搜索平台**。这意味着,**从索引一个文档直到这个文档能够被搜索到有一个轻微的延迟(通常是1秒内)**

3.2、索引

ElasticSearch操作流程

  1. 当ElasticSearch执行添加操作时,先将数据添加到索引中,然后根据指定好的分词器规则对text类型字段进行分词。
  2. 字段分词之后,会得到一系列词根,ElasticSearch将这些词根保存到一张倒排索引表中,这张表会建立词根与文档之间的关联关系。
  3. 当用户进行全文检索的时候,输入查询语句关键词语的时候,ElasticSearch会对这个关键词语进行分词,然后根据这些去匹配倒排索引表,如果这些分词与倒排索引表词根能够匹配,那么词根关联文档的id就是满足搜索条件的文档。
  4. ElasticSearch会将满足搜索的文档一个一个去查询,然后进行综合评分,排序后再返回。

一个索引就是一个拥有几分相似特征的文档的集合。比如说,你可以有一个客户数据的索引,另一个产品目录的索引,还有一个订单数据的索引。一个索引由一个名字来标识(必须全部是小写字母的),并且当我们要对这个索引中的文档进行索引、搜索、更新和删除的时候,都要使用到这个名字。索引类似于关系型数据库中Database 的概念。在一个集群中,如果你想,可以定义任意多的索引。

3.3、类型

在一个索引中,你可以定义一种或多种类型。一个类型是你的索引的一个逻辑上的分类/分区,其语义完全由你来定。通常,会为具有一组共同字段的文档定义一个类型。比如说,我们假设你运营一个博客平台并且将你所有的数 据存储到一个索引中。在这个索引中,你可以为用户数据定义一个类型,为博客数据定义另一个类型,当然,也可 以为评论数据定义另一个类型。类型类似于关系型数据库中Table的概念。 不同的版本对索引的要求也不同。

版本 Type
5.x 支持多种 type
6.x 只能有一种 type
7.x 默认不再支持自定义索引类型(默认类型为:_doc)

3.4、映射

Mapping是ES中的一个很重要的内容,它类似于传统关系型数据中table的schema,用于定义一个索引(index)中的类型(type)的数据的结构。 在ES中,我们可以手动创建type(相当于table)和mapping(相关与schema),也可以采用默认创建方式。在默认配置下,ES可以根据插入的数据自动地创建type及其mapping。 mapping中主要包括字段名、字段数据类型和字段索引类型

3.5、文档

**一个文档是一个可被索引的基础信息单元,类似于表中的一条记录。**比如,你可以拥有某一个员工的文档,也可以拥有某个商品的一个文档。文档以采用了轻量级的数据交换格式JSON(Javascript Object Notation)来表示。

3.6、分片

一个索引可以存储超出单个节点硬件限制的大量数据。比如,一个具有 10 亿文档数据的索引占据 1TB 的磁盘空间,而任一节点都可能没有这样大的磁盘空间。或者单个节点处理搜索请求,响应太慢。为了解决这个问题,Elasticsearch 提供了将索引划分成多份的能力,每一份就称之为分片。当你创建一个索引的时候,你可以指定你想要的分片的数量。每个分片本身也是一个功能完善并且独立的“索引”,这个“索引”可以被放置到集群中的任何节点上。

分片很重要,主要有两方面的原因:

  1. 允许你水平分割 / 扩展你的内容容量。
  2. 允许你在分片之上进行分布式的、并行的操作,进而提高性能/吞吐量。

至于一个分片怎样分布,它的文档怎样聚合和搜索请求,是完全由 Elasticsearch 管理的,对于作为用户的你来说,这些都是透明的,无需过分关心。

3.6.1、分片原理

传统的数据库每个字段存储单个值,但这对全文检索并不够。文本字段中的每个单词需要被搜索,对数据库意味着需要单个字段有索引多值的能力。最好的支持是一个字段多个值需求的数据结构是倒排索引。

Elasticsearch 使用一种称为倒排索引的结构,它适用于快速的全文搜索。

见其名,知其意,有倒排索引,肯定会对应有正向索引。正向索引(forward index),反向索引(inverted index)更熟悉的名字是倒排索引。

3.6.2、正排索引

所谓的正向索引,就是搜索引擎会将待搜索的文件都对应一个文件 ID,搜索时将这个ID 和搜索关键字进行对应,形成 K-V 对,然后对关键字进行统计计数。

正排索引

但是互联网上收录在搜索引擎中的文档的数目是个天文数字,这样的索引结构根本无法满足实时返回排名结果的要求。于是倒排索引他来了!

3.6.3、倒排索引

倒排索引是把文件ID 对应到关键词的映射转换为关键词到文件ID 的映射,每个关键词都对应着一系列的文件,这些文件中都出现这个关键词。

倒排索引

一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。例如,假设我们有两个文档,每个文档的 content 域包含如下内容:

  1. The quick brown fox jumped over the lazy dog
  2. Quick brown foxes leap over lazy dogs in summer

为了创建倒排索引,我们首先将每个文档的 content 域拆分成单独的 词(我们称它为 词条或 tokens ),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。

image-20210528193508693

现在,如果我们想搜索 quick和brown ,我们只需要查找包含每个词条的文档.

image-20210528193530543

两个文档都匹配,但是第一个文档比第二个匹配度更高。如果我们使用仅计算匹配词条数量的简单相似性算法,那么我们可以说,对于我们查询的相关性来讲,第一个文档比第二个文档更佳。

3.7、副本

在一个网络 / 云的环境里,失败随时都可能发生,在某个分片/节点不知怎么的就处于离线状态,或者由于任何原因消失了,这种情况下,有一个故障转移机制是非常有用并且是强烈推荐的。为此目的,Elasticsearch 允许你创建分片的一份或多份拷贝,这些拷贝叫做复制分片(副本)。

复制分片之所以重要,有两个主要原因:

  1. 在分片/节点失败的情况下,提供了高可用性。因为这个原因,注意到复制分片从不与原/主要(original/primary)分片置于同一节点上是非常重要的。
  2. 扩展你的搜索量/吞吐量,因为搜索可以在所有的副本上并行运行。

本文转载自: 掘金

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

SpringBoot集成Mybatis-plus

发表于 2021-08-13

本节主要介绍如何在新项目中集成Mybatis-plus;从而完成对一个数据库表的增删查改操作。

创建一个Springboot工程

— 此处略过

引入对应的依赖包

完整的pom.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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.hank</groupId>
<artifactId>mybatisdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mybatisdemo</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>com.alibaba</groupId>-->
<!-- <artifactId>druid</artifactId>-->
<!-- <version>1.1.20</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

以上内容,与Mybatis-plus相关主要为以下部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xml复制代码 <!-- mybatis -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>com.alibaba</groupId>-->
<!-- <artifactId>druid</artifactId>-->
<!-- <version>1.1.20</version>-->
<!-- </dependency>-->

Mybatis-plus是基于Mybatis的一个增强插件,默认具备Mybatis的所有特性外加分页功能;

最后我们可以在项目结构中最终映入的Jar包资源,其中与Mybatis-plus相关的资源如下

(在IDEA工具中,通过File–>Project Structure…–>Modules–>选中自己的项目–>Dependencies)

从以上我们可以看到,虽然我们只在pom.xml中引入了mybatis-plus-boot-starter,版本为3.3.1,但是最终实际引入了其他的相关资源包。

\

编码部分

添加启动扫描注解

在项目启动类添加启动注解扫描;如果不在启动类配置,也可以在每个mapper上加上@Mapper注解

1
2
3
4
5
6
7
java复制代码@SpringBootApplication
@MapperScan("com.hank.mybatisdemo.dao")
public class MybatisdemoApplication {
public static void main(String[] args) {
SpringApplication.run(MybatisdemoApplication.class, args);
}
}

编写实体类和Mapper类

此处以T_Temp为例;注意,这里接口类需要继承BaseMapper类,泛型为User实体对象

1
2
3
4
5
6
7
java复制代码package com.hank.mybatisdemo.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hank.mybatisdemo.entity.User;

public interface UserMapper extends BaseMapper<User> {
}

User实体对象如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码package com.hank.mybatisdemo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName(value = "T_Temp")
public class User {

@TableId(value = "id",type= IdType.AUTO)
Integer id;
@TableField(value = "user_name")
String userName;
Integer age;
String sex;
String remark;
}

对应表的结构如下

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码/*Table structure for table `t_temp` */

DROP TABLE IF EXISTS `t_temp`;

CREATE TABLE `t_temp` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_name` varchar(30) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`sex` varchar(2) DEFAULT NULL,
`remark` varchar(225) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8mb4;

配置文件

1
2
3
4
5
6
7
8
properties复制代码spring:
datasource:
# type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://127.0.0.1:3306/btest?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.mysql.cj.jdbc.MysqlDataSource

\

编写测试用例验证

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复制代码@RunWith(SpringRunner.class)
@SpringBootTest
public class UserTest {
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@Autowired
UserMapper userMapper;

@Test
public void testUser(){
List<User> userList=userMapper.selectList(null);
userList.forEach(System.out::println);
}

@Test
public void testUser2(){
for (int i = 0; i <10 ; i++) {
User u=new User();
u.setUserName("张三"+i);
Random random = new Random();
u.setAge(random.nextInt());
u.setSex(i%2>1?"男":"女");
int row=userMapper.insert(u);
Assert.assertEquals(1, row);
}
}
}

本文转载自: 掘金

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

Web框架Gin | Gin 路由

发表于 2021-08-13

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

Gin 是一个标准的 Web 服务框架,遵循 Restful API 接口规范,其路由库是基于 httproute 实现的。

本节将从 Gin 路由开始,详细讲述各种路由场景下,如何通过 Gin 来实现。

基本路由

Gin 支持 GET、POST、PUT、PATCH、DELETE、OPTIONS 等请求类型。

示例:

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
go复制代码package main

import (
"net/http"

"github.com/gin-gonic/gin"
)

func main() {
route := gin.Default()

// 设置一个get请求,其URL为/hello,并实现简单的响应
route.GET("/get", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "this is a get method response!",
})
})

// 具体实现可单独定义一个函数
route.POST("/post", postHandler)

route.PUT("/put", func(c *gin.Context) {

})

route.PATCH("/patch", func(c *gin.Context) {

})

route.DELETE("/delete", func(c *gin.Context) {

})

// ……

route.Run()
}

func postHandler(c *gin.Context) {
c.JSON(http.StatusOK, "this is a post method response!")
}

参数处理

path 参数

可通过 *gin.Context 的 Param 函数获取请求 Path 中的参数,Path 路径中参数以:开头,如:/user/:name,能够匹配路径 /user/xx。

示例:

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
go复制代码package main

import (
"fmt"
"net/http"

"github.com/gin-gonic/gin"
)

type User struct {
Username string `json:"username"`
Sex string `json:"sex"`
Age int `json:"age"`
Labels []string `json:"lalels"`
}

func main() {
route := gin.Default()

users := []User{
{
Username: "xcbeyond",
Sex: "男",
Age: 18,
Labels: []string{"年轻", "帅"},
},
}

// 请求path中存在参数
route.GET("/user/:name", func(c *gin.Context) {
// 获取请求path中的参数
name := c.Param("name")
for _, user := range users {
if user.Username == name {
c.JSON(http.StatusOK, user)
return
}
}
c.JSON(http.StatusOK, fmt.Errorf("not found user [%s]", name))
})

route.Run()
}

查询参数

可通过 *gin.Context 的 Query 函数获取参数,如:/user?name=xcbeyond。

示例:

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
go复制代码package main

import (
"net/http"

"github.com/gin-gonic/gin"
)

type User struct {
Username string `json:"username"`
Sex string `json:"sex"`
Age int `json:"age"`
Labels []string `json:"lalels"`
}

func main() {
route := gin.Default()

users := []User{
{
Username: "xcbeyond",
Sex: "男",
Age: 18,
Labels: []string{"年轻", "帅"},
},
}

// 查询参数,如:/user?name=xcbeyond
route.GET("/user", func(c *gin.Context) {
// 获取中的参数
name := c.Query("name")
for _, user := range users {
if user.Username == name {
c.JSON(http.StatusOK, user)
return
}
}
c.JSON(http.StatusOK, "not found user "+name)
})

route.Run()
}

……

路由分组

Gin 提供了路由分组的能力,方便管理分组管理路由,将具有相同路由 URL 前缀的进行分类处理,常见于不同版本的分组,如:/api/v1、/api/v1。

此外,还支持多层分组。

示例:

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
go复制代码package main

import (
"math/rand"
"net/http"

"github.com/gin-gonic/gin"
)

type User struct {
Username string `json:"username"`
Sex string `json:"sex"`
Age int `json:"age"`
Labels []string `json:"lalels"`
}

func main() {
route := gin.Default()

users := []User{
{
Username: "xcbeyond",
Sex: "男",
Age: 18,
Labels: []string{"年轻", "帅"},
},
{
Username: "niki",
Sex: "女",
Age: 16,
Labels: []string{"漂亮"},
},
}

// api分组
api := route.Group("/api")
{
// v1分组
v1 := api.Group("/v1")
{
v1.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
for _, user := range users {
if user.Username == name {
c.JSON(http.StatusOK, user)
return
}
}
c.JSON(http.StatusOK, "not found user :"+name)
})
}

// v2分组
v2 := api.Group("/v2")
{
v2.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
for _, user := range users {
if user.Username == name {
c.JSON(http.StatusOK, user)
return
}
}

// 如果没有查到,则随机返回一个
user := users[rand.Intn(len(users)-1)]
c.JSON(http.StatusOK, "not found user ["+name+"],but user ["+user.Username+"] is exist!")
})
}
}

route.Run()
}

路由拆分

上述路由示例中,都是将所有路由信息写在同一个源文件、函数中,但在实际项目中会涉及大量的接口,写在一起就显得太不合适了。

在实际项目中,我们更倾向于将路由代码拆分出来,可拆分为单独的包、多个路由源文件等。

根据实际项目的规模大小,可分不同粒度拆分。

(以下路由拆分仅供参考,可根据具体项目灵活调整!)

路由拆分为单独源文件或包

将路由实现分离到单独的包下,使得项目结构更加清晰。项目结构如下:

1
2
3
4
sh复制代码.
├── main.go
└── routes
└── routes.go

示例完整源码:route-split-v1

在 /routes/routes.go 文件中实现并注册路由信息:

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
go复制代码package routes

import (
"net/http"

"github.com/gin-gonic/gin"
)

type User struct {
Username string `json:"username"`
Sex string `json:"sex"`
Age int `json:"age"`
Labels []string `json:"lalels"`
}

var users []User

// 为方便测试,则直接通过init方式赋值。实际项目中,一般通过数据库等其它方式查询获取数据。
func init() {
users = []User{
{
Username: "xcbeyond",
Sex: "男",
Age: 18,
Labels: []string{"年轻", "帅"},
},
{
Username: "niki",
Sex: "女",
Age: 16,
Labels: []string{"漂亮"},
},
}
}

// SetupRouter 配置路由
func SetupRouter() *gin.Engine {
route := gin.Default()

route.GET("/user/:name", querUserHandler)
// 其它更多路由

return route
}

// handler
func querUserHandler(c *gin.Context) {
name := c.Param("name")
for _, user := range users {
if user.Username == name {
c.JSON(http.StatusOK, user)
return
}
}
c.JSON(http.StatusOK, "not found user :"+name)
}

并在 main.go 中调用路由配置函数 SetupRouter 即可:

1
2
3
4
5
6
go复制代码func main() {
route := routes.SetupRouter()
if err := route.Run(); err != nil {
fmt.Printf("startup server failed,err: %v", err)
}
}

路由拆分为多个源文件

当随着项目的业务功能丰富,体量变大,所有的路由都写在一个 routes.go 源文件中,将会导致这个源文件越来越臃肿,不便于后期维护和阅读。

因此,可根据某种维度拆分为多个路由文件来实现不同业务功能。项目结构如下:

1
2
3
4
5
6
sh复制代码.
├── main.go
└── routes
├── auth.go
├── routes.go
└── user.go

示例完整源码:route-split-v2

在 routes 包下,根据某种维度拆分为多个路由实现文件,如:根据业务模块,可拆分为认证模块(auth.go)、用户模块(user.go)等,并在各自路由文件中实现具体的业务功能,并进行路由注册。

如,认证模块 auth.go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码package routes

import (
"github.com/gin-gonic/gin"
)

// AuthRegister route register
func AuthRegister(e *gin.Engine) {
e.GET("/auth/login", loginHandler)
e.POST("/auth/logout", logoutUserHanler)
// ……
}

// loginHandler
func loginHandler(c *gin.Context) {

}

// logoutUserHanler
func logoutUserHanler(c *gin.Context) {

}

其中,定义 AuthRegister 函数将该模块下所有路由进行注册,注意该函数为大写字母开头,作为全局函数能够被包外 main.go 调用。

routes/routes.go 中,配置路由,并统一注册所有模块的路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码package routes

import (
"github.com/gin-gonic/gin"
)

// SetupRouter 配置路由
func SetupRouter() *gin.Engine {
route := gin.Default()

// other config

// register all route.
UserRegister(route)
AuthRegister(route)
// ……

return route
}

main.go 和上一版本一样,作为程序等入口:

1
2
3
4
5
6
go复制代码func main() {
route := routes.SetupRouter()
if err := route.Run(); err != nil {
fmt.Printf("startup server failed,err: %v", err)
}
}

参考资料:

  1. api-examples
  2. gin框架路由拆分与注册

本文转载自: 掘金

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

ReentrantLock 中的 4 个坑!

发表于 2021-08-13

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

JDK 1.5 之前 synchronized 的性能是比较低的,但在 JDK 1.5 中,官方推出一个重量级功能 Lock,一举改变了 Java 中锁的格局。JDK 1.5 之前当我们谈到锁时,只能使用内置锁 synchronized,但如今我们锁的实现又多了一种显式锁 Lock。

前面的文章我们已经介绍了 synchronized,详见以下列表:

《synchronized 加锁 this 和 class 的区别!》

《synchronized 优化手段之锁膨胀机制!》

《synchronized 中的 4 个优化,你知道几个?》

所以本文咱们重点来看 Lock。

Lock 简介

Lock 是一个顶级接口,它的所有方法如下图所示:
image.png
它的子类列表如下:
image.png
我们通常会使用 ReentrantLock 来定义其实例,它们之间的关联如下图所示:
image.png

PS:Sync 是同步锁的意思,FairSync 是公平锁,NonfairSync 是非公平锁。

ReentrantLock 使用

学习任何一项技能都是先从使用开始的,所以我们也不例外,咱们先来看下 ReentrantLock 的基础使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class LockExample {
// 创建锁对象
private final ReentrantLock lock = new ReentrantLock();
public void method() {
// 加锁操作
lock.lock();
try {
// 业务代码......
} finally {
// 释放锁
lock.unlock();
}
}
}

ReentrantLock 在创建之后,有两个关键性的操作:

  • 加锁操作:lock()
  • 释放锁操作:unlock()

ReentrantLock 中的坑

1.ReentrantLock 默认为非公平锁

很多人会认为(尤其是新手朋友),ReentrantLock 默认的实现是公平锁,其实并非如此,ReentrantLock 默认情况下为非公平锁(这主要是出于性能方面的考虑),比如下面这段代码:

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复制代码import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
// 创建锁对象
private static final ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) {
// 定义线程任务
Runnable runnable = new Runnable() {
@Override
public void run() {
// 加锁
lock.lock();
try {
// 打印执行线程的名字
System.out.println("线程:" + Thread.currentThread().getName());
} finally {
// 释放锁
lock.unlock();
}
}
};
// 创建多个线程
for (int i = 0; i < 10; i++) {
new Thread(runnable).start();
}
}
}

以上程序的执行结果如下:
image.png
从上述执行的结果可以看出,ReentrantLock 默认情况下为非公平锁。因为线程的名称是根据创建的先后顺序递增的,所以如果是公平锁,那么线程的执行应该是有序递增的,但从上述的结果可以看出,线程的执行和打印是无序的,这说明 ReentrantLock 默认情况下为非公平锁。

想要将 ReentrantLock 设置为公平锁也很简单,只需要在创建 ReentrantLock 时,设置一个 true 的构造参数就可以了,如下代码所示:

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复制代码import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
// 创建锁对象(公平锁)
private static final ReentrantLock lock = new ReentrantLock(true);

public static void main(String[] args) {
// 定义线程任务
Runnable runnable = new Runnable() {
@Override
public void run() {
// 加锁
lock.lock();
try {
// 打印执行线程的名字
System.out.println("线程:" + Thread.currentThread().getName());
} finally {
// 释放锁
lock.unlock();
}
}
};
// 创建多个线程
for (int i = 0; i < 10; i++) {
new Thread(runnable).start();
}
}
}

以上程序的执行结果如下:
image.png
从上述结果可以看出,当我们显式的给 ReentrantLock 设置了 true 的构造参数之后,ReentrantLock 就变成了公平锁,线程获取锁的顺序也变成有序的了。

其实从 ReentrantLock 的源码我们也可以看出它究竟是公平锁还是非公平锁,ReentrantLock 部分源码实现如下:

1
2
3
4
5
6
java复制代码 public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

从上述源码中可以看出,默认情况下 ReentrantLock 会创建一个非公平锁,如果在创建时显式的设置构造参数的值为 true 时,它就会创建一个公平锁。

2.在 finally 中释放锁

使用 ReentrantLock 时一定要记得释放锁,否则就会导致该锁一直被占用,其他使用该锁的线程则会永久的等待下去,所以我们在使用 ReentrantLock 时,一定要在 finally 中释放锁,这样就可以保证锁一定会被释放。

反例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
// 创建锁对象
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
// 加锁操作
lock.lock();
System.out.println("Hello,ReentrantLock.");
// 此处会报异常,导致锁不能正常释放
int number = 1 / 0;
// 释放锁
lock.unlock();
System.out.println("锁释放成功!");
}
}

以上程序的执行结果如下:
image.png
从上述结果可以看出,当出现异常时锁未被正常释放,这样就会导致其他使用该锁的线程永久的处于等待状态。

正例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
// 创建锁对象
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
// 加锁操作
lock.lock();
try {
System.out.println("Hello,ReentrantLock.");
// 此处会报异常
int number = 1 / 0;
} finally {
// 释放锁
lock.unlock();
System.out.println("锁释放成功!");
}
}
}

以上程序的执行结果如下:
image.png
从上述结果可以看出,虽然方法中出现了异常情况,但并不影响 ReentrantLock 锁的释放操作,这样其他使用此锁的线程就可以正常获取并运行了。

3.锁不能被释放多次

lock 操作的次数和 unlock 操作的次数必须一一对应,且不能出现一个锁被释放多次的情况,因为这样就会导致程序报错。

反例

一次 lock 对应了两次 unlock 操作,导致程序报错并终止执行,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
java复制代码import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
// 创建锁对象
private static final ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) {
// 加锁操作
lock.lock();

// 第一次释放锁
try {
System.out.println("执行业务 1~");
// 业务代码 1......
} finally {
// 释放锁
lock.unlock();
System.out.println("锁释锁");
}

// 第二次释放锁
try {
System.out.println("执行业务 2~");
// 业务代码 2......
} finally {
// 释放锁
lock.unlock();
System.out.println("锁释锁");
}
// 最后的打印操作
System.out.println("程序执行完成.");
}
}

以上程序的执行结果如下:
image.png
从上述结果可以看出,执行第 2 个 unlock 时,程序报错并终止执行了,导致异常之后的代码都未正常执行。

4.lock 不要放在 try 代码内

在使用 ReentrantLock 时,需要注意不要将加锁操作放在 try 代码中,这样会导致未加锁成功就执行了释放锁的操作,从而导致程序执行异常。

反例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
// 创建锁对象
private static final ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) {
try {
// 此处异常
int num = 1 / 0;
// 加锁操作
lock.lock();
} finally {
// 释放锁
lock.unlock();
System.out.println("锁释锁");
}
System.out.println("程序执行完成.");
}
}

以上程序的执行结果如下:
image.png
从上述结果可以看出,如果将加锁操作放在 try 代码中,可能会导致两个问题:

  1. 未加锁成功就执行了释放锁的操作,从而导致了新的异常;
  2. 释放锁的异常会覆盖程序原有的异常,从而增加了排查问题的难度。

总结

本文介绍了 Java 中的显式锁 Lock 及其子类 ReentrantLock 的使用和注意事项,Lock 在 Java 中占据了锁的半壁江山,但在使用时却要注意 4 个问题:

  1. 默认情况下 ReentrantLock 为非公平锁而非公平锁;
  2. 加锁次数和释放锁次数一定要保持一致,否则会导致线程阻塞或程序异常;
  3. 加锁操作一定要放在 try 代码之前,这样可以避免未加锁成功又释放锁的异常;
  4. 释放锁一定要放在 finally 中,否则会导致线程阻塞。

本系列推荐文章

  1. 线程的 4 种创建方法和使用详解!
  2. Java中用户线程和守护线程区别这么大?
  3. 深入理解线程池 ThreadPool
  4. 线程池的7种创建方式,强烈推荐你用它…
  5. 池化技术到达有多牛?看了线程和线程池的对比吓我一跳!
  6. 并发中的线程同步与锁
  7. synchronized 加锁 this 和 class 的区别!
  8. volatile 和 synchronized 的区别
  9. 轻量级锁一定比重量级锁快吗?
  10. 这样终止线程,竟然会导致服务宕机?
  11. SimpleDateFormat线程不安全的5种解决方案!
  12. ThreadLocal不好用?那是你没用对!
  13. ThreadLocal内存溢出代码演示和原因分析!
  14. Semaphore自白:限流器用我就对了!
  15. CountDownLatch:别浪,等人齐再团!
  16. CyclicBarrier:人齐了,司机就可以发车了!
  17. synchronized 优化手段之锁膨胀机制
  18. synchronized 中的 4 个优化,你知道几个?

关注公号「Java中文社群」查看更多有意思、涨知识的 Java 并发文章。

本文转载自: 掘金

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

1…564565566…956

开发者博客

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