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

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


  • 首页

  • 归档

  • 搜索

Node服务器端JavaScript REPL 模式 单线程

发表于 2021-11-18

核心的JavaScript语法定义了最小限度的API,可以操作数值、文本数组等,但不包含输入输出功能。输出入是内嵌JavaScript的宿主环境的责任 。

宿主环境:浏览器、node

  • 前端的JavaScript是由ECMAScript、DOM、BOM组合而成,Node.js是由ECMAScript、OS、File、Net、DB组成
  • Node.js是使 JavaScript 运行在浏览器之外,即服务端的开发平台, 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,也是一个库
  • 可以在node.green/上获取到Node各个版本对ES语法的支持

Node,JavaScript与底层操作系统绑定的结合,与限制JavaScript只能使用浏览器提供的API不同,Node给予JavaScript可以访问整个操作系统的权限,允许JavaScript读写文件、通过网络发送和接收数据。应用:

  • 替代命令行脚本、交互式终端程序
  • 运行受信程序的编程语言,没有浏览器运行不受信代码带来的安全限制
  • 编写高效、高并发web服务器的流行环境:高并发、实时聊天、实时消息推送、基于社交网络的大规模 Web 应用
  • 客户端逻辑强大的SPA
  • Restful API,可以处理数万条链接
  • 实时websocket应用
  • 前端工具链,e.g:单元测试工具、客户端 JavaScript 编译器
  • 桌面开发、带有图形用户界面的本地应用程序
  • TCP/UDP 套接字应用程序
  • Node.js 还可以部署到非网络应用的环境下,比如一个命令行工具。Node.js 还可以调用C/C++ 的代码,这样可以充分利用已有的诸多函数库,也可以将对性能要求非常高的部分用C/C++ 来实现。

Node.js的结构图
在这里插入图片描述
libuv是提供异步功能的C库,在运行时负责一个事件循环,一个线程池、文件系统I/O、DNS相关的IO和网络IO。

  • node.js启动后,会开启一个JS主线程和libuv提供的线程池和Event Loop。当发现有IO操作就交给线程池并注册回调函数。
  • node.js的全局对象有global、process、console、module、exports
  • process :node进程相关的信息,比如运行node程序时的命令行参数。或者设置进程相关信息,比如设置环境变量。

REPL 模式

  • REPL【Read-eval-print loop输入—求值—输出循环】
  • 和python一样,Node.js 也有这样的功能,运行无参数的 node 将会启动一个 JavaScript 的交互式 shell
  • 打开命令提示符,然后输入 node,进入 REPL 模式以后,会出现一个“>”提示符提示你输入命令,输入后按回车,Node.js 将会解析并执行命令。
  • 执行了一个函数,REPL 会在下一行显示这个函数的返回值。
  • 连续按两次 Ctrl + C 退出Node.js 的 REPL 模式。
  • 如果你输入了一个错误的指令,REPL 则会立即显示错误并输出调用栈。

process.argv命令行参数

  • 第一、第二个的元素是node可执行文件(环境)和被执行JavaScript文件路径
  • 提供给node可执行文件且由它解释的命令行参数会被node可执行文件使用,不会出现在process.argv中

node也会从unix风格的环境变量中获取输入,node把这些环境变量保存在process.env对象中使用

1
2
3
js复制代码SHELL: '/bin/bash',
USER: 'joy',
HOME: '/Users/joy'

单线程、非阻塞的事件编程模式

  • 传统的架构是多线程模型,为每个业务逻辑提供一个系统线程,通过系统线程切换来弥补同步式【即时回复】 I/O 调用时的时间开销
  • Node.js 使用的是单线程模型
  • V8引擎提供的异步执行回调接口可以处理大量的高并发,IO密集型处理是强项
  • 对于所有 I/O 都采用异步请求,避免了频繁的上下文切换
  • node创建于JavaScript支持promise之前,其异步主要依赖回调函数实现
  • **事件式编程:在执行的过程中会维护一个事件队列,程序在执行时进入事件循环等待下一个事件到来,每个异步式 I/O 请求完成后会被推送到事件队列,等待程序进程进行处理。
  • 同步的阻塞式IO在高并发环境将是一个很大的性能问题,所以同步一般只在基础框架启动时使用,用来加载配置文件、初始化程序等。
  • 其他同步的应用需求:和操作系统的shell命令交互,调用可执行文件等
  • Node与进程相关的模块process、child_process、cluster
  • 大多数API都有同步/异步两个版本

注意:Node.js在底层访问I/O其实还是多线程,可以翻看fs模块源码,里面用libuv来处理I/O

异步错误不能被同步线程捕获,有可能会变成致命错误,终止程序。注册全局处理程序

1
2
3
js复制代码process.on("unhandledRejection", (reason, promise) => {
//
});

多线程

在支持HTML5的浏览器里,我们可以使用web worker来处理一些耗时运算。
对应Node.js也提供了cluster、child_process模块的web worker来解决。

模块

  • node在JavaScript支持模块系统之前就诞生,所以它拥有自己的模块系统——module.exports 和 require
  • 模块是 Node.js 应用程序的基本组成部分,文件即模块。文件可能是 JavaScript 代码、JSON 或者编译过的 C/C++ 扩展。
  • Node 13增加了对ES6模块import、export的支持,同时支持自己的模块系统。
  • mjs扩展名文件-ES6模块
  • cjs扩展名文件-CommonJS模块格式
  • 没有明确扩展名的,会向上查找package.json文件,检查JSON对象的顶级type属性的值commonjs、module
  • 运行node并不需要有package.json,如果没有找到默认CommonJS模块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
javascript复制代码//module.js 
var name;
exports.setName = function(thyName) {
name = thyName;
};
exports.sayHello = function() {
console.log('Hello ' + name);
};

//getmodule.js
var myModule1 = require('./module');
myModule1.setName('1');
var myModule = require('./module');
myModule.setName('BYVoid');
myModule1.sayHello();

运行node getmodule.js,结果是:
Hello BYVoid

require 不会重复加载模块,也就是说无论调用多少次获得的模块都是最后一次。

封装一个对象到模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
javascript复制代码//hello.js 
function Hello() {
var name;

this.setName = function(thyName) {
name = thyName;
};

this.sayHello = function() {
console.log('Hello ' + name);
};
};
module.exports = Hello;//如果是exports.Hello = Hello;

//gethello.js
var Hello = require('./hello'); //那么是require('./hello').Hello
hello = new Hello();
hello.setName('BYVoid');
hello.sayHello();

http模块

http 是 Node.js 的一个核心模块,其内部是用 C++ 实现的,外部用 JavaScript 封装。
node内建了 HTTP 服务器支持,这和 PHP、Perl 不一样,因为在使用 PHP 的时候,必须先搭建一个 Apache 之类的HTTP 服务器,然后通过 HTTP 服务器的模块加载或 CGI 调用,才能将 PHP 脚本的执行结果呈现给用户。
而使用 Node.js 时,无需额外搭建一个 HTTP 服务器。直接可以用来调试代码,并且它本身就可以部署到产品环境,其性能足以满足要求。

1
2
3
4
5
6
7
8
9
10
js复制代码function serve(rootDirectory, port) {
let server = new http.Server();
server.listen(port);
console.log('listening on port', port);

server.on('request', (request,response) => {
let temp = url.parse(request.url).pathname;
//查找文件,返回状态码及文件
})
}

包

包package

NPM

安装node同时会得到npm包管理工具

通常一个包的配置如下

  1. GZIP压缩文件
  2. 解析GZIP的URL
  3. 为注册表添加< name > @< version >的URL信息
  4. package.json配置文件(在包目录中运行npm init 会根据交互式问答产生一个符合标准的package.json)
  • package.json文件中使用script定义脚本命令(脚本文件一般位于node_modules/.bin子目录里)
  • Node.js 在调用某个包时,会首先检查包中 package.json 文件的 main 字段,将其作为包的接口模块,如果 package.json 或 main 字段不存在,会尝试寻找 index.js 或 index.node 作为包的接口。
1
2
3
4
5
6
7
8
9
10
css复制代码{
//...
"script":{
"build":"node index.js"
}
//...
"main" : "./lib/interface.js"
}

//npm run build等同于node index.js
  • 二进制文件应该在 bin 目录下;
  • JavaScript 代码应该在 lib 目录下;
  • 文档应该在 doc 目录下;
  • 单元测试应该在 test 目录下

工具模块

  • JavaScript 的单元测试可使用一个 Node.js 的 模 块,Karma 执行
  • JavaScript 没有编译器,不能在早期验证代码的合法性。有效的单元测试常常扮演一个伪编译器的角色,立刻反馈代码的质量,并且一有缺陷,就能检测到。
  • JavaScript 中有很多自动化文档生成工具。JSDoc有着和Javadoc 类似的标记和输出,Dox是一个生成文档的Node.js 模块。
    Docco将代码和注释组织成一种类似文章的格式。虽然Docco 不直接验证和执行代码规范,使用它却能鼓励大家使用良好的代码结构和注释

package.json

完全符合规范的 package.json 文件应该含有以下字段。

  • name:包的名称,必须是唯一的,由小写英文字母、数字和下划线组成,不能包含空格。
  • description:包的简要说明。
  • version:符合语义化版本识别规范的版本字符串。
  • keywords:关键字数组,通常用于搜索。
  • maintainers:维护者数组,每个元素要包含 name、email(可选)、web(可选)
    字段
  • contributors:贡献者数组,格式与maintainers相同。包的作者应该是贡献者
    数组的第一个元素。
  • bugs:提交bug的地址,可以是网址或者电子邮件地址。
  • licenses:许可证数组,每个元素要包含 type (许可证的名称)和 url (链接到
    许可证文本的地址)字段。
  • repositories:仓库托管地址数组,每个元素要包含 type (仓库的类型,如 git )、
    url (仓库的地址)和 path (相对于仓库的路径,可选)字段。
  • dependencies:包的依赖,一个关联数组,由包名称和版本号组成。
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
javascript复制代码{ 
"name": "mypackage",
"description": "Sample package for CommonJS. This package demonstrates the required
elements of a CommonJS package.",
"version": "0.7.0",
"keywords": [
"package",
"example"
],
"maintainers": [
{
"name": "Bill Smith",
"email": "bills@example.com",
}
],
"contributors": [
{
"name": "BYVoid",
"web": "http://www.byvoid.com/"
}
],
"bugs": {
"mail": "dev@example.com",
"web": "http://www.example.com/bugs"
},
"licenses": [
{
"type": "GPLv2",
"url": "http://www.example.org/licenses/gpl.html"
}
],
"repositories": [
{
"type": "git",
"url": "http://github.com/BYVoid/mypackage.git"
}
],
"dependencies": {
"webkit": "1.2",
"ssl": {
"gnutls": ["1.0", "2.0"],
"openssl": "0.9.8"
}
}
}

使用npm安装包

在使用 npm 安装包的时候,有两种模式

  1. 本地模式:把目标包作为工程的一部分
    使用 npm 安装包的命令格式为:
1
js复制代码npm [install/i] [package_name]
  • 安装成功放置在当前目录的 node_modules 子目录下,require 在加载模块时会尝试搜寻 node_modules 子目录
  • npm 本地模式仅仅是把包安装到 node_modules 子目录下,其中的 bin 目录下可执行文件没有包含在 PATH 环境变量中,不能直接在命令行中调用。
  1. 全局模式:全局使用,多个工程,支持在命令行下使用包
1
js复制代码npm [install/i] -g [package_name]
  • 使用全局模式安装时,npm 会将包安装到系统目录,譬如/usr/local/lib/node_modules/,同时 package.json 文件中 bin 字段包含的文件会被链接到 /usr/local/bin/。
  • /usr/local/bin/ 是在PATH 环境变量中默认定义的,因此就可以直接在命令行中运行 xxx.js命令了。
  • 注:使用全局模式安装的包并不能直接在 JavaScript 文件中用 require 获得,因为 require 不会搜索 /usr/local/lib/node_modules/

相比npm、yarn有了很多改进:yarn new package/

  • 安装速度快(cache和依赖解析做得好)
  • 模块安装保证幂等性
  • 锁定各依赖模块的版本(npm 使用的semver默认是指定了一个range)
  • 兼容性好(兼容旧有npm的工作流)

npm link创建符号链接

npm 提供了一个命令 npm link,在本地包和全局包之间创建符号链接,解决-使用全局模式安装的包不能直接通过 require 使用、局部包不支持命令行

npm link 命令不支持 Windows。

1
2
3
4
js复制代码npm install -g express
npm link express
// 显示
./node_modules/express -> /usr/local/lib/node_modules/express

在 node_modules 子目录中发现一个指向安装到全局的包的符号链接。通过这种方法,就可以把全局包当本地包来使用了。

将本地的包链接到全局:

1
js复制代码在包目录( package.json 所在目录)中运行 npm link 命令。

开发包时,利用这种方法可以非常方便地在不同的工程间进行测试。

发布包

在包目录下运行

1
2
3
4
js复制代码npm init :生成一个符合 npm 规范的 package.json 文件
npm adduser :获得一个账号用于今后维护自己的包
npm whoami :测验是否已经取得了账号
npm publish:发布

访问 search.npmjs.org/ 就可以找到刚刚发布的包了。可以在世界的任意一台计算机上使用 npm install byvoidmodule 命令来安装它
更新:

1
2
js复制代码在 package.json 文件中修改 version 字段,然后npm publish
npm unpublish 命令来取消发布。

Runtime和vm

Runtime :解释型语言的数据类型的确定由编译推迟至运行时,所以需要一个运行时系统来处理编译后的代码。

JavaScript引擎负责解析和JIT编译,例如编译成机器码。Runtime提供内建的库(例如常用数据类型、Window对象、DOM API),可以在运行时使用。

  • vm:通常认为是硬件和二进制文件之间的中间层
  • c++编译好的二进制文件交给OS直接调用
  • Java编译好的二进制文件交给Java虚拟机运行,对开发者屏蔽了不同操作系统的差异
  • Node的一个核心模块vm提供了一系列API用于在V8虚拟机环境中编译和运行代码。

node.js中的stream流

基于EventEmitter的数据管理模式,由各种不同的抽象接口组成,主要包括可写、可读、可读写、可转换等类型

  • 流是非阻塞数据处理模式,可以提升效率、节省内存、有助于处理管道且可扩展等。
  • 流的应用:文件读写、网络请求、数据转换、音频、视频等方面有很广泛的应用
  • 监听error事件,可以捕获流的错误事件

node支持四种流

  1. 可读流
  2. 可写流
  3. 双工流
  4. 转换流

流常与管道共用

有时候,我们需要把可读流中获取的数据写入可写流。例如,写一个HTTP服务器提供对静态文件目录的访问。需要将文件流写入网络套接字。与其自己写代码来处理读和写,不如把这两个接口连接为一个“管道”,将可写流简单传递给可读流的pipe()方法,即可让node帮我们实现复杂操作

1
2
3
4
5
js复制代码const fs = require('fs');

function pipeFileToSocket(file, socket){
fs.createReadStream(file).pipe(socket);
}

使用

1
2
3
4
5
6
7
8
javascript复制代码//app.js 
var http = require('http');
http.createServer(function(req, res) {
res.writeHead(200, {'Content-Type': 'text/html'});
res.write('<h1>Node.js</h1>');
res.end('<p>Hello World</p>');
}).listen(3000);
console.log("HTTP server is listening at port 3000.");

运行 node app.js命令,打开浏览器访问 http://127.0.0.1:3000
这个程序调用了 Node.js 提供的http 模块,对所有 HTTP 请求答复同样的内容并监听 3000 端口。在终端中运行这个脚本时,我们会发现它并不像 Hello World 一样结束后立即退出,而是一直等待,直到按下 Ctrl + C 才会结束。这是因为 listen 函数中创建了事件监听器,使得 Node.js 进程不会退出事件循环。

因为 Node.js 只有在第一次引用到某部份时才会去解析脚本文件,以后都会直接访问内存,避免重复载入,开发 Node.js 实现的 HTTP 应用时会发现,无论修改了代码的哪一部份,都必须终止Node.js 再重新运行才会奏效。

1
2
3
4
5
js复制代码supervisor 可以监视你对代码的改动,并自动重启 Node.js。【相当于持续重启,有选择的使用】
首先使用 npm 安装 supervisor:
$ npm install -g supervisor
接下来,使用 supervisor 命令启动 app.js:
$ supervisor app.js

调试

node debug debug.js:打开了一个 Node.js 的调试终端
node-inspector

控制流很大程度上要靠事件和回调函数来组织,一个逻辑要拆分为若干个单元。

Mocha

一个基于Node.js和浏览器的集合各种特性的JavaScript测试框架

本文转载自: 掘金

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

这个无敌设计,可以解析并运算任意数学表达式

发表于 2021-11-18

本文节选自《设计模式就该这样学》

1 使用解释器模式解析数学表达式

下面用解释器模式来实现一个数学表达式计算器,包含加、减、乘、除运算。
首先定义抽象表达式角色IArithmeticInterpreter接口。

1
2
3
4
java复制代码
public interface IArithmeticInterpreter {
int interpret();
}

创建终结表达式角色Interpreter抽象类。

1
2
3
4
5
6
7
8
9
10
11
java复制代码
public abstract class Interpreter implements IArithmeticInterpreter {

protected IArithmeticInterpreter left;
protected IArithmeticInterpreter right;

public Interpreter(IArithmeticInterpreter left, IArithmeticInterpreter right) {
this.left = left;
this.right = right;
}
}

然后分别创建非终结符表达式角色加、减、乘、除解释器,加法运算表达式AddInterpreter类的代码如下。

1
2
3
4
5
6
7
8
9
10
11
java复制代码
public class AddInterpreter extends Interpreter {

public AddInterpreter(IArithmeticInterpreter left, IArithmeticInterpreter right) {
super(left, right);
}

public int interpret() {
return this.left.interpret() + this.right.interpret();
}
}

减法运算表达式SubInterpreter类的代码如下。

1
2
3
4
5
6
7
8
9
10
java复制代码
public class SubInterpreter extends Interpreter {
public SubInterpreter(IArithmeticInterpreter left, IArithmeticInterpreter right) {
super(left, right);
}

public int interpret() {
return this.left.interpret() - this.right.interpret();
}
}

乘法运算表达式MultiInterpreter类的代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码
public class MultiInterpreter extends Interpreter {

public MultiInterpreter(IArithmeticInterpreter left, IArithmeticInterpreter right){
super(left,right);
}

public int interpret() {
return this.left.interpret() * this.right.interpret();
}

}

除法运算表达式DivInterpreter类的代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码
public class DivInterpreter extends Interpreter {

public DivInterpreter(IArithmeticInterpreter left, IArithmeticInterpreter right){
super(left,right);
}

public int interpret() {
return this.left.interpret() / this.right.interpret();
}

}

数字表达式NumInterpreter类的代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码
public class NumInterpreter implements IArithmeticInterpreter {
private int value;

public NumInterpreter(int value) {
this.value = value;
}


public int interpret() {
return this.value;
}
}

接着创建计算器GPCalculator类。

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
java复制代码
public class GPCalculator {
private Stack<IArithmeticInterpreter> stack = new Stack<IArithmeticInterpreter>();

public GPCalculator(String expression) {
this.parse(expression);
}

private void parse(String expression) {
String [] elements = expression.split(" ");
IArithmeticInterpreter left,right;

for (int i = 0; i < elements.length ; i++) {
String operator = elements[i];
if(OperatorUtil.ifOperator(operator)){
left = this.stack.pop();
right = new NumInterpreter(Integer.valueOf(elements[++i]));
System.out.println("出栈" + left.interpret() + "和" + right.interpret());
this.stack.push(OperatorUtil.getInterpreter(left,right,operator));
System.out.println("应用运算符:" + operator);
}else {
NumInterpreter numInterpreter = new NumInterpreter(Integer.valueOf(elements[i]));
this.stack.push(numInterpreter);
System.out.println("入栈:" + numInterpreter.interpret());
}

}
}

public int calculate() {
return this.stack.pop().interpret();
}
}

工具类OperatorUtil的代码如下。

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

public static boolean isOperator(String symbol) {
return (symbol.equals("+") || symbol.equals("-") || symbol.equals("*"));
}

public static Interpreter getInterpreter(IArithmeticInterpreter left, IArithmeticInterpreter
right, String symbol) {
if (symbol.equals("+")) {
return new AddInterpreter(left, right);
} else if (symbol.equals("-")) {
return new SubInterpreter(left, right);
} else if (symbol.equals("*")) {
return new MultiInterpreter(left, right);
} else if (symbol.equals("/")) {
return new DivInterpreter(left, right);
}
return null;
}
}

最后编写客户端测试代码。

1
2
3
4
5
6
java复制代码
public static void main(String[] args) {
System.out.println("result: " + new GPCalculator("10 + 30").calculate());
System.out.println("result: " + new GPCalculator("10 + 30 - 20").calculate());
System.out.println("result: " + new GPCalculator("100 * 2 + 400 * 1 + 66").calculate());
}

运行结果如下图所示。

file

当然,上面的简易计算器还没有考虑优先级,就是从左至右依次运算的。在实际运算中,乘法和除法属于一级运算,加法和减法属于二级运算。一级运算需要优先计算。另外,我们可以通过使用括号手动调整运算的优先级。我们再优化一下代码,首先新建一个枚举类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码
public enum OperatorEnum {
LEFT_BRACKET("("),
RIGHT_BRACKET(")"),
SUB("-"),
ADD("+"),
MULTI("*"),
DIV("/"),
;
private String operator;

public String getOperator() {
return operator;
}

OperatorEnum(String operator) {
this.operator = operator;
}
}

然后修改OperatorUtil的处理逻辑,设置两个栈。

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

public static Interpreter getInterpreter(Stack<IArithmeticInterpreter> numStack, Stack<String> operatorStack) {
IArithmeticInterpreter right = numStack.pop();
IArithmeticInterpreter left = numStack.pop();
String symbol = operatorStack.pop();
System.out.println("数字出栈:" + right.interpret() + "," + left.interpret() + ",操作符出栈:" + symbol);
if (symbol.equals("+")) {
return new AddInterpreter(left, right);
} else if (symbol.equals("-")) {
return new SubInterpreter(left, right);
} else if (symbol.equals("*")) {
return new MultiInterpreter(left, right);
} else if (symbol.equals("/")) {
return new DivInterpreter(left, right);
}
return null;
}
}

修改GPCalculator的代码。

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

//数字stack
private Stack<IArithmeticInterpreter> numStack = new Stack<IArithmeticInterpreter>();
//操作符stack
private Stack<String> operatorStack = new Stack<String>();
/**
* 解析表达式
* @param expression
*/
public GPCalculator(String expression) {
this.parse(expression);
}

private void parse(String input) {
//对表达式去除空字符操作
String expression = this.fromat(input);
System.out.println("标准表达式:" + expression);
for (String s : expression.split(" ")) {
if (s.length() == 0){
//如果是空格,则继续循环,什么也不操作
continue;
}
//如果是加减,因为加减的优先级最低,所以这里只要遇到加减号,无论操作符栈中是什么运算符都要运算
else if (s.equals(OperatorEnum.ADD.getOperator())
|| s.equals(OperatorEnum.SUB.getOperator())) {
//当栈不是空的,并且栈中最上面的一个元素是加减乘除的任意一个
while (!operatorStack.isEmpty()
&&(operatorStack.peek().equals(OperatorEnum.SUB.getOperator())
|| operatorStack.peek().equals(OperatorEnum.ADD.getOperator())
|| operatorStack.peek().equals(OperatorEnum.MULTI.getOperator())
|| operatorStack.peek().equals(OperatorEnum.DIV.getOperator()))) {
//结果存入栈中
numStack.push(OperatorUtil.getInterpreter(numStack,operatorStack));
}
//运算完后将当前的运算符入栈
System.out.println("操作符入栈:"+s);
operatorStack.push(s);
}
//当前运算符是乘除的时候,因为优先级高于加减
//所以要判断最上面的是否是乘除,如果是乘除,则运算,否则直接入栈
else if (s.equals(OperatorEnum.MULTI.getOperator())
|| s.equals(OperatorEnum.DIV.getOperator())) {
while (!operatorStack.isEmpty()&&(
operatorStack.peek().equals(OperatorEnum.MULTI.getOperator())
|| operatorStack.peek().equals(OperatorEnum.DIV.getOperator()))) {
numStack.push(OperatorUtil.getInterpreter(numStack,operatorStack));
}
//将当前操作符入栈
System.out.println("操作符入栈:"+s);
operatorStack.push(s);
}
//如果是左括号,则直接入栈,什么也不用操作,trim()函数是用来去除空格的,由于上面的分割 操作,可能会令操作符带有空格
else if (s.equals(OperatorEnum.LEFT_BRACKET.getOperator())) {
System.out.println("操作符入栈:"+s);
operatorStack.push(OperatorEnum.LEFT_BRACKET.getOperator());
}
//如果是右括号,则清除栈中的运算符直至左括号
else if (s.equals(OperatorEnum.RIGHT_BRACKET.getOperator())) {
while (!OperatorEnum.LEFT_BRACKET.getOperator().equals(operatorStack.peek())) {
//开始运算
numStack.push(OperatorUtil.getInterpreter(numStack,operatorStack));
}
//运算完之后清除左括号
String pop = operatorStack.pop();
System.out.println("括号运算操作完成,清除栈中右括号:"+pop);
}
//如果是数字,则直接入数据的栈
else {
//将数字字符串转换成数字,然后存入栈中
NumInterpreter numInterpreter = new NumInterpreter(Integer.valueOf(s));
System.out.println("数字入栈:"+s);
numStack.push(numInterpreter);
}
}
//最后当栈中不是空的时候继续运算,直到栈为空即可
while (!operatorStack.isEmpty()) {
numStack.push(OperatorUtil.getInterpreter(numStack,operatorStack));
}
}

/**
* 计算结果出栈
* @return
*/
public int calculate() {
return this.numStack.pop().interpret();
}

/**
* 换成标准形式,便于分割
* @param expression
* @return
*/
private String fromat(String expression) {
String result = "";
for (int i = 0; i < expression.length(); i++) {
if (expression.charAt(i) == '(' || expression.charAt(i) == ')' ||
expression.charAt(i) == '+' || expression.charAt(i) == '-' ||
expression.charAt(i) == '*' || expression.charAt(i) == '/')
//在操作符与数字之间增加一个空格
result += (" " + expression.charAt(i) + " ");
else
result += expression.charAt(i);
}
return result;
}
}

此时,再来看客户端测试代码。

1
2
3
4
java复制代码
public static void main(String[] args) {
System.out.println("result: " + new GPCalculator("10+30/((6-4)*2-2)").calculate());
}

运行得到预期的结果,如下图所示。

file

2 解释器模式在JDK源码中的应用

先来看JDK源码中的Pattern对正则表达式的编译和解析。

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
java复制代码
public final class Pattern implements java.io.Serializable {
...
private Pattern(String p, int f) {
pattern = p;
flags = f;

if ((flags & UNICODE_CHARACTER_CLASS) != 0)
flags |= UNICODE_CASE;


capturingGroupCount = 1;
localCount = 0;

if (pattern.length() > 0) {
compile();
} else {
root = new Start(lastAccept);
matchRoot = lastAccept;
}
}
...
public static Pattern compile(String regex) {
return new Pattern(regex, 0);
}
public static Pattern compile(String regex, int flags) {
return new Pattern(regex, flags);
}

...
}

3 解释器模式在Spring源码中的应用

再来看Spring中的ExpressionParser接口。

1
2
3
4
5
6
7
8
java复制代码
public interface ExpressionParser {

Expression parseExpression(String expressionString) throws ParseException;

Expression parseExpression(String expressionString, ParserContext context) throws ParseException;

}

这里我们不深入讲解源码,通过我们前面编写的案例大致能够清楚其原理。不妨编写一段客户端代码验证一下。客户端测试代码如下。

1
2
3
4
5
6
7
java复制代码
public static void main(String[] args) {
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression("100 * 2 + 400 * 1 + 66");
int result = (Integer) expression.getValue();
System.out.println("计算结果是:" + result);
}

运行结果如下图所示。

file

由上图可知,运行结果与预期的结果是一致的。

关注『 Tom弹架构 』回复“设计模式”可获取完整源码。

【推荐】Tom弹架构:30个设计模式真实案例(附源码),挑战年薪60W不是梦

本文为“Tom弹架构”原创,转载请注明出处。技术在于分享,我分享我快乐!如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力。关注『 Tom弹架构 』可获取更多技术干货!

本文转载自: 掘金

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

MyBatis的从零到壹 什么是MyBits?

发表于 2021-11-18

什么是MyBits?

1
2
3
4
5
6
7
sql复制代码提到mybits,我们就得想到之前学过的JDBC,还记得我们用jdbc操作数据库时需要哪些步骤吗
1.注册驱动
2.获取连接
3.获取操作对象PrepardStatement
4.执行sql
5.处理结果
6.关闭资源

image.png
代码在上面了,不记得的可以看一下

1
2
3
4
5
6
7
sql复制代码通过上面的jdbc我们会发现还是有很多问题存在的
1.数据库频繁的连接创建,释放资源会照成系统的资源浪费,从而影响系统的性能,如果使用数据库连接池就可以解决这个问题
2.sql语句中硬编码,照成代码不易维护,实际应用sql变化的可能性非常大,那么我们就需要更改java代码
3.使用peepardStatement向占位符传参也存在硬编码,以为后面sql语句的条件不一定,修改sql语句也是需要修改代码的,不利于维护‘
4.对结果集解析存在硬编码(查询列名),如果列名改变也会导致代码变化,系统非常的不利于维护,如果能够将数据库记录封装成pojo对象解析会比较方便

综上所述:硬编码问题严重,步骤繁琐,sql语句不易维护

Mybatis的介绍

1
2
sql复制代码mybits是一个优秀的基于java的持久层框架,它内部封装了jdbc,能够让开发者只关注sql语句本身,而不需要花费精力去处理加载驱动,获取连接这些烦琐的操作
mybits通过xml或者注解的方式将statement配置起来,并通过java对象和statement中的动态sql生成映射生成最终的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。采用了ORM思想解决了实体和数据库映射的问题,对jdbc底层进行了封装,屏蔽了jdbc api的访问细节,使我们不用与和jdbc打交道,就可以完成数据库的持久化操作

Mybatis快速入门

1
复制代码1.在pom文件里使用下面的依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
js复制代码    <dependencies>
<!--单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!--MyBatis坐标-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--lombok 依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.18</version>
</dependency>
</dependencies>

创建数据库

1
2
3
4
5
6
7
8
9
10
11
12
CREATE复制代码USE mybatis_day01;
CREATE TABLE t_user(
uid int PRIMARY KEY auto_increment,
username varchar(40),
sex varchar(10),
birthday date,
address varchar(40)
);

INSERT INTO `t_user` VALUES (null, 'zs', '男', '2018-08-08', '北京');
INSERT INTO `t_user` VALUES (null, 'ls', '女', '2018-08-30', '武汉');
INSERT INTO `t_user` VALUES (null, 'ww', '男', '2018-08-08', '北京');

创建实体类

1
2
3
4
5
6
7
8
9
@AllArgsConstructor复制代码@NoArgsConstructor
@Data
public class User implements Serializable {
private Integer uid;
private String username;
private String sex;
private Date birthday;
private String address;
}

创建UserDao接口

1
2
3
4
5
6
public复制代码
/**
* 查询所有的用户
*/
List<User> findAll();
}
3.2.4创建 UserDao.xml 映射文件

image.png

在创建mybatis-config.xml核心配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xml复制代码<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>

主要介绍2点,上面configuration中的配置是数据库的配置,具体的可以去mybits官网上查看
mappers标签下主要是引入上面配的UserDao.xml文件
注意:dao接口的目录一定要和映射文件的目录和名字相同

1
2

接下来我们使用测试类在测试一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ini复制代码@Test
public void test01() throws IOException {
//首先得到MyBatis核心文件的文件流
InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
//加载配置文件,获得SqlSessionFactory对象(使用了建造者模式)
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
//获取连接对象(使用了工厂模式)
SqlSession sqlSession = sqlSessionFactory.openSession();
//获取dao接口的代理对象,(使用了代理模式)
UserDao userDao = sqlSession.getMapper(UserDao.class);
List<User> l = userDao.findAll();
for (User user : l) {
System.out.println(user);
}
sqlSession.close();
resourceAsStream.close();
}

详解核心配置文件(mybatis-config)

1.当我们需要使用数据库的配置文件时,使用properties标签,从而引入进行使用

image.png

然后在数据库的配置中使用${key}来获取配置文件中的value,例如;

image.png

2.起别名:typeAliases
我们每次写全限定类名都比较麻烦,可以给起个别名方便我们引用

image.png

image.png

这样就可以直接引用了

3.引入配置文件:Mapper
可以直接引入映文件路径,也可以通过扫描接口的方式

image.png

详解配置文件UserDao.xml

image.png
在上述代码中,我们可以发现有2个参数可能大家还很陌生

resultType介绍:
1
2
3
4
5
6
7
arduino复制代码    resultType从字面上看是结果类型,没错,就相当于是返回值类型
那么我们要怎么写呢?分以下几种情况
1.如果是返回简单的类型,如果是基本数据类型,直接写名字就行,int|long
如果返回的是String类型,还要写它String的全路径名
2.如果返回的是pojo对象,则直接返回对象的全类名
3.返回pojo列表,eg:返回List<User> list;返回的是一个集合,我们返回类型写该集合发泛型就好(注:在mybits中,mybatis会对数据的数量进行分析,判别,如果超过1条就会使用集合来进行封装,所以我们直接返回泛型就好)
4.返回的是Map集合类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
csharp复制代码这是基本类型
<select id="findCount" resultType="int">
select count(*) from t_user ;
</select>

String类型

<select id="findCount" resultType="java.lang.String">
select count(*) from t_user ;
</select>


pojo类型

<select id="findCount" resultType="com.albb.User">
select count(*) from t_user ;
</select>

ParameterType详解

1
2
3
4
5
csharp复制代码1.基本类型直接写int|long,字符串直接写全限定类名 java.long.String
如果要使用传递的参数,直接写#{任意字段} ${value}
2.传递pojo对象,也是写pojo的全限定类名,
如果要使用传递参数,直接写#{对象属性名}
3.如果传递的是包装对象数据,也是使用#{}取值,取值的时候先找到属性,然后再通过属性名.属性名来找值
1
2
3
4
5
6
xml复制代码   <!--parameterType传递简单的参数:
1. 直接使用#{} 取值即可
2. 大括号里面一般写参数的名字,当然写什么都可以!!但是不建议这么做-->
<select id="findUserByUsername" resultType="com.albb.bean.User" parameterType="java.lang.String">
select * from t_user where username = #{username}
</select>
1
2
3
4
5
6
7
8
xml复制代码     <!--parametertype传递对象数据
1. 也是使用#{}取值
2. 大括号里面一般写javabean属性的名字。
-->
<select id="findUserByUsername02" resultType="com.albb.bean.User" parameterType="com.itheima.bean.User">
<!-- select * from t_user where username = #{对象的属性名}-->
select * from t_user where username = #{username}
</select>
1
2
3
4
5
6
7
8
9
10
parametertype复制代码        1. 也是使用#{}取值
2. 取值的时候,需要先找到QueryVo里面的属性user
3. 再使用user.uid 去找到uid 的值。
-->
<!--根据用户id查询用户 使用QueryVo传递参数user user对象中包含uid属性
当parameterType传递的是包装类型时,获取对应的参数#{对象属性.属性名称}
-->
<select id="getUserByQueryVo" parameterType="QueryVo" resultType="User">
select * from t_user where uid=#{user.uid}
</select>

当我们如果想要传递多个简单类型参数,该如何传呢?
第一种方法:把参数封装到pojo对象中去【推荐】
第二种方法:把多个简单类型封装到map集合中【map的key用的比较少】
第三种办法:直接在参数前面加上@Param注解

1
2
less复制代码   List<User> getUserListByNameAndAddres(@Param("username") String username,@Param("address") String address);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
csharp复制代码// 方式一:使用@Param注解 直接传递   #{参数名称}
List<User> getList(@Param("username") String username,@Param("roleId") Integer roleId);
select * from t_user where username like concat('%',#{username},'%') and roleId=#{roleId}
// 方式二:封装成一个pojo对象类型 #{javabean属性名称} 【实际开发】
public class ParamVo(){
private String username;
private Integer roleId;
}
select * from t_user where username like concat('%',#{username},'%') and roleId=#{roleId}
// 方式三:封装成一个Map集合 #{map的key}
Map map = new HashMap();
map.put("username","张");
map.put("roleId",3);
select * from t_user where username like concat('%',#{username},'%') and roleId=#{roleId}
1
2
bash复制代码扩展:当我们在进行新增操作时,怎么样才能拿到这条记录的id呢
方式一:selectKey获取主键

image.png
方式二:通过属性配置

image.png

resultMap结果类型(当表中字段和javabean中字段不一致时需要手动进行映射配置)

1
2
复制代码1.比较简单就是在查询语句中给字段起别名,从而让他和javabean中的字段一致
2.通过resultMap映射的方式,当他们一一对应起来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
xml复制代码<!--
id:是resultMap的唯一表示
type:表示配置的是哪个javabean
-->
<resultMap id="user" type="User">
<!--
id标签配置主键列字段的映射
property:表示javabean中的属性
column:表示表中的字段
-->
<id column="u_id" property="uid"></id>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
</resultMap>

此时我们的返回值就不能写resultTye了,应该写resultMap

1
2
3
4
sql复制代码<select id="findAll" resultMap="user">

SELECT *from t_user
</select>

${}和#{}的区别

1
2
3
4
5
6
bash复制代码1.他们都可以进行属性取值,
如果是基本类型:${value}来进行取值或者#{任意字段(推荐用属性名称)}
如果是pojo类型,${属性名称},#{属性名称}
2.#{}可以进行预编译,有效防止数据注入问题,会自动进行类型转换,一般会在字段头上加’‘,所以如果在进行模糊查询或者排序时,必须使用${}
${}只会进行sql拼接,不能防止sql注入,不会进行类型转换,在模糊查询或者排序时使用它’
一般工作时使用#{}多一点

本文转载自: 掘金

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

快速剪辑-助力度咔智能剪辑提效实践 一、设计背景 二、整体架

发表于 2021-11-18

导读:为了提升好看创作者剪辑效率,快速剪辑通过智能识别视频中的字幕、重复句、空白句,一键清除无效片段,提升剪辑效率。本文章旨在与大家分享快速剪辑的建设路径与实践过程中遇到的问题。

全文5886字,预计阅读时间15分钟。

图片

一、设计背景

度咔剪辑作为百度出品的一款泛知识类视频剪辑工具,承载着创作者的剪辑工作,而快速剪主要解决快速去除无效片段的问题。

图片

  • 修剪片段

通常我们修剪片段时,需要人工跳转至大致位置,再通过播放视频来定位具体想要修剪的位置。快剪通过NLP识别视频中的字幕,按照视频timeline对齐展示,再通过字幕时长映射视频时间片段,快速修剪。

  • 语气、停顿、重复句

由于泛知识类视频的特殊性,创作者录制过程中经常会出现语气、停顿、重复句等,假如我们需要修剪类似片段,只能够通过播放视频人工判定,操作繁琐。

快速剪辑基于视频中提取的音频,借助NLP将含有语气的片段标记出来,再通过字幕之间的时间间隙,直接标记出无声片段区。重复句则可依据计算相邻句式的字符重复度来标记。

二、整体架构

图片

  • Plugin:用于展示快剪控制器。
  • Window:用于渲染与视频控制。
  • Caption:用于字幕展示与操作。

2.1 Window

图片

Window主要包含视频渲染、控制及撤销等功能:

  • Timeline:存储视频的clip数据;TimelineModel:存储贴纸、字幕等数据信息。
  • Streaming:负责渲染Timeline与TimelineModel数据信息,对视频进行预加载,播放控制,进度回调等。
  • LiveWindow:用于展示Streaming渲染好的View,调节尺寸背景,坐标换算等。
  • Restorer:用于保存用户操作,并对操作进行撤销、重做等。

2.2 Caption

Caption包含快剪的主要功能实现:字幕识别、字幕匹配、语气词、重复句等。

图片

  • 当进入快速剪辑页面时,会率先对是否已识别过字幕进行检测,若N,则提取视频音频并上传至后端,进行NLP分析,获取字幕和语气片段数据。

由于获取到字幕仅包含有效片段,得到后需要字幕与timeline做时间匹配。

(视频信息是由clip对象存储的,每个clip对象表示一个单独的视频片段。一个clip包含trimIn、trimOut、inPoint、outPoint,通过这些数据来确定一个视频片段展示哪个视频,展示在视频的什么位置。字幕同理,通过字幕的clip数据,把文字放在正确的位置上。)

图片

  • 若已经有字幕文件,则对比距上次识别是否有增添视频,如果有新增,则把未进行字幕识别的视频进行音频提取,传至后端进行NLP分析与识别。
  • 若对比后无新增视频,则直接进入空白句识别阶段。
  • 以上文得到的字幕clip与视频的clip数据,对相邻字幕依据空白句规则增加空白字幕clip。

图片

  • 目前快剪重复句以上下4句、长度大于3且须包含中文为判定条件,综合字符重复度、莱文斯坦距离、余弦相似性三种策略计算重复比率。

图片

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
csharp复制代码/**
* 字符重复度
*/
private func similarity(s1: String, s2: String) -> Float {
var simiCount: Float = 0
var string2Array = [String]()
for i in 0..<s2.count {
// 从任意位置开始截取到任意位置,闭区间
let string = subOneString(string: s2, from: i)
string2Array.append(string)
}
for i in 0..<s1.count {
let string1 = subOneString(string: s1, from: i)
if string2Array.contains(string1) {
let index2 = string2Array.firstIndex(of: string1)
string2Array.remove(at: index2!)
simiCount = simiCount + 1
}
}
if simiCount == 0 {
return 0.0
}
let rate: Float = simiCount / Float(max(s1.count, s2.count))
return rate
}
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
ini复制代码/**
* 莱文斯坦距离,是编辑距离的一种。指两个字串之间,由一个转成另一个所需的最少编辑操作次数。
*/
-(CGFloat)levenshteinDistance:(NSString *)s1 compare:(NSString *)s2 {
NSInteger n = s1.length;
NSInteger m = s2.length;
// 有一个字符串为空串
if (n * m == 0) {
return n + m;
}
// DP 数组
int D[n + 1][m + 1];
// 边界状态初始化
for (int i = 0; i < n + 1; i++) {
D[i][0] = i;
}
for (int j = 0; j < m + 1; j++) {
D[0][j] = j;
}
// 计算所有 DP 值
for (int i = 1; i < n + 1; i++) {
for (int j = 1; j < m + 1; j++) {
int left = D[i - 1][j] + 1;
int down = D[i][j - 1] + 1;
int left_down = D[i - 1][j - 1];
NSString *i1 = [s1 substringWithRange:NSMakeRange(i - 1, 1)];
NSString *j1 = [s2 substringWithRange:NSMakeRange(j - 1, 1)];
if ([i1 isEqualToString:j1] == NO) {
left_down += 1;
}
D[i][j] = MIN(left, MIN(down, left_down));
}
}
NSInteger maxLength = MAX(s1.length, s1.length);
CGFloat rate = 1.0 - ((CGFloat) D[n][m] / (CGFloat)maxLength);
returnrate;
}
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
ini复制代码/**
* 余弦相似性: 首先将字符串向量化,之后在一个平面空间中求出他们向量之间夹角的余弦值。
*/
-(CGFloat)cos:(NSString *)s1 compare:(NSString *)s2 {
NSMutableSet *setA = [NSMutableSet new];
for (int i = 0; i < [s1 length]; i++) {
NSString *string = [s1 substringWithRange:NSMakeRange(i, 1)];
[setA addObject:string];
}
NSMutableSet *setB = [NSMutableSet new];
for (int i = 0; i < [s2 length]; i++) {
NSString *string = [s2 substringWithRange:NSMakeRange(i, 1)];
[setB addObject:string];
}
// 统计字频
NSMutableDictionary *dicA = [NSMutableDictionary new];
NSMutableDictionary *dicB = [NSMutableDictionary new];
for (NSString *key in setA) {
NSNumber *value = dicA[key];
if (value == nil) {
value = @(0);
}
NSNumber *newValue = @([value integerValue] + 1);
dicA[key] = newValue;
}
for (NSString *key in setB) {
NSNumber *value = dicB[key];
if (value == nil) {
value = @(0);
}
NSNumber *newValue = @([value integerValue] + 1);
dicB[key] = newValue;
}
// 向量化,求并集
NSMutableSet *unionSet = [setA mutableCopy]; //取并集后
[unionSet unionSet:setB];
NSArray *unionArray = [unionSet allObjects];
NSMutableArray *aVec = [[NSMutableArray alloc] initWithCapacity:unionSet.count];
NSMutableArray *bVec = [[NSMutableArray alloc] initWithCapacity:unionSet.count];
for (NSInteger i = 0; i < unionArray.count; i++) {
[aVec addObject:@(0)];
[bVec addObject:@(0)];
}
for (NSInteger i = 0; i < unionArray.count; i++) {
NSString *object = unionArray[i];

NSNumber *numA = dicA[object];
if (numA == nil) {
numA = @(0);
}
NSNumber *numB = dicB[object];
if (numB == nil) {
numB = @(0);
}
aVec[i] = numA;
bVec[i] = numB;

}
// 分别计算三个参数
NSInteger p1 = 0;
for (NSInteger i = 0; i < aVec.count; i++) {
p1 += ([aVec[i] integerValue] * [bVec[i] integerValue]);
}
CGFloat p2 = 0.0f;
for (NSNumber *i in aVec) {
p2 += ([i integerValue] * [i integerValue]);
}
p2 = (CGFloat)sqrt(p2);

CGFloat p3 = 0.0f;
for (NSNumber *i in bVec) {
p3 += ([i integerValue] * [i integerValue]);
}
p3 = (CGFloat)sqrt(p3);
CGFloat rate = ((CGFloat) p1) / (p2 * p3);
return rate;
}
  • 由于刷新timeline十分耗费性能,在删除字幕时,可只修改字幕clip数据源,刷新下方tableview展示,在用户退出时再刷新timeline,这样即可避免因实时刷新带来的性能问题。
  • 一键优化后,将字幕数据回传于通用剪辑进行缓存及展示,也便于再次进入快速剪辑。

三、思考与总结

由于字幕识别在远端进行,严重依赖网络,未来度咔将通过飞桨在端中完成音频源数据分析、字幕、重复、语气等片段识别,增强时效性与数据安全性,同时也能节约流量存储成本。以上是整个快速剪辑一键智能剪辑的路径实践,希望通过本篇文章能够让大家有所收获、有所借鉴。

推荐阅读:

|基于etcd实现大规模服务治理应用实战

|短视频个性化Push工程精进之路

|百度爱番番数据分析体系的架构与实践

———- END ———-

百度 Geek 说

百度官方技术公众号上线啦!

技术干货 · 行业资讯 · 线上沙龙 · 行业大会

招聘信息 · 内推信息 · 技术书籍 · 百度周边

欢迎各位同学关注

本文转载自: 掘金

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

今年双11,阿里业务100%上云!

发表于 2021-11-18

简介: 阿里巴巴业务的研发效率提升了20%、CPU资源利用率提升30%、应用100%云原生化、在线业务容器可达百万规模,同时计算效率大幅提升,双11整体计算成本三年下降30%。

今天,阿里巴巴首席技术官程立宣布——2021天猫双11是首个100%的云上双11,整体计算成本三年下降了30%。

程立透露,要把阿里巴巴的业务全部迁上公共云,挑战巨大,不仅要保证业务不中断,还需应对期间可能出现的突发状况。

今年年初,阿里巴巴把最繁重的一个业务——搜索业务顺利搬到了云上,而消费者和商家对这个“开着飞机换引擎”的过程毫无感知。

阿里是全球首家将所有业务都放在自家公共云上的大型科技公司,这意味着阿里云有能力应对高难度复杂环境下的技术挑战。

程立说,上云带来的好处显而易见:

阿里巴巴业务的研发效率提升了20%、CPU资源利用率提升30%、应用100%云原生化、在线业务容器可达百万规模,同时计算效率大幅提升,双11整体计算成本三年下降30%。

这些提升源于交易链路的大幅优化,通过统一资源池进行调度,支撑大规模的离在线混部,实现在线业务优先调度,应对脉冲式的流量冲击。

作为世界上规模最大的数字工程之一,双11是对阿里技术的“大考”。

从2014年采用自研数据库承载交易系统、2015年实现全球最大规模的混合云架构、2019年核心交易系统上云,再到今年全栈自研技术支持双11,包括自研芯片和服务器投入超大规模实战、数百台小蛮驴物流机器人全国配送等。

大规模释放的技术红利,让消费者和商家享受到丝般顺滑的体验。

↓阿里自研技术双11都在忙些啥↓

原文链接

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

本文转载自: 掘金

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

「计算机系统」重启就是关机再开机?想什么呢

发表于 2021-11-18

前两天在刷 pyq 的时候,看到了一朋友发的一张与客服的对话截图:

说实话,这种情况我也遇到过,还不止一次..

真是 Amazing!虽然先前就有听说重启和关机后再开机不一样,但当电脑跳出“需要重启”的弹窗时,我依旧会选择“稍后重启”,然后在使用完电脑后关机,下次使用时开机,当作重启…

当时还天真地以为,这样既没有打断正在进行的工作,还完成了重启,可谓是做到了效率的最大化…

今天亲眼见证了两种方式所带来的不同结果,我决心好好探索一下:

“重启”和“关机后再开机”到底有哪些不同?

01-定性来看

从最表层看,当我们点击“重启”后,计算机会自动进行注销用户、关闭系统、重新打开并装载系统等操作,主机似乎停了一下,但并没有完全关闭;

而选择关机再开机,不仅中途主机会完全关闭,我们还要手动按一下主机上的电源键才能再次将系统开启。

所以从操作手段上,关机再开机相比重启要多按一次按钮;从主机经历的过程上看,关机再开机相比重启经历得更多。emmm,似乎说了一段废话… 不过通过主机呈现出的不同状态,我们应该能感觉到两者肯定是有不同的。

在主机没有完全关闭的背后,究竟还藏着什么不为人知的秘密?

02-定量来看

想要深挖计算机重启背后的秘密,就要知道计算机在重启过程中都发生了什么。

首先我们来看看,操作系统是通过什么来区分重启和关机再开机的,这里就要引入一个概念——高级配置电源管理接口(Advanced Configuration and Power Interface),简称 ACPI。

ACPI 是1997年由多家公司共同提出、制定的操作系统电源管理、硬件配置接口,是一种开放标准。

ACPI 整体框架图

相信大家看到上图的感觉一定是非常头大,不过我们今天不是去深挖 ACPI 的,而是看 ACPI 是如何决定计算机的重启的。

ACPI 规范定义了一台兼容ACPI的计算机系统可以有以下七个状态(所谓的全局状态):

  • G0(S0):正常工作状态。计算机的正常工作状态-操作系统和应用程序都在运行。

  • G1:睡眠。这个状态还可以再细分为以下几种:

    • (1)S0ix:Modern Standby。在这种睡眠状态下,计算机还能联网,音乐还可以播放,其余大部分应用处于暂停的状态,轻按任意键即可登录账户并进入工作状态。

    • (2)S1:比较耗电的睡眠模式。CPU 的所有寄存器被刷新,并且停止执行指令,但 CPU 和内存的电源会被维持。

    • (3)S2:一种比S1更深的睡眠状态,会停止CPU的电源供应,这种模式通常不被采用。

    • (4)S3:称为 Suspend to RAM,简称 STR。这个模式就是我们计算机上常见的“睡眠”

    • (5)S4:称为Suspend to Disk,也是我们常说的“休眠”,其和 S3 的差别在于,S4 消耗的时间更长,而且如果此时系统断电,S3状态下没有保存的数据会丢失,而 S4 状态下不会影响。

  • G2(S5):称为 Soft Off。此状态和下面所所述的 G3(S6) 类似,不过在这个状态下,系统仍可以被部分设备(如键盘等)唤醒。

  • G3(S6):称为 Mechanical Off。此状态下所有部件断电,需要再次按下电源键才能唤醒。

虽然看起来我们一直没有提到重启,但从 ACPI 规范下计算机的几种状态,我们却可以看出其不同的运行模式和唤醒模式,这和我们马上要讲到的重启都是有关联的。

⭐其实你可能想不到,重启也分很多个种类,而我们平时最常接触到的重启,叫 hard reset,其通过信号通知所有芯片、外围网卡等等一起进行 reset 操作,使系统大多数寄存器重置到缺省值(默认值),而后 CPU 从 reset vector 开始执行程序。

⭐在这个过程中系统不会完全断电,不会进入 G3(S6) 的状态,CPU 在其它组件的协助下执行 reset 的相关程序。

⭐而关机再开机的过程中,系统会完全切断电源,即进入 G3(S6) 的状态,让所有寄存器均恢复到初始状态,整个过程需要执行的程序会更多,速度上也会更慢一些。

03-重启和关机再开机该怎么选择?

⭐我们选择重启计算机,可能是计算机进行了软件更新或者系统更新,亦或是系统的某个部件出了问题(就好比开头处那张截图里遇到的问题:微软和 Intel 芯片兼容问题)

一般情况下,如果是系统希望我们进行重启,那就按照要求进行重启就行。因为在软件或系统进行更新,计算机需要重新加载配置文件,这时候重启会显得更加得有针对性并且效率较高。

⭐而当计算机的部分功能出现障碍时,由于重启无法保证所有寄存器都恢复到默认状态,所以就会导致重启也可能解决不了问题,所以这时候就需要通过关机的方式来尝试解决。

写到这里,不禁感叹计算机这个系统真的太过庞大,一个小小的操作背后都隐藏有如此丰富的知识点,真心佩服研究计算机这个领域的大佬们,向你们学习!

04-参考资料

  • [1] 高级配置与电源接口 - 维基百科
  • [2] 计算机重启的原理是什么?- 知乎
  • [3] Sindre G, Conradi R, Karlsson E A. The REBOOT approach to software reuse[J]. Journal of Systems and Software, 1995, 30(3): 201-212.

❤💛💙本文系转载,原文地址:重启就是关机再开机?你以为错了…

本文转载自: 掘金

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

我就获取个时间,机器就down了

发表于 2021-11-18
  1. 背景

linux 时间管理,包含clocksource,clockevent,timer,tick,timekeeper等等概念 ,这些概念有机地组成了完整的时间代码体系。当然,是代码就会有bug,本文通过一个bug入手,在实战中加深对理论的认识。获取时间,但是crash了。

  1. 故障现象

OPPO云内核团队接到连通性告警报障,发现机器复位:

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
yaml复制代码PID: 0      TASK: ffff8d2b3775b0c0  CPU: 1   COMMAND: "swapper/1"
#0 [ffff8d597f6489f0] machine_kexec at ffffffffa5a63b34
#1 [ffff8d597f648a50] __crash_kexec at ffffffffa5b1e242
#2 [ffff8d597f648b20] panic at ffffffffa615d85b
#3 [ffff8d597f648ba0] nmi_panic at ffffffffa5a9859f
#4 [ffff8d597f648bb0] watchdog_overflow_callback at ffffffffa5b4a881
#5 [ffff8d597f648bc8] __perf_event_overflow at ffffffffa5ba26b7
#6 [ffff8d597f648c00] perf_event_overflow at ffffffffa5babd24
#7 [ffff8d597f648c10] intel_pmu_handle_irq at ffffffffa5a0a850
#8 [ffff8d597f648e38] perf_event_nmi_handler at ffffffffa616d031
#9 [ffff8d597f648e58] nmi_handle at ffffffffa616e91c
#10 [ffff8d597f648eb0] do_nmi at ffffffffa616ebf8
#11 [ffff8d597f648ef0] end_repeat_nmi at ffffffffa616dd89
[exception RIP: __getnstimeofday64+144]
RIP: ffffffffa5b03940 RSP: ffff8d597f643c78 RFLAGS: 00000212
RAX: 15b5c8320b8602cd RBX: ffff8d597f643cb0 RCX: 000000005f89ee29
RDX: 00000000ee4479fe RSI: 0000012b5478f3b2 RDI: 0009709c7629b240
RBP: ffff8d597f643c90 R8: 00000000007001de R9: ffff8d596d5c0000
R10: 000000000000007a R11: 000000000000000e R12: ffffffffa662ea80
R13: 000000003ccbcfb6 R14: ffff8d895de08000 R15: 0000000000000081
ORIG_RAX: ffffffffffffffff CS: 0010 SS: 0018
--- <NMI exception stack> ---
#12 [ffff8d597f643c78] __getnstimeofday64 at ffffffffa5b03940
#13 [ffff8d597f643c98] getnstimeofday64 at ffffffffa5b0398e
#14 [ffff8d597f643ca8] ktime_get_real at ffffffffa5b03a45
#15 [ffff8d597f643cd0] netif_receive_skb_internal at ffffffffa603b936
#16 [ffff8d597f643d00] napi_gro_receive at ffffffffa603c588
#17 [ffff8d597f643d28] mlx5e_handle_rx_cqe_mpwrq at ffffffffc052ef1d [mlx5_core]
#18 [ffff8d597f643db8] mlx5e_poll_rx_cq at ffffffffc052f4b8 [mlx5_core]
#19 [ffff8d597f643e08] mlx5e_napi_poll at ffffffffc05304c6 [mlx5_core]
#20 [ffff8d597f643e78] net_rx_action at ffffffffa603bf1f
#21 [ffff8d597f643ef8] __do_softirq at ffffffffa5aa2155
#22 [ffff8d597f643f68] call_softirq at ffffffffa617a32c
#23 [ffff8d597f643f80] do_softirq at ffffffffa5a2e675

从堆栈看,我们的0号进程在处理软中断收包的过程中,因为获取个时间,导致了crash。hardlock的分析之前已经给出了很多了,无非是关中断时间长了,具体关中断的地方,可以看call_softirq函数即可。

  1. 故障现象分析

1)理论知识

在处理网络包的软中断过程中,会打时间戳,也就是说,对于oppo云的机器来说,以上的调用栈路径是一个热点且成熟的路径。成熟的路径出问题比较少见,所以有必要分享一下。在timekeeping初始化的时候,很难选择一个最好的clock source,因为很有可能最好的那个还没有初始化呢。因此,策略就是采用一个在timekeeping初始化时一定是ready的clock source,比如基于jiffies 的那个clocksource。

一般而言,timekeeping模块是在tick到来的时候更新各种系统时钟的时间值,ktime_get调用很有可能发生在两次tick之间,这时候,仅仅依靠当前系统时钟的值精度就不够了,毕竟那个时间值是per tick更新的。因此,为了获得高精度,ns值的获取是通过timekeeping_get_ns完成的,timekeeping_get_ns就是本文的主角,该函数获取了real time clock的当前时刻的纳秒值,而这是通过上一次的tick时候的real time clock的时间值(xtime_nsec)加上当前时刻到上一次tick之间的delta时间值计算得到的。系统运行之后,real time clock+ wall_to_monotonic是系统的uptime,而
real time clock+ wall_to_monotonic + sleep time也就是系统的boot time。

2)实战分析

根据调用堆栈,简单地看,__getnstimeofday64只有一个循环,那就是读取timekeeper_seq的顺序锁,代码分析如下:

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
ini复制代码int __getnstimeofday64(struct timespec64 *ts)
{
struct timekeeper *tk = &timekeeper;
unsigned long seq;
s64 nsecs = 0;


do {
seq = read_seqcount_begin(&timekeeper_seq);


ts->tv_sec = tk->xtime_sec;//caq:秒值赋值
nsecs = timekeeping_get_ns(&tk->tkr_mono);


} while (read_seqcount_retry(&timekeeper_seq, seq));


ts->tv_nsec = 0;
timespec64_add_ns(ts, nsecs);//caq:这里面还有个循环呢,


/*
* Do not bail out early, in case there were callers still using
* the value, even in the face of the WARN_ON.
*/
if (unlikely(timekeeping_suspended))
return -EAGAIN;
return 0;
}

但是从汇编展开来看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
perl复制代码0xffffffffa5b0393b <__getnstimeofday64+139>:    xor    %edx,%edx----清零 u32 ret = 0;
0xffffffffa5b0393d <__getnstimeofday64+141>: nopl (%rax)
0xffffffffa5b03940 <__getnstimeofday64+144>: sub $0x3b9aca00,%rax---------------------1s就是1000000000 ns,循坏在这,而栈中的rax为 15b5c8320b8602cd
0xffffffffa5b03946 <__getnstimeofday64+150>: add $0x1,%edx---------ret++;edx is the lower 32 bit of rdx,rdx为00000000ee4479fe,所以edx为 0xee4479fe,也就是3997465086
0xffffffffa5b03949 <__getnstimeofday64+153>: cmp $0x3b9ac9ff,%rax-------------------------------------剩余是否小于1ns
0xffffffffa5b0394f <__getnstimeofday64+159>: ja 0xffffffffa5b03940 <__getnstimeofday64+144>
/include/linux/time.h: 215---对应 timespec_add_ns
0xffffffffa5b03951 <__getnstimeofday64+161>: add %rcx,%rdx---delta算出的秒值+之前保存的秒值,就是最新的秒值
0xffffffffa5b03954 <__getnstimeofday64+164>: mov %rax,0x8(%rbx)----剩余的ns,赋值给a->tv_nsec = ns;
0xffffffffa5b03958 <__getnstimeofday64+168>: mov %rdx,(%rbx)---加完delta秒值的最新的秒值,赋值给a->tv_sec
0xffffffffa5b0395b <__getnstimeofday64+171>: cmpl $0x1,0xc55702(%rip) # 0xffffffffa6759064----if(timekeeping_suspended)
/kernel/time/timekeeping.c: 512
0xffffffffa5b03962 <__getnstimeofday64+178>: pop %rbx
0xffffffffa5b03963 <__getnstimeofday64+179>: pop %r12
0xffffffffa5b03965 <__getnstimeofday64+181>: pop %r13

从堆栈看出,我们循环在__getnstimeofday64+144

1
lua复制代码0xffffffffa5b03940 <__getnstimeofday64+144>: sub $0x3b9aca00,%rax---------------------1s就是1000000000 ns,循坏在这,而栈中的rax为 15b5c8320b8602cd

原来我们循环在timespec64_add_ns 函数里面:

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
rust复制代码static __always_inline void timespec64_add_ns(struct timespec64 *a, u64 ns)
{
a->tv_sec += __iter_div_u64_rem(a->tv_nsec + ns, NSEC_PER_SEC, &ns);
a->tv_nsec = ns;
}
__iter_div_u64_rem展开如下:


static __always_inline u32
__iter_div_u64_rem(u64 dividend, u32 divisor, u64 *remainder)
{
u32 ret = 0;


while (dividend >= divisor) {//这个循环
/* The following asm() prevents the compiler from
optimising this loop into a modulo operation. */
asm("" : "+rm"(dividend));


dividend -= divisor;
ret++;
}


*remainder = dividend;


return ret;
}

我们的入参divisor是 NSEC_PER_SEC,也就是10的9次方,16进制为0x3b9aca00,既然在循环,那么我们的dividend是rax,请注意看值为:

1
2
3
4
5
ini复制代码RAX: 15b5c8320b8602cd


crash> p 0x15b5c8320b8602cd/0x3b9aca00
$7 = 1564376562

按照这样计算,要计算完毕,还得循环 1564376562 这么多次。
这么大的一个值,确实不知道循环到猴年马月去。
那么这个值怎么来的呢?原来这个值是前后两次读取closk_source的cycle差值计算出来的。

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
scss复制代码static inline s64 timekeeping_get_ns(struct tk_read_base *tkr)
{
u64 delta;


delta = timekeeping_get_delta(tkr);//caq:上次读取与本次读取之间的差值
return timekeeping_delta_to_ns(tkr, delta);//caq:差值转换为ns
}


delta的来源是:
static inline u64 timekeeping_get_delta(struct tk_read_base *tkr)
{
u64 cycle_now, delta;
struct clocksource *clock;


/* read clocksource: */
clock = tkr->clock;
cycle_now = tkr->clock->read(clock);//当前值是通过读取来的


/* calculate the delta since the last update_wall_time */
delta = clocksource_delta(cycle_now, clock->cycle_last, clock->mask);//计算差值


return delta;
}

原来,delta的获取是线读取当前clocksource的cycle值,然后通过clocksource_delta 计算对应的差值,根据以上代码,首先我们得知道当前的clocksource是哪个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ini复制代码crash> timekeeper
timekeeper = $1 = {
tkr_mono = {------------------------------timekeeping_get_ns(&tk->tkr_mono)
clock = 0xffffffffa662ea80, ------------这个就是 clocksource,这个值当前就是 clocksource_tsc
cycle_last = 16728674596502256,
mult = 7340510,
shift = 24,
xtime_nsec = 2657092090049088, 这个值并不是ns,而是要 >>tkr->shift
base = {
tv64 = 2788453640047242
}
},
tkr_raw = {
clock = 0xffffffffa662ea80,
cycle_last = 16728674596502256,
mult = 8007931,
shift = 24,
xtime_nsec = 0,
base = {
tv64 = 2788490058099290
}
},
xtime_sec = 1602874921, ------------------当前的秒数

timekeeper是选择当前精度最高的clocksource来工作的:

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
ini复制代码crash> dis -l 0xffffffffa662ea80
0xffffffffa662ea80 <clocksource_tsc>: addb $0xa5,-0x5d(%rcx)--------------就是 clocksource_tsc ,tsc就是一个clock_source




crash> clocksource_tsc
clocksource_tsc = $2 = {
read = 0xffffffffa5a34180, -----------read_tsc
cycle_last = 16728674596502256, ------上次更新墙上时间的时刻取的cycle值
mask = 18446744073709551615,
mult = 8007931,
shift = 24, ----------------------注意位数
max_idle_ns = 204347035648,
maxadj = 880872,
archdata = {
vclock_mode = 1
},
name = 0xffffffffa647c1cd "tsc", ---名称
list = {
next = 0xffffffffa6633ff8,
prev = 0xffffffffa665c9b0
},
rating = 300, --------------优先级,
enable = 0x0,
disable = 0x0,
flags = 35, ---没有CLOCK_SOURCE_UNSTABLE标志
suspend = 0x0,
resume = 0x0,
owner = 0x0
}

差值的计算分析如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
rust复制代码static inline s64 timekeeping_delta_to_ns(struct tk_read_base *tkr,
u64 delta)
{
s64 nsec;//注意,这里是带符号数


nsec = delta * tkr->mult + tkr->xtime_nsec;
nsec >>= tkr->shift;//换算成ns


/* If arch requires, add in get_arch_timeoffset() */
return nsec + arch_gettimeoffset();
}

timekeeping_delta_to_ns返回值过大,有两种可能:一种是delta的偏大,delta * tkr->mult 对s64的值产生溢出,这个算是个bug。还有一种可能是,直接前后读取的delta值太大,这涉及到 update_wall_time 并没有及时调用去读取当前clocksource的cycle。

  1. 故障复现

这个s64溢出的bug,在社区已经修复了。

1
2
3
4
5
6
arduino复制代码-static inline s64 timekeeping_delta_to_ns(struct tk_read_base *tkr,
+static inline u64 timekeeping_delta_to_ns(struct tk_read_base *tkr,
cycle_t delta)
{
- s64 nsec;
+ u64 nsec;

而且查看红帽的changelog,也按照上游这样修复,但是我觉得风险还在的,因为 update_wall_time 有时候更新就不是那么及时,而哪怕从s64改到u64,并没有解决溢出问题,因为 timekeeping_delta_to_ns函数中明显可以看到,u64的64位并没有全部用到cycle的差值上。我相信社区最终应该会有人爆这个问题的。

  1. 故障规避或解决

可能的解决方案是:
增加告警,对于softlock的要及时介入,有可能导致update_wall_time 更新不及时。

作者简介

Anqing 高级后端工程师

目前主要负责linux内核及容器,虚拟机等虚拟化方面的工作。

获取更多精彩内容,扫码关注[OPPO数智技术]公众号

本文转载自: 掘金

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

轻松搞懂MySQL的执行计划,再也不怕SQL优化了

发表于 2021-11-18

摘要:数据库的解释计划阐明了sql的执行过程,展示了执行的细节,只要根据数据库告诉我们的问题按图索骥的分析就可以。

本文分享自华为云社区《轻松搞懂mysql的执行计划,再也不怕sql优化了》,作者:香菜聊游戏。

近期要做一些sql优化的工作,虽然记得一些常用的sql 优化技巧,但是在工作中还是不够,所以需要借助工具的帮助,数据库的解释计划阐明了sql的执行过程,展示了执行的细节,我们只要根据数据库告诉我们的问题按图索骥的分析就好了,但是解释计划也不是那么容易看懂,所以今天就学习下解释计划的一些参数的意义。

1、准备工作

准备三张表,一张角色表,一张装备表,一张基础数据表,这里只展示一些教程中需要的字段,在游戏开发的过程中肯定不止这么几个字段,我想大家都懂的。

角色表:

1
2
3
4
sql复制代码CREATE TABLE `role` (
`n_role_id` int DEFAULT NULL,
`s_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

​装备表:

1
2
3
4
5
sql复制代码CREATE TABLE `equip` (
`n_equip_id` int DEFAULT NULL,
`s_equip_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`n_config_id` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

​装备配置表:

1
2
3
4
sql复制代码CREATE TABLE `dict_equip` (
`n_equip_id` int DEFAULT NULL,
`s_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

2、初识解释计划

有两种方式可以查看解释计划:

1、命令的方式:explainsql,或者descsql ,两个命令都可以,我觉得记住explain比较好,单词很直接。

2、借助工具 Navicat(其他的不熟,估计也有),点击查询窗口的解释,可以不用加关键字explain

可以看到结果里面包含了很多列,有的是null 有的有值,只要我们看懂了解释计划是不是就可以有的放矢的优化sql。

3、字段详解

解释计划的字段还是蛮多的,Navicat显示了12个字段,有些字段我们需要重点关注,有些知道怎么回事就好了。

官方的文档解释:dev.mysql.com/doc/refman/…

1、id 执行的顺序

id 是select的执行顺序,id越大优先级越高,越先被执行,id 相同时​下面的先执行.

原因是因为执行子查询时,先查内层的,再查外层

1
2
3
4
5
6
7
8
9
10
sql复制代码SELECT
de.*
FROM
dict_equip de
WHERE
de.n_equip_id = (
SELECT n_equip_id FROM equip e WHERE
e.n_role_id = (
SELECT n_role_id FROM role r WHERE r.s_name = '香菜' )
)

从上面的执行计划可以看到先执行了查询role表,后执行了equip ,最后执行了 dict_equip

2、select_type select 的类型

​3、table 查询涉及的表或衍生表

当前输出的正在使用的表,可以有下面几种:

<union_M_,N> : 行数据是联合之后的数据id 处于 m和n

<derived*N*>: 衍生表

<subquery_N_>: 子查询

4、partitions 查询涉及到的分区

在使用分区表的时候才能用到,暂时没用到过这种高级功能。

5、type 查询的类型

表示MySQL在表中找到所需行的方式,又称“访问类型”,常见类型如下:

​性能:all < index < range < index_merge < ref_or_null < ref < eq_ref < system/const

由左至右,由最差到最好

在进行优化的时候如果查询出的数据量大的话可以使用全表扫描,避免使用索引。

如果只是查询很少的数据尽量使用索引。

6、possible_keys:预计可能使用的索引

在不和其他表进行关联的时候,查询表的是可能使用的索引

7、key:实际查询的过程中使用的索引

显示MySQL在查询中实际使用的索引,若没有使用索引,显示为NULL

8、key_len

表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度

9、ref 显示该表的索引字段关联了哪张表的哪个字段

注: 我在equip和 dict_equip 两张表都分别添加了索引,索引列是n_equip_id

通过上面的执行计划可以看出,首先使用了索引

10、rows:根据表统计信息及选用情况,大致估算出找到所需的记录或所需读取的行数,数值越小越好

比如 一个列上虽然没做索引,但是都是唯一的,这个时候查找的时候如果是全表读取,就是表里有多少数据这个值就是多少,这个时候你需要优化的就是尽可能的读取少的表,可以增加索引,减少读取行数

11、filtered:返回结果的行数占读取行数的百分比,值越大越好

比如全表有100条数据,可能读取了全表数据,但是只有一条匹配上,这个时候百分比就是1,所以你需要让这个比例越大越好,也就是读到的数据尽量都是有用的,避免读取不用的数据,因为IO是很费时的。

12、extra

常见的有下面几种

use filesort:MySQL需要额外的一次传递,以找出如何按排序顺序检索行,如果是这个值,应该优化索引。

use temporary:为了解决查询,MySQL需要创建一个临时表来容纳结果。典型情况如查询包含可以按不同情况列出列的GROUP BY和ORDERBY子句时。

use index:从只使用索引树中的信息而不需要进一步搜索读取实际的行来检索表中的列信息。当查询只使用作为单一索引一部分的列时,可以使用该策略

use where:where子句用于限制哪一行

4、总结

sql 优化的原则就是在保证正确的情况下缩短时间,目标是确定的,通过目标进行回推可以知道想要执行的快就要尽可能的少读数据,减少读取数据的方式大的只有两种过滤和使用索引,在这样的规则范围内进行优化,但是注意索引会占用额外的空间,要平衡好这两者的关系。

点击关注,第一时间了解华为云新鲜技术~

本文转载自: 掘金

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

烂大街的阻塞队列你真的学会了吗——ArrayBlocking

发表于 2021-11-18

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

BlockingQueue简介

在讲ArrayBlockingQueue之前先讲一下阻塞队列,阻塞队列支持阻塞的插入和移除。即当队列满了的情况下,队列会阻塞插入元素的线程,直到队列不满;当队列为空的情况下,队列会阻塞获取元素的线程,直到队列非空。

BlockingQueue的核心方法

offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false。本方法不阻塞当前执行方法的线程

offer(E o, long timeout, TimeUnit unit):可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。

put(anObject):把anObject加到BlockingQueue里,如果BlockingQueue没有空间,则调用此方法的线程被阻断,直到BlockingQueue里面有空间再继续。

poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null。

poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则知道时间超时还没有数据可取,返回失败。

take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入。

drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数), 通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。

需要注意的是BlockingQueue不接受null值的插入,相应的方法在碰到null的插入时会抛出NullPointerException 异常。null值用于作为特殊值返回,代表poll失败。

常见的BlockingQueue:
1094831408-4dac22f5a10a2e85_fix732.png

ArrayBlockingQueue

ArrayBlockingQueue是一个用数组实现的有界阻塞队列。按照先进先出(FIFO)的原则对元素进行排序。有界是指它不能够存储无限多数量的元素,在创建ArrayBlockingQueue时,必须要给它指定一个队列的大小。阻塞指在添加/取走元素时,当队列没有空间/为空的时候会阻塞,知道队列有空间/有新的元素加入时再继续。因为采用的是循环数组的形式表达队列,所以队列的容量一旦在构造时指定,后续不能改变,对于数组的长度来说,根据初始化的参数为标准,类中没有默认的数组长度。

源码分析

重要属性

ArrayBlockingQueue的底层组包括:数组,一个重入锁,两个条件队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
arduino复制代码//队列中元素保存的地方
final Object[] items;

//items index for next take, poll, peek or remove(下一个待删除位置的索引: take, poll, peek, remove方法使用)
int takeIndex;

// items index for next put, offer, or add(下一个待插入位置的索引: put, offer, add方法使用)
int putIndex;

//队列中元素个数
int count;

//全局锁
final ReentrantLock lock;

//非空条件队列:当队列空时,线程在该队列等待获取
private final Condition notEmpty;

//非满条件队列:当队列满时,线程在该队列等待插入
private final Condition notFull;

构造方法

1
2
3
4
5
6
7
8
9
10
11
12
csharp复制代码
//队列初始容量和公平/非公平锁的构造器.

public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();

this.items = new Object[capacity];
lock = new ReentrantLock(fair); // 独占锁
notEmpty = lock.newCondition(); //队列为空时,线程在该队列等待获取
notFull = lock.newCondition(); // 队列满时,线程在该队列等待插入
}

重要方法

添加元素

add()方法:此方法,实际上调用了offer(e)方法。

1
2
3
4
5
6
7
8
9
10
11
typescript复制代码public boolean add(E e) {
// 调用父类的add(e)方法
return super.add(e);
}
public boolean add(E e) {
// 调用offer(e)如果成功返回true,如果失败抛出异常
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}

offer(e)方法入队入参e为null,会抛空指针异常。

offer(E e)方法:将元素添加到 BlockingQueue 里,如果可以容纳返回 true 否则返回 false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
csharp复制代码public boolean offer(E e) {
checkNotNull(e); // 检查元素是否为 null
final ReentrantLock lock = this.lock;
lock.lock(); // 加锁
try {
if (count == items.length) // 如果队列已经满了返回 false
return false;
else { // 队列还没有满,则添加到队列中
enqueue(e); // 进队
return true;
}
} finally {
lock.unlock(); // 释放锁
}
}

put(e)方法:将元素添加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻塞,直到有空间

1
2
3
4
5
6
7
8
9
10
11
12
csharp复制代码public void put(E e) throws InterruptedException {
checkNotNull(e); // 检查元素是否为 null
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 加锁
try {
while (count == items.length)
notFull.await(); //如果队列已经满了,就阻塞(添加到 notFull 条件队列中等待唤醒)
enqueue(e); // 如果队列没有满直接添加
} finally {
lock.unlock(); // 释放锁
}
}

enqueue(E x)入队,利用放指针循环使用数组来存储元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码private void enqueue(E x) {
// 获取当前数组
final Object[] items = this.items;
// 通过索引赋值
items[putIndex] = x;
// 如果当前添加对象的位置 +1 等于 数组的长度,也就是当前对象的位置在数组的最后一个
// 那么下一个应该从数组的第一个添加
if (++putIndex == items.length)
putIndex = 0;
count++;
// 唤醒正在等待获取对象的线程
notEmpty.signal();
}

获取元素

poll()方法,取队头(首个)元素并删除,没有则返回null。

1
2
3
4
5
6
7
8
9
10
11
csharp复制代码public E poll() {        
final ReentrantLock lock = this.lock;
lock.lock();
try {
//如果队列里没有数据就直接返回null
//否则从队列头部出队
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}

take()方法,如果队列中有元素,则获取并删除,如果没有元素,则阻塞等待

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
csharp复制代码public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
//加锁,如果线程中断了抛出异常
lock.lockInterruptibly();
try {
//队列中不存元素
while (count == 0){
/*
* 一直等待条件notEmpty,即被其他线程唤醒
* (唤醒其实就是,有线程将一个元素入队了,然后调用notEmpty.signal()
* 唤醒其他等待这个条件的线程,同时队列也不空了)
*/
notEmpty.await();
}
//否则出队
return dequeue();
} finally {
lock.unlock();
}
}

dequeue()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ini复制代码private E dequeue() {
final Object[] items = this.items;
@SuppressWarnings("unchecked")
// 根据索引获取对象
E x = (E) items[takeIndex];
// 当前位置的对象被取走,位置就腾出来了
items[takeIndex] = null;
// 如果被取走的是数组的最后一个,那下一个要从第一个取
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
// 唤醒正在等待添加对象的线程
notFull.signal();
return x;
}

peek()方法,只取不删,当队列中没有元素时会返回null。

1
2
3
4
5
6
7
8
9
csharp复制代码public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return itemAt(takeIndex);
} finally {
lock.unlock();
}
}

删除元素

remove(o)方法,如果队列为空或者没有找到该元素返回false,否则删除元素并且返回true。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ini复制代码public boolean remove(Object o) {
if (o == null) return false;
final Object[] items = this.items;
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count > 0) {
final int putIndex = this.putIndex;
int i = takeIndex; // 从取得位置开始,到添加的位置
do {
if (o.equals(items[i])) {
removeAt(i);
return true;
}
if (++i == items.length) // 若果判断的位置到了队列最末尾,那下一个从第一个判断
i = 0;
} while (i != putIndex);
}
return false;
} finally {
lock.unlock();
}
}

removeAt(index)方法,删除指定位置上的元素

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
ini复制代码void removeAt(final int removeIndex) {
final Object[] items = this.items;
if (removeIndex == takeIndex) { // 当删除的元素是下次取操作要取到的元素时,既队头元素
items[takeIndex] = null; // 删除队头元素,并且 takeIndex 加 1
if (++takeIndex == items.length) // 如果删除得是数组最后一个元素,则 takeIndex 从数组第一个元素开始
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
} else {
// 如果删除的不是队头元素
// 则从删除元素的后面一直到添加元素的位置(removeIndex ~ putIndex)期间的元素都要往前挪一个位置
// 取 putIndex 作为循环结束判断条件
final int putIndex = this.putIndex;
for (int i = removeIndex;;) { // 顺序往前挪一个位置
int next = i + 1;
if (next == items.length)// 当循环到数组最后一个元素,下一个元素应该是数组第一个元素
next = 0;
if (next != putIndex) { // 如果查找的索引不等于要添加元素的索引,说明元素可以再移动
items[i] = items[next];
i = next;
} else { // 在 removeIndex 索引之后的元素都往前移动完毕后清空最后一个元素
items[i] = null;
this.putIndex = i;
break;
}
}
count--;
if (itrs != null)
itrs.removedAt(removeIndex);
}
notFull.signal();
}

总结

ArrayBlockingQueue特点是不需要扩容,因为是初始化时指定容量,并利用takeIndex和putIndex循环利用数组。并且有重入锁和两个条件保证并发安全。

本文转载自: 掘金

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

前沿分享|阿里云数据库资深技术专家 姚奕玮:Analytic

发表于 2021-11-18

简介: 本篇内容为2021云栖大会-云原生数据仓库AnalyticDB技术与实践峰会分论坛中,阿里云数据库资深技术专家 姚奕玮关于“AnalyticDB MySQL离在线一体化技术揭秘”的分享。

姚奕玮.jpeg

本篇内容将通过三个部分来介绍AnalyticDB MySQL离在线一体化技术。

一、传统大数据架构面临的问题和挑战

二、云原生数据仓库的架构与弹性

三、云原生数据仓库诊断和运维

一、传统大数据架构面临的问题和挑战

传统大数据架构面临的挑战和问题主要有:第一,数据散乱、不一致,没有一套统一的系统对这些数据进行分析。第二,分析不实时,一般会在夜间12点后对数据进行ETL清洗和转换,数据直到第二天的早上才能被查询到,数据时效性差。第三,系统复杂,为了解决数据时效性差的问题,一般的做法是在批处理上又引入了流式计算的引擎,形成著名的lambda架构,让整套系统变得越来越复杂。第四,高学习成本。专业的研发人员是非常少的,导致他们的工资非常高,所以要维护这一套系统的成本也非常高。

二、云原生数据仓库的架构与弹性

为了解决以上问题,我们构建这套离在线一体的架构。我们的愿景是:让用户会用数据库就会用大数据。第一,是我们是高度兼容MySQL,我们对MySQL的兼容超过了99%。AnalyticDB MySQL是一个云原生的架构,并且是存储计算分离的,存储计算可以分别扩缩容。我们用一套存储系统支持了实时写入以及多维的分析,并且通过智能索引来支持任意维度的分析。除此之外,我们具备例如审计、自建账号等完备的企业级的能力以及整套的备份还原能力。你如果误删了数据,AnalyticDB可以把数据闪回到你想要的时间点上。最后,我们的融合的计算引擎在同一套架构里面同时支持了离线和在线、结构化和非结构化数据的查询。

云原生数据仓库AnalyticDB的整个架构分为三层,最上面的是接入层,它负责生成一个执行计划,并且我们的优化器会优化这个执行计划、产生最终最优的物理计划、切分计划并且下发到计算层进行执行。整个数据的存储,我们是分为两级分区。一级分区,把数据打散在各个分片上面,保证了整个系统的水平扩展能力。第二部分提供了用户自定义的二级分区。你可以按照时间来分区,比如按照天或者小时来进行分区。我们的计算引擎也会自动根据这些分区来做分区裁剪。整个存储引擎支持强一致的实时增删改,你可以高并发的写入这些数据,并且数据写入后实时可见。与此同时,我们的计算引擎还支持混合负载。

如果用户需要一个离在线一体化的系统的话,需要哪些功能?第一个,你需要有支持多维分析以及ETL的能力。同时,必须支持数据的明细查询和检索。最后,你还要支持实时的高吞吐的查询和写入。这三个需求的交集就是AnalyticDB想要做到的部分。我们通过支持混合负载的融合计算引擎来做到高性能的查询;我们通过行列混存以及深度优化的写入方式来达到高并发以及高吞吐的写入;然后我们通过智能索引来做到明细的查询以及数据内文本的检索。

接下来看一看我们具体是怎么做的。首先是写入部分,离在线一体化的写入部分有两个需求。第一,高并发的数据流式地写入。第二,对于已经有的存量数据,能够高吞吐的把它导入到AnalyticDB里边。左边的部分,它是高并发的,整个流程当中,我们实现了数据的编码和传输的各种优化,使得数据在整个过程中的流转是零拷贝的,并且通过shard级并行和shard内部的表级并行做到了高并发。通过这套架构我们实现了千万级每秒的数据写入。右边的部分是高吞吐写入的架构。我们通过源头向量化读数据源、计算引擎向量化直接写入到存储来做到高吞吐的写入。

这部分讲的是AnalyticDB提供的高性价比。如果用户想把数据全部存在AnalyticDB上面的话,肯定会有冷存数据和热存数据。比如说用户想存三年的数据,但是有可能你只对最近一个星期的数据有热存的要求。因为最近一个星期的数据需要经常查,剩下的数据,用户希望低成本的把它存在AnalyticDB上,那就会放在冷存上面。所以我们提供了三种类型的表,一种是全热的表,数据全部存在热存。一种是全冷的表,数据全部存在冷存。还有一种是冷热混合,也就是部分数据可以存在热存里,剩下的数据存在冷存里。

接下来,看一下我们明细查询。明细查询利用了AnalyticDB的智能索引能力。我们对于不同的数据类型有不同的索引。我们通过CBO来估算索引筛选率的高低,来决定是否使用索引。AnalyticDB根据不同的过滤条件使用不同的索引,最后渐进地多路归并返回查询结果的行号。我们内部的数据通过行列混存的方式进行存储,并且通过meta里面存储的粗糙集来进一步过滤数据。我们还用了字典编码来压缩字符串类型的数据。

我们在一套计算系统里实现了离线和在线的融合。对于在线的查询场景,用户希望它的查询能够尽量的快。我们可以做到几十毫秒甚至几毫秒的分析型的查询结果返回。我们通过调起所有的stage,并且算子流式地、不落盘地处理数据来达到极短的延时。右边的是离线的场景,延时并不是第一优先级。用户希望离线场景查询能够在固定的时间内稳定地跑完。

ETL类型查询有可能会跑个几天,这时候我们采取另一种batch的执行方式,整个过程非常稳定。数据在stage间的shuffle都会落盘。我们对Coordinator和Executor节点的宕机都做了failover的支持,同时我们通过自适应的分批调度来实现子计划的规模化调度。在整个计算的过程当中,我们通过Codegen减少虚函数的开销、减少数据物化到内存,从而进一步优化我们的查询。

Adaptive Execution解决的问题是,优化器估的再准,总是有误差的。有可能最终生成的计划和我想要生成的最优的计划是不一样的。那我们就需要在计划执行的过程当中去自适应地调整这个计划。我们实现了基于数据中间结果的自适应分区和基于数据结果的自适应计划,起到了runtime矫正计划的作用。

说完了计算和存储,再说一下优化器。我们实现了整套智能的优化。优化器分为两个部分,第一个部分是底层统计信息的采集部分。我们会根据查询条件,自动在某些列上采集统计信息。第二,我们会在规定的时间内通过Cascades的框架搜索出最优的执行计划,我们用一套优化器支持了整套离在线的查询。并且我们的优化器,不仅对接了AnalyticDB内部的数据源,还支持了外部的例如存储在OSS、HDFS上的数据源。做到了湖仓一体的查询优化。

除了上面提及的一些性能的优化之外,我们还做了很多其他的性能优化:比如源头向量化读取;向量化算法优化;自动物化视图的改写;基于代价的最大执行子树复用等等。

AnalyticDB是支持多维的弹性的,计算支持从1个节点到5000节点,ETL+在线分析按需动态扩展。存储的弹性分为两个维度:存储的容量支持从GB到100PB;存储节点的QPS支持从1到百万级。

来看一下我们为什么要做弹性的功能。这是我们AnalyticDB在去年的某一周的所有的查询。我们对它进行了分析。我们发现只有万分之五的查询等待超过了1秒。但是通过另一个维度从实例级别来看,反而有大约有10%的查询超过1秒或者5秒的等待。这说明这万分之五的查询分散在不同的实例上面。说明业务有很多场景,它的查询量,在非常短的时间内会暂时超过它的预估或者期望值,造成查询排队。这时候弹性就能很好的解决这个问题。

AnalyticDB提供了三种弹性能力,第一种是分时弹性。比如你知道下午4点到8点会有一个大促活动。那4点之前,我们会把这些计算节点帮用户给弹出来。第二个是租户隔离的能力,假如两个部门有不同的查询在同时跑,A部门的查询并不会影响B部门的查询。第三个是按需弹性。这个主要为了处理不可预期的流量,我们可以按需地弹出用户所想要的节点来保证高优先级业务的SLA。

我们的分时和按需弹性是怎么做的呢?我们自己维护了一个资源池,然后在池子上写了一套资源管理器。当用户有弹性需要时,我们会从这个池子里面取出节点,加到用户的AnalyticDB里。当他用完时,我们会自动把这个节点归还回资源池里。整个过程是非常快的,我们可以在分钟级别完成这个操作。

AnalyticDB提供了资源组隔离的能力。不同的资源组的资源在物理上是隔离的。比如A部门的测试查询并不会影响B部门的营销查询。

三、云原生数据仓库诊断和运维

一个优秀的数据仓库,不仅仅内核要做的好,我们还要给用户智能诊断的能力。能够让用户知道自己的系统的问题出在哪里。所以我们做了一整套的智能诊断系统。这套智能诊断系统有很多技术组件,功能组件,这些都深度结合到我们的内核里。当你有新的查询来的时候,我们会根据聚类算法来检测是否有异常出现。如果有异常的话,我们会对接智能告警系统,通过钉钉、电话或者邮件给你发送消息。

我们的智能优化提供了自动分析的能力;提供了数据仓库建模建议,根据系统的实际运行情况,我们会给出具体的建议来修改数据分布或者分区,让系统更加平滑地运行;同时,我们提供了智能巡检告警的能力。

原文链接

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

本文转载自: 掘金

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

1…293294295…956

开发者博客

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