参考博文:2024最强秋招八股文(精简、纯手打)
参考博文:2024年 Java 面试八股文(20w字)
参考博文:一文讲完Java常用设计模式(全23种)
文章目录
- 一、java基础篇
- 二、多线程&并发篇
- A.线程状态:
- B.线程终止:
- 1.Java中实现多线程有几种方法?
- 2.线程池核心的参数?
- 3.线程池拒绝策略?
- 4.什么是线程安全?怎么实现线程安全?
- 5.线程的三大特性?
- 6.Threadlocal原理
- 7.sleep()和wait()的区别
- 8.为什么wait, notify 和 notifyAll这些方法不在thread类里面?
- 9.volatile原理
- 10.Synchronized锁原理?
- 11.能说下Synchronized的效率真的很低吗?
- 12.能说下Synchronized的升级过程吗?
- 13.synchronized和volatile比较
- 14.能说下Synchronized和Lock的区别吗?
- 15. Java中的锁优化的方法
- 16.如何根据 CPU 核心数设计线程池线程数量
- 17.yield()和join()区别
- 18.有三个线程T1,T2,T3,如何保证顺序执行?
- 19.什么是JMM内存模型?为什么需要JMM?
- 20. i++为什么不是线程安全的?
- 21.同步锁的几种方式(锁对象):
- 22.用户线程和守护线程:
- 23.什么是CAS锁,有什么缺点?
- 24.什么是AQS及工作原理?
- 25.J.U.C之并发工具类
- 三、JVM篇
- 四、Mysql数据库
- 五、spring框架
- 1.什么是Spring?
- 2.IOC(控制反转)与DI(依赖注入)理解与区别?
- 3.AOP是什么?
- 4.如何定义一个全局异常处理类?
- 5.BeanFactory 和 ApplicationContext的关系是什么?
- 6.说下 FactoryBean 和 BeanFactory有什么区别?
- 7.Spring循环依赖是什么?spring中是怎么解决循环依赖的?
- 8.Bean 的作用域
- 9.Bean 生命周期
- 10.Spring 事务原理?
- 11.spring事务失效场景
- 12.spring事务的隔离级别
- 13.说一下Spring的事务传播行为
- 14.SpringMVC工作原理
- 15.spring的bean是线程安全的吗?
- 拓展:mybatis如何实现分页?
- 六、SpringCloud微服务
- 七、redis篇
- 八、分布式事务 分布式锁
- 九、MQ消息中间件
- 十、设计模式
- 数据结构与算法
- 网路编程(拓展)
- linux系列(拓展)
- 分库分表(拓展)
- 数据库设计规范(拓展)
一、java基础篇
1.重写重载区别?
重载:在类中创建多个方法,方法名相同,参数及定义不同,返回值也可以不同
重写:子类对父类允许访问的方法实现进行重新编写,返回值和参数不可改变
2.说一下你理解的多态?
同一个行为具有多个不同表现形式或形态的能力
3.== 和 equals 的区别 ?
==:基本类型比较的是值是否相同,引用类型比较堆内存地址是否相同
equals:引用类型默认情况下比较地址,也可重写equals方法用来比较值是否相同
4.ArrayList和LinkedList的区别
ArratList的底层使用动态数组,默认容量为10,当元素数量到达容量时,生成一个新的数组,大小为前一次的1.5倍,然后将原来的数组copy过来;因为数组在内存中是连续的地址,所以ArrayList查找数据更快,由于扩容机制添加数据效率更低
LinkedList的底层使用链表,没有扩容机制;LinkedList在查找数据时需要从头遍历,所以查找慢,但是添加数据效率更高
5.如何保证ArrayList的线程安全?
(1)使用collentions.synchronizedList()方法为ArrayList加锁
(2)使用Vector,Vector底层与Arraylist相同,但是每个方法都由synchronized修饰,速度很慢
(3)使用juc下的CopyOnWriterArrayList,该类实现了读操作不加锁,写操作时为list创建一个副本,期间其它线程读取的都是原本list,写操作都在副本中进行,写入完成后,再将指针指向副本。
6.String、StringBuffer、StringBuilder的区别
String 由 char[] 数组构成,使用了 final 修饰,对 String 进行改变时每次都会新生成一个 String 对象,然后把指针指向新的引用对象。
StringBuffer可变并且线程安全
StringBuiler可变但线程不安全。
操作少量字符数据用 String;单线程操作大量数据用 StringBuilder;多线程操作大量数据StringBuffer
7.深拷贝和浅拷贝
浅拷贝:浅拷贝只复制某个对象的引用,而不复制对象本身,新旧对象还是共享同一块内存
深拷贝:深拷贝会创造一个相同的对象,新对象和原对象不共享内存,修改新对象不会改变原对象。
8.cookie和session区别
- cookie数据存放在客户的浏览器上,session数据放在服务器上
- cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗,如果主要考虑到安全应当使用session
- session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,如果主要考虑到减轻服务器性能方面,应当使用COOKIE
- 单个cookie在客户端的限制是3K,就是说一个站点在客户端存放的COOKIE不能3K。
- 将登陆信息等重要信息存放为SESSION;其他信息如果需要保留,可以放在COOKIE中
- 当客户端禁用 cookie 时将无法使用 cookie
- 在存储的数据量方面:session 能够存储任意的java 对象,cookie 只能存储 String 类型的对象
9.HashMap数据结构
在JDK1.7中,HashMap数据结构为数组+链表;JDK1.8之后增加了数组+链表+红黑树变换,如果链表的长度超过了 8且数组长度最小要达到64 ,那么链表将转化为红黑树;链表长度低于6,就把红黑树转回链表; 初始化 当创建一个 HashMap 实例时,它会初始化一个默认大小的数组(默认为16),每个数组元素是一个链表。
9.1 HashMap底层实现原理?
- put过程
插入数据:对key键hash对数组长度个数取余找到对应下标的位置
如果当前位置上有元素,判断key是否一致,如果一致覆盖
如果不一致,加入该元素的next下一个节点,形成链表结构
如果链表长度超过8个就把链表结构改变成红黑树结构(Node->TreeNode)
如果没有元素,就直接存储到该数组对应下标的位置
扩容:检查当前map的容量和元素个数,如果元素个数超过阈值(容量*扩容因子(默认0.75))
如果超过阈值就开始扩容,扩容为原来两倍,要对其中的元素key键重新hash取余找到节点
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
通过hash()计算出key的hash值,然后调用putVal()执行put操作
/**
* Implements Map.put and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
1、判断数组是否为空,是则进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
2、根据hash值求出数组下标(n-1&hash),并判断该下标是否有元素,没有则直接放入该下标
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
3、如果有值,则判断该下标的对象和要存储的对象是否相等,
先判断hash值是否相等,再判断key是否相等,如果是同一个对象,则直接覆盖并返回
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
4、如果不是同一个对象则判断是否为树节点对象,是就直接添加
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
5、既不是同一个对象,又不是树节点则找到链表的尾部插入,判断链表长度是否需要树化。如果key相同则直接覆盖
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
6、最后判断hashmap的size是否达到阈值,进行扩容处理
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
- get 原理
get方法
1.计算 key 的 hash 值,根据 hash 值找到对应数组下标。
2.如果该位置上没有元素,则返回 null
3.如果该位置上有元素,判断对应位置上的第一个node是否满足条件,如果满足条件,直接返回
4.如果不满足条件,判断当前node是否是最后一个,如果是,说明不存在key,则返回null
5.如果不是最后一个,判断该元素类型是否是红黑树,如果是红黑树,则使用红黑树的方式获取数据
6.如果不是红黑树,遍历链表是否有满足条件的,如果有,直接返回,否则返回null
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods.
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
// 设置一些局部变量
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 首先获取hashmap中的数组和长度,并判断是否为空,如果为空,返回null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 获取key对应的下标对应的链表对象, 并比较第一个是否满足条件
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
// 第一个如果满足条件,则直接返回
return first;
// 判断当前对象是否是最后一个,如果是,说明没有找到对应的key的值
if ((e = first.next) != null) {
// 如果不为空,判断是否是红黑树,如果是红黑树,使用红黑树获取对应key的值
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 如果不是红黑树, 遍历链表,找到对应hash和key的node对象
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
9.2 hashmap为什么采用两倍扩容?
以二次幂展开,容器的元素要么保持原来的索引,要么以二次幂的偏移量出现在新表中。也就是说hashmap采用2倍扩容,可以尽可能的减少元素位置的移动。
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
9.3 hashmap负载因子为什么是0.75?
时间和空间的权衡 值越高空间开销小但是增加了查找开销
As a general rule, the default load factor (.75) offers a good
* tradeoff between time and space costs. Higher values decrease the
* space overhead but increase the lookup cost
9.4 hashmap初始长度为什么是16?
服务于从Key映射到index的Hash算法,在性能和内存的使用上取平衡,实现一个尽量均匀分布的Hash函数,选取16,是通过位运算的方法进行求取的。
9.5 HashMap 和 Hashtable 的区别
- HashMap 允许 key 和 value 为 null,Hashtable 不允许。
- HashMap 的默认初始容量为 16,Hashtable 为 11。
- HashMap 的扩容为原来的 2 倍,Hashtable 的扩容为原来的 2 倍加 1。
- HashMap 是非线程安全的,Hashtable是线程安全的。
- HashMap 的 hash 值重新计算过,Hashtable 直接使用 hashCode。
- HashMap 去掉了 Hashtable 中的 contains 方法。
- HashMap 继承自 AbstractMap 类,Hashtable 继承自 Dictionary 类。
9.6 为什么HashMap会产生死循环?
导致死循环的主要原因是扩容后,节点的顺序会反掉。多线程环境下建议采用ConcurrentHashMap替代。在JDK1.8中,HashMap改成了尾插法,解决了链表死循环的问题。
10.ConcurrentHashMap如何保证的线程安全 为什么性能比HashTable高?
- ConcurrentHashMap是线程安全的Map容器
- 在JDK1.7使用分段锁,将数据分成16段存储,每个数据段配置一把锁,即segment类,这个类继承ReentrantLock来保证线程安全,
- JKD8采用CAS+Synchronized保证线程安全,底层也是使用Node数组+链表+红黑树
- hashtable类基本上所有的方法都是采用synchronized进行线程安全控制,高并发情况下效率就降低 ,
当多个线程访问同步方法时,会发生阻塞或轮询状态;当一个线程使用put()方法添加元素时,另一个线程不能使用put()方法添加元素,也不能使用get()方法,竞争会越来越激烈。
11.java1.8开始为什么ConcurrentHashMap弃用分段锁?
- 加入分段锁浪费内存空间
- 减少锁竞争
- 更高的并发性能和拓展性,避免了分段锁自旋等待开销
- 提高gc效率
12:为什么ConcurrentHashMap1.8使⽤synchronsized⽽不⽤lock?
1.7lock锁是加上了while循环实现⾃旋,效率不⾼,JDK1.6以后 对 synchronized锁做了优化
二、多线程&并发篇
A.线程状态:
- NEW:线程被创建但是没有start运行
- RUNABLE:线程可以运行,是否运行取决于cpu是否调度该线程,如果没有调度就是ready,如调度到就是running
- WAITING:当锁对象调用wait方法,会让持有该锁对象的线程进入无限等待状态,这个状态只有被同一个锁对象的notify才能解- 除,解除后进入RUNABLE状态。
- TIMED_WAITING:sleep(time),wait(time)的时候进入计时等待,当时间到了,继续运行
- BLOCKED:在线程获取不到锁对象的时候,就会进入阻塞状态,当其他线程释放锁,本线程获取到锁才能够继续运行。
- TERMINATED:run方法执行完毕之后,进入终止状态。
B.线程终止:
(1)设置退出标志,使线程正常退出。
(2)使用interrupt()方法中断线程。
1.Java中实现多线程有几种方法?
继承Thread类,实现runnable,callable接口 使用线程池创建线程
1.1为什么不建议使用 Executors静态工厂构建线程池
目的是为了更加明确线程池的运行规则,规避资源耗尽的风险
2.线程池核心的参数?
- corePoolSize 线程池核心线程大小
- maximumPoolSize 线程池最大线程数量
- keepAliveTime 空闲线程存活时间
- unit 空闲线程存活时间单位
- workQueue 工作队列
①ArrayBlockingQueue
基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
②LinkedBlockingQuene
基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而基本不会去创建新线程直到maxPoolSize(很难达到Interger.MAX这个数),因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
③SynchronousQuene
一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
④PriorityBlockingQueue
具有优先级的无界阻塞队列,优先级通过参数Comparator实现。 - threadFactory 线程工厂
- handler 拒绝策略
3.线程池拒绝策略?
- AbortPolicy:默认策略,在需要拒绝任务时抛出RejectedExecutionException;
- CallerRunsPolicy:直接在 execute 方法的调用线程中运行被拒绝的任务,如果线程池已经关闭,任务将被丢弃;
- DiscardPolicy:直接丢弃任务;
- DiscardOldestPolicy:丢弃队列中等待时间最长的任务,并执行当前提交的任务,如果线程池已经关闭,任务将被丢弃。
4.什么是线程安全?怎么实现线程安全?
- 线程安全:当多线程执行同一段程序的时候,如果发生了和预期结果不一致的情况,就是线程不安全的,如果和预期结果一致就是线程安全的,可以加锁解决(把并行运行的线程变成串行化执行)。
- 1.加锁 利用Synchronized或者ReenTrantLock来对不安全对象进行加锁
- 2.Threadlocal来为每一个线程创造一个共享变量的副本来避免几个线程同时操作一个对象时发生线程安全问题
5.线程的三大特性?
- 原子性:一次或多次操作在执行期间不被其他线程影响
- 可见性:当一个线程在工作内存修改了变量,其他线程能立刻知道
- 有序性:JVM对指令的优化会让指令执行顺序改变,有序性是禁止指令重排
6.Threadlocal原理
- 原理:为每个线程创建变量副本,不同线程之间不可见,保证线程安全。每个线程内部都维护了一个Map,key为threadLocal实例,value为要保存的副本。
- 问题:使用ThreadLocal会存在内存泄露问题,因为key为弱引用,而value为强引用,每次gc时key都会回收,而value不会被回收。所以为了解决内存泄漏问题,可以在每次使用完后删除value或者使用static修饰ThreadLocal,可以随时获取value
6.1 ThreadLocalMap它初始长度是多少呢?扩容因子又是多少呢?
。初始长度是16
。扩容因子是2/3
6.2 Threadlocal如何解决hash冲突的呢?
。Hashmap用的是链式地址法
。Threadlocal用的是线性探测法
所谓线性探测,就是根据初始key的hashcode值,确定元素在table数组中的位置,一旦发生哈希冲突,就继续往后找(环形),找到第一个空节点的位置,再把当前 Entry 放进去。查找的过程也是一样的,先根据哈希值计算下标,再从这个位置开始往后找,如果找到第一个空节点还没找到,就认为 key 不存在。
7.sleep()和wait()的区别
- wait() 是Object的方法,sleep() 是Thread类的方法
- wait() 会释放锁,sleep() 不会释放锁
- wait() 要在同步方法或者同步代码块中执行,sleep() 没有限制
- wait() 要调用 notify() 或 notifyall() 唤醒, sleep() 自动唤醒
8.为什么wait, notify 和 notifyAll这些方法不在thread类里面?
这些方法的作用是在多个线程之间进行协调和通信,而不是单个线程的控制,需要一个共享的锁对象来实现。因为所有的 Java 对象都可以作为锁,因此这些方法是定义在 Object
类中的,而不是 Thread
类中。它们被用于实现线程间的协作和同步,可以在多个线程之间共享同一个对象的锁来进行通信和控制。
9.volatile原理
- 缓存一致协议保证读到最新值
- 内存屏障防止指令重排(解决指令重排对volatile修饰的变量不会产生影响)
- 保证变量的可见性和有序性,不保证原子性。并通过引入内存屏障,防止volatile附近的变量执行重排。使用了 volatile 修饰变量后,在变量修改后会立即同步到主存中,每次用这个变量前会从主存刷新。
10.Synchronized锁原理?
synchronized同步块使用了monitorenter和monitorexit指令实现同步,这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。
线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁,
而执行monitorexit,就是释放monitor的所有权。
11.能说下Synchronized的效率真的很低吗?
Synchronized在jdk1.6以下效率很低的,因为直接使用了重量级锁,
而到了1.6及以上,经过编译器优化以及jvm的锁优化效率几乎接近到了lock的水平,
但是Synchronized用起来是比较简单,不需要关心锁释放问题,所以大多数场景下可以直接使用Synchronized
JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁
等技术来减少锁操作的开销。
12.能说下Synchronized的升级过程吗?
Synchronized经过优化后,升级步骤如下:偏向锁、无锁、轻量级锁、重量级锁
13.synchronized和volatile比较
- volatile不需要加锁,比synchronized更轻便,不会阻塞线程
- synchronized既能保证可见性、有序性,又能保证原子性,而volatile只能保证可见性、有序性,无法保证原子性
14.能说下Synchronized和Lock的区别吗?
- 底层工作机制不同
synchronized关键字是属于JVM层面实现的,它的底层是通过monitor对象来完成的,
Lock与synchronized不同,它是一个具体的类,它是java api层面的锁 - 使用方式的区别
Synchronized关键字运行后是不需要用户去手动释放锁的,
在synchronized代码执行成功后系统会自动让线程释放对锁的占据。
ReentrantLock锁运行后需要用户手动去释放锁,如若用户没有主动去释放锁,就有可能导致出现死锁现象。
ReentrantLock需要使用lock()和unlock()方法配合try finally语句块来完成。 - 是否可中断
synchronized不能中断,除非抛出异常或者正常运行完成。
ReetrantLock可中断,无影响。 - 是否公平
synchronized是一个非公平锁
ReetrantLock可以实现公平也可以实现非公平 - 是否支持条件唤醒
synchronized不支持多条件唤醒
ReentrantLock可以精确唤醒
15. Java中的锁优化的方法
- jdk1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁开销。
- 减少锁的持有时间:只在必要的时候持有锁,尽量缩短锁的持有时间,从而减少线程阻塞的可能性。
- 减少锁的粒度:使用更细粒度的锁,例如,如果只有一个线程会访问一个对象的某个字段,那么就不需要对整个对象加锁。
- 锁分离:如果一个类有多个独立的操作,那么可以为每个操作使用不同的锁,这样就可以避免不必要的同步。
- 锁粗化:如果一个线程在一段时间内会多次获取和释放同一个锁,那么JVM可能会尝试将这些操作合并为一次。
- 锁消除:如果JVM检测到一段代码中的锁操作是不必要的,那么可能会消除这个锁操作。
- 无锁数据结构:例如,juc包中提供了许多无锁数据结构,如ConcurrentHashMap、CopyOnWriteArrayList等。
- 读写锁:如果一个数据结构的读操作比写操作更频繁,那么使用读写锁可以提高性能。
- 偏向锁和轻量级锁:JVM在1.6之后引入了偏向锁和轻量级锁,优化无竞争的同步代码,有效减少无必要的重量级锁操作。
16.如何根据 CPU 核心数设计线程池线程数量
- IO 密集型:线程中十分消耗Io的线程数*2
- CPU密集型: cpu线程数量
17.yield()和join()区别
yield()调用后线程进入就绪状态,A线程中调用B线程的join() ,则B执行完前A进入阻塞状态
18.有三个线程T1,T2,T3,如何保证顺序执行?
- 设置优先级priority: 在JAVA线程中,通过一个int priority来控制优先级,范围为1-10,其中10最高,默认值为5。
- join() 方法 :Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并行变串行。
19.什么是JMM内存模型?为什么需要JMM?
JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。
- Java 内存模型将内存分为了主内存和工作内存(也称为栈空间)。主内存存放所有的共享变量,所有线程都可以访问。每个线程都有自己的工作内存,存储了该线程使用到的变量的副本,线程对变量的所有操作都必须在自己的工作内存中完成,不能直接操作主存中的变量。操作时,首先将变量从主内存拷贝到自己的工作内存中,然后在自己的工作内存中对变量进行操作,操作完成后再将变量写回主存。不同的线程间也无法直接访问对方的工作内存的变量,线程间的变量值的传递必须通过主内存来完成。
- 在Java中,可以使用synchronized和volatile来保证多线程之间操作的有序性。
- 实现方式有所区别:
volatile关键字会禁止指令重排。synchronized关键字保证同一时刻只允许一条线程操作。
20. i++为什么不是线程安全的?
- 把变量读到cpu缓存
- 操作缓存中的值++
- 写回主内存
- 由于上述三个步骤不是原子性的,所以会导致线程安全问题
21.同步锁的几种方式(锁对象):
- 同步代码块加锁:sync…(obj)
- 同步方法加锁:等价于sync…(this)
- 静态同步方法加锁:等价于sync…(this.getClass())
- 死锁:线程之间互相等待对方释放锁,就产生了死锁,尽量不要同步中嵌套同步。
22.用户线程和守护线程:
- 用户线程:一般是用户创建的,不会随着主线程的终止而终止
- 守护线程:一般是系统创建的,会随着主线的终止而终止,垃圾回收线程就是守护线程,可以使用
Thread::setDaemon方法将用户线程转化为守护线程
23.什么是CAS锁,有什么缺点?
- CAS锁可以保证原子性,思想是更新内存时会判断内存值是否被别人修改过,如果没有就直接更新。如果被修改,就重新获取值,直到更新完成为止。
- 缺点
(1)只能支持一个变量的原子操作,不能保证整个代码块的原子操作
(2)CAS频繁失败导致CPU开销大
(3)ABA问题:如果另一个线程修改V值假设原来是A,先修改成B,再修改回成A。当前线程的CAS操作无法分辨当前V
值是否发生过变化。可以通过版本号或时间戳解决
24.什么是AQS及工作原理?
AQS即队列同步器,是juc的locks包下组件的基础框架,AQS维护了一个volatile int类型的变量state表示当前同步状态。当state>0时表示当前已有线程获取到了资源,当state = 0时表示释放了资源,如果获取不到资源就将当前线程加入队列,通过自旋的方式重复尝试获取资源。
- 工作原理
AQS内部维护着一个FIFO队列 先进先出,该队列就是CLH同步队列。
node对应的是被阻塞的线程,head,tail,这两个变量的操作包括入列操作都是cas原子操作。但是出列并不是cas,因为独享方式下只有一个线程获取到了state状态。
CLH队列入列非常简单,就是tail指向新节点、新节点的prev指向当前最后的节点,当前最后一个节点的next指向当前节点。
出列:CLH同步队列遵循FIFO,首节点的线程释放同步状态后,将会唤醒它的后继节点(next),而后继节点将会在获取同步状态成功时将自己设置为首节点。
- AQS定义两种资源共享方式:
Exclusive(独占,只有一个线程能执行,如ReentrantLock)
Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)
25.J.U.C之并发工具类
- CyclicBarrier的使用 eg:所有运动员准备好才可以起跑
同步屏障,当线程到达同步屏障会被阻塞,最后一个到达的线程会唤醒其他被阻塞的线程同时运行
CyclicBarrier(int num,Runable runbale):第一个参数是同步屏障拦截线程的数量,当拦截的线程数量到达同步数,就让最后一个线程执行runbale的run方法,同时唤醒其他线程执行。 - CountDownLatch的使用 eg:谁最后一个走谁关门
CountDownLatch(int num):通过CountDownLatch#await()阻塞线程,如果阻塞的num为0,就执行不阻塞,如果不为0就阻塞了,要等到其他线程通过countDown()减少num,减到0了会唤醒所有被阻塞的线程去执行,一般被框架用来做线程之间的调度。 - Semaphore的使用 eg:停车场等候空位
信号量Semaphore(int count,boolean fair):内部是通过实现aqs的sync内部类实现的,用来控制同时访问资源的线程数量,也就是Semaphore#acquire()尝试获取资源,获取资源的过程其实就是对state减1的过程,如果获取成功了就继续执行,如果获取失败了阻塞着等待其他线程释放了许可之后(把state+1),该线程可以继续尝试获取到许可执行。
三、JVM篇
1.JVM运行时数据区(内存结构)
- 线程私有区:
1.虚拟机栈:是一个栈结构,每次调用方法都会在虚拟机栈中产生一个栈帧(栈帧入栈),每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后栈帧出栈
2.本地方法栈:本地方法栈服务的对象是JVM执行的native方法,hotspot把它和虚拟机栈合并成了1个
3.程序计数器:存储当前线程执行的字节码的偏移量;各线程之间独立存储,互不影响。 - 线程共享区:
4.堆内存:Jvm进行垃圾回收的主要区域,存放对象信息,分为新生代和老年代,内存比例为1:2,新生代的Eden区内存不够时时发生MinorGC,老年代内存不够时发生FullGC
5.方法区:存放类信息、静态变量、常量、运行时常量池等信息。JDK1.8之前用持久代实现,JDK1.8后用元数据空间实现,元空间使用的是本地内存,而非在JVM内存结构中。
2.JVM的内存模型
- JDK1.7的堆内存模型
- Young 年轻代
Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中,Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在Eden区间变满的时候, GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到Tenured区间 - Tenured 年老区
Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。 - Perm 永久区
Perm代主要保存class,method,filed对象,这部份的空间一般不会溢出,除非一次性加载了很多的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到java.lang.OutOfMemoryError :PermGen space 的错误,造成这个错误的很大原因就有可能是每次都重新部署,但是重新部署后,类的class没有被卸载掉,这样就造成了大量的class对象保存在了perm中,这种情况下,一般
重新启动应用服务器可以解决问题。
Virtual区:
最大内存和初始内存的差值,就是Virtual区。 - JDK1.8的堆内存模型
jdk1.8的内存模型是由2部分组成,年轻代 + 年老代。
年轻代:Eden + 2*Survivor
年老代:OldGen
在jdk1.8中变化最大的Perm区,用Metaspace(元数据空间)进行了替换。
3.为什么要废弃1.7中的永久区?
移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。
由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError:PermGen。
4.JVM有哪些垃圾回收算法?
- 引用计数法:运行时根据对象的计数器是否为0,就可以直接回收
缺点:每次对象被引用时,都需要去更新计数器,有一点时间开销;浪费CPU资源;无法解决循环引用问题(最大的缺点) - 标记清除算法: 标记不需要回收的对象,然后清除没有标记的对象,会造成许多内存碎片。效率较低,标记和清除两个动作都需要遍历所有的对象,并且在GC时,需要停止应用程序STW
- 标记压缩算法:和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的清理未标记的对象,而是将存活的对象压缩到内存的一端,然后清理边界以外的垃圾,从而解决了碎片化的问题。其效率也有有一定的影响
- 复制算法: 将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。用在新生代
- 分代算法: 在jvm中,年轻代适合使用复制算法,老年代适合使用标记清除或标记压缩算法。
5.垃圾回收器
- 串行垃圾收集器:是指使用单线程进行垃圾回收,垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停,
等待垃圾回收的完成。这种现象称之为STW(Stop-The-World)。 - 并行垃圾收集器:将单线程改为了多线程进行垃圾回收,这样可以缩短垃圾回收的时间
- ParallelGC垃圾收集器:(jdk8默认垃圾收集器)ParallelGC收集器工作机制和ParNewGC收集器一样,只是在此基础之上,新增了两个和系统吞吐量相关的参数,使得其使用起来更加的灵活和高效。
- CMS:是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的
- G1 :JDK1.9以后的默认垃圾回收器,注重响应速度,支持并发,采用标记整理+复制算法回收内存,使用可达性分析法来判断对象是否可以被回收。
G1的设计原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优:
第一步,开启G1垃圾收集器
第二步,设置堆的最大内存
第三步,设置最大的停顿时间
G1中提供了三种模式垃圾回收模式,Young GC、Mixed GC 和 Full GC,在不同的条件下被触发。
Young GC:主要是对Eden区进行GC,它在Eden空间耗尽时会被触发
Mixed GC:当越来越多的对象晋升到老年代old region时,会触发Mixed GC,该算法并不是一个Old GC,除了回收整个Young Region,还会回收一部分的Old Region
Full GC:显式调用System.gc方法(建议JVM触发)。老年代空间不足,引起Full GC - ZGC收集器:它是JDK11中新引入的一种基于标记-整理算法实现的,以低延迟为首要目标的一款并发的垃圾收集器,可以在不超过10ms的停顿时间内进行全堆垃圾回收,适用于超大型的内存应用场景。
6.什么情况下会内存溢出?
- 堆内存溢出:(1)当对象一直创建而不被回收时(2)加载的类越来越多时(3)虚拟机栈的线程越来越多时
- 栈溢出:方法调用次数过多,一般是递归不当造成
7.什么是双亲委派机制?为什么会有这种机制?
what:
(1)当加载一个类时,先判断此类是否已经被加载,如果类已经被加载则返回;
(2)如果类没有被加载,则先委托父类加载(父类加载时会判断该类有没有被自己加载过),如果父类加载过则返回;如果没被加载过则继续向上委托;
(3)如果一直委托都无法加载,子类加载器才会尝试自己加载
why:
1.为了避免类重复加载(确保Class对象的唯一性)以及JVM的安全性。
2.保证了java核心api不被篡改
双亲委派模型的实现在: ClassLoader.loadClass() 方法中:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
首先,检测是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
//如果没有加载,开始按如下规则执行:
long t0 = System.nanoTime();
try {
if (parent != null) {
//重点!父加载器不为空则调用父加载器的loadClass
c = parent.loadClass(name, false);
} else {
//没有父加载器也会先让Bootstrap加载器去加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//父加载器没有找到,则调用findclass,自己查找并加载
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
8.JVM中有哪些引用?
- 强引用:new的对象。哪怕内存溢出也不会回收
- 软引用:只有内存不足时才会回收
- 弱引用:每次垃圾回收都会回收
- 虚引用:必须配合引用队列使用,一般用于追踪垃圾回收动作
9.类加载过程
- 加载 :把字节码通过二进制的方式转化到方法区中的运行数据区
一个Java文件从编码完成到最终执行,一般主要包括两个过程:编译和运行,其中编译就是把我们写好的java文件,通过javac
命令编译成字节码.class文件,然后运行则是把编译声称的.class文件交给Java虚拟机(JVM)执行。而我们所说的类加载过程即是指JVM虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程。 链接
- 验证:验证字节码文件的正确性。
1、文件格式验证(版本号,是不是CAFEBABYE开头,…)
2、元数据验证(验证属性、字段、类关系、方法等是否合规)
3、字节码验证
4、符号引用验证 - 准备:为 static 变量分配空间,设置默认值。(注意是对应类型的初始值,赋具体值在后面的初始化阶段)
final类型的变量在编译时已经赋值了 - 解析:将常量池中的符号引用(如类的全限定名)解析为直接引用(类在实际内存中的地址)
- 初始化 :执行类构造器(不是常规的构造方法),为静态变量赋初值并初始化静态代码块。
10.JVM类初始化顺序
父类静态代码块和静态成员变量->子类静态代码块和静态成员变量->父类代码块和普通成员变量->父类构造方法->子类代码块和普成员变量->子类构造方法
11.对象的创建过程
(1)检查类是否已被加载,没有加载就先加载类
(2)为对象在堆中分配内存
(3)初始化,将对象中的属性都分配0值或null
(4)设置对象头
(5)为属性赋值和执行构造方法
12.对象头中有哪些信息
对象头中有两部分,一部分是MarkWork,存储对象运行时的数据,如对象的hashcode、GC分代年龄、GC标记、锁的状态、获取到锁的线程ID等;另外一部分是类型指针,指向类元数据局 InstanceKlass,确定该对象所属的类型,如果是数组,还有一个部分存放数组长度
13.JVM内存参数
-Xmx[]:堆空间最大内存
-Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的
-Xmn[]:新生代的最大内存
-xx:[survivorRatio=3]:eden区与from+to区的比例为3:1,默认为4:1
-xx[use 垃圾回收器名称]:指定垃圾回收器
-xss:设置单个线程栈大小
一般设堆空间为最大可用物理地址的百分之80
JVM调优
- 简约版:
1.调整JVM内存参数,如堆内存大小、栈大小等。
2.分析并减少GC(垃圾回收)的频率和时间,如选择合适的GC算法、调整GC相关参数等。
3.优化类加载,尽可能减少类加载次数和时间,如使用预编译类、避免动态生成类等。
4.优化线程池的使用,如合理设置线程数量、避免死锁等。
5.诊断和解决内存泄漏问题,如通过分析dump文件等找出占用过多内存的对象和代码。
6.使用性能分析工具进行监测和分析,如VisualVM、JProfiler等。
以上是JVM调优中的一些常见步骤和方法,需要结合具体情况进行实际操作。
Java虚拟机(JVM)调优是指通过对JVM参数、程序代码、数据结构、算法等进行调整,以提升应用程序的性能、降低资源消耗、提高系统稳定性的一种技术手段。以下是进行JVM调优时需要关注的主要方面和一些常用方法: - 实操版:
-
理解应用需求与性能指标:
- 确定应用的业务场景、并发用户数、响应时间要求、吞吐量等关键指标。
- 诊断和解决内存泄漏问题:通过分析dump文件等找出占用过多内存的对象和代码。
- 使用监控工具(如JDK自带的
jconsole
、jvisualvm
,或第三方工具如VisualVM
、Grafana
搭配Prometheus
等)收集系统运行时的各项性能数据,包括CPU使用率、内存占用、GC频率与耗时、线程状态、堆栈信息等。
-
JVM参数配置:
- 堆内存设置:通过
-Xms
(初始堆大小)和-Xmx
(最大堆大小)调整堆内存大小。一般保持两者相等以避免堆扩展带来的额外开销。如果堆太小,程序可能会频繁地进行GC,导致程序变慢;如果堆太大,会占用过多的内存资源。 - 垃圾回收器选择:根据应用特性和对响应时间、吞吐量的需求选择合适的垃圾回收器(如
Parallel GC
、CMS
、G1
、ZGC
或Shenandoah
)。并可能需要调整相关参数(如-XX:MaxTenuringThreshold
、-XX:InitiatingHeapOccupancyPercent
等)。 - 元空间与栈设置:通过
-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
控制元空间大小,用-Xss
设置每个线程的栈大小。 - 其他参数:如
-XX:SurvivorRatio
(新生代中Eden区与Survivor区比例)、-XX:NewRatio
(老年代与新生代比例)、-XX:+UseCompressedOops
(开启对象指针压缩)等。
- 堆内存设置:通过
-
代码优化:
- 减少对象创建:避免不必要的临时对象创建,利用对象池复用对象,合理设计数据结构减少冗余对象。
- 减少内存泄漏:及时释放不再使用的对象引用,避免长生命周期对象持有短生命周期对象引用导致内存无法释放。
- 合理使用集合类:根据数据特性和访问模式选择合适的数据结构(如ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等),避免过度扩容。
- 使用并发容器与工具类:在多线程环境下,使用
ConcurrentHashMap
、CopyOnWriteArrayList
等线程安全集合类,以及Semaphore
、CyclicBarrier
、CountDownLatch
等并发控制工具。 - 避免锁竞争:合理设计数据结构和算法减少锁粒度,使用
Lock
接口提供的更细粒度的锁(如ReentrantLock
、StampedLock
),或考虑无锁数据结构(如Atomic
系列类)。 - 优化代码:通过优化算法、减少循环次数等方式,减少CPU的使用率。
- 线程数调优:JVM中的线程数也会影响程序的性能和稳定性。如果线程数过多,会导致竞争和上下文切换的开销增加,从而影响程序的性能;如果线程数过少,会导致程序并发性能的下降。因此,需要根据程序的特点和需求进行线程数的合理调整。
-
数据库与I/O优化:
- SQL优化:编写高效SQL语句,避免全表扫描,合理使用索引,减少数据库连接的创建与关闭。
- 缓存策略:使用缓存(如
Redis
、Memcached
)存储热点数据,减轻数据库压力。 - 批量操作与异步处理:批量处理数据库操作,利用消息队列实现异步处理以提高系统响应速度。
-
监控与分析:
- 使用 profiling 工具:如
JProfiler
、YourKit
等进行CPU、内存、线程等深度剖析,找出性能瓶颈。 - 分析GC日志:通过
-XX:+PrintGCDetails
、-XX:+PrintGCDateStamps
等参数输出GC日志,分析GC频率、暂停时间、晋升老年代对象数量等,判断是否存在内存问题。 - 监控JVM详细信息:使用
jstat
、jmap
、jstack
等命令行工具获取JVM运行时状态。
- 使用 profiling 工具:如
-
JIT编译器调优:JIT编译器是JVM的另一个核心组件。通过调整JIT编译器的优化级别和参数,可以提高程序的执行效率和响应速度。
JVM调优是一个持续的过程,需要结合实际应用情况、监控数据和性能测试结果不断调整优化。同时,应遵循“二八原则”,优先解决影响最大的性能瓶颈,避免过度优化。
四、Mysql数据库
1.MyIsAm和InnoDB的区别
- 事务:InnoDB支持事务,MyISAM不支持
- 锁: InnoDB支持行级锁,MyISAM支持表锁
- 索引:
InnoDB不支持全文索引,MyISAM支持;
InnoDB是聚集索引,使用B+Tree作为索引结构,叶子节点就是数据文件。
MyISAM是非聚集索引,也是使用B+Tree作为索引结构,索引和数据文件是分离的,索引保存的是数据文件的指针
InnoDB表必须有唯一索引(如主键)(没有指定会自己找/生产一个隐藏列Row_id来充当默认主键),而MyISAM可以没有 - 外键: InnoDB支持外键,而MyISAM不支持
- 适用场景:
- 是否要支持事务,如果要请选择innodb,如果不需要可以考虑MyISAM;
- 如果表中绝大多数都只是读查询,可以考虑MyISAM,如果既有读也有写,请使用InnoDB。
- MyISAM恢复起来更困难,能否接受;
2.Mysql事务特性
- 分别是原子性、一致性、隔离性、持久性。
- 1.原子性(Atomicity) 由undolog日志保证,他记录了需要回滚的日志信息,回滚时撤销已执行的sql
原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。 - 2.一致性(Consistency)由其他三大特性共同保证,是事务的目的
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。举例来说,假设用户A和用户B两者的钱加起来一共是1000,那么不管A和B之间如何转账、转几次账,事务结束后两个用户的钱相加起来应该还得是1000,这就是事务的一致性。 - 3.隔离性(Isolation) 由MVCC保证
隔离性是当多个用户并发访问数据库时,比如同时操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。关于事务的隔离性数据库提供了多种隔离级别,稍后会介绍到。 - 4.持久性(Durability) 由redolog日志和内存保证,mysql修改数据时内存和redolog会记录操作,宕机时可恢复
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的
3.三大范式
- 第一范式:每一列都是不可分割的原子数据项。
- 第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。
- 第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。
4.事务的隔离级别
- 读未提交: 允许一个事务读取另一个事务已提交的数据,可能出现不可重复读,幻读。
- 读提交: 只允许事务读取另一个事务没有提交的数据可能出现不可重复读,幻读。
- 可重复读: 确保同一字段多次读取结果一致,可能出现幻读。MySQL默认级别
- 串行化: 所有事务逐次执行,没有并发问题
Inno DB 默认隔离级别为可重复读级别,分为快照度和当前读,并且通过间隙锁解决了幻读问题。
5.MySQL有哪些索引
- 主键索引:一张表只能有一个主键索引,主键索引列不能有空值和重复值
- 唯一索引:唯一索引不能有相同值,但允许为空
- 普通索引:允许出现重复值
- 组合索引:对多个字段建立一个联合索引,减少索引开销,遵循最左匹配原则
- 全文索引:myisam引擎支持,通过建立倒排索引提升检索效率,广泛用于搜索引擎
6.聚簇索引和非聚簇索引的区别
- 聚簇索引:聚簇索引的叶子节点存放的是主键值和数据行;辅助索引(在聚簇索引上创建的其它索引)的叶子节点存放的是主键值或指向数据行的指针。
优点:根据索引可以直接获取值,所以他获取数据更快;对于主键的排序查找和范围查找效率更高;
缺点:如果主键值很大的话,辅助索引也会变得很大;如果用uuid作为主键,数据存储会很稀疏;修改主键或乱序插入会让数据行移动导致页分裂;所以一般我们定义主键时尽量让主键值小,并且定义为自增和不可修改。 - 非聚簇索引(辅助索引):叶子节点存放的是数据行地址,先根据索引找到数据地址,再根据地址去找数据
7.MySQL整个查询的过程
(1)客户端向 MySQL 服务器发送一条查询请求
(2)服务器首先检查查询缓存,如果命中缓存,就从缓存中返回结果。否则进入下一阶段
(3)服务器进行 SQL 解析、预处理、
(4)查询优化器优化查询,生成对应的执行计划
(5)执行引擎调用存储引擎API执行查询
(6)将结果返回给客户端,同时缓存查询结果
注意:只有在8.0之前才有查询缓存,8.0之后查询缓存被去掉了
8.哪些情况索引会失效/Mysql调优手段?
(1)where条件中有or,除非所有查询条件都有索引,否则失效,or改union效率更高
(2)like查询用%开头
(3)避免在where子句中对字段进行函数计算操作
(4)违背最左匹配原则
(5)字符串不加单引号索引失效 (隐式类型转换)
(6)在使用不等于(!= 或者<>)的时候无法使用索引会导致全表扫描
(7)is not null 也无法使用索引,由于索引字段本身不为空,所以该条件也失效,会造成全表扫描;is null是可以使用索引的
(8)中间有范围查询会导致后面的索引列全部失效
(9)尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致)),减少select * 使用
(10)表中数据是否太大,是不是要分库分表 垂直分表:按字段拆分 明细、描述 水平分表:2个表字段一样 200w数据一个表
(11)用 exists 代替 in 是一个好的选择
(12)in 和 not in 也要慎用,否则会导致全表扫描,对于连续的数值,能用 between 就不要用 in 了
(13)使用复杂查询时,使用关联查询来代替子查询,并且最好使用内连接
(14)分页查询时,如果limit 后面的数字太大,可以使用子查询查出主键,再limit主键后n条数据就能走覆盖索引
(15)在写update语句时,where条件要使用索引,否则会锁会从行锁升级为表锁
(16)主从复制 读写分离
(17) explain分析 执行计划 是否走索引
(18) mysql执行日志分析
9.B树和B+树的区别
-
B树特点:
1.节点排序(节点存储:地址信息+索引+表结构中除了索引外的其他信息)
2.一个节点可以存多个元素,元素也排序 -
B+树的特点:
1.拥有B树的特点
2.叶子节点之间有指针
3.非叶子节点上的元素在叶子节点上都冗余了,也就是叶子节点中存储了所有的元素,并且排好顺序。
4.非节点存储:地址信息+索引、叶子节点存储:索引+表结构中除了索引外的其他信息
在Mysql中一个innodb页就是一个B+树节点,一个innodb页默认16kb,一般情况下,一颗两层的B+树,可以存2000万行左右数据 -
区别:
B树key和value都在节点上。并且叶子节点之间没有关系。
而B+树的非叶子节点没有存value。叶子节点之间有双向指针,有引用链路。 -
查找方式不同,因为B树的key和value都存在节点上,因此在查找过程中,可能不用查找的叶子节点就找到了对应的值。
而B+树需要查找到叶子节点,才能获取值 -
结论:由于B树每个节点都存储了一条记录的所有数据,因此每次IO开销大。
B+树的叶子节点有指针,可以很好的支持全表扫描、范围查找。
10.MySQL中有哪几种锁?
- 1、共享锁:共享锁锁定的资源可以被其他用户读取,但不能修改
- 2、独占锁:独占锁锁定的资源只允许锁定操作的程序使用,其他任何对他的操作都不会被接受,执行增删改命令时,自动会使用独占锁,直到事务结束后才会被释放
- 3、乐观锁:假设不会出现并发问题,每次取数据都认为不会有其他线程对数据进行修改,因此不会上锁,但是更新的时候会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制实现,在数据表中增加一个版本号,表示被修改的次数,当数据被修改时,版本号+1,当线程要更新数据时,读取版本号,提交更新的时候判断刚才读取到的版本号和当前版本号相等时才更新,否则重试,直到更新成功为止
- 4、悲观锁:假设最坏的情况,每次取数据都认为其他线程会修改,所以都会枷锁,synchronized的思想就是悲观锁
11.MySQL插入百万级的数据如何优化?
(1)一次sql插入多条数据,可以减少写redolog日志和binlog日志的io次数(sql是有长度限制的,但可以调整)
(2)保证数据按照索引进行有序插入
(3)可以分表后多线程插入
12.MySQL B+树索引和哈希索引的区别
-
B+树索引
是一种多路径的平衡搜索树,具有如下特点:
1.非叶子节点不保存数据,只保存索引值
2.叶子节点保存所有的索引值和数据
3.同级节点通过指针自小而大顺序链接
4.节点内的数据也是自小而大顺序存放
5.叶子节点拥有父节点的所有信息
优点
由于数据顺序存放,所以无论是区间还是顺序扫描都更快。
非叶子节点不存储数据,因此几乎都能放在内存中,搜索效率更高
单节点中可存储的数据更多,平均扫描I/O请求树更少
平均查询效率稳定(每次查询都从根结点到叶子结点,查询路径长度相同)
缺点
新增数据不是按顺序递增时,索引树需要重新排列,容易造成碎片和页分裂情况。 -
哈希索引
哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从根节点到叶子节点逐级查找,只需一次哈希算法即可立刻定位到相应的位置,速度非常快,具有如下特点:
1.哈希索引建立在哈希表的基础上
2.对于每个值,需要先计算出对应的哈希码(Hash Code),不同值的哈希码唯一
3.把哈希码保存在哈希表中,同时哈希表也保存指向对应每行记录的指针
优点
大量唯一等值查询时,哈希索引效率通常更高。
缺点
哈希索引对于范围查询和模糊匹配查询显得无能为力。
哈希索引不支持排序操作,对于多列联合索引的最左匹配规则也不支持。
哈希索引不支持部分索引列匹配查找,因为哈希索引始终是使用索引列的全部内容来计算哈希值的。
13.索引、组合索引、覆盖索引?
- 组合索引:对表上多个列进行索引,跟创建单个索引的方法一样。
遵循最左匹配原则,如果第一个字段遇到范围查询,就停止后面的匹配。 - 覆盖索引:需查询的字段建立索引,只需扫描一次索树,不需回表查询
参考:https://www.cnblogs.com/s42-/p/13596212.html
覆盖索引
1 A: select id from user_table where name= ‘张三’
2 B: select password from user_table where name= ‘张三’
语句A: 因为 name索引树 的叶子结点上保存有 name和id的值 ,所以通过 name索引树 查找到id后,因此可以直接提供查询结果,不需要回表,也就是说,在这个查询里面,索引name 已经 “覆盖了” 我们的查询需求,我们称为 覆盖索引
语句B: name索引树 上 找到 name=‘张三’ 对应的主键id, 通过回表在主键索引树上找到满足条件的数据
因此我们可以得知,当sql语句的所求查询字段(select列)和查询条件字段(where子句)全都包含在一个索引中(联合索引),可以直接使用索引查询而不需要回表。这就是覆盖索引。
MyBatis一级缓存与二级缓存
- 一级缓存是SqlSession级别的缓存,Mybatis默认开启一级缓存
一级缓存的作用域是同一个SqlSession,在同一个sqlSession中两次执行相同的sql语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。
当一个sqlSession结束后该sqlSession中的一级缓存也就不存在了。 - 一级缓存失效的四种情况
不同的SqlSession对应不同的一级缓存
同一个SqlSession但是查询条件不同
同一个SqlSession两次查询期间执行了增删改操作
同一个SqlSession两次查询期间手动清空了缓存 - 二级缓存是mapper(namespace)级别的缓存
工作机制:一个会话查询一条数据会放到会话的一级缓存中。如果会话关闭了,一级缓存中的数据会保存带到二级缓存中。
Mybatis默认没有开启二级缓存,需要在全局配置文件中开启二级缓存
<setting name="cacheEnabled" value="true"/>
二级缓存(second level cache),全局作用域缓存。二级缓存默认不开启,需要手动配置。MyBatis提供二级缓存的接口以及实现,缓存实现要求POJO实现Serializable接口。二级缓存在SqlSession 关闭或提交之后才会生效
多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession去操作数据库得到数据会存在二级缓存区域,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。
- 清空sqlSession缓存的六种方式 :
① session.clearCache( ) ;
② execute update(增删改) ;
③ session.close( );
④ xml配置 flushCache=“true”;
⑤ rollback;
⑥ commit。
五、spring框架
1.什么是Spring?
Spring框架是一个轻量级的框架,其核心原则是面向接口编程和控制反转(IoC),简化java开发。
1.1:Spring的优点?
- 方便解耦:Spring框架实现了控制反转(IoC)和依赖注入(DI)等特性,可以将对象的创建和依赖关系交给Spring容器管理,从而简化了开发过程。
- AOP编程的支持:Spring提供了面向切面编程(AOP)的支持,可以方便地实现对程序进行权限拦截、运行监控等功能。
- 声明式事务的支持:Spring的事务管理机制可以通过配置来实现,从而实现声明式事务的效果,降低了手动编码的复杂性。
- 方便程序的测试:Spring对Junit4支持,可以通过注解方便地测试Spring程序。
- 方便集成各种优秀框架:Spring可以与各种优秀的开源框架无缝集成,如Struts、Hibernate、MyBatis等。
降低Java EE API的使用难度:Spring提供了许多方便的API和工具类来简化Java EE API的使用,从而降低了使用难度。
1.2.Spring用了哪些设计模式?
- BeanFactory用了工厂模式
- AOP用了动态代理模式
- RestTemplate用来模板方法模式
- SpringMVC中handlerAdaper用来适配器模式
- Spring里的监听器用了观察者模式
2.IOC(控制反转)与DI(依赖注入)理解与区别?
- IOC的意思是控制反转,DI的意思是依赖注入
- 前者以前创建对象的主动权和创建时机是由自己把控的,IOC则是把创建对象并给对象中的属性赋值交由spring工厂管理,从而达到控制反转的目的;
- 而后者则是通过依赖注入的手段让spring工厂来管理对象的创建和属性的赋值 。DI依赖注入:就是在构造某对象时,能将对象依赖的东西自动初始化进去。
3.AOP是什么?
- AOP是面向切面编程,可以将那些与业务不相关但是很多业务都要调用的代码抽取出来,思想就是不侵入原有代码的情况下对功能进行增强。
Spring 的 AOP 实现原理其实很简单,就是通过动态代理实现的。如果我们为 Spring 的某个 bean 配置了切⾯,
那么 Spring 在创建这个 bean 的时候,实际上创建的是这个 bean 的⼀个代理对象,我们后续对 bean 中⽅法的调
⽤,实际上调⽤的是代理类重写的代理⽅法。 - SpringAOP是基于动态代理实现的,动态代理是有两种,一种是jdk动态代理,一种是cglib动态代理;
- Spring默认使用jdk动态代理,当被代理的类没有实现接口时就使用cglib动态代理
3.1如何使用aop自定义日志?
- 第一步:创建一个切面类,把它添加到ioc容器中并添加@Aspect注解
- 第二步:在切面类中写一个通知方法,在方法上添加通知注解并通过切入点表达式来表示要对哪些方法进行日志打印
- 第三步:通过JoinPoint这个参数可以获取当前执行的方法名、方法参数等信息,根据需求在方法进入或结束时打印日志
3.2Spring AOP 中有哪些 Advice 类型?
- 前置通知(Before):在⽬标⽅法被调⽤之前调⽤通知功能;
- 最终通知(After):在⽬标⽅法完成之后调⽤通知,此时不会关⼼⽅法的输出是什么;
- 后置通知(After-returning ):在⽬标⽅法成功执⾏之后调⽤通知;
- 异常通知(After-throwing):在⽬标⽅法抛出异常后调⽤通知;
- 环绕通知(Around):通知包裹了被通知的⽅法,在被通知的⽅法调⽤之前和调⽤之后执⾏⾃定义的⾏为。
- 执⾏顺序(Spring 5.2.7 之前的版本):Around “前处理” > Before > ⽅法执⾏ > Around “后处理” > After >
AfterReturning|AfterThrowing - 执⾏顺序(Spring 5.2.7 开始):Around “前处理” > Before > ⽅法执⾏ > AfterReturning|AfterThrowing >
After > Around “后处理”
4.如何定义一个全局异常处理类?
添加@ControllerAdvice注解,然后定义一些用于捕捉不同异常类型的方法,在这些方法上添加@ExceptionHandler(value = 异常类型.class)和@ResponseBody注解,方法参数是HttpServletRequest和异常类型,然后将异常消息进行处理。
5.BeanFactory 和 ApplicationContext的关系是什么?
Spring 中设计了BeanFactory ApplicationContext 两个接⼝用以表示容器。
- 1.功能上的区别:
BeanFactory是Spring中最底层的接⼝,是IOC的核心,其功能包含了各种Bean的定义、加载、实例化、依赖注入
和⽣命周期的管理。是IOC最基本的功能。我们可以称之为 “低级容器”。
BeanFactorty接⼝提供了配置框架及基本功能,但是⽆法⽀持spring的aop功能和web应⽤。而
ApplicationContext接⼝作为BeanFactory的派生,因⽽提供BeanFactory所有的功能,还在功能上做了扩展。 - 2、加载方式的区别:
BeanFactory是延时加载,也就是说在容器启动时不会注入bean,而是在需要使⽤bean的时候,才会对该bean进⾏加载实例化。如果bean的某个属性没有注⼊,BeanFactory加载不会抛出异常,第⼀次调⽤getBean()⽅法时才会抛出异常。
ApplicationContext 是在容器启动的时候,⼀次性创建所有的bean(单例⾮懒加载),所以运⾏的时候速度相对BeanFactory比较快。(也因为其⼀次性加载的原因,导致占用内存空间,当Bean较多时,影响程序启动的速度) - 3、注册方式的区别
BeanFactory和ApplicationContext都⽀持BeanPostProcessor、BeanFactoryPostProcessor的使⽤
BeanFactory是需要手动注册的。
ApplicationContext 是⾃动注册的
6.说下 FactoryBean 和 BeanFactory有什么区别?
- BeanFactory:是 Bean 的工厂, ApplicationContext 的⽗类,IOC 容器的核⼼,负责⽣产和管理 Bean 对象。
- FactoryBean:是 Bean,可以通过实现 FactoryBean 接⼝定制实例化 Bean 的逻辑,通过代理⼀个Bean对象,对⽅法前后做⼀些操作(通常是⽤来创建⽐较复杂的bean)。
7.Spring循环依赖是什么?spring中是怎么解决循环依赖的?
循环依赖:一个或多个对象实例之间存在直接或间接的依赖关系,这种依赖关系构成了一个环形调用。
Spring框架通过以下策略来解决单例模式下的循环依赖问题:
- 三级缓存机制:
- 一级缓存(singletonObjects):用于存放完全初始化的单例Bean,即已完成实例化、属性填充、初始化的Bean。
- 二级缓存(earlySingletonObjects):用于存放提前曝光的Bean实例,这些实例已经完成了实例化,但尚未完成依赖注入等后续步骤。
- 三级缓存(singletonFactories):存放的是Bean的早期实例工厂,它存储的是生产Bean实例的
ObjectFactory
对象。
注意,Spring对于循环依赖的解决是有条件的:
- 构造器注入不支持循环依赖:因为构造器注入是在实例化阶段完成的,此时Bean还未被放入任何缓存中,因此构造器循环依赖会导致
BeanCurrentlyInCreationException
异常。 - 非单例Bean不支持循环依赖:Spring只管理单例作用域的Bean的循环依赖,对于原型(prototype)作用域的Bean,每次请求都会创建新的实例,因此循环依赖不会被解决。
当Spring遇到循环依赖时,其处理流程大致如下:
- 获取单例 Bean 的时候会通过 BeanName 先去 singletonObjects(一级缓存) 查找完整的Bean,如果找到则直接返回。
- 看对应的 Bean 是否在创建中,如果不在直接返回找不到,如果是,则会去 earlySingletonObjects (二级缓存)查找
Bean,如果找到则返回,否则进行步骤 3 - 去 singletonFactories (三级缓存)通过BeanName 查找到对应的工厂,如果存在单例对象工厂,则通过工厂创建 Bean,并且放置到 earlySingletonObjects 中,移除该beanName对应的单例对象工厂。
如果三个缓存都没找到,则返回 null
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// Quick check for existing instance without full singleton lock
// 1.从单例对象缓存(1级缓存)中获取beanName对应的单例对象
Object singletonObject = this.singletonObjects.get(beanName);
// 2.如果单例对象缓存(1级缓存)中没有,并且该beanName对应的单例bean正在创建中
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 3.从早期单例对象缓存中(二级缓存)获取单例对象(之所称成为早期单例对象,
// 是因为earlySingletonObjects里的对象的都是通过提前曝光的ObjectFactory创建出来的,还未进行属性填充等操作)
singletonObject = this.earlySingletonObjects.get(beanName);
// 4.如果在早期单例对象缓存中(二级缓存)也没有,并且允许创建早期单例对象引用
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
// Consistent creation of early reference within full singleton lock
// 5.从单例工厂缓存中(三级缓存)获取beanName的单例工厂
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
// 再次从二级缓存中获取,重复校验
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
// 6.再从单例工厂缓存中(三级缓存)获取beanName的单例工厂
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 7.如果存在单例对象工厂,则通过工厂创建一个单例对象
singletonObject = singletonFactory.getObject();
// 8.将通过单例对象工厂创建的单例对象,放到早期单例对象缓存中
this.earlySingletonObjects.put(beanName, singletonObject);
// 9.移除该beanName对应的单例对象工厂,因为该单例工厂已经创建了一个实例对象,并且放到earlySingletonObjects缓存了
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
- spring2.6之前默认会解决循环依赖。在spring2.6之后需要通过配置开启解决循环依赖
8.Bean 的作用域
(1)Singleton:一个IOC容器只有一个
(2)Prototype:每次调用getBean()都会生成一个新的对象
(3)request:每个http请求都会创建一个自己的bean
(4)session:同一个session共享一个实例
(5)application:整个serverContext只有一个bean
(6)webSocket:一个websocket只有一个bean
9.Bean 生命周期
-
实例化Bean:当使用构造函数或者工厂方法创建Bean对象时,就进入了创建阶段。
-
Bean属性赋值:在Bean对象创建后,通过setter方法设置Bean的各个属性。
-
初始化Bean
- 当Bean的属性设置完成后,会触发初始化回调方法,进行一些额外的初始化工作。
实现了各种 Aware 通知的⽅法,如 BeanNameAware、BeanFactoryAware、 - ApplicationContextAware 的接⼝⽅法
- 执⾏ BeanPostProcessor 初始化前置⽅法
- 执⾏ @PostConstruct 初始化⽅法,依赖注⼊操作之后被执⾏
- 执⾏⾃⼰指定的 init-method ⽅法
- 执⾏ BeanPostProcessor 初始化后置⽅法
- 当Bean的属性设置完成后,会触发初始化回调方法,进行一些额外的初始化工作。
-
使用Bean:在初始化完成后,Bean对象处于可用状态,可以供应用程序使用。
-
销毁Bean:当Bean对象不再需要时,会触发销毁回调方法,进行资源释放等清理工作,销毁容器的各种⽅法,如 @PreDestroy、DisposableBean 接⼝⽅法、destroy-method
10.Spring 事务原理?
- spring事务有编程式和声明式,我们一般使用声明式,在某个方法上增加@Transactional注解
- 原理是:当一个方法加上@Transactional注解,spring会基于这个类生成一个代理对象并将这个代理对象作为bean,当使用这个bean中的方法时,如果存在@Transactional注解,就会将事务自动提交设为false,然后执行方法,没有异常则提交,有异常则回滚。
11.spring事务失效场景
(1)事务方法所在的类没有加载到容器中
(2)事务方法不是public类型
(3)在同⼀个类中的⽅法直接内部调⽤
(4)spring事务默认只回滚运行时异常,可以用rollbackfor属性设置
(5)业务自己捕获了异常,事务会认为程序正常秩序
12.spring事务的隔离级别
- DEFAULT:默认的事务隔离级别
- READ_UNCOMMITTED:读未提交
- READ_COMMITTED:读已提交
- REPEATABLE_READ:可重复读
- SERIALIZABLE:串行化
13.说一下Spring的事务传播行为
① PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务
② PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
③ PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前 不存在事务,就抛出异常。
④ PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
⑤ PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前 事务挂起。
⑥ PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
⑦ PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则 按REQUIRED属性执行。
14.SpringMVC工作原理
- 前端控制器接收用户请求将请求发送给处理器映射器
- 处理器映射器根据请求url找到具体的handle和拦截器,返回给前端控制器
- 前端控制器调用处理器适配器,执行具体的controller,并将controller返回的ModelAndView返回给前端控制器
- 前端控制器将ModelAndView传给视图解析器,视图解析器解析后返回具体view
- 前端控制器根据view进行视图渲染,返回给用户
15.spring的bean是线程安全的吗?
实际上⼤部分时候 spring bean ⽆状态的(⽐如 dao 类),所有某种程度上来说 bean 也是安全的,但如果 bean
有状态的话(⽐如 view model 对象),那就要开发者⾃⼰去保证线程安全了,最简单的就是改变 bean 的作⽤
域,把“singleton”变更为“prototype”,这样请求 bean 相当于 new Bean()了,所以就可以保证线程安全了。
如果要保证线程安全
1.可以将bean的作用域改为prototype,比如像Model View。
2.采用ThreadLocal来解决线程安全问题。ThreadLocal为每个线程保存一个副本变量,每个线程只操作自己的副本变量。
拓展:mybatis如何实现分页?
Limit,pagehelper,rowbounds
六、SpringCloud微服务
1. 什么是 SpringBoot?
Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,主要是简化了使用Spring 的难度,简省了繁重的配置,提供了各种启动器,使开发者能快速上手。
2. SpringBoot与SpringCloud 区别
- SpringBoot可以单独使用,它不依赖于SpringCloud 。而SpringCloud 必然依赖于SpringBoot,属于依赖关系。
- SpringBoot专注于快速方便的开发单个个体微服务。
- SpringCloud是关注全局的微服务协调整理治理框架,它将SpringBoot开发的一个个单体微服务整合并管理起来,为各个微服务之间提供,配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策竞选、分布式会话等等集成服务
3.Springboot启动流程?
简洁版:
1.SpringApplication类初始化
2.执行SpringApplication类的run方法
2.1获取并启动监听器
2.2构造应用上下文环境
2.3初始化应用上下文
2.4刷新应用上下文前的准备阶段,prepareContext()方法。
2.5刷新应用上下文
2.6发布容器启动完成事件
详细版:
一、SpringBoot启动的时候,会构造一个SpringApplication的实例,构造SpringApplication的时候会进行初始化的工作,初始化的时候会做以下几件事:
1、把参数sources设置到SpringApplication属性中,这个sources可以是任何类型的参数.
2、判断是否是web程序,并设置到webEnvironment的boolean属性中.
3、创建并初始化ApplicationInitializer,设置到initializers属性中 。
4、创建并初始化ApplicationListener,设置到listeners属性中 。
5、初始化主类mainApplicatioClass。
二、SpringApplication构造完成之后调用run方法,启动SpringApplication,run方法执行的时候会做以下几件事:
1、构造一个StopWatch计时器,用来记录SpringBoot的启动时间 。
2、初始化监听器,获取SpringApplicationRunListeners并启动监听,用于监听run方法的执行。
3、创建并初始化ApplicationArguments,获取run方法传递的args参数。
4、创建并初始化ConfigurableEnvironment(环境配置)。封装main方法的参数,初始化参数,写入到 Environment中,发布 ApplicationEnvironmentPreparedEvent(环境事件),做一些绑定后返回Environment。
5、打印banner和版本。
6、构造Spring容器(ApplicationContext)上下文。先填充Environment环境和设置的参数,如果application有设置beanNameGenerator(bean)、resourceLoader(加载器)就将其注入到上下文中。调用初始化的切面,发布ApplicationContextInitializedEvent(上下文初始化)事件。
7、SpringApplicationRunListeners发布finish事件。
8、StopWatch计时器停止计时,日志打印总共启动的时间。
9、发布SpringBoot程序已启动事件(started())
10、调用ApplicationRunner和CommandLineRunner
11、最后发布就绪事件ApplicationReadyEvent,标志着SpringBoot可以处理就收的请求了(running())
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.sources = new LinkedHashSet();
this.bannerMode = Mode.CONSOLE;
this.logStartupInfo = true;
this.addCommandLineProperties = true;
this.addConversionService = true;
this.headless = true;
this.registerShutdownHook = true;
this.additionalProfiles = new HashSet();
this.isCustomEnvironment = false;
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = this.deduceMainApplicationClass();
}
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
this.configureHeadlessProperty();
SpringApplicationRunListeners listeners = this.getRunListeners(args);
listeners.starting();
Collection exceptionReporters;
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
this.configureIgnoreBeanInfo(environment);
Banner printedBanner = this.printBanner(environment);
context = this.createApplicationContext();
exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
this.refreshContext(context);
this.afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
(new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
}
listeners.started(context);
this.callRunners(context, applicationArguments);
} catch (Throwable var10) {
this.handleRunFailure(context, var10, exceptionReporters, listeners);
throw new IllegalStateException(var10);
}
try {
listeners.running(context);
return context;
} catch (Throwable var9) {
this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
throw new IllegalStateException(var9);
}
}
4.springboot自动配置原理
- 1.通过注解@SpringBootApplication=>@EnableAutoConfiguration=>@Import({AutoConfigurationImportSelector.class})实现自动装配
- 2.AutoConfigurationImportSelector类中重写了ImportSelector中selectImports方法,批量返回需要装配的配置类
- 3.通过Spring提供的SpringFactoriesLoader机制,扫描classpath下META-INF/spring.factories文件,读取需要自动装配的配置类
- 4.依据条件筛选的方式,把不符合的配置类移除掉,最终完成自动装配
5.Spring Cloud和dubbo区别?
- 通信方式
Dubbo 使用的是 RPC 通信;Spring Cloud 使用的是 HTTP RestFul 方式。 - 注册中心
Dubbo 使用 ZooKeeper(官方推荐),还有 Redis、Multicast、Simple 注册中心,但不推荐。;
Spring Cloud 使用的是 Spring Cloud Netflflix Eureka。 - 监控
Dubbo 使用的是 Dubbo-monitor;Spring Cloud 使用的是 Spring Boot admin。 - 断路器
Dubbo 在断路器这方面还不完善,Spring Cloud 使用的是 Spring Cloud Netflflix Hystrix。
分布式配置、网关服务、服务跟踪、消息总线、批量任务等。 Dubbo 不完善,而 Spring Cloud 都有相应的组件来支撑。
6.dubbo的工作原理
- Container服务容器负责启动,加载以及运行Provider服务提供者
- Provider服务提供者启动时,需要将自身暴露出去让远程服务器可以发现,同时向Registry注册中心注册自己提供的服务
- Consumer服务消费者启动时,向Registry注册中心订阅所需要的服务
- Registry注册中心返回服务提供者列表给消费者,同时如果发生变更,注册中心将基于长连接推送实时数据给消费者
- 服务消费者需要调用远程服务时,会从提供者的地址列表中,基于负载均衡算法选出一台提供者服务器进行调用,如果调用失败,会基于集群容错策略进行调用重试
- 服务消费者与提供者会在内存中统计调用次数和调用时间,然后通过定时任务将数据发送给Monitor监控中心
7.springcloud常用组件?
常用组件有服务治理Eureka、配置中心config、远程调用feign、负载均衡Ribbon、服务熔断Hystrix、网关gateway
8.RestTemplate怎么实现负载均衡的?
在使用了@LoadBalanced后,Spring容器在启动的时候会为被修饰过的RestTemplate添加拦截器,拦截器里会使用LoadBalanced相关的负载均衡接口来处理请求,通过这样一个间接的处理,会使原来的RestTemplate变得不是原来的RestTemplate了,具备了负载均衡的功能
9.Feign跟RestTemplate的区别
FeignClient简化了请求的编写,且通过动态负载进行选择要使用哪个服务进行消费,而这一切都由Spring动态配置实现RestTemplate还需要写上服务器IP这些信息等
10.熔断限流的理解?
- SprngCloud中用Hystrix组件来进行降级、熔断、限流
- 熔断是对于消费者来讲,当对提供者请求时间过久时为了不影响性能就对链接进行熔断,
- 限流是对于提供者来讲,为了防止某个消费者流量太大,导致其它更重要的消费者请求无法及时处理。
- 限流可用通过拒绝服务、服务降级、消息队列延时处理、限流算法来实现
11.常用限流算法
- 计数器算法:使用redis的setnx和过期机制实现
- 漏桶算法:一般使用消息队列来实现,系统以恒定速度处理队列中的请求,当队列满的时候开始拒绝请求。
- 令牌桶算法:计数器算法和漏桶算法都无法解决突然的大并发,令牌桶算法是预先往桶中放入一定数量token,然后用恒定速度放入token直到桶满为止,所有请求都必须拿到token才能访问系统
- 滑动窗口算法
限流组件:sentinel
12.如何自定义Starter
参考链接:Springboot自定义Starter启动器
1.引入POM依赖;
2.配置和配置文件对应的xxxProperties类;
3.配置业务类;
4.配置自动配置xxxAutoConfiguration类;
5.配置spring.factories文件;
13.各自注册中心比较
七、redis篇
1.redis数据类型及应用场景
- String(字符串)类型 key/ value
应用场景:1.缓存结构体信息 2.incr命令实现自增或自减的计数器 3.分布式锁 - Hash 对象的键是一个字符串类型,值是一个键值对集合。
应用场景:该类型非常适合于存储对象的信息(结构体信息)。如一个用户有姓名,密码,年龄等信息,购物车。 - List 可以向Redis列表的头部或尾部添加元素。
应用场景:1、消息队列 2、list可用于秒杀抢购场景 - Set 无序集合,集合成员是唯一的。
应用场景:共同关注的人、共同喜好、朋友圈点赞,可能认识的人 - Zset (有序集合类型)
Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。每个元素都会关联一个double类型的分数。
应用场景:1、热搜 2.排行榜 - HyperLogLog 是一种算法,它提供了不精确的去重计数方案。可以用来统计网站的登陆人数以及其他指标
- Geo主要用来存储地理位置信息
- Bitmap 通常用于处理一些状态、计数等需求。统计网站活跃用户 统计用户是否在线、用户每个月的签到情况
2.redis为什么快?
- 纯内存操作
- 数据结构简单,对数据操作简单
- redis执行命令是单线程的,避免了上下文切换带来的性能问题,也不用考虑锁的问题
- 采用了非阻塞的io多路复用机制,使用了单线程来处理并发的连接;内部采用的epoll+自己实现的事件分离器
redis执行命令是单线程的,Redis6.0 以后的版本里,网络 IO 的部分变为了多线程处理,以提高性能。
3.redis持久化机制
- RDB(Redis DataBase)持久化是一种基于快照的持久化方式。在指定的时间间隔内,如果满足一定条件(如某段时间内发生的写操作次数),Redis会生成一个包含当前内存数据的RDB文件。这个RDB文件可以用于数据恢复或备份。
- AOF(Append Only File)持久化是一种基于日志的持久化方式。Redis将所有的写操作命令记录到一个AOF文件中。当Redis重新启动时,可以通过重放AOF文件中的命令来恢复数据。AOF持久化提供了更高的数据安全性,可以保证数据的完整性。然而,与RDB持久化相比,AOF文件通常较大,数据加载速度较慢。
- 混合持久化(RDB + AOF)
混合持久化结合了RDB持久化和AOF持久化的优点,可以在保证数据安全性的同时,提供较快的数据加载速度。
在这种持久化方式下,Redis会同时生成RDB文件和AOF文件。当Redis重新启动时,优先使用AOF文件恢复数据,以确保数据的完整性。混合持久化适用于对数据安全性和性能要求较高的场景。
4.Redis如何实现key的过期删除?
采用的定期过期+惰性过期
定期删除 :Redis 每隔一段时间从设置过期时间的 key 集合中,随机抽取一些 key ,检查是否过期,如果已经过期做删除处理。
惰性删除 :Redis 在 key 被访问的时候检查 key 是否过期,如果过期则删除。
5.Redis缓存穿透如何解决?
缓存穿透是指频繁请求客户端和缓存中都不存在的数据,缓存永远不生效,请求都到达了数据库。
解决方案:
(1)接口校验
(2)对空值进行缓存:找不到的数据也缓存起来,并设置过期时间,可能会造成短期不一致
(3)布隆过滤器:在客户端和缓存之间添加一个过滤器,拦截掉一定不存在的数据请求
6.Redis如何解决缓存击穿?
缓存击穿是值一个key非常热点,key在某一瞬间失效,导致大量请求到达数据库
解决方案:
(1)设置热点数据永不过期
(2)使用互斥锁,缺点是性能低
7.Redis如何解决缓存雪崩?
缓存雪崩是值某一时间Key同时失效或缓存服务故障宕机,导致大量请求到达数据库
解决方案:
(1)搭建集群保证高可用
(2)进行数据预热,给不同的key设置随机的过期时间
(3)给缓存业务添加限流降级,通过加锁或队列控制操作redis的线程数量
(4)给业务添加多级缓存
8.Redis分布式锁的实现原理
原理是使用setnx+setex命令来实现,但是会有一系列问题:
(1)任务时常超过缓存时间,锁自动释放。可以使用Redision看门狗解决
(2)加锁和释放锁的不是同一线程。可以在Value中存入uuid,删除时进行验证。但是要注意验证锁和删除锁也不是一个原子性操作,可以用lua脚本使之成为原子性操作
(3)不可重入。可以使用Redision解决(实现机制类似AQS,计数)
(4)redis集群下主节点宕机导致锁丢失。使用红锁解决
9.Redis集群方案
(1)主从模式:一个master节点,多个slave节点,master节点宕机slave自动变成主节点
(2)哨兵模式:在主从集群基础上添加哨兵节点或哨兵集群,用于监控master节点健康状态,通过投票机制选择slave成为主节点
(3)分片集群:主从模式和哨兵模式解决了并发读的问题,但没有解决并发写的问题,因此有了分片集群。分片集群有多个master节点并且不同master保存不同的数据,master之间通过ping相互监测健康状态。客户端请求任意一个节点都会转发到正确节点,因为每个master都被映射到0-16384个插槽上,集群的key是根据key的hash值与插槽绑定
10.Redis集群主从同步原理
主从同步第一次是全量同步:slave第一次请求master节点会根据replid判断是否是第一次同步,是的话master会生成RDB发送给slave
后续为增量同步:在发送RDB期间,会产生一个缓存区间记录发送RDB期间产生的新的命令,slave节点在加载完后,会持续读取缓存区间中的数据
11.Redis缓存一致性解决方案
Redis缓存一致性解决方案主要思考的是删除缓存和更新数据库的先后顺序
分布式读写锁 RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(rwLock);
解决方案是用中间件canal订阅binlog日志提取需要删除的key,然后另写一段非业务代码去获取key并尝试删除,若删除失败就把删除失败的key发送到消息队列,然后进行删除重试。
12.Redis缓存淘汰策略
- volatile-lru:针对设置了过期时间的key,使用LRU算法进行淘汰
- allkeys-lru:针对所有key使用LRU算法进行淘汰 (推荐) Least Recently Used–最近最少使用
- volatile-lfu:针对设置了过期时间的key,使用LFU算法进行淘汰 Least Frequently Used --最不经常使用
- allkeys-lfu:针对所有key使用LFU算法进行淘汰
- volatile-random: 从设置了过期时间的key中随机删除 --随机
- allkeys-random: 从所有key中随机删除
- volatile-ttl:删除生存时间最近的一个键
- noeviction(默认策略):不删除键,返回错误OOM,只能读取不能写入
13.如何解决超卖问题?
mysql排它锁
版本号
Redis放队列单线程扣减库存
redisson分布式锁
14.如何解决高并发问题?
集群,缓存,sql优化,读写分离,分库分表,限流,消息中间键异步处理
15.Redis哨兵模式(了解)
哨兵模式是Redis可用性的解决方案;它由一个或多个 sentinel 实例构成 sentinel 系统;该系统可以监视任意多个主库以及这些主库所属的从库;当主库处于下线状态,自动将该主库所属的某个从库升级为新的主库;客户端来连接集群时,会首先连接 sentinel,通过 sentinel 来查询主节点的地址,然后再连接主节点进行数据交互。当主节点发生故障时,客户端会重新向 sentinel 索要主库地址,sentinel 会将最新的主库地址告诉客户端。通过这样客户端无须重启即可自动完成节点切换。
哨兵模式当中涉及多个选举流程采用的是 Raft 算法
的领头选举方法的实现:sentinel 会以每秒一次的频率向所有节点(其他sentinel、主节点、以及从节点)发送 ping 消息,然后通过接收返回判断该节点是否下线;如果在配置指定 down-after-milliseconds 时间内,sentinel收到的都是无效回复, 则被判断为主观下线;当一个 sentinel 节点将一个主节点判断为主观下线之后,为了确认这个主节点是否真的下线,它会向其他sentinel 节点进行询问,如果收到一定数量的已下线回复,sentinel 会将主节点判定为客观下线,并通过领头 sentinel 节点对主节点执行故障转移;主节点被判定为客观下线后,开始领头 sentinel 选举,需要一半以上的 sentinel 支持,选举领头sentinel后,开始执行对主节点故障转移;从从节点中选举一个从节点作为新的主节点通知其他从节点复制连接新的主节点。若故障主节点重新连接,将作为新的主节点的从节点。
缺点:redis 采用异步复制的方式,意味着当主节点挂掉时,从节点可能没有收到全部的同步消息,这部分未同步的消息将丢失。
如果主从延迟特别大,那么丢失可能会特别多。sentinel 无法保证消息完全不丢失,但是可以通过配置来尽量保证少丢失。
八、分布式事务 分布式锁
1.什么是分布式事务?
分布式事务就是为了保证不同数据库的数据一致性。要么全部成功,要么全部失败。
2.分布式锁
2.1 数据库悲观锁 利用 select … where … for update 排他锁
2.2 数据库乐观锁 基于CAS思想,是不具有互斥性,不会产生锁等待而消耗资源,操作过程中认为不存在并发冲突,只有 update version 失败后才能觉察到。
2.3 Redis分布式锁 基于REDIS的SETNX()、EXPIRE() 、GETSET()方法做分布式锁。 redisson
2.4 zookeeper分布式锁
- zookeeper获取和释放锁原理利用临时节点与 watch 机制。每个锁占用一个普通节点 /lock,当需要获取锁时在 /lock 目录下创建一个临时节点,创建成功则表示获取锁成功,失败则 watch/lock 节点,有删除操作后再去争锁。临时节点好处在于当进程挂掉后能自动上锁的节点自动删除即取消锁。
- zookeeper获取锁的顺序原理:上锁为创建临时有序节点,每个上锁的节点均能创建节点成功,只是其序号不同。只有序号最小的可以拥有锁,如果这个节点序号不是最小的则 watch 序号比本身小的前一个节点 (公平锁)。
3.如何提高系统的并发能力?
系统拆分,缓存,mq,分库分表,读写分离,es
4.分布式有哪些理论?
CAP理论:
C:一致性,这里指的强一致性,也就是数据更新完,访问任何节点看到的数据完全一致
A:可用性,就是任何没有发生故障的服务必须在规定时间内返回合正确结果
P:容灾性,当网络不稳定时节点之间无法通信,造成分区,这时要保证系统可以继续正常服务。提高容灾性的办法就是把数据分配到每一个节点当中,所以P是分布式系统必须实现的,然后需要在C和A中取舍。因此一般是 CP ,或者 AP。
Base 理论:采用适当的方式来使系统达到最终一致性。
5.你知道哪些分布式事务解决方案?
基于XA协议的两阶段提交2pc,三阶段提交3pc,tcc,本地消息表,消息事务,seata
基于XA协议的两阶段提交(2PC)
1 基于XA协议的两阶段提交(2PC)
两阶段提交协议(Two Phase Commitment Protocol)中,涉及到两种角色
一个事务协调者(coordinator):负责协调多个参与者进行事务投票及提交(回滚)
多个事务参与者(participants):即本地事务执行者
总共处理步骤有两个
(1)投票阶段(voting phase):协调者将通知事务参与者准备提交或取消事务,然后进入表决过程。参与者将告知协调者自己的决策:同意(事务参与者本地事务执行成功,但未提交)或取消(本地事务执行故障);
(2)提交阶段(commit phase):收到参与者的通知后,协调者再向参与者发出通知,根据反馈情况决定各参与者是否要提交还是回滚;
如果任一资源管理器在第一阶段返回准备失败,那么事务管理器会要求所有资源管理器在第二阶段执行回滚操作。通过事务管理器的两阶段协调,最终所有资源管理器要么全部提交,要么全部回滚,最终状态都是一致的
优点: 尽量保证了数据的强一致(无法完全保证),适合对数据强一致要求很高的关键领域。
缺点:
同步阻塞问题:执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
单点故障:由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
数据不一致:在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。
二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交
2 三段提交(3PC)
三段提交是两段提交的升级版
CanCommit阶段:询问阶段
类似2PC的准备阶段,协调者向参与者发送CanCommit请求,询问是否可以执行事务提交操作,然后开始等待参与者的响应。
PreCommit阶段:事务执行但不提交阶段
协调者根据参与者的反应情况来决定是否可以进行事务的PreCommit操作:
协调者从所有的参与者获得的反馈都是Yes响应
1.发送预提交请求协调者向参与者发送PreCommit请求;
2.参与者接收到PreCommit请求后,执行事务操作,并将undo(执行前数据)和redo(执行后数据)信息记录到事务日志中;
3.参与者成功的执行了事务操作,则返回ACK(确认机制:已确认执行)响应,同时开始等待最终指令。
有任何一个参与者向协调者发送了No响应,或者等待超时
1.协调者向所有参与者发送中断请求请求。
2.参与者收到来自协调者的中断请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。
doCommit阶段:事务提交阶段
执行提交
1.协调接收到所有参与者返回的ACK响应后,协调者向所有参与者发送doCommit请求。
2.参与者接收到doCommit请求之后,执行最终事务提交,事务提交完之后,向协调者发送Ack响应并释放所有事务资源。
3.协调者接收到所有参与者的ACK响应之后,完成事务。
中断事务
1.协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),协调者向所有参与者发送中断请求;
2.参与者接收到中断请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后,向协调者发送ACK消息,释放所有的事务资源。
3.协调者接收到参与者反馈的ACK消息之后,执行事务的中断。
优点:相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。
缺点:会导致数据一致性问题。由于网络原因,协调者发送的中断响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到中断命令并执行回滚的参与者之间存在数据不一致的情况。
3 TCC补偿机制
TTCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。三个阶段如下:
操作方法 | 含义 |
---|---|
Try | 预留业务资源/数据效验-尝试检查当前操作是否可执行 |
Confirm | 确认执行业务操作,实际提交数据,不做任何业务检查。try成功,confirm必定成功 |
Cancel | 执行业务出错时,需要回滚数据的状态下执行的业务逻辑 |
其核心在于将业务分为两个步骤完成。不依赖事务协调器对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务。
TCC属于应用层的一种补偿方式,需要程序员在实现的时候多写很多补偿的代码,复杂业务场景下代码逻辑非常复杂。
幂等性无法确保。
4 本地消息表(异步确保)
本地消息表这种实现方式应该是业界使用最多的,其核心思想是将分布式事务拆分成本地事务进行处理,这种思路是来源于ebay。我们可以从下面的流程图中看出其中的一些细节:
工作流程:
消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。
优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性。
缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。
5 MQ 事务消息
完整流程图:
优点: 实现了最终一致性,不需要依赖本地数据库事务。
缺点: 目前主流MQ中只有RocketMQ支持事务消息。
- 生产者先发送一条半事务消息到MQ
- MQ收到消息后返回ack确认
- 生产者开始执行本地事务
- 如果事务执行成功发送commit到MQ,失败发送rollback
- 如果MQ长时间未收到生产者的二次确认commit或者rollback,MQ对生产者发起消息回查
- 生产者查询事务执行最终状态
- 根据查询事务状态再次提交二次确认
6.Seata
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
Seata的优点
对业务无侵入:即减少技术架构上的微服务化所带来的分布式事务问题对业务的侵入
高性能:减少分布式事务解决方案所带来的性能消耗(2PC)
AT模式
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:
提交异步化,非常快速地完成。
回滚通过一阶段的回滚日志进行反向补偿。
第一阶段:本地数据备份阶段
1.Seata 的 JDBC 数据源代理通过对业务 SQL 的解析,把业务数据在变化前后的数据镜像组织成回滚日志(XID/分支事务ID(Branch ID/变化前的数据/变化后的数据)。
2.将回滚日志存入一张日志表UNDO_LOG(需要手动创建),并对UNDO_LOG表中的这条数据形成行锁(for update)。
3.若锁定失败,说明有其他事务在操作这条数据,它会在一段时间内重试,重试失败则回滚本地事务,并向TC汇报本地事务执行失败。这样,可以保证:任何提交的业务数据的更新一定有相应的回滚日志存在
第二阶段:全局事务提交/回滚
全局提交:
所有分支事务此时已经完成提交,所有分支事务提交都正常。
TM从TC获知后会决议执行全局提交,TC异步通知所有的RM释放UNDO_LOG表中的行锁,同时清理掉UNDO_LOG表中刚才释放锁的那条数据。
全局回滚:
若任何一个RM一阶段事务提交失败,通知TC提交失败。
TM从TC获知后会决议执行全局回滚,TC向所有的RM发送回滚请求。
RM通过XID和Branch ID找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚,同时释放锁,清除UNDO_LOG表中释放锁的那条数据。
TCC模式
seata也针对TCC做了适配兼容,支持TCC事务方案,原理前面已经介绍过,基本思路就是使用侵入业务上的补偿及事务管理器的
协调来达到全局事务的一起提交及回滚。
AT 模式基于 支持本地 ACID 事务 的 关系型数据库:
一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。
相应的,TCC 模式,不依赖于底层数据资源的事务支持:
一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
二阶段 commit 行为:调用 自定义 的 commit 逻辑。
二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。
Saga 模式
Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
Saga的实现:
基于状态机引擎的 Saga 实现
状态机引擎是无状态的,它是内嵌在应用中。
当应用正常运行时(图中上半部分):
状态机引擎会上报状态到Seata Server;
状态机执行日志存储在业务的数据库中;
当一台应用实例宕机时(图中下半部分):
Seata Server 会感知到,并发送事务恢复请求到还存活的应用实例;
状态机引擎收到事务恢复请求后,从数据库里装载日志,并恢复状态机上下文继续执行;
九、MQ消息中间件
1. 为什么要使用MQ,有什么优缺点?
优点
解耦、异步、削峰。
缺点
增加了系统的复杂度:因为一个系统引入了MQ之后会造成系统的复杂性的提升,复杂性提升后,增加MQ的维护成本。
降低的系统的可用性:复杂性的提升意味这系统可用性的降低,因为MQ一旦出现问题就会造成系统出现问题。
一致性问题:因为MQ是异步处理消息,需要处理类似于消息丢失以及重复消费的问题,一旦处理不好就会造成重复消费问题。
2.消息可靠性
RabbitMQ如何保证消息的可靠性?
生产者 :confirm确认机制
持久化:将队列、交换器和消息都标记为持久化(durable),这样可以保证消息在服务器重启后不会丢失。
消费者 :basicAck机制、死信队列、重试机制、消息补偿机制。
RocketMQ如何保证消息的可靠性?
- 生产者:重试机制 / 发送一条事务消息
- 消息存储:同步刷盘策略+主从复制保证 Broker 的消息可靠性
- 消费者ACK机制:消费者消费完消息后向服务器发送ACK确认,未收到ACK会重新推送。
- 重试机制和死信队列:消费失败时进行重试,重试多次仍失败的消息可以存入死信队列。
RocketMQ 在发送端保证发送消息的可靠性主要就是通过 重试机制 来实现的
生产者发送消息分为了 同步发送 、 异步发送 、单向发送 三种方式:
同步发送 :发送消息后,阻塞线程等待消息发送结果
异步发送 :发送消息后,并不会阻塞等待,回调任务会在另一个线程中执行
单向发送 :发送消息后,立即返回,不返回消息发送是否成功,因此不可以保证发送消息的可靠性
只有单向发送没有消息可靠性的保证,在 同步 和 异步 发送中,都可以通过设置发送消息的 重试次数 来保证发送端的可靠性,默认重试次数为 2 次
消费消息的可靠性
可靠性保证一:消息重试保证可靠性
消费者只有返回 CONSUME_SUCCESS
才算消费完成,如果返回 CONSUME_LATER
则会按照不同的延迟时间再次消费,
如果消费满 16 次之后还是未能消费成功,则会将消息发送到死信队列
可靠性保证二:死信队列保证可靠性
如果消息最终重试消费失败,并不会立即丢弃,而是将消息放入到了死信队列,之后还可以通过 MQ 提供的接口获取对应的消息, 保证消费消息的可靠性
同步、异步刷盘
同步刷盘:当消息持久化到 broker 的磁盘后才算是消息写入成功。
异步刷盘:当消息写入到 broker 的内存后即表示消息写入成功,无需等待消息持久化到磁盘。
2.2RabbitMQ导致的死信的几种原因?
消息被拒( Basic.Reject /Basic.Nack ) 且 requeue = false 。
消息TTL过期。
队列满了,无法再添加。
3.如何保证消息的顺序性?
生产者生产的消息会放置在队列中,基于队列先进先出的特性天然的可以保证存入队列的消息顺序和拉取的消息顺序是一致的.
RabbitMQ:一个queue但是对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理
RocketMQ:生产者投递到同一个队列 ,消费者有序消费。
RockerMQ的MessageListener回调函数提供了两种消费模式,有序消费模式MessageListenerOrderly和并发消费模式MessageListenerConcurrently。在消费的时候,还需要保证消费者注册MessageListenerOrderly类型的回调接口实现顺序消费,如果消费者采用Concurrently并行消费,则仍然不能保证消息消费顺序。
4.为什么会出现重复消费?如何防止消息重复消费?
1.发送方消息重试 RocketMQ:producer.setRetryTimesWhenSendFailed(3);
2 消费方消息重试 RocketMQ:msg.getReconsumeTimes()获取重试次数进行控制
消费端处理消息的业务逻辑保持幂等性
保证每条消息都有唯一编号且保证消息处理成功与去重表的日志同时出现
5.消息积压处理
临时紧急扩容具体操作步骤和思路如下:
先修复 consumer 的问题,确保其恢复消费速度,然后将现有 consumer 都停掉。
新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。
然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。
接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。
等快速消费完积压数据之后,恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。
对于 RocketMQ,官方针对消息积压问题,提供了解决方案。
- 提高消费并行度2. 批量方式消费3. 跳过非重要消息4. 优化每条消息消费过程
增加消费者数量:通过增加消费者的数量来提高消息的处理速度。可以根据系统的负载情况动态地增加或减少消费者的数量。
提高消费者的处理能力:可以通过优化消费者的代码逻辑、提升消费者的性能等方式来提高消费者的处理能力,从而加快消息的消费速度。
增加消息队列的吞吐量:可以通过增加消息队列的并发处理能力来提高消息的处理速度。可以考虑增加队列的分区、增加消息的分发策略等方式来提高消息队列的吞吐量。
设置消息的过期时间:可以设置消息的过期时间,当消息在队列中超过一定时间还未被消费时,可以将其丢弃或进行其他处理
监控和报警:可以通过监控消息队列的积压情况,并及时报警,以便及时发现和解决消息积压的问题。
使用延迟队列:可以使用延迟队列来实现消息的延时处理,将消息发送到延迟队列中,然后在指定的时间后再进行消费
6.如何解决消息队列的延时及过期失效问题?
- 延时队列:可以使用延时队列来处理延时消息。延时队列是一种特殊的消息队列,它可以在指定的时间后将消息重新投递到主队列中。当消息到达延时时间后,将其重新发送到主队列,以便消费者可以处理。
- 过期失效策略:可以为消息设置过期时间,在消息发送时为每个消息设置一个过期时间戳。当消息过期时,可以选择将其丢弃或者进行特殊处理。消费者处理消息时,可以检查消息的过期时间,如果消息已经过期,则可以选择不处理或者进行相应的理。
- 定时任务:可以使用定时任务来处理延时消息。在发送消息时,可以将消息的执行时间记录下来,并使用定时任务来定期检查是否有需要执行的消息。当定时任务触发时,可以将需要执行的消息发送到主队列中,供消费者处理。
- 重试机制:对于延时或过期失效的消息,可以采取重试机制。当消息未能及时处理或者过期失效时,可以将其重新发送到队列中,供消费者再次尝试处理。可以设置最大重试次数,超过次数后可以选择丢弃或者进行其他处理。
7.MQ如何保证分布式事务的最终一致性?
rocketmq支持事务
RabbitMQ:
1、确认生产者一定要将数据投递到MQ服务器中(采用MQ消息确认机制)
2、MQ消费者消息能够正确消费消息,采用手动ACK模式(注意重试幂等性问题)
3、如何保证第一个事务先执行,采用补偿机制,在创建一个补单消费者进行监听,如果订单没有创建成功,进行补单。
8.描述一下消息中间件Ack确认机制?
ACK机制是消费者从RabbitMQ收到消息并处理完成后,反馈给RabbitMQ,RabbitMQ收到反馈后才将此消息从队列中删除。如果一个消费者在处理消息出现了网络不稳定、服务器异常等现象,那么就不会有ACK反馈,RabbitMQ会认为这个消息没有正常消费,会将消息重新放入队列中。
9.MQ延迟消息如何实现?
1.可以采用RabbitMQ的死信队列来实现延时队列,RabbitMQ可以针对Queue和Message设置 x-message-tt,来控制消息的生存时间,如果超时,则消息变为dead letter,dead letter会被投递到死信交换器,然后通过死信队列将消息发送出去
2.RabbitMQ 将消息发送到延迟交换机,消息达到延迟时间会投递到对应的队列
3.RocketMQ 可以实现18个等级的消息延时,但是不可以实现任意时间的消息延时,使用RocketMQ的延时消息只需要按照正常消息发送,并指定延时等级即可,简单高效,并且这个延时时间可以在RocketMQ的配置参数中进行配置。具体的时间可以为1s、 5s、 10s、 30s、 1m、 2m、 3m、 4m、 5m、 6m、 7m、 8m、 9m、 10m、 20m、 30m、 1h、 2h。
10.怎样选型MQ?
11.RocketMQ的事务消息实现原理?
1.生产者先发送一条半事务消息到MQ
2.MQ收到消息后返回ack确认
3.生产者开始执行本地事务
4.如果事务执行成功发送commit到MQ,失败发送rollback
5.如果MQ长时间未收到生产者的二次确认commit或者rollback,MQ对生产者发起消息回查
6.生产者查询事务执行最终状态
7.根据查询事务状态再次提交二次确认
12.RocketMQ Consumer消息分配模式:
集群模式:consumer.setMessageModel(MessageModel.CLUSTERING);
广播模式:consumer.setMessageModel(MessageModel.BROADCASTING);
13.RocketMQ存储
消息主体以及元数据都存储在CommitLog当中;
Consume Queue相当于kafka中的partition,是一个逻辑队列,存储了这个Queue在
CommiLog中的起始offset,log大小和MessageTag的hashCode;
每次读取消息队列先读取consumerQueue,然后再通过consumerQueue去commitLog中拿到
消息主体。
14.RabbitMQ的工作模式
一.simple简单模式
二.WorkQueues工作队列模式(资源的竞争)
三.Publish/Subscribe发布订阅(共享资源)
四.Routing路由模式
五.Topics 主题模式(路由模式的一种)
六.RPC远程调用模式
七.Publisher Confirms发布者确认模式
十、设计模式
设计模式分类
创建型模式:单例模式、工厂方法模式、抽象工厂模式、创建者模式、原型模式。
结构型模式:适配器模式、代理模式、装饰器模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、
访问者模式、中介者模式、解释器模式。
Java设计模式–原则
单一职责原则:一个类只负责一个功能领域中的相应职责
开闭原则:对扩展开放,对修改关闭
里氏代换原则:所有引用基类(父类的)地方都可以用子类来替换,且程序不会有任何的异常。
依赖倒转原则:要针对接口编程,而不是针对实现编程。
接口隔离原则:
1.使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口
2:类间的依赖关系应该建立在最小的接口上
合成复用原则:尽量使用对象组合,而不是继承来达到复用的目的。
以下是常用的设计模式的实战:
参考博文:一文讲完Java常用设计模式(全23种)
单例模式
单例模式(Singleton Pattern)用于确保某个类在整个应用程序中只有一个实例,并提供一个全局访问点来获取该实例。
写法:懒汉式,饿汉式,静态内部类,双重校验锁,枚举
观察者模式
观察者模式又称发布-订阅模式。观察者模式是一种通知机制,多个观察者对某个事物(被观察方)感兴趣时,事物(被观察方)一有动作就通知观察者。观察者模式能让发送通知的一方(被观察方)和接收通知的一方(观察者)彼此分离,减少耦合。
以生活中的微信公众号为例:我们对某个微信公众号感兴趣,关注了这个微信公众号后能收到新的文章的通知,了解最新的资讯。这是一种典型的观察者模式,用户(观察者)关注公众号(被观察者),公众号(被观察者)更新文章通知所有用户(观察者)。
以项目中的实际场景为例:在取消订单的时候,后边要跟一系列操作,比如:增加对应商品的库存、增加对应用户的账户余额。这里用观察者模式就很合适,取消订单这个动作是个被观察者,库存业务和余额业务都是观察者。
策略模式
策略模式的含义:定义一系列可互相替换的算法,并且每个算法独立封装,然后在运行的时候动态替换。
策略模式的核心:一个抽象+多个具体的实现,策略持有类要持有实现类的集合(要用抽象类替代),程序调用时根据类型去策略持有类中找到对应的实现类,然后调用实现类的具体方法。
策略模式的好处有哪些?
添加策略时很方便,只添加一个策略的实现类即可,符合开闭原则(对新增开放,对修改关闭)
逻辑解耦。每个策略负责自己的事情,调用方去选择使用哪个策略。符合单一职责原则。
提高了代码优雅度。调用方减少了对业务的if else的判断,直接给策略传入相应的类型即可。
责任链模式
使多将请求的发送者和接收者进行解耦,使多个接受者都有机会处理请求,这些接收者连成一条链,请求沿着这条链传递,直到有一个对象处理它为止。
还有一种责任链模式是:链上的所有接收者都要处理。比如:员工的转正申请,从小领导到大领导,每个领导都进行审批。
实际项目场景:
用户取消订单时,要按顺序处理:退回商品的库存、退回用户的余额、退回用户的优惠券。
在校验权限时,要按顺序判断:校验token是否过期、校验是否有这个url权限、是否被拉黑。有一个校验不通过则不允许请求。
责任链模式的好处有哪些?
调用者与链上的各个被调用者进行解耦。
各个被调用者只需负责自己的逻辑。(符合单一职责原则)
添加一个被调用者很方便,在链上加一个即可。(符合开闭原则)
提高了代码的可维护性、扩展性。
代理模式
代理模式的含义:使用代理对象来代替对真实对象的访问。
作用是:可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。
适配器模式
适配器模式:将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器。
生活场景:如果去美国,我们随身带的电器是无法直接使用的,因为美国的插座标准和中国不同,所以,我们需要一个适配器。
实际项目场景
SpringMVC支持多种数据,比如:HTTP请求、WebSocket请求。当请求进来时,SprinvMVC就会根据请求去获取适配器,然后去调用HTTP的处理器或者WebSocket的处理器。
建造者模式
含义:将一个对象的构建过程与这个对象分离,使得可以很方便的用构建过程创建不同属性的对象。
建造者模式可以做到:
可以只指定某几个属性
可以肉眼可见地给某个属性赋值,不需要再点进去看构造方法的定义
lombok可以自动生成Builder,方法是:在实体类上加个@Builder即可。
工厂模式
含义:将对象的实例化封装在工厂类中,将对象的创建与使用分离。也就是说:是用工厂类的方法来代替 new 操作的。
工厂模式的优点
降低耦合度,对象的创建与使用分离,使用者无需关心创建对象的细节,符合单一职责原则。
使代码更简洁,易维护
享元模式:
利用共享技术有效地支持大量细粒度的对象,享元模式能够解决重复对象的内存浪费问题。
应用场景是需要缓冲池的场景,比如String常量池、数据库连接池。
模版模式:
是一种基于继承实现的设计模式,它是行为型的模式。就是将某一行为制定一个框架,然后子类填充细节。
迭代器模式:
Java迭代器模式是一种行为设计模式,它提供了一种访问集合对象元素的方法,而不需要暴露该对象的内部表示。该模式适用于需要遍历集合对象的场景,例如数组、列表、树等。 while (iterator.hasNext())
原型模式:
原型设计模式允许通过复制现有对象来创建新对象,而不是通过实例化类来创建新对象。
在需要创建大量相似对象时非常有用,它可以避免重复创建对象,从而提高性能,并且可以根据需要实现浅拷贝或深拷贝。
在Java中,原型模式的实现通常涉及到实现Cloneable接口和重写clone()方法。
外观模式:
外观模式(Facade Pattern)是一种结构型设计模式,它提供了一个简单的接口来访问复杂系统中的子系统,从而隐藏了子系统的复杂性。外观模式属于对象型模式,它通过创建一个外观类,将客户端与子系统解耦,使得客户端只需要与外观类交互即可完成操作。
数据结构与算法
参考:努力的老周
数据算法篇
冒泡排序
冒泡排序(Bubble Sort)是一种简单的排序算法,其基本思想是对待排序的元素从前向后依次比较相邻的两个元素,如果顺序不对则交换它们的位置,一轮比较下来,最大的元素就会“冒泡”到数组的末尾。
重复这个过程,直到没有需要交换的元素为止,排序完成。
public static void bubbleSort(int []arr) {
for(int i =1;i<arr.length;i++) {
for(int j=0;j<arr.length-i;j++) {
if(arr[j]>arr[j+1]) {
int temp = arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
}
}
选择排序
工作原理是每一次从待排序的数据元素中选出最小(最大)的一个元素,存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到全部待排序的数据元素排完。
public class SelectionSortExample {
public static void main(String[] args) {
int[] arr = {5, 1, 9, 3, 7};
selectionSort(arr);
}
public static void selectionSort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
int minIdx = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[minIdx]) {
minIdx = j;
}
}
int temp = arr[i];
arr[i] = arr[minIdx];
arr[minIdx] = temp;
System.out.println(Arrays.toString(arr));
}
}
}
插入排序
维护一个有序区,将数据一个一个插入到有序区的适当位置,直到整个数组都有序。即每步将一个待排序的记录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止。
public class InsertSort {
public static void main(String[] args) {
int[] a = {12, 15, 9, 20, 6, 31, 24};
Sort(a);//调用方法
}
public static void Sort(int[] s) {
for (int i = 1; i < s.length; i++) {//注意i不能等于数组长度,因为数组下标从零开始而不是从1.
int temp = s[i];//存储待插入数的数值
int j;//j必须在此声明,如果在for循环内声明,j就不能拿出for循环使用,最后一步插入就无法执行
for (j = i - 1; j >= 0 && temp < s[j]; j--) {
s[j + 1] = s[j];//如果大于temp,往前移动一个位置
}
s[j + 1] = temp;//将temp插入到适当位置
}
System.out.println(Arrays.toString(s));//输出排好序的数组
}
}
快速排序
划分是快速排序的根本机制。划分数据就是把数组分为两组,使所有关键字大于特定值的数组项在一组,使所有关键字小于特定值的数据在另一组。
左右指针法:
定义一个begin指向第一个元素,定义一个end指向最后一个元素。令第一个元素为key,begin向后找大于key的值,end向前找小于key的值,此时把begin跟end位置的值交换,直到begin大于等于end时结束。
三数据项取中
N个数据项数组的最坏的划分情况是一个子数组只有一个数据项,另外一个子数组包含N-1个数据项
为了避免我们选取的枢纽是数据项中最大或最小的,我们需要一种能够避免且简单的选取办法,办法如下:
- 随机选出一个
- 遍历整个待划分数组,选出最适合当枢纽的数据项
- 取头,中,尾三个元素,已中值为枢纽
归并排序
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
归并排序算法有两个基本的操作,一个是分,也就是把原数组划分成两个子数组的过程。另一个是治,它将两个有序数组合并成一个更大的有序数组。
1.将待排序的线性表不断地切分成若干个子表,直到每个子表只包含一个元素,这时可以认为只包含一个元素的子表是有序表。
2.将子表两两合并,每合并一次,就会产生一个新的且更长的有序表,重复这一步骤,直到最后只剩下一个子表,这个子表就是排好序的线性表。
图解算法
假设我们有一个初始数列为{8, 4, 5, 7, 1, 3, 6, 2},整个归并排序的过程如下图所示。
可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。
合并两个有序数组流程
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。
网路编程(拓展)
1.socket通信流程
(1)服务端创建socket并调用bind()方法绑定ip和端口号
(2)服务端调用listen()方法建立监听,此时服务的scoket还没有打开
(3)客户端创建socket并调用connect()方法像服务端请求连接
(4)服务端socket监听到客户端请求后,被动打开,调用accept()方法接收客户端连接请求,当accept()方法接收到客户端connect()方法返回的响应成功的信息后,连接成功
(5)客户端向socket写入请求信息,服务端读取信息
(6)客户端调用close()结束链接,服务端监听到释放连接请求后,也结束链接
linux系列(拓展)
1.linux常用命令
ifconfig:查看网络接口详情
ping:查看与某主机是否能联通
ps -ef|grep 进程名称:查看进程号
lost -i 端口 :查看端口占用情况
top:查看系统负载情况,包括系统时间、系统所有进程状态、cpu情况
free:查看内存占用情况
kill:正常杀死进程,发出的信号可能会被阻塞
kill -9:强制杀死进程,发送的是exit命令,不会被阻塞
分库分表(拓展)
分库分表是在海量数据下,由于单库、表数据量过大,导致数据库性能持续下降的问题,演变出的技术方案
分库分表共分为四种方式:水平分库、水平分表、垂直分库、垂直分表
执行流程
sql解析→查询优化→sql路由→sql改写→sql执行→结构归并
分库分表最佳实践
系统设计之初就应该对业务数据的耦合进行考量,从而进行垂直分库分表,使数据结构清晰明了,若非必要无需进行水平切分,应从缓存技术着手降低对数据库的访问压力。如果缓存使用过后,数据库访问还是非常大,可以考虑数据库读写分离原则,若数据库压力依然大,且业务数据增长无法估量,最后可以考虑水平分库分表,单表数据控制在1000万以内。
数据库设计规范(拓展)
(一)基础规范
1、表存储引擎必须使用InnoD,表字符集默认使用utf8,必要时候使用utf8mb4
解读:
(1)通用,无乱码风险,汉字3字节,英文1字节
(2)utf8mb4是utf8的超集,有存储4字节例如表情符号时,使用它
2、禁止使用存储过程,视图,触发器,Event
3、禁止在数据库中存储大文件,例如照片,可以将大文件存储在对象存储系统,数据库中存储路径
4、禁止在线上环境做数据库压力测试
5、测试,开发,线上数据库环境必须隔离
(二)命名规范
1、库名,表名,列名必须用小写,采用下划线分隔
2、库名,表名,列名必须见名知义,长度不要超过32字符
3、库备份必须以bak为前缀,以日期为后缀
4、从库必须以-s为后缀
5、备库必须以-ss为后缀
(三)表设计规范
1、单实例表个数必须控制在2000个以内
2、单表分表个数必须控制在1024个以内
3、表必须有主键,推荐使用UNSIGNED整数为主键
4、禁止使用外键,如果要保证完整性,应由应用程式实现
5、建议将大字段,访问频度低的字段拆分到单独的表中存储,分离冷热数据
(四)列设计规范
1、根据业务区分使用tinyint/int/bigint,分别会占用1/4/8字节
2、根据业务区分使用char/varchar
3、根据业务区分使用datetime/timestamp
4、必须把字段定义为NOT NULL并设默认值
5、使用INT UNSIGNED存储IPv4,不要用char(15)
6、使用varchar(20)存储手机号,不要使用整数
7、使用TINYINT来代替ENUM
(五)索引规范
1、唯一索引使用uniq_[字段名]来命名
2、非唯一索引使用idx_[字段名]来命名
3、单张表索引数量建议控制在5个以内
4、组合索引字段数不建议超过5个
5、不建议在频繁更新的字段上建立索引
6、非必要不要进行JOIN查询,如果要进行JOIN查询,被JOIN的字段必须类型相同,并建立索引
7、理解组合索引最左前缀原则,避免重复建设索引,如果建立了(a,b,c),相当于建立了(a), (a,b), (a,b,c)
(六)SQL规范
1、禁止使用select *,只获取必要字段
2、insert必须指定字段,禁止使用insert into T values()
3、隐式类型转换会使索引失效,导致全表扫描
4、禁止在where条件列使用函数或者表达式
5、禁止负向查询以及%开头的模糊查询
6、禁止大表JOIN和子查询
7、同一个字段上的OR必须改写问IN,IN的值必须少于50个
8、应用程序必须捕获SQL异常