### JVM
#### 为什么会出现脏读?
`JAVA`内存模型简称`JMM`
`JMM`规定所有的变量存在在主内存,每个线程有自己的工作内存,线程对变量的操作都在工作内存中进行,不能直接对主内存就行操作
使用`volatile`修饰变量
每次读取前必须从主内存属性最新的值
每次写入需要立刻写到主内存中
`volatile`关键字修修饰的变量随时看到的自己的最新值,假如线程`1`对变量`v`进行修改,那么线程`2`是可以马上看见

#### 你说`volatile`可以避免指令重排,能否解释下什么是指令重排?
指令重排序分两类 编译器重排序和运行时重排序
`JVM`在编译`java`代码或者`CPU`执行`JVM`字节码时,对现有的指令进行重新排序,主要目的是优化运行效率(不改变程序结果的前提)
```java
int a = 3 //1
int b = 4 //2
int c =5 //3
int h = a*b*c //4
```
定义顺序 `1,2,3,4`
计算顺序 `1,3,2,4` 和 `2,1,3,4` 结果都是一样
虽然指令重排序可以提高执行效率,但是多线程上可能会影响结果,有什么解决办法?
解决办法:内存屏障
解释:内存屏障是屏障指令,使`CPU`对屏障指令之前和之后的内存操作执行结果的一种约束
#### 知道`happens-before`吗,能否简单解释下?
先行发生原则,`volatile`的内存可见性就提现了该原则之一
例子:
```java
//线程A操作
int k = 1;
//线程B操作
int j = k;
//线程C操作
int k = 2
```
分析:
假设线程`A`中的操作`k=1`先行发生于线程`B`的操作`j=k`,那确定在线程`B`的操作执行后,变量`j`的值一定等于`1`,依据有两个:一是先行发生原则,`k=1`的结果可以被观察到;二是第三者线程`C`还没出现,线程`A`操作结束之后没有其他线程会修改变量`k`的值。
但是考虑线程`C`出现了,保持线程`A`和线程`B`之间的先行发生关系,线程`C`出现在线程`A`和线程`B`的操作之间,但是线程`C`与线程`B`没有先行发生关系,那`j`的值会是多少?答案是`1`和`2`都有可能,因为线程`C`对变量`k`的影响可能会被线程`B`观察到,也可能不会,所以线程`B`就存在读取到不符合预期数据的风险,不具备多线程安全性
八大原则(对这个不理解,一定要去补充相关博文知识)
* 程序次序规则
* 管程锁定规则
* `volatile`变量规则
* 线程启动规则
* 线程中断规则
* 线程终止规则
* 对象终结规则
* 传递性
### 堆内存中的数据是线程共享的吗?
首先了解下,JVM分配对象内存的过程:
线程中给对象分配内存,主要是对象的引用指向这个内存区域,然后进行初始化操作,可能有多个线程在堆上申请空间,对象的内存分配过程就必须进行同步控制。但是我们都知道,无论是使用哪种同步方案(实际上虚拟机使用的可能是CAS),都会影响内存的分配效率。
而Java对象的分配是Java中的高频操作,所有,人们想到另外一个办法来提升效率。这里我们重点说一个HotSpot虚拟机的方案:
```
每个线程在Java堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这块”私有”内存中分配,当这部分区域用完之后,再分配新的”私有”内存。
```
这种方案被称之为TLAB分配,即Thread Local Allocation Buffer。这部分Buffer是从堆中划分出来的,但是是本地线程独享的。
这里值得注意的是,我们说TLAB是线程独享的,但是只是在“分配”这个动作上是线程独占的,至于在读取、垃圾回收等动作上都是线程共享的。而且在使用上也没有什么区别。

所以,“堆是线程共享的内存区域”这句话并不完全正确,因为TLAB是堆内存的一部分,他在读取上确实是线程共享的,但是在内存分分配上,是线程独享的。
TLAB的空间其实并不大,所以大对象还是可能需要在堆内存中直接分配。那么,对象的内存分配步骤就是先尝试TLAB分配,空间不足之后,再判断是否应该直接进入老年代,然后再确定是再eden分配还是在老年代分配。

### 中间件消息队列
#### 你用过消息队列,引入队列有啥优缺点,对比其他消息中间产品,选择这款的原因是啥?
* 优点:解耦系统、异步化、削峰
* 缺点: 系统可用性降低、复杂度增高、维护成本增高
* 主流消息队列`Apache ActiveMQ`、`Kafka`、`RabbitMQ`、`RocketMQ`
* `ActiveMQ`:http://activemq.apache.org/
* `Apache`出品,历史悠久,支持多种语言的客户端和协议,支持多种语言`Java`, `.NET`,` C++` 等,基于`JMS Provider`的实现
* 缺点:吞吐量不高,多队列的时候性能下降,存在消息丢失的情况,比较少大规模使用
* `Kafka`:http://kafka.apache.org/
* 是由`Apache`软件基金会开发的一个开源流处理平台,由`Scala`和`Java`编写。`Kafka`是一种高吞吐量的分布式发布订阅消息系统,它可以处理大规模的网站中的所有动作流数据(网页浏览,搜索和其他用户的行动),副本集机制,实现数据冗余,保障数据尽量不丢失;支持多个生产者和消费者
* 缺点:不支持批量和广播消息,运维难度大,文档比较少, 需要掌握`Scala`
* `RabbitMQ`:http://www.rabbitmq.com/
* 是一个开源的`AMQP`实现,服务器端用`Erlang`语言编写,支持多种客户端,如:`Python`、`Ruby`、`.NET`、`Java`、`JMS`、`C`、用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不错
* 缺点:使用`Erlang`开发,阅读和修改源码难度大
* `RocketMQ`:http://rocketmq.apache.org/
* 阿里开源的一款的消息中间件, 纯`Java`开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点, 性能强劲(零拷贝技术),支持海量堆积, 支持指定次数和时间间隔的失败消息重发,支持`consumer`端`tag`过滤、延迟消息等,在阿里内部进行大规模使用,适合在电商,互联网金融等领域使用
* 缺点:成熟的资料相对不多,社区处于新生状态但是热度高
#### 消息队列的发送方式有哪几种,使用场景分别是怎样的?
发送方式一般分三种
* `SYNC` 同步发送
应用场景:重要通知邮件、报名短信通知、营销短信系统等
* `ASYNC` 异步发送
应用场景:对`RT`时间敏感,可以支持更高的并发,回调成功触发相对应的业务,比如注册成功后通知积分系统发放优惠券
* `ONEWAY` 无需要等待响应
应用场景:主要是日志收集,适用于某些耗时非常短,但对可靠性要求并不高的场景, 也就是`LogServer`, 只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求 不等待应答
发送方式汇总对比
|发送方式| 发送TPS| 发送结果反馈| 可靠性|
|----|----|----|----|
|同步发送| 快| 有|不丢失|
|异步发送| 快| 有| 不丢失|
|单向发送| 最快| 无| 可能丢失|
#### 有没用过延迟消息,使用场景是怎样的?
什么是延迟消息:`Producer` 将消息发送到消息队列` broker`服务端,但并不期望这条消息立马投递,而是推迟到在当前时间点之后的某一个时间投递到 `Consumer` 进行消费
使用场景一:通过消息触发一些定时任务,比如在某一固定时间点向用户发送提醒消息
使用场景二:消息生产和消费有时间窗口要求,比如在天猫电商交易中超时未支付关闭订单的场景,在订单创建时会发送一条 延时消息。这条消息将会在 `30` 分钟以后投递给消费者,消费者收到此消息后需要判断对应的订单是否已完成支付。 如支付未完成,则关闭订单。如已完成支付则忽略

#### 如何保证消息队列里消息的生成和消费的顺序性
##### 你用的队列是否支持顺序消息,是怎么实现顺序消息的?
什么是顺序消息:
消息的生产和消费顺序一致
全局顺序:`topic`下面全部消息都要有序(少用),性能要求不高,所有的消息严格按照`FIFO` 原则进行消息发布和消费的 场景,并行度成为消息系统的瓶颈, 吞吐量不够
使用场景:在证券处理中,以人民币兑换美元为例子,在价格相同的情况下,先出价者优先处理,则可以通过全局顺序的方式按照 `FIFO` 的方式进行发布和消费
局部顺序:只要保证一组消息被顺序消费即可,性能要求高
使用场景:电商的订单创建,同一个订单相关的创建订单消息、订单支付消息、订单退款消息、订单物流消息、订单交易成功消息 都会按照先后顺序来发布和消费
(阿里巴巴集团内部电商系统均使用局部顺序消息,既保证业务的顺序,同时又能保证业务的高性能)
下面是用`RocketMQ`举例(用`kafka`或`rabbitmq`类似)
一个`topic`下面有多个`queue`
* 顺序发布
对于指定的一个 `Topic`,客户端将按照一定的先后顺序发送消息
举例:订单的顺序流程是:创建、付款、物流、完成,订单号相同的消息会被先后发送到同一个队列中,
根据`MessageQueueSelector`里面自定义策略,根据同个业务`id`放置到同个`queue`里面,如订单号取模运算再放到`selector`中,同一个模的值都会投递到同一条`queue`
```java
public MessageQueue select(List mqs, Message msg, Object arg) {
//如果是订单号是字符串,则进行hash,得到一个hash值
Long id = (Long) arg;
long index = id % mqs.size();
return mqs.get((int)index);
}
```
* 顺序消费
对于指定的一个 `Topic`,按照一定的先后顺序接收消息,即先发送的消息一定会先被客户端接收到。
举例:消费端要在保证消费同个`topic`里的同个队列,不应该用`MessageListenerConcurrently`,
应该使用`MessageListenerOrderly`,自带单线程消费消息,不能再`Consumer`端再使用多线程去消费,消费端分配到的`queue`数量是固定的,集群消费会锁住当前正在消费的队列集合的消息,所以会保证顺序消费。
注意:
顺序消息暂不支持广播模式
顺序消息不支持异步发送方式,否则将无法严格保证顺序
不能再`Consumer`端再使用多线程去消费

#### 你的业务系统有没做消息的重复消费处理,是怎么做的?
* 幂等性:一个请求,不管重复来多少次,结果是不会改变的。
* `RabbitMQ`、`RocketMQ`、`Kafka`等任何队列不保证消息不重复,如果业务需要消息不重复消费,则需要消费端处理业务消息要保持幂等性
* 方式一:`Redis`的`setNX()` , 做消息`id`去重 `java`版本目前不支持设置过期时间
```java
//Redis中操作,判断是否已经操作过 TODO
boolean flag = jedis.setNX(key);
if(flag){
//消费
}else{
//忽略,重复消费
}
```
* 方式二:`redis`的 `Incr` 原子操作:`key`自增,大于`0` 返回值大于`0`则说明消费过,(`key`可以是消息的`md5`取值, 或者如果消息`id`设计合理直接用`id`做`key`)
```java
int num = jedis.incr(key);
if(num == 1){
//消费
}else{
//忽略,重复消费
}
```
* 方式三:数据库去重表
* 设计一个去重表,某个字段使用`Message`的`key`做唯一索引,因为存在唯一索引,所以重复消费会失败
```sql
CREATE TABLE message_record ( id int(11) unsigned NOT NULL AUTO_INCREMENT, key varchar(128) DEFAULT NULL, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY key (key) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

#### 消息队列常见问题之如何保证消费的可靠性传输?
消息可靠性传输,是非常重要,消息如果丢失,可能带来严重后果,一般从是个角度去分析
* `producer`端
不采用`oneway`发送,使用同步或者异步方式发送,做好重试,但是重试的`Message key`必须唯一
投递的日志需要保存,关键字段,投递时间、投递状态、重试次数、请求体、响应体
* `broker`端
多主多从架构,需要多机房
同步双写、异步刷盘 (同步刷盘则可靠性更高,但是性能差点,根据业务选择)
机器断电重启:异步刷盘,消息丢失;同步刷盘消息不丢失
硬件故障:可能存在丢失,看队列架构
* `consumer`端
消息队列一般都提供的`ack`机制,发送者为了保证消息肯定消费成功,只有消费者明确表示消费成功,队列才会认为消息消费成功,中途断电、抛出异常等都不会认为成功——即都会重新投递,每次在确保处理完这个消息之后,在代码里调用`ack`,告诉消息队列消费成功
消费端务必做好幂等性处理
消息消费务必保留日志,即消息的元数据和消息体,

#### 消息队列常见问题之消息发生大量堆积应该怎么处理?
* 消息堆积了`10`小时,有几千万条消息待处理,现在怎么办?
* 修复`consumer`, 然后慢慢消费?也需要几小时才可以消费完成,新的消息怎么办?
核心思想:紧急临时扩容,更快的速度去消费数据
- 修复`Consumer`不消费问题,使其恢复正常消费,根据业务需要看是否要暂停
- 临时`topic`队列扩容,并提高消费者能力,但是如果增加`Consumer`数量,但是堆积的`topic`里面的`message queue`数量固定,过多的`consumer`不能分配到`message queue`
- 编写临时处理分发程序,从旧`topic`快速读取到临时新`topic`中,新`topic`的`queue`数量扩容多倍,然后再启动更多`consumer`进行在临时新的`topic`里消费
- 直到堆积的消息处理完成,再还原到正常的机器数量



### MySQL数据库的面试题你遇过多少
#### 常说的事务ACID是什么
##### 你知道Mysql事务的四大特性不,简单说下?
事务的四大特性ACID
* 原子性Atomicity:
一个事务必须被事务不可分割的最小工作单元,整个操作要么全部成功,要么全部失败,一般就是通过commit和rollback来控制
* 一致性Consistency:
数据库总能从一个一致性的状态转换到另一个一致性的状态,比如小滴课堂下单支付成功后,开通视频播放权限,只要有任何一方发生异常就不会成功提交事务
* 隔离性Isolation:
一个事务相对于另一个事务是隔离的,一个事务所做的修改是在最终提交以前,对其他事务是不可见的
* 持久性Durability:
一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失
#### 脏读-不可重复读-幻读你知道多少
##### 能否简单解释下脏读、不可重复读、幻读的意思?
* 脏读
事务中的修改即使没有提交,其他事务也能看见,事务可以读到未提交的数据称为脏读
* 不可重复读
同个事务前后多次读取,不能读到相同的数据内容,中间另一个事务也操作了该同一数据
* 幻读
当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,发现两次不一样,产生幻读
幻读和不可重复读的区别是:前者是一个范围,后者是本身,从总的结果来看, 两者都表现为两次读取的结果不一致

##### 常见的隔离级别由低到高有哪几种,mysql默认是哪种?
事务的隔离级别越高,事务越安全,但是并发能力越差。
* Read Uncommitted(未提交读,读取未提交内容)
事务中的修改即使没有提交,其他事务也能看见,事务可以读到为提交的数据称为脏读
也存在不可重复读、幻读问题
例子:
小滴课堂运营小姐姐配置了一个课程活动,原价500元的课程,配置成50元,但是事务没提交。
你刚好看到这个课程那么便宜准备购买,但是Anna小姐姐马上回滚了事务,重新配置并提交了事务,你准备下单的时候发现价格变回了500元
* Read Committed(提交读,读取提交内容)
一个事务开始后只能看见已经提交的事务所做的修改,在事务中执行两次同样的查询可能得到不一样的结果,也叫做不可重复读(前后多次读取,不能读到相同的数据内容),也存幻读问题
例子:
老王在小滴课堂有1000积分,准备去兑换《面试专题课程》,查询数据库确实有1000积分
但是老王的女友同时也在别的地方登录,把1000积分兑换了《SpringCloud微服务专题课程》,且在老王之前提交事务;当系统帮老王兑换《面试专题课程》是发现积分预计没了,兑换失败。
老王事务A事先读取了数据,他女友事务B紧接了更新了数据且提交了事务,事务A再次读取该数据时,数据已经发生了改变
* Repeatable Read(可重复读,mysql默认的事务隔离级别)
解决脏读、不可重复读的问题,存在幻读的问题,使用 MMVC机制 实现可重复读
例子
老王在小滴课堂有1000积分,准备去兑换《面试专题课程》,查询数据库确实有1000积分
老王的女友同时也在别的地方登录先兑换了这个《面试专题课程》,老王的事务提交的时候发现存在了,之前读取的没用了,像是幻觉
幻读问题:MySQL的InnoDB引擎通过MVCC自动帮我们解决,即多版本并发控制
* Serializable(可串行化)
解决脏读、不可重复读、幻读,可保证事务安全,但强制所有事务串行执行,所以并发效率低
#### Mysql常见的存储引擎你知道不
##### 说下你知道Mysql常见的存储引擎,新版Mysql默认是哪个?
常见的有多类,InnoDB、MyISAM、MEMORY、MERGE、ARCHIVE、CSV等
一般比较常用的有InnoDB、MyISAM
MySQL 5.5以上的版本默认是InnoDB,5.5之前默认存储引擎是MyISAM
#### 存储引擎InnoDB、MyISAM异同点和选择
##### mysql的存储引擎 innodb和myisam有什么区别,应该怎么选择?
|区别项| Innodb| myisam|
|---|---|---|
|事务| 支持| 不支持|
|锁粒度| 行锁,适合高并发| 表锁,不适合高并发|
|是否默认| 默认| 非默认|
|支持外键| 支持外键| 不支持|
|适合场景| 读写均衡,写大于读场景,需要事务 |读多写少场景,不需要事务|
|全文索引| MySQL5.6之前不支持,可以通过插件实现, 更多使用ElasticSearch |支持全文索引|
#### Mysql数据库索引你知道多少
##### mysql常用的功能索引有哪些?分别在什么场景下使用?创建语句是怎样的?
|索引名称|特点| 创建语句|
| ---- | ---- | ---- |
|普通索引| 最基本的索引,仅加速查询 | CREATE INDEX idx_name ON table_name(filed_name)|
|唯一索引| 加速查询,列值唯一,允许为空;组合索引则列值的组合必须唯一| CREATE UNIQUE INDEX idx_name ON table_name(filed_name_1,filed_name_2)|
|主键索引| 加速查询,列值唯一,一个表只有1个,不允许有空值| ALTER TABLE table_name ADD PRIMARY KEY ( filed_name )|
|组合索引| 加速查询,多条件组合查询| CREATE INDEX idx_name ON table_name(filed_name_1,filed_name_2);|
|覆盖索引| 索引包含所需要的值,不需要“回表”查询,比如查询,两个字段,刚好是 组合索引 的两个字段||
|全文索引| 对内容进行分词搜索,仅可用于Myisam, 更多用ElasticSearch做搜索 | ALTER TABLE table_name ADD FULLTEXT ( filed_name )|
#### 数据库索引的好处和坏处,你常用的最佳实践
##### 你们线上数据量每天有多少新增,都是存储在mysql库吗,有没做优化?
中型公司或者业务发展好的公司,一天新增几百万数据量
业务核心数据存储在Mysql里面,针对业务创建合适的索引
打点数据、日志等存储在ElasticSearch或者MongoDB里面
##### 你创建索引的时候主要考虑啥,使用索引的优缺点有哪些,使用应该注意些什么?
考虑点:结合实际的业务场景,在哪些字段上创建索引,创建什么类型的索引
索引好处:
快速定位到表的位置,减少服务器扫描的数据
有些索引存储了实际的值,特定情况下只要使用索引就能完成查询
索引缺点:
索引会浪费磁盘空间,不要创建非必要的索引
插入、更新、删除需要维护索引,带来额外的开销
索引过多,修改表的时候重构索引性能差
* 索引优化实践
* 前缀索引,特别是`TEXT`和`BLOG`类型的字段,只检索前面几个字符,提高检索速度
* 尽量使用数据量少的索引,索引值过长查询速度会受到影响
* 选择合适的索引列顺序
* 内容变动少,且查询频繁,可以建立多几个索引
* 内容变动频繁,谨慎创建索引
* 根据业务创建适合的索引类型,比如某个字段常用来做查询条件,则为这个字段建立索引提高查询速度
* 组合索引选择业务查询最相关的字段
### 数据库设计查询和上线里面的坑你走过多少
#### 数据库查询关键词执行顺序
##### 数据库查询的指令有多个,说下执行顺序 select、where、from、group by、having、order by?
from 从哪个表查询
where 初步过滤条件
group by 过滤后进行分组[重点]
having 对分组后的数据进行二次过滤[重点]
select 查看哪些结果字段
order by 按照怎样的顺序进行排序返回[重点]
select video_id,count(id) num from chapter group by video_id having num >10
order by video_id desc
官方地址:https://www.percona.com/downloads/percona-toolkit/LATEST/
其他资料:https://www.cnblogs.com/zishengY/p/6852280.html
#### 设计数据库表时相似字段类型你能区分吗《上》
##### varchar(len) char(len) len存储的是字符还是字节?MySQL中的varchar和char有什么区别,应该怎么选择?
|对比项 |char(16)|varchar(16)|
|---|---|---|
|长度特点| 长度固定,存储字符| 长度可变,存储字符|
|长度不足情况| 插入的长度小于定义长度时,则用空格填充| 小于定义长度时,按实际插入长度存储|
|性能| 存取速度比varchar快得多| 存取速度比char慢得多
|使用场景| 适合存储很短的,固定长度的字符串,如手机号,MD5值等| 适合用在长度不固定场景,如收货地址,邮箱地址等|
#### 2038年1月19号会有多少系统产生bug,相似字段类型区分《下》
##### MySQL中的datetime和timestamp有什么区别?
* 存储空间
|类型| 占据字节| 范围| 时区问题|
|---|---|---|---|
|datetime |8 字节 |1000-01-01 00:00:00到 9999-12-31 23:59:59 |存储与时区无关,不会发生改变 |
|timestamp |4 字节 |1970-01-01 00:00:01 到 2038-01-19 11:14:07 |存储的是与时区有关,随数据库的时区而发生改变|
* 时间范围
可表示的时间范围不同。timestamp可表示范围:1970-01-01 00:00:00~2038-01-09 03:14:07,datetime支持的范围更宽1000-01-01 00:00:00 ~ 9999-12-31 23:59:59
* 索引速度
索引速度不同。timestamp更轻量,索引相对datetime更快。
* 跨库问题
不同的数据库对时间类型有不同的解释,如Oracle中的date和mysql中的date就不能直接兼容转换为实现跨平台性,将时间记录为unix时间戳
##### 为什么timestamp只能到2038年?
```
MySQL的timestamp类型是4个字节,最大值是2的31次方减1,结果是2147483647,
转换成北京时间就是2038-01-19 11:14:07
```
#### 场景模拟之千万级Mysql数据表分页查询优化
##### 线上数据库的一个商品表数据量过千万,做深度分页的时候性能很慢,有什么优化思路?
现象:千万级别数据很正常,比如数据流水、日志记录等,数据库正常的深度分页会很慢
慢的原因:
```sql
select * from product limit N,M
```
MySQL执行此类SQL时需要先扫描到N行,然后再去取M行,N越大,MySQL扫描的记录数越多,SQL的性能就会越差
* 后端、前端缓存
* 使用ElasticSearch分页搜索
* 合理使用 mysql 查询缓存,覆盖索引进行查询分页
```sql
select title,cateory from product limit 1000000,100
```
* 如果id是自增且不存在中间删除数据,使用子查询优化,定位偏移位置的 id
```sql
select * from oper_log where type='BUY' limit 1000000,100; //5.秒
select id from oper_log where type='BUY' limit 1000000,1; // 0.4秒
select * from oper_log where type='BUY' and id>=(select id from oper_log where type='BUY' limit 1000000,1) limit 100; //0.8秒
```
#### BAT大厂里面 应用版本更新,数据库上线流程
##### 你公司里面产品迭代更新,开发好代码和数据库,上线流程是怎样的?

### 生产环境数据库性能监控和优化面试环节
#### 生产环境的数据库,你会做哪些操作保证安全
##### 针对线上的数据库,你会做哪些监控,业务性能 + 数据安全 角度分析?
大厂一般都有数据库监控后台,里面指标很多,但是开发人员也必须知道
* 业务性能
* 应用上线前会审查业务新增的sql,和分析sql执行计划
比如是否存在 select * ,索引建立是否合理
* 开启慢查询日志,定期分析慢查询日志
* 监控CPU/内存利用率,读写、网关IO、流量带宽 随着时间的变化统计图
* 吞吐量QPS/TPS,一天内读写随着时间的变化统计图
* 数据安全
* 短期增量备份,比如一周一次。 定期全量备份,比如一月一次
* 检查是否有非授权用户,是否存在弱口令,网络防火墙检查
* 导出数据是否进行脱敏,防止数据泄露或者黑产利用
* 数据库 全量操作日志审计,防止数据泄露
* 数据库账号密码 业务独立,权限独立控制,防止多库共用同个账号密码
* 高可用 主从架构,多机房部署
#### 你知道Mysql里面有多少种日志
##### Mysql有多少种常见的日志,分别解释日志的作用?
* redo 重做日志
作用:确保事务的持久性,防止在发生故障,脏页未写入磁盘。重启数据库会进行redo log执行重做,到达事务一致性
* undo 回滚日志
作用:保证数据的原子性,记录事务发生之前的数据的一个版本,用于回滚。
innodb事务的可重复读和读取已提交 隔离级别就是通过mvcc+undo实现
* errorlog 错误日志
作用:Mysql本身启动、停止、运行期间发生的错误信息
* slow query log 慢查询日志
作用:记录执行时间过长的sql,时间阈值可以配置,只记录执行成功
* binlog 二进制日志
作用:用于主从复制,实现主从同步
* relay log 中继日志
作用:用于数据库主从同步,将主库发送来的binlog先保存在本地,然后从库进行回放
* general log 普通日志
作用:记录数据库操作明细,默认关闭,开启会降低数据库性能
#### 层层套路之数据库主从复制里面知识考查
##### 你们数据库是单点的吗?有没做多节点优化 ,怎么做的?
```
我们公司数据库不是单节点,是多节点的,有做主从复制
```
##### 既然搭建过数据库主从复制,你能画下流程图说下异步复制原理不?

#### 层层套路之数据库主从同步遇到的问题
##### 你们搭建数据库主从复制的目的有哪些?
* 容灾使用,用于故障切换
* 业务需要,进行读写分离减少主库压力
##### 既然你们搭建了主从同步,且你们日增量数据量也不少,有没遇到同步延迟问题?为什么会有同步延迟问题,怎么解决?
保证性能第一情况下,不能百分百解决主从同步延迟问题,只能增加缓解措施。
现象:主从同步,大数据量场景下,会发现写入主库的数据,在从库没找到。
* 原因
* 主从复制是单线程操作,当主库TPS高,产生的超过从库sql线程执行能力
* 从库执行了大的sql操作,阻塞等待
* 服务器硬件问题,如磁盘,CPU,还有网络延迟等
* 解决办法
* 业务需要有一定的容忍度,程序和数据库直接增加缓存,降低读压力
* 业务适合的话,写入主库后,再写缓存,读的时候可以读缓存,没命中再读从库
* 读写分离,一主多从,分散主库和从库压力
* 提高硬件配置,比如使用SSD固态硬盘、更好的CPU和网络
* 进行分库分表,减少单机压力

#### Mysql主从复制数据一致性校验方案怎么做
##### 什么场景下会出现主从数据不一致?
1、本身复制延迟导致
2、主库宕机或者从库宕机都会导致复制中断
3、把一个从库提升为主库,可能导致从库和主库的数据不一致性
4、主库执行更改前有执行set sql_log_bin=0,会使主库不记录binlog,从库也无法变更这部分数据
5、主从实例版本不一致,特别是高版本是主,低版本为从的情况下,主数据库上面支持的功能,从数据库上面可能不支持该功能
6、从节点未设置只读,误操作写入数据
##### 是否有做过主从一致性校验,你是怎么做的,如果没做过,你计划怎么做?如果不一致你会怎么修复?
Mysql主从复制是基于binlog复制,难免出现复制数据不一致的风险,引起用户数据访问前后不一致的风险
所以要定期开展主从复制数据一致性的校验并修复,避免这些问题
解决方案之一,使用`Percona`公司下的工具
* pt-table-checksum工具进行一致性校验
* 原理
主库利用表中的索引,将表的数据切割成一个个`chunk`(块),然后进行计算得到checksum值。
从库也执相应的操作,并在从库上计算相同数据块的checksum,然后对比主从中各个表的checksum是否一致并存储到数据库,最后通过存储校验结果的表就可以判断出哪些表的数据不一致
* pt-table-sync(在从库执行)工具进行修复不一致数据,可以修复主从结构数据的不一致,也可以修复非主从结构数据表的数据不一致
* 原理
在主库上执行数据的更改,再同步到从库上,不会直接更改成从的数据。在主库上执行更改是基于主库现在的数据,也不会更改主库上的数据,可以同步某些表或整个库的数据,但它不同步表结构、索引,只同步不一致的数据
注意:
默认主库要检查的表在从库都存在,并且同主库表有相同的表结构
如果表中没有索引,pt-table-checksum将没法处理,一般要求最基本都要有主键索引
pt-table-sync工具会修改数据,使用前最好备份下数据,防止误操作
##### pt-table-checksum怎么保证某个chunk的时候checksum数据一致性?
当pt工具在计算主库上某chunk的checksum时,主库可能在更新且从库可能复制延迟,那该怎么保证主库与从库计算的是”同一份”数据,答案把要checksum的行加上for update锁并计算,这保证了主库的某个chunk内部数据的一致性
官方地址:https://www.percona.com/downloads/percona-toolkit/LATEST/
其他资料:https://www.cnblogs.com/zishengY/p/6852280.html
##### 如何防止主从数据不一致?
* 主库binlog采用ROW格式。
* 主从实例数据库版本保持一致。
* 主库做好账号权限把控,不可以执行set sql_log_bin=0。
* 从库开启只读,不允许人为写入。
* 定期进行主从一致性检验。
#### mysql -load data csv.file超大数据量迁移?
Load的处理机制是:在执行load之前,会关掉索引,当load全部执行完成后,再重新创建索引。
Insert的处理机制是:每插入一条则更新一次数据库,更新一次索引。
### redis和memcache区别?
* 数据存储位置
* `memecache`把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。
* redis有部份存在硬盘上,这样能保证数据的持久性,支持数据的持久化(笔者注:有快照和AOF日志两种持久化方式,在实际应用的时候,要特别注意配置文件快照参数,要不就很有可能服务器频繁满载做dump)。
* 数据支持类型
* redis在数据支持上要比memecache多的多(memcache简单的key-value结构的数据)。
* 数据持久化
* redis支持,memcache不支持。
* 集群管理的不同
* Memcached本身并不支持分布式,因此只能在客户端通过像一致性哈希这样的分布式算法来实现Memcached的分布式存储。
* Redis已经支持了分布式存储功能。`Redis Cluster`,哨兵`sentinel`,分布式锁`redlock`。
* 生态环境
### SpringIOC创建对象的三种方式?
* 默认构造方法创建
* 需要创建的对象
```java
public class HelloIoc {
public void sayHello(){
System.out.println("Hello Spring IOC");
}
}
```
* xml配置
```xml
```
* test
```java
@Test
public void TestHelloIoc(){
//从spring容器获得 //1 获得容器
String xmlPath="bean.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath);
//2获得内容 --不需要自己new,都是从spring容器获得
HelloIoc helloIoc = (HelloIoc) applicationContext.getBean("helloIoc");
helloIoc.sayHello();
//利用配置文件 alias 别名属性创建对象
HelloIoc helloIoc2 = (HelloIoc) applicationContext.getBean("helloIoc2");
helloIoc2.sayHello();
}
```
* 静态工厂方式
* 实例化对象
```java
public class HelloStaticFactory {
public HelloStaticFactory(){
System.out.println("HelloStaticFactory constructor");
}
//静态工厂方法
public static HelloIoc getInstances(){
return new HelloIoc();
}
}
```
* xml配置
```xml
```
* test
```java
@Test
public void TestHelloIoc2(){
//从spring容器获得 //1 获得容器
String xmlPath="bean.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath);
//2获得内容 --不需要自己new,都是从spring容器获得
HelloIoc helloFactoryIoc = (HelloIoc) applicationContext.getBean("helloStaticFactory");
helloFactoryIoc.sayHello();
}
```
* 实例工厂方式
* 实例化对象
```java
public class HelloInstanceFactory {
public HelloInstanceFactory(){
System.out.println("实例工厂方法构造函数");
}
//利用实例工厂方法创建对象
public HelloIoc getInstance(){
HelloIoc instanceIoc = new HelloIoc();
return instanceIoc;
}
}
```
* bean xml
```xml
```
* test
```java
@Test
public void TestHelloIoc3(){
//从spring容器获得 //1 获得容器
String xmlPath="bean.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath);
//2获得内容 --不需要自己new,都是从spring容器获得
HelloIoc helloFactoryIoc = (HelloIoc) applicationContext.getBean("instance");
helloFactoryIoc.sayHello();
}
```
### Spring Bean的声明周期?
* singleton单例非懒加载对象: IOC容器启动的时候会调用方法创建对象并放到IOC容器中,以后每次获取的就是直接从容器中拿(大Map.get)的同一个bean。
* prototype多实例: IOC容器启动的时候,IOC容器启动并不会去调用方法创建对象, 而是每次获取的时候才会调用方法创建对象。
`spring`容器在创建时会初始化一些处理器实例对象到容器中,这些用这些处理器对象在容器中处理维护业务`bean`的增强。

* `BeanFactoryPostProcessor`
1. bean的后置处理器;
2. 实现这个接口的bean自定义bean加载数据;
* 调用`postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)`
* 实例化`BeanPostProcessor`实现类
* 实例化`InstantiationAwareBeanPostProcessorAdapter`实现类
* 执行InstantiationAwareBeanPostProcessorAdapter
postProcessorBefore
* 创建`BeanFactory`对象
* `refreshBeanFactory()`: 刷新或创建`beanFactory`;
* 初始化业务`bean`对象
`finishBeanFactoryInitialization(beanFactory)`
```
在`doCreateBean()`中首先进行`bean`实例化工作,主要由`createBeanInstance()`实现,该方法返回一个`BeanWrapper`对象。`BeanWrapper`对象是`Spring`的一个低级`Bean`基础结构的核心接口,为什么说是低级呢?因为这个时候的`Bean`还不能够被我们使用,连最基本的属性都没有设置。而且在我们实际开发过程中一般都不会直接使用该类,而是通过`BeanFactory`隐式使用。
`BeanWrapper`接口有一个默认实现类`BeanWrapperImpl`,其主要作用是对`Bean`进行“包裹”,然后对这个包裹的`bean`进行操作,比如后续注入`bean`属性。
在实例化`bean`过程中,`Spring`采用“策略模式”来决定采用哪种方式来实例化`bean`,一般有反射和`CGLIB`动态字节码两种方式。
```
* 业务bean的属性赋值
* `populateBean(beanName, mbd, instanceWrapper)`属性赋值。
* 激活`Aware`
* 作用:
```
例如:`BeanNameAware`接口是为了让自身`Bean`能够感知到,获取到自身在`Spring`容器中的`id`属性;
同理,其他的`Aware`接口也是为了能够感知到自身的一些属性。
比如实现了`ApplicationContextAware`接口的类,能够获取到`ApplicationContext`,实现了`BeanFactoryAware`接口的类,能够获取到`BeanFactory`对象。
```
```
当`Spring`完成`bean`对象实例化并且设置完相关属性和依赖后,则会开始`bean`的初始化进程`(initializeBean())`,初始化第一个阶段是检查当前`bean`对象是否实现了一系列以`Aware`结尾的的接口。
`Aware`接口为`Spring`容器的核心接口,是一个具有标识作用的超级接口,实现了该接口的`bean`是具有被`Spring`容器通知的能力,通知的方式是采用回调的方式。
```
* `BeanPostProcessor`前置处理器
* 作用:
```
它主要是对`Spring`容器提供的`bean`实例对象进行有效的扩展,允许`Spring`在初始化`bean`阶段对其进行定制化修改,注入其它组件, 生命周期注解功能等。
```
* `InitializingBean`和`init-method`
```
`InitializingBean`是一个接口,它为`Spring Bean`的初始化提供了一种方式,它有一个`afterPropertiesSet()`方法,在`bean`的初始化进程中会判断当前`bean`是否实现了`InitializingBean`,如果实现了则调用`afterPropertiesSet()`进行初始化工作。然后再检查是否也指定了`init-method()`,如果指定了则通过反射机制调用指定的 `init-method()`。
```
* `DisposableBean`和`destroy-method`
`AnnotationConfigApplicationContext.close()`关闭容器是触发。
```
`DisposableBean`和`destroy-method`则用于对象的自定义销毁工作。
当一个`bean`对象经历了实例化、设置属性、初始化阶段,那么该`bean`对象就可以供容器使用了(调用的过程)。当完成调用后,如果是`singleton`类型的`bean`,则会看当前`bean`是否应实现了`DisposableBean`接口或者配置了`destroy-method`属性,如果是的话,则会为该实例注册一个用于对象销毁的回调方法,便于在这些 `singleton`类型的`bean`对象销毁之前执行销毁逻辑。
但是,并不是对象完成调用后就会立刻执行销毁方法,因为这个时候`Spring`容器还处于运行阶段,只有当`Spring`容器关闭的时候才会去调用。但是,`Spring`容器不会这么聪明会自动去调用这些销毁方法,而是需要我们主动去告知`Spring`容器。
```
### spring为什么默认使用JDK动态代理?
JDK 和 CGLib动态代理性能对比-教科书上的描述
我们不管是看书还是看文章亦或是我那个上搜索参考答案,可能很多时候,都可以找到如下的回答:
关于两者之间的性能的话,JDK动态代理所创建的代理对象,在以前的JDK版本中,性能并不是很高,虽然在高版本中JDK动态代理对象的性能得到了很大的提升,但是他也并不是适用于所有的场景。主要体现在如下的两个指标中:
1、CGLib所创建的动态代理对象在实际运行时候的性能要比JDK动态代理高不少,有研究表明,大概要高10倍;
2、但是CGLib在创建对象的时候所花费的时间却比JDK动态代理要多很多,有研究表明,大概有8倍的差距;
3、因此,对于singleton的代理对象或者具有实例池的代理,因为无需频繁的创建代理对象,所以比较适合采用CGLib动态代理,反正,则比较适用JDK动态代理。
因为spring容器中的bean默认是单实例;
### Spring IOC存储的实例为什么默认是单实例的?
单例只在初始化加载的时候实例化一次,一方面提高了效率,另一方面大大降,低了内存开销。
### 并发情况下Spring Bean是否安全?
Spring框架并没有对单例bean进行任何多线程的封装处理。关于单例bean的线程安全和并发问题需要开发者自行去搞定。但实际上,大部分的Spring bean并没有可变的状态(比如Serview类和DAO类),所以在某种程度上说Spring的单例bean是线程安全的。如果你的bean有多种状态的话(比如 View Model 对象),就**需要自行保证线程安全**。
Spring根本就没有对bean的多线程安全问题做出任何保证与措施。对于每个bean的线程安全问题,根本原因是每个bean自身的设计。不要在bean中声明任何有状态的实例变量或类变量,如果必须如此,那么就使用ThreadLocal把变量变为线程私有的,如果bean的实例变量或类变量需要在多个线程之间共享,那么就只能使用synchronized、lock、CAS等这些实现线程同步的方法了。
* 最浅显的解决办法就是将多态bean的作用域由“singleton”变更为“prototype”
* 使用ThreadLocal
参考: Spring中bean的安全性
### Spring IoC的控制反转的理解?
控制:IOC容器控制了对象的创建
反转:要创建的对象被动接受其依赖的对象,而不是该对象自己创建其依赖的对象
### 自动装配注解
* `@Autowired`默认根据bean类型装配,如果类型匹配到多个,那么在根据属性名和bean的id进行匹配(可以由Qualifier注解强制匹配指定的bean id),找不到则报错;
* `@Resource`根据名称查找bean;
* `@Resource`不支持`@Primary`功能;
* `@Resource`不支持`@Autowired(required = false)`的功能,使用`Resource`注解如果没有`IOC`容器中没有对应的ID则会报错;
### 设计模式
#### 静态代理和动态代理?
* 静态代理其实就是在程序运行之前,提前写好被代理方法的代理类,编译后运行。在程序运行之前,class已经存在。
```java
public interface Target {
public String execute();
}
```
实现
```java
public class TargetImpl implements Target {
@Override
public String execute() {
System.out.println("TargetImpl execute!");
return "execute";
}
}
```
代理类
```java
public class Proxy implements Target{
private Target target;
public Proxy(Target target) {
this.target = target;
}
@Override
public String execute() {
System.out.println("perProcess");
String result = this.target.execute();
System.out.println("postProcess");
return result;
}
}
```
测试
```java
public class ProxyTest {
public static void main(String[] args) {
Target target = new TargetImpl();
Proxy p = new Proxy(target);
String result = p.execute();
System.out.println(result);
}
}
```
* 动态代理主要是通过反射机制,在运行时动态生成所需代理的class
* JDK动态代理具体实现原理
* 通过实现InvocationHandler接口创建自己的调用处理器;
* 通过为Proxy类指定ClassLoader对象和一组interface来创建动态代理;
* 通过反射机制获取动态代理类的构造函数,其唯一参数类型就是调用处理器接口类型;
* 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数参入;
JDK动态代理: 要代理的类需要实现接口
Spirng默认采用JDK动态代理实现机制
接口
```java
public interface Target {
public String execute();
}
```
实现类
```java
public class TargetImpl implements Target {
@Override
public String execute() {
System.out.println("TargetImpl execute!");
return "execute";
}
}
```
```java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class DynamicProxyHandler implements InvocationHandler{
private Target target;
public DynamicProxyHandler(Target target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("========before==========");
Object result = method.invoke(target,args);
System.out.println("========after===========");
return result;
}
}
```
测试
```java
import java.lang.reflect.Proxy;
public class DynamicProxyTest {
public static void main(String[] args) {
Target target = new TargetImpl();
DynamicProxyHandler handler = new DynamicProxyHandler(target);
Target proxySubject = (Target) Proxy.newProxyInstance(TargetImpl.class.getClassLoader(),TargetImpl.class.getInterfaces(),handler);
String result = proxySubject.execute();
System.out.println(result);
}
}
```
无论是动态代理还是静态代理,都需要定义接口,然后才能实现代理功能。这同样存在局限性,因此,为了解决这个问题,出现了第三种代理方式:cglib代理。
* cglib代理动态代理
CGLib采用了字节码底层继承要代理类来实现(如果被代理类被final关键字所修饰,那么抱歉会失败),其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。JDK动态代理与CGLib动态代理均是实现Spring AOP的基础。
在创建动态代理对象时,需要设置一个或多个回调拦截器,这个回调拦截器必须要实现`MethodInterceptor`接口,代理类对象执行execute方法时,顺序执行拦截器;
目标类
```java
public class Target {
public String execute() {
String message = "-----------test------------";
System.out.println(message);
return message;
}
}
```
通用代理类
```java
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class MyMethodInterceptor implements MethodInterceptor{
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println(">>>>MethodInterceptor start...");
Object result = proxy.invokeSuper(obj,args);
System.out.println(">>>>MethodInterceptor ending...");
return "result";
}
}
```
测试
```java
import net.sf.cglib.proxy.Enhancer;
public class CglibTest {
public static void main(String ... args) {
System.out.println("***************");
Target target = new Target();
CglibTest test = new CglibTest();
Target proxyTarget = (Target) test.createProxy(Target.class);
String res = proxyTarget.execute();
System.out.println(res);
}
public Object createProxy(Class targetClass) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(targetClass);
enhancer.setCallback(new MyMethodInterceptor());
return enhancer.create();
}
}
```
#### 观察者模式
给你举个栗子:假设有三个人,小美(女,22),小王和小李。小美很漂亮,小王和小李是两个程序猿,时刻关注着小美的一举一动。有一天,小美说了一句:“谁来陪我打游戏啊。”这句话被小王和小李听到了,结果乐坏了,蹭蹭蹭,没一会儿,小王就冲到小美家门口了,在这里,小美是被观察者,小王和小李是观察者,被观察者发出一条信息,然后观察者们进行相应的处理,看代码:
```java
public interface Person {
//小王和小李通过这个接口可以接收到小美发过来的消息
void getMessage(String s);
}
```
这个接口相当于小王和小李的电话号码,小美发送通知的时候就会拨打`getMessage`这个电话,拨打电话就是调用接口,看不懂没关系,先往下看
```java
public class LaoWang implements Person {
private String name = "小王";
public LaoWang() {
}
@Override
public void getMessage(String s) {
System.out.println(name + "接到了小美打过来的电话,电话内容是:" + s);
}
}
public class LaoLi implements Person {
private String name = "小李";
public LaoLi() {
}
@Override
public void getMessage(String s) {
System.out.println(name + "接到了小美打过来的电话,电话内容是:->" + s);
}
}
```
代码很简单,我们再看看小美的代码:
```java
public class XiaoMei {
List list = new ArrayList();
public XiaoMei(){
}
public void addPerson(Person person){
list.add(person);
}
//遍历list,把自己的通知发送给所有暗恋自己的人
public void notifyPerson() {
for(Person person:list){
person.getMessage("你们过来吧,谁先过来谁就能陪我一起玩儿游戏!");
}
}
}
```
我们写一个测试类来看一下结果对不对
```java
public class Test {
public static void main(String[] args) {
XiaoMei xiao_mei = new XiaoMei();
LaoWang lao_wang = new LaoWang();
LaoLi lao_li = new LaoLi();
//小王和小李在小美那里都注册了一下
xiao_mei.addPerson(lao_wang);
xiao_mei.addPerson(lao_li);
//小美向小王和小李发送通知
xiao_mei.notifyPerson();
}
}
```
#### 装饰者模式
对已有的业务逻辑进一步的封装,使其增加额外的功能,如Java中的IO流就使用了装饰者模式,用户在使用的时候,可以任意组装,达到自己想要的效果。 举个栗子,我想吃三明治,首先我需要一根大大的香肠,我喜欢吃奶油,在香肠上面加一点奶油,再放一点蔬菜,最后再用两片面包夹一下,很丰盛的一顿午饭,营养又健康。(ps:不知道上海哪里有卖好吃的三明治的,求推荐~)那我们应该怎么来写代码呢? 首先,我们需要写一个Food类,让其他所有食物都来继承这个类,看代码:
```java
public class Food {
private String food_name;
public Food() {
}
public Food(String food_name) {
this.food_name = food_name;
}
public String make() {
return food_name;
};
}
```
代码很简单,我就不解释了,然后我们写几个子类继承它:
```java
//面包类
public class Bread extends Food {
private Food basic_food;
public Bread(Food basic_food) {
this.basic_food = basic_food;
}
public String make() {
return basic_food.make()+"+面包";
}
}
//奶油类
public class Cream extends Food {
private Food basic_food;
public Cream(Food basic_food) {
this.basic_food = basic_food;
}
public String make() {
return basic_food.make()+"+奶油";
}
}
//蔬菜类
public class Vegetable extends Food {
private Food basic_food;
public Vegetable(Food basic_food) {
this.basic_food = basic_food;
}
public String make() {
return basic_food.make()+"+蔬菜";
}
}
```
这几个类都是差不多的,构造方法传入一个`Food`类型的参数,然后在`make`方法中加入一些自己的逻辑,如果你还是看不懂为什么这么写,不急,你看看我的`Test`类是怎么写的,一看你就明白了
```java
public class Test {
public static void main(String[] args) {
Food food = new Bread(new Vegetable(new Cream(new Food("香肠"))));
System.out.println(food.make());
}
}
```
看到没有,一层一层封装,我们从里往外看:最里面我new了一个香肠,在香肠的外面我包裹了一层奶油,在奶油的外面我又加了一层蔬菜,最外面我放的是面包,是不是很形象,哈哈~ 这个设计模式简直跟现实生活中一摸一样,看懂了吗? 我们看看运行结果吧
运行结果
```
香肠+奶油+蔬菜+面包
```
一个三明治就做好了~
#### 工厂模式
##### 简单工厂模式
简单工厂模式:一个抽象的接口,多个抽象接口的实现类,一个工厂类,用来实例化抽象的接口
```java
// 抽象产品类
abstract class Car {
public void run();
public void stop();
}
// 具体实现类
class Benz implements Car {
public void run() {
System.out.println("Benz开始启动了。。。。。");
}
public void stop() {
System.out.println("Benz停车了。。。。。");
}
}
class Ford implements Car {
public void run() {
System.out.println("Ford开始启动了。。。");
}
public void stop() {
System.out.println("Ford停车了。。。。");
}
}
// 工厂类
class Factory {
public static Car getCarInstance(String type) {
Car c = null;
if ("Benz".equals(type)) {
c = new Benz();
}
if ("Ford".equals(type)) {
c = new Ford();
}
return c;
}
}
public class Test {
public static void main(String[] args) {
Car c = Factory.getCarInstance("Benz");
if (c != null) {
c.run();
c.stop();
} else {
System.out.println("造不了这种汽车。。。");
}
}
}
```
##### 工厂方法模式
工厂方法模式:有四个角色,抽象工厂模式,具体工厂模式,抽象产品模式,具体产品模式。不再是由一个工厂类去实例化具体的产品,而是由抽象工厂的子类去实例化产品
```java
// 抽象产品角色
public interface Moveable {
void run();
}
// 具体产品角色
public class Plane implements Moveable {
@Override
public void run() {
System.out.println("plane....");
}
}
public class Broom implements Moveable {
@Override
public void run() {
System.out.println("broom.....");
}
}
// 抽象工厂
public abstract class VehicleFactory {
abstract Moveable create();
}
// 具体工厂
public class PlaneFactory extends VehicleFactory {
public Moveable create() {
return new Plane();
}
}
public class BroomFactory extends VehicleFactory {
public Moveable create() {
return new Broom();
}
}
// 测试类
public class Test {
public static void main(String[] args) {
VehicleFactory factory = new BroomFactory();
Moveable m = factory.create();
m.run();
}
}
```
##### 抽象工厂模式
抽象工厂模式:与工厂方法模式不同的是,工厂方法模式中的工厂只生产单一的产品,而抽象工厂模式中的工厂生产多个产品
```java
/抽象工厂类
public abstract class AbstractFactory {
public abstract Vehicle createVehicle();
public abstract Weapon createWeapon();
public abstract Food createFood();
}
//具体工厂类,其中Food,Vehicle,Weapon是抽象类,
public class DefaultFactory extends AbstractFactory{
@Override
public Food createFood() {
return new Apple();
}
@Override
public Vehicle createVehicle() {
return new Car();
}
@Override
public Weapon createWeapon() {
return new AK47();
}
}
//测试类
public class Test {
public static void main(String[] args) {
AbstractFactory f = new DefaultFactory();
Vehicle v = f.createVehicle();
v.run();
Weapon w = f.createWeapon();
w.shoot();
Food a = f.createFood();
a.printName();
}
}
```
##### 简单工厂和抽象工厂有什么区别?
简单工厂和工厂方法模式的区别就在于需要创建对象的复杂程度上。而且抽象工厂模式是三个里面最为抽象、最具一般性的。抽象工厂模式的用意为:给客户端提供一个接口,可以创建多个产品族中的产品对象。
### 强引用,软引用,弱引用和虚引用?
|引用类型被| 垃圾回收时间| 示例|用途 |生存时间|表示方法|
|----|----| ---- |----|----|----|
|强引用| 从来不会|Object o=new Object(); // 强引用| 对象的一般状态,多只`new`出来的对象| `JVM`停止运行时终止||
|软引用 |当内存不足时|String str=new String("abc");// 强引用 SoftReference softRef=new SoftReference(str);// 软引用|可用来实现内存敏感的高速缓存,浏览器页面对象,用于获取历史页面对象| 内存不足时终止|java.lang.ref.SoftReference类来表示|
|弱引用 |一旦发现了只具有弱引用的对象|String str=new String("abc");WeakReference abcWeakRef =new WeakReference(str); | 如果这个对象是偶尔的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你应该用 Weak Reference 来记住此对象。 | 垃圾回收后终止|java.lang.ref.WeakReference类来表示|
|虚引用 |正常垃圾回收时 ||用来跟踪对象被垃圾回收的活动,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收| 垃圾回收后终止|java.lang.ref.PhantomReference类表示|
```
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
```
### `ThreadLocal`为什么容易内存泄漏?
```java
C c = new C(b);
b = null;
```
当 b 被设置成null时,那么是否意味这一段时间后GC工作可以回收 b 所分配的内存空间呢?答案是否定的,因为即使 b 被设置成null,但 c 仍然持有对 b 的引用,而且还是强引用,所以GC不会回收 b 原先所分配的空间,既不能回收,又不能使用,这就造成了 内存泄露。
那么如何处理呢?
可以通过c = null;,也可以使用弱引用WeakReference w = new WeakReference(b);。因为使用了弱引用WeakReference,GC是可以回收 b 原先所分配的空间的。
```java
private void set(ThreadLocal> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// tab是弱引用元素数组;key为当前ThreadLocal对象,value为设置的值
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
```

查看源代码发现引用关系是这样的:当前ThreadLocal -> 当前Thread —> ThreaLocalMap[ThreadLocalMap getMap(Thread t)] -> Entry[keyhash]对象(Entry(ThreadLocal> k, Object v)Entry对象引用了弱引用对象ThreadLocal) -> value
```java
private static ThreadLocal threadLocal = new ThreadLocal<>();
threadLocal.set(T);
```
`threadLocal`一直没有被外部强引用引用他,`GC`势必会回收`new ThreadLocal<>()`,这是`key(Entry[keyhash]对象作为key)`这个`key`就为`null`;
`ThreadLocalMap`中就会出现`key`为`null`的`Entry`,就没有办法访问这些`key`为`null`的`Entry`的`value`,如果当前线程再迟迟不结束的话,这些`key`为`null`的`Entry`的`value`就会一直存在一条强引用链:`Thread Ref` -> `Thread` -> `ThreaLocalMap` -> `Entry` -> `value` 永远无法回收,造成内存泄漏。
`ThreadLocalMap`的设计中已经考虑到这种情况,也加上了一些防护措施:在`ThreadLocal`的`get()`,`set()`,`remove()`的时候都会清除线程`ThreadLocalMap`里所有`key`为`null`的`value`。
我们要考虑一种会发生内存泄漏的情况,如果ThreadLocal被设置为null后,而且没有任何强引用指向它,根据垃圾回收的可达性分析算法,ThreadLocal将会被回收。这样一来,ThreadLocalMap中就会含有key为null的Entry,而且ThreadLocalMap是在Thread中的,只要线程迟迟不结束,这些无法访问到的value会形成内存泄漏。为了解决这个问题,ThreadLocalMap中的getEntry()、set()和remove()函数都会清理key为null的Entry,以下面的getEntry()函数的源码为例。
#### 为什么使用ThreadLocal为弱引用而不是强引用?
再分析一遍所谓内存泄漏:
每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收。所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的就可能出现内存泄露 。(在web应用中,每次http请求都是一个线程,tomcat容器配置使用线程池时会出现内存泄漏问题)
* key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
* key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用get(),set(),remove()的时候会被清除。
* 比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:`弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用get(),set(),remove()的时候会被清除。`
### TCP?
#### 三次握手

三次握手:
* 客户端–发送带有SYN标志的数据包–一次握手–服务端
* 服务端–发送带有SYN/ACK标志的数据包–二次握手–客户端
* 客户端–发送带有带有ACK标志的数据包–三次握手–服务端
详细分析:
* TCP服务器进程先创建传输控制块TCB,时刻准备接受客户进程的连接请求,此时服务器就进入了LISTEN(监听)状态;
* TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这是报文首部中的同部位SYN=1,同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。
* TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号。
* TCP客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。
* 当服务器收到客户端的确认后也进入ESTABLISHED状态,此后双方就可以开始通信了。
##### 为什么TCP客户端最后还要发送一次确认呢?
一句话,主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。
如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。
如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。
#### 四次挥手

四次挥手:
* 客户端-发送一个FIN,用来关闭客户端到服务器的数据传送
* 服务器-收到这个FIN,它发回一个ACK,确认序号为收到的序号加1 。和SYN一样,一个FIN将占用一个序号
* 服务器-关闭与客户端的连接,发送一个FIN给客户端
* 客户端-发回ACK报文确认,并将确认序号设置为收到序号加1
详细分析:
* 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
* 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。 这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
* 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
* 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
* 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
* 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
##### 为什么客户端最后还要等待2MSL?
MSL(Maximum Segment Lifetime),TCP允许不同的实现可以设置不同的MSL值。
第一,保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。
第二,防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。
为什么建立连接是三次握手,关闭连接确是四次挥手呢?
建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。 而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。
#### TCP特点?
* 面向连接
数据传输之前需要建立连接:三次握手
数据传输结束之后需要释放连接: 四次挥手
* TCP连接的时候为何是三次握手而不是两次?
```
三次握手解决的主要是同步请求报文SYN超时的问题
如果只有两次握手(前两次),客户端发起的SYN同步报文如果发生丢失或者超时的现象,那么SYN同步报文在网络路由中逗留,客户端会启用超时重传策略,重新发送一个SYN,服务端收到之后会发送一个同步确认报文(SYNACK),连接建立完毕。此时如果第一次超时的SYN传递成功,server端会误认为客户端又进行了一次连接请求,造成误会。
三次握手中,客户端再收到server端的同步确认报文(SYNACK)之后,会发送一个ACK确认报文,进行连接的建立。在第一种情况下客户端对server超时报文的同步确认报文不会有ACK确认报文,所以server端不再进行操作。
```
* TCP的四次挥手为何要进行两方面的断开呢?
```
因为客户端与server端的连接通道是全双工的,两条通道都可以发送或者接收,挥手两次达到半关闭状态,挥手四次才可以全部关闭连接。
```
* 可靠传输
* 特点
* 无差错
* 不丢失
* 不重复
* 按序到达
TCP具体是通过怎样的方式来保证数据的顺序化传输呢?
```
主机每次发送数据时,TCP就给每个数据包分配一个序列号并且在一个特定的时间内等待接收主机对分配的这个序列号进行确认,如果发送主机在一个特定时间内没有收到接收主机的确认,则发送主机会重传此数据包。接收主机利用序列号对接收的数据进行确认,以便检测对方发送的数据是否有丢失或者乱序等,接收主机一旦收到已经顺序化的数据,它就将这些数据按正确的顺序重组成数据流并传递到高层进行处理。
具体步骤如下:
(1)为了保证数据包的可靠传递,发送方必须把已发送的数据包保留在缓冲区;
(2)并为每个已发送的数据包启动一个超时定时器;
(3)如在定时器超时之前收到了对方发来的应答信息(可能是对本包的应答,也可以是对本包后续包的应答),则释放该数据包占用的缓冲区;
(4)否则,重传该数据包,直到收到应答或重传次数超过规定的最大次数为止。
(5)接收方收到数据包后,先进行CRC校验,如果正确则把数据交给上层协议,然后给发送方发送一个累计应答包,表明该数据已收到,如果接收方正好也有数据要发给发送方,应答包也可方在数据包中捎带过去。
```
* 原因: TCP拥有停止等待协议特性
* 误差错情况

* 超时重传

* 确认丢失

* 确认迟到

* 面向字节流

* 流量控制(滑动窗口协议:接收窗口可以通过报文字段动态调整发送窗口速率)

* 阻塞控制
* 特点
* 慢开始、拥塞控制、快恢复、快重传
* 简单描述TCP慢启动(慢开始)的特点

### 如果TCP已经建立了连接,但是客户端突然出现故障了怎么办?
TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75分钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
所谓的无状态,是指浏览器每次向服务器发起请求的时候,不是通过一个连接,而是每次都建立一个新的连接。如果是一个连接的话,服务器进程中就能保持住这个连接并且在内存中记住一些信息状态。而每次请求结束后,连接就关闭,相关的内容就释放了,所以记不住任何状态,成为无状态连接。
### HTTP(超文本协议)介绍?
Http协议是建立在TCP协议基础之上的,当浏览器需要从服务器获取网页数据的时候,会发出一次Http请求。Http会通过TCP建立起一个到服务器的连接通道,当本次请求需要的数据完毕后,Http会立即将TCP连接断开,这个过程是很短的。所以Http连接是一种短连接,是一种无状态的连接。
所谓的无状态,是指浏览器每次向服务器发起请求的时候,不是通过一个连接,而是每次都建立一个新的连接。如果是一个连接的话,服务器进程中就能保持住这个连接并且在内存中记住一些信息状态。而每次请求结束后,连接就关闭,相关的内容就释放了,所以记不住任何状态,成为无状态连接。
http传输流:

发送端在层与层间传输数据时,没经过一层都会被加上首部信息,接收端每经过一层都会删除一条首部
#### http常用状态码?
2XX 成功
200 OK,表示从客户端发来的请求在服务器端被正确处理
204 No content,表示请求成功,但响应报文不含实体的主体部分
206 Partial Content,进行范围请求
3XX 重定向
301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
302 found,临时性重定向,表示资源临时被分配了新的 URL
303 see other,表示资源存在着另一个 URL,应使用 GET 方法丁香获取资源
304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况
307 temporary redirect,临时重定向,和302含义相同
4XX 客户端错误
400 bad request,请求报文存在语法错误
401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息
403 forbidden,表示对请求资源的访问被服务器拒绝
404 not found,表示在服务器上没有找到请求的资源
5XX 服务器错误
500 internal sever error,表示服务器端在执行请求时发生了错误
503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求
#### http组成?
起始行、消息头、空行和消息体
* 起始行:GET /home HTTP/1.1
也就是方法 + 路径 + http版本。
对于响应报文来说,起始行一般张这个样:
HTTP/1.1 200 OK
* 请求头
展示一下请求头和响应头在报文中的位置:


* 空格
很重要,用来区分开头部和实体。
问: 如果说在头部中间故意加一个空行会怎么样?
那么空行后的内容全部被视为实体。
* 实体
就是具体的数据了,也就是body部分。请求报文对应请求体, 响应报文对应响应体。
### GET 和 POST 有什么区别?
* 从长度的角度: GET请求的url长度大小会受到浏览器限制,post没有
* 从缓存的角度,GET 请求会被浏览器主动缓存下来,留下历史记录,而 POST 默认不会。
* 从编码的角度,GET 只能进行 URL 编码,只能接收 ASCII 字符,而 POST 没有限制。
* 从参数的角度,GET 一般放在 URL 中,因此不安全,POST 放在请求体中,更适合传输敏感信息。
* 从幂等性的角度,GET是幂等的,而POST不是。(幂等表示执行相同的操作,结果也是相同的)
* 从TCP的角度,GET 请求会把请求报文一次性发出去,而 POST 会分为两个 TCP 数据包,首先发 header 部分,如果服务器响应 100(continue), 然后发 body 部分。(火狐浏览器除外,它的 POST 请求只发一个 TCP 包)
### http缺点?
HTTP 缺点
* 无状态
所谓的优点和缺点还是要分场景来看的,对于 HTTP 而言,最具争议的地方在于它的无状态。
在需要长连接的场景中,需要保存大量的上下文信息,以免传输大量重复的信息,那么这时候无状态就是 http 的缺点了。
但与此同时,另外一些应用仅仅只是为了获取一些数据,不需要保存连接上下文信息,无状态反而减少了网络开销,成为了 http 的优点。
* 明文传输
即协议里的报文(主要指的是头部)不使用二进制数据,而是文本形式。
这当然对于调试提供了便利,但同时也让 HTTP 的报文信息暴露给了外界,给攻击者也提供了便利。WIFI陷阱就是利用 HTTP 明文传输的缺点,诱导你连上热点,然后疯狂抓你所有的流量,从而拿到你的敏感信息。
* 队头阻塞问题
当 http 开启长连接时,共用一个 TCP 连接,同一时刻只能处理一个请求,那么当前请求耗时过长的情况下,其它的请求只能处于阻塞状态,也就是著名的队头阻塞问题。接下来会有一小节讨论这个问题。
### UDP协议的三大特点?
* 无连接
* 尽最大努力交付
```
不管在不在,知不知道,只管发送
```
* 面向报文: 既不合并,也不拆分

#### UDP协议的功能?
* 复用
* 分用

* 差错检测

### TCP和UDP区别?
一、UDP:
1、将数据源和目的地封装到数据包中,不需要建立连接
2、每个数据包的大小限制在64k以内
3、因无连接,是不可靠协议
4、不需要建立连接,速度快
例子:聊天、对讲机就是UDP的,面向无连接(不管在不在,知不知道,只管发送,求速度),丢数据也不管。速度快。数据被分成包
二、TCP:
1、建立连接,形成传输数据的通道
2、在连接中进行大量数据的传输
3、通过三次握手完成连接、是可靠协议
4、必须建立连接,效率会稍低
例子:电话通话,必须连接,对方同意才可以发送数据(不然就等待),不能丢失数据。
区别总结:
TCP是面向连接的,支持可靠传输的面向字节流的,具有流量控制和拥塞控制的协议,UDP只具有复用,分用和差错检测的功能,且是无连接的。
### 同一个包中可以创建内部类对象?
一个包中是不能直接访问另一个包中的内部类,所以无法直接创建另一个包中内部类对象。
内部类做为其外部类的成员,因此可以使用任意访问控制符如private、protected和public修饰。
外部类的上一级程序单元是包,所以它只有2个作用域:同一个包和任何位置。因此只需两种访问权限:包访问权限和公开访问权限。正好对应省略访问控制符和public访问控制符。省略访问控制符是包访问权限,即同一包中的其他类可访问省略访问控制符的成员。因此,如果一一个外部类不使用任何访问控制符修饰,则只能被同一一个包中其他类访问。而内部类的_上一级程序单元是外部类,它就具有四个作用域:同一个类、同一个包、父子类和任何位置,因此可以使用四种访问控制权限。
### Minor GC(Yang GD)和Full GC?
新生代内存不够用时候发生MGC也叫YGC,JVM内存不够的时候发生FGC
### 聚集索引和非聚集索引(辅助索引) 物理索引和逻辑索引?
聚集索引和非聚集索引可以类比为字典查询方式,按照拼音查询和部首查询,拼音是有序的,汉子在字典中也是有序的,而部首笔画索引,虽然笔画相同的字在笔画索引中相邻,但是实际存储页码却不相邻。
* 聚集索引: 正文内容按照一个特定维度排序存储,这个特定的维度就是聚集索引
* 非聚集索引(辅助索引): 索引项顺序存储,但索引项对应的内容却是随机存储的
* 物理索引: 物理索引是存储在磁盘上的实际索引结构
* 逻辑索引: 逻辑索引是对物理索引的引用
```sql
create table student (
`id` INT UNSIGNED AUTO_INCREMENT,
`name` VARCHAR(255),
PRIMARY KEY(`id`),
KEY(`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```
以student表为例,该表中主键id是该表的聚集索引、name为非聚集索引;表中的每行数据都是按照聚集索引id排序存储的;比如要查找name='Arla'和name='Arle'的两个同学,他们在name索引表中位置可能是相邻的,但是实际存储位置可能差的很远。**name索引表节点按照name排序,检索的是每一行数据的主键。聚集索引表按照主键id排序,检索的是每一行数据的真实内容**,也就是说查询name='Arle'的记录时,首先通过name索引表查找到Arle的主键id(可能有多个主键id,因为有重名的同学),再根据主键id的聚集索引找到相应的行记录;
每张表只有一个聚集索引
### MySQL覆盖索引?
覆盖索引:如果查询条件使用的是普通索引(或是联合索引的最左原则字段),查询结果是联合索引的字段或是主键,不用回表操作,直接返回结果,减少IO磁盘读写读取正行数据
覆盖索引
建立两列以上的索引,即可查询复合索引里的列的数据而不需要进行回表二次查询,如index(col1, col2),执行下面的语句
select col1, col2 from t1 where col1 = '213';
要注意使用复合索引需要满足最左侧索引的原则,也就是查询的时候如果where条件里面没有最左边的一到多列,索引就不会起作用。
覆盖索引是select的数据列只用从索引中就能够取得,不必读取数据行,换句话说查询列要被所建的索引覆盖。索引的字段不只包含查询列,还包含查询条件、排序等。
### mysql最左侧索引原则?
如果一个表中只有a,b,c,d四个字段,给他们加上联合索引(a,b,c),此时
where a and b and c
where a and c
where c and a
都是用到索引的但是
where b and c 不会用到索引
如果一个表中只有a,b,c三个字段,给他们加上联合索引(a,b,c),此时
SELECT * FROM test WHERE c=2是否用到索引?答案是用到的
如果表中的字段除了(col1,col2,col3),还有别的字段。那么的 EXPLAIN 中结果将不会是 INDEX 而是 ALL。如果表中的字段只有(col1,col2,col3),那么的 EXPLAIN 中结果才会是 INDEX ,这种情况用到了mysql的覆盖索引。
最左侧原则不是要求索引(a,b,c)必须按照where a=x and b=x and c=x 才可以。顺序是可以互换的,关键是要有这个a,而且这个a一定要是等值匹配。
个人认为,所谓最左前缀原则就是先要看第一列,在第一列满足的条件下再看左边第二列,以此类推。
索引是因为B+树结构 所以查找快 如果单看第三列 是非排序的。
多列索引是先按照第一列进行排序,然后在第一列排好序的基础上再对第二列排序,如果没有第一列的话,直接访问第二列,那第二列肯定是无序的,直接访问后面的列就用不到索引了。
所以如果不是在前面列的基础上而是但看后面某一列,索引是失效的。
### B+Tree特点?
B-tree 索引可以用于使用 =, >, >=, <, <= 或者 BETWEEN 运算符的列比较。如果 LIKE 的参数是一个没有以通配符起始的常量字符串的话也可以使用这种索引。
### java有8种基本数据类型?
数值类型byte、short、int、long、float、double。 数值类型又可以分为整数类型byte、short、int、long和浮点数类型float、double
字符类型char
布尔类型boolean
占用字节分别是byte(1),char(2),short(2),int(4),float(4),long(8),double(8),boolean(boolean类型的数组,每个boolean占1个字节,单个boolean变量占4个字节)
### HashMap尾插?
HashMap在jdk1.7中采用头插入法,在扩容时会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题。而在jdk1.8中采用尾插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。
1.7源码在扩容resize()过程中,在将旧数组上的数据 转移到 新数组上时,转移数据操作 = 按旧链表的正序遍历链表、在新链表的头部依次插入,即在转移数据、扩容后,容易出现链表逆序的情况
由于 JDK 1.8 转移数据操作 = 按旧链表的正序遍历链表、在新链表的尾部依次插入,所以不会出现链表**逆序、倒置**的情况,故不容易出现环形链表的情况。
参考: https://juejin.im/post/5aa5d8d26fb9a028d2079264
### HashMap扩容机制?
1.扩容
阈值是0.75
创建一个新的Entry空数组,长度是原数组的2倍。
2.ReHash
### 为什么采用 哈希码 与运算(&) (数组长度-1) 计算数组下标?
根据HashMap的容量大小(数组长度),按需取 哈希码一定数量的低位 作为存储的数组下标位置,从而 解决 “哈希码与数组大小范围不匹配” 的问题
### 1.8HashMap为什么在计算数组下标前,需对哈希码进行二次处理:扰动处理?
加大哈希码低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性 & 均匀性,最终减少Hash冲突
### HashMap什么情况下出现链表环?
ReHash在并发的情况下可能会形成链表环。
### 为什么1.8扩容的时候为啥一定必须是2的多少次幂?
1.7源码定位数组下标
```java
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
```
* 元素随机平均分布到数组
上述代码也相当于对length求模。 注意最后return的是h&(length-1)。如果length不为2的幂,比如15。那么length-1的2进制就会变成1110。在h为随机数的情况下,和1110做&操作。尾数永远为0。那么0001、1001、1101等尾数为1的位置就永远不可能被entry占用。这样会造成浪费,不随机等问题。 length-1 二进制中为1的位数越多,那么分布就平均。
* 扩容时快速定位元素下标位置
1.8扩容源码
```java
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode)e).split(this, newTab, j, oldCap);
else { // preserve order
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
```
resize过程中不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图(一方面位运算更快,另一方面抗碰撞的Hash函数其实挺耗时的)
参考: https://blog.csdn.net/dalong3976/article/details/83934609
### 为什么在JDK1.8中进行对HashMap优化的时候,把链表转化为红黑树的阈值是8,而不是7或者不是20呢?
jdk作者选择8,一定经过了严格的运算,觉得在长度为8的时候,与其保证链表结构的查找开销,不如转换为红黑树,改为维持其平衡开销。
### 如何判断链表有环?
方法一:节点索引比较
遍历链表,每遍历到一个新节点,就从头节点重新遍历新节点之前的所有节点,用新节点ID和此节点之前所有节点ID依次作比较。如果发现新节点之前的所有节点当中存在相同节点ID,则说明该节点被遍历过两次,链表有环
方法二:指针追踪
首先创建两个指针1和2(在java里就是两个对象引用),同时指向这个链表的头节点。然后开始一个大循环,在循环体中,让指针1每次向下移动一个节点,让指针2每次向下移动两个节点,然后比较两个指针指向的节点是否相同。如果相同,则判断出链表有环,如果不同,则继续下一次循环。
### 表分区、水平拆分和垂直查分表?
#### 分区
分区优点:
* 可以让单表存储更多的数据
* 分区表的数据更容易维护,可以通过清楚整个分区批量删除大量数据,也可以增加新的分区来支持新插入的数据。另外,还可以对一个独立分区进行优化、检查、修复等操作
* 部分查询能够从查询条件确定只落在少数分区上,速度会很快
* 分区表的数据还可以分布在不同的物理设备上,从而搞笑利用多个硬件设备
* 可以使用分区表赖避免某些特殊瓶颈,例如InnoDB单个索引的互斥访问、ext3文件系统的inode锁竞争
* 可以备份和恢复单个分区
#### 适合场景
* 最适合的场景数据的时间序列性比较强,则可以按时间来分区,如下所示:
```sql
CREATE TABLE members (
firstname VARCHAR(25) NOT NULL,
lastname VARCHAR(25) NOT NULL,
username VARCHAR(16) NOT NULL,
email VARCHAR(35),
joined DATE NOT NULL
)
PARTITION BY RANGE( YEAR(joined) ) (
PARTITION p0 VALUES LESS THAN (1960),
PARTITION p1 VALUES LESS THAN (1970),
PARTITION p2 VALUES LESS THAN (1980),
PARTITION p3 VALUES LESS THAN (1990),
PARTITION p4 VALUES LESS THAN MAXVALUE
);
```
查询时加上时间范围条件效率会非常高,同时对于不需要的历史数据能很容的批量删除。
* 如果数据有明显的热点,而且除了这部分数据,其他数据很少被访问到,那么可以将热点数据单独放在一个分区,让这个分区的数据能够有机会都缓存在内存中,查询时只访问一个很小的分区表,能够有效使用索引和缓存
#### 垂直拆分
常见的是把一个多字段的大表按常用字段和非常用字段进行拆分,每个表里面的数据记录数一般情况下是相同的,只是字段不一样,使用主键关联
垂直拆分的优点是:
* 可以使得行数据变小,一个数据块(Block)就能存放更多的数据,在查询时就会减少I/O次数(每次查询时读取的Block 就少)
* 可以达到最大化利用Cache的目的,具体在垂直拆分的时候可以将不常变的字段放一起,将经常改变的放一起
* 数据维护简单
缺点:
* 主键出现冗余,需要管理冗余列
* 会引起表连接JOIN操作(增加CPU开销)可以通过在业务服务器上进行join来减少数据库压力
* 依然存在单表数据量过大的问题(需要水平拆分)
* 事务处理复杂
#### 水平拆分
水平拆分的优点是:
* 不存在单库大数据和高并发的性能瓶颈
* 应用端改造较少
* 提高了系统的稳定性和负载能力
缺点:
分片事务难以解决 ,跨节点Join性能较差,逻辑复杂
### MySQL的FULLTEXT索引?
使用全文索引
EXPLAIN select * FROM t_index WHERE MATCH(gender) AGAINST('r');
联合name,en_name字段必须是FULLTEXT的联合索引,否则执行下面语句会报错,联合fulltext索引不能使用单个字段查询
select * FROM test WHERE MATCH(name,en_name) AGAINST('r hate');
注意: match() 函数中指定的列必须和全文索引中指定的列完全相同,否则就会报错,无法使用全文索引,这是因为全文索引不会记录关键字来自哪一列。如果想要对某一列使用全文索引,请单独为该列创建全文索引。

如图表中只有gender字段作为FULLTEXT索引,执行语句
```sql
select * FROM t_index WHERE MATCH(gender) AGAINST('11');
```
查询是没有数据的,原因是MySQL最小搜索长度 MyISAM 引擎下默认是 4,InnoDB 引擎下是 3,也即,MySQL 的全文索引只会对长度大于等于 4 或者 3 的词语建立索引;

如图表中gender和school创建联合FULLTEXT索引,执行语句
```sql
select * FROM t_index WHERE MATCH(gender,school) AGAINST('1131 222');
```
查询是没有数据的,原因是只有gender匹配上1131时,才去匹配school,虽然school可以匹配上222,但是根据最左索引原则,则无数据。
### 为什么索引能提高查询速度?
查询汉字可以通过拼音或部首查询,能够快速定位汉字所在的页码,索引也是这个道理
* 索引是使记录数据有序化的技术
* 它可以指定按某列/某几列预先排序
* 根据索引值进行分类并按需排序
* 查询数据先查询索引表,获取数据所在行的物理地址
* 就不用再进行全表扫描了

比如上图,action值为2的索引值分类存储在了索引空间,可以快速地查询到索引值所对应的列。
索引的优缺点
优势:以快速检索,减少I/O次数,加快检索速度;根据索引分组和排序,可以加快分组和排序;
### 为什么HashMap默认设置为16?
原因一:均匀分布在桶中,减少hash碰撞
`index = hash & (n - 1)`
16是2的幂次方,length - 1的值是所有二进制位全为1,hash是key的hashcode作为随机数,hash和n-1与运算后最后一位有可能是0或1;如果length不是2的幂次方,那么hash和n-1与运算后最后一位有只能是0,所以0001,1001,100001,这些脚标所在的桶永远都是空的。
原因二:作为经验值
在效率和内存使用上做一个权衡。这个值既不能太小,也不能太大。
太小了就有可能频繁发生扩容,影响效率。太大了又浪费空间,不划算。
所以,16就作为一个经验值被采用了
### countdownlatch和join方法的区别?
CountDownLatch与join的区别:调用thread.join() 方法必须等thread 执行完毕,当前线程才能继续往下执行,而CountDownLatch通过计数器提供了更灵活的控制,只要检测到计数器为0当前线程就可以往下执行而不用管相应的thread是否执行完毕
### synchronized原理?
* 实现原理
JVM 是通过进入、退出对象监视器( Monitor )来实现对方法、同步块的同步的。
具体实现是在编译之后在同步方法调用前加入一个 monitor.enter 指令,在退出方法和异常处插入 monitor.exit 的指令。
当代码进入同步块时,如果同步对象为无锁状态时,当前线程会在栈帧中创建一个锁记录(Lock Record)区域,同时将锁对象的对象头中 Mark Word 拷贝到锁记录中,再尝试使用 CAS 将 Mark Word 更新为指向锁记录的指针。
如果更新成功,当前线程就获得了锁。
如果更新失败 JVM 会先检查锁对象的 Mark Word 是否指向当前线程的锁记录。
如果是则说明当前线程拥有锁对象的锁,可以直接进入同步块。
不是则说明有其他线程抢占了锁,如果存在多个线程同时竞争一把锁,轻量锁就会膨胀为重量锁。
偏向锁->轻量级锁->重量级锁
* 锁升级示意图

`synchronized`是解决线程安全的问题,常用在 同步普通方法、静态方法、代码块中;
`synchronized`是非公平、可重入
每个对象有一个锁和一个等待队列,锁只能被一个线程持有,其他需要锁的线程需要阻塞等待。锁被释放后,对象会从队列中取出一个并唤醒,唤醒哪个线程是不确定的,不保证公平性
* 两种形式
* 方法
生成的字节码文件中会多一个`ACC_SYNCHRONIZED`标志位,当一个线程访问方法时,会去检查是否存在`ACC_SYNCHRONIZED`标识,如果存在,执行线程将先获取`monitor`,获取成功之后才能执行方法体,方法执行完后再释放`monitor`。在方法执行期间,其他任何线程都无法再获得同一个`monitor`对象,也叫隐式同步
* 代码快
加了 `synchronized` 关键字的代码段,生成的字节码文件会多出 `monitorenter` 和 `monitorexit` 两条指令,每个`monitor`维护着一个记录着拥有次数的计数器, 未被拥有的`monitor`的该计数器为`0`,当一个
线程获执行`monitorenter`后,该计数器自增`1`;当同一个线程执行`monitorexit`指令的时候,计数器再自减`1`。当计数器为`0`的时候,`monitor`将被释放.也叫显式同步
两种本质上没有区别,底层都是通过`monitor`来实现同步, 只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成
```sh
# 查看字节码
javac XXX.java
javap -v XXX.class
```
* 同步方法字节码

* 同步代码块字节码

### 同步容器和并发容器?
* 同步容器
同步容器通过synchronized关键字修饰容器保证同一时刻内只有一个线程在使用容器,从而使得容器线程安全
Hashtable/Vector/同步工具类包装Collections.synXXX
* 并发容器
并发容器指的是允许多线程同时使用容器,并且保证线程安全。而为了达到尽可能提高并发,Java并发工具包中采用了多种优化方式来提高并发容器的执行效率,核心的就是:锁、CAS(无锁)、COW(读写分离)、分段锁。
ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentSkipListMap(ConcurrentSkipListSet和ConcurrentSkipListMap原理一样,它是实现了高并发线程安全的TreeSet。)
### 设计模式?
AQS继承模板方法设计模式
### 如何排查内存溢出?
内存溢出现象
1. 堆/Perm 区不断增长, 没有下降趋势(回收速度赶不上增长速度), 最后不断触发FullGC

2. 每次FullGC后, 堆/Perm 区在慢慢的增长, 最后不断触发FullGC, 甚至crash

* 排查内存溢出
1. 使用 jmap 查看哪些对象个数非常多,内存占用多
2. 虚拟机统计信息监视工具
jstat”监视虚拟机各种运行状态信息。
jstat命令格式为:
jstat [ option vmid [interval[s|ms] [count]] ]
3. 分析 dump 文件和堆占用情况
4. 定位具体的类和相关代码的调用过程,一步一步的查找问题所在
### JVM调优工具?
jdk自带监控工具:jconsole和jvisualvm,第三方有:MAT(Memory Analyzer Tool)、GChisto。jconsole,Java Monitoring and Management Console是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存,线程和类等的监控jvisualvm,jdk自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC变化等。MAT,Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗GChisto,一款专业分析gc日志的工具
### ArrayBlockingQueue和LinkedBlockingQueue区别?
* 保证安全机制
ArrayBlockingQueue: ReenTrantLock
LinkedBlockingQueue: 各种锁
* 使用锁对象
ArrayBlockingQueue: ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行
LinkedBlockingQueue: 对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能
* 额外内存开销
ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象
### MySQL主键和外键的作用?
#### 定义
主键: 唯一标识一条记录,不能有重复的,不允许为空
外键: 表的外键是另一表的主键, 外键可以有重复的, 可以是空值
#### 作用
主键: 用来保证数据完整性
外键: 用来和其他表建立联系用的
索引: 是提高查询排序的速度
#### 个数
主键: 主键只能有一个
外键: 一个表可以有多个外键
索引: 一个表可以有多个唯一索引
不适合使用外键的场景: 互联网行业,用户量大,并发度高,为此数据库服务器很容易成为性能瓶颈,尤其受IO能力限制
外键缺点:
有性能问题
1.数据库需要维护外键的内部管理;
2.外键等于把数据的一致性事务实现,全部交给数据库服务器完成;
3.有了外键,当做一些涉及外键字段的增,删,更新操作之后,需要触发相关操作去检查,而不得不消耗资源;
4.外键还会因为需要请求对其他表内部加锁而容易出现死锁情况;
优点:
* 由数据库自身保证数据一致性,完整性,更可靠,因为程序很难100%保证数据的完整性,而用外键即使在数据库服务器当机或者出现其他问题的时候,也能够最大限度的保证数据的一致性和完整性
* 有主外键的数据库设计可以增加ER图的可读性,这点在数据库设计时非常重要。
* 外键在一定程度上说明的业务逻辑,会使设计周到具体全面。
外键的使用:
address表中user_id为外键,关联了user表中的id,当删除user中的数据时,如果address表中有关联的user_id数据,那么删除失败;
可以使用`SET FOREIGN_KEY_CHECKS = 0`设置当前连接对于外键关联失效,这时使用当前连接就可以删除user表中的数据。
### count(*),count(1),count(id)?
执行效果:
count(*)包括了所有的列,相当于行数,在统计结果的时候,不会忽略列值为NULL
count(1)包括了忽略所有列,用1代表代码行,在统计结果的时候,不会忽略列值为NULL
count(列名)只包括列名那一列,在统计结果的时候,会忽略列值为空(这里的空不是只空字符串或者0,而是表示null)的计数,即某个字段值为NULL时,不统计
执行效率:
列名为主键,count(列名)会比count(1)快
列名不为主键,count(1)会比count(列名)快
如果表多个列并且没有主键,则 count(1) 的执行效率优于 count(*)
如果有主键,则 select count(主键)的执行效率是最优的
如果表只有一个字段,则 select count(*)最优
### UTF8Bmb4区别?
mb4就是most bytes 4的意思,专门用来兼容四字节的unicode,mysql支持的 utf8 编码最大字符长度为 3 字节,如果遇到 4 字节的宽字符就会插入异常了。而emoji一个表情的长度是4个字节。
### select * 为什么效率低?
例如,有一个表为t(a,b,c,d,e,f),其中,b列有索引,那么,在磁盘上有两棵b+树,即聚集索引和辅助索引,分别保存(a,b,c,d,e,f)和(b,a),如果查询条件中where条件可以通过b列的索引过滤掉一部分记录,查询就会先走辅助索引,如果用户只需要a列和b列的数据,直接通过辅助索引就可以知道用户查询的数据,如果用户select *,获取了不需要的数据,则首先通过辅助索引过滤数据,然后通过聚集索引获取所有的列,这就多了一次b+树查询,速度必然会慢很多。
高级工程师必备知识点(二)