一点点学习心得

1.udp.

udp用户数据包协议,udp提供了应用程序之间传输数据的基本机制,是不可靠,无连接的数据交付服务,像dns。它急于端口号区分在一台机器运行的多个程序,在传输每个udp报文时,除携带用户数据外,还携带目的端口号和原端口号,使得接收机器上的udp软件能够将报文交给正确的接收进程,接收进程也能正确返回应答报文。经常用户音视频的传输。

2.java 基础类Objec公共方法有哪些作用。

equals方法

一般equals和==是不一样的,但是在Object中两者是一样的。子类一般都要重写这个方法。

toString方法

一般子类都有覆盖,返回该对象字符串。

hashCode方法

返回该对象的哈希码值

该方法用于哈希查找,重写了equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到。

一般必须满足obj1.equals(obj2)==true。可以推出obj1.hashCode()==obj2.hashCode()。

但是hashCode相等不一定就满足equals。

不过为了提高效率,应该尽量使上面两个条件接近等价。

wait方法

wait方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。

wait()方法一直等待,直到获得锁或者被中断。

wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。

调用该方法后当前线程进入睡眠状态,直到以下事件发生。

(1)其他线程调用了该对象的notify方法。

(2)其他线程调用了该对象的notifyAll方法。

(3)其他线程调用了interrupt中断该线程。

(4)时间间隔到了。

此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。

notify方法

该方法唤醒在该对象上等待的某个线程。

notifyAll方法

该方法唤醒在该对象上等待的所有线程。

3.hashcode方法作用及应用场景

返回该对象的哈希码值。支持此方法是为了提高哈希表(例如 java.util.Hashtable 提供的哈希表)的性能。

hashCode 的常规协定是:

在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行equals 比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
如果根据 equals(Object) 方法,两个对象是相等的,那么对这两个对象中的每个对象调用hashCode 方法都必须生成相同的整数结果。和目的
如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么对这两个对象中的任一对象上调用hashCode 方法不 要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。

应用场景

1、涉及到重写equals()方法时,需要考虑到覆盖hashcode()方法

2、如果用对象作为map的键时,需要考虑到覆盖hashcode,以便提高或者达到自己的目的

例如用户学习记录,在重写equals(Object) 方法时,需要考虑到覆盖hashcode()方法使不相等对象返回不同的哈希码值。

4.快速排序原理以及时间空间复杂度,最好最坏情况原始数据是什么,怎么计算出来的时间复杂度和空间复杂度。

算法包含简单操作次数叫做时间复杂度,程序运行从开始到结束所需的存储空间叫做空间复杂度。

原理:任意取带排序序列一个元素作为基准元素,将带排序列分为左右2个子序列,左子序列的值比基准序列值小,右子序列值比基准序列值大,这样基准元素在带排序序列的位置就确定下来,这称为一趟快速排序。然后以此递归,对子序列在进行快速排序,知道所有子序列中只有一个元素为止,整个快速排序过程结束。

最好情况:每次划分,正好分成2个等长的子序列,时间复杂度 o(nlog2n),空间复杂度为o(log2n).

最坏情况:每次划分,只得到一个子序列,和冒泡排序一样,时间复杂度o(n的平方)  空间复杂度为o(n).

快速排序是个不稳定的排序方法。

5.冒泡排序原理,以及时间空间复杂度。

原理:每相邻的2个元素比较大小,大的记录互换位置,使大的值下沉,每一遍把最后一个下沉的位置记下,下一遍只需检查比较到此为止,到所有记录都不发生位置改变为止,整个冒泡排序过程结束。

手写一个冒泡算法

void bubbleshot(int [] array){

       for(i=0,i<array.length-1,i++){

           for(j=array.length-1,j>i,j--){

               if(array[j]<array[j-1]){

                     int  temp=array[j-1]

                     array[j-1]=array[j];

                     array[j]=temp;

               }

           }

      }

}

空间复杂度为1   时间复杂度为n的平方

6.线程池有哪些以及使用场景

  • CachedThreadPool 用于并发执行大量短期的小任务,或者是负载较轻的服务器。
  • FixedThreadPool 用于负载比较重的服务器,为了资源的合理利用,需要限制当前线程数量。
  • SingleThreadExecutor 用于串行执行任务的场景,每个任务必须按顺序执行,不需要并发执行。
  • ScheduledThreadPoolExecutor 用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景。

1.newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

可以看到,FixedThreadPool 的核心线程数和最大线程数都是指定值,也就是说当线程池中的线程数超过核心线程数后,任务都会被放到阻塞队列中。

此外 keepAliveTime 为 0,也就是多余的空余线程会被立即终止(由于这里没有多余线程,这个参数也没什么意义了)。

而这里选用的阻塞队列是 LinkedBlockingQueue,使用的是默认容量 Integer.MAX_VALUE,相当于没有上限。

因此这个线程池执行任务的流程如下:

  1. 线程数少于核心线程数,也就是设置的线程数时,新建线程执行任务
  2. 线程数等于核心线程数后,将任务加入阻塞队列 
    • 由于队列容量非常大,可以一直加加加
  3. 执行完任务的线程反复去队列中取任务执行

FixedThreadPool 用于负载比较重的服务器,为了资源的合理利用,需要限制当前线程数量。

2.newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

从参数可以看出来,SingleThreadExecutor 相当于特殊的 FixedThreadPool,它的执行流程如下:

  1. 线程池中没有线程时,新建一个线程执行任务
  2. 有一个线程以后,将任务加入阻塞队列,不停加加加
  3. 唯一的这一个线程不停地去队列里取任务执行

听起来很可怜的样子 - -。

SingleThreadExecutor 用于串行执行任务的场景,每个任务必须按顺序执行,不需要并发执行。

3.newCachedThreadPool

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

可以看到,CachedThreadPool 没有核心线程,非核心线程数无上限,也就是全部使用外包,但是每个外包空闲的时间只有 60 秒,超过后就会被回收。

CachedThreadPool 使用的队列是 SynchronousQueue,这个队列的作用就是传递任务,并不会保存。

因此当提交任务的速度大于处理任务的速度时,每次提交一个任务,就会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。

它的执行流程如下:

  1. 没有核心线程,直接向 SynchronousQueue 中提交任务
  2. 如果有空闲线程,就去取出任务执行;如果没有空闲线程,就新建一个
  3. 执行完任务的线程有 60 秒生存时间,如果在这个时间内可以接到新任务,就可以继续活下去,否则就拜拜

由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不会占用任何资源。

CachedThreadPool 用于并发执行大量短期的小任务,或者是负载较轻的服务器。

4.newScheduledThreadPool

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
}
private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;

ScheduledThreadPoolExecutor 继承自 ThreadPoolExecutor, 最多线程数为 Integer.MAX_VALUE ,使用 DelayedWorkQueue 作为任务队列。

ScheduledThreadPoolExecutor 添加任务和执行任务的机制与ThreadPoolExecutor 有所不同。

ScheduledThreadPoolExecutor 添加任务提供了另外两个方法:

  • scheduleAtFixedRate() :按某种速率周期执行
  • scheduleWithFixedDelay():在某个延迟后执行
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                              long initialDelay,
                                              long period,
                                              TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    if (period <= 0L)
        throw new IllegalArgumentException();
    ScheduledFutureTask<Void> sft =
        new ScheduledFutureTask<Void>(command,
                                      null,
                                      triggerTime(initialDelay, unit),
                                      unit.toNanos(period),
                                      sequencer.getAndIncrement());
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
    sft.outerTask = t;
    delayedExecute(t);
    return t;
}
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                 long initialDelay,
                                                 long delay,
                                                 TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    if (delay <= 0L)
        throw new IllegalArgumentException();
    ScheduledFutureTask<Void> sft =
        new ScheduledFutureTask<Void>(command,
                                      null,
                                      triggerTime(initialDelay, unit),
                                      -unit.toNanos(delay),
                                      sequencer.getAndIncrement());
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
    sft.outerTask = t;
    delayedExecute(t);
    return t;

可以看到,这两种方法都是创建了一个 ScheduledFutureTask 对象,调用 decorateTask() 方法转成 RunnableScheduledFuture 对象,然后添加到队列中

看下 ScheduledFutureTask 的主要属性:

private class ScheduledFutureTask<V>
        extends FutureTask<V> implements RunnableScheduledFuture<V> {
 
    //添加到队列中的顺序
    private final long sequenceNumber;
    //何时执行这个任务
    private volatile long time;
    //执行的间隔周期
    private final long period;
    //实际被添加到队列中的 task
    RunnableScheduledFuture<V> outerTask = this;
    //在 delay queue 中的索引,便于取消时快速查找
    int heapIndex;
    //...

DelayQueue 中封装了一个优先级队列,这个队列会对队列中的 ScheduledFutureTask 进行排序,两个任务的执行 time 不同时,time 小的先执行;否则比较添加到队列中的顺序 sequenceNumber ,先提交的先执行。

ScheduledThreadPoolExecutor 的执行流程如下:

  1. 调用上面两个方法添加一个任务
  2. 线程池中的线程从 DelayQueue 中取任务
  3. 然后执行任务

具体执行任务的步骤也比较复杂:

  1. 线程从 DelayQueue 中获取 time 大于等于当前时间的 ScheduledFutureTask 
    • DelayQueue.take()
  2. 执行完后修改这个 task 的 time 为下次被执行的时间
  3. 然后再把这个 task 放回队列中 
    • DelayQueue.add()

ScheduledThreadPoolExecutor 用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景。

两种提交任务的方法

ExecutorService 提供了两种提交任务的方法:

execute

    execute():提交不需要返回值的任务

    submit():提交需要返回值的任务

   void execute(Runnable command)

    execute() 的参数是一个 Runnable,也没有返回值。因此提交后无法判断该任务是否被线程池执行成功。

ExecutorService executor = Executors.newCachedThreadPool();
executor.execute(new Runnable() {
    @Override
    public void run() {
        //do something
    }
});

submit

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

submit() 有三种重载,参数可以是 Callable 也可以是 Runnable

同时它会返回一个 Funture 对象,通过它我们可以判断任务是否执行成功。

获得执行结果调用 Future.get() 方法,这个方法会阻塞当前线程直到任务完成。

提交一个 Callable 任务时,需要使用 FutureTask 包一层:

FutureTask futureTask = new FutureTask(new Callable<String>() {    //创建 Callable 任务
    @Override
    public String call() throws Exception {
        String result = "";
        //do something
        return result;
    }
});
Future<?> submit = executor.submit(futureTask);    //提交到线程池
try {
    Object result = submit.get();    //获取结果
} catch (InterruptedException e) {
    e.printStackTrace();
} catch (ExecutionException e) {
    e.printStackTrace();
}

如何合理地选择或者配置

了解 JDK 提供的几种线程池实现,在实际开发中如何选择呢?

根据任务类型决定。 
前面已经介绍了,这里再小节一下:

  • CachedThreadPool 用于并发执行大量短期的小任务,或者是负载较轻的服务器。
  • FixedThreadPool 用于负载比较重的服务器,为了资源的合理利用,需要限制当前线程数量。
  • SingleThreadExecutor 用于串行执行任务的场景,每个任务必须按顺序执行,不需要并发执行。
  • ScheduledThreadPoolExecutor 用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景。

自定义线程池时,如果任务是 CPU 密集型(需要进行大量计算、处理),则应该配置尽量少的线程,比如 CPU 个数 + 1,这样可以避免出现每个线程都需要使用很长时间但是有太多线程争抢资源的情况; 
如果任务是 IO密集型(主要时间都在 I/O,CPU 空闲时间比较多),则应该配置多一些线程,比如 CPU 数的两倍,这样可以更高地压榨 CPU。

为了错误避免创建过多线程导致系统奔溃,建议使用有界队列。因为它在无法添加更多任务时会拒绝任务,这样可以提前预警,避免影响整个系统。

执行时间、顺序有要求的话可以选择优先级队列,同时也要保证低优先级的任务有机会被执行。

6.并发集合concurrenthashmap内部原理以及数据结构。

ConcurrentHashMap是Java1.5中引用的一个线程安全的支持高并发的HashMap集合类。这篇文章总结了ConcurrentHashMap的内部实现原理,是对于自己理解后的一些整理。

1.HashTable与ConcurrentHashMap的对比

HashTable本身是线程安全的,写过Java程序的都知道通过加Synchronized关键字实现线程安全,这样对整张表加锁实现同步的一个缺陷就在于使程序的效率变得很低。这就是为什么Java中会在1.5后引入ConcurrentHashMap的原因。

内部结构的对比

从图中可以看出,HashTable的锁加在整个Hash表上,而ConcurrentHashMap将锁加在segment上(每个段上),这样我们在对segment1操作的时候,同时也可以对segment2中的数据操作,这样效率就会高很多。

2.ConcurrentHashMap的内部结构

这里写图片描述

ConcurrentHashMap主要有三大结构:整个Hash表,segment(段),HashEntry(节点)。每个segment就相当于一个HashTable。

(1)HashEntry类

每个HashEntry代表Hash表中的一个节点,在其定义的结构中可以看到,除了value值没有定义final,其余的都定义为final类型,我们知道Java中关键词final修饰的域成为最终域。用关键词final修饰的变量一旦赋值,就不能改变,也称为修饰的标识为常量。这就意味着我们删除或者增加一个节点的时候,就必须从头开始重新建立Hash链,因为next引用值需要改变。

在使用锁来协调多线程间并发访问的模式下,减小对锁的竞争可以有效提高并发性。有两种方式可以减小对锁的竞争:

  • 减小请求同一个锁的频率。
  • 减少持有锁的时间。

ConcurrentHashMap 的高并发性主要来自于三个方面:

  • 用分离锁实现多个线程间的更深层次的共享访问。
  • 用 HashEntery 对象的不变性来降低执行读操作的线程在遍历链表期间对加锁的需求。
  • 通过对同一个 Volatile 变量的写 / 读访问,协调不同线程间读 / 写操作的内存可见性。

使用分离锁,减小了请求同一个锁的频率

7.常见数据类型占用内存,以及内存是什么决定的。

boolean  1个字节,8位

byte:1个字节,8位,-128~127 最大存储数据量 255

short:2个字节,16位,-32768~32767 最大存储数据量 65535

char:2个字节,16位,存储Unicode编码,用‘’

int:4个字节,32位,负的2的31次方~2的31次方-1  最大存储数据量 2的31次方-1

float:4个字节,32位

double:8个字节,64位

long:8个字节,64位,负的2的64次方~2的64次方-1  最大存储数据量 2的64次方-1

 8.设计模式

  1. 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
  2. 结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
  3. 行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录 

9.多线程的理解,如何保证线程安全?

进程:是计算机运用程序实例,拥有独立的内存空间和数据(猜测内存堆应该是作用的进程上),一个进程包含多个子线程,不同进程相互独立;

线程:cpu执行的基本单位,拥有独立的寄存器和栈,同一进程下的线程共享地址&内存空间;线程栈存放方法的栈帧,

串行:一个线程执行到底,相当于单线程。

并发:多个线程交替执行,抢占cpu的时间片,但是速度很快,在外人看来就像是多个线程同时执行。

并行:多个线程在不同的cpu中同时执行。

并发与并行的区别:

并发严格的说不是同时执行多个线程,只是线程交替执行且速度很快,相当于同时执行。

而并行是同时执行多个线程,也就是多个cpu核心同时执行多个线程。

在实际开发中,我们不需要关心是否是并发还是并行,因为cpu会帮我们处理多线程,开发中可以认为多线程就是同时执行多个线程。

当多个线程要共享一个实例对象的值得时候,那么在考虑安全的多线程并发编程时就要保证下面3个要素:

原子性(Synchronized, Lock)

有序性(Volatile,Synchronized, Lock)

可见性(Volatile,Synchronized,Lock)

当然由于synchronized和Lock保证每个时刻只有一个线程执行同步代码,所以是线程安全的,也可以实现这一功能,但是由于线程是同步执行的,所以会影响效率。

下面是对3个要素的详细解释:

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

在Java中,基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取共享变量时,它会去内存中读取新值。

普通的共享变量不能保证可见性,因为普通共享变量被修改后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

更新主存的步骤:当前线程将其他线程的工作内存中的缓存变量的缓存行设置为无效,然后当前线程将变量的值跟新到主存,更新成功后将其他线程的缓存行更新为新的主存地址

其他线程读取变量时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。

11.ArrayList和LinkedList的大致区别如下:

1.ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。 
2.对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。 
3.对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。

ArrayList,Vector主要区别为以下几点: 
(1):Vector是线程安全的,源码中有很多的synchronized可以看出,而ArrayList不是。导致Vector效率无法和ArrayList相比; 

(2):ArrayList和Vector都采用线性连续存储空间,当存储空间不足的时候,ArrayList默认增加为原来的50%,Vector默认增加为原来的一倍; 

12.

HashSet与HashMap的区别:

HashMap                                                       HashSet
实现了Map接口                                              实现Set接口
存储键值对                                                    仅存储对象
调用put()向map中添加元素                     调用add()方法向Set中添加元素
HashMap使用键(Key)计算Hashcode    
                                                                      HashSet使用成员对象来计算hashcode值,

                                                                           对于两个对象来说hashcode可能相同,

                                                                          所以equals()方法用来判断对象的相等性,

                                                                            如果两个对象不同的话,那么返回false

HashMap相对于HashSet较快,因为它是使用唯一的键获取对象    HashSet较HashMap来说比较慢


HashMap和Hashtable的区别:
HashMap和Hashtable都实现了Map接口,但决定用哪一个之前先要弄清楚它们之间的分别。主要的区别有:线程安全性,同步(synchronization),以及速度。

HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。
HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。
另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。
由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。
HashMap不能保证随着时间的推移Map中的元素次序是不变的。

13.

Thread类的sleep()方法和对象的wait()方法都可以让线程暂停执行,它们有什么区别?

答:sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复(线程回到就绪状态,请参考第66题中的线程状态转换图)。wait()是Object类的方法,调用对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。

14.

简述synchronized 和java.util.concurrent.locks.Lock的异同?

答:Lock是Java 5以后引入的新的API,和关键字synchronized相比主要相同点:Lock 能完成synchronized所实现的所有功能;主要不同点:Lock有比synchronized更精确的线程语义和更好的性能,而且不强制性的要求一定要获得锁。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且最好在finally 块中释放(这是释放外部资源的最好的地方)。


15.MySQL索引背后的数据结构及算法原理

https://www.cnblogs.com/sanwenyu/p/6129082.html

本文以MySQL数据库为研究对象,讨论与数据库索引相关的一些话题。特别需要说明的是,MySQL支持诸多存储引擎,而各种存储引擎对索引的支持也各不相同,因此MySQL数据库支持多种索引类型,如BTree索引,哈希索引,全文索引等等。为了避免混乱,本文将只关注于BTree索引,因为这是平常使用MySQL时主要打交道的索引,至于哈希索引和全文索引本文暂不讨论。

文章主要内容分为三个部分。

第一部分主要从数据结构及算法理论层面讨论MySQL数据库索引的数理基础。

第二部分结合MySQL数据库中MyISAM和InnoDB数据存储引擎中索引的架构实现讨论聚集索引、非聚集索引及覆盖索引等话题。

第三部分根据上面的理论基础,讨论MySQL中高性能使用索引的策略。

猜你喜欢

转载自blog.csdn.net/qq_29678299/article/details/86509009