Star's Blog

Keep learning, Keep improving


  • 首页

  • 分类

  • 关于

  • 标签

  • 归档

  • 搜索

异步任务——FutureTask

发表于 2018-11-05 | 分类于 多线程

Runnable、Callable、Future、FutureTask

和Java异步打交道就不能回避掉Runnable,Callable,Future,FutureTask等类,首先来介绍下这几个类的区别。

Runnable

Runnable接口是我们最熟悉的,它只有一个run函数。然后使用某个线程去执行该runnable即可实现多线程,Thread类在调用start()函数后就是执行的是Runnable的run()函数。Runnable最大的缺点在于run函数没有返回值。

Callable

Callable接口和Runnable接口类似,它有一个call函数。使用某个线程执行Callable接口实质就是执行其call函数。call方法和run方法最大的区别就是call方法有返回值:

1
2
3
4
5
6
7
8
9
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}

Callable 接口有返回值,泛型 V 就是要返回的结果类型,可以返回子任务的执行结果。

Future接口

Future 接口表示异步计算的结果,通过 Future 接口提供的方法,可以很方便的查询异步计算任务是否执行完成,获取异步计算的结果,取消未执行的异步任务,或者中断异步任务的执行,接口定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Future<V> {

boolean cancel(boolean mayInterruptIfRunning);

boolean isCancelled();

boolean isDone();

V get() throws InterruptedException, ExecutionException;

V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}

cancel(boolean mayInterruptIfRunning):取消子任务的执行,如果这个子任务已经执行结束,或者已经被取消,或者不能被取消,这个方法就会执行失败并返回false;如果子任务还没有开始执行,那么子任务会被取消,不会再被执行;如果子任务已经开始执行了,但是还没有执行结束,根据mayInterruptIfRunning的值,如果mayInterruptIfRunning = true,那么会中断执行任务的线程,然后返回true,如果参数为false,会返回true,不会中断执行任务的线程。
需要注意,这个方法执行结束,返回结果之后,再调用isDone()会返回true。

isCancelled():判断任务是否被取消,如果任务执行结束(正常执行结束和发生异常结束,都算执行结束)前被取消,也就是调用了cancel()方法,并且cancel()返回true,则该方法返回true,否则返回false。

isDone():判断任务是否执行结束,正常执行结束,或者发生异常结束,或者被取消,都属于结束,该方法都会返回true.

V get():获取结果,如果这个计算任务还没有执行结束,该调用线程会进入阻塞状态。如果计算任务已经被取消,调用get()会抛出CancellationException,如果计算过程中抛出异常,该方法会抛出ExecutionException,如果当前线程在阻塞等待的时候被中断了,该方法会抛出InterruptedException。

V get(long timeout, TimeUnit unit):带超时限制的get(),等待超时之后,该方法会抛出TimeoutException,如果子任务执行结束了,但是超时时间还没有到,这个方法也会返回结果。

FutureTask

Future只是一个接口,在实际使用过程中,诸如ThreadPoolExecutor返回的都是一个FutureTask实例。

1
2
3
public class FutureTask<V> implements RunnableFuture<V> 

public interface RunnableFuture<V> extends Runnable, Future<V>

FutureTask可以像Runnable一下,封装异步任务,然后提交给Thread或线程池执行,然后获取任务执行结果。原因在于 FutureTask 实现了 RunnableFuture 接口,RunnableFuture是什么呢,其实就是 Runnable 和 Future 的结合,实现了 Runnable 和 Future 两个接口。

FutureTask的成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class FutureTask<V> implements RunnableFuture<V> {
/*
* FutureTask中定义了一个state变量,用于记录任务执行的相关状态 ,状态的变化过程如下
* NEW -> COMPLETING -> NORMAL
* NEW -> COMPLETING -> EXCEPTIONAL
* NEW -> CANCELLED
* NEW -> INTERRUPTING -> INTERRUPTED
*/
private volatile int state;
//主流程状态
private static final int NEW = 0; //当FutureTask实例刚刚创建到callbale的call方法执行完成前,处于此状态
private static final int COMPLETING = 1; //callable的call方法执行完成或出现异常时,首先进行此状态
private static final int NORMAL = 2;//callable的call方法正常结束时,进入此状态,将outcom设置为正常结果
private static final int EXCEPTIONAL = 3;//callable的call方法异常结束时,进入此状态,将outcome设置为抛出的异常
//取消任务执行时可能处于的状态
private static final int CANCELLED= 4;// FutureTask任务尚未执行,即还在任务队列的时候,调用了cancel方法,进入此状态
private static final int INTERRUPTING = 5;// FutureTask的run方法已经在执行,收到中断信号,进入此状态
private static final int INTERRUPTED = 6;// 任务成功中断后,进入此状态

private Callable<V> callable;//需要执行的任务,提示:如果提交的是Runnable对象,会先转换为Callable对象,这是构造方法参数
private Object outcome; //任务运行的结果
private volatile Thread runner;//执行此任务的线程

//等待该FutureTask的线程链表,对于同一个FutureTask,如果多个线程调用了get方法,对应的线程都会加入到waiters链表中,同时当FutureTask执行完成后,也会告知所有waiters中的线程
private volatile WaitNode waiters;
......
}
阅读全文 »

Java并发队列BlockingQueue

发表于 2018-11-04 | 分类于 多线程

在Concurrent包中(在Java5版本开始提供),BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利。本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景。

BlockingQueue

阻塞队列,顾名思义,首先它是一个队列,而一个队列在数据结构中所起的作用大致如下图所示:

从上图我们可以很清楚看到,通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出;

常用的队列主要有以下两种:(当然通过不同的实现方式,还可以延伸出很多不同类型的队列,DelayQueue就是其中的一种)

先进先出(FIFO):先插入的队列的元素也最先出队列,类似于排队的功能。从某种程度上来说这种队列也体现了一种公平性。

后进先出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件。  

​ 多线程环境中,通过队列可以很容易实现数据共享,比如经典的“生产者”和“消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然。然而,在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。好在此时,强大的concurrent包横空出世了,而他也给我们带来了强大的BlockingQueue。(在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒),下面两幅图演示了BlockingQueue的两个常见阻塞场景:

如上图所示:当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。

如上图所示:当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。

  这也是我们在多线程环境下,为什么需要BlockingQueue的原因。作为BlockingQueue的使用者,我们再也不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了。既然BlockingQueue如此神通广大,让我们一起来见识下它的常用方法

BlockingQueue的核心方法

放入数据

offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.(本方法不阻塞当前执行方法的线程);      
offer(E o, long timeout, TimeUnit unit):可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。

put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续.

获取数据

poll():取走BlockingQueue里排在首位的对象,取不到时返回null;

poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则直到时间超时还没有数据可取,返回失败。

take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入;

drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。

BlockingQueue 对插入操作、移除操作、获取元素操作提供了四种不同的方法用于不同的场景中使用:1、抛出异常;2、返回特殊值(null 或 true/false,取决于具体的操作);3、阻塞等待此操作,直到这个操作成功;4、阻塞等待此操作,直到成功或者超时指定时间。总结如下:

Throws exception Special value Blocks Times out
Insert add(e) offer(e) put(e) offer(e, time, unit)
Remove remove() poll() take() poll(time, unit)
Examine element() peek() not applicable not applicable

对于 BlockingQueue,我们的关注点应该在 put(e) 和 take() 这两个方法,因为这两个方法是带阻塞的。

ThrowsException:如果操作不能马上进行,则抛出异常

SpecialValue:如果操作不能马上进行,将会返回一个特殊的值,一般是true或者false

Blocks: 如果操作不能马上进行,操作会被阻塞

TimesOut: 如果操作不能马上进行,操作会被阻塞指定的时间,如果指定时间没执行,则返回一个特殊值,一般是true或者false

BlockingQueue 不接受 null 值的插入,相应的方法在碰到 null 的插入时会抛出 NullPointerException 异常。null 值在这里通常用于作为特殊值返回(表格中的第三列),代表 poll 失败。所以,如果允许插入 null 值的话,那获取的时候,就不能很好地用 null 来判断到底是代表失败,还是获取的值就是 null 值。

常见的几种BlockingQueue

阅读全文 »

Linux的iptables原理

发表于 2018-11-03 | 分类于 网络

在Linux系统中,对于防火墙的实现一般分为包过滤防火墙,TCP-Wrapper即程序管控,代理服务器等几种方式。其中,iptables作为一种基于包过滤方式的防火墙工具,在实际中应用非常广泛,是非常重要的一个安全工具。真正实现防火墙功能的是 netfilter,它是一个 linux 内核模块,做实际的包过滤。实际上,除了 iptables 以外,还有很多类似的用户空间工具。

iptables的“链”与“表”

netfilter 使用表(table)和 链(chain)来组织网络包的处理规则(rule)。它默认定义了以下表和链:

表 表功能 链 链功能
raw PREROUTING OUTPUT RAW 拥有最高的优先级,它使用PREROUTING和OUTPUT两个链,因此 RAW 可以覆盖所有包。在raw表中支持一个特殊的目标:TRACE,使内核记录下每条匹配该包的对应iptables规则信息。使用raw表内的TRACE target 即可实现对iptables规则的跟踪调试。比如:# iptables -t raw -A OUTPUT -p icmp -j TRACE # ipt ables -t raw -A PREROUTING -p icmp -j TRACE
Filter 包过滤 FORWARD 过滤目的地址和源地址都不是本机的包
INPUT 过滤目的地址是本机的包
OUTPUT 过滤源地址是本机的包
Nat 网络地址转换 PREROUTING 在路由前做地址转换,使得目的地址能够匹配上防火墙的路由表,常用于转换目的地址。
POSTROUTING 在路由后做地址转换。这意味着不需要在路由前修改目的地址。常用于转换源地址。
OUTPUT 对防火墙产生的包做地址转换(很少量地用于 SOHO 环境中)
Mangle TCP 头修改 PREROUTING POSTROUTING OUTPUT INPUT FORWARD 在路由器修改 TCP 包的 QoS(很少量地用在 SOHO 环境中)

先是透过路由判断, 决定了输出的路径后,再透过 filter 的 OUTPUT 链来传送的, mangle 这个表格很少被使用,如果将上图的mangle 拿掉的话,那就容易看的多了:

如果你的防火墙事实上是用来管制 LAN 内的其他主机的话,那么你就必须要再针对 filter 的 FORWARD 这条链,还有 nat 的 PREROUTING, POSTROUTING 以及 OUTPUT 进行额外的规则订定才行。

iptables实现SNAT与DNAT

NAT 服务器的重点就在于上面流程NAT table 的两条重要的链:PREROUTING 与 POSTROUTING。

举例如下:

SNAT封包传送和封包接收

客户端所发出的封包表头中,来源会是 192.168.1.100 ,然后传送到 NAT 这部主机;NAT 这部主机的内部接口 (192.168.1.2) 接收到这个封包后,会主动分析表头数据, 因为表头数据显示目的并非 Linux 本机,所以开始经过路由, 将此封包转到可以连接到 Internet 的 Public IP 处;由于 private IP 与 public IP 不能互通,所以 Linux 主机透过 iptables 的 NAT table 内的 Postrouting 链将封包表头的来源伪装成为 Linux 的 Public IP ,并且将两个不同来源 (192.168.1.100 及 public IP) 的封包对应写入暂存内存当中, 然后将此封包传送出去了;
此时 Internet 上面看到这个封包时,都只会知道这个封包来自那个 Public IP 而不知道其实是来自内部啦。 好了,那么如果 Internet 回传封包呢?又会怎么作?

在 Internet 上面的主机接到这个封包时,会将响应数据传送给那个 Public IP 的主机;当 Linux NAT 服务器收到来自 Internet 的回应封包后,会分析该封包的序号,并比对刚刚记录到内存当中的数据, 由于发现该封包为后端主机之前传送出去的,因此在 NAT Prerouting 链中,会将目标 IP 修改成为后端主机,亦即那部 192.168.1.100,然后发现目标已经不是本机 (public IP), 所以开始透过路由分析封包流向;封包会传送到 192.168.1.2 这个内部接口,然后再传送到最终目标 192.168.1.100 机器上去!

SNAT 主要是应付内部 LAN 连接到 Internet 的使用方式,至于 DNAT 则主要用在内部主机想要架设可以让 Internet 存取的服务器啦!

DNAT封包传送

假设我的内部主机 192.168.1.210 启动了 WWW 服务,这个服务的 port 开启在 port 80 , 那么 Internet 上面的主机 (61.xx.xx.xx) 要如何连接到我的内部服务器呢?当然啦, 还是得要透过 Linux NAT 服务器嘛!所以这部 Internet 上面的机器必须要连接到我们的 NAT 的 public IP 才行。外部主机想要连接到目的端的 WWW 服务,则必须要连接到我们的 NAT 服务器上头;我们的 NAT 服务器已经设定好要分析出 port 80 的封包,所以当 NAT 服务器接到这个封包后, 会将目标 IP 由 public IP 改成 192.168.1.210 ,且将该封包相关信息记录下来,等待内部服务器的响应;上述的封包在经过路由后,来到 private 接口处,然后透过内部的 LAN 传送到 192.168.1.210 上头!

192.186.1.210 会响应数据给 61.xx.xx.xx ,这个回应当然会传送到 192.168.1.2 上头去;经过路由判断后,来到 NAT Postrouting 的链,然后透过刚刚的记录,将来源 IP 由 192.168.1.210 改为 public IP 后,就可以传送出去了!

iptables常用命令

阅读全文 »

JAVA打印Array数组内容的几种方法

发表于 2018-11-03 | 分类于 Java基础

下面是几种常见的打印Array数组内容的方式。

方法一:使用循环打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Demo {
public static void main(String[] args) {
String[] infos = new String[] {"Java", "Android", "C/C++", "Kotlin"};

StringBuffer strBuffer = new StringBuffer();
for(int i = 0; i< infos.length; i++) {
if(i > 0) {
strBuffer.append(", ");
}
strBuffer.append(infos[i]);
}
System.out.println(strBuffer);
}
}

方法二:使用 Arrays.toString() 打印

1
2
3
4
5
6
7
public class Demo {
public static void main(String[] args) {
String[] infos = new String[] {"Java", "Android", "C/C++", "Kotlin"};

System.out.println(Arrays.toString(infos));
}
}

方法三:使用 JDK 8 的 java.util.Arrays.stream() 打印

1
2
3
4
5
6
7
public class Demo {
public static void main(String[] args) {
String[] infos = new String[] {"Java", "Android", "C/C++", "Kotlin"};

Arrays.stream(infos).forEach(System.out::println);
}
}

方法四:使用 Arrays.deepToString() 方法打印。如果数组中有其它数组,即多维数组,也会用同样的方法深度显示。

1
2
3
4
5
6
7
public class Demo {
public static void main(String[] args) {
String[] infos = new String[] {"Java", "Android", "C/C++", "Kotlin"};

System.out.println(Arrays.deepToString(infos));
}
}

茴香豆的写法有很多种,打开眼界最重要~

MySql数据库中的索引

发表于 2018-10-28 | 分类于 数据库

索引对于良好的性能非常关键。尤其是当表中的数据量越来越大时,索引对性能的影响愈发重要。

b-tree索引应该是mysql里最广泛的索引的了,除了archive基本所有的存储引擎都支持它。

创建索引的3种方法:

1、创建索引

1
CREATE INDEX <索引的名字> ON tablename (列的列表);

2、修改表

1
ALTER TABLE tablename ADD INDEX [索引的名字] (列的列表);

3、创建表的时候指定索引

1
2
3
CREATE TABLE tablename ( [...], INDEX [索引的名字] (列的列表) ); 
--示例
CREATE TABLE mytable( ID INT NOT NULL, username VARCHAR(16) NOT NULL, INDEX [indexName] (username(length)) );

MySQL索引类型

mysql里目前只支持4种索引分别是:full-text,b-tree,hash,r-tree。

full-text索引(全文索引)

full-text在mysql里仅有myisam支持它,而且支持full-text的字段只有char、varchar、text数据类型。

full-text主要是用来代替like “%***%”效率低下的问题。

全文索引就是使用倒排索引的方式实现的。

MySQL5.6版本后的InnoDB存储引擎开始支持全文索引,5.7版本后通过使用ngram插件开始支持中文。

b-tree索引

b-tree在myisam里的形式和innodb稍有不同(下文会重点介绍)

在 innodb里,有两种形态:一是primary key形态,其leaf node里存放的是数据,而且不仅存放了索引键的数据,还存放了其他字段的数据。二是secondary index,其leaf node和普通的b-tree差不多,只是还存放了指向主键的信息.

而在myisam里,主键和其他的并没有太大区别。不过和innodb不太一样的地方是在myisam里,leaf node里存放的不是主键的信息,而是指向数据文件里的对应数据行的信息。

hash索引

目前我所知道的就只有memory和ndb cluster支持这种索引。

hash索引由于其结构,所以在每次查询的时候直接一次到位,不像b-tree那样一点点的前进。所以hash索引的效率高于b-tree,但hash也有缺点(本篇后文介绍)。

r-tree索引

r-tree在mysql很少使用,仅支持geometry数据类型,支持该类型的存储引擎只有myisam、bdb、innodb、ndb、archive几种。

相对于b-tree,r-tree的优势在于范围查找。

索引的数据结构

B-Tree索引

维基百科对B树的定义为“在计算机科学中,B树(B-tree)是一种树状数据结构,它能够存储数据、对其进行排序并允许以O(log n)的时间复杂度运行进行查找、顺序读取、插入和删除的数据结构。B树,概括来说是一个节点可以拥有多于2个子节点的二叉查找树。与自平衡二叉查找树不同,B-树为系统最优化大块数据的读和写操作。B-tree算法减少定位记录时所经历的中间过程,从而加快存取速度。普遍运用在数据库和文件系统。”

目前大多数的存储引擎使用B-Tree索引,严格来说是 B+树。相比B树,二叉树,Hash,它有哪些优势呢?

相对于二叉树,明显的优势是避免树的深度过大而造成磁盘I/O读写过于频繁;相对于Hash,见下面Hash索引限制描述;相比较B树来说,B+树的非叶子结点只包含导航信息,不包含实际的值,所有的叶子结点和相连的节点使用链表相连,便于区间查找和遍历。

适用场景:等值匹配,全值匹配,匹配最左前缀,匹配列前缀,范围匹配,只访问索引的查询,如覆盖索引。

Hash 索引

哈希索引基于哈希表实现,只有精确匹配索引所有列时才有效。对于每一行数据,存储引擎都会根据索引列计算一个哈希值,哈希索引将所有的hash值存储在索引中,同时在哈希表中保存指向每个数据行的指针。

MySQL中只有Memory引擎显式支持哈希索引。这也是Memory引擎的默认存储引擎,Memory引擎同时也支持B-Tree索引,哈希索引解决碰撞的方式是使用链表。

Hash索引的限制:

1、哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行;

2、哈希索引数据并不是按照索引列的值顺序存储的,所以也就无法用于排序;

3、哈希索引也不支持部分索引列匹配查找,因为哈希索引始终是使用索引的全部列值内容来计算哈希值的。如:数据列(a,b)上建立哈希索引,如果只查询数据列a,则无法使用该索引;

4、哈希索引只支持等值比较查询,如:=,in(), <=>,不支持任何范围查询;

5、访问哈希索引的数据非常快,除非有很多哈希冲突,当出现哈希冲突的时候,存储引擎必须遍历链表中所有的行指针,逐行进行比较,直到找到所有符合条件的行;

6、如果哈希冲突很多的话,一些索引维护操作的代价也很高,如:如果在某个选择性很低的列上建立哈希索引(即很多重复值的列),那么当从表中删除一行时,存储引擎需要遍历对应哈希值的链表中的每一行,找到并删除对应的引用,冲突越多,代价越大;

适用场景:只需要做等值比较查询,而不包含排序或范围查询的需求,都适合使用哈希索引。

MyISAM和InnoDB对B-Tree索引实现

MyISAM索引文件和数据文件是分离的,索引文件仅保存记录所在页的指针(物理位置),通过这些地址来读取页,进而读取被索引的行,对于二级(辅助)索引,与主索引在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复,可见MyISAM索引是“非聚合的”。

InnoDB的主索引是采用“聚集索引”的数据存储方式,所谓“聚集”,就是指数据行和键值紧凑地存储在一起(InnoDB 只能聚集一个叶子页(16K)的记录),因为InnoDB的数据文件本身要按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形;对于二级(辅助)索引,InnoDB采用的方式是在叶子页中保存主键值,通过这个主键值来回表查询到一条完整记录,因此按辅助索引检索实际上进行了二次查询,效率肯定是没有按照主键检索高的。由于每个辅助索引都包含主键索引,因此,为了减小辅助索引所占空间,我们通常希望 InnoDB 表中的主键索引尽量定义得小一些(值得一提的是,MySIAM会使用前缀压缩技术使得索引变小,而InnoDB按照原数据格式进行存储),并且希望InnoDB的主键是自增长的,因为如果主键并非自增长,插入时,由于写入时乱序的,会使得插入效率变低。

索引的优点

最常见的B-Tree索引,按照顺序存储数据,索引可以做ORDER BY和GROUP BY操作。因为数据是有序的,所以B-Tree也会将相关的列存储在一起,因为索引中存储了实际的列值,所以某些查询只是用索引就能够完成全部查询(覆盖索引,索引包含所有满足查询需要的数据的索引,也就是平时所说的不需要回表操作):索引大大减少了服务器需要扫描的数据量;索引可以帮助服务器避免排序和临时表;索引可以将随机IO变为顺序IO;

阅读全文 »
1…121314…20
Morning Star

Morning Star

100 日志
14 分类
37 标签
GitHub
© 2021 Morning Star
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4