JAVA高级面试题
2025-08-07 19:50:31 0 举报
AI智能生成
核心大厂面试题
作者其他创作
大纲/内容
Java进阶
JavaCore核⼼反射<br>获取类对象<br>
<font color="#f44336">1. 类名.class<br></font> Class<User> userClass1 = User.class;<br><font color="#f44336">2. 对象.getClass( )<br></font> User user = new User();<br> Class<? extends User> userClass2 = user.getClass();<br><br><font color="#f44336">3. Class.forName("类的全名")<br></font> Class<?> userClass3 = Class.forName("net.xdclass.reflect.User");<br><font color="#f44336">4. 使⽤类的加载器 ClassLoader<br></font>ClassLoader classLoader = ReflectTest.class.getClassLoader();<br>Class<?> userClass4 = classLoader.loadClass("net.xdclass.reflect.User");<br>
反射API⽐较多,宏观分类<br> get+要获取的东⻄,例如:获取属性为getField()、获取⽅法为getMethod()<br> 需要是public访问权限的⽅法<br> 注意:getMethod可以获取到本类及其⽗类的⽅法<br><br> get+Declared+要获取的东⻄,例如:获取属性为getDeclaredField()、获取⽅法为geDeclaredtMethod()<br> 获取当前运⾏时类中声明的全部⽅法,不包含⽗类中声明的⽅法<div> 注意:getDeclaredMethod只能获取到本类的⽅法</div>
零拷⻉ZeroCopy
减少不必要的内核缓冲区跟用户缓冲区之间的拷贝工作<br>从而减少CPU的开销和减少了kernel和user模式的上下文切换,达到性能的提升。<br>从磁盘中读取文件通过网络发送出去,只需要拷贝2\3次 和 24的内核态和用户态的切换即可<br><br><font color="#f44336">ZeroCopy技术实现有两种 【内核态和用户态切换次数不一样】<br>方式- mmap + write</font><br> 操作系统都使用虚拟内存,虚拟地址 通过多级页表 映射物理地址<br> 多个虚拟内存可以指向同一个物理地址,虚拟内存的总空间远大于物理内存空间<br> 如果把内核空间和用户空间的虚拟地址映射到同一个物理地址,就不需要来回复制数据了<br> mmap系统调用函数会直接把内核缓冲区里的数据映射到用户空间<br> 内核空间和用户空间就不需要进行数据拷贝操作,节省了CPU开销<br><br><font color="#f44336"> 函数</font><br> mmap()读取<br> write( )写出<br><br><font color="#f44336">步骤</font><br> 应用程序先调用mmap() 方法,将数据从磁盘捷贝到内核缓冲区,返回结束(DMA负责)<br> 调用write()内核缓冲区的数据直接拷贝到内核socket buffer (CPU负责)<br> 然后把内核缓冲区的Socket Buffer给直接拷贝给Socket协议栈 即网卡设备中,返回结束 (DMA负责)<br><br><font color="#f44336">损耗</font><br> 没用零拷贝的前,4次CPU上下文切换和4次数据拷贝<br> 采用mmap后,CPU用户态和内核态上下文切换依旧是4次和全程有3次数据拷贝<br> 2次DMA拷贝、1次CPU拷贝、4次内核态用户态切换<br> 减少1次CPU拷贝不是减少两次,因为内核之间有新增一次拷贝)<br>
mmap + write
<font color="#f44336">方式二 sendfile</font><br> Linux kernal2.1新增发送文件的系统调用函数 sendfile( )<br> 替代read( )和 writ( ) 两个系统调用,减少一次系统调用,即减少2次CPU上下文切换的开销<br> 调用sendfile( )从磁盘读取到内核缓冲区,然后直接把内核缓冲区的数据拷贝到 socket buffer缓冲区里<br> 再把内核缓冲区的Socket Buffer给直接拷贝给Socket协议栈即网卡设备中) (DMA负责)<br><br><font color="#f44336">函数</font><br> Sendfile( )<br><br><font color="#f44336">步骤</font><br> 应用程序先调用sendfile()方法,将数据从磁盘拷贝到内核缓冲区 (DMA负贵)<br> 然后把 内核缓冲区的数据直接持贝到内核socket buffer(CPU负责)<br> 然后把内核缓冲区的Socket Buffer给直接持贝给Socket协议栈 即网卡没备中,返回结束 (DMA负责)<br><br><font color="#f44336">损耗</font><br> 没用零拷贝的前,4次CPU上下文切换和4次数据拷贝<br> 采用sendfile后,CPU用户态和内核态上下文切换是2次 和全程有3 次数据拷贝<br> 2次DMA拷贝,1次cpu拷贝、2次内核态用户态切换<br>
sendfile()
Linux2.4+版本之后改进sendfile,利用DMA Gathel带有收集功能的DMA),变成了真正的零拷贝 (没有CPU copy)<br> 应用程序先调用sendfile()方法,将数据从碰盘拷贝到内核缓冲区(DMA负责)<br> 把内存地址,偏移量的缓冲区 fd 描述符 贝到Socket Buffer中去 拷贝很少的数据 可忽路<br> 本质和虚拟内存的解决方法思路一样,就是内存地址的记录<br> 然后把内核缓冲区的Socket Buffer给直接拷贝给Socket协议栈 即网卡设备中返回结束(DMA负责)
改进sendfile()
零拷⻉技术应⽤
Java NIO对mmap fileChannel.map()<br>Java NIO对sendfile fileChannel.transferTo() 和 fileChannel.transferFrom()<br><br><font color="#f44336">什么是FileChannel</font><br> 是⼀个连接到⽂件的通道,可以通过⽂件通道读写⽂件,该常被⽤于⾼效的⽹络/⽂件的数据传输和⼤⽂件拷⻉<br> 应⽤程序使⽤FileChannel 写完以后,数据是在PageCache上的,操作系统不定时的把PageCache的数据写⼊到磁盘<br> 使⽤ channel.force(true) 把⽂件相关的数据强制刷⼊磁盘上去,避免宕机数据丢失<br> 使⽤之前必须先打开它,但是⽆法直接new ⼀个 FileChanne<br> <font color="#f44336"> 常规通过使⽤⼀个 InputStream、OutputStream 或 RandomAccessFile 来获取⼀个FileChannel实例</font><br><br><font color="#f44336">mmap实现</font><br> map⽅法,把⽂件映射成内存映射⽂件<br> MappedByteBuffer ,是抽象类 也是ByteBuffer的⼦类 ,具体实现⼦类是DirectByteBuffer,可被通道进⾏读写<br><font color="#f44336"> ⼀次 map ⼤⼩要限制 2G 内</font>,过⼤ map 会增加虚拟内存回收和重新分配的压⼒ ,直接报错<br> FileChannel.java 中的 map 对 long size 进⾏了限制,不能⼤于 Integer.MAX_VALUE,否则就报错<br> JDK 层的为何要限制,是因为底层 C++的类型,⽆符号int类型最⼤是2^31 -1, 2^31 -1 字节就是 2GB - 1B<br><br>//position: ⽂件开始<br>//size:映射的⽂件区域⼤⼩<br>//mode: 访问该内存映射⽂件的⽅式: READ_ONLY(只读) READ_WRITE(读写),PRIVATE(创建⼀个修改副本)<br>MappedByteBuffer map(int mode,long position,long size);<br><br><font color="#f44336">sendfile实现<br></font>//position - ⽂件中的位置,从此位置开始传输,必须⾮负数<br>//count - 要传输的最⼤字节数,必须⾮负数<br>//target - ⽬标通道<br>返回:实际已传输的字节数,可能为零<br>将字节从此通道的⽂件传输到给定的可写⼊字节通道,返回值为真实拷⻉的size,最⼤拷⻉2G,超出2G的部分将丢弃<font color="#f44336"><br></font>fileChannel.transferTo(long position, long count, WritableByteChannel target)<font color="#f44336"><br></font><br>//src - 源通道<br>//position - ⽂件中的位置,从此位置开始传输,必须⾮负数<br>//count - 要传输的最⼤字节数, 必须⾮负数<br>//返回:实际已传输的字节数,可能为零<br>将字节从给定的可读取字节通道传输到此通道的⽂件中,对⽐ 从源通道读取并将内容写⼊此通道的循环语句相⽐,此⽅法更⾼效<br>fileChannel.transferFrom(ReadableByteChannel src, long position, long count)<br>
注意<br> 上述⽅法允许将⼀个通道连接到另⼀个通道,不需要在⽤户态和内核态来回复制,同时通道间的内核态数据也⽆需复制<br> transferTo()只有源为FileChannel才⽀持transfer这种⾼效的复制⽅式,其他如SocketChannel都不⽀持transfer模式<br> ⼀般可以做FileChannel->FileChannel 和 FileChannel->SocketChannel的transfer零拷⻉
普通IO<br>BufferIO<br>
局都性原理: 指计算机在执行某个程序时,倾向于使用最近使用的数据<br> 时间属部性:如果程序中的某条指令一旦被执行,则不久的将来该指令可能再次被执行<br> 空间属部性: 一旦程序访问了某个存健单元,在不久的将来,其附近的存健单元也最有可能被访问<br>
<font color="#f44336">为啥带buffer的IO比黄通IO性能高?</font><br>文件读取,OS的做了优化操作 (普通IO和Bufferio都有]<br> 每次读数据的时候 , 统根据局部性原理,通过 DMA 会读入更多的数据到内核缓冲区里面<br> OS根据局部性原理会在一次read() 系统调用过程中预读更多的文件数据缓存在内核10缓冲区中<br> 当继续访问的文件数据在罐冲区中时便直接拷贝数据到进程缓冲区,避免了再次的低效率磁盘IO操作<br> OS已经帮减少磁盘IO操作次数,提高了性能<br>
<font color="#f44336">BufferedInputStream为啥性能⾼点?<br></font> 通过减少系统调用次数来提高IO性能 ,即减少CPU在内核态和用户态的上下文切换次数 <br> 在kernel buffer 把数据找贝到 user buffer 的时候。把数据多持贝到 user buffer 中<br><br>比如<br> 进程user buffer想要向内核态读取4个字节,但是内核本上面有8个字节数据,大方点都拷贝到user buffer里面。<br> 当进程user buffer下次要再读取4个字节的时候,因为数据已经在user buffer中了,就不需要上下文切换<br>
RocketMQ-Kafka<br>和Nginx零拷⻉<br>
<font color="#f44336">Nginx 使⽤就是 sendfile 零拷⻉<br></font> Web Server 处理静态⻚⾯请求时,是从磁盘中读取⽹⻚的内容,所以选择这个<br> 因为 sendfile不能在应⽤程序中修改数据,所以适合 静态⽂件服务器或者是直接转发数据的代理服务器<br><br><font color="#f44336">rocketmq : 主要是mmap,也有⼩部分使⽤sendfile<br></font> rocketMQ在消息存盘和⽹络发送使⽤mmap, 单个CommitLog⽂件⼤⼩默认1GB<br> 要在⽤户进程内处理数据,然后再发送出去的话,⽤户空间和内核空间的数据传输就是不可避免的,<br><br><font color="#f44336">Kafka :主要是sendfile,也有⼩部分使⽤mmap<br></font> kafka 在客户端和 broker 进⾏数据传输时,broker 使⽤ sendfile 系统调⽤,类似<br> 【FileChannel.transferTo】 API,将磁盘⽂件读到 OS 内核缓冲区后,直接转到 socket buffer <br> 进⾏⽹络发送,即 Linux 的 sendfile。<br><br>Hadoop、Tomcat、Kafka、Netty、Zookeeper、Rabbitmq... 等都有⽤到零拷⻉<br>
ZeroCopy零拷⻉对比
<font color="#f44336">sendfile</font><br> ⽆法在调⽤过程中修改数据,只适⽤于应⽤程序不需要对所访问数据进⾏处理修改情况<br><font color="#f44336"> 场景</font><br> ⽐如 静态⽂件传输,MQ的Broker发送消息给消费者<br> 如果想要在传输过程中修改数据,可以使⽤mmap系统调⽤。<br> ⽂件⼤⼩:适合⼤⽂件传输<br> 切换和拷⻉:2次上下⽂切换,3 次数据拷⻉。<br> 2次DMA拷贝,1次cpu拷贝、2次内核态用户态切换<br><br><font color="#f44336">mmap</font><br> 在mmap调⽤可以在应⽤程序中直接修改Page Cache中的数据,使⽤的是mmap+write两步<br> 调⽤⽐sendfile成本⾼,但优于传统I/O的拷⻉实现⽅式,虽然⽐ sendfile 多了上下⽂切换<br> 但⽤户空间与内核空间并不需要数据拷⻉,在正确使⽤情况下并不⽐ sendfile 效率差<br> <font color="#f44336"> 场景</font><br> 多个线程以只读的⽅式同时访问⼀个⽂件, mmap 机制下多线程共享同⼀物理内存空间,节约内存<br> ⽂件⼤⼩:适合⼩数据ᰁ读写<div> 切换和拷⻉:4 次上下⽂切换,3 次数据拷⻉<br> CPU用户态和内核态上下文切换依旧是4次和全程有3次数据拷贝<br> 2次DMA拷贝、1次CPU拷贝、4次内核态用户态切换<br></div><br>
AQS<br>JUC<br>
shutdown<br>shutdownNow区别<br>
<font color="#e74f4c">shutdown()<br></font> 可以安全关闭线程池,调⽤ shutdown() ⽅法之后线程池并不是⽴刻就被关闭,这个时候线程池不能接受新的任务<br> 线程池中可能还有很多任务正在被执⾏,或是任务队列中有⼤ᰁ正在等待被执⾏的任务,<br> ⽤ shutdown() ⽅法后线程池会在执⾏完正在执⾏的任务和队列中等待的任务后才彻底关闭,没有返回值<br><br><font color="#e74f4c">shutdownNow()<br></font> ⽴刻关闭的意思,不推荐使⽤这⼀种⽅式关闭线程池<br> 调⽤法后线程池不能接受新的任务,会给所有线程池中的线程发送 interrupt 中断信号<br> 尝试中断这些任务的执⾏,会将任务队列中正在等待的所有任务转移到⼀个 List 中并返回,<br> 可以根据返回的任务 List 来进⾏⼀些补救的操作,返回⼀个List< Runnable ><br>
线程池的执行流程
查看核⼼线程池是否已满,不满就创建⼀条线程执⾏任务,否则执⾏第⼆步。<br>查看阻塞队列是否已满,不满就将任务存储在阻塞队列中,否则执⾏第三步。<br>查看线程池是否已满,即是否达到最⼤线程池数,不满就创建⼀条线程执⾏任务,否则就按照策略处理⽆法执⾏的任务<br><br><font color="#e74f4c">注意<br></font> 如果队列的任务为空,阻塞队列可以保证任务队列中没有任务时,阻塞获取任务的线程,线程进入wait状态,释放CPU资源<br> 阻塞队列自带阻塞和唤醒的功能,无任务执行时线程池利用阻塞队列的take方法挂起<br> 阳塞队列可以维持核心线程的存活且不一直占用CPU资源,而一般的队列只能作为一个有限长度的缓冲没有其他功能
非公平锁会带来[锁饥饿] ,<br>说下什么是[锁饥饿]?<br>
当有几个线程同时来抢占锁时,其中有的线程一直抢到锁。<br>但一些线程由于优先级太低,一直得不到 CPU 调度执行,<br>导致其他线程一直抢不到锁,这个就是出现了锁饥饿.<br>
ReentrantLock为啥要设计有公平锁和非公平锁?<br>他们有什么优缺点?<br>
公平锁获取锁和释放锁后的相关操作,相关线程也会从休眠和恢复这间变化,<br>这个就涉及到用户态内核态互相转变。<br><br>非公平锁获取锁不用道循先到先得的规则,没有了阻塞和恢复执行的步骤,<br>避免了线程休眠和恢复的操作所以非公平锁性能高于公平锁,更能面段利用CPU<br><br>如果是为了各个线程都可以获取锁资源,则推荐采用公平锁,等特锁的线程不会饿死<br>如果是为了业务有更大的吞吐面晶则推荐采用非公平锁<br><br><font color="#e74f4c">结论</font><br> 锁的默认实现都是非公平锁晶原因是非公平顿的效需更高<br> 非公平锁注重的是性能,而公平锁注重的是锁资源明平均分配<br>
什么是对象锁 (synchronized method ( ) }<br>什么是类锁 (static sychronized method { } )<br>
<font color="#e74f4c">也叫实例锁</font>,当多个线程访问多个实例时,它们互不干扰,每个对象都拥有自己的锁<br>但如果是多个线程访问同个对象的sychronized块,是同步的加锁,访问不同对象的话就是不同步的。<br>synchronized(objec){ } 的 效果和在实例方法上加锁一样,不同的是可以在里添加不同的对象<br><br><br><font color="#e74f4c">是类锁 </font>,是一个全局锁不创建多心个对都共享同一个锁,保障同一个时刻多个线程同时访问同一个<br>synchronzed块,当是一个线程在访同时,其他的线程等待。静态方法使用synchronized关键字后,<br>无论是多线程访同单个对象还是多个对象的sychronized块,都是同步的。<br>synchronized(xxxclass)0的效果和在静态方法方法上加锁一样,不同的是可以在 { }里添加不同的类对象<br>
synchronized介绍
synchronized是解决线程安全的问题,常用在 同步普通方法、静态方法、代码块中。<br><font color="#f44336">每个对象有一个锁和一个等待队列,锁只能被一个线程持有,其他需要锁的线程需要阻塞等待<br>锁被释放后,对象会从队列中取出一个并唤醒,唤醒哪个线程是不确定的,不保证公平性<br>所以是”非公平、可重入的悲观锁。</font><br><br>实现线程同步的机制,它们的目的都是为了保证多个线程访问共享资源时的互斥性和可见性。<br>
<font color="#e74f4c">两种形式:<br></font> 同步方法<br> 生成的字节码文件中会多一个 ACC SYNCHRONIZED 标志位<br> 当一个线程访问方法时,会去检查是否存在ACC SYNCHRONIZED(acc synchronized)标识<br> 如果存在,执行线程将先获取monitor,获取成功之后才能执行方法体,方法体执行完后再释放monitor<br> 在方法执行期间,其他任何线程都无法再获得同一个monitor对象,<font color="#f44336">也叫隐式同步</font><br><br> 同步代码块<br> 加了 synchronized关键字的代码段,生成的字节码文件会多出<font color="#f44336"> monitor enter和 monitor exit </font>两条指令<br> 每个monitor维护着<font color="#f44336">一个记录</font>拥有次数的计数器 , 未被拥有的monitor的该计数器为0,<br> 当一个线程获执行monitorenter后,该计计数器自增1 ,<br> 当同一个线程执行monitorexit指令的时候,计数器再自减1,<br> 当计数器为0的时候monitor将被释放, 也叫<font color="#f44336">显式同步<br></font><br>底层都是通过monitor来实现同步,只是方法的同步是一种隐式的方式来实现,无需进过字节码来完成<br>
Synchronized和ReentrantLock都是Java中实现线程<font color="#f44336">同步的机制,<br></font>都是<font color="#f44336">保证多个线程访问共享资源时的互斥性和可见性</font><br>Synchronized 的实现原理是基于 <font color="#f44336">Java 中的内置监视器锁 或称作对象锁<br>每个 Java 对象都有一个与之关联的锁。</font><br><br>当一个 synchronized 代码块执行时,会首先尝试获得内置监视器锁。<br>如果锁已经被其他线程持有,则当前线程将进入阻塞状态,<br>直到该锁被释放为止;<br>如果锁未被持有,则当前线程获得该锁。在 synchronized 块结束时,会自动释放锁<br>
ReentrantLock介绍
它利用了 Java中的AQS来实现<font color="#f44336">线程的同步,它允许重入特性</font>,<br>即同一线程可以对已经获取到的锁再次进行请求,如果不支持重入,<br>则会java.lang.IllegalMonitorStateException异常<br>
ReentrantReadWriteLock<br>与ReentrantLock不同<br>
<font color="#e74f4c">ReentrantReadWriteLock<br></font> 1、读写锁接口ReadWriteLock接口的一个具体实现,实现了读写锁的分离,<br> 2、支持公平和非公平,底层也是基于AQS实现<br> 3、允许从写锁降级为读锁<br> 流程:先获取写锁,然后获取读锁,最后释放写锁;但不能从读锁升级到写锁<br> <br> 4、重入:读锁后还可以获取读锁;获取了写锁之后既可以再次获取写锁又可以获取读锁<br> 核心:读锁是共享的,写锁是独占的。 <font color="#e74f4c">读和读之间不会互斥</font>,读和写、写和读、写和写之间才会互斥,<br> 主要是提升了读写的性能<br>
<font color="#e74f4c">ReentrantLock<br></font>是独占锁且可重入的,相比synchronized而言功能更加丰富也更适合复杂的并发场景,但是也有弊端,<br>假如有两个线程A/B访问数据,加锁是为了防止线程A在写数据, 线程B在读数据造成的数据不一致; <br>但线程A在读数据,线程C也在读数据,读数据是不会改变数据没有必要加锁,但是还是加锁了,<br>降低了程序的性能,所以就有了ReadWriteLock读写锁接口<br><br>场景:读多写少,比如设计一个缓存组件 或 提高Collection的并发性<br>
非阻塞队列ConcurrentLinkedQueue<br>它怎么实现线程安全的?<br>
<font color="#e74f4c">线程安全原因:<br></font>ConcurrentLinkedQueue是基于链表实现的无界线程安全队列,采用<font color="#f44336">FIFO先进先出排序</font><br>保证线程安全的三要素:原子、有序、可见性<br><br>1、底层结构是Node,链表头部和尾部节点是head和tail,使用节点变量和内部类属性使用 ,<br> volatile声明保证了有序和可见性<br>2、插入、移除、更新操作使用CAS无锁操作,保证了原子性<br>3、假如多线程并发修改导致 CAS 更新失败,采用for循环插入保证更新操作成功<br>
Semaphore介绍
它是操作系统中PV操作的原语在java的实现,它也是基于AbstractQueuedSynchronizer实现的。<br><br>Semaphore的功能非常强大,大小为1的信号量就类似于互斥锁,通过同时只能有一个线程获取<br>信号量实现。大小为n(n>0)的信号量可以实现限流的功能,它可以实现只能有n个线程同时获<br>取信号量<br><br>应用场景:<font color="#f44336">可以用于做流量控制,特别是公用资源有限的应用场景</font><br>
PV操作是操作系统一种实现进程互斥与同步的有效方法。PV操作与信号量(S)的处理相关,P表示通过<br>的意思,V表示释放的意思。用PV操作来管理共享资源时,首先要确保PV操作自身执行的正确性。<br><font color="#f44336">P操作的主要动作是:</font><br>①S减1;<br>②若S减1后仍大于或等于0,则进程继续执行;<br>③若S减1后小于0,则该进程被阻塞后放入等待该信号量的等待队列中,然后转进程调度。<br><font color="#f44336">V操作的主要动作是:</font><br>①S加1;<br>②若相加后结果大于0,则进程继续执行;<br>③若相加后结果小于或等于0,则从该信号的等待队列中释放一个等待进程,然后再返回原进程继续执行<br>或转进程调度。
CyclicBarrier介绍
通过它可以实现让一组线程等待至某个状态(屏障点)之后再全部同<br>时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用<br><br><font color="#f44336">应用场景:</font>可以用于多线程计算数据,最后合并计算结果的场景<br><br>特性:<font color="#f44336">重置,屏障可以重复使用</font><br>
CountDownLatch介绍
CountDownLatch(闭锁)是一个同步协助类,允许一个或多个线程等待,直到其他线程完成操作集<br><br>CountDownLatch使用:给定的计数值(count)初始化。await方法会阻塞直到当前的计数值<br>(count)由于countDown方法的调用达到0,count为0之后所有等待的线程都会被释放,并且<br>随后对await方法的调用都会立即返回。这是一个一次性现象 —— count不会被重置<br><br>应用场景:一般用作多线程倒计时计数器,强制它们等待其他一组(CountDownLatch的初始化决定)任务执行完成<br>场景1:让多个线程等待<br>场景2:让单个线程等待<br>
<font color="#f44336">实现原理</font><br>底层基于 AbstractQueuedSynchronizer 实现,CountDownLatch 构造函数中指定的<br>count直接赋给AQS的state;每次countDown()则都是release(1)减1,最后减到0时unpark阻塞<br>线程;这一步是由最后一个执行countdown方法的线程执行的。<br><br>而调用await()方法时,当前线程就会判断state属性是否为0,如果为0,则继续往下执行,<br>如果不为0,则使当前线程进入等待状态,直到某个线程将state属性置为0,其就会唤醒在<br>await()方法中等待的线程。<br>
CountDownLatch与CyclicBarrier的区别
都能够实现线程之间的等待,只不过它们侧重点不同<br>1. CountDownLatch的计数器只能<font color="#f44336">使用一次,而CyclicBarrier的计数器可以使用reset()方法重置<br></font><br>2. CyclicBarrier还提供getNumberWaiting(可以获得CyclicBarrier阻塞的线程数量)、<br> isBroken(用来知道阻塞的线程是否被中断)等方法<br><br>3. CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程<br>
4. CountDownLatch一般用于一个或多个线程,等待其他线程执行完任务之后,再执<br>行,不可重复使用。CyclicBarrier一般用于一组线程互相等待至某个状态,<br>然后这一组线程再同时执行,可重用的<br><br>5. CyclicBarrier 还可以提供一个 barrierAction,合并多线程计算结果<br>6. <font color="#f44336">CyclicBarrier是通过ReentrantLock的"独占锁"和Conditon来实现一组线程的阻塞唤<br> 醒的,而CountDownLatch则是通过AQS的“共享锁”实现</font><br>
CountDownLatch与Thread.join的区别
1. CountDownLatch的作用就是<font color="#f44336">允许一个或多个线程等待其他线程完成操作</font>,看起来<br> 有点类似join() 方法,但其提供了比 join() 更加灵活的API<br><br>2. CountDownLatch可以<font color="#f44336">手动控制在n个线程里调用n次countDown()方法使计数器<br> 进行减一操作,也可以在一个线程里调用n次执行减一操作<br></font><br>3. join() 的实现原理是<font color="#f44336">不停检查join线程是否存活,如果 join 线程存活则让当前线程永远等待<br><br></font>Thread.join使用场景:线程A执行到一半,需要一个数据,这个数据需要线程B去执行修改,<br> 只有B修改完成之后,A才能继续操作<br>
lock与synchronized的区别
lock 获取锁与释放锁的过程,都需要程序员手动的控制 ,Lock用的是乐观锁方式<br>乐观锁实现的机制就 是CAS操作<br><br>synchronized托管给jvm执行 原始采用的是CPU悲观锁机制,即线程获得的是独占锁。<br>独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。<br>
Redis
Redis性能为啥那么快?<br>常规读写QPS有多高?<br>
Redis 将数据保存在内存中,因此可以高效地读取和写入数据,缩短了数据存取中碰盘操作带来的时间成本。<br>支持多种数据结构,支持对这些数据的原子操作、原子操作避免了<font color="#f44336">多个操作的竞争和数据冲突,减少了锁的使用</font><br>采用非阻塞和多路复用IO,服务端一个线程处理多个请求,<font color="#f44336">避免了线程上下文切换和IO阻塞</font><br><br><font color="#f44336">性能:</font>常规读写 QPS 的性能指标,因具体的部署环境和硬件条件的不同而有所差异<br> 可以轻松处理百万级别的并发读请求,写请求每秒数万到10W+写性能,集群部署可以进一步提升<br><br><font color="#f44336">官方数据 : </font> Redis单例能处理key:2.5亿个<br> 一个key或value大小最大是512M<br><br><font color="#f44336">常规瓶颈 : </font> 一般机器的内存和带宽,而不是CPU<br>
Redis是单线程还是多线程?
redis4.X-6.X<font color="#f44336">工作线程是单统程</font>,但是整个<font color="#f44336">服务来说也是多线程</font>,比如 rdb备份、<br>主从同步、异步删除也是独立线程完成<br> 对于 Big Key删除,不推荐直接del key,<font color="#f44336">会阻塞主线程,</font> 而应该采用unlink key,异步删除<br> redis4.X 引入的 unlink key和 flushall async,异步删除<br><br>redis6.X开始多线程只是用来<font color="#f44336">处理网络数据的读写和协议解析上</font>,<font color="#f44336">但底层数据操作还是单线程,</font><br>执行命令仍然是单线程,这么设计是不想因为多线程而变得复杂,<font color="#f44336">需要去控制 key、lua事务,<br>LPUSH/LPOP 等等的并发问题</font><br><br><font color="#f44336">默认不开启:</font>io-threads-do-reads yes io-threads 线程数<br><br>官方建议( 线程数小于机器核数)<br> 4核的机器建议设置为2或3个线程<br> 8 核的建议设置为 4或6个线程<br>
Redis的key过期时间删除策略你知道多少?
Redis服务器使用的是<font color="#f44336">情性刑除和定期刑除两种策略</font><br>通过配合使用这两种删除策略,服务器可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡<br><br><font color="#f44336">定期删除</font>:隔一段时间,就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就刷除<br> 定期删除可能会导致很多过期 key 到了时间并没有被删除掉,那咋整呢,所以就是惰性删除<br><br><font color="#f44336">惰性删除: </font>概念: 当一些客户端尝试访问它时,key会被发现并主动的过期,<br> 放任键过期不管,但是每次从键空间中获取键时。都检查取得的键是否过期,如果过期的话,就删除该键<br><br>问题:如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走情性删除,此时会怎么样?<br> 如果大量过期 key 堆积在内存里,导致 redis 内在块耗尽了,就需要走内存淘汰机制<br>
Redis内存不够怎么办? Redis 就只能用 10G,<br>你要是往里面写了 20G 的数据,<br>会发生什么? 淘汰哪些数据<br>
redis在占用的内存超过指定的maxmemony之后,通过<br> maxmemonpolicy确定redis是否释放内存以及如何释放内存<br><br><font color="#f44336">提供多种策略</font><br> 最近最少使用算法,从设置了过期时间的键中选择空转时间最长的键值对清除掉<br> 最近最不经常使用算法,从设置了过期时间的键中选择某段时间之内使用频次最小的键值对清除掉<br> 从设置了过期时间的键中选择过期时间最早的键值对清除(制除即将过期的)<br> 从设置了过期时间的键中,随机选择键进行清除<br><br> allkeys-lru,最近最少使用算法,从所有的键中选择空转时间最长的键值对清除<br> allkeys-lfu,最近最不经常使用算法,从所有的键中选择某段时间之内使用频次最少的键值对清除<br> allkeys-random,所有的键中,随机选择键进行删除<br> noeviction,不做任何的清理工作,在redis的内存超过限制之后,<br> 所有的写入操作都会返回错误;但是读操作都能正常的进行<br>
Redis的持久化机制
append only file,追加文件的方式,文件容易被人读懂<br>以独立日志的方式记录每次写命令, 重启时再重新执行AOF文件中的命令达到恢复数据的目的<br>写入过程宕机,也不影响之前的数据,可以通过 redis-check-aof检查修复<br><font color="#f44336"><br>AOF持久化优点:<br></font>1 相比RDB来说 AOF数据更加安全。<br>2 当Redis AOF文件太大时,Redis能够在后台自动重写AOF<br>3 AOF以易于理解和解析的格式,一个接一个地包含所有操作的日志<br><font color="#f44336">缺点:</font><br>1 AOF文件通常比同一数据集的等效RDB文件大<br>2 根据确切的fsync策略,恢复的时候AOF可能比RDB慢<br>
在指定的时间间隔内将内存中的数据集快照写入磁盘<br>默认的文件名为dump.rdb<br><font color="#f44336"><br>RDB持久化优点:</font><br>1 RDB最大限度地提高了Redis的性能,父进程不需要参与磁盘I/O<br>2 RDB文件紧凑,适用于进行全量备份和灾难性恢复<br>3 在恢复大数据集时的速度比 AOF 的恢复速度要快<br>4 生成的是一个紧凑压缩的二进制文件<br><font color="#f44336">缺点:</font><br>每次快照是一次全量备份,fork子进程进行后台操作,子进程存在开销<br>在快照持久化期间修改的数据不会被保存,可能丢失数据<br>
缓存击穿<br>缓存雪崩<br>缓存穿透<br>
缓存中没有但数据库中有的数据,假如是热点数据,那key在缓存过期的⼀刻,同时有⼤ᰁ的请求<br>这些请求都会击穿到DB,造成瞬时DB请求ᰁ⼤、压⼒增⼤<br>和缓存雪崩的区别在于这⾥针对某⼀key缓存,后者则是很多key<br><br><font color="#f44336">解决⽅案</font><br> 设置热点数据不过期<br> 定时任务定时更新缓存<br> 多级缓存,nginx前置短期缓存,然后Redis配置不过期<br> 设置互斥锁,案例代码(在架构大课笔记)<br>
多个热点key都过期或<font color="#e74f4c">Redis宕机</font>,⼤量的key设置了相同的过期时间,导致在缓存在同⼀时刻全部失效,<br>造成瞬时DB请求量⼤、压⼒骤增,引起雪崩<br><br><font color="#f44336">解决⽅案<br></font> 搭建⾼可⽤集群环境 ,分布式缓存组合本地缓存使⽤<br> 存数据的过期时间设置随机,防⽌同⼀时间⼤量数据过期现象发⽣<br> 设置热点数据永远不过期,定时任务定时更新<br>
查询⼀个不存在的数据,由于缓存是不命中的,如发起为id为“-1”不存在的数据如果从存储层<br>查不到数据则不写⼊缓存,导致这个不存在的数据每次请求都要到存储层去查询,⼤量查询不存在的数据,<br>可能DB就挂掉了,是⿊客利⽤不存在的key频繁攻击应⽤的⼀种⽅式<br><br><font color="#f44336">解决⽅案<br></font> 接⼝层增加校验,数据合理性校验<br> 缓存取不到的数据,在数据库中也没有取到,将key-value对写为key-null,设置短点过期时间,防⽌同个key被⼀直攻击(<font color="#e74f4c">不能100%解决</font>)<br> 布隆过滤器(类似⽩名单)<br> 将所有要【缓存的数据】经过处理后存储布隆过滤器中,即对应的bit上是1<br> 当外部请求发起时,⾸先会把请求的参数 通过哈希算法处理,获得相应的哈希值;<br> 根据哈希值计算出位数组中的位置,如果全部计算的hash值对于的bit存储都是1,则表示数据在合理中<br> 再从缓存读出,缓存失效则从数据库中取出,少数误判不影响,可以把误判的value 设置null, 加上短期过期时间<br> 如果计算的hash值对应的bit存储存在⼀个是0或以上,表示数据不合理,直接返回数据不存在,不查缓存和数据库<br> 如果布隆过滤器认为值不存在,那么值⼀定是不存在的,⽆需查询缓存也⽆需查询数据库<br>
Redis 有哪些集群部署方式?
<font color="#f44336">单机模式: </font> 只需要一台机器,负责读写,一般只用于开发人员自己测试<br><br><font color="#f44336">哨兵模式: <br></font> 1. 哨兵机制建立了多个哨兵节点(进程),共同监控数据节点的运行状况。<br> 2.同时哨兵节点之间也互相通信,交换对主从节点的监控状况。<br> 3.每隔1秒每个哨兵会向整个集群发送一次ping命令做一次心跳检测。<br> Master主服务器+Slave从服务器+其他Sentinel(哨兵)进程,<br><br><font color="#f44336">优点:<br></font> 主从可以自动切换,系统更健壮,可用性更高;<br> 在master节点故障后能自动将salve节点提升成master节点,不需要人工干预操作就能恢复服务高可用<br><br><font color="#f44336">缺点:</font>1.要多维护一套哨兵模式,实现起来也变的更加复杂增加维护成本;<br> 2.每个节点存储的数据是一样的,浪费内存。<br> 3.数据量太大,主从同步时严重影响了master性能。<br> 4.只有一个Redis主机来接收和处理写请求,写操作还是受单机瓶颈影响,没有实现真正的分布式架构<br> 5. 主从切换会丢失短暂数据<br> 6. 主节点的写能力和存储能力受限<br>
<font color="#f44336">主从复制工作原理</font><br> 1. 当slave启动后主动向master发送SYNC命令;<br> 2. master接收到SYNC命令后,在后台保存快照和缓存,然后将保存的快照文件和缓存的命令发送给slave;<br> 3. slave接收到快照文件和命令后加载快照文件和缓存的执行命令;<br> 4. 复制初始化后,master每次接收到的写命令都会同步发送给slave,保证主从数据一致性<br><br><font color="#f44336">优点:</font>做到读写分离,提高服务器性能;提高效率、数据备份,提供多个副本等<br><font color="#f44336">缺点:</font>一旦Master节出现故障不能提供服务,需要人工将Slave节点晋升为Master节点<br> 不具备自动容错和恢复功能<br><br><font color="#f44336">主从复制 哨兵模式区别</font>:<br> 主从模式master节点故障后服务,需要人为的手动将slave节点切换成为maser节点后服务才恢复。<br> 哨兵模式能在master节点故障后能自动将salve节点提升成master节点,不需要人工干预操作就能恢复服务高可用<br><br><br><font color="#f44336">cluster集群模式: </font><br> 这是一种服务器Sharding技术(分片和路由都是在服务端实现),采用多主多从,<br> 每一个分区都是由一个Redis主机和多个从机组成,片区和片区之间是相互平行的
热点Key
缓存中的某些Key对应的value存储在集群中⼀台机器,所有流<span style="color:rgb(237, 151, 69);">量</span>涌向同⼀机器,<br>成为系统的瓶颈,⽆法通过增加机器容ᰁ来解决。【流量/数据倾斜】 Kafka、Mysql、Flink <br>等普遍都存在,简单说就是数据和访问分配不均<br><br>请求集中,超过单Server的性能极限<br> 超过该主机⽹卡上限时<br> 热点 Key 的缓存过多,超过⽬前的缓存容量时,缓存分⽚服务被打垮<br> 当缓存服务崩溃后,请求击穿到后台 DB 上,进⼀步导致缓存雪崩<br>
如何发现热点key问题
<font color="#e74f4c">⽅式⼀:经验处理</font><br> 根据借业务经验,预估热 Key 出现,⽐如上架了茅台商品秒杀活动<br> 优点:⽅案简单,凭经验发现热 Key ,提早发现处理<br> 缺点:没有办法预测所有热 Key 出现,⽐如某些突然⽕爆的商品,⽆法提前预测(特别短视频流⾏情况下)<br><font color="#e74f4c"><br>⽅案⼆:节点和solt流ᰁ监控</font><br> 对于每个节点或者slot中的流<span style="color:rgb(237, 151, 69);">量</span>做监控,上报对每个slot的流ᰁ对⽐,热key出现时发现影响到的具体slot<br> 优点:监控⽅便,⽅案简单<br> 缺点:粒度过于粗,适⽤于前期集群监控⽅案,不适⽤于精准探测到热key<br><br><font color="#e74f4c">⽅式三:客户端统计(推荐)</font><br> 客户端⼆次定制收集,应⽤程序连接redis都会使⽤客户端⽐如jedis等进⾏统计,基于这个客户端⼆次封装<br> 在发送请求前进⾏收集采集,定时把收集到的数据上报到统⼀的计算服务进⾏聚合统计<br> 优点:准确性⾼,实时⾏好<br> 缺点:需要对 SDK ⼯具进⾏⼆次开发,业务架构链路⽐较复杂<br>
如何解决热点key问题
没固定值,但是依据个⼈经验,当某个key的访问频率很⾼,当⼀个key的qps到达1000的时候就需要关注<br>Redis集群⼀般根据crc算法计算key归属哪个槽位,当某个key的qps过⾼,<br>容易出现⼤ᰁ的读请求落在某⼀个数据分⽚节点上<br><br><font color="#e74f4c">热点key的解决⽅案⼀:本地缓存【推荐项⽬前期】</font><br> 避免带宽或者传输影响,本地缓存热点key数据,对于每次读请求<br> 将⾸先检查key是否存在于本地缓存中,如果存在则直接返回,如果不存在再去访问分布式缓存的机器<br>
<font color="#e74f4c">热点key的解决⽅案⼆:离散热点key<br></font> 保证不出现热key问题,分拆key也是⼀个好的解决⽅案<br> 常⽤⽅案 ,结合应⽤程序所在的机器ip或者mac⽹卡,得出⼀个固定值,拼接到key上⾯<br> 访问的时候也是根据业务本身的key,拼接到⼀样的算法,得到后端固定节点的redis上<br> 两步骤:拆key和组合key,<br><br>缺点:每次更新和新增时都需要去改动这N个key<br> 带来⼀些额外的开销,包括存储空间占⽤和应⽤程序修改复杂度等问题
<font color="#e74f4c">热点key的解决⽅案三:⽹关缓存+实时计算【推荐项⽬后期】<br></font> 应⽤程序⽇志打印访问数据,结合流式计算,时间窗范围内统计<br> 计算出超过⼀定阈值的热点key,时间窗内访问增⻓快,预判会成为热点key,<br> 放置到业务⽹关缓存⾥⾯,前置热点缓存<br> ⽹关访问时先查找⽹关缓存是否存在对应的数据,如果存在则直接读取返回
什么是Redis的哈希槽 slot
Redis集群预分好16384个槽,当需要在 Redis 集群中放置⼀个 key-value 时<br>根据 CRC16(key) mod 16384的值,决定将⼀个key放到哪个桶中<br><br><font color="#e74f4c">⼤体流程</font><br> 假设主节点的数量为3,将16384个槽位按照【⽤户⾃⼰的规则】去分配这3个节点,每个节点复制⼀部分槽位<br> 节点1的槽位区间范围为0-5460<br> 节点2的槽位区间范围为5461-10922<br> 节点3的槽位区间范围为10923-16383<br>注意:从节点是没有槽位的,只有主节点才有<br><br>存储查找<br> 对要存储查找的键进⾏crc16哈希运算,得到⼀个值,并取模16384,判断这个值在哪个节点的范围区间<br> 假设crc16("test_key")%16384=3000,就是节点⼀<br> crc16算法不是简单的hash算法,是⼀种校验算法<br>
什么是BigKey
在 Redis 中,当某个 Key 存储的值超过⼀定阈值时,就可能成为 BigKey<br>占⽤⽐较⼤的 Key,通常超过了 10KB,甚⾄达到了 M 或者 G 级别,会导致 Redis 的内存使⽤率过⾼<br>Redis⼀个字符串最⼤512MB,⼀个⼆级数据结构(例如hash、list、set、zset)可以存储⼤约40亿个<br>(2^32-1)个元素<br><br>经验值<br> 当String类型的数据>10K<br> list、hash、set、sort set中元素个数超过1000个时<br><br><div>问题点</div><div> 内存空间不均匀</div><div> 不利于充分利⽤各个节点的系统内存资源</div><div> IO和带宽阻塞</div><div> Redis单线程,操作bigkey的通常⽐较耗时,也就意味着阻塞Redis可能性越⼤</div><div> 假设⼀个bigkey为1MB,客户端每秒访问ᰁ为1000,那么每秒产⽣1000MB的流ᰁ,普通的千兆<span style="font-size:inherit;">⽹卡128MB/s 直接不可⽤</span></div><div> 过期删除</div><div> 设置了过期时间,当它过期后,会被删除,这个时候如果使⽤主线程,则会阻塞redis,影响正常<span style="font-size:inherit;">使⽤</span></div>
怎么来的:<br>开发⼈员程序设计不当,对于数据规模预料不清楚造成<br><br>案例<br> 社交类App:通篇⼀律采⽤⼀样的数据结构存储,⼤V粉丝列表和新⼿⼀样,1千万粉丝和10个粉丝<br> 业务缓存:从数据库检索的数据,整个对象序列化放在缓存⾥⾯,未确认是否需要全部字段存储<br> 新闻详情⻚、电商商品详情⻚<br>
如何发现bigkey<br>如何解决bigkey<br>
<font color="#e74f4c">redis官⽅提供redis-cli --bigkeys ⽤来查找<br></font> 给出每种数据结构的top 1 bigkey,同时给出了每种数据类型的键值个数以及平均⼤⼩<br> 注意事项<br> 由于bigkeys是调⽤scan,建议从节点执⾏<br><br>客户端统计<br> 封装jedis客户端,存储前判断下对象的⼤⼩和元素个数,对应超过阈值的打印相关⽇志<br> 结合elk采集和可视化监控展示<br>
<font color="#e74f4c">删除操作</font><br> big key删除不推荐使⽤ del key,会阻塞,推荐⽤lazy del,即unlink 命令<br> 异步延时释放key内存的功能,把key释放操作放在Background I/O单独的⼦线程处理,减少删除<br> key对redis主线程的阻塞<br><br><font color="#e74f4c">选择合适的数据结构</font><br> 多字段对象不采⽤String存储<br> 进⾏对象拆分,⽐如hash结构,mget获取需要的字段<br><br><font color="#e74f4c">优化数据存储架构</font><br> 存储不同的地⽅,⽐如⼤V的粉丝列表,超过⼀定数值后,账号标识级别<br> 不同的数据源或集群存储查找,避免和业务redis混⽤<br>
Redis分布式锁同步问题
就是保证同⼀时间只有⼀个客户端可以对共享资源进⾏操作<br>案例:优惠券领劵限制张数、商品库存超卖<br><br><font color="#e74f4c">核⼼</font><br>为了防⽌分布式系统中的多个进程之间相互⼲扰,我们需要⼀种分布式协调技术来对这些进程进⾏调度<br>利⽤互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题<br><br><font color="#e74f4c">解决⽅案</font><br> Redis+Lua脚本<br> 使⽤Redission框架(底层也是Lua+WatchDog机制)<br><br>⽣产环境下⼀般都是集群部署,则存在主从架构下的锁同步问题<br> 节点⼀ 在主节点获取分布式锁成功<br> Redis主节点再同步数据到从节点时宕机,数据没同步成功;⾼可⽤机制 则Redis从节点升级为主节点(但是没有锁信息)<br> 节点⼆ 在新的Redis上也成功获取分布式锁<br> 导致⼀个锁资源同时被两个节点获取,这个就出现了问题<br>
解决⽅案采⽤ Redlock 算法
Redis从3.0版本开始⽀持 Redlock 算法,通过在多个 Redis 节点上创建同⼀个锁来防⽌主从节点之间出现的数据不⼀致的问题<br>在 Redlock 算法中,需要从多个Redis节点获取锁,并对取锁结果进⾏校验,从⽽避免数据不⼀致性带来的问题。<br>锁变ᰁ由多个实例维护,即使有实例发⽣了故障,锁变ᰁ仍然是存在的,客户端还是可以完成锁操作<br>需要注意的是, redlock算法会引⼊⼀定的错误率,需要根据业务场景进⾏权衡和控制<br><br><font color="#e74f4c">加锁流程</font><br> 客户端 获取当前毫秒级时间戳,并设置超时时间 TTL<br> 依次向 N 个 Redis 服务发出请求,⽤能够保证全局唯⼀的 value 申请锁 key<br> 如果从 N/2+1 个 redis 服务中都获取锁成功,那本次分布式锁的获取被视为成功,否则获取锁失败<br> 如果获取锁失败,或执⾏达到 TTL,则向所有 Redis 服务都发出解锁请求<br><br><font color="#e74f4c">注意</font><br> 这种架构上redis全部节点都是主节点,没有从节点,抛弃了主从的异步复制<br> 各个节点之间没关系,不是集群也不是主从,互相独⽴<br> <font color="#e74f4c">由于使⽤⽐较复杂且概率性较低,但多数公司还是采⽤了主从架构</font><br> 在⼀些特定的场景且要求⾼的情况才会采⽤,且节点数会根据情况增加<br><br>⾮绝对安全<br> 解决了单Redis节点的分布式锁在failover的时候锁失效的问题<br> 但节点如果出现奔溃᯿启,对锁的安全性依旧存在问题<br> 场景:⼀共有5个Redis节点:A, B, C, D, E<br> 客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)<br> 节点C崩溃᯿启,但客户端1在C上加的锁没有持久化下来,aof机制导致<br> 节点C᯿启后,客户端2锁住了C, D, E,获取锁成功<br><br>分析<br> Redis 的 AOF 持久化⽅式是每秒执⾏fsync写⼀次磁盘,最坏情况下可能丢失1秒的数据<br> 尽可能不丢数据,Redis允许设置成每次修改数据都进⾏fsync,但会降低性能<br> 即使执⾏了fsync也仍然有可能丢失数据,因为也取决操作系统的刷盘策略,⽂件系统写到了buffer⾥⾯<br> 建议 <font color="#e74f4c">延迟重启</font>,⼀个节点崩溃后,先不⽴即重启它,⽽是等待⼀段时间再<span style="color:rgb(231, 79, 76);">重</span>启,这段时间应该⼤于锁的有效时间<br> 那这个节点在<span style="color:rgb(231, 79, 76);">重</span>启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响<br><br>注意<br> 测试完成redlock记得删除相关容器镜像<br> 注释相关代码,避免影响后续其他应⽤<br>
Redis缓存的读写的三种模式
背景<br>业务开发⾥⾯,基本都是从数据库读取到缓存⾥⾯<br>那缓存和数据库的读写顺序是怎样的?<br><br><font color="#e74f4c">Cache Aside</font><br> <font color="#ed9745">读写分离模式,是最常⻅的Redis缓存模式,多数采⽤</font><br> 读写数据时需要先查找缓存,如果缓存中没有,则从数据库中查找数据<br> 如果查询到数据,需要将数据放到缓存中,下次访问再直接从缓存中获取数据,以提⾼访问效率<br><font color="#e74f4c"> </font><font color="#ed9745">写操作通常不会直接更新缓存,⽽是删除缓存,因为存储结构是hash、list,则更新数据需要遍历</font><br><font color="#e74f4c">优点:</font>读取效率⾼,缓存命中率⾼,写操作与数据库同步,数据⼀致性较⾼,实现较为简单。<br><font color="#e74f4c">缺点:</font>数据库和缓存之间存在数据不⼀致的问题,需要考虑缓存失效和数据库更新操作带来的缓存不⼀致问题。<br>应⽤场景<br> 适⽤于读操作⾮常频繁⽽写操作相对⽐较少的情况,如电商⽹站的商品详情⻚,读取次数远⾼于更新和添加操作<br><br><font color="#e74f4c">Read/Write Through</font><br> 读写穿透模式,读写操作会直接修改缓存,然后再同步更新数据库中的数据,<font color="#ed9745">开发较为复杂,⼀般少⽤</font><br> 在Read/Write Through模式下,每次数据的读写操作都会操作缓存,再同步到数据库,以保证缓存和数据库数据的⼀致性<br><font color="#ed9745"> 应⽤程序将缓存作为主要的数据源,数据库对于应⽤程序是透明的,更新数据库和从数据库的读取的任务都交给缓存来实现<br></font><font color="#e74f4c">优点:</font>写操作速度较快,⼀致性较⾼,缓存与数据库的数据保持⼀致,缓存命中率较⾼。<br><font color="#e74f4c">缺点:</font>读操作较慢,如果缓存没有可⽤数据,每次都会进⾏数据库查询,数据ᰁ较⼤时会对性能带来较⼤的影响。<br>应⽤场景<br> 系统处理写操作频繁且读操作不频繁的场景,如云存储Ceph<font color="#ed9745"><br></font><br><font color="#e74f4c">Write Behind</font><br> 被称为Write Back模式或异步写⼊模式,<font color="#e74f4c">⼀般较少使⽤</font><br> 如果有写操作,缓存会记录修改了缓存的数据,但是并不会⽴即同步到数据库中<br> ⼀般会把缓存中的数据更新到磁盘中,等到后续有查询数据操作时,再异步批ᰁ更新数据库中的数据<br> 该模式的优点就是写操作速度很快,不会对性能产⽣影响,同时也避免了频繁更新数据库的情况,提升了数据库性能。<br><font color="#e74f4c">优点:</font>写操作速度快,性能较⾼,数据⼀致性⼀般较⾼。<br><font color="#e74f4c">缺点:</font>读操作较慢,由于异步⽅式更新数据库,可能会存在数据的延迟。<br>应⽤场景:<br> ⽤于较数据读写⽐᯿较⾼的场景,如游戏中的⽤户活动积分等信息,刚开始对写操作性能要求很⾼,后续查询⽐较少。<br><br><div>总结<br> 选择什么样的缓存模式需要根据⾃⼰的应⽤场景进⾏选择,根据应⽤场景选择合适的缓存模式可以提⾼系统效率和安全性。<br></div>
Redis更新数据库和缓存
背景<br>⾼并发业务下,Cache Aside 更新数据库和缓存的顺序问题,<font color="#ed9745">缓存中的是脏数据或者不⼀致</font><br><br>三种场景<br> 场景⼀:先更新数据库,再更新缓存<br> 线程A 更新数据库<br> 线程A 更新缓存<br> 线程A 数据库事务commit失败,进⾏rollback<br> 结果:缓存和数据库不⼀致<br><br> 场景⼆:先删缓存,再更新数据库<br> 线程A 删除缓存,更新数据库,但是还没commit<br> 线程B访问缓存,发现没数据,去数据库读取未commit的放到缓存(⽼数据 )<br> 线程A 进⾏了commit操作(数据库是新数据)<br> 结果:缓存是⽼数据和数据库是新数据,不⼀致<br><br>场景三:先删除缓存,再更新数据库,再删除缓存<font color="#ed9745">(推荐代码在架构大课)<br></font> 线程A 删除缓存,更新数据库,但是还没commit<br> 线程B访问缓存,发现没数据,去数据库读取未commit的放到缓存(⽼数据 )<br> 线程A 进⾏了commit操作(数据库是新数据)<br> 线程A 再次删除缓存数据(缓存为空,后续读取就是最新的)<br> 结果:数据⼀致性,但是浪费了多次IO
读多写少业务常⽤⽅案<br> 不直接读取数据库,数据库和缓存之间交互,靠定时任务同步<br> 读取分布式缓存,缓存命中直接返回,并更新本地缓存<br> 分布式缓存不命中,直接读取本地缓存并返回
Redis更新数据库和缓存其他方案
什么是多级缓存架构<br> 是⼀种通常⽤于优化应⽤程序性能且⾼可⽤的缓存技术<br> 通常由多层缓存组成,其中每⼀层缓存可以根据其不同的特征和作⽤进⾏选择和调整<br> 通过缓存数据的多份副本,最⼤化应⽤程序性能和可⽤性,避免缓存穿透、缓存击穿问题<br> 基本思想:<font color="#ed9745">将不同性能和可⽤性特征的缓存组合在⼀起,以最⼤限度地减少应⽤程序到源数据的访问量<br><br></font>https://blog.csdn.net/fdqzq/article/details/118525995<br>
案例⼀:springboot+本地缓存+分布式缓存<br> ⽅案简单可控,但缓存读取需要java程序读取,Tomcat并发量偏低
案例⼆:nginx+lua+分布式缓存<br>通过nginx+lua直连redis, 减少了数据加载路径,程序并发性能翻倍提升<br>缓存管理也可以nginx直接连接mysql并操作redis<br>⼆次开发lua脚本,不同应⽤程序维护多个key,数据库和缓存⼀致性问题需要解决
案例三:nginx+lua+canal+redis架构<font color="#ed9745">(架构大课有代码)</font><br>读:通过Lua查询Nginx的缓存,如果Nginx缓存没有数据,则查询Redis缓存,如果Redis缓存也没<br> 有数据,直接查询mysql<br>写<br> 写数据库,Canal监听数据库指定表的增量变化,Java程序消费Canal监听到的增量变化<br> Java-canal程序操作Redis缓存,Nginx本地缓存是否应⽤和失效,取决项⽬类型<br>
多线程
异步编程CompletableFuture
微服务架构下,接⼝单⼀职责,⼀个⻚⾯打开涉及多个模块需要同时调⽤<br>由于需要同时建⽴多个连接,中间会有性能损耗,部分⻚⾯需要使⽤聚合接⼝<br>则可以⽤CompletableFuture聚合多个响应结果⼀次性返回<br><br><font color="#f44336">优点:</font>1.减少建⽴连接数ᰁ<br> 2.⽹关和服务端可以处理更多连接<br><br><font color="#f44336">缺点:</font>1.如果接⼝性能差异⼤,则容易性能好的接⼝被性能差的拖垮<br> 2.需要开发更多接⼝,数据ᰁ⼤则需要更⼤的带宽<br><br><font color="#f44336">其他场景<br></font> 爬⾍业务多个URL并⾏爬取和解析处理<br> 商品详情⻚信息组装:主图信息,详情图信息,SKU信息,评价信息等<br> 业务聚合接⼝:⽤户信息(积分,基本信息,权限)<br>
CompletableFuture
JDK1.5有了Future和Callable的实现,想要异步获取结果,通常会以轮询的⽅式去获取结果<br><font color="#f44336">核⼼⽤途:</font><br> 在项⽬开发中,由于业务规划逻辑的原因,业务需要从多个不同的地⽅获取数据,<br> 然后汇总处理为最终的结果,再返回给请求的调⽤⽅,就是聚合信息处理类的处理逻辑<br> 如果常⽤串⾏请求,则接⼝响应时间⻓;利⽤CompletableFuture则可以⼤⼤提升性能<br> 针对多任务,需要进⾏任务编排调度,也可以使⽤CompletableFuture进⾏完成<br><br><div>CompletableFuture类实现了Future和CompletionStage接⼝,相当于⼀个Task编排⼯具</div><div><br><div>在没有指定线程池的情况下,使⽤的是CompletableFuture内部的线程池 ForkJoinPool ,线<span style="font-size:inherit;">程数默认是 CPU 的核⼼数</span></div><div> ⼀般不要所有业务共⽤⼀个线程池,避免有任务执⾏⼀些很慢的 I/O 操作,</div><div> 会导致线程池中所有线程都阻塞在 I/O 操作上,从⽽造成线程饥饿,影响整个系统的性<span style="font-size:inherit;">能</span></div></div>
CompletableFuture静态⽅法,<br>执⾏异步任务的API<br>
//⽆返回值,默认使⽤ForkJoinPool.commonPool() 作为它的线程池执⾏异步代码,可以⾃定义线程池<br>public static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor)<br><br><div>//有返回值,默认使⽤ForkJoinPool.commonPool() 作为它的线程池执⾏异步代码,可以⾃定义线程池</div><div>public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,Executor executor)</div>
//⽅法⽆返回值,当前任务正常完成以后执⾏,当前任务的执⾏结果可以作为下⼀任务的输⼊参数<br>thenAccept<br>//⽅法有返回值,当前任务正常完成以后执⾏,当前任务的执⾏的结果会作为下⼀任务的输⼊参数<br>thenApply<br>//对不关⼼上⼀步的计算结果,执⾏下⼀个操作<br>thenRun
//如果返回值没有返回,⼀直阻塞<br>V get()<br>//设置等待超时的时间<br>V get(long timeout,Timeout unit);<br>//有返回值就返回, 线程抛出异常就返回设置的默认值<br>T getNow(T defaultValue);
CompletableFuture嵌套
⽇常的任务中,通常定义的⽅法都会返回 CompletableFuture 类型,⽅便后续操作<br>然后将该任务的执⾏结果Future作为⽅法⼊参然后执⾏指定的⽅法, 返回⼀个新的CompletableFuture<br>任务它们之间存在着业务逻辑上的先后顺序<br><br><font color="#f44336">thenCompose<br></font> ⽤来连接两个CompletableFuture,是⽣成⼀个新的CompletableFuture,⽤于组合多个CompletableFuture<br> 也可以使⽤ thenApply() ⽅法来描述关系,但返回的结果就会发⽣ CompletableFuture 的嵌套<br> CompletableFuture<CompletableFuture< Product >> 这样的情况,需要get两次<br>
需要请求两个个接⼝,然后把对应的CompletableFuture进⾏合并,返回⼀个新的CompletableFuture<br><br><font color="#f44336">thenCombine<br></font>在两个任务都执⾏完成后,把两个任务的结果合并。<br>
<font color="#f44336">⽅法 allOf 和 anyOf</font><br>两个函数都是静态函数,参数是变⻓的 CompletableFuture 的集合,前者是「与」,后者是「或」<br><br><font color="#f44336">allOf:</font>返回值是 CompletableFuture< Void >类型<br> 因为allOf没有返回值,所以通过thenApply,获取每个 CompletableFuture 的执⾏结果<br><br><font color="#f44336">anyOf:</font>只要有任意⼀个 CompletableFuture 结束,就可以做接下来的事情,不像 allOf 要等待所有的 CompletableFuture 结束<br> 每个 CompletableFuture 的返回值类型都可能不同。<br> ⽆法判断是什么类型,所以 anyOf 的返回值是 CompletableFuture< Object >类型<br>
ThreadLocal<br>InheritableThreadLocal<br>TransmittableThreadLocal<br>
是基于线程的数据隔离,每个线程都有⾃⼰独⽴的ThreadLocal实例,线程之间的数据互相隔离<br><font color="#ed9745">主要解决的问题是在线程内部共享数据,避免线程间的数据竞争和同步问题<br>但是 在主线程中设置ThreadLocal的值,在⼦线程中是⽆法获取到该值的,即使它们是通过相同<br>的ThreadLocal对象操作</font>
是ThreadLocal的⼀个⼦类,它允许⼦线程继承⽗线程的数据,在主线程中设置InheritableThreadLocal的值,<br>在⼦线程中也能够获取到该值;当⼦线程创建时,会将⽗线程的InheritableThreadLocal值拷⻉⼀份,供⼦线程使⽤<br><br>主要解决的问题是⼦线程需要获取⽗线程的上下⽂数据的情况<br> 例如线程池中的任务需要获取主线程中的⼀些环境配置、会话信息等<br> 通过InheritableThreadLocal,这些数据可以在整个线程执⾏过程中共享,避免了每次传递这些数据的麻烦<br><br>但是 创建线程如果是⽤显示创建使⽤inheritableThreadLocal是没有问题,但如果<font color="#ed9745">是使⽤线程池就会有问题</font><br>⽐如线程复⽤时可能会导致数据残留,容易造成资源泄露或数据错乱等问题<br>
是由Alibaba开源的⼀个扩展库 - TransmittableThreadLocal(简称TTL)提供的⼯具类<br>不仅继承了InheritableThreadLocal的功能,还解决了⼀些InheritableThreadLocal的限制和问题。<br>主要解决在异步线程场景中,⼦线程任务需要继承⽗线程的数据,包括线程池中的异步任务、<br>消息队列的消费者线程等<br><br>TTL通过在线程间传递数据时对Java中的"线程上下⽂类加载器"、"线程ID"等进⾏了特殊处理<br>保证了在⼦线程中可以正确地继承⽗线程的ThreadLocal数,还⽀持线程池复⽤、<br>线程间数据清理和防⽌数据泄露等功能。<br>根据具体的使⽤场景和需求,选择合适的⼯具类来处理多线程共享数据问题。<br>
计算机⽹络
Linux服务器⽣产环境1
<font color="#f44336">⽣产环境Linux服务问题⼀</font><br><font color="#f44336">现象</font><br> 业务突然访问不了,诊断出是⽹络⽅向的问题<br> Linux实例的 /var/log/messages ⽇志信息全是类似“ kernel: TCP: time wait bucket table overflow ”的报错信息<br> 提示“ time wait bucket table ”溢出<br><br><font color="#f44336">原因分析</font><br> 参数 net.ipv4.tcp_max_tw_buckets 可以调整内核中管理TIME_WAIT状态的数ᰁ<br> 当实例中处于TIME_WAIT状态,需要转换为TIME_WAIT状态的连接数之和超过net.ipv4.tcp_max_tw_buckets 参数值<br> messages⽇志中将报“ time wait bucket table ” 错误,同时内核关闭超出参数值的部分TCP连接<br> 根据实际情况适当调⾼ net.ipv4.tcp_max_tw_buckets 参数,同时从业务层⾯去改进TCP连接<br><br><font color="#f44336">解决⽅案</font><br> 统计TCP连接数 netstat -atnp |grep tcp |wc -l ,如果确认连接使⽤很⾼,容易超出限制<br> vim /etc/sysctl.conf , 增加 net.ipv4.tcp_max_tw_buckets 参数值的⼤⼩<br> 执⾏ sysctl -p 命令,使配置⽴刻⽣效<br>
<font color="#f44336">⽣产环境Linux服务问题⼆<br>现象</font><br> 业务突然访问不了,诊断出是⽹络⽅向的问题<br> Linux实例中FIN_WAIT2状态的TCP链接过多<br><font color="#f44336"><br>原因分析</font><br> 在HTTP服务中,Server由于某种原因会主动关闭连接,作为主动关闭连接的Server就会进⼊FIN_WAIT2状态。<br> 如果Client不关闭,FIN_WAIT2状态将保持到系统᯿启,越来越多的FIN_WAIT2状态会致使内核Crash。<br> 建议调⼩ net.ipv4.tcp_fin_timeout 参数的值,加快系统关闭处于 FIN_WAIT2 状态的TCP连接<br> TCP连接处于 FIN_WAIT2 状态,下⼀步会进⼊ TIME_WAIT 状态<br><font color="#f44336"><br>解决⽅案</font><br> 统计 TCP处于连接的数ᰁ netstat -atn|grep FIN_WAIT2|wc -l<br> 执⾏ vim /etc/sysctl.conf 命令,调整 net.ipv4.tcp_fin_timeout = 30<br> 阿⾥云ECS默认是60秒,查看 sysctl -a | grep net.ipv4.tcp_fin_timeout<br> 执⾏ sysctl -p 命令,使配置⽴刻⽣效<br>
Linux服务器⽣产环境2
<font color="#f44336">⽣产环境Linux服务问题三<br>现象</font><br> 业务突然访问不了,诊断出是⽹络⽅向的问题<br> Linux实例中出现⼤ᰁCLOSE_WAIT状态的TCP连接<br><br><font color="#f44336">原因分析</font><br> 根据机器的业务ᰁ判断CLOSE_WAIT数ᰁ是否超出了正常的范围<br> TCP连接断开时需要进⾏四次挥⼿,TCP连接的两端都可以发起关闭连接的请求<br> 如果对端发起了关闭连接,但本地没有关闭连接,那么该连接就会处于CLOSE_WAIT状态<br> 虽然该连接已经处于半开状态,但是已经⽆法和对端通信,需要及时的释放该连接<br> 建议从业务层⾯及时判断某个连接是否已经被对端关闭,即在程序逻辑中对连接及时关闭,并进⾏检查<br><br><div><font color="#f44336">解决⽅案</font></div><div> 编程语⾔中对应的读、写函数⼀般包含了检测CLOSE_WAIT状态的TCP连接功能,</div><div> 查看处于CLOSE_WAIT状态的连接数 netstat -atnp|grep CLOSE_WAIT|wc -l</div><div> ⽐如是java业务,检查相关IO操作是否有正常关闭</div> 通过 read ⽅法来判断I/O ,当read⽅法返回 -1 时,则表示已经到达末尾<br> 通过 close ⽅法关闭该连接<br>
<font color="#f44336">⽣产环境Linux服务器问题四<br>现象</font><br> 业务突然访问不了,诊断出是⽹络⽅向的问题<br> 存在⼤量处于TIME_WAIT状态的连接<br><br><font color="#f44336">原因分析</font><br> 主动发起关闭的⼀端,在发送最后⼀个ACK之后会进⼊time_wait的状态<br> 该发送⽅会保持2MSL时间之后才会回到初始状态,MSL值是数据包在⽹络中的最⼤⽣存时间<br> 处于这个状态的TCP连接在2MSL等待期间,定义这个连接的四元组 客户端IP和端⼝,服务端IP和端⼝号 不能被使⽤<br><br><font color="#f44336">解决⽅案</font><br> 查看处于TIME_WAIT状态的连接数 netstat -atnp|grep TIME_WAIT|wc -l<br> 执⾏ vim /etc/sysctl.conf 命令,调整参数<br><br> 执⾏ sysctl -p 命令,使配置⽴刻⽣效<br> 除了的减少TIME_WAIT状态的连接,也可以通过扩⼤端⼝范围和对TIME_WAIT的bucket进⾏扩容等⼿段优化系统性能<br>
子#开启SYN的cookies,当出现SYN等待队列溢出时,启⽤cookies进⾏处理<br>net.ipv4.tcp_syncookies = 1<br>#允许将TIME-WAIT的socket᯿新⽤于新的TCP连接<br>net.ipv4.tcp_tw_reuse = 1<br>#开启TCP连接中TIME-WAIT的sockets快速回收功能<br>net.ipv4.tcp_tw_recycle = 1<br>#如果socket由服务端要求关闭,则该参数决定了保持在FIN-WAIT-2状态的时间。<br>net.ipv4.tcp_fin_timeout = 30
JVM
怎么防止死锁?
<font color="#f44336">互斥条件:</font>进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,<br> 只能等待,直至占有该资源的进程使用完成后释放该资源<br><font color="#f44336">请求和保持条件:</font>进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被<br>其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放<br><br><font color="#f44336">不可抢占条件:</font>是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放<br><font color="#f44336">循环等待条件</font>:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系<br><br><font color="#f44336">常见的解决办法:</font><br> 调整申请锁的范围<br> 调整申请锁的顺序<br> 做好代码review<br><br><font color="#f44336">排查方式</font><br> 方式一 : jps + istack+线程id<br> 方式二:jconsole
操作系统-虚拟机-JRE-JDK的关系
JDK包含JRE<br>JRE包含JVM<br>JVM包含操作系统 <br>
生产环境部署应用是安装J]DK还是JRE?
在生产环境部者Java程序时,一般会选择安装JRE而不是]DK<br>JRE安装包更小,打包成镜像更省空间,但是缺少相关调试开发工具,比如<br> 编译器: JRE中没有Java编译器,无法将ava源代码编译成字节码文件<br> 调试器: JRE中没有Java调试器,无法进行Java程序的调试和测试<br><br>但在某些特殊情况下,如需要进行Java程序的开发、编译、调试等操作时,就需要安装JDK<br>
说说 JVM 内存区域
<font color="#f44336">程序计数器:</font>也叫PC寄存器,用于记录当前线程执行的字节码指令位置,<br> 以便线程在恢复执行时能够从正确的位置开始。<font color="#f44336">线程私有<br>运行时数据区中唯一 不会出现OOM的区域,没有垃圾回收</font><br><br><font color="#f44336">Java虚拟机栈:</font>用于存储Java方法执行过程中的局部变量、方法参数和返回值,方法调用,<br> 以及方法执行时的操作数栈。<font color="#f44336">线程私有</font><br> <br>1.每个线程都有自己的虚拟机栈,其生命周期与线程相同<br><br><font color="#f44336">异常情况<br></font> 默认情况下JVM虚拟机找的大小是固定的,JDK1.5后通常为1MB<br> 如果线程在执行方法时需要更多的栈空间,JVM会抛出StackOverlowError异常<br> JVM参数 xss,⽐如 -Xss1m 表示1MB<font color="#f44336"><br></font><br><font color="#f44336">本地方法栈:</font>用于存储Java程序调用本地方法的参 数和返回值等信息。<font color="#f44336">线程私有</font><br><br><font color="#f44336">堆:</font>用于存储ava程序创建的对象,所有线程共享一个堆,堆中的对象可以被垃圾回收器回教,<br> 以便为新的线程共对象分配空间。<font color="#f44336">线程公有<br></font><br><font color="#f44336">元数据区/元空间(JDK8以后):</font>用于存储类的元数据信息,如类名、方法名、字段名等,以及动态生成的代理类、<br> 动态生成的字节码等。<font color="#f44336">线程公有</font><br> 元空间是位于本地(直接)内存中的,而不是像JDK8之前方法区位于堆内存中的。<font color="#f44336">线程公有</font><br>
说下JVM里面 堆 内存划分和<br>堆内存垃圾回收流程<br>
1.新建对象,放到Eden区,满后触发<font color="#d32f2f">Minor GC</font> (每次都是由Eden区满触发Minor GC,接连放对象到50或S1)<br>2.存活的对象移动到Survivor的S0区, 如果S0满后触发Minor GC<br>3.S0存活下来的对象移动到S1区,然后S0区空闲<br>4.S1满后触发<font color="#d32f2f">Minor GC,</font>再次移动到S0区,然后S1区空闲<br>5.反复GC每次对象涨1岁,到达一定次数后(默认15),进入老年代<br>6.当老年代内存不足会触发Full GC,出现STW (Stop The World)
1.堆被垃圾回收,基本都是采用<font color="#f44336">分代收集算法,</font>不同区域的采用不同的垃圾回收算法<br>2.方法结束后,堆中的对象不会马上移除,在垃圾回收的时候才会被移除(不是实时GC的)
JVM参数格式分类<br>JVM堆栈内存配置参数<br>JVM常见的命令行参数配置<br>
JDK8之后的方法区实现和元空间的联系
<font color="#f44336">什么是方法区和元空间<br></font>[方法区] 是JVM中用来存储类的元数据信息的区域,包括类的结构、<br> 方法、字段信息等,Java堆类似各个线理共享的内存区域<br><br>元空间、永久代是方法区具体的落地实现<br> java8之前是称为永久代 PermGen),java8后引入的一个新概念元空间] 用于替代旧版M中的永久代 (PermGen)<br> 方法区和永久代以及元空间的关系很像 Java 中接口和类的关系<br> 类实现了接口,<font color="#f44336">类就可以看作是永久代和元空间,接口可以看作是方法区</font><br> 永久代是DK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现便成为元空间<br><br> 元空间的大小是<font color="#d32f2f">动态的</font>,可以根据需要进行自动扩展,如果元空间不足,JVM会抛出 OutoMemoryError:Metaspace<br>
元空间大小配置
元空间大小配置<br><font color="#f44336">-XX:MetaspaceSize </font> 用来设置元空间初始大小的参数,它的默认值是21 MB<br><font color="#d32f2f">-XX:MaxMetaspaceSize </font>1用来设置元空间最大大小的参数,它的默认值是-] 即不限制,<br> 使用的是本地内存,不像旧版的永久代是堆内存<br> 2 如果不限制元空间的大小,可能会导致元空间占用过多的内存。从而引起内存溢出<br><br><font color="#d32f2f">系统参数查看 这</font>两个参致的单位是字节 (B),可以使用K、M、 G等后缀来表示更大的单位<br> 查看命令; jps #查看进程号<br> jinfo -flag MetaspaceSize 进程号 #查看Metaspace分配内存空间<br> jinfo -flag MaxMetaspaceSize 进程号 #查看Metaapace最大空间<br><br> <font color="#f44336"> 调整 -XX:MetaspaceSize=126m -XX:MaxMetaspaceSize=524m</font><br><font color="#d32f2f">空间不足异常信息 </font>Java.Lang.OutorMenoryError: Metaspace<br>
什么时候容易出现元空间不足的情况
1. 应用程序使用大量的反时技术,例如使用Class.forName0等方法加载类 或方便用Java Reflection API进行操作<br>2. 使用大量的动态代理技术,例如使用Java Proxy<br>3. 使用大量的注解,倒如使用Spring框架的注解等<br><br>4. 使用的第三方库过多,这些库可能会在运行时动态生成新的类,导致元空间内存占用过多<br>5. 应用程序的业务逻银比较复杂,需更加引大量的类<br>6. 应用程序使用大量的JSP页面,其中每个页面都对应一个类文件[现在很少)<br>
JVM虚拟机的类 加载子系统
<font color="#f44336">什么是类加载子系统<br></font> 是java虚拟机的一个重要子系统,主要负责将类的字节码加截到JVM内存的方法区,<br> 并将其转换为IM内部的数据结构<br><br><font color="#d32f2f">三大特点<br> 双亲委派模 </font><font color="#d32f2f">延识加载 </font><font color="#d32f2f">动态加载</font><br>
<font color="#f44336">三个模块组成类加载子系统<br></font> <font color="#d32f2f"> 加载器(ClassLoader) </font><br> 类加载器用父类加数器、子类加戴器这样的名字,虽然看似是继承关系,实际上是组合 (Composition)关系<br><font color="#d32f2f"> 结接器 (Linker) </font><br> 1. 负责将lava类的二进制代码链接到lava虑拟机中,并生成可执行的lava虑拟机代码,包括 验证、准备和解折 等<br> <font color="#d32f2f"> 初始化器 (initializer) </font><br> 1. 负责执行Java类的静态初始化,包括静态变量的赋值、静态代码块的执行等<br> 2. 初始化器是类加载子系统的最后一个阶段<br>
为啥需要这个双亲委派横型?<br>
1. 比如java.lang.Object 这些存放在rt.jar中的类,无论使用哪个类加载器加载,最终都会<br> 委派给最顶端的启动类加载器加载<br><br>2. 不同加载器加载的Object类都是同一个,如果没有使用双亲委派模型,各个类加载器<br> 自行去加载的话,就会出现问题<br><br>3. 比如用户编写了一个称为java.lang.Object或String的类,并放在classpath下<br>4. 那系统将会出现多个不同的Obiect类,Java类型体系中最基础的行为也就无法保证<br>
双亲委派机制
<font color="#f44336">加载流程:</font> 1 一个类的加载请求首先会被委派给其父类加戴器进行处理<br> 2 如果父类加载器无法加蒙该类,则会将加载请求委派给其自身进行加载<br> 3 如果自身也无法加载该类,则会将加载请求委派给其子类加载器进行处理,<br> 直到找到能够加载该类的类加想器为止<br><br><font color="#f44336">优点 </font>1. 可以保证类的唯一性和安全性。由于每个类加载器都只能加载自己的命名空间中的类<br> 2. 由于类加载器之间形成了一条继承链,因此可以保证类的安全性,防止恶意代码的注入<br>
新版本的JDK9后的类加载器
模块化系统中的类加载器可以分为两种类型<br> 1. 应用程序类加载器,加载器用于加载应用程序中的模块;平台类加载器,加载器用于加载JDK 中的模块<br> 2. 当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中<br> 3. 如果findLoadedModule 可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载<br><br>关键类 BuiltincCassLoader: <br> BuiltinClassLoader 是 JDK9 中代警 URLCassLoader 的加载器,是 PlatformClassLoader 与 AppClassloader 的父类
自定义类加载器流程
继承ClassLoader类<br><br>重写loadClass方法(会破坏双亲委派机制L.不推荐)<br>重写findClass方法(推荐)<br>
为啥需要用到自定义类加载器
为Java 应用程序<font color="#f44336">提供更加灵活和可定制的类加载机制</font><br><br>场景案例 1.拓展加载源 从网络,数据库等地方加载类<br> 2.防止源码泄漏 自定义类加载器可以加载加密的类文件,保护类的安全性<br> 3.实现类隔离 (tomcat里面大量应用) 自定义类加载器可以实现类隔离,避免类之间的冲突和干扰<br><br> <font color="#f44336"> 注意:</font><br> 比较两个类是否相等,只有两个类是由同一个类加载器加戴的前提下才有意义<br> 否则即使两个类来自同一个class文件,但是由于加戴他们的类加戴器不同,那这两个类就不相等<br> 不同类加载器加载同一个class文件得到的类型是不同的
ClassLoader核心源码解读<br> loadClass <font color="#f44336">用于加载指定名称的类,双亲委派模型核心实现,一般不建议重写相关方法</font>,直接由ClassLoader自己实现<br> 迎循双亲委派模型,首先委派给父类加载器进行加载,如果父类加载器无法加载该类,则自身进行加载<br><br> findClass <font color="#f44336">是用于意找类的方法,它通常由子类加截器实现,用于查找自身命名空间中的类</font><br> 由于历史IDK12之前版本兼容问题,<font color="#f44336">自定义类加载器则推荐重写这个方法</font><br> findCass()方法是在loadClass()方法中调用,当LoadClass()方法中加载失败后,则调用自己的findClass()方法来完成类加载<br> 但是ClassLoader的findClass没有实现,需要自己实现具体逻辑,findClass方法通常是和defineClass方法一起使用的<br><br> defindClass <font color="#f44336">是用于定义类的方法,它将字节数组转换为Class对象,并将其添加到类加载器的命名空间中</font><br> ClassLoader方法里面已经实现,findClass方法通常是和defineClass方法一起使用的<br><br> resolveClass <font color="#f44336">是用于解析类的方法,它将类的引用转换为实际的类,并进行链接和初始化</font>
findClass和 <br>loadClass区别<br>
<font color="#f44336">findClass() </font>用于写类加载逻辑<br><br><font color="#f44336">loadClass() </font>方法的逻辑里,如果父类加载器加载失败则会调用自己的findClass(方法完成加载,保证了双亲委派规则<br> 如果不想打破双亲委派模型,那么只需要重写findClass方法即可(推荐)<br> 如果想打破双亲委派模时,多数情况下需要重写整个loadClass⽅法
不同类加载器是否会重复加载同个class类
问题: 两个不同的类加数器加载同一class类,JVM是否认为它们相同。<br>答案: 不同类加载器会加载同名的Class 类,这些类在JVM 中是不同的,即它们的Class 对象是不同的<br><br>考查点 自定义类加载器知识<br> 定义类相同的条件 1类加载器相同 2 Class文件相同
<font color="#f44336">结论:</font> 在JVM 中,不同类加载器加载同一个类时,可能会出现重复加载的情况<br> 当不同类加载器加载同一个类时,每个类加载器都会在自己的命名空间中创建一个新的 Class 对象<br> 即使这些 Class对象的字节码是一样的,也会被认为是不同的类<br> 重复加载同一个类会导致一些问题,例如类的静态变量和代码块会被多次执行,导致出现意料之外的行为<br> JVM 采用了类的双亲委派模型来避免重复加载同一个类
新版JVM垃圾GC日志参数配置
GC日志<br> Java虚拟机中垃圾收集器在运行过程中输出的日志信息<br> 主要用于分析垃圾收集器的运行状态,优化垃圾收集器的工作效率以及定位垃圾收集相关的问题<br> GC日志会包含以下内容<br> 垃级收集器的名称和版本信息<br> 垃圾收集器的运行时间、开始时间和结束时间<br> 垃圾收集器的运行模式、垃圾收集算法和垃圾收集器的参数设置<br> 垃级收集器的运行情况,包括垃圾收集的次数、垃圾收集的时间。垃圾回收的内存空间等
参数配置
新版JDK的GC日志采用 Unified Logging<br> Java开发很多日志框架,slf4]、10g4 等,框架提供了统一的编程接口,方便用户进行个性化输出<br> JVM内部一直缺少类似的机制,从JDK9开始引入UnifiedLogging格式,是一种新的日志格式<br> <br>特点<br> 统一的日志格式:统一了GC、JIT、类加载等日志格式,使得日志更加易于分析。<br> 可定制化的日志输出:提供了丰富的日志输出远项,根据需要灵活配置,包括日志级别、标签、输出方式、输出格式等<br> 低开销的日志记录:采用异步日志记录机制,将日志记录与应用程序运行分离,降低日志记录对应用程序性能的影响<br><br>新版GC日志输出的组成部分<br> 时间戳: 记录GC发生的时间戳,精确到毫秒<br> 日志级别: 包括debug.trace. info. warning、error等<br> 日志的标签: 用于区分不同类是型的日志<br> 日志内容: 记录GC相关的信息,包括GC算法。GC的时间、GC前后的内存使用情况、回收的对象数量等<br>
新版GC日志配置格式
-Xlog:[selectors]:[output]:[decorators][output-options]<br> JVM采用的是<tag-set>=<level>的形式来表示selectors<br> 默认情况下tag为all,表示所有的 tag, level 为INFO<br> selector 可以进行组合的,不同的 selector 之间用这号分隔<br> 同时输出gc和 gc+metaspace 这两类 tag 的日志 -Xlog:gc=debuq,gc+metaspace:gc.log<br> -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xms524m -Xmx524m -XX:+PrintCommandLinePlags <br> -Xlog:gc=debug.gc+metaspace:gc.log<br><br> JVM 提供了通配符*来解决精确匹配的问题,比如想要所有 tag 为 gc的debug级别日志<br> -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xms524m -Xmx524m -XX:+PrintCommandLinePlags -Xlog:gc*=debug:gc.log
GC日志配置
GC日志配置
G1垃圾收集器
查看默认垃城收集:-XX:+PrintCommandLineFlags<br><br><font color="#f44336">核心逻辑(流程)<br></font>【保留了分代思想】把内存划分为多个独立的Region区域,每个区域中包含逻辑上的年轻代、老年代区域。<br>取消了年轻代、老年代的物理划分,不在单独对每个年代空间进行设置<br>Region区域类型是<font color="#f44336">动态变化的</font>,可能之前是年轻代,经过了垃圾回收之后就变成了老年代,实现更加精细化的垃圾回收<br><br><font color="#f44336">整体采用 标记整理算法, 局都采用标记复制算法, 不会产生内存碎片<br></font>把整个Java堆划分成约2048个独立Region块,每个Region块大小根据堆空间的大小而定,为2的N次幂,1MB~32MB<br> 每个Region的⼤⼩可通过参数 -XX:G1HeapRegionSize 配置<br><br>新增加一种叫 <font color="#f44336">Humongous内存区域,</font>用于存储大对象,<br>如果超过1.5个region,就是巨型对象,就放到H区,默认直接会被分配在老年代,一般被视为老年代<br>如果一个H区装不下一个巨型对象,G1会寻找连续的H区来存储,为了能找到连续的H区,有时需要启动Full GC<br>
G1提供三种模式垃圾回收模式
<font color="#f44336">Young GC</font><br>G1与之前垃圾收集器的Young GC不同,不是当新生代的Eden区放满了就进行垃圾回收<br>G1会计算当前Eden区回收大概需要多久时间,<br><font color="#f44336">如果接近参数 -XX:MaxGCPauseMills 设定的值,会触发Young GC</font><br><br>回收过程也是将Eden区和Survivor区中的存活对象复制到空闲的Survivor区,<br>并清空Eden区和原来的Survivor区,如果Survivor区也满了,那么会将存活对象复期到Old区。<br><font color="#f44336">在Young GC期间,应用程序会被暂停</font><br>
<font color="#f44336">Mixed GC</font><br>多数对象晋升到老年代old region时,为了谢免堆内存被耗尽问题,会触发混合的GC<br>回收整个Young Region区域,还会回收一部分的Old Region区域,<br><font color="#f44336">注意不是全部Old Region区域</font><br><br><font color="#f44336">触发条件:</font>参数 -XX:InitiatingHeapOccupancyPercent=n 决定<br> 默认:45%,即 当⽼年代⼤⼩占整个堆⼤⼩百分⽐达到该阀值时触发<br><br><font color="#f44336">Ful GC:</font>单个线程会对整个堆的所有代中所有分区做标记,清除以及压缩动作,非常耗时<br><br><font color="#f44336">总结:<br></font>在Young GC和Mixed GC中,G1垃圾收集器都会对每个Region的存活对象数量进行统计<br>根据存活对象数量和空闲Region的数量,动态地决定垃圾收集的区域和顺序<br>这种动态的垃圾收集策略,可以避免Full GC的发生,提高了应用程序的响应速度<br>
G1的Mix GC回收过程<br>分为以下四个步骤<br>
<font color="#f44336">1. 初始标记:</font>记录下GC Roots能直接引⽤的对象,并标记所有存活的对象,会执⾏⼀次年轻代GC,速度很快,<span style="color:rgb(244, 67, 54); font-size:inherit;">需要STW</span><br><font color="#f44336">2. 并发标记:</font>与应⽤线程⼀起⼯作,进⾏可达性分析<br> G1收集器会对堆内存进⾏并发标记,找出所有存活的对象,并记录它们所在的Region<br><br><font color="#f44336">3. 最终标记:</font>修正并发标记期间, 部分因程序运⾏导致发⽣变化的那⼀部分对象,根据算法修复⼀些引⽤的状态,<font color="#f44336">需要STW</font><br><font color="#f44336">4. 筛选回收:</font>对各个Region的回收价值和成本进⾏排序,根据⽤户所期望的GC停顿STW时间,<br> 即 -XX:MaxGCPauseMillis 制定计划<font color="#f44336">(需要STW)</font><br><br><div>成本排序案例</div><div> 现在有Region1、Region2和Region3三个区域</div><div> Region1预计可以回收1.5MB内存,预计耗时2MS,投产⽐ROI=1.5/2</div><div> Region2预计可以回收1MB内存,预计耗时1MS,投产⽐ROI=1/1</div><div> Region3预计可以回收0.5MB内存,预计耗时1MS,投产⽐ROI=0.5/1<br><br></div><div> 那Region1、Region2和Region3各⾃的回收价值与成本⽐值分别是:0.75、1和0.5,</div><div> ⽐值越⾼说明同样的付出收益越⾼,如果此时只能回收⼀个Region的内存空间,G1就会<span style="font-size:inherit;">选择Region2进⾏回收</span></div><div> 保证了G1收集器在有限的时间内尽可能地提⾼收集效率</div>
G1垃圾收集器和常⻅参数
启⽤G1垃圾收集器 -XX:+UseG1GC<br>-XX:G1HeapRegionSize=n<br> Java 堆⼤⼩划 分出约 2048 个区域,默认是堆内存的1/2000;配置需要为2的N次幂,1MB~32MB<br> 使⽤G1垃圾回收器最⼩堆内存应为 1MB*2048=2GB ,低于这个的建议使⽤其它垃圾回收器。 <br><br>XX:MaxGCPauseMillis=n<br> 设置最⼤停顿时间,单位为毫秒,默认为200毫秒(JVM会尽⼒实现,但不能保证达到)<br>-XX:ParallelGCThreads=n<br> 设置 STW ⼯作线程数的值。⼀般设置为逻辑处理器的数ᰁ,最多为 8<div> 是在STW阶段,并⾏执⾏【垃圾收集动作】的线程数<br><br></div>-XX:ConcGCThreads=n<br> 在【并发标记】阶段,并发执⾏标记的线程数,⼀般将 n 设置为并⾏垃圾回收线程数(ParallelGCThreads) 的 1/4<br>-XX:InitiatingHeapOccupancyPercent=n<br> 设置G1 Mix垃圾回收的触发阈值,默认为45%<br><br><span style="color:rgb(244, 67, 54);">相关命令参数使⽤(JDK11):<br></span> -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xms524m -Xmx524m -XX:+PrintCommandLineFlags<br>
应⽤场景<br> ⼤型应⽤程序:可以将堆内存划分为多个区域,以实现更加精细化的垃圾回收。<br> ⾼并发、低延迟:对响应时间要求较⾼的应⽤程序,如Web应⽤程序、电⼦商务等⾼并发场景<br> ⼤内存应⽤:可以在垃圾回收过程中释放⼤ᰁ的空间,提⾼了内存的利⽤率。
ZGC垃圾收集器
是Oracle公司开发一种<font color="#f44336">可伸缩、低停顿时间的垃圾收集器,标记-复制算法(进行了改进)</font><br>垃圾回收过程<font color="#f44336">几乎全部都是井发,实际STW停顿时间极短,停顿时间控制10ms内</font>,主要采用的<font color="#f44336">染色指针和读屏障技术</font><br>在 JDK11 中是实验性的特性引入,在JDK15 中ZGC可以正式投入生产使用,使⽤ –XX:+UseZGC 启⽤<br><br>ZGC 的堆内存也是基于 Region 来分布和G1类似,不区分新生代老年代的,Region 支持动态地创建和销毁,大小不是固定<br><font color="#f44336">三种类型的 Region<br></font> 小型页面 Small Region: 容量固定2MB,主要用于放置小于 256 KB 的小对象<br> 中型页面Medium Repion: 容图定32MB,主要用于放置大于等于 256 KB 小于4MB 的对象<br> 大型页面Large Region: 容量不周定为N*2MB,Region 是可以动态变化的,但必须是 2MB 的整数倍,最小支持 4 MB<br>
<font color="#f44336">三大特点<br>低停顿时间</font><br> ZGC最大的特点是在不增加延迟的情况下,能够处理非常大的内存数据<br> 可以将停顿时间限制在10ms以内,对于需要快速响应的应用程序来说是非常重要<br><br><font color="#f44336">可伸缩性<br></font> 以处理非常大的内存数据,适应不同规横的应用程序,从小形应用程序到大型企业级应用程序<br><font color="#f44336">不需要分代<br></font> 不需要将内存分为新生代和老年代,不需要复杂的内存回收算法<br><font color="#f44336">并发处理<br></font> 采用了并发处理的方式来进行垃圾回收可以在应用程序运行的同时进行垃圾回收<br>
<font color="#f44336">工作流程<br>初始标记(STW): </font> 找 GC Roots 直接引⽤的对象,处理时间和GC Roots的数ᰁ成正⽐,停顿时间不随着堆的⼤⼩⽽增加<br><font color="#f44336">并发标记(没有STW):</font> 扫描剩余的所有对象,处理时间⽐较⻓,业务线程与GC线程同时运⾏,但这个阶段会有漏标问题<br><br><font color="#f44336">再标记 (STW): </font>通过算法解决漏标对象,和G1中的解决漏标的算法类似<br><font color="#f44336">并发转移准备 (没有STW) </font>:分析最有回收价值GC分页,即ROI计算<br><font color="#f44336">初始转移(STW): </font>转移初始标记的存活对象和做对象᯿定位,时间和GC Roots的数ᰁ成正⽐,时间不随堆的⼤⼩⽽增加<br><font color="#f44336">并发转移 (没有STW) : </font>对转移并发标记的存活对象做转移<br><br><span style="color:rgb(244, 67, 54);">相关命令参数使⽤(JDK17):<br></span> -XX:+UseZGC -XX:+PrintCommandLineFlags -Xms32m -Xmx32m
对象可回收,就⼀定会被回收吗?
不⼀定会回收,对象的finalize⽅法给了对象⼀次最后⼀次的机会。<br> 当对象不可达(可回收)并发⽣ GC 时,会先判断对象是否执⾏了 finalize ⽅法,如果未执⾏则会先执⾏ finalize ⽅法<br> 将当前对象与 GC Roots 关联,执⾏ finalize ⽅法之后,GC 会再次判断对象是否可达<br> 如果不可达,则会被回收,如果可达,则不回收!<br><br>注意:finalize ⽅法只会被执⾏⼀次,如果第⼀次执⾏ finalize ⽅法,对象变成了可达,则不会回收<br> 但如果对象再次被 GC,则会忽略 finalize ⽅法,对象会被直接回收掉<br>
可达性分析算法为什么可以解决<br>循环引⽤造成的内存泄漏问题?<br>
当两个或多个对象相互引⽤时,它们的引⽤链会形成⼀个环<br>但是由于这个环中的对象与GC Roots没有任何引⽤链相连,<br>所以JVM会将这些对象判定为不可⽤的,从回收它们<br>
新⽣代收集<br>⽼年代收集<br>整堆收集(Full GC)<br>
对象从Young 区域消失的过程称为”minor GC / Young GC “<br>Eden 的清理,S0\S1的清理都由于MinorGC<br>YoungGen区内存不⾜,会触发minorGC<br><br>对象从⽼年代中消失的过程,清理整合OldGen的内存空间,称为”Major GC/Old GC“<br>有些垃圾收集器 针对⽼年代单独回收,所以⽐较少⽤<br><br>是清理整个堆空间,包括年轻代和⽼年代<br>理解为Major GC+Minor GC组合后进⾏的⼀整个过程,是清理JVM整个堆空间<br>
注意<br> 不⽤去关⼼是叫 Major GC 还是 Full GC,应该关注当前的 GC 是否停⽌了所有应⽤程序的线程<br> 许多Major GC 是由 Minor GC 触发的, 出现 Major GC通常出现⾄少⼀次的 Minor GC<br> MajorGC 的速度⼀般⽐ Minor GC 慢 10倍以上
jstack命令CPU占⽤高原因
分析思路<br> 1.确认问题:是否是 CPU 过⾼导致的应⽤程序性能问题<br> 2.确认进程 ID,使⽤top命令查找该【进程】下CPU使⽤最⾼的【线程】 top -Hp 进程id<br> 3.分析线程堆栈信息:使⽤ jstack 命令查看 Java 应⽤程序中所有线程的堆栈信息。<br> 把⼗进制的线程id转为16进制 printf "%x\n" 线程id<br><br> 4. 定位问题线程堆栈信息,⼀般会⽣成快照到⽂本⽂件⾥⾯进⾏分析。jstack -l [PID]>/tmp/log.txt<br> 5. 分析解决问题,根据线程id的⼗六进制查找所属代码的位置,采取相应的措施解决问题<br> ⽂本搜索,cat、more、vim⽅式都⾏<br>
如何排查CPU长时间100%
1、先通过top命令找到消耗cpu很高的进程id<br>2、根据进程找到消耗cpu很高的线程ID<br>3、对当前进程做stack,输出所有的堆栈信息<br>4、将第4步得到的线程ID转成16进制 (BA9、BAA5)<br>5. 根据得到16进制的线程ID找到堆栈的具体信息<br>6. 解读堆栈信息,定位问题及代码位置<br>
CPU指标
查看占用CPU最多的进程<br>查看占用CPU最多的线程<br><br>查看线程堆栈快照信息<br>分析代码执行热点<br><br>查看哪个代码占用CPU执行时间最长<br>查看每个方法占用CPU时间比例
// 显示系统各个进程的资源使用情况 top<br>// 查看某个进程中的线程占用情况 top -Hp pid<br><br>// 查看当前 Java 进程的线程堆栈信息 jstack pid<br><br><font color="#e74f4c">常见的工具:JProfiler、JVM Profiler、Arthas等。</font><br>
JVM 内存指标
查看当前 JVM 堆内存参数配置是否合理<br>查看堆中对象的统计信息<br><br>查看堆存储快照,分析内存的占用情况<br>查看堆各区域的内存增长是否正常<br><br>查看是哪个区域导致的GC<br>查看GC后能否正常回收到内存
<br>// 查看当前的 JVM 参数配置 ps -ef | grep java<br>// 查看 Java 进程的配置信息,包括系统属性和JVM命令行标志 jinfo pid<br><br>// 输出 Java 进程当前的 gc 情况 jstat -gc pid<br>// 输出 Java 堆详细信息 jmap -heap pid<br><br>// 显示堆中对象的统计信息 jmap -histo:live pid<br>// 生成 Java 堆存储快照dump文件<br>jmap -F -dump:format=b,file=dumpFile.phrof pid<br><br><font color="#e74f4c">常见的工具:Eclipse MAT、JConsole等。</font><br>
JVM GC指标
查看每分钟GC时间是否正常<br>查看每分钟YGC次数是否正常<br><br>查看FGC次数是否正常<br>查看单次FGC时间是否正常<br><br>查看单次GC各阶段详细耗时,找到耗时严重的阶段<br>查看对象的动态晋升年龄是否正常
<font color="#e74f4c">GC日志常用 JVM 参数:</font><br>// 打印GC的详细信息 -XX:+PrintGCDetails<br>// 打印GC的时间戳 -XX:+PrintGCDateStamps<br><br>// 在GC前后打印堆信息 -XX:+PrintHeapAtGC<br>// 打印Survivor区中各个年龄段的对象的分布信息 -XX:+PrintTenuringDistribution<br><br>// JVM启动时输出所有参数值,方便查看参数是否被覆盖 -XX:+PrintFlagsFinal<br>// 打印GC时应用程序的停止时间 -XX:+PrintGCApplicationStoppedTime<br>// 打印在GC期间处理引用对象的时间(仅在PrintGCDetails时启用) -XX:+PrintReferenceGC<br>
JVM 有哪些核心指标
<font color="#e74f4c">单台服务器来说:<br></font>jvm.gc.time:每分钟的GC耗时在1s以内,500ms以内尤佳<br>jvm.gc.meantime:每次YGC耗时在100ms以内,50ms以内尤佳<br><br>jvm.fullgc.count:FGC最多几小时1次,1天不到1次尤佳<br>jvm.fullgc.time:每次FGC耗时在1s以内,500ms以内尤佳<br>
Spring
Spring源码核心思想IOC和DI区别
<font color="#f44336">什么是lOC<br></font> 对象的创建、管理、销毁等过程不再由开发人员自行控制,而是由容器来帮助我们管理<br> 容器负责从配置文件中读取bean的定义以及对bean的创建和销毁等过程<br> 开发人体只需在容器中配置好Bean的定义,容器便会管理Ben的生命周期,完成相应的操作<br><br><font color="#f44336">为啥叫[控制反转]</font><br> 控制:指的对象创建,管理的工作<br> 反转: 想工作交给外部环境,比如 sprng框架的OC容器<br><br><font color="#f44336">什么是DI<br></font> 在lOC的基础上,容器自动将一些对象的依赖关系注入到另一个对象中,也就是一个对象被注入了它所依赖的对象<br> 比如:可以将DAO和Service注入到Controller中. Controller就可以轻松的使用DAO和Service中的方法<br><br><font color="#f44336">DI主要有三种注入方式:<br></font> 1. Setter Injection(Setter注入)<br> 2. Constructor lnjection (构造函数注入)<br> 3. FieldInjection (字段注入)<br><br><font color="#f44336">总结 IOC和DI描述的是同一件事情,就是角度不一样<br></font> 1. IOC是在对象的角度,把对象的管理创建交给了IOC容器<br> 2. DI是在容器的角度,容器把对象里面需要依赖的其他对象注入进去
Spring之Bean生命周期
1、解析类得到BeanDefinition<br>2、如果有多个构造方法,则要推断构造方法<br>3、确定好构造方法之后,进行实例化得到一个对象<br>4、对象中的加@Autowired注解的属性进行属性填充<br>5、回调Aware方法,比如BeanNameAware,BeanFactoryAware,ApplicationContextAware<br><br>6、调用BeanPostProcessor的<font color="#d32f2f">初始化前</font>的方法<br>7、调用初始化方法<br>8、调用BeanPostProcessor的<font color="#d32f2f">初始化后</font>的方法,在这里会进行AOP<br>9、如果当前创建的bean是单例的话,则会把bean放入单例池中。多列(把准备就绪的bean交给调用者)<br>10、使用bean<br>11、Spring容器关闭时调用DisposableBean中destory()方法
<font color="#f44336">小滴<br>Spring之Bean生命周期</font><br>
1 lOC 容器通过配置文件读取 Bean 的定义信息<br>2 BeanFactoryPostProcessor的PostProcessBeanFacton方法对bean定义信息处理<br>3 通过反射实例化对象,构造函数被调用<br>4 设置属性值<br><br>5 调用BeanNameAware的setBeanName方法<br>6 调用BeanFactoryAware的setBeanFactory方法<br>7 调用ApplicationContextAware的setApplicatlonContext方法<br><br>8 调用BeanPostProcessor的postProcessBeforelnitializatlon方法<br>9 实现了InitializingBean调用afterPropertiesSet方法,如果声明了init-method也会调用<br><br>10 实现了BeanPostProcessor调用postProcessAfterlnitializatlon方法<br>11 【Bean准备就堵,可以被应用程序使用】 单列(spring缓存池) , 多列(把准备就绪的bean交给调用者)<br>12 如果实现DisposableBean,调用destony方法,如果声明了destor-method也会被调用<br>
BeanFactoryPostProcessor
Spring框架中一个重要的扩展接口,可以在spring的bean创建之前,可以修改Bean的定义属性,注入第三方数据等<br><font color="#f44336">在Spring 容器加载定义 bean 的XML 文件之后,在 bean 实例化之前执行的,对Bean进行后置处理<br><br></font>在BeanFactory初始化时执行,它的执行顺序是在BeanFactory的后置处理器 (BeanPostProcessor) 之前。<br>BeanFactoryPostProcessor接口的实现类必须由开发人员自行编写,并在Spring的配置文件中配置对应的Bean实例<br>常用的实现类有PropertyPlaceholderConfigurer、PropertyOverrideConfigurer等<br>
作用<br> 读取应用程序上下文 (ApplicationContext) 中的所有Bean定义<br> 修改定义中的属性值等信息<br> 对修改后的定义进行处理,使其达到预期效果<br> 将修改后的Bean定义反馈到容器中,容器将重新执行Bean创建和初始化的过程<br><br>//ConfigurableListableBeanPactory 可以获取bean定义信息,里面进行修改bean定义<br>
BeanPostProcessor
是Spring loC容器提供的一个扩展机制,用于拦截和修改新实例化的Bean对象的过程<br>默认是对整个Spring容器中[所有的bean] 进行处理,如要对具体的某个bean处理,通过方法参数判断即可<br><font color="#f44336">在容器实例化Bean对象后(执行构造函数),初始化方法执行[前后] 的回调方法,提供Bean实例初始化期间加入自定义逻辑<br></font><br>初始化方法,指的是以下两种<br> bean 实现InitializingBean 接口,对应的方法为 afterPropentiesSet<br> XML中定义 bean 的时候,有个属性叫做 init-method 指定初始化方法
<font color="#f44336">注意<br></font> BeanPostProcessor 是在spring 容器加载了bean 的定义文件,且实例化 bean 之后执行<br> BeanPostProcessor 的执行顺序是在 BeanFactoryPostProcessor 之后<br><br>BeanPostProcessor 都是处理 bean 的生命周期中拓展点,使用场景不同<br> BeanFactorvPostProcessor作用于bean 实例化之前,读取配置元数据BeanDefinition,且可以修改<br> BeanPostProcessor 作用于bean 的实例化过程中,可以改变 bean 实例的值
顶层类 BeanFactory
最核心的接口之一,主要负责创建和管理bean对象实例,具体包括定义、加毅、<br> 实例化和维护Bean之间的依赖关系等<br><br><font color="#f44336">主要作用<br></font> 加载Bean的配置信息: BeanFactory根据XML文件中定义的Bean信息构造Bean实例,并装载到容器中<br> 实例化Bean: BeanFactorv在Bean的定义信息加后,利用ava反射机制来实例化Bean,并根据依赖关系装配Bean实例<br> 维护Bean之间的依赖关系: BeanFactory能够自动识别Bean之间的依赖关系,实现Bean的依赖注入<br> 提供统一的配置方式:BeanFactory可以将所有Bean的配置信息放在一起,提供统一的配置入口<br> 对Bean进行作用域管理: BeanFactory负责对Bean的作用域进行管理,如: 单例、多例等<br><br><font color="#f44336">BeanFactory 只是个接口,并不是IOC容器的具体实现,Spring容器给出了很多种实现<br></font> 1. ClassPathXmlApplicationContext 使用XML配置<br> 2. AnnotationConfigApplicationContext 使用注解配置<br> 3. ApplicationContext 、ConfigurableApplicationContext<br> 4. BeanFactory、 ListableBeanFactory<br><font color="#f44336"><br>总结</font><br> 1. BeanFactory是顶层接口,定义多数最基础的API,称为[基础容器]<br> 2. 对应BeanFactory的子类ApplicationContext,可以基于不同需求拓展更多的功能,称为[高级容器]<br> 3. 这样的设计避免全部功能都集中在一个类,分散到不同接口,实现的时候根据需求选择即可
FactoryBean介绍
什么是FactoryBean<br> BeanFactory区分开来,BeanFactory是一个Bean工厂,创建和管理bean的工厂,是顶级接口。<br> FactoryBean 用来自定义Bean的创建过程,完成复杂Bean的定义<br> Spring中Bean主要有有两种<br> 普通Bean<br> 工厂Bean(FactoryBean)<br> FactoryBean是一个工厂bean,可以生成某一个类型的Bean实例,通过实现该接口定制实例化bean的逻辑<br> 在Spring框架中非常重要,Spring自身就提供了70多个FactonrBean接口的实现<br> 通过 getBean0方法返回的不是FactoryBean本身<br> 而是FactorvBean#petObject0方法所返回的对象,相当于FacoryBean#getObject0代理了getBean() 方法
BeanFactory/FactoryBean<br>有什么区别?<br>
<font color="#f44336">【BeanFactory是IOC容器的顶层接口】</font>,<br>是一个Bean工厂,负责创建和管理bean对象实例,<br>具体包括定义、加载Bean的配置信息、实例化和维护Bean之间的依赖关系,<br>提供统⼀的配置⽅式,对Bean进⾏作⽤域管理等。<br><br><font color="#f44336">【FactoryBean 是接口】<br></font>是⼀个⼯⼚bean,可以⽣成某⼀个类型的Bean实例,通过实现该接⼝定制实例化<br>bean的逻辑,用来自定义Bean的创建过程,完成复杂Bean的定义。<br><br>通过 getBean( )⽅法返回的不是FactoryBean本身<br>⽽是FactoryBean#getObject()⽅法<font color="#f44336">所返回的对象</font>,FactoryBean#getObject()代理了getBean()⽅法<br> Spring中Bean主要有有两种<br> 普通Bean<br> ⼯⼚Bean(FactoryBean)<br>
BeanDefinition介绍
是Spring容器中最重要的概念之一,它是容器创建和管理Bean实例的基础,对Bean的定义信息的抽象和封装<br>描述一个Bean的定义信息,包括Bean的名称,类型、作用域、属性等信息<br><br>可以对Bean的创建和管理进行详细的配置和控制,例如可以指定Bean的作用域、是否懒加载,是否自动注入等属性<br>在XML 中定义的 bean标签,被Spring 解析成为一个JavaBean,这个avaBean 就是 BeanDefinition<br>
什么是Spring容器
Spring的应用上下文Application Context,官方称为IOC容器,存储Bean的叫单例池,包括Map和其他多个组件<br><br>主要就是: BeanFactony和ApplicationContext<br> BeanFactory是Spring安器最基本的接口,该接口定义了保存和管通Bean定义和实列的通用规范<br> ApplicationContext是BeanFactory的子接口,它提供了更多面向应用程序的功能,如国际化支持、事件传递、<br> Bean命周期管理、AOP集成、消息资源处理、事务管理等等<br><br>总结: 关键类的关键方法入口AbstractApplicationContext类的refresh方法<br>
Spring之Bean的懒加载流程
Bean的懒加载<br> 指在容器启动时,不会实例化所有Bean,只有在第一次使用该Bean时才会进行实例化,提高容器的启动速度和资源使用效率<br> 使用<br> 在Spring中,可以通过在BeanDefinition上设置lazy-init属性为true来开启微加载<br> 可以通过设置@Bean注解的lazy属性来实现方法级别的懒加载
Spring 循环依赖
循环依赖<br> 指的是两个或多个Bean之间相互依赖,如Bean A依赖于BeanB,而Bean B又依赖于BeanA,则会出现循环依赖的情况<br> Spring默认采用Singleton模式,Bean默认是单例的,在容器启动时就会创让Bean实例,同时也会注入它所依赖的Bean实例<br> 当出现循环依赖的情况时,Spring的Bean实例化顾略就会出现问题<br><br>大家开发过程中好像对循环依赖这个概念无感知,这种错觉是因为工作在Spring的中,已经帮你解决了<br>
解决方案<br> Spring引入了“提前暴露Bean”的机制,在创建A对象时,会先创建一个A的空对象并将其添加到缓存池中<br> 即“提射暴露Bean”,然后继续创建B对象,将其注入A对象中<br> 在创建B对象时,由于A对像已经在缓存池中,可以直接获取到A对象,接着将B对象注入到A对象中,完成Bean的初始化注意<br> <br>注意 :循环依赖的场景<br> 单例<br> 构造器循环依赖《构造器注入,循环依赖无法解决,日常开发不推荐构造器注入》<br> Field属性循环依赖 (set方法注入,循环依赖问题可以解决,通过三级缓存)<br> 多列<br> prototype类型bean 《循环依赖无法解决,构造函数和属性注入都不能解决循环依赖》<br> AbstractBeanFactory #doGetBean
Spring三级缓存
Spring 使用三级缓存<font color="#f44336">(都是Map结构)</font>去解决循环依赖的,其「核心逻辑就是把<font color="#f44336">实例化和初始化</font>的步骤分开,<br> 然后放入缓存中」,供另一个对象调用<br><br><font color="#f44336">「第一级缓存」 new ConcurrentHashMapo( initialCapacity: 256);<br></font> singletonObiects 单例池, 用来保存实例化、初始化都完成的对象,<br> 存储已经创建好的无代理的单例Bean对象,从该缓存中取出的 bean 可以直接使用<br><br><font color="#f44336">「第二级缓存」new HashMap( initialCapacity: 16);<br></font> earlySingletonObjects用来保存实例化完成,但是未初始化完成的对象, <font color="#f44336">那么多线程环境下可能取到的对象就不⼀致了。</font><br> 提前曝光的单例对像的cache。存放原始的 bean 对象(尚未填充属性),用于解决循环依赖。<br><br><font color="#f44336">「第三级缓存」 new HashMap( initialCapacity: 16);<br></font>singletonFactories用来保存一个<font color="#f44336">对象工厂,</font>提供一个匿名内部类,它可以⽣成代理对象,<br>也可以是普通的实例对象,用于创建二级缓存中的对象,使⽤三级缓存主要是为了保证不管什么时候<font color="#f44336">使⽤的都是⼀个对象。</font><br>单例对象工厂的cache,存放ben 工厂对象,解决循环依赖避免Bean的实例化顺序出现问题<br>
注意:循环依赖的场景<br> 单例:构造器循环依赖《构造器注入,循坏依然无法解决,日需开发不推荐构造器注入)<br> Feld属性循环依赖 (set方法注入,循环依赖问题可以解决 通过三级缓存)<br><br> 多列:prototype类型bean (循环依赖无法解决,构造函数和属性注入都不能能决循环依赖]<br> AbstractBeanFactory #doGetBean<br> 如果正在创建一个原型实倒,则抛出异常,表示当前单例实例与其存在循环依赖
使用三级缓存,<br>二级缓存不能解决吗?<br>
<font color="#f44336">主要是为了⽣成代理对象。<br></font>因为三级缓存中放的是⽣成<font color="#f44336">具体对象的匿名内部类</font>,他可以⽣成代理对象,也可以是普通的实例对象。<br>使⽤三级缓存主要是为了保证不管什么时候使⽤的<font color="#f44336">都是⼀个对象</font>。<br><br>假设只有⼆级缓存的情况,往⼆级缓存中放的显示⼀个普通的Bean对象, BeanPostProcessor 去⽣成<br>代理对象之后,覆盖掉⼆级缓存中的普通Bean对象,<font color="#f44336">那么多线程环境下可能取到的对象就不⼀致了。<br><br>二级缓存不能解决吗:</font>可以,但风险比三级缓存更大。二级缓存存放的是半成品的bean,没有过多的扩展,<br>如果仅仅用于解决循环依赖是完全可以的,但是三级缓存是一个Factory,里面可以在创建的前后嵌入我们的代码<br>和前后置处理器,Aop(getObject中)之类的操作就发生在这里,他的扩展性比二级缓存强。<br><br>循环依赖是否一定需要三级缓存来解决? 不一定,但三级缓存会更合适,风险更小。<br><font color="#f44336">【如果没有AOP的话确实可以两级缓存就可以解决循环依赖的问题,但是加上AOP的话两级缓存是无法解决的,<br>因为不可能每次执行singleFactory.getObject()方法都给我产生一个新的代理对象,所以还要借助另外一个缓存<br> 来保存产生的代理对象】</font><br>
SpringBoot
SpringBoot源码启动类流程
<br> 1 初始化springApplication, 运行SpringApplication的run方法<br> 2 读取spring.factories 的多个初始化器和监听器<br> 3 配置项目中环境变量 JVM配置信息 配置文件信息<br> 4 预初始化环境,创建环境对象<br><br> 5 创建Spring容器对象(ApplicatonContext)<br> 6 调用spring的refresh加载IOC容器、自动配置类,并创建bean等信息<br> 7 调用很多监听器并传传递上下文对像<br> 8 运行相关Runner<br>
SpringBoot 自动装配原理
@EnableAutoConfiguration该注解又通过@import注解导入了<br>AutoComfigurationlmportSelector,在该类中加载META-JN下/<br>spring.factories配置信息,然后筛选出以EnableAutoComfiguration<br>为Key的数据,之后加载到 IOC容器中,实现自动配置功能。
Mysql
Mysql主从复制
<font color="#f44336">流程</font><br> master 将数据变更写⼊slave的⼆进制⽇志 binary log<br> slave 将 master 的 传来的⼆进制⽇志 binary log events 拷⻉到它的中继⽇志(relay log)<br> slave 重放中继⽇志 relay log 事件,将数据变更反映它⾃⼰的数据<br> SQL线程读取中继日志(relay log),解析然后写入到从库<br><br><font color="#f44336">注意:</font><br> 1、存在几个线程: 主库一个线程,从库两个线程<br> 2、主库生成一个 log dump线程,和从库IO线程交互<br> 3、IO线程请求主库binlog,写入到中继日志relay log<br> 4、SQL线程读取中继日志,解析然后写入到从库<br>
Mysql主从复制配置<br>binlog⽇志分类<br>
注意<br> 运⾏以下命令检查是否开启了binlog<br> SHOW VARIABLES LIKE 'log_bin'; 或者SHOW VARIABLES LIKE '%log_bin%';<br> 如果需要确认binlog⽇志的⽂件名和位置,则可以运⾏以下命令<br> SHOW MASTER STATUS;<br><br>Mysql数据库配置<br> 编辑配置⽂件,默认情况下MySQL的binlog⽇志是⾃动开启<br> vim /home/data/mysql/my.cnf<br><br># 开启 binlog, 可以不加,默认开启<br> log-bin=mysql-bin<br># 选择 ROW 模式<br> binlog_format=row<br>#server_id不要和canal的slaveId᯿复<br> server-id=1<br><br>重启服务 docker restart xdclass_mysql<br>
<font color="#e74f4c">STATEMENT 格式</font><br> Statement-Based Replication,SBR,每⼀条会修改数据的 SQL 都会记录在 binlog 中,<br> 每⼀条会修改数据 SQL 都会记录在 binlog 中,性能⾼,发⽣的变更操作只记录所执⾏的 SQL 语句,⽽不记录具体变更的值<br><br>优点:不需要记录每⼀⾏数据的变化,极⼤的减少了 binlog 的⽇志量,避免了⼤量的 IO 操作,提升了系统的性能<br>缺点:由于 Statement 模式只记录 SQL,⽽如果⼀些 SQL 中 包含了函数,那么可能会出现执⾏结果不⼀致的情况<br> uuid() 函数,每次执⾏都会⽣成随机字符串,在 master 中记录了 uuid,当同步到 slave 后再次执⾏,结果不⼀样<br> now()之类的函数以及获取系统参数的操作, 都会出现主从数据不同步的问题<br><br><font color="#e74f4c">ROW 格式(默认)</font><br> Row-Based Replication,RBR,Row 格式不记录 SQL 语句上下⽂相关信息,仅保存哪条记录被修改,仅记录某⼀条记录被修改成什么样⼦<br> <br>优点:清楚地记录下每⼀⾏数据修改的细节,不会出现 Statement 中存在的那种数据⽆法被正常复制的情况,保证最⾼精度和粒度<br>缺点:Row 格式存在问题,就是⽇志量太⼤,批ᰁ update、整表 delete、alter 表等操作,要记录每⼀⾏数据的变化,此时会产⽣⼤量的⽇志, ⼤量的⽇志也会带来 IO 性能问题<br><br><font color="#e74f4c">MIXED 格式</font><br> 在 STATEMENT 和 ROW 之间⾃动进⾏切换的模式。在没有⼤ᰁ变更时使⽤ STATEMENT 格式<br> ⽽在发⽣⼤ᰁ变更时使⽤ ROW 格式,以确保⽇志具有⾼精度和粒度,同时保证存储空间的有效使⽤。<br><br>总结<br> MySQL 的⼆进制⽇志⽀持三种不同的格式<br> 其中 STATEMENT 格式占⽤空间较少,但可能因为版本不兼容导致数据库不同步;<br> ROW 格式占⽤的空间最⼤,但能精确记录所有变更操作<br> ⽽ MIXED 格式在两者之间⾃动切换,兼顾了精度和空间效率<br> 在实际开发中,根据数据ᰁ⼤⼩、变更频率等要素,选择合适的 binlog 格式是⾮常᯿要的<br>
慢查询日志
默认情况下MySQL数据库是<font color="#f44336">不开启</font>慢查询日志的 long_query_time的默认值为10(即10秒,通常设置为1秒)<br>慢查询日志记录SQL语句相关信息包括但不限于以下内容:<br> 执行时间执行次数、告警时间、执行的SOL语句、使用的索引、扫描的行数、等待锁的时间<br>
查询状态show variables like 'slow%'<br>slow_query_log属性是OFF,处于关闭状态,也可以通过0和1进行配置,1表示开启,0表示关闭<br> 开启慢查询日志set globalslow query_log =on<br><br>slow_querylog_file表示慢查询日志文件的存放路径<br> set global slow query log file='自定义路径'<br><br>如何定义慢sql语句 show variables like '%long%'<br> long_query_time属性,值为10.000000,表示的只记录查询时间在10s以上的语句<br> 数据不多的情况,慢查询的临界值设置为0.02: set long_query_time=0.02<br><br>慢查询配置临时⽣效,重启数据库则会丢失<br><br>配置持久化 vim /home/data/mysql/my.cnf<br>[mysqld]<br>slow_query_log = 1<br>long_query_time = 0.1<br>slow_query_log_file =/usr/local/mysql/mysql_slow.log<br><br>᯿启mysql⽣效, docker restart xdclass_mysql<br>利⽤上⾯的命令进⾏验证 参数是否⽣效<br>模拟慢查询,睡眠10秒, select sleep(10);<br>进⼊容器查看⽇志 cat /usr/local/mysql/mysql_slow.log<br>
索引失效
最左匹配原则,最左边的列一定需要存在,否则索引会不生效<br>联合索引范围查询右边的列,不走索引<br>联合索引跳过中间字段,则只有左侧存在的索引的才生效<br>or 连接索引,or 前面字段有索引,但是or后面字段没索引,那涉及的索引都失效<br>like查询索引,左边精确查询索引生效;左边模糊查询,索引失效<br><br>字段重复性高导致索引失效,mysql优化器会检查如果全表扫描比找索引快,则会不走索引<br>字符串没加引号导致索引失效<br>索引字段上进行函数操作,运算符操作导致索引失效<br>IS NULL不走索引,<font color="#f44336">IS NOT NULL走索引</font>,设计字段的时候,<font color="#f44336">如果没有要求必须为NULL</font>,那最好给个默认值空字符串<br>
索引设计常见案例原则
高频次查询目数据量大的表建立索引<br>经常需要排序、分组和联合操作的字段建立索引<br>短索引可以提升访问的10效率,对于BLOB、TEXT或很长的varchar列使用前缀索引<br>删除无用索引,同列上创建多个索引,越多索引维护成本越高,优化器在优化查询时也需要逐个考虑,会影响性能。<br>根据业务需求,设计好联合索引,业务使用的时候尽量用到联合索引,避免回表查询<br><br>尽量选择区分度高的列作为索引,区分度越高性能越好,比如唯一索引<br>索引列不参与计算,带函数的查询不建议做为索引列<br>尽量扩展利用现有索引,联合索引的查询效率比多个独立索引高<br>尽量避免NULL,应该指定列为NOT NULL,含有空值的列很难进行查询优化,可以用0或一个空串代替NULL<br>
<font color="#f44336">唯⼀索引与普通索引</font><br> 唯⼀索引和普通索引在性能上没有本质的区别,但在数据的唯⼀性⽅⾯<br> 唯⼀索引在数据插⼊和更新时需要更多计算,因此略微慢⼀些。<br><br><font color="#f44336">聚簇索引与⾮聚簇索引</font><br> 聚簇索引在性能上优于⾮聚簇索引,因为聚簇索引是将数据存储在⼀起的<br> 这样检索数据时可以最⼤程度地减少磁盘 IO 操作。但如果经常更新表中的数据,则聚簇索引的维护成本相对较⾼。<br><font color="#f44336"><br>覆盖索引与⾮覆盖索引</font><br> 覆盖索引可以直接从索引中获取数据,⽆需回表查询,因此执⾏速度更快<br> 但是如果查询需要取出的数据列不在索引中,则⽆法使⽤覆盖索引,需要进⾏回表查询,效率较低<br>
脏读<br>不可重复读<br>幻读<br>
<font color="#f44336">「脏读」Read Uncommitted(未提交读,读取未提交内容)</font><br>事务中的修改即使没有提交,其他事务也能看见,事务可以读到未提交的数据称为脏读<br><br><font color="#f44336">「不可重复读」 Read Committed(提交读,读取提交内容)</font><br>同个事务前后多次读取,不能读到相同的数据内容,中间另一个事务也操作了该同一数据<br><br><font color="#f44336">Repeatable Read(可重复读,mysql默认的事务隔离级别)</font><br> 解决脏读、不可重复读的问题,存在幻读的问题,使用 MMVC机制 实现可重复读<br><br><font color="#f44336">【幻读」: </font> 当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,<br> 当之前的事务再次读取该范围的记录时,发现两次不一样,产生幻读<br><br><font color="#f44336">幻读和不可重复读的区别是:前者是一个范围,后者是本身,从总的结果来看, 两者都表现为两次<br> 读取的结果不一致</font><br><br>设置事务隔离级别代码 set session transaction isolation level serializable;<br>
事务的隔离级别?<br>
<font color="#f44336">1.【读未提交」:</font>称为脏读 即能够「读取到没有被提交」的数据。也存在不可重复读、幻读问题<br><font color="#f44336">2.「读已提交」:(</font>不可重复读)即能够「读取到那些已经提交」的数据。也存幻读问题<br><font color="#f44336">3.「可重复读」:</font>可重复读指的是在一个事务内,最开始读到的数据和事务结束前的「任意时刻读到的同一批数据都是一致的」<br> 解决脏读、不可重复读的问题,存在幻读的问题,使用 MMVC机制 实现可重复读 <br> <font color="#f44336">mysql默认的事务隔离级别 </font><br><font color="#f44336">4.「串行化」: </font>最高事务隔离级别,不管多少事务,都是「依次按序一个一个执行」<br> 解决脏读、不可重复读、幻读,可保证事务安全,但强制所有事务串行执行,所以并发效率低
聚簇索引<br>⾮聚簇索引<br>
表数据⽂件按B+Tree组织的,叶节点data域保存完整<font color="#f44336">⾏数据, </font>树上的key就是主键, 以主键构建的B+树索引<br>聚簇索引⼀般为主键索引,⽽主键⼀个表中只能有⼀个,所以聚集索引⼀个表只能有⼀个<br>聚簇索引叶⼦节点存储的是⾏数据,⽽⾮聚簇索引叶⼦节点存储的是聚簇索引(通常是主键 ID)<br><br>索引结构的叶⼦节点放的是指向数据的主键(或者是地址)构建的B+树索引<br>这种索引叫做⾮聚集索引、⼆级索引、辅助索引,⾮聚集索引⼀个表可以存在多个<br>叶⼦节点中保存的<font color="#f44336">不是实际数据,⽽是主键</font>,获得主键值后去聚簇索引中获得数据⾏<br><br><font color="#f44336">⼤分类<br></font> 聚簇索引:数据和索引⼀起的叫做聚簇索引<br> ⾮聚簇索引(⼆级索引/辅助索引):数据和索引分开存储的叫做⾮聚簇索引<br> myisam中只有⾮聚簇索引,innodb既⽀持聚簇索引也⽀持⾮聚簇索引<br>
<font color="#f44336">注意:</font><br>⾮聚簇索引的叶⼦节点上存储的并不是真正的⾏数据,⽽是主键 ID或记录的地址<br>当使⽤⾮聚簇索引进⾏查询时,会得到⼀个主键 ID,再使⽤主键 ID 去聚簇索引上找真正的<font color="#f44336">⾏数据</font>,这个过程称<font color="#f44336">为回表查询</font><br>所以聚簇索引查询效率更⾼,⽽⾮聚簇索引需要进⾏回表查询,性能不如聚簇索引<br>
联合索引<br>回表查询<br>
常规表中只会选择⼀个列作为索引字段,但是在某些特殊情况下,需要将多个列共同组成 ⼀个索<br>引字段,称之为联合索引<br><br>在使⽤索引进⾏查询时,MySQL 执⾏的额外⼀次查询操作,⽤于从数据表中检索相关数据,并将其返回给⽤户<br>语句 select * from table where name = "冰冰"<br>当执⾏这个sql时发⽣回表,从 【⾮聚簇索引】的叶⼦结点中获取id值,根据id再去 【聚簇索引】中获取全部数据<br>在实际中,回表查询次数太多会影响查询性能,多使⽤包含查询到的所有列的索引称为“覆盖索引”)来避免回表查询<br>
覆盖索引⼀定是联合索引吗?<br>联合索引⼀定是覆盖索引吗?<br>
<font color="#f44336">不⼀定是</font>,它是指仅通过索引本身就能够获取到查询结果所需的所有列,⽽不必回到<br> 主表中去查询的情况<br><br><font color="#f44336">覆盖索引</font><br> 当⼀个索引覆盖了⼀个查询时,这个索引包含了查询时所需的所有列<br> 如果查询语句只需要从表中检索少量列数据,⽽索引正好包含了这些列,那么它就可以被认为是⼀个覆盖索引<br> 这个索引可能由单个字段或者多个联合字段构成<br>
<font color="#f44336">联合索引</font><br> 联合索引可以包含多个字段,但并⾮所有联合索引都是覆盖索引<br> 当查询语句需要使⽤联合索引来加速查询时,如果索引中包含了查询所需的所有列信息,那么这个联合索引就是覆盖索引<br> 否则,它不是覆盖索引,可能会发⽣回表查询操作<br><br><font color="#f44336">注意</font><br> 由于使⽤了联合索引来覆盖查询,语句中只查询了索引中涉及到的列,⽽未查询其他列,也就避免了回表查询。<br> 如果这个联合索引只包含 name 和 age 两列,则该查询⽆法使⽤⼀个覆盖索引,<br> MySQL 仍将需要进⾏回表查询<br>
int(10)和int(1) 区别
<font color="#ed9745">如果不使⽤ zerofill 是没有任何区别的<br></font>在mysql中 int占4个字节,那么对于⽆符号的int,最⼤值是2^32-1 = 4294967295,将近40亿<br>int后⾯的数字不能表示字段的⻓度,是⼀样⼤⼩,不会限制值的合法范围<br>int(3)、int(4)、int(8) 在磁盘上都是占⽤4 bytes的存储空间<br><br>作⽤:定义字段的时候,定义了zerofill,那int(1),你存⼊10显示就是10<br> ⽽int(4),存⼊10显示就是0010,也就是前⾯补0<br> 定义⼀定⻓度的编号,不⾜补0的情况,⽐如卡号、学⽣编号等<br>
MQ
消息堆积解决方案
<font color="#f44336">增加消费者数量(不通用)</font><br> 通过增加消费者的数量,可以提⾼消息的处理速度,从⽽减少消息堆积<br> 可以动态地增加或减少消费者的数量来适应实际的负载情况。<br><font color="#f44336">提⾼消费者的处理能⼒</font><br> 优化消费者的处理逻辑,例如通过并发处理、异步处理、批量处理等⽅式提⾼消费者的吞吐能力<br><br><font color="#f44336">增加队列的处理能⼒</font><br> 可以通过增加MQ服务器的资源(例如CPU、内存、磁盘)来提⾼队列的处理能⼒<br> 也可以考虑使⽤分区队列来将消息分散到多个队列中,从⽽提⾼整体的处理能⼒。<br><div><font color="#f44336">设置合适的队列容量限制</font></div><div> 通过设置队列的最⼤⻓度和最⼤消息数量限制,可以控制队列的容量,避免消息堆积过多。</div><div> 将超过限制的消息进⾏丢弃或者进⾏其他的处理,以避免队列溢出。</div><br><font color="#f44336">配置合适的持久化策略<br></font> 确保消息和队列都进⾏持久化设置,即使MQ服务器重启或崩溃,也能够恢复消息和队列状态,避免数据丢失。<br><font color="#f44336">监控和预警<br></font> 设置监控系统来实时监测队列的消息数量、消费者的处理速度,以及服务器的负载情况<br> 在堆积发⽣时及时发出预警,以便及时采取措施<br><br><font color="#f44336">上述是⽐较通⽤的解决⽅案,但是不同MQ的⽣产和消费模型不⼀样,导致并⾮是通⽤的</font><br>
如何保证消息不积压
1 修复Consumer不消费问题,使其恢复正常消费,根据业务需要看是否要暂停<br>2 临时topic队列扩容,并提高消费者能力。如果增加Consumer数量,但是堆积里面的<br>topic message queue数量是固定的,过多的consumer不能分配到message queue<br><br>3 编写临时处理分发程序,从旧topic快速读取到临时新topic中,新topic的queue数量<br> 扩容多倍,然后再启动更多consumer进行在临时新的topic里消费<br>4 直到堆积的消息处理完成,再还原到正常的机器数量
如何保证消息不丢失<br>如何保证消息的顺序
<font color="#f44336">(1)如何保证消息不丢失 生产者:</font><br>方案 1: 开启 RabbitMQ 事务(同步,性能差)<br>方案 2: 开启 confirm 模式(异步,性能较好),实现ConfirmCallback回调接口,<br> 为RabbitTemplate配置回调函数<br><br>MQ: (1)exchange 持久化 (2)queue 持久化(3)消息持久化 <br>消费者: 关闭自动 ACK<br><br><font color="#f44336">(2)如何保证消息的顺序<br></font>这个知识点是有一个背景的,在rocketmq里面,有一个完善的机制,<br>在产品层面上对消息进行顺序的保证,而kafka与rabbitmq是没有这样的设计的。<br><br><font color="#f44336">消息顺序分为全局有序和局部有序,</font>MQ只需要保证局部有序,不需要保证全局有序。<br>保证一个窗口内的消息是有序的,多个窗口之间的消息有序没有业务意义。<br>例如一个订单,有许多处理步骤,这些步骤是不能乱的 ,消息必须是从上往下进<br>行消费。订单与订单之间消息可以不是有序的,没有必要等到1号订单发完再发2号订单。<br><font color="#f44336"><br>(3)Kafka 怎么保证消息是有序的?</font><br>消息在被追加到 Partition(分区)的时候都会分配一个特定的偏移量 (offset) 。Kafka 通过<br>偏移量 (offset)来保证消息在分区内的顺序性。发送消息的时候指定 key/Partition。<br>
如何保证消息可靠性-<br>幂等性(重复消费问题)
<font color="#f44336">消息重复</font><br>消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功Broker<br>的消息重新由unack变为ready,并发送给其他消费者<br><br>消息消费失败,由于重试机制,自动又将消息发送出去成功消费,<br>ack时宕机,消息由unack变为ready,Broker又重新发送。<br> 消费者的业务消费接口应该设计为幂等性的。比如扣库存有工作单的状态标志<br> 使用防重表 (redis/mysql) ,发送消息每一个都有业务的唯一标识,处理过就不用处理<br> rabbitMQ的每一个消息都有redelivered字段,可以获取是否是被重新投递过来的,<br> 而不是第一次投递过来的
<font color="#f44336">1.利用数据库的唯一约束</font><br>在进行消费时,需要取一个唯一标识,比如id作为唯一约束字段,先添加数据,<br>如果添加失败,后续做错误提示,或者不错后续操作<br><font color="#f44336">2.redis设置全局唯一ID</font><br>每次生产者发送消息前,设置一个全局唯一id放在消息体内,并存放在redis;<br>消费者在消费时先到redis上找是否存在这个id,如果存在,调用消费接口并<br>删除全局id,如果不存在,不进行操作<br><br><font color="#f44336">3.多版本机制(乐观锁)</font><br>生产者给消息添加一个版本号,每次更新数据前,比较当前版本和消息中的版本是否一致,<br>如果一直就更新数据并且版本号+1,如果不一致就不做处理。<br><br><font color="#f44336">4方式四:</font>Redis的setNX() , 做消息id去重 java版本目前不支持设置过期时间<br><font color="#f44336">5方式五:</font>Redis的 Incr 原子操作:key自增,返回值大于0则说明消费过,<br> (key可以是消息的md5取值, 或者如果消息id设计合理直接用id做key)<br><br><font color="#f44336">6 方式六:数据库主键去重表</font> <br> 设计一个去重表,某个字段使用Message的key做唯一索引,<br> 因为存在唯一索引,所以重复消费会失败<br>
Rabbitmq<br>如何实现延时队列
<font color="#f44336">1)使用TTL+死信队列组合实现延迟队列的效果。</font><br><font color="#f44336">(1)TTL 全称 Time To Live(存活时间/过期时间)。</font>当消息到达存活时间后,<br>还没有被消费,会被自动清除。RabbitMQ可以对消息设置过期时间,也可以对整个队列<br>(Queue)设置过期时间。可以类比redis的TTL。<br><font color="#f44336">(2)死信队列 DLX </font>--------消息成为死信的三种情况:<br> a. 队列消息长度到达限制<br> b. 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列<br> requeue=false;<br> c. 原队列存在消息过期设置,消息到达超时时间未被消费;<br><br>使用TTL+死信队列组合实现延迟队列的效果。<br><font color="#f44336">消息生产者--------->交换机</font>-------绑定路由健A-----<font color="#f44336">>队列1--</font>-----》消息过期,成为死信<br>------><font color="#f44336">死信交换机</font>------绑定路由健B-----<font color="#f44336">->队列2--</font>--------<font color="#f44336">>消息消费者1/2</font><br><br>死信交换机和普通队列没区别。死信队列和普通队列也没区别
子主题
RabbitMQ消息丢失的场景
<font color="#f44336">场景⼀:未开启消息持久化机制<br></font> ⽣产者已经将消息发送给了队列,但消费者还没以及对消息进⾏消费<br> 如果队列所属的主机宕机,则存储在队列的消息也会丢失<br><br><font color="#f44336">解决⽅案<br></font> 对消息进⾏持久化操作,如果消息⼀旦被发送到mq中的某⼀个队列,那么此时Rabbitmq会⽴⻢将消息进⾏持久化<br> ⽇常开发中,SpringBoot和Rabbitmq整合默认消息的存储就是持久化⽅式<br> 可以将所有的消息都设置为持久化,但持久化会写到磁盘,写磁盘速度肯定不上写内存的速度,影响Rabbitmq的性能。<br> 对于可靠性不是那么⾼的消息可以不采⽤持久化处理,以提⾼整体系统的吞吐量<br> 在RabbitMQ中,deliveryMode是消息的持久化属性,⽤于指定消息在队列中的持久化⽅式<br> 非持久化消息,deliveryMode = 1<br> 消息在发送到队列后,会被保存在内存中如果RabbitMQ服务重启或崩溃,这些消息将会丢失。<br> 适⽤场景:适⽤于⼀些临时性消息,对于丢失消息没有严格的要求,例如临时计算结果、通知等<br> 持久化消息,deliveryMode = 2<br> 消息在发送到队列后,会被保存在磁盘上,即使RabbitMQ服务重启或崩溃,这些消息仍然可以被恢复和投递<br> 适⽤场景:适⽤于对消息的可靠性和持久化要求较⾼的场景,例如重要的业务消息、订单信息、⽇志记录等<br>
消息如果开启⾮持久化,那是否⼀定会丢失呢?<br> 不⼀定,因为⽆论消息是持久化还是⾮持久化,都可以被写⼊磁盘<br> 持久化消息会在到达队列时被写⼊磁盘,同时可能会在内存中保留备份,以提⾼性能<br> 当内存紧张时,可以从内存中清除这些备份<br> 非持久化消息通常只保存在内存中,但是当内存紧张时,会被写⼊磁盘,以节省内存空间,前⾯学过的swap分区<br>
RabbitMQ消息丢失的场景
<font color="#f44336">场景⼆:⽣产者到交换机失败<br></font> ⽣产者投递消息到交换机,未经确认导致消息丢失<br><br><font color="#f44336">解决⽅案<br></font> 发送确认机制可以通过confirmCallback实现,开启该机制后,发送端可以感知消息是否成功到达交换机<br> ⽣产者投递消息后,如果Broker收到消息后,会给⽣产者⼀个ACK<br> ⽣产者通过ACK,可以确认这条消息是否正常发送到Broker,这种⽅式是消息可靠性投递的核⼼<br> 开启消息确认机制以后,保证了消息的准确送达,但由于频繁的确认交互, rabbitmq 整体效率变低<br> 吞吐量下降严重,不是⾮常重要的消息不建议⽤消息确认机制<br>
<font color="#f44336">场景三:交换机转发消息到队列失败<br></font> 交换机需要转发到队列,如果转发失败,也会导致消息丢失<br><br><font color="#f44336">解决⽅案<br></font> 通过returnCallback,消息从交换器发送到对应队列失败时触发<br> 两种模式<br> 交换机到队列不成功,则丢弃消息(默认)<br> 交换机到队列不成功,返回给消息⽣产者,触发returnCallback<br> # 开启消息从交换机到队列的确认机制,感知消息是否到达队列spring.rabbitmq.publisher-returns=true<br> # true表示交换机转发消息到队列失败,将消息返给发送者spring.rabbitmq.template.mandatory=true
RabbitMQ消息丢失的场景
<font color="#f44336">消息丢失场景四<br></font> 消费者从broker中监听消息,需要确保消息被合理处理<br> SpringAMQP则允许配置三种确认模式<br> manual:⼿动ack,根据业务情况,判断什么时候该ack,调⽤api发送ack<br> auto:⾃动ack,由spring⾃身监测消费者代码是否出现异常,没有异常则返回ack;抛出异常则返回nack,消息回滚到mq<br> none:关闭ack,消息投递不可靠,可能丢失,认为消费者获取消息后会成功处理,所以消息投递后⽴即被删除
RabbitMQ消息堆积解决⽅案<br>
<font color="#f44336">特点:</font>会尽可能的将消息存⼊磁盘中,在消费者消费到相应的消息时才会被加载到内存中<br> 设计⽬标是能够⽀持更⻓的队列,即⽀持更多的消息存储<br><br><font color="#f44336">普通队列</font><br> ⽣产者将消息发送到RabbitMQ的时候,队列中的消息会尽可能的存储在内存之中<br> 消息开启持久化,在被写⼊磁盘的同时也会在内存中驻留⼀份备份<br><br><font color="#f44336">惰性队列<br></font> Broker接收消息后将消息直接存⼊磁盘,不管是持久化的或者是⾮持久化的<br> 消费者需要消费时才会从磁盘中读取,并加载到内存中<br><br> <font color="#e74f4c">优点</font>:惰性队列和普通队列相⽐,只有很⼩的内存开销<br> <font color="#f44336">缺点:</font>基于磁盘存储,写⼊磁盘有IO损耗,性能和容量受限于磁盘<br> 扩⼤队列容积,提⾼堆积上限 ,把消息存储从内存中剥离出来,使⽤了磁盘<br><br><font color="#e74f4c">官⽅案例数据</font><br>发送100万条消息,每条消息的⼤⼩为1KB,那么普通队列会消耗1GB多内存,⽽惰性队列只<br>能消耗1MB多的内存<br><br><font color="#f44336">⽅案⼀ 惰性队列 :</font>扩⼤队列容积,提⾼堆积上限 ,把消息存储从内存中剥离出来,使⽤了磁盘<br><font color="#f44336">⽅案⼆ </font><font color="#e74f4c">增加更多消费者</font><font color="#f44336">: </font>提⾼消费速度即可,队列上绑定更多消费者即可,Work queues模式<br><br><font color="#e74f4c">WorkQueue⼯作队列介绍</font><br> 消息⽣产能⼒⼤于消费能⼒,增加多⼏个消费节点<br> 让多个消费者绑定到⼀个队列,共同消费队列中的消息,不同的消费节点,处于竞争关系<br> 当消息处理不过来或者产⽣积压的时候,可以直接采⽤这个⽅案进⾏<br>
<font color="#e74f4c">结论</font><br> 两个队列<font color="#e74f4c">(普通队列和惰性队列)</font>都持久化了1,000,000条消息,并使⽤了1.2 GB的磁盘空间<br> 测试表明,在相同的消息数ᰁ和⼤⼩情况下,惰性队列相对于默认队列节省了⼤ᰁ的内存空间<br> 惰性队列只在需要时分配内存资源,⽽默认队列会保留所有的消息在内存中。<br><br>消息堆积解决的<font color="#e74f4c">关键点</font><br> 优化应⽤程序的性能,缩短响应时间<br> 增加消费者节点实例 ,成本增加,底层数据库操作和其他关联链路也可能是瓶颈 (特别注意)<br> 调整并发消费的线程数,⽐如程序内部使⽤线程池和队列临时缓冲,但是要避免宕机丢失内存消息<br>
Kafka海量消息堆积
<font color="#e74f4c">Kafka⾥⾯发⽣消息堆积的常⻅分析思路和解决⽅案</font><br> 识别消息堆积的原因和选择合适的解决⽅案<br> 通过监控Kafka Broker和消费者的指标,确定消息堆积发⽣在哪个部分,是在⽣产者端还是消费者端。<br> 根据堆积的原因选择相应的解决⽅案,如增加消费者、增加分区、调整消费者端配置<br><br><font color="#f44336">场景解决⽅案(不管哪种⽅案,都需要避免消费者的重复消费)</font><br>
<font color="#f44336">增加消费者数量</font><br> 通过增加消费者的数量来提⾼消息的消费速度<br> 可以启动更多的消费者实例,并将它们添加到消费者组中,前提是 现有Topic的分区数 > 消费者数<br><font color="#f44336">增加分区数</font><br> 通过增加Kafka主题的分区数来增加消息的并⾏处理能⼒。<br> 增加分区数将允许更多的消费者并⾏消费消息,并减少每个分区上的消息堆积量。<br> 注意,增加分区数可能会导致消息重新分配和消费者组的重新平衡<br><font color="#f44336"><br>临时队列分发</font><br> 通过将现有积压的Topic分发到另外⼀个⽀持更多分区的临时Topic<br> 启动更多消费者消费临时的Topic完成消息积压的处理<br><div><font color="#ed9745">问题</font></div><div> 由于本身消息队列设计的特性,没法直接增加消费者进⾏快速提升消费者能⼒</div><div> ⼀个Topic⾥⾯有多个partition分区,⼀个分区只能给⼀个消费者组内的⼀个消费者消费</div><div> 假如有个3个分区,同个消费者组有3个消费者,那刚好每个消费者可以消费⼀个分区<br><div><font color="#f44336">解决⽅案(临时分发扩容,适合⽐较多MQ场景)</font><br> 如果堆积的消息需要⽐较久才可以消费完成,则可以建⽴⼀个临时Topic,建⽴多个分区<br></div></div><br><font color="#f44336">提⾼消费者端的处理能⼒:</font><br> 检查消费者端的处理逻辑,确保消费者能够⾼效地处理消息<br> 优化消费者的代码,减少处理逻辑的复杂性,提⾼消息的处理速度。<br> 例如<br> 调整消费者的最⼤拉取数据量(fetch.max.bytes)和拉取间隔(fetch.min.bytes)<br> 批量处理的消息数量(max.poll.records)等,以提⾼消费者的吞吐量<br><br><font color="#f44336">增加Kafka Broker的数量</font><br> 如果消息堆积是由于Kafka Broker的性能瓶颈导致的,可以考虑增加Kafka Broker的数量来<br> 提⾼整个集群的处理能⼒<br><font color="#f44336">扩展硬件资源</font><br> 如果堆积的原因是由于硬件资源的限制,例如磁盘空间不⾜或⽹络带宽受限<br> 可以通过增加硬盘容量、更换⾼性能磁盘、增加⽹络带宽等⽅式扩展硬件资源<br>
消息如果开启⾮持久化,<br>那是否⼀定会丢失呢?<br>
不⼀定,因为⽆论消息是持久化还是⾮持久化,都可以被写⼊磁盘<br>持久化消息会在到达队列时被写⼊磁盘,同时可能会在内存中保留备份,以提⾼性能<br>当内存紧张时,可以从内存中清除这些备份<br><br>⾮持久化消息通常只保存在内存中,但是当内存紧张时,会被写⼊磁盘,以节省内存空间<br>free -m
⼿⼯确认消息<br>存在的问题点分析<br>
消费者收到消息后,在未确认前,消息的状态是UnAcked状态<br>当确认消息后,服务器会删除消息,如果是拒收消息,服务端继续将这条消息投递给消费者<br>如果在UnAcked状态客户端宕机,消费失败的消息会回退到队列,消息状态变为Ready,<br>消费端再次启动后,消息继续投递<br><br><font color="#e74f4c">异常情况</font><br> 如果消费者消息完成,但是由于⽹络不好,⽐如突然⽹络抖动,断电等,ACK信息没有响应给MQ服务端<br> 消费者与MQ服务器断开,消息会被回退到队列,那消息发送给其他消费者,消费者要做好幂等性处理,防⽌消息᯿复消费<br>
幂等性常⻅解决⽅案
在消息队列中,保证不᯿复消费消息的⼀个关键概念是【幂等性】操作<br>幂等性操作是指⽆论进⾏多少次操作,其结果都是⼀致的,不会产⽣额外的副作⽤<br>在消息消费的场景中,幂等性操作可以确保消息被多次消费时,只会产⽣⼀次实际的影响<br><br><font color="#e74f4c">唯⼀标识符</font><br> 为每条消息分配⼀个唯⼀的标识符(例如消息ID)消费者在处理消息时<br> 可以根据标识符判断是否已经处理过该消息,避免重复消费,⽐如数据库唯⼀索引<br><br><font color="#e74f4c"> 去重机制</font><br> 可以使⽤缓存、分布式锁、布隆过滤器等机制来记录已经处理过的消息,以确保消息不会被重复消费<br>
搭建集群以后是否可以保证消息不丢失?
不可以,消息丢失可能有多个原因,⽐如消息的可靠性传输,操作系统刷盘机制,<br>普通集群还是镜像集群<br>热⻔中间件设计思想:可靠性越⾼,则性能会越低<br><br><font color="#e74f4c">注意:</font>集群需要保证各个节点有相同的token令牌<br>拓展<br><font color="#e74f4c">内存节点(ram):</font>将元数据都放在内存⾥,内存节点的话,只要服务᯿启,该节点的所有数据将会丢失<br><font color="#e74f4c">硬盘节点(disc):</font>将元数据都放在硬盘⾥,所以服务᯿启的话,数据也还是会存在的<br>在RabbitMQ集群⾥,⾄少有⼀个磁盘节点,它⽤来持久保存元数据;考虑到⾼可⽤性,推荐在集群⾥<br>保持2个磁盘节点<br><br>#加⼊集群时设置节点为内存节点<br>rabbitmqctl join_cluster --ram rabbit@rabbit-node1<br>#通过命令修改节点的类型<br>rabbitmqctl changeclusternode_type disc | ram<br>
Canal
阿⾥巴巴基于 MySQL 的增ᰁ⽇志解析和订阅发布系统,主要⽤于解决数据订阅与消费问题<br>Canal主要⽀持了MySQL的binlog解析,解析完成利⽤canal client ⽤来处理获得的相关数据<br><br>基于⽇志增ᰁ订阅和消费的业务包括<br> 数据库镜像<br> 数据库实时备份<br> 索引构建和实时维护(拆分异构索引、倒排索引等)<br> 业务 cache 刷新<br> 带业务逻辑的增ᰁ数据处理<br><br>Canal原理<br> Canal 模拟 MySQL slave 的交互协议,伪装⾃⼰为 MySQL slave ,向 MySQL master 发送dump 协议<br> MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )<br> Canal 解析 binary log 对象(原始为 byte 流) ,并解析成独⽴的数据操作事件,如插⼊、更新、删除等。<br><br>总结<br> Canal 的主要功能包括 binlog 解析、数据筛选、数据转换和数据发布等<br> 它是⼀种轻ᰁ级的、⾼性能的数据订阅和发布系统,⾮常适合进⾏ MySQL 数据同步、数据监控和数据分析等业务场景。<br>
监控系统
模式
<font color="#e74f4c">Pull模式</font><br> 优点:可以根据需要定时获取数据,避免数据的多余传输,节约⽹络带宽和存储空间。<br> 可以根据需要通过请求的⽅式,选定性地获取部分数据。<br> 适⽤于对被监控对象的稳定性要求不⾼的场景<br> Pull模式核⼼消耗在监控系统侧,应⽤侧的代价较低<br><br>缺点::由于需要定时发送请求获取数据,对于需要实时响应的业务,Pull模式的数据传输速度难以达到。<br> Pull模式需要能够处理推送事件的应⽤程序,因此需要有较⾼的成本和复杂性。<br><br><font color="#e74f4c">Push模式<br></font>优点:被监控对象可以主动推送数据,监控客户端不需要发送请求,因此避免了⽹络负载。<br> 对于需要实时响应数据的业务,Push模式具有更⾼的传输效率和更佳的实时性。<br> Push模式适⽤于稳定的⽹络环境,可以实现实时数据的快速传输<br><br>缺点:Push模式核⼼消耗在推送和Push Agent端,监控系统侧的消耗相⽐Pull要⼩<br> 被监控对象需要主动发送数据,因此不适⽤于对被监控对象要求较⾼的场景,如需要严格控<br> 制被监控对象的数据流量<br><br>总结:公司内部的监控系统来说,应该同时具备Pull和Push的能⼒才是⽐较合适的<br>
主流监控告警框架对⽐
<font color="#e74f4c">Zabbix(⽀持 pull/push 两种模式)<br></font> 基于C语⾔开发, 是⼀种基于服务器-客户端体系结构开发的企业级监控软件。<br> 它允许监控各种⽹络服务,服务器资源和硬件<br><br>优点:⽤户友好,易于部署和设置<br> 具有插件式框架,可以灵活地满⾜不同的监控需求<br> 提供了丰富的图形化监控数据<br> 具有⾼扩展性,⽤户可以轻松添加⾃定义功能。<br><br>缺点:功能强⼤,使⽤⽐较笨重,Zabbix的安装和部署需要更多的时间和资源<br> 对于复杂的IT架构,Zabbix的配置难度较⼤,<br> 响应时间不够实时,对于实时性要求较⾼的应⽤场景可能不够理想<br>
<font color="#e74f4c">Open-Falcon(push模式 小米公司)<br></font> 是⼀种分布式、⾼性能的监控解决⽅案,它被⽤于⼤型的⾼可⽤性系统和⼴泛的云计算环境<br>优点:轻量级,可以部署在物理机和虚拟机上,部署和配置⾜够简单<br> ⽀持查询优化,⽀持多种数据源输⼊<br> 具有简单和易于使⽤的图形化界⾯<br><br>缺点:国内使⽤量较⼩,社区不够活跃<br> 对于某些复杂的IT架构,Open-Falcon在准备和安装⽅⾯可能存在困难<br> 针对不同需求经验丰富的开发⼈员需代码调整<br>
<font color="#e74f4c">Prometheus(Pull模式)</font><br> go语⾔开发, 是可扩展的开源监控系统,⽤于收集存储多维度的时间序列数据<br> ⽀持PromQL查询语⾔和提供的图形化展示⼯具<br> 可视化和告警这些功能交由Grafana和Alertmanager等第三⽅产品来实现,拓展性强<br> 功能上的简洁,作为⼀个轻量级的后起之秀,在性能和展示⽅⾯优势⽐较明显,对容器监控⽀持的⾮常好<br><br>优点:可扩展性强,⽀持⽔平扩展,满⾜⼤规模监控需求<br> ⽀持灵活的查询和查询语⾔,提供了丰富的查询函数和操作符<br> 可以从不同的数据源收集数据,⽀持多维度数据聚合<br> 可以轻松集成Alertmanager实现告警<br><br>缺点:存储机制不够灵活,⼤规模数据的存储和处理存在压⼒<br> 对运维⼯程师的技术⽔平要求较⾼<br>
0 条评论
下一页