# yang-Comments
**Repository Path**: Yang2162325525/yang-comments
## Basic Information
- **Project Name**: yang-Comments
- **Description**: 这个黑马点评项目的扩展版,里面对原项目各个地方进行了更加完善的扩展,比如点赞功能,秒杀功能,还有引入消息队列并且保证可靠性等。同时提供了项目的简历写法以及本人面试遇见过的八股文。如果对你有帮助的话,麻烦给一个stat~吧,这将是作者维护下去的动力!!!
- **Primary Language**: Java
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 77
- **Forks**: 8
- **Created**: 2023-05-10
- **Last Updated**: 2026-04-22
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 八股面经
## java基础
### 基础知识
**1.面向对象思想**:
封装:封装就是将数据和方法组合在一起,形成一个整体,这样的好处就是防止被类以外的程序破坏,常见的有封装工具类
继承:继承主要实现重用代码,节省开发时间,对于共性的逻辑避免重复开发
多态:指不同对象接收到同一方法时会产生不同的行为(一个接口,多种方法),通过重写可以让我们的代码变得更灵活
**2.反射**:
**反射的基本原理**
在编译过程中,编译器会将类的信息保存为一种称为元数据的数据结构。元数据包含类的名称、字段、方法、访问修饰符等信息。在实际使用反射时,程序可以通过反射API获取类的元数据,获取类的构造函数、字段和方法等信息,然后使用这些信息动态创建对象、调用方法、获取和设置字段的值。
**反射的使用场景**
反射可以在运行的状态中获取类的信息
- 定时器管理,就是运用了反射,自己输入bean或者是全类名和方法的字符串,通过反射来进行方法调用
- 还有就是参数校验工具,也是通过反射拿到对象和类,判断对象中的属性是否为空
**3.接口和抽象类**:
**相同点:**
接口和抽象类都是不能实例化的,都只能通过子类继承或者实现才能被实例化
**不同点:**
**接口**:主要讲究的是特定功能的实现,比如飞行能力,不只是鸟能飞行,飞机也能飞行。强调能力,谁实现了这个接口,就能用于这个能力
**抽象类**:我觉得是强调关系的,比如鸟就是一个抽象类,能飞的鸟和不能飞的鸟都可以继承,飞机却不能继承。这样我们就能差异化,能飞的鸟去重写,不能飞的鸟就不重写
### 集合
**1.单列集合**:**ArrayList,LinkedList,Set**
ArrayList原理:基于数组实现,内存连续,懒加载,非并发安全,初始容量为10,后续扩容每次为1.5倍
LinkedList原理:基于双向链表,无序连续内存,随机访问慢,要遍历整个链表,占用内存多
Set原理:基于hashMap的key来实现的
**2.双列集合**:**HashMap**
1.**基本数据结构**
* 1.7 数组 + 链表
* 1.8 数组 + (链表 | 红黑树)
2.**树化与退化**
**树化意义**
* 红黑树用来避免 DoS 攻击,防止链表超长时性能下降,树化应当是偶然情况,是保底策略
* hash 表的查找,更新的时间复杂度是 O(1),而红黑树的查找,更新的时间复杂度是 O(log_2n ),TreeNode 占用空间也比普通 Node 的大,如非必要,尽量还是使用链表
* hash 值如果足够随机,则在 hash 表内按泊松分布,在负载因子 0.75 的情况下,长度超过 8 的链表出现概率是 0.00000006,树化阈值选择 8 就是为了让树化几率足够小
**树化规则**
* 当链表长度超过树化阈值 8 时,先尝试扩容来减少链表长度,如果数组容量已经 >=64,才会进行树化
**退化规则**
* 情况1:在扩容时如果拆分树时,树元素个数 <= 6 则会退化链表
* 情况2:remove 树节点时,若 root、root.left、root.right、root.left.left 有一个为 null ,也会退化为链表
3.**索引计算**
**索引计算方法**
* 首先,计算对象的 hashCode()
* 再进行调用 HashMap 的 hash() 方法进行二次哈希
* 二次 hash() 是为了综合高位数据,让哈希分布更为均匀
* 最后 & (capacity – 1) 得到索引
**数组容量为何是 2 的 n 次幂**
1. 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
2. 扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap
**注意**
* 二次 hash 是为了配合 **容量是 2 的 n 次幂** 这一设计前提,如果 hash 表的容量不是 2 的 n 次幂,则不必二次 hash
* **容量是 2 的 n 次幂** 这一设计计算索引效率更好,但 hash 的分散性就不好,需要二次 hash 来作为补偿,没有采用这一设计的典型例子是 Hashtable
**4.put 与扩容**
**put 流程**
1. HashMap 是懒惰创建数组的,首次使用才创建数组
2. 计算索引(桶下标)
3. 如果桶下标还没人占用,创建 Node 占位返回
4. 如果桶下标已经有人占用
1. 已经是 TreeNode 走红黑树的添加或更新逻辑
2. 是普通 Node,走链表的添加或更新逻辑,如果链表长度超过树化阈值,走树化逻辑
5. 返回前检查容量是否超过阈值,一旦超过进行扩容
**1.7 与 1.8 的区别**
1. 链表插入节点时,1.7 是头插法,1.8 是尾插法
2. 1.7 是大于等于阈值且没有空位时才扩容,而 1.8 是大于阈值就扩容
3. 1.8 在扩容计算 Node 索引时,会优化
**扩容(加载)因子为何默认是 0.75f**
1. 在空间占用与查询时间之间取得较好的权衡
2. 大于这个值,空间节省了,但链表就会比较长影响性能
3. 小于这个值,冲突减少了,但扩容就会更频繁,空间占用也更多
**5.key 的设计要求**
1. HashMap 的 key 可以为 null,但 Map 的其他实现则不然
2. 作为 key 的对象,必须实现 hashCode 和 equals,并且 key 的内容不能修改(不可变)
3. key 的 hashCode 应该有良好的散列性
**6.并发问题**
1.hashMap并不是一个并发安全的集合,在并发的情况会出现并发丢失数据的情况
**3.并发安全集合**:HashTable,ConcurrentHashMap
**Hashtable 对比 ConcurrentHashMap**
* Hashtable 与 ConcurrentHashMap 都是线程安全的 Map 集合
* Hashtable 并发度低,整个 Hashtable 对应一把锁,同一时刻,只能有一个线程操作它
* ConcurrentHashMap 并发度高,整个 ConcurrentHashMap 对应多把锁,只要线程访问的是不同锁,那么不会冲突
**ConcurrentHashMap 1.8**
* 数据结构:`Node 数组 + 链表或红黑树`,数组的每个头节点作为锁,如果多个线程访问的头节点不同,则不会冲突。首次生成头节点时如果发生竞争,利用 cas 而非 syncronized,进一步提升性能
* 并发度:Node 数组有多大,并发度就有多大,与 1.7 不同,Node 数组可以扩容
* 扩容条件:Node 数组满 3/4 时就会扩容
* 扩容单位:以链表为单位从后向前迁移链表,迁移完成的将旧数组头节点替换为 ForwardingNode
* 扩容时并发 get
* 根据是否为 ForwardingNode 来决定是在新数组查找还是在旧数组查找,不会阻塞
* 如果链表长度超过 1,则需要对节点进行复制(创建新节点),怕的是节点迁移后 next 指针改变
* 如果链表最后几个元素扩容后索引不变,则节点无需复制
* 扩容时并发 put
* 如果 put 的线程与扩容线程操作的链表是同一个,put 线程会阻塞
* 如果 put 的线程操作的链表还未迁移完成,即头节点不是 ForwardingNode,则可以并发执行
* 如果 put 的线程操作的链表已经迁移完成,即头结点是 ForwardingNode,则可以协助扩容
* 与 1.7 相比是懒惰初始化
* capacity 代表预估的元素个数,capacity / factory 来计算出初始数组大小,需要贴近 $2^n$
* loadFactor 只在计算初始数组大小时被使用,之后扩容固定为 3/4
* 超过树化阈值时的扩容问题,如果容量已经是 64,直接树化,否则在原来容量基础上做 3 轮扩容
## JVM
### JVM的内存模型
- 程序计数器:线程私有,用于记录线程读取代码的位置,在线程恢复时,能够在正确的位置继续执行
- 方法区:与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括**类的名称、方法信息、字段信息**、**静态变量、常量**)以及编译器编译后的代码等。
在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。
在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,
对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。
- 堆:Java中的堆是用来存储对象本身的以及数组(数组引用是存放在Java栈中的)。堆是被所有线程共享的,在JVM中只有一个堆。
- 栈
- **Java栈**:存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括**局部变量表**,**操作数栈**,**指向运行时常量池的引用**,**方法返回地址**
- **本地方法栈:**本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的
**会发生内存溢出的区域**
* 不会出现内存溢出的区域 – 程序计数器
* 出现 OutOfMemoryError 的情况
* 堆内存耗尽 – 对象越来越多,又一直在使用,不能被垃圾回收
* 方法区内存耗尽 – 加载的类越来越多,很多框架都会在运行期间动态产生新的类
* 虚拟机栈累积 – 每个线程最多会占用 1 M 内存,线程个数越来越多,而又长时间运行不销毁时
* 出现 StackOverflowError 的区域
* JVM 虚拟机栈,原因有方法递归调用未正确结束、反序列化 json 时循环引用
**方法区、永久代、元空间**
* **方法区**是 JVM 规范中定义的一块内存区域,用来存储类元数据、方法字节码、即时编译器需要的信息等
* **永久代**是 Hotspot 虚拟机对 JVM 规范的实现(1.8 之前)
* **元空间**是 Hotspot 虚拟机对 JVM 规范的另一种实现(1.8 以后),使用本地内存作为这些信息的存储空间
* 堆内存中:当一个**类加载器对象**,这个类加载器对象加载的所有**类对象**,这些类对象对应的所有**实例对象**都没人引用时,GC 时就会对它们占用的对内存进行释放
* 元空间中:内存释放**以类加载器为单位**,当堆中类加载器内存释放时,对应的元空间中的类元信息也会释放
### 垃圾回收
**三种垃圾回收算法**
标记清除法

解释:
1. 找到 GC Root 对象,即那些一定不会被回收的对象,如正执行方法内局部变量引用的对象、静态变量引用的对象
2. 标记阶段:沿着 GC Root 对象的引用链找,直接或间接引用到的对象加上标记
3. 清除阶段:释放未加标记的对象占用的内存
要点:
* 标记速度与存活对象线性关系
* 清除速度与内存大小线性关系
* 缺点是会产生内存碎片
标记整理法

解释:
1. 前面的标记阶段、清理阶段与标记清除法类似
2. 多了一步整理的动作,将存活对象向一端移动,可以避免内存碎片产生
特点:
* 标记速度与存活对象线性关系
* 清除与整理速度与内存大小成线性关系
* 缺点是性能上较慢
标记复制法

解释:
1. 将整个内存分成两个大小相等的区域,from 和 to,其中 to 总是处于空闲,from 存储新创建的对象
2. 标记阶段与前面的算法类似
3. 在找出存活对象后,会将它们从 from 复制到 to 区域,复制的过程中自然完成了碎片整理
4. 复制完成后,交换 from 和 to 的位置即可
特点:
* 标记与复制速度与存活对象成线性关系
* 缺点是会占用成倍的空间
**GC 与分代回收算法**
GC 的目的在于实现无用对象内存自动释放,减少内存碎片、加快分配速度
GC 要点:
* 回收区域是**堆内存**,不包括虚拟机栈
* 判断无用对象,使用**可达性分析算法**,**三色标记法**标记存活对象,回收未标记对象
* GC 具体的实现称为**垃圾回收器**
* GC 大都采用了**分代回收思想**
* 理论依据是大部分对象朝生夕灭,用完立刻就可以回收,另有少部分对象会长时间存活,每次很难回收
* 根据这两类对象的特性将回收区域分为**新生代**和**老年代**,新生代采用标记复制法、老年代一般采用标记整理法
* 根据 GC 的规模可以分成 **Minor GC**,**Mixed GC**,**Full GC**
**GC 规模**
* Minor GC 发生在新生代的垃圾回收,暂停时间短
* Mixed GC 新生代 + 老年代部分区域的垃圾回收,G1 收集器特有
* Full GC 新生代 + 老年代完整垃圾回收,暂停时间长,**应尽力避免**
**垃圾回收器 - Parallel GC**
* eden 内存不足发生 Minor GC,采用标记复制算法,需要暂停用户线程
* old 内存不足发生 Full GC,采用标记整理算法,需要暂停用户线程
* **注重吞吐量**
**垃圾回收器 - ConcurrentMarkSweep GC**
* 它是工作在 old 老年代,支持**并发标记**的一款回收器,采用**并发清除**算法
* 初始标记:标记根对象,作用时间短,需要暂停用户线程
* 并发标记:通过引用链标记对象,作用时间长,不需要暂停用户线程
* 重新标记:用于标记,在并发标记过程中的新对象,作用时间短,需要暂停用户线程
* 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC
* **注重响应时间**
**垃圾回收器 - G1 GC**
* **响应时间与吞吐量兼顾**
* 划分成多个区域,每个区域都可以充当 eden,survivor,old, humongous,其中 humongous 专为大对象准备
* 分成三个阶段:
* 新生代回收:使用标记复制算法复制存活对象到幸存区,需要暂停用户线程,释放内存
* 并发标记:通过引用链标记对象,作用时间长,不需要暂停用户线程
* 混合收集:选择新生代区域和老年代回收价值高的地方进行收集回收
* 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC
**可达性分析算法**
**原理:**通过一系列被称为「GC Roots」的根对象作为起始节点集,从这些节点开始,通过引用关系向下搜寻,搜寻走过的路径称为「引用链」,如果某个对象到GC Roots没有任何引用链相连,就说明该对象不可达,即可以被回收。
**GC Roots**:
1. 方法区静态属性引用的对象
2. 方法区常量池引用的对象
3. 方法栈中栈帧本地变量表引用的对象
4. JNI本地方法栈中引用的对象
5. 被同步锁持有的对象
**四种引用**
**强引用**
1. 普通变量赋值即为强引用,如 A a = new A();
2. 通过 GC Root 的引用链,如果强引用不到该对象,该对象才能被回收
**软引用(SoftReference)**
1. 例如:SoftReference a = new SoftReference(new A());
2. 如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存仍不足,再次回收时才会释放对象
3. 软引用自身需要配合引用队列来释放
4. 典型例子是反射数据
**弱引用(WeakReference)**
1. 例如:WeakReference a = new WeakReference(new A());
2. 如果仅有弱引用引用该对象时,只要发生垃圾回收,就会释放该对象
3. 弱引用自身需要配合引用队列来释放
4. 典型例子是 ThreadLocalMap 中的 Entry 对象
**虚引用(PhantomReference)**
1. 例如: PhantomReference a = new PhantomReference(new A(), referenceQueue);
2. 必须配合引用队列一起使用,当虚引用所引用的对象被回收时,由 Reference Handler 线程将虚引用对象入队,这样就可以知道哪些对象被回收,从而对它们关联的资源做进一步处理
3. 典型例子是 Cleaner 释放 DirectByteBuffer 关联的直接内存
### 类加载机制
**类加载的流程**
1. **加载**
1. 通过类加载器,将类的字节码载入方法区,并创建类.class 字节码对象(存储在方法区,并且能通过反射获取类信息)
2. 如果此类的父类没有加载,先加载父类(双亲委派模型)
3. 加载是懒惰执行
2. **链接**
1. 验证 – 验证类是否符合 Class 规范,合法性、安全性检查
2. 准备 – 为 static 变量分配空间,设置默认值
3. 解析 – 将常量池的符号引用解析为直接引用(符号引用只是一个代号,直接引用才能指向正确的目标)
3. **初始化**
1. 静态代码块、static 修饰的变量赋值、static final 修饰的引用类型变量赋值,会被合并成一个 `` 方法,在初始化时被调用
2. static final 修饰的基本类型变量赋值,在链接阶段就已完成
3. 初始化是懒惰执行
**jdk 8 的类加载器**
| **名称** | **加载哪的类** | **说明** |
| ------------------------------ | --------------------- | ------------------------------ |
| Bootstrap ClassLoader 启动类 | JAVA_HOME/jre/lib | 无法直接访问 |
| Extension ClassLoader 扩展类 | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap,显示为 null |
| Application ClassLoader 应用类 | classpath | 上级为 Extension |
| 自定义类加载器 | 自定义 | 上级为 Application |
**双亲委派模型**
所谓的双亲委派,就是指优先委派上级类加载器进行加载,如果上级类加载器
* 能找到这个类,由上级加载,加载后该类也对下级加载器可见
* 找不到这个类,则下级类加载器才有资格执行加载
双亲委派的目的有两点
1. 让上级类加载器中的类对下级共享(反之不行),即能让你的类能依赖到 jdk 提供的核心类
2. 让类的加载有优先次序,保证核心类优先加载
**如何破坏双亲委派模型**
自定义类继承 ClassLoader,作为自定义类加载器,重写 loadClass() 方法,不让它执行双亲委派逻辑,从而打破双亲委派。但是遇到自定义类加载器和核心类重名或者篡改核心类内容,jvm会使用沙箱安全机制,保护核心类,防止打破双亲委派机制,防篡改,如果重名的话就报异常。(例如:Tomcat)
**为什么要打破双亲委派模型**
1. 类冲突:当两个不同的类加载器加载同一个类时,会导致类的不一致性和冲突。
2. 动态加载:在某些情况下,应用程序需要在运行时动态加载一些类,但由于双亲委派模型的限制,可能无法实现这个需求。
**java代码的执行流程**
* 执行 javac 命令编译源代码为字节码
* 执行 java 命令
1. 创建 JVM,调用类加载子系统加载 class,将类的信息存入**方法区**
2. 创建 main 线程,使用的内存区域是 **JVM 虚拟机栈**,开始执行 main 方法代码
3. 如果遇到了未见过的类,会继续触发类加载过程,同样会存入**方法区**
4. 需要创建对象,会使用**堆**内存来存储对象
5. 不再使用的对象,会由**垃圾回收器**在内存不足时回收其内存
6. 调用方法时,方法内的局部变量、方法参数所使用的是 **JVM 虚拟机栈**中的栈帧内存
7. 调用方法时,先要到**方法区**获得到该方法的字节码指令,由**解释器**将字节码指令解释为机器码执行
8. 调用方法时,会将要执行的指令行号读到**程序计数器**,这样当发生了线程切换,恢复时就可以从中断的位置继续
9. 对于非 java 实现的方法调用,使用内存称为**本地方法栈**(见说明)
10. 对于热点方法调用,或者频繁的循环代码,由 **JIT 即时编译器**将这些代码编译成机器码缓存,提高执行性能
## JUC
### 线程
**线程的六种状态**

分别是
* **新建**
* 当一个线程对象被创建,但还未调用 start 方法时处于**新建**状态
* 此时未与操作系统底层线程关联
* **可运行**
* 调用了 start 方法,就会由**新建**进入**可运行**
* 此时与底层线程关联,由操作系统调度执行
* **终结**
* 线程内代码已经执行完毕,由**可运行**进入**终结**
* 此时会取消与底层线程关联
* **阻塞**
* 当获取锁失败后,由**可运行**进入 Monitor 的阻塞队列**阻塞**,此时不占用 cpu 时间
* 当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的**阻塞**线程,唤醒后的线程进入**可运行**状态
* **等待**
* 当获取锁成功后,但由于条件不满足,调用了 wait() 方法,此时从**可运行**状态释放锁进入 Monitor 等待集合**等待**,同样不占cpu 时间
* 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的**等待**线程,恢复为**可运行**状态
* **有时限等待**
* 当获取锁成功后,但由于条件不满足,调用了 wait(long) 方法,此时从**可运行**状态释放锁进入 Monitor 等待集合进行**有时限等待**,同样不占用 cpu 时间
* 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的**有时限等待**线程,恢复为**可运行**状态,并重新去竞争锁
* 如果等待超时,也会从**有时限等待**状态恢复为**可运行**状态,并重新去竞争锁
* 还有一种情况是调用 sleep(long) 方法也会从**可运行**状态进入**有时限等待**状态,但与 Monitor 无关,不需要主动唤醒,超时时间到自然恢复为**可运行**状态
* **wait 和 sleep**
* 共同点
* wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态
* 不同点
* 方法归属不同
* sleep(long) 是 Thread 的静态方法
* 而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有
* 醒来时机不同
* 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
* wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去
* 它们都可以被打断唤醒
* 锁特性不同(重点)
* wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
* wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)
* 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)
**创建线程的方式**
**1.继承Thread类**
```java
//创建线程方法一,继承Thread类,重写run()方法,调用start开启线程
public class TestThread1 extends Thread{
public void run(){
for (int i = 0; i < 20; i++) {
System.out.println("我在看代码--"+i);
}
}
public static void main(String[] args) {
//创建一个线程对象,调用start方法,开启多线程
//注:线程开启不一定立即执行,由CPU调度安排
TestThread1 testThread1 = new TestThread1();
testThread1.start();
for (int i = 0; i < 20; i++) {
System.out.println("我在学习多线程--"+i);
}
}
}
```
**2.实现Runnable接口**
```java
//方法二创建线程:实现Runnable接口,重写run方法,执行线程需要丢入runnable接口实现类,调用start方法
public class TestThread3 implements Runnable{
public void run(){
//run方法线程体
for (int i = 0; i < 200; i++) {
System.out.println("我再看代码————"+i);
}
}
public static void main(String[] args) {
//创建runnable解口的实现类对象
TestThread3 testThread3 = new TestThread3();
//创建线程对象,通过线程对象来开启我们的线程,代理
new Thread(testThread3).start();
for (int i = 0; i < 1000; i++) {
System.out.println("我再学习多线程--"+i);
}
}
}
```
**3.线程池创建线程**
```java
public class Main {
public static ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, //1.核心线程数量
50, //2.最大线程数量
5, //3.救急线程存活时间
TimeUnit.MINUTES, //4.时间单位
new ArrayBlockingQueue<>(50), //5.阻塞队列,长度为50
new ThreadFactory(){ //6.线程工厂
@Override
public Thread newThread(Runnable runnable){
return new Thread(runnable,"我是线程工厂创建的"+runnable.hashCode());
}},
new ThreadPoolExecutor.DiscardOldestPolicy()//7.拒绝策略
);
public static void main(String[] args) {
executor.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello world");
}
});
}
}
```
**线程池的核心参数**
1. corePoolSize 核心线程数目 - 池中会保留的最多线程数
2. maximumPoolSize 最大线程数目 - 核心线程+救急线程的最大数目
3. keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
4. unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
5. workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
6. threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
7. handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略
1. 抛异常:直接抛出异常
2. 由调用者执行任务:交给调用这个线程的调用者来执行
3. 丢弃任务:直接丢弃最后添加的任务
4. 丢弃最早排队任务:丢弃最早的任务
**线程池执行任务流程**
1. 当任务数量还没有超过**核心线程**,交给核心线程去做
2. 当任务数量超过了**核心线程**,存放到**阻塞队列**
3. 当任务数量超过了**核心线程+阻塞队列**,开启**救急线程**来工作
4. 当任务数量全部超过了,触发拒绝策略
### 并发安全
**线程并发的安全问题**
1. **竞态条件**(Race Condition):当多个线程同时访问共享的资源时,如果对资源的访问顺序和时间不加控制,就可能导致竞态条件。这会导致程序结果的不确定性和错误的操作。(例如:商品超卖,代码原子性的问题)
2. **死锁**(Deadlock):当多个线程相互等待对方占用的资源而无法继续执行时,就会发生死锁。如果不能正确地管理和释放资源的锁定状态,就可能导致死锁发生,进而导致程序无法继续执行。
3. **活锁**(Livelock):和死锁类似,活锁也是线程无法继续执行的一种情况,但不同的是,在活锁中线程一直在响应其他线程的操作,导致它们无法顺利完成自己的任务。
4. **数据不一致**(Inconsistent Data):当多个线程同时修改共享的数据时,如果没有正确地同步操作,就可能导致数据不一致的问题。例如,一个线程正在读取数据,而另一个线程同时在修改该数据,就可能导致读取到错误的数据值。
**保证线程并发的几种方式**
1. **synchronized关键字**
所有的Java 对象都有自己唯一的隐式[同步锁](https://so.csdn.net/so/search?q=同步锁&spm=1001.2101.3001.7020)。该锁只能同时被一个线程获得,其他试图获得该锁的线程都会被阻塞在对象的等待队列中直到获得该锁的线程释放锁才能继续工作。
2. **lock接口**
也是属于**互斥同步锁**,只能有一个线程能够获取,其他的阻塞,需要显示释放锁
3. **cas乐观锁(共享线程安全变量**)
**非阻塞同步**,也是只有一个线程能够获取锁,其他的线程不阻塞不断重试来获取锁
4. **ThreadLocal线程本地类(线程隔离)**
**作用**
* ThreadLocal 可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】,避免争用引发的线程安全问题
* ThreadLocal 同时实现了线程内的资源共享
**原理**
每个线程内有一个 ThreadLocalMap 类型的成员变量,用来存储资源对象
* 调用 set 方法,就是以 ThreadLocal对象 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中
* 调用 get 方法,就是以 ThreadLocal 对象自己作为 key,到当前线程中查找关联的资源值
* 调用 remove 方法,就是以 ThreadLocal对象 自己作为 key,移除当前线程关联的资源值
ThreadLocalMap 的一些特点
* key 的 hash 值统一分配
* 初始容量 16,扩容因子 2/3,扩容容量翻倍
* key 索引冲突后用开放寻址法解决冲突
**特点**
**弱引用 key**
ThreadLocalMap 中的 key 被设计为弱引用,原因如下
* Thread 可能需要长时间运行(如线程池中的线程),如果 key 不再使用,需要在内存不足(GC)时释放其占用的内存
**内存释放时机**
* 被动 GC 释放 key
* 仅是让 key 的内存释放,关联 value 的内存并不会释放
* 懒惰被动释放 value
* get key 时,发现是 null key,则释放其 value 内存
* set key 时,会使用启发式扫描,清除临近的 null key 的 value 内存,启发次数与元素个数,是否发现 null key 有关
* 主动 remove 释放 key,value
* 会同时释放 key,value 的内存,也会清除临近的 null key 的 value 内存
* 推荐使用它,因为一般使用 ThreadLocal 时都把它作为静态变量(即强引用),因此无法被动依靠 GC 回收
### 锁机制和原理
**cas原理**
**原理**:需要读写的内存位置(V)、预期原值(A)、新值(B)。如果内存位置与预期原值的A相匹配,那么将内存位置的值更新为新值B。如果内存位置与预期原值的值不匹配,那么处理器不会做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。
**ABA问题**:就是在CAS的过程中,预期原值(A)发生了变化然后又变了回来,这样就好像没变,解决方案 **版本号法**
**使用场景**:AtomicIntege里面使用了,cas+volatile 来保证原子性,可见性,有序性
**synchronized原理**
**对象头**
原理:因为synchronized锁的是对象,其实在对象的对象头里面一个**Monior**的,这个里面包含
- EntryQ(阻塞集合):所有获取对象锁失败的线程都在这里阻塞
- Owner(锁标识): 当对象头和线程进行交换时,保存线程标识,没有锁则为null
- Nest(锁重入):记录线程记录锁重入的次数
- Candidate(等待集合):当线程获取锁成功后的等待队列
**加锁流程**
1. 执行 monitorenter 指令后,当前线程试图获取对象所对应的 monitor 的持有权,当monitor的进入计数器为0,则该线程可以成功获取 monitor,并将计数器值设置为1,此时取锁成功。
2. 如果当前线程已经拥有该对象 monitor 的所有权,那它可以进入这个 monitor ,重入计数器的的值加1。
3. 如果其他线程已经拥有该对象 monitor 的所有权,那么当前线程将会被阻塞,直到正在执行的线程执行完毕,即 monitorexit 指令被执行,执行线程将释放 monitor锁并将计数器值设为0。
**volatile**
**原子性**
* 起因:多线程下,不同线程的**指令发生了交错**导致的共享变量的读写混乱
* 解决:用悲观锁或乐观锁解决,volatile 并不能解决原子性
**可见性**
* 起因:由于**编译器优化、或缓存优化、或 CPU 指令重排序优化**导致的对共享变量所做的修改另外的线程看不到
* 解决:用 volatile 修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见
**有序性**
* 起因:由于**编译器优化、或缓存优化、或 CPU 指令重排序优化**导致指令的实际执行顺序与编写顺序不一致
* 解决:用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果
* 注意:
* **volatile 变量写**加的屏障是阻止上方其它写操作越过屏障排到 **volatile 变量写**之下
* **volatile 变量读**加的屏障是阻止下方其它读操作越过屏障排到 **volatile 变量读**之上
* volatile 读写加入的屏障只能防止同一线程内的指令重排
## MySql
### 事务
**1.事务的四大特性**
- 原子性(Atomicity):原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚。
- 一致性(Consistency):一致性是指一个事务执行之前和执行之后都必须处于一致性状态。比如a与b账户共有1000块,两人之间转账 之后无论成功还是失败, 它们的账户总和还是1000。
- 隔离性(Isolation):隔离性跟隔离级别相关,如read committed,一个事务只能读到已经提交的修改。
- 持久性(Durability):持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
**2.并发事务带来的问题**
先了解下几个并发事务带来的问题:脏读、不可重复读、幻读。
- **脏读**:是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
- **不可重复读**:是指在对于数据库中的某行记录,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,另一个事务修改了数据并提交了。
- **幻读**:是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行,就像产生幻觉一样,这就是发生了幻读。
- **脏读,不可重复读,幻读的区别**:脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。幻读和不可重复读都是读取了另一条已经提交的事务,不同的是不可重复读的重点是修改,幻读的重点在于新增或者删除
**3.事务的隔离级别**
**事务隔离**就是为了解决上面提到的脏读、不可重复读、幻读这几个问题。
MySQL数据库为我们提供的四种隔离级别:
- **串行化(S)**:通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。
- **可重复读(RR)**:MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行,解决了不可重复读的问题。
- **读已提交(RC)**:一个事务只能看见已经提交事务所做的改变。可避免脏读的发生。
- **读未提交(RU)**:所有事务都可以看到其他未提交事务的执行结果。
查看隔离级别:
select @@transaction_isolation;
设置隔离级别:
set session transaction isolation level read uncommitted;
**4.事务的原理**
- 原子性,**undo log** 日志
- 持久性,是由 **redo log** 实现的
- 隔离性,是通过 **锁 和 MVCC** 来保证的
- 一致性,是通过上面三个共同实现的
**redo log**
概念:
用来记录Innodb存储引擎的事务日志,不管事务是否提交都会记录下来,用于数据恢复。
组成:
重做日志缓冲(内存中):数据库事务提交后,将修改的信息先保存到内存的缓冲页里面,等到合适的时机写入到磁盘中。因为这样是顺序写入,属于顺序IO,比直接刷新的随机IO效率高
重做日志文件(磁盘中):用于在刷新脏页到磁盘,发生错误时, 进行数据恢复使用。
**undo log**
概念:
属于逻辑日志,相当于你执行一条删除语句,他就写入一条新增语句,你执行修改,他写入反修改。就能实现回滚
作用:
用于记录数据被修改前的信息,作用包含两个: 提供回滚(**保证事务的原子性**) 和 MVCC(**多版本并发控制**)
**MVCC**
概念:
当前读:记录的是最新版本,读取时还要保证事务其不能修改当前记录,会加锁
快照读:记录的是数据的可见版本,读取时不加锁
MVCC:多版本并发控制,指维护一个数据的多个版本,使得读写操作没有冲突,快照读为MySQL实现MVCC提供了一个非阻塞读功能。
原理:
1. 我们在表结构里面会有,三个隐藏字段,分别记录了(最近修改数据的事务id,上一个版本的指针,隐藏主键)
2. 每一次修改都会记录修改的事务id和上个版本存入到undo log里面
3. 在undo log里面通过三个隐藏字段来维护形成一条版本链
4. 版本链保存有历史版本记录,通过 read view 判断当前版本的数据是否可见,如果不可见,再从版本链中找到上一个版本,继续进行判断,直到找到一个可见的版本。
### 索引
**什么是索引?**
索引是存储引擎用于提高数据库表的访问速度的一种数据结构。
**索引的优缺点?**
优点:
- 加快数据查找的速度
- 为用来排序或者是分组的字段添加索引,可以加快分组和排序的速度
- 加速表与表之间的连接
缺点:
- 建立索引需要占用物理空间
- 会降低表的增删改的效率,因为每次对表记录进行增删改,需要进行动态维护索引,导致增删改时间变长
**有那些常见索引?**
1. B+Tree索引,最常见的索引类型,大部分引擎都支持 B+ 树索引
2. Hash索引,底层数据结构是用哈希表实现的, 只有精确匹配索引列的查询才有效, 不支持范围查询,存在hash碰撞
3. 空间索引,空间索引是MyISAM引擎的一个特殊索引类型,主要用于地理空间数据类型,通常使用较少
4. 全文索引,是一种通过建立倒排索引,快速匹配文档的方式。类似于Lucene,Solr,ES
**什么情况下需要建索引?**
1. 经常用于查询的字段
2. 经常用于连接的字段(如外键)建立索引,可以加快连接的速度
3. 经常需要排序的字段建立索引,因为索引已经排好序,可以加快排序查询速度
**什么情况下不建索引?**
1. where条件中用不到的字段不适合建立索引
2. 表记录较少
3. 需要经常增删改
4. 参与列计算的列不适合建索引
5. 区分度不高的字段不适合建立索引,性别等
**Hash索引和B+树索引的区别?**
1. 哈希索引不支持排序,因为哈希表是无序的。
2. 哈希索引不支持范围查找。
3. 哈希索引不支持模糊查询及多列索引的最左前缀匹配。
4. 因为哈希表中会存在哈希冲突,所以哈希索引的性能是不稳定的,而B+树索引的性能是相对稳定的,每次查询都是从根节点到叶子节点。
**为什么B+树比B树更适合实现数据库索引?**
1. B+树的节点只存储索引key值,具体信息的地址存在于叶子节点的地址中。这就使以页为单位的索引中可以存放更多的节点。减少更多的I/O支出。
2. B+树会在叶子节点上建立类似于一种双向链表的结构,能够旁边排序和分组
**索引有什么分类?**
1. **主键索引**:名为primary的唯一非空索引,不允许有空值。
2. **唯一索引**:索引列中的值必须是唯一的,但是允许为空值。唯一索引和主键索引的区别是:
UNIQUE 约束的列可以为null且可以存在多个null值。UNIQUE KEY的用途:唯一标识数据库表中的每条记录,主要是用来防止数据重复插入。创建唯一索引的SQL语句如下:
3. **联合索引**:在表中的多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使用,使用组合索引时遵循最左前缀原则。
4. **全文索引**:只有在MyISAM引擎上才能使用,只能在CHAR,VARCHAR,TEXT类型字段上使用全文索引。
**最左匹配原则**
如果 SQL 语句中用到了组合索引中的最左边的索引,那么这条 SQL 语句就可以利用这个组合索引去进行匹配。
当遇到范围查询(>、<、between、like)就会停止匹配,后面的字段不会用到索引。
对(a,b,c)建立索引,查询条件使用 a/ab/abc 会走索引,使用 bc 不会走索引。对(a,b,c,d)建立索引,查询条件为 a = 1 and b = 2 and c > 3 and d = 4 ,那么,a,b,c三个字段能用到索引,而d就匹配不到。因为遇到了范围查询!
直接执行 b = 2 这种查询条件没有办法利用索引。从局部来看,当a的值确定的时候,b是有序的。例如a = 1时,b值为1,2是有序的状态。当a=2时候,b的值为1,4也是有序状态。
因此,你执行 a = 1 and b = 2 是a,b字段能用到索引的。而你执行 a > 1 and b = 2 时,a字段能用到索引,b字段用不到索引。因为a的值此时是一个范围,不是固定的,在这个范围内b值不是有序的,因此b字段用不上索引。
**聚拢索引**
**概念**:对于InnoDB来说,聚集索引一般是表中的主键索引,如果表中没有显示指定主键,则会选择表中的第一
个不允许为NULL的唯一索引。如果没有主键也没有合适的唯一索引,那么innodb内部会生成一个隐藏
的主键作为聚集索引,这个隐藏的主键长度为6个字节,它的值会随着数据的插入自增。
**作用**:聚拢索引下面的叶子节点包含的是当前行的所有数据,走聚拢索引查询数据比一般的索引要快
**覆盖索引**
**概念**:select的数据列只用从索引中就能够取得,不需要**回表**进行二次查询,换句话说查询列要被所使用的索
引覆盖。对于innodb表的二级索引,如果索引能覆盖到查询的列,那么就可以避免回表查询
**注意:**不是所有类型的索引都可以成为覆盖索引。覆盖索引要存储索引列的值,而哈希索引、全文索引不存储
索引列的值,所以MySQL只能使用b+树索引做覆盖索引。
**索引创建原则**
- 表的主键、外键必须有索引;
- 数据量超过300的表应该有索引;
- 经常与其他表进行连接的表,在连接字段上应该建立索引;
- 经常出现在Where子句中的字段,特别是大表的字段,应该建立索引;
- 索引应该建在选择性高的字段上;
- 索引应该建在小字段上,对于大的文本字段甚至超长字段,不要建索引;
- 复合索引的建立需要进行仔细分析,尽量考虑用单字段索引代替;
- 正确选择复合索引中的主列字段,一般是选择性较好的字段;
- 复合索引的几个字段是否经常同时以AND方式出现在Where子句中?单字段查询是否极少甚至没有?如果是,则可以建立复合索引;否则考虑单字段索引;
- 如果复合索引中包含的字段经常单独出现在Where子句中,则分解为多个单字段索引;
- 如果复合索引所包含的字段超过3个,那么仔细考虑其必要性,考虑减少复合的字段;
- 如果既有单字段索引,又有这几个字段上的复合索引,一般可以删除复合索引;
- 频繁进行数据操作的表,不要建立太多的索引;
- 删除无用的索引,避免对执行计划造成负面影响;
- 表上建立的每个索引都会增加存储开销,索引对于插入、删除、更新操作也会增加处理上的开销。另外,过多的复合索引,在有单字段索引的情况下,一般都是没有存在价值的;相反,还会降低数据增加删除时的性能,特别是对频繁更新的表来说,负面影响更大。
- 尽量不要对数据库中某个含有大量重复的值的字段建立索引。
**索引失效**
1. 联合索引没有遵循最左前缀法则,会导致失效或者部分失效
2. 使用了 (>,<,!=,)等范围查询 导致索引失效
3. 字符串没有加引号,会出现隐式类型转到,索引失效
4. 在索引列上进行函数运算,索引失效
5. 在头部进行模糊匹配,索引失效 (%yang:索引失效)(yang%:索引不失效)
6. or的连接左右两边都要加上索引,否则失效
### 锁
锁是计算机为了协调多个进程或线程并发访问某个资源的机制。
按照锁的粒度分为三类:
1. 全局锁:锁定数据库中的所有表,做全库的逻辑备份,对所有表进行锁定
2. 表级锁:锁住整张表
3. 行级锁:锁住对应的行数据
**全局锁**
引用场景:全库的逻辑备份,对所有表进行锁定,DDL,DML全部都处于阻塞状态,但是可以执行DQL语句
这样就保证了数据的一致性和完整性。
**表级锁**
**表锁**(手动添加)
表锁分为两类:
1. 共享锁(读锁):允许不同事务加入共享锁读取,阻止其他事务修改或加入排他锁。
2. 排他锁(写锁):允许获取排他锁的事务更新数据,阻止其他事务共享读锁和排他写锁。
**读锁不会阻塞其他客户端的读,但是会阻塞写;写锁既会阻塞其他客户端的读,又会阻塞其他客户端的写。**
```
# 加锁
lock tables table_name read/write;
# 释放锁
unlock tables;
```
**元数据锁**(自动添加)
MDL加锁过程是系统自动控制,**不需要显式使用**,访问一张表时会自动加上。MDL锁主要作用时维护表元数据(表结构)的数据一致 性,在表上有活动事务的时候,不可以对元数据进行写操作。简单来说,**表存在未提交的事务,不可以去修改表的结构**。为了避免DML 与DDL冲突,保证读写的正确性。在MySQL5.5中引入了MDL,当对一个表做 **增删改查操作** 的时候,加MDL **读锁**;当 **要对表做结构 变更**操作的时候,加**MDL写锁**,会阻塞全部。
**意向锁**(自动添加)
举个例子,线程A先开启事务,执行update操作,然后它会对这一行加上行锁,紧接着它会对这整张表加上意向锁。之后,线程B来 对这张表进行加表锁,此时它检查这张表意向锁的情况,如果当前加的锁与意向锁兼容就会成功,不兼容就会处于阻塞状态,阻塞到 线程A的事务提交,释放行锁和意向锁。
* **意向共享锁**(IS):事务有意向对表中的某些行加**共享锁**,必须先取得该表的IS锁。
* **意向排它锁**(IX):事务有意向对表中的某些行加**排他锁**,必须先取得该表的IX锁。
| | 意向共享锁(IS) | 意向排他锁(IX) |
| ------------- | ---------------- | ---------------- |
| 表共享锁(S) | 兼容 | 互斥 |
| 表排它锁(X) | 互斥 | 互斥 |
* **意向锁不会与行锁互斥**。
```mysql
# 查看意向锁及行锁的加锁情况
select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from performance_shema.data_locks;
```
* 意向锁解决的问题:意向锁是由InnoDB引擎来完成的,意向锁的存在使得在加表锁的过程中,**不再需要去对每行数据检查是否加锁**,使用意向锁来减少表锁的检查。为了避免DML在执行时,加的行锁与表锁的冲突。
**行级锁**
InnoDB的数据是基于索引组织的,行锁是**通过对索引上的索引项加锁来**实现的,而不是对记录加的锁。InnoDB行锁分为3种情形。
* 行锁(Record Lock):**对索引项加锁**,防止其他事务的update、delete,在RC、RR的隔离级别下支持。
* 间隙锁(Gap Lock):对**索引项之间**的“间隙”、**第一条记录前**的“间隙”或**最后一条记录后**的“间隙”加锁。
* 临键锁(Next-Key Lock):行锁和间隙锁组合,对记录及前面的间隙加锁,在RR隔离级别下支持。
**Gap锁/Next-key锁**
默认情况下,InnoDB在RR隔离级别下运行,InnoDB使用next-key锁进行搜索和索引扫描,以防止幻读。
* 索引上的等值查询(唯一查询),给不存在的记录加锁时,优化为间隙锁。
* 索引上的等值查询(普通索引),向右遍历时最后一个值不满足查询需求时,next-key lock退化为间隙锁。
* 索引上的范围查询(唯一索引),会访问到不满足条件的第一个值为止。
使用间隙锁的目的时防止其他事务插入间隙。间隙锁可以共存,一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁。
### 存储引擎
MySQL中常用的四种存储引擎分别是: MyISAM存储引擎、innoDB存储引擎、MEMORY存储引擎、ARCHIVE存储引擎。
MySQL 5.5版本后默认的存储引擎为InnoDB。
**InnoDB存储引擎**
InnoDB是MySQL默认的事务型存储引擎,使用最广泛,基于聚簇索引建立的。InnoDB内部做了很多优
化,如能够自动在内存中创建自适应hash索引,以加速读操作。
**优点**:支持事务和崩溃修复能力。InnoDB引入了行级锁和外键约束以及事务
**缺点**:占用的数据空间相对较大。
**适用场景**:需要事务支持,并且有较高的并发读写频率。
**MyISAM存储引擎**
数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,可以使用MyISAM引擎。
MyISAM会将表存储在两个文件中,数据文件.MYD和索引文件.MYI。
**优点**:访问速度快。
**缺点**:MyISAM不支持事务和行级锁,不支持崩溃后的安全恢复,也不支持外键。
**适用场景**:对事务完整性没有要求;只读的数据,或者表比较小,可以忍受修复repair操作。
MyISAM特性:
1. MyISAM对整张表加锁,而不是针对行。读取数据时会对需要读到的所有表加共享锁,写入时则对
表加排它锁。但在读取表记录的同时,可以往表中插入新的记录(并发插入)。
2. 对于MyISAM表,MySQL可以手动或者自动执行检查和修复操作。执行表的修复可能会导致数据丢
失,而且修复操作非常慢。可以通过 CHECK TABLE tablename 检查表的错误,如果有错误执行
REPAIR TABLE tablename 进行修复。
**MEMORY存储引擎**
MEMORY引擎将数据全部放在内存中,访问速度较快,但是一旦系统奔溃的话,数据都会丢失。
MEMORY引擎默认使用哈希索引,将键的哈希值和指向数据行的指针保存在哈希索引中。
**优点**:访问速度较快。
**缺点**:
email列创建前缀索引
ALTER TABLE table_name ADD KEY(column_name(prefix_length));
1. 哈希索引数据不是按照索引值顺序存储,无法用于排序。
2. 不支持部分索引匹配查找,因为哈希索引是使用索引列的全部内容来计算哈希值的。
3. 只支持等值比较,不支持范围查询。
4. 当出现哈希冲突时,存储引擎需要遍历链表中所有的行指针,逐行进行比较,直到找到符合条件的
**MyISAM和InnoDB的区别?**
1. **是否支持行级锁** : MyISAM 只有表级锁,而 InnoDB 支持行级锁和表级锁,默认为行级锁。
2. **是否支持事务和崩溃后的安全恢复**: MyISAM 注重性能,每次查询具有原子性,其执行速度比
InnoDB 类型更快,但是不提供事务支持。而 InnoDB 提供事务支持,具有事务、回滚和崩溃修复
能力。
3. **是否支持外键:** MyISAM 不支持,而 InnoDB 支持。
4. **是否支持MVCC** : MyISAM 不支持, InnoDB 支持。应对高并发事务,MVCC比单纯的加锁更高
效。
5. **MyISAM 不支持聚集索引, InnoDB 支持聚集索引**。
MyISAM 引擎主键索引和其他索引区别不大,叶子节点都包含索引值和行指针。
innoDB 引擎二级索引叶子存储的是索引值和主键值(不是行指针),这样可以减少行移动和
数据页分裂时二级索引的维护工作。
### sql优化
### **SQL优化必懂概念**
| 名词 | 概念 | 重点 |
| ---------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| 基数 | 某个列唯一建的数量叫做基数 | 查询结果是返回表中5%以内的数据时,应该走索引;当查询结果返回的是超过表中5%的数据时,应该走全表扫描 |
| 选择性 | 基数与总行数的比值再乘以100%就是某个列的选择性 | 一个列选择性大于20%,该列的数据分别就比较均衡了。一个列出现在where条件中,该列没有创建索引并且选择行大于20%,那么该列就必须创建索引,从而提升SQL查询性能。 |
| 直方图 | 如果没有对基数低的列收集直方图统计信息,基于成本的优化器(CBO)会认为该列数据分布是均衡的。会造成执行计划里面的Rows是假的。 | 直方图用来帮助CBO在对基数很低、数据分布不均衡的列进行Rows估算的时候,可以得到更精确的Row就够了。 |
| 回表 | 通过索引记录的rowid访问表中的数据就叫回表。回表一般是单块读,回表此时太多会严重影响SQL性能,如果回表此时太多,就不应该走索引扫描,应该走全表扫描。 | SQL优化时,一定要注意回表次数!特步时要注意回表的物理I/O次数。可以解释为什么返回表中5%以内的数据走索引、超过表中5%的数据要走全表扫描。 |
| 集群因子 | 集群因子用于判断索引回表需要消耗的物理I/O次数。介于表的块数和表行数中间。 | 集群因子与块数接近,说明表的数据基本上是有序的,而且其顺序基本与索引顺序一样。这样在进行索引范围或者索引扫描的时候,回表只需要读取少量的数据块就能完成。如果集群因子与表记录数接近,说明表的数据和索引顺序差异很大,在进行索引范围扫描或者索引全扫描的时候,回表会读取更多的数据块。**建议合适的组合索引消除回表,或者建议组合索引进来减少回表次数** |
| 表与表之间的关系 | 1:1关系 1:N关系 N:N关系 | 子查询等价改写、半连接等价改写就会用到表与表关系这个重要的概念 |
### **SQL优化概念**
**SQL语句性能优化**
1, 对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
2,应尽量避免在 where 子句中对字段进行 null 值判断,创建表时NULL是默认值,但大多数时候应该使用NOT NULL,或者使用一个特殊的值,如0,-1作为默 认值。
3,应尽量避免在 where 子句中使用!=或<>操作符, MySQL只有对以下操作符才使用索引:<,<=,=,>,>=,BETWEEN,IN,以及某些时候的LIKE。
4,应尽量避免在 where 子句中使用 or 来连接条件, 否则将导致引擎放弃使用索引而进行全表扫描, 可以 使用UNION合并查询: select id from t where num=10 union all select id from t where num=20
5,in 和 not in 也要慎用,否则会导致全表扫描,对于连续的数值,能用 between 就不要用 in 了:Select id from t where num between 1 and 3
6,下面的查询也将导致全表扫描:select id from t where name like ‘%abc%’ 或者select id from t where name like ‘%abc’若要提高效率,可以考虑全文检索。而select id from t where name like ‘abc%’ 才用到索引
7, 如果在 where 子句中使用参数,也会导致全表扫描。
8,应尽量避免在 where 子句中对字段进行表达式操作,应尽量避免在where子句中对字段进行函数操作
9,很多时候用 exists 代替 in 是一个好的选择: select num from a where num in(select num from b).用下面的语句替换: select num from a where exists(select 1 from b where num=a.num)
10,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有必要。
11,应尽可能的避免更新 clustered 索引数据列, 因为 clustered 索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统需要频繁更新 clustered 索引数据列,那么需要考虑是否应将该索引建为 clustered 索引。
12,尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。
13,尽可能的使用 varchar/nvarchar 代替 char/nchar , 因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。
14,最好不要使用”“返回所有: select from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。
15,尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。
16,使用表的别名(Alias):当在SQL语句中连接多个表时,请使用表的别名并把别名前缀于每个Column上.这样一来,就可以减少解析的时间并减少那些由Column歧义引起的语法错误。
17,使用“临时表”暂存中间结果
简化SQL语句的重要方法就是采用临时表暂存中间结果,但是,临时表的好处远远不止这些,将临时结果暂存在临时表,后面的查询就在tempdb中了,这可以避免程序中多次扫描主表,也大大减少了程序执行中“共享锁”阻塞“更新锁”,减少了阻塞,提高了并发性能。
18,一些SQL查询语句应加上nolock,读、写是会相互阻塞的,为了提高并发性能,对于一些查询,可以加上nolock,这样读的时候可以允许写,但缺点是可能读到未提交的脏数据。使用 nolock有3条原则。查询的结果用于“插、删、改”的不能加nolock !查询的表属于频繁发生页分裂的,慎用nolock !使用临时表一样可以保存“数据前影”,起到类似Oracle的undo表空间的功能,能采用临时表提高并发性能的,不要用nolock 。
19,常见的简化规则如下:不要有超过5个以上的表连接(JOIN),考虑使用临时表或表变量存放中间结果。少用子查询,视图嵌套不要过深,一般视图嵌套不要超过2个为宜。
20,将需要查询的结果预先计算好放在表中,查询的时候再Select。这在SQL7.0以前是最重要的手段。例如医院的住院费计算。
21,用OR的字句可以分解成多个查询,并且通过UNION 连接多个查询。他们的速度只同是否使用索引有关,如果查询需要用到联合索引,用UNION all执行的效率更高.多个OR的字句没有用到索引,改写成UNION的形式再试图与索引匹配。一个关键的问题是否用到索引。
22,在IN后面值的列表中,将出现最频繁的值放在最前面,出现得最少的放在最后面,减少判断的次数。
23,尽量将数据的处理工作放在服务器上,减少网络的开销,如使用存储过程。存储过程是编译好、优化过、并且被组织到一个执行规划里、且存储在数据库中的SQL语句,是控制流语言的集合,速度当然快。反复执行的动态SQL,可以使用临时存储过程,该过程(临时表)被放在Tempdb中。
24,当服务器的内存够多时,配制线程数量 = 最大连接数+5,这样能发挥最大的效率;否则使用 配制线程数量<最大连接数启用SQL SERVER的线程池来解决,如果还是数量 = 最大连接数+5,严重的损害服务器的性能。
25,查询的关联同写的顺序
select a.personMemberID, * from chineseresume a,personmember b where personMemberID = b.referenceid and a.personMemberID = ‘JCNPRH39681’ (A = B ,B = ‘号码’)
select a.personMemberID, * from chineseresume a,personmember b where a.personMemberID = b.referenceid and a.personMemberID = ‘JCNPRH39681’ and b.referenceid = ‘JCNPRH39681’ (A = B ,B = ‘号码’, A = ‘号码’)
select a.personMemberID, * from chineseresume a,personmember b where b.referenceid = ‘JCNPRH39681’ and a.personMemberID = ‘JCNPRH39681’ (B = ‘号码’, A = ‘号码’)
26,尽量使用exists代替select count(1)来判断是否存在记录,count函数只有在统计表中所有行数时使用,而且count(1)比count(*)更有效率。
27,尽量使用“>=”,不要使用“>”。
28,索引的使用规范:索引的创建要与应用结合考虑,建议大的OLTP表不要超过6个索引;尽可能的使用索引字段作为查询条件,尤其是聚簇索引,必要时可以通过index index_name来强制指定索引;避免对大表查询时进行table scan,必要时考虑新建索引;在使用索引字段作为条件时,如果该索引是联合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用;要注意索引的维护,周期性重建索引,重新编译存储过程。
29,下列SQL条件语句中的列都建有恰当的索引,但执行速度却非常慢:
SELECT * FROM record WHERE substrINg(card_no,1,4)=’5378’ (13秒)
SELECT * FROM record WHERE amount/30< 1000 (11秒)
SELECT * FROM record WHERE convert(char(10),date,112)=’19991201’ (10秒)
分析:
WHERE子句中对列的任何操作结果都是在SQL运行时逐列计算得到的,因此它不得不进行表搜索,而没有使用该列上面的索引;如果这些结果在查询编译时就能得到,那么就可以被SQL优化器优化,使用索引,避免表搜索,因此将SQL重写成下面这样:
SELECT * FROM record WHERE card_no like ‘5378%’ (< 1秒)
SELECT * FROM record WHERE amount< 1000*30 (< 1秒)
SELECT * FROM record WHERE date= ‘1999/12/01’ (< 1秒)
30,当有一批处理的插入或更新时,用批量插入或批量更新,绝不会一条条记录的去更新!
31,在所有的存储过程中,能够用SQL语句的,我绝不会用循环去实现!
(例如:列出上个月的每一天,我会用connect by去递归查询一下,绝不会去用循环从上个月第一天到最后一天)
32,选择最有效率的表名顺序(只在基于规则的优化器中有效):
oracle 的解析器按照从右到左的顺序处理FROM子句中的表名,FROM子句中写在最后的表(基础表 driving table)将被最先处理,在FROM子句中包含多个表的情况下,你必须选择记录条数最少的表作为基础表。如果有3个以上的表连接查询, 那就需要选择交叉表(intersection table)作为基础表, 交叉表是指那个被其他表所引用的表.
33,提高GROUP BY语句的效率, 可以通过将不需要的记录在GROUP BY 之前过滤掉.下面两个查询返回相同结果,但第二个明显就快了许多.
低效:
SELECT JOB , AVG(SAL)
FROM EMP
GROUP BY JOB
HAVING JOB =’PRESIDENT’
OR JOB =’MANAGER’
高效:
SELECT JOB , AVG(SAL)
FROM EMP
WHERE JOB =’PRESIDENT’
OR JOB =’MANAGER’
GROUP BY JOB
34,sql语句用大写,因为oracle 总是先解析sql语句,把小写的字母转换成大写的再执行。
35,别名的使用,别名是大型数据库的应用技巧,就是表名、列名在查询中以一个字母为别名,查询速度要比建连接表快1.5倍。
36,避免死锁,在你的存储过程和触发器中访问同一个表时总是以相同的顺序;事务应经可能地缩短,在一个事务中应尽可能减少涉及到的数据量;永远不要在事务中等待用户输入。
37,避免使用临时表,除非却有需要,否则应尽量避免使用临时表,相反,可以使用表变量代替;大多数时候(99%),表变量驻扎在内存中,因此速度比临时表更快,临时表驻扎在TempDb数据库中,因此临时表上的操作需要跨数据库通信,速度自然慢。
38,最好不要使用触发器,触发一个触发器,执行一个触发器事件本身就是一个耗费资源的过程;如果能够使用约束实现的,尽量不要使用触发器;不要为不同的触发事件(Insert,Update和Delete)使用相同的触发器;不要在触发器中使用事务型代码。
40**,mysql查询优化总结:使用慢查询日志去发现慢查询,使用执行计划去判断查询是否正常运行,总是去测试你的查询看看是否他们运行在最佳状态下。久而久之性能总会变化,避免在整个表上使用count(*),它可能锁住整张表,使查询保持一致以便后续相似的查询可以使用查询缓存**
,在适当的情形下使用GROUP BY而不是DISTINCT,在WHERE, GROUP BY和ORDER BY子句中使用有索引的列,保持索引简单,不在多个索引中包含同一个列,有时候MySQL会使用错误的索引,对于这种情况使用USE INDEX,检查使用SQL_MODE=STRICT的问题,对于记录数小于5的索引字段,在UNION的时候使用LIMIT不是是用OR。
为了 避免在更新前SELECT,使用INSERT ON DUPLICATE KEY或者INSERT IGNORE ,不要用UPDATE去实现,不要使用 MAX,使用索引字段和ORDER BY子句,LIMIT M,N实际上可以减缓查询在某些情况下,有节制地使用,在WHERE子句中使用UNION代替子查询,在重新启动的MySQL,记得来温暖你的数据库,以确保您的数据在内存和查询速度快,考虑持久连接,而不是多个连接,以减少开销,基准查询,包括使用服务器上的负载,有时一个简单的查询可以影响其他查询,当负载增加您的服务器上,使用SHOW PROCESSLIST查看慢的和有问题的查询,在开发环境中产生的镜像数据中 测试的所有可疑的查询。
41,MySQL 备份过程:
从二级复制服务器上进行备份。在进行备份期间停止复制,以避免在数据依赖和外键约束上出现不一致。彻底停止MySQL,从数据库文件进行备份。
如果使用 MySQL dump进行备份,请同时备份二进制日志文件 – 确保复制没有中断。不要信任LVM 快照,这很可能产生数据不一致,将来会给你带来麻烦。为了更容易进行单表恢复,以表为单位导出数据 – 如果数据是与其他表隔离的。
当使用mysqldump时请使用 –opt。在备份之前检查和优化表。为了更快的进行导入,在导入时临时禁用外键约束。
为了更快的进行导入,在导入时临时禁用唯一性检测。在每一次备份后计算数据库,表以及索引的尺寸,以便更够监控数据尺寸的增长。
通过自动调度脚本监控复制实例的错误和延迟。定期执行备份。
42,查询缓冲并不自动处理空格,因此,在写SQL语句时,应尽量减少空格的使用,尤其是在SQL首和尾的空格(因为,查询缓冲并不自动截取首尾空格)。
43,member用mid做標準進行分表方便查询么?一般的业务需求中基本上都是以username为查询依据,正常应当是username做hash取模来分表吧。分表的话 mysql 的partition功能就是干这个的,对代码是透明的;
在代码层面去实现貌似是不合理的。
44,我们应该为数据库里的每张表都设置一个ID做为其主键,而且最好的是一个INT型的(推荐使用UNSIGNED),并设置上自动增加的AUTO_INCREMENT标志。
45,在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON ,在结束时设置 SET NOCOUNT OFF 。
无需在执行存储过程和触发器的每个语句后向客户端发送 DONE_IN_PROC 消息。
46,MySQL查询可以启用高速查询缓存。这是提高数据库性能的有效Mysql优化方法之一。当同一个查询被执行多次时,从缓存中提取数据和直接从数据库中返回数据快很多。
47,EXPLAIN SELECT 查询用来跟踪查看效果
使用 EXPLAIN 关键字可以让你知道MySQL是如何处理你的SQL语句的。这可以帮你分析你的查询语句或是表结构的性能瓶颈。EXPLAIN 的查询结果还会告诉你你的索引主键被如何利用的,你的数据表是如何被搜索和排序的……等等,等等。
48,当只要一行数据时使用 LIMIT 1
当你查询表的有些时候,你已经知道结果只会有一条结果,但因为你可能需要去fetch游标,或是你也许会去检查返回的记录数。在这种情况下,加上 LIMIT 1 可以增加性能。这样一样,MySQL数据库引擎会在找到一条数据后停止搜索,而不是继续往后查少下一条符合记录的数据。
49,选择表合适存储引擎:
myisam: 应用时以读和插入操作为主,只有少量的更新和删除,并且对事务的完整性,并发性要求不是很高的。
Innodb: 事务处理,以及并发条件下要求数据的一致性。除了插入和查询外,包括很多的更新和删除。(Innodb有效地降低删除和更新导致的锁定)。对于支持事务的InnoDB类型的表来说,影响速度的主要原因是AUTOCOMMIT默认设置是打开的,而且程序没有显式调用BEGIN 开始事务,导致每插入一条都自动提交,严重影响了速度。可以在执行sql前调用begin,多条sql形成一个事物(即使autocommit打开也可以),将大大提高性能。
50,优化表的数据类型,选择合适的数据类型:
原则:更小通常更好,简单就好,所有字段都得有默认值,尽量避免null。
例如:数据库表设计时候更小的占磁盘空间尽可能使用更小的整数类型.(mediumint就比int更合适)
比如时间字段:datetime和timestamp, datetime占用8个字节,而timestamp占用4个字节,只用了一半,而timestamp表示的范围是1970—2037适合做更新时间
MySQL可以很好的支持大数据量的存取,但是一般说来,数据库中的表越小,在它上面执行的查询也就会越快。
因此,在创建表的时候,为了获得更好的性能,我们可以将表中字段的宽度设得尽可能小。例如,
在定义邮政编码这个字段时,如果将其设置为CHAR(255),显然给数据库增加了不必要的空间,
甚至使用VARCHAR这种类型也是多余的,因为CHAR(6)就可以很好的完成任务了。同样的,如果可以的话,
我们应该使用MEDIUMINT而不是BIGIN来定义整型字段。
应该尽量把字段设置为NOT NULL,这样在将来执行查询的时候,数据库不用去比较NULL值。
对于某些文本字段,例如“省份”或者“性别”,我们可以将它们定义为ENUM类型。因为在MySQL中,ENUM类型被当作数值型数据来处理,
而数值型数据被处理起来的速度要比文本类型快得多。这样,我们又可以提高数据库的性能。
51, 字符串数据类型:char,varchar,text选择区别
52,任何对列的操作都将导致表扫描,它包括数据库函数、计算表达式等等,查询时要尽可能将操作移至等号右边。
**索引优化**
1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
2.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:select id from t where num is null可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:select id from t where num=0
3.应尽量避免在 where 子句中使用!=或<>操作符,否则引擎将放弃使用索引而进行全表扫描。
4.应尽量避免在 where 子句中使用or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:select id from t where num=10 or num=20可以这样查询:select id from t where num=10 union all select id from t where num=20
5.in 和 not in 也要慎用,否则会导致全表扫描,如:select id from t where num in(1,2,3) 对于连续的数值,能用 between 就不要用 in 了:select id from t where num between 1 and 3
6.下面的查询也将导致全表扫描:select id from t where name like ‘李%’若要提高效率,可以考虑全文检索。
7.如果在 where 子句中使用参数,也会导致全表扫描。因为SQL只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然 而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面语句将进行全表扫描:select id from t where num=@num可以改为强制查询使用索引:select id from t with(index(索引名)) where num=@num
8.应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如:select id from t where num/2=100应改为:select id from t where num=100*2
9.应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如:select id from t where substring(name,1,3)=’abc’ ,name以abc开头的id
应改为:
select id from t where name like ‘abc%’
10.不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。
11.在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致。
12.不要写一些没有意义的查询,如需要生成一个空表结构:select col1,col2 into #t from t where 1=0
这类代码不会返回任何结果集,但是会消耗系统资源的,应改成这样:
create table #t(…)
13.很多时候用 exists 代替 in 是一个好的选择:select num from a where num in(select num from b)
用下面的语句替换:
select num from a where exists(select 1 from b where num=a.num)
14.并不是所有索引对查询都有效,SQL是根据表中数据来进行查询优化的,当索引列有大量数据重复时,SQL查询可能不会去利用索引,如一表中有字段sex,male、female几乎各一半,那么即使在sex上建了索引也对查询效率起不了作用。
15.索引并不是越多越好,索引固然可 以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有 必要。
16.应尽可能的避免更新 clustered 索引数据列,因为 clustered 索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统需要频繁更新 clustered 索引数据列,那么需要考虑是否应将该索引建为 clustered 索引。
17.尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。
18.尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。
19.任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。
20.尽量使用表变量来代替临时表。如果表变量包含大量数据,请注意索引非常有限(只有主键索引)。
21.避免频繁创建和删除临时表,以减少系统表资源的消耗。
22.临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。但是,对于一次性事件,最好使用导出表。
23.在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后insert。
24.如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。
25.尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过1万行,那么就应该考虑改写。
26.使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来解决问题,基于集的方法通常更有效。
27.与临时表一样,游标并不是不可使 用。对小型数据集使用 FAST_FORWARD 游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。在结果集中包括“合计”的例程通常要比使用游标执行的速度快。如果开发时 间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一种方法的效果更好。
28.在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON ,在结束时设置 SET NOCOUNT OFF 。无需在执行存储过程和触发器的每个语句后向客户端发送DONE_IN_PROC 消息。
29.尽量避免大事务操作,提高系统并发能力。
30.尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。
### **SQL优化思想**
定位慢查询:开启慢查询日志,可以将查询较慢的sql记录起来, 然后使用expian查看sql执行计划,还有记录sql执行的时间
拆分慢SQL:将sql语句进行拆分,拆分成多个子查询并添加表名
定位问题:执行每个子查询和查看执行计划,找出慢sql的问题所在,并且解决问题
联表分析:如果子查询没有问题,判断在联表的过程中是否存在问题,我们将子查询逐步加入来定位问题,判断是否是多表联查的问题,例如表关联字段没有添加索引
前后对比:解决问题之后,判断优化前后的执行时间对比
真实案例:
- 问题1:由于查询表的字段缺少索引导致表连接慢导致sql执行缓慢,解决方案:给对应的字段添加索引
- 问题2:union all 分成上下两层去优化,上层获取子查询扫描行数过多,解决方案:缩小子查询的结果集
- 问题3:由于缺少正确的索引导致的 解决方案:重新建立联合索引,缩小结果集
## Redis
### 应用场景和常见问题
1.**redis的常见应用场景**
- **缓存验证码**:一般用户登录的验证码之类的存放在redis里面比较好,设置获取时间来对应
- **游览量点赞量等**:通过redis的字符串的自增特性,来缓存这些数据,然后定时刷新到数据库里面
- **排行榜**:通过redis的sortset来实现,按照分数来进行排序
- **分布式锁**:利用setNx的机制,实现分布式锁,作用在多个不同的模块之下
- **限流器**:可用于限制某个用户访问某个接口的频率,比如秒杀场景用于防止用户快速点击带来不必要的压力。
- **幂等性判断**:可以使用redis来保证MQ重复消费的幂等性问题
- **共同关注**:利用set里面的交集,并集的功能
2.**redis的缓存问题**
- **缓存雪崩**
- 设置缓存时采用了相同的过期时间,**导致缓存在某一时刻同时失效**,请求全部转发到DB,DB瞬时压力过重挂掉。
- 解决方案:1.**添加随机值**,2.**提高redis集群的可用性**,3.**业务降级限流**,4.**多级缓存**
- **缓存击穿**
- 大量的请求同时查询一个 key 时,此时这个 key 正好失效了,就会导致大量的请求都落到DB导致压力过重挂掉。
- 解决方案:实时找到热点key,**互斥锁**,**逻辑过期**
- **缓存穿透**
- 缓存穿透是指查询一个**不存在的数据**,不经过redis,直接命中了DB,给DB压力
- 解决方案:**缓存空值**,**布隆过滤器**,将所有可能存在的数据哈希到一个足够大的 bitmap 中,加一层
- **数据一致性**
- 数据库和缓存可能会出现数据不一致的问题
- 解决方案:先更新数据库,在更新缓存,redisson分布式锁,mq异步更新
### 数据结构
- **string**:
- 字符串最基础的数据结构。字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串(例如JSON、XML))、数字 (整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能超过512MB。
- 场景:**缓存功能,计数,共享Session,限速**
- **hash**:
- 哈希类型是指键值本身又是一个键值对结构。
- 场景:**缓存用户信息,缓存对象**,**幂等性判断**
- **set**:
- 集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一 样的是,集合中不允许有重复元素,并且集合中的元素是无序的。
- 场景:**唯一性数据,共同关注,共同好友**
- **sortSet**:
- 有序集合中的元素可以排序。但是它和列表使用索引下标作为排序依据不同的是,它给每个元素设置一个权重(score)作为排序的依据。
- 场景:**排行榜**
- **list**:
- 列表(list)类型是用来存储多个有序的字符串。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色。
- 场景:**消息队列,文章列表**
### 持久化原理
**AOF:**
- 工作机制:AOF 是 以日志的形式来记录每个写操作,将每一次对数据进行修改,都把新建、修改数据的命令保存到指 定文件中。Redis 重新启动时读取这个文件,重新执行新建、修改数据的命令恢复数据。当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复
- 优点:
- 数据安全,AOF持久化可以配置 appendfsync 属性,有 always,每进行一次 命令操作就记录到 AOF文件中一次。
- 通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
- AOF日志文件的命令通过非常可读的方式进行记录,这个非常适合做灾难性的误删除紧急恢复,如果某人不小心用flushall命令
清空了所有数据,只要这个时候还没有执行rewrite,那么就可以将日志文件中的flushall删除,进行恢复
- 缺点:
- 对于同一份文件AOF文件比RDB数据快照要大。
- AOF开启后支持写的QPS会比RDB支持的写的QPS低,因为AOF一般会配置成每秒fsync操作,每秒的fsync操作还是很高的、
- 数据恢复比较慢,不适合做冷备。
**RDB:**
- RDB是Redis默认的持久化方式。
- 工作机制:每隔一段时间,就把内存中的数据保存到硬盘上的指定文件中。对应产生的数据文件为dump.rdb触发RDB的方式有两种:手动触发 (save) 和 自动触发 (bgsave)
- 优点:
- 只有一个文件 dump.rdb,方便持久化。
- 容灾性好,一个文件可以保存到安全的磁盘。
- 性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能
- 相对于数据集大时,比 AOF 的启动效率更高。
- 缺点:
- 数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候。如果redis要故障时要尽可能少的丢失数据,RDB没有AOF好,例如1:00进行的快照,在1:10又要进行快照的时候宕机了,这个时候就会丢失10分钟的数据。
- RDB每次fork出子进程来执行RDB快照生成文件时,如果文件特别大,可能会导致客户端提供服务暂停数毫秒或者几秒
### 淘汰策略
**过期键的删除策略**
1、**被动删除**:在访问key时,如果发现key已经过期,那么会将key删除。
2、**主动删除**:定时清理key,每次清理会依次遍历所有DB,从db随机取出20个key,如果过期就删除,如果其中有5个key过期,那么就 继续对这个db进行清理,否则开始清理下一个db。
3、**内存不够时清理**:Redis有最大内存的限制,通过maxmemory参数可以设置最大内存,当使用的内存超过了设置的最大内存,就要进 行内存释放, 在进行内存释放的时候,会按照配置的淘汰策略清理内存。
**内存淘汰策略**
当Redis的内存超过最大允许的内存之后,Redis 会触发内存淘汰策略,删除一些不常用的数据,以保证Redis服务器正常运行。
**Redisv4.0**前提供 **6** **种数据淘汰策略**:
- **volatile-lru**:LRU( Least Recently Used ),最近使用。利用LRU算法移除设置了过期时间的key (重点)
- **allkeys-lru**:当内存不足以容纳新写入数据时,从数据集中移除最近最少使用的key (重点)
- **volatile-ttl**:从已设置过期时间的数据集中挑选将要过期的数据淘汰
- **volatile-random**:从已设置过期时间的数据集中任意选择数据淘汰
- **allkeys-random**:从数据集中任意选择数据淘汰
- **no-eviction**:禁止删除数据,当内存不足以容纳新写入数据时,新写入操作会报错(默认)
### 集群模式
**主从复制**:是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为 **主节点(master)**,后者称为 **从节点(slave)**。且数据的复制是 **单向** 的,只能由主节点到从节点。Redis 主从复制支持 **主从同步** 和 **从从同步** 两种,后者是 Redis 后续版本新增的功能,以减轻主节点的同步负担。
作用:
- **数据冗余:** 主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
- **故障恢复:** 当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复 *(实际上是一种服务的冗余)*。
- **负载均衡:** 在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务 *(即写 Redis 数据时应用连接主节点,读 Redis 数据时应用连接从节点)*,分担服务器负载。尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。
- **高可用基石:** 除了上述作用以外,主从复制还是哨兵和集群能够实施的 **基础**,因此说主从复制是 Redis 高可用的基础。
缺点:
- 一旦主节点出现故障,需要手动将一个从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令其他从节点去复制新的主节点,整个过程都需要人工干预。
- 主节点的写能力受到单机的限制。
- 主节点的存储能力受到单机的限制。
- 第一个问题是Redis的高可用问题,第二、三个问题属于Redis的分布式问题。
**哨兵模式**:Redis Sentinel ,它由两部分组成,哨兵节点和数据节点:
- **哨兵节点:** 哨兵系统由一个或多个哨兵节点组成,哨兵节点是特殊的 Redis 节点,不存储数据,对数据节点进行监控。
- **数据节点:** 主节点和从节点都是数据节点;
在复制的基础上,哨兵实现了 **自动化的故障恢复** 功能,下面是官方对于哨兵功能的描述:
- **监控(Monitoring):** 哨兵会不断地检查主节点和从节点是否运作正常。
- **自动故障转移(Automatic failover):** 当 **主节点** 不能正常工作时,哨兵会开始 **自动故障转移操作**,它会将失效主节点的其中一个 **从节点升级为新的主节点**,并让其他从节点改为复制新的主节点。
- **配置提供者(Configuration provider):** 客户端在初始化时,通过连接哨兵来获得当前 Redis 服务的主节点地址。
- **通知(Notification):** 哨兵可以将故障转移的结果发送给客户端。
其中,监控和自动故障转移功能,使得哨兵可以及时发现主节点故障并完成转移。而配置提供者和通知功能,则需要在与客户端的交互中才能体现。
**分片集群**:分布式的存储中,要把数据集按照分区规则映射到多个节点,常见的数据分区规则三种:
- 哈希取余分区
- 一致性哈希算法分区
- 哈希槽分区
### 其他问题
**1.Redis常见性能问题和解决方案**
```java
1.Master 最好不要做任何持久化工作,包括内存快照和 AOF 日志文件,特别是不要启用内存快照做持久化。
2.如果数据比较关键,某个 Slave 开启 AOF 备份数据,策略为每秒同步一次。
3.为了主从复制的速度和连接的稳定性,Slave 和 Master 最好在同一个局域网内。
4.尽量避免在压力较大的主库上增加从库。
5.Master 调用 BGREWRITEAOF 重写 AOF 文件,AOF 在重写的时候会占大量的 CPU 和内存资源,导致服务 load 过高,出现短暂服务暂停现象。
6.为了 Master 的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关为:Master<–Slave1<–Slave2<–Slave3…,这样的结构也方便解决单点故障问题,实现 Slave 对 Master 的替换,也即,如果 Master 挂了,可以立马启用 Slave1 做 Master,其他不变。
```
**2.Redis和Lua脚本的使用了解吗**
Redis的事务功能比较简单,平时的开发中,可以利用Lua脚本来增强Redis的命令。
Lua脚本能给开发人员带来这些好处:
- Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。
- Lua脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这 些命令常驻在Redis内存中,实现复用的效果。
- Lua脚本可以将多条命令一次性打包,有效地减少网络开销。
```lua
-- 库存未预热
if (redis.call('exists', KEYS[2]) == 1) then
return -9;
end;
-- 秒杀商品库存存在
if (redis.call('exists', KEYS[1]) == 1) then
local stock = tonumber(redis.call('get', KEYS[1]));
local num = tonumber(ARGV[1]);
-- 剩余库存少于请求数量
if (stock < num) then
return -3
end;
-- 扣减库存
if (stock >= num) then
redis.call('incrby', KEYS[1], 0 - num);
-- 扣减成功
return 1
end;
return -2;
end;
-- 秒杀商品库存不存在
return -1;
```
**3.大key问题了解吗**
Redis使用过程中,有时候会出现大key的情况, 比如:
- 单个简单的key存储的value很大,size超过10KB
- hash, set,zset,list 中存储过多的元素(以万为单位)
**大key会造成什么问题呢?**
- 客户端耗时增加,甚至超时
- 对大key进行IO操作时,会严重占用带宽和CPU
- 造成Redis集群中数据倾斜
- 主动删除、被动删等,可能会导致阻塞
**如何找到大key?**
- bigkeys命令:使用bigkeys命令以遍历的方式分析Redis实例中的所有Key,并返回整体统计信息与每个数据类型中Top1的大Key
- redis-rdb-tools:redis-rdb-tools是由Python写的用来分析Redis的rdb快照文件用的工具,它可以把rdb快照文件生成json文件或者生成报表用来分析Redis的使用详情。
**如何处理大key?**
- **删除大key**
- - 当Redis版本大于4.0时,可使用UNLINK命令安全地删除大Key,该命令能够以非阻塞的方式,逐步地清理传入的Key。
- 当Redis版本小于4.0时,避免使用阻塞式命令KEYS,而是建议通过SCAN命令执行增量迭代扫描key,然后判断进行删除。
- **压缩和拆分key**
- - 当vaule是string时,比较难拆分,则使用序列化、压缩算法将key的大小控制在合理范围内,但是序列化和反序列化都会带来更多时间上的消耗。
- 当value是string,压缩之后仍然是大key,则需要进行拆分,一个大key分为不同的部分,记录每个部分的key,使用multiget等操作实现事务读取。
- 当value是list/set等集合类型时,根据预估的数据规模来进行分片,不同的元素计算后分到不同的片。
**4.Redis报内存不足怎么处理**
Redis 内存不足有这么几种处理方式:
- 修改配置文件 redis.conf 的 maxmemory 参数,增加 Redis 可用内存
- 也可以通过命令set maxmemory动态设置内存上限
- 修改内存淘汰策略,及时释放内存空间
- 使用 Redis 集群模式,进行横向扩容。
## RabbitMQ
- 消息重复消费问题
- 如何保证消息的可靠性
- 如何防止消息堆积
- 延迟队列问题
## 设计模式
**spring体现了什么设计模式**
(1)工厂模式:Spring使用工厂模式,通过BeanFactory和ApplicationContext来创建对象
(2)单例模式:Bean在容器内默认为单例模式
(3)组合模式:适配器在解析控制器的上的参数时,通过不同的参数解析器按照顺序来解析参数,体现了组合模式,这也是为什么参数可能没有加任何注解,依然能够被正确的解析的原因
(4)代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术
(5)模板方法:可以将相同部分的代码放在父类中,而将不同的代码放入不同的子类中,用来解决代码重复的问题。比如RestTemplate, JmsTemplate, JpaTemplate
(6)适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式,Spring MVC中也是用到了适配器模式适配Controller
(7)观察者模式:Spring事件驱动模型就是观察者模式的一个经典应用。
(8)桥接模式:可以根据客户的需求能够动态切换不同的数据源。比如我们的项目需要连接多个数据库,客户在每次访问中根据需要会去访问不同的数据库
**你的项目中用到了什么设计模式?**
- 策略模式
- 模板方法模式
- 单例模式
- 工厂方法模式
**手写单例模式和工厂模式**
**饿汉式**
```java
public class Singleton1 implements Serializable {
private static final Singleton1 INSTANCE = new Singleton1();
private Singleton1(){
//反射破坏单例
//这个代码目的是构造方法抛出异常是防止反射破坏单例
if (INSTANCE != null){
throw new RuntimeException("单例对象不能重复创建");
}
System.out.println("private Singleton1()");
}
//获取单例对象
public static Singleton1 getInstance(){
return INSTANCE;
}
public static void otherMethod() {
System.out.println("otherMethod()");
}
//反序列化破坏单例
//重写这个方法的目的是,为了防止反序列化破坏单例
public Object readResolve() {
return INSTANCE;
}
}
```
**双检锁懒汉式**
```java
//用于解决并发情况下的单例情况
public class Singleton4 implements Serializable {
private static volatile Singleton4 INSTANCE = null;
//为什么要添加volatile?
//1.添加volatile是因为 INSTANCE = new Singleton4()不是原子性的,有三步
//创建对象,调用构造,给静态变量赋值。其中后两步可能被指令重排序优化,变成先赋值、再调用构造
//2.如果线程1 先执行了赋值,线程2 执行到第一个 INSTANCE == null 时发现
//INSTANCE 已经不为 null,直接返回了,此时就会返回一个未完全构造的对象
//所以 volatile 相当于就是一个屏障,防止指令重排序优化的而导致返回一个不完全的单例对象情况发生
public static Singleton4 getInstance(){
if (INSTANCE == null){
synchronized (Singleton4.class){
if (INSTANCE == null){
INSTANCE = new Singleton4();
}
}
}
return INSTANCE;
}
//其他方法
public static void otherMethod() {
System.out.println("otherMethod()");
}
}
```
**内部类懒汉式**
```java
//内部类懒汉式:避免了双检锁的缺点
//在静态中创建单例对象,完美的解决了并发问题
public class Singleton5 implements Serializable {
private Singleton5(){
System.out.println("构建对象");
}
private static class Holder{
//在内部类中构建对象
static Singleton5 INSTANCE = new Singleton5();
}
public static Singleton5 getInstance(){
return Holder.INSTANCE;
}
}
```
**简单工厂:用来生产同一等级结构中的任意产品。(不支持扩展增加产品)**
```java
interface Car {
public void run();
}
class AoDiCar implements Car {
@Override
public void run() {
System.out.println("奥迪汽车在跑");
}
}
class BMWCar implements Car {
@Override
public void run() {
System.out.println("有个小姐姐坐在宝马里面哭");
}
}
class CarFactory {
public static Car createCar(String name){
switch (name) {
case "aodi":
return new AoDiCar();
case "bmw":
return new BMWCar();
default: return null;
}
}
}
public class Client01 {
public static void main(String[] args) {
Car aodi = CarFactory.createCar("aodi");
Car bmw = CarFactory.createCar("bmw");
aodi.run();
bmw.run();
}
}
```
**工厂方法:用来生产同一等级结构中的固定产品。(支持扩展增加产品)**
```java
interface CarFactory2 {
public Car createCar();
}
class AodiCarFactory implements CarFactory2 {
@Override
public Car createCar() {
return new AoDiCar();
}
}
class BmwCarFactory implements CarFactory2 {
@Override
public Car createCar() {
return new BMWCar();
}
}
public class Client02 {
public static void main(String[] args) {
Car car = new AodiCarFactory().createCar();
Car bmw = new BmwCarFactory().createCar();
car.run();
bmw.run();
}
}
```
**抽象工厂:用来生产不同产品族的全部产品**
```java
interface Engine {
void run();
void start();
}
class EngineA implements Engine {
@Override
public void run() {
System.out.println("转的快");
}
@Override
public void start() {
System.out.println("启动快,自动挡");
}
}
class EngineB implements Engine {
@Override
public void run() {
System.out.println("转的慢");
}
@Override
public void start() {
System.out.println("启动快,手动挡");
}
}
interface CarFactory {
public Engine createEngine();
public Chair createChair();
}
class BmwCar implements CarFactory {
@Override
public Engine createEngine() {
return new EngineB();
}
@Override
public Chair createChair() {
return new ChairB();
}
}
class AoDiCarFactory implements CarFactory {
@Override
public Engine createEngine() {
return new EngineA();
}
@Override
public Chair createChair() {
return new ChairA();
}
}
class BmwCar implements CarFactory {
@Override
public Engine createEngine() {
return new EngineB();
}
@Override
public Chair createChair() {
return new ChairB();
}
}
interface Chair {
void run();
}
class ChairA implements Chair {
@Override
public void run() {
System.out.println("可以自动加热");
}
}
class ChairB implements Chair {
@Override
public void run() {
System.out.println("真皮,舒适");
}
}
public class Client {
public static void main(String[] args) {
CarFactory aoDiCar = new AoDiCar();
Engine engine = aoDiCar.createEngine();
engine.run();
engine.start();
}
}
```
## ssm和boot
### spring
**IOC**
**1.BeanFactory** 和 **ApplicationContext**
1. BeanFactory 是 ApplicationContext 的父接口
2. BeanFactory 是 spring容器的核心,ApplicationContext 实现组合了 BeanFactory 的功能;
3. BeanFactory 是 ApplicationContext 的成员变量
4. BeanFactory的实现类,实现了很多功能 控制反转,依赖注入 之类的
5. ApplicationContext 扩展了 BeanFactory 的功能
1. 国际化:扩展了提供国际语言
2. 通配符:扩展了通配符的方式获取一组 Resource 资源的能力
3. 事件监听机制:扩展了用于代码解耦
**ApplicationContext**
1.ApplicationContext 只要创建了就有许多后处理器提供功能
2.ClassPathXmlApplicationContext: 通过classPath下的xml文件创建容器
3.AnnotationConfigWebApplicationContext: 通过java配置类的形式创建容器
4.AnnotationConfigApplicationContext: 通过添加 @Configuration 注解的方式创建容器
**BeanFactory **
1.BeanFactory 可以通过 registerBeanDefinitely 注册一个 beanDefinitely 对象
2.配置类,xml,组件扫描都是通过生成 beanDefinitely 对象注册到 BeanFactory 中
3.beanDefinitely 就是bean的创建蓝图,包含了scope,用构造还是工厂创建,初始化方法是什么的信息
4.通过 beanDefinitely 的信息就可以创建bean对象
5.BeanFactory 需要手动调用后 BeanFactory 后处理器来增强,通过这些后处理器才能识别 @Bean @ComponentScan 等注解
6.BeanFactory 需要手动调用后 Bean 后处理器来增强,以便对后续 bean 的创建过程提供增强
**2.bean的生命周期**
1. 创建实例:根据 bean 的构造方法或者工厂方法创建 bean 实例对象
2. 依赖注入:根据 @Autowired,@Value 之类的,给bean的成员变量填充值
3. 初始化前:回调 Aware 接口,调用这些方法来对bean进行增强
4. 初始化:通过指定的初始化方法来初始化,注解,接口之类的
5. 初始化后:创建代理对象之类的,看这个bean有没有这个需求
6. 销毁:在容器关闭时,会销毁所有单例对象
**为什么 @Autowired,@Value 就能给bean的成员变量填充值?**
```java
bean的后处理器:一般是在bean的生命周期阶段完成扩展功能
一些常用的bean后处理器?
1.AutowiredAnnotationBeanPostProcessor:解析 @AutoWired 和 @Value 等功能的
2.CommonAnnotationBeanPostProcessor:解析@Resource、@PostConstruct、@PreDestroy
3.ConfigurationPropertiesBindingPostProcessor:@ConfigurationProperties
4.ContextAnnotationAutowiredCandidateResolver:解析 @Lazy 取 @Value 的值
AutowiredAnnotationBeanPostProcessor怎么通过 @AutoWired 来进行依赖注入?
1.通过 bean工厂去遍历每一个bean
2.Autowired的后处理就会去执行一个寻找Autowired源数据的方法,去获取bean上加了 @Value,@Autowired的成员变量
方法的参数信息,然后转化成 (InjectionMetadata)元数据类型,里面包含了加了@Value,@Autowired注解的信息
3.拿到(InjectionMetadata)元数据对象之后,根据成员变量,方法参数封装为 DependencyDescriptor(依赖描述)类型
4.然后根据工厂的解决依赖方法,基于类型查找,完成依赖注入
```
**Aware接口是什么?**
```java
一:Aware接口是什么?
1.Aware 接口提供了一种【内置】 的注入手段,同样能够增强bean
2.BeanNameAware 注入 bean 的名字
3.BeanFactoryAware 注入存放自己的 BeanFactory 容器对象
4.ApplicationContextAware 注入存放自己的 ApplicationContext 容器对象
5.EmbeddedValueResolverAware 注入 ${} 解析器
6.InitializingBean 接口提供了一种【内置】的初始化手段,可以初始化Bean
二:明明注解能实现的功能,还要Aware?
1.因为 @AutoWired 注解之类的需要后处理器,相当于扩展功能
2.Aware接口属于内置功能,不用扩展也能实现,总会被执行
3.而扩展功能受某些情况影响可能会失效
三:什么情况下@Autowired会失效?
1.被注入的对象没有加载到spring容器中
2.需要自动注入的对象不是spring加载,而是new的方式创建
3.需要自动注入的对象不是spring加载,而是采用反射的方式创建
四:怎么解决?
1.因为 @Autowired 啥的是后处理带来的功能,那我们就用内置的 Aware接口 就肯定不会出错
2.将实例工厂方法转化为静态工厂,避免被提前创建
```
**初始化和销毁的方法有几种?**
```java
Bean的初始化方法有几种?
1. @PostConstruct 标注的初始化方法
2. InitializingBean 接口的初始化方法
3. @Bean(initMethod) 指定的初始化方法
bean销毁方法有几种
1. @PreDestroy 标注的销毁方法
2. DisposableBean 接口的销毁方法
3. @Bean(destroyMethod) 指定的销毁方法
```
**循环依赖了怎么办?**
**set循环依赖**:set构造Bean的循环依赖解决过程

1.首先先通过getBean操作获取ABean
2.如果**一级缓存**(singletonObjects)里面有则直接获取到,如果没有则执行创建ABean的过程
3.但是创建ABean需要BBean,创建BBean又需要ABean。因此产生了循环依赖
4.这时候我们需要一个**三级缓存**(singletonFactories)
5.在依赖注入之前,将半成品ABean放入到这个**三级缓存**里面去,然后在执行创建Bbean的流程
6.当BBean里面走到需要ABean的时候,去**三级缓存**里面找。找到了之和,这时候判断是否发生了循环依赖,如果发生了
7.则需要提前创建ABean的代理对象放入到**二级缓存**里面,给BBean进行依赖注入。
8.这是BBean的代理对象已经创建好了,将Bean放入到**一级缓存**里面然后给ABean进行依赖注入
9.ABean的setB操作能够顺利执行,然后ABean的创建过程也已经结束
10最后从**二级缓存**中获取到A的代理对象放入到**一级缓存**当中
**构造循环依赖**:构造方法发生了循环依赖
如果构造方法的循环依赖的话是不能通过三级缓存解决的
- 解决思路一:a注入b的代理对象,这样能够保证a的流程走通.后续需要用到b的真实对象时,可以通过代理间接访问
例如:使用@Lazy放在参数上,产生代理对象,解决循环依赖
- @Lazy的创建代理的过程是,先创建代理,放入一个自定义的TargetSource对象
- TargetSource的getTarget,被调用的时候回去找目标对象,相当于推迟了对目标对象的获取
- 解决思路二:a注入b的工厂对象,让b的实例创建被推迟,这样能够保证a的流程先走通
- 后续需要用到 b 的真实对象时。再通过 ObjectFactory 工厂对象来获取
**AOP:AOP是由动态代理+切面(切点,通知)实现的**
**动态代理**
1.创建动态代理的两种方式
- JDK代理增强:目标对象实现接口,因为产生的代理类也实现了相同接口,平级效果
- cglib代理增强:代理对象继承代理增强,通过重写目标方法
2.两种方式如何选择
- 如果指定了接口,且 proxyTargetClass = false 使用 JdkDynamicAopProxy
- 如果没有指定接口,或者 proxyTargetClass = true,使用 ObjenesisCglibAopProxy
**切点**
- 切点匹配的逻辑(方法名字)
1.通过切点里面的 matches(匹配) 方法传递 目标可能会增强的方法 和 目标对象
2.匹配上了返回 true 进行增强
- 切点匹配的逻辑(注解)
1.matches(匹配) 判断方法有没有特定的注解
2.对于注解方法进行增强
- @Transactional详解
1.如果方法上添加了这个注解,那么为这个方法创建代理对象,然后去匹配事务增强的切面
2.如果类上添加了,那么这个类的所有方法都创建代理对象,然后去匹配事务增强的切面
3.实现的接口添加了,那么这个类的所有方法都创建代理对象,然后去匹配事务增强的切面
**通知**
通知在spring的作用下都会转化成环绕通知(体现了适配器模式)
**切面**
```
能说一说切面吗?
答:有两种切面,高级切面@Aspect和低级切面Advisor
两个切面有什么关系?
答;spring在处理中 高级切面会统一转换成低级切面?
切面的工作流程是什么?
第一步:高级切面转化低级切面(创建代理时执行):1.从高级切面内找到注解 2.解析切点 3.找到通知类 4.封装起来转化为低级切面类
第二步:通知统一转化为环绕通知(代理对象执行时转换):体现了适配器模式
第三步:调用执行链(调用链对象执行)
怎么转换的?
答:通过一个bean后处理器,将能匹配上的Advisor存入起来,能匹配上的高级切面先转为低级然后存储起来
怎么转化为环绕通知?
答:体现了适配器模式
哦?另外一个作用能够细说一下吗?
答:内部也还是调用之前(寻找符合条件的切面)的方法,如果返回集合不为空,就说明需要创建代理
它的调用时机通常在原始对象初始化后执行, 但碰到循环依赖会提前至依赖注入之前执行
代理对象的创建时机?
1.代理的创建时机
初始化之后 (无循环依赖时)
实例创建后, 依赖注入前 (有循环依赖时), 并暂存于二级缓存
2.依赖注入与初始化不应该被增强, 仍应被施加于原始对象
```
**spring事务失效场景**
**1. 抛出检查异常导致事务不能正确回滚**
```java
@Service
public class Service1 {
@Autowired
private AccountMapper accountMapper;
@Transactional
public void transfer(int from, int to, int amount) throws FileNotFoundException {
int fromBalance = accountMapper.findBalanceBy(from);
if (fromBalance - amount >= 0) {
accountMapper.update(from, -1 * amount);
new FileInputStream("aaa");
accountMapper.update(to, amount);
}
}
}
```
* 原因:Spring 默认只会回滚非检查异常
* 解法:配置 rollbackFor 属性
* `@Transactional(rollbackFor = Exception.class)`
**2. 业务方法内自己 try-catch 异常导致事务不能正确回滚**
```java
@Service
public class Service2 {
@Autowired
private AccountMapper accountMapper;
@Transactional(rollbackFor = Exception.class)
public void transfer(int from, int to, int amount) {
try {
int fromBalance = accountMapper.findBalanceBy(from);
if (fromBalance - amount >= 0) {
accountMapper.update(from, -1 * amount);
new FileInputStream("aaa");
accountMapper.update(to, amount);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
```
* 原因:事务通知只有捉到了目标抛出的异常,才能进行后续的回滚处理,如果目标自己处理掉异常,事务通知无法知悉
* 解法1:异常原样抛出
* 在 catch 块添加 `throw new RuntimeException(e);`
* 解法2:手动设置 TransactionStatus.setRollbackOnly()
* 在 catch 块添加 `TransactionInterceptor.currentTransactionStatus().setRollbackOnly();`
**3. 非 public 方法导致的事务失效**
```java
@Service
public class Service4 {
@Autowired
private AccountMapper accountMapper;
@Transactional
void transfer(int from, int to, int amount) throws FileNotFoundException {
int fromBalance = accountMapper.findBalanceBy(from);
if (fromBalance - amount >= 0) {
accountMapper.update(from, -1 * amount);
accountMapper.update(to, amount);
}
}
}
```
* 原因:Spring 为方法创建代理、添加事务通知、前提条件都是该方法是 public 的
* 解法1:改为 public 方法
* 解法2:添加 bean 配置如下(不推荐)
```java
@Bean
public TransactionAttributeSource transactionAttributeSource() {
return new AnnotationTransactionAttributeSource(false);
}
```
**6. 调用本类方法导致传播行为失效**
```java
@Service
public class Service6 {
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void foo() throws FileNotFoundException {
LoggerUtils.get().debug("foo");
bar();
}
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void bar() throws FileNotFoundException {
LoggerUtils.get().debug("bar");
}
}
```
* 原因:本类方法调用不经过代理,因此无法增强
* 解法1:依赖注入自己(代理)来调用
* 解法2:通过 AopContext 拿到代理对象,来调用
* 解法3:通过 CTW,LTW 实现功能增强
### springMVC
**MVC的流程**:
1.客户端发起请求到**前端控制器**(DispatcherServlet)
2.DispatcherServlet 根据请求的 URL 找到对应的**处理器映射器**(HandlerMapping)
3.HandlerMapping 根据url找到对应的处理器和拦截器生成一条调用链交给 DispatcherServlet
4.DispatcherServlet 交给**处理器适配器**(HandlerAdapter)
5.适配器根据Handler方法找到对应的 **控制器**(controller)执行方法
6.controller返回一个(**视图模型数据**)ModelAndView,如果添加了@requestBody注解就不用解析
7.将ModelAndView 交给 DispatcherServlet ,DispatcherServlet 转交给**返回值处理器**或者是**错误解析器**
8.如果整个过程没有错误,**返回值处理器**进行模型渲染,如果 @ResponseBody则不需要模型渲染
9.如果有错误,交给错误解析器去解析
**DispatcherServlet**
```java
DispatcherServlet初始化时机?
1.DispatcherServlet是由容器创建,tomcat进行初始化
2.DispatcherServlet是第一次访问时被初始化,也可以修改配置启动后就初始化
DispatcherServlet初始化都做了什么?
答:在初始化时会从 Spring 容器中找一些 Web 需要的组件
如 HandlerMapping、HandlerAdapter 等,并逐一调用它们的初始化
```
**HandlerMapping**
```
HandlerMapping 有什么用?
1.先到当前容器里面找到所有的控制器类
2.之后查看控制器类里面的方法,找出加了RequestMapping注解的方法
作用.解析RequestMapping注解以及派生注解,生成路径与控制器方法的映射关系,初始化方法生成
```
**HandlerAdapter**
```java
一:HandlerAdapter 有什么用?
1.初始化时,会准备 HandlerMethod 调用时需要的各个组件
2.HandlerMethodArgumentResolver 解析控制器方法参数:用来解析添加注解的参数
HandlerMethodReturnValueHandler 处理控制器方法返回值:用来解析返回参数,变成json,yml啥的
作用:通过各种参数解析器和返回值解析器,来解析数据比如 @RequestParam,@RequestBody 之类的来执行控制器方法
二:HandlerAdapter的调用过程?
1.控制器方法封装为 HandlerMethod
2.准备对象绑定和类型转换,就是将传递过来的参数数据传入到设定好的对象当中
3.将传递好的对象数据存入到 model对象里面
4.解析每个参数值
三:怎么解析参数?
1.通过各种各样的参数解析器来解析,判断有没有各种注解
四:那些注解标注的要被参数解析器解析
1.@RequestParam
2.省略 @RequestParam(不添加这个也能被解析)
3.@RequestParam(defaultValue)
4.MultipartFile
5.@PathVariable
6.@RequestHeader:
7.@CookieValue
8.@Value
9.HttpServletRequest 等
10.@ModelAttribute
11.省略 @ModelAttribute
12.@RequestBody
这些注解通过解析器组合来解析,单个解析器解决不了这么多,体现了组合模式的运用?
```
**controller**
```Java
1.当 RequestMappingHandlerMapping 将路径和控制器方法进行映射之后
2.由 RequestMappingHandlerAdapter 去准备数据绑定工厂来绑定数据,这时自己添加的模型绑定方法也就是
添加了@InitBinder注解的方法也会加入数据绑定工厂.
3.创建模型工厂,用来创建模型数据(也就是javaBean对象之类的),同时将添加了@ModelAttribute注解的方法也放入进去
将这个方法的返回值也放入模型工厂里面创建
4.创建 ModelAndViewContainer,将创建好的模型对象统一进行封装
5.接下来调用 ServletInvocableHandlerMethod 他会进行 5.准备参数 6.反射调用控制器方法 7.处理返回值
细说一下 ServletInvocableHandlerMethod ?
一.准备参数,通过下面的来实现
1.WebDataBinderFactory 负责对象绑定、类型转换
2.ParameterNameDiscoverer 负责参数名解析
3.HandlerMethodArgumentResolverComposite 负责解析参数
二.反射调用控制器方法
他继承了 HandlerMethod 里面有 当前的bean是哪个bean,当前的方法是哪个方法
三.处理返回值
HandlerMethodReturnValueHandlerComposite 负责处理返回值
```
**返回值处理器**
```java
ModelAndView,分别获取其模型和视图名,放入 ModelAndViewContainer
试图解析器体现出了组合模式的设计模式
返回值处理器的作用?
就算将返回值统一转换为 ModelAndView 存入 ModelAndView容器里面。然后进行解析
但是随着前后端分离,现在一般都转化为模型数据,视图交给前端
有模型有视图
1.返回值类型为 String 时,把它当做视图名,放入 ModelAndViewContainer
2.返回值添加了 @ModelAttribute 注解时,将返回值作为模型,放入 ModelAndViewContainer
此时需找到默认视图名
3.返回值省略 @ModelAttribute 注解且返回非简单类型时,将返回值作为模型,放入 ModelAndViewContainer
此时需找到默认视图名
只有模型:setRequestHandled为ture,表示已经处理过了,不再需要那些试图那些步骤
1.返回值类型为 ResponseEntity 时
此时走 MessageConverter,并设置 ModelAndViewContainer.requestHandled 为 true
2.返回值类型为 HttpHeaders 时
会设置 ModelAndViewContainer.requestHandled 为 true
3.返回值添加了 @ResponseBody 注解时
此时走 MessageConverter,并设置 ModelAndViewContainer.requestHandled 为 true
```
**异常解析器**
```java
ExceptionHandlerExceptionResolver
1.在执行参数解析器,调用控制器方法,处理返回值的过程中
如果出现了异常,就会收集起来,交给异常处理解析器,其中最重要的异常解析器就是
2.ExceptionHandlerExceptionResolver,他可以解析 @ExceptionHandler 注解
对于异常进行处理,处理过程中也可以将异常信息进去处理转换之类的,比如将map转换为json进行输出
3.它能够重用参数解析器、返回值处理器,实现组件重用
4.它能够支持嵌套异常,就是可以 Exception 嵌套 RuntimeException 嵌套 ArithmeticException
```
### springBoot
**springboot的启动过程**:
```java
阶段一:SpringApplication 构造
1. 记录 BeanDefinition 源
从配置类或者xml文件里面去获取到一些bean的定义信息
2. 推断应用类型
根据项目里面的一些jar和关键类推断应用类型,来选择不同的容器
就是查看在类路径下有什么类并且没有什么类就选择容器
3. 记录 ApplicationContext 初始化器
在 refresh 之前,对于 ApplicationContext 进行一些功能扩展
4. 记录监听器
监听spring.run方法中发布的一写事件
5. 推断主启动类
找到这个项目的启动类
阶段二:执行run方法
1.得到事件发布器(一共会发布7个事件):去发布 Application starting 事件(1)
2.封装启动args,对于传递到main参数分为两类,选项参数和非选项参数
3-6主要获取到环境属性和环境遍历之类的,例如 yml文件,properties文件
3.准备 Environment
作用:创建Environment,去获取一些命令行的选项参数
4.ConfigurationPropertySources
作用:将配置文件里面的不同的命名规范,横杠,点,驼峰统一转换为-号格式
5.通过 Environment后处理器进行env后处理 处理:发布 application environment 已准备好事件(2)
作用:解析配置文件,从而得到配置信息
6.绑定 spring.main开头的配置信息 到 SpringApplication(启动类) 对象里面
7.打印banner
8.创建容器: 就是创建一个容器
9.准备容器: 发布 application context 已初始化事件(3)
作用:当容器创建好了之后,将之前创建好的初始化器拿过来,执行容器增强效果
10.加载 bean 定义:发布 application prepared 事件(4)
作用:从不同的来源去加载bean定义对象用来创建bean,从配置类,xml,扫描路径之类的
11.refresh容器:发布 application started 事件(5)
作用:refresh容器里面有很多详细步骤
12.执行 runner:发布 application ready 事件(6)
这其中有异常,发布 application failed 事件(7)
```
**springBoot的自动装配原理**:
springboot的自动装配原理就藏在@SpringBootApplication这个注解中,他其实是一个组合注解包含了:
- @SpringBootConfiguration:表明这是一个配置类,可以在这里配置bean
- @ComponentScan:组件扫描,默认的扫描路径是启动类所在的包
- @EnableAutoConfiguration:这个就是自动配置的核心,也是一个组合注解
- @Import里面导入了一个类,是一个自动配置的类,这个类会从jar包下面的META-IN/spring.factories中获取信息
- 而META-IN/spring.factories的文件里面包含的就是一些需要自动配置的类的信息,从而来实现自动配置
- 我们知道这个原理之后,可以按照spring的自动配置规则来加载我们自己编写的类
# **达人-探店 后端开发**
**达人-探店 后端开发 2023.1 —— 2023.9**
**项目简介:**给用户发布自己体验过店铺的经验和店铺自己发布信息招揽顾客的平台。用户可以发布经验贴子来评价店铺,此外还有点赞,抢购优惠券,查看笔记,店铺等,店家身份发布自家店铺的优惠券以及活动来吸引顾客
**技术栈**:SpringBoot,Redis,xxl-job,RabbitMQ,MySQL,Mybatis,Canal
**项目难点**:
- **点赞优化**:为了提高点赞接口性能。将点赞逻辑判断放到redis,通过定时器将点赞数据刷新到数据库,对接口进行限流防止恶意刷赞,在保证高可用的同时,性能提升近7倍
- **消息可靠性:**基于最终一致性的思想,确保抢购订单消息的可靠性,并且对于订单消息堆积和消息重复等问题提供了解决方案
- **抢购优化**:为实现抢购商品优化。从将 (请求尽量拦截在系统上游,充分利用缓存,同步拆分异步) 三个角度出发,并进行限流,在单机模式下,从QPS: 423--->QPS:2243
- **数据一致性:**用于解决缓存和数据一致性问题,通过canal监听数据库的改变,将删除缓存的操作交给MQ,利用重发机制保证缓存一定删除,尽可能降低与业务代码解耦合。
- **redis工具:**为了简化代码开发,自实现redis工具包,提供注解限流器,分布式锁,布隆过滤器,以及针对缓存穿透,击穿,以及缓存一致性,等系列问题提供了解决方案。
## 点赞优化
**MySQL设计**:
(点赞消息表)点赞人id,被点赞博客id,点赞状态(0:未点赞,1:点赞) 这三个核心字段
(博客表)包含点赞数量
**Redis设计**:
点赞数量:sortSet,里面包含了所有博客的点赞数(value:博客id,sorce:点赞量+时间戳)方便做排行榜功能
点赞记录:HashMap,我们可以把 前缀+用户id作为大key,博客id作为小key,然后把点赞状态作为value,这样做的目的是减少大key的问题,因为一个点赞1w篇博客比较罕见,但是一篇博客被1w人点赞很常见
**定时器设计**:
通过scan查找前缀找出所有的点赞的map集合,这么做是为了防止redis阻塞
将每一个点赞的map集合的点赞数据刷新到点赞中间表里面。
查找出数据库不存在的数据,从集合里面分离出来,存在的数据进行状态修改(交给两个线程)
**代码逻辑**:
1. 当用户点击点赞的时候,我们可以通过判断用户有没有点赞(如何查看点赞状态,我们可以从redis里面查询,如果redis里面没有,查询数据库判断点赞状态)
2. 如果没点赞就点赞数量+1,记录到点赞记录表里面
3. 如果点赞了就点赞数量-1,修改redis里面的点赞状态
**性能测试**:通过Jmeit进行1000个人点赞
**优化点**:
- 对于点赞接口进行限流,防止某个用户恶意点击点赞按钮
- 使用lua脚本,将(点赞数量和点赞记录)这两个操作打包,保证原子性,并且提高效率
- 使用管道,将redis里面的数据批量刷新到数据库,避免查询阻塞,提高效率
## 消息可靠性
实现一个消息的公共模块
**数据库设计**:
本地消息表:(消息id,消息体,routing_key,消息状态,重试次数,是否死亡,发送时间)
消息防重表:用来防止消息的重复消费
**核心逻辑设计**:
1.消息预发送:生产者在执行业务之前,先将消息保存到本地,并且设置状态为未发送
2.生产者执行业务:在生产者执行完业务后,真正发送消息(注意1,2要放在一个事务中)
3.发送消息:开始真正发送消息给消费者,将消息状态设置为发送中
4.消费者接收:接收到消息之后,手动进行ACK操作,把这个事务存放到唯一消息表里面。(开启事务)
5.生产者接收到ACK之后,将这条消息状态设置为消费完毕
**定时器设计**:
定时器不断从消息表里面,获取到发送中的消息,不断重新发送,如果超过5次还是失败的话,标记为死亡交给人工处理
其他的分布式事务的方案?
**MQ设计:**
生产者哪里设置开启了confirm模式,消费者开始手动ack模式,队列,交换机,消息,都要进行磁盘落地
## **抢购优化**
**秒杀优化方向有三个方向**
**将请求尽量拦截在系统上游**:传统秒杀系统之所以挂,请求都压倒了后端数据层,数据读写锁冲突严重,几乎所有请求都超时,流量虽大,下单成功的有效流量甚小,我们可以通过限流、降级等措施来最大化减少对数据库的访问,从而保护系统。
- 使用限流操作对接口进行限流,避免过多的无效请求进入
- 在秒杀阶段使用本地标记对用户秒杀过的商品做标记,若被标记过直接返回重复秒杀,未被标记才查询redis,通过本地标记来减少对redis的访问
- 抢购开始前,将商品和库存数据同步到redis中,所有的抢购操作,比如判断库存是否充足,是否重复下单,乐观锁扣减库存,都在redis中进行处理,通过Redis预减少库存,只有预检库存成功的用户才算抢单成功
**充分利用缓存**:秒杀商品是一个典型的读多写少的应用场景,充分利用缓存将大大提高并发量
- 页面缓存 + 对象缓存
- 页面缓存:通过在手动渲染得到的html页面缓存到redis
- 对象缓存:包括对用户信息、商品信息、订单信息等数据进行缓存,利用缓存来减少对数据库的访问,大大加快查询速度。
- 页面静态化
- 对商品详情和订单详情进行页面静态化处理,页面是存在html,动态数据是通过接口从服务端获取,实现前后端分离,静态页面无需连接数据库打开速度较动态页面会有明显提高
**同步拆分异步:**
- 在执行数据库扣减库存之后,直接返回用户的订单号信息,将保存订单号的操作发送消息给redis,直接返回订单号给用户,提升用户体验
**其他细节:**
1. **限流是怎么做的?**
- 对同一用户进行限流:通过redis的自增特性和拦截器,防止用户通过软件大量点击抢购接口
- 加验证码:在秒杀之前添加验证码
- 提高抢购能力资格:提高用户的抢购成本
2. **如何保证消息的可靠性,以及防止重复订单信息,消息堆积?**
- 在上面
3. **怎么测的QBS?**
- 通过jmetr来进行的测试
4. **超卖问题?**
- 通过缓存预热将优惠券的信息,例如秒杀时间段,库存等信息缓存到redis里面
- 先判断用户是否已经下过单,如果已经下过单,直接返回交给内存标记
- 判断库存是否充足,库存充足通过乐观锁的方式来进行扣减库存
- 这些操作交给lua脚本,保证原子性
- 数据库层面,减库存的时候同时判断此时库存是否大于0。
5. **一人一单?**
- 添加一个消息防重表,只有先插入消息成功,才能执行保存订单的业务
- 数据库层面,秒杀的订单表设置唯一索引,用来做兜底操作
6. **接口安全性操作?**
- 客户端第一次请求秒杀接口的时候,会通过用户id和商品id为key加密生成一个随机数据,然后将这个随机数保存一份到redis,然后返回给前端
- 前端获取到这个随机数会立即请求真实的秒杀接口
- 真实的秒杀接口会从redis里面获取到数据,进行比对,比对成功才能进行真实的秒杀
7. **资源隔离**:
- 秒杀活动带来的请求流量巨大,我是将秒杀的缓存额外放到一个库里面,这是单机的情况,在集群模式下,我们需要把秒杀商品的库存信息用单独的实例保存,而不要和日常业务系统的数据保存在同一个实例上,这样可以避免干扰业务系统的正常运行。
## **redis工具包**
- **AOP分布式锁**:通过注解添加到方法上,添加环绕通知,代码执行前加锁,执行完毕解锁。
- **限流器**:通过注解里面包含两个属性,代表着在多少时间最多执行多少次。然后通过一个拦截器判断拦截的方法上是否有这个注解。如果有,则当第一次访问时生成一个redis缓存初始值为1,之后每次访问初始值就+1. 当记录数超过注解里面设置的最大次 数时经行拦截
- **布隆过滤器**:
- **缓存击穿:**通过互斥锁和逻辑过期两种方式来实现
- **缓存穿透:**通过缓存空值和布隆过滤器来实现
- **缓存一致性:**
## **其他问题**
- **能简单介绍一下你的这个项目吗?**
- 这个项目是一个用来降低用户踩雷概率的项目,一般我们想要一家店铺好不好吃只能自己去吃,或者去搜这个店铺,结果全是刷的评论。这个就可以通过查看网友的经验博客来判断当前这家店铺适不适合自己,比较独特的是支持店铺跟网友互动,可以发布一些自家店铺的优惠券来进行吸引顾客。
- **为什么设计他?**
- 因为现在的这种网红店铺太多,眼花缭乱。很难找得到一家称心如意的店铺。为了减少用户试错成本,我们就可以开发一个这样的软件,但是目前来说只是实现了基本功能,无法预知那种有一定用户量的情况下是否会有问题
- **有哪些优秀的设计?**
- 点赞功能
- 抢购功能
- 缓存一致性
- **遇到的最大的困难是什么?**
- 点赞功能:该功能好的实现方案较少,大部分都是自己慢慢总结出来的。如何设计redis数据,如何设计数据库数据。如何优化接口性能都是我自己来实现的
- 秒杀功能:涉及到的细节,知识点太多了
- **它还有什么不足?**
- 登录功能不够完善
- 秒杀功能,秒杀接口没有隐藏,页面静态化没有做
- **redis存储了什么数据?**
- 在用户登录这里,使用redis缓存验证码。
- 缓存博客数据到sortSet,做了一个博客排行榜。使用redis缓存店铺的点赞数
- 缓存点赞数据,使用redis缓存了点赞信息
- 在秒杀场景中,手动渲染页面进行缓存,抢购优惠券的信息,缓存接口隐藏数据,接口幂等性缓存接口信息
- 限流注解中,缓存一个注解的限流信息
- 唯一ID生成器,缓存唯一ID数据
- 分布式锁,缓存锁信息
- **MQ在哪里使用了?**
- 秒杀那里做了异步操作
- 缓存和数据库的一致性
- **点赞排行榜是怎么设计的?**
- 因为点赞是一个整数嘛,redi的分数是一个小数。所以我们可以给定一个固定的很大时间戳。
- 当每一次更新的时候,我们可以拿到 (大时间戳—小时间戳)/(大时间戳)作为小数,拼接到点赞量里面
- 这样就不仅可以获取到一个正确的点赞量,还能通过点赞量和时间进行排序
- **redis空间不足怎么办?**
- 增加Redis的内存:可以通过修改Redis配置文件中的内存大小来增加Redis的空间。
- 定期清理Redis中的过期数据:可以通过定期触发Redis的过期命令来清理过期的数据,以释放空间。
- 修改内存淘汰策略:可以修改为lru,默认是不删除的策略
- 横向扩容:可以通过分布式部署Redis来扩展Redis的空间。可以将数据分散到多个Redis实例中,以减轻单个实例的压力。
- **redis等系列场景八股?**
- redis为什么这么快?
- redis持久化机制?
- redis如何保证高可用性?
- redis内存不足了怎么办?
- redis大key问题如何解决?
- redis查找热key?
- 如果想要查找一些key应该怎么实现?
- **rebbit等系列八股?**
- 用的什么mq?
- mq有什么效果?
- 作用在了那些场景上?
- 为什么使用mq?
- 线程池也可以做异步,为什么不用线程池异步?
- **缓存一致性怎么做的?**
- 通过cannal监听修改操作,当修改操作之后将交给mq进行删除,删除多次失败转人工处理,超时时间兜底
- 这么做的好处是,与业务代码不耦合,确保了修改之后缓存一定会删除
- **接口幂等性?**