知识点整理
2023-08-08 15:46:02 0 举报
AI智能生成
知识点整理
作者其他创作
大纲/内容
JUC
1.基础概念
<b>并行:</b>同一时刻都在执行<br><b>并发:</b>同一时间段都执行,依赖CPU快速切换<br><br><b>进程:</b>系统进行资源分配和调度的基本单位<br><b>线程:</b>线程是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源,线程是 CPU分配的基本单位。
2.创建线程
继承Thread类,重写run()方法<br>实现 Runnable 接口,重写run()方法<br>实现Callable接口,重写call()方法,可以通过FutureTask获取任务执行的返回值<br> <br>JVM执行start方法,会先创建一条线程,由创建出来的新线程去执行thread的run方法,这才起到多线程的效果。
3.ThreadLocal是什么?
线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到<b>线程隔离</b>的作用,避免了线程安全问题。
4.ThreadLocal实现
Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,每个线程都有一个属于自己的ThreadLocalMap。<br><br>ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal的弱引用,value是ThreadLocal的泛型值。<br><br>每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。<br><br>ThreadLocal本身不存储值,它只是作为一个key来让线程往ThreadLocalMap里存取值。
<font color="#a23735">ThreadLocal提供线程局部变量:它实现了让每一个线程都有自己专属的本地变量副本,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。</font><br> <br>在不使用某个ThreadLocal对象后,<b>要记得remove()</b>,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题<br> <br><font color="#a23c73">SimpleDateFormat是线程不安全的,在使用时一般不要定义为静态的,如果静态就必须加锁,不然就会报错。</font><br>原因:SimpleDateFormat 类内部有一个Calendar对象引用,它用来储存和这个 SimpleDateFormat 相关的日期信息,如果你的 SimpleDateFormat 是个 static 的,那么多个 thread 之间就会共享这个 SimpleDateFormat,同时也是共享这个Calendar引用。<br><br> 解决:1.将SimpleDateFormat定义成局部变量<br> 2.ThreadLocal<br> 3.DateTimeFormatter 代替 SimpleDateFormat<br> <br><b>Thread 类里边有一个 ThreadLocal,ThreadLocal 里边有一个 ThreadLocalMap,ThreadLocalMap 里边实际干活的是一个 Entry。<br>threadLocalMap 实际上就是一个以 threadLocal 实例为 key,任意对象为 value 的 Entry 对象。</b>
5.线程池作用
管理线程,避免增加创建线程和销毁线程的资源损耗<br>提高响应速度<br>重复利用
6.多线程工作流程
创建线程池,调用execute()方法添加一个任务:<br>1.如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;<br>2.如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;<br>3.如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;<br>4.如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会根据拒绝策略来对应处理。
7.线程池七大参数
corePoolSize:<b>核心线程数</b><br>maximumPoolSize:<b>最大线程数</b><br>keepAliveTime:<b>非核心线程闲置存活时间</b><br>unit:keepAliveTime的<b>时间单位</b><br>workQueue:<b>等待队列</b><br>threadFactory:创建一个新线程时使用的<b>工厂</b><br>handler:<b>拒绝策略</b>
8.线程池拒绝策略有哪些?
1.直接抛异常(默认)<br>2.用调用者所在的线程来执行任务<br>3.丢弃阻塞队列里最老的任务,也就是队列里靠前的任务<br>4.当前任务直接丢弃
9.线程池提交execute和submit有什么区别?
1.execute 用于提交<b>不需要返回值的任务</b><br>2.submit()方法用于提交<b>需要返回值的任务</b>。线程池会返回一个future类型的对象,通过这个 future 对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值
10.线程池关闭
调用 shutdown() 方法,将线程池状态置为shutdown,并不会立即停止:<br>1.停止接受外部提交任务<br>2.执行完当前任务和等待队列任务<br>3.停止<br> <br>shutdown()方法的原理是<b>逐个调用线程池中工作线程的interrupt方法来中断线程。</b>
11.多线程的重要性?
硬件:摩尔定律失效<br>软件:高并发系统,异步+回调等生产需求
12.认识多线程
在new一个线程,调用他的start()方法时,其实是调用了Thread类里边的本地start0()方法,这个方法与底层的C++代码相对应,底层实际是对操作系统的调用。<br><br>线程的daemon属性为true表示是守护线程,false表示是用户线程<br>管程:Monitor(监视器),就是我们平时所说的锁
13.Future和CompletableFuture
<b>Future:</b>JUC下的一个接口,定义了操作异步任务执行的一些方法。<br>FutureTask调用get()方法会阻塞等待结果出来再运行,所以一般get()要放最后使用或者设置超时时间。<br>轮询比阻塞好一点,但是还会浪费一些CPU资源。<br><br><b>CompletableFuture扩展了Future:</b>它实现了Future 接口和 CompletionStage 接口,他有函数式编程和回调处理能力,能代表一个完成阶段,计算完成后触发某些动作。它有默认的线程池,也可以指定线程池。它在异步任务结束或出错时,能自动调用某个方法;主线程设置好回调后,异步任务之间可以顺序执行。
<b>例子:</b><br>如果现在有一些网站物品价格初始数据串,我们想要进行比价,可以将这些数据保存到Redis的Zset里边,保证数据没有重复,然后进行比较,<br>第一种笨办法就是一条数据一条数据的过,但是这样性能不高;<br>第二种,在JUC里边,有一个 CompletableFuture,用他做多线程异步并发是不阻塞的,而且可以自定义线程池,线程池根据数据量的大小伸缩,在数据量变大时仍能保证效率,大大提升程序性能。
14.各种锁
<b>乐观锁:</b>无锁编程,采用CAS实现,适合读多场景<br><b>悲观锁:</b>synchronized和lock的实现都是悲观锁,适合写多场景<br><b>公平锁:</b>按顺序获得锁(避免锁饥饿)<br><b>非公平锁:</b>直接尝试获取,获取不到再进入等待队列(减小线程开销,会导致锁饥饿)<br><br>如果为了更高的吞吐量,非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;否则就用公平锁,大家公平使用。<br><br>可重入锁(递归锁):在一个 synchronized 修饰的方法或代码块的内部调用本类的其他 synchronized 修饰的方法或代码块时,是永远可以得到锁的。<br><br><b>重入原理:</b>每个锁对象拥有一个锁计数器 和 一个指向持有该锁的线程的指针,当执行 monitorenter 时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。<br>在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。<br>当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。<br><br><b>死锁:</b>死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象(竞争资源,进程运行推进的顺序不合适,资源分配不当)
15.中断
一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。中断只是一种协作机制,中断的过程完全需要我们自己实现。<br><br>实例方法<b>interrupt()仅仅是设置线程的中断状态为true</b>,不会停止线程。<br>isInterrupted()通过检查中断标志位,<b>判断当前线程是否被中断</b>。<br>静态interrupted()方法:<b>返回当前线程的中断状态并将中断状态设为false</b><br><br>中断标识停止线程:1、volatile标记;2、AtomicBoolean标记;3、interrupt() + isInterrupted()<br><br><font color="#ff0000">如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常</font><br> <br>线程等待唤醒机制:<br>1、Object 的 wait() + notify();<br>2、JUC包中 Condition 的 await() 方法让线程等待,signal() 方法唤醒线程;<br>3、LockSupport类中的park等待和unpark唤醒,不需要锁块,无顺序要求(一个线程只能发一张通行证,比如 t1 park一次,t2可以 unpark 唤醒,如果 t1 park 了两次,就需要两个线程 t2 park 一次,t3 park 一次,以此类推,一个线程只能唤醒一次阻塞。)
16.JMM内存模型
这个规范定义了<b>程序中各个变量的读写访问方式</b>,以及一个线程对共享变量的写入和对另一个线程的可见。<br> <br><b>三大特性:</b><br><font color="#ff0000">1、可见性:</font>一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更。<br>普通变量不保证可见性,因为所有变量存储在主内存,每个线程都有自己的工作内存,操作变量时,线程需要将主内存变量拷贝到工作内存进行。<br><font color="#ff0000">2、原子性:</font>一个操作是不可中断的<br><font color="#ff0000">3、有序性:</font>一个线程的执行并不总是从上到下,编译器和处理器会进行重排序提高性能,指令重排单线程环境可确保程序最终执行结果和代码顺序执行的结果一致,多线程环境中线程交替执行,结果无法确定。<br> <br><b>Happens-Before规则:</b><br>如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
17.volatile
<b>volatile用来保证可见性、有序性(禁止令重排):</b>就是在写一个volatile变量时,写完后会立即刷新回主内存,读一个volatile变量时时,会直接从主内存读取。<br> <br>它的底层采用的是<font color="#a23c73">内存屏障</font>,内存屏障是一组JVM指令,也叫同步点,作用就是保证 此点之前的所有读写操作都执行完,才可以执行此点之后的操作。<br><br>它底层其实是 利用Unsafe类本地方法loadFence(),storeFence(),调用底层C++四大屏障:loadload(),storestore(),loadstore(),storeload()。<br> <br><b>volatile变量不保证原子性,不适合参与到依赖当前值的运算</b><br> <br>使用场景:1、单一赋值时。2、一次性标志。3、读远多于写的变量,读时volatile保证可见性,写时synchronized保证原子性。4、双重检查锁的单例模式,使用volatile禁重排,防止获取对象为空。
18.CAS
<b>CAS是JDK提供的、通过硬件保证的非阻塞原子性操作:</b>执行CAS操作的时候,会将内存位置的值与预期原值比较:如果相匹配,那么处理器会自动将该位置值更新为新值,如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功。<br><br><font color="#a23c73">CAS的原子性实际是由CPU实现的,如果是多核系统,还会先给总线加锁,只有一个线程会对总线加锁成功。</font><br><b>Unsafe是CAS的核心类</b>,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,其内部方法操作可以像C的指针一样直接操作内存。<br> <br>存在问题:1、长时间循环开销大。2、ABA问题。<br><b>如何解决:</b><br>AtomicStampedReference : 维护一个对象引用以及一个整数“标记”,可以原子方式更新。携带版本号的引用类型原子类,可以解决ABA问题,可以记录修改过几次<br>AtomicMarkableReference : 维护一个对象引用以及一个标记位,可以原子方式更新。一次性的,只能记录是否修改过,因为它将状态标记简化为了Boolean 的 true/false
19.原子操作类,LongAdder
AtomicInteger、AtomicBoolean、AtomicLong、AtomicIntegerArray、AtomicReference <br> <br>对象的属性修改原子类: 以一种线程安全的方式操作非线程安全对象内的某些字段。<br>AtomicIntegerFieldUpdater 原子更新对象中int类型字段的值<br>AtomicLongFieldUpdater 原子更新对象中Long类型字段的值<br><b>使用要求:</b>1、更新的对象属性必须使用 public volatile 修饰符。 2、因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。<br> <br>原子操作增强类:<br>DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder<br>LongAdder只能用来计算加法,且从零开始计算,LongAccumulator提供了自定义的函数操作。<br><br><b>LongAdder的基本思路就是分散热点</b>,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。<br><br>LongAdder在无竞争的情况,跟AtomicLong一样,对同一个base进行操作,当出现竞争关系时则是采用化整为零的做法,从空间换时间,用一个数组cells,将一个value拆分进这个数组cells。多个线程需要同时对value进行操作时候,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组cells的所有值和无竞争值base都加起来作为最终结果。
20.强、软、弱、虚四引用
强引用:最常见,如一个对象赋值给一个引用变量,当对象被强引用变量引用时,处于可达状态,不可回收。<br>软引用:常用于内存敏感的程序,如高速缓存,内存够就保留,不够就回收。<br>弱引用:垃圾回收机制一运行就回收。<br>虚引用:和没引用一样,任何时候都可能被回收,常用于跟踪对象回收状态,对象被回收时通知。
21.软引用实例
假如有一个应用需要读取大量的本地图片:如果每次读取图片都从硬盘读取则会严重影响性能,如果一次性全部加载到内存中又可能造成内存溢出。<br><br>此时使用软引用可以解决这个问题:<br>用一个HashMap来保存 图片的路径 和 相应图片对象关联的软引用 之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。
22.ThreadLocal中的内存泄露问题
<b>指不再会被使用的对象或者变量占用的内存不能被回收。</b><br><br>ThreadLocalMap中的Entry的key指向一个ThreadLocal,这个ThreadLocal就被WeakReference 包装成了一个弱引用。<br> <br>当ThreadLocal的外部强引用t1被置为null时,根据可达性分析,这个ThreadLocal就会被回收,这样一来,ThreadLocalMap 中就会出现一个 key 为 null 的 Entry,在使用线程池的情况下,为了复用,我们是不会结束线程的,长时间就会造成内存泄露。
23.对象在堆内存中的存储布局
<b>对象头:</b><br> Mark Word:存储对象的 HashCode、分代年龄 和 锁标志位等。<br> 类型指针:指向类元数据的指针<br><b>实例数据:</b>存放类(包含父类)属性,数组长度。<br><b>对齐填充:</b>不必须存在,为了对齐8字节整数倍
24.Synchronized
在Java5以前,Synchronized是重量级的,因为monitor依赖操作系统的Lock,需要用户态和内核态之间的转换,效率较低。<br><br><font color="#a23735">Java6开始,引入了轻量级锁和偏向锁,让锁有个逐步升级的过程。</font><br>每个对象天生就是Monitor锁,如果一个java对象被某个线程锁住,则该java对象的Mark Word字段中LockWord指向monitor的起始地址,Monitor的Owner字段会存放拥有相关联对象锁的线程id。
25.锁升级
<b>1.无锁</b><br><b>2.偏向锁:</b><br>多线程下,锁总被同一个线程持有:在第一次获得锁时,记下偏向线程ID,以后不需要再次加锁和释放锁,而是直接比较对象头里面存储的偏向线程ID是否是它,是就直接进入,不是则意味着发生了竞争,需要锁升级。<br><font color="#a23c73">偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。</font><br><b>3.轻量级锁:</b><br>CAS自旋,Java6之前默认10次,之后变为自适应<br><b>4.重锁:</b>大量线程竞争,冲突很高
26.JIT即时编译器
锁消除:每个线程new自己new对象当做锁,相当于没加,没用<br>锁粗化:连续几个synchronized 使用同一个锁对象,编译器将他们合并加大范围
27.AQS(抽象队列同步器)
通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态。<br><br>锁面向程序员,而同步器面向锁的实现者,而在JUC中,加锁就会导致阻塞,有阻塞就需要排队,实现排队必然就需要队列。<br><br>抢到资源的线程执行,抢不到的线程排队等候(仍然保留获取锁的可能且获取锁流程仍在继续)<br><br>AQS使用一个volatile的int型成员变量表示同步状态,把要去抢占资源的线层加入AQS的等待队列(CLH队列变体),他将请求线程封装成一个Node,通过CAS、自旋以及 LockSupport.park() 的方式,维护state变量的状态,使并发达到同步的效果。
子主题
28.ReentrantReadWriteLock(读写锁)
一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程(读读共存,读写互斥)
RabbitMQ
1.生产者和消费者都属于客户端
子主题
2.服务端:虚拟主机,交换机,队列
<b>虚拟主机:</b>对 交换机 和 队列 逻辑隔离<br><b>交换机:</b>接收生产者发来的消息,并分配给队列<br><b>队列:</b>存储消息,消费者从这里取
3.连接和信道
一个连接包含多个信道,连接是线程共享的,信道是线程私有的<br>因为连接使用TCP连接,需要三握四挥,每个线程建立一个连接比较耗资源
4.工作模式
<b>简单模式</b><br>生产者将消息发送给队列,消费者从队列中获取消息队列<br><br><b>工作队列模式</b><br>消费者从一个变成了多个<br> <br><b>广播模式</b><br>增加了交换机,生产者只生产了一条消息,它对应的所有消费者都能全部接收
5.使用场景
<b>1.异步处理</b><br>用户注册 -- 保存数据库 -- 发送短信 -- 发送邮件<br>用户将注册信息提交保存到数据库,然后将注册成功的消息保存到消息中间件,然后直接返回。其他服务从消息中间件获取消息,该发短信发短信,该发邮件发邮件。<br> <br><b>2.应用解耦</b><br>订单系统下订单,库存系统减库存<br>如果订单系统直接调用库存系统,库存系统升级了,订单系统也要跟着升级。<br>可以让订单系统下完订单后,将订单信息发送给消息队列,库存系统实时订阅消息队列,有订单信息时自己分析参数减库存。<br> <br><b>3.流量控制</b><br>大量请求时,全部处理业务,可能将资源耗尽<br>可以将请求先存储到消息队列里边,然后直接返回成功,其他业务服务订阅消息队列,根据它自己的消费能力挨个进行处理,防止宕机。
6.RabbitMQ工作流程
<b>生产者</b>与消息代理建立一条长连接,在连接里边开辟多条 channel来发送消息,每一个消息必须指定路由键,消息到达消息代理服务器,会交给交换机,交换机根据自己和消息队列的绑定关系,决定将消息放到哪个队列里边。<br><br><b>消费者</b>与消息代理服务器建立长连接,连接里边有多个信道,用于监听消息队列,监听到消息来消费
7.Exchange类型
<b>direct</b><br>单播,点对点通信,路由键和队列完全匹配<br><b>fanout</b><br>广播到绑定它的所有队列<br><b>topic</b><br>部分广播模式,使用通配符匹配单词(#:0个或多个单词;*:一个单词)<br>headers
8.消息确认机制--可靠抵达
<b>生产者投送消息给Broker保证</b><br>confirmCallback 确认回调,消息只要被broker接收到就会执行<br><b>Exchange交给Queue保证</b><br>returnCallback 回调,消息没有投递给指定的queue,触发这个失败回调<br><b>Queue到 消费者</b><br>ack确认机制,消费者从queue中成功拿到消息,会给Broker返回一个ack<br><br><b>问题:</b>自动确认模式,如果消息收到一半宕机了,MQ会删除所有消息,导致数据丢失。所以我们采用手动确认方式,处理一个回复一个。一旦consumer宕机,所有 unacked 会回到ready。
9.1、为什么使用消息队列?
<b>解耦</b><br>多个子系统之间需要交互数据,就需要考虑哪些系统都需要这些数据?后边哪个系统不需要了或者哪个系统又需要了?需要数据的系统挂了怎么办?要不要重发?要不要把消息存起来?<br><font color="#a23735">管理这样的耦合关系,就可以用MQ</font><br>如果使用 MQ,A 系统产生一条数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。这样下来,A 系统压根儿不需要去考虑要给谁发送数据,也不需要考虑人家是否调用成功、失败超时等情况。<br><b>异步</b><br>A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 + 450 + 200 = 953ms,就特别慢。<br><font color="#a23c73">一般互联网类的企业,对于用户直接的操作,一般要求是每个请求都必须在 200 ms 以内完成,对用户几乎是无感知的。</font><br>如果使用 MQ,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms,8s后直接返回,真快。<br><b>削峰</b><br>每天 0:00 到 12:00,A 系统风平浪静,每秒并发请求数量就 50 个。结果每次一到 12:00 ~ 13:00 ,每秒并发请求数量突然会暴增到 5k+ 条。但是系统是直接基于 MySQL 的,大量的请求涌入 MySQL,每秒钟对 MySQL 执行约 5k 条 SQL。<br><font color="#a23735">一般的 MySQL,扛到每秒 2k 个请求就差不多了,如果每秒请求到 5k 的话,可能就直接把 MySQL 给打死了,导致系统崩溃,用户也就没法再使用系统了。</font><br>但是高峰期一过,到了下午的时候,就成了低峰期,可能也就 1w 的用户同时在网站上操作,每秒中的请求数量可能也就 50 个请求,对整个系统几乎没有任何的压力。<br><br>如果使用 MQ,每秒 5k 个请求写入 MQ,A 系统每秒钟最多处理 2k 个请求,因为 MySQL 每秒钟最多处理 2k 个。A 系统从 MQ 中慢慢拉取请求,每秒钟就拉取 2k 个请求,不要超过自己每秒能处理的最大请求数量就 ok,这样下来,哪怕是高峰期的时候,A 系统也绝对不会挂掉。而 MQ 每秒钟 5k 个请求进来,就 2k 个请求出去,结果就导致在中午高峰期(1 个小时),可能有几十万甚至几百万的请求积压在 MQ 中。<br>这个短暂的高峰期积压是 ok 的,因为高峰期过了之后,每秒钟就 50 个请求进 MQ,但是 A 系统依然会按照每秒 2k 个请求的速度在处理。所以说,只要高峰期一过,A 系统就会快速将积压的消息给解决掉。<br>
JVM
1.JVM内存区域
<b>程序计数器:</b>用于存放下一条指令所在单元的地址<br> <br><b>虚拟机栈:</b>线程私有,生命周期与线程相同。描述的是 Java 方法执行的线程内存模型,用来存储局部变量表、操作数栈、动态连接等<br> <br><b>本地方法栈:</b>和<b style="">虚拟机栈</b>相似,它是为虚拟机用到的本地方法服务<br> <br><b>堆:</b>线程共享,虚拟机启动时创建,目的是存放对象实例。根据垃圾回收机制划分为:新生代(Eden,s0,s1),老年代<br> <br><b> 方法区:</b>各线程共享,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。<br> <font color="#a23735"> jdk1.6,使用永久代实现方法区<br> jdk1.7,将字符串常量池、静态变量,存放在堆上<br> jdk8开始,用元空间代替永久代:元空间与永久代的区别:元空间不在虚拟机设置的内存中,而是使用本地内存</font><br>
2.对象创建的过程
<b>从一个new指令开始:</b><br>1.在常量池中定位这个类的符号引用<br>2.检查这个引用指向的类是否被加载过,没有才开始类加载<br>3.加载完后分配内存<br>4.然后初始化零值<br>5.设置对象头
3.分配内存的方式
内存规整:指针碰撞<br>不规整 :维护一个空闲列表
4.new对象会发生抢占吗?JVM怎么保证线程安全?
<b>会,方案如下:</b><br>1.采用 CAS 分配重试的方式来保证更新操作的原子性<br>2.每个线程在 Java 堆中预先分配一小块内存,也就是本地线程分配缓冲(TLAB),要分配内存的线程,先在本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。
5.对象内存布局
<b>对象头:</b>存hash码,分代年龄,锁信息和类型指针<br><b>实例数据:</b>真正信息<br><b>对齐填充:</b>占位
6.内存泄露和内存溢出
<b>内存泄露:内存空间没有被正确释放</b><br>原因:<br>1.静态集合生命周期和JVM一致,它引用的对象不能被释放。<br>2.静态遍历引用的对象也不能被释放。<br>3.不再使用对象没有及时将对象设置为 null<br>4.ThreadLocal 使用不当<br> <br><b>内存溢出:内存不够了</b>
7.判断对象是否存活
<b>引用计数器:</b><br>在对象中添加一个计数器,每有一个地方引用它,计数器就加一,引用失效时减一,为0就说明是垃圾了<br><br><b>可达性分析:</b><br>将GC Roots作为初始存活对象集,从它出发,把所有能被引用的对象加到集合中,也就是标记,最后,未被标记的就是可以回收的。<br> <br><b>GC Roots对象:栈中引用对象,静态属性引用对象,常量引用对象</b>
8.finalize()方法
对象被回收前调用该对象的finalize()方法,这是它逃脱死亡的最后一次机会
9.对象分配步骤
1.new的对象先放伊甸园区。此区有大小限制。<br><br>2.当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中不再被其他对象所引用的对象进行销毁<br><br>3.然后将伊甸园中的剩余对象移动到幸存者0区。<br><br>4.如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。<br><br>5.如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。<br><br>6.啥时候能去养老区呢?可以设置次数。默认是15次。可以设置参数: -XX:MaxTenuringThreshold=进行设置。<br><br>7.在养老区,相对悠闲。当养老区内存不足时,再次触发GC: Major GC,进行养老区的内存清理。<br><br>8.若养老区执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常
10.垃圾收集算法
<b>标记-清除算法:</b><br>标记阶段,在可达对象的对象头中标记<br>清除:对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收<br>(清除就是把需要清除的对象地址保存在空闲的地址列表里,下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够就存放)<br><font color="#a23735">缺点:会产生碎片,GC时需要停顿,效率不高</font>
<b>复制算法:</b><br>将活着的内存空间分为两块,每次使用其中一块。在垃圾回收时,将正在使用的内存中的存活的对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有的对象,交换两个内存的角色,最后完成垃圾回收<br><font color="#a23c73">它实现简单不会出现碎片,但浪费内存空间</font>
<b>标记压缩算法:</b><br>标记:从根节点开始标记所有被引用的对象<br>清除:将所有的存活对象压缩在内存的一端,按照顺序排放,之后清理边界外所有的空间<br><font color="#ff0000">最终效果等同于标记清除算法执行完成后,再进行一次内存碎片整理。</font>
<b>分代收集算法:</b><br>不同生命周期的对象可以采取不同的收集方式,以便提高回收效率<br>新生代的存活对象比较少:适合复制算法<br>老年代使用:标记-整理算法
11.GC
Minor GC:指目标只是新生代的垃圾收集,Eden满时触发<br><br>Major GC:指目标只是老年代的垃圾收集<br><br>Full GC:收集整个 Java 堆和方法区的垃圾收集,触发:Young GC 之后老年代空间不足;System.gc()命令
12.对象什么时候进入老年代
长期存活对象:分代年龄达到15(默认可修改)<br>大对象直接进入老年代
13.Serial 收集器
<b>单线程收集器</b><br>进行垃圾收集时,必须暂停其他所有工作线程,直到垃圾收集结束(STW)<br><br>新生代:标记-复制算法<br>老年代:标记-压缩算法
14.ParNew 收集器
Serial 收集器的多线程并行版本,使用多条线程进行垃圾收集。
15.Parallel 收集器
基于标记-复制算法实现,能够并行收集<br><br>它主要关注的是垃圾收集的<font color="#ff0000">吞吐量</font>(单位时间处理请求的数量)
16.Serial Old,Parallel Old
Serial, Parallel 的老年代版本,采用标记-压缩算法
17.CMS 收集器
<b>并发标记清除收集器</b>,jdk1.5推出,它第一次实现了让垃圾收集线程与用户线程同时工作,它是老年代的收集器,采用标记-清除算法。<br> <br>初始标记:仍是STW,仅标记GC Roots直接关联的对象<br>并发标记:从GC Roots直接对象开始全局遍历,时间较长但无需停顿<br>重新标记:STW,标记并发标记阶段产生对象<br>并发清除:并发清理死亡对象<br> <br> 优点:并发收集,低延迟<br> 缺点:<br> 会产生碎片<br> 在并发阶段会占用一部分线程导致应用程序变慢<br> 如果并发阶段产生垃圾对象,CMS无法进行标记,没有及时回收
子主题
18.G1收集器
<b>区域化分代式,在延迟可控的情况下,获得尽可能高的吞吐量</b><br> <br>他把堆分成很多不相关的区域,使用不同的region表示Eden,s0,s1,老年代等,G1跟踪各个region里面垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,避免整堆收集<br> <br>初始标记:标记了从 GC Root 开始直接关联可达的对象,STW执行。<br>并发标记:并发执行,从 GC Root 开始递归扫描整个堆里的对象图,找出要回收的对象。<br>最终标记:STW,标记再并发标记过程中产生的垃圾。<br>筛选回收:制定回收计划,选择多个 Region 构成回收集,把回收集中 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。需要 STW。<br> <br><b>优点:</b><br>并行并发<br>分代收集兼顾年轻代与老年代<br>region之间用复制算法,整体可以看做是标记压缩算法,避免了内存碎片<br>可预测停顿时间
19.双亲委派机制<br>
1、如果一个类加载器收到了类加载请求,它并不会自己先去加载。而是把这个请求委托给父类的加载器去执行<br>2、如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将达到顶层的启动类加载器<br>3、如果父类的加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载<br> <br><b>作用:</b><br>避免类的重复加载<br>保护程序安全,防止核心API被篡改
子主题
分布式
1.CAP原则
<b>一致性,可用性,分区容错性</b><br><br><font color="#ff0000">分布式系统分区是必然存在的,在分区容错的前提下, ⼀致性和可⽤性是⽭盾的。</font><br><br><b>BASE理论:</b><br>基于CAP演化而来,核心思想是 即便不能达到强一致性,也可以根据应⽤特点采⽤适当的⽅式来达到最终⼀致性的效果。
2.幂等性
<b>同⼀个接⼝,多次发出同⼀个请求,请求的结果是⼀致的。</b><br> <br>问题情景:<br>1.提交form表单快速点了两次,表中出现两条相同记录,只是id不同<br>2.接口超时重试机制,其实可能成功了<br>3.mq消费者有时会读取到重复消息,也会产生重复数据
3.怎么保证接口幂等性
1.insert前先select<br>2.加唯一索引,重复插入会抛异常,我们捕获这个异常<br>3.加悲观锁:更新时把对应行数据锁住,同⼀时刻只允许⼀个请求获得<br>锁,其他请求则等待。缺点是获取不到锁就报失败了。<br>4.加乐观锁:表中增加version字段,在更新前,先查询⼀下数据,将version也作为更新的条件,同时也更新version<br>5.状态机:有状态的业务(订单:下单,已支付,完成等),可以限制状态流动实现幂等<br>6.分布式锁<br>7.token机制:请求接⼝之前,需要先获取⼀个唯⼀的token,再带着这个token去完成业务操作,服务端根据这个token是否存在,来判断是否是重复的请求。
Java核心
基础
1.重载
发生在同一个类:<br>1、方法名相同,参数列表顺序、类型、个数不同;<br>2、与返回值无关;<br>3、可以不同修饰符,可以抛出不同异常<br>
2.重写
发生在子父类之间,重写方法返回类型相同,不能比父类抛出更多异常。
3.抽象类与接口
接口方法默认public,接口中不能有实现(Java8 开始可以有默认实现),只能有static、final 变量,接口多实现。接口是对行为的抽象,是一种行为规范。<br><br> 抽象类可以有非抽象方法,抽象类单继承,抽象方法不能private修饰,因为它就是为了让别人重写。抽象是对类的抽象,是一种模板设计。<br> <br> 接口演化:<br> jdk7之前:接口中只能有常量和抽象方法,接口方法必须实现<br> jdk8:可以有默认方法和静态方法<br> jdk9:引入了私有方法和私有静态方法
4.为什么重写 quals 时必须重写 hashCode ⽅法?<br>
如果两个对象相等,则 hashcode ⼀定也是相同的。两个对象相等,对两个对象分别调⽤ equals ⽅法都返回 true。<br> 反之,两个对象有相同的 hashcode 值,它们也不⼀定是相等的 。因此,equals ⽅法被覆盖过,则 hashCode ⽅法也必须被覆盖。<br> hashCode()默认产生独特值,即使两个对象指向相同数据。
5.深拷贝和浅拷贝<br>
浅拷贝:拷贝基本数据类型的值和引用数据类型的地址。Object 类提供的 clone()方法就是浅拷贝<br><br>深拷贝:完全拷贝一个对象(浅拷贝 + 堆中对象也拷贝一份)。实现:重写克隆方法、序列化。<br>
6.创建对象的方式
new 创建新对象<br>通过反射机制<br>采用 clone 机制<br>通过序列化机制
7.String 和 StringBuilder、StringBuffer<br>
String:类被final修饰,不可变类,不能被继承。<br>StringBuffer:可修改,使用synchronized保证线程安全。<br>StringBuilder:StringBuffer 的非线程安全版本,性能上更高一些。<br> <br>在Java8 时JDK 对“+” 号拼接进行了优化,“+” 拼接方式会被优化为基于 StringBuilder 的 append 方法进行处理。Java 会在编译期对“+” 号进行处理。<br>
8.String s = new String("abc") 创建了几个对象?<br>
如果字符串常量池已经有“abc”,则是一个;<br>否则,两个。常量池一个,堆一个<br>
9.intern 方法<br>
如果当前字符串内容存在于字符串常量池,直接返回字符串常量池中的字符串;<br>否则,将此 String 对象添加到池中,并返回 String 对象的引用。<br>
10.反射
Java 程序的执行分为编译和运行两步,编译之后会生成字节码(.class)文件,JVM 进行类加载的时候,会加载字节码文件,将类型相关的所有信息加载进方法区,反射就是去获取这些信息,然后进行各种操作。<br>
11.Java8新特性
接口默认方法:Java 8 允许我们给接口添加一个非抽象的方法实现,只需要使用 default 关键字修饰即可<br><br>Lambda 表达式和函数式接口:Lambda 表达式本质上是一段匿名内部类,也可以是一段可以传递的代码,允许把函数作为一个方法的参数。<br><br>Stream API:高效处理数据<br>新日期时间 API<br>Optional 类:用来解决空指针异常的问题
12.集合容器:快速失败和安全失败<br>
快速失败:<br>A线程用迭代器遍历一个集合对象时,B不能对它修改,否则抛出并发修改异常。因为迭代器在遍历时直接访问集合中的内容,遍历中有一个 modCount 变量,集合被修改就会改变 modCount 的值,迭代器使用 hashNext()/next() 遍历下一个元素前,都会检测 modCount 是否为期望值,是就遍历,不是就抛异常终止。但是如果修改集合是 modCount 刚好又设置成了期望值,就不会抛出异常了,所以不适合并发。<br><br>安全失败:<br>遍历时先复制原有集合内容,在拷贝的集合上遍历。它不会出发并发修改异常,但迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。<br>
13.流的特性
<b>先进先出:</b>最先写入输出流的数据最先被输入流读取到。<br><br><b>顺序存取:</b>可以一个接一个地往流中写入一串字节,读出时也将按写入顺序读取一串字节,不能随机访问中间的数据。(RandomAccessFile除外)<br><br><b>只读或只写:</b>每个流只能是输入流或输出流的一种,不能同时具备两个功能,输入流只能进行读操作,输出流只能进行写操作。在一个数据传输通道中,如果既要写入数据,又要读取数据,则要分别提供两个流。<br> <br><b>核心4个抽象类:</b>InputStream、OutputStream、Reader、Writer。<br><br>字节流可以处理一切文件,而字符流只能处理文本。<br>
集合框架<br>
map
1.HashMap
<b>结构:</b><br>哈希表:数组(存储元素) + 链表(解决冲突) + 红黑树(提高查询效率)<br>初始大小为16<br>链表长度>8 且 数组大小>=64 转为红黑树<br>红黑树结点 < 6 转为链表<br>
<b>扩容:</b><br>在进行扩容操作时,HashMap 会先将数组的长度扩大一倍,然后将原来的元素重新散列到新的数组中。<br>
<b>加载因子0.75(泊松分布):</b><br>用来表示 HashMap 中数据的填满程度,扩容的临界值 = 初始容量 * 加载因子<br>
<b>哈希函数:</b><br>根据key.hashcode(),是一个32位的int值,然后让hashcode的高16位和低16位进行异或操作。
<b>PUT流程:</b><br>1.判断table数组是否为 null 或 lenth=0,是扩容,否继续<br>2.根据key计算哈希值<br>3.根据哈希值计算下标<br>4.判断该下标位置是否为null,是直接添加,否,判断table[i]首个元素是否和要插入的一样,一样就覆盖<br>5.不一样就判断table[i]是否是红黑树,如果是红黑树,直接在树中插入键值对。<br>6.如果不是,遍历链表长度,大于8就转成红黑树插入,不大于就链表中插入<br>7.插入成功,判断size是否超过最大容量,超了就扩容。
<b>解决哈希冲突:</b><br>链地址法(在用)<br>其他:线性探测法,平方探测法,再哈希法
<b>jdk1.8对HashMap主要做了哪些优化</b><br>1.数据结构:数组+链表 --> 数组+链表+红黑树<br>2.链表插入方式:头插法 --> 尾插法<br>3.扩容:重新计算数组位置 --> 不用重新计算,新位置不变或者为 (索引 + 新增容量大小)<br>4.扩容时机:判断是否需要扩容,再插入 --> 先插入,再判断扩容<br>5.散列函数:四次移位和四次异或 --> 一次<br>
<b>HashMap是线程安全的吗?</b><br>不是,多线程下 1.7头插法可能导致环形链表,形成死循环,1.8尾插法解决。<br>多线程 put 可能导致元素丢失,一个key被另一个 key 覆盖,导致元素丢失<br>put 和 get 并发,put 导致扩容,get就可能为null<br>
<b>解决线程不安全问题?</b><br>HashTable、Collections.synchronizedMap、以及 ConcurrentHashMap可以实现线程安全的 Map<br>HashTable直接在操作方法上加synchronized<br>
HashMap无序,要想有序可以使用 LinkedHashMap 或者 TreeMap<br>
2.ConcurrentHashmap
ConcurrentHashMap 内部采用了分段锁(Segment),将整个 Map 拆分为多个小的 HashMap,每个小的 HashMap 都有自己的锁,不同的线程可以同时访问不同的小 Map,从而实现了线程安全。在进行插入、删除和扩容等操作时,只需要锁住当前小 Map,不会对整个 Map 进行锁定,提高了并发访问的效率。<br>
<b>ConcurrentHashmap的实现?</b><br> 1.7采用分段锁:里面包含一个Segment数组,Segment里边是HashEntry的数组,HashEntry是链表结构,可以保存key,value并指向下一个节点。<br> 相当于每个Segment都是一个HashMap,默认的Segment长度是16,也就是说支持16个线程并发写。put时先定位具体Segment,然后通过ReentrantLock去操作,后边和HashMap相同。<br> <br> 1.8采用CAS+synchronized:它和HashMap数据结构相同,利用put流程实现线程安全。<b>在写入和扩容时加上CAS自旋和synchronized锁。</b><br>
3.红黑树
本质是<b>二叉查找树</b>,二叉查找树有一个明显的不足,就是容易变成瘸子,就是一侧多,一侧少,为了保持平衡,加了一些规则:(旋转,染色)<br><br>1.节点要么红,要么黑<br>2.根节点永远黑<br>3.所有叶子节点都黑<br>4.每个红色节点的两个子节点一定都黑<br>5.任一节点到子树路径黑色数量相同<br> <br>红黑树是一种<b>平衡的二叉树</b>,插入、删除、查找可以避免最坏情况复杂度。<br>平衡二叉树比红黑树更加严格,保持平衡旋转次数更多,效率更低。<br>
4.LinkedHashMap
一个<b>有序的Map</b>,可以看作是 HashMap + LinkedList 的合体,使用链表来记录插入/访问元素的顺序<br><br><b>LinkedHashMap实现有序?</b><br> LinkedHashMap维护了一个双向链表,有头尾节点,它的节点 Entry 内部除了继承 HashMap 的 Node 属性,还有 before 和 after 用于标识前置节点和后置节点,可以实现按插入的顺序或访问顺序排序。<br> 可以按照插入顺序或访问顺序来遍历键值对(LRU 最不经常访问的放在头部,访问谁谁被放最后)<br>
5.TreeMap
实现了 SortedMap 接口,可以<b>自动将键按照自然顺序或指定的比较器顺序排序</b>,并保证其元素的顺序。内部使用红黑树来实现键的排序和查找。<br> <br><b>TreeMap 实现有序?</b><br> TreeMap 是按照 Key 的自然顺序或者 Comprator 的顺序进行排序,内部是通过红黑树来实现。所以要么 key 所属的类实现 Comparable 接口,或者自定义一个实现了 Comparator 接口的比较器,传给 TreeMap 用于 key 的比较。<br>
List
1.ArrayList
由<b>数组</b>实现,支持<b>随机存取</b>,尾部插入删除较快,中间插入删除涉及元素移动较慢,容量不足时会自动扩容到1.5倍<br><br> 实现了Cloneable接口,支持拷贝。<br> 实现了 Serializable 接口,支持序列化,但他的<b>elementData 使用了 transient 关键字修饰</b>,自己内部提供了两个私有方法 writeObject 和 readObject 。<br> 因为ArrayList支持动态扩容,所以序列化是肯定有空闲空间,浪费资源,而自己实现的私有方法使用实际大小 size 而不是数组的长度序列化。<br>
2.LinkedList
由<b>双向链表</b>实现,不支持随机存取,<b>随机增删快</b><br>支持拷贝,可以按照自己的方式序列化(只保留了元素的内容 item,并没有保留元素的前后引用),反序列化时,linkLast() 方法可以把链表重新链接起来<br> <br>可以用于实现 LRU缓存淘汰算法:<br>当缓存空间不够时,优先淘汰最近最少使用的缓存数据。使用 LinkedList 来存储缓存数据,每次访问缓存数据时,将该数据从链表中删除并移动到链表的头部,这样链表的尾部就是最近最少使用的缓存数据,当缓存空间不够时,只需要将链表尾部的缓存数据淘汰即可。
3.实现ArrayList线程安全的方法
在使用 ArrayList 时,使用同步机制去控制<br> <br>使用 Collections.synchronizedList() 包装 ArrayList,然后操作包装后的 list。<br> <br>使用 CopyOnWriteArrayList,他是线程安全版的 ArrayList。它采用一种读写分离的并发策略,读操作无锁,允许并发读,性能较高;写时先将容器复制一份,然后在副本上执行写操作,再将原容器的引用指向新容器。<br>
Set
1.HashSet
由HashMap实现,键用于操作,值固定用一个Object填充
2.LinkedHashSet
继承自 HashSet,由 LinkedHashMap 实现,并且<b>使用链表维护了元素的插入顺序</b>,它既具有HashSet的快速查找、插入和删除操作的优点,又可以维护元素的插入顺序<br>
3.TreeSet
<b>由 TreeMap 实现,操作的是键,值由一个固定的 Object 对象填充。<br></b><br>TreeSet 是一种基于红黑树实现的有序集合,它实现了 SortedSet 接口,可以自动对集合中的元素进行排序(自然顺序或指定比较器)<br>TreeSet 不允许插入 null 元素,否则会抛出 NullPointerException 异常<br>
Queue
1.ArrayDeque
一个基于数组实现的<b>双端队列</b>,可以同时在数组两端插入或删除元素,数组必须是循环的。<br> 当需要使用栈时候,请首选ArrayDeque,因为Stack 是一个“原始”类,它的核心方法上都加了 synchronized 关键字以确保线程安全。<br> ArrayDeque是非线程安全的,并且不允许放 null 元素。<br>
2.PriorityQueue
<span style="font-size: inherit;"> 一种</span><b style="font-size: inherit;">优先级队列</b><span style="font-size: inherit;">,它的出队顺序与元素的优先级有关,执行 remove 或者 poll 方法,返回的总是优先级最高的元素。</span><br><span style="font-size: inherit;"> </span><span style="font-size: inherit;">它不支持直接修改元素,需要先删除再添加</span><br><span style="font-size: inherit;"> 要想有优先级,元素就需要实现 Comparable 接口或者 Comparator 接口</span><br>
MySQL
1.处理请求流程<br>
客户发起请求 <font color="#e74f4c">--></font> MySQL接收请求,三次握手建立TCP连接 <br><font color="#e74f4c">--></font> 身份认证,从线程池分配交互线程 <font color="#e74f4c">--></font> 解析SQL语句,创建语法树 <br><font color="#e74f4c">--></font> SQL优化,生成执行计划 <font color="#e74f4c">--></font> 执行器调用存储引擎api <font color="#e74f4c">--></font> 文件系统读取存储数据
2.InnoDB和MyISAM区别
<b>InnoDB(5.5后默认):</b><br>支持事务,支持外键,支持行级锁,索引是聚簇索引,数据结构是 B+树<br>8.0前两张表保存(1表结构,2数据和索引)<br>8.0合并为了一张表<br><br><b>MyISAM:</b><br>不支持事务,不支持外键,只支持表级锁,索引为非聚簇索引,数据结构是B树<br>崩溃后无法安全恢复<br>保存了三张表,访问快,适合主要读的业务<br><font color="#a23c73">MyISAM索引文件和数据文件分离,索引仅保存数据地址,它的索引都是非聚簇的,每次都要进行一次回表,并且回表是根据地址直接去拿,比较快速</font><br>
3.为什么使用索引
<b>索引是一种数据结构,存储引擎用它来加快查询效率,减少磁盘I/O次数。<br></b>缺点:<br>创建维护耗费时间空间,修改表数据也要维护,降低更新表的速度。
4.InnoDB索引演化<br>
<b>表中的数据以页为单位存储,表中的每条记录以单链表连接,页之间通过双向链表连接<br></b><br>当数据量少时,可以被放在一个页中:如果按照主键查找,主键一般是递增的,可以使用二分查找;如果以其他列作为条件搜索,只能遍历,效率较差。<br><br>当数据量大点,一页存不下时,第一步就需要先定位页,然后按上述查找。<br><br>如果没有索引,就不能快速定位所在的页,只能从第一页开始按顺序找。<br><br>基于这种情况,考虑可以给所有的页建立一个目录项,让目录项连续依次递增,每个目录项记录页的最小记录和页号,就可以利用二分法查找目录项,快速定位到所在页。<br><br>随着数据的增大,目录项越来越多难以管理,考虑将目录项以单链表的方式存储,所有目录项也存储到一个页中,把它叫做目录页。<br><br>数据再增大,导致一个目录页存不下,考虑将目录项放在多个目录页中,目录页之间逻辑连续,采用双向链表链接。<br><br>当目录页大于一个时,考虑将目录页再向上抽取一个更高级的页。<br><br>这样就一步步演化出了B+树<br><br><font color="#ff0000">B+树 非叶子节点仅用于索引,不保存数据记录,跟记录有关的信息都放在叶子节点中。而B树中,非叶子节点既<br>保存索引,也保存数据记录。</font><br>
5.索引分类
<b>聚簇索引:</b><br>所有完整用户记录都存储在叶子节点,记录之间单向链表相连,索引即数据,数据即索引<br>这种索引自动创建,数据访问快,无需回表,但插入速度严重依赖插入顺序,更新主键代价较高<br><br><b>非聚簇索引(二级索引):</b><br>可以有多个,不保存完整记录,需要回表操作<br><br><b>联合索引:</b>同时为多个列建立索引,本质上也是二级索引<br><br><font color="#a23c73">从功能逻辑上来说,分为:普通索引、唯一索引、主键索引、全文索引。<br>按照物理实现来分,分为:聚簇索引 和 非聚簇索引。<br>按照作用字段个数来分,分成:单列索引 和 联合索引。</font><br>
6.适合创建索引的情况
1.唯一性约束的字段<br>2.频繁使用where查询的字段<br>3.经常GROUP BY 和 ORDER BY 的列<br>4.UPDATE,DELETE 时的 where 条件列<br>5.DISTINCT的字段<br>6.多表JOIN连接时创建索引注意<br> 连接表的数量尽量不超过3张,每加一张表就相当于嵌套一次循环<br> 对 where 条件创建索引<br> 用于连接的字段创建索引<br>7.尽量给数据类型小的列添加索引,节省空间,让一页存在更多记录,比较速度页更快<br>8.使用区分度高的列作为索引<br>9.联合索引时,使用最频繁的列放在联合索引的左侧<br>
7.不适合创建索引的情况
1.where,group by ,orderby 条件使用不到的字段不要加索引<br>2.数据量小的表不要添加索引<br>3.有大量重复数据的列不要添加索引<br>4.经常更新的表尽量少创建索引<br>5.不建议使用无序的值作为索引<br>6.不再使用或很少使用的索引及时删除
8.数据库服务器优化
1.观察服务器是否存在周期性波动,是的话可以尝试加缓存或者调整缓存失效策略<br>2.开启慢查询日志<br>3.分析SQL语句:EXPLAIN, SHOW PROFILE<br>4.如果SQL等待时间较长:调整服务器参数<br>5.如果SQL执行之间较长:优化索引,SQL语句本身,表设计<br>6.还不行考虑达到瓶颈,考虑读写分离,分库分表<br><br>定位问题:<br>用户反馈、日志分析、系统监控(服务器资源监控,数据库内活动会话监控)<br><br>调优步骤:<br>首先,要选择适合的DBMS<br>第二,优化表设计:表间关系,存储引擎,三范式和反范式化,数据类型选择<br>第三,逻辑优化:SQL语句<br>第四,物理优化:索引的创建和使用<br>第五,考虑使用缓存<br>第六,库级优化:读写分离,分库分表<br><br>数据库结构优化:冷热分离,增加中间表,冗余字段,优化字段数据类型<br>
9.索引失效的情况
1.最佳左前缀规则:对于多列索引,过滤条件要使用索引必须按照索引建立时的顺序,依次满足,一旦跳过某个字段,索引后面的字段都无法被使用。<br><br>2.计算,函数,类型转换导致索引失效<br><br>3.范围条件右边的列索引失效:所以在创建索引时,务必把涉及范围查找的字段放在最后<br><br>4.不等于索引失效<br><br>5.is null可以使用索引,is not null无法使用索引<br><br>6.like以通配符%开头索引失效
10.覆盖索引、索引下推
<b>覆盖索引:</b>一个索引包含了满足查询结果的数据就叫做覆盖索引。<br><br>好处:避免回表,可以把随机IO变成顺序IO加快查询效率(覆盖索引是按键值的顺序存储的)<br><br><b>索引下推:</b>key1是索引,如下查询 EXPLAIN SELECT * FROM s1 WHERE key1 > 'z' AND key1 LIKE '%a';<br><br>优化器会 先用索引查询条件 key1 > 'z',不回表,继续在这些索引中过滤条件 key1 LIKE '%a',最后只回表一次<br><br><font color="#a23735">ICP可用于InnoDB 和 MyISAM,对于InnoDB表,ICP仅用于二级索引,覆盖索引时不支持ICP</font><br>
11.三范式
1.每个字段原子不可再分<br>2.不能产生部分依赖(所有非主键字段完全依赖主键)<br>3.不能产生传递依赖(数据表中的每一个非主键字段都和主键字段直接相关)<br>
12.事务ACID与隔离级别
<b>原子性:</b>事务不可再分,要么同时成功,要么同时失败<br><b>一致性(语义上):</b>事务执行前后,数据从一个 合法性状态 变换到 另外一个合法性状态<br><b>隔离性:</b>一个事务的执行不能被其他事务干扰<br><b>持久性:</b>一个事务一旦被提交,它对数据库中数据的改变就是永久性的<br><br><b>隔离级别</b><br>读未提交:问题:脏读、幻读、不可重复读<br>读已提交:解决:脏读。 问题:幻读、不可重复读<br>可重复读(mysql默认):解决:脏读、不可重复读。 问题:幻读<br>序列化读:性能低
13.MySQL事物日志
事务的隔离性由锁机制实现,而事务的 原子性、一致性和持久性 由事务的 redo日志 和 undo日志 来保证,<br>都是存储引擎层生成的日志。<b><br><br>redo日志:重做日志,保证事务持久性</b><br>记录的是物理级别上页的修改,主要为了保证数据的可靠性<br><br>InnoDB以页存储,访问页之前,会先把磁盘上的页缓存到Buffer Pull,缓存池中的脏页以一定的频率刷盘;<br>叫 checkPoint机制,它不是每次变更就触发,所以如果刚提交到缓存还没刷盘宕机了,这些数据就丢失了;<br>想要保证数据可靠性,粗暴方法是只要内存中数据修改了就立即刷盘,但这样会导致仅修改了一个字节,就<br>不得不刷新整个页,或者一条修改语句修改了不相邻的多个页,就会多次随机IO<br>这时redo日志就来了,思路是:不频繁刷盘,而是先将所做的修改保存到一个文件里边<br><br><font color="#ff0000">过程:给内存读数据 -> 生成 redo log buffer -> commit追加到 redo log file -> 定期刷新到盘</font><br><br><b>undo日志:回滚日志,保证事务原子性、一致性<br></b>记录的是逻辑操作日志(逆过程),用于回滚和MVCC(一致性非锁定读)<br><br>在事物更新数据之前,有一步写入 undo log 的操作,保证事务原子性<br>写undo log 时,会先申请一个undo页,为每一个事务申请undo页比较浪费内存,所以undo页设计为可重用的<br>当事务提交时,先判断当前undo页是否可重用(使用空间是否小于3/4)<br><br><font color="#a23735">注意区分binlog,bin log是数据库层产生的,一个事物过程中,会一直不断的往redo log顺序记录,而bin log不会记录,直到这个事务提交,才会一次写入到bin log文件中。</font><br>
14.MVCC:多版本并发控制
通过数据行的多个版本管理来实现数据库的并发控制,<b>不用等待一个事务释放锁,就能看到它被更新之前的值<br><br></b>这里有两个概念:<br><b>快照读:</b>基于MVCC,不加锁,提高并发性能,读到的数据不一定是最新的<br><b>当前读:</b>读到的是最新数据,会加锁<br><br>MVCC实现依赖:隐藏字段,Undo log,read view<br><b>隐藏字段两个:</b>当前修改记录事务的ID,undo日志回滚指针(数据改动会记到undo日志,每条undo日志也都有一个回滚指针属性,可以将这些undo日志都连起来,串成一个链表,就是版本链)<br><b>read view:</b>历史快照,记录多个事务对同一数据修改产生的多个版本<br><br><font color="#314aa4">隔离级别:读已提交和可重复读使用到了MVCC(读未提交和序列化用不着)</font><br><br>ReadView结构:<br>creator_trx_id:创建这个ReadView的事物ID<br>trx_ids:创建ReadView时当前系统的活跃的读写事物列表<br>up_limit_id:活跃事物中最小的ID<br>low_limit_id:已提交事物最大的事物ID(1,2,3事物,1、2未提交3已提交,最大事物ID为3+1=4)<br><br><b>ReadView规则:</b><br><font color="#a23735">1.被访问事务id 与 ReadView中事务创建者id 相同,表示访问自己修改过得数据,可以访问<br>2.被访问事务id 小于 ReadView中最小未提交事务id,表示被访问事务已提交,可以访问<br>3.被访问事务id 大于等于 ReadView中最大已提交事务id,不可访问<br>4.被访问事务id 在 最小未提交事务id 和 最大已提交事务id 之间,需要比较 活跃事务列表,在列表中,不可访问,不在,可以访问。</font><br><br><br>过程:<br><font color="#ff0000">获取ReadView -> 查询数据 -> 对比 -> 如果不符合ReadView规则,顺着版本链从undo log中找历史快照 -> 如果找到最后一个版本还不可见,说明此记录对该事务完全不可见 -> 最后返回符合规则的数据。</font><br>
15.数据库日志
<b>慢查询日志:</b>记录时间超过long_query_time的查询<br><b>通用查询日志:</b>记录所有连接所有指令,用于复原实际场景或审计<br><b>错误日志:</b>记录出现的问题<br><b>二进制日志:</b>从服务器用来存放主服务器二进制日志内容的一个中间文件<br><b>中继日志:</b>只在主从服务器架构的从服务器上存在<br><br><font color="#ff0000">怎么保证binlog 和 redolog 一致性:两阶段提交,将 redolog 的写入拆成 预备 和 提交 两个阶段</font><br>
16.主从复制
<b>作用:</b><br>读写分离(读多写少)<br>数据备份(热备份)<br>实现高可用(冗余机制)<br><br><b>原理:</b><br>基于binlog进行数据同步,基于三个线程,一个主库线程,两个从库线程<br>主库线程:从库连接时,主库将二级制日志发送给从库<br>从库I/O线程:连接到主库,向主库发送请求更新binlog,本拷贝到本地中继日志<br>从库SQL线程:读取从库中继日志并执行<br><br><b>过程(三步):</b><br>主库记录binlog -> 从库连接主库将binlog拷贝到自己中继日志 -> 从库重做中继日志事件<br><br><b>如何解决数据一致性问题?<br><font color="#a23735">异步复制:</font></b>客户端提交COMMIT之后不需要等从库返回任何结果,而是直接将结果返回给客户端,<br>这样不会影响主库写效率,但可能会存在<font color="#ff0000">主库宕机,而Binlog还没有同步到从库的情况<br></font><br><b style=""><font color="#a23735">半同步复制:</font></b>客户端提交COMMIT之后不直接将结果返回给客户端,而是<font color="#ff0000">等待至少有一个从库接收到了Binlog,并且写入到中继日志中,再返回给客户端。<br></font>MySQL5.7增加了一个参数,对应答的从库数量进行设置,默认为1<br><br>上边两个都无法最终保证数据一致性<br><font color="#a23735"><b>组复制:</b></font>基于Paxos协议的状态机复制<br>将多个节点共同组成一个复制组,读写事务想要进行提交,必须要经过组里“大多数人”(对应Node节点)的同意(N/2+1),这样才可以进行提交,而不是原发起方一个说了算。<br><br>而针对只读事务则不需要经过组内同意,直接COMMIT即可。<br><br>在一个复制组内有多个节点组成,它们各自维护了自己的数据副本,并且在一致性协议层实现了原子消息和全局有序消息,从而保证组内数据的一致性。<br>
redis
1.Redis演化
3.x 单线程<br>4.x 加了异步删除(惰性删除:避免删除大文件卡顿)<br>6.x 多线程IO
2.Redis为什么选择单线程还很快?
基于内存操作<br>单线程数据结构简单<br>多路复用和非阻塞 I/O<br>避免不必要的上下文切换和线程竞争<br><br>对于 Redis 系统来说,主要的性能瓶颈是<b>内存</b>或者<b>网络带宽</b>而并非 CPU。
3.IO多路复用
通过监测文件的读写事件后再通知线程执行相关操作,保证Redis的非阻塞IO能顺利执行的机制。<br><b>多路:多个socket连接</b><br><b>复用:复用一个线程</b><br> <br> 多路复用主要有三种技术:select,poll,epoll<br> epoll是目前最好用的多路复用技术,可以让单个线程高效的处理多个连接请求。
4.工作线程是单线程的,整个Redis是多线程的。
Redis 6.0 将网络数据读写、请求协议解析通过多个IO线程来处理,对于真正的命令执行来说,仍然使用主线程操作。<b>多个线程IO解决网络IO问题,单个工作线程保证安全性。</b><br> <br>多线程机制默认关闭,启动需要在配置文件中开启,线程数配置需小于核数。
5.Redis--String场景
抖音直播连续点赞,微信公众号喜欢作者,阅读量
6.Redis--hash场景
<b>Map<String,Map<Object,Object>></b><br>京东早期手机购物车<br>加入购物车 :hset shopcar : uid 1024 334488 1<br>增加商品数量:hincrby shopcar : uid 1024 334488 1<br>商品总数 :hlen shopcar : uid 1024<br>全选 : hgetall shopcar : uid 1024
7.Redis--list场景
<b>双端链表结构</b><br>微信公众号订阅的消息:关注了A,B两个公众号,A发布文章01,B发布文章02,lpush likeArtical :用户id 01 02<br>商品评论列表:某商品被不同用户评论 【items:comment:1001】为key<br>lpush items:comment:1001 {"id":1001,"name":"huawei","date":1600484283054,"content":"评论"}
8.Redis--set场景
<b>无重复容器</b><br>抽奖:SPOP,随机弹出并删除 ; SRANDMEMBER,随机弹出不删除<br>朋友圈点赞:点赞,取消赞,统计数量,遍历展示<br>微博好友关注社交关系:共同关注(交集)<br>QQ可能认识的人:先交集,再差集
9.set集合运算
sdiff a b : 差集:属于a不属于b<br>sinter a b : 交集<br>sunion a b : 并集<br>
10.Redis--Zset场景
<b>排行榜</b><br>根据商品销售对商品进行排序显示:<br> zadd goods:sellsort 9 1001 15 1002<br> ZRANGE goods:sellsort 0 10 withscores<br>抖音热搜<br><br><font color="#e74f4c">在⾯对需要展示最新列表、排行榜等场景时,如果数据更新频繁或者需要分页显示,建议使⽤ZSet</font>
11.bitmap
用<b>String类型</b>作为底层数据结构实现的一种<b>统计二值状态</b>的数据类型,本质是由<b>多个二进制位组成的数组</b><br>用git获取,得到对应ASCII码值<br><br><font color="#a23c73">场景:二值统计,如是否签到,广告是否被点击,日活</font><br>签到日历实现:如果每个用户每天一条数据,大体量下很可怕。可以使用bitmap,一个月最多31天,使用int刚好32位,一个int类型可以搞定一个月的记录。<br>bitlen,统计字节:8位算一个字节,超过8位扩容<br>bitcount,统计多少个1
12.统计名字
UV:独立访客,也就是客户端IP(去重)<br>PV:页面浏览量(不用去重)<br>DAU:日活<br>MAU:月活
13.hyperloglog
<b>去重统计功能的基数估计算法</b>(基数:一组数去除重复的),它不保存数据,只记录数量,会有0.81%的误差。<br>实践,天猫首页亿级UV统计
14.Redis集群最大槽数为什么是16384?
1、集群要发送心跳包,心跳包包含结点完整配置,其中就有插槽配置,如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大,浪费带宽。<br>2、Redis集群结点不会超过1000个,16384个槽够用了。<br>3、槽位越小,节点少的情况下,压缩比高,容易传输
15.GEO
以给定的经纬度为中心,找出某一半径内的元素<br>实战:美团地图位置附近的酒店推送
16.布隆过滤器
<b>是一个很长的二进制数组(初值为0)+一系列随机hash算法映射函数,主要用于判断一个元素是否在集合中。<br><br>特点:</b><br>1、高效插入查询,占用空间少,但返回结果不确定<br>2、判断结果存在,不一定存在,判断结果不存在,一定不存在<br>3、可添加元素,不可删除元素(删元素误判率增加)<br>4、误判只会发生在过滤器没有添加过的元素<br><br><b>场景:</b><br>解决缓存穿透:<br> -先查询Redis,没有,再查询数据库,也没有,叫缓存穿透。<br> -大量请求穿透查询数据库,会把他拖垮。<br> -可将已存在数据的key存到布隆过滤器中,请求时先到布隆过滤器查询是否存在,不存在直接返回,存在才去Redis,MySQL查询。<br><br><b>原理:</b><br>添加key时,使用多个hash函数对进行哈希运算,得到多个位置,将数字中这几个对应的位置都置为1,查询时,对key多次哈希的位置都为1,则可能存在,有一个为0,就一定不存在。<br><b>所以结论是:</b>有,可能有,无,一定无。
17.缓存穿透
查询一条数据,这条数据既不在Redis,也不再MySQL,但是请求每次都会打到数据库,导致数据库压力暴增,称为缓存穿透。<br><b>解决:</b><br>1、一旦发生穿透,可以根据查询的数据,在Redis中缓存一个空值或者缺省值,避免下次还打到数据库<br>2、Google的布隆过滤器Guava,只能单机用:默认误判率0.03,使用了5个Hash函数,误判率可以自定义,但误判率越小,效率越低。<br>3、Redis布隆过滤器
18.缓存击穿(热点key失效)
大量请求查询一个key,这个key失效了,全部打到了MySQL,导致数据库压力剧增。<br><b>解决:</b><br>1、对于访问频繁的热点key,干脆就不设置过期时间<br>2、互斥独占锁防止击穿<br> <br><b>案例:淘宝聚划算实现(24h高并发) + 防止缓存击穿</b><br><font color="#ff0000">实现:</font>Redis的list,采用定时器扫描数据库,将参与聚划算活动的特价商品新增进入redis的list中,分页从Redis读到页面展示。活动商品到期时删除Redis数据,从数据库读取新一轮活动商品。<br><font color="#ff0000">隐患:</font>在更新活动商品时,删除和插入没有原子性,高并发下,更新时间容易出现热点key失效,缓存击穿。<br><font color="#ff0000">解决:</font>双缓存,定时轮询,互斥更新,差异失效时间<br>开辟两块缓存A和B,AB失效时间不同,更新时先更新B再更新A
19.分布式锁具备条件
独占性:任何时刻只能有且仅有一个线程持有<br>高可用:集群环境下不能因为某个结点挂了出现获取锁或释放锁失败<br>防死锁:超时控制,兜底方案<br>不乱抢:自己加锁只能自己释放<br>可重入:同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁。
20.Redis分布式锁演化
<b>CAP:</b>Redis单机CP, Redis集群AP<br> zookeeper集群CP, Eureka集群AP<br><br><b>1、在单机环境下,可以使用synchronized或Lock加锁</b><br>问题:在分布式系统中,竞争线程可能不在同一节点上,所以需要一个让所以线程都能访问到的锁(Redis或zookeeper)。<br><br><b>2、可以使用setnx分布式锁</b><br>问题:如果出异常的话,可能无法释放锁,所以必须在代码finally释放锁<br>3、问题:如果服务器直接挂了宕机了,没有走到finally,也不能释放锁,需要给key加一个过期时间<br>4、问题:保证自己的锁自己删,别人不能动,还需要在finally中判断value。<br>5、问题:finally块的判断和删除锁不是原子的,可以使用lua脚本保证原子性。<br> <br><b>6、这样也有问题,不能确保redisLock过期时间大于业务执行的时间,如何续期?</b><br>客户端A获取锁,通过exists判断,如果锁不存在,加锁成功,在获取锁成功后,会启动一个 watchdog 后台线程定时任务,每隔10秒检查一次,如果客户端A还持有锁key,那么就会不断的延长锁key的生存时间,默认每次续命又从30秒新开始。<br>当锁不是当前线程的时候,证明其他线程持有锁了,返回当前锁的过期时间,加锁失败。<br>
21.RedLock之Redisson<br>
单机:<br><b>加锁</b> set/setnx + 过期时间<br><b>解锁</b> 先判断key值与锁值是否相等,然后再删 + lua脚本<br> <br>多机:<br>一主多从缺点:线程1获取到锁,写入master,还没同步给slave呢master就故障了,另一slave上位,线程2又拿到了锁,所以两个线程获取到锁了。<br><br>RedLock:多主模式 N = 2X + 1<br>
22.Redisson底层
<b>看门狗 + 三段Luau脚本:</b><br>首次新建,同线程可重入,然后根据返回时间决定锁还有多久过期,过期后触发unlock()和消除分布式锁看门狗<br>
几句话
23.Redis的三大删除策略<br>
<b>立即删除:</b>能保证内存数据最大新鲜度,但性能消耗大,对CPU不友好,用处理器性能换取存储空间。<br> <br><b>惰性删除:</b>数据到达过期时间,不做处理。等下次访问该数据时,如果未过期,返回数据 ;发现已过期,删除,返回不存在。对内存不友好,用存储空间换取处理器性能。<br> <br><b>定期删除:</b>每隔一段时间执行一次删除过期键操作,随机抽取key是否过期删除,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。<br>难点:合理地设置删除操作的执行时长和执行频率。<br>缺点:随机抽取,可能会有漏网之鱼<br><br>以上三种都不完美,所以出现了缓存淘汰策略。。。<br>
24.redis缓存淘汰策略(8种)<br>
<b>2个纬度:</b><br> 过期键中筛选<br> 所有键中筛选<br><b>4个方面:</b><br> LRU<br> LFU<br> random<br> ttl<br> <br>默认为:noeviction: 不会驱逐任何key<br>我们用:allkeys-lru: 对所有key使用LRU算法进行删除<br>
25.Redis底层
redis 是 key-value 数据库,每个键值对都会有一个dictEntry,里面是指向key、value的指针,next指向下一个dictEntry,其中key类型一般为字符串,value 类型则为redis对象(redisObject)<br> <br>Redis定义了redisObject结构体来表示string、hash、list、set、zset等数据类型。它是一个结构体,里边有 type,encoding,lru(最近访问时间戳),refcount,ptr(指向真正数据的指针)<br> <br>bitmap,hyperLogLog 实质是String<br>GEO实质是Zset<br>
数据结构
26.String
Redis没有直接复用C语言的字符串,而是新建了属于自己的结构--SDS,Redis里所有包含字符串的键值队都由SDS实现(所有键+包含字符串的值)<br><br>String有三种编码格式:<br>int:保存long型的有符号整数,8个字节64位,但如果是浮点数,Redis内部会自动将其转化为字符串,再保存。<br>embstr:SDS简单动态字符串,保存长度小于44字节的字符串<br>raw:保存长度大于44字节的字符串
27.Redis为什么重新设计一个 SDS 数据结构?
SDS属性:len,alloc,flags,buf[]<br><br><b>好处:</b><br>1.字符串长度处理<br><font color="#ff0000">C语言用char数组保存字符串,获取字符串长度O(n),SDS直接读取O(1);</font><br>2.内存重新分配<br>C语言可能下标越界或内存溢出<br><font color="#ff0000">SDS可空间预分配,SDS 修改后,len 长度小于 1M,那么将会额外分配与 len 相同长度的未使用空间。如果修改后长度大于 1M,那么将分配1M的使用空间。</font><br><font color="#a23c73">SDS惰性空间释放,SDS 缩短时并不会回收多余的内存空间,而是使用 free 字段将多出来的空间记录下来。如果后续有变更操作,直接使用 free 中记录的空间,减少了内存的分配</font><br>3.二进制安全<br><font color="#ff0000">C语言以 '\0' 作为结束,可能丢失数据,SDS根据 len 长度来判断字符串结束,解决二进制安全的问题。</font>
28.String源码分析
Redis 启动时会预先建立 10000 个分别存储 0~9999 的 redisObject 变量作为共享对象,这就意味着如果 set字符串的键值在这之间的话,可以 直接指向共享对象 而不需要再建立新对象,此时键值不占空间!<br>-------------------<br>set k1 v1时,底层调用setCommand()方法,当v1长度小于等于20时,Redis会将键值转化为long型来进行存储,对应int编码类型,如果其值在0到10000,直接从共享数据拿,否则直接赋值,节省指针开销;<br><br>当v1长度大于20小于等于44时,Redis采用embstr编码,将字符串sds结构体与其对应的 redisObject 对象分配在同一块连续的内存空间,就像嵌入一样,避免内存碎片;<br><br>当v1长度大于44时,redis将采用raw编码,此时动态字符串sds的内存与其依赖的redisObject的内存不再连续了。<br>-------------------<br>对于embstr,由于其实现是只读的,因此在对embstr对象进行修改时,都会先转化为raw再进行修改。因此,只要是修改embstr对象,修改后的对象一定是raw的,无论是否达到了44个字节。
29.hash
底层结构为:ziplist + hashtable<br>当 哈希对象保存的键值对数量小于 512 个,且所有的键值对的健和值的字符串长度都小于等于 64byte时用 ziplist,反之不满足就用hashtable,可升级不可降级。<br>
30.ziplist的结构
本质上是字节数组,可以将其分成三个部分:<b>header + entry集合 + end</b><br> <br>zlentry结构:prev_len + encoding + entry-data<br>prev_len:前一个节点的长度,值可以为1或者5,前一个entry长度小于254,值为1,否则为5。(为什么没有255,因为zlend默认255,其他地方就不能用了)<br> <br>压缩列表的遍历:<br>通过指向表尾节点的位置指针p1, 减去节点的previous_entry_length,得到前一个节点起始地址的指针。<br>
图
31.ziplist的优势
ziplist是一个经过特殊编码的双向链表,它不存储指向上一个链表节点和指向下一个链表节点的指针,而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能,来换取高效的内存空间利用率,节约内存,是一种时间换空间的思想。只用在字段个数少,字段值小的场景里面。<br> <br>优势:<br>1.普通的双向链表会有两个指针,在存储数据很小的情况下,我们存储的实际数据的大小可能还没有指针占用的内存大,得不偿失。ziplist 是一个特殊的双向链表,没有维护双向指针prev next,而是存储上一个 entry的长度和 当前entry的长度,通过长度推算下一个元素在什么地方。牺牲读取的性能,获得高效的存储空间。<br> <br>2.链表在内存中一般是不连续的,遍历相对比较慢,而ziplist可以很好的解决这个问题,ziplist将一些必要的偏移量信息记录在了每一个节点里,使之能跳到上一个节点或下一个节点。<br> <br>3.头节点有一个参数 len,用来记录链表长度的。因此获取链表长度时不用再遍历整个链表,直接拿到len值就可以了,这个时间复杂度是 O(1)。
32.hashtable
在Redis中,hashtable被称为字典,它是一个数组 + 链表结构,可以实现O(1)复杂度的读写操作。
图
33.list
list底层用quicklist来存储,quicklist是一个双向链表,双向链表的每个节点都是一个ziplist
34.set
Redis用 intset 或 hashtable 存储set。如果元素都是整数类型,就用 intset 存储。如果不是整数类型,就用 hashtable(数组+链表的存来储结构)。key就是元素的值,value为null。
35.Zset
当有序集合中包含的元素数量超过配置值(默认为128),或者有序集合中新添加元素的 member 的长度大于配置值(默认为 64 )时,redis会使用跳跃表作为有序集合的底层实现,否则会使用ziplist。<br>
36.skiplist
skiplist是一种以空间换取时间的结构,由链表 + 多级索引组成,是可以实现二分查找的有序链表。<br>实现:索引升级,两两取首。<br>跳表在数据量较大的情况下才能体现出来优势,而且应该是读多写少的情况下才能使用,所以它的适用范围应该还是比较有限的。<br>缺点:维护成本相对较高,新增或者删除时需要把所有索引都更新一遍。
37.MySQL主从复制步骤
1、当 master 主服务器上的数据发生改变时,则将其改变写入二进制事件日志文件中(BinaryLog);<br>2、salve 从服务器会在一定时间间隔内对 master 主服务器上的二进制日志进行探测其是否发生过改变,如果发生了改变,则开始一个 I/O Thread 请求 master 二进制事件日志;<br>3、同时 master 主服务器为每个 I/O Thread 启动一个dump Thread,用于向其发送二进制事件日志;<br>4、slave 从服务器将接收到的二进制事件日志保存至自己本地的中继日志文件中(RelayLog);<br>5、salve 从服务器将启动 SQL Thread 从中继日志中读取二进制日志,在本地重放,使得其数据和主服务器保持一致;<br>6、最后 I/O Thread 和 SQL Thread 将进入睡眠状态,等待下一次被唤醒;
38.canal 工作原理
canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议。<br>MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal ),canal 解析 binary log 对象(原始为 byte 流)
39.缓存双写一致性理解
如果redis中有数据,需要和数据库中的值相同;<br>如果redis中无数据,数据库中的值要是最新值。
40.缓存双写一致性目的:最终一致性
给缓存设置过期时间,是保证最终一致性的解决方案。<br> <br>我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存,达到一致性,切记以mysql的数据库写入库为准。
41.如何保证缓存和数据库一致
什么时候同步直写策略:<br>小数据,某条、某一小挫热点数据,要求立刻变更,可以前台服务降级一下,后台马上同步直写。<br> <br>什么时候异步缓写策略:<br>1.正常业务,马上更新了mysql,可以在业务上容许出现1个小时后redis起效。<br>2.出现异常后,不得不将失败的动作重新修补,不得不借助kafka或者RabbitMQ等消息中间件,实现解耦后重试重写。<br>
42.更新策略
<b>1.先更新数据库,再更新缓存</b><br>问题:数据库更新成功,Redis更新失败的话,会产生数据不一致,从Redis读到脏数据。<br> <br><b>2.先删除缓存,再更新数据库</b><br>问题:<br>A线程先成功删除Redis缓存,然后去更新MySQL,在MySQL还没有更新好时,B要来读取缓存:在高并发下,会出现缓存击穿;在低并发下,B可能会读到数据库未更新的旧数据,并将它写回缓存,A线程白干了,出现了数据不一致,缓存中是B回写的旧值,MySQL是A更新的新值。<br>解决:延时双删策略:当A线程更新完成MySQL之后,延迟一会(大于B线程读取数据再写入缓存的时间),再删除一次缓存。<br><br>延迟时间:需要根据读数据业务逻辑估算,再加上百毫秒,确保读请求结束。<br>MySQL采用主从读写分离,请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值怎么办?睡眠时间修改为在主从同步的延时时间基础上,加几百ms<br>这样等一会导致吞吐量降低,怎么办?第二次删除新起一个线程去执行,异步删除。<br><b> <br>3.先更新数据库,再删除缓存(一般用这个)</b><br>问题:更新数据库后,假如缓存删除失败或者来不及,导致请求再次访问redis时缓存命中,读取到的是缓存旧值。<br>解决:canal思想:先更新数据库,将操作记录到binlog日志,订阅binlog日志,非业务代码获取操作信息,然后删除缓存,删除失败时将这些信息发到消息队列,从消息队列中获取数据重试。
43.Redis单线程如何处理那么多并发客户端连接,为什么快?
Redis利用epoll来实现IO多路复用,将连接信息和时间放到队列中,依次放到文件事件分派器,事件分派器将事件分发给事件处理器。<br><br>I/O 多路复用机制:就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪,能够通知程序进行相应的读写操作。这种机制的使用需要 select 、 poll 、 epoll 来配合。多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。<br><br>Redis 服务采用 Reactor 的方式来实现文件事件处理器(每一个网络连接其实都对应一个文件描述符) <br>Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。<br> <br>因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型<br>
图
44.Reactor设计模式
基于 I/O 复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。<br> <br><b>Reactor 模式中有 2 个关键组成:</b><br>1)Reactor:Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应。<br>2)Handlers:处理程序执行 I/O 事件要完成的实际事件,Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作。
45.IO多路复用(事件驱动型IO)<br>
<b>select():bitmap记录</b><br> <br>流程:select是一个阻塞函数,当没有数据时,会一直阻塞在select那一行,当有数据时,会将rset对应位置置为1,select函数返回不再阻塞,然后遍历,判断哪个fd被置为了,处理数据。<br> <br>优点:select 其实就是把NIO中用户态要遍历的fd数组拷贝到了内核态,让内核态来遍历,因为用户态判断socket是否有数据还是要调用内核态的,所有拷贝到内核态后,这样遍历判断的时候就不用一直用户态和内核态频繁切换了。<br> <br>缺点:<br>bitmap默认1024,可以调但还是有限制<br>rset不可复用,没有重新置0;<br>用户到内核拷贝有开销<br>select返回后还是要遍历<br> <br><b>poll():它搞了一个结构体pollfd</b><br> <br>流程:将多个fd拷贝到内核态,poll为阻塞方法,如果有数据了将对应fd的revents置为POLLIN, poll方法返回,循环遍历找到被置位的,将revents置0以便复用,fd处理。<br> <br>解决问题:(解决了select的前两个问题)<br>解决了bitmap大小限制<br>解决了rset不可重用问题<br> <br><b>epoll()</b><br>流程:epoll是非阻塞的,当有数据的时候,会把有数据的fd放到队首,epoll会返回有数据的fd的个数,根据个数读取前N个fd处理。<br>
46.总结
多路复用快的原因在于,操作系统提供了这样的系统调用,使得原来的 while 循环里多次系统调用,变成了一次系统调用 + 内核层遍历这些文件描述符。<br> <br>epoll是现在最先进的IO多路复用器,Redis、Nginx,linux中的Java NIO都使用的是epoll。<br> <br>这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。<br> <br>1、一个socket的生命周期中只有一次从用户态拷贝到内核态的过程,开销小;<br>2、使用event事件通知机制,每次socket中有数据会主动通知内核,并加入到就绪链表中,不需要遍历所有的socket。<br> <br>在多路复用IO模型中,会有一个内核线程不断地去轮询多个 socket 的状态,只有当真正读写事件发送时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有真正有读写事件进行时,才会使用IO资源,所以它大大减少来资源占用。<br><br>多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。<br> <br>采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈。
Spring
1.beanFactory 和 factoryBean 的区别?
都是用来创建对象的<br><br>当使用beanFactory时必须要遵循完整的创建流程,这个过程由Spring管理控制<br><br>而使用factoryBean只需要调用getObject就可以返回具体的对象,整个对象的创建过程是由用户自己来控制的,更加灵活。
2.BeanFactory 和 ApplicantContext?
ApplicationContext 由 BeanFactory 派生而来,提供了更多面向实际应用的功能<br><br>BeanFactory 和 ApplicationContext 的实例化时机不太一样,BeanFactory 采用的是延迟初始化的方式,只有在第一次 getBean()的时候,才会实例化 Bean;ApplicationContext 启动之后会实例化所有的 Bean 定义。
3.Spring Bean 生命周期
实例化:调用构造方法实例化--createBeanInstance()<br>属性赋值:设置属性和依赖--populateBean()<br>初始化:initializeBean() -- 前置处理器-- 初始化 --后置处理器<br>销毁:<br><br><b>创建过程:</b><br>AbstractApplicationContext 类里边有一个 refresh() 方法,里边调用 finishBeanFactoryInitialization() 方法,它里边再调用 AbstractBeanFactory的 getBean() ,真正操作在 doGetBean 中
4.Spring中Bean的作用域
singleton、prototype、request、session、globalSession(spring5中也不存在)
5.Spring中的单例Bean会存在线程安全问题嘛?
<b>Spring中的单例Bean不是线程安全的</b><br>单例Bean,全局只有一个,所以线程共享。<br>如果单例Bean是一个无状态的,线程不会对Bean执行查询以外的操作,那他就是安全的,如Spring MVC 的Controller、Service、Dao 等。<br>假如这个 Bean 是有状态的,也就是会对 Bean 中的成员变量进行写操作,那么可能就存在线程安全的问题。
6.怎么解决单例Bean线程安全问题?
1.定义为多例<br>2.避免可变的成员变量<br>3.ThredLoca 能保证多线程下变量的隔离,可以在类中定义一个 ThreadLocal 成员变量,<b>将需要可变成员变量保存在 ThreadLocal 里</b>(推荐)
7.循环依赖&&怎么解决的?
自己依赖自己,或者和别的Bean相互依赖(只有单例bean才会存在循环依赖,原型情况下会直接抛异常)<br> <br>循环依赖发生在<font color="#e74f4c" style=""><b>属性赋值</b></font>阶段,Spring 通过<b><font color="#e74f4c">三级缓存</font></b>解决循环依赖:<br><font color="#a23c73">一级缓存 : </font>Map<String,Object> singletonObjects,单例池,用于保存实例化、属性赋值(注入)、初始化完成的 bean 实例<br><font color="#a23c73">二级缓存 : </font>Map<String,Object> earlySingletonObjects,早期曝光对象,用于保存实例化完成的 bean 实例<br><font color="#a23c73">三级缓存 :</font> Map<String,ObjectFactory<?>> singletonFactories,早期曝光对象工厂,用于保存 bean 创建工厂,以便于后面扩展有机会创建代理对象。<br> <br><b>过程:</b><br>1.创建 A 实例,实例化的时候把 A 对象⼯⼚放⼊三级缓存,表示 A 开始实例化了<br>2.A 注⼊属性时,发现依赖 B,此时 B 还没有被创建出来,所以去实例化 B<br>3.B 注⼊属性时发现依赖 A,它就会从缓存里找 A 对象。依次从⼀级到三级缓存查询 A,从三级缓存通过对象⼯⼚拿到 A,把 A 放⼊⼆级缓存,同时删除三级缓存中的 A,此时,B 已经实例化并且初始化完成,把 B 放入⼀级缓存。<br>4.接着 A 继续属性赋值,顺利从⼀级缓存拿到实例化且初始化完成的 B 对象,A 对象创建也完成,删除⼆级缓存中的 A,同时把 A 放⼊⼀级缓存<br> <br>所以,Spring 能解决 setter 注入的循环依赖,因为实例化和属性赋值是分开的,里面有操作的空间,如果都是构造器注入的话,那么都得在实例化这一步完成注入,所以自然是无法支持了。
8.动态代理
<b>AOP 的核心为动态代理,分为JDK和CGLIB两种:<br></b><br><font color="#a23c73">JDK动态代理</font>需要实现一个接口,通过实现 InvocationHandler 定义横切逻辑,再通过反射调用目标代码,Proxy生成代理对象<br> <br><font color="#a23c73">CGLIB动态代理</font>没有接口的限制,通过字节码技术为一个类创建子类,在子类中拦截父类方法调用,织入横切逻辑<br> <br><b><font color="#e74f4c">CGLIB代理对象比JDK的性能高一点,但CGLIB创建对象时间比较长,所以单例对象用CGLIB较合适,反之用JDK</font><br></b>
9.Spring AOP 和 AspectJ AOP
<b>Spring AOP</b>,运行时增强,基于动态代理只能用于Spring容器<br> <br><b>AspectJ</b>,可单独使用,编译时增强,通过修改代码静态织入
10.Spring事务隔离级别
ISOLATION_DEFAULT:使用后端数据库默认的隔离界别,MySQL 默认可重复读<br>ISOLATION_READ_UNCOMMITTED:读未提交<br>ISOLATION_READ_COMMITTED:读已提交<br>ISOLATION_REPEATABLE_READ:可重复读<br>ISOLATION_SERIALIZABLE:串行化
11.Spring事务传播机制
当多个事务同时存在的时候——一般指的是多个事务方法相互调用时,Spring 如何处理这些事务的行为。<br><b>事务传播机制是使用简单的 ThreadLocal 实现的,所以,如果调用的方法是在新线程调用的,事务传播实际上是会失效的</b><br> <br> 1.没有事务就新建,有就加入<br> 2.支持当前事务,没有就非事务<br> 3.使用当前事务,没有就抛异常<br> 4.新建事务,存在当前事务就挂起当前事务(默认)<br> 5.以非事务方式执行,当前存在事务就挂起它<br> 6.以非事务方式执行,当前存在事务就抛异常<br> 7.如果当前存在事务,在当前事务内执行,否则使用1
12.声明式事务原理
<b>通过AOP动态代理实现</b><br><br>Spring 容器在初始化每个单例 bean 的时候,会遍历容器中的所有 BeanPostProcessor 实现类,会遍历容器中所以的切面,然后查找与当前实例化Bean 匹配的切面,也就是匹配 @Transactional 注解,根据得到的切面创建代理对象。<br>当通过代理对象调用 Bean 方法的时候,在执行目标方法时进行事务增强操作。
13.声明式事务什么情况下失效?
<b>非public方法:</b>不是 public 则不会获取@Transactional 的属性配置信息<br><br>@Transactional 注解属性 propagation 设置错误<br><br>@Transactional 注解属性 rollbackFor 设置错误<br><br><b>同一个类中方法调用,导致@Transactional 失效:</b>比如有一个类 Test,它的一个方法 A,A 再调用本类的方法 B(不论方法 B 是用 public 还是 private 修饰),但方法 A 没有声明注解事务,而 B 方法有。则外部调用方法 A 之后,方法 B 的事务是不会起作用的。因为只有当事务方法被当前类以外的代码调用时,才会由 Spring 生成的代理对象来管理。
14.Spring MVC 执行流程
1.客户端向服务端发送一次请求,这个<b>请求会先到前端控制器 DispatcherServlet</b>(也叫中央控制器)。<br>2.DispatcherServlet <b>接收到请求后会调用 HandlerMapping</b> 处理器映射器。由此得知,该请求该由哪个 Controller 来处理(并未调用 Controller,只是得知)<br>3.DispatcherServlet <b>调用 HandlerAdapter</b> 处理器适配器,告诉处理器适配器应该要去执行哪个 Controller<br>4.HandlerAdapter 处理器适配器<b>去执行 Controller 并得到 ModelAndView</b>(数据和视图),并层层返回给 DispatcherServlet<br>5.DispatcherServlet <b>将 ModelAndView 交给 ViewReslover 视图解析器解析</b>,然后返回真正的视图。<br>6.DispatcherServlet 将模型数据填充到视图中<br>7.DispatcherServlet 将结果响应给客户端
Spring MVC 执行流程
15.SpringBoot 自动配置
@SpringBootApplication是一个复合注解,包含 @EnableAutoConfiguration 开启自动配置<br>@EnableAutoConfiguration 注入了自动装配类 @Import({AutoConfigurationImportSelector.class})<br> <br>AutoConfigurationImportSelector 实现了<b>ImportSelector</b>接口,这个接口的作用就是收集需要导入的配置类,配合@Import()就可以将相应的类导入到 Spring 容器中<br> <br><b>获取注入类的方法是 selectImports(),过程: </b><br> 1.获取注解的属性<br> 2.获取所有需要自动装配的配置类的路径:从 META-INF/spring.factories 获取自动配置<br> 3.去掉重复的和需要排除的,把需要自动加载的配置类的路径存储起来
16.Aware
Spring 容器和Bean是松耦合的,Bean并不知道Spring容器的存在,理论上我们可以随心所欲的把Spring容器换成其他容器<br>但是,在开发中往往我们需要用的Spring容器提供的各种资源,如获取容器中的配置,Bean等,这时,我们就需要某一个Bean能够感知到Spring容器的存在,这样才能想容器要一些东西,这就用到了Aware接口。<br>
Mybatis<br>
1.作用域和生命周期
<b>SqlSessionFactoryBuilder </b>用来创建 SqlSessionFactory ,一旦创建好就不需要它了,因此它的最佳作用域是方法作用域(局部变量)。<br> <br><b>SqlSessionFactory</b> 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。<br> <br><b>每个线程都应该有它自己的 SqlSession 实例</b>。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。使用Web框架时,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它,应该把这个关闭操作放到 finally 块中。<br> <br>映射器接口的实例是从 SqlSession 中获得,映射器实例应该在调用它们的方法中被获取,使用完毕之后即可丢弃,它不需要显示的被关闭。
2.#{}和${}
<b>${}是拼接符</b>,字符串替换,没有预编译处理<br><br><b>#{}是占位符</b>,预编译处理<br>它会将SQL中的#{}替换为?,调用PreparedStatement的set方法来赋值。<br>可以有效防止SQL注入<br>
3.Mybatis的一级、二级缓存?
<b>一级缓存:</b>基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为SqlSession,各个SqlSession之间的缓存相互隔离,当 Session flush 或 close 之后,该 SqlSession 中的所有 Cache 就将清空,MyBatis默认打开一级缓存<br> <br><b>二级缓存</b>与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同之处在于其存储作用域为 Mapper(Namespace),可以在多个SqlSession之间共享,并且可自定义存储源,如 Ehcache。<br><br><b>默认不打开二级缓存</b>,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置。
4.Mybatis 工作原理
两大步
<b>构建会话工厂:</b><br>1.从xml中获取配置,生成一个Configuration实例<br>2.构建SqlSessionFactory:SqlSessionFactory只是一个接口,构建出来的实际上是它的实现类的实例,一般我们用的都是它的实现类DefaultSqlSessionFactory<br> <br><b>会话运行:</b><br>Executor(执行器)<br>StatementHandler(数据库会话器)<br>ParameterHandler (参数处理器)<br>ResultSetHandler(结果处理器)
整体流程
<b>1.读取 MyBatis 配置文件</b>——mybatis-config.xml 、加载映射文件——映射文件即 SQL 映射文件,文件中配置了操作数据库的 SQL 语句。最后生成一个配置对象。<br><b>2.构造会话工厂:</b>通过 MyBatis 的环境等配置信息构建会话工厂 SqlSessionFactory。<br><b>3.创建会话对象:</b>由会话工厂创建 SqlSession 对象,该对象中包含了执行 SQL 语句的所有方法。<br><b>4.Executor 执行器:</b>MyBatis 底层定义了一个 Executor 接口来操作数据库,它将根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护。<br><b>5.StatementHandler:</b>数据库会话器,串联起参数映射的处理和运行结果映射的处理。<br><b>6.参数处理:</b>对输入参数的类型进行处理,并预编译。<br><b>7.结果处理:</b>对返回结果的类型进行处理,根据对象映射规则,返回相应的对象。
5.为什么Mapper接口不需要实现类?
Mapper映射其实是通过动态代理实现的
6.Mybatis执行器
<b>SimpleExecutor:</b>每次执行一次update或select,用完就关闭<br><b>ReuseExecutor:</b>以sql作为key查找Statement对象,存在就使用,不存在就创建,用完不关闭,而是放在Map中下次使用<br><b>BatchExecutor:</b>执行update,不支持select,,将所有SQL加到批处理中统一执行
7.插件
实现Mybatis的Interceptor接口并重写intercept()方法
0 条评论
下一页