自我介绍
重写和重载的区别,JAVA面向对象编程的三大特点
重载是指在同一个类中,允许存在多个同名的方法,但这些方法的参数列表必须不一样,重载主要用于提供多种不同的方法实现,根据传入的参数不同来执行不同的操作,例如Arrays.sort()方法就是一个重载方法,它可以对不同类型的数组进行排序。
重写是指子类对父类中的方法进行重新定义,重写的方法和父类的方法具有相同的方法名、参数列表和返回值类型,重写主要用于扩展父类的功能,例如子类重写了父类的toString()方法,可以在子类中添加一些新的功能。
JAVA面向对象编程的三大特点是:封装、继承和多态。
封装:将对象的属性和方法包装成一个整体,使得外部无法直接访问对象的内部细节,从而保证了对象的安全性和可靠性。
继承:子类可以继承父类的属性和方法,从而减少了代码的重复,提高了代码的复用性。继承可以实现代码的重用,也可以实现代码的扩展。
多态:同一种类型的对象,在不同的情况下会表现出不同的行为特征,这种现象称为多态。多态可以提高代码的灵活性和可扩展性。依赖于重写和重载。
HASHMAP解决哈希冲突的方法
哈希冲突是指不同的key经过哈希函数计算后得到相同的哈希值,这种现象称为哈希冲突。哈希冲突会导致数据丢失,因此需要解决哈希冲突。常用的解决哈希冲突的方法有以下几种:
- 开放定址法:当发生哈希冲突时,使用某种探测技术在散列表中找到另一个空的存储位置,将其插入到该位置。常用的探测技术有线性探测、二次探测和双重散列。
- 链地址法:将哈希表的每个单元作为链表的头结点,所有哈希值相同的元素都放在同一个链表中。
- 再哈希法:当发生哈希冲突时,使用另一个哈希函数计算出另一个哈希值,直到不再发生冲突为止。
- 建立公共溢出区:将哈希表分为基本表和溢出表两部分,哈希值相同的元素放在同一个链表中,当发生冲突时,将冲突的元素放在溢出表中。
- 负载因子调整:当哈希表中元素的数量超过哈希表长度的一半时,就需要扩容,否则会导致哈希冲突的概率增大。
hashmap为什么每次扩容都是2的幂次方
扩容的目的是为了减少哈希冲突,提高哈希表的性能。如果每次扩容都是1的倍数,那么扩容后的容量会与之前的容量相等,这样就无法有效地减少哈希冲突的发生。
而选择2的倍数作为扩容的大小,可以保证扩容后的容量是原容量的两倍,这样在进行元素重新哈希分布时,可以更加均匀地将元素分布到新的桶中,减少哈希冲突的概率。
此外,选择2的倍数还有一个好处是,通过位运算来计算哈希值对容量取模时,可以用位运算替代取模运算,提高计算效率。例如,对于容量为2的幂次方的哈希表,可以用位运算来计算哈希值对容量取模,即 hash & (capacity - 1),而对于其他容量的哈希表,则需要使用取模运算,即 hash % capacity。
综上所述,HashMap每次扩容都是2的倍数是为了保持哈希表的性能和效率。
JAVA垃圾回收是怎么处理的
Java的垃圾回收机制是Java虚拟机(JVM)自动处理的一种内存管理方式。它能够自动识别和回收不再使用的对象,释放其占用的内存空间,以避免内存泄漏和内存溢出等问题。Java垃圾回收器采用的是可达性分析算法,即从一组称为“GC Roots”的对象开始,通过引用链向下搜索,搜索到的对象都是存活的,未搜索到的对象则被标记为垃圾对象。GC Roots包括虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象以及本地方法栈中JNI引用的对象等
常用的一些垃圾回收算法有以下几种:
- 标记-清除算法:标记-清除算法分为标记和清除两个阶段,首先标记出所有需要回收的对象,然后统一回收所有被标记的对象。标记-清除算法会产生大量不连续的内存碎片,导致内存分配效率降低。
- 复制算法:复制算法将可用内存分为大小相等的两块,每次只使用其中一块,当这一块内存用完时,将还存活的对象复制到另一块内存上,然后清除已使用的内存。复制算法简单高效,但是会浪费一半的内存空间。
- 标记-整理算法:标记-整理算法分为标记和整理两个阶段,首先标记出所有需要回收的对象,然后将所有存活的对象都向一端移动,然后清除端边界以外的内存。标记-整理算法不会产生内存碎片,但是需要移动对象,效率较低。
- 分代收集算法:分代收集算法根据对象的存活周期将内存分为新生代和老年代,新生代中的对象存活周期较短,老年代中的对象存活周期较长。新生代中的对象使用复制算法,老年代中的对象使用标记-整理算法。
Spring AOP?IOC?讲一下什么意思
Spring AOP是Spring框架中的一个重要特性,全称为Spring Aspect-Oriented Programming,即面向切面编程。它通过在程序运行期间动态地将代码切入到指定的方法或代码段中,实现对这些方法或代码段的增强。AOP主要用于解决横切关注点(Cross-cutting Concerns)的问题,例如日志记录、性能监控、事务管理等。
IOC是Inversion of Control的缩写,即控制反转。它是一种设计原则,用于降低模块之间的耦合度。在传统的编程模型中,模块之间的依赖关系是由模块自身创建和管理的,而在IOC模式下,控制权被反转给了容器,容器负责创建和管理模块之间的依赖关系。Spring框架通过IOC容器来实现控制反转,将对象的创建和依赖关系的管理交给了Spring容器,开发者只需要通过配置文件或注解来描述对象之间的依赖关系,而不需要手动创建和管理对象。
综上所述,Spring AOP用于解决横切关注点的问题,而IOC用于实现控制反转,降低模块之间的耦合度。两者结合使用可以使代码更加模块化、可维护性更强。
类加载双亲委派模型
类加载双亲委派模型(ClassLoader Delegation Model)是Java中的一种类加载机制。它是一种层次化的类加载方式,通过一系列的ClassLoader按照特定的顺序来加载类。
在Java中,类加载器(ClassLoader)负责将类的字节码文件加载到内存中,并创建对应的Class对象。类加载器根据双亲委派模型来加载类,即当一个类加载器需要加载一个类时,它会先委派给其父类加载器去尝试加载,只有当父类加载器无法加载时,才由当前类加载器来加载。
双亲委派模型的主要优势在于可以保证类的唯一性和安全性。通过层层委派,当一个类需要被加载时,首先会由最顶层的启动类加载器(Bootstrap ClassLoader)尝试加载。如果启动类加载器无法加载,那么会由扩展类加载器(Extension ClassLoader)尝试加载。如果扩展类加载器也无法加载,那么会由应用程序类加载器(Application ClassLoader)尝试加载。只有当这些类加载器都无法加载时,才会由自定义的类加载器来加载。
这种层次化的加载方式可以有效地避免类的重复加载,提高了类加载的效率。同时,通过双亲委派模型,Java可以保证核心类库的安全性,防止恶意代码替换核心类库。
需要注意的是,虽然双亲委派模型是Java的默认类加载机制,但是在某些情况下,我们也可以自定义类加载器来改变加载行为。
sleep()和wait()的区别
sleep()和wait()都可以使线程进入阻塞状态,但是sleep()不会释放锁,而wait()会释放锁。
什么是threadlocal? 工作原理
ThreadLocal是Java中的一个类,用于实现线程级别的数据隔离。它提供了一种在多线程环境下,每个线程都可以独立地获取、设置和修改自己的变量副本的机制。
工作原理如下:
- 每个ThreadLocal对象都维护了一个独立的变量副本,这个副本只能被当前线程访问和修改,其他线程无法直接访问到。
- 当使用ThreadLocal的get()方法获取变量的值时,它会首先获取当前线程的ThreadLocalMap对象,然后以当前ThreadLocal对象作为key,在ThreadLocalMap中查找对应的变量副本。
- 如果当前线程的ThreadLocalMap中不存在对应的变量副本,则通过调用ThreadLocal的initialValue()方法来初始化一个副本,并将其存储到ThreadLocalMap中。
- 当使用ThreadLocal的set()方法设置变量的值时,它会同样先获取当前线程的ThreadLocalMap对象,然后将当前ThreadLocal对象作为key,将变量副本作为value存储到ThreadLocalMap中。
- 当线程结束时,ThreadLocalMap会被垃圾回收,从而避免了内存泄漏。
通过使用ThreadLocal,每个线程都可以独立地操作自己的变量副本,而不会影响其他线程的副本。这在一些需要保证线程安全的场景下非常有用,例如在Web应用中,每个请求都可以使用一个独立的ThreadLocal来保存请求相关的数据,避免了线程安全问题。
删除倒数第n个节点
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode fast = dummy;
ListNode slow = dummy;
for (int i = 0; i <= n; i++) {
fast = fast.next;
}
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return dummy.next;
}
编程实现两个线程交替修改同一个变量10次
public class Test {
private static volatile int count = 0;
private static final Object lock = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
synchronized (lock) {
while (count % 2 == 1) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":"+count);
count++;
lock.notify();
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
synchronized (lock) {
while (count % 2 == 0) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":"+count);
count++;
lock.notify();
}
}
});
t1.start();
t2.start();
}
}
手写线程安全的单例模式
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
给定一个字符串,找出其中出现次数最多的字符
public class Test {
public static void main(String[] args) {
//String str = "abbbccdd";
// 任意输入字符串
Scanner sc = new Scanner(System.in);
String str = sc.nextLine();
// 定义一个map,用于存储字符和出现次数
Map<Character, Integer> map = new HashMap<>();
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
map.put(c, map.getOrDefault(c, 0) + 1);
}
int max = 0;
char res = ' ';
for (Map.Entry<Character, Integer> entry : map.entrySet()) {
if (entry.getValue() > max) {
max = entry.getValue();
res = entry.getKey();
}
}
System.out.println(res);
}
}
#include <iostream>
#include <string>
#include <unordered_map>
int main(){
int max = 0;
char res = ' ';
std::string str;
std::cin >> str;
std::unordered_map<char, int> map;
for (int i = 0; i < str.size(); i++) {
map[str[i]]++;
if (map[str[i]] > max) {
max = map[str[i]];
res = str[i];
}
}
std::cout << res << std::endl;
return 0;
}
ThreadLocal和Synchronized的区别
- 都是用来解决多线程中相同变量的访问冲突问题
- 不同的是Synchonized是通过线程等待,牺牲时间来解决访问冲突,而ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且ThreadLocal解决的是变量在不同线程间的隔离问题,Synchronized解决的是多个线程之间访问资源的同步性问题。
JVM内存模型
- 堆:存放对象实例,几乎所有的对象实例都在这里分配内存,是垃圾收集器管理的主要区域,也被称为GC堆。
- 栈:存放基本类型的变量和对象的引用变量,是线程私有的,每个线程都有一个独立的栈,栈中的数据(原始类型和对象引用)只能由所属线程访问。
- PC计数器:记录当前线程执行的字节码的行号,用于实现线程切换和恢复执行的功能。
- 方法区:存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 运行时常量池:是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
- 元空间:是方法区在HotSpot中的实现,它与持久代不同的是,元空间并不在虚拟机中,而是使用本地内存,因此,默认情况下,元空间的大小仅受本地内存限制,但是可以通过参数来指定元空间的大小。
- 本地方法栈:与虚拟机栈类似,区别在于虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机使用到的Native方法服务。
redis的AOF和RDB
AOF(Append Only File)是一种日志型的持久化方式,它将所有的写操作以追加的方式写入到文件中,当Redis重启时,会通过重新执行文件中保存的写操作来在内存中重建整个数据库的内容。AOF持久化方式有三种同步策略,分别是:
- always:每个Redis写命令都立即同步到AOF文件,这样会严重降低Redis的速度。
- everysec:每秒同步一次,Redis默认的同步策略。
- no:完全依赖操作系统来进行同步,速度最快,但是也最不安全,如果操作系统出现故障,可能会导致数据丢失。
RDB(Redis DataBase)是一种快照型的持久化方式,它会将某个时间点的数据库状态保存到一个RDB文件中,当Redis重启时,会通过加载RDB文件来在内存中重建整个数据库的内容。RDB持久化方式有两种同步策略,分别是:
- save:当save配置的时间间隔内,数据库中的键值对数量超过了save配置的数量时,就会触发RDB持久化操作。
- bgsave:当bgsave命令被调用时,就会触发RDB持久化操作。
@springbootApplication包含哪些注解
@SpringBootApplication注解包含了以下三个注解:
- @SpringBootConfiguration:标注当前类是一个配置类,它会将当前类作为配置类注册到Spring容器中。
- @EnableAutoConfiguration:开启自动配置功能,它会根据依赖的jar包自动配置项目的配置信息。
- @ComponentScan:开启组件扫描功能,它会自动扫描当前类所在包及其子包下的所有类,并将它们作为Spring容器中的组件进行注册。
nacos包含哪些组件
Nacos包含以下五大部分
- 注册中心:用于服务的注册和发现。
- 配置中心:用于动态配置服务。
- Sentinel:用于服务的流量控制和熔断降级。
- 负责均衡:用于服务的负载均衡。
- 网关:用于服务的网关代理和流量聚合。
线程池有哪几种类型
线程池有以下几种类型:
- newCachedThreadPool:创建一个可缓存的线程池,如果线程池的当前规模超过了处理需求时,那么将回收空闲的线程,而当需求增加时,则可以添加新的线程,线程池的规模不存在任何限制。
- newFixedThreadPool:创建一个固定大小的线程池,该线程池中的线程数量始终不变,当有一个新的任务提交时,线程池中若有空闲线程,则立即执行,若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
- newScheduledThreadPool:创建一个固定大小的线程池,该线程池可以延迟或定时的执行任务。
- newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。
- newSingleThreadScheduledExecutor:创建一个单线程化的线程池,该线程池可以延迟或定时的执行任务。
JVM中方法区主要存放哪些类型数据
JVM中方法区主要存放以下类型的数据:
- 类型信息:包括类的完整信息、字段信息、方法信息、接口信息等。
- 常量:包括字符串常量、基本类型常量和符号引用。
什么是内存泄漏,如何避免内存泄漏
内存泄漏是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏危害可以忽略,但内存泄漏堆积后果很严重,无论多少内存,迟早会被占光。内存泄漏的主要原因是:程序中动态分配内存空间后,失去了对该内存的控制,因而造成了内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
在JVM中,内存泄漏主要是由于以下几种情况导致的:
- 静态集合类引起的内存泄漏:静态集合类(如HashMap、ArrayList等)是常用的内存泄漏的根源,因为静态集合类的生命周期和应用程序一致,如果静态集合类持有对象的引用,那么这些对象将无法被GC回收,从而导致内存泄漏。
- 单例模式引起的内存泄漏:单例模式中,如果单例对象持有外部对象的引用,那么这个外部对象将无法被GC回收,从而导致内存泄漏。
- 监听器和回调引起的内存泄漏:如果在代码中注册了监听器,但是没有显示地反注册,那么这个监听器将无法被GC回收,从而导致内存泄漏。
- 对象池和连接池未关闭导致的内存泄漏:如果使用完对象池和连接池后,没有显示地关闭,那么这些对象将无法被GC回收,从而导致内存泄漏。
避免内存泄漏的方法主要有以下几种:
- 尽早释放对象的引用:当对象不再被使用时,应该尽早释放它的引用,使其可以被GC回收。
- 使用弱引用:弱引用是一种比软引用更弱的引用类型,当GC执行时,无论内存是否充足,都会回收只被弱引用关联的对象。
- 使用虚引用:虚引用是一种比弱引用更弱的引用类型,它是唯一一种无法通过引用来获取对象的引用类型,它的作用是在对象被GC回收时收到一个系统通知。
ArrayList和LinkedList的区别
ArrayList是基于可扩容的动态数组,初始化时要设置容量大小
LinkedList是基于双向链表
ArrayList的查询效率高,增删效率低
LinkedList的查询效率低,增删效率高
ArrayList和LinkedList扩容机制
ArrayList的扩容机制是当数组容量不足时,会创建一个新的数组,将原数组中的元素复制到新数组中,然后将原数组垃圾回收
LinkedList不用初始化大小,也没有扩容机制,因为它是基于链表实现的,每次添加元素时,都会创建一个新的节点,然后将新节点添加到链表的尾部
HashMap的数据结构,get和put流程
HashMap的数据结构是数组+链表+红黑树,数组是HashMap的主体,链表和红黑树是为了解决哈希冲突而存在的,链表是为了解决低于8的冲突,红黑树是为了解决大于8的冲突
get流程:首先调用hashcode方法计算key的哈希值,然后将哈希值与数组长度-1进行逻辑与运算得到index值,用这个index值来确定数据存储在数组中的位置,通过循环来遍历索引位置对应的链表,初始值为数据存储在数组中的位置,循环条件为Entry对象不为null,改变循环条件为Entry对象的下一个节点,如果key值相等,并且Entry对象当中的key值与get方法传进来的key值是同一个对象,或者key值的hashcode和key值相等,那么就找到了对应的value值,否则返回null
put流程:
1.调用hash函数得到key的HashCode值
2.通过HashCode值与数组长度-1逻辑与运算得到一个index值
3.遍历索引位置对应的链表,如果Entry对象的hash值与hash函数得到的hash值相等,并且该Entry对象的key值与put方法传过来的key值相等则,将该Entry对象的value值赋给一个变量,将该Entry对象的value值重新设置为put方法传过来的value值。将旧的value返回。
4.添加Entry对象到相应的索引位置对应的链表中,如果该链表的长度大于等于8,并且数组的长度大于等于64,则将该链表转换为红黑树
HashMap的扩容机制
HashMap的扩容机制是当数组中的元素个数超过数组长度*负载因子时,就会进行扩容,扩容后的数组长度为原数组长度的两倍
扩容流程:
1.创建一个新的Entry数组,长度为原数组长度的两倍
2.遍历原数组,将原数组中的每个Entry对象重新计算hash值,然后将Entry对象添加到新数组中
3.将新数组赋值给原数组
4.如果原数组长度大于等于64,则将原数组中的链表转换为红黑树
HashMap中桶的下标是如何确定的
HashMap中桶的下标是通过key的hash值与数组长度-1进行逻辑与运算得到的
Arraylist和hashmap的使用场景
ArrayList适用于查询多,增删少的场景
为什么并发会带来线程不安全
多个资源对统一资源的进行数据竞争,导致数据不一致,例如一个线程正在对一个数据进行写操作,另一个线程在对这个数据进行读操作,由于读写操作的顺序不确定,导致读到的数据不是最新的数据,从而导致数据不一致
Syncronized和CAS
Syncronized是悲观锁,CAS是乐观锁
Syncronized是通过锁升级的方式来保证线程安全的,当线程获取到锁时,其他线程只能等待,从而保证了线程安全
CAS是通过比较内存中的值和期望值是否相等来保证线程安全的,如果相等则更新内存中的值,否则不更新,从而保证了线程安全
CAS会出现ABA问题,可以通过版本号来解决
线程池的核心参数以及使用流程
线程池的核心参数有以下几个:
- corePoolSize:核心线程数,线程池中始终存活的线程数量。
- maximumPoolSize:最大线程数,线程池中允许的最大线程数量。
- keepAliveTime:线程空闲时间,当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量等于corePoolSize。
- TimeUnit:时间单位,keepAliveTime的时间单位。
- workQueue:任务队列,用于存放还未执行的任务,它是一个阻塞队列。
- threadFactory:线程工厂,用于创建线程,一般用默认即可。一般可以用来给线程设置名字或一些定制化的使用
- AbortPolicy:拒绝策略,当任务太多来不及处理时,如何拒绝任务。四种拒绝策略分别是:AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy。分别代表抛出异常、直接在调用者线程中运行、丢弃队列中最老的任务、丢弃无法处理的任务。
MySQL
- 有一张表(学生,科目,分数)字段,查出语文成绩排名第二的学生
select name from student order by score desc limit 1,1
- 有一张表(学生,科目,分数)字段,查出每个学生的最高分
select name,max(score) from student group by name
联合索引(a,b,c)现在用where里面的条件只有a,b,会不会用到索引
会用到索引,因为联合索引是按照索引的顺序来建立的,所以只要用到了索引的前两个字段,就会用到索引
InnoDB的底层数据结构
InnoDB的底层数据结构是B+树
// B+树的数据结构
public class BPlusTree<K extends Comparable<K>, V> {
// 根节点
protected BPlusTreeNode<K, V> root;
// 阶数,M值
protected int order;
// 叶子节点的链表头
protected BPlusTreeNode<K, V> head;
// 树高
protected int height = 0;
// 叶子节点个数
protected int size = 0;
// 非叶子节点最大的子节点数量
protected int maxNumber;
// 非叶子节点最小的子节点数量
protected int minNumber;
// 用于比较key值的比较器
protected Comparator<K> kComparator;
// 用于比较value值的比较器
protected Comparator<V> vComparator;
}
// B+树的节点
public class BPlusTreeNode<K extends Comparable<K>, V> {
// 是否为叶子节点
protected boolean isLeaf;
// 是否为根节点
protected boolean isRoot;
// 父节点
protected BPlusTreeNode<K, V> parent;
// 叶子节点的前节点
protected BPlusTreeNode<K, V> previous;
// 叶子节点的后节点
protected BPlusTreeNode<K, V> next;
// 节点的关键字
protected List<Entry<K, V>> entries;
// 子节点
protected List<BPlusTreeNode<K, V>> children;
}
Redis为什么这么快
- 纯内存操作
- 单线程的模型,避免了线程切换和竞争的开销
- 非阻塞IO
- 数据结构简单,对数据结构进行了优化
Redis使用的线程模型是怎样的
Reactor模型:利用IO多路复用接收客户端请求,然后负责接收请求,读取请求到命令执行再到响应请求都是单线程的,这样就避免了线程切换和竞争的开销
Redis的IO模型是怎么样的
给个订单表,海量数据如何分表?分表策略有哪些?
- 水平分表:按照订单号的hash值进行分表,这样可以保证同一个订单号的数据在同一个表中,但是会导致数据分布不均匀,因为订单号的hash值不一定是均匀的
- 垂直分表:按照订单表的字段进行分表,例如将订单表分为订单信息表和订单详情表,这样可以减少单个表的数据量,但是会导致查询时需要关联多张表
- 分库分表:按照某种规则将其分散到多个数据库和多张表中
- 分区分表:将数据按照某个规则(如时间、地理位置等)分散到多个分区表中。例如,按照订单创建时间的月份将订单数据分散到不同的分区表中。这样可以根据查询条件只查询特定分区,提高查询效率
InnoDB和MyISAM的区别
- InnoDB支持事务,MyISAM不支持事务
- Innodb支持行级锁,MyISAM支持表级锁
- InnoDB支持外键,MyISAM不支持外键
- InnoDB具有崩溃修复能力,MyISAM不具备,需要手动修复
InnoDB适合于事务支持和并发性能的场景,然而MyISAM适合与读密集的场景