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

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


  • 首页

  • 归档

  • 搜索

【JS 逆向百例】医保局 SM2+SM4 国产加密算法实战

发表于 2021-11-11

关注微信公众号:K哥爬虫,QQ交流群:808574309,持续分享爬虫进阶、JS/安卓逆向等技术干货!

声明

本文章中所有内容仅供学习交流,抓包内容、敏感网址、数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关,若有侵权,请联系我立即删除!

逆向目标

  • 目标:医疗保障局公共查询
  • 主页:aHR0cHM6Ly9mdXd1Lm5oc2EuZ292LmNuL25hdGlvbmFsSGFsbFN0LyMvc2VhcmNoL21lZGljYWw=
  • 接口:aHR0cHM6Ly9mdXd1Lm5oc2EuZ292LmNuL2VidXMvZnV3dS9hcGkvbnRobC9hcGkvZml4ZWQvcXVlcnlGaXhlZEhvc3BpdGFs
  • 逆向参数:Request Payload 的 encData 和 signData、Request Headers 的 x-tif-nonce 和 x-tif-signature

逆向过程

抓包分析

来到公共查询页面,点击翻页,就可以看到一个 POST 请求,Request Payload 的参数部分是加密的,主要是 appCode、encData 和 signData 参数,同样返回的数据也有这些参数,其加密解密方法是一样的,其中 encType 和 signType 分别为 SM4 和 SM2,所以大概率这是国密算法了,有关国密算法 K 哥前期文章有介绍:《爬虫逆向基础,认识 SM1-SM9、ZUC 国密算法》,此外请求头还有 x-tif-nonce 和 x-tif-signature 参数,如下图所示:

01.png

参数逆向

直接全局搜索 encData 或 signData,搜索结果仅在 app.1634197175801.js 有,非常明显,上面还有设置 header 的地方,所有参数都在这里,埋下断点,可以看到这里就是加密的地方,如下图所示:

02.png

这里的加密函数,主要都传入了一个 e 参数,我们可以先看一下这个 e,里面的参数含义如下:

  • addr:医疗机构详细地址,默认空;
  • medinsLvCode:医疗机构等级代码,默认空;
  • medinsName:医疗机构名称,默认空;
  • medinsTypeCode:医疗机构类型代码,默认空;
  • pageNum:页数,默认 1;
  • pageSize:每页数据条数,默认 10;
  • regnCode:医疗机构所在地代码,默认 110000(北京市);
  • sprtEcFlag:暂时不知其含义,默认空。

等级代码、类型代码、所在地代码,都是通过请求加密接口得到的,他们的加密和解密方法都一样,在最后的完整代码里有分享,这里不再赘述。其他参数比如 appCode,是在 JS 里写死的。

03.png

我们再观察一下整个 JS 文件,在头部可以看到 .call 语句,并且有 exports 关键字,很明显是一个 webpack 形式的写法。

04.png

我们回到加密的地方,从上往下看,整个函数引用了很多其他模块,如果想整个扣下来,花费时间肯定是无比巨大的,如果想直接拿下整个 JS,再将参数导出,这种暴力做法可是可以,但是整个 JS 有七万多行,运行效率肯定是有所影响的,所以观察函数,将不用的函数去掉,有用的留下来,是比较好的做法,观察 function d,第一行 var t = n("6c27").sha256,点进去来到 createOutputMethod 方法,这里整个是一个 SHA256 算法,从这个方法往下整个 copy 下来即可,如下图所示:

05.png

06.png

这里要注意的是,观察这个函数后面导出的 sha256 实际上是调用了 createMethod 这个方法,那么我们 copy 下来的方法直接调用 createMethod 即可,即 var t = createMethod(),不需要这些 exports 了。

07.png

另外还有一些变量需要定义,整个 copy 下来的结构如下:

08.png

接着前面的继续往下看,还有一句 o = Object(i.a)(),同样点进去直接 copy 下来即可,这里没有什么需要注意的地方。

09.png

再往下看就来到了 e.data.signData = p(e),点进 function p,将整个函数 copy 下来,这时候你本地调试会发现没有任何错误,实际上他这里使用了 try-catch 语句,捕获到了异常之后就没有任何处理,可以自己加一句 console.log(e) 来输出异常,实际上他这里会在 o.doSignature、e.from 两个位置提示未定义,同样的我们可以点进去将函数扣出来,但是后面会遇到函数不断引用其他函数,为了方便,我们可以将其写到 webpack 里,下面的 e.from 也是一样。

10.png

11.png

将模块写成 webpack 形式,在自执行方法里调用,然后定义全局变量来接收,再将原来的 o, e 换成全局变量即可,这里还需要注意的一个地方,那就是 o.doSignature 传入的 h,是一个定值,需要定义一下,不然后面解密是失败的。如下图所示:

12.png

13.png

这里扣 webpack 模块的时候也需要注意,不要把所有原方法里有的模块都扣出来,有些根本没用到,可以直接注释掉,这个过程是需要有耐心的,你如果全部扣,那将会是无穷无尽的,还不如直接使用整个 JS 文件,所有有用的模块如下(可能会多,但不会少):

14.png

接着原来的说,encData: v("SM4", e) 这里用到了 function v,v 里面又用到了 A、g 等函数,全部扣下来即可,同时还需要注意,前面所说的 e 在 A 函数里也用到了,同样需要换成我们自己定义的全局变量,如下图所示:

15.png

16.png

到此加密用到的函数都扣完了,此时我们可以写一个方法,对加密的过程进行封装,使用时只需要传入类似以下参数即可:

1
2
3
4
5
6
7
8
9
10
json复制代码{
"addr": "",
"regnCode": "110000",
"medinsName": "",
"sprtEcFlag": "",
"medinsLvCode": "",
"medinsTypeCode": "",
"pageNum": 1,
"pageSize": 10
}

如下图所示 getEncryptedData 就是加密方法:

17.png

那么解密方法呢?很明显返回的数据是 encData,直接搜索 encData 就只有三个结果,很容易找到就行 function y,同样的,这里要注意把 e.from 改成我们自定义的 e_.Buffer.from,另外我们也可以将 header 参数的生成方法也封装成一个函数,便于调用。

18.png

19.png

完整代码

GitHub 关注 K 哥爬虫,持续分享爬虫相关代码!欢迎 star !github.com/kgepachong/

以下只演示部分关键代码,不能直接运行! 完整代码仓库地址:github.com/kgepachong/…

JavaScript 加密关键代码架构

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
javascript复制代码var sm2, sm4, e_;
!function (e) {
var n = {},
i = {app: 0},
r = {app: 0};

function o(t) {}

o.e = function (e) {}
o.m = e
o.c = n
o.d = function (e, t, n) {}
o.r = function (e) {}
o.n = function (e) {}
o.o = function (e, t) {}

sm2 = o('4d09')
e_ = o('b639')
sm4 = o('e04e')

}({
"4d09": function (e, t, n) {},
'f33e': function (e, t, n) {},
"4d2d": function (e, t, n) {},
'b381': function (e, t, n) {},
// 此处省略 N 个模块
})

// 此处省略 N 个变量

var createOutputMethod = function (e, t) {},
createMethod = function (e) {},
nodeWrap = function (method, is224) {},
createHmacOutputMethod = function (e, t) {},
createHmacMethod = function (e) {};

function Sha256(e, t) {}

function HmacSha256(e, t, n) {}

// 此处省略 N 个方法

function i() {}

function p(t) {}

function m(e) {}

var c = {
paasId: undefined,
appCode: "T98HPCGN5ZVVQBS8LZQNOAEXVI9GYHKQ",
version: "1.0.0",
appSecret: "NMVFVILMKT13GEMD3BKPKCTBOQBPZR2P",
publicKey: "BEKaw3Qtc31LG/hTPHFPlriKuAn/nzTWl8LiRxLw4iQiSUIyuglptFxNkdCiNXcXvkqTH79Rh/A2sEFU6hjeK3k=",
privateKey: "AJxKNdmspMaPGj+onJNoQ0cgWk2E3CYFWKBJhpcJrAtC",
publicKeyType: "base64",
privateKeyType: "base64"
},
l = c.appCode,
u = c.appSecret,
f = c.publicKey,
h = c.privateKey,
t = createMethod(),
// t = n("6c27").sha256,
r = Math.ceil((new Date).getTime() / 1e3),
o = i(),
a = r + o + r;

function getEncryptedData(data) {
var e = {"data": data}
return e.data = {
data: e.data || {}
},
e.data.appCode = c.appCode,
e.data.version = c.version,
e.data.encType = "SM4",
e.data.signType = "SM2",
e.data.timestamp = r,
e.data.signData = p(e),
e.data.data = {
encData: v("SM4", e)
},
// e.data = JSON.stringify({
// data: e.data
// }),
e
}

function getDecryptedData(t) {
if (!t)
return null;
var n = e_.Buffer.from(t.data.data.encData, "hex")
, i = function(t, n) {
var i = sm4.decrypt(n, t)
, r = i[i.length - 1];
return i = i.slice(0, i.length - r),
e_.Buffer.from(i).toString("utf-8")
}(g(l, u), n);
return JSON.parse(i)
}

function getHeaders(){
var headers = {}
return headers["x-tif-paasid"] = c.paasId,
headers["x-tif-signature"] = t(a),
headers["x-tif-timestamp"] = r.toString(),
headers["x-tif-nonce"] = o,
headers["Accept"] = "application/json",
headers["contentType"] = "application/x-www-form-urlencoded",
headers
}

Python 获取数据关键代码

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
python复制代码# ==================================
# --*-- coding: utf-8 --*--
# @Time : 2021-11-03
# @Author : 微信公众号:K哥爬虫
# @FileName: nhsa.py
# @Software: PyCharm
# ==================================


import execjs
import requests


regn_code_url = "脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler"
lv_and_type_url = "脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler"
result_url = "脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler"
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36"

with open('nhsa.js', 'r', encoding='utf-8') as f:
nhsa_js = execjs.compile(f.read())


def get_headers():
"""获取 header 参数,每次请求改变"""
headers = nhsa_js.call("getHeaders")
headers["User-Agent"] = UA
headers["Content-Type"] = "application/json"
headers["Host"] = "脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler"
headers["Origin"] = "脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler"
headers["Referer"] = "脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler"
# print(headers)
return headers


def get_regn_code():
"""获取城市代码,返回结果无加密"""
payload = {"data": {"transferFlag": ""}}
response = requests.post(url=regn_code_url, json=payload, headers=get_headers())
print(response.text)


def get_medins_lv_or_type_code(key):
"""获取医疗机构等级 (LV) or 类型 (TYPE) 代码"""
if key == "LV":
payload = {"type": "MEDINSLV"}
elif key == "TYPE":
payload = {"type": "MEDINS_TYPE"}
else:
print("输入有误!")
return
encrypted_payload = nhsa_js.call("getEncryptedData", payload)
encrypted_data = requests.post(url=lv_and_type_url, json=encrypted_payload, headers=get_headers()).json()
decrypted_data = nhsa_js.call("getDecryptedData", encrypted_data)
print(decrypted_data)


def get_result():
addr = input("请输入医疗机构详细地址(默认无): ") or ""
medins_lv_code = input("请输入医疗机构等级代码(默认无): ") or ""
medins_name = input("请输入医疗机构名称(默认无): ") or ""
medins_type_code = input("请输入医疗机构类型代码(默认无): ") or ""
regn_code = input("请输入医疗机构所在地代码(默认北京市): ") or "110000"
page_num = input("请输入要爬取的页数(默认1): ") or 1

for page in range(1, int(page_num)+1):
payload = {
"addr": addr,
"medinsLvCode": medins_lv_code,
"medinsName": medins_name,
"medinsTypeCode": medins_type_code,
"pageNum": page,
"pageSize": 10,
"regnCode": regn_code,
"sprtEcFlag": ""
}
page += 1
encrypted_payload = nhsa_js.call("getEncryptedData", payload)
encrypted_data = requests.post(url=result_url, json=encrypted_payload, headers=get_headers()).json()
decrypted_data = nhsa_js.call("getDecryptedData", encrypted_data)
print(decrypted_data)


def main():
# 获取城市代码
# get_regn_code()
# 获取医疗机构等级代码
# get_medins_lv_or_type_code("LV")
# 获取医疗机构类型代码
# get_medins_lv_or_type_code("TYPE")
# 获取搜索结果
get_result()


if __name__ == "__main__":
main()

本文转载自: 掘金

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

C语言04-函数递归(下)

发表于 2021-11-11

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

最近,想复习一下C语言,所以笔者将会在掘金每天更新一篇关于C语言的文章! 各位初学C语言的大一新生,以及想要复习C语言/C++知识的不要错过哦! 夯实基础,慢下来就是快!


5.用非递归的方式模拟实现strlen函数

strlen:计算字符串长度的库函数,需要引用string.h头文件。遇到\0停止计数。

strlen和sizeof是一对有点相似的东西,具体的大家可以去看看我之前的文章.关于strlen和sizeof区别的文章链接:
C语言-strlen与sizeof区别

1
2
3
4
5
6
7
8
9
10
11
arduino复制代码//非递归方式
int my_strlen1(char* s)
{
int count = 0;
//循环判断 不能用if
while (*s != '\0')
{
s++;
count++;
}
return count;

6.用递归的方式模拟实现strlen函数

图解
image.png


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
arduino复制代码//递归的方式
int my_strlen2(char* s)
{
if (*s != '\0')
return 1 + my_strlen2(s + 1);
else
return 0;
}
int main()
{
char arr[] = "Mango";
int ret1 = strlen(arr);
int ret2 = my_strlen1(arr);
int ret3 = my_strlen2(arr);
printf("%d %d %d", ret1, ret2, ret3);
return 0;
}

image.png


7.用非递归实现字符串逆序

方法:使用双指针,一个指向左边,一个指向右边。左指针指向的字符和右指针指向的字符交换。 循环条件为:left < right


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
ini复制代码//非递归方式
void reverse(char* arr)
{
int len = strlen(arr);
//strlen接受的参数是地址,所以写成s
//解引用传过去的是char类型的数据,二者不匹配
char* right = arr + len - 1;
char* left = arr; //注意此处不能赋值为0 相当于NULL
while (left < right)
{
char tmp = 0;
tmp = *left;
*left = *right;
*right = tmp;
left++;
right--;
}
}
int main()
{
char arr[] = "ognam";
reverse(arr);
for (int i = 0; i < 5; i++)
{
printf("%c", arr[i]);
}
return 0;
}

运行结果:


8.用递归实现字符串逆序

图解

image.png

当大家遇到看不懂得递归,也可以像我一样,假设一个例子,然后用展开图来理解一下!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ini复制代码//递归方式	
void reverse_string(char* arr)
{
int len = strlen(arr);
char tmp = *arr;
*arr = *(arr + len - 1);

*(arr + len - 1) = '\0';
if (strlen(arr + 1) >= 2)
reverse_string(arr + 1);

*(arr + len - 1) = tmp;
}
int main()
{
char arr[] = "ognam";
reverse_string(arr);
for (int i = 0; i < 5; i++)
{
printf("%c", arr[i]);
}
return 0;
}

image.png

9.用递归计算一个数拆分成每一位之后的每位之和

图解

image.png


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
arduino复制代码int DigitSum(int n)
{
if (n)
return n % 10 + DigitSum(n / 10);
else
return 0;
}
int main()
{
int n = 0;
scanf("%d", &n);
int sum = DigitSum(n);
printf("%d", sum);
return 0;
}

运行结果:


10.用递归实现计算n的k次方

图解

image.png


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
arduino复制代码int my_pow(int n, int k)
{
//一个数的0次方=1
if (k == 0)
{
return 1;
}
else if (k >= 1)
return my_pow(n, k - 1)*n;
}
int main()
{
int n = 0;
int k = 0;
scanf("%d %d", &n, &k);
int ret1 = pow(n, k);

int ret2 = my_pow(n, k);
printf("%d %d", ret1, ret2);
}

运行结果:


11.用非递归求第n个斐波那契数

图解

image.png


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
arduino复制代码//8.求第n个斐波那契数
//递归方式
//方式1:
int Fic(int n)
{
return n <= 2 ? 1 : Fic(n - 1) + Fic(n - 2);
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fic(n);
printf("%d", ret);
return 0;
}

此种写法需要计算很多重复的数,效率低!


方法2:迭代计算

image.png


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
ini复制代码//方式2
int Fic(int n)
{
//思路:a, b, c 算出a + b的值放到c, 下一次:将b的值给a,将c的值给b 循环计算
//从第3个斐波那契数开始算,计算1次,计算第4个斐波那契数要计算2次
//- - 》》所以计算第n个要计算n - 2次
int a = 1;//第1个斐波那契数
int b = 1;//第2个斐波那契数
int c = 0;
while (n > 2)
{
c = a + b;
a = b;
b = c;
n--;
}
return c;
//当n是1或2时 不进入循环 返回c = 0 err
//所以c初始化为1
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fic(n);
printf("%d", ret);
return 0;
}

运行结果:


递归习题练习到这里就差不多结束了,明天,我会给大家带来两个经典的递归问题:青蛙跳台阶和汉诺塔问题!欢迎大家持续关注!

今天就先到这吧~感谢你能看到这里!希望对你有所帮助!欢迎老铁们点个关注订阅这个专题! 同时欢迎大佬们批评指正!

本文转载自: 掘金

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

电竞入亚,行业爆发,你的游戏是否还缺个通信「大招」?

发表于 2021-11-11

❄吃饺子、晒雪景,“你那里下雪了吗? ”

🎉狂欢、霸屏,高呼“EDG NB! ”

2021年的这个立冬,两种画风交织,代际差异泾渭分明。

“EDG 夺冠”、“电竞入亚”让电竞再次站在风口浪尖,赞扬、批判交杂之声甚嚣尘上。但无庸质疑的是,电竞的成功出圈让游戏行业再度爆火,游戏的商业价值正在受到主流市场认可。艾媒咨询数据显示,预计到 2022 年中国电竞市场规模将增长至 1843.3 亿元。【融云全球互联网通信云】

行业向好之下的另一个趋势是,游戏正和社交娱乐深度融合发展,一款叫好又叫座的游戏离不开围绕社交进行的精妙设计。

如何让玩家有更好的社交体验?敲黑板,下面这份答案拿去抄。

针对基于 Unity 平台开发和运营的休闲小游戏、策略类游戏和沙盒游戏等不同种类的游戏业务,融云提供了简单可行的通信能力接入方案,请查收👇。

BTW,11 月 20 日,*Unity 大中华区业务总经理肖蓓蓓将莅临 WICC · 广州,分享打开元宇宙的密钥* *,[欢迎报名](https://www.huodongxing.com/event/6617469398111?qd=weixin) *。

休闲小游戏

这一类别以五子棋、斗兽棋、麻将等棋牌类游戏和台球、连连看等休闲益智小游戏为主,如下图示。

图片!

该类游戏特点:

① 游戏时只有一个 RTC 房间,所有人员都在同一个房间内;

② 玩家默认都订阅其他人的音频;

③ 玩家不能控制其他玩家的麦克风开关,只能关闭自己的麦克风;

④ 玩家可以选择性收听某些玩家的音频,对于不友善的音频可以选择针对性屏蔽。

通讯方式:

IM: 单聊、聊天室;文字消息、表情、emoji

RTC: 1V1 语音;随机匹配,邀请好友

游戏过程中的语音需求:

① 匹配到对手后,将本方玩家和对方玩家加入同⼀个 RTC 房间,默认打开麦克风和听筒/扬声器;

② 本方玩家可以关闭自己的麦克风,对方将听不到本方发言;

③ 本方玩家可以关闭自己的听筒/扬声器,自己将听不到对方的发言;

④ 游戏结束后,默认通话结束,将本方玩家和对方玩家都踢出房间,且销毁房间(若直接再来一局则不算游戏结束)。

组队策略类游戏

这一类别包含组队贪吃蛇大作战,时空召唤、王者荣耀、LOL 手游版等 MOBA(多人在线战术竞技游戏)类,Free Fire、和平精英等射击类游戏。

图片)图片

该类游戏特点:

① 游戏时有 3 个 IM 聊天室,2 个 RTC 房间,如下图示。

图片

(队伍对应语音房间关系示意图)

② 本方队伍在一个 IM 聊天室,对方队伍在一个 IM 聊天室,全部玩家都在 IM 公共聊天室; 本方队伍在一个 RTC 房间,对方队伍在一个 RTC 房间,两个 RTC 房间的语音不互通。

③ 玩家通过读取配置列表来判断进入游戏后是否订阅其他人的音频。

图片

(配置列表)

④ 玩家可以关闭自己的麦克风,对方将听不到其他玩家发言;

⑤ 玩家可以选择性收听某些玩家的音频,对于不友善的音频可以选择针对性屏蔽。

通讯方式:

IM: 单聊、群聊、聊天室;文字消息、表情、emoji,语音消息

RTC: 会议模型;互相订阅

游戏过程中的语音需求:

图片

(队伍语音)

① 游戏开始后,A 队伍和 B 队伍的玩家全部加入到 IM 公共聊天室,然后 A 队伍中的玩家加入 A 队伍的 IM 聊天室 和 RTC 语音房间,B 队伍中的玩家加入 B 队伍的 IM 聊天室 和 RTC 语音房间;

② 加入各房间功能解释:

  • A 队伍和 B 队伍的玩家全部加入到 IM 公共聊天室:A 队伍和 B 队伍中成员可以在公共聊天室内互相发消息,发送的消息彼此队伍玩家都可见 ;
  • A 队伍中的玩家加入 A 队伍的 IM 聊天室:A 队伍成员可以在 A 队伍的 IM 聊天室内互相发送消息,发送的消息只有 A 队伍内成员可见;
  • A 队伍中的玩家加入 A 队伍的 RTC 语音房间:A 队伍成员可以在 A 队伍的 RTC 语音房间内进行语音聊天,B 队伍的成员是无法收听到的。

③ 当 A 队伍内玩家有组队情况出现 AA 时,那么 AA 中的分组成员通过互相订阅的方式实现彼此语音交流。

④ 房间内分组功能解释:

  • 订阅全部: AA 分组内的成员可以选择订阅房间 A 队伍 RTC 房间内的其他成员的全部语音,这样实现的是和 A 队伍中的全部成员进行语音交流 ;
  • 订阅组内: 也可以选择只订阅 AA 分组内成员的语音,这样实现的是只和 AA 分组内的成员进行语音交流。

沙盒游戏

该类别游戏以崽崽、奇妙派对、Roblox(罗布乐思)、迷你世界、我的世界为主要代表。

图片)图片

该类游戏特点:

① 房间内全员皆可上麦,且互相订阅;

② 房间内所有成员都可以自由发言。

通讯方式:

IM: 单聊、群聊、聊天室;文字消息、表情、emoji,语音消息

RTC: 会议模型;互相订阅

游戏过程中语音需求:

① 玩家通过读取配置列表来判断进入游戏后是否自动订阅其他人的音频;

② 房间内所有成员都在麦上或者成员可以向房间内管理员申请上麦;

③ 麦上成员可以随时下麦;

④ 房间内所有成员默认互相订阅,可以听到其他所有成员音频;

⑤ 玩家可以选择性收听某些玩家的音频,对于不友善的音频可以选择针对性屏蔽。

接入方式及涉及接口

以上游戏类别,都可以通过接入融云 SDK 获得通信能力加持,为玩家提供高质量、低延迟的交互体验。

接入方式:

直接集成融云的 Unity RTCLib SDK 到游戏项目中,Android 和 iOS 可以直接调用 RTCLib 封装的 Unity 的接口实现游戏内语音需求。

方案优势:

① 跨平台,一次编写实现多平台运行;

② 节省人力成本,便于维护;

③ 避免 iOS 端和 Android 端分别集成带来的业务中调用两个接口的繁琐。

涉及接口:

① 加入房间:

以主播身份加入房间

1
ini复制代码// 加⼊RTC房间 RCRTCRoomSetup setup = RCRTCRoomSetup.Builder.Create().WithRole(RCRTCRole.LIVE_BROADCASTER).Build(); Engine.JoinRoom(RoomId, setup);

② 麦克风控制:

主播可以在房间内打开或关闭本端麦克风

1
python复制代码// 是否启⽤⻨克⻛ int EnableMicrophone(bool enable);

③ 扬声器控制:

主播可以在房间切换声音的播放设置,扬声器或听筒。接入外设时调用此方法,如蓝牙音箱等返回 -1。

1
python复制代码// 是否启⽤扬声器 int EnableSpeaker(bool enable);

④ 发布本地音频:

加入 RTC 房间成功后,发布本地音频

1
ini复制代码Engine.Publish(RCRTCMediaType.AUDIO);

⑤ 取消发布本地音频:

1
ini复制代码Engine.Unpublish(RCRTCMediaType.AUDIO);

⑥ 订阅用户:

1
python复制代码// 订阅⽤户 int Subscribe(String userId, RCRTCMediaType type); int Subscribe(IList<String> userIds, RCRTCMediaType type);

⑦ 取消订阅用户:

1
python复制代码// 取消订阅⽤户 int Unsubscribe(String userId, RCRTCMediaType type); int Unsubscribe(IList<String> userIds, RCRTCMediaType type);

⑧ 离开房间:

1
ini复制代码Engine.LeaveRoom();

本文转载自: 掘金

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

Mysql的常用优化方案及实际操作案例 1 字段类型的设计(

发表于 2021-11-11

4个方面入手

l 设计层面, 使用的字段类型(重要), 存储引擎, 范式规范

l 功能层面, 索引, 数据分区, 查询缓存

l 架构层面, 读写分离, 负载均衡.(主从复制)

l SQL层面, select * 少用.

1 字段类型的设计(字段)

1.1 单表字段数量不要太多

30 极限!

1.2 预留字段

extra1, extra2, extra3

修改表结构的开销很大

alter table add column. 尽可能避免.

线上修改表结构

1, 创建新表

2, 复制旧数据

3, 使用新表

1.3 尽可能小

在满足存储需求的情况下, 少占用空间

tinyint-1, int-4, bigint-8

年月日时分秒:
timestamp-4(1970-2038, 4bytes整数), datetime-8(0000-9999)

存储时间, 使用int, 更容易计算. 存储整型的时间戳, 不用考虑时区差异.

1.4 尽可能定长,占用固定的存储空间

是否占用固定的存储空间, 是定长, 否变长.

varchar(32)变长在乎存储空间,, char(32)定长字符串处理效率

hellokang, hellokang

text不占用记录空间独立文档空间存储. varchar占用记录空间
image.png
存储小数

double-8双精度(选, 13-14位有效数字), float-4单精度, 都是定长类型. 在乎存储空间选浮点数.

php的的浮点类型, 就是双精度

decimal, 定点数. 不会出现精度丢失. 变长数据类型. 存储的有效数字越长, 占用的存储空间越大. 在乎精度, 选择定点数.

实操中, 存储余额, 小单位, 大数值方式存储. int, bigint..

定长表, 如果一个表中所有的字段, 都是定长数据类型. 这样的表, 就是定长表. 每条记录占用的存储空间是一致的. 表的空间结构 更容易被优化.

1.5 尽可能使用整数代替字符串

整数的计算速度很快的.

enum, 枚举, (单选), set, 集合.(多选)

用起来是字符串类型, 内部存储是, 整数型.

enum(‘男’, ‘女’, ‘保密’);

insert into user values (‘男’)

实操时, enum, 和 set类型, 不建议使用. 是因为, 结构维护的成本很高.

enum(‘男’, ‘女’, ‘保密’, ‘男转女’)

1
mysql复制代码alter table modify gender enum(‘男’, ‘女’, ‘保密’, ‘男转女’)

增加枚举选项, 意味着修改表结构. 意味着大量的锁表操作要执行.

enum类型, 使用表关联实现

姓名 性别ID
helleJin 4
性别ID 标题
1 男
2 女
3 保密
4 男转女

set的实现方案, 多对多关联表实现

hobby set(‘篮球1’, ‘羽毛球2’, ‘足球4’, ‘各种球8’)

7 = 蓝,羽毛,球足球

10 = 羽毛球, 各种球

实操实现

用户表

用户ID 用户
helloXing 42

爱好表

爱好ID 内容
1 篮球
2 足球
3 羽毛球

用户和爱好的关联表:

用户ID 爱好ID
42 3
42 2

ip地址, ipv4地址

192.168.93.128

ipv4 char(15)

实操时: int unsigned

ipv4可以很容易的转换成整数:

MySQL:
image.png
PHP:

image.png

1.6 强制增加注释

字段 类型 其他属性 comment “字段描述”

1.7 尽可能not null

null, 在mysql中, 是一个特殊的字段属性.

存储, 计算时, 都比较麻烦.

null存储时, 记录需要开辟额外的存储空间, 记录下来哪些字段可以为null.

计算时, is null, is not Null, 需要使用特殊的运算符. null 和谁计算结果都是null

image.png
通常都使用, 一个特殊的默认值表示

goods表:

1
2
3
mysql复制代码category_id int unsigned null. 

category_id int unsigned not null default 0.

实操时, 更加通用的方案是, 在分类表中, 强制增加一个: 未分类 特殊分类. 用于管理哪些, 不属于任何分类的商品.

分类表:

category_id 分类
1 未分类 不可以编辑

商品表

商品ID 分类ID
停产的商品 1

1.8 外键约束,foreign key

分类表:

category_id 分类
1 未分类 不可以编辑

商品表

商品ID 分类ID
停产的商品 1

保证 商品和分类的完整性, 在商品的分类ID字段上建立外键约束. 与分类表的id建立关联即可.

外键约束的目的: 保证2个表数据之间的关系完整.

此目的, 工作, 在PHP层面(在应用程序层面), 也可以实现.

程序员(应用程序设计人员): 业务逻辑程序员来说, 能使用应用程序做到的, 都使用程序完成.

1.9 字符集尽量使用utf8

utf8mb4, 在utf8字符集的基础上, 增加4字节的字符.

emoji表情, 就是存储在mb4字符集中的字符!

gbk:

2 范式,NF,Normal,Format,规范的格式

设计表结构的一些规范,约束等.

根据约束的等级, 形成1NF, 2NF, 3NF…6NF.

常规的设计,要求满足3NF即可.
结论:

在设计数据表时, 要满足下面的要求(重要)

1, 每种类型的数据, 使用独立的表进行存储.

2, 每张表, 存在一个独立的主键字段, id.最好与业务逻辑无关.. 主键字段独立, 仅有标志性, 没有业务逻辑性.

3, 数据间的关联联系, 使用关联字段进行处理. 一对多(关联字段), 多对多(关联表).

2.1 1NF,第一范式,原子性

要求, 设计的表, 满足字段的原子性, 字段不能再次分割.
例如, 讲师授课信息.

讲师 性别 班级 教室 时间段
孔子 男 鲁6 305 2017-01-02,2017-03-20

时间段这个列, 在逻辑上, 由2个数据构成: 开始和结束

如果程序, 需要独立的获取开始时间. 以上的表的设计, 及没有满足原子性.

应该: 拆分成2个字段, 开始和结束.

讲师 性别 班级 教室 开始 结束
孔子 男 鲁6 305 2017-01-02 2017-03- 20

以上的原子性的例子, 基于业务逻辑(功能).

注意: 关系型数据库,默认满足原子性, 满足第一范式

2.2 2NF,第二范式,消除部分依赖

在满足1NF的基础上, 要求消除对主键的部分依赖.

主键, 记录的标志主键.

依赖, A字段确定, 那么B字段的值也随之确定, 那么说B字段依赖于A字段.

下面的例子: 讲师代课信息的例子

讲师 性别 班级 教室 开始 结束
孔子 男 鲁6 305 2017-01-02 2017-03- 20
孔子 男 齐3 405 2017-03-20 2017-04- 05
老子 保密 鲁6 305 2017-03-21 2017-04- 05

设置主键: 需要使用联合主键, primary key (讲师, 班级)
可见:

性别, 由讲师即可确定

教室, 由班级即可确定

开始, 结束, 由主键(讲师+班级)确定

部分依赖: 性别和教室, 就是部分依赖. 如果字段依赖于联合主键中的一部分字段, 称之为, 对主键的部分依赖.

2NF, 要求表, 没有部分依赖.

实现方式: 增加一个独立的字段主键.

ID 讲师 性别 班级 教室 开始 结束
2 孔子 男 鲁6 305 2017-01-02 2017-03- 20
3 孔子 男 齐3 405 2017-03-20 2017-04- 05
6 老子 保密 鲁6 305 2017-03-21 2017-04- 05

此时, 对主键的部分依赖就消失了.

2.3 3NF,第三范式,消除传递依赖

在满足2NF的前提下, 消除对主键的传递依赖.

A依赖于B, B依赖与主键, 则A传递依赖来于主键.

ID 讲师 性别 班级 教室 开始 结束
2 孔子 男 鲁6 305 2017-01-02 2017-03- 20
3 孔子 男 齐3 405 2017-03-20 2017-04- 05
6 老子 保密 鲁6 305 2017-03-21 2017-04- 05

上面的例子, 性别依赖于讲师, 讲师依赖与主键. 性别传递依赖于主键.

3NF的设计, 消除以上的传递依赖.

方案:

将讲师, 与 班级 信息, 独立到不同的表中.

关系使用关联字段进行表示, 形成的结构如下:

讲师:

讲师ID 讲师 性别
23 孔子 男
45 老子 保密

班级:

班级ID 班级 教室
4 鲁6 305
6 齐3 405

讲师授课信息

ID 讲师ID 班级ID 开始 结束
1 23 4 2017-01-02 2017-03- 20
2 23 6 2017-03-20 2017-04- 05
3 45 4 2017-03-21 2017-04- 05

2.4 目的

1, 减少数据冗余.

不要出现重复的数据

2, 便于更新维护.

2.5 逆范式, 打破范式, 不满足范式

有时, 为了优化某些操作.. 可能增加数据冗余.(相当于缓存的概念)

goods

goods_id, category_id

category

category_id, title

例如, 在查询商品信息时, 每次都需要获取到商品所属分类.

商品, 某某分类

商品, 某某分类

常规的, 使用join查询完成.

1
msysql复制代码select g.*, c.title from goods g left join category c on g.category_id=c.category_id.

此时, 在goods表中, 增加category_title字段:

goods

1
2
3
mysql复制代码goods_id, category_id, category_title

select * from goods

即可.

以上, 就是逆范式.

通过逆范式的手段, 达到优化某些查询(操作)的目的.
慎做, 一旦打破方式, 需要通过业务逻辑补偿, 数据的完整性.

(任何更新分类名称的操作, 都需要去同时维护商品表)

3 存储引擎选择

(之前的问题: innodb还是myisam?) 目前的答案: innodb.

(现在的问题: mysql还是mariaDb?)

MySQL, 支持多种存储引擎. storageengine, 数据和索引存储的不同方式, 不同的文件系统.

ISAM文件系统, 被MysQL拿来存储数据, 形成了MyISAM.

innodb以插件的形式, 进入到MySQL

3.1 ## 1.1 show engines,查看所有的引擎

image.png
image.png
image.png
image.png

3.2 myisam

高速的存取引擎, 适合做以查询和插入为主的业务逻辑.

3.2.1 存储方式

3.2.1.1 数据和索引是分开存储到不同的文件中,myd, myi

image.png
形成的文件: .frm结构文件, .myd数据文件, .myi索引文件

image.png

3.2.1.2 记录是按照插入顺序存储的:

插入数据

image.png
直接获取不排序, 使用原生的顺序获取:

image.png
每当插入一个数据, 自动在表的末端, 进行插入(追加)即可. 插入速度特别快.

3.2.2 功能上

image.png
支持, 全文索引. 但是不支持中文.
节省存储空间.

3.2.3 处理并发

仅支持, 表级锁定. 并发能力相对较弱.

并发插入.

3.3 innodb

复杂的业务逻辑更新, 频繁的update, delete, 适合innodb. 查询性能也不差.

3.3.1 存储方式

3.3.1.1 数据和索引, 集中存储在一个文件中, 称之为表空间文件 tablespace.

image.png
存储的结构, 2个文件..frm结构文件. .ibd表空间文件,数据和索引都在其中.
image.png

3.3.1.2 记录是按照主键顺序存储的:

插入数据
image.png
获取数据, 与插入不一致, 自动主键顺序排序.
image.png
意味着, 插入时, 自动完成排序工作. 相对较慢.

注意: 主键对于innodb来说, 非常重要. 数据是按照主键顺序排序, 一旦更改了主键, 数据需要重新排序, 慢. innodb表一定要使用与业务逻辑无关的字段, 作为主键.

3.3.2 功能上

image.png
支持: 事务(ACID), 外键.

事务, 外键, 都是在数据库层面保证数据完整性的问题.

1.3.3 处理并发

支持, 行级锁定. 也支持表级别锁定.

并发能力强.

4 锁定

本文转载自: 掘金

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

3万字聊聊什么是Redis(一)

发表于 2021-11-11

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

大家好,我是Leo

结束了漫长了MySQL,开始步入了Redis的殿堂。最近在做Redis技术输出时,明显发现进一步熟悉MySQL之后,对Redis的理解容易了许多。或许这就是进步吧!

下面的思路部分,可以帮助你更好的理解这篇文章的知识体系。

思路


整体结构


Redis主要是由访问框架,操作模块,索引模块,存储模块,高可用集群支撑模块,高可用扩展支撑模块等组成,

Redis还有一些,丰富的数据类型,数据压缩,过期机制,数据淘汰策略,分片机制,哨兵模式,主从复制,集群化,高可用,统计模块,通知模块,调试模块,元数据查询等辅助功能。

接下来的Redis学习之路,主要是围绕介绍上述模块,功能,策略,机制,算法等知识的输出。

五大类型

String

String类型应该是我们用的最多的一种类型,它的底层是由简单的动态字符串实现的。

hash

hash类型也是我们用的最多的一种类型了,它是由压缩列表+哈希表共同实现的一种数据类型

list

list它是一种列表类型,也是我们常用类型之一,它是由双向链表+压缩列表共同实现的一种数据类型

set

set集合和上述类型不同,他不允许重复,所以一些特定的场景会优先考虑set类型,它是由整数数组+哈希表共同实现的一种数据类型

sort set

sortset是在set的基础上,做的一个提升,不允许重复的时候,还可以处理有序。主要应用与排序表之类的场景需求,它是由压缩链表+跳表实现的一种数据类型

数据结构

哈希表会在下文rehash那里详细介绍一下。

整数数组和双向链表也很常见,它们的操作特征都是顺序读写,也就是通过数组下标或者链表的指针逐个元素访问,操作复杂度基本是 O(N),操作效率比较低。

压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。和数组不同的是,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了。

跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位。在下述文章中的第五章节介绍过了跳表的相关说明。

3万字聊聊什么是MySQL

哈希为啥变慢了

Redis在处理一个键值对时,会进行一次hash处理,把键处理成一个地址码写入Redis的存储模块,随着我们key的越来越多,有一些key会存在同一个地址码的情况。(我在写hashmap的时候就介绍过hash碰撞的问题)

出现这种情况之后Redis作了一个键值对的扩展,也就是键值对+链表的方式。如下图,多个数据经过hash处理之后,都落到了key1值上。一个卡槽不可能存放两个值,于是就在这个卡槽存了指向一个链表的指针,通过链表存储多个值。

哈希链表

链表处理的就是多个key一样的问题,随着数据量的发展,哈希碰撞的情况越来越频繁,链表的数据也就越来越多。hash的性能是O(1),链表的性能是O(n)。所以整体的性能被拖下来了。为了改变这一现状,Redis引入了rehash。

rehash

rehash就是增加现有的哈希桶的数量,让逐渐增多的元素能在更多的哈希桶之间分散保存。从而减少单个桶的链表的元素数量,同时也减少单个桶的冲突。

首先Redis会先创建两个全局哈希表,我们这里定义为哈希表A,哈希表B。我们在插入一个数据时,先先存入A,随着A越来越多,Redis开始执行rehash操作。主要分为三步:

  • 给B分配更多的空间,一般都是A的两倍
  • 把A中的数据全部拷贝到B中
  • 释放A

上述rehash流程我们可以看出,当A中存在大量的数据,拷贝的效率是非常慢的!因为Redis的单线程性还会造成阻塞,导致Redis短时间无法提供服务。为了避免这一问题,Redis在rehash的基础上,采用了渐进式rehash。

渐进式 rehash

进化点就是在第二步拷贝的时候,并不是一次性拷贝的,而是分批次拷贝。在处理一个请求时,从A中的第一个索引位置开始,顺带着将这个索引位置上的所有元素拷贝到B中。等下一个请求后,再从A表中的下一个索引位置继续拷贝操作。这样就巧妙地把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。

Redis单线程还是多线程

先来普及一下多线程的知识,一个CPU在运行多个线程时,会有一个多线程调用的消耗问题,而且还有多个线程调用时数据一致性的问题。这些都要单独处理,单独处理又会消耗性能。于是Redis统筹兼顾采用了单,多线程并用的思路。

在处理数据写入,读取属于键值对数据操作,采用单线程操作。在请求连接,从socket中读取请求,解析客户端发送请求,采用多线程操作。

Redis巧妙的把所有需要延迟等待的操作全部转交给了多线程处理,在不需要等待的全部单线程处理。个人感觉这种设计思路很棒

tip:如果不按照这种方式设计的,连接之后等待,发送等待,接收等待估计要等死你哦。造成Redis线程阻塞,无法处理其他请求。

多路复用机制

IO多路复用机制是指一个线程处理多个IO流,也是我们经常听到的select/epoll机制。那么那些连接,等待的操作Redis都是如何处理的呢?

在Redis只运行单线程的情况下,同一时间存在多个监听套接字,和已连接的套接字,内核会一直监听这些连接请求和数据请求。一旦客户端发送请求就会以事件的方式通知Redis主线程处理。这就是Redis线程处理多个IO流的效果。

上文说到以事件方式通知Redis这里我们做一个扩展,select/epoll提供了基于事件的回调机制,不同的事件会调用相应的处理函数。一旦请求来了,立刻加到事件队列中,Redis单线程就会源源不断的处理该事件队列。解决了等待与扫描的资源浪费问题。

安全机制

Redis的持久化安全机制主要有两大块,一块是AOF日志,一块是RDB快照,接下来我们聊聊AOF与RDB的一些区别吧

AOF

Redis为了提升性能采用的是写后日志,先执行命令,后写日志,这样做的好处主要有两点

  • 只有当命令执行成功之后才会写入日志。这样就避免了写入日志之后,命令执行错误还要把日志删掉的问题。
  • 先执行写入操作,后写日志,这样同时也避免了阻塞当前的写操作

坏处是:

  • 如果一个命令执行完后,还没记录日志就宕机了,那么这个命令和相应的数据就有丢失的风险。
  • AOF虽然避免了对当前命令的阻塞,但可能会对下一个操作带来阻塞风险。因为AOF日志也是在主线程中执行的,并且是写入磁盘。

文件格式:

Redis收到一个 “set huanshao 公众号欢少的成长之路” 命令后,AOF的日志内容是,”*3” 表示当前命令有三个部分,每部分都是由“数字开头,后面紧跟着具体的命令、键或值。这里,数字表示这部分中的命令、键或值一共有多少字节。例如,+数字”开头,后面紧跟着具体的命令、键或值。这里,“数字”表示这部分中的命令、键或值一共有多少字节。例如,“3 set”表示这部分有 3 个字节,也就是“set”命令。

AOF写入策略

AOF提供了三种appendfsync可选值

  • Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
  • Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
  • No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。

这三种都无法做到两全其美,同步写会可以做到数据一致性,但是写入磁盘的这个性能对比内存来说太差了,如果是每秒写的话,就会丢失1秒的数据,如果No配置的话宕机后丢失数据比较多。

最后三种配置如何选择,应该根据特定的业务场景。如果数据安全性过高就选择同步写回,如果适中就每秒写回,没安全性的话就选择No。

AOF重写机制

AOF日志是追加形式的,避免不了的就是文件过大之后,再写入日志的性能会有所下降,Redis为了解决这一难题,引入了重写机制。

重写机制主要做的事情是记录一个key值的最终修改结果,修改的历史记录一律排除。这样一来,一个命令就只有一个日志。如果要拿AOF日志恢复数据的话也能恢复出正确的数据。

重写机制流程就是主线程fork出一个后台子线程 bgrewriteaof后,fork会把主线程的内存拷贝一份给子线程bgrewriteaof,这样子线程就可以在不影响主线程阻塞的情况下进行重写操作了。

在这段期间,如果有新的请求写入过来,Redis会有两个日志,一个日志指正在使用的 AOF 日志,Redis 会把这个操作写到它的缓冲区。这样一来,即使宕机了,这个 AOF 日志的操作仍然是齐全的,可以用于恢复。另一处日志指新的 AOF 重写日志。这个操作也会被写到重写日志的缓冲区。这样,重写日志也不会丢失最新的操作。等到拷贝数据的所有操作记录重写完成后,重写日志记录的这些最新操作也会写入新的 AOF 文件,以保证数据库最新状态的记录。此时,我们就可以用新的 AOF 文件替代旧文件了。

RDB

RDB是一种内存快照,它是系统某一刻的数据备份写到磁盘上。这样就可以达到宕机后,可以恢复某一刻之前的所有数据。

生成RDB的两种方式

  • save:在主线程中执行,会导致阻塞;
  • bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的 默认配置。

写时复制技术

首先介绍一下写时复制技术的由来,在Redis做RDB快照时(当前RDB还没有做完),来了一个修改数据的请求。如果把这个请求写入快照,那么就不符合那一刻的数据一致性。如果不写入快照把他丢弃,就会造成数据丢失还是会有数据一致性的问题。所以Redis借助操作系统提供的写时复制技术,在执行快照的同时,正常处理写操作。

处理流程

主线程fork创建子线程bgsave,可以共享主线程的所有内存数据,bgsave子线程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。如果主线程对这些数据都是读操作,那么互不影响。如果是修改操作的话就会把这块数据复制一份,生成该数据的副本。然后主线程在这个副本上进行修改。同时bgsave 子进程可以继续把原来的数据写入 RDB 文件。

这样保证了快照的数据一致性,也保证了快照期间对正常业务的影响。

既然RDB那么牛逼,可否用RDB做持久化呢?

如果我们采用RDB做持久化的话,那么就要一直进行RDB快照,如果每2秒做一次快照的话,最坏的打算就要少50%的数据量,如果每秒做一次快照,可以完全保证数据的一致性但是带来的负面影响也是非常大的。

  • 频繁快照,导致磁盘IO占用影响,且磁盘内存开销非常大
  • RDB由bgsave处理,虽然不阻塞主线程,但是主线程新建bgsave时,会影响主线程,如果每秒新建一次,有可能会阻塞主线程的。

全量备份不行的话,增量备份是否可以用RDB做持久化呢?

增量备份与全量备份的区别就是,增量备份只备份修改的数据。如果是这样的话,我们就需要对每一个数据都加一个记录,这样开销是十分大的。如果为了增量备份牺牲了宝贵的内存资源,这就有点得不偿失了。

实战应用

上述我们介绍了AOF与RDB的区别,流程,优缺点。我们可以发现,如果只依靠某一种方式进行持久化都无法有效的达到数据一致性。

如果只用RDB,快照的频率不好把握,如果使用AOF,文件持续变大也是吃不消的。

最优的策略就是 RDB + AOF 假如每小时备份一次RDB,我们就可以利用RDB文件恢复那一刻的所有数据,然后再用AOF日志恢复这一小时的数据。

结尾

看到这里,应该都是真粉了,点赞+分享+在看+关注 就是对我最大支持。欢少的成长之路 感谢你

我们下期再见!

本文转载自: 掘金

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

JDK16的新特性 简介 JDK16的新特性 语言方面的提升

发表于 2021-11-11

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

简介

在2021年3月16日,JDK的迎来了它的一个新版本JDK16,虽然JDK16不是LTS版本,但是作为下一个LTS版本JDK17的先行版本,JDK16为我们带来了17个方面的提升,包括了新的语言特性、新的工具、内存管理的提升等方面。

所以一起来看看,JDK16到底为我们提供了些什么新的特性。

JDK16的新特性

总的来说,JDK16有下面的一些新特性:

  • 一些在JDK14中引入的新特性,最终在JDK16中确定了。
  • 内存管理的提升
  • 新的打包工具
  • UNIX-Domain Socket channels
  • Value-based Classes的警告
  • Encapsulating JDK Internals by default
  • 提供了 C++ 14语言特性
  • 其他的一些预览版本的新特性

下面图是JDK从8开始到16的新特性个数:

可以看到JDK8和JDK9是最多的,后面基本上变动比较少。

JDK8引入了stream,lambda,泛型等一系列非常有用的特性。而JDK9则引入了新的JPMS模块化系统,所以变动比较多。

相对而言,JDK10之后变动基本上比较小,也有可能跟固定6个月发一次版本有关系。毕竟时间比较短,所以版本的变动也比较小。

注意,JDK16并不是一个LTS版本,在9月发布的JDK17才是!,大家可以关注我的后续关于JDK17新特性的文章。到现在为止,JAVA的LTS版本就有JDK8,JDK11和JDK17了。你现在用的是哪个呢?

语言方面的提升

JDK16在语言上的提升主要有两个:Pattern matching和records。这两个新特性都是在JDK14中作为预览版本引入了,最终到JDK16变成了final版本。

先来看一下Pattern matching, Pattern matching主要说的就是instanceof关键词,我们知道在JAVA中判断一个对象是不是某个类的实例,则可以使用instanceof,如果是该类的实例或者子类,则返回true,否则返回false。

但是在判断完之后,要想使用对应的对象,还需要显示的进行类型转换如下所示:

1
2
3
4
5
javascript复制代码//传统写法
if(site instanceof String){
String stringSite = (String)site;
System.out.println(stringSite.length());
}

在JDK16中的Pattern matching中,可以这样写:

1
2
3
4
scss复制代码 //JDK16写法
if(site instanceof String stringSite){
System.out.println(stringSite.length());
}

另外一个final版本的就是在JDK14和15中引入的Records,Records是一个特殊的java类,主要用来表示不可变对象的结构体。

来看一个Records的定义:

1
2
3
4
5
arduino复制代码public record Address(
String addressName,
String city
) {
}

上面我们定义了一个Address对象,它有两个属性,分别是addressName和city,如果反编译上面代码的编译结果,可以得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
arduino复制代码public record Address(String addressName, String city) {
public Address(String addressName, String city) {
this.addressName = addressName;
this.city = city;
}

public String addressName() {
return this.addressName;
}

public String city() {
return this.city;
}
}

实际上就等于传统的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
arduino复制代码public class AddressOld {

private final String addressName;
private final String city;

public AddressOld(String addressName, String city) {
this.addressName = addressName;
this.city = city;
}

public String getAddressName() {
return addressName;
}

public String getCity() {
return city;
}
}

但是在编写上要方便和简单很多。

内存管理方面的提升

在看看内存管理方面的提升,主要有两方面:Elastic Metaspace和ZGC的并发线程堆栈处理。

Metaspace 的主要功能是管理类的元数据的内存。 引入 Elastic Metaspace 是为了改进 HotSpot JVM 中元空间内存的分配和释放。 可以更快地将不需要的内存返回给操作系统,从而减少开销和内存碎片。

Elastic Metaspace使用较小的块分配内存,并通过将未使用的元空间内存返回给操作系统来提高弹性。 它可以提高性能并降低维护成本。

那么什么是ZGC的并发线程堆栈处理呢?

我们知道ZGC是HotSpot JVM中一种低延时的垃圾回收算法。但是在线程的堆栈处理过程中,总有一个制约因素就是safepoints。在safepoints这个点,java的线程是要暂停执行的,从而限制了GC的效率。

而ZGC的并发线程堆栈处理可以保证java线程可以在GC safepoints的同时可以并发执行。

Unix-Domain Socket Channel

一般来说Socket通信是基于TCP/IP的,但是熟悉unix的朋友应该知道,在unix中一切都是以文件形式存在的,即便是在内部进程的通讯也是如此。

如果是同一个host上的进程进行通讯,使用unix本身的inter-process communication (IPC)无疑是最快的方式,并且更加安全。

所以在JDK16中增加了对Unix-Domain Socket Channel的支持。

Warning For Value-based Classes

这个是什么意思呢? 我们知道java中对应的primary类型都有一个Object类型,比如int对应的是Integer。

如果是用Integer的构造函数,则我们可以这样构造:

1
sql复制代码 Integer integer= new Integer(100);

但是在JDK16中,这种构造函数已经被废弃了:

1
2
3
4
ini复制代码    @Deprecated(since="9", forRemoval = true)
public Integer(int value) {
this.value = value;
}

我们可以直接这样写:

1
ini复制代码Integer integer2= 100;

封装内部的JDK包

一般来说,我们用的包都是JDK公开的API,但是有时候还是会用到一些JDK内部使用的类,这种类是不建议直接在外部使用的,JDK16对大部分的这种类做了封装,后面大家直接在标准JDK中查找使用即可。

C++ 14语言特性

这个是值JDK底层的C++ 源代码使用C++ 14语言特性,一般的JDK使用者是无法直接感受的。

预览语言新特性

在JDK16中还加入了几个预览的语言新特性.这里主要讲一下Vector API和Sealed Classes.

Vector API的想法是提供一种向量计算方法,最终能够比传统的标量计算方法(在支持 CPU 架构上)执行得更好。什么叫做向量计算呢?熟悉pandas的朋友可能知道,在pandas可以方便的对矩阵进行计算,如果用java实现则需要计算矩阵中的每个元素,非常麻烦,这也是python的pandas库能够流行的原因。

现在JDK16也可以做到了,我们一起来看看,先是传统写法:

1
2
3
4
5
6
7
8
9
ini复制代码//传统写法
int[] x = {1, 2, 3, 4};
int[] y = {4, 3, 2, 1};

int[] c = new int[x.length];

for (int i = 0; i < x.length; i++) {
c[i] =x[i] * y[i];
}

如果我们希望两个数组的数字相乘,则只能进行每个元素的遍历。现在的写法:

1
2
3
4
ini复制代码        var vectorA = IntVector.fromArray(IntVector.SPECIES_128, x, 0);
var vectorB = IntVector.fromArray(IntVector.SPECIES_128, y, 0);
var vectorC = vectorA.mul(vectorB);
vectorC.intoArray(c, 0);

我们构建两个Vector变量,直接调用Vector类的mul方法即可。

fromArray中有三个参数,第一个是向量的长度,第二是原数组,第三个是偏移量。因为一个int有4个字节,所以这里我们使用SPECIES_128。

Sealed Classes是在JDK15中引入的概念,它表示某个类允许哪些类来继承它:

1
2
3
4
5
6
7
8
scala复制代码public sealed class SealExample permits Seal1, Seal2{
}

public non-sealed class Seal1 extends SealExample {
}

public final class Seal2 extends SealExample {
}

final表示Seal2不能再被继承了。non-sealed 表示可以允许任何类继承。

总结

以上就是JDK16给我们带来的新特性,总体而言是很有用的,大家觉得呢?

本文例子learn-java-base-9-to-20

本文已收录于 www.flydean.com/26-jdk16-ne…

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

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

本文转载自: 掘金

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

字节跳动自研 OpenBMC 方案成功上线,STE 团队工程

发表于 2021-11-11

OpenBMC 是 Linux Foundation 组织里的一个项目,也是目前开源 BMC 里方案最成熟、社区最活跃的项目。日前,我们得知字节跳动系统部 STE 团队自研的 OpenBMC,已在内部成功上线,并稳定运行了两个多月。这也是字节跳动首次正式上线并初步产品化的 OpenBMC 方案。

为此,我们专访了字节跳动系统部 STE 团队工程师,他为我们分享了字节跳动自研 OpenBMC 的布局和推进过程。

传统 BMC 生态封闭,及时响应困难重重

BMC 是服务器上的管理模块,它包含独立的 SoC 以及 SoC 上运行的系统,完成对服务器的管理、监控、并对外提供服务。对于服务器来说是不可或缺的重要组成部分。

目前服务器 BMC 的方案里,部分厂商有完全自研的方案,部分厂商则会基于 IBV (Independent BMC Vendor) 的商业 BMC 进行定制化开发,但这些都是闭源的,服务器使用者无法掌控 BMC 的完整功能。

同时,商业 BMC 也具有诸多局限,比如一些不必要的功能,可能影响网络和系统稳定性;修复 bug、漏洞往往需要反馈 ODM 厂商,甚至需要 IBV 支持,这使得修复时间过长;部分 bug 只在特定环境下复现,更增加了修复的难度。

随着互联网的不断发展,数据中心的不断壮大,对数据中心服务的运维需求,也越来越呈现出精细化、定制化的趋势。

严重依赖各 ODM 厂商、IBV 的传统 BMC 开发模式,开发周期长,无法实时响应互联网多变的需求。一个更加开放、更加现代的 BMC 方案,成为当务之急。

功能新架构优,OpenBMC 成字节跳动首选

OpenBMC 是 Linux Foundation 里的一个开源项目,它方案成熟、社区活跃,受到 IBM、Google、Facebook、Intel、Inspur 等国内外各大厂商的支持。

在架构方面,OpenBMC 支持 Aspeed、Nuvoton 等多种 BMC 芯片,以及 x86、ARM、OpenPOWER 等多种服务器架构,更是采用 Linux kernel 5.15、Systemd 249 (249.4+)、GCC 11.2.0、C++20 等最新的现代化技术架构。

OpenBMC 解决方案流程一览

但是 OpenBMC 最初是为了解决国外企业的数据中心需求设计开发的,无法完全适配国内互联网数据中心需求。基于 OpenBMC,结合字节跳动服务器管理和运维,开发自研版本,成为更理想的选择。

从社区版本到落地应用,OpenBMC 在字节跳动产品化

为了加快 OpenBMC 与字节跳动内部业务的适配,STE 团队调研了 OpenBMC 社区里,各个模块的技术方案,对比字节跳动内部对服务器的管理和运维,挑选出适用的方案。

OpenBMC 在字节跳动产品化过程示意图

对于能满足字节跳动内部需求的模块,STE 团队决定直接使用社区版本;对于额外的需求,则会基于社区模块,添加并增强功能,最终形成一个完整的解决方案,进行深度测试。

STE 团队工程师,为我们分享了具体的方案:

  • 对于 x86 specfic 的模块,复用 Intel 的模块,例如 PECI,node-manager
  • 对于 power control,复用x86-power-control
  • 对于各类传感器,基于现有的entity-manager/dbus-sensors/virtual-sensor,添加 yaml 来定义需要的 sensor
  • 对于风扇控制,复用phosphor-pid-control并添加了单、双转子的检测和适配
  • 对于 FRU,根据硬件情况改造相关代码,使其支持非标准 FRU 格式
  • 对于 SEL,实现新的统一的 logging 方式,把 log entry 转化为标准 SEL
  • 对于 code-update,基于phopsphor-bmc-code-mgmt添加了 BIOS、CPLD、FPGA 等模块的支持
  • 对于字节跳动内部的管理、运维,新增了代码来完成线上的适配工作

此外, STE 团队也在字节跳动内部搭建了 CI 服务器,完成了自动化的编译和测试,并且与开源社区的 Gerrit 打通:

  • 对于内部的每一个 Push/MergeRequest,会跑 CI
  • 对于每一次编译,都会通过 QEMU 来跑基本的测试
  • 对于重要的 release tag,还会通过 HW CI 在实际服务器上验证
  • 对于开源社区里meta-bytedance的每一个改动,也会跑编译和 QEMU CI

积极呼吁行业合作,共同参与社区共建

据 STE 团队工程师介绍,OpenBMC 项目目前已经在两种不同配置的机器上顺利完成上线,并持续稳定运行了两个多月。

同时,STE 团队的工程师们,也在 OpenBMC 开源社区里提交了涉及新 feature 及 bug fix 的诸多 commits,目前已经成为了 OpenBMC Technical Oversight Forum (TOF) 的一员。

未来,字节跳动系统部 STE 团队将在更多的平台上持续研发 OpenBMC,来更好、更快速地支持服务器的管理和运维。工程师还表示,在后续开发中,STE 团队希望能与更多厂商进行全方位、多维度的紧密合作共建,共同开发更符合国内互联网数据中心需求的开源 BMC 方案。

专访最后,STE 团队的工程师也充分表达了对于优秀人才的渴求,欢迎对新技术、开源感兴趣的小伙伴加入字节跳动系统部 STE 团队,用最新的 kernel、最新的编译器,写最 modern 的 C++。


关于字节跳动系统部 STE 团队:

字节跳动系统部 STE 团队 (STE=System Technologies & Engineering,系统技术与工程) 一直致力于操作系统内核与虚拟化、系统基础软件与基础库的构建和性能优化、超大规模数据中心的系统稳定性和可靠性建设、新硬件与软件的协同设计等基础技术领域的研发与工程化落地,具备全面的基础软件工程能力,为字节上层业务保驾护航。同时,团队积极关注社区技术动向,拥抱开源和标准。

更多招聘信息,可联系 chenziying@bytedance.com 获取。

本文转载自: 掘金

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

MyBatis-Plus快速入门 1简介 2快速上手 3

发表于 2021-11-11

1.简介

MyBatis-Plus (简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

1.1.特性

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
  • 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
  • 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
  • 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
  • 内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

1.2.支持数据库

任何能使用 mybatis 进行 CRUD, 并且支持标准 SQL 的数据库,具体支持情况如下,如果不在下列表查看分页部分教程 PR 您的支持。

  • mysql,oracle,db2,h2,hsql,sqlite,postgresql,sqlserver,Phoenix,Gauss ,clickhouse,Sybase,OceanBase,Firebird,cubrid,goldilocks,csiidb
  • 达梦数据库,虚谷数据库,人大金仓数据库,南大通用(华库)数据库,南大通用数据库,神通数据库,瀚高数据库

1.3.框架结构

image-20211110150643496

2.快速上手

2.1.pom.xml导入MyBatis Plus依赖

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>Latest Version</version>
</dependency>

2.2.在application.yml中配置数据源

1
2
3
4
5
6
7
8
9
10
11
12
yaml复制代码# 配置数据源
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/wyl? useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
username: root
password: 123456

# 配置日志,输出更详细日志信息
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

2.3.根据数据库表创建实体类

1
2
3
4
5
6
7
8
9
java复制代码@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Long id;
private String name;
private Integer age;
private String email;
}

2.4.创建Mapper接口

1
2
3
4
5
6
java复制代码//在对应的mapper上面继承基本的接口BaseMapper
@ResponseBody //代表持久层
public interface UserMapper extends BaseMapper<User> {
//所有的CRUD操作都已经编写完成了
//你不需要像以前的配置一大堆文件了
}

2.5.测试

启动类需要加 @MapperScan(“mapper所在的包”),否则无法加载mapper bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@SpringBootTest
class MybatisPlusApplicationTests {
//继承了BaseMapper,所有的方法都来自父类
//我们也可以编写自己的扩展方法
@Autowired
private UserMapper userMapper;

@Test
void contextLoads() {
//参数是一个Wrapper,条件构造器,这里我们先不用 null
//查询全部用户
List<User> users = userMapper.selectList(null);
users.forEach(System.out::println);
}
}

那么我的sql 谁写的,方法又去哪了,其实都是mybatis plus。

3.配置日志

我们所有的sql现在是不可见的,我们希望知道他是怎么执行的,所以我们必须要看日志!

1
2
yaml复制代码# 配置日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

4.CRUD

4.1.Service CRUD 接口

4.1.1.Save

1
2
3
4
5
6
java复制代码// 插入一条记录(选择字段,策略插入)
boolean save(T entity);
// 插入(批量)
boolean saveBatch(Collection<T> entityList);
// 插入(批量)
boolean saveBatch(Collection<T> entityList, int batchSize);

4.1.2.SaveOrUpdate

1
2
3
4
5
6
7
8
java复制代码// TableId 注解存在更新记录,否插入一条记录
boolean saveOrUpdate(T entity);
// 根据updateWrapper尝试更新,否继续执行saveOrUpdate(T)方法
boolean saveOrUpdate(T entity, Wrapper<T> updateWrapper);
// 批量修改插入
boolean saveOrUpdateBatch(Collection<T> entityList);
// 批量修改插入
boolean saveOrUpdateBatch(Collection<T> entityList, int batchSize);

4.1.3.Remove

1
2
3
4
5
6
7
8
java复制代码// 根据 entity 条件,删除记录
boolean remove(Wrapper<T> queryWrapper);
// 根据 ID 删除
boolean removeById(Serializable id);
// 根据 columnMap 条件,删除记录
boolean removeByMap(Map<String, Object> columnMap);
// 删除(根据ID 批量删除)
boolean removeByIds(Collection<? extends Serializable> idList);

4.1.4.Update

1
2
3
4
5
6
7
8
9
10
java复制代码// 根据 UpdateWrapper 条件,更新记录 需要设置sqlset
boolean update(Wrapper<T> updateWrapper);
// 根据 whereWrapper 条件,更新记录
boolean update(T updateEntity, Wrapper<T> whereWrapper);
// 根据 ID 选择修改
boolean updateById(T entity);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList, int batchSize);

4.1.5.Get

1
2
3
4
5
6
7
8
9
10
java复制代码// 根据 ID 查询
T getById(Serializable id);
// 根据 Wrapper,查询一条记录。结果集,如果是多个会抛出异常,随机取一条加上限制条件 wrapper.last("LIMIT 1")
T getOne(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
T getOne(Wrapper<T> queryWrapper, boolean throwEx);
// 根据 Wrapper,查询一条记录
Map<String, Object> getMap(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
<V> V getObj(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);

4.1.6.List

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码// 查询所有
List<T> list();
// 查询列表
List<T> list(Wrapper<T> queryWrapper);
// 查询(根据ID 批量查询)
Collection<T> listByIds(Collection<? extends Serializable> idList);
// 查询(根据 columnMap 条件)
Collection<T> listByMap(Map<String, Object> columnMap);
// 查询所有列表
List<Map<String, Object>> listMaps();
// 查询列表
List<Map<String, Object>> listMaps(Wrapper<T> queryWrapper);
// 查询全部记录
List<Object> listObjs();
// 查询全部记录
<V> List<V> listObjs(Function<? super Object, V> mapper);
// 根据 Wrapper 条件,查询全部记录
List<Object> listObjs(Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录
<V> List<V> listObjs(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);

4.1.7.Page

1
2
3
4
5
6
7
8
java复制代码// 无条件分页查询
IPage<T> page(IPage<T> page);
// 条件分页查询
IPage<T> page(IPage<T> page, Wrapper<T> queryWrapper);
// 无条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page);
// 条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page, Wrapper<T> queryWrapper);

4.1.8.Count

1
2
3
4
java复制代码// 查询总记录数
int count();
// 根据 Wrapper 条件,查询总记录数
int count(Wrapper<T> queryWrapper);

4.1.9.Chain

query

1
2
3
4
5
6
7
8
java复制代码// 链式查询 普通
QueryChainWrapper<T> query();
// 链式查询 lambda 式。注意:不支持 Kotlin
LambdaQueryChainWrapper<T> lambdaQuery();

// 示例:
query().eq("column", value).one();
lambdaQuery().eq(Entity::getId, value).list();

update

1
2
3
4
5
6
7
8
java复制代码// 链式更改 普通
UpdateChainWrapper<T> update();
// 链式更改 lambda 式。注意:不支持 Kotlin
LambdaUpdateChainWrapper<T> lambdaUpdate();

// 示例:
update().eq("column", value).remove();
lambdaUpdate().eq(Entity::getId, value).update(entity);

4.2.Mapper CRUD 接口

4.2.1.Insert

1
2
java复制代码// 插入一条记录
int insert(T entity);

4.2.2.Delete

1
2
3
4
5
6
7
8
java复制代码// 根据 entity 条件,删除记录
int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper);
// 删除(根据ID 批量删除)
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根据 ID 删除
int deleteById(Serializable id);
// 根据 columnMap 条件,删除记录
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);

4.2.3.Update

1
2
3
4
java复制代码// 根据 whereWrapper 条件,更新记录
int update(@Param(Constants.ENTITY) T updateEntity, @Param(Constants.WRAPPER) Wrapper<T> whereWrapper);
// 根据 ID 修改
int updateById(@Param(Constants.ENTITY) T entity);

4.2.4.Select

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码// 根据 ID 查询
T selectById(Serializable id);
// 根据 entity 条件,查询一条记录
T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

// 查询(根据ID 批量查询)
List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根据 entity 条件,查询全部记录
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 查询(根据 columnMap 条件)
List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
// 根据 Wrapper 条件,查询全部记录
List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录。注意: 只返回第一个字段的值
List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

// 根据 entity 条件,查询全部记录(并翻页)
IPage<T> selectPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录(并翻页)
IPage<Map<String, Object>> selectMapsPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询总记录数
Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

案例

1
2
3
java复制代码List<User> userList1 = user.selectList(
new EntityWrapper<User>().eq("name", "王延领")
);

分页

1
2
3
4
5
java复制代码// 分页查询 10 条姓名为‘wyl’的用户记录
List<User> userList = user.selectPage(
new Page<User>(1, 10),
new EntityWrapper<User>().eq("name", "wyl")
).getRecords();

结合

1
2
3
4
5
6
7
java复制代码// 分页查询 10 条姓名为‘wyl’、性别为男,且年龄在18至50之间的用户记录
List<User> userList = userMapper.selectPage(
new Page<User>(1, 10),
new EntityWrapper<User>().eq("name", "wyl")
.eq("sex", 0)
.between("age", "18", "50")
);

4.3.mapper 层 选装件

AlwaysUpdateSomeColumnById

1
java复制代码int alwaysUpdateSomeColumnById(T entity);

insertBatchSomeColumn

1
java复制代码int insertBatchSomeColumn(List<T> entityList);

logicDeleteByIdWithFill

1
java复制代码int logicDeleteByIdWithFill(T entity);

4.4.条件构造器

十分重要:Wappper
1636611228(1).png
我们写一些复杂的SQL就可以使用他来替代!
1、测试一,记住查看输出的SQL进行分析

1
2
3
4
5
6
7
8
9
java复制代码@Test
void contextLoads() {
//查询name不为空的用户,并且邮箱不为空的用户,年龄大于12
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.isNotNull("name")
.isNotNull("email")
.ge("age", 12);
userMapper.selectList(wrapper).forEach(System.out::println); //和我们刚刚学习的map对比一下
}

2、测试二,记住查看输出的SQL进行分析

1
2
3
4
5
6
7
8
java复制代码@Test
void test2(){
//查询名字Chanv
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name", "Chanv");
User user = userMapper.selectOne(wrapper);
System.out.println(user);
}

3、测试三

1
2
3
4
5
6
7
8
java复制代码@Test
void test3(){
//查询年龄在19到30岁之间的用户
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.between("age", 19, 30); //区间
Integer count = userMapper.selectCount(wrapper);
System.out.println(count);
}

4、测试四,记住查看输出的SQL进行分析

1
2
3
4
5
6
7
8
9
10
11
java复制代码//模糊查询
@Test
void test4(){
//查询年龄在19到30岁之间的用户
QueryWrapper<User> wrapper = new QueryWrapper<>();
//左和右
wrapper.notLike("name", "b")
.likeRight("email", "t");
List<Map<String, Object>> maps = userMapper.selectMaps(wrapper);
maps.forEach(System.out::println);
}

5、测试五

1
2
3
4
5
6
7
8
java复制代码@Test
void test5(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
//id 在子查询中查出来
wrapper.inSql("id", "select id from user where id < 3");
List<Object> objects = userMapper.selectObjs(wrapper);
objects.forEach(System.out::println);
}

6、测试六

1
2
3
4
5
6
7
8
java复制代码@Test
void test6(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
//通过id进行排序
wrapper.orderByDesc("id");
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}

5.代码生成器

dao、pojo、service、controller都给我自己去编写完成!

AutoGenerator 是 MyBatis-Plus 的代码生成器,通过 AutoGenerator 可以快速生成 Entity、Mapper、Mapper XML、Service、Controller 等各个模块的代码,极大的提升了开发效率。

5.1.导入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<!-- 代码生成器依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.3.1.tmp</version>
</dependency>
<!-- 生成器需要根据模板生成各种组件,所以模板也需要导入 -->
<!-- velocity是默认的模板,除了它以外常用的还有:Freemarker、Beetl -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>1.7</version>
</dependency>

5.2、启动类,任意一个main、@Test方法都行

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
java复制代码package com.wyl.mybatisplus;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
public class Code {
public static void main(String[] args) {
//需要构建一个 代码自动生成器 对象
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
//配置策略

//1、全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
gc.setAuthor("ChanV");
gc.setOpen(false);
gc.setFileOverride(false); //是否覆盖
gc.setServiceName("%sService"); //去Service的I前缀
gc.setIdType(IdType.ID_WORKER);
gc.setDateType(DateType.ONLY_DATE);
gc.setSwagger2(true);
mpg.setGlobalConfig(gc);

//2、设置数据源
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/mybatis-plus?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("root");
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);

//3、包的配置
PackageConfig pc = new PackageConfig();
pc.setModuleName("blog");
pc.setParent("com.chanv");
pc.setEntity("pojo");
pc.setMapper("mapper");
pc.setService("service");
pc.setController("controller");
mpg.setPackageInfo(pc);

//4、策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setInclude("user"); //设置要映射的表名
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setEntityLombokModel(true); //自动lombok
strategy.setLogicDeleteFieldName("deleted");
//自动填充配置
TableFill createTime = new TableFill("create_time", FieldFill.INSERT);
TableFill updateTime = new TableFill("update_time", FieldFill.UPDATE);
ArrayList<TableFill> tableFills = new ArrayList<>();
tableFills.add(createTime);
tableFills.add(updateTime);
strategy.setTableFillList(tableFills);
//乐观锁
strategy.setVersionFieldName("version");
strategy.setRestControllerStyle(true);
strategy.setControllerMappingHyphenStyle(true); //localhost:8080/hello_id_2
mpg.setStrategy(strategy);

mpg.execute(); //执行代码构造器
}
}

以上两步即可完成生成代码功能!

启动类上扫描

1
2
3
4
5
6
7
java复制代码@SpringBootApplication  // 启动类
@MapperScan(value = {"com.wyl.mybatisplus.generator.mapper"}) // 扫描mapper
public class MybatisplusApplication {
public static void main(String[] args) {
SpringApplication.run(MybatisplusApplication.class, args);
}
}

测试类上扫描

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@SpringBootTest
@MapperScan(value = {"com.wyl.mybatisplus.generator.mapper"}) // @MapperScan("mapper的包位置")
class UserServiceTest {

@Autowired
private UserMapper mapper;

@Test
public void test(){
mapper.selectList(null).forEach(System.out::println);
}
}

5.3.测试MVC

5.3.1.后端:controller.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码package com.wyl.mybatisplus.generator.controller;
import com.wyl.mybatisplus.generator.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/generator/user")
public class UserController {

@Autowired
private UserService userService;

@GetMapping("/success")
public ModelAndView index(){
ModelAndView mav = new ModelAndView();
mav.setViewName("success");
mav.addObject("list",userService.list());
return mav;
}
}

5.3.2.前端:html

记得放在templates下哦

index.html

1
2
html复制代码<h1>Index Page...</h1>
<a href="/generator/user/success">展示数据</a>

succuss.html

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
html复制代码<!DOCTYPE html>
<html lang="en">
<!-- 配置thymeleaf模板标签库 -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Success Page</title>
</head>
<body>

<h1>Success Page...</h1>

<table border="1" cellspacing="0" cellpadding="1">
<tr>
<th>编号</th>
<th>用户名</th>
<th>年龄</th>
</tr>
<tr th:each="user:${list}">
<td th:text="${user.id}"></td>
<td th:text="${user.userName}"></td>
<td th:text="${user.userAge}"></td>
</tr>
</table>

</body>
</html>

前端用到thymeleaf模板引擎,需要配置application.yml

1
2
3
4
5
yaml复制代码spring:
# 视图解析
thymeleaf:
prefix: classpath:/templates/
suffix: .html

6.MybatisX 快速开发插件

MybatisX 是一款基于 IDEA 的快速开发插件,为效率而生。

功能

XML跳转跳转

生成代码(需先在idea配置Database配置数据源)

生成代码

重置模板生成代码

JPA提示

生成新增
生成新增

生成查询

生成查询

生成修改

生成修改

生成删除

生成删除

8.乐观锁

在面试过程中,我们经常会被问到乐观锁,悲观锁!这个其实非常简单!

原子引用!

乐观锁:顾名思义十分乐观,他总是认为不会出现问题,无论干什么不去上锁!如果出现了问题,再次更新值测试!

悲观锁:顾名思义十分悲观,他总是任务总是出现问题,无论干什么都会上锁!再去操作!

我们这里主要讲解,乐观锁机制!

乐观锁实现方式:

  • 取出记录,获取当前version
  • 更新时,带上这个version
  • 执行更新时,set version = new version where version = oldversion
  • 如果version不对,就更新失败
1
2
3
4
5
6
7
8
sql复制代码乐观锁:1、先查询,获得版本号 version = 1
-- A
update user set name = "wyl", version = version + 1
where id = 2 and version = 1

-- B 线程抢先完成,这个时候 version = 2,会导致 A 修改失败!
update user set name = "wjm", version = version + 1
where id = 2 and version = 1

测试一下MP的乐观锁插件

1、给数据库中增加version字段!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码//测试乐观锁成功!
@Test
public void testOptimisticLocker(){
//1、查询用户信息
User user = userMapper.selectById(1330080433207046145L);
//2、修改用户信息
user.setName("ChanV");
user.setEmail("1277077741@qq.com");
//3、执行更新操作
userMapper.updateById(user);
}

//测试乐观锁失败!多线程下
@Test
public void testOptimisticLocker2(){
//线程1
User user = userMapper.selectById(5L);
user.setName("ChanV111");
user.setEmail("1277077741@qq.com");
//模拟另一个线程执行了插队操作
User user2 = userMapper.selectById(5L);
user2.setName("ChanV222");
user2.setEmail("1277077741@qq.com");
userMapper.updateById(user2);

//自旋锁多次尝试提交
userMapper.updateById(user); //如果没有乐观锁就会覆盖队线程的值
}

9.全局策略配置:

通过上面的小案例我们可以发现,实体类需要加@TableName注解指定数据库表名,通过@TableId注解指定id的增长策略。实体类少倒也无所谓,实体类一多的话也麻烦。所以可以在spring-dao.xml的文件中进行全局策略配置。

1
2
3
4
5
6
7
8
9
xml复制代码<!-- 5、mybatisplus的全局策略配置 -->
<bean id="globalConfiguration" class="com.baomidou.mybatisplus.entity.GlobalConfiguration">
<!-- 2.3版本后,驼峰命名默认值就是true,所以可不配置 -->
<!--<property name="dbColumnUnderline" value="true"/>-->
<!-- 全局主键自增策略,0表示auto -->
<property name="idType" value="0"/>
<!-- 全局表前缀配置 -->
<property name="tablePrefix" value="tb_"/>
</bean>

这里配置了还没用,还需要在sqlSessionFactory中注入配置才会生效。如下:

1
2
3
4
5
6
7
8
xml复制代码<!-- 3、配置mybatisplus的sqlSessionFactory -->
<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<property name="typeAliasesPackage" value="com.zhu.mybatisplus.entity"/>
<!-- 注入全局配置 -->
<property name="globalConfig" ref="globalConfiguration"/>
</bean>

如此一来,实体类中的@TableName注解和@TableId注解就可以去掉了。

本文转载自: 掘金

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

工作效率:通过pycharm的模板代码减少重复工作

发表于 2021-11-11

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

摘要

在常见的业务开发场景下,经常要开发大量重复的代码,这里代码耗时但又必要,就像我们写分析报告一样,每次都要为固定的格式耗费精力。我们可以更加日常开发经验总结出一些常用的模板代码来帮助我们实现一秒五行的代码开发效率。

业务开发场景

我使用Flask框架来开发后端api服务,以下是开发两个api需要实现的大致代码,需要在urls.py文件中注册路由连接和处理请求类,在views.py文件中实现处理请求类的具体执行逻辑。

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
python复制代码# urls.py::
bp = Blueprint("api", __name__)

bp.add_url_rule(
"/courses/<string:course_id>",
view_func=v.CourseDetailView.as_view("course_detail"),
methods=["GET"],
)

bp.add_url_rule(
"/courses/<string:course_id>/instruction",
view_func=v.CourseInstructionView.as_view("course_instruction"),
methods=["GET"],
)


# views.py::
class CourseDetailView(MethodView):
@swag_from_yml_file("users/course_detail_get.yml")
@permission_required()
def get(self, course_id):
...
return Response()


class CourseInstrauView(MethodView):
@swag_from_yml_file("users/course_instruction_get.yml")
@permission_required()
def get(self, course_id):
...
return Response()

可以看到像注册路由的代码可以抽象为一个模板

1
2
3
4
5
python复制代码bp.add_url_rule(
"$路由链接$",
view_func=$处理方法类$.as_view("$别名$"),
methods=["$支持请求类型$", ...]
)

处理方法实现的代码可以抽象为两个模板(因为一个处理方法类里可以处理多个请求,如:GET、POST)

1
python复制代码class $处理方法类名$View(MethodView):
1
2
3
4
5
python复制代码@swag_from_yml_file("$api文档路径$")
@permission_required()
def $请求方法$(self, $参数$):
$业务逻辑$
return Response()

IDE开发工具pycharm的Live Template

  1. 使用快捷键command+,(mac) 或Ctrl+Alt+s(Windows)
    image
  2. 进入设置页面。在Editor栏搜索Live Templates
    image
  3. 新建Template Group,名字叫Python Flask
    image
  4. 在新的Group下新建模板regisbp
    这个模板是用来注册路由代码的
1
2
3
4
5
6
7
8
python复制代码"""
Abbreviation: regisbp
Description: register blueprint
Template text:
"""
bp.add_url_rule(
"$url$", view_func=$ViewName$View.as_view("$viewIdentify$"),methods=[$Methods$]
)

image
需要注意,记得点这里

image

  1. 在新的Group下新建模板viewClass
    这个模板用来声明处理方法类
1
2
3
4
5
6
python复制代码"""
Abbreviation: viewClass
Description: create ViewClass
Template text:
"""
class $ViewName$View(MethodView):
  1. 在新的Group下新建模板viewMethod
1
2
3
4
5
6
7
8
9
10
python复制代码"""
Abbreviation: viewMethod
Description: view method
Template text:
"""
@swag_from_yml_file("$doc_path$")
@permission_required()
def $method$(self, $args$):
$code$
return encoder.json_response($rv$)

完成,之后开发需要时,只要输入regisbp、viewClass、viewMethod后回车即可。
image

image.png

本文转载自: 掘金

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

集群打包Sealer全系列之四 -- 升级备份扩缩容 前言

发表于 2021-11-11

image.png

前言

  上回对 Clusterfile 进行了详细对讲解,想必大家对 sealer 有更进一步的了解了,那么这篇文章我们聊聊集群备份扩缩容相关的知识。

1.扩缩容

1.1 ALI_CLOUD 模式扩缩容

  ALI_CLOUD 模式就是,provider: ALI_CLOUD

1.1.1 Clusterfile 配置扩缩容

  比如下面这个配置文件是你当前运行的集群 .sealer/my-cluster/ 路径下的 clusterfile 文件(当然你也可以重新写一个文件,然后覆盖运行中的文件):

  我们可以看到 master 节点上的 count 有3个,如果此时你想扩展节点,把 count 的节点数改成4;如果你想缩小节点,把 count 的节点数改成2,然后使用 apply 命令生效 clusterfile

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
yaml复制代码apiVersion: sealer.aliyun.com/v1alpha1
kind: Cluster
metadata:
name: my-cluster
spec:
 #集群镜像
image: registry.cn-qingdao.aliyuncs.com/sealer-io/kubernetes:v1.19.9
 #当前的集群模式
provider: ALI_CLOUD
network:
   # in use NIC name
  interface: eth0
   # Network plug-in name
  cniName: calico
  podCIDR: 100.64.0.0/10
  svcCIDR: 10.96.0.0/22
  withoutCNI: false
certSANS:
  - aliyun-inc.com
  - 10.0.0.2
   
masters:
  cpu: 4
  memory: 4
   #根据count的值进行缩容或者扩容
  count: 3
  systemDisk: 100
  dataDisks:
  - 100
nodes:
  cpu: 4
  memory: 4
   #根据count的值进行缩容或者扩容
  count: 3
  systemDisk: 100
  dataDisks:
  - 100

1.1.2 join 命令配置扩容,delete 缩容

  当然在这个模式下,你也可以用 join 命令扩缩容

1
2
3
4
5
css复制代码# 扩容
sealer join --masters 2 --nodes 3
​
# 缩容
sealer delete --masters 2 --nodes 3

1.2 BAREMETAL 模式扩缩容

  BAREMETAL 模式就是,provider: BAREMETAL

1.2.1 Clusterfile配置扩缩容

  比如下面这个配置文件是你当前运行的集群 .sealer/my-cluster/ 路径下的 clusterfile 文件(当然你也可以重新写一个文件,然后覆盖运行中的文件):

我们可以看到 master 节点上的 ipList 有三个,如果要缩小则删掉其中一个 ip,如果要增加,则加上一个ip,然后 apply 命令让 clusterfile 生效一下。

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
yaml复制代码apiVersion: sealer.aliyun.com/v1alpha1
kind: Cluster
metadata:
name: my-cluster
spec:
 #集群镜像
image: registry.cn-qingdao.aliyuncs.com/sealer-io/kubernetes:v1.19.9
 #当前的集群模式
provider: BAREMETAL
ssh:
   # SSH login password, if you use the key to log in, you don’t need to set it
  passwd:
   ## The absolute path of the ssh private key file, for example /root/.ssh/id_rsa
  pk: xxx
   # The password of the ssh private key file, if there is none, set it to ""
  pkPasswd: xxx
   # ssh login user
  user: root
network:
   # in use NIC name
  interface: eth0
   # Network plug-in name
  cniName: calico
  podCIDR: 100.64.0.0/10
  svcCIDR: 10.96.0.0/22
  withoutCNI: false
certSANS:
  - aliyun-inc.com
  - 10.0.0.2
   
masters:
   #根据iplList的值缩容或者扩容
  ipList:
    - 172.20.126.4
    - 172.20.126.5
    - 172.20.126.6
nodes:
   #根据iplList的值缩容或者扩容
  ipList:
    - 172.20.126.8
    - 172.20.126.9
    - 172.20.126.10

1.2.2 join命令扩容,delete命令缩容

  当然,这个模式下也可以使用 join 命令进行扩容,delete 命令进行缩容

1
2
3
4
5
css复制代码# 扩容
sealer join --masters 192.168.0.2 --nodes 192.168.0.3
​
#缩容
sealer delete --masters 192.168.0.2 --nodes 192.168.0.3

二.备份恢复

  备份利用的是插件机制,kind:Plugin,如下例子 如果你的 yaml 中写了 oss 地址,那么会将你的备份文件上传到oss上去, 如果没有呢,默认是放到 $(rootfs)/plugin/ETCD_BACKUP file 路径下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yaml复制代码---
apiVersion: sealer.aliyun.com/v1alpha1
kind: Plugin
metadata:
name: ETCD_BACKUP
spec:
action: Manual
 # 可以配置oss地址,进行备份存储
data: |
    alioss:
      ossendpoint: oss-cn-hangzhou.aliyuncs.com
      accesskeyid: *****
      accesskeysecrets: ****
      bucketname: etcdbackup
      objectpath: /sealos/

三.升级

  升级集群镜像使用如下命令

1
复制代码sealer upgrade

  upgrade命令的工作流程可以概括如下:

解析命令的参数 -> 执行 Apply() 函数 -> 执行 diff() 函数得到 todoList -> 执行 todoList 中的函数 -> 升级成功 -> 更新当前集群的Image变量值。

源码分析参考官方文档sealer upgrade 命令的原理与实现

结语

  好了,各位铁汁们,关于Clusterfile的详细内容,就介绍到这,更多信息请到sealer官方git地址上去了解。创作不易,请点个赞,点个关注,后续将分享更多有用信息。

推荐阅读

  • 集群打包工具Sealer全系列之一 —— 下载、安装及使用
  • 集群打包Sealer全系列之二——总体架构介绍
  • 集群打包Sealer全系列之三 —— Clusterfile超详解

微信公众号

image.png

本文转载自: 掘金

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

1…371372373…956

开发者博客

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