Just Do Java

Java 's Blog


  • 首页

  • 分类

  • 作者

  • 归档

  • 关于

手把手教你给 SQL 做个优化

发表于 2020-08-08 | 分类于 mysql

在开始之前,咱们要知道:如果我的 SQL 语句执行的足够快,还有没有必要去做优化?

完全没有必要对吧

所以我们一般说,要给 SQL 做个优化,那肯定就是这条 SQL 语句执行的比较慢了

那么,为什么它会执行比较慢呢?

SQL 语句执行较慢的 3 个原因

没有建立索引,或者索引失效导致了 SQL 语句执行较慢

这个应该是比较好理解的,如果数据比较多,在千万级别以上,然后呢又没有建立索引,在这千万级别的数据中查找你想要的内容,简直就是在肉搏啊(哎呦,可了不得,竟然敢肉搏

索引失效这块内容说起来就比较多了,比如在查询的时候,让 like 通配符在前面了,比如经常念叨的“最左匹配原则”,又比如我们在查询条件中使用 or ,而且 or 前后条件中有一个列没有索引,等等这些情况都会导致索引失效

锁等待

常用的存储引擎主要有 InnoDB 和 MyISAM 这两种了,前者支持行锁和表锁,后者就只支持表锁

如果数据库操作都是基于表锁的话,意思就是说,现在有个更新操作,就会把整张表锁起来,那么查询的操作都不被允许,所以就不要说提高系统的并发性能了

  • 聪明的你肯定就知道了,既然 MyISAM 只支持表锁,那么使用 InnoDB 不就好了?你以为 InnoDB 的行锁不会升级成表锁嘛? too young too simple !

  • 如果对一张表进行大量的更新操作, mysql 就觉得你这样用会让事务的执行效率降低,到最后还是会导致性能下降,这样的话,还不如把你的行锁升级成表锁呢

  • 还有一点,行锁可是基于索引加的锁,在执行更新操作时,条件索引都失效了,那么这个锁也会执行从行锁升级为表锁

不恰当的 SQL 语句

这个也比较常见了,啥是不恰当的 SQL 语句呢?就比如,明明你需要查找的内容是 name , age ,但是呢,为了省事,直接 select * ,或者在 order by 时,后面的条件不是索引字段,这就是不恰当的 SQL 语句

优化 SQL 语句

在知道了 SQL 语句执行比较慢的原因之后,接下来要做的就是对症下药了

针对 没有索引/索引失效 这块,最有效的办法就是 EXPLAIN 语法了,那你知不知道 Show Profile 也可以嘞

针对 锁等待 这块,没办法了,只能自己多注意

针对 不恰当的 SQL 语句 这块,介绍几个常用的 SQL 优化,比如分页查询怎么优化一下可以查询的更快一些呀,你不是说 select * 不是正确的打开方式嘛?那什么是正确的 select 方式呢?别急嘛,阿粉下面都会说到的

废话不多说,咱们开始了

先来个表

为了确保优化后的结果和我写的一样(起码 90% 是相符的

所以咱们用一样的数据库好不好?乖~

首先建个 demo 的数据库

接下来咱们建表,就建个非常简单的表好不好

1
2
3
4
5
6
CREATE TABLE demo.table(
	id int(11) NOT NULL,
	a int(11) DEFAULT NULL,
	b int(11) DEFAULT NULL,
	PRIMARY KEY(id)
) ENGINE = INNODB

然后插入 10 万条数据

1
2
3
4
5
6
7
8
9
10
11
DROP PROCEDURE IF EXISTS demo_insert;
CREATE PROCEDURE demo_insert()
BEGIN
    DECLARE i INT; 
		SET i = 1;
    WHILE i <= 100000 DO
        INSERT INTO demo.`table` VALUES (i, i, i);
        SET i = i + 1 ;
    END WHILE;
END;
CALL demo_insert();

OK ,准备工作做好了,接下来开始实战

通过 EXPLAIN 分析 SQL 是怎样执行的

只要说 SQL 调优,那就离不开 EXPLAIN

EXPLAIN SELECT * FROM table WHERE id < 100 ORDER BY a;

咱们能够看到有好几个参数:

  • id :每个执行计划都会有一个 id ,如果是一个联合查询的话,这里就会显示好多个 id

  • select_type :表示的是 select 查询类型,常见的就是 SIMPLE (普通查询,也就是没有联合查询/子查询), PRIMARY (主查询), UNION ( UNION 中后面的查询), SUBQUERY (子查询)

  • table :执行查询计划的表,在这里我查的就是 table ,所以显示的是 table, 那如果我给 table 起了别名 a ,在这里显示的就是 a

  • type :查询所执行的方式,这是咱们在分析 SQL 优化的时候一个非常重要的指标,这个值从好到坏依次是: system > const > eq_ref > ref > range > index > ALL

    • system/const :说明表中只有一行数据匹配,这个时候根据索引查询一次就能找到对应的数据

    • eq_ref :使用唯一索引扫描,这个经常在多表连接里面,使用主键和唯一索引作为关联条件时可以看到

    • ref :非唯一索引扫描,也可以在唯一索引最左原则匹配扫描看到

    • range :索引范围扫描,比如查询条件使用到了 < , > , between 等条件

    • index :索引全表扫描,这个时候会遍历整个索引树

    • ALL :表示全表扫描,也就是需要遍历整张表才能找到对应的行

  • possible_keys :表示可能使用到的索引

  • key :实际使用到的索引

  • key_len :使用的索引长度

  • ref :关联 id 等信息

  • rows :找到符合条件时,所扫描的行数,在这里虽然有 10 万条数据,但是因为索引的缘故,所以扫描了 99 行的数据

  • Extra :额外的信息,常见的有以下几种

    • Using where :不用读取表里面的所有信息,只需要通过索引就可以拿到需要的数据,这个过程发生在对表的全部请求列都是同一个索引部分时

    • Using temporary :表示 mysql 需要使用临时表来存储结果集,常见于 group by / order by

    • Using filesort :当查询的语句中包含 order by 操作的时候,而且 order by 后面的内容不是索引,这样就没有办法利用索引完成排序,就会使用”文件排序”,就像例子中给出的,建立的索引是 id , 但是我的查询语句 order by 后面是 a ,没有办法使用索引

    • Using join buffer :使用了连接缓存

    • Using index :使用了覆盖索引

如果对这些参数了解的非常不错,那么 EXPLAIN 这块内容就难不住你了

Show Profile 分析下 SQL 执行性能

通过 EXPLAIN 分析执行计划,只能说明 SQL 的外部执行情况,如果想要知道 mysql 具体是如何查询的,需要通过 Show Profile 来分析

可以通过 SHOW PROFILES; 语句来查询最近发送给服务器的 SQL 语句,默认情况下是记录最近已经执行的 15 条记录,如下图我们可以看到:

我想看具体的一条语句,看到 Query_ID 了嘛?然后运行下 SHOW PROFILE FOR QUERY 82; 这条命令就可以了:

可以看到,在结果中, Sending data 耗时是最长的,这是因为此时 mysql 线程开始读取数据并且把这些数据返回到客户端,在这个过程中会有大量磁盘 I/O 操作

通过这样的分析,我们就能知道, SQL 语句在查询过程中,到底是 磁盘 I/O 影响了查询速度,还是 System lock 影响了查询速度,知道了病症所在,接下来对症下药就容易多了

分页查询怎么可以更快一些

在使用分页查询时,都会使用 limit 关键字

但是对于分页查询,其实还可以优化一步

我这里给出的数据库不是太好,因为它太简单了,看不出来有什么区别,我使用目前项目上正在用的表来做个实验,可以看下区别(使用的 SQL 语句如下面):

1
2
3
4
EXPLAIN SELECT * FROM `te_paper_record` ORDER BY id LIMIT 10000, 20;

EXPLAIN SELECT * FROM `te_paper_record` WHERE id >= ( SELECT id FROM `te_paper_record` ORDER BY id LIMIT 10000, 1) LIMIT 20;

上面一张图片,我没有使用子查询,可以看到执行了 0.033s ,下面的查询语句,我使用了子查询去做优化,能够看到执行了 0.007s ,优化的结果还是很显而易见的

那么,为什么使用了子查询,查询的速度就提上来了呢,这是因为当我们没有使用子查询时,查询到的 10020 行数据都返回回来了,接下来要对这 10020 行数据再进行过滤操作

那可不可以直接就返回需要的 20 行数据呢,这样就不需要再做过滤操作了,直接返回就可以了嘛

你也太聪明了吧。子查询就是在做这件事情

所以查询时间上有了一个很大的优化

正确的 select 打开方式

在查询时,有时为了省事,直接使用 select * from table where id = 1 这样的 SQL 语句,但是这样的写法在一些环境下是会存在一定的性能损耗的

所以最好的 select 查询就是,需要什么字段就查询什么字段

一般在查询时,都会有条件,按照条件查找

这个时候正确的 select 打开方式是什么呢?

如果可以通过主键索引的话, where 后面的条件,优先选择主键索引

为什么呢?这就要知道 MySQL 的存储规则

MySQL 常用的存储引擎有 MyISAM 和 InnoDB , InnoDB 会创建主键索引,而主键索引属于聚簇索引,也就是在存储数据时,索引是基于 B+ 树构成的,具体的行数据则存储在叶子节点

也就是说,如果是通过主键索引查询的,会直接搜索 B+ 树,从而查询到数据

如果不是通过主键索引查询的,需要先搜索索引树,得到在 B+ 树上的值,再到 B+ 树上搜索符合条件的数据,这个过程就是“回表”

很显然,回表能够产生时间。

这也是为什么建议, where 后面的条件,优先选择主键索引

其他调优

看完上面的,心里应该就大概有数了, SQL 调优主要就是建立索引/防止产生锁等待/使用恰当的 SQL 语句去查询

但是,如果问你除了索引,除了上面这些手段,还有没有其他调优方式

啥?竟然还有?!

有的,这就需要跳出来,不要局限在具体的 SQL 语句上了,需要在数据库设计之初就考虑好

比如说,我们常说的要遵循三范式,但是在有的业务场景里面,如果在数据库里面多几个冗余字段的话,可能要比严格遵循三范式带来的性能要好很多。

但是这点就及其考验平时的积累了,阿粉在这里把这一点提出来之后,希望读者们可以看看自己项目上目前用的数据库有没有多余的字段,为什么要这样设计呢?这样多去观察,你的技术能力想不提高都很难

以上,就这样啦~

阅读全文 »

面试官:生产服务器变慢了,你能谈谈诊断思路吗

发表于 2020-08-04 | 分类于 linux

面试官都这么问了,我能说不能吗?

阅读全文 »

公司差点因为代码写得差把阿粉直接给开掉

发表于 2020-08-03 | 分类于 CodeReview

前几天阿粉的公司不是在进行CodeReview吗,而在此期间,不光进行了CodeReview,而且间接的也进行了代码的重构,这时候阿粉就不得自己开始研究这个代码重构这块的事情了,于是砸锅卖铁的买了这个《重构:改善既有的代码设计》这本书,看了几篇文章,确实有一些心得,阿粉也是把自己的心得体会一点点的分享给大家。

阅读全文 »

这是花的最值得的一笔钱了!

发表于 2020-08-02 | 分类于 java

Hello 大家好,我是阿粉,转眼已经到八月份了,好久也没有给大家介绍知识星球的动态了,今天给大家看下星球七月份的一些优质的分享,有专业技术的,有职场生涯的,有干货分享的,也有解答疑惑的,下面我们来看下吧。

阅读全文 »

单例模式

发表于 2020-08-02 | 分类于 java

作为对象的创建模式,单例模式确保其某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类。

阅读全文 »

模版模式

发表于 2020-08-02 | 分类于 java

模版模式又叫模板方法模式,在一个方法中定义算法的骨架,而将一些步骤延迟到子类中去实现。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

阅读全文 »

List 中的元素去重操作

发表于 2020-08-02 | 分类于 java

本文主要介绍List集合中的元素去重操作

阅读全文 »

三次握手和四次挥手说完了,还让我手动写个HTTP协议代码

发表于 2020-07-31 | 分类于 HTTP

最近阿粉的同事们在准备面试,其中也有收到offer的几个不错的人,毕竟疫情稳定了,而阿粉在电话面试的时候,被问到关于HTTP协议的内容的时候,却显得有点麻木了,为什么呢?因为套路太深了,让阿粉猝不及防呀。 <–more–>

面试官:你了解TCP/IP协议么?

说实话在阿粉听到这个问题的时候,阿粉的第一想法就是,我回答了这个问题,接下来肯定还有一个三次握手和四次挥手等着我,但是还是得回答呀,于是阿粉就开始作答了。

阿粉开始作答:TCP/IP协议虽然会放在一起说,但是他们其实呢是属于两个不同的协议。

  • IP协议:

    1
      IP协议实际上是用来查找地址的,而它对应的层级也是网络层,也可以称之为网际互联层,区别不大,叫法不同而已。
    
  • TCP协议:

    1
      TCP协议是用来规范传输规则的,和IP协议是不同的,而它对应的层级是传输层,而这样的话,也就是IP去寻找地址,把所有的传输任务都交给TCP,而TCP这时候就相当于一个快递员的身份出现并且存在。
    

面试官:那你说说什么是三次握手,什么是四次挥手吧

三次握手

大家看这个图,图是来自于百度搜索,而且百度上有各种各样的图,当你看到图的时候第一时间肯定是看不懂的,也就是只能通过这个画的标志的“线”来进行分析,其实这仅仅只是一个方面。

那么我们就来根据图来解析一下这个图中都代表了什么意思,图中存在着两个序号和三个不同的标志位其中有大小写容易混淆的呦。

序号:

  • seq:sequence number 的缩写,直译的话,序号,对没错,它就是序号,你没有翻译错,相信自己,而这个seq表示的则是自己传递的序号,TCP在传输的时候,其中的每一个字节,都会有一个序号,发送数据的时候,会把第一个数据的第一个序号发送给对方,就是我们所看到的第一步,而接收的这一方面,会按照这个序号来检查是否是一个连接完整的数据,如果说你数据是完整的,那么好,我们可以继续下一步,如果你不是完整的,那就重新传送呗,而这样的话也能保证数据的完整性不被破坏。

  • ack:注意,这是小写的ack,也就是acknoledgement number的缩写,而他表示的是确认号,这个要和ACK(确认位)进行区分,接收端这时候用它来给发送端返回成功接收消息的数据信息,而这时候,它的值就是表明,我现在想接收下一个数据包了,而这个值就是下一个数据包的开始的序号,而这个ack所代表的的值的序号前面的数据都已经接收成功了。

  • ACK:确认位,确认位来了,只有当ACK=1的时候ack才会起到自己应该起的作用,而在我们第一次发起请求的时候,因为没有需要我们确认的接收的数据,所以这个时候的ACK就是0,而正常通信的情况下,ACK就1.

  • SYN:同步位,而同步位的作用就是用于建立连接时同步序号,而刚连接的时候,说ACK是0,那么ack就不起作用,这时候SYN就来说,你看没我你们不行了把,要你们有何用,当接收端接收到SYN=1的报文的时候,就会将ack设置为接收到的seq+1的值,这也是大家在看百度上提供的内容的时候看到的,各种seq=k,ACK=k+1,这玩意就是这么来的,这时候ack的值就是根据SYN来直接设置的,这样你才能正常的进行传输,而SYN有时候会被面试官问到为什么在前两次握手的时候都是1呢?其实这是因为传输数据的双方的ack都是要一个初始值的,不然你还怎么传输,还怎么玩。

  • FIN:终止位,这个在本图中,并没有完全的体现,在四次挥手的时候就能完全的体现出来了。而它则是用来在数据传输都完成之后来释放连接的。

那么关于这个图,我们怎么给面试官说呢?

(1) 第一次握手(SYN=1, seq=x):

客户端发送一个 TCP 的 SYN 标志位置1的包,指明客户端打算连接的服务器的端口,以及初始序号 X,保存在包头的序列号(Sequence Number)字段里。

发送完毕后,客户端进入 SYN_SEND 状态。

(2) 第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1):

服务器发回确认包(ACK)应答。即 SYN 标志位和 ACK 标志位均为1。服务器端选择自己 ISN 序列号,放到 Seq 域里,同时将确认序号(Acknowledgement Number)设置为客户的 ISN 加1,即X+1。 发送完毕后,服务器端进入 SYN_RCVD 状态。

(3) 第三次握手(ACK=1,ACKnum=y+1)

客户端再次发送确认包(ACK),SYN 标志位为0,ACK 标志位为1,并且把服务器发来 ACK 的序号字段+1,放在确定字段中发送给对方,并且在数据段放写ISN的+1

发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手结束。

你如果这么说,面试官有可能还会问,你这也太官方了,能不能说说你的理解,那么你可以用一个实际上的例子来给他说一下,

阿粉:鸡丁,嘿,我是阿粉,你听的到我说话么?

鸡丁:吵吵啥,听到了,除了你我还能认识谁。

阿粉:你听的到你还不赶紧回复,怪不得你没有女朋友呢。那我们再继续交流一下吧。

而这三次的对话过程就是通俗的三次握手,期间对话三次,以此来确定两个方向上的数据传输通道是否正常。

四次挥手

那么四次挥手怎么来回答呢?

(1)第一次挥手(FIN=1,seq=x)

假设客户端想要关闭连接,客户端发送一个 FIN 标志位置为1的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据。

发送完毕后,客户端进入 FIN_WAIT_1 状态。

(2) 第二次挥手(ACK=1,ACKnum=x+1)

服务器端确认客户端的 FIN 包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。

发送完毕后,服务器端进入 CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态,等待服务器端关闭连接。

(3) 第三次挥手(FIN=1,seq=y)

服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN 置为1。

发送完毕后,服务器端进入 LAST_ACK 状态,等待来自客户端的最后一个ACK。

(4) 第四次挥手(ACK=1,ACKnum=y+1)

客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待可能出现的要求重传的 ACK 包。

服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。

客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态。两次后会重传直到超时。如果多了会有大量半链接阻塞队列。

那么怎么去通俗的给面试官说呢?

阿粉:鸡丁呀,我要说的都说完了,你还有啥事么?

鸡丁:你说的我都明白了,但是别断,我还有要嘱咐你的,给我找女朋友的事情。

鸡丁:xxxxx,我说完了。

阿粉,行啦,别BB了,记住了,挂了把。

如果面试官问你的时候,你这么回答的话,既有官方的解释,还有本身自己的理解,那么这个问题就已经算是差不多了,

而面试官显然不可能会这么放过你,肯定再给你来个雷,为啥是三次握手,而是四次挥手呢?为啥不是三次呢?

这是因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方是否现在关闭发送数据通道,需要上层应用来决定,因此,己方ACK和FIN一般都会分开发送。所以这时候挥手的时候就是四次,而不再是三次了。

那么我们怎么去手写一个HTTP协议呢?代码送上:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
public class Server {
    public static void main(String[] args) throws Exception{
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.socket().bind(new InetSocketAddress(8080));
        ssc.configureBlocking(false);
        Selector selector = Selector.open();
        ssc.register(selector, SelectionKey.OP_ACCEPT);
        while (true){
            if (selector.select(3000)==0){
                continue;
            }
            Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
            while (keyIterator.hasNext()){
                SelectionKey key = keyIterator.next();
                new Thread(new HttpHandler(key)).run();
                keyIterator.remove();
            }
        }
    }
    private static class HttpHandler implements Runnable{

        private int bufferSize = 1024;
        private String localCharset = "UTF-8";
        private SelectionKey key;
        public HttpHandler(SelectionKey key){
            this.key=key;
        }

        public void handleAccept()throws IOException{
            SocketChannel clientChannel = ((ServerSocketChannel)key.channel()).accept();
            clientChannel.configureBlocking(false);
            clientChannel.register(key.selector(),SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));
        }

        @Override
        public void run() {
            try {
                if (key.isAcceptable()){
                    handleAccept();
                }
                if (key.isReadable()){
                    handleRead();
                }
            }catch (IOException e){
               e.printStackTrace();
            }
        }

        public void handleRead() throws IOException{
            SocketChannel sc = (SocketChannel) key.channel();
            ByteBuffer buffer = (ByteBuffer) key.attachment();
            buffer.clear();
            if (sc.read(buffer)==-1){
                sc.close();
            }else {
                buffer.flip();
                String receiveString = Charset.forName(localCharset).newDecoder().decode(buffer).toString();
                String[] requestMessage = receiveString.split("\r\n");
                for (String s:requestMessage) {
                    System.out.println(s);
                    if (s.isEmpty()){
                        break;
                    }
                    String[] firstLine = requestMessage[0].split(" ");
                    System.out.println();
                    System.out.println("Method:\t"+firstLine[0]);
                    System.out.println("url:\t"+firstLine[1]);
                    System.out.println("HTTP Version:\t"+firstLine[2]);
                    System.out.println();

                    StringBuffer sendString = new StringBuffer();
                    sendString.append("HTTP/1.1 200 OK\r\n");
                    sendString.append("Content-Type:text/html;charset="+localCharset+"\r\n");
                    sendString.append("\r\n");
                    sendString.append("<html><head><title>显示报文</title></head><body>");
                    sendString.append("接受到的请求报文是:<br/>");
                    for (String s1:requestMessage) {
                        sendString.append(s1+"<br/>");
                    }
                    sendString.append("</body></html>");
                    buffer = ByteBuffer.wrap(sendString.toString().getBytes(localCharset));
                    sc.write(buffer);
                    sc.close();
                }
            }

        }

    }
}

这这是一个简单的实现,只是实现思路,并不是真正的处理请求,而大家也要注意设置Content-Type的类型,不然容易出问题的,毕竟长度是有限制的。 文章参考 《HTTP协议详解》 《TCP/IP协议详解》

阅读全文 »

SpringBoot2.x 整合 邮件发送

发表于 2020-07-27 | 分类于 SpringBoot

在实际项目中,经常需要用到邮件通知功能。比如,用户通过邮件注册,通过邮件找回密码等;又比如通过邮件发送系统情况,通过邮件发送报表信息等等,实际应用场景很多。

正常我们会用 JavaMail 相关 api 来写发送邮件的相关代码,但现在 SpringBoot 提供了一套更简易使用的封装。这篇文章,阿粉就带大家通过 SpringBoot 快速的实现发送邮件的功能。

阅读全文 »

那些你应该知道的,但是你一定不知道的 Git 骚操作

发表于 2020-07-24 | 分类于 java

Hello 大家好,我是阿粉,作为团队中的主程阿粉经常参与很多核心功能的开发,而且很多时候一个需求没做好中间又插入新的紧急的需求或者 bug 修复,每次遇到这种情况,如果两个地方代码不冲突的话还好,可以直接在本分支修改然后提交,但是当遇到需要修改同一个类文件的时候就比较麻烦了。这种情况如何优雅的处理呢?让阿粉来带你了解 Git 的高级骚操作!

阅读全文 »

面试官:假如说我们现在要做一个千万级用户量网站,你怎么设计高并发架构?

发表于 2020-07-24 | 分类于 面试

之前的时候,阿粉一直在看同事面试,但是呢,阿粉并没有自己去面试,而无意间打开Boss的时候,发现一家公司私聊了我,我回复了一下之后,竟然说我可以去面试,不曾想,面试一个问题,让我的薪资瞬间被砍掉了5K,你如果不想自己出去要的薪资被砍,那么你要会设计这个。 <–more–>

一般的普通项目架构

像阿粉的朋友所在的公司属于一个中小型的公司,公司的项目都是按照客户的要求来定制进行开发的,而服务器的数量那是少的可怜,什么高并发,不考虑,什么高可用,也不考虑,一台服务器上面部署了自己的项目,有时候连个图片服务器都没有,他们的图就是这个样子的。

是不是看着很简陋的样子,直接浏览器客户端和单一服务器之间进行交流了,如果访问人数在前人以上,比如秒杀个限量款,那估计可能直接就凉了,“Boom”的一下就访问失败了。

稍微进阶版本的项目架构

这个时候一般网站架构还是采用单体架构,但是服务器也相对的增加了,终于增加了数据库服务器和应用类服务器在加上图片服务器,算是组成了一个小小的进阶版本的项目架构。

也就是说我们在部署应用的时候,手动把应用服务器上的Tomcat给关掉,然后替换系统的代码war包,接着重新启动Tomcat。

这时候把数据单独的部署在另外的一个服务器上面,存放网站的全部核心数据。

然后在另外一台独立的服务器上部署NFS作为图片服务器,存放网站的全部图片。

这时候呢,代码执行的是请求,数据访问在另外一台服务器,而图片在另外一台服务器上面,这样就通过增加了服务器来部署的普通版本的项目架构就完成了,而大部分的项目都是这个样子的吧,毕竟小公司人力有限呀。

简单的支持高并发,高可用的项目架构

不知道大家在面试的时候说这个的时候面试官有没有问过,如果你们的应用服务器挂了,你们怎么处理?

是的,应用服务器真的会出事,那在项目的应用服务器真的出事之后,我们怎么处理,在换个服务器,那么你很多想对应的配置就要做出更改,需要更改一大堆的东西,那么你会有很长时间的维护期,维护代价很高了呀。

这时候我们就出现了这个:

如果说我们有一台其中的应用服务器宕机了,不干活了,那么接下来,另外的一台服务器就会正常的使用,并且,在同时使用的时候,我们还能够把从浏览器来的大量的请求分发一下,直接对半劈成2份,如果说其中有一个服务器的配置“高的飞起”,那么我们还可以通过配置,给他的请求让他多一点,“设置权重”,这样让他分担更多的压力,而那个比较 low 的服务器的话,承担的相应的请求就会减少一点,毕竟性能比较低。

而在这个时候还会出现另外一种意外,那就是数据库服务器宕机了,怎么办?相同的原理,增加另外的一个数据库服务器,也就是主从数据库。

那么为什么要做主从复制,不单单是因为这一个服务器宕机的问题,还有如果对数据库的读和写都在同一个数据库服务器中操作,业务系统性能会降低。

而我们很多的项目在数据上面需要的就是比较重要的,就很多就是做读写分离,而为了提升业务系统性能,优化用户体验,可以通过做主从复制(读写分离)来减轻主数据库的负载。

一台用于写入数据,一台用于同步主的数据并用于数据查询操作。

配置好主从复制之后,同一张表,只能对一个服务器写操作。

业务分离

现在很多项目的架构都是所有人的业务代码都写在了一起,乱七八糟的,好几个人的代码,维护起来那叫一个崩溃,当你看到这样的项目的时候,你就会发现,人都傻了,这是个什么鬼,改动一点,其他的不好用了,那叫一个崩溃呀。

这时候我们就需要把我们的业务彻底的做出分割,比如商城里面的,订单属于一块的业务,积分属于一部分的业务,物流属于另外一部分的业务,这时候我们就需要分成三块的业务模块。

也就是相当于每一块的内容都属于一个服务,而这些服务叠加起来可就不是1+1=2的问题了,这些业务服务相加起来,这时候完整的项目和你把所有的内容同一写到一个部分的内容差距是非常大的,尤其是在代码冗余方面,做的那是相当透彻的。

而说到这给内容的话,其实已经算差不多了,但是再更深的阿粉是真的没有了解到那么多,而阿粉之前没想过这些内容会在你面试的时候问到,可能阿粉当时只是想自己做个咸鱼,不去关心架构方面的事情,而架构方面的事情确实很多公司很看重的一点。

在这里阿粉给大家再次贡献出此次面试的其他的问题,并且给出详细的解答,并且阿粉也是很欣慰的,只是压了工资,而没有说直接拒绝。

1.Redis分区实现方案

  • 客户端分区:

客户端分区就是在客户端就已经决定数据会被存储到哪个redis节点或者从哪个redis节点读取。大多数客户端已经实现了客户端分区。

  • 代理分区:

代理分区 意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些Redis实例,然后根据Redis的响应结果返回给客户端。

  • 查询路由:

查询路由(Query routing) 的意思是客户端随机地请求任意一个redis实例,然后由Redis将请求转发给正确的Redis节点。Redis Cluster实现了一种混合形式的查询路由,但并不是直接将请求从一个redis节点转发到另一个redis节点,而是在客户端的帮助下直接redirected到正确的redis节点。

这些都是阿粉死记硬背背下来的,关于这种东西,没有实际亲身实践过的,没有遇到过的问题的,都是属于被diss的,但是面试官好像理解也不是很深,也是知道,但是具体的也没有仔细的深挖我这块的内容。

2.SpringBean的生命周期

说实话,这个问题,面试好像现在都是必问的知识点了,阿粉的同事面试也是被问,但是巧了,之前阿粉就写过关于SpringBean的生命周期的,文章链接给大家送上,大家可以仔细查看,可以共同交流呦。

面试官:兄弟你来阐述一下Spring框架中Bean的生命周期?

所以大家一定要把这个准备好呦,SpringBean的生命周期,从初始化到最终的销毁中间经历了什么,过程是什么,流程怎么理解。

3.HashMap 和 Hashtable

这个问题我在回答的时候我就分开了,分门别类的比较比如说:

  • 线程安全

  • 性能优劣

  • NULL

  • 实现方式

  • 容量扩容

我分成了五块的内容作答,如果说你只能够刚刚想起来其中的三到四点也是不错的,至少比你只会说他们的线程安全和是否允许键为空来的好很多,如果大家有兴趣,请寻找集合系列文章,在过往的历史当中寻找一下,有很多关于HashMap 和 Hashtable的面试点。

4.JVM内存模型和优化

这个我相信只要是工作了两到三年的程序员肯定都会,我就给大家简单说说,之前的文章图解,

关于两篇文章通俗易懂,简单明了,几次面试,回答没有遗漏,面试官觉得还行,文章连接送上:

你还在为了JVM而烦恼么?(内存结构和垃圾回收算法)

老年代的垃圾回收算法

关于其他的问题,都不是比较经典的,都是属于项目业务中的了,阿粉就不再一一给大家介绍了,总结起来就一句话,基础你要掌握,扩展你更要会,不然面试想要高工资?那是不可能的,毕竟不是每家公司都缺“大爷”,不是么?

阿粉在这里希望大家找到自己理想的工作,等疫情稳定了,直接跳槽工资“Double”。

阅读全文 »

面试不说点分布式的东西,面试官都有点看不起我呀

发表于 2020-07-19

现在的面试和几年前的面试差距很大了,现在培训机构出来的同学很多都占一大部分,而且那张口要的可是真的有点多,甚至比一些工作两三年的朋友们还多,不排除有一些大牛的存在,但是还是有一些不是很给力的,但是想一口吃撑的,而有经验的面试官,分分钟就能看出来你到底是有工作经验还是没有工作经验的。而分布式中的内容,将会是极其重要的一点内容。

阅读全文 »

分享几个 Markdown 写作软件 Typora 的骚操作

发表于 2020-07-18 | 分类于 java

Hello 大家好,我是阿粉,Markdown 语法想必很多写作的朋友都了解(不了解的可以网上找找,绝对是写作装逼必备神器),Typora 软件必定是 Markdown 写作的一大利器(阿粉不接受反驳),今天阿粉给大家分享一下 Typora 的几个骚操作,我们来一起看下吧。

阅读全文 »

不藏着掖着了,阿粉的各种利器都在这儿了

发表于 2020-07-15 | 分类于 nginx

全是阿粉想要推荐给你的

阅读全文 »

阿粉一个循环引起的生产事故

发表于 2020-07-15 | 分类于 java

Hello 大家好,我是阿粉,工作这么多年虽然经历过风风雨雨,但是每次线上发布版本的时候都是一场硬战,这不最近发布了一个版本,一不小心写了个 bug,差点造成了生产事故,幸好运维老大发现及时。

love2

阅读全文 »

手把手教你在 CentOS7 上搭建 Nginx

发表于 2020-07-14 | 分类于 nginx

是阿粉用了心写的搭建教程

阅读全文 »

Java容器集合经典面试题集

发表于 2020-07-13 | 分类于 java容器集合

本文总结了Java集合容器的经典面试题,所有题目阿粉都给出了自己思考,适合面试前复习扫盲使用。阿粉不能保证里面包含了所有集合面试题,但只要认真深挖好每一道题,做到触类旁通,就能以不变应万变。

  • 大纲:
  • 概述型面试题
  • List
  • Map
  • 小结

概述类面试题

1. 请说一下Java容器集合的分类,各自的继承结构

  • Java中的容器集合分为两大阵营,一个是Collection,一个是Map
  • Collection下分为Set,List,Queue
  • Set的常用实现类有HashSet,TreeSet等
  • List的常用实现类有ArrayList,LinkedList等
  • Queue的常用实现类有LinkedList,ArrayBlockingQueue等
  • Map下没有进一步分类,它的常用实现类有HashMap,ConcurrentHashMap等

能把上面的基本框架答出来基本就没问题了,对于各种类型我只列举了一些实际工作中常用的实现类。但其实在Set,List和Queue下还有更细的划分,如果想要在面试时表现一番,那得对着JDK好好背一背了>_<

2. 请谈一谈Java集合中的fail-fast和fail-safe机制

fail-fast是一种错误检测机制,Java在适合单线程使用的集合容器中很好地实现了fail-fast机制,举一个简单的例子:在多线程并发环境下,A线程在通过迭代器遍历一个ArrayList集合,B线程同时对该集合进行增删元素操作,这个时候线程A就会抛出并发修改异常,中断正常执行的逻辑。

而fail-safe机制更像是一种对fail-fast机制的补充,它被广泛地实现在各种并发容器集合中。回头看上面的例子,如果线程A遍历的不是一个ArrayList,而是一个CopyOnWriteArrayList,则符合fail-safe机制,线程B可以同时对该集合的元素进行增删操作,线程A不会抛出任何异常。

要理解这两种机制的表象,我们得了解这两种机制背后的实现原理:

我们同样用ArrayList解释fail-fast背后的原理:首先ArrayList自身会维护一个modCount变量,每当进行增删元素等操作时,modCount变量都会进行自增。当使用迭代器遍历ArrayList时,迭代器会新维护一个初始值等于modCount的expectedModCount变量,每次获取下一个元素的时候都会去检查expectModCount和modCount是否相等。在上面举的例子中,由于B线程增删元素会导致modCount自增,当A线程遍历元素时就会发现两个变量不等,从而抛出异常。

CopyOnWriteArrayList所实现的fail-safe在上述情况下没有抛出异常,它的原理是:当使用迭代器遍历集合时,会基于原数组拷贝出一个新的数组(ArrayList的底层是数组),后续的遍历行为在新数组上进行。所以线程B同时进行增删操作不会影响到线程A的遍历行为。

这种题目我觉得要先答出核心原理,如果你对多线程和单线程下容器的使用有自己的见解,可以考虑多聊点。

3. 如何一边遍历一边删除Collection中的元素?

使用集合迭代器自身的remove方法进行删除

1
2
3
4
5
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
   *// do something*
   it.remove();
}

可能笔试考的更多,算是Java的基本常识吧

List类面试题

4. 谈谈ArrayList和LinkedList的区别

本质的区别来源于两者的底层实现:ArrayList的底层是数组,LinkedList的底层是双向链表。

数组拥有O(1)的查询效率,可以通过下标直接定位元素;链表在查询元素的时候只能通过遍历的方式查询,效率比数组低。

数组增删元素的效率比较低,通常要伴随拷贝数组的操作;链表增删元素的效率很高,只需要调整对应位置的指针即可。

以上是数组和链表的通俗对比,在日常的使用中,两者都能很好地在自己的适用场景发挥作用。

比如说我们常常用ArrayList代替数组,因为封装了许多易用的api,而且它内部实现了自动扩容机制,由于它内部维护了一个当前容量的指针size,直接往ArrayList中添加元素的时间复杂度是O(1)的,使用非常方便。

而LinkedList常常被用作Queue队列的实现类,由于底层是双向链表,能够轻松地提供先入先出的操作。

我觉得可以分两部分答,一个是数组与链表底层实现的不同,另一个是答ArrayList和LinkedList的实现细节。

5. 谈谈ArrayList和Vector的区别

两者的底层实现相似,关键的不同在于Vector的对外提供操作的方法都是用synchronized修饰的,也就是说Vector在并发环境下是线程安全的,而ArrayList在并发环境下可能会出现线程安全问题。

由于Vector的方法都是同步方法,执行起来会在同步上消耗一定的性能,所以在单线程环境下,Vector的性能是不如ArrayList的

除了线程安全这点本质区别外,还有一个实现上的小细节区别:ArrayList每次扩容的大小为原来的1.5倍;Vector可以指定扩容的大小,默认是原来大小的两倍。

感觉可以顺带谈谈多线程环境下ArrayList的替代品,比如CopyOnWriteArrayList,但是要谈谈优缺点。

6. 为什么ArrayList的elementData数组要加上transient修饰

由于ArrayList有自动扩容机制,所以ArrayList的elementData数组大小往往比现有的元素数量大,如果不加transient直接序列化的话会把数组中空余的位置也序列化了,浪费不少的空间。

ArrayList中重写了序列化和反序列化对应的writeObject和readObject方法,在遍历数组元素时,以size作为结束标志,只序列化ArrayList中已经存在的元素。

细节题

Map类面试题

HashMap死亡连环Call即将来临,看爽了记得点个赞啊

7. 请介绍一下HashMap的实现原理

  1. 我们一般用HashMap存储key-value类型的数据,它的底层是一个数组,当我们调用put方法的时候,首先会对key进行计算得出一个hash值,然后根据hash值计算出存放在数组上的位置
  2. 这个时候我们会遇到两种情况:一是数组上该位置为空,可以直接放入数据;还有一种情况是该位置已经存放值了,这就发生了哈希冲突。
  3. 在现在使用较为普遍的JDK1.8中是这样处理哈希冲突的:先用链表把冲突的元素串起来,如果链表的长度达到了8,并且哈希表的长度大于64,则把链表转为红黑树。(在JDK1.7中没有转化为红黑树这一步,只用链表解决冲突)

先热身

8. HashMap是怎样确定key存放在数组的哪个位置的?

JDK1.8

首先计算key的hash值,计算过程是:先得到key的hashCode(int类型,4字节),然后把hashCode的高16位与低16位进行异或,得到key的hash值。

接下来用key的hash值与数组长度减一的值进行按位与操作,得到key在数组中对应的下标。

追问:为什么计算key的hash时要把hashCode的高16位与低16位进行异或?(变式:为什么不直接用key的hashCode)

计算key在数组中的下标时,是通过hash值与数组长度减一的值进行按位与操作的。由于数组的长度通常不会超过2^16,所以hash值的高16位通常参与不了这个按位与操作。

为了让hashCode的高16位能够参与到按位与操作中,所以把hashCode的高16位与低16位进行异或操作,使得高16位的影响能够均匀稀释到低16位中,使得计算key位置的操作能够充分散列均匀。

9. 为什么要把链表转为红黑树,阈值为什么是8?

在极端情况下,比如说key的hashCode()返回的值不合理,或者多个密钥共享一个hashCode,很有可能会在同一个数组位置产生严重的哈希冲突。

这种情况下,如果我们仍然使用使用链表把多个冲突的元素串起来,这些元素的查询效率就会从O(1)下降为O(N)。为了能够在这种极端情况下仍保证较为高效的查询效率,HashMap选择把链表转换为红黑树,红黑树是一种常用的平衡二叉搜索树,添加,删除,查找元素等操作的时间复杂度均为O(logN)

至于阈值为什么是8,这是HashMap的作者根据概率论的知识得到的。当key的哈希码分布均匀时,数组同一个位置上的元素数量是成泊松分布的,同一个位置上出现8个元素的概率已经接近千分之一了,这侧面说明如果链表的长度达到了8,key的hashCode()肯定是出了大问题,这个时候需要红黑树来保证性能,所以选择8作为阈值。

追问:为什么红黑树转换回链表的阈值不是7而是6呢?

如果是7的话,那么链表和红黑树之间的切换范围值就太小了。如果我的链表长度不停地在7和8之间切换,那岂不是得来回变换形态?所以选择6是一种折中的考虑。

10. 请说一下HashMap的扩容原理

  1. 首先得到新的容量值和新的扩容阈值,默认都是原来大小的两倍。
  2. 然后根据新容量创建新的数组
  3. 最后把元素从旧数组中迁移到新数组中

在JDK1.7中,迁移数据的时候所有元素都重新计算了hash,并根据新的hash重新计算数组中的位置。

在JDK1.8中,这个过程进行了优化:如果当前节点是单独节点(后面没有接着链表),则根据该节点的hash值与新容量减一的值按位与得到新的地址。

如果当前节点后面带有链表,则根据每个节点的hash值与旧数组容量进行按位与的结果进行划分。如果得到的值为0,这些元素会被分配回原来的位置;如果得到的结果不为0,则分配到新位置,新位置的下标为当前位置下标加上旧数组容量。

还有一种情况是当前节点是树节点,那么会调用一个专门的拆分方法进行拆分。

追问:为什么HashMap不支持动态缩容?

开放性题目?以下是个人见解:

如果要支持动态缩容,可能就要把缩容安排在remove方法里,这样可能会导致remove方法的时间复杂度从O(1)上升为O(N)。

还有一点可能和我们编写Java代码的习惯有关:由于Java有自动垃圾回收机制,让我们得以可劲地new对象,Java也默认了我们这种吃饭不收拾盘子的行为。既然对象会被回收,HashMap动态缩容在这样的大环境下似乎就显得没那么重要了,这可以说是一种空间换时间的策略吧。

11. 为什么HashMap中适合用Integer,String这样的基础类型作为key?

因为这些基础类内部已经重写了hashCode和equals方法,遵守了HashMap内部的规范。

追问:如果要用我们自己实现的类作为key,要注意什么?

一定要重写hashCode()和equals()方法,而且要遵从以下规则:

equals()是我们判断两个对象是否相同的依据,如果我们重写了equals方法,用自己的逻辑去判断两个对象是否相同,那么一定要保证:

两个equals()返回true的对象,一定要返回相同的hashCode。

这样,在HashMap的put方法中才能正确判断key是否相同。

不是经常有一个问题嘛,两个对象hashCode相同,equals一定返回true吗?答案肯定是否的,这和你的设计密切相关:如果在你的编程思路中这两个对象是不同的,那么就算恰巧两个对象的hashCode相同,equals也应该返回false。

12. 为什么HashMap数组的长度是2的幂次方?

因为这样能够提高根据key计算数组位置的效率。

HashMap根据key计算数组位置的算法是:用key的hash值与数组长度减1的值进行按位与操作。

在我们正常人的思维中,获取数组的某个位置最直接的方法是对数组的长度取余数。但是如果被除数是2的幂次方,那么这个对数组长度取余的方法就等价于对数组长度减一的值进行按位与操作。

在计算机中,位运算的效率远高于取模运算,所以为了提高效率,把数组的长度设为2的幂次方。

13. HashMap与HashTable有什么区别?

在JDK1.7之前,两者的实现极为相似,最大的区别在于HashTable的方法都用synchronized关键字修饰起来了,表明它是线程安全的。

但是由于直接在方法上加synchronized关键字的同步效率较低,在并发情况下,官方推荐我们使用ConcurrentHashMap。

所以我们看到在JDK1.8中,官方甚至没有对HashTable进行链表转树这样的优化,HashTable已经不被推荐使用了。

14. 请说一下ConcurrentHashMap的实现原理

在JDK1.7中ConcurrentHashMap采用了一种分段锁的机制,它的底层实现是一个segment数组,每个segment的底层结构和HashMap相似,也是数组加链表。

当对segment里面的元素进行操作之前,需要获得该segment独有的一把ReentrantLock。ConcurrentHashMap如果不进行手动设置的话,默认有16个segment,可以支持16个线程对16个不同的segment进行并发写操作。

在JDK1.8之后摒弃了segment这种臃肿的设计,新的实现和HashMap非常相似,底层用的也是数组加链表加红黑树。

在新实现中,在put方法里使用了CAS + synchronized进行同步。如果插入元素的位置为空,则使用CAS进行插入。如果插入的位置不为空,则对当前位置的对象进行加锁,也就链表或红黑树的头节点,加锁后再进行后续的插入操作。

这样设计的好处是:

  1. CAS是十分轻量的加锁操作,如果能够直接插入,用CAS能够大幅度节省加锁的开销。
  2. 如果发生冲突,只用锁住当前位置的头结点,理论上数组的长度有多大,并发操作的线程数就能有多少,比原来只能有16个线程效率更高。

这道题如果想深挖扩展可以开始往Java多线程并发方面扯:synchronized,CAS。Java多线程方面我也会出一份总结,有兴趣的不妨先点赞关注一波

小结

我感觉面试的时候对集合的考察会偏向实现原理多一些,所以一定要看一遍源码,相比于框架的源码,集合的源码简直太友好了。在笔试的时候可能还会考一些集合的使用,比如遍历,排序,比较等等,这些算是Java基础了,用得多也就熟了。

最后如果你觉得阿粉的回答有问题,欢迎指正!

阅读全文 »

阿粉带你3分钟看完关于树的故事

发表于 2020-01-17 | 分类于 数据结构

在计算机中,树随处可在,可说是图论和计算机科学中的重中之重,理解树的结构、树的思想和树的优异性质对于程序设计大有裨益!

阅读全文 »

集合知识全系列回顾

发表于 2020-01-17 | 分类于 集合系列

实际开发中,经常用到的 ArrayList、LinkedList、HashMap、LinkedHashMap 等集合类,其实涵盖了很多数据结构和算法,每个类可以说都是精华,今天阿粉想和大家一起来梳理一下!

阅读全文 »

并发神器之 CopyOnWriteArrayList

发表于 2020-01-17 | 分类于 集合系列

相信大家对 ConcurrentHashMap 这个线程安全类非常熟悉,但是如果我想在多线程环境下使用 ArrayList,该怎么处理呢?阿粉今天来给你揭晓答案!

阅读全文 »
1 … 11 12 13 … 27
Java Geek Tech

Java Geek Tech

一群热爱 Java 的技术人

539 日志
114 分类
24 作者
RSS
GitHub 知乎
Links
  • 纯洁的微笑
  • 沉默王二
  • 子悠
  • 江南一点雨
  • 炸鸡可乐
  • 郑璐璐
  • 程序通事
  • 懿
© 2019 - 2021 Java Geek Tech
由 Jekyll 强力驱动
主题 - NexT.Mist