集合
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产生哈希冲突时如何利用链表(或红黑树)来解决?
哈希冲突时,不同的键值对(但哈希值相同)会被映射到同一个数组上,这时会通过链表(或红黑树)来将相同哈希值的键值对串联在一起
- 当需要查找键值对时,HashMap 会先计算它的哈希值,然后找到这个哈希值的数组
- 如果这个数组没有对应的链表,那么这个键值对就是要查找的对象
- 如果这个数组有对应的链表(或红黑树),那么 HashMap 会遍历这个链表,直到找到要查找的键值对
比如A、B键值对哈希值为5,那么他们将串联在代表着哈希5的数组下,形成一个链表
注意点
- List排序可用
list.sort(Comparator.comparing(对象类::get属性))
,根据对象属性进行排序 Arrays.asList()
返回的集合只支持遍历和取值,不能做任何修改操作(增删改)- List快速去重的方法是转为Set类型,
Set set = new LinkedHashSet(list);
- HashMap底层是数组+链表,数组用来存储键值对,链表(或红黑树)用来解决哈希冲突(存储哈希值相同的键值对)
- JDK1.8之后,HashMap链表长度大于阈值(默认8)且数组长度大于(默认64)转红黑树
- 哈希冲突的产生原因:哈希函数的设计通常是将键值映射到一个较小的、固定长度的哈希值上。因为键的数量通常比哈希值的数量大得多,所以会有不同的键映射到相同的哈希值上
- HashSet的底层其实就是HashMap,
set.add(E e) == map.put(e, new Object())
,所以HashSet不允许重复,因为底层HashMap中key不能重复
I/O
三种模型
- BIO(同步阻塞):读取过程中一直阻塞,直到读取完成
- NIO(同步非阻塞):需要通过轮询的方式来检查数据是否已经准备好,只有已经准备完成才能开始数据的读取或写入
- AIO(异步非阻塞):异步操作,通知回调
字节/字符流
- InputStream/Reader
- OutputStream/writer
设计模式
- 工厂模式:用于创建对象,将对象的创建与使用分离,使代码更加灵活。
例如:XX.newInstance()
,将具体的对象的创建逻辑进行封装,不对外暴露
- 单例模式:用于确保一个类只有一个实例,并提供一个全局访问点。
例如:XX.getInstance()
,私有化构造方法,防止外部创建实例,对外暴露一个返回单例的静态方法
- 适配器模式:用于将一个接口转换成另一个接口,使不兼容的接口可以一起工作。
例如:InputStream适配成InputStreamReader,通过InputStreamReader可以接收字节流转换为字符流
- 装饰器模式:用于动态地给一个对象添加一些额外的职责,而不需要修改其源代码。
例如:BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName), "UTF-8"));
观察者模式:用于实现对象之间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都会得到通知。
策略模式:用于在运行时动态地选择算法,将算法的实现与算法的使用分离。
命令模式:将请求封装成对象,以便在不同的请求、队列或日志中参数化客户端,并支持可撤销的操作。
迭代器模式:用于顺序地访问集合中的元素,而无需暴露集合的内部结构。
模板方法模式:用于定义一个算法的骨架,而将一些步骤延迟到子类中实现。
组合模式:用于将对象组合成树形结构来表示“部分-整体”的层次结构,使得用户可以一致地处理单个对象和组合对象。
说到底就是封装,可重用,方便维护
并发
线程生命周期
- 新建:
Thread t = new Thread()
- 就绪:
t.strat()
- 运行:获取CPU资源后,执行
run()
方法 - 阻塞:因为某些原因(如等待 I/O 完成、等待锁、调用 sleep() 方法等)无法继续执行时,该线程进入阻塞状态
- 死亡:
run()
方法执行结束或调用了stop()
方法
执行流程
阻塞和等待的区别
阻塞是因为拿不到锁进入了阻塞队列中,不耗费cpu资源。
等待是原本拿到了锁,但是因为某些原因主动放弃 CPU 的使用权(释放锁)等待其他线程通知或者信号才能继续执行
如何预防或者避免线程死锁?
- 一次性申请所有资源,避免锁中锁
- 获取锁的顺序一致
- 占用锁的资源再进一步申请其他锁前,释放原本自己持有的锁
乐观锁、悲观锁
乐观锁:实际上是不采用锁的策略,更新数据时比对原先数据版本号,相同的情况下才去进行修改操作(适合多读)
悲观锁:每次只允许一个线程去修改共享资源,效率低但安全(适合多写),行锁、表锁
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 | // 可以将所有需要CAS操作的数据建立一个对象统一管理 |
举几个常用的锁
ReentrantLock:可重入锁、公平锁(默认为非公平)、可中断锁(正在等待的线程可放弃等待
lock.lockInterruptibly()
)ReadWriteLock:读写锁,它允许多个线程同时访问共享资源的读取操作,而在写入操作时独占锁定,提高了读取性能,并保证数据在写入时的安全性
1 | private final ReadWriteLock rwLock = new ReentrantReadWriteLock(); |
Synchronized同步关键字
可修饰在方法和代码块上,其中分为3种情况,属于互斥锁(只允许一个线程在任意时刻访问共享资源),可重入
- 修饰在非静态的方法上,锁的是对象(Object)
- 修饰在静态方法上,锁的是类(.class)
- 修饰在代码块上,是对Synchronized关键字后面括号中的对象(Object)或类(.class)进行加锁
Semaphore信号量
允许指定数量的线程在任意时刻访问共享资源,通过许可证机制实现的等待机制,线程可以请求许可证,如果可用则可以执行,否则会被阻塞等待
1 | private final Semaphore semaphore = new Semaphore(3); // 限制同时访问的线程数量为3 |
CountDownLatch线程同步工具类
1 | CountDownLatch countDownLatch = new CountDownLatch(3); // 定义同步的线程数量 |
AQS原理
以ReentrantLock
可重入锁为例,state
表示同步状态,初始为0
- 调用
lock()
方法,线程A获取锁,随后其会独占该锁并将state+1
,因为是可重入锁,所以线程A在释放锁之前可以重复获取此锁,每次获取锁state
都需+1
,如一共获取了4次锁,那么state
为4,每次unlock
使state-1
,需要执行4次unlock
释放锁 - 线程B判断
state
状态,值大于0,持锁失败,线程阻塞 - 值等于0,则持锁成功
ThreadLocal线程本地变量
每个线程维护一个独属于自己的本地变量,互相隔离,同一个线程定义多个ThreadLocal对象也存在同一个ThreadLocalMap中
分布式项目中traceId传递采用的是MDC的方式,其内部维护了一个InheritableThreadLocal
对象,子线程共享父线程中创建的线程副本数据
线程池
创建参数
- corePoolSize – 核心线程数,即使它们处于空闲状态也要保留在池中
- maximumPoolSize – 池中允许的最大线程数
- keepAliveTime – 当线程数大于核心时,这是多余的空闲线程在终止之前等待新任务的最长时间
- TimeUnit – keepAliveTime 的单位,比如秒
TimeUnit.SECONDS
- BlockingQueue – 阻塞队列 ,比如有界阻塞队列
new ArrayBlockingQueue<>(100)
- RejectedExecutionHandler – 拒绝策略,阻塞队列满时采用的策略
1 | ThreadPoolExecutor tpe = new ThreadPoolExecutor(6, 100, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), new ThreadPoolExecutor.AbortPolicy()); |
拒绝策略
- AbortPolicy:抛出异常来拒绝新任务
- CallerRunsPolicy:任何请求都会被执行,不拒绝,但是延迟会高
- DiscardOldestPolicy:丢弃最早的未处理任务
- DiscardPolicy:不处理新的任务,直接丢弃
最大线程数设置
线程数太大:大量上下文切换资源浪费
线程数太小:同一时间大量任务无法执行放入队列中,导致OOM
- CPU密集型(大量数据计算):N+1线程,N指cpu核心数
- I/O密集型(网络/文件读取,数据库操作,RPC调用):2N线程
并发集合
ConcurrentHashMap
:线程安全的HashMap- volatile关键字修饰,读写操作直接写入内存中,避免缓存不一致
1
2volatile 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三握四挥
三握
- 第一次握手,接收方确定发送方发送正常,自己接收正常
- 第二次握手,发送方确定自己发送、接收正常,对方发送、接收正常,接收方确定发送方发送正常,自己接收正常
- 第三次握手,确定双方发送、接收都正常
四挥
- 发送方告知接收方数据传输结束
- 接收方告知发送方自己收到了通知
- 接收方将剩余内容告知发送方,并通知自己传输也已经结束
- 发送方告知接收方收到通知
JVM总结
程序执行过程
1 | public class App{ |
编译好 App.java 后得到 App.class 后,执行 App.class,系统会启动一个 JVM 进程,从 classpath 路径中找到一个名为 App.class 的二进制文件,将 App 的类信息加载到运行时数据区的方法区内,这个过程叫做 App 类的加载
JVM 找到 App 的主程序入口,执行 main 方法
这个 main 中的第一条语句为 Student student = new Student(“tellUrDream”) ,就是让 JVM 创建一个 Student 对象,但是这个时候方法区中是没有 Student 类的信息的,所以 JVM 马上加载 Student 类,把Student 类的信息放到方法区中
加载完 Student 类后,JVM 在堆中为一个新的 Student 实例分配内存,然后调用构造函数初始化 Student 实例,这个 Student 实例持有指向方法区中的 Student 类的类型信息的引用
执行
student.sayName()
时,JVM 根据 student 的引用找到 student 对象,然后根据 student 对象持有的引用定位到方法区中 student 类的类型信息的方法表,获得 sayName() 的字节码地址,然后执行sayName()
内存区
栈管运行,堆管存储⭐️
线程私有的:
- 程序计数器:线程切换后能恢复到正确的执行位置
- 虚拟机栈(简称栈):栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈(存放方法执行过程中产生的中间计算结果)、动态链接( 主要服务一个方法需要调用其他方法的场景)、方法返回地址
- 本地方法栈:和上面栈类似,唯一不同在这里为Native方法服务
线程共享的(有线程安全问题):
堆:存在对象实例以及数组列表,堆内存被通常分为下面三部分
- 新生代
- 老生代
- 元空间(JDK1.8之后),永久代(JDK1.8之前)这两种是方法区的实现方式
永久代被元空间替代的原因:固定大小无法调整,存放的元数据由系统内存控制,不受JVM调控
方法区:存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据
- 运行时常量池:类似于传统编程语言的符号表
- 字符串常量池:针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建
- 直接内存 (非运行时数据区的一部分)
内存分配
- 对象优先分配在
对象创建过程(重要)
- 类加载检查:遇到new指令时查看类是否已被加载过、解析和初始化过,如果没有,那必须先执行相应的类加载过程
- 给新生对象分配内存
- 指针碰撞:适用于没有内存碎片的场景,将用过的内存统一放在一侧,每次分配内存,只需向着没用过的内存方向将该指针移动对象内存大小位置即可,适用GC:Serial, ParNew
- 空闲列表:适用于有内存碎片的场景,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,适用GC:CMS
- 初始化零值:赋值给对象中涉及到的字段的数据类型所对应的零值
- 设置对象头:对象的基本数据(哈希码、是哪个类的实例等)存放在对象头中
- 执行 init 方法:把对象按照程序员的意愿进行初始化,即按照构造方法
类加载器
类加载器是一个负责加载类的对象,类加载器的主要作用就是加载 Java 类的字节码到 JVM 中
内置加载器
BootstrapClassLoader
:最顶层的加载类,主要用来加载 JDK 内部的核心类库,rt.jarExtensionClassLoader
:扩展类加载器,加载扩展的 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
正确使用索引
- 合适的字段:尽量不为null、被频繁查询、需要排序、用于连接的字段
- 不会经常被更新的字段
- 索引字段过多,建议使用联合索引,他的最佳体验是涉及的字段全部命中,当只有个别命中时仍然可用索引但效率下降
- 最左匹配原则:我们在使用联合索引时,可以将区分度高的字段放在最左边,这也可以过滤更多数据
索引失效的场景
- 使用
SELECT *
进行查询; - 联合索引未遵守最左匹配原则
- 在索引列上进行计算、函数、类型转换等操作
- 以
%
开头的 LIKE 查询比如like '%abc'
,以%
结尾没有问题
查看是否使用索引
使用explain
进行查询该SQL是否用到索引
日志
redolog(重做日志) :保证事务的持久性
比如 MySQL
实例挂了或宕机了,重启时,InnoDB
存储引擎会使用redo log
恢复数据,保证数据的持久性与完整性
binlog(同步日志) :同步数据,保证数据一致性
数据库备份、主从同步会用到,其记录了详细的SQL
undolog(回滚日志):来保证事务的原子性
在异常发生时,对已经执行的操作进行回滚
隔离级别
- 读未提交:事务开启,A能读取到B未提交的数据
- 读已提交:事务开启,A能读取到B已提交的数据,所以造成无法重复读
- 可重复度(默认隔离级别):事务开启,无论B是否修改,A都能读到修改之前的数据,并且多次值相同,但是B进行增删,A读到结果会不一致,这叫幻读
- 可串行化:最高隔离级别,该级别可以防止脏读、不可重复读以及幻读
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 | # 升序 |
explain()
执行逻辑
使用explain()
方法可以查看当前查询是否用到索引
1 | "winningPlan" : { |
Elasticsearch
Query和Filter的区别
- Query和Filter都是根据条件查找符合的数据
- Query性能差,计算每个文档对于搜索条件的相关度分数, 再根据评分倒序排序
- Filter性能好,但是没有进行任何的计算,也不会进行排序
一般追求性能的话,先用Filter过滤出大的条件,然后使用Query进行计算排序
1 | NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder(); |
Score Mode
sum
模式:将所有子查询得分相加。适用于需要将所有匹配项的得分相加的情况,例如在满足多个查询条件时返回文档。avg
模式:将所有子查询得分相加并除以子查询数。适用于需要平均得分的情况,例如在搜索结果中排序时。max
模式:返回所有子查询的最高得分。适用于需要最高得分的情况,例如只要有一个子查询匹配文档就返回该文档。min
模式:返回所有子查询的最低得分。适用于需要最低得分的情况,例如需要满足所有子查询才能匹配文档的情况。multiply
模式:将所有子查询得分相乘。适用于需要所有子查询得分的乘积的情况,例如需要匹配所有查询条件的情况。
SearchType
QUERY_THEN_FETCH
:默认的搜索方式
DFS_QUERY_THEN_FETCH
:准确度更高,但是速度慢
FieldType
text:String类型,可进行分词,用于全文检索,不用于排序、聚合
keyword:String类型,不可进行分词,用于关键词搜索、排序、聚合
注意:ElasticSearch字符串将默认被同时映射成
text
和keyword
类型1
2
3
4
5
6
7
8
9
10
11{
"message": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}nested:当查找数组列表中同时需要满足多个字段时,需要使用此类Type
1 | // 新增 John Smith和Alice 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 li
、like i
、i eating
、i like eating and cooking
不行,i
、like
、eating
、and
、cooking
可以
boolQuery
:通过逻辑运算符(AND(must)、OR(should)、NOT(mustNot))组合多个子句prefixQuery
:适用于搜索特定字段中以指定前缀开头的文档,常用于前缀匹配、自动补全等场景。rangeQuery
:适用于搜索特定字段中在指定范围内的文档,可以用于按时间、价格、评分等范围过滤、排序等场景。wildcardQuery
:适用于搜索特定字段中匹配指定通配符表达式的文档,常用于模糊匹配、正则表达式匹配等场景。QueryBuilders.
wildcardQuery(“supplierName”,
““+param+`”“)类似于sql中的
%`,进行模糊匹配- 适合使用keyword的形式,即语句不分祠,分词会导致一些param被错误分词,以至于无法匹配到
fuzzyQuery
:适用于执行近似匹配,可以处理拼写错误或相似性问题,常用于纠错、模糊匹配等场景。
聚合
- Avg Aggregation(平均聚合):计算文档字段的平均值。
- Sum Aggregation(求和聚合):计算文档字段的总和。
- Max Aggregation(最大聚合):返回文档字段的最大值。
- Min Aggregation(最小聚合):返回文档字段的最小值。
- Stats Aggregation(统计聚合):计算文档字段的统计信息,包括平均值、最大值、最小值和标准差等。
- Cardinality Aggregation(基数聚合):计算文档字段的不同值的数量。
- Terms Aggregation(词项聚合):返回文档中某个字段的所有不同值及其出现次数。
- Date Histogram Aggregation(日期直方图聚合):按照时间范围对文档进行聚合,并按照时间间隔返回结果。
- Range Aggregation(范围聚合):将文档字段的值按照一定范围进行分组,并返回每个范围内文档的数量或聚合结果。
- Geo Distance Aggregation(地理距离聚合):将文档按照地理位置进行聚合,并返回指定地理距离内的文档数量或聚合结果。
1 | // 求content.caseType每种类型的数量 |
Spring
@Resource和@Autowired区别
- @Resource根据名称去注入,@Autowired根据类型去注入
1 |
|
Spring AOP
ProceedingJoinPoint joinPoint 方法
proceed()
:proceed前的是方法执行前的准备工作,然后调用proceed执行任务,最后再进行任务执行完毕的结尾工作getArgs()
:获取连接点方法的参数列表getSignature()
:获取连接点方法的签名对象1
2
3// 可获取方法对象
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();getTarget()
:获取连接点所在的目标对象getThis()
:获取代理对象本身
设计模式
工厂模式:
BeanFactory
、ApplicationContext
创建bean对象代理模式:Spring AOP
单例模式:Spring 中的 Bean 默认都是单例的
1
2// Spring默认bean为单例,如需改非单例,设置如下
(ConfigurableBeanFactory.SCOPE_PROTOTYPE)观察者模式:Spring中事件驱动模型就是采用了观察者模式
1
2
3
4
5
6
7
8
9
10
11
12// 事件监听者
public void handleUserCreatedEvent(LoginBean event) {
System.out.println("rawData:" + rawData);
}
private ApplicationEventPublisher eventPublisher;
// 事件发布者
public void publish(){
LoginBean loginBean = new LoginBean();
eventPublisher.publishEvent(loginBean);
}
事务
事务类型
编程式事务:通过
TransactionTemplate
或者TransactionManager
手动管理(不推荐)1
2
3
4
5
6
7
8
9
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 | (propagation = Propagation.REQUIRED) |
TransactionDefinition.PROPAGATION_REQUIRES_NEW
:无论外围方法是否开启事务,Propagation.REQUIRES_NEW
修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰
1 | (propagation = Propagation.REQUIRED) |
TransactionDefinition.PROPAGATION_NESTED
:在外围方法开启事务的情况下Propagation.NESTED
修饰的内部方法属于外部事务的子事务,外围主事务回滚,子事务一定回滚,而内部子事务可以单独回滚而不影响外围主事务和其他子事务
1 | (propagation = Propagation.REQUIRED) |
REQUIRED,REQUIRES_NEW,NESTED 异同
- NESTED 和 REQUIRED
- 父事务回滚:子事务回滚。
- 子事务回滚:NESTED父事务不回滚,REQUIRED父事务回滚
- NESTED 和 REQUIRES_NEW
- 子事务回滚:父事务不回滚。
- 父事务回滚:NESTED子事务回滚,REQUIRES_NEW子事务不回滚
事务只读属性
如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询 SQL 必须保证整体的读一致性,否则,在前条 SQL 查询之后,后条 SQL 查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持
@Transactional
的使用注意事项总结
@Transactional
注解只有作用到 public 方法上事务才生效,不推荐在接口上使用;- 避免同一个类中调用
@Transactional
注解的方法,这样会导致事务失效; - 正确的设置
@Transactional
的rollbackFor
和propagation
属性,否则事务可能会回滚失败; - 被
@Transactional
注解的方法所在的类必须被 Spring 管理,否则不生效; - 底层使用的数据库必须支持事务机制,否则不生效,比如mysql中
innodb
引擎支持事务,myisam
不支持
Mybatis
标签
1 | <resultMap> 、 <parameterMap> |
常见知识点
#{}
是 sql 的参数占位符,${}
通常用于替换静态的 SQL 片段,例如表名、列名等Mapper
接口方法是可以重载的,但是同一个mapperxml
文件中同一个id只能出现一次,可用动态sql解决
1 | public interface StuMapper { |
1 | <select id="getAllStu" resultType="com.pojo.Student"> |
权限系统设计
设计方式
RBAC
:通过不同的角色去赋予权限,比如Admin
、visitor
(游客)ABAC
:通过属性来动态判断一个操作是否可以被允许,比如早上九点前禁止 A 部门的人访问 B 系统或者在除了上海以外的地方禁止以管理员身份访问 A 系统
读写分离、分库分表
主从同步延迟问题
将那些必须获取最新数据的读请求都交给主库处理
1
2
3HintManager hintManager = HintManager.getInstance();
hintManager.setMasterRouteOnly();
// 继续JDBC操作延迟读取,通过设置页面手动跳转进行,在用户手动去点击跳转的时候已经主从同步了
具体实现
- 部署多台服务器,一主多从
- 保证主数据库和从数据库数据一致,实时同步
- 按照写主读从进行分配
实现原理
根据主数据库的binlog
进行多台数据库同步
分库分表
- 水平分库:是把同一个表按一定规则拆分到不同的数据库中,两个库中表字段一样
- 垂直分库:不同的业务使用不同的数据库,进而将一个数据库的压力分担到多个数据库,两个库中表字段不一样
- 水平分表:是对数据表行的拆分,把一张行比较多的表拆分为多张表,可以解决单一 表数据量过大的问题,两个表中表字段一样
- 垂直分表:是对数据表列的拆分,把一张列比较多的表拆分为多张表,两个表中表字段不一样
分片算法
- 哈希分片:求指定 key(比如 id) 的哈希,适合随机读写的场景
- 范围分片:比如id根据一定的范围进行划分,适合范围查找的场景
- 地理位置分片:根据地理位置(如城市、地域)来分配数据
分库分表后,数据怎么迁移呢?
- 闲时停机迁移
- 双写方案:我们对老库的更新操作(增删改),同时也要写入新库(双写),再将老库和新库进行比对,将新库没有的数据进行插入,一直重复此操作直到老库和新库的数据一致为止