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

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


  • 首页

  • 归档

  • 搜索

Redis键值类型及底层原理全解析 String hash

发表于 2021-11-18

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

String

常见命令

  1. set :数字写命令

image.png

  1. get 获取数据

image.png

  1. del 数据删除命令

image.png

  1. getset 获取原有value值写入新的value值

image.png

  1. incr,incrby,数据加法计算,incr是+1运算,incrby是+n计算

image.png

  1. decr,decrby,数据减法运算,decr是-1运算,decrby是减n运算

image.png

  1. 获取String长度

image.png

  1. 设置value值并设置过期时间命令setex

image.png

  1. 从指定位置替换赋值,从指定位置开始替换,命令setrange

image.png

  1. redis截取字符串

image.png

  1. 批量获取和批量赋值操作mget,mset

image.png

应用场景

  • 分布式锁
  • 计数功能
  • 全局的序列号:例如用在全局id自增

hash

  1. hset 写入数据

image.png

  1. hget 获取数据

image.png

  1. hdel 删除数据

image.png

  1. hexists key field 查字段是否存在

image.png

  1. HLEN 返回哈希表键 key 中域的数量

image.png

  1. hincrby key field increment 将key中的域field中存储的数字增加 increment

image.png

  1. hmget key field 返回哈希表键key中,一个或多个给定域的值

image.png

  1. hmset key field value,同时为哈希表键key设置一个或多个key-value 键值对

image.png

  1. hkeys key 返回哈希表键key中所有域

image.png

  1. hvals key 返回key所有域的值

image.png

应用场景

存储对象

购物车实现

list

  1. rpush key value 从队列右边入队

image.png

  1. lpush key value 从队列左边入队

image.png

  1. LPOP key 从队列左边出队

image.png

  1. RPOP key 从队列右边出队一个元素
  2. llen key 获取队列长度

image.png

  1. lrange key start stop 从列表中获取指定返回的元素

image.png

  1. lset key index value 设置队列里面一个元素的值

image.png

应用场景

Redis数据结构底层原理

String 底层原理 简单动态字符串(SDS)

底层原理

Redis 没有直接使用C语言传统的字符串表(以空字符结尾的字符数组,)而是自己构建了一个简单动态字符串(SDS)的抽象类型, 并将SDS用作Redis的默认字符串表示。

在Redis里面,C字符串只会作为字符串字面量用在一些无须对字符串值进行修改的地方,例如打印日志。

set msg “hello world”

  • 键值对的键是一个字符串对象,对象底层实现是一个保存着字符串“msg”的SDS。
  • 键值对的值也是一个字符串对象,对象的底层实现是一个保存着字符串“hello world” 的 SDS。

在list命令中

image.png

  • 键值对的键是一个字符串对象,对象底层实现是一个保存着字符串“mypush”的SDS。
  • 键值对的值是一个列表对象,列表对象包含了五个字符串对象,这三个字符串对象分别由五个SDS实现

SDS

SDS是什么呢,先来看张图

image.png

  • len 属性值是5,表示这个SDS保存了一个五个字节长度的字符串
  • free 属性的值为0,表示这个SDS没有分配任何未使用的空间
  • buf属性是个char类型的数组,数组前五个字节分别保存了’R’、‘e’、‘d’、‘i’、‘s’五个字符,而最后一个字节则保存了空字符‘\0’

SDS遵循C字符串以空字符结尾的惯例。保存的空字符的1字节空间不计算在sds的len属性里。

SDS与C字符串的区别

C字符串使用长度为N+1的字符数组表示长度为N的字符串。

image.png

  • C字符串不记录自身长度,所以当计算C字符串长度,就要遍历计算,复杂度为0(N),而SDS记录字符串长度,复杂度为O(1).
  • C字符串不记录自身长度会带来容易造成缓冲区溢出的情况,C字符串通过strcat函数将src字符串的内容拼接到dest字符串的末尾。

image.png

这里举个例子,假设程序又两个内存中紧邻的C字符串s1和s2,s1保存了“Redis”,s2保存了’MongoDB’,

image.png

如果决定执行下面这个命令

image.png

将s1内容修改为“Redis Cluster”,但可能在执行strcat前忘了为s1分配足够空间,那么在strcat函数执行后,s1的数据将溢出到s2的所在空间导致s2的内容被意外修改。

与C字符串不同,SDS完全杜绝了发送缓冲区溢出的可能性,当SAS api 需要对SDS进行修改时,会检查SDS空间是否满足修改所需需求,不满足AP会自动扩展空间,修改所需大小之后再进行实际的修改。

  • 减少修改字符串带来的内存重分配次数

在C字符串中,C字符串的长度总是N+1个字符长的数组,因此每次增长或者缩短一个C字符串都需要对C字符串数组进行一个内存重分配操作,如果是频繁的修改这样会对性能造成影响

SDS会通过未使用空间解除字符串长度和底层数组之间的关联,buf数组的长度不一定极速字符数量加一,数组里会包含未使用字节,这些未使用字节由free属性记录,通过未使用空间,实现空间预分配和惰性空间释放两种优化策略。

空间预分配就是当字符串进行增长操作,除了会分配修改所需要的必要空间,还会为SDS分配额外的未使用空间,这样就可以减少连续执行增长操作所需的内存重分配次数。

惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序不会立即使用内存重分配来回收多出来的字节,而是使用free属性将这些字节数量记录起来,等待将来使用。

image.png

image.png

list的底层原理 链表

list 底层是用链表来存储数据,这个链表是一个双向链表

来看下内部结构

image.png

链表每个节点都是由listNode组成,前置节点,后置节点和节点值

整个链表的组成内部结构如下图

image.png

list结构提供了头节点,尾节点,以及链表长度len,而dup、free、match用于实现多态链表所需类型的函数

dup函数用于复制链表节点所保存的值

free函数用于释放链表节点所保存的值

match函数用于对比链表节点所保存的值和另一个输入值是否相等

链表特性总结

  • 双端:链表节点有prev何next指针,所以是双向链表
  • 无环:表头节点prev和表尾节点指针都指向null
  • 带表头指针和表尾指针
  • 带链表长度计数器
  • 多态:可以通过dup,free,match为节点值设置类型特定函数

image.png

字典表

字典是一种用于保存键值对的抽象数据结构

在字典中,一个键(key)可以和一个值进行关联,这些关联的键和值就称为键值对。

字典中每个键都是独一无二的,程序可以在字典中根据键查找与之关联的值,或者通过键来更新值删除值等。

Redis自己构建了字典的实现,字典在redis的应用很广泛,例如Redis的数据库就是使用字典来作为底层实现。

例如 image.png
键是msg,value是 hello,这个键值对就是保存在代表数据库的字典里面

字典还是哈希键的底层实现之一,当哈希键包含的键值对比较多,又或者键值对中元素都是比较长的字符串,Redis会使用字典作为哈希键的底层实现。

例如 website是包含10086个键值对的哈希键,这个websiet键的底层实现就是一个字典,字典中包含了10086个键值对。

字典底层实现原理

Redis的字典使用哈希表作为底层实现,一个哈希表可以有多个哈希表节点,每个哈希表节点保存了字典中的一个键值对

哈希表

image.png

  • table属性是一个数组,数组中的每个元素都是一个指向dict.h/dictEntry结构的指针,每个dictEntry结构保存着一个键值对
  • size属性记录了哈希表的大小,也就是table数组的大小
  • used属性记录了哈希表目前已有节点(键值对)的数量。
  • sizemask属性的值总是等于size-1,这个属性和哈希值一起决定一个键应该被放到table数组的哪个索引上面。

image.png

哈希表节点

哈希表节点使用dictEntry结构表示,每个dictEntry结构都保存着一个键值对

image.png

key属性保存着键值对中的键,val属性保存着键值对中的值。

next属性指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接在一起,用来解决键哈希冲突的问题。

image.png

字典

image.png

  • type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置。

type属性是一个指向dicttype结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数,而privdata属性则保存了需要传给那些类型特定函数的可选参数。

image.png

  • ht属性是一个包含两个项的数组,数组中每个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。
  • rehashidx,它记录了rehash目前的进度,如果目前没有在进行rehash,那么它的值为-1.

下图展示了一个普通的字典完整的结构

image.png

哈希算法

当要将一个新的键值对添加到字典中时,程序需要先根据键值对的中的键计算出哈希值和索引值,然后在根据索引值将包含新键值对的哈希表放到哈希表数组的指定索引上面。

计算哈希值和索引值方法如下

image.png

例如要将一个键值对k0和v0添加到字典里面,那么程序会先使用语句:

hash=dict->type->hashFunction(k0);

计算出hash值后,接着计算索引值

index=hash&dict->ht[0].sizemask=8&3=0;

计算出键k0的索引值是0,那么键值对k0和v0的节点就会被放置在哈希数组的索引0的位置上

image.png

解决键冲突

当有两个或两个以上数量的键被分配到哈希表数组的同一个索引上面时,我们称这些键发生了冲突。

Redis哈希表使用链地址法来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上多个节点就可以用这个单向链表连接,从而解决键冲突问题。

当发生冲突时总是将新节点添加到链表表头位置,所以这里使用的是头插法。

image.png

总结

image.png

跳跃表

跳跃表是一种有序的数据结构,他通过在每个节点维持多个指向其他节点的指针,从而达到快速访问节点的目的,跳跃表支持平均O(logN),最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。

在Redis中,跳跃表只会用在两个地方,一个是作为有序集合的底层数据结构,一个是作为集群节点内部数据结构。

跳跃表的实现

Redis的跳跃表由redis.h/zskiplistNode和redis.h/zskiplist两个结构定义,其中zskiplistNode结构用于表示跳跃表节点,而zskiplist结构则用于保存跳跃表节点的相关信息,例如节点数量,以及指向表头节点和表尾节点的指针等。

跳跃表结构

image.png

image.png

图中展示了跳跃表zskiplist的结构

  • head:指向跳跃表的表头节点
  • tail:指向跳跃表的表尾节点
  • level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)
  • length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内)

跳跃表节点结构

image.png

image.png

  • 层

跳跃表节点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度,一般来说,层的数量越多,访问其他节点的速度就越快。程序会根据幂次定律,随机生成介于1到32之间的值作为level数组的大小,这个大小就是层的高度。

  • 前进指针

每个层都有一个指向表尾方向的前进指针,用于从表头节点向表尾方向访问节点。

image.png

图中虚线标书出程序从表头向表尾方向,遍历跳跃表中所有节点的路径

  • 跨度

层的跨度用于记录两个节点之间的距离。

  • 后退指针

节点的后退指针用于从表尾向表头方向访问节点,每个节点只有一个后退指针,所以不能向前进指针一样可以跳过多个节点,后退节点每次只能后退至前一个节点。

  • 分值和成员

节点的分值是一个double类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序。

节点的成员对象obj 是一个指针,指向一个字符串对象,而字符串对象保存着一个SDS值。

在同一个跳跃表内,各个节点保存的成员对象必须是唯一的,但多个节点保存的分值可以是相同的。

分值相同的节点将按照成员对象大小来来进行排序

例如

image.png

这里面的三个跳跃表节点分值相同,因为o1成员对象最小,o2次之,o3最大,所以他们的排序规则是o1<=o2<=o3

总结

  • 跳跃表是有序集合的底层实现之一。
  • Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息,zskiplistNode用于表示跳跃表节点。
  • 每个跳跃表节点的层都是1至32之间的随机数
  • 在同一个跳跃表中,多个节点key包含相同的分值,但每个节点成员对象是唯一的。
  • 跳跃表的结点按照分值大小进行排序,分值相同时,按照成员对象大小来进行排序。

整数集合

整数集合(intset)是集合键底层实现之一,当一个集合只包含整数值元素,并且结合元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。

举个简单的例子

image.png

这里向集合中存储了6个数字,查了下采用底层结构,就是整数集合。

整数集合的实现

整数集合可以保存int16_t、int32_t或者int_64_t的整数值,这是啥意思呢,就是整数集合可以保存16位,32位,64位的整数,并且会保证集合不会出现重复元素。

image.png

  • contents数组保存的是整数集合里的具体元素值,并且是按照值的袋熊有序排列,并且不包含重复项。
  • length: 记录整数集合包含的元素数量,也就是contents数组的长度。
  • encoding:encoding包含了编码方式,实际上也是决定contents数组里元素的类型,如果encoding属性的值是int16,那么contents就是一个int16_t类型的数组,数组里每个项都是一个int16_t类型的整数值,

encoding如果是int32,那么contents就是int32_t的数组。

升级

如果当前contents数组里的元素都是int16_t类型,这时进来一个int32_t类型的元素,那么整个集合的所有元素都会被转换成int64_t,具体是如何升级的,接着往下看。

升级整数集合大致分为三步

  • 根据新元素类型,扩展整数集合底层数组空间大小,并为新元素分配空间
  • 将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位置上,放置时同时也要保住有序性不变。
  • 将新元素添加到底层数组里。

因为每次向整数集合添加新元素都有可能会引起升级,所以添加新元素的时间复杂度为O(N)。

升级可以提升整数集合的灵活性,另一个是尽可能的节约内存。

降级

整集合不支持降级操作,一旦升级,就会保持升级的状态。

总结

  • 整数集合是集合键的实现之一
  • 整数集合的底层实现为数组
  • 升级操作为整数集合带来了操作的灵活性,并节约了内存
  • 整数集合只支持升级操作,不支持降级操作

压缩列表

压缩列表是列表键和哈希键的底层实现之一。当一个列表键只包含少量的列表项,并且每个列表项要么是小整数值,要么是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。而当一个哈希键只包含少量键值对,并且每个键值对和值要么是小整数值,要么是长度比较短的字符串,Redis也会使用压缩列表来作为哈希键的底层实现。

这里举例说明,来验证上述的情况

image.png

image.png

压缩列表构成

压缩列表是由一系列特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值。

image.png

  • zlbytes:记录整个压缩列表占用的字节数
  • zltail: 记录压缩列表尾节点距离压缩列表的起始地址有多少字节。
  • zllen:记录了压缩列表包含的节点数量
  • entryX:压缩列表包含的各个节点,节点的长度由节点保存的内容决定
  • zlend:用于标记压缩列表的末端

压缩列表节点的构成

每个压缩列表节点可以保存一个字节数组或者一个整数值

字节数组可以是三种长度的其中一种:

长度小于等于63字节的字节数组

长度小于等于2的14次方-1的字节数组

长度小于等于2的32次方减1字节的字节数组

整数值则是6种长度的其中一种:

4位长的整数

1字节长

3字节长

int16_t类型

int_32_t类型

int_64_t类型

压缩列表节点构成是由三部分组成

image.png

  • previous_entry_length

记录了压缩列表前一个节点的长度hang’d性长度可以是1字节或者5字节

如果前一节点长度小于254字节,那么previous_entry_length长度为1字节

如果前一节点长度大于等于254字节,那么属性长度为5字节

  • encoding
  • 节点encoding记录了节点的content属性所保存数据的类型及长度

image.png

  • content

content负责保存节点的值,节点值可以是字节数组或者整数

image.png

连锁更新

如果在一个压缩列表,有多个连续的、长度介于250字节到253字节之间的节点e1至eN

image.png

这些节点长度都小于254字节,所以记录这些节点的长度只需要1字节长的previous_entry_length属性

这时,如果我们将一个长度大于等于254字节的新节点new设置为压缩列表的表头节点,那么new将成为e1的前置节点。

image.png

因为e1的previous_entry_length 属性仅长1字节,它没办法保存新节点new的长度,所以程序将压缩列表执行空间重分配操作,并将e1节点的previous_entry_length属性从原来1字节变为5字节,但是当变为5字节,e1长度就超过了254,那么e2的provious_entry_lengt就要从原来1字节变为5字节,这样就会往后影响到e3,e4以及之后的节点,造成连锁更新,同样的删除节点也会造成连锁更新

总结

image.png

image.png

本文转载自: 掘金

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

(五)Nacos开发教程之SpringCloud集成配置管理

发表于 2021-11-18

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

前情回顾

我们在上一篇文章中讲述了SpringCloud项目集成Nacos服务注册功能,学习微服务的路上,已经迈出了第一步。

前面的文章里,我们暂时只讲述这些组件的基本集成,但是不要被我误导,这些只是基本使用,不能和实战相提并论,所以要想在真实的项目中使用这些组件,需要考虑的东西还会有很多。

这些我们之后有机会也会讲到的。

SpringCloud集成Nacos配置管理功能

微服务中的配置管理功能,SpringCloud中提供了SpringCloud Config组件来实现的,而SpringCloud Alibaba系列中则是通过Nacos来实现的。

这样的话,Nacos除了服务治理功能,还实现了配置管理功能,功能的强大性就不说了,随着Nacos2的发布,各项功能的集成也已经实现,这里如果大家有所兴趣,可以去官网上看一下版本更新日志。

在上篇文章的旧项目中进行集成

让我们把上篇文章中的项目复制一份出来,使用IDEA来打开它。

引入Nacos配置管理相关的jar包

1
2
3
4
5
6
7
8
xml复制代码<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

配置Nacos配置管理的相关参数

修改application.yml文件

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码spring:
application:
name: demo-config
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
config:
server-addr: 127.0.0.1:8848
file-extension: yaml
group: DEFAULT_GROUP
prefix: ${spring.application.name}

增加config配置,

server-addr: nacos服务器地址

file-extension: 指定文件类型

group:配置分组

prefix:文件名称前缀,如果你配置了不同环境的话,这里只需要配置前缀,Nacos会根据你的环境不同加入不同的后缀。

在Nacos页面中进行配置的添加

我们打开nacos服务端页面,进入如下图菜单。

image.png

我们通常在此功能中添加相应的配置.

image.png

我这里输入了一些参数,当我们运行项目时,会先加载我们本地的配置,随后加载这里的配置,如果相同的话,会被覆盖掉。

格外知识点

这里需要注意的是,我们平时使用的都是application配置文件,SpringCloud中有多了一个配置文件,那就是bootstrap配置文件。

这里需要提一下的是,bootstrap配置文件要优先于application配置文件执行,所以我们平时开发过程中,会在bootstrap文件中进行一些基础配置;其余的配置用于application配置文件来维护,或者是使用nacos配置管理中心来管理一些配置信息。

总结

Nacos配置中心目前已经成熟了,这里面我们还有一些点没有说到,那就是配置的灰度发布等功能,这个之后我们慢慢学习。

本文转载自: 掘金

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

不会深度学习怎么写人脸识别 一、前言 二、准备工作 三、实现

发表于 2021-11-18

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

一、前言

今天给大家带了的人脸识别非常简单,不需要大家了解TensorFlow,只需要对Python基本语法有一定了解。由于TensorFlow的火爆,把人脸识别再度推向我们的视线。像前段时间比较火的dee pfake,和人脸支付技术。虽然现阶段人脸识别仍有很大的争议性,但是它已经走进我们的视线当中了。很多小区在门禁系统中加入了人脸识别的功能,有些景区也添加了刷脸通道。但是对于技术的争议不是今天探讨的课题。下面开始我们的准备工作。

二、准备工作

今天是通过使用百度的SDK来实现的,首先我们要进入AI开放平台。

AI开放平台界面

我们选择开放能力->人脸与人体识别->人脸识别,进入后或出现下面page1的界面,点击立即使用。

点击后出现page2的界面。登录自己的账号就可以了。进去后我们会看到如下page3界面。如果没有应用就创建应用:

如果应用的话就管理应用。然后我们看到page4中几个重要的参数。分别是AppID、API Key、Secret Key。到这里我们的准备工作就完成了。初步准备工作完成了,我们还需要做一件事,就是下载人脸识别的SDK。我们可以点击page4中的服务端SDK下载Python对应的SDK,也可以使用pip下载:

1
python复制代码pip install baidu-aip

接下来我们就可以着手代码的编写了。

三、实现人脸识别

使用SDK实现人脸识别非常简单,下面我会从最简单的人脸识别开始。然后识别多个人脸,再到情绪识别、年龄识别等。

3.1、人脸识别

人脸识别非常简单,首先需要我们上面申请到的3个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
python复制代码# 导入需要用的模块
from aip import AipFace

# 准备AppID、API Key、Secret Key
APP_ID = '18101974'
API_KEY = 'HG5NFSFpfBiqqmBYVBpXQFKs'
SECRET_KEY = 'ytdOFrcBxZvvGlMKGtmkmpBQ6arbjupx'

# 调用人脸检测
client = AipFace(APP_ID, API_KEY, SECRET_KEY)

# 图片的类型,三种可选BASE64、URL、FACE_TOKEN
image_type = 'URL'
# 图片的url
iamge = 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1577292322895&di=f7fa123b95016762f1f0c3d2999328c4&imgtype=0&src=http%3A%2F%2Fww1.sinaimg.cn%2Flarge%2Fc7b64b24jw1f7k7i3aap4j20qo1407ck.jpg'

# 识别人脸
face_data = client.detect(iamge, image_type)

在识别人脸时,我们调用了client.detect()方法,在上面的例子中,接收了两个参数。分别是image和image_type。参数详情如下:

参数名称 参数类型 参数作用
image string 图片信息(总数据大小应小于10M),图片上传方式根据image_type来判断
image_type string 图片类型 BASE64:图片的base64值,base64编码后的图片数据,编码后的图片大小不超过2M;URL:图片的 URL地址( 可能由于网络等原因导致下载图片时间过长);FACE_TOKEN: 人脸图片的唯一标识,调用人脸检测接口时,会为每个人脸图片赋予一个唯一的FACE_TOKEN,同一张图片多次检测得到的FACE_TOKEN是同一个

可以看到,image_type可以为三种值,这里我们选用url的方式。

其返回的为json数据,具体数据就不给大家展示了,其结构如下和image链接的图片如下:

其中,左边为image链接的图片。虽然很漂亮,但是这不是我们研究的重点,我们把视线转移到右边这段冷冰冰的文字,我选取几个重要参数和大家说一下:

参数名称 参数类型 参数作用
face_list array 人脸信息列表
+face_probability double 人脸置信度,范围【0~1】,代表这是一张人脸的概率,0最小、1最大。
+location array 人脸的位置
++left double 人脸区域离左边界的距离
++top double 人脸区域离上边界的距离
++width double 人脸区域的宽度
++height double 人脸区域的高度
++rotation int64 人脸框相对于竖直方向的顺时针旋转角,[-180,180]
+face_num int 人脸的个数

从上面的数据中,我们可以获取人脸的位置。我们可以将检测到的人脸裁剪下来,这就需要用的Pillow模块了。其安装如下:

1
python复制代码pip install pillow

Pillow模块的一些简单操作可以看看这篇博客,因为我们只使用截图功能所以还是非常简单的。我们在上面的基础上加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
python复制代码# 从Pillow中导入Image模块
from PIL import Image
import urllib

# 将image保存到本地文件
resp = urllib.request.urlopen(image_url)
f = open('im.jpg', 'wb')
f.write(resp.read())

# 使用Image模块,打开刚刚保存的图像
im = Image.open('im.jpg')

# 获取face_data中人脸的位置
left = face_data['result']['face_list'][0]['location']['left']
top = face_data['result']['face_list'][0]['location']['top']
width = face_data['result']['face_list'][0]['location']['width']
height = face_data['result']['face_list'][0]['location']['height']

# 截取人脸
face_im = im.crop((left, top, left+width, top+height))
face_im.save('face_im.jpg')

代码执行后截取的face_im.jpg如下:

上面这段代码有许多不严谨的地方,因为现在是简单的实现功能,所以没太在意。接下来我们实现更复杂的人脸识别,虽然在技术上要复杂的多,但是在代码上却没有什么太多的添加。

3.2、多人脸识别

也非常简单,我们执行上面代码,发现即使我们识别多人脸的图片,face_num依旧是1。这并非是识别失败,而是由一个参数决定的,即 max_face_num。其类型为string,默认值为1,所以我们只能识别一个人脸,我们将 识别人脸代码修改如下:

1
2
3
4
5
6
7
8
9
10
11
python复制代码# 类型依旧是URL
image_type = 'URL'
# 一张包含多个人脸的image
image = 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1577296093031&di=f88c884ea4440935055c704fd20c0549&imgtype=0&src=http%3A%2F%2F5b0988e595225.cdn.sohucs.com%2Fimages%2F20180209%2F3a8e9aca0b8c481298246a26451bf242.jpeg'

# 准备参数
options = {}
options['max_face_num'] = '4'

# 识别人脸,比之前多了个options,options为字典类型
face_data = client.detect(image, image_type, options)

我们修改后,face_data将最多返回四个人脸数据。所以我们需要多face_list进行遍历:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python复制代码# 将url中的图片保存到本地
resp = urllib.request.urlopen(image_url)
f = open('im.jpg', 'wb')
f.write(resp.read())

# 打开图片
im = Image.open('im.jpg')
# 用于区分图片
n = 0
# 遍历face_list
for i in face_data['result']['face_list']:
n += 1
left = i['location']['left']
top = i['location']['top']
width = i['location']['width']
height = i['location']['height']

# 截图并保存
face_im = im.crop((left, top, left + width, top + height))
face_im.save('face_im' + str(n) + '.jpg')

执行上面代码后我们截取到如下几个人脸(左边为原图,右边为人脸):

因为设置max_face_num为4,所以检测到了四张人脸。

3.3、情绪识别

识别情绪也非常简单,我们在传入参数时再添加如下参数:

1
2
3
python复制代码# 准备参数
options = {}
options['face_field'] = 'expression'

在添加该参数后,返回数据中,face_list下多了参数 expression 。具体结构如下:

在这里插入图片描述

我们识别其中蓝色的即为识别出来的情绪。

除此之外,我们还可以识别性别、年龄、颜值、是否带眼睛、单双眼皮、情绪等…更详细的内容可以参考其官方文档Python-SDK

四、总结

除了上面这些简单操作外,SDK还提供了更加复杂的人脸服务。像人脸注册、人脸更新、身份验证等功能,这种面相企业的应用,个人认为还是不要过度依赖。无止境的广告短信,莫名其妙的推销电话,这些都是信息泄露的后果。在大数据时代,人类没有隐私可言。即使是对于软件行业的人来说,很多东西依旧是无可奈可。所以我们需要多加注意,对自己的隐私多留一份心。

本文转载自: 掘金

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

力扣第九十九题-恢复二叉搜索树 前言 一、思路 二、实现 三

发表于 2021-11-18

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

前言

力扣第九十九题 恢复二叉搜索树 如下所示:

给你二叉搜索树的根节点 root ,该树中的两个节点被错误地交换。请在不改变其结构的情况下,恢复这棵树。

进阶: 使用 O(n) 空间复杂度的解法很容易实现。你能想出一个只使用常数空间的解决方案吗?

示例 1:

1
2
3
csharp复制代码输入: root = [1,3,null,null,2]
输出: [3,1,null,null,2]
解释: 3 不能是 1 左孩子,因为 3 > 1 。交换 1 和 3 使二叉搜索树有效。

一、思路

题目中有一个重要的信息:树中有两个节点被错误的交换。那么我们只需要找到这两个节点,再将他两交换,就能获得一个正确的二叉搜索树。

那怎么样才能找到这两个错误交换的节点呢?

我们知道有效 二叉搜索树定义如下:

  • 节点的左子树只包含 小于 当前节点的数。
  • 节点的右子树只包含 大于 当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。

我们发现 左子树的值是小于当前节点的值 且 右子树的值是大于当前节点的值。我们可以使用 中序遍历(左根右) 来遍历这个树,我们可以得到一个数组。我们再在这个数组中找到需要交换的两个节点,最后再将这两个节点的值交换即可。

举个例子

我们以下图的树 [3, 1, 4, null, null, 2]作为例子:

image.png

中序遍历后,得到的数组为 [1, 3, 2, 4]

数组中交换的位置为:第一次下降的位置和第一次上升的位置。(如果将递增数组看作一个斜向上的直线, 只有交换凸起处和下凹处的值,才能重新得到一条斜向上的直线)

很明显我们应该交换 2 和 3 的位置,交换后数组为 [1, 2, 3, 4],树如下图所示:

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
java复制代码    TreeNode x=null, y=null; // 下标

public void recoverTree(TreeNode root) {
dfs(root, new LinkedList<>());
// 交换x,y的值(两种情况)
int temp = x.val;
x.val = y.val;
y.val = temp;
}

public void dfs(TreeNode node, Deque<TreeNode> stack) {
if (node == null) {
return;
}
dfs(node.left, stack);
if (!stack.isEmpty() && node.val < stack.peek().val) {
if (x == null) {
x = stack.peek();
}
y = node;
}
stack.push(node);
dfs(node.right, stack);
}

测试代码

1
2
3
4
java复制代码    public static void main(String[] args) {
TreeNode treeNode = new TreeNode(1, new TreeNode(3, null, new TreeNode(2)), null);
new Number99().recoverTree(treeNode);
}

结果

image.png

三、总结

感谢看到最后,非常荣幸能够帮助到你~♥

如果你觉得我写的还不错的话,不妨给我点个赞吧!如有疑问,也可评论区见~

本文转载自: 掘金

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

用Nodejs爬取图片的踩坑日志

发表于 2021-11-18

最近写了个node小程序来爬取网页上的图片,不知道是不是我要抓取的内容是动态生成的,地址后缀是以.do结尾而不是以.png/.jpg结尾的原因,以为是个小case,没想到踩坑了!!!

在百度和bing上找了好久,都没找出来原因所在,后来换了种思路才解决,专门记下来给大家避雷!!!

以爬取png图片为例子,我原本的思路是axios.get请求图片地址,然后把返回内容保存下来。

大致如下:

1
2
3
4
5
6
7
8
javascript复制代码const fs = require('fs');
axios.get(img_url, { responseType: Buffer })
.then(async function (response) {
let d = response.data;
fs.writeFileSync(`${fileName}.png`, d);
}).catch(e => {
console.error(e);
});

万万没想到,就这么短短几行的代码,踩到了天坑,图片显示不出来!

然后我先后把responseType改成了ArrayBuffer和String还是不行,然后在想,会是文件编码的问题吗?

把writeFileSync的encoding乱改一通,从binary、ascii试到utf16le都不行。

尼玛

然后开始上网找,先后安装了据说能够识别Buffer类型的FileType,识别我axios取回来的buffer显示undefined;安装了据说能够处理图片的shark,显示格式出错;安装了据说处理png图片很专业的pngjs,这个起码告诉我Error: Invalid file signature

image.png

用hexEditor可以看到编码(左:正常图,右:程序抓取)的确不一样。

行吧…然后试了n多方法,终于找到一种能行的!不用Buffer,直接用Stream!

1
2
3
4
5
javascript复制代码        const writer = fs.createWriteStream(img_save_path)
let response = await axios.get(url, {
responseType:"stream"
})
response.data.pipe(writer)

然后就可以了。

不知道为毛。

本文转载自: 掘金

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

Java API操作kafka以及kafka原理

发表于 2021-11-18

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

Java访问kafka准备

image.png

修改本机的Host文件

1
2
3
复制代码192.168.200.20 kafka1
192.168.200.20 kafka2
192.168.200.20 kafka3
  • 创建maven的工程, 导入kafka相关的依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xml复制代码<dependencies>
<!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka-clients -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>

<build>
<plugins>
<!-- java编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>

生产者代码

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

public static String topic = "test";//定义主题

public static void main(String[] args) throws Exception {
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.200.20:9092,192.168.200.20:9093,192.168.200.20:9094");
//网络传输,对key和value进行序列化
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
//创建消息生产对象,需要从properties对象或者从properties文件中加载信息
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);

try {
while (true) {
//设置消息内容
String msg = "Hello," + new Random().nextInt(100);
//将消息内容封装到ProducerRecord中
ProducerRecord<String, String> record = new ProducerRecord<String, String>(topic, msg);
kafkaProducer.send(record);
System.out.println("消息发送成功:" + msg);
Thread.sleep(500);
}
} finally {
kafkaProducer.close();
}
}
}

消费者代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class ConsumerDemo {

public static void main(String[] args){
Properties p = new Properties();
p.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.200.20:9092,192.168.200.20:9093,192.168.200.20:9094");
p.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
p.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
//指定组名
p.put(ConsumerConfig.GROUP_ID_CONFIG, "test");

KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<String, String>(p);
kafkaConsumer.subscribe(Collections.singletonList(ProducerDemo.topic));// 订阅消息

while (true) {
ConsumerRecords<String, String> records = kafkaConsumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
System.out.println(String.format("topic:%s,offset:%d,消息:%s", record.topic(), record.offset(), record.value()));
}
}
}

}

分区副本机制

kafka有三层结构:kafka有多个主题,每个主题有多个分区,每个分区又有多条消息。

  • 分区机制:主要解决了单台服务器存储容量有限和单台服务器并发数限制的问题 一个分片的不同副本不能放到同一个broker上。当主题数据量非常大的时候,一个服务器存放不了,就将数据分成两个或者多个部分,存放在多台服务器上。每个服务器上的数据,叫做一个分片

分区对于Kafka集群的好处是:实现负载均衡,高存储能力、高伸缩性。分区对于消费者来说,可以提高并发度,提高效率。

  • 副本:副本备份机制解决了数据存储的高可用问题。当数据只保存一份的时候,有丢失的风险。为了更好的容错和容灾,将数据拷贝几份,保存到不同的机器上。多个follower副本通常存放在和leader副本不同的broker中。通过这样的机制实现了高可用,当某台机器挂掉后,其他follower副本也能迅速”转正“,开始对外提供服务。

kafka的副本都有哪些作用?

  • 在kafka中,实现副本的目的就是冗余备份,且仅仅是冗余备份,所有的读写请求都是由leader副本进行处理的。follower副本仅有一个功能,那就是从leader副本拉取消息,尽量让自己跟leader副本的内容一致。

说说follower副本为什么不对外提供服务?

  • 这个问题本质上是对性能和一致性的取舍。试想一下,如果follower副本也对外提供服务那会怎么样呢?首先,性能是肯定会有所提升的。但同时,会出现一系列问题。类似数据库事务中的幻读,脏读。
  • 比如你现在写入一条数据到kafka主题a,消费者b从主题a消费数据,却发现消费不到,因为消费者b去读取的那个分区副本中,最新消息还没写入。而这个时候,另一个消费者c却可以消费到最新那条数据,因为它消费了leader副本。
  • 为了提高那么些性能而导致出现数据不一致问题,那显然是不值得的。

kafka保证数据不丢失机制

从Kafka的大体角度上可以分为数据生产者,Kafka集群,还有就是消费者,而要保证数据的不丢失也要从这三个角度去考虑。

消息生产者

消息生产者保证数据不丢失:消息确认机制(ACK机制),参考值有三个:0,1,-1

1
2
3
4
5
6
7
8
9
10
11
java复制代码//producer无需等待来自broker的确认而继续发送下一批消息。
//这种情况下数据传输效率最高,但是数据可靠性确是最低的。
properties.put(ProducerConfig.ACKS_CONFIG,"0");

//producer只要收到一个分区副本成功写入的通知就认为推送消息成功了。
//这里有一个地方需要注意,这个副本必须是leader副本。
//只有leader副本成功写入了,producer才会认为消息发送成功。
properties.put(ProducerConfig.ACKS_CONFIG,"1");

//ack=-1,简单来说就是,producer只有收到分区内所有副本的成功写入的通知才认为推送消息成功了。
properties.put(ProducerConfig.ACKS_CONFIG,"-1");

消息消费者

kafka消费消息的模型:

image.png

即消费消息,设置好offffset,类比一下:

Kafka动作 看书动作
消费消息 看书
offffset位移 书签

什么时候消费者丢失数据呢?

  • 由于Kafka consumer默认是自动提交位移的(先更新位移,再消费消息),如果消费程序出现故障,没消费完毕,则丢失了消息,此时,broker并不知道。

解决方案:

  • enable.auto.commit=false关闭自动提交位移
  • 在消息被完整处理之后再手动提交位移
1
java复制代码properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"false");

消息存储及查询机制

kafka使用日志文件的方式来保存生产者消息,每条消息都有一个offffset值来表示它在分区中的偏移量。

Kafka中存储的一般都是海量的消息数据,为了避免日志文件过大,一个分片并不是直接对应在一个磁盘上的日志文件,而是对应磁盘上的一个目录,这个目录的命名规则是<topic_name>_<partition_id>。

kafka容器数据目录:

  • /kafka/kafka-logs-kafka1

image.png

消息存储机制

image.png

log分段

每个分片目录中,kafka 通过分段的方式将 数据分为多个 LogSegment。

一个 LogSegment 对应磁盘上的一个日志文件(00000000000000000000.log)和一个索引文件(如上:00000000000000000000.index)。

其中日志文件是用来记录消息的。索引文件是用来保存消息的索引。

每个LogSegment 的大小可以在server.properties 中log.segment.bytes=107370 (设置分段大小,默认是1gb)选项进行设置。

当log文件等于1G时,新的会写入到下一个segment中。

image.png

timeindex文件,是kafka的具体时间日志

通过 offffset 查找 message

存储的结构:一个主题 –> 多个分区 —-> 多个日志段(多个文件)

  • 第一步:查询segment fifile:

segment fifile命名规则跟offffset有关,根据segment fifile可以知道它的起始偏移量,因为Segment fifile的命名规则是上一个segment文件最后一条消息的offffset值。所以只要根据offffset 二分查找文件列表,就可以快速定位到具体文件。

比如

第一个segment fifile是00000000000000000000.index表示最开始的文件,起始偏移量(offffset)为0。

第二个是00000000000000091932.index:代表消息量起始偏移量为91933 = 91932 + 1。那么offffset=5000时应该定位00000000000000000000.index

  • 第二步通过segment fifile查找message:

通过第一步定位到segment fifile,当offffset=5000时,依次定位到00000000000000000000.index的元数据物理位置和00000000000000000000.log的物理偏移地址,然后再通过00000000000000000000.log顺序查找直到offffset=5000为止。

生产者消息分发策略

kafka在数据生产的时候,有一个数据分发策略。默认的情况使用DefaultPartitioner.class类。

这个类中就定义数据分发的策略。

1
2
3
4
5
6
7
8
9
10
java复制代码public interface Partitioner extends Configurable, Closeable {

public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);

/**
* This is called when partitioner is closed.
*/
public void close();

}

默认实现类:org.apache.kafka.clients.producer.internals.DefaultPartitioner

  • 如果是用户指定了partition,生产就不会调用DefaultPartitioner.partition()方法

数据分发策略的时候,可以指定数据发往哪个partition。

当ProducerRecord 的构造参数中有partition的时候,就可以发送到对应partition上

1
2
3
4
java复制代码
public ProducerRecord(String topic, Integer partition, K key, V value) {
this(topic, partition, null, key, value, null);
}
  • DefaultPartitioner源码

如果指定key,是取决于key的hash值

如果不指定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
java复制代码public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
//获取该topic的分区列表
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
//获得分区的个数
int numPartitions = partitions.size();
//如果key值为null
if (keyBytes == null) {//如果没有指定key,那么就是轮询
//维护一个key为topic的ConcurrentHashMap,并通过CAS操作的方式对value值执行递增+1操作
int nextValue = nextValue(topic);
//获取该topic的可用分区列表
List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
if (availablePartitions.size() > 0) {//如果可用分区大于0
//执行求余操作,保证消息落在可用分区上
int part = Utils.toPositive(nextValue) % availablePartitions.size();
return availablePartitions.get(part).partition();
} else {
// 没有可用分区的话,就给出一个不可用分区
return Utils.toPositive(nextValue) % numPartitions;
}
} else {//不过指定了key,key肯定就不为null
// 通过计算key的hash,确定消息分区
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
}

消费者负载均衡机制

同一个分区中的数据,只能被一个消费者组中的一个消费者所消费。例如 P0分区中的数据不能被Consumer Group A中C1与C2同时消费。

消费组:一个消费组中可以包含多个消费者,properties.put(ConsumerConfifig.GROUP_ID_CONFIG,”groupName”);如果该消费组有四个

消费者,主题有四个分区,那么每人一个。多个消费组可以重复消费消息。

image.png

  • 如果有3个Partition, p0/p1/p2,同一个消费组有3个消费者,c0/c1/c2,则为一一对应关系
  • 如果有3个Partition, p0/p1/p2,同一个消费组有2个消费者,c0/c1,则其中一个消费者消费2个分区的数据,另一个消费者消费一个分区的数据
  • 如果有2个Partition, p0/p1,同一个消费组有3个消费者,c0/c1/c3,则其中有一个消费者空闲,另外2个消费者消费分别各自消费一个分区的数据

本文转载自: 掘金

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

前味菜:先来了解一下 Redis 的主从架构

发表于 2021-11-18

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

当我在写一上来就主从、集群、哨兵,这谁受得了的时候,好多小伙伴就迫不及待的留言想看这些模式了,今天我们就从配置文件、设计原理、面试真题三个方面来聊一聊 Redis 的主从复制。

在 Redis 复制的基础上,使用和配置主从复制非常简单,能使得从 Redis 服务器(下文称 replica)能精确的复制主 Redis 服务器(下文称 master)的内容。每次当 replica 和 master 之间的连接断开时, replica 会自动重连到 master 上,并且无论这期间 master 发生了什么, replica 都将尝试让自身成为 master 的精确副本。

主从复制,从 5.0.0 版本开始,Redis 正式将 SLAVEOF 命令改名成了 REPLICAOF 命令并逐渐废弃原来的 SLAVEOF 命令

Redis使用默认的异步复制,其特点是低延迟和高性能,是绝大多数 Redis 用例的自然复制模式。但是,replica 会异步地确认它从主 master 周期接收到的数据量。

主从拓扑架构

master 用来写操作,replicas 用来读取数据,适用于读多写少的场景。而对于写并发量较高的场景,多个从节点会导致主节点写命令的多次发送从而过度消耗网络带宽,同时也加重了 master 的负载影响服务稳定性。

replica 可以接受其它 replica 的连接。除了多个 replica 可以连接到同一个 master 之外, replica 之间也可以像层叠状的结构(cascading-like structure)连接到其他 replica 。自 Redis 4.0 起,所有的 sub-replica 将会从 master 收到完全一样的复制流。

当 master 需要多个 replica 时,为了避免对 master 的性能干扰,可以采用树状主从结构降低主节点的压力。

为了让大家对概念有更清晰的认识,下文我们将细致全面的看一下配置文件中主从复制的参数。如果你有不同的意见或者更好的idea,欢迎联系阿Q,添加阿Q可以加入技术交流群参与讨论呦!

本文转载自: 掘金

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

Spring 自动装配【11】 import - 解析 前

发表于 2021-11-18

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

前言

我们知道:

  • SpringBoot简化了Spring的配置,有了SpringBoot之后我们就不用写一大堆东西来引入中间件了,只需要我们在启动类上加上:
1
java复制代码@SpringBootApplication

并加上对应的中间件的注解,比如此时我们需要引入一个Eureka:

1
css复制代码@EnableEurekaClient

这样我们就能把对应的中间件引入到应用中了(当然,其他的什么地址之类的东西还是要配置的)。

那么这个功能是如何实现的呢?

@SpringBootApplication

首先,我们来看看这个注解:

1
2
3
4
5
6
7
8
9
java复制代码@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

自动配置自动配置,这里有个注解和我们想看到的关系很大:

@EnableAutoConfiguration

这个注解如下:

1
2
3
4
5
6
java复制代码@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)

这里两个auto,我们先看看这个:

1
2
3
4
5
6
java复制代码@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {

到这里出现了两个import,这是干啥用的?看起来应该是很重要的东西。

那么来看看这个Import注解:

1
2
3
4
java复制代码@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {

注解上写的是:标识一个或多个component类需要被引入,例如:@Configuration标识的类。

@Configuration注解可太熟了,注册数据源、redis之类的东西,都会写这么个玩意。

打开idea看看用到@Import注解的地方,可以发现其实大部分的enableXXX的注解,都是通过这玩意实现的,例如:

1
2
3
4
5
java复制代码@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {

那么此时就有问题了:

  • 这个@Import是如何工作的?
  • @Import中的类,又该如何编码呢?

@Import

继续通过idea,来看看@Import是在什么地方被处理的.

找了好久终于找到了两个类对于这个注解有处理:

  • ConfigurationClassParser
  • ConfigurationClassUtils

我们一个个看。

ConfigurationClassParser

收集import信息

我们是从这段代码中看到Import注解被处理的:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited)
throws IOException {

if (visited.add(sourceClass)) {
for (SourceClass annotation : sourceClass.getAnnotations()) {
String annName = annotation.getMetadata().getClassName();
if (!annName.equals(Import.class.getName())) {
collectImports(annotation, imports, visited);
}
}
imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
}
}

方法上的注解写得很明白:

  • 递归地将Import的值收集起来。
  • 至于为什么是递归,下面也给了个例子:
1
2
3
java复制代码For example, it is common for a @Configuration class to declare direct
@Import s in addition to meta-imports originating from an @Enable
annotation.

​ 意思大概就是**@Configuration经常用一个@Import注解,来引入一些源于@Enable**注解的元导入。

​ 也就是说一层是不够解析出要import的所有对象的,写代码的经常除了@Import之外给你用一些@EnableXXX的玩意,这些玩意在这里也不好直接解析(万一enable里再enable的情况咋整),所以这里就直接给开个递归解决这种问题。

这个方法粗浅一点看也大概知道是个什么意思:将sourceClass里包括的(以及可能存在的更多层次的)@Import里标识的那些类,给你塞到这个imports里。

这里的sourceClass不必深究,我们此时看到的代码层次只要知道是对于class对象的一个封装即可。【1】

上面方法的链路

上面的方法我们也看到了:

  • 这其实就是个收集的方法

那么这里离我们要看到的如何注入的东西还是相差甚远的,此时我们在这个子问题中需要解决的是:

  • 哪里调用来收集?
  • 哪里处理已收集的信息?

我们先来看看这个方法的调用链:

doProcessConfigurationClassgetImportscollectImportsprocessImports
getImport就是个safe的代理方法,本身没什么就是保证两个list不是null。

上面的关系是从这里得出的:

1
2
java复制代码// Process any @Import annotations
processImports(configClass, sourceClass, getImports(sourceClass), filter, true);

看样子,processImports就是处理这些import注解的方法了。

import信息的处理

这里一共五十几行代码,为了方便对应啥意思就写代码上了,可能会出现阅读困难的地方做了标记,下面会解释:

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
java复制代码private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter,
boolean checkForCircularImports) {

//如果你要import的东西都没有,那我就不处理了
if (importCandidates.isEmpty()) {
return;
}

//这里就是判断是否有循环import的说法了
//我们知道Spring会给你处理循环的bean,但是这里如果出现循环就直接报错了
if (checkForCircularImports && isChainedImportOnStack(configClass)) {
this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
}

//其实这里都不用写else,上面的卫语句已经处理掉了
else {
//上面判断是不是有循环的stack,在这里做更新
this.importStack.push(configClass);
try {
for (SourceClass candidate : importCandidates) {
//如果我们import的类是个ImportSelector:实例化这个selector【A1】
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
Predicate<String> selectorFilter = selector.getExclusionFilter();
//这里就是加上了selector中指定的判断了
if (selectorFilter != null) {
exclusionFilter = exclusionFilter.or(selectorFilter);
}
//看名字也知道:延迟importSelector,把这个行为往后延迟,等到后面再处理【A2】
if (selector instanceof DeferredImportSelector) {
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
//这里对应的是:是importSelector,但不是延迟的,那么就直接处理这个importSelector了呗
else {
//调用selectIimports方法,获取需要import的类名称
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
//封装
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);
//递归了,原因跟上面的collectImports类似
processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);
}
}
//这里的else对应的是:sourceClass不是ImportSelector,而是:ImportBeanDefinitionRegistrar【A3】
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// Candidate class is an ImportBeanDefinitionRegistrar ->
// delegate to it to register additional bean definitions
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar =
ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
this.environment, this.resourceLoader, this.registry);
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
//这里看注释就知道是来处理@Configuration的【2】
else {
// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
// process it as an @Configuration class
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
}
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configClass.getMetadata().getClassName() + "]", ex);
}
finally {
this.importStack.pop();
}
}
}
  • 【A1】:ImportSelector

还记不记得之前提到的EnableAutoConfiguration?这里import的就是个ImportSelector:

@Import(AutoConfigurationImportSelector.class)

这个接口提供了两个方法:

1
2
3
4
5
6
7
8
9
> javascript复制代码//根据元数据,返回需要导入的具体的class名称
> String[] selectImports(AnnotationMetadata importingClassMetadata);
>
> //返回一个判断函数,这个函数如果返回的是true,代表输入的String不会被作为一个配置类处理
> default Predicate<String> getExclusionFilter() {
> return null;
> }
>
>
  • 【A2】:DeferredImportSelector

从类名可以看出来:延迟的importSelector,当然是继承ImportSelector的。

这个接口额外提供了一个方法:

1
2
3
> scss复制代码Class<? extends Group> getImportGroup()
>
>

这个ImportGroup,是这个接口中定义的一个子类,用于收集不同importSelector中的结果的。

上面说的AutoConfigurationImportSelector实际上实现的是这个接口。

而这里的handle其实做的事情就是把这个延迟的给存起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> java复制代码public void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) {
> DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(configClass, importSelector);
> if (this.deferredImportSelectors == null) {
> DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
> handler.register(holder);
> //这里会处理一下存起来的
> handler.processGroupImports();
> }
> else {
> this.deferredImportSelectors.add(holder);
> }
> }
>
>
  • 【A3】:ImportBeanDefinitionRegistrar

看类名,这个就和BeanDefinition相关了,该接口上的注释也是这么说的:

1
2
3
4
5
> > java复制代码Interface to be implemented by types that register additional bean definitions when
> > processing @{@link Configuration} classes. Useful when operating at the bean definition
> > level (as opposed to {@code @Bean} method/instance level) is desired or necessary.
> >
> >

大概就是说这个类,是用来注册一些额外的beanDefinition的,这部分我们放到后面再来看看。【3】

小结

那么,上面的代码做了什么事情?

  • 通过预先取出来import里的SourceClass来做处理。
  • 如何处理(下面说的是sourceClass对应的类)?
+ 如果是**importSelector**:
    - 如果不是:递归往下处理
    - 如果是延迟(DeferedImportSelector):
        * 那么先存起来
+ 如果不是,分为**ImportBeanDefinitionRegistrar**和**其他的情况**,分别处理【2】【3】这里做一下分析:


+ 因为处理必须是**有目的**的:要么改变某些东西,要么保存某些东西。
+ 而这里如果是ImportSelector,且不是Defered,那么其实是**递归处理的**,**没有做额外的保存**等工作
+ 因此,因为这里的处理有目的性,可见**ImportSelector**中的**selectImport**方法,返回的**最终结果**(多次递归之后最终不需要再往下递归的情况),**在这里**必须是**DeferImportSelector**或者是**ImportBeanDefinitionRegistrar**,或者是其他的东西(多是@Configuration,反正是可以按照@Configuration处理的),而不能只是ImportSelector。因此这里回过头来看【A1】,是否对于**selectImports**方法有了更深一点的理解?

小结

这里我们算是知道了:

  • @import注解是在哪个方法中做的处理。

但就现在来看,我们只知道这个注解可能:

  • 有多层次的情况
  • 有循环import的可能

以及一些具体处理的细节。

但是具体处理的落地,在挖的坑【2】和【3】中。

这就意味着此时我们并没有看完整个过程的代码,只是流程中的一小部分,此时还涉及到:

  • 这个方法在哪里被调用? - 这个事情很关键,我们需要知道@Import是如何工作的。
  • 下面的坑要填 - 【2】【3】具体如何落地?
  • 延迟的ImportSelector是如何工作的?

还有【1】,这个SourceClass是什么东西?

本文转载自: 掘金

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

Java项目 校园超市管理系统(java+SSM+Mysql

发表于 2021-11-18

前端模板框架为Bootstrap,系统分为前台和后台。后台主要为管理员角色,功能有:

商品类型管理、商品管理、订单管理、会员管理、管理员管理等。前台用户功能有:登录、注册、查看商品、加入购物车、付款、查看订单、个人中心等。该系统总共9张表

运行环境:windows/linux、jdk1.8、mysql5.x、maven3.5\3.6、tomcat7.0

image-20211118212501232

image-20211118212521609

image-20211118212535704

image-20211118212547097

image-20211118212602292

image-20211118212616224

前端商品控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
typescript复制代码/**
* <p>
* 前端控制器
* </p>
*/
@RestController
@RequestMapping("/goods")
public class GoodsController {


   @Autowired
   private GoodsService goodsService;

   @Autowired
   private ProviderService providerService;

   @Autowired
   private CategoryService categoryService;

   /**
    * 商品模糊查询
    *
    * @param
    * @return
    */
   @SysLog("商品查询操作")
   @RequestMapping("/goodsList")
   public DataGridViewResult goodsList(GoodsVO goodsVO) {
       //创建分页信息   参数1 当前页 参数2 每页显示条数
       IPage<Goods> page = new Page<>(goodsVO.getPage(), goodsVO.getLimit());
       QueryWrapper<Goods> queryWrapper = new QueryWrapper<>();
       queryWrapper.eq(goodsVO.getProviderid() != null && goodsVO.getProviderid() != 0, "providerid", goodsVO.getProviderid());
       queryWrapper.like(!StringUtils.isEmpty(goodsVO.getGname()), "gname", goodsVO.getGname());
       IPage<Goods> goodsIPage = goodsService.page(page, queryWrapper);
       List<Goods> records = goodsIPage.getRecords();
       for (Goods goods : records) {
           Provider provider = providerService.getById(goods.getProviderid());
           if (null != provider) {
               goods.setProvidername(provider.getProvidername());
          }
      }
       return new DataGridViewResult(goodsIPage.getTotal(), records);
  }


   /**
    * 添加商品信息
    *
    * @param goods
    * @return
    */
   @SysLog("商品添加操作")
   @PostMapping("/addgoods")
   public Result addGoods(Goods goods) {
       String id = RandomStringUtils.randomAlphanumeric(8);
       if (goods.getGoodsimg()!=null&&goods.getGoodsimg().endsWith("_temp")){
           String newName = AppFileUtils.renameFile(goods.getGoodsimg());
           goods.setGoodsimg(newName);
      }
       goods.setGnumbering(id);
       boolean bool = goodsService.save(goods);
       if (bool) {
           return Result.success(true, "200", "添加成功!");
      }
       return Result.error(false, null, "添加失败!");
  }

   /**
    * 修改商品信息
    *
    * @param goods
    * @return
    */
   @SysLog("商品修改操作")
   @PostMapping("/updategoods")
   public Result updateGoods(Goods goods) {
       //商品图片不是默认图片
       if (!(goods.getGoodsimg()!=null&&goods.getGoodsimg().equals(Constast.DEFAULT_IMG))){
           if (goods.getGoodsimg().endsWith("_temp")){
               String newName = AppFileUtils.renameFile(goods.getGoodsimg());
               goods.setGoodsimg(newName);
               //删除原先的图片
               String oldPath = goodsService.getById(goods.getGid()).getGoodsimg();
               AppFileUtils.removeFileByPath(oldPath);
          }
      }
       boolean bool = goodsService.updateById(goods);
       if (bool) {
           return Result.success(true, "200", "修改成功!");
      }
       return Result.error(false, null, "修改失败!");
  }


   /**
    * 删除单条数据
    *
    * @param id
    * @return
    */
   @SysLog("商品删除操作")
   @RequestMapping("/deleteOne")
   public Result deleteOne(int id) {

       boolean bool = goodsService.removeById(id);
       if (bool) {
           return Result.success(true, "200", "删除成功!");
      }
       return Result.error(false, null, "删除失败!");
  }

   /**
    * 根据id查询当前商品拥有的类别
    *
    * @param id
    * @return
    */
   @RequestMapping("/initGoodsByCategoryId")
   public DataGridViewResult initGoodsByCategoryId(int id) {
       List<Map<String, Object>> mapList = null;
       try {
           //查询所有类别列表
           mapList = categoryService.listMaps();
           //根据商品id查询商品拥有的类别
           Set<Integer> cateIdList = categoryService.findGoodsByCategoryId(id);
           for (Map<String, Object> map : mapList) {
               //定义标记 默认不选中
               boolean flag = false;
               int cateId = (int) map.get("cateid");
               for (Integer cid : cateIdList) {
                   if (cid == cateId) {
                       flag = true;
                       break;
                  }
              }
               map.put("LAY_CHECKED", flag);
          }
      } catch (Exception e) {
           e.printStackTrace();
      }
       return new DataGridViewResult(Long.valueOf(mapList.size()), mapList);

  }

   /**
    * 根据商品id加载商品信息
    * @param goodsid
    * @return
    */
   @GetMapping("/loadGoodsById")
   public DataGridViewResult loadGoodsById(int goodsid) {


       QueryWrapper<Goods> goodsQueryWrapper = new QueryWrapper<>();
       goodsQueryWrapper.eq(goodsid != 0, "gid", goodsid);
       Goods goods = goodsService.getById(goodsid);

       return new DataGridViewResult(goods);

  }

   /**
    * 为商品分配类别
    *
    * @param categoryids
    * @param goodsid
    * @return
    */
   @SysLog("类别添加操作")
   @RequestMapping("/saveGoodsCategory")
   public Result saveGoodsCategory(String categoryids, int goodsid) {

       try {
           if (goodsService.saveGoodsCategory(goodsid, categoryids)) {
               return Result.success(true, null, "分配成功");
          }

      } catch (Exception e) {
           e.printStackTrace();
      }
       return Result.error(false, null, "分配失败");

  }

   /**
    * 加载下拉框
    *
    * @return
    */
   @RequestMapping("/loadAllGoods")
   public DataGridViewResult loadAllGoods() {
       QueryWrapper<Goods> queryWrapper = new QueryWrapper<>();
       List<Goods> list = goodsService.list(queryWrapper);
       return new DataGridViewResult(list);

  }


   /**
    * 根据供应商查商品下拉框
    *
    * @param providerid
    * @return
    */
   @RequestMapping("/loadGoodsByProvidreId")
   public DataGridViewResult loadGoodsByProvidreId(Integer providerid) {
       QueryWrapper<Goods> goodsQueryWrapper = new QueryWrapper<>();
       goodsQueryWrapper.eq(providerid != null, "providerid", providerid);
       List<Goods> list = goodsService.list(goodsQueryWrapper);
       for (Goods goods : list) {
           Provider provider = providerService.getById(goods.getProviderid());
           if (null != provider) {
               goods.setProvidername(provider.getProvidername());
          }

      }
       return new DataGridViewResult(list);

  }
}

前端销售控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
kotlin复制代码/**
* <p>
* 前端控制器
* </p>
*/
@RestController
@RequestMapping("/sale")
public class SaleController {
   @Autowired
   private SaleService saleService;

   @Autowired
   private GoodsService goodsService;
   
   @Autowired
   private CustomerService customerService;

   /**
    * 销售查询
    *
    * @param
    * @return
    */
   @SysLog("销售查询操作")
   @RequestMapping("/saleList")
   public DataGridViewResult saleList(SaleVO saleVO) {

       //创建分页信息   参数1 当前页 参数2 每页显示条数
       IPage<Sale> page = new Page<>(saleVO.getPage(), saleVO.getLimit());
       QueryWrapper<Sale> queryWrapper = new QueryWrapper<>();
       queryWrapper.like(!StringUtils.isEmpty(saleVO.getNumbering()),"numbering", saleVO.getNumbering());
       queryWrapper.eq(saleVO.getGid() != null && saleVO.getGid() != 0, "gid", saleVO.getGid());
       queryWrapper.ge(saleVO.getStartTime() != null, "buytime", saleVO.getStartTime());
       queryWrapper.le(saleVO.getEndTime() != null, "buytime", saleVO.getEndTime());

       queryWrapper.orderByDesc("buytime");

       IPage<Sale> saleIPage = saleService.page(page, queryWrapper);

       List<Sale> records = saleIPage.getRecords();

       for (Sale sale : records) {
           sale.setAllmoney(sale.getMoney()*sale.getBuyquantity());
           Customer customer = customerService.getById(sale.getCustid());

           if (null != customer) {
               sale.setCustomervip(customer.getCustvip());
               sale.setCustomername(customer.getCustname());
          }
           Goods goods = goodsService.getById(sale.getGid());
           if (null != goods) {

               sale.setGoodsname(goods.getGname());
               sale.setGnumbering(goods.getGnumbering());
          }
      }

       return new DataGridViewResult(saleIPage.getTotal(), records);

  }


   /**
    * 添加销售单信息
    *
    * @param sale
    * @return
    */
   @SysLog("销售添加操作")
   @PostMapping("/addsale")
   public Result addsale(Sale sale, HttpSession session) {
       if (sale.getGid()==0){
           return Result.error(false, null, "添加失败!未选商品");
      }
       Goods goods = goodsService.getById(sale.getGid());
       Integer gquantity = goods.getGquantity();
       if(gquantity<sale.getBuyquantity()){
           return Result.error(false, null, "添加失败!库存不足,库存为:"+gquantity);
      }
       User user = (User) session.getAttribute("username");
       String num = RandomStringUtils.randomAlphanumeric(7);
       sale.setNumbering(num);
       sale.setPerson(user.getUsername());
       sale.setBuytime(new Date());
       sale.setRealnumber(sale.getBuyquantity());
       boolean bool = saleService.save(sale);
       if (bool) {
           return Result.success(true, "200", "添加成功!");
      }
       return Result.error(false, null, "添加失败!库存不足");
  }


   /**
    * 修改销售单信息
    *
    * @param sale
    * @return
    */
   @SysLog("销售修改操作")
   @PostMapping("/updatesale")
   public Result updatesale(Sale sale, HttpSession session) {

       User user = (User) session.getAttribute("username");
       sale.setPerson(user.getUsername());
       sale.setBuytime(new Date());
       boolean bool = saleService.updateById(sale);
       if (bool) {
           return Result.success(true, "200", "修改成功!");
      }
       return Result.error(false, null, "修改失败!");
  }
   /**
    * 删除单条数据
    *
    * @param id
    * @return
    */
   @SysLog("销售删除操作")
   @RequestMapping("/deleteOne")
   public Result deleteOne(int id) {

       boolean bool = saleService.removeById(id);
       if (bool) {
           return Result.success(true, "200", "删除成功!");
      }
       return Result.error(false, null, "删除失败!");
  }



}

本文转载自: 掘金

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

ribbon中自定义负载均衡算法

发表于 2021-11-18

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

ribbon是Netflix开源的一款用于客户端负载均衡的软件工具,它在集群中为各个客户端的通信提供了支持,有助于控制HTTP和TCP客户端的行为,提供了很多负载均衡的算法,例如轮询,随机等,同时也可以实现自定义的算法。

在Spring Cloud 构建的微服务中,Ribbon作为服务消费者的负载均衡器,有两种使用方式,一种是与RestTemplate相结合,另一种是与Feign相结合。Feign已经默认集成了Ribbon,并且关于Feign的相关知识我们将在第4章进行详细讲解。

Ribbon包含很多子模块,但很多子模块没有用于生产环境,目前用于生产的Ribbon的子模块具体如下:

● ribbon-core:包括定义负载均衡接口、客户端接口、内置的负载均衡实现等API。

● ribbon-eureka:提供eureka客户端实现负载均衡的API。

● ribbon-httpclient:对Apache的HttpClient进行封装,该模块提供了含有负载均衡功能的REST客户端。

Spring Cloud也允许开发人员声明额外的配置(在RibbonClientConfiguration之上)-@RibbonClient来取得客户端的全部控制权。例如:

1
2
3
4
java复制代码@Configuration
@RibbonClient(name = "foo", configuration = FooConfiguration.class)
public class TestConfiguration {
}

本例中,客户端由已在RibbonClientConfiguration中的组件以及FooConfiguration中的任意组件组成(后者通常覆盖前者)。

警告 FooConfiguration必须有@Configuration,但注意它并不在主应用上下文的@ComponentScan中,否则它会被所有的@RibbonClients分享(意思就是覆盖所有客户端的默认值)。如果开发人员使用@ComponentScan(或@SpringBootApplication),那就必须采取措施避免被覆盖到(==例如将其放入一个独立的,不重叠的包中==,或以@ComponentScan指明要扫描的包。

引入pom依赖

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码 <!--引入ribbon-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
<version>1.4.6.RELEASE</version>
</dependency>
<!--引入erueka-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
<version>1.4.6.RELEASE</version>
</dependency>

配置erueka

1
2
3
4
5
6
yml复制代码# 配置eureka
eureka:
client:
register-with-eureka: false # 不向eureka注册自己
service-url: # 获取服务的地址
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/

在主启动类上开启服务

1
2
3
4
5
6
7
8
java复制代码@SpringBootApplication
@EnableEurekaClient // eureka客户端
@RibbonClient(name = "SPRINGCLOUD-PROVIDER-DEPT",configuration = MyRule.class )
public class DeptConsumer {
public static void main(String[] args) {
SpringApplication.run(DeptConsumer.class,args);
}
}

创建配置类

注意配置类存放的包,不能跟启动类平级
在这里插入图片描述
在MyRule类中进行springboot配置类的声明

1
2
3
4
5
6
7
java复制代码@Configuration // 表明其为基本的配置类
public class MyRule {
@Bean // 把组件放入ioc容器中
public IRule myrule(){
return new MyRandomRule();
}
}

定义自己的类MyRule实现负载均衡算法,自定类应该继承父类AbstractLoadBalancerRule(接口IRule的实现类都继承了该类)

最后启动,检验自己的负载均衡算法

本文转载自: 掘金

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

1…288289290…956

开发者博客

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