java并发
一、JAVA如何开启线程,怎么保证线程安全
线程和进程的区别: 进程是操作系统进行资源分配的最小单元,线程是操作系统有进行任务分配的最小单元,线程隶属于进程。
如何开启线程:
- 继承Thread类,重写run方法(由于Java只支持单继承,所以用继承的方式创建线程,不够灵活,所以不推荐使用继承Thread)
- 实现Runnable接口,实现run方法,run()不可以抛出异常
- 实现Callable接口,实现call方法,通过FutureTask创建一个线程,获取到线程的返回值。call方法可以抛出异常
- 线程池
怎么保证安全:
- jvm提供的锁, Synchronized
- JDK提供的各种锁Lock
线程安全在三个方面体现:
原子性—atomic、synchronized
可见性—volatile、synchronized
有序性—volatile、synchronized、lock
二、Volatile和Synchronized有什么区别?Volatile能不能保证线程安全?DCL(Double Check Lock)单例为什么要加Volatile?
1、Synchronized,用来加锁。Volatile只是保持变量的线程可见性。通常使用于一个线程写,多个线程读的场景。
2、不能,Volatile只能保证线程可见性,不能保证原子性
3、Volatile防止指令重排,在DCl中,防止高并发情况下,指令重排造成的线程安全问题
//懒汉式单例
public class LazyMan {
private LazyMan() {
System.out.println(Thread.currentThread().getName() + "ok");
}
private volatile static LazyMan lazyMan;
//双重检测锁模式的 懒汉式单例 DCL懒汉式
public static void getInstance() {
//加锁
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();//不是一个原子性操作
/**
* 1.分派内存空间
* 2.执行构造方法,初始化对象
* 3.把 这个对象指向这个空间 volatile
*/
}
}
}
}
//多线程并发
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(new God() {
@Override
public void run() {
LazyMan.getInstance();
}
}).start();
}
}
}
class God implements Runnable {
@Override
public void run() {
LazyMan.getInstance();
}
}
三、JAVA线程锁机制是怎样的?偏向锁、轻量级锁、重量级锁有什么区别,锁机制是如何升级的?
1、java的锁就是在对象的markWord中记录一个锁状态。无锁,偏向锁,轻量级锁,重量级锁对应不同的锁状态。
2、java的锁机制就是根据资源竞争的激烈程度不断进行锁升级的过程
四、AQS如何实现可重入锁?
可重入锁:可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。
- synchronized
- ReentrantLock
package com.test.reen;
// 演示可重入锁是什么意思,可重入,就是可以重复获取相同的锁,synchronized和ReentrantLock都是可重入的
// 可重入降低了编程复杂性
public class WhatReentrant {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (this) {
System.out.println("第1次获取锁,这个锁是:" + this);
int index = 1;
while (true) {
synchronized (this) {
System.out.println("第" + (++index) + "次获取锁,这个锁是:" + this);
}
if (index == 10) {
break;
}
}
}
}
}).start();
}
}
1、AQS是JAVA线程同步的框架,是JDK中很多锁工具的核心实现框架
2、在AQS中,维护可一个信号量state和一个线程组成的双向链表队列。其中,这个线程队列,就是用来给线程排队的,而state就像是一个红绿灯,用来控制线程排队或者放行的。
3、在可重入锁的这个场景下,state用来表示加锁的次数,0表示无锁,每次加锁state就加1。释放锁state就减1。
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
五、三个线程同时执行?并发情况下保证三个线程依次执行?三个线程有序交错进行?
CountDownLatch:countdownLatch.countDown(),当计数器的值变为0时,在CountDownLatch上await()的线程就会被唤醒。
CylicBarrier:让所有线程都等待完成后才会继续下一步行动。
Semaphore:用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。
/**
* 三个线程同时执行
*/
public class T {
public int count = 0;
public void add() {
count++;
}
public static void main(String[] args) throws InterruptedException {
int size = 3;
T t = new T();
CountDownLatch countDownLatch = new CountDownLatch(1);
for (int i = 0; i < size; i++) {
new Thread(() -> {
try {
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
countDownLatch.await();//三个线程都在这里等待
System.out.println(System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
Thread.sleep(5000);
countDownLatch.countDown();
}
}
/**
* 三个线程依次执行
*/
public class T {
static volatile int ticket = 1;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
if (ticket == 1) {
try {
Thread.sleep(100);
for (int i = 0; i < 10; i++) {
System.out.println("a" + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket = 2;
return;
}
}
});
Thread t2 = new Thread(() -> {
while (true) {
if (ticket == 2) {
try {
Thread.sleep(100);
for (int i = 0; i < 10; i++) {
System.out.println("b" + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket = 3;
return;
}
}
});
Thread t3 = new Thread(() -> {
while (true) {
if (ticket == 3) {
try {
Thread.sleep(100);
for (int i = 0; i < 10; i++) {
System.out.println("c" + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket = 1;
return;
}
}
});
t1.start();
t2.start();
t3.start();
}
}
/**
* 三个线程交替执行
*/
public class T {
private static Semaphore s1 = new Semaphore(1);
private static Semaphore s2 = new Semaphore(1);
private static Semaphore s3 = new Semaphore(1);
public static void main(String[] args) {
try {
s2.acquire();
s3.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
while (true){
try {
s1.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("A");
s2.release();
}
}).start();
new Thread(()->{
while (true){
try {
s2.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B");
s3.release();
}
}).start();
new Thread(()->{
while (true){
try {
s3.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("C");
s1.release();
}
}).start();
}
}
六、如何对一个字符串快速进行排序?
Fork/Join框架
/**
* ForkJoin JDK1.7, 并行执行任务!提高效率,大数据量
* 如何使用ForkJoin
* 1、forkjoinPool通过它来执行
* 2、计算任务forkjoinPool.execute(ForkJoinTask task)
* 3、计算类要继承ForkJoinTask
*/
public class ForkJoinDemo extends RecursiveTask<Long> {
private Long start;
private Long end;
//临界值
private Long temp = 10000L;
public ForkJoinDemo(Long start, Long end) {
this.start = start;
this.end = end;
}
//计算方法
@Override
protected Long compute() {
if (end - start < temp) {
Long sum = 0L;
for (Long i = start; i < end; i++) {
sum += i;
}
return sum;
} else {//forkjion
long middle = (start + end) / 2;
ForkJoinDemo task1 = new ForkJoinDemo(start, middle);
task1.fork();//拆分任务,把任务压入线程队列
ForkJoinDemo task2 = new ForkJoinDemo(middle + 1, end);
task2.fork();//拆分任务,把任务压入线程队列
return task1.join() + task2.join();
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinDemo task = new ForkJoinDemo(0L, 10_0000_0000L);
ForkJoinTask<Long> submit = forkJoinPool.submit(task);
Long sum = submit.get();
System.out.println(sum);
}
}
网络
一、TCP和UDP有什么区别?TCP为什么是三次握手?
TCP Transfer Control Protocol是一种面向连接的、可靠的、传输层通信协议
- 打电话:面向连接,点对点的通信,高可靠的,效率低,占用的系统资源比较多
UDP User Datagram Protocol 是一种无连接的,不可靠的、传输层通信协议
- 特点:广播:不需要连接,发送方不管接收方有没有准备好,直接发送消息;可以进行广播发送的;传输不可靠,有可能丢失消息;效率比较高;协议就会比较简单,占用的系统资源就比较少
TCP 建立连接三次握手,断开连接四次握手
如果是两次握手,可能造成连接资源浪费的情况
二、JAVA有哪几种IO模型?有什么区别?
BIO 同步阻塞IO:blocking 可靠性差,适用于连接比较少且比较固定的场景,
NIO 同步非阻塞IO:non-blocking 可靠性高,吞吐量高,适用于连接比较多并且连接比较短。例如聊天室
多路复用IO模型是目前使用得比较多的模型。Java NIO实际上就是多路复用IO。
在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。
AIO 异步非阻塞IO:Asynchronous IO 可靠性最好,吞吐量高,适用于连接比较多,并且连接比较长。JDK7,需要操作系统的支持
同步、异步、阻塞、非阻塞
在一个网络请求中,客户端会发一个请求到服务端。
1、客户端发了请求后,就一直等待服务端的响应。客户端:阻塞。请求:同步
2、客户端发了请求后,就去干别的事情去了,时不时的过来检查服务端是否给出了响应。客户端:非阻塞。请求:同步
3、换成异步请求,客户端发了请求后,就等待服务端返回响应。客户端:阻塞,服务端:异步
4、客户端发了请求后,就去干别的事情了。等到服务端给出响应后,再过来处理业务。客户端:非阻塞。请求:异步
同步模型(synchronous IO)
- 阻塞IO(bloking IO)
- 非阻塞IO(non-blocking IO)
- 多路复用IO(multiplexing IO)
- 多路复用IO(multiplexing IO)
异步IO(asynchronous IO)
IO模型详解
三、JAVA NIO的几个核心组件是什么?分别有什么作用?
Buffer Channel Selector
channel 类似于流,每个channel对应一个buffer缓冲区。channel会注册到selector。
select会根据channel上发生的读写事件,将请求交由某个空闲的线程处理。selector对应一个或多个线程。
Buffer、Channel都是可读,可写的
四、select,poll和epoll有什么区别?
他们是NIO中多路复用的三种实现机制,是由Linux操作系统提供的。
用户空间和内核空间:操作系统为了保护系统安全,将内核划分为两个部分,一个是用户空间,一个是内核空间。用户空间不能直接访问底层的硬件设备,必须通过内核空间。
文件描述符 File Descriptor(FD):是一个抽象的概念,形式上是一个整数,实际上是一个索引值。指向内核中为每个进程维护进程所打开的文件记录表。当程序打开一个文件或者创建一个文件时,内核就会向进程返回一个FD。
- Select机制:会维护一个FD的结合fd_set(数组)。将fd_set从用户空间复制到内核空间,激活socket。 x64 2048
- poll机制:和select机制差不多,把fd_set结构进行了优化,FD集合的大小就突破了操作系统的限制。poll结构来代替fd_set,通过链表实现的。
- EPoll:Event Poll。EPoll不再扫描所有的FD,只将用户关心的FD的事件存放到内核的一个事件表当中。这样,可以减少用户空间与内核空间之前需要拷贝的数据。
. | 操作方式 | 底层实现 | 最大链接数 | IO效率 |
---|---|---|---|---|
Select | 遍历 | 数组 | 受限于内核 | 一般 |
poll | 遍历 | 链表 | 无上限 | 一般 |
EPoll | 事件回调 | 红黑树 | 无上限 | 高 |
五、HTTP和HTTPS的区别?
HTTP:是互联网上应用最为广泛的一种网络通信协议,基于TCP,可以使浏览器工作更为高效,减少网络传输。
HTTPS:是HTTP的加强版,可以认为是HTTP+SSl(secure socket layer)。在HTTP的基础上增加了一些列的安全机制,乙方面保证数据传输安全,另一方面对访问者增加了验证机制,是目前最为安全的解决方案。
主要区别:
- HTTP的连接是简单无状态的,HTTPS的数据传输时经过证书加密的,安全性更高。
- 传输协议不同,所以端口不一样,80、443
HTTPS的缺点:
- HTTPS的握手协议比较费时,所以会影响服务的响应速度以及吞吐量。
- HTTPS也并不是完全安全的。他的证书体系其实并不是完全安全的。
HTTP特点:
- 无状态:协议对客户端没有状态存储,对事物处理没有“记忆”能力,比如访问一个网站需要反复进行登录操作
- 无连接:HTTP/1.1之前,由于无状态特点,每次请求需要通过TCP三次握手四次挥手,和服务器重新建立连接。比如某个客户机在短时间多次请求同一个资源,服务器并不能区别是否已经响应过用户的请求,所以每次需要重新响应请求,需要耗费不必要的时间和流量。
- 基于请求和响应:基本的特性,由客户端发起请求,服务端响应
- 简单快速、灵活
- 通信使用明文、请求和响应不会对通信方进行确认、无法保护数据的完整性
结果分析:HTTP协议传输数据以明文形式显示
针对无状态的一些解决策略:
场景:逛电商商场用户需要使用的时间比较长,需要对用户一段时间的HTTP通信状态进行保存,比如执行一次登陆操作,在30分钟内所有的请求都不需要再次登陆。
- 通过Cookie/Session技术
- HTTP/1.1持久连接(HTTP keep-alive)方法,只要任意一端没有明确提出断开连接,则保持TCP连接状态,在请求首部字段中的Connection: keep-alive即为表明使用了持久连接
HTTPS特点:
基于HTTP协议,通过SSL或TLS提供加密处理数据、验证对方身份以及数据完整性保护
- 内容加密:采用混合加密技术,中间者无法直接查看明文内容
- 验证身份:通过证书认证客户端访问的是自己的服务器
- 保护数据完整性:防止传输的内容被中间人冒充或者篡改
为什么需要三次握手呢?为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。
为什么需要四次挥手呢?TCP是全双工模式,当client发出FIN报文段时,只是表示client已经没有数据要发送了,client告诉server,它的数据已经全部发送完毕了;但是,这个时候client还是可以接受来server的数据;当server返回ACK报文段时,表示它已经知道client没有数据发送了,但是server还是可以发送数据到client的;当server也发送了FIN报文段时,这个时候就表示server也没有数据要发送了,就会告诉client,我也没有数据要发送了,如果收到client确认报文段,之后彼此就会愉快的中断这次TCP连接。
JVM
一、JVM的内存模型
1.8
二、JAVA 类加载的过程是怎样的?什么是双亲委派机制?
AppClassLoader->ExtentionClassLoader->BootStrapClassLoader
每种类加载器都有他自己的加载目录。
每个类加载器对他加载过的类都有缓存。
双亲委派:向上委托查找,向下委托加载。作用保护JAVA的层的类不会被应用程序覆盖。
类加载过程:
- 加载:把java的字节码数据加载到JVM中,并映射成JVM认可的数据结
- 连接:分为三个小的
- 验证:检查加载到的字节信息是否符合JVM规范
- 准备:创建类或接口的静态变量,并赋初值 半初始化状态
- 解析:把符号引用转为直接应勇 - 初始化
一个对象从加载到JVM,再到被GC清除,都经历了什么过程?
-
用户创建一个对象,JVM首先需要到方法区去找对象的类型信息。然后再创建对象
-
JVM要实例化一个对象,首先要在堆当中先创建一个对象。(半初始化状态)
-
对象首先会分配在堆内存中新生代的Eden区,然后经过一个Minnor GC,对象如果存活,就会进入S区,在后续的每次GC中,如果对象一直存活,就会在S区来回拷贝,每移动一次,年龄+1。(多大年龄才会移入老年代?)
由于对象头存储年龄的比特域占4bit,所以年龄最大为15(1111)超过后,对象转入老年代。 -
当方法执行结束后,栈中 的指针会先移除掉。
-
堆中的对象,经过Full GC,就会被标记为垃圾,然后被GC线程清理掉。
三、怎么确定一个对象到底是不是垃圾,什么是GC Root?
有两种定位垃圾的方式:
- 引用计数法:这种方式是给堆内存当中的每个对象记录一个引用个数,引用个数为0的就认为是垃圾。引用计数无法解决循环引用的问题。
- 根可达算法:这种方式是在内存中,从引用根对象向下一直找引用,找不到的对象就是垃圾。
哪些是GC Root?Stack->JVM Stack,Native Stack,class类,run-time constant pool 常量池,static reference 静态变量。
四、垃圾回收算法
MarkSweep标记清除算法
标记阶段,把垃圾内存标记出来,清除阶段,直接将垃圾内存回收。会产生内存碎片
Copying拷贝算法
为了解决标记清除算法的内存碎片问题,就产生了拷贝算法。拷贝算法将内存分为大小相等的两半,每次只使用其中一半,垃圾回收时,将当前这一块的存活对象全部拷贝到另一半,然后当前这一半内存就可以直接清除。
这种算法没有内存碎片,但是会浪费空间,只使用一半,他的效率跟存活对象有关。
MarkCompack标记压缩算法
为了解决拷贝算法的缺陷。这种算法在标记阶段跟标记清除算法是一样的,但是在完成标记后,不是直接清理垃圾内存,而是将存活对象往一端移动,然后将这一端边界以外的所有内存直接清除。
五、JVM有哪些垃圾回收器,他们都是怎么工作的?什么是STW?他都发生在哪些阶段?什么是三色标记?如何解决错标记和漏标记的问题?为什么要这几这么多的垃圾回收器?
Jvm垃圾回收器(基础篇)
Jvm垃圾回收器(基础篇)
Jvm垃圾回收器(终结篇)
STW:是垃圾回收算法执行过程中,需要将JVM内存冻结的一种状态。在STW状态下,JAVA的所有线程都是停止执行的,native方法可以执行,但是不能与JVM交互,GC算法优化的重点就是减少STW。
serial:GC时需STW,这个垃圾回收器是早期垃圾回收器,只有一个线程执行GC,在多CPU架构下,性能就会下降严重,只适用于几十兆的内存空间。
Parallerl:在串行基础上,增加多线程GC,PS+PO这种组合是JDK1.8默认的垃圾回收器。在多CPU的架构下,性能会比Serial高很多。
CMS:Concurrent Mark Sweep,一部分GC线程与用户线程并发执行,整个过程分为四个阶段
- 初始标记阶段:STW只标记出根对象直接引用的对象
- 并发标记:继续标记其它对象,与应用程序是并发执行的
- 重新标记:STW对并发执行阶段的对象进行重新标记
- 并发清除:并行,将产生的垃圾清除。清除过程中,应用程序又会不断的产生新的垃圾,叫做浮动垃圾。这些垃圾就要留到下一次GC过程中清除。
G1:Garbage First 垃圾优先
他的内存模型是实际不分代,但是逻辑上是分代的。在内存模型中,对于堆内存就不再分老年代和新生代,而是划分成一个一个的小内存块,叫做Regon。每个Region可以隶属于不同的年代。
GC分为四个阶段:
- 初始标记:标记出GCRoot直接引用的对象。STW
- 标记Region,通过Rset标记出上一个阶段标记的Region引用到的Old区Region
- 并发标记阶段:跟CMS差不多,只是遍历的范围不再是整个Old区,而是需要遍历第二部标记出来的Region
- 重新标记
- 垃圾清理:与CMS不同的是,G1可以采用拷贝算法,直接将整个Region中的对象拷贝到另一个Region。而这个阶段,G1只选择垃圾较多的Region来清理,并不是完全清理。
CMS的核心算法就是三色标记
三色标记:将每个内存对象分成三种颜色,黑色:表示自己和成员变量都已经标记完毕,灰色:自己标记完了,但是成员变量还没有标记完成,白色:自己未标记完
CMS通过增量标记increment update 的方式来解决漏标的问题
六、如何进行JVM调优,JVM参数有哪些?怎么查看JAVA进程的JVM参数?如果一个JAVA程序每次运行一段时间后,就变得卡顿,你准备如何优化?
JVM调优主要就是通过定制JVM运行参数来提高JAVA应用程序的运行数据
JVM参数大致分为三类:
- 标注指令:-开头,这些是所有的HotSpot都支持的参数。可以用java -help打印出来
- 非标准指令:-X开头,这些指令通常是跟特定的HotSpot版本对应的, 可以用java -X打印出来
- 不稳定参数:-XX开头,
java -XX:+PrintCommandLineFlags,查看当前命令的不稳定指令
java -XX:+PrintFlagsinitial,查看所有不稳定指令的默认值
java -XX:+PrintFlagsFinal,查看所有不稳定指令最终生效的实际值
MQ
一、MQ有什么用,有哪些具体的使用场景?
MQ:MessageQueue,消息队列。队列是一种FIFO先进先出的数据结构。消息由生产者发送到MQ进行排队,然后由消费者对消息进行处理。QQ、微信就是典型的MQ场景。
MQ的作用主要有三个方面:
- 异步
例子:快递,快递员->菜鸟驿站<-客户
作用:异步能提高系统的响应速度和吞吐量 - 解耦:
作用:服务之间进行解耦,可以减少服务之间的影响,提高系统的稳定性和可扩展性。解耦之后可以实现数据分发,生产者发送一个消息后,可以由多个消费者来处理。 - 削峰:
作用:以稳定的系统资源应对突发的流量冲击。
MQ的缺点:
- 系统的可用性降低:一旦MQ宕机,整个业务就会产生影响。高可用
- 系统的复杂度提高:引入MQ之后,数据链路就会变得复杂。如何保证消息不丢失?消息不会重复调用?怎么保证消息的顺序性?
- 数据一致性:A系统发消息,需要由B、C两个系统一同处理,如果B系统处理成功、C系统处理失败,这就会造成数据一致性的问题。
二、如何进行产品选型?
Kafka
优点:吞吐里大,性能非常好,集群高可用
缺点:会丢数据,功能比较单一。
使用场景:日志分析,大数据采集
RabbitMQ
优点:可靠性高,功能全面。
缺点:吞吐量比较低,消息积累会严重影响性能。erlang语言不好定制
使用场景:小规模场景。
RocketMQ
优点:高吞吐,高性能,高可用,功能非常全面
缺点:客户端只支持java
三、如何保证消息不丢失?
1、哪些环节会丢消息
2、怎么防止消息丢失
2.1生产者发送消息不丢失
kafka:消息发送+回调
RocketMQ:
- 消息发送+回调。
- 事务消息
RabbitMQ:
- 消息发送+回调
- 手动事务:channel.txSelect()开启事务,channel.txComment()提交事务,channel.txRollback()回滚事务。这种方式对channel会产生阻塞,造成吞吐量下降。
- Pulisher Confirm。整个处理流程跟RocketMQ事务消息基本一样。
2.2MQ主从消息同步不丢失
RocketMQ:1、普通集群中,同步同步,异步同步。异步效率高,但是会丢消息,同步不会。2、Dledger集群-两阶段提交
RabbitMQ:
- 普通集群:消息是分散存储的,节点之间不会主动进行消息同步,是有可能丢失消息的。
- 镜像集群:镜像集群会在节点之间主动进行数据同步,这样数据安全性得到提高
Kafka:通常都是用在消息少量丢失的场景
2.3MQ消息存盘不丢失
RocketMQ:同步刷盘,异步刷盘
RabbitMQ:将队列配置成持久化队列
2.4消费者消费消息不丢失
RocketMQ:使用默认的方式消费就行,不要采用异步
RabbitMQ:autoCommit->手动提交offset
Kafka:手动提交offset
四、如何保证消息消费的幂等性?
其实就是要防止消费者重复消费消息的问题
RocketMQ:给每个消息分配了MessageID,这个MessageID就可以作为消费者判断幂等的依据。
五、如何保证消息的顺序?
全局有序和局部有序:MQ只需要保证局部有序,不需要保证全局有序。
生产者把一组有序的消息放到同一个队列中,而消费者一次消费整个队列中的消息。
RocketMQ中有完整的设计
RabbitMQ:要保证目标exchange只对应一个队列,并且一个队列只对应一个消费者
Kaf卡:生产者通过定制partion分配规则,将消息分配到同一个partition。Topic下只对应一个消费者
五、如何保证消息的高速读写?
零拷贝:kafka和RocketMQ都是通过零拷贝技术来优化文件读写
传统文件复制方式
零拷贝:有两种方式,mmap和tansfile
七、使用MQ如何让保证分布式事务的最终一致性?
分布式事务:业务相关的多个操作,保证他们同时成功或者同时失败。
最终一致性:与之对应的就是强一致性
MQ中要保护事务的最终一致性,就需要做到两点
- 生产者要保证100%的消息投递。事务消息机制
- 消费者这一端需要保证幂等消费。唯一ID+业务自己实现幂等
八、如何设计一个MQ?
- 实现一个单机的队列数据结构,高效,可以扩展
- 将单机队列扩展成为分布式队列。分布式集群管理
- 基于Topic定制消息路由策略。发送者路由策略,消费者与队列对应关系,消费路由策略
- 实现高效的网络通信。Netty、Http
- 规划日志文件,实现文件高效读写。零拷贝,顺序写。服务重启后,快速还要运行现场
- 定制高级功能,死信队列、延迟队列,事务消息。
缓存
一、为什么使用缓存?
1、高可用
mysql 数据库对于高并发来说天然支持不好,mysql 单机支撑到 2000QPS 也开始容易报警了。
所以若是系统高峰期一秒钟有1万个请求,那么一个 mysql 单机绝对会死掉。这个时候就只能上缓存,把很多数据放入缓存,别放入 mysql。缓存功能简单,说白了就是 key-value 式操作,单机支撑的并发量一秒可达几万十几万,单机承载并发量是 mysql 单机的几十倍。
缓存是走内存的,内存天然就支撑高并发。
2、高性能
假设这么个场景,有个操作,一个请求过来,耗时 600ms 操作 mysql查出来一个结果,但是这个结果可能接下来几个小时都不会变了,或者变了也可以不会立即反馈给用户。那么此时咋办?
将折腾 600ms 查出来的结果放入缓存里,一个 key 对应一个 value,下次查找时不经过 mysql,直接从缓存里通过一个 key 查出来一个 value,2ms 搞定,性能提升 300 倍。
所以对于一些需要复杂操作耗时查出来的结果,确定后面不怎么变化,但是有很多读请求,直接将查询出来的结果放在缓存中,后面直接读缓存就好。
二、什么是缓存穿透?缓存击穿? 缓存雪崩?
缓存穿透:缓存中查不到,数据库中也查不到
解决方案:1、对数据进行合法效验,2、将数据库中没有=查到结果的数据也写入缓存。这时要注意为了防止Redis被无用的Key占满,这一类缓存的有效期要设置的短一点。3、引入布隆过滤器。在访问Redis之前判断数据是否存在。布隆过滤器存在一定的误判率,并且,布隆过滤器只能加数据不能减数据。
缓存击穿:缓存中没有,数据库中有,一般是出现在key过期了的情况。他的问题在于,重新写入缓存需要一定的时间,如果是在高并发场景下,过多的请求就会瞬间写道DB上,给DB造成很大的压力
解决方案:1、设置这个热点缓存永不过期。这时要注意在value当中包含一个逻辑上的过期时间,然后另起一个线程,定期重建这些缓存。2、加载DB的时候,要防止并发。
缓存雪崩:缓存大面积 过期,导致请求都被转发到DB
解决方案:1、把缓存的失效时间分散开。例如,在原有的统一失效时间基础上增加一个随机值。2、对热点数据设置永不过期。
三、如何保证Redis与数据库的数据一致?
mysql
一 、MySQL有哪几种数据存储引擎?有什么区别?
二、什么是脏读、幻读、不可重复读?怎么处理?
脏读:在事务进行过程中,读到了其它事务未提交的数据
不可重复读:在一个事务过程中,多次查询的结果不一致
幻读:在一个事务过程中,用同样的操作查询数据,得到的记录数不相同
处理方式:加锁、事务隔离、MVCC
加锁:
- 脏读:在修改时加排他锁,直到事务提交才释放 。读取时加共享锁,读完释放锁
- 不可重复读:读数据时加共享锁,写数据时加排他锁
- 幻读:加范围锁
三、事务的基本特性和隔离级别有哪些?
事务:表示多个数据操作组成一个完整的事务单元,这个事务内的所有数据操作要么同时成功,要么同时失败。
事务的特性:ACID
- 原子性 Atomicity:事务是不可分割的,要么完全成功,要么完全失败
- 一致性 Consistency:事务无论是完成还是失败,都必须保持事务内操作的一致性。当失败时,都要对前面的操作进行回滚,不管中途是否成功。也就是说一个事务执行之前和执行之后都必须处于一致性状态。
拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。 - 隔离性 Isolation:当多个事务操作一个数据的时候,为了防止数据损坏,需要将每个事务进行隔离,互补干扰。
- 持久性(Durability): 持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
MySQL当中有五种隔离级别
- NONE: 不使用事务
- READ UNCOMMIT:允许脏读
- READ COMMIT:防止脏读,最常用的隔离级别
- REPEATABLE READ: 防止脏读和不可重复读。mysql默认
- SERIALZEBLE:事务串行,可以防止脏读、幻读,不可重复读
脏读(READ UNCOMMIT)是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
不可重复读(REPEATABLE READ)是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
幻读(READ COMMIT)是事务非独立执行时发生的一种现象。例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。
不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。
幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。
隔离级别越高,事务的安全性更高,但是事务的并行性能就会越低。
MySQL锁有哪些?什么是间隙锁?
从锁的粒度来区分
1、 行锁:加锁粒度小,但是加锁资源开销比较大。InnoDB支持
- 共享锁:读锁,多个事务可以同时对同一个数据共享一把锁。持有锁的事务都可以访问数据,但是只能读不能修改 。select * * LOCK IN SHARE MODE
- 排他锁: 写锁。只有一个事务能够获得排他锁,其他事务都不能获取该行的锁。InnoDB会对update/delete/insert语句自动添加排他锁。SELECT xxx FOR UPDATE
- 自增锁: 通常是针对MySQL当中的自增字段,如果有事务回滚这种情况,数据会回滚,但是自增序列不会回滚
2、表锁:加锁粒度大,加锁资源开销比较小。MySAM和InnoDB都支持。
- 表共享锁
- 表排他锁
- 意向锁:是innoDB自动添加的一种锁,不需要用户干预
3、全局锁:Flush table with read lock 加锁之后整个数据库实例都处于制度状态。所有的数据变更操作都会被挂起,一般用于全库备份
常见的锁算法:
1、记录锁:锁一条具体的数据
2、间隙锁:RR隔离级别下,会加间隙锁。锁一定的范围,而不锁具体的记录,是为了防止产生幻读
3、Next-key
五、mysql的索引是什么样的,聚簇索引和非聚簇索引又是什么?
二叉树:每个节点最多有两个节点,左边的子节点都比当前节点小,右边的子节点都比当前节点大。
AVL树:树种任意系欸但的两个子树的高度差最大为1
红黑树:每个节点都是红色或者黑色,根节点是黑色,每个叶子节点都是黑色的空节点,红色节点的父子节点都必须是黑色。从任一节点到其每个叶子节点的所有路径都包含相同的黑色节点
B-树:1、B-树的每一个非叶子节点的子节点个数都不会超过D(D是阶)2、所有的叶子节点都在同一层。3、所有节点关键字都是按照递增序列排序
B+树:1、非叶子节点不存储数据,只存储数据索引。 2、所有数据都存储在叶子节点当中。3、每个叶子节点都存有相邻叶子节点的指针。4、叶子节点按照本身关键字从小到大排序。
聚簇索引就是数据和索引是在一起的
myisam使用的是非聚簇索引,树的子节点上的data不是数据本身,而是数据存放的地址。InnoDB采用的是聚簇索引,树的叶子节点上的data就是数据本身。