java总结

集合

List、Queue、Deque、Set、Map的区别?

List(可重复):ArrayList(底层数组、有序)、LinkedList(底层链表、有序)、Vector(线程安全但效率低,不会去用

Queue(单端队列,尾加头删):PriorityQueue(根据传入的比较器排序后,优先级高的先出,属于优先队列)、BlockingQueue(线程安全的有界阻塞队列)、ConcurrentLinkedQueue(线程安全的无界非阻塞队列)

1
PriorityQueue<Integer> queue = new PriorityQueue<Integer>((o1, o2) -> Integer.compare(o1, o2));

Deque(双端队列,头尾皆可增删):ArrayDeque(无界非阻塞)、ConcurrentLinkedDeque(线程安全的无界非阻塞队列)

Set(不可重复):TreeSet(红黑树,根据传入的比较器排序)、HashSet(无序)、LinkedHashSet(有序)

Map(键值对):HashMap、HashTable(线程安全但效率低,不会去用)、ConcurrentHashMap(线程安全)、TreeMap、LinkedHashMap(有序)

总结:(LinkedXX都是有序)、(TreeXX、PriorityXX都根据比较器排序)、(ConcurrentXX都是线程安全的)

有界和无界队列如何选择?

有界队列适用于系统资源有限的情况,比如线程池创建中的任务队列,如果采用无界队列容易OOM,所以一般采用有界队列配合拒绝策略来完成

无界队列适用于需要保证所有事件都能够被添加到队列的场景,比如重要的消息队列,无界队列通常与异步执行结合使用

集合如何扩容?

ArrayList扩容

初始为0 —> 添加1个数据容量扩大为10 —> 再添加10个数据,容量扩大至原先的1.5倍(oldCapacity + (oldCapacity >> 1))即10*1.5=15

HashMap扩容

默认初始是16,也可指定初始容量,如果指定初始容量不是2的幂次方则自动向上扩容到离给定值最近的2的幂次方,默认加载因子是0.75,只要容量到阈值(阈值=容量*加载因子)即16*0.75=12则开始扩容,新容量 = 旧容量*2新阈值=新容量*加载因子

扩容涉及到rehash,所以很耗性能,推荐初始就能给定一个合适的容量,避免经常扩容

HashMap产生哈希冲突时如何利用链表(或红黑树)来解决?

哈希冲突时,不同的键值对(但哈希值相同)会被映射到同一个数组上,这时会通过链表(或红黑树)来将相同哈希值的键值对串联在一起

  1. 当需要查找键值对时,HashMap 会先计算它的哈希值,然后找到这个哈希值的数组
  2. 如果这个数组没有对应的链表,那么这个键值对就是要查找的对象
  3. 如果这个数组有对应的链表(或红黑树),那么 HashMap 会遍历这个链表,直到找到要查找的键值对

比如A、B键值对哈希值为5,那么他们将串联在代表着哈希5的数组下,形成一个链表

注意点

  1. List排序可用list.sort(Comparator.comparing(对象类::get属性)),根据对象属性进行排序
  2. Arrays.asList()返回的集合只支持遍历和取值,不能做任何修改操作(增删改)
  3. List快速去重的方法是转为Set类型,Set set = new LinkedHashSet(list);
  4. HashMap底层是数组+链表,数组用来存储键值对,链表(或红黑树)用来解决哈希冲突(存储哈希值相同的键值对
  5. JDK1.8之后,HashMap链表长度大于阈值(默认8)且数组长度大于(默认64)转红黑树
  6. 哈希冲突的产生原因:哈希函数的设计通常是将键值映射到一个较小的、固定长度的哈希值上。因为键的数量通常比哈希值的数量大得多,所以会有不同的键映射到相同的哈希值上
  7. HashSet的底层其实就是HashMap,set.add(E e) == map.put(e, new Object()),所以HashSet不允许重复,因为底层HashMap中key不能重复

I/O

三种模型

  1. BIO(同步阻塞):读取过程中一直阻塞,直到读取完成
  2. NIO(同步非阻塞):需要通过轮询的方式来检查数据是否已经准备好,只有已经准备完成才能开始数据的读取或写入
  3. AIO(异步非阻塞):异步操作,通知回调

字节/字符流

  1. InputStream/Reader
  2. OutputStream/writer

设计模式

  • 工厂模式:用于创建对象,将对象的创建与使用分离,使代码更加灵活。

例如:XX.newInstance(),将具体的对象的创建逻辑进行封装,不对外暴露

  • 单例模式:用于确保一个类只有一个实例,并提供一个全局访问点。

例如:XX.getInstance(),私有化构造方法,防止外部创建实例,对外暴露一个返回单例的静态方法

  • 适配器模式:用于将一个接口转换成另一个接口,使不兼容的接口可以一起工作。

例如:InputStream适配成InputStreamReader,通过InputStreamReader可以接收字节流转换为字符流

  • 装饰器模式:用于动态地给一个对象添加一些额外的职责,而不需要修改其源代码。

例如:BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName), "UTF-8"));

  • 观察者模式:用于实现对象之间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都会得到通知。

  • 策略模式:用于在运行时动态地选择算法,将算法的实现与算法的使用分离。

  • 命令模式:将请求封装成对象,以便在不同的请求、队列或日志中参数化客户端,并支持可撤销的操作。

  • 迭代器模式:用于顺序地访问集合中的元素,而无需暴露集合的内部结构。

  • 模板方法模式:用于定义一个算法的骨架,而将一些步骤延迟到子类中实现。

  • 组合模式:用于将对象组合成树形结构来表示“部分-整体”的层次结构,使得用户可以一致地处理单个对象和组合对象。

说到底就是封装,可重用,方便维护

并发

线程生命周期

  1. 新建:Thread t = new Thread()
  2. 就绪:t.strat()
  3. 运行:获取CPU资源后,执行run()方法
  4. 阻塞:因为某些原因(如等待 I/O 完成、等待锁、调用 sleep() 方法等)无法继续执行时,该线程进入阻塞状态
  5. 死亡:run()方法执行结束或调用了stop()方法

执行流程

阻塞和等待的区别

阻塞是因为拿不到锁进入了阻塞队列中,不耗费cpu资源。

等待是原本拿到了锁,但是因为某些原因主动放弃 CPU 的使用权(释放锁)等待其他线程通知或者信号才能继续执行

如何预防或者避免线程死锁?

  1. 一次性申请所有资源,避免锁中锁
  2. 获取锁的顺序一致
  3. 占用锁的资源再进一步申请其他锁前,释放原本自己持有的锁

乐观锁、悲观锁

  • 乐观锁:实际上是不采用锁的策略,更新数据时比对原先数据版本号,相同的情况下才去进行修改操作(适合多读)

  • 悲观锁:每次只允许一个线程去修改共享资源,效率低但安全(适合多写),行锁、表锁

CAS算法

乐观锁最常用的算法,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新

  • V :要更新的变量值(Var)
  • E :预期值(Expected)
  • N :拟写入的新值(New)

只有V == E,才会进行修改,修改为N

ABA问题

A -> B -> A 虽然最终结果没有变,但其实已经改变过了。

CAS中ABA问题的解决方法V==E && V.version == E.version,不止比较V和E的值,还要比较两者的版本号,只有两者全部相同才能说明A没有发生过变化

1.5后引入AtomicReference,其中compareAndSet()以原子的方式将值设置为给定的更新值

1
2
3
4
5
// 可以将所有需要CAS操作的数据建立一个对象统一管理
AtomicReference<Param> ref = new AtomicReference<>(Param);
// 只有当AtomicReference中值确认为原先的对象时,才会将对象更新
// compareAndSet 为原子操作
ref.compareAndSet(Param, newParam);

举几个常用的锁

  • ReentrantLock:可重入锁、公平锁(默认为非公平)、可中断锁(正在等待的线程可放弃等待 lock.lockInterruptibly()

  • ReadWriteLock:读写锁,它允许多个线程同时访问共享资源的读取操作,而在写入操作时独占锁定,提高了读取性能,并保证数据在写入时的安全性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
public String readMessage() {
// 在读锁被持有的情况下,其他线程可以继续获取读锁,但不能获取写锁
rwLock.readLock().lock(); // 获取读锁
try {
return message;
} finally {
rwLock.readLock().unlock(); // 释放读锁
}
}

public void writeMessage(String newMessage) {
rwLock.writeLock().lock(); // 获取写锁
try {
// 执行写操作
message = newMessage;
} finally {
rwLock.writeLock().unlock(); // 释放写锁
}
}

Synchronized同步关键字

可修饰在方法和代码块上,其中分为3种情况,属于互斥锁(只允许一个线程在任意时刻访问共享资源),可重入

  1. 修饰在非静态的方法上,锁的是对象(Object)
  2. 修饰在静态方法上,锁的是类(.class)
  3. 修饰在代码块上,是对Synchronized关键字后面括号中的对象(Object)或类(.class)进行加锁

Semaphore信号量

允许指定数量的线程在任意时刻访问共享资源,通过许可证机制实现的等待机制,线程可以请求许可证,如果可用则可以执行,否则会被阻塞等待

1
2
3
private final Semaphore semaphore = new Semaphore(3); // 限制同时访问的线程数量为3
semaphore.acquire(); // 请求许可证
semaphore.release(); // 释放许可证

CountDownLatch线程同步工具类

1
2
3
4
5
6
7
8
CountDownLatch countDownLatch = new CountDownLatch(3); // 定义同步的线程数量
for (int i = 1; i <= 3; i++) {
new Thread(() -> {
countDownLatch.countDown(); // 执行成功,数值-1
}).start();
}
countDownLatch.await(); // 线程阻塞
System.out.println("三个线程全部执行完成");

AQS原理

ReentrantLock可重入锁为例,state表示同步状态,初始为0

  1. 调用lock()方法,线程A获取锁,随后其会独占该锁并将 state+1,因为是可重入锁,所以线程A在释放锁之前可以重复获取此锁,每次获取锁state都需+1,如一共获取了4次锁,那么state为4,每次unlock使state-1,需要执行4次unlock释放锁
  2. 线程B判断state状态,值大于0,持锁失败,线程阻塞
  3. 值等于0,则持锁成功

ThreadLocal线程本地变量

每个线程维护一个独属于自己的本地变量,互相隔离,同一个线程定义多个ThreadLocal对象也存在同一个ThreadLocalMap中

分布式项目中traceId传递采用的是MDC的方式,其内部维护了一个InheritableThreadLocal对象,子线程共享父线程中创建的线程副本数据

线程池

创建参数

  1. corePoolSize – 核心线程数,即使它们处于空闲状态也要保留在池中
  2. maximumPoolSize – 池中允许的最大线程数
  3. keepAliveTime – 当线程数大于核心时,这是多余的空闲线程在终止之前等待新任务的最长时间
  4. TimeUnit – keepAliveTime 的单位,比如秒TimeUnit.SECONDS
  5. BlockingQueue – 阻塞队列 ,比如有界阻塞队列new ArrayBlockingQueue<>(100)
  6. RejectedExecutionHandler – 拒绝策略,阻塞队列时采用的策略
1
ThreadPoolExecutor tpe = new ThreadPoolExecutor(6, 100, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), new ThreadPoolExecutor.AbortPolicy());

拒绝策略

  1. AbortPolicy:抛出异常来拒绝新任务
  2. CallerRunsPolicy:任何请求都会被执行,不拒绝,但是延迟会高
  3. DiscardOldestPolicy:丢弃最早的未处理任务
  4. DiscardPolicy:不处理新的任务,直接丢弃

最大线程数设置

线程数太大:大量上下文切换资源浪费

线程数太小:同一时间大量任务无法执行放入队列中,导致OOM

  1. CPU密集型(大量数据计算):N+1线程,N指cpu核心数
  2. I/O密集型(网络/文件读取,数据库操作,RPC调用):2N线程

并发集合

  • ConcurrentHashMap:线程安全的HashMap

    • volatile关键字修饰,读写操作直接写入内存中,避免缓存不一致
    1
    2
    volatile V val;
    volatile Node<K,V> next
    • CAS操作
    1
    U.compareAndSwapObject()
    • 锁机制
    1
    synchronized (f) {}
  • ConcurrentSkipListMap:跳表,是一种可以用来快速查找的数据结构,有点类似于平衡树

  • CopyOnWriteArrayList:线程安全的ArrayList,读不加锁写加锁,写入时不会阻塞读取操作,原因是写操作不在原数据中进行,而是复制到新的副本上进行修改,写完之后,再将修改完的副本替换原来的数据

  • ConcurrentLinkedQueue:通过CAS来实现线程安全,无界非阻塞

  • BlockingQueue:通过锁来实现线程安全,有界阻塞

    • ArrayBlockingQueue,有界,创建时给定大小,底层数组
    • LinkedBlockingQueue,看是否给定初始大小,如不指定则无界,如指定则有界
    • PriorityBlockingQueue,无界,空间不够自动扩容,(空间小于64则newCap=oldCap+2,大于64则newCap=oldCap+oldCap<<1,扩大原先的一半),支持自定义排序,compareTo()

TCP三握四挥

三握

  1. 第一次握手,接收方确定发送方发送正常,自己接收正常
  2. 第二次握手,发送方确定自己发送、接收正常,对方发送、接收正常,接收方确定发送方发送正常,自己接收正常
  3. 第三次握手,确定双方发送、接收都正常

四挥

  1. 发送方告知接收方数据传输结束
  2. 接收方告知发送方自己收到了通知
  3. 接收方将剩余内容告知发送方,并通知自己传输也已经结束
  4. 发送方告知接收方收到通知

JVM总结

程序执行过程

1
2
3
4
5
6
public class App{
public static void main(String[] args) {
Student s = new Student();
s.sayName()
}
}
  1. 编译好 App.java 后得到 App.class 后,执行 App.class,系统会启动一个 JVM 进程,从 classpath 路径中找到一个名为 App.class 的二进制文件,将 App 的类信息加载到运行时数据区的方法区内,这个过程叫做 App 类的加载

  2. JVM 找到 App 的主程序入口,执行 main 方法

  3. 这个 main 中的第一条语句为 Student student = new Student(“tellUrDream”) ,就是让 JVM 创建一个 Student 对象,但是这个时候方法区中是没有 Student 类的信息的,所以 JVM 马上加载 Student 类,把Student 类的信息放到方法区

  4. 加载完 Student 类后,JVM 在堆中为一个新的 Student 实例分配内存,然后调用构造函数初始化 Student 实例,这个 Student 实例持有指向方法区中的 Student 类的类型信息的引用

  5. 执行student.sayName()时,JVM 根据 student 的引用找到 student 对象,然后根据 student 对象持有的引用定位到方法区中 student 类的类型信息的方法表,获得 sayName() 的字节码地址,然后执行sayName()

内存区

栈管运行,堆管存储⭐️

线程私有的:

  • 程序计数器:线程切换后能恢复到正确的执行位置
  • 虚拟机栈(简称栈):栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈(存放方法执行过程中产生的中间计算结果)、动态链接( 主要服务一个方法需要调用其他方法的场景)、方法返回地址
  • 本地方法栈:和上面栈类似,唯一不同在这里为Native方法服务

线程共享的(有线程安全问题):

  • 堆:存在对象实例以及数组列表,堆内存被通常分为下面三部分

    • 新生代
    • 老生代
    • 元空间(JDK1.8之后),永久代(JDK1.8之前)这两种是方法区的实现方式

    永久代被元空间替代的原因:固定大小无法调整,存放的元数据由系统内存控制,不受JVM调控

  • 方法区:存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据

    • 运行时常量池:类似于传统编程语言的符号表
    • 字符串常量池:针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建
  • 直接内存 (非运行时数据区的一部分)

内存分配

  1. 对象优先分配在

对象创建过程(重要)

  1. 类加载检查:遇到new指令时查看类是否已被加载过、解析和初始化过,如果没有,那必须先执行相应的类加载过程
  2. 给新生对象分配内存
    1. 指针碰撞:适用于没有内存碎片的场景,将用过的内存统一放在一侧,每次分配内存,只需向着没用过的内存方向将该指针移动对象内存大小位置即可,适用GC:Serial, ParNew
    2. 空闲列表:适用于内存碎片的场景,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,适用GC:CMS
  3. 初始化零值:赋值给对象中涉及到的字段的数据类型所对应的零值
  4. 设置对象头:对象的基本数据(哈希码、是哪个类的实例等)存放在对象头中
  5. 执行 init 方法:把对象按照程序员的意愿进行初始化,即按照构造方法

类加载器

类加载器是一个负责加载类的对象类加载器的主要作用就是加载 Java 类的字节码到 JVM 中

内置加载器

  • BootstrapClassLoader:最顶层的加载类,主要用来加载 JDK 内部的核心类库,rt.jar
  • ExtensionClassLoader扩展类加载器,加载扩展的 jar 包
  • AppClassLoader:面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类
  • Custom ClassLoade:自定义的类加载器

自定义加载器

比如可以实现对字节码的加密和解密,继承ClassLoader

  • 如果想使用双亲委派模型就重写findClass(String name)根据类的二进制名称来查找类,官方建议使用这个⭐️
  • 如果不想使用双亲委派模型就重写loadClass(String name, boolean resolve)

双亲委派模型

  • 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器,在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载
  • 优点:可以避免类的重复加载
  • 所有请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader

垃圾回收器

GC的区域:分为部分收集(Partial GC)和整堆收集(Full GC),要尽可能避免Full GC

  • 部分收集
    • 新生代收集
    • 老年代收集
    • 混合收集:对整个新生代和部分老年代进行垃圾收集
  • 整堆收集:收集整个 Java 堆和方法区

垃圾收集算法

  • 标记–清除算法:标记出不需要回收的对象,没被标记的全部统一回收
  • 标记–复制算法:将内存平分两块,每次使用其中的一块,当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉,每次的内存回收都是对内存区间的一半进行回收
  • 标记–整理算法:标记出不需要回收的对象,让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
  • 分代收集算法:新生代–标记复制,老生代–标记清除/整理

垃圾收集器

  • Serial(新生代)/Old(老年代) 收集器:单线程,GC时暂停其他所有的工作线程,用户体验差,采用分代收集算法
  • ParNew收集器:Serial 收集器的多线程版本,采用分代收集算法
  • Parallel Scavenge(新生代)/Old(老年代) 收集器:多线程,高效率的利用 CPU,吞吐量优先,采用分代收集算法java8默认
  • CMS 收集器:第一款真正意义上的并发收集器,响应速度优先,采用标记–清除算法,会产生大量空间碎片
  • G1 收集器:以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征java9默认

重要参数调优

从重要到次要排序

  • 堆内存调优

    • 显式指定堆内存–Xms-Xmx,如-Xms2G -Xmx5G

    • 显式新生代内存,降低新对象直接进入老年代的情况,如-XX:NewSize=256m -XX:MaxNewSize=1024m

    • 显示指定元空间的大小,如-XX:MetaspaceSize=N -XX:MaxMetaspaceSize=N

  • 垃圾回收器调优

    1
    2
    3
    4
    -XX:+UseSerialGC
    -XX:+UseParallelGC
    -XX:+UseParNewGC
    -XX:+UseG1GC

Mysql

索引

提高检索速度,降低增删改速度

底层数据结构

采用B+树,多路平衡查找树,之所以不用hash,因为范围查找需要hash次数太多,比如id<500,精确查找时速度快,比如id=1

正确使用索引

  1. 合适的字段:尽量不为null、被频繁查询、需要排序、用于连接的字段
  2. 不会经常被更新的字段
  3. 索引字段过多,建议使用联合索引,他的最佳体验是涉及的字段全部命中,当只有个别命中时仍然可用索引但效率下降
  4. 最左匹配原则:我们在使用联合索引时,可以将区分度高的字段放在最左边,这也可以过滤更多数据

索引失效的场景

  • 使用 SELECT * 进行查询;
  • 联合索引未遵守最左匹配原则
  • 在索引列上进行计算、函数、类型转换等操作
  • % 开头的 LIKE 查询比如 like '%abc'% 结尾没有问题

查看是否使用索引

使用explain进行查询该SQL是否用到索引

日志

redolog(重做日志) :保证事务的持久性

比如 MySQL 实例挂了或宕机了,重启时,InnoDB存储引擎会使用redo log恢复数据,保证数据的持久性与完整性

binlog(同步日志) :同步数据,保证数据一致性

数据库备份、主从同步会用到,其记录了详细的SQL

undolog(回滚日志):来保证事务的原子性

在异常发生时,对已经执行的操作进行回滚

隔离级别

  1. 读未提交:事务开启,A能读取到B未提交的数据
  2. 读已提交:事务开启,A能读取到B已提交的数据,所以造成无法重复读
  3. 可重复度(默认隔离级别):事务开启,无论B是否修改,A都能读到修改之前的数据,并且多次值相同,但是B进行增删,A读到结果会不一致,这叫幻读
  4. 可串行化:最高隔离级别,该级别可以防止脏读、不可重复读以及幻读

Redis

单线程

Redis读写一直采用单线程模型通过多路IO复用来监听多个连接,只有删除一些大键值对才会使用异步多线程处理

常用缓存策略

  • 旁路缓存(用的最多)⭐️
    • 写:1. 更新DB 2.删除cache(为防止数据没有删除可以引入重试机制)
    • 读:1.从cache读取,读取到就直接返回 2.读取失败则从DB中读取后返回 3.将DB中读取的数据存储到cache中
  • 读写穿透(Redis 并没有提供 cache 将数据写入 db 的功能)
    • 写:1. cache中存在则更新cache,由cache自动更新到DB 2. cache不存在则直接更新DB
    • 读:1. 从cache读取,读取到就直接返回 2.读取失败则从DB中读取写入cache后返回(这个是 cache 服务自己来写入缓存的,旁路缓存则是客户端负责把数据写入 cache)
  • 异步缓存写入:类似读写穿透,唯一区别是读写穿透是同步更新 cache 和 db,而 异步缓存写入则是只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db,风险太大

基本数据结构

  • String:可用来存储任何类型的数据
  • List:双向链表结构,可重复
  • Hash:以键值对的形式存储
  • Set:集合,无重复
  • Zset:有序集合,根据score自定义排序

特殊数据结构

  • Bitmap:用于储存0或1,极大的节省储存空间
  • HyperLogLog:用于估算大数据(百万、千万级别以上计数场景)的数据量
  • Geospatial index:用于存储地理位置信息,实现功能比如 附近的人

数据删除策略

redis过期的key不是立刻被删除的,有两种删除策略

  • 惰性删除:只有在取key的时候进行检查,如果过期则删除,利好cpu
  • 定期删除:隔一段时间抽取一批key并删除过期的key

当出现大量key集中过期的问题,解决方法是给key设置不同的过期时间

内存淘汰机制

最常用的是当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key

持久化

  • RDB快照,默认方式
  • AOF只追加文件,通过append only yes开启,每执行一条命令就将命令写入缓存,再根据appendfsync来同步到硬盘AOF文件中

优缺点:RDB更小,恢复速度更快,但是无法做到秒级持久化数据,AOF支持

生产问题

  • 缓存穿透:大量数据库和cache都不存在的数据,大量请求直达数据库

解决方法:缓存无效的key、布隆过滤器

  • 缓存击穿:cache中热点数据过期瞬间,大量请求直达数据库

解决方法:设置热点数据永不过期,抵达数据库前加互斥锁

  • 缓存雪崩:cache中大量数据同一时间过期,大量请求直达数据库

解决方法:设置key随机过期时间

基于Redis的分布式锁

使用Redisson来实现分布式锁,内部主要核心在于setNX,即key不存在的话就为它设置值

MongoDB

索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
## 升序
db.users.createIndex({ "age": 1 })
## 降序
db.users.createIndex({ "email": -1 })
## 全文索引
db.users.createIndex({ "email": "text" })
## hash索引
db.users.createIndex({ "email": "hashed" })
## 联合索引
db.users.createIndex({ "email": 1, "age": 1 })
## TTL索引 将 "expireAt" 作为一个字段添加到文档中,表示文档的过期时间
## "expireAfterSeconds" 参数指定了文档过期的时间(以秒为单位)。如果将其设置为 0,则表示文档应在 "expireAt" 字段中指定的时间过期
## db.users.insert( { "name": "John", "age": 30, "expireAt": new Date(ISODate().getTime() + (60 * 60 * 24 * 7 * 1000)) } )
db.users.createIndex( { "expireAt": 1 }, { expireAfterSeconds: 0 } )

explain()执行逻辑

使用explain()方法可以查看当前查询是否用到索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
## IXSCAN 表示 MongoDB 使用了索引扫描来优化查询
"stage" : "IXSCAN",
## keyPattern 表示使用的索引键
"keyPattern" : {
"orders.orderNumber" : 1.0
},
## 使用的索引名称
"indexName" : "orders.orderNumber_1",
## 查询使用的索引范围
"indexBounds" : {
"orders.orderNumber" : [
"[\"123456\", \"123456\"]"
]
}
},

Elasticsearch

Query和Filter的区别

  • Query和Filter都是根据条件查找符合的数据
  • Query性能差,计算每个文档对于搜索条件的相关度分数, 再根据评分倒序排序
  • Filter性能好,但是没有进行任何的计算,也不会进行排序

一般追求性能的话,先用Filter过滤出大的条件,然后使用Query进行计算排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 指定brandId,排除其他不符合条件的数据
boolQueryBuilder.must(QueryBuilders.termQuery("brandId", brandId));
// 添加进过滤条件
nativeSearchQueryBuilder.withFilter(boolQueryBuilder);
// 将keyword和name字段进行比对,权重为10
filterFunctionBuilders.add(new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.matchQuery("name", keyword),ScoreFunctionBuilders.weightFactorFunction(10)));
// 创建一个数组
FunctionScoreQueryBuilder.FilterFunctionBuilder[] builders = new FunctionScoreQueryBuilder.FilterFunctionBuilder[filterFunctionBuilders.size()];
filterFunctionBuilders.toArray(builders);
// 设置scoreMode和MinScore
FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(builders)
.scoreMode(FunctionScoreQuery.ScoreMode.SUM)
.setMinScore(2);
// 添加进查询条件
nativeSearchQueryBuilder.withQuery(functionScoreQueryBuilder);
// 结果根据id降序
nativeSearchQueryBuilder.withSorts(SortBuilders.fieldSort("id").order(SortOrder.DESC));
// 将所有条件进行构造
NativeSearchQuery searchQuery = nativeSearchQueryBuilder.build();
// 进行最终的es查询
SearchHits<EsProduct> searchHits = elasticsearchRestTemplate.search(searchQuery, EsProduct.class);

Score Mode

  1. sum模式:将所有子查询得分相加。适用于需要将所有匹配项的得分相加的情况,例如在满足多个查询条件时返回文档。
  2. avg模式:将所有子查询得分相加并除以子查询数。适用于需要平均得分的情况,例如在搜索结果中排序时。
  3. max模式:返回所有子查询的最高得分。适用于需要最高得分的情况,例如只要有一个子查询匹配文档就返回该文档。
  4. min模式:返回所有子查询的最低得分。适用于需要最低得分的情况,例如需要满足所有子查询才能匹配文档的情况。
  5. multiply模式:将所有子查询得分相乘。适用于需要所有子查询得分的乘积的情况,例如需要匹配所有查询条件的情况。

SearchType

QUERY_THEN_FETCH:默认的搜索方式

DFS_QUERY_THEN_FETCH:准确度更高,但是速度慢

FieldType

  • text:String类型,可进行分词,用于全文检索,不用于排序、聚合

  • keyword:String类型,不可进行分词,用于关键词搜索、排序、聚合

    注意:ElasticSearch字符串将默认被同时映射成textkeyword类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {
    "message": {
    "type": "text",
    "fields": {
    "keyword": {
    "type": "keyword",
    "ignore_above": 256
    }
    }
    }
    }
  • nested:当查找数组列表中同时需要满足多个字段时,需要使用此类Type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 新增 John Smith和Alice White二人
PUT my_index/_doc/1
{
"group" : "fans",
"user" : [
{
"first" : "John",
"last" : "Smith"
},
{
"first" : "Alice",
"last" : "White"
}
]
}
// 可以查询到 可其实没有john white这个人
GET my_index/_search
{
"query": {
"bool": {
"must": [
{ "match": { "user.first": "John" }},
{ "match": { "user.last": "White" }}
]
}
}
}
// 不可查询
GET /my_index_nest/_search
{
"query": {
"nested": {
"path": "user",
"query": {
"bool": {
"must": [
{ "match": { "user.first": "John" }},
{ "match": { "user.last": "White" }}
]
}
}
}
}
}

QueryBuilders

举例语句:i like eating and cooking,共5个分词词语

  • matchQuery:适用于执行全文本搜索,若分词中的任意一个词与目标字段匹配上,则可查询到

    • 匹配到五个中任意一个完整分词词语,即可查询成功,i li可以,like i 可以,i eating可以
  • matchPhraseQuery:适用于执行短语搜索,需要连续完整的单词和顺序要完全一样

    • i li不行,like i 不行,i eating不行
  • matchPhrasePrefixQuery:和 match_phrase用法是一样的,区别就在于它允许对最后一个词条前缀匹配

    • i li可以,like i 不行,i eating不行
  • termQuery:只能接收一个词语,然后将这个词语和语句分词进行匹配,命中一个分词即匹配,如搜索指定用户、指定ID等。

    • i lilike ii eatingi like eating and cooking不行,ilikeeatingandcooking可以
  • boolQuery:通过逻辑运算符(AND(must)、OR(should)、NOT(mustNot))组合多个子句
  • prefixQuery:适用于搜索特定字段中以指定前缀开头的文档,常用于前缀匹配、自动补全等场景。
  • rangeQuery:适用于搜索特定字段中在指定范围内的文档,可以用于按时间、价格、评分等范围过滤、排序等场景。
  • wildcardQuery:适用于搜索特定字段中匹配指定通配符表达式的文档,常用于模糊匹配、正则表达式匹配等场景。
    • QueryBuilders.wildcardQuery(“supplierName”,“+param+`”“) 类似于sql中的%`,进行模糊匹配
    • 适合使用keyword的形式,即语句不分祠,分词会导致一些param被错误分词,以至于无法匹配到
  • fuzzyQuery:适用于执行近似匹配,可以处理拼写错误或相似性问题,常用于纠错、模糊匹配等场景。

聚合

  1. Avg Aggregation(平均聚合):计算文档字段的平均值。
  2. Sum Aggregation(求和聚合):计算文档字段的总和。
  3. Max Aggregation(最大聚合):返回文档字段的最大值。
  4. Min Aggregation(最小聚合):返回文档字段的最小值。
  5. Stats Aggregation(统计聚合):计算文档字段的统计信息,包括平均值、最大值、最小值和标准差等。
  6. Cardinality Aggregation(基数聚合):计算文档字段的不同值的数量。
  7. Terms Aggregation(词项聚合):返回文档中某个字段的所有不同值及其出现次数。
  8. Date Histogram Aggregation(日期直方图聚合):按照时间范围对文档进行聚合,并按照时间间隔返回结果。
  9. Range Aggregation(范围聚合):将文档字段的值按照一定范围进行分组,并返回每个范围内文档的数量或聚合结果。
  10. Geo Distance Aggregation(地理距离聚合):将文档按照地理位置进行聚合,并返回指定地理距离内的文档数量或聚合结果。
1
2
3
4
5
6
7
8
9
10
11
12
// 求content.caseType每种类型的数量
GET /indexmapping/_search
{
"size": 0,
"aggs": {
"by_type": {
"terms": {
"field": "content.caseType.keyword"
}
}
}
}

Spring

@Resource和@Autowired区别

  • @Resource根据名称去注入,@Autowired根据类型去注入
1
2
3
4
5
6
7
8
9
10
11
12
13
@Bean
public EsTestBean2 bean2() {
return new EsTestBean2("fi","la");
}
// 使用@Resource 如不标注name,则对象名要和上面的bean名称一致
@Resource
private EsTestBean2 bean2;
// 或者标注name为bean2
@Resource(name="bean2")
private EsTestBean2 esTestBean2;
// 或者使用@Autowired
@Autowired
private EsTestBean2 esTestBean2;

Spring AOP

ProceedingJoinPoint joinPoint 方法

  1. proceed():proceed前的是方法执行前的准备工作,然后调用proceed执行任务,最后再进行任务执行完毕的结尾工作

  2. getArgs():获取连接点方法的参数列表

  3. getSignature():获取连接点方法的签名对象

    1
    2
    3
    // 可获取方法对象
    MethodSignature methodSignature = (MethodSignature) signature;
    Method method = methodSignature.getMethod();
  4. getTarget():获取连接点所在的目标对象

  5. getThis():获取代理对象本身

设计模式

  • 工厂模式:BeanFactoryApplicationContext创建bean对象

  • 代理模式:Spring AOP

  • 单例模式:Spring 中的 Bean 默认都是单例的

    1
    2
    // Spring默认bean为单例,如需改非单例,设置如下
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
  • 观察者模式:Spring中事件驱动模型就是采用了观察者模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 事件监听者
    @EventListener
    public void handleUserCreatedEvent(LoginBean event) {
    System.out.println("rawData:" + rawData);
    }
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    // 事件发布者
    public void publish(){
    LoginBean loginBean = new LoginBean();
    eventPublisher.publishEvent(loginBean);
    }

事务

事务类型

  • 编程式事务:通过 TransactionTemplate或者 TransactionManager 手动管理(不推荐)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Autowired
    private PlatformTransactionManager transactionManager;
    TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
    try {
    // .... 业务代码
    transactionManager.commit(status);
    } catch (Exception e) {
    transactionManager.rollback(status);
    }
  • 声明式事务:@Transactional(rollbackFor = Exception.class)

    • propagation:事务的传播行为,默认值为 REQUIRED
    • isolation:事务的隔离级别,默认值采用 DEFAULT
    • timeout:事务的超时时间,默认值为-1(不会超时)。如果超过该时间限制但事务还没有完成,则自动回滚事务。
    • readOnly:指定事务是否为只读事务,默认值为 false
    • rollbackFor:指定能够触发事务回滚的异常类型,默认为 RuntimeException 和 Error ,这里需要指定为Exception

事务传播行为

为了解决业务层方法之间互相调用的事务问题

  • TransactionDefinition.PROPAGATION_REQUIRED:默认传播行为,当最外围方法开启事务并为PROPAGATION_REQUIRED,那么只要一个方法回滚,整个事务均回滚,如果最外围没有开启事务,则事务间互相独立
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Transactional(propagation = Propagation.REQUIRED)
public void addRequired(User1 user){
user1Mapper.insert(user);
}
@Transactional(propagation = Propagation.REQUIRED)
public void addRequiredException(User2 user){
user2Mapper.insert(user);
throw new RuntimeException();
}
// 外部开启事务,所有事务方法互相关联,只要一个方法回滚,整个事务均回滚
@Transactional(propagation = Propagation.REQUIRED)
public void notransaction_required_required_exception(){
user1.setName("张三");
//因为李四方法修饰为REQUIRED形成一个整体事务,所以try/catch可以被外围感知
// 张三插入失败
user1Service.addRequired(user1);
// 李四插入失败
user2.setName("李四");
try {
user2Service.addRequiredException(user2);
} catch (Exception e) {
System.out.println("方法回滚");
}
}
  • TransactionDefinition.PROPAGATION_REQUIRES_NEW:无论外围方法是否开启事务,Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Transactional(propagation = Propagation.REQUIRED)
public void transaction_required_requiresNew_requiresNew_exception_try(){
// 因为王五方法修饰为REQUIRES_NEW形成一个独立事务,所以try/catch无法被感知
// 插入
user1.setName("张三");
user1Service.addRequired(user1);
// 插入
user2.setName("李四");
user2Service.addRequiresNew(user2); //REQUIRES_NEW
// 未插入
user3.setName("王五");
try {
user2Service.addRequiresNewException(user3);
} catch (Exception e) {
System.out.println("回滚");
}
}
  • TransactionDefinition.PROPAGATION_NESTED在外围方法开启事务的情况下Propagation.NESTED修饰的内部方法属于外部事务的子事务,外围主事务回滚,子事务一定回滚,而内部子事务可以单独回滚而不影响外围主事务和其他子事务
1
2
3
4
5
6
7
8
9
10
11
12
13
 @Transactional(propagation = Propagation.REQUIRED)
public void transaction_nested_nested_exception_try(){
// 插入
user1.setName("张三");
user1Service.addNested(user1);
// 未插入
user2.setName("李四");
try {
user2Service.addNestedException(user2);
} catch (Exception e) {
System.out.println("方法回滚");
}
}

REQUIRED,REQUIRES_NEW,NESTED 异同

  • NESTED 和 REQUIRED
    • 父事务回滚:子事务回滚。
    • 子事务回滚:NESTED父事务不回滚,REQUIRED父事务回滚
  • NESTED 和 REQUIRES_NEW
    • 子事务回滚:父事务不回滚。
    • 父事务回滚:NESTED子事务回滚,REQUIRES_NEW子事务不回滚

事务只读属性

如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询 SQL 必须保证整体的读一致性,否则,在前条 SQL 查询之后,后条 SQL 查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持

@Transactional 的使用注意事项总结

  • @Transactional 注解只有作用到 public 方法上事务才生效,不推荐在接口上使用;
  • 避免同一个类中调用 @Transactional 注解的方法,这样会导致事务失效;
  • 正确的设置 @TransactionalrollbackForpropagation 属性,否则事务可能会回滚失败;
  • @Transactional 注解的方法所在的类必须被 Spring 管理,否则不生效;
  • 底层使用的数据库必须支持事务机制,否则不生效,比如mysql中innodb引擎支持事务,myisam不支持

Mybatis

标签

1
2
3
<resultMap> 、 <parameterMap> 
<sql>(sql片段)、<include>(引入sql片段)、 <selectKey>(返回插入数据的主键数值)
trim|where|set|foreach|if|choose|when|otherwise|bind

常见知识点

  • #{}是 sql 的参数占位符,${}通常用于替换静态的 SQL 片段,例如表名、列名等
  • Mapper接口方法是可以重载的,但是同一个mapperxml文件中同一个id只能出现一次,可用动态sql解决
1
2
3
4
5
6
public interface StuMapper {

List<Student> getAllStu();

List<Student> getAllStu(@Param("id") Integer id);
}
1
2
3
4
5
6
7
8
<select id="getAllStu" resultType="com.pojo.Student">
select * from student
<where>
<if test="id != null">
id = #{id}
</if>
</where>
</select>

权限系统设计

设计方式

  1. RBAC:通过不同的角色去赋予权限,比如Adminvisitor(游客)
  2. ABAC:通过属性来动态判断一个操作是否可以被允许,比如早上九点前禁止 A 部门的人访问 B 系统或者在除了上海以外的地方禁止以管理员身份访问 A 系统

读写分离、分库分表

主从同步延迟问题

  1. 将那些必须获取最新数据的读请求都交给主库处理

    1
    2
    3
    HintManager hintManager = HintManager.getInstance();
    hintManager.setMasterRouteOnly();
    // 继续JDBC操作
  2. 延迟读取,通过设置页面手动跳转进行,在用户手动去点击跳转的时候已经主从同步

具体实现

  1. 部署多台服务器,一主多从
  2. 保证主数据库和从数据库数据一致,实时同步
  3. 按照写主读从进行分配

实现原理

根据主数据库的binlog进行多台数据库同步

分库分表

  • 水平分库:是把同一个表按一定规则拆分到不同的数据库中,两个库中表字段一样
  • 垂直分库:不同的业务使用不同的数据库,进而将一个数据库的压力分担到多个数据库,两个库中表字段不一样
  • 水平分表:是对数据表行的拆分,把一张比较多的表拆分为多张表,可以解决单一 表数据量过大的问题,两个表中表字段一样
  • 垂直分表:是对数据表列的拆分,把一张比较多的表拆分为多张表,两个表中表字段不一样

分片算法

  • 哈希分片:求指定 key(比如 id) 的哈希,适合随机读写的场景
  • 范围分片:比如id根据一定的范围进行划分,适合范围查找的场景
  • 地理位置分片:根据地理位置(如城市、地域)来分配数据

分库分表后,数据怎么迁移呢?

  1. 闲时停机迁移
  2. 双写方案:我们对老库的更新操作(增删改),同时也要写入新库(双写),再将老库和新库进行比对,将新库没有的数据进行插入,一直重复此操作直到老库和新库的数据一致为止

消息队列

赏个🍗吧
0%