基础面试题总结

基础面试题总结

基本功

面向对象的特征
  1. 继承

    ​ 子类继承父类,子类可以获取父类的属性和方法,这里需要注意权限修饰符的使用,父类使用private修饰的属性和方法不能被继承,子类可以在父类的基础上进行方法的重写

  2. 封装

    ​ 是指的是类的内部信息进行隐藏,一般是指对类内部的属性进行私有化,外部无法直接访问或影响内部的属性,只能通过特定的方法对封装的内容进行访问,提高了代码的安全性。

  3. 多态

​ 一个接口可被多个类实现,对接口方法进行不同的实现,父类可以接受所有子类对象。

静态方法的作用

1、初始化类

2、访问静态变量

3、所有对象都可直接使用该类的方法,无需创建对象

try-with-resource用法(java7特性)

被try后面小括号包裹的资源会自动释放,与添加finally代码块的作用一样,但是省略了对close()方法繁琐的try-catch(),释放的资源对象需要实现AutoCloseable接口,否者无法自动找到关闭方法

try(FileInputStream fix = new FileInputStream("1.text")){

		int data = fis.read();

}catch(Exception e){

		e.printStackTrace();

}
optional的作用(java8特性)

Optional没有给出公共的构造方法,需要调用三个静态方法来构建Optional对象,

1、Optional.of()直接构建Optional对象

2、Optional.ofNullable()判断是否为空对象,如果给的入参为null则返回空对象Optional.empty

3、Optional.empty()直接返回空对象Optional.empty

判断是否为空对象,需要调用Optional的isPresent()和isEmpty()方法,但是isEmpty方法在Java11才出现,后续操作可以直接集成在isPresent(Consumer action)和isPresentOrElse(Consumer action,Runnable emptyAction)方法中,直接设置对象属性的默认值,或是抛出异常

final、finally、finalize的区别

final修饰符可作用于类和类属性上,作用在类上时,表示该类为最终的,不可被继承,例如String类。作用于属性上时,表示这是个常量,仅可被赋值一次后续不可被更改。

finally是try-catch的一个部分,常用于IO流和链接的释放,避免出现非必要的内存和通道的占用。

finalize是手动清理对象的方法,通过调用finalize()方法完成垃圾回收器将对象从内存中清除前做必要的清理工作。

int和Integer的区别

int是Java的基本数据类型,而Integer是int的包装类,int的默认值是0,而Integer的默认值是null,使用Integer作为接口接收参数类型时,可以自动转换String类型的数字串为数字,作为返回值时,属性值可为空。

Java如何禁止变量被序列化

想要类中的某些变量不被序列化,使用transient修饰即可,表示这是个临时变量,不能被序列化

重载和重写的区别

重载指的是类中多个同名方法,参数类型和个数都不同,而返回值相同,常用于类的构造器。

重写指的是子类继承父类,将父类方法进行重定义,写自己需要的业务代码,方法的参数类型和个数都不可更改。

抽象类和接口的区别

一个类只能继承一个抽象类,而一个类可以实现多个接口

抽象类可以有方法实现,而接口不能(jdk8里可以有默认实现代码)

抽象类中的变量为普通变量,而接口中的是公共的静态常量

接口里的方法全是public的方法,抽象类可以有非抽象的方法

接口是对方法的抽象,是行为的规范,而抽象方法是对类的抽象,是模版设计

需要子类继承成员变量,或需要控制子类实现时需要继承抽象类(表现在抽象类的构造去上,抽象类构造器添加参数,来达到规范子类的目的),其他的则使用接口
反射的用途及实现

核心是JVM在运行时才动态加载类或调用方法/访问属性,不需要事先知道运行对象是谁,主要用于开发各种通用框架

获取类

​ 直接获取某个类的class

​ 调用类实例的getClass()方法

​ 使用Class类的forName()方法

创建实例

​ 使用Class类的newInstance()方法

​ 先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建实例

获取方法

​ getDeclaredMethods()方法

​ getMetheds()方法

​ getMethed(String methedName, …Object args)方法

自定义注解作用及实现

请求拦截,校验是否满足要求,全局方法拦截,进行日志打印

注解分为元注解和自定义注解

常用元注解:

Target:描述了注解修饰的对象范围,取值在java.lang.annotation.ElementType定义,常用的包括:

  • ​ METHOD:用于描述方法
  • ​ PACKAGE:用于描述包
  • ​ PARAMETER:用于描述方法变量
  • ​ TYPE:用于描述类、接口或enum类型

Retention: 表示注解保留时间长短。取值在java.lang.annotation.RetentionPolicy中,取值为:

  • SOURCE:在源文件中有效,编译过程中会被忽略
  • CLASS:随源文件一起编译在class文件中,运行时忽略
  • RUNTIME:在运行时有效
@Target(ElementType.FIELD)  //  注解用于字段上
@Retention(RetentionPolicy.RUNTIME)  // 保留到运行时,可通过注解获取
public @interface MyField {
    String description();
    int length();
}
HTTP请求的GET和POST的区别

get请求通过url直接请求数据,数据信息在url中能直接看见,post请求的数据被隐藏,不能直观的看见请求数据,且请求url长度有限制,因此get请求携带的数据量是受到限制的,post请求理论上没有限制

session和cookie的区别

session存放于服务端,cookie存放于客户端

cookie存储的数据有限制,而session没有

cookie生命周期是累积的,而session的生命周期是间隔的

session的分布式处理

使用token替代session,改用jwt验证

服务器间session的同步

使用redis存放session信息,以达成服务间的数据同步

JDBC流程

加载驱动类

获取链接对象

获取语句对象

执行sql,如果是查询语句,需要处理结果集

关闭语句对象、数据库链接

MVC流程

基础面试题总结_第1张图片

equals和==的区别

equals比较的是对象的值,而==比较的是对象的地址

可重写hashCode和equals方法,用于比较两个类是否相等

lambda表达式
使用场景:简化函数式接口匿名内部类的写法
使用前提:必须是函数式接口
函数式接口:只有一个抽象方法的接口,通常会用@FunctionalInterface注解标识

lambda只能简化函数式接口的写法,表达形式为(匿名内部类被重写的形参列表) ->{

​ 被重写的方法代码

};

示例:

public class LambdaDemo {
    public static void main(String[] args) {
      String str = "小狗快跑"
      //实现方式1
        Animal animal = (str) ->{
            System.out.println(str);
        };
      	animal.run();
			//实现方式2
        run((str) -> {
            System.out.println(str);
        });
    }
        
    static void run(Animal animal){
        animal.run();
    }
}

@FunctionalInterface
public interface Animal {
    void run(String str);
}
可省略部分:
Arrays.sort(args, (Integer arg1, Integer arg2) ->{

		return arg1 - arg2;

})


1、参数类型可省略不写

Arrays.sort(args, (arg1, arg2) ->{

		return arg1 - arg2;

})

2、如果只有一个参数,括号也可以省略不写

3、如果方法体只有一行代码,大括号可以不写,删除分号

Arrays.sort(args, (arg1, arg2) -> return arg1 - arg2)

4、如果方法只有一行代码,且是return语句,省略return,删除分号和大括号

Arrays.sort(args, (arg1, arg2) -> arg - arg2)

集合

List和Set的区别

List与Set都继承自Collection接口,List保持插入时的顺序,而Set不会(linkedHashSet除外,linkedHashSet可保留插入顺序,但不能拥有重复值),Set存入元素值时根据计算出的HashCode值进行排列存放,因此Set中不允许存在重复的值,且只能有一个空值,反之List可以有多个空值。List底层以数组或双向链表的形式存储值,而Set以哈希表或树形存储数据值。

List和Map的区别

Map以k-v键值对的形式存储值,底层以数组加链表的形式存储数据,在1.8之后,当数据量超过8时会转化为红黑树,HashMap和TreeMap都是不安全的集合,需要在存取数据时加锁以达成数据一致性,ConcurrentHashMap和HashTable是线程安全的Map集合,但不推荐使用HashTable,其底层时添加synchronized关键字来达成线程安全,在大量线程访问时可能会进入阻塞或轮询状态,而ConcurrentHashMap底层采用分段锁,在Map的node上添加16个Segment,这也意味着最多只支持16个线程同时操作数据,Segment继承了ReentrantLock,具备锁和释放锁的能力,因此在Segment下的操作上安全的。

线程

线程的生命周期

它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5中状态。

基础面试题总结_第2张图片

启动线程使用start(),而不是run()。永远不要调用线程对象的run()方法。如果调用直接调用线程对象的run()方法,则run()方法立即就会被执行,而且在run()方法返回之前其他线程无法并发执行——也就是说,系统把线程对象当成一个普通对象,而run()也是一个普通方法,而不是线程执行体。

Vector是一个线程安全类吗?

Vector是一个线程安全的类,其在add()等操作上添加了synchronized关键字实现同步,但是并非是绝对的线程安全类.
当进行迭代遍历时,如果在另一个线程执行add(),remove()操作,仍然会有机率抛出异常ConcurrentModificationException.

创建线程的方式

直接初始化Thread类,实现Runnable接口的run方法

public static void main(String args[]){
    new Thread(new Runnable(){
 
        @Override
        public void run() {
            System.out.println("test create thread by Runable");
        }
    }).start();
}

实现Runnable接口

class Demo implements Runnable{		
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

public static void main(String args[]) {
    Demo d = new Demo();
    Thread thread1 = new Thread(d,"first thread");
    Thread thread2 = new Thread(d,"second thread");
    thread1.start();
    thread2.start();
}	

实现callable接口配合FutureTask类实现

class Demo implements Callable{

    @Override
    public Object call() throws Exception {
        int sum=0;
        for(int i=0;i<10;i++){
            sum=sum+i;
            Thread.sleep(300);
        }
        System.out.println("sum:"+sum);
        return sum;
    }

}


public static void main(String args[]) throws Exception{

    System.out.println("time1:"+new Date());
    Demo d = new Demo();
    FutureTask futureTask = new FutureTask(d);
    Thread thread = new Thread(futureTask);
    thread.start();
    System.out.println("time2:"+new Date());
    Object sum = futureTask.get(5,TimeUnit.SECONDS);
    System.out.println("time3:"+new Date());
    System.out.println(sum);

}

使用线程池创建

class Demo implements Callable{
    @Override
    public Object call() throws Exception {
        int sum=0;
        for(int i=0;i<10;i++){
            sum=sum+i;
            Thread.sleep(300);
        }
        System.out.println("sum:"+sum);
        return sum;
    }
}

public static void main(String args[]) throws Exception{
  ThreadPoolExecutor pool = new ThreadPoolExecutor(10,20,60L,TimeUnit.SECONDS,new SynchronousQueue<>(), new ThreadFactoryBuilder().setNameFormat("test thread pool").build(),new ThreadPoolExecutor.AbortPolicy());
  Demo d = new Demo();
  Future f = pool.submit(d);
  System.out.println("当前时间:"+new Date());
  Object result = f.get(5L,TimeUnit.SECONDS);
  System.out.println("当前时间:"+new Date()+"==结果:"+result);
}


终止线程4种方式

1、线程正常执行任务结束后关闭

2、使用退出标志,线程运行期间判断到标志符合条件将中止后续的操作跳出循环等待,直接执行线程执行后的逻辑。

3、使用interrupted()方法来中止线程,这时需要注意原线程的状态

​ 3.1原来线程处于阻塞状态,这时调用interrupted()方法会抛出异常InterruptedException,需要对异常进行捕获后使用break打断循环,结束线程转而执行后续的业务逻辑

​ 3.2原来的线程未阻塞,使用 isInterrupted 方法判断线程的中断标志来退出循环。在调用 interrupt 方法的时候,中断标志会设置为 true,此时并不能立刻退出线程而是需要执行线程终止前的资源释放操作,等待资源释放完毕后方可安全退出该线程。

4、使用stop方法终止线程,但是不推荐使用,直接终止线程出现的问题无法预料

sleep()、wait()、join()、yield()区别

​ sleep()方法需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。但是sleep()方法不会释放“锁标志”,也就是说如果有synchronized同步块,其他线程仍然不能访问共享数据。

​ wait()方法需要和notify()及notifyAll()两个方法一起介绍,这三个方法用于协调多个线程对共享数据的存取,所以必须在synchronized语句块内使用,也就是说,调用wait(),notify()和notifyAll()的任务在调用这些方法前必须拥有对象的锁。注意,它们都是Object类的方法,而不是Thread类的方法。
  wait()方法与sleep()方法的不同之处在于,wait()方法会释放对象的“锁标志”。当调用某一对象的wait()方法后,会使当前线程暂停执行,并将当前线程放入对象等待池中,直到调用了notify()方法后,将从对象等待池中移出任意一个线程并放入锁标志等待池中,只有锁标志等待池中的线程可以获取锁标志,它们随时准备争夺锁的拥有权。当调用了某个对象的notifyAll()方法,会将对象等待池中的所有线程都移动到该对象的锁标志等待池。
  除了使用notify()和notifyAll()方法,还可以使用带毫秒参数的wait(long timeout)方法,效果是在延迟timeout毫秒后,被暂停的线程将被恢复到锁标志等待池。
  此外,wait(),notify()及notifyAll()只能在synchronized语句中使用,但是如果使用的是ReenTrantLock实现同步,该如何达到这三个方法的效果呢?解决方法是使用ReenTrantLock.newCondition()获取一个Condition类对象,然后Condition的await(),signal()以及signalAll()分别对应上面的三个方法。

​ yield()方法和sleep()方法类似,也不会释放“锁标志”,区别在于,它没有参数,即yield()方法只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行,另外yield()方法只能使同优先级或者高优先级的线程得到执行机会,这也和sleep()方法不同。

​ join()方法会使当前线程等待调用join()方法的线程结束后才能继续执行

public class TestJoin {

  public static void main(String[] args) {
    Thread thread = new Thread(new JoinDemo());
    thread.start();

    for (int i = 0; i < 20; i++) {
      System.out.println("主线程第" + i + "次执行!");
      if (i >= 2)
        try {
          // t1线程合并到主线程中,主线程停止执行过程,转而执行t1线程,直到t1执行完毕后继续。
          thread.join();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
    }
  }
}

class JoinDemo implements Runnable {

  @Override
  public void run() {
    for (int i = 0; i < 10; i++) {
      System.out.println("线程1第" + i + "次执行!");
    }
  }
}

程序运行结果如下:

img

CountDownLatch原理

​ CountDownLatch可以使一个获多个线程等待其他线程各自执行完毕后再执行。

​ CountDownLatch 定义了一个计数器,和一个阻塞队列, 当计数器的值递减为0之前,阻塞队列里面的线程处于挂起状态,当计数器递减到0时会唤醒阻塞队列所有线程,这里的计数器是一个标志,可以表示一个任务一个线程,也可以表示一个倒计时器,CountDownLatch可以解决那些一个或者多个线程在执行之前必须依赖于某些必要的前提业务先执行的场景。

基础面试题总结_第3张图片

​ CountDownLatch常用方法:

CountDownLatch(int count); //构造方法,创建一个值为count 的计数器。

await();//阻塞当前线程,将当前线程加入阻塞队列。

await(long timeout, TimeUnit unit);//在timeout的时间之内阻塞当前线程,时间一过则当前线程可以执行,

countDown();//对计数器进行递减1操作,当计数器递减至0时,当前线程会去唤醒阻塞队列里的所有线程。

​ 使用实例

public class CountDownLatchTest {

    private static final CountDownLatch cdl = new CountDownLatch(3);
    
    public static void main(String[] args) throws InterruptedException {
        System.out.println("开始");
    
        new Thread(() -> {
            try {
                System.out.format("当前计数=%s,需等待 \n", cdl.getCount());
                Thread.sleep(1000);
                cdl.countDown();
                System.out.format("当前计数=%s,需等待 \n", cdl.getCount());
                Thread.sleep(1000);
                cdl.countDown();
                System.out.format("当前计数=%s,需等待 \n", cdl.getCount());
                Thread.sleep(1000);
                cdl.countDown();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
    
        cdl.await(6000, TimeUnit.MILLISECONDS);
        print();
        System.out.println("结束");
    }
    
    private static void print() {
        System.out.println("执行了");
    }

}

【控制台输出】
开始
当前计数=3,需等待
当前计数=2,需等待
当前计数=1,需等待
执行了
结束

CyclicBarrier原理

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。

这个屏障之所以用循环修饰,是因为在所有的线程释放彼此之后,这个屏障是可以重新使用的(reset()方法重置屏障点),这一点与CountDownLatch不同。

CyclicBarrier是一种同步机制允许一组线程相互等待,等到所有线程都到达一个屏障点才退出await方法,它没有直接实现AQS而是借助ReentrantLock来实现的同步机制。它是可循环使用的,而CountDownLatch是一次性的,另外它体现的语义也跟CountDownLatch不同,CountDownLatch减少计数到达条件采用的是release方式,而CyclicBarrier走向屏障点(await)采用的是Acquire方式,Acquire是会阻塞的,这也实现了CyclicBarrier的另外一个特点,只要有一个线程中断那么屏障点就被打破,所有线程都将被唤醒(CyclicBarrier自己负责这部分实现,不是由AQS调度的),这样也避免了因为一个线程中断引起永远不能到达屏障点而导致其他线程一直等待。屏障点被打破的CyclicBarrier将不可再使用(会抛出BrokenBarrierException)除非执行reset操作。

CyclicBarrier有两个构造函数:

CyclicBarrier(int parties)
int类型的参数表示有几个线程来参与这个屏障拦截,(拿上面的例子,即有几个人跟团旅游);
CyclicBarrier(int parties,Runnable barrierAction)
当所有线程到达一个屏障点时,优先执行barrierAction这个线程。
//创建初始化3个线程的线程池
private ExecutorService threadPool = Executors.newFixedThreadPool(3);
//创建3个CyclicBarrier对象,执行完后执行当前类的run方法
private CyclicBarrier cb = new CyclicBarrier(3, this);
//保存每个学生的平均成绩
private ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

private void count() {
    for (int i = 0; i < 3; i++) {
        threadPool.execute(() -> {
            //计算每个学生的平均成绩,代码略()假设为60~100的随机数
            int score = (int) (Math.random() * 40 + 60);
            try {
                Thread.sleep(Math.round(Math.random() * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            map.put(Thread.currentThread().getName(), score);
            System.out.println(Thread.currentThread().getName() + "同学的平均成绩为" + score);
            try {
                //执行完运行await(),等待所有学生平均成绩都计算完毕
                cb.await();
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        });
    }
    threadPool.shutdown();
}

@Override
public void run() {
    int result = 0;
    Set<String> set = map.keySet();
    for (String s : set) {
        result += map.get(s);
    }
    System.out.println("三人平均成绩为:" + (result / 3) + "分");
}

public static void main(String[] args) throws InterruptedException {
    long now = System.currentTimeMillis();
    CyclicBarrier1 cb = new CyclicBarrier1();
    cb.count();
    Thread.sleep(100);
    long end = System.currentTimeMillis();
    System.out.println(end - now);
}

运行结果

在这里插入图片描述

CountDownLatch与CyclicBarrier区别

​ CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset()
方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。
​ CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。比如以下代码执行完之后会返回true。
​ CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程。
​ 某线程中断CyclicBarrier会抛出异常,避免了所有线程无限等待。

Semaphore信号量

Semaphore(信号量),是JUC包下的一个工具类,我们可以通过其限制执行的线程数量,达到限流的效果。

当一个线程执行时先通过其方法进行获取许可操作,获取到许可的线程继续执行业务逻辑,当线程执行完成后进行释放许可操作,未获取达到许可的线程进行等待或者直接结束。

基础面试题总结_第4张图片

public class SemaphoreTest {

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(2);
        for (int i = 0; i <= 10; i ++) {
            MyRunnable runnable = new MyRunnable(semaphore);
            Thread thread = new Thread(runnable, "Thread-" + i);
            thread.start();
        }
    }
    private static class MyRunnable implements Runnable {
        // 成员属性 Semaphore对象
        private final Semaphore semaphore;
        public MyRunnable(Semaphore semaphore) {
            this.semaphore = semaphore;
        }
        public void run() {
            String threadName = Thread.currentThread().getName();
            // 获取许可
            boolean acquire = semaphore.tryAcquire();
            // 未获取到许可 结束
            if (!acquire) {
                System.out.println("线程【" + threadName + "】未获取到许可,结束");
                return;
            }
            // 获取到许可
            try {
                System.out.println("线程【" + threadName + "】获取到许可");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 释放许可
                semaphore.release();
                System.out.println("线程【" + threadName + "】释放许可");
            }
        }
    }

}

运行结果:
基础面试题总结_第5张图片

Exchanger原理

​ Exchanger用于线程间进行通信、数据交换。Exchanger提供了一个同步点exchange方法,两个线程调用exchange方法时,无论调用时间先后,两个线程会互相等到线程到达exchange方法调用点,此时两个线程可以交换数据,将本线程产出数据传递给对方。

import java.util.concurrent.Exchanger;

public class ExchangerTester {

	// Exchanger实例.
	private static final Exchanger<String> exchanger = new Exchanger<String>();
	
	public static void main(String[] args) {
		// 模拟阻塞线程.
		new Thread(() -> {
			try {
				String wares = "红烧肉";
				System.out.println(Thread.currentThread().getName() + "商品方正在等待金钱方,使用货物兑换为金钱.");
				Thread.sleep(2000);
				String money = exchanger.exchange(wares);
				System.out.println(Thread.currentThread().getName() + "商品方使用商品兑换了" + money);
			} catch (InterruptedException ex) {
				ex.printStackTrace();
			}
		}).start();
		// 模拟阻塞线程.
		new Thread(() -> {
			try {
				String money = "人民币";
				System.out.println(Thread.currentThread().getName() + "金钱方正在等待商品方,使用金钱购买食物.");
				Thread.sleep(4000);
				String wares = exchanger.exchange(money);
				System.out.println(Thread.currentThread().getName() + "金钱方使用金钱购买了" + wares);
			} catch (InterruptedException ex) {
				ex.printStackTrace();
			}
		}).start();
	}

}

输出结果

Thread-0商品方正在等待金钱方,使用货物兑换为金钱.
Thread-1金钱方正在等待商品方,使用金钱购买食物.
Thread-0商品方使用商品兑换了人民币
Thread-1金钱方使用金钱购买了红烧肉
ThreadLocal原理

​ ThreadLocal与线程同步机制不同,线程同步机制是多个线程共享同一个变量,对这个共享变量的修改,通过无锁或者有锁的机制保证线程的安全,而ThreadLocal是为每一个线程,创建一个只属于它自己的变量副本,线程可以改变自己所拥有的变量副本,而不会影响其他线程所对应的副本,简而言之,往ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,一个线程使用自己的局部变量比使用全局变量方便且安全,因为局部变量只有线程自己能看见,不会影响其他线程

基础面试题总结_第6张图片

​ 在使用过程中需要注意内存泄漏的问题,Thread强引用着ThreadLocalMap, ThreadLocalMap弱引用着ThreadLocal。因此如果长时间使用ThreadLocal而未释放数据,将出现内存泄漏的问题,在使用过后需要执行ThreadLocal.remove()方法释放变量,或者手动赋值为null。

线程池的实现原理

1、线程在有任务的时候会创建核心的线程数corePoolSize

2、当线程满了(有任务但是线程被使用完)不会立即扩容,而是放到阻塞队列中,当阻塞队列满了之后才会继续创建线程。

3、如果队列满了,线程数达到最大线程数则会执行拒绝策略。

4、当线程数大于核心线程数事,超过KeepAliveTime(闲置时间),线程会被回收,最终会保持corePoolSize个线程。

线程池的实现方式
1、newSingleThreadExecutor()

池里只有一条线程,如果线程因为异常而停止,会自动新建一个线程补充

2、newFixedThreadPool()

创建一个核心线程数跟最大线程数相同的线程池,线程池数量大小不变,如果有任务放入队列,等待空闲线程。

3、newCachedThreadPool()

线程池是创建一个核心线程数为0,最大线程为Inter.MAX_VALUE的线程池,线程池数量不确定,有空闲线程则优先使用,没有则创建新的线程处理任务,处理完放入线程池。

4、newScheduledThreadPool()

创建一个没有最大线程数限制的可以定时执行线程池,还有创建一个只有单个线程的可以定时执行线程池(Executors.newSingleThreadScheduledExecutor())

七个核心参数

corePoolSize: 线程池核心线程个数

workQueue:用于保存等待执行任务的阻塞队列

maximunPoolSize: 线程池最大线程数量

ThreadFactory: 创建线程的工厂

RejectedExecutionHandler: 队列满,并且线程达到最大线程数量的时候,对新任务的处理策略

keeyAliveTime: 空闲线程存活时间

TimeUnit: 存活时间单位

锁机制

volatile实现原理

对于用volatile修饰的变量,对其写操作时会额外执行一个Lock指令,这个Lock指令的作用如下

1、将该处理器的工作内存写回到主内存:即保证主内存中的值是最新的,为其他处理器读取到最新的值提供基础

2、使其他处理器的工作内存失效:如果其他处理器的工作内容仍然有效,则不会从主内存中重新取值,所以需要先使其他处理器的工作内存失效,然后让其从主内存中重新读取

volatile与synchronized的对比

1、volatile轻量级,只能修饰变量;synchronized重量级,可以修饰代码块和方法

2、volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞;synchronized不仅保证可见性,而且还保证原子性,因为只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象时会出现阻塞

synchronized实现原理

synchronized锁的是对象头,而对象头在jvm加载对象信息的时候已经加载,对象头中包含mark work、class word、数组长度,其中mark workh中有标志是记录偏向锁的,synchronized就是通过更改状态值来达到线程同步的目的

synchronized与lock区别

synchronized是Java的一个关键字,是jvm级别的锁,而lock是java.util.concurrent包里的一个接口,其下有很多实现类(ReentrantLock),

synchronized作用于方法或代码块,生命周期区别于锁的对象的生命周期,而lock由lock()和unlock()方法确定同步的代码块,生命周期由lock决定,且lock更加灵活,lock还提供了非阻塞竞争锁的方法trylock(),由返回的布尔值确定当前线程是否获取到了锁,synchronized无法实现这一种方式,synchronized提供偏向锁来完成对锁的控制和性能优化,而lock提供自旋锁来完成性能的优化。·

tryLock和lock和lockInterruptibly的区别?

lock 方法是 Lock 接口中最基础的获取锁的方法,当有可用锁时直接得到锁并立即返回,当没有可用锁时会一直等待,直到获取到锁为止

lockInterruptibly 方法和 lock 方法类似,当有可用锁时会直接得到锁并立即返回,如果没有可用锁会一直等待直到获取锁,但和 lock 方法不同,lockInterruptibly 方法在等待获取时,如果遇到线程中断会放弃获取锁。

使用无参的 tryLock 方法会尝试获取锁,并立即返回获取锁的结果(true 或 false),如果有可用锁返回 true,并得到此锁,如果没有可用锁会立即返回 false

有参数的 tryLock(long,TimeUnit) 方法需要设置两个参数,第一个参数是 long 类型的超时时间,第二个参数是对参数一的时间类型描述(比如第一参数是 3,那么它究竟是 3 秒还是 3 分钟,是第二个参数说了算的)。在这段时间内如果获取到可用的锁了就返回 true,如果在定义的时间内,没有得到锁就会返回 false

CAS乐观锁

CAS是比较并交换由Unsafe类的compareAndSwap方法提供,能保证修改变量操作的原子性。底层是 lock cmpxchg 指令(X86 架构),当执行到 lock 的指令时, 会把总线锁住,当cpu执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,又借助 了volatile保证了多个线程对内存操作的准确性,是原子的。(总线是计算机底层传送信息的公共通信干线,分为地址总线、数据总线等)

CAS必须借助 volatile才能保证原子性。CAS主要包含三个参数(偏移量表示要更新的变量在内存中的位置,pre旧值表示根据之前获得的情况预期可能获得的值,next表示更改后的新值), 如果运行该方法的时候当前线程获取到的值和prev相同,就把该变量修改为next,如果该方法成功修改返回true,没有修改成功,运行方法时的变量值和之前获取到的变量值prev不一致,说明有别的线程修改了变量,返回false。失败之后不会进入阻塞,而是会修改预期值再次尝试。

ABA问题

所谓ABA问题,就是比较并交换的循环,存在一个时间差,而这个时间差可能带来意想不到的问题。比如线程1和线程2同时也从内存取出A,线程T1将值从A改为B,然后又从B改为A。线程T2看到的最终值还是A,经过与预估值的比较,二者相等,可以更新,此时尽管线程T2的CAS操作成功,但不代表就没有问题。有的需求,比如CAS,只注重头和尾的一致,只要首尾一致就接受。但是有的需求,还看重过程,中间不能发生任何修改,这就引出了AtomicReference原子引用

AtomicInteger对整数进行原子操作,AtomicLong对长整型数进行原子操作,AtomicBoolean对布尔型数进行原子操作,但实际上这些是完全不够的,如果是一个POJO呢?可以用AtomicReference来包装这个POJO,使其操作原子化Class AtomicReference < V >,Value就是我们需要进行原子包装的泛型类。

@Getter
@ToString
@AllArgsConstructor
class User {
    String userName;
    int age;
}

public class AtomicRefrenceDemo {
    public static void main(String[] args) {
        User z3 = new User("张三", 22);
        User l4 = new User("李四", 23);
        AtomicReference<User> atomicReference = new AtomicReference<>();
        atomicReference.set(z3);
        System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
        System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
    }
}
乐观锁业务场景及实现方式

每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。

AQS(AbstractQueuedSynchronizer)

AQS( AbstractQueuedSynchronizer )是一个用来构建锁和同步器(所谓同步,是指线程之间的通信、协作)的框架,Lock 包中的各种锁(如常见的 ReentrantLock, ReadWriteLock), concurrent 包中的各种同步器(如 CountDownLatch, Semaphore, CyclicBarrier)都是基于 AQS 来构建

死锁的形成条件

1、互斥:每个资源只能被一个进程使用

2、请求于保持:一个进程请求资源时阻塞,对已持有的资源不进行释放

3、不剥夺:进程已获得的资源在使用完以前不能被强行剥夺

4、循环等待:若干进程之间形成一种头尾相接的循环等待资源关系;

开发中如何避免死锁

1、避免多次锁定,主现场对资源进行了锁定,自线程就不必对资源再进行锁定

2、多个线程具有相同的加锁顺序

3、使用定时锁

4、死锁检测

redis

redis的常用数据结构
1.字符串string

字符串类型是Redis中最基本的数据存储类型,它是一个由字节组成的序列,在Rediss中是二进制安全的。这意味着该类型可以接受任何格式数据,如JPEG图像数据和Json对象说明信息。它是标准的key-value,通常用于存储字符串、整数和浮点。Value可容纳高达512MB的数据。应用程序场景:非常常见的场景用于计算站点访问量、当前在线人数等。incr命令(++操作)

2.散列hash

在Memcached中,我们经常将一些结构化的信息打包成hashmap,在客户端序列化后存储为一个字符串的值,比如用户的昵称、年龄、性别、积分等,这时候在需要修改其中某一项时,通常需要将所有值取出反序列化后,修改某一项的值,再序列化存储回去。这样不仅增大了开销,也不适用于一些可能并发操作的场合(比如两个并发的操作都需要修改积分)。而Redis的Hash结构可以使你像在数据库中Update一个属性一样只修改某一项属性值。应用程序方案:存储部分更改数据,如用户信息、会话共享。

3.列表list

Redis的列表允许用户从序列的两端推入或者弹出元素,列表由多个字符串值组成的有序可重复的序列,是链表结构,所以向列表两端添加元素的时间复杂度为0(1),获取越接近两端的元素速度就越快。这意味着,即使有数以千万计的元素列表,也可以极快地获得10条记录在头部或尾部。可列入名单的要素最多只有4294967295个。应用场景:(1)最新消息排行榜。(2)消息队列,以完成多程序之间的消息交换。可以用push操作将任务存在list中(生产者),然后线程在用pop操作将任务取出进行执行。(消费者)

4.集合sets

所谓集合就是一堆不重复值的组合,并且是没有顺序的。在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis还提供了诸如collection、union和differences等操作,使得实现诸如commandism、poperhike、secondfriends这样的功能变得很容易,或者选择是将结果返回给客户机,还是将它们保存到使用不同命令的新的集合中。

5.有序集合zset

Redis zset和set一样也是string类型元素的集合,且不允许重复的成员。不同之处在于,每个元素都与双类型的分数相关联。Redis使用分数将集合的成员从小到大排序。zset的成员是唯一的,但是grazentra的分数可以重复。sorted set是插入有序的,即自动排序。常用命令:zadd、zrange、zrem、zcard等。当你需要一个有序的并且不重复的集合列表时,那么可以选择sorted set数据结构。应用举例:例如存储全班同学的成绩,其集合value可以是同学的学号,而score就可以是成绩。(2)排行榜应用,根据得分列出topN的用户等。

redis持久化机制
RDB(在指定的时间间隔内生成数据集的时间点快照)

1.RDB 是一个非常紧凑的文件

它保存了 Redis 在某个时间点上的数据集。 这种文件非常适合用于进行备份: 比如说,你可以在最近的 24 小时内,每小时备份一次 RDB 文件,并且在每个月的每一天,也备份一个 RDB 文件。 这样的话,即使遇上问题,也可以随时将数据集还原到不同的版本。

2.RDB 非常适用于灾难恢复

它只有一个文件,并且内容都非常紧凑,可以(在加密后)将它传送到别的数据中心,或者亚马逊 S3 中。

3.RDB 可以最大化 Redis 的性能

父进程在保存 RDB 文件时唯一要做的就是 fork 出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。

4.RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

RDB 的缺点

如果你需要尽量避免在服务器故障时丢失数据,那么 RDB 不适合你。 虽然 Redis 允许你设置不同的保存点来控制保存 RDB 文件的频率, 但是, 因为RDB 文件需要保存整个数据集的状态, 所以它并不是一个轻松的操作。 因此你可能会至少 5 分钟才保存一次 RDB 文件。 在这种情况下, 一旦发生故障停机, 你就可能会丢失好几分钟的数据。

每次保存 RDB 的时候,Redis 都要 fork() 出一个子进程,并由子进程来进行实际的持久化工作。 在数据集比较庞大时, fork() 可能会非常耗时,造成服务器在某某毫秒内停止处理客户端; 如果数据集非常巨大,并且 CPU 时间非常紧张的话,那么这种停止时间甚至可能会长达整整一秒。 虽然 AOF 重写也需要进行 fork() ,但无论 AOF 重写的执行间隔有多长,数据的耐久性都不会有任何损失。

AOF

AOF 的优点

使用 AOF 持久化会让 Redis 变得非常耐久:你可以设置不同的 fsync 策略,比如无 fsync ,每秒钟一次 fsync ,或者每次执行写入命令时 fsync 。 AOF 的默认策略为每秒钟 fsync 一次,在这种配置下,Redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据( fsync 会在后台线程执行,所以主线程可以继续努力地处理命令请求)。

AOF 文件是一个只进行追加操作的日志文件, 因此对 AOF 文件的写入不需要进行 seek , 即使日志因为某些原因而包含了未写入完整的命令(比如写入时磁盘已满,写入中途停机,等等), redis-check-aof 工具也可以轻易地修复这种问题。

Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。

AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析也很轻松。 导出 AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。

AOF 的缺点

对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。

根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间。

AOF 在过去曾经发生过这样的 bug : 因为个别命令的原因,导致 AOF 文件在重新载入时,无法将数据集恢复成保存时的原样。 (举个例子,阻塞命令 BRPOPLPUSH 就曾经引起过这样的 bug 。) 测试套件里为这种情况添加了测试: 它们会自动生成随机的、复杂的数据集, 并通过重新载入这些数据来确保一切正常。 虽然这种 bug 在 AOF 文件中并不常见, 但是对比来说, RDB 几乎是不可能出现这种 bug 的。

Redis分布式锁
  • 多个客户端,通过watch一个键-值,然后开启事务
  • 如果在开启事务的期间,watch的值没有被其他客户端修改过,则执行成功
  • 如果在开启事务的期间,watch的值被其他客户端修改了,则执行失败
  • 使用redission工具类来实现分布式锁

在这里插入图片描述

Redis 队列和MQ 对比
  1. Redis没有相应的机制保证消息的消费,当消费者消费失败的时候,消费体丢失,需要手动处理。MQ:具有消息消费确认,即使消费者消费失败,也会自动使消息体返回原队列,同时可全程持久化,保证消息体被正确消费
  2. Redis采用主从模式,读写分离,但是故障转移还没有非常完善的官方解决方案;MQ集群采用磁盘、内存节点,任意单点故障都不会影响整个队列的操作
  3. 将整个Redis实例持久化到磁盘,MQ的队列、消息,都可以选择是否持久化
  4. Redis的特点是轻量级,高并发,延迟敏感,用于即使数据分析、秒杀计数器、缓存等,MQ的特点是重量级,高并发,用于异步、批量数据异步处理、并发任务串行化,高负载任务的负载均衡等
redis数据同步

主从模式:从节点发送psync命令至主节点,主节点生成RDB文件,并同时将接收到的客户端写命令缓存,RDB文件有主节点向从节点传输完成后(如果中途从节点断开链接,会从上次传输完成的地方开始传输),从节点先将数据写入磁盘,再加载到内存中,然后主节点将缓存的写命令发送给从节点,从节点再同步数据

微服务

RPC框架

RPC,全称为Remote Procedure Call,即远程过程调用,它是一个计算机通信协议。它允许像调用本地服务一样调用远程服务。它可以有不同的实现方式。如RMI(远程方法调用)、Hessian、Http invoker等。另外,RPC是与语言无关的。

RPC架构分为三部分:

1)服务提供者,运行在服务器端,提供服务接口定义与服务实现类。

2)服务中心,运行在服务器端,负责将本地服务发布成远程服务,管理远程服务,提供给服务消费者使用。

3)服务消费者,运行在客户端,通过远程代理对象调用远程服务。

事务的传播机制

多个事务方法互相调用时,事务在方法间的传播,spring事务是基于数据库事务和AOP机制实现的,在新开启一个事务时,其实是新开了一个数据库连接,在该链接上再执行该方法所需要执行的sql。

传播方式 外部没有事务 外部有事务 适用场景
REQUIRED(默认) 创建新的事务 加入外部事务 当前方法必须在事务中运行
SUPPORTS 使用非事务方式 加入外部事务 当前方法不必在事务中运行
MANDATORY 抛出异常 加入外部事务 当前方法必须在调用者事务中运行
REQUIRES_NEW() 创建新的事务 创建新的事务 当前方法必须在事务中运行
NOT_SUPPORTED() 以非事务的方式运行 刮起调用者的事务,以非事务的方式运行 当前方法不支持在事务中运行
NEVER() 以非事务的方式运行 抛出异常 调用者必须以非事务方式运行
NESTED() 开启新的事务 以“嵌套事务”形式加入外部事务 当前方法必须在事务中运行
分布式事务锁

①分布式环境下,一个方法在同一时间只能被一个机器的一个线程执行

②高可用的获取锁和释放锁

③高性能的获取锁和释放锁

④具备可重入特性

⑤具备锁失效机制,防止死锁

分布式锁的三种实现:

A. 基于数据库实现分布式锁;

B. 基于缓存(Redis等)实现分布式锁;

C. 基于Zookeeper实现分布式锁

A.基于数据库的实现:

在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就是用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁

B.基于缓存(Redis等)实现分布式锁:

推荐: Redis有很高的性能;

​ Redis命令对此支持较好,实现起来比较方便

实现:

(1)获取锁的时候,使用setnx加锁,并使用expire 命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。

(2)获取锁的时候设置一个获取的超时时间,若超过这个时间就放弃获取锁。

(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,就执行delete进行锁释放。

C.基于Zookeeper的实现方式

原因: Zookeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。

实现:

  1. 创建一个目录mylock
  2. 线程A想获取锁就在mylock目录下创建临时顺序节点
  3. 获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
  4. 线程B获取所有节点,判断自己是不是最小的节点,设置监听比自己次小的节点
  5. 线程A处理完,删除自己的节点,线程B监听到便跟事件,判断自己是不是最小的节点,如果是则获得锁。
  6. 使用Apache的开源库Curator,它是一个Zookeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。

Mysql

ACID

原子性(Atomicity):一个事务是一个不可分割的工作单位,事务中包括的动作要么都做要么都不做。
一致性(Consistency):事务必须保证数据库从一个一致性状态变到另一个一致性状态,一致性和原子性是密切相关的。
隔离性(Isolation):一个事务的执行不能被其它事务干扰,即一个事务内部的操作及使用的数据对并发的其它事务是隔离的,并发执行的各个事务之间不能互相打扰。
持久性(Durability):持久性也称为永久性,指一个事务一旦提交,它对数据库中数据的改变就是永久性的,后面的其它操作和故障都不应该对其有任何影响。

索引失效的原因
mysql索引使用注意事项

《1》只涉及到其中一个字段时,都能使用到索引

《2》模糊查询时,%如果在前面,那么不会使用索引

《3》涉及到多个索引字段时,如果这些索引字段中,存在主键索引,那么只会使用主索引

《4》涉及到多个索引字段时,如果这些索引字段中,不存在主键索引的话,那么就会使用该使用的索引(注:如果通过其中的部分索引就能准确定位的话,那么其余的索引就不再被使用)

《5》当对索引字段进行 >, <,>=, <=,not in,between …… and ……,函数(索引字段)时不会使用该索引

mysql的日志文件种类
  • 重做日志(redo log)

    确保事务的持久性。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。

  • 回滚日志(undo log)

    保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读

  • 二进制日志(binlog)

    用于复制,在主从复制中,从库利用主库上的binlog进行重播,实现主从同步。用于数据库的基于时间点的还原。

  • 错误日志(errorlog)
  • 慢查询日志(slow query log)
  • 一般查询日志(general log)
  • 中继日志(relay log)。
索引是什么

在关系数据库中,索引是一种单独的、物理的对数据库表中一列或多列的值进行排序的一种存储结构,它是某个表中一列或若干列值的集合和相应的指向表中物理标识这些值的数据页的逻辑指针清单。索引的作用相当于图书的目录,可以根据目录中的页码快速找到所需的内容。在操作系统中可以减少磁盘IO,大大的节省了查找数据的时间。

数据库索引原理

MyISAM引擎使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址。可以看出MyISAM的索引文件仅仅保存数据记录的地址。在MyISAM中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复

虽然InnoDB也使用B+Tree作为索引结构,但具体实现方式却与MyISAM截然不同。

第一个重大区别是InnoDB的数据文件本身就是索引文件。从上文知道,MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。而在InnoDB中,表数据文件本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。

为什么使用B+树

**B树:**data为数据的磁盘地址,还可能是整条列的数据

1、数据是分散在每个结点上的,所有结点元素不重复,

2、结点元素从左到右递增

3、叶子结点具有相同的深度,叶子结点的指针为空

B+树:data为数据的磁盘地址,还可能是整条列的数据

1、叶子结点包含了所有的索引字段,以及数据的磁盘地址,而非叶子结点不在存储data数据,作用只是便于查找的冗余索引

2、非叶子结点是从子结点选取一页的开头来作为自己的值,指针为子结点那页的地址

3、每一个结点里的值都是排好序的

4、叶子结点之间还有指针可以互相访问,这样方便了范围查找,比如where col > 10,这是mysql对B+的变种,也是对比B树的一个优势

5、由于data可能会很大,非叶子结点在不存储data后,非叶子可以存储的元素则会变多,还可以降低树的高度,提高了查询的效率,这是与B树对比,B+树的一个优势

事务隔离级别
一、读未提交

如果一个事务已经开始写数据,则另外一个事务不允许同时进行写操作,但允许其他事务读此行数据,该隔离级别可以通过“排他写锁”,但是不排斥读线程实现。这样就避免了更新丢失,却可能出现脏读,也就是说事务B读取到了事务A未提交的数据

二、读提交

如果是一个读事务(线程),则允许其他事务读写,如果是写事务将会禁止其他事务访问该行数据,该隔离级别避免了脏读,但是可能出现不可重复读。事务A事先读取了数据,事务B紧接着更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。

三、可重复读取

可重复读取是指在一个事务内,多次读同一个数据,在这个事务还没结束时,其他事务不能访问该数据(包括了读写),这样就可以在同一个事务内两次读到的数据是一样的,因此称为是可重复读隔离级别,读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务(包括了读写),这样避免了不可重复读和脏读,但是有时可能会出现幻读。(读取数据的事务)可以通过“共享读镜”和“排他写锁”实现。

四、可序化

提供严格的事务隔离,它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行,如果仅仅通过“行级锁”是无法实现序列化的,必须通过其他机制保证新插入的数据不会被执行查询操作的事务访问到。序列化是最高的事务隔离级别,同时代价也是最高的,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻读

事务锁

Innodb中的锁
表锁和行锁

表锁

Innodb有两种内部使用的意向锁(Intention Locks),都是表锁。

表锁分成三种:

意向共享锁(IS):」

事务计划给数据行加行共享锁,加共享锁之前必先获取该锁

意向排他锁(IX):」

事务打算给数据行加行排他锁,加排他锁之前必先获取该锁

自增锁(AUTO-INC Locks):」

特殊表锁,自增长计数器通过该“锁”来获得自增长计数器最大的计数值。

行锁

共享锁(S)和排它锁(X)。

「共享锁(S):」

多个事务可以一起读,共享锁之间不互斥,共享锁会阻塞排它锁。

「排他锁(X):」

允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。

RabbitMQ

RabbitMq有哪些组件
  1. ConnectionFactory(连接管理器):应用程序与 rabbit 之间建立连接的管理器,程序代码中使用;
  2. Channel(信道):消息推送使用的通道;
  3. Exchange(交换器):用于接受、分配消息;
  4. Queue(队列):用于存储生产者的消息;
  5. RoutingKey(路由键):用于把生成者的数据分配到交换器上;
  6. BindingKey(绑定键):用于把交换器的消息绑定到队列上。
vhost的作用

相当于一个虚拟的主机,可以隔离不同的应用,拥有自己的权限、用户、交换机、队列、绑定关系等,可以避免不同应用间通道和交换机命名冲突的问题产生,默认虚拟主机是“/”

RabbitMq的部署模式

1、单机模式

2、普通集群模式:

​ 提高系统吞吐量,多节点服务于一个queue的读写操作

3、镜像模式:

​ 每个节点都同步数据,同步的数据内容包含“元数据”和queue里的消息,这样即使服务宕机后任保证服务的可用性,但是也存在无法线性扩展的问题,服务器性能不足时,无法通过扩从服务器数量来达到减轻负载的效果,因为增加的服务器还是会同步之前的所有数据,而不是分担。

RabbitMq广播模式
  1. fanout:所有 bind 到此 exchange 的 queue 都可以接收消息;很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout 交换机转发消息是最快的。
  2. topic:所有符合 routingKey 所 bind 的 queue 可以接收消息。
  3. direct:通过 routingKey 和 exchange 中的 bindingKey 决定的那个唯一的 queue 可以接收消息;
如何保证消息不被重复消费

保证消息的幂等性,即请求多少次都是同样的结果,在插入消息前,先查询消息是否存在,如存在则更新消息内容,通过主键或唯一约束即可完成,

在消息生产时,MQ 内部针对每条生产者发送的消息生成一个 inner-msg-id,作为去重的依据(消息投递失败并重传),避免重复的消息进入队列;在消息消费时,要求消息体中必须要有一个 bizId(对于同一业务全局唯一,如支付 ID、订单 ID、帖子 ID 等)作为去重的依据,避免同一条消息被重复消费。

如何保证消息的可靠性(消息丢失问题)

生产者:

1、开启rabbitMq事务(不推荐)
2、将信道设置成 confirm 模式(发送方确认模式)
生产者确认机制

所有在信道上发布的消息都会被指派一个唯一的 ID,一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一 ID)。如果 RabbitMQ 发生内部错误从而导致消息丢失,会发送一条 nack(notacknowledged,未确认)消息。发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。

消费者确认机制

消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ 才能安全地把消息从队列中删除。

这里并没有用到超时机制,RabbitMQ 仅通过 Consumer 的连接中断来确认是否需要重新发送消息。也就是说,只要连接不中断,RabbitMQ 给了 Consumer 足够长的时间来处理消息。保证数据的最终一致性;

该模式在网络因素下可能存在消息重复消费,其次是操作失误导致消费者接受不到消息

1)如果消费者接收到消息,在确认之前断开了连接或取消订阅,RabbitMQ 会认为消息没有被分发,然后重新分发给下一个订阅的消费者。(可能存在消息重复消费的隐患,需要去重)

2)如果消费者接收到消息却没有确认消息,连接也未断开,则 RabbitMQ 认为该消费者繁忙,将不会给该消费者分发更多的消息。

如何保证消息的顺序性

拆分多个queue,每个queue一个consumer,或是一个queue,一个consumer,但是consumer需要在内存中对消息进行排序后在发送给不同的worker处理。

大量消息在 MQ 里长时间积压,该如何解决?
  1. 先修复 consumer 的问题,确保其恢复消费速度,然后将现有 consumer 都停掉;
  2. 新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量;
  3. 然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue;
  4. 接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据;
  5. 等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。
消息丢失了怎么办

RabbitMQ可以设置消息的过期时间(TTL),在queue中积压一定时间后的消息会被RabbitMq直接清理掉,这时候可以可以采用批量重导的方式。大量消息积压导致consumer处理不及时,消息被RabbitMq清理,等高峰期过后再把消息查出来重新灌入mq进行处理。

消息持久化

消息持久化,当然前提是队列必须持久化

RabbitMQ 确保持久性消息能从服务器重启中恢复的方式是,将它们写入磁盘上的一个持久化日志文件,当发布一条持久性消息到持久交换器上时,Rabbit 会在消息提交到日志文件后才发送响应。一旦消费者从持久队列中消费了一条持久化消息,RabbitMQ 会在持久化日志中把这条消息标记为等待垃圾收集。如果持久化消息在被消费之前 RabbitMQ 重启,那么 Rabbit 会自动重建交换器和队列(以及绑定),并重新发布持久化日志文件中的消息到合适的队列。

你可能感兴趣的:(java,开发语言)