1、文章可能会优先更新在Github,个人博客【包括文章纠错与增加内容】。其它平台会晚一段时间。个人博客备用地址
2、如果Github很卡,可以在Gitee浏览,或者电子书在线阅读,个人博客。电子书在线阅读和个人博客加载速度比较快。
3、转载须知:转载请注明GitHub出处,让我们一起维护一个良好的技术创作环境!
4、如果你要提交 issue 或者 pr 的话建议到 Github 提交。
5、笔者会陆续更新,如果对你有所帮助,不妨Github点个Star~。你的Star是我创作的动力。
6、所有更新日志,写作计划,公告等均在此发布 ==> 时间轴。
- 这篇文章是笔者校招时整理的一部分,有的是直接给出了url链接,也有很多是自己看书的总结(所以直接看答案可能会看不懂),都是相当简洁的版本(反正就是很水很乱,哈哈哈)。这篇文章所包含的知识点,我后面大部分都会重新写详细版,目前放出这篇文章的意义只是给需要看的人一个参考。
- 比如说
- Java集合的源码,我会写详细版【TODO,这些东西大部分已经看过了,在计划中】
- Java并发我已经写了详细版。不过还有一些的东西没写比如:阻塞队列,ConcurrentHashMap,CopyOnWriteArrayList等这些并发容器,也是常考点,还有一些常非面试的内容比如手写线程池等等,手写阻塞队列等等我觉得很有意思的点。【TODO,看了一部分内容,在计划中】
- jvm也已经写了详细版。还有jvm实战还没写【暂时没计划,因为我还不会,嘿嘿】
- Mysql部分很重要,下面给出的是极简版【TODO,看了一部分内容,在计划中】
- Redis【暂时没计划】
- Dubbo源码【TODO,正在学习,在计划中】
- 等等
- 希望大家多看些书和视频,背答案只是为了应试(无奈),多看书和视频以及一些讲的比较好的博客,才能理解的更深刻。
- 此篇文章发表于2021年4月,不再更新,后续可能删除
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在 JDK 的 Object.java 中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。
hashCode() 在散列表中才有用,在其它情况下没用。散列表存储的是键值对(key-value),在散列表中hashCode() 的作用是获取对象的散列码,进而快速确定该对象在散列表中的位置。(可以快速找到所需要的对象)
**问题:**假设,HashSet中已经有1000个元素。当插入第1001个元素时,需要怎么处理?
因为HashSet是Set集合,它不允许有重复元素。“将第1001个元素逐个的和前面1000个元素进行比较”?显然,这个效率是相等低下的。
散列表很好的解决了这个问题,它根据元素的散列码计算出元素在散列表中的位置,然后将元素插入该位置即可。对于相同的元素,自然是只保存了一个。
在散列表中,
1、如果两个对象相等,那么它们的hashCode()值一定要相同; 这里的相等是指,通过equals()比较两个对象 时返回true
2、如果两个对象hashCode()相等,它们并不一定相等。(不相等时就是哈希冲突)
注意:这是在散列表中的情况。在非散列表中一定如此!
考虑只重写equals而不重写 hashcode 时,虽然两个属性值完全相同的对象通过equals方法判断为true,但是当把这两个对象加入到 HashSet 时。会发现HashSet中有重复元素,这就是因为HashSet 使用 hashcode 判断对象是否已存在时造成了歧义,结果会导 致HashSet 的不正常运行。所以重写 equals 方法必须重写 hashcode 方法。
https://blog.csdn.net/baiye_xing/article/details/71788741
深拷贝实现思路:
1、对于每个子对象都实现Cloneable 接口,并重写clone方法。最后在最顶层的类的重写的 clone 方法中调用所有子对象的 clone 方法即可实现深拷贝。【简单的说就是:每一层的每个子对象都进行浅拷贝=深拷贝】
2、利用序列化。【先对对象进行序列化,紧接着马上反序列化出 】
为什么是-128-127 :https://zhidao.baidu.com/question/588564780479617005.html
从高精度转到低精度有可能损失精度,所以不会自动强转。比如int(高精度)就不会被自动强转为short,如果需要强转就只能强制转换。
首先计算机中保存的都是补码,都是二进制的补码
int型能表示的最大正数
int型的32bit位中,第一位是符号为,正数位0。因此,int型能表示的最大的正数的二进制码是0111 1111 1111 1111 1111 1111 1111 1111,也就是2^31-1。
int型能表示的最小负数
最小的负数的二进制码是1000 0000 0000 0000 0000 0000 0000 0000,其补码还是1000 0000 0000 0000 0000 0000 0000 0000,值是231。用231来代替-0
名词解释
Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。反射被视为动态语言的关键
用途
原理
public class NewInstanceTest {
@Test
public void test1() throws IllegalAccessException, InstantiationException {
Class<Person> clazz = Person.class;
/*
newInstance():调用此方法,创建对应的运行时类的对象。内部调用了运行时类的空参的构造器。
要想此方法正常的创建运行时类的对象,要求:
1.运行时类必须提供空参的构造器
2.空参的构造器的访问权限得够。通常,设置为public。
在javabean中要求提供一个public的空参构造器。原因:
1.便于通过反射,创建运行时类的对象
2.便于子类继承此运行时类时,默认调用super()时,保证父类有此构造器
*/
Person obj = clazz.newInstance();
System.out.println(obj);
}
//体会反射的动态性
@Test
public void test2(){
for(int i = 0;i < 100;i++){
int num = new Random().nextInt(3);//0,1,2
String classPath = "";
switch(num){
case 0:
classPath = "java.util.Date";
break;
case 1:
classPath = "java.lang.Object";
break;
case 2:
classPath = "com.atguigu.java.Person";
break;
}
try {
Object obj = getInstance(classPath);
System.out.println(obj);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/*
创建一个指定类的对象。
classPath:指定类的全类名
*/
public Object getInstance(String classPath) throws Exception {
Class clazz = Class.forName(classPath);
return clazz.newInstance();
}
https://www.jianshu.com/p/7298f0c559dc
https://www.cnblogs.com/cC-Zhou/p/9525638.html
静态代理
代理模式可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强。值得注意的是,代理类和被代理类应该共同实现一个接口,或者是共同继承某个类。上面介绍的是静态代理的内容,为什么叫做静态呢?因为它的代理类是事先写好的。而动态代理是动态生成的代理类
动态代理和静态代理区别
/**
*
* 动态代理:
* 特点:字节码随用随创建,随用随加载
* 作用:不修改源码的基础上对方法增强
* 分类:
* 基于接口的动态代理
* 基于子类的动态代理
* 基于接口的动态代理:
* 涉及的类:Proxy
* 提供者:JDK官方
* 如何创建代理对象:
* 使用Proxy类中的newProxyInstance方法
* 创建代理对象的要求:
* 被代理类最少实现一个接口,如果没有则不能使用
* newProxyInstance方法的参数:
* ClassLoader:类加载器
* 它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。固定写法。
* Class[]:字节码数组
* 它是用于让代理对象和被代理对象有相同方法。固定写法。固定写接口
* InvocationHandler:用于提供增强的代码
* 它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
* 此接口的实现类都是谁用谁写。
*/
IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
producer.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 作用:执行被代理对象的任何接口方法都会经过该方法
* 方法参数的含义
* @param proxy 代理对象的引用
* @param method 当前执行的方法
* @param args 当前执行方法所需的参数
* @return 和被代理对象方法有相同的返回值
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//提供增强的代码
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())) {
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
proxyProducer.saleProduct(10000f);
}
来源:黑马Spring讲的动态代理部分
Cglib动态代理
public static void main(String[] args) {
final Producer producer = new Producer();
/**
* 动态代理:
* 特点:字节码随用随创建,随用随加载
* 作用:不修改源码的基础上对方法增强
* 分类:
* 基于接口的动态代理
* 基于子类的动态代理
* 基于子类的动态代理:
* 涉及的类:Enhancer
* 提供者:第三方cglib库
* 如何创建代理对象:
* 使用Enhancer类中的create方法
* 创建代理对象的要求:
* 被代理类不能是最终类
* create方法的参数:
* Class:字节码
* 它是用于指定被代理对象的字节码。
*
* Callback:用于提供增强的代码
* 它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
* 此接口的实现类都是谁用谁写。
* 我们一般写的都是该接口的子接口实现类:MethodInterceptor
*/
Producer cglibProducer = (Producer)Enhancer.create(producer.getClass(), new MethodInterceptor() {
/**
* 执行被代理对象的任何方法都会经过该方法
* @param proxy
* @param method
* @param args
* 以上三个参数和基于接口的动态代理中invoke方法的参数是一样的
* @param methodProxy :当前执行方法的代理对象
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//提供增强的代码
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())) {
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
cglibProducer.saleProduct(12000f);
}
https://www.cnblogs.com/starry-skys/p/12157141.html
1、JavaGuide-Java基础知识:https://snailclimb.gitee.io/javaguide/#/docs/java/Java基础知识1.4.2
2、https://www.zhihu.com/question/31203609
值传递( pass by value )是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
引用传递( pass by reference )是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
JavaGuide里的第三个例子。如果是引用传递的话,交换数据时(不是第二个例子里的赋值语句),应该是真正的交换内存地址,而不是交换引用。
JavaGuide-Java基础知识:https://snailclimb.gitee.io/javaguide/#/docs/java/Java基础知识 1.3.2
在装箱的时候自动调用的是Integer的valueOf(int)方法。而在拆箱的时候自动调用的是Integer的intValue方法。
笔者的JVM篇说的很详细
JDK7以上,静态变量存储在其对应的Class对象中。而Class对象作为对象,和其他普通对象一样,都是存在java堆中的。
this()和this. 不一样
public class JZ_056 {
private int num;
private String str;
public JZ_056() {
System.out.println("调用无参构造");
}
public JZ_056(int num) {
this(); //调用无参构造
System.out.println("调用有参构造");
this.num = num;
}
public JZ_056(int num, String str) {
this(1); //这个会调用只有一个参数的有参构造
this.num = num;
this.str = str;
}
public static void main(String[] args) {
JZ_056 jz_056 = new JZ_056(2, "哈哈");
}
}
由上面代码可以看出来,this()方法可以调用本类的无参构造函数。如果本类继承的有父类,那么无参构造函数会有一个隐式的super()的存在。所以在同一个构造函数里面如果this()之后再调用super(),那么就相当于调用了两次super(),也就是调用了两次父类的无参构造,就失去了语句的意义,编译器也不会通过。
https://www.cnblogs.com/wujing-hubei/p/6012105.html
1、多态是建立在继承的基础上的,是指子类类型的对象可以赋值给父类类型的引用变量,但运行时仍表现子类的行为特征。也就是说,同一种类型的对象执行同一个方法时可以表现出不同的行为特征。
笔者有篇泛型文章讲的很详细,包括泛型擦除这些都讲了
https://www.nowcoder.com/questionTerminal/d31ea6176417421b9152d19e8bd1b689
https://www.nowcoder.com/profile/2164604/test/35226827/365890
http://www.mamicode.com/info-detail-2252140.html
https://developer.aliyun.com/article/653204
https://imlql.cn/post/50ac3a1c.html:我在此篇文章的 堆是分配对象的唯一选择么?
有讲解。
这部分我会重新写新文章
头部插入:由于ArrayList头部插入需要移动后面所有元素,所以必然导致效率低。LinkedList不用移动后面元素,自然会快一些。
中间插入:查看源码会注意到LinkedList的中间插入其实是先判断插入位置距离头尾哪边更接近,然后从近的一端遍历找到对应位置,而ArrayList是需要将后半部分的数据复制重排,所以两种方式其实都逃不过遍历的操作,相对效率都很低,但是从实验结果还是ArrayList更胜一筹,我猜测这与数组在内存中是连续存储有关。
尾部插入:ArrayList并不需要复制重排数据,所以效率很高,这也应该是我们日常写代码时的首选操作,而LinkedList由于还需要new对象和变换指针,所以效率反而低于ArrayList。
删除操作和添加操作没有什么区别。所以笼统的说LinkedList插入删除效率比ArrayList高是不对的,有时候反而还低。之所以笼统的说LinkedList插入删除效率比ArrayList高,我猜测是ArrayList复制数组需要时间,也占一定的时间复杂度。而因为数据量太少,这种效果就体现不出来。
https://blog.csdn.net/hollis_chuang/article/details/102480657
https://blog.csdn.net/zwwhnly/article/details/104987143
https://blog.csdn.net/Kato_op/article/details/80356618
https://blog.csdn.net/striner/article/details/86375684
https://www.cnblogs.com/xiaowangbangzhu/p/10445574.html
https://blog.csdn.net/qq_28051453/article/details/71169801
1、在实现上,LinkedHashMap 很多方法直接继承自 HashMap(比如put remove方法就是直接用的父类的),仅为维护双向链表覆写了部分方法(get()方法是重写的)。
2、重新定义了数组中保存的元素Entry(继承于HashMap.node),该Entry除了保存当前对象的引用外,还保存了其上一个元素before和下一个元素after的引用,从而在哈希表的基础上又构成了双向链接列表。仍然保留hash拉链法的next属性,所以既可像HashMap一样快速查找,用next获取该链表下一个Entry。也可以通过双向链接,通过after完成所有数据的有序迭代.
3、在这个构造方法中,有个accessOrder
,它不同的值有不同的意义: 默认为false,即按插入时候的顺序进行迭代。设置为true后,按访问时候的顺序进行迭代输出,即链表的最后一个元素总是最近才访问的。
**访问的顺序:**如果有1 2 3这3个Entry,那么访问了1,就把1移到尾部去,即2 3 1。每次访问都把访问的那个数据移到双向队列的尾部去,那么每次要淘汰数据的时候,双向队列最头的那个数据不就是最不常访问的那个数据了吗?换句话说,双向链表最头的那个数据就是要淘汰的数据。
4、链表节点的删除过程
与插入操作一样,LinkedHashMap 删除操作相关的代码也是直接用父类的实现,但是LinkHashMap 重写了removeNode()方法 afterNodeRemoval()方法,该removeNode方法在hashMap 删除的基础上有调用了afterNodeRemoval 回调方法。完成删除。
删除的过程并不复杂,上面这么多代码其实就做了三件事:
https://blog.csdn.net/qq_36610462/article/details/83277524
下面只补充遗漏的,剩余的所有在我的Java并发系列文章都有
外层的if是为了让线程尽量少的进入synchronized块
内层的if则看下面的解释
public static Object getInstance() {
if(instance == null) {//线程1,2到达这里
synchronized(b) {//线程1到这里开始继续往下执行,线程2等待
if(instance == null) {//线程1到这里发现instance为空,继续执行if代码块,
//执行完成后退出同步区域,然后线程2进入同步代码块,如果在这里不再加一次判断,
//就会造成instance再次实例化,由于增加了判断,
//线程2到这里发现instance已被实例化于是就跳过了if代码块
instance = new Object();
}
}
}
}
/* 笔记
* 1.只有一边写一边读的时候才会报java.util.ConcurrentModificationException
* 单独测写的时候都没有报这个异常
* */
public class Video20_01 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 20; i++) {
new Thread(() ->{
list.add(UUID.randomUUID().toString().substring(0,8)); //这个是写
System.out.println(list);//这个是读
},"线程" + String.valueOf(i)).start();
}
}
}
/**
* @Author:
* @Date: 2019/9/25 8:45
*
* 功能描述: 集合不安全问题的解决(写时复制)
*/
public class Video20_02 {
public static void main(String[] args) {
// List list = new Vector<>();//不推荐
// List list = Collections.synchronizedList(new ArrayList<>());//不推荐
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 20; i++) {
new Thread(() ->{
list.add(UUID.randomUUID().toString().substring(0,8)); //这个是写
System.out.println(list);//这个是读
},"线程" + String.valueOf(i)).start();
}
ConcurrentHashMap<Object, Object> test = new ConcurrentHashMap<>();
test.put(1,1);
}
}
/* public class ThreadLocal {}源码 */
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); //①
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; //②
}
/* public class ThreadLocal {}源码 */
/* public class Thread implements Runnable {}*/
ThreadLocal.ThreadLocalMap threadLocals = null; ③
1、由源码可以看到,ThreadLocal是在set的时候,才和线程建立关系。并且在get的时候通过获取线程,来判断是哪一个线程。
2、同时每个Thread类都有一个ThreadLocal.ThreadLocalMap
类的变量,set方法里通过getMap获取这个变量,然后在这个map里设置值,所以才有了每一个线程都可以通过ThreadLocal设置专属变量的说法。
https://blog.csdn.net/puppylpg/article/details/80433271
这里我只截图下我准备的面试题目录,因为所有答案你都能在笔者的JVM文章里找到答案
用缓存,主要有两个用途:高性能、高并发。
假设这么个场景,你有个操作,一个请求过来,吭哧吭哧你各种乱七八糟操作 mysql,半天查出来一个结果,耗时 600ms。但是这个结果可能接下来几个小时都不会变了,或者变了也可以不用立即反馈给用户。那么此时咋办?
缓存啊,折腾 600ms 查出来的结果,扔缓存里,一个 key 对应一个 value,下次再有人查,别走 mysql 折腾 600ms 了,直接从缓存里,通过一个 key 查出来一个 value,2ms 搞定。性能提升 300 倍。
就是说对于一些需要复杂操作耗时查出来的结果,且确定后面不怎么变化,但是有很多读请求,那么直接将查询出来的结果放在缓存中,后面直接读缓存就好。
mysql 这么重的数据库,压根儿设计不是让你玩儿高并发的,虽然也可以玩儿,但是天然支持不好。mysql 单机支撑到 2000QPS
也开始容易报警了。
所以要是你有个系统,高峰期一秒钟过来的请求有 1 万,那一个 mysql 单机绝对会死掉。你这个时候就只能上缓存,把很多数据放缓存,别放 mysql。缓存功能简单,说白了就是 key-value
式操作,单机支撑的并发量轻松一秒几万十几万,支撑高并发 so easy。单机承载并发量是 mysql 单机的几十倍。
缓存是走内存的,内存天然就支撑高并发。
使用场景:
使用场景:
使用场景:
使用场景:
统计网页的uv(独立访客,每个用户每天只记录一次),如果用SET的话空间耗费会很大
pv(浏览量,用户每点一次记录一次)可以用String来统计
读的这篇文章:https://mp.weixin.qq.com/s/NOsXdrMrWwq4NTm180a6vw
总结的时候参考了这些文章:
- https://juejin.im/book/5afc2e5f6fb9a07a9b362527/section/5b5ac63d5188256255299d9c
- http://zhangtielei.com/posts/blog-redis-skiplist.html
什么是跨度:https://www.cnblogs.com/handwrit2000/p/12626570.html
下面是总结了一下
跳跃表
假设我们需要查找的值为k(score)
注意:
首先是有一个搜索确定位置的过程,逐步降级寻找目标节点,得到「搜索路径」
然后才开始插入
2-1. 为每个节点随机出一个层数(level)
2-2. 创建新节点,再将搜索路径上的节点和这个新节点通过前向后向指针串起来
2-3. 如果分配的新节点的高度高于当前跳跃列表的最大高度,更新一下跳跃列表的最大高度,并且填充跨度
删除过程和插入过程类似,都需先把这个「搜索路径」找出来。然后对于每个层的相关节点都重排一下前向后向指针就可以了。同时还要注意更新一下最高层数maxLevel
。
ZADD
方法时,如果对应的 value 不存在,那就是插入过程,如果这个 value 已经存在,只是调整一下 score 的值,那就需要走一个更新流程。ZADD
代码似乎还有进一步优化的空间。span
属性,用来 记录了前进指针所指向节点和当前节点的距离(也就是跨过了几个节点)。在源码中我们也可以看到 Redis 在插入、删除操作时都会小心翼翼地更新 span
值的大小。span
值进行累加就可以算出当前元素的最终 rank 值了。这个讲得好:https://doocs.gitee.io/advanced-java/#/./docs/high-concurrency/redis-expiration-policies-and-lru
补充:
LRU,即:最近最少使用淘汰算法(Least Recently Used)。LRU是淘汰一段时间内,没有被使用的页面。
LFU,即:最不经常使用淘汰算法(Least Frequently Used)。LFU是淘汰一段时间内,使用次数最少的页面。
参考:
- JavaGuide-Redis常见问题总结
- https://mp.weixin.qq.com/s/O_qDco6-Dasu3RomWIK_Ig
- https://juejin.im/book/5afc2e5f6fb9a07a9b362527/section/5afc364c6fb9a07aaf3567c8
Redis 的数据 全部存储 在 内存 中,如果 突然宕机,数据就会全部丢失,因此必须有一套机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的 持久化机制,它会将内存中的数据库状态 保存到磁盘 中。
下面是总结
Redis 快照 是最简单的 Redis 持久性模式。当满足特定条件时,它将生成数据集的时间点快照,例如,如果先前的快照是在2分钟前创建的,并且现在已经至少有 100 次新写入,则将创建一个新的快照。此条件可以由用户配置 Redis 实例来控制【JavaGuide里有如何配置】。快照是一次全量备份
但我们知道,Redis 是一个 单线程 的程序,这意味着,我们不仅仅要响应用户的请求,还需要进行内存快照。而后者要求 Redis 必须进行 IO 操作,这会严重拖累服务器的性能。
还有一个重要的问题是,我们在 持久化的同时,内存数据结构 还可能在 变化,比如一个大型的 hash 字典正在持久化,结果一个请求过来把它删除了,还没持久化完呢。怎么办呢
操作系统多进程 COW(Copy On Write) 机制可以解决上述问题
4-1.Redis 在持久化时会调用 glibc
的函数 fork
产生一个子进程,简单理解也就是基于当前进程 复制 了一个进程,主进程和子进程会共享内存里面的代码块和数据段
4-2.所以 快照持久化 可以完全交给 子进程 来处理,父进程 则继续 处理客户端请求。子进程 做数据持久化,它 不会修改现有的内存数据结构,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。但是 父进程 不一样,它必须持续服务客户端请求,然后对 内存数据结构进行不间断的修改
4-3.这个时候就会使用操作系统的 COW 机制来进行 数据段页面 的分离。数据段是由很多操作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复 制一份分离出来,然后 对这个复制的页面进行修改。这时 子进程 相应的页面是 没有变化的,还是进程产生时那一瞬间的数据。
4-4.子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,这也是为什么 Redis 的持久化 叫「快照」的原因。接下来子进程就可以非常安心的遍历数据了进行序列化写磁盘了。
MySQL
、LevelDB
、HBase
等存储引擎,如果我们先存储日志再做逻辑处理,这样就可以保证即使宕机了,我们仍然可以通过之前保存的日志恢复到之前的数据状态,但是 Redis 为什么没有这么做呢?glibc
提供了fsync(int fd)
函数可以将指定文件的内容强制从内核缓存刷到磁盘。只要 Redis 进程实时调用 fsync 函数就可以保证 aof 日志不丢失。但是 fsync 是一个磁盘 IO 操作,它很慢!https://juejin.im/book/5afc2e5f6fb9a07a9b362527/section/5afc3747f265da0b71567686
redis事务在执行时是单线程运行的。但是在执行前有可能别的客户端已经修改了事务里执行的key。所以在multi事务开始之前用watch检测这个key避免被其他客户端改变的。如果这个key被改变 了 exec的时候就会报错 不执行这个事务。
这里的“other client” 并不一定是另外一个客户端,watch操作执行之后,multi之外任何操作都可以认为是other clinet在操作(即使仍然是在同一个客户端上操作),exec该事务也仍旧会失败。
redis使用watch实现cas具体示例:https://www.jianshu.com/p/0244a875aa26
分布式锁是悲观锁,redis的watch机制是乐观锁。悲观锁的意思就是我不允许你修改。乐观锁的意思就是你修改了之后要告诉我,我让我的操作失败。
https://doocs.github.io/advanced-java/#/./docs/high-concurrency/redis-single-thread-model
我写过这篇文章,可以翻一下
select, poll, epoll 都是I/O多路复用的具体的实现,之所以有这三个存在,其实是他们出现是有先后顺序的。
- https://blog.csdn.net/nanxiaotao/article/details/90612404
- https://www.cnblogs.com/aspirant/p/9166944.html
- https://www.zhihu.com/question/32163005
poll本质上和select没有区别,采用链表的方式替换原有fd_set数据结构,而使其没有连接数的限制。
详细看这里:https://doocs.gitee.io/advanced-java/#/./docs/high-concurrency/redis-cluster
redis cluster采用hash slot。(中文就是hash槽)
优点:
思想上就是一个环,key取hash,然后顺时针找第一个节点
优点:
缺点:
参考:
https://mp.weixin.qq.com/s/9dtGe3d_mbbxW5FpVPDNow
https://juejin.im/book/5afc2e5f6fb9a07a9b362527/section/5b336548e51d4558a426ff56
https://www.jianshu.com/p/55defda6dcd2
https://www.baidu.com/s?wd=HyperLogLog%E5%8E%9F%E7%90%86&ie=UTF-8
整体参考:
- https://juejin.im/book/5afc2e5f6fb9a07a9b362527/section/5b33657cf265da597b0f99ab
- https://www.wmyskxz.com/2020/03/11/redis-5-yi-ji-shu-ju-guo-lu-he-bu-long-guo-lu-qi/#toc-heading-1
原理:https://juejin.im/book/5afc2e5f6fb9a07a9b362527/section/5b33657cf265da597b0f99ab
看上面那篇文章的-布隆过滤器的原理-这部分
https://doocs.gitee.io/advanced-java/#/./docs/high-concurrency/redis-master-slave
如果问到的话,答个大概其实就可以了
https://doocs.gitee.io/advanced-java/#/./docs/high-concurrency/redis-sentinel
如果问到的话,答个大概其实就可以了
https://doocs.gitee.io/advanced-java/#/./docs/high-concurrency/redis-cluster
注意redis cluster和redis replication ,redis哨兵的关系
redis cluster 功能强大,直接集成了 replication 和 sentinel 的功能。
缓存穿透
会在接口层增加校验,比如用户鉴权校验,参数做校验,不合法的参数直接代码Return,比如:id 做基础校验,id <=0的直接拦截等。
缓存空,但它的过期时间会很短,最长不超过五分钟。为了防止缓存穿透将,null或者空字符串值设置给redis。比如
set -999 UNKNOWN ,set -1 null
布隆过滤器
1、Redis还有一个高级用法**布隆过滤器(Bloom Filter)**这个也能很好的防止缓存穿透的发生,他的原理也很简单就是利用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在,不存在你return就好了,存在你就去查了DB刷新KV再return。
2、请注意,用 redis 也可以做到判断 key 对应的value 在数据库中存不在,那就是把数据库里的所有value对应的key都储存在redis 中,而value可以为空,然后判断下key.IsExists()
就可以了,但是这无疑会浪费大量空间,因为存储了数据库中所有的key。而且这也不符合缓存的初衷:咱不能暴力的把所有key都存下来,而是查询了啥key,我们缓存啥key。而布隆过滤器是一种非常高效的数据结构,把所有数据库的value对应的key 存储到布隆过滤器里,几乎不消耗什么空间,而且查询也是相当的快!但是请注意,它只能判断 key 是否存在(而且会有一定的误差)。所以一个查询先通过布隆顾虑器判断key是否存在(key 对应的value是否存在数据库中),如果不存在直接返回空就好了。
缓存雪崩
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。
不同场景下的解决方式可如下:
https://doocs.gitee.io/advanced-java/#/./docs/high-concurrency/redis-consistence
讲的蛮好
https://www.cnblogs.com/sddai/p/9739900.html
1、LRU和LFU都是内存管理的页面置换算法。
2、LRU,即:最近最少使用淘汰算法(Least Recently Used)。LRU是淘汰最长时间没有被使用的页面。
3、LFU,即:最不经常使用淘汰算法(Least Frequently Used)。LFU是淘汰一段时间内,使用次数最少的页面。
4、LRU的意思是只要在最近用了一次这个页面,这个页面就可以不用被淘汰。LFU的意思是即使我最近用了某个页面,但是这个页面在一段时间内使用次数还是最少的话,我还是要淘汰它,相当于LFU说的是使用频率。
下面是参考的比较多的,参考的比较少的直接在文中写明了
Mysql这里特别强调一下,下面的内容其实我看了一些书,所以下面的参考链接是不完全的,只是答案版本。Mysql的部分详细文章也在我的计划中
事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作,要么都执行,要么都不执行。
https://www.zhihu.com/question/31346392
一致性
的。】总结一下:
保证一致性:
保证原子性:是利用Innodb的undo log,undo log名为回滚日志,是实现原子性的关键。undo log记录了这些回滚需要的信息,当事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。
保证持久性:是利用Innodb的redo log。当做数据修改的时候,不仅在内存中操作,还会在redo log中记录这次操作。当事务提交的时候,会将redo log日志进行刷盘。当数据库宕机重启的时候,会将redo log中的内容恢复到数据库中,再根据undo log和binlog内容决定回滚数据还是提交数据
保证隔离性:利用的是锁和MVCC机制
脏写(Dirty Write
):如果一个事务修改了另一个未提交事务修改过的数据,那就意味着发生了脏写
,
脏读(Drity Read):如果一个事务读到了另一个未提交事务修改过的数据,那就意味着发生了脏读
【某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。】
不可重复读(Non-repeatable read):如果一个事务能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值。【强调的是每次都能读到最新数据】
幻读(Phantom Read):如果一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来,那就意味着发生了幻读
【那对于先前已经读到的记录,之后又读取不到这种情况,算啥呢?其实这相当于对每一条记录都发生了不可重复读的现象。幻读只是重点强调了读取到了之前读取没有获取到的记录。】
具体讲解:https://blog.csdn.net/qq_35433593/article/details/86094028
不能,串行化就直接把表锁住了,无法并发。
https://blog.csdn.net/Waves___/article/details/105295060
多版本控制(MVCC): 指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。在内部实现中,InnoDB通过undo log保存每条数据的多个版本,并且能够找回数据历史版本提供给用户读,每个事务读到的数据版本可能是不一样的。在同一个事务中,用户只能看到该事务创建快照之前已经提交的修改和该事务本身做的修改。
MVCC只在 Read Committed 和 Repeatable Read两个隔离级别下工作。其他两个隔离级别和MVCC不兼容,Read Uncommitted总是读取最新的记录行,而不是符合当前事务版本的记录行;Serializable 则会对所有读取的记录行都加锁。
MySQL的InnoDB存储引擎默认事务隔离级别是RR(可重复读),是通过 "行级锁+MVCC"一起实现的,正常读的时候不加锁,写的时候加锁。而 MCVV 的实现依赖:隐藏字段、Read View、Undo log
具体看这里:https://blog.csdn.net/Waves___/article/details/105295060
小总结一下:
①每次对记录进行改动,都会记录一条undo日志
,每条undo日志
也都有一个roll_pointer
属性(INSERT
操作对应的undo日志
没有该属性,因为该记录并没有更早的版本),可以将这些undo日志
都连起来,串成一个链表。所有的版本都会被roll_pointer
属性连接成一个链表,我们把这个链表称之为版本链
,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务id
②ReadView:需要判断一下版本链中的哪个版本是当前事务可见的。
m_ids
:表示在生成ReadView
时当前系统中活跃的读写事务的事务id
列表。min_trx_id
:表示在生成ReadView
时当前系统中活跃的读写事务中最小的事务id
,也就是m_ids
中的最小值。max_trx_id
:表示生成ReadView
时系统中应该分配给下一个事务的id
值。creator_trx_id
:表示生成该ReadView
的事务的事务id
。③有了这个ReadView
,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:
trx_id
属性值与ReadView
中的creator_trx_id
值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。trx_id
属性值小于ReadView
中的min_trx_id
值,表明生成该版本的事务在当前事务生成ReadView
前已经提交,所以该版本可以被当前事务访问。trx_id
属性值大于或等于ReadView
中的max_trx_id
值,表明生成该版本的事务在当前事务生成ReadView
后才开启,所以该版本不可以被当前事务访问。trx_id
属性值在ReadView
的min_trx_id
和max_trx_id
之间,那就需要判断一下trx_id
属性值是不是在m_ids
列表中,如果在,说明创建ReadView
时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView
时生成该版本的事务已经被提交,该版本可以被访问。如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。
对于哈希索引来说,底层的数据结构就是哈希表。对于哈希索引,InnoDB是自适应哈希索引的(hash索引的创建由InnoDB存储引擎引擎自动优化创建,我们干预不了
1、B+树只有叶节点存放数据,其余节点用来存索引,而B-树是每个索引节点都会有Data域。B+树单一节点存储更多的数据,使得查询的IO次数更少。
2、所有查询都要查找到叶子节点,查询性能稳定。
3、B+树所有数据都在叶子节点,所有叶子节点形成有序链表,便于范围查询
1.在大规模数据存储的时候,平衡二叉树往往出现由于树的深度过大而造成磁盘IO读写过于频繁,进而导致效率低下的情况
2.数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。
这个网址有答案:http://www.coder55.com/question/139
1、由于存储介质的特性,磁盘本身存取就比主存慢很多,再加上机械运动耗费,磁盘的存取速度往往是主存的几百分分之一,因此为了提高效率,要尽量减少磁 盘I/O。为了达到这个目的,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放 入内存。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。
https://www.cnblogs.com/leezhxing/p/4420988.html
使用记录主键值的大小进行记录和页的排序,这包括三个方面的含义:
B+
树的叶子节点存储的是完整的用户记录。
所谓完整的用户记录,就是指这个记录中存储了所有列的值(包括隐藏列)。
也叫二级索引(使用唯一索引或普通索引)
c2
列的大小进行记录和页的排序,这包括三个方面的含义:
c2
列的大小顺序排成一个单向链表。c2
列大小顺序排成一个双向链表。c2
列大小顺序排成一个双向链表。B+
树的叶子节点存储的并不是完整的用户记录,而只是c2列+主键
这两个列的值。主键+页号
的搭配,而变成了c2列+页号
的搭配。如果用C2和C3列共同建二级索引,这个二级索引也叫做联合索引。
使用非聚集索引查询出数据时,拿到叶子上的主键再去查到想要查找的数据。(拿到主键再查找这个过程叫做回表)
13、如何创建索引?
14、如何使用索引避免全表扫描?
15、Explain语句各字段的意义
联合索引B+树是如何建立的?是如何查询的?当where子句中出现>时,联合索引命中是如何的? 如 where a > 10 and b = “111”时,联合索引如何创建?mysql优化器会针对得做出优化吗?
如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始,并且不跳过索引中的列。遇到范围查询(>、<、between、like
左匹配)等就不能进一步匹配了,后续退化为线性查找。
剩余相关知识看JavaGuide:
https://snailclimb.gitee.io/javaguide/#/docs/database/一条sql语句在mysql中如何执行的?id=%e4%b8%89-%e6%80%bb%e7%bb%93
select * from tb_student A where A.age='18' and A.name=' 张三 ';
先用连接器检查该语句是否有权限,如果没有权限,直接返回错误信息,如果有权限,在 MySQL8.0 版本以前,会先查询缓存,以这条 sql 语句为 key 在内存中查询是否有结果,如果有直接缓存,如果没有,执行下一步。
通过分析器进行词法分析,提取 sql 语句的关键元素,比如提取上面这个语句是查询 select,提取需要查询的表名为 tb_student,需要查询所有的列,查询条件是这个表的 id=‘1’。然后判断这个 sql 语句是否有语法错误,比如关键词是否正确等等,如果检查没问题就执行下一步。
接下来就是优化器进行确定执行方案,上面的 sql 语句,可以有两种执行方案:
a.先查询学生表中姓名为“张三”的学生,然后判断是否年龄是 18。
b.先找出学生中年龄 18 岁的学生,然后再查询姓名为“张三”的学生。Copy to clipboardErrorCopied
那么优化器根据自己的优化算法进行选择执行效率最好的一个方案(优化器认为,有时候不一定最好)。那么确认了执行计划后就准备开始执行了。
执行器执行并返回引擎的执行结果【进行权限校验,如果没有权限就会返回错误信息,如果有权限就会调用数据库引擎接口,返回引擎的执行结果。】
SQL Select语句完整的执行顺序[从DBMS使用者角度] :
from子句组装来自不同数据源的数据;
where子句基于指定的条件对记录行进行筛选;
group by子句将数据划分为多个分组;
使用聚集函数进行计算;
使用having子句筛选分组;
计算所有的表达式;
使用order by对结果集进行排序。
SQL Select语句的执行步骤[从DBMS实现者角度,这个对我们用户意义不大] :
1)语法分析,分析语句的语法是否符合规范,衡量语句中各表达式的意义。
2)语义分析, 检查语句中涉及的所有数据库对象是否存在,且用户有相应的权限。
3)视图转换,将涉及视图的查询语句转换为相应的对基表查询语句。
4)表达式转换,将复杂的SQL表达式转换为较简单的等效连接表达式。
5)选择优化器,不同的优化器一般产生不同的“ 执行计划"
6)选择连接方式,ORACLE 有三种连接方式,对多表连接ORACLE可选择适当的连接方式。
7)选择连接顺序,对多表连接ORACLE选择哪-对表先连接,选择这两表中哪个表做为源数据表。
8)选择数据的搜索路径,根据以上条件选择合适的数据搜索路径,如是选用全表搜索还是利用索引或是其他的方式。9)运行"执行计划”。
update tb_student A set A.age='19' where A.name=' 张三 ';
其实更新语句也基本上会沿着上一个查询的流程走,只不过执行更新的时候肯定要记录日志啦,这就会引入日志模块了,MySQL 自带的日志模块式 binlog(归档日志) ,所有的存储引擎都可以使用,我们常用的 InnoDB 引擎还自带了一个日志模块 redo log(重做日志),我们就以 InnoDB 模式下来探讨这个语句的执行流程。流程如下:
这样会形成一个有序数组,文件系统和数据库的索引都是存在硬盘上的,并且如果数据量大的话,不一定能一次性加载到内存中。有序数组没法一次性加载进内存,会进行很多次IO,效率又会降下来。这时候B+树的多路存储威力就出来了,可以每次加载B+树的一个结点,然后一步步往下找
https://www.jianshu.com/p/785cef383ed6
我们分别分析了拥有IS NULL
、IS NOT NULL
、!=
这三个条件的查询是在什么情况下使用二级索引来执行的,核心结论就是:成本决定执行计划,跟使用什么查询条件并没有什么关系。优化器会首先针对可能使用到的二级索引划分几个范围区间,然后分别调查这些区间内有多少条记录,在这些范围区间内的二级索引记录的总和占总共的记录数量的比例达到某个值时,优化器将放弃使用二级索引执行查询,转而采用全表扫描。
INNODB在做SELECT的时候,要维护的东西比MYISAM引擎多很多:
1)数据块,INNODB要缓存,MYISAM只缓存索引块, 这中间还有换进换出的减少;
2)innodb寻址要映射到块,再到行,MYISAM记录的直接是文件的OFFSET,定位比INNODB要快
3)INNODB还需要维护MVCC一致;虽然你的场景没有,但他还是需要去检查和维护
MVCC (Multi-Version Concurrency Control)多版本并发控制
https://www.cnblogs.com/cai170221/p/7122289.html
会 https://blog.csdn.net/qq_34208844/article/details/104043486
https://blog.csdn.net/weixin_43066287/article/details/90024600
这里我们只关注三种,分别是type,key,rows
代表着MySQL
对某个表的执行查询时的访问类型
常见的几种:
all const:根据主键或者唯一二级索引列与常数进行等值匹配时 ref:当通过普通的二级索引列与常量进行等值匹配时来查询某个表, 在 上述执行计划的 如果查询优化器决定使用全表扫描的方式对某个表执行查询时,执行计划的 正确方式: 说明开启慢查询日志,可以让MySQL记录下查询超过指定时间的语句,通过定位分析性能的瓶颈,才能更好的优化数据库系统的性能。 首先打开慢查询 方法一:全局变量设置 方法二:配置文件设置 3.重启MySQL服务 1.执行一条慢查询SQL语句 2.查看是否生成慢查询日志 如果日志存在,MySQL开启慢查询设置成功! 1.数据库刷新脏页 2.无法获取锁 1、设计数据库的大叔把这些为了回滚而记录的这些东东称之为撤销日志,英文名为 2、我们只是想让已经提交了的事务对数据库中数据所做的修改永久生效,即使后来系统崩溃,在重启后也能把这种修改恢复出来。所以我们其实没有必要在每次事务提交时就把该事务在内存中修改过的全部页面刷新到磁盘,只需要把修改了哪些东西记录一下就好,所以上述内容也被称之为 3、binlog其实就是记录了数据库执行更改的所有操作,因此很显然,它可以用来做数据归档和数据恢复 4、binlog主要用来做数据归档,但是它并不具备崩溃恢复的能力,也就是说如果你的系统突然崩溃,重启后可能会有部分数据丢失,而redo log的存在则可以完美解决这个问题。 5、redo log是InnoDB引擎特有的;binlog是MySQL的Server层实现的,所有引擎都可以使用。 6、redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这一行的c字段加1 ”。 7、redo log是循环写的,空间固定会用完;binlog是可以追加写入的。“追加写”是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。 count(*)包括了所有的列,相当于行数,在统计结果的时候,不会忽略列值为NULL count(列名)在统计结果的时候,会忽略列值为NULL count(1)包括了所有列,用1代表代码行,在统计结果的时候,不会忽略列值为NULL 对于COUNT(1)和COUNT(*)执行优化器的优化是完全一样的 https://www.cnblogs.com/whb11/p/11315463.html 1、池是一种广义上的池,比如数据库连接池、线程池、内存池、对象池等。以数据库连接池为例:数据库连接是一种关键的有限的昂贵的资源,这一点在多用户的网页应用程序中体现得尤为突出。 一个数据库连接对象均对应一个物理数据库连接,每次操作都打开一个物理连接,使用完都关闭连接,这样会造成系统的性能低下。数据库连接池的解决方案是在应用程序启动时建立足够的数据库连接,并讲这些连接组成一个连接池(简单说:在一个“池”里放了好多半成品的数据库联接对象),由应用程序动态地对池中的连接进行申请、使用和释放,但并不销毁。对于多于连接池中连接数的并发请求,应该在请求队列中排队等待。 2、池技术的优势是: https://blog.csdn.net/weixin_43433032/article/details/89293663 1、索引列类型是字符串,查询条件未加引号。 2、使用like时通配符在前 3、在查询条件中使用OR 4、对索引列进行函数运算 5、虽然使用了索引列,但是对索引进行了诸如加减乘除的运算 6、联合索引不符合最左前缀法则 7、使用索引需要扫描的行数过多,这时mysql优化器会直接使用全表扫描 特性: ①不支持事务。 ②表级锁定,并发性能大大降低。 ③读写互相阻塞。 适用场景: ①不支持事务。 ②并发相对较低,表锁定。 ③执行大量select语句操作的表。 ④count(*)操作较快。 ⑤不支持外键。 注:查询速度快的原因:a.MyISAM存储的直接是文件的offset。b.不用维护mvcc。 特征: ①良好的事务支持:支持事务隔离的四个级别。 ②行级锁定:使用间隙锁?????? ③外键约束。 ④支持丢失数据的自动恢复。 在内存中,默认使用hash索引,等值条件查找快速快,范围查找慢,断电后数据丢失,但表结构存在 https://www.jianshu.com/p/faf0127f1cb2 https://doocs.gitee.io/advanced-java/#/./docs/high-concurrency/mysql-read-write-separation.md 前提是作为主服务器角色的数据库服务器必须开启二进制日志,且复制是异步的 1、主服务器上面的任何修改都会通过自己的 I/O tread(I/O 线程)保存在二进制日志 2、从服务器上面也启动一个 I/O thread,通过配置好的用户名和密码, 连接到主服务器上面请求读取二进制日志,然后把读取到的二进制日志写到本地的一个 3、从服务器上面同时开启一个 SQL thread 定时检查 这里有一个非常重要的一点,就是从库同步主库数据的过程是串行化的,也就是说主库上并行的操作,在从库上会串行执行。所以这就是一个非常重要的点了,由于从库从主库拷贝日志以及串行执行 SQL 的特点,在高并发场景下,从库的数据一定会比主库慢一些,是有延时的。所以经常出现,刚写入主库的数据可能是读不到的,要过几十毫秒,甚至几百毫秒才能读取到。 而且这里还有另外一个问题,就是如果主库突然宕机,然后恰好数据还没同步到从库,那么有些数据可能在从库上是没有的,有些数据可能就丢失了。 所以 MySQL 实际上在这一块有两个机制,一个是半同步复制,用来解决主库数据丢失问题;一个是并行复制,用来解决主从同步延时问题。 这个所谓半同步复制,也叫 所谓并行复制,指的是从库开启多个线程,并行读取 relay log 中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。 一般来说,如果主从延迟较为严重,有以下解决方案: https://gitee.com/youthlql/advanced-java/blob/master/docs/high-concurrency/database-shard.md 比如你单表都几千万数据了,,单表数据量太大,会极大影响你的 sql 执行的性能,到了后面你的 sql 可能就跑的很慢了。一般来说,单表到几百万的时候,性能就会相对差一些了,就得分表了。 分库就是你一个库一般而言,最多支撑到并发 2000,一定要扩容了,而且一个健康的单库并发值你最好保持在每秒 1000 左右,不要太大。那么你可以将一个库的数据拆分到多个库中,访问的时候就访问一个库好了。 当当开源的,属于 client 层方案,目前已经更名为 Sharding-jdbc 这种 client 层方案的优点在于不用部署,运维成本低,不需要代理层的二次转发请求,性能很高,但是如果遇到升级啥的需要各个系统都重新升级版本再发布,各个系统都需要耦合 Sharding-jdbc 的依赖; 基于 Cobar 改造的,属于 proxy 层方案,支持的功能非常完善。 Mycat 这种 proxy 层方案的缺点在于需要部署,自己运维一套中间件,运维成本高,但是好处在于对于各个项目是透明的,如果遇到升级之类的都是自己中间件那里搞就行了。 水平拆分的意思,就是把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来扛更高的并发,还有就是用多个库的存储容量来进行扩容。 垂直拆分的意思,就是把一个有很多字段的表给拆分成多个表**,或者是多个库上去。每个库表的结构都不一样,每个库表都包含部分字段。一般来说,会将较少的访问频率很高的字段放到一个表里去,然后将较多的访问频率很低的字段放到另外一个表里去。因为数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。 1、简单来说,就是在线上系统里面,之前所有写库的地方,增删改操作,除了对老库增删改,都加上对新库的增删改,这就是所谓的双写,同时写俩库,老库和新库。 2、然后系统部署之后,新库数据差太远,用导数据工具,跑起来读老库数据写新库,写的时候要根据 modified 这类字段判断这条数据最后修改的时间,除非是读出来的数据在新库里没有,或者是比新库的数据新才会写。简单来说,就是不允许用老数据覆盖新数据。 3、导完一轮之后,有可能数据还是存在不一致,那么就程序自动做一轮校验,比对新老库每个表的每条数据,接着如果有不一样的,就针对那些不一样的,从老库读数据再次写。反复循环,直到两个库每个表的数据都完全一致为止。 4、接着当数据完全一致了,就 ok 了,基于仅仅使用分库分表的最新代码,重新部署一次,不就仅仅基于分库分表在操作了么,还没有几个小时的停机时间,很稳。所以现在基本玩儿数据迁移之类的,都是这么干的。 1、第一次分库分表,就一次性给他分个够,一开始上来就是 32 个库,每个库 32 个表,那么总共是 1024 张表。这个分法,第一,基本上国内的互联网肯定都是够用了,第二,无论是并发支撑还是数据量支撑都没问题。 2、每个库正常承载的写入并发量是 1000,那么 32 个库就可以承载 32 * 1000 = 32000 的写并发,如果每个库承载 1500 的写并发,32 * 1500 = 48000 的写并发,接近 5 万每秒的写入并发,前面再加一个MQ,削峰,每秒写入 MQ 8 万条数据,每秒消费 5 万条数据。 3、一个实践是利用 snowflake 算法 snowflake 算法是 twitter 开源的分布式 id 生成算法,采用 Scala 语言实现,是把一个 64 位的 long 型的 id,1 个 bit 是不用的,用其中的 41 bit 作为毫秒数,用 10 bit 作为工作机器 id,12 bit 作为序列号。 ThinkWon博客:https://blog.csdn.net/ThinkWon/article/details/104397516 控制反转IoC是一个很大的概念,可以用不同的方式来实现。其主要实现方式有两种:依赖注入和依赖查找 依赖注入:相对于IoC而言,依赖注入(DI)更加准确地描述了IoC的设计理念。所谓依赖注入(Dependency Injection),即组件之间的依赖关系由容器在应用系统运行期来决定,也就是由容器动态地将某种依赖关系的目标对象实例注入到应用系统中的各个关联的组件之中。组件不做定位查询,只提供普通的Java方法让容器去决定依赖关系。 依赖注入的基本原则是:应用组件不应该负责查找资源或者其他依赖的协作对象。配置对象的工作应该由IoC容器负责,“查找资源”的逻辑应该从应用组件的代码中抽取出来,交给IoC容器负责。容器全权负责组件的装配,它会把符合依赖关系的对象通过属性(JavaBean中的setter)或者是构造器传递给需要的对象。 依赖注入之所以更流行是因为它是一种更可取的方式:让容器全权负责依赖查询,受管组件只需要暴露JavaBean的setter方法或者带参数的构造器或者接口,使容器可以在初始化时组装对象的依赖关系。其与依赖查找方式相比,主要优势为: 依赖注入是时下最流行的IoC实现方式,依赖注入分为接口注入(Interface Injection),Setter方法注入(Setter Injection)和构造器注入(Constructor Injection)三种方式。其中接口注入由于在灵活性和易用性比较差,现在从Spring4开始已被废弃。 构造器依赖注入:构造器依赖注入通过容器触发一个类的构造器来实现的,该类有一系列参数,每个参数代表一个对其他类的依赖。 Setter方法注入:Setter方法注入是容器通过调用无参构造器或无参static工厂 方法实例化bean之后,调用该bean的setter方法,即实现了基于setter的依赖注入。 看ThinkWon博客 Bean 容器找到配置文件中 Spring Bean 的定义,Bean 容器利用 Java Reflection API 创建一个Bean的实例。 如果涉及到一些属性值 利用set方法设置一些属性值。 如果 Bean 实现了 (如果 Bean 实现了 与上面的类似,如果实现了其他 如果bean实现了BeanFactoryAware接口,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入; 如果bean实现了ApplicationContextAware接口,Spring将调用setApplicationContext()方法,将bean所在的应用上下文的引用传入进来; 如果bean实现了BeanPostProcessor接口(如果有和加载这个 Bean 的 Spring 容器相关的 如果Bean实现了 如果bean使用initmethod声明了初始化方法,该方法也会被调用; 如果bean实现了BeanPostProcessor接口(如果有和加载这个 Bean的 Spring 容器相关的 此时,bean已经准备就绪,可以被应用程序使用了,它们将一直驻留在应用上下文中,直到该应用上下文被销毁; 当要销毁 Bean 的时候,如果 Bean 实现了 当要销毁 Bean 的时候,同样,如果bean使用destroy-method声明了销毁方法,该方法也会被调用。 BeanPostProcessor作用:https://www.jianshu.com/p/369a54201943 singleton(单例) 不是,Spring框架中的单例bean不是线程安全的 在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域,因为Spring对一些Bean中非线程安全状态采用ThreadLocal进行处理,解决线程安全问题。 (实际上大部分时候 spring bean 无状态的(比如 dao 类),所有某种程度上来说 bean 也是安全的,但如果 bean 有状态的话(比如 view model 对象),那就要开发者自己去保证线程安全了,最简单的就是改变 bean 的作用域,把“singleton”变更为“prototype”,这样请求 bean 相当于 new Bean()了,所以就可以保证线程安全了。) 支持当前事务的情况: 不支持当前事务的情况: 其他情况: 脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。 具体讲解:https://blog.csdn.net/qq_35433593/article/details/86094028 https://blog.csdn.net/hsf15768615284/article/details/81623881 @Autowired可用于:构造函数、成员变量、Setter方法 @Autowired默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在(可以设置它required属性为false)。 @Resource默认是按照名称来装配注入的,只有当找不到与名称匹配的bean才会按照类型来装配注入。 @Autowired是根据类型进行自动装配的。如果当Spring上下文中存在不止一个UserDao类型的bean时,就会抛出BeanCreationException异常;如果Spring上下文中不存在UserDao类型的bean,也会抛出BeanCreationException异常。我们可以使用@Qualifier配合@Autowired来解决这些问题。如果我们想使用名称装配可以结合@Qualifier注解进行使用,如下: 再不懂的话看这里:https://www.cnblogs.com/aspirant/p/10431029.html 1.Spring Boot启动的时候会通过@EnableAutoConfiguration注解找到所有jar包中META-INF/spring.factories配置文件中的所有自动配置类,并对其进行加载。 2.而这些自动配置类都是以AutoConfiguration结尾来命名的,xxxAutoConfiguration配置类中,通过@EnableConfigurationProperties注解,取得xxxProperties类在全局配置文件中配置的属性(如:server.port) 3.而XxxxProperties类是通过@ConfigurationProperties注解与全局配置文件中对应的属性进行绑定的。 具体讲解:https://blog.csdn.net/u014745069/article/details/83820511 https://github.com/Snailclimb/springboot-guide/blob/master/docs/interview/springboot-questions.md 1、Spring事务 的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。 2、@Transactional注解可以帮助我们把事务开启、提交或者回滚的操作,,免去了重复的事务管理逻辑。是通过aop的方式进行管理. 3、Spring AOP中对一个方法进行代理的话,肯定需要定义切点。在@Transactional的实现中,同样如此,spring为我们定义了以 @Transactional 注解为植入点的切点,这样才能知道@Transactional注解标注的方法需要被代理。@Transactional的作用一个就是标识方法需要被代理,一个就是携带事务管理需要的一些属性信息。 4、bean在进行实例化的时候会判断适配了BeanFactoryTransactionAttributeSourceAdvisor(也就是调用的方法是否适配了切面) 5、如果没有适配的话,就返回原对象给IOC容器 6、如果适配了的话就会创建代理对象返回给IOC容器。AOP动态代理时,开始执行的方法是DynamicAdvisedInterceptor#intercept【这个方法只要是aop都会执行,下面的TransactionInterceptor相当于其具体的实现】。最终执行的方法是TransactionInterceptor#invoke方法。并且把CglibMethodInvocation注入到invoke方法中,CglibMethodInvocation就是包装了目标对象的方法调用的所有必须信息,因此,在TransactionInterceptor#invoke里面也是可以调用目标方法的,并且还可以实现类似@Around的逻辑,在目标方法调用前后继续注入一些其他逻辑,比如事务管理逻辑。 7、TransactionInterceptor内部依赖于TransactionManager,TransactionManager是实际的事务管理对象 工厂设计模式:Spring使用工厂模式可以通过 单例设计模式:Spring 中 bean 的默认作用域就是 singleton(单例)的。 代理设计模式:Spring AOP 就是基于代理的,AOP能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度。 观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。常用的地方是listener的实现,如ApplicationListener。 适配器模式:适配器模式(Adapter Pattern) 将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。 模板方法模式:JdbcTemplate中的execute方法 包装器模式:转换数据源 讲解的地方:https://blog.csdn.net/qq_34337272/article/details/90487768 https://www.jianshu.com/p/ace7de971a57 看ThinkWon博客 AOP实现的关键在于 代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。 (1)AspectJ是静态代理的增强,所谓静态代理,就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强,他会在编译阶段将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。 (2)Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。 Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理: JDK动态代理只提供接口的代理,不支持类的代理。核心InvocationHandler接口和Proxy类,InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;接着,Proxy利用 InvocationHandler动态创建一个符合某一接口的的实例, 生成目标类的代理对象。 如果代理类没有实现 InvocationHandler 接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。(CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。)CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。 静态代理与动态代理区别在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。 https://www.cnblogs.com/java-chen-hao/p/11139887.html https://blog.csdn.net/qq_36381855/article/details/79752689 可以 Bean在创建的时候给其打个标记,如果递归调用回来发现正在创建中的话—>即可说明循环依赖。 https://blog.csdn.net/f641385712/article/details/92801300 1.他们就是 Spring 解决 singleton bean 的关键因素所在,我称他们为三级缓存,第一级为 singletonObjects,第二级为 earlySingletonObjects,第三级为 singletonFactories。 2.Spring 在创建 bean 的时候并不是等它完全完成,而是在创建过程中将创建中的 bean 的 ObjectFactory 提前曝光(即加入到 singletonFactories 缓存中),这样一旦下一个 bean 创建的时候需要依赖 bean ,从三级缓存中拿到ObjectFactory,然后直接使用 ObjectFactory 的 参考:https://blog.nowcoder.net/n/31217f1bdf644371842a1bc85f1f4987 流程说明(重要): 1、用户请求发送至 DispatcherServlet 类进行处理 2、DispatcherServlet 类调用HandlerMapping 类请求查找 Handler 3、HandlerMapping 类根据 request 请求的 URL 等信息,以及相关拦截器 interceptor ,查找能够进行处理的 Handler最后给DispatcherServlet(前端控制器) 返回一个执行链,也就是一个HandlerExecutionChain 4、前端控制器请求(适配器)HandlerAdapter 执行 Handler(也就是常说的controller控制器) 5-7、HandlerAdapter 执行相关 Handler ,并获取 ModelAndView 类的对象。最后将ModelAndView 类的对象返回给前端控制器 8、DispatcherServlet 请求 ViewResolver 类进行视图解析。 9、ViewResolver 类进行视图解析获取 View 对象,最后向前端控制器返回 View 对象 10、DispatcherServlet 类进行视图 View 渲染,填充Model 11、DispatcherServlet 类向用户返回响应。 就是经过controller的请求,直接输入页面路径不拦截? http://localhost:8080/admin/index/index.html这样的话,就不走拦截器,直接跳转到页面上了。 简单的总结:https://cxis.me/2020/03/22/Spring%E4%B8%ADIOC%E5%AE%B9%E5%99%A8%E7%9A%84%E5%88%9D%E5%A7%8B%E5%8C%96%E8%BF%87%E7%A8%8B%E6%80%BB%E7%BB%93/ 基本没碰到过问的 https://blog.csdn.net/qq_41246635/article/details/81392818 https://www.cnblogs.com/PoetryAndYou/p/11622334.html https://blog.csdn.net/j04110414/article/details/78914787 首先我们先看下秒杀场景的难点到底在哪?在秒杀场景中最大的问题在于容易产生大并发请求、产生超卖现象和性能问题,下面我们分别分析下下面这三个问题: 1)瞬时大并发:一提到秒杀系统给人最深刻的印象是超大的瞬时并发,这时你可以联想到小米手机的抢购场景,在小米手机抢购的场景一般都会有10w+的用户同时访问一个商品页面去抢购手机,这就是一个典型的瞬时大并发,如果系统没有经过限流或者熔断处理,那么系统瞬间就会崩掉,就好像被DDos攻击一样; 2)超卖:秒杀除了大并发这样的难点,还有一个所有电商都会遇到的痛,那就是超卖,电商搞大促最怕什么?最怕的就是超卖,产生超卖了以后会影响到用户体验,会导致订单系统、库存系统、供应链等等,产生的问题是一系列的连锁反应,所以电商都不希望超卖发生,但是在大并发的场景最容易发生的就是超卖,不同线程读取到的当前库存数据可能下个毫秒就被其他线程修改了,如果没有一定的锁库存机制那么库存数据必然出错,都不用上万并发,几十并发就可以导致商品超卖; 3)性能:当遇到大并发和超卖问题后,必然会引出另一个问题,那就是性能问题,如何保证在大并发请求下,系统能够有好的性能,让用户能够有更好的体验,不然每个用户都等几十秒才能知道结果,那体验必然是很糟糕的; 4)黄牛:你这么低的价格,假如我抢到了,我转手卖掉我不是血赚?就算我不卖我也不亏啊,那用户知道,你知道,别的别有用心的人(黑客、黄牛…)肯定也知道的。 那简单啊,我知道你什么时候抢,我搞个几十台机器搞点脚本,我也模拟出来十几万个人左右的请求,那我是不是意味着我基本上有80%的成功率了。 5)F12链接提前暴露 从整个秒杀系统的架构其实和一般的互联网系统架构本身没有太多的不同,核心理念还是通过缓存、异步、限流来保证系统的高并发和高可用。下面从一笔秒杀交易的流程来描述下秒杀系统架构设计的要点: 1)对于大秒杀活动,一般运营会配置静态的活动页面,配置静态活动页面主要有两个目的一方面是为了便于在各种社交媒体分发,另一方面是因为秒杀活动页的流量是大促期间最大的,通过配置成静态页面可以将页面发布在公有云上动态的横向扩展; 2)将秒杀活动的静态页面提前刷新到CDN节点,通过CDN节点的页面缓存来缓解访问压力和公司网络带宽,CDN上缓存js、css和图片;或者Nginx服务器提供的静态资源功能。 3)将秒杀服务部署在公有云的web server上,使用公有云最大的好处就是能够根据活动的火爆程度动态扩容而且成本较低,同时将访问压力隔离在公司系统外部; 4)在提供真正商品秒杀业务功能的app server上,需要进行交易限流、熔断控制,防止因为秒杀交易影响到其他正常服务的提供。 5)服务降级处理,除了上面讲到的限流和熔断控制,我们还设定了降级开关,对于首页、购物车、订单查询、大数据等功能都会进行一定程度的服务降级。比如进行秒杀的时候,将订单查询系统进行降级,减少秒杀压力 6)如何防止超卖现象的发生,我们日常的下单过程中防止超卖一般是通过在数据库上加锁来实现。但是还是无法满足秒杀的上万并发需求,我们的方案其实也很简单实时库存的扣减在缓存中进行,异步扣减数据库中的库存,保证缓存中和数据库中库存的最终一致性。 7)库存预热:那不简单了,我们要开始秒杀前你通过定时任务或者运维同学提前把商品的库存加载到Redis中去,让整个流程都在Redis里面去做,然后等秒杀介绍了,再异步的去修改库存就好了。 8)MQ削峰填谷:你买东西少了你直接100个请求改库我觉得没问题,但是万一秒杀一万个,10万个呢? 你可以把它放消息队列,然后一点点消费去改库存就好了嘛,不过单个商品其实一次修改就够了,我这里说的是某个点多个商品一起秒杀的场景,像极了双十一零点。 由秒杀引发的一个问题 我们假设现在商品只剩下一件了,此时数据库中 num = 1; 但有100个线程同时读取到了这个 num = 1,所以100个线程都开始减库存了。 但你会最终会发觉,其实只有一个线程减库存成功,其他99个线程全部失败。 为何? 这就是MySQL中的排他锁起了作用。 排他锁又称为写锁,简称X锁,顾名思义,排他锁就是不能与其他所并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。 就是类似于我在执行update操作的时候,这一行是一个事务**(默认加了排他锁**)。这一行不能被任何其他线程修改和读写 这种方式采用了版本号的方式,其实也就是CAS的原理。 假设此时version = 100, num = 1; 100个线程进入到了这里,同时他们select出来版本号都是version = 100。 然后直接update的时候,只有其中一个先update了,同时更新了版本号。 那么其他99个在更新的时候,会发觉version并不等于上次select的version,就说明version被其他线程修改过了。那么我就放弃这次update 利用redis的单线程预减库存。比如商品有100件。那么我在redis存储一个k,v。例如 每一个用户线程进来,key值就减1,等减到0的时候,全部拒绝剩下的请求。 那么也就是只有100个线程会进入到后续操作。所以一定不会出现超卖的现象 可见第二种CAS是失败重试,并无加锁。应该比第一种加锁效率要高很多。类似于Java中的Synchronize和CAS。possible_keys和key
EXPLAIN
语句输出的执行计划中,possible_keys
列表示在某个查询语句中,对某个表执行单表查询时可能用到的索引有哪些,key
列表示实际用到的索引有哪些,比方说下边这个查询:mysql> EXPLAIN SELECT * FROM s1 WHERE key1 > 'z' AND key3 = 'a';
+----+-------------+-------+------------+------+-------------------+----------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+-------------------+----------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | s1 | NULL | ref | idx_key1,idx_key3 | idx_key3 | 303 | const | 6 | 2.75 | Using where |
+----+-------------+-------+------------+------+-------------------+----------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.01 sec)
possible_keys
列的值是idx_key1,idx_key3
,表示该查询可能使用到idx_key1,idx_key3
两个索引,然后key
列的值是idx_key3
,表示经过查询优化器计算使用不同索引的成本后,最后决定使用idx_key3
来执行查询比较划算。rows
rows
列就代表预计需要扫描的行数,如果使用索引来执行查询时,执行计划的rows
列就代表预计扫描的索引记录行数。比如下边这个查询:怎么抓取慢sql
show variables like 'slow_query%';
show variables like 'long_query_time';
将 slow_query_log 全局变量设置为“ON”状态mysql> set global slow_query_log='ON';
设置慢查询日志存放的位置mysql> set global slow_query_log_file='/usr/local/mysql/data/slow.log';
查询超过1秒就记录mysql> set global long_query_time=1;
修改配置文件my.cnf,在[mysqld]下的下方加入[mysqld]
slow_query_log = ON
slow_query_log_file = /usr/local/mysql/data/slow.log
long_query_time = 1
service mysqld restart
mysql> select sleep(2);
ls /usr/local/mysql/data/slow.log
explain分析sql
show profile分析sql
SQL执行慢的原因
偶尔比较慢
一直很慢
redo日志,undo日志,binlog日志
undo log
,我们也可以土洋结合,称之为undo日志
。重做日志
,英文名为redo log
count(1),count(*),count(filed)
数据库连接池
池化技术
常见的连接池参数
maxActive
连接池同一时间可分配的最大活跃连接数
100
maxIdle
始终保留在池中的最大连接数,如果启用,将定期检查限制连接,超出此属性设定的值且空闲时间超过minEvictableIdleTimeMillis的连接则释放
与maxActive设定的值相同
minIdle
始终保留在池中的最小连接数,池中的连接数量若低于此值则创建新的连接,如果连接验证失败将缩小至此值
与initialSize设定的值相同
initialSize
连接池启动时创建的初始连接数量
10
maxWait
最大等待时间(毫秒),如果在没有连接可用的情况下等待超过此时间,则抛出异常
30000(30秒)
minEvictableIdleTimeMillis
连接在池中保持空闲而不被回收的最小时间(毫秒)
60000(60秒)
数据库范式
索引失效的场景
三大引擎
MyISAM
InnoDB
Memory
Mysql主从复制
Binary log
里面。Realy log
(中继日志)里面。Realy log
(这个文件也是二进制的),如果发现有更新立即把更新的内容在本机的数据库上面执行一遍。
semi-sync
复制,指的就是主库写入 binlog 日志之后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的 relay log 之后,接着会返回一个 ack 给主库,主库接收到至少一个从库的 ack 之后才会认为写操作完成了。MySQL 主从同步延时问题(精华)
Mysql分库分表
分表
分库
分库分表中间件
Sharding-jdbc
ShardingSphere
(后文所提到的 Sharding-jdbc
,等同于 ShardingSphere
)。确实之前用的还比较多一些,因为 SQL 语法支持也比较多,没有太多限制,而且截至 2019.4,已经推出到了 4.0.0-RC1
版本,支持分库分表、读写分离、分布式 id 生成、柔性事务(最大努力送达型事务、TCC 事务)。Mycat
拆分维度
如何让系统从未分库分表动态切换到分库分表上?
停机迁移方案
双写迁移方案
如何设计可以动态扩容缩容的分库分表方案?
32 * 32
来分库分表,即分为 32 个库,每个库里一个表分为 32 张表。一共就是 1024 张表。根据某个 id 先根据 32 取模路由到库,再根据 32 取模路由到库里的表。分库分表之后,id 主键如何处理?
2^41 - 1
,也就是可以标识 2^41 - 1
个毫秒值,换算成年就是表示69年的时间。2^5
个机房(32个机房),每个机房里可以代表 2^5
个机器(32台机器)。2^12 - 1 = 4096
,也就是说可以用这个 12 bit 代表的数字来区分同一个毫秒内的 4096 个不同的 id。0 | 0001100 10100010 10111110 10001001 01011100 00 | 10001 | 1 1001 | 0000 00000000
Spring
Spring IOC
什么是依赖注入
依赖注入的基本原则
依赖注入有什么优势
有哪些不同类型的依赖注入实现方式?
Spring AOP,动态
Bean生命周期
BeanNameAware
接口,调用 setBeanName()
方法,传入Bean的名字。BeanClassLoaderAware
接口,调用 setBeanClassLoader()
方法,传入 ClassLoader
对象的实例。)*.Aware
接口,就调用相应的方法。比如:
BeanPostProcessor
对象),执行postProcessBeforeInitialization()
方法InitializingBean
接口,执行afterPropertiesSet()
方法。BeanPostProcessor
对象),执行postProcessAfterInitialization()
方法DisposableBean
接口,执行 destroy()
方法。Bean作用域?默认什么级别?是否线程安全?Spring如何保障线程安全的?
Bean作用域
默认级别
是否线程安全
Spring如何解决线程安全
Spring事务隔离级别和事务传播属性
补充
不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
幻读(Phantom Read):同一个事务内多次查询返回的结果集不一样。比如同一个事务 A 第一次查询时候有 n 条记录,但是第二次同等条件下查询却有 n+1 条记录,这就好像产生了幻觉。发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据,同一个记录的数据内容被修改了,所有数据行的记录就变多或者变少了Spring以及Spring MVC常见注解
@autowired和@resource的区别,当UserDao存在不止一个bean或没有存在时,会怎样?怎么解决?
区别
@Autowired和@Resource之间的区别
会怎么样
SpringBoot自动配置的原理是什么?介绍SpringBootApplication注解.
原理是什么
SpringBootApplication注解
@EnableAutoConfiguration
注解通过Spring 提供的 @Import
注解导入了AutoConfigurationImportSelector
类@Transactional注解加载和运行机制?
事务的原理
Spring中用到了哪些设计模式?单例、***、工厂、适配、观察者之类的说一说就行
BeanFactory
或 ApplicationContext
创建 bean 对象。
Spring由哪些模块组成?
Spring中的代理
Spring AOP and AspectJ AOP 有什么区别?AOP 有哪些实现方式?
JDK动态代理和CGLIB动态代理的区别
IOC循环依赖及其解决方案
1.什么是循环依赖
2.如何检测循环依赖
3.Spring如何解决循环依赖
总结
/** Cache of singleton objects: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);
/** Cache of singleton factories: bean name --> ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);
/** Cache of early singleton objects: bean name --> bean instance */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);
缓存
用途
singletonObjects
用于存放完全初始化好的 bean,从该缓存中取出的 bean 可以直接使用
earlySingletonObjects
存放原始的 bean 对象(尚未填充属性),用于解决循环依赖
singletonFactories
存放 bean 工厂对象,用于解决循环依赖
getObject()
获取bean,也就是 getSingleton()
中的代码片段了。Springmvc原理流程
关于interceptor与Filter区别
属性
拦截器Interceptor
过滤器Filter
原理
基于java的反射机制
基于函数回调
创建
(在context.xml中配置)由Spring容器初始化。
(在web.xml中配置filter基本属性)由web容器创建
servlet 容器
拦截器不直接依赖于servlet容器
过滤器依赖于servlet容器
作用对象
拦截器只能对action请求起作用
过滤器则可以对几乎所有的请求起作用
访问范围
拦截器可以访问action上下文、值栈里的对象,可以获取IOC容器中的各个bean。
不能
使用场景
即可用于Web,也可以用于其他Application
基于Servlet规范,只能用于Web
使用选择
可以深入到方法执行前后,使用场景更广
只能在Servlet前后起作用
在Action的生命周期中,拦截器可以多次调用
而过滤器只能在容器初始化时被调用一次。
什么是action请求
IOC容器初始化流程
Mybatis
SQL注入
Mybatis中#与$的区别
场景题
如何设计一个秒杀系统
秒杀系统的难点
如何解决高并发秒杀的超卖问题
1 update goods set num = num - 1 WHERE id = 1001 and num > 0
1 select version from goods WHERE id= 1001
2 update goods set num = num - 1, version = version + 1 WHERE id= 1001 AND num > 0 AND version = @version(上面查到的version);