字节面试问题合集1-摘抄网络

1. Object的方法,9大方法

  1. public final native Class<?> getClass();
    返回 运行时对象的类别

  2. public native int hashCode();
    返回 这个对象的一个 hash code

  3. public boolean equals(Object obj)
    返回 表示 是否另外一个对象是否 和这个对象 "相同"

  4. protected native Object clone() throws CloneNotSupportedException;
    返回 这个实例的一个克隆

  5. public String toString()
    返回 一个string 代表这个对象

  6. public final native void notify();

  7. public final void wait() throws InterruptedException {}

  8. public final native void notifyAll();
    线程在运行的时候,如果发现某些条件没有背满足,可以调用wait方法暂停自己的执行,并且放弃已经获得的锁,然后进入等待状态。当该线程被其他线程唤醒并获得锁后,可以沿着之前暂停的地方继续向后执行,而不是而不是再次从同步代码块开始的地方开始执行。需要注意的一点是,**对线程等待的条件的判断要使用while而不是if来进行判断。这样在线程被唤醒后,会再次判断条件是否满足。

  9. protected void finalize() throws Throwable { }
    当没有任何引用指向这个对象时,产生的一个回调。

补充 2. 实现生产者消费者

package thread;

import java.util.Deque;
import java.util.LinkedList;

/***
 * @Description: "实现生产者消费者"
 * @Author: ZBZ
 * @Date: 2022/4/6
 */
public class ProducerConsumer {

    /**
     * 生产者
     */
    public static class Producer extends Thread {

        Deque<Integer> queue;
        int maxSize;

        public Producer(Deque<Integer> queue, int maxSize, String name) {
            this.queue = queue;
            this.maxSize = maxSize;
            this.setName(name);
        }

        @Override
        public void run() {
            super.run();
            while (true) {
                synchronized (queue) {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {

                    }
                    System.out.println(this.getName() + "获得队列的锁");
                    while (queue.size() == maxSize) {
                        System.out.println("队列已满, 生产者" + this.getName() + "等待");
                        try {
                            queue.wait();
                        } catch (InterruptedException e) {

                        }
                    }

                    int num = (int) (Math.random() * 100);
                    queue.offer(num);

                    System.out.println(this.getName() + "生产了一个元素:" + num);

                    queue.notifyAll();

                    System.out.println(this.getName() + "退出一次生产过程!");
                }
            }
        }
    }

    /**
     * 消费者
     */
    public static class Consumer extends Thread {

        Deque<Integer> queue;
        int maxSize;

        public Consumer(Deque<Integer> queue, int maxSize, String name) {
            this.queue = queue;
            this.maxSize = maxSize;
            this.setName(name);
        }

        @Override
        public void run() {
            super.run();
            while (true) {
                synchronized (queue) {
                    try {
                        Thread.sleep(0);
                    } catch (InterruptedException e) {

                    }
                    System.out.println(this.getName() + "获得队列的锁");

                    while (queue.isEmpty()) {
                        System.out.println("队列为空, 消费者" + this.getName() + "等待");
                        try {
                            queue.wait();
                        } catch (InterruptedException e) {

                        }
                    }
                    int num = queue.poll();
                    System.out.println(this.getName() + "消费一个元素" + num);

                    queue.notifyAll();
                    System.out.println(this.getName() + "退出一次消费过程");

                }
            }
        }
    }


    public static void main(String[] args) {
        Deque<Integer> queue = new LinkedList<>();
        int maxSize = 5;

        Producer producer = new Producer(queue, maxSize, "Producer");
        Consumer consumer = new Consumer(queue, maxSize, "Consumer1");
        Consumer consumer2 = new Consumer(queue, maxSize, "Consumer2");
        Consumer consumer3 = new Consumer(queue, maxSize, "Consumer3");

        producer.start();
        consumer.start();
        consumer2.start();
        consumer3.start();

    }

}

3. synchronized方法讲解

synchronized是Java中的关键字,是一种同步锁。 它修饰的对象有以下几种:

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括{}括起来的代码,作用的对象是调用这个代码块的对象。
  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。
  3. 修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象。
  4. 修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象。

3.1 修饰一个代码块

一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程被阻塞。

当两个并发线程访问同一个对象的synchronized代码块时,在同一时刻只能有一个线程得到执行,另一个线程阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。
synchronized只锁定对象,每个对象只有一个锁(lock)与之相关联。

3.2 修饰一个方法

Synchronized修饰一个方法很简单,就是在方法的前面加synchronized,public synchronized void method(){}; synchronized修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数。如将的run方法改成如下的方式,实现的效果一样。

在使用synchronized修饰方法时要注意以下几点:

  1. synchronized关键字不能被继承
    虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。这两种方式的例子代码如下:
    在子类方法中加上synchronized关键字

  2. 在定义接口方法时不能使用synchronized关键字。

  3. 构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。
    使用当前对象当作锁,但是当前对象都没产生,所以是不能修饰的。

3.3 修饰一个静态方法

静态方法是属于类的而不是属于对象的。 同样的,synchronized修饰的静态方法绑定的是这个类的 所有对象

3.4 修饰一个类

效果和synchronized修饰静态方法是一样的,synchronized作用于一个类T时,是给这个类T加锁,T的锁哟u对象使用的是同一把锁。
image

总结:

  1. 无论synchroniz3ed关键字加在方法上还是对象上,如果它的作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象使用的是同一把锁。
  2. 每个对象只有一个锁与之相关联,谁拿到这把锁,谁就可以运行它所控制的那段代码。
  3. 实现同步是需要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

摘抄: https://www.cnblogs.com/weibanggang/p/9470718.html

4. synchronized方法实现原理

查看带有synchronized语句块的字节码文件,
在同步代码块的起始位置插入了moniterenter指令,
在同步代码块结束的位置插入了moniterexit指令。
image
image

查看同步方法的字节码文件时,同步方法并没有通过指令moniterenter和moniterexit来完成,而是被翻译成普通的方法啊

5. Java类的加载过程

一个java文件从编码到最终执行,一般主要包括两个过程: 编译+执行。
我们所说的类加载过程就是JVM虚拟机把.class文件中 类信息加载进内存,并进行解析生成对应的class对象的过程
举个例子:JVM在执行某段代码,遇到了class A,此次内存中没有class A,于是JVM就会到相应的class文件中寻找class A的类信息,并加载进内存。
由此可见,JVM不是一开始就把所有的类都加载进内存,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次。

类加载过程主要分为三个部分: 加载、链接、初始化三个阶段。

加载

加载: 把class字节码文件从各个来源通过类加载器装载入 内存。
来源一般包括本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译。
类加载器:启动类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器。
加载阶段,虚拟机需要完成以下三件事:

  1. 通过一个类的全限定名来获得其定义的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在Java堆中生存一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

连接

连接阶段可以分为3个小阶段: 验证,准备,解析

连接- 验证

验证:主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。
文件格式的验证、元数据的验证、字节码的验证、符号引用的验证
验证做的具体内容:

  1. 文件格式验证: 验证字节流是否符合Class文件格式的规范
  2. 元数据验证:对字节码描述的信息进行语义分析
  3. 字节码验证:通过数据流和控制流分析,确定程序是合法的
  4. 符号引用验证:确保解析动作能正确执行
    验证阶段是非常重要的,但不是必须的,对程序运行期没有影响,如果所引用的类经过反复验证,可以考虑采用-Xverifnone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

连接- 准备

在准备阶段,将静态变量的初值赋为jvm默认的初值,而不是我们在程序中设定的初值。jvm默认为静态变量的初值是这样的

  1. 静态变量是基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0
  2. 静态变量时引用类型的默认值为null
  3. 被final和static共同修饰的静态变量,我们通常称为常量,然后常量的默认值为我们程序中设定的值,
    比如我们在程序中定义final static int a = 100,则准备阶段中a的初值就是100。
    为什么被final和static变量修饰的成员变量在准备阶段的赋值会比较特别呢,这是因为,被final和static修改的变量,我们叫做ConstantValue属性。

连接- 解析

这一阶段的任务就是把常量池中的符号引用转换为直接引用。
什么是符号引用,什么是直接引用?
符号引用:即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
直接引用:可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针。

初始化阶段

类加载过程中的初始化阶段主要做的事就是:JVM负责主要对类变量(类变量就是static修改的变量)进行初始化。
JVM负责主要对类变量(类变量就是static修改的变量)进行初始化主要有两个方式:

  1. 声明静态类变量时指定初始值
  2. 使用静态代码块为类变量指定初始值
    然后这里要注意一下,类变量进行显示赋值和使用静态代码块对类变量进行赋值,这里谁优先呢?
    这个是看顺序的,比如下面的代码,为什么会这样,暂时不知道,记住就好了
    image

摘抄: https://blog.csdn.net/lijingjingchn/article/details/111605394?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_baidulandingword~default-1.pc_relevant_default&spm=1001.2101.3001.4242.2&utm_relevant_index=4

6 ThreadLocal原理

ThreadLocal变量其实就是“线程局部变量":简单的说就是,其在一个线程中是共享的,在不同线程之间是隔离的。
这里来看一看ThreadLocal的代码实现:主要就看三个方法的实现: set、get、remove。

getMap

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

set

   public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

set

private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

get

    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();
    }

getEntry

    private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

remove

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

remove

        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

7. 什么是MYSQL回表查询?

mysql中PK和UK分别是unique key和primary key,两者区别:
主键值必须是唯一且非空的;唯一键必须是唯一可以为空的。
select id, name where name = "shanghai"
select id, name, sex where name = "shanghai"
多查询一个属性,为何检索过程会完全不同?
什么是回表查询?什么是索引覆盖?如何实现索引覆盖?哪些场景可以利用索引覆盖来优化SQL?

回表查询

InnoDB的索引实现有两大索引:
聚簇索引和普通索引
聚簇索引和普通索引有什么差异?
InnoDB聚簇索引 的叶子结点存储行记录,因此InnoDB必须要有,且只有一个聚簇索引:

  1. 如果表定义了PK,则PK就是聚簇索引
  2. 如果表没有定义PK,则第一个not NULL unique列是聚集索引
  3. 否则,InnoDB会创建一个隐藏row-id作为聚集索引
    tips: 所以PK查询非常快,直接定位行记录。
    InnoDB普通索引的叶子结点存储主键值。
    tips: 普通索引的叶子结点不是存储的是记录头指针,而是存储的主键值,MyISAM的索引叶子存储记录指针。
    因此: 从普通索引无法直接定位行记录,那普通索引的查询过程是怎么样的呢?
    通常情况下: 需要扫描两遍索引树。
    这就是所谓的 回表查询, 先定位主键值,再定位行记录,它的性能较扫描一遍索引树更低。

索引覆盖

MYSQL 官网,类似的说话出现在explain查询计划优化章节,即EXPLAIN的输出结果为Extra字段为Using index时,能够触发索引覆盖。
Using Index(JSON property: using index)
the column information is retrieved from the table using only information in the index tree without having to do an additional seek to read the actual row. This strategy can be used when the quey uses only columns that are part of a single index.
只需要在一棵索引树上就能获得SQL所需的所有列数据,无需回表,速度更快。

实现索引覆盖

常见方法: 将被查询的字段,建立到联合索引里去

  1. 添加主键: alter table tbl_name add primary key(col_list)

  2. 添加唯一索引: alter table tbl_name add unique index_name(col_list)

  3. 添加普通索引: alter table tabl_name add index index_name(col_list)

  4. 添加全文索引: alter table table_name add fulltext index_name(col_list)

  5. 删除索引: alter table tbl_name drop index index_name
    alter table tabl_name drop primary key

user(id, name, sex, index) pk: id index(name)
select id, name from user where name = 'shanghai'
能够命中name索引,索引叶子结点存储了主键ID(value),通过name的索引树即可获取id和name,无需回表,符合素音覆盖,效率较高。这个时候会出现:
Extra: Using index

select id, name, sex from user where name = 'shanghai';
能够命中name索引,索引叶子结点存储了主键id,但是sex字段必须回表才能获取到,不符合索引覆盖,需要再次通过id值去 检索聚簇索引树来获取sex字段,效率会降低。
Extra: Using index condition
这个时候运行: alter table user add index index_name_sex(name,sex)
然后explain的结果为 extra: using index

哪些场景可以利用索引覆盖来优化SQl?

场景1: 全表count查询优化
explain select count(name) from user;
extra: null
alter table user add key(name)
explain select count(name) from user;
extra: using index

场景2: 列查询回表优化
将单列索引升级为联合索引(name,sex),即可避免回表

场景3: 分页查询
select id, name, sex ... order by name limit 500,100;
将单列索引(name) 升级为联合索引(name, sex),也可以避免回表。

MySQL 列少的字段不建议使用索引

选择性低(🈯️字段种类比较少,比如性别字段只有男,女)
但经常出现在where条件中的字段到底建不建立索引?
不建议建立索引,意义不大。

摘抄: https://www.cnblogs.com/huangwentian/p/14605645.html

8. explain中type的解释

mysql为我们提供了很有用的辅助武器explain,它向我们展示了mysql接收到一条sql语句的执行计划。
虽然 explain返回的结果项很多,这里我们只关注三种,分别是type,key,rows。
其中key表明的是这次查找中所用到的索引,rows是指这次查找数据所扫描的行数。而type则是本文要详细记录的连接类型。

最常用的type类型: all,index, range, ref, eq_ref, const。从左到右,它们的效率依次是增强的。
all: 全表扫描, 不是主键也不是索引
explain select * from employee order by no ; (all, using filesort)

index: 另外一种形式的全表扫描,只不过它的扫描顺序是按照索引的顺序。根据索引然后回表随机取数据,因此index不可能会比all快。
快的可能性在于: 按照索引扫描全表的数据是有序的。
explain select * from emplyee order by rec_id (index)
type : index
extra: using index : 覆盖索引
explain select rec_id from employee ;(index, using index)
range: 指有范围的索引扫描,相对于index的全索引扫描,它有范围限制,因此要优于index。
如果出现了range那么一定是基于索引。 between, and 以及 < > , in 和or也是索引范围扫描。

ref: 查找条件列使用了索引而且不为主键和unique。
意思就是虽然使用了索引,但该索引列的值并不唯一,有重复。这样即使使用索引快速查找到了第一条数据,仍然不能停止,要进行目标值附近的小范围扫描。但它的好处是它并不需要扫全表,因为索引是有序的,即便有重复值,也是在一个非常小的范围内扫描。
explain select * from employee where name = '张三';(ref, using index condition)

ref_eq: ref_eq 与 ref相比牛的地方是,它知道这种类型的查找结果集只有一个?什么情况下结果集只有一个呢!那便是使用了主键或者唯一性索引进行查找的情况,比如根据学号查找某一学校的一名同学,在没有查找前我们就知道结果一定只有一个,所以当我们首次查找到这个学号,便立即停止了查询。这种连接类型每次都进行着精确查询,无需过多的扫描,因此查找效率更高,当然列的唯一性是需要根据实际情况决定的。
explain select ep.name,sc.mark from employee ep,score sc where ep.rec_id = sc.stu_id;(all , eq_ref)

const: 通常情况下,如果将一个主键放置到where后面作为条件查询,mysql优化器就能把这次查询优化转化为一个常量。至于如何转化以及何时转化,这个取决于优化器。

刚刚自己去做实验,all, range, ref, const都挺好复现的。

9 MYSQL数据库索引的最左匹配原则

建立三个字段的联合索引(a,b,c) ,相当于建立了索引: (a) (a,b,c) (a,b)
ac是否能用到索引呢?
a可以命中索引(a,b,c),c无法命中,所以ac组合无法命中联合索引
image

直接建立ac两个字段得联合索引
image

ab 索引查询
image

abc索引,acb会走索引吗?
最左前缀匹配原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,
比如a=3 and b=4 and c>5 and d=6如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。

摘抄: https://www.jb51.net/article/229609.htm

10 MYSQL 数据库索引以及失效场景详解

什么是索引,索引就是排好序的快速查找数据结构。
提高数据检索的效率, 降低数据库的IO成本。
通过索引列对数据进行排序, 降低数据排序的成本, 降低了CPU的消耗。
虽然索引大大提高了查询速度, 同时却会降低更新表的速度, 如对表进行INSERT、 UPDATE和DELETE。 因为更新表时, MySQL不仅要保存数据, 还要保存一下索引文件每次更新添加了索引列的字段, 都会调整因为更新所带来的键值变化后的索引信息。

适合创建索引和不适合创建索引的情况

适合:
1.主键自动建立唯一索引;
2.频繁作为查询条件的字段应该创建索引
3.查询中与其它表关联的字段, 外键关系建立索引
4.单键/组合索引的选择问题, 组合索引性价比更高
5.查询中排序的字段, 排序字段若通过索引去访问将大大提高排序速度
6.查询中统计或者分组字段
不适合:
1.表记录太少
2.经常增删改的表或者字段
3.Where 条件里用不到的字段不创建索引
4.过滤性不好的不适合建索引

索引失效场景

  1. or: 减少使用or
  2. like: like的前后模糊匹配
  3. null: 字段的is not null 与is null
  4. 不等: 使用<>
  5. 索引覆盖: 尽量使用索引覆盖
  6. 索引范围: 索引列上不能有范围查询,比如大于,小于,大于等于,小于等于
  7. 索引计算: 不要在索引上做任何计算
  8. 最佳做前缀: 查询条件的列与索引列的字段相同,顺序不同,从不同顺序列开始后边都不走索引。




10. SQL语句从开始运行到查出结果过程中数据库里发生了什么?

image
Mysql逻辑架构 分为 1. Serve 层, 2. 存储引擎层

Server层

连接器

是指通常我们所看见的在创建一个表的时候需要连接数库,所以在连接数据库时候就需要用到连接器,保证客户端能连接到数据库。连接器就负责跟客户端建立连接获取权限,维持和管理连接。
在连接完成后,如果没有其他的操作便处于空闲状态,默认8小时自动断开连接,当下次进行操作的时候需要重新连接数据库。
但是在连接之前咱们还有一个必不可少的环节,那就是需要通过TCP的三次握手连接MySQL服务。具体三次握手的过程是什么样的,详情见:TCP三次握手四次挥手
如果用户名或密码不对,你就会收到一个"Access denied for user"的错误,然后客户端程序结束执行。
如果用户名密码认证通过,连接器会到权限表里面查出你拥有的权限。之后,这个连接里面的权限判断逻辑,都将依赖于此时读到的权限

查询缓存

当连接器连接成功之后,所执行的SQL从缓存查询结果,如果有结果直接返回,没结果就继续往下走,走分析器,在查询的时候在SQL语句中写上关键字SQL_CACHE,表示要查缓存。在MySQL8.0之后就取消了查询缓存这一过程,取消了缓存。

分析器

如果没有命中查询缓存,就要开始真正执行语句了。首先,MySQL需要知道你要做什么,因此需要对SQL语句做解析。分析器分析分为两个阶段,分别是词法分析和语法分析

  • 词法分析: 识别出里面的字符串分别是什么,代表什么,识别哪些是关键字,哪些不是关键字
  • 语法分析: 将SQL语句解析成一颗语法树

如果要查询的字段在数据表中不存在,在词法分析中就会出错。

优化器

将根据执行的SQL,决定执行那部分,比如说有联表查询,通过join连接,则优化器就会判断先连接那个表,表里有多个索引的时候决定用那个索引,最终生成一个执行计划进行执行。

执行器

MySQL通过分析器知道了你要做什么,通过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句,返回查询结果。

参考: https://blog.csdn.net/yyp0304Devin/article/details/117405333

11. 数据库中数据的主从复制

写操作,直接操作主库,读操作,直接操作从库,这种结构称为读写分离。
MySQL数据库默认是支持主从复制的,不需要借助于其他的技术,我们只需要在数据库中简单的配置即可。
MySQL主从复制是一个异步的复制过程,底层是基于Mysql数据库自带的 二进制日志 功能。
就是一台或多台MySQL数据库(slave,即从库)从另一台MySQL数据库(master,即主库)进行日志的复制,然后再解析日志并应用到自身,最终实现 从库 的数据和 主库 的数据保持一致。MySQL主从复制是MySQL数据库自带功能,无需借助第三方工具。
二进制日志: 二进制日志(BINLOG)记录了所有的 DDL(数据定义语言)语句和 DML(数据操纵语言)语句,但是不包括数据查询语句。此日志对于灾难时的数据恢复起着极其重要的作用,MySQL的主从复制, 就是通过该binlog实现的。默认MySQL是未开启该日志的。

MySQL复制过程分成三步:
1). MySQL master 将数据变更写入二进制日志( binary log)

2). slave将master的binary log拷贝到它的中继日志(relay log)

3). slave重做中继日志中的事件,将数据变更反映它自己的数据

SQL题:

  1. 查询某个月销量前十的商品名称
    select 商品名称 from 商品列表
    where 商品ID in (
    select 商品ID, sum(商量数量) 销量总数量
    from 销售表
    where 销售时间 between xxx and xxx
    group by 商品ID
    order by 销量总数量 desc
    )
    ------------恢复内容开始------------

1. Object的方法,9大方法

  1. public final native Class<?> getClass();
    返回 运行时对象的类别

  2. public native int hashCode();
    返回 这个对象的一个 hash code

  3. public boolean equals(Object obj)
    返回 表示 是否另外一个对象是否 和这个对象 "相同"

  4. protected native Object clone() throws CloneNotSupportedException;
    返回 这个实例的一个克隆

  5. public String toString()
    返回 一个string 代表这个对象

  6. public final native void notify();

  7. public final void wait() throws InterruptedException {}

  8. public final native void notifyAll();
    线程在运行的时候,如果发现某些条件没有背满足,可以调用wait方法暂停自己的执行,并且放弃已经获得的锁,然后进入等待状态。当该线程被其他线程唤醒并获得锁后,可以沿着之前暂停的地方继续向后执行,而不是而不是再次从同步代码块开始的地方开始执行。需要注意的一点是,**对线程等待的条件的判断要使用while而不是if来进行判断。这样在线程被唤醒后,会再次判断条件是否满足。

  9. protected void finalize() throws Throwable { }
    当没有任何引用指向这个对象时,产生的一个回调。

补充 2. 实现生产者消费者

package thread;

import java.util.Deque;
import java.util.LinkedList;

/***
 * @Description: "实现生产者消费者"
 * @Author: ZBZ
 * @Date: 2022/4/6
 */
public class ProducerConsumer {

    /**
     * 生产者
     */
    public static class Producer extends Thread {

        Deque<Integer> queue;
        int maxSize;

        public Producer(Deque<Integer> queue, int maxSize, String name) {
            this.queue = queue;
            this.maxSize = maxSize;
            this.setName(name);
        }

        @Override
        public void run() {
            super.run();
            while (true) {
                synchronized (queue) {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {

                    }
                    System.out.println(this.getName() + "获得队列的锁");
                    while (queue.size() == maxSize) {
                        System.out.println("队列已满, 生产者" + this.getName() + "等待");
                        try {
                            queue.wait();
                        } catch (InterruptedException e) {

                        }
                    }

                    int num = (int) (Math.random() * 100);
                    queue.offer(num);

                    System.out.println(this.getName() + "生产了一个元素:" + num);

                    queue.notifyAll();

                    System.out.println(this.getName() + "退出一次生产过程!");
                }
            }
        }
    }

    /**
     * 消费者
     */
    public static class Consumer extends Thread {

        Deque<Integer> queue;
        int maxSize;

        public Consumer(Deque<Integer> queue, int maxSize, String name) {
            this.queue = queue;
            this.maxSize = maxSize;
            this.setName(name);
        }

        @Override
        public void run() {
            super.run();
            while (true) {
                synchronized (queue) {
                    try {
                        Thread.sleep(0);
                    } catch (InterruptedException e) {

                    }
                    System.out.println(this.getName() + "获得队列的锁");

                    while (queue.isEmpty()) {
                        System.out.println("队列为空, 消费者" + this.getName() + "等待");
                        try {
                            queue.wait();
                        } catch (InterruptedException e) {

                        }
                    }
                    int num = queue.poll();
                    System.out.println(this.getName() + "消费一个元素" + num);

                    queue.notifyAll();
                    System.out.println(this.getName() + "退出一次消费过程");

                }
            }
        }
    }


    public static void main(String[] args) {
        Deque<Integer> queue = new LinkedList<>();
        int maxSize = 5;

        Producer producer = new Producer(queue, maxSize, "Producer");
        Consumer consumer = new Consumer(queue, maxSize, "Consumer1");
        Consumer consumer2 = new Consumer(queue, maxSize, "Consumer2");
        Consumer consumer3 = new Consumer(queue, maxSize, "Consumer3");

        producer.start();
        consumer.start();
        consumer2.start();
        consumer3.start();

    }

}

3. synchronized方法讲解

synchronized是Java中的关键字,是一种同步锁。 它修饰的对象有以下几种:

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括{}括起来的代码,作用的对象是调用这个代码块的对象。
  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。
  3. 修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象。
  4. 修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象。

3.1 修饰一个代码块

一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程被阻塞。

当两个并发线程访问同一个对象的synchronized代码块时,在同一时刻只能有一个线程得到执行,另一个线程阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。
synchronized只锁定对象,每个对象只有一个锁(lock)与之相关联。

3.2 修饰一个方法

Synchronized修饰一个方法很简单,就是在方法的前面加synchronized,public synchronized void method(){}; synchronized修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数。如将的run方法改成如下的方式,实现的效果一样。

在使用synchronized修饰方法时要注意以下几点:

  1. synchronized关键字不能被继承
    虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。这两种方式的例子代码如下:
    在子类方法中加上synchronized关键字

  2. 在定义接口方法时不能使用synchronized关键字。

  3. 构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。
    使用当前对象当作锁,但是当前对象都没产生,所以是不能修饰的。

3.3 修饰一个静态方法

静态方法是属于类的而不是属于对象的。 同样的,synchronized修饰的静态方法绑定的是这个类的 所有对象

3.4 修饰一个类

效果和synchronized修饰静态方法是一样的,synchronized作用于一个类T时,是给这个类T加锁,T的锁哟u对象使用的是同一把锁。
image

总结:

  1. 无论synchroniz3ed关键字加在方法上还是对象上,如果它的作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象使用的是同一把锁。
  2. 每个对象只有一个锁与之相关联,谁拿到这把锁,谁就可以运行它所控制的那段代码。
  3. 实现同步是需要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

摘抄: https://www.cnblogs.com/weibanggang/p/9470718.html

4. synchronized方法实现原理

查看带有synchronized语句块的字节码文件,
在同步代码块的起始位置插入了moniterenter指令,
在同步代码块结束的位置插入了moniterexit指令。
image
image

查看同步方法的字节码文件时,同步方法并没有通过指令moniterenter和moniterexit来完成,而是被翻译成普通的方法啊

5. Java类的加载过程

一个java文件从编码到最终执行,一般主要包括两个过程: 编译+执行。
我们所说的类加载过程就是JVM虚拟机把.class文件中 类信息加载进内存,并进行解析生成对应的class对象的过程
举个例子:JVM在执行某段代码,遇到了class A,此次内存中没有class A,于是JVM就会到相应的class文件中寻找class A的类信息,并加载进内存。
由此可见,JVM不是一开始就把所有的类都加载进内存,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次。

类加载过程主要分为三个部分: 加载、链接、初始化三个阶段。

加载

加载: 把class字节码文件从各个来源通过类加载器装载入 内存。
来源一般包括本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译。
类加载器:启动类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器。
加载阶段,虚拟机需要完成以下三件事:

  1. 通过一个类的全限定名来获得其定义的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在Java堆中生存一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

连接

连接阶段可以分为3个小阶段: 验证,准备,解析

连接- 验证

验证:主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。
文件格式的验证、元数据的验证、字节码的验证、符号引用的验证
验证做的具体内容:

  1. 文件格式验证: 验证字节流是否符合Class文件格式的规范
  2. 元数据验证:对字节码描述的信息进行语义分析
  3. 字节码验证:通过数据流和控制流分析,确定程序是合法的
  4. 符号引用验证:确保解析动作能正确执行
    验证阶段是非常重要的,但不是必须的,对程序运行期没有影响,如果所引用的类经过反复验证,可以考虑采用-Xverifnone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

连接- 准备

在准备阶段,将静态变量的初值赋为jvm默认的初值,而不是我们在程序中设定的初值。jvm默认为静态变量的初值是这样的

  1. 静态变量是基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0
  2. 静态变量时引用类型的默认值为null
  3. 被final和static共同修饰的静态变量,我们通常称为常量,然后常量的默认值为我们程序中设定的值,
    比如我们在程序中定义final static int a = 100,则准备阶段中a的初值就是100。
    为什么被final和static变量修饰的成员变量在准备阶段的赋值会比较特别呢,这是因为,被final和static修改的变量,我们叫做ConstantValue属性。

连接- 解析

这一阶段的任务就是把常量池中的符号引用转换为直接引用。
什么是符号引用,什么是直接引用?
符号引用:即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
直接引用:可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针。

初始化阶段

类加载过程中的初始化阶段主要做的事就是:JVM负责主要对类变量(类变量就是static修改的变量)进行初始化。
JVM负责主要对类变量(类变量就是static修改的变量)进行初始化主要有两个方式:

  1. 声明静态类变量时指定初始值
  2. 使用静态代码块为类变量指定初始值
    然后这里要注意一下,类变量进行显示赋值和使用静态代码块对类变量进行赋值,这里谁优先呢?
    这个是看顺序的,比如下面的代码,为什么会这样,暂时不知道,记住就好了
    image

摘抄: https://blog.csdn.net/lijingjingchn/article/details/111605394?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_baidulandingword~default-1.pc_relevant_default&spm=1001.2101.3001.4242.2&utm_relevant_index=4

6 ThreadLocal原理

ThreadLocal变量其实就是“线程局部变量":简单的说就是,其在一个线程中是共享的,在不同线程之间是隔离的。
这里来看一看ThreadLocal的代码实现:主要就看三个方法的实现: set、get、remove。

getMap

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

set

   public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

set

private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

get

    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();
    }

getEntry

    private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

remove

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

remove

        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

7. 什么是MYSQL回表查询?

mysql中PK和UK分别是unique key和primary key,两者区别:
主键值必须是唯一且非空的;唯一键必须是唯一可以为空的。
select id, name where name = "shanghai"
select id, name, sex where name = "shanghai"
多查询一个属性,为何检索过程会完全不同?
什么是回表查询?什么是索引覆盖?如何实现索引覆盖?哪些场景可以利用索引覆盖来优化SQL?

回表查询

InnoDB的索引实现有两大索引:
聚簇索引和普通索引
聚簇索引和普通索引有什么差异?
InnoDB聚簇索引 的叶子结点存储行记录,因此InnoDB必须要有,且只有一个聚簇索引:

  1. 如果表定义了PK,则PK就是聚簇索引
  2. 如果表没有定义PK,则第一个not NULL unique列是聚集索引
  3. 否则,InnoDB会创建一个隐藏row-id作为聚集索引
    tips: 所以PK查询非常快,直接定位行记录。
    InnoDB普通索引的叶子结点存储主键值。
    tips: 普通索引的叶子结点不是存储的是记录头指针,而是存储的主键值,MyISAM的索引叶子存储记录指针。
    因此: 从普通索引无法直接定位行记录,那普通索引的查询过程是怎么样的呢?
    通常情况下: 需要扫描两遍索引树。
    这就是所谓的 回表查询, 先定位主键值,再定位行记录,它的性能较扫描一遍索引树更低。

索引覆盖

MYSQL 官网,类似的说话出现在explain查询计划优化章节,即EXPLAIN的输出结果为Extra字段为Using index时,能够触发索引覆盖。
Using Index(JSON property: using index)
the column information is retrieved from the table using only information in the index tree without having to do an additional seek to read the actual row. This strategy can be used when the quey uses only columns that are part of a single index.
只需要在一棵索引树上就能获得SQL所需的所有列数据,无需回表,速度更快。

实现索引覆盖

常见方法: 将被查询的字段,建立到联合索引里去

  1. 添加主键: alter table tbl_name add primary key(col_list)

  2. 添加唯一索引: alter table tbl_name add unique index_name(col_list)

  3. 添加普通索引: alter table tabl_name add index index_name(col_list)

  4. 添加全文索引: alter table table_name add fulltext index_name(col_list)

  5. 删除索引: alter table tbl_name drop index index_name
    alter table tabl_name drop primary key

user(id, name, sex, index) pk: id index(name)
select id, name from user where name = 'shanghai'
能够命中name索引,索引叶子结点存储了主键ID(value),通过name的索引树即可获取id和name,无需回表,符合素音覆盖,效率较高。这个时候会出现:
Extra: Using index

select id, name, sex from user where name = 'shanghai';
能够命中name索引,索引叶子结点存储了主键id,但是sex字段必须回表才能获取到,不符合索引覆盖,需要再次通过id值去 检索聚簇索引树来获取sex字段,效率会降低。
Extra: Using index condition
这个时候运行: alter table user add index index_name_sex(name,sex)
然后explain的结果为 extra: using index

哪些场景可以利用索引覆盖来优化SQl?

场景1: 全表count查询优化
explain select count(name) from user;
extra: null
alter table user add key(name)
explain select count(name) from user;
extra: using index

场景2: 列查询回表优化
将单列索引升级为联合索引(name,sex),即可避免回表

场景3: 分页查询
select id, name, sex ... order by name limit 500,100;
将单列索引(name) 升级为联合索引(name, sex),也可以避免回表。

MySQL 列少的字段不建议使用索引

选择性低(🈯️字段种类比较少,比如性别字段只有男,女)
但经常出现在where条件中的字段到底建不建立索引?
不建议建立索引,意义不大。

摘抄: https://www.cnblogs.com/huangwentian/p/14605645.html

8. explain中type的解释

mysql为我们提供了很有用的辅助武器explain,它向我们展示了mysql接收到一条sql语句的执行计划。
虽然 explain返回的结果项很多,这里我们只关注三种,分别是type,key,rows。
其中key表明的是这次查找中所用到的索引,rows是指这次查找数据所扫描的行数。而type则是本文要详细记录的连接类型。

最常用的type类型: all,index, range, ref, eq_ref, const。从左到右,它们的效率依次是增强的。
all: 全表扫描, 不是主键也不是索引
explain select * from employee order by no ; (all, using filesort)

index: 另外一种形式的全表扫描,只不过它的扫描顺序是按照索引的顺序。根据索引然后回表随机取数据,因此index不可能会比all快。
快的可能性在于: 按照索引扫描全表的数据是有序的。
explain select * from emplyee order by rec_id (index)
type : index
extra: using index : 覆盖索引
explain select rec_id from employee ;(index, using index)
range: 指有范围的索引扫描,相对于index的全索引扫描,它有范围限制,因此要优于index。
如果出现了range那么一定是基于索引。 between, and 以及 < > , in 和or也是索引范围扫描。

ref: 查找条件列使用了索引而且不为主键和unique。
意思就是虽然使用了索引,但该索引列的值并不唯一,有重复。这样即使使用索引快速查找到了第一条数据,仍然不能停止,要进行目标值附近的小范围扫描。但它的好处是它并不需要扫全表,因为索引是有序的,即便有重复值,也是在一个非常小的范围内扫描。
explain select * from employee where name = '张三';(ref, using index condition)

ref_eq: ref_eq 与 ref相比牛的地方是,它知道这种类型的查找结果集只有一个?什么情况下结果集只有一个呢!那便是使用了主键或者唯一性索引进行查找的情况,比如根据学号查找某一学校的一名同学,在没有查找前我们就知道结果一定只有一个,所以当我们首次查找到这个学号,便立即停止了查询。这种连接类型每次都进行着精确查询,无需过多的扫描,因此查找效率更高,当然列的唯一性是需要根据实际情况决定的。
explain select ep.name,sc.mark from employee ep,score sc where ep.rec_id = sc.stu_id;(all , eq_ref)

const: 通常情况下,如果将一个主键放置到where后面作为条件查询,mysql优化器就能把这次查询优化转化为一个常量。至于如何转化以及何时转化,这个取决于优化器。

刚刚自己去做实验,all, range, ref, const都挺好复现的。

9 MYSQL数据库索引的最左匹配原则

建立三个字段的联合索引(a,b,c) ,相当于建立了索引: (a) (a,b,c) (a,b)
ac是否能用到索引呢?
a可以命中索引(a,b,c),c无法命中,所以ac组合无法命中联合索引
image

直接建立ac两个字段得联合索引
image

ab 索引查询
image

abc索引,acb会走索引吗?
最左前缀匹配原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,
比如a=3 and b=4 and c>5 and d=6如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。

摘抄: https://www.jb51.net/article/229609.htm

10 MYSQL 数据库索引以及失效场景详解

什么是索引,索引就是排好序的快速查找数据结构。
提高数据检索的效率, 降低数据库的IO成本。
通过索引列对数据进行排序, 降低数据排序的成本, 降低了CPU的消耗。
虽然索引大大提高了查询速度, 同时却会降低更新表的速度, 如对表进行INSERT、 UPDATE和DELETE。 因为更新表时, MySQL不仅要保存数据, 还要保存一下索引文件每次更新添加了索引列的字段, 都会调整因为更新所带来的键值变化后的索引信息。

适合创建索引和不适合创建索引的情况

适合:
1.主键自动建立唯一索引;
2.频繁作为查询条件的字段应该创建索引
3.查询中与其它表关联的字段, 外键关系建立索引
4.单键/组合索引的选择问题, 组合索引性价比更高
5.查询中排序的字段, 排序字段若通过索引去访问将大大提高排序速度
6.查询中统计或者分组字段
不适合:
1.表记录太少
2.经常增删改的表或者字段
3.Where 条件里用不到的字段不创建索引
4.过滤性不好的不适合建索引

索引失效场景

  1. or: 减少使用or
  2. like: like的前后模糊匹配
  3. null: 字段的is not null 与is null
  4. 不等: 使用<>
  5. 索引覆盖: 尽量使用索引覆盖
  6. 索引范围: 索引列上不能有范围查询,比如大于,小于,大于等于,小于等于
  7. 索引计算: 不要在索引上做任何计算
  8. 最佳做前缀: 查询条件的列与索引列的字段相同,顺序不同,从不同顺序列开始后边都不走索引。




10. SQL语句从开始运行到查出结果过程中数据库里发生了什么?

image
Mysql逻辑架构 分为 1. Serve 层, 2. 存储引擎层

Server层

连接器

是指通常我们所看见的在创建一个表的时候需要连接数库,所以在连接数据库时候就需要用到连接器,保证客户端能连接到数据库。连接器就负责跟客户端建立连接获取权限,维持和管理连接。
在连接完成后,如果没有其他的操作便处于空闲状态,默认8小时自动断开连接,当下次进行操作的时候需要重新连接数据库。
但是在连接之前咱们还有一个必不可少的环节,那就是需要通过TCP的三次握手连接MySQL服务。具体三次握手的过程是什么样的,详情见:TCP三次握手四次挥手
如果用户名或密码不对,你就会收到一个"Access denied for user"的错误,然后客户端程序结束执行。
如果用户名密码认证通过,连接器会到权限表里面查出你拥有的权限。之后,这个连接里面的权限判断逻辑,都将依赖于此时读到的权限

查询缓存

当连接器连接成功之后,所执行的SQL从缓存查询结果,如果有结果直接返回,没结果就继续往下走,走分析器,在查询的时候在SQL语句中写上关键字SQL_CACHE,表示要查缓存。在MySQL8.0之后就取消了查询缓存这一过程,取消了缓存。

分析器

如果没有命中查询缓存,就要开始真正执行语句了。首先,MySQL需要知道你要做什么,因此需要对SQL语句做解析。分析器分析分为两个阶段,分别是词法分析和语法分析

  • 词法分析: 识别出里面的字符串分别是什么,代表什么,识别哪些是关键字,哪些不是关键字
  • 语法分析: 将SQL语句解析成一颗语法树

如果要查询的字段在数据表中不存在,在词法分析中就会出错。

优化器

将根据执行的SQL,决定执行那部分,比如说有联表查询,通过join连接,则优化器就会判断先连接那个表,表里有多个索引的时候决定用那个索引,最终生成一个执行计划进行执行。

执行器

MySQL通过分析器知道了你要做什么,通过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句,返回查询结果。

参考: https://blog.csdn.net/yyp0304Devin/article/details/117405333

11. 数据库中数据的主从复制

写操作,直接操作主库,读操作,直接操作从库,这种结构称为读写分离。
MySQL数据库默认是支持主从复制的,不需要借助于其他的技术,我们只需要在数据库中简单的配置即可。
MySQL主从复制是一个异步的复制过程,底层是基于Mysql数据库自带的 二进制日志 功能。
就是一台或多台MySQL数据库(slave,即从库)从另一台MySQL数据库(master,即主库)进行日志的复制,然后再解析日志并应用到自身,最终实现 从库 的数据和 主库 的数据保持一致。MySQL主从复制是MySQL数据库自带功能,无需借助第三方工具。
二进制日志: 二进制日志(BINLOG)记录了所有的 DDL(数据定义语言)语句和 DML(数据操纵语言)语句,但是不包括数据查询语句。此日志对于灾难时的数据恢复起着极其重要的作用,MySQL的主从复制, 就是通过该binlog实现的。默认MySQL是未开启该日志的。

MySQL复制过程分成三步:
1). MySQL master 将数据变更写入二进制日志( binary log)

2). slave将master的binary log拷贝到它的中继日志(relay log)

3). slave重做中继日志中的事件,将数据变更反映它自己的数据

12. Mysql中的几种log

binlog

binlog记录了数据库表结构和表数据变更,比如update/delete/insert/truncate/create。它不会记录select(因为这没有对表没有进行变更)
主要有两个作用: 复制和恢复数据
MySQL在公司使用的时候往往都是一主多从结构的,从服务器需要与主服务器的数据保持一致,这就是通过binlog来实现的
数据库的数据被干掉了,我们可以通过binlog来对数据进行恢复。

因为binlog记录了数据库表的变更,所以我们可以用binlog进行复制(主从复制)和恢复数据。

bin-log长什么样子?
可以使用mysqlbinlog命令去查看。
mysqlbinlog /data/mysql_data/bin.000008 --database EpointFrame --base64-output=decode-rows -vv --skip-gtids=true |grep -C 1 -i "delete from Audit_Orga_Specialtype" > /opt/sql.log

参考:https://www.jianshu.com/p/ec0796f95ec9

如何查看binlog是否打开,如果没打开怎么设置?
使用命令show variables like '%log_bin%',查看binlog是否打开

如果没有开启binlog,那怎么开启呢?
需要找到my.cnf配置文件,增加下面的配置(mysql:5.7)

打开binlog

log-bin=mysql-bin

选择row行模式

binlog-format=ROW
修改后,重启mysql,配置生效

binlog 日志的几种模式

  1. row行模式
    日志中回记录每一行数据被修改的形式,然后在slave端对相同的数据进行修改
    优点:在row level模式下,bin- log中可以不记录执行的sql语句的上下文相关信息,仅仅只需要记录那一条修改。
    不会出现某些特定的情况下的存储过程或function,以及trigger的调用和触发无法被正确复制的问题。
    缺点:所有的执行的语句当记录到日志中的时候,都将以每行记录到修改来记录,会产生大量的日志内容。

  2. stament模式
    每一条回修改数据的sql都会记录到master的bin-log中。slave在复制的时候sql进程会解析成和原来master端执行相同的sql来再次执行。
    优点:解决了row level下的缺点,不需要记录每一行数据的变化,减少bin-log日志量,节约IO,提高性能,因为它只需要在MASTER上锁执行的语句的细节,以及执行语句的上下文信息。
    缺点:修改数据的时候使用了某些特定归档函数或者功能的时候会出现。

  3. mixed模式
    MySQL会根据执行的每一条具体的sql语句来区分对待记录的日志格式。在statement和row之间选择一种。

执行show master status,可以查看当前写入的binlog文件名。

redo log

update user_table set name='java' where id = '1'
MySQL执行这条SQL语句,肯定是先把id=1的这条记录查出来,然后将name字段给改掉。
实际上Mysql的基本存储结构是页(记录都存在页里边),所以MySQL是先把这条记录所在的页找到,然后把该页加载到内存中,将对应记录进行修改。
现在就可能存在一个问题:如果在内存中把数据改了,还没来得及落磁盘,而此时的数据库挂了怎么办?显然这次更改就丢了。
如果每个请求都需要将数据立马落磁盘之后,那速度会很慢,MySQL可能也顶不住。

MySQL引入了redo log,内存写完了,然后会写一份redo log,这份redo log记载着这次在某个页上做了什么修改。

其实写redo log的时候,也会有buffer,是先写buffer,再真正落到磁盘中的。至于从buffer什么时候落磁盘,会有配置供我们配置。

写redo log也是需要写磁盘的,但它的好处就是顺序IO(我们都知道顺序IO比随机IO快非常多)。

所以,redo log的存在为了:当我们修改的时候,写完内存了,但数据还没真正写到磁盘的时候。此时我们的数据库挂了,我们可以根据redo log来对数据进行恢复。因为redo log是顺序IO,所以写入的速度很快,并且redo log记载的是物理变化(xxxx页做了xxx修改),文件的体积很小,恢复速度很快。

redo log 与事务
image

binlog和redo log都可以数据恢复,有什么区别?

  1. redo log是在InnoDB存储引擎层产生的,而binlog是在Mysql数据库的上层产生的,并且二进制日志不仅仅针对innodb存储引擎,mysql数据库中的任何存储引擎对于数据库的更改都会产生二进制日志。
  2. redo log是物理日志,binlog是逻辑日志。
  3. 两种日志写入磁盘的时间点不同,二进制日志只在事务提交完成后进行一次写入。而innodb存储引擎的redo log在事务进行中不断被写入,并且日志不是随事务提交的顺序写入的。
  4. binlog不是循环使用的,在写满或者重启之后,会生成新的binlog文件,redo log是循环使用。
  5. binlog可以作为恢复数据使用,主从复制搭建,redo log 作为异常宕机或者故障时后的数据恢复使用。

redo log 恢复在内存更新后,还没来得及刷到磁盘的数据。
binlog是存储所有数据变更的情况,理论上只要记录在binlog上的数据,都可以恢复。
举个例子,如果数据库的数据都被删除了,能使用redo log文件恢复吗?
不可以使用redo log文件恢复,只能使用binlog文件恢复。因为redo log文件不会存储历史所有的数据变更,当内存数据刷新到磁盘中,redo log的数据就失效了,也就是redo log文件内容是会被覆盖的。

undo log

undo log 作用主要用于回滚,mysql数据库的事务的原子性就是通过undo log实现的。
undo log主要存储的是数据的逻辑比那花,比如我们要insert一条数据,那么undo log就会生产一条对应的delete日志。
另一个作用就是实现多版本控制,undo记录中包含了记录更改前的镜像,如果更改数据的事务未提交,对于隔离级别大于等于read commit的事务而言,不应该返回更改后的数据,而应该返回老版本的数据。

慢日志

MySQL的慢查询日志是MySQL提供的一种日志记录,它用来记录在MySQL中响应时间超过阀值的语句,具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中。long_query_time的默认值为10,意思是运行10S以上的语句。默认情况下,Mysql数据库并不启动慢查询日志,需要我们手动来设置这个参数,当然,如果不是调优需要的话,一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响。慢查询日志支持将日志记录写入文件,也支持将日志记录写入数据库表。

摘抄: https://wenku.baidu.com/view/2140386dfd4733687e21af45b307e87101f6f8d3.html
摘抄: https://blog.csdn.net/weixin_43217065/article/details/106947337

SQL题:

  1. 查询某个月销量前十的商品名称
    select 商品名称 from 商品列表
    where 商品ID in (
    select 商品ID, sum(商量数量) 销量总数量
    from 销售表
    where 销售时间 between xxx and xxx
    group by 商品ID
    order by 销量总数量 desc
    )

------------恢复内容结束------------

posted @   CrazyShanShan  阅读(50)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
点击右上角即可分享
微信分享提示