1. Java基础
顶顶顶顶的点点滴滴
1.1 java集合关系结构图
1.2 如何保证ArrayList的线程安全
方法一:
使用 Collections 工具类中的 synchronizedList 方法
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
使用锁机制
在访问ArratList时使用显式的锁机制(如ReentrantLock)来保证线程安全。
List<String> list = new ArrayList<>();
ReentrantLock lock = new ReentantLock();
//在访问list之前获取锁
lock.lock();
try{
//对list进行访问
}finally{
//操作完成后释放锁
lock.unlock();
}
方法二:
使用线程安全的替代类
使用CopyOnWriteArrayList 类:CopyOnWriteArrayList 是java.util.concurrent包下的一个线程安全的ArrayList实现。它通过在修改操作时创建一个新的数组来实现线程安全,适用于读多写少的场景。
List<String> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
1.3 HashMap的底层结构
在jdk1.8及之后,HashMap底层引入了红黑树,形成数组+链表+红黑树的组合。
数组:用"桶数组",用于基本的数据存储。
链表:在解决哈希冲突时,如果某个桶位上的元素数量未达到阈值(jdk8默认8),则仍旧使用链表来存储。
红黑树:当某个桶位上的元素数量达到阈值,且HashMap的总容量大于64时,会将链表转换为红黑树,以减少搜索时间。红黑树是一种自平衡的二叉查找树,相比于链表,它在最坏的情况下提供了更好的查找性能O((log n)).
扩容和重哈希:重哈希自jdk8之后被优化,减少了重新计算哈希值的需求,提高了效率。
1.4 ConcurrentHashMap如何保证线程的安全
在jdk8之后,放弃了分段的锁的概念,转而使用更加细粒度的锁策略,结合CAS操作和synchronized关键字来保证线程的安全。
CAS操作:ConcurrentHashMap在某些关键操作上使用了CAS操作,比如更新计数值等。CAS操作时一种无锁的同步机制,它通过比较内存中的某个值和预期值,如果相同则更新它。由于CAS是原子操作,因此可以保证线程的安全。
synchronized关键字:对于需要更新二链表或红黑树结点的操作,ConcurrentHashMap使用了synchronized关键字来锁定结点或者树的根节点。因此,每次只有一个线程可以修改链表或红黑树的结构,从而保证了线程的安全。
细粒度的锁:通过在节点级别使用synchronized而不是在整个哈希表或者段上使用锁,ConcurrentHashMap实现了更为细粒度的锁定策略。这样,即使多个线程访问相同的桶位,只要他们操作的是不同的节点,这些操作也能够并发执行。
1. 5 线程池分为几种以及各自的使用场景
Executor框架提供了一种将任务的提交与每个任务如何运行分离开来的机制,包括线程的使用、调度等。这个框架位于java.util.concurrent包下,旨在简化并发编程,提高多线程应用程序的性能和可管理性。
核心接口
Executor框架的核心是Executor接口,它仅定义了一个方法execute(Runnable command),用于执行给定的任务。然而,这个接口是一个更广泛的并发框架的基础,其中包括:
ExecutorService:是Executor的子接口,提供了更为丰富的线程池管理功能,包括任务提交、关闭线程池、任务取消等。它可以执行Callable任务以及Runnable任务,并且可以返回表示任务状态和结果的Future对象。
ScheduleExecutorService:时ExecutorService的子接口,支持任务的延迟执行或定期执行。
线程池
Executor框架通过不同类型的线程池提供了线程的管理机制。线程池负责分配线程给任务执行,管理空闲线程,限制线程数量等,从而避免了为每个任务创建新线程的开销。常见的线程池实现包括:
FixedThreadPool:固定大小的线程池,可以重用池中的线程,适用于负载较重的服务器。
CachedThreadPool:一个根据需要创建新线程的线程池,但会在先前构造的线程可用时重用它们。适用于执行很多短期异步任务的程序。
SingleThreadExecutor:单线程的Executor,它创建单个工作线程来执行任务,适用于需要保证顺序执行各个任务的场景。
ScheduledThreadPool:一个能延迟执行任务或定期执行任务的线程池。
- 工具类
它是一个工厂类,提供了静态方法来创建不同类型的线程池。
import java.util.concurrent.ExcutorService;
import java.util.concurrent.Executors;
public class ExecutorExample{
public static void main(String[] args){
//创建一个固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(4);
//向线程池提交任务
for(int i =0; i < 10; ++i){
int taskId = i;
executor.execute(() -> {
System.out.println("Executing task " + taskId + "via" + Thread.currentThread().getName());
})
}
//关闭线程池
executor.shutdown();
}
}
1.6内存溢出和内存泄露的区别
内存溢出的常见原因
内存溢出 out of memory,指程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。
内存溢出:
堆内存溢出(outOfMemoryError:java heap space):内存泄漏会导致
方法区内存溢出(outOfMemoryError:permgem space):类加载过多、反射或cglib过多
线程栈溢出(java.lang.StackOverflowError),由于递归太深或方法调用层级过多导致,错误信息为java.lang.StackOverflowError
内存泄露 memory leak,指程序中动态分配内存给一些临时对象,但是对象不会被GC所回收,它始终占用内存。即被分配的对象可达但已无用。
memory leak会最终会导致out of memory
避免发生内存泄漏方案:
a. 尽早释放无用对象的引用
b. 使用字符串处理,避免使用String,应大量使用StringBuffer,每一个String对象都得独立占用内存一块区域
c. 避免在循环中创建对象,JDK1.8已优化,无影响;
d. 开启大型文件或从数据库一次拿了太多的数据很容易造成内存溢出,所以在这些地方要大概计算一下数据量的最大值是多少,并且设定所需最小及最大的内存空间值
1 .7 进程和线程的区别
都是系统进行资源分配和调度的基本单位。
进程
定义:进程是操作系统进行资源分配和调度的一个独立单位。每个进程都有自己的地址空间、内存、数据栈以及记录其运行轨迹的辅助数据。进程之间相互独立,一个进程崩溃不会直接影响到其他进程。
资源隔离:由于每个进程都有独立的空间,因此进程间的通信需要进程间通信(IPC)机制进行,如管道、消息队列、共享内存等。
开销:创建或销毁进程、进程间的切换等操作涉及到较大的开销,因为这些操作需要操作系统介入进行更多的资源分配和状态保存。
线程
定义:线程是进程的执行单元,是CPU调度和分配的基本单位。一个进程可以包含多个线程,他们共享进程的地址空间和资源,每个线程都有自己的执行栈和程序计数器。
资源共享:线程间自然地共享进程资源和数据,使得线程间的数据交换和通信更加容易。但这也意味着需要注意同步和数据一致性问题,避免出现竟态条件。
开销:相比进程,线程的创建、销毁和切换的开销要小得多,因为这些操作无需重复进行资源分配,只需要少量的寄存器和栈的变动。
区别:
- 资源分配和隔离
进程是资源分配的基本单位,拥有独立的内存空间,进程相互间隔;
线程是CPU调度的基本单位,同一进程内的线程共享内存空间和资源。
- 通信方式
进程间通信(IPC)需要特定的机制,成本较高;
线程间可以直接通过读写共享数据来通信,但需要处理同步问题。
- 创建和管理开销
进程的创建、销毁和切换开销较大;
线程的这些开销相对较小。
- 应用场景
进程适用于需要较大独立运行和资源保护的大型任务;
线程适用于轻量级任务,尤其是需要频繁共享内存数据或进行并发执行的场景。
1 . 8 Volatile解析
在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。那么Java内存模型规定了哪些东西呢,它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。
Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中 进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。
volatile关键字的两层语义
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
使用volatile关键字的场景
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
volatile关键字禁止指令重排序有两层意思:
1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
1 .9 线程 的 状态有哪些
A. 新建状态:通过关键字new创建一个线程对象后,该线程就处于新建状态。
B. 就绪状态:当调用start()方法启动线程后,线程进入就绪状态,等待获取CPU时间片执行任务。
C. 运行状态:线程获取到CPU时间片开始执行任务,处于运行状态。
D. 阻塞状态:线程因为某些原因(如等待I/O操作、获取锁失败)而暂时停止运行,进入阻塞状态。当等待的条件满足时,线程会进入就绪状态。
E. 等待状态:线程调用Object.wait()、或等方法进入等待状态,需要其他线程显式唤醒才能继续执行。
F. 超时等待状态:线程调用带有超时参数的等待方法(如Object.wait(long timeout))、Thread.sleep(long millis)、Thread.join(long millis)或LockSupport.parkNanos(long nanos)进入超时等待状态,一段时间后自动返回。
G. 终止状态:线程执行完任务或者发生异常导致线程结束时,进入终止状态。
1 . 10 死锁 发生的条件
死锁是指在多线程或并发程序中,两个或多个线程无限期地等待对方释放资源的一种状态。在死锁状态下,每个线程都在等待其他线程释放资源,而导致所有线程都无法继续执行下去。
死锁通常涉及多个资源,例如内存、文件、数据库连接等。当线程之间相互竞争获取资源,并且持有自己的资源同时等待获取其他线程的资源时,就有可能发生死锁。
死锁发生的四个必要条件:
- 互斥条件:至少有一个资源必须处于非共享状态,即一次只能被一个进程使用。
- 占有且等待:一个进程必须占有至少一个资源,并等待获取其他线程占有的资源。
- 不可抢占:已经分配给一个进程的资源是不能被强制性的抢占,它只能被占有它的进程显式地释放。
- 循环等待:若干进程之间形成一种头尾相接的循环等待资源的关系。
1. 11 synchronized和lock的区别
都是用于实现线程同步和互斥访问共享资源的目的。主要区别:
使用方式:synchronized是Java内置的关键字,可以直接在方法或代码块上使用,使用起来简单,而Lock是一个接口,需要通过具体的实现类(如ReentrantLock)创建实例,并且需要手动进行加锁和解锁的操作。
灵活性:Lock相对于synchronized提供了更多的灵活性,例如,Lock可以实现公平锁(按线程的请求顺序获取锁),而synchronized只能是非公平锁。此外,Lock还支持尝试获取锁、定时获取锁等功能,而synchronized没有提供这些特性。
性能:在低竞争情况下,synchronized的性能通常比Lock好,因为synchronized是由JVM底层进行优化的。在高竞争情况下,Lock的性能可能会更好,因为它提供了更细粒度的控制和更多的优化机会。
异常处理:使用synchronized时,如果发生异常,JVM会自动释放锁,而使用Lock时,需要手动在finally块中释放锁,以确保锁的释放,避免死锁的发生。
synchronized适用于简单的线程同步场合,使用起来更加方便,而Lock适用于复杂的同步需求,提供了更多的高级特性。
1. 12 什么是AQS锁 及常见的AQS锁
AQS(AbsreactQueueSunchronized)是Java中用于构建同步器的框架,可以用来实现各种形式的同步控制。AQS提供了一种基于FIFO等待队列的机制,通过维护一个状态变量和一个等待队列来管理同步状态和线程的竞争。
AQS是一个抽象类,它定义了两种锁:独占锁(Exclusive Lock)和共享锁(Shared Lock)。具体的同步器可以通过继承AQS并实现其中的几个方法来定义同步器,如ReentrantLock、Semaphore等都是基于AQS实现的。
AQS的核心思想是使用一个整型的state表示同步状态,当state为0时表示没有线程持有锁,大于0表示有线程持有锁。大于0表示有线程持有锁,小于0表示有线程在等待获取锁。通过对state的修改和CAS操作,实现线程的加锁和释放锁。
AQS提供了两种主要的方法:acquire和release。acquire用来获取独占锁,如果获取失败则会将当前线程加入到等待队列中;release用来释放锁,并唤醒等待队列中的线程去竞争锁。
常见的AQS锁
ReentrantLock:可重入锁,是java.util.concurent包中提供的基于AQS独占锁。它允许线程重复获取已持有的锁,同时支持公平性和非公平性两种模式。
ReentrantReadWriteLock:可重入读写锁,有一个读锁和一个写锁组成,支持多线程同时读取共享资源,但只允许一个线程写入共享资源。
StampedLock:是Java 8引入的一种基于乐观读写锁机制,性能较高,适用于读操作远远多于写操作的场景。
Semaphore:信号量,基于AQS实现的共享锁,可以控制同时访问某个资源的线程数量。
CountDownLatch:倒计时门闩,用于等待其他线程执行完特定操作后再继续执行。
CyclicBarrier:循环屏障,用于同步多个线程,让它们在达到屏障点时互相等待,然后同时继续执行。
1. 13 线程池的 常用 参数
在Java中,线程池的七大参数通常是指ThreadPoolExecutor构造函数中的参数,这些参数用于配置线程池的行为。
- corePoolSize:核心线程数 ,指定线程池中能保持活动状态的线程数量。当提交任务时,如果核心线程数还未满,则会创建新线程执行任务。
- maximunPoolSize:最大线程数 ,指定线程池中允许的最大线程数两。当任务队列已满且线程池中的线程数已达到核心线程数时,如果仍有新任务提交,则会创建新线程,但不会超过最大线程数。
- keepAliveTime:线程空闲时间 ,即当线程池中线程数量超过核心线程数时,空闲线程的存活时间。当前线程空闲时间超过指定时间时,多余的线程会被销毁,直到线程池中的线程数量等于核心线程数。
- unit:keepAliveTime的时间单位 ,例如TimeUnit.SECONDS表示秒。
- workQueue:任务队列 ,用于存储等待执行的任务。线程池中的线程会从任务队列中获取任务并执行。
- threadFactory:线程工厂 ,用于创建新线程。可以自定义线程工厂来创建线程,看i如设置线程名称、优先级等。
- handler:拒绝策略 ,用于处理无法接受新任务的情况。当任务队列已满且线程池中的线程数已达到最大线程数时,新任务无法被处理时,会根据拒绝策略来进行处理,默认策略是异常抛出。
1.14 加密算法分为几种
加密算法:
- 单向散列:对信息进行散列加密,得到固定长度的密文,算法包括:MD5/SHA;
- 对称加密:加密、解密使用同一个秘钥,算法包括:DES/RC/AES
- 非对称加密:分为公钥、私钥,私钥保存在服务器端,公钥加密的只有私钥能解,反之也是,算法包括:RSA
对称加密算法
对称加密算法用来对敏感数据等信息进行加密,常用的算法包括:
DES(Data Encryption Standard):数据加密标准,速度较快,适用于加密大量数据的场合。
3DES(Triple DES):是基于DES,对一块数据用三个不同的密钥进行三次加密,强度更高。
AES(Advanced Encryption Standard):高级加密标准,是下一代的加密算法标准,速度快,安全级别高;
算法原理:
AES 算法基于排列和置换运算。排列是对数据重新进行安排,置换是将一个数据单元替换为另一个。AES 使用几种不同的方法来执行排列和置换运算。
AES 是一个迭代的、对称密钥分组的密码,它可以使用128、192 和 256 位密钥,并且用 128 位(16字节)分组加密和解密数据。与公共密钥密码使用密钥对不同,对称密钥密码使用相同的密钥加密和解密数据。通过分组密码返回的加密数据的位数与输入数据相同。迭代加密使用一个循环结构,在该循环中重复置换和替换输入数据。
非对称算法
常见的非对称加密算法如下:
RSA:由 RSA 公司发明,是一个支持变长密钥的公共密钥算法,需要加密的文件块的长度也是可变的;
DSA(Digital Signature Algorithm):数字签名算法,是一种标准的 DSS(数字签名标准);
ECC(Elliptic Curves Cryptography):椭圆曲线密码编码学。
散列算法
散列是信息的提炼(信息映射),通常其长度要比信息小得多,且为一个固定长度。加密性强的散列一定是不可逆的,这就意味着通过散列结果,无法推出任何部分的原始信息。任何输入信息的变化,哪怕仅一位,都将导致散列结果的明显变化,这称之为雪崩效应。散列还应该是防冲突的,即找不出具有相同散列结果的两条信息。具有这些特性的散列结果就可以用于验证信息是否被修改。
单向散列函数一般用于产生消息摘要,密钥加密等,常见的有:
MD5(Message Digest Algorithm 5):是RSA数据安全公司开发的一种单向散列算法。
SHA(Secure Hash Algorithm):可以对任意长度的数据运算生成一个160位的数值;
1.15 JVM内存组成结构
结构图如下所示:
java语言本身是不能操作内存的,它的一切都是交给JVM来管理和控制的,因此Java内存区域的划分也就是JVM的区域划分,在说JVM的内存划分之前,我们先来看一下Java程序的执行过程,如下图:
有图可以看出:Java代码被编译器编译成字节码之后,JVM开辟一片内存空间(也叫运行时数据区),通过类加载器加到到运行时数据区来存储程序执行期间需要用到的数据和相关信息,在这个数据区中,它由以下几部分组成:
1 .1 5 .1 虚拟机栈-先进后出
虚拟机栈是Java方法执行的内存模型,栈中存放着栈帧,每个栈帧分别对应一个被调用的方法,方法的调用过程对应栈帧在虚拟机中入栈到出栈的过程。栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中
方法想要执行通过线程执行
栈是线程私有的,也就是线程之间的栈是隔离的;当程序中某个线程开始执行一个方法时就会相应的创建一个栈帧并且入栈(位于栈顶),在方法结束后,栈帧出栈。先进后出
每个栈帧对应着每个方法的每次调用,而栈帧又是有局部变量区和操作数栈、动态连接、方法返回地址四部分组成,局部变量区用于存放方法中的局部变量和参数,操作数栈中用于存放方法执行过程中产生的中间结果,动态连接是运行时编译的将符号引用转为直接引用,方法返回地址指正常返回,异常返回。
下图表示了一个Java栈的模型以及栈帧的组成:
栈帧:是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。
每个栈帧中包括:
- 局部变量表:用来存储方法中的局部变量(非静态变量、函数形参)。当变量为基本数据类型时,直接存储值,当变量为引用类型时,存储的是指向具体对象的引用。
- 操作数栈 :Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",其中所指的栈就是指操作数栈。先进后出,负责参数的传递,算数运算、赋值, 操作数栈的的元素可以是任意Java类型,包括long和double,32位数据占用栈空间为1,64位数据占用2
- 动态连接-指向运行时常量池的引用:存储程序执行时可能用到常量的引用。
- 方法返回地址:存储方法执行完成后的返回地址。
1.15 .2 堆
堆是用来存储对象本身和数组的,在JVM中只有一个堆,因此,堆是被所有线程共享的。堆中不存放基本类型和对象引用,只存放对象本身。分为新生代和老年代。对应java内存模型里的主内存
1 .1 5 .3 程序计数器
线程私有的。记录当前线程执行方法的位置以及下一条要执行的代码指令
记录着当前线程所执行的字节码的行号指示器,在程序运行过程中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、异常处理、线程恢复等基础功能都需要依赖计数器完成。
1 .1 5 .4 方法区
方法区是一块所有线程共享的内存逻辑区域,在JVM中只有一个方法区,用来存储一些线程可共享的内容,它是线程安全的,多个线程同时访问方法区中同一个内容时,只能有一个线程装载该数据,其它线程只能等待。
方法区可存储的内容有:类信息包含类的全路径名、类的直接超类的权全限定名、类的访问修饰符、类的类型(类或接口)、类的直接接口全限定名的有序列表、常量池(字段,方法信息,静态变量,类型引用(class)(jdk1.8已经移到元空间))等
2. rabbitMQ
2.1 exchange模式
AMQP 协议中的核心思想就是:生产者和消费者隔离,生产者从不直接将消息发送给队列。生产者通常不知道是否一个消息会被发送到队列中,只是将消息发送到一个交换机。先由 Exchange 来接收,然后 Exchange 按照特定的策略转发到 Queue 进行存储。同理,消费者也是如此。Exchange 就类似于一个交换机,转发各个消息分发到相应的队列中。
RabbitMQ 提供了6种 Exchange 模式:简单模式、work模式、fanout、direct、topic、header. 由于 header 模式在实际使用中较少,因此本节只对前三种模式进行比较。
Direct Exchange: 直接匹配,通过Exchange名称+RountingKey来发送与接收消息.
Fanout Exchange: 广播订阅,向所有的消费者发布消息,但是只有消费者将队列绑定到该路由器才能收到消息,忽略Routing Key.
Topic Exchange:主题匹配订阅,这里的主题指的是RoutingKey,RoutingKey可以采用通配符,如:*或#,RoutingKey命名采用'.'来分隔多个词,只有消息将队列绑定到该路由器且指定RoutingKey符合匹配规则时才能收到消息;
Headers Exchange:消息头订阅,消息发布前,为消息定义一个或多个键值对的消息头,然后消费者接收消息同时需要定义类似的键值对请求头:(如:x-mactch=all或者x_match=any),只有请求头与消息头匹配,才能接收消息,忽略RoutingKey.
默认的exchange:如果用空字符串去声明一个exchange,那么系统就会使用"amq.direct"这个exchange,我们创建一个queue时,默认的都会有一个和新建queue同名的routingKey绑定到这个默认的exchange上去
2.1.1 fanout-订阅模式
所有发送到 Fanout Exchange 的消息都会被转发到与该 Exchange 绑定(Binding)的所有 Queue 上。
Fanout Exchange 不需要处理 RouteKey,只需要简单的将队列绑定到 exchange 上,这样发送到 exchange 的消息都会被转发到与该交换机绑定的所有队列上。类似子网广播,每台子网内的主机都获得了一份复制的消息。
所以,Fanout Exchange 转发消息是最快的。
2.1.2 direct-路由模式(默认)
所有发送到 Direct Exchange 的消息被转发到 RouteKey 中指定的 Queue.
Direct 模式可以使用 RabbitMQ 自带的 Exchange:default Exchange ,因此不需要将 Exchange 进行任何绑定(binding)操作 。消息传递时,RouteKey 必须完全匹配,才会被队列接收,否则该消息会被抛弃。
注意:所有声明的队列都会绑定到一个空字符的direct交换器上。
2.1.3 topic-模式
所有发送到 Topic Exchange 的消息被转发到所有关心 RouteKey 中指定 Topic 的 Queue 上,Exchange 将 RouteKey 和某 Topic 进行模糊匹配。此时队列需要绑定一个 Topic,可以使用通配符进行模糊匹配,符号#匹配一个或多个词,符号*匹配不多不少一个词。因此log.#能够匹配到log.info.oa,但是log.* 只会匹配到log.error.
所以,Topic Exchange 使用非常灵活。
场景:是否一个动作触发其他动作,并且可以并行运行时;
总结:
Fanout :广播模式,速度最快,不需要关心routekey,只需要将queue绑定到exchange;
Direct:根据routekey精确匹配,queue不需要绑定exchange,消息发送到指定的queue;
Topic:根据route模糊匹配,#-匹配一个或多个单词,*-匹配一个单词,queue不需要绑定exchange,消息发送到指定的queue,使用很灵活,使用较广。内存占用比fanout、direct更多
注意:一个生产者,多个消费者,每个消费者获取到的消息唯一
2.2 如何解决消息堆积和死信队列
消息堆积出现场景:消息发送的速率远远大于消息消费的速率
方案:设置并发消费两个关键属性concurrentConsumers和prefetchCount
- concurrentConsumers设置的是对每个listener在初始化的时候设置的并发消费者的个数,prefetchCount是每次一次性从broker里面取的待消费的消息的个数,从源码中分析org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer:启动的时候会根据设置的concurrentConsumers创建N个BlockingQueueConsumer(N个消费者);
- prefetchCount是BlockingQueueConsumer内部维护的一个阻塞队列LinkedBlockingQueue的大小,其作用就是如果某个消费者队列阻塞,就无法接收新的消息,该消息会发送到其它未阻塞的消费者
死信队列
死信队列:DLX,dead-letter-exchange
利用DLX,当消息在一个队列中变成死信 (dead message) 之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX
消息变成死信有以下几种情况:
- 消息被拒绝(basic.reject / basic.nack),并且requeue = false
- 消息TTL过期
- 队列达到最大长度
死信处理过程
DLX也是一个正常的Exchange,和一般的Exchange没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性。
当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上去,进而被路由到另一个队列。
可以监听这个队列中的消息做相应的处理。
2.3 任务分发机制
Round-robin dispathching循环分发
RabbbitMQ的分发机制非常适合扩展,而且它是专门为并发程序设计的,如果现在load加重,那么只需要创建更多的Consumer来进行任务处理。
Message acknowledgment消息确认
为了保证数据不被丢失,RabbitMQ支持消息确认机制,为了保证数据能被正确处理而不仅仅是被Consumer收到,那么我们不能采用no-ack,而应该是在处理完数据之后发送ack.
在处理完数据之后发送ack,就是告诉RabbitMQ数据已经被接收,处理完成,RabbitMQ可以安全的删除它了.
如果Consumer退出了但是没有发送ack,那么RabbitMQ就会把这个Message发送到下一个Consumer,这样就保证在Consumer异常退出情况下数据也不会丢失.
RabbitMQ它没有用到超时机制.RabbitMQ仅仅通过Consumer的连接中断来确认该Message并没有正确处理,也就是说RabbitMQ给了Consumer足够长的时间做数据处理。
如果忘记ack,那么当Consumer退出时,Mesage会重新分发,然后RabbitMQ会占用越来越多的内存.
Fair dispath 公平分发
你可能也注意到了,分发机制不是那么优雅,默认状态下,RabbitMQ将第n个Message分发给第n个Consumer。n是取余后的,它不管Consumer是否还有unacked Message,只是按照这个默认的机制进行分发.
那么如果有个Consumer工作比较重,那么就会导致有的Consumer基本没事可做,有的Consumer却毫无休息的机会,那么,Rabbit是如何处理这种问题呢?
通过basic.qos方法设置prefetch_count=1,这样RabbitMQ就会使得每个Consumer在同一个时间点最多处理一个Message,换句话说,在接收到该Consumer的ack前,它不会将新的Message分发给它。
注意,这种方法可能会导致queue满。当然,这种情况下你可能需要添加更多的Consumer,或者创建更多的virtualHost来细化你的设计。prefetch count一般设置在100 - 300之间。也就是一个消费者服务最多接收到100 - 300个message来处理,允许处于unack状态
Prefetch count 和 basicQos中的参数一致(Prefetch count就是basicQos的参数值)
channel.basicQos(1)要和channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);是配套使用,只有在channel.basicQos被使用的时候才起到作用。
分发到多个Consumer
如果有两个接收程序都是用了同一个的queue和相同的routingKey去绑定direct exchange的话,分发的行为是负载均衡的,也就是说第一个是程序1收到,第二个是程序2收到,以此类推。
如果有两个接收程序用了各自的queue,但使用相同的routingKey去绑定direct exchange的话,分发的行为是复制的,也就是说每个程序都会收到这个消息的副本。行为相当于fanout类型的exchange。
同queue不同key->direct exchange,负载均衡
不同queue同key->direct exchange,分发复制
3. 数据库
3.1 Update语句加锁规则及更新顺序
Update语句加锁规则
- 当update的字段是主键或者是索引的时候只会锁住索引或者主键上对应的行;
(rc+rr级别下一致)
- 当update是唯一索引时,会同时锁住唯一索引对应的行以及聚簇索引上的行;(rc+rr级别下一致)
- 当update是非唯一索引时,会同时锁住索引对应的行以及聚簇索引上的行,比C多锁很多符合条件的行;
- 当update没有索引的字段时会锁住整个表,SQL会走聚簇索引的全扫描进行过滤,由于过滤是由MySQL Server层面进行的。因此每条记录,无论是否满足条件,都会被加上X锁
- rr隔离级别下,id列上有一个非唯一索引,对应SQL:delete from t1 where id = 10; 首先,通过id索引定位到第一条满足查询条件的记录,加记录上的X锁,加GAP上的GAP锁,然后加主键聚簇索引上的记录X锁,然后返回;然后读取下一条,重复进行。直至进行到第一条不满足条件的记录[11,f],此时,不需要加记录X锁,但是仍旧需要加GAP锁,最后返回结束;
- rr隔离级别下,update字段无索引时,会进行全表扫描的当前读,那么会锁上表中的所有记录,同时会锁上聚簇索引内的所有GAP,杜绝所有的并发 更新/删除/插入 操作。当然,也可以通过触发semi-consistent read,来缓解加锁开销与并发影响,但是semi-consistent read本身也会带来其他问题,不建议使用
全局锁主要用在逻辑备份过程中。对于全部是 InnoDB 引擎的库,我建议你选择使用--single-transaction 参数,对应用会更友好。
表锁一般是在数据库引擎不支持行锁的时候才会被用到的。如果你发现你的应用程序里有 lock tables 这样的语句,你需要追查一下,比较可能的情况是:
要么是你的系统现在还在用 MyISAM 这类不支持事务的引擎,那要安排升级换引擎; 要么是你的引擎升级了,但是代码还没升级。我见过这样的情况,最后业务开发就是把 lock tables 和 unlock tables 改成 begin 和 commit,问题就解决了。
MDL 会直到事务提交才释放,在做表结构变更的时候,你一定要小心不要导致锁住线上查询和更新。
更新语句跟查询语句逻辑结构是一样的
3.2 sql性能优化方法
1. 短连接
a. 先处理占着连接但是不工作的线程
max_connections: 连接就占用一个计数位置,
先执行show processlist,查询information_schema 库的 innodb_trx 表
kill 调trx_state为非running的线程,连接被kill后应用客户端不会马上知道,需要在下一个请求的才会收到"ERROR 2013 (HY000): Lost connection to MySQL server during query"。应用端需要做到重建新连接。
b. 减少连接过程的消耗
有的业务代码会在短时间内先大量申请数据库连接做备用,如果现在数据库确认是被连接行为打挂了,那么一种可能的做法,是让数据库跳过权限验证阶段。
跳过权限验证的方法是:重启数据库,并使用--skip-grant-tables 参数启动。这样,整个 MySQL 会跳过所有的权限验证阶段,包括连接过程和语句执行过程在内,这种方式风险极高不推荐。
- 慢查询性能问题
原因:
- 索引没有设计好,
- Sql问题
- Mysql选错索引
a索引没设计好解决办法:
理想的是能够在备库先执行。假设你现在的服务是一主一备,主库 A、备库 B,这个方案的大致流程是这样的:
在备库 B 上执行 set sql_log_bin=off,也就是不写 binlog,然后执行 alter table 语句加上索引;
执行主备切换;
这时候主库是 B,备库是 A。在 A 上执行 set sql_log_bin=off,然后执行 alter table 语句加上索引。
- sql问题解决办法:
1.1 mysql5.7 提供了query_rewrite功能,语句重写
1.2 强制使用索引force index
- QPS(每秒查询数)突增
解决:
- 一种是由全新业务的 bug 导致的。假设你的 DB 运维是比较规范的,也就是说白名单是一个个加的。这种情况下,如果你能够确定业务方会下掉这个功能,只是时间上没那么快,那么就可以从数据库端直接把白名单去掉。
- 如果这个新功能使用的是单独的数据库用户,可以用管理员账号把这个用户删掉,然后断开现有连接。这样,这个新功能的连接不成功,由它引发的 QPS 就会变成 0。
- 如果这个新增的功能跟主体功能是部署在一起的,那么我们只能通过处理语句来限制。这时,我们可以使用上面提到的查询重写功能,把压力最大的 SQL 语句直接重写成"select 1"返回,不推荐。
其实方案 1 和 2 都要依赖于规范的运维体系:虚拟化、白名单机制、业务账号分离。由此可见,更多的准备,往往意味着更稳定的系统。连接异常断开是常有的事,代码里要有正确地重连并重试的机制。
3.3 sql语句执行顺序及索引的设计规则
Sql解析执行顺序图如下:
案例:
建表语句:
DROP TABLE IF EXISTS student;
CREATE TABLE `student` (
`id` int(5) NOT NULL AUTO_INCREMENT,
`name` varchar(10) DEFAULT NULL,
`subject` varchar(10) DEFAULT NULL,
`grade` double(4,1) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=40 DEFAULT CHARSET=utf8;
初始化数据:
INSERT INTO student(`name`,`subject`,grade)VALUES('aom','语文',88);
INSERT INTO student(`name`,`subject`,grade)VALUES('aom','数学',99);。。。
需求:
要求查询出挂科数目多于两门(包含两门)的前两名学生的姓名,如果挂科数目相同按学生姓名升序排列。
sql:
SELECT `name`,COUNT(`name`) AS num FROM student WHERE grade < 60 GROUP BY `name` HAVING num >= 2 ORDER BY num DESC,`name` ASC LIMIT 0,2;
sql执行流程:
form>on>join>where>group by>having>select>distinct>order by>limit
a. 从数据库的表文件加载到内存中,取出where grade<60的数据并进行过滤,生成一张临时表;
b. 执行group by name会把a步骤生成的临时表切分成若干个临时表,select的执行读取规则分为有无group by两种情况:
-
没有group by时,select会根据后面的字段名称对内存中的一张临时表整列读取;
-
有group by时,从若干个临时表分别执行select,select后面跟的是分组字段和聚合函数,聚合函数的目的是将多列值合并成单个值并且会忽略空值,每一个分组只能返回一条记录,最后会把每个临时表合并成新的临时表。
c. 执行having num>=2 对上述临时表的数据再次过滤,执行之后生成一张临时表
having:用在group by之后,从select语句执行之后从临时表的数据中过滤,having中可以使用别名;
where: 用在group by之前,where是对表文件加载到内存中的原生数据过滤,不能使用别名;
d. 执行order by num desc,name asc对以上的临时表按照num降序,name升序;
e. 执行limit 0,2 取排序后的前两条记录
3.4 索引的设计
1、覆盖索引:如果查询条件使用的是普通索引(或是联合索引的最左原则字段),查询结果是联合索引的字段或是主键,不用回表操作,直接返回结果,减少IO磁盘读写读取正行数据
2、最左前缀:联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符(索引项是按照索引定义里面出现的字段顺序排序
第一原则:如果通过调整顺序可以少维护一个索引,那么这个顺序往往优先考虑)
3、联合索引:根据创建联合索引的顺序,以最左原则进行where检索,比如(age,name)以age=1 或 age= 1 and name='张三'可以使用索引,单以name='张三' 不会使用索引,考虑到存储空间的问题,还请根据业务需求,将查找频繁的数据进行靠左创建索引。
4、索引下推:like 'hello%'and age >10 检索,MySQL5.6版本之前,会对匹配的数据进行回表查询。5.6版本后,会先过滤掉age<10的数据,再进行回表查询,减少回表率,提升检索速度
5、删除表记录时,索引是会一直存在不会释放的。
6、使用前缀索引,扫描函数变多,好处是占用空间小,不能用覆盖索引了。
7、倒序存储,倒序存储方式在主键索引上,不会消耗额外的存储空间,而 hash 字段方法需要增加一个字段,hash字段综合性能更稳定。
8、hash字段:不支持范围查询
索引组织表:根据主键顺序以索引的形式存放的表
总结:
- 尽量使用主键查询;
- 每一张表其实就是多个B+树,树结点的key值就是某一行的主键,value是该行的其他数据。新建索引就是新增一个B+树,查询不走索引就是遍历主键的B+树;
- B+树的叶子节点存放的是页,页里可以存多行
- 主键索引的类型建议设置为bigint;
- 表的逻辑结构 ,表 ---> 段 ---> 段中存在数据段(leaf node segment) ,索引段( Non-leaf node segment),数据段就是主键索引的数据, 索引段就是二级索引和主键的数据;
- 建立的每个索引都有要维护一个数据段,那么新插入一行值 ,每个索引段都会维护这个值
- 查询数据的时候,大致的流程:通过执行引擎到表里的数据段/索引段取数据 ,数据是按照段->区->页维度去取 ,取完后先放到数据缓冲池中,再通过二分法查询叶结点的有序链表数组找到行数据返回给用户 。当数据量大的时候,会存在不同的区,取范围值的时候会到不同的区取页的数据返回用户
- 覆盖索引的目的就是"不回表", 所以只有索引包含了where条件部分和select返回部分的所有字段才能实现。