diff --git a/docs/Java/Java.md b/docs/Java/Java.md index 350905f..9be2f49 100644 --- a/docs/Java/Java.md +++ b/docs/Java/Java.md @@ -408,17 +408,7 @@ finalize **final数据**: -许多程序设计语言都有自己的办法告诉编译器某个数据是“常数”。常数主要应用于下述两个方面: - -(1) 编译期常数,它永远不会改变 - -(2) 在运行期初始化的一个值,我们不希望它发生变化 - -对于编译期的常数,编译器(程序)可将常数值“封装”到需要的计算过程里。也就是说,计算可在编译期间提前执行,从而节省运行时的一些开销。在 Java 中,这些形式的常数必须属于基本数据类型(Primitives),而且要用 final 关键字进行表达。在对这样的一个常数进行定义的时候,必须给出一个值。 - -无论static 还是 final 字段,都只能存储一个数据,而且不得改变。 - -对于基本数据类型,final 会将值变成一个常数;但对于对象句柄,final 会将句柄变成一个常数。进行声明时,必须将句柄初始化到一个具体的对象。而且永远不能将句柄变成指向另一个对象。然而,对象本身是可以修改的。Java 对此未提供任何手段,可将一个对象直接变成一个常数(但是,我们可自己编写一个类,使其中的对象具有 “常数”效果)。这一限制也适用于数组,它也属于对象。 +对于基本数据类型,final 会将值变成一个常数;但对于对象句柄,final 会将句柄变成一个常数。进行声明时,必须将句柄初始化到一个具体的对象。而且永远不能将句柄变成指向另一个对象。然而,对象本身是可以修改的。Java 对此未提供任何手段,可将一个对象直接变成一个常数。这一限制也适用于数组,它也属于对象。 final数据必须赋初值,以后不能修改,初始化位置: @@ -442,7 +432,7 @@ final数据必须赋初值,以后不能修改,初始化位置: **final类**: -将类定义成 final 后,结果只是禁止进行继承——没有更多的限制。然 而,由于它禁止了继承,所以一个 final 类中的所有方法都默认为final。因为此时再也无法覆盖它们。所以与我们将一个方法明确声明为final 一样,编译器此时有相同的效率选择。 +将类定义成 final 后,结果只是禁止进行继承——没有更多的限制。然而,由于它禁止了继承,所以一个 final 类中的所有方法都默认为final。因为此时再也无法覆盖它们。所以与我们将一个方法明确声明为final 一样,编译器此时有相同的效率选择。 @@ -593,25 +583,33 @@ enum Season ![](./Java/异常.jpeg) -执行过程中发生的异常分为两大类 +Exception 和 Error 都是继承了 Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。 + +Error 是指在正常情况下,不大可能出现的情况,绝大部分的 Error 都会导致程序(比如 JVM 自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如 OutOfMemoryError 之类,都是 Error 的子类。 + +Exception 是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。 + +Exception 又分为**可检查**(checked)异常和**不检查**(unchecked)异常。 + +可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。如果受检查异常没有被 `catch`或者`throws` 关键字处理的话,就没办法通过编译。 -1. Error(错误):Java虚拟机无法解决的严重问题,如`JVM`系统内部错误、资源耗尽等。比如`StackOverflowError`和`OOM(out of memory)` +不检查异常就是所谓的运行时异常,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。 -2. Exception:其他因编程或偶然的外在因素导致的一般问题,可以使用针对性的代码处理,分为两大类:运行时异常和编译时异常。编译异常程序中必须处理,运行时异常程序中没有处理默认是`throws` - 1. 运行时异常: - 1. 空指针异常 NullPointerException - 2. 数学运算异常 ArithmeticException - 3. 数组下标越界异常 ArrayIndexOutOfBoundsException - 4. 类型转换异常 ClassCastException - 5. 数字格式不正确异常 NumberFormatException +编译时异常(可检查异常): - 2. 编译时异常: - 1. 数据库操作异常 SQLException - 2. 文件操作异常 IOException - 3. 文件不存在 FileNotFoundException - 4. 类不存在 ClassNotFoundException - 5. 文件末尾发生异常 EOPException - 6. 参数异常 IllegalArguementException +1. 数据库操作异常 SQLException +2. 文件操作异常 IOException +3. 文件不存在 FileNotFoundException +4. 类不存在 ClassNotFoundException +5. 文件末尾发生异常 EOPException +6. 参数异常 IllegalArguementException + +运行时异常(不检查异常): +1. 空指针异常 NullPointerException +2. 数学运算异常 ArithmeticException +3. 数组下标越界异常 ArrayIndexOutOfBoundsException +4. 类型转换异常 ClassCastException +5. 数字格式不正确异常 NumberFormatException @@ -643,7 +641,7 @@ finally子句中包含return语句时肯产生意想不到的结果,finally中 编译异常程序中必须处理,运行时异常程序中没有处理默认是`throws` -子类重写父类的方法时,对抛出异常的规定:子类重写的方法抛出的异常类型要么和父类抛出的异常一致,要么为父类抛出异常类型的子类型 +子类重写父类的方法时,对抛出异常的规定:子类重写的方法抛出的异常类型要么和父类抛出的异常一致,要么为父类抛出异常类型的子类型。 在`throws`过程中,如果有`try-catch`,相当于处理异常,就可以不必`throws` @@ -699,7 +697,7 @@ try (Resource res = ...) { // 可以指定多个资源, ';'间隔 `String` 真正不可变有下面几点原因: -1. 保存字符串的数组被 `final` 修饰且为私有的,并且`String` 类没有提供/暴露修改这个字符串的方法。 +1. 保存字符串的数组被 `final` 修饰且为私有的,并且`String` 类没有提供/暴露修改这个字符串的方法。拼接、裁剪字符串等动作,都会产生新的 String 对象。 2. `String` 类被 `final` 修饰导致其不能被继承,进而避免了子类破坏 `String` 不可变。 `String` 中的 `equals` 方法是被重写过的,比较的是 String 字符串的值是否相等。 `Object` 的 `equals` 方法是比较的对象的内存地址。 @@ -714,7 +712,7 @@ try (Resource res = ...) { // 可以指定多个资源, ';'间隔 和StringBuffer本质上没什么区别,就是去掉了保证线程安全的那部分,减少了开销。 -`StringBuilder` 与 `StringBuffer` 都继承自 `AbstractStringBuilder` 类,在 `AbstractStringBuilder` 中使用char数组保存字符串(JDK9以后是byte数组),不过没有使用 `final` 和 `private` 关键字修饰,是**可变的**, +`StringBuilder` 与 `StringBuffer` 都继承自 `AbstractStringBuilder` 类,在 `AbstractStringBuilder` 中使用char数组保存字符串(JDK9以后是byte数组),不过没有使用 `final` 和 `private` 关键字修饰,是**可变的**,构建时初始字符串长度加 16。 @@ -982,8 +980,6 @@ Java序列化机制只会保存对象的实例变量的状态,而不会保存 反射机制允许程序在执行期间借助于ReflectionAPI取得任何类的内部信息,并能操作对象的属性及方法。 -加载完类之后,在堆中产生一个Class类型的对象(一个类只有一个Class对象),这个对象包含类的完整结构信息。 - 反射相关的主要类: ```java @@ -1117,23 +1113,22 @@ getType:以Class形式返回类型 -## 代理模式 - -### 静态代理 - -需要对每个目标类都单独写一个代理类,不灵活且麻烦 - -### 动态代理 +## 动态代理 动态代理是在运行时动态生成类字节码,并加载到 JVM 中 -#### JDK动态代理 +### JDK动态代理 基于接口的,代理类一定是有定义的接口,在 Java 动态代理机制中 `InvocationHandler` 接口和 `Proxy` 类是核心。 `Proxy` 类中使用频率最高的方法是:`newProxyInstance()` ,这个方法主要用来生成一个代理对象。 ```java +/** + * loader :类加载器,用于加载代理对象。 + * interfaces : 被代理类实现的一些接口; + * h : 实现了 `InvocationHandler` 接口的对象; + */ public static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) @@ -1143,12 +1138,6 @@ public static Object newProxyInstance(ClassLoader loader, } ``` -这个方法一共有 3 个参数: - -1. **loader** :类加载器,用于加载代理对象。 -2. **interfaces** : 被代理类实现的一些接口; -3. **h** : 实现了 `InvocationHandler` 接口的对象; - 要实现动态代理的话,还必须需要实现`InvocationHandler` 来自定义处理逻辑。 当我们的动态代理对象调用一个方法时,这个方法的调用就会被转发到实现`InvocationHandler` 接口类的 `invoke` 方法来调用。 ```java @@ -1156,17 +1145,16 @@ public interface InvocationHandler { /** * 当使用代理对象调用方法的时候实际会调用到这个方法 + * proxy :动态生成的代理类 + * method : 与代理类对象调用的方法相对应 + * args : 当前 method 方法的参数 */ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; } ``` -1. **proxy** :动态生成的代理类 -2. **method** : 与代理类对象调用的方法相对应 -3. **args** : 当前 method 方法的参数 - -**通过`Proxy` 类的 `newProxyInstance()` 创建的代理对象在调用方法的时候,实际会调用到实现`InvocationHandler` 接口的类的 `invoke()`方法。** 可以在 `invoke()` 方法中自定义处理逻辑,比如在方法执行前后做什么事情。 +通过`Proxy` 类的 `newProxyInstance()` 创建的代理对象在调用方法的时候,实际会调用到实现`InvocationHandler` 接口的类的 `invoke()`方法。 可以在 `invoke()` 方法中自定义处理逻辑,比如在方法执行前后做什么事情。 @@ -1176,9 +1164,43 @@ public interface InvocationHandler { 2. 自定义代理类实现 `InvocationHandler`接口并重写`invoke`方法,在 `invoke` 方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑; 3. 通过 `Proxy.newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h)` 方法创建代理对象; +```java +interface Hello { + void sayHello(); +} +class HelloImpl implements Hello { + @Override + public void sayHello() { + System.out.println("Hello World"); + } +} +class MyInvocationHandler implements InvocationHandler { + private Object target; + public MyInvocationHandler(Object target) { + this.target = target; + } + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + System.out.println("Invoking sayHello"); + Object result = method.invoke(target, args); + return result; + } +} +public class MyDynamicProxy { + public static void main (String[] args) { + HelloImpl hello = new HelloImpl(); + MyInvocationHandler handler = new MyInvocationHandler(hello); + // 构造代码实例 + Hello proxyHello = (Hello) Proxy.newProxyInstance(HelloImpl.class.getClassLoader(), HelloImpl.class.getInterfaces(), handler); + // 调用代理方法 + proxyHello.sayHello(); + } +} +``` -#### CGLIB 动态代理 + +### CGLIB 动态代理 CGLIB基于ASM字节码生成工具,它通过继承的方式实现代理类,所以不需要接口,可以代理普通类,但需要注意 final 方法(不可继承)。 @@ -1187,11 +1209,20 @@ CGLIB基于ASM字节码生成工具,它通过继承的方式实现代理类, 你需要自定义 `MethodInterceptor` 并重写 `intercept` 方法,`intercept` 用于拦截增强被代理类的方法。 ```java -public class ServiceMethodInterceptor implements MethodInterceptor{ - // 拦截被代理类中的方法 +public class ServiceMethodInterceptor implements MethodInterceptor { + @Override - public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,MethodProxy proxy) throws Throwable { - + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + // 在方法调用之前执行 + System.out.println("Before invoking method: " + method.getName()); + + // 调用原始目标类的方法 + Object result = proxy.invokeSuper(obj, args); + + // 在方法调用之后执行 + System.out.println("After invoking method: " + method.getName()); + + return result; } } ``` diff --git "a/docs/Java/Java\351\233\206\345\220\210.md" "b/docs/Java/Java\351\233\206\345\220\210.md" index 048585b..5ddcd10 100644 --- "a/docs/Java/Java\351\233\206\345\220\210.md" +++ "b/docs/Java/Java\351\233\206\345\220\210.md" @@ -16,7 +16,7 @@ Collection接口没有直接的实现子类,是通过它的子接口Set、List -和Array区别? +和数组区别? 1. 大小和自动扩容 2. 支持泛型 @@ -203,7 +203,7 @@ BlockingQueue的实现类: 线程不安全,保证线程安全就选用 `ConcurrentHashMap`。 -`HashMap` 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个 +`HashMap` 可以存储 null 的 key 和 value,通常情况下,HashMap 进行 put 或者 get 操作,可以达到常数时间的性能,所以它是绝大部分利用键值对存取场景的首选。 JDK1.8 之前 `HashMap` 由数组+链表组成的,数组是 `HashMap` 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 @@ -263,7 +263,9 @@ JDK1.8 之前 `HashMap` 由数组+链表组成的,数组是 `HashMap` 的主 ### ConcurrentHashMap -Java 7 中 `ConcurrnetHashMap` 由很多个 `Segment` 组合,而每一个 `Segment` 是一个类似于 `HashMap` 的结构,所以每一个 `HashMap` 的内部可以进行扩容。但是 `Segment` 的个数一旦**初始化就不能改变**,默认 `Segment` 的个数是 16 个,你也可以认为 `ConcurrentHashMap` 默认支持最多 16 个线程并发。 +Java 7 中 `ConcurrnetHashMap` 由很多个 `Segment` 组合,而每一个 `Segment` 是一个类似于 `HashMap` 的结构,所以每一个 `HashMap` 的内部可以进行扩容。但是 `Segment` 的个数一旦**初始化就不能改变**,默认 `Segment` 的个数是 16 个,可以认为 `ConcurrentHashMap` 默认支持最多 16 个线程并发。 + + Java 8 中 不再是之前的 **Segment 数组 + HashEntry 数组 + 链表**,而是 **Node 数组 + 链表 / 红黑树**。当冲突链表达到一定长度时,链表会转换成红黑树。 @@ -322,9 +324,11 @@ for (int i = 1; i <= 5; i++) { ### Hashtable -* 键和值都不能为空 -* 使用方法基本和HashMap一样 -* Hashtable是线程安全的,通过在每个⽅法上添加同步关键字来实现的,但这也可能 导致性能下降。 +键和值都不能为null + +使用方法基本和HashMap一样 + +Hashtable是线程安全的,通过在每个⽅法上添加同步关键字来实现的,但这也可能导致性能下降。 ``` 底层数组Hashtable$Entry[] 初始化大小 11 @@ -346,7 +350,7 @@ for (int i = 1; i <= 5; i++) { ### TreeMap -基于红黑树数据结构的实现的 +TreeMap 则是基于红黑树的一种提供顺序访问的 Map,和 HashMap 不同,它的 get、put、remove 之类操作都是 O(log(n))的时间复杂度。 实现 `NavigableMap` 接口让 `TreeMap` 有了对集合内元素的搜索的能力。 diff --git "a/docs/Java/Java\351\233\206\345\220\210/concurentHashMap-Java7.jpeg" "b/docs/Java/Java\351\233\206\345\220\210/concurentHashMap-Java7.jpeg" new file mode 100644 index 0000000..7575f7f Binary files /dev/null and "b/docs/Java/Java\351\233\206\345\220\210/concurentHashMap-Java7.jpeg" differ diff --git a/docs/MySQL/MySQL.md b/docs/MySQL/MySQL.md new file mode 100644 index 0000000..6ea272f --- /dev/null +++ b/docs/MySQL/MySQL.md @@ -0,0 +1,1468 @@ +[MySQL :: MySQL 8.4 Reference Manual](https://dev.mysql.com/doc/refman/8.4/en/) + + + +## select 语句的执行过程 + +![](MySQL\基础架构.png) + + + +### 连接器 + +作用: + +1. 与客户端进行TCP三次握手建立连接 +2. 校验用户名、密码 +3. 校验权限 + + + +`MySQL`也会“安排”一条线程维护当前客户端的连接,这条线程也会时刻标识着当前连接在干什么工作,可以通过`show processlist;`命令查询所有正在运行的线程。 + + + +MySQL的最大线程数可以通过参数`max-connections`来控制,如果到来的客户端连接超出该值时,新到来的连接都会被拒绝,关于最大连接数的一些命令主要有两条: + +- `show variables like '%max_connections%';`:查询目前`DB`的最大连接数。默认151 +- `set GLOBAL max_connections = 200;`:修改数据库的最大连接数为指定值。 + + + +MySQL 定义了空闲连接的最大空闲时长,由 `wait_timeout` 参数控制的,默认值是 8 小时(28880秒),如果空闲连接超过了这个时间,连接器就会自动将它断开。 + +一个处于空闲状态的连接被服务端主动断开后,这个客户端并不会马上知道,等到客户端在发起下一个请求的时候,才会收到这样的报错“ERROR 2013 (HY000): Lost connection to MySQL server during query”。 + + + +MySQL 的连接也跟 HTTP 一样,有短连接和长连接的概念。长连接的好处就是可以减少建立连接和断开连接的过程,但是,使用长连接后可能会占用内存增多。有两种解决方式: + +第一种,**定期断开长连接**。既然断开连接后就会释放连接占用的内存资源,那么我们可以定期断开长连接。 + +第二种,**客户端主动重置连接**。MySQL 5.7 版本实现了 `mysql_reset_connection()` 函数的接口,注意这是接口函数不是命令,那么当客户端执行了一个很大的操作后,在代码里调用 mysql_reset_connection 函数来重置连接,达到释放内存的效果。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。 + + + +### 查询缓存 + +连接器的工作完成后,客户端就可以向 MySQL 服务发送 SQL 语句了,MySQL 服务收到 SQL 语句后,就会解析出 SQL 语句的第一个字段,看看是什么类型的语句。 + +如果 SQL 是查询语句(select 语句),MySQL 就会先去查询缓存( Query Cache )里查找缓存数据,看看之前有没有执行过这一条命令,这个查询缓存是以 key-value 形式保存在内存中的,key 为 SQL 查询语句,value 为 SQL 语句查询的结果。 + +MySQL 8.0 版本已经将查询缓存删掉。 + + + +### 解析SQL + +第一件事情,**词法分析**。MySQL 会根据你输入的字符串识别出关键字出来,例如,SQL语句 select username from userinfo,在分析之后,会得到4个Token,其中有2个Keyword,分别为select和from。 + +第二件事情,**语法分析**。根据词法分析的结果,语法解析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法,如果没问题就会构建出 SQL 语法树,这样方便后面模块获取 SQL 类型、表名、字段名、 where 条件等等。 + + + +`SQL`语句分为五大类: + +- `DML`:数据库操作语句,比如`update、delete、insert`等都属于这个分类。 +- `DDL`:数据库定义语句,比如`create、alter、drop`等都属于这个分类。 +- `DQL`:数据库查询语句,比如最常见的`select`就属于这个分类。 +- `DCL`:数据库控制语句,比如`grant、revoke`控制权限的语句都属于这个分类。 +- `TCL`:事务控制语句,例如`commit、rollback、setpoint`等语句属于这个分类。 + + + +存储过程:是指提前编写好的一段较为常用或复杂`SQL`语句,然后指定一个名称存储起来,然后先经过编译、优化,完成后,这个“过程”会被嵌入到`MySQL`中。 + + + +触发器则是一种特殊的存储过程,但[触发器]与[存储过程]的不同点在于:**存储过程需要手动调用后才可执行,而触发器可由某个事件主动触发执行**。在`MySQL`中支持`INSERT、UPDATE、DELETE`三种事件触发,同时也可以通过`AFTER、BEFORE`语句声明触发的时机,是在操作执行之前还是执行之后。 + + + +### 执行SQL + +#### 预处理阶段 + +检查 SQL 查询语句中的表或者字段是否存在; + +将 `select *` 中的 `*` 符号,扩展为表上的所有列; + +#### 优化阶段 + +**优化器主要负责将 SQL 查询语句的执行方案确定下来**,比如在表里面有多个索引的时候,优化器会基于查询成本的考虑,来决定选择使用哪个索引。 + +`MySQL`优化器的一些优化准则如下: + +- 多条件查询时,重排条件先后顺序,将效率更好的字段条件放在前面。 + +- 当表中存在多个索引时,选择效率最高的索引作为本次查询的目标索引。 + +- 使用分页`Limit`关键字时,查询到对应的数据条数后终止扫表。 + +- 多表`join`联查时,对查询表的顺序重新定义,同样以效率为准。 + +- 对于`SQL`中使用函数时,如`count()、max()、min()...`,根据情况选择最优方案。 + +- - `max()`函数:走`B+`树最右侧的节点查询(大的在右,小的在左)。 + - `min()`函数:走`B+`树最左侧的节点查询。 + - `count()`函数:如果是`MyISAM`引擎,直接获取引擎统计的总行数。 + +- 对于`group by`分组排序,会先查询所有数据后再统一排序,而不是一开始就排序。 + + + +#### 执行阶段 + +在执行的过程中,执行器就会和存储引擎交互,交互是以记录为单位的。 + + + +## update 语句的执行过程 + +查询语句的那一套流程,更新语句也是同样会走一遍: + +1. 客户端先通过连接器建立连接,连接器自会判断用户身份、权限校验; +2. 因为这是一条 update 语句,所以不需要经过查询缓存,但是表上有更新语句,是会把整个表的查询缓存清空的,所以说查询缓存很鸡肋,在 MySQL 8.0 就被移除这个功能了; +3. 解析器会通过词法分析识别出关键字 update,表名等等,构建出语法树,接着还会做语法分析,判断输入的语句是否符合 MySQL 语法; +4. 预处理器会判断表和字段是否存在; +5. 优化器确定执行计划; +6. 执行器负责具体执行。 + +与查询流程不一样的是,更新流程还涉及两个重要日志模块:redo log(重做日志)和 bin log(归档日志)。 + +update执行流程:` update T set c=c+1 where ID=2;` + +7. 执行器先找引擎取ID=2这一行。如果ID=2这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。 + +8. 执行器拿到引擎给的行数据,把这个值加上1,比如原来是N,现在就是N+1,得到新的一行数据,再调用引擎接口写入这行新数据。 + +9. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 undo-log 和 redo log(prepare状态)里面。然后告知执行器执行完成了,随时可以提交事务。 + +10. 执行器生成这个操作的binlog,并把binlog写入磁盘。 + +11. 执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交(commit)状态,更新完成。 + +![](MySQL\update.png) + +图中浅色框表示是在InnoDB内部执行的,深色框表示是在执行器中执行的。 + +将 redo log 的写入拆成了两个步骤: prepare 和 commit,这就是"两阶段提交"。为了使两个日志之间保持一致: + +1. 当在写bin log之前崩溃时:此时 binlog 还没写,redo log 也还没提交,事务会回滚。 日志保持一致 + +2. 当在写bin log之后崩溃时: 重启恢复后虽没有commit,但满足prepare和binlog完整,自动commit。日志保持一致 + +溃恢复时的判断规则: + +1. 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,则直接提交; +2. 如果 redo log 里面的事务只有完整的 prepare,则判断对应的事务 binlog 是否存在并完整: + 1. 如果是,则提交事务; + 2. 否则,回滚事务。 + + + +## MySQL一行记录的存储结构 + +先来看看 MySQL 数据库的文件存放在哪个目录? + +```sh +mysql> SHOW VARIABLES LIKE 'datadir'; ++---------------+-----------------+ +| Variable_name | Value | ++---------------+-----------------+ +| datadir | /var/lib/mysql/ | ++---------------+-----------------+ +1 row in set (0.00 sec) +``` + +我们每创建一个 database(数据库) 都会在 /var/lib/mysql/ 目录里面创建一个以 database 为名的目录,然后保存表结构和表数据的文件都会存放在这个目录里。 + +- db.opt,用来存储当前数据库的默认字符集和字符校验规则。 +- 表名.frm ,数据库表的**表结构**会保存在这个文件。在 MySQL 中建立一张表都会生成一个.frm 文件,该文件是用来保存每个表的元数据信息的,主要包含表结构定义。 +- 表名.ibd,数据库表的**表数据**会保存在这个文件。 MySQL 中每一张表的数据都存放在一个独立的 .ibd 文件。 + + + +### 表空间 + +**表空间由段(segment)、区(extent)、页(page)、行(row)组成**,InnoDB存储引擎的逻辑存储结构大致如下图: + +![](.\MySql\表空间结构.drawio.webp) + +> 记录是按照行来存储的 +> +> InnoDB 的数据是按「页」为单位来读写的,默认每个页的大小为 16KB。一次最少从磁盘中读取 16K 的内容到内存中,一次最少把内存中的 16K 内容刷新到磁盘中。 + + + +**区(extent)** + +我们知道 InnoDB 存储引擎是用 B+ 树来组织数据的。 + +B+ 树中每一层都是通过双向链表连接起来的,如果是以页为单位来分配存储空间,那么链表中相邻的两个页之间的物理位置并不是连续的,可能离得非常远,那么磁盘查询时就会有大量的随机I/O,随机 I/O 是非常慢的。 + +解决这个问题也很简单,就是让链表中相邻的页的物理位置也相邻,这样就可以使用顺序 I/O 了,那么在范围查询(扫描叶子节点)的时候性能就会很高。 + +那具体怎么解决呢? + +**在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照区(extent)为单位分配。每个区的大小为 1MB,对于 16KB 的页来说,连续的 64 个页会被划为一个区,这样就使得链表中相邻的页的物理位置也相邻,就能使用顺序 I/O 了**. + + + +表空间是由各个段(segment)组成的,段是由多个区(extent)组成的。段一般分为数据段、索引段和回滚段等。 + + + +### InnoDB 行格式 + +InnoDB 提供了 4 种行格式,分别是 Redundant、Compact、Dynamic和 Compressed 行格式。 + +Compact 行格式: + +![](.\MySql\COMPACT.drawio.webp) + +#### 记录的额外信息 + +##### **变长字段长度列表** + +varchar(n) 和 char(n) 的区别是char 是定长的,varchar 是变长的,变长字段实际存储的数据的长度(大小)是不固定的。 + +所以,在存储数据的时候,也要把数据占用的大小存起来,存到「变长字段长度列表」里面,读取数据的时候才能根据这个「变长字段长度列表」去读取对应长度的数据。其他 TEXT、BLOB 等变长字段也是这么实现的。 + +![](.\MySql\t_test.webp) + +第一条记录: + +- name 列的值为 a,真实数据占用的字节数是 1 字节,十六进制 0x01; +- phone 列的值为 123,真实数据占用的字节数是 3 字节,十六进制 0x03; +- age 列和 id 列不是变长字段,所以这里不用管。 + +这些变长字段的真实数据占用的字节数会按照列的顺序**逆序存放**,所以「变长字段长度列表」里的内容是「 03 01」,而不是 「01 03」。 + +![](.\MySql\变长字段长度列表1.webp) + + + +> **为什么「变长字段长度列表」的信息要按照逆序存放?** + +这个设计是有想法的,主要是因为「记录头信息」中指向下一个记录的指针,指向的是下一条记录的「记录头信息」和「真实数据」之间的位置,这样的好处是向左读就是记录头信息,向右读就是真实数据,比较方便。 + +「变长字段长度列表」中的信息之所以要逆序存放,是因为这样可以**使得位置靠前的记录的真实数据和数据对应的字段长度信息可以同时在一个 CPU Cache Line 中,这样就可以提高 CPU Cache 的命中率**。 + + + +> **每个数据库表的行格式都有「变长字段字节数列表」吗?** + +**当数据表没有变长字段的时候,比如全部都是 int 类型的字段,这时候表里的行格式就不会有「变长字段长度列表」了**,因为没必要,不如去掉以节省空间。 + + + +##### NULL值列表 + +表中的某些列可能会存储 NULL 值,如果把这些 NULL 值都放到记录的真实数据中会比较浪费空间,所以 Compact 行格式把这些值为 NULL 的列存储到 NULL值列表中。 + +如果存在允许 NULL 值的列,则每个列对应一个二进制位(bit),二进制位按照列的顺序**逆序排列**。 + +- 二进制位的值为`1`时,代表该列的值为NULL。 +- 二进制位的值为`0`时,代表该列的值不为NULL。 + +另外,NULL 值列表必须用**整数个字节**的位表示(1字节8位),如果使用的二进制位个数不足整数个字节,则在字节的高位补 `0`。 + +第三条记录 phone 列 和 age 列是 NULL 值,所以,对于第三条数据,NULL 值列表用十六进制表示是 0x06。 + +![](.\MySql\null值列表4.webp) + +> **每个数据库表的行格式都有「NULL 值列表」吗?** + +**当数据表的字段都定义成 NOT NULL 的时候,这时候表里的行格式就不会有 NULL 值列表了**。 + +所以在设计数据库表的时候,通常都是建议将字段设置为 NOT NULL,这样可以至少节省 1 字节的空间 + + + +##### 记录头信息 + +记录头信息中包含的内容很多: + +- delete_mask :标识此条数据是否被删除。从这里可以知道,我们执行 detele 删除记录的时候,并不会真正的删除记录,只是将这个记录的 delete_mask 标记为 1。 +- next_record:下一条记录的位置。从这里可以知道,记录与记录之间是通过链表组织的。在前面我也提到了,指向的是下一条记录的「记录头信息」和「真实数据」之间的位置,这样的好处是向左读就是记录头信息,向右读就是真实数据,比较方便。 +- record_type:表示当前记录的类型,0表示普通记录,1表示B+树非叶子节点记录,2表示最小记录,3表示最大记录 + + + +#### 记录的真实数据 + +记录真实数据部分除了我们定义的字段,还有三个隐藏字段,分别为:row_id、trx_id、roll_pointer。 + +* row_id:如果我们建表的时候指定了主键或者唯一约束列,那么就没有 row_id 隐藏字段了。如果既没有指定主键,又没有唯一约束,那么 InnoDB 就会为记录添加 row_id 隐藏字段。row_id不是必需的,占用 6 个字节。 + +- trx_id:事务id,表示这个数据是由哪个事务生成的。 trx_id是必需的,占用 6 个字节。 + +- roll_pointer:这条记录上一个版本的指针。roll_pointer 是必需的,占用 7 个字节。 + + + +### varchar(n) 中 n 最大取值为多少? + +**MySQL 规定除了 TEXT、BLOBs 这种大对象类型之外,其他所有的列(不包括隐藏列和记录头信息)占用的字节长度加起来不能超过 65535 个字节**。 + +要算 varchar(n) 最大能允许存储的字节数,还要看数据库表的字符集,因为字符集代表着,1个字符要占用多少字节,比如 ascii 字符集, 1 个字符占用 1 字节。 + +存储字段类型为 varchar(n) 的数据时,其实分成了三个部分来存储: + +- 真实数据 +- 真实数据占用的字节数 +- NULL 标识,如果不允许为NULL,这部分不需要 + +所以,我们在算 varchar(n) 中 n 最大值时,需要减去 「变长字段长度列表」和 「NULL 值列表」所占用的字节数的。 + + + +```sql +CREATE TABLE test ( +`name` VARCHAR(65532) NULL +) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT; +``` + +上述例子,在数据库表只有一个 varchar(n) 字段且字符集是 ascii 的情况下,varchar(n) 中 n 最大值 = 65535 - 2 - 1 = 65532。 + + + +### 行溢出后,MySQL 是怎么处理的? + +MySQL 中磁盘和内存交互的基本单位是页,一个页的大小一般是 `16KB`,也就是 `16384字节`,而一个 varchar(n) 类型的列最多可以存储 `65532字节`,一些大对象如 TEXT、BLOB 可能存储更多的数据,这时一个页可能就存不了一条记录。这个时候就会**发生行溢出,多的数据就会存到另外的「溢出页」中**。 + +当发生行溢出时,在记录的真实数据处只会保存该列的一部分数据,而把剩余的数据放在「溢出页」中,然后真实数据处用 20 字节存储指向溢出页的地址,从而可以找到剩余数据所在的页. + + + +## 索引 + +索引分类: + +按「数据结构」分类:**B+tree索引、Hash索引、Full-text索引**。 + +按「物理存储」分类:**聚簇索引(主键索引)、二级索引(辅助索引)**。 + +按「字段特性」分类:**主键索引、唯一索引、普通索引、前缀索引**。 + +按「字段个数」分类:**单列索引、联合索引**。 + +### B+Tree索引 + +***1、B+Tree vs B Tree*** + +B+Tree 只在叶子节点存储数据,而 B 树 的非叶子节点也要存储数据,所以 B+Tree 的单个节点的数据量更小,在相同的**磁盘 I/O 次数**下,就能查询更多的节点。 + +另外,B+Tree 叶子节点采用的是双链表连接,适合 MySQL 中常见的**范围查询**,而 B 树无法做到这一点。 + +B+Tree**插入和删除效率更高**,不会涉及复杂的树的变形 + +***2、B+Tree vs 二叉树*** + +对于有 N 个叶子节点的 B+Tree,其搜索复杂度为`O(logdN)`,其中 d 表示节点允许的最大子节点个数为 d 个。 + +在实际的应用当中, d 值是大于100的,这样就保证了,即使数据达到千万级别时,B+Tree 的高度依然维持在 3~4 层左右,也就是说一次数据查询操作只需要做 3~4 次的磁盘 I/O 操作就能查询到目标数据。 + +而二叉树的每个父节点的儿子节点个数只能是 2 个,意味着其搜索复杂度为 `O(logN)`,这已经比 B+Tree 高出不少,因此二叉树检索到目标数据所经历的磁盘 I/O 次数要更多。如果索引的字段值是按顺序增长的,二叉树会转变为链表结构,检索的过程和全表扫描无异。 + +**3、B+Tree vs 红黑树** + +虽然对比二叉树来说,树高有所降低,但数据量一大时,依旧会有很大的高度。每个节点中只存储一个数据,节点之间还是不连续的,依旧无法利用局部性原理。 + +***4、B+Tree vs Hash*** + +Hash 在做等值查询的时候效率贼快,搜索复杂度为 O(1)。 + +但是 Hash 表不适合做范围查询,它更适合做等值的查询,这也是 B+Tree 索引要比 Hash 表索引有着更广泛的适用场景的原因。 + +### 聚集索引和二级索引 + +![](MySQL\聚集索引和二级索引.webp) + +所以,在查询时使用了二级索引,如果查询的数据能在二级索引里查询的到,那么就不需要回表,这个过程就是覆盖索引。如果查询的数据不在二级索引里,就会先检索二级索引,找到对应的叶子节点,获取到主键值后,然后再检索主键索引,就能查询到数据了,这个过程就是回表。 + +### 主键索引和唯一索引 + +一张表最多只有一个主键索引,索引列的值不允许有空值。 + +唯一索引建立在 UNIQUE 字段上的索引,一张表可以有多个唯一索引,索引列的值必须唯一,但是允许有空值。 + +### 联合索引 + +使用联合索引时,存在**最左匹配原则**,也就是按照最左优先的方式进行索引的匹配。在使用联合索引进行查询的时候,如果不遵循「最左匹配原则」,联合索引会失效。 + +联合索引有一些特殊情况,并不是查询过程使用了联合索引查询,就代表联合索引中的所有字段都用到了联合索引进行索引查询。联合索引的最左匹配原则会一直向右匹配直到遇到「范围查询」就会停止匹配。**也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段无法用到联合索引**。 + + + +> **`select * from t_table where a > 1 and b = 2`,联合索引(a, b)哪一个字段用到了联合索引的 B+Tree?** + +由于联合索引(二级索引)是先按照 a 字段的值排序的,所以符合 a > 1 条件的二级索引记录肯定是相邻,于是在进行索引扫描的时候,可以定位到符合 a > 1 条件的第一条记录,然后沿着记录所在的链表向后扫描,直到某条记录不符合 a > 1 条件位置。所以 a 字段可以在联合索引的 B+Tree 中进行索引查询。 + +**但是在符合 a > 1 条件的二级索引记录的范围里,b 字段的值是无序的**。所以 b 字段无法利用联合索引进行索引查询。 + +**这条查询语句只有 a 字段用到了联合索引进行索引查询,而 b 字段并没有使用到联合索引**。 + + + +> **`select * from t_table where a >= 1 and b = 2`,联合索引(a, b)哪一个字段用到了联合索引的 B+Tree?** + +由于联合索引(二级索引)是先按照 a 字段的值排序的,所以符合 >= 1 条件的二级索引记录肯定是相邻,于是在进行索引扫描的时候,可以定位到符合 >= 1 条件的第一条记录,然后沿着记录所在的链表向后扫描,直到某条记录不符合 a>= 1 条件位置。所以 a 字段可以在联合索引的 B+Tree 中进行索引查询。 + +虽然在符合 a>= 1 条件的二级索引记录的范围里,b 字段的值是「无序」的,**但是对于符合 a = 1 的二级索引记录的范围里,b 字段的值是「有序」的**. + +于是,在确定需要扫描的二级索引的范围时,当二级索引记录的 a 字段值为 1 时,可以通过 b = 2 条件减少需要扫描的二级索引记录范围(b 字段可以利用联合索引进行索引查询的意思)。也就是说,从符合 a = 1 and b = 2 条件的第一条记录开始扫描,而不需要从第一个 a 字段值为 1 的记录开始扫描。 + +所以,**Q2 这条查询语句 a 和 b 字段都用到了联合索引进行索引查询**。 + + + +> **`SELECT * FROM t_table WHERE a BETWEEN 2 AND 8 AND b = 2`,联合索引(a, b)哪一个字段用到了联合索引的 B+Tree?** + +在 MySQL 中,BETWEEN 包含了 value1 和 value2 边界值,类似于 >= and =<,所以**这条查询语句 a 和 b 字段都用到了联合索引进行索引查询**。 + + + +> **`SELECT * FROM t_user WHERE name like 'j%' and age = 22`,联合索引(name, age)哪一个字段用到了联合索引的 B+Tree?** + +由于联合索引(二级索引)是先按照 name 字段的值排序的,所以前缀为 ‘j’ 的 name 字段的二级索引记录都是相邻的, 于是在进行索引扫描的时候,可以定位到符合前缀为 ‘j’ 的 name 字段的第一条记录,然后沿着记录所在的链表向后扫描,直到某条记录的 name 前缀不为 ‘j’ 为止。 + +虽然在符合前缀为 ‘j’ 的 name 字段的二级索引记录的范围里,age 字段的值是「无序」的,**但是对于符合 name = j 的二级索引记录的范围里,age字段的值是「有序」的** + +所以,**这条查询语句 a 和 b 字段都用到了联合索引进行索引查询**。 + + + +**联合索引的最左匹配原则,在遇到范围查询(如 >、<)的时候,就会停止匹配,也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段无法用到联合索引。注意,对于 >=、<=、BETWEEN、like 前缀匹配的范围查询,并不会停止匹配**。 + + + +**索引下推:** + +对于联合索引(a, b),在执行 `select * from table where a > 1 and b = 2` 语句的时候,只有 a 字段能用到索引,那在联合索引的 B+Tree 找到第一个满足条件的主键值后,还需要判断其他条件是否满足(看 b 是否等于 2),那是在联合索引里判断?还是回主键索引去判断呢? + +- 在 MySQL 5.6 之前,只能从 ID2 (主键值)开始一个个回表,到「主键索引」上找出数据行,再对比 b 字段值。 +- 而 MySQL 5.6 引入的**索引下推优化**(index condition pushdown), **可以在联合索引遍历过程中,对联合索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数**。 + + + +**联合索引进行排序**: + +这里出一个题目,针对针对下面这条 SQL,你怎么通过索引来提高查询效率呢? + +```sql +select * from order where status = 1 order by create_time asc +``` + +有的同学会认为,单独给 status 建立一个索引就可以了。 + +但是更好的方式给 status 和 create_time 列建立一个联合索引,因为这样可以避免 MySQL 数据库发生文件排序。 + +因为在查询时,如果只用到 status 的索引,但是这条语句还要对 create_time 排序,这时就要用文件排序 filesort,也就是在 SQL 执行计划中,Extra 列会出现 Using filesort。 + +所以,要利用索引的有序性,在 status 和 create_time 列建立联合索引,这样根据 status 筛选后的数据就是按照 create_time 排好序的,避免在文件排序,提高了查询效率。 + + + + + +### 索引设计原则 + +什么时候适合索引? + +1. 针对数据量较大,且查询比较繁琐的表建立索引; + +2. 针对于常作为查询条件(where),排序(order by),分组(group by)操作的字段,建立索引; + +3. 尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高使用索引的效率越高; + +4. 如果是字符串类型的字段,字段的长度过长,可以针对字段的特点,建立前缀索引; + +5. 尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率; + +6. 要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率; + +7. 如果索引列不能存储null值,在创建表时使用not null约束它。当优化器知道每列是否包含null值时,它可以更好地确定哪个索引最有效地用于查询。 + +8. 表的主外键或连表字段,必须建立索引,因为能很大程度提升连表查询的性能。 + + + +什么时候不适合索引? + +1. 大量重复值的字段 +2. 当表的数据较少,不应当建立索引,因为数据量不大时,维护索引反而开销更大。 +3. 经常更新的字段,因为索引字段频繁修改,由于要维护 B+Tree的有序性,那么就需要频繁的重建索引,这个过程是会影响数据库性能。 +4. 索引不能参与计算,因此经常带函数查询的字段,并不适合建立索引。 +5. 一张表中的索引数量并不是越多越好,一般控制在`3`,最多不能超过`5`。 +6. 索引的字段值无序时,不推荐建立索引,因为会造成页分裂,尤其是主键索引。 + + + +### 索引优化方法 + +#### 前缀索引优化 + +当字段类型为字符串(varchar, text, longtext等)时,有时候需要索引很长的字符串,导致索引较大,查询是浪费大量的磁盘IO,影响查询效率。此时可以只对字符串的一部分前缀建立索引,节约索引空间,提高索引效率。 + +不过,前缀索引有一定的局限性,例如: + +- order by 就无法使用前缀索引; +- 无法把前缀索引用作覆盖索引; + + + +#### 覆盖索引优化 + +SQL 中 query 的所有字段,在索引 B+Tree 的叶子节点上都能得到,从而不需要通过聚簇索引查询获得,可以避免回表的操作。 + +使用覆盖索引的好处就是,不需要查询出包含整行记录的所有信息,也就减少了大量的 I/O 操作。 + + + +#### 主键索引自增 + +**如果我们使用自增主键**,那么每次插入的新数据就会按顺序添加到当前索引节点的位置,不需要移动已有的数据,当页面写满,就会自动开辟一个新页面。因为每次**插入一条新记录,都是追加操作,不需要重新移动数据**,因此这种插入数据的方法效率非常高。 + +**如果我们使用非自增主键**,由于每次插入主键的索引值都是随机的,因此每次插入新的数据时,就可能会插入到现有数据页中间的某个位置,这将不得不移动其它数据来满足新数据的插入,甚至需要从一个页面复制数据到另外一个页面,我们通常将这种情况称为**页分裂**。**页分裂还有可能会造成大量的内存碎片,导致索引结构不紧凑,从而影响查询效率**。 + + + +主键字段的长度不要太大,因为**主键字段长度越小,意味着二级索引的叶子节点越小(二级索引的叶子节点存放的数据是主键值),这样二级索引占用的空间也就越小**。 + + + +#### 索引最好设置为 NOT NULL + +为了更好的利用索引,索引列要设置为 NOT NULL 约束。有两个原因: + +- 第一原因:索引列存在 NULL 就会导致优化器在做索引选择的时候更加复杂,更加难以优化,因为可为 NULL 的列会使索引、索引统计和值比较都更复杂,比如进行索引统计时,count 会省略值为NULL 的行。 +- 第二个原因:NULL 值是一个没意义的值,但是它会占用物理空间,所以会带来的存储空间的问题,因为 InnoDB 存储记录的时候,如果表中存在允许为 NULL 的字段,那么行格式中**至少会用 1 字节空间存储 NULL 值列表** + + + +### 索引失效 + +1. 左或左右模糊查询 `like %x 或者 like %x%`。 因为索引 B+ 树是按照「索引值」有序排列存储的,只能根据前缀进行比较。 +2. 查询中对索引做了计算、函数、类型转换操作。因为索引保存的是索引字段的原始值,而不是经过函数计算后的值,自然就没办法走索引了。 +3. 联合索引要遵循最左匹配原则 +4. 联合索引中,出现范围查询(>,<),范围查询右侧的列索引失效。 +5. 在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。因为 OR 的含义就是两个只要满足一个即可,只要有条件列不是索引列,就会进行全表扫描。 +6. 隐式类型转换 + + + +**索引隐式类型转换**: + +如果索引字段是字符串类型,但是在条件查询中,输入的参数是整型的话,你会在执行计划的结果发现这条语句会走全表扫描; + +但是如果索引字段是整型类型,查询条件中的输入参数即使字符串,是不会导致索引失效,还是可以走索引扫描。 + +MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。 + + + +### SQL提示 + +我们在查询时,可以使用mysql的sql提示,加入一些人为的提示来达到优化操作的目的: + +- user index:建议mysql使用哪一个索引完成此次查询(仅仅是建议,mysql内部还会再次进行评估); +- ignore index:忽略指定的索引; +- force index:强制使用索引。 + +```sql +explain select * from tb_user use index(idx_user_pro) where profession=’软件工程’; +explain select * from tb_user use index(idx_user_pro) where profession=’软件工程’; +explain select * from tb_user force index(idx_user_pro) where profession=’软件工程’; +``` + + + +## 事务 + +> **事务的特性** + +* **原子性**:一个事务中的所有操作,要么全部完成,要么全部失败。 + +* **一致性**:是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。 + +* **隔离性**:多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。 + +* **持久性**:一个事务一旦被提交,它会保持永久性,所更改的数据都会被写入到磁盘做持久化处理。 + + + +> **InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?** + +- 持久性是通过 redo log (重做日志)来保证的,宕机后能数据恢复; +- 原子性是通过 undo log(回滚日志) 来保证的,事务能够进行回滚; +- 隔离性是通过 MVCC(多版本并发控制) 或锁机制来保证的; +- 一致性则是通过持久性+原子性+隔离性来保证; + + + +> **并行事务会引发的问题?** + +* **脏读**:读到其他事务未提交的数据; + +* **不可重复读**:一个事务内,前后读取的数据不一致; + +* **幻读**:一个事务中,前后读取的记录数量不一致。事务中同一个查询在不同的时间产生不同的结果集。 + +严重性:脏读 > 不可重读读 > 幻读 + + + +> **隔离级别** + +- **读未提交(read uncommitted)**,指一个事务还没提交时,它做的变更就能被其他事务看到; +- **读提交(read committed)**,指一个事务提交之后,它做的变更才能被其他事务看到; +- **可重复读(repeatable read)**,指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,**MySQL InnoDB 引擎的默认隔离级别**; +- **串行化(serializable)**;会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行; + + + + + +> **这四种隔离级别具体是如何实现的呢?** + +- 对于「读未提交」隔离级别的事务来说,因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了; +- 对于「串行化」隔离级别的事务来说,通过加读写锁的方式来避免并行访问; +- 对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 **Read View 来实现的,它们的区别在于创建 Read View 的时机不同,大家可以把 Read View 理解成一个数据快照,就像相机拍照那样,定格某一时刻的风景。「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View,而「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View**。 + + + +> **MVCC** + +* Read View 中的字段:活跃事务(启动但还未提交的事务)id列表、活跃事务中的最小事务id、下一个事务id、创建RV的事务id +* 聚簇索引记录中两个跟事务有关的隐藏列:事务id、旧版本指针 + +![](.\MySql\mvcc.webp) + +一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况: + +- 如果记录的 trx_id 值小于 Read View 中的 `min_trx_id` 值,表示这个版本的记录是在创建 Read View **前**已经提交的事务生成的,所以该版本的记录对当前事务**可见**。 +- 如果记录的 trx_id 值大于等于 Read View 中的 `max_trx_id` 值,表示这个版本的记录是在创建 Read View **后**才启动的事务生成的,所以该版本的记录对当前事务**不可见**。 +- 如果在二者之间,需要判断 trx_id 是否在 m_ids 列表中: + - 如果记录的 trx_id **在** `m_ids` 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务**不可见**。 + - 如果记录的 trx_id **不在** `m_ids`列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务**可见**。 + + + +> **可重复读如何工作的?** + +**可重复读隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View**。 + +如果记录中的 trx_id 小于 Read View中的最小事务id,或者在最小事务id和下一个事务id之间并且不在活跃事务id中,则直接读取记录值; + +如果记录中的 trx_id 在最小事务id和下一个事务id之间并且在活跃事务id中,则**沿着 undo log 旧版本指针往下找旧版本的记录,直到找到 trx_id 「小于」当前事务 的 Read View 中的 min_trx_id 值的第一条记录**。 + + + +> **读已提交如何工作的?** + +**读提交隔离级别是在每次读取数据时,都会生成一个新的 Read View**。 + + + +> **MySQL 可重复读和幻读** + +MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象(并不是完全解决了),解决的方案有两种: + +- 针对**快照读**(普通 select 语句),是**通过 MVCC 方式解决了幻读**,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。 +- 针对**当前读**(select ... for update 等语句,会读取最新的数据),是**通过 next-key lock(记录锁+间隙锁)方式解决了幻读**,因为当执行 select ... for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内执行增、删、改时,就会阻塞,所以就很好了避免幻读问题。 + + + +> **MySQL Innodb 中的 MVCC 并不能完全避免幻读现象** + +第一个发生幻读现象的场景: + +在可重复读隔离级别下,事务 A 第一次执行普通的 select 语句时生成了一个 ReadView,之后事务 B 向表中新插入了一条 id = 5 的记录并提交。接着,事务 A 对 id = 5 这条记录进行了更新操作,在这个时刻,这条新记录的 trx_id 隐藏列的值就变成了事务 A 的事务 id,之后事务 A 再使用普通 select 语句去查询这条记录时就可以看到这条记录了,于是就发生了幻读。 + +![](.\MySql\幻读发生.drawio.webp) + +第二个发生幻读现象的场景: + +T1 时刻:事务 A 先执行「快照读语句」:select * from t_test where id > 100 得到了 3 条记录。 + +T2 时刻:事务 B 往插入一个 id= 200 的记录并提交; + +T3 时刻:事务 A 再执行「当前读语句」 select * from t_test where id > 100 for update 就会得到 4 条记录,此时也发生了幻读现象。 + +**要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select ... for update 这类当前读的语句**,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。 + + + +## 锁 + +### 全局锁 + +使用全局锁 `flush tables with read lock` 后数据库处于只读状态,`unlock tables` 释放全局锁,会话断开全局锁自动释放。 + +应用场景:全库逻辑备份 + +如果数据库的引擎支持的事务支持**可重复读的隔离级别**,那么在备份数据库之前先开启事务,会先创建 Read View,然后整个事务执行期间都在用这个 Read View,而且由于 MVCC 的支持,备份期间业务依然可以对数据进行更新操作。 + +备份数据库的工具是 mysqldump,在使用 mysqldump 时加上 `–single-transaction` 参数的时候,就会在备份数据库之前先开启事务。这种方法只适用于支持「可重复读隔离级别的事务」的存储引擎。 + + + +### 表级锁 + +MySql表级锁:表锁、元数据锁、意向锁、AUTO-INC锁 + +**表锁** + +```sh +//表级别的共享锁,也就是读锁; +lock tables t_student read; + +//表级别的独占锁,也就是写锁; +lock tables t_stuent write; + +// 释放会话所有表锁,会话退出后,也会释放所有表锁 +unlock tables; +``` + +表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。表锁的颗粒度太大,尽量避免使用。 + + + +**元数据锁(MDL)** + +我们不需要显示的使用 MDL,因为当我们对数据库表进行操作时,会自动给这个表加上 MDL。MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。 + +- 对一张表进行 CRUD 操作时,加的是 MDL 读锁; +- 对一张表做结构变更操作的时候,加的是 MDL 写锁; + +MDL 是在事务提交后才会释放,这意味着**事务执行期间,MDL 是一直持有的**。 + +申请 MDL 锁的操作会形成一个队列,队列中**写锁获取优先级高于读锁**,一旦出现 MDL 写锁等待,会阻塞后续该表的所有 CRUD 操作。 + + + +**意向锁** + +如果没有「意向锁」,那么加「独占表锁」时,就需要遍历表里所有记录,查看是否有记录存在独占锁,这样效率会很慢。 + +那么有了「意向锁」,由于在对记录加独占锁前,先会加上表级别的意向独占锁,那么在加「独占表锁」时,直接查该表是否有意向独占锁,如果有就意味着表里已经有记录被加了独占锁,这样就不用去遍历表里的记录。 + +所以,**意向锁的目的是为了快速判断表里是否有记录被加锁**。 + +- 在使用 InnoDB 引擎的表里对某些记录加上「共享锁」之前,需要先在表级别加上一个「意向共享锁」; +- 在使用 InnoDB 引擎的表里对某些纪录加上「独占锁」之前,需要先在表级别加上一个「意向独占锁」; + +**意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁和独占表锁发生冲突。** + + + +**AUTO-INC 锁** + +声明 `AUTO_INCREMENT` 属性的字段数据库自动赋递增的值,主要是通过 AUTO-INC 锁实现的。 + +AUTO-INC 锁是特殊的表锁机制,锁**不再是一个事务提交后才释放,而是再执行完插入语句后就会立即释放**。 + +在插入数据时,会加一个表级别的 AUTO-INC 锁,然后为被 `AUTO_INCREMENT` 修饰的字段赋值递增的值,等插入语句执行完成后,才会把 AUTO-INC 锁释放掉。 + +在 MySQL 5.1.22 版本开始,InnoDB 存储引擎提供了一种**轻量级的锁**来实现自增。一样也是在插入数据的时候,会为被 `AUTO_INCREMENT` 修饰的字段加上轻量级锁,**然后给该字段赋值一个自增的值,就把这个轻量级锁释放了,而不需要等待整个插入语句执行完后才释放锁**。 + + + +### 行级锁 + +共享锁(S锁)满足读读共享,读写互斥。独占锁(X锁)满足写写互斥、读写互斥。 + +在读已提交隔离级别下,行级锁的种类只有记录锁,也就是仅仅把一条记录锁上。 + +在可重复读隔离级别下,行级锁的种类除了有记录锁,还有间隙锁(目的是为了避免幻读) + +**Record Lock** + +记录锁,也就是仅仅把一条记录锁上; + + + +**Gap Lock** + +间隙锁,锁定一个范围,但是不包含记录本身; + +间隙锁,只存在于可重复读隔离级别,**目的是为了解决可重复读隔离级别下幻读的现象**。 + +**间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的**。 + + + +**Next-Key Lock** + +临键锁,Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。 + +**如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的。** + + + +**插入意向锁** + +一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁(next-key lock 也包含间隙锁)。 + +如果有的话,插入操作就会发生**阻塞**,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻),在此期间会生成一个**插入意向锁**,表明有事务想在某个区间插入新记录,但是现在处于等待状态。 + + + +### 怎么加行级锁 + +> **什么SQL语句会加行级锁?** + +1. 普通的 select 语句是不会对记录加锁的(除了串行化隔离级别),因为它属于快照读,是通过 MVCC(多版本并发控制)实现的。如果要在查询时对记录加行级锁,可以使用下面这两个方式,这两种查询会加锁的语句称为**锁定读**。 + + ```sql + //对读取的记录加共享锁(S型锁) + select ... lock in share mode; + + //对读取的记录加独占锁(X型锁) + select ... for update; + ``` + + 这两条语句必须在一个事务中,因为当事务提交了,锁就会被释放,所以在使用这两条语句的时候,要加上 begin 或者 start transaction 开启事务的语句。 + +2. update 和 delete 操作都会加行级锁,且锁的类型都是独占锁(X型锁)。 + + + +> **行级锁有哪些?** + +在读已提交隔离级别下,行级锁的种类只有记录锁,也就是仅仅把一条记录锁上。 + +在可重复读隔离级别下,行级锁的种类除了有记录锁,还有间隙锁(目的是为了避免幻读),所以行级锁的种类主要有三类: + +- Record Lock,记录锁,也就是仅仅把一条记录锁上; +- Gap Lock,间隙锁,锁定一个范围,但是不包含记录本身; +- Next-Key Lock:Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。 + + + +> **有什么命令可以分析加了什么锁?** + +`select * from performance_schema.data_locks\G;` + +LOCK_TYPE 中的 RECORD 表示行级锁,而不是记录锁的意思。 + +LOCK_MODE 可以确认是 next-key 锁,还是间隙锁,还是记录锁: + +- 如果 LOCK_MODE 为 `X`,说明是 next-key 锁; +- 如果 LOCK_MODE 为 `X, REC_NOT_GAP`,说明是记录锁; +- 如果 LOCK_MODE 为 `X, GAP`,说明是间隙锁; + +![](.\MySql\事务a加锁分析.webp) + + + +**加锁的对象是索引,加锁的基本单位是 next-key lock**,它是由记录锁和间隙锁组合而成的,**next-key lock 是前开后闭区间,而间隙锁是前开后开区间**。 + +但是,next-key lock 在一些场景下会退化成记录锁或间隙锁。 + +那到底是什么场景呢?总结一句,**在能使用记录锁或者间隙锁就能避免幻读现象的场景下, next-key lock 就会退化成记录锁或间隙锁**。 + + + +#### 唯一索引等值查询 + +当我们用唯一索引进行等值查询的时候,查询的记录存不存在,加锁的规则也会不同: + +- 当查询的记录是「存在」的,在索引树上定位到这一条记录后,将该记录的索引中的 next-key lock 会**退化成「记录锁」**。 +- 当查询的记录是「不存在」的,在索引树找到第一条大于该查询记录的记录后,将该记录的索引中的 next-key lock 会**退化成「间隙锁」**。 + + + +> **为什么唯一索引等值查询并且查询记录存在的场景下,该记录的索引中的 next-key lock 会退化成记录锁?** + +原因就是在唯一索引等值查询并且查询记录存在的场景下,仅靠记录锁也能避免幻读的问题。 + +幻读的定义就是,当一个事务前后两次查询的结果集,不相同时,就认为发生幻读。所以,要避免幻读就是避免结果集某一条记录被其他事务删除,或者有其他事务插入了一条新记录,这样前后两次查询的结果集就不会出现不相同的情况。 + +- 由于主键具有唯一性,所以**其他事务插入 id = 1 的时候,会因为主键冲突,导致无法插入 id = 1 的新记录**。这样事务 A 在多次查询 id = 1 的记录的时候,不会出现前后两次查询的结果集不同,也就避免了幻读的问题。 +- 由于对 id = 1 加了记录锁,**其他事务无法删除该记录**,这样事务 A 在多次查询 id = 1 的记录的时候,不会出现前后两次查询的结果集不同,也就避免了幻读的问题。 + + + +> **为什么唯一索引等值查询并且查询记录「不存在」的场景下,在索引树找到第一条大于该查询记录的记录后,要将该记录的索引中的 next-key lock 会退化成「间隙锁」?** + +原因就是在唯一索引等值查询并且查询记录不存在的场景下,仅靠间隙锁就能避免幻读的问题。 + +- 为什么 id = 5 记录上的主键索引的锁不可以是 next-key lock?如果是 next-key lock,就意味着其他事务无法删除 id = 5 这条记录,但是这次的案例是查询 id = 2 的记录,只要保证前后两次查询 id = 2 的结果集相同,就能避免幻读的问题了,所以即使 id =5 被删除,也不会有什么影响,那就没必须加 next-key lock,因此只需要在 id = 5 加间隙锁,避免其他事务插入 id = 2 的新记录就行了。 +- 为什么不可以针对不存在的记录加记录锁?锁是加在索引上的,而这个场景下查询的记录是不存在的,自然就没办法锁住这条不存在的记录。 + + + +#### 唯一索引范围查询 + +当唯一索引进行范围查询时,**会对每一个扫描到的索引加 next-key 锁,然后如果遇到下面这些情况,会退化成记录锁或者间隙锁**: + +- 情况一:针对「大于等于」的范围查询,因为存在等值查询的条件,那么如果等值查询的记录是存在于表中,那么该记录的索引中的 next-key 锁会**退化成记录锁**。 +- 情况二:针对「小于或者小于等于」的范围查询,要看条件值的记录是否存在于表中: + - 当条件值的记录不在表中,那么不管是「小于」还是「小于等于」条件的范围查询,**扫描到终止范围查询的记录时,该记录的索引的 next-key 锁会退化成间隙锁**,其他扫描到的记录,都是在这些记录的索引上加 next-key 锁。 + - 当条件值的记录在表中,如果是「小于」条件的范围查询,**扫描到终止范围查询的记录时,该记录的索引的 next-key 锁会退化成间隙锁**,其他扫描到的记录,都是在这些记录的索引上加 next-key 锁;如果「小于等于」条件的范围查询,扫描到终止范围查询的记录时,该记录的索引 next-key 锁不会退化成间隙锁。其他扫描到的记录,都是在这些记录的索引上加 next-key 锁。 + + + +#### 非唯一索引等值查询 + +当我们用非唯一索引进行等值查询的时候,**因为存在两个索引,一个是主键索引,一个是非唯一索引(二级索引),所以在加锁时,同时会对这两个索引都加锁,但是对主键索引加锁的时候,只有满足查询条件的记录才会对它们的主键索引加锁**。 + +针对非唯一索引等值查询时,查询的记录存不存在,加锁的规则也会不同: + +- 当查询的记录「存在」时,由于不是唯一索引,所以肯定存在索引值相同的记录,于是**非唯一索引等值查询的过程是一个扫描的过程,直到扫描到第一个不符合条件的二级索引记录就停止扫描,然后在扫描的过程中,对扫描到的二级索引记录加的是 next-key 锁,而对于第一个不符合条件的二级索引记录,该二级索引的 next-key 锁会退化成间隙锁。同时,在符合查询条件的记录的主键索引上加记录锁**。 +- 当查询的记录「不存在」时,**扫描到第一条不符合条件的二级索引记录,该二级索引的 next-key 锁会退化成间隙锁。因为不存在满足查询条件的记录,所以不会对主键索引加锁**。 + + + +> **针对非唯一索引等值查询时,查询的值不存在的情况。** + +执行 `select * from user where age = 25 for update;` 定位到第一条不符合查询条件的二级索引记录,即扫描到 age = 39,于是**该二级索引的 next-key 锁会退化成间隙锁,范围是 (22, 39)**。 + +![](.\MySql\非唯一索引等值查询age=25.drawio.webp) + +其他事务无法插入 age 值为 23、24、25、26、....、38 这些新记录。不过对于插入 age = 22 和 age = 39 记录的语句,在一些情况是可以成功插入的,而一些情况则无法成功插入。 + +**插入语句在插入一条记录之前,需要先定位到该记录在 B+树 的位置,如果插入的位置的下一条记录的索引上有间隙锁,才会发生阻塞**。 + +插入 age = 22 记录的成功和失败的情况分别如下: + +- 当其他事务插入一条 age = 22,id = 3 的记录的时候,在二级索引树上定位到插入的位置,而该位置的下一条是 id = 10、age = 22 的记录,该记录的二级索引上没有间隙锁,所以这条插入语句可以执行成功。 +- 当其他事务插入一条 age = 22,id = 12 的记录的时候,在二级索引树上定位到插入的位置,而该位置的下一条是 id = 20、age = 39 的记录,正好该记录的二级索引上有间隙锁,所以这条插入语句会被阻塞,无法插入成功。 + +插入 age = 39 记录的成功和失败的情况分别如下: + +- 当其他事务插入一条 age = 39,id = 3 的记录的时候,在二级索引树上定位到插入的位置,而该位置的下一条是 id = 20、age = 39 的记录,正好该记录的二级索引上有间隙锁,所以这条插入语句会被阻塞,无法插入成功。 +- 当其他事务插入一条 age = 39,id = 21 的记录的时候,在二级索引树上定位到插入的位置,而该位置的下一条记录不存在,也就没有间隙锁了,所以这条插入语句可以插入成功。 + +所以,**当有一个事务持有二级索引的间隙锁 (22, 39) 时,插入 age = 22 或者 age = 39 记录的语句是否可以执行成功,关键还要考虑插入记录的主键值,因为「二级索引值(age列)+主键值(id列)」才可以确定插入的位置,确定了插入位置后,就要看插入的位置的下一条记录是否有间隙锁,如果有间隙锁,就会发生阻塞,如果没有间隙锁,则可以插入成功**。 + + + +> **针对非唯一索引等值查询时,查询的值存在的情况。** + +执行 `select * from user where age = 22 for update;` + +![](.\MySql\非唯一索引等值查询存在.drawio.webp) + +在 age = 22 这条记录的二级索引上,加了范围为 (21, 22] 的 next-key 锁,意味着其他事务无法更新或者删除 age = 22 的这一些新记录,不过对于插入 age = 21 和 age = 22 新记录的语句,在一些情况是可以成功插入的,而一些情况则无法成功插入。 + +- 是否可以插入 age = 21 的新记录,还要看插入的新记录的 id 值,**如果插入 age = 21 新记录的 id 值小于 5,那么就可以插入成功**,因为此时插入的位置的下一条记录是 id = 5,age = 21 的记录,该记录的二级索引上没有间隙锁。**如果插入 age = 21 新记录的 id 值大于 5,那么就无法插入成功**,因为此时插入的位置的下一条记录是 id = 10,age = 22 的记录,该记录的二级索引上有间隙锁。 +- 是否可以插入 age = 22 的新记录,还要看插入的新记录的 id 值,从 `LOCK_DATA : 22, 10` 可以得知,其他事务插入 age 值为 22 的新记录时,**如果插入的新记录的 id 值小于 10,那么插入语句会发生阻塞;如果插入的新记录的 id 大于 10,还要看该新记录插入的位置的下一条记录是否有间隙锁,如果没有间隙锁则可以插入成功,如果有间隙锁,则无法插入成功**。 + +在 age = 39 这条记录的二级索引上,加了范围 (22, 39) 的间隙锁。意味着其他事务无法插入 age 值为 23、24、..... 、38 的这一些新记录。不过对于插入 age = 22 和 age = 39 记录的语句,在一些情况是可以成功插入的,而一些情况则无法成功插入。 + +- 是否可以插入 age = 22 的新记录,还要看插入的新记录的 id 值,**如果插入 age = 22 新记录的 id 值小于 10,那么插入语句会被阻塞,无法插入**,因为此时插入的位置的下一条记录是 id = 10,age = 22 的记录,该记录的二级索引上有间隙锁( age = 22 这条记录的二级索引上有 next-key 锁)。**如果插入 age = 22 新记录的 id 值大于 10,也无法插入**,因为此时插入的位置的下一条记录是 id = 20,age = 39 的记录,该记录的二级索引上有间隙锁。 +- 是否可以插入 age = 39 的新记录,还要看插入的新记录的 id 值,从 `LOCK_DATA : 39, 20` 可以得知,其他事务插入 age 值为 39 的新记录时,**如果插入的新记录的 id 值小于 20,那么插入语句会发生阻塞,如果插入的新记录的 id 大于 20,则可以插入成功**。 + + + +#### 非唯一索引范围查询 + +**非唯一索引范围查询,索引的 next-key lock 不会有退化为间隙锁和记录锁的情况**,也就是非唯一索引进行范围查询时,对二级索引记录加锁都是加 next-key 锁。 + +执行 `select * from user where age >= 22 for update;` + +![](.\MySql\非唯一索引范围查询age大于等于22.drawio.webp) + +是否可以插入age = 21、age = 22 和 age = 39 的新记录,还需要看新记录的 id 值。 + +> **在 age >= 22 的范围查询中,明明查询 age = 22 的记录存在并且属于等值查询,为什么不会像唯一索引那样,将 age = 22 记录的二级索引上的 next-key 锁退化为记录锁?** + +因为 age 字段是非唯一索引,不具有唯一性,所以如果只加记录锁(记录锁无法防止插入,只能防止删除或者修改),就会导致其他事务插入一条 age = 22 的记录,这样前后两次查询的结果集就不相同了,出现了幻读现象。 + +#### 没有索引的查询 + +**如果锁定读查询语句,没有使用索引列作为查询条件,或者查询语句没有走索引查询,导致扫描是全表扫描。那么,每一条记录的索引上都会加 next-key 锁,这样就相当于锁住的全表,这时如果其他事务对该表进行增、删、改操作的时候,都会被阻塞**。 + +不只是锁定读查询语句不加索引才会导致这种情况,update 和 delete 语句如果查询条件不加索引,那么由于扫描的方式是全表扫描,于是就会对每一条记录的索引上都会加 next-key 锁,这样就相当于锁住的全表。 + +因此,**在线上在执行 update、delete、select ... for update 等具有加锁性质的语句,一定要检查语句是否走了索引,如果是全表扫描的话,会对每一个索引加 next-key 锁,相当于把整个表锁住了**,这是挺严重的问题。 + + + +### Insert语句怎么加行级锁 + +Insert 语句在正常执行时是不会生成锁结构的,它是靠聚簇索引记录自带的 trx_id 隐藏列来作为**隐式锁**来保护记录的。 + +> 什么是隐式锁? + +当事务需要加锁的时,如果这个锁不可能发生冲突,InnoDB会跳过加锁环节,这种机制称为隐式锁。隐式锁是 InnoDB 实现的一种延迟加锁机制,其特点是只有在可能发生冲突时才加锁,从而减少了锁的数量,提高了系统整体性能。 + +隐式锁就是在 Insert 过程中不加锁,只有在特殊情况下,才会将隐式锁转换为显示锁,这里我们列举两个场景。 + +- 如果记录之间加有间隙锁,为了避免幻读,此时是不能插入记录的; +- 如果 Insert 的记录和已有记录存在唯一键冲突,此时也不能插入记录; + +**记录之间加有间隙锁:** + +每插入一条新记录,都需要看一下待插入记录的下一条记录上是否已经被加了间隙锁,如果已加间隙锁,此时会生成一个插入意向锁,然后锁的状态设置为等待状态(*PS:MySQL 加锁时,是先生成锁结构,然后设置锁的状态,如果锁状态是等待状态,并不是意味着事务成功获取到了锁,只有当锁状态为正常状态时,才代表事务成功获取到了锁*),现象就是 Insert 语句会被阻塞。 + +**遇到唯一键冲突:** + +如果在插入新记录时,插入了一个与「已有的记录的主键或者唯一二级索引列值相同」的记录(不过可以有多条记录的唯一二级索引列的值同时为NULL,这里不考虑这种情况),此时插入就会失败,然后对于这条记录加上了 **S 型的锁**。 + +- 如果主键索引重复,插入新记录的事务会给已存在的主键值重复的聚簇索引记录**添加 S 型记录锁**。 +- 如果唯一二级索引重复,插入新记录的事务都会给已存在的二级索引列值重复的二级索引记录**添加 S 型 next-key 锁** + + + +> **两个事务执行过程中,执行了相同的 insert 语句的场景** + +![](.\MySql\唯一索引加锁.drawio.webp) + +两个事务的加锁过程: + +- 事务 A 先插入 order_no 为 1006 的记录,可以插入成功,此时对应的唯一二级索引记录被「隐式锁」保护,此时还没有实际的锁结构(执行完这里的时候,你可以看查 performance_schema.data_locks 信息,可以看到这条记录是没有加任何锁的); +- 接着,事务 B 也插入 order_no 为 1006 的记录,由于事务 A 已经插入 order_no 值为 1006 的记录,所以事务 B 在插入二级索引记录时会遇到重复的唯一二级索引列值,此时事务 B 想获取一个 S 型 next-key 锁,但是事务 A 并未提交,**事务 A 插入的 order_no 值为 1006 的记录上的「隐式锁」会变「显示锁」且锁类型为 X 型的记录锁,所以事务 B 向获取 S 型 next-key 锁时会遇到锁冲突,事务 B 进入阻塞状态**。 + +并发多个事务的时候,第一个事务插入的记录,并不会加锁,而是会用隐式锁保护唯一二级索引的记录。 + +但是当第一个事务还未提交的时候,有其他事务插入了与第一个事务相同的记录,第二个事务就会**被阻塞**,**因为此时第一事务插入的记录中的隐式锁会变为显示锁且类型是 X 型的记录锁,而第二个事务是想对该记录加上 S 型的 next-key 锁,X 型与 S 型的锁是冲突的**,所以导致第二个事务会等待,直到第一个事务提交后,释放了锁。 + + + +## 日志 + +**undo log(回滚日志)**:是 Innodb 存储引擎层生成的日志,实现了事务中的**原子性**,主要**用于事务回滚和 MVCC**。 + +**redo log(重做日志)**:是 Innodb 存储引擎层生成的日志,实现了事务中的**持久性**,主要**用于掉电等故障恢复**; + +**binlog (归档日志)**:是 Server 层生成的日志,主要**用于数据备份和主从复制**; + + + +> **redo log 和 undo log 区别在哪?** + +这两种日志是属于 InnoDB 存储引擎的日志,它们的区别在于: + +- redo log 记录了此次事务「**完成后**」的数据状态,记录的是更新**之后**的值; +- undo log 记录了此次事务「**开始前**」的数据状态,记录的是更新**之前**的值; + +事务提交之前发生了崩溃,重启后会通过 undo log 回滚事务,事务提交之后发生了崩溃,重启后会通过 redo log 恢复事务 + + + +> **redo log 要写到磁盘,数据也要写磁盘,为什么要多此一举?** + +写入 redo log 的方式使用了追加操作, 所以磁盘操作是**顺序写**,而写入数据需要先找到写入位置,然后才写到磁盘,所以磁盘操作是**随机写**。 + +磁盘的「顺序写 」比「随机写」 高效的多,因此 redo log 写入磁盘的开销更小。 + +可以说这是 WAL 技术的另外一个优点:**MySQL 的写操作从磁盘的「随机写」变成了「顺序写」**,提升语句的执行性能。这是因为 MySQL 的写操作并不是立刻更新到磁盘上,而是先记录在日志上,然后在合适的时间再更新到磁盘上 。 + +至此, 针对为什么需要 redo log 这个问题我们有两个答案: + +- **实现事务的持久性,让 MySQL 有 crash-safe(奔溃恢复) 的能力**,能够保证 MySQL 在任何时间段突然崩溃,重启后之前已提交的记录都不会丢失; +- **将写操作从「随机写」变成了「顺序写」**,提升 MySQL 写入磁盘的性能。 + + + +> **缓存在 redo log buffer 里的 redo log 还是在内存中,它什么时候刷新到磁盘?** + +主要有下面几个时机: + +- MySQL 正常关闭时; +- 当 redo log buffer 中记录的写入量大于 redo log buffer 内存空间的一半时,会触发落盘; +- InnoDB 的后台线程每隔 1 秒,将 redo log buffer 持久化到磁盘。 +- 每次事务提交时都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘 + + + +>**redo log 和 binlog 有什么区别?** + +1. 适用对象不同: + * binlog 是 MySQL 的 Server 层实现的日志,所有存储引擎都可以使用; + * redo log 是 Innodb 存储引擎实现的日志; + +2. 文件格式不同: + + - binlog 有 3 种格式类型,分别是 STATEMENT(默认格式)、ROW、 MIXED,区别如下: + - STATEMENT:每一条修改数据的 SQL 都会被记录到 binlog 中(相当于记录了逻辑操作,所以针对这种格式, binlog 可以称为逻辑日志),主从复制中 slave 端再根据 SQL 语句重现。但 STATEMENT 有动态函数的问题,比如你用了 uuid 或者 now 这些函数,你在主库上执行的结果并不是你在从库执行的结果,这种随时在变的函数会导致复制的数据不一致; + - ROW:记录行数据最终被修改成什么样了(这种格式的日志,就不能称为逻辑日志了),不会出现 STATEMENT 下动态函数的问题。但 ROW 的缺点是每行数据的变化结果都会被记录,比如执行批量 update 语句,更新多少行数据就会产生多少条记录,使 binlog 文件过大,而在 STATEMENT 格式下只会记录一个 update 语句而已; + - MIXED:包含了 STATEMENT 和 ROW 模式,它会根据不同的情况自动使用 ROW 模式和 STATEMENT 模式; + + - redo log 是物理日志,记录的是在某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新; + +3. 写入方式不同: + + - binlog 是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志。 + + - redo log 是循环写,日志空间大小是固定,全部写满就从头开始,保存未被刷入磁盘的脏页日志。 + +4. 用途不同: + + - binlog 用于备份恢复、主从复制; + + - redo log 用于掉电等故障恢复。 + + + +> **binlog 什么时候刷盘?** + +在事务提交的时候,执行器把 binlog cache 里的完整事务写入到 binlog 文件中,但是并没有把数据持久化到磁盘,因为数据还缓存在文件系统的 page cache 里,后续由sync_binlog 参数来控制数据库的 binlog 刷到磁盘上的频率。 + + + +### 两阶段提交 + + + +> **为什么需要两阶段提交?** + +redo log 影响主库的数据,binlog 影响从库的数据,所以 redo log 和 binlog 必须保持一致才能保证主从数据一致。 + +MySQL 为了避免出现两份日志之间的逻辑不一致的问题,使用了「两阶段提交」来解决。 + + + +> **两阶段提交的过程是怎样的?** + +两个阶段提交就是**将 redo log 的写入拆成了两个步骤:prepare 和 commit,中间再穿插写入binlog**,具体如下: + +- **prepare 阶段**: 将 redo log 持久化到磁盘(innodb_flush_log_at_trx_commit = 1 的作用),将 redo log 对应的事务状态设置为 prepare; +- **commit 阶段**:将 binlog 持久化到磁盘(sync_binlog = 1 的作用),然后将 redo log 状态设置为 commit。 + +当在写bin log之前崩溃时:此时 binlog 还没写,redo log 也还没提交,事务会回滚。 日志保持一致 + +当在写bin log之后崩溃时: 重启恢复后虽没有commit,但满足prepare和binlog完整,自动commit。日志保持一致 + + + +> **两阶段提交有什么问题?** + +* **磁盘IO次数高**:对于“双1”配置,每个事务提交都会进行两次 fsync(刷盘),一次是 redo log 刷盘,另一次是 binlog 刷盘。 +* **锁竞争激烈**:两阶段提交虽然能够保证「单事务」两个日志的内容一致,但在「多事务」的情况下,却不能保证两者的提交顺序一致,因此,在两阶段提交的流程基础上,还需要加一个锁来保证提交的原子性,从而保证多事务的情况下,两个日志的提交顺序一致。 + + + +### 组提交 + +MySQL 引入了 binlog 组提交(group commit)机制,当有多个事务提交的时候,会将多个 binlog 刷盘操作合并成一个,从而减少磁盘 I/O 的次数。 + +引入了组提交机制后,prepare 阶段不变,只针对 commit 阶段,将 commit 阶段拆分为三个过程: + +- **flush 阶段**:多个事务按进入的顺序将 binlog 从 cache 写入文件(不刷盘); +- **sync 阶段**:对 binlog 文件做 fsync 操作(多个事务的 binlog 合并一次刷盘); +- **commit 阶段**:各个事务按顺序做 InnoDB commit 操作; + +上面的**每个阶段都有一个队列**,每个阶段有锁进行保护,因此保证了事务写入的顺序,第一个进入队列的事务会成为 leader,leader领导所在队列的所有事务,全权负责整队的操作,完成后通知队内其他事务操作结束。 + + + +## SQL优化 + +### 主键优化 + +1. 满足业务需求的情况下,尽量降低主键的长度; + +2. 插入数据时,尽量选择顺序插入,选择使用AUTO_INCREMENT自增主键; + +3. 尽量不要使用UUID做主键或者是其他自然主键,如身份证号; + +4. 业务操作时,避免对主键的修改。 + +### order by优化 + +MySQL的排序,有两种方式: + +![](MySql\orderby优化.webp) + +对于以上的两种排序方式,Using index的性能高,而Using filesort的性能低,我们在优化排序操作时,尽量要优化为 Using index。 + +order by优化原则: + +1. 根据排序字段建立合适的索引,多字段排序时,也遵循最左前缀法则; + +2. 尽量使用覆盖索引; + +3. 多字段排序, 一个升序一个降序,此时需要注意联合索引在创建时的规则(ASC/DESC); + +4. 如果不可避免的出现filesort,大数据量排序时,可以适当增大排序缓冲区大小sort_buffer_size(默认256k)。 + +### group by优化 + +在分组操作中,我们需要通过以下两点进行优化,以提升性能: + +1. 在分组操作时,可以通过索引来提高效率; + +2. 分组操作时,索引的使用也是满足最左前缀法则的。 + +### limit优化 + +在数据量比较大时,如果进行limit分页查询,在查询时,越往后,分页查询效率越低。 + +因为,当在进行分页查询时,如果执行 limit 2000000,10 ,此时需要MySQL排序前2000010 记录,仅仅返回 2000000 - 2000010 的记录,其他记录丢弃,查询排序的代价非常大 。 + +优化思路: 一般分页查询时,通过创建覆盖索引能够比较好地提高性能,可以通过覆盖索引加子查询形式进行优化。 + +```sql +explain select * from tb_sku t , (select id from tb_sku order by id limit 2000000,10) a where t.id = a.id; +``` + + + +### count优化 + +如果数据量很大,在执行count操作时,是非常耗时的。InnoDB 引擎中,它执行 count(*) 的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数。 + + + +count() 是一个聚合函数,对于返回的结果集,一行行地判断,如果 count 函数的参数不是 NULL,累计值就加 1,否则不加,最后返回累计值。 + +![](MySql\count.webp) + +性能: + +```sql +count(*) = count(1) > count(主键字段) > count(字段) +``` + +count(1)、 count(*)、 count(主键字段)在执行的时候,如果表里存在二级索引,优化器就会选择二级索引进行扫描。因为二级索引记录比聚簇索引记录占用更少的存储空间。 + +count(1)时, server 层每从 InnoDB 读取到一条记录,就将 count 变量加 1,不会读取任何字段。 + +**count(`*`) 其实等于 count(`0`)**,也就是说,当你使用 count(`*`) 时,MySQL 会将 `*` 参数转化为参数 0 来处理。**count(\*) 执行过程跟 count(1) 执行过程基本一样的** + +count(字段) 来统计记录个数,它的效率是最差的,会采用全表扫描的方式来统计。 + + + +优化思路: + +1. 近似值:使用 show table status 或者 explain 命令来表进行估算。 +2. 额外表保存计数值(redis或mysql) + + + +### update优化 + +我们主要需要注意一下update语句执行时的注意事项。 + +update course set name = 'javaEE' where id = 1 ; + +当我们在执行删除的SQL语句时,会锁定id为1这一行的数据,然后事务提交之后,行锁释放。 + +当我们开启多个事务,再执行如下SQL时: + +update course set name = 'SpringBoot' where name = 'PHP' ; + +我们发现行锁升级为了表锁。导致该update语句的性能大大降低。 + +Innodb的行锁是针对索引加的锁,不是针对记录加的锁,并且该索引不能失效,否则会从行锁升级成表锁。 + + + +## SQL性能分析 + +### sql执行频率 + +Mysql客户端链接成功后,通过以下命令可以查看当前数据库的 insert/update/delete/select 的访问频次: + +show [session|global] status like ‘com_____’; + +session: 查看当前会话; + +global: 查看全局数据; + +com insert: 插入次数; + +com select: 查询次数; + +com delete: 删除次数; + +com updat: 更新次数; + +通过查看当前数据库是以查询为主,还是以增删改为主,从而为数据库优化提供参考依据,如果以增删改为主,可以考虑不对其进行索引的优化;如果以查询为主,就要考虑对数据库的索引进行优化 + +### 慢查询日志 + +慢查询日志记录了所有执行时间超过指定参数(long_query_time,单位秒,默认10秒)的所有sql日志: + +开启慢查询日志前,需要在mysql的配置文件中(/etc/my.cnf)配置如下信息: + +1. 开启mysql慢日志查询开关: + + ``` + slow_query_log = 1 + ``` + + + +2. 设置慢日志的时间,假设为2秒,超过2秒就会被视为慢查询,记录慢查询日志: + + ``` + long_query_time=2 + ``` + + + +3. 配置完毕后,重新启动mysql服务器进行测试: + + ``` + systemctl restarmysqld + ``` + + + +4. 查看慢查询日志的系统变量,是否打开: + + ``` + show variables like “slow_query_log”; + ``` + + + +5. 查看慢日志文件中(/var/lib/mysql/localhost-slow.log)记录的信息: + + ``` + Tail -f localhost-slow.log + ``` + + + +最终发现,在慢查询日志中,只会记录执行时间超过我们预设时间(2秒)的sql,执行较快的sql不会被记录。 + + + +### Profile 详情 + +show profiles 能够在做SQL优化时帮助我们了解时间都耗费到哪里去了。 + +1. 通过 have_profiling 参数,可以看到mysql是否支持profile 操作: + + ``` + select @@have_profiling; + ``` + + + +2. 通过set 语句在session/global 级别开启profiling: + + ``` + set profiling =1; + ``` + + + +​ 开关打开后,后续执行的sql语句都会被mysql记录,并记录执行时间消耗到哪儿去了。比如执行以下几条sql语句: + +​ select * from tb_user; + +​ select * from tb_user where id = 1; + +​ select * from tb_user where name = '白起'; + +​ select count(*) from tb_sku; + + + +3. 查看每一条sql的耗时基本情况: + + ``` + show profiles; + ``` + + + +4. 查看指定的字段的sql 语句各个阶段的耗时情况: + + ``` + show profile for query Query_ID; + ``` + + + +5. 查看指定字段的sql语句cpu 的使用情况: + + ``` + show profile cpu for query Query_ID; + ``` + + + +### explain 详情 + +EXPLAIN 或者 DESC命令获取 MySQL 如何执行 SELECT 语句的信息,包括在 SELECT 语句执行过程中,表如何连接和连接的顺序。 + +语法 :直接在 select 语句之前加上关键字 explain/desc; + +![](MySQL\explain.webp) + +extra 几个重要的参考指标: + +- Using filesort :当查询语句中包含 group by 操作,而且无法利用索引完成排序操作的时候, 这时不得不选择相应的排序算法进行,甚至可能会通过文件排序,效率是很低的,所以要避免这种问题的出现。 +- Using temporary:使了用临时表保存中间结果,MySQL 在对查询结果排序时使用临时表,常见于排序 order by 和分组查询 group by。效率低,要避免这种问题的出现。 +- Using index:所需数据只需在索引即可全部获得,不须要再到表中取数据,也就是使用了覆盖索引,避免了回表操作,效率不错。 + + + +## 范式 + +第一范式:确保原子性,表中每一个列数据都必须是不可再分的字段。 + +第二范式:确保唯一性,每张表都只描述一种业务属性,一张表只描述一件事。 + +第三范式:确保独立性,表中除主键外,每个字段之间不存在任何依赖,都是独立的。 + +巴斯范式:主键字段独立性,联合主键字段之间不能存在依赖性。 + + + +### 第一范式 + +所有的字段都是基本数据字段,不可进一步拆分。 + +### 第二范式 + +在满足第一范式的基础上,还要满足数据表里的每一条数据记录,都是可唯一标识的。而且所有字段,都必须完全依赖主键,不能只依赖主键的一部分。 + +把只依赖于主键一部分的字段拆分出去,形成新的数据表。 + +### 第三范式 + +在满足第二范式的基础上,不能包含那些可以由非主键字段派生出来的字段,或者说,不能存在依赖于非主键字段的字段。 + +### 巴斯-科德范式(BCNF) + +巴斯-科德范式也被称为`3.5NF`,至于为何不称为第四范式,这主要是由于它是第三范式的补充版,第三范式的要求是:任何非主键字段不能与其他非主键字段间存在依赖关系,也就是要求每个非主键字段之间要具备独立性。而巴斯-科德范式在第三范式的基础上,进一步要求:**任何主属性不能对其他主键子集存在依赖**。也就是规定了联合主键中的某列值,不能与联合主键中的其他列存在依赖关系。 + + + +```sh ++-------------------+---------------+--------+------+--------+ +| classes | class_adviser | name | sex | height | ++-------------------+---------------+--------+------+--------+ +| 计算机-2201班 | 熊竹老师 | 竹子 | 男 | 185cm | +| 金融-2201班 | 竹熊老师 | 熊猫 | 女 | 170cm | +| 计算机-2201班 | 熊竹老师 | 子竹 | 男 | 180cm | ++-------------------+---------------+--------+------+--------+ +``` + +例如这张学生表,此时假设以`classes`班级字段、`class_adviser`班主任字段、`name`学生姓名字段,组合成一个联合主键。在这张表中,一条学生信息中的班主任,取决于学生所在的班级,因此这里需要进一步调整结构: + +```sh +SELECT * FROM `zz_classes`; ++------------+-------------------+---------------+ +| classes_id | classes_name | class_adviser | ++------------+-------------------+---------------+ +| 1 | 计算机-2201班 | 熊竹老师 | +| 2 | 金融-2201班 | 竹熊老师 | ++------------+-------------------+---------------+ + +SELECT * FROM `zz_student`; ++------------+--------+------+--------+ +| classes_id | name | sex | height | ++------------+--------+------+--------+ +| 1 | 竹子 | 男 | 185cm | +| 2 | 熊猫 | 女 | 170cm | +| 1 | 子竹 | 男 | 180cm | ++------------+--------+------+--------+ +``` + +经过结构调整后,原本的学生表则又被拆为了班级表、学生表两张,在学生表中只存储班级`ID`,然后使用`classes_id`班级`ID`和`name`学生姓名两个字段作为联合主键。 + + + diff --git a/docs/MySQL/MySQL/COMPACT.drawio.webp b/docs/MySQL/MySQL/COMPACT.drawio.webp new file mode 100644 index 0000000..f7598e3 Binary files /dev/null and b/docs/MySQL/MySQL/COMPACT.drawio.webp differ diff --git a/docs/MySQL/MySQL/count.webp b/docs/MySQL/MySQL/count.webp new file mode 100644 index 0000000..9aa2cfa Binary files /dev/null and b/docs/MySQL/MySQL/count.webp differ diff --git a/docs/MySQL/MySQL/explain.webp b/docs/MySQL/MySQL/explain.webp new file mode 100644 index 0000000..9720af6 Binary files /dev/null and b/docs/MySQL/MySQL/explain.webp differ diff --git a/docs/MySQL/MySQL/mvcc.webp b/docs/MySQL/MySQL/mvcc.webp new file mode 100644 index 0000000..045cffb Binary files /dev/null and b/docs/MySQL/MySQL/mvcc.webp differ diff --git "a/docs/MySQL/MySQL/mysql\346\237\245\350\257\242\346\265\201\347\250\213.webp" "b/docs/MySQL/MySQL/mysql\346\237\245\350\257\242\346\265\201\347\250\213.webp" new file mode 100644 index 0000000..386c6ec Binary files /dev/null and "b/docs/MySQL/MySQL/mysql\346\237\245\350\257\242\346\265\201\347\250\213.webp" differ diff --git "a/docs/MySQL/MySQL/null\345\200\274\345\210\227\350\241\2504.webp" "b/docs/MySQL/MySQL/null\345\200\274\345\210\227\350\241\2504.webp" new file mode 100644 index 0000000..80829b3 Binary files /dev/null and "b/docs/MySQL/MySQL/null\345\200\274\345\210\227\350\241\2504.webp" differ diff --git "a/docs/MySQL/MySQL/orderby\344\274\230\345\214\226.webp" "b/docs/MySQL/MySQL/orderby\344\274\230\345\214\226.webp" new file mode 100644 index 0000000..83c763b Binary files /dev/null and "b/docs/MySQL/MySQL/orderby\344\274\230\345\214\226.webp" differ diff --git a/docs/MySQL/MySQL/read_view.png b/docs/MySQL/MySQL/read_view.png new file mode 100644 index 0000000..7a07e90 Binary files /dev/null and b/docs/MySQL/MySQL/read_view.png differ diff --git a/docs/MySQL/MySQL/redo_log.png b/docs/MySQL/MySQL/redo_log.png new file mode 100644 index 0000000..fdf0b9a Binary files /dev/null and b/docs/MySQL/MySQL/redo_log.png differ diff --git a/docs/MySQL/MySQL/t_test.webp b/docs/MySQL/MySQL/t_test.webp new file mode 100644 index 0000000..94145dd Binary files /dev/null and b/docs/MySQL/MySQL/t_test.webp differ diff --git a/docs/MySQL/MySQL/update.png b/docs/MySQL/MySQL/update.png new file mode 100644 index 0000000..6b88c1e Binary files /dev/null and b/docs/MySQL/MySQL/update.png differ diff --git "a/docs/MySQL/MySQL/\344\270\273\345\244\207\346\265\201\347\250\213.png" "b/docs/MySQL/MySQL/\344\270\273\345\244\207\346\265\201\347\250\213.png" new file mode 100644 index 0000000..2676ad1 Binary files /dev/null and "b/docs/MySQL/MySQL/\344\270\273\345\244\207\346\265\201\347\250\213.png" differ diff --git "a/docs/MySQL/MySQL/\344\272\213\345\212\241a\345\212\240\351\224\201\345\210\206\346\236\220.webp" "b/docs/MySQL/MySQL/\344\272\213\345\212\241a\345\212\240\351\224\201\345\210\206\346\236\220.webp" new file mode 100644 index 0000000..97e1f01 Binary files /dev/null and "b/docs/MySQL/MySQL/\344\272\213\345\212\241a\345\212\240\351\224\201\345\210\206\346\236\220.webp" differ diff --git "a/docs/MySQL/MySQL/\345\217\214M\347\273\223\346\236\204\344\270\273\345\244\207.png" "b/docs/MySQL/MySQL/\345\217\214M\347\273\223\346\236\204\344\270\273\345\244\207.png" new file mode 100644 index 0000000..8c9871d Binary files /dev/null and "b/docs/MySQL/MySQL/\345\217\214M\347\273\223\346\236\204\344\270\273\345\244\207.png" differ diff --git "a/docs/MySQL/MySQL/\345\217\230\351\225\277\345\255\227\346\256\265\351\225\277\345\272\246\345\210\227\350\241\2501.webp" "b/docs/MySQL/MySQL/\345\217\230\351\225\277\345\255\227\346\256\265\351\225\277\345\272\246\345\210\227\350\241\2501.webp" new file mode 100644 index 0000000..ff1d58f Binary files /dev/null and "b/docs/MySQL/MySQL/\345\217\230\351\225\277\345\255\227\346\256\265\351\225\277\345\272\246\345\210\227\350\241\2501.webp" differ diff --git "a/docs/MySQL/MySQL/\345\224\257\344\270\200\347\264\242\345\274\225\345\212\240\351\224\201.drawio.webp" "b/docs/MySQL/MySQL/\345\224\257\344\270\200\347\264\242\345\274\225\345\212\240\351\224\201.drawio.webp" new file mode 100644 index 0000000..7569b67 Binary files /dev/null and "b/docs/MySQL/MySQL/\345\224\257\344\270\200\347\264\242\345\274\225\345\212\240\351\224\201.drawio.webp" differ diff --git "a/docs/MySQL/MySQL/\345\237\272\347\241\200\346\236\266\346\236\204.png" "b/docs/MySQL/MySQL/\345\237\272\347\241\200\346\236\266\346\236\204.png" new file mode 100644 index 0000000..ae014d6 Binary files /dev/null and "b/docs/MySQL/MySQL/\345\237\272\347\241\200\346\236\266\346\236\204.png" differ diff --git "a/docs/MySQL/MySQL/\345\271\266\350\241\214\345\244\215\345\210\266.png" "b/docs/MySQL/MySQL/\345\271\266\350\241\214\345\244\215\345\210\266.png" new file mode 100644 index 0000000..bbe4ee1 Binary files /dev/null and "b/docs/MySQL/MySQL/\345\271\266\350\241\214\345\244\215\345\210\266.png" differ diff --git "a/docs/MySQL/MySQL/\345\271\273\350\257\273\345\217\221\347\224\237.drawio.webp" "b/docs/MySQL/MySQL/\345\271\273\350\257\273\345\217\221\347\224\237.drawio.webp" new file mode 100644 index 0000000..623a9bc Binary files /dev/null and "b/docs/MySQL/MySQL/\345\271\273\350\257\273\345\217\221\347\224\237.drawio.webp" differ diff --git "a/docs/MySQL/MySQL/\346\236\266\346\236\204.jpeg" "b/docs/MySQL/MySQL/\346\236\266\346\236\204.jpeg" new file mode 100644 index 0000000..2dc942f Binary files /dev/null and "b/docs/MySQL/MySQL/\346\236\266\346\236\204.jpeg" differ diff --git "a/docs/MySQL/MySQL/\346\237\245\350\257\242\346\205\242.png" "b/docs/MySQL/MySQL/\346\237\245\350\257\242\346\205\242.png" new file mode 100644 index 0000000..564bde3 Binary files /dev/null and "b/docs/MySQL/MySQL/\346\237\245\350\257\242\346\205\242.png" differ diff --git "a/docs/MySQL/MySQL/\346\241\210\344\276\213\344\270\200.png" "b/docs/MySQL/MySQL/\346\241\210\344\276\213\344\270\200.png" new file mode 100644 index 0000000..133056b Binary files /dev/null and "b/docs/MySQL/MySQL/\346\241\210\344\276\213\344\270\200.png" differ diff --git "a/docs/MySQL/MySQL/\346\241\210\344\276\213\344\270\203.png" "b/docs/MySQL/MySQL/\346\241\210\344\276\213\344\270\203.png" new file mode 100644 index 0000000..87a8026 Binary files /dev/null and "b/docs/MySQL/MySQL/\346\241\210\344\276\213\344\270\203.png" differ diff --git "a/docs/MySQL/MySQL/\346\241\210\344\276\213\344\270\211.png" "b/docs/MySQL/MySQL/\346\241\210\344\276\213\344\270\211.png" new file mode 100644 index 0000000..271103b Binary files /dev/null and "b/docs/MySQL/MySQL/\346\241\210\344\276\213\344\270\211.png" differ diff --git "a/docs/MySQL/MySQL/\346\241\210\344\276\213\344\271\235.png" "b/docs/MySQL/MySQL/\346\241\210\344\276\213\344\271\235.png" new file mode 100644 index 0000000..83b4539 Binary files /dev/null and "b/docs/MySQL/MySQL/\346\241\210\344\276\213\344\271\235.png" differ diff --git "a/docs/MySQL/MySQL/\346\241\210\344\276\213\344\272\214.png" "b/docs/MySQL/MySQL/\346\241\210\344\276\213\344\272\214.png" new file mode 100644 index 0000000..fa203da Binary files /dev/null and "b/docs/MySQL/MySQL/\346\241\210\344\276\213\344\272\214.png" differ diff --git "a/docs/MySQL/MySQL/\346\241\210\344\276\213\344\272\224.png" "b/docs/MySQL/MySQL/\346\241\210\344\276\213\344\272\224.png" new file mode 100644 index 0000000..60f98b3 Binary files /dev/null and "b/docs/MySQL/MySQL/\346\241\210\344\276\213\344\272\224.png" differ diff --git "a/docs/MySQL/MySQL/\346\241\210\344\276\213\345\205\253.png" "b/docs/MySQL/MySQL/\346\241\210\344\276\213\345\205\253.png" new file mode 100644 index 0000000..f1d576a Binary files /dev/null and "b/docs/MySQL/MySQL/\346\241\210\344\276\213\345\205\253.png" differ diff --git "a/docs/MySQL/MySQL/\346\241\210\344\276\213\345\205\255.png" "b/docs/MySQL/MySQL/\346\241\210\344\276\213\345\205\255.png" new file mode 100644 index 0000000..4c73bfd Binary files /dev/null and "b/docs/MySQL/MySQL/\346\241\210\344\276\213\345\205\255.png" differ diff --git "a/docs/MySQL/MySQL/\346\241\210\344\276\213\345\233\233.png" "b/docs/MySQL/MySQL/\346\241\210\344\276\213\345\233\233.png" new file mode 100644 index 0000000..4b171dd Binary files /dev/null and "b/docs/MySQL/MySQL/\346\241\210\344\276\213\345\233\233.png" differ diff --git "a/docs/MySQL/MySQL/\350\201\232\351\233\206\347\264\242\345\274\225\345\222\214\344\272\214\347\272\247\347\264\242\345\274\225.webp" "b/docs/MySQL/MySQL/\350\201\232\351\233\206\347\264\242\345\274\225\345\222\214\344\272\214\347\272\247\347\264\242\345\274\225.webp" new file mode 100644 index 0000000..0761647 Binary files /dev/null and "b/docs/MySQL/MySQL/\350\201\232\351\233\206\347\264\242\345\274\225\345\222\214\344\272\214\347\272\247\347\264\242\345\274\225.webp" differ diff --git "a/docs/MySQL/MySQL/\350\241\250\347\251\272\351\227\264\347\273\223\346\236\204.drawio.webp" "b/docs/MySQL/MySQL/\350\241\250\347\251\272\351\227\264\347\273\223\346\236\204.drawio.webp" new file mode 100644 index 0000000..9500c59 Binary files /dev/null and "b/docs/MySQL/MySQL/\350\241\250\347\251\272\351\227\264\347\273\223\346\236\204.drawio.webp" differ diff --git "a/docs/MySQL/MySQL/\351\232\224\347\246\273\347\272\247\345\210\253.webp" "b/docs/MySQL/MySQL/\351\232\224\347\246\273\347\272\247\345\210\253.webp" new file mode 100644 index 0000000..46d0b35 Binary files /dev/null and "b/docs/MySQL/MySQL/\351\232\224\347\246\273\347\272\247\345\210\253.webp" differ diff --git "a/docs/MySQL/MySQL/\351\235\236\345\224\257\344\270\200\347\264\242\345\274\225\347\255\211\345\200\274\346\237\245\350\257\242age=25.drawio.webp" "b/docs/MySQL/MySQL/\351\235\236\345\224\257\344\270\200\347\264\242\345\274\225\347\255\211\345\200\274\346\237\245\350\257\242age=25.drawio.webp" new file mode 100644 index 0000000..1dd715e Binary files /dev/null and "b/docs/MySQL/MySQL/\351\235\236\345\224\257\344\270\200\347\264\242\345\274\225\347\255\211\345\200\274\346\237\245\350\257\242age=25.drawio.webp" differ diff --git "a/docs/MySQL/MySQL/\351\235\236\345\224\257\344\270\200\347\264\242\345\274\225\347\255\211\345\200\274\346\237\245\350\257\242\345\255\230\345\234\250.drawio.webp" "b/docs/MySQL/MySQL/\351\235\236\345\224\257\344\270\200\347\264\242\345\274\225\347\255\211\345\200\274\346\237\245\350\257\242\345\255\230\345\234\250.drawio.webp" new file mode 100644 index 0000000..72bf3ad Binary files /dev/null and "b/docs/MySQL/MySQL/\351\235\236\345\224\257\344\270\200\347\264\242\345\274\225\347\255\211\345\200\274\346\237\245\350\257\242\345\255\230\345\234\250.drawio.webp" differ diff --git "a/docs/MySQL/MySQL/\351\235\236\345\224\257\344\270\200\347\264\242\345\274\225\350\214\203\345\233\264\346\237\245\350\257\242age\345\244\247\344\272\216\347\255\211\344\272\21622.drawio.webp" "b/docs/MySQL/MySQL/\351\235\236\345\224\257\344\270\200\347\264\242\345\274\225\350\214\203\345\233\264\346\237\245\350\257\242age\345\244\247\344\272\216\347\255\211\344\272\21622.drawio.webp" new file mode 100644 index 0000000..635616b Binary files /dev/null and "b/docs/MySQL/MySQL/\351\235\236\345\224\257\344\270\200\347\264\242\345\274\225\350\214\203\345\233\264\346\237\245\350\257\242age\345\244\247\344\272\216\347\255\211\344\272\21622.drawio.webp" differ diff --git "a/docs/Redis/Redis/3\345\261\202\350\267\263\350\241\250-\350\267\250\345\272\246.drawio.webp" "b/docs/Redis/Redis/3\345\261\202\350\267\263\350\241\250-\350\267\250\345\272\246.drawio.webp" new file mode 100644 index 0000000..5f95af2 Binary files /dev/null and "b/docs/Redis/Redis/3\345\261\202\350\267\263\350\241\250-\350\267\250\345\272\246.drawio.webp" differ diff --git a/docs/Redis/Redis/SDS.png b/docs/Redis/Redis/SDS.png new file mode 100644 index 0000000..1648e7f Binary files /dev/null and b/docs/Redis/Redis/SDS.png differ diff --git a/docs/Redis/Redis/aofewwrite.png b/docs/Redis/Redis/aofewwrite.png new file mode 100644 index 0000000..2484f3d Binary files /dev/null and b/docs/Redis/Redis/aofewwrite.png differ diff --git a/docs/Redis/Redis/embstr.png b/docs/Redis/Redis/embstr.png new file mode 100644 index 0000000..60eb0b8 Binary files /dev/null and b/docs/Redis/Redis/embstr.png differ diff --git a/docs/Redis/Redis/lazyfree.png b/docs/Redis/Redis/lazyfree.png new file mode 100644 index 0000000..2ac8e2a Binary files /dev/null and b/docs/Redis/Redis/lazyfree.png differ diff --git a/docs/Redis/Redis/listpack.webp b/docs/Redis/Redis/listpack.webp new file mode 100644 index 0000000..486575d Binary files /dev/null and b/docs/Redis/Redis/listpack.webp differ diff --git a/docs/Redis/Redis/quicklist.png b/docs/Redis/Redis/quicklist.png new file mode 100644 index 0000000..c1f721c Binary files /dev/null and b/docs/Redis/Redis/quicklist.png differ diff --git a/docs/Redis/Redis/raw.png b/docs/Redis/Redis/raw.png new file mode 100644 index 0000000..37467bb Binary files /dev/null and b/docs/Redis/Redis/raw.png differ diff --git "a/docs/Redis/Redis/redisson\345\210\206\345\270\203\345\274\217\351\224\201.png" "b/docs/Redis/Redis/redisson\345\210\206\345\270\203\345\274\217\351\224\201.png" new file mode 100644 index 0000000..29921d8 Binary files /dev/null and "b/docs/Redis/Redis/redisson\345\210\206\345\270\203\345\274\217\351\224\201.png" differ diff --git a/docs/Redis/Redis/rehash.png b/docs/Redis/Redis/rehash.png new file mode 100644 index 0000000..782a1bc Binary files /dev/null and b/docs/Redis/Redis/rehash.png differ diff --git a/docs/Redis/Redis/scan.png b/docs/Redis/Redis/scan.png new file mode 100644 index 0000000..a67266d Binary files /dev/null and b/docs/Redis/Redis/scan.png differ diff --git a/docs/Redis/Redis/slot.jpg b/docs/Redis/Redis/slot.jpg new file mode 100644 index 0000000..c9d81dc Binary files /dev/null and b/docs/Redis/Redis/slot.jpg differ diff --git a/docs/Redis/Redis/slot.png b/docs/Redis/Redis/slot.png new file mode 100644 index 0000000..281a146 Binary files /dev/null and b/docs/Redis/Redis/slot.png differ diff --git a/docs/Redis/Redis/stream.png b/docs/Redis/Redis/stream.png new file mode 100644 index 0000000..316e449 Binary files /dev/null and b/docs/Redis/Redis/stream.png differ diff --git "a/docs/Redis/Redis/\344\272\224\347\247\215\346\225\260\346\215\256\347\261\273\345\236\213.webp" "b/docs/Redis/Redis/\344\272\224\347\247\215\346\225\260\346\215\256\347\261\273\345\236\213.webp" new file mode 100644 index 0000000..0b8bf7d Binary files /dev/null and "b/docs/Redis/Redis/\344\272\224\347\247\215\346\225\260\346\215\256\347\261\273\345\236\213.webp" differ diff --git "a/docs/Redis/Redis/\345\215\207\347\272\247\346\223\215\344\275\234.png" "b/docs/Redis/Redis/\345\215\207\347\272\247\346\223\215\344\275\234.png" new file mode 100644 index 0000000..e18619c Binary files /dev/null and "b/docs/Redis/Redis/\345\215\207\347\272\247\346\223\215\344\275\234.png" differ diff --git "a/docs/Redis/Redis/\345\216\213\347\274\251\345\210\227\350\241\250.png" "b/docs/Redis/Redis/\345\216\213\347\274\251\345\210\227\350\241\250.png" new file mode 100644 index 0000000..e5b524a Binary files /dev/null and "b/docs/Redis/Redis/\345\216\213\347\274\251\345\210\227\350\241\250.png" differ diff --git "a/docs/Redis/Redis/\345\220\216\345\217\260\347\272\277\347\250\213.webp" "b/docs/Redis/Redis/\345\220\216\345\217\260\347\272\277\347\250\213.webp" new file mode 100644 index 0000000..0d52bbe Binary files /dev/null and "b/docs/Redis/Redis/\345\220\216\345\217\260\347\272\277\347\250\213.webp" differ diff --git "a/docs/Redis/Redis/\345\223\210\345\270\214\350\241\250.png" "b/docs/Redis/Redis/\345\223\210\345\270\214\350\241\250.png" new file mode 100644 index 0000000..0b397d1 Binary files /dev/null and "b/docs/Redis/Redis/\345\223\210\345\270\214\350\241\250.png" differ diff --git "a/docs/Redis/Redis/\346\225\260\346\215\256\347\273\223\346\236\204.png" "b/docs/Redis/Redis/\346\225\260\346\215\256\347\273\223\346\236\204.png" new file mode 100644 index 0000000..99cf70c Binary files /dev/null and "b/docs/Redis/Redis/\346\225\260\346\215\256\347\273\223\346\236\204.png" differ diff --git "a/docs/Redis/Redis/\346\234\211\345\272\217\351\233\206\345\220\210.jpeg" "b/docs/Redis/Redis/\346\234\211\345\272\217\351\233\206\345\220\210.jpeg" new file mode 100644 index 0000000..1b98b45 Binary files /dev/null and "b/docs/Redis/Redis/\346\234\211\345\272\217\351\233\206\345\220\210.jpeg" differ diff --git "a/docs/Redis/Redis/\350\267\263\350\241\250.jpeg" "b/docs/Redis/Redis/\350\267\263\350\241\250.jpeg" new file mode 100644 index 0000000..d8d8db7 Binary files /dev/null and "b/docs/Redis/Redis/\350\267\263\350\241\250.jpeg" differ diff --git "a/docs/Redis/Redis/\351\200\273\350\276\221\350\277\207\346\234\237.png" "b/docs/Redis/Redis/\351\200\273\350\276\221\350\277\207\346\234\237.png" new file mode 100644 index 0000000..c659b8d Binary files /dev/null and "b/docs/Redis/Redis/\351\200\273\350\276\221\350\277\207\346\234\237.png" differ diff --git "a/docs/Redis/Redis/\351\223\276\350\241\250.png" "b/docs/Redis/Redis/\351\223\276\350\241\250.png" new file mode 100644 index 0000000..03affca Binary files /dev/null and "b/docs/Redis/Redis/\351\223\276\350\241\250.png" differ diff --git "a/docs/Redis/Redis/\351\230\277\351\207\214\344\272\221Redis\345\274\200\345\217\221\350\247\204\350\214\203.jpg" "b/docs/Redis/Redis/\351\230\277\351\207\214\344\272\221Redis\345\274\200\345\217\221\350\247\204\350\214\203.jpg" new file mode 100644 index 0000000..b38001a Binary files /dev/null and "b/docs/Redis/Redis/\351\230\277\351\207\214\344\272\221Redis\345\274\200\345\217\221\350\247\204\350\214\203.jpg" differ diff --git "a/docs/Redis/Redis\345\237\272\347\241\200.md" "b/docs/Redis/Redis\345\237\272\347\241\200.md" new file mode 100644 index 0000000..987f478 --- /dev/null +++ "b/docs/Redis/Redis\345\237\272\347\241\200.md" @@ -0,0 +1,763 @@ +https://github.com/redis/redis + +https://redis.io/ + +https://redis.com.cn/ + +http://doc.redisfans.com/ + +## Redis基础 + +修改redis.conf文件 + +``` +redis.conf配置文件,改完后确保生效,记得重启 + 1 默认daemonize no 改为 daemonize yes + 2 默认protected-mode yes 改为 protected-mode no + 3 默认bind 127.0.0.1 改为 直接注释掉(默认bind 127.0.0.1只能本机访问)或改成本机IP地址,否则影响远程IP连接 + 4 添加redis密码 改为 requirepass 你自己设置的密码 +``` + +启动服务 `redis-server 配置文件` + +连接服务 `redis-cli -a 密码 -p 6379` + +关闭服务 单例模式 `redis-cli -a 密码 shutdown` ;多例模式 `redis-cli -p 6379 shutdown` + + + +### 数据结构 + +应用场景: + +- String 类型的应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。 +- List 类型的应用场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)等。 +- Hash 类型:缓存对象、购物车等。 +- Set 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。 +- Zset 类型:排序场景,比如排行榜、电话和姓名排序等。 + +- BitMap(2.2 版新增):二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等; +- HyperLogLog(2.8 版新增):海量数据基数统计的场景,比如百万级网页 UV 计数等; +- GEO(3.2 版新增):存储地理位置信息的场景,比如滴滴叫车; +- Stream(5.0 版新增):消息队列,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。 + + + +#### String + +String是redis最基本的数据类型,一个key对应一个value。value可以保存字符串和数字,value最多可以容纳 **512 MB** + +String 类型的底层的数据结构实现主要是 **SDS(简单动态字符串)** + +**应用场景** + +1. 缓存对象:缓存对象的json;属性分离缓存 + +2. 常规计数:因为 Redis 处理命令是单线程,所以执行命令的过程是原子的。访问次数、点赞、转发、库存量等(`INCR key`) + +3. 分布式锁(`setnx key value`) + +4. 共享Session信息:分布式系统中将Session保存到redis中 + + + +#### List + +Redis列表是最简单的字符串列表,按照插入顺序排序。List 类型的底层数据结构是由**双向链表或压缩列表**,最多可以包含`2^32-1`个元素。在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表。 + + + +**应用场景**:消息队列 + +消息队列必须满足三个要求:消息保序、处理重复消息、消息可靠 + +- 消息保序:使用 LPUSH + RPOP;阻塞读取:使用 BRPOP; +- 重复消息处理:生产者自行实现全局唯一 ID; +- 消息的可靠性:使用 BRPOPLPUSH,作用是让消费者程序从一个 List 中读取消息,同时,Redis 会把这个消息再插入到另一个 List(可以叫作备份 List)留存 + +List 作为消息队列有什么缺陷?不支持多个消费者消费同一个消息 + + + +#### Hash + +Redis Hash是一个string类型的field(字段)和value(值)的映射表,Hash特别适合用户存储对象。Redis中每个Hash可以存储2^32-1个键值对。 + +Hash 类型的底层数据结构是**压缩列表或哈希表**,在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。 + + + +**应用场景:** + +1. 缓存对象 + + String + Json也是存储对象的一种方式,那么存储对象时,到底用 String + json 还是用 Hash 呢? + + 一般对象用 String + Json 存储,对象中某些频繁变化的属性可以考虑抽出来用 Hash 类型存储。 + +2. 购物车 + + + +#### Set + +Set 类型是一个无序并唯一的键值集合,它的存储顺序不会按照插入的先后顺序进行存储。 + +一个集合最多可以存储 `2^32-1` 个元素。概念和数学中个的集合基本类似,可以交集,并集,差集等等,所以 Set 类型除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。 + +Set 类型的底层数据结构是哈希表或整数集合。 + + + +**应用场景:** + +集合的主要几个特性,无序、不可重复、支持并交差等操作。因此 Set 类型比较适合用来数据去重和保障数据的唯一性,还可以用来统计多个集合的交集、差集和并集等,当我们存储的数据是无序并且需要去重的情况下,比较适合使用集合类型进行存储。 + +Set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞。 + +在主从集群中,为了避免主库因为 Set 做聚合计算(交集、差集、并集)时导致主库被阻塞,我们可以选择一个从库完成聚合统计,或者把数据返回给客户端,由客户端来完成聚合统计。 + +1. 抽奖:去重功能。key为抽奖活动名,value为员工名称(`spop key 3 或者 SRANDMEMBER key 3 `) + +2. 点赞:一个用户点一次赞。key 是文章id,value 是用户id + +3. 共同好友:交集运算(`sinter key1 key2`) + +#### ZSet + +Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数,redis正是通过分数来为集合中的成员进行从小到大的排序。 + + + +**应用场景:** + +1. 排行榜 + +2. 电话、姓名排序 + + 使用有序集合的 `ZRANGEBYLEX` 或 `ZREVRANGEBYLEX` 可以帮助我们实现电话号码或姓名的排序,我们以 `ZRANGEBYLEX` (返回指定成员区间内的成员,按 key 正序排列,分数必须相同)为例。 + + **注意:不要在分数不一致的 SortSet 集合中去使用 ZRANGEBYLEX和 ZREVRANGEBYLEX 指令,因为获取的结果会不准确。** + + 例如,获取132、133开头的电话 + + ```sh + > ZADD phone 0 13300111100 0 13210414300 0 13252110901 + > ZRANGEBYLEX phone [132 (134 + 1) "13200111100" + 2) "13210414300" + 3) "13252110901" + 4) "13300111100" + 5) "13310414300" + 6) "13352110901" + ``` + +3. 延迟队列:使用有序集合(ZSet)的方式来实现延迟消息队列的,ZSet 有一个 Score 属性可以用来存储延迟执行的时间。 + + 使用 zadd score1 value1 命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务, 通过循环执行队列任务即可。 + +#### 地理空间(GEO) + +Redis GEO主要用于存储地理位置信息,并对存储的信息进行操作,包括:添加地理位置的坐标、获取地理位置的坐标、计算两个位置之间的距离、根据用户给定的经纬度坐标来获取指定范围内的地址位置集合。 + +**内部实现:** + +GEO 本身并没有设计新的底层数据结构,而是直接使用了 **Sorted Set** 集合类型。 + +GEO 类型使用 GeoHash 编码方法实现了经纬度到 Sorted Set 中元素权重分数的转换,这其中的两个关键机制就是「对二维地图做区间划分」和「对区间进行编码」。一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为 Sorted Set 元素的权重分数。 + +这样一来,我们就可以把经纬度保存到 Sorted Set 中,利用 Sorted Set 提供的“按权重进行有序范围查找”的特性,实现 LBS 服务中频繁使用的“搜索附近”的需求。 + +**应用场景**:导航定位、打车 + +#### 基数统计(HyperLogLog) + +HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定且是很小的(使用12KB就能计算2^64个不同元素的基数)。但要注意,HyperLogLog 是统计规则是基于概率完成的,不是非常准确,标准误算率是 0.81%。 + +应用场景:去重、计数 + +1. UV:unique visitor 独立访客数 +2. PV:page view 页面浏览量 +3. DAU:daily active user 日活 +4. MAU:月活 + +#### 位图(bitmap) + +Bitmap,即位图,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。BitMap通过最小的单位bit来进行`0|1`的设置,表示某个元素的值或者状态,时间复杂度为O(1)。 + +由于 bit 是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用**二值统计的场景**。 + +**内部实现** + +Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。 + +String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态,你可以把 Bitmap 看作是一个 bit 数组。 + +**应用场景** + +1. 签到统计 +2. 判断用户登录态。将用户 ID 作为 offset,在线就设置为 1,下线设置 0。通过 `GETBIT`判断对应的用户是否在线。 5000 万用户只需要 6 MB 的空间。 +3. 连续签到用户总数。把每天的日期作为 Bitmap 的 key,userId 作为 offset,对应的 bit 位做 『与』运算 + +#### 位域(bitfield) + +通过bitfield命令可以一次性操作多个比特位,它会执行一系列操作并返回一个响应数组,这个数组中的元素对应参数列表中的相应操作的执行结果。主要功能是: + +* 位域修改 +* 溢出控制 + * WRAP:使用回绕(wrap around)方法处理有符号整数和无符号整数溢出情况 + * SAT:使用饱和计算(saturation arithmetic)方法处理溢出,下溢计算的结果为最小的整数值,而上溢计算的结果为最大的整数值 + * fail:命令将拒绝执行那些会导致上溢或者下溢情况出现的计算,并向用户返回空值表示计算未被执行 + +#### Redis流(Stream) + +Redis Stream 主要用于消息队列(MQ,Message Queue) + +* Redis 本身是有一个 **Redis 发布订阅 (pub/sub)** 来实现消息队列的功能,但它有个缺点就是**消息无法持久化**,如果出现网络断开、Redis 宕机等,消息就会被丢弃。 + +* List 实现消息队列的方式不能重复消费,一个消息消费完就会被删除,而且生产者需要自行实现全局唯一 ID,**不支持多播,分组消费** + +而 Redis Stream 支持消息的持久化、支持自动生成全局唯一ID、支持ack确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠 + + + +### 常用命令 + +```shell +keys * # 查看当前库所有的key +set key value +get key +exists key # 判断某个key是否存在 +type key # 查看你的key是什么类型 +del key # 删除指定的key数据 是原子的删除,只有删除成功了才会返回删除结果 +unlink key # 非阻塞删除,仅仅将keys从keyspace元数据中删除,真正的删除会在后续异步中操作。 +ttl key # 查看还有多少秒过期,-1表示永不过期,-2表示已过期 +expire key 秒钟 # 为给定的key设置过期时间 +move key dbindex[0-15]# 将当前数据库的key移动到给定的数据库DB当中 +select dbindex # 切换数据库【0-15】,默认为0 +dbsize # 查看当前数据库key的数量 +flushdb # 清空当前库 +flushall # 通杀全部库 +``` + +帮助命令: **`help @类型`** 。例如,`help @string` + + + +### 持久化 + +AOF 文件的内容是操作命令; + +RDB 文件的内容是二进制数据。 + +#### RDB (Redis Database) + +在指定的时间间隔内将内存中的数据集快照写入磁盘,恢复时再将硬盘快照文件直接读回到内存里。 + +Redis的数据都在内存中,保存备份时它执行的是**全量快照**,也就是说,把内存中的所有数据都记录到磁盘中。 + +RDB保存的是dump.rdb文件。 + +**持久化方式**: + +自动触发:修改 redis.conf 里配置的 `save `。自动执行 bgsave 命令,会创建子进程来生成 RDB 快照文件。 + +手动触发:使用`save`或者`bgsave`命令。save在主程序中执行会**阻塞**当前redis服务器,直到持久化工作完成, 执行save命令期间,Redis不能处理其他命令,**线上禁止使用**。bgsave会在后台异步进行快照操作,**不阻塞**,该方式会fork一个子进程由子进程完成持久化过程。 + +如果服务器开启了AOF持久化功能,那么服务器优先使用AOF文件来还原数据库状态,只有AOF处于关闭状态,才会使用RDB。 + + + +**优势**: + +1. 适合大规模的数据恢复 +2. 按照业务定时备份 +3. 父进程不会执行磁盘I/О或类似操作,会fork一个子进程完成持久化工作,性能高。 +4. RDB文件在内存中的**加载速度要比AOF快很多** + + + +**劣势**: + +1. 在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失从当前至最近一次快照期间的数据,**快照之间的数据会丢失** +2. 内存数据的全量同步,如果数据量太大会导致IO严重影响服务器性能。因为RDB需要经常fork()以便使用子进程在磁盘上持久化。如果数据集很大,fork()可能会很耗时,并且如果数据集很大并且CPU性能不是很好,可能会导致Redis停止为客户端服务几毫秒甚至一秒钟。 + + + +如何检查修复dump.rdb文件? + +进入到redis安装目录,执行redis-check-rdb命令 `redis-check-rdb ./redisconfig/dump.rdb` + + + +哪些情况会触发RDB快照? + +1. 配置文件中默认的快照配置 +2. 手动save/bgsave命令 +3. 执行flushdb/fulshall命令也会产生dump.rdb文件,但是也会将命令记录到dump.rdb文件中,恢复后依旧是空,无意义 +4. 执行shutdown且没有设置开启AOF持久化 +5. 主从复制时,主节点自动触发 + + + +如何禁用快照? + +1. 动态所有停止RDB保存规则的方法:redis-cli config set value "" +2. 手动修改配置文件 + + + +RDB 在执行快照的时候,数据能修改吗? + +可以的,执行 bgsave 过程中,Redis 依然**可以继续处理操作命令**的,也就是数据是能被修改的,关键的技术就在于**写时复制技术(Copy-On-Write, COW)。** + +执行 bgsave 命令的时候,会通过 fork() 创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,但是页表指向的物理内存还是一个,此时如果执行读操作,则主进程和 bgsave 子进程互相不影响。 + +如果主进程执行写操作,则被修改的数据会复制一份副本,主线程在这个数据副本进行修改操作,然后 bgsave 子进程会把原来的数据写入 RDB 文件,新修改的数据只能交由下一次的 bgsave 快照。 + + + +#### AOF (Append Only File) + +**以日志的形式来记录每个写操作**,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但是不可以改写文件,恢复时,以逐一执行命令的方式来进行数据恢复。 + +默认情况下,redis是没有开启AOF的。 + +**开启:** + +开启AOF功能需要设置配置:appendonly yes + +AOF保存的是 appendonly.aof 文件 + + + +**aof文件**: + +* redis6及之前:appendonly.aof + +* Redis7 Multi Part AOF的设计, 将AOF分为三种类型: + + * BASE: 表示基础AOF,它一般由子进程通过重写产生,该文件最多只有一个。 + * INCR:表示增量AOF,它一般会在AOFRW开始执行时被创建,该文件可能存在多个。 + * HISTORY:表示历史AOF,它由BASE和INCR AOF变化而来,每次AOFRW成功完成时,本次AOFRW之前对应的BASE和INCR AOF都将变为HISTORY,HISTORY类型的AOF会被Redis自动删除。 + + 为了管理这些AOF文件,我们引入了一个manifest (清单)文件来跟踪、管理这些AOF。 + + +异常修复命令:`redis-check-aof --fix incr文件` + + + +**AOF持久化流程**: + +**先执行写命令,然后记录命令到AOF日志。** + +1. 日志并不是直接写入AOF文件,会将其这些命令先放入AOF缓存中进行保存。这里的AOF缓冲区实际上是内存中的一片区域,存在的目的是当这些命令达到一定量以后再写入磁盘,避免频繁的磁盘IO操作。 + +2. AOF缓冲会根据AOF缓冲区同步文件的三种写回策略将命令写入磁盘上的AOF文件。 + 1. **ALways**:同步写回,每个写命令执行完立刻同步地将日志写回磁盘。 + + 2. **everysec**(默认):每秒写回,每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔1秒把缓冲区中的内容写入到磁盘 + 3. **no**:操作系统控制的写回,每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘 + +3. 随着写入AOF内容的增加为避免文件膨胀,会根据规则进行命令的合并(**AOF重写**),从而起到AOF文件压缩的目的。 + +4. 当Redis Server服务器重启的时候会对AOF文件载入数据。 + + + +**为什么先执行命令,再把数据写入日志呢?** + +Reids 是先执行写操作命令后,才将该命令记录到 AOF 日志里的,这么做其实有两个好处。 + +- **避免额外的检查开销**:因为如果先将写操作命令记录到 AOF 日志里,再执行该命令的话,如果当前的命令语法有问题,那么如果不进行命令语法检查,该错误的命令记录到 AOF 日志里后,Redis 在使用日志恢复数据时,就可能会出错。 +- **不会阻塞当前写操作命令的执行**:因为当写操作命令执行成功后,才会将命令记录到 AOF 日志。 + +当然,这样做也会带来风险: + +- **数据可能会丢失:** 执行写操作命令和记录日志是两个过程,那当 Redis 在还没来得及将命令写入到硬盘时,服务器发生宕机了,这个数据就会有丢失的风险。 +- **可能阻塞其他操作:** 由于写操作命令执行成功后才记录到 AOF 日志,所以不会阻塞当前命令的执行,但因为 AOF 日志也是在主线程中执行,所以当 Redis 把日志文件写入磁盘的时候,还是会阻塞后续的操作无法执行。 + + + +**AOF重写机制**: + +随着写入AOF内容的增加为避免文件膨胀,会根据规则进行命令的合并(**又称AOF重写**),从而起到**AOF文件压缩的目的。** + +配置项:auto-aof-rewrite-percentage 和 auto-aof-rewrite-min-size 同时满足两个条件时触发重写 + +* 自动触发: 满足配置文件中的选项后,Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时 +* 手动触发: `bgrewriteaof` + +**AOF文件重写并不是对AOF件进行重新整理,而是直接读取服务器数据库中现有的键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。** + +**重写原理:** + +1. 触发重写机制后,主进程会创建一个“重写子进程 **bgrewriteaof**”,这个子进程会携带主进程的数据副本(fork子进程时复制页表,父子进程在写操作之前都共享物理内存空间,从而实现数据共享。主进程第一次写时发生写时复制才会进行物理内存的复制)。重写子进程只会对这个内存进行只读,重写 AOF 子进程会读取数据库里的所有数据,并逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志(新的 AOF 文件)。 + +2. 与此同时,主进程依然能正常处理命令。但是执行写命令时怎么保证数据一致性呢?在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会**同时将这个写命令写入到 「AOF 缓冲区」和 「AOF 重写缓冲区」**。 + + ![](.\Redis\aofewwrite.png) + +3. 当“重写子进程”完成重写工作后,它会给父进程发一个信号,父进程收到信号后将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中 + +4. 当追加结束后,redis就会用新AOF文件来代替旧AOF文件,之后再有新的写指令,就都会追加到新的AOF文件中 + + + +Redis 的**重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的**,这么做可以达到两个好处: + +1. 子进程进行 AOF 重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程; + +2. 子进程带有主进程的数据副本,这里使用子进程而不是线程,因为如果是使用线程,多线程之间会共享内存,那么在修改共享内存数据的时候,需要通过加锁来保证数据的安全,而这样就会降低性能。而使用子进程,创建子进程时,父子进程是共享内存数据的,不过这个共享的内存只能以只读的方式,而当父子进程任意一方修改了该共享内存,就会发生「写时复制」,于是父子进程就有了独立的数据副本,就不用加锁来保证数据安全。 + + + +#### 混合持久化 + +RDB 优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。 + +AOF 优点是丢失数据少,但是数据恢复不快。 + +混合持久化,既保证了 Redis 重启速度,又降低数据丢失风险。 + + + +开启:设置 appendonly 为 yes、aof-use-rdb-preamble 为 yes + + + +当开启了混合持久化时,在 AOF 重写日志时,`fork` 出来的重写子进程会先将与主线程共享的内存数据以 **RDB 方式写入到 AOF 文件**,然后主线程处理的操作命令会被记录在**重写缓冲区**里,重写缓冲区里的增量命令会以 **AOF 方式写入到 AOF 文件**,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。 + +也就是说,使用了混合持久化,AOF 文件的**前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据**。 + + + +#### 纯缓存模式 + +同时关闭RDB+AOF,专心做缓存 + +1. save "" -- 禁用RDB + + 禁用RDB持久化模式下,我们仍然可以使用命令save、bgsave生成RDB文件 + +2. appendonly no -- 禁用AOF + + 禁用AOF持久化模式下,我们仍然可以使用命令bgrewriteaof生成AOF文件 + + + +AOF 重写机制和 RDB 快照(bgsave 命令)的过程,都会分别通过 `fork()` 函数创建一个子进程来处理任务。会有两个阶段会导致阻塞父进程(主线程): + +- 创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长; +- 创建完子进程后,如果父进程修改了共享数据中的大 Key,就会发生写时复制,这期间会拷贝物理内存,由于大 Key 占用的物理内存会很大,那么在复制物理内存这一过程,就会比较耗时,所以有可能会阻塞父进程。 + + + +### 事务 + +redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。**Redis 中并没有提供回滚机制 ,并不一定保证原子性** + +1. 开启:以multi开始一个事务 + +2. 入队:将多个命令入队到事务中,接到这些命令并不会立即执行,而是放到等待执行的事务队列里面 + +3. 执行:exec命令触发事务 + +| 性质 | 解释 | +| ---------------- | ------------------------------------------------------------ | +| **不保证原子性** | Redis的事务不保证原子性,也就是不保证所有指令同时成功或同时失败,只有决定是否开始执行全部指令的能力,没有执行到一半进行回滚的能力 | +| 一致性 | redis事务可以保证命令失败的情况下得以回滚,数据能恢复到没有执行之前的样子,是保证一致性的,除非redis进程意外终结 | +| 隔离性 | redis事务是严格遵守隔离性的,原因是redis是单进程单线程模式(v6.0之前),可以保证命令执行过程中不会被其他客户端命令打断 | +| 不保证持久性 | redis持久化策略中不管是RDB还是AOF都是异步执行的,不保证持久性是出于对性能的考虑 | + +* 正常执行:MULTI 标记事务开始 、EXEC 执行事务 +* 放弃事务:MULTI、DISCARD 取消事务 +* 事务全部失败:MULTI后的命令直接报错,EXEC执行也会报错,事务失败,命令全部失效。类似编译错误 +* 事务部分失败:MULTI后的命令没有直接报错(例如,INCR email),EXEC时报错,该条命令失败,其余命令成功。类似运行错误 +* watch监控:使用watch提供乐观锁定。可以在EXEC执行前监视任意数量的键值对,并在EXEC命令执行时检查被监视的键是否至少有一个被修改过,如果是的话拒绝执行事务。 + + + +### 管道 + +管道(pipeline)可以一次性发送多条命令给服务端,**服务端依次处理完毕后,通过一 条响应一次性将结果返回,通过减少客户端与redis的通信次数来实现降低往返延时时间**。pipeline实现的原理是队列,先进先出特性就保证数据的顺序性 + +`cat cmd.txt | redis-cli -a 密码 --pipe` + +pipeline与原生批量命令对比 + +1. 原生批量命令是原子性(例如:mset、mget),pipeline是非原子性的 +2. 原生批量命令一次只能执行一种命令,pipeline支持批量执行不同命令 +3. 原生批量命令是服务端实现,而pipeline需要服务端与客户端共同完成 + +pipeline与事务对比 + +1. 事务具有原子性,管道不具有原子性 +2. 管道一次性将多条命令发送到服务器,事务是一条一条的发,事务只有在接收到exec命令后才会执行 +3. 执行事务时会阻塞其他命令的执行,而执行管道中的命令时不会 + +使用pipeline注意事项 + +1. pipeline缓冲的指令只是会依次执行,不保证原子性,如果执行中指令发生异常,将会继续执行后续的指令 +2. 使用pipeline组装的命令个数不能太多,不然数量过大客户端阻塞的时间可能过久,同时服务端此时也被迫回复一个队列答复,占用很多内存 + + + +### 复制(replication) + +主从复制,读写分离,master可以读写,slave以读为主,当master数据变化的时候,自动将新的数据异步同步到其他的slave数据库。 + + + +**配置**:(配从不配主) + +配置从机: + +1. master 如果配置了 `requirepass` 参数,需要密码登录 ,那么slave就要配置 `masterauth` 来设置校验密码,否则的话master会拒绝slave的访问请求; +2. `replicaof 主库IP 主库端口` + + + +**基本操作命令**: + +`info replication` :可以查看节点的主从关系和配置信息 + +`replicaof 主库IP 主库端口` :一般写入进Redis.conf配置文件内,重启后依然生效 + +`slaveof 主库IP 主库端口 `:每次与master断开之后,都需要重新连接,除非你配置进了redis.conf文件;在运行期间修改slave节点的信息,如果该数据库已经是某个主数据库的从数据库,那么会停止和原主数据库的同步关系,转而和新的主数据库同步 + +`slaveof no one` :使当前数据库停止与其他数据库的同步,转成主数据库 + + + +**主从问题演示** + +1. Q:从机可以执行写命令吗? + + A:不可以,从机只能读 + +2. Q:从机切入点问题?slave是从头开始复制还是从切入点开始复制? + + A: 首次接入全量复制,后续增量复制 + +3. Q:主机shutdown后,从机会上位吗? + + A:从机不动,原地待命,从机数据可以正常使用,等待主机重启归来 + +4. Q:主机shutdown后,重启后主从关系还在吗?从机还能否顺利复制? + + A:主从关系依然存在,从机依旧是从机,可以顺利复制 + +5. Q:某台从机down后,master继续,从机重启后它能跟上大部队吗? + + A:可以,类似于从机切入点问题 + + + +复制功能分为同步和命令传播两个操作:同步又分为完整重同步和部分重同步: + +**完整重同步**:完整重同步用于处理初次复制情况,主服务器创建并发送RDB文件,以及向从服务器发送保存在缓冲区中的写命令进行同步。 + +1. 从服务器向主服务器发送 PSYNC 命令 +2. 收到 PSYNC 命令的主服务器执行 bgsave 命令,后台生成RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令 +3. bgsave 执行完后,将 RDB文件发送给从服务器,从服务器接收并载入这个RDB文件 +4. 主服务器将缓冲区中的所有写命令发送给从服务器,从服务器执行这些命令 + +**部分重同步**:处理断线后重复制情况。主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器来使主从一致。部分重同步功能由三个部分组成: + +* 主服务器的复制偏移量和从服务器的复制偏移量 +* 主服务器的复制积压缓冲器:一个默认大小1MB的队列 +* 服务器的运行ID + +当从服务器重新连接上主服务器时,从服务器会通过 PSYNC 命令将自己的偏移量和服务器运行ID发送给主服务器: + +* 如果偏移量之后的数据仍然存在于复制积压缓冲区里面,那么执行部分重同步操作 +* 相反,如果偏移量之后的数据已经不存在于复制积压缓缓区,那么执行完整重同步操作 + +**命令传播**:主服务器将自己执行的写命令发送给从服务器,从服务器接收并执行这些命令。 + + + +**怎么判断 Redis 某个节点是否正常工作?** + +Redis 判断节点是否正常工作,基本都是通过互相的 ping-pong 心态检测机制,如果有一半以上的节点去 ping 一个节点的时候没有 pong 回应,集群就会认为这个节点挂掉了,会断开与这个节点的连接。 + +Redis 主从节点发送的心态间隔是不一样的,而且作用也有一点区别: + +- Redis 主节点默认每隔 10 秒对从节点发送 ping 命令,判断从节点的存活性和连接状态,可通过参数repl-ping-slave-period控制发送频率。 +- Redis 从节点每隔 1 秒发送 `replconf ack ` 命令,作用: + - 检测主从节点网络连接状态; + - 检测命令丢失。 + + + +### 哨兵(Sentinel) + +监视一个或多个主服务器及这些主服务器属下的从服务器,当主服务器进入下线状态时,自动将下线主服务器的某个从服务器升级为新的主服务器继续处理命令请求。 + + + +**配置:**sentinel.conf + +`sentinel monitor ` : 设置要监控的master服务器,quorum表示最少有几个哨兵认可客观下线,同意故障迁移的法定票数 + +`sentinel auth-pass ` + +其他参数: + +``` +sentinel down-after-milliseconds :指定多少毫秒之后,主节点没有应答哨兵,此时哨兵主观上认为主节点下线 +sentinel parallel-syncs :表示允许并行同步的slave个数,当Master挂了后,哨兵会选出新的Master,此时,剩余的slave会向新的master发起同步数据 +sentinel failover-timeout :故障转移的超时时间,进行故障转移时,如果超过设置的毫秒,表示故障转移失败 +sentinel notification-script :配置当某一事件发生时所需要执行的脚本 +sentinel client-reconfig-script :客户端重新配置主节点参数脚本 +``` + +注意:主机后续可能会变成从机,所以也需要设置 masterauth 项访问密码,不然后续可能报错master_link_status:down + + + +**启动哨兵方式:** + +1. `redis-sentinel /path/to/sentinel.conf` +2. `redis-server /path/to/sentinel.conf --sentinel` + + + +文件的内容,在运行期间会被sentinel动态进行更改。Master-Slave切换后,master_redis.conf、slave_redis.conf和sentinel.conf的内容都会发生改变,即master_redis.conf中会多一行slaveof的配置,sentinel.conf的监控目标会随之调换 + + + +**哨兵运行流程和选举原理:** + +当一个主从配置中master失效后,哨兵可以选举出一个新的master用于自动接替原master的工作,主从配置中的其他redis服务器自动指向新的master同步数据 + +* **主观下线**:指的是**单个Sentinel实例对服务器做出的下线判断**,即单个sentinel认为某个服务下线(有可能是接收不到订阅,之间的网络不通等等原因)。如果主节点或者从节点没有在规定的时间内响应哨兵的 PING 命令,哨兵就会将它们标记为「**主观下线**」。 + +* **客观下线**:当一个哨兵判断主节点为「主观下线」后,就会向其他哨兵发起命令,其他哨兵收到这个命令后,就会根据自身和主节点的网络状况,做出赞成投票或者拒绝投票的响应。当这个哨兵的**赞同票数达到哨兵配置文件中的 quorum 配置项设定的值后**,这时主节点就会被该哨兵标记为「客观下线」。 + + + +**整体流程**: + +*1、第一轮投票:判断主节点下线* + +当哨兵集群中的某个哨兵判定主节点下线(主观下线)后,就会向其他哨兵发起命令,其他哨兵收到这个命令后,就会根据自身和主节点的网络状况,做出赞成投票或者拒绝投票的响应。 + +当这个哨兵的赞同票数达到哨兵配置文件中的 quorum 配置项设定的值后,这时主节点就会被该哨兵标记为「客观下线」。 + +*2、第二轮投票:选出哨兵 leader* + +某个哨兵判定主节点客观下线后,该哨兵就会发起投票,告诉其他哨兵,它想成为 leader,想成为 leader 的哨兵节点,要满足两个条件: + +- 第一,拿到半数以上的赞成票; +- 第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。 + +*3、由哨兵 leader 进行主从故障转移* + +选举出了哨兵 leader 后,就可以进行主从故障转移的过程了。该操作包含以下四个步骤: + +- 第一步:挑选出一个从节点,并将其转换为主节点,选择的规则: + - 过滤掉已经离线的从节点; + - 过滤掉历史网络连接状态不好的从节点; + - 将剩下的从节点,进行三轮考察:优先级、偏移量、ID 号。在每一轮考察过程中,如果找到了一个胜出的从节点,就将其作为新主节点。 +- 第二步,让其他从节点修改复制目标,修改为复制新主节点; +- 第三步:将新主节点的 IP 地址和信息,通过发布者/订阅者机制通知给客户端主节点更换; +- 第四步:继续监视旧主节点,当这个旧主节点重新上线时,将它设置为新主节点的从节点; + + + +### 集群(Cluster) + +集群通过分片来进行数据共享,并提供复制和故障转移功能。 + + + +**槽指派** + +redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于16384槽中的的其中一个,集群中的每个节点可以处理0个或最多16384个槽。 + +Redis集群有16384个哈希槽每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。 + +如果键所在的槽正好就指派给当前节点,那么节点直接执行这个命令;否则会指引客户端转向正确的节点再次发送要执行的命令。 + +![](.\Redis\slot.jpg) + + + +**slot槽位映射3种解决方案**: + +* 哈希取余分区: hash(key)%N 在服务器个数固定不变时没有问题,如果需要弹性扩容或故障停机的情况下,取模公式就会发生变化 + +* 一致性哈希算法分区: + + 1. 构建一致性哈希环: 一致性Hash算法是对2^32取模,简单来说,一致性Hash算法将整个哈希值空间组织成一个虚拟的圆环 + 2. 将redis服务器 IP节点映射到哈希环某个位置 + 3. key落到服务器的落键规则:当我们需要存储一个kv键值对时,首先将这个key使用相同的函数Hash计算出哈希值并确定此数据在环上的位置,**从此位置沿环顺时针“行走”**,**第一台遇到的服务器就是其应该定位到的服务器**,并将该键值对存储在该节点上。 + + 优点: + + 1. 容错性:在一致性Hash算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响 + 2. 扩展性:数据量增加了,需要增加一台节点NodeX,X的位置在A和B之间,那收到影响的也就是A到X之间的数据,重新把A到X的数据录入到X上即可,不会导致hash取余全部数据重新洗牌。 + + 缺点:一致性Hash算法在服务节点太少时,容易因为节点分布不均匀而造成**数据倾斜**(集中存储在某台服务器上) + +* 哈希槽分区:为解决数据倾斜问题,在数据和节点之间又加入了一层,把这层称为哈希槽(slot)。Redis集群中内置了16384个哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点,数据映射到哈希槽,根据操作分配到对应节点。集群会记录节点和槽的对应关系,哈希槽的计算:HASH_SLOT = CRC16(key) mod 16384。 + + + +**为什么集群的最大槽数是16384个?** + +![](Redis\slot.png) + +1. 如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大。 + + 在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。 当槽位为65536时,这块的大小是: 65536÷8÷1024=8kb + + 在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。 当槽位为16384时,这块的大小是: 16384÷8÷1024=2kb + + 因为每秒钟,redis节点需要发送一定数量的ping消息作为心跳包,如果槽位为65536,这个ping消息的消息头太大了,浪费带宽。 + +2. redis的集群节点数量基本不可能超过1000个。 + + 集群节点越多,心跳包的消息体内携带的数据越多。如果节点过1000个,也会导致网络拥堵。因此redis作者不建议redis cluster节点数量超过1000个。 那么,对于节点数在1000以内的redis cluster集群,16384个槽位够用了。没有必要拓展到65536个。 + +3. 槽位越小,节点少的情况下,压缩比高,容易传输 + + Redis主节点的配置信息中它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中会对bitmap进行压缩,但是如果bitmap的填充率slots / N很高的话(N表示节点数),bitmap的压缩率就很低。 如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。 + + + +**集群脑裂导致数据丢失怎么办?** + +脑裂: + +在 Redis 主从架构中,如果主节点的网络突然发生了问题,它与所有的从节点都失联了,但是此时的主节点和客户端的网络是正常的,这个客户端并不知道 Redis 内部已经出现了问题,还在照样的向这个失联的主节点写数据(过程A),此时这些数据被旧主节点缓存到了缓冲区里,因为主从节点之间的网络问题,这些数据都是无法同步给从节点的。 + +这时,哨兵也发现主节点失联了,它就认为主节点挂了(但实际上主节点正常运行,只是网络出问题了),于是哨兵就会在「从节点」中选举出一个 leader 作为主节点,这时集群就有两个主节点了 —— **脑裂出现了**。 + +然后,网络突然好了,哨兵因为之前已经选举出一个新主节点了,它就会把旧主节点降级为从节点(A),然后从节点(A)会向新主节点请求数据同步,**因为第一次同步是全量同步的方式,此时的从节点(A)会清空掉自己本地的数据,然后再做全量同步。所以,之前客户端在过程 A 写入的数据就会丢失了,也就是集群产生脑裂数据丢失的问题**。 + +解决方案: + +当主节点发现从节点下线或者通信超时的总数量小于阈值时,那么**禁止主节点进行写数据**,直接把错误返回给客户端。 + + + +**复制与故障转移**: + +集群中节点分为主节点和从节点,主节点用于处理槽,从节点用于复制某个主节点,并在主节点下线时代替主节点继续处理命令请求。 + +当从节点发现主节点下线时,从节点对下线主节点进行故障转移: + +1. 在从节点中选取一个成为主节点 +2. 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽指派给自己 +3. 新的主节点向集群广播一条PONG消息,让其他节点立即直到这个节点已经由从节点变为主节点。 +4. 新的主节点开始接收和处理自己负责的槽 + + + +### RedisTemplate + +redis客户端:Redisson、Jedis、lettuce等等,官方推荐使用Redisson。 diff --git "a/docs/Redis/Redis\351\253\230\347\272\247.md" "b/docs/Redis/Redis\351\253\230\347\272\247.md" new file mode 100644 index 0000000..886a71c --- /dev/null +++ "b/docs/Redis/Redis\351\253\230\347\272\247.md" @@ -0,0 +1,832 @@ +## Redis高级 + +### Redis单线程 + +redis单线程主要是指Redis的**网络IO和键值对读写是由一个线程来完成**的,Redis在处理客户端的请求时包括**获取 (socket 读)、解析、执行、内容返回 (socket 写**) 等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。这也是Redis对外提供键值存储服务的主要流程。 + +但Redis的其他功能,比如**关闭文件、AOF 刷盘、释放内存**等,其实是由额外的线程执行的。 + +**Redis命令工作线程是单线程的,但是,整个Redis来说,是多线程的;** + + + +之所以 Redis 为「关闭文件、AOF 刷盘、释放内存」这些任务创建单独的线程来处理,是因为这些任务的操作都是很耗时的,如果把这些任务都放在主线程来处理,那么 Redis 主线程就很容易发生阻塞,这样就无法处理后续的请求了。 + +后台线程相当于一个消费者,生产者把耗时任务丢到任务队列中,消费者(BIO)不停轮询这个队列,拿出任务就去执行对应的方法即可。 + + + +#### Redis 采用单线程为什么还这么快? + +1. **基于内存操作**: Redis 的所有数据都存在内存中,因此所有的运算都是内存级别的,所以他的性能比较高; +2. **数据结构简单**: Redis 的数据结构是专门设计的,而这些简单的数据结构的查找和操作的时间大部分复杂度都是 0(1),因此性能比较高; +3. Redis **采用单线程模型可以避免了多线程之间的竞争**,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。 +4. **多路复用和非阻塞 I/O**: Redis使用 I/O多路复用功能来监听多个 socket连接客户端,这样就可以使用一个线程连接来处理多个请求,减少线程切换带来的开销,同时也避免了 I/O 阻塞操作; + + + +#### Redis6.0之前一直采用单线程的主要原因 + +1. 使用单线程模型是 Redis 的开发和维护更简单,因为单线程模型方便开发和调试; +2. 即使使用单线程模型也并发的处理多客户端的请求,主要使用的是IO多路复用和非阻塞IO; +3. 对于Redis系统来说,主要的性能瓶颈是内存或者网络带宽而并非 CPU。 + + + +#### 为什么逐渐加入多线程特性? + +删除一个很大的数据时,因为是单线程原子命令操作,这就会导致 Redis 服务卡顿,于是在 Redis 4.0 中就新增了多线程的模块,主要是为了解决删除数据效率比较低的问题的。 + +使用惰性删除可以有效的解决性能问题, 在Redis4.0就引入了多个线程来实现数据的异步惰性删除等功能 + +```sh +unlink key +flushdb async +flushall async +``` + +**但是其处理读写请求的仍然只有一个线程**,所以仍然算是狭义上的单线程。 + +在Redis6/7中引入了I/0多线程的读写,这样就可以更加高效的处理更多的任务了,Redis只是将I/O读写变成了多线程,而命令的执行依旧是由主线程串行执行的,因此在多线程下操作 Redis不会出现线程安全的问题。 + + + +多线程开启: + +1. 设置`io-thread-do-reads`配置项为yes,表示启动多线程。 +2. 设置线程个数 `io-threads`。关于线程数的设置,官方的建议是如果为4核的CPU,建议线程数设置为2或3,如果为8核CPU建议线程数设置为6,安程数一定要小于机器核数,线程数并不是越大越好。 + + + +因此, Redis 6.0 版本之后,Redis 在启动的时候,默认情况下会**额外创建 6 个线程**(*这里的线程数不包括主线程*): + +- Redis-server : Redis的主线程,主要负责执行命令; +- bio_close_file、bio_aof_fsync、bio_lazy_free:三个后台线程,分别异步处理关闭文件任务、AOF刷盘任务、释放内存任务; +- io_thd_1、io_thd_2、io_thd_3:三个 I/O 线程,io-threads 默认是 4 ,所以会启动 3(4-1)个 I/O 多线程,用来分担 Redis 网络 I/O 的压力。 + + + +### BigKey + +**多大算BigKey?** + +通常我们说的BigKey,不是在值的Key很大,而是指的Key对应的value很大 + +![](E:\mkdocs\wnotes\docs\Redis\Redis\阿里云Redis开发规范.jpg) + + + +**大 key 会带来以下四种影响:** + +1. **客户端超时阻塞**。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。 + +2. **引发网络阻塞**。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。 + +3. **阻塞工作线程**。如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。 + +4. **内存分布不均**。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS 也会比较大。 + + + +**如何发现BigKey?** + +redis-cli --bigkey + +``` +加上 -i 参数,每隔100 条 scan指令就会休眠0.1s. ops就不会剧烈抬升,但是扫描的时间会变长 +redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.1 +``` + +最好选择在从节点上执行该命令。因为主节点上执行时,会阻塞主节点; + +如果没有从节点,那么可以选择在 Redis 实例业务压力的低峰阶段进行扫描查询,以免影响到实例的正常运行;或者可以使用 -i 参数控制扫描间隔,避免长时间扫描降低 Redis 实例的性能。 + +想查询大于10kb的所有key,--bigkeys参数就无能为力了,需要用到memory usage来计算每个键值的字节数 + +`memory usage 键` + + + +**BigKey如何删除?** + +分批次删除和异步删除 + +- String:一般用del,如果过于庞大使用unlink key 删除 + +- hash + + 使用 hscan 每次获取少量field-value,再使用 hdel 删除每个field, 最后删除field-value + +- list + + 使用 ltrim 渐进式逐步删除,直到全部删除完成 + +- set + + 使用 sscan 每次获取部分元素,在使用 srem 命令删除每个元素 + +- zset + + 使用 zscan 每次获取部分元素,在使用 zremrangebyrank 命令删除每个元素 + + + +**大批量往redis里面插入2000W测试数据key** + +Linux Bash下面执行,插入100W数据 + +1. 生成100W条redis批量设置kv的语句(key=kn,value=vn)写入到/tmp目录下的redisTest.txt文件中 + + ```sh + for((i=1;i<=100*10000;i++)); do echo "set ksi v$i" >> /tmp/redisTest.txt ;done; + ``` + +2. 通过redis提供的管道-pipe命令插入100W大批量数据 + + ```sh + cat /tmp/redisTest.txt | /opt/redis-7.0.0/src/redis-cli -h 127.0.0.1 -p 6379-a 111111 --pipe + ``` + + + +**生产上限制 keys * /flushdb/flushall等危险命令以防止误删误用?** + +通过配置设置禁用这些命令,redis.conf在SECURITY这一项中 + +``` +rename-command keys "" +rename-command flushdb "" +rename-command flushall "" +``` + + + +**不用keys *避免卡顿,那该用什么?** `scan, sscan, hscan, zscan` + +``` +scan cursor [MATCH pattern] [COUNT count] +``` + +* cursor : 游标 +* pattern:匹配的模式 +* count:指定数据集返回多少数据,默认10 + +SCAN 命令是一个基于游标的迭代器,每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。 + +SCAN的遍历顺序非常特别,它不是从第一维数组的第零位一直遍历到末尾,而是采用了高位进位加法来遍历。之所以使用这样特殊的方式进行遍历,是考虑到字典的扩容和缩容时避免槽位的遍历重复和遗漏。 + + + +**分别说说三种写回策略,在持久化 BigKey 的时候,会影响什么?** + +- Always 策略就是每次写入 AOF 文件数据后,就执行 fsync() 函数; +- Everysec 策略就会创建一个异步任务来执行 fsync() 函数; +- No 策略就是永不执行 fsync() 函数; + +**当使用 Always 策略的时候,如果写入是一个大 Key,主线程在执行 fsync() 函数的时候,阻塞的时间会比较久,因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的**。 + +当使用 Everysec 策略的时候,由于是异步执行 fsync() 函数,所以大 Key 持久化的过程(数据同步磁盘)不会影响主线程。 + +当使用 No 策略的时候,由于永不执行 fsync() 函数,所以大 Key 持久化的过程不会影响主线程 + + + +AOF 重写机制和 RDB 快照(bgsave 命令)的过程,都会分别通过 `fork()` 函数创建一个子进程来处理任务。会有两个阶段会导致阻塞父进程(主线程): + +- 创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长; +- 创建完子进程后,如果父进程修改了共享数据中的大 Key,就会发生写时复制,这期间会拷贝物理内存,由于大 Key 占用的物理内存会很大,那么在复制物理内存这一过程,就会比较耗时,所以有可能会阻塞父进程。 + + + +### 缓存双写一致性(缓存更新策略) + +#### 常见的缓存更新策略 + +- Cache Aside(旁路缓存)策略; +- Read/Write Through(读穿 / 写穿)策略; +- Write Back(写回)策略; + + + +**Cache Aside(旁路缓存)** + +Cache Aside(旁路缓存)策略是最常用的,应用程序直接与「数据库、缓存」交互,并负责对缓存的维护,该策略又可以细分为「读策略」和「写策略」。 + +写策略的步骤: + +- 先更新数据库中的数据,再删除缓存中的数据。 + +读策略的步骤: + +- 如果读取的数据命中了缓存,则直接返回数据; +- 如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户。 + + + +**Read/Write Through(读穿 / 写穿)策略** + +Read/Write Through(读穿 / 写穿)策略原则是应用程序只和缓存交互,不再和数据库交互,而是由缓存和数据库交互,相当于更新数据库的操作由缓存自己代理了。 + +Read Through 策略: + +先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库查询数据,并将结果写入到缓存组件,最后缓存组件将数据返回给应用。 + +Write Through 策略: + +当有数据更新的时候,先查询要写入的数据在缓存中是否已经存在: + +- 如果缓存中数据已经存在,则更新缓存中的数据,并且由缓存组件同步更新到数据库中,然后缓存组件告知应用程序更新完成。 +- 如果缓存中数据不存在,直接更新数据库,然后返回; + + + +**Write Back(写回)策略** + +Write Back(写回)策略在更新数据的时候,只更新缓存,同时将缓存数据设置为脏的,然后立马返回,并不会更新数据库。对于数据库的更新,会通过批量异步更新的方式进行。 + +Write Back 是计算机体系结构中的设计,比如 CPU 的缓存、操作系统中文件系统的缓存都采用了 Write Back(写回)策略。 + +**Write Back 策略特别适合写多的场景**,因为发生写操作的时候, 只需要更新缓存,就立马返回了。比如,写文件的时候,实际上是写入到文件系统的缓存就返回了,并不会写磁盘。**但是带来的问题是,数据不是强一致性的,而且会有数据丢失的风险**。 + + + +#### **数据库和缓存一致性的几种更新策略** + +##### 1. 先更新数据库,再更新缓存 + +问题: + +1. 更新数据库成功,更新缓存失败,读到redis脏数据 + +2. 多线程下 + + ``` + 【正常逻辑】 + 1 A update mysql 100 + 2 A update redis 100 + 3 B update mysql 80 + 4 B update redis 80 + ============================= + 【异常逻辑】多线程环境下,A、B两个线程有快有慢,有前有后有并行 + 1 A update mysql 100 + 3 B update mysql 80 + 4 B update redis 80 + 2 A update redis 100 + ============================= + 最终结果,mysql和redis数据不一致: mysql80,redis100 + ``` + +##### 2. 先更新缓存,再更新数据库 + +问题: + +1. 多线程下 + + ``` + 【正常逻辑】 + 1 A update redis 100 + 2 A update mysql 100 + 3 B update redis 80 + 4 B update mysql 80 + ==================================== + 【异常逻辑】多线程环境下,A、B两个线程有快有慢有并行 + A update redis 100 + B update redis 80 + B update mysql 80 + A update mysql 100 + ==================================== + ----mysql100,redis80 + ``` + +##### 3. 先删除缓存,再更新数据库 + +问题: + +1. 多线程下 + + ``` + A删除缓存后,B查询操作没有命中缓存,B先把老数据读出来后放到缓存中,然后A更新操作更新了数据库。 + 于是,在缓存中的数据还是老的数据,数据库的数据是新数据 + ``` + +解决方案: + +1. 延时双删策略: + + ``` + #删除缓存 + redis.delKey(X) + #更新数据库 + db.update(X) + #睡眠 + Thread.sleep(N) + #再删除缓存 + redis.delKey(X) + + 线程A删除并更新数据库后等待一段时间,B将数据库数据写入缓存后,A再删除。 + 等待时间大于B读取并写入时间。如何获取?评估耗时;后台监控程序(WatchDog) + 第二次删除可以使用异步删除,可以增加吞吐量 + ``` + +##### 4. 先更新数据库,再删除缓存 + +问题: + +1. 假如缓存删除失败或者来不及,导致请求再次访问redis时缓存命中,读取到的是缓存旧值。 + +2. 多线程下 + + ``` + 假如某个用户数据在缓存中不存在,请求 A 读取数据时从数据库中查询到年龄为 20,在未写入缓存中时另一个请求 B 更新数据。它更新数据库中的年龄为 21,并且清空缓存。这时请求 A 把从数据库中读到的年龄为 20 的数据写入到缓存中。 + ``` + + 但是在实际中,这个问题出现的概率并不高。因为缓存的写入通常要远远快于数据库的写入 + + + +解决方案: + +1. 重试机制:引入消息队列把删除缓存要操作的数据加入消息队列,删除缓存失败则从队列中重新读取数据再次删除,删除成功就从队列中移除 +2. 订阅MySql binlog,再操作缓存:更新数据库成功,就会产生一条变更日志,记录在 binlog 里。订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除。 + + + +优先使用**先更新数据库,再删除缓存的方案(先更库→后删存)**。理由如下: + +1. 先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力导致打满mysql。 + +2. 如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置。 + + + +#### cannal + +[alibaba/canal: 阿里巴巴 MySQL binlog 增量订阅&消费组件 (github.com)](https://github.com/alibaba/canal) + +canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送 dump 协议 + +MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal ) + +canal 解析 binary log 对象(原始为 byte 流) + + + +[QuickStart · alibaba/canal Wiki (github.com)](https://github.com/alibaba/canal/wiki/QuickStart) + +1. mysql + + 1. 查看MySql binlog是否开启: show variables like 'log_bin'; + + 2. 开启 Binlog 写入功能 :my.cnf 中配置 + + ``` + [mysqld] + log-bin=mysql-bin # 开启 binlog + binlog-format=ROW # 选择 ROW 模式 + server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复 + ``` + + * ROW模式 除了记录sql语句之外,还会记录每个字段的变化情况,能够清楚的记录每行数据的变化历史,但会占用较多的空间。 + + * STATEMENT模式只记录了sql语句,但是没有记录上下文信息,在进行数据恢复的时候可能会导致数据的丢失情况; + * MIX模式比较灵活的记录,理论上说当遇到了表结构变更的时候,就会记录为statement模式。当遇到了数据更新或者删除情况下就会变为row模式; + + 3. 重启mysql + + 4. 授权 canal 链接 MySQL 账号具有作为 MySQL slave 的权限, 如果已有账户可直接 grant + +2. cannal服务端 + + 1. 下载cannal + + 2. 修改配置,主机、账户、密码等 + + 3. 启动 + +3. cannal客户端 [ClientExample · alibaba/canal Wiki (github.com)](https://github.com/alibaba/canal/wiki/ClientExample) + + + + + +### 布隆过滤器 Bloom Filter + +布隆过滤器由「初始值都为 0 的 bit 数组」和「 N 个哈希函数」两部分组成,用来快速判断集合是否存在某个元素。 + +当我们在写入数据库数据时,在布隆过滤器里做个标记,这样下次查询数据是否在数据库时,只需要查询布隆过滤器,如果查询到数据没有被标记,说明不在数据库中。 + +布隆过滤器会通过 3 个操作完成标记: + +- 第一步,使用 N 个哈希函数分别对数据做哈希计算,得到 N 个哈希值; +- 第二步,将第一步得到的 N 个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的对应位置。 +- 第三步,将每个哈希值在位图数组的对应位置的值设置为 1; + +一个元素如果判断结果:存在--元素可能存在;不存在--元素一定不存在 + +布隆过滤器只能添加元素,不能删除元素,因为布隆过滤器的bit位可能是共享的,删掉元素会影响其他元素导致误判率增加 + + + +应用场景: + +1. 解决缓存穿透问题 +2. 黑白名单校验 + + + +为了解决布隆过滤器不能删除元素的问题,布谷鸟过滤器横空出世。https://www.cs.cmu.edu/~binfan/papers/conext14_cuckoofilter.pdf#:~:text=Cuckoo%20%EF%AC%81lters%20support%20adding%20and%20removing%20items%20dynamically,have%20lower%20space%20overhead%20than%20space-optimized%20Bloom%20%EF%AC%81lters. + + + +### 缓存预热/缓存雪崩/缓存击穿/缓存穿透 + +#### 缓存预热 + +将热点数据提前加载到redis缓存中 + +#### 缓存雪崩 + +redis**故障或**者redis中**大量的缓存数据同时失效,大量请求直接访问数据库,从而导致数据库压力骤增** + +和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。 + +解决: + +1. 大量数据同时过期: + + 1. 均匀设置过期时间或不过期:设置过期时间时可以加上一个随机数 + + 2. 互斥锁:保证同一时间只有一个请求访问数据库来构建缓存 + 3. 后台更新缓存:业务线程不再负责更新缓存,缓存也不设置有效期,而是让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新。 + +2. redis故障 + + 1. 缓存集群高可用:哨兵、集群、持久化 + + 2. 服务降级、熔断 + +#### 缓存穿透 + +**缓存和数据库中都没有数据** + +解决: + +1. 空对象缓存或缺省值:如果发生了缓存穿透,我们可以针对要查询的数据,在Redis里存一个和业务部门商量后确定的缺省值(比如,零、负数、defaultNull等)。 +2. 布隆过滤器:Google布隆过滤器Guava解决缓存穿透 [guava](https://github.com/google/guava/blob/master/guava/src/com/google/common/hash/BloomFilter.java) +3. 非法请求限制:在 API 入口处我们要判断求请求参数是否合理 +4. 增强id复杂度,避免被猜测id规律(可以采用雪花算法) +5. 做好数据的基础格式校验 +6. 加强用户权限校验 +7. 做好热点参数的限流 + +#### 缓存击穿 + +**缓存中没有但数据库中有数据** + +热点key突然失效,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。 + +解决: + +1. 差异失效时间:开辟两块缓存,设置不同的缓存过期时间,主A从B,先更新B再更新A,先查询A没有再查询B +2. 加锁策略,保证同一时间只有一个业务线程更新缓存 +3. 不给热点数据设置过期时间 +4. 后台更新缓存 +5. 接口限流、熔断与降级 + + + +### 缓存过期删除/内存淘汰策略 + +**redis默认内存多少可用?** + +如果不设置最大内存或者设置最大内存大小为0,在64位操作系统下不限制内存大小,在32位操作系统下最多使用3GB内存 + +注意:在64bit系统下,maxmemory设置为0表示不限制redis内存使用 + +一般推荐Redis设置内存为最大物理内存的3/4 + +**如何修改redis内存设置**? + +1. 通过修改文件配置 `maxmemory` + +2. 通过命令修改,但是redis重启后会失效 `config set maxmemory SIZE` + +什么命令查看redis内存使用情况 + +info memory + +config get maxmemory + + + +#### 过期删除策略 + +1. **立即/定时删除**:在设置 key 的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器自动执行 key 的删除操作。对内存友好,对CPU不友好 +2. **惰性删除**:不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。对内存不友好,对CPU友好。开启惰性删除淘汰,lazyfree-lazy-eviction=yes +3. **定期删除**:每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。超过一定比例则重复此操作。Redis 为了保证定期删除不会出现循环过度,导致线程卡死现象,为此增加了定期删除循环流程的时间上限,默认不会超过 25ms。缺点是难以确定删除操作执行的时长和频率。 + + **Redis 选择「惰性删除+定期删除」这两种策略配和使用**,以求在合理使用 CPU 时间和避免内存浪费之间取得平衡。 + + + +#### 内存淘汰策略 + +超过redis设置的最大内存,就会使用内存淘汰策略删除符合条件的key + +LRU:最近最少使用页面置换算法,淘汰**最长时间未被使用**的页面,看页面最后一次被使用到发生调度的时间长短,首先淘汰最长时间未被使用的页面。 + +LFU:最近最不常用页面置换算法,淘汰**一定时期内被访问次数最少**的页面,看一定时间段内页面被使用的频率,淘汰一定时期内被访问次数最少的页 + + + +淘汰策略有哪些(Redis7版本): + +1. noeviction:不淘汰任何key,表示即使内存达到上限也不进行置换,所有能引起内存增加的命令都会返回error +2. 对设置了过期时间的数据中进行淘汰 + 1. LRU + 2. LFU + 3. random + 4. TTL:优先淘汰更早过期的key +3. 全部数据进行淘汰 + 1. random + 2. LRU + 3. LFU + + + + +**如何修改 Redis 内存淘汰策略?** + +1. `config set maxmemory-policy <策略>` 设置之后立即生效,不需要重启 Redis 服务,重启 Redis 之后,设置就会失效。 + +2. 通过修改 Redis 配置文件修改,设置“`maxmemory-policy <策略>`”,它的优点是重启 Redis 服务后配置不会丢失,缺点是必须重启 Redis 服务,设置才能生效。 + + + +**Redis 是如何实现 LRU 算法的?** + +传统 LRU 算法的实现是基于「链表」结构,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可,因为链表尾部的元素就代表最久未被使用的元素。 + +Redis 并没有使用这样的方式实现 LRU 算法,因为传统的 LRU 算法存在两个问题: + +- 需要用链表管理所有的缓存数据,这会带来额外的空间开销; +- 当有数据被访问时,需要在链表上把该数据移动到头端,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时,进而会降低 Redis 缓存性能。 + +Redis 实现的是一种**近似 LRU 算法**,目的是为了更好的节约内存,它的**实现方式是在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间**。 + +当 Redis 进行内存淘汰时,会使用**随机采样的方式来淘汰数据**,它是随机取 N 个值,然后**淘汰最久没有使用的那个**。 + +Redis 实现的 LRU 算法的优点: + +- 不用为所有的数据维护一个大链表,节省了空间占用; +- 不用在每次数据访问时都移动链表项,提升了缓存的性能; + + + +**Redis 是如何实现 LFU 算法的?** + +LFU 算法相比于 LRU 算法的实现,多记录了「数据的访问频次」的信息。 + +```c +typedef struct redisObject { + ... + + // 24 bits,用于记录对象的访问信息 + unsigned lru:24; + ... +} robj; +``` + +Redis 对象头中的 lru 字段,在 LRU 算法下和 LFU 算法下使用方式并不相同。 + +**在 LRU 算法中**,Redis 对象头的 24 bits 的 lru 字段是用来记录 key 的访问时间戳,因此在 LRU 模式下,Redis可以根据对象头中的 lru 字段记录的值,来比较最后一次 key 的访问时间长,从而淘汰最久未被使用的 key。 + +**在 LFU 算法中**,Redis对象头的 24 bits 的 lru 字段被分成两段来存储,高 16bit 存储 ldt(Last Decrement Time),用来记录 key 的访问时间戳;低 8bit 存储 logc(Logistic Counter),用来记录 key 的访问频次。 + + + +### 分布式锁 + +基于 Redis 节点实现分布式锁时,对于加锁操作,我们需要满足三个条件。 + +- 加锁包括了读取锁变量、检查锁变量值和设置锁变量值三个操作,但需要以原子操作的方式完成,所以,我们使用 SET 命令带上 NX 选项来实现加锁; +- 锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,所以,我们在 SET 命令执行时加上 EX/PX 选项,设置其过期时间; +- 锁变量的值需要能区分来自不同客户端的加锁操作,以免在释放锁时,出现误释放操作,所以,我们使用 SET 命令设置锁变量值时,每个客户端设置的值是一个唯一值,用于标识客户端; + +满足这三个条件的分布式命令如下: + +```sh +SET lock_key unique_value NX PX 10000 +``` + +而解锁的过程就是将 lock_key 键删除(del lock_key),但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。 + +可以看到,解锁是有两个操作,这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。 + +```lua +-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示 +-- 获取锁中的标示,判断是否与当前线程标示一致 +if (redis.call('GET', KEYS[1]) == ARGV[1]) then + -- 一致,则删除锁 + return redis.call('DEL', KEYS[1]) +end +-- 不一致,则直接返回 +return 0 +``` + +基于 Redis 实现分布式锁的**优点**: + +1. 性能高效(这是选择缓存实现分布式锁最核心的出发点)。 +2. 实现方便。因为 Redis 提供了 setnx 方法,实现分布式锁很方便。 +3. 避免单点故障(因为 Redis 是跨集群部署的,自然就避免了单点故障)。 + +基于 Redis 实现分布式锁的**缺点**: + +1. 超时时间不好设置。可以基于续约的方式设置超时时间:先给锁设置一个超时时间,然后启动一个守护线程,让守护线程在一段时间后,重新设置这个锁的超时时间 + +2. Redis 主从复制模式中的数据是异步复制的,这样导致分布式锁的不可靠性。如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。 + + + +#### Redisson + +**Redisson分布式锁原理** + +可重入:利用hash结构记录线程id和重入次数 + +可重试:利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制 + +超时续约:利用watchDog,每个一段时间(releaseTime),重置超时时间。 + +![](E:/mkdocs/wnotes/docs/Redis/Redis/redisson分布式锁.png) + +1. 依赖 + + ```xml + + org.redisson + redisson + 3.13.6 + + ``` + +2. 配置类 + + ```java + @Configuration + public class RedissonConfig { + @Bean + public RedissonClient redissonClient() { + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://localhost:6379"); + return Redisson.create(config); + } + } + ``` + +3. 使用分布式锁 + + ```java + @Resource + private RedissonClient redissonClient; + + @Test + void testRedisson() throws InterruptedException { + //获取可重入锁 + RLock lock = redissonClient.getLock("anyLock"); + //尝试获取锁,三个参数分别是:获取锁的最大等待时间(期间会重试),锁的自动释放时间,时间单位 + boolean success = lock.tryLock(1,10, TimeUnit.SECONDS); + //判断获取锁成功 + if (success) { + try { + System.out.println("执行业务"); + } finally { + //释放锁 + lock.unlock(); + } + } + } + ``` + + + +**Redisson可重入锁原理** + +在分布式锁中,采用hash结构来存储锁,其中外层key表示这把锁是否存在,内层key则记录当前这把锁被哪个线程持有。 + +``` +lock -> {thread: state} #如果持有这把锁的人(同一线程下)再次持有这把锁,那么state会+1 +``` + +获取锁的逻辑: + +```lua +local key = KEYS[1]; -- 锁的key +local threadId = ARGV[1]; -- 线程唯一标识 +local releaseTime = ARGV[2]; -- 锁的自动释放时间 +-- 锁不存在 +if (redis.call('exists', key) == 0) then + -- 获取锁并添加线程标识,state设为1 + redis.call('hset', key, threadId, '1'); + -- 设置锁有效期 + redis.call('expire', key, releaseTime); + return 1; -- 返回结果 +end; +-- 锁存在,判断threadId是否为自己 +if (redis.call('hexists', key, threadId) == 1) then + -- 锁存在,重入次数 +1,这里用的是hash结构的incrby增长 + redis.call('hincrby', key, thread, 1); + -- 设置锁的有效期 + redis.call('expire', key, releaseTime); + return 1; -- 返回结果 +end; +return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败 +``` + +释放锁的逻辑: + +```lua +local key = KEYS[1]; +local threadId = ARGV[1]; +local releaseTime = ARGV[2]; +-- 如果锁不是自己的 +if (redis.call('HEXISTS', key, threadId) == 0) then + return nil; -- 直接返回 +end; +-- 锁是自己的,锁计数-1,还是用hincrby,不过自增长的值为-1 +local count = redis.call('hincrby', key, threadId, -1); +-- 判断重入次数为多少 +if (count > 0) then + -- 大于0,重置有效期 + redis.call('expire', key, releaseTime); + return nil; +else + -- 否则直接释放锁 + redis.call('del', key); + return nil; +end; +``` + +**MutiLock锁** + +Redisson提出来了MutiLock锁,使用这把锁的话,那我们就不用主从了,每个节点的地位都是一样的,都可以当做是主机,那我们就需要将加锁的逻辑写入到每一个主从节点上,只有所有的服务器都写入成功,此时才是加锁成功,假设现在某个节点挂了,那么他去获取锁的时候,只要有一个节点拿不到,都不能算是加锁成功,就保证了加锁的可靠性. + + + +#### RedLock + +它是基于**多个 Redis 节点**的分布式锁,即使有节点发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。官方推荐是至少部署 5 个 Redis 节点,而且都是主节点,它们之间没有任何关系,都是一个个孤立的节点。 + +Redlock 算法的基本思路,**是让客户端和多个独立的 Redis 节点依次请求申请加锁,如果客户端能够和半数以上的节点成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败**。 + +这样一来,即使有某个 Redis 节点发生故障,因为锁的数据在其他节点上也有保存,所以客户端仍然可以正常地进行锁操作,锁的数据也不会丢失。 + +Redlock 算法加锁三个过程: + +1. 第一步是,客户端获取当前时间(t1)。 + +2. 第二步是,客户端按顺序依次向 N 个 Redis 节点执行加锁操作: + + - 加锁操作使用 SET 命令,带上 NX,EX/PX 选项,以及带上客户端的唯一标识。 + + - 如果某个 Redis 节点发生故障了,为了保证在这种情况下,Redlock 算法能够继续运行,我们需要给「加锁操作」设置一个超时时间(不是对「锁」设置超时时间,而是对「加锁操作」设置超时时间),加锁操作的超时时间需要远远地小于锁的过期时间,一般也就是设置为几十毫秒。 + +3. 第三步是,一旦客户端从超过半数(大于等于 N/2+1)的 Redis 节点上成功获取到了锁,就再次获取当前时间(t2),然后计算计算整个加锁过程的总耗时(t2-t1)。如果 t2-t1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败。 + +客户端只有在满足下面的这**两个条件**时,才能认为是加锁成功: + +1. 客户端从超过半数(大于等于N/2+1)的Redis节点上成功获取到了锁; + +2. 客户端获取锁的总耗时没有超过锁的过期时间。 + + + +### 过期键 + +#### Redis 持久化时,对过期键会如何处理的? + +Redis 持久化文件有两种格式:RDB(Redis Database)和 AOF(Append Only File)。 + +RDB: + +- **RDB 文件生成阶段**:从内存状态持久化成 RDB文件的时候,会对 key 进行过期检查,过期的键不会被保存到新的 RDB 文件中,因此 Redis 中的过期键不会对生成新 RDB 文件产生任何影响。 + +- **RDB 文件加载阶段**: + - **主服务器模式运行**:在载入 RDB 文件时,程序会对文件中保存的键进行检查,过期键不会被载入到数据库中。所以过期键不会对载入 RDB 文件的主服务器造成影响; + - **从服务器模式运行**:在载入 RDB 文件时,不论键是否过期都会被载入到数据库中。但由于主从服务器在进行数据同步时,从服务器的数据会被清空。所以一般来说,过期键对载入 RDB 文件的从服务器也不会造成影响。 + +AOF: + +- **AOF 文件写入阶段**:当 Redis 以 AOF 模式持久化时,如果数据库某个过期键还没被删除,那么 AOF 文件会保留此过期键,当此过期键被删除后,Redis 会向 AOF 文件追加一条 DEL 命令来显式地删除该键值。 +- **AOF 重写阶段**:执行 AOF 重写时,会对 Redis 中的键值对进行检查,已过期的键不会被保存到重写后的 AOF 文件中,因此不会对 AOF 重写造成任何影响。 + + + +#### Redis 主从模式中,对过期键会如何处理? + +服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制: + +* 主服务器删除一个过期键后,会向从服务器发送DEL命令告知删除过期键。 +* 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会删除,而是像处理未过期键一样处理过期键。 +* 从服务器只有接收到主服务器的DEL命令后,才会删除过期键。 \ No newline at end of file diff --git "a/docs/Redis/\346\225\260\346\215\256\347\273\223\346\236\204.md" "b/docs/Redis/\346\225\260\346\215\256\347\273\223\346\236\204.md" new file mode 100644 index 0000000..89abeda --- /dev/null +++ "b/docs/Redis/\346\225\260\346\215\256\347\273\223\346\236\204.md" @@ -0,0 +1,390 @@ +## 数据结构 + +### SDS + +字符串在 Redis 中是很常用的,键值对中的键是字符串类型,值有时也是字符串类型。 + +Redis 是用 C 语言实现的,但是它没有直接使用 C 语言的 char* 字符数组来实现字符串,而是自己封装了一个名为简单动态字符串(simple dynamic string,SDS) 的数据结构来表示字符串。 + +char* 缺陷: + +1. 获取字符串长度的时间复杂度为 O(N); + +2. 字符串的结尾是以 `'\0'` 字符标识,字符串里面不能包含有`'\0'` 字符,因此不能保存二进制数据; + +3. 字符串操作函数不高效且不安全,比如有缓冲区溢出的风险,有可能会造成程序运行终止; + +![](E:/mkdocs/wnotes/docs/Redis/Redis/SDS.png) + +SDS改进: + +1. Redis 的 SDS 结构因为加入了 len 成员变量,获取字符串长度的时间复杂度为O(1)。 + +2. 二进制安全。SDS 不需要用`'\0'`字符来标识字符串结尾,可以保存包含`'\0'` 的数据,但是SDS为了兼容结尾仍然加上 `'\0'` 空字符。SDS 的所有 API 都会以处理二进制的方式来处理 SDS 存放在 `buf[]` 数组里的数据,不会做任何限制。 + +3. 不会发生缓冲区溢出、减少内存分配次数。字符串操作时可以判断缓冲区大小是否足够,当缓冲区大小不够用时,Redis 会自动将扩大 SDS 的空间大小。进行空间扩展时,不仅分配所需的必要的空间,还会分配额外的未使用的空间,有效的减少内存分配次数。 + + * 如果所需的 sds 长度小于 1 MB, 两倍扩容 + + * 如果所需的 sds 长度大于等于 1 MB,增加1MB空间 + +4. 节省内存空间。设计了 5 种sds类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。能灵活保存不同大小的字符串,从而有效节省内存空间。取消结构体在编译过程中的优化对齐,按照实际占用字节数进行对齐 + + + +| C字符串 | SDS | +| ---------------------------------- | ---------------------------------- | +| 获取字符串长度的复杂度O(n) | 获取字符串长度复杂度为O(1) | +| API是不安全的,可能造成缓冲区溢出 | API是安全的,不会造成缓冲区溢出 | +| 修改字符串N次必然执行N次内存重分配 | 修改字符串N次最多执行N次内存重分配 | +| 只能保存文本数据 | 可以保存文本或二进制数据 | +| 可以使用所有 库中的函数 | 可以使用部分 库中的函数 | + + + +### 链表 + +![](E:/mkdocs/wnotes/docs/Redis/Redis/链表.png) + +list 结构为链表提供了链表头指针 head、链表尾节点 tail、链表节点数量 len、以及可以自定义实现的 dup(节点值复制)、free(节点值释放)、match(节点值比较) 函数。 + + + +### 压缩列表 + +压缩列表的最大特点,就是它被设计成一种内存紧凑型的数据结构,占用一块连续的内存空间,不仅可以利用 CPU 缓存,而且会针对不同长度的数据,进行相应编码,这种方法可以有效地节省内存开销。 + +但是,压缩列表的缺陷也是有的: + +- 不能保存过多的元素,否则查询效率就会降低; +- 会记录前一个结点的大小,新增或修改某个元素时,压缩列表占用的内存空间需要重新分配,甚至可能引发连锁更新的问题。 + +Redis 对象(List 对象、Hash 对象、Zset 对象)包含的元素数量较少,或者元素值不大的情况才会使用压缩列表作为底层数据结构。 + + + +![](E:/mkdocs/wnotes/docs/Redis/Redis/压缩列表.png) + +* ***zlbytes***:记录整个压缩列表占用对内存字节数; +* ***zltail***:记录压缩列表「尾部」节点距离起始地址有多少字节,也就是列表尾的偏移量; +* ***zllen***:记录压缩列表包含的节点数量; +* ***zlend***:标记压缩列表的结束点,固定值 0xFF(十进制255)。 +* ***entryX***:可以保存一个字节数组或者一个整数值。 + * ***prevlen***,记录了「前一个节点」的长度,可以结合当前节点的起始地址计算前一个节点的起始地址; + * 如果前一个节点的长度小于 254 字节,那么 prevlen 属性需要用 1 字节的空间来保存这个长度值; + * 如果前一个节点的长度大于等于 254 字节,那么 prevlen 属性需要用 5 字节的空间来保存这个长度值;其中第一个字节设置为254,后面4个字节用来存储长度值。 + * ***encoding***,记录了当前节点实际数据的「类型和长度」,类型主要有两种:字节数组和整数。 + * ***data***,记录了当前节点的实际数据,类型和长度都由 `encoding` 决定; + + + +**连锁更新问题**: + +由于 prevlen 保存前一个节点的长度,压缩列表新增、修改或删除某个元素时,可能导致后续元素的 prevlen 占用空间都发生变化,引起「连锁更新」问题。 + +连锁更新在最坏情况下需要对压缩列表执行N次空间重分配操作,而每次空间重分配的最坏情况复杂度为O(n),所以连锁更新的最坏情况复杂度为O(n^2)。 + +1. 假设一个压缩列表中有多个连续的、长度在 250~253 之间的节点,因为这些节点长度值小于 254 字节,所以 prevlen 属性需要用 1 字节的空间来保存这个长度值。 +2. 这时,如果将一个长度大于等于 254 字节的新节点加入到压缩列表的表头节点,即新节点将成为 e1 的前置节点。 +3. 因为 e1 节点的 prevlen 属性只有 1 个字节大小,无法保存新节点的长度,此时就需要对压缩列表的空间重分配操作,并将 e1 节点的 prevlen 属性从原来的 1 字节大小扩展为 5 字节大小。 +4. 导致后面节点的 prevlen 属性也必须从 1 字节扩展至 5 字节大小。 + + + +尽管连锁更新的复杂度较高,但真正造成性能问题的几率是很低的: + +1. 压缩列表里恰好有多个连续的、长度介于250-253字节之间的节点; +2. 即使出现连锁更新,但只要被更新的节点数量不多,就不会造成性能影响。 + + + +### 哈希表 + +![](E:/mkdocs/wnotes/docs/Redis/Redis/哈希表.png) + +哈希表是一个数组(dictEntry **table),数组的每个元素是一个指向「哈希表节点(dictEntry)」的指针。使用头插法。 + +```c +typedef struct dictEntry { + //键值对中的键 + void *key; + + //键值对中的值 - 联合体节省内存空间 + union { + void *val; // 指针 + uint64_t u64; // 无符号的 64 位整数 + int64_t s64; // 有符号的 64 位整数 + double d; // double值 + } v; + // 拉链法解决hash冲突 + struct dictEntry *next; +} dictEntry; +``` + + + +**rehash**: + +当哈希表保存的键值对数量太多或太少时,需要对哈希表大小进行相应的扩展或收缩,通过执行rehash(重新散列)完成。 + +![](E:/mkdocs/wnotes/docs/Redis/Redis/rehash.png) + +在实际使用哈希表时,Redis 定义一个 dict 结构体,这个结构体里定义了两个哈希表(ht[2])。一般情况下,只会使用ht[0]哈希表,ht[1]只会在对ht[0]进行rehash时使用。 + +rehash步骤: + +1. 为 ht[1] 分配空间; +2. 将保存在 ht[0] 中的所有键值对 rehash(重新计算键的哈希值和索引值) 到 ht[1] 上; +3. 迁移完成后,释放 ht[0],将 ht[1] 设置为 ht[0],并在 ht[1] 新创建一个空白哈希表,为下次 rehash 做准备。 + + + +**渐进式 rehash**: + +为了避免 rehash 在数据迁移过程中,因拷贝数据的耗时,影响 Redis 性能的情况,所以 Redis 采用了**渐进式 rehash**,也就是将数据的迁移的工作不再是一次性迁移完成,而是分多次迁移。 + +渐进式 rehash 步骤如下: + +1. 为 ht[1] 分配空间,哈希表同时持有 ht[0] 和 ht[1] 两个哈希表; + +2. 在 rehash 进行期间,每次哈希表元素进行新增、删除、查找或者更新操作时,Redis 除了会执行对应的操作之外,还会顺带将 ht[0] 中索引位置上的所有 key-value 迁移到 ht[1] 上; + +3. 随着处理客户端发起的哈希表操作请求数量越多,最终在某个时间点会把 ht[0] 的所有 key-value 迁移到 ht[1],从而完成 rehash 操作。 + +在渐进式 rehash 进行期间,哈希表元素的删除、查找、更新等操作都会在这两个哈希表进行。比如,查找一个 key 的值的话,先会在 ht[0] 里面进行查找,如果没找到,就会继续到 ht[1] 里面进行找到。新增一个 key-value 时,会被保存到 ht[1] 里面,而 ht[0] 则不再进行任何添加操作。 + + + +**rehash 触发条件**: + +触发 rehash 操作的条件,主要有两个: + +- 当负载因子大于等于 1 ,并且 Redis 没有在执行 bgsave 命令或者 bgrewiteaof 命令,也就是没有执行 RDB 快照或没有进行 AOF 重写的时候,就会进行 rehash 操作。 +- 当负载因子大于等于 5 时,此时说明哈希冲突非常严重了,不管有没有有在执行 RDB 快照或 AOF 重写,都会强制进行 rehash 操作。 + + + +### 整数集合 + +整数集合是 Set 对象的底层实现之一。当一个 Set 对象只包含整数值元素,并且元素数量不大时,就会使用整数集这个数据结构作为底层实现。 + +整数集合本质上是一块连续内存空间。 + +```c +typedef struct intset { + //编码方式 + uint32_t encoding; + //集合包含的元素数量 + uint32_t length; + //保存元素的数组 + int8_t contents[]; +} intset; +``` + +虽然 contents 被声明为 int8_t 类型的数组,但是实际上 contents 数组并不保存任何 int8_t 类型的元素,contents 数组的真正类型取决于 intset 结构体里的 encoding 属性的值。比如int16_t, int32_t, int64_t. + + + +**升级**: + +当我们将一个新元素加入到整数集合里面,如果新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级,才能将新元素加入到整数集合里。整数集合不支持降级。 + +1. 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间; +2. 将底层数组现有的所有元素都转换为与新元素相同的类型,并将转换后的元素放置到正确的位置; +3. 将新元素添加到底层数组中。 + +整数集合升级的过程不会重新分配一个新类型的数组,而是在原本的数组上扩展空间,然后在将每个元素按间隔类型大小分割,如果 encoding 属性值为 INTSET_ENC_INT16,则每个元素的间隔就是 16 位。 + +![](E:/mkdocs/wnotes/docs/Redis/Redis/升级操作.png) + +因为每次向整数集合中添加元素都有可能引起升级,而每次升级都需要对底层数组中已有的所有元素进行类型转换,所以向整数集合中添加元素的时间复杂度为O(n)。 + +升级的好处: + +1. 提升灵活性。整数集合可以通过自动升级底层数组来适应新元素,所以可以随意将int16_t, int32_t, int64_t类型整数添加到集合中,不必担心出现类型错误。 +2. 节约内存。升级只会在有需要的时候进行,可以尽量节省内存 + + + +### 跳表 + +跳表支持平均 O(logN) 复杂度的节点查找,大部分情况下,跳表的效率可以和平衡树相媲美,并且跳表的实现比平衡树更简单。 + +Redis中两个地方用到了跳表:有序集合(ZSet)、集群节点中用作内部数据结构。 + + + +跳表是在链表基础上改进过来的,实现了一种「多层」的有序链表,这样的好处是能快速定位数据。 + +![](E:/mkdocs/wnotes/docs/Redis/Redis/跳表.jpeg) + +```c +typedef struct zskiplistNode { + // Zset 对象的元素值 + sds ele; + // 分值:节点按分值排序 + double score; + // 后退指针:指向前一个节点,目的是为了方便从跳表的尾节点开始访问节点,这样倒序查找时很方便。 + struct zskiplistNode *backward; + + // 节点的level数组,保存每层上的前进指针和跨度 + struct zskiplistLevel { + // 前进指针:指向下一个跳表节点的指针 + struct zskiplistNode *forward; + // 跨度:记录了前进指针所指向的节点和当前节点的距离。跨度是用来排位的:在查找节点的过程中,将沿途访问过的所有层的跨度累计起来,得到的结果就是目标节点在跳表中的排位。 + unsigned long span; + } level[]; +} zskiplistNode; +``` + +```c +typedef struct zskiplist { + struct zskiplistNode *header, *tail; + unsigned long length; + int level; +} zskiplist; +``` + + + +在跳表中查找,就最高层开始,水平地逐个比较直至当前节点的下一个节点大于等于目标节点,然后移动至下一层。重复这个过程直至到达第一层且无法继续进行操作。此时,若下一个节点是目标节点,则成功查找;反之,则元素不存在。这样一来,查找的过程中会跳过一些没有必要的比较,所以相比于有序链表的查询,跳表的查询更快。 + + + +### quicklist + +quicklist 就是「双向链表 + 压缩列表」组合,因为一个 quicklist 就是一个链表,而链表中的每个元素又是一个压缩列表。 + +quicklist 通过控制每个链表节点中的压缩列表的大小或者元素个数,来规避连锁更新的问题。因为压缩列表元素越少或越小,连锁更新带来的影响就越小,从而提供了更好的访问性能。 + + + +![](E:/mkdocs/wnotes/docs/Redis/Redis/quicklist.png) + +在向 quicklist 添加一个元素的时候,不会像普通的链表那样,直接新建一个链表节点。而是会检查插入位置的压缩列表是否能容纳该元素,如果能容纳就直接保存到 quicklistNode 结构里的压缩列表,如果不能容纳,才会新建一个新的 quicklistNode 结构。 + +quicklist 会控制 quicklistNode 结构里的压缩列表的大小或者元素个数,来规避潜在的连锁更新的风险,但是这并没有完全解决连锁更新的问题。 + + + +### listpack + +Redis 在 5.0 新设计一个数据结构叫 listpack,目的是替代压缩列表,它最大特点是 listpack 中每个节点不再包含前一个节点的长度了,压缩列表每个节点正因为需要保存前一个节点的长度字段,就会有连锁更新的隐患。 + + + +* encoding,定义该元素的编码类型,会对不同长度的整数和字符串进行编码; +* data,实际存放的数据; +* len,encoding+data的总长度; + +listpack 没有压缩列表中记录前一个节点长度的字段了,listpack 只记录当前节点的长度,当我们向 listpack 加入一个新元素的时候,不会影响其他节点的长度字段的变化,从而避免了压缩列表的连锁更新问题。 + + + +## 对象类型和编码 + +redis中每个对象都由一个redisObject结构表示: + +```c +typedef struct redisObject { + // 数据类型 + unsigned type:4; + // 编码和底层实现 + unsigned encoding:4; + // 指向底层实现数据结构的指针 + void *ptr; + // ... +} robj; +``` + +type属性记录了对象的数据类型,包括:String(字符串),Hash(哈希),List(列表),Set(集合),Zset(有序集合), BitMap(2.2 版新增),HyperLogLog(2.8 版新增),GEO(3.2 版新增),Stream(5.0 版新增)。对于Redis数据库保存的键值对来说,键总是一个字符串对象,而值可以是以上数据类型的一种。 + +encoding属性记录了对象使用的编码,即对象使用的底层数据结构。 + +![](E:/mkdocs/wnotes/docs/Redis/Redis/数据结构.png) + + + +### string + +字符串对象的编码可以是int, raw, embstr。 + +如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在ptr里面(将void * 转换为long),并将字符串对象编码设置为int。 + +如果字符串对象保存的是一个字符串值,并且这个字符申的长度小于等于 32 字节(redis 2.+版本),那么字符串对象将使用一个SDS保存这个字符串值,并将对象编码设置为embstr。 + +![](./Redis/embstr.png) + +如果字符串对象保存的是一个字符串值,并且这个字符申的长度大于 32 字节(redis 2.+版本),那么字符串对象将使用一个SDS保存这个字符串值,并将对象编码设置为raw。 + +![](./Redis/raw.png) + +embstr 编码和 raw 编码的边界在 redis 不同版本中是不一样的: + +- redis 2.+ 是 32 字节 +- redis 3.0-4.0 是 39 字节 +- redis 5.0 是 44 字节 + +`embstr`和`raw`编码都会使用`SDS`来保存值,但不同之处在于`embstr`会通过一次内存分配函数来分配一块连续的内存空间来保存`redisObject`和`SDS`,而`raw`编码会通过调用两次内存分配函数来分别分配两块空间来保存`redisObject`和`SDS`。 + + + +### list + +列表对象的编码可以是 ziplist 和 linkedlist。 + +同时满足以下两个条件时,列表对象使用 ziplist 编码: + +* 列表每个元素的值都小于 `64` 字节(默认值,可由 `list-max-ziplist-value` 配置) + +- 如果列表的元素个数小于 `512` 个(默认值,可由 `list-max-ziplist-entries` 配置) + +如果列表的元素不满足上面的条件,Redis 会使用双向链表作为 List 类型的底层数据结构。但是在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表。 + + + +### hash + +哈希对象的编码可以时ziplist 和 hashtable。 + +同时满足以下两个条件时,哈希对象使用 ziplist 编码: + +* 所有键值对的键和值的字符串长度都小于 `64` 字节(默认值,可由 `list-max-ziplist-value` 配置) + +- 保存的键值对数量小于 `512` 个(默认值,可由 `list-max-ziplist-entries` 配置) + +不能满足这两个条件的哈希对象需要使用 hashtable 编码。在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。 + + + +### set + +集合对象的编码可以是 intset 和 hashtable。 + +同时满足以下两个条件时,集合对象使用 intset 编码: + +* 集合对象保存的所有元素都是整数值 + +- 集合对象保存的元素个数不超过512个。(默认值,`set-maxintset-entries`配置) + +不能满足这两个条件的哈希对象需要使用 hashtable 编码。 + + + +### zset + +有序集合对象的编码可以是 ziplist 和 skiplist。 + +同时满足以下两个条件时,对象使用 ziplist 编码: + +* 保存的所有元素成员的长度都小于 64 字节; + +- 有序集合的元素个数小于 `128` 个。 + +如果有序集合的元素不满足上面的条件,会使用 skiplist 编码。在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。 + + + +zset结构中的dict字典为有序集合创建了一个从成员到分值的映射。通过这个字典可以O(1) 复杂度查找给定成员的分值。虽然同时使用 skiplist 和 字典保存有序集合,但这个两种数据结构都会通过指针共享相同元素的成员和分值,不会浪费额外内存。 + +![](./Redis/有序集合.jpeg) \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 9894f1b..2032f59 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -18,6 +18,10 @@ nav: - Java集合: Java/Java集合.md - JUC: JUC/JUC.md - JVM: JVM/JVM.md + - Redis: + - Redis基础: Redis/Redis基础.md + - Redis数据结构: Redis/数据结构.md + - Redis高级: Redis/Redis高级.md - 设计模式: 设计模式/设计模式.md # Theme