Java面试题

Java面试题

1、Java基础

1. 三大特性
封装:将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法来实现隐藏信息的操作和访问;
继承:子类拥有父类的所有属性和方法(除了private修饰的属性),从而实现代码的复用;
多态:使用父类引用接受不同的子类的对象实例,父类引用调用相同的方法,根据子类不同的实例,产生不同结果。(多态三要素:继承、重写、父类引用指向子类对象)
2. String、StringBuffer、StringBuilder区别
String类是有final修饰,所以值不可变;StringBuffer和StringBuilder的是可变。
运行速度:StringBuilder>StringBuffer>String;StringBuffer线程是安全的(有synchronized修饰)适合多线程;StringBuilder线程不安全适合单线程;
如果不需要频繁修改值则用String;如果要操作少量数据用String;多线程操作字符串缓冲区下操作大量数据用StringBuffer,单线程操作字符串缓冲区大量数据用StringBuilder;
3. 两个对象的hashCode()相同,则equals()是否也一定相同?
equals与hashCode关系:如果两个对象调用equals比较返回true,那么它们的hashCode值一定相同;如果两个对象的hashCode相同,它们并不一定相同。
hashCode方法主要是用来提升对象比较的效率,先进性hashCode比较,如果不相同,则不必进行equals比较,这样就大大减少了equals的比较次数,当比较的数量很大的时候能提升效率。
之所以重写equals要重写hashCode,是为了保证equals方法返回true的情况下hashCode值也要一致,如果重写了equals没有重写hashCode,就会出现两个对象相等但hashCode不相等的情况。
4.equals和==区别
对于基本数据类型,== 比较的是他们的值。基本数据类型没有equals方法。
对于复合数据类型,== 比较的是它们存放地址(是否是同一个对象)。equals默认比较地址值,重写的话按照重写逻辑比较。
1.== 判断两个变量或实例是不是指向同一个内存空间,equals判断两个变量或实例所指向的内存空间的值是不是相同。
2.== 指对内存地址进行比较,equals是对字符串的内容进行比较。
3.== 引用是否相同,equals指的是值是否相同。
5.final、finally、finalize区别
final用于修饰属性、方法和类,分别表示属性不能被重新赋值,方法不可被覆盖,类不可被继承。
finally是异常处理语句结构的一部分,一般以try- catch- finally出现,finally代码块表示总是被执行。
finalize是Object类的一个方法,该方法一般由垃圾回收器来调用,当我们调用System.gc()方法的时候,由垃圾回收器调用finalize()方法,回收垃圾,JVM并不保证此方法被调用。
6.重载和重写
重载:发生在同一个类中,方法名必须相同、参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
重写:发生在父子类,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符大于等于父类;如果父类方法访问修饰符为private则子类不能重写。
7.接口和抽象类
1.实现:抽象类的子类使用extends来继承;接口必须使用implements来实现接口;
2.构造函数:抽象类可以有构造函数;接口不能;
3.main方法:抽象类可以有main方法,并且我们能运行它;接口不能有main方法。
4.实现数量:类可以实现多个接口;但是只能继承一个抽象类;
5.访问修饰符:接口中的方法默认使用public修饰;抽象类中的方法可以是任意访问修饰符;
8.Java1.8新特性
简介:1.速度更快;2.代码更少(Lambda表达式);3.强大的Stream API;4.便于并行;5.最大化减少空指针异常Optional;

1.Lambda表达式:是一个匿名函数,我们可以把Lambda表达式理解为一段可以传递的代码。
可以写出更加简洁灵活的代码。作为一种更加紧凑的代码风格,使Java的语言表达能力得到了提升。
public class Demo {
    public static void main(String[] args) {
        //匿名内部类方法
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("匿名内部类方法");
            }
        };
        //Lambda表达式
        Runnable runnable1 = () -> System.out.println("Lambda表达式");

        runnable.run();
        runnable1.run();
    }
}
2.函数式接口:只包含一个抽象方法的接口,称为函数式接口
可以通过Lambda表达式来创建该接口的对象(若Lambda表达式抛出一个受检异常,那么该异常需要在目标接口的抽象方法上进行声明)
可以在任意函数式接口上使用@FunctionalInterface注解,这样可以检查它是否是一个函数式接口,同时Javac也会包含一条声明,说明这个接口是一个函数式接口。

java内置的四大核心函数式接口:

函数式接口参数类型返回类型用途
Consumer 消费型接口Tvoid对类型为T的对象进行操作,包含方法:void accept(T t)
Supplier 供给型接口T返回类型为T的对象,包含方法:T get()
Function<T,R> 函数型接口TR对类型为T的对象进行操作,并返回结果。结果R类型的对象,包含方法:R apply(T t)
Predicate 断言型接口Tboolean确定类型为T的对象是否满足某约束,并返回boolean值

3.方法引用与构造器引用
1.方法引用:当要传递给Lambda体的操作,已经有实现方法,可以使用方法引用。方法引用:使用操作符“::”将方法名和对象或类的名字分隔开来。
有如下三种情况:
对象::实例方法
类::静态方法
类::实例方法

//二次函数式接口
BinaryOperator<Double> b = (x,y) -> Math.pow(x,y);
//等同
BinaryOperator<Double> b2 = Math::pow;

2.构造器引用 格式:ClassName::new

Function<Integer,MyClass> c = (n) -> new MyClass(n);
Function<Integer,MyClass> c2 = MyClass::new ;
Function<Integer,Integer[]> a = (n) -> new Integer[n];
Function<Integer,Integer[]> a2 = Integer[]::new;

4.Stream API
Stream是Java8中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤映射数据等操作。使用Stream API对集合数据进行操作,就类似于使用SQL执行对数据库查询。也可以使用Stream API来并行操作。简而言之,Stream API提供了一种高效且易于使用处理数据方式。
注意:
1.Stream自己不会存储元素;
2.Stream不会改变源对象。相反他们会返回一个持有结果的新Stream;
3.Stream操作时延迟执行的,这意味着他们会等到需要的结果时候才会执行。
Stream操作的三个步骤
1.创建Stream:一个数据源(如:集合、数组)获取一个流
2.中间操作:一个中间操作链,对数据源的数据进行处理
3.终止操作(终端操作):一个终止操作,执行中间操作链,并产生结果

5.新时间日期API
6.接口中的默认方法与静态方法
7.Optional类
是一个容器类,代表一个值存在或不存在,原来null表示一个值不存在,现在Optional可以更好的表达这个概念。并且可以避免空指针异常。
常用方法:
Optional.of(T t):创建一个Optional实例
Optional.empty():创建一个空的Optional实例
Optional.ofNullable(T t):若t不为空创建一个Optional实例否则创建一个空实例;
isPresent():判断是否包含值;
orElse(T t):如果调用对象包含值,返回该值,否则返回t;
orElseGet(Supplier s):如果调用对象包含值,返回该值,否则返回s获取的值;
map(Function f):如果有值处理,并返回处理后的Optional,否则返回Optional.empty();
flatMap(Function mapper):与map类似,要求返回值必须是Optional;
9.IO流
1.字节流和字符流
字节流的操作不会经过缓冲区(内存)而是直接操作文本本身,而字符流的操作先经过缓冲区然后通过缓冲区再操作文件以字节为单位输入输出数据,字节流8位数据传输(任何数据类型、照片、文件、音乐视频),字符流16位传输(一般char数据)
2.IO里面常见类,字节流、字符流、接口、实现类、方法阻塞
输入流(从外部文件输入到内存);输出流(从内存输出到文件);
字节流有抽象类InputStream和OutputStream,它们的子类FileInputStream、FileOutputStream、bufferedOutputStream等;字符流BufferReader和Writer等。都实现了Closeable、Flushable、Appendable这些接口。
程序中的输入输出都是以流的形式保存的,流中保存的实际上全部都是字节文件。
Java中的阻塞方法是指查询调用该方法时,必须等待输入数据可用或者检查到输入结束或者抛出异常,否则程序会一直停留在该语句上,不会执行下面的语句。比如read()和readLine()方法。
3.NIO和传统IO区别
NIO是同步非阻塞,核心类:1.Buffer为所有的原始类提供(Buffer)缓存支持;2.Charset字符集编码解码解决方案;Channel一个新的原始I/O抽象,用于读写Buffer类型,通道可以认为是一种连接,可以提供到特定设备、程序或者网络的连接。

1、传统IO一般是一个线程等待连接,连接过来之后分配给processor线程,processor线程与通道连接后如果通道没有数据过来则会阻塞(线程被动挂起)。
NIO则不同,首先在selector线程轮询的过程中已经过滤掉不感兴趣的事件,其次在processor处理感兴趣的事件read和write都是非阻塞操作即直接返回的,线程没有被挂起。
2、传统IO的管道是单向的,NIO的管道是双向的。
3、两者都是同步的,也就是Java程序亲力亲为的去读写数据,不管IO还是NIO都需要read和write方法,这些都是Java程序调用而不是系统帮我们调用的。
NIO2.0里这点得到改观,即使用异步阻塞Asynchronous XXX四个类来处理。

4、BIO、NIO、AIO区别
同步:Java自己去处理IO;异步:Java将IO交给操作系统处理,告诉缓冲区大小,处理完成回调。阻塞:使用阻塞IO时,Java调用会一直阻塞到读写完才返回;非阻塞:使用非阻塞IO时,如果不能立马读写,Java调用马上返回,当IO事件分发器通知可读写时进行读写,不断循环直到读写完成。
BIO:同步阻塞,服务器的实现模式是一个连接一个线程,这样的模式很明显的一个缺陷是:由于客户端连接服务器线程数成正比关系,可能造成不必要的线程开销,严重的还会导致服务器内存溢出。当然这种情况可以通过线程池机制改善,但并不能从本质上消除这个弊端。
NIO:同步非阻塞,在JDK1.4之前,Java的IO模式一直都是BIO。1.4后引入NIO,而服务器的实现模式是多个请求一个线程,即请求会组册到多路复用器selector上,多路复用器轮询到连接有IO请求时才启动一个线程处理。
AIO:异步非阻塞,JDK1.7发布了NIO2.0,服务器的实现模式为多个有效请求一个线程,客户端的IO请求都是有OS先完成再通知服务器应用器启动线程处理(回调)。
应用场景:并发连接数不多时采用BIO,因为它编程和调试都非常简单,如果设计高并发的情况,应选择NIO或AIO,更好的建议时采用成熟的网络通讯框架Netty。

10.多线程
1.Java实现线程方式
1.继承Thread类实现多线程
2.实现Runnable接口实现多线程
3.使用ExecutorService、Callable、Future实现有返回结果的多线程
2.线程状态
在Java当中,线程通常有五种状态:创建、运行、阻塞、等待、终止

状态含义
NEW新建状态,没有调用start方法之前的状态
RUNNABLE运行状态(running执行中,ready就绪(等待CPU时间片))
BLOCKED阻塞状态
WAITING等待状态,没有明确的等待结束,调用wait方法
TIME_WAITING超时等待状态,有明确的等待时间,如sleep()
TERMINATED终止状态

3,run和start区别
start:作用是启动一个新线程,当用start开始一个线程后,线程就进入就绪状态,使线程所代表的虚拟处理于可运行状态,这意味着它可以由JVM调度并执行。但是并不意味着线程就会立即运行,只有当CPU分配时间片时,这个线程获得时间片时,才开始执行run方法,start不能被重复调用,它调用run方法,run方法是必须重写的。
run:和普通的成员方法一样,可以被重复调用。如果直接调用run方法,并不会启动新线程。程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样没有达到多线程的目的。调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程执行。

public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        });
        t.start();
    }
//输出结果
Thread-0

=============================

public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        });
        t.run();
    }
    
//输出结果
main

4.wait和sleep区别
1.原理不同。sleep是Thread类的静态方法,是线程用来控制自身流程的。它会使此线程暂停执行指定时间,等待计时时间到时,此线程会自动苏醒。而wait是Object类的方法,用于线程间的通信,这个方法会使当前拥有对象锁的进程等待,直到其他线程调用notify/notifyAll才会醒来。
2.对锁的处理机制不同。由于sleep方法的主要作用是让线程休眠指定的一段时间自动恢复,不涉及线程间的通信,因此调用sleep方法不会释放锁。而wait方法则不同,调用wait方法后,线程会释放掉它所占用的锁,从而使线程所在的对象的其他synchronized数据可被其他线程使用。
3.使用区域不同。由于wait方法的特殊意义,所以它必须放在同步控制方法或者同步语句块使用,而sleep则可以放在任何地方使用。
4.sleep方法必须捕获异常,而wait、notify/noitfyAll不需要捕捉异常。在sleep的过程中,有可能被其他对象调用它的interrupt(),产生Interrupt Exception异常。由于sleep不会释放“锁标志”,容易导致死锁问题的发生,所以一般情况下,不推荐使用sleep方法,而是推荐wait方法。

5.Volatile关键字
1.线程的可见性:当一个线程修改一个“共享变量”时,另一个线程能读到这个修改的值。
2.顺序一致性:禁止指令重排序。

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。
源代码-->1.编译器优化重排序-->2.指令级并行重排序-->3.内存系统重排序-->最终执行的指令排序

1.属于编译器重排序,2和3属于处理器重排序。这些重排序可能导致很多线程出现内存可见性问题。
当变量声明为volatile时,Java编译器在生成指令排序时,会插入内存屏障指令。通过内存屏障指令来禁止重排序。

JMM内存屏障插入策略如下:
在每个volatile写操作的前面插入一个StoreStore屏障,后面插入StoreLoad屏障。
在这里插入图片描述

在每个volatile读操作后面插入一个LoadLoad、LoadStored屏障。
在这里插入图片描述
6.volatile与synchronized比较
1.volatile是轻量级的synchronized,因为它不会引起上下文的切换和调度,所以volatile性能更好。
2.volatile只能修饰变量,synchronized可以修饰方法、静态方法、代码块。
3.volatile对任意单个变量的读写具有原子性,但是类似于i++这种复合操作不具有原子性。而锁的互斥执行的特性可以确保对整个临界区代码执行具有原子性。
4。多线程访问volatile不会发生阻塞,而synchronized会发生阻塞。
5.volatile是变量在多线程之间的可见性,synchronized是多线程之间访问资源的同步性。

11.Java集合
在这里插入图片描述
在这里插入图片描述
1、集合底层数据结构
Collection集合

  • List
    1.ArrayList集合底层采用了”数组“数据结构,非线程安全;
    2.LinkedList集合底层采用“双向链表”的数据结构;
    3.Vector集合底层采用了“数组”数据结构,线程安全,所有方法都有synchronized关键字修饰,比较安全但效率低,所以使用率低;

  • Set
    1.HashSet集合在new的时候,底层实际上new了一个HashMap集合。向HashSet集合中存储元素,实际上是存储到HashMap集合中,HaspMap集合是一个哈希表数据结构;
    2.TreeSet集合实际是TreeMap,new TreeSet集合的时候,底层实际上new了一个TreeMap集合,和上面同理。采用了二叉树数据结构;

Map集合和Collection集合没有关系,存储方式不同,所有Map集合key元素无序不可重复。

Map集合

  • HashMap结合底层是哈希表数据结构,非线程安全;
  • HashTable底层是哈希表数据结构,线程安全,所有的方法都有synchronized关键字,效率比较低,使用比较少;

2、LinkedList和ArrayList的区别
1.数据结构不同
ArrayList是Array(动态数组)的数据结构;LinkedList是Link(链表)数据结构;
2.效率不同
当随机访问List(get/set操作)时,ArrayList比LinkedList的效率更高,因为LinkedList是线性的数据存储方法,所以需要移动指针从前往后依次查找;
当对数据进行增加或删除的操作(add/remove)时,LinkedList比ArrayList效率更高,因为ArrayList是数组,所以在其中进行增删操作时,会对操作点之后所有数据的下标索引造成影响,需要进行数据的移动。
3.自由性不同
ArrayList自由性比较低,因为它需要手动的设置固定大小的容量,但是它的使用比较方便,只需要创建,然后添加数据,通过调用下标进行使用;而LinkedList自由性比较高,能够动态的随数据量的变化而变化,但是它不便于使用;
4.主要控件开销不同
ArrayList主要控件开销在于需要在List列表预留一定空间;而Linked主要控件开销在于需要存储结点信息以及结点指针信息;

3、HashMap和Hashtable区别

  1. 线程是否安全:HashMap是非线程安全的,Hashtable是线程安全的,因为Hashtable内部的方法基本都经过synchronized修饰;
  2. 效率:因为线程安全的问题,HashMap比hashtable效率高一点。另外Hashtable基本被淘汰,不要在代码中使用它;
  3. 对Null key 和 Null value的支持:HashMap可以存储null的key和value,但是null作为key只能有一个,null作为value可以有多个;Hashtable不允许null的key和value,否则抛出NullPointerException;
  4. 初始容量大小和每次扩充容量大小的不同:
    创建时如果不指定容量初始值,HashMap默认初始化大小为16,每次扩充容量变为原来的2倍;Hashtable默认的厨师大小为11,之后每次扩充,容量变为原来的2n+1;

4、HashMap
JDK1.8之前HashMap底层是“数组+链表”也就是 链表散列
JDK1.8之后在解决哈希冲突时有了比较大的变化,当链表长度大于阀值(默认8)(将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转换为红黑树,减少搜索时间
(转红黑树条件:链表大于8且数组长度大于64)

5、ConcurrentHashMap和Hashtable区别

  • 底层数据结构:JDK1.7的ConcurrentHashMap底层采用分段的”数组+链表“ 实现,JDK1.8采用的数据结构跟HashMap 1.8的结构一样,数组+链表/红黑二叉树。
    Hashtable和JDK1.8之前的hashMap的底层数据结构类型“数组+链表”,数组时HashMap的主体,链表则是主要为了解决哈希冲突而存在的。
  • 实现线程安全的方式:1.在JDK1.7的时候,ConcurrentHashMap(分段锁)对整个桶数组进行分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。JDK1.8的时候已经摒弃(Segment)概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronized和CAS来操作。
    2.Hashtable(同一把锁):使用synchronized来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如果使用put添加元素,另一个线程不能使用put添加元素,也不能使用get,竞争会越来越激烈效率越低。

6、Array和ArrayList区别
Array可以存储基本数据类型和对象,存储的数据都是固定大小;
ArrayList只能存储对象,存储的数据大小可以自动扩展;
7、Collection和Collections区别
Collection提供了对集合对象进行基本操作的通用接口方法,其直接继承接口有List与Set;
Collections时工具类,主要功能用于对集合中元素进行排序、搜索以及线程安全等;

12.IOC、AOP
1、AOP面向切面编程
如果在多个类中出现重复代码,那么我们可以定义一个共同的抽象类,把共同的代码抽取到抽象类中,比如Teacher、Student都有username,那么就可以把username及相关的get、set方法抽取出来,这就是纵向抽取。
例子:SQL
1、获取连接对象
2、执行SQL(核心业务代码)
3、如果有异常则回滚事物,否则提交事物;
4、关闭连接
AOP术语

  • 连接点(Joinpoint):程序执行的某个特定位置,如果某个方法调用前、后,方法抛出异常后,这些代码中的特点称为连接点。(就是在哪加入你的逻辑增强)
  • 切点(PointCut):每个程序的连接点有多个,如何定位到某个感兴趣的连接点,就需要通过切点来定位(连接点->数据库的记录;切点->查询条件)
  • 增强(Advice):增强是织入到目标类连接点上到一段代码。
    前置通知(before):在执行业务代码前做些操作,比如获取连接对象;
    后置通知(after):在执行业务代码后做些操作,无论是否发生异常,它都会执行,比如关闭连接;
    异常通知(afterThrowing):在执行业务代码后出现异常,会执行的操作;
    返回通知(afterReturning):在执行业务代码后无异常,会执行的操作;
    环绕通知(around):
  • 目标对象(Target):需要被加强的业务对象;
  • 织入(Weaving):织入就是将增强添加到对目标类具体连接点上的过程;
  • 代理类(Proxy):一个类被AOP织入增强后,就产生一个代理类;
  • 切面(Aspect):切面由切点和增强组成,它既包括了横切逻辑的定义,也包括了连接点的定义,Spring AOP就是将切面定义的横切逻辑织入到切面所制定的连接点中。
  • 在这里插入图片描述
    1.AOP编程可不是Spring独有的,Spring只是支持AOP编程的框架之一,这点非常重要切勿搞反关系;
    2.AOP分两类,一类可以对方法的参数进行拦截,一类是对方法进行拦截;

2、IOC控制反转
是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(DI),还有一种方式叫做“依赖查找”。通过控制反转,对象在被创建的时候由一个调控系统(IOC容器),将其所依赖的对象的引用传递给它。

3、Spring循环依赖处理
A依赖B,B依赖A(A中有个属性B,B中有个属性A)
1、A创建的时候先初始化,初始化的时候通过ObjectFactory提前曝光,并且一般都是将这个曝光的半成品放到三级缓存里面,如何发现自己有个B的属性,就尝试get(B),发现B还没有被创建。
2、这个时候B就会进行创建,创建的过程发现有个属性A,就会尝试get(A),这个时候A已经在缓存里面了,通过ObjectFactory.getObject方法拿到这个A的半成品,进行创建。
3、B创建成功后放到一级缓存,然后A再从这里拿到B完成创建。

13.设计模式

2、数据库

1、索引
索引:排好序的快速查找数据结构!加速查询速度
缺点:
空间方面:每次创建索引,会自动生成一个索引文件,比较占用内存;
时间方面:当增加、删除数据时,索引文件需要更新;
1、为什么加了索引能够提高效率?

  • 索引底层实现时B+树。B+树类似折半查找,比如如果10亿条数据,依次能砍到一半;
  • 数据量越多,索引的作用越大;

2、索引类别

  • 主键索引:它是一种特殊的唯一索引,不允许有空值;
  • 普通索引:既一个索引值包含单个列,一个表可以有多个单例索引;它是最基本的索引,没有任何限制;普通索引允许索引的数据列包含重复的值;
  • 唯一索引:与“普通索引”类似,不同的就是“索引列的值必须唯一,但允许有空值”;
  • 联合索引:一个索引包含多个列,专门用于组合搜索,其效率大于索引合并;
  • 全文索引:仅可用于MyISAM表,针对比较大的数据,生成全文索引很耗时和空间;Innodb不要用全文索引;

3、聚簇索引与非聚簇索引
(聚簇索引和非聚簇索引是建立在B+树的基础上)
聚簇索引:key为主键,value为其余列的数据。一个表只能有一个聚簇索引;
非聚簇索引:除了聚簇索引外都叫非聚簇索引;

4、Mysql的最左前缀原则

2、事务ACID
原子性(Atomicity):单个事务,为了一个不可分割的最小工作单元,整个事务中的所有的操作要么全部成功,要么全部失败,对于一个事务来说不可能只执行其中的一部分SQL操作,这就是事物的原子性;

一致性(Consistency):数据库总是从一个一致性的状态转换到另一个一致性的状态。(总额不变)

隔离性(Isolation):一个事务所做的修改在最终提交以前,对于其他事务是不可见的

持久性(Durability):一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。

3、数据库调优、分库分表、数据迁移方案
1、索引「索引最左匹配原则」:在模糊查询中,如果出现左模糊和全模糊,则不会命中索引(既左边有%时,不命中索引);

在联合索引中,左边的字段命中索引,假设Index(id,username,iphone)的联合索引(如果查询条件只出现了左边的字段一样可以命中索引,但是如果左边的字段没有出现,却出现了右边的字段则没有命中索引)

//名中索引
SELECT * FROM user WHERE id='001' AND username='tom' AND iphone='110'//名中索引
SELECT * FROM user WHERE id='001' AND username='tom'//名中索引
SELECT * FROM user WHERE id='001'//未命中索引
SELECT * FROM user WHERE username='tom' AND iphone='110'//未命中索引
SELECT * FROM user WHERE id='001' AND iphone='110'//未命中索引
SELECT * FROM user WHERE iphone='110'

2、在where子句对进行(运算操作:+ - / …)和or、使用 NOT IN
3、exists和in:exists适合外表小而内表大的情况;in适合外表大而内表小的情况;
4、分库分表:当数据库数据达到千万级别开始,索引的作用越来越差,为了提高效率,拆分数据的方法

  1. 水平分表:将表A拆分成A1,A2,A3…字段都一样,将数据以一定规律均匀分布在多个表中;
  2. 垂直分表:有些表字段很多,比较长的字段拆分出来,做成一个新表,“大表拆小表”,以便开发与维护;

「分表说到底还是在一个数据库中操作,没有解决数据库连接不够的问题,数据库瓶颈依然存在,因此我们进行分库操作」

  1. 水平分库:将不同类型的表放在不同的数据库(用户相关的表放在用户数据库,考试相关的表放在考试数据库…)
  2. 垂直分库 :就是根据业务耦合性,将关联度低的不同表存储在不同的数据库。做法与大系统拆分多个小系统类似,按业务分类进行独立划分“与微服务治理”做法类似

垂直切分
优点

  • 解决业务系统层面的耦合性,业务清晰;
  • 与微服务的治理类似,也能对不同业务的数据进行分级管理、维护、监控、扩展等;
  • 高并发场景下,垂直切分一定程度的提升IO、数据库连接数、单机硬件资源的瓶颈;

缺点

  • 部分表无法join,只能通过接口聚合方式解决,提升了开发的复杂度;
  • 分布式事务处理复杂;
  • 依然存在单表数据量过大的问题(需要水平切分);

水平切分
优点

  • 不存在单库数据量过大、高并发的性能瓶颈,提升系统稳定性和负载能力;
  • 应用端改造比较小,不需要拆分业务模块;

缺点

  • 跨分片的事务一致性难以保证;
  • 跨库的join关联查询性能较差;
  • 数据多次扩展难度和维护量极大;

水平切分后同一张表会出现在多个数据库/表中,每个库/表的内容不同。几种典型的数据分片规则为:
1、根据数值范围
按照时间区间或ID区间来切分。“在某种意义上,某些系统中使用的「冷热数据分离」,将一些使用比较少的历史数据迁移到其他库中,业务功能上只提供热点数据的查询,也是类似的实践。”
优点
- 单表大小可控
- 天然便于水平扩展,后期如果想对整个分片集群扩展时,只需要添加节点即可,无需对其他分片的数据进行迁移
- 使用分片字段进行范围查找时,连续分片可快速定位分片进行快速查询,有效避免跨分片查询的问题
缺点
- 热点数据成为性能瓶颈。连续分片可能存在的数据热点。例如按时间字段分片,有些分片存储最近时间段内的数据,可能会被频繁的读写,而有些存储的历史数据则很少被读写;

2、根据数值取模
一般采用hash取模mod的切分方式。例如Customer表根据cusno字段切分到4个库,余数为0到放到第一个库,余数为1的放到第二个库,以此类推。这样同一个用户的数据分散到同一个库中,如果查询条件带有cuson字段,则可明确定位到相应库去查询。
优点
- 数据分片相对比较均匀,不容易出现热点和并发访问的瓶颈
缺点
- 后期分片集群扩容时,需要迁移旧的数据(使用一致性hash算法能较好的避免这个问题);
- 容易面临跨分片查询的复杂问题。比如如果频繁用到的查询条件中不带cusno时,将会导致无法定位数据库,从而需要同时向4个数据库发起查询,再在内存中合并数据,取最小集返回给应用,分库反而成为拖累。

一、分库分表带来的问题
分库分表能有效的缓解单机和单库带来的性能瓶颈和压力,突破网络IO、硬件资源、连接数的瓶颈。同时也带来一些问题。
1、事务一致性问题
分布式事务
当更新内容同时分布在不同库中,不可避免会带来跨库事务问题。跨分片事务也是分布式事务,没有简单的方案,一般可用“XA协议”和“两阶段提交”处理。
分布式事务能最大限度保证了数据库操作的原子性。但在提交事务时需要协调多个节点,推后了提交事务的时间点,延长了事务的执行时间。导致事务在访问共享资源时发生冲突或死锁的概率增高。随着数据库节点的增多,这种趋势越来越严重,从而成为系统在数据库层面水平扩展的枷锁。
最终一致性
对于那些性能要求很高,但对一致性要求不高的系统,只要在允许的时间段内达到最终一致性即可,可采用事务补偿的方式。与事务在执行中发生错误回滚的方式不同,事务补偿是一种事后检查补救的措施,一些常见的实现方法有:对数据进行对账检查,基于日志进行对比,定期同标准数据来源进行同步等等。事务补偿还要结合业务系统考虑。

2、跨节点关联查询join问题
切分之前,系统中很多列表和详情页所需要的数据可以通过join来完成。切分后,数据可能分布在不同的节点上,此时join带来的问题比较麻烦,考虑到性能,尽量避免使用join查询。

解决这个问题的一些方法:
1)全局表
全局表也可以看做是“数据字典表”,就是系统中所有模块都可能依赖的一些表,为了避免跨库join查询,可以将这类表在每个数据库中都保存一份。这些数据通常很少会进行修改,所以不必担心一致性的问题。
2)字段冗余
一种经典的反范式设计,利用空间换时间。为了性能而避免join查询,比如:订单表保存userID时候,也将userName冗余保存一份,这样查询订单详情时就不需要再去查询“买家user表”了。
3)数据组装
在系统层面,分两次查询,第一次查询的结果集中找出关联数据ID,然后根据ID发起第二次请求得到关联数据,最后将获取到的数据进行关键字拼接。
4)RE分片
关系型数据库中,如果可以先确定表之间的关联关系,并将那些存在关联关系的表记录存放在同一个分片上,那么就能比较好的避免跨分片join问题。

3、跨节点分页、排序、函数问题
跨节点多库进行查询时,会出现limit分页、order by排序等问题。需要“现在不同的分片节点中将数据进行排序并返回,然后将不同分片返回的结果集进行汇总和再次排序,最终返回给用户”
这样随着页数越大,系统性能越差。
在这里插入图片描述

方案:”在使用Max、Min、Sum、Count之类的函数进行计算的时候,也需要在每个分片上执行相应的函数,然后将各个分片结果集进行汇总和再次计算,最终将结果返回“
在这里插入图片描述

4、全局主键避重问题
1)UUID
2)结合数据库维护主键ID表
3)Snowflake分布式自增ID算法

数据库迁移方法

  1. 将表结构和数据,直接导出成sql,去目标库中执行
    要求:如果源库和目标库类型不同,就需要做一些调整变更,否则一旦表多了后,整个处理流程比较麻烦
    做法:需要停掉源库,保证在数据库同步过程中的数据一致性;
  2. 使用备份/还原
    要求:源库和目标库是同构数据库,即同种类型的数据库(mysql–>mysql)
  3. 使用开源etl工具:比如kettle
    要求:由于是全量备份,数据量会很大,一般需要比较长的备份时长,备份过程可能会出现数据库报错或者连接数不足等异常,且insert/update会占用大量的CPU资源
    做法:一次性or实时增量
  4. 使用代码工具实现,可以是数据级别的,也可以考虑更底层的。比如mysql走binlog,sqlserver走cdc
  5. 使用云数据库(阿里云DTS、华为云DRS(数据复制服务))

4、数据库高可用场景

  1. 主从半同步复制
    - 全同步复制:master写入binlog后强制同步日志到从库,所有从库执行完成后才返回给客户端
    - 半同步复制:从库写入日志成功后返回ACK确认给主库,主库收到至少一个从库确认就认为写操作完成;

    	- 优点:架构、部署比较简单、主机宕机直接切换即可
    	- 缺点:完全依赖半同步复制,半同步复制退化为异步复制,无法保证数据一致性
    ***注意:半同步复制机制是可靠的,可以保证数据一致性。但是如果网络发生波动,半同步复制发生超时会切换为异步复制,异步复制是无法保证数据一致性的*** 
    
  2. 双通道复制:针对半同步复制方案(可以在半同步复制基础上优化,尽可能保证半同步复制)
    - 优点:这种方案架构、部署也是比较简单,主机宕机也是直接切换即可,比主从半同步复制更能保证数据一致性
    - 缺点:需要修改内核源码或者使用mysql通信协议,没有从根本上解决数据一致性问题。

  3. 数据库集群-主从集群
    - 优点:保证了整个系统的高可用性,扩展性也比较好,可以扩展为大规模集群
    - 缺点:数据一致性仍然依赖原生的mysql半同步复制

  4. 共享存储:(实现了数据服务器和存储设备的解藕,不同数据库之间的数据同步不再依赖于mysql的原生复制功能,而是通过磁盘数据同步的手段来保证数据一致性)
    - 优点:部署简单、价格合适,保证数据的强一致性
    - 缺点:对IO性能影响比较大,从库不提供读操作

  5. pxc强一致性方案:(分布式协议可以很好解决数据一致性问题,常见的部署方案就是Mysql cluster,它就是官方集群的部署方案,通过使用NDB存储引擎实时备份冗余数据,实现数据库的高可用性和数据一致性)
    - 优点:不依赖于第三方软件,可以实现数据的强一致性;
    - 缺点:配置比较复杂,需要使用NDB存储引擎,至少三节点;

6、mysql引擎
常用引擎分别:MyISAM、InnoDB、Memory、CSV
MyISAM是mysql5.58版本之前默认引擎,在5.5.8之后默认存储引擎InnoDB。

  1. InnoDB引擎
    特点:
    - 支持事务
    - 默认使用行级锁
    - 5.7版本后支持全文检索与空间函数

Mysql锁

职责分类粒度分类
共享锁-读锁行级锁
独占/排他锁-写锁表级锁

行级锁和表级锁在并发情况下,行级锁远优于表锁,这也是MyISAM被淘汰的原因(表锁)

在InnoDB中,只有利用索引的更新,删除操作才会使用行级锁。不能使用索引的写操作则是表级锁

  1. MyISAM
    特点
    - 不支持事务
    - 支持全文检索,支持text前缀检索
    - 支持数据压缩
    - 紧密存储,顺序读性能好
    - 表级锁

    	应用场景
    	   1. 非事务应用,例如保存日志
    	   2. 只读类引用,报表数据、字段数据
    	   3. 系统临时表,SQL查询,分组的临时表引擎
    
  2. Memory
    特点
    - 不支持事务
    - 内存读写,临时存储
    - 读写效率高
    - 表级锁

     应用场景
     	1. 读多写少的静态数据
     	2. 充当缓存使用,保存高频访问静态资源
     	3. 系统临时表
    
  3. CSV
    特点:
    - 纯文件表村
    - 不支持事务
    - 不支持索引

7、脏写、脏读、不可重复读和幻读

脏写
事务B修改了事务A修改过的值,但是此时事务A还没有提交,所以事务A随时会回滚,导致事务B修改的值也没有了在这里插入图片描述
在这里插入图片描述

脏读
无论是脏写还是脏读,都是因为一个事务去更新或查询了另一个还没有提交事务的更新过的数据。因为另一个事务还没有提交,所以它随时可能会回滚,那必然导致跟新的数据没有,或者查询的数据没有。这就是脏写和脏读情况

在这里插入图片描述

在这里插入图片描述
不可重复读
事务A重复查询结果不一样

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
幻读
事务A第一次条件查询SQL结果有10条,然后事务B插入了几条数据。事务A第二次查询SQL结果跟第一次结果不一致。

在这里插入图片描述

在这里插入图片描述

对于脏写、脏读、不可重复读、幻读都是因为业务系统多线程并发执行,每个线程可能都会开启一个事务,每个事务都会执行增删改查操作。然后数据库会并发执行多个事务,多个事务可能会并发地对缓存页里的同一批数据进行增删改查操作,于是并发这个并发增删改查同一批数据的问题。(数据库才设计事务隔离机制、MVCC多版本隔离之际、锁机制,用一整套机制解决多事务并发问题)

在这里插入图片描述
在这里插入图片描述
1)Read uncommitted (读未提交):最低级别,任何情况都无法保证。

2)Read committed (读已提交):可避免脏读的发生。

3)Repeatable read (可重复读):可避免脏读、不可重复读的发生。【默认】

4)Serializable (串行化):可避免脏读、不可重复读、幻读的发生。

8、数据库分页

  1. Mysql
    - 语法:limit n (获取查询结果前n条数据)
    - 语法:limit n,m (n是开始下标,m是获取多少条数据;n=(第几页-1)*每页数量,m=每页数量)
    在分页中下标是从0开始的
  2. Oracle
    - 只能用rownum

Mybatis- Plus 实现分页是 new Page(index,size)

9、Mysql锁
主要分为:表锁、行锁、页面锁。
MyISAM只支持表锁,InnoDB不仅支持行锁,在一定程度也支持表锁。
按照行为可以分为:共享锁(读锁)、排他锁(写锁)和意向锁。按照思想分为乐观锁和悲观锁;

悲观锁 比较消极的一种锁处理方式。直接在操作数据时,抢占锁。其他的事务在进行时就会等待,知道占有锁的事务释放锁为止。

  • 适用场景:比较适合「写入操作比较频繁的场景」,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁开销,降低了系统的吞吐量。

乐观锁认为数据一般情况下不会造成冲突,只有当数据执行修改情况时,才会针对数据冲突做处理。

  • 适用场景:比较适合「写入操作比较少的场景」,即冲突真的很少发生时候,这样可以省去了锁的开销,加大了系统的吞吐量。

悲观锁比较适合强一致性的场景,但效率比较低,特别是读的并发低。乐观锁则适合读多写少,并发冲突少的场景

10、Oracle与Mysql区别

  1. 本质区别
    • Mysql是一个开源的关系数据库管理系统(免费)
    • Oracle是一个对象关系数据库管理系统(收费)
  2. 数据库安全性
    • Mysql适用三个参数验证用户,即用户名、密码和位置
    • Oracle使用了许多安全功能,如用户、密码、配置文件、本地身份证验证等等
  3. SQL语法区别
    • Oracle的sql语法与Mysql有很大不同。Oracle称为PL/SQL的编程语言提供了更大的灵活性,相比Mysql更多命令,用于生产报表输出和变量定义。
  4. 对事务的提交
    • Mysql默认自动提交
    • Oracle默认不自动提交,需要用户手动提交,需要写commit指令或者点commit按钮
  5. 存储上的区别
    • 与Oracle相比,Mysql没有表空间、角色管理、快照,同义词和包以及自动存储管理
  6. 分页查询
    • Mysql是直接在SQL语句写“limit”实现分页
    • Oracle则是需要用到伪列rownum和嵌套查询
  7. 字符数据类型比较
    • Mysql具有char(255字节)和varchar(65535字节)
    • oracle支持四种字符类型,char、nchar、varchar2、nvarchar2,四种字符类型需要至少1个字节长度,(char和nchar 2000字节、nvarchar2和varchar2 4000字节)
  8. 事务隔离级别
    • Mysql是repeatable read的隔离级别
      1)Read uncommitted (读未提交):最低级别,任何情况都无法保证。
      2)Read committed (读已提交):可避免脏读的发生。
      3)Repeatable read (可重复读):可避免脏读、不可重复读的发生。【默认】
      4)Serializable (串行化):可避免脏读、不可重复读、幻读的发生。
    • Oracle是read commit的隔离级别
      1)Read committed (读已提交):可避免脏读的发生。【默认】
      2)Serializable (串行化):可避免脏读、不可重复读、幻读的发生。
      注:因为Oracle有undo,它天生就是读写不阻塞,因此在Oracle里,根本就不会出 现脏读。
  9. 备份类型
    • Mysql有mysql dump 和mysql hotcopy 备份工具
    • Oracle提供不同类型的备份工具,如冷备份、热备份、导出、导入、数据泵。
  10. 字符串的模糊比较
    • Mysql用like ‘%字符串%’
    • Oracle也可以用like ‘%字符串%’ 但这种方法不能使用索引,速度不快,用字符串比较函数instr(字段名,‘字符串’)>0 会得到更精确的查找结果
  11. 性能诊断
    • Mysql的诊断调优方法较少,主要有慢查询日志
    • Oracle有各种成熟的性能诊断调优工具,能实现很多自动分析、诊断功能。awr、addm、sqltrace、tkproof等
  12. 日期字段的处理转换
    • Mysql中有2种日期格式DATE和TIME
    • Oracle只有一种DATE
  13. 空字符的处理
    • Mysql的非空字段也有空的内容
    • Oracle定义非空字段不允许有空内容

3、Mybatis

一、#{}和 $ {}区别
1. #{}预编译处理(防止SQL注入);$ {}字符串String替换
2.#{}解析String类型的数据会自动加上引号,其他数据类型不会;$ {}解释什么就是什么,不做处理

二、当实体类中的属性名和表中字段名不一样
1、别名方式

<select id=”selectorder” parametertype=”int” resultetype=”me.gacl.domain.order”> 
       select order_id id, order_no orderno ,order_price price form orders where order_id=#{id}; 
    </select> 

2、< resultMap >

<select id="getOrder" parameterType="int" resultMap="orderresultmap">
        select * from orders where order_id=#{id}
    </select>
   <resultMap type=”me.gacl.domain.order” id=”orderresultmap”> 
        <!–用id属性来映射主键字段–> 
        <id property=”id” column=”order_id”> 
        <!–用result属性来映射非主键字段,property为实体类属性名,column为数据表中的属性–> 
        <result property = “orderno” column =”order_no”/> 
        <result property=”price” column=”order_price” /> 
    </reslutMap>

三、分页
1.使用limit来实现分页
2.PageHelper实现分页
1)导入依赖
2)全局配置文件中配置插件

分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数

四、Mybatis与Hibernate区别

  • Mybatis:学习门槛低,简单易学,程序员直接编写原生态SQL,可严格控制sql执行性能,灵活度高,非常适合对关系数据库模型要求不高的软件开发
  • Hibernate对象/关系映射能力强,数据库无关系好,对于关系模型要求高的软件(需求固定的制定化软件)如果使用Hibernate开发可节省很多代码,提高效率。但是学习门槛高。

五、缓存
Mybatis中有一级缓存和二级缓存。一级缓存又被称为本地缓存,是Seesion会话级别的,一级缓存是Mybatis内部实现的一个特点,用户不能配置,默认情况下一级缓存是开启的,而不是关闭的。二级缓存是针对mapper进行的缓存,它的生命周期很长,跟Application生命周期一样,也就是说它的作用范围是整个Application应用。

一级缓存的原理:
在这里插入图片描述
工作机制:一般而言,一个SqlSession对象会使用一个Executor对象来完成会话操作,Executor对象会维护一个Cache缓存,以提高查询性能。

步骤:
1.第一次发出查询SQL语句,查询结果写入sqlsession的一级缓存中,缓存使用的数据结构是一个map (key:MapperID+offset+limit+sql+传入的参数;value:用户信息)
2.同一个sqlsession再次发生相同的sql语句,就从缓存中取出数据
3.如果两次中间出现commit操作(修改、添加、删除),close(),clearCache()等方法时,这次sqlsession中的一级缓存区域全部清空,下次再去缓存中查询不到,所以要从数据库查询再写入缓存中。

二级缓存
SqlSessionFactory层面上的耳机缓存默认时不开启的,二级缓存需要进行配置,实现二级缓存的时候,Mybatis要求返回pojo必须可序列化的,也就是要求实现Serializable接口。
1.Mybatis全局配置中启用二级缓存配置
2.在对应的Mapper.xml中配置cache节点
3.在对应对select查询节点中添加useCache=true

在这里插入图片描述
步骤:
1.二级缓存的范围是mapper级别(mapper同一个命名空间),mapper以命名空间为单位创建缓存数据结构,结构是map
2.mybatis的二级缓存是通过CacheExecutor实现的,CacheExecutor其实是Executor的代理对象,所有的查询操作在CacheExecutor中先匹配是否存在,不存在则查询数据库

第三方缓存库
在这里插入图片描述

4、redis

NoSQL
1.泛指非关系型的数据库,作为关系型数据库的补充
2.不支持SQL语法
3.存储结构跟传统关系型数据库完全不同,nosql存储数据都是key:value形式
4.每种nosql都有自己的API和语法,以及擅长的业务场景
5.种类:redis、Mongodb、memcache等
6.作用:应对基本海量用户和海量数据高并发处理问题

NoSQL和SQL数据库比较

  • 适用场景不同:SQL数据库适合用于关系特别复杂的数据库查询场景,NoSQL反之
  • 事务特性的支持:SQL对事务支持并且非常完善,NoSQL基本不支持事务
  • 两者在不断的取长补短,呈现融合趋势

redis特性

  • 支持数据等持久化,可以将内存中的数据保存到磁盘中,重启的时候可以再次加载进行使用
  • 不仅仅支持简单的key-value类型的数据,同时还提供list、set、zset、hash等数据结构等存储
  • 支持数据等备份,即master- slave模式等数据备份
  • 内部采用单线程工作

应用场景

  • 做缓存
  • 时效性信息控制:session共享、购物车、验证码等
  • 热点数据加速查询:热点商品、热点新闻、热点资讯、推广类高访问量信息
  • 任务队列:秒杀、抢购、购票排队
  • 即时信息查询:排行榜、网站访问统计、公交车到站、点赞评论数量、在线人数
  • 分布式数据共享:分布式集群框架等session共享
  • 消息队列
  • 分布式锁

一、数据类型
1、字符串String:
字符串类型是redis中最基础的数据存储类型,它在redis中是二进制安全的,这便意味着该类型可以接受任何格式的数据
应用场景:投票、微博粉丝数、微博数量、热门商品和热点新闻(设置时效性)设置数据具有指定的生命周期

2、哈希Hash:列表的元素类型为String,对象类型的存储,如果具有比较频繁的更新需求操作,则用String类型比较笨重
应用场景:购物车(添加、删除、清空等)

3、列表List:存储多个数据,并对数据进入存储空间的顺序进行区分
应用场景:微信朋友圈点赞(按照顺序显示点赞好友信息,如果取消点赞,移除对于好友信息)、微博的好友列表

4、集合Set:存储大量的数据,在查询方面高效,无序集合,元素String类型,唯一性不重复
应用场景:统计网站访问量,服务次数,软件上面的兴趣爱好标签

5、有序集合Zset:数据排序有利于数据的有效展示,需要提供一种可以根据自身特征进行排序的方式,唯一性不重复,元素String
应用场景:排行榜(各类资源网站TOP10:音乐、电影、游戏等);游戏友好亲密度;热点直播间

二、AOF、RDB
1、RDB
指定时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后再替换之前的文件,用二进制压缩存储。

在这里插入图片描述
优点:使用单独子进程来进行持久化,主进程不会进行任何IO操作,保证了redis的高可用。RDB文件紧凑,全量备份,非常适合用于进行备份和灾难恢复。
缺点:RDB是间隔一段时间进行持久化,如果持久化之间redis发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候。

对于RDB来说,提供了三种机制去触发持久化,分别:自动触发、save触发、bgsave触发
自动触发修改redis.config配置文件
save触发该命令会阻塞当前redis服务,执行save命令期间,redis不能处理其他命令,知道RDB过程完成为止

在这里插入图片描述
save触发为什么会阻塞redis服务器呢,因为占用了redis主线程去进行RDB文件处理

bgsave触发执行该命令时,redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。(redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。基本上redis内部所有的RDB操作都是采用bgsave命令)

在这里插入图片描述

2、AOF 默认关闭,开启方法是修改redis.config appendonly yes
日志的形式记录服务器所处理的每一个写操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

在这里插入图片描述
优点:可以保持更高的数据完整性
缺点:AOF文件比RDB文件大,且恢复速度慢

在这里插入图片描述
AOF和RDB混合使用
混合配置:配置文件 aof-use-rdb-preamble yes

三、雪崩、击穿、穿透
缓存雪崩 指在系统运行过程中,缓存服务宕机或者大量key指同时过期,导致所有请求直接访问数据,导致数据库压力增大
解决方法
1.将key的过期时间打散,避免大量key同时过期
2.对缓存服务做高可用处理
3.加互斥锁,同一key值只允许一个线程访问数据库,其余线程等待写入后直接从缓存中获取

缓存穿透访问一个缓存和数据库中都不存在的key,由于这个key在缓存中不存在则会到数据库中查询,数据库也不存在该key,无法将数据添加缓存中,所以每次都会访问数据库导致数据库压力增大
解决方法
1.将空key添加到缓存中
2.使用布隆过滤空key

缓存击穿大量访问缓存中的一个key时,该key过期了,导致这些请求直接访问数据库,短时间的大量请求可能会造成数据库击垮
解决方法
1.添加互斥锁或者分布式锁,让一个线程去访问数据库,将数据添加到缓存中后,其他线程直接访问缓存获取
2.热点数据key不过期,定时更新缓存

四、Redis各集群高可用方案优缺点

在服务开发中,单机都会存在单点故障的问题,及服务部署在一台服务器上,一旦服务器宕机服务就不可用,所以为了服务高可用,分布式服务就出现了,将同一服务部署到多台机器上,即使其中几台服务器宕机,只要有一台服务器可用就行了。

redis为了解决单机故障引入了主从模式,但是主从模式存在一个问题:master节点故障后五福需要人为手动将slave节点切换成为master节点服务才恢复
为了解决这个问题又引入哨兵模式,能在master节点故障后能自动将slave节点提升成master节点,不需要人工干预操作就能恢复服务可用

但是主从模式、哨兵模式都没有达到真正的数据分片存储,每个redis实例中存储的都是全量数据,为了解决这个问题,redis cluster诞生了。实现了真正的数据分片存储

1、主从模式
redis单节点虽然通过RDB和AOF持久化机制能讲数据持久到磁盘上,但数据是存储在一台服务器上的,如果服务器出现磁盘故障等问题,会导致数据不可用,且读写无法分离,读写都在同一台服务器上,请求量大时会出现I/O瓶颈。

为了避免单点故障和读写不分离 redis提供了复制功能实现master数据库中的数据更新后,会自动将更新的数据同步到其他slave数据库上。

主从模式优缺点
优点:主从结构具有读写分离,提供效率,数据备份,提供多个副本等优点
缺点:不具备自动容错和恢复功能,主节点故障,集群则无法进行工作,可用性比较低,从节点升主节点需要人工干预。

2、哨兵模式
哨兵模式核心还是主从复制,只不过相比主从模式,多了一个竞选机制,从所有的从节点竞选出新的主节点。竞选机制的实现是依赖于在系统中启动一个sentinel进程

哨兵模式的作用:

  • 监控所有服务器是否正常运行,通过发送命令返回监控服务器的运行状态,处理监控主服务器、从服务器外,哨兵之间也是相互监控。
  • 故障切换:当哨兵检测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他服务器,修改配置文件,让他们切换master。同时那台有问题的旧主也会变成新主的从。

1)主观下线和客观下线
哨兵节点发送ping命令时,当超过一定时间后,如果节点未回复,则哨兵认为主观下线。如果该节点为主数据库,哨兵会进一步判断是否需要进行故障切换,这时候就要发生命令咨询其他哨兵节点是否认为该主节点主观下线,当达到指定数量时,哨兵就会认为是客观下线。

当主节点客观下线时就需要进行主从切换,主从切换的步骤:
1)选出领头哨兵
2)领头哨兵所有的slave选出优先级最高的从数据库,优先级可以通过slave- priority选项设置
3)如果优先级相同,则从复制的命令偏移量越大,越优先
4)如果以上条件都一样,则选择run id比较小的从数据库

哨兵模式优缺点
优点:哨兵模式基于主从模式,解决主从模式中master故障不可以自动切换的问题
缺点:
1)是一种中心化的集群实现方案:始终只有一个redis主机来接收和处理请求,写操作受单机瓶颈影响
2)集群里所有节点保存的都是全量数据,浪费内存空间,没有真正实现分布式存储,数据量大时,主从同步严重影响master的性能
3)在投票选举结束之前,此时redis会开启保护机制,禁止写操作,直到选举出了新的redis主机

为了解决redis数据分片存储
1)客户端分片
是把分片的逻辑放在redis客户端实现,通过redis客户端预先定义好的路由规则(一致性哈希),把对key的访问转发到不同的redis实例中,查询数据时把返回结果汇集

2)代理分片
基本原理:通过中间件的形式,redis客户端把请求发送到Twemproxy,根据路由规则发送到正确的redis实例,最后Twemproxy把结果汇集返回给客户端

3)redis cluster
cluster模式实现了redis的分布式存储,也就是说每台redis节点上存储不同的数据(分片和路由都是在服务端实现)采用多主多从,每一个分区都是由一个redis主机和多个从机组成,分片和片区之间相互平行。redis cluster集群采用了P2P的模式,完全去中心化。
特点:

  • 集群完全去中心化,采用多主多从;所有的redis节点彼此互联,内部使用二进制协议优化传输速度和宽带
  • 客户端与redis节点直连,不需要中间代理层。客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
  • 每一个分区都是由一个redis主机和多个从机组成,分片和分片之间是相互平行的
  • 每一个master节点负责维护一部分槽,以及槽所映射的键值数据

redis cluster主要针对海量数据+高并发+高可用的场景

五、reids主从同步
假设有A、B两个实例,如何让B作为A的slave节点?
在B节点执行命令:slaveof A的IP A的端口

1、全量同步
主从第一次连接时会执行全量同步,将master节点所有的数据拷贝给slave节点

在这里插入图片描述

master如何判断一个slave是否是第一次同步?
slave原本也是master,有自己的replication id和offset,与master连接时。将自己的replication id 和offset发给master,master收到replication id后,发现与自己的replication id不一致,master就可以判断该slave是第一次连接,于是开始全量同步,master会将自己的replied和offset都发送给这个slave。slave保存这些信息,master和offset的replid就保存一致了。

2、增量同步
在这里插入图片描述

repl_baklog会记录redis处理的命令日志及offset(包括master当前offset和slave已经拷贝到offset)

repl_baklog原理
是一个固定大小数组,只不过数组是环形,也就是说角标达到数组末尾后,会再次从0开始读写,这样数组头部的数据就会被覆盖。
数组一旦写满,再有新数据写入时,就会覆盖数组的旧数据。此时slave来增量同步,发现自己offset已经被覆盖,此时只能全量同步。

redis主从集群优化

  • 在master中配置repl_diskless-sync yes启用无磁盘复制,避免全量同步时的磁盘IO
  • redis单节点上的内存占用不要太大,减少RDB导致过多磁盘IO
  • 适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步
  • 限制一个master上的slave节点数量,如果实在太多slave,则可以采用主-从-链式结构,减少master压力

全量同步和增量同步区别
全量同步:master将完整内存数据生产RDB,发送RDB到slave,后续命令则记录在repl_baklog,逐个发送给slave
增量同步:slave提交自己的offset到master,master获取repl_baklog中从offset之后命令给slave

什么时候执行全量同步、增量同步
全量同步:1.slave节点第一次连接master节点时,slave节点断开时间太久,repl_baklog中的offset已经被覆盖时。
增量同步:slave节点断开又恢复,并且在repl_baklog中能找到offset时。

5、MQ

1.为什么要用MQ?

  1. 有些复杂的业务系统,一次性用户请求可能会同步调用N个系统的接口,需要等待所有的接口都返回了才能正真的获取执行结果(这种同步接口调用方式总耗时比较长,非常影响用户体验,而且容易出现接口超时问题)
  2. 很多复杂的业务系统,一般都会拆分成多个子系统。以下单为例(请求先通过订单系统,然后分别调用:支付系统、库存系统、积分系统、物流系统等)系统之间耦合性太高,如果调用的任何一个子系统出现异常,整个请求都会异常,对系统的稳定性非常不利。
  3. 用户请求突增(秒杀活动)一时间所有请求都到数据库,可能会导致数据库无法承受这么大的压力,响应变慢或者直接挂掉。对于这种突然出现请求峰值,无法保证系统的稳定性。

异步:对于问题1,同步接口调用导致响应时间长的问题,使用MQ之后,将同步调用改成异步,能够显著减少系统响应时间

在这里插入图片描述
在这里插入图片描述

解藕子系统间耦合性太大的问题,使用MQ之后,我们只需依赖于MQ,避免了各个子系统间的强依赖问题

在这里插入图片描述
在这里插入图片描述

消峰突然出现请求峰值,导致系统不稳定的问题,使用MQ后能够起到肖峰的作用

在这里插入图片描述
在这里插入图片描述

引入MQ会出现哪些问题?

1.重复消息问题:重复消费问题一直都是mq普遍存在的问题,无法避免
场景:信息生产者生产了重复的信息、消费消费者确认超时、业务系统主动发起重试、Kafka和rocketmq的offset被回调

在这里插入图片描述
解决方案推荐增加一张消费消息表,来解决mq的这类问题。消费消息表中使用messageId作为唯一索引,在处理业务逻辑之前,先根据messageId查询一下该消息有没有处理过,如果已经处理过了则直接返回成功,如果没有处理则继续走业务流程。

在这里插入图片描述

2.数据一致性问题:如果mq的消费者业务处理异常的话,就会出现数据一致性问题(如果下单和送积分在同一个事务中,要么成功要么失败,是不会出现数据一致性问题的,但是由于跨系统调用,为了性能考虑,一般不会使用强一致性,而是最终一致性)

解决方案(强一致性、弱一致性、最终一致性)
为了性能考虑使用的是最终一致性,那么必定会出现数据不一致的问题。这类问题大概率是因为消费者读取消息后,业务逻辑处理失败导致的,这时候可以增加重试机制。

重试分为:同步重试和异步重试
有些消息量比较小的业务场景,可以采用同步重试,在消费消息时如果处理失败,立刻重试3-5次,如果还是失败则写入记录表中,但是如果消息量比较大,则不建议使用同步重试,因为如果出现网络异常,可能会导致大量的消息不断重试,影响读取速度,造成消息堆积
建议采用异步重试,在消费者处理失败之后,立刻写入重试表,有个job专门定时重试。

3.消息丢失问题:消息丢失的原因有很多(生产者、消费者、mq服务器)都可能产生问题
场景:1.消费生产者发送消息时,由于网络原因发送到mq失败;2.mq服务器持久化湿磁盘出现异常;3.Kafka和rocketmq的offset被回调时,略过了很多消息;4.消息消费者刚读取消息,已经ack确认了,但业务还没有处理完,服务器就被重启了。
解决方案我们可以增加一张消息发送表,当生产者发完消息之后,会往该表中写入一条数据,将状态status标记为待确认。如果消费者读取消息之后,调用生产者的API更新该消息的status为已确认。有个job,每隔一段时间检查一次消息发送表,如果5分钟(时间可以自定)后还有状态是待确认的消息,则认为该消息已经丢失,重新发送消息。

在这里插入图片描述

4.消息顺序问题:有些业务数据是有状态的,比如订单有下单、支付、完成、退货等状态
解决这类问题之前,我们先确认一下,消费者是否真的需要知道中间状态,只知道最终状态行不行?其实大部分情况,我们只需知道最终状态即可。

在这里插入图片描述

在这里插入图片描述
但如果真的需要保证消息顺序的需求。订单号路由到不同的partition,同一个订单号的消息,每次都发送同一个partition。

在这里插入图片描述

5.消息堆积:很多时候,由于某些批处理或者其他原因,导致消息消费等速度小于生产速度。这样会直接导致消息堆积问题,从而影响业务功能
解决方案
1、如果不需要保证顺序,可以读取消息之后用多线程处理业务逻辑。

在这里插入图片描述
2、如果需要保证顺序,可以读取消息之后,将消息按照一定的规则分发到多个队列中,然后在队列中用单线程处理。

在这里插入图片描述

6.系统复杂度提升:因为引入mq(生产者、消费者、mq服务器)

6、Docker

1、docker有三大核心组件
1.镜像(Image),一个Linux的文件系统;
2.容器(Container),镜像的运行时实例;
3.仓库(Repository),集中存储镜像的地方;

镜像
就是一个Linux的文件系统,这个文件系统里面包含可以运行在Linux内核的出现及相关的数据。
镜像特点

  • 镜像是分层的:即一个镜像可以多个中间层组成,多个镜像可以共享同一个中间层,我们也可以通过在镜像添加多一层来生层一个新的镜像。
  • 镜像是只读:镜像在构建完成之后,便不可用再修改,而上面我们所说的添加一层构建新的镜像,这中间实际是通过创建一个临时的容器,在容器上增加或删除文件,从而形成新的镜像,因为容器是可以动态改变的。

容器
容器与镜像的关系,就如同面向编程中对象与类之间的关系。
因为容器是通过镜像来创建的,所以必须先有镜像才能创建容器,而生成的容器是一个独立于宿主机的隔离进程,并且有属于容器自己的网络和命名空间。
镜像由多个中间层组成,生成的镜像是只读的,但是容器却是可读可写的,这是因为容器是在镜像上面添一层读写层来实现的

仓库
是集中存储镜像的地方

2、docker挂载
在docker中,挂载就是宿主机的文件或文件夹覆盖容器内的文件或文件夹,可以实现宿主机和容器目录(文件)的双向数据自动同步

文件夹挂载
host上文件夹一定会覆盖container中文件夹

hostcontainermount result
文件夹不存在/文件夹存在但为空文件夹不存在/存在但为空/存在且不为空container文件被覆盖(清空)
文件夹存在且不为空文件夹不存在/存在但为空/存在且不为空container中文件夹内容被覆盖(原内容清空,覆盖为host上文件夹内容)
  • 允许不存在的文件夹或者存在的空文件夹挂载进container,container中对应的文件夹被清空
  • 非空文件夹挂载进container将会覆盖container中原有的文件夹

文件挂载
文件挂载与文件夹挂载最大的不同点在于:

  • docker禁止用主机上不存在的文件挂载到container中已经存在的文件
  • 文件挂载不会对同一文件夹下其他文件产生任何影响
  • 存在的文件挂载进container中将会覆盖container中对应的文件,若文件不存在则新建
hostcontainermount result
不存在的文件已经存在的文件禁止行为
存在文件不存在的文件/以及存在的文件新增/覆盖

应用场景
文件夹挂载以整个文件夹为单位进行文件覆盖,故可在需要大量文件挂载进行container时使用,另外如果挂载一个空文件夹或者不存在的文件夹,一般是做逆向使用:即容器启动后,可能会在容器内挂载点点文件夹下生成一些文件,此时在对应的host上文件夹就能直接看到。

文件挂载由于只会覆盖单个文件而不会影响container中同一文件夹下的其他文件,常常被用来挂载配置文件,以在运行时,动态的修改默认配置。

7、ES

Elasticsearch时一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮组我们从海量数据中快速找到需要的内容。如:百度搜索、电商搜索、打车软件附近搜索等等。

  • 什么是elasticsearch?一个开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控等功能
  • 什么是elastic stack(ELK)?是以elasticsearch为核心的技术栈,包括beats、Logstash、kibana、elasticsearch

1、倒排索引
倒排索引的概念是基于Mysql的正向索引而言的

1)正向索引

在这里插入图片描述
如果是根据ID查询,那么直接走索引,查询速度非常快。
但是如果基于title做模糊查询,只能逐行扫描数据,流程如下:
1、用户搜索数据,条件是title符合%手机%
2、逐行获取数据,比如ID为1的数据
3、判断数据中的title是否符合用户搜索条件
4、如果符合则放入结果集,不符合则丢弃,返回步骤1
逐行扫描就相当于全表扫描,随着数据量增加,器查询效率也会越来越低,当数据量达到数百万时,就是一场灾难

2)倒排索引
倒排索引中有两个非常重要的概念
1.文档:用于索搜的数据,其中每一条数据就是一个文档,如一个网页,一个商品信息
2.词条:对文档数据或用户搜索数据,利用某种算法分析,得到的具备含义的词语就是词条

在这里插入图片描述
步骤流程
1.用户输入条件华为手机进行搜索
2.对用户输入内容分词,得到词条:华为、手机
2.拿着词条在倒排索引中查找,可以得到包含词条的文档ID:1、2、3
4.拿着文档的ID到正向索引中查找具体文档:如图

在这里插入图片描述
虽然要先查询倒排索引,再查询正向索引,但是无论是词条还是文档ID都是建立了索引,查询速度非常快,无需全表扫描。

正向索引与倒排索引优缺点
正向索引
优点

  • 可以给多个字段创建索引,根据索引字段索搜,排序速度非常快
    缺点
  • 根据非索引字段或者索引字段中部分词条查找时,只能全表扫描

倒排索引
优点

  • 根据词条搜索,模糊搜索时,速度非常快
    缺点
  • 只能给词条创建索引,而不是字段。无法根据字段做排序

8、JVM

在这里插入图片描述
1、堆
所有创建的对象信息都放在这个区域,堆也是JVM中最大的一部分,是线程共享。
在这里插入图片描述
1)新生代:分为三部分(Eden、ServivorFrom、ServivorTo)
新创建的对象会存放在新生代,经历了一定次数的GC(垃圾回收)之后,依然存活下来的数据,会移动至老年代。
2)老年代:
经历了一定次数还存活的对象及大对象

为什么大对象直接存放到老年代?
因为大对象创建和消耗的时间比较多,性能比较低,如果放到新生代,可能需要频繁的创建和销毁对象,导致JVM运行效率降低。

为什么要进行堆分代?
分代的理由就是为了优化GC性能,如果没有分代,那么我们所有的对象都在一块,GC的时候我们要找到哪些对象没用,这样会对堆堆所有区域进行扫描。所以我们把新创建的对象放到某一个地方,当GC的时候先对新生代的区域进行回收,这样可以提高效率。

2、Java虚拟机栈
在这里插入图片描述
Java虚拟机栈时线程私有的

3、本地方法栈
本地方法栈和JVM虚拟机栈类似,只不过JVM栈时为了字节码服务的,而本地方法栈时为本地方法服务的,本地方法栈是线程私有的。

4、程序计数器记录线程执行的行号,线程私有的
5、方法区存储被虚拟机加载的类型信息、常量、静态变量。

线程私有(本地方法栈、Java虚拟机栈、程序计数器);线程共有(堆、方法区)

何时进行JVM调优

  • Heap内存(老年代)持续上涨达到设置的最大值
  • Full GC次数频繁
  • GC停顿时间过长(超过1秒)
  • 应用出现OutOfMemory内存异常
  • 应用中有使用本地缓存且占用大量内存空间
  • 系统吞吐量与响应性能不高或下降

JVM调优的基本原则 JVM调优是一个手段,但并不是一定所有的问题都JVM进行调优解决

  • 大多数的java应用不需要进行JVM调优
  • 大多数导致GC问题的原因是代码层面的问题导致的
  • 上线之前,应先考虑将机器的JVM参数设置到最优
  • 减少创建对象的数量和减少全局变量和大对象
  • 优先架构调优和代码调优,JVM优化是不得已的手段
  • 分析GC情况优化代码比优化JVM参数更好

JVM调优目标 1.延迟(GC低停顿和GC低频率);2.低内存占用;3.高吞吐量

JVM调优步骤

  • 分析GC日志及dump文件,判断是否需要优化,确定瓶颈的问题点
  • 确定JVM调优目标
  • 确定JVM调优参数
  • 依次调优内存、延迟、吞吐量等指标
  • 对比观察调优前后的差异
  • 不断分析和调整,直到找到合适的JVM参数配置
  • 找到最合适的参数,将这些参数应用到所有服务器并且进行后续跟踪

JVM参数调优
例子:-Xmx4g –Xms4g –Xmn1200m –Xss512k -XX:NewRatio=4 -XX:SurvivorRatio=8 -XX:PermSize=100m -XX:MaxPermSize=256m -XX:MaxTenuringThreshold=15
1)-Xmx10m 堆最大容量
2)-Xms10m堆最小容量
3)通常情况下可以将Xmx和Xms的大小设置相同,这样可以防止堆扩容造成抖动

内存溢出在程序运行中,无法申请到足够的内存资源
原因:

  • 内存中加载的数据过于庞大
  • 代码中存在死循环或者存在大量对象的创建
  • 虚拟机内存参数设置过小
    解决方案:
  • 修改JVM内存参数,一般将-Xms和-Xmx设置成相同的值,避免每次GC后调整堆堆大小
  • 检查错误日志,查看内存溢出背后的实际原因
  • 对代码进行查走和分析,排查产生错误代码
  • 使用内存查看工具,动态地监视内存的使用情况,尝试复现内存溢出的场景,进而发现问题。

内存泄漏不再被使用的对象占用的内存空间,本应该被释放,但没有被垃圾回收掉
原因:

  • 使用静态的集合类。静态变量不会被垃圾回收,而集合占用的内存又一般很大。
  • 各种连接没有及时关闭,比如数据库连接、IO连接
  • 一些强引用的对象,在不使用后没有设置为null,导致无法被回收
  • 变量的作用域设置不合理,存在周期过长
  • 过多的单例模式类

9、Linux

常用命令

10、简单算法

冒泡、选择、插入、快速、希尔排序
1、有一对兔子从出生后第3个月起每个月都生一对兔子,兔子长到第3个月后每个月又生一对兔子,假如兔子都不死,请问每个月都兔子总数为多少?
前10个月的总数为:1、1、2、3、5、8、13、21、34、55
发现其规律是从第3个数字开始,每个数字的值等于前面相邻的两个数字的和,所以这是斐波那契数列。

    public static int num(int month){
    	int temp = 0;
    	int month1num = 1;
    	int month2num = 1;
    	for(int i =3;i <= month;i++){
    		temp = month2num;
    		month2num = month2num +month1num;
    		month1num = temp;
    	}
    	return month2num;
    }

2、判断101-200之间有多少个素数,并输出所有的素数。(素数一般指质数,质数是指大于1的自然数中,除了1和它本身以外不再有其他因数的自然数)

 public static void main(String[] args) {
        int j=0;
        int k=0;
        for(int i=101;i<200;i++){
            k=(int)Math.sqrt((double)i+1);
            for(j=2;j<=k;j++){
                if(i%j == 0){
                    /*代表该数字i不是素数*/
                    break;
                }
            }
            if(j==(k+1)){
                /*当j和i相等时,这个数就是素数*/
                System.out.print(i+" ");
            }
        }
    }

3、打印出所有的“水仙花数”,所谓水仙花数是指一个三位数,其各位数字立方和等于该数本身。例如153是一个水仙花数,因为153=1的三次方+5点三次方+3的三次方。(从题目分析可知水仙花数是三位数,范围101-999)

    public static void main(String[] args) {
        for(int i =101;i<1000;i++){
            /*百位数字*/
            int hundredNum=i/100;
            /*十位数字*/
            int tenNum=i/10%10;
            /*个位数字*/
            int oneNum=i%10;
            if(i == hundredNum*hundredNum*hundredNum+tenNum*tenNum*tenNum+oneNum*oneNum*oneNum){
                System.out.println(i+" ");
            }
        }
    }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值