Node-JS专精03递归记忆化

递归

阶乘

1
2
3
4
5
6
7
8
9
10
11
12
const j = (n) => 
n === 1 ? 1
: n * j(n - 1)

j(4)
= 4 * j(3)
= 4 * ( 3 * j(2))
= 4 * ( 3 * 2 * j(1))
= 4 * ( 3 * 2 * 1)
= 4 * ( 3 * 2 )
= 4 * 6
= 24

斐波那契数列

1
2
3
4
const fib = (n) =>
n === 0 ? 0 :
n === 1 ? 1 :
fib(n - 1) + fib(n - 2)

递归的过程

  • 压栈
    • 弹匣压入子弹
  • 出栈
    • 从弹匣(栈顶)拿出子弹

而上面的几个例子利用的就是方法栈

调用栈用来记忆“回到那”

  • 如果记忆过多,就会爆栈

如果降低压栈次数

  • 尾递归优化 (使用迭代代替递归)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
f = (n) => f_inner(2,n,1,0)

f_inner = (start, end, prev1, prev2) =>
start === end ? prev1 + prev2
: f_inner(start + 1, end , prev1 + prev2, prev1)
// 这就是尾递归

// 还可以写成循环
f = (n) =>{
let array = [0,1]
for(let i=0;i< n - 2;i++){
array[i+2] = array[i+1] + array[i]
}
return array[array.length - 1]
}
  • 记忆化函数

记忆化

可以大量减少重复计算

Lodash 实现

1
2
3
4
5
6
7
8
9
10
11
_.memoize = function(func, hasher) {
var memoize = function(key) {
var cache = memoize.cache;
var address = '' + (hasher ? hasher.apply(this, arguments) : key)
if (!has(cache, address))
cache[address] = func.apply(this, arguments)
return cache[address];
};
memoize.cache = {};
return memoize;
};

React记忆化

React.memo 减少计算

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
import React from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
const onClick = () => {
setN(n + 1);
};
const [n, setN] = React.useState(0);
return (
<div className="App">
<div>
<button onClick={onClick}>update {n}</button>
</div>
<Child />
<Child2 />
</div>
);
}

function Child() {
console.log("child 执行了");
return <div>Child</div>;
}

function Child_2() {
console.log("child2 执行了");
return <div>Child2</div>;
}

const Child2 = React.memo(Child_2);

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

解析

  • 如果你用函数式组件,那么每次setN 都会导致执行 Child() 里的打印
    • 如果 Child() 里有定义变量,那么也会重新执行,非常浪费
  • 优化就是使用 React.memo减少计算 记忆化组件(把结果缓存起来)

一个新的问题每次 onclick都会重新创建

1
2
3
4
5
6
7
const onClickChild = React.useCallback(() => {
console.log(m);
}, [m]);
// 第一个参数是 执行的函数
// 第二个参数是 它依赖于什么 如果 m 变了 则它重新执行

只要 m 不变 onClickChild 就不会重新执行

Node-JS专精01函数与闭包

函数是什么?

  • 最开始程序员写的大部分是汇编,后来有了 C / C++ 那时候没有函数,只有子程序
    • 比如有四行代码完成特定的功能
    • 给这四行代码起了一个名字 叫做函数
    • 但是那个时候不叫 函数而叫 子程序

子程序

  • 一个或多个语句组成
  • 完成特定功能
  • 功能相对独立

子程序又分三种

  • 函数(有返回值) function
  • 过程(无返回值) procedure
  • 方法 (类或对象中) method

JS 里只有函数和方法

因为JS的所有函数都有返回值,你不写return 但是返回值是 undefined

1
2
3
4
5
6
7
8
9
10
11
// 函数
function fn(){
console.log(1)
}

const a = fn() // undefined

// 方法
var obe = {
fn:fn //方法
}

数学领域里的函数

  • 数学中的函数
    • 两个集合之间的对应关系 如y = f(x) 比如方程式 y = a*x + b
      • X 叫做 定义域 Y 叫做 值域
    • 定义域里的每个元素都对应一项 值域中的元素
  • f()在数学领域是不合法的,因为没有输入
  • 函数式编程
    • 如果你的函数符合数学函数的定义,就是 函数式
1
2
3
4
5
如 f(x) = 3 * x +2
y = f(x)

x = 1 时 y = 5
x = 2 时 y = 8

但是 JS的函数不是数学函数

最大的区别就是,你给我一个值我可能给你返回不同的结果

1
2
3
4
5
6
7
var a = 0
function add(){
a += 1
return a
}
add() // 1
add() // 2

我们平时用的是哪个函数?

是编程里的子程序,还是数学里的函数?

  • 答案是 子程序

函数的返回值由什么确定?

影响因素

  • 调用时输入的参数 params
  • 定义时的环境 env

这个例子说明 a 是定义时的 a ,而不是执行时的 a

1
2
3
4
5
6
7
8
9
10
11
12
let x = 'x'
let a = '1'
function f1(x){
return x + a
}

{
let a = '2'
f1('x') // 值是多少?
}

// 答案是 'x1'

为什么不是 ‘x2’?

  • x 很好理解是 参数 所以 f1('x')x = 'x'
  • a 在这里不是参数 ,它是环境,它由 f1 所处的环境确定的
    • 如果你是参数 则在调用时 确定
    • 如果你是环境 则在 定义时确定

再看

1
2
3
4
5
6
7
8
9
10
11
12
let x = 'x'
let a = '1'
function f1(x){
return x + a
}
a = '3'
{
let a = '2'
f1('x') // 值是多少?
}

// 答案是 'x3'

再看

1
2
3
4
5
6
7
8
9
10
11
12
13
let x = 'x'
let a = '1'
function f1(x){
return x + a
}
a = '3'
{
let a = '2'
f1('x') // 值是多少?
}
a = '4'

// 答案是 'x3'
另一个例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let x = 'x'
let a = '1'
function f1(c){
c()
}
{
let a = '2'
function f2(){
console.log(x + a)
}
f1(f2) // 打印什么?
}

// 打印 'x2'
// 因为 x 和 a 都不是参数,所以只能看定义的地方,从内向外依次找
// 这个例子说明 a 是定义时的a ,而不是执行的 a

这个就是闭包,函数里面可以访问外面的变量

但并不是一定的,Ruby就不允许

1
2
3
4
5
6
7
8
9
10
11
12
# ruby 代码
def f1
a = 1 # 局部变量
def f2
print a # 打印 a
end
f2()
end

f1()

# 报错: NameError(undefined local variable or method 'a' for main: Object)

如果你就要在 ruby里访问局部变量,需要使用 lambda

1
2
3
4
5
6
7
8
def f1
a = 1 # 局部变量
f2 = lambda {print a} # 使用 lambda
f2.call()
end

f1()
# 成功 打印 1

闭包

函数里面可以访问外面的变量,那么这个函数 + 这些变量 = 闭包

闭包 + 时间

常见的考题

请问打印什么?

1
2
3
4
5
6
7
8
9
10
11
12
for(var i=0;i<6;i++){
setTimeout(
()=>console.log(i) //
)
}
// 打印 6个 6
// 因为六个函数共用了一个 i
// 这其实就是闭包

// setTimeout 是在 for循环之后执行的,所以循环已经结束了 那时 i = 6

// 如果你不理解它,那就回想一个成语 “刻舟求剑”

如何打印成 0 1 2 3 4 5呢?

  • 改用 let
1
2
3
4
5
6
7
8
9
for(let i=0;i<6;i++){
setTimeout(
()=>console.log(i)
)
}

// 打印 0 1 2 3 4 5

// let 的魔法:每次循环会把这个值复制一份, 然后把 这个值 分给这个闭包用
  • 立即执行函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
// 立即执行函数
(function(i){
console.log(i)
})(x)

// 立即执行函数
!function(j){
console.log(j)
}(i)
*/
for(var i=0;i<6;i++){
(function(x){
setTimeout(
()=>console.log(x)
)
})(i)
}

为什么?

  • 用 var 声明的时候 用的都是 一个 i,每个闭包用的同一个 i
  • 用了 let 后,每个闭包分配一个新的 i
结论

闭包的特点

  • 能让一个函数维持住一个变量
  • 但不能维持这个变量的值
  • 尤其是变量的值会变化的时候

对象是穷人的闭包

  • 对象也可以来维持一个变量
  • 如果一门语言不支持闭包,你可以用对象代理
1
2
3
4
5
6
7
8
9
10
11
12
13
var obj = {
i:0,
fn(){
console.log(this.i)
}
}

const handle = function(){
var i = 0;
return function(){
console.log(i)
}
}

闭包是穷人的对象

  • 如果一门语言不支持对象,你可以用闭包代理
1
2
3
4
5
6
7
8
9
10
11
12
// person 有 name age 属性

function createPerson(age,name){
return function(key){
if(key === 'name') return name;
if(key === 'age') return age;
}
}

var person = createPerson(18,'hjx');
person('name') // 'hjx'
person('age') // 18

Node-JS专精02this变态面试题

回顾基础知识,JS如何声明函数

1
2
3
4
5
6
7
const f1 = new Function('x','y','return x + y')

function f2(x,y){return x + y}

const f3 = function(x,y){return x + y}

const f4 = (x, y)=> x + y

其中,f1,f2,f3 是ES6之前的语法,支持 this,arguments,new
f4 是 ES6语法,不支持 this,arguments,new

你可以去 浏览器控制台里试试

1
2
3
4
const f2 = ()=>console.log(this)
f2() // window

f2.call({name:'hjx'}) // 还是 window

箭头函数不支持this

箭头函数如何处理 a ,就如何处理 this

1
2
3
4
5
6
const a = 233
const f2 = ()=>console.log(a)
f2() //

console.log(this)
const f2 = ()=>console.log(this)

箭头函数把 this 当作外部的变量,仅此而已,但是非箭头函数的 this 有很多特殊处理

箭头函数不支持this 指的就是尖头函数对 this 和其他变量一视同仁,不会特殊对待

非箭头函数的this

网传死记方法

  • this是上下文
  • 全局环境执行时候,this是全局对象
  • 调用对象方法时,this是该对象
  • 函数里调用函数时,this是全局对象
  • 箭头函数的this,不看调用,看定义(静态作用域)
  • 还有人说箭头函数里的 this 指向外面的this
  • 用new调用函数时,this是新增对象
  • 可以用call/apply/bind指定this

记忆力好可以这样记

  • 我记不住

那么this是参数还是环境?

  • 答案:this是参数,只有箭头函数 this是环境

如何确定this

显式this

1
2
3
fn.call(asThis,1,2)
fn.bind(asThis,1,2)()
obj.method.call(obj,'hi')

隐式this

1
2
3
4
5
6
7
8
fn(1,2)
// fn.call(undefined,1,2)

obj.method('hi')
// obj.method.call(obj,'hi')

array[0]('hi')
// array[0].call(array,'hi')

为什么你很难确定this,因为你总是让js帮你选this

所以你应该自己指定this

测试一下

1
2
3
4
5
button.onclick = function(e){
console.log(this)
}

// this 是参数, 答 button 是错的

答案是:不知道,要看怎么调用

因为你没看到call,没指定this,所以没法确定

1
2
3
4
5
6
7
8
9
10
11
12
13
button.onclick = function(e){
console.log(this)
}

// 当你手点的时候 是 button

// 如果js调用呢?
button.click()

// 而如果这样,就不是button
var fn = button.onclick
fn() // 打印的是 window
fn.call({name:'hjx'}) // {name:'hjx'}

标准答案

这个this的值无法确定,我们要看this是如何调用的,如果是用户点击按钮的时候浏览器调用的,那么浏览器一定会把button作为this传递进来,
如果是通过其他方式调用的,那么就要看它是如何调用的如以下代码:

1
2
var fn = button.onclick
fn()

由于fn没指定this所以相当于 fn.call(undefined) 那么浏览器就会把这个this变成全局对象 window

再来一个题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const vm = new Vue({
data:{
msg:'hi'
},
methods:{
sayHi(){
console.log(this.msg) // this是什么
}
}
})

答案依然是 不知道
如果你是在 vue正常情况下调用 这个this 就是 vm
但如果你是把 vm.sayHi放在其他地方调用就要看你调用的时候怎么传递参数
根据传参的形式不同,我才能确定this是什么
所以this是一个参数,只有在调用的时候唯一的确定

真实面试题

1
2
3
4
5
6
7
8
9
10
11
12
let length = 10
function fn(){ console.log(this.length) }

let obj = {
length:5,
method(fn){
fn()
arguments[0]()
}
}

obj.method(fn,1) // 输出什么,有陷阱

你可以拷贝代码到浏览器里试一下

分析以下,先把 fn()改写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let length = 10
function fn(){ console.log(this.length) }

let obj = {
length:5,
method(fn){
// fn()
fn.call(undefined) // 所以此时 this 实际是 window
}
}


obj.method(fn,1)
//所以打印的是 window.length
// 然而 let length 是不是 window 的 length?
// 答案是 没关系
// window.length 是什么?

window.length 默认指的是 当前页面有多少个窗口或者说多少个 iframe
// 你说这题是不是非常坑

再来分析arguments[0]()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 改写 arguments[0]()
// 实际就变成了这样 arguments.0.call(arguments)
let length = 10
function fn(){ console.log(this.length) }

let obj = {
length:5,
method(fn){
fn()
arguments[0]()
// arguments.0.call(arguments)
// 所以此时 this是 arguments
// arguments.length 是什么?
// 是形参的长度还是,实参的长度
// 答案是 arguments.length 是 实参的长度
}
}

obj.method(fn,1)

总结

  • 首先碰到这些this问题,一定要改写,你不改写就是坑自己

    1
    2
    3
    4
    5
    6
    7
    8
    fn(1,2)
    // fn.call(undefined,1,2)

    obj.method('hi')
    // obj.method.call(obj,'hi')

    array[0]('hi')
    // array[0].call(array,'hi')
  • this 是 call 的第一个参数

  • new 重新设计了 this/箭头函数不接受 this
  • 函数的返回值由参数和环境确定
  • this是参数,arguments也是参数
  • 全局变量和自由变量是环境

ZB-054-01自动化测试

自动化测试

为什么需要⾃动化测试

  • 你在接⼿⼀份前⼈留下来的代码的时候,你更喜欢:
    • ⼀份具有完善的⾃动化测试的代码,改完了跑⼀下⾃动化测试
    • ⼀份没有测试覆盖的代码,改完了听天由命

什么是⾃动化测试

  • ⼀段程序代码
    • 不提供具体的程序功能,只保证主要程序功能符合预期
      • 代码界的炊事班?
    • 编写需要(很⾼的)成本
  • 由机器负责的代码质量检查
  • 我们来看看著名的开源项⽬中的⾃动化测试

为什么需要⾃动化测试

  • 代码写完了
  • 怎么保证它是对的
  • 怎么保证它会永远对下去
  • ⻓久来看,没有测试的代码质量是⽆法保证的

测试的意义就是保证代码质量

测试种类有哪些?

常见的测试

  • ⼿⼯测试:最low的测试⽅式
    • 人肉点点点
  • 单元测试:快速检查⼀个类的功能
    • 只测这一个类的功能
  • 集成测试:检查整个系统的功能
    • 把手工的所有过程以代码的方式运行
  • 回归测试:检查新的代码有没有破坏旧的功能
    • 修一个bug把原来的bug触发了
  • 冒烟测试:快速检查代码有没有⼤问题
  • User Accepted Test:⽤户/甲⽅是否接受
    • 验收
  • ⿊盒测试:将整个系统看成⼀个⿊盒进⾏测试
    • 不关心内部细节
  • ⽩盒测试:根据系统的内部细节设计专⽤的测试
  • 压⼒测试:对系统施加⼀定的压⼒,确保系统的稳定性
    • 能扛住多少个用户,一个,10个,10000个?

丰富的自动化测试的仓库

Java中的测试形态

  • TestNG
  • JUnit(现在最流行的测试框架)
    • JUnit 4
    • JUnit 5

maven约定

  • src/main 生产代码
  • src/test 测试代码

IDEA里写一个测试demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.test.demo;

public class Add_Junit4 {
public static int add(int a, int b, int c){
return a + b + c;
}
}


// 按住快捷键 idea 默认的 (如果你没修改) 可以自动生成测试类
command + shift + t
此时你可以选择 测试框架的种类
这里只推荐
junit4 和 junit5

我们先用 junit4

junit4

  • 此时你可以添加 scope 了,因为这些代码仅在测试阶段使用
1
2
3
4
5
6
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.test.demo;

import org.junit.Assert;
import org.junit.Test;

// junit4 测试方式
public class Add_Junit4Test {

@Test
public void testAdd(){
// 断言 我期望值 3 和 Add_Junit4.add(1,1,1) 的结果是相等的
Assert.assertEquals(3, Add_Junit4.add(1,1,1));
}

@Test
public void testAddFail(){
// 断言 我期望值 4 和 Add_Junit4.add(1,1,1) 的结果是 "不相等的"
Assert.assertNotEquals(4, Add_Junit4.add(1,1,1));
}
}

junit5

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.4.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.4.2</version>
<scope>test</scope>
</dependency>

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.test.demo;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

class Add_Junit5Test {
@Test
public void testAdd(){
// 断言 我期望值 3 和 Add_Junit5.add(1,1,1) 的结果是相等的
Assertions.assertEquals(3, Add_Junit5.add(1,1,1));
}

@Test
public void testAddFail(){
// 断言 我期望值 4 和 Add_Junit5.add(1,1,1) 的结果是 "不相等的"
Assertions.assertNotEquals(4, Add_Junit5.add(1,1,1));
}
}

junit深入

  • 上面的test类的里每个 @Test的方法不是 static 而是 实例方法
    • 这个对象谁创建的?
    • 这个对象在哪里创建的?怎么看不到

答案是: junit框架帮你做的

  • 首先 junit 通过反射扫描@Test注解的地方,然后通过把它包出来
    • 可以通过在构造器哪里打断点,来验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Add_Junit5Test {

public Add_Junit5Test() {
System.out.println("Created!");
}

@Test
public void testAdd(){
// 断言 我期望值 3 和 Add_Junit5.add(1,1,1) 的结果是相等的
Assertions.assertEquals(3, Add_Junit5.add(1,1,1));
}

@Test
public void testAddFail(){
// 断言 我期望值 4 和 Add_Junit5.add(1,1,1) 的结果是 "不相等的"
Assertions.assertNotEquals(4, Add_Junit5.add(1,1,1));
}
}

Q:为什么测试方法是实例方法

  • 答案是:junit 通过反射偷偷帮你创建该类的实例

Q:已知我现在有两个测试用例

  • 原始类:注意add()已经是实例方法了!!!
  • 原始类:注意add()已经是实例方法了!!!
  • 原始类:注意add()已经是实例方法了!!!
1
2
3
4
5
public class Add_Junit5_1 {
public int add(int a, int b, int c){
return a + b + c;
}
}
  • 测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Add_Junit5_1Test {

Add_Junit5_1 adder = new Add_Junit5_1();
@BeforeAll
public static void setUpAll(){
System.out.println("在所有的测试用例运行之前运行且只运行一次");
}

@BeforeEach
public void setUp(){
System.out.println("初始化测试类的实例");
}

@Test
public void testAdd(){
Assertions.assertEquals(3, adder.add(1,1,1));
}

@Test
public void testAddFail(){
Assertions.assertNotEquals(4, adder.add(1,1,1));
}
}
  • 这两个测试实例所用的Add_Junit5_1类的 adder 对象是同一个吗?
  • 这两个实例如果是同一个对象,那么它们的调用顺序是怎样的?从上到下?还是从下到上?还是其他顺序?我能改变这个顺序吗?
  • 如果不是同一个对象,那么它们又是什么时候被分别创建的?

答案是:

  • 每次你运行的时候,在测试函数执行之前,junit 会通过反射帮我们创建 Add_Junit5_1类的实例 adder
  • 而且里面所有测试方法用到的Add_Junit5_1类的实例 adder 都是新的
    • 为什么?因为不希望测试方法之间互相干扰
    • 每个测试方法开始之前:期望这个实例对象 是全新的。跟其他测试方法是隔离的,没有依赖关系
  • debug运行 你会发现运行时候测试方法的执行顺序不是按照声明顺序
    • 因为这是java反射顺序不是按照声明顺序

测试的⽣命周期

  • Maven约定:所有放在src/test下的都是测试代码(在 maven test 阶段会被自动识别)
  • Maven的⽣命周期:
    • test
    • integration-test(集成测试)
  • 测试的时候到底发⽣了什么?(mvn test 之后发生了什么)
    • 新的JVM
    • 每个测试⽤例新建⼀个测试类的实例
      • 额外的操作
      • before
      • forEach
      • beforeAll

mvn test 后发生了什么

  • 首先在当前运行环境里:寻找 mvn 命令(mvn 的本质就是 拼一个java命令)
    • java 所有工具都是 帮你拼一个命令行
  • mvn 本质就是一个 jvm
    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
    开始走一个 maven 生命周期 
    clean --> validate --> compile --> test --> ...

    首先会启动一个 jvm,当走到 test 周期的时候它会 重新启动一个 jvm
    原因是 你的测试可能会破坏 jvm 某个属性,因此放在一个新的 jvm 是更安全的 ,隔离的


    验证它:开启新的 jvm

    第一步:在一个测试函数内 睡 60秒
    @Test
    public void testAddFail() throws InterruptedException {
    Thread.sleep(60 * 1000);
    ...
    }


    第二步:命令行运行 jps

    22385 surefirebooter7222466039461078627.jar
    22404 Jps
    20090 KotlinCompileDaemon
    19628
    19965 RemoteMavenServer
    21983 Launcher


    surefirebooter 就是默认测试插件新开的 jvm

编写单元测试

  • @Test
  • Assertion 断⾔
  • Mock 模拟
  • 测试⼀个类/⽅法是否符合预期

编写集成测试

  • 启动测试环境
  • 保证测试环境的隔离
  • 使⽤各类⾃动化⼯具模拟外界输⼊

运⾏⾃动化测试

  • mvn verify/test
  • mvn test -DskipTests

ZB-053-01操作系统和计算机原理

操作系统和计算机原理简介

计算机体系原理

  • 最基本的计算

计算机

  • 核心是一个CPU 中央处理器,它是计算机的大脑只有它负责计算,其它所有东西都是为它服务的
    • 穿孔纸带
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
通俗例子:
一个人(CPU)
手里拿着笔和一个小本本(阅读的说明书就是程序的指令)
他面前有几个临时的桶(寄存器) 大部分人做数学题的时候都会把东西抄在一个地方用于下次计算时使用


我们通过一个穿孔纸带,根据穿孔纸带 01二进制码识别指令,
往面前这个桶里放一个数字1,
然后看下一个机器码是什么
如 +2
就把刚才桶里的数加一下放进去


CPU就是一个很聪明的人
它手里拿的小本本——说明书 就是阅读机器码
第一条指令执行 xxx
第二条指令 若 xxx成立 执行 yyy操作

深入

1
2
3
4
5
6
7
8
9
10
CPU(一个聪明的人)
它可以阅读说明书
并且操作面前的内存

说明书实际也放在内存里
内存除了放说明书他还存放指令

因此我们称这种结构叫做 ——
冯·诺伊曼结构 https://zh.wikipedia.org/wiki/%E5%86%AF%C2%B7%E8%AF%BA%E4%BC%8A%E6%9B%BC%E7%BB%93%E6%9E%84
一种将程序指令存储器和数据存储器合并在一起的电脑设计概念结构

CPU不出错吗?

1
2
3
4
5
6
7
8
9
10
11
CPU里放了上亿的晶体管
用特殊的硬件逻辑完成我们的功能

比如有个东西叫做:与非门 1和0相与取非

CPU它会出错吗?
我们认为不会,但并不能保证它不会出错——一个著名CPU bug 1994 cpu
https://en.wikipedia.org/wiki/Pentium_FDIV_bug 浮点数错误
https://zh.wikipedia.org/wiki/%E5%A5%94%E8%85%BE%E6%B5%AE%E7%82%B9%E9%99%A4%E9%94%99%E8%AF%AF

就是用硬件电路预定义了一些除法算数了,不幸的是焊错了,导致这是一个硬件问题,无法通过软件来解决。

内存就是很长很长的连续数据存储空间,每个单元(字节)代表一个数据或者一个指令

  • 其他之外都是存储的

扩展链接:计算机基础 | 多核、缓存…现代CPU是如何工作的
为什么寄存器比内存快?

程序的装载和执行

1
2
3
每个程序有不同的说明书,如播放器,浏览器,QQ,微信,它们如何区分呢?

通过 把说明书和一些数据的打包叫做 可执行程序 如 .exe 文件

你在你电脑上运行 .exe文件时发生了什么

  • 首先,可执行程序里有一段是 说明书+一些数据 可能它只有说明书
    • 一个exe可以启动很多个进程
    • 假如你每次双击一些记事本就弹出一个窗口
    • 它每次开始执行,它会装载到内存中某个地方里去
    • 你在次点击记事本,就又弹出了一个窗口

这多个进程和 说明书本身有什么联系吗?

- 说明书本身指导了这个程序如何被启动和执行
- 但是这两个进程的数据是互不相同的

这就引出了物理和虚拟寻址

物理和虚拟寻址

你打开了多个记事本,CPU看着说明书 按着exe里的东西一行一行执行

首先这个exe装载了很多次,你打开了多个记事本,输入不同东西,对其他记事本的内容没任何影响。我们称之为虚拟寻址空间

虚拟地址空间

  • 在每个进程自己看来,它自己好像占据了所有内存,实际上是在漫长的内存里分配给它的一小节

假如你有个 32G内存的笔记本

32G的空间就是物理地址,每个程序里内部跳来跳去的地方叫做虚拟地址空间

  • 好处是:每个记事本之间互不干扰的执行,假如一个记事本崩溃了,不会影响其他记事本,同时每个记事本无法获取其他记事本的数据。

为什么一个程序无法跨平台?

CPU傻傻的执行,这个说明书的 .exe里的指令 一条一条的执行。。。

因此,一种CPU和另外一种CPU他们的说明书肯定是不同的。很容易理解这是因为他们的CPU架构不同

为什么最新的 win10还能运行 win95的一些游戏呢?

  • 最广泛的桌面架构x86架构 :它是一种指令集的东西
    • 只要指令集不变,它们就可以用同一份说明书

如果CPU完全相同都是x86为什么 win/linux/mac 它们的程序还不能共用

  • 因为在说明书中可能需要调用在操作系统里的一些相关东西,如windows图形接口这些在linux上没有
  • 一份说明书 .exe需要装载到内存中,这个内存的布局 不同系统是不一样的。

因此我们该怎么办呢?

于是我们有了 python/java 各种高级语言,在操作系统上维护的虚拟机。

它可以解释和当前操作系统说明书完全不同的另外一种说明书叫做——字节码

go是直接编译成 native的所以没法跨平台

这就是程序的装载和执行

程序启动的本质

从硬盘上的一份说明书,加载到内存里变成一个可以执行的程序

动态链接库

1
2
3
4
5
6
执行程序的时候,就是按照说明书的指令
12345~~。。。
有些时候这些指令是不全的
比如在执行某个指令时:说请去调用一个foo函数,但是这个foo函数不在本程序中,而在遥远操作系统的另一个地方

我们称之为 这个程序和它当前要使用的库分离的情况 叫做 动态链接库

好处

  • 把说明书某些关键功能拆出来,单独放在一个部分 省空间,提取公共

坏处

  • 我们在没有这个依赖的说明书运行我们的程序就会报错
    • 我们都知道玩游戏的时候有时候会弹出一个报错 xxx找不到
  • 版本问题
    • 这个游戏需要 一个库版本是0.5
    • 但是你机子安装的是 2.0
  • 最要命的是这个这个崩溃不会立即发生而是项目正常上线一个月之后突然崩了

有关动态链接库推荐看一本书

程序的分时复用

假如你只有一个CPU,就是你的电脑上同时只能有一个人可以不停的做各种运算,但是你却可以看网页,听音乐,下载东西。。。。

这就设计一个概念:分时复用

CPU的速度远远快于内存\IO\磁盘

  • CPU和内存就差了3个数量级
    • 内存做1件事,CPU能做1000件事

所以在CPU看来其他人都是慢的跟蜗牛一样,于是CPU说请在这个时间我去做点别的。

于是又一个概念就是 时间片轮转

  • 执行一会儿A程序,执行一会儿B程序,因为CPU是在太快了,这些程序的响应时间太慢了。
  • 所以在你看来,它好像同时在做很多件事,只是你的时间感知维度不在一个级别。
  • CPU所应对的其他外设是在太慢了

这就是CPU的基本工作流程

是谁可以占据它的时间片呢?

  • 进程/线程都可以

什么时候会放弃时间片

假如有个程序很难缠一直请求 CPU,给你带来直观的感受就是 卡顿。你的鼠标移动的时候都是受CPU控制的。

因此我们不能让任何一个程序如此霸道的占据CPU,于是有了调度

  • 进程的调度是 操作系统

看看你的任务管理器

这里有如此多的进程在进行,因此我们不能让其中的一个程序霸道的占据 CPU的所有,于是有了调度,根据每个程序的优先级来划分程序执行的时间。

挂起和中断

  1. 当你听着歌,开着ide,开着浏览器,开着word,
  2. 然后在 word里写下一段文字按下 ctrl + s 的瞬间,因为保存操作涉及文件的写入就是磁盘的读写 IO
  3. IO的时间非常慢,CPU很快 所以它不会等word写入后在弹出个保存成功
  4. 于是CPU在此时去(轮流)执行其他进程/应用
  5. 当文件成功写入磁盘后,发起一个中断
  6. CPU通过中断处理程序接收到这个中断信号来了之后,停下手中其他的程序,恢复word 程序的执行弹出 保存成功

上下文切换

  • 上下⽂context:程序运⾏的环境(寄存器、PC)临时的数据
  • 放弃CPU时,保存上下⽂
  • 拥有CPU时,恢复上下⽂
1
2
3
4
5
6
7
8
9
10
CPU是一个人
他面前又多个桶 寄存器

他面前还有一堆说明书(一堆应用程序)

操作系统告诉它 你现在该执行谁了。

CPU开始执行 ”说明书1“ 的时候,假如执行到第三条指令,此时“说明书1”的时间片到了,它要去执行 另一个“说明书2”的指令,当恢复执行 “说明书1” 的时候 CPU 如何知道 从那条指令开始?

一定需要一个东西 把 “说明书1” 当前执行的阶段保留,这个东西就叫 "program counter" 也就是 上下文 context

例子

  • CPU轮流执行说明书,当从 说明书2 切换到 说明书1 的时候
  • 要 保存 说明书2 的上下文
  • 此时 说明书1 拥有了 CPU ,恢复 上下文 继续执行
  • 当 “说明书1” 时间片到了, 就 保存 它的上下文
  • 此时 说明书2 拥有了 CPU ,恢复 上下文 继续执行
  • 如此循环往复

program counter 只有一个吗?

1
2
3
4
5
6
是的,program counter 每个CPU只有一个,这也是为什么上下文切换的原因
每当你从一个程序 切换到另一个程序的时候,你都需要把 当前的所有数据 保存到内存中
下次 在此运行这个 程序的时候 再从内存中恢复

只有当前CPU执行的程序的 pc才被 CPU 持有
其他程序的的 数据都保存在 内存里

这样频繁切换不会造成性能浪费吗?

  • 这也是为什么要有 协程 的原因
1
2
3
4
5
6
7
8
9
10
11
12
13
对于 3Ghz CPU
在切换上下文的过程中,大概需要10的4次方个时钟周期,尽管它很慢,但还是比 IO 快
虽然他很浪费,但这是不得已的。因为切换上下文必须这样做。

可以不用不那么浪费? 可以的,
我们希望一个线程自己在跑,它自己内部又维护一些分时复用的东西呢,并行的执行一些工作?
于是出了一个新的概念 —— 用户态线程

还有一个概念叫做 协程

一个线程单独又一个方法栈 大约 1M
而一个协程 大约 4KB 差距200倍
但是你需要自己写一套调度算法

协程是操作系统概念还是编程语言概念

  • 答案是 它是编程语言的概念

什么是操作系统

  • 欺上瞒下(承上启下):
    • 对上层提供统⼀的API,简化应⽤开发逻辑
      • 例如:通过Windows的CreateProcessA⽅法创建新的进程
      • 通过Unix的fork系统调⽤创建新的进程
      • 只要使用统一的 api
    • 对下层通过硬件驱动提供兼容性
      • 非常好的向后兼容

x86

来源于8086处理器 也就是 CPU
90年代,说一个电脑性能 会说它是386/486/586 的机器

因此当你下载一个软件的时候可以指明它CPU是什么的标准 如 ubuntu i586

驱动的概念

  • 比如显示器/鼠标
    • 假如你想正常显示你需要给我这个xxx端口发个信号

在此过程我们发现我们一直在抽象

  • 好处:一套代码,到处运行
  • 坏处:效率低。

计算机科学领域任何一个问题都可以通过添加一个中间层来解决

进程和线程

  • 共同点:
    • 都拥有独⽴的PC(程序计数器),可以独⽴地执⾏程序(性格测试题?)
    • 在Linux上甚⾄⽤同⼀种数据结构处理进程和线程
  • 进程:拥有独⽴的内存地址空间、⽂件等资源
  • 线程:和其他线程共享内存地址空间、⽂件等资源

你把电脑启动之后,打开 chrome,叮叮,vscode,网易云音乐,有道云笔记之后

一个CPU发现操作系统又若干个说明书,每个程序通过 分时复用 的方式执行各种各样的程序这是进程

线程

把其中某个进程 放大一下,如 java进程,一个程序本来只有一个计数器从上往下,当一个程序执行慢的时候,我想要加快速度,在这个进程内部再开一个小的说明书,我们称之为 线程

⽂件系统与IO

  • ⽂件的本质是字节流
  • ⽂件与⽂件指针
    • 读到哪一行,读某块内容
  • 缓存与缓冲区
    • IO很慢 从磁盘读到CPU是非常慢的

源代码是如何变成程序的

  • 远古时代:用针戳纸带
    • 剪贴和粘贴才是真的剪贴和粘贴
  • 机器帮你生成机器码:高级语言到机器码的过程——编译

从源代码到AST(编译器前端)

  • 源代码 -> tokens (解析成一个一个关键字)
  • tokens -> AST (关键字变成一个抽象语法树)

从AST到⽬标代码(编译器后端)

  • AST -> 字节码 (机器指令 0101…的内容)
  • AST -> 平台相关的⽬标代码(真正可以执行的说明书)

AST in JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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
let a = 1;
let b = 2;
let sum = (a,b)=>a + b;
sum(a,b);

// 会变成
{
"type": "Program",
"start": 0,
"end": 55,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 10,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 9,
"id": {
"type": "Identifier",
"start": 4,
"end": 5,
"name": "a"
},
"init": {
"type": "Literal",
"start": 8,
"end": 9,
"value": 1,
"raw": "1"
}
}
],
"kind": "let"
},
{
"type": "VariableDeclaration",
"start": 11,
"end": 21,
"declarations": [
{
"type": "VariableDeclarator",
"start": 15,
"end": 20,
"id": {
"type": "Identifier",
"start": 15,
"end": 16,
"name": "b"
},
"init": {
"type": "Literal",
"start": 19,
"end": 20,
"value": 2,
"raw": "2"
}
}
],
"kind": "let"
},
{
"type": "VariableDeclaration",
"start": 22,
"end": 45,
"declarations": [
{
"type": "VariableDeclarator",
"start": 26,
"end": 44,
"id": {
"type": "Identifier",
"start": 26,
"end": 29,
"name": "sum"
},
"init": {
"type": "ArrowFunctionExpression",
"start": 32,
"end": 44,
"id": null,
"expression": true,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 33,
"end": 34,
"name": "a"
},
{
"type": "Identifier",
"start": 35,
"end": 36,
"name": "b"
}
],
"body": {
"type": "BinaryExpression",
"start": 39,
"end": 44,
"left": {
"type": "Identifier",
"start": 39,
"end": 40,
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 43,
"end": 44,
"name": "b"
}
}
}
}
],
"kind": "let"
},
{
"type": "ExpressionStatement",
"start": 46,
"end": 55,
"expression": {
"type": "CallExpression",
"start": 46,
"end": 54,
"callee": {
"type": "Identifier",
"start": 46,
"end": 49,
"name": "sum"
},
"arguments": [
{
"type": "Identifier",
"start": 50,
"end": 51,
"name": "a"
},
{
"type": "Identifier",
"start": 52,
"end": 53,
"name": "b"
}
]
}
}
],
"sourceType": "module"
}

ZB-050-03docker部署相关链接

ZB-050-02springboot三种部署方式

java程序的部署

基于这个项目 https://github.com/slTrust/spring_ecosystemspring程序的部署分支

  • 你也可以跟着我从头到尾写 从master 开分支即可

项目的依赖

  • 依赖 mysql
  • 依赖 redis
  • nginx
    • 监听80端口
    • 负载均衡器,访问我们的三种部署方式
      • maven exec
      • jar
      • docker

三种方式部署

  • maven exec
  • jar 方式
  • docker 方式

项目先跑起来 按照readme.md即可

项目默认用的数据库是 h2 数据库,我们要换成 mysql

application.properties 里修改连接串

1
spring.datasource.url=jdbc:mysql://localhost:3306/match

由于已经按照 maven exec 插件 所以可以命令行启动

1
mvn compile exec:exec

直接报错 Unable to connect to localhost:6379 因为没开 redis

1
2
# docker 启动 redis
docker run -p 6379:6379 --name some-redis -d redis

再次运行 mvn compile exec:exec,报错 mysql 不存在

docker 启动 mysql 并持久化数据目录

1
2
3
4
5
6
7
8
9
# 这样你需要一个 数据库客户端连接之后 创建数据库 xdml
docker run -v "$PWD/data":/var/lib/mysql --name my-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -p 3306:3306 -d mysql
# 客户端 连上它
create database `match`;
# 这里有个坑 match 是 mysql的关键字
# 所以涉及 match 的地方要这样 `match` 本项目包含它的地方有 V1_CreateTables.sql 和 MyBatis 的 MyMapper.xml 里

# 参数指定连接的数据库
docker run -v "$PWD/data":/var/lib/mysql --name my-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -e MYSQL_DATABASE=match -p 3306:3306 -d mysql

pom.xml 里添加 mysql 的 maven依赖 ,修改 flyway的连接信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.17</version>
</dependency>

<plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
<version>6.0.7</version>
<configuration>
<url>jdbc:mysql://localhost:3306/match</url>
<user>root</user>
<password>my-secret-pw</password>
</configuration>
</plugin>

运行 数据库迁移命令

1
mvn flyway:migrate

继续运行项目mvn compile exec:exec 报错 Driver org.h2.Driver claims to not accept
jdbcUrl

意思是你的 driver 是 h2的 不是 mysql的

修改 连接信息

1
2
3
4
5
6
7
8
9
10
11
12
13
# 搜索 java mysql driver class
com.mysql.cj.jdbc.Driver


# 修改后的 application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/match
spring.datasource.username=root
spring.datasource.password=my-secret-pw
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
mybatis.config-location=classpath:db/mybatis/config.xml
spring.aop.proxy-target-class=true
spring.redis.host=localhost
spring.redis.port=6379

在此运行 mvn compile exec:exec 成功!!!

  • 这样就完成了第一种方式的部署

本机做域名映射

1
2
3
4
5
6
vi /etc/hosts
# 在里面添加

127.0.0.1 abc.com

# 此时你可以通过访问 http://abc.com:8080/rank2 达到我们的效果

nginx 做负载均衡

1
2
3
4
5
6
7
8
9
10
11
12
13
docker run --name my-custom-nginx-container -v /host/path/nginx.conf:/etc/nginx/nginx.conf:ro -d nginx

# 参数解释
# --name 就是docker里你这个应用的名字 可以不要
# -v 非常重要 这里是指定 nginx 配置的目录
# -d 代表使用那个镜像

我们要额外添加一个参数 ,防止 nginx挂了

--restart=always

# 完整命令 指定的是 当前目录的 nginx.conf文件
docker run --restart=always -v `pwd`/nginx.conf:/etc/nginx/nginx.conf:ro -d nginx

不懂 nginx的配置,没关系 googlenginx load balancer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
http {
upstream myapp1 {
server abc.com:8080;
server abc.com:8081;
server abc.com:8082;
}

server {
listen 80;

location / {
proxy_pass http://myapp1;
}
}
}

# 意思是
nginx监听 80端口
并把所有80端口访问转发到 http://myapp1 的应用
myapp1 就是开始定义的服务端口
# 实际上这里是有错的,为了加深记忆故意写错的
# server abc.com:8080; mvn exec方式
# server abc.com:8081; jar方式
# server abc.com:8082; docker方式

启动 nginx 运行 ,注意在你定义的 nginx.conf目录里执行

1
docker run  --restart=always -v `pwd`/nginx.conf:/etc/nginx/nginx.conf:ro -d nginx

通过docker ps -a 查看我们启动的应用,发现 nginx 一直在重启

  • 原因是 --restart=always 导致它只要失败一直重启

docker ngxin的排错

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
# 查看 nginx的 容器id 形如 xxx
docker ps

# 看日志
docker logs 容器id

# 日志显示
no "events" section in configuration

# google 搜索这个信息

https://stackoverflow.com/questions/54481423/nginx-startup-prompt-emerg-no-events-section-in-configuration

得到答案 添加 events { }

nginx.conf文件

events { }
http {
upstream myapp1 {
server abc.com:8080;
server abc.com:8081;
server abc.com:8082;
}

server {
listen 80;

location / {
proxy_pass http://myapp1;
}
}
}

重新启动 nginx

1
2
# 注意杀掉之前的 容器 / 还有你当前所在目录
docker run --restart=always -v `pwd`/nginx.conf:/etc/nginx/nginx.conf:ro -d nginx

尝试访问 abc.com

  • 因为 nginx监听 80端口 所以可以省略
  • 但是似乎失败了
    • 那就查看有没有监听我们的 80端口
    • 我们有犯错了,docker nginx的 80端口和 宿主机的80端口是隔离的,我们需要端口映射

运行 docker run --restart=always -vpwd/nginx.conf:/etc/nginx/nginx.conf:ro -p 80:80 -d nginx

访问 abc.com 得到了 502 bad gateway错误

  • 继续看log *1 no live upstreams while connecting to upstream, client: 172.17.0.1
    • 意思是上游的应用 没启动。
    • 继续看 nginx.conf,也感觉没问题
    • 进入 容器的内部看问题 docker exec -it 容器id bash
  • 原因是 容器内部是找不到 abc.com
    • 你肯定会疑问 不是在本机host里改了 域名映射吗?
    • 答案是 你的本机和 docker容器内部完全是八杆子打不着的两个计算机

docker常见问题 解决思路,灵魂三问

  • 当前发生问题在那台计算机里面,在这台计算机里有我需要的资源吗,它能访问到配置文件吗,它能访问这个host吗?
  • 这种问题有两种方式,一个是 把host或者文件 做映射
  • 一个是用 IP

于是你这样改了 nginx.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 实际上还是错的,因为这是 docker 的 127.0.0.1 仍然不是你本机的 ip
events { }
http {
upstream myapp1 {
server 127.0.0.1:8080;
server abc.com:8081;
server abc.com:8082;
}

server {
listen 80;

location / {
proxy_pass http://myapp1;
}
}
}

# 所以应该通过你的局域网ip才对
#查看你的ip
ifconfig
inet 开头的

继续修改 nginx.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
events { }
http {
upstream myapp1 {
server 192.168.31.176:8080;
server 192.168.31.176:8081;
server 192.168.31.176:8082;
}

server {
listen 80;

location / {
proxy_pass http://myapp1;
}
}
}

此时 访问 abc.com 成功

这样我们就完成了nginx的配置,nginx监听80端口把它接收到的请求转发到后端的8080端口

疑问,nginx 的转发什么时候转发到其他一个 端口上

官网已经说过了

1
2
3
4
5
The following load balancing mechanisms (or methods) are supported in nginx:

- round-robin — requests to the application servers are distributed in a round-robin fashion,
- least-connected — next request is assigned to the server with the least number of active connections,
- ip-hash — a hash-function is used to determine what server should be selected for the next request (based on the client’s IP address).

通过 jar方式启动你的应用

1
2
3
4
5
# 打包你的应用
mvn package

# jar方式启动,结果报错了, 因为 8080 端口被占用了(exec方式)
java -jar target/gs-spring-boot-0.1.0.jar

如何给 springboot 更换端口

  • google springboot change port
    • 这个答案说 修改配置文件(add application.properties) 或者 加一个-Dserver.port=8090 参数
    • 注意!!! 此时我们不能通过修改配置文件 因为 exec 的方式依赖那个配置文件

启动时 添加参数指定端口

1
java -Dserver.port=8081 -jar target/gs-spring-boot-0.1.0.jar

通过 docker方式启动spring

  • 根目录新建Dockerfile
1
2
3
4
5
6
7
8
9
10
11
FROM java:openjdk-8u111-alpine

RUN mkdir /app

WORKDIR /app

COPY target/gs-spring-boot-0.1.0.jar /app

EXPOSE 8080

CMD [ "java", "-jar", "gs-spring-boot-0.1.0.jar" ]
  • 生成镜像 项目根目录 docker build . 启动成功后得到一个 id 形如0931ab63f672
  • 启动应用(注意端口映射) docker run -p 8082:8080 0931ab63f672
  • 继续访问abc.com/rank2 访问失败了,redis 6379有问题
    • docker里的 spring访问的localhost是 docker的localhost

如何把数据库或者一些配置信息传递进 docker启动的 springboot程序里呢?

spring通过外部导入配置

spring 加载配置(application.properties)的顺序是

1
2
3
4
5
6
SpringApplication will load properties from application.properties files in the following locations and add them to the Spring Environment:

1. A /config subdir of the current directory
2. The current directory
3. A classpath /config package
4. The classpath root

文件映射

  • 新建 src/main/resources/deploy/application.properties
  • 内容仅仅把 localhost改成你的 ip地址
1
2
3
4
5
6
7
8
spring.datasource.url=jdbc:mysql://192.168.31.176:3306/match
spring.datasource.username=root
spring.datasource.password=my-secret-pw
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
mybatis.config-location=classpath:db/mybatis/config.xml
spring.aop.proxy-target-class=true
spring.redis.host=192.168.31.176
spring.redis.port=6379
  • 进入这个目录
  • docker 添加 -v
1
docker run -v `pwd`/application.properties:/app/config/application.properties -p 8082:8080 0931ab63f672

至此,我们就完成了三种方式部署 springboot程序

项目说明

项目地址

依赖内容

  • docker
    • redis
    • mysql
    • java
    • nginx
  • idea

项目启动

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
# 00修改 本地 host
vi /etc/hosts
# 添加
127.0.0.1 abc.com

# 01启动redis
docker run -p 6379:6379 --name some-redis -d redis

# 02启动 mysql
docker run -v "$PWD/data":/var/lib/mysql --name my-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -p 3306:3306 -d mysql

# 03通过数据库客户端连接进入 并创建 match数据库
create database `match`;

# 04初始化数据,项目根目录运行
mvn flyway:migrate

# 05启动 nginx,进入项目 src/main/resources/deploy 目录执行如下命令
# 注意ip地址 要更好为你的电脑的 ip地址
docker run --restart=always -v `pwd`/nginx.conf:/etc/nginx/nginx.conf:ro -p 80:80 -d nginx

# 06exec方式启动 项目根目录运行
mvn compile exec:exec

# 07jar方式启动 项目
# 项目根目录运行 打包命令
mvn compile
# 运行 jar命令 注意指定端口
java -Dserver.port=8081 -jar target/gs-spring-boot-0.1.0.jar

# 08 docker方式启动 项目根目录运行
# 构建镜像,会生成一个 镜像id 你也可以通过参数给镜像命名
docker build .
# 进入 src/main/resources/deploy目录 用外部的 配置文件 覆盖 你打包jar文件的 配置
# 把 application.properties 文件的 ip 修改为你的ip
# 根据刚才生成的镜像id启动应用,注意目录在 根目录的 src/main/resources/deploy里运行
docker run -v `pwd`/application.properties:/app/config/application.properties -p 8082:8080 0931ab63f672

# 09访问 abc.com

ZB-050-01java程序的部署

分布式概念

  • 中国最大的网游:淘宝每天晚上有8千万用户

当用户访问过怎么办?

垂直扩展

  • 90年代通用方法:买更好的机器

水平扩展

  • 加机器
    • 能加机器解决的问题都不是问题

水平扩张的方式

  • 负载均衡
  • 容灾(单点故障)

如何保证数据的唯一性?

  • 单一数据源
    • 统一连接一个数据库
    • 数据库挂了怎么办?
      • 分库分表
      • 基本大部分公司每有达到这个量级就死掉了
      • 达到了,再去请专门的DBA也来得及

一台机器的时候

把开发好的程序,部署到服务器上

发布和部署Java程序

  • 使之在生产环境下可以运行
  • 需要解决的问题
    • 你自己编写的代码怎么放到服务器上
    • 你所依赖的第三方库
    • 你所依赖的特殊环境配置(数据库/缓存等)
    • 稳定性(挂了怎么办)
    • 升级和回滚

实战:使用 Maven exec plugin

  • 自动将所有的传递性依赖假如
  • 优点
    • 简单
  • 缺点
    • 不适用于自动化的场景

001MyDemo.java

1
2
3
4
5
6
7
8
9
public class MyDemo {

public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
Thread.sleep(100);
System.out.println(i);
}
}
}

002点击绿色运行按钮后,首行会显示这样一堆命令

1
/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/bin/java "-javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=53152:/Applications/IntelliJ IDEA.app/Contents/bin" -Dfile.encoding=UTF-8 -classpath /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/dt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/javafx-mx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/jconsole.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/packager.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/tools.jar:/Users/admin/Desktop/bsdemo01/target/classes com.bs.demo.MyDemo

003安装 maven exec插件在 pom.xml里

1
2
3
4
5
6
7
8
9
10
11
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.6.0</version>
<configuration>
<executable>java</executable>
<arguments>
<argument>com.bs.demo.MyDemo</argument>
</arguments>
</configuration>
</plugin>

004运行mvn exec:exec

结果出错了 说找不到 xxx

运行 mvn exec:exec -X 可以看详细信息

而你在命令行运行 java com.bs.demo.MyDemo 也会报错

那是因为你没有告诉他 classpath

005 修改mvn exec 参数

  • 添加 -classpath 参数来让maven自动把 classpath包含进来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.6.0</version>
<configuration>
<executable>java</executable>
<arguments>
<argument>-classpath</argument>
<!-- automatically creates the classpath using all project dependencies,
also adding the project build directory -->
<classpath/>
<argument>com.bs.demo.MyDemo</argument>
</arguments>
</configuration>
</plugin>

006此时 你可以直接mvn exec:exec 来运行你的程序

mvn exec:exec的缺陷

  • 你把 target目录删了在运行就报错了,因为它不会帮你编译
  • 你不可能把源代码搞到服务器上去 然后编译 然后在 mvn exec:exec

jar包方式部署

实际就是编译后的代码资源打包 mvn package

什么样的jar包可以直接运行

  • jar包 和 Manifest(清单文件,最重要的就是一个 main class,入口文件)

优点

  • 简单可靠
    • java -jar 你的jar包

缺点

  • 依赖于JVM环境
    • 你本地和线上的jvm版本不一致
jar包和 war包区别

jar包

  • 内嵌tomcat
  • 可以直接运行

war包

  • 包含了所有的依赖
  • 不能直接运行,没有 tomcat
  • 需要放在Servlet容器里运行

jar包 = war包 + tomcat

ZB-049-01docker原理

Docker原理与应⽤

Docker改变了软件世界

Docker出现之前…

  • 软件在操作系统上如何工作的?
  • 如何交付软件?
  • 如何部署软件?

各种环境问题,版本,软件依赖,那个时候我们使用的是虚拟机

每次都是重新安装虚拟机。

  • 它很重,安装很慢。
  • 独立的操作系统,非常吃内存
  • 基本你开几个虚拟机内存就都被虚拟机吃掉了

Docker出现之后

启动一个 ubuntu

1
2
docker run ubuntu
# 真正的秒开

Docker能做什么?

  • 保证开发、测试、交付、部署的环境完全⼀致
  • 保证资源的隔离
  • 启动临时的、⽤完即弃的环境,例如测试
  • 迅速(秒级)超⼤规模部署和扩容

Docker的基本概念

核心有两个 image 和 container

  • 镜像 image
    • ⼀个预先定义好的模板⽂件,Docker引擎可以按照这个模板
      ⽂件启动⽆数个⼀模⼀样,互不⼲扰的容器
  • 容器 container
    • ⼀台虚拟的计算机,拥有独⽴的:
      • ⽹络
      • ⽂件系统
      • 进程
    • 默认和宿主机不发⽣任何交互
      • 意味着数据是没有持久化的!

查看所有的镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
> docker images
# 显示出所有的配置清单
REPOSITORY TAG IMAGE ID CREATED SIZE
jenkins/jenkins latest ec2bfee7c206 4 weeks ago 553MB
ubuntu latest 775349758637 3 weeks ago 64.2MB
mysql latest c7109f74d339 5 months ago 443MB
hello-world latest fce289e99eb9 11 months ago 1.84kB

# mysql 就是一个镜像,一份说明书
# 每当你运行这句话的时候
< docker run -d ubuntu
> 7104470ea1e97e7596517da9e9a50b352e3f9e6867647d4789162e8b8dec369b 容器的id
# docker都会安装这个说明书(清单), 帮你组装一个机器,变成一个电脑的实例

启动三个一模一样的mysql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 如果启动失败使用
docker run -it mysql 来启动一个交互式的命令行 让你知道哪里出了问题

# 你可以连续启动 mysql -e代表指定环境变量
< docker run -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql
> 2b3c2df780ea82cfa28f2a0565fe3f788cb298d8e87a6485f071fd83245beabc

< docker run -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql
> a91be367d2e16b161fc79e486b7ab99a21b6953aabf8876b93080a72ba0b518a

< docker run -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql
> 2a081daaa8628e19bbd55c174712f604a1f85ab511b4581332ea505270db02c5

# 查看你启动的 容器
< docker ps
> # 此时 启动⽆数个⼀模⼀样,互不⼲扰的容器
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2a081daaa862 mysql "docker-entrypoint.s…" 2 minutes ago Up 2 minutes 3306/tcp, 33060/tcp jovial_bell
a91be367d2e1 mysql "docker-entrypoint.s…" 2 minutes ago Up 2 minutes 3306/tcp, 33060/tcp youthful_perlman
2b3c2df780ea mysql "docker-entrypoint.s…" 2 minutes ago Up 2 minutes 3306/tcp, 33060/tcp friendly_ramanujan
docker pull/images
  • 下载⼀个指定的镜像,⽅便随时启动
  • docker pull mysql:5.7.28 下载指定镜像
  • docker images 查看本地已有的镜像

下载指定的镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
docker pull mysql:5.7.28

# 比你以前安装软件的方式 非常快
docker pull 镜像的名字:tag

# tag可以不加,意思是下载最新版本
docker pull mysql
# 等价于
docker pull mysql:latest


registry.cn-beijing.aliyuncs.com/dr1/hcsp:0.0.16
镜像仓库 registry.cn-beijing.aliyuncs.com/ 不加就会从 docker的中央仓库下载
镜像名 hcsp
tag 0.0.16

docker run/ps

  • docker run 装载镜像成为⼀个容器
    • 就好像从蛋糕模⼦做出来⼀个蛋糕
    • 在这个容器看来,⾃⼰就是⼀台独⽴的计算机
    • 每个容器有⼀个ID,⽀持缩写
  • docker run -it <镜像名> <镜像中要运⾏的命令和参数>

    • 交互式命令⾏,当前shell中运⾏,Ctrl-C退出
    • 就是在当前的命令行里启动一个容器并和这个容器进行交互(进入这个容器内部)
      1
      2
      3
      4
      5
      docker run -it mysql
      等价于进入 mysql 的命令行

      docker run -it ubuntu echo "hahaha"
      进入 ubuntu 命令行环境并 执行 echo "hahaha"
  • docker run -d <镜像名> <镜像中要运⾏的命令和参数>

    • daemon模式,在后台运⾏
      • 立刻返回,给你给id ,在后台默默的运行,
      • 长期执行的后台进程,如 mysql/redis

删除一个容器

1
2
3
4
5
6
7
8
# 这个id就是 容器id 可以缩写
docker rm id
# 有时候会提示 你的容器还在运行不能直接删除,此时你应该 先停止这个容器 然后在删除
docker stop id
docker rm id

# 当然你还可以这样 强制删除
docker rm -f id

docker run

  • --name 为容器指定⼀个「名字」
  • --restart=always 遇到错误⾃动重启
  • -v <本地⽂件>:<容器⽂件>
  • -p <本地端⼝>:<容器端⼝>
  • -e NAME=VALUE

给容器起一个名字 --name

  • 注意区分docker参数 和容器参数,他们有强烈的区分
    1
    2
    3
    4
    docker run --name my-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -e MYSQL_DATABASE=xdml -p 3306:3306 -d mysql

    -d mysql 之前是 docker的参数
    -d mysql 之后是 给 mysql的参数
1
2
3
4
5
6
7
8
9
10
docker run --name my-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw  -d mysql
# 此时你通过 镜像(清单) mysql 启动了一个容器
# 这个容器名字为 my-mysql
# 指定它的环境变量参数 MYSQL_ROOT_PASSWORD=my-secret-pw

此时你可以通过这个 名字来对 容器进行操作了 ,也可以用它的id
docker run -it my-mysql
docker run -it id

docker rm -f my-mysql

通过-v 与宿主机产生联系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 进入shell 
# 当前路径 ~/Desktop
# 我在 ~/Desktop上新建文件 1.txt 内容 aaaaaa
# cat 1.txt >> aaaaaa
# 把宿主机上的文件 映射给 容器上的某个文件
< docker run -it -v `pwd`/1.txt:/1126/aaa.txt ubuntu
# 进入 docker启动的容器 ubuntu命令行环境
< cat /1126/aaa.txt
> 得到 aaaaaa
# 修改 /1126/aaa.txt的内容
< echo "hahaha" > /1126/aaa.txt

# 退出 ubuntu 命令行
> exit
# 此时回回到 宿主机 shell的命令行环境 也就是 ~/Desktop
< cat 1.txt
> hahaha # 内容被成功修改了

文件读写权限也会映射吗?

不会,取决于你的dockerfile怎么定义的

-p 端口映射

1
2
3
4
5
6
7
8
# 这样会在docker里启动一个 mysql 默默的运行在后台
docker run --name my-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql

# 你是无法访问它的 ,因为它运行在 docker的 3306端口上,跟宿主机是隔离的

# 你只能通过 -p 的端口映射来访问它
docker run -v "$PWD/data":/var/lib/mysql --name my-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -e MYSQL_DATABASE=xdml -p 3306:3306 -d mysql
# 将 宿主机的 3306端口 映射到 docker 容器内的 3306端口上

这样你就可以实现分布式部署

1
2
3
4
你的spring01应用
8080:8081
你的spring02应用
8080:8082

-e 传递环境变量

1
2
3
< docker run -e AAA=BBB -it ubuntu
< echo AAA
> BBB

一般用来传递参数,如mysql,传递初始化参数

1
docker run -v "$PWD/data":/var/lib/mysql --name my-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -e MYSQL_DATABASE=xdml -p 3306:3306 -d mysql

docker start/stop

  • 启动/停⽌⼀个容器
  • 可以想象为开关机

docker rm

  • 删除⼀个容器
  • 想象成将电脑丢掉

docker exec

指定⽬标容器,进⼊容器执⾏命令

  • docker run -it <⽬标容器ID> <⽬标命令(通常为bash)>
  • 可以「想象」成ssh
  • 调试、解决问题必备命令
1
2
3
4
假设你mysql 做目录映射来持久化数据的时候 目录写错了,但是你自己不知道

# 你就只能通过这样来操作了
docker exec -it my-mysql bash

盗梦空间可能吗?

一层一层的梦

1
docker in docker 有可能的 但是非常不推荐

docker logs

通过id去看一个容器的日志

  • docker logs <容器ID或容器名>
  • 查看⽬标容器的输出
  • docker logs -f <容器ID或容器名>

docker inspect

  • ⾼级命令,可以⽆视
  • 查看容器的详细状态

删除镜像

1
docker rmi 镜像id

dockerfile和镜像仓库

分层的镜像

  • 为了复用
    1
    2
    假如你有两个应用 A 和 B 他们都基于 ubuntu
    这样他们就可以都共用一个镜像

Dockerfile

  • 指定镜像如何⽣成
  • 编写第⼀个Dockerfile
  • docker build .
  • 每个镜像会有⼀个唯⼀的ID

编写一个简单的 dockerfile

1
2
3
mkdir my-dockerfile
cd my-dockerfile
vi Dockerfile

Dockerfile 内容如下

1
2
3
4
5
6
7
8
# 指定基础镜像 如果没有就去下载
FROM ubuntu:16.04
# 按照 nginx
RUN apt-get update && apt-get install -y nginx

RUN echo "HHHH" > /usr/share/nginx/html/index.html
# 暴露80端口
EXPOSE 80
  • 运行docker build . 构建你的镜像 (通过装机清单构建我们的镜像)
  • 我们的镜像没有起名字 所以只能通过 id 启动
    1
    2
    3
    4
    5
    # 启动你的镜像
    docker run -p 8080:80 -d 你的id

    # 启动并进入镜像内部
    docker run -p 8080:80 -it 你的id

Docker的镜像仓库与tag

  • 可以任意对镜像进⾏tag操作
  • 决定了未来这个镜像会被push到哪⾥
  • 决定了未来从哪⾥下载镜像
  • 可以⽅便的创建镜像仓库的私服
  • --registry-mirror
  • --insecure-registry

给你的镜像起个名字

1
2
3
4
5
6
docker tag 镜像id  镜像名字:镜像tag

docker tag 77 hahaha:123

注意这个名字 不是随便起的,它决定了未来这个库 push 从哪进行
指明了 未来 push的地址 可以是中央仓库 可以是 私服

Docker与K8s

Docker 与 Kubernetes

  • Kubernetes 可以做到一个应用启动 n 个同一镜像,
  • 还可以实现应用版本的更新(滚动更新)
    • 应用从 0.0.1 –》 0.0.2的更新
    • 假设你的应用有100个 他会杀掉几个 更新成最新版本,依次迭代到所有的版本都更新了

没法继续扩展了,基本只有大公司用的到

小公司达不到这个量级。小公司撑死启两个容器就不错了

参考链接

实战一下本地部署 spring-boot

  • 参考文章
    • 注意它的docker run -t hxy -p 8081:8081 是有问题的
    • 你要 先 -p 8080:8080 -d 你的id 形如 docker run -p 8081:8081 -d 镜像id
  • 参考Dockerfile为这个安装更快
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    FROM java:openjdk-8u111-alpine

    RUN mkdir /app

    WORKDIR /app

    COPY target/gs-spring-boot-0.1.0.jar /app

    EXPOSE 8080

    CMD [ "java", "-jar", "gs-spring-boot-0.1.0.jar" ]

01 直接拉代码spring_issue_api

02 idea打开运行 mvn package

03 根目录新建 Dockerfile文件

1
2
3
4
5
6
7
8
9
10
11
FROM java:openjdk-8u111-alpine

RUN mkdir /app

WORKDIR /app

COPY target/gs-spring-boot-0.1.0.jar /app

EXPOSE 8080

CMD [ "java", "-jar", "gs-spring-boot-0.1.0.jar" ]

04 运行docker build -t hjx .

  • -t 代表 tag 就是你可以用这个名字来启动它

05 docker run -p 8080:8080 -d hjx