整理一
- HashMap相关面试题:
- HashMap存储原理
- HashCode作用
- 遍历HashMap
- 有没有可能 2 个不相等的对象有相同的 hashcode
- Linux 常用命令
- mybatis相关面试题
- MyBatis
- MyBatis编程步骤是什么样的?
- Mybatis的一级、二级缓存
- union和union all
- 返回insert之后对应的主键
- MyBatis中#{}和${}
- tk和mp
- URI和URL
- SpringBoot相关面试题:
- spring boot 核心配置文件是什么?bootstrap.properties 和 application.properties 有何区别 ?
- 使用了哪些 starter maven 依赖项?
- spring-boot-starter-parent 有什么用 ?
- Spring Boot 需要独立的容器运行吗?
- 如何使用 Spring Boot 实现异常处理?
- Spring相关面试题
- @Autowired和@Resource
- Spring拦截器和Servlet过滤器
- 常用注解
- Spring容器默认创建Bean是是单例还是多例的
- Spring Bean 的生命周期
- Spring Bean 的作用域
- Spring 框架中都用到了哪些设计模式
- Spring 应用程序有哪些不同组件
- Spring框架中的单例bean是线程安全的吗?
- spring 自动装配 bean 有哪些方式
- 可以在Spring中注入一个null 和一个空字符串吗?
- SpringMVC面试题
- 什么是Spring MVC?简单介绍下你对Spring MVC的理解?
- SpringMVC工作流程
- Tomcat 优化
- Tomcat内存优化
- Tomcat线程优化
- 禁用DNS查询
- 设置session过期时间
- 数据库相关面试题
- SQL执行流程
- oracle和mysql区别
- 数据库三大范式
- mysql常用搜索引擎
- 数据库优化
- 按照锁的粒度分数据库锁有哪些?
- 从锁的类别上分MySQL都有哪些锁呢?
- 什么是死锁?怎么解决?
- 数据库的乐观锁和悲观锁是什么?怎么实现的?
- SQL 约束
- 关联查询
- mysql中 in 和 exists 区别
HashMap相关面试题:
HashMap存储原理
- 获取到传过来的key,调用hash算法获取到hash值
- 获取到hash值之后调用indexFor方法,通过获取到的hash值以及数组的长度算出数组的下标 (把哈希值和数组容量转换为二进,再在数组容量范围内与哈希值进行一次与运算,同为1则1,不然则为0,得出数组的下标值,这样可以保证计算出的数组下标不会大于当前数组容量)
- 把传过来的key和value存到该数组下标当中。如该数组下标下以及有值了,则使用链表将值添加到尾部节点。
HashCode作用
也就是:重写了equals(Object obj)方法,则为什么必要重写hashCode()方法
比如说我们new一个Object,java都会根据这个Object产生一个hashCode并丢到一个Hash哈希表中去,这样的话,下次做Object的比较或者取这个对象的时候,它会根据对象的hashcode再从Hash表中取这个对象。这样做可以提高取对象的效率
- new Object(),JVM根据这个对象的Hashcode值放入到对应的Hash表对应的Key上,如果不同的对象却产生了相同的hash值,也就是发生了Hash key相同导致冲突的情况,那么就在这个Hash key的地方产生一个链表,将所有产生相同hashcode的对象放到这个单链表上串在一起。
- 比较两个对象的时候,首先根据他们的hashcode去hash表中找他的对象,当两个对象的hashcode相同,那么就是说他们这两个对象放在Hash表中的同一个key上,那么他们一定在这个key上的链表上。那么此时就只能根据Object的equal方法来比较这个对象是否equal。当两个对象的hashcode不同的话,肯定他们不能equal。
遍历HashMap
Map<String,Object> map = new HashMap<>();
map.put("one","1");
map.put("two","2");
map.put("three","3");
//遍历key
for(String str : map.keySet()){
System.out.print(str);
}
//遍历value
for(Object obj : map.values()){
System.out.print(obj);
}
//迭代器
Iterator<Map.Entry<String,Object>> iterator = map.entrySet().iterator();
while(iterator.hasNext()){
Map.Entry<String,Object> entry = iterator.next();
System.out.print(entry.getKey());
System.out.print(entry.getValue());
}
//entryset()
for (Map.Entry<String,Object> entry : map.entrySet()){
System.out.print(entry.getKey());
System.out.print(entry.getValue());
}
有没有可能 2 个不相等的对象有相同的 hashcode
有
public static void main(String[] args) {
String str1 = "通话";
String str2 = "重地";
System.out.println(str1.hashCode()+ ":" + str2.hashCode());
System.out.println(str1.equals(str2));
}
Linux 常用命令
- sz 文件名:下载指定文件
- ls:查看当前目录下的所有文件
- ll:查看当前目录下的所有文件及详情
- rz:上传指定文件
- pwd:查看路径
- SSH 用户@ip:连接服务器
- rm -rf 文件名:删除指定文件
- cp -r 文件名:复制
- mv 文件名 文件名/目录名:移动和重命名文件
- cd:切换目录
- vim/vi 文件名:编辑文件
shift + g:从下向上看
":q!":不保存退出;
":wq":保存退出;
":q":没有修改的情况下的正常退出;
":!":强制退出;
":w":保存但不退出;
":w!":强制保存但不退出;
":e!":放弃修改从上次修改的地方重新编辑
- getconf LONG_BIT 查看linux32位还是64的
- cat 文件名:显示文件内容
- ps -ef | grep 端口号:查看进程
- journalctl -xe:查看linux日志
- yum install gcc:安装GCC软件套件
- top:查看实时端口号对应的CPU使用情况
- netstat -ntu | grep 端口号:查看进程对应的端口号
- tail -f 文件名:实时查看文件信息,一般在查看日志文件时候使用
- clear:清屏
- df -h:显示磁盘使用情况
- sudo 文件名:以管理员的身份执行命令
- chmod +x 文件名:给指定文件添加权限
- chmod 777 文件名:给文件夹加权限,只有加权限之后才能使用xftp上传成功,否则提示上传失败
- chmod -R 用户名:组名 文件夹名 :给此文件夹及其子文件赋予用户及组
- lsof -p pid | wc -l :查看pid对应的端口的资源使用情况
mybatis相关面试题
MyBatis
是java持久层框架,封装了JDBC,提供了简洁的SqlSession,利用SqlSession来操作数据库
业务层数据到达数据访问层调用Mapper接口,MyBatis中有一个SqlSesssion会通过调用的Mapper接口创建一个实例,然后会通过映射配置文件,也就是你写的sql,最后通过封装的JDBC访问数据库。
MyBatis编程步骤是什么样的?
- 创建SqlSessionFactory
- 通过SqlSessionFactory创建SqlSession
- 通过sqlsession执行数据库操作
- 调用session.commit()提交事务
- 调用session.close()关闭会话
Mybatis的一级、二级缓存
- 一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。
- 二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置 ;
- 对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。
union和union all
我用的最多的就是用它来创建临时表:
union:结果是去除重复项之后的数据
union all:所有数据,包括重复项
返回insert之后对应的主键
添加:useGeneratedKey=“true” keyProperty=“id”>
使用此方法的前提是返回必须使用对象进行接收,否则失效
MyBatis中#{}和${}
#{}:占位符,会替换成"?",防止sql注入,转变成sql加双引号
${}:简单的字符串替换,有可能sql注入,转变成sql不加双引号
tk和mp
- 都是简化操作数据库sql的
- tk封装到mapper,mp可封装到mapper,也可封装到service层
- Mybatis-Plus通过AutoGenerator 来实现代码生成;tk通过MBG 插件
- mp有自己的分页插件,tk使用pagehelper来实现分页
URI和URL
- URI:用来标识资源,比如说公司的名称
- URL:用来资源定位,比如说公司具体的地址,可以 找到你的公司在哪里
- URL是URI的子集
- URI还包括URN:作为特定内容的唯一名称使用的,与当前资源的所在地无关。使用URN,就可以将资源四处迁移,而不用担心迁移后无法访问
SpringBoot相关面试题:
spring boot 核心配置文件是什么?bootstrap.properties 和 application.properties 有何区别 ?
单纯做 Spring Boot 开发,可能不太容易遇到 bootstrap.properties 配置文件,但是在结合 Spring Cloud 时,这个配置就会经常遇到了,特别是在需要加载一些远程配置文件的时侯。
spring boot 核心的两个配置文件:
bootstrap (. yml 或者 . properties):boostrap 由父 ApplicationContext 加载的,比 applicaton 优先加载,配置在应用程序上下文的引导阶段生效。一般来说我们在 Spring Cloud Config 或者 Nacos 中会用到它。且 boostrap 里面的属性不能被覆盖;
application (. yml 或者 . properties): 由ApplicatonContext 加载,用于 spring boot 项目的自动化配置。
使用了哪些 starter maven 依赖项?
spring-boot-starter-jdbc
spring-boot-starter-web
spring-boot-starter-data-redis
spring-boot-starter-test
spring-boot-starter-parent 有什么用 ?
我们都知道,新创建一个 Spring Boot 项目,默认都是有 parent 的,这个 parent 就是 spring-boot-starter-parent ,spring-boot-starter-parent 主要有如下作用:
- 定义了 Java 编译版本为 1.8 。
- 使用 UTF-8 格式编码。
- 继承自 spring-boot-dependencies,这个里边定义了依赖的版本,也正是因为继承了这个依赖,所以我们在写依赖时才不需要写版本号。
- 执行打包操作的配置。
- 自动化的资源过滤。
- 自动化的插件配置。
- 针对 application.properties 和 application.yml 的资源过滤,包括通过 profile 定义的不同环境的配置文件,例如 application-dev.properties 和 application-dev.yml。
Spring Boot 需要独立的容器运行吗?
可以不需要,内置了 Tomcat/ Jetty 等容器
如何使用 Spring Boot 实现异常处理?
Spring 提供了一种使用 @ControllerAdvice处理异常的非常有用的方法。 我们通过实现一个 @ControllerAdvice类,来处理控制器类抛出的所有异常。
Spring相关面试题
@Autowired和@Resource
@Autowired:按照类型来装配,可以配合使用@Qualifier(“name”)来实现名称装配
@Resource:按照名称装配,@Resource(name=""),可将name替换成type,按照类型装配
Spring拦截器和Servlet过滤器
共同点: 都可以实现权限检查、日志记录,都是AOP的体现
不同点:
- 规范不同:拦截器属于Spring;过滤器属于Servlet
- 范围不同:过滤器只能在web程序中使用,拦截器既可以在web中使用也可以在Application程序中使用
- 作用域不同:过滤器只在servlet前后起作用,拦截器可以在方法前后和异常前后起作用
说明:Application程序是java应用程序,比如说"Hello World!"
常用注解
Spring注解:
- @Component
- @Controller
- @Service
- @Repository:需要配置扫描地址
- @Mapper:不需要配置扫描地址
- @ComponentScan:扫描指定包下的类并放到bean容器中
- @Bean
- @Resource
- @Autowired
- @Qualifier:名称装配
- @Configuration:配置类
- @Value:注入值
- @RestController:@ResponseBody + @Controller
- @JsonProperty:用于属性,将序列化的名称改成另一个名称,用于序列化名称错误时比如说"isParent"不加会变成"isparent"
- @PropertySouce:读取配置文件
- @Scheduled:执行定时任务
- @Transactional:事务注解
SpringMVC注解:
- @RequestBody:接收前端传递给后台的json串,前端要使用POST请求
- @RequestParam
- @ResponseBody:将数据转成json串格式返回
Lombok注解:
- @Data
- @Setter
- @Getter
- @EqualsAndHashCode:生成equal()和hashcode()
- @ToString:生成toString()
- @AllArgsConstructor:生成全部参数的构造方法
- @NoArgsConstructor:生成空的构造方法
日志注解:
- @Slf4j
Spring容器默认创建Bean是是单例还是多例的
单例的:节省对象的创建时间和垃圾回收时间但是可能面临并发的问题
Spring Bean 的生命周期
- 实例化 Instantiation
- 属性赋值 Populate
- 初始化 Initialization
- 销毁 Destruction
实例化 -> 属性赋值 -> 初始化 -> 销毁
Spring Bean 的作用域
singleton:单例的,在ioc容器中只能有一个实例,默认就是singleton
prototype:多例的,可以生成任意数量的实例
request:每次http请求都会创建一个实例,作用域是一个http请求周期
session:每次http的session都会创建一个实例,作用域是http会话周期
globalsession:将bean设置成全局的session会话周期
request、session、globalsession只有在web中使用spring才能有效
Spring 框架中都用到了哪些设计模式
- 单例模式:Bean默认为单例模式
- 代理模式:SpringAOP
- 工厂模式:BeanFactory就是简单工厂模式的体现,用来创建对象的实例
- 模板方法:用来解决代码重复的问题。比如 RestTemplate,RedisTemplate等
- 适配器模式:
Spring 应用程序有哪些不同组件
- 接口
- bean类
- 配置文件
- 面向切面编程,提供面向切面编程的功能
- 用户程序,谁使用的接口
Spring框架中的单例bean是线程安全的吗?
不是,Spring框架中的单例bean不是线程安全的。
spring 中的 bean 默认是单例模式,spring 框架并没有对单例 bean 进行多线程的封装处理。
spring 自动装配 bean 有哪些方式
可以使用@Autowired自动装配
在Spring框架xml配置中共有5种自动装配:
- no:默认的方式是不进行自动装配的,通过手工设置ref属性来进行装配bean。
- byName:通过bean的名称进行自动装配,如果一个bean的 property 与另一bean 的name 相同,就进行自动装配。
- byType:通过参数的数据类型进行自动装配。
- constructor:利用构造函数进行装配,并且构造函数的参数通过byType进行装配。
- autodetect:自动探测,如果有构造方法,通过 construct的方式自动装配,否则使用 byType的方式自动装配。
可以在Spring中注入一个null 和一个空字符串吗?
可以。
SpringMVC面试题
什么是Spring MVC?简单介绍下你对Spring MVC的理解?
Spring MVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把模型-视图-控制器分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间的配合。
SpringMVC工作流程
- 用户通过发送请求(request)到前端控制器(DispatcherServlet)
- DisptcherServlet接收到请求之后调用处理器映射器(HandlerMapping)
- HandlerMapping通过请求的url找到具体的处理器并生成处理器对象和处理器拦截器,返回到DisptcherServlet
- DisptcherServlet通过处理器适配器HandlerAdapter调用处理器
- 执行处理器(Controller > Service > mapper)
- 执行之后将结果通过ModelAndView方式返回给DsiptcherServlet
- DisptcherServlet将结果传给ViewResolver视图解析器
- ViewResolver解析出具体的view返回给DisptcherServlet
- DisptcherServlet通过渲染或view组件将结果展示到页面
说明:
1. 处理器适配器就是通过请求的地址找到具体的controller中间的过程;
可以分为两种:
a. 通过配置的方式(非注解处理器);
b. 注解类处理器(@Controller、@Service、@Mapper等)
2. ModelAndView:包含处理完的数据和需要访问view层的url地址,可以看出它的作用是储存处理结果并且设置地址最后将数据传给view
Tomcat 优化
Tomcat内存优化
在linux修改TOMCAT_HOME/bin/catalina.sh,在前面加入
JAVA_OPTS="-XX:PermSize=64M -XX:MaxPermSize=128m -Xms512m -Xmx1024m
-XX:Permsize:内存永久保存区域
-XX:MaxPermSize:最大内存的永久保护区
-Xms:使用的最小内存
-Xmx:使用的最大内存
Tomcat线程优化
在TOMCAT_HOME/conf/server.xml找到<Connector...>
元素,设置一下
<Connector port="9090" protocol="HTTP/1.1" maxThreads="600" minSpareThreads="100" maxSpareThreads="500" acceptCount="700"
connectionTimeout="20000" redirectPort="8443" />
maxThreads:最大线程数
minSpareThreads:初始化时创建的线程数
maxSpareThreads:一旦创建的线程超过这个值,Tomcat就会关闭不再需要的socket线程。
acceptCount:指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理
禁用DNS查询
DNS会记录客户端的IP地址或者通过域名服务器查找机器名 转换为IP地址
DNS查询需要占用网络,修改server.xml文件中的Connector元素,修改属性enableLookups参数值: enableLookups=“false”
设置session过期时间
在TOMCAT_HOME/conf/web.xml
默认是30min,改成180就行了,单位分钟
数据库相关面试题
SQL执行流程
oracle和mysql区别
- Oracle是大型数据库,而MySQL是中小型数据库。MySQL是开源的,Oracle收费
- MySQL支持主键自增,Oracle主键一般使用序列实现自增
- MySQL分页用limit关键字,而Oracle使用rownum
- MySQL中默认commit,但是Oracle需要手动提交
- MySQL字符串可以使用双引号包起来,而Oracle只可以单引号
- mysql对于事务默认是不支持的,只是有某些存储引擎中如:innodb可以支持;而Oracle对于事物是完全支持的。
- Mysql以表锁为主,Oracle使用行级锁
- mysql是可重复读,oracle是读写提交
数据库三大范式
- 第一范式:所有字段是不可拆分的,比如说我有一个树的表,但是系统中有很多地方访问了树叶、树枝、树干,那么表定义的时候就应该定义成树叶、树枝、树干,不能只定义树,因为是可以拆分的
- 第二范式:在第一范式的基础上,每一列都与主键相关,不能只是一部分;一个表只能保存一种数据不能保存其他数据;联合主键使用
- 第三范式:确保每一列数据都与主键有直接关系,不能是间接关系;主表中不能有其他表的数据
mysql常用搜索引擎
- MYISAM:不支持事务,不支持外键,有很好的插入和查询速度;使用其创建数据库会生成三个文件:
a. 存储表定义的frm文件
b. 存储数据文件的MYData(.MYD)
c. 存储索引文件的MYIndex(.MYI) - INNODB:支持事务、支持行锁和外键、支持自动增长列、默认的MYSQL引擎,与MYISAM相比系的效率低一些
- MEMORY:将表中的数据存储到内存中,可以快速查询和访问其他表数据
总结:如果要提供事务安全能力并实现并发控制可以使用INNODB
如果主要用来插入和查询使用MYISAM;如果用来临时存储数据数据量少不需要较高的安全性可以使用MEMORY
数据库优化
- 优化sql语句
- 不使用select *
- 使用between and 代替in
- 使用join代替子查询,速度更快
- 用in代替or
- where后避免使用表达式和函数操作
- 分页时使用limit
- 一条复杂的查询可以拆分成多条进行查询
- 多次查询,查询20w条数据可以查询四次每次5w
- 使用exists:外表数据少则使用exists,内表数据少则使用in
- 尽可能使用not null,不要对字段进行null判断,会导致引擎放弃使用索引而进行全表扫描
- 避免where子句使用 != 和<> - 使用union代替临时表,union是查询不包括重复行,union all 查询所有数据,包括重复的
- 使用索引
- 选择合适的字段属性,varchar代替char等,节省空间
- 使用外键
- 存储引擎选择
按照锁的粒度分数据库锁有哪些?
在关系型数据库中,可以按照锁的粒度把数据库锁分为行级锁(INNODB引擎)、表级锁(MYISAM引擎)和页级锁(BDB引擎 )。
MyISAM和InnoDB存储引擎使用的锁:
- MyISAM采用表级锁(table-level locking)。
- InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁
行级锁,表级锁和页级锁对比
主要分为:
- 行级锁 行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁 和 排他锁。
特点:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。 - 表级锁 表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。
特点:开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。 - 页级锁 页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。
特点:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般
从锁的类别上分MySQL都有哪些锁呢?
从锁的类别上来讲,有共享锁和排他锁。
- 共享锁: 又叫做读锁。 当用户要进行数据的读取时,对数据加上共享锁。共享锁可以同时加上多个。
- 排他锁: 又叫做写锁。 当用户要进行数据的写入时,对数据加上排他锁。排他锁只可以加一个,他和其他的排他锁,共享锁都相斥。
用上面的例子来说就是用户的行为有两种,一种是来看房,多个用户一起看房是可以接受的。 一种是真正的入住一晚,在这期间,无论是想入住的还是想看房的都不可以。
锁的粒度取决于具体的存储引擎,InnoDB实现了行级锁,页级锁,表级锁。
他们的加锁开销从大到小,并发能力也是从大到小。
什么是死锁?怎么解决?
死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。
常见的解决死锁的方法
1、如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。
2、在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
3、对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;
数据库的乐观锁和悲观锁是什么?怎么实现的?
数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。
- 悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制
- 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。实现方式:乐一般会使用版本号机制或CAS算法实现。
两种锁的使用场景
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
SQL 约束
- 主键约束(PRIMARY KEY):唯一性,非空
- 外键约束(FOREIGN KEY):建立两表之间的关系,引用主表的一列
- 唯一约束(UNIQUE):唯一性,可以为空,一个表中可以有多个唯一约束
- 非空约束(NOT NULL):不能为空
- 检查约束(CHECK):控制字段的值范围
关联查询
- 内连:INNER JOIN
- 外连:LEFT JOIN / RIGHT JOIN
- 交叉连接:CROSS JOIN
- 联合查询:UNION / UNION ALL
- 全连接:FULL JOIN
mysql中 in 和 exists 区别
mysql 中的in是把内表进行循环,然后每次都对外表进行查询;exists是 对外表进行循环,每次循环对内表进行查询,使用的时候都差不多,需要区分环境:
- 如果两个表大小差不多,那么使用哪个都是一样的
- 如果内表小那么就用in,外表比内表小,就用exists
- 当使用not in 和not exists 时候,not in 回放弃索引,进行全文搜索;但是not exists不会放弃索引,所以无论哪个表大,not exists 都比not in 快