动态规划

斐波那契数列Fibonacci Sequence

F(0)=1,F(1)=1,F(n)=F(n-1)+F(n-2)

1
2
3
4
5
6
7
8
9
int fib(int n){
if(n==0){
return 0;
}
if(n==1){
return 1;
}
return fib(n-1)+fib(n-2);
}

记忆化搜索

自上向下的解决问题

Json详解以及fastjson使用教程

Json详解以及fastjson使用教程

Json是一种轻量级的数据交换格式,采用一种“键:值”对的文本格式来存储和表示数据,在系统交换数据过程中常常被使用,是一种理想的数据交换语言。在使用Java做Web开发时,不可避免的会遇到Json的使用。下面我们就简单讲一下Json的使用以及fastjson.jar包的使用。

JSON形式与语法

JSON对象

我们先来看以下数据

1
2
3
4
5
{
"ID": 1001,
"name": "张三",
"age": 24
}

第一个数据就是一个Json对象,观察它的数据形式,可以得出以下语法:

1:数据在花括号中

2:数据以”键:值”对的形式出现(其中键多以字符串形式出现,值可取字符串,数值,甚至其他json对象)

3:每两个”键:值”对以逗号分隔(最后一个”键:值”对省略逗号)

遵守上面3点,便可以形成一个json对象。

JSON对象数组

接下来我们再看第二个数据,观察它的数据形式,可以得出以下语法

1
2
3
4
5
[
{"ID": 1001, "name": "张三", "age": 24},
{"ID": 1002, "name": "李四", "age": 25},
{"ID": 1003, "name": "王五", "age": 22}
]

1:数据在方括号中(可理解为数组)

2:方括号中每个数据以json对象形式出现

3:每两个数据以逗号分隔(最后一个无需逗号)

遵守上面3点,便可形成一个json对象数组(及一个数组中,存储了多个json对象)

理解了上面两种基本的形式,我们就可以得出其他的数据形式,例如下面这个:

1
2
3
4
5
6
7
8
{
"部门名称":"研发部",
"部门成员":[
{"ID": 1001, "name": "张三", "age": 24},
{"ID": 1002, "name": "李四", "age": 25},
{"ID": 1003, "name": "王五", "age": 22}],
"部门位置":"xx楼21号"
}

这是上面两个基本形式结合出来的一种变形,通过这种变形,使得数据的封装具有很大的灵活性,能让开发者自由的发挥想象力。

JSON字符串

JSON字符串也是在平时开发中使用较多的,json字符串应满足以下条件:

1:它必须是一个字符串,由” “或者’ ‘包裹数据,支持字符串的各种操作

2:里面的数据格式应该要满足其中一个格式,可以是json对象,也可以是json对象数组或者是两种基本形式的组合变形。

总结:json可以简单的分为基本形式:json对象,json对象数组。两种基本格式组合变形出其他的形式,但其本质还是json对象或者json对象数组中的一种。json对象或对象数组可以转化为json字符串,使用于不同的场合。

注意点:在封装json数据的时候,很容易出现错误,比如粗心的在最后一条数据的末尾加上了逗号等等,这里我提供一个在线验证工具,方便大家验证json数据格式的正确性

http://www.bejson.com/

fastjson介绍与使用

fastjson简介与jar下载

fastjson.jar是阿里爸爸开发的一款专门用于Java开发的包,可以方便的实现json对象与JavaBean对象的转换,实现JavaBean对象与json字符串的转换,实现json对象与json字符串的转换。除了这个fastjson以外,还有Google开发的Gson包,其他形式的如net.sf.json包,都可以实现json的转换。方法名称不同而已,最后的实现结果都是一样的。

1
2
3
4
5
6
将json字符串转化为json对象
在net.sf.json中是这么做的
JSONObject obj = new JSONObject().fromObject(jsonStr);//将json字符串转换为json对象

在fastjson中是这么做的
JSONObject obj=JSON.parseObject(jsonStr);//将json字符串转换为json对象

今天我们主要讲fastjson的使用,首先应该在Java工程中导入对应的fastjson.jar包,fastjson.jar包原始下载地址:https://github.com/alibaba/fastjson,点击页面中的download即可下载最新的包

fastjson.jar包百度云盘下载地址:https://pan.baidu.com/s/1CCGoRCdSjNUDB736cRCDBw

fastjson源码分析与使用

在包中,可以发现主要的3个类,JSON,JSONArray,JSONObject

三者之间的关系如下,JSONObject和JSONArray继承JSON

image-20200421124732824

如果你们看不到源代码,请参考另一篇博客,先安装Java反编译工具:

https://blog.csdn.net/srj1095530512/article/details/81587601

联系上面讲到的json基础知识并对应这三个类,可以发现,JSONObject代表json对象,JSONArray代表json对象数组,JSON代表JSONObject和JSONArray的转化。

JSONObject类源码分析与使用

1
public class JSONObject extends JSON implements Map<String, Object>, Cloneable, Serializable, InvocationHandler {

观察该类的继承与实现关系,不难发现,JSONObject实现了Map接口,而json对象中的数据都是以”键:值”对形式出现,可以猜想, JSONObject底层操作是由Map实现的。

再来看类中的主要方法:

类中主要是get()方法。因为JSONObject相当于json对象,所以该类中主要封装了各种get方法,通过”键:值”对中的键来获取其对应的值。且方法的输入参数几乎皆为String类型,这是因为json对象中,”键:值”对的键都是String类型的。

来看一下平时用到较多的getString(String key)方法,该方法输入参数为String key(键),输出为String ,用于获取json对象中的字符串型数据。例如通过该方法获取 “name”:”bob”键值对中name这个键所对应的值bob。

看其源码,可以发现,内部主要是由get(key)方法实现,找到这个方法如下:

1
2
3
4
private static final long serialVersionUID = 1L;
private static final int DEFAULT_INITIAL_CAPACITY = 16;

private final Map<String, Object> map;

发现内部主要由Map接口中的get()方法实现

再去看JSONObject中另一个常用的方法getInteger(String key),该方法获取json对象中的整型数据,例如获取”age:20”键值对中age对应的整型数值20.

对比getString(String key)方法,两者极为相似,都是通过Map接口的get()方法实现

再看几个其他的方法,也是由Map接口中的相应方法实现的,这里不再赘述

总结:JSONObject对应json对象,通过各种形式的get()方法可以获取json对象中的数据,也可利用诸如size(),isEmpty()等方法获取”键:值”对的个数和判断是否为空。其本质是通过实现Map接口并调用接口中的方法完成的

JSONArray类源码分析与使用

1
public class JSONArray extends JSON implements List<Object>, Cloneable, RandomAccess, Serializable {

观察JSONArray的继承与实现,并结合上面对JSONObject的分析,不难发现,其内部是有List接口中的方法来完成操作的

同样观察JSONArray类中的方法,由于方法较多,下面分为两部分

img

首先来明确一点,因为JSONArray代表json对象数组,json数组对象中存储的是一个个json对象,所以类中的方法主要用于直接操作json对象。比如这其中的add(),remove(),containsAll()方法,对应于json对象的添加,删除与判断。

其内部主要有List接口中的对应方法来实现。

跟JSONObject一样,JSONArray里面也有一些get()方法,不过都不常用,最有用的应该是getJSONObject(int index)方法,该方法用于获取json对象数组中指定位置的JSONObject对象,配合size()方法,可用于遍历json对象数组中的各个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
public JSONObject getJSONObject(int index) {
Object value = list.get(index);

if (value instanceof JSONObject) {
return (JSONObject) value;
}

if (value instanceof Map) {
return new JSONObject((Map) value);
}

return (JSONObject) toJSON(value);
}

通过以上两个方法,在配合for循环,即可实现json对象数组的遍历,当然JSONArray中也实现了迭代器方法来遍历,这和List的遍历极为相似

通过遍历得到JSONObject对象,然后再利用JSONObject类中的get()方法,即可实现最终json数据的获取!!!

好了,接下来我们看最后一个,也是最重要的一个类JSON类。之所以把这个放在最后,是因为这个类主要是实现转化用的,最后的数据获取,还是要通过上面的JSONObject和JSONArray来实现。

JSON类源码分析与使用

先来看一下这个类中的主要方法,方法比较多,也分为两部分

仔细观察这些方法,主要是实现json对象,json对象数组,javabean对象,json字符串之间的相互转化

JSON类之toJSONString()方法,实现json对象转化为json字符串和javabean对象转化为json 字符串

该方法经过多次重载,但最终都是实现json对象转化为json字符串和javabean对象转化为json 字符串。其中,有关键字transient修饰的toJSONString()用于json对象序列化过程中,希望某个”键:值”对数据不变的应用中

JSON类之parseObject()方法,实现json字符串转换为json对象或javabean对象

1
2
3
4
5
6
7
8
9
10
11
12
public static JSONObject parseObject(String text) {
Object obj = parse(text);
if (obj instanceof JSONObject) {
return (JSONObject) obj;
}

try {
return (JSONObject) JSON.toJSON(obj);
} catch (RuntimeException e) {
throw new JSONException("can not cast to JSONObject.", e);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public static Object parse(String text) {
return parse(text, DEFAULT_PARSER_FEATURE);
}

/**
*
* @since 1.2.38
*/
public static Object parse(String text, ParserConfig config) {
return parse(text, config, DEFAULT_PARSER_FEATURE);
}

/**
*
* @since 1.2.38
*/
public static Object parse(String text, ParserConfig config, int features) {
if (text == null) {
return null;
}

DefaultJSONParser parser = new DefaultJSONParser(text, config, features);
Object value = parser.parse();

parser.handleResovleTask(value);

parser.close();

return value;
}

public static Object parse(String text, int features) {
return parse(text, ParserConfig.getGlobalInstance(), features);
}

该方法返回JSONObject对象,用于实现json字符串向json对象的转化,其内部调用了parse()方法,调用底层的DefaultJSONParser解析类进行转化,在转化失败时,抛出can not cast to JSONObject异常。

该方法不仅能实现json字符串向json对象的转化,经过重载之后,还能实现json字符串向javabean对象的转化

json字符串与javaBean之间的转换可以使用 TypeReference 这个类,也可以使用Class这个类。

1
2
Student stu1=JSON.parseObject(jsonstr,new TypeReference<Student>(){});
Student stu1=JSON.parseObject(jsonstr,Student.class);

我推荐使用第二种Class类反射来实现,比较简单

JSON类之JSONArray()方法,实现json字符串转化为json对象数组或List

与parseObject()方法类似,parseArray()将json字符串转化为json对象数组或转化成包含泛型的List

JSON类之toJSON()方法,实现javabean对象转化为json对象

该方法用的比较少,主要用于将javabean对象转化为json对象,内部通过Map,LinkedHashMap,HashMap等集合接口实现。

JSON类之toJavaObject()方法,实现json对象转化为javabean对象

该方法也经过重载,通过TypeReference类和Class类反射来实现,主要讲json对象转化为javabean对象,用的也比较少。

至此,JSON类中的方法也讲解的差不多了,下面给出Java实例来实现以上的各种转换

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package jsonTest;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;


public class MyJson {

public static void main(String[] args) {

List<Student> list=new ArrayList<>();
Student student=new Student("bob",24);
Student student12=new Student("lily", 23);
list.add(student);
list.add(student12);
System.out.println("*******javaBean to jsonString*******");
String str1=JSON.toJSONString(student);
System.out.println(str1);
System.out.println(JSON.toJSONString(list));
System.out.println();

System.out.println("******jsonString to javaBean*******");
//Student stu1=JSON.parseObject(str1,new TypeReference<Student>(){});
Student stu1=JSON.parseObject(str1,Student.class);
System.out.println(stu1);
System.out.println();

System.out.println("******javaBean to jsonObject******");
JSONObject jsonObject1=(JSONObject)JSON.toJSON(student);
System.out.println(jsonObject1.getString("name"));
System.out.println();

System.out.println("******jsonObject to javaBean******");
Student student2=JSON.toJavaObject(jsonObject1, Student.class);
System.out.println(student2);
System.out.println();

System.out.println("*******javaBean to jsonArray******");
List<Student> stulist=new ArrayList<>();
for(int i=0;i<5;i++){
stulist.add(new Student("student"+i, i));

}
JSONArray jsonArrays=(JSONArray)JSON.toJSON(stulist);
for(int i=0;i<jsonArrays.size();i++){
System.out.println(jsonArrays.getJSONObject(i));
}
System.out.println();

System.out.println("*****jsonArry to javalist******");
List<Student> myList=new ArrayList<>();
for(int i=0;i<jsonArrays.size();i++){

Student student3=JSON.toJavaObject(jsonArrays.getJSONObject(i), Student.class);
myList.add(student3);
}
for(Student stu:myList){
System.out.println(stu);
}

System.out.println();

System.out.println("*****jsonObject to jsonString*****");
String str4=JSON.toJSONString(jsonObject1);
System.out.println(str4);
System.out.println();

System.out.println("*******jsonString to jsonObject*****");
JSONObject jso1=JSON.parseObject(str1);
System.out.println(jso1.getString("name"));
System.out.println();

System.out.println("*****jsonString to jsonArray*****");
JSONArray jArray=JSON.parseArray(JSON.toJSONString(stulist));
for(int i=0;i<jArray.size();i++){
System.out.println(jArray.getJSONObject(i));
}
System.out.println();
}
}

测试类对应的javabean类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package jsonTest;

public class Student {

private String name;
private int age;

public Student() {
// TODO Auto-generated constructor stub
}

public Student(String name,int age){
this.name=name;
this.age=age;
}

public void setName(String name){
this.name=name;

}

public String getName(){
return name;
}

public void setAge(int age){
this.age=age;
}

public int getAge(){
return age;
}

@Override
public String toString() {
// TODO Auto-generated method stub
return "student [name="+name+" , "+"age="+age+"]";
}

}

SpringMVC

课堂目标

  • 熟悉springmvc六大组件的作用及使用方式

  • 掌握搭建ssm开发框架

  • 掌握springmvc对于返回值的处理

  • 掌握springmvc对于参数的处理

  • 理解REST和RESTful,掌握springmvc对于RESTful的支持

  • 掌握springmvc拦截器的应用

  • 掌握springmvc中基于cors的跨域解决方案

  • 掌握spring中的父子容器

  • 了解springmvc的mock测试

  • 了解ControllerAdvice类的开发ModelAttribute、InitBinder、ExceptionHandler

  • 了解springmvc异常处理器

  • 了解springmvc中的get\post请求乱码和响应乱码处理

  • 了解springmvc中非注解开发方式

介绍篇

基础概念介绍

BS和CS开发架构

一种是C/S 架构,也就是客户端/服务器

一种是B/S 架构 ,也就是浏览器/服务器架构

说明:
我们现在使用Java开发的大多数都是web应用 ,这些应用几乎全都是基于B/S 架构进行开发的。那么在B/S
架构中,应用系统标准的三层架构分为:表现层、业务层、持久层 。这种三层架构在我们的实际开发中使用的非常多,所以我们课程中的案例也都是基于三层架构设计的。

JavaEE制定了一套规范,去进行BS结构的处理。这套规范就是Servlet 。

应用系统三层架构

表现层:
  • 也就是我们常说的web 层 。
  • 它负责接收客户端请求,向客户端响应结果 ,通常客户端使用http 协议请求web 层,web 层需要接收http 请求,完成http 响应。
  • 表现层包括展示层和控制层 :控制层负责接收请求,展示层负责结果的展示。
  • 表现层依赖业务层,接收到客户端请求一般会调用业务层进行业务处理,并将处理结果响应给客户端。
  • 表现层的设计一般都使用 MVC 模型 。(MVC 是表现层的设计模型,和其他层没有关系)
业务层 :
  • 也就是我们常说的service 层。

  • 它负责业务逻辑处理,和我们开发项目的需求息息相关。web 层依赖业务层,但是业务层不依赖web层。

  • 业务层在业务处理时可能会依赖持久层,如果要对数据持久化需要保证事务一致性。(也就是我们说的,
    事务应该放到业务层来控制)

持久层 :
  • 也就是我们是常说的dao 层。
  • 负责数据持久化,包括数据层即数据库和数据访问层,数据库是对数据进行持久化的载体,数据访问层是
    业务层和持久层交互的接口,业务层需要通过数据访问层将数据持久化到数据库中。
  • 通俗的讲,持久层就是和数据库交互,对数据库表进行增删改查的。

MVC设计模式

MVC
是模型(model)-视图(view)-控制器(controller) 的缩写,是一种用于设计编写Web 应用程序表现层的模式。

MVC 设计模式的三大角色:

  • Model(模型):

    模型包含业务模型和数据模型 ,数据模型用于封装数据,业务模型用于处理业务。

  • View(视图):

    通常指的就是我们的jsp 或者html。作用一般就是展示数据的。

    通常视图是依据数据模型创建的。

  • Controller(控制器):

    是应用程序中处理用户交互的部分。作用一般就是处理程序逻辑的。

SpringMVC介绍

SpringMVC是什么

  • SpringMVC 是一种基于MVC 设计模型的请求驱动类型的轻量级 Web 框架 ,属于SpringFrameWork 的后续
    产品,已经融合在Spring Web Flow 里面。Spring 框架提供了构建Web 应用程序的全功能MVC 模块。
  • 使用Spring 可插入的MVC 架构,从而在使用Spring 进行WEB 开发时,可以选择使用Spring 的Spring MVC 框架或集成其他MVC 开发框架,如Struts1(现在一般不用),Struts2 等。
  • SpringMVC 已经成为目前最主流的 MVC 框架 之一,并且随着 Spring3.0 的发布,全面超越 Struts2,成为最优秀的 MVC 框架。
  • 它通过一套注解,让一个简单的Java 类成为处理请求的控制器,而无须实现任何接口。同时它还支持RESTful 编程风格的请求。

SpringMVC与Spring的联系

Spring MVC 全名叫Spring Web MVC ,它是Spring家族Web模块的一个重要成员。这一点,我们可以从Spring 的整体结构中看得出来:

image-20200418194902816

为什么学习SpringMVC

也许你要问,为什么要学习Spring MVC呢?Struts2不才是主流吗?看SSH的概念有多火?

其实很多初学者混淆了一个概念,SSH 实际上指的是Struts1.x+Spring+Hibernate 。这个概念已经有十几年的历
史了。在Struts1.x时代,它是当之无愧的霸主,但是在新的MVC框架涌现的时代,形式已经不是这样了,
Struts2.x借助了Struts1.x的好名声,让国内开发人员认为Struts2.x是霸主继任者(其实两者在技术上无任何关
系),导致国内程序员大多数学习基于Struts2.x的框架,又一个貌似很火的概念出来了S2SH (Struts2+Spring+Hibernate )整合开发。

不要再被蒙蔽了,看看下面的调查统计吧

image-20200418195028056

SpringMVC的市场占有率是40% ,而Struts2只有可怜的6%。这已然说明了学习SpringMVC的必要性了,再说了,SpringMVC本身就是spring家族的一员,与整合spring时,SpringMVC根本无需中间整合包,而struts2得需要。

六大组件介绍

image-20200418195124015

六大组件(MVC组件其他三大组件)说明

说明:

  1. 在springmvc 的各个组件中,前端控制器、处理器、视图 称为springmvc 的MVC组件
  2. 在springmvc 的各个组件中,处理器映射器、处理器适配器、视图解析器 称为springmvc 的三大组件
  3. 需要开发的组件有:处理器、视图

DispatcherServlet :前端控制器

用户请求到达前端控制器,它就相当于mvc模式中的C,dispatcherServlet是整个流程控制的中心,由它调用其它组件处理用户的请求,dispatcherServlet的存在降低了组件之间的耦合性。

Handler :处理器

Handler 是继DispatcherServlet前端控制器的后端控制器,在DispatcherServlet的控制下Handler对具体的用户请求进行处理。
由于Handler涉及到具体的用户业务请求,所以一般情况需要程序员根据业务需求开发Handler。

View :视图

springmvc框架提供了很多的View视图类型的支持,包括:jstlView、freemarkerView、pdfView等。我们最常用的视图就是jsp。

一般情况下需要通过页面标签或页面模版技术将模型数据通过页面展示给用户,需要由程序员根据业务需求开发
具体的页面。

HandlerMapping :处理器映射器

HandlerMapping负责根据用户请求找到Handler即处理器,springmvc提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。

HandlAdapter :处理器适配器

通过HandlerAdapter对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进
行执行。

View Resolver :视图解析器

View Resolver负责将处理结果生成View视图,View Resolver首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成View视图对象,最后对View进行渲染将处理结果通过页面展示给用户。

项目搭建篇

搭建入门工程

添加依赖

就是mvc 依赖、jstl 依赖还有servlet-api 依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kkb</groupId>
<artifactId>springmvc-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<!-- spring MVC依赖包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<!-- jstl -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 配置Maven的JDK编译级别-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<path>/</path>
<port>8080</port>
</configuration>
</plugin>
</plugins>
</build>
</project>

开发步骤

配置部分

web.xml

在web.xml 中添加前端控制器DispatcherServlet 的配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<!-- 学习前置条件-->
<!-- 问题1:web.xml中servelet、filter、listener、context-param加载顺序-->
<!-- 问题2:load-on-startup标签的作用,影响了servlet对象创建的时机-->
<!-- 问题3:url-pattern标签的配置方式有四种:/dispatcherServlet、/servlet/* 、* 、/
,以上四种配置,优先级是如何的-->
<!-- 问题4:url-pattern标签的配置为/*报错,原因是它拦截了JSP请求,但是又不能处理JSP请求。为什
么配置/就不拦截JSP请求,而配置/*,就会拦截JSP请求-->
<!-- 问题5:配置了springmvc去读取spring配置文件之后,就产生了spring父子容器的问题-->
<!-- 配置前端控制器-->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servletclass>
<!-- 设置spring配置文件路径-->
<!-- 如果不设置初始化参数,那么DispatcherServlet会读取默认路径下的配置文件-->
<!-- 默认配置文件路径:/WEB-INF/springmvc-servlet.xml -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<!-- 指定初始化时机,设置为2,表示Tomcat启动时,DispatcherServlet会跟随着初始化-->
<!-- 如果不指定初始化时机,DispatcherServlet就会在第一次被请求的时候,才会初始化,而且只
会被初始化一次-->
<load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<!-- URL-PATTERN的设置-->
<!-- 不要配置为/*,否则报错-->
<!-- 通俗解释:/*,会拦截整个项目中的资源访问,包含JSP和静态资源的访问,对于静态资源的访问
springMVC提供了默认的Handler处理器-->
<!-- 但是对于JSP来讲,springmvc没有提供默认的处理器,我们也没有手动编写对于的处理器,此时
按照springmvc的处理流程分析得知,它短路了-->
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- <servlet> -->
<!-- <servlet-name>sss</servlet-name> -->
<!-- <servlet-class>sss</servlet-class> -->
<!-- </servlet> -->
<!-- <servlet-mapping> -->
<!-- <servlet-name>sss</servlet-name> -->
<!-- <url-pattern>/sss</url-pattern> -->
<!-- </servlet-mapping> -->
</web-app>

springmvc.xml

DispatcherServlet 启动时会去加载springmvc.xml 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/spring-mvc.xsd">
   <!-- 配置处理器Bean的读取-->
<!-- 扫描controller注解,多个包中间使用半角逗号分隔-->
<context:component-scan base-package="com.kkb.springmvc.controller"/>
   
   <!-- 配置三大组件之处理器适配器和处理器映射器-->
   <!-- 内置了RequestMappingHandlerMapping和RequestMappingHandlerAdapter等组件注册-->
   <mvc:annotation-driven />  
   
   <!-- 配置三大组件之视图解析器-->
   <!-- InternalResourceViewResolver:默认支持JSP视图解析-->
   <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
</beans>

配置说明

mvc:annotation-drivern

  • ContentNegotiationManagerFactoryBean
  • RequestMappingHandlerMapping(重要)
  • ConfigurableWebBindingInitializer
  • RequestMappingHandlerAdapter(重要)
  • CompositeUriComponentsContributorFactoryBean
  • ConversionServiceExposingInterceptor
  • MappedInterceptor
  • ExceptionHandlerExceptionResolver
  • ResponseStatusExceptionResolver
  • DefaultHandlerExceptionResolver
  • BeanNameUrlHandlerMapping
  • HttpRequestHandlerAdapter
  • SimpleControllerHandlerAdapter
  • HandlerMappingIntrospector

编码部分

Controller

处理器开发方式有多种:

  • 实现HttpRequestHandler接口
  • 实现Controller接口
  • 使用注解方式

不过企业开发中,推荐使用注解方式 开发处理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//@Controller:在类上添加该注解,指定该类为一个请求处理器,不需要实现任何接口或者继承任何类。
@Controller
public class HelloController {
   //@RequestMapping:在方法上或者类上添加该注解,指定请求的`url`由该方法处理。
@RequestMapping("showView")
public String showView() {
return "hello";
}
   @RequestMapping("showData")
   @ResponseBody
public String showData() {
return "showData";
}
}

注意事项

@Controller 注解的类中每个@RequestMapping 注解的方法,最终都会转为HandlerMethod 类(而这个类才是SpringMVC 注解开发方式中真正意义的处理器)

访问测试

访问地址:http://localhost:8080/showView\

SSM框架整合

整合思路

将工程的三层结构中的JavaBean 分别使用Spring容器(通过XML方式 )进行管理。

  1. 整合持久层mapper ,包括数据源、SqlSessionFactory 及mapper 代理对象的整合;
  2. 整合业务层Service ,包括事务Bean 及service 的bean 的配置;
  3. 整合表现层Controller ,直接使用springmvc 的配置。
  4. Web.xml 加载spring 容器(包含多个XML文件,还分为父子容器)

核心配置文件

  • applicationContext-dao.xml

  • applicationContext-service.xml

  • springmvc.xml

  • web.xml

工程搭建

依赖包

  • spring(包括springmvc)
  • mybatis
  • mybatis-spring整合包
  • 数据库驱动
  • 第三方连接池
  • JSTL
  • servlet-api
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kkb</groupId>
<artifactId>ssm-5</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<!-- 持久层依赖开始-->
<!-- spring ioc组件需要的依赖包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<!-- spring 事务管理和JDBC依赖包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<!-- mysql数据库驱动包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.35</version>
</dependency>
<!-- dbcp连接池的依赖包-->
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
<!-- mybatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<!-- mybatis和spring的整合依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.1</version>
</dependency>
<!-- 持久层依赖结束-->
<!-- 业务层依赖开始-->
<!-- 基于AspectJ的aop依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<!-- 业务层依赖结束-->
<!-- 表现层依赖开始-->
<!-- spring MVC依赖包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<!-- jstl 取决于视图对象是否是JSP -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-oxm</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.10</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.6</version>
</dependency>
<!-- 表现层依赖结束-->
<!-- spring 单元测试组件包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<!-- 单元测试Junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!-- Mock测试使用的json-path依赖-->
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.2.0</version>
</dependency>
<!-- 文件上传-->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 配置Maven的JDK编译级别-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<path>/</path>
<port>80</port>
</configuration>
</plugin>
</plugins>
</build>
</project>

工程整合(配置文件)

整合Mapper

applicationContext-dao.xml(核心)

在classpath 下,创建spring 目录,然后创建SqlMapConfig.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 加载db.properties -->
<context:property-placeholder location="classpath:db.properties" />
<!-- 配置数据源-->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="${jdbc.driver}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<property name="maxActive" value="30" />
<property name="maxIdle" value="5" />
</bean>
<!-- 配置SqlSessionFacotory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 配置数据源-->
<property name="dataSource" ref="dataSource" />
       <property name="typeAliasesPackage" value="com.kkb.ssm.po"></property>
</bean>
<!-- 配置mapper扫描器,SqlSessionConfig.xml中的mapper配置去掉-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 指定扫描的包-->
<property name="basePackage" value="com.kkb.ssm.mapper" />
</bean>
</beans>

db.properties

在classpath 下,创建db.properties

1
2
3
4
jdbc.driver=com.mysql.jdbc.Driver  
jdbc.url=jdbc:mysql://localhost:3306/ssm?characterEncoding=utf8  
jdbc.username=root  
jdbc.password=root

log4j.properties

在classpath 下,创建log4j.properties

1
2
3
4
5
6
#dev env [debug] product   env [info]  
log4j.rootLogger=DEBUG,   stdout  
# Console output...  
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p   [%t] - %m%n

整合Service

applicationContext-service.xml

在spring 文件夹下创建applicationContext-service.xml ,文件中配置service 。在这个配置文件中,需要配置service 的bean 和事务管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"                
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 扫描Service -->
<context:component-scan base-package="com.kkb.ssm.service" />
<!-- 配置事务-->
<!-- 事务管理器,对mybatis操作数据库进行事务控制,此处使用jdbc的事务控制-->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 指定要进行事务管理的数据源-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 传播行为-->
<tx:method name="save*" propagation="REQUIRED" />
<tx:method name="add*" propagation="REQUIRED" />
<tx:method name="insert*" propagation="REQUIRED" />
<tx:method name="delete*" propagation="REQUIRED" />
<tx:method name="del*" propagation="REQUIRED" />
<tx:method name="remove*" propagation="REQUIRED" />
<tx:method name="update*" propagation="REQUIRED" />
<tx:method name="modify*" propagation="REQUIRED" />
<tx:method name="find*" read-only="true" />
<tx:method name="query*" read-only="true" />
<tx:method name="select*" read-only="true" />
<tx:method name="get*" read-only="true" />
</tx:attributes>
</tx:advice>
<!-- aop -->
<aop:config>
<aop:advisor advice-ref="txAdvice"
pointcut="execution(* com.kkb.ssm.service.impl.*.*(..))" />
</aop:config>
</beans>

整合Controller

Spring 和springmvc 之间无需整合,直接用springmvc的配置

web.xml

在web.xml 中添加springmvc 的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID" version="2.5">
<!-- 配置springmvc的前端控制器-->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servletclass>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>

springmvc.xml

在spring 包下创建springmvc.xml 文件,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 配置处理器映射器和处理器适配器-->
<mvc:annotation-driven />
<!-- 配置视图解析器-->
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
<!-- 使用注解的handler可以使用组件扫描器,加载handler -->
<context:component-scan base-package="com.kkb.ssm.controller" />
</beans>

web.xml加载spring父容器

在web.xml中,使用监听器来对spring的配置文件进行加载

1
2
3
4
5
6
7
8
9
10
<!-- 加载spring容器-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:spring/applicationContext-*.xml
</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listenerclass>
</listener>

整合测试(编写代码)

需求

实现商品查询列表,从mysql 数据库查询商品信息

需求分析

表现层
请求URL:/queryItem
请求参数:无
请求返回值:json格式数据
业务层
业务处理逻辑(需求分析):实现商品列表的查询
持久层
只针对表进行增删改查操作

持久层代码

根据需求分析,持久层需要查询item表中的记录,使用逆向工程的代码即可。

通过逆向工程,把po类、mapper.xml和mapper.java类生成出来并拷贝到项目中。

业务层代码

根据需求开发service的接口以及实现类,注意:使用注解 @Service开发service 。

1
2
3
4
5
6
7
8
9
@Service
public class ItemServiceImpl implements ItemService {
@Autowired
private ItemMapper mapper;
public List<Item> queryItemList() {
ItemExample example = new ItemExample();
return mapper.selectByExample(example);
}
}

表现层代码

在Controller 类上添加@Controller注解

在Controller 方法上添加@RequestMapping 注解进行url请求映射

1
2
3
4
5
6
7
8
9
10
11
12
@Controller
public class ItemController {
@Autowired
private ItemService service;
@RequestMapping("/item")
@ResponseBody
public List<Item> queryItem() {
// 根据查询条件去数据库中查询商品列表
List<Item> itemList = service.queryItemList();
return itemList;
}
}

部署测试

http://localhost:8080/ssm/item

应用掌握篇

返回值处理

不使用注解修饰

ModelAndView

Controller方法中定义ModelAndView对象并返回,对象中可添加model数据、指定view。

void

在Controller 方法形参上可以定义request和response ,使用request 或response 指定响应结果

void service(HttpServletRequest request,HttpServletResponse response){}

使用request转发向页面,如下:

String(推荐)

逻辑视图名

分布式消息系统Kafka

第1章Kafka概述

kafaka 简介

Apache Kafka 是一个快速、可扩展的、高吞吐的、可容错的分布式“发布-订阅”消息系统,使用Scala 与Java 语言编写,能够将消息从一个端点传递到另一个端点,较之传统的消息中间件(例如ActiveMQ、RabbitMQ),Kafka 具有高吞吐量、内置分区、支持消息副本和高容错的特性,非常适合大规模消息处理应用程序。

Kafka 官网:http://kafka.apache.org/

Kafa 系统架构

image-20200418150830785

应用场景

Kafka 的应用场景很多,这里就举几个最常见的场景。

用户的活动追踪

用户在网站的不同活动消息发布到不同的主题中心,然后可以对这些消息进行实时监测实时处理。当然,也可加载到Hadoop 或离线处理数据仓库,对用户进行画像。像淘宝、京东这些大型的电商平台,用户的所有活动都是要进行追踪的。

日志聚合

image-20200418151054236

限流削峰

image-20200418151120619

kafka 高吞吐率实现

Kafka 与其它MQ 相比,其最大的特点就是高吞吐率。为了增加存储能力,Kafka 将所有的消息都写入到了低速大容的硬盘。按理说,这将导致性能损失,但实际上,kafka 仍可保持超高的吞吐率,性能并未受到影响。其主要采用了如下的方式实现了高吞吐率。

  • 顺序读写:Kafka 将消息写入到了分区partition 中,而分区中消息是顺序读写的。顺序读写要远快于随机读写。
  • 零拷贝:生产者、消费者对于kafka 中消息的操作是采用零拷贝实现的。
  • 批量发送:Kafka 允许使用批量消息发送模式。
  • 消息压缩:Kafka 支持对消息集合进行压缩。

第2章Kafka工作原理与工作过程

Kafka 基本原理

Kafka 工作原理与过程

Kafka 集群搭建

在生产环境中为了防止单点问题,Kafka 都是以集群方式出现的。下面要搭建一个Kafka
集群,包含三个Kafka 主机,即三个Broker。

Kafka的下载

image-20200418151311727

安装并配置第一台主机

上传并解压

将下载好的Kafka 压缩包上传至CentOS 虚拟机,并解压。

image-20200418151358365

创建软链接

image-20200418151414166

修改配置文件

在kafka 安装目录下有一个config/server.properties 文件,修改该文件。

image-20200418151434608

image-20200418151441434

image-20200418151447611

再克隆两台Kafka

以kafkaOS1 为母机再克隆两台Kafka 主机。在克隆完毕后,需要修改server.properties中的broker.id、listeners 与advertised.listeners。

image-20200418151515232

image-20200418151522584

kafka的启动与停止

启动zookeeper

image-20200418151546794

启动kafka

在命令后添加-daemon 参数,可以使kafka 以守护进程方式启动,即不占用窗口。

image-20200418151603611

停止kafka

image-20200418151615853

kafka操作

创建topic

image-20200418151637049

查看topic

image-20200418152242515

发送消息

该命令会创建一个生产者,然后由其生产消息。

image-20200418152258625

消费消息

image-20200418152308952

继续生产消费

image-20200418152320714

image-20200418152328445

删除topic

image-20200418152345425

日志查看

我们这里说的日志不是Kafka 的启动日志,启动日志在Kafka 安装目录下的logs/server.log中。消息在磁盘上都是以日志的形式保存的。我们这里说的日志是存放在/tmp/kafka_logs目录中的消息日志,即partition 与segment。

查看分区与备份

1 个分区1 个备份

我们前面创建的test 主题是1 个分区1 个备份。

image-20200418152504351

3 个分区1 个备份

再次创建一个主题,命名为one,创建三个分区,但仍为一个备份。依次查看三台broker,可以看到每台broker 中都有一个one 主题的分区。

image-20200418152523123

3 个分区3 个备份

再次创建一个主题,命名为two,创建三个分区,三个备份。依次查看三台broker,可以看到每台broker 中都有三份two 主题的分区。

image-20200418152542890

查看段segment

segment 文件

segment 是一个逻辑概念,其由两类物理文件组成,分别为“.index”文件和“.log”文件。“.log”文件中存放的是消息,而“.index”文件中存放的是“.log”文件中消息的索引。

00000000000000001456.log

image-20200418152627694

查看segment

对于segment 中的log 文件,不能直接通过cat 命令查找其内容,而是需要通过kafka自带的一个工具查看。

1
bin/kafka-run-class.sh kafka.tools.DumpLogSegments --files /tmp/kafka-logs/test-0/00000000000000000000.log --print-data-log

一个用户的一个主题会被提交到一个__consumer_offsets 分区中。使用主题字符串的hash 值与50 取模,结果即为分区索引。

第3章Kafka API

首先在命令行创建一个名称为cities 的主题,并创建该主题的订阅者。

使用kafka 原生API

创建工程

创建一个Maven 的Java 工程,命名为kafkaDemo。创建时无需导入依赖。为了简单,后面的发布者与消费者均创建在该工程中。

image-20200418153357980

导入依赖

1
2
3
4
5
6
<!-- kafka依赖-->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_2.12</artifactId>
<version>1.1.1</version>
</dependency>

创建发布者OneProducer

创建发布者类OneProducer

image-20200418153456885

image-20200418153505207

创建测试类OneProducerTest

image-20200418153522689

创建发布者TwoProducer

前面的方式在消息发送成功后,代码中没有任何提示,这里可以使用回调方式,即发送成功后,会触发回调方法的执行。

创建发布者类TwoProducer

复制OneProducer 类,仅修改sendMsg()方法。

image-20200418153557976

创建测试类TwoProducerTest

image-20200418153611185

批量发送消息

创建发布者类SomeProducerBatch

复制前面的发布者类,在其基础上进行修改。

image-20200418153637689

image-20200418153653616

创建测试类ProducerBatchTes

image-20200418153708533

消费者组

创建消费者类SomeConsumer

image-20200418153726603

image-20200418153734746

创建测试类ConsumerTest

image-20200418153753774

消费者同步手动提交

自动提交的问题

前面的消费者都是以自动提交offset 的方式对broker 中的消息进行消费的,但自动提交可能会出现消息重复消费的情况。所以在生产环境下,很多时候需要对offset 进行手动提交,以解决重复消费的问题。

手动提交分类

手动提交又可以划分为同步提交、异步提交,同异步联合提交。这些提交方式仅仅是doWork()方法不相同,其构造器是相同的。所以下面首先在前面消费者类的基础上进行构造器的修改,然后再分别实现三种不同的提交方式。

创建消费者类SyncManualConsumer

原理

同步提交方式是,消费者向broker 提交offset 后等待broker 成功响应。若没有收到响应,则会重新提交,直到获取到响应。而在这个等待过程中,消费者是阻塞的。其严重影响了消费者的吞吐量。

修改构造器

直接复制前面的SomeConsumer,在其基础上进行修改。

image-20200418153932380

修改doWork()方法

image-20200418153942897

创建测试类SyncManulTest

image-20200418153959049

消费者异步手动提交

原理

手动同步提交方式需要等待broker 的成功响应,效率太低,影响消费者的吞吐量。异步提交方式是,消费者向broker 提交offset 后不用等待成功响应,所以其增加了消费者的吞吐量。

创建消费者类AsyncManualConsumer

复制前面的SyncManualConsumer 类,在其基础上进行修改。

image-20200418154045926

创建测试类AsyncManulTest

image-20200418154101849

消费者同异步手动提交

原理

同异步提交,即同步提交与异步提交组合使用。一般情况下,若偶尔出现提交失败,其也不会影响消费者的消费。因为后续提交最终会将这次提交失败的offset 给提交了。

但异步提交会产生重复消费,为了防止重复消费,可以将同步提交与异常提交联合使用。

创建消费者类SyncAsyncManualConsumer

复制前面的AsyncManualConsumer 类,在其基础上进行修改。

image-20200418154143311

Spring Boot Kafka

为了简单,以下代码是将消息发布者与订阅者定义到了一个工程中的。

创建工程

创建一个Spring Boot 工程,导入如下依赖。

image-20200418154236404

定义发布者

Spring 是通过KafkaTemplate 来完成对Kafka 的操作的。

修改配置文件

image-20200418154330765

定义发布者处理器

Spring Kafka 通过KafkaTemplate 完成消息的发布。

image-20200418154350130

定义消费者

Spring 是通过监听方式实现消费者的。

修改配置文件

在配置文件中添加如下内容。注意,Spring 中要求必须为消费者指定组。

image-20200418154432579

定义消费者

Spring Kafka 是通过KafkaListener 监听方式来完成消息订阅与接收的。当监听到有指定主题的消息时,就会触发@KafkaListener 注解所标注的方法的执行。

image-20200418154504832

Idea

IDEA

IDEA中的.iml文件和.idea文件夹作用和意义

.iml文件

information of module,即idea对module的配置信息

iml是 intellij idea的工程配置文件,里面是当前project的一些配置信息

iml文件是IntelliJ IDEA自动创建的模块文件,用于Java应用开发,存储一些模块开发相关的信息,比如一个Java组件,插件组件,Maven组件等待,还可能会存储一些模块路径信息,依赖信息以及别的一些设置

Redis1

Redis

Redis介绍

什么是Redis

Redis是用C语言开发的一个开源的高性能键值对(key-value )内存数据库 ,它是一种NoSQL
数据库

它是【单进程单线程】的内存数据库,所以说不存在线程安全问题

它可以支持并发10WQPS,所以说性能非常优秀。之所以单进程单线程性能还这么好,是因为底层采用了【IO多路复用(NIO思想)】

相比Memcache这种专业缓存技术,Redis有更优秀的读写性能,及丰富的数据类型

它提供了五种数据类型来存储【值】:字符串类型(string)、散列类型(hash)、列表类型(list)、集合类型(set)、有序集合类型(sortedset、zset)

Spring Cloud基础

微服务框架Spring Cloud

第1章Spring Cloud入门

Spring Cloud 简介

image-20200329173058595

IoT(Internet of Things)物联网,mobile移动端,browser浏览器统称为客户端

API Gateway网关

microservices微服务

breaker dashboard熔断仪表盘

config dashboard 配置仪表盘

service registry 服务注册中心

distributed tracing 分布链跟踪

第1章Zookeeper理论基础

分布式协调服务器Zookeeper

第1章Zookeeper理论基础

Zookeeper 简介

ZooKeeper 由雅虎研究院开发,后来捐赠给了Apache。ZooKeeper 是一个开源的分布式应用程序协调服务器,其为分布式系统提供一致性服务。其一致性是通过基于Paxos 算法的ZAB 协议完成的。其主要功能包括:配置维护、域名服务、分布式同步、集群管理等

zookeeper 的官网:http://zookeeper.apache.org

image-20200329073617838

事务原理及MVCC

事务概述

在MySQL中的事务是由存储引擎实现的,而且支持事务的存储引擎不多,我们主要讲解InnoDB存储引擎中的事务。所以,今天我们就一起来分析和探讨InnoDB的事务机制,希望能建立起对事务底层实现原理的具体了解

image-20200326101714003

数据库事务具有ACID四大特性。ACID是以下4个词的缩写:

  • 原子性(atomicity) :事务最小工作单元,要么全成功,要么全失败
  • 一致性(consistency):事务开始和结束后,数据库的完整性不会被破坏
  • 隔离性(isolation) :不同事务之间互不影响,四种隔离级别为RU(读未提交)、RC(读已提交)、RR(可重复读)、SERIALIZABLE (串行化)
  • 持久性(durability) :事务提交后,对数据的修改是永久性的,即使系统故障也不会丢失

隔离级别

事前准备

我们需要创建一个表

1
2
3
4
CREATE TABLE t (
  id INT PRIMARY KEY,
  c VARCHAR(100)
) Engine=InnoDB CHARSET=utf8;

然后向这个表里插入一条数据

1
INSERT INTO t VALUES(1, '刘备');

现在表里的数据就是这样的

1
2
3
4
5
6
7
mysql> SELECT * FROM t;
+----+--------+
| id | c     |
+----+--------+
|  1 | 刘备  |
+----+--------+
1 row in set (0.01 sec)

未提交读(READ UNCOMMITTED/RU)

脏读:一个事务读取到另一个事务未提交的数据

如果一个事务读到了另一个未提交事务修改过的数据,那么这种隔离级别就称之为未提交读(英文名:READ UNCOMMITTED ),示意图如下:

image-20200326102355671

如上图,Session A 和Session B 各开启了一个事务,Session B 中的事务先将id 为1 的记录的列c 更新为’关羽’ ,然后Session A 中的事务再去查询这条id 为1 的记录,那么在未提交读的隔离级别下,查询结果就是’关羽’ ,也就是说某个事务读到了另一个未提交事务修改过的记录。但是如果Session B 中的事务稍后进行了回滚,那么Session A 中的事务相当于读到了一个不存在的数据,这种现象就称之为脏读,就像这个样子

image-20200326102522861

脏读违背了现实世界的业务含义,所以这种READ UNCOMMITTED 算是十分不安全的一种隔离级别

已提交读(READ COMMITTED/RC)

不可重复读:一个事务因读取到另一个事务已提交的update。导致对同一条记录读取两次以上的结果不一致

这里有两个重点

如果一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值,那么这种隔离级别就称之为已提交读(英文名:READCOMMITTED ),如图所示:

image-20200326102709917

MySQL锁篇

MySQL锁介绍

乐观锁

用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?

即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加1。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据

举例

数据库表三个字段,分别是id、value、version

1
select id,value,version from TABLE where id = #{id}

每次更新表中的value字段时,为了防止发生冲突,需要这样操作

1
update TABLE set value=2,version=version+1 where id=#{id} and version=#{version}

锁的分类

按照锁的粒度

MySQL主要包含三种类型(级别)的锁定机制

全局锁:锁的是整个database。由MySQL的SQL layer层实现的

表级锁:锁的是某个table。由MySQL的SQL layer层实现的

行级锁:锁的是某行数据,也可能锁定行之间的间隙。由某些存储引擎实现,比如InnoDB

表级锁和行级锁的区别

表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低

行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高

按照锁的功能

共享读锁和排他写锁

按照锁的实现方式

悲观锁和乐观锁(使用某一版本列或者唯一列进行逻辑控制)

MySQL表级锁

表级锁介绍

由MySQL SQL layer层实现

MySQL的表级锁有两种

一种是表锁,一种是元数据锁(meta data lock,MDL)

MySQL 实现的表级锁定的争用状态变量

1
show status like 'table%';

table_locks_immediate:产生表级锁定的次数

table_locks_waited:出现表级锁定争用而发生等待的次数

表锁介绍

MySQL允许客户端会话明确获取表锁,以防止其他会话在特定时间段内访问表。客户端会话只能为自己获取或释放表锁。它不能获取或释放其他会话的表锁

表锁有两种表现形式

表共享读锁(Table Read Lock),表独占写锁(Table Write Lock)

手动增加表锁

1
LOCK TABLES table_name [READ | WRITE];

可将表的名称放在LOCK TABLES关键字后面,后跟一个锁类型。 MySQL提供两种锁类型:READWRITE

查看表锁情况

1
show open tables;

删除表锁

1
unlock tables;

表锁演示

环境准备

新建表

1
2
3
4
5
6
7
8
CREATE DATABASE IF NOT EXISTS testdb;

USE testdb;
CREATE TABLE tbl (
id int(11) NOT NULL AUTO_INCREMENT,
col int(11) NOT NULL,
PRIMARY KEY (id)
);

读锁演示

表锁定为READ

表的READ锁具有以下功能:

  • 同时可以通过多个会话获取表的READ锁。此外,其他会话可以从表中读取数据,而无需获取锁定
  • 持有READ锁的会话只能从表中读取数据,但不能写入。此外,其他会话在释放READ锁之前无法将数据写入表中。来自另一个会话的写操作将被放入等待状态,直到释放READ
  • 如果会话正常或异常终止,MySQL将会隐式释放所有锁。这也与WRITE锁相关

首先,连接到testdb数据库。要查找当前的连接ID,请使用CONNECTION_ID()函数,如下所示

1
2
3
4
5
6
7
mysql> SELECT CONNECTION_ID();
+-----------------+
| CONNECTION_ID() |
+-----------------+
| 6 |
+-----------------+
1 row in set (0.00 sec)

注:

CONNECTION_ID():MySQL的这个函数返回的是这个连接的连接ID或者thread ID。对于已经建立的连接的客户端,都有一个唯一的连接ID

然后,在向tbl表中插入一个新行

1
INSERT INTO tbl(col) VALUES(10);

接下来,从上表tbl中检索所有行

1
2
3
4
5
6
7
mysql> SELECT * FROM tbl;
+----+-----+
| id | col |
+----+-----+
| 1 | 10 |
+----+-----+
1 row in set (0.00 sec)

之后,要获取锁,可以使用LOCK TABLE语句

1
2
mysql> LOCK TABLES tbl READ;
Query OK, 0 rows affected (0.00 sec)

最后,在同一个会话中,如果您尝试在tbl表中插入一个新行,将收到一条错误消息

1
2
mysql> INSERT INTO tbl(col) VALUES(11);
ERROR 1099 (HY000): Table 'tbl' was locked with a READ lock and can't be updated

所以一旦获得了READ锁定,就不能在同一个会话中的表中写入数据。让我们从不同的会话中来查看READ

首先,打开另一个终端并连接到数据库testdb,然后检查连接ID:

1
2
3
4
5
6
7
mysql> SELECT CONNECTION_ID();
+-----------------+
| CONNECTION_ID() |
+-----------------+
| 11 |
+-----------------+
1 row in set (0.00 sec)

然后,从tbl检索数据,如下所示

1
2
3
4
5
6
7
mysql> SELECT * FROM tbl;
+----+-----+
| id | col |
+----+-----+
| 1 | 10 |
+----+-----+
1 row in set (0.00 sec)

接下来,从第二个会话(会话ID为7)插入一个新行到tbl表中

image-20200324130737365

第二个会话的插入操作处于等待状态,因为第一个会话已经在tbl表上获取了一个READ锁,并且尚未释放

可以使用SHOW PROCESSLIST;语句查看详细信息,如下所示

image-20200324131019621

之后,返回第一个会话并使用UNLOCK TABLES;语句来释放锁。从第一个会话释放READ锁之后,在第二个会话中执行INSERT操作

最后,查看tbl表中的数据,以查看第二个会话中的INSERT操作是否真的执行

1
2
3
4
5
6
7
8
mysql> SELECT * FROM tbl;
+----+-----+
| id | col |
+----+-----+
| 1 | 10 |
| 5 | 12 |
+----+-----+
2 rows in set (0.00 sec)

写锁演示

表锁定WRITE

表锁为WRITE具有以下功能:

  • 只有拥有表锁定的会话才能从表读取和写入数据
  • 在释放WRITE锁之前,其他会话不能从表中读写

首先,从第一个会话获取一个WRITE

1
LOCK TABLE tbl WRITE;

然后,在tbl表中插入一个新行

1
INSERT INTO tbl(col) VALUES(14);

没有问题,上面语句可能正常执行。接下来,从tbl表读取数据

1
2
3
4
5
6
7
8
9
mysql> SELECT * FROM tbl;
+----+-----+
| id | col |
+----+-----+
| 1 | 10 |
| 5 | 12 |
| 6 | 14 |
+----+-----+
3 rows in set (0.00 sec)

之后,打开第二个连接到MySQL的会话,尝试写和读数据

image-20200324132340513

image-20200324132743428

MySQL将这些操作置于等待状态。可以在第一个会话中,使用SHOW PROCESSLIST;语句来查看它

image-20200324132447232

最后,从第一个会话释放锁。执行以下语句

1
UNLOCK TABLES;

执行上面语句后,将看到第二个会话中的所有待处理已经执行操作

1
2
3
4
5
6
7
8
9
10
mysql> SELECT * FROM tbl;
+----+-----+
| id | col |
+----+-----+
| 1 | 10 |
| 5 | 12 |
| 6 | 14 |
| 7 | 20 |
+----+-----+
4 rows in set (0.00 sec)

元数据锁介绍

MDL不需要显式使用,在访问一个表的时候会被自动加上。MDL的作用是,保证读写的正确性。你可以想象一下,如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个表结构做变更,删了一列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的

因此,在MySQL 5.5 版本中引入了MDL,当对一个表做增删改查操作的时候,加MDL 读锁,当要对表做结构变更操作的时候,加MDL 写锁

读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查

读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行

元数据锁演示

image-20200324134431137

session1(Mysql1)、session2(Mysql2)

1、session1

开启事务

1
begin;

加MDL读锁

1
2
3
4
5
6
7
8
9
10
mysql> select * from tbl;
+----+-----+
| id | col |
+----+-----+
| 1 | 10 |
| 5 | 12 |
| 6 | 14 |
| 7 | 20 |
+----+-----+
4 rows in set (0.00 sec)

2、session2

修改阻塞

1
alter table tbl add size int;

image-20200324143346439

3、session1

提交事务或者rollback 释放读锁

1
commit;

4、session2

修改完成

1
2
Query OK, 0 rows affected (38.67 sec)  
Records: 0 Duplicates: 0 Warnings: 0

补充

表读锁

image-20200324144429574

session1(Navicat)、session2(mysql)

1
2
3
4
5
6
7
8
1、session1: lock table mylock read; -- 给mylock表加读锁
2、session1: select * from mylock; -- 可以查询
3、session1:select * from tdep; --不能访问非锁定表
4、session2:select * from mylock; -- 可以查询没有锁
5、session2:update mylock set name='x' where id=2; -- 修改阻塞,自动加行写锁
6、session1:unlock tables; -- 释放表锁
7、session2:Rows matched: 1 Changed: 1 Warnings: 0 -- 修改执行完成
8、session1:select * from tdep; --可以访问

表写锁

image-20200324144526704

session1(Navicat)、session2(mysql)

1
2
3
4
5
6
7
8
1、session1: lock table mylock write; -- 给mylock表加写锁
2、session1: select * from mylock; -- 可以查询
3、session1:select * from tdep; --不能访问非锁定表
4、session1:update mylock set name='y' where id=2; --可以执行
5、session2:select * from mylock; -- 查询阻塞
6、session1:unlock tables; -- 释放表锁
7、session2:4 rows in set (22.57 sec) -- 查询执行完成
8、session1:select * from tdep; --可以访问

MySQL行锁

InnoDB行锁是通过给索引上的索引项加锁来实现的,因此InnoDB这种行锁实现特点意味着:只有通过索引条件检索的数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁

行锁介绍

行锁的劣势:开销大;加锁慢;会出现死锁

行锁的优势:锁的粒度小,发生锁冲突的概率低,处理并发的能力强

加锁的方式:自动加锁。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(行写锁),对于普通SELECT语句,InnoDB不会加任何锁

当然我们也可以显式的加锁

共享锁:select * from tableName where … + lock in share more

排他锁:select * from tableName where … + for update

InnoDB和MyISAM的最大不同点有两个

一,InnoDB支持事务(transaction)

二,默认采用行级锁。加锁可以保证事务的一致性,可谓是有人(锁)的地方,就有江湖(事务)

查看行锁状态

show STATUS like 'innodb_row_lock%';

Innodb_row_lock_current_waits:当前正在等待锁定的数量

Innodb_row_lock_time:从系统启动到现在锁定总时间长度

Innodb_row_lock_time_avg:每次等待所花平均时间

Innodb_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间

Innodb_row_lock_waits:系统启动后到现在总共等待的次数

行读锁

session1(Mysql1)、session2(Mysql2)

1
2
3
4
5
6
7
8
9
10
11
1、session1: begin;--开启事务未提交
         select * from mylock  where ID=1 lock in share mode; --手动加id=1
的行读锁,使用索引
2、session2:update mylock set name='y' where id=2; -- 未锁定该行可以修改
3、session2:update mylock set name='y' where id=1; -- 锁定该行修改阻塞
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting
transaction  -- 锁定超时
4、session1: commit; --提交事务或者rollback 释放读锁
5、session2:update mylock set name='y' where id=1; --修改成功
          Query OK, 1 row affected (0.00 sec)
          Rows matched: 1 Changed: 1 Warnings: 0

注:使用索引加行锁,未锁定的行可以访问

行读锁升级为表锁

session1(Mysql1)、session2(Mysql2)

背景:name列没有加索引

1
2
3
4
5
6
7
8
9
1、session1: begin;--开启事务未提交
            --手动加name='c'的行读锁,未使用索引
            select * from mylock  where name='c' lock in share mode;
2、session2:update mylock set name='y' where id=2; -- 修改阻塞未用索引行锁升级
为表锁
3、session1: commit; --提交事务或者rollback 释放读锁
4、session2:update mylock set name='y' where id=2; --修改成功
          Query OK, 1 row affected (0.00 sec)
          Rows matched: 1 Changed: 1 Warnings: 0

注:未使用索引行锁升级为表锁

行写锁

session1(Mysql1)、session2(Mysql2)

1
2
3
4
5
6
7
8
9
1、session1: begin;--开启事务未提交
            --手动加id=1的行写锁,
            select * from mylock  where id=1 for update;
           
2、session2:select * from mylock  where id=2 ; -- 可以访问
3、session2: select * from mylock  where id=1 ; -- 可以读不加锁 
4、session2: select * from mylock  where id=1 lock in share mode ; --加读锁被阻塞
5、session1:commit; -- 提交事务或者rollback 释放写锁
6、session2:执行成功

注:主键索引产生记录锁

结论:读锁和读锁不冲突,读锁和写锁冲突,带有读锁&写锁这两个任何之一的语句和未加锁的语句是不冲突的

间隙锁

一般来说,事务的幻读问题,都是通过Seriablizable隔离级别来解决的。但是MySQL使用的间隙锁来解决了,只有在RR(可重复读)隔离级别才会产生间隙锁

image-20200324154034728

image-20200324154111628

image-20200324154123678

间隙锁防止两种情况
1、防止插入间隙内的数据

2、防止已有数据更新为间隙内的数据

间隙的范围

update news set number=3 where number=4;

number : 2 3 4

id:1 2 3 4 5

间隙情况:

id、number均在间隙内

id、number均在间隙外

id在间隙内、number在间隙外

id在间隙外,number在间隙内

id、number为边缘数据

1
2
3
4
5
6
案例演示:
mysql> create table news (id int, number int,primary key (id));
mysql> insert into news values(1,2);
......
--加非唯一索引
mysql> alter table news add index idx_num(number);

非唯一索引等值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 非唯一索引的等值
session 1:
start transaction ;
update news set number=3 where number=4;
session 2:
start transaction ;
insert into news value(2,3);#(均在间隙内,阻塞)
insert into news value(7,8);#(均在间隙外,成功)
insert into news value(2,8);#(id在间隙内,number在间隙外,成功)
insert into news value(4,8);#(id在间隙内,number在间隙外,成功)
insert into news value(7,3);#(id在间隙外,number在间隙内,阻塞)
insert into news value(7,2);# (id在间隙外,number为上边缘数据,阻塞)
insert into news value(2,2);#(id在间隙内,number为上边缘数据,阻塞)
insert into news value(7,5);#(id在间隙外,number为下边缘数据,成功)
insert into news value(4,5);#(id在间隙内,number为下边缘数据,阻塞)

结论:只要number(where后面的)在间隙里(2 3 4),不包含最后一个数(5)则不管id是多少都会阻塞

主键索引范围

1
2
3
4
5
6
7
8
9
10
11
12
--主键索引范围
session 1:
start transaction ;
update news set number=3 where id>1 and id <6;
session 2:
start transaction ;
insert into news value(2,3);#(均在间隙内,阻塞)
insert into news value(7,8);#(均在间隙外,成功)
insert into news value(2,8);#(id在间隙内,number在间隙外,阻塞)
insert into news value(4,8);#(id在间隙内,number在间隙外,阻塞)
insert into news value(7,3);#(id在间隙外,number在间隙内,成功)
--id无边缘数据,因为主键不能重复

结论:只要id(在where后面的)在间隙里(2 4 5),则不管number是多少都会阻塞。非唯一索引无穷大

session1(Navicat)、session2(mysql)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
--无穷大
session 1:
start transaction ;
update news set number=3 where number=13 ;
session 2:
start transaction ;
insert into news value(11,5);#(执行成功)
insert into news value(12,11);#(执行成功)
insert into news value(14,11);#(阻塞)
insert into news value(15,12);#(阻塞)
检索条件number=13,向左取得最靠近的值11作为左区间,向右由于没有记录因此取得无穷大作为右区
间,因此,session 1的间隙锁的范围(11,无穷大)
结论:id和number同时满足
注:非主键索引产生间隙锁,主键范围产生间隙锁

死锁

两个session 互相等等待对方的资源释放之后,才能释放自己的资源,造成了死锁

image-20200324153644093

session1(Navicat)、session2(mysql)

1
2
3
4
5
6
7
8
9
10
1、session1: begin;--开启事务未提交
            --手动加行写锁id=1 ,使用索引
           update mylock set name='m' where id=1;
2、session2:begin;--开启事务未提交
--手动加行写锁id=2 ,使用索引
           update mylock set name='m' where id=2
3、session1: update mylock set name='nn' where id=2; -- 加写锁被阻塞
4、session2:update mylock set name='nn' where id=1; -- 加写锁会死锁,不允许操作
ERROR 1213 (40001): Deadlock found when trying to get lock; try
restarting transaction

InnoDB架构分析

InnoDB架构图

image-20200324165722062

image-20200324165733234

上图详细显示了InnoDB存储引擎的体系架构,从图中可见,InnoDB存储引擎由内存池,后台线程和磁盘文件三大部分组成。接下来我们就来简单了解一下内存相关的概念和原理

InnoDB内存结构

Buffer Pool缓冲池

概述

InnoDB存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。但是由于CPU速度和磁盘速度之间的鸿沟,基于磁盘的数据库系统通常使用缓冲池记录来提高数据库的的整体性能

所以,缓冲池的大小直接影响着数据库的整体性能,可以通过配置参数innodb_buffer_pool_size来设置

具体来看,缓冲池中缓存的数据页类型有:索引页、数据页、undo页、插入缓冲(insertbuffer)、自适应哈希索引(adaptive hash index)、InnoDB存储的锁信息(lock info)和数据字典信息(data dictionary)

物理上是存在一个文件里的,但是逻辑上不是存储在一起

在架构图上可以看到,InnoDB存储引擎的内存区域除了有缓冲池之外,还有重做日志缓冲和额外内存池。InnoDB存储引擎首先将重做日志信息先放到这个缓冲区中,然后按照一定频率将其刷新到重做日志文件中。重做日志缓冲一般不需要设置的很大,该值可由配置参数innodb_log_buffer_size控制

数据页和索引页

InnoDB存储引擎工作时,需要以Page页为最小单位去将磁盘中的数据加载到内存中,与数据库相关的所有内容都存储在Page结构里。

Page分为几种类型,数据页和索引页就是其中最为重要的两种类型

插入缓冲(Insert Buffer)

主要针对次要索引的数据插入存在的问题而设计

我们都知道,在InnoDB引擎上进行插入操作时,一般需要按照主键顺序进行插入,这样才能获得较高的插入性能。当一张表中存在次要索引时,在插入时,数据页的存放还是按照主键进行顺序存放,但是对于次要索引叶节点的插入不再是顺序的了,这时就需要离散的访问次要索引页,由于随机读取的存在导致插入操作性能下降

InnoDB为此设计了Insert Buffer来进行插入优化。对于次要索引的插入或者更新操作,不是每一次都直接插入到索引页中,而是先判断插入的非主键索引是否在缓冲池中,若在,则直接插入;若不在,则先放入到一个Insert Buffer中。看似数据库这个非主键的索引已经插到叶节点,而实际没有,这时存放在另外一个位置。然后再以一定的频率和情况进行Insert Buffer和非聚簇索引页子节点的合并操作。这时通常能够将多个插入合并到一个操作中,这样就大大提高了对于非聚簇索引的插入性能

自适应哈希索引(Adaptive Hash Index)

InnoDB会根据访问的频率和模式,为热点页建立哈希索引,来提高查询效率。InnoDB存储引擎会监控对表上各个索引页的查询,如果观察到建立哈希索引可以带来速度上的提升,则建立哈希索引,所以叫做自适应哈希索引

自适应哈希索引是通过缓冲池B+树页构建而来,因此建立速度很快,而且不需要对整张数据表建立哈希索引。其有一个要求,即对这个页的连续访问模式必须是一样的,也就是说其查询的条件(WHERE)必须完全一样,而且必须是连续的

锁信息(Lock Info)

InnoDB存储引擎会在行级别上对表数据进行上锁,不过InnoDB也会在数据库内部其他很多地方使用锁,从而允许对多种不同资源提供并发访问,数据库系统使用锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性

数据字典信息(Data Dictionary)

InnoDB有自己的表缓存,可以称为表定义缓存或数据字典,当InnoDB打开一张表,就会增加一个对应的对象到数据字典

数据字典是对数据库中的数据、库对象、表对象等元素的集合。在MySQL中,数据字典信息内容就包括表结构、数据库名或表名、字段的数据类型、视图、索引、表字段信息、存储过程、触发器等内容。

内存数据落盘分析

整体思路分析

image-20200324185925517

InnoDB内存缓冲池中的数据page要完成持久化的话,是通过两个流程来完成的,一个是脏页落盘,一个是预写redo log日志,这样才能保证数据的可靠性

当缓冲池中的页的版本比磁盘要新时,数据库需要将新版本的页从缓冲池刷新到磁盘。但是如果每次一个页发送变化,就进行刷新,那么性能开发是非常大的,于是InnoDB对于数据文件和日志文件的刷盘遵守了Write AheadLog(WAL)策略和Force Log at Commit两种规则,二者保证了事务的持久性

WAL要求数据的变更写入到磁盘前,首先必须将内存中的日志写入到磁盘,即当事务提交时,先写重做日志,然后再择时将脏读写入磁盘,如果发生宕机导致数据丢失,就通过重做日志进行数据恢复

InnoDB存储引擎会首先将重做日志信息先放入重做日志缓冲中,然后再按照一定频率将其刷新到重做日志文件。重做日志缓冲一般不需要设置得很大,因为一般情况每一秒都会将重做日志缓冲刷新到日志文件中,可通过配置参数innodb_log_buffer_size控制,默认为8MB

除每秒刷新机制之外,每次事务提交时重做日志缓冲也会刷新到日志中。InnoDB是事务的存储引擎,其通过Force-log-at-commit机制实现事务的持久性,即当事务提交时,必须先将该事务的所有日志写入到重做日志文件进行持久化,然后事务的提交操作才算完成

Force-log-at-commit要求当一个事务提交时,所有产生的日志都必须刷新到磁盘上,如果日志刷新成功后,缓冲池中的数据刷新到磁盘前数据库发生了宕机,那么重启时,数据库可以从日志中恢复数据

如上图所示,InnoDB在缓冲池中变更数据时,会首先将相关变更写入重做日志缓冲中,然后再按时或者当事务提交时写入磁盘,这符合Force-log-at-commit原则;当重做日志写入磁盘后,缓冲池中的变更数据才会依据checkpoint机制择时写入到磁盘中,这符合WAL原则。 在checkpoint择时机制中,就有重做日志文件写满的判断。所以,如前文所述,如果重做日志文件太小,经常被写满,就会频繁导致checkpoint将更改的数据写入磁盘,导致性能抖动

为了确保每次日志都写入到重做日志文件,在每次将重做日志缓冲写入重做日志后,必须调用一次fsync操作(操作系统的函数),将缓冲文件从文件系统缓存中真正写入磁盘

可以通过innodb_flush_log_at_trx_commit来控制重做日志刷新到磁盘的策略

该参数的默认值为1,表示事务提交必须进行一次fsync操作(操作系统的函数),还可以设置为0和2

0表示事务提交时不进行写入重做日志操作,该操作只在主线程中完成

2表示提交时写入重做日志,但是之邪入文件系统缓存,不进行fsync操作

由此可见,设置为0时,性能最高,但是丧失了事务的一致性

捋一捋

将数据加载到内存中,是在内存中发生的修改,以page为单位加载到内存中进行存储

磁盘中页中的数据和内存中页的数据不一样的时候,就是脏页

此时就要开始两个流程,一个就是脏页落盘,一个就是预写rudo log日志

为什么要写redo log日志,因为写redo log写入速度很快,它是一种顺序写入,它的作用是先写redo log日志,之后进行脏页落盘,如果在进行脏页落盘的时候发生了宕机数据也不会丢,所以在rudo log日志中存入相应的日志数据

在进行rudo log file的时候先缓存到redo log buffer中,之后一并将redo log buffer中的数据存储到rudo log file中,这样就变相提高写rudo log日志的性能

rudo log日志只有在数据丢失的时候才会有用,如果落盘成功的话,redo log日志中相应的内容就可以被清掉

只要事务提交,就强制写入redo log file

脏页落盘

在数据库中进行读取操作,将从磁盘中读到的页放在缓冲池中,下次再读相同的页时,首先判断该页是否在缓冲池中。若在缓冲池中,称该页在缓冲池中被命中,直接读取该页。否则,读取磁盘上的页

对于数据库中页的修改操作,则首先修改在缓冲池中的页,然后再以一定的频率刷新到磁盘上。页从缓冲池刷新回磁盘的操作并不是在每次页发生更新时触发,而是通过一种称为CheckPoint的机制刷新回磁盘

重做日志落盘

InnoDB存储引擎会首先将重做日志信息先放入重做日志缓冲中,然后再按照一定频率将其刷新到重做日志文件。重做日志缓冲一般不需要设置得很大,因为一般情况每一秒钟都会讲重做日志缓冲刷新到日志文件中。可通过配置参数innodb_log_buffer_size控制,默认为8MB

CheckPoint检查点机制

简介

思考一下这个场景:如果重做日志可以无限地增大,同时缓冲池也足够大,那么是不需要将缓冲池中页的新版本刷新回磁盘。因为当发生宕机时,完全可以通过重做日志来恢复整个数据库系统中的数据到宕机发生的时刻

但是这需要两个前提条件:1. 缓冲池可以缓存数据库中所有的数据,2. 重做日志可以无限增大

因此Checkpoint(检查点)技术就诞生了,目的是解决以下几个问题

1、缩短数据库的恢复时间

2、缓冲池不够用时,将脏页刷新到磁盘

3、重做日志不可用时,刷新脏页

当数据库发生宕机时,数据库不需要重做所有的日志,因为Checkpoint之前的页都已经刷新回磁盘。数据库只需对Checkpoint后的重做日志进行恢复,这样就大大缩短了恢复的时间

当缓冲池不够用时,根据LRU算法会溢出最近最少使用的页,若此页为脏页,那么需要强制执行Checkpoint,将脏页也就是页的新版本刷回磁盘

当重做日志出现不可用时,因为当前事务数据库系统对重做日志的设计都是循环使用的,并不是让其无限增大的。重做日志可以被重用的部分是指这些重做日志已经不再需要,当数据库发生宕机时,数据库恢复操作不需要这部分的重做日志,因此这部分就可以被覆盖重用。如果重做日志还需要使用,那么必须强制Checkpoint,将缓冲池中的页至少刷新到当前重做日志的位置

对于InnoDB存储引擎而言,是通过LSN(Log Sequence Number)来标记版本的

Checkpoint发生的时间、条件及脏页的选择等都非常复杂。而Checkpoint所做的事情无外乎是将缓冲池中的脏页刷回到磁盘,不同之处在于每次刷新多少页到磁盘,每次从哪里取脏页,以及什么时间触发Checkpoint

Checkpoint分类

在InnoDB存储引擎内部,有两种Checkpoint,分别为:Sharp Checkpoint、Fuzzy Checkpoint

sharp checkpoint:在关闭数据库的时候,将buffer pool中的脏页全部刷新到磁盘中

fuzzy checkpoint:数据库正常运行时,在不同的时机,将部分脏页写入磁盘。仅刷新部分脏页到磁盘,也是为了避免一次刷新全部的脏页造成的性能问题

Fuzzy Checkpoint:

1、Master Thread Checkpoint

在Master Thread中,会以每秒或者每10秒一次的频率,将部分脏页从内存中刷新到磁盘,这个过程是异步的。正常的用户线程对数据的操作不会被阻塞

2、FLUSH_LRU_LIST Checkpoint

FLUSH_LRU_LIST checkpoint是在单独的page cleaner线程中执行的

MySQL对缓存的管理是通过buffer pool中的LRU列表实现的,LRU 空闲列表中要保留一定数量的空闲页面,来保证buffer pool中有足够的空闲页面来相应外界对数据库的请求

当这个空间页面数量不足的时候,发生FLUSH_LRU_LIST checkpoint

空闲页的数量由innodb_lru_scan_depth参数表来控制的,因此在空闲列表页面数量少于配置的值的时候,会发生checkpoint,剔除部分LRU列表尾端的页面

image-20200325201401026

3、Async/Sync Flush Checkpoint

Async/Sync Flush checkpoint是在单独的page cleaner线程中执行的

Async/Sync Flush checkpoint 发生在重做日志不可用的时候,将buffer pool中的一部分脏页刷新到磁盘中,在脏页写入磁盘之后,事物对应的重做日志也就可以释放了

关于redo_log文件的的大小,可以通过innodb_log_file_size 来配置

对于是执行Async Flush checkpoint还是Sync Flush checkpoint,由checkpoint_age以及async_water_mark和sync_water_mark来决定

1
2
3
4
##即checkpoint_age等于最新的lsn减去已经刷新到磁盘的lsn的值
checkpoint_age = redo_lsn-checkpoint_lsn
async_water_mark = 75%*innodb_log_file_size
sync_water_mark = 90%*innodb_log_file_size
  1. 当checkpoint_age<sync_water_mark的时候,无需执行Flush checkpoint。也就说,redo
    log剩余空间超过25%的时候,无需执行Async/Sync Flush checkpoint
  2. 当async_water_mark<checkpoint_age<sync_water_mark的时候,执行Async Flush
    checkpoint,也就说,redo log剩余空间不足25%,但是大于10%的时候,执行Async Flush
    checkpoint,刷新到满足条件1
  3. 当checkpoint_age>sync_water_mark的时候,执行sync Flush checkpoint。也就说,redo
    log剩余空间不足10%的时候,执行Sync Flush checkpoint,刷新到满足条件1。
    在mysql 5.6之后,不管是Async Flush checkpoint还是Sync Flush checkpoint,都不会阻
    塞用户的查询进程

总结

由于磁盘是一种相对较慢的存储设备,内存与磁盘的交互是一个相对较慢的过程由于innodb_log_file_size定义的是一个相对较大的值,正常情况下,由前面两种checkpoint刷新脏页到磁盘,在前面两种checkpoint刷新脏页到磁盘之后,脏页对应的redo log空间随即释放,一般不会发生Async/Sync Flush checkpoint。同时也要意识到,为了避免频繁低发生Async/SyncFlush checkpoint,也应该将innodb_log_file_size配置的相对较大一些

4、Dirty Page too much Checkpoint

Dirty Page too much Checkpoint是在Master Thread 线程中每秒一次的频率实现的

Dirty Page too much 意味着buffer pool中的脏页过多,执行checkpoint脏页刷入磁盘,保证buffer pool中有足够的可用页面

Dirty Page 由innodb_max_dirty_pages_pct配置,innodb_max_dirty_pages_pct的默认值在innodb 1.0之前是90%,之后是75%

Double Write双写

如果说Insert Buffer给InnoDB存储引擎带来了性能上的提升,那么Double Write带给InnoDB存储引擎的是数据页的可靠性

image-20200325082955488

如上图所示,Double Write由两部分组成,一部分是内存中的double write buffer,大小为2MB,另一部分是物理磁盘上共享表空间连续的128个页,大小也为2MB

在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是通过memcpy函数将脏页先复制到内存中的double write buffer区域,之后通过double write buffer再分两次,每次1MB顺序地写入共享表空间的物理磁盘上,然后马上调用fsync函数,同步磁盘,避免操作系统缓冲写带来的问题。在完成doublewrite页的写入后,再讲double wirite buffer中的页写入各个表空间文件中

如果操作系统在将页写入磁盘的过程中发生了崩溃,在恢复过程中,InnoDB存储引擎可以从共享表空间中的double write中找到该页的一个副本,将其复制到表空间文件中,再应用重做日志

Redo log Buffer重做日志缓冲

image-20200325083053080

如上图所示,InnoDB在缓冲池中变更数据时,会首先将相关变更写入重做日志缓冲中,然后再按时或者当事务提交时写入磁盘,这符合Force-log-at-commit原则

当重做日志写入磁盘后,缓冲池中的变更数据才会依据checkpoint机制择时写入到磁盘中,这符合WAL原则

在checkpoint择时机制中,就有重做日志文件写满的判断,所以,如前文所述,如果重做日志文件太小,经常被写满,就会频繁导致checkpoint将更改的数据写入磁盘,导致性能抖动

操作系统的文件系统是带有缓存的,当InnoDB向磁盘写入数据时,有可能只是写入到了文件系统的缓存中,没有真正的“落袋为安”

InnoDB的innodb_flush_log_at_trx_commit属性可以控制每次事务提交时InnoDB的行为

当属性值为0时,事务提交时,不会对重做日志进行写入操作,而是等待主线程按时写入

当属性值为1时,事务提交时,会将重做日志写入文件系统缓存,并且调用文件系统的fsync,将文件系统缓冲中的数据真正写入磁盘存储,确保不会出现数据丢失

当属性值为2时,事务提交时,也会将日志文件写入文件系统缓存,但是不会调用fsync,而是让文件系统自己去判断何时将缓存写入磁盘

innodb_flush_log_at_commit是InnoDB性能调优的一个基础参数,涉及InnoDB的写入效率和数据安全。

当参数值为0时,写入效率最高,但是数据安全最低

参数值为1时,写入效率最低,但是数据安全最高

参数值为2时,二者都是中等水平。

一般建议将该属性值设置为1,以获得较高的数据安全性,而且也只有设置为1,才能保证事务的持久性

日志的刷盘机制如下图所示

image-20200324165324400

该参数默认值为1
可以通过innodb_flush_log_at_trx_commit来控制重做日志刷新到磁盘的策略。该参数默认值为1,表示事务提交必须进行一次fsync操作,还可以设置为0和2。

0表示事务提交时不进行写入重做日志操作,该操作只在主线程中完成

2表示提交时写入重做日志,但是只写入文件系统缓存,不进行fsync操作

由此可见,设置为0时,性能最高,但是丧失了事务的一致性。

InnoDB磁盘文件

InnoDB的主要的磁盘文件主要分为三大块:一是系统表空间,二是用户表空间,三是redo日志文件和归档文件。二进制文件(binlog)等文件是MySQL Server层维护的文件,所以未列入InnoDB的磁盘文件中

系统表空间和用户表空间

image-20200325153751857

上图显示InnoDB存储引擎对于文件的存储方式,其中frm文件是表结构定义文件,记录每个表的表结构定义

系统表空间存储哪些数据

系统表空间是一个共享的表空间,因为它是被多个表共享的

InnoDB系统表空间包含InnoDB数据字典(元数据以及相关对象)、double write buffer、change buffer、undo logs的存储区域

系统表空间也默认包含任何用户在系统表空间创建的表数据和索引数据。系统表空间是一个共享的表空间因为它是被多个表共享的

1、数据字典(data dictionary):记录数据库相关信息

2、doublewrite write buffer:解决部分写失败(页断裂)

3、insert buffer:内存insert buffer数据,周期写入共享表空间,防止意外宕机

4、回滚段(rollback segments)

5、undo空间:undo页

系统表空间也默认包含任何用户在系统表空间创建的表数据和索引数据

系统表空间是以每个数据库为单位的

系统表空间配置解析

系统表空间是由一个或者多个数据文件组成

默认情况下,一个初始大小为10MB,名为ibdata1的系统数据文件在MySQL的data目录下被创建。用户可以使用innodb_data_file_path对数据文件的大小和数量进行配置

1
2
3
4
5
6
7
8
mysql> show variables like 'innodb_data%';
+-----------------------+------------------------+
| Variable_name | Value |
+-----------------------+------------------------+
| innodb_data_file_path | ibdata1:12M:autoextend |
| innodb_data_home_dir | |
+-----------------------+------------------------+
2 rows in set (0.01 sec)

innodb_data_file_path 的格式如下

1
innodb_data_file_path=datafile1[,datafile2]...

用户可以通过多个文件组成一个表空间,同时制定文件的属性

1
innodb_data_file_path = /db/ibdata1:1000M;/dr2/db/ibdata2:1000M:autoextend

这里将/db/ibdata1和/dr2/db/ibdata2两个文件组成系统表空间

如果这两个文件位于不同的磁盘上,磁盘的负载可能被平均,因此可以提高数据库的整体性能

两个文件的文件名之后都跟了属性,表示文件ibdata1的大小为1000MB,文件ibdata2的大小为1000MB,而且用完空间之后可以自动增长(autoextend)

设置innodb_data_file_path参数之后,所有基于InnoDB存储引擎的表的数据都会记录到该系统表空间中,如果设置了参数innodb_file_per_table,则用户可以将每个基于InnoDB存储引擎的表产生一个独立的用户表空间。用户表空间的命名规则为:表名.ibd。

通过这种方式,用户不用将所有数据都存放于默认的系统表空间中,但是用户表空间只存储该表的数据、索引和插入缓冲BITMAP等信息,其余信息还是存放在默认的表空间中

innodb_data_file_path用来指定innodb tablespace文件,如果我们不在My.cnf文件中指定innodb_data_home_dir和innodb_data_file_path那么默认会在datadir目录下创建ibdata1 作为innodb tablespace

如何使用用户表空间

如果设置了参数innodb_file_per_table,则用户可以将每个基于InnoDB存储引擎的表产生一个独立的用户表空间。用户表空间的命名规则为:表名.ibd

通过这种方式,用户不用将所有数据都存放于默认的系统表空间中

用户表空间存储哪些数据

用户表空间只存储该表的数据、索引和插入缓冲BITMAP等信息,其余信息还是存放在默认的系统表空间中

1、每个表的数据和索引都会存在自已的表空间中

2、每个表的结构

3、undo空间:undo页(需要设置)

重做日志文件和归档文件

哪些文件是重做日志文件

默认情况下,在InnoDB存储引擎的数据目录下会有两个名为ib_logfile0ib_logfile1的文件,这就是InnoDB的重做日志文件(redo log file),它记录了对于InnoDB存储引擎的事务日志

重做日志文件的作用是什么

当InnoDB的数据存储文件发生错误时,重做日志文件就能派上用场。InnoDB存储引擎可以使用重做日志文件将数据恢复为正确状态,以此来保证数据的正确性和完整性

为了得到更高的可靠性,用户可以设置多个镜像日志组,将不同的文件组放在不同的磁盘上,以此来提高重做日志的高可用性

重做日志文件组是如何写入数据的

每个InnoDB存储引擎至少有1个重做日志文件组(group),每个文件组下至少有2个重做日志文件,如默认的ib_logfile0ib_logfile1

在日志组中每个重做日志文件的大小一致,并以循环写入的方式运行

InnoDB存储引擎先写入重做日志文件1,当文件被写满时,会切换到重做日志文件2,再当重做日志文件2也被写满时,再切换到重做日志文件1

如何设置重做日志文件大小

用户可以使用innodb_log_file_size来设置重做日志文件的大小,这对InnoDB存储引擎的性能有着非常大的影响

如果重做日志文件设置的太大,数据丢失时,恢复时可能需要很长的时间

另一方面,如果设置的太小,重做日志文件太小会导致依据checkpoint的检查需要频繁刷新脏页到磁盘中,导致性能的抖动

InnoDB的事务分析

image-20200324191044587

数据库事务具有ACID四大特性。ACID是以下4个词的缩写:

  • 原子性(atomicity) :事务最小工作单元,要么全成功,要么全失败
  • 一致性(consistency):事务开始和结束后,数据库的完整性不会被破坏
  • 隔离性(isolation) :不同事务之间互不影响,四种隔离级别为RU(读未提交)、RC(读已提交)、RR(可重复读)、SERIALIZABLE (串行化)
  • 持久性(durability) :事务提交后,对数据的修改是永久性的,即使系统故障也不会丢失

下面我们就来详细讲解一下上述示例涉及的事务的ACID特性的具体实现原理。总结来说,事务的隔离性由多版本控制机制和锁实现,而原子性、一致性和持久性通过InnoDB的redo log、undo log和Force Log at Commit机制来实现

原子性,持久性和一致性

原子性,持久性和一致性主要是通过redo logundo logForce Log at Commit机制机制来完成的。redo log用于在崩溃时恢复数据,undo log用于对事务的影响进行撤销,也可以用于多版本控制。而Force Log at Commit机制保证事务提交后redo log日志都已经持久化

RedoLog

数据库日志和数据落盘机制,如下图所示

image-20200324191302065

redo log写入磁盘时,必须进行一次操作系统的fsync操作,防止redo log只是写入了操作系统的磁盘缓存中。参数innodb_flush_log_at_trx_commit可以控制redo log日志刷新到磁盘的策略

UndoLog

UndoLog没有专门存储成一个文件,它存储到系统表空间里,UndoLog有两个作用,一个是用来做事务回滚,一个是用来做MVCC的版本记录

UndoLog中分为两类进行存储

insert undolog:做insert插入操作时,产生的回滚日志

update undolog:做delete和update操作时,产生的回滚日志

undolog没有单独的文件,而是存储到系统表空间中的(ibdata1)

数据库崩溃重启后需要从redo log中把未落盘的脏页数据恢复出来,重新写入磁盘,保证用户的数据不丢失。当然,在崩溃恢复中还需要回滚没有提交的事务。由于回滚操作需要undo日志的支持,undo日志的完整性和可靠性需要redo日志来保证,所以崩溃恢复先做redo恢复数据,然后做undo回滚

image-20200324191417286

在事务执行的过程中,除了记录redo log,还会记录一定量的undo log。undo log记录了数据在每个操作前的状态,如果事务执行过程中需要回滚,就可以根据undo log进行回滚操作

image-20200324191431981

事前准备

第一种其实没有加锁,串行化就是读写都加锁,中间两种是mvcc,即 读不加锁写加锁

聚集索引和非聚集索引

主键索引是聚集索引,辅助索引是非聚集索引

|