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

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


  • 首页

  • 归档

  • 搜索

Jenkins 构建后执行 nohup 脚本,前台不退出

发表于 2021-08-21

本文已参与掘金创作者训练营第三期「高产更文」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力。

Jenkins 简介

Jenkins自动化部署可以解决集成、测试、部署等重复性的工作,工具集成的效率明显高于人工操作;并且持续集成可以更早的获取代码变更的信息,从而更早的进入测试阶段,更早的发现问题,这样解决问题的成本就会显著下降:持续集成缩短了从开发、集成、测试、部署各个环节的时间,从而也就缩短了中间出现的等待时间;持续集成也意味着开发、集成、测试、部署得以持续。

Jenkins是一个用Java编写的开源的持续集成工具。官方网站:jenkins.io

Jenkins能实时监控持续集成过程中所存在的问题,提供详细的日志文件和提醒功能,还能用图表的形式,形象地展示项目构建的趋势和稳定性。

1 脚本内容

1
2
3
4
5
6
7
8
9
bash复制代码#!/bin/bash
cat /dev/null > nohup.out
NUM=`netstat -tunlp | grep 8000 | wc -l`
if [ ${NUM} -eq 0 ];then
echo "Service not start.starting......."
  nohup python36 /data/webPage/manage.py runserver 0.0.0.0:8000 &
else
echo "Service already run on 0.0.0.0:8000"
fi

脚本内容很简单,就是检测 8000 端口是否监听,没有监听的话尝试启动服务,监听的话就正常退出即可。

2 问题

Jenkins 构建拉取代码后,将代码通过 Publish Vver SSH 插件推送到业务机器,然后执行脚本时,脚本中的 nohup 命令无法正常退出,导致构建任务前台卡住。如图:

3 排查

查询后发现:

Since you are executing a script from a non-TTY environment; The Jenkins is not able to get the exit properly, out of your script.What you want is to exit immediately, after script execution! Don’t want to wait for the entire timeout to happen and then disconnect improperly!

意为:

从一个非 tty 环境执行脚本;Jenkins 不能正常从你的脚本中退出

4 解决

在PTY中使用Exec

虽然构建完成了,但是服务却没有启动,可能的原因是因为,在执行openapi start命令时,刚启动,pty(伪终端)就断开连接,

解决的办法就是,加 nohup 让脚步在后台运行,也就即使伪终端断开了,项目依然可以启动完成。

本文转载自: 掘金

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

To-B与To-C 你了解多少?

发表于 2021-08-21

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

B端和C端的概念

  • B端: Business,通常为企业内部或商家使用的系统或平台。
    • 面对企业客户,多为一个群体使用,本质是满足工作需求,多为多种功能的整合,讲究协同合作。使用B端往往是长时间、沉浸式,B端是服务于公司或企业所有员工。
    • 举例:企业内部ERP管理系统、财务管理平台等。
  • C端: Consumer(也可理解为Customer),通常为消费者、个人终端用户使用的客户端。
    • 面对面对个人用户,多为单个个体使用,讲究用户的使用感觉,有一个核心的功能,其他的功能都是附加的,是为“碎片化的时间”服务的,特点是数据量大,因为用户群大,数据增长都是指数级的。
    • 举例:微信、淘宝、网易云音乐等。

image.png

区别于联系

1.用户类型

  • B端:用户量相对较小 –>> 企业、商家,一般称之为“客户”
  • C端:用户量相对较大 –>> 终端个体,一般称之为“用户”

2.展示方式及盈利模式

  • B端:基本为PC端,大部分提供的是定制化的付费产品,偏向于企业工作系统 –> “定制付费”
  • C端:手机端为主、PC端为次;大多功能免费功能,免费基础上增加广告、促销等付费功能 –> “规模经济”

3.产品设计

  • B端:满足不同组织之间协作,系统呈现模块化,系统用户一般会分为多种角色,有角色分配和权限管理等
  • C端:有一个核心功能点,在此基础上增加增值服务及更好的用户体验,用户一般按照年龄、性格、喜好等划分

4.运营策略

  • B端:线下为主;相对传统,通过“大型会议、行业展会”等,专业角度吸引企业客户。
  • C端:线上为主;通过“优惠券、红包” 等奖励方式 激励用户进行线上分享,提高产品关注度。

参考链接

  • B端和C端 知乎
  • 国内SaaS平台有哪些?
  • B端和C端产品的理解

本文转载自: 掘金

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

高级树、AVL树和红黑树

发表于 2021-08-21

覃超的《算法训练营》课程学习已接近尾声,通过这段时间的学习及训练,对数据结构和算法的认知进一步加深。从业务系统的CURD下沉到底层的数据结构和算法,回顾这些知识给我最大的感触是:这些数据结构和算法科学家/专家非常伟大。

今天用一些时间总结课程里关于“树”的知识。读者需要注意的是,对于下文中提到的几种“树”,这里只做简单的介绍、小结。详细的推理和证明建议有兴趣的读者可以查阅详细的资料,本文就不再花费大量篇幅去拷贝各位科学家的论文。提示:本文里有些超链接需要自备科学上网工具才可访问。

树(Tree)

image.png

  • Root:根节点
  • Parent Node:父节点
  • Child Node:孩子节点
  • Left Node:叶子节点
  • Sub-tree:子树

二叉树(Binary Tree)

image.png

二叉树每个父节点最多拥有两个孩子节点,有如下三种遍历顺序:

1. 前序遍历(Pre-order):根 -> 左 -> 右

1
2
3
4
5
6
7
8
9
10
java复制代码// sample code
public void preorder(List<Integer> res, Node root) {
// terminator
if (root == null) return;

// business
res.add(root.val);
preorder(res, root.left);
preorder(res, root.right);
}

2. 中序遍历(In-order):左 -> 根 -> 右

1
2
3
4
5
6
7
8
9
10
java复制代码// sample code
public void preorder(List<Integer> res, Node root) {
// terminator
if (root == null) return;

// business
preorder(res, root.left);
res.add(root.val);
preorder(res, root.right);
}

3. 后序遍历(Post-order):左 -> 右 -> 根

1
2
3
4
5
6
7
8
9
10
java复制代码// sample code
public void preorder(List<Integer> res, Node root) {
// terminator
if (root == null) return;

// business
preorder(res, root.left);
preorder(res, root.right);
res.add(root.val);
}

二叉搜索树(Binary Search Tree)

image.png

二叉搜索树,也称二叉搜索树、有序二叉树(Ordered Binary Tree)、排序二叉树(Sorted Binary Tree),是指一颗空树或具有下列性质的二叉树:

  1. 左子树上所有节点的值均小于它的根节点的值;
  2. 右子树上所有节点的值均大于它的根节点的值;
  3. 以此类推:左、右子树也分别为二叉搜索树(这就是重复性)。

注意:二叉搜索树中序遍历的结果是升序排列。

下面我们来思考下面这幅图,找出红色节点所有的时间复杂度是多少?
image.png
在上图这个二叉搜索树中找出红色节点所有的时间复杂度是O(log2N),这里的2为底数,N为节点数量。

如果出现下图的极端情况,退化成了一个线性的结构,那么找出最底部的节点,其时间复杂度是多少?
image.png
在上图的线性结构中找出最底部节点的时间复杂度是O(N),N为节点数量。

从上面的现象可以得出这样的结论:需要保证二维维度,让左右子树节点平衡。

从维基百科Self-balancing binary search tree可以详细查看实现了上述结论的几种树。

  • 2–3 tree
  • AA tree
  • AVL tree
  • B-tree
  • Red–black tree
  • Scapegoat tree
  • Splay tree
  • Tango tree
  • Treap
  • Weight-balanced tree

AVL树

AVL树的每个节点都有其平衡因子,以平衡因子判断子树是否要通过旋转操作(有四种旋转操作)来进行平衡。

Balance Factor(平衡因子):某父节点的左子树高度减去它的右子树高度(有时候右子树减去左子树)。平衡因子有三个值可取,分别是-1, 0, 1。如果某个节点的平衡因子不属于这三个值其中之一,就需要对其和其子树进行旋转操作。

下图是一个AVL树,绿色的数字代表其节点的平衡因子。
image.png

当增加一个数值为14的节点,该树的平衡因子变为下图。
image.png

当增加一个数值为3的节点,该树的平衡因子变为下图。
image.png
此时数值为10的节点的平衡因子已经不是-1, 0, 1的其中一个了,而是-2,就需要对其和其子树进行旋转操作。

如何进行旋转操作?首先介绍四种旋转,最后再对结合上图具体说明使用哪种旋转方法。

1. 左旋

当子树状态为右右子树(某父节点仅有单一的右子树,且该右子树也仅有单一的右子树),就对其进行左旋,如下图(图中左边为右右子树状态,右边为旋转后的状态)。
image.png

2. 右旋

当子树状态为左左子树(某父节点仅有单一的左子树,且该左子树也仅有单一的左子树),就对其进行右旋,如下图(图中左边为左左子树状态,右边为旋转后的状态)。
image.png

3. 左右旋

当子树状态为左右子树(某父节点仅有单一的左子树,且该左子树仅有单一的右子树),就对其进行左右旋,如下二张图。

首先对左右子树进行左旋,让其成为左左子树。
image.png

再对左左子树进行右旋。
image.png

4. 右左旋

当子树状态为右左子树(某父节点仅有单一的右子树,且该左子树仅有单一的左子树),就对其进行右左旋,如下二张图。

首先对右左子树进行右旋,让其成为右右子树。
image.png

再对右右子树进行左旋。
image.png

上面介绍的四种旋转操作的简图没将子树画出来。带有子树旋转链接1/链接2的示意图可参考下图。
image.png

旋转操作例题1

介绍完四种旋转操作后,回到前面提到的一个AVL树:「当增加一个数值为3的节点,该树的平衡因子变为下图。」
image.png
此时数值为10的节点的平衡因子已经不是-1, 0, 1的其中一个了,而是-2,就需要对其和其子树进行旋转操作。

因为对数值为10的节点属于左左子树的状态,所以需要对其进行右旋,如下图。
image.png

参考上面带子树旋转的示意图可知,旋转后要将数值为5的节点的子树(8)挂载到数值为10的节点下。旋转后的结果如下图。
image.png

旋转操作例题2

一个AVL树的平衡因子如下图。
image.png

当增加数值为15的节点,其平衡因子变为下图。
image.png
此时数值为9的节点和数值为7的节点的平衡因子已经不是-1, 0, 1的其中一个了,而是+2,就需要对其和其子树进行旋转操作。

我们对层数更高的数值为9的节点进行旋转操作,其属于左右子树的状态,所以需要对其进行左右旋,首先对其进行右旋,如下两张图。
image.png

右旋后的结果如下图。
image.png

再对其进行左旋,如下两张图。
image.png

左旋后的结果如下图。
image.png

AVL树小结

  1. 它是平衡二叉树;
  2. 每个节点存balance factor = {-1, 0, 1};
  3. 四种旋转操作。

得益于AVL树的数据结构,每个节点的左右子树高度差不超过1,极端境况的时间复杂度也非常接近O(log2N),因此它的查询效率比较高。但也因此带来了一些不足之处:节点需要存储额外信息(平衡因子),且调整次数频繁,因此出现了红黑树。

红黑树

红黑树是一种近似平衡的二叉搜索树(BinarySearch Tree),它能够确保任何一个节点的左右子树的高度差小于两倍。具体来说,红黑树是满足如下条件的二叉搜索树:

  • 每个节点要么是红色,要么是黑色
  • 根节点是黑色
  • 每个叶节点(NIL节点,空节点)是黑色的。
  • 不能有相邻接的两个红色节点
  • 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

关键性质:从根节点到叶子节点的最长的可能路径不多于最短的可能路径的两倍。

image.png
image.png

最后有个AVL树和红黑树的概述和比较,如下图。

image.png

  • AVL树的查询效率比红黑树高,因为AVL树更严格的平衡。上面提到的AVL树的左右子树高度差不超过1,而红黑树从根节点到叶子节点的最长的可能路径不多于最短的可能路径的两倍。
  • 红黑树的插入和删除效率比AVL高,因为红黑树是一个近似平衡树。AVL树如果增加/删除节点会因为平衡因子的变化而进行更多的旋转操作。
  • AVL树存的信息要比红黑树更多,所以AVL树额外的消耗空间要比红黑树多。AVL要存平衡因子和树的高度,而红黑树只要存一个bit来存0或1表示是黑还是红。
  • 红黑树一般使用在高级语言(Java, C++ …)里面的map结构比较多,AVL树在数据库里的使用更多。因为根据上面的结论,如果写操作比较多或者读/写操作一半一半的时候,用红黑树即可,所以用在高级语言里的map比较多。而数据库往往是读操作多于写操作,所以一般是用AVL树。

本文转载自: 掘金

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

Go,一文搞定 defer 实现原理 答题环节 defer

发表于 2021-08-21

本文已参与掘金创作者训练营第三期「高产更文」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力。

defer 语句用于延迟函数的调用,使用 defer 关键字修饰一个函数,会将这个函数压入栈中,当函数返回时,再把栈中函数取出执行。

老规矩,我们先来答几道题试试水。

答题环节

  1. 下面程序输出什么?
1
2
3
4
5
6
7
go复制代码func deferTest() {
var a = 1
defer fmt.Println(a)

a = 2
return
}

答

案

是:1

解析:延迟函数 fmt.Println(a) 的参数在 defer 语句出现的时候就已经确定下来了,所以不管后面如何修改 a 变量,都不会影响延迟函数。

  1. 下面程序输出什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
go复制代码package main

import "fmt"

func main() {
deferTest()
}

func deferTest() {
var arr = [3]int{1, 2, 3}
defer printTest(&arr)

arr[0] = 4
return
}

func printTest(array *[3]int) {
for i := range array {
fmt.Println(array[i])
}
}

答

案

是:

1
2
3
go复制代码4
2
3

解析:延迟函数 printTest() 的参数在 defer 语句出现的时候就已经确定下来了,即为数组的地址,延迟函数执行的时机是在 return 语句之前,所以对数组的最终修改的值会被打印出来。

  1. 下面程序输出什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码package main

import "fmt"

func main() {
res := deferTest()
fmt.Println(res)
}

func deferTest () (result int) {
i := 1

defer func() {
result++
}()

return i
}

答

案

是:

1
go复制代码2

解析:函数的 return 语句并不是原子级的,实际的执行过程为为设置返回值—>ret,defer 语句是在返回前执行,所以返回过程是:设置返回值—>执行defer—>ret。所以 return 语句先把 result 设置成 i 的值(1),defer 语句中又把 result 递增 1 ,所以最终返回值为 2 。

defer 规则

  • 延迟函数的参数在defer语句出现时就已经确定
+ 注意:对于指针类型的参数,规则仍然适用,不过延迟函数的参数是一个地址值,这种情况下,defer 后面的语句对变量的修改可能会影响延迟函数。
  • 延迟函数执行按 先进后出 顺序执行,即先出现的 defer 最后执行
  • 延迟函数可能操作主函数的具名返回值

函数返回过程

上面题目中我们已经了解到,函数的 return 语句并不是原子级的,实际上 return 语句只代理汇编指令 ret。返回过程是:设置返回值—>执行defer—>ret。

1
2
3
4
5
6
7
8
9
go复制代码func deferTest () (result int) {
i := 1

defer func() {
result++
}()

return i
}

上面有 defer 例子的return 语句实际执行过程是:

1
2
3
go复制代码result = i
result++
return

主函数拥有匿名返回值,返回字面值时

当主函数有一个匿名返回值,返回时使用字面值,例如返回 “1”,“2”,“3” 这样的值,此时 defer 语句是不能操作返回值的。

1
2
3
4
5
6
7
8
go复制代码func test() int {
var i int
defer func() {
i++
}()

return 1
}

上面的 return 语句,直接把1作为返回值,延迟函数无法操作返回值,所以也就不能修改返回值。

主函数拥有匿名返回值,返回变量时

当主函数有一个匿名返回值,返回会使用本地或者全局变量,此时 defer 语句可以引用到返回值,但不会改变返回值。

1
2
3
4
5
6
7
8
go复制代码func test() int {
var i int
defer func() {
i++
}()

return i
}

上面的函数,返回一个局部变量,defer 函数也有操作这个局部变量。对于匿名返回值来说,我们可以假定仍然有一个变量用来存储返回值,例如假定返回值变量为 ”aaa”,上面的返回语句可以拆分成以下过程:

1
2
3
go复制代码aaa = i
i++
return

由于i是整型,会将值拷贝给变量 aaa,所以defer语句中修改 i的值,对函数返回值不造成影响。

主函数拥有具名返回值时

主函声明语句中带名字的返回值,会被初始化成一个局部变量,函数内部可以像使用局部变量一样使用该返回值。如果 defer 语句操作该返回值,可能会改变返回结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码package main

import "fmt"

func main() {
res := test()
fmt.Println(res) // 1
}
func test() (i int) {
defer func() {
i++
}()
return 0
}

上面的返回语句可以拆分成以下过程:

1
2
3
go复制代码i = 0
i++
return

defer实现原理

源码包 src/src/runtime/runtime2.go:_defer 定义了defer的数据结构:

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
go复制代码type _defer struct {
siz int32 // includes both arguments and results
started bool
heap bool
// openDefer indicates that this _defer is for a frame with open-coded
// defers. We have only one defer record for the entire frame (which may
// currently have 0, 1, or more defers active).
openDefer bool
sp uintptr // sp at time of defer
pc uintptr // pc at time of defer
fn *funcval // can be nil for open-coded defers
_panic *_panic // panic that is running defer
link *_defer

// If openDefer is true, the fields below record values about the stack
// frame and associated function that has the open-coded defer(s). sp
// above will be the sp for the frame, and pc will be address of the
// deferreturn call in the function.
fd unsafe.Pointer // funcdata for the function associated with the frame
varp uintptr // value of varp for the stack frame
// framepc is the current pc associated with the stack frame. Together,
// with sp above (which is the sp associated with the stack frame),
// framepc/sp can be used as pc/sp pair to continue a stack trace via
// gentraceback().
framepc uintptr
}
  • sp 函数栈指针
  • pc 程序计数器
  • fn 函数地址
  • link 指向自身结构的指针,用于链接多个 defer

defer 语句后面是要跟一个函数的,所以 defer 的数据结构跟一般的函数类似,不同之处是 defer 结构含有一个指针,用于指向另一个 defer ,每个 goroutine 数据结构中实际上也有一个 defer 指针指向一个 defer 的单链表,每次声明一个defer 时就将 defer 插入单链表的表头,每次执行 defer 时就从单链表的表头取出一个 defer 执行。保证 defer 是按 FIFO 方式执行的。

defer的创建和执行

源码包 src/runtime/panic.go 中定义了两个方法分别用于创建defer和执行defer。

  • deferproc(): 在声明 defer 处调用,其将defer 函数存入 goroutine 的链表中;
  • deferreturn():在 return 指令,准确的讲是在 ret 指令前调用,其将 defer 从 goroutine链表中取出并执行。

归纳总结

  1. defer 定义的延迟函数的参数在 defer 语句出时就已经确定下来了
  2. defer 定义顺序与实际执行顺序相反
  3. return 不是原子级操作的,执行过程是: 保存返回值—>执行 defer —>执行ret

本文转载自: 掘金

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

冲|2年CRUD|社招|后端开发|上岸面试分享(附面经) 前

发表于 2021-08-21

前言

背景

  • 普通本科(非985/211),计算机科学与技术;
  • 2019届,2年经验;
  • 在广州一家传统中大厂从事后端研发工作;
  • 本次参与的招聘为社招;

知识储备

  • 我们常常说“面试造火箭,入职拧螺丝”,确实绝大多数的面试考察内容在工作中可能用不到,俗称“八股文”;
  • 但往往很多深度的知识能够帮助我们在遇到更难的问题中提供必要的知识储备,提升我们的思维能力,去完善我们的知识体系;
  • 知识来源可以通过高分专业书籍,博主/公众号/博客总结等,还有算法(leetcode刷起,建议100+);
  • 可以通过思维导图去完善我们的知识体系,查漏补缺,总体还是非常高效的(不过也因人而异,推荐尝试下~);

image.png

  • 在面试的过程中,从沟通交流,知识体系,业务思考,每一项都是对自身成长的考验和提升,每次面试后及时复盘,摆正心态,查漏补缺;

面经

Fordeal

业务:跨境电商,供应链;

结论:薪资没有谈妥,HR面挂;

一面(40min)

  • 自我介绍, 项目介绍,整体服务层次,业务逻辑流转,上层业务有哪些,项目难点,怎么解决?
  • Dubbo:
    • 常见的dubbo filter(一时想不起来,讲了ValidationFilter, exceptionFilter);
    • 自定义注入实现原理?(Dubbo SPI注入,链式调用);
    • 是否可以在filter中修改默认配置?比如通信协议/序列化方式。(dubbo通过spring进行配置初始化通信协议/序列化方式,应该是不能在filter去修改的,不确定,面试官引导说可能会导致运行时异常?)
  • ES:
    • 业务场景使用?(多条件关联搜索+全文检索需求);
    • 索引存储设计,宽表,用DB为什么无法实现?(设计思路,减少宽表冗余+避免join索引);
    • DB->ES数据异构同步延时点有哪些(DB->Canal参考MySQL主备延迟,MQ投递消费,上报ES写buffer,写translog防刷盘前丢失,刷新到OS Cache,1s可见);
    • 强实时性场景怎么避免时延?(走DB,ES近实时无法保障),同步处理速度多少?
  • Spring:
    • Spring Schedule原理(内置ScheduledExecutorService);
  • MQ:
    • 数据异构,重复上报问题(幂等处理);
  • Java:
    • YGC频繁场景(流量突增,DB getConnection慢,db连接数不稳定,无慢查询,YGC频率高,STW时间长,日志时间点符合,临时处理措施->实例扩容,增大年轻代空间,总体表述不清晰);

二面(50min)

  • 自我介绍,当前薪资-对应能力维度支撑有哪些,项目中最得意的设计/模块;
  • Java:
+ JVM, 配额,年轻代,老年代,堆内存;
+ YGC过程(对象生产过快,gc回收过慢,年轻代-> Eden,S1,S2->老年代,分配担保,交叉GC现象,这部分没有讲好);
+ GC问题排查过程(APM,告警,jstat -gcutil pid, jstat dump, mat,与服务异常时间点比对,确定堆内存大对象,产生原因,解决);
+ gc dump导致应用内存雪崩怎么处理(挂载磁盘?设置dump策略,按频率阈值,收敛?说了配置printGCDetail,好像不是很满意);
+ CountDownlatch , cycleBarr区别(一组多个线程同步,一组多个线程多次同步,问线程池100,countdownlatch 1000,和cyc1000,哪个能正常执行,哪个会阻塞);
+ 通知机制(wait, notify)&信号量机制(没答到要点,不大记得了);
  • MySQL:
+ 回表(二级索引查找->主键id->一级索引+随机IO查找完整数据);
+ 如何减少回表(覆盖索引), 好处(减少回表随机IO查找),坏处(索引更新,存储成本);
+ 锁,锁行还是锁索引(mysql不是数据即索引吗?有点懵),说了锁行record锁;
+ explain分析,using index, using condition index区别(using index,using where直接走索引扫描,condition需要多走一次回表)
  • 微服务:
+ 常见微服务结构层次(说了服务节点,注册中心(服务注册与发现,更新), 网关,监控,各个组成,作用)
+ 网关核心作用(讲到限流,统一流量入口,面试官提示从RPC层面-做统一的协议封装和转换, RPC过程);
+ http1.1, 2区别(长连接/短连接,过程,区别;其他的想不起来了,头部信息压缩,二进制流,http请求下TCP连接复用)
  • Es:
+ 倒排索引结构;(分词key,文档id为value)
+ 查询优化(硬件,索引结构设计,DSL优化)
+ 查询过程(queryAndFetch巴拉巴拉);
+ 主分片聚合结果过大怎么处理(分页限制,单个节点就有。面试官提示主分片优先队列)。
+ 分片作用(数据冗余备份,查询负载)
+ 动态分片扩容(副分片直接扩容,主分片扩容过程,不可写新主分片,保证数据一致?待确定,后面看了下,主分片数不可变更,副本分片可调整,可通过集群节点扩容,主分片数据重新路由到新集群节点实现);

HR面(40min)

  • 当前组织协作形式,跨部门沟通,流程;
  • 部门协作做的不足的地方,怎么去推进;
  • 未来发展规划,业务领域方向怎么看;
  • 期望薪资;

YY

业务:YY直播;

结论:面完说部门没有hc了,帮转部门面,没有下文了,一面挂;

一面(40min)

  • 项目:
+ 服务排查过程(APM链路,日志->针对具体问题,慢SQL分析/网络耗时/STW->GC问题排查);
+ jstack, jmap, 问了具体的几个gc排查命令,有个一直循环显示gc情况的命令,一时想不起来了,凉凉~后来查了下,jstat -gcutil pid)
+ 全链路压测过程(确定业务场景,用例,预估量,部署环境,依赖屏蔽,多轮压测,发现问题,针对性解决);
+ 链路传递怎么实现(大概讲了traceId通过agent注入,请求转发时在协议里带上,对业务开发无感)
+ 面试官好像不是很满意,问了不同协议请求怎么埋点;
+ 说抛开当前系统,自己会怎么设计(服务session传递,通过请求线程threadLocal传递)
    - 面试官又问到异步线程怎么处理?说通过变量传递?他说那就需要业务编码接入了吧,不大合适(后面看了内部实现,通过agent注入+http header/ dubbo rpcContext+日志组件解析记录的方式);
  • Sentinel:
+ 限流实现, 有哪些限流算法(漏桶,令牌桶);
+ 如果是自己设计,要怎么做,要求简单轻量(说了单机限流,通过starter注入,自动配置限流降级规则,加载到内存,通过接口代理去做异常,耗时统计,dubbo服务还可以通过filter实现拦截,巴拉巴拉);
  • Java:
+ hashMap集合结构(巴拉巴拉);
  • MySQL:
+ 事务隔离级别,&默认级别(InnoDB存储引擎下RR,并且可以防止幻读);
+ MVCC实现,版本链查找过程(不同的事务id,min\_ids, 活跃事务id,下一个id,跟版本链事务id比较,巴拉巴拉);
+ RP怎么实现(前面+ReadView视图生成)
  • Dubbo:
+ 通信协议,区别,序列化方式,使用场景(讲了dubbo+hession和RMI+JDK序列化,展开);
+ 问到protofer实现跟hession有什么不同吗(不懂);
+ 网络协议层面怎么去实现负载均衡(不清楚是想问啥,协议是网络通信交换,跟负载均衡关联不起来。。。)
  • Spring:
+ 事务传递,生产中怎么使用(Spring事务传递使用);
+ 同步线程能否读到事务中写入变量(当前事务可以),异步线程呢(猜想不可以,需要等事务线程提交后才可见);
  • Redis
+ 数据结构(讲了string, list, hash, set, zset)
+ HA,Sentinel,讲一下故障发现和转移过程(巴拉巴拉);
+ 怎么去发现新加入节点的redis节点(当时没想起来:Sentinel模式下可以通过集群通信发现新节点,cluster模式下可以通过槽指派);
+ redis pipeline有用过吗,讲一下(没有)
  • 逻辑:
+ 电话记录本,内容非常多,怎么实现统计相同电话号码记录次数,和top N记录(HashMap-key为电话号码,value为出现次数++,topN通过队列插入单独存,,,,应该是可以用有序map去存的,当时没想起来。。。)
    - 内存不够放怎么办?说了分组统计,比如两两对比去分而治之,面试官没有继续追问了,现在看来应该是不行的)

金山WPS

业务:云平台,偏中间件;

结论:拿到offer;

一面(83min)

  • Java:
    • 集合,ArrayList,LinkedList(区别,使用场景),HashMap结构,头插尾插区别(hash冲突链条冲突节点加入方式),解决什么问题(并发时死循环),红黑树转换条件(数组长度>64 && 链表长度>8),查询时间复杂度(logN);
    • 并发,CorrentHashMap结构,分段锁实现Lock,Synchronized关键字实现&区别(java类库实现,JVM层面实现,Lock手动加解锁,代码块,Synchronized可在代码块,方法加锁,区别,实现原理,acc_synchnorized,锁对象头monitor enter, exit);
    • CAS频繁会有什么问题(cpu飙高->走锁升级),讲一下锁升级过程(偏向锁-轻量级锁-自旋-重量级锁);
    • Java线程池(worker,boss线程),工作原理,各个阶段线程数的变化(核心线程数-队列-最大线程数-拒绝策略);
    • JVM内存模型,各个区域的作用,堆,堆内存放数据是什么(《深入理解JVM虚拟机》都有讲到);
    • 垃圾回收过程,算法,会导致什么问题(标记清除-内存碎片,提前GC),分代回收对应算法,GCRoot过程,有哪些对象关联,怎样判断一个对象可以被回收(对象无引用,GCroot不可达);
    • CMS,优势(并发,STW时间短),垃圾回收过程,STW阶段(初始标记,最终标记),弊端(CPU资源敏感,标记清除导致浮动垃圾);
  • Redis:
    • 加锁场景,过程,使用命令,讲了get,setnx+expiretime,del,原子性实现(说到lua合并命令,面试官说前期版本是,后期不是了,官方提供了原子指令级实现);
    • setnx含义(nx忘记了,面试官引导if not Exist,问没有原子性会导致什么问题)
    • 场景题:t1执行时间长,达到过期时间,被其他锁获取,破坏竞态条件怎么解决(说了锁续期,redission看门狗实现,t1耗时无法估计,什么时候做锁续期?只能瞎猜说异步线程计时器,过期时间之前进行续期,按一定策略去终止续期动作;面试官引导在加解锁的时候,key为锁变量,value为uuid或是版本号值,删除时做判断是否为当前线程持有锁,避免其他线程删除);
    • Redis的并发安全问题是否有考虑(瞎说了加锁?面试官提示性能问题,引导Redis单线程模型,讲了单个事件队列,不存在竞态条件)
  • 网络协议:
    • http, tcp层次,关系(应用层,传输层,网络包逐层包装交付);
    • tcp,udp区别,tcp三次握手,四次挥手过程;三次挥手有什么问题,四次挥手过程客户端和服务端分别处于什么状态(close_wait和time_wait说反了。。);
    • 为什么服务端发起fin后,客户端ack后是否直接close,说了2个MSL时间,问为什么需要这样设计(确保网络阻塞时服务端ack重发确认),RTT是什么;
    • tcp粘包问题,为什么会发生粘包(连续字节流传输,无边界),拆包发生在哪一层(应用层协议);
    • 滑动窗口过程,拥塞控制实现方式(慢开始,问怎么确定当前提升速率,说到往返包时间确认;怎么确认,发送端计时器,统计,面试官好像不大满意~,后面的拥塞控制算法细节一时想不起来了。。);
  • MySQL:
    • 索引结构(数据页,叶子结点,非叶子结点,有序性,页内数据);
    • B+树(为什么不是B树,讲了B树非叶子结点数据存储,树高问题);
    • 常见的SQL优化(讲到索引,覆盖索引,索引有序性,最左匹配,explain),回表过程是什么(二级索引+一级索引),怎么优化(select+覆盖索引);
    • 业务分库分表场景,问题点,跨表查询问题,是否有用过读写分离,主从结构(答了从库只是做备份,避免主从延迟问题);
  • ES:
    • 讲一下ES,倒排索引,分片结构,为什么快(key为分词,value为文档id,分布式存储,查询负载,扩容灵活);
    • 搜索过程(客户端,请求,协调节点,转发,本地查询,返回);
    • 查询优化方式(业务单索引设计,字段类型设计,主副分片配置,冗余备份+查询负载,DSL优化,评分/非评分查询,业务查询数量限制);
    • 遇到什么性能瓶颈(单个索引数据量不大,做业务索引区分,暂无);
  • MQ:
    • 使用场景,服务解耦,削峰;

二面( 40min)

  • MySQL:
    • 索引,有序性,失效场景,常见优化方式(索引结构,顺序查找,合理建立和使用索引,explain分析);
    • 场景题:学生表-学号,身高,体重三列,查询身高大于1米7,并且按体重排序,怎么做;
  • Redis:
    • 加解锁过程;(check判断是否为当前线程持有,尝试加锁setNx+Ex, 结束后del锁)
    • 线程超时后锁释放被其他线程获取怎么办(可以通过线程id加入到key,加锁时判断,确保过期后不被其他线程获取造成业务操作异常/守护线程提前续期);
  • 全链路压测:
    • 实施过程,怎么提升服务指标;
    • 异步化的场景是怎样的;(非同步消息推送,可以通过异步线程/MQ)
    • 压测有考虑DB数据量写入大小吗(保持与生产集群数据量一致,依据场景用例,业务层面通过查询维度过滤,暂无;对于流水数据查询做了日期水平分表);
  • Sentinel:
    • 实现原理;(Entry入口,Slot链构建,限流降级链条调用);
    • 有哪些限流降级方式(RT,异常数,耗时数),限流/统计算法;
    • 滑动窗口,时间窗怎么实现;
  • 项目:
    • 设计过程;
    • 考量哪些维度(调研,需求分析,得到业务元素,尽可能每个模块解耦提升灵活性)
    • 设计模式使用,策略模式场景,过程是怎样的;
    • 如果项目中有多个if嵌套,可以怎样优化(答了责任链模式,每次处理完之后传递给下一个节点)

三面(43min)

  • 算法:
+ 只出现一次的数


    - 讲了通过Map(num, count)存,统计,面试官说那不用额外空间怎么做,异或,没想起来;
+ LRU


    - 讲了访问链表,LRU链表;更新,插入时更新LRU链表头部,删除移除,最少使用挤出淘汰,讲的不清楚~;
    - 面试官问怎样在LRU链表查找指定节点,说了链表不支持随机访问,只能遍历,或通过额外hash数组做索引访问,数组值指向LRU链表节点,好像面试官没说啥,但好像不大满意。。。;
+ 算术运算,给定一个合法的计算式,计算结果


    - 讲了通过栈结构,算式拆解入栈,数字直接入栈,遇到匹配括号出栈,运算结果后入栈,直到结束;
+ 图查找最大价值,路径为权重,节点为值,求路径最大价值,不能同时分叉多个节点-其实是树;


    - 讲了动态规划,定义最大路径价值dp[node],通过DFS遍历,获取一个路径A的最大价值,再遍历B路径,更新MaxValue值,后面想用第二维去存储累计值,不大清楚~-面试官说不大行,后面讲了用第二维存储是否选择节点去做统计。
  • 项目:
+ 假设了一部分场景,问怎么实现,考察项目扩展性,数据一致性实现(非常深入,暂不展开);

HR面(20min)

  • 自我介绍,项目情况;
  • 沟通协作;
  • 薪资,工作地点确认;

涂鸦智能

业务:IOT & 物联网测试平台基础设施;

结论:过了一面后比较忙,拒了;

一面(30min)

  • NIO:
    • 讲一下NIO,零拷贝实现原理,优势,会有什么问题(文件IO/调用IO,阻塞/非阻塞,同步/异步,通过网络调用讲零拷贝实现过程);
  • Dubbo:
    • 服务注册发现(讲了整体结构,包括注册中心的服务注册发现,zk监听机制实现,服务生产与消费);
    • 调用过程(服务发现,调用,序列化,方法包装,链式调用,invoke,处理,逐层返回,反序列化);
    • 多个提供者场景怎么去调用(讲了四种负载均衡策略,场景,容错处理,追问了一致性hash,还有实现);
  • Zookeeper:
    • 使用场景(讲了注册中心和锁)
    • 节点数据结构(文件树),类型(临时,持久化,顺序节点搭配)
    • 选举过程(zk的不是很熟悉,参考Redis的主从&哨兵选举过程类比了下)
    • 脑裂问题怎么处理(trxId对比);
  • MySQL:
    • 场景:查询,select 列,where条件列在和不在索引列分别的查询过程(不在索引列多了回表过程);
    • 索引数据结构,回表,解决,随机/顺序IO问题;
  • Java:
    • JVM,常见的垃圾收集算法,特点,使用场景,过程;
    • CMS垃圾回收过程,不同阶段做了什么(初始标记,并发标记,重新标记,并法清除),STW,GC/用户线程在不同阶段的执行情况;
      (过了10min hr小姐姐打电话约下一轮,刚好工作上又来了个大需求,也有其他大厂面试,顶不住,拒了)

酷狗音乐

业务:酷狗K歌;

结论:拿到offer;

一面 (30min)

  • 全链路压测:
+ 流程(讲了压测场景,用例梳理,指标确认,压测环境搭建,样本,异常/耗时比例,APM链路排查耗时点);
+ 如何保证SLA三个9,统计维度有哪些(服务不可用时间,异常失败,耗时比例,自动采集统计);
  • 熔断限流:
+ 指标(讲了异常数,耗时数,比例统计);
+ 熔断措施(返回null,抛异常,设置熔断时长拒绝,到时自动恢复);
+ 讲一下服务雪崩(讲了微服务间调用,链路上下游,一个节点异常,拖垮正常节点,同时服务会有容错处理,比如重试,可能加剧请求压力,最终一个影响多个正常服务奔溃)
  • MySQL:
+ 查询优化,联合索引,减少回表等(巴拉巴拉);
+ 给定A,B, A+B字段查询,如何建索引(A\_B\_index, B\_index,第一个通过联合索引覆盖,第二个直接走B单列索引)
+ 分库分表:给定一个5亿订单表,查询总数;(按日期,获取每个订单分表count大小,汇总统计,看业务场景,这里不大行;讲了通过订单id hash,设定分表步长统计,面试官没有继续问。。。)
  • 缓存:
+ 数据结构,缓存&DB一致性保证(讲了旁路缓存模式,缓存查询,更新流程,最终一致性,完全强一致性则加锁保证)
    - 面试官认为场景比较简单,好像不大满意,期望复杂对象缓存和强一致性有所应用;
  • 问题排查:
+ CPU飙高问题排查(top, top -Hp pid, jstack -l tid)
+ 线程堆栈分析场景(代码死锁,循环等);

二面(20min)

  • 介绍认为设计的比较不错的项目/模块;
    • 讲了xx服务,基于什么考虑去开发(数据量,上层业务困扰,平台层面分析,调研,开发,推动,业务接入);
  • ES:
    • 选型(跟MySQL对比,讲了业务需求,场景,ES分词特性,搜索性能,社区活跃度,运维亲和性等维度)
    • 优化方式(运维层面:多分片副本,查询负载和存储冗余;索引层面:索引模板设计;查询层面:DSL优化);
    • 出现毛刺现象怎么解决,ES数据量,集群配置等;
  • Canal:
    • 同步流程,同步链路可靠性怎么保证(Canal监控+服务定时调度监控,消息堆积自动扩容,拉取批次参数动态调整,消息ack/rollback保障);
    • 为什么通过MQ削峰(讲了大数据量变更同步场景,利用MQ做堆积可靠性保证,削峰减少服务同步压力),服务对外异常怎么处理(通过服务层面自定义异常,业务处理)
  • 其他:
    • 年限/平台看法,团队项目,协作情况沟通,对方业务介绍,加班怎么看等;

三面(30min)

  • 项目:
    • 讲一下项目的整体流程,设计,选型,思考过程;
    • Es查询和同步是怎么实现的(queryAndFetch过程,分词存储,倒排索引);
    • 项目开发过程中,怎么主导,涉及业务方,内部,外部,需求,方案,评审,沟通,协调,风险,进度把控去怎么做;
  • Spring:
    • IOC实现过程(巴拉巴拉讲了一遍之后,面试官说可以再讲细节一点:包括到延迟加载判断,循环依赖过程一步步。。细致的流程没串起来)
    • 平常怎么使用spring的(讲了ServiceBean,自定义组件封装,自动配置starter,springMvc相关,jdbc层抽象封装,扩展);
  • Redis:
    • 选型,使用场景(锁/缓存),业务redis加解锁过程(get, check, setex, delete);
    • 任务执行时锁被其他线程抢占怎么处理(通过value值判断锁是否为当前线程持有);
    • 任务执行超过过期时间怎么处理(建立当前线程的守护线程+锁提前续期+线程任务结束释放);
  • MySQL:
    • 索引树结构(B+树展开);
    • 耗时排查过程(APM定位, explain分析);
    • HA怎么做的(主备);

字节跳动

业务:抖音直播;

结论:一面挂;

一面(70min)

  • Java
    • 线程池,核心参数,变化过程,有没有办法先走最大线程再走队列(没想到,后面了解到Dubbo线程池是这种实现,对于RPC调用,IO密集型,优先分配资源处理任务);
    • 面试官问说有什么好处,JDK线程池跟他这种,说到任务类型,IO,CPU密集,资源占用,好像不满意),场景,阻塞队列;
    • GC, cms,垃圾回收过程,stw阶段,初始和重新标记为什么需要stw(增量垃圾标记);
    • ClassNotFoundException和ClassNotDefException区别,什么场景发生(编译时/运行时类加载查找,);
    • Java进程占用高问题排查(常用命令使用);
  • Mybatis
    • Mapper接口怎么实现查询(讲了xml绑定代理),追问是静态还是动态代理实现;
  • Spring
    • 循环依赖怎么解决(三级缓存,过程)
  • MySQL
    • 索引类型,覆盖索引,场景(讲了查询过程,二级+聚簇,减少随机IO);
    • 隔离级别实现(MVCC);
    • 间隙锁,为了处理什么问题,什么时候会加上, select * from table for update会加吗;
    • 给一条sql: select * from table where a=‘x’ and b=‘x’ and c!=‘x’ order by d limit 0, 10 怎么加索引;
  • 网络:
    • http常见状态码,301,302请求转发地址放在报文哪个部分,cookie呢;(都答的http header);
    • https原理,加密过程(RSA握手,应用层,传输层之间,SSL加密,客户端,服务端,CA证书校验);有了解哪些加密算法吗;
  • Redis
    • 淘汰策略(random, Lru *2, abord);
    • 删除策略(定时,惰性,定期);
  • 算法
    • 二叉树序列化&反序列化;(先问了思路,评估下再让你写:一开始通过树迭代#换层,*记录null节点,树节点拉满,后面对于null处理有困难;使用序列化前序,中序节点,再反序列化建树;没写好)
    • leetcode-cn.com/problems/se…

阿里

业务:菜鸟网络;

结论:已拿offer;

一面(42min)

  • 项目:(怼了很多细节)
    • 参与的项目中,在团队中的角色,负责项,背景;
    • 讲一下项目中的难点;
    • 服务吞吐量多少,同步瓶颈在哪,怎么解决的;
    • 搜索引擎选型,DB数据同步选型(业务场景,特性,社区活跃性,运维亲和性),问是运维团队还是自己部署的(Canal服务端,Es运维部署,客户端+DTS自己实现);
    • 工作中的优缺点,怎么去处理;
  • Java
    • Synchronized使用场景,原理(竞态变量同步,锁代码块,方法,monitor);
    • volatile作用,实现原理(从可见性实现和原子);
    • Java反射实现(反射方式,类检查,加载过程);
    • JVM运行时区域,具体作用(常见八股文~);
    • FGC怎么发生的(YGC晋升老年代空间不足,分配担保空间不足,System.gc等);
      • system.gc一定会出发fgc吗(不一定,只是通知jvm);
  • MySQL
    • 存储结构(讲了熟悉的InnoDB引擎,索引B+树)
    • 为什么使用B+树?(树高问题,目录项节点存储更多关联孩子节点,双向链表范围查询,按页加载减少磁盘IO);
    • 事务,隔离级别,实现原理(MVCC, undoLog,版本链查找过程);
  • Canal
    • 工作原理(模拟Mysql slave交互协议,binlog主从同步,数据解析)
    • 底层架构(不清楚)
  • 设计模式
    • 熟悉的设计模式讲一下(模版方法),业务使用场景讲一下;

二面(20min)

  • 项目
    • 项目设计流程,角色,相关方,难点,怎么解决,领域知识怎么解决(项目一个个过了一遍);
    • 用到哪些设计模式,场景(讲了模版方法模式);
    • 平常学习哪些技术,源码(讲了MQ跟ES,没继续深问。。。)
  • Dubbo
    • 服务,A->B,并发调用(讲了调用线程池)
    • 超时熔断机制怎么实现(猜想,说讲了守护线程+计数器+容错机制),提示用并发包相关类(凉凉,不清楚,查了下是Future和Condition类);

三面(30min)

  • 项目
    • 自我介绍;
    • 挑一个认为最有挑战和成长的项目,业务内容存储结构/解析是怎样实现的;
    • 服务调用量,资源配置,实例数;
    • 当前就职公司规模,业务,研发比例;
  • Es
    • 存储结构(倒排索引),分词器,语言/引擎实现(Java+lucene);
    • 实现查找原理(请求发起,协调节点,路由,节点查询负载,分片冗余),ES分片,结构,是否为完整数据;
    • 怎么持久化的(内存buffer->transLog->OS Cache->Disk);
    • 内存热点数据保持(淘汰策略);
    • 如果数据分布在不同机器节点,怎么确保查询到指定节点(文档id路由),如果是根据关键字,一开始没有文档id怎么路由;
    • 怎么获取分词器分词结果(_analyze),如果分词结果不符合预期,怎么去调整,比如地址[浙江]被拆分为[浙][江])

笔试(20min)

  • 工程化编码题;

HR(40min)

  • 自我介绍;
  • 项目介绍,承担的角色,负责模块是什么,上下游;
  • 说一下你的优势,擅长的点;
  • 参与开发的产品优势,亮点有哪些,业务方角色有哪些;
  • 业务竞品有哪些,特点,差异,怎么去追赶;
  • 工作2年来遇到的困难是什么,怎么解决的,有什么收获;
  • 项目/业务创新方面是否有实践;
  • 你的性格怎样;
  • 政策监管怎么看,是否影响业务发展,怎么去提升;
  • 当前&期望薪资,是否有其他offer选择,对要去的部门有什么了解;

阿里

业务:Lazada, 东南亚电商,基础架构部/交易支付;

结论:HR太拖沓,信息沟通不对称,拒了;

一面(55min)

  • 项目
    • 自我介绍,认为比较有挑战性的项目,有什么问题,怎么解决的,方案设计;
  • Canal
    • 监听需要MySQL配置是怎样的(不大记得了,,瞎说了个Compact行格式?说忘记了。后面看了下,是开启binlog+ROW模式);
    • DTS同步一致性,延时处理措施,TPS多少,怎样去提升;
  • Es
    • 集群,怎么做节点扩容(告警+分片扩容);
  • MySQL:
    • ACID特性是怎样实现的(ACID含义,事务隔离不干扰,MVCC, 日志+刷盘持久化)
    • 常见的MySQL日志有哪些(undo log, redo log, binlog)
    • undo log作用(事务回滚撤销,MVCC,RollPointer指针引用)
    • redo log作用(顺序写入,持久化,减少16kb数据整页刷盘IO)
    • 插入一条数据是怎样写log的(SQL->BufferPool->undo log->redo log -> 事务提交->binlog ->redoLog commit);
    • 存储引擎索引结构(B+树结构,多路查找,叶子结点数据),时间复杂度多少(二分logN);
    • B树结构,为什么不用(数据非叶子结点,树高底,内存页与磁盘页交换效率高);
    • 锁类型,幻读是什么(T1多次查询,T2插入数据,导致T1结果集数量变多),怎么解决的(RecordLock, GapLock, NextKey-Lock);
  • Redis:
    • 数据结构(常见5种,string, list, set, zset, hash),还有呢(bloomFilter, Geo),业务使用场景;
  • Java:
    • Synchronized实现原理(对象头,Monitor对象,代码块:monitor enter, exit, 方法:acc_synchronized,隐式monitor);
    • 线程池ForkingJoin有没有了解过(不熟悉,生产中没用到,没继续问);
    • 当前用的JDK版本(8),相对于7有什么新特性(集合流式操作,lambda表达式,增强函数式编程);
  • Spring:
    • Spring-boot启动原理,spring.factories作用(key-value, 自动配置加载查找,注入bean);
    • 一个API要实现DB数据插入后,三方调用失败,数据回滚,怎么实现(加入到同一个Spring事务中,@Transactional)
    • 讲一下Spring事务传播行为(讲了Requied, SUPPORT, NESTED三种,还有个无事务时抛出异常,英文记不住名字。。。后来查了下:MANDATORY)
  • 算法:
    • 整型无序数组,查找第2大的数(一开始无脑两层for比较…..面试官要求一层且效率尽可能高,最后采用类似动归降维,定义最大两个数int num1, int num2, 一层for循环比较,维护num1, num2, 返回num2);

二面(55min)

  • 项目:
+ 自我介绍+项目介绍,难点,角色,事情,你在里面做什么;
  • Es:
+ 选型,跟solr, lucene对比,相同和不同点,出现问题一般是运维还是开发处理;
+ Es架构,讲了存储结构(倒排索引),分布式,冗余+查询(主副分片设计,查询路由,分片);
+ 有没有出现性能问题(DSL慢查询,同一集群其他业务资源占用),怎么处理(DSL优化,按优先级迁移部分业务),性能问题的根因是什么(CPU,内存占用,磁盘交换IO等);
+ ES节点扩容怎么做(印象中好像不能修改主分片数,说猜想增加节点,重新hash分片,路由迁移数据),是否会导致停机(不会),集群迁移有没有处理过,怎么实现;
+ ES跟MySQL怎么配合,选型区别,是否可以只有ES不使用MySQL(关系型数据库,非关系型数据库,看业务场景(比如分词搜索,数据量,查询需求),存储需求, 一般是结合使用DB+ES);
  • RocketMQ:
+ 使用场景(服务解耦,削峰);
+ 组件组成结构(producer+comsumer+nameServer+broker), 不同结构之间怎么实现通信的(nameServer->broker心跳,producer与nameServer进行broker发现,消息负载到broker端,comsumer消费)。
+ 10个broker20个消费者,10个broker5个消费者分别是怎么消费的(broker锁,消费负载);
+ 顺序消费实现原理(消息生产端队列路由,存储端FIFO,消费端加broker,queue锁消费),源码是否有读过;
  • 场景:
+ 支付-》订单(RPC)调用如何保证数据一致性,不考虑时延,调用时请求丢失场景怎么去检查和恢复(说了异步调用+轮询检查订单完成结果,或是订单完成后通知支付端,调用方重试等),面试官说有些复杂,最后说了个直接阻塞调用获取结果。。。
+ 业务最终一致性实现,落地场景(讲了DTS中利用MQ可靠消费+重试实现),2pc, 3pc,tcc的场景;
+ 订单ID生成器设计(业务含义,划分ID段,一部分可以使用雪花算法去生成唯一id,中间段加订单日期类业务号,性能角度,可以使用发号器一次性分配号段,减少频繁生成请求);
    - 雪花算法实现有了解吗(说了分配为几个段,机房唯一id,后面的组成细节不记得了)
  • 其他:
+ 平常学习情况,最近学了哪些,性格怎样,项目难度,怎么去做业务沟通,相关方有哪些等;

三面(40min)

  • 项目:
    • 项目介绍,领域划分和设计依据,难点,怎么解决,做了什么优化,结构层次,依赖组件原理,问的比较深入;
    • DB->ES数据异构一致性和延时问题,分别怎么解决;
  • ES:
    • 优化(硬件配置,业务索引设计,DSL优化三个角度展开);
    • 分片存储(备份HA+查询负载);
    • 大量写入/查询会导致什么问题,怎么解决(定时刷transLog,磁盘,做集群/分片负载);
    • Segment文件刷盘频繁怎么解决;
  • MQ:
    • 顺序消费实现原理(生产端,存储端,消费端),源码实现;
    • 消息存储结构&功能(CommitLog, ComsumerQueue,IndexFile);
    • MQ事务消息实现原理(实现过程,半事务消息流转);
  • Java:
    • 锁类型(Synchronized->JVM层面,Lock->AQS实现,展开);
    • 为什么ReentrantLock需要支持可重入,实现原理(再次获取竞态资源,同一线程安全,AQS State++—判断);
    • CMS跟G1, 异同点,生产实践选择;

四面(35min)

  • 项目:
    • 介绍,难点,怎么解决;
    • 英文沟通能力怎样;
    • 资金安全场景;
  • ES:
    • 搜索引擎选型,深分页场景优化,Scoll游标原理,日期范围查询;
    • DB->ES数据异构,一致性/延时处理,消息堆积优化策略,消息优先级判断;
    • 同步异常处理,容错(消费阻塞,监控+存储业务id到Redis定时重试);
  • MQ:
    • 可靠消费&Ack, rollback机制(ComsumeQueue消费位点确认);
    • broker宕机,数据一致性怎么保障(主从broker同步,异步方式,写log);
  • 缓存:
    • 击穿,穿透,雪崩场景,怎么解决(常见八股文~);
  • Canal:
    • 同步优化,自定义客户端MQ投递实现;

HR面

  • 时间沟通问题,放弃流程;

腾讯

业务:PCG, 电商;

结论:一面挂;

一面(85min)

  • 算法:
    • 三数之和: leetcode-cn.com/problems/3s…
  • 项目:
    • 架构,ES数据异构怎么做的,同步速度,延迟;
  • java:
    • JMM内存模型(当时讲成了JVM内存模型,,有些尴尬);
    • 线程池场景,模型,核心参数,涉及数据结构,原理,等待结构怎么实现的(问的比较细~);
    • Synchronized和Lock实现原理,区别,使用场景(常见八股文~);
  • CS基础:
    • 操作系统用户态和内核态,区别(概念~);
    • 零拷贝原理,常见实现(mmap,sendFile,列举了用户数据变更-CPU拷贝-DMA操作-缓冲区-网卡,过程);
  • Redis:
    • 数据结构;
    • Cluster集群实现,槽指派,新节点加入如何分配(槽指派,重新分配节点过程);
    • 渐进式hash迁移过程(H0,H1);
    • bloom过滤器实现(bit数组,多次hash函数);
  • MySQL:
    • 分库分表(拆分依据,时间/id),实践(代理式/应用层两种,对应中间件);
    • 常见MySQL锁,场景(X/S锁,record/表锁,IS/IX锁,GapLock);
    • 隔离级别,MVCC实现(常见八股文~);

最后

  • 面试是一场场对自己技术体系,知识储备,思考沟通,应变能力等等多方面的考验,过程中确实会遇到很多的困难,及时总结,不断改进,可以的;
  • 找到自己的目标,不断地赋予自己前行的动力;

在有限的青春里,做有意义的事,做让自己成长的事,对未来有价值的事 -空白女侠
山脚拥挤,我们高处见。

本文转载自: 掘金

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

Dubbo的架构与调用流程详解

发表于 2021-08-21

前言

本文主要对 Dubbo 的简介,架构等宏观方面对 Dubbo 进行介绍。

应用架构的演进

单体应用

早期的应用架构是大多是单体应用,随着互联网的快速发展以及数据量的急剧增长和业务的复杂度,很多企业会对应用进行垂直拆分,即把业务上没有关联的系统独立拆分出来,形成独立对外提供服务的系统。此时,服务之间完全独立,无法进行远程调用,很多基础代码不能复用,需要复制使用。为了解决这些存在的问题,衍生出了分布式应用。

分布式应用

SOA架构

为了解决单体应用出现的问题,面向服务的架构(SOA)出现了,SOA 将单一进程的应用做了拆分,形成独立对外提供服务的组件,每个组件通过网络协议对外提供服务。网络协议可以是TCP,也可以是 Http等,通过这种协议,服务之间可以通过接口进行交互。SOA有以下特点:

  • 明确的协议,服务间的交互通过特定的协议进行
  • 明确的接口,每个服务有明确的对外接口,让服务可以复用
  • 合作方式的改变,有了更加细化的分工与合作方式
  • 通信方式,初期通信方式通常为 XML,后来被 Json 取代

SOA 的实现方式有两种:Web Service 和 ESB,但这两种方式的缺点都显而易见,Web Service 的通信协议笨重,服务化管理设置不完善;ESB 本身就很重,系统的变更可能又反过来影响总线的变更。

微服务化

为了解决SOA中存在的各种问题,近几年的服务化架构得到了进一步演进,主键形成了更加细粒度的微服务架构,在微服务架构中,一个服务被拆分成一个个独立、可配置、可运行、可维护的子服务,极大的方便了服务的复用,通过不同的服务编排方式,快速产生新的业务逻辑。

Dubbo

在分布式场景下,很重要的分布式通信问题的解决方案之一就是 Dubbo,编写分布式场景下高并发、高可扩展的系统对技能的要求很高,因为其中涉及序列化/反序列化、网络、多线程、设计模式、性能优化等众多知识,Dubbo 可以很好的将这些做更高层的抽象和封装,提供了各种开箱即用的特性,让用户傻瓜式的使用。

架构


上图为Dubbo的架构,流程如下:

  • 服务的Container负责启动,加载并运行 Provider
  • Provider 在启动时会向注册中心把自己的元数据注册上去(服务Ip,Port等)
  • Consumer 启动时向注册中心订阅服务提供方的元数据
  • 注册中心将服务提供者地址列表给消费者,如发生数据变更会推送给订阅的 Consumer
  • 在获取元数据后,Consumer 可以发起 RPC 调用
  • 在 RPC 调用前后会向监控中心上报统计信息(并发数,调用接口等)

Dubbo解决的问题:

1、高性能、透明的RPC调用

2、服务的自动注册与发现

  • 自动负载与容错
  • 动态流量调度
  • 依赖分析与调用统计

分层

Dubbo的实现采用分层的思想,每层负责不同的职责,让用户可以基于Dubbo框架做二次开发,扩展其功能。Dubbo的扩展能力非常强,这也是Dubbo广受欢迎的原因。

Dubbo总体分为三层:业务层(Biz),RPC层,Remote层。如果把每一层细分,可以分为十层。

image.png

Dubbo框架中的分层代表了不同的逻辑实现,它们是一个个组件,组件构成了整个Dubbo体系,下面是Dubbo的核心组件。

image.png

总体调用过程

服务的暴露过程:

首先,服务器端(服务提供者)在框架启动时,会初始化服务实例,通过 Proxy 组件调用具体协议,把服务端要暴露的接口封装成Invoker (真实类型是AbstractProxyInvoker),然后转换成Exporter,这个时候框架会打开服务端口等并记录服务实例到内存中,最后通过Registry 把服务元数据注册到注册中心。这就是服务端(服务提供者)整个接口暴露的过程。

image.png

首先,调用过程也是从一个Proxy开始的,Proxy持有了一个Invoker对象。然后触发invoke调用。在invoke调用过程中,需要使用Cluster,Cluster 负责容错,如调用失败的重试。Cluster 在调用之前会通过 Directory获取所有可以调用的远程服务Invoker列表(一个接口可能有多个节点提供服务)。由于可以调用的远程服务有很多,此时如果用户配置了路由规则(如指定某些方法只能调用某个节点),那么还会根据路由规则将Invoker列表过滤一遍。

然后,存活下来的Invoker可能还会有很多,此时要调用哪一个呢?于是会继续通过LoadBalance方法做负载均衡,最终选出一个可以调用的Invoker。这个Invoker在调用之前又会经过一个过滤器链,这个过滤器链通常是处理上下文、限流、计数等。
接着,会使用Client做数据传输,如我们常见的Netty Client 等。传输之前肯定要做一些私有协议的构造,此时就会用到Codec接口。构造完成后,就对数据包做序列化(Serialization),然后传输到服务提供者端。服务提供者收到数据包,也会使用Codec处理协议头及一些半包、粘包等。处理完成后再对完整的数据报文做反序列化处理。

随后,这个Request会被分配到线程池(ThreadPool)中进行处理。Server 会处理这些Request,根据请求查找对应的Exporter(它内部持有了Invoker)。Invoker是被用装饰器模式一层一层套
了非常多Filter的,因此在调用最终的实现类之前,又会经过一个服务提供者端的过滤器链。

最终,得到了具体接口的真实实现并调用,再原路把结果返回。到此,一个完整的远程调用过程就结束了。

小结

本文介绍了应用架构的演进过程,还包括Dubbo的架构,分层,总体调用过程等。如对 Dubbo 感兴趣可继续关注本专栏。

本文转载自: 掘金

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

实战小技巧11:数组拷贝

发表于 2021-08-21

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

每天一个实战小技巧:数组拷贝

说实话,在实际的业务开发中,基本上很少很少很少…会遇到数组拷贝的场景,甚至是我们一般都不怎么用数组,List它不香嘛,为啥要用数组

现在问题来了,要实现数组拷贝,怎么整?

1. 基础写法

最简单直接的写法,那就是新建一个数组,一个一个拷贝进去,不就完事了么

1
2
3
4
5
java复制代码String[] data = new String[]{"1", "2", "3"};
String[] ans = new String[data.length];
for (int index = 0; index < data.length; index ++) {
ans[index] = data[index];
}

2. 借用容器中转

数组用起来有点麻烦,还是用容器舒爽,借助List来实现数组的拷贝,也就几行代码

1
2
3
4
java复制代码String[] data = new String[]{"1", "2", "3"};
List<String> list = Arrays.asList(data);
String[] out = new String[data.length];
list.toArray(out);

3. Array.copy

上面这个有点绕得远了, 直接使用Array.copy

1
2
java复制代码String[] data = new String[]{"1", "2", "3"};
String[] out = Arrays.copyOf(data, data.length);

4. System.arraycopy

除了上面的,还可以使用更基础的用法

1
2
3
java复制代码String[] data = new String[]{"1", "2", "3"};
String[] out = new String[data.length];
System.arraycopy(data, 0, out, 0, data.length);

如果有看过jdk源码的小伙伴,上面这个用法应该不会陌生,特别是在容器类,这种数组拷贝的方式比比可见

参数说明:

1
2
3
java复制代码public static native void arraycopy(Object src,  int  srcPos,
Object dest, int destPos,
int length);
  • src : 原数组
  • srcPos: 原数组用于拷贝的起始下标
  • dest: 拷贝后的数组
  • destPos: 目标数组的小标
  • length: 原数组中拷贝过去的数组长度

从上面的描述也能看出来,这个方法不仅能实现数组拷贝,还可以实现数组内指定片段的拷贝

系列博文:

  • 实战小技巧1:字符串占位替换-JDK版
  • 实战小技巧2:数组与list互转
  • 实战小技巧3:字符串与容器互转
  • 实战小技巧4:优雅的实现字符串拼接
  • 实战小技巧5:驼峰与下划线互转
  • 实战小技巧6:枚举的特殊用法
  • 实战小技巧7:排序比较需慎重
  • 实战小技巧8:容器的初始化大小指定
  • 实战小技巧9:List.subList使用不当StackOverflowError
  • 实战小技巧10:不可变容器

II. 其他

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

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

2. 声明

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

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

本文转载自: 掘金

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

Mysql 性能查询 show profile

发表于 2021-08-20

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

猫和老鼠.jpg

前言: 本篇文章 是我关于MySQL的第20篇文章,水平一般、能力有限。文章写的比较浅,适合新手来看。

show profile命令

在日常的开发中,我们经常会遇到一些慢查询sql。如果想要分析一下此sql占用的开销,就可以用show profile命令来查看。然后找到问题,并针对性的进行优化。这个命令主要功能是来查看sql各部分所占用的具体时间。

开启show profile

一般来说 show profile命令是默认禁用的,我们需要把它开启。

1
2
sql复制代码mysql> set profiling=1;
Query OK, 0 rows affected, 1 warning (0.00 sec)

开启之后,执行的所有语句都会被记录其详细的耗时。

内部逻辑是,在服务器进行一条查询时会解析内部的执行并记录到一张临时表中。并给一个id。

首先执行一条查询sql

1
2
3
sql复制代码mysql> select * from food;
+-------+--------+------+
10003 rows in set (0.02 sec)

查询执行记录

这条查询返回了10003行,用了0.02秒。我们执行show profiles;看一下所有的执行记录.

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码mysql> show profiles;
+----------+------------+----------------------------------------------------------------------------------+
| Query_ID | Duration | Query |
+----------+------------+----------------------------------------------------------------------------------+
| 1 | 0.00012475 | select * from food |
| 2 | 0.00017675 | SELECT DATABASE() |
| 3 | 0.00017900 | SELECT DATABASE() |
| 4 | 0.00087125 | show databases |
| 5 | 0.00093550 | show tables |
| 6 | 0.01715250 | select * from food |
+----------+------------+----------------------------------------------------------------------------------+
6 rows in set, 1 warning (0.00 sec)

最后执行的query_id 为6的就是我刚执行的sql;

查询具体执行时间

我们再根据这条的id来详细查询它.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sql复制代码mysql> show profile for query 6;
+--------------------------------+----------+
| Status | Duration |
+--------------------------------+----------+
| starting | 0.000082 |
| Executing hook on transaction | 0.000005 |
| starting | 0.000009 |
| checking permissions | 0.000007 |
| Opening tables | 0.000035 |
| init | 0.000005 |
| System lock | 0.000012 |
| optimizing | 0.000005 |
| statistics | 0.000016 |
| preparing | 0.000019 |
| executing | 0.016889 |
| end | 0.000021 |
| query end | 0.000010 |
| closing tables | 0.000012 |
| freeing items | 0.000012 |
| cleaning up | 0.000015 |
+--------------------------------+----------+
16 rows in set, 1 warning (0.00 sec)

下面是详细的过程解释

  • starting //开始
  • checking permissions //检查权限
  • Opening tables //打开数据表
  • init //初始化
  • System lock //锁机制
  • optimizing //优化器
  • statistics //分析语法树
  • prepareing //预准备
  • executing //引擎执行开始
  • end //引擎执行结束
  • query end //查询结束
  • closing tables //释放数据表
  • freeing items //释放内存
  • cleaning up //彻底清理

参考文档

《高性能mysql》

本文转载自: 掘金

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

Springboot 结合 Netty 实战聊天系统

发表于 2021-08-20

音视频技术为什么需要微服务

微服务,英文名:microservice,百度百科上将其定义为:SOA 架构的一种变体。微服务(或微服务架构)是一种将应用程序构造为一组低耦合的服务。

微服务有着一些鲜明的特点:

  • 功能单一
  • 服务粒度小
  • 服务间独立性强
  • 服务间依赖性弱
  • 服务独立维护
  • 服务独立部署

对于每一个微服务来说,其提供的功能应该是单一的;其粒度很小的;它只会提供某一业务功能涉及到的相关接口。如:电商系统中的订单系统、支付系统、产品系统等,每一个系统服务都只是做该系统独立的功能,不会涉及到不属于它的功能逻辑。

微服务之间的依赖性应该是尽量弱的,这样带来的好处是:不会因为单一系统服务的宕机,而导致其它系统无法正常运行,从而影响用户的体验。同样以电商系统为例:用户将商品加入购物车后,提交订单,这时候去支付,发现无法支付,此时,可以将订单进入待支付状态,从而防止订单的丢失和用户体验的不友好。如果订单系统与支付系统的强依赖性,会导致订单系统一直在等待支付系统的回应,这样会导致用户的界面始终处于加载状态,从而导致用户无法进行任何操作。

当出现某个微服务的功能需要升级,或某个功能需要修复 bug 时,只需要把当前的服务进行编译、部署即可,不需要一个个打包整个产品业务功能的巨多服务,独立维护、独立部署。

上面描述的微服务,其实突出其鲜明特性:高内聚、低耦合,问题来了。什么是高内聚,什么是低耦合呢?所谓高内聚:就是说每个服务处于同一个网络或网域下,而且相对于外部,整个的是一个封闭的、安全的盒子。盒子对外的接口是不变的,盒子内部各模块之间的接口也是不变的,但是各模块内部的内容可以更改。模块只对外暴露最小限度的接口,避免强依赖关系。增删一个模块,应该只会影响有依赖关系的相关模块,无关的不应该受影响。

所谓低耦合:从小的角度来看,就是要每个 Java 类之间的耦合性降低,多用接口,利用 Java 面向对象编程思想的封装、继承、多态,隐藏实现细节。从模块之间来讲,就是要每个模块之间的关系降低,减少冗余、重复、交叉的复杂度,模块功能划分尽可能单一。

在音视频应用技术中,我们知道其实主要占用的资源是 cpu、memory,而且涉及到资源的共享问题,所以需要结合 NFS 来实现跨节点的资源共享。当然,单节点暴露的问题是,如果一旦客户端与服务器保持长时间的连接,而且,不同客户端同时发送请求,此时,单节点的压力是很大的。很有可能导致 cpu、memory 吃紧,从而导致节点的 crash,这样,不利于系统的高可用、服务的健壮性。此时,需要解决的是音视频通信中的资源吃紧的问题,在系统领域,通常可以采用多节点的方式,来实现分布式、高并发请求,当请求过来时,可以通过负载均衡的方式,通过一定的策略,如:根据最小请求数,或为每一个服务器赋予一个权重值,服务器响应时间越长,这个服务器的权重就越小,被选中的几率就会降低。这样来控制服务请求压力,从而让客户端与服务器能够保持长时间、有效的进行通信。

如何使用 Springboot 框架搭建微服务

介绍

这几年的快速发展,微服务已经变得越来越流行。其中,Spring Cloud 一直在更新,并被大部分公司所使用。代表性的有 Alibaba,2018 年 11 月左右,Spring Cloud 联合创始人 Spencer Gibb 在 Spring 官网的博客页面宣布:阿里巴巴开源 Spring Cloud Alibaba,并发布了首个预览版本。随后,Spring Cloud 官方 Twitter 也发布了此消息。

在 Spring Boot1.x 中,主要包括 Eureka、Zuul、Config、Ribbon、Hystrix 等。而在 Spring Boot2.x 中,网关采用了自己的 Gateway。当然在 Alibaba 版本中,其组件更是丰富:使用 Alibaba 的 Nacos 作为注册中心和配置中心。使用自带组件 Sentinel 作为限流、熔断神器。

搭建注册中心

我们今天主要来利用 Springboot 结合阿里巴巴的插件来实现微聊天系统的微服务设计。首先先来创建一个注册中心 Nacos。

我们先下载 Nacos,Nacos 地址:github.com/alibaba/nac…我们下载对应系统的二进制文件后,对应自己的系统,执行如下命令:

1
2
3
复制代码Linux/Unix/Mac:sh startup.sh -m standalone

Windows:cmd startup.cmd -m standalone

启动完成之后,访问:http://127.0.0.1:8848/nacos/,可以进入 Nacos 的服务管理页面,具体如下:

img

默认用户名与密码都是 nacos。

登陆后打开服务管理,可以看到注册到 Nacos 的服务列表:

img

可以点击配置管理,查看配置:

img

如果没有配置任何服务的配置,可以新建:

img

上面讲述了 Nacos 如何作为注册中心与配置中心的,很简单吧。

第一个微服务

接下来,对于微服务,那需要有一个服务被注册与被发现,我们讲解服务提供者代码:

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
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
xml复制代码<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>


<groupId>com.damon</groupId>
<artifactId>provider-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>


<name>provider-service</name>
<url>http://maven.apache.org</url>


<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath/>
</parent>


<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<swagger.version>2.6.1</swagger.version>
<xstream.version>1.4.7</xstream.version>
<pageHelper.version>4.1.6</pageHelper.version>
<fastjson.version>1.2.51</fastjson.version>
<!-- <springcloud.version>2.1.8.RELEASE</springcloud.version> -->
<springcloud.version>Greenwich.SR3</springcloud.version>
<springcloud.kubernetes.version>1.1.1.RELEASE</springcloud.kubernetes.version>
<mysql.version>5.1.46</mysql.version>

<alibaba-cloud.version>2.1.1.RELEASE</alibaba-cloud.version>
<springcloud.alibaba.version>0.9.0.RELEASE</springcloud.alibaba.version>
</properties>


<dependencyManagement>
<dependencies>
<!-- <dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${alibaba-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency> -->


<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${springcloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${springcloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>


<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>


<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>

<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>


<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>

<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
</dependency>


<!--分页插件-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>${pageHelper.version}</version>
</dependency>

<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>

<!-- datasource pool-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.3</version>
</dependency>

<!-- 对redis支持,引入的话项目缓存就支持redis了,所以必须加上redis的相关配置,否则操作相关缓存会报异常 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>

</dependencies>


<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<jvmArguments>-Dfile.encoding=UTF-8</jvmArguments>
<fork>true</fork>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.8</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>

<!-- 自动生成代码 插件 begin -->
<!-- <plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.2</version>
<configuration>
<configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
<verbose>true</verbose>
<overwrite>true</overwrite>
</configuration>
<dependencies>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
</plugin> -->
</plugins>
</build>
</project>

一如既往的引入依赖,配置 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
104
105
106
107
108
109
110
111
112
113
yaml复制代码management:
endpoint:
restart:
enabled: true
health:
enabled: true
info:
enabled: true

spring:
application:
name: provider-service

cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
config:
server-addr: 127.0.0.1:8848
refreshable-dataids: actuator.properties,log.properties

http:
encoding:
charset: UTF-8
enabled: true
force: true
mvc:
throw-exception-if-no-handler-found: true
main:
allow-bean-definition-overriding: true #当遇到同样名称时,是否允许覆盖注册



logging:
path: /data/${spring.application.name}/logs


cas-server-url: http://oauth-cas #http://localhost:2000#设置可以访问的地址


security:
oauth2: #与cas对应的配置
client:
client-id: provider-service
client-secret: provider-service-123
user-authorization-uri: ${cas-server-url}/oauth/authorize #是授权码认证方式需要的
access-token-uri: ${cas-server-url}/oauth/token #是密码模式需要用到的获取 token 的接口
resource:
loadBalanced: true
#jwt: #jwt存储token时开启
#key-uri: ${cas-server-url}/oauth/token_key
#key-value: test_jwt_sign_key
id: provider-service
#指定用户信息地址
user-info-uri: ${cas-server-url}/api/user #指定user info的URI,原生地址后缀为/auth/user
prefer-token-info: false
#token-info-uri:
authorization:
check-token-access: ${cas-server-url}/oauth/check_token #当此web服务端接收到来自UI客户端的请求后,需要拿着请求中的 token 到认证服务端做 token 验证,就是请求的这个接口
application 文件;

server:
port: 2001
undertow:
accesslog:
enabled: false
pattern: combined
servlet:
session:
timeout: PT120M
cookie:
name: PROVIDER-SERVICE-SESSIONID #防止Cookie冲突,冲突会导致登录验证不通过



client:
http:
request:
connectTimeout: 8000
readTimeout: 30000

mybatis:
mapperLocations: classpath:mapper/*.xml
typeAliasesPackage: com.damon.*.model



backend:
ribbon:
client:
enabled: true
ServerListRefreshInterval: 5000


ribbon:
ConnectTimeout: 3000
# 设置全局默认的ribbon的读超时
ReadTimeout: 1000
eager-load:
enabled: true
clients: oauth-cas,consumer-service
MaxAutoRetries: 1 #对第一次请求的服务的重试次数
MaxAutoRetriesNextServer: 1 #要重试的下一个服务的最大数量(不包括第一个服务)
#listOfServers: localhost:5556,localhost:5557
#ServerListRefreshInterval: 2000
OkToRetryOnAllOperations: true
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule




hystrix.command.BackendCall.execution.isolation.thread.timeoutInMilliseconds: 5000
hystrix.threadpool.BackendCallThread.coreSize: 5

接下来启动类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
kotlin复制代码package com.damon;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;




/**
* @author Damon
* @date 2020年1月13日 下午3:23:06
*
*/


@Configuration
@EnableAutoConfiguration
@ComponentScan(basePackages = {"com.damon"})
@EnableDiscoveryClient
@EnableOAuth2Sso
public class ProviderApp {


public static void main(String[] args) {
SpringApplication.run(ProviderApp.class, args);
}


}

注意:注解 @EnableDiscoveryClient、@EnableOAuth2Sso 都需要。

这时,同样需要配置 ResourceServerConfig、SecurityConfig。

如果需要数据库,可以加上:

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
java复制代码package com.damon.config;

import java.util.Properties;
import javax.sql.DataSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.EnableTransactionManagement;


import com.alibaba.druid.pool.DruidDataSourceFactory;
import com.github.pagehelper.PageHelper;


/**
*
*
* created by Damon
* 2018年5月23日 下午7:39:37
*
*/
@Component
@Configuration
@EnableTransactionManagement
@MapperScan("com.damon.*.dao")
public class MybaitsConfig {


@Autowired
private EnvConfig envConfig;

@Autowired
private Environment env;


@Bean(name = "dataSource")
public DataSource getDataSource() throws Exception {
Properties props = new Properties();
props.put("driverClassName", envConfig.getJdbc_driverClassName());
props.put("url", envConfig.getJdbc_url());
props.put("username", envConfig.getJdbc_username());
props.put("password", envConfig.getJdbc_password());
return DruidDataSourceFactory.createDataSource(props);
}


@Bean
public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) throws Exception {


SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
// 指定数据源(这个必须有,否则报错)
fb.setDataSource(dataSource);
// 下边两句仅仅用于*.xml文件,如果整个持久层操作不需要使用到xml文件的话(只用注解就可以搞定),则不加
fb.setTypeAliasesPackage(env.getProperty("mybatis.typeAliasesPackage"));// 指定基包
fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(env.getProperty("mybatis.mapperLocations")));// 指定xml文件位置


// 分页插件
PageHelper pageHelper = new PageHelper();
Properties props = new Properties();
// 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页
//禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据
props.setProperty("reasonable", "true");
//指定数据库
props.setProperty("dialect", "mysql");
//支持通过Mapper接口参数来传递分页参数
props.setProperty("supportMethodsArguments", "true");
//总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page
props.setProperty("returnPageInfo", "check");
props.setProperty("params", "count=countSql");
pageHelper.setProperties(props);
// 添加插件
fb.setPlugins(new Interceptor[] { pageHelper });


try {
return fb.getObject();
} catch (Exception e) {
throw e;
}
}


/**
* 配置事务管理器
* @param dataSource
* @return
* @throws Exception
*/
@Bean
public DataSourceTransactionManager transactionManager(DataSource dataSource) throws Exception {
return new DataSourceTransactionManager(dataSource);
}

@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}


}

接下来新写一个 controller 类:

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
kotlin复制代码package com.damon.user.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


import com.damon.commons.Response;
import com.damon.user.service.UserService;


/**
*
*
* @author Damon
* @date 2020年1月13日 下午3:31:07
*
*/
@RestController
@RequestMapping("/api/user")
public class UserController {

private static final Logger logger = LoggerFactory.getLogger(UserController.class);

@Autowired
private UserService userService;

@GetMapping("/getCurrentUser")
@PreAuthorize("hasAuthority('admin')")
public Object getCurrentUser(Authentication authentication) {
logger.info("test password mode");
return authentication;
}


@PreAuthorize("hasAuthority('admin')")
@GetMapping("/auth/admin")
public Object adminAuth() {
logger.info("test password mode");
return "Has admin auth!";
}

@GetMapping(value = "/get")
@PreAuthorize("hasAuthority('admin')")
//@PreAuthorize("hasRole('admin')")//无效
public Object get(Authentication authentication){
//Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
authentication.getCredentials();
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails)authentication.getDetails();
String token = details.getTokenValue();
return token;
}

@GetMapping("/getUserInfo")
@PreAuthorize("hasAuthority('admin')")
public Response<Object> getUserInfo(Authentication authentication) {
logger.info("test password mode");
Object principal = authentication.getPrincipal();
if(principal instanceof String) {
String username = (String) principal;
return userService.getUserByUsername(username);
}
return null;
}


}

基本上一个代码就完成了。接下来测试一下:

认证:

1
bash复制代码curl -i -X POST -d "username=admin&password=123456&grant_type=password&client_id=provider-service&client_secret=provider-service-123" http://localhost:5555/oauth-cas/oauth/token

拿到 token 后:

1
bash复制代码curl -i -H "Accept: application/json" -H "Authorization:bearer f4a42baa-a24a-4342-a00b-32cb135afce9" -X GET http://localhost:5555/provider-service/api/user/getCurrentUser

这里用到了 5555 端口,这是一个网关服务,好吧,既然提到这个,我们接下来看网关吧,引入依赖:

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
xml复制代码<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>


<groupId>com.damon</groupId>
<artifactId>alibaba-gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>


<name>alibaba-gateway</name>
<url>http://maven.apache.org</url>


<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath/>
</parent>


<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<swagger.version>2.6.1</swagger.version>
<xstream.version>1.4.7</xstream.version>
<pageHelper.version>4.1.6</pageHelper.version>
<fastjson.version>1.2.51</fastjson.version>
<!-- <springcloud.version>2.1.8.RELEASE</springcloud.version> -->
<springcloud.version>Greenwich.SR3</springcloud.version>
<springcloud.kubernetes.version>1.1.1.RELEASE</springcloud.kubernetes.version>
<mysql.version>5.1.46</mysql.version>

<alibaba-cloud.version>2.1.1.RELEASE</alibaba-cloud.version>
<springcloud.alibaba.version>0.9.0.RELEASE</springcloud.alibaba.version>
</properties>


<dependencyManagement>
<dependencies>
<!-- <dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${alibaba-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency> -->


<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${springcloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${springcloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>


<dependencies>
<!-- 不要依赖spring-boot-starter-web,会和spring-cloud-starter-gateway冲突,启动时异常 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

<!--基于 reactive stream 的redis -->
<!-- <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency> -->

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>


<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>


<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>

</dependencies>

<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<jvmArguments>-Dfile.encoding=UTF-8</jvmArguments>
<fork>true</fork>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.8</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>

<!-- 自动生成代码 插件 begin -->
<!-- <plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.2</version>
<configuration>
<configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
<verbose>true</verbose>
<overwrite>true</overwrite>
</configuration>
<dependencies>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
</plugin> -->
</plugins>
</build>
</project>

同样利用 Nacos 来发现服务。

这里的注册配置为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
yaml复制代码spring:
cloud:
gateway:
discovery:
locator:
enabled: true #并且我们并没有给每一个服务单独配置路由 而是使用了服务发现自动注册路由的方式
lowerCaseServiceId: true

nacos:
discovery:
server-addr: 127.0.0.1:8848
config:
server-addr: 127.0.0.1:8848
refreshable-dataids: actuator.properties,log.properties

前面用的是 kubernetes。

好了,网关配置好后,启动在 Nacos dashboard 可以看到该服务,表示注册服务成功。接下来就可以利用其来调用其他服务了。具体 curl 命令:

1
bash复制代码curl -i -H "Accept: application/json" -H "Authorization:bearer f4a42baa-a24a-4342-a00b-32cb135afce9" -X GET http://localhost:5555/consumer-service/api/order/getUserInfo

Ok,到此鉴权中心、服务提供者、服务消费者、服务的注册与发现、配置中心等功能已完成。

为什么选择 Netty 作为即时通信的技术框架

简介

Netty 是一个高性能、异步事件驱动的 NIO 框架,它提供了对 TCP、UDP 和文件传输的支持。作为当前最流行的 NIO 框架,Netty 在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用。

特点

  • 高并发
  • 传输快
  • 封装好

Netty 通信的优势

Netty 是一个高性能、高可拓展性的异步事件驱动的网络应用程序框架,极大地简化了 TCP 和 UDP 客户端和服务器端开发等网络编程,它的四个重要内容:

  • 内存管理:增强 ByteBuf 缓冲区
  • Reactor 线程模型:一种高性能的多线程程序设计
  • 增强版的通道 channel 概念
  • ChannelPipeline 责任链设计模式:事件处理机制

Netty 实现了 Reactor 线程模型,Reactor 模型有四个核心概念:Resources 资源(请求/任务)、Synchronous Event Demultiplexer 同步事件复用器、Dispatcher 分配器、Request Handler 请求处理器。主要是通过 2 个 EventLoopGroup(线程组,底层是 JDK 的线程池)来分别处理连接和数据读取,从而提高线程的利用率。

Netty 中的 Channel 是一个抽象的概念,可以理解为对 JDK NIO Channel 的增强和拓展。增加了很多属性和方法。

ChannelPipeline 责任链保存了通道所有处理器信息。创建新 channel 时自动创建一个专有的 pipeline,并且在对应入站事件(通常指 I/O 线程生成了入站数据,详见 ChannelInboundHandler)和出站事件(经常是指 I/O 线程执行实际的输出操作,详见 ChannelOutboundHandler)时调用 pipeline 上的处理器。当入站事件时,执行顺序是 pipeline 的 first 执行到 last。当出站事件时,执行顺序是 pipeline 的 last 执行到 first。处理器在 pipeline 中的顺序由添加的时候决定。

JDK 的 ByteBuffer 存在如无法动态扩容、API 使用复杂的问题,Netty 自己的 ByteBuf 解决了其问题。ByteBuf 实现了四个方面的增强:API 操作便捷,动态扩容,多种 ByteBuf 实现,高效的零拷贝机制。

实现一个简单的 Netty 客户端、服务器通信

实战服务端

前面介绍了 Netty 在音视频流域实践的优势与特点,接下来,我们先写一个服务端。首先创建一个 Java 项目:

img

创建项目后,我们需要引入基础依赖:

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
xml复制代码<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.damon</groupId>
<artifactId>netty-client-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>netty-client-service</name>
<url>http://maven.apache.org</url>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/>
</parent>

<properties>
<java.version>1.8</java.version>
<spring-boot.version>2.1.1.RELEASE</spring-boot.version>
<springcloud.kubernetes.version>1.0.1.RELEASE</springcloud.kubernetes.version>
<springcloud.version>2.1.1.RELEASE</springcloud.version>
<swagger.version>2.6.1</swagger.version>
<fastjson.version>1.2.51</fastjson.version>
<pageHelper.version>4.1.6</pageHelper.version>
<protostuff.version>1.0.10</protostuff.version>
<objenesis.version>2.4</objenesis.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<type>pom</type>
<scope>import</scope>
<version>${spring-boot.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<!-- <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-core</artifactId>
<version>${springcloud.kubernetes.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-discovery</artifactId>
<version>${springcloud.kubernetes.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-ribbon</artifactId>
<version>${springcloud.kubernetes.version}</version>
</dependency> -->

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
<version>${springcloud.version}</version>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>

<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.11.3</version>
</dependency>

<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>

<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
</dependency>

<!-- mybatis -->
<!-- <dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency> -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<!-- <dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>1.4.1</version>
</dependency> -->
<!-- <dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>opencv-platform</artifactId>
<version>3.4.1-1.4.1</version>
</dependency> -->

<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.64.Final</version>
</dependency>

<!-- protobuf -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.5.0</version>
</dependency>
<dependency>
<groupId>com.googlecode.protobuf-java-format</groupId>
<artifactId>protobuf-java-format</artifactId>
<version>1.2</version>
</dependency>

<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>${protostuff.version}</version>
</dependency>
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>${protostuff.version}</version>
</dependency>
<dependency>
<groupId>org.objenesis</groupId>
<artifactId>objenesis</artifactId>
<version>${objenesis.version}</version>
</dependency>

</dependencies>
</project>

服务启动类:

1
2
3
4
5
6
7
8
9
less复制代码@EnableScheduling
@SpringBootApplication(scanBasePackages = { "com.damon" })
public class StorageServer {

public static void main(String[] args) {
SpringApplication.run(StorageServer.class, args);
}

}

首先启动 netty 服务时,只需要我们添加 Netty 的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码spring.application.name=netty-server
server.port=2002
netty.host=127.0.0.1
netty.port=9999

logging.path=/data/${spring.application.name}/logs
spring.profiles.active=dev

spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true
spring.mvc.throw-exception-if-no-handler-found=true

server.undertow.accesslog.enabled=false
server.undertow.accesslog.pattern=combined

client.http.request.readTimeout=30000
client.http.request.connectTimeout=8000

添加完配置,我们可以启动服务看看,这时候有日志:

img

添加完 netty 服务配置后,这里需要注入一个 Server Handle,用来当客户端主动链接服务端的链接后,这时候,该处理类会被触发,从而执行一些消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
SocketChannel channel = (SocketChannel) ctx.channel();
logger.info("链接报告开始");
logger.info("链接报告信息:有一客户端链接到本服务端");
logger.info("链接报告IP:{}", channel.localAddress().getHostString());
logger.info("链接报告Port:{}", channel.localAddress().getPort());
logger.info("链接报告完毕");
ChannelHandler.channelGroup.add(ctx.channel());
// 通知客户端链接建立成功
String str = "通知客户端链接建立成功" + " " + new Date() + " " + channel.localAddress().getHostString() + "\r\n";
ByteBuf buf = Unpooled.buffer(str.getBytes().length);
buf.writeBytes(str.getBytes("GBK"));
ctx.writeAndFlush(buf);
}

意思就是说,假如这时候有个客户端连接服务端时,会被打印一些信息,这里是我提前加入客户端后打印的结果:

img

当客户端主动断开服务端的链接后,这个通道就是不活跃的。也就是说客户端与服务端的关闭了通信通道并且不可以传输数据:

1
2
3
4
5
less复制代码@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
logger.info("客户端断开链接{}", ctx.channel().localAddress().toString());
ChannelHandler.channelGroup.remove(ctx.channel());
}

当然获取数据函数在这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws UnsupportedEncodingException {

if(msg instanceof ByteBuf) {
ByteBuf buf = (ByteBuf) msg;
byte[] msgByte = new byte[buf.readableBytes()];
buf.readBytes(msgByte);
System.out.println(new String(msgByte, Charset.forName("GBK")));

//通知客户端链消息发送成功
String str = "服务端收到:" + new Date() + " " + new String(msgByte, Charset.forName("GBK")) + "\r\n";
ByteBuf buf2 = Unpooled.buffer(str.getBytes().length);
buf2.writeBytes(str.getBytes("GBK"));
ctx.writeAndFlush(buf2);
}
}

如果出现异常,抓住异常,当发生异常的时候,可以做一些相应的处理,比如打印日志、关闭链接**:**

1
2
3
4
5
java复制代码@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
logger.info("异常信息:\r\n" + cause.getMessage());
}

此外,在服务端,一般需要定义一些信息协议信息,如:连接的信息,是自发信息还是群发信息,通信管道是哪个,还有通信信息等:

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

private int type; //链接信息;1自发信息、2群发消息
private String channelId; //通信管道ID,实际使用中会映射成用户名
private String userHeadImg; //用户头像[模拟分配]
private String msgInfo; //通信消息

public int getType() {
return type;
}

public void setType(int type) {
this.type = type;
}

public String getChannelId() {
return channelId;
}

public void setChannelId(String channelId) {
this.channelId = channelId;
}

public String getUserHeadImg() {
return userHeadImg;
}

public void setUserHeadImg(String userHeadImg) {
this.userHeadImg = userHeadImg;
}

public String getMsgInfo() {
return msgInfo;
}

public void setMsgInfo(String msgInfo) {
this.msgInfo = msgInfo;
}
}

以上,就是一个简单的服务端,梳理一下还是比较清晰的。

实战客户端

接下来,我们看看客户端是如何连接服务端,并且与其通信的呢?客户端要想与服务端通信,首先肯定需要与服务端进行连接,这里加一个配置服务端 NIO 线程组:

1
2
ini复制代码private EventLoopGroup workerGroup = new NioEventLoopGroup();
private Channel channel;

连接服务端的逻辑是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ini复制代码public ChannelFuture connect(String inetHost, int inetPort) {
ChannelFuture channelFuture = null;
try {
Bootstrap b = new Bootstrap();
b.group(workerGroup);
b.channel(NioSocketChannel.class);
b.option(ChannelOption.AUTO_READ, true);
b.handler(new MyChannelInitializer());
channelFuture = b.connect(inetHost, inetPort).syncUninterruptibly();
this.channel = channelFuture.channel();
channel.closeFuture();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != channelFuture && channelFuture.isSuccess()) {
System.out.println("demo-netty client start done.");
} else {
System.out.println("demo-netty client start error.");
}
}
return channelFuture;
}

接下来再看如何销毁连接:

1
2
3
4
5
csharp复制代码public void destroy() {
if (null == channel) return;
channel.close();
workerGroup.shutdownGracefully();
}

最后,我们来连接到服务端:

1
scss复制代码new NettyClient().connect("127.0.0.1", 9999);

由于前面我们的服务端的 netty 的 ip 与端口设置为:本地,9999 端口,这里直接配置。

同样的,客户端如果需要接收数据信息,也需要定义如何在管道中进行接收:

1
2
3
4
5
6
7
8
9
scala复制代码public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {

@Override
protected void initChannel(SocketChannel channel) throws Exception {
// 在管道中添加我们自己的接收数据实现方法
channel.pipeline().addLast(new MyClientHandler());
}

}

当客户端主动链接服务端的链接后,这个通道就是活跃的了。也就是客户端与服务端建立了通信通道并且可以传输数据:

1
2
3
4
5
6
7
8
9
csharp复制代码@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
SocketChannel channel = (SocketChannel) ctx.channel();
System.out.println("链接报告开始");
System.out.println("链接报告信息:本客户端链接到服务端。channelId:" + channel.id());
System.out.println("链接报告IP:" + channel.localAddress().getHostString());
System.out.println("链接报告Port:" + channel.localAddress().getPort());
System.out.println("链接报告完毕");
}

当客户端主动断开服务端的链接后,这个通道就是不活跃的。也就是说客户端与服务端的关闭了通信通道并且不可以传输数据:

1
2
3
4
5
java复制代码@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("断开链接" + ctx.channel().localAddress().toString());
super.channelInactive(ctx);
}

遇到异常时,抓住异常,当发生异常的时候,可以做一些相应的处理,比如打印日志、关闭链接:

1
2
3
4
5
java复制代码@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
System.out.println("异常信息:\r\n" + cause.getMessage());
}

客户端连接服务端、处理接收服务端发送的信息、异常处理等完成后,这时候,我们来启动客户端,客户端控制面会打印如下信息:

img

如果客户端主动断开连接时,这时候,服务端会提示:

1
2
yaml复制代码远程主机强迫关闭了一个现有的连接。
2021-05-13 19:33:35.691 INFO 148736 --- [ntLoopGroup-3-2] com.leinao.handler.ServerHandler : 客户端断开链接/127.0.0.1:9999

到此,一个简单的 Netty 客户端、服务端的通信就完成了。

微服务 Springboot 下实战聊天系统

在前面介绍了一个简单的 Netty 客户端、服务端通信的示例,接下来,我们开始实战聊天系统。

websocket 服务端启动类

基于前面讲的 Netty 的特性,这里聊天室需要前、后端。那么,首先对于后端,我们需要创建一个 Websocket Server,这里需要有一对线程组 EventLoopGroup,定义完后,需要定义一个 Server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
scss复制代码public static void main(String[] args) throws Exception {

EventLoopGroup mainGroup = new NioEventLoopGroup();
EventLoopGroup subGroup = new NioEventLoopGroup();

try {
ServerBootstrap server = new ServerBootstrap();
server.group(mainGroup, subGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new WSServerInitialzer());

ChannelFuture future = server.bind(8088).sync();

future.channel().closeFuture().sync();
} finally {
mainGroup.shutdownGracefully();
subGroup.shutdownGracefully();
}
}

将线程组加入 Server,接下来,需要设置一个 channel:NioServerSocketChannel,还有一个初始化器:WSServerInitialzer。

第二步,需要对 Server 进行端口版绑定:

1
ini复制代码ChannelFuture future = server.bind(8088).sync()

最后,需要对 future 进行监听。而且监听结束后需要对线程资源进行关闭:

1
2
ini复制代码mainGroup.shutdownGracefully();
subGroup.shutdownGracefully();

websocket 子处理器 initialzer

上面说了 WebSocket Server,那么对于 socket,有一个初始化处理器,这里我们来定义一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scala复制代码public class WSServerInitialzer extends ChannelInitializer<SocketChannel> {

@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new HttpObjectAggregator(1024*64));

pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));

pipeline.addLast(new ChatHandler());
}

}

由于 websocket 是基于 http 协议,所以需要有 http 的编解码器 HttpServerCodec,同时,在一些 http 上,有一些数据流的处理,而且,数据流有大有小,那么可以添加一个大数据流的处理:ChunkedWriteHandler。

通常,会有对 httpMessage 进行聚合,聚合成 FullHttpRequest 或 FullHttpResponse,而且,几乎在 netty 中的编程,都会使用到此 hanler。

另外,websocket 服务器处理的协议,用于指定给客户端连接访问的路由 : “/ws”,本 handler 会帮你处理一些繁重的复杂的事,比如,会帮你处理握手动作: handshaking(close, ping, pong) ping + pong = 心跳。对于 websocket 来讲,都是以 frames 进行传输的,不同的数据类型对应的 frames 也不同。

最后,我们自定义了一个处理消息的 handler:ChatHandler。

chatHandler 对消息的处理

在 Netty 中,有一个用于为 websocket 专门处理文本的对象 TextWebSocketFrame,frame 是消息的载体。

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复制代码public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg)
throws Exception {
String content = msg.text();
System.out.println("接受到的数据:" + content);

clients.writeAndFlush(new TextWebSocketFrame("服务器时间在 " + LocalDateTime.now() + " 接受到消息, 消息为:" + content));

}

@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
clients.add(ctx.channel());
System.out.println("客户端连接,channle对应的长id为:" + ctx.channel().id().asLongText());
System.out.println("客户端连接,channle对应的短id为:" + ctx.channel().id().asShortText());
}

@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
System.out.println("客户端断开,channle对应的长id为:" + ctx.channel().id().asLongText());
System.out.println("客户端断开,channle对应的短id为:" + ctx.channel().id().asShortText());
}
}

一开始消息在载体 TextWebSocketFrame 中,这时候可以直接拿到其中的内容,并且打印出来。而且可以把消息发到对应请求的客户端。当然,也可以把消息转发给所有的客户端,这就涉及到 Netty 中的 channel。这时候,需要管理 channel 中的用户,这样才能把消息转发到所有 channel 的用户。也就是上面的 handlerAdded 函数,当客户端连接服务端之后打开连接,获取客户端的 channle,并且放到 ChannelGroup 中去进行管理。同时,客户端与服务端断开、关闭连接后,会触发 handlerRemoved 函数,同时 ChannelGroup 会自动移除对应客户端的 channel。

接下来,需要把数据获取后刷新到所有客户端:

1
2
3
less复制代码for (Channel channel : clients) {
channel.writeAndFlush(new TextWebSocketFrame("[服务器在]" + LocalDateTime.now() + "接受到消息, 消息为:" + content));
}

注意:这里需要借助于载体来把信息 Flush,因为 writeAndFlush 函数是需要传对象载体,而不是直接字符串。其实同样,作为 ChannelGroup clients,其本身提供了 writeAndFlush 函数,可以直接输出到所有客户端:

1
less复制代码clients.writeAndFlush(new TextWebSocketFrame("服务器时间在 " + LocalDateTime.now() + " 接受到消息, 消息为:" + content));

基于 js 的 websocket 相关 api 介绍

首先,需要一个客户端与服务端的连接,这个连接桥梁在 js 中就是一个 socket:

1
ini复制代码var socket = new WebSocket("ws://192.168.174.145:8088/ws");

再来看看其生命周期,在后端,channel 有其生命周期,而前端 socket 中:

  • onopen(),当客户端与服务端建立连接时,就会触发 onopen 事件
  • onmessage(),是在客户端收到消息时,就会触发 onmessage 事件
  • onerror(),出现异常时,前端会触发 onerror 事件
  • onclose(),客户端与服务端连接关闭后,就会触发 onclose 事件

接下来看看两个主动的方法:

  • Socket.send(),在前端主动获取内容后,通过 send 进行消息发送
  • Socket.close(),当用户触发某个按钮,就会断开客户端与服务端的连接

以上就是对于前端 websocket js 相对应的 api。

实现前端 websocket

上面介绍了后端对于消息的处理、编解码等,又介绍了 websocket js 的相关。接下来,我们看看前端如何实现 websocket,首先我们先写一个文本输入、点击等功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>

<div>send msg:</div>
<input type="text" id="msgContent"/>
<input type="button" value="send" onclick="CHAT.chat()"/>

<div>receive msg:</div>
<div id="receiveMsg" style="background-color: gainsboro;"></div>
</body>
</html>

访问连接:C:\Users\damon\Desktop\netty\WebChat\index.html,我们可以看到效果:

img

接下来,我们需要写 websocket js:

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
ini复制代码<script type="application/javascript">

window.CHAT = {
socket: null,
init: function() {
if (window.WebSocket) {
CHAT.socket = new WebSocket("ws://192.168.174.145:8088/ws");
CHAT.socket.onopen = function() {
console.log("连接建立成功...");
},
CHAT.socket.onclose = function() {
console.log("连接关闭...");
},
CHAT.socket.onerror = function() {
console.log("发生错误...");
},
CHAT.socket.onmessage = function(e) {
console.log("接受到消息:" + e.data);
var receiveMsg = document.getElementById("receiveMsg");
var html = receiveMsg.innerHTML;
receiveMsg.innerHTML = html + "<br/>" + e.data;
}
} else {
alert("浏览器不支持websocket协议...");
}
},
chat: function() {
var msg = document.getElementById("msgContent");
CHAT.socket.send(msg.value);
}
};

CHAT.init();

</script>

这样,一个简单的 websocket js 就写完了,接下来,我们来演示下。

打开网页,访问 index 页面,我们可以看到连接 websocket 失败,而且会打印发生错误、连接关闭信息,这是因为连接失败时,触发 onerror 事件、onclose 事件:

img

接下来,我们先启动后端 WSServer,同时,刷新页面,可以看到页面显示:连接成功。控制台信息:

img

这里由于我打开了两个页面,所以可以看到后端控制台有打印两次客户端连接的信息,分别对应不同的客户端。接下来,我们输入:Hi,Damon

img

发送后,我们可以看到页面上输出信息:“服务器时间在 2021-05-17T20:05:22.802 接受到消息, 消息为:Hi,Damon”。同时,在另一个客户端窗口,也可以看到输出信息:

img

这是因为后端接收到第一个客户端的请求信息后,将信息转发给所有客户端。接下来,如果我们关闭第一个客户端窗口,则后端会监听到,并且输出:

img

同样,如果我新开一个客户端,并且输入信息,也会被转发到其它客户端:

img

同时,后端控制台会打印对应的请求信息:

img

最后,如果我们主要关闭后端服务,此时,所有的客户端都会失去 socket 连接,会提示:

img

后端整合 Springboot 实现聊天系统

前面介绍了 Websocket 后端处理以及前端的实现逻辑,最后,我们结合 Springboot,来看看后端逻辑的实现。

首先,我们进入依赖 pom:

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
xml复制代码<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.10.RELEASE</version>
<relativePath/>
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.64.Final</version>
</dependency>

<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.41</version>
</dependency>

<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<!--mapper -->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>1.2.4</version>
</dependency>
<!--pagehelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>

<!-- 高性能分布式文件服务器 -->
<dependency>
<groupId>com.github.tobato</groupId>
<artifactId>fastdfs-client</artifactId>
<version>1.26.2</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>

<!-- 二维码 -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.3</version>
</dependency>

这里主要依赖 Springboot 较高版本 2.3.10.RELEASE,同时,加入了 netty 的依赖,以及数据库 mybatis、fastdfs 等分布式文件服务的依赖。

接下来,我们看看启动类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
less复制代码@SpringBootApplication
// 扫描mybatis mapper包路径
@MapperScan(basePackages="com.damon.mapper")
// 扫描 所有需要的包, 包含一些自用的工具类包 所在的路径
@ComponentScan(basePackages= {"com.damon", "org.n3r.idworker"})
public class Application {

@Bean
public SpringUtil getSpingUtil() {
return new SpringUtil();
}

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

在启动类中,我们看到依据 Springboot 来注入注解,并且,我们扫描注入有些启动 bean。接下来,我们再看看如何引入 Netty 服务端启动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript复制代码@Component
public class NettyBooter implements ApplicationListener<ContextRefreshedEvent> {

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (event.getApplicationContext().getParent() == null) {
try {
WSServer.getInstance().start();
} catch (Exception e) {
e.printStackTrace();
}
}
}

}

这里主要通过注解@Component 注入一个监听器,同时是在主服务启动的时候来启动 Netty 服务。那么 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
25
26
27
28
29
30
csharp复制代码@Component
public class WSServer {

private static class SingletionWSServer {
static final WSServer instance = new WSServer();
}

public static WSServer getInstance() {
return SingletionWSServer.instance;
}

private EventLoopGroup mainGroup;
private EventLoopGroup subGroup;
private ServerBootstrap server;
private ChannelFuture future;

public WSServer() {
mainGroup = new NioEventLoopGroup();
subGroup = new NioEventLoopGroup();
server = new ServerBootstrap();
server.group(mainGroup, subGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new WSServerInitialzer());
}

public void start() {
this.future = server.bind(8088);
System.err.println("netty websocket server start over");
}
}

对于线程组来讲,当客户端与从线程组进行通信后,从线程组会对对应的 Channel 进行处理。同时,每一个 Channel 都是有初始化器,所以这里有 childHandler 函数。channelHandler 的处理器会进行处理 Http、Websocket 等各种协议的请求的支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
scala复制代码public class WSServerInitialzer extends ChannelInitializer<SocketChannel> {

@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();

// websocket 基于http协议,所以要有http编解码器
pipeline.addLast(new HttpServerCodec());
// 对写大数据流的支持
pipeline.addLast(new ChunkedWriteHandler());

pipeline.addLast(new HttpObjectAggregator(1024*64));

pipeline.addLast(new IdleStateHandler(8, 10, 12));
// 自定义的空闲状态检测
pipeline.addLast(new HeartBeatHandler());

pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));

// 自定义的handler
pipeline.addLast(new ChatHandler());
}

}

到此,所有的后端的技术部分就都讲完了。

本文转载自: 掘金

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

Redis Bitmap 学习和使用

发表于 2021-08-20

你必须非常努力,才能看起来毫不费力!

微信搜索公众号[ 漫漫Coding路 ],一起From Zero To Hero !

前言

Bitmap,即位图,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。BitMap通过最小的单位bit来进行0|1的设置,表示某个元素的值或者状态,时间复杂度为O(1)。由于bit是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用二值统计的场景。

这里的二值状态就是指集合元素的取值就只有 0 和 1 两种。例如在签到打卡的场景中,我们只用记录签到(1)或未签到(0),所以它就是非常典型的二值状态。在签到统计时,每个用户一天的签到用 1 个 bit 位就能表示,一个月(假设是 31 天)的签到情况用 31 个 bit 位就可以,而一年的签到也只需要用 365 个 bit 位,根本不用太复杂的集合类型。这个时候,我们就可以选择 Bitmap。

Bitmap不属于Redis的基本数据类型,而是基于String类型进行的位操作。而Redis中字符串的最大长度是 512M,所以 BitMap 的 offset 值也是有上限的,其最大值是:

1
shell复制代码8 * 1024 * 1024 * 512  =  2^32

下面我们就来看下Bitmap的相关操作吧!

SETBIT

可用版本:>= 2.2.0

时间复杂度:O(1)

命令格式

1
shell复制代码SETBIT key offset value

命令描述

  • 针对key存储的字符串值,设置或清除指定偏移量offset上的位(bit)
  • 位的设置或清除取决于value值,即1或0
  • 当key不存在时,会创建一个新的字符串。而且这个字符串的长度会伸展,直到可以满足指定的偏移量offset(0 ≤offset< 2^32),在伸展过程中,新增的位的值被设置为0

警告!

如果设置较大的offset,内存分配可能会导致Redis阻塞。

如果key对应的字符串不存在或长度较短,但是设置的offset较大(比如最大为 2^32 -1),Redis需要对中间的位数进行内存分配,Redis可能会阻塞。

拿2010 MacBook Pro举例,offset = 2^32 -1 (分配512MB内存),需要耗时300ms左右;offset = 2^30 -1 (分配128内存),需要耗时80ms左右;offset = 2^28 -1 (分配32MB内存),需要耗时30ms左右;offset = 2^26 -1 (分配8MB内存),需要耗时80ms左右。

第一次分配内存后,后续对该key的相同操作不会再有内存分配开销。

设置Bitmap

如果想要设置Bitmap的非零初值,该怎么设置呢?一种方式就是将每个位挨个设置为0或1,但是这种方式比较麻烦,我们可以考虑直接使用SET命令存储一个字符串。

由于Bitmap就是基于String类型,因此Bitmap类型的数据也可以使用String类型的命令,主要是SET和GET。

比如对于字符串‘42’,底层保存数据时,使用0-7位保存‘4’,使用8-15位保存‘2’,‘4’对应的ASCII码为0011 0100,‘2’对应的ASCII码为0011 0010,我们通过挨个设置Bitmap来观察下:

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
shell复制代码127.0.0.1:6379> SETBIT bitmapsarestrings 2 1
(integer) 0
127.0.0.1:6379> SETBIT bitmapsarestrings 3 1
(integer) 0
127.0.0.1:6379> SETBIT bitmapsarestrings 5 1
(integer) 0
127.0.0.1:6379> SETBIT bitmapsarestrings 10 1
(integer) 0
127.0.0.1:6379> SETBIT bitmapsarestrings 11 1
(integer) 0
127.0.0.1:6379> SETBIT bitmapsarestrings 14 1
(integer) 0

# 通过挨个设置比特位,GET数据就是字符串”42“
127.0.0.1:6379> GET bitmapsarestrings
"42"

# 直接设置字符串,查出来bit位也是一致的
127.0.0.1:6379> set bitkey "42"
OK
127.0.0.1:6379> getbit bitkey 0
(integer) 0
127.0.0.1:6379> getbit bitkey 1
(integer) 0
127.0.0.1:6379> getbit bitkey 2
(integer) 1
127.0.0.1:6379> getbit bitkey 3
(integer) 1
127.0.0.1:6379> getbit bitkey 4
(integer) 0
127.0.0.1:6379> getbit bitkey 5
(integer) 1
127.0.0.1:6379> getbit bitkey 6
(integer) 0
127.0.0.1:6379> getbit bitkey 7
(integer) 0
127.0.0.1:6379> getbit bitkey 8
(integer) 0
127.0.0.1:6379> getbit bitkey 9
(integer) 0
127.0.0.1:6379> getbit bitkey 10
(integer) 1
127.0.0.1:6379> getbit bitkey 11
(integer) 1
127.0.0.1:6379> getbit bitkey 12
(integer) 0
127.0.0.1:6379> getbit bitkey 13
(integer) 0
127.0.0.1:6379> getbit bitkey 14
(integer) 1
127.0.0.1:6379> getbit bitkey 15
(integer) 0

因此通过上述例子我们可以明白,在设置非零初值时,不需要挨个设置比特位,只需要给定一个字符串就可以了。

返回值

整数:偏移量offset位置的原始值

示例

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
shell复制代码# 8对应的ASCII码为:0011 1000
127.0.0.1:6379> set mykey "8"
OK

# 8对应的ASCII码为:0011 1000
127.0.0.1:6379> getbit mykey 0
(integer) 0
127.0.0.1:6379> getbit mykey 1
(integer) 0
127.0.0.1:6379> getbit mykey 2
(integer) 1
127.0.0.1:6379> getbit mykey 3
(integer) 1
127.0.0.1:6379> getbit mykey 4
(integer) 1
127.0.0.1:6379> getbit mykey 5
(integer) 0
127.0.0.1:6379> getbit mykey 6
(integer) 0
127.0.0.1:6379> getbit mykey 7
(integer) 0

# 将第8位(索引为7)的位设置为1
127.0.0.1:6379> setbit mykey 7 1
(integer) 0
127.0.0.1:6379> getbit mykey 7
(integer) 1

GETBIT

可用版本:>= 2.2.0

时间复杂度:O(1)

命令格式

1
shell复制代码GETBIT key offset

命令描述

  • 返回key对应的字符串,offset位置的位(bit)
  • 当offset大于值的长度时,返回0
  • 当key不存在时,可以认为value为空字符串,此时offset肯定大于空字符串长度,参考上一条,也返回0

返回值

整数:偏移量offset位置的bit值

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
shell复制代码# 8对应的ASCII码为:0011 1000
127.0.0.1:6379> set mykey "8"
OK

# 8对应的ASCII码为:0011 1000
127.0.0.1:6379> getbit mykey 0
(integer) 0
127.0.0.1:6379> getbit mykey 1
(integer) 0
127.0.0.1:6379> getbit mykey 2
(integer) 1
127.0.0.1:6379> getbit mykey 3
(integer) 1
127.0.0.1:6379> getbit mykey 4
(integer) 1
127.0.0.1:6379> getbit mykey 5
(integer) 0
127.0.0.1:6379> getbit mykey 6
(integer) 0
127.0.0.1:6379> getbit mykey 7
(integer) 0

BITCOUNT

可用版本:>= 2.6.0

时间复杂度:O(N)

命令格式

1
shell复制代码BITCOUNT key [start end]

命令描述

  • 统计给定字符串中,比特值为1的数量
  • 默认会统计整个字符串,同时也可以通过指定 start 和 end 来限定范围
  • start 和 end 也可以是负数,-1表示最后一个字节,-2表示倒数第二个字节。注意这里是字节,1字节=8比特
  • 如果key不存在,返回0

返回值

整数:bit值为1的数量

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
shell复制代码# a: 0110 0001	
# b: 0110 0010
# c: 0110 0011
# mykey: 01100001 01100010 01100011
127.0.0.1:6379> set mykey 'abc'
OK

# 统计整个字符串对应的bit=1的数量
127.0.0.1:6379> bitcount mykey
(integer) 10

# a
127.0.0.1:6379> bitcount mykey 0 0
(integer) 3

# ab
127.0.0.1:6379> bitcount mykey 0 1
(integer) 6

# c
127.0.0.1:6379> bitcount mykey 2 2
(integer) 4

使用场景

比如我们有一个App,需要统计2021年每个用户登录的情况,针对这个需求,我们以用户id+2021作为key,将用户上线那天对应的offset设置为1,这样就可以统计每个用户在本年度登录的情况,使用bitcount可以统计登录天数。

举个例子,今天是2021年第100天,而user_id:10001在今天阅览过网站,那么执行命令 SETBIT 2021:user_id:10001 100 1 ;如果明天 该用户也登录的App,那么执行命令 SETBIT 2021:user_id:10001 101 1 ,以此类推。

最后使用 BITCOUNT 2021:user_id:10001,就可以统计该用户本年度登录App的次数了

通过上例我们可以看出,使用Bitmap来统计二值数据非常节省内存,一个用户一年只需要占用 365个比特,10年也只需要 365*10/8=456个字节。

BITPOS

可用版本:>= 2.8.7

时间复杂度:O(N)

命令格式

1
shell复制代码BITPOS key bit [start [end]]

命令描述

  • 返回字符串中,从左到右,第一个比特值为bit(0或1)的偏移量
  • 默认情况下会检查整个字符串,但是也可以通过指定start和end变量来指定字节范围,与BITCOUNT中的范围描述一致
  • SETBIT和GETBIT指定的都是比特偏移量,BITCOUNT和BITPOS指定的是字节范围
  • 不论是否指定查询范围,该命令返回的偏移量都是基于0开始的
  • 如果key不存在,认为是空字符串

返回值

整数:第一个比特值为指定bit(0或1)的偏移量

  • 如果命令中参数bit=1,但是字符串为空,此时返回 -1 ;
  • 如果命令中参数bit=0,但是字符串中所有的比特值都为1,此时命令返回字符串最大的offset+1。例如字符串对应的比特值为’11111111‘,那么此时会返回8。
  • 默认情况下,如果查询bit=0,且没有指定范围,或者只指定了start,命令默认在字符串后面补0用于查询bit=0的offset。但是如果指定了start和end,且范围内所有值都为0,此时会返回-1,因为用户指定了范围且范围内没有0,不会在后面补充
  • 如果查询bit=1,始终不会在字符串后面补充1,查询不到就会返回-1

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
shell复制代码# 例1
# a: 0110 0001
127.0.0.1:6379> SET mykey "a"
OK
# 第一个bit=1的偏移量
127.0.0.1:6379> bitpos mykey 1
(integer) 1
# 第一个bit=0的偏移量
127.0.0.1:6379> bitpos mykey 0
(integer) 0

# 例2
# '\xff': 1111 1111
# 全为1时,查询bit=1和bit=0
127.0.0.1:6379> SET mykey "\xff"
OK
127.0.0.1:6379> bitpos mykey 1
(integer) 0
127.0.0.1:6379> bitpos mykey 0
(integer) 8

# 例3
# '\xff': 0000 0000
# 全为0时,查询bit=1和bit=0
127.0.0.1:6379> SET mykey "\x00"
OK
127.0.0.1:6379> bitpos mykey 1
(integer) -1
127.0.0.1:6379> bitpos mykey 0
(integer) 0

# 例4
# 11111111 11111111 11111111
127.0.0.1:6379> set mykey "\xff\xff\xff"
OK
# 指定前两个字节,都为1,不会补充0,返回-1
127.0.0.1:6379> bitpos mykey 0 0 1
(integer) -1

BITOP

可用版本:>= 2.6.0

时间复杂度:O(N)

命令格式

1
shell复制代码BITOP operation destkey key [key ...]

命令描述

  • 对多个字符串进行位操作,并将结果保存到destkey中
  • operation 可以是 AND、OR、XOR 或者 NOT
  • BITOP AND destkey srckey1 srckey2 srckey3 ... srckeyN,对多个key求逻辑与,并将结果保存到destkey中
  • BITOP OR destkey srckey1 srckey2 srckey3 ... srckeyN,对多个key求逻辑或,并将结果保存到destkey中
  • BITOP XOR destkey srckey1 srckey2 srckey3 ... srckeyN,对多个key求异或,并将结果保存到destkey中
  • BITOP NOT destkey srckey,对key求逻辑非,并将结果保存到destkey中
  • 除了 NOT 操作之外,其他操作都可以接受一个或多个 key 作为输入。

不同长度的字符串

当给定的参数中,字符串长度不同时,较短的那个字符串与最长字符串之间缺少的部分会被看作 0 。

空的 key 也被看作是包含 0 的字符串序列。

返回值

整数:保存到 destkey 的字符串的长度(和参数给定的key中最长的字符串长度相等)

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
shell复制代码127.0.0.1:6379> set key1 "\xff"
OK
127.0.0.1:6379> set key2 "\x00"
OK

# 逻辑与
127.0.0.1:6379> bitop AND andkey key1 key2
(integer) 1
127.0.0.1:6379> get andkey
"\x00"

# 逻辑或
127.0.0.1:6379> bitop OR orkey key1 key2
(integer) 1
127.0.0.1:6379> get orkey
"\xff"

# 异或
127.0.0.1:6379> bitop XOR xorkey key1 key2
(integer) 1
127.0.0.1:6379> get xorkey
"\xff"

# 逻辑非
127.0.0.1:6379> bitop not notkey key1
(integer) 1
127.0.0.1:6379> get notkey
"\x00"

BITFIELD

可用版本:>= 3.2.0

时间复杂度:对于每个子命令,复杂度为O(1)

命令格式

1
shell复制代码BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]

命令描述

  • BITFIELD 将Redis字符串看作一个由很多整数组成的数组,能够处理不同宽度的比特位,同时可以处理任意偏移量的字段。换句话说,通过这个命令,用户可以进行如下操作:“将从偏移量1234开始的5位有符号整数设置为一个值”、 “获取从偏移量4567开始的31位无符号整数”等
  • BITFIELD 命令还可以对指定的整数执行加法操作和减法操作, 并且这些操作可以通过设置,有好的地处理计算时的溢出情况
  • BITFIELD可以在一个命令中,操作多个比特区间。即命令接受多个操作,并返回每个操作返回值组成的列表。

如下命令有两个操作:对从偏移量100处开始的5位有符号整数执行加法操作,获取偏移量0开始的4位无符号整数

1
2
3
shell复制代码> BITFIELD mykey INCRBY i5 100 1 GET u4 0
1) (integer) 1
2) (integer) 0

注意:

使用GET超出当前当前字符串范围时(key不存在相当于空字符串,也属于这种情况),超出的部分会被当做0

使用INCRBY或SET命令,且超出字符串范围时,缺失部分会被填充0

支持的子命令和整数类型

  • GET <type> <offset> – 返回指定的比特范围
  • SET <type> <offset> <value> – 设置指定的比特范围并返回旧值
  • INCRBY <type> <offset> <increment> – 增加或减少(如果increment参数为负数)指定的比特范围并返回新值

除了上面三个子命令外,还有一个子命令可以改变SET和INCRBY在发生溢出时的行为:

  • OVERFLOW [WRAP|SAT|FAIL]

当被设置的二进制位范围值为整数时,我们可以在类型参数前面添加 i 来表示有符号整数,或者使用 u 来表示无符号整数。比如说,我们可以使用 u8 来表示8位无符号整数,也可以使用i16来表示 16 位有符号整数。

比特值和位置偏移量

我们有两种方式来设置偏移量:如果数字前没有前缀,那么就是基于0的比特位偏移量;如果数字有带有 ‘#’ 前缀,offset就等于提供的整数宽度乘以’#‘后面的偏移量。例如:

1
less复制代码BITFIELD mystring SET i8 #0 100 SET i8 #1 200

第一个SET对应的偏移量为:8*0=0

第二个SET对应的偏移量为:8*1=8

使用 # 前缀可以让我们免去手动计算被设置比特位所在位置的麻烦。

溢出控制

通过OVERFLOW命令,可以控制BITFIELD在执行增加或减少时,发生向上溢出(overflow)或向下溢出(underflow)的情况时的行为:

  • WRAP:使用环绕的方式来控制有符号和无符号整数的溢出。对于无符号整数来说,环绕就是使用数值本身与能够被储存的最大无符号整数取模,这也是 C 语言的标准行为。 对于有符号整数来说,上溢将导致数字从可以表示的最小负数开始计算,而下溢将导致数字从可以表示的最大正数开始计算。例如对一个值为 127 的 i8 整数执行加1操作,那么将得到结果 -128 。
  • SAT: 使用饱和算法(saturation arithmetic)计算,也就是下溢结果为可以表示的最小的整数值, 而上溢的结果为可以表示的最大的整数值。例如对一个值为 120 的 i8 整数执行加 10 计算, 那么命令的结果将为 i8 类型所能储存的最大整数值 127,如果继续增加,始终为127不变 。 与此相反,如果一个针对 i8 值的计算造成了下溢, 那么这个 i8 值将被设置为 -127 。
  • FAIL:当发生上溢或者下溢情况时,不会执行操作,并向用户返回空值表示计算未被执行。

需要注意的是,OVERFLOW子命令只会影响在它之后的SET和INCRBY命令,直到遇到下一个OVERFLOW就会停止作用。默认使用WARP模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
shell复制代码# 初始都为0,执行无符号加一操作,前一个incrby默认使用 WARP 模式,后一个指定 SAT 模式
> BITFIELD mykey incrby u2 100 1 OVERFLOW SAT incrby u2 102 1
1) (integer) 1
2) (integer) 1

> BITFIELD mykey incrby u2 100 1 OVERFLOW SAT incrby u2 102 1
1) (integer) 2
2) (integer) 2

> BITFIELD mykey incrby u2 100 1 OVERFLOW SAT incrby u2 102 1
1) (integer) 3
2) (integer) 3

# u2最大表示为3,再加一会向上溢出。 WARP: (3+1)%4=0 SAT: 始终保持最大值
> BITFIELD mykey incrby u2 100 1 OVERFLOW SAT incrby u2 102 1
1) (integer) 0
2) (integer) 3

返回值

列表:每个子命令对应的返回值,OVERFLOW不会返回数据,只会影响后续命令的返回值。

动机

将很多小的整数储存到一个长度较大的位图中, 又或者将一个非常庞大的键分割为多个较小的键来进行储存,可以非常高效地使用内存,从而使得 Redis 能够得到更多不同的应用 —— 特别是在实时分析领域: BITFIELD 能够以指定的方式对控制计算溢出, 使得它可以被应用于这一领域。

性能考量

BITFIELD是一个较快的命令,但是需要注意的是:对较短的字符串,处理较大偏移量的比特位,会导致内存分配,进而导致耗时增加;而处理已存在的比特位耗时较少。

比特位的顺序

BITFIELD 把位图第一个字节偏移量为 0 的二进制位看作是 most significant 位,以此类推。 举个例子,如果我们对一个已经预先被全部设置为 0 的位图进行设置, 将它在偏移量为 7 的值设置为 5 位无符号整数值 23 (二进制位为 10111 ), 那么命令将生产出以下这个位图表示:

1
2
3
diff复制代码+--------+--------+
|00000001|01110000|
+--------+--------+

当偏移量和整数长度与字节边界进行对齐时, BITFIELD 表示二进制位的方式跟大端表示法(big endian)一致, 但是在没有对齐的情况下, 理解这些二进制位是如何进行排列也是非常重要的。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
shell复制代码127.0.0.1:6379> set mykey "\x00"
OK

# offset从0开始,获取前5位有符号正数
127.0.0.1:6379> bitfield mykey get i5 0
1) (integer) 0

# offset从0开始,设置前5位有符号正数
127.0.0.1:6379> bitfield mykey set i5 0 10
1) (integer) 0

127.0.0.1:6379> bitfield mykey get i5 0
1) (integer) 10

# offset从0开始,前5位有符号正数加一
127.0.0.1:6379> bitfield mykey incrby i5 0 1
1) (integer) 11
127.0.0.1:6379> bitfield mykey get i5 0
1) (integer) 11

总结

本文介绍了Bitmap的相关操作,主要包括以下命令

  • SETBIT:设置比特位
  • GETBIT:查询比特值
  • BITCOUNT:统计比特值为1的数量
  • BITPOS:查询第一个比特值为0或1的偏移量
  • BITOP:对Bitmap做逻辑与、或、异或、非操作
  • BITFIELD:将Bitmap看作由多个整数组成的,对其中的整数操作

更多

个人博客: lifelmy.github.io/

微信公众号:漫漫Coding路

本文转载自: 掘金

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

1…556557558…956

开发者博客

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