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

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


  • 首页

  • 归档

  • 搜索

面试官问我MySQL索引

发表于 2021-09-03

面试官:我看你简历上写了MySQL,对MySQL InnoDB引擎的索引了解吗?

候选者:嗯啊,使用索引可以加快查询速度,其实上就是将无序的数据变成有序(有序就能加快检索速度)

候选者:在InnoDB引擎中,索引的底层数据结构是B+树

面试官:那为什么不使用红黑树或者B树呢?

候选者:MySQL的数据是存储在硬盘的,在查询时一般是不能「一次性」把全部数据加载到内存中

候选者:红黑树是「二叉查找树」的变种,一个Node节点只能存储一个Key和一个Value

候选者:B和B+树跟红黑树不一样,它们算是「多路搜索树」,相较于「二叉搜索树」而言,一个Node节点可以存储的信息会更多,「多路搜索树」的高度会比「二叉搜索树」更低。

候选者:了解了区别之后,其实就很容易发现,在数据不能一次加载至内存的场景下,数据需要被检索出来,选择B或B+树的理由就很充分了(一个Node节点存储信息更多(相较于二叉搜索树),树的高度更低,树的高度影响检索的速度)

候选者:B+树相对于B树而言,它又有两种特性。

候选者:一、B+树非叶子节点不存储数据,在相同的数据量下,B+树更加矮壮。(这个应该不用多解释了,数据都存储在叶子节点上,非叶子节点的存储能存储更多的索引,所以整棵树就更加矮壮)

候选者:二、B+树叶子节点之间组成一个链表,方便于遍历查询(遍历操作在MySQL中比较常见)

候选者:我稍微解释一下吧,你可以脑补下画面

候选者:我们在MySQL InnoDB引擎下,每创建一个索引,相当于生成了一颗B+树。

候选者:如果该索引是「聚集(聚簇)索引」,那当前B+树的叶子节点存储着「主键和当前行的数据」

候选者:如果该索引是「非聚簇索引」,那当前B+树的叶子节点存储着「主键和当前索引列值」

候选者:比如写了一句sql:select * from user where id >=10,那只要定位到id为10的记录,然后在叶子节点之间通过遍历链表(叶子节点组成的链表),即可找到往后的记录了。

候选者:由于B树是会在非叶子节点也存储数据,要遍历的时候可能就得跨层检索,相对麻烦些。

候选者:基于树的层级以及业务使用场景的特性,所以MySQL选择了B+树作为索引的底层数据结构。

候选者:对于哈希结构,其实InnoDB引擎是「自适应」哈希索引的(hash索引的创建由InnoDB存储引擎引擎自动优化创建,我们是干预不了)

面试官:嗯…那我了解了,顺便想问下,你知道什么叫做回表吗?

候选者:所谓的回表其实就是,当我们使用索引查询数据时,检索出来的数据可能包含其他列,但走的索引树叶子节点只能查到当前列值以及主键ID,所以需要根据主键ID再去查一遍数据,得到SQL 所需的列

候选者:举个例子,我这边建了给订单号ID建了个索引,但我的SQL 是:select orderId,orderName from orderdetail where orderId = 123

候选者:SQL都订单ID索引,但在订单ID的索引树的叶子节点只有orderId和Id,而我们还想检索出orderName,所以MySQL 会拿到ID再去查出orderName给我们返回,这种操作就叫回表

候选者:想要避免回表,也可以使用覆盖索引(能使用就使用,因为避免了回表操作)。

候选者:所谓的覆盖索引,实际上就是你想要查出的列刚好在叶子节点上都存在,比如我建了orderId和orderName联合索引,刚好我需要查询也是orderId和orderName,这些数据都存在索引树的叶子节点上,就不需要回表操作了。

面试官:既然你也提到了联合索引,我想问下你了解最左匹配原则吗?

候选者:嗯,说明这个概念,还是举例子比较容易说明

候选者:如有索引 (a,b,c,d),查询条件 a=1 and b=2 and c>3 and d=4,则会在每个节点依次命中a、b、c,无法命中d

候选者:先匹配最左边的,索引只能用于查找key是否存在(相等),遇到范围查询 (>、<、between、like左匹配)等就不能进一步匹配了,后续退化为线性查找

候选者:这就是最左匹配原则

面试官:嗯嗯,我还想问下你们主键是怎么生成的?

候选者:主键就自增的

面试官:那假设我不用MySQL自增的主键,你觉得会有什么问题呢?

候选者:首先主键得保证它的唯一性和空间尽可能短吧,这两块是需要考虑的。

候选者:另外,由于索引的特性(有序),如果生成像uuid类似的主键,那插入的的性能是比自增的要差的

候选者:因为生成的uuid,在插入时有可能需要移动磁盘块(比如,块内的空间在当前时刻已经存储满了,但新生成的uuid需要插入已满的块内,就需要移动块的数据)

面试官:OK…

本文总结:

  • 为什么B+树?数据无法一次load到内存,B+树是多路搜索树,只有叶子节点才存储数据,叶子节点之间链表进行关联。(树矮,易遍历)
  • 什么是回表?非聚簇索引在叶子节点只存储列值以及主键ID,有条件下尽可能用覆盖索引避免回表操作,提高查询速度
  • 什么是最左匹配原则?从最左边为起点开始连续匹配,遇到范围查询终止
  • 主键非自增会有什么问题?插入效率下降,存在移动块的数据问题

欢迎关注我的微信公众号【Java3y】来聊聊Java面试

【对线面试官-移动端】系列 一周两篇持续更新中!

【对线面试官-电脑端】系列 一周两篇持续更新中!

原创不易!!求三连!!

本文转载自: 掘金

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

公式解析引擎(文本计算)-java实现

发表于 2021-09-02

起因

有些时候要计算很复杂的场景,各种的加减乘除,甚至还有取模,取整,不止是要计算出结果,还的保留计算公式,所有就写了个公式计算引擎,真的不想用表达式引擎,觉得太重了!

特点

支持拓展

  • 加减乘除
  • 取模
  • 取整
  • 括号
  • 科学计

演示

计算字符串:”3 * (2 + 3) / 2” 运算结果:7.5

System.out.println(executeExpression(“3 * (2 + 3) / 2”));

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
java复制代码/**
* 公式解析引擎
* <p>
* Created by songzhaoying on 2020/8/11 14:44.
*
* @author songzhaoying@com.
* @date 2020/8/11 14:44.
*/
public class CalculatorNewUtil {

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

/**
* 表达式字符合法性校验正则模式
*/
private static final String EXPRESSION_PATTERN_REGEX = "[0-9\\.\\+\\-\\*\\/\\\(\\)_%Ee\\s]+";

/**
* 判断数字正则
*/
private static final String NUM_REGEX = "\d+(\\.)?\\d*((E|e|E\\+|e\\+|E-|e-)\\d+)?";

/**
* 操作符 需要加 空格
* ((|)|+|-|*|/|_|%)
*/
private static final String ADD_SPACE_REGEX = "((\\d+.?\\d*[Ee]([+\\-])?\\d+)|([()+\\-*/_%]))";

/**
* 空格
*/
private static final String SPACE = "\\s";

/**
* 计算中保留小数位数
*/
private static final int CAL_SCALE_SIZE = 10;

/**
* 计算中保留小数位数 处理原则
*/
private static final int CAL_SCALE_ROUND = BigDecimal.ROUND_HALF_DOWN;

/**
* 保留小数
*
* @param calExpressionStr
* @param scaleSize
* @param bigRoundType
* @return
* @throws Exception
*/
public static BigDecimal executeExpression(String calExpressionStr, int scaleSize, int bigRoundType) throws Exception {
return executeExpression(calExpressionStr).setScale(scaleSize, bigRoundType);
}

/**
* 计算值 10 位小数 0 去掉
*
* @param calExpressionStr
* @return
* @throws Exception
*/
public static BigDecimal executeExpression(String calExpressionStr) throws Exception {
return new BigDecimal(calculate(calExpressionStr).stripTrailingZeros().toPlainString());
}

/**
* 计算 中序 字符串
*
* @param calExpressionStr
* @return
*/
public static BigDecimal calculate(String calExpressionStr) throws Exception {
calExpressionStr = check2RepairExpression(calExpressionStr);

List<String> inorderExpressionList = getInorderExpressionList(calExpressionStr);

// 生成 逆波兰 表达式 list
List<String> suffixExpressionList = getSuffixExpressionList(inorderExpressionList);

if (logger.isDebugEnabled()) {
logger.info("中序表达式:{}", JsonUtil.writeValueAsString(inorderExpressionList));
logger.info("后缀表达式:{}", JsonUtil.writeValueAsString(suffixExpressionList));
}

return calculate(suffixExpressionList);
}

/**
* 生成中序表达式 list
*
* @param calExpressionStr
* @return
*/
private static List<String> getInorderExpressionList(String calExpressionStr) {
// 生成 中序表达式 list
return Arrays.stream(calExpressionStr.split(SPACE))
.filter(StringUtils::isNotBlank)
.collect(Collectors.toList());
}

/**
* 计算 逆波兰 / 后序表达式 List
*
* @param suffixExpressionList
* @return
*/
private static BigDecimal calculate(List<String> suffixExpressionList) throws Exception {
if (CollectionUtils.isEmpty(suffixExpressionList)) {
return BigDecimal.ZERO;
}

Stack<BigDecimal> numStack = new Stack<>();

for (String op2num : suffixExpressionList) {
if (StringUtils.isBlank(op2num)) {
continue;
}
// 使用正则表达式取出
if (isNumber(op2num)) {
// 数字
numStack.push(new BigDecimal(op2num));
} else {
// 运算符
OptEnum opEnum = OptEnum.getEnum(op2num);
if (opEnum.getCalFunction() == null) {
throw new RuntimeException("操作符不支持!");
}

BigDecimal bigDecimal2 = numStack.pop();
BigDecimal bigDecimal1 = numStack.pop();

BigDecimal resultBigDecimal = opEnum.getCalFunction().apply(bigDecimal1, bigDecimal2);

// 结果入栈
numStack.push(resultBigDecimal);
}
}

return numStack.pop();
}

/**
* 生成逆波兰 / 后续表达式 list
*
* @param inorderExpressionList
* @return
*/
private static List<String> getSuffixExpressionList(List<String> inorderExpressionList) {
Stack<String> opStack = new Stack<>();

// 不通过中间栈,在进行逆序处理,直接输出到list中,就是需要的逆波兰表达式
List<String> resList = new ArrayList<>(inorderExpressionList.size());
for (String op2num : inorderExpressionList) {
if (isNumber(op2num)) {
// 数字
resList.add(op2num);
} else if (Objects.equals(op2num, OptEnum.OP_LEFT_BRACKET.getOpt())) {
// (
opStack.push(op2num);
} else if (Objects.equals(op2num, OptEnum.OP_RIGHT_BRACKET.getOpt())) {
// )
while (!opStack.peek().equals(OptEnum.OP_LEFT_BRACKET.getOpt())) {
resList.add(opStack.pop());
}
// 去掉 (
opStack.pop();
} else {
// 操作符 优先级
while (!CollectionUtils.isEmpty(opStack)
&& OptEnum.getEnum(opStack.peek()).getOptPriority() >= OptEnum.getEnum(op2num).getOptPriority()) {
resList.add(opStack.pop());
}
// 将 操作符 最后加入
opStack.push(op2num);
}
}

// 处理剩余的操作符
while (!CollectionUtils.isEmpty(opStack)) {
resList.add(opStack.pop());
}

return resList;
}

/**
* 判断是否数字
*
* @param numberStr
* @return
*/
public static boolean isNumber(String numberStr) {
if (numberStr == null) {
return false;
}
return numberStr.matches(NUM_REGEX);
}

/**
* 运算符 枚举
*/
public enum OptEnum {
/**
* 运算符
*/
OP_LEFT_BRACKET("(", "左括号", 0, null),
OP_RIGHT_BRACKET(")", "右括号", 7, null),

OP_ADD("+", "加", 2, (p1, p2) -> p1.add(p2)),
OP_SUB("-", "减", 2, (p1, p2) -> p1.subtract(p2)),
OP_MULTIPLY("*", "乘", 3, (p1, p2) -> p1.multiply(p2)),
OP_DIVIDE("/", "除", 3, (p1, p2) -> p1.divide(p2, CAL_SCALE_SIZE, CAL_SCALE_ROUND)),

OP_FLOOR("_", "取整", 3, (p1, p2) -> p1.divide(p2, 0, BigDecimal.ROUND_DOWN)),
OP_MODE("%", "取模", 3, (p1, p2) -> p1.divideAndRemainder(p2)[1]),

;

/**
* 运算符
*/
private String opt;

/**
* 运算说明
*/
private String optName;

/**
* 运算级别
*/
private Integer optPriority;

/**
* 运算 函数
*/
private BiFunction<BigDecimal, BigDecimal, BigDecimal> calFunction;

/**
* @param opt
* @param optName
* @param optPriority
* @param biFunction
*/
OptEnum(String opt, String optName, Integer optPriority
, BiFunction<BigDecimal, BigDecimal, BigDecimal> biFunction) {
this.opt = opt;
this.optName = optName;
this.optPriority = optPriority;
this.calFunction = biFunction;
}

/**
* 取枚举
*
* @param opt
* @return
*/
protected static OptEnum getEnum(String opt) {
return Stream.of(OptEnum.values())
.filter(t -> Objects.equals(opt, t.getOpt()))
.findFirst().orElseThrow(() -> new RuntimeException("运算符不支持"));
}

public String getOpt() {
return opt;
}

public String getOptName() {
return optName;
}

public Integer getOptPriority() {
return optPriority;
}

public BiFunction<BigDecimal, BigDecimal, BigDecimal> getCalFunction() {
return calFunction;
}
}

/**
* 校验 & 整理字符串
* 科学计数处理
*
* @param calExpressionStr
* @return
*/
private static String check2RepairExpression(String calExpressionStr) {
// 非空校验
if (StringUtils.isBlank(calExpressionStr)) {
throw new IllegalArgumentException("表达式不能为空!");
}

// 表达式字符合法性校验
if (!calExpressionStr.matches(EXPRESSION_PATTERN_REGEX)) {
throw new IllegalArgumentException("表达式含有非法字符!" + calExpressionStr);
}

// 整理字符串
calExpressionStr = calExpressionStr.replaceAll(SPACE, "");

// (- 替换为 (0-
calExpressionStr = calExpressionStr.replace(OptEnum.OP_LEFT_BRACKET.getOpt() + OptEnum.OP_SUB.getOpt()
, OptEnum.OP_LEFT_BRACKET.getOpt() + BigDecimal.ZERO + OptEnum.OP_SUB.getOpt());

// - 开始 前缀 0-
if (calExpressionStr.startsWith(OptEnum.OP_SUB.getOpt())) {
calExpressionStr = BigDecimal.ZERO + calExpressionStr;
}

calExpressionStr = calExpressionStr.replaceAll(ADD_SPACE_REGEX, " $1 ").trim();

if (logger.isDebugEnabled()) {
logger.info("整理后的运算串:{}", calExpressionStr);
}

return calExpressionStr;
}


public static void main(String[] args) throws Exception {
String str = "3.E+3 \n* 2";
boolean number = isNumber("3.E+3");

BigDecimal bigDecimal = executeExpression(str);

System.out.println(bigDecimal);

System.out.println(executeExpression("-1 + (-1) + 2"));
System.out.println(executeExpression(" 2 + 13/3 * 3 - 4 *(2 + 5 -2*4/2+9) + 3 + (2*1)-3"));
System.out.println(executeExpression("330000000000*121000000000000000000000000000"
, 4, BigDecimal.ROUND_HALF_UP));

System.out.println(executeExpression("9.2 *(20-1)-1+199 / 13"
, 4, BigDecimal.ROUND_HALF_UP));
System.out.println(executeExpression("9.2 *(20-1)-1+199 / 13"));
System.out.println(executeExpression("11"));
}
}

本文转载自: 掘金

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

MySQL修炼、游标 前言 实例

发表于 2021-09-02

前言

要明白什么是游标,首先要了解存储过程,存储过程是事先经过编译并且存储在数据库中的一段SQL语句,他可以接受参数,也可以在其中使用IF语句、设置变量、循环等,比如下面语句用于创建一个存储过程。

1
2
sql复制代码delimiter $$
create procedure select_all() begin select * from user; end;$$

调用存储过程。

1
sql复制代码mysql>  call select_all;$$

存储过程可以减少数据库和应用服务器之间的传输,对提供数据库处理效率还是有好处的,而游标(Cursor)有的地方又叫光标,可以在存储过程中,对结果集进行循环处理,但是目前,MySQL只允许我们从SELECT语句从头到尾获取结果集中的每一行,无法从最后一行获取到第一行,也无法直接跳转到结果集中的指定行。

使用游标有以下几个步骤。

  1. 游标定义
1
sql复制代码DECLARE cursor_name CURSOR FOR select_statement
  1. 打开游标
1
sql复制代码OPEN cursor_name;
  1. 获取游标中的数据
1
sql复制代码FETCH cursor_name INTO var_name [, var_name]...

4.关闭光标

1
sql复制代码CLOSE cursor_name;
  1. 释放光标
1
sql复制代码DEALLOCATE cursor_name;

实例

创建表

1
2
3
4
5
6
7
8
9
10
sql复制代码CREATE TABLE cursor_table
(id INT ,name VARCHAR(10),age INT
)ENGINE=innoDB DEFAULT CHARSET=utf8;
insert into cursor_table values(1, '张三', 500);
insert into cursor_table values(2, '李四', 200);
insert into cursor_table values(3, '王五', 100);
insert into cursor_table values(4, '老六', 20);


create table cursor_table_user(name varchar(10));

下面我们通过游标,遍历cursor_table表,把年龄大于30的人名存放到cursor_table_user

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
sql复制代码drop procedure getTotal;
delete from cursor_table_user ;

CREATE PROCEDURE getTotal()
BEGIN
DECLARE total INT;
DECLARE sid INT;
DECLARE sname VARCHAR(10);
DECLARE sage INT;
DECLARE done INT DEFAULT false;
DECLARE cur CURSOR FOR SELECT id,name,age from cursor_table where age>30;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = true;
SET total = 0;
OPEN cur;
FETCH cur INTO sid, sname, sage;
WHILE(NOT done)
DO
insert cursor_table_user values(sname);
SET total = total + 1;
FETCH cur INTO sid, sname, sage;

END WHILE;
CLOSE cur;
SELECT total;
END
1
2
3
4
5
6
7
8
9
10
11
sql复制代码call getTotal();

mysql> select * from cursor_table_user;
+--------+
| name |
+--------+
| 张三 |
| 李四 |
| 王五 |
+--------+
3 rows in set (0.00 sec)

这段程序有一行非常重要,DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = true; ,他表示如果游标或SELECT语句没有数据的时候,将done变量的值设置 为 true,用来退出循环。

下面就是通过WHILE来依次遍历。

本文转载自: 掘金

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

ElasticSearch7 安装及入门

发表于 2021-09-02

一、ElasticSearch介绍

Elasticsearch 是一个基于JSON的分布式、高扩展、高实时、RESTful 风格的搜索和数据分析引擎,能够解决不断涌现出的各种用例。 作为 Elastic Stack 的核心,它集中存储您的数据,帮助您发现意料之中以及意料之外的情况。

Elasticsearch是与名为Logstash的数据收集和日志解析引擎以及名为Kibana的分析和可视化平台一起开发的。这三个产品被设计成一个集成解决方案,称为“Elastic Stack”(以前称为“ELK stack”)。

Elasticsearch可以用于搜索各种文档。它提供可扩展的搜索,具有接近实时的搜索,并支持多租户。Elasticsearch是分布式的,这意味着索引可以被分成分片,每个分片可以有0个或多个副本。每个节点托管一个或多个分片,并充当协调器将操作委托给正确的分片。再平衡和路由是自动完成的。“相关数据通常存储在同一个索引中,该索引由一个或多个主分片和零个或多个复制分片组成。一旦创建了索引,就不能更改主分片的数量。

Elasticsearch使用Lucene,并通过JSON和Java API提供其所有特性。它支持facetting和percolating,如果新文档与注册查询匹配,这对于通知非常有用。另一个特性称为“网关”,处理索引的长期持久性;例如,在服务器崩溃的情况下,可以从网关恢复索引。Elasticsearch支持实时GET请求,适合作为NoSQL数据存储,但缺少分布式事务。

二、ElasticSearch 7环境搭建

ps: 全文基于 ElasticSearch 7.12.1 ,需要java 8及以上环境,安装ElasticSearch相关软件/插件时,注意版本要一致。

1、安装ElasticSearch

官网下载ElasticSearch

直接解压,运行bin目录下elasticsearch.bat

如果一切正常,浏览器访问127.0.0.1:9200,就会看到一下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
json复制代码{
"name" : "DESKTOP-V4GSUJH",
"cluster_name" : "elasticsearch",
"cluster_uuid" : "4tnI-jAtTXqXbMDJ8CVRjQ",
"version" : {
"number" : "7.12.1",
"build_flavor" : "default",
"build_type" : "zip",
"build_hash" : "3186837139b9c6b6d23c3200870651f10d3343b7",
"build_date" : "2021-04-20T20:56:39.040728659Z",
"build_snapshot" : false,
"lucene_version" : "8.8.0",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}

elasticsearch文件目录介绍:

1
2
3
4
5
6
7
8
9
java复制代码elasticsearch
bin :可执行文件。我们用来启动elasticsearch的脚本就在这里面
config :elastic-search的全局设置和你的具体设置,如果你需要更改JVM,数据路径,日志路径等,就需 要改这里。同时端口设置等也都在这里。
data :你的索引数据,即你存放具体用来搜索数据的地方
jdk :自带的JDK,不重要可忽略
lib :存放源码jar包
logs :存放一些日志文件
modules :自带的一些模块,不可删除。比如x-pack模块等(对我们学习不重要,可忽略)
plugins :放置插件的地方,比如第三方的分词器等

2、安装ElasticSearch-head插件

安装ElasticSearch-head插件

解压head插件,进入ElasticSearch-head目录,在cmd下运行下面命令启动:

1
2
bash复制代码npm install  // 安装插件
npm run start // 启动插件

启动成功后访问 http://localhost:9100

解决跨域问题

修改 Elasticsearch 配置文件 config/elasticsearch.yml,在文件末尾加上:

http.cors.enabled: true
http.cors.allow-origin: “*“

3、安装Kibana

下载Kibana

Kibana 是为 Elasticsearch设计的开源分析和可视化平台。你可以使用 Kibana 来搜索,查看存储在 Elasticsearch 索引中的数据并与之交互。你可以很容易实现高级的数据分析和可视化,以图表的形式展现出来。

① 解压Kibana

② 进入kibana/bin下,运行kibana.bat

国际化进入config/kibana.yml,在文末加上i18n.locale: "zh-CN",修改kibana界面为中文。

4、安装 ik 中文分词器

下载elasticsearch-analysis-ik

直接解压到Elasticsearch/elasticsearch-7.12.1/plugins/下就行(先在plugins下建个ik文件夹)。

可以在Kibana控制台里测试:

  • ik_smart:最少切分
  • ik_max_word:最细粒度切分
1
2
3
4
5
6
7
8
9
10
11
json复制代码GET _analyze
{
"analyzer": "ik_smart",
"text": ["我是好学生"]
}

GET _analyze
{
"analyzer": "ik_max_word",
"text": ["我是好学生"]
}

可以去plugins中写自己的字典my.dic ,多词典用分号分隔。

三、基本操作

Rest风格说明

一种软件架构风格,而不是标准,只是提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁。更有层次,更易于实现缓存等机制。
基本Rest命令说明:(从es7开始弃用类型types,所以在url中可以不用再写类型名称,或者写成_doc)

method url地址 描述
PUT localhost:9200/索引 名称/类型名称/文档id
POST localhost:9200/索引 名称/类型名称
POST localhost:9200/索引 名称/类型名称/文档id/_update
DELETE localhost:9200/索引 名称/类型名称/文档id
GET localhost:9200/索引 名称/类型名称/文档id
POST localhost:9200/索引 名称/类型名称_search

基本概念

Node 与 Cluster

Elastic 本质上是一个分布式数据库,允许多台服务器协同工作,每台服务器可以运行多个 Elastic 实例。

单个 Elastic 实例称为一个节点(node)。一组节点构成一个集群(cluster)。

Index

Elastic 会索引所有字段,经过处理后写入一个反向索引(Inverted Index)。查找数据的时候,直接查找该索引。

所以,Elastic 数据管理的顶层单位就叫做 Index(索引)。它是单个数据库的同义词。每个 Index (即数据库)的名字必须是小写。

下面的命令可以查看当前节点的所有 Index。

1
bash复制代码GET _cat/indices?v
Document

Index 里面单条的记录称为 Document(文档)。许多条 Document 构成了一个 Index。

Document 使用 JSON 格式表示,下面是一个例子。

1
2
3
4
5
json复制代码{
"user": "张三",
"title": "工程师",
"desc": "数据库管理"
}

同一个 Index 里面的 Document,不要求有相同的结构(scheme),但是最好保持相同,这样有利于提高搜索效率。

Type

Document 可以分组,比如weather这个 Index 里面,可以按城市分组(北京和上海),也可以按气候分组(晴天和雨天)。这种分组就叫做 Type,它是虚拟的逻辑分组,用来过滤 Document。

不同的 Type 应该有相似的结构(schema),举例来说,id字段不能在这个组是字符串,在另一个组是数值。这是与关系型数据库的表的一个区别。性质完全不同的数据(比如products和logs)应该存成两个 Index,而不是一个 Index 里面的两个 Type(虽然可以做到)。

下面的命令可以列出每个 Index 所包含的 Type。

1
bash复制代码GET _mapping?pretty=true

根据规划,Elastic 6.x 版只允许每个 Index 包含一个 Type,7.x 版将会彻底移除 Type。

关于索引的操作

创建索引

1
2
3
4
5
json复制代码PUT /test1/_doc/1
{
"name": "老王",
"age": 18
}

_doc为默认类型,自动推断类型。

数据类型:

  • 字符串:text、keyword
  • 数值类型:long、integer、short、byte、double、float、half float、scaled float
  • 日期类型:date
  • 布尔类型:boolean
  • 二进制:binary
  • 等等

创建规则

1
2
3
4
5
6
7
8
9
10
11
12
13
json复制代码PUT /test2
{
"mappings": {
"properties": {
"name": {
"type": "text"
},
"age": {
"type": "long"
}
}
}
}

获取索引情况,通过 _cat 命令

1
json复制代码GET _cat/plugins

修改索引

1、方法1:

1
2
3
4
5
json复制代码PUT /test1/_doc/1
{
"name": "老王",
"age": 19
}

直接更改值,但是漏掉某个值,该值会为空。

2、方法2:(不加_update,其他属性会置空 )

1
2
3
4
5
6
json复制代码POST /test1/_doc/1/_update
{
"doc":{
"age": 20
}
}

关于文档的操作

返回所有记录

使用 GET 方法,直接请求/Index/Type/_search,就会返回所有记录。

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
bash复制代码GET accounts/person/_search
{
"took":2,
"timed_out":false,
"_shards":{"total":5,"successful":5,"failed":0},
"hits":{
"total":2,
"max_score":1.0,
"hits":[
{
"_index":"accounts",
"_type":"person",
"_id":"AV3qGfrC6jMbsbXb6k1p",
"_score":1.0,
"_source": {
"user": "李四",
"title": "工程师",
"desc": "系统管理"
}
},
{
"_index":"accounts",
"_type":"person",
"_id":"1",
"_score":1.0,
"_source": {
"user" : "张三",
"title" : "工程师",
"desc" : "数据库管理,软件开发"
}
}
]
}
}

上面代码中,返回结果的 took字段表示该操作的耗时(单位为毫秒),timed_out字段表示是否超时,hits字段表示命中的记录,里面子字段的含义如下。

  • total:返回记录数,本例是2条。
  • max_score:最高的匹配程度,本例是1.0。
  • hits:返回的记录组成的数组。

返回的记录中,每条记录都有一个_score字段,表示匹配的程序,默认是按照这个字段降序排列。

全文搜索

更多查询语法,见官网

Elastic 的查询非常特别,使用自己的查询语法,要求 GET 请求带有数据体。

1
2
3
4
bash复制代码GET accounts/person/_search
{
"query" : { "match" : { "desc" : "软件" }}
}

上面代码使用 Match 查询,指定的匹配条件是desc字段里面包含”软件”这个词。返回结果如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
json复制代码{
"took":3,
"timed_out":false,
"_shards":{"total":5,"successful":5,"failed":0},
"hits":{
"total":1,
"max_score":0.28582606,
"hits":[
{
"_index":"accounts",
"_type":"person",
"_id":"1",
"_score":0.28582606,
"_source": {
"user" : "张三",
"title" : "工程师",
"desc" : "数据库管理,软件开发"
}
}
]
}
}

Elastic 默认一次返回10条结果,可以通过size字段改变这个设置。

1
2
3
4
5
bash复制代码GET accounts/person/_search
{
"query" : { "match" : { "desc" : "管理" }},
"size": 1
}

上面代码指定,每次只返回一条结果。

还可以通过from字段,指定位移。

1
2
3
4
5
6
bash复制代码GET accounts/person/_search
{
"query" : { "match" : { "desc" : "管理" }},
"from": 1,
"size": 1
}

上面代码指定,从位置1开始(默认是从位置0开始),只返回一条结果。

逻辑运算

如果有多个搜索关键字, Elastic 认为它们是or关系。

1
2
3
4
bash复制代码GET accounts/person/_search
{
"query" : { "match" : { "desc" : "软件 系统" }}
}

上面代码搜索的是软件 or 系统。

如果要执行多个关键词的and搜索,必须使用布尔查询。

1
2
3
4
5
6
7
8
9
10
11
bash复制代码GET accounts/person/_search
{
"query": {
"bool": {
"must": [
{ "match": { "desc": "软件" } },
{ "match": { "desc": "系统" } }
]
}
}
}

更多查询语法,见官网

Documentation

本文转载自: 掘金

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

ThinkPHP60 实现 图片审核+文本内容审核(敏感词

发表于 2021-09-02

应用场景

  • 用户评论过滤:对网站用户的评论信息进行检测,审核出涉及违规内容,保证良好的用户体验
  • 注册信息筛查:对用户的注册信息进行筛查,避免黑产通过用户名实现违规信息的推广
  • 文章内容审核:对UGC文章内容进行多个维度的审核,避免因内容违规导致的APP下架等损失

开通应用

1.内容审核控制台:
console.bce.baidu.com/ai/?fromai=…

image.png

2.领取免费资源

image.png

image.png

3.创建应用

image.png

image.png

  1. 查看应用信息

image.png

这里面有我们需要的配置信息

实战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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
php复制代码<?php
/**
* Author: 柯作
* Email: kezuo@foxmail.com
* Date: 2021/9/2
* Time: 11:15
*/

namespace app\api\controller;


use app\Request;

class Audit
{

/**
* 内容审核
*/
public function contentAudit(Request $request)
{
$content = $request->post('content');
$token = $this->getAccessToken('API Key', 'Secret Key');

$url = 'https://aip.baidubce.com/rest/2.0/solution/v1/text_censor/v2/user_defined?access_token=' . $token;
$bodys = array(
'text' => $content
);
$res = $this->curlPost($url, $bodys);

//结果转成数组
$res = json_decode($res, true);
//根据自己的业务逻辑进行处理
print_r($res);die;
}

/**
* 图片审核
*/
public function imageAudit()
{

$token = $this->getAccessToken('API Key', 'Secret Key');
$url = 'https://aip.baidubce.com/rest/2.0/solution/v1/img_censor/v2/user_defined?access_token=' . $token;
$img = file_get_contents('C:\Users\Pictures\Saved Pictures\1.png');
$img = base64_encode($img);
$bodys = array(
'image' => $img
);
$res = $this->curlPost($url, $bodys);
//结果转成数组
$res = json_encode($res, true);
//根据自己的业务逻辑进行处理
print_r($res);
}

/**
* CURL的Post请求方法
* @param string $url
* @param string $param
* @return bool|string
*/
function curlPost($url = '', $param = '')
{
if (empty($url) || empty($param)) {
return false;
}

$postUrl = $url;
$curlPost = $param;
// 初始化curl
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $postUrl);
curl_setopt($curl, CURLOPT_HEADER, 0);
// 要求结果为字符串且输出到屏幕上
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
// post提交方式
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, $curlPost);
// 运行curl
$data = curl_exec($curl);
curl_close($curl);

return $data;
}

/**
* 获取百度开放平台的票据
* 参考链接:https://ai.baidu.com/ai-doc/REFERENCE/Ck3dwjhhu
*/
public function getAccessToken($ApiKey = '', $SecretKey = '', $grantType = 'client_credentials')
{

$url = 'https://aip.baidubce.com/oauth/2.0/token';
$post_data['grant_type'] = $grantType;
$post_data['client_id'] = $ApiKey;
$post_data['client_secret'] = $SecretKey;
$o = "";
foreach ($post_data as $k => $v) {
$o .= "$k=" . urlencode($v) . "&";
}
$post_data = substr($o, 0, -1);

$res = $this->curlPost($url, $post_data);
//进行把返回结果转成数组
$res = json_decode($res, true);
if (isset($res['error'])) {
exit('API Key或者Secret Key不正确');
}
$accessToken = $res['access_token'];
return $accessToken;
}
}

配置路由,进行调用就行

image.png

文本内容审核测试

1.输入文本为‘你好’

image.png

结果为合规

2.输入文本内容为‘敏感词’

image.png

结果则为不合规

本文转载自: 掘金

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

Gitee + Jenkins 实现自动触发构建 简介 插件

发表于 2021-09-02

Gitee Jenkins Plugin 是Gitee基于 GitLab Plugin 开发的 Jenkins 插件。用于配置 Jenkins 触发器,接受Gitee平台发送的 WebHook 触发 Jenkins 进行自动化持续集成或持续部署,并可将构建状态反馈回Gitee平台。

简介

目前支持特性:

  • 推送代码到Gitee时,由配置的 WebHook 触发 Jenkins 任务构建。
  • 评论提交记录触发提交记录对应版本 Jenkins 任务构建
  • 提交 Pull Request 到Gitee项目时,由配置的 WebHook 触发 Jenkins 任务构建,支持PR动作:新建,更新,接受,关闭,审查通过,测试通过。
  • 支持 [ci-skip] 指令过滤 或者 [ci-build] 指令触发构建。
  • 过滤已经构建的 Commit 版本,若是分支 Push,则相同分支Push才过滤,若是 PR,则是同一个PR才过滤。
  • 按分支名过滤触发器。
  • 正则表达式过滤可触发的分支。
  • 设置 WebHook 验证密码。
  • 构建后操作可配置 PR 触发的构建结果评论到Gitee对应的PR中。
  • 构建后操作可配置 PR 触发的构建成功后可自动合并对应PR。
  • 对于 PR 相关的所有事件,若 PR 代码冲突不可自动合并,则不触发构建;且若配置了评论到PR的功能,则评论到 PR 提示冲突。
  • PR 评论可通过 WebHook 触发构建(可用于 PR 触发构建失败是便于从Gitee平台评论重新触发构建)
  • 支持配置 PR 不要求必须测试时过滤触发构建。(可用于不需测试则不构建部署测试环境)
  • 支持相同 PR 触发构建时,取消进行中的未完成构建,进行当前构建(相同 PR 构建不排队,多个不同 PR 构建仍需排队)

计划中特性

  1. PR 审查并测试通过触发构建(可用户触发部署,且可配合自动合并 PR 的特性完善工作流。)
  2. 勾选触发方式自动添加WebHook至Gitee。

插件安装

  1. 在线安装
* 前往 Manage Jenkins -> Manage Plugins -> Available
* 右侧 Filter 输入: Gitee
* 下方可选列表中勾选 Gitee(如列表中不存在 Gitee,则点击 Check now 更新插件列表)
* 点击 Download now and install after restart

112748_b81a1ee3_58426.png

  1. 手动安装
* 从 [release](https://gitee.com/oschina/Gitee-Jenkins-Plugin/releases) 列表中进入最新发行版,下载对应的 XXX.hpi 文件
* 前往 Manage Jenkins -> Manage Plugins -> Advanced
* Upload Plugin File 中选择刚才下载的 XXX.hpi 点击 Upload
* 后续页面中勾选 Restart Jenkins when installation is complete and no jobs are running

113303_2a1d0a03_58426.png

插件配置

添加Gitee链接配置

  1. 前往 Jenkins -> Manage Jenkins -> Configure System -> Gitee Configuration -> Gitee connections
  2. 在 Connection name 中输入 Gitee 或者你想要的名字
  3. Gitee host URL 中输入Gitee完整 URL地址: https://gitee.com (Gitee私有化客户输入部署的域名)
  4. Credential 中如还未配置Gitee APIV5 私人令牌,点击 Add - > Jenkins
0. `Domain` 选择 `Global credentials`
1. `Kind` 选择 `Gitee API Token`
2. `Scope` 选择你需要的范围
3. `Gitee API Token` 输入你的Gitee私人令牌,获取地址:[gitee.com/profile/per…](https://gitee.com/profile/personal_access_tokens)
4. `ID`, `Descripiton` 中输入你想要的 ID 和描述即可。
  1. Credentials 选择配置好的 Gitee APIV5 Token
  2. 点击 Advanced ,可配置是否忽略 SSL 错误(视您的Jenkins环境是否支持),并可设置链接测超时时间(视您的网络环境而定)
  3. 点击 Test Connection 测试链接是否成功,如失败请检查以上 3,5,6 步骤。

配置成功后如图所示: 185651_68707d16_58426.png

新建构建任务

前往 Jenkins -> New Item , name 输入 ‘Gitee Test’,选择 Freestyle project 保存即可创建构建项目。

任务全局配置

任务全局配置中需要选择前一步中的Gitee链接。前往某个任务(如’Gitee Test’)的 Configure -> General,Gitee connection 中选择前面所配置的Gitee链接,如图:

191715_9660237b_58426.png

源码管理配置

前往某个任务(如’Gitee Test’)的 Configure -> Source Code Management 选项卡

  1. 点击 Git
  2. 输入你的仓库地址,例如 git@your.gitee.server:gitee_group/gitee_project.git
0. 点击 *Advanced* 按钮, *Name* 字段中输入 `origin`, *Refspec* 字段输入 `+refs/heads/*:refs/remotes/origin/* +refs/pull/*/MERGE:refs/pull/*/MERGE` ,注意新版jenkins不再接受多条同时包含 \* 通配符的refs描述,如只对push触发可写前半部分,如只对PR触发可只写后半段。具体可见下图:![220940_0ce95dd0_58426.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/37d0e669f9bb51e9d0ad0a616ba33409d28de414aae273fa74fa44de67997a4f)
  1. 凭据Credentials 中请输入 git 仓库 https 地址对应的 用户名密码凭据,或者 ssh 对应的 ssh key 凭据,注意 Gitee API Token 凭据不可用于源码管理的凭据,只用于 gitee 插件的 API 调用凭据。
  2. Branch Specifier选项:
0. 对于单仓库工作流输入: `origin/${giteeSourceBranch}`
1. 对于 PR 工作流输入: `pull/${giteePullRequestIid}/MERGE`
  1. Additional Behaviours 选项:
0. 对于单仓库工作流,如果你希望推送的分支构建前合并默认分支(发布的分支),可以做以下操作:


    0. 点击 *Add* 下拉框
    1. 选择 *Merge before build*
    2. 设置 *Name of repository* 为 `origin`
    3. 设置 *Branch to merge to* 为 `${ReleaseBranch}` 即您要合并的默认分支(发布分支)
1. 对于 PR 工作流,Gitee服务端已经将 PR 的原分支和目标分支作了预合并,您可以直接构建,如果目标分支不是默认分支(发布分支),您也可以进行上诉构建前合并。

配置如图所示:

191913_ef0995f4_58426.png

触发器配置

前往任务配置的触发器构建: Configure -> Build Triggers 选项卡

  1. Enabled Gitee triggers 勾选您所需要的构建触发规则,如Push Event,Opened Merge Request Events,勾选的事件会接受 WebHook,触发构建。目前支持触发事件有:
* Push Events :推送代码事件
* Commit Comment Events :评论提交记录事件
* Opened Merge Request Events :提交 PR 事件
* Updated Merge Request Events :更新 PR 事件
* Accepted Merge Request Events :接受/合并 PR 事件
* Closed Merge Request Events :关闭 PR 事件
* Approved Pull Requests : 审查通过 PR 事件
* Tested Pull Requests :测试通过 PR 事件
  1. Build Instruction Filter:
* `None` : 无过滤
* `[ci-skip] skip build` :commit message 或者 PR 说明包含 `[ci-skip]` 时,跳过构建触发。
* `[ci-build] trigger build` :commit message 或者 PR 说明包含 `[ci-build]` 时,触发构建。
  1. Ignore last commit has build 该选项可以跳过已经构建过的 Commit 版本。
  2. Cancel incomplete build on same Pull Requests 该选项在 PR 触发构建时,会判断是否存在相同 PR 且未完成的构建,有则取消未完成构建,再进行当前构建。
  3. Ignore Pull Request conflicts 该选项在 PR 触发构建时,会根据 PR 冲突情况选择是否进行构建。
  4. Allowed branches 可以配置允许构建的分支,目前支持分支名和正则表达式的方式进行过滤。
  5. Secret Token for Gitee WebHook 该选项可以配置 WebHook 的密码,该密码需要与Gitee WebHook配置的密码一致方可触发构建。
  6. 注意:若 PR 状态为不可自动合并,则不触发构建。 171932_e25c8359_2102225.png

构建后步骤配置

前往任务配置的构建后配置: Configure -> Post-build Actions 选项卡

构建结果回评至Gitee

  1. 点击 Add post-build action 下拉框选择:Add note with build status on Gitee pull requests
  2. Advanced 中可以配置:
* Add message only for failed builds :仅为构建失败回评到Gitee
* 自定义各状态的回评内容(内容可以引用 Jenkins 的环境变量,或者自定义的环境变量)
  1. 若开启该功能,还可将不可自动合并的状态回评至Gitee

构建成功自动合并PR

点击 Add post-build action 下拉框选择:Accept Gitee pull request on success

192304_0e323bc0_58426.png

新建Gitee项目WebHook

进入源码管理配置中设置的Gitee项目中,进入 管理 -> WebHooks

  1. 添加 WebHook, URL 填写 触发器配置:Build when a change is pushed to Gitee. Gitee webhook URL 中所示 URL,如:: http://127.0.0.1:8080/jenkins/project/fu
  2. 密码填写:触发器配置第 5 点中配置的 WebHook密码,不设密码可以不填
  3. 勾选 PUSH, Pull Request

测试推送触发构建

  1. Gitee的 WebHook 管理中选择勾选了PUSH的 WebHook 点击测试,观察 Jenkins 任务的构建状态
  2. Gitee项目页面编辑一个文件提交,观察 Jenkins 任务的构建状态

测试PR触发构建

  1. Gitee的 WebHook 管理中选择勾选了 Pull Request 的 WebHook 点击测试,观察 Jenkins 任务的构建状态
  2. 在Gitee项目中新建一个Pull Request,观察 Jenkins 任务的构建状态

本文转载自: 掘金

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

Flink 在顺丰的应用实践 一、建设背景 二、建设思路 三

发表于 2021-09-02

简介: 顺丰基于 Flink 建设实时数仓的思路,引入 Hudi On Flink 加速数仓宽表,以及实时数仓平台化建设的实践。

本⽂由社区志愿者苗文婷整理,内容源⾃顺丰科技大数据平台研发工程师龙逸尘在 Flink Forward Asia 2020 分享的《Flink 在顺丰的应用实践》,主要分享内容为:顺丰基于 Flink 建设实时数仓的思路,引入 Hudi On Flink 加速数仓宽表,以及实时数仓平台化建设的实践。分为以下 5 个部分:

建设背景建设思路落地实践应用案例未来规划

一、建设背景

顺丰是国内领先的快递物流综合服务商,经过多年的发展,顺丰使用大数据技术支持高质量的物流服务。以下是一票快件的流转过程,可以看到从客户下单到最终客户收件的整个过程是非常长的,其中涉及的一些处理逻辑也比较复杂。为了应对复杂业务的挑战,顺丰进行了数据仓库的探索。

传统数仓主要分为离线和实时两个部分。

  • 离线部分以固定的计算逻辑,通过定时调度,完成数据抽取,清洗,计算,最后产出报表;
  • 而实时部分则是需求驱动的,用户需要什么,就马上着手开发。

这种数仓架构在数据量小、对实时性要求不高的情况下运行得很好。然而随着业务的发展,数据规模的扩大和实时需求的不断增长,传统数仓的缺点也被放大了。

  • 从业务指标的开发效率来看

实时指标采用的是需求驱动的、纵向烟囱式的开发模式,需要用户手写 Flink 任务进行开发,这种开发方式效率低门槛高,输出的指标很难统一管理与复用。

  • 从技术架构方面来看

离线和实时两套架构是不统一的,开发方式、运维方式、元数据方面都存在差异。传统架构整体还是以离线为主,实时为辅,依赖离线 T+1 调度导出报表,这些调度任务通常都运行在凌晨,导致凌晨时集群压力激增,可能会导致报表的产出不稳定;如果重要的报表产出有延迟,相应的下游的报表产出也会出现延迟。这种以离线为主的架构无法满足精细化、实时化运营的需要。

  • 从平台管理的角度来看

传统数仓的实时指标开发是比较粗放的,没有 Schema 的规范,没有元数据的管理,也没有打通实时和离线数据之间的联系。

为了解决传统数仓的问题,顺丰开始了实时数仓的探索。实时数仓和离线数仓实际上解决的都是相同的业务问题,最大的区别就在于时效性。

  • 离线数仓有小时级或天级的延迟;
  • 而实时数仓则是秒级或分钟级的延迟。

其他特性,比如数据源、数据存储以及开发方式都是比较相近的。因此,我们希望:

  • 用户能从传统数仓平滑迁移到实时数仓,保持良好的体验;
  • 同时统一实时和离线架构,加快数据产出,减少开发的撕裂感;
  • 加强平台治理,降低用户使用门槛,提高开发效率也是我们的目标。

二、建设思路

经过总结,我们提炼出以下 3 个实时数仓的建设思路。首先是通过统一数仓标准、元数据以及开发流程,使得用户达到开发体验上的批流统一。随后,引入 Hudi 加速数仓宽表,基于 Flink SQL 建设我们的实时数仓。最后是加强平台治理,进行数仓平台化建设,实现数据统一接入、统一开发、以及统一的元数据管理。

1. 批流统一的实时数仓

建设批流统一的实时数仓可以分为以下 3 个阶段:

1.1 统一数仓规范

首先,无规矩不成方圆,建设数仓必须有统一的数仓规范。统一的数仓规范包括以下几个部分:

  • 设计规范
  • 命名规范
  • 模型规范
  • 开发规范
  • 存储规范
  • 流程规范

统一好数仓规范之后,开始数仓层级的划分,将实时和离线统一规划数仓层级,分为 ODS、DWD、DWS、ADS 层。

1.2 统一元数据

基于以上统一的数仓规范和层级划分模型,可以将实时和离线的元数据进行统一管理。下游的数据治理过程,比如数据字典、数据血缘、数据质量、权限管理等都可以达到统一。这种统一可以沉淀实时数仓的建设成果,使数仓能更好的落地实施。

1.3 基于 SQL 统一开发流程

开发人员都知道,使用 DataStream API 开发 Flink 任务是比较复杂的。在数据量比较大的情况下,如果用户使用 API 不规范或者开发能力不足,可能会导致性能和稳定性的问题。如果我们能将实时开发的过程统一到 SQL 上,就可以达到减少用户开发成本、学习成本以及运维成本的目的。

之前提到过我们已经统一了实时和离线的元数据,那么就可以将上图左边的异构数据源和数据存储抽象成统一的 Table ,然后使用 SQL 进行统一的数仓开发,也就是将离线批处理、实时流处理以及 OLAP 查询统一 SQL 化。

1.4 实时数仓方案对比

完成了数仓规范、元数据、开发流程的统一之后,我们开始探索数仓架构的具体架构方案。业界目前的主流是 Lambda 架构和 Kappa 架构。

  • Lambda 架构

Lambda 架构是在原有离线数仓的基础上,将对实时性要求比较高的部分剥离出来,增加了一个实时速度层。Lambda 架构的缺点是需要维护实时和离线两套架构和两套开发逻辑,维护成本比较高,另外两套架构带来的资源消耗也是比较大的。

  • Kappa 架构

为了应对 Lambda 架构的缺陷,Jay Kreps 提出了 Kappa 架构,Kappa 架构移除了原有的离线部分,使用纯流式引擎开发。 Kappa 架构的最大问题是,流数据重放处理时的吞吐能力达不到批处理的级别,导致重放时产生一定的延时。

  • 实时数仓方案对比与实际需求

在真实的生产实践中,并不是一定要严格遵循规范的 Lambda 架构或 Kappa 架构,可以是两者的混合。比如大部分指标使用流式引擎开发,少部分重要的指标使用批处理开发,并增加数据校对的过程。

在顺丰的业务场景中,并非所有用户都需要纯实时的表,许多用户的报表还是依赖离线 T+1 调度产出的宽表,如果我们能够加速宽表的产出,那么其他报表的时效性也能相应地得到提高。

另外,这个离线 T+1 调度产出的宽表,需要聚合 45 天内多个数据源的全量数据,不管是 Lambda 架构还是 Kappa 架构,都需要对数据进行全量聚合,如果能够直接更新宽表,就可以避免全量重新计算,大大降低资源消耗和延时。

2. 引入 Hudi 加速宽表

之前说过,维护 Lambda 架构的复杂性在于需要同时维护实时和离线两套系统架构。而对于这个缺点,我们可以通过批流统一来克服。

经过权衡,我们决定改造原有 Lambda 架构,通过加速它的离线部分来建设数仓宽表。此时,就需要一个工具来实时快速的更新和删除 Hive 表,支持 ACID 特性,支持历史数据的重放。基于这样的需求,我们调研了市面上的三款开源组件:Delta Lake、Iceberg、Hudi,最后选择 Hudi 来加速宽表。

2.1 Hudi 关键特性

Hudi 的关键特性包括:可回溯历史数据,支持在大规模数据集中根据主键更新删除数据;支持数据增量消费;支持 HDFS 小文件压缩。这些特性恰好能满足我们的需求。

2.2 引入 Hudi 加速宽表

引入 Hudi 有两种方式加速数仓。首先,在 ODS 层引入 Hudi 实现实时数据接入,将 ODS 层 T+1 的全量数据抽取改成 T+0 的实时接入,从数据源头实现 Hive 表的加速。

另外,使用 Flink 消费 Kafka 中接入的数据,进行清洗聚合,通过 Hudi 增量更新 DWD 层的 Hive 宽表,将宽表从离线加速成准实时。

2.3 构建实时数仓宽表示例

这里通过一个例子介绍如何构建实时数仓宽表。

假设运单宽表由运单表,订单表和用户表组成,分别包含运单号、运单状态、订单号、订单状态、用户 ID、用户名等字段。

首先将运单表数据插入宽表,运单号作为宽表主键,并且将运单号和订单号的映射存入临时表。当订单表数据更新后,首先关联用户维表,获取用户名,再从临时表中获取对应运单号。最后根据运单号将订单表数据增量插入宽表,以更新宽表状态。

3. 最终架构

引入 Hudi 后,基于 Lambda 架构,我们定制化的实时数仓最终架构如下图所示。实时速度层通过 CDC 接入数据到 Kafka,采用 Flink SQL 处理 Kafka 中的数据,并将 ODS 层 Kafka 数据清洗计算后通过 Hudi 准实时更新 DWD 层的宽表,以加速宽表的产出。离线层采用 Hive 存储及处理。最后由 ADS 层提供统一的数据存储与服务。

除了制定数仓标准和构建数仓架构,我们还需要构建数仓平台来约束开发规范和流程,提升开发效率,提高用户体验。

站在数据开发人员的角度,我们不仅要提供快速的数据接入能力,还需要关注开发效率以及统一的元数据治理。因此可以基于 Table 和 SQL 抽象,对数据接入、数据开发、元数据管理这三个主要功能进行平台化,为实时数仓用户提供统一、便捷、高效的体验。

三、落地实践

1. Hudi On Flink

顺丰是最早将 Hudi On Flink 引入生产实践的公司,顺丰内部使用版本基于 T3 出行的内部分支进行了许多修改和完善,大大提升了 Hudi on Flink 的性能和稳定性。

1.1 实现原理

这里介绍下 Hudi On Flink 的原理。Hudi 原先与 Spark 强绑定,它的写操作本质上是批处理的过程。为了解耦 Spark 并且统一 API ,Hudi On Flink 采用的是在 Checkpoint 期间攒批的机制,在 Checkpoint 触发时将这一批数据Upsert 到 Hive,根据 Upsert 结果统一提交或回滚。

Hudi On Flink 的实现流可以分解为几个步骤:

  1. 首先使用 Flink 消费 Kafka 中的 Binlog 类型数据,将其转化为 Hudi Record。
  2. Hudi Record 进入 InstantTime Generator,该 Operator 并不对数据做任何处理,只负责转发数据。它的作用是每次 Checkpoint 时在 Hudi 的 Timeline 上生成全局唯一且递增的 Instant,并下发。
  3. 随后,数据进入 Partitioner ,根据分区路径以及主键进行二级分区。分区后数据进入 File Indexer ,根据主键找到在 HDFS 上需要更新的对应文件,将这个对应关系按文件 id 进行分桶,并下发到下游的 WriteProcessOperator 。
  4. WriteProcessOperator 在 Checkpoint 期间会积攒一批数据,当 Checkpoint 触发时,通过 Hudi 的 Client 将这批数据 Upsert 到 HDFS 中,并且将 Upsert 的结果下发到下游的 CommitSink 。
  5. CommitSink 会收集上游所有算子的 upsert 结果,如果成功的个数和上游算子的并行度相等时,就认为本次 commit 成功,并将 Instant 的状态设置为 success ,否则就认为本次 commit 失败并进行回滚。

1.2 优化

顺丰基于社区代码对 Hudi On Flink 进行了一些优化,主要目的是增强性能和提升稳定性。

  • 二级分区

对于增量写入的场景,大部分的数据都写入当天的分区,可能会导致数据倾斜。因此,我们使用分区路径和主键 id 实现二级分区,避免攒批过程中单个分区数据过多,解决数据倾斜问题。

  • 文件索引

Hudi 写入过程的瓶颈在于如何快速找到记录要写入的文件并更新。为此 Hudi 提供了一套索引机制,该机制会将一个记录的键 + 分区路径的组合映射到一个文件 ID. 这个映射关系一旦记录被写入文件组就不会再改变。Hudi 当前提供了 HBase、Bloom Filter 和内存索引 3 种索引机制。然而经过生产实践,HBase 索引需要依赖外部的组件,内存索引可能存在 OOM 的问题,Bloom Filter 存在一定的误算率。我们研究发现,在 Hudi 写入的 parquet 文件中存在一个隐藏的列,通过读取这个列可以拿到文件中所有数据的主键,因此可以通过文件索引获取到数据需要写入的文件路径,并保存到 Flink 算子的 state 中,也避免了外部依赖和 OOM 的问题。

  • 索引写入分离

原先 Hudi 的 Upsert 过程,写入和索引的过程是在一个算子中的,算子的并行度只由分区路径来决定。我们将索引和写入的过程进行分离,这样可以提高 Upsert 算子的并行度,提高写入的吞吐量。

  • 故障恢复

最后我们将整个流程的状态保存到 Flink State 中,设计了一套基于 State 的故障恢复机制,可以保证端到端的 exactly-once 语义。

2. 实时数仓的产品化

在实时数仓产品化方面,我们也做了一些工作。提供了包括数据接入、元数据管理、数据处理在内的数仓开发套件。

2.1 实时数据接入

实时数据接入采用的是表单式的流程接入方式,屏蔽了复杂的底层技术,用户只需要经过简单的操作就可以将外部数据源接入到数仓体系。以 MySQL 为例,用户只需要选择 MySQL 数据源,平台就会自动抽取并展示 Schema ,用户确认 Schema 之后,就会将 Schema 插入到平台元数据中。

随后,用户选择有权限的集群,设置 Hive 表的主键 ID 和分区字段,提交申请之后,平台就会自动生成 Flink 任务,抽取数据到 Kafka 并自动落入 Hive 表中。对数据库类型的数据源,还支持分库分表功能,将分库分表的业务数据写入 ODS 层的同一张表。另外也支持采集主从同步的数据库,从从库中查询存量数据,主库拉取 Binlog,在减轻主库压力的同时降低数据同步延迟。

2.2 实时元数据更新

实时元数据更新的过程,还是以 MySQL 为例。CDC Source 会抽取数据库中的 Binlog ,区分 DDL 和 DML 语句分别处理,DDL 语句会上报到元数据中心,DML 语句经过转化变成 avro 格式的 Binlog 数据发送到 Kafka ,如果下游有写入到 Hive 的需求,就消费 Kafka 的数据通过 Hudi Sink 写入到 Hive 。

2.3 数据资产管理体系

基于实时数据的统一接入,并将其与现有的离线数仓结合,我们构建了数据资产管理体系。包括规范数仓标准,统一管理元数据,提升数据质量,保障数据安全,盘点数据资产。

3. 实时计算平台架构

有了数据统一接入的基础和数据资产资产管理体系的保驾护航,我们还需要一个数据开发套件,将整个数据开发的过程整合到实时计算平台。实时计算平台的最底层是数据接入层,支持 Kafka 和 Binlog 等数据源。上一层是数据存储层,提供了 Kafka 、ES、HBase、Hive、ClickHouse、MySQL 等存储组件。支持 JStorm 、Spark Streaming、Flink 计算引擎。并进行了框架封装和公共组件打包。

3.1 多种开发模式 - JAR & DRAG

实时计算平台提供了多种开发模式供不同用户选择。以 Flink 为例,Flink JAR 模式由用户编写 Flink 任务代码,打成 jar 包上传到平台,满足高级用户的需求。Flink DRAG 模式则是图形化的拖拽式开发,由平台封装好公共组件之后,用户只需要拖拽公共组件,将其组装成一个 Flink 任务,提交至集群运行。

3.2 多种开发模式 - SQL

实时计算平台同样提供 SQL 开发模式,支持手动建表,根据元数据自动识别表及设置表属性。支持创建 UDF、自动识别 UDF、执行 DML 等。

3.3 任务管控

在任务管控方面,实时计算平台尽量简化任务的配置,屏蔽了一些复杂的配置。用户开发完成之后,只需要选择集群,填写资源,就能将任务提交到集群中运行。对每个任务,平台还提供了历史版本控制能力。

当用户操作任务时,平台会自动解析任务的配置,根据不同的组件提供不同的选项。比如选择了 Kafka 数据源,启动的时候,可以选择从上次消费位置、最早位置、最新位置或指定位置启动。

任务恢复方面,用户可以选择从 Savepoint 启动已停止的 Flink 任务,便于快速恢复历史状态。

3.4 任务运维

对实时任务来说,任务运维是一个难点也是一个痛点。平台提供了日志查询功能,采集历史的启动日志和任务运行日志,用户可以方便的进行对比和查询。

当任务启动之后,平台会自动采集并上报任务的指标,用户可以根据这些指标自定义告警配置,当告警规则被触发时,平台会通过各种方式告警到用户。最后,平台提供了指标的实时监控看板,当然用户也可以自行在 Grafana 中配置监控看板。

通过采集日志、指标以及监控告警,以及过往的历史经验,我们实现了一个智能的机器客服,可以实现任务故障的一些自助诊断。这些举措大大降低了任务的运维成本,减轻平台研发人员的压力。

3.5 Flink 任务稳定性保障

实时作业运维最关注的是稳定性,在保障 Flink 任务稳定性上我们也有一些实践。首先提供多种异常检测和监控告警的功能,方便用户快速的发现问题。每个任务都会定时的生成任务快照,保存任务历史的 Savepoint,以方便任务回滚和故障恢复。任务可能会由于某种异常原因导致任务失败,任务失败之后会被平台重新拉新,并指定从上次失败的位置开始重新消费。

基于 Zookeeper 的高可用机制,以保障 JobManager 的可用性。支持多集群、多机房的容灾切换能力,可以将任务一键切换至容灾集群上运行。实现了一套实时离线集群隔离、队列管理的资源隔离系统。

四、应用案例

以业务宽表计算为例,需要获取 45 天内的多个数据源的数据,进行计算聚合。如果使用离线数仓,大概需要 3000 核的 CPU、12000G 的内存,耗时 120 ~ 150 min 完成计算,处理的数据量大概为 450T。如果使用实时数仓,大概需要 2500 核的 CPU、1400G 的内存,更新宽表大概有 2~5 min 的延时,处理的数据量约为 18T。

五、未来规划

顺丰的实时数仓建设取得了一些成果,但未来仍需要进行不断的优化。

1. 增强 SQL 能力

首先,希望能够支持更多 SQL 的语法和特性,支持更多可用的连接器,以及实现 SQL 任务的自动调优等。

2. 精细化资源管理

其次,基于 Flink On Kubernets 、任务的自动弹性扩缩容,Task 级别的细粒度资源调度实现精细化的资源调度管理,使得 Flink 任务达到全面的弹性化和云原生化。

3. 流批一体

最后,希望能够实现流批一体,通过统一的高度兼容性的 SQL ,经过 SQL 解析以及引擎的适配,通过 Flink 统一的引擎去处理流和批。

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

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

Docker 初级

发表于 2021-09-02

看这篇文章之前,你需要提前了解容器化的概念,以及docker的基础架构和概念
architecture.svg

安装环境

一个好的程序员,怎么能没有属于自己的测试环境。接下来我将记录真实的操作,一步一步搭建属于自己的测试环境。

环境准备:虚拟机,CentOS7.x,docker-ce,防火墙关闭。

  1. 虚拟机安装、关闭防火墙
1
复制代码略
  1. docker-ce安装
1
2
3
4
5
6
7
8
9
10
11
bash复制代码# step 1: 安装必要的一些系统工具
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
# Step 2: 添加软件源信息
sudo yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
# Step 3
sudo sed -i 's+download.docker.com+mirrors.aliyun.com/docker-ce+' /etc/yum.repos.d/docker-ce.repo
# Step 4: 更新并安装Docker-CE
sudo yum makecache fast
sudo yum -y install docker-ce
# Step 4: 开启Docker服务
sudo systemctl enable docker --now
  1. docker镜像仓库加速、日志配置并重启docker服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
javascript复制代码cat > /etc/docker/daemon.json << EOF
{
"registry-mirrors": [
"https://mirror.ccs.tencentyun.com"
],
"log-driver":"local",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
EOF

service docker restart
  1. 验证
1
复制代码docker -v

至此,docker运行环境就弄好了

运行简单容器

接下来使用docker来运行第一个容器,以mysql为例

第一步:docker中央仓库调研

  • dockerhub 找到官方提供的mysql镜像
  • 提取关键信息
1
2
3
4
5
6
7
ini复制代码Starting a MySQL instance is simple:
$ docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag

Where to Store Data
$ docker run --name some-mysql -v /my/own/datadir:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag

# 还有很多关于配置文件如何挂载,和一些环境变量,详细的Dockerfile也可以查看,此处只取最常用的一些

第二步:镜像拉取及容器运行

  • 拉取mysql镜像
1
复制代码docker pull mysql:latest
  • 查看镜像
1
复制代码docker images
  • 后台运行容器
1
bash复制代码docker run -d --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -v /root/mysql-data:/var/lib/mysql mysql:latest
  • 查看运行的容器
1
复制代码docker ps
  • 进入mysql容器执行sql
1
2
3
4
bash复制代码docker exec -it mysql bash
mysql -uroot -p
# 输入密码:123456
show databases;

ok,至此,一个完整的容器运行完毕。当虚拟机重启,只需使用docker start mysql启动mysql服务。

小结:所有软件的镜像都如出一辙,通过简单的两步,任何软件都能使用docker管理起来。下一步,分享docker的网络和数据卷以及docker排错和一些使用上的技巧,敬请期待~

接下来:docker 高级

本文转载自: 掘金

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

再见 Shiro!权限认证我选择 Sa-Token:简单、优

发表于 2021-09-02

前言:

在 JavaWeb 项目中,认证授权是必不可少的功能,比较传统的框架有 Shiro、SpringSecurity 等等,这些框架在如今“前后端分离” 已成主流的情况下,并不是十分好用,需要写不少兼容性代码才能集成。

今天给大家推荐的这个框架是 Sa-Token,可以让我们非常优雅的集成鉴权功能!

GitHub开源地址:github.com/dromara/sa-…

Sa-Token 介绍

Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、Session会话、单点登录、OAuth2.0、微服务网关鉴权 等一系列权限相关问题。

Sa-Token 的 API 设计非常简单,有多简单呢?以登录认证为例,你只需要:

1
2
3
4
5
6
java复制代码// 在登录时写入当前会话的账号id
StpUtil.login(10001);

// 然后在需要校验登录处调用以下方法:
// 如果当前会话未登录,这句代码会抛出 `NotLoginException` 异常
StpUtil.checkLogin();

至此,我们已经借助 Sa-Token 完成登录认证!

此时的你小脑袋可能飘满了问号,就这么简单?自定义 Realm 呢?全局过滤器呢?我不用写各种配置文件吗?

没错,在 Sa-Token 中,登录认证就是如此简单,不需要任何的复杂前置工作,只需这一行简单的API调用,就可以完成会话登录认证!

当你受够 SpringSecurity、Shiro 等框架的三拜九叩之后,你就会明白,相对于这些传统老牌框架,Sa-Token 的 API 设计是多么的简单、优雅!

权限认证示例(只有具备 user:add 权限的会话才可以进入请求)

1
2
3
4
5
6
java复制代码@SaCheckPermission("user:add")
@RequestMapping("/user/insert")
public String insert(SysUser user) {
// ...
return "用户增加";
}

将某个账号踢下线(待到对方再次访问系统时会抛出NotLoginException异常)

1
2
java复制代码// 使账号id为 10001 的会话强制注销登录
StpUtil.logoutByLoginId(10001);

在 Sa-Token 中,绝大多数功能都可以 一行代码 完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码StpUtil.login(10001);   // 标记当前会话登录的账号id
StpUtil.getLoginId(); // 获取当前会话登录的账号id
StpUtil.isLogin(); // 获取当前会话是否已经登录, 返回true或false
StpUtil.logout(); // 当前会话注销登录
StpUtil.logoutByLoginId(10001); // 让账号为10001的会话注销登录(踢人下线)
StpUtil.hasRole("super-admin"); // 查询当前账号是否含有指定角色标识, 返回true或false
StpUtil.hasPermission("user:add"); // 查询当前账号是否含有指定权限, 返回true或false
StpUtil.getSession(); // 获取当前账号id的Session
StpUtil.getSessionByLoginId(10001); // 获取账号id为10001的Session
StpUtil.getTokenValueByLoginId(10001); // 获取账号id为10001的token令牌值
StpUtil.login(10001, "PC"); // 指定设备标识登录,常用于“同端互斥登录”
StpUtil.logoutByLoginId(10001, "PC"); // 指定设备标识进行强制注销 (不同端不受影响)
StpUtil.openSafe(120); // 在当前会话开启二级认证,有效期为120秒
StpUtil.checkSafe(); // 校验当前会话是否处于二级认证有效期内,校验失败会抛出异常
StpUtil.switchTo(10044); // 将当前会话身份临时切换为其它账号

即使不运行测试,相信您也能意会到绝大多数 API 的用法。

Sa-Token 功能一览

  • 登录认证 —— 单端登录、多端登录、同端互斥登录、七天内免登录
  • 权限认证 —— 权限认证、角色认证、会话二级认证
  • Session会话 —— 全端共享Session、单端独享Session、自定义Session
  • 踢人下线 —— 根据账号id踢人下线、根据Token值踢人下线
  • 账号封禁 —— 指定天数封禁、永久封禁、设定解封时间
  • 持久层扩展 —— 可集成Redis、Memcached等专业缓存中间件,重启数据不丢失
  • 分布式会话 —— 提供jwt集成、共享数据中心两种分布式会话方案
  • 微服务网关鉴权 —— 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证
  • 单点登录 —— 内置三种单点登录模式:无论是否跨域、是否共享Redis,都可以搞定
  • OAuth2.0认证 —— 基于RFC-6749标准编写,OAuth2.0标准流程的授权认证,支持openid模式
  • 二级认证 —— 在已登录的基础上再次认证,保证安全性
  • 独立Redis —— 将权限缓存与业务缓存分离
  • 临时Token验证 —— 解决短时间的Token授权问题
  • 模拟他人账号 —— 实时操作任意用户状态数据
  • 临时身份切换 —— 将会话身份临时切换为其它账号
  • 前后台分离 —— APP、小程序等不支持Cookie的终端
  • 同端互斥登录 —— 像QQ一样手机电脑同时在线,但是两个手机上互斥登录
  • 多账号认证体系 —— 比如一个商城项目的user表和admin表分开鉴权
  • 花式token生成 —— 内置六种Token风格,还可:自定义Token生成策略、自定义Token前缀
  • 注解式鉴权 —— 优雅的将鉴权与业务代码分离
  • 路由拦截式鉴权 —— 根据路由拦截鉴权,可适配restful模式
  • 自动续签 —— 提供两种Token过期策略,灵活搭配使用,还可自动续签
  • 会话治理 —— 提供方便灵活的会话查询接口
  • 记住我模式 —— 适配[记住我]模式,重启浏览器免验证
  • 密码加密 —— 提供密码加密模块,可快速MD5、SHA1、SHA256、AES、RSA加密
  • 全局侦听器 —— 在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作
  • 开箱即用 —— 提供SpringMVC、WebFlux等常见web框架starter集成包,真正的开箱即用

单点登录 & OAuth2.0

通过 Sa-Token ,我们可以很方便的搭建单点登录认证中心:

1
2
3
4
5
6
7
8
9
10
11
java复制代码/**
* Sa-Token-SSO Server端 Controller
*/
@RestController
public class SsoServerController {
// SSO-Server端:处理所有SSO相关请求
@RequestMapping("/sso/*")
public Object ssoRequest() {
return SaSsoHandle.serverRequest();
}
}

同样,我们也可以很方便的搭建一个 OAuth2.0 认证中心:

1
2
3
4
5
6
7
8
9
10
11
java复制代码/**
* Sa-Token-OAuth2.0 Server端 Controller
*/
@RestController
public class SaOAuth2ServerController {
// SSO-Server端:处理所有SSO相关请求
@RequestMapping("/oauth2/*")
public Object request() {
return SaSsoHandle.serverRequest();
}
}

详细代码请参考框架官网介绍。

总结

通过以上示例我们可以看出,Sa-Token 同时兼顾 简单、全面 两个优点,是一个非常值得上手的优秀框架。

同时,其官网非常文档也写的非常详细,不光介绍了 Sa-Token 框架,还介绍了非常多的权限设计思想,是真正的授人以渔。

官方文档

参考资料

  • 开源地址:github.com/dromara/sa-…
  • 官方文档:sa-token.dev33.cn/

本文转载自: 掘金

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

开箱即用!看看人家的微服务权限解决方案,那叫一个优雅!

发表于 2021-09-02

记得之前写过一篇文章微服务权限终极解决方案,Spring Cloud Gateway + Oauth2 实现统一认证和鉴权! ,提供了Spring Cloud中的权限解决方案,其实一开始整合的时候我一直玩不转,又是查资料又是看源码,最终才成功了。最近尝试了下Sa-Token提供的微服务权限解决方案,用起来感觉很优雅,推荐给大家!

SpringBoot实战电商项目mall(50k+star)地址:github.com/macrozheng/…

前置知识

我们将采用Nacos作为注册中心,Gateway作为网关,使用Sa-Token提供的微服务权限解决方案,此方案是基于之前的解决方案改造的,对这些技术不了解的朋友可以看下下面的文章。

  • Spring Cloud Gateway:新一代API网关服务
  • Spring Cloud Alibaba:Nacos 作为注册中心和配置中心使用
  • 微服务权限终极解决方案,Spring Cloud Gateway + Oauth2 实现统一认证和鉴权!
  • Sa-Token使用教程

应用架构

还是和之前方案差不多的思路,认证服务负责登录处理,网关负责登录认证和权限认证,其他API服务负责处理自己的业务逻辑。为了能在多个服务中共享Sa-Token的Session,所有服务都需要集成Sa-Token和Redis。

  • micro-sa-token-common:通用工具包,其他服务公用的用户类UserDTO和通用返回结果类CommonResult被抽取到了这里。
  • micro-sa-token-gateway:网关服务,负责请求转发、登录认证和权限认证。
  • micro-sa-token-auth:认证服务,仅包含一个登录接口,调用Sa-Token的API实现。
  • micro-sa-token-api:受保护的API服务,用户通过网关鉴权通过后可以访问该服务。

方案实现

接下来实现下这套解决方案,依次搭建网关服务、认证服务和API服务。

micro-sa-token-gateway

我们首先来搭建下网关服务,它将负责整个微服务的登录认证和权限认证。

  • 除了通用的Gateway依赖,我们还需要在pom.xml中添加如下依赖,包括Sa-Token的Reactor响应式依赖,整合Redis实现分布式Session的依赖以及我们的micro-sa-token-common依赖;
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
xml复制代码<dependencies>
<!-- Sa-Token 权限认证(Reactor响应式集成) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot-starter</artifactId>
<version>1.24.0</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用jackson序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.24.0</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- micro-sa-token通用依赖 -->
<dependency>
<groupId>com.macro.cloud</groupId>
<artifactId>micro-sa-token-common</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
  • 接下来修改配置文件application.yml,添加Redis配置和Sa-Token的配置,如果你看过之前那篇Sa-Token使用教程的话,基本就知道这些配置的作用了;
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
yaml复制代码spring:
redis:
database: 0
port: 6379
host: localhost
password:

# Sa-Token配置
sa-token:
# token名称 (同时也是cookie名称)
token-name: Authorization
# token有效期,单位秒,-1代表永不过期
timeout: 2592000
# token临时有效期 (指定时间内无操作就视为token过期),单位秒
activity-timeout: -1
# 是否允许同一账号并发登录 (为false时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个token (为false时每次登录新建一个token)
is-share: false
# token风格
token-style: uuid
# 是否输出操作日志
is-log: false
# 是否从cookie中读取token
is-read-cookie: false
# 是否从head中读取token
is-read-head: true
  • 添加Sa-Token的配置类SaTokenConfig,注入一个过滤器用于登录认证和权限认证,在setAuth方法中添加路由规则,在setError方法中添加鉴权失败的回调处理;
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
java复制代码@Configuration
public class SaTokenConfig {
/**
* 注册Sa-Token全局过滤器
*/
@Bean
public SaReactorFilter getSaReactorFilter() {
return new SaReactorFilter()
// 拦截地址
.addInclude("/**")
// 开放地址
.addExclude("/favicon.ico")
// 鉴权方法:每次访问进入
.setAuth(r -> {
// 登录认证:除登录接口都需要认证
SaRouter.match("/**", "/auth/user/login", StpUtil::checkLogin);
// 权限认证:不同接口访问权限不同
SaRouter.match("/api/test/hello", () -> StpUtil.checkPermission("api:test:hello"));
SaRouter.match("/api/user/info", () -> StpUtil.checkPermission("api:user:info"));
})
// setAuth方法异常处理
.setError(e -> {
// 设置错误返回格式为JSON
ServerWebExchange exchange = SaReactorSyncHolder.getContent();
exchange.getResponse().getHeaders().set("Content-Type", "application/json; charset=utf-8");
return SaResult.error(e.getMessage());
});
}
}
  • 扩展下Sa-Token提供的StpInterface接口,用于获取用户的权限,我们在用户登录以后会把用户信息存到Session中去,权限信息也会在里面,所以权限码只要从Session中获取即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码/**
* 自定义权限验证接口扩展
*/
@Component
public class StpInterfaceImpl implements StpInterface {

@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 返回此 loginId 拥有的权限码列表
UserDTO userDTO = (UserDTO) StpUtil.getSession().get("userInfo");
return userDTO.getPermissionList();
}

@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 返回此 loginId 拥有的角色码列表
return null;
}

}

micro-sa-token-auth

接下来我们来搭建下认证服务,只要集成Sa-Token并实现登录接口即可,非常简单。

  • 首先在pom.xml中添加相关依赖,包括Sa-Token的SpringBoot依赖、整合Redis实现分布式Session的依赖以及我们的micro-sa-token-common依赖;
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
xml复制代码<dependencies>
<!-- Sa-Token 权限认证 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.24.0</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用jackson序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.24.0</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- micro-sa-token通用依赖 -->
<dependency>
<groupId>com.macro.cloud</groupId>
<artifactId>micro-sa-token-common</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
  • 接下来修改配置文件application.yml,照抄之前网关的配置即可;
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
yaml复制代码spring:
redis:
database: 0
port: 6379
host: localhost
password:

# Sa-Token配置
sa-token:
# token名称 (同时也是cookie名称)
token-name: Authorization
# token有效期,单位秒,-1代表永不过期
timeout: 2592000
# token临时有效期 (指定时间内无操作就视为token过期),单位秒
activity-timeout: -1
# 是否允许同一账号并发登录 (为false时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个token (为false时每次登录新建一个token)
is-share: false
# token风格
token-style: uuid
# 是否输出操作日志
is-log: false
# 是否从cookie中读取token
is-read-cookie: false
# 是否从head中读取token
is-read-head: true
  • 在UserController中定义好登录接口,登录成功后返回Token,具体实现在UserServiceImpl类中;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码/**
* 自定义Oauth2获取令牌接口
* Created by macro on 2020/7/17.
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserServiceImpl userService;

@RequestMapping(value = "/login", method = RequestMethod.POST)
public CommonResult login(@RequestParam String username, @RequestParam String password) {
SaTokenInfo saTokenInfo = userService.login(username, password);
if (saTokenInfo == null) {
return CommonResult.validateFailed("用户名或密码错误");
}
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", saTokenInfo.getTokenValue());
tokenMap.put("tokenHead", saTokenInfo.getTokenName());
return CommonResult.success(tokenMap);
}
}
  • 在UserServiceImpl中添加登录的具体逻辑,首先验证密码,密码校验成功后,通知下Sa-Token登录的用户ID,然后把用户信息直接存储到Session中去;
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复制代码/**
* 用户管理业务类
* Created by macro on 2020/6/19.
*/
@Service
public class UserServiceImpl{

private List<UserDTO> userList;

public SaTokenInfo login(String username, String password) {
SaTokenInfo saTokenInfo = null;
UserDTO userDTO = loadUserByUsername(username);
if (userDTO == null) {
return null;
}
if (!SaSecureUtil.md5(password).equals(userDTO.getPassword())) {
return null;
}
// 密码校验成功后登录,一行代码实现登录
StpUtil.login(userDTO.getId());
// 将用户信息存储到Session中
StpUtil.getSession().set("userInfo",userDTO);
// 获取当前登录用户Token信息
saTokenInfo = StpUtil.getTokenInfo();
return saTokenInfo;
}
}
  • 这里有一点需要提醒下,Sa-Token的Session并不是我们平时理解的HttpSession,而是它自己实现的类似Session的机制。

micro-sa-token-api

接下来我们来搭建一个受保护的API服务,实现获取登录用户信息的接口和需要特殊权限才能访问的测试接口。

  • 首先在pom.xml中添加相关依赖,和上面的micro-sa-token-auth一样;
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
xml复制代码<dependencies>
<!-- Sa-Token 权限认证 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.24.0</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用jackson序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.24.0</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- micro-sa-token通用依赖 -->
<dependency>
<groupId>com.macro.cloud</groupId>
<artifactId>micro-sa-token-common</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
  • 接下来修改配置文件application.yml,照抄之前网关的配置即可;
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
yaml复制代码spring:
redis:
database: 0
port: 6379
host: localhost
password:

# Sa-Token配置
sa-token:
# token名称 (同时也是cookie名称)
token-name: Authorization
# token有效期,单位秒,-1代表永不过期
timeout: 2592000
# token临时有效期 (指定时间内无操作就视为token过期),单位秒
activity-timeout: -1
# 是否允许同一账号并发登录 (为false时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个token (为false时每次登录新建一个token)
is-share: false
# token风格
token-style: uuid
# 是否输出操作日志
is-log: false
# 是否从cookie中读取token
is-read-cookie: false
# 是否从head中读取token
is-read-head: true
  • 添加获取用户信息的接口,由于使用了Redis实现分布式Session,直接从Session中获取即可,是不是非常简单!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码/**
* 获取登录用户信息接口
* Created by macro on 2020/6/19.
*/
@RestController
@RequestMapping("/user")
public class UserController{

@GetMapping("/info")
public CommonResult<UserDTO> userInfo() {
UserDTO userDTO = (UserDTO) StpUtil.getSession().get("userInfo");
return CommonResult.success(userDTO);
}

}
  • 添加需要api:test:hello权限访问的测试接口,预置的admin用户拥有该权限,而macro用户是没有的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码/**
* 测试接口
* Created by macro on 2020/6/19.
*/
@RestController
@RequestMapping("/test")
public class TestController {

@GetMapping("/hello")
public CommonResult hello() {
return CommonResult.success("Hello World.");
}

}

功能演示

三个服务搭建完成后,我们用Postman来演示下微服务的认证授权功能。

  • 首先启动Nacos和Redis服务,然后再启动micro-sa-token-gateway、micro-sa-token-auth和micro-sa-token-api服务,启动顺序无所谓;

  • 直接通过网关访问登录接口获取Token,访问地址:http://localhost:9201/auth/user/login

  • 通过网关访问API服务,不带Token调用获取用户信息的接口,无法正常访问,访问地址:http://localhost:9201/api/user/info

  • 通过网关访问API服务,带Token调用获取用户信息的接口,可以正常访问;

  • 通过网关访问API服务,使用macro用户访问需api:test:hello权限的测试接口,无法正常访问,访问地址:http://localhost:9201/api/test/hello

  • 登录切换为admin用户,该用户具有api:test:hello权限;

  • 通过网关访问API服务,使用admin用户访问测试接口,可以正常访问。

总结

对比之前使用Spring Security的微服务权限解决方案,Sa-Token的解决方案更简单、更优雅。使用Security我们需要定义鉴权管理器、分别处理未认证和未授权的情况、还要自己定义认证和资源服务器配置,使用非常繁琐。而使用Sa-Token,只要在网关上配置过滤器实现认证和授权,然后调用API实现登录及权限分配即可。具体区别可以参考下图。

参考资料

官方文档:sa-token.dev33.cn/

项目源码地址

github.com/macrozheng/…

本文转载自: 掘金

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

1…540541542…956

开发者博客

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