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

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


  • 首页

  • 归档

  • 搜索

「初看声明宏」匹配

发表于 2021-11-16

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


匹配

这个越到后面你会越清晰,现在直接告诉你:其实类似于 regex。你可以定义特定的参数,固定的值,定义可接受的重复值等等。总之如果你对正则很熟悉,这一块应该没有问题。

下面来看看关于匹配中几个比较重要的事情。

变量

变量以 $ 开头(例如,$value:expr)。它们的结构是:$ name : designator。

  • $ 和 : 都是固定的。
  • 名称遵循Rust变量的惯例。当在transcriber中使用时(见下文),被称为元变量。
  • 标志符不是变量类型。你可以把它们看作是 “语法类别”。在这里,我会坚持使用表达式(expr),因为Rust是 “主要是一种表达式语言”。在这里可以找到一些可能的标志符。

注意:对于 “designator” 这个名字,似乎还没有达成共识。小册 中称它为 “捕获”;Rustbook 中称它为 “片段指定器”;你也会发现有人把它们称为 “类型”。在从源码跳转到源码时要注意这一点。在这里,我将坚持使用标志符这个称呼,正如Rust中提出的例子。

固定参数

这里没有什么神秘的。只需添加它们,不需要$,例如:

1
2
3
4
5
6
7
rust复制代码macro_rules! power {
($value:expr, squared) => { $value.pow(2) }
}

fn main() {
println!("{}", power!(3_i32, squared));
}

这里有一些事情还没有解释。我现在要谈一谈它们。

分隔符

一些标志符需要一些具体的后续工作。表达式需要其中之一:=>/,/;。这就是为什么我不得不在$value:expr和固定值的平方之间添加一个逗号。你可以在 here 找到一个完整的列表。

多匹配

如果我们想让我们的宏不仅计算一个数字的平方,还能计算一个数字的立方,这个该怎么做?

1
2
3
4
rust复制代码macro_rules! power {
($value:expr, squared) => { $value.pow(2_i32) }
($value:expr, cubed) => { $value.pow(3_i32) }
}

多重匹配可以用来匹配不同逻辑。通常情况下,你要从最具体的到最不具体的,来写匹配规则,这样你的调用就不会落入错误的匹配。更多的解释可以在这里找到。

重复项

我们使用的大多数宏都允许有灵活的输入数量。

例如,我们可以调用 vec![2] 或 vec![1,2,3]。这就是匹配与Regex最相似的地方。基本上,我们把变量包在$()里面,然后用一个重复操作符推进后面的匹配。

  • * → 表示任何数量的重复
    • → 表示任何数量,但至少有一个
  • ? → 表示一个可选项,有零个或一个出现

比方说,我们想加N个数字。我们需要至少两个加数。所以我们将有:第一个值;和一个或多个(+)第二个值:

1
2
3
4
5
6
7
rust复制代码macro_rules! adder {
($left:expr, $($right:expr),+) => {}
}

fn main() {
adder!(1, 2, 3, 4);
}

本文转载自: 掘金

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

MySQL数据库基本命令 基本数据命令

发表于 2021-11-16

基本数据命令

– 01.查看mysql服务器中所有数据库

1
mysql复制代码show databases;

– 02.进入某一数据库(进入数据库后,才能操作库中的表和表记录)

– 语法:USE 库名;

1
mysql复制代码use db10;

– 03.查看当前数据库中的所有表

– 先进入某一个库,再查看当前库中的所有表

1
mysql复制代码show tables;

– 04.删除mydb1库

– 语法:DROP DATABASE 库名;

1
mysql复制代码drop database test2

如果存在删除test2,如果不存在则不执行也不报错

1
mysql复制代码drop database if exists test2;

– 05.重新创建mydb1库,指定编码为utf8

– 语法:CREATE DATABASE 库名 CHARSET 编码;

– 需要注意的是,mysql中不支持横杠(-),所以utf-8要写成utf8;

– 如果不存在则创建mydb1;

1
mysql复制代码create database test3 charset=utf8;

– 06.查看建库时的语句(并验证数据库库使用的编码)

– 语法:SHOW CREATE DATABASE 库名;

1
mysql复制代码 show create database test3;

1.2.创建、删除、查看表

– 07.进入mydb1库,删除stu学生表(如果存在)

1
mysql复制代码use mydb1;

– 语法:DROP TABLE 表名;

1
mysql复制代码drop table stu;

删除表,如果存在删除,不存在不报错

1
mysql复制代码drop table if exists stu;

– 08.创建stu学生表(编号[数值类型]、姓名、性别、出生年月、考试成绩[浮点型]),建表的语法:

​ CREATE TABLE 表名(
​ 列名 数据类型,
​ 列名 数据类型,
​ …
​ 列名 数据类型
​ );
​ 创建stu表的SQL语句如下:

1
2
3
4
5
6
7
mysql复制代码create table student(
id int(11),
name VARCHAR(32),
sex char(2),
birthday datetime,
score float
)

– 09.查看stu学生表结构
– 语法:desc 表名

1
mysql复制代码desc stu;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QAqHWuXs-1617954028383)(C:\Users\Administrator.USER-20201125AL\AppData\Roaming\Typora\typora-user-images\image-20210408153333594.png)]


2.新增、更新、删除表记录

– 10.往学生表(stu)中插入记录(数据)

– 语法:INSERT INTO 表名(列名1,列名2,列名3…) VALUES(值1,值2,值3…);
– 如果是在cmd中执行插入记录的语句,先 set names gbk; 再插入记录!

1
mysql复制代码INSERT into stu(id,name,gender,birthday,score) values(1,"张三","男","2021-4-8",99.9);

– 11.查询stu表所有学生的信息

– 语法:SELECT 列名 | * FROM 表名

1
mysql复制代码SELECT * from stu;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LN4p1hrf-1617954028387)(C:\Users\Administrator.USER-20201125AL\AppData\Roaming\Typora\typora-user-images\image-20210408153927313.png)]

– 12.修改stu表中所有学生的成绩,加10分特长分

– 修改语法: UPDATE 表名 SET 列=值,列=值,列=值…[WHERE子句];\

1
mysql复制代码update stu set score=score+10;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ww2EcMT9-1617954028405)(C:\Users\Administrator.USER-20201125AL\AppData\Roaming\Typora\typora-user-images\image-20210408154756728.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KRQbt4Yn-1617954028408)(C:\Users\Administrator.USER-20201125AL\AppData\Roaming\Typora\typora-user-images\image-20210408154828368.png)]

– 13.修改stu表中编号为1的学生成绩,将成绩改为83分。

提示:where子句用于对记录进行筛选过滤,保留符合条件的记录,将不符合条件的记录剔除。

1
mysql复制代码UPDATE stu set score=83 where id=1;

– 14.删除stu表中所有的记录

– 删除记录语法: DELETE FROM 表名 [where子句]

全部删除

1
mysql复制代码delete from stu;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-guUh6Gu2-1617954028410)(C:\Users\Administrator.USER-20201125AL\AppData\Roaming\Typora\typora-user-images\image-20210408155819337.png)]

– 仅删除符合条件的

条件删除

1
mysql复制代码delete from stu where id=2;

3.查询表记录

3.1.基础查询

– 准备数据: 以下练习将使用db10库中的表及表记录,请先进入db10数据库!!

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
mysql复制代码-- -----------------------------------
-- 创建db10库、emp表并插入记录
-- -----------------------------------
-- 删除db10库(如果存在)
drop database if exists db10;
-- 重新创建db10库
create database db10 charset utf8;
-- 选择db10库
use db10;

-- 删除员工表(如果存在)
drop table if exists emp;
-- 创建员工表
create table emp(
id int primary key auto_increment, -- 员工编号
name varchar(50), -- 员工姓名
gender char(1), -- 员工性别
birthday date, -- 员工生日
dept varchar(50), -- 所属部门
job varchar(50), -- 所任职位
sal double, -- 薪资
bonus double -- 奖金
);

-- 往员工表中插入记录
INSERT INTO `emp` VALUES (null, '王海涛', '男', '1995-03-25', '培优部', '讲师', 1800, 400);
INSERT INTO `emp` VALUES (null, '齐雷', '男', '1994-04-06', '培优部', '讲师', 2500, 700);
INSERT INTO `emp` VALUES (null, '刘沛霞', '女', '1996-06-14', '培优部', '讲师', 1400, 400);
INSERT INTO `emp` VALUES (null, '陈子枢', '男', '1991-05-18', '培优部', '总监', 4500, 600);
INSERT INTO `emp` VALUES (null, '刘昱江', '男', '1993-11-18', '培优部', '讲师', 2600, 600);
INSERT INTO `emp` VALUES (null, '王克晶', '女', '1998-07-18', '就业部', '讲师', 3700, 700);
INSERT INTO `emp` VALUES (null, '苍老师', '男', '1995-08-18', '就业部', '总监', 4850, 500);
INSERT INTO `emp` VALUES (null, '范传奇', '男', '1999-09-18', '就业部', '讲师', 3200, 700);
INSERT INTO `emp` VALUES (null, '刘涛', '男', '1990-10-18', '就业部', '讲师', 2700, 500);
INSERT INTO `emp` VALUES (null, '韩少云', '男', '1980-12-18', NULL, 'CEO', 5000, null);
INSERT INTO `emp` VALUES (null, '董长春', '男', '1988-02-05', '培优部', '讲师', 3200, 300);
INSERT INTO `emp` VALUES (null, '张久军', '男', '1989-01-11', '培优部', '讲师', 4200, 500);
-- -----------------------------------
-- 创建db20库、dept表、emp表并插入记录
-- -----------------------------------
-- 删除db20库(如果存在)
drop database if exists db20;
-- 重新创建db20库
create database db20 charset utf8;
-- 选择db20库
use db20;

-- 删除部门表, 如果存在
drop table if exists dept;
-- 重新创建部门表, 要求id, name字段
create table dept(
id int primary key auto_increment, -- 部门编号
name varchar(20) -- 部门名称
);
-- 往部门表中插入记录
insert into dept values(null, '财务部');
insert into dept values(null, '人事部');
insert into dept values(null, '科技部');
insert into dept values(null, '销售部');

-- 删除员工表, 如果存在
drop table if exists emp;
-- 创建员工表, 要求id, name, dept_id
create table emp(
id int primary key auto_increment, -- 员工编号
name varchar(20), -- 员工姓名
dept_id int -- 部门编号
-- ,foreign key(dept_id) references dept(id)
);
insert into emp values(null, '张三', 1);
insert into emp values(null, '李四', 2);
insert into emp values(null, '老王', 3);
insert into emp values(null, '赵六', 4);
insert into emp values(null, '刘能', 4);

-- -----------------------------------
-- 创建db30库、dept表、emp表并插入记录
-- -----------------------------------

-- 删除db30库(如果存在)
drop database if exists db30;
-- 重新创建db30库
create database db30 charset utf8;
-- 选择db30库
use db30;

-- 删除部门表, 如果存在
drop table if exists dept;
-- 重新创建部门表, 要求id, name字段
create table dept(
id int primary key auto_increment, -- 部门编号
name varchar(20) -- 部门名称
);
-- 往部门表中插入记录
insert into dept values(null, '财务部');
insert into dept values(null, '人事部');
insert into dept values(null, '科技部');
insert into dept values(null, '销售部');

-- 删除员工表, 如果存在
drop table if exists emp;
-- 创建员工表(员工编号、员工姓名、所在部门编号)
create table emp(
id int primary key auto_increment, -- 员工编号
name varchar(20), -- 员工姓名
dept_id int -- 部门编号
);
-- 往员工表中插入记录
insert into emp values(null, '张三', 1);
insert into emp values(null, '李四', 2);
insert into emp values(null, '老王', 3);
insert into emp values(null, '赵六', 5);



-- -----------------------------------
-- 创建db40库、dept表、emp表并插入记录
-- -----------------------------------

-- 删除db40库(如果存在)
drop database if exists db40;
-- 重新创建db40库
create database db40 charset utf8;
-- 选择db40库
use db40;

-- 创建部门表
create table dept( -- 创建部门表
id int primary key, -- 部门编号
name varchar(50), -- 部门名称
loc varchar(50) -- 部门位置
);

-- 创建员工表
create table emp( -- 创建员工表
id int primary key, -- 员工编号
name varchar(50), -- 员工姓名
job varchar(50), -- 职位
topid int, -- 直属上级
hdate date, -- 受雇日期
sal int, -- 薪资
bonus int, -- 奖金
dept_id int, -- 所在部门编号
foreign key(dept_id) references dept(id)
);

-- 往部门表中插入记录
insert into dept values ('10', '培优部', '北京');
insert into dept values ('20', '就业部', '上海');
insert into dept values ('30', '大数据部', '广州');
insert into dept values ('40', '销售部', '深圳');

-- 往员工表中插入记录
insert into emp values ('1001', '王克晶', '办事员', '1007', '1990-12-17', '800', 500, '20');
insert into emp values ('1003', '齐雷', '分析员', '1011', '1991-02-20', '1900', '300', '10');
insert into emp values ('1005', '王海涛', '推销员', '1011', '1991-02-22', '2450', '600', '10');
insert into emp values ('1007', '刘苍松', '经理', '1017', '1991-04-02', '3675', 700, '20');
insert into emp values ('1009', '张慎政', '推销员', '1011', '1991-09-28', '1250', '1400', '10');
insert into emp values ('1011', '陈子枢', '经理', '1017', '1991-05-01', '3450', 400, '10');
insert into emp values ('1013', '张久军', '办事员', '1011', '1991-06-09', '1250', 800, '10');
insert into emp values ('1015', '程祖红', '分析员', '1007', '1997-04-19', '3000', 1000, '20');
insert into emp values ('1017', '韩少云', '董事长', null, '1991-11-17', '5000', null, null);
insert into emp values ('1019', '刘沛霞', '推销员', '1011', '1991-09-08', '1500', 500, '10');
insert into emp values ('1021', '范传奇', '办事员', '1007', '1997-05-23', '1100', 1000, '20');
insert into emp values ('1023', '赵栋', '经理', '1017', '1991-12-03', '950', null, '30');
insert into emp values ('1025', '朴乾', '分析员', '1023', '1991-12-03', '3000', 600, '30');
insert into emp values ('1027', '叶尚青', '办事员', '1023', '1992-01-23', '1300', 400, '30');
-- ------------------- 执行完毕 -----------------------

– 15.查询emp表中的所有员工,显示姓名,薪资,奖金

1
mysql复制代码select NAME,sal,bonus from emp;

– 16.查询emp表中的所有部门和职位

1
mysql复制代码select dept,job from emp;

去除重复的记录,如何提出重复记录,只保留一条

1
mysql复制代码select DISTINCT dept,job from emp;

3.2.WHERE子句查询

– 17.查询emp表中【薪资大于3000】的所有员工,显示员工姓名、薪资

1
mysql复制代码select name,sal from emp where sal>3000;

– 18.查询emp表中【总薪资(薪资+奖金)大于3500】的所有员工,显示员工姓名、总薪资

1
mysql复制代码select name,sal+bonus from emp where (sal+bonus)>3500;

– 19.查询emp表中【薪资在3000和4500之间】的员工,显示员工姓名和薪资

1
mysql复制代码select name,sal from emp where sal BETWEEN 3000 and 4500;

– 20.查询emp表中【薪资为 1400、1600、1800】的员工,显示员工姓名和薪资

1
mysql复制代码select name,sal from emp where sal=1400 or sal=1600 or sal=1800;

– 21.查询薪资不为1400、1600、1800的员工,显示员工姓名和薪资

1
mysql复制代码select name,sal from emp where sal!=1400 and sal!=1600 and sal!=1800;

– 22.(自己完成) 查询emp表中薪资大于4000和薪资小于2000的员工,显示员工姓名、薪资。

1
mysql复制代码select name,sal from emp where sal>4000 or sal<2000;

– 23.(自己完成) 查询emp表中薪资大于3000并且奖金小于600的员工,显示员工姓名、薪资、奖金。

1
mysql复制代码select name,sal,bonus from emp where sal>3000 AND bonus<600;

– 24.查询没有部门的员工(即部门列为null值)

1
mysql复制代码SELECT * from emp WHERE dept is NULL;

– 思考:如何查询有部门的员工(即部门列不为null值)

1
mysql复制代码select * from emp where dept is not NULL;

3.3.模糊查询

– 25.查询emp表中姓名中以”刘”字开头的员工,显示员工姓名。

1
mysql复制代码select name from emp where name like '刘%';

– 26.查询emp表中姓名中包含”涛”字的员工,显示员工姓名。

1
mysql复制代码select name from emp where name like '%涛%';

– 27.查询emp表中姓名以”刘”开头,并且姓名为两个字的员工,显示员工姓名。

1
mysql复制代码select name from emp where name like '刘_';

3.4.多行函数查询

– 28.统计emp表中薪资大于3000的员工个数

1
mysql复制代码select count(*) from emp where sal>3000;

– 29.求emp表中的最高薪资

1
mysql复制代码select MAX(sal) from emp;

– 30.统计emp表中所有员工的薪资总和(不包含奖金)

1
mysql复制代码SELECT sum((sal+bonus)) from emp;

– 31.统计emp表员工的平均薪资(不包含奖金)

1
mysql复制代码select avg(sal) from emp;

3.5.分组查询
语法:SELECT 列 | * FROM 表名 [WHERE子句] GROUP BY 列;
– 32.对emp表,按照部门对员工进行分组,查看分组后效果。

1
mysql复制代码SELECT dept from emp GROUP BY dept;

– 33.对emp表按照职位进行分组,并统计每个职位的人数,显示职位和对应人数

1
mysql复制代码select job,count(*) from emp GROUP BY job;

– 34.对emp表按照部门进行分组,求每个部门的最高薪资(不包含奖金),显示部门名称和最高薪资

1
mysql复制代码select dept,MAX(sal) from emp GROUP BY dept;

3.6.排序查询
语法:SELECT 列名 FROM 表名 ORDER BY 列名 [ASC|DESC]
ASC(默认)升序,即从低到高;DESC 降序,即从高到低。
– 35.对emp表中所有员工的薪资进行升序(从低到高)排序,显示员工姓名、薪资。

1
sql复制代码select name,sal from emp ORDER BY sal ASC;

– 36.对emp表中所有员工的奖金进行降序(从高到低)排序,显示员工姓名、奖金。

1
mysql复制代码select name,sal from emp ORDER BY sal DESC;

3.7.分页查询
在mysql中,通过limit进行分页查询,查询公式为:
limit (页码-1)*每页显示记录数, 每页显示记录数0
– 37.查询emp表中的所有记录,分页显示:每页显示3条记录,返回所有页的数据

1
mysql复制代码select * from emp LIMIT 0,3;

– 38.求emp表中薪资最高的前3名员工的信息,显示姓名和薪资

1
mysql复制代码select name,sal from emp ORDER BY sal DESC LIMIT 0,3;

3.8.其他函数
– 39.查询emp表中所有【在1993和1995年之间出生】的员工,显示姓名、出生日期。

1
mysql复制代码select name,birthday from emp where birthday BETWEEN '1993-1-1' and '1995-12-31';

– 40.查询emp表中本月过生日的所有员工

1
mysql复制代码select * from emp where WEEKOFYEAR(birthday)=WEEKOFYEAR(CURDATE());
1
mysql复制代码SELECT * from emp where MONTH(birthday)=MONTH(CURDATE());

– 41.查询emp表中员工的姓名和薪资(薪资格式为: xxx(元) )

1
mysql复制代码SELECT name,CONCAT(sal,"/元") from emp;

王振华 1800/元

– 补充练习:查询emp表中员工的姓名和薪资(薪资格式为: xxx/元 )


4.多表查询
4.1.连接查询

– 准备数据: 以下练习将使用db30库中的表及表记录,请先进入db30数据库!!!

– 42.查询部门和部门对应的员工信息


1
mysql复制代码SELECT dept.name,emp.name from dept,emp where dept.id=emp.dept_id;

4.2.连接查询


– 43.查询【所有部门】及部门对应的员工,如果某个部门下没有员工,员工显示为null

【左外连接查询】:可以将左边表中的所有记录都查询出来,右边表只显示和左边相对应的数据,如果左边表中某些记录在右边没有对应的数据,右边显示为null即可。

1
mysql复制代码select dept.name,emp.name from dept LEFT JOIN emp ON dept.id=emp.dept_id;

– 44.查询【所有员工】及员工所属部门,如果某个员工没有所属部门,部门显示为null

【右外连接查询】:可以将右边表中的所有记录都查询出来,左边表只显示和右边相对应的数据,如果右边表中某些记录在左边没有对应的数据,可以显示为null。

1
mysql复制代码select dept.name,emp.name from dept RIGHT  JOIN emp ON dept.id=emp.dept_id;

4.3.子查询练习

– 准备数据:以下练习将使用db40库中的表及表记录,请先进入db40数据库!!!

– 45.列出薪资比’王海涛’的薪资高的所有员工,显示姓名、薪资

1
2
3
mysql复制代码SELECT name,sal from emp where sal > (
select sal from emp where name='王海涛'
);

– 46.列出与’刘沛霞’从事相同职位的所有员工,显示姓名、职位。

1
2
3
mysql复制代码SELECT emp.name,emp.job from emp LEFT JOIN dept ON emp.dept_id=dept.id where emp.job=(
select job from emp where name='刘沛霞'
);

4.4.多表查询练习

– 47.列出在’培优部’任职的员工,假定不知道’培优部’的部门编号,显示部门名称,员工名称。

1
mysql复制代码SELECT dept.name,emp.name from emp LEFT JOIN dept ON emp.dept_id=dept.id where dept.name='培优部'

– 48.(自查询)列出所有员工及其直接上级,显示员工姓名、上级编号,上级姓名

1
mysql复制代码select e1.name,e2.id,e2.name from emp e1,emp e2 where e1.topid=e2.id;

– 49.列出最低薪资大于1500的各种职位,显示职位和该职位的最低薪资

1
2
mysql复制代码SELECT dept.name,emp.sal from emp LEFT JOIN dept ON emp.dept_id=dept.id  
GROUP BY emp.dept_id HAVING emp.sal>1500;

– 50.列出在每个部门就职的员工数量、平均工资。显示部门编号、员工数量,平均薪资。

1
mysql复制代码SELECT  dept.id,dept.name,count(*),avg(sal) from emp INNER JOIN dept on emp.dept_id=dept.id group by dept_id

– 51.列出受雇日期早于直接上级的所有员工,显示员工编号、员工姓名、部门名称。

1
mysql复制代码select e1.hdate,e1.name,e2.id,e2.name,dept.name,e2.hdate from emp e1,emp e2,dept where e1.topid=e2.id and e1.hdate < e2.hdate and e1.dept_id=dept.id;

本文转载自: 掘金

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

部署 bookinfo 示例

发表于 2021-11-16

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

首发于istio 实践手册

部署官方 Bookinfo 示例应用。

该示例部署了一个用于演示多种 Istio 特性的应用,该应用由四个单独的微服务构成。 这个应用模仿在线书店的一个分类,显示一本书的信息。 页面上会显示一本书的描述,书籍的细节(ISBN、页数等),以及关于这本书的一些评论。

Bookinfo 应用分为四个单独的微服务:

  • productpage:这个微服务会调用 details 和 reviews 两个微服务,用来生成页面。
  • details:这个微服务中包含了书籍的信息。
  • reviews:这个微服务中包含了书籍相关的评论。它还会调用 ratings 微服务。
  • ratings:这个微服务中包含了由书籍评价组成的评级信息。

reviews 微服务有 3 个版本:

  • v1 版本不会调用 ratings 服务。
  • v2 版本会调用 ratings 服务,并使用 1 到 5 个黑色星形图标来显示评分信息。
  • v3 版本会调用 ratings 服务,并使用 1 到 5 个红色星形图标来显示评分信息。

下图展示了这个应用的端到端架构。

Bookinfo部署图

图 5.3.1:Bookinfo部署图

1、部署服务

  1. 进入 Istio 安装目录。
  2. Istio 默认 自动注入 sidecar。请为 default 命名空间打上标签 istio-injection=enabled:
1
2
arduino复制代码$ kubectl label namespace default istio-injection=enabled
namespace/default labeled
  1. 使用 kubectl apply -f 命令部署应用:
1
bash复制代码kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yaml
  1. 确认所有的服务和 Pod 都已经正确的定义和启动:
1
2
3
arduino复制代码kubectl get service

kubectl get pod
  1. 要确认 Bookinfo 应用是否正在运行,请在某个 Pod 中用 curl 命令对应用发送请求,例如 ratings:
1
bash复制代码kubectl exec -it $(kubectl get pod -l app=ratings -o jsonpath='{.items[0].metadata.name}') -c ratings -- curl productpage:9080/productpage | grep -o "<title>.*</title>"

2、确定 Ingress 的 IP 和端口

现在 Bookinfo 中的所有服务都启动并运行中,您需要使应用程序可以从外部访问,例如使用浏览器。可以用 Istio Gateway 来实现这个目标。

  1. 为应用程序定义 Ingress 网关
1
bash复制代码kubectl apply -f samples/bookinfo/networking/bookinfo-gateway.yaml
  1. 确认网关创建完成
1
2
3
arduino复制代码$ kubectl get gateway
NAME AGE
bookinfo-gateway 32s
  1. 确认 Ingress 的 IP 和端口

执行如下命令,明确自身 Kubernetes 集群环境支持外部负载均衡:

1
sql复制代码kubectl get svc istio-ingressgateway -n istio-system

如果 EXTERNAL-IP 值存在,则说明环境正在使用外部负载均衡,可以用其为 ingress gateway 提供服务。如果 EXTERNAL-IP值为 <none>(或持续显示 <pending>),则说明环境没有提供外部负载均衡,无法使用 ingress gateway。可通过设置 EXTERNAL-IP 值,从外部访问。
4. 设置 EXTERNAL-IP

执行如下命令,添加 EXTERNAL-IP 值,即:设置外部访问,常设置为 master 的 IP。

1
perl复制代码kubectl edit  service istio-ingressgateway -n istio-system

EXTERNAL-IP设置

图 5.3.2:EXTERNAL-IP设置

确认 istio-ingressgateway 的 EXTERNAL-IP 值是否设置成功:

1
sql复制代码kubectl get  service istio-ingressgateway -n istio-system

3、确认可以从集群外部访问应用

用浏览器打开网址 http://<EXTERNAL-IP>/productpage,来浏览应用的 Web 页面。如果刷新几次应用的页面,就会看到 productpage 页面中会随机展示 reviews 服务的不同版本的效果(红色、黑色的星形或者没有显示)。reviews 服务出现这种情况是因为我们还没有使用 Istio 来控制版本的路由。

BookInfo应用页面

图 5.3.3:BookInfo应用页面

接下来的 Istio 学习中,可以使用此示例来验证 Istio 的流量路由、故障注入等功能。

4、卸载示例应用

当完成 Bookinfo 示例的实验后,如有需要可按照以下说明进行卸载和清理:

  1. 删除路由规则,并终止应用程序容器
1
bash复制代码samples/bookinfo/platform/kube/cleanup.sh
  1. 确认卸载
1
2
3
4
sql复制代码kubectl get virtualservices   #-- there should be no virtual services
kubectl get destinationrules #-- there should be no destination rules
kubectl get gateway #-- there should be no gateway
kubectl get pods #-- the Bookinfo pods should be deleted

\

本文转载自: 掘金

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

Go&Java算法之二叉搜索树和双向链表 二叉搜索树和双向链

发表于 2021-11-16

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

二叉搜索树和双向链表

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。

为了让您更好地理解问题,以下面的二叉搜索树为例:

image.png

我们希望将这个二叉搜索树转化为双向循环链表。链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。

下图展示了上面的二叉搜索树转化成的链表。“head” 表示指向链表中有最小元素的节点。

image.png

特别地,我们希望可以就地完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。还需要返回链表中的第一个节点的指针。

题解

方法一:递归法—中序遍历——java

本文解法基于性质:二叉搜索树的中序遍历为 递增序列 。
将 二叉搜索树 转换成一个 “排序的循环双向链表” ,其中包含三个要素:

排序链表: 节点应从小到大排序,因此应使用 中序遍历 “从小到大”访问树的节点。

双向链表: 在构建相邻节点的引用关系时,设前驱节点 pre 和当前节点 cur ,不仅应构建 pre.right = cur ,也应构建 cur.left = pre 。

循环链表: 设链表头节点 head 和尾节点 tail ,则应构建 head.left = tail 和 tail.right = head 。

根据以上分析,考虑使用中序遍历访问树的各节点 cur ;并在访问每个节点时构建 cur 和前驱节点 pre 的引用指向;中序遍历完成后,最后构建头节点和尾节点的引用指向即可。

算法流程:
dfs(cur): 递归法中序遍历;

终止条件: 当节点 cur 为空,代表越过叶节点,直接返回;
递归左子树,即 dfs(cur.left) ;

构建链表:

当 pre 为空时: 代表正在访问链表头节点,记为 head ;

当 pre 不为空时: 修改双向节点引用,即 pre.right = cur , cur.left = pre ;

保存 cur : 更新 pre = cur ,即节点 cur 是后继节点的 pre ;

递归右子树,即 dfs(cur.right) ;

treeToDoublyList(root):

特例处理: 若节点 root 为空,则直接返回;

初始化: 空节点 pre ;

转化为双向链表: 调用 dfs(root) ;

构建循环链表: 中序遍历完成后,head 指向头节点, pre 指向尾节点,因此修改 head 和 pre 的双向节点引用即可;

返回值: 返回链表的头节点 head 即可;

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
Java复制代码class Solution {
private Node treeToDoublyListPre;
private Node treeToDoublyListHead;

public Node treeToDoublyList(Node root) {
if (root == null) {
return null;
}

Node pre = null;
treeToDoublyListHelper(root);

treeToDoublyListHead.left = treeToDoublyListPre;
treeToDoublyListPre.right = treeToDoublyListHead;
return treeToDoublyListHead;
}

public void treeToDoublyListHelper(Node root) {
if (root == null) {
return;
}
treeToDoublyListHelper(root.left);
if (treeToDoublyListPre != null) {
treeToDoublyListPre.right = root;
} else {
treeToDoublyListHead = root;
}
root.left = treeToDoublyListPre;
treeToDoublyListPre = root;
treeToDoublyListHelper(root.right);
}
}

方法一:递归法—中序遍历——Go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
go复制代码type Node struct {
Val int
Left, Right *Node
}

func treeToDoublyList(root *Node) *Node {
if root == nil {
return nil
}
var dfs func(node *Node) (head, tail *Node)
dfs = func(node *Node) (head, tail *Node) {
if node == nil {
return
}
//递归,左子树
lHead, lTail := dfs(node.Left)
if lHead != nil {
//如果左子树不为空,头结点为左子树的头节点.并拼接当前节点到左子树的尾节点
head = lHead
lTail.Right = node
node.Left = lTail
} else {
//左子树为空,头结点为当前节点
head = node
}
//递归,右子树
rHead, rTail := dfs(node.Right)
if rTail != nil {
//如果右子树不为空,尾节点为右子树的尾节点.并拼接当前节点到右子树的头结点
tail = rTail
node.Right = rHead
rHead.Left = node
} else {
//右子树为空,尾节点为当前节点
tail = node
}
return
}
head, tail := dfs(root)
//最后将返回的头尾节点拼接成环
tail.Right = head
head.Left = tail
return head
}

本文转载自: 掘金

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

Spring Boot中实现文件上传和下载

发表于 2021-11-16

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

  1. 文件上传和下载请求

Web项目中,文件的上传和下载服务也是基于HTTP请求的,文件上传由于需要向服务接口提交数据,可以使用POST的请求方式,而文件的下载只是获取数据,因此可以使用GET请求方式。

基于Spring Boot框架的项目中实现文件的上传和下载还是比较简单的,只需要提供上传和下载的服务接口,在文件保存时将其放入指定的路径,获取文件时再从指定路径读取文件即可。

在本地运行项目时,需要指定本地的文件路径作为文件存储位置。

  1. 基于Spring Boot的Web项目

上传和下载都是在Web服务接口中完成的,因此在创建Spring Boot项目时只需要引入web依赖信息即可。

1
2
3
4
java复制代码<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>

2.1 上传和下载控制器

控制器controller层用来定义服务接口,文件上传和下载分别通过一个POST类型和GET类型的请求接口实现。

对于文件上传接口,接口使用MultipartFile对象作为参数,在使用postman等工具请求接口时选择文件后,服务接口接收文件参数数据。

而文件下载接口中,需要提供下载文件的名称,接口会根据文件名称在资源路径中寻找指定文件并通过结果返回。

1
2
3
4
5
6
7
8
9
10
11
java复制代码//文件上传接口
@RequestMapping(value = "/upload",method = RequestMethod.POST)
public String upload(MultipartFile file){
...
}
​
//文件下载接口
@RequestMapping(value = "/downloadFile/{fileName}", method = RequestMethod.GET)
public Resource getFile(@PathVariable("fileName") String fileName){
...
}

2.2 上传和下载逻辑实现

2.2.1 初始化文件存放路径

在本地实现文件上传和下载时,需要指定一个路径作为文件池,并在项目启动时将文件夹创建好。

  • Paths.get("file"),相对路径,指定一个当前项目下的file文件夹路径
  • Files.createDirectory(path),对指定的路径创建文件夹
  • @PostConstruct表示在当前类初始化后自动执行
1
2
3
4
5
6
7
8
9
10
11
java复制代码//指定文件夹路径并创建
Path path = Paths.get("file");
@PostConstruct
@Override
public void init() {
   try {
       Files.createDirectory(path);
  } catch (IOException e) {
       e.printStackTrace();
  }
}

2.2.2 上传文件到路径

创建好指定的文件存放路径文件夹后,上传逻辑只需要将接收到的文件数据赋值到指定路径后即可。

  • file.getInputStream(),接收文件参数的对应字节流
  • this.path.resolve(file.getOriginalFilename()),指定的path路径拼接接收文件的原始名称作为文件的路径信息
  • Files.copy(),复制文件的方法
1
2
3
4
5
6
7
8
9
java复制代码//上传逻辑,将接收的文件以字节流方式复制到指定路径
@Override
public void upload(MultipartFile file) {
   try {
 Files.copy(file.getInputStream(),this.path.resolve(file.getOriginalFilename()));
  } catch (IOException e) {
       e.printStackTrace();
  }
}

2.2.3 从路径中下载文件

文件的下载逻辑是根据指定的文件名称到文件资源文件夹中获取,如果存在则返回文件。

  • this.path.resolve(fileName),构建文件全路径
  • new UrlResource(file.toUri()),根据文件路径创建URL源
  • resource.exists() && resource.isReadable(),文件存在并且可读时返回
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码//下载文件逻辑
@Override
public Resource downloadFile(String fileName) {
   Path file = this.path.resolve(fileName);
   try {
       Resource resource = new UrlResource(file.toUri());
       if(resource.exists() && resource.isReadable()){
           return resource;
      }
  } catch (MalformedURLException e) {
       e.printStackTrace();
  }
}

2.3 请求上传和下载接口

业务逻辑实现后,可以使用postman来实现HTTP请求调用。

2.3.1 文件上传

首先请求文件上传服务接口,请求接口并选择本地文件作为body体中的参数,点击send发起请求,接口返回success说明文件上传成功。

image-20211116230119837

上传完成后,在项目根目录下会创建一个files文件夹,并将上传文件复制到此处。注意如果文件夹中已经存在上传的文件,上传时会抛出文件存在异常FileAlreadyExistsException。

image-20211116225401602

2.3.2 文件下载

调用接口下载指定文件时,需要在请求接口时拼接上文件名称作为参数。

image-20211116225839473

使用postman请求下载接口时,接口返回文件,postman会直接解析文件内容,如果无法正确解析则会显示乱码信息。如果在浏览器请求接口时,返回文件时浏览器会弹出下载文件的提示。

本文转载自: 掘金

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

【分布式事务系列】Spring Cloud集成Seata 实

发表于 2021-11-16

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

  • 在spring-cloud-ozx-account、spring-cloud-ozx-repo、spring-cloud-ozx-order三个服务中添加一个配置类SeataAutoConfig,主要实现如下:
  1. 配置数据源代理DataSourceProxy
  2. 初始化GlobalTransactionScanner,装载到Spring IOC容器中。

下面配置类是手动配置完成,GlobalTransactionScanner中的两个参数分别是applicationId(应用名称)和txServiceGroup(事务分组),但是seata-spring-boot-starter主动完成了这些功能,并且Seata自动完成了数据源的代理

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
java复制代码@Configuration
@EnableConfigurationProperties({SeataProperties.class})
public class SeataAutoConfig{
@Autowired
private DataSourceProperties dataSourceProperties;
@Autowired
private ApplicationContext applicationContext;
@Autowired
private SeataProperties seataProperties;

public SeataAutoConfig(SeataProperties seataProperties,ApplicationContext applicationContext){
this.applicationContext=applicationContext;
this.seataProperties=seataProperties;
}
@Bean
public DruidDataSource druidDataSource(){
DruidDataSource druidDataSource= new DruidDataSource();
druidDataSource.setUrl(dataSourceProperties.getUrl());
druidDataSource.setUsername(dataSourceProperties.getUsername());
druidDataSource.setPassword(dataSourceProperties.getPassword());
druidDataSource.setDriverClassName(dataSourceProperties.getDriverClassName());
return druidDataSource;
}

@Bean
public DataSourceProxy dataSourceProxy(DruidDataSource druidDataSource){
return new DataSourceProxy(druidDataSource);
}

@Bean
public DataSourceTransactionManager transactionManager(DataSourceProxy dataSourceProxy){
return new DataSourceTransactionManager(dataSourceProxy);
}

@Bean
public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception{
SqlSessionFactoryBean sqlSessionFactoryBean=new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:com/ozx/springcloud/seata/orderprovider/mappper/*Mapper.xml"));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagerTransactionFactory());
return sqlSessionFactoryBean.getObject();
}

@Bean
public GlobalTransactionScanner globalTransactionScanner(){
String applicationName=this.applicationContext.getEnvironment().getProperty("spring.application.name");
String txServiceGroup=this.seataProperties.getTxServiceGroup();
if(StringUtils.isEmpty(txServiceGroup)){
txServiceGroup= applicationName +"-seata-service-group";
this.seataProperties.setTxServiceGroup(txServiceGroup);

}
return new GlobalTransactionScanner(applicaitonName,txServiceGroup);
}
}

注意点:

2.1.1.RELEASE版本的内嵌的seata的版本是0.9.0,所以它无法和seata-spring-boot-starter兼容。

采用自定义配置类SeataAutoConfig,需要在@SpringBootApplication注解内exclude去掉spring-cloud-alibaba-seata内的GlobalTransactionAutoConfiguration,否则两个配置类会产生冲突。

`@SpringBootApplication(exclude=GlobalTransactionAutoConfiguration.class)

spring-cloud-ozx-rest项目配置类如下,它没有关联数据源,所以只需要装载GlobalTransactionScanner,它自动扫描包含GlobalTransactional注解的代码

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复制代码@EnableConfigurationProperties({SeataProperties.class})
@Configuration
public class SeataAutoConfig{
@Autowired
private ApplicationContext applicationContext;
@Autowired
private SeataProperties seataProperites;

public SeataAutoConfig(SeataProperties seataProperties,ApplicationContext applicationContext){
this.seataProperties=seataProperties;
this.applicationContext=applicationContext;
}

@Bean
@Bean
public GlobalTransactionScanner globalTransactionScanner(){
String applicationName=this.applicationContext.getEnvironment().getProperty("spring.application.name");
String txServiceGroup=this.seataProperties.getTxServiceGroup();
if(StringUtils.isEmpty(txServiceGroup)){
txServiceGroup= applicationName +"-seata-service-group";
this.seataProperties.setTxServiceGroup(txServiceGroup);

}
return new GlobalTransactionScanner(applicaitonName,txServiceGroup);
}
}

基于Spring Cloud框架集成Seata框架配置完成,由于Spring Cloud没有提供分布式事务处理规范,它不能像配置中心插拔式集成各种主流的解决方案,而Spring Cloud Alibaba Seata本质是基于Spring Boot 自动装配来整合的。`

本文转载自: 掘金

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

Redis主从搭建及主从复制原理详解 搭建主从复制 主从复制

发表于 2021-11-16

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

干货预警来了,本篇文章首先会给大家讲解主从复制如何搭建,然后详细介绍主从复制原理,耐心看完,你会收获满满。

搭建主从复制

首先修改主节点配置文件

1
2
3
4
5
6
7
8
9
10
11
12
arduino复制代码
daemonize yes //开启守护线程

logfile "6379.log"

dbfilename "dump-6379.rdb"

protected-mode no //关闭代表外部网络可以直接访问,开启需配置bind ip或者访问密码

port 6379

bind 0.0.0.0

然后复制两份配置文件,将文件名进行更改

image.png

然后对从节点配置文件进行更改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码
daemonize yes

logfile "6378.log"

dbfilename "dump-6378.rdb"

protected-mode no

port 6378

bind 0.0.0.0

slaveof 192.168.121.133 6379

启动三个节点

1
2
3
4
5
6
bash复制代码
./redis-server ../conf/redis-6379-master.conf

./redis-server ../conf/redis-6378.conf

./redis-server ../conf/redis-6377.conf

进入客户端

1
2
bash复制代码
./redis-cli -p 6379

image.png

可以看到有两个从节点

再来看看从节点情况

image.png

可以看到是从节点

主从搭建就成功了

主从复制原理

主从复制在Redis2.8之前和redis2.8之后有索改动,这里会分别讲解这两者的实现原理

旧版复制

redis复制功能分为同步(sync)和命令传播两个操作

  • 同步操作用于将从服务器数据库状态更新至和主服务器当前所处状态 保持相同
  • 命令传播用于主服务器的数据库状态被修改时,导致主从数据库状态不一致时,让主从服务器的数据库重新回到一致状态。

同步

当从服务器开始和主服务器建立主从关系时(从服务器发送slaveof命令),从服务器首先需要执行同步操作,从服务器对主服务器的同步操作是需要通过向主服务器发送SYNC命令来完成

  • 从服务器向主服务器发送SYNC命令
  • 收到SYNC命令的主服务器执行BGSAVE命令,在后台生成一个RDB文件,并使用一个缓存区记录从现在开始执行的所有写命令。
  • 主服务器BGSAVE命令完成时,主服务器会将生成的RDB文件发送给从服务器,从服务器会将RDB文件的数据读取存储到自己的库中。
  • 主服务器将记录在缓冲区里面的所有写命令发送给从服务器,从服务器执行这些写命令,将这些数据进行存储。

image.png

命令传播

假设一个主服务器和一个从服务器刚完成同步操作,他们数据库状态保持一致,如果这时客户端向主服务器发送写命令,此时主从就不一致了,主服务器会对从服务器执行命令传播操作,主服务器会将自己执行的写命令发送给从服务器执行,从而让主从再次回到一致状态。

旧版复制缺陷

从服务器对主服务器的复制分为两种情况:

  • 初次复制:从服务器以前没有复制过任何主服务器,或者从服务器当前要复制的主服务器和上一次复制的主服务器不同
  • 断线后复制:处于命令传播阶段的主从服务器因为各种原因中断了复制,但从服务器通过自动重连重新连接上了主服务器,并继续复制主服务器。

对于短线后复制,旧版复制功能效率很低,当重连时,从服务器会向主服务器发送SYNC命令,主服务器会将包含所有数据的RDB文件发送给从服务器,来实现最终的数据一致,这种全量的推送其实是可以优化的,可以只将断线期间没有同步的写命令进行同步,但旧版复制没有实现这个机制。

新版复制

为了解决旧版复制在处理断线复制情况的低效问题,Redis从2.8开始,使用PSYNC命令代替SYNC命令来执行复制时的同步操作。

PSYNC命令具有完整重同步和部分重同步两种模式:

  • 完整重同步用于处理初次复制情况,和旧版SYNC命令步骤基本一致
  • 部分重同步用于处理断线后重复制情况,主服务器可以只将断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可重新保持一致。

部分重复复制实现

部分重复制由一下三个部分构成:

  • 主服务器的复制偏移量和从服务器的复制偏移量
  • 主服务器的复制积压缓冲区
  • 服务器的运行ID

复制偏移量

执行复制的双方都分别维护一个复制偏移量,主服务器每次向从服务器传播N个字节的数据时,就会将自己的复制偏移量值加上N,从服务器每次收到主服务器传播来的N个字节数据时,就将自己的复制偏移量值加上N。

复制积压缓冲区

复制积压缓冲区是由主服务器维护的一个固定长度先进先出的队列,默认大小1MB。

当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务器,还会将写命令入队到复制积压缓冲区里。

image.png

当从服务器重新连上主服务器,从服务器会通过PSYNC命令将自己的复制偏移量发送给主服务器,主服务器会根据这个复制偏移量来决定对从服务器执行何种同步操作:

  • 如果偏移量之后的数据存在于复制积压缓冲区,那么主服务器将对从服务器执行部分重同步操作。
  • 如果偏移量之后的数据不存在于复制积压缓冲区,主服务器将对从服务器执行完整重同步操作。

服务器运行ID

每个Redis服务器,都会有自己的运行ID.

当从服务器对主服务器进行初次复制时,主服务器会将自己的运行ID传送给从服务器,而从服务器会将运行ID保存起来。

当从服务器短信并重新连上主服务器,从服务器会向当前连接的主服务器发送之前保存的运行ID。

如果从服务器发送的运行ID和当前主服务器运行ID相同,主服务器会先尝试执行部分重同步操作。

如果运行ID不相等,主服务器会对从服务器执行完整重同步操作。

PSYNC 命令实现

  • 如果从服务器以前没有复制过任何主服务器,从服务器开始新的复制将向主服务器发送PSYNC ? -1命令,主动请求主服务器进行完整重同步。
  • 如果从服务器已经复制过某个主服务器,从服务器开始新的复制会向主服务器发送PSYNC 命令:runid是上次复制的主服务器运行ID,offset是从服务器当前偏移量,主服务器会通过这两个参数来决定执行哪种同步操作。
  • 如果住服务器返回+FULLRESYNC 回复,那么表示是完整重同步操作,如果返回+CONTINUE回复,那么表示主服务器将与从服务器执行部分重同步操作。如果主服务器返回-ERR,表示主服务器版本低于2.8,它识别不了PSYNC命令,从服务器会发送一个SYNC命令,执行完整重同步。

image.png

复制整体流程

  • 从节点执行slaveof 命令
  • 从节点保存slaveof命令中主节点信息
  • 从节点内部的定时任务发现有主节点的信息,开始使用socket连接主节点
  • 连接建立成功,发送ping命令,得到ping命令响应,否则会进行重连
  • 主节点设置了权限,就需要进行权限验证
  • 验证通过,进行数据同步

image.png

心跳

命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令

image.png

replication_offset 是从服务器当前的复制偏移量。

发送REPLCONF ACK 命令有三个作用

  • 检测主从服务器网络连接状态

主从服务器会通过这个命令检查两者之间的网络连接是否正常,如果主服务器超过一秒没有收到从服务器发来的REPLCONF ACK命令,那么主服务器就知道主从服务器之间连接出了问题

  • 辅助实现min-salves配置选项

Redis可以通过min-slaves-to-write和min-salves-mas-lag两个选项防止主服务器不安全情况下执行写命令

image.png

经过设置之后,如果从服务器数量少于3个,或者三个从服务器延迟值都大于或等于10秒时,主服务器拒绝执行写命令。

  • 检测命令丢失

如果因为网络故障,主服务器传播给从服务器写命令半路消失,那么当从服务器向主服务器发送REPLCONF ACK命令时,主服务器发送跟从服务器偏移量有差异,就会在复制积压缓冲区找到从服务器缺少的数据,然后重新发给从服务器。

本文转载自: 掘金

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

线程创建的三种方式和线程池创建的四种方式 线程创建的三种方式

发表于 2021-11-16

线程创建的三种方式和线程池创建的四种方式

1.1线程创建的方式

java创建线程的三种方式:

  1. 继承Thread类创建线程类
  2. 实现Runnable接口
  3. 通过Callable和Future创建线程

1.2创建线程

1.2.1继承Thread类

(1)创建Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。

(2)创建了Thread子类的实例,即创建了线程对象。

(3)调用线程对象的start()方法来启动该线程。

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

public class Thread01 {
public static void main(String[] args) {
MyThread01 t1=new MyThread01();
MyThread01 t2=new MyThread01();
t1.start();
t2.start();
}
}

class MyThread01 extends Thread{

@Override
public void run() {
System.out.println("线程名:"+currentThread().getName());
}
}

image-20210717184818785

1.2.2实现Runnable接口

(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。

(2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。

(3)调用线程对象的start()方法来启动该线程。

线程的执行流程很简单,当执行代码start()时,就会执行对象中重写的void run()方法,该方法执行完成后,线程就消亡了。

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.thread;

public class Thread02 {

public static void main(String[] args) {

MyThread02 target=new MyThread02();

Thread t1=new Thread(target);
Thread t2=new Thread(target);

t1.start();
t2.start();
}
}

class MyThread02 implements Runnable{

@Override
public void run() {
System.out.println("线程名:"+Thread.currentThread().getName());
}
}

image-20210717184836777

1.2.3实现Callable接口

(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。

(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

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

import java.util.concurrent.*;

public class Thread03 {

public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
Callable callable=new MyThread03();
FutureTask task1=new FutureTask(callable);
FutureTask task2=new FutureTask(callable);
new Thread(task1).start();
new Thread(task2).start();
Thread.sleep(10);//等待线程执行结束

//task.get() 获取call()的返回值。若调用时call()方法未返回,则阻塞线程等待返回值
System.out.println(task1.get());
System.out.println(task2.get());

//get的传入参数为等待时间,超时抛出超时异常;传入参数为空时,则不设超时,一直等待
System.out.println(task1.get(10L, TimeUnit.MILLISECONDS));
System.out.println(task2.get(10L, TimeUnit.MILLISECONDS));
}
}

class MyThread03 implements Callable{

@Override
public Object call() throws Exception {
System.out.println("线程名:"+Thread.currentThread().getName());
return "实现callable:"+Thread.currentThread().getName();
}
}

image-20210717184854015

1.2.4三种方式的对比

采用继承Thread类方式:

(1)优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程。
(2)缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类。

采用实现Runnable接口方式:

(1)优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
(2)缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。

Runnable和Callable的区别:

(1)Callable规定的方法是call(),Runnable规定的方法是run().
(2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
(3)call方法可以抛出异常,run方法不可以,因为run方法本身没有抛出异常,所以自定义的线程类在重写run的时候也无法抛出异常
(4)运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

1.2.5start()和run()的区别

start()方法用来,开启线程,但是线程开启后并没有立即执行,他需要获取cpu的执行权才可以执行

run()方法是由jvm创建完本地操作系统级线程后回调的方法,不可以手动调用(否则就是普通方法)

2.创建线程池的四种方式

(1)newCachedThreadPool 创建一个可缓存的线程池,如果线程池长度超过处理需求,可灵活回收空闲线程,若无可回收,则新建线程。

(2)newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待

(3)newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行

(4)newSingleThreadExecutor 创建一个单线程化的线程池,它只会唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行

线程池的优点:

(1)重用存在的线程,减少对象创建、消亡的开销,性能佳。

(2)可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。

(3)提供定时执行、定期执行、单线程、并发数控制等功能。

使用newThread的弊端

  1. 每次new Thread新建对象性能差。
  2. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom(out of memory)。
  3. 缺乏更多功能,如定时执行、定期执行、线程中断。

2.1使用newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

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

import sun.rmi.runtime.Log;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolDemo01 {
public static void main(String[] args) {
ExecutorService cachedThreadPool= Executors.newCachedThreadPool();
for(int i=0;i<20;i++){
final int index=i;
try {
Thread.sleep(index*100);
} catch (InterruptedException e) {
e.printStackTrace();
}

cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println("此线程名:"+Thread.currentThread().getName());
}
});
}
}
}

线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

image-20210720143817502

2.2 newFixedThreadPool

需要指定线程池的大小,创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码package com.thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolDemo02 {

public static void main(String[] args) {

ExecutorService fixedThreadPool= Executors.newFixedThreadPool(3);
for(int i=0;i<10;i++){
final int index=i;

fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程名:"+Thread.currentThread().getName());
}
});
}
}
}

image-20210720143930106

因为线程池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字。定长线程池的大小最好根据系统资源进行设置。

2.3newScheduledThreadPool

创建一个定长线程池,支持定时及周期性任务执行。延迟执行示例代码如下:

延迟3秒执行

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

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ThreadPoolDemo03 {

public static void main(String[] args) {
ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(5);
scheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
System.out.println("线程名:"+Thread.currentThread().getName());
}
},3, TimeUnit.SECONDS);
}
}

定期执行:

表示延迟1秒后每3秒执行一次。

ScheduledExecutorService比Timer更安全,功能更强大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码package com.thread;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ThreadPoolDemo03_2 {
public static void main(String[] args) {
ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(5);
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("线程名:"+Thread.currentThread().getName());
}
},1,3, TimeUnit.SECONDS);
}
}

image-20210720145539685

2.4newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。示例代码如下:

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.thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolDemo04 {
public static void main(String[] args) {
ExecutorService singleThreadExecutor= Executors.newSingleThreadExecutor();
for(int i=0;i<10;i++){
singleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程名:"+Thread.currentThread().getName());
}
});
}
}
}

结果依次输出,相当于顺序执行各个任务。

image-20210720145438997

线程池的作用:

线程池作用就是限制系统中执行线程的数量。

根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。

为什么要用线程池:

1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。

比较重要的几个类:

ExecutorService: 真正的线程池接口。

ScheduledExecutorService: 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。

ThreadPoolExecutor: ExecutorService的默认实现。

ScheduledThreadPoolExecutor: 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。

本文转载自: 掘金

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

java并发工具 AQS为开发者提供了什么? 读写锁设计 C

发表于 2021-11-16

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

学完AQS,了解到有一些基于AQS的组件,本文在了解读写锁、CountDownLatch、CyclicBarrier、Semaphore的功能后,自己设想一下如何实现这几个功能。

AQS为开发者提供了什么?

一开始的我,看了AQS源码,什么CAS,什么CLH自旋转,什么双向队列,说的贼溜,但是为什么要做呢?我完全不知道。
其实AQS是为了我们开发者提供的,AQS组件让继承者重写以下接口:

  • tryAcquire: 返回true表示当前线程获取锁成功,false表示获取锁失败,进入同步队列
  • tryRelease: 返回true表示释放锁成功
  • tryAcquireShared:与tryAcquire区别是,允许多个线程返回true
  • tryReleaseShared:允许多个线程返回true
  • isHeldExclusively:当前线程是否占有锁

读写锁设计

读写锁功能是:

  1. 读允许并发
  2. 写只能独占

先从简单开始,读写锁,顾名思义,我们先搞两把锁出来:

  • 读锁
  • 写锁

读允许并发,写只能独占。怎么搞?一开始我的想法是两个标志标示正在读,标示正在写。读标志=true,不让其进行write,写标志=true,不让其进行读。但是问题是释放时无法知道何时将标志设置为false。那就只能退而其次,用两个变量进行计数:

  • 读数量
  • 写数量
    当然写的数量最大=1,也可以使用标志进行表示。
    下面就是简单设计一下核心流程了:

怎么允许读线程允许同时进入多个?

AQS tryAcquireShare,读计数通过循环+CAS进行+1操作,返回true表示读锁加成功

当有读线程正在读,此时多个线程加写锁怎么处理?

加写锁的线程进入同步队列,等当前的读计数=0,在唤醒需要加写锁的线程

当有读线程在读,后面有多个写线程加写锁,然后又有线程加读锁,后面的读锁如何处理?

让读线程进入同步队列,等待读线线程前面的写执行完后被唤醒。

从以上场景可以看出,读写锁加锁时需要阻塞的线程是进入同个队列的,所以要用同一个sync的对象进行加锁操作,在抽象出一把读锁,一把写锁提供给使用者。

CountDownLatch

CountDownLatch的效果是,单个线程等待多个线程。
其方法wait可以直接调用sync.accquire(),判断state=0?不为0直接进入阻塞队列。

其方法countDown()可以设计为sync.releaseShare(),当state减少到0时,唤醒阻塞队列。

CyclicBarrier

CyclicBarrier的效果是,每个线程执行wait,然后阻塞等待,知道wait线程的数量达到指定的阈值,执行某个特定的操作。

这个可以通过RetreentLock实现,做一个变量初始化=阈值,每次wait时 通过lock.lock进入,如果变量!=0,则condition.wait(),若达到阈值则执行对应的runnable方法。并唤醒其它线程。

本文转载自: 掘金

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

戏说 Spring MVC 框架

发表于 2021-11-16

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

Spring MVC 是 Spring 框架的一部分,和 Struts 一样都是属于 web 层框架,根据项目分层的思想,一个项目基本可以分为持久层,业务层和 web 层。而 Spring MVC 主要作用在 web 层,也叫表现层。

web 层核心的功能自然是处理用户的请求并返回数据,那我们就要介绍一下 Spring MVC 中是如何处理请求并返回数据的。首先来看一下它的大致流程。

图片

上图是框架中的大致方向,从用户发出请求开始,前端处理器接收请求,但是自己不干活,会把请求发送到不同的处理器,处理器中会调用业务方法进行处理,得到结果返回给前端控制器,控制器再将结果发送给 JSP 来渲染页面,最终将含有结果的 HTML 页面返回给浏览器。

仔细说一下,首先你要告诉我处理什么样的请求,不能来个阿猫阿狗我全接收了,这就需要我们在 web.xml 文件中配置一下什么样的请求才能进入框架。

请求是进来了,但是那是一串 URL 呀,框架需要分析这个请求呀,分析请求这个活前端控制器依旧不干,而是由处理器映射器 HandlerMapping 来处理。这个对象是 Spring MVC 提供好的,我们只需要配置一下就好,不要忘了 Spring 中强大的 IoC 功能。

处理器映射器的作用就在于,你给我一串 URL ,我来分析 URL,然后找到处理这个请求的包名+类名+方法名返回给控制器。找不到就报 404 呗。而这种 URL 和方法的映射关系,我们一般使用注解@RequestMapping 在方法的上面配置。

你以为控制器知道具体的方法以后就会执行嘛,想太多,这时候处理器适配器就出现了,这就是控制器下面的二腿子啊,它也不干活,那谁来处理啊,那肯定是我们牛逼哄哄的程序员了。

你自己配的方法的 URL ,你不写方法你还想飞不成啊!

图片

肯定是需要自己写处理 URL 的逻辑呀,有了包名+类名+方法名之后,前端控制器和处理器适配器说 “去看看这个方法在那里,处理一下”。处理器适配器一看,这不就是隔壁家的二狗子嘛,然后把二狗子叫来了,去处理一下请求吧。

好了,终于到找真正干活的人了,就是我们可爱的程序员啊,然后,不出意外的情况下,方法返回了一个结果给到前端控制器。这时候老大又发话了,“视图解析器你过来一下,给你个结果,去把结果渲染到页面上去。“

视图解析器拿到结果和页面(不一定是 JSP,但是常见的是 JSP)一顿操作,该填充的填充,就得到了一个 HTML 页面,然后这些完美的二进制通过网线就从远方的服务器传到了浏览器中供用户欣赏。

以上说的就是一个请求在 web 层中 Spring MVC 框架处理的逻辑,要注意呦,我们手写的处理器 Controller 中会调用 Service 层的东西,那就是业务层的范畴了。详细的还可以看看下面这张图。

图片

但事情哪有那么顺利呢,总会出现异常啊,什么 404,500,为了使我们的用户大人蒙在鼓里,我们还可以配置一个全局的异常处理类。只需要实现 HandlerExceptionResolver 即可,当然,我们也要在 Spring 中注册一下实现类。

这样在发生异常的时候,我们可以设置跳转到一个卡哇伊的页面,殊不知其实是系统出现问题了……

正常的和不正常的 Spring MVC 都能处理,但你以为这样就够了嘛,不,它还为我们提供了许多其它的功能。

还有什么上传图片啊,简直就是逆天啊,直接把图片当成一个参数传到后台了,当然,肯定需要一定的条件,比方说前台 form 必须有 enctype,后台接收图片的参数时必须使用 MultipartFile 接口且形参名一致,而且实现类还是固定的,我们需要注入一下 CommonsMultipartResolver 类。

还有一个拦截器的概念,这就是 AOP 编程的一个体现呀,一夫当关,万夫莫开。而我们需要做的就是在配置文件中使用标签配置一下拦截器拦截的路径和拦截器类。

1
xml复制代码<mvc:interceptor>

在拦截器类中我们就可以处理具体的逻辑了,比方说,验证用户是否登录呀。给用户分类呀,不用的用户跳转的页面不一致。

最后说一下 Spring MVC 和 Struts2 的区别。

1、Spring MVC 的入口是一个 Servlet 即前端控制器,而 Struts2 入口是一个 Filter 过滤器。

2、Spring MVC 是基于方法开发 (一个 url 对应一个方法),请求参数传递到方法的形参,可以设计为单例或多例(建议单例),Struts2 是基于类开发,传递参数是通过类的属性,只能设计为多例。

3、Struts2 采用值栈存储请求和响应的数据,通过 OGNL 存取数据, Spring MVC 通过参数解析器是将 request 请求内容解析,并给方法形参赋值,将数据和视图封装成 ModelAndView 对象,最后又将 ModelAndView 中的模型数据通过 request 域传输到页面。Jsp 视图解析器默认使用 jstl 表达式。

本文转载自: 掘金

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

1…319320321…956

开发者博客

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