22-08-22 西安 MySQL高级(01)MySQL逻辑架构、InnoDB存储引擎、SQL执行流程、javaSE面试题

一万五千年前,一根断裂又愈合的大腿骨头,标志着人类文明的起点。

大腿骨被折断是一件极其危险的事,这其实意味着死亡,因为无法逃避危险,不能去河
边喝水或狩猎食物,很快会被四处游荡的野兽吃掉。
而愈合的骨头则表明:有人花了很长时间来照顾受伤的人一一处理伤口,提供食物,保护
他不受攻击。
从困难中学会团结,学会帮助他人,人类文明由此诞生。

 在历史长河的一个又一个节点里,人类的命运都取决于自己的选择。


JavaSE部分面试题

1、== 和 equals的区别

你要是回答:‘==比较内存地址,equals比较值’,我很生气,还是哪凉快哪去吧

==既可以比较基本类型,又可以比较引用类型

基本类型:比较值是否相等,对于引用类型,比较内存地址是否相等

-------------------------

JVM,new一个对象,实际而言堆内存的新生区的伊甸园区里面。
结论:
也就是说,只要是new出来的对象,100%内存地址不可能相同,
所以,只要用==比较,一定是false

equals是Object类的方法,看源码知道equals只能比较引用类型

 //以下是Object类源码中的equals方法
    public boolean equals(Object obj) {
        return (this == obj);
    }

equals比较规则:

1.没有被重写过。equals方法就是==
2.被重写过,就具体问题,具体分析。以String类为例,equals就是比较每个字符是否相等进而决定字符串整体是否相等。

//以下为String类源码部分
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

2、方法中实参变量值传递

java里到底是传递值还是传递引用?注意变量的作用域范围

一个方法里,定义的局部变量且基本类型

则传递的是复印件,原件不动。

一个方法里,定义的局部变量且是引用类型,

传递的是内存地址

测试代码

public class TestValue {
    public static void main(String[] args) {
        TestValue tv = new TestValue();
        int age=20;
        tv.c1(age);//基本类型传递的是复印件
        System.out.println("age---"+age);

        User user = new User("坦然");
        tv.c2(user);//引用类型传递的是地址值
        System.out.println("userName---"+user.getName());

        String str="nbw";//字符串常量池
        tv.c3(str);//引用类型传递的是地址值
        System.out.println("str---"+str);
    }
    public void c1(int age){
     age=30;
    }
    public void c2(User user){
        user.setName("清融");
    }
    public void c3(String str){
        str="xyg";//字符串常量池
    }
}

运行结果

结果分析,前2个打印结果就不说了,主要分析第三个

String有点特殊,字符串常量池

tv.c3(str) //传递的也是地址值

 字符串常量池,池子里面有直接复用,没有新建

   str="xyg";  //执行这个的时候,新建了一块内存放“xyg”,所以这俩str一个指向“xyg”,一个指向“nbw”。

3、HashSet的add方法

@Test
public void test5(){
    HashSet set = new HashSet();
    User u1 = new User(1001L, "AA");
    User u2 = new User(1002L, "BB");
    set.add(u1);
    set.add(u2);
    System.out.println(set);
    //1.考点一 删除的时候会先计算 (1001L, "CC")的哈希值
    u1.setName("CC");
    boolean b = set.remove(u1);
    System.out.println("是否删除成功: "+b);
    System.out.println("考点一:"+set);
    //2.考点二
    set.add(new User(1001L,"CC"));
    System.out.println("考点二: "+set);
    //3、考点三
    set.add(new User(1001L,"AA"));
    System.out.println("考点三: "+set);
}

运行结果:当然前提是你要重写User中的equals方法和hashcode方法 ,或者加个@Data注解

考点1:先算出来(1001L,“CC”)的哈希值,它极大概率是和(1001L,“AA”)、(1002L,“BB”)不一样的,所以在remove删除判断的时候,并没有找到该hashcode,删除失败。

考点2:既然这个位置(1001L,“CC”)的哈希值,没有东西,那就自然可以加的进去了,所以set里元素是3个,且看着俩个User长的一样

考点3:再加(1001L,“AA”),它和现在的第一处的(1001L,“CC”)是在同一处数组的索引,所以再在这个元素后面加链


4、HashMap初始化容量问题

设置初始容量大小的必要性

HashMap中的扩容机制决定了每次扩容都需要重建hash表,是非常影响性能的。 随着元素的不断增加,HashMap会有可能发生多次扩容

设置初始化容量为多少

结论:

当我们明确知道HashMap中元素的个数(initialCapacity )的时候,把默认容量(构造器的参数)设置成 initialCapacity/ 0.75F + 1.0F

但是,JDK并不会直接拿用户传进来的数字当做默认容量,而是会进行一番运算,最终得到一个2的幂

不使用这个公式:

如果我们设置的默认值是7(明确要存储7个元素,然后构造器的参数也就傻乎乎的传了7),经过Jdk处理之后,会被设置成8,但是,这个HashMap在元素个数达到 8*0.75 = 6的时候就会进行一次扩容,这明显是我们不希望见到的。

使用公式 initialCapacity/ 0.75F + 1.0F

7/0.75 + 1 = 10 ,10经过Jdk处理之后,会被设置成16,这就大大的减少了扩容的几率。

为什么必须是2的n次幂?如果输入值不是2的幂比如10会怎么样?

在确定要存储的元素在数组中的具体位置时,HashMap用某种算法尽量把数据分配均匀,这样每个链表长度大致相同,这个算法实际就是取模。

但是:计算机中直接求余效率不如位移运算

实际上hash%length等于hash&(length-1)的前提是length是2的n次幂

2的n次方实际就是1后面n个0,2的n次方-1 实际就是n个1

20%16=4

20&(2^4-1)=20&15= 4

   0001 0100

& 0000 1111

------------------

   0000 0100

默认情况下HashMap的容量是16,但是,如果用户通过构造函数指定了一个数字作为容量,那么Hash会选择大于该数字的第一个2的幂作为容量。(3->4、7->8、9->16)

//HashMap类源码
//构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

存储引擎

MySQL从上到下分为4层:连接-服务-引擎-存储

1、MySQL逻辑架构

mysql插件式的存储引擎架构将查询处理和其它的系统任务以及数据的存储提取相分离。让我们可以根据业务的需求和实际需要选择合适的存储引擎。

连接层,并不属于mysql

  • java----jdbc---mysql

服务层

  • 查询mysql的自带的缓存,有的话不用调用mysql更多的后台内容
  • SQL接口分析,分析sql语句的语法,语法验证
  • 漏斗分分类,到底是读还是写、还是定义语句
  • 查询优化器,mysql自身认为最优的效果返回,不见得你最喜欢

引擎层,插件式的存储引擎架构,有10多个以上。

  • 用的最多的MyISAM、InnoDB
在不同的环境,可以拔插式处理,换不同的引擎,mysql还是那个mysql,维护成本急剧下降

存储层

  • 数据真正是放在硬盘上

2、存储引擎InnoDB和MyISAM

查看MySQL的存储引擎,默认是Innodb

SHOW ENGINES;

结果解析 

InnoDB和MyISAM区别

前3个要知道。。。

MyISAM 一锁就是锁住了整张表,在并发写的时候InnoDB性能远超MyISAM。而 InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁

MyISAM允许没有主键的表存在。InnoDB表必须有主键,如果没有设定主键,就会自动生成一个 6 字节隐藏列作为主键列(用户不可见)。

MyISAM使用非聚集索引,索引和数据分开,只缓存索引;InnoDB使用聚集索引,主索引和数据存在一个文件。


3、SQL执行流程

SQL语句在MySQL中的执行流程是:

1.查询缓存就是如果在缓存中发现了这条SQL语句,就会直接将结果返回给客户端了;如果没有命中缓存,再进入到解析器阶段。但是缓存的命中率并不高,MySQL8.0开始就没有缓存了

# 查看缓存的状态
SHOW VARIABLES LIKE '%query_cache_type';

query_cache_type有3个值,

  • 0代表关闭查询缓存OFF
  • 1代表关闭查询缓存ON
  • 2代表DEMAND(按需使用)
#SQL_CACHE 指定该条sql使用查询缓存
SELECT SQL_CACHE * FROM test WHERE id=5;

#SQL_NO_CACHE 指定该条sql使用不使用缓存
SELECT SQL_NO_CACHE * FROM test WHERE id=5;

2.解析器,也叫分析器。在解析器中对SQL语句进行语法分析、语义分析。SQL没啥问题就会生成解析树。

Mysql服务端程序要从文本中将具体的请求含义解析出来,如查询什么字段,查询哪一些表等

经过解析器,MySQL就知道你要做什么了

3.优化器,在优化器中会确定SQL语句的执行路径。比如全表检索还是根据索引检索。优化的结果就是生成一个执行计划,就是平常我们使用Explain关键字看到的一个结果。

一条查询SQL可以有很多种执行方式,最后都会返回相同的结果。优化器的作用就是找到这其中最好的执行计划

4.执行器。优化器会把执行计划交给执行器,执行器要先判断用户是否具备权限,如果有权限,就打开表继续执行。打开表的时候,执行器会根据表的引擎定义,调用存储引擎API对表进行读写。

Mysql中,将对数据存储和提取的操作抽取到了一个叫存储引擎的模块中。在逻辑上,我们看到的是表的数据是一行行的形式,但实际物理层面上,表的数据如何存储、如何读取表的数据、这都是存储引擎需要负责的操作,Mysql中提供了不同的存储引擎,不同的存储引擎存储的数据结构可能不相同,采用的算法也可能不同


事务的隔离性由锁来实现

当多个线程并发访问某个数据的时候,尤其是一些敏感的数据(比如订单、金额),我们就需要保证这个数据在任何时刻“最多只有一个线程”在访问,保证数据的完整性 和 一致性。

1、并发事务访问相同记录

1.读-读情况

读-读情况,即并发事务相继读取相同的记录,这种情况非常安全。。。不需要考虑锁

======================

2.写-写情况

写-写情况,即并发事务相继对相同的记录做出改动,可能会发生脏写问题。

任何一种隔离级别都不允许脏写问题的发生。所以在多个未提交事务相继对这条记录做改动时,需要让它们排队执行,这个排队的过程其实是通过来实现的。

这个所谓的其实是一个内存中的结构,在事务执行前本来是没有锁的,也就是说一开始是没有锁结构和记录进行关联的。
注意:有几个事务,就会有几个锁结构

当一个事务想对这条记录做改动时,首先会看看内存中有没有与这条记录关联的锁结构,当没有的时候就会在内存中生成一个锁结构与之关联。比如,事务T1要对这条记录做改动,就需要生成一个事务T1的锁结构与之关联:

锁结构的属性解释:
trx信息:代表这个锁结构是哪个事务生成的。
is-waiting:代表当前事务是否在等待。

在事务T1提交或者回滚之后,就会把该事务生成的锁结构释放掉,然后看看还有没有别的事务在等待获取锁, 发现了事务T2还在等待获取锁,所以把事务T2对应的锁结构的is-waiting属性设置为false,然后把该事务对应的线程唤醒,让它继续执行,此时事务T2就算获取到锁了。

===================

3.读-写情况 (重点)

读-写或写-读,即一个事务进行读取操作,另一个进行改动操作。这种情况下可能发生脏读、不可重复读、幻读的问题。

注意:MySQL在REPEATABLE READ隔离级别上就已经解决了幻读问题


2、并发问题的2种解决方案

脏写的问题,任何一种隔离级别都给解决掉了,这里的并发问题主要指脏读、不可重复读、幻读

方案一:读操作利用多版本并发控制(MVCC),写操作进行加锁。

方案二:读、写操作都采用加锁的方式。


3、俩种方案对比

  • 采用MVCC方式的话,读-写操作彼此并不冲突,性能更高。
  • 采用加锁方式的话,读-写操作彼此需要排队执行,影响性能。

一般情况下我们当然愿意采用MVCC来解决读-写操作并发执行的问题,但是业务在某些特殊情况下,要求必须采用加锁的方式执行。


4、共享锁和排它锁

共享锁(Shared Lock,S Lock)和 排他锁(Exclusive Lock,X Lock),也叫读锁(readlock)和写锁(write lock)。

需要注意的是对于InnoDB引擎来说,读锁和写锁可以加在表上,也可以加在行上。

对读取的记录加 S锁:
 

SELECT ... LOCK IN SHARE MODE;
#或
SELECT ... FOR SHARE;(8.0新增语法)

对读取的记录加 X锁:

SELECT ... FOR UPDATE;

悲观锁的核心思想是在操作期间持有锁来保护数据的一致性,但也会降低并发性能。因此,在使用悲观锁时需要注意锁的粒度和持有时间,避免过度锁定导致性能问题。

读操作是可以加S锁或X锁的,演示思路如下:

1.开启事务1,加s锁,开启事务2,加s锁(成功),s锁之间是共享的

2.在1的基础上再开启事务3,加x锁(阻塞)提交事务1,事务3仍然阻塞,继续提交事务2,事务3结束阻塞

3.开启事务1,加X锁,开启事务2,加s锁/x锁(都会阻塞),因为X锁是排它的

8.0中的新特性

能查就查,查不了也不会去阻塞,会执行相应的行为

在8.O版本中,SELECT...FOR UPDATE,SELECT...FOR SHARE添加NOWAIT、SKIP LOCKED语法,
跳过锁等待,或者跳过锁定。

通过添加NOWAIT、SKIP LOCKED语法,能够立即返回。如果查询的行已经加锁:
1.那么NOWAIT会立即报错返回。
2.而SKIP LOCKED也会立即返回,只是返回的结果中不包含被锁定的行。

写操作指增删改,是一定加要X锁(排它锁)的


MVCC 多版本并发控制

MVCC更好的去处理 读写冲突,提高数据库的并发性能。MVCC的实现依赖于:隐藏字段、Undo Log、Read View。

1、快照读和当前读

快照读

快照读又叫一致性读,读取的是快照数据。不加锁的简单的SELECT都属于快照读,如下

SELECT * FROM player WHERE ...

快照读:读取到的并不一定是数据的最新版本,可能是之前的历史版本

当前读

当前读:读取的是记录的最新版本,最新数据。读取时会对读取的记录加锁,加锁的SELECT或者对数据进行增删改都会进行当前读,如:


2、行格式中的隐藏字段


3、ReadView 

ReadView和事务是一对一的。

ReadView就是一个事务在使用MVCC机制进行快照读操作时产生的读视图。ReadView要解决的核心问题是:判断版本链中的哪个版本是当前事务可见的

ReadView中4个重要的内容如下

 活跃指,已经启动但是没提交的事务,提交ReadView访问规则了的事务不在ids里边

ReadView访问规则

在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见,某个版本也就是下文的被访问的版本。

在隔离级别为读已提交(Read Committed)时,一个事务中的每一次SELECT查询都会重新获取一次Read View。

当隔离级别为可重复读的时候,一个事务只在第一次SELECT的时候会获取一次Read View,
而后面所有的SELECT都会复用这个Read View,如下表所示:


Innodb的行锁到底锁了什么

1、mysql锁级别

表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。

==========

行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高

页级锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般

从操作粒度来说:表级锁>页级锁>行级锁

为了尽可能的提高并发度,每次锁定的数据范围越小越好


2、索引命中与没命中

索引没命中,行锁变表锁

案例一 

1.在session一会话窗口,

BEGIN;
UPDATE t_customer SET age=55 WHERE phone='13811112222'

2.在session2会话窗口,操作另外俩条记录

UPDATE t_customer SET age=55 WHERE id=5; #转圈等锁。
或者
UPDATE t_customer SET age=44 WHERE id=6; #转圈等锁。

 会发现转圈现象,有了表锁???

3.对session1中的事务 commit/rollback;接着session就好使了

原因:在session1中操作数据时,phone字段上面我们没有建索引,不会命中索引,使得行锁变表锁


3、行锁通过锁住索引实现

InnoDB的行锁,是通过锁住索引来实现的,如果加锁查询的时候没有使用到索引,会将整个聚簇索引都锁住,相当于锁表了。

按照主键索引 id

id主键索引+聚簇索引+一级索引  都是一个意思

操作前

1.session1中,注意此时我们使用到了主键索引id,则会是行锁

BEGIN;
UPDATE t_customer SET age=55 WHERE id=4

2.在session2中,只要你不跟人家抢那一行,都是OK的

UPDATE t_customer SET age=55 WHERE id=5;  # OK
或者
UPDATE t_customer SET age=33 WHERE id=6; # OK
或者
UPDATE t_customer SET age=11 WHERE id=4; # 转圈圈

按照二级索引cname

辅助索引+非聚簇索引+二级索引 都是一个意思

1.在cname字段自建一个索引

CREATE INDEX idx_cname ON t_customer(cname);

此时t_customer表中数据如下:

2.按照我们自建的索引去命中

在session1中,使用到了我们自建的索引。所以会是行锁,只会把这一条记录锁住

BEGIN;
UPDATE t_customer SET age=1 WHERE cname='z3'

===============

在session2中,这俩个SQL操作的是另外两条记录,所以可以。

UPDATE t_customer SET age=44 WHERE cNAME='z4'; #ok
 
UPDATE t_customer SET age=55 WHERE cNAME='z5'; #ok

在session2中,这俩个操作都是不行的,因为被session1行锁了。

UPDATE t_customer SET age=11 WHERE cNAME='z3' # 转圈圈
UPDATE t_customer SET age=11 WHERE id=4 # 转圈圈

在session1中,使用率commit/rollback,一切都回归正常

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值