导图社区 【架构师】技术图谱
【架构师】技术图谱,包括Java、go语言、前端、业务场景问题、生产问题调查、devops等等。
编辑于2022-11-09 20:49:54时间管理-读书笔记,通过学习和应用这些方法,读者可以更加高效地利用时间,重新掌控时间和工作量,实现更高效的工作和生活。
本书是法兰教授的最新作品之一,主要阐明了设计史的来源、设计史现在的状况以及设计史的未来发展可能等三个基本问题。通过对设计史学科理论与方法的讨论,本书旨在促进读者对什么是设计史以及如何写作一部好的设计史等问题的深入认识与反思。
《计算机组成原理》涵盖了计算机系统的基本组成、数据的表示与运算、存储系统、指令系统、中央处理器(CPU)、输入输出(I/O)系统以及外部设备等关键内容。通过这门课程的学习,学生可以深入了解计算机硬件系统的各个组成部分及其相互之间的连接方式,掌握计算机的基本工作原理。
社区模板帮助中心,点此进入>>
时间管理-读书笔记,通过学习和应用这些方法,读者可以更加高效地利用时间,重新掌控时间和工作量,实现更高效的工作和生活。
本书是法兰教授的最新作品之一,主要阐明了设计史的来源、设计史现在的状况以及设计史的未来发展可能等三个基本问题。通过对设计史学科理论与方法的讨论,本书旨在促进读者对什么是设计史以及如何写作一部好的设计史等问题的深入认识与反思。
《计算机组成原理》涵盖了计算机系统的基本组成、数据的表示与运算、存储系统、指令系统、中央处理器(CPU)、输入输出(I/O)系统以及外部设备等关键内容。通过这门课程的学习,学生可以深入了解计算机硬件系统的各个组成部分及其相互之间的连接方式,掌握计算机的基本工作原理。
【架构师】技术图谱
go语言
安装
官网:https://golang.google.cn/dl/ 下载二进制压缩包
解压后,得到一个“go”目录
把“go”目录放入指定位置
在Linux系统中放在/usr/local 目录下 /usr/local/go 即为Go语言的安装根目录
在Windows系统中,放在C:\目录下 c:\go 即为Go语言安装根目录
环境变量
GOROOT: 包含Go语言的安装根目录的路径
GOPATH: 包含若干个工作区目录的路径
GOBIN: 包含用于放置Go程序生成的可执行文件的目录的路径
代码组织:
工作区
src子目录:放置源码文件的目录
pkg子目录:防护归档文件的目录
bin子目录
放置可执行文件的目录
仅在未设置GOBIN时有用
代码包
与src子目录下的某个子目录——对应
可通过代码包导入路径来导入到源码文件 代码包导入路径即为从src子目录到该包对应目录的相对路径
同一个代码包中的源码文件和代码包声明语句都应该一致
代码包名称可与其对应的目录名称不同
安装源码文件时以代码包名称为准
导入代码包时以代码包名称为准
强烈建议两个名称一致
源码文件
命令源码文件
独立程序的入口
属于main包,包含无参数无结果的main函数
可通过go run 命令运行,可接受命令行参数
main函数执行的结果就意味着当前程序运行的结束
同一个代码包中不要放多个命令源码文件
命令源码文件与库源码文件也不要放在同一个代码包
构建
构建后可生成可执行文件 可执行文件即executable file
可在命令航中运行的文件
在Windows中就是扩展名为“exe”的文件
在Linux中一般无扩展名
生成位置在命令执行目录
安装
安装后生成可执行文件
生成位置在当前工作区的bin子目录或GOBIN包含的目录
库源码文件
专用于放置可供其他代码使用的程序实体
构建
作用在于检查和验证
构建后只生成临时文件
在操作系统的临时目录下
开发者一般可以不关心
安装
安装后生成归档文件 归档文件即archive file
扩展名为“a”的文件
即为静态链接库文件
生成位置在当前工作区的pkg子目录
测试源码文件
功能测试源码文件
测试函数名称 TestXXX
测试函数签名 (t. *testing.T)
性能(基准)测试源码文件
测试函数签名 (b * testing.B)
示例(样本)测试源码文件
测试函数名称 ExampleXXX
测试函数签名 <没有硬性要求>
测试函数期望输出
放置在函数末尾
用注释行表示
形如 // Output: xxx
代码块
由花括号扩起来的区域 可包含表达式和语句
层层嵌套,相当于大圆套小圆
每个代码块最多只会有一个直接父级代码块,后者称为前者的外层代码块
分类
全域代码块 无父级代码块
源码文件代码块
语句代码块
if
for
switch
select case
自定义代码块
空代码块 {}
作用域
体现了程序实体(特别是局部变量)的作用范围
可与代码块对应起来
(可)重名变量
内层代码块中的变量与外层代码块中的变量重名了,即称为(可)重名变量
内外层代码块中的变量若重名就可能产生“屏蔽”现象
在内层代码块中,内层代码块的变量“屏蔽” 外层代码块的重名变量
程序实体
变量、常量、函数、结构体和接口的统称
需要先声明再使用,有必要时还需要先进行初始化
由标识符作为唯一标识和名称,不同代码包中的程序实体可同名
引用其他代码包的程序实体时要先导入该代码包
引用其他代码包的程序实体时需要使用限定符
只能引用其他代码包中公开的程序实体
程序实体的访问权限
名称首字母大些的程序实体就是公开的
名称首字母小写的程序实体就是包级私有的
internal包
与其直接父级代码包中所有代码共同组成了一个模块
该包中的名称首字母大写的程序实体都不是公开的,而是模块级私有的
其中代码的可见性
其直接父级代码包中的所有代码均可见
其直接父级代码包之外的代码均不可见
字面量
值字面量
用于表示值本身的字符序列
分类
基本(值)字面量
布尔字面量
true
false
整数字面量 123
字符字面量 ‘中’
浮点数字面量 3.14
复数字面量 1.2e-3i
字符串字面量 “中国”
复合(值)字面量
数组字面量 [3] int {1, 2, 3}
切片字面量 [] int {1, 2, 3}
字典字面量 map[int] string {1: "a", 2: "b", 3: "c"}
函数字面量 func (x, y int) int { return x + y }
结构体字面量 struct { name string age uint} {"郝林", 36 }
整数字面量本身是无类型的,不过:
赋给某个变量,如:
var num int8 = 1 存在显式的类型制定,因此num的类型是int8
var num = 1 整数字面量的默认类型是int,因此num的类型是int
有显式的类型转化,如: uint16(1) 此表达式的(结果)类型是uint16
类型字面量 用于表示(自定义)类型本身的字符序列 示例
type Person interface { Name() string Age() uint }
chan Person
*Person
常量
常量的类型可以是基本类型,可被赋予对应的基本(值)字面量
常量表达式
仅以常量作为操作数的表达式
合法的操作数
无类型的布尔常量
无类型的数值常量
父类型的字符串常量
结果
整数+浮点数 浮点数常量
整数/浮点数 浮点数常量
字符+整数 字符常量
整数或浮点数位移 整数常量
比较大小 布尔常量
常量声明
const num = 1
iota
代表连续的、无类型的整数常量
以const开始的常量声明语句为单位
从0开始,每赋给一个常量就递增一次
一旦跨越以const开始的常量声明语句就归零
固定步幅赋值 const ( a = 1 << iota b c )
变量
声明方式
var声明
显式指定类型
不显式指定类型
短变量声明
仅允许在函数中出现
拥有不显式指定类型的var声明的优势
变量重声明
同一代码块中
使用短变量声明
声明时必须要有新变量
类型判断 类型断言表达式
建议将结果赋给两个变量 第二个变量代码是否断言成功
只将结果赋给一个变量 有可能会引发panic
类型转换 类型转换表达式
转换规则 https://golang.google.cn/ref/spec#Conversions
注意事项
数值类型值之间的转换 可能会发生截断
整数类型值与字符串类型值之间的转换
类型
内建数据类型
常规分类
基本数据类型
布尔类型 bool
数值类型
整数类型
byte
rune
int/uint
int8/uint8
int16/uint16
int32/uint32
int64/uint64
浮点数类型
float32
float64
复数类型
complex64
complex128
字符串类型 string
高级数据类型
数组 array
切片 slice
字典 map
通道 channel
函数 function
结构体 struct
接口 interface
指针 *Xxx、unsafe.Pointer、uintptr
按底层结构分类
值类型
所有的基本数据类型
数组
结构体
引用类型
切片
字典
通道
函数
接口
指针
特殊概念
别名类型
潜在类型
类型再定义
操作符
共21个
分类
算术操作符 7^13
比较操作符 "A" < "C"
逻辑操作符 condition1 || condition2
地址操作符
&originalValue
*pointerValue
接受操作符
intChan <-1
优先级
一元操作符优先级最高 +-^*&!<-
二元操作符优先级
最高 */%<<>>&&^
次高 +- | ^
中等 == != < <= > >=
次低 &&
最低 | |
表达式
把操作符和函数作用于操作数的计算方法
分类
算术表达式 123+456
选择表达式 myStruct.name
索引表达式 intSlice[0]
切片表达式
intSlice[0:2]
intSlice[0:2:4]
类型断言表达式
myPet.(Dog)
interface{}(myDog).(Animal)
类型转换表达式 uint16(123)
调用表达式 myTpye.Name()
特色语句
go 语句
if 语句
for 语句
switch 语句
select 语句
defer 语句
错误处理
error 接口
errors.New函数
fmt.Error 函数
自定义错误类型
自定义错误值
panic 函数 与error接口及其实现类型值的联用
recover 函数 与defer语句的联用
前端
基础
html
html 5的新特性
语义化标签
<header> 文档头部区域
<footer> 档的尾部区域
<nav> 导航
<section> 节(section、区段)
<article> 独立的内容区域
<aside> 侧边栏内容
<detailes> 描述文档或文档某个部分的细节
<summary> 标签包含 details 元素的标题
<dialog> 定义对话框,比如提示框
2. 增强型表单
(1)新的表单输入类型
color date(从一个日期选择器选择一个日期) datetime datetime-local email month number range(一定范围内数字值的输入域) search tel time url week(选择周和年)
(2)新表单元素
<datalist> 规定输入域的选项列表
<keygen> 提供一种验证用户的可靠方法,标签规定用于表单的密钥对生成器字段。
<output> 用于不同类型的输出,比如计算机或脚本的输出。
(3)新表单属性
placehoder 简短的提示在用户输入值前会显示在输入域上。即我们常见的输入框默认提示,在用户输入后消失。
required 是一个 boolean 属性, 要求填写的输入域不能为空
pattern 描述了一个正则表达式用于验证<input> 元素的值
min 和 max 设置元素最小值与最大值
step 为输入域规定合法的数字间隔
height 和 width 用于 image 类型的 <input> 标签的图像高度和宽度
autofocus 是一个 boolean 属性。规定在页面加载时,域自动地获得焦点。
multiple 是一个 boolean 属性。规定<input> 元素中可选择多个值
3. 视频和音频
<audio controls> <source src="horse.ogg" type="audio/ogg"> <source src="horse.mp3" type="audio/mpeg">您的浏览器不支持 audio 元素。</audio>
control属性供添加播放、暂停、和音量控件
在<audio> 与 </audio> 之间你需要插入浏览器不支持的<audio>元素的提示文本 。
<audio> 元素允许使用多个 <source> 元素. <source> 元素可以链接不同的音频文件,浏览器将使用第一个支持的音频文件
目前, <audio>元素支持三种音频格式文件: MP3, Wav, 和 Ogg
<video width="320" height="240" controls> <source src="movie.mp4" type="video/mp4"> <source src="movie.ogg" type="video/ogg">您的浏览器不支持Video标签。</video>
control 提供了 播放、暂停和音量控件来控制视频。也可以使用dom操作来控制视频的播放暂停,如 play() 和 pause() 方法。
同时 video 元素也提供了 width 和 height 属性控制视频的尺寸.如果设置的高度和宽度,所需的视频空间会在页面加载时保留。如果没有设置这些属性,浏览器不知道大小的视频,浏览器就不能再加载时保留特定的空间,页面就会根据原始视频的大小而改变。
video 元素支持多个source 元素. 元素可以链接不同的视频文件。浏览器将使用第一个可识别的格式( MP4, WebM, 和 Ogg)
4. Canvas绘图(图形、路径、文本、渐变、图像)
canvas只是图形容器,必须使用脚本来绘制图形。
图形
(1) 创建一个画布,一个画布在网页中是一个矩形框,通过 <canvas> 元素来绘制。默认情况下 元素没有边框和内容。
id="myCanvas" width="200" height="100" style="border:1px solid #000000;"></canvas>
标签通常需要指定一个id属性 (脚本中经常引用), width 和 height 属性定义的画布的大小,使用 style 属性来添加边框。你可以在HTML页面中使用多个 <canvas> 元素
(2) 使用Javascript来绘制图像,canvas 元素本身是没有绘图能力的。所有的绘制工作必须在 JavaScript 内部完成
<script> var c=document.getElementById("myCanvas"); var ctx=c.getContext("2d");//getContext("2d") 对象是内建的 HTML5 对象,拥有多种绘制路径、矩形、圆形、字符以及添加图像的方法。 ctx.fillStyle="#FF0000"; ctx.fillRect(0,0,150,75);</script>//设置 fillStyle 属性可以是CSS颜色,渐变,或图案。fillStyle默认设置是#000000(黑色)。//fillRect(x,y,width,height) 方法定义了矩形当前的填充方式。意思是:在画布上绘制 150x75 的矩形,从左上角开始 (0,0)。
路径
<script> var c=document.getElementById("myCanvas"); var ctx=c.getContext("2d"); ctx.moveTo(0,0); ctx.lineTo(200,100); ctx.stroke();</script>//定义开始坐标(0,0), 和结束坐标 (200,100). 然后使用 stroke() 方法来绘制线条//moveTo(x,y) 定义线条开始坐标 lineTo(x,y) 定义线条结束坐标
文本 (重要的属性和方法如下:)
font - 定义字体
fillText(text,x,y) - 在 canvas 上绘制实心的文本
strokeText(text,x,y) - 在 canvas 上绘制空心的文本
ex:使用 "Arial" 字体在画布上绘制一个高 30px 的文字(实心)
var c=document.getElementById("myCanvas");var ctx=c.getContext("2d");ctx.font="30px Arial";ctx.fillText("Hello World",10,50);
渐变
渐变可以填充在矩形, 圆形, 线条, 文本等等, 各种形状可以自己定义不同的颜色。
以下有两种不同的方式来设置Canvas渐变:
createLinearGradient(x,y,x1,y1) - 创建线条渐变
createRadialGradient(x,y,r,x1,y1,r1) - 创建一个径向/圆渐变
当我们使用渐变对象,必须使用两种或两种以上的停止颜色。addColorStop()方法指定颜色停止,参数使用坐标来描述,可以是0至1.
使用渐变,设置fillStyle或strokeStyle的值为渐变,然后绘制形状,如矩形,文本,或一条线。
ex: 创建了一个线性渐变,使用渐变填充矩形
var c=document.getElementById("myCanvas");var ctx=c.getContext("2d"); // Create gradientvar grd=ctx.createLinearGradient(0,0,200,0);grd.addColorStop(0,"red");grd.addColorStop(1,"white"); // Fill with gradientctx.fillStyle=grd;ctx.fillRect(10,10,150,80);
图像
把一幅图像放置到画布上, 使用 drawImage(image,x,y) 方法
var c=document.getElementById("myCanvas");var ctx=c.getContext("2d");var img=document.getElementById("scream");ctx.drawImage(img,10,10);
5. SVG绘图 (与Canvas的区别)
SVG是指可伸缩的矢量图形
区别:
SVG 是一种使用 XML 描述 2D 图形的语言。
Canvas 通过 JavaScript 来绘制 2D 图形。
SVG 基于 XML,这意味着 SVG DOM 中的每个元素都是可用的。您可以为某个元素附加 JavaScript 事件处理器。
在 SVG 中,每个被绘制的图形均被视为对象。如果 SVG 对象的属性发生变化,那么浏览器能够自动重现图形。
Canvas 是逐像素进行渲染的。在 canvas 中,一旦图形被绘制完成,它就不会继续得到浏览器的关注。如果其位置发生变化,那么整个场景也需要重新绘制,包括任何或许已被图形覆盖的对象。
6. 地理定位
HTML5 Geolocation(地理定位)用于定位用户的位置。
7. 拖放API
拖放是一种常见的特性,即抓取对象以后拖到另一个位置。在 HTML5 中,拖放是标准的一部分,任何元素都能够拖放。
拖放的过程分为源对象和目标对象。源对象是指你即将拖动元素,而目标对象则是指拖动之后要放置的目标位置。
拖放的源对象(可能发生移动的)可以触发的事件——3个:
dragstart:拖动开始
drag:拖动中
dragend:拖动结束
整个拖动过程的组成: dragstart*1 + drag*n + dragend*1
拖放的目标对象(不会发生移动)可以触发的事件——4个:
dragenter:拖动着进入
dragover:拖动着悬停
dragleave:拖动着离开
drop:释放
整个拖动过程的组成1: dragenter*1 + dragover*n + dragleave*1
整个拖动过程的组成2: dragenter*1 + dragover*n + drop*1
dataTransfer:用于数据传递的“拖拉机”对象;
在拖动源对象事件中使用e.dataTransfer属性保存数据:
e.dataTransfer.setData( k, v )
在拖动目标对象事件中使用e.dataTransfer属性读取数据:
var value = e.dataTransfer.getData( k )
8. Web Worker
当在 HTML 页面中执行脚本时,页面的状态是不可响应的,直到脚本已完成。
web worker 是运行在后台的 JavaScript,独立于其他脚本,不会影响页面的性能。您可以继续做任何愿意做的事情:点击、选取内容等等,而此时 web worker 在后台运行。
(1) 首先检测浏览器是否支持 Web Worker
if(typeof(Worker)!=="undefined"){ // 是的! Web worker 支持! // 一些代码..... }else{ // //抱歉! Web Worker 不支持 }
下面的代码检测是否存在 worker,如果不存在,- 它会创建一个新的 web worker 对象,然后运行 "demo_workers.js" 中的代码
if(typeof(w)=="undefined") { w=new Worker("demo_workers.js"); }
然后我们就可以从 web worker 发送和接收消息了。向 web worker 添加一个 "onmessage" 事件监听器:
w.onmessage=function(event){document.getElementById("result").innerHTML=event.data;};
当 web worker 传递消息时,会执行事件监听器中的代码。event.data 中存有来自 event.data 的数据。当我们创建 web worker 对象后,它会继续监听消息(即使在外部脚本完成之后)直到其被终止为止。
如需终止 web worker,并释放浏览器/计算机资源,使用 terminate() 方法。
完整的Web Worker实例
Count numbers: Start Worker Stop Worker
创建的计数脚本,该脚本存储于 "demo_workers.js" 文件中
var i=0; function timedCount() { i=i+1; postMessage(i); setTimeout("timedCount()",500); } timedCount();
9. Web Storage
使用HTML5可以在本地存储用户的浏览数据。早些时候,本地存储使用的是cookies。
Web 存储需要更加的安全与快速. 这些数据不会被保存在服务器上,但是这些数据只用于用户请求网站数据上.
它也可以存储大量的数据,而不影响网站的性能。
数据以 键/值 对存在, web网页的数据只允许该网页访问使用。
客户端存储数据的两个对象为:
localStorage - 没有时间限制的数据存储
sessionStorage - 针对一个 session 的数据存储, 当用户关闭浏览器窗口后,数据会被删除。
在使用 web 存储前,应检查浏览器是否支持 localStorage 和sessionStorage
if(typeof(Storage)!=="undefined") { // 是的! 支持 localStorage sessionStorage 对象! // 一些代码..... } else { // 抱歉! 不支持 web 存储。 }
不管是 localStorage,还是 sessionStorage,可使用的API都相同,常用的有如下几个(以localStorage为例):
保存数据:localStorage.setItem(key,value);
读取数据:localStorage.getItem(key);
删除单个数据:localStorage.removeItem(key);
删除所有数据:localStorage.clear();
得到某个索引的key:localStorage.key(index);
10. Web Socket
WebSocket是HTML5开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。
<!DOCTYPE HTML><html> <head> <meta charset="utf-8"> <title>W3Cschool教程(w3cschool.cn)</title> <script type="text/javascript"> function WebSocketTest() { if ("WebSocket" in window) { alert("您的浏览器支持 WebSocket!"); // 打开一个 web socket var ws = new WebSocket("ws://localhost:9998/echo"); ws.onopen = function() { // Web Socket 已连接上,使用 send() 方法发送数据 ws.send("发送数据"); alert("数据发送中..."); }; ws.onmessage = function (evt) { var received_msg = evt.data; alert("数据已接收..."); }; ws.onclose = function() { // 关闭 websocket alert("连接已关闭..."); }; } else { // 浏览器不支持 WebSocket alert("您的浏览器不支持 WebSocket!"); } } </script> </head> <body> <div id="sse"> <a href="javascript:WebSocketTest()">运行 WebSocket</a> </div> </body></html>
html 5 基本操作
获取元素
document.getElementsByClassName ('class');
//通过类名获取元素,以伪数组形式存在。
document.querySelector('selector');
//通过CSS选择器获取元素,符合匹配条件的第1个元素。
document.querySelectorAll('selector');
//通过CSS选择器获取元素,以伪数组形式存在。
类名操作
Node.classList.add('class');
//添加class
Node.classList.remove('class');
//移除class
Node.classList.toggle('class');
//切换class,有则移除,无则添加
Node.classList.contains('class');
//检测是否存在class
自定义属性
/*
Node.dataset是以对象形式存在的,当我们为同一个DOM节点指定了多个自定义属性时,
Node.dataset则存储了所有的自定义属性的值。
*/
var demo = document.querySelector(反馈);
//获取
//注:当我们如下格式设置时,则需要以驼峰格式才能正确获取
var name = demo.dataset['myName'];
var age = demo.dataset['age'];
//设置
demo.dataset['name'] = 'web developer';
多媒体
方法
方法 描述
addTextTrack() 向音频/视频添加新的文本轨道
canPlayType() 检测浏览器是否能播放指定的音频/视频类型
load() 重新加载音频/视频元素
play() 开始播放音频/视频
pause() 暂停当前播放的音频/视频
属性
属性 描述
audioTracks 返回表示可用音轨的 AudioTrackList 对象
autoplay 设置或返回是否在加载完成后随即播放音频/视频
buffered 返回表示音频/视频已缓冲部分的 TimeRanges 对象
controller 返回表示音频/视频当前媒体控制器的 MediaController 对象
controls 设置或返回音频/视频是否显示控件(比如播放/暂停等)
crossOrigin 设置或返回音频/视频的 CORS 设置
currentSrc 返回当前音频/视频的 URL
currentTime 设置或返回音频/视频中的当前播放位置(以秒计)
defaultMuted 设置或返回音频/视频默认是否静音
defaultPlaybackRate 设置或返回音频/视频的默认播放速度
duration 返回当前音频/视频的长度(以秒计)
ended 返回音频/视频的播放是否已结束
error 返回表示音频/视频错误状态的 MediaError 对象
loop 设置或返回音频/视频是否应在结束时重新播放
mediaGroup 设置或返回音频/视频所属的组合(用于连接多个音频/视频元素)
muted 设置或返回音频/视频是否静音
networkState 返回音频/视频的当前网络状态
paused 设置或返回音频/视频是否暂停
playbackRate 设置或返回音频/视频播放的速度
played 返回表示音频/视频已播放部分的 TimeRanges 对象
preload 设置或返回音频/视频是否应该在页面加载后进行加载
readyState 返回音频/视频当前的就绪状态
seekable 返回表示音频/视频可寻址部分的 TimeRanges 对象
seeking 返回用户是否正在音频/视频中进行查找
src 设置或返回音频/视频元素的当前来源
startDate 返回表示当前时间偏移的 Date 对象
textTracks 返回表示可用文本轨道的 TextTrackList 对象
videoTracks 返回表示可用视频轨道的 VideoTrackList 对象
volume 设置或返回音频/视频的音量
事件
事件 描述
abort 当音频/视频的加载已放弃时
canplay 当浏览器可以播放音频/视频时
canplaythrough 当浏览器可在不因缓冲而停顿的情况下进行播放时
durationchange 当音频/视频的时长已更改时
emptied 当目前的播放列表为空时
ended 当目前的播放列表已结束时
error 当在音频/视频加载期间发生错误时
loadeddata 当浏览器已加载音频/视频的当前帧时
loadedmetadata 当浏览器已加载音频/视频的元数据时
loadstart 当浏览器开始查找音频/视频时
pause 当音频/视频已暂停时
play 当音频/视频已开始或不再暂停时
playing 当音频/视频在已因缓冲而暂停或停止后已就绪时
progress 当浏览器正在下载音频/视频时
ratechange 当音频/视频的播放速度已更改时
seeked 当用户已移动/跳跃到音频/视频中的新位置时
seeking 当用户开始移动/跳跃到音频/视频中的新位置时
stalled 当浏览器尝试获取媒体数据,但数据不可用时
suspend 当浏览器刻意不获取媒体数据时
timeupdate 当目前的播放位置已更改时
volumechange 当音量已更改时
waiting 当视频由于需要缓冲下一帧而停止
本地存储
特性
设置、读取方便
容量较大,sessionStorage约5M、
localStorage约20M
只能存储字符串,可以将对象JSON.stringify() 编码后存储
window.sessionStorage
生命周期为关闭浏览器窗口
在同一个窗口(页面)下数据可以共享
window.localStorage
永久生效,除非手动删除(服务器方式访问然后清除缓存)
可以多窗口(页面)共享
方法
setItem(key, value) 设置存储内容
getItem(key) 读取存储内容
removeItem(key) 删除键值为key的存储内容
clear() 清空所有存储内容
历史管理
pushState(data, title, url) 追加一条历史记录
data用于存储自定义数据,通常设为null+ url 以当前域为基础增加一条历史记录,不可跨域设置
title网页标题,基本上没有被支持,一般设为空
replaceState(data, title, url) 与pushState()基本相同,
不同之处在于replaceState(),只是替换当前url,不会增加/减少历史记录。
onpopstate事件,当前进或后退时则触发
离线应用(没应用上)
优势
1、可配置需要缓存的资源
2、网络无连接应用仍可用
3、本地读取缓存资源,提升访问速度,增强用户体验
4、减少请求,缓解服务器负担
缓存清单
一个普通文本文件,其中列出了浏览器应缓存以供离线访问的资源,推荐使用.appcache为后缀名
例如我们创建了一个名为demo.appcache的文件,然后在需要应用缓存在页面的根元素(html)添加属性manifest="demo.appcache",路径要保证正确。
manifest文件格式
1、顶行写CACHE MANIFEST
2、CACHE: 换行 指定我们需要缓存的静态资源,如.css、image、js等
3、NETWORK: 换行 指定需要在线访问的资源,可使用通配符
4、FALLBACK: 换行 当被缓存的文件找不到时的备用资源
其它
1、CACHE: 可以省略,这种情况下将需要缓存的资源写在CACHE MANIFEST
2、可以指定多个CACHE: NETWORK: FALLBACK:,无顺序限制
3、#表示注释,只有当demo.appcache文件内容发生改变时或者手动清除缓存后,才会重新缓存。
4、chrome 可以通过chrome://appcache-internals/工具和离线(offline)模式来调试管理应用缓存
文件读取(没应用上)
例如
<img src="" alt="" id="img">
/*获取到了文件表单元素*/
var file = document.querySelector('.file');
/*选择文件后触发*/
file.onchange = function () {
/*初始化了一个文件读取对象*/
var reader = new FileReader();
/*读取文件数据 this.files[0] 文件表单元素选择的第一个文件 */
reader.readAsDataURL(this.files[0]);
/*读取的过程就相当于 加载过程 */
/*读取完毕 预览 */
reader.onload = function () {
/*读取完毕 base64位数据 表示图片*/
console.log(this.result);
document.querySelector('#img').src = this.result;
}
}
上面的代码演示了如何通过H5的file API读取文件内容。DateURL的形式读取到的问见是一个字符串,类似
data:image/png;base64,iVBORw0KGgoAAAA,常,用于设置图像。如果需要服务器端处理,把字符串base64,后面的字符发送给服务器并用Base64解码就可以得到原始文件的二进制内容。
JavaScript的一个重要特性就是单线程执行模式,关于H5 文件操作这里还有一个说明是廖雪峰写的on故宫异步实现的文件操作,有兴趣的可以看一下
拖拽(没应用上)
事件
box.addEventListener('drag', function (e) {});
drag整个拖拽执行
dragleave 拖拽离开
dragstart 拖拽开始
dragend 拖拽结束
dragover 拖拽智商
drop 拖拽松开
网络状态(没应用上)
我们可以通过window.onLine来检测,用户当前的网络状况,返回一个布尔值
window.online用户网络连接时被调用
window.offline用户网络断开时被调用
css
基础
定位
理论
盒模型
block
inline
文档流
坐标系
margin、padding
position
absolute
relative
fixed
static
float
left
center
right
清除浮动
overflow
visible
hidden
scroll
边框
border
圆角
border-radius
margin
垂直重叠
居中定位
padding
box-shadow
样式
背景
颜色
background-color
图片
background-image
重复
background-repeat
定位
background-position
background-attachment
样式
background-size
文字
颜色
字体
字号
字重
行高
尺寸
宽度
最大宽度
最小宽度
高度
最大高度
最小高度
列表
有序列表
无序列表
HTML5 标签
header
nav
section
article
footer
aside
CSS3 布局
flex: 弹性布局
轴线
容器属性
flex-direction:主轴的方向(即项目的排列方向)。
row(默认值):主轴为水平方向,起点在左端。
row-reverse:主轴为水平方向,起点在右端。
column:主轴为垂直方向,起点在上沿。
column-reverse:主轴为垂直方向,起点在下沿。
flex-wrap: 定义换行方式
nowrap(默认):不换行。
wrap:换行,第一行在上方。
wrap-reverse:换行,第一行在下方。
flex-flow: 属性是flex-direction属性和flex-wrap属性的简写形式,默认值为row nowrap。
justify-content: 属性定义了项目在主轴上的对齐方式。
flex-start(默认值):左对齐
flex-end:右对齐
center: 居中
space-between:两端对齐,项目之间的间隔都相等。
space-around:每个项目两侧的间隔相等。所以,项目之间的间隔比项目与边框的间隔大一倍。
align-items: 属性定义项目在交叉轴上如何对齐。
flex-start:交叉轴的起点对齐。
flex-end:交叉轴的终点对齐。
center:交叉轴的中点对齐。
baseline: 项目的第一行文字的基线对齐。
stretch(默认值):如果项目未设置高度或设为auto,将占满整个容器的高度。
align-content: 属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。
flex-start:与交叉轴的起点对齐。
flex-end:与交叉轴的终点对齐。
center:与交叉轴的中点对齐。
space-between:与交叉轴两端对齐,轴线之间的间隔平均分布。
space-around:每根轴线两侧的间隔都相等。所以,轴线之间的间隔比轴线与边框的间隔大一倍。
stretch(默认值):轴线占满整个交叉轴。
内容属性
order: 属性定义项目的排列顺序。数值越小,排列越靠前,默认为0。
flex-grow: 属性定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大。
如果所有项目的flex-grow属性都为1,则它们将等分剩余空间(如果有的话)。如果一个项目的flex-grow属性为2,其他项目都为1,则前者占据的剩余空间将比其他项多一倍。
flex-shrink: 属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小。
如果所有项目的flex-shrink属性都为1,当空间不足时,都将等比例缩小。如果一个项目的flex-shrink属性为0,其他项目都为1,则空间不足时,前者不缩小。
flex-basis: 属性定义了在分配多余空间之前,项目占据的主轴空间(main size)。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即项目的本来大小。
flex: flex属性是flex-grow, flex-shrink 和 flex-basis的简写,默认值为0 1 auto。后两个属性可选。
align-self: 属性允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性。默认值为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretch。
Grid 网格布局
版本二
CSS3新增浏览器存储功能:
local 本地 美 [ˈloʊkl]
session 会话 [ˈseʃn]
storage 存储 美 [ˈstɔːrɪdʒ]
setItem 存储方法 美 [set ˈaɪtəm]
getItem 获取方法 美 [get ˈaɪtəm]
伪类选择器
::selection 选中 美 [sɪˈlekʃn]
语法:p::selection{background:pink;color:red;}
::first-line 第一行 [for si t ` lai n]
p::first-line{text-shadow:2px 2px 5px yellow;}
::last-line 最后一行 [la`si t` lai n]
p::last-letter{font-size:30px;background:cyan;}
::first-letter 第一个字符 [for si t ˈletər]
p::first-letter{font-size:30px;background:cyan;}
属性选择器
^ 以什么什么开头的;
p[data-memeda^="xi"] //通过属性+属性值以xi开头的
$ 以什么什么结尾的;
p[data-memeda$="hei"] /通过属性+属性值以hei结尾的
~ 包含这个属性的所有元素
p[data-memeda~="jch"] 通过属性+属性值包含jch结尾的【空格】
* 通配;
p[data-memeda*="h"] 所有带h的属性都选择上
属性+值;
p[data-memeda="ha"]
属性;
p[data-memeda] data自定义属性 memeda 属性
关系型选择器
> 获取子元素 (亲的);
div>p{// div 亲儿子p
+ 匹配标签的下一个标签;
div>p+ul{ //div亲儿子的p下一个姊妹元素ul
~ 匹配标签后面所有一样的标签;
div>p~div{ //div的亲儿子的后面全部的姊妹元素div
伪选择器
:hover 鼠标移上 美 [ˈhʌvər] ;
div:hover{}
:focus 聚 焦 美 [ˈfoʊkəs];
input:focus
:disabled 不可使用的 美 [dɪsˈeɪbld]; [di s a bo d]
button:disabled
:empty 空的 美 [ˈempti]; [aim t]
p:empty
子类选择器
不区分类型的
first-child 第一个子元素 [fɜːrst] [tʃaɪld];
p:first-child //某一个父元素的第一个子元素且是p
last-child 最后一个元素 [læst] [tʃaɪld];
Hr:last-child //某一个父元素的最后一个子元素且是hr
nth-child() 第几个子元素 nth [tʃaɪld];
p:nth-child(6) /某一个父元素的第六个子元素且是p类型的
Nth-last-child() 倒数第几个子元素 ;
span:nth-last-child(4) //某一父元素的倒数第四个子元素且是span类型的
nth-child(2n) 倍数写法匹配子元素 ;
p:nth-child(3n)
nth-last-child(2n) 倒数倍数匹配子元素;
p:nth-last-child(2n)
区分类型的
first-of-type 某一个类型的第一个元素;
p:first-of-type //作为子元素:p类型的第一个
last-of-type 某一个类型的最后一个元素;
p:last-of-type 子元素:p类型的最后一个
nth-of-type() 某一类型的第几个元素;
p:nth-of-type(2n) p类型的第2n个元素
nth-last-of-type() 倒数某一类型的第几个元素;
p:nth-last-of-type(3n) /p类型的倒数第几个子元素
CSS3新增
calc() 计算函数
语法:width:calc(33.33% - 10px);
box-sizing 内容塌陷 ;元素向内‘坍塌’
语法: box-sizing:border-box;
document.documentElement.clientWidth
//获取pc端视口:既能看到的部分;
css样式
命名规则 #前class - #方向 如: test-in 或者 #前id- #自己的标签或者作用
基础
class #则是使用. #可以重复
name #是后台使用
id #是使用 # #不可以重复
px是绝对大小,rem是相对的大小不同手机用的比较多
选择器
#常用的
.intro #选择class="intro" 的所有元素。
#firstname #选择id="firstname" 的所有元素。
* #选择所有元素。
p #选择所有 元素。
div,p #选择所有 元素和所有 元素。
div p #选择 元素内部的所有 元素。
div>p #选择父元素为 元素的所有 元素。
div+p #选择紧接在 元素之后的所有 元素。
#常用的
a:hover #选择鼠标指针位于其上的链接。
p:first-child #选择属于父元素的第一个子元素的每个 元素。
p:before #在每个 元素的内容之前插入内容。
p:after #在每个 元素的内容之后插入内容。
p:first-of-type #选择属于其父元素的首个 元素的每个 元素。
p:last-of-type #选择属于其父元素的最后 元素的每个 元素。
p:only-child #选择属于其父元素的唯一子元素的每个 元素。
p:nth-child(2) #选择属于其父元素的第二个子元素的每个 元素。
#不常用的
[target] #选择带有 target 属性所有元素。
[target=_blank] #选择target="_blank" 的所有元素。
[title~=flower] #选择 title 属性包含单词 "flower" 的所有元素。
[lang|=en] #选择lang 属性值以 "en" 开头的所有元素。
a:link #选择所有未被访问的链接。
a:visited #选择所有已被访问的链接。
a:active #选择活动链接。
input:focus #选择获得焦点的 input 元素。
p:first-letter #选择每个 元素的首字母。
p:first-line #选择每个 元素的首行。
p:lang(it) #选择带有以 "it" 开头的 lang 属性值的每个 元素。
p~ul #选择前面有 元素的每个 元素。
a[src^="https"] #选择其 src 属性值以 "https" 开头的每个 <a> 元素。
a[src$=".pdf"] #选择其 src 属性以 ".pdf" 结尾的所有 <a> 元素。
a[src*="abc"] #选择其 src 属性中包含 "abc" 子串的每个 <a> 元素。
p:first-of-type #选择属于其父元素的首个 元素的每个 元素。
p:last-of-type #选择属于其父元素的最后 元素的每个 元素。
p:only-of-type #选择属于其父元素唯一的 元素的每个 元素。
p:only-child #选择属于其父元素的唯一子元素的每个 元素。
p:nth-child(2) #选择属于其父元素的第二个子元素的每个 元素。
p:nth-last-child(2) #同上,从最后一个子元素开始计数。
p:nth-of-type(2) #选择属于其父元素第二个 元素的每个 元素。
p:nth-last-of-type(2) #同上,但是从最后一个子元素开始计数。
p:last-child #选择属于其父元素最后一个子元素每个 元素。
:root #选择文档的根元素。
p:empty #选择没有子元素的每个 元素(包括文本节点)。
#news:target #选择当前活动的#news 元素。
input:enabled #选择每个启用的<input> 元素。
input:disabled #选择每个禁用的<input> 元素
input:checked #选择每个被选中的<input> 元素。
:not(p) #选择非 元素的每个元素。
::selection #选择被用户选取的元素部分。
内部属性
备注:什么时候用动画,当没有参数数值参与的时候使用,当有数据有参数的时候使用js
#background的属性
#渐变色或导入背景图片background-image: #自己加 ;
image-set() #背景图片
radial-gradient(#第一个颜色, #第二个颜色, #第三个颜色) #圆形扩散渐变
linear-gradient(#颜色 #百分比或者宽度px , #颜色#百分比或宽度px) #线性渐变
#背景色background-color: #颜色 ;
#大小设置属性 #图片也可以直接设置
width : 3px ; #宽
height : 10px ; #高
#边框 border: #粗细px soid #颜色 ;#soid是实线模式
#四个角圆弧border-radius :3px;
#圆形 border-radius:50%;
#box的属性
#阴影 box-shadow: #水平阴影位置 #垂直阴影位置#模糊半径 #模糊扩散半径 #阴影颜色 #外部阴影改成内部阴影 ; #最后一个磨人inset外部阴影改成outset内部阴影
#固定外大小不因为边框大小而改变box-sizing:border-box
#定位
#定位目标position:absoluter; #绝对定位#relative相对定位 #启动了才可以使用定位 #fixed固定主位置配合z-index使用
#固定位置position:fixed ; #不会因为页面滑动而改变所在窗体的位置
#定位效果left: 50%; top : 50%; #定位到哪里要基于上面的使用
#定位配套位置z-index:10 ; #层次数值越大越上面不会被覆盖
#animation动画属性
备注:一般和3D动画一起使用
#animationanimation: #动画名 #动画整个时间 #动画执行几次 #动画过程速度 ;#时间是动画 #动画执行几次infinite这是无限一直持续 #速度可以是线性速度linear
#动画设置@keyframes#动画名字 {
0%{ } #初始状态
100%{} #最后状态
}
#transform的属性#3D的图片转换旋转动画
#transform函数 transform : all 0.2s ease-in ; #all全部事件 #动画时间#补
备注:z轴所在的线就是垂直于桌面的
备注:要开启3D模式才可以使用3D动画效果是吗 , 必须在父级启动才可以使用
#移动translateX(#移动距离 px ); #函数最后一个表示x轴可以改成y或者z
#旋转ratateX(#旋转角度deg) ; #函数最后一个表示x轴可以改成y或者z
#放大scale(1.5 , 1.5); #第一个为x的放大倍数第二个为y的放大倍数#只有一个参数时候就是x和y的放大倍数
#旋转中心点transform-origin:right top ; #右上角left bottom #左下角 center#中间 #还可以使用百分比默认前后2个都是百分比50也就是剧中
#允许2d转3dtransform-style : preserve -3d;#也叫开启3D模式
#背面是否显示backface-visibility: visible #显示 ;#hidden #不可见
#待定perspective
#font的属性
#功能好像是使用什么字体font-family: '#字体名字'; #待定 #补 #需要修改
#加粗 font-weight:bold ; #如果已经加粗则取消
#斜体 font-style:italic ;
#删除线 text-decoration:line-through;
<del>内容</del> #在body中设置的
<s>内容</s>
<strike>内容</strike>
#text的属性
#text text:
#文字居中text-align:center;
#文字大小font-size:22px;
#字体阴影text-shadow :5px 5px 5px #FF0000; #第一个是x轴偏移量 #第二个y轴偏移量 #第三个阴影大小或半径 #第四个颜色
#div中自动换行 word-wrap:break-word;
#line的属性
#列间距 line-height:60px;#第1行和第2行到第n行的距离
#ul的属性
#li前面是否有特殊符号 line-height: none;#none是取消
#display的属性
#转换成为块级元素display:block;
#转换成为行内元素display:inline;
#隐藏起来没人看得见display:none;
#模糊 filter:blur(#数值 px); #越大越模糊
#透明 opactity : .6 ;#1是透明度最大0是最小
#外间距 margin:auto ; #居中对其 #参数可以写4个分别依次是上右下左
#上下左右都居中需要
top:50%;
left:50%;
margin-left:-10px; #这个为这个宽度的一半
margin-top:-10px;#这个为这个高度的一半
#内间距 padding: 3px ; #内部的间距 #参数可以写4个分别依次是上右下左
#浮动 float :left ; #左浮动 #right右浮动 clear=none #
取消浮动
#事件
.index:hover h1{ #鼠标放在class为index之后触发的事件
}
.index::after{ content: ""; #必须有这句话 #是在这个index之前使用的效果
}
.index::before{content:""; #必须有这句话 #这是在index之后使用的效果
}
JS
基础
声明变量
var
声明全局变量
示例
let
声明局部变量,多用于for循环中
示例
const
定义常量,定义完成后修改变量值报错
示例
数据类型
数字
不区分整型与浮点型
示例
字符串
可以用单引号或双引号
JS中字符串鼓励用"+"拼接
示例
length
相当于python的len()
trim
相当于python的strip()
charAt(索引值)
输出对应索引值的字符
indexOf
查找字符对应的索引值
slice
字符串切片
toUpperCase
相当于python中upper()
toLowerCase
相当于python中lower()
split
分割字符串,返回列表
``
用于多行文本
${变量名}
用于字符串替换
布尔
true
false
undefined
表示变量不含有值
数组
相当于python中列表
示例
push
相当于python中append
pop
删除最后一个元素,有返回值
unshift
添加元素
shift
删除元素
reduce 等同于for和foreach
var sum = arr.reduce(function(prev, cur, index, arr) { console.log(prev, cur, index); return prev + cur; } , 0 )
有两个参数,一个是调用函数,一个是从下标几开始
函数有四个参数,上一次结果,当前的值是多少,下标,和arr数组
map
array.map(function(currentValue,index,arr), thisValue)
null
清空变量值
对象
相当于python中字典
示例
字符串(String)、数字(Number)、布尔(Boolean)、对空(Null)、未定义(Undefined)、Symbol(ES6新增)。
复杂数据类型(引用数据类型):对象(Object)、数组(Array)、函数(Function)。
区别是在栈中基础数据类型存储的是值,而应用数据类型保的是内存地址
流程语句
if
if...else
三目运算
示例
if...else if ... else
switch
语法
注意:每个case后加break,否则代码会继续往下执行返回其他case中的结果
for循环
示例
forEach
用于遍历数组
语法
while循环
do/while循环
语法
运算符
==
弱等于
===
强等于
&&
相当于python中and
||
相当于python中or
!
相当于python中not
函数
函数声明
函数表达式
函数表达式可以存储在变量中,该变量也可作为一个函数使用
示例
返回值
js函数不写返回值默认返回undefined
写多个值返回最后一个,想返回多个值可以用数组存储
箭头函数
示例
时间
https://www.w3cschool.cn/javascript/js-obj-date.html
注意:
月份 : 0~ 11
星期: 0~6 , 0代表周日
JSON
JSON.parse()
相当于python中的loads
JSON.stringify()
相当于python中的dumps
正则表达式(RegExp)
修饰符
i - 用来执行不区分大小写的匹配
g - 用于执行全文的搜索
全局匹配时,注意lastindex参数
高级
数据类型
1.分类
基本(值)类型
String:任意字符串
Number:任意的数字
boolean:true/false
undefined:undefined,代表定义了未赋值
null:null,定义并赋值,只是值为null;初始赋值,表明将要赋值为对象;结束前,让对象称为垃圾对象(被垃圾回收器回收)
对象(引用)类型
Object:任意对象
Function:一种特别的对象(可以执行)
Array:一种特别的对象(数值下标,内部数据是有序的)
2.判断
typeof:返回数据类型的字符串表达
可以判断:undefined、数值、字符串、布尔值、function;
不可以判断:null与object obje与array
instanceof:判断对象的具体类型
===:可以判断undefined、null
数据——内存——变量
数据:存储在内存中代表特定信息的“东西”,本质上是0101...
数据的特点:可传递,可运算
一切皆数据
内存中所有操作的目标:数据
算数运算
逻辑运算
赋值
运行函数
内存:内存条通电后产生的可储存数据的空间(临时的)
一块小内存的两个数据:内部存储的数据和地址值
内存分类:
栈:全局变量/局部变量
堆:对象
变量:可变化的量,由变量名和变量值组成,每个变量都对应一块小内存,变量名用来查找对应的内存,变量值就是内存中保存的数据
对象
什么是对象?
多个数据的封装体
用来保存多个数据的容器
一个对象代表现实中的一个事物
为什么要用对象?
统一管理多个数据
对象的组成
属性:属性名(字符串)和属性值(任意类型)组成
方法:一种特别的属性(属性值是函数)
如何访问对象内部数据?
.属性名
['属性名']
什么时候必须用['属性名']的方式?
1.属性名包含特殊字符:- 、空格
2.属性名不确定
函数
什么是函数?
实现特定功能的n条语句的封装体;只有函数是可以执行的,其他类型的数据不能执行
为什么要用函数?
提高代码的复用;便于阅读交流
如何定义函数?
函数声明
表达式
如何调用(执行)函数?
test():直接调用
obj.test():通过对象调用
new test():new调用
test.call/apply(obj):临时让test成为obj的方法临时调用
回调函数
什么函数才是回调函数?
1.你定义的
2.你没有调
3.但最终执行了
常见的回调函数?
dom事件回调函数
定时器回调函数
ajax回调函数
声明周期回调函数
IIFE
全称:Immediately-Invoked Function Expression
作用:
隐藏实现
不会污染外部(全局)命名空间
函数中的this
this是什么?
任何函数本质上都是通过某个对象调用的,如果没有直接指定就是window;
所有函数内部都有一个变量this,它的值是调用函数的当前对象
如何确定this的值?
test():window
p.test():p
new test():新创建的对象
p.call(obj):obj
分号问题
在下面两种情况下不加分号会有问题
小括号开头的前一条语句
中方括号开头的前一条语句
解决方法:在行首加分号
函数高级
原型与原型链
原型
1.函数的prototype属性
每个函数都有一个prototype属性,它默认指向一个Object空对象(即称为:原型对象)
原型对象中有一个属性constructor,它指向函数对象
2.给原型对象添加属性(一般都是方法)
作用:函数的所有实例对象自动拥有原型中的属性(方法)
显式原型与隐式原型
1.每个函数function都有一个prototype,即显示原型(属性)
2.每个实例对象都有一个__proto__,可称为隐式原型(属性)
3.对象的隐式原型的值为其对应构造函数的显式原型的值
4.内存结构(图)
5.总结:
函数的prototype属性:在定义函数时自动添加的,默认值是一个空Object对象
对象的__proto__属性:创建对象时自动添加的。默认值为构造函数的prototype属性值
程序员能直接操作显示原型,但不能直接操作隐式原型(ES6之前)
原型链
访问一个对象的属性时,
先在自身属性中查找,找到返回
如果没有,再沿着__proto__这条链向上查找,找到返回
如果最终没找到,返回undefined
别名:隐式原型链
作用:查找对象的属性(方法 )
构造函数/原型/实体对象的关系
构造函数/原型/实体对象的关系2
注意:
1.函数的显式原型指向的对象默认是空Object实例对象(但Object不满足)
2.所有函数都是Function的实例(包含Function)
3.Object的原型对象是原型链尽头
实例对象的隐式原型对象指向构造函数的显式原型
执行上下文与执行上下文栈
变量提升与函数提升
1.变量声明提升
通过var定义(声明)的变量,在定义语句之前就可以访问到
值:undefined
2.函数声明提升
通过function声明的函数,在之前就可以调用
值:函数定义(对象)
3.变量提升和函数提升是如何产生的?
执行上下文
作用域与作用域链
1.理解
就是一块“地盘”,一个代码段所在的区域
他是静态的(相对于上下文对象),在编写代码时就确定了
2.分类
全局作用域
函数作用域
没有块作用域(ES6有了)
3.作用
隔离变量。不同作用域下同名变量不会有冲突
argumen
作用域与执行上下文
区别1:
全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经去确定了。而不是在函数调用时
全局执行上下文是在全局作用域确定之后,js代码马上执行之前创建
函数执行上下文是在调用函数时,函数体代码执行之前创建
区别2
作用域是静态的,只要函数定义好了就一直存在,且不会再变化
执行上下文是动态的,调用函数时创建,函数调用结束时就会自动释放
联系
上下文环境(对象)是从属于所在的作用域
全局执行上下文==》全局作用域
函数执行上下文环境==》对应的函数使用域
闭包
1.如何产生闭包?
当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时,就产生了闭包
2.闭包到底是什么?
使用chrome调试查看
理解一:闭包是嵌套的内部函数(绝大多数人)
理解二:包含被引用变量(函数)的对象(极少数人)
注意:闭包存在于嵌套的内部函数中
3.产生闭包的条件?
函数嵌套
内部函数引用了外部函数的数据(变量/函数)
常见的闭包
1.将函数作为另一个函数的返回值
2.将函数作为实参传递给另一个函数调用
闭包的作用
1.使函数内部的变量在函数执行完后,仍然存活再内存中(延长了局部变量的声明周期)
2.让函数外部可以操作(读写)到函数内部的数据(变量/函数)
闭包的生命周期
产生:在嵌套内部函数定义执行完时就产生了(不是在调用)
死亡:在嵌套的内部函数成为垃圾对象时
闭包的应用:定义JS模块
具有特定功能的js文件
将所有的数据和功能都封装在一个函数内部(私有的)
只向外暴露一个包含n个方法的对象或函数
模块的使用者,只需要通过模块暴露的对象调用方来实现对应的功能
闭包的缺点与解决
缺点
函数执行完后,函数内的局部变量没有释放,占用内存时间会变长
容易造成内存泄漏
解决
能不用闭包就不用
及时释放
内存溢出与内存泄漏
1.内存溢出
一种程序运行出现的错误
当程序运行需要的内存超过了剩余的内存时,就会抛出内存溢出的错误
2.内存泄漏
占用的内存没有及时释放
内存泄漏积累多了就容易导致内存溢出
常见的内存泄漏:
意外的全局变量
没有及时清理的计时器或回调函数
闭包
面向对象高级
对象创建方式
方式一: Object构造函数模式
套路:先创建空Object对象,再动态添加属性/方法
适用场景:起始时不确定对象内部数据
问题:语句太多
方式二:对象字面量模式
套路:使用{}创建对象,同时指定属性/方法
适用场景:起始时对象内部数据确定
问题:如果创建多个对象,有重复代码
方式三:工厂模式
套路:通过工厂函数动态创建对象并返回
适用场景:需要创建多个对象
问题:对象没有一个具体的类型,都是Object类型
方式四:自定义构造函数模式
套路:自定义构造函数,通过new创建对象
适用场景:需要创建多个类型确定的对象
问题:每个对象都有相同的数据,浪费内存
方式五:构造函数+原型的组合模式
套路:自定义构造函数,属性再函数中初始化,方法添加到原型上
适用场景:需要创建多个类型确定的对象
继承模式
方式一:原型链继承
套路
1.定义父类型构造函数
2.给父类型的原型添加方法
3.定义子类型的构造函数
4.创建父类型的对象赋值给子类型的原型
5.将子类型原型的构造属性设置为子类型
6.给子类型原型添加方法
7.创建子类型的对象:可以调用父类型的方法
关键
1.子类型的原型为父类型的一个实例对象
方式二:借用构造函数继承(假的)
套路
1.定义父类型构造函数
2.定义子类型构造函数
3.在子类型构造函数中调用父类型构造
关键
在子类型构造函数中通过call()调用父类型构造函数
方式三:原型链+借用构造函数的组合继承
1.利用原型链实现对父类型对象的方法继承
2.利用call()借用父类型构建函数初始化相同属性
线程机制与事件机制
进程与线程
游览器内核
Trident 内核:IE,MaxThon,TT,The World,360,搜狗浏览器等。[又称 MSHTML]
Gecko 内核:Netscape6 及以上版本,FF,MozillaSuite/SeaMonkey 等
Presto 内核:Opera7 及以上。 [Opera 内核原为:Presto,现为:Blink;]
Webkit 内核:Safari,Chrome 等。 [ Chrome 的:Blink(WebKit 的分支)]
防抖和节流
区别:防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。
防抖
// 这个是用来获取当前时间戳的
function now() {
return +new Date()
}
/**
* 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {boolean} immediate 设置为ture时,是否立即调用函数
* @return {function} 返回客户调用函数
*/
function debounce (func, wait = 50, immediate = true) {
let timer, context, args
// 延迟执行函数
const later = () => setTimeout(() => {
// 延迟函数执行完毕,清空缓存的定时器序号
timer = null
// 延迟执行的情况下,函数会在延迟函数中执行
// 使用到之前缓存的参数和上下文
if (!immediate) {
func.apply(context, args)
context = args = null
}
}, wait)
// 这里返回的函数是每次实际调用的函数
return function(...params) {
// 如果没有创建延迟执行函数(later),就创建一个
if (!timer) {
timer = later()
// 如果是立即执行,调用函数
// 否则缓存参数和调用上下文
if (immediate) {
func.apply(this, params)
} else {
context = this
args = params
}
// 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
// 这样做延迟函数会重新计时
} else {
clearTimeout(timer)
timer = later()
}
}
}
节流
function throttle(fn) {
let canRun = true; // 通过闭包保存一个标记
return function () {
if (!canRun) return; // 在函数开头判断标记是否为 true,不为 true 则 return
canRun = false; // 立即设置为 false
setTimeout(() => { // 将外部传入的函数的执行放在 setTimeout 中
fn.apply(this, arguments);
// 最后在 setTimeout 执行完毕后再把标记设置为 true(关键)
//表示可以执行下一次循环了。当定时器没有执行的时候
//标记永远是 false,在开头被 return 掉
canRun = true;
}, 500);
};
}
function sayHi(e) {
console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi));
Set、Map、WeakSet 和 WeakMap 的区别?
Set
成员唯一、无序且不重复;
[value, value],键值与键名是一致的(或者说只有键值,没有键名);
可以遍历,方法有:add、delete、has、clear、entries、forEach、keys、values
Set 也能用来保存 NaN 和 undefined, 如果有重复的 NaN, Set 会认为就一个 NaN(实际上 NaN!=NaN);
Map
本质上是键值对的集合,类似集合;
可以遍历,方法很多,可以跟各种数据格式转换。
WeakSet
成员都是对象;
成员都是弱引用,可以被垃圾回收机制回收,可以用来保存 DOM 节点,不容易造成内存泄漏;
不能遍历,方法有 add、delete、has。
WeakMap
只接受对象作为键名(null 除外),不接受其他类型的值作为键名;
键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的;
不能遍历,方法有 get、set、has、delete。
WebStorage
目的
提供一种在cookie之外存储会话数据的途径。
提供一种存储大量可以跨会话存在的数据的机制。
sessionStorage #浏览器关闭消失
localStorage #一直存着
setItem (key, value) 保存数据,以键值对的方式储存信息。
getItem (key) 获取数据,将键值传入,即可获取到对应的value值。
removeItem (key) 删除单个数据,根据键值移除对应的信息
clear () 删除所有的数据
key (index) 获取某个索引的key
如
var storage = null;
if(window.localStorage){ //判断浏览器是否支持localStorage
storage = window.localStorage;
storage.setItem("name", "Rick"); //调用setItem方法,存储数据
alert(storage.getItem("name")); //调用getItem方法,弹框显示 name 为 Rick
storage.removeItem("name"); //调用removeItem方法,移除数据
alert(storage.getItem("name")); //调用getItem方法,弹框显示 name 为 null }
ES6标准
修改的内容
变量
var
可以重复声明
无法限制修改
块级作用域
let
不能重复声明
可以修改
块级作用域
const
不能重复声明
不能修改
块级作用域
函数
function 名字 () {}
箭头函数 let fun = ()=>{}
只有一个参数()省略 let t = a=>{return a*2}
如果只有一条语句就可以省略{} let t = a =>a*2
参数扩展
存放多个参数function show ( a, b ,...c){} ; show(1,2,3,4,5) 参数扩展三个点点,a存1,b存2,c存3,4,5 必须是最后一个
展开数组
let arr=[1,2,3] show(...arr)
设置默认参数
function show ( a, ,b=5,c = 90) {} b默认传5,c=90 调用show(99)即可
数组
结构赋值 json也可以结构赋值
let [a,b,c]=[1,2,3] 结构必须对应,就是a=1,b=2,c=3
let {a,b}={a:1,b:2} 对象的解构赋值
新方法
let arr=[12,32,24,32]
映射 map
arr.map(item=>item*2) #返回的arr等于 24 , 64,24,32
arr.map(item=> item>60?'及格':'不及格' )
汇总 reduce #返回总数或者平均数之类的 #返回一个结果
let result=arr.reduce(function(tmp,item,index){
if(index==this.length-1){return (tmp+item)/this.length} #算平均值的 #这里this可以替换成为arr就是数组变量名
return tmp+item #返回累计值加当前数值 #算总和
}) #其中tmp是存储数值中间变量 #item是当前数值 #index当前下标 #自动循环n次
过滤器 filter
let json=arr.filter(json=>{json>20}) #返回内容就是[32,24,32]
支持数组内容是对象的使用 arr=[{a:1,b:2},{a:3,b:3}]
let json=arr.filter(json=>{json.b>2}) #返回内容就是[{a:3,b:3}]
迭代(循环) forEach
arr.forEach((item,index)=>{ #item当前内容 #index下标
#操作自定义
})
排序 sort
let arr=[1,3,4,5]
arr.sort((a,b)=>{return a-b})
字符串
三个符号都是字符串
let str="www.baidu.com"
let str='www.baidu.com'
let str=`www.baidu.com`
新方法
判断内容开头 startsWith
if(str.startWith('http')){ #判断字符串开头内容是否http
}
判断内容尾部 endsWidth
if(str.endsWidth('.doc')){}
字符串连接
使用``反单引号
let a = `www.#{b},#{c}.com`
消除空格包括tab或者换行符号 trimStart
str.trimStart() #消除字符串前面的空格
str.trimEnd() #消除后面的空格
满足正则表达式的内容 matchAll
过滤正则表达式的内容
let string= str.matchAll(rule)
正则表达式对象
RegExp 构造函数 #后续补充 #补
数值
Number对象的方法
新方法
转换二进制 八进制的转换成为十进制 其中第二个不区分大小写
Number('0b100001') #0b开头的是二进制 #是数字的0
Number('0o105401') #0b开头的是二进制 #是数字的0和字母的o
检查一个数值是否为有限的(finite),即不是 Infinity。
Number.isFinite(123) 返回就是true
Number.isFinite(NaN) 返回就是false
检查一个数值是否未NaN
Number..isNaN()
转发为整数
Number.parseInt()
Number.isparseInt() #判断是否是整数
转发为双精度
Number.parseFloat()
Number.isparseFloat() #判断是否是数 #存在精度问题
math对象的方法
新方法
截取整数
Math.trunc(3.5)
判断正数负数还是0
Math.sign()
参数为正数,返回+1;
参数为负数,返回-1;
参数为 0,返回 0;
参数为-0,返回-0;
其他值,返回 NaN;
计算立方根
Math.cbrt()
面向对象
class #类名字 {
constructor (name,pass){ #构造
this.name=name;
this.pass=pass;
}
#方法名 (){}
}
继承
class #类名 extends #继承的类名 {
constructor(a,b,c){
super(a,b) #给继承的类的参数
this.c=c
}
}
setTimeout、Promise、Async/Await
setimeout 定时器优先级比promise的then低
是线程做完之后才才执行
Promise #异步交互
Promise本身是同步的立即执行函数, 当在executor中执行resolve或者reject的时候, 此时是异步操作, 会先执行then、catch等,当主栈完成后,才会去调用resolvereject中存放的方法执行,打印p的时候,是打印的返回结果,一个Promise实例。
console.log('script start')
let promise1 = new Promise(function (resolve) {
console.log('promise1')
resolve() //成功进入then中,或者reject失败进入catch中
console.log('promise1 end')
}).then(function () {
console.log('promise2')
})
setTimeout(function(){
console.log('settimeout')
})
console.log('script end')
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
Async/Await
await等于返回一个promise
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(function() {
console.log("setTimeout");
}, 0);
async1();
new Promise(function(resolve) {
console.log("promise1");
resolve();
}).then(function() {
console.log("promise2");
});
console.log("script end");
async function async1(){
console.log('async1 start');
await async2();
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start');
async1();
console.log('script end')
// 输出顺序:
generator #同步拆成异步操作
模块化 #自带模块化
AMD和CMD
对象
Proxy代理
var proxy = new Proxy(target, handler);
target参数
{}或者函数
handler参数 需要拦截代理的操作
get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo']。
set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。
has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。
Reflect反射
方法
Reflect.get(target, name, receiver)
Reflect.set(target, name, value, receiver)
var myObject = { foo: 4, set bar(value) { return this.foo = value; }, };
var myReceiverObject = { foo: 0, };
Reflect.set(myObject, 'bar', 1, myReceiverObject);
myObject.foo // 4
myReceiverObject.foo // 1
Reflect.apply(target, thisArg, args)
Reflect.construct(target, args)
new 的写法
let person= new Person('chen')
Reflect.construct 的写法
let person = Reflect.construct(Person, ['chen']);
Reflect.defineProperty(target, name, desc)
let obj={ name:"chen" } let handler={ set(target,key,value,receiver){ console.log("Proxy拦截赋值操作") //Reflect完成赋值操作 Reflect.set(target,key,value,receiver) }, defineProperty(target,key,attribute){ console.log("Proxy拦截定义属性操作") //Reflect完成定义属性操作 Reflect.defineProperty(target,key,attribute) } } let proxy=new Proxy(obj,handler) proxy.name="ya" //Proxy拦截赋值操作 //Proxy拦截定义属性操作
Reflect.deleteProperty(target, name)
Reflect.has(target, name)
Reflect.ownKeys(target)
Reflect.isExtensible(target)
Reflect.preventExtensions(target)
Reflect.getOwnPropertyDescriptor(target, name)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)
函数
Promise异步交互对象
有三个状态pending(进行)、fulfilled(成功)、rejected(失败)
使用的用法
let promise=new Promise(function(resolve,regect){
if(异步操作){
resolve(value);}
else{
reject(error)}
}})
promise.then(function(value){
seuccess},
function(error){
failure;}
})
.catch(function(error){
})
等于then(null,function(error){})
.finally(()=>{
let P = this.constructor; return this.then( value => P.resolve(callback()).then(() => value), reason => P.resolve(callback()).then(() => { throw reason }) );})
附加
Promise.all([promise,promises]).then{(posts)=>{}}.catch((reason)=>{})
// 生成一个Promise对象的数组
const promises = [2, 3, 5, 7, 11, 13].map(function (id) { return getJSON('/post/' + id + ".json"); }); Promise.all(promises).then(function (posts) { // ... }).catch(function(reason){ // ... });
Promise.all([promise,promises]).then{(posts,postss)=>{}}.catch((reason)=>{})
Promise.race([promises])
fetch('/resource-that-may-take-a-while'), new Promise(function (resolve, reject) { setTimeout(() => reject(new Error('request timeout')), 5000) })
Promise.allSettled([promises])
Promise.any([promises])
Promise.any(promises).then( (first) => { // Any of the promises was fulfilled. }, (error) => { // All of the promises were rejected. } );
或者
var resolved = Promise.resolve(42); var rejected = Promise.reject(-1); var alsoRejected = Promise.reject(Infinity); Promise.any([resolved, rejected, alsoRejected]).then(function (result) { console.log(result); // 42 }); Promise.any([rejected, alsoRejected]).catch(function (results) { console.log(results); // [-1, Infinity] });
Promise.resolve()
Promise.reject('String');
Promise.finally(()=>{});
// 等同于 promise .then( result => { // 语句 return result; }, error => { // 语句 throw error; } );
例子
new Promise((resolve, reject) => {
resolve(1); console.log(2); }).then(r => { console.log(r);
});
// 2 // 1
Generator 函数 #类似于Iterator单向产出
函数声明function和函数名中加 * 且函数中有yield和return
原理是利用指针
例子
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending'; }
var hw = helloWorldGenerator();
使用是
hw.next()
如果放在函数中需要加(yield)或者是把整个gen函数放入另一个gan函数中则yield * 函数名();
function* bar() { yield 'x'; yield* foo(); yield 'y'; }
或者在数组前添加yield * [1,2,3,4]就会依次返回
如 console.log('hellp'+(yield))
next()可以加参数,对应在函数中也需要参数
async 函数 #Generator函数的语法糖。
async表示含有异步函数的操作
await后面跟着需要等待的异步函数 #等同于yield
正则表达式
语法
字符
普通字符:字母、数字、汉字、下划线,匹配与之相同的一个字符
简单转义字符:\n(换行),\t(制表),\\(\本身)和 \^...(\^等有特殊作用的符号如要匹配自己的话要用转义)
标准字符集合
\d
\w
\s
.
自定义字符集合
[ab5@]
[^abc]
[f-k]
[^A-F0-3]
量词(Quantifier)
{n}
{m,n}
贪婪模式 (默认)
非贪婪模式 (在量词后面加 ? 例:{m,n}? )
{m,}
?
+
*
字符边界
^
$
\b
预搜索(零宽断言、环视)
(?=exp)
(?!exp)
(?<=exp)
(?<!exp)
匹配模式
IGNORECASE 忽略大小写模式
匹配时忽略大小写
默认是区分大小写的
SINGLELINE 单行模式
整个文本看作一个字符串,只有一个开头一个结尾
使小数点"."可以匹配包含换行符(\n)在内的任意字符
MULTILINE 多行模式
每行都是一个字符串
在多行模式下,如果需要仅匹配字符串开始和结束位置,可以使用\A和\Z
选择符和分组
| 分支结构
( ) 捕获组
(1)、在被修饰匹配次数的时候,括号中的表达式可以作为整体被修饰
(2)、取匹配结果的时候,括号中的表达式匹配到的内容可以被单独得到
(3)、每一对括号会分配一个编号,使用()的捕获根据左括号的顺序从1开始自动编号。捕获编号为零的第一个捕获是整个正则表达式模式匹配的文本
反向引用:通过反向引用,可以对分组已捕获的字符串进行引用。
(?:Expression) 非捕获组
vue
生命周期函数
图
分析生命周期相关方法的执行时机
生命周期
beforeCreate():这是我们遇到的第一个生命周期函数,表示实例完全被创建出来之前,会执行它…
created(): 这是遇到的第二个生命周期函数…
beforeMount():这是遇到的第3个生命周期函数,表示 模板已经在内存中编辑完成,但是尚未把模板渲染(挂载)到页面中。在 beforeMount 执行的时候,页面中的元素,还没有被真正替换过来,只是之前写的一些模板字符串。就像{{text}}这样
mounted():这是遇到的第四个生命周期函数,表示内存中的模板,已经真实的挂载到了页面中,用户已经可以看到渲染好的页面了。只要执行完这个生命周期,就表示整个vue实例已经初始化完毕了,此时,组件已经脱离了创建阶段,进入到了运行阶段。
beforeUpdate():这时候表示,我们的界面还没有被更新[但数据已经被更新了页面中显示的数据,还是旧的,此时data数据是最新的。页面尚未和最新的数据保持同步
update() : 这一步执行的是 先根据data中最新的数据,在内存中重新渲染出一份最新的内存dom树,当最新的内存dom树被更新后,会把最新的内存DOM树重新渲染到真实的页面中去,这时候,就完成了数据data(model层)->view(视图层)的更新,页面和data数据已经保持同步了,都是最新的。
beforeDestory :当执行 beforeDestory 钩子函数的时候,Vue实例就已经从运行阶段,进入到销毁阶段, 当执行beforeDestroy的时候,实例身上所有的data和所有的methods以及过滤器、指令...都处于可用状态,此时,还没有真正执行销毁的过程。
destroyed :当执行这个函数的时候,组件已经被完全销毁了,此时,组件中所有的数据,方法,指令,过滤器...都已经不可用了
原文链接:https://blog.csdn.net/u010716530/article/details/103754983
MVVM
图
基础指令 https://blog.csdn.net/u010716530/article/details/103754983
v-if 和v-show:条件指令
v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。
v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
相比之下,v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。
一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。
原文链接:https://blog.csdn.net/guorui_java/article/details/106917601
v-for:列表循环指令
在遍历对象的时候有多个index索引,遍历数字时是从1开始的。绑定key时属性值必须是number或者string
v-cloak 解决{{}}插值闪烁问题
v-text 会先执行 覆盖 元素中原本的内容 但是插值表达式只会覆盖自己的占位符,默认不会闪烁
v-html 渲染 html标签 覆盖元素中原有元素内容
v-bind: 简写为: 用来绑定数据 可以写合法的js表达式
v-on: 简写为 @ 用来点击事件
常用事件修饰符
stop 阻止冒泡 :外层和里层都有方法 点击里层会产生冒泡,也会触发外层的事件。
顺序 从里到外产生事件
prevent 阻止浏览器默认行为 :
a标签有浏览器默认行为。
capture 捕获事件 :点击里层先触发外层再触发里层 顺序从外到里产生事件
self 只触发自己本身的事件 不会产生冒泡和捕获事件 类似于阻止冒泡 但只针对自己那一层 最外层还是会被最里层冒泡冒到 stop 是阻止所有层次
once 事件只执行一次
class 绑定
1.数组带对象
单纯的对象
组件
局部组件
例子
全局组件
使用Vue.extend 来创建全局的Vue组件
使用 vue.component 来创建组件
如果使用了Vue.component 定义了全局组件的时候,组件名称使用了驼峰命名,在引用的时候大写的驼峰要改为小写,同时两个单词之间 使用-链接
Vue.component第一个参数:组件的名称,将来在引用组件的时候,就是一个标签形式来引入的,第二个参数:
Vue.extend 创建的组件,其中 template就是组件将来要展示的内容
注意:不论是哪种方式创建出来的组件,组件的template 属性指向的模板内容,必须有且只能有唯一的一个根元素。
使用 template 来创建组件
例子
组件里的data
组件可以有自己的data数据
组件的data和实例中的data有点不一样,实例中的data 可以为一个对象,但是组件中的data必须是一个方法。
组件中的data 除了必须为一个方法之外,这个方法内部,还必须返回一个对象才行。
组件中的data数据,使用方式,和实例中的data使用方式完全一样!
组件里data为什么必须是个方法返回个对象呢? 因为要确保每个实例里的数据是唯一的,独有的。如果data里的数据是放在实例外部的,会被其他实例共享。
父子组件通讯
父子组件传值,通过v-bind:(:)来传值,通过props来接收值
父组件用事件绑定机制传递方法给子组件—v-on 简写 @
emit @handle=show 父组件传show方法给子组件。子组件接收父组件的方法,并用$emit把子组件的值传给父组件
在父组件中接收子组件所有参数的同时,添加自定义参数
链接
https://www.jianshu.com/p/8e06a83c955d
动画
过渡类名实现动画
第三方类实现动画
在属性中声明js钩子 实现半场动画(只需要进场,不需要离场)
计算属性(Computed)
作用
使用插值表达式可以很方便地将数据渲染到页面元素上,但是插值表达式只能进行简单地计算,如果要进行复杂地计算或者逻辑,可以在vue地计算属性中完成
例子
计算属性缓存 vs 方法
为了能够快速响应,不会陷入线程阻塞
侦听属性(Watch)
作用
可以使用watch监听器来检测某个数据发生的变化。计算属性仅仅是对于依赖数据的变化后进行的数据操作。而监听属性Watch侧重于对监听中某个数据发生变化后执行的一系列功能逻辑操作
用法
监听器以key-value键值对的形式定义
Key是字符串,被监听的对象
value可以是:字符串(方法名),函数(可以获取到监听对象改变之前的值和改变之后的值),对象或数组
计算属性vs侦听属性
vuex
vuex是为了保存组件之间共享数据而诞生的,如果组件之间 有要共享的数据,可以直接挂载到vuex中,而不必通过父子组件之间传值了,如果组件的数据不需要共享,此时,这些不需要共享的私有数据,没有必要放到vuex中;
vuex,存放共享数据,data 存放组件私有的数据 props存放父组件传过来的数据
操作vuex里的state
直接操作store里的属性值,不推荐这种做法
推荐只通过mutations 提供的方法,才能操作对应的数据,
vuex里的getters
如果store中state上的数据,在对外提供的时候,需要做一层包装,那么推荐使用getters。
过滤器
概念
局部定义和全局定义过滤器
过滤器串联
指令(二)
v-once:只渲染元素和组件一次,优化性能
v-if:条件渲染
概念
根据表达式值得真假来渲染元素。与v-if搭配使用的还有v-else和v-else-if。V-if可以单独使用,v-else-if必须跟在v-if后面,v-else必须跟在v-if和v-else-if后面,v-else-if可以连续使用。
v-if
v-else
v-else-if
用key管理可复用的元素:vue会尽可能高效地进行渲染,通常会复用已有元素而不是重新渲染。切换不会清除用户已输入地内容,因为两个模板使用了相同的元素,input标签不会被替换,只是替换他的placeholder属性值
例子
1
v-show:用法和v-if大致一样(没有v-else),不同的是带有v-show地元素始终会被渲染并保留在DOM中
v-if和v-show的区别:
v-if是真正的条件渲染,它会确保在切换过程中,条件块内的事件监听器和子组件适当的被销毁和重建。
v-show总是渲染,它是简单的基于CSS切换显示,本质上是对Html元素的display属性进行设置。
初始渲染时,v-show开销更高,切换时v-if开销更高。
v-for:遍历数组或者枚举对象,进行迭代循环,或者是迭代数字。
例子
1
v-for渲染元素列表时,默认使用“就地更新”策略。即如果数据顺序改变,vue不会移动dom元素以匹配改变后的顺序,而是就地更新每个元素,有时会导致渲染出错。这时应该为每一项提供唯一的key属性值以管理可复用元素。
对象变更检测注意事项
由于js的限制,vue不能检测对象属性的添加或删除。如果一个对象的属性没有在data字段中声明,它就不是响应式的。
对于已经创建的实例,vue不允许动态添加根级别的响应式属性。但可以使用Vue.set(object,propertyName,value)向嵌套对象添加响应式属性。
v-for和v-if一起使用:v-for优先级高,v-if运行于每个v-for。
react和vue
1.相似
都是用于创建UI的JavaScript库;
都快速轻便(专注于创造前端的富应用。不同于早期的JavaScript框架“功能齐全”,)
都有基于组件的架构;
都是用虚拟DOM;
都可放入单个HTML文件中,或者成为更复杂webpack设置中的模块;
都有独立但常用的路由器和状态管理库(Reat与Vue只有框架的骨架,其他的功能如路由、状态管理等是框架分离的组件。)
它们之间的最大区别是Vue通常使用HTML模板文件,而React则完全是JavaScript。Vue有双向绑定语法糖。
2.区别
监听数据变化的实现原理不同
Vue通过 getter/setter以及一些函数的劫持,能精确知道数据变化。
React默认是通过比较引用的方式(diff)进行的,如果不优化可能导致大量不必要的VDOM的重新渲染。为什么React不精确监听数据变化呢?这是因为Vue和React设计理念上的区别,Vue使用的是可变数据,而React更强调数据的不可变,两者没有好坏之分,Vue更加简单,而React构建大型应用的时候更加鲁棒。
数据流不同
Vue1.0中可以实现两种双向绑定:父子组件之间,props可以双向绑定;组件与DOM之间可以通过v-model双向绑定。Vue2.x中去掉了第一种,也就是父子组件之间不能双向绑定了(但是提供了一个语法糖自动帮你通过事件的方式修改),并且Vue2.x已经不鼓励组件对自己的 props进行任何修改了。
React一直不支持双向绑定,提倡单向数据流,称为onChange/setState()模式。
模板渲染方式不同
在表层上,模板的语法不同,React通过JSX渲染模板。Vue通过一种拓展的HTML语法进行渲染,但其实这只是表面现象,毕竟React并不必须依赖JSX。
在深层上,模板的原理不同,这才是他们的本质区别:React是在组件JS代码中,通过原生JS实现模板中的常见语法,比如插值,条件,循环等,都是通过JS语法实现的,更加纯粹更加原生。而Vue是在和组件JS代码分离的单独的模板中,通过指令来实现的,比如条件语句就需要 v-if 来实现对这一点,这样的做法显得有些独特,会把HTML弄得很乱。
框架本质不同
Vue本质是MVVM框架,由MVC发展而来;
React是前端组件化框架,由后端组件化发展而来。
链接
https://blog.csdn.net/Sicily_winner/article/details/103915061
typescript
ES6 | Map和Set
Map和Set
Map
【创建】let map = new Map();
添加数据
通过set方法添加数据
通过链式的方法添加
可以用键-值对数组初始化映射
【属性】map.size
常用方法
map.get("A");
map.has("A");
map.delete("A");
map.clear()
循环Map
示例Map对象:
【遍历】keys() / values()方法
keys()方法
values()方法
entries()方法
entries()
解构(直接的只用key和value 遍历)
通过条目对键-值对进行循环非常常见,这是映射的默认值
Set
【创建】let set = new Set();
【添加】set.add('APPLE');
has()、delete()、clear() 方法
set.size 属性
循环set
【常用特殊用途】类、组件
属性检查器
@property({type:cc.Integer, displayName:"int_number", tooltip:"是cc类中: 整形属性"})
@property({type:Number, slide:true, range:[-5,5]})
【注意】声明数组时:@property({ type : [cc.Button] })
日期(javaScript的 Date类)
日期、时间
testDate.toLocaleString()
testDate.toLocaleDateString()
testDate.toLocaleTimeString()
年、月、日
testDate.getFullYear()
testDate.getMonth()
testDate.getDate()
星期
testDate.getDay()
小时数、分钟数、秒数、毫秒数
testDate.getHours()
testDate.getMinutes()
testDate.getSeconds()
testDate.getMilliseconds()
BlockInputEvents 组件
持久化数据 & 性能优化
【持久化数据】JSON格式
初见 JSON结构
生成步骤
解析过程
【拖拽】属性中拖入 .json文件(cc.JsonAsset 类)
【动态】 resources 文件夹下:
循环访问方式
【性能优化】DrawCall
【为什么?】要减少 DrawCall
【举个栗子】传输文件那个快
问:尝试在两个硬盘之间传输文件,传输 1 个 1MB 的文件和传输 1024 个 1KB 的文件,同样是传输了共 1MB 的文件,哪个更快?
答:传输 1 个 1MB 的文件要比传输 1024 个 1KB 的文件要快得多得多。因为在每一个文件传输前,CPU 都需要做许多额外的工作来保证文件能够正确地被传输,而这些额外工作造成了大量额外的性能和时间开销,导致传输速度下降。
图形渲染管线的大致流程如下:
【谁的锅?】
1. GPU 渲染图像的速度 非常快
2. CPU 的内存显存读写、数据处理和渲染状态切换,相对于 GPU 渲染来说是非常非常慢
如何减少 DrawCall ?
静态合图
【 .pac文件】自动图集资源(Auto Atlas)
动态合图(Dynamic Atlas)
位图字体(BMFont)
参考文献
https://blog.csdn.net/iFasWind/article/details/107094075
TypeScript语言 | 运算符
第一个 TypeScript 程序
输出 "Hello World":
TypeScript 面向对象编程实例:
【判断string是不是number】const isFixedString = (s: string) => !isNaN(+s) && isFinite(+s) && !/e/i.test(s)
从数组中随机取出几个数组元素
代码如下:
【666】汉字 --转-> 拼音
代码如下:
运算符
TypeScript中==、===、!==、!=的作用
==
例子:(A == B)为 false,A == '10' 为 true
===
例子:(A === B)为 false,A === '10' 为 false
!=
例子:(A != B) 为 true
!==
例子:(A !== '10') 为 true
vcCode 中配置
TypeScripy 关键字 | for语句 特殊用法
共46个
for语句 特殊用法
for...in 循环
for…of 循环
在 ES6 中引入,以替代 for...in 和 forEach() ,并支持新的迭代协议
forEach、every 和 some 是 JavaScript 的循环语法
【every、some 取代 forEach】因为 forEach 在 iteration 中是无法返回的
基础类型 | 联合类型
typeof 运算符
【任意类型】any
【数字类型】number
【字符串】string
【注意】反引号(`)来定义多行文本和内嵌表达式
【布尔类型】boolean
【数组类型】
【元组】
【枚举】enum
【空返回函数】void
【null】null
【undefined】undefined
【详细】Null 和 Undefined
在TypeScript中启用严格的空校验(--strictNullChecks)特性,就可以使得null 和 undefined 只能被赋值给 void 或本身对应的类型
【never】never
never 是其它类型(包括 null 和 undefined)的子类型,代表从不会出现的值
【联合类型】
【语法格式】Type1 | Type2 | Type3
【示例】对象,参数等,甚至可以是数组
联合类型 对象
联合类型 参数
联合类型 数组
TypeScript 函数
【普通的】含参数有返回值的函数
【可选参数】可选参数使用问号标识 ?
【默认参数】
【剩余参数】剩余参数语法允许我们将一个不确定数量的参数作为一个数组传入
【匿名函数】
匿名函数自调用
【构造函数】
【递归函数】
【Lambda函数】箭头函数
不指定函数的参数类型,通过函数内来推断参数类型:
单个参数 () 是可选的:
无参数时可以设置空括号:
【函数重载】
标准库函数
Number对象
【语法】var num = new Number(value);
Number对象【属性】constructor构造函数
属性 | 具体描述
MAX_VALUE、MIN_VALUE、NEGATIVE_INFINITY、POSITIVE_INFINITY 属性
NaN属性 | 实例
prototype属性 | 实例
Number对象【方法】
toExponential()
toFixed()
toLocaleString()
toPrecision()
toString()
valueOf()
String对象
【语法】var txt = new String("string");
String对象【属性】
属性 | 具体描述
String对象【方法】
charAt()
【注意】没有的下标
charCodeAt()
concat()
indexOf()
lastIndexOf()
localeCompare()
【相等】0【大于】1【小于】-1
match()
replace()
search()
slice()
split()
substr()
substring()
toLocaleLowerCase()
toLocaleUpperCase()
toLowerCase()
toUpperCase()
toString()
valueOf()
数组 && Array对象
【声明数组的语法格式】var array_name : datatype[];
【Array对象语法】var arr_names : number[] = new Array(4);
数组解构
多维数组
【语法】var arr_name : datatype[][] = [ [val1, val2, val3], [v1, v2, v3] ]
数组方法
concat()
every()【检测时】不满足时,立即跳出
【注意】这个 isBigEnough函数的参数数目可以是:1、2、3个
some()
filter()
【注意】这个 isBigEnough函数的参数数目可以是:1、2、3个
forEach()
slice()
join()
toString()
indexOf()
lastIndexOf()
map()【常用】Math类方法时
splice()
push()
pop()
unshift()
shift()
reduce()
reduceRight()
将数组元素计算为一个值(从右到左)
reduceRight()
reverse()
sort()
元组
【语法】var mytuple = [10, "Runoob"];
元组运算
示例
push()
pop()
解构元组
TypeScript 接口、类、对象、命名空间、模块、声明文件
TypeScript 接口
【TypeScript 接口定义】interface interface_name { ...... }; | 示例
示例:定义了一个接口 IPerson,定义变量 customer,类型为 IPerson接口
联合类型 和 接口
【注意】声明的方式
接口 和 数组
接口 与 函数
鸭子类型 是动态类型的一种风格,是多态(polymorphism)的一种形式
接口继承
单接口继承 语法格式:
示例【注意】定义对象的语句
多接口继承 语法格式:
示例【注意】定义对象,初始化接口属性的操作
TypeScript 类 | 类继承
【类包含数据成员】字段、方法、constructor构造函数 | 示例
示例:
访问控制修饰符
【注意】TypeScript 只支持 3 种不同的访问权限
【static 关键字】静态字段、静态方法
示例:
【instanceof 关键字】
示例:
类的继承
【注意】派生类构造函数中 调用父类构造函数:即 super()函数
继承类的【方法重写】
【使用】super.其他父类函数();
类 和 接口
示例:
TypeScript 对象(Key-Value)
【注意】格式:元素以 ','逗号分割,最后一个元素后,不能多加
【注意】Typescript 中的对象 必须是 特定类型的实例
TypeScript 命名空间
【语法格式】定义了一个命名空间 SomeNameSpaceName | 示例
【在另外一个命名空间中】调用【语法格式】如下:
如果一个命名空间在一个单独的 TypeScript 文件中,则应使用三斜杠 /// 引用它【语法格式】如下:
嵌套命名空间
【语法格式】
实例 | 成员的访问使用点号 . 来实现
实例 | 演示同命名空间,多个不同文件中的使用
【注意操作】1.使用了另外一个命名空间中调用;2.引用不同文件
TypeScript 模块
模块是在其自身的作用域里执行,并不是在全局作用域
【模块间 -> 关系】是通过在文件级别上使用 import、export关键字 建立的
模块加载器【运行时作用】在执行此模块代码前,去查找并执行其所有的依赖
示例:1.import 导入其他模块导出的各种变量;2.export导出操作
TypeScript 声明文件
https://www.runoob.com/typescript/ts-ambient.html
webpack
package.json
https://mp.weixin.qq.com/s/dUXdwaH55hIl0C0pTIsUiw
微服务分布式
分布式锁
安全
互联网企业安全
· 基础架构与网络安全:
IDC、生产网络的各种链路和设备、服务器、大量的服务端程序和中间件,数据库等,偏运维侧,跟漏洞扫描、打补丁、ACL、安全配置、网络和主机入侵检测等这些事情相关性比较大,约占不到30%的工作量。
· 应用与交付安全:
对各BG、事业部、业务线自研的产品进行应用层面的安全评估,代码审计,渗透测试,代码框架的安全功能,应用层的防火墙,应用层的入侵检测等,属于有点“繁琐”的工程,“撇不掉、理还乱”,大部分甲方团队都没有足够的人力去应付产品线交付的数量庞大的代码,没有能力去实践完整的SDL,这部分是当下比较有挑战的安全业务,整体比重大于30%,还在持续增长中。
· 业务安全:
包括账号安全、交易风控、征信、反价格爬虫、反作弊、反bot程序、反欺诈、反钓鱼、反垃圾信息、舆情监控(内容信息安全)、防游戏外挂、打击黑色产业链、安全情报等,是在“吃饱饭”之后“思淫欲”的进阶需求,在基础安全问题解决之后,越来越受到重视的领域。整体约占30%左右的工作量,有的甚至大过50%。这里也已经纷纷出现乙方的创业型公司试图解决这些痛点。
文章链接
http://www.bjdcfy.com/fangan/zyxtyjfa/2020-7/1331594.html
安全测试技术
1、登录认证模块测试
1.1 暴力破解
原理:指针对应用系统用户账号与密码进行的穷举测试,针对账号密码进行逐一比较,直到找到正确的账号和密码
修复:增加验证码,配置登录失败次数限制策略,增加手机验证码实现双因素认证的防暴力破解机制
1.2 加密传输测试
原理:使用不安全的传输协议
修复:部署有效的SSL证书服务
1.3 Session 测试
会话固定
原理:注销系统后,再次登录系统后本次授权的SessionID是否使用上次相同的SessionID值,如相同,则存在固定会话风险
修复:销毁游览器留存的Session认证会话,要求客户端游览器重新生成Session认证会话属性标识
会话注销测试
原理:用户注销退出系统时、如果未能清空Session认证会话,认证会话将持续有效,此时攻击者获得Session认证会话导致用户权限被盗取
修复:服务器应及时销毁Session认证会话信息,清空客户端游览器Session属性标识
会话超时测试
原理:用户成功登录系统获得Session认证后,没有设置Session会话的生命周期,导致会话持续化
修复:对每个生成胡Session认证会话配置生命周期,常规业务30分钟以内
Cookie仿冒测试
原理:攻击者修复Cookie中的身份表示,从而达到仿冒其他用户身份的目的
修复:建议客户端标识的用户敏感信息数据,使用Session会话认证方式,避免被他人仿冒
密文对比认证测试
原理:采用前台游览器客户端对密码进行Hash加密,会泄露密码加密方式
修复:密码加密过程及密文对比过程放置在服务器后台
登录失败信息测试
原理:系统在页面显示用户登录失败的信息,从而进行有针对性的暴力破解口令测试
修复:对系统登录失败提示语句表达内容进行统一的模糊描述
2、业务办理模块测试
2.1 订单ID篡改测试
原理:开发人员没有考虑登录后用户间权限隔离的问题是,就会导致权限绕过漏洞
修复:查看订单需要通过Session机制判断用户身份,做好平行权限控制,服务端需校验相应订单是否和登录身份一致
2.2 手机号码篡改测试
原理:手机号码经常代表一个用户的身份,测试是否存在越权漏洞
修复:后台请求要通过Session机制判断用户身份,对于手机APP程序,不要相信从手机中直接读取的手机号码,需要做好常规的身份认证,规范登录流程,防止未授权登录
2.3 用户ID篡改测试
原理:发现在GET或POSt请求中有userid这类参数传输,并且后台通过此参数显示对应用户隐私信息,这就导致了攻击者可以通过篡改用户ID越权访问其他yoghurt隐私信息.
修复:后台请求要通过Session机制判断用户身份,不要相信客户端传来的用户ID,服务端需要晓燕Userid是否和登录者的Session身份不一致
2.4 邮件和用户篡改测试
原理:发送邮件或者站内信,直接篡改其中发件人参数,导致攻击者可以伪造发信人进行钓鱼等操作,属于平行权限绕过漏洞
修复:写信,发送消息时需要通过Session机制判断用户身份,需要对客户端传输邮件、发件人、服务端需要校验邮箱、发件人是否和登录者身份一致
2.5商品编号篡改测试
原理:如在生成商品订单、跳转支付页面时,修改HTTP请求中的金额参数,实现一分钱买东西,利用此业务漏洞以低价购买高价的物品.
修复:商品金额不要在客户端传入,防止被篡改
2.6竞争条件测试
原理:竞争条件是指在操作系统编程时会遇到的安全问题,当两个或者多个进程试图在同一时刻访问共享内存或读写某些共享数据时,最后进程结果取决于线程执行的顺序(线程运行的时许)称为竞争条件(Race Conditions)
测试过程:攻击者在提交订单时抓包,然后设置很多线程重方此包,个别请求就有可能绕过金额,次数的判断,交易成功
修复:在处理订单、支付等关键业务时,使用悲观锁或者乐观锁保证事物的ACID特性(原子性,一致性,隔离性,持久性),避免数据脏读(一个事物读取了另一个事务未提交的数据),
3、业务授权访问模块测试
非授权访问测试
原理:用户没有在通过认证授权的情况下能够直接访问需要通过认证才能访问到额页面或文本信息
修复:对未授权访问页面做Session认证,并对用户访问的每一个URL做身份鉴别,正确校验用户ID及Token
越权测试
原理:越权一般分为水平越权和垂直越权,水平越权指相同权限的不同用户可以互相访问,垂直越权是指使用权限低的用户可以访问权限较高的用户
修复:服务端需要校验身份唯一性,自己的身份只能查看、修改自己的信息
4、输入输出模块测试
SQL注入测试
原理:通过把SQL命令插入Web表单提交或输入域名页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令的目的
类型:GET、POST、Cookie | 数字型、字符型
测试方法:报错型、延时型、盲注型、布尔型、宽字节、堆叠注入
修复:关键字过滤: and or select declare update xp_cmdshell ' " ;等 ,最小权限原则、参数化查询攻击
XSS测试
原理:攻击者可以往Web页面里插入恶意JavaScript、HTML石花洞,用户操纵客户会话和Cookie
类型:存储型、反射型、DOM型
修复:过滤关键符号及符号的Unicode值,HTML实体化 如:htmlspecialchars
命令执行测试
原理:用户可以控制命令执行函数中的参数时,将可注入恶意系统命令到正常命令中,造成命令执行攻击.如PHP中 system,exec shell_exec
修复:禁用执行命令的函数,对参数进行过滤,对敏感字符进行转义 如php (escapeshellcmd)
5、回退模块测试
原理:Web业务在密码修改成功后或者订单付款等业务模块 ,如果能返回上一步重复操作,而且还能更改或者重置结果,则存在业务回退漏洞
修复:业务流程有多步得情况,首先判断该步骤的请求是否是上一步骤的业务发起的,如果不是则返回错误或页面无效
6、验证码机制测试
验证码暴力破解测试
原理:如果没有对验证码的失效时间和尝试失败的次数做限制,攻击者就可以通过尝试这个区间内所有数字来进行暴力破解攻击
修复:设置验证码失效时间建议180秒,限制单位时间内验证码失败的尝试次数
验证码重复使用测试
原理:如果验证码认证成功后没有将session及时情况,将会导致验证码首次认证成功之后可重复使用。
修复:建议验证码在一次认证成功后,服务端情况验证成功的session,以防止验证码一次认证反复使用的问题
验证码客户端回显测试
原理:验证码在客户端生成而非服务端生产时,会造成此问题,可借助游览器工具查看与服务器进行的交互详细信息
修复:禁止验证码在客户端生成,设置验证码时效性,随机生成验证码,使用一次即失效
验证码绕过
原理:通过修改前端提交服务器返回的数据,可以实现绕过验证码
修复:服务端增加验证码认证机制,客户端提交的验证码进行二次校验
验证码自动识别测试
原理:字符识别就是把处理后的图片还原回字符文本的过程,验证码的识别流程:图像二值化处理-》去干扰-》字符分割-》字符识别
修复:增加背景元素干扰、字体扭曲、使用公式、逻辑作为验证码
7、业务数据安全测试
商品支付金额篡改测试
原理:在订购类交易流程中,容易出现服务器端未对用户提交的业务数据进行强制校验,过度信赖客户端提交的业务数据而导致商品金额篡改漏洞
修复:商品信息等原始数据的校验应来自于服务器端,不接受客户端传递过来的值
商品订购数量篡改测试
原理:通过在业务流程中抓包修改订购商品数量等字段后业务系统以修改该后的数量完成业务流程
修复:服务端应该考虑风险控制,对异常情况的交易行为直接予以限制、阻断
前端JS限制绕过测试
原理:服务器仅在页面通过JS脚本限制,没有在服务端校验用户提交的数量
修复:商品信息等原始数据的校验应来自于服务器端,不应该相信客户端传递过来的值,确保业务重要数据在平台间传输的一致
请求重放测试
原理:是业务逻辑漏洞中一种常见的由设计缺陷所引发的漏洞,参照订购商品的正常流程请求,进行完全模拟正常业务流程的重放操作,来实现一次次购买多次下单等违背业务逻辑的漏洞
修复:Token不应该能重复提交,应该对订单Token对应的信息内容进行强校验
业务上限测试
原理:服务端没有对用户提交的查询范围、数量、金额等数据进行严格校验而引发的逻辑漏洞
修复:服务端对订单Token对应的订购信息内容、身份等信息进行强校验,对产生异常情况的交易行为应当直接限制、阻断
8、业务流程乱序测试
业务流程绕过测试
原理:假设将业务流程分为三部,第一步,注册并发送验证码,第二步,输入验证码,第三步,注册成功,如果可以在第三步进行抓包改包,将手机号替换其他人,如果还能注册成功,则绕过了正常的业务流程
修复:对敏感信息进行教秘处理,并在服务端对其进行二次对比
9、密码找回模块测试
验证码客户端回显测试
原理:找回密码测试中验证码会回显在响应中
修复:避免返回验证码到响应包中,验证码一定要放在服务端校验
验证码暴力破解测试
原理:有些应用程序在验证码发送功能中验证码位数及复杂性较弱,没有对验证码做次数限制而导致验证码可以被暴力枚举
修复:建议用户输入的验证码校验采取错误次数限制,提高验证码的复杂度
接口参数账号修改测试
原理:如果服务端对账号的控制逻辑不当,会导致在找回密码逻辑中,原有账号被篡改为其他账号,服务端把凭证发送给篡改后的账号
修复:对找回密码的Token做一对一校验,一个Token只能修改一个用户,同时保证Token不泄露
Response状态值修改测试
原理:修改请求的响应结果来达到密码重置的目的,存在这种漏洞网站或者手机App往往是因为校验不严格而导致的
修复:不要在前端利用服务端返回的值判断是否可以修改该密码,要把整个校验环节交给服务端验证
Session覆盖测试
原理:服务端通过读取当前Session会话来判断要修改密码的账号,这种情况对Session中的内容做修改来达到任意密码重置的目的
修复:在重置密码请求中一定要对修改的账号和凭证是否一致做进一步校验
弱Token设计缺陷测试
原理:找回密码链接通畅会加入校验参数来确认链接的有效性,通过校验参数的值与数据库生成的值是否一致来判断当前找回密码的链接是否有效,如果这个Token值可以被猜测,就可以通过暴力枚举的方式获得,如:时间戳,uid加密md5,base64编码等
修复:密码找回的Token不能使用有规律可循的数字字符,应该使用负载的Token生成机制使攻击者无法推测出具体的值
密码找回流程绕过测试
原理:在找回密码逻辑中,第二部校验凭证最为重要,不是账号主人是无法收到校验凭证的,如果可以绕过第二步凭证校验,直接进入第三步重置密码。
修复:防止跳过验证步骤一定要在后端逻辑校验中确认上一步流程已经完成
10、业务接口调用模块测试
接口调用重放测试
原理:如果业务经过调用(重发)后多次生成有效的业务或数据结果,则存在接口调用(重放)问题
修复:对生成订单环节采用验证码机制,每一个订单使用唯一的Token,订单提交一次后,Token失效
接口调用遍历测试
原理:如果可以通过BurpSuite等改包工具对某一个参数进行遍历,如果不同的值对应不同用户的信息,则存在该漏洞
修复:在Session中存储当前用户的凭证或者Id,只有传入凭证或者id参数值与Session中的一致才返回数据内容
接口调用参数篡改
原理:在短信、邮件调用业务环节中,修改该对应请求手机号码或邮件地址参数提交后,如果能收到系统发送的信息,则表示接口数据调用参数可更改
修复:在重要的发送短信验证码业务中,不要从客户请求的参数中获取。要与Session中的凭证进行对比,验证通过后才允许进行业务操作
接口未授权访问/调用测试
原理:在未登录的情况下,使用游览器访问对应敏感功能的请求,如果返回的数据与登录状态后的一致,则表明存在此缺陷
修复:采用Token校验,在Url中添加一个Token参数,接口被待用时,后端对会话状态进行验证
Callback自定义测试
原理:但使用Ajax 异步传输数据时,非同源域名之间会存在限制,采用一种解决方法时JSONP (JSON with Padding)基本原理时利用HTML里<script></script>元素标签,远程调用JSON文件来实现数据传递,JSONP使用Callback(回调函数)来声明回调时所使用的函数名
修复:严格定义HTTP 响应中的Content-Type为json数据格式,Content-Type:application/json ,建立callback函数白名单,对callback参数进行HTML实体编码来过滤<、>等符号
WebService测试
原理:WebService是一种跨编程语言和跨操作系统平台的远程调用技术,WebService就是一个应用程序向外界暴露除一个能通过Web进行调用的API,如果完全信任用户的输入,不进行过滤,则由可能导致SQL注入漏洞的发生
修复:为WebService 添加身份认证,认证成功后才允许访问和调用
tu
1
业务场景问题
秒杀
秒杀
tu
分布式锁
分布式锁
演进
分布式分段锁
实现方法
图
其实这就是分段加锁。你想,假如你现在iphone有1000个库存,那么你完全可以给拆成20个库存段,要是你愿意,可以在数据库的表里建20个库存字段,比如stock_01,stock_02,类似这样的,也可以在redis之类的地方放20个库存key。
就是把你的1000件库存给他拆开,每个库存段是50件库存,比如stock_01对应50件库存,stock_02对应50件库存。
每秒1000个请求过来了,好!此时其实可以是自己写一个简单的随机算法,每个请求都是随机在20个分段库存里,选择一个进行加锁。
同时可以有最多20个下单请求一起执行,每个下单请求锁了一个库存分段,然后在业务逻辑里面,就对数据库或者是Redis中的那个分段库存进行操作即可,包括查库存 -> 判断库存是否充足 -> 扣减库存。
可以并发处理掉20个下单请求,那么1秒,也就可以依次处理掉20 * 50 = 1000个对iphone的下单请求了。
一旦对某个数据做了分段处理之后,有一个坑大家一定要注意:就是如果某个下单请求,咔嚓加锁,然后发现这个分段库存里的库存不足了,此时咋办?
这时你得自动释放锁,然后立马换下一个分段库存,再次尝试加锁后尝试处理。这个过程一定要实现。
https://mp.weixin.qq.com/s/Iwa75V53PDVyplr9jMeA3w
https://mp.weixin.qq.com/s/PMLmjx7ZVgmoU3p3IhDhUA
https://mp.weixin.qq.com/s/tVr6lUbLLG8OyNfto09rPQ
缓存读写策略
Cache Aside Pattern(旁路缓存模式)
Read/Write Through Pattern(读写穿透)
图
Write Behind Pattern(异步缓存写入)
缓存雪崩
原因
图
解决方案
图
事前:redis高可用,主从+哨兵,redis cluster,避免全盘崩溃
事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL被打死
事后:redis持久化,快速恢复缓存数据
延时任务队列
延时任务队列的原理与实现总结
实现
Kafka
原生Kafka默认是不支持延时消息的,需要开发者自己实现一层代理服务,比如发送端将消息发送到延时Topic,代理服务消费延时Topic的消息然后转存起来,代理服务通过一定的算法,计算延时消息所附带的延时时间是否到达,然后将延时消息取出来并发送到实际的Topic里面,消费端从实际的Topic里面进行消费。
RabbitMQ
RabbitMQ实现延时消息有两种方案,第一种是采用rabbitmq-delayed-message-exchange 插件实现,第二种则是利用DLX(Dead Letter Exchanges)+ TTL(消息存活时间)来间接实现。大致的实现思路如下:
创建一个普通队列delay_queue,为此队列设置死信交换机 (通过x-dead-letter-exchange参数) 和 RoutingKey (通过x-dead-letter-routing-key参数),生产者将向delay_queue发送延时消息。
创建步骤1中设置的死信交换机,同时创建一个目标队列 target_queue,并使用步骤1中设置的RoutingKey将两者绑定起来。消费者将从target_queue里面消费延时消息。
设置消息的存活时间TTL,可以在步骤1中设置到队列级别delay_queue的消息存活时间,或者在发送消息时动态设置消息级别的存活时间。
RocketMQ
开源RocketMQ支持延迟消息,但是不支持秒级精度。默认支持18个level的延迟消息,这是通过broker端的messageDelayLevel配置项确定的
messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
消息队列服务在启动时,会创建一个内部topic:SCHEDULE_TOPIC_XXXX,根据延迟level的个数,创建对应数量的队列。生产者发送消息时可以设置延时等级
https://mp.weixin.qq.com/s/SEJObnJF_dhNOoXabDSQFA
分布式ID生成
分布式ID生成
6 种常见分布式唯一ID生成策略及它们的优缺点对比
双写不一致问题
问题解决
消息重复消费
l[ink]
https://mp.weixin.qq.com/s/EOU2haFWfRI01PZEfsrgkA
CAS和ABA
[基于数组的无锁队列]
定时任务
https://mp.weixin.qq.com/s/4Rn0c1zGk1-y1-ZRHKjqaw
限流
https://mp.weixin.qq.com/s/RRzsc3UASgJcDX7-FdbArQ
生产问题调查
某个SQL导致数据库CPU飙高,如何快速定位
https://mp.weixin.qq.com/s/YKh-TNS7T7Zo7hp3c3y9_A
系统运行缓慢,CPU 100%,以及Full GC次数过多问题的排查思路
通过 top命令查看CPU情况,如果CPU比较高,则通过 top-Hp<pid>命令查看当前进程的各个线程运行情况,找出CPU过高的线程之后,将其线程id转换为十六进制的表现形式,然后在jstack日志中查看该线程主要在进行的工作。这里又分为两种情况
如果是正常的用户线程,则通过该线程的堆栈信息查看其具体是在哪处用户代码处运行比较消耗CPU;
如果该线程是 VMThread,则通过 jstat-gcutil<pid><period><times>命令监控当前系统的GC状况,然后通过 jmapdump:format=b,file=<filepath><pid>导出系统当前的内存数据。导出之后将内存情况放到eclipse的mat工具中进行分析即可得出内存中主要是什么对象比较消耗内存,进而可以处理相关代码;
如果通过 top命令看到CPU并不高,并且系统内存占用率也比较低。此时就可以考虑是否是由于另外三种情况导致的问题。具体的可以根据具体情况分析:
如果是接口调用比较耗时,并且是不定时出现,则可以通过压测的方式加大阻塞点出现的频率,从而通过 jstack查看堆 栈信息,找到阻塞点;
如果是某个功能突然出现停滞的状况,这种情况也无法复现,此时可以通过多次导出 jstack日志的方式对比哪些用户线程是一直都处于等待状态,这些线程就是可能存在问题的线程;
如果通过 jstack可以查看到死锁状态,则可以检查产生死锁的两个线程的具体阻塞点,从而处理相应的问题。
线上服务内存OOM问题定位三板斧
链接
https://mp.weixin.qq.com/s/PY2MB3-kt5xnjnh3Yb7WvA
DevOps
图
k8s
WHAT
架构
Master 节点
Master 是 Kubernetes Cluster 的大脑
运行着如下 Daemon 服务
kube-apiserver
API Server 提供 HTTP/HTTPS RESTful API,即 Kubernetes API。API Server 是 Kubernetes Cluster 的前端接口,各种客户端工具(CLI 或 UI)以及 Kubernetes 其他组件可以通过它管理 Cluster 的各种资源。
kube-scheduler
Scheduler 负责决定将 Pod 放在哪个 Node 上运行。Scheduler 在调度时会充分考虑 Cluster 的拓扑结构,当前各个节点的负载,以及应用对高可用、性能、数据亲和性的需求。
Controller Manager 负责管理 Cluster 各种资源,保证资源处于预期的状态。Controller Manager 由多种 controller 组成,包括 replication controller、endpoints controller、namespace controller、serviceaccounts controller 等。 不同的 controller 管理不同的资源。例如 replication controller 管理 Deployment、StatefulSet、DaemonSet 的生命周期,namespace controller 管理 Namespace 资源。
etcd
etcd 负责保存 Kubernetes Cluster 的配置信息和各种资源的状态信息。当数据发生变化时,etcd 会快速地通知 Kubernetes 相关组件。
Pod 网络(例如 flannel)
Node
Node是 Pod 运行的地方
Kubernetes 支持 Docker、rkt 等容器 Runtime
Node上运行的 Kubernetes 组件有 kubelet、kube-proxy 和 Pod 网络
kubelet
kubelet 是 Node 的 agent,当 Scheduler 确定在某个 Node 上运行 Pod 后,会将 Pod 的具体配置信息(image、volume 等)发送给该节点的 kubelet,kubelet 根据这些信息创建和运行容器,并向 Master 报告运行状态。
kube-proxy
每个 Node 都会运行 kube-proxy 服务,它负责将访问 service 的 TCP/UPD 数据流转发到后端的容器。如果有多个副本,kube-proxy 会实现负载均衡。
node状态
图
Pod
Service
Service 定义了这样一种抽象:一个 Pod 的逻辑分组,一种可以访问它们的策略 —— 通常称为微服务。 这一组 Pod 能够被 Service 访问到
在 Kubernetes 集群中,每个 Node 运行一个 kube-proxy 进程。kube-proxy 负责为 Service 实现了一种 VIP(虚拟 IP)的形式
在 Kubernetes 集群中,每个 Node 运行一个 kube-proxy 进程。kube-proxy 负责为 Service 实现了一种 VIP(虚拟 IP)的形式
userspace 代理模式
通过 round-robin 算法来选择 backend Pod
iptables 代理模式
组件
核心组件
概述
etcd保存了整个集群的状态;
apiserver提供了资源操作的唯一入口,并提供认证、授权、访问控制、API注册和发现等机制;
controller manager负责维护集群的状态,比如故障检测、自动扩展、滚动更新等;
scheduler负责资源的调度,按照预定的调度策略将Pod调度到相应的机器上;
kubelet负责维护容器的生命周期,同时也负责Volume(CVI)和网络(CNI)的管理;
Container runtime负责镜像管理以及Pod和容器的真正运行(CRI);
kube-proxy负责为Service提供cluster内部的服务发现和负载均衡;
图1
图2
Master ASCE
controller manager
如果说APIServer做的是“前台”的工作的话,那controller manager就是负责“后台”的。
每个资源一般都对应有一个控制器,而controller manager就是负责管理这些控制器的。
比如我们通过APIServer创建一个pod,当这个pod创建成功后,APIServer的任务就算完成了。
而后面保证Pod的状态始终和我们预期的一样的重任就由controller manager去保证了。
Controller Manager 是集群的大脑,是确保整个集群动起来的关键; 其作用是确保 Kubernetes 遵循声明式系统规范,确保系统的真实状态(Actual State)与用户定义的期望状态(Desired State 一直);
Controller Manager 是多个控制器的组合,每个 Controller 事实上都是一个 control loop,负责侦听其管控的对象,当对象发生变更时完成配置;
Controller 配置失败通常会触发自动重试,整个集群会在控制器不断重试的机制下确保最终一致性( Eventual Consistency)。
原理
etcd
etcd是一个高可用的键值存储系统,Kubernetes使用它来存储各个资源的状态,从而实现了Restful的API。
组成
网路层:提供网路数据读写、监听服务端口,完成集群结点之间的数据通信,收发客户端数据。
raft模块:完整实现了raft协议。
存储模块:KV存储,wal存储,snapshot管理
复制状态机:状态机的数据维护在内存中,定期持久化到磁盘,每次写请求都会持久化到wal文件,根据写请求的内容修改状态机的数据。
raft分布式协议
组成
主节点leader
候选节点candidate
从节点flower
客户端只能从主节点写数据而从节点读取数据。
raft选主流程
初始化都是flower状态结点,等待100-300ms没有收到leader的心跳之后就变成了候选节点后给大家发选票,而候选人或得大多数结点的选票就变成了leader结点。
raft事务提交
每次提交事务先提交给leader,leader会暂存数据之后,复制数据到flower结点,等待大多数结点提交事务成功之后再将leader的事务提交。
raft选举超时是如何处理
等待时间短的结点会先发选票从而变为主节点。
但是如果2个候选或得的票数一样多则加时赛。直到选的票数最多的那个。
raft心跳超时
每个结点都会记录主节点是谁,并和主节点维持一个心跳超时时间,一旦没有收到主节点的回复从节点就要重新选举候选人结点。
raft的集群中断、脑裂问题并如何恢复
结点之间部分结点丢失通信,则会导致主节点的数据不能推给多个从节点也就主节点的数据不能进行提交。
集群恢复后,原来的主节点发现自己不是选票最多的结点就会变成从节点,并回滚日志,最后主节点再把数据推给从节点从而保证数据一致性。
scheduler
scheduler的职责很明确,就是负责调度pod到合适的Node上。如果把scheduler看成一个黑匣子,那么它的输入是pod和由多个Node组成的列表,输出是Pod和一个Node的绑定,即将这个pod部署到这个Node上。Kubernetes目前提供了调度算法,但是同样也保留了接口,用户可以根据自己的需求定义自己的调度算法。
特殊的 Controller,工作原理与其他控制器无差别;
Scheduler 的特殊职责在于监控当前集群所有未调度的Pod,并且获取当前集群所有节点的健康
状况和资源使用情况,为待调度 Pod 选择最佳计算节点,完成调度。
调度阶段分为:
• Predict:过滤不能满足业务需求的节点,如资源不足,端口冲突等。
• Priority:按既定要素将满足调度需求的节点评分,选择最佳节点。
• Bind:将计算节点与 Pod 绑定,完成调度。
调度过程
图
作用是把pod调度到最佳的node上去。需要根据不同的策略考虑node的资源使用情况例如端口、内存、存储等。
APIServer
图
apiserver主要包括认证、授权、准入、处理者四个处理逻辑。
APIServer负责对外提供RESTful的Kubernetes API服务,它是系统管理指令的统一入口,任何对资源进行增删改查的操作都要交给APIServer处理后再提交给etcd。
kubectl(Kubernetes提供的客户端工具,该工具内部就是对Kubernetes API的调用)是直接和APIServer交互的。
• 提供集群管理的 REST API 接口,包括:
• 认证 Authentication;
• 授权 Authorization;
• 准入 Admission(Mutating & Valiating)。
• 提供其他模块之间的数据交互和通信的枢纽(其他模块通过 API Server 查询或修改数据,只有 APIServer 才直接操作 etcd)。
• APIServer 提供 etcd 数据缓存以减少集群对 etcd 的访问。
Node RPKP
runtime
runtime指的是容器运行环境,目前Kubernetes支持docker和rkt两种容器。
kube-proxy
该模块实现了Kubernetes中的服务发现和反向代理功能。
反向代理方面:kube-proxy支持TCP和UDP连接转发,默认基于Round Robin算法将客户端流量转发到与service对应的一组后端pod。
服务发现方面,kube-proxy使用etcd的watch机制,监控集群中service和endpoint对象数据的动态变化,
并且维护一个service到endpoint的映射关系,从而保证了后端pod的IP变化不会对访问者造成影响。另外kube-proxy还支持session affinity。
kube-proxy 提供一种代理的服务,可以让你从service通过service访问到pods.
运行在每个node上的代理组件,提供了tcp和udp的连接转发支持。
图
工作原理使用iptables.
监控集群中用户发布的服务,并完成负载均衡配置。
每个节点的Kube-Proxy都会配置相同的负载均衡策略,使得整个集群的服务发现建立在分布式负载均衡器之上,服务调用无需经过额外的网络跳转(Network Hop)。
• 负载均衡配置基于不同插件实现:
• userspace。
• 操作系统网络协议栈不同的 Hooks 点和插件:
• iptables;
• ipvs。
kubelet
Kubelet是Master在每个Node节点上面的agent,是Node节点上面最重要的模块,它负责维护和管理该Node上面的所有容器,但是如果容器不是通过Kubernetes创建的,它并不会管理。本质上,它负责使Pod得运行状态与期望的状态一致。
在每个node中都会起一个kubelet的服务进程。该进程用于master下发到本节点的任务,管理pod和pod中的容器。
每个kubelete会定时向master的apiserver汇报节点资源信息。
从不同源获取 Pod 清单,并按需求启停 Pod 的核心组件:
Pod 清单可从本地文件目录,给定的 HTTPServer 或 KubeAPIServer 等源头获取;
Kubelet 将运行时,网络和存储抽象成了 CRI,CNI,CSI。
负责汇报当前节点的资源信息和健康状态;
负责 Pod 的健康检查和状态汇报。
工作原理
图
kubelet 的工作核心,就是一个控制循环,即:SyncLoop(图中的大圆圈)。
而驱动这个控制循环运行的事件,包括四种:Pod 更新事件;Pod 生命周期变化;kubelet 本身设置的执行周期;定时的清理事件。
kubelet 启动的时候,要做的第一件事情,就是设置 Listers,也就是注册它所关心的各种事件的 Informer。这些 Informer,就是 SyncLoop 需要处理的数据的来源。
kubelet 还负责维护着很多很多其他的子控制循环(也就是图中的小圆圈)。
这些控制循环的名字,一般被称作某某 Manager,比如 Volume Manager、Image Manager、Node Status Manager 等等。不难想到,这些控制循环的责任,就是通过控制器模式,完成 kubelet 的某项具体职责。
比如 Node Status Manager,就负责响应 Node 的状态变化,然后将 Node 的状态收集起来,并通过 Heartbeat 的方式上报给 APIServer。
再比如 CPU Manager,就负责维护该 Node 的 CPU 核的信息,以便在 Pod 通过 cpuset 的方式请求 CPU 核的时候,能够正确地管理 CPU 核的使用量和可用量。
kubelet 调用下层容器运行时的执行过程,并不会直接调用 Docker 的 API,而是通过一组叫作 CRI(Container Runtime Interface,容器运行时接口)的 gRPC 接口来间接执行的。
Pod
Pod 是Kubernetes的基本操作单元,也是应用运行的载体。整个Kubernetes系统都是围绕着Pod展开的,比如如何部署运行Pod、如何保证Pod的数量、如何访问Pod等。
Pod是一个或多个机关容器的集合,提供了一种容器的组合的模型。
pod中影响调度的字段
request 资源调度依据 (limits是给k8s来限制资源使用)
schedulerName 执行调度的调度器。
nodeName 执行结果
nodeSelect、affinity、tolerations 高级调度
创建pod的整个流程
pod通过两类探针判断容器的健康状况。
一个是livenessProbe探针一类是readinessProbe探针。
livenessProbe判断容器是否健康 告诉kubelet一个容器什么时候处于不健康状态。
判断容器是否启动完成并且准备接收请求。
k8s中调度器的资源分配机制
基于pod中容器的request资源“总和”调度
resources.limit 影响pod的运行资源上限 不影响调度。
initContainer 取最大值,container取累加值 最后取最大值即:Max(Max(initContainer.requests),Sum(container.requests))
若未指定request资源时,按照-资源需求进行调度。
基于资源声明的调度,而非实际使用
不依赖监控,系统不会过于敏感
是否调度成功: pod.request < node.allocatable - node.request
资源分配的相关算法
GeneralPredicates
LeastRequestedPriority 平衡调度次数 优先调度到最少调度的node
BalancedResourceAllocation (平衡cpu和内存等消耗比例)
什么是Pod
Pod是一个服务的多个进程的聚合单位,它由一个或者多个容器组成(例如Docker容器),它们共享容器存储、网络和容器运行配置项。
Pod是kubernetes中你可以创建和部署的最小也是最简单位。一个Pod代表着集群中运行的一个进程。
Pod中封装着应用的容器(有的情况下是好几个容器),存储、独立的网络IP,管理容器如何运行的策略选项。
Pod代表着部署的一个单位:kubernetes中应用的一个实例,可能由一个或者多个容器组合在一起共享资源。
Pod中共享的环境包括Linux的namespace,cgroup和其他可能的隔绝环境,这一点跟Docker容器一致
Pod中的容器总是被同时调度,有共同的运行环境
你可以把单个Pod想象成是运行独立应用的“逻辑主机”——其中运行着一个或者多个紧密耦合的应用容器
每个Pod都是应用的一个实例。如果你想平行扩展应用的话(运行多个实例),你应该运行多个Pod
共享两种资源
网络
每个Pod都会被分配一个唯一的IP地址。Pod中的所有容器共享网络空间,包括IP地址和端口。Pod内部的容器可以使用localhost互相通信。Pod中的容器与外界通信时,必须分配共享网络资源(例如使用宿主机的端口映射)。
存储
可以Pod指定多个共享的Volume。Pod中的所有容器都可以访问共享的volume。Volume也可以用来持久化Pod中的存储资源,以防容器重启后文件丢失。
Init 容器
Init 容器总是运行到成功完成为止。
每个 Init 容器都必须在下一个 Init 容器启动之前成功完成
安全策略
基于布尔值控制:这种类型的字段默认为最严格限制的值。
基于被允许的值集合控制:这种类型的字段会与这组值进行对比,以确认值被允许。
基于策略控制:设置项通过一种策略提供的机制来生成该值,这种机制能够确保指定的值落在被允许的这组值中。
图
生命周期
生命周期
挂起(Pending):
Pod 已被 Kubernetes 系统接受,但有一个或者多个容器镜像尚未创建。等待时间包括调度 Pod 的时间和通过网络下载镜像的时间,这可能需要花点时间。
运行中(Running):
该 Pod 已经绑定到了一个节点上,Pod 中所有的容器都已被创建。至少有一个容器正在运行,或者正处于启动或重启状态。
成功(Successed):
Pod 中的所有容器都被成功终止,并且不会再重启。
失败(Failed):
Pod 中的所有容器都已终止了,并且至少有一个容器是因为失败终止。也就是说,容器以非0状态退出或者被系统终止。
未知(Unkonwn):
因为某些原因无法取得 Pod 的状态,通常是因为与 Pod 所在主机通信失败。
容器探针
探针 是由 kubelet 对容器执行的定期诊断
成功:容器通过了诊断。
失败:容器未通过诊断。
未知:诊断失败,因此不会采取任何行动。
创建Pod流程
图
① kubectl 发送部署请求到 API Server。
② API Server 通知 Controller Manager 创建一个 deployment 资源。
③ Scheduler 执行调度任务,将两个副本 Pod 分发到 k8s-node1 和 k8s-node2。
④ k8s-node1 和 k8s-node2 上的 kubelet 在各自的节点上创建并运行 Pod。
网络分层
图
内部服务对外暴露
NodePort是K8s内部服务对外暴露的基础,LoadBalancer底层有赖于NodePort。NodePort背后是Kube-Proxy,Kube-Proxy是沟通Service网络、Pod网络和节点网络的桥梁。
将K8s服务通过NodePort对外暴露是以集群方式暴露的,每个节点上都会暴露相应的NodePort,通过LoadBalancer可以实现负载均衡访问。公有云(如阿里云/AWS/GCP)提供的K8s,都支持自动部署LB,且提供公网可访问IP,LB背后对接NodePort。
Ingress扮演的角色是对K8s内部服务进行集中反向代理,通过Ingress,我们可以同时对外暴露K8s内部的多个服务,但是只需要购买1个(或者少量)LB。Ingress本质也是一种K8s的特殊Service,它也通过HostPort(80/443)对外暴露。
通过Kubectl Proxy或者Port Forward,可以在本地环境快速调试访问K8s中的服务或Pod。
K8s的Service发布主要有3种type
type=ClusterIP,表示仅内部可访问服务,
type=NodePort,表示通过NodePort对外暴露服务,
type=LoadBalancer,表示通过LoadBalancer对外暴露服务(底层对接NodePort,一般公有云才支持)。
Pod可以完成四种通信
本地通信
本地通信是Pod内部不同容器之间的通信。因为Pod内网容器共享同一个网络协议栈,所以它们之间的通信可以通过Loopback设备完成。
同节点Pod通信
同节点Pod之间的通信,是Cni0虚拟网桥内部的通信,相当于一个二层局域网内部设备通信。
跨节点Pod通信
发送端数据包通过Cni0网桥的网关流转到节点上,
然后经过节点eth0发送给VPC路由。这不会经过任何封包操作。
当VPC路由收到数据包时,它通过查询路由表,确认数据包目的地,并把数据包发送给对应的ECS节点。
而进入节点之后,因为Flanneld在节点上创建了Cni0的路由,所以数据包会被发送到目的地的Cni0局域网,再到目的地Pod。
Pod和Pod之外网络实体的通信。
Pod与非Pod网络的实体通信,需要经过节点上的iptables规则做源地址转换,
而此规则就是Flanneld依据命令行--ip-masq选项做的配置。
网络
内部存在三类IP
Node IP:宿主机的IP地址
Pod IP:使用网络插件创建的IP(如flannel),使跨主机的Pod可以互通
Cluster IP:虚拟IP,通过iptables规则访问服务
Flannel
Flannel是作为一个二进制文件的方式部署在每个node上,主要实现两个功能
为每个node分配subnet,容器将自动从该子网中获取IP地址
当有node加入到网络中时,为每个node增加路由配置
图
calico
概念
Calico 创建和管理一个扁平的三层网络(不需要overlay),每个容器会分配一个可路由的ip。由于通信时不需要解包和封包,网络性能损耗小,易于排查,且易于水平扩展。
Calico架构
图
Etcd:负责存储网络信息
BGP client:负责将Felix配置的路由信息分发到其他节点
Felix:Calico Agent,每个节点都需要运行,主要负责配置路由、配置ACLs、报告状态
BGP Route Reflector:大规模部署时需要用到,作为BGP client的中心连接点,可以避免每个节点互联
对象
对象的通用设计:
TypeMeta
G(roup)
K(ind)
V(ersion)
• Metadata
Namespace
Name
Labels & Annotation
Finalizers
ResourceVersion
SelftLink
核心对象
对象分组
图
对象关系
关系图
对象介绍
node
Node 是 Pod 真正运行的主机,可以物理机,也可以是虚拟机。
Node 对象用来描述节点的计算资源:
Capacity:计算能力,代表当前节点的所有资源;
Allocatable:可分配资源,代表当前节点的可分配资源,通常是所有资源-预留资源-已分配资源;
Kubelet 会按固定频率检查节点健康状态并上报给 APIServer,该状态会记录在 Node 对象的status 中。
pod
Pod 是一组紧密关联的容器集合,是 Kubernetes 调度的基本单位,容器进程运行在不同 PID、IPC、Network 和 UTS namespace,且彼此之间共享网络。
Pod 的设计理念是支持多个容器在一个 Pod 中共享网络和文件系统,可以通过进程间通信和文件共享这种简单高效的方式组合完成服务。
描述
replicaSet
Pod 描述的是具体的应用实例,当 Pod 被删除后,就彻底消失了;
为保证应用的高可用,引入副本集来确保应用的总副本数永远与期望一致;
若某个 Pod 隶属于某个副本集,若该 Pod 被删除,则 ReplicaSet Controller 会发现当前运行的副本数量与用户的期望不一致,则会创建新的 Pod。
Namespace
Namespace 是对一组资源和对象的抽象集合,比如可以用来将系统内部的对象划分为不同的项目组或用户组。
从 Namespace 划分的维度看,Kubernetes 对象分为两类:
Non-namespaced object:
全局对象,这些对象不与任何 Namespace 绑定,属于集群范围的对象。如 Node,PersistVolume,ClusterRole。
• Namespaced object:
• 与具体 Namespace 绑定对象,如 Pod,Service。
Service
Service 是应用服务的抽象,通过 labels 为应用提供负载均衡和服务发现。匹配 labels的 Pod IP和端口列表组成 endpoints,由 Kube-proxy 负责将服务 IP 负载均衡到这些 endpoints 上。
每个默认类型 Service 都会自动分配一个 cluster IP(仅在集群内部可访问的虚拟地址)和 DNS名,其他容器可以通过该地址或 DNS 来访问服务,而不需要了解后端容器的运行。
RC、RS 和 Deployment 只是保证了支撑服务的微服务 Pod 的数量,但是没有解决如何访问这些服务的问题。一个 Pod 只是一个运行服务的实例,随时可能在一个节点上停止,在另一个节点以一个新的 IP 启动一个新的Pod,因此不能以确定的 IP 和端口号提供服务。
要稳定地提供服务需要服务发现和负载均衡能力。服务发现完成的工作,是针对客户端访问的服务,找到对应的的后端服务实例。在 Kubernetes 集群中,客户端需要访问的服务就是 Service 对象。每个 Service 会对应一个集群内部有效的虚拟 IP,集群内部通过虚拟 IP 访问一个服务。
在 Kubernetes 集群中微服务的负载均衡是由 Kube-proxy 实现的。Kube-proxy 是 Kubernetes 集群内部的负载均衡器。它是一个分布式代理服务器,在 Kubernetes 的每个节点上都有一个;这一设计体现了它的伸缩性优势,需要访问服务的节点越多,提供负载均衡能力的 Kube-proxy 就越多,高可用节点也随之增多。与之相比,我们平时在服务器端使用反向代理作负载均衡,还要进一步解决反向代理的高可用问题。
Deployment
部署表示用户对 Kubernetes 集群的一次更新操作。
部署是一个比 RS 应用模式更广的 API 对象,可以是创建一个新的应用,更新一个已存在的应用,也可以是滚动升级一个应用。
滚动升级一个服务,实际是创建一个新的 RS,然后逐渐将新 RS 中副本数增加到理想状态,将旧 RS 中的副本数减小到 0 的复合操作;这样一个复合操作用一个 RS 是不太好描述的,所以用一个更通用的 Deployment 来描述。以 Kubernetes 的发展方向,未来对所有长期伺服型的的业务的管理,都会通过 Deployment 来管理。
💡WHY
使用Kubernetes的好处
简化应用程序部署
更好地利用硬件
健康检查和自修复
自动扩容
简化应用部署
特性概括
(1)自动装箱
建构于容器之上,基于资源依赖及其他约束自动完成容器部署且不影响其可用性,并通过调度机制混合关键型应用和非关键型应用的工作负载于同一节点以提升资源利用率。
(2)自我修复(自愈)
支持容器故障后自动重启、节点故障后重新调度容器,以及其他可用节点、健康状态检查失败后关闭容器并重新创建等自我修复机制。
(3)水平扩展
支持通过简单命令或UI手动水平扩展,以及基于CPU等资源负载率的自动水平扩展机制。
(4)服务发现和负载均衡
Kubernetes通过其附加组件之一的KubeDNS(或CoreDNS)为系统内置了服务发现功能,它会为每个Service配置DNS名称,并允许集群内的客户端直接使用此名称发出访问请求,而Service则通过iptables或ipvs内建了负载均衡机制。
(5)自动发布和回滚
Kubernetes支持“灰度”更新应用程序或其配置信息,它会监控更新过程中应用程序的健康状态,以确保它不会在同一时刻杀掉所有实例,而此过程中一旦有故障发生,就会立即自动执行回滚操作。
(6)密钥和配置管理
Kubernetes的ConfigMap实现了配置数据与Docker镜像解耦,需要时,仅对配置做出变更而无须重新构建Docker镜像,这为应用开发部署带来了很大的灵活性。此外,对于应用所依赖的一些敏感数据,如用户名和密码、令牌、密钥等信息,Kubernetes专门提供了Secret对象为其解耦,既便利了应用的快速开发和交付,又提供了一定程度上的安全保障。
(7)存储编排
Kubernetes支持Pod对象按需自动挂载不同类型的存储系统,这包括节点本地存储、公有云服务商的云存储
(8)批量处理执行
除了服务型应用,Kubernetes还支持批处理作业及CI(持续集成),如果需要,一样可以实现容器故障后恢复。
🔑HOW
架构🦉
整体架构🍑
🍱🍱流程样个列
整体架构
由 Master 和 Node 两种节点组成,而这两种角色分别对应着控制节点和计算节点。
控制节点,即 Master 节点,由三个紧密协作的独立组件组合而成
负责 API 服务的 kube-apiserver
负责调度的 kube-scheduler,
负责容器编排的 kube-controller-manager。
整个集群的持久化数据,则由 kube-apiserver 处理后保存在 Etcd 中。
运算节点(node)
etcd服务
kube-apiserver服务
kube-controller-manager服务
kube-scheduler服务
kube-kubelet服务
kube-proxy服务
功能
全景图
我们从容器这个最基础的概念出发,首先遇到了容器间“紧密协作”关系的难题,于是就扩展到了 Pod;
有了 Pod 之后,我们希望能一次启动多个应用的实例,这样就需要 Deployment 这个 Pod 的多实例管理器
而有了这样一组相同的 Pod 后,我们又需要通过一个固定的 IP 地址和端口以负载均衡的方式访问它,于是就有了 Service。
Kubernetes 定义了新的、基于 Pod 改进后的对象。比如 Job,用来描述一次性运行的 Pod(比如,大数据任务);再比如 DaemonSet,用来描述每个宿主机上必须且只能运行一个副本的守护进程服务;又比如 CronJob,则用于描述定时任务等等。
tu
pod
Pod,是 Kubernetes 项目的原子调度单位。
通过设置 restartPolicy,改变 Pod 的恢复策略。
Always:在任何情况下,只要容器不在运行状态,就自动重启容器;
OnFailure: 只在容器 异常时才自动重启容器;
Never: 从来不重启容器。
设计原理:
只要 Pod 的 restartPolicy 指定的策略允许重启异常的容器(比如:Always),那么这个 Pod 就会保持 Running 状态,并进行容器重启。否则,Pod 就会进入 Failed 状态 。
对于包含多个容器的 Pod,只有它里面所有的容器都进入异常状态后,Pod 才会进入 Failed 状态。在此之前,Pod 都是 Running 状态。此时,Pod 的 READY 字段会显示正常容器的个数
service
Service 服务的主要作用,就是作为 Pod 的代理入口(Portal),从而代替 Pod 对外暴露一个固定的网络地址
Service是K8s提供的反向代理机制
Service的三种类型
ClusterIP ~ 内部服务访问
NodePort ~ 对外暴露服务
LoadBalancer ~ 对外暴露服务(公有云)
NodePort是Service的一种类型,可将Service暴露给外网, Selector是K8s中的路由选择定位机制
Label是K8s中的一种打标签机制
服务发現
在K8s平台的每个Worker节点上,都部署有两个组件,一个叫Kubelet,另外一个叫Kube-Proxy,这两个组件+Master是K8s实现服务注册和发现的关键。
服务注册发现流程。
首先,在服务Pod实例发布时(可以对应K8s发布中的Kind: Deployment),Kubelet会负责启动Pod实例,启动完成后,Kubelet会把服务的PodIP列表汇报注册到Master节点。
其次,通过服务Service的发布(对应K8s发布中的Kind: Service),K8s会为服务分配ClusterIP,相关信息也记录在Master上。
第三,在服务发现阶段,Kube-Proxy会监听Master并发现服务ClusterIP和PodIP列表映射关系,并且修改本地的linux iptables转发规则,指示iptables在接收到目标为某个ClusterIP请求时,进行负载均衡并转发到对应的PodIP上。
运行时,当有消费者Pod需要访问某个目标服务实例的时候,它通过ClusterIP发起调用,这个ClusterIP会被本地iptables机制截获,然后通过负载均衡,转发到目标服务Pod实例上。
流程
deployment
Deployment是对RepcliaSet+滚动发布流程的一种包装。
工作流程
1.Deployment 控制器从 Etcd 中获取到所有携带了“app: nginx”标签的 Pod,然后统计它们的数量,这就是实际状态;
2.Deployment 对象的 Replicas 字段的值就是期望状态;
3.Deployment 控制器将两个状态做比较,然后根据比较结果,确定是创建 Pod,还是删除已有的 Pod
Deployment 控制器,由上半部分的控制器定义(包括期望状态),加上下半部分的被控制对象的模板组成的。
Deployment 实现了 Pod 的“水平扩展 / 收缩”(horizontal scaling out/in)
Deployment 实际上是一个两层控制器。
首先,它通过 ReplicaSet 的个数来描述应用的版本;
然后,它再通过 ReplicaSet 的属性(比如 replicas 的值),来保证 Pod 的副本数量。
滚动发布
滚动发布是一种高级发布机制,支持按批次滚动发布,用户体验不中断,适用于版本兼容发布。蓝绿发布则适用于版本不兼容发布。
K8s中的Deployment是对RepcliaSet+滚动发布流程的一种包装。
发布机制类似Replicaset
kind: Deployment
selector -> matchLabels
replicaSet
Deployment 控制器实际操纵的是ReplicaSet 对象,而不是 Pod 对象。
K8s中的ReplicaSet副本集可以实现应用集群
发布规范·
kind: ReplicaSet
apiVersion: extensions/v1beta1
ReplicaSet具有自愈(self-healing)能力
样例:
statefulset
StatefulSet 的核心功能,就是通过某种方式记录这些状态,然后在 Pod 被重新创建时,能够为新 Pod 恢复这些状态。
应用场景
拓扑状态。应用的多个实例之间不是完全对等的关系。这些应用实例,必须按照某些顺序启动,它们再次被创建出来时也必须严格按照这个顺序才行。并且,新创建出来的 Pod,必须和原来 Pod 的网络标识一样,这样原先的访问者才能使用同样的方法,访问到这个新 Pod。
存储状态。应用的多个实例分别绑定了不同的存储数据。对于这些应用实例来说,Pod A 第一次读取到的数据,和隔了十分钟之后再次读取到的数据,应该是同一份,哪怕在此期间 Pod A 被重新创建过。这种情况最典型的例子,就是一个数据库应用的多个存储实例。
StatefulSet 这个控制器的主要作用
使用 Pod 模板创建 Pod 的时候,对它们进行编号,并且按照编号顺序逐一完成创建工作。
而当 StatefulSet 的“控制循环”发现 Pod 的“实际状态”与“期望状态”不一致,需要新建或者删除 Pod 进行“调谐”的时候,它会严格按照这些 Pod 编号的顺序,逐一完成这些操作。
kubectl
Basic Commands (Beginner):
create Create a resource from a file or from stdin.
expose 使用 replication controller, service, deployment 或者 pod 并暴露它作为一个 新的 Kubernetes
Service
run 在集群中运行一个指定的镜像
set 为 objects 设置一个指定的特征
Basic Commands (Intermediate):
explain 查看资源的文档
get 显示一个或更多 resources
edit 在服务器上编辑一个资源
delete Delete resources by filenames, stdin, resources and names, or by resources and label selector
Deploy Commands:
rollout Manage the rollout of a resource
scale Set a new size for a Deployment, ReplicaSet or Replication Controller
autoscale 自动调整一个 Deployment, ReplicaSet, 或者 ReplicationController 的副本数量
Cluster Management Commands:
certificate 修改 certificate 资源.
cluster-info 显示集群信息
top Display Resource (CPU/Memory/Storage) usage.
cordon 标记 node 为 unschedulable
uncordon 标记 node 为 schedulable
drain Drain node in preparation for maintenance
taint 更新一个或者多个 node 上的 taints
Troubleshooting and Debugging Commands:
describe 显示一个指定 resource 或者 group 的 resources 详情
logs 输出容器在 pod 中的日志
attach Attach 到一个运行中的 container
exec 在一个 container 中执行一个命令
port-forward Forward one or more local ports to a pod
proxy 运行一个 proxy 到 Kubernetes API server
cp 复制 files 和 directories 到 containers 和从容器中复制 files 和 directories.
auth Inspect authorization
Advanced Commands:
diff Diff live version against would-be applied version
apply 通过文件名或标准输入流(stdin)对资源进行配置
patch 使用 strategic merge patch 更新一个资源的 field(s)
replace 通过 filename 或者 stdin替换一个资源
wait Experimental: Wait for a specific condition on one or many resources.
convert 在不同的 API versions 转换配置文件
kustomize Build a kustomization target from a directory or a remote url.
Settings Commands:
label 更新在这个资源上的 labels
annotate 更新一个资源的注解
completion Output shell completion code for the specified shell (bash or zsh)
Other Commands:
alpha Commands for features in alpha
api-resources Print the supported API resources on the server
api-versions Print the supported API versions on the server, in the form of "group/version"
config 修改 kubeconfig 文件
plugin Provides utilities for interacting with plugins.
version 输出 client 和 server 的版本信息
其他资料
k8s概念入门
网络
flannel
what
“flannel 是一种配置第三层网络结构的简单易用方式,该层网络专门针对 Kubernetes 设计。
flannel 在每个主机上以称为 flanneld 的单个微小二进制程序运行代理,并负责从更大的预配置地址空间中对每个主机分配租用子网。Flannel 直接使用 Kubernetes API 或 etcd 存储网络配置。配置包括所分配的子网及所有的辅助数据,例如主机的公共 IP 等。flannel 支持使用多种后端机制转发数据包,例如 VXLAN、各种云集成机制等。
非云环境中Kubernetes的配置和运行:flannel
原文链接: https://www.infoq.cn/article/ntg12fh31hq2vkxkt48i
why
1.使集群中的不同Node主机创建的Docker容器都具有全集群唯一的虚拟IP地址。
2.建立一个覆盖网络(overlay network),通过这个覆盖网络,将数据包原封不动的传递到目标容器。
覆盖网络是建立在另一个网络之上并由其基础设施支持的虚拟网络。
覆盖网络通过将一个分组封装在另一个分组内来将网络服务与底层基础设施分离。
在将封装的数据包转发到端点后,将其解封装。
3.创建一个新的虚拟网卡flannel0接收docker网桥的数据,通过维护路由表,对接收到的数据进行封包和转发(vxlan)。
4.etcd保证了所有node上flanned所看到的配置是一致的。同时每个node上的flanned监听etcd上的数据变化,实时感知集群中node的变化。
how
tu
1
各组件功能
1.Cni0:网桥设备,每创建一个pod都会创建一对 veth pair。其中一端是pod中的eth0,另一端是Cni0网桥中的端口(网卡)。Pod中从网卡eth0发出的流量都会发送到Cni0网桥设备的端口(网卡)上。Cni0 设备获得的ip地址是该节点分配到的网段的第一个地址。
2.Flannel.1: overlay网络的设备,用来进行 vxlan 报文的处理(封包和解包)。不同node之间的pod数据流量都从overlay设备以隧道的形式发送到对端。
3.Flanneld:flannel在每个主机中运行flanneld作为agent,它会为所在主机从集群的网络地址空间中,获取一个小的网段subnet,本主机内所有容器的IP地址都将从中分配。同时Flanneld监听K8s集群数据库,为flannel.1设备提供封装数据时必要的mac,ip等网络数据信息。
跨主机通信
how
tu
1
网络包通过网络接口 eth1 离开 Pod 1,通过虚拟网络接口 veth1 到达 root netns。
网络包离开 veth1,到达 cni0,查找 Pod 6 的地址。
网络包离开 cni0,重定向到 eth0。
网络包从 Master 1 离开 eth0,到达网关。
网络包离开网关,通过工作节点 1 的网络接口 eth0 到达 root netns。
网络包离开 eth0,到达 cni0,并查找 Pod 6 的地址。
网络包离开 cni0,重定向到虚拟网络接口 veth6。
网络包通过虚拟地址接口 veth6 离开 root netns。
原文链接: https://www.infoq.cn/article/ntg12fh31hq2vkxkt48i
核心组件
核心附件
CNI网络插件(flannel/calico)
服务发现插件(coredns)
服务暴露插件(traefik)
GUI管理插件(dashboard)
开放接口
CRI(Container Runtime Interface):容器运行时接口,提供计算资源
RuntimeService
ImageService
CNI(Container Network Interface):容器网络接口,提供网络资源
添加网络、删除网络、添加网络列表、删除网络列表
CNI设计的时候考虑了以下问题
容器运行时必须在调用任何插件之前为容器创建一个新的网络命名空间。
然后,运行时必须确定这个容器应属于哪个网络,并为每个网络确定哪些插件必须被执行。
网络配置采用JSON格式,可以很容易地存储在文件中。网络配置包括必填字段,如name和type以及插件(类型)。网络配置允许字段在调用之间改变值。为此,有一个可选的字段args,必须包含不同的信息。
容器运行时必须按顺序为每个网络执行相应的插件,将容器添加到每个网络中。
在完成容器生命周期后,运行时必须以相反的顺序执行插件(相对于执行添加容器的顺序)以将容器与网络断开连接。
容器运行时不能为同一容器调用并行操作,但可以为不同的容器调用并行操作。
容器运行时必须为容器订阅ADD和DEL操作,这样ADD后面总是跟着相应的DEL。 DEL可能跟着额外的DEL,但是,插件应该允许处理多个DEL(即插件DEL应该是幂等的)。
容器必须由ContainerID唯一标识。存储状态的插件应该使用(网络名称,容器ID)的主键来完成。
运行时不能调用同一个网络名称或容器ID执行两次ADD(没有相应的DEL)。换句话说,给定的容器ID必须只能添加到特定的网络一次。
CSI(Container Storage Interface):容器存储接口,提供存储资源
核心组件功能
配置存储中心-etcd
etcd是一个非关系型数据库,作用类似于zookeeper注册中心
用于各种服务的注册和数据缓存
kube-apiserver(master)
提供季军管理的REST API接口,包括鉴权、数据校验、集群状态变更
负责其他模块之间的数据交互,承担通信枢纽的功能
和etcd通信,是资源配额控制的入口
提供玩备的集群控制机制
kube-controller-manager
由一系列控制器组成,通过apiserver监控整个集群的状态,确保集群处于预期的工作状态
是管理所有控制器的控制器
kube-scheduler
主要是接收调度POD到合适的node节点上
通过apiserver,从etcd中获取资源信息进行调度
只负责调度工作,启动工作是node节点上的kubelet负责
调度策略:预算策略(predict)、优选策略(priorities)
kube-kubelet
定时从apiserver获取节点上POD的期望状态(如副本数量、网络类型、存储空间、容器类型等)然后调用容器平台接口达到这个状态
提供POD节点具体使用的网络
定时汇报当前节点状态给apiserver,以供调度
复制镜像和容器的创建和清理工作
kube-proxy
是K8S在每个节点上运行网络的代理,service资源的载体
不直接为POD节点提供网络,而是提供POD间的集群网络
建立了POD网络和集群网络的关系(clusterIp->podIp)
负责建立、删除、更新调度规则
与apiserver通信,以更新自己和获取其他kube-proxy的的调度规则
常用的调度模式:Iptables(不推荐)、Ipvs(推荐)
🎛️容器模式
Sidecar
i∩it
🔔运维相关
5.1.1 Node的管理
1.Node的隔离与恢复
❗创建配置文件unschedule_node.yaml,在spec部分指定unschedulable为true:然后,通过kubectl replace命令完成对Node状态的修改:
❗也可以不使用配置文件,直接使用kubectl patch命令完成:
❗需要注意 将某个Node脱离调度范围时,在其上运行的Pod并不会自动停止,管理员需要手动停止在该Node上运行的Pod。
⭕在Kubernetes当前的版本中,kubectl的子命令cordon和uncordon也用于实现将Node进行隔离和恢复调度的操作。
2.Node的扩容
👉在Kubernetes集群中,一个新Node的加入是非常简单的。在新的Node节点上安装Docker、kubelet和kube-proxy服务,
👉然后配置kubelet和kube-proxy的启动参数,将Master URL指定为当前Kubernetes集群Master的地址,最后启动这些服务。
👉通过kubelet默认的自动注册机制,新的Node将会自动加入现有的Kubernetes集群中
5.1.2 更新资源对象的Label
Label(标签)作为用户可灵活定义的对象属性,在正在运行的资源对象上,仍然可以随时通过kubectl label命令对其进行增加、修改、删除等操作。
删除一个Label时,只需在命令行最后指定Label的key名并与一个减号相连即可:
修改一个Label的值时,需要加上--overwrite参数:
5.1.3 Namespace:集群环境共享与隔离
5.1.4 Kubernetes资源管理
1.计算资源管理(Compute Resources)
计算资源的配置项分为两种:
一种是资源请求(Resource Requests,简称Requests),表示容器希望被分配到的、可完全保证的资源量,Requests 的值会提供给 Kubernetes 调度器(Kubernetes Scheduler)以便于优化基于资源请求的容器调度;
另外一种是资源限制(Resource Limits,简称 Limits),Limits 是容器最多能使用到的资源量的上限,这个上限值会影响节点上发生资源竞争时的解决策略。
Pod中的每个容器都可以配置以下4个参数。
spec.container[].resources.requests.cpu.
spec.container[].resources.limits.cpu.
spec.container[].resources.requests.memory.
spec.container[].resources.limits.memory.
◎ Requests和Limits都是可选的。在某些集群中如果在Pod创建或者更新时,没设置资源限制或者资源请求值,那么可能会使用系统提供一个默认值,这个默认值取决于集群的配置。
◎ 如果Request没有配置,那么默认会被设置为等于Limits。
◎ 而任何情况下Limits都应该设置为大于或者等于Requests。
基于Requests和Limits的Pod调度机制
当一个Pod创建成功时,Kubernetes调度器(Scheduler)为该Pod选择一个节点(Node)来执行。对于每种计算资源(CPU和内存)而言,每个节点都有一个能用于运行Pod的最大容量值。
调度器在调度时,首先要确保调度后该节点上所有Pod的CPU和内存的Requests总和不能超过该节点能提供给Pod使用的CPU和内存的最大容量值。
Kubernetes提供了LimitRange机制对Pod和容器的Requests和Limits配置进一步做出限制。
1)创建一个namespace
2)为namespace设置LimitRange
4.资源的配额管理(Resource Quotas)
5.ResourceQuota和LimitRange实践指南
6.资源管理总结
Kubernetes中的资源管理的基础是容器和Pod的资源配置(Requests和Limits)。容器的资源配置(Requests和Limits)指定了容器请求的资源和容器能使用的资源上限,而Pod的资源配置则是Pod中所有容器的资源配置总和的上限。
通过资源配额(Resource Quota)机制,我们可以对命名空间下所有Pod使用资源的总量进行限制,
如果我们需要对用户的Pod或容器的资源配置做更多的限制,则我们可以使用资源配置范围(LimitRange)来达到这个目的。LimitRange可以有效地限制Pod和容器的资源配置的最大、最小范围,也可以限制Pod和容器的Limits与Requests的最大比例上限,此外LimitRange还可以为Pod中的容器提供默认的资源配置。
Kubernetes基于Pod的资源配置(Requests和Limits)实现了资源服务质量(QoS)。不同QoS级别的Pod在系统中拥有不同的优先级:高优先级的Pod具有更高的可靠性,可以用于运行可靠性要求较高的服务;而低优先级的Pod可以实现集群资源的超售,能有效地提高集群资源利用率。
5.1.5 资源紧缺时的Pod驱逐机制
Kubernetes集群中常见问题的排查方法
首先,查看Kubernetes对象的当前运行时信息,特别是与对象关联的Event事件。
这些事件记录了相关主题、发生时间、最近发生时间、发生次数及事件原因等,对排查故障非常有价值。此外,通过查看对象的运行时数据,我们还可以发现参数错误、关联错误、状态异常等明显问题。
由于Kubernetes中多种对象相互关联,因此,这一步可能会涉及多个相关对象的排查问题。
其次,对于服务、容器的问题,则可能需要深入容器内部进行故障诊断,此时可以通过查看容器的运行日志来定位具体问题。
最后,对于某些复杂问题,比如Pod调度这种全局性的问题,可能需要结合集群中每个节点上的 Kubernetes 服务日志来排查。
比如搜集 Master 上 kube-apiserver、kube-schedule、kube-controler-manager服务的日志,以及各个Node节点上的kubelet、kube-proxy服务的日志,综合判断各种信
常见问题
1.由于无法下载pause镜像导致Pod一直处于Pending的状态
(1)如果服务器可以访问Internet,并且不希望使用HTTPS的安全机制来访问gcr.io,则可以在Docker Daemon的启动参数中加上--insecure-registry gcr.io来表示可以进行匿名下载。 (2)如果Kubernetes的集群环境在内网环境中,无法访问gcr.io网站,则可以先通过一台能够访问gcr.io的机器将pause镜像下载下来,导出后,再导入内网的Docker私有镜像库中,并在kubelet的启动参数中加上--pod_infra_container_image,配置为:
2.Pod创建成功,但状态始终不是Ready,且RESTARTS的数量持续增加
istio
定义:
流量管理(Pilot)
控制服务之间的流量和API调用的流向,使得调用更灵活可靠,并使网络在恶劣情况下更加健壮。
图
策略执行(mixer)
图
图
将组织策略应用于服务之间的互动,确保访问策略得以执行,资源在消费者之间良好分配。策略的更改是通过配置网格而不是修改应用程序代码。
可观察性:
过集成zipkin等服务,快速了解服务之间的依赖关系,以及它们之间流量的本质和流向,从而提供快速识别问题的能力。
服务身份和安全(Istio-auth)
图
为网格中的服务提供可验证身份,并提供保护服务流量的能力,使其可以在不同可信度的网络上流转。
架构
架构图
1
2
Istio 服务网格逻辑上分为数据平面和控制平面。
数据平面由一组以 sidecar 方式部署的智能代理(Envoy)组成。这些代理可以调节和控制微服务及 Mixer 之间所有的网络通信。
控制平面负责管理和配置代理来路由流量。此外控制平面配置 Mixer 以实施策略和收集遥测数据。
规则配置
Istio 提供了一个简单的配置模型,用来控制 API 调用以及应用部署内多个服务之间的四层通信。
运维人员可以使用这个模型来配置服务级别的属性,这些属性可以是断路器、超时、重试,以及一些普通的持续发布任务,例如金丝雀发布、A/B 测试、使用百分比对流量进行控制,从而完成应用的逐步发布等。
Istio 中包含有四种流量管理配置资源
VirtualService 在 Istio 服务网格中定义路由规则,控制路由如何路由到服务上。
DestinationRule 是 VirtualService 路由生效后,配置应用与请求的策略集
ServiceEntry 是通常用于在 Istio 服务网格之外启用对服务的请求。
Gateway 为 HTTP/TCP 流量配置负载均衡器,最常见的是在网格的边缘的操作,以启用应用程序的入口流量。
控制面
[文章]
安装
[[教程]](https://www.41sh.cn/?id=59)
https://blog.csdn.net/weixin_39714191/article/details/111861952
https://blog.csdn.net/chenxun_2010/article/details/106764240
中间件
Netty【净水器】
核心组件
架构图
图
组件
ServerBootstrap
服务器端程序的入口,这是 Netty 为简化网络程序配置和关闭等生命周期管理,所引入的 Bootstrapping 机制
我们通常要做的创建 Channel、绑定端口、注册 Handler 等,都可以通过这个统一的入口,
以 Fluent API 等形式完成,相对简化了 API 使用。与之相对应, Bootstrap则是 Client 端的通常入口。
Channel
作为一个基于 NIO 的扩展框架,Channel 和 Selector 等概念仍然是 Netty 的基础组件,但是针对应用开发具体需求,提供了相对易用的抽象。
EventLoop
这是 Netty 处理事件的核心机制。例子中使用了 EventLoopGroup。
我们在 NIO 中通常要做的几件事情,如注册感兴趣的事件、调度相应的 Handler 等,都是 EventLoop 负责。
ChannelFuture
这是 Netty 实现异步 IO 的基础之一,保证了同一个 Channel 操作的调用顺序。Netty 扩展了 Java 标准的 Future,提供了针对自己场景的特有Future定义。
ChannelHandler
这是应用开发者放置业务逻辑的主要地方,也是我上面提到的“Separation Of Concerns”原则的体现。
ChannelPipeline
它是 ChannelHandler 链条的容器,每个 Channel 在创建后,自动被分配一个 ChannelPipeline。
在上面的示例中,我们通过 ServerBootstrap 注册了 ChannelInitializer,并且实现了 initChannel 方法,而在该方法中则承担了向 ChannelPipleline 安装其他 Handler 的任务。
优势
优势概述
【NIO】一个高性能、异步事件驱动的NIO框架,它提供了对TCP、UDP和文件传输的支持
【避免空轮询】使用更高效的socket底层,对epoll空轮询引起的cpu占用飙升在内部进行了处理,避免了直接使用NIO的陷阱,简化了NIO的处理方式。
【避免粘包分包】采用多种decoder/encoder 支持,对TCP粘包/分包进行自动化处理
可使用接受/处理线程池,提高连接效率,对重连、心跳检测的简单支持
【零拷贝】可配置IO线程数、TCP参数, TCP接收和发送缓冲区使用直接内存代替堆内存,通过内存池的方式循环利用ByteBuf
实现原理
netty深入理解系列-Netty零拷贝的实现原理
从传统的场景看,会注意到上图,第2次和第3次拷贝根本就是多余的。应用程序只是起到缓存数据被将传回到套接字的作用而已,别无他用。
应用程序使用zero-copy来请求kernel直接把disk的数据传输到socket中,而不是通过应用程序传输。zero-copy大大提高了应用程序的性能,并且减少了kernel和user模式的上下文切换。
数据可以直接从read buffer 读缓存区传输到套接字缓冲区,也就是省去了将操作系统的read buffer 拷贝到程序的buffer,以及从程序buffer拷贝到socket buffer的步骤,直接将read buffer拷贝到socket buffer。JDK NIO中的的transferTo() 方法就能够让您实现这个操作,这个实现依赖于操作系统底层的sendFile()实现的
【低GC】通过引用计数器及时申请释放不再引用的对象,降低了GC频率
【Reactor单线程】使用单线程串行化的方式,高效的Reactor线程模型
【Java层优化】大量使用了volitale、使用了CAS和原子类、线程安全类的使用、读写锁的使用
优势
心跳
对服务端:会定时清除闲置会话inactive(netty5),对客户端:用来检测会话是否断开,是否重来,检测网络延迟,其中idleStateHandler类 用来检测会话状态
串行无锁化设计
即消息的处理尽可能在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁。
表面上看,串行化设计似乎CPU利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。
可靠性
链路有效性检测:链路空闲检测机制,读/写空闲超时机制;
内存保护机制:通过内存池重用ByteBuf;ByteBuf的解码保护;
优雅停机:不再接收新消息、退出前的预处理操作、资源的释放操作。
安全性
支持的安全协议:SSL V2和V3,TLS,SSL单向认证、双向认证和第三方CA认证。
高效并发
volatile的大量、正确使用;
CAS和原子类的广泛使用;线程安全容器的使用;
通过读写锁提升并发性能。IO通信性能三原则:传输(AIO)、协议(Http)、线程(主从多线程)
流量整型的作用(变压器):防止由于上下游网元性能不均衡导致下游网元被压垮,业务流中断;防止由于通信模块接受消息过快,后端业务线程处理不及时导致撑死问题。
TCP参数配置:SO_RCVBUF和SO_SNDBUF:通常建议值为128K或者256K;SO_TCPNODELAY:NAGLE算法通过将缓冲区内的小封包自动相连,组成较大的封包,阻止大量小封包的发送阻塞网络,从而提高网络应用效率。但是对于时延敏感的应用场景需要关闭该优化算法;
零拷贝
https://zhuanlan.zhihu.com/p/390239094
Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。堆内存多了一次内存拷贝,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。ByteBuffer由ChannelConfig分配,而ChannelConfig创建ByteBufAllocator默认使用Direct Buffer
CompositeByteBuf 类可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。addComponents方法将 header 与 body 合并为一个逻辑上的 ByteBuf, 这两个 ByteBuf 在CompositeByteBuf 内部都是单独存在的, CompositeByteBuf 只是逻辑上是一个整体
通过 FileRegion 包装的FileChannel.tranferTo方法 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel,避免了传统通过循环write方式导致的内存拷贝问题。
通过 wrap方法, 我们可以将 byte[] 数组、ByteBuf、ByteBuffer等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操作。
Selector BUG:若Selector的轮询结果为空,也没有wakeup或新消息处理,则发生空轮询,CPU使用率100%,
Netty的解决办法:对Selector的select操作周期进行统计,每完成一次空的select操作进行一次计数,若在某个周期内连续发生N次空轮询,则触发了epoll死循环bug。重建Selector,判断是否是其他线程发起的重建请求,若不是则将原SocketChannel从旧的Selector上去除注册,重新注册到新的Selector上,并将原来的Selector关闭。
时间轮算法
粘包/拆包解决
TCP 是以流~的方式来处理数据,一个完整的包可能会被 TCP 拆分成多个包进行发送,也可能把小的封装成一个大的数据包发送。
TCP 粘包/分包的原因: 应用程序写入的字节大小大于套接字发送缓冲区的大小,会发生拆包现象,而应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包现象;
进行 MSS 大小的 TCP 分段,当 TCP 报文长度-TCP 头部长度>MSS 的时候将发生拆包 以太网帧的 payload(净荷)大于 MTU( 1500 字节)进行 ip 分片。
解决方法 消息定长: FixedLengthFrameDecoder 类包尾增加特殊字符分割:行分隔符类: LineBasedFrameDecoder 或自定义分隔符类 : DelimiterBasedFrameDecoder将消息分为消息头和消息体: LengthFieldBasedFrameDecoder 类。分为有头部的拆包与粘包、长度字段在前且有头部的拆包与粘包、多扩展头部的拆包与粘包。
高性能内存管理
线程模型
图
线程模型图
Netty通过Reactor模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,boss线程池和work线程池,其中boss线程池的线程负责处理请求的accept事件,当接收到accept事件的请求时,把对应的socket封装到一个NioSocketChannel中,并交给work线程池,其中work线程池负责请求的read和write事件,由对应的Handler处理。
单线程模型:所有I/O操作都由一个线程完成,即多路复用、事件分发和处理都是在一个Reactor线程上完成的。既要接收客户端的连接请求,向服务端发起连接,又要发送/读取请求或应答/响应消息。一个NIO 线程同时处理成百上千的链路,性能上无法支撑,速度慢,若线程进入死循环,整个程序不可用,对于高负载、大并发的应用场景不合适。
多线程模型:有一个NIO 线程(Acceptor) 只负责监听服务端,接收客户端的TCP 连接请求;NIO 线程池负责网络IO 的操作,即消息的读取、解码、编码和发送;1 个NIO 线程可以同时处理N 条链路,但是1 个链路只对应1 个NIO 线程,这是为了防止发生并发操作问题。但在并发百万客户端连接或需要安全认证时,一个Acceptor 线程可能会存在性能不足问题。
主从多线程模型:Acceptor 线程用于绑定监听端口,接收客户端连接,将SocketChannel 从主线程池的Reactor 线程的多路复用器上移除,重新注册到Sub 线程池的线程上,用于处理I/O 的读写等操作,从而保证mainReactor只负责接入认证、握手等操作;
Netty和Mina区别
Mina
Apache Mina是一个能够帮助用户开发高性能和高伸缩性网络应用程序的框架。
它通过Java nio技术基于TCP/IP和UDP/IP协议提供了抽象的、事件驱动的、异步的API。
Netty
Netty是由JBOSS提供的一个java开源框架。
Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
也就是说,Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程,例如:基于TCP和UDP的socket服务开发。
“快速”和“简单”并不用产生维护性或性能上的问题。Netty 是一个吸收了多种协议(包括FTP、SMTP、HTTP等各种二进制文本协议)的实现经验,并经过相当精心设计的项目。最终,Netty 成功的找到了一种方式,在保证易于开发的同时还保证了其应用的性能,稳定性和伸缩性。
应用场景
使用Netty,我们到底在开发些什么?
Netty 的最佳实践
1)业务线程池必要性
业务逻辑尤其是阻塞时间较长的逻辑,不要占用netty的IO线程,dispatch到业务线程池中去。
2)WriteBufferWaterMark
注意默认的高低水位线设置(32K~64K),根据场景适当调整(可以思考一下如何利用它)。
3)重写 MessageSizeEstimator 来反应真实的高低水位线
默认实现不能计算对象size,由于write时还没路过任何一个outboundHandler就已经开始计算message size,此时对象还没有被encode成Bytebuf,所以size计算肯定是不准确的(偏低)。
4)注意EventLoop#ioRatio的设置(默认50)
这是EventLoop执行IO任务和非IO任务的一个时间比例上的控制。
5)空闲链路检测用谁调度?
Netty4.x默认使用IO线程调度,使用eventLoop的delayQueue,一个二叉堆实现的优先级队列,复杂度为O(log N),每个worker处理自己的链路监测,有助于减少上下文切换,但是网络IO操作与idle会相互影响。
如果总的连接数小,比如几万以内,上面的实现并没什么问题,连接数大建议用HashedWheelTimer实现一个IdleStateHandler,HashedWheelTimer复杂度为 O(1),同时可以让网络IO操作和idle互不影响,但有上下文切换开销。
6)使用ctx.writeAndFlush还是channel.writeAndFlush?
ctx.write直接走到下一个outbound handler,注意别让它违背你的初衷绕过了空闲链路检测。
channel.write从末尾开始倒着向前挨个路过pipeline中的所有outbound handlers。
7)使用Bytebuf.forEachByte() 来代替循环 ByteBuf.readByte()的遍历操作,避免rangeCheck()
8)使用CompositeByteBuf来避免不必要的内存拷贝
缺点是索引计算时间复杂度高,请根据自己场景衡量。
9)如果要读一个int,用Bytebuf.readInt(),不要Bytebuf.readBytes(buf,0,4)
这能避免一次memory copy (long,short等同理)。
10)配置UnpooledUnsafeNoCleanerDirectByteBuf来代替jdk的DirectByteBuf,让netty框架基于引用计数来释放堆外内存
io.netty.maxDirectMemory:
== 0: 使用cleaner,netty方面不设置最大direct memory size。
0:不使用cleaner,并且这个参数将直接限制netty的最大direct memory size,(jdk的direct memory size是独立的,不受此参数限制)。
11)最佳连接数
一条连接有瓶颈,无法有效利用cpu,连接太多也白扯,最佳实践是根据自己场景测试。
12)使用PooledBytebuf时要善于利用 -Dio.netty.leakDetection.level 参数
四种级别:DISABLED(禁用),SIMPLE(简单),ADVANCED(高级),PARANOID(偏执)。
SIMPLE,ADVANCED采样率相同,不到1%(按位与操作 mask ==128 - 1)。
默认是SIMPLE级别,开销不大。
出现泄漏时日志会出现“LEAK: ”字样,请时不时grep下日志,一旦出现“LEAK: ”立刻改为ADVANCED级别再跑,可以报告泄漏对象在哪被访问的。
PARANOID:测试的时候建议使用这个级别,100%采样。
13)Channel.attr(),将自己的对象attach到channel上
拉链法实现的线程安全的hash表,也是分段锁(只锁链表头),只有hash冲突的情况下才有锁竞争(类似ConcurrentHashMapV8版本)。
默认hash表只有4个桶,使用不要太任性。
9 从 Netty 源码中学到的代码技巧
1)海量对象场景中 AtomicIntegerFieldUpdater --> AtomicInteger
Java中对象头12 bytes(开启压缩指针的情况下),又因为Java对象按照8字节对齐,所以对象最小16 bytes,AtomicInteger大小为16 bytes,AtomicLong大小为 24 bytes。
AtomicIntegerFieldUpdater作为static field去操作volatile int。
2)FastThreadLocal,相比jdk的实现更快
线性探测的Hash表 —> index原子自增的裸数组存储。
3)IntObjectHashMap / LongObjectHashMap …
Integer—> int
Node[] —> 裸数组
4)RecyclableArrayList
基于前面说的Recycler,频繁new ArrayList的场景可考虑。
5)JCTools
一些jdk没有的 SPSC/MPSC/SPMC/MPMC 无锁并发队以及NonblockingHashMap(可以对比ConcurrentHashMapV6/V8)
WEB服务器
tomcat
核心组件
连接器和容器,其中连接器负责外部交流,容器负责内部处理。连接器处理 Socket 通信和应用层协议的解析,得到 Servlet 请求;而容器则负责处理 Servlet 请求。
容器的层次结构
图
Context 表示一个 Web 应用程序;
Wrapper 表示一个 Servlet,一个 Web 应用程序中可能会有多个 Servlet;
Host 代表的是一个虚拟主机,或者说一个站点,可以给 Tomcat 配置多个虚拟主机地址,而一个虚拟主机下可以部署多个 Web 应用程序;
Engine 表示引擎,用来管理多个虚拟站点,一个 Service 最多只能有一个 Engine。
调优
JVM 参数调优:
-Xms 表示 JVM 初始化堆的大小,-Xmx表示 JVM 堆的最大值。这两个值的大小一般根据需要进行设置。当应用程序需要的内存超出堆的最大值时虚拟机就会提示内存溢出,并且导致应用服务崩溃。因此一般建议堆的最大值设置为可用内存的最大值的 80%。在 catalina.bat 中,设置 JAVA_OPTS='-Xms256m -Xmx512m',表示初始化内存为 256MB,可以使用的最大内存为 512MB。
禁用 DNS 查询
当 web 应用程序向要记录客户端的信息时,它也会记录客户端的 IP 地址或者通过域名服务器查找机器名转换为 IP 地址。DNS 查询需要占用网络,并且包括可能从很多很远的服务器或者不起作用的服务器上去获取对应的 IP 的过程,这样会消耗一定的时间。为了消除 DNS 查询对性能的影响我们可以关闭 DNS 查询,方式是修改 server.xml 文件中的 enableLookups 参数值:Tomcat4 Tomcat5
调整线程数
通过应用程序的连接器(Connector)进行性能控制的的参数是创建的处理请求的线程数。Tomcat 使用线程池加速响应速度来处理请求。在 Java 中线程是程序运行时的路径,是在一个程序中与其它控制线程无关的、能够独立运行的代码段。它们共享相同的地址空间。多线程帮助程序员写出 CPU 最大利用率的高效程序,使空闲时间保持最低,从而接受更多的请求。Tomcat4 中可以通过修改 minProcessors 和 maxProcessors 的值来控制线程数。这些值在安装后就已经设定为默认值并且是足够使用的,但是随着站点的扩容而改大这些值。minProcessors 服务器启动时创建的处理请求的线程数应该足够处理一个小量的负载。也就是说,如果一天内每秒仅发生 5 次单击事件,并且每个请求任务处理需要 1 秒钟,那么预先设置线程数为 5 就足够了。但在你的站点访问量较大时就需要设置更大的线程数,指定为参数maxProcessors 的值。maxProcessors 的值也是有上限的,应防止流量不可控制(或者恶意的服务攻击),从而导致超出了虚拟机使用内存的大小。如果要加大并发连接数,应同时加大这两个参数。web server 允许的最大连接数还受制于操作系统的内核参数设置,通常Windows 是 2000 个左右,Linux 是 1000 个左右。在 Tomcat5 对这些参数进行了调整,请看下面属性:maxThreads Tomcat 使用线程来处理接收的每个请求。这个值表示 Tomcat 可创建的最大的线程数。acceptCount 指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理。connnectionTimeout 网络连接超时,单位:毫秒。设置为 0 表示永不超时,这样设置有隐患的。通常可设置为 30000 毫秒。minSpareThreads Tomcat 初始化时创建的线程数。maxSpareThreads 一旦创建的线程超过这个值,Tomcat 就会关闭不再需要的 socket 线程。最好的方式是多设置几次并且进行测试,观察响应时间和内存使用情况。在不同的机器、操作系统或虚拟机组合的情况下可能会不同,而且并不是所有人的 web 站点的流量都是一样的,因此没有一刀切的方案来确定线程数的值。
undertow
架构图
特性
非常轻量级,Undertow核心瓶子在1Mb以下。它在运行时也是轻量级的,有一个简单的嵌入式服务器使用少于4Mb的堆空间。
支持HTTP升级,允许多个协议通过HTTP端口进行多路复用。
提供对Web套接字的全面支持,包括JSR-356支持。
提供对Servlet 3.1的支持,包括对嵌入式servlet的支持。还可以在同一部署中混合Servlet和本机Undertow非阻塞处理程序。
可以嵌入在应用程序中或独立运行,只需几行代码。
通过将处理程序链接在一起来配置Undertow服务器。它可以对各种功能进行配置,方便灵活。
在高并发系统中,Tomcat相对来说比较弱。在相同的机器配置下,模拟相等的请求数,Undertow在性能和内存使用方面都是最优的。并且Undertow新版本默认使用持久连接,这将会进一步提高它的并发吞吐能力。所以,如果是高并发的业务系统,Undertow是最佳选择。
NGINX
架构图
图
运行时架构图
存储
FastDFS
架构图
架构
图二
角色
Tracker Server:跟踪服务器
主要做调度工作,并对Storage Server起到负载均衡的作用;
负责管理所有的 storage server和 group,每个 storage 在启动后会连接 Tracker,告知自己所属 group等信息,并保持周期性心跳。
Tracker Server可以有多台,Tracker Server之间是相互平等关系同时提供服务Tracker Server不存在单点故障。
客户端请求Tracker Server采用轮询方式,如果请求的Tracker无法提供服务则换另一个Tracker。
Storage Server:存储服务器
主要提供容量和备份服务;
以 group 为单位,不同group之间互相独立,每个 group 内可以有多台 storage server,数据互为备份。
采用分组存储方式的好处是灵活、可控性较强。比如上传文件时,可以由客户端直接指定上传到的组也可以由Tracker进行调度选择。
一个分组的存储服务器访问压力较大时,可以在该组增加存储服务器来扩充服务能力(纵向扩容)。当系统容量不足时,可以增加组来扩充存储容量(横向扩容)。
存储策略:
为了支持大容量,存储节点(服务器)采用了分卷(或分组)的组织方式。存储系统由一个或多个卷组成,卷与卷
之间的文件是相互独立的,所有卷的文件容量累加就是整个存储系统中的文件容量。一个卷可以由一台或多台存储服务器组成,一个卷下的存储服务器中的文件都是相同的,卷中的多台存储服务器起到了冗余备份和负载均衡的作用。在卷中增加服务器时,同步已有的文件由系统自动完成,同步完成后,系统自动将新增服务器切换到线上提供服
当存储空间不足或即将耗尽时,可以动态添加卷。只需要增加一台或多台服务器,并将它们配置为一个新的卷,这样就扩大了存储系统的容量。
Storage状态收集:
Storage Server会通过配置连接集群中所有的Tracker Server,定时向他们报告自己的状态,包括磁盘剩余空间、文件同步状况、文件上传下载次数等统计信息。
storage server有7个状态,如下:
FDFS_STORAGE_STATUS_INIT :初始化,尚未得到同步已有数据的源服务器
FDFS_STORAGE_STATUS_WAIT_SYNC :等待同步,已得到同步已有数据的源服务器
FDFS_STORAGE_STATUS_SYNCING :同步中
FDFS_STORAGE_STATUS_DELETED :已删除,该服务器从本组中摘除(注:本状态的功能尚未实现)
FDFS_STORAGE_STATUS_OFFLINE :离线
FDFS_STORAGE_STATUS_ONLINE :在线,尚不能提供服务
FDFS_STORAGE_STATUS_ACTIVE :在线,可以提供服务
当storage server的状态为 FDFS_STORAGE_STATUS_ONLINE 时,当该storage server向tracker server发起一次heart beat时,tracker server将其状态更改为 FDFS_STORAGE_STATUS_ACTIVE
HDFS
架构图
Docker 【冰箱】
定义
namespace和cgroup
Linux Namespace 是一种 Linux Kernel 提供的资源隔离方案
系统可以为进程分配不同的 Namespace;
并保证不同的 Namespace 资源独立分配、进程彼此隔离,即不同的 Namespace 下的进程互不干扰 。
• Cgroups (Control Groups)是 Linux 下用于对一个或一组进程进行资源控制和监控的机制
可以对诸如 CPU 使用时间、内存、磁盘 I/O 等进程所需的资源进行限制;
不同资源的具体管理工作由相应的 Cgroup 子系统(Subsystem)来实现 ;
针对不同类型的资源限制,只要将限制策略在不同的的子系统上进行关联即可 ;
Cgroups 在不同的系统资源管理子系统中以层级树(Hierarchy)的方式来组织管理:每个
Cgroup 都可以包含其他的子 Cgroup,因此子 Cgroup 能使用的资源除了受本 Cgroup 配置
的资源参数限制,还受到父 Cgroup 设置的资源限制 。
区别
namespace: 环境隔离
PID
UTS
IPC
Network
Mount
User
cgroup: 资源限制
memory
cpu
Cpuset
Blkio
容器:
Namespace 和 Cgroups 可以让程序在一个资源可控的独立(隔离)环境中运行,这个就是容器了
docker和VM差异:
docker是一个应用层的抽象,容器之间通过网络命名空间进行隔离,多个容器共享同一个操作系统内核。VM是对物理硬件层的抽象,每个VM都包含独立的操作系统,重且启动缓慢。VM主要为了提供系统环境,容器主要是为了提供应用环境。
Docker容器都可以有四种状态。
运行 已暂停 重新启动 已退出
docker组件:
docker引擎【包含Docker客户端&服务端】,
docker镜像,
docker容器,
Registry【镜像仓库】
docker的架构: C/s架构
如何控制容器占用系统资源(CPU,内存)的份额
在使用docker create命令创建容器或使用docker run 创建并运行容器的时候,可以使用-c|–cpu-shares[=0]参数来调整同期使用CPU的权重,使用-m|–memory参数来调整容器使用内存的大小。
容器和镜像的区别:
镜像是一个只读模板,包括运行容器所需的数据,其内容在构建之后就不会被改变,可以用来创建新的容器。 镜像由多个只读层组成,容器在只读层的基础上多了一个读写层。
优势
封装性:
不需要再启动内核,所以应用扩缩容时可以秒速启动。
资源利用率高,直接使用宿主机内核调度资源,性能损失小。
方便的 CPU、内存资源调整。
能实现秒级快速回滚。
一键启动所有依赖服务,测试不用为搭建环境犯愁,PE 也不用为建站复杂担心。
镜像一次编译,随处使用。
测试、生产环境高度一致(数据除外)
隔离性:
• 应用的运行环境和宿主机环境无关,完全由镜像控制,一台物理机上部署多种环境的镜像测试。
• 多个应用版本可以并存在机器上。
镜像增量分发:
由于采用了 Union FS, 简单来说就是支持将不同的目录挂载到同
一个虚拟文件系统下,并实现一种 layer 的概念,每次发布只传输
变化的部分,节约带宽。
网络模型
Null(--net=None)
把容器放入独立的网络空间但不做任何网络配置;
用户需要通过运行 docker network 命令来完成网络配置。
Host
使用主机网络命名空间,复用主机网络。
Container
重用其他容器的网络。
Bridge(--net=bridge)
使用 Linux 网桥和 iptables 提供容器互联,
Docker 在每台主机上创建一个名叫 docker0的网桥,通过 veth pair 来连接该主机的每一个 EndPoint。
Overlay(libnetwork, libkv)
• 通过网络封包实现。
Remote(work with remote drivers)
Underlay:
• 使用现有底层网络,为每一个容器配置可路由的网络 IP。
Overlay:
• 通过网络封包实现。
默认使用bridge网络模型,容器的初次启动会虚拟化出来一个新的网卡名为docker0,
在多机器部署下docker0地址可能会冲突。所以docker对多机部署支持的不够友好。
命令操作
都是容器操作指令:
CMD 用于指定容器启动时候默认执行的命令。可以被docker run指定的启动命令覆盖。
ENTRYPONIT 指令可让容器以应用程序或者服务的形式运行。一般不会被docker run指定的启动命令覆盖。dockerfile中的多个CMD & ENTRYPONIT 只有最后一个会生效。
注意区别 docker run 和RUN 一个是容器启动命令,一个是镜像构建时候所用。
copy & add
ADD & COPY 选取目标文件复制到镜像当中。是针对镜像的指令,唯一差别在于add源文件可以支持url且可以对压缩文件进行解压操作。而copy针对的是当前构建环境。
docker-compose & docker swarm
使用Docker compose可以用YAML文件来定义一组需要启动的容器,以及容器运行时的属性。docker-compose用来对这一组容器进行操作。
docker swarm 原生的Docker集群管理工具,依赖docker本身,很多重要功能依赖团队二次开发。且社区不够活跃,一般公司生产环境会选择k8s,个人项目或者容器数量较少可选swarm,只需要docker即可完成,相对较轻。
构建原则
基础系统镜像
尽量选取满足需求但较小的
建议选择 debian:wheezy 镜像,仅有
86MB 大小
清理编译生成文件、安装包的缓存等临时文件
安装各个软件时候要指定准确的版本号,并避免引入不需要的依赖
从安全的角度考虑,应用尽量使用系统的库和依赖
使用 Dockerfile 创建镜像时候要添加.dockerignore 文件或使用干净的工作目录
Docker启动
初始化时也是将 rootfs 以 readonly 方式加载并检查,然而接下来利用 union mount 的方式将一个 readwrite 文件系统挂载在 readonly 的 rootfs 之上;
并且允许再次将下层的 FS(file system) 设定为 readonly 并且向上叠加;这样一组 readonly 和一个 writeable 的结构构成一个 container 的运行时态, 每一个 FS 被称作一个 FS 层。
微服务
springboot【CPU】
1、什么是springboot
1. 用来简化spring应用的初始搭建以及开发过程 使用特定的方式来进行配置(properties或yml文件)
2. 创建独立的spring引用程序 main方法运行
3. 嵌入的Tomcat 无需部署war文件
4. 简化maven配置
2、什么是 JavaConfig?
Spring JavaConfig 是 Spring 社区的产品,它提供了配置 Spring IoC 容器的纯 Java 方法。因此它有助于避免使用 XML 配置。 使用 JavaConfig 的优点在于: 面向对象的配置。由于配置被定义为 JavaConfig 中的类,因此用户可以充分利用 Java 中的面向对象功能。一个配置类可以继承另一个,重写它的@Bean 方法等。 减少或消除 XML 配置。基于依赖注入原则的外化配置的好处已被证明。但是,许多开发人员不希望在 XML 和 Java 之间来回切换。 JavaConfig 为开发人员提供了一种纯 Java 方法来配置与 XML 配置概念相似的 Spring 容器。从技术角度来讲,只使用 JavaConfig 配置类来配置容器是可行的,但实际上很多人认为将 JavaConfig 与 XML 混合匹配是理想的。 类型安全和重构友好。 JavaConfig 提供了一种类型安全的方法来配置 Spring 容器。由于Java 5.0 对泛型的支持,现在可以按类型而不是按名称检索 bean,不需要任何强制转换或基于字符串的查找。
3、Spring Boot有哪些优点?
1. 快速创建独立运行的spring项目与主流框架集成
2. 使用嵌入式的servlet容器,应用无需打包成war包
3. starters自动依赖与版本控制
4. 大量的自动配置,简化开发,也可修改默认值
6. 与云计算的天然集成
4、Spring Boot 提供了哪些核心功能?
1. 独立运行 Spring 项目
2. 内嵌 Servlet 容器 Spring Boot 可以选择内嵌 Tomcat、Jetty 或者 Undertow,这样我们无须以 war 包形式部署项目。
3. 提供 Starter 简化 Maven 配置 例如,当你使用了 spring-boot-starter-web ,会自动加入如下依赖:spring-boot-starter-web 的 pom 文件
4. 自动配置 Spring Bean Spring Boot 检测到特定类的存在,就会针对这个应用做一定的配置,进行自动配置 Bean ,这样会极大地减少我们要使用的配置。
5. 准生产的应用监控 Spring Boot 提供基于 HTTP、JMX、SSH 对运行时的项目进行监控。
6. 无代码生成和 XML 配置 Spring Boot 没有引入任何形式的代码生成,它是使用的 Spring 4.0 的条件 @Condition 注解以实现根据条件进行配置。同时使用了 Maven /Gradle 的依赖传递解析机制来实现 Spring 应用里面的自动配置。
5、如何重新加载Spring Boot上的更改,而无需重新启动服务器?
这可以使用DEV工具来实现。通过这种依赖关系,您可以节省任何更改,嵌入式tomcat将重新启动。 Spring Boot有一个开发工具(DevTools)模块,它有助于提高开发人员的生产力。Java开发人员面临的一个主要挑战是将文件更改自动部署到服务器并自动重启服务器。 开发人员可以重新加载Spring Boot上的更改,而无需重新启动服务器。这将消除每次手动部署更改的需要。Spring Boot在发布它的第一个版本时没有这个功能。 这是开发人员最需要的功能。DevTools模块完全满足开发人员的需求。该模块将在生产环境中被禁用。它还提供H2数据库控制台以更好地测试应用程序。
6、创建一个 Spring Boot Project 的最简单的方法是什么? Spring Initializer 是创建 Spring Boot Projects 的一个很好的工具
7、运行 Spring Boot 有哪几种方式?
1. 打包成 Fat Jar ,直接使用 java -jar 运行。目前主流的做法,推荐。
2. 在 IDEA 或 Eclipse 中,直接运行应用的 Spring Boot 启动类的 #main(String[] args) 启动。适用于开发调试场景。
3. 如果是 Web 项目,可以打包成 War 包,使用外部 Tomcat 或 Jetty 等容器。
8、Spring Boot中的监视器是什么? Spring boot actuator是spring启动框架中的重要功能之一。Spring boot监视器可帮助您访问生产环境中正在运行的应用程序的当前状态。 有几个指标必须在生产环境中进行检查和监控。即使一些外部应用程序可能正在使用这些服务来向相关人员触发警报消息。监视器模块公开了一组可直接作为HTTP URL访问的REST端点来检查状态。
12、如何集成Spring Boot和ActiveMQ? 对于集成Spring Boot和ActiveMQ,我们使用spring-boot-starter-activemq依赖关系。 它只需要很少的配置,并且不需要样板代码。
13、springboot常用的starter有哪些? spring-boot-starter-web 嵌入tomcat和web开发需要servlet与jsp支持 spring-boot-starter-data-jpa 数据库支持 spring-boot-starter-data-redis redis数据库支持 spring-boot-starter-data-solr solr支持 mybatis-spring-boot-starter 第三方的mybatis集成starter
自动配置的原理
在spring程序main方法中 添加@SpringBootApplication或者@EnableAutoConfiguration 会自动去maven中读取每个starter中的spring.factories文件 该文件里配置了所有需要被创建spring容器中的bean
15、springboot读取配置文件的方式
springboot默认读取配置文件为application.properties或者是application.yml
16、Spring Boot 需要独立的容器运行吗?
可以不需要,内置了 Tomcat/ Jetty 等容器。
17、运行 Spring Boot 有哪几种方式?
1)打包用命令或者者放到容器中运行
2)用 Maven/ Gradle 插件运行
3)直接执行 main 方法运行
18、Spring Boot 的核心配置文件有哪几个?它们的区别是什么?
Spring Boot 的核心配置文件是 application 和 bootstrap 配置文件。 application 配置文件这个容易了解,主要用于 Spring Boot 项目的自动化配置。 bootstrap 配置文件有以下几个应用场景。 使用 Spring Cloud Config 配置中心时,这时需要在 bootstrap 配置文件中增加连接到配置中心的配置属性来加载外部配置中心的配置信息; 少量固定的不能被覆盖的属性; 少量加密/解密的场景;
19、Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?
启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:
@Configuration 注解,实现配置文件的功能。
@EnableAutoConfiguration:打开自动配置的功能,
也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。
@ComponentScan:Spring组件扫描
20、为什么我们需要 spring-boot-maven-plugin?
spring-boot-maven-plugin 提供了一些像 jar 一样打包或者运行应用程序的命令。
spring-boot:run 运行你的 SpringBoot 应用程序。
spring-boot:repackage 重新打包你的 jar 包或者是 war 包使其可执行 spring-boot:start 和 spring-boot:stop 管理 Spring Boot 应用程序的生命周期(也可以说是为了集成测试)。 spring-boot:build-info 生成执行器可以使用的构造信息。
21、如何使用Spring Boot实现分页和排序?
使用Spring Boot实现分页非常简单。使用Spring Data-JPA可以实现将可分页的 org.springframework.data.domain.Pageable 传递给存储库方法。
22、什么是Swagger?你用Spring Boot实现了它吗? Swagger广泛用于可视化API,使用Swagger UI为前端开发人员提供在线沙箱。Swagger是用于生成RESTful Web服务的可视化表示的工具,规范和完整框架实现。它使文档能够以与服务器相同的速度更新。当通过Swagger正确定义时,消费者可以使用最少量的实现逻辑来理解远程服务并与其进行交互。因此,Swagger消除了调用服务时的猜测。
23、什么是Spring Profiles? Spring Profiles允许用户根据配置文件(dev,test,prod等)来注册bean。因此,当应用程序在开发中运行时,只有某些bean可以加载,而在PRODUCTION中,某些其他bean可以加载。假设我们的要求是Swagger文档仅适用于QA环境,并且禁用所有其他文档。这可以使用配置文件来完成。Spring Boot使得使用配置文件非常简单。
24、什么是Spring Batch? Spring Boot Batch提供可重用的函数,这些函数在处理大量记录时非常重要,包括日志/跟踪,事务管理,作业处理统计信息,作业重新启动,跳过和资源管理。它还提供了更先进的技术服务和功能,通过优化和分区技术,可以实现极高批量和高性能批处理作业。简单以及复杂的大批量批处理作业可以高度可扩展的方式利用框架处理重要大量的信息。
25、什么是FreeMarker模板?
FreeMarker是一个基于Java的模板引擎,最初专注于使用MVC软件架构进行动态网页生成。使用Freemarker的主要优点是表示层和业务层的完全分离。程序员可以处理应用程序代码,而设计人员可以处理html页面设计。最后使用freemarker可以将这些结合起来,给出最终的输出页面。
26、什么是JavaConfig? Spring JavaConfig是Spring社区的产品,它提供了配置Spring IoC容器的纯Java方法。因此它有助于避免使用XML配置。使用JavaConfig的优点在于: 面向对象的配置。由于配置被定义为JavaConfig中的类,因此用户可以充分利用Java中的面向对象功能。一个配置类可以继承另一个,重写它的@Bean方法等。 减少或消除XML配置。基于依赖注入原则的外化配置的好处已被证明。但是,许多开发人员不希望在XML和Java之间来回切换。 JavaConfig为开发人员提供了一种纯Java方法来配置与XML配置概念相似的Spring容器。 从技术角度来讲,只使用JavaConfig配置类来配置容器是可行的,但实际上很多人认为将JavaConfig与XML混合匹配是理想的。 类型安全和重构友好。JavaConfig提供了一种类型安全的方法来配置Spring容器。由于Java 5.0对泛型的支持,现在可以按类型而不是按名称检索bean,不需要任何强制转换或基于字符串的查找
27、启动类注解:
@SpringBootConfiguration:Spring Boot的配置类; 标注在某个类上,表示这是一个Spring Boot的配置类; @Configuration:配置类上来标注这个注解;配置类 ----- 配置文件;配置类也是容器中的一个组件;@Component @EnableAutoConfiguration:开启自动配置功能; 以前我们需要配置的东西,Spring Boot帮我们自动配置;
@EnableAutoConfiguration告诉SpringBoot开启自动配置功能;这样自动配置才能生效; Spring Boot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值,将这些值作为自动配置类导入到容器中,自动配置类就失效,帮我们进行自动配置工作
28、配置文件的加载顺序
由jar包外向jar包内进行寻找; 优先加载带profile jar包外部的application-{profile}.properties或application.yml(带spring.profile)配置文件 jar包内部的application-{profile}.properties或application.yml(带spring.profile)配置文件 再来加载不带profile jar包外部的application.properties或application.yml(不带spring.profile)配置文件 jar包内部的application.properties或application.yml(不带spring.profile)配置文件
29、自动配置原理
1)、SpringBoot启动的时候加载主配置类,开启了自动配置功能 @EnableAutoConfiguration 2)、@EnableAutoConfiguration 作用: 将类路径下 META-INF/spring.factories 里面配置的所有EnableAutoConfiguration的值加入到了容器中; 每一个这样的 xxxAutoConfiguration类都是容器中的一个组件,都加入到容器中;用他们来做自动配置; 3)、每一个自动配置类进行自动配置功能; 根据当前不同的条件判断,决定这个配置类是否生效; 4)、一但这个配置类生效;这个配置类就会给容器中添加各种组件;这些组件的属性是从对应的properties类中获取 的,这些类里面的每一个属性又是和配置文件绑定的; 5)、所有在配置文件中能配置的属性都是在xxxxProperties类中封装者‘;配置文件能配置什么就可以参照某个功 能对应的这个属性类
30、怎么用好自动配置,精髓:
1)、SpringBoot启动会加载大量的自动配置类 2)、我们看我们需要的功能有没有SpringBoot默认写好的自动配置类; 3)、我们再来看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件有,我们就不需要再来配置了) 4)、给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。我们就可以在配置文件中指定这 些属性的值;
31、日志框架:
SpringBoot选用 SLF4j和logback; 如何让系统中所有的日志都统一到slf4j; 1、将系统中其他日志框架先排除出去; 2、用中间包来替换原有的日志框架; 3、我们导入slf4j其他的实现 SpringBoot能自动适配所有的日志,而且底层使用slf4j+logback的方式记录日志
32、Spring Boot、Spring MVC 和 Spring 有什么区别
Spring 是一个“引擎”, Spring MVC是基于Spring的一个 MVC 框架, Spring Boot是基于 Spring的一套快速开发整合包
33、我们如何监视所有 Spring Boot 微服务?
Spring Boot 提供监视器端点以监控各个微服务的度量。这些端点对于获取有关应用程序的信息(如它们是否已启动)以及它们的组件(如数据库等)是否正常运行很有帮助。但是,使用监视器的一个主要缺点或困难是,我们必须单独打开应用程序的知识点以了解其状态或健康状况。想象一下涉及 50 个应用程序的微服务,管理员将不得不击中所有 50 个应用程序的执行终端。
链路追踪
要求
探针的性能消耗。APM组件服务的影响应该做到足够小。在一些高度优化过的服务,即使一点点损耗也会很容易察觉到,而且有可能迫使在线服务的部署团队不得不将跟踪系统关停。
代码的侵入性对于应用的程序员来说,是不需要知道有跟踪系统这回事的。如果一个跟踪系统想生效,就必须需要依赖应用的开发者主动配合,那么这个跟踪系统也太脆弱了,往往由于跟踪系统在应用中植入代码的bug或疏忽导致应用出问题,这样才是无法满足对跟踪系统“无所不在的部署”这个需求。
可扩展性能够支持的组件越多当然越好。或者提供便捷的插件开发API,对于一些没有监控到的组件,应用开发者也可以自行扩展。
数据的分析数据的分析要快 ,分析的维度尽可能多。跟踪系统能提供足够快的信息反馈,就可以对生产环境下的异常状况做出快速反应。分析的全面,能够避免二次开发。
skywalking
架构图
Zipkin
由Twitter公司开源,开放源代码分布式的跟踪系统,用于收集服务的定时数据,以解决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现。
图
Pinpoint
Pinpoint是一款对Java编写的大规模分布式系统的APM工具,由韩国人开源的分布式跟踪组件。
SpringCloud【电脑】
架构图
Seata (阿里 分布式事务的中间件)
Sentinel(阿里 流量控制、熔断、系统负载保护)
GateWay 【网卡】
架构图
(网关、限流、日志、鉴权)
SpringCloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Zuul,在Spring Cloud 2.0以上版本中,没有对新版本的Zuul 2.0以上最新高性能版本进行集成,仍然还是使用的Zuul 2.0之前的非Reactor模式的老版本。而为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。
Spring Cloud Gateway 的目标,不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。
Feign
原理
图
组件
远程接口的本地JDK Proxy代理实例
(1)Proxy代理实例,实现了一个加 @FeignClient 注解的远程调用接口;
(2)Proxy代理实例,能在内部进行HTTP请求的封装,以及发送HTTP 请求;
(3)Proxy代理实例,能处理远程HTTP请求的响应,并且完成结果的解码,然后返回给调用者。
Nacos【硬盘】
架构图
功能
(服务发现、配置、管理)
链接
https://www.jianshu.com/p/90bac11dd02d
https://mp.weixin.qq.com/s/kjkXGWQA6pA6UTJakhFmrw
Zookeeper
一致性服务 比如:配置维护、域名维护、分布式同步
Zookeeper 保证了如下分布式一致性特性:
1、顺序一致性
2、原子性
3、单一视图
4、可靠性
5、实时性( 最终一致性)
客户端的读请求可以被集群中的任意一台机器处理, 如果读请求在节点上注册了监听器,这个监听器也是由所连接的 zookeeper 机器来处理。对于写请求,这些请求会同时发给其他 zookeeper 机器并且达成一致后,请求才会返回成功。因此, 随着 zookeeper 的集群机器增多,读请求的吞吐会提高但是写请求的吞吐会下降。
有序性是 zookeeper 中非常重要的一个特性, 所有的更新都是全局有序的, 每个更新都有一个唯一的时间戳,这个时间戳称为 zxid( Zookeeper Transaction Id)。而读请求只会相对于更新有序, 也就是读请求的返回结果中会带有这个
zookeeper 最新的 zxid。
1. ZooKeeper 提供了什么?
1、文件系统
2、通知机制
2. Zookeeper 文件系统
Zookeeper 提供一个多层级的节点命名空间( 节点称为 znode)。与文件系统不同的是, 这些节点都可以设置关联的数据, 而文件系统中只有文件节点可以存放数据而目录节点不行。
Zookeeper 为了保证高吞吐和低延迟, 在内存中维护了这个树状的目录结构, 这种特性使得 Zookeeper 不能用于存放大量的数据, 每个节点的存放数据上限为1M。
3. ZAB 协议?
ZAB 协议是为分布式协调服务 Zookeeper 专门设计的一种支持崩溃恢复的原子广播协议。
ZAB 协议包括两种基本的模式: 崩溃恢复和消息广播。
当整个 zookeeper 集群刚刚启动或者 Leader 服务器宕机、重启或者网络故障导致不存在过半的服务器与 Leader 服务器保持正常通信时,所有进程( 服务器)进入崩溃恢复模式, 首先选举产生新的 Leader 服务器, 然后集群中 Follower 服务器开始与新的 Leader 服务器进行数据同步, 当集群中超过半数机器与该 Leader 服务器完成数据同步之后, 退出恢复模式进入消息广播模式, Leader 服务器开始接收客户端的事务请求生成事物提案来进行事务请求处理。
4. 四种类型的数据节点 Znode
1、PERSISTENT-持久节点
除非手动删除, 否则节点一直存在于 Zookeeper 上
2、EPHEMERAL-临时节点
临时节点的生命周期与客户端会话绑定, 一旦客户端会话失效( 客户端与
zookeeper 连接断开不一定会话失效), 那么这个客户端创建的所有临时节点都会被移除。
3、PERSISTENT_SEQUENTIAL-持久顺序节点
基本特性同持久节点, 只是增加了顺序属性, 节点名后边会追加一个由父节点维护的自增整型数字。
4、EPHEMERAL_SEQUENTIAL-临时顺序节点
基本特性同临时节点, 增加了顺序属性, 节点名后边会追加一个由父节点维护的自增整型数字。
1. ACL 权限控制机制
UGO( User/Group/Others)
目前在 Linux/Unix 文件系统中使用,也是使用最广泛的权限控制方式。是一种粗粒度的文件系统权限控制模式。
ACL( Access Control List) 访问控制列表包括三个方面:
权限模式( Scheme)
1、IP: 从 IP 地址粒度进行权限控制
2、Digest: 最常用, 用类似于 username:password 的权限标识来进行权限配置, 便于区分不同应用来进行权限控制
3、World:最开放的权限控制方式,是一种特殊的 digest 模式,只有一个权限标识“ world:anyone”
4、Super: 超级用户
授权对象
授权对象指的是权限赋予的用户或一个指定实体, 例如 IP 地址或是机器灯。
权 限 Permission
1、CREATE: 数据节点创建权限, 允许授权对象在该 Znode 下创建子节点
2、DELETE: 子节点删除权限, 允许授权对象删除该数据节点的子节点
3、READ: 数据节点的读取权限, 允许授权对象访问该数据节点并读取其数据内容或子节点列表等
4、WRITE: 数据节点更新权限, 允许授权对象对该数据节点进行更新操作
5、ADMIN: 数据节点管理权限,允许授权对象对该数据节点进行 ACL 相关设置操作
1. 数据同步
整个集群完成 Leader 选举之后, Learner( Follower 和 Observer 的统称) 回向Leader 服务器进行注册。当 Learner 服务器想 Leader 服务器完成注册后, 进入数据同步环节。
数据同步流程:( 均以消息传递的方式进行) Learner 向 Learder 注册
数据同步同步确认
Zookeeper 的数据同步通常分为四类:
1、直接差异化同步( DIFF 同步)
2、先回滚再差异化同步( TRUNC+DIFF 同步)
3、仅回滚同步( TRUNC 同步)
4、全量同步( SNAP 同步)
apollo
tu
链接
https://www.jianshu.com/p/80a53a592178
数据存储
ElasticSearch
架构图
ES集群核心概念
集群(cluster)
一个ES集群由多个节点(node)组成, 每个集群都有一个共同的集群名称最为标识
节点(node)
一个es实例即为一个节点,一台机器可以有多个节点,正常使用下每个实例都应该会部署在不同的机器上。
ES的配置文件中可以通过node.master、 node.data 来设置节点类型
node.master: true/false 表示节点是否具有成为主节点的资格
node.data: true/false 表示节点是否为存储数据
node节点的组合方式
主节点+数据节点: 默认方式,节点既可以作为主节点,又存储数据
数据节点: 节点只存储数据,不参与主节点选举
客户端节点: 不会成为主节点,也不存储数据,主要针对海量请求时进行负载均衡
分片(shard):
如果我们的索引数据量很大,超过硬件存放单个文件的限制,就会影响查询请求的速度,ES引入了分片技术。一个分片本身就是一个完成的搜索引擎,文档存储在分片中,而分片会被分配到集群中的各个节点中,随着集群的扩大和缩小,ES会自动的将分片在节点之间进行迁移,以保证集群能保持一种平衡。分片有以下特点:
ES的一个索引可以包含多个分片(shard);
每一个分片(shard)都是一个最小的工作单元,承载部分数据;
每个shard都是一个lucene实例,有完整的简历索引和处理请求的能力;
增减节点时,shard会自动在nodes中负载均衡;
一个文档只能完整的存放在一个shard上
一个索引中含有shard的数量,默认值为5,在索引创建后这个值是不能被更改的。
优点:水平分割和扩展我们存放的内容索引;分发和并行跨碎片操作提高性能/吞吐量;
每一个shard关联的副本分片(replica shard)的数量,默认值为1,这个设置在任何时候都可以修改。
副本:replica
副本(replica shard)就是shard的冗余备份,它的主要作用:
冗余备份,防止数据丢失;
shard异常时负责容错和负载均衡;
索引文档的过程
文档写入包含:单文档写入和批量 bulk 写入,这里只解释一下:单文档写入流程
第一步:客户写集群某节点写入数据,发送请求。( 如果没有指定路由/协调节点, 请求的节点扮演路由节点的角色。)
第二步: 节点 1 接受到请求后, 使用文档_id 来确定文档属于分片 0。请求会被转到另外的节点, 假定节点 3。因此分片 0 的主分片分配到节点 3 上。
第三步: 节点 3 在主分片上执行写操作, 如果成功, 则将请求并行转发到节点 1 和节点 2 的副本分片上, 等待结果返回。所有的副本分片都报告成功, 节点 3 将向协调节点( 节点 1) 报告成功, 节点 1 向请求客户端报告写入成功。
第二步中的文档获取分片的过程?
借助路由算法获取,路由算法就是根据路由和文档 id 计算目标的分片 id 的过程。
1shard = hash(_routing) % (num_of_primary_shards)
lucense
tu
ES依赖一个重要的组件Lucene,关于数据结构的优化通常来说是对Lucene的优化,它是集群的一个存储于检索工作单元
在Lucene中,分为索引(录入)与检索(查询)两部分,索引部分包含分词器、过滤器、字符映射器等,检索部分包含查询解析器等。
一个Lucene索引包含多个segments,一个segment包含多个文档,每个文档包含多个字段,每个字段经过分词后形成一个或多个term。
NOSQL
redis (厨房)
优势和场景【洗碗机】
全景
问题画像
优点
对于 redis 和 memcached 我总结了下面四点。现在公司一般都是用 redis 来实现缓存,而且 redis 自身也越来越强大了!
1. redis支持更丰富的数据类型(支持更复杂的应用场景):
Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。
2. Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memecache把数据全部存在内存之中。
3. 集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前是原生支持 cluster 模式的.
5. Redis使用单线程的多路 IO 复用模型。
高效率的原因
1)纯内存操作(主要)
2)核心是基于非阻塞的IO多路复用机制(主要)
3)单线程反而避免了多线程的频繁上下文切换问题
6.
使用场景
1、会话缓存( Session Cache)
2、全页缓存( FPC)
除基本的会话 token 之外, Redis 还提供很简便的 FPC 平台。回到一致性问题, 即使重启了 Redis 实例, 因为有磁盘的持久化, 用户也不会看到页面加载速度的下降,这是一个极大改进,类似 PHP 本地 FPC。 再次以 Magento 为例,Magento 提供一个插件来使用 Redis 作为全页缓存后端。 此外, 对 WordPress 的用户来说, Pantheon 有一个非常好的插件 wp-redis, 这个插件能帮助你以最快速度加载你曾浏览过的页面。
3、队列
Reids 在内存存储引擎领域的一大优点是提供 list 和 set 操作, 这使得 Redis 能作为一个很好的消息队列平台来使用。Redis 作为队列使用的操作,就类似于本地程序语言( 如 Python)对 list 的 push/pop 操作。 如果你快速的在 Google 中搜索“ Redis queues”, 你马上就能找到大量的开源项目, 这些项目的目的就是利用 Redis 创建非常好的后端工具, 以满足各种队列需求
4, 排行榜/计数器
Redis 在内存中对数字进行递增或递减的操作实现的非常好。集合( Set) 和有序集合( Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis 只是正好提供了这两种数据结构。所以, 我们要从排序集合中获取到排名最靠前的 10 个用户– 我们称之为“ user_scores”, 我们只需要像下面一样执行即可: 当然, 这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数, 你需要这样执行: ZRANGE user_scores 0 10 WITHSCORES Agora Games 就是一个很好的例子, 用 Ruby 实现的, 它的排行榜就是使用 Redis 来存储数据的, 你可以在这里看到。
5、发布/订阅
最后( 但肯定不是最不重要的)是 Redis 的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用, 还可作为基于发布/订阅的脚本触发器, 甚至用 Redis 的发布/订阅功能来建立聊天系统!
性能问题【油烟机】
常见问题
1、Master 最好不要写内存快照,如果 Master 写内存快照,save 命令调度 rdbSave函数, 会阻塞主线程的工作, 当快照比较大时对性能影响是非常大的, 会间断性暂停服务
2、如果数据比较重要, 某个 Slave 开启 AOF 备份数据, 策略设置为每秒同步一
3、为了主从复制的速度和连接的稳定性, Master 和 Slave 最好在同一个局域网
4、尽量避免在压力很大的主库上增加从库
5、主从复制不要用图状结构, 用单向链表结构更为稳定, 即:Master <- Slave1<- Slave2 <- Slave3 … 这样的结构方便解决单点故障问题,实现 Slave 对 Master 的替换。如果 Master 挂了, 可以立刻启用 Slave1 做 Master, 其他不变。
优化
fork耗时导致高并发请求延时
fork耗时跟redis主进程的内存有关系,一般控制redis的内存在10GB以内,slave -> master,全量复制
主从复制风暴问题
如果一下子让多个slave从master去执行全量复制,一份大的rdb同时发送到多个slave,会导致网络带宽被严重占用
如果一个master真的要挂载多个slave,那尽量用树状结构,不要用星型结构
缓存雪崩
原因
解决方案
图
事前:redis高可用,主从+哨兵,redis cluster,避免全盘崩溃
事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL被打死
事后:redis持久化,快速恢复缓存数据
数据倾斜
分类
数据量倾斜:
某个实例上保存了 bigkey
bigkey 的操作一般都会造成实例 IO 线程阻塞,如果 bigkey 的访问量比较大,就会影响到这个实例上的其它请求被处理的速度。
对策:
我们在业务层生成数据时,要尽量避免把过多的数据保存在同一个键值对中。
如果 bigkey 正好是集合类型,把 bigkey 拆分成很多个小的集合类型数据,分散保存在不同的实例上。
Slot 分配不均衡
如果集群运维人员没有均衡地分配 Slot,就会有大量的数据被分配到同一个 Slot 中,而同一个 Slot 只会在一个实例上分布,这就会导致,大量数据被集中到一个实例上,造成数据倾斜。
我们可以通过运维规范,在分配之前,我们就要避免把过多的 Slot 分配到同一个实例。
如果是已经分配好 Slot 的集群,先查看 Slot 和实例的具体分配关系,判断是否有过多的 Slot 集中到了同一个实例。
如果有的话,就将部分 Slot 迁移到其它实例,从而避免数据倾斜。
Hash Tag
Hash Tag 是指加在键值对 key 中的一对花括号{}。这对括号会把 key 的一部分括起来,客户端在计算 key 的 CRC16 值时,只对 Hash Tag 花括号中的 key 内容进行计算。如果没用 Hash Tag 的话,客户端计算整个 key 的 CRC16 的值。
举个例子,假设 key 是 user:profile:3231,我们把其中的 3231 作为 Hash Tag,此时,key 就变成了 user:profile:{3231}。当客户端计算这个 key 的 CRC16 值时,就只会计算 3231 的 CRC16 值。否则,客户端会计算整个“user:profile:3231”的 CRC16 值。
Hash Tag 的好处是,如果不同 key 的 Hash Tag 内容都是一样的,那么,这些 key 对应的数据会被映射到同一个 Slot 中,同时会被分配到同一个实例上。
Hash Tag 应用场景
用在 Redis Cluster 和 Codis 中,支持事务操作和范围查询。因为 Redis Cluster 和 Codis 本身并不支持跨实例的事务操作和范围查询,当业务应用有这些需求时,就只能先把这些数据读取到业务层进行事务处理,或者是逐个查询每个实例,得到范围查询的结果。
这样操作起来非常麻烦,所以,使用 Hash Tag 把要执行事务操作或是范围查询的数据映射到同一个实例上,这样就能很轻松地实现事务或范围查询了
该怎么应对这种问题呢?我们就需要在范围查询、事务执行的需求和数据倾斜带来的访问压力之间,进行取舍了。
数据访问倾斜:
虽然每个集群实例上的数据量相差不大,但是某个实例上的数据是热点数据,被访问得非常频繁。
淘汰策略【垃圾桶】
1、定时删除:在设置键的过期时间的同时,创建一个定时器 timer. 让定时器在键的过期时间来临时, 立即执行对键的删除操作。
2、惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是 否过期, 如果过期的话, 就删除该键;如果没有过期, 就返回该键。
3、定期删除:每隔一段时间程序就对数据库进行一次检查,删除里面的过期键。至 于要删除多少过期键, 以及要检查多少个数据库, 则由算法决定。
Redis 的回收策略(淘汰策略)
volatile-lru:从已设置过期时间的数据集( server.db[i].expires)中挑选最近最少使用的数据淘汰
volatile-ttl: 从已设置过期时间的数据集( server.db[i].expires) 中挑选将要过期的数据淘汰
volatile-random: 从已设置过期时间的数据集( server.db[i].expires) 中任意选择数据淘汰
allkeys-lru: 从数据集( server.db[i].dict) 中挑选最近最少使用的数据淘汰
allkeys-random: 从数据集( server.db[i].dict) 中任意选择数据淘汰
no-enviction( 驱逐) : 禁止驱逐数据
注意这里的 6 种机制,volatile 和 allkeys 规定了是对已设置过期时间的数据集淘汰数据还是从全部数据集淘汰数据, 后面的 lru、ttl 以及 random 是三种不同的淘汰策略, 再加上一种 no-enviction 永不回收的策略。
高可用【洗衣机】
哨兵和集群
Redis Sentinal 着眼于高可用, 在 master 宕机时会自动将 slave 提升为master, 继续提供服务。
Redis Cluster 着眼于扩展性, 在单个 redis 内存不足时, 使用 Cluster 进行分片存储。
Redis 可以使用主从同步,从从同步。第一次同步时,主节点做一次 bgsave, 并同时将后续修改操作记录到内存 buffer, 待完成后将 rdb 文件全量同步到复制节点, 复制节点接受完成后将 rdb 镜像加载到内存。加载完成后, 再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。
图
集群
架构图
一个Redis集群通常由多个节点组成,最初每个节点都是独立的,它们都处于只包含自己的集群之中,当通过CLUSTER MEET <ip> <port>命令将各个独立的节点连接起来之后,它们就组成了一个集群。
一个节点其实就是一个运行在集群模式下的Redis服务器。其所提供的功能与普通的Redis服务器一致的。
作用
读拓展
一个 master 用于写,多个 slave 用于分摊读的压力。
图
高可用
Redis Cluster
原因
1.主从复制不能实现高可用
2.业务需要更高的QPS,而主从复制中单机的QPS可能无法满足业务需求
3.数据量的考虑,现有服务器内存不能满足业务数据的需要时,单纯向服务器添加内存不能达到要求,此时需要考虑分布式需求,把数据分布到不同服务器上
4.网络流量需求:业务的流量已经超过服务器的网卡的上限值,可以考虑使用分布式来进行分流
5.离线计算,需要中间环节缓冲等别的需求
优缺点
优点:
(1)无需Sentinel哨兵监控,如果Master挂了,Redis Cluster内部自动将Slave切换Master
(2)可以进行水平扩容
(3)支持自动化迁移,当出现某个Slave宕机了,那么就只有Master了,这时候的高可用性就无法很好的保证了,万一master也宕机了,咋办呢? 针对这种情况,如果说其他Master有多余的Slave ,集群自动把多余的Slave迁移到没有Slave的Master 中。
缺点:
(1)批量操作是个坑
(2)资源隔离性较差,容易出现相互影响的情况。
限制Redis Cluster规模的关键因素
实例间的通信开销会随着实例规模增加而增大,在集群超过一定规模时(比如 800 节点),集群吞吐量反而会下降。
Gossip 协议的工作原理可以概括成两点。
一是,每个实例之间会按照一定的频率,从集群中随机挑选一些实例,把 PING 消息发送给挑选出来的实例,用来检测这些实例是否在线,并交换彼此的状态信息。PING 消息中封装了发送消息的实例自身的状态信息、部分其它实例的状态信息,以及 Slot 映射表。
二是,一个实例在接收到 PING 消息后,会给发送 PING 消息的实例,发送一个 PONG 消息。PONG 消息包含的内容和 PING 消息一样。
实例间使用 Gossip 协议进行通信时,通信开销受到通信消息大小和通信频率这两方面的影响,
每个实例在发送一个 Gossip 消息时,除了会传递自身的状态信息,默认还会传递集群十分之一实例的状态信息。
一个 Gossip 消息的大小:nodename 和 ip 这两个字节数组的长度是 40 字节和 46 字节,我们再把结构体中其它信息的大小加起来,就可以得到一个 Gossip 消息的大小了,即 104 字节
PING 消息大小:对于一个包含了 1000 个实例的集群来说,每个实例发送一个 PING 消息时,会包含 100 个实例的状态信息,总的数据量是 10400 字节,再加上发送实例自身的信息,一个 Gossip 消息大约是 10KB。
此外,为了让 Slot 映射表能够在不同实例间传播,PING 消息中还带有一个长度为 16,384 bit 的 Bitmap,这个 Bitmap 的每一位对应了一个 Slot,如果某一位为 1,就表示这个 Slot 属于当前实例。这个 Bitmap 大小换算成字节后,是 2KB。我们把实例状态信息和 Slot 分配信息相加,就可以得到一个 PING 消息的大小了,大约是 12KB。
PONG 消息和 PING 消息的内容一样,所以,它的大小大约是 12KB。每个实例发送了 PING 消息后,还会收到返回的 PONG 消息,两个消息加起来有 24KB。
主从复制
作用
数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
原理
重同步(resync)
完整重同步(full resynchronization)
slave 通过 SYNC 或 PSYNC 命令,向 master 发起同步请求。
master 执行 BGSAVE 命令,将当前数据库状态保存为 RDB 文件。
生成 RDB 文件完毕后,master 将该文件发送给 slave。
slave 收到 RDB 文件后,将其加载至内存。
master 将 backlog 中缓冲的命令发送给 slave(一开始在 BGSAVE 时记录了当时的 offset)。
slave 收到后,逐个执行这些命令。
部分重同步(partial resynchronization)
slave 通过 PSYNC <replication-id> <offset> 命令,向 master 发起「部分重同步」请求。
master 返回 CONTINUE 告知 slave 同意执行「部分重同步」,先决条件为:
<replication-id> 是 master 的 replication-id,并且 slave 给的 <offset> 在 master 的「复制积压缓冲区」backlog 里面
master 将 backlog 中缓冲的命令发送给 slave(根据 slave 给的 offset)。
slave 收到后,逐个执行这些命令。
复制积压缓冲区
是 master 维护的一个固定长度(fixed-sized)的先进先出(FIFO)的内存队列。值得注意的是:
队列的大小由配置 repl-backlog-size 决定,默认为 1MB。当队列长度超过 repl-backlog-size 时,最先入队的元素会被弹出,用于腾出空间给新入队的元素。7
队列的生存时间由配置 repl-backlog-ttl 决定,默认为 3600 秒。如果 master 不再有与之相连接的 slave,并且该状态持续时间超过了 repl-backlog-ttl,master 就会释放该队列,等到有需要(下次又有 slave 连接进来)的时候再创建。
命令传播(command propagate)
「命令传播」用于在 master 的数据库状态被修改时,将导致变更的命令传播给 slave,从而让 slave 的数据库状态与 master 保持一致。
原理步骤
建立连接
保存主节点信息
建立socket连接
发送ping命令
身份验证
发送从节点端口信息
同步数据
命令传播
全量复制和部分复制
全量复制:用于初次复制或其他无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,是一个非常重型的操作。
(1)从节点判断无法进行部分复制,向主节点发送全量复制的请求;或从节点发送部分复制的请求,但主节点判断无法进行部分复制;具体判断过程需要在讲述了部分复制原理后再介绍。
(2)主节点收到全量复制的命令后,执行bgsave,在后台生成RDB文件,并使用一个缓冲区(称为复制缓冲区)记录从现在开始执行的所有写命令
(3)主节点的bgsave执行完成后,将RDB文件发送给从节点;从节点首先清除自己的旧数据,然后载入接收的RDB文件,将数据库状态更新至主节点执行bgsave时的数据库状态
(4)主节点将前述复制缓冲区中的所有写命令发送给从节点,从节点执行这些写命令,将数据库状态更新至主节点的最新状态
(5)如果从节点开启了AOF,则会触发bgrewriteaof的执行,从而保证AOF文件更新至主节点的最新状态
部分复制:用于网络中断等情况后的复制,只将中断期间主节点执行的写命令发送给从节点,与全量复制相比更加高效。需要注意的是,如果网络中断时间过长,导致主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制。
心跳机制
PING
每隔指定的时间,主节点会向从节点发送PING命令,这个PING命令的作用,主要是为了让从节点进行超时判断。
REPLCONF ACK
实时监测主从节点网络状态
检测命令丢失
辅助保证从节点的数量和延迟
主要出现的问题
(1)数据同步阶段:在主从节点进行全量复制bgsave时,主节点需要首先fork子进程将当前数据保存到RDB文件中,然后再将RDB文件通过网络传输到从节点。如果RDB文件过大,主节点在fork子进程+保存RDB文件时耗时过多,可能会导致从节点长时间收不到数据而触发超时;此时从节点会重连主节点,然后再次全量复制,再次超时,再次重连……这是个悲伤的循环。为了避免这种情况的发生,除了注意Redis单机数据量不要过大,另一方面就是适当增大repl-timeout值,具体的大小可以根据bgsave耗时来调整。
(2)命令传播阶段:如前所述,在该阶段主节点会向从节点发送PING命令,频率由repl-ping-slave-period控制;该参数应明显小于repl-timeout值(后者至少是前者的几倍)。否则,如果两个参数相等或接近,网络抖动导致个别PING命令丢失,此时恰巧主节点也没有向从节点发送数据,则从节点很容易判断超时。
(3)慢查询导致的阻塞:如果主节点或从节点执行了一些慢查询(如keys *或者对大数据的hgetall等),导致服务器阻塞;阻塞期间无法响应复制连接中对方节点的请求,可能导致复制超时。
哨兵
Sentinel(哨兵)是用于监控redis集群中Master状态的工具,是Redis 的高可用性解决方案,sentinel哨兵模式已经被集成在redis2.4之后的版本中。
sentinel是redis高可用的解决方案,sentinel系统可以监视一个或者多个redis master服务,以及这些master服务的所有从服务;当某个master服务下线时,自动将该master下的某个从服务升级为master服务替代已下线的master服务继续处理请求。
主要功能
(1)集群监控,负责监控redis master和slave进程是否正常工作
(2)消息通知,如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员
(3)故障转移,如果master node挂掉了,会自动转移到slave node上
(4)配置中心,如果故障转移发生了,通知client客户端新的master地址
工作原理
1)每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他 Sentinel 实例发送一个PING命令。
2)如果一个实例(instance)距离最后一次有效回复PING命令的时间超过 own-after-milliseconds 选项所指定的值,则这个实例会被Sentinel标记为主观下线。
3)如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master的确进入了主观下线状态。
4)当有足够数量的Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态,则Master会被标记为客观下线。
5)在一般情况下,每个Sentinel 会以每10秒一次的频率向它已知的所有Master,Slave发送 INFO 命令。
6)当Master被Sentinel标记为客观下线时,Sentinel 向下线的 Master 的所有Slave发送 INFO命令的频率会从10秒一次改为每秒一次。
7)若没有足够数量的Sentinel同意Master已经下线,Master的客观下线状态就会被移除。 若 Master重新向Sentinel 的PING命令返回有效回复,Master的主观下线状态就会被移除。
仲裁会
这个区别看起来很微妙,但是很容易理解和使用。例如,集群中有5个sentinel,票数被设置为2,当2个sentinel认为一个master已经不可用了以后,将会触发failover,但是,进行failover的那个sentinel必须先获得至少3个sentinel的授权才可以实行failover。
领头Sentinel
一个redis服务被判断为客观下线时,多个监视该服务的sentinel协商,选举一个领头sentinel,对该redis服务进行故障转移操作。
持久化【冰箱】
Redis 提供两种持久化机制 RDB 和 AOF 机制:
1、RDB (Redis DataBase)持久化方式: 是指用数据集快照的方式半持久化模式) 记录 redis 数据库的所有键值对,在某个时间点将数据写入一个临时文件, 持久化结束后, 用这个临时文件替换上次持久化的文件, 达到数据恢复。
优点:
1、只有一个文件 dump.rdb, 方便持久化。
2、容灾性好, 一个文件可以保存到安全的磁盘。
3、性能最大化, fork 子进程来完成写操作, 让主进程继续处理命令, 所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能)
4.相对于数据集大时, 比 AOF 的启动效率更高。
缺点:
1、数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障, 会发生数据丢失。所以这种方式更适合数据要求不严谨的时候)
2、AOF (Append-only file)持久化方式: 是指所有的命令行记录以 redis 命令请求协议的格式完全持久化存储)保存为 aof 文件。
优点:
1、数据安全, aof 持久化可以配置 appendfsync 属性, 有 always, 每进行一次命令操作就记录到 aof 文件中一次。
2、通过 append 模式写文件, 即使中途服务器宕机, 可以通过 redis-check-aof 工具解决数据一致性问题。
3、AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前( 文件过大时会对命令进行合并重写), 可以删除其中的某些命令( 比如误操作的 flushall))
缺点:
1、AOF 文件比 RDB 文件大, 且恢复速度慢。
2、数据集大的时候, 比 rdb 启动效率低。
数据恢复
重启Redis时,rdbLoad函数就会被执行,它将读取RDB文件,并将RDB文件数据载入到内存中。但是我们很少用选择某一时间的RDB快照方式来恢复数据,因为这样会丢失很多数据,较好的选择就是通过AOF日志来恢复数据,但是回放AOF日志文件是相对很耗时的一个操作。我们可以采用手动RDB+AOF的方式恢复:选某一时间点的RDB文件恢复数据后,跟RDB最后生成时间通过运维工具去修改AOF日志文件,选择RDB文件时间之后的AOF指令保存为新的AOF文件回放(为了保证数据的完整性,一般会选择RDB时间前一点点)。到了Redis4.0它给运维同学带来了解放双手的混合持久化模式,它将RDB文件和增量的AOF日志存放在一起,这时候AOF不在是全量日志,而是自这次持久化开始到持久化结束时间发生的增量日志。这样就很大程度的提高了数据恢复的速度也减少了手工运维的烦恼。
结构组成
架构图
文件事件处理器
文件事件处理器是单线程模式运行的,但是通过IO多路复用机制监听多个socket,可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了redis内部的线程模型的简单性。
多个socket,
IO多路复用程序,
文件事件分派器,
事件处理器
命令处理器
命令回复器
连接应答器
数据结构
数据结构
启动操作
图
mongodb
数据结构
架构图
数据库-->collection-->record
MongoDB在数据存储上按命名空间来划分,一个collection是一个命名空间,一个索引也是一个命名空间。
同一个命名空间的数据被分成很多个Extent,Extent之间使用双向链表连接。
在每一个Extent中,保存了具体每一行的数据,这些数据也是通过双向链接连接的。
每一行数据存储空间不仅包括数据占用空间,还可能包含一部分附加空间,这使得在数据update变大后可以不移动位置。
索引以BTree结构实现。
如果你开启了jorunaling日志,那么还会有一些文件存储着你所有的操作记录。
特性
概述
所用语言:C++
特点:保留了SQL一些友好的特性(查询,索引)。
使用许可: AGPL(发起者: Apache)
协议: Custom, binary( BSON)
Master/slave复制(支持自动错误恢复,使用 sets 复制)
内建分片机制
支持 javascript表达式查询
可在服务器端执行任意的 javascript函数
update-in-place支持比CouchDB更好
在数据存储时采用内存到文件映射
对性能的关注超过对功能的要求
建议最好打开日志功能(参数 --journal)
在32位操作系统上,数据库大小限制在约2.5Gb
空数据库大约占 192Mb
采用 GridFS存储大数据或元数据(不是真正的文件系统)
2.MongoDB优点:
1)更高的写负载,MongoDB拥有更高的插入速度。
2)处理很大的规模的单表,当数据表太大的时候可以很容易的分割表。
3)高可用性,设置M-S不仅方便而且很快,MongoDB还可以快速、安全及自动化的实现节点 (数据中心)故障转移。
4)快速的查询,MongoDB支持二维空间索引,比如管道,因此可以快速及精确的从指定位置 获取数据。MongoDB在启动后会将数据库中的数据以文件映射的方式加载到内存中。如果内 存资源相当丰富的话,这将极大地提高数据库的查询速度。
5)非结构化数据的爆发增长,增加列在有些情况下可能锁定整个数据库,或者增加负载从而 导致性能下降,由于MongoDB的弱数据结构模式,添加1个新字段不会对旧表格有任何影响, 整个过程会非常快速。
3.MongoDB缺点:
1)不支持事务。
2)MongoDB占用空间过大 。
3)MongoDB没有成熟的维护工具。
4.MongoDB应用场景
1.)适用于实时的插入、更新与查询的需求,并具备应用程序实时数据存储所需的复制及高度伸缩性;
2) 非常适合文档化格式的存储及查询;
3.)高伸缩性的场景:MongoDB 非常适合由数十或者数百台服务器组成的数据库。
4.)对性能的关注超过对功能的要求。
通信方式
多线程方式,主线程监听新的连接,连接后,启动新的线程做数据的操作(IO切换)。
数据同步
图
上图是 MongoDB 采用 Replica Sets 模式的同步流程
红色箭头表示写操作写到 Primary 上,然后异步同步到多个 Secondary 上
蓝色箭头表示读操作可以从 Primary 或 Secondary 任意一个上读
各个 Primary 与 Secondary 之间一直保持心跳同步检测,用于判断 ReplicaSets 的状态
持久化存储
MMap方式把文件地址映射到内存的地址空间,直接操作内存地址空间就可以操作文件,不用再调用write,read操作,性能比较高。
mongodb调用mmap把磁盘中的数据映射到内存中的,所以必须有一个机制时刻的刷数据到硬盘才能保证可靠性,多久刷一次是与sync delay参数相关的。
journal(进行恢复用)是Mongodb中的redo log,而Oplog则是负责复制的binlog。
如果打开journal,那么即使断电也只会丢失100ms的数据,这对大多数应用来说都可以容忍了。从1.9.2+,mongodb都会默认打开journal功能,以确保数据安全。
而且journal的刷新时间是可以改变的,2-300ms的范围,使用 --journalCommitInterval 命令。
Oplog和数据刷新到磁盘的时间是60s,对于复制来说,不用等到oplog刷新磁盘,在内存中就可以直接复制到Sencondary节点。
可以把movechunk目录里的旧文件删除吗?
没问题,这些文件是在分片(shard)进行均衡操作(balancing)的时候产生的临时文件.一旦这些操作已经完成,相关的临时文件也应该被删除掉.但目前清理工作是需要手动的,所以请小心地考虑再释放这些文件的空间
如果块移动操作(movechunk)失败了,我需要手动清除部分转移的文档吗?
不需要,移动操作是一致(consistent)并且是确定性的(deterministic);一次失败后,移动操作会不断重试;当完成后,数据只会出现在新的分片里(shard).
分片
数据在什么时候才会扩展到多个分片里?
Mongodb分片是基于区域的,所以一个集合中的所有的对象都被存放在一个块中,只有当存在多余一个块的时候,才会有多个分片获取数据的选项,现在每个默认块的大小是64mb,所以至少64mb的空间才可以实施一个迁移。
如果在一个分片(shard)停止或者很慢的时候,我发起一个查询会怎样?
如果一个分片停止了,除非查询设置了“partial”选项,否则查询会返回一个错误,如果一个分片响应很慢,Mongodb则会等待他的响应
HA集群
用的比较多的是Replica Sets,采用选举算法,自动进行leader选举,在保证可用性的同时,可以做到强一致性要求。
当然对于大量的数据,mongodb也提供了数据的切分架构Sharding。
HBASE
HBase 特点
1) 存储容量大,一个表可以容纳上亿行,上百万列;
2.)可通过版本进行检索,能搜到所需的历史版本数据;
3.)负载高时,可通过简单的添加机器来实现水平切分扩展,跟Hadoop的无缝集成保障了其数据可靠性(HDFS)和海量数据分析的高性能(MapReduce);
4.)在第3点的基础上可有效避免单点故障的发生。
4.HBase 缺点
1. 基于Java语言实现及Hadoop架构意味着其API更适用于Java项目;
2. node开发环境下所需依赖项较多、配置麻烦(或不知如何配置,如持久化配置),缺乏文档;
3. 占用内存很大,且鉴于建立在为批量分析而优化的HDFS上,导致读取性能不高;
4. API相比其它 NoSql 的相对笨拙。
5.HBase 适用场景
bigtable类型的数据存储;
对数据有版本查询需求;
应对超大数据量要求扩展简单的需求。
https://mp.weixin.qq.com/s/annlDd_tcSKqvrpRAUxaRg
mysql
MySQL架构
MySQL与众不同主要体现在存储引擎的架构上,插件式的存储引擎架构将查询处理和其它的系统任务以及数据的存储提取相分离。
tu
连接层
主要完成一些类似连接处理、授权认证及相关安全方案;
引入线程池的概念,为通过认证安全连接的客户端提供线程;
可以实现基于SSL的安全连接,服务器也会为安全接入的客户端验证它所具有的操作权限。
服务层
主要完成大部分的核心服务功能, 包括查询解析、分析、优化、缓存、以及所有的内置函数;
所有跨存储引擎的功能也都在这一层实现,包括触发器、存储过程、视图等;
引擎层
存储引擎真正的负责了MySQL中数据的存储和提取;
服务器通过API与存储引擎进行通信;
不同的存储引擎具有的功能不同,我们可以根据实际需要进行选取;
存储层
主要是将数据存储在运行于该设备的文件系统之上,并完成与存储引擎的交互。
面试题
MySQL查询的具体流程?或一条SQL语句在MySQL中如何执行的?
流程图
流程
客户端请求
连接器(验证用户身份,给予权限)
查询缓存(存在缓存则直接返回,不存在则执行后续操作)
分析器(对SQL进行词法分析和语法分析操作)
优化器(主要对执行的sql优化选择最优的执行方案方法)
执行器(执行时会先看用户是否有执行权限,有才去使用这个引擎提供的接口)
去引擎层获取数据返回(如果开启查询缓存则会缓存查询结果)
存储引擎
存储引擎是MySQL的组件,用于处理不同表类型的SQL操作。一个数据库中多个表可以使用不同引擎以满足各种性能和实际需求。
查看存储引擎
查看支持的存储引擎
查看默认存储引擎
准确查看某个数据库中的某一表所使用的存储引擎
设置存储引擎
建表时指定存储引擎(默认的就是INNODB,不需要设置)
修改存储引擎
修改默认存储引擎,也可以在配置文件my.cnf中修改默认引擎
存储引擎对比
InnoDB支持事务;MyISAM不支持事务;
InnoDB支持外键;MyISAM不支持;对一个包含外键的InnoDB表转MyISAM会失败;
InnoDB是聚簇索引;MyISAM是非聚簇索引;
InnoDB不保存表的具体行数,执行 select count(*) from table 需要全表扫描;MyISAM用一个变量保存了整个表的行数,执行上述语句读出该变量即可,速度非常快;
InnoDB最小的锁粒度是行锁,适合高并发;MyISAM最小的锁粒度是表锁,不适合高并发;一个更新语句会锁住整张表,导致其他查询和更新都会被阻塞,因此并发访问受限。
InnoDB不仅缓存索引还缓存真实数据,对内存要求较高,内存大小对性能有决定性的影响;MyISAM只缓存索引,不缓存真实数据;
InnoDB表占用空间大;MyISAM表占用空间小;
InnoDB关注点在事务;MyISAM关注点在性能;
面试题
一张表,里面有ID自增主键,当insert了17条记录之后,删除了第15,16,17条记录,再把Mysql重启,再insert一条记录,这条记录的ID是18还是15 ?
如果表的引擎是MyISAM,那么是18。因为MyISAM表会把自增主键的最大ID 记录到数据文件中,重启MySQL自增主键的最大ID也不会丢失;
如果表的引擎是InnoDB,那么是15。因为InnoDB 表只是把自增主键的最大ID记录到内存中,所以重启数据库或对表进行OPTION操作,都会导致最大ID丢失。
哪个存储引擎执行 select count(*) 更快,为什么?
MyISAM更快,因为MyISAM内部维护了一个计数器,把表的总行数存储在磁盘上,可以直接调取;
InnoDB没有将表的总行数存储在磁盘上,需要全表扫描累加行数,所以数据表越大越耗时;InnoDB这样做的苦衷与其支持事务有关,由于多版本并发控制(MVCC)的原因,InnoDB表“应该返回多少行”是不确定的。
数据类型
整数类型
浮点数类型
字符串类型
日期类型
其他数据类型
面试题
char 和 varchar 的区别?
char 是固定长度;varchar 长度可变;
相同点
char(n),varchar(n)中的n都代表字符的个数;
超过char,varchar最大长度n的限制后,字符串会被截断;
不同点
char不论实际存储的字符数都会占用n个字符的空间,而varchar只会占用实际字符应该占用的字节空间加1(实际长度length,0<=length<255)或加2(length>255)。
能存储的最大空间限制不一样:char的存储上限为255字节。
char在存储时会截断尾部的空格,而varchar不会。
BLOB和TEXT有什么区别?
BLOB是一个二进制对象,可以容纳可变数量的数据。有四种类型的BLOB:TINYBLOB、BLOB、MEDIUMBLO和 LONGBLOB
TEXT是一个不区分大小写的BLOB。四种TEXT类型:TINYTEXT、TEXT、MEDIUMTEXT 和 LONGTEXT。
BLOB 保存二进制数据,TEXT 保存字符数据。
索引
索引介绍
MYSQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构,所以说索引的本质是:数据结构。
索引的目的在于提高查询效率,可以类比字典、 火车站的车次表、图书的目录等 。
简单理解为“排好序的快速查找的数据结构”。数据本身之外,数据库还维护着一个满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引。
索引本身也很大,不可能全部存储在内存中,一般以索引文件的形式存储在磁盘上
平常说的索引,没有特别指明的话,就是B+树(多路搜索树,不一定是二叉树)结构组织的索引。其中聚集索引,次要索引,覆盖索引,复合索引,前缀索引,唯一索引默认都是使用B+树索引,统称索引。此外还有哈希索引等。
图例说明
下图是一种可能的索引方式示例
左边的数据表,一共有两列七条记录,最左边的是数据记录的物理地址。
为了加快Col2的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值,和一个指向对应数据记录物理地址的指针,这样就可以运用二叉查找在一定的复杂度内获取到对应的数据,从而快速检索出符合条件的记录。
基本语法
创建
创建索引
修改表结构(添加索引)
删除
查看
常用的 alter 命令
ALTER TABLE tableName ADD PRIMARY KEY (column_list): 添加一个主键,这意味着索引值必须是唯一的,且不能为NULL。
ALTER TABLE tableName ADD UNIQUE indexName (column_list) 创建索引的值必须是唯一的(除了NULL外,NULL可能会出现多次)。
ALTER TABLE tableName ADD INDEX indexName (column_list) 添加普通索引,索引值可出现多次。
ALTER TABLE tableName ADD FULLTEXT indexName (column_list)指定了索引为 FULLTEXT ,用于全文索引。
索引分类
数据结构角度
B+树索引
Hash索引
Full-Text全文索引
R-Tree索引
物理存储角度
聚簇索引(clustered index)
非聚簇索引(non-clustered index),也叫 辅助索引(secondary index)
以上两个索引都是B+树结构
逻辑角度
主键索引:主键索引是一种特殊的唯一索引,不允许有空值;
普通索引/单列索引:每个索引只包含单个列,一个表可以有多个单列索引;
多列索引(复合索引、联合索引):复合索引指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用复合索引时遵循最左前缀集合;
唯一索引:限制值必须唯一,可以有一个NULL值;
空间索引:空间索引是对空间数据类型的字段建立的索引;
索引结构
首先要明白索引(index)是在存储引擎(storage engine)层面实现的,而不是server层面。不是所有的存储引擎都支持所有的索引类型。即使多个存储引擎支持某一索引类型,它们的实现和行为也可能有所差别。
B+Tree索引
认识 B-Tree (B树)
3阶B树示例
每个节点最多有m个孩子;
除根节点和叶子节点外,其他每个节点至少有Ceil(m/2)个孩子;
若根节点不是子节点,则至少有2个孩子;
所有叶子节点都在同一层,且不包含其他关键字信息;
每个非终端节点包含n个关键字信息,m/2 <= n <= m,n=孩子个数-1;
每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。
看图好理解
认识B+Tree(B+树)
B-Tree结构图中可以看到每个节点中不仅包含数据的key值,还有data值。而每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度。
4阶B+树示例
有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。
除根节点和子节点外,每个节点至少有 (m+1)/2个孩子。
所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。
B+Tree 与 B-Tree 的几点不同
非叶子节点只存储键值信息;
所有叶子节点之间都有一个链指针;
数据记录都存放在叶子节点中;
B+Tree的优势(相对B-Tree)
单一节点存储更多的元素,使得查询的IO次数更少。
所有查询都要查找到叶子节点,查询性能稳定。
所有叶子节点形成有序链表,便于范围查询。
MyISAM 和 InnoDB 引擎索引都采用B+索引
MyISAM 主键索引与辅助索引
MyISAM索引图示(索引和数据存放位置分离)
MyISAM引擎的索引文件和数据文件是分离的。这样的索引称为"非聚簇索引"。
MyISAM引擎索引结构的叶子节点的数据域,存放的并不是实际的数据记录,而是数据记录的地址。
MyISAM的主索引与辅助索引区别并不大,只是主键索引不能有重复的关键字。
InnoDB主键索引与辅助索引
InnoDB主键索引图示
InnoDB辅助(非主键)索引图示
辅助索引检索过程(以上图name字段为例)
先在辅助索引上检索name,到达其叶子节点获取对应的主键;
②再使用主键在主索引上再进行对应的检索操作。
InnoDB的数据文件就是主键索引文件(索引和数据存放在一起),这种索引被称为“聚簇索引”,一个表只能有一个聚簇索引。
InnoDB引擎索引结构的叶子节点的数据域,存放的就是实际的数据记录(对于主索引,此处会存放表中所有的数据记录;对于辅助索引此处会引用主键,检索的时候通过主键到主键索引中找到对应数据行)。
Hash索引
主要就是通过Hash算法,将数据库字段数据转换成定长的Hash值,与这条数据的行指针一并存入Hash表的对应位置;如果发生Hash碰撞(两个不同关键字的Hash值相同),则在对应Hash键下以链表形式存储。
检索算法:在检索查询时,就再次对待查关键字再次执行相同的Hash算法,得到Hash值,到对应Hash表对应位置取出数据即可,如果发生Hash碰撞,则需要在取值时进行筛选。
MySQL目前有Memory引擎和NDB引擎支持Hash索引。
Full-Text全文索引
全文索引是MyISAM的一种特殊索引类型,主要用于全文索引;InnoDB从MYSQL5.6版本提供对全文索引的支持。
它用于替代效率较低的LIKE模糊匹配操作,而且可以通过多字段组合的全文索引一次性全模糊匹配多个字段。
同样使用B-Tree存放索引数据,但使用的是特定的算法,将字段数据分割后再进行索引(一般每4个字节一次分割),索引文件存储的是分割前的索引字符串集合,与分割后的索引信息,对应Btree结构的节点存储的是分割后的词信息以及它在分割前的索引字符串集合中的位置。
R-Tree空间索引
空间索引是MyISAM的一种特殊索引类型,主要用于地理空间数据类型。
MySQL高效索引
覆盖索引(Covering Index)
select 的数据列只用从索引中就能够获取到,不必根据索引再次读取数据文件。查询列要被所建的索引覆盖。
判断标准
使用explain,可以通过输出的extra列来判断,对于一个索引覆盖查询,显示为 using index。
面试题
为什么要用索引?
通过唯一性索引可确保数据的唯一性。
加快数据的检索速度(大大减少检索的数据量),这是建索引最主要的原因。
加快表之间的连接。
在使用分组和排序字句进行数据检索的时候,可以减少分组和排序时间。
可以在查询过程中,使用优化隐藏器,提供系统的性能。
创建索引有哪些缺点?
创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加;
索引需要占物理空间;
当对表的数据进行增删改的时候,索引也要动态的维护,这样降低了数据的维护速度;
数据库索引的原理,为什么要用 B+树,为什么不用二叉树?
当从算法逻辑上讲,二叉树的查找速度和比较次数是最小的;
但是由于数据库索引是存储在磁盘上的,所以必须考虑磁盘IO的问题,磁盘IO是比较耗时的操作;
当数据量比较大的时候,索引的大小可能有几个G,是不可能全部加载到内存中的;
做法是逐一加载每一个磁盘页,这里的磁盘页对应着索引树的节点;
索引树的高度(层级)就是需要的磁盘IO次数;
在相同数据量的情况下,B+树的高度是小于二叉树的,数据量越大差距越明显。
聚簇索引和非聚簇索引的区别?
聚簇索引(InnoDB特有,Myisam没有)
聚簇索引具有唯一性:将数据跟索引结构放到一块,因此一个表仅有一个聚簇索引。
误区:把主键自动设置为聚簇索引
聚簇索引默认是主键,如果表中没有定义主键,InnoDB会选择一个唯一的非空索引代替。如果没有这样的索引,InnoDB会隐式定义一个主键来昨晚聚簇索引。如果已经设置了主键为聚簇索引,必须先删除主键,然后添加想要的聚簇索引,最后恢复设置主键即可。
优点
聚簇索引的查询速度非常的快,因为整个B+树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。
缺点
依赖于有序的数据,不是有序的数据插入或查找的速度比较慢;
更新代价大;
非聚簇索引
优点
更新代价比聚簇索引小;
缺点
聚簇索引与非聚簇索引 检索过程对比
案例数据表
检索过程图
聚簇索引
InnoDB使用的是聚簇索引,将主键组织到一颗B+树中,而行数据存储在叶子节点上,若使用“where id=7” 这样的条件查找主键,则按B+树的检索算法即可查找到对应的节点,直接获得行数据。
若对Name列进行条件搜索,则需要两个步骤:1、在辅助索引B+树中检索Name,到达其他叶子节点获取对应的主键。2、使用主键在主索引B+树再执行一次B+树检索操作,最终到达叶子节点即可获得整行数据。
非聚簇索引
MyISAM使用的是非聚簇索引,非聚簇索引的两颗B+树看上去没什么不同,节点的结构完全一致,只是存储的内容不同;
主键索引B+树的节点存储了主键,辅助索引B+树存储了辅助键;
表数据存储在独立的地方,这两颗B+树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别;
由于索引树是独立的,通过辅助键检索无需访问主键的索引树。
使用场景
为什么推荐使用整型自增主键而不是选择UUID?
UUID是字符串,比整型消耗更多的存储空间;
在B+树中进行查找时需要跟经过节点值比较大小,整型数据的比较运算比字符串更快速;
自增的整型索引在磁盘中会连续存储,在读取一页数据时也是连续;UUID是随机产生的,读取的上下两行数据存储是分散的,不适合执行按范围条件(如:where id>5 and id<10)查询语句。
在插入或删除数据时,整型自增主键会在叶子结点的末尾建立新的叶子节点,不会破坏左侧子树的结构;UUID主键很容易出现这样的情况:B+树为了维持自身的特性,有可能会进行结构的重构,消耗更多的时间。
为什么InnoDB非主键索引结构叶子节点存储的是主键值?
保证数据一致性和节省存储空间。可以这么理解:商城系统订单表会存储一个用户ID作为关联外键,而不推荐存储完整的用户信息,因为当我们用户表中的信息(真实名称、手机号、收货地址···)修改后,不需要再次维护订单表的用户数据,同时也节省了存储空间。
为什么Mysql索引要用B+树不是B树?
用B+树不用B树考虑的是IO对性能的影响,B树的每个节点都存储数据,而B+树只有叶子节点才存储数据,所以查找相同数据量的情况下,B树的高度更高,IO更频繁。
数据库索引是存储在磁盘上的,当数据量大时,就不能把整个索引全部加载到内存了,只能逐一加载每一个磁盘页(对应索引树的节点)。其中在MySQL底层对B+树进行进一步优化:在叶子节点中是双向链表,且在链表的头结点和尾节点也是循环指向的。
为何不采用Hash方式?
因为Hash索引底层是哈希表,哈希表是一种以key-value存储数据的结构,多个数据在存储关系上是完全没有任何顺序关系的;
所以,对于区间查询是无法直接通过索引查询的,就需要全表扫描。
哈希索引只适用于等值查询的场景。
而B+ Tree是一种多路平衡查询树,他的节点是天然有序的(左子节点小于父节点、父节点小于右子节点),所以对于范围查询的时候不需要做全表扫描。
哈希索引不支持多列联合索引的最左匹配规则,如果有大量重复键值得情况下,哈希索引的效率会很低,因为存在哈希碰撞问题。
哪些情况需要创建索引?
主键自动建立唯一索引;
频繁昨晚查询条件的字段;
查询中与其他表关联的字段,外键关系建立索引;
单键/组合索引的选择问题,高并发下倾向创建组合索引;
查询中排序的字段,排序字段通过索引访问大幅度提高排序速度;
查询中统计或分组字段;
哪些情况不需要创建索引?
表记录太少;
经常增删改的表;
数据重复且分布均匀的表字段;
频繁更新的字段不适合创建索引(会加重IO负担);
where 条件里用不到的字段不创建索引;
查询
SQL执行顺序
手写
机读
面试题
count(*) 、count(1) 和 count(列名) 的区别?
执行效果对比
count(*) 包括了所有列,相当于行数,在统计结果的时候,不会忽略列值为NULL;
count(1) 包括了所有列,用1代表代码行,在统计结果的时候,不会忽略列值为NULL;
count(列名) 只包括列名那一列,在统计结果的时候,会忽略列值为NULL的计数;
执行效率对比
列名为主键,count(列名) 效率最优;
如果表只有一个字段(不为主键),则count(*) 效率最优;
列名不为主键,count(1) 比 count(列名)快;
如果表有多个列并且没有主键,则count(1) 比count(*)快;
MySQL中 in 和 exists 的区别?
exists:对外表用loop逐条查询,每次查询都会查看exists的条件语句,当exists里的条件语句能够返回记录时,条件就为真,返回当前loop到的这条记录;反之,如果exists里的条件语句不能返回记录,则当前loop到的这条记录被丢弃;exists的条件就像一个 bool 条件,当能返回结果集则 为true,不能返回结果集则为false。
in:in查询相当于多个or条件的叠加。
如果查询的两个表大小相当,那么用 in 和 exists 差别不大;
如果两个表一个较小,另一个是大表,则子查询表大的用exists,子查询表小的用 in;
UNION 和 UNION ALL的区别?
两者都是将结果集合并为一个,两个要联合的SQL语句,字段个数必须一样,而且字段类型要“相容”(一致);
UNION 在进行表连接后会去掉重复的数据记录(效率较低),而UNION ALL则不会去掉重复的数据记录;
UNION会按照字段的顺序进行排序,而UNION ALL只是简单的将两个结果合并就返回;
MySQL的两种连接:内连接、外连接?
内连接(inner join)
交叉连接(cross join)
交叉连接会把第一张表的每个值和第二张表的每个值进行匹配,结果如下:
相等连接
两个表有一共同列,且相等
不等连接
两个表有一共同列,且不相等
自然连接
自然连接只有在两张表中有相同的列(列的名称都相同)时才会有用,自然连接就是自动识别相同列的相等连接;
外连接(out join)
左连接(左外连接)
右连接(右外连接)
全外连接
语法如下
事务
事务的基本要素(ACID)
原子性(Atomicity)
一致性(Consistency)
隔离性(Isolation)
持久性(Durability)
并发事务代来的问题
更新丢失(Lost Update)
脏读(Dirty Reads)
不可重复读(Non-Repeatable Reads)
幻读(Phantom Reads)
事务隔离级别 读未 读已 可重 可串
Read-Uncommitted(读未提交)
Read-Committed(读已提交)
Repeatable-read(可重复读)
Serializable(可串行化)
查看当前数据库隔离级别
show variables like 'tx_isolation'; 或者 select @@tx_isolation;
show variables like 'transaction_isolation'; 或者 select @@transaction_isolation;
MVCC(多版本并发控制)
MVCC是行级锁的一个变种,但它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只是锁定必要的行。MVCC的实现是通过保存数据在某个时间的快照来实现的。不管需要执行多长时间,每个事务看到的数据都是一致的。
MVCC只在Committed Read 和 Repeatable Read 两种隔离级别下工作。
MVCC实现原理
InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现。一列保存了行的创建时间,一列保存行的过期时间(或删除时间)。存储的并不是实际的时间值,而是系统版本号。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来查询到每行的版本号进行比较。
Repeatable Read 隔离级别下,MVCC工作方式
select
InnoDB只查找创建版本号小于或等于当前事务版本号的数据行,这样可以确保事务读取的行,要么是在开始事务之前已经存在,要么是事务自身插入或修改过的。
过期版本号要么未定义,要么大于当前事务版本号,这样可以确保事务读取到的行在事务开始之前未被删除。
insert
delete
update
优缺点
解决不可重复读和幻读的问题;大多数操作都不用加锁,使数据操作简单,性能好。
缺点:每行记录都需要额外的存储空间,需要做更多的行检查工作和一些额外的维护工作。
事务日志
InnoDB使用日志来减少提交事务时的开销。因为日志中已经记录了事务,就无需在每个事物提交时把缓冲池的脏块刷新到磁盘中。
事务修改的数据和索引通常会映射到表空间的随机位置,所以刷新这些变更到磁盘需要很多随机IO。随机IO比顺序IO昂贵得多,因为一个IO请求需要时间把磁头移到正确的位置,然后等待磁盘上读出需要的部分,再转到开始位置。
InnoDB用日志把随机IO变成顺序IO。一旦日志安全写到磁盘,事务就持久化了,即使断电了,InnoDB重启后可以通过redo log恢复以及提交的事务。
InnoDB使用一个后台线程智能地刷新这些变更到数据文件。这个线程可以批量组合写入,使得数据写入更顺序,以提高效率。
事务的实现
事务的实现是基于数据库的存储引擎。不同的存储引擎对事务的支持程度不一样。MySQL 中支持事务的存储引擎有 InnoDB 和 NDB。
事务的实现就是实现ACID特性。事务隔离性是通过锁实现,而事务的原子性、一致性和持久性则通过事务日志实现。
事务日志包括:redo log(重做日志)和undo log(回滚日志)
redo log(重做日志)实现持久化和原子性
在InnoDB存储引擎中,事务日志通过redo log 和日志缓存(InnoDB Log Buffer)实现。
事务开启时,事务中的操作,都会先写入存储引擎的日志缓存中,在事务提交之前,这些缓存的日志都需要提前刷新到磁盘上持久化,这就是“日志先行”(Write-Ahead logging).
当事务提交之后,在Buffer Pool中影射的数据文件才会慢慢刷新到磁盘。此时如果宕机,那么当系统重启进行恢复时,可以根据redo log中记录的日志,把数据库恢复到奔溃前的一个状态。未完成的事务,可以继续提交或者选择回滚,这基于恢复的策略而定。
在系统启动的时候,就已经为redo log分配了一块连续的存储空间,以顺序追加的方式记录redo log,通过顺序IO改善性能。所有事务共享redo log的存储空间,它们的redo log按语句的执行顺序,一次交替的记录在一起。
redo log 记录示例
undo log(回滚日志) 实现一致性
undo log主要为事务的回滚服务。
undo log记录了数据在某个操作前的状态,如果事务执行过程中需要回滚,就可以根据undo log进行回滚操作。
undo log记录的是已部分完成并写入磁盘的未完成(未提交)的事务。
单个事务的回滚,不会影响到其他事务做的操作。
redo log 与 undo log 的区别
两种日志都是为了恢复操作。
redo log是恢复提交事务修改的页操作。而undo log是回滚行记录到特定版本。
两者记录的内容也不同,redo log是物理日志,记录页的物理修改操作。而undo log是逻辑日志,根据每行记录继续记录。
redo + undo log简化过程
假设操作示例
在1-8的任意一步系统宕机,事务未提交,该事务就不会对磁盘上的数据做任何影响。如果在8-9之间宕机,恢复之后可以选择回滚,也可以选择继续完成事务提交,因为此时redo log已经持久化。若在9之后系统宕机,内存映射中变更的数据还来不及刷回磁盘,那么系统恢复之后,可以根据redo log把数据刷回磁盘。
面试题
事务的隔离级别有哪些?MySQL默认隔离级别是什么?
MySQL事务的四大特性及实现原理?
MVCC是什么,它的底层原理是?
什么是幻读、脏读、不可重复读?
不可重复读与幻读的区别?
不可重复读的重点是修改
幻读的重点在于新增或删除
并发事务处理带来的问题及解决办法
更新丢失
通常是应该完全避免的。但防止更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决,因此防止更新丢失应该是应用的责任。
脏读、不可重复读、幻读 都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决
一种是加锁
另一种是多版本并发控制
MySQL有多少种日志?
错误日志
查询日志
慢查询日志
二进制日志
中继日志
事务日志
锁机制
锁是计算机协调多个进程或线程并发访问某一资源的机制。
数据库锁机制简单说,就是数据库为了保证数据的一致性,而使各种共享资源在被并发访问变得有序所设计的一种规则。
锁定分类
对数据操作的类型分类
读锁(共享锁)
写锁(排他锁)
对数据操作的粒度分类
表级锁
行级锁
页面锁
适用:从锁的角度来说,表锁适合已查询为主,只有少量按索引条件更新数据的应用。行锁适合有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用。
MyISAM表锁
表锁两种模式
表共享读锁
表独占写锁
MyISAM表的读操作与写操作之间,以及写操作之间是串行的。
默认情况下,写锁比读锁具有更高的优先级:当一个锁释放时,这个锁会优先给写锁队列中等候的获取锁请求,然后再给读锁队列的获取锁请求。
InnoDB行锁
InnoDB实现了2种行锁类型
共享锁(S)
排他锁(X)
为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(都是表锁)
意向共享锁(IS)
意向排它锁(IX)
索引失效hi导致行锁变表锁。比如:varchar 查询不写单引号的情况。
加锁机制
乐观锁与悲观锁
乐观锁
悲观锁
InnoDB的三种行锁
记录锁(Record Locks)
SELECT * FROM table WHERE id = 1 FOR UPDATE;
UPDATE SET age = 50 WHERE id = 1; -- id 列为主键或唯一索引列
间隙锁(Gap Locks)
间隙锁基于非唯一索引,使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。
SELECT * FROM table WHERE id BETWEN 1 AND 10 FOR UPDATE;
Gap锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。
临键锁(Next-key Locks)
可以理解为一种特殊的间隙锁,通过临键锁可以解决幻读的问题。每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据的临键锁时,会锁住一段左开右闭区间的数据。InnoDB中行级锁是基于索引实现的,临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁。
死锁
死锁产生
死锁是指两个或多个事务在同一资源上互相占用,并请求锁定占用的资源,从而导致恶性循环。
当事务试图以不同的顺序锁定资源时,就可能产生死锁。多个事务同时锁定同一个资源时也可能回发生死锁。
锁的行为和顺序和存储引擎相关。以同样的顺序执行语句,有些存储引擎会产生死锁,有些不会。
死锁有两个原因:真正数据冲突;存储引擎的实现方式;
检测死锁
数据库系统实现了各种死锁检测和死锁超时的机制,InnoDB存储引擎能检测到死锁的循环依赖并立即返回一个错误。
死锁恢复
死锁发生后,只有部分或完全回滚其中一个事务,才能打破死锁。InnoDB目前处理死锁的方法是:将持有最少行级排它锁的事务进行回滚。
外部锁的死锁检测
发生死锁后,InnoDB一般都能自动检测到,并使一个事务释放锁并回退,另一个事务获得锁,继续完成事务。但在涉及外部锁或涉及表锁的情况下,InnoDB并不能完全自动检测到死锁,这需要通过设置锁等待超时参数innodb_lock_wait_timeout来解决。
死锁影响性能
死锁会影响性能而不是产生严重错误,因为InnoDB会自动检测并处理死锁。在高并发系统上,当许多线程等待同一个锁时,死锁检测可能导致速度变慢。有时当发生死锁时,禁用死锁检测(innodb_deadlock_detect配置选项)可能会更有效,这时可以依赖 innodb_lock_wait_timeout 设置进行事务回滚。
MyISAM避免死锁
在自动加锁的情况下,MyISAM总是一次获得SQL语句所需要的全部锁,所以MyISAM表不会出现死锁。
InnoDB避免死锁
使用 select ... for update 语句获取必要的锁;
直接申请足够级别的锁;
约定多个表的访问顺序;
改变事务隔离级别
面试题
数据库的乐观锁和悲观锁?
MySQL 中有哪几种锁,列举一下?
MySQL中InnoDB引擎的行锁是怎么实现的?
MySQL 间隙锁有没有了解,死锁有没有了解,写一段会造成死锁的 sql 语句,死锁发生了如何解决,MySQL 有没有提供什么机制去解决死锁?
select for update有什么含义,会锁表还是锁行还是其他?
for update 仅适用于InnoDB,且必须在事务块(begin/commit)中才能生效。
在进行事务操作时,通过 for update 语句,MySQL会对查询结果集中每行数据都添加排他锁(包含 行锁、表锁),其他线程对该记录的更新与删除操作都会阻塞。
只有通过索引条件检索数据,InnoDB才使用行级锁。否则InnoDB将使用表锁。
如何在MySQL分析死锁产生的原因?
如果出现死锁,可以用 show engine innodb status; 命令来确定最后一个死锁产生的原因。返回结果包括:死锁相关事务详细信息,如:引发死锁的SQL语句,事务已经获得的锁,正在等待什么锁,已及被回滚的事务等。
MySQL调优
影响MySQL性能的因素
业务需求(适合度)
存储定位
不适合放进MySQL的数据
二进制多媒体数据
流水队列数据
超大文本数据
需要放进缓存的数据
系统各种配置及规则
活跃用户的基本信息
活跃用户的个性化定制信息
准实时的统计信息
其他一些访问频繁但变更较少的数据
Schema设计对系统的性能影响
尽量减少对数据库访问的请求
尽量减少无用数据的查询请求
硬件环境对系统性能的影响
性能分析
MySQL常见瓶颈
CPU:CPU在饱和的时候,一般发生在数据装入内存或从磁盘读取数据。
IO:磁盘IO瓶颈发生在装入数据远大于内存容量的时候。
服务器硬件性能:top、free、iostat 和 vmstat 查看系统性能状态。
性能下降、SQL慢、执行时间长、等待时间长,原因分析
查询语句写的太烂;
索引失效(单值、复合);
关联查询太多join(设计缺陷或不得已的需求);
服务器调优及各个参数设置(缓冲、线程数等);
常见性能分析手段
慢查询日志
MySQL提供的一种日志记录,用于记录MySQL中响应时间超过阈值的语句,具体指运行时间超过 long_query_time 值的收起来,会被记录到慢查询日志中。
long_query_time 的默认值为10,运行10秒以上的语句被记录。
默认情况下,MySQL数据库没有开启慢查询日志,需要手动设置参数开启。
查看开启状态
开启慢查询日志
临时配置
永久配置
日志分析工具 mysqldumpslow
得到返回记录集最多的10个SQL
得到访问次数最多的10个SQL
得到按照实际排序的前10条里面含有左连接的查询语句
Explain(执行计划)
是什么?
使用Explain 关键字可以模拟优化器执行SQL查询语句,从而知道MySQL是如何处理你的SQL语句的。分析你的查询语句或表结构的性能问题。
能干什么?
表的读取顺序
数据读取操作的操作类型
哪些索引可以使用
哪些索引被实际使用
表之间的引用
每张表有多少行被优化器查询
怎么用?
Explain + SQL语句, 得到以下信息
Explain返回的各字段解析
id
id相同,执行顺序从上往下;
id全不同,如果是子查询,id的序号会递增,id值越大优先级越高;
id部分相同,执行顺序是按照数字大的先执行,然后数字相同的安装从上往下的顺序执行;
select_type
SIMPLE:简单的select查询,不含子查询或union;
PRIMARY:查询中若包含复杂的子部分,最外层被标记为PRIMARY;
SUBQUERY:在select或where列表中包含了子查询;
DERIVED:在from列表中包含的资产性被标记为derived,mysql会递归执行这些子查询,把结果放在临时表里;
UNION:若第二个select出现在union之后,则被标记为union,若union包含在from子句的子查询中,外层select将被标记为derived;
UNION RESULT:从union表获取结果的select;
table
type
system:表只有一行记录(等于系统表),是const类型的特例,平时不会出现。
const:表示通过索引一次就找到了,const用于比较primary key或unique索引,因为只要匹配一行数据,所以很快,如将主键置于where列表中,MySQL就能将该查询转换为一个常量。
eq_ref:唯一索引扫描,对于每个索引键,表中只有一条记录与之匹配,常见于主键或唯一索引扫描。
ref:非唯一索引扫描,范围匹配某个单独值得所有行。本质上也是一种索引访问,它返回所有匹配某个单独值的行。
range:只检索给定返回的行,使用一个索引来选择行。key列显示使用了哪个索引。一般是在where语句中出现 between、<、>、in等的查询。
index:full index scan,index与ALL的区别为index类型值遍历索引树,通常比ALL快,因为索引文件通常比数据文件小。(index 和 all 都是全表扫描,但index是从索引中读取,all是从磁盘中读取。)
ALL:Full table scan,将遍历全表找到匹配的行。
possible_keys
key
key_len
表示索引中使用的字节数,可通过该列计算计算查询中使用的索引长度。在不损失精确性的情况下,长度越短越好。
key_len显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len是根据表定义计算而得,不是通过表内检索出的。
ref
rows
Extra
using filesort: 说明mysql会对数据使用一个外部的索引排序,不是按照表内的索引顺序进行读取。mysql中无法利用索引完成的排序操作称为“文件排序”。常见于order by和group by语句中
Using temporary:使用了临时表保存中间结果,mysql在对查询结果排序时使用临时表。常见于排序order by和分组查询group by。
using index:表示相应的select操作中使用了覆盖索引,避免访问了表的数据行,效率不错,如果同时出现using where,表明索引被用来执行索引键值的查找;否则索引被用来读取数据而非执行查找操作
using where:使用了where过滤
using join buffer:使用了连接缓存
impossible where:where子句的值总是false,不能用来获取任何元祖
select tables optimized away:在没有group by子句的情况下,基于索引优化操作或对于MyISAM存储引擎优化COUNT(*)操作,不必等到执行阶段再进行计算,查询执行计划生成的阶段即完成优化
distinct:优化distinct操作,在找到第一匹配的元祖后即停止找同样值的动作
示例
示例图
第一行(执行顺序4):id列为1,表示是union里的第一个select,select_type列的primary表示该查询为外层查询,table列被标记为,表示查询结果来自一个衍生表,其中derived3中3代表该查询衍生自第三个select查询,即id为3的select。【select d1.name......】
第二行(执行顺序2):id为3,是整个查询中第三个select的一部分。因查询包含在from中,所以为derived。【select id,name from t1 where other_column=''】
第三行(执行顺序3):select列表中的子查询select_type为subquery,为整个查询中的第二个select。【select id from t3】
第四行(执行顺序1):select_type为union,说明第四个select是union里的第二个select,最先执行【select name,id from t2】
第五行(执行顺序5):代表从union的临时表中读取行的阶段,table列的<union1,4>表示用第一个和第四个select的结果进行union操作。【两个结果union操作】
show profile分析查询
MySQL 提供可以用来分析当前会话中语句执行的资源消耗情况。
默认情况下,参数处于关闭状态,并保存最近15次的运行结果。
性能优化
索引优化
全值(等值)匹配 是最优选择;
最佳左前缀法则,如联合索引(a,b,c),可利用的索引就有(a), (a,b), (a,b,c);
不在索引列上做任何操作(计算、函数、类型转换),会导致索引失效而转向全表扫描;
存储引擎不能使用索引中范围条件右边的列;
尽量使用覆盖索引,减少select;
is null ,is not null 也无法使用索引;
like "xxx%" 是可以用到索引的,like以通配符开头('%abc'或'%abc%')索引失效会变成全表扫描的操作;
字符串不加单引号索引失效;
少用or,用它来连接时会索引失效;
<、<=、>、>=、BETWEEN、IN 可用到索引,<>、not in、!= 则不行,会导致全表扫描;
查询优化
永远小表驱动大表
in 和 exists 的选择
order by 关键字优化
MySQL支持两种方式的排序:Index和FileSort;index 效果高(扫描索引本身完成排序),FileSort效率较低。
order by 子句尽量使用 Index方式排序,避免使用FileSort方式排序;
order by 语句使用索引最左前列;
使用where子句与order by子句条件组合满足索引最左前列;
filesort方式排序有两种算法
双路排序:MySQL4.1之前是使用该方式,两次扫描磁盘,最终得到数据。
单路排序:从磁盘读取查询需要的所有列,按照order by列在buffer对它们进行排序,然后扫描排序后的列表进行输出,效率高于双路排序。
优化策略
增大 sort_buffer_size 参数的设置;
增大 max_length_for_sort_data 参数的设置;
group by 关键字优化
group by 实际是先排序后进行分组,遵照索引建的最佳左前缀;
当无法使用索引列,增大 max_length_for_sort_data和sort_buffer_size参数的设置;
where 高于having,能在where限定的条件就不要去having限定了;
数据类型优化
更小的通常更好
简单就好
尽量避免NULL
面试题
日常工作中你是怎么优化SQL的?
SQL优化的一般步骤是什么,怎么看执行计划(explain),如何理解其中各个字段的含义?
如何写sql能够有效的使用到复合索引?
一条sql执行过长的时间,你如何优化,从哪些方面入手?
什么是最左前缀原则?什么是最左匹配原则?
查询中哪些情况不会使用索引?
分区、分表、分库
MySQL分区
是什么?
一般情况下,创建的表对应一组存储文件,使用MyISAM存储引擎时是一个 .MYI 和 .MYD 文件,使用InnoDB存储引擎时是一个 .ibd 和 .frm(表结构)文件。
当数据量较大时(千万条级别以上),MySQL性能会开始下降,这时就需要将数据分散到多组存储文件,保证单个文件的执行效率。
能做什么?
逻辑数据分割;
提高单一文件的写和读应用速度;
提高分区范围读查询的速度;
分割数据能够有多个不同的物理文件路径;
高效的保存历史数据;
怎么操作?
查看当前数据库是否支持分区
SHOW VARIABLES LIKE '%partition%'; -- 5.6及之前版本
show plugins; -- 5.6版本
分区类型
Range 分区
List 分区
Hash 分区
Key 分区
MySQL分表
垂直拆分
垂直分表,通常是按照业务功能的使用频次,把主要的、热门的字段放在一起作为主表。把不常用的,按照各自的业务属性进行聚集,拆分到不同的次要表中。主表和次要表的关系一般都是一对一的。
水平拆分(数据分片)
单表的容量不超过500W,否则建议水平拆分。是把一个表复制成同样表结构的不同表,然后把数据按照一定的规则划分,分别存储到这些表中,从而保证单表的容量不会太大,提升性能;当然这些结构一样的表,可以放在一个或多个数据库中。
水平分割的几种方法
使用MD5哈希
按时间划分
按热度划分
按ID值划分
MySQL分库
为什么要分库?
数据库集群环境后,都是多台slave,基本满足了读取操作;但是写入或大数据、频繁的写入操作,对master性能影响就比较大,这个时候单库并不能解决大规模并发写入的问题,需要考虑分库。
分库是什么?
一个库里表太多了,导致了海量数据,系统性能下降,把原本存储于一个库的表拆分存储到多个库上,通常是将表按照功能模块、关系密切程度划分出来,部署到不同的库上。
优点
减少增量数据写入时的锁,对查询的影响;
由于单表数量下降,常见的查询操作由于减少了需要扫描的记录,使得单表单词查询所需的检索行数变少,减少磁盘IO,延时变短;
分库分表后的难题?
分布式事务的问题,数据完整性和一致性的问题;
数据操作维度问题:用户、交易、订单各个不同的维度;
跨库联合查询的问题,可能需要两次查询跨节点的count、order by、 group by 以及聚合函数问题。可能需要分别在各个节点上得到结果后,在应用程序端进行合并处理,增加额外的数据管理负担;
面试题
为什么大部分互联网选择自己分库分表,而不选择分区表呢?
分区表,分区键设计不太灵活,如果不走分区键,很容易出现全表锁;
一旦数据并发量上来,如果在分区表实施关联,就是一个灾难;
自己分库分表,自己掌控业务场景和访问模式,可控。分区表不太可控;
随着业务的发展,数据量越来越多,高并发读写操作超过单个数据库服务器的处理能力怎么办?
采用数据分片,数据分片指按照某个维度,将存放在单一数据库中的数据分散地存放至多个数据库或表中。
数据分片的有效手段就是对关系型数据库进行分库和分表。
区别于分区的是,分区一般都是放在单机里,用的比较多的是时间范围分区,方便归档。分库分表需要代码实现,分区则是MySQL内部实现。分库分表和分区并不冲突,可以结合使用。
主从复制
复制的基本原理
slave 会从 master 读取binlog 来进行数据同步;
三个步骤
1. master 将改变记录到二进制日志(binary log)。这些记录过程叫做:二进制日志事件(binary log events);
salve 将 master 的binary log events 拷贝到它的中继器日志(relay log);
slave 重做中继日志中的事件,将改变应用到自己的数据库中。MySQL复制是异步且串行化的。
原理图
复制的基本原则
每个slave 只有一个master;
每个 slave 只能有一个唯一的服务器ID;
每个 master 可以有多个slave;
复制的最大问题
延时。
其他
数据库三大范式
第一范式(确保表中每列保持原子性)
数据库表中的字段都是单一属性的,不可再分;
这个单一属性有基本类型过程,包括:整型、字符型、逻辑型、日期等;
例如 “地址” 这个属性,如果业务上还可以细分,如拆分为省市区等,那就必须细分,否则不符合第一范式;
第二范式(确保表中每列都和主键列相关)
数据库表中不存在非关键字段对任一候选关键字的部分函数依赖,即所有非关键字段都完全依赖于任意一组候选关键字;
部分函数依赖指的是存在组合关键字中某些字段觉得非关键字的情况;
第三范式(确保表中每列都和主键列直接相关,而不是间接相关/传递依赖)
在第二范式的基础上,数据表中如果不存在非关键字段对任一候选关键字段的传递函数依赖则符合第三范式;
传递函数依赖,指的是如果存在“A->B->C”的决定关系,则C传递函数依赖A。
满足第三范式的数据库表不应存在传递依赖关系:关键字段 -> 非关键字段X -> 非关键字段Y;
关系型数据库与非关系型数据库的区别?
关系型数据库最典型的数据结构是表,由二维表及其之间的联系所组成的一个数据组织;
菲关系型数据库严格上不上一种数据库,应该是一种数据结构化存储方法的集合,可以是文档或者键值对等;
消息队列
理论
WHY
应用场景
缓冲和削峰 :
上游数据时有突发流量,下游可能扛不住,或者下游没有足够多的机器来保证冗余,kafka在中间可以起到一个缓冲的作用,把消息暂存在kafka中,下游服务就可以按照自己的节奏进行慢慢处理。
解耦和扩展性 :
项目开始的时候,并不能确定具体需求。消息队列可以作为一个接口层,解耦重要的业务流程。只需要遵守约定,针对数据编程即可获取扩展能力。
冗余 :
可以采用一对多的方式,一个生产者发布消息,可以被多个订阅topic的服务消费到,供多个毫无关联的业务使用。
健壮性 :
消息队列可以堆积请求,所以消费端业务即使短时间死掉,也不会影响主要业务的正常进行。
异步通信 :
很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。
可恢复性:
系统的一部分组件失效时,不会影响整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。
顺序保证:
在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。Kafka可保证一个分区内的消息是有序的。
WHAT
消息模型
队列模型
图
生产者(Producer)发消息就是入队操作,
消费者(Consumer)收消息就是出队也就是删除操作,
服务端存放消息的容器自然就称为“队列”。
发布 - 订阅模型
图
消息的发送方称为发布者(Publisher),消息的接收方称为订阅者(Subscriber),服务端存放消息的容器称为主题(Topic)。
发布者将消息发送到主题中,订阅者在接收消息之前需要先“订阅主题”。“订阅”在这里既是一个动作,同时还可以认为是主题在消费时的一个逻辑副本,每份订阅中,订阅者都可以接收到主题的所有消息。
HOW
数据传输的事务定义
最多一次:
消息不会被重复发送,最多被传输一次,但也有可能一次不传输
最少一次:
消息不会被漏发送,最少被传输一次,但也有可能被重复传输.
精确的一次( Exactly once)
不会漏传输也不会重复传输,每个消息都传输被一次而且仅仅被传输一次,这是大家所期望的
主要产品
图
Kafka、RabbitMQ、RocketMQ的调研比对
Kafka
优势:
在性能方面kafka可以说是业界非常优秀的一款中间件,在常规的机器配置下,一台机器可以达到每秒几十万的QPS。并且Kafka的性能也非常高,基本上发给kafka的消息都是毫秒级别的,
可用性也特别高,kafka是支持集群部署的,并且其中部分机器宕机,还是可以运行的。
劣势:
kafka有可能会丢失数据,因为kafka收到消息之后,会写一个磁盘缓冲区里,并没有直接落地到物理磁盘上去,所以机器故障之后,可能会导致磁盘缓冲区的数据丢失。
kafka的功能比较单一,主要是支持发送消息给它,然后从里面消费消息,其它就没有什么额外的高级功能了,所以基于kafka有限的功能,可能适用的场景并不是很多。
综上所述:一般公司会利用kafka收集一些日志之类的消息,因为日志一般量特别大,即使丢几条数据也没事,并且要求吞吐量也高,一般就是收发消息,不需要太多的功能,所以kafka非常适合这个场景。
RabbitMQ
优势:
在RocketMQ没有出现之前,好多公司都从ActiveMQ切换到了RabbitMQ,它的优势在于可以保证数据不丢失,也能保证高可用性,即使集群部署部分机器宕机也能运行,然后支持部分高级功能,比如死信队列,消息重试之类的。
缺点:
RabbitMQ的吞吐量比较低,一般就是几万的级别,如果遇到特别高的并发时,支撑起来有点困难。并且进行集群的扩展也是比较麻烦的。还有就是开发语言用的是erlang,国内使用此语言的很少,所以对其深入的研究也是比较麻烦的。
RocketMQ
优点:
RocketMQ几乎同时解决了Kafka和RabbitMQ的缺陷。
它的吞吐量也非常高,单机可以达到10万的QPS以上,
可以保证高可用性,
可以通过配置达到数据保证不会丢失,
可以部署大规模的集群,
支持各种高级功能,比如说延迟消息、事务消息、消息回溯、死信队列、消息积压等。
是利用java开发的,符合国内的大多数公司的技术栈,很容易进行阅读源码和修改其内容。
缺点:
RocketMQ的官方文档相比较于kafka和RabbitMQ来说的话会相对简单一些,没有人家kafka和RabbitMQ的文档写的详细。
kafka
架构图
what
架构组成
broker:
Kafka 服务器,负责消息存储和转发
broker 是消息的代理,Producers往Brokers里面的指定Topic中写消息,Consumers从Brokers里面拉取指定Topic的消息,然后进行业务处理,broker在中间起到一个代理保存消息的中转站。
topic:
消息类别, Kafka 按照 topic 来分类消息
partition:
topic 的分区,一个 topic 可以包含多个 partition, topic 消息保存在各个partition 上
offset:
消息在日志中的位置,可以理解是消息在 partition 上的偏移量,也是代表该消息的唯一序号
Producer:
消息生产者
Consumer:
消息消费者
Consumer Group:
消费者分组,每个 Consumer 必须属于一个 group
Zookeeper:
保存着集群 broker、 topic、 partition 等 meta 数据;另外,还负责 broker 故障发现
partition leader:
选举,负载均衡等功能
消息模型
消费者
消费数据
消费者每次消费数据的时候,消费者都会记录消费的物理偏移量( offset)的位置等到下次消费时,他会接着上次位置继续消费
消费者负载均衡策略
一个消费者组中的一个分片对应一个消费者成员,他能保证每个消费者成员都能访问,如果组中成员太多会有空闲的成员
数据有序
一个消费者组里它的内部是有序的 消费者组与消费者组之间是无序的
维护消费状态跟踪的方法
Kafka 采用了不同的策略。Topic 被分成了若干分区,每个分区在同一时间只被一个 consumer 消费。这意味着每个分区被消费的消息在日志中的位置仅仅是一个简单的整数:offset。这样就很容易标记每个分区消费状态就很容易了,仅仅需要一个整数而已。这样消费状态的跟踪就很简单了。这带来了另外一个好处:consumer 可以把 offset 调成一个较老的值,去重新消费老的消息。这对传统的消息系统来说看起来有些不可思议,但确实是非常有用的,谁规定了一条消息只能被消费一次呢?
消息丢失和重复消费
要确定Kafka的消息是否丢失或重复,从两个方面分析入手:消息发送和消息消费。
大部分消息系统在 broker 端的维护消息被消费的记录:一个消息被分发到consumer 后 broker 就马上进行标记或者等待 customer 的通知后进行标记。这样也可以在消息在消费后立马就删除以减少空间占用。但是这样会不会有什么问题呢?如果一条消息发送出去之后就立即被标记为消费过的,一旦 consumer 处理消息时失败了(比如程序崩溃)消息就丢失了。为了解决这个问题, 很多消息系统提供了另外一个个功能:当消息被发送出去之后仅仅被标记为已发送状态,当接到 consumer 已经消费成功的通知后才标记为已被消费的状态。这虽然解决了消息丢失的问题,
但产生了新问题,首先如果 consumer处理消息成功了但是向 broker 发送响应时失败了,这条消息将被消费两次。第二个问题时,broker 必须维护每条消息的状态,并且每次都要先锁住消息然后更改状态然后释放锁。这样麻烦又来了,且不说要维护大量的状态数据,比如如果消息发送出去但没有收到消费成功的通知,这条消息将一直处于被锁定的状态,
1、消息发送
kafka 的 ack 机制
request.required.acks 有三个值 0 1 -1
0:生产者不会等待 broker 的 ack,这个延迟最低但是存储的保证最弱当 server 挂掉的时候就会丢数据
1:服务端会等待 ack 值 leader 副本确认接收到消息后发送 ack 但是如果 leader 挂掉后他不确保是否复制完成新 leader 也会导致数据丢失
-1:同样在 1 的基础上 服务端会等所有的 follower 的副本受到数据后才会受到 leader 发出的 ack,这样数据不会丢失
Kafka消息发送有两种方式:同步(sync)和异步(async),默认是同步方式,可通过producer.type属性进行配置。
Kafka通过配置request.required.acks属性来确认消息的生产:0---表示不进行消息接收是否成功的确认;1---表示当Leader接收成功时确认;-1---表示Leader和Follower都接收成功时确认;
综上所述,有6种消息生产的情况,下面分情况来分析消息丢失的场景:
(1)acks=0,不和Kafka集群进行消息接收确认,则当网络异常、缓冲区满了等情况时,消息可能丢失;
(2)acks=1、同步模式下,只有Leader确认接收成功后但挂掉了,副本没有同步,数据可能丢失;
2、消息消费Kafka消息消费有两个consumer接口,Low-level API和High-level API:
Low-level API:消费者自己维护offset等值,可以实现对Kafka的完全控制;
High-level API:封装了对parition和offset的管理,使用简单;
如果使用高级接口High-level API,可能存在一个问题就是当消息消费者从集群中把消息取出来、并提交了新的消息offset值后,还没来得及消费就挂掉了,那么下次再消费时之前没消费成功的消息就“诡异”的消失了;解决办法:
针对消息丢失:
同步模式下,确认机制设置为-1,即让消息写入Leader和Follower之后再确认消息发送成功;
异步模式下,为防止缓冲区满,可以在配置文件设置不限制阻塞超时时间,当缓冲区满时让生产者一直处于阻塞状态;
针对消息重复:将消息的唯一标识保存到外部介质中,每次消费时判断是否处理过即可。
消息顺序性
kafka每个partition中的消息在写入时都是有序的,消费时,每个partition只能被每一个group中的一个消费者消费,保证了消费时也是有序的。整个topic不保证有序。如果为了保证topic整个有序,那么将partition调整为1.
Pull 模式
最终 Kafka 还是选取了传统的 pull 模式
Pull 模式的另外一个好处是 consumer 可以自主决定是否批量的从 broker 拉取数据。
Push模式必须在不知道下游 consumer 消费能力和消费策略的情况下决定是立即推送每条消息还是缓存之后批量推送。如果为了避免 consumer 崩溃而采用较低的推送速率,将可能导致一次只推送较少的消息而造成浪费。
Pull 模式下, consumer 就可以根据自己的消费能力去决定这些策略
Pull 有个缺点是,如果 broker 没有可供消费的消息,将导致 consumer 不断在循环中轮询,直到新消息到 达。为了避免这点, Kafka 有个参数可以让 consumer 阻塞知道新消息到达(当然也可以阻塞知道消息的数量达到某个特定的量这样就可以批量发)
Kafka 的消息模型和 RocketMQ 是完全一样的
队列这个概念的名称不一样,Kafka 中对应的名称是“分区(Partition)
与传统消息系统区别
Kafka 持久化日志,这些日志可以被重复读取和无限期保留
Kafka 是一个分布式系统:它以集群的方式运行,可以灵活伸缩,在内部通过复制数据提升容错能力和高可用性
Kafka 支持实时的流式处理
配置相关
生产数据时数据的分组策略
生产者决定数据产生到集群的哪个 partition 中 每一条消息都是以( key, value)格式 Key是由生产者发送数据传入 所以生产者( key)决定了数据产生到集群的哪个 partition
实现延迟队列
Kafka并没有使用JDK自带的Timer或者DelayQueue来实现延迟的功能,而是基于时间轮自定义了一个用于实现延迟功能的定时器(SystemTimer)。JDK的Timer和DelayQueue插入和删除操作的平均时间复杂度为O(nlog(n)),并不能满足Kafka的高性能要求,而基于时间轮可以将插入和删除操作的时间复杂度都降为O(1)。
时间轮的应用并非Kafka独有,其应用场景还有很多,在Netty、Akka、Quartz、Zookeeper等组件中都存在时间轮的踪影。
底层使用数组实现,数组中的每个元素可以存放一个TimerTaskList对象。
TimerTaskList是一个环形双向链表,在其中的链表项TimerTaskEntry中封装了真正的定时任务TimerTask.Kafka中到底是怎么推进时间的呢?Kafka中的定时器借助了JDK中的DelayQueue来协助推进时间轮。具体做法是对于每个使用到的TimerTaskList都会加入到DelayQueue中。Kafka中的TimingWheel专门用来执行插入和删除TimerTaskEntry的操作,而DelayQueue专门负责时间推进的任务。再试想一下,DelayQueue中的第一个超时任务列表的expiration为200ms,第二个超时任务为840ms,这里获取DelayQueue的队头只需要O(1)的时间复杂度。如果采用每秒定时推进,那么获取到第一个超时的任务列表时执行的200次推进中有199次属于“空推进”,而获取到第二个超时任务时有需要执行639次“空推进”,这样会无故空耗机器的性能资源,这里采用DelayQueue来辅助以少量空间换时间,从而做到了“精准推进”。Kafka中的定时器真可谓是“知人善用”,用TimingWheel做最擅长的任务添加和删除操作,而用DelayQueue做最擅长的时间推进工作,相辅相成。
无消息丢失的配置
不要使用 producer.send(msg),而要使用 producer.send(msg, callback)。记住,一定要使用带有回调通知的 send 方法。
设置 acks = all。acks 是 Producer 的一个参数,代表了你对“已提交”消息的定义。如果设置成 all,则表明所有副本 Broker 都要接收到消息,该消息才算是“已提交”。这是最高等级的“已提交”定义。
设置 retries 为一个较大的值。这里的 retries 同样是 Producer 的参数,对应前面提到的 Producer 自动重试。当出现网络的瞬时抖动时,消息发送可能会失败,此时配置了 retries > 0 的 Producer 能够自动重试消息发送,避免消息丢失。
设置 unclean.leader.election.enable = false。这是 Broker 端的参数,它控制的是哪些 Broker 有资格竞选分区的 Leader。如果一个 Broker 落后原先的 Leader 太多,那么它一旦成为新的 Leader,必然会造成消息的丢失。故一般都要将该参数设置成 false,即不允许这种情况的发生。
设置 replication.factor >= 3。这也是 Broker 端的参数。其实这里想表述的是,最好将消息多保存几份,毕竟目前防止消息丢失的主要机制就是冗余。
设置 min.insync.replicas > 1。这依然是 Broker 端参数,控制的是消息至少要被写入到多少个副本才算是“已提交”。设置成大于 1 可以提升消息持久性。在实际环境中千万不要使用默认值 1
。确保 replication.factor > min.insync.replicas。如果两者相等,那么只要有一个副本挂机,整个分区就无法正常工作了。我们不仅要改善消息的持久性,防止数据丢失,还要在不降低可用性的基础上完成。推荐设置成 replication.factor = min.insync.replicas + 1。确保消息消费完成再提交。
Consumer 端有个参数 enable.auto.commit,最好把它设置成 false,并采用手动提交位移的方式。就像前面说的,这对于单 Consumer 多线程处理的场景而言是至关
消费指定分区消息
Kafka consumer 消费消息时,向 broker 发出"fetch"请求去消费特定分区的消息, consumer指定消息在日志中的偏移量( offset),就可以消费从这个位置开始的消息, customer 拥有了 offset 的控制权,可以向后回滚去重新消费之前的消息,这是很有意义的
Kafka 创建 Topic 时如何将分区放置到不同的 Broker 中
(1).副本因子不能大于 Broker 的个数;
(2).第一个分区(编号为 0)的第一个副本放置位置是随机从 brokerList 选择的;
(3).其他分区的第一个副本放置位置相对于第 0 个分区依次往后移。也就是如果我们有 5 个Broker, 5 个分区,假设第一个分区放在第四个 Broker 上,那么第二个分区将会放在第五个 Broker 上;第三个分区将会放在第一个 Broker 上;第四个分区将会放在第二个Broker 上,依次类推;
(4).剩余的副本相对于第一个副本放置位置其实是由 nextReplicaShift 决定的,而这个数也是随机产生的
通信协议
图
详情
ProducerRequest:
生产者发送消息的请求,生产者将消息发送至Kafka集群中的某个Broker,Broker接收到此请求后持久化此消息并更新相关元数据信息。
TopicMetadataRequest:
获取Topic元数据信息的请求,无论是生产者还是消费者都需要通过此请求来获取感兴趣的Topic的元数据。
FetchRequest:
消费者获取感兴趣Topic的某个分区的消息的请求,除此之外,分区状态为Follower的副本也需要利用此请求去同步分区状态为Leader的对应副本数据。
OffsetRequest:
消费者发送至Kafka集群来获取感兴趣Topic的分区偏移量的请求,通过此请求可以获知当前Topic所有分区在不同时间段的偏移量详情。
OffsetCommitRequest:
消费者提交Topic被消费的分区偏移量信息至Broker,Broker接收到此请求后持久化相关偏移量信息。
OffsetFetchRequest:
消费者发送获取提交至Kafka集群的相关Topic被消费的详细信息,和OffsetCommitRequest相互对应。
集群
Kafka 判断一个节点是否还活着有那两个条件
( 1)节点必须可以维护和 ZooKeeper 的连接, Zookeeper 通过心跳机制检查每个节点的连接
(2)如果节点是个 follower,他必须能及时的同步 leader 的写操作,延时不能太久
副本 :ISR、AR
ISR: In-Sync Replicas 副本同步队列
AR: Assigned Replicas 所有副本
ISR 是由leader维护,follower从leader同步数据有一些延迟
(包括延迟时间replica.lag.time.max.ms和延迟条数replica.lag.max.messages两个维度, 当前最新的版本0.10.x中只支持replica.lag.time.max.ms这个维度),
任意一个超过阈值都会把follower剔除出ISR, 存入OSR(Outof-Sync Replicas)列表,新加入的follower也会先存放在OSR中。AR=ISR+OSR。
什么情况下一个 broker 会从 isr中踢出去
leader会维护一个与其基本保持同步的Replica列表,该列表称为ISR(in-sync Replica),每个Partition都会有一个ISR,而且是由leader动态维护 ,如果一个follower比一个leader落后太多,或者超过一定时间未发起数据复制请求,则leader将其重ISR中移除 。
producer 是否直接将数据发送到broker 的 leader(主节点)?
producer 直接将数据发送到 broker 的 leader(主节点),不需要在多个节点进行分发,为了帮助 producer 做到这点,所有的 Kafka 节点都可以及时的告知:哪些节点是活动的,目标topic 目标分区的 leader 在哪。这样 producer 就可以直接将消息发送到目的地了
why
磁盘顺序读写。
Kafka的message是不断追加到本地磁盘文件末尾的,而不是随机的写入,这使得Kafka写入吞吐量得到了显著提升 。
利用了操作系统本身的Page Cache,
就是利用操作系统自身的内存而不是JVM空间内存,为了优化读写性能。
“零拷贝” 机制
使用了sendfile方法, 允许操作系统将数据从Page Cache 直接发送到网络,只需要最后一步的copy操作将数据复制到 NIC 缓冲区, 这样避免重新复制数据 。示意图如下:
分区分段+索引的设计
Kafka的message消息实际上是分布式存储在一个一个小的segment中的,每次文件操作也是直接操作的segment。为了进一步的查询优化,Kafka又默认为分段后的数据文件建立了索引文件,就是文件系统上的.index文件。这种分区分段+索引的设计,不仅提升了数据读取的效率,同时也提高了数据操作的并行度
启用批次写入
在向Kafka写入数据时,可以启用批次写入,这样可以避免在网络上频繁传输单个消息带来的延迟和带宽开销。
它把所有的消息都变成一个批量的文件,并且进行合理的批量压缩,减少网络IO损耗,通过mmap提高I/O速度,写入数据的时候由于单个Partion是末尾添加所以速度最优;读取数据的时候配合sendfile直接暴力输出。
高效文件存储
Kafka 把 topic 中一个 parition 大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用。
通过索引信息可以快速定位 message 和确定 response 的最大大小。
通过 index 元数据全部映射到 memory,可以避免 segment file 的 IO 磁盘操作。
通过索引文件稀疏存储,可以大幅降低 index 文件元数据占用空间大小。
存储消息格式
消息由一个固定长度的头部和可变长度的字节数组组成。头部包含了一个版本号和 CRC32校验码。
1. 消息长度: 4 bytes (value: 1+4+n)
2. 版本号: 1 byte 3CRC
3. 校验码: 4 bytes
4. 具体的消息: n bytes
partition 数据保存
topic 中的多个 partition 以文件夹的形式保存到 broker,每个分区序号从 0 递增,且消息有序 Partition 文件下有多个 segment( xxx.index, xxx.log)
segment 文件里的 大小和配置文件大小一致可以根据要求修改 默认为 1g 如果大小大于 1g 时,会滚动一个新的 segment 并且以上一个 segment 最后一条消息的偏移量命名
Kafka 新建的分区会在哪个目录下创建
在启动 Kafka 集群之前,我们需要配置好 log.dirs 参数,其值是 Kafka 数据的存放目录,这个参数可以配置多个目录,目录之间使用逗号分隔,通常这些目录是分布在不同的磁盘上用于提高读写性能。当然我们也可以配置 log.dir 参数,含义一样。只需要设置其中一个即可。如果 log.dirs 参数只配置了一个目录,那么分配到各个 Broker 上的分区肯定只能在这个目录下创建文件夹用于存放数据。但是如果 log.dirs 参数配置了多个目录,那么 Kafka 会在哪个文件夹中创建分区目录呢?答案是:Kafka 会在含有分区目录最少的文件夹中创建新的分区目录,分区目录名为 Topic名+分区 ID。注意,是分区文件夹总数最少的目录,而不是磁盘使用量最少的目录!也就是说,如果你给 log.dirs 参数新增了一个新的磁盘,新的分区目录肯定是先在这个新的磁盘上创建直到这个新的磁盘目录拥有的分区目录不是最少为止。
rabbitMQ
架构图
架构
消息模型
少数依然坚持使用队列模型的产品之一。
Exchange 位于生产者和队列之间,生产者并不关心将消息发送给哪个队列,而是将消息发送给 Exchange,由 Exchange 上配置的策略来决定将消息投递到哪些队列中。
组件
交换器
主要有以下4种。
①fanout:所有bind到此exchange的queue都可以接收消息(纯广播,绑定到RabbitMQ的接受者都能收到消息);
②direct:通过routingKey和exchange决定的那个唯一的queue可以接收消息;
③topic:所有符合routingKey(此时可以是一个表达式)的routingKey所bind的queue可以接收消息;
匹配规则:
RoutingKey 为一个 点号'.': 分隔的字符串。比如: java.xiaoka.show
BindingKey和RoutingKey一样也是点号“.“分隔的字符串。
BindingKey可使用 * 和 # 用于做模糊匹配,*匹配一个单词,#匹配多个或者0个
headers:不依赖路由键匹配规则路由消息。是根据发送消息内容中的headers属性进行匹配。性能差,基本用不到。
vhost
vhost可以理解为虚拟broker ,即mini-RabbitMQ server。其内部均含有独立的 queue、exchange和binding等,
其拥有独立的权限系统,可以做到 vhost 范围的用户控制。
vhost可以作为不同权限隔离的手段(一个典型的例子就是不同的应用可以跑在不同的vhost中)。
消息路由
AMQP 中的消息路由
AMQP 中消息的路由过程和 Java 开发者熟悉的 JMS 存在一些差别,AMQP 中增加了 Exchange 和 Binding 的角色。生产者把消息发布到 Exchange 上,消息最终到达队列并被消费者接收,而 Binding 决定交换器的消息应该发送到那个队列。
AMQP 的消息路由过程
Exchange 类型
Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、fanout、topic、headers 。headers 匹配 AMQP 消息的 header 而不是路由键,此外 headers 交换器和 direct 交换器完全一致,但性能差很多,目前几乎用不到了,所以直接看另外三种类型:
direct
原理图
direct 交换器
消息中的路由键(routing key)如果和 Binding 中的 binding key 一致, 交换器就将消息发到对应的队列中。路由键与队列名完全匹配,如果一个队列绑定到交换机要求路由键为“dog”,则只转发 routing key 标记为“dog”的消息,不会转发“dog.puppy”,也不会转发“dog.guard”等等。它是完全匹配、单播的模式。
fanout
fanout 交换器
每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去。fanout 交换器不处理路由键,只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout 类型转发消息是最快的。
topic
topic 交换器
topic 交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。它同样也会识别两个通配符:符号“#”和符号“”。#匹配0个或多个单词,匹配不多不少一个单词。
集群
集群模式
图
主备模式:实现rabbitMQ高可用集群,一般在并发量和数据不大的情况下,这种模式好用简单。又称warren模式。(区别于主从模式,主从模式主节点提供写操作,从节点提供读操作,主备模式从节点不提供任何读写操作,只做备份)如果主节点宕机备份从节点会自动切换成主节点,提供服务。
集群模式:经典方式就是Mirror模式,保证100%数据不丢失,实现起来也是比较简单。
镜像队列,是rabbitMQ数据高可用的解决方案,主要是实现数据同步,一般来说是由2-3节点实现数据同步,(对于100%消息可靠性解决方案一般是3个节点)
延迟消息队列 两种方式
通过消息过期后进入死信交换器,再由交换器转发到延迟消费队列,实现延迟功能;
使用 RabbitMQ-delayed-message-exchange 插件实现延迟功能。
文章链接
RabbitMQ 总结
rocketMQ
架构图
tu
消息模型
图
RocketMQ 使用的消息模型是标准的发布 - 订阅模型
每个主题包含多个队列,通过多个队列来实现多实例并行生产和消费。
RocketMQ 只在队列上保证消息的有序性,主题层面是无法保证消息的严格顺序的。
订阅者的概念是通过消费组(Consumer Group)来体现的。
每个消费组都消费主题中一份完整的消息,不同消费组之间消费进度彼此不受影响,也就是说,一条消息被 Consumer Group1 消费过,也会再给 Consumer Group2 消费。
组成部分
生产者:不会保存消息发送状态,本身无状态,可以scale out扩展
消费者:
1)集群消费模式下会保存消费进度,并定期更新到broker,本地不存储,重启时到broker上获取最新消费进度,相
当于无状态,可以集群部署。
2)广播消费模式下,每个consumer消费进度保存到本地文件系统,启动时从本地加载消费进度,但是各个
consumer之间消费进度互不影响,相当于share nothing,故也可以扩展,但是不同的consumer可能会重复消费。
Name sever:
1)各个name server之间没有通信,一个宕机不影响其他,可以集群部署。
2)Name server的扩容可以通过单独的http服务器来维护所有namesrv地址列表,让producer和consumer以及broker
集群动态感知最新的namesrc上下线。(http://jmenv.tbsite.net:8080/rocketmq/nsaddr) 但是这样增加了系统的复杂度和运维负担。
3)个人认为一个更好的方案是通过zk来动态扩容(类似于dubbo的服务发现)
Broker:
1)每个broker保存topic的部分信息,彼此之间无共享,可以通过部署多台broker来分担负载。
2)broker扩容通过name server来实现,新加入的broker会把自己注册到name server,这样producer和consumer就可
以从name server获取到最新的broker信息
3)集群方案:单master方案有单点;多master当某个master宕机会对写没有影响,但是该master上的消息再恢复前
不能读,影响消息实时性;多master多slave方式,异步复制下会有丢消息可能,同步复制下不会丢消息但性能略低。
工作流程
启动Namesrv,Namesrv起来后监听端口,等待Broker、Produer、Consumer连上来,相当于一个路由控制中心。
Broker启动,跟所有的Namesrv保持***长连接***,定时发送心跳包。心跳包中包含当前Broker信息(IP+端口等)以及存储所有topic信息。注册成功后,namesrv集群中就有Topic跟Broker的映射关系。
收发消息前,先创建topic,创建topic时需要指定该topic要存储在哪些Broker上。也可以在发送消息时自动创建Topic。
Producer发送消息,启动时先跟Namesrv集群中的其中一台建立长连接,并从Namesrv中获取当前发送的Topic存在哪些Broker上,然后跟对应的Broker建立长连接,直接向Broker发消息。
Consumer跟Producer类似。跟其中一台Namesrv建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,开始消费消息。
消费模式
集群消费 CLUSTERING
一条消息只会被同Group中的一个Consumer消费
多个Group同时消费一个Topic时,每个Group都会有一个Consumer消费到数据
默认是 CLUSTERING 模式,由 Broker 端存储和控制 Offset 的值,使用 RemoteBrokerOffsetStore 结构。
广播消费BROADCASTING
消息将对一个Consumer Group 下的各个 Consumer 实例都消费一遍。即即使这些 Consumer 属于同一个Consumer Group ,消息也会被 Consumer Group 中的每个 Consumer 都消费一次。
消息重复消费
出现原因
正常情况下在consumer真正消费完消息后应该发送ack,通知broker该消息已正常消费,从queue中剔除
当ack因为网络原因无法发送到broker,broker会认为词条消息没有被消费,此后会开启消息重投机制把消息再次投递到consumer。
消费模式:在CLUSTERING模式下,消息在broker中会保证相同group的consumer消费一次,但是针对不同group的consumer会推送多次
解决方案
数据库表:处理消息前,使用消息主键在表中带有约束的字段中insert
Map:单机时可以使用map做限制,消费时查询当前消息id是不是已经存在
Redis:使用分布式锁。
顺序消费
首先多个queue只能保证单个queue里的顺序,queue是典型的FIFO,天然顺序。多个queue同时消费是无法绝对保证消息的有序性的。
可以使用同一topic,同一个QUEUE,发消息的时候一个线程去发送消息,消费的时候 一个线程去消费一个queue里的消息。
消息丢失
Producer端
采取send()同步发消息,发送结果是同步感知的。
发送失败后可以重试,设置重试次数。默认3次。
Broker端
修改刷盘策略为同步刷盘。默认情况下是异步刷盘的。
集群部署
Consumer端
完全消费正常后在进行手动ack确认
分布式事务
1、生产者向MQ服务器发送half消息。
2、half消息发送成功后,MQ服务器返回确认消息给生产者。
3、生产者开始执行本地事务。
4、根据本地事务执行的结果(UNKNOW、commit、rollback)向MQ Server发送提交或回滚消息。
5、如果错过了(可能因为网络异常、生产者突然宕机等导致的异常情况)提交/回滚消息,则MQ服务器将向同一组中的每个生产者发送回查消息以获取事务状态。
6、回查生产者本地事物状态。
7、生产者根据本地事务状态发送提交/回滚消息。
8、MQ服务器将丢弃回滚的消息,但已提交(进行过二次确认的half消息)的消息将投递给消费者进行消费。
Half Message:预处理消息,当broker收到此类消息后,会存储到RMQ_SYS_TRANS_HALF_TOPIC的消息消费队列中
检查事务状态:Broker会开启一个定时任务,消费RMQ_SYS_TRANS_HALF_TOPIC队列中的消息,每次执行任务会向消息发送者确认事务执行状态(提交、回滚、未知),如果是未知,Broker会定时去回调在重新检查。
超时:如果超过回查次数,默认回滚消息。
也就是他并未真正进入Topic的queue,而是用了临时queue来放所谓的half message,等提交事务后才会真正的将half message转移到topic下的queue。
消息堆积
1、如果可以添加消费者解决,就添加消费者的数据量
2、如果出现了queue,但是消费者多的情况。可以使用准备一个临时的topic,同时创建一些queue,在临时创建一个消费者来把这些消息转移到topic中,让消费者消费。
HAProxy
HAProxy性能为何这么好?
单进程、事件驱动模型显著降低了.上下文切换的开销及内存占用.
在任何可用的情况下,单缓冲(single buffering)机制能以不复制任何数据的方式完成读写操作,这会节约大量的CPU时钟周期及内存带宽
借助于Linux 2.6 (>= 2.6.27.19). 上的splice()系统调用,HAProxy可以实现零复制转发(Zero-copy forwarding),在Linux 3.5及以上的OS中还可以实现心零复制启动(zero-starting)
内存分配器在固定大小的内存池中可实现即时内存分配,这能够显著减少创建一个会话的时长
树型存储:侧重于使用作者多年前开发的弹性二叉树,实现了以O(log(N))的低开销来保持计时器命令、保持运行队列命令及管理轮询及最少连接队列
大数据
Storm
架构图
工作原理
整个集群的管理是通过zookeeper来进行的。
客户端提交拓扑到nimbus。
Nimbus针对该拓扑建立本地的目录根据topology的配置计算task,分配task,在zookeeper上建立assignments节点存储task和supervisor机器节点中woker的对应关系。
在zookeeper上创建taskbeats节点来监控task的心跳;
启动topology。
Supervisor去zookeeper上获取分配的tasks,启动多个woker进行,每个woker生成task,一个task一个线程;
根据topology信息初始化建立task之间的连接;Task和Task之间是通过zeroMQ管理的;之后整个拓扑运行起来。
Tuple是流的基本处理单元,也就是一个消息,Tuple在task中流转,Tuple的发送和接收过程如下:
发送Tuple,Worker提供了一个transfer的功能,用于当前task把tuple发到到其他的task中。以目的taskid和tuple参数,序列化tuple数据并放到transfer queue中。
在0.8版本之前,这个queue是LinkedBlockingQueue,0.8之后是DisruptorQueue。
在0.8版本之后,每一个woker绑定一个inbound transfer queue和outbond queue,inbound queue用于接收message,outbond queue用于发送消息。
发送消息时,由单个线程从transferqueue中拉取数据,把这个tuple通过zeroMQ发送到其他的woker中。
接收Tuple,每个woker都会监听zeroMQ的tcp端口来接收消息,消息放到DisruptorQueue中后,后从queue中获取message(taskid,tuple),根据目的taskid,tuple的值路由到task中执行。每个tuple可以emit到direct steam中,也可以发送到regular stream中,在Reglular方式下,由Stream Group(stream id-->component id -->outbond tasks)功能完成当前tuple将要发送的Tuple的目的地。
通过以上分析可以看到,Storm在伸缩性、容错性、高性能方面的从架构设计的角度得以支撑;同时在可靠性方面,Storm的ack组件利用异或xor算法在不失性能的同时,保证每一个消息得到完整处理的同时。
spark
架构图
tu
1.Driver Program
用户编写的Spark程序称为Driver Program。每个Driver程序包含一个代表集群环境的SparkContext对象,程序的执行从Driver程序开始,所有操作执行结束后回到Driver程序中,在Driver程序中结束。如果你是用spark shell,那么当你启动 Spark shell的时候,系统后台自启了一个 Spark 驱动器程序,就是在Spark shell 中预加载的一个叫作 sc 的 SparkContext 对象。如果驱动器程序终止,那么Spark 应用也就结束了。
2. SparkContext对象
每个Driver Program里都有一个SparkContext对象,职责如下:
1)SparkContext对象联系 cluster manager(集群管理器),让 cluster manager 为Worker Node分配CPU、内存等资源。此外, cluster manager会在 Worker Node 上启动一个执行器(专属于本驱动程序)。
2)和Executor进程交互,负责Task(任务)的调度分配。
3. cluster manager 集群管理器(Master)
它对应的是Master进程。集群管理器负责集群的资源调度,比如为Worker Node分配CPU、内存等资源。并实时监控Worker的资源使用情况。一个Worker Node默认情况下分配一个Executor(进程)。
从图中可以看到sc和Executor之间画了一根线条,这表明:程序运行时,sc是直接与Executor进行交互的。
所以,cluster manager 只是负责资源的管理调度,而任务的分配和结果处理它不管。
4.Worker Node
Worker节点。集群上的计算节点,对应一台物理机器
5.Worker进程
它对应Worder进程,用于和Master进程交互,向Master注册和汇报自身节点的资源使用情况,并管理和启动Executor进程
6.Executor
负责运行Task计算任务,并将计算结果回传到Driver中。
7.Task
在执行器上执行的最小单元。比如RDD Transformation操作时对RDD内每个分区的计算都会对应一个Task。
运行基本流程
如图9-7 所示,Spark的基本运行流程如下:
(1)当一个Spark应用被提交时,首先需要为这个应用构建起基本的运行环境,即由任务控制节点(Driver)创建一个SparkContext,由SparkContext负责和资源管理器(Cluster Manager)的通信以及进行资源的申请、任务的分配和监控等。SparkContext会向资源管理器注册并申请运行Executor的资源;
(2)资源管理器为Executor分配资源,并启动Executor进程,Executor运行情况将随着“心跳”发送到资源管理器上;
(3)SparkContext根据RDD的依赖关系构建DAG图,DAG图提交给DAG调度器(DAGScheduler)进行解析,将DAG图分解成多个“阶段”(每个阶段都是一个任务集),并且计算出各个阶段之间的依赖关系,然后把一个个“任务集”提交给底层的任务调度器(TaskScheduler)进行处理;Executor向SparkContext申请任务,任务调度器将任务分发给Executor运行,同时,SparkContext将应用程序代码发放给Executor;
(4)任务在Executor上运行,把执行结果反馈给任务调度器,然后反馈给DAG调度器,运行完毕后写入数据并释放所有资源。
yarn
[文档] 19_尚硅谷大数据之MapReduce_Yarn.pdf
架构
组成
Yarn 是一个资源调度平台,负责为运算程序提供服务器运算资源,相当于一个分布式的操作系统平台,
工作机制
图
工作机制详解
(0)Mr 程序提交到客户端所在的节点。
(1)Yarnrunner 向 Resourcemanager 申请一个 Application。
(2)rm 将该应用程序的资源路径返回给 yarnrunner。
(3)该程序将运行所需资源提交到 HDFS 上。
(4)程序资源提交完毕后,申请运行 mrAppMaster。
(5)RM 将用户的请求初始化成一个 task。
(6)其中一个 NodeManager 领取到 task 任务。
(7)该 NodeManager 创建容器 Container,并产生 MRAppmaster。
(8)Container 从 HDFS 上拷贝资源到本地。
(9)MRAppmaster 向 RM 申请运行 maptask 资源。
(10)RM 将运行 maptask 任务分配给另外两个 NodeManager,另两个 NodeManager 分别领取任务并创建容器。
(11)MR 向两个接收到任务的 NodeManager 发送程序启动脚本,这两个 NodeManager分别启动 maptask,maptask 对数据分区排序。
(12)MrAppMaster 等待所有 maptask 运行完毕后,向 RM 申请容器,运行 reduce task。
(13)reduce task 向 maptask 获取相应分区的数据。
(14)程序运行完毕后,MR 会向 RM 申请注销自己。
Prometheus
架构图
特征
多维度数据模型
方便的部署和维护
灵活的数据采集
强大的查询语言
资料
资料1
数据结构/算法/设计模式
**数据结构 **
数组 书
队列 插线盒
顺序队列和链式队列。
阻塞队列
并发队列
一种极致性能的缓冲队列
堆 首饰盒
堆是一个完全二叉树;堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。
堆通常是一个可以被看做一棵完全二叉树的数组对象。
将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。
建堆时间复杂度O(n)
堆总是满足下列性质:堆总是一棵完全二叉树;堆中某个结点的值总是不大于或不小于其父结点的值;
栈 笔记本工作站
链表 手表
单链表
循环链表
双向链表
树 鼠标
满二叉树
除最后一层无任何子节点外,每一层上的所有结点都有两个子结点。也可以这样理解,除叶子结点外的所有结点均有两个子结点。节点数达到最大值,所有叶子结点必须在同一层上
完全二叉树(特殊的满二叉树)
若设二叉树的深度为h,除第 h 层外,其它各层 (1~(h-1)层) 的结点数都达到最大个数,第h层所有的结点都连续集中在最左边,这就是完全二叉树。
判断算法,层序遍历,遇上不满的,如果右边节点还有不满的,就返回false
二叉查找树
左子树小于根结点,右子树大于根结点
增加节点
按查找算法,找到父节点插入
删除节点
如果该节点无子节点,直接删除
有一个,将子节点接到删除节点的位置
有两个,将待删除节点和中序遍历后续节点换位置,如果没子节点,就删除
平衡二叉树
就是平衡了的二叉查找树
红黑树
红黑树(Red-Black Tree,R-B Tree)是一种自平衡的二叉查找树。在红黑树的每个节点上都多出一个存储位表示节点的颜色,颜色只能是红(Red)或者黑(Black)。
4.6.1 红黑树的特性
红黑树的特性如下。
◎ 每个节点或者是黑色的,或者是红色的。
◎ 根节点是黑色的。
◎ 每个叶子节点(NIL)都是黑色的。
◎ 如果一个节点是红色的,则它的子节点必须是黑色的。
◎ 从一个节点到该节点的子孙节点的所有路径上都包含相同数量的黑色节点。
具体的数据结构如图4-15所示。
4.6.2 红黑树的左旋
对a节点进行左旋,指将a节点的右子节点设为a节点的父节点, 即将a节点变成一个左节点。因此左旋意味着被旋转的节点将变成一个左节点,具体流程如图 4-16所示。
图4-16
4.6.3 红黑树的右旋
对b节点进行右旋,指将b节点的左子节点设为b节点的父节点, 即将b节点设为一个右节点。因此右旋意味着被旋转的节点将变成一个右节点,具体流程如图 4-17所示。
图4-17
4.6.4 红黑树的添加
红黑树的添加分为 3步:
①将红黑树看作一颗二叉查找树,并以二叉树的插入规则插入新节点;
②将插入的节点涂为“红色”或“黑色”;
③通过左旋、右旋或着色操作,使之重新成为一颗红黑树。 根据被插入的节点的父节点的情况,可以将具体的插入分为3种
情况来处理。
(1) 如果被插入的节点是根节点,则直接把此节点涂为黑色的。
(2) 如果被插入的节点的父节点是黑色的,则什么也不需要做,在节点插入后,仍然是红黑树。
(3) 如果被插入的节点的父节点是红色的,则在被插入节点的父节点是红色的时,被插入节点一定存在非空祖父节点,即被插入节点也一定存在叔叔节点,即使叔叔节点(叔叔节点指当前节点的祖父节点的另一个子节点)为空,我们也视之为存在,空节点本身就是黑色节点。然后根据叔叔节点的颜色,在被插入节点的父节点是红色的时,进一步分为3种情况来处理。
◎ 如果当前节点的父节点是红色的,当前节点的叔叔节点是红色的,则将父节点设为黑色的,将叔叔节点设为黑色的,将祖父节点设为红色的,将祖父节点设为当前节点。
◎ 如果当前节点的父节点是红色的,当前节点的叔叔节点是黑色的且当前节点是右节点,则将父节点设为当前节点,以新节点为支点左旋。
◎ 如果当前节点的父节点是红色的,当前节点的叔叔节点是黑色的且当前节点是左节点,则将父节点设为黑色的,将祖父节点设为红色的,以祖父节点为支点右旋。
4.6.5 红黑树的删除
红黑树的删除分为两步:①将红黑树看作一颗二叉查找树,根据二叉查找树的删除规则删除节点;②通过左旋、旋转、重新着色操作进行树修正,使之重新成为一棵红黑树,具体操作如下。
(1) 将红黑树看作一颗二叉查找树,将节点删除。
◎ 如果被删除的节点没有子节点,那么直接将该节点删除。
◎ 如果被删除的节点只有一个子节点,那么直接删除该节点, 并用该节点的唯一子节点替换该节点的位置。
◎ 如果被删除的节点有两个子节点,那么先找出该节点的替换节点,然后把替换节点的数据复制给该节点的数据,之后删除替换节点。
(2) 通过左旋、旋转、重新着色操作进行树修正,使之重新成为一棵红黑树,因为红黑树在删除节点后可能会违背红黑树的特性, 所以需要通过旋转和重新着色来修正该树,使之重新成为一棵红黑 树:
①如果当前节点的子节点是“红+黑”节点,则直接把该节点设为黑色的;
②如果当前节点的子节点是“黑+黑”节点,且当前节点是根节点,则什么都不做;
③如果当前节点的子节点是“黑+黑”节点,且当前节点不是根节点,则又可以分为以下几种情况进行处理。
◎ 如果当前节点的子节点是“黑+黑”节点,且当前节点的兄弟节点是红色的,则将当前节点的兄弟节点设置为黑色的,将父节点设置为红色的,对父节点进行左旋,重新设置当前节点的兄弟节点。
◎ 如果当前节点的子节点是“黑+黑”节点,且当前节点的兄弟节点是黑色的,兄弟节点的两个子节点也都是黑色的,则将当前节点的兄弟节点设置为红色的,设置当前节点的父节点为新节点。
◎ 如果当前节点的子节点是“黑+黑”节点,且当前节点的兄弟节点是黑色的,兄弟节点的左子节点是红色的且右子节点是黑色的,则将当前节点的左子节点设置为黑色的,将兄弟节点设置为红色的,对兄弟节点进行右旋,重新设置当前节点的兄弟节点。
◎ 如果当前节点的子节点是“黑+黑”节点,且当前节点的兄弟节点是黑色的,兄弟节点的右子节点是红色的且左子节点是任意颜色 的,则将当前节点的父节点的颜色赋值给兄弟基点,将父节点设置为黑色的,将兄弟节点的右子节点设置为黑色的,对父节点进行左旋, 设置当前节点为根节点。
B树和B+树
b树
所有键值分布在整颗树中(索引值和具体data都在每个节点里);
任何一个关键字出现且只出现在一个结点中;
搜索有可能在非叶子结点结束(最好情况O(1)就能找到数据);
在关键字全集内做一次查找,性能逼近二分查找;
b+树
1.B+树内节点不存储数据,所有 data 存储在叶节点导致查询时间复杂度固定为 log n。而B-树查询时间复杂度不固定,与 key 在树中的位置有关,最好为O(1)。
2. B+树叶节点两两相连可大大增加区间访问性,可使用在范围查询等,而B-树每个节点 key 和 data 在一起,则无法区间查找。
根据空间局部性原理:如果一个存储器的某个位置被访问,那么将它附近的位置也会被访问。
B+树可以很好的利用局部性原理,若我们访问节点 key为 50,则 key 为 55、60、62 的节点将来也可能被访问,我们可以利用磁盘预读原理提前将这些数据读入内存,减少了磁盘 IO 的次数。
当然B+树也能够很好的完成范围查询。比如查询 key 值在 50-70 之间的节点。
3.B+树更适合外部存储。由于内节点无 data 域,每个节点能索引的范围更大更精确
mysql为什么使用b+树作为索引数据结构
Mysql索引使⽤的是B+树,
因为索引是⽤来加快查询的,⽽B+树通过对数据进⾏排序所以是可以提⾼查询 速度的,然后通过⼀个节点中可以存储多个元素,从⽽可以使得B+树的⾼度不会太⾼,在Mysql中⼀个Innodb⻚就是⼀个B+树节点,⼀个Innodb⻚默认16kb,所以⼀般情况下⼀颗两层的B+树可以存2000万
⾏左右的数据,然后通过利⽤B+树叶⼦节点存储了所有数据并且进⾏了排序,并且叶⼦节点之间有指针, 可以很好的⽀持全表扫描,范围查找等SQL语句。
递归树
Trie 树(字典树)
图
描述
根节点不包含字符,除根节点外每一个节点都只包含一个字符。
从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
每个节点的所有子节点包含的字符都不相同。
散列 键盘
散列表用的是数组支持按照下标随机访问数据的特性,所以散列表其实就是数组的一种扩展,由数组演化而来。可以说,如果没有数组,就没有散列表。
图 显示器
图中的元素我们就叫做顶点(vertex)。从我画的图中可以看出来,图中的一个顶点可以与任意其他顶点建立连接关系。我们把这种建立的关系叫做边(edge)。
广度优先搜索(BFS)
深度优先搜索(DFS)
跳表 玩具
图解
是基于链表实现的一种类似“二分”的算法。它可以快速的实现增,删,改,查操作。这种链表加多级索引的结构,就叫做跳表。跳表的查询时间复杂度可以达到O(logn)。
跳表也可以实现高效的动态更新,定位到要插入或者删除数据的位置需要的时间复杂度为O(logn).
在插入的时候,我们需要考虑将要插入的数据也插入到索引中去。在这里使用的策略是通过随机函数生成一个随机数K,然后将要插入的数据同时插入到k级以下的每级索引中。
算法
排序(8种)查毛选 快归 积极通
冒泡、插入、选择( 平方)
归并、快速 (n lgn)
计数、基数、桶(n)
复杂度
排序算法
直接插入排序
两层循环,第一层,遍历所有数
第二层,拿着该数遍历之前所有已经排过顺序的数然后插入
希尔排序
将数组分成length/2组
将组内元素进行插入排序
再次进行length/2/2组
再进行组内元素插入排序
直到length/2n次方等于1
最后进行整个数组的插入排序
选择排序
算法描述
n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:
初始状态:无序区为R[1..n],有序区为空;
第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
n-1趟结束,数组有序化了。
动画
代码
堆排序
将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
动画
大顶堆的构造
初始状态
此时我们从最后一个非叶子结点开始(倒数第二层最左),从左至右,从下至上进行调整。
同样的找下一个
此时找到第一个最大元素,将该元素和末尾元素调换位置
按照上述规则找下一个最大元素
重复上述步骤
冒泡排序
算法描述
比较相邻的元素。如果第一个比第二个大,就交换它们两个;
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
针对所有的元素重复以上的步骤,除了最后一个;
重复步骤1~3,直到排序完成。
图
快排
思路,将数组分成两半,不一定均分,左边的统统小于右边的
再对这两个半个数组重复上序步骤,
过程
选取第一个数作为基数
从左往右找到第一个比基数大的数,再从右往左找到第一个比基数小的数,调换他们俩对位置
归并排序
就不断的拆,拆到最后只剩两个,然后排这两个顺序,排好最小的组之后,排前一次拆分的组,
两个组在进行排序,规则是a组的第一个元素和b组的第一个元素比较,完事之后放在最前面
基数排序
建立一个数组加链表结构
按个位插入相应的位置
取出的时候数组低位放前面,链表头放前面,保证按个位计算时,前面比后面小
在从左到右遍历将各个数按十位放入数组和链表结构,此时保证的是按十位和个位一起算前面的比后面小
重复操作
题目
如何在数据流中找到中位数
建立一个大根堆和一个小根堆,大根堆最大值小于小根堆最小值
排序方法选择
若n较小(如n≤50),可采用直接插入或直接选择排序。当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插人,应选直接选择排序为宜。
若文件初始状态基本有序(指正序),则应选用直接插入、冒泡或随机的快速排序为宜;
若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序。
a) 快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
b) 堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。
c) 若要求排序稳定,则可选用归并排序。
TopK或优先队列通常用堆排序来实现
查找
图
分治
动态规划(背包问题)
回溯(骑士周游问题)
贪心算法
基本思路
建立数学模型来描述问题
把求解的问题分成若干个子问题
对每个子问题求解,得到子问题的局部最优解
把子问题的解局部最优解合成原来问题的一个解
存在问题
不能保证求得的最后解是最佳的
不能用来求最大值或最小值的问题
只能求满足某些约束条件的可行解的范围
【背包问题】有一个背包,容量是M=150,有7个物品,物品可以分割成任意大小。要求尽可能让装入背包中的物品总价值最大,但不能超过总容量。
物品:A B C D E F G
重量:35 30 60 50 40 10 25
价值:10 40 30 50 35 40 30
分析:
目标函数: ∑pi最大
约束条件是装入的物品总质量不超过背包容量:∑wi<=M( M=150)
(1)根据贪心的策略,每次挑选价值最大的物品装入背包,得到的结果是否最优?
(2)每次挑选所占重量最小的物品装入是否能得到最优解?
(3)每次选取单位重量价值最大的物品,成为解本题的策略
对于例题中的3种贪心策略,都是无法成立(无法被证明)的
KMP
Prim
kruskai
floyd(最短路径)
迪杰斯特拉(最短路径)
限流算法
固定窗口与滑动窗口
滑动窗口
漏桶算法与令牌筒算法
令牌桶
如果我们需要在一秒内限制访问次数为 N 次,那么就每隔 1/N 的时间,往桶内放入一个令牌;在处理请求之前先要从桶中获得一个令牌,如果桶中已经没有了令牌,那么就需要等待新的令牌或者直接拒绝服务;桶中的令牌总数也要有一个限制,如果超过了限制就不能向桶中再增加新的令牌了。这样可以限制令牌的总数,一定程度上可以避免瞬时流量高峰的问题。
图
总结
限流是一种常见的服务保护策略,你可以在整体服务、单个服务、单个接口、单个 IP 或者单个用户等多个维度进行流量的控制;
基于时间窗口维度的算法有固定窗口算法和滑动窗口算法,两者虽然能一定程度上实现限流的目的,但是都无法让流量变得更平滑;
令牌桶算法和漏桶算法则能够塑形流量,让流量更加平滑,但是令牌桶算法能够应对一定的突发流量,所以在实际项目中应用更多。
Bitmap位图算法
位图是指内存中连续的二进制位,用于对大量的整型数据做去重和查询。Bit-map就是用一个bit位来标记某个元素对应的Value,而Key即是该元素。由于采用了Bit为单位来存储数据,因此在存储空间方面,可以大大节省。
bitmap应用
1)可进行数据的快速查找,判重,删除,一般来说数据范围是int的10倍以下。
2)去重数据而达到压缩数据
位图只是可以映射数字类型的数据,变成字符串以及其他文件好像就不再那么得心应手了,这时就要使用布隆过滤器
压缩
性能对比图
对比
大数据
算法
1、海量日志数据,提取出某日访问百度次数最多的那个IP
此题,在我之前的一篇文章算法里头有所提到,当时给出的方案是:IP的数目还是有限的,最多2^32个,所以可以考虑使用hash将ip直接存入内存,然后进行统计。
再详细介绍下此方案:首先是这一天,并且是访问百度的日志中的IP取出来,逐个写入到一个大文件中。注意到IP是32位的,最多有个2^32个IP。同样可以采用映射的方法,比如模1000,把整个大文件映射为1000个小文件,再找出每个小文中出现频率最大的IP(可以采用hash_map进行频率统计,然后再找出频率最大的几个)及相应的频率。然后再在这1000个最大的IP中,找出那个频率最大的IP,即为所求。
2、搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是1千万,但如果除去重复后,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门。),请你统计最热门的10个查询串,要求使用的内存不能超过1G。
典型的Top K算法,还是在这篇文章里头有所阐述。文中,给出的最终算法是:第一步、先对这批海量数据预处理,在O(N)的时间内用Hash表完成排序;然后,第二步、借助堆这个数据结构,找出Top K,时间复杂度为N‘logK。即,借助堆结构,我们可以在log量级的时间内查找和调整/移动。因此,维护一个K(该题目中是10)大小的小根堆,然后遍历300万的Query,分别和根元素进行对比所以,我们最终的时间复杂度是:O(N) + N'*O(logK),(N为1000万,N’为300万)。ok,更多,详情,请参考原文。
或者:采用trie树,关键字域存该查询串出现的次数,没有出现为0。最后用10个元素的最小推来对出现频率进行排序。
3、有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。
方案:顺序读文件中,对于每个词x,取hash(x)%5000,然后按照该值存到5000个小文件(记为x0,x1,...x4999)中。这样每个文件大概是200k左右。
如果其中的有的文件超过了1M大小,还可以按照类似的方法继续往下分,直到分解得到的小文件的大小都不超过1M。对每个小文件,统计每个文件中出现的词以及相应的频率(可以采用trie树/hash_map等),并取出出现频率最大的100个词(可以用含100个结点的最小堆),并把100个词及相应的频率存入文件,这样又得到了5000个文件。下一步就是把这5000个文件进行归并(类似与归并排序)的过程了。
4、有10个文件,每个文件1G,每个文件的每一行存放的都是用户的query,每个文件的query都可能重复。要求你按照query的频度排序。
还是典型的TOP K算法,解决方案如下:
方案1:顺序读取10个文件,按照hash(query)%10的结果将query写入到另外10个文件(记为)中。这样新生成的文件每个的大小大约也1G(假设hash函数是随机的)。找一台内存在2G左右的机器,依次对用hash_map(query, query_count)来统计每个query出现的次数。利用快速/堆/归并排序按照出现次数进行排序。将排序好的query和对应的query_cout输出到文件中。这样得到了10个排好序的文件(记为)。
对这10个文件进行归并排序(内排序与外排序相结合)。
方案2:一般query的总量是有限的,只是重复的次数比较多而已,可能对于所有的query,一次性就可以加入到内存了。这样,我们就可以采用trie树/hash_map等直接来统计每个query出现的次数,然后按出现次数做快速/堆/归并排序就可以了。
方案3:与方案1类似,但在做完hash,分成多个文件后,可以交给多个文件来处理,采用分布式的架构来处理(比如MapReduce),最后再进行合并。
5、 给定a、b两个文件,各存放50亿个url,每个url各占64字节,内存限制是4G,让你找出a、b文件共同的url?
方案1:可以估计每个文件的大小为5G×64=320G,远远大于内存限制的4G。所以不可能将其完全加载到内存中处理。考虑采取分而治之的方法。
遍历文件a,对每个url求取hash(url)%1000,然后根据所取得的值将url分别存储到1000个小文件(记为a0,a1,...,a999)中。这样每个小文件的大约为300M。
遍历文件b,采取和a相同的方式将url分别存储到1000小文件(记为b0,b1,...,b999)。这样处理后,所有可能相同的url都在对应的小文件(a0 vs b0,a1 vs b1,...,a999 vs b999)中,不对应的小文件不可能有相同的url。然后我们只要求出1000对小文件中相同的url即可。
求每对小文件中相同的url时,可以把其中一个小文件的url存储到hash_set中。然后遍历另一个小文件的每个url,看其是否在刚才构建的hash_set中,如果是,那么就是共同的url,存到文件里面就可以了。
方案2:如果允许有一定的错误率,可以使用Bloom filter,4G内存大概可以表示340亿bit。将其中一个文件中的url使用Bloom filter映射为这340亿bit,然后挨个读取另外一个文件的url,检查是否与Bloom filter,如果是,那么该url应该是共同的url(注意会有一定的错误率)。
6、在2.5亿个整数中找出不重复的整数,注,内存不足以容纳这2.5亿个整数。
方案1:采用2-Bitmap(每个数分配2bit,00表示不存在,01表示出现一次,10表示多次,11无意义)进行,共需内存内存,还可以接受。然后扫描这2.5亿个整数,查看Bitmap中相对应位,如果是00变01,01变10,10保持不变。所描完事后,查看bitmap,把对应位是01的整数输出即可。
方案2:也可采用与第1题类似的方法,进行划分小文件的方法。然后在小文件中找出不重复的整数,并排序。然后再进行归并,注意去除重复的元素。
7、腾讯面试题:给40亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中?
与上第6题类似,我的第一反应时快速排序+二分查找。以下是其它更好的方法:
方案1:申请512M的内存,一个bit位代表一个unsigned int值。读入40亿个数,设置相应的bit位,读入要查询的数,查看相应bit位是否为1,为1表示存在,为0表示不存在。
方案2:这个问题在《编程珠玑》里有很好的描述,大家可以参考下面的思路,探讨一下:又因为2^32为40亿多,所以给定一个数可能在,也可能不在其中;这里我们把40亿个数中的每一个用32位的二进制来表示假设这40亿个数开始放在一个文件中。
然后将这40亿个数分成两类: 1.最高位为0 2.最高位为1 并将这两类分别写入到两个文件中,其中一个文件中数的个数<=20亿,而另一个>=20亿(这相当于折半了);与要查找的数的最高位比较并接着进入相应的文件再查找。再然后把这个文件为又分成两类: 1.次最高位为0 2.次最高位为1
并将这两类分别写入到两个文件中,其中一个文件中数的个数<=10亿,而另一个>=10亿(这相当于折半了);与要查找的数的次最高位比较并接着进入相应的文件再查找。以此类推,就可以找到了,而且时间复杂度为O(logn),方案2完。
附:这里,再简单介绍下,位图方法:使用位图法判断整形数组是否存在重复 判断集合中存在重复是常见编程任务之一,当集合中数据量比较大时我们通常希望少进行几次扫描,这时双重循环法就不可取了。
位图法比较适合于这种情况,它的做法是按照集合中最大元素max创建一个长度为max+1的新数组,然后再次扫描原数组,遇到几就给新数组的第几位置上1,如遇到5就给新数组的第六个元素置1,这样下次再遇到5想置位时发现新数组的第六个元素已经是1了,这说明这次的数据肯定和以前的数据存在着重复。这种给新数组初始化时置零其后置一的做法类似于位图的处理方法故称位图法。它的运算次数最坏的情况为2N。如果已知数组的最大值即能事先给新数组定长的话效率还能提高一倍。
8、怎么在海量数据中找出重复次数最多的一个?
方案1:先做hash,然后求模映射为小文件,求出每个小文件中重复次数最多的一个,并记录重复次数。然后找出上一步求出的数据中重复次数最多的一个就是所求(具体参考前面的题)。
9、上千万或上亿数据(有重复),统计其中出现次数最多的钱N个数据。
方案1:上千万或上亿的数据,现在的机器的内存应该能存下。所以考虑采用hash_map/搜索二叉树/红黑树等来进行统计次数。然后就是取出前N个出现次数最多的数据了,可以用第2题提到的堆机制完成。
设计模式
分类
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
设计模式
工厂设计模式
在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
优点
1、一个调用者想创建一个对象,只要知道其名称就可以了。
2、扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
3、屏蔽产品的具体实现,调用者只关心产品的接口。
缺点
每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。
单例模式
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
分类
立即加载“饿汉式”
在类的内部用static修饰已经造出来的对象,并将构造器私有化,对象在类加载时已经创建完毕
延迟加载 / “懒汉模式”
调用get方法时判断是否创建了,如果是第一次调用则创建对象,不是就返回已经创造好的对象
优点
1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
2、避免对资源的多重占用(比如写文件操作)。
缺点
没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
会造成内存泄漏,因为单例对象的生命周期和程序一样长,如果在该对象内部对其他对象进行引用,则被引用对象永远无法进行垃圾回收
ps:
在多线程的情况下,使用懒汉式单例模式,会造成创建不止一个对象例如,在同一时刻,多个线程同时对是否已经创建了对象进行检测,如果检测结果为不存在,则会同时创建多个对象
不能使用synchronized关键字对if else整体上锁,这样会锁住业务逻辑,效率低
只能对if后面的代码块上锁,但同样的,会造成同时判断,同时创建对象问题,可以使用双重判断解决,第二次判断可以对整个if 上锁
单例模式在多线程环境下必须加voltile关键字
对象对创建过程
先开辟空间,并半初始化
然后调用初始化函数
再与方法栈上的引用进行关联
如果不加voltile关键字,一旦产生指令重排序,先进行关联,再初始化,其他线程有可能调用到这个未被初始化完全的对象造成数据丢失
原型模式
这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆
优点:
1、性能提高。
2、逃避构造函数的约束。
缺点:
1、配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。
2、必须实现 Cloneable 接口。
代理模式
迭代器模式
这种模式用于顺序访问集合对象的元素,不需要知道集合对象的底层表示。
优点: 1、它支持以不同的方式遍历一个聚合对象。 2、迭代器简化了聚合类。 3、在同一个聚合上可以有多个遍历。 4、在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码。
缺点:由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
观察者模式
当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。
MVC
略
数据访问对象模式
数据访问对象模式(Data Access Object Pattern)或 DAO 模式用于把低级的数据访问 API 或操作从高级的业务服务中分离出来。以下是数据访问对象模式的参与者。
前端控制器模式
前端控制器模式(Front Controller Pattern)是用来提供一个集中的请求处理机制,所有的请求都将由一个单一的处理程序处理。该处理程序可以做认证/授权/记录日志,或者跟踪请求,然后把请求传给相应的处理程序。以下是这种设计模式的实体。
如MVC里的dispatcherServlet
云原生模式
■ 请求/响应模式—这是一种服务间的通信协议,一个客户端会向某个远程服务发起一个请求,并且在大多数情况下希望接收到一个响应。这种通信可以是同步的,也可以是异步的,通常基于HTTP完成。
■ 事件驱动模式—这是一个分布式系统中各个实体之间进行事件通信的协议,在一个云原生的应用程序中,各个服务之间通过这些事件来保持最新的状态。
■ CQRS(Command Query Responsibility Segregation,命令查询职责分离)模式—在这种模式中,对某个领域实体的查询处理(读)与命令处理(写)是分开的。
■ 多服务实例模式—通过部署应用程序/服务的多个实例来保证弹性、可伸缩性及云原生的运维实践。
■ 无状态服务模式—应用程序/服务不会被后续的服务调用,在内存或者本地磁盘中不会存储任何状态。
■ 水平伸缩模式—通过创建额外的应用程序实例来增加某个服务的容
■ 有状态服务模式—指用来持久化状态的服务,例如,数据库和消息队列。这些服务用来为无状态服务提供持久化数据。
■ 通过环境变量配置应用程序的模式—在应用程序启动时,通过环境变量将参数值注入应用程序。
■ 配置服务模式—这是一个(有状态的)服务,用来将配置参数传递给多个应用程序的实例,以确保各个实例的行为是一致的。
■ 配置即代码模式—通过对配置文件进行版本控制来管理配置。
■ 零停机时间升级模式—一种让应用程序/服务在升级所有实例时仍然可以提供完整功能的方法。
■ 滚动升级模式—一种通过分批次、增量式升级部分服务实例,来实现应用程序整体零停机时间升级的技术。
■ 蓝/绿升级—一种通过部署一组新的应用程序实例,然后全部切换到这些实例来升级应用程序的技术。
■ 应用程序健康检查—实现一个可以被调用的端点,用来访问某个应用程序的健康状态。
■ 活性探测—定期调用应用程序的健康端点,如果健康检查失败,则重新创建一个应用程序实例。
■ 服务端负载均衡—一种将请求在多个应用程序实例之间路由的方法,使得客户端只需向一个单独实体(负载均衡器)发送请求。
■ 客户端负载均衡—一种将请求在多个应用程序实例之间路由的方法,客户端知道并且可以控制路由到某个服务的多个实例。
■ 服务发现—客户端可以发现它所要调用服务的一个或多个地址。
■ 重试—当客户端无法接收到服务的响应时,可以重复发起一次请求。
■ 安全服务—一个服务无论被调用零次或者多次,返回的结果都是一样的。
■ 幂等服务—一个服务无论被调用一次或者多次,返回的结果都是一样的。
■ 回调—当发往下游服务的请求无法生成一个结果时,执行的应用程序逻辑。
■ 断路器—一种用来阻止不断向某个故障服务实例发送请求,并且当服务恢复正常后允许继续发送请求的技术。
■ API网关—一个具有多种用途的服务代理,包括访问控制、审计、路由等。
■ 挎斗—一种服务代理方法,代理服务本身与服务在一起。
■ 服务网格—多个挎斗的网络和控制台。
■ 分布式跟踪—一种为了跟踪问题原因,用一个线程来跟踪一系列相关的分布式服务的方法。
■ 事件溯源—一种将事件日志作为软件的真实来源,并通过物化视图来满足服务实例需求的模式。
分布式事务
7.1.1 CAP
CAP原则又称CAP定理,指的是在一个分布式系统中,一致性
(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)三者不可兼得。
◎ 一致性:在分布式系统的所有数据备份中,在同一时刻是否有同样的值(等同于所有节点都访问同一份最新的数据副本)。
◎ 可用性:在集群中一部分节点发生故障后,集群整体能否响应客户端的读写请求(对数据更新具备高可用性)。
◎ 分区容错性:系统如果不能在时限内达成数据的一致性,就意味着发生了分区,必须就当前操作在C和A 之间做出选择。以实际效果而言,分区相当于对通信的时限要求。
7.1.2 两阶段提交协议
分布式事务指涉及操作多个数据库的事务,在分布式系统中,各个节点之间在物理上相互独立,通过网络进行沟通和协调。
二阶段提交(Two-Phase Commit)指在计算机网络及数据库领域内,为了使分布式数据库的所有节点在进行事务提交时都保持一致性而设计的一种算法。在分布式系统中,每个节点虽然都可以知道自己的操作是否成功,却无法知道其他节点的操作是否成功。
在一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果,并最终指示这些节点是否真正提交操作结果(比如将更新后的数据写入磁盘等)。因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈决定各参与者是提交操作还是中止操作。
1. Prepare(准备阶段)
事务协调者(事务管理器)给每个参与者(源管理器)都发送Prepare消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的redo和undo日志但不提交,是一种“万事俱备,只欠东风”的状态。
2. Commit(提交阶段)
如果协调者接收到了参与者的失败消息或者超时,则直接给每个参与者都发送回滚消息,否则发送提交消息,参与者根据协调者的指令执行提交或者回滚操作,释放在所有事务处理过程中使用的锁资源,如图7-8所示。
3. 两阶段提交的缺点
◎ 同步阻塞问题:在执行过程中,所有参与者的任务都是阻塞执行的。
◎ 单点故障:所有请求都需要经过协调者,在协调者发生故障时,所有参与者都会被阻塞。
◎ 数据不一致:在二阶段提交的第2 阶段,在协调者向参与者发送Commit(提交)请求后发生了局部网络异常,或者在发送Commit 请求过程中协调者发生了故障,导致只有一部分参与者接收到Commit请求,于是整个分布式系统出现了数据不一致的现象,这也被称为脑裂。
◎ 协调者宕机后事务状态丢失:协调者在发出Commit消息之后宕机,唯一接收到这条消息的参与者也宕机,即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没有人知道事务是否已被提交。
7.1.3 三阶段提交协议
三阶段提交(Three-Phase Commit),也叫作三阶段提交协议
(Three-Phase Commit Protocol),是二阶段提交(2PC)的改进版本。具体改进如下。
◎ 引入超时机制:在协调者和参与者中引入超时机制,如果协调者长时间接收不到参与者的反馈,则认为参与者执行失败。
◎ 在第1 阶段和第2 阶段都加入一个预准备阶段,以保证在最后的任务提交之前各参与节点的状态是一致的。也就是说,除了引入超时机制,三阶段提交协议(3PC)把两阶段提交协议(2PC)的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。
1. CanCommit阶段
协调者向参与者发送Commit请求,参与者如果可以提交就返回
Yes响应,否则返回No响应。
2. PreCommit阶段
协调者根据参与者的反应来决定是否继续进行,有以下两种可能。
◎ 假如协调者从所有参与者那里获得的反馈都是Yes响应,就预执行事务。
◎ 假如有任意参与者向协调者发送了No响应,或者在等待超时之后协调者都没有接收到参与者的响应,则执行事务的中断。
3. DoCommit阶段
该阶段进行真正的事务提交,主要包括:协调者发送提交请求, 参与者提交事务,参与者响应反馈(在事务提交完之后向协调者发送Ack响应),协调者确定完成事务,如图7-9所示。
7.1.4 分布式事务
1. 传统事务
传统事务遵循ACID原则,即原子性、一致性、隔离性和持久性。
◎ 原子性:事务是包含一系列操作的原子操作,事务的原子性确保这些操作全部完成或者全部失败。
◎ 一致性:事务执行的结果必须使数据库从不一致性状态转为一致性状态。保证数据库的一致性指在事务完成时,必须使所有数据都有一致的状态。
◎ 隔离性:因为可能在相同的数据集上同时有许多事务要处理,所以每个事务都应该与其他事务隔离,避免数据被破坏。
◎ 持久性:一旦事务完成,其结果就应该能够承受任何系统的错误,比如在事务提交过程中服务器的电源被切断等。在通常情况 下,事务的结果被写入持续性存储中。
2. 柔性事务
在分布式数据库领域,基于CAP理论及BASE理论,阿里巴巴提
出了柔性事务的概念。BASE理论是CAP理论的延伸,包括基本可用
(Basically Available)、柔性状态(Soft State)、最终一致性
(Eventual Consistency)三个原则,并基于这三个原则设计出了柔性事务。
我们通常所说的柔性事务分为:两阶段型、补偿型、异步确保型、最大努力通知型。
两阶段型事务指分布式事务的两阶段提交,对应技术上的XA和
JTA/JTS,是分布式环境下事务处理的典型模式。
TCC型事务(Try、Confirm、Cancel)为补偿型事务,是一种基于补偿的事务处理模型。如图 7-10所示,服务器A发起事务,服务器B参与事务,如果服务器A的事务和服务器B的事务都顺利执行完成并提交,则整个事务执行完成。但是,如果事务B执行失败,事务B本身就回滚,这时事务A已被提交,所以需要执行一个补偿操作,将已经提交的事务A执行的操作进行反操作,恢复到未执行前事务A的状态。需要注意的是,发起提交的一般是主业务服务,而状态补偿的一般是业务活动管理者,因为活动日志被存储在业务活动管理中,补偿需要依靠日志进行恢复。TCC事务模型牺牲了一定的隔离性和一致 性,但是提高了事务的可用性。
图7-10
异步确保型事务指将一系列同步的事务操作修改为基于消息队列异步执行的操作,来避免分布式事务中同步阻塞带来的数据操作性能
下降。如图 7-11所示,在写业务数据A触发后将执行以下流程。
(1) 业务A的模块在数据库A上执行数据更新操作。
(2) 业务A调用写消息数据模块。
(3) 写消息日志模块将数据库的写操作状态写入数据库A中。
(4) 写消息日志模块将写操作日志发送给消息服务器。
(5) 读消息日志模块接收操作日志。
(6) 读消息数据调用写业务B的模块。
(7) 写业务B更新数据到数据库B。
(8) 写业务数据B的模块发送异步消息更新数据库A中的写消息日志状态,说明自己已经完成了异步数据更新操作。
图7-11
最大努力通知型事务也是通过消息中间件实现的,与前面异步确保型操作不同的是:在消息由MQ服务器发送到消费者之后,允许在达到最大重试次数之后正常结束事务,因此无法保障数据的最终一致性。如图 7-12所示,写业务数据A在更新数据库后调用写消息日志将数据操作以异步消息的形式发送给读消息日志模块;读消息日志模块在接收到数据操作后调用写业务B写数据库。和异步确保型不同的 是,数据库B在写完之后将不再通知写状态到数据库A,如果因为网络或其他原因,在如图 7-12所示的第4步没有接收到消息,则消息服务器将不断重试发送消息到读消息日志,如果经过 N次重试后读消息日志还是没有接收到日志,则消息不再发送,这时会出现数据库A和数据库B数据不一致的情况。最大努力型通知事务通过消息服务使分布式事务异步解耦,并且模块简单、高效,但是牺牲了数据的一致 性,在金融等对事务要求高的业务中不建议使用,但在日志记录类等对数据一致性要求不是很高的应用上执行效率很高。
图7-12
JAVA
线程
线程(状士同心安城池)
状态
状态转换
转换图
新建(NEW)
新创建了一个线程对象,表示线程被创建出来,还没真正启动。
就绪(RUNNABLE)
线程对象创建后,其他线程调用了该对象的start( )方法。
表示该线程硬在JVM中执行,由于执行需要计算资源,它可能是正在运行,也可能是等待系统分配给它CPU片段,在就绪队列里排队。
运行(RUNNING)
就绪状态的线程获取了CPU,执行run( )方法。
阻塞(BLOCKED)
阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行(放弃CPU时间片)。直到线程进入就绪状态,才有机会转到运行状态。
表示线程在等待 Monitor lock。
比如:线程试图通过Synchronized去获取某个锁,但是其他线程已经独占了,那么当前线程就会处于阻塞状态。
阻塞的情况分三种:
等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
等待(WAITING)
表示正在等待其他线程才去某些操作。
如:生产者消费者模式,发现任务条件尚未满足,就让当前消费者线程等待,另外的生产者线程去准备任务数据,然后通过类似notify等动作,通知消费线程可以继续工作了。
Thread.join( )也会令线程进入等待状态。
计时等待(TIME_WAIT)
进入条件和等待状态类似,但是调用的是存在超时条件的方法,比如在wait或join方法中指定超时版本。
public final native void wait(long timeout) throws InterruptedException;
终止(TERMINATED)
不管是意外退出还是正常执行结束,线程已经完成使命,终止运行,该状态也成为死亡。
线程执行完了或者因异常退出了run( )方法,该线程结束生命周期。
生命周期
当我们在Java程序中新建一个线程时,它的状态是New。
当我们调用线程的start( )方法时,状态被改变为Runnable。
线程调度器会为Runnable线程池中的线程分配CPU时间并且讲它们的状态改变为Running。
线程执行完毕或者意外终止则状态变为Dead。
如果线程访问的资源被其他线程锁定,则状态变为Blocked。
实现
继承Thread类,重写run函数
定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。
因此把run( )方法称为执行体。
创建Thread子类的实例,即创建了线程对象。
调用线程对象的start( )方法来启动该线程。
通过Runnable接口,重写run函数
定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
创建 Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
调用线程对象的start()方法来启动该线程。
通过Callable和Future创建线程,重写call函数
Callable接口可以看作是Runnable接口的补充,call方法带有返回值,并且可以抛出异常。
创建Callable接口的实现类,并实现call( )方法,该call()方法将作为线程执行体,并且有返回值。
创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。(FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了Future和Runnable接口。)
使用FutureTask对象作为Thread对象的target创建并启动新线程。
调用FutureTask对象的get( )方法来获得子线程执行结束后的返回值。
创建线程的三种方式的对比
采用实现Runnable、Callable接口的方式创建多线程时
优势是:
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势是:
编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
使用继承Thread类的方式创建多线程时,
优势是:
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
劣势是:
线程类已经继承了Thread类,所以不能再继承其他父类。
Runnable和Callable的区别
Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
call方法可以抛出异常,run方法不可以。
运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
同步
机制
Lock与Synchronized
Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
Synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
Lock
JDK5增加了一个java.util.concurrent包,java.util.concurrent.locks。
ReentrantLock 会由最近成功获得锁定并且还没有释放该锁的线程拥有。当锁没有被另一个线程拥有时,调用lock线程将成功返回该锁定。
如果当前线程已经拥有了该锁,则会立即返回。
公平机制,提供了一个可选的公平锁参数。
锁【冰箱】
状态
无锁状态
偏斜锁(Biased Locking)
JVM 会利用 CAS 操作,在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。
这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。
偏斜锁并不适合所有应用场景,撤销操作(revoke)是比较重的行为,只有当存在较多不会真正竞争的Synchronized块儿时,才能体现出明显改善。
轻量级锁
JDK 1.6中默认是开启偏向锁和轻量级锁的,我们也可以通过-XX:-UseBiasedLocking来禁用偏向锁。
重量级锁
依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。
升级
定义
所谓锁的升级、降级,就是 JVM 优化 Synchronized 运行的机制。
当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。
当没有竞争出现时默认会使用偏斜锁,随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁;
锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级;
流程
当没有竞争出现时默认使用偏斜锁。
JVM 会利用 CAS 操作,在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。
如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏斜锁,(撤销操作是比较重的行为)并切换到轻量级锁实现。
轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;
否则,进一步升级为重量级锁。
分类
图
6大类
图解
分类
乐观锁和悲观锁
悲观锁
每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁。这样别人想拿这个数据就会阻塞直到它拿到锁。
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
如果经常产生冲突,上层应用会不断的进行retry,用乐观锁反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
乐观锁
乐观锁可以使用版本号机制和CAS算法实现。在 Java 语言中 java.util.concurrent.atomic包下的原子类就是使用CAS 乐观锁实现的。
每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的
可重入锁
如果锁具备可重入性,则称作为可重入锁。
像Synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。
可中断锁
顾名思义,就是可以相应中断的锁。
Synchronized就不是可中断锁,而Lock是可中断锁。
处理等待资源状态的线程,可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
公平锁
尽量以请求锁的顺序来获取锁。
比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的,这样就可能导致某个或者一些线程永远获取不到锁。
Synchronized是非公平锁,它无法保证等待的线程获取锁的顺序。
ReentrantLock和ReentrantReadWriteLock默认情况下是非公平锁,但是可以设置为公平锁。
读写锁
读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。
正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。
ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。
可以通过 readLock( ) 获取读锁,通过 writeLock( ) 获取写锁。
自旋锁
首先是一种锁,与互斥锁相似,基本作用是用于线程(进程)之间的同步。
与普通锁不同的是,一个线程A在获得普通锁后,如果再有线程B试图获取锁,那么这个线程B将会挂起(阻塞);
试想下,如果两个线程资源竞争不是特别激烈,而处理器阻塞一个线程引起的线程上下文的切换的代价高于等待资源的代价的时候(锁的已保持者保持锁时间比较短),那么线程B可以不放弃CPU时间片,而是在“原地”忙等,直到锁的持有者释放了该锁,这就是自旋锁的原理,可见自旋锁是一种非阻塞锁。
独占锁
JDK中的synchronized和java.util.concurrent(JUC)包中Lock的实现类就是独占锁。
独占锁是指锁一次只能被一个线程所持有。如果一个线程对数据加上排他锁后,那么其他线程不能再对该数据加任何类型的锁。获得独占锁的线程即能读数据又能修改数据。
共享锁
共享锁是指锁可被多个线程所持有。如果一个线程对数据加上共享锁后,那么其他线程只能对数据再加共享锁,不能加独占锁。获得共享锁的线程只能读数据,不能修改数据。
在 JDK 中 ReentrantReadWriteLock 就是一种共享锁。
CAS
Compare and Swap 比较并交换。
CAS是乐观锁技术,乐观锁是一种思想,CAS是这种思想的一种实现方式。
CAS 操作中包含三个操作数
需要读写的内存位置(V)
进行比较的预期原值(A)
拟写入的新值(B)
操作流程
我认为位置 V 应该包含值 A;
如果包含该值,则将 B 放到这个位置;
否则,不要更改该位置,只告诉我这个位置现在的值即可。
在JDK1.5 中新增 java.util.concurrent (J.U.C) 就是建立在CAS之上的。
Synchronized
作用
其他线程 必须等待当前线程执行完该方法 / 代码块后才能执行该方法 / 代码块
原理
依赖 JVM 实现同步
底层通过一个监视器对象(monitor)完成, wait()、notify() 等方法也依赖于 monitor 对象
实现
在字节码层面
使用monitor enter和monitor exit
执行过程中自动升级锁
汇编lock comxchg
使用
对象设置
修饰代码块时,需1个reference对象 作为锁的对象
当锁对象为这个方法所在的类的.class对象,synchronized锁class可以防止多个线程同时访问这个类所创建的对象中的synchronized方法。它可以对这个类创建的所有对象实例起作用。
修饰实例方法时,默认的锁对象=当前对象(就是该方法所在的类的实例对象)
如果这个对象有多个synchronized方法,其它线程就不能同时访问这个对象中任何一个synchronized方法
修饰类方法(静态)时,默认的锁对象=当前类的Class对象
一把锁同时只能被一个线程持有
当对象获取多个锁时,必须以相反顺序释放锁
特性
独占锁、
非公平锁、
悲观锁、
可重入锁、
线程获取锁进入一个synchronized方法/代码块,又调用了一个synchronized方法/代码块,在进入第二个synchronized方法/代码块时,不需要先释放进入第一个synchronized时获取的锁,也不需要再次争抢锁,而是直接进入
不可中断锁
缺点
试图获得锁时,不能设置超时时间,不能中断正在等待获取锁的线程
加锁和解锁条件单一,加锁仅仅有单一条件(对象或者类)
早期版本效率低下(重量级锁)
1.在使用synchronized关键字的情形下,假如占有锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,那么其他线程就只能一直等待,别无他法。这会极大影响程序执行效率。因此,就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间 (解决方案:tryLock(long time, TimeUnit unit)) 或者 能够响应中断 (解决方案:lockInterruptibly())),这种情况可以通过 Lock 解决。
2.我们知道,当多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是如果采用synchronized关键字实现同步的话,就会导致一个问题,即当多个线程都只是进行读操作时,也只有一个线程在可以进行读操作,其他线程只能等待锁的释放而无法进行读操作。因此,需要一种机制来使得当多个线程都只是进行读操作时,线程之间不会发生冲突
3.我们可以通过Lock得知线程有没有成功获取到锁 (解决方案:ReentrantLock) ,但这个是synchronized无法办到的。
Object的wait(),notify()方法
例子
1.wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”)
2.notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。
上锁的过程
同步方法和同步块,哪个是更好的选择?
同步块是更好的选择,比较灵活,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。
同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。
Lock
方法
lock( )
锁定的资源被其他线程锁定,则进入等待状态;
被lock锁定的资源,使用完后不会自动释放锁,甚至发生异常时也不会自动释放锁;
配合try使用,在使用完资源后需手动调用 unlock 释放锁,避免死锁。
tryLock( )
try + lock的结合 ,会立即返回结果,成功true、失败false。
tryLock(long time, TimeUnit unit)
遇到资源被其他线程锁定时,增加等待时间,超过等待时间则返回结果。
lockInterruptibly( )
资源被其他线程锁定,则抛出 InterruptiException。
优势
它的优势有
可以使锁更公平
可以使线程在等待锁的时候响应中断
可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
可以在不同的范围,以不同的顺序获取和释放锁
注意
Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
特点
从Java 5之后,在java.util.concurrent.locks包下提供了另外一种方式来实现同步访问。
Lock用的是乐观锁方式。
乐观锁实现的机制就是CAS操作(Compare and Swap)。
我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。
在加锁和解锁处需要通过 lock( ) 和 unlock( ) 显式调用,所以一般会在finally块中写 unlock( ) 以防死锁。
需要显式指定起始位置和终止位置。
实现类
ReenTrantLock
Java 5提供的锁机制。
实现了Lock 和 java.io.Serializable;
一个可重入的互斥锁Lock,它具有与使用Synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
再入锁通过代码直接调用 lock( ) 方法获取,代码书写也更加灵活。
ReentrantLock 提供了很多实用的方法,能够实现很多 Synchronized 无法做到的细节控制,比如
可以设置公平性(fairness),我们可在创建再入锁时选择是否是公平的。
这里所谓的公平性是指在竞争场景中,当公平性为真时,会倾向于将锁赋予等待时间最久的线程。
公平性是减少线程“饥饿”(个别线程长期等待锁,但始终无法获取)情况发生的一个办法。
条件变量(java.util.concurrent.Condition)
如果说 ReentrantLock 是 synchronized 的替代选择
Condition 则是将 wait、notify、notifyAll 等操作转化为相应的对象,将复杂而晦涩的同步操作转变为直观可控的对象行为。
编码中需要注意,必须要明确调用 unlock( ) 方法释放,不然就会一直持有该锁。
使用场景
某个线程在等待一个锁的控制权的这段时间需要中断;
需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程;
具有公平锁功能,每个到来的线程都将排队等候;
ReentrantLock的公平性。
https://www.cnblogs.com/xiongmaotailang/p/6139214.html
https://blog.csdn.net/zhang199416/article/details/70792587
ReentrantLock 和 Synchronized的区别?
从性能角度,Synchronized 早期的实现比较低效,对比 ReentrantLock 大多数场景性能都相差较大。
但是在 Java 6 中对其进行了非常多的改进,可以参考性能对比,在高竞争情况下,ReentrantLock 仍然有一定优势。
https://blog.csdn.net/hunterliy/article/details/53954197
实现原理
ReentrantLock主要利用CAS+CLH队列来实现。它支持公平锁和非公平锁,两者的实现类似。
CAS:Compare and Swap,比较并交换。
CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现。
CLH队列:带头结点的双向非循环链表(如下图所示)
图
ReentrantLock的基本实现可以概括为:
先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入CLH队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:
非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;
公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。
ReadWriteLock
一个用来获取读锁,一个用来获取写锁。
也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。
ReentrantReadWriteLock
读写分离,多个线程可以同时读取使用读锁的资源。
如果要写入,则需要等待读锁释放。
StampedLock
StampedLock是为了优化可重入读写锁性能的一个锁实现工具,jdk8开始引入
相比于普通的ReentranReadWriteLock主要多了一种乐观读的功能
在API上增加了stamp的入参和返回值
不支持重入
链接
https://mp.weixin.qq.com/s/IfPkIxoTn5eqZExclaCUTA
死锁
Java程序在什么情况下会产生死锁?
我们都知道死锁是由于多个对象或多个线程之间相互需要对方锁持有的锁而又没有释放对方所持有的锁,导致双方都永久处于阻塞状态;
举例
线程1锁定资源A,等待获得资源B后,才能继续执行。
同时线程2锁定资源B,等待获得资源A后,才能继续执行。
这样就会发生死锁,程序无法正常执行。
如何定位、修复Java程序死锁?
hashmap死锁过程分析
死锁过程分析
有线程A、线程B
Entry<K,V> next = e.next;
当B第一次执行完以下代码,进行了挂起,那么:e=3,next=7,在B挂起的时,线程A完成了transfer的扩容,那么7.next=3,3.next=null,扩容后如下图:
接着线程B继续执行,计算e=3的index为3,完成第一次循环:
接着进行第二次循环,此时:e=7,next=3
为啥next=3呢?因为在线程A完成扩容后,7的next为3,那么在执行到 Entry<K,V> next = e.next时,所以next=3
接着进行第三次循环,此时:e=3,next=null,而当执行到
e.next = newTable[i] 语句时,坑爹的事情发生了,形成闭环了,如下图:
注意此时还没有发送死锁,死锁是在获取元素时产生的,如调用一个不存在的元素,并且index是3,那么恭喜你死锁发生了。
避免死锁
使用tryLock( )方法代替lock( ),tryLock还可以指定获取锁超时时间,到了超时时间还没获得到锁就会放弃获取锁。
排查工具
LockCop
jconsole
jvisualvm
打开jvisualvm连接到当前的应用程序即可看到程序的监控信息,如内存、CPU、性能、GC等等;打开进入线程的tab项查看程序的线程信息,这里很明显的就看到了提示该程序被检测除了死锁!
jstack
实时死锁扫描
JDK提供了MXBean Api可用于扫描程序是否存在死锁,ThreadMXBean提供了findDeadlockedThreads()方法,可以用于找到产生死锁的线程;
Java中死锁的定位与修复:http://www.51testing.com/html/96/n-3727296.html
当一个方法被锁住了,其他线程是否可以访问该对象其他的没有被锁住的方法?
其它线程可以访问该对象的非同步方法;
其他线程不能访问该对象的同步方法;
其他线程可以访问该对象的同步静态方法;
因为它用的同步锁是当前类的字节码,与非静态的方法不能同步,因为非静态的方法用的是this。
因为非静态方法上的Synchronized修饰符要求执行方法时要获得对象的锁,如果已经进入A方法说明对象锁已经被取走,那么试图进入B方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁。
如果这个方法内部调用了wait,则可以进入其他Synchronized方法。
https://blog.csdn.net/wilver/article/details/53132062
安全
共享资源
如果一段代码是线程安全的,则它不包含竞态条件。只有当多个线程更新共享资源时,才会发生竞态条件。
栈封闭时,不会在线程之间共享的变量,都是线程安全的。
局部对象引用本身不共享,但是引用的对象存储在共享堆中。如果方法内创建的对象,只是在方法中传递,并且不对其他线程可用,那么也是线程安全的。(即方法中创建的对象只在方法中引用)
不可变对象就算共享也是线程安全的
解决方式
原子性
加锁
CAS机制(Compare and swap)
当两个线程同时想操作一个数,在第一个线程完成操作之前,第二个线程也读取了初始值,等到第一个线程完成操作之后,第二个线程开始操作,但是根据CAS他会再次读取并比较原来读取的旧值和内存地址的新值有无发生变化,然后更新成新值
通过unsafe类的compareAndSwapInt方法
通过反射拿到unsafe类的对象
调用compareAndSwapint方法,如果失败则用循环来重复操作直到成功
java中有已经封装好了的原子操作封装类
AtomicBoolean
AtomicInteger
AtomicLong
缺点
1.循环+CAS,自旋的实现让所有线程都处于高频运行,争抢CPU执行时间的状态。如果操作长时间不成功,会带来很大的CPU资源消耗。
2.仅针对单个变量的操作,不能用于多个变量来实现原子操作。
3. ABA问题。
有一个单向链表,存入a,b两个值,此时a.next=b 想通过cas原子操作将栈顶的a改成b
在此操作之前,将全部出栈,依次压入d,c,a金栈
cas判断此时栈顶还是a,则完成操作,将栈顶的a改成b,但此时,b.next为空,cd则被丢失
解决:加版本号,每次进行操作加1
应用Atomic类
屏障(barrier)
屏障是多线程各自做自己的工作,如果某一线程完成了工作,就等待在屏障那里,直到其他线程的工作都完成了,再一起做别的事,如果没有达到指定的数量,就会一直被阻塞。
使用方法
Barrier被破坏
如果有线程已经处于等待状态,调用reset方法会导致已经在等待的线程出现BrokenBarrierException异常。并且由于出现了BrokenBarrierException,将会导致始终无法等待。
如果在等待的过程中,线程被中断,也会抛出BrokenBarrierException异常,并且这个异常会传播到其他所有的线程。
如果在执行屏障操作过程中发生异常,则该异常将传播到当前线程中,其他线程会抛出BrokenBarrierException,屏障被损坏。
如果超出指定的等待时间,当前线程会抛出 TimeoutException 异常,其他线程会抛出BrokenBarrierException异常。
线程封闭
ThreadLocal
它是一一个线程级别变量,每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了,在并发模式下是绝对安全的变量。
用法: ThreadLocal<T> var = new ThreadLocal<T>(); .
会自动在每一个线程上创建一一个T的副本,副本之间彼此独立,互不影响。
可以用ThreadLocal存储一些参数,以便在线程中多个方法中使用,用来代替方法传参的做法。
T写的就是该ThreadLocal所装载的对象类型
通过var.get,var.set存取对象,只在单独线程内共享
实现原理
该线程的ThreadLocal存放的对象都放在同一个Map中,map的key为对应的Threadlocal对象,value为被存放的对象
这个map也是属于单独且唯一的当前线程
线程池
分类
标准线程池
队列无界,最大数量大于核心数量,此时线程池中的线程不会超过核心数量,因为只要队列没满,就不会在线程池中新加入线程,新加的任务存进队列中
限制数量的线程池
队列有界,则线程能同时添加的任务最多数量为 :最大线程数量+队列长度,超出则会执行重写的rejectedExecution方法
动态无限缓存的线程池
核心线程数量为零,队列为最大值,此时每一个添加的任务线程池都会为其新建新的线程,无限增加,任务结束后不会消失,而是保存设定的时间,在时间范围内,如果有新的任务添加,则直接调用已经存在的线程池
属性
corePoolSize
核心线程数
maximumPoolSize
线程池最大线程数
keepAliveTime
空闲线程存活的时间,如果线程空闲了keepAliveTime这么久以后,还是没有任务分配给它,那么线程会被关闭。当然,如果当前线程池中的线程数
workQueue
任务队列,用于存储要执行的任务。要求传入BlockingQueue接口的实现类,如ArrayBlockingQueue等
threadFactory
用于创建线程的工厂,我们可以通过自定义threadFactory统一为线程设置一些属性,如线程名称等。
handler
拒绝策略 都是RejectedExecutionHandler 的实现
执行时机
任务成功入队,但是线程池关闭了,并且线程池并没有移除掉这个任务,这个时候执行拒绝策。也就是说任务入队和线程池关闭并发执行了
当前线程数达到maximumPoolSize
创建流程
1.线程池刚创建的时候,里面没有任何线程,等到有任务过来的时候才会创建线程。当然也可以调用 prestartAllCoreThreads() 或者 prestartCoreThread() 方法预创建corePoolSize个线程
2.调用execute()提交一个任务时,如果当前的工作线程数
3.如果当时工作线程数量>=corePoolSize,会将任务放入任务队列中缓存
4.如果队列已满,并且线程池中工作线程的数量
5.如果队列已满,并且线程池中的线程已达到maximumPoolSize,这个时候会执行拒绝策略,JAVA线程池默认的策略是AbortPolicy,即抛出RejectedExecutionException异常
线程复用
答案在runWorker方法中,一个线程执行完一个任务后会不断从任务队列中取出任务来执行,如果队列中已经没有任务了,allowCoreThreadTimeOut设置为false并且线程数<=corePoolSize,调用BlokingQueue.take()方法阻塞,直到获取到任务
如果队列中没有任务了,allowCoreThreadTimeOut设置为true或者线程数>corePoolSize,调用BlockingQueue带超时的poll方法尝试获取任务,获取不到的话,这个线程就会被回收掉
执行任务异常
执行任务的线程会被关闭,而不会继续执行其他任务。最后会启动一个新的线程来取代它。
集合
类图
分类
List
是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序。
可以允许重复的对象;
可以插入多个null元素;
常用的实现类有 ArrayList、LinkedList 和 Vector。ArrayList 最为流行,它提供了使用索引的随意访问,而 LinkedList 则对于经常需要从 List 中添加或删除元素的场合更为合适。
Set
不允许重复对象;
无序容器,无法保证每个元素的存储顺序,TreeSet通过 Comparator 或者 Comparable 维护了一个排序顺序;
只允许一个 null 元素;
最流行的几个实现类是 HashSet、LinkedHashSet 以及 TreeSet。
最流行的是基于 HashMap 实现的 HashSet;TreeSet 还实现了 SortedSet 接口,因此 TreeSet 是一个根据其 compare() 和 compareTo() 的定义进行排序的有序容器。
Map
Map不是Collection的子接口或者实现类。Map是一个接口。
Map 的 每个 Entry 都持有两个对象,也就是一个键一个值,Map 可能会持有相同的值对象但键对象必须是唯一的。
TreeMap 也通过 Comparator 或者 Comparable 维护了一个排序顺序。
Map 里你可以拥有随意个 null 值但最多只能有一个 null 键。
Map 接口最流行的几个实现类是 HashMap、LinkedHashMap、Hashtable 和 TreeMap。(HashMap、TreeMap最常用)
Queue
◎ArrayBlockingQueue:基于数组数据结构实现的有界阻塞队列。
◎LinkedBlockingQueue:基于链表数据结构实现的有界阻塞队列。
◎PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
◎ DelayQueue:支持延迟操作的无界阻塞队列。
◎ SynchronousQueue:用于线程同步的阻塞队列。
◎LinkedTransferQueue:基于链表数据结构实现的无界阻塞队列。
◎LinkedBlockingDeque:基于链表数据结构实现的双向阻塞队列。
线程安全队列
图
LinkedBlockingQueue、ArrayBlockingQueue 和 SynchronousQueue
根据需求可以从很多方面考量:考虑应用场景中对队列边界的要求
ArrayBlockingQueue 是有明确的容量限制的,
而 LinkedBlockingQueue 则取决于我们是否在创建时指定
SynchronousQueue 则干脆不能缓存任何元素。
从空间利用角度,
数组结构的 ArrayBlockingQueue 要比 LinkedBlockingQueue 紧凑,因为其不需要创建所谓节点,
但是其初始分配阶段就需要一段连续的空间,所以初始内存需求更大。
吞吐量
通用场景中,LinkedBlockingQueue 的吞吐量一般优于 ArrayBlockingQueue,因为它实现了更加细粒度的锁操作。
ArrayBlockingQueue 实现比较简单,性能更好预测,属于表现稳定的“选手”。
如果我们需要实现的是两个线程之间接力性(handoff)的场景,你可能会选择 CountDownLatch,但是SynchronousQueue也是完美符合这种场景的,而且线程间协调和数据传输统一起来,代码更加规范。
很多时候 SynchronousQueue 的性能表现,往往大大超过其他实现,尤其是在队列元素较小的场景。
JUC
图
包结构
组成
executor
collections
locks
atomic(原子类)
tools
CountDownLatch
Exchanger
ThreadLocal
CyclicBarrier
并发容器
Java集合类都是快速失败的,这就意味着当集合被改变且一个线程在使用迭代器遍历集合的时候,迭代器的next()方法将抛出ConcurrentModificationException异常。
主要的类有ConcurrentHashMap, CopyOnWriteArrayList 和CopyOnWriteArraySet。
ConcurrentHashMap
hashtable比较低效,简单实用sys修饰符
基于分离锁实现,分段设计(segment)、锁定相应段。
不是整体扩容,单独对segment进行扩容。
分离锁副作用,size时,如果不锁定所有segment,有可能因为并发put导致结果不准确。如果锁定全部segment,开销很大。
ConcurrentSkipListMap
https://www.cnblogs.com/ygj0930/p/6543901.html
ConcurrentSkipListSet
CopyOnWriteArrayList
应用场景
CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。
1、由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致 young gc 或者 full gc;
2、不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set操作后,读取到数据可能还是旧的,虽然 CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;
CopyOnWriteArrayList 透露的思想
1、读写分离,读和写分开
2、最终一致性
3、使用另外开辟空间的思路,来解决并发冲突
CopyOnWriteArraySet
ForkJoinPool
ForkJoinTask
ForkJoinWorkerThread
FutureTask
LinkedBlockingDeque
CompletableFuture
Concurrency API里有哪些原子类?
int++并不是一个原子操作,所以当一个线程读取它的值并加1时,另外一个线程有可能会读到之前的值,这就会引发错误。
为了解决这个问题,必须保证增加操作是原子的,在JDK1.5之前我们可以使用同步技术来做到这一点。
到JDK1.5,java.util.concurrent.atomic包提供了int和long类型的装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。
Callable和Future?
Java 5在concurrency包中引入了java.util.concurrent.Callable 接口,它和Runnable接口很相似,但它可以返回一个对象或者抛出一个异常。
Callable接口使用泛型去定义它的返回类型。
Executors类提供了一些有用的方法去在线程池中执行Callable内的任务。
由于Callable任务是并行的,我们必须等待它返回的结果。
java.util.concurrent.Future对象为我们解决了这个问题。
在线程池提交Callable任务后返回了一个Future对象,使用它我们可以知道Callable任务的状态和得到Callable返回的执行结果。
Future提供了get()方法让我们可以等待Callable结束并获取它的执行结果。
Callable和Runnable
Java 5在concurrency包中引入了java.util.concurrent.Callable接口,它和Runnable接口很相似,但它可以返回一个对象或者抛出一个异常。
Callable
Callable规定的方法是call( )
call( )方法可以抛出异常
Callable的任务执行后可返回值
Runnable
Runnable规定的方法是run( )
run()方法不可以抛出异常
Runnable的任务是不能返回值的
ThreadFactory
为了统一在创建线程时设置一些参数,如是否守护线程。线程一些特性等,如优先级。通过这个TreadFactory创建出来的线程能保证有相同的特性。
https://blog.csdn.net/chenleixing/article/details/42583701
Concurrent、CopyOnWrite、Blocking三类容器有什么区别?
Blocking大部分实现基于锁,并提供等待性方法;
CopyOnWrite 之类容器修改开销相对较重;
Concurrent类型的容器:
基于lock-free 再常见的多线程访问场景,一般可以提供较高吞吐量;
往往提供了较低的遍历一致性,也就说当迭代器遍历时,修改了容器中的元素,而迭代器继续遍历;
读取的性能具有一定的不确定性;
链接
https://www.kancloud.cn/luoyoub/java-thread/1890460
JVM
内存模型【洗碗机】
JMM
内存划分
主内存:Java内存模型规定所有的变量都存储在主内存中,为线程公有。可以理解为RAM
工作内存:线程执行时为了加快速度会从主内存中copy一份,然后再工作内存中执行相应命令,最后再写回主内存。可以理解为Cache
内存间交互操作?
lock(锁定):作用主内存变量,标识该变量线程独占
unlock(解锁):作用于主内存变量,与lock相对应释放锁
read(读取):作用于主内存变量,直接从主内存中读取变量到工作内存中
load(载入):作用于工作内存变量,把read操作得到的变量值放入工作内存的变量副本中
use(使用):作用于工作内存变量,传递其到执行引擎,每当虚拟机遇到指令需要变量值的时候就会执行该操作。
assign(复制):作用于工作内存变量,把执行引擎结果存储到工作内存变量中,虚拟机每遇到赋值操作指令时会执行该操作。
store(存储):作用于工作内存变量,将其传送到主内存中,供write使用
write(写入):作用于主内存的变量,将store传入的变量值写入到主内存变量中
问题:
工作内存如何从主内存中读取变量?
工作内存如何向主内存写入变量?
long与dubbo的特殊规则
volatile的特殊规则
基本内存屏障:XY表示方法,禁止任何X操作与任何Y操作之间的重排序,即在Y之前,X的操作一定完成且对Y可见。
LoadLoad:禁止屏障之前任何的读操作都会都会在该屏障之后的任何读操作之前被加载。
LoadStore:确保该屏障之前任何一个读操作的数据都会在屏障之后的写操作之前。
StoreStore:禁止屏障之前任何的写操作的结果都会在屏障之后的任何一个写操作之前对其他处理器是可同步的,也就是第一个写操作要刷新到主内存中。
StoreLoad:确保屏障之前的任何写操作的结果都会在屏障之后的读操作之前对其他处理器来说是可同步的。
可见性保证
在对volatile变量写操作时,会在写操作之前插入一个StoreStore屏障,该指令能保证写操作之前的任何写与其发生重排序,然后在写操作之后加一条StoreLoad屏障,该屏障保证之前的写入操作与之后的读操作不会发生重排序。
在对volatile变量读操作时,在读取操作之前使用LoadLoad,使得处理器中获取到最新的volatile值,在读取之后,使用loadStroe屏障,该屏障禁止了volatile读之后的任何读写操作与其进行重排序。
禁止重排序
通过内存屏障禁止重排序,参考可见性讲解。
final的特殊规则
读操作:在读操作之前插入LoadLoad屏障,保证之前的读与当前读final不会发生重排序
写操作:在写之后插入StoreStore屏障,保证之后的写操作不会与当前的写操作发生重排序。
锁的特殊规则
synchronized / lock
JVM规定加锁前必须从主内存中刷新相应的值,解锁前,必须把共享变量的最新值刷新到主内存,因此保证了可见性。
实现原理
Java中每一个对象都有其对应的内部锁,也称作是监视器(monitor)
代码块:对象锁,monitorenter与monitorexit
方法:存在对应实例的对象头之中,对象头中专门有标志位标识是否有锁状态,对于静态方法则是当前的Class对象。
编译期间的优化
锁粗化:合并多个锁,不如StringBuffer的连续append
锁消除:消除不会产生竞争的锁,在一个方法中调用局部变量,不会产生竞争,这里的锁会消除掉。
运行期间的优化
偏向锁:当一个线程获取到锁时,在锁对应的对象头与栈帧之中变会记录当前线程的id,当该线程第二次来获取锁时,判断是否是上次取到锁的线程,是则直接给予锁,而不需要使用昂贵的CAS操作获取锁,不是的话则替换记录的线程id,升级为轻量级锁。
轻量级锁:在栈帧中创建存储锁的空间,将对象头的mark word复制到锁记录中,线程尝试把对象头的mark word替换为指向锁记录的指针,成功则获取锁,否则升级到自旋锁。
自旋锁:自旋的理论是锁的操作总是很快执行完毕,因此避免线程频繁唤醒与挂起,采取线程空转策略。
其他问题
有序性的保证与volatile原理一致,都是通过相关内存屏障来防止重排序。
锁能保证有序性与临界区不会发生重排序不是等价关系,该有序性是指对读线程来说是有序性保证,那么临界区内的代码是可以发生重排序的,其并不影响最终对读线程的结果。
Happen-Before规则:无法从其推导出顺序的可能会发生重排序
程序次序原则: 一个线程内按照代码顺序执行,写在前的操作先行发生于写在后面的操作,保证语义串行性
锁规则: 解锁必然发生在随后的加锁前
volatile规则: volatile变量的写先发生于读,保证其volatile变量的可见性
写volatile变量操作与该操作之前的任何读写不会发生重排序
读volatile变量操作与该操作之后的任何读写不会发生重排序
传递性: A先于B,B先于C,那么A必然先于C
线程的start()方法先于它的每一个动作
线程所有操作先于线程的终结(Thread.join)
线程中断先于被中断线程的代码
对象构造函数执行结束先于finalize方法
并发
原子性
即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
可见性
实现
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建-一个 工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
ps:
这里所讲的主内存、工作内存与Java内存区域中的Java堆、栈、方法区等并不是同一个层次的内存划分。如果两者一定要勉强对应起来,那从变量、主内存、工作内存的定义来看,主内存主要对应于Java堆中对象的实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。从更低的层次来说,主内存就是硬件的内存,而为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和告诉缓冲中。
同步
线程解锁前,必须把共享变量的值刷新回主内存
线程加锁前,必须读取主内存的最新值到自己的工作内存
加锁解锁是同一把锁
有序性
重排序
编译器优化的重排序
对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序
指令级并行的重排序。
现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
对于处理器重排序,JMM 的处理器重排序规则会要求 java 编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel 称之为 memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)
内存屏障
内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
happens-before
原则规定()
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
volatile关键字
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
volatile读前插读屏障,写后加写屏障,避免CPU重排导致的问题,实现多线程之间数据的可见性。
3)volatile关键字无法保证操作的原子性
例子
每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
使用了volatile关键字之后
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
volatile的原理和实现机制
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
它会强制将对缓存的修改操作立即写入主存;
如果是写操作,它会导致其他CPU中对应的缓存行无效。
使用volatile关键字的场景
条件
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
场景举例
标记状态量
双重验证
final关键字
final的重排序的规则
写final域的重排序的规则
JMM禁止编译器把final域的写重排序到构造函数之外;
编译器会在final域写之后,构造函数return之前,插入一个storestore屏障。这个屏障可以禁止处理器把final域的写重排序到构造函数之外。
即final关键字能保证变量在构造函数运行之内被成功写入,而普通域有可能在构造函数结束之后,类的变量并未被写入
读final域的重排序规则
在读一个对象的final域之前,一定会先读包含这个final域的对象的引用。在这个示例程序中,如果该引用不为null,那么引用对象的final域一定已经被A线程初始化过了。
而读普通域有可能被处理器安排在读取对象引用之前,那么这是一次错误的读取
final域为引用类型
在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
在构造函数中的intArray = new int[1]与线程a的新建类的实例化对象不能重排序,
构造函数中的intArray[0] = 1;和实例化也不能重排序
也就是说线程c一定能看到在构造函数对其final引用和该引用的成员变量的初始化,但不一定能看到b线程对其初始值的改变
内存区域【洗手台】
例图
1
组成
heap 堆 放 对象 也就是new 出来的东西
stack 栈 放局部变量
static segment 静态区 用来放 静态变量 和字符串常量
data segement 代码区 用来放代码的
String s = new srting(“123”); 请说明这段代码哪个在栈中,哪个是在堆内存?
系统内存一般情况来说分为四个
heap 堆 放 对象 也就是new 出来的东西
stack 栈 放局部变量
static segment 静态区 用来放 静态变量 和字符串常量
data segement 代码区 用来放代码的
如果 一个字符串是 String s = "abc";它放在栈里
如果一个字符串 用创建对象的方式 String s = new String("abc");
那它是放在了 堆里 而如果单纯的 一个 "abc" 这个输入字符串常量 是放在static segement里
JVM运行时数据区
图
热点代码及探测方式
当然是否需要启动JIT编译器将字节码直接编译为对应平台的本地机器指令,则需要根据代码被调用执行的频率而定。关于那些需要被编译为本地代码的字节码,也被称之为“热点代码”,JIT编译器在运行时会针对那些频繁被调用的“热点代码”做出深度优化,将其直接编译为对应平台的本地机器指令,以此提升Java程序的执行性能。
热点探测
采用基于计数器的热点探测,HotSpot VM将 会为每一个方法 都建立2个不同类型的计数器,分别为方法调用计数器( Invocation Counter)和回边计数器(BackEdge Counter) 。
➢方法调用计数器用于统计方法的调用次数
➢回边计数器则用于统计循环体执行的循环次数
热度衰减
如果不做任何没置,方法调用计数器統汁的并不是方法被凋用的絶対次数,而是一个相対的执行頻率,即一段吋同之内方法被調用的次数。
当超辻一定的时间限度,如果方法的調用次数仍然不足以让它提交蛤即吋編降器编译,那込个方法的凋用汁数器就会被緘少一-半, 这个过程称カ方法調用汁数器熱度的衰減(Counter Decay) ,而这段吋囘就称カ此方法統汁的半衰周期(Counter Half Life Time) 。
类加载器【】
双亲委派
模型
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。
这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。
类加载器双亲委派模型
工作过程
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成;
每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
JVM中存在三个默认的类加载器:
1. BootstrapClassLoader
2. ExtClassLoader
3. AppClassLoader
源码解析,有助于对双亲托管模型工作方式的了解
http://note.youdao.com/noteshare?id=38d0693ecf0187881d8aff26407615dc
参考链接
https://www.jianshu.com/p/5f79217f2e18
目的
解决了什么问题?
每一个类都只会被加载一次,避免了重复加载;
每一个类都会被尽可能的加载(从引导类加载器往下,每个加载器都可能会根据优先次序尝试加载它)
有效避免了某些恶意类的加载(比如自定义了Java.lang.Object类,一般而言在双亲委派模型下会加载系统的Object类而不是自定义的Object类)
能不能自己写个类,也叫java.lang.String?
可以,但无法运行;
因为最终都会由启动类加载器去加载,始终为加载jre.jar包里的类。
是否可以用自己的类加载器去加载?
即使自定义了自己的类加载器,强行用defineClass( )方法去加载一个以“java.lang”开头的类也不会成功。
如果尝试这样做的话,将会收到一个由虚拟机自己抛出的“java.lang.SecurityException:Prohibited package name:java.lang”异常
定义
通过一个类的全限定名来获取描述此类的二进制字节流,实现这个动作的代码模块称为“类加载器”。
虚拟机设计团队把类加载阶段中的“类加载动作”放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器都拥有一个独立的类名称空间。
这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
这里所指的“相等”,包括代表类的Class对象的equals( )方法、isAssignableFrom( )方法、isInstance( )方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。
分类
从Java虚拟机的角度来讲,加载器可分为两类
启动类加载器(Bootstrap ClassLoader)
负责加载Java的核心类,使用C++实现,是虚拟机自身的一部分
所有其它的加载器。
由Java语言实现,独立于虚拟机之外,全部继承自抽象类 java.lang.ClassLoader。
更细分的话,ClassLoader分为
启动类加载器(Bootstrap ClassLoader)
负责将%JAVA_HOME%/lib目录中或-Xbootclasspath中参数指定的路径中的,并且是虚拟机识别的(按名称)类库加载到JVM中
扩展类加载器(Extension ClassLoader)
负责加载%JAVA_HOME%/lib/ext中的所有类库
应用程序加载器(Application ClassLoader)
负责ClassPath中的类库
自定义类加载器
一个Java应用程序使用两种类型的类装载器
根装载器(bootstrap)和用户定义的装载器(user-defined)。
根装载器以某种默认的方式将类装入,包括那些Java API的类。在运行期间一个Java程序能安装用户自己定义的类装载器。根装载器是虚拟机固有的一部分,而用户定义的类装载器则不是,它是用Java语言写的,被编译成class文件之后然后再被装入到虚拟机,并像其它的任何对象一样可以被实例化。
Java类装载器的体系结构如下所示
Bootstrap(根装载器)
Bootstrap(根装载器)
Extension (扩展装载器) | System | UserDefine1 / UserDefine2 UserDefine3 |
UserDefine4Java的类装载模型是一种代理(delegation)模型。
当JVM 要求类装载器CL(ClassLoader)装载一个类时,CL首先将这个类装载请求转发给他的父装载器。只有当父装载器没有装载并无法装载这个类时,CL才获得装载这个类的机会。这样, 所有类装载器的代理关系构成了一种树状的关系。
树的根是类的根装载器(bootstrap ClassLoader) , 在JVM 中它以”null”表示。除根装载器以外的类装载器有且仅有一个父装载器。在创建一个装载器时, 如果没有显式地给出父装载器, 那么JVM将默认系统装载器为其父装载器
下面针对各种类装载器分别进行详细的说明
根(Bootstrap) 装载器:该装载器没有父装载器,它是JVM实现的一部分,从sun.boot.class.path装载运行时库的核心代码。
扩展(Extension) 装载器:继承的父装载器为根装载器,不像根装载器可能与运行时的操作系统有关,这个类装载器是用纯Java代码实现的,它从java.ext.dirs (扩展目录)中装载代码。
系统(System or Application) 装载器:装载器为扩展装载器,我们都知道在安装JDK的时候要设置环境变量(CLASSPATH),这个类装载器就是从java.class.path(CLASSPATH 环境变量)中装载代码的,它也是用纯Java代码实现的,同时还是用户自定义类装载器的缺省父装载器。
小应用程序(Applet) 装载器: 装载器为系统装载器,它从用户指定的网络上的特定目录装载小应用程序代码。
JVM装载class文件?
所谓装载就是寻找一个类或是一个接口的二进制形式并用该二进制形式来构造代表这个类或是这个接口的class对象的过程。
将二进制类数据构造成类class对象的过程,成为类装载。
类装载器把一个类装入Java虚拟机中,要经过哪几个步骤?
装载
查找和导入类或接口的二进制数据
链接(执行下面的校验、准备和解析步骤,其中解析步骤是可以选择的)
检验
检查导入类或接口的二进制数据的正确性;
准备
给类的静态变量分配并初始化存储空间;
解析
将符号引用转成直接引用;
初始化
激活类的静态变量的初始化Java代码和静态Java代码块,JVM中类的装载是由ClassLoader和它的子类来实现的,Java ClassLoader 是一个重要的Java运行时系统组件。它负责在运行时查找和装入类文件的类。
调优【化妆品】
导图
JVM何时启动、何时终止?
JVM在执行Java命令时启动。
JVM终止运行的条件
调用了exit( )方法,并且exit( )有权限被正常执行。
所有的“非守护线程”都死了(即JVM中仅仅只有“守护线程”)。
调优工具
所有工具都在 jdk目录/Contents/Home/bin 目录下
jps 虚拟机统计信息监视工具
jinfo java配置信息工具
jmap java内存映像工具
jhat 虚拟机栈堆存储快照分析工具
jstack java堆栈跟踪工具
HSDIS JIT生成代码反汇编
JConsole Java监视与管理控制台
VisualVM 多合一故障处理工具
JVM优化
http://www.100mian.com/mianshi/java/4828.html
调整JVM启动时候堆内存设置。在32位操作系统下只能够设置到4G,但是在64位操作系统下已经可以设置到8G甚至更大的值。
其中JVM启动的主要控制参数说明如下:
-Xmx设置最大堆空间
-Xms设置最小堆空间
-XX:MaxNewSize设置最大新生代空间
-XX:NewSize设置最小新生代空间
-XX:MaxPermSize设置最大永久代空间(注:新内存模型已经替换为Metaspace)
-XX:PermSize设置最小永久代空间(注:新内存模型已经替换为Metaspace)
-Xss设置每个线程的堆栈大小
那么这些值究竟设置多大合适,具体来讲:
Java整个堆大小设置,Xmx 和 Xms设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍。永久代 PermSize和MaxPermSize设置为老年代存活对象的1.2-1.5倍。
年轻代Xmn的设置为老年代存活对象的1-1.5倍。
老年代的内存大小设置为老年代存活对象的2-3倍。
注意在新的JVM内存模型下已经没有PermSize而是变化为Metaspace,因此需要考虑Heap内存和Metaspace大小的配比,同时还需要考虑相关的垃圾回收机制是采用哪种类型等。
Thread Dump
Thread Dump是非常有用的诊断Java应用问题的工具,它是一个JVM活动线程的列表,它对于分析系统瓶颈和死锁非常有用。
每一个Java虚拟机都有及时生成显示所有线程在某一点状态的thread-dump的能力。
有很多方法可以获取线程转储
使用Profiler,Kill -3命令,jstack工具等等。
推荐使用jstack工具,因为它容易使用并且是JDK自带的。
由于它是一个基于终端的工具,所以我们可以编写一些脚本去定时的产生线程转储以待分析。
Thread Dump特点
能在各种操作系统下使用
能在各种Java应用服务器下使用
可以在生产环境下使用而不影响系统的性能
可以将问题直接定位到应用程序的代码行上
Thread Dump能诊断的问题包括
查找内存泄露,常见的是程序里load大量的数据到缓存
发现死锁线程
有很多方式可用于获取ThreadDump, 下面列出一部分获取方式:
操作系统命令获取ThreadDump
Windows
转向服务器的标准输出窗口并按下Control + Break组合键, 之后需要将线程堆栈复制到文件中;
UNIX/ Linux
首先查找到服务器的进程号(process id), 然后获取线程堆栈.
1. ps –ef | grep java
2. kill -3 <pid>
注意:一定要谨慎, 一步不慎就可能让服务器进程被杀死。kill -9 命令会杀死进程。
JVM 自带的工具获取线程堆栈
JDK自带命令行工具获取PID,再获取ThreadDump
jps 或 ps –ef|grepjava (获取PID)
jstack [-l ]<pid> | tee -a jstack.log (获取ThreadDump)
GC【马桶】
图
GC过程。
在GC开始的时候,对象只会存在于Eden区和名为 “From” 的Survivor区,Survivor区 “To” 是空的。
进行GC,Eden区中所有存活的对象都会被复制到 “To”,而在 “From” 区中,仍存活的对象会根据他们的年龄值来决定去向。
年龄达到阈值的对象会被移动到年老代中;
没有达到阈值的对象会被复制到“To”区域;
年龄阈值
可以通过-XX:MaxTenuringThreshold来设置,默认为15;
每个对象在坚持过一次Minor GC之后,年龄就加1。
经过这次GC后,Eden区和From区已经被清空。
这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。
不管怎样,都会保证名为To的Survivor区域是空的。
Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
画图解释一下年轻代、年老代、比例分配及为啥要这样分代回收。
HotSpot JVM把年轻代分为了三部分
1个Eden区
2个Survivor区(一个from和一个to)
默认比例为8:1
图1
https://blog.csdn.net/wy5612087/article/details/52369677/
一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理);
这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。
对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。
年轻代的垃圾回收算法使用的是复制算法;
基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。
复制算法不会产生内存碎片。
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。
紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”;
在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。
年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中;
没有达到阈值的对象会被复制到“To”区域。
经过这次GC后,Eden区和From区已经被清空。
这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。
不管怎样,都会保证名为To的Survivor区域是空的。
Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
可达性分析
回收前,第一件事情就是要确定堆中的对象哪些还“存活”着,哪些已经“死去”(即不可能再被任何途径使用的对象)。
主流的JVM里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。
通过可达性分析算法来判断对象是否存活。
这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点;
从这些节点开始向下搜索;
搜索所走过的路径称为引用链(Reference Chain);
当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
可达性分析算法示意图
哪些对象可以作为GC Roots?
虚拟机栈(栈桢中的本地变量表)中的引用的对象
方法区中的类静态属性引用的对象
方法区中的常量引用的对象
本地方法栈中JNI的引用的对象
https://blog.csdn.net/bolg_hero/article/details/79344745
如果排查内存溢出的情况?
Java中定义了哪几种引用?
JDK 1.2之后
强引用(strong Reference)
类似“Object obj=new Object()”这类的引用
只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用(Soft Reference)
对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
弱引用(Weak Reference)
被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
虚引用(Phantom Reference)
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
GC相关的JVM参数有哪些?
-XX:NewSize和-XX:MaxNewSize
用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。
-XX:SurvivorRatio
用于设置Eden和其中一个Survivor的比值,这个值也比较重要。
-XX:+PrintTenuringDistribution
这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小。
-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold
用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1。
如果把一个对象引用设置为 null,虚拟机是否会马上释放其占用的内存?
不会马上释放内存。
如果对象的引用被置为null,只是断开了当前线程栈帧中对该对象的引用关系,而垃圾收集器是运行在后台的线程,只有当用户线程运行到安全点(safe point)或者安全区域才会扫描对象引用关系,扫描到对象没有被引用则会标记对象,这时候仍然不会立即释放该对象内存,因为有些对象是可恢复的(在 finalize方法中恢复引用 )。
只有确定了对象无法恢复引用的时候才会清除对象内存。
线程的安全点和安全区域指什么?
Java编程如何避免内存溢出?
尽早释放无用对象的引用(XX = null;)
谨慎使用集合数据类型,如数组,树,图,链表等数据结构,这些数据结构对GC来说回收更复杂。
避免显式申请数组空间,不得不显式申请时,尽量准确估计其合理值。
尽量避免在类的默认构造器中创建、初始化大量的对象,防止在调用其自类的构造器时造成不必要的内存资源浪费
尽量避免强制系统做垃圾内存的回收,增长系统做垃圾回收的最终时间
尽量做远程方法调用类应用开发时使用瞬间值变量,除非远程调用端需要获取该瞬间值变量的值。
尽量在合适的场景下使用对象池技术以提高系统性能
调用 system.gc( ) 是否会立即回收垃圾?如何强制垃圾回收?
不保证立即执行。
不能强制进行垃圾回收, 但是可以通过System.gc( )来请求垃圾回收,JVM不保证垃圾回收立即执行。
内存的优化?
内存泄露容易导致内存溢出,又称为OOM。
内存优化策略
在循环内尽量不要使用局部变量
不用的对象即时释放,即指向NULL
数据库的cursor即时关闭。
构造adapter时使用缓存contentview
调用registerReceiver()后在对应的生命周期方法中调用unregisterReceiver()
即时关闭InputStream/OutputStream。
android系统给图片分配的内存只有8M, 图片尽量使用软引用, 较大图片可通过BitmapFactory缩放后再使用,并及时recycle
尽量避免static成员变量引用资源耗费过多的实例。
GC内存泄露在什么情况下会出现?怎么解决?
查询数据库没有关闭游标
构造Adapter时,没有使用缓存的 convertView
Bitmap对象不在使用时调用recycle()释放内存
不用的对象没有及时释放对象的引用
内存分配【化妆柜】
Java内存模型
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。
此处的变量与Java编程时所说的变量不一样,指包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,不会被共享。
Java内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成,线程、主内存和工作内存的交互关系如下图所示,和上图很类似。
图1:这里的主内存、工作内存与Java内存区域的Java堆、栈、方法区不是同一层次内存划分。
Java内存模型
What?
Java内存模型规定Java虚拟机JVM在计算机内存(RAM)中的执行方式。
Why?
因为Java是跨平台语言,那么要执行就要屏蔽掉不同的操作系统以及硬件在内存上的访问差异,因此在计算机之上建立一套属于Java的内存模型,使得Java程序员只需要关心这一点就足够了。
HOW?
CPU缓存概念
RAM:属于多核CPU下共享内存区域,也是主要存储内存的地方。
Cache:高速缓存,现在多核CPU下缓存一般为三级,且每个CPU独享自己的缓存,
CPU Core:CPU寄存器缓存,高速缓存之上,与CPU绑定,属于速度最快的缓存区域。
问题
为什么需要缓存?
缓存之间一致性问题?
乱序执行优化
内存间有哪些交互操作协议?
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节。
Java内存模型定义了以下八种操作来完成
lock(锁定)
作用于主内存的变量,把一个变量标识为一条线程独占状态。
unlock(解锁)
作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read(读取)
作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
load(载入)
作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用)
作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
assign(赋值)
作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储)
作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
write(写入)
作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。也就是read和load之间,store和write之间是可以插入其他指令的,如对主内存中的变量a、b进行访问时,可能的顺序是read a,read b,load b, load a。
Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则
不允许read和load、store和write操作之一单独出现
不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现
如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。
指令重排
为了使得处理器内部的运算单元能尽可能被充分利用,处理器可能会对输入代码进行乱起执行(Out-Of-Order Execution)优化,处理器会在计算之后将对乱序执行的代码进行结果重组,保证结果准确性。与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Recorder)优化。
在执行程序时为了提高性能,编译器和处理器经常会对指令进行重排序。
重排序分成三种类型
编译器优化的重排序
编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序。
指令级并行的重排序
现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
内存系统的重排序
由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
从Java源代码到最终实际执行的指令序列,会经过下面三种重排序
内存屏障
为了保证内存的可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。
Java内存模型把内存屏障分为LoadLoad、LoadStore、StoreLoad和StoreStore四种
IO
BIO、 NIO 、AIO
BIO:一个连接一个线程,客户端有连接请求时服务器端就需要启动一个线程进行处理。线程开销大。 伪异步 IO:将请求连接放入线程池,一对多,但线程还是很宝贵的资源。
NIO:一个请求一个线程,但客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求时才启动一个线程进行处理。
AIO:一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理,
异同
BIO是面向流的,NIO 是面向缓冲区的;
BIO 的各种流是阻塞的。而NIO是非阻塞的;
BIO的 Stream 是单向的,而NIO的channel 是双向的。
NIO
NIO 的特点:
事件驱动模型、单线程处理多任务、非阻塞 I/O, I/O 读写不再阻塞,而是返回 0、
基于 block 的传输比基于流的传输更高效、更高级的 IO 函数 zero-copy、
IO 多路复用大大提高了 Java 网络应用的可伸缩性和实用性。
基于 Reactor 线程模型。
在 Reactor 模式中,事件分发器等待某个事件或者可应用或个操作的状态发生,事件分发器就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。
如在 Reactor 中实现读:注册读就绪事件和相应的事件处理器、事件分发器等待事件、事件到来,激活分发器,分发器调用事件对应的处理器、事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。
IO与NIO的区别
IO
面向流
面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。
它不能前后移动流中的数据。
阻塞IO
NIO
面向缓冲
Java NIO的缓冲导向方法略有不同。
数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。
非阻塞IO
选择器
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
面向流和面向Buffer的区别是什么?
例如IO是面向流,NIO是面向buffer
面向流
每次从流中读一个或多个字节,读取所有字节,并直接对流进行操作,它们没有被缓存在任何地方。
它不能前后移动流中的数据。
面向buffer
会先申请一块堆外内存,每次读取的数据放在该内存中,然后对该内存进行操作。
增加了处理数据的灵活性,当然也增加了操作的复杂度。
读取文件方式
NIO2提供两种主要的文件读取方法:
使用buffer和channel类
使用Path 和 File 类
NIO读取文件有以下三种方式
http://www.100mian.com/mianshi/java/1210.html
类和方法?
Channels
FileChannel 从文件中读写数据。
DatagramChannel 通过UDP读写网络中的数据。
SocketChannel 通过TCP读写网络中的数据。
ServerSocketChannel 可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
Buffers
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
MappedByteBuffer 用于表示内存映射文件
Selectors
Selector允许单线程处理多个 Channel。
http://ifeve.com/overview/
https://www.cnblogs.com/xiaonantianmen/p/9130428.html
组成
Buffer:与 Channel 进行交互,数据是从 Channel 读入缓冲区,从缓冲区写入 Channel 中的 flip 方法 : 反转此缓冲区,将 position 给 limit,然后将 position 置为 0,其实就是切换读写模式
clear 方法 :清除此缓冲区,将 position 置为 0,把 capacity 的值给 limit。
rewind 方法 : 重绕此缓冲区,将 position 置为0,DirectByteBuffer 可减少一次系统空间到用户空间的拷贝。但 Buffer 创建和销毁的成本更高,不可控,通常会用内存池来提高性能。直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。如果数据量比较小的中小应用情况下,可以考虑使用 heapBuffer,由 JVM 进行管理。
Channel:表示 IO 源与目标打开的连接,是双向的,但不能直接访问数据,只能与 Buffer进行交互。通过源码可知, FileChannel 的 read 方法和 write 方法都导致数据复制了两次! Selector 可使一个单独的线程管理多个 Channel, open 方法可创建 Selector, register 方法向多路复用器器注册通道,可以监听的事件类型:读、写、连接、 accept。
注册事件后会产生一个 SelectionKey:它表示 SelectableChannel 和 Selector 之间的注册关系,
wakeup 方 法:使尚未返回的第一个选择操作立即返回,唤醒的原因是:注册了新的 channel 或者事件; channel 关闭,取消注册;优先级更高的事件触发(如定时器事件),希望及时处理。 Selector 在 Linux 的实现类是 EPollSelectorImpl,委托给 EPollArrayWrapper 实现,其中三个native 方法是对 epoll 的封装,而 EPollSelectorImpl. implRegister 方法,通过调用 epoll_ctl向 epoll 实例中注册事件,还将注册的文件描述符(fd)与 SelectionKey 的对应关系添加到fdToKey 中,这个 map 维护了文件描述符与 SelectionKey 的映射。 fdToKey 有时会变得非常大,因为注册到 Selector 上的 Channel 非常多(百万连接);过期或失效的 Channel 没有及时关闭。 fdToKey 总是串行读取的,而读取是在 select 方法中进行的,该方法是非线程安全的。 Pipe:两个线程之间的单向数据连接,数据会被写到 sink 通道,从 source 通道读取 NIO 的服务端建立过程: Selector.open():打开一个 Selector; ServerSocketChannel.open():创建服务端的 Channel; bind():绑定到某个端口上。并配置非阻塞模式; register():注册Channel 和关注的事件到 Selector 上; select()轮询拿到已经就绪的事件
NIO非阻塞网络编程原理分析:
(Selector、SelectionKey、ServerSocketChannel、SocketChannel)关系。
1、当客户端连接时,会通过ServerSocketChannel得到SocketChannel
2、将SocketChannel注册到Selector上
3、注册后返回一个SelectionKey,会和该Selector关联(集合)
4、Selector进行监听(用select方法),返回有事件发生的通道的个数
5、进一步得到各个SelectionKey(有事件发生)
6、再通过SelectionKey反向获取SocketChannel
7、可以通过得到的Channel完成业务处理
tu
1
文件
文件和目录(I/O)操作
file.listFiles( ) 可以获取所有文件和目录;
file.isFile( ) 和 file.isDirectory() 来判断是文件还是目录;
file.exists( ) 可以判断文件或目录是否存在;
用FileInputStream来读取文件;
用FileWriter来写文件;
https://www.cnblogs.com/fnlingnzb-learner/p/6010165.html
Java的文件分隔符有哪些?
File类中定义的静态常量
pathSeparator
与系统有关的路径分隔符,为了方便,它被表示为一个字符串。
pathSeparatorChar
与系统有关的路径分隔符。
separator
与系统有关的默认名称分隔符,为了方便,它被表示为一个字符串。
separatorChar
与系统有关的默认名称分隔符。
Java如何获取系统目录?
System类的静态方法getProperties和setProperties可以用于获取和设置系统目录
调用方式:System.getProperties().getProperty(String name);
name的备选值如下
java.version Java运行时环境版本
java.vendor Java运行时环境供应商
java.vendor.url Java供应商的 URL
java.home Java安装目录
java.vm.specification.version Java虚拟机规范版本
java.vm.specification.vendor Java虚拟机规范供应商
java.vm.specification.name Java虚拟机规范名称
java.vm.version Java虚拟机实现版本
java.vm.vendor Java虚拟机实现供应商
java.vm.name Java虚拟机实现名称
java.specification.version Java运行时环境规范版本
java.specification.vendor Java运行时环境规范供应商
java.specification.name Java运行时环境规范名称
java.class.version Java类格式版本号
java.class.path Java类路径
java.library.path 加载库时搜索的路径列表
java.io.tmpdir 默认的临时文件路径
java.compiler 要使用的 JIT 编译器的名称
java.ext.dirs 一个或多个扩展目录的路径
os.name 操作系统的名称
os.arch 操作系统的架构
os.version 操作系统的版本
file.separator 文件分隔符(在 UNIX 系统中是“/”)
path.separator 路径分隔符(在 UNIX 系统中是“:”)
line.separator 行分隔符(在 UNIX 系统中是“/n”)
user.name 用户的账户名称
user.home 用户的主目录
user.dir 用户的当前工作目录
使用Java NIO包处理上G的大文件
http://www.100mian.com/mianshi/java/1182.html
字节流与字符流
在程序中所有的数据都是以流的方式进行传输或保存的,程序需要数据的时候要使用输入流读取数据,而当程序需要将一些数据保存起来的时候,就要使用输出流完成。
程序中的输入输出都是以流的形式保存的,流中保存的实际上全都是字节文件。
在java.io包中操作文件内容的主要有两大类:字节流、字符流,两类都分为输入和输出操作。
InputStream 和OutputStream两个是为字节流设计的,主要用来处理字节或二进制对象;
Reader和Writer两个是为字符流(一个字符占两个字节)设计的,主要用来处理字符或字符串;
字节流在操作时本身不会用到缓冲区(内存),是文件本身直接操作的;
字符流在操作时使用了缓冲区,通过缓冲区再操作文件;
字符流处理的单元为2个字节的Unicode字符,分别操作字符、字符数组或字符串;
字节流处理单元为1个字节,操作字节和字节数组。
字符流是由JVM将字节转化为2个字节的Unicode字符为单位的字符而成的,所以它对多国语言支持性比较好!如果是关系到中文(文本)的,用字符流好点。
如果是音频文件、图片、歌曲,就用字节流好点。
字节流可用于任何类型的对象,包括二进制对象;字符流只能处理字符或者字符串;
字节流提供了处理任何类型的IO操作的功能,但它不能直接处理Unicode字符,而字符流就可以;
https://www.cnblogs.com/DONGb/p/7844123.html
Writer / Reader 和 OutStream / InputStream
FileReader、FileWriter分别是对文本文件的读写使用的封装了一些字符集操作。
FileInputStream/FileOutputStream是集成InputStream/OutputStream的文件读写流,用于读写任何文件,可以作为构造其他InputStream/OutputStream的基础。
FileWriter是字符流,文本类的可以用它效率高。
FileOutputStream 是字节流,任何类型都可以用它操作。操作文本内容肯定没Writer快。
Reader类的read( )方法返回类型为int :作为整数读取的字符(占两个字节共16位),范围在 0 到 65535 之间 (0x00-0xffff),如果已到达流的末尾,则返回 -1;
inputStream的read()虽然也返回int,但由于此类是面向字节流的,一个字节占8个位,所以返回 0 到 255 范围内的 int 字节值。如果因为已经到达流末尾而没有可用的字节,则返回值 -1。因此对于不能用0-255来表示的值就得用字符流来读取!比如说汉字;
FileInputStream有哪些方法?
read
close 关闭文件流
available 获取字节数
getFD
getChannel
skip
mark
markSupported
reset
对象序列化
什么是对象序列化?
序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。
可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。
序列化的缺陷
不能跨语言
序列化体积大
使用场景如远程服务调用RPC
https://blog.csdn.net/do_bset_yourself/article/details/77173143?locationNum=7&fps=1
https://blog.csdn.net/liji_xc/article/details/47290695
对象序列化id是干嘛用的?
序列化ID决定着是否能够成功反序列化。
Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。
在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地实体类中的serialVersionUID进行比较,如果相同则认为是一致的,便可以进行反序列化,否则就会报序列化版本不一致的异常。
当我们一个实体类中没有显式的定义一个名为“serialVersionUID”、类型为long的变量时。
Java序列化机制会根据编译时的class自动生成一个serialVersionUID作为序列化版本比较,这种情况下,只有同一次编译生成的class才会生成相同的serialVersionUID。譬如,当我们编写一个类时,随着时间的推移,我们因为需求改动,需要在本地类中添加其他的字段,这个时候再反序列化时便会出现serialVersionUID不一致,导致反序列化失败。
那么如何解决呢?
便是在本地类中添加一个“serialVersionUID”变量,值保持不变,便可以进行序列化和反序列化。
虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)。
https://blog.csdn.net/liji_xc/article/details/47302455
如何实现对象序列化?
JRE本身就提供了这种支持,我们可以调用OutputStream的writeObject方法来做,如果要让Java帮我们做,要被传输的对象必须实现serializable接口,这样,javac编译时就会进行特殊处理,编译的类才可以被writeObject方法操作,这就是所谓的序列化。
需要被序列化的类必须实现Serializable接口,该接口是一个mini接口,其中没有需要实现的方法,Implements Serializable只是为了标注该对象是可被序列化的。
将需要被序列化的类实现Serializable接口,该接口没有需要实现的方法,Implements Serializable只是为了标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个 ObjectOutputStream(对象流)对象,接着,使用ObjectOutputStream对象的writeObject(Object obj)方法就可以将参数为obj的对象写出(即保存其状态),要恢复的话则用输入流。
对象实现 java.io.Serializable 接口,然后将对象进行文件读写操作,或网络流传输操作即可,Java在传输过程中会自动将对象进行序列化。
序列化(写文件)
outputStream=new ObjectOutputStream(new FileOutputStream("E:/hello.txt"));
outputStream.writeObject(person);
反序列化(读文件)
inputStream = new ObjectInputStream(new FileInputStream("E:/hello.txt"));
person = (Person) inputStream.readObject();
**序列化协议 **
序列化(编码)是将对象序列化为二进制形式(字节数组),主要用于网络传输、数据持久化等;而反序列化(解码)则是将从网络、磁盘等读取的字节数组还原成原始对象,主要用于网络传输对象的解码,以便完成远程调用。 影响序列化性能的关键因素:序列化后的码流大小(网络带宽的占用)、序列化的性能( CPU 资源占用);是否支持跨语言(异构系统的对接和开发语言切换)。 Java 默认提供的序列化:无法跨语言、序列化后的码流太大、序列化的性能差XML, 优点:人机可读性好,可指定元素或特性的名称。 缺点:序列化数据只包含数据本身以及类的结构,不包括类型标识和程序集信息;只能序列化公共属性和字段;不能序列化方法;文件庞大,文件格式复杂,传输占带宽。 适用场景:当做配置文件存储数据,实时数据转换。
JSON,是一种轻量级的数据交换格式 优点:兼容性高、数据格式比较简单,易于读写、序列化后数据较小,可扩展性好,兼容性好、与 XML 相比,其协议比较简单,解析速度比较快。 缺点:数据的描述性比 XML 差、不适合性能要求为 ms 级别的情况、额外空间开销比较大。 适用场景(可替代XML):跨防火墙访问、可调式性要求高、基于 Webbrowser 的 Ajax 请求、传输数据量相对小,实时性要求相对低(例如秒级别)的服务。
Fastjson,采用一种“假定有序快速匹配”的算法。 优点:接口简单易用、目前 java 语言中最快的 json 库。 缺点:过于注重快,而偏离了“标准”及功能性、代码质量不高,文档不全。 适用场景:协议交互、 Web 输出、 Android 客户端
Thrift,不仅是序列化协议,还是一个 RPC 框架。 优点:序列化后的体积小, 速度快、支持多种语言和丰富的数据类型、对于数据字段的增删具有较强的兼容性、支持二进制压缩编码。 缺点:使用者较少、跨防火墙访问时,不安全、不具有可读性,调试代码时相对困难、不能与其他传输层协议共同使用(例如 HTTP)、无法支持向持久层直接读写数据,即不适合做数据持久化序列化协议。 适用场景:分布式系统的 RPC 解决方案
Avro, Hadoop 的一个子项目,解决了 JSON 的冗长和没有 IDL 的问题。 优点:支持丰富的数据类型、简单的动态语言结合功能、具有自我描述属性、提高了数据解析速度、快速可压缩的二进制数据形式、可以实现远程过程调用 RPC、支持跨编程语言实现。 缺点:对于习惯于静态类型语言的用户不直观。 适用场景:在 Hadoop 中做 Hive、 Pig 和 MapReduce的持久化数据格式。
Protobuf,将数据结构以.proto 文件进行描述,通过代码生成工具可以生成对应数据结构的POJO 对象和 Protobuf 相关的方法和属性。 优点:序列化后码流小,性能高、结构化数据存储格式( XML JSON 等)、通过标识字段的顺序,可以实现协议的前向兼容、结构化的文档更容易管理和维护。 缺点:需要依赖于工具生成代码、支持的语言相对较少,官方只支持Java 、 C++ 、 python。 适用场景:对性能要求高的 RPC 调用、具有良好的跨防火墙的访问属性、适合应用层对象的持久化
其它 protostuff 基于 protobuf 协议,但不需要配置 proto 文件,直接导包即可 Jboss marshaling 可以直接序列化 java 类, 无须实java.io.Serializable 接口 Message pack 一个高效的二进制序列化格式 Hessian 采用二进制协议的轻量级 remoting onhttp 工具 kryo 基于 protobuf 协议,只支持 java 语言,需要注册( Registration),然后序列化( Output),反序列化( Input)
网络
三次握手和四次挥手
图解HTTP
简单示意图:
客户端–发送带有 SYN 标志的数据包–一次握手–服务端
服务端–发送带有 SYN/ACK 标志的数据包–二次握手–客户端客户端–发送带有带有 ACK 标志的数据包–三次握手–服务端
为什么要三次握手?
三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是 双方确认自己与对方的发送与接收是正常的。
第一次握手:Client 什么都不能确认;Server 确认了对方发送正常
第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己接收正常,对方发送正常
第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常, 对方发送接收正常
所以三次握手就能确认双发收发功能都正常,缺一不可。
为什么要传回 SYN
接收端传回发送端所发送的 SYN 是为了告诉发送端,我接收到的信息确实就是你所发送的信号了。
SYN 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 TCP 网络连接时,客户机首先发出一个 SYN 消息,服务器使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以ACK(Acknowledgement[汉译:确认字符 ,在数据通信传输中,接收站发给发送站的一种传输控制字符。它表示确认发来的数据已经接受无误。 ])消息响应。这样在客户机和服务器之间才能建立起可靠的TCP连接,数据才可以在客户机和服务器之间传递。
-
传了 SYN,为啥还要传 ACK
双方通信无误必须是两者互相发送信息都无误。传了 SYN,证明发送方到接收方的通道没有问题,但是接收方到发送方的通道还需要 ACK 信号来进行验证。
断开一个 TCP 连接则需要“四次挥手”:
客户端-发送一个 FIN,用来关闭客户端到服务器的数据传送
服务器-收到这个 FIN,它发回一 个 ACK,确认序号为收到的序号加1 。和 SYN 一样,一个 FIN 将占用一个序号服务器-关闭与客户端的连接,发送一个FIN给客户端
客户端-发回 ACK 报文确认,并将确认序号设置为收到序号加1
为什么要四次挥手
任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送 的时候,则发出连接释放通知,对方确认后就完全关闭了TCP连接。
举个例子:A 和 B 打电话,通话即将结束后,A 说“我没啥要说的了”,B回答“我知道了”,但是 B 可能还会有要说的话,A 不能要求 B 跟着自己的节奏结束通话,于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”,A 回答“知道了”,这样通话才算结束。
io模式
1.多线程同步:TCE,YAAF
2.纯异步haproxy,redis,nodejs
3.半同步半异步tce
4.多进程同步spp,fastcgi
5.多线程异步memcached
6.多进程异步nginx
7.微进程框架erlang,go
8.不推荐使用的模式每请求每进程(线程)APACHE
Reactor(反应堆):非阻塞同步网络模型
I/O多路复用
条连接共用一个阻塞对象
实现方式
select
epoll
kqueue
当某条连接有新的数据可以处理,操作系统通知进程
三种实用模式
Java 语言一般使用线程(例如,Netty),C 语言使用进程和线程都可以。例如,Nginx 使用进程,Memcache 使用线程。
单 Reactor 单进程 / 线程
优点
没有进程间的通信
没有进程竞争
缺点
无法发挥多核CPU的性能
Handler在处理某个连接上的业务时,整个进程无法处理其他连接的事件
只适用于业务处理非常快速的场景
单 Reactor 多线程
图
流程
主线程中,Reactor 对象通过 select 监控连接事件,收到事件后通过 dispatch 进行分发。
如果是连接建立的事件,则由 Acceptor 处理,Acceptor 通过 accept 接受连接,并创建一个 Handler 来处理连接后续的各种事件。
如果不是连接建立事件,则 Reactor 会调用连接对应的 Handler(第 2 步中创建的Handler)来进行响应。Handler 只负责响应事件,不进行业务处理;
Handler 通过 read 读取到数据后,会发给Processor 进行业务处理。
Processor 会在独立的子线程中完成真正的业务处理,然后将响应结果发给主进程的Handler 处理;
Handler 收到响应后通过 send 将响应结果返回给 client。
缺点
多线程数据共享访问比较复杂
Reactor 承担所有事件的监听和响应,瞬间高并发时会成为性能瓶颈
多 Reactor 多进程 / 线程
父进程中 mainReactor 对象通过 select 监控连接建立事件,收到事件后通过 Acceptor接收,将新的连接分配给某个子进程。
子进程的 subReactor 将 mainReactor 分配的连接加入连接队列进行监听,并创建一个Handler 用于处理连接的各种事件。
当有新的事件发生时,subReactor 会调用连接对应的 Handler(即第 2 步中创建的Handler)来进行响应。
Handler 完成 read→业务处理→send 的完整业务流程。
图
优点
父子进程的职责非常明确,父进程只负责接收新连接,子进程负责完成后续的业务处理
父进程和子进程的交互很简单,父进程只需要把新连接传给子进程,子进程无须返回数据。
子进程之间是互相独立的,无须同步共享之类的处理(这里仅限于网络模型相关的select、read、send 等无须同步共享,“业务处理”还是有可能需要同步共享的)
Memcache 和 Netty