【面试】网易游戏面试题目整理及答案(2)

网易游戏面试题目整理及题目(2)

  • Java部分
  • 数据库部分
  • 参考资料

Java部分

  1. 多线程安全问题
    答:首先,了解什么情况下会产生线程安全问题?同时满足以下两个条件时:
    ①多个线程在操作共享的数据
    ②操作共享数据的线程代码有多条
    当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生。
    解决思路:将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程不可以参与运算。当前线程把这些代码都执行完毕后,其他线程才可以参与运算。在java中,用同步代码块就可以解决这个问题
    同步代码块的格式:
synchronized(对象)
{
	需要被同步的代码 ;
}

这个对象一般称为同步锁。同步的前提:同步中必须有多个线程并使用同一个锁同步的好处:解决了线程的安全问题
同步的弊端:相对降低了效率,因为同步外的线程的都会判断同步锁
然后,需要知道同步锁是什么?同步函数使用的锁是 this静态的同步函数使用的锁是该函数所属字节码文件对象,可以用 getClass()方法获取,也可以用当前类名.class表示。
同步函数和同步代码块的区别:
①同步函数的锁是固定的this。
②同步代码块的锁是任意的对象。
③建议使用同步代码块。
同步嵌套时,两个线程你拿了我的锁,我拿了你的锁,都不释放,造成死锁。示例代码如下:

class MyLock {
    public static final Object locka = new Object();
    public static final Object lockb = new Object();
}

public class Testa implements Runnable {
    private boolean flag;

    Testa(boolean flag) {
        this.flag = flag;
    }

    public void run() {
        if (flag) {
            while (true) {
                synchronized (MyLock.locka) {
                    System.out.println(Thread.currentThread().getName() + "..if locka...");
                    synchronized (MyLock.lockb) {
                        System.out.println(Thread.currentThread().getName() + "...if lockb...");
                    }
                }
            }
        } else {
            while (true) {
                synchronized (MyLock.lockb) {
                    System.out.println(Thread.currentThread().getName() + "...if lockb...");
                    synchronized (MyLock.locka) {
                        System.out.println(Thread.currentThread().getName() + "...if locka...");
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        Testa a = new Testa(true);
        Testa b = new Testa(false);

        Thread t1 = new Thread(a);
        Thread t2 = new Thread(b);
        t1.start();
        t2.start();
    }
}

单例模式中的线程安全问题

/**
 * 饿汉式 单例模式
 */
public class ESingle {
    private static final ESingle s = new ESingle();
    private ESingle(){}
    public static ESingle getInstance(){
        return s;
    }
}
/**
 * 懒汉式 单例模式
 */
public class LSingle {
    private static LSingle s = null;

    private LSingle() {
    }

    public static LSingle getInstance() {
        if (s == null) {
            synchronized (LSingle.class) {
                if (s == null)
                    s = new LSingle();
            }
        }
        return s;
    }
}

开发用饿汉式,没有线程安全问题。——饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的
懒汉式的特点:延迟加载,不足:当多线程访问时会出现安全问题。可以采用加同步的方式解决。

  1. 并发和并行的区别
    答:并发和并行从宏观上来讲都是同时处理多路请求的概念。但并发和并行又有区别,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。在操作系统中,并发是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。①程序与计算不再一一对应,一个程序副本可以有多个计算。②并发程序之间有相互制约关系,直接制约体现为一个程序需要另一个程序的计算结果,间接制约体现为多个程序竞争某一资源,如处理机、缓冲区等。③并发程序在执行中是走走停停,断续推进的。

  2. Java的并发安全机制(答了synchronized、ReentrantLock、CAS)
    答:首先弄清楚什么是进程?什么是线程?进程是正在运行的一个程序。线程在进程里边,也可以说是程序内部的一条执行路径。若同一个进程同一时间并执行多个线程,就是支持多线程的。线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器。单核CPU:其实是一种假的多线程,因为在一个时间单位内,也只能执行一个线程的任务。多核CPU:能够更好的发挥多线程效率。一个Java应用程序java.exe其实至少有三个线程:main()主线程,gc()垃圾回收机制,异常处理线程
    并行:多个CPU同时执行多个任务。
    并发:一个CPU同时执行多个任务。
    何时需要多线程?①当一个程序需要同时执行两个或多个任务;②程序需要实现一些需要等待的任务时,如用户输入、文件读写等;③需要一些后台运行的程序时。
    创建多线程的方式一:继承Thread类,步骤如下:①创建一个继承于Thread类的子类;②重写Thread类的run()方法;③创建该子类的对象;④通过此对象调用start()方法。注意:如果线程对象直接调用run(),即m.run(),此时只是普通的对象调用方法,并未启动线程。
    Thread类的方法:
    1.start():启动当前线程,调用当前线程的run();
    2.run():通常需要重写Thread类中的此方法,将创建线程要执行的操作声明在此方法中;
    3.currentThread():静态方法,返回执行当前代码的线程;
    4.getName():获取当前线程名字;
    5.setName():设置当前线程名;
    6.yield():释放当前CPU的执行权;
    7.join():在线程A中调用线程B的join(),此时线程A就进入阻塞状态,直到线程B完全执行完以后,线程A才结束阻塞状态;
    8.stop():已过时。强制结束当前线程;
    9.sleep(long millitime):单位为毫秒,让当前线程“睡眠”指定的millitime毫秒。在此时间之内,当前线程是阻塞状态;
    10.isAlive():判断当前线程是否存活,返回ture或false
    线程的调度:①同优先级线程组成先进先出队列;②对高优先级,会抢占CPU的执行权;
    线程的优先级: ①线程的优先等级分为3个:MAX_PRIORITY:10 ,MIN_PRIORITY:1,NORM_PRIORITY:5;
    如何获取和设置当前线程的优先级?getPriority():获取线程的优先级;setPriority():设置线程的优先级
    注意:高优先级的线程会抢占CPU的执行权,但这只是从概率上讲,并不意味着只有高优先级的线程执行完后才执行低优先级的线程
    创建多线程的方式二:实现Runnable接口,步骤如下:①定义类实现Runnable接口;②覆盖Runnable接口中的run方法,将线程要运行的代码存放在该run方法中;③通过Thread类建立线程对象;④将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数;⑤调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。
    这里有两个问题:1. 为什么要将Runnable接口的子类对象传递给Thread的构造函数?因为,自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程去指定对象的run方法,就必须明确该run方法所属对象。2.实现方式和继承方式创建线程有什么区别?实现方式的好处是避免了单继承的局限性。在定义线程时,建议使用实现方式。两种方式的区别:继承Thread:线程代码存放Thread子类run方法中;实现Runnable:线程代码存在接口的子类的run方法。
    线程的生命周期:
    【面试】网易游戏面试题目整理及答案(2)_第1张图片
    新建:使用new关键字创建Thread类或及其子类对象后,该线程就处于新建状态。此时,通过对象调用start()方法后,线程进入就绪状态;
    就绪:此时线程已经具备了运行条件,但是还没有分配到CPU的执行权,处于线程就绪队列,等待系统为其分配CPU。一旦获得了CPU的执行权,那么线程就进入运行状态,并自动调用自己的run()方法。
    运行:此时线程执行自己的run()方法,直到调用其他方法而终止,或等待某资源而阻塞或完成任务而死亡。
    阻塞:处于运行状态的线程在某种情况下,如执行了sleep()方法后,此时将让出CPU的执行权并停止自己的运行。只有当引起阻塞的原因消除时,如睡眠时间已到,此时线程便转入就绪状态,再次等待CPU的执行权。其实阻塞状态时线程具备运行资格,但没有CPU执行权。
    死亡:死亡状态是线程生命周期的最后一个阶段。线程死亡有两个原因:一是正常运行的线程完成了自己的工作,而是一个线程被强制终止,如通过stop()。
    然后我们要了解一下
    内存模型的相关内容
    java的内存模型中有主内存和线程的工作内存之分主内存上存放的是线程共享的变量(实例字段,静态字段和构成数组的元素),线程的工作内存是线程私有的空间,存放的是线程私有的变量(方法参数与局部变量)线程在工作的时候如果要操作主内存上的共享变量,为了获得更好的执行性能并不是直接去修改主内存而是会在线程私有的工作内存中创建一份变量的拷贝(缓存),在工作内存上对变量的拷贝修改之后再把修改的值刷回到主内存的变量中去,JVM提供了8中原子操作来完成这一过程:lock, unlock, read, load, use, assign, store, write。深入理解java虚拟机-jvm最高特性与实践这本书中有一个图很好的表示了线程,主内存和工作内存之间的关系:
    【面试】网易游戏面试题目整理及答案(2)_第2张图片
    如果只有一个线程当然不会有什么问题,但是如果有多个线程同时在操作主内存中的变量,因为
    8种操作的非连续性和线程抢占cpu执行的机制就会带来冲突的问题,也就是多线程的安全问题
    线程安全的定义就是:如果线程执行过程中不会产生共享资源的冲突就是线程安全的
    Java一般用一下几种机制保证线程安全
    1)互斥同步锁(悲观锁)
    ①Synchronized
    ②ReentrantLock
    互斥同步锁也叫做阻塞同步锁,特征是会对没有获取锁的线程进行阻塞。要理解互斥同步锁,首选要明白什么是互斥什么是同步。简单的说互斥就是非你即我,同步就是顺序访问。互斥同步锁就是以互斥的手段达到顺序访问的目的。操作系统提供了很多互斥机制比如信号量,互斥量,临界区资源等来控制在某一个时刻只能有一个或者一组线程访问同一个资源
    Java里面的互斥同步锁就是Synchronized和ReentrantLock前者是由语言级别实现的互斥同步锁,理解和写法简单但是机制笨拙,在JDK6之后性能优化大幅提升,即使在竞争激烈的情况下也能保持一个和ReentrantLock相差不多的性能,所以JDK6之后的程序选择不应该再因为性能问题而放弃synchorized。ReentrantLock是API层面的互斥同步锁,需要程序自己打开并在finally中关闭锁,和synchorized相比更加的灵活,体现在三个方面**:等待可中断,公平锁以及绑定多个条件**。但是如果开发人员对ReentrantLock理解不够深刻,或者忘记释放lock,那么不仅不会提升性能反而会带来额外的问题。另外Synchronized是JVM实现的,可以通过监控工具来监控锁的状态,遇到异常JVM会自动释放掉锁。而ReentrantLock必须由程序主动的释放锁
    互斥同步锁都是可重入锁,好处是可以保证不会死锁。但是因为涉及到核心态和用户态的切换,因此比较消耗性能。JVM开发团队在JDK5-JDK6升级过程中采用了很多锁优化机制来优化同步无竞争情况下锁的性能。比如:自旋锁和适应性自旋锁,轻量级锁,偏向锁,锁粗化和锁消除。
    2)非阻塞同步锁
    1.原子类(CAS)
    非阻塞同步锁也叫乐观锁
    ,相比悲观锁来说,它会先进行资源在工作内存中的更新,然后根据与主存中旧值的对比来确定在此期间是否有其他线程对共享资源进行了更新,如果旧值与期望值相同,就认为没有更新,可以把新值写回内存,否则就一直重试直到成功。它的实现方式依赖于处理器的机器指令:CAS(Compare And Swap)
    JUC中提供了几个Automic类以及每个类上的原子操作就是乐观锁机制。不激烈情况下,性能比synchronized略逊,而激烈的时候,也能维持常态。激烈的时候,Atomic的性能会优于ReentrantLock一倍左右。但是其有一个缺点,就是只能同步一个值,一段代码中只能出现一个Atomic的变量,多于一个同步无效。因为它不能在多个Atomic之间同步。 非阻塞锁是不可重入的,否则会造成死锁
    3)无同步方案
    1.可重入代码:在执行的任何时刻都可以中断-重入执行而不会产生冲突。特点就是不会依赖堆上的共享资源
    2.ThreadLocal/Volaitile:线程本地的变量,每个线程获取一份共享变量的拷贝,单独进行处理。
    3.线程本地存储:如果一个共享资源一定要被多线程共享,可以尽量让一个线程完成所有的处理操作,比如生产者消费者模式中,一般会让一个消费者完成对队列上资源的消费。典型的应用是基于请求-应答模式的web服务器的设计

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
补充知识:
4. 乐观锁/悲观锁
1)乐观锁:乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
2)悲观锁:悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试CAS乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。
5. 公平锁/非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
6. 独享锁/共享锁
独享锁是指该锁一次只能被一个线程所持有;共享锁是指该锁可被多个线程所持有。
对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写、写读 、写写的过程是互斥的。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。对于synchronized而言,当然是独享锁
7. 互斥锁/读写锁
上面说到的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。互斥锁在Java中的具体实现就是ReentrantLock读写锁在Java中的具体实现就是ReadWriteLock
8. 可重入锁
可重入锁又名递归锁,是指同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。对于Java ReentrantLock而言, 其名字是Reentrant Lock即是重新进入锁。对于synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁
9. 分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。当需要put元素的时候,并不是对整个HashMap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计size的时候,可就是获取HashMap全局信息的时候,就需要获取所有的分段锁才能统计
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
10. 自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
11. 偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对synchronized。在Java 5通过引入锁升级的机制来实现高效synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低
Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。重量级锁是悲观锁的一种,自旋锁、轻量级锁与偏向锁属于乐观锁。后面将会对这四种状态进行详细说明。

补充知识:markword/AQS/CAS

  1. markword
    markword是java对象数据结构中的一部分,这里只做markword的详细介绍,因为对象的markword和java各种类型的锁密切相关;
    markword数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,它的最后2bit是锁状态标志位,用来标记当前对象的状态,对象的所处的状态,决定了markword存储的内容,如下表所示:
    【面试】网易游戏面试题目整理及答案(2)_第3张图片
    32位虚拟机在不同状态下markword结构如下图所示:
    【面试】网易游戏面试题目整理及答案(2)_第4张图片
  2. AQS
    AbstractQueuedSynchronized 抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch…
    【面试】网易游戏面试题目整理及答案(2)_第5张图片
    AQS维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。state的访问方式有三种:getState()、setState()、compareAndSetState()
    AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。
  3. CAS
    CAS(Compare and Swap 比较并交换)是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。   
    CAS操作中包含三个操作数——需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,否则处理器不做任何操作。无论哪种情况,它都会在CAS 指令之前返回该位置的值(在CAS的一些特殊情况下将仅返回CAS是否成功,而不提取当前值)。CAS有效地说明了“ 我认为位置V应该包含值A;如果包含该值,则将 B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可”。这其实和乐观锁的冲突检查 + 数据更新的原理是一样的。
    补充知识:Synchronized的执行过程
    1.检测MarkWord里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁
    2.如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1
    3.如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
    4.当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁
    5.如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
    6.如果自旋成功则依然处于轻量级状态。
    7.如果自旋失败,则升级为重量级锁。
    锁的优缺点:
    【面试】网易游戏面试题目整理及答案(2)_第6张图片
    补充知识:Synchronized与Lock的区别
    1.synchronized是关键字,是JVM层面的底层啥都帮我们做了,而Lock是一个接口,是JDK层面的有丰富的API。
    2.synchronized会自动释放锁,而Lock必须手动释放锁。
    3.synchronized是不可中断的,Lock可以中断也可以不中断。
    4.通过Lock可以知道线程有没有拿到锁,而synchronized不能。
    5.synchronized能锁住方法和代码块,而Lock只能锁住代码块。
    6.Lock可以使用读锁提高多线程读效率。
    7.synchronized是非公平锁,ReentrantLock可以控制是否是公平锁。
    两者一个是JDK层面的一个是JVM层面的,最大的区别其实在,是否需要丰富的api,还有一个是我们的场景。

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

  1. String、StringBuilder、StringBuffer的区别和应用场景
    答:String、StringBuffer与StringBuilder的区别是:①String内容不可变,是值不可变的变量,且使用了final修饰符,是不可继承的,StringBuffer和StringBuilder内容可变;②StringBuilder非线程安全(单线程使用),String与StringBuffer线程安全(多线程使用);③如果程序不是多线程的,那么使用StringBuilder效率高于StringBuffer。
    String与StringBuffer区别:String在修改时不会改变对象自身,在每次对 String 类型进行改变的时候其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象,所以经常改变内容的字符串最好不要用 String 。StringBuffer在修改时会改变对象自身,每次结果都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,再改变对象引用。所以在一般情况下我们推荐使用 StringBuffer ,特别是字符串对象经常改变的情况下。StringBuffer 上的主要操作是 append 和 insert 方法。StringBuffer类中的方法主要偏重于对于字符串的变化,例如追加、插入和删除等,常用方法有:append方法、insert方法、deleteCharAt方法、reverse方法等。
    ①如果要操作少量的数据用 String;②(多线程下)经常需要对一个字符串进行修改,例如追加、插入和删除等操作,使用StringBuffer要更加适合一些。
    StringBuffer与StringBuilder区别:StringBuilder是可变的对象,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。StringBuffer:线程安全的; StringBuilder:线程非安全的。
    String,StringBuffer与StringBuilder速度区别:在大部分情况下,StringBuilder > StringBuffer > String;特殊情况, String > StringBuffer
    (1)如果要操作少量的数据用 String;
    (2)多线程操作字符串缓冲区下操作大量数据 StringBuffer;
    (3)单线程操作字符串缓冲区下操作大量数据 StringBuilder。

  2. String a=“abc” String b=“abc” ,a等于b吗?常量池位于JVM的哪里?String提供了什么方法使用常量池?(intern)
    答:首先注意和equals()的区别:对于对象而言,==是判断两个对象是否指向同一地址,而equals()才是判断两个对象的内容是否相同(所以说自定义对象需要重写hashcode()以及equals()方法)。String类型属于不可变(immutable)对象,也就是说String变量的内容一旦确定就不能修改。当使用String a = “abc”;语句时实际上是在字符串常量池中建立了一个"abc"字符串,String a = "bcd"同理,所以两个语句只是改变了a变量指向内存池中的位置,其内容并没有改变。当你使用String a = “abc”; String b = “abc”;时,实际上是在Java常量池中新建了一个"abc"字符串,而a和b都是指向内存池中的这个"abc"的,两者的地址也是相同的,所以ab返回true。
    JVM的结构图如下:
    【面试】网易游戏面试题目整理及答案(2)_第7张图片
    Java6和6之前,常量池是存放在方法区(永久代)中的。Java7,将常量池是存放到了堆中。Java8之后,取消了整个永久代区域,取而代之的是元空间。运行时常量池和静态常量池存放在元空间中,而字符串常量池依然存放在堆中
    jdk1.6下字符串常量池是在永久区中,是与堆完全独立的两个空间。intern()方法能使一个位于堆中的字符串在运行期间动态地加入到字符串常量池中(字符串常量池的内容是程序启动的时候就已经加载好了),如果字符串常量池中有该对象对应的字面量,则返回该字面量在字符串常量池中的引用,否则,创建复制一份该字面量到字符串常量池并返回它的引用
    jdk1.7,1.8下字符串常量池已经转移到堆中了,是堆中的一部分内容,jvm设计人员对intern()进行了一些修改,当执行intern()时,jvm不再把字符串对应的字面量复制一份到字符串常量池中,而是在字符串常量池中存储一份字符串的引用,这个引用指向堆中的字面量,当运行到String s = "hellohello"时,发现字符串常量池已经存在一个指向堆中该字面量的引用,则返回这个引用。

  3. String为什么不会变?
    答:String类是不可变类,一个String对象被创建以后,包含这个对象中的字符串序列是不可改变的。与其问String为什么是不可变的,还不如问String类是如何实现其对象不可变的。
    当使用final修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。但对于引用类型变量而言,它保存的仅仅是一个引用,final只保证这个引用变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变
    String对象真的不可变吗?虽然value是final修饰的,只是说明value不能再重新指向其他的引用。但是value指向的数组可以改变,一般情况下我们是没有办法访问到这个value指向的数组的元素。但是,反射可以。可以反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构

public static void main(String[] args) throws Exception {
        String str = "Hello World";
        System.out.println("修改前的str:" + str);
        System.out.println("修改前的str的内存地址" + System.identityHashCode(str));
        // 获取String类中的value字段
        Field valueField = String.class.getDeclaredField("value");
        // 改变value属性的访问权限
        valueField.setAccessible(true);
        // 获取str对象上value属性的值
        char[] value = (char[]) valueField.get(str);
        // 改变value所引用的数组中的字符
        value[3] = '?';
        System.out.println("修改后的str:" + str);
        System.out.println("修改前的str的内存地址" + System.identityHashCode(str));
    }
  1. collection与collections区别
    答:java.util.Collection 是一个集合框架的父接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。
    java.util.Collections 是一个包装类。它包含有各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,服务于Java的Collection框架。它提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。排序(Sort)、混排(Shuffling)、反转(Reverse)、替换所以的元素(Fill)、拷贝(Copy)、返回Collections中最小元素(min)、返回Collections中最小元素(max)、lastIndexOfSubList、IndexOfSubList、Rotate等

数据库部分

  1. MySQL架构
    答:和其它数据库相比,MySQL有点与众不同,它的架构可以在多种不同场景中应用并发挥良好作用。主要体现在存储引擎的架构上,插件式的存储引擎架构将查询处理和其它的系统任务以及数据的存储提取相分离。这种架构可以根据业务的需求和实际需要选择合适的存储引擎。
    【面试】网易游戏面试题目整理及答案(2)_第8张图片
    1.**连接层:最上层是一些客户端和连接服务。**主要完成一些类似于连接处理、授权认证、及相关的安全方案。在该层上引入了线程池的概念,为通过认证安全接入的客户端提供线程。同样在该层上可以实现基于SSL的安全链接。服务器也会为安全接入的每个客户端验证它所具有的操作权限。
    2.服务层:第二层服务层,主要完成大部分的核心服务功能, 包括查询解析、分析、优化、缓存、以及所有的内置函数,所有跨存储引擎的功能也都在这一层实现,包括触发器、存储过程、视图等
    3.引擎层:第三层存储引擎层,存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过API与存储引擎进行通信。不同的存储引擎具有的功能不同,这样我们可以根据自己的实际需要进行选取
    4.存储层:第四层为数据存储层,主要是将数据存储在运行于该设备的文件系统之上,并完成与存储引擎的交互

  2. 一条SQL语句在MySQL中如何执行的?
    答:客户端请求 —> 连接器(验证用户身份,给予权限) —> 查询缓存(存在缓存则直接返回,不存在则执行后续操作) —> 分析器(对SQL进行词法分析和语法分析操作) —> 优化器(主要对执行的sql优化选择最优的执行方案方法) —> 执行器(执行时会先看用户是否有执行权限,有才去使用这个引擎提供的接口) —> 去引擎层获取数据返回(如果开启查询缓存则会缓存查询结果)
    【面试】网易游戏面试题目整理及答案(2)_第9张图片

  3. MySQL有哪些存储引擎?都有哪些区别?
    答:存储引擎是MySQL的组件,用于处理不同表类型的SQL操作。不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能,使用不同的存储引擎,还可以获得特定的功能。
    使用哪一种引擎可以灵活选择,一个数据库中多个表可以使用不同引擎以满足各种性能和实际需求,使用合适的存储引擎,将会提高整个数据库的性能 。
    MySQL服务器使用可插拔的存储引擎体系结构,可以从运行中的 MySQL 服务器加载或卸载存储引擎
    查看存储引擎:

SHOW ENGINES; #查看支持的存储引擎
SHOW VARIABLES LIKE 'storage_engine'#查看默认存储引擎
SHOW table status like 'tablename'
SHOW table status from database where name="tablename" # 准确查看某个数据库的某一个表所使用的存储引擎

设置存储引擎:

# 建表时指定存储引擎
CREATE TBALE t1 (i INT) ENGINE = INNODB;
CREATE TABLE t2 (i INT) ENGINE = MEMORY; 
# 修改存储引擎
ALTER TABLE t_ ENGINE = InnoDB# 修改默认存储引擎
SET default_storage_engine = NDBCLUSTER;

存储引擎对比:
常见的存储引擎就 InnoDB、MyISAM、Memory、NDB
InnoDB 现在是 MySQL 默认的存储引擎,支持事务行级锁定外键
文件存储结构对比:
MySQL中建立任何一张数据表,在其数据目录对应的数据库目录下都有对应表的 .frm 文件.frm 文件是用来保存每个数据表的元数据(meta)信息,包括表结构的定义等,与数据库存储引擎无关,也就是任何存储引擎的数据表都必须有.frm文件,命名方式为 数据表名.frm,如user.frm。查看MySQL 数据保存在哪里:

show variables like 'data%';

1)MyISAM 物理文件结构为:

  • .frm文件:与表相关的元数据信息都存放在frm文件,包括表结构的定义信息等
  • .MYD (MYData) 文件:MyISAM 存储引擎专用,用于存储MyISAM 表的数据
  • .MYI (MYIndex)文件:MyISAM 存储引擎专用,用于存储MyISAM 表的索引相关信息
    2)InnoDB 物理文件结构为:
  • .frm 文件:与表相关的元数据信息都存放在frm文件,包括表结构的定义信息等
  • .ibd 文件或 .ibdata 文件:这两种文件都是存放 InnoDB 数据的文件,之所以有两种文件形式存放 InnoDB 的数据,是因为 InnoDB 的数据存储方式能够通过配置来决定是使用共享表空间存放存储数据,还是用独享表空间存放存储数据独享表空间存储方式使用.ibd文件,并且每个表一个.ibd文件;共享表空间存储方式使用.ibdata文件,所有表共同使用一个.ibdata文件(或多个,可自己配置)
    MyISAM与InnoDB的对比:
  1. InnoDB 支持事务,MyISAM 不支持事务。这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;
  2. InnoDB 支持外键,而 MyISAM 不支持。对一个包含外键的 InnoDB 表转为 MYISAM 会失败;
  3. InnoDB 是聚簇索引,MyISAM 是非聚簇索引聚簇索引的文件存放在主键索引的叶子节点上,因此 InnoDB 必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而 MyISAM 是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的
  4. InnoDB 不保存表的具体行数,执行select count(*) from table 时需要全表扫描。而 MyISAM 用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;
  5. InnoDB 最小的锁粒度是行锁MyISAM 最小的锁粒度是表锁。一个更新语句会锁住整张表,导致其他查询和更新都会被阻塞,因此并发访问受限。这也是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;

补充问题:
问题1:一张表,里面有ID自增主键,当insert了17条记录之后,删除了第15,16,17条记录,再把Mysql重启,再insert一条记录,这条记录的ID是18还是15 ?
答:如果表的类型是MyISAM,那么是18。因为MyISAM表会把自增主键的最大ID 记录到数据文件中,重启MySQL自增主键的最大ID也不会丢失;如果表的类型是InnoDB,那么是15。因为InnoDB 表只是把自增主键的最大ID记录到内存中,所以重启数据库或对表进行OPTION操作,都会导致最大ID丢失。
问题2:哪个存储引擎执行 select count(*) 更快,为什么?
答:MyISAM更快,因为MyISAM内部维护了一个计数器,可以直接调取。在 MyISAM 存储引擎中,把表的总行数存储在磁盘上,当执行 select count(*) from t 时,直接返回总数据。
在 InnoDB 存储引擎中,跟 MyISAM 不一样,没有将总行数存储在磁盘上,当执行 select count(*) from t 时,会先把数据读出来,一行一行的累加,最后返回总数量。InnoDB 中 count(*) 语句是在执行的时候,全表扫描统计总数量,所以当数据越来越大时,语句就越来越耗时了,为什么 InnoDB 引擎不像 MyISAM 引擎一样,将总行数存储到磁盘上?这跟 InnoDB 的事务特性有关,由于多版本并发控制(MVCC)的原因,InnoDB 表“应该返回多少行”也是不确定的。

  1. MySQL中的数据类型
    答:主要包括以下五大类:
  • 整数类型:BIT、BOOL、TINY INT、SMALL INT、MEDIUM INT、 INT、 BIG INT
  • 浮点数类型:FLOAT、DOUBLE、DECIMAL
  • 字符串类型:CHAR、VARCHAR、TINY TEXT、TEXT、MEDIUM TEXT、LONGTEXT、TINY BLOB、BLOB、MEDIUM BLOB、LONG BLOB
  • 日期类型:Date、DateTime、TimeStamp、Time、Year
  • 其他数据类型:BINARY、VARBINARY、ENUM、SET、Geometry、Point、MultiPoint、LineString、MultiLineString、Polygon、GeometryCollection等
    补充问题:
    问题1:CHAR 和 VARCHAR 的区别?
    答:char是固定长度,varchar长度可变:char(n) 和 varchar(n) 中括号中 n 代表字符的个数,并不代表字节个数,比如 CHAR(30) 就可以存储 30 个字符。存储时,前者不管实际存储数据的长度,直接按 char 规定的长度分配存储空间;而后者会根据实际存储的数据分配最终的存储空间
    相同点:
  • char(n),varchar(n)中的n都代表字符的个数
  • 超过char,varchar最大长度n的限制后,字符串会被截断。
    不同点:
  • char不论实际存储的字符数都会占用n个字符的空间,而varchar只会占用实际字符应该占用的字节空间加1(实际长度length,0<=length<255)或加2(length>255)。因为varchar保存数据时除了要保存字符串之外还会加一个字节来记录长度(如果列声明长度大于255则使用两个字节来保存长度)。
  • 能存储的最大空间限制不一样:char的存储上限为255字节
  • char在存储时会截断尾部的空格,而varchar不会。
    总之,char是适合存储很短的、一般固定长度的字符串。例如,char非常适合存储密码的MD5值,因为这是一个定长的值。对于非常短的列,char比varchar在存储空间上也更有效率。
    问题2:列的字符串类型可以是什么?
    答:字符串类型是:SET、BLOB、ENUM、CHAR、CHAR、TEXT、VARCHAR
    问题3:BLOB和TEXT有什么区别?
    答:BLOB是一个二进制对象,可以容纳可变数量的数据。有四种类型的BLOB:TINYBLOB、BLOB、MEDIUMBLO和 LONGBLOB;TEXT是一个不区分大小写的BLOB。四种TEXT类型:TINYTEXT、TEXT、MEDIUMTEXT 和 LONGTEXT。BLOB 保存二进制数据,TEXT 保存字符数据
  1. 对MySQL索引的理解
    答:MYSQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构,所以说索引的本质是:数据结构
    ①索引的目的在于提高查询效率,可以类比字典、 火车站的车次表、图书的目录等。
    除数据本身之外,数据库还维护者一个满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引
    ③索引本身也很大,不可能全部存储在内存中,一般以索引文件的形式存储在磁盘上
    ④平常说的索引,没有特别指明的话,就是B+树(多路搜索树,不一定是二叉树)结构组织的索引。其中聚集索引,次要索引,覆盖索引,符合索引,前缀索引,唯一索引默认都是使用B+树索引,统称索引。此外还有哈希索引等。
    创建索引:CREATE [UNIQUE] INDEX indexName ON mytable(username(length));如果是CHAR,VARCHAR类型,length可以小于字段实际长度;如果是BLOB和TEXT类型,必须指定 length。
    修改表结构(添加索引):ALTER table tableName ADD [UNIQUE] INDEX indexName(columnName);
    删除索引:DROP INDEX [indexName] ON mytable;
    查看索引:SHOW INDEX FROM table_name\G; --可以通过添加 \G 来格式化输出信息。
    修改索引(使用ALTER命令):
ALTER TABLE tbl_name ADD PRIMARY KEY (column_list)#该语句添加一个主键,这意味着索引值必须是唯一的,且不能为NULL。
ALTER TABLE tbl_name ADD UNIQUE index_name (column_list); #这条语句创建索引的值必须是唯一的(除了NULL外,NULL可能会出现多次)。
ALTER TABLE tbl_name ADD INDEX index_name (column_list); #添加普通索引,索引值可出现多次。
ALTER TABLE tbl_name ADD FULLTEXT index_name (column_list); #该语句指定了索引为 FULLTEXT ,用于全文索引。

索引的优势:

  • 提高数据检索效率,降低数据库IO成本
  • 降低数据排序的成本,降低CPU的消耗
    索引的劣势:
  • 索引也是一张表,保存了主键和索引字段,并指向实体表的记录,所以也需要占用内存
  • 虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE。因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件每次更新添加了索引列的字段, 都会调整因为更新所带来的键值变化后的索引信息
  1. MySQL索引分类
    答:从三个角度对MySQL索引进行分类:
    1)从数据结构角度
  • B+树索引
  • Hash索引
  • Full-Text全文索引
  • R-Tree索引
    2)从物理存储角度
  • 聚集索引(clustered index)
  • 非聚集索引(non-clustered index),也叫辅助索引(secondary index)
  • 聚集索引和非聚集索引都是B+树结构
    3)从逻辑角度
  • 主键索引主键索引是一种特殊的唯一索引,不允许有空值
  • 普通索引或者单列索引:每个索引只包含单个列,一个表可以有多个单列索引
  • 多列索引(复合索引、联合索引)复合索引指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用复合索引时遵循最左前缀集合
  • 唯一索引或者非唯一索引
  • 空间索引:空间索引是对空间数据类型的字段建立的索引,MYSQL中的空间数据类型有4种,分别是GEOMETRY、POINT、LINESTRING、POLYGON。MYSQL使用SPATIAL关键字进行扩展,使得能够用于创建正规索引类型的语法创建空间索引。创建空间索引的列,必须将其声明为NOT NULL,空间索引只能在存储引擎为MYISAM的表中创建
  1. 为什么MySQL索引中用B+tree,不用B-tree 或者其他树,为什么不用 Hash 索引?聚簇索引/非聚簇索引,MySQL 索引底层实现,叶子结点存放的是数据还是指向数据的内存地址,使用索引需要注意的几个地方?使用索引查询一定能提高查询的性能吗?为什么?
    答:首先要明白索引(index)是在存储引擎(storage engine)层面实现的,而不是server层面。不是所有的存储引擎都支持所有的索引类型。即使多个存储引擎支持某一索引类型,它们的实现和行为也可能有所差别。
    MyISAM 和 InnoDB 存储引擎,都使用 B+Tree的数据结构,它相对与 B-Tree结构,所有的"数据都存放在叶子节点上",且把叶子节点通过指针连接到一起,形成了一条数据链表,以加快相邻数据的检索效率
    B-Tree 和 B+Tree 的区别:
    1. B-Tree是为磁盘等外存储设备设计的一种平衡查找树
    这里需要了解系统如何从磁盘中读取数据到内容中?系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么。InnoDB 存储引擎中有页(Page)的概念,页是其磁盘管理的最小单位InnoDB 存储引擎中默认每个页的大小为16KB,可通过参数 innodb_page_size 将页的大小设置为 4K、8K、16K,在 MySQL 中可通过如下命令查看页的大小:show variables like 'innodb_page_size';而系统一个磁盘块的存储空间往往没有这么大,因此InnoDB 每次申请磁盘空间时都会是若干地址连续磁盘块来达到页的大小 16KB。InnoDB 在把磁盘数据读入到磁盘时会以页为基本单位,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘I/O次数,提高查询效率。
    B-Tree 结构的数据可以让系统高效的找到数据所在的磁盘块。为了描述 B-Tree,首先定义一条记录为一个二元组[key, data] ,key为记录的键值,对应表中的主键值,data 为一行记录中除主键外的数据。对于不同的记录,key值互不相同。
    一棵m阶的B-Tree有如下特性:
    1 每个节点最多有m个孩子
    2 除了根节点和叶子节点外,其它每个节点至少有Ceil(m/2)个孩子。
    3 若根节点不是叶子节点,则至少有2个孩子
    4 所有叶子节点都在同一层,且不包含其它关键字信息
    5 每个非终端节点包含n个关键字信息(P0,P1,…Pn, k1,…kn)
    6 关键字的个数n满足:ceil(m/2)-1 <= n <= m-1
    7 ki(i=1,…n)为关键字,且关键字升序排序
    8 Pi(i=1,…n)为指向子树根节点的指针。P(i-1)指向的子树的所有节点关键字均小于ki,但都大于k(i-1)
    B-Tree 中的每个节点根据实际情况可以包含大量的关键字信息和分支,如下图所示为一个 3 阶的 B-Tree:
    【面试】网易游戏面试题目整理及答案(2)_第10张图片
    每个节点占用一个盘块的磁盘空间,一个节点上有两个升序排序的关键字和三个指向子树根节点的指针,指针存储的是子节点所在磁盘块的地址。两个关键词划分成的三个范围域对应三个指针指向的子树的数据的范围域。以根节点为例,关键字为17和35,P1指针指向的子树的数据范围为小于17,P2指针指向的子树的数据范围为17~35,P3指针指向的子树的数据范围为大于35。
    模拟查找关键字29的过程:
    1 根据根节点找到磁盘块1,读入内存。【磁盘I/O操作第1次】
    2 比较关键字29在区间(17,35),找到磁盘块1的指针P2。
    3 根据P2指针找到磁盘块3,读入内存。【磁盘I/O操作第2次】
    4 比较关键字29在区间(26,30),找到磁盘块3的指针P2。
    5 根据P2指针找到磁盘块8,读入内存。【磁盘I/O操作第3次】
    6 在磁盘块8中的关键字列表中找到关键字29。
    分析上面过程,发现需要3次磁盘I/O操作,和3次内存查找操作。由于内存中的关键字是一个有序表结构,可以利用二分法查找提高效率。而3次磁盘I/O操作是影响整个B-Tree查找效率的决定因素。B-Tree相对于AVLTree缩减了节点个数,使每次磁盘I/O取到内存的数据都发挥了作用,从而提高了查询效率。
    2. B+Tree 是在 B-Tree 基础上的一种优化,使其更适合实现外存储索引结构,InnoDB 存储引擎就是用 B+Tree 实现其索引结构。
    从上一节中的B-Tree结构图中可以看到每个节点中不仅包含数据的key值,还有data值。而每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度。
    B+Tree相对于B-Tree有几点不同:
    1 非叶子节点只存储键值信息;
    2 所有叶子节点之间都有一个链指针;
    3 数据记录都存放在叶子节点中
    将上一节中的B-Tree优化,由于B+Tree的非叶子节点只存储键值信息,假设每个磁盘块能存储4个键值及指针信息,则变成B+Tree后其结构如下图所示:
    【面试】网易游戏面试题目整理及答案(2)_第11张图片
    通常在B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。因此可以对B+Tree进行两种查找运算:一种是对于主键的范围查找和分页查找,另一种是从根节点开始,进行随机查找。
    可能上面例子中只有22条数据记录,看不出B+Tree的优点,下面做一个推算:
    InnoDB存储引擎中页的大小为16KB,一般表的主键类型为INT(占用4个字节)或BIGINT(占用8个字节),指针类型也一般为4或8个字节,也就是说一个页(B+Tree中的一个节点)中大概存储16KB/(8B+8B)=1K个键值(因为是估值,为方便计算,这里的K取值为10^3)。也就是说一个深度为3的B+Tree索引可以维护10^3 * 10^3 * 10^3 = 10亿 条记录。
    实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree的高度一般都在2-4层。MySQL的InnoDB存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只需要1~3次磁盘I/O操作
    B+Tree性质
    1)通过上面的分析,我们知道IO次数取决于b+数的高度h假设当前数据表的数据为N,每个磁盘块的数据项的数量是m,则有h=㏒(m+1)N,当数据量N一定的情况下,m越大,h越小;而m = 磁盘块的大小 / 数据项的大小,磁盘块的大小也就是一个数据页的大小,是固定的,如果数据项占的空间越小,数据项的数量越多,树的高度越低。这就是为什么每个数据项,即索引字段要尽量的小,比如int占4字节,要比bigint8字节少一半。这也是为什么b+树要求把真实的数据放到叶子节点而不是内层节点,一旦放到内层节点,磁盘块的数据项会大幅度下降,导致树增高。当数据项等于1时将会退化成线性表。
    2)当b+树的数据项是复合的数据结构,比如(name,age,sex)的时候,b+树是按照从左到右的顺序来建立搜索树的,比如当(张三,20,F)这样的数据来检索的时候,b+树会优先比较name来确定下一步的所搜方向,如果name相同再依次比较age和sex,最后得到检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道下一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据了, 这个是非常重要的性质,即索引的最左匹配特性
    MyISAM主键索引与辅助索引的结构
    MyISAM引擎的索引文件和数据文件是分离的。MyISAM引擎索引结构的叶子节点的数据域,存放的并不是实际的数据记录,而是数据记录的地址索引文件与数据文件分离,这样的索引称为"非聚簇索引"。MyISAM的主索引与辅助索引区别并不大,只是主键索引不能有重复的关键字。
    【面试】网易游戏面试题目整理及答案(2)_第12张图片
    在MyISAM中,索引(含叶子节点)存放在单独的.myi文件中,叶子节点存放的是数据的物理地址偏移量(通过偏移量访问就是随机访问,速度很快)。主索引是指主键索引,键值不可能重复;辅助索引则是普通索引,键值可能重复。通过索引查找数据的流程:先从索引文件中查找到索引节点,从中拿到数据的文件指针,再到数据文件中通过文件指针定位了具体的数据。辅助索引类似。
    InnoDB主键索引与辅助索引的结构
    InnoDB引擎索引结构的叶子节点的数据域,存放的就是实际的数据记录(对于主索引,此处会存放表中所有的数据记录;对于辅助索引此处会引用主键,检索的时候通过主键到主键索引中找到对应数据行),或者说,InnoDB的数据文件本身就是主键索引文件,这样的索引被称为“聚簇索引”,一个表只能有一个聚簇索引。
    1)主键索引:我们知道InnoDB索引是聚集索引,它的索引和数据是存入同一个.idb文件中的,因此它的索引结构是在同一个树节点中同时存放索引和数据,如下图中最底层的叶子节点有三行数据,对应于数据表中的id、stu_id、name数据项。
    【面试】网易游戏面试题目整理及答案(2)_第13张图片
    在Innodb中,索引分叶子节点和非叶子节点,非叶子节点就像新华字典的目录,单独存放在索引段中,叶子节点则是顺序排列的,在数据段中。Innodb的数据文件可以按照表来切分(只需要开启innodb_file_per_table),切分后存放在xxx.ibd中,默认不切分,存放在xxx.ibdata中。
    辅助(非主键)索引:
    这次我们以示例中学生表中的name列建立辅助索引,它的索引结构跟主键索引的结构有很大差别,在最底层的叶子结点有两行数据,第一行的字符串是辅助索引,按照ASCII码进行排序,第二行的整数是主键的值。这就意味着,对name列进行条件搜索,需要两个步骤:
    ① 在辅助索引上检索name,到达其叶子节点获取对应的主键;
    ② 使用主键在主索引上再进行对应的检索操作
    这也就是所谓的“回表查询
    【面试】网易游戏面试题目整理及答案(2)_第14张图片
    InnoDB 索引结构需要注意的点
    1 数据文件本身就是索引文件
    2 表数据文件本身就是按 B+Tree 组织的一个索引结构文件
    3 聚集索引中叶节点包含了完整的数据记录
    4 InnoDB 表必须要有主键,并且推荐使用整型自增主键
    正如我们上面介绍 InnoDB 存储结构,索引与数据是共同存储的,不管是主键索引还是辅助索引,在查找时都是通过先查找到索引节点才能拿到相对应的数据,如果我们
    在设计表结构时没有显式指定索引列的话,MySQL 会从表中选择数据不重复的列建立索引,如果没有符合的列,则 MySQL 自动为 InnoDB 表生成一个隐含字段作为主键,并且这个字段长度为6个字节,类型为整型。

    Hash索引
    主要就是通过Hash算法(常见的Hash算法有直接定址法、平方取中法、折叠法、除数取余法、随机数法),将数据库字段数据转换成定长的Hash值,与这条数据的行指针一并存入Hash表的对应位置;如果发生Hash碰撞(两个不同关键字的Hash值相同),则在对应Hash键下以链表形式存储
    ①检索算法:在检索查询时,就再次对待查关键字再次执行相同的Hash算法,得到Hash值,到对应Hash表对应位置取出数据即可,如果发生Hash碰撞,则需要在取值时进行筛选。目前使用Hash索引的数据库并不多,主要有Memory等。
    MySQL目前有Memory引擎和NDB引擎支持Hash索引
    full-text全文索引
    全文索引也是MyISAM的一种特殊索引类型,主要用于全文索引,InnoDB从MYSQL5.6版本提供对全文索引的支持。它用于替代效率较低的LIKE模糊匹配操作,而且可以通过多字段组合的全文索引一次性全模糊匹配多个字段。同样使用B-Tree存放索引数据,但使用的是特定的算法,将字段数据分割后再进行索引(一般每4个字节一次分割),索引文件存储的是分割前的索引字符串集合,与分割后的索引信息,对应Btree结构的节点存储的是分割后的词信息以及它在分割前的索引字符串集合中的位置
    R-Tree空间索引
    空间索引是MyISAM的一种特殊索引类型,主要用于地理空间数据类型
    问题1:为什么Mysql索引要用B+树不是B树?
    答:用B+树不用B树考虑的是IO对性能的影响,B树的每个节点都存储数据,而B+树只有叶子节点才存储数据,所以查找相同数据量的情况下,B树的高度更高,IO更频繁。数据库索引是存储在磁盘上的,当数据量大时,就不能把整个索引全部加载到内存了,只能逐一加载每一个磁盘页(对应索引树的节点)。其中在MySQL底层对B+树进行进一步优化:在叶子节点中是双向链表,且在链表的头结点和尾节点也是循环指向的。
    问题2:为何不采用Hash方式?
    答:因为Hash索引底层是哈希表,哈希表是一种以key-value存储数据的结构,所以多个数据在存储关系上是完全没有任何顺序关系的,所以,对于区间查询是无法直接通过索引查询的,就需要全表扫描。所以,哈希索引只适用于等值查询的场景。而B+ Tree是一种多路平衡查询树,所以他的节点是天然有序的(左子节点小于父节点、父节点小于右子节点),所以对于范围查询的时候不需要做全表扫描。哈希索引不支持多列联合索引的最左匹配规则,如果有大量重复键值得情况下,哈希索引的效率会很低,因为存在哈希碰撞问题

  2. 哪些情况需要创建索引
    答:1 主键自动建立唯一索引
    2 频繁作为查询条件的字段
    3 查询中与其他表关联的字段,外键关系建立索引
    4 单键/组合索引的选择问题,高并发下倾向创建组合索引
    5 查询中排序的字段,排序字段通过索引访问大幅提高排序速度
    6 查询中统计或分组字段
    补充问题:哪些情况不要创建索引?
    1 表记录太少
    2 经常增删改的表
    3 数据重复且分布均匀的表字段,只应该为最经常查询和最经常排序的数据列建立索引(如果某个数据类包含太多的重复数据,建立索引没有太大意义)
    4 频繁更新的字段不适合创建索引(会加重IO负担)
    5 where条件里用不到的字段不创建索引

  3. MySQL覆盖索引
    答:覆盖索引(Covering Index),或者叫索引覆盖, 也就是平时所说的不需要回表操作;就是select的数据列只用从索引中就能够取得,不必读取数据行,MySQL可以利用索引返回select列表中的字段,而不必根据索引再次读取数据文件,换句话说查询列要被所建的索引覆盖。索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它不必读取整个行。毕竟索引叶子节点存储了它们索引的数据,当能通过读取索引就可以得到想要的数据,那就不需要读取行了。一个索引包含(覆盖)满足查询结果的数据就叫做覆盖索引
    判断标准:使用explain,可以通过输出的extra列来判断,对于一个索引覆盖查询,显示为using index,MySQL查询优化器在执行查询前会决定是否有索引覆盖查询

  4. MySQL中 in和 exists 的区别?
    答:exists:exists对外表用loop逐条查询,每次查询都会查看exists的条件语句,当exists里的条件语句能够返回记录行时(无论记录行是的多少,只要能返回),条件就为真,返回当前loop到的这条记录;反之,如果exists里的条件语句不能返回记录行,则当前loop到的这条记录被丢弃,exists的条件就像一个bool条件,当能返回结果集则为true,不能返回结果集则为false
    in:in查询相当于多个or条件的叠加

SELECT * FROM A WHERE A.id IN (SELECT id FROM B);
SELECT * FROM A WHERE EXISTS (SELECT * from B WHERE B.id = A.id);

如果查询的两个表大小相当,那么用in和exists差别不大。如果两个表中一个较小,一个是大表,则子查询表大的用exists,子查询表小的用in:

  1. UNION和UNION ALL的区别?
    答:UNION和UNION ALL都是将两个结果集合并为一个,两个要联合的SQL语句 字段个数必须一样,而且字段类型要“相容”(一致);UNION在进行表连接后会筛选掉重复的数据记录(效率较低),而UNION ALL则不会去掉重复的数据记录UNION会按照字段的顺序进行排序,而UNION ALL只是简单的将两个结果合并就返回

  2. SQL执行顺序
    答:SQL的执行顺序图如下:【面试】网易游戏面试题目整理及答案(2)_第15张图片

  3. mysql 的内连接、左连接、右连接有什么区别?什么是内连接、外连接、交叉连接、笛卡尔积呢?
    答:MySQL的JOIN图 【面试】网易游戏面试题目整理及答案(2)_第16张图片

参考资料

  1. https://www.cnblogs.com/kkkky/p/7754383.html
  2. https://blog.csdn.net/jackieeecheng/article/details/69779824
  3. https://blog.csdn.net/JianNingGao/article/details/80351010
  4. https://www.zhihu.com/question/57697842
  5. https://www.cnblogs.com/tongkey/p/8587060.html

你可能感兴趣的:(面试题)