java高频面试

595 阅读1小时+

MyISAm与Innodb对比

image.png

mysql的执行顺序

from
	<left_table>
on <join_condition>
<join_type> join <right_table>
where
	<where_condition>
group by
	<group_by_list>
having
	<having_condition>
select
distinct
	<select_list>
order by
	<order_by_condition>
limit
	<limit_number>

image.png

B树与B+树的区别

B树的特点: 每个节点都有数据和指针

 

节点排序

一个节点可以存多个元索,多个元索也排序了

每个节点都存储key和data

B+树的特点: 数据只存在于叶子节点

拥有B树的特点

非叶子节点上的都是索引指针,叶子节点中存储了所有的元素与索引,并且排好了顺序

叶子节点之间有指针

增加了顺序访问指针 ,每个叶子节点增加一个指向相邻叶子节点的指针

只有叶子节点存储data,叶子节点包含了这棵树的所有索引和数据

数据读取的操作类型

select_type是查询的类型

用于区分普通查询、联合查询、子查询等的复杂查询

  • simple

    • 简单的select查询,不包含子查询或者union
  • primary

    • 查询中若包含任何复杂的子部分,最外层的查询将标记为PRIMARY
  • subquery

    • select或where后跟的子查询
  • derived

    • 在from列表中包含的子查询衍生出的临时虚拟表将标记为DERIVED
  • union

    • 若第二个select出现在union之后,则被标记为UNION
    • 若UNION包含在from子句的子查询中,外层select将被标记为DERIVED
  • union result

    • 从UNION表获取结果的select

type是访问类型

显示查询用了哪种类型

从最好到最差依次是

system>const>eq_ref>ref>range>index>ALL(ALL就是全表扫描)

一般来说,至少保证查询要在range级别,最好能达到ref

  • system

    • 表只有一行记录(等于系统表),这是const类型的特列,平时不会出现,这个也可以忽略不计
  • const

    • **单表查询情况下通过索引一次就找到了,**const用于比较primary key主键或者unique聚簇索引。
    • 因为只匹配一行数据,所以很快如将主键置于where列表中,MySQL就能将该查询转换一个常量
  • eq_ref

    • 多表查询的情况下的唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配
    • 常见于主键或唯一索引扫描的联合查询
  • ref

    • 非唯一性索引扫描,返回匹配某个单独值的所有行
    • 本质上也是一种索引访问,它返回所有匹配某个单独值的行
    • 它可能会找到多个符合条件的行,所以他应该属于查找和扫描的混合体
  • range

    • 只检索给定范围的行,使用一个索引来选择行。key列显示使用了哪个索引
    • 一般就是在where语句中出现了between、<、>、in等的查询
    • 这种范围扫描索引扫描比全索引扫描要好,因为它只需要开始于索引的某一点, 而结束语另一点,不用扫描全部索引(前提是加了索引)
  • index

    • Full Index Scan, index 与ALL区别为index类型只遍历索引树
    • 这通常比ALL快,因为索引文件通常比数据文件小
    • 也就是说虽然all和Index都是读全表,但index是从索引中读取的,而all是从硬盘中读的
  • ALL

    • 遍历全表以找到匹配的行

应不应该创建索引

哪些情况需要创建

  • 主键自动创建唯一索引,默认命名为PRIMARY
  • 频繁作为查询条件的字段
  • 与其他表关联,作为查询条件的外键字段
  • 单键索引/复合索引?高并发下推荐复合索引
  • 查询中排序的字段,排序字段通过索引访问将提高排序速度
  • 查询中统计或分组的字段,group by也要排序,所以也推荐建立索引

哪些情况不需要创建

  • 表记录太少,建了也没效果,可能引起索引失效
  • 频繁更新的字段,更新字段的同时也会更新索引,消耗性能、降低效率
  • where条件中不使用的字段
  • 经常增删改的表
  • 包含许多重复内容的数据列,建立索引没有太大的效果

1. MySQL 主从同步

1.1 什么是 MySQL 主从 ?

所谓 MySQL 主从,就是建立两个完全一样的数据库,一个是主库,一个是从库,主库对外提供读写的操作,从库对外提供读的操作

1.2 为什么使用 MySQL 主从 ?

对于数据库单机部署,在 4 核 8G 的机器上运行 MySQL 5.7 时,大概可以支撑 500 的 TPS 和 10000 的 QPS,当遇到一些活动时,查询流量骤然,就需要进行主从分离。

大部分系统的访问模型是读多写少,读写请求量的差距可能达到几个数量级,所以我们可以通过一主多从的方式,主库只负责写入和部分核心逻辑的查询,多个从库只负责查询,提升查询性能,降低主库压力。

当主库宕机时,从库可以切成主库,保证服务的高可用,然后主库也可以做数据的容灾备份,整体场景总结如下:

  • 读写分离:从库提供查询,减少主库压力,提升性能;
  • 高可用:故障时可切换从库,保证服务高可用;
  • 数据备份:数据备份到从库,防止服务器宕机导致数据丢失。

主从复制

2.1 主从复制原理

MySQL 的主从复制是依赖于 binlog,也就是记录 MySQL 上的所有变化并以二进制形式保存在磁盘上二进制日志文件。

主从复制就是将 binlog 中的数据从主库传输到从库上,一般这个过程是异步的,即主库上的操作不会等待 binlog 同步地完成。

详细流程如下:

  1. 主库写 binlog:主库的更新 SQL(update、insert、delete) 被写到 binlog;
  2. 主库发送 binlog:主库创建一个 log dump 线程来发送 binlog 给从库;
  3. 从库写 relay log:从库在连接到主节点时会创建一个 IO 线程,以请求主库更新的 binlog,并且把接收到的 binlog 信息写入一个叫做 relay log 的日志文件;
  4. 从库回放:从库还会创建一个 SQL 线程读取 relay log 中的内容,并且在从库中做回放,最终实现主从的一致性。

2.2 如何保证主从一致

当主库和从库数据同步时,突然中断怎么办?因为主库与从库之间维持了一个长链接,主库内部有一个线程,专门服务于从库的这个长链接。

对于下面的情况,假如主库执行如下 SQL,其中 a 和 create_time 都是索引:

delete from t where a > '666' and create_time<'2022-03-01' limit 1;
复制代码

我们知道,数据选择了 a 索引和选择 create_time 索引,最后 limit 1 出来的数据一般是不一样的。

所以就会存在这种情况:在 binlog = statement 格式时,主库在执行这条 SQL 时,使用的是索引 a,而从库在执行这条 SQL 时,使用了索引 create_time,最后主从数据不一致了。

那么我们该如何解决呢?

可以把 binlog 格式修改为 row,row 格式的 binlog 日志记录的不是 SQL 原文,而是两个 event:Table_map 和 Delete_rows。

Table_map event 说明要操作的表,Delete_rows event用于定义要删除的行为,记录删除的具体行数。row 格式的 binlog 记录的就是要删除的主键 ID 信息,因此不会出现主从不一致的问题。

但是如果 SQL 删除 10 万行数据,使用 row 格式就会很占空间,10 万条数据都在 binlog 里面,写 binlog 的时候也很耗 IO。但是 statement 格式的 binlog 可能会导致数据不一致。

设计 MySQL 的大叔想了一个折中的方案,mixed 格式的 binlog,其实就是 row 和 statement 格式混合使用,当 MySQL 判断可能数据不一致时,就用 row 格式,否则使用就用 statement 格式。

3. 主从延迟

有时候我们遇到从数据库中获取不到信息的诡异问题时,会纠结于代码中是否有一些逻辑会把之前写入的内容删除,但是你又会发现,过了一段时间再去查询时又可以读到数据了,这基本上就是主从延迟在作怪。

主从延迟,其实就是“从库回放” 完成的时间,与 “主库写 binlog” 完成时间的差值,会导致从库查询的数据,和主库的不一致

3.1 主从延迟原理

谈到 MySQL 数据库主从同步延迟原理,得从 MySQL 的主从复制原理说起:

  • MySQL 的主从复制都是单线程的操作,主库对所有 DDL 和 DML 产生 binlog,binlog 是顺序写,所以效率很高;
  • Slave 的 Slave_IO_Running 线程会到主库取日志,放入 relay log,效率会比较高;
  • Slave 的 Slave_SQL_Running 线程将主库的 DDL 和 DML 操作都在 Slave 实施,DML 和 DDL 的 IO 操作是随机的,不是顺序的,因此成本会很高,还可能是 Slave 上的其他查询产生 lock 争用,由于 Slave_SQL_Running 也是单线程的,所以一个 DDL 卡住了,需要执行 10 分钟,那么所有之后的 DDL 会等待这个 DDL 执行完才会继续执行,这就导致了延时。

总结一下主从延迟的主要原因:主从延迟主要是出现在 “relay log 回放” 这一步,当主库的 TPS 并发较高,产生的 DDL 数量超过从库一个 SQL 线程所能承受的范围,那么延时就产生了,当然还有就是可能与从库的大型 query 语句产生了锁等待。

3.2 主从延迟情况

  • 从库机器性能:从库机器比主库的机器性能差,只需选择主从库一样规格的机器就好。
  • 从库压力大:可以搞了一主多从的架构,还可以把 binlog 接入到 Hadoop 这类系统,让它们提供查询的能力。
  • 从库过多:要避免复制的从节点数量过多,从库数据一般以3-5个为宜。
  • 大事务:如果一个事务执行就要 10 分钟,那么主库执行完后,给到从库执行,最后这个事务可能就会导致从库延迟 10 分钟啦。日常开发中,不要一次性 delete 太多 SQL,需要分批进行,另外大表的 DDL 语句,也会导致大事务。
  • 网络延迟:优化网络,比如带宽 20M 升级到 100M。
  • MySQL 版本低:低版本的 MySQL 只支持单线程复制,如果主库并发高,来不及传送到从库,就会导致延迟,可以换用更高版本的 MySQL,支持多线程复制。

3.3 主从延迟解决方案

我们一般会把从库落后的时间作为一个重点的数据库指标做监控和报警,正常的时间是在毫秒级别,一旦落后的时间达到了秒级别就需要告警了。

解决该问题的方法,除了缩短主从延迟的时间,还有一些其它的方法,基本原理都是尽量不查询从库,具体解决方案如下:

  • 使用缓存:我们在同步写数据库的同时,也把数据写到缓存,查询数据时,会先查询缓存,不过这种情况会带来 MySQL 和 Redis 数据一致性问题。
  • 查询主库:直接查询主库,这种情况会给主库太大压力,不建议这种方式。
  • 数据冗余:对于一些异步处理的场景,如果只扔数据 ID,消费数据时,需要查询从库,我们可以把数据全部都扔给消息队列,这样消费者就无需再查询从库。(这种情况应该不太能出现,数据转了一圈,MySQL 主从还没有同步好,直接去撕 DBA 吧)

Mysql锁机制

juejin.cn/post/703563…

从锁粒度上划分:

  • 表级锁:又分为表锁、元数据锁(表结构)、意向锁

每次操作锁住整个表

锁定力度大

发生锁冲突的概率最高

并发度最低

应用在MyISAM、InnoDB、BDB等存储引擎中

 

  • l  行级锁(InnoDB存储引擎)又分为:RecordLock(记录锁) GapLock(间隙锁)、Next-keyLock(记录锁+间隙锁),其中记录锁的代表为主键、唯一索引,间隙锁的代表为:普通索引

每次操作锁住一行数据

锁定粒度最小

发生锁冲突的概率最低

并发读最高

是由InnoDB存储引擎实现的

image.png

  • l  页级锁(BDB存储引擎)

 

从锁操作上划分

读锁与写锁

 

从实现方式上

乐观锁与悲观锁

mysql的执行顺序


from

       <left_table>

on <join_condition>

<join_type> join <right_table>

where

       <where_condition>

group by

       <group_by_list>

having

       <having_condition>

select

distinct

       <select_list>

order by

       <order_by_condition>

limit

       <limit_number>

![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/969c0d01b97449a3abd114aac8d6a087~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=554&h=324&s=95301&e=png&b=efe6da)

Mysql一条sql是如何执行的

1.        Tomcat服务器与MySQL建立连接

2.        -依次经过Server层的分析器、优化器、执行器

3.        -执行器根据执行计划操作InnoDB引擎

4.        InnoDB从磁盘数据文件中将age=1读到缓冲池

5.        -在修改之前,写入一条undolog将age=1存起来

6.        -将缓冲池中的age=l改成age=2

7.        -写入一条redo log将修改后的值age=2存起来

8.        -写入一条binlog将修改后的值age=2存起来

9.        -后台IO线程将缓存池中被修改的值刷入磁盘

B树与B+树的区别

B树的特点: 每个节点都有数据和指针

 

l  节点排序

l  一个节点可以存多个元索,多个元索也排序了

l  每个节点都存储key和data

B+树的特点: 数据只存在于叶子节点

拥有B树的特点

l  非叶子节点上的都是索引指针,叶子节点中存储了所有的元素与索引,并且排好了顺序

l  叶子节点之间有指针

l  增加了顺序访问指针 ,每个叶子节点增加一个指向相邻叶子节点的指针

l  只有叶子节点存储data,叶子节点包含了这棵树的所有索引和数据

Mysql事务四大特性

l  原子性(Atomicity):当前事务的操作要么同时成功,要么同时失败。原子性由 undo log日志来保证

l  一致性(Consistency):使用事务的最终目的,由业务代码正确逻辑保证隔离性 (Isolation):在事务并发执行时,他们内部的操作不能互相干扰

l  持久性 (Durability):一旦提交了事务,它对数据库的改变就应该是永久性的。

l  持久性由redo log日志来保证

Mysql 3层B+tree可以存放多少条数据

很多人说,MySQL每张表最好不要超过2000万条数据,否则就会导致性能下降。阿里的Java开发手册上也提出:单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。

但实际上,这个2000万或者500万都只是一个大概的数字,并不适用于所有场景,

实际情况下,每张表由于自身的字段不同、字段所占用的空间不同等原因,它们在最佳性能下可以存放的数据量也就不同。

例如三层B+Tree它的上两层是索引,最后一层存数据 ,而MySQL每个节点(页)大小默认为16KB,也就是每个节点最多存16KB的数据,前面两层每一条索引记录包含了当前索引的值(假设为主键int 4 个字节) ~~ ~~~~、~~~~ 一个 6 字节 ~~~~****的指针信息 ~~~~、一个~~ 5 字节的行标头,用来指向下一层数据页的指针,这样每条记录等于~~~~4+6+5=15~~~~字节,。每页可以存放~~~~16KB/15B=1066

 

前两层非叶子节点计算

在 B+ 树当中,当一个节点索引记录为 N 条时,它就会有 N 个子节点。由于我们 3 层B+树的前两层都是索引记录,第一层根节点有 N 条索引记录,那第二层就会有 N 个节点这样到了第三层就会有N平方个节点,因此主键为Int型就可以存放16KB/4B字节主键/6B指针/5B行表的平方

数据条数计算

存放条数的话假设每条记录占用1KB,那一页就可以存放16条,那就等于1610661066=差不多两千万条

MVCC机制

MVCC(Multi-Version Concurrency Control)多版本并发控制,就可以做到读写不阻塞,且避免了类似脏读这样的问题,主要通过undo日志链来实现,因为数据库表底层有两个隐藏的字段叫做trx_id与roll_pointer ,每次修改的时候都会插入记录

read commit (读已提交),语句级快照

repeatable read(可重复读),事务级快照

 

 

mysql查询语句需要开启事务吗

 

如果是可重读(REPEATABLE READ)的隔离级别的话,就需要使用事务,哪怕一个方法多个读的操作也要使用事务,如果不使用事务在统计报表的时候可能会读取到不一样时间段是数据 ,因为统计报表是基于同一个时间点的

 

Buffer pool的数据什么时候刷到磁盘

Innodb会启动一个io线程,将buffer pool的数据刷随机到磁盘,具体什么时候刷有操作系统决定

 

不同存储引擎索引结构

MyISAM底层对应三个文件.frm文件、.myd文件、.MYI文件

Innodb底层对应两个文件.frm文件、.idb文件

 

 

聚集索引与非聚集索引区别

·  聚簇索引的叶子节点一般情况下存的是这条数据的所有字段信息。所以我们 select * from table where id = 1 的时候,都是要去叶子节点拿数据的。

·  非聚簇索引的叶子节点存的是这条数据所对应的主键和索引列信息。比如这条非聚簇索引是username,然后表的主键是id,那该非聚簇索引的叶子节点存的就是 username 和 id,而不存其他字段。 相当于是先从非聚簇索引查到主键的值,再根据主键索引去查数据内容,一般情况下要查两次(除非索引覆盖),这也称之为 回表 ,就有点类似于存了个指针,指向了数据存放的真实地址。

 

 

mysql为什么推荐使用自增型整数主键

因为mysql会帮我们维护索引的有序性,如果不是采用自增多的话,在比大小的时候可能放在当前最大索引的前面也可能放在后面,如果放在前面的话又刚好mysql的一个页16kB存放满了,此时插入的话就会引起分裂,因为树的分裂是会消耗性能的需要对它进行重新排列。所以尽量使用增加这样就不会分裂

如果采用UUID作为主键的话,由于UUID是无序的,且太长了这样会浪费空间和插入效率

 

 

Mysql最左前缀底层设计原理

 索引元素是从左到右依次排序的,第二个字段要满足从左到右排序的提前,是第一个字段相等的前提。如果不看第一个字段,那么第二个字段在索引中存储是无序的,相当于全索引查找。第一个字段相等的情况下第二字段才是有序的

mysql为什么不推荐使用hash索引

 

Hash

对索引的key进行一次hash计算就可以定位出数据存储的位置

很多时候Hash索引要比B+树索引更高效

仅能满足“=”,“IN”,不支持范围查询

hash冲突问题

Mysql 3层B+tree可以存放多少条数据

很多人说,MySQL每张表最好不要超过2000万条数据,否则就会导致性能下降。阿里的Java开发手册上也提出:单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。

但实际上,这个2000万或者500万都只是一个大概的数字,并不适用于所有场景,

实际情况下,每张表由于自身的字段不同、字段所占用的空间不同等原因,它们在最佳性能下可以存放的数据量也就不同。

例如三层B+Tree它的上两层是索引,最后一层存数据 ,而MySQL每个节点(页)大小默认为16KB,也就是每个节点最多存16KB的数据,前面两层每一条索引记录包含了当前索引的值(假设为主键int 4 个字节) ~~ ~~~~、~~~~ 一个 6 字节 ~~~~****的指针信息 ~~~~、一个~~ 5 字节的行标头,用来指向下一层数据页的指针,这样每条记录等于~~~~4+6+5=15~~~~字节,。每页可以存放~~~~16KB/15B=1066

 

前两层非叶子节点计算

在 B+ 树当中,当一个节点索引记录为 N 条时,它就会有 N 个子节点。由于我们 3 层B+树的前两层都是索引记录,第一层根节点有 N 条索引记录,那第二层就会有 N 个节点这样到了第三层就会有N平方个节点,因此主键为Int型就可以存放16KB/4B字节主键/6B指针/5B行表的平方

数据条数计算

存放条数的话假设每条记录占用1KB,那一页就可以存放16条,那就等于1610661066=差不多两千万条

B+Tree双向与B-tree的区别


B-tree

叶子节点之间是没有指针

每个节点包含键和值****

B+Tree

所有的非叶子节点都冗余了一份在叶子节点

B+tree叶子节点之间是有指针的

非叶子节点是叶子节点的索引

要存储的数据都存放在叶子节点中

2. 主从复制

说一下MySQL事务隔离级别

juejin.cn/post/711449…

MySQL 事务隔离级别是为了解决并发事务互相干扰的问题的,MySQL 事务隔离级别总共有以下 4 种:

1.READ UNCOMMITTED:读未提交。

2.READ COMMITTED:读已提交。

3.REPEATABLE READ:可重复读。

4.SERIALIZABLE:序列化。

READ UNCOMMITTED:读未提交

1.四种事务隔离级别

1.1 READ UNCOMMITTED

读未提交,也叫未提交读,该隔离级别的事务可以看到其他事务中未提交的数据。该隔离级别因为可以读取到其他事务中未提交的数据,而未提交的数据可能会发生回滚,因此我们把该级别读取到的数据称之为脏数据,把这个问题称之为脏读。

1.2 READ COMMITTED

读已提交,也叫提交读,该隔离级别的事务能读取到已经提交事务的数据,因此它不会有脏读问题。但由于在事务的执行中可以读取到其他事务提交的结果,所以在不同时间的相同 SQL 查询中,可能会得到不同的结果,这种现象叫做不可重复读。

1.3 REPEATABLE READ

可重复读,MySQL 默认的事务隔离级别。可重复读可以解决“不可重复读”的问题,但还存在幻读的问题。所谓的幻读指的是,在同一事务的不同时间使用相同 SQL 查询时,会产生不同的结果。例如,一个 SELECT 被执行了两次,但是第二次返回了第一次没有返回的一行,那么这一行就是一个“幻像”行。

注意:幻读和不可重复读的侧重点是不同的,不可重复读侧重于数据修改,两次读取到的同一行数据不一样;而幻读侧重于添加或删除,两次查询返回的数据行数不同。

1.4 SERIALIZABLE

序列化,事务最高隔离级别,它会强制事务排序,使之不会发生冲突,从而解决了脏读、不可重复读和幻读问题,但因为执行效率低,所以真正使用的场景并不多

2.1 主从复制原理

MySQL 的主从复制是依赖于 binlog,也就是记录 MySQL 上的所有变化并以二进制形式保存在磁盘上二进制日志文件。

主从复制就是将 binlog 中的数据从主库传输到从库上,一般这个过程是异步的,即主库上的操作不会等待 binlog 同步地完成。

详细流程如下:

  1. 主库写 binlog:主库的更新 SQL(update、insert、delete) 被写到 binlog;
  2. 主库发送 binlog:主库创建一个 log dump 线程来发送 binlog 给从库;
  3. 从库写 relay log:从库在连接到主节点时会创建一个 IO 线程,以请求主库更新的 binlog,并且把接收到的 binlog 信息写入一个叫做 relay log 的日志文件;
  4. 从库回放:从库还会创建一个 SQL 线程读取 relay log 中的内容,并且在从库中做回放,最终实现主从的一致性。

2.2 如何保证主从一致

当主库和从库数据同步时,突然中断怎么办?因为主库与从库之间维持了一个长链接,主库内部有一个线程,专门服务于从库的这个长链接。

对于下面的情况,假如主库执行如下 SQL,其中 a 和 create_time 都是索引:

delete from t where a > '666' and create_time<'2022-03-01' limit 1;
复制代码

我们知道,数据选择了 a 索引和选择 create_time 索引,最后 limit 1 出来的数据一般是不一样的。

所以就会存在这种情况:在 binlog = statement 格式时,主库在执行这条 SQL 时,使用的是索引 a,而从库在执行这条 SQL 时,使用了索引 create_time,最后主从数据不一致了。

那么我们该如何解决呢?

可以把 binlog 格式修改为 row,row 格式的 binlog 日志记录的不是 SQL 原文,而是两个 event:Table_map 和 Delete_rows。

Table_map event 说明要操作的表,Delete_rows event用于定义要删除的行为,记录删除的具体行数。row 格式的 binlog 记录的就是要删除的主键 ID 信息,因此不会出现主从不一致的问题。

但是如果 SQL 删除 10 万行数据,使用 row 格式就会很占空间,10 万条数据都在 binlog 里面,写 binlog 的时候也很耗 IO。但是 statement 格式的 binlog 可能会导致数据不一致。

设计 MySQL 的大叔想了一个折中的方案,mixed 格式的 binlog,其实就是 row 和 statement 格式混合使用,当 MySQL 判断可能数据不一致时,就用 row 格式,否则使用就用 statement 格式。

3. 主从延迟

有时候我们遇到从数据库中获取不到信息的诡异问题时,会纠结于代码中是否有一些逻辑会把之前写入的内容删除,但是你又会发现,过了一段时间再去查询时又可以读到数据了,这基本上就是主从延迟在作怪。

主从延迟,其实就是“从库回放” 完成的时间,与 “主库写 binlog” 完成时间的差值,会导致从库查询的数据,和主库的不一致

3.1 主从延迟原理

谈到 MySQL 数据库主从同步延迟原理,得从 MySQL 的主从复制原理说起:

  • MySQL 的主从复制都是单线程的操作,主库对所有 DDL 和 DML 产生 binlog,binlog 是顺序写,所以效率很高;
  • Slave 的 Slave_IO_Running 线程会到主库取日志,放入 relay log,效率会比较高;
  • Slave 的 Slave_SQL_Running 线程将主库的 DDL 和 DML 操作都在 Slave 实施,DML 和 DDL 的 IO 操作是随机的,不是顺序的,因此成本会很高,还可能是 Slave 上的其他查询产生 lock 争用,由于 Slave_SQL_Running 也是单线程的,所以一个 DDL 卡住了,需要执行 10 分钟,那么所有之后的 DDL 会等待这个 DDL 执行完才会继续执行,这就导致了延时。

总结一下主从延迟的主要原因:主从延迟主要是出现在 “relay log 回放” 这一步,当主库的 TPS 并发较高,产生的 DDL 数量超过从库一个 SQL 线程所能承受的范围,那么延时就产生了,当然还有就是可能与从库的大型 query 语句产生了锁等待。

3.2 主从延迟情况

  • 从库机器性能:从库机器比主库的机器性能差,只需选择主从库一样规格的机器就好。
  • 从库压力大:可以搞了一主多从的架构,还可以把 binlog 接入到 Hadoop 这类系统,让它们提供查询的能力。
  • 从库过多:要避免复制的从节点数量过多,从库数据一般以3-5个为宜。
  • 大事务:如果一个事务执行就要 10 分钟,那么主库执行完后,给到从库执行,最后这个事务可能就会导致从库延迟 10 分钟啦。日常开发中,不要一次性 delete 太多 SQL,需要分批进行,另外大表的 DDL 语句,也会导致大事务。
  • 网络延迟:优化网络,比如带宽 20M 升级到 100M。
  • MySQL 版本低:低版本的 MySQL 只支持单线程复制,如果主库并发高,来不及传送到从库,就会导致延迟,可以换用更高版本的 MySQL,支持多线程复制。

3.3 主从延迟解决方案

我们一般会把从库落后的时间作为一个重点的数据库指标做监控和报警,正常的时间是在毫秒级别,一旦落后的时间达到了秒级别就需要告警了。

解决该问题的方法,除了缩短主从延迟的时间,还有一些其它的方法,基本原理都是尽量不查询从库,具体解决方案如下:

  • 使用缓存:我们在同步写数据库的同时,也把数据写到缓存,查询数据时,会先查询缓存,不过这种情况会带来 MySQL 和 Redis 数据一致性问题。
  • 查询主库:直接查询主库,这种情况会给主库太大压力,不建议这种方式。
  • 数据冗余:对于一些异步处理的场景,如果只扔数据 ID,消费数据时,需要查询从库,我们可以把数据全部都扔给消息队列,这样消费者就无需再查询从库。(这种情况应该不太能出现,数据转了一圈,MySQL 主从还没有同步好,直接去撕 DBA 吧)

在实际应用场景中,对于一些非常核心的场景,比如库存,支付订单等,需要直接查询主库,其它非核心场景,就不要去查主库了。

如何保障 MySQL 和 Redis 的数据一致性

image.png

不好的方案

1. 先写 MySQL,再写 Redis

图片

图解说明:

  • 这是一副时序图,描述请求的先后调用顺序;
  • 橘黄色的线是请求 A,黑色的线是请求 B;
  • 橘黄色的文字,是 MySQL 和 Redis 最终不一致的数据;
  • 数据是从 10 更新为 11;
  • 后面所有的图,都是这个含义,不再赘述。

请求 A、B 都是先写 MySQL,然后再写 Redis,在高并发情况下,如果请求 A 在写 Redis 时卡了一会,请求 B 已经依次完成数据的更新,就会出现图中的问题。

这个图已经画的很清晰了,我就不用再去啰嗦了吧,不过这里有个前提,就是对于读请求,先去读 Redis,如果没有,再去读 DB,但是读请求不会再回写 Redis。  大白话说一下,就是读请求不会更新 Redis。

2. 先写 Redis,再写 MySQL

图片

同“先写 MySQL,再写 Redis”,看图可秒懂。

3. 先删除 Redis,再写 MySQL

这幅图和上面有些不一样,前面的请求 A 和 B 都是更新请求,这里的请求 A 是更新请求,但是请求 B 是读请求,且请求 B 的读请求会回写 Redis。

图片

请求 A 先删除缓存,可能因为卡顿,数据一直没有更新到 MySQL,导致两者数据不一致。

这种情况出现的概率比较大,因为请求 A 更新 MySQL 可能耗时会比较长,而请求 B 的前两步都是查询,会非常快。

好的方案

4. 先删除 Redis,再写 MySQL,再删除 Redis

对于“先删除 Redis,再写 MySQL”,如果要解决最后的不一致问题,其实再对 Redis 重新删除即可,这个也是大家常说的“缓存双删”。

图片

为了便于大家看图,对于蓝色的文字,“删除缓存 10”必须在“回写缓存10”后面,那如何才能保证一定是在后面呢?网上给出的第一个方案是,让请求 A 的最后一次删除,等待 500ms。

对于这种方案,看看就行,反正我是不会用,太 Low 了,风险也不可控。

那有没有更好的方案呢,我建议异步串行化删除,即删除请求入队列

图片

异步删除对线上业务无影响,串行化处理保障并发情况下正确删除。

如果双删失败怎么办,网上有给 Redis 加一个缓存过期时间的方案,这个不敢苟同。个人建议整个重试机制,可以借助消息队列的重试机制,也可以自己整个表,记录重试次数,方法很多。

简单小结一下:

  • “缓存双删”不要用无脑的 sleep 500 ms;
  • 通过消息队列的异步&串行,实现最后一次缓存删除;
  • 缓存删除失败,增加重试机制。

5. 先写 MySQL,再删除 Redis

图片

对于上面这种情况,对于第一次查询,请求 B 查询的数据是 10,但是 MySQL 的数据是 11,只存在这一次不一致的情况,对于不是强一致性要求的业务,可以容忍。 (那什么情况下不能容忍呢,比如秒杀业务、库存服务等。)

当请求 B 进行第二次查询时,因为没有命中 Redis,会重新查一次 DB,然后再回写到 Reids。

图片

这里需要满足 2 个条件:

  • 缓存刚好自动失效;
  • 请求 B 从数据库查出 10,回写缓存的耗时,比请求 A 写数据库,并且删除缓存的还长。

对于第二个条件,我们都知道更新 DB 肯定比查询耗时要长,所以出现这个情况的概率很小,同时满足上述条件的情况更小。

6. 先写 MySQL,通过 Binlog,异步更新 Redis

这种方案,主要是监听 MySQL 的 Binlog,然后通过异步的方式,将数据更新到 Redis,这种方案有个前提,查询的请求,不会回写 Redis。

图片

这个方案,会保证 MySQL 和 Redis 的最终一致性,但是如果中途请求 B 需要查询数据,如果缓存无数据,就直接查 DB;如果缓存有数据,查询的数据也会存在不一致的情况。

所以这个方案,是实现最终一致性的终极解决方案,但是不能保证实时性。

几种方案比较

我们对比上面讨论的 6 种方案:

  1. 先写 Redis,再写 MySQL
  • 这种方案,我肯定不会用,万一 DB 挂了,你把数据写到缓存,DB 无数据,这个是灾难性的;
  • 我之前也见同学这么用过,如果写 DB 失败,对 Redis 进行逆操作,那如果逆操作失败呢,是不是还要搞个重试?
  1. 先写 MySQL,再写 Redis
  • 对于并发量、一致性要求不高的项目,很多就是这么用的,我之前也经常这么搞,但是不建议这么做;
  • 当 Redis 瞬间不可用的情况,需要报警出来,然后线下处理。
  1. 先删除 Redis,再写 MySQL
  • 这种方式,我还真没用过,直接忽略吧。
  1. 先删除 Redis,再写 MySQL,再删除 Redis
  • 这种方式虽然可行,但是感觉好复杂,还要搞个消息队列去异步删除 Redis。
  1. 先写 MySQL,再删除 Redis
  • 比较推荐这种方式,删除 Redis 如果失败,可以再多重试几次,否则报警出来;
  • 这个方案,是实时性中最好的方案,在一些高并发场景中,推荐这种。
  1. 先写 MySQL,通过 Binlog,异步更新 Redis
  • 对于异地容灾、数据汇总等,建议会用这种方式,比如 binlog + kafka,数据的一致性也可以达到秒级;
  • 纯粹的高并发场景,不建议用这种方案,比如抢购、秒杀等。

个人结论:

  • 实时一致性方案:采用“先写 MySQL,再删除 Redis”的策略,这种情况虽然也会存在两者不一致,但是需要满足的条件有点苛刻,所以是满足实时性条件下,能尽量满足一致性的最优解。
  • 最终一致性方案:采用“先写 MySQL,通过 Binlog,异步更新 Redis”,可以通过 Binlog,结合消息队列异步更新 Redis,是最终一致性的最优解。

hashmap

  • hashmap是线程不安全的(效率比较高) hashtable是线程安全的(效率比较低)
  • hashmap是允许存放key=null,hashtable是不允许存放key=null
  • hashmap是允许存放key=null,存放在数组的哪个位置呢?index=0

hashmap集合底层实现方式

  • 基于arrayList集合
  • 基于数组+链表实现
  • 基于数组+链表+红核树实现

如何解决hash冲突的问题

  • jdk1.7版本:链表 增删效率非常高 查询效率非常低(o(n)) image.png

存在问题:如果index冲突概率过多(也就是hashcode值存在多个相同的),导致都会追加在链表的后面,链表的长度变大,查询效率变低

比如:

hashmap.put("a","A");

hashmap.put(97,"a97");

此时map的key的hashcode都是相同,导致都会追加在链表的后面,链表的长度变大,查询效率变低

  • jdk1.8版本:链表+红黑树

红黑树:基于1.7的问题1.8改变了,当链表的长度超过8的时候,自动转换为红黑树

  • hashmap集合是有序存放key吗?

是无序,是散列。因为例如:97%1000和98%1000 这两者取模不是递增的

image.png

  • 在高并发的情况下如何使用hashmap集合?

hashmap线程不完全--hahstable--线程安全 concurrentHashMap 1.7分段锁,1.8node节点 cas+syn锁 取消分段锁设计

image.png

  • 相对hashMap初始化1万条key效率如何实现高效 因为初始化容量是16个,会去不断的扩容。需要重新构建hash表,这样性能就比较差

JVM知识点

一个类什么时候进入JVM,3个时间节点

  • 虚拟机启动时,进入main方法的时候
  • new对象的时候
  • 读取静态字段或静态方法的时候

类的生命周期

它的生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。

image.png

image.png

3、双亲委派模型

如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式

java 的ClassLoader java的classloader作用_jvm_02

4、ConcurrentHashMap

HashMap的底层是通过数组+链表+红黑树来实现的,在没有手动设置的情况下,HashMap默认数组长度为16,而它进行扩容的阈值是 16 * 0.75 = 12。为了减少Hash冲突,让数据分布的更加均匀,当HashMap上存放的值超过了阈值后,就需要对HashMap进行扩容

JDK 1.8 中 ConcurrentHashMap 使用的是数组+链表/红黑树的方式实现的,它是通过 CAS 或 synchronized 来实现线程安全的,并且它的锁粒度更小,查询性能也更高

kafka生产者原理

image.png

生产的整理发送流程

  • 在main线程创建producer对象
  • produce对象调用send方法。然后经过拦截器,序列化器(一般采用自带的)最后经过分区器将数据发往缓存队列里面一个队列对应一个分区,这个队列的缓存大小是32M,kafka每批次的 大小是16kb
  • 接着sender线程来拉取数据,拉取有两个条件一个是batch.size默认16kb,一个是linger.ms时间到了,任何一个满足要求就能发送数据。
  • 发送数据的时候是通过节点的方式,每一个节点一个队列
  • 发送过去的时候如果brocker没有及时应答,sender线程中国 最多只能缓存5个请求,
  • 通过selector将底层的链路打通。发送过去之后集群kafka进行应答(1、0、-1), 如果成功应答清楚掉对应的sender中请求数据同时清楚掉对应的分区数据。如果kafka应答失败进行重试retries,重试的次数默认int的最大值

kafka生产者分区策略

有指定分区按照分区来分配,没有指定分区按照key的hashcode取值,都没有的话按照粘性来分配

kafka生产者如何提高吞吐量

image.png

  • 调整批次大小batch.size
  • 修改等待时间linger.ms
  • compression_type 压缩snappy
  • 修改缓存区大小 修改为64M

kafka生产者数据可靠性

kafka集群数据的可靠性1 0 -1

  • 0 :生产者发送过来的数据,不需要等待数据落盘就应答
  • 1 生产者发送过来的数据,leader收到数据后应答(存在leader应答完成,还没开始同时副本,leader挂了,此时新的的leader不会在次收到消息,因为生产者已发送成功)
  • -1(all)生产者发送过来的数据,leader+isr队列里面的所有节点收齐数据后应答-1与all等价

思考

思考ack=-1 leader收到数据,所有follwer都开始同步数据,但是有个flower因为某种故障,迟迟不能leader进行同步,这个问题怎么解决

Leader维护了一个动态的in-sync replicaset(ISR),意为和 Leader保持同步的Follower+Leader集合(leader:0,isr:0,1,2)。 如果Follower长时间未向Leader发送通信请求或同步数据,则该Follower将被踢出ISR。该时间阈值由replica.lag.time.max.ms参数设定,默认30s。 例如2超时,(leader:0,isr:0,1)

数据可靠性分析:

如果分区副本设置为1个,或者ISR里应答的最小副本数量( min.insync.replicas默认为1)设置为1,和ack=1的效果是一样的,仍然有丢数的风险(leader:0,isr:0)。

**因此数据完全可靠条件=ACK级别设置为-1+分区副本大于等于2+ISR里面的最小副本数据大于等于2 ** image.png

kafka幂等性

image.png

幂等性原理

  • 幂等性就是指Producer不论向Broker发送多少次重复数据,Broker端都只会持久化一条,保证了不重复。

  • 精确一次(Exactly Once)=幂等性+至少一次(ack=-1+分区副本数>=2+ISR最小副本数量>=2)。

  • 重复数据的判断标准:具有<PID,Partition,SeqNumber>相同主键的消息提交时,Broker只会持久化一条。其中PID是Kafka每次重启都会分配一个新的;Partition表示分区号;Sequence Number是单调自增的。

  • 所以幂等性只能保证的是在单分区单会话内不重复。

如何使用幂等性

开启参数enable.idempotencem默认true false关闭

kafka事务原理

开启你事务必须开始幂等性

image.png

kafka broker总体工作流程

image.png AR:kafka分区中的所有副本统称

ISR:所有与follow

l  每个broker启动之后都会想zookeeper进行注册

l  注册完之后开始选择controller节点,每个broker都有一个controller,谁会成为未来leadedr选举的选择的老大,争先去抢占controller的节点,谁抢到了谁就是leader,负责日后的选举

l  由选举出来的controller 监听brokers节点的变化

l  Controller决定leader选举(选举的规则,在isr中存活为前提,安装AR中排在前面的优先。例如ar[1,0,2],isr【1,0,2】,那么leader机会按照1,0,2的顺序轮询)

l  将选举出来得broker leader节点信息上传到ZK

l  其他controller从ZK同步相关信息

l  接下去生产者往集群发送数据,发送数据之后follower主动与leader进行同步,同步信息在底层是怎么存储的呢,采用是的log的方式 ,其实底层存储的是segment的,一个segment一个G,

如果broker挂了,集群就会监听的到,就会重新拉取信息重新选举,并将信息同步到ZK

follower挂了之后底层处理方式

  • follower发送故障后会被踢出isr
  • 这个期间其他leader与follower正常进行数据听不
  • 带该follower回复后,follower会读取本地磁盘记录的上次HW,并将log文件高于HW的部分截取掉,从HW开始向leader进行同步
  • 等待follower的LEO大于等于Partition的HW,即Follower追上leader之后,就可以重新加入ISR了

image.png

leader挂了之后处理方式

  • leader发生故障之后,会从isr中选出一个新的leader
  • 为了保证多个f副本之间数据的一致性,其余的follwer会先将各自的log文件高于HW的部分截掉,
  • 然后从新的leader同步数据

注意:这智能只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复

image.png

leader Partion自动平衡

image.png

5、kafka文件存储机制

  • Kafka文件存储机制

Topic是逻辑上的概念,而partition是物理上的概念,每个partition对应于一个log文件,该log文件中存储的就是Producer生产的数

据。Producer生产的数据会被不断追加到该log文件末端,为防止log文件过大导致数据定位效率低下,Kafka采取了分片索引机制,

将每个partition分为多个segment(每个1G)。每个segment包括:“.index”文件、“.log”文件和.timeindex等文件。这些文件位于一个文件夹下,该

文件夹的命名规则为:topic名称+分区序号,例如:first-0。

1、一个topic分为多个partition,一个partition分为多个segment,一个segment包含:.log 日志文件、.index 偏移量索引文件、.timeindex 时间戳索引文件

.timeindex 时间戳索引文件,每个segment文件大小是1个G

说明: index和log文件以当前segment的第一条消息的offset命名。

image.png

  • index与log文件的详解

如何在log文件中定位找到offset=600的记录呢?

1、根据目标的offset定位到segment文件,因为segment文件的命名是以第一条消息的offset命名

2、找到小于等于目标的offset(也就是绝对offset<600的index记录)的索引项

3、定位到log文件记录位置

4、向下遍历找到目标的record,然后返回

注意:

1、index为稀疏所以,大约每往log文件写入4KB数据,会往index文件写入一条索引,参数log.index.interval.bytes默认4kb

2、index文件中保存的offset为相对offset,这样能确保offset的值所占用空间不会过大,因此能将offset的值控制在固定的大小

整体流程图

image.png

5、kafka高效读写数据

1、Kafka 本身是分布式集群,可以采用分区技术,并行度高

2、读数据采用稀疏索引,可以快速定位要消费的数据**

3、顺序写磁盘,顺序写能到 600M/s

4、页缓存** + 零拷贝技术

6、kafka文件清理策略

Kafka 中默认的日志保存时间为 7 天,可以通过调整如下参数修改保存时间。

⚫ log.retention.hours,最低优先级小时,默认 7 天。

⚫ log.retention.minutes,分钟。

⚫ log.retention.ms,最高优先级毫秒。

⚫ log.retention.check.interval.ms,负责设置检查周期,默认 5 分钟。

那么日志一旦超过了设置的时间,怎么处理呢?

Kafka 中提供的日志清理策略有 delete 和 compact 两种。

1)delete 日志删除:将过期数据删除

⚫ log.cleanup.policy = delete 所有数据启用删除策略

(1)基于时间:默认打开。以 segment 中所有记录中的最大时间戳作为该文件时间戳。

(2)基于大小:默认关闭。超过设置的所有日志总大小,删除最早的 segment。

log.retention.bytes,默认等于-1,表示无穷大。

思考: 如果一个 segment 中有一部分数据过期,一部分没有过期,怎么处理?

2)compact 日志压缩

compact日志压缩:对于相同key的不同value值,只保留最后一个版本

7、Kafka 消费方式

pull* (拉)模 式:*

consumer采用从broker中主动拉取数据。

pull模式不足之处是,如 果Kafka没有数

据,消费者可能会陷入循环中,一直返回

空数据。

Kafka* 采用这种方式。*

push* (推)模式:*

Kafka没有采用这种方式,因为由broker

决定消息发送速率,很难适应所有消费者的

消费速率。例如推送的速度是50m/s,

Consumer1、Consumer2就来不及处理消息。

8、Kafka消费者组初始化流程

1、coordinator:辅助实现消费者组的初始化和分区的分配。每个broker都有一个coordinator

消费者组要选择哪个coordinator作为他辅助呢?

coordinator节点选择 = groupid的hashcode值 % 50( __consumer_offsets的分区数量)

例如: groupid的hashcode值 = 1,1% 50 = 1,那么__consumer_offsets 主题的1号分区,在哪个broker上,就选择这个节点的coordinator

作为这个消费者组的老大。消费者组下的所有的消费者提交offset的时候就往这个分区去提交offset。

筛选出coordinator之后

1)每个consumer都发送JoinGroup请求到coordinator中,首要加入到这个group中

2)对应的coordinator会随机通过消费者id,来选出一个leade作为老大

3)对应的coordinator收集到各个consumer之后,把要消费的topic情况发送给leader 消费者

4)leader会负责制定消费方案,制定各个消费者消费哪个分区(分区分配策略 rang、roundRobin、sticky、cooperativeSticky)

5)leader把制定后的消费方案发给对应的coordinator

6)对应的coordinator就把消费方案下发给各个consumer

7)每个消费者都会和coordinator保持心跳(默认3s),一旦超时(session.timeout.ms=45s),该消费者会被移除,并触发再平衡;或者消费者处理消息的时间过长(max.poll.interval.ms5分钟),也会触发再平衡

image.png

kafka消费者详细消费流程

  • 首先消费者组要与kafka集群进行通信,首先需要创建一个对象ConsumerNetworkClient
  • 创建完对象之后进行初始化,初始化过程中涉及到了三个参数 Fetch.min.bytes每批次最小抓取的大小 默认1字节 fetsh、.max.wait.ms一批次数据最小值未达到的超时时间 默认500ms fetch.max.bytes每批次最大抓取大小 默认50M
  • 下面开始发送请求,通过onsuccess回调过来,数据是一波一波的放到消息队列里面
  • 接下来消费者会每批次拉取500条(max.poll.records=500)
  • 然后依次经过消费者的反序列化=》拦截器=》真正的处理

image.png

9、kafka消费者分区的分配以及再平衡

1、一个consumer group中有多个consumer组成,一个 topic有多个partition组成,现在的问题是,到底由哪个consumer来消费哪个

partition的数据(请阅读kafka消费者初始化过程)。

2、Kafka有四种主流的分区分配策略: Range、RoundRobin、Sticky、CooperativeSticky。

可以通过配置参数partition.assignment.strategy,修改分区的分配策略。默认策略是Range + CooperativeSticky。Kafka可以同时使用

多个分区分配策略。

1)每个consumer都发送JoinGroup请求到coordinator中,首要加入到这个group中

2)对应的coordinator会随机通过消费者id,来选出一个leade作为老大

3)对应的coordinator收集到各个consumer之后,把要消费的topic情况发送给leader 消费者

4)leader会负责制定消费方案,制定各个消费者消费哪个分区(分区分配策略 rang、roundRobin、sticky、cooperativeSticky)

5)leader把制定后的消费方案发给对应的coordinator

6)对应的coordinator就把消费方案下发给各个consumer

7)每个消费者都会和coordinator保持心跳(默认3s),一旦超时(session.timeout.ms=45s),该消费者会被移除,并触发再平衡;或者消费者处理消息的时间过长(max.poll.interval.ms5分钟),也会触发再平衡

参数名称描述
heartbeat.interval.msKafka 消费者和 coordinator 之间的心跳时间,默认 3s。 该条目的值必须小于 session.timeout.ms,也不应该高于 session.timeout.ms 的 1/3。
session.timeout.msKafka 消费者和 coordinator 之间连接超时时间,默认 45s。超 过该值,该消费者被移除,消费者组执行再平衡。session.timeout.ms 的 1/3。
max.poll.interval.ms消费者处理消息的最大时长,默认是 5 分钟。超过该值,该 消费者被移除,消费者组执行再平衡。
partition.assignment.strategy消 费 者 分 区 分 配 策 略 , 默 认 策 略 是 Range + CooperativeSticky。Kafka 可以同时使用多个分区分配策略。 可 以 选 择 的 策 略 包 括 : Range 、 RoundRobin 、 Sticky 、CooperativeSticky

9.1Range 以及再平衡

  • Range 分区策略原理

Range 是对每个 topic 而言的。

首先对同一个 topic 里面的分区按照序号进行排序,并

对消费者按照字母顺序进行排序。

假如现在有 7 个分区,3 个消费者,排序后的分区将会

是0,1,2,3,4,5,6;消费者排序完之后将会是C0,C1,C2。

例如,7/3 = 2 余 1 ,除不尽,那么 消费者 C0 便会多

消费 1 个分区。 8/3=2余2,除不尽,那么C0和C1分别多

消费一个。

通过 partitions数/consumer数 来决定每个消费者应该

消费几个分区。如果除不尽,那么前面几个消费者将会多

消费 1 个分区。

分区分配策略之Range

注意:如果只是针对 1 个 topic 而言,C0消费者多消费1

个分区影响不是很大。但是如果有 N 多个 topic,那么针对每

个 topic,消费者 C0都将多消费 1 个分区,topic越多,C0消

费的分区会比其他消费者明显多消费 N 个分区。

  • Range 分区分配再平衡案例

(1)停止掉 0 号消费者,快速重新发送消息观看结果(45s 以内,越快越好)。

1 号消费者:消费到 3、4 号分区数据。

2 号消费者:消费到 5、6 号分区数据。

0 号消费者的任务会整体被分配到 1 号消费者或者 2 号消费者。

说明:0 号消费者挂掉后,消费者组需要按照超时时间 45s 来判断它是否退出,所以需

要等待,时间到了 45s 后,判断它真的退出就会把任务分配给其他 broker 执行。

(2)再次重新发送消息观看结果(45s 以后)。

1 号消费者:消费到 0、1、2、3 号分区数据。

2 号消费者:消费到 4、5、6 号分区数据。

说明:消费者 0 已经被踢出消费者组,所以重新按照 range 方式分配。

9.2RoundRobin 以及再平衡

  • RoundRobin 分区策略原理

RoundRobin 针对集群中所有Topic而言。

RoundRobin 轮询分区策略,是把所有的 partition 和所有的

consumer 都列出来,然后按照 hashcode 进行排序,最后

通过轮询算法来分配 partition 给到各个消费者。

  • RoundRobin 分区分配再平衡案例

(1)停止掉 0 号消费者,快速重新发送消息观看结果(45s 以内,越快越好)。

1 号消费者:消费到 2、5 号分区数据

2 号消费者:消费到 4、1 号分区数据

0 号消费者的任务会按照 RoundRobin 的方式,把数据轮询分成 0 、6 和 3 号分区数据,

分别由 1 号消费者或者 2 号消费者消费。

说明:0 号消费者挂掉后,消费者组需要按照超时时间 45s 来判断它是否退出,所以需

要等待,时间到了 45s 后,判断它真的退出就会把任务分配给其他 broker 执行。

(2)再次重新发送消息观看结果(45s 以后)。

1 号消费者:消费到 0、2、4、6 号分区数据

2 号消费者:消费到 1、3、5 号分区数据

说明:消费者 0 已经被踢出消费者组,所以重新按照 RoundRobin 方式分配。

9.3 Sticky 以及再平衡

  • 粘性分区定义

粘性分区定义: 可以理解为分配的结果带有“粘性的”。即在执行一次新的分配之前,

考虑上一次分配的结果,尽量少的调整分配的变动,可以节省大量的开销。

粘性分区是 Kafka 从 0.11.x 版本开始引入这种分配策略,首先会尽量均衡的放置分区

到消费者上面,在出现同一消费者组内消费者出现问题的时候,会尽量保持原有分配的分

区不变化

  • Sticky 分区分配再平衡案

停止掉 0 号消费者,快速重新发送消息观看结果(45s 以内,越快越好)。

1 号消费者:消费到 2、5、3 号分区数据。

2 号消费者:消费到 4、6 号分区数据。

0 号消费者的任务会按照粘性规则,尽可能均衡的随机分成 0 和 1 号分区数据,分别

由 1 号消费者或者 2 号消费者消费

说明:0 号消费者挂掉后,消费者组需要按照超时时间 45s 来判断它是否退出,所以需

要等待,时间到了 45s 后,判断它真的退出就会把任务分配给其他 broker 执行。

(2)再次重新发送消息观看结果(45s 以后)。

1 号消费者:消费到 2、3、5 号分区数据。

2 号消费者:消费到 0、1、4、6 号分区数据。

说明:消费者 0 已经被踢出消费者组,所以重新按照粘性方式分配。

10、kafka—数据积压(消费者如何提高吞吐量)

1)如果是Kafka消费能力不足,则可以考虑增加Topic的分区数,并且同时提升消费组的消费者数量,消费者数 = 分区数。(两者缺一不可)

2)如果是下游的数据处理不及时:提高每批次拉取的数量。批次拉取数据过少(拉取数据/处理时间 < 生产速度),使处理的数据小于生产的数据,也会造成数据积压。

参数名称描述
fetch.max.bytes默认 Default: 52428800(50 m)。消费者获取服务器端一批消息最大的字节数。如果服务器端一批次的数据大于该值(50m)仍然可以拉取回来这批数据,因此,这不是一个绝对最大值。一批次的大小受 message.max.bytes (broker config)or max.message.bytes (topic config)影响。
max.poll.records一次 poll 拉取数据返回消息的最大条数,默认是 500 条

Session?单点登录?JWT

  • 传统的Session认证有什么问题?不是能解决登录问题吗?
  1. 每个用户的登录信息都保存在服务器的session中,随着用户的增多,服务器开销会明显上升
  2. Session是存储在服务器的物理内存中的,所以在分布式系统中,这种方式也会导致登录失败,什么意思呢?举个例子,比如一个SpringBoot项目对应的是一个jvm虚拟机,现在有一个SpringCloud项目(就是多个SpringBoot),然你只在一个SpringBoot的jvm中进行了登录,并且在这一个jvm的tomcat进行了session认证,但是其他的jvm是没有这个session,即没有你的登录信息,
  3. 对于非浏览器的客户端、手机移动端等不适用,因为session依赖于Cookie,而移动端经常没有Cookie
  4. Session认证基于Cookie,若Cookie被截获了,用户很容易受到跨站请求伪造攻击,如果浏览器禁用Cookie,这种方式也会失效
  5. Session基于Cookie,而Cookie无法跨域,所以Session认证也是无法跨域的,单点登录不适用(除非采用redis进行session共享)
  6. 前后端分离系统中更加不适用,后端部署复杂,前端的请求往往经过多个中间件到达后端,Cookie中的Session信息需转发多次
  • token认证的优势

    这种基于token的认证方式与传统的session认证方式更加节约服务器资源,并且对移动端和分布式都友好,其优点如下:

  1. 支持跨域访问:COokie是无法实现跨域的,而token并没有用到Cookie(前提是将token放到请求头里面去)
  2. 无状态:token机制在服务端不需要存储session信息,因为token自身包含了所有登录用户的信息,所以可以减轻服务器压力
  3. 适用移动端:当客户端是非浏览器平台时,Cookie谁不被支持的,此时采用token认证的方式会简单很多
  4. 无需考虑CSRF:token不依赖于Cookie,所以采用token的认证方式不用担心Cookie被截获,无需考虑CSRF

SPI

Java SPI机制--ServiceLoader

Java SPI(Service Provider Interface)是Java官方提供的一种服务发现机制,它允许在运行时动态地加载实现特定接口的类,而不需要在代码中显式地指定该类,从而实现解耦和灵活性

  • ServiceLoader是Java提供的一种简单的SPI机制的实现,Java的SPI实现约定了以下两件事:

    • 文件必须放在META-INF/services/目录底下
    • 文件名必须为接口的全限定名,内容为接口实现的全限定名

JDK SPI就是通过IO流读取文件的内容,这样就可以获取到接口的实现的全限定名

实现原理 Java SPI 的实现原理基于 Java 类加载机制和反射机制。

当使用 ServiceLoader.load(Class service) 方法加载服务时,会检查 META-INF/services 目录下是否存在以接口全限定名命名的文件。如果存在,则读取文件内容,获取实现该接口的类的全限定名,并通过 Class.forName() 方法加载对应的类。

在加载类之后,ServiceLoader 会通过反射机制创建对应类的实例,并将其缓存起来。

应用场景

Java SPI机制是一种服务提供者发现的机制,适用于需要在多个实现中选择一个进行使用的场景。

常见的应用场景包括:

image.png

Spring SPI机制--SpringFactoriesLoader

Spring的SPI机制的约定如下:

  • 配置文件必须在META-INF/目录下,文件名必须为spring.factories
  • 文件内容为键值对,一个键可以有多个值,只需要用逗号分割就行,同时键值都需要是类的全限定名,键和值可以没有任何类与类之间的关系,当然也可以有实现的关系。

实现原理跟Java实现的差不多,只不过读的是META-INF/目录下spring.factories文件内容,然后解析出来键值对

Spring的SPI机制相对于Java原生的SPI机制进行了改造和扩展

支持多个实现类:Spring的SPI机制允许为同一个接口定义多个实现类,而Java原生的SPI机制只支持单个实现类。这使得在应用程序中使用Spring的SPI机制更加灵活和可扩展。

支持自动装配:Spring的SPI机制支持自动装配,可以通过将实现类标记为Spring组件(例如@Component),从而实现自动装配和依赖注入。这在一定程度上简化了应用程序中服务提供者的配置和管理。

支持动态替换:Spring的SPI机制支持动态替换服务提供者,可以通过修改配置文件或者其他方式来切换服务提供者。而Java原生的SPI机制只能在启动时加载一次服务提供者,并且无法在运行时动态替换。

Dubbo SPI

dubbo的SPI机制除了解决了无法获取指定实现类的问题,还提供了很多额外的功能,这些功能在dubbo内部用的非常多,接下来就来详细讲讲。

redis知识点

5种类型数据结构

  • String:内部结构实现上类似于 JavaArrayList,采用预分配冗余空间的方式来减少内存的频繁分配.
  • List 的数据结构为快速链表 quickList。首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是 ziplist,也即是压缩列表。

它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成 quicklist

  • Set 数据结构是字典,字典是用哈希表实现的。

  • Hash 类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当 field-value 长度较短且个数较少时,使用 ziplist,否则使用 hashtable

  • zset 底层使用了两个数据结构

    • hashhash 的作用就是关联元素 value 和权重 score,保障元素 value 的唯一性,可以通过元素 value 找到相应的 score
    • 跳跃表,跳跃表的目的在于给元素 value 排序,根据 score 的范围获取元素列表

redis6 的3种新数据类型

1、bitmaps、=》基于位运算的

HyperLogLog=》基于基数运算的(也就是去重)

Geospatial=》基于地理位置位置运算的

持久化

RDB、aof

RDB:在指定的时间间隔内将内存中的数据集快照写入磁盘, 即 Snapshot 快照,恢复时是将快照文件直接读到内存里。

RDB

原理

redis 会单独创建一个子进程( fork*)来进行持久化。

先将数据写入到一个临时文件中,待持久化过程完成后,再将这个临时文件内容覆盖到 dump.rdb

整个过程中,主进程是不进行任何 IO 操作的,这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效。

RDB 的缺点是最后一次持久化后的数据可能丢失

优点
  • 适合大规模的数据恢复;
  • 对数据完整性和一致性要求不高更适合使用;
  • 节省磁盘空间;
  • 恢复速度快。
缺点
  • Fork 的时候,内存中的数据被克隆了一份,大致 2 倍的膨胀性需要考虑;
  • 虽然 Redisfork 时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能;
  • 在备份周期在一定间隔时间做一次备份,所以如果 Redis 意外 down 掉的话,就会丢失最后一次快照后的所有修改。

AOF

以日志的形式来记录每个写操作(增量保存),将 Redis 执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,Redis 启动之初会读取该文件重新构建数据,换言之,如果 Redis 重启就会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。

执行流程原理
  • 客户端的请求写命令会被 append 追加到 AOF 缓冲区内;
  • AOF 缓冲区根据 AOF 持久化策略 [always,everysec,no] 将操作 sync 同步到磁盘的 AOF 文件中;
  • AOF 文件大小超过重写策略或手动重写时,会对 AOF 文件 Rewrite 重写,压缩 AOF 文件容量;
  • Redis 服务重启时,会重新 load 加载 AOF 文件中的写操作达到数据恢复的目的。

AOFRDB 同时开启时,系统默认读取 AOF 的数据(数据不会存在丢失)

优点
  • 备份机制更稳健,丢失数据概率更低;
  • 可读的日志文本,通过操作 AOF 稳健,可以处理误操作。
缺点
  • 比起 RDB 占用更多的磁盘空间;
  • 恢复备份速度要慢;
  • 每次读写都同步的话,有一定的性能压力;
  • 存在个别 Bug,造成不能恢复。

主从复制

复制原理

  • slave 启动成功连接到 master 后会发送一个 sync 命令(同步命令)。
  • master 接到命令启动后台的存盘进程,对数据进行持久化操作,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master 将传送整个数据文件(rdb)到 slave,以完成一次完全同步。
  • 当主服务进行写操作后,和从服务器进行数据同步。
  • 全量复制:而 slave 服务在接收到数据库文件数据后,将其存盘并加载到内存中。
  • 增量复制:master 继续将新的所有收集到的修改命令依次传给 slave,完成同步。
  • 只要是重新连接 master,一次完全同步(全量复制)将被自动执行。

redis为什么这么快

Redis之所以速度快,主要是因为以下几个方面:

  1. 内存存储:Redis将数据存储在内存中,这样可以避免频繁的进行写盘操作,降低了响应时间。内存的读写速度远超过磁盘,使得Redis能够实现极低的读写延迟。
  1. 单线程模型:虽然Redis采用单线程模型,看似可能成为性能瓶颈,但实际上通过高效的event loop机制,Redis能够处理大量并发连接,而不会出现性能下降。这是因为Redis内部的事件循环机制将请求排队并以非阻塞方式处理。
  1. 数据结构支持:Redis支持多种复杂数据结构,如字符串、列表、集合、有序集合和哈希表等。这些数据结构在某些场景下能够显著提高性能。例如,有序集合可以用于实现排行榜功能,哈希表可用于存储对象属性。
  2. 持久化机制:尽管Redis主要是一个内存数据库,但它也支持将数据持久化到磁盘。Redis提供了两种持久化选项:快照(snapshot)和日志(append-only file)。这些机制确保了即使在服务器重启时,数据也不会丢失,满足了需要持久化数据的应用场景。1
  1. 非阻塞I/O多路复用技术:Redis使用非阻塞I/O模型,避免了线程上下文切换和系统调用带来的开销,从而大幅提高了并发吞吐量。245
  1. 预分配内存:在初始化时,Redis会预先分配一定量的内存空间,避免了频繁的内存分配和释放过程,提高了性能。2
  1. 高性能的多路复用IO模型:Redis使用多路复用IO模型,一个线程可以处理多个客户端连接,通过事件监听的机制,并通过基于事件的回调机制,避免了Redis去一直轮询关注是否有对应的事件发生,从而避免了

哨兵模式

通常配置为单数

哨兵的作用

  • 监控

    不断地检查master与slave是否正常运行 master存活检测,master与slave运行情况检测

  1. sentinel会向master、svale以及其他sentinel获取状态
  2. sentinel之间会组件“对应频道”,大家一起发布信息,订阅信息、收信息同步信息等

image.png

  • 通知

    当被监控的服务出现问题时,向其他(哨兵)发送通知

  • 自动故障转移

    断开master与savle连接,选取一个slave作为master,将其他的svale连接到新的master,并告知客户端的服务地址

反客为主的自动版,即能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。

Redis哨兵模式是一种用于保证Redis集群的高可用性的架构方案。哨兵模式由多个哨兵进程组成,它们负责监控集群中的主节点和从节点,并在发生故障时触发故障转移流程。通过哨兵模式,我们可以确保Redis集群在发生故障时能够自动恢复,从而保证集群的高可用性。

哨兵工作原理

Redis哨兵模式中的哨兵进程分为主哨兵和备用哨兵两种角色。主哨兵负责监控集群中的主节点,如果发现主节点出现故障,则会触发故障转移流程。备用哨兵则负责监控集群中的从节点,如果发现从节点出现故障,则会向主哨兵发出警报。

当主哨兵接收到警报或发现主节点出现故障时,它会开始选举新的主节点。选举过程中,哨兵会按照一定的规则确定新的主节点,例如选取从节点作为主节点,或者选取集群中存活时间最长的节点作为主节点等。

在选举完成后,哨兵会将新的主节点的信息广播给集群中的所有节点,并将从节点转换为新的从节点,进行数据同步。此时,集群已经完成了故障转移,并且恢复了正常的工作状态。

哨兵还具有定期检测集群状态的功能,如果发现集群出现异常,例如主节点或从节点的存活时间过短,则会触发自动恢复流程。这样,我们就可以确保Redis集群始终处于健康的状态,并且能够快速恢复故障。

总的来说,Redis哨兵模式通过监控集群中各个节点的状态,以及在发生故障时触发自动恢复流程,可以有效保证Redis集群的高可用性。

选举规则

  • 根据优先级别,slave-priority/replica-priority,优先选择优先级靠前的。

  • 根据偏移量,优先选择偏移量大的。

  • 根据 runid,优先选择最小的服务。

什么是 redis集群slots

一个 Redis 集群包含 16384 个插槽(hash slot), 数据库中的每个键都属于这 16384 个插槽的其中一个。

集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 *CRC16(key) *语句用于计算键 keyCRC16 校验和 。

集群中的每个节点负责处理一部分插槽。 例如, 如果一个集群可以有主节点, 其中:

  • 节点 A 负责处理 0 号至 5460 号插槽。
  • 节点 B 负责处理 5461 号至 10922 号插槽。
  • 节点 C 负责处理 10923 号至 16383 号插槽。

如何查询集群中的值?

每个主机只能查询自己范围内部的插槽。

cluster keyslot <key>:查询某个 keyslot

cluster countkeysinslot <slot>:查询某个 slot 是否有值。

CLUSTER GETKEYSINSLOT <slot><count>:返回 countslot 槽中的键。

redis缓存穿透、缓存击穿、缓存雪崩

缓存穿透

现象

key 对应的数据在数据源并不存在,每次针对此 key 的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。

比如用一个不存在的用户 id 获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。

如何解决

对空值缓存

如果一个查询返回的数据为空(不管是数据是否不存在),仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟。

设置可访问的名单(白名单):

使用 bitmaps 类型定义一个可以访问的名单,名单 id 作为 bitmaps 的偏移量,每次访问和 bitmap 里面的 id 进行比较,如果访问 id 不在 bitmaps 里面,进行拦截,则不允许访问。

采用布隆过滤器

布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。

布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

将所有可能存在的数据哈希到一个足够大的 bitmaps 中,一个一定不存在的数据会被这个 bitmaps 拦截掉,从而避免了对底层存储系统的查询压力。

进行实时监控

缓存击穿

key 对应的数据存在,但在 redis 中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端 DB 压垮。

如何解决

  • 预先设置热门数据

    redis 高峰访问之前,把一些热门数据提前存入到 redis 里面,加大这些热门数据 key 的时长。

  • 实时调整

    现场监控哪些数据热门,实时调整 key 的过期时长。

  • 使用锁

缓存雪崩

key 对应的数据存在,但在 redis 中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端 DB 压垮。

缓存雪崩与缓存击穿的区别在于这里针对很多 key 缓存,前者则是某一个 key

  1. 数据库压力变大。
  2. 即极少的时间段,查询大量 key 的集中过期情况。

如何解决

  • 构建多级缓存架构

    nginx 缓存 + redis 缓存 + 其他缓存(ehcache等)

  • 使用锁或队列:

    用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况。

  • 设置过期标志更新缓存:

    记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际 key 的缓存。

  • 将缓存失效时间分散开:

    比如我们可以在原有的失效时间基础上增加一个随机值,比如 1~5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

缓存过期策略

  • 定时过期
  • 定期过期
  • 惰性过期

缓存淘汰策略

  • 先进先出

  • 最近最少使用

  • 最不经常使用的

简述全量同步和增量同步区别?

·全量同步:master将完整内存数据生成RDB,发送RDB到slave。后 续命令则记录在repl_baklog,逐个发送给slave。

什么时候执行全量同步?

·slave节点第一次连接master节点时

·slave节点断开时间太久,repl_baklog中的offset已经被覆盖时

什么时候执行增量同步

AOP与RDB的区别

image.png

·slave节点断开又恢复,并且在repl_baklog中能找到offset时

设备优化升级

遇到问题: 1、并发触发升级时,设备并发上包升级进度,对数据库的压力如何处理 处理方式:将设备上报的进度放到kafka中,然后一定时间去拉取kafka的消息,根据设备维度选择升级进度最大的那条进行更新, 这样可以将多条进度信息合并为一条,减少数据库压力 2、200并发确认升级的时候响应时间过长在1.2-1.5s之间 处理方式:优化sql、利用阿里的Arthas分析耗时情况 新建升级任务流程:

优化前存在问题

  • 设备升级过程会卡住,如:我们有1000台设备要升级,但是目前只支持并发50台,如果有一台网络较差,则会一直占用云的资源,导致其它设备等待时间很长,升级不了

  • 存在慢sql,大量的left join

  • 所有的升级入口共用一个繁忙队列

    繁忙队列主要用于当设备上报升级情况的时候将记录以hash结构存到redis中,表示这个设备正在升级中,下发设备升级的时候判断队列是否到达阙值,就会提示繁忙,且如果是升级比较慢的升级包会一直占着队列。

    目前静默升级,施工app、施工平台、社区平台、智慧生活app共用一个升级队列,在存在大量静默升级任务的情况下,手动更新会一直提示繁忙,现在智慧生活app、和施工平台app已经放开限制不判断繁忙。

  • app升级链路后端接口设计不合理

    对某个版本发了2个任务,1个是全量升级,1个是灰度升级。因为只有灰度才生成设备升级任务op_upgrade_state表。所以这个查询的sql要分2次查询。

    第一次查询全部,第二次关联op_upgrade_state查询灰度升级,查询出来后再对这2个结果进行组装,筛选出最高版本。

优化的方向

领域驱动设计

(1)app功能入口改为iot-gateway家居组件,加工好设备,再去op组件获取升级信息。

(2)前面可以看到进行了很多的表关联,目的只是为了找出可升级的版本号,就可以提前预加载好数据,通过领域的概念冗余部分字段,减少组件链路上的交互逻辑。

新建升级任务流程

用户添加升级任务=》流程审批=》审核通过=》发送rabbitmq进行异步添加(in.project.upgrade)

发送已有升级任务

根据规则添加state表=》判断升级类型(静默与普通)=》发送设备已有升级任务事件到rabbitmq(静默与普通是不同的topic)=》

1、住家纬度消费消息判断是否有升级任务=》有的话插入或者更新到表中

2、设备维度消费rabbitmq的消息,判断更新最大可升级版本

image.png

获取升级列表

image.png

确认升级

image.png

获取升级进度、更新升级状态

image.png

上报升级进度

图片4转存失败,建议直接上传图片文件

image.png

发送固件自动升级任务

平台开启xxl调度,每5分分钟一次,查找普通升级任务中符合自动升级的设备

2.在设置的时间段内对设备下发升级任务,设备根据自身逻辑升级。

静默升级

filebeat工作原理

filebeat主要由两个组件构成prospector(探测器)和harvester(收集器),这两类组件一起协作完成Filebeat的工作。当开启Filebeat程序的时候,它会启动一个或多个探测器去检测指定的日志目录或文件。

对于探测器找出的每一个日志文件,Filebeat会启动收集进程,每一个收集进程读取一个日志文件的内容,然后将这些日志数据发送到后台处理程序,后台处理程序会集合这些事件,最后发送集合的数据到output指定的目的地。

这样讲:它会启动一个或多个探测器去检测指定的日志目录或文件。对于探测器找出的每一个日志文件,Filebeat会启动收集进程

sso单点登录

登录

image.png

  1. 用户访问系统1的受保护资源,系统1发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
  2. sso认证中心发现用户未登录,将用户引导至登录页面(带系统1地址)
  3. 用户输入用户名密码提交登录申请
  4. sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话(这时该会话信息保存到cookie中),同时创建授权令牌
  5. sso认证中心带着令牌跳转到最初的请求地址(系统1)
  6. 系统1拿到令牌,去sso认证中心校验令牌是否有效
  7. sso认证中心校验令牌,返回有效,注册系统1
  8. 系统1使用该令牌创建与用户的会话,称为局部会话(seesion),返回受保护资源
  9. 用户访问系统2的受保护资源
  10. 系统2发现用户未登录,跳转至sso认证中心,并将自己的地址和之前和sso认证中心的会话cookie信息作为参数
  11. sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌
  12. 系统2拿到令牌,去sso认证中心校验令牌是否有效
  13. sso认证中心校验令牌,返回有效,注册系统2
  14. 系统2使用该令牌创建与用户的局部会话,返回受保护资源

用户登录成功之后,会与sso认证中心及访问的子系统建立会话,用户与sso认证中心建立的会话称为全局会话,用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过sso认证中心,全局会话与局部会话有如下约束关系。

注销

image.png

用户向系统1发起注销请求

系统1根据用户与系统1建立的会话id拿到令牌,向sso认证中心发起注销请求

sso认证中心校验令牌有效,销毁全局会话,同时取出所有用此令牌注册的系统地址

sso认证中心向所有注册系统发起注销请求

各注册系统接收sso认证中心的注销请求,销毁局部会话

sso认证中心引导用户至登录页面

子系统登录:

1、访问子系统后台地址(http://localhost:9914/

2、后台拦截器检验到未登录,就重定向到认证服务器sso,并带上自己的子系统地址sso-dev.leelen.net/api/login?r…

3、认证服务器sso检验之前是否已经登录过,没有登录过就重定向到认证服务器的前端登录页sso-dev.leelen.net/user/login?…,并带上子系统的后台地址

4、认证服务器sso前端登录页,获取到步骤3重定向过来的子系统url参数(子系统url)

5、用户点击登录时,带上用户名密码和子系统的url地址,到认证服务器的后台。

6、认证服务器检验用户名密码都正确之后,就生成全局token保存到redis中(redis的key:xxl_sso_sessionid#userId,value:用户信息)并设置过期时间例如2h。同时生成ticket保存到redis中(redis的key:* SSO:TICKET:userId value:userId_uuid***)**并设置过期时间5m分钟,将ticket拼接到子系统的url后面(http://localhost:9914/?ticket=xxxx)。最后将登录的结果返回给前端(同时带上token、拼接ticket后的子系统url等参数,code=301)

{token:xxxx

redirectUrl:http://localhost:9914/?ticket=xxxx

code:301\200//重定向的话是code=301

}

7、认证服务器sso前端,根据登录结果(前端有响应拦截器)将用户信息和全局token保存到浏览器的localStorage,并根据返回的code判断是否需要重定向到子系统url,若不需要重定向直接跳转到自己的首页

8、此时访问子系统会带上ticket (http://localhost:9914/?ticket=xxxx),子系统后台拦截器就能拿到ticket之后,会去认证服务器校验ticket是否有效同时将子系统的退出地址拼接到url后面一并携带过去

sso-dev.leelen.net/api/verifyT…

9、认证服务器检验ticket是否有效,若有效则生成子系统的token或者sid并保存到Redis中 (redis的key:* SSO:CLIENT:userId(具体值) value:子系统退出的地址logoutUrl+"sid="+***)不需要设置过期时间,**然后将当前的用户信息返回给子系统。(备注子系统的sid=xxlSsoUser.getUserid().concat("_").concat(xxlSsoUser.getVersion()))

10 、子系统拿到ticket的校验结果后,校验通过会返回用户信息。然后子系统会生成自己的子token或者子会话,将子token保存到redis中 (redis的key:* xxl_client_sessionid_userid value:用户信息***)**

11、至此完成登录认证,子系统跳回到自己访问的页面。然后由于原先的访问连接经过sso认证服务器后会被拼接上ticket,以及经过校验ticket后悔拼接上sid。因此最总浏览器的地址栏展示的是

http://localhost:9914/index?ticket=520503_94d5c330-4f44-4596-8240-9a6a5a63663c&sid=520503_a33ce32d84da41f9bff8d5163d2af16f

因此前端的VUE路由卫士就能够拿到sid,将sid保存到localStorage。以后每次请求时前端的请求拦截器也是axios的service.interceptors.request.use,就会将sid(自token)放到请求头中

12。后端拿到sid直接,去redis中判断是否有该用户,有则放行,实现子系统的访问

直接访问已经登录的子系统

1、访问子系统后台地址(http://localhost:9914/

2、子系统后台拦截器校验没有携带子系统的token或者sid,此时重定向到认证服务sso并带上自己的子系统地址sso-dev.leelen.net/api/login?r…

3、认证服务器不管是在请求头还是请求体都获取不到token或者sid(主要是没有经过sso前端),因此重定向到sso-web登录页sso-dev.leelen.net/user/login?…,此时sso-web前置路由router.beforeEach拦截到本地的localStorage有token或者sid便获取重定向回来的redirect_url ,并拼接上sid跳转到子系统http://localhost:9914/?sid=xxxxx

4、子系统拿到请求参数中的sid后,将sid作为redis的key判断redis中是否存在该用户的信息,有的话就代表已登录直接放行

5、进入到子系统的页面的后会请求后台的获取用户信息的接口,将用户信息和子token(sid)保存到本地浏览器的localstorage中,以后每次请求的时将sid添加到请求头中

直接访问已经登录的sso-web系统

1、直接访问sso-web的页面时,由于sso-web前置路由router.beforeEach拦截到本地的localStorage有token或者sid

2、挡在请求后台某个接口时就会带上sid,后台拦截器就可以获取到sid,并判断redis中是否存在该sid的用户信息,有的话直接放行

分片上传

首先新建分片对象

{
  "fileName": "文件名称",
  "fileSize": 文件大小,
  "fileMD5": "文件md5值",
  "part": [
    分片二进制
  ],
  "partSize": 分片大小,
  "partIndex": 第几分片,
  "partTotal": 总分片
}

步骤:

1、创建分片保存目录

//分片文件存放目录=》/home/data/nas/crm/{parentHexDir}/{sonHexDir}/{md5}/分片文件

2、获取文件的扩展名

3、创建分片文件名=》第几分片+后缀(1.txt)

4、分片文件对象

5、判断分片上传情况,防止用户没有先调用check

判断分片文件是否已上传过,已上传且大小相等返回此分片

6、保存分片到磁盘中

7、判断当前是不是最后一个分片 如果不是就继续等待其他分片 否则合并分片

如果是最后一个分片需要校验:

7.1获取已上传分片总大小

7.2判断分片文件总大小是否等于文件总大小

7.3合并分片

7.4保存最终的文件到数据库

安卓差分包

  1. 独立一个app包管理的微服务,命名为apk,可以管理任意数量的app,包括公司已有的或者将要上架的app,比如智慧生活,家服app等等
  2. 提供后台管理界面,运营人员可以发布新版app,可以查看发布记录
  3. 发布流程全自动化,一键发布
  4. 支持差分包的生产,减小升级apk的大小,可选操作
  5. 支持灰度升级,支持导入用户,或者选择小区的方式(后续可以增加其他灰度的方式),可选操作
  6. 提供RPC接口,供业务方的系统调用,比如app版本检查接口等等
  7. 支持安卓端应用商店地址,有应用商店地址时,优先使用
  8. 该微服务接入SSO和AUTH,放到门户网站后,统一入口,统一的账号体系
  9. 也需要支持iphone应用的发布,iphone没有差分包功能

具体过程

后台上传包过程

在apk系统的后台发布新版app,初始发布新版app时,假设版本为1.0,需要填写本次发版的一些基本信息,并且上传该包,后台首先会自己保留一份该app包,再自动把该包传输到前置配置了CDN加速的那台nginx上的指定目录,上传完后,获得app 1.0版本的公网的CDN加速链接的地址,这样就可以让终端用户进行下载了,当然后台也可以不走CDN直接下载该包

假设现在要发布新版1.1的app,填写本次发版的一些基本信息,并且上传该包,后台首先先会自己保留一份该app包,然后找到上一次发版的app包1.0,此时来生成差异包patch1.1_1.0,自己保留一份,再把原始的1.1的app包和差异包patch1.1_1.0一同传输到前置配置了CDN加速的那台nginx上的指定目录中,获得app 1.1版本的公网的CDN加速链接的地址,以及app 1.1版本和1.0版本差异包的公网的CDN加速链接的地址

依赖

差分包的生成在服务端,依赖于基于C语言的bsdiff

java程序调用的话需要通过jni的方式

合并在安卓端,基于bspatch,效果见后面的视频

智慧康养

image.png

描述介绍

当智能化设备感知到数据异常上报报警事件时,外部康养系统需要接收到该报警消息通知管理人员,管理人员根据设备类型进行上门巡查确认或指派服务人员上门服务,第一时间识别危险和隐患,保障老人居家安全。

rabbitmq

架构设计

image.png

rabbitmq的持久化机制

1、交换机持久化:exchange_declare创建交互机时通过参数指定 2、队列持久化:queue declare创建队列时通过参数指定 3、消息持久化:new AMQPMessage创建消息时通过参数指定

rabbitmq启动时会创建两个进程,一个负责持久化消息的存储,另一个负责非持久化消息的存储(内存不够时

rabbitmq事务消息机制(可以不用管)

RabbitMQ事务消息 通过对信道设置实现 \1. channel.txSelect();通知服务器开启事务模式;服务端会返回Tx.Select-Ok \2. channel.basicPublish;发送消息,可以是多条,可以是消费消息提交ack 3.channel.txCommit()提交事务; 4.channel.txRollback()回滚事务; 消费者使用事务: \1. autoAck=false,手动提交ack,以事务提交或回滚为准; \2. autoAck=true,不支持事务的,也就是说你即使在收到消息之后在回滚事务也是于事无补的,队列已经把消息移除了 如果其中任意一个环节出现问题,就会抛出loException异常,用户可以拦截异常进行事务回滚,或决定要不要重复消息。事务消息会降低rabbitmg的性能

rabbitmq如何保证消息的可靠性

1、使用事务消息 2、使用消息确认机制 发送方确认: ·channel设置为confirm模式,则每条消息会被分配一个唯一id ·消息投递成功,信道会发送ack给生产者,包含了id,白调ConfirmCallback接口·如果发生错误导致消息丢失,发生nack给生产者。回调ReturnCallback接口·ack和nack只有一个触发,且只有一次,异步触发。可以继续发送消息 接收方确认: ·声明队列时,指定noack=false,broker会等待消费者手动返回ack、才会删除消息,否则立刻删除·broker的ack没有超时机制,只会判断链接是否断开,如果断开、消息会被重新发送

rabbitmq的死信队列、延迟队列原理

死信消息: 1.消息被消费方否定确认,使用channe1.basicNack 或channel.basicReject,并且此时requeue属性被设置为fa1se。 2.消息在队列的存活时间超过设置的TTL时间。 3.消息队列的消息数量已经超过最大队列长度。 那么该消息将成为死信消息。如果配置了死信队列信息,那么该消息将会被丢进死信队列中,如果没有配置,则该消息将会被丢弃 为每个需要使用死信的业务队列配置一个死信交换机,同一个项目的死信交换机可以共用一个,然后为每个业务队列分配一个单独的routeKey,死信队列只不过是绑定在死信交换机上的队列,死信交换机也不是什么特殊的交换机,只不过是用来接受死信的交换机,所以可以为任何类型【Direct、Fanout、Topic)

TTL:一条消息或者该队列中的所有消息的最大存活时间 如果一条消息设置了TTL属性或者进入了设置TTL属性的队列,那么这条消息如果在TTL设置的时间内没有被消费,则会成为“死信”。如果同时配置了队列的TTL和消息的TTL,那么较小的那个值将会被使用。 只需要消费者一直消费死信队列里的消息

消费者消息确认机制

image.png

消费者消息消费失败处理策略

image.png

如何保证rabbitmq消息的可靠性

image.png

# kafka


架构设计

image.png

和rabbitmq不同的时候,消息是保存到topic中,有分区、副本的概念

高性能高吞吐的原因

1、磁盘顺序读写:保证了消息的堆积

2、零拷贝:避免CPU将数据从一块存储拷贝到另外一块存储的技术 3、分区分段+索引

4、批量压缩:多条消息一起压缩、降低带宽

5、批量读写

kafka副本同步机制

Kafka消息高可靠解决方案

消息发送:

  • .ack:0、不重试,1、lead写入成功就返回了,all/-1、等待ISR同步完再返回.
  • unclean.leader.election.enable : false,禁止选举ISR以外的follower为leader·
  • tries>1,重试次数
  • ·min.insync.replicas>1:同步副本数,没满足该值前、不提供读写服务、写操作会异常

消费:

  • 手工提交offset
  • broker: 减小刷盘间隔
  • 事务消息

kafka的rebalance机制

什么叫rebalance,要么是topic的分区数发生了变化,要么是消费者组的消费者发生了变化

发送重平衡的时间

  • consumer group中的消费者与topic下的partion重新匹配的过程何时会产生rebalance:
  • ·consumer group中的成员个数发生变化 consumer消费超时
  • ·group订阅的topic个数发生变化
  • ·group订阅的topic的分区数发生变化

coordinator: 通常是partition的leader节点所在的broker,负责监控group中consumer的存活,consumer维持到coordinator的心跳,判断consumer的消费超时 .coordinator通过心跳返回通知consumer进行rebalance .consumer请求coordinator加入组,coordinator选举产生leader consumer · leader consumer从coordinator获取所有的consumer,发送syncGroup(分配信息)给到coordinator.coordinator通过心跳机制将syncGroup下发给consumer·完成rebalance

rocketmq

rocketmq的架构设计

  • 其中的queue相当于kafka中的partion
  • nameservice相当于kafka中的zk
  • nameservice主要是维护broker、consumer、product的路由信息
  • 每个broker要与naeservice建立长连接,底层是居于netty实现的。长连接用于维护心跳,
  • 生产者或许消费者启动的时候都要去nameserve拉取topic的信息

image.png

rocketmq的事务消息原理

image.png

上图说明了事务消息的大致方案,其中分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。 1.事务消息发送及提交: (1) 发送消息(这个消息暂时称为:half消息(半事务消息)) (2) 服务端响应消息写入结果。 (3) 根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行) (4) 根据本地事务状态执行Commit或者Rollback(Commit操作生成消息索引,消息对消费者可见)

2.补偿流程: (1) 对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查” (2) Producer收到回查消息,检查回查消息对应的本地事务的状态 (3) 根据本地事务状态,重新Commit或者Rollback

image.png

image.png

rocketmq怎么实现顺序消息

image.png

rocketmq持久化机制

image.png

.commitLog:日志数据文件,被所有的queue共享,大小为1G,写满之后重新生成,顺序写 .consumeQueue: 逻辑queue,消息先到达commitLog、然后异步转发到consumeQueue,包含queue 在 CommitLog 中的物理位置偏移量Offset,消息实体内容的大小和Message Tag 的 hash值。大小约为600W个字节,写满之后重新生成,顺序写 · indexFile:通过key或者时间区间来查找CommitLog中的消息,文件名以创建的时间戳命名,固定的单个 IndexFile大小为400M,可以保存2000W个索引 所有队列共用一个日志数据文件,避免了kafka的分区数过多、日志文件过多导致磁盘IO读写压力较大造成性能瓶颈,rocketmq的queue只存储少量数据、更加轻量化,对于磁盘的访问是串行化避免磁盘竞争,缺点在于:写入是顺序写,但读是随机的,先读ConsumeQueue,再读CommitLog,会降低消息读的效率

原理:* 消息发送到broker后,会被写入commitLog,写之前加锁,保证顺序写入。然后转发到consumeQueue* 消息消费时先从consumeQueue读取消息在CommitLog中的起始物理偏移量Offset,消息大小、和消息Tag 的 HashCode值。在从CommitLog读取消息内容 ·同步刷盘, 消息持久化到磁盘才会给生产者返回ack,可以保证消息可靠、但是会影响性能 ·异步刷盘: 消息写入pageCache就返回ack给生产者,刷盘采用异步线程,降低读写延迟提高性能和吞吐

rocketmq如何保证消息不丢失

image.png

JAVA IO与Netty

异步和同步的理解

调用者发送请求,如果等着对方回应之后采取做其他事情就是同步,如果发送请求之后不等着对方回应就去做其他事情就是异步

阻塞和非阻塞

被调用者收到请求之后,做完请求任务之后才给出反馈就是阻塞,收到请求之后马上给出反馈然后再去做事情就是非阻塞

netty采用的的是异步非阻塞

image.png

同步阻塞BIO

image.png

服务器实现模式为一个链接启动一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如何这个连接不做任何事情会造成不必要的线程开销

BIO:是以流的方式进行处理数据

同步非阻塞NIO

image.png

NIO有三个核心部分:channel(通道)、Buffer(缓冲区)、Selector(选择器)

NIO是以块或者缓冲区的方式进行处理,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中

异步阻塞

image.png

服务器实现模式为一个线程处理多个请求,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理

异步非阻塞AIO

image.png

Netty-NIO

  • 当客户端连接时,会通过serveSocketChannel得到SocketChannel
  • selector进行监听,sekect()方法,返回有事件发送的通道的个数
  • 将SocketChannel注册到selector上,register(),一个selector上可以注册多个sockertChannel
  • 注册后返回一个selectionKey,会和该selector关联(集合)
  • 进一步得到各个selectionKey(有事件发送)
  • 在通过selectionKey反向获取SocketChannel,方法channel()
  • 可以通过得到channel,完成业务处理

NIO与零拷贝

所谓的零拷贝是没有cpu的拷贝

nmap优化

mmap是通过内存映射,将文件映射到内核缓冲区,同时用户控件可以共享内核控件的数据(kemel buffer与User buffer 可以共享),这样我们的数据就可以直接在内核缓冲区通过cpu拷贝到Socket buff缓冲区中。这样在进行网络传输是,就可以减少内核空间到用户空间的拷贝次数,如图

image.png

拷贝次数4次减少到了3次

上线文的切换为3次,比传统少了一次

sendFile优化

在linux2.1的版本中原理如下:数据根本不经过用户态,直接从内核缓冲区进入到socket buffer,同时由于和用户态完全无关,就减少了一次上线文切换

image.png

拷贝次数4次减少到了3次

上线文的切换为由3次减少到了2次,比mmap少了一次

在linux2.4的版本中做了一些修改,避免了从内核缓冲区拷贝到socket buffer的操作,直接拷贝到协议栈,从而再次减少了数据拷贝

image.png

由原先的4次拷贝减少到了2次拷贝,减少了2次cpu的拷贝

2次上线问切换

mmap和sendFile的区别

  • mmap适合小数据量读写,sendFile适合大文件传输
  • mmap需要4次上下文切换,3次数据拷贝:sendFile需要3次上线文切换,最少2次数据拷贝
  • sendFile可以利用DMA方式,减少了cpu拷贝,mmap则不能(必须从内核拷贝到socket缓冲区)

各种IO模型对比

BIO(同步阻塞io)

特点:

  • 采用阻塞io模型获取输入的数据
  • 每个连接都需要独立的线程完成数据的输入,业务处理,数据返回

存在问题

  • 当并发数很大,救护创建大量的线程,占用很大的系统资源
  • 连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在read操作,造成线程资源浪费

NIO(同步非阻塞)

特点:

  • NIO有三个核心部分:channel(通道)、Buffer(缓冲区)、Selector(选择器)
  • NIO是以块或者缓冲区的方式进行处理,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中
  • 采用一个selector线程处理多个请求,进行轮询,实现多路复用

存在问题

存在空轮询问题

Reactor线程

  • 单reactor单线程
  • 多reactor多线程
  • 主从reactor多线程

多reactor多线程

  1. Reactor 对象通过select监控客户端请求 事件,收到事件后,通过dispatch进行分发2)如果建立连接请求,则右Acceptor通过 accept处理连接请求,然后创建一个Handler对象处理完成连接后的各种事件 3)如果不是连接请求,则由reactor分发调用连接对 应的handler来处理

  2. handler只负责响应事件,不做具体的业务处理, 通过read 读取数据后,会分发给后面的worker线程池的某个线程处理业务

  3. worker线程池会分配独立线程完成真正的业务, 并将结果返回给handler 6) handler收到响应后,通过send将结果返回给 client

image.png

主从reactor多线程

netty模型

  • 简单版

    BossGroup线程维护selector,只关注accecpt

    当接收到 Accept事件,获取到对应的socketChannel,封装成NioScoketChannel并注册到worker线程(时间循环),并进行维护

    当worker线程监听到selector中通道发生自己敢兴趣的事件后,就进行处理(就有handler),注意handle以及加入到通道

  • 详细版

    1. Netty抽象出两组线程池BossGroup专门负责接收客户端的连接, WorkerGroup 专门负责网络的读写
    2. BossGroup和 WorkerGroup 类型都是NioEventLoopGroup
    3. NioEventLoopGroup相当于一个事件循环组,这个组中含有多个事件 循环,每一个事件循环是NioEventLoop
    4. NioEventLoop 表示一个不断循环的执行处理任务的线程,每个 NioEventLoop都有一个selector,用于监听绑定在其上的socket的网络通讯
    5. NioEventLoopGroup 可以有多个线程,即可以含有多个NioEventLoop6)每个Boss NioEventlbop循环执行的步骤有3步
    1. 轮询accept事件
    2. 处理accept事件,与client建立连接,生成NioScocketChannel,并将 其注册到某个worker NIOEventLoop上的selector
    3. 处理任务队列的任务,即runAllTasks7)每个Worker NIOEventLoop循环执行的步骤
    4. 轮询read, write 事件
    5. 处理i/o事件,即read, write事件,在对应NioScocketChannel处理 3.处理任务队列的任务,即runAllTasks

image.png

Netty之tcp粘包和拆包问题

TCP是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端与服务端)都要有一一对应的socket,因此发送端为了将多个发送给接收端的包,更有效的发给对方,使用了额优化方法,将多次间隔较小且数据量小的数据,合并成一个大的数据块。然后进行风暴,这样做随访提高了效率。但是接收端就难分辨出完整的数据包,因为面向流的通信是无消息保护边界的

解决方案:使用自定义协议+编解码器来解决

关键就是要解决服务器端每次读取的数据长度问题,这个问题解决了就不会出现服务器多读少读数据的问题,从而避免的TCP粘包和拆包问题

Netty之编码与解码

编写网络应用程序是,因为数据在网络中传输的都是二进制字节码数据,在发送数据时就需要编码、接收数据时就需要解码

Netty之心跳机制

在服务端的initChannel方法中添加

  /**
         * todo 如果想要开启心跳机制的记得开起来
         * IdleStateHandler 是netty提供的空闲状态处理器
         * 参数1:long readerIdleTime, 表示多少时间没有读,就会触发一个心跳检测包检测是否连接
         * 参数2: long writerIdleTime, 表示多少时间没有写,就会触发一个心跳检测包检测是否连接
         * 参数3:long allIdleTime 表示多少时间没有读写,就会触发一个心跳检测包检测是否连接
         * 当IdleStateHandler 触发后,机会传递给管道的下一个handler去处理通过调用下一个jandler的
         * userEvetTiggered,在该方法中去处理IdleStateHandler(读空闲、写空闲、读写空闲)
         */
        socketChannel.pipeline().addLast(new IdleStateHandler(3,5,7, TimeUnit.SECONDS));
       socketChannel.pipeline().addLast(写上下个handler)

Netty服务端在接到到消息时需要判断接收到的消息与发送的是否一致

在解码器进行数据解码时,需要判断缓存区(byteBuf)的数据是否足够,否则接收到的结果会和期望的结果不一致

java线程

CAS 的特点

  • CAS是基于乐观锁的思想:
  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
  • CAS体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思
  • 因为没有使用synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一9如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响 、

cas为什么无锁效率高

  • 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而synchronized会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。打个比喻 线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速..恢复到高速运行,代价比较大
  • 但无锁情况下,因为线程要保持运行,需要额外CPU的支持,CPU在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。

wait和sleep的区别?

  • 单词不一样。
  • sleep属于Thread类中的static方法、wait属于Object类的方法
  • sleep属于TIMED WAITING,自动被唤醒、wait属于WAITING,需要手动唤醒。
  • sleep方法在持有锁时,执行,不会释放锁资源、wait在执行后,会释放锁资源。
  • sleep可以在持有锁或者不持有锁时,执行。wait方法必须在只有锁时才可以执行。

Lock锁和同步锁(synchronized)的选择

  1. 类型不同 synchronized是关键字。修饰方法,修饰代码块 Lock是接口
  2. 加锁和解锁机制同步 synchronized是自动加锁和解锁,程序员不需要控制。 Lock必须由程序员控制加锁和解锁过程,解锁时,需要注意出现异常不会自动解锁
  3. 异常机制 synchronized碰到没有处理的异常,会自动解锁,不会出现死锁。 Lock碰到异常不会自动解锁,可能出现死锁。所以写Lock锁时都是把解锁放入到finally{}
  4. Lock功能更强大 Lock里面提供了tryLock()/isLocked()方法,进行判断是否上锁成功。synchronized因为是
  5. Lock性能更优 如果多线程竞争锁特别激烈时,Lock的性能更优。如果竞争不激烈,性能相差不大。
  6. 锁住内容不同 synchronized可以锁方法、可以锁代码块 Lock只可以锁代码块

线程生命周期

image.png

java集合

List系列集合:添加的元素是有序、可重复、有索引

Set系列集合:添加的元素是无序、不重复、无索引

List

ArrayList

ArrayList 是这个单项链表 LinkedList集合是个双向链表

集合原理

  • arrayList底层是数组结构的,数组默认长度为10
  • 当数据添加满了之后,会自动扩容1.5倍
  • 存满时,会扩容1,5倍
  • 如果一次添加多个元素,1.5倍还放不下,则新创建数组的长度以实际为准

image.png

LinkedList

底层数据结构是双向链表,查询慢,守卫操作的数据是极快的,所以多了很多收尾操作的可有api

是个双向链表

image.png

源码分析

image.png

Set

  • 无序:存取顺序不一致
  • 不重复:可以去除重复
  • 无索引:没有带索引的方法,索引不能使用普通for循环遍历,也不能通过索引来获取元素

Set集合实现类

hashSet:无序、不重复、无索引
LinkedHashSet:有序、不重复、无索引
TreeSet:可排序,不重复、无索引

TreeSet有两种排序规则

实现comparable接口,指定比较规则

创建集合时传递Comparator比较器对象,指定比较规则 

hashSet

  • 哈希表是一种对于增删改查数据性能都比较好的结构
HashSet底层原理
  • 创建一个默认长度16,默认加载因为0.75的数组,数组名table
  • JDK8以前:数组+链表 JDK8以后:数组+链表+红黑树
  • 根据元素的哈希值跟数组的长度计算出应存入的位置
  • 判断当前位置是否为null,如果是null直接存入
  • 如果位置不为null,表示有元素,则调用equals方法比较属性值
  • 一样:不存 不一样:存入数组,形成链表
  • JDK8以前:新元素存入数组,老元素挂在新元素下面
  • JDK8以后:新元素直接挂在老元素下面
  • 当链表长度大于8 而且数组成都大于等于64时,才会采用数组+链表+红黑树
    image.png

image.png

哈希表组成:

  • JDK8之前:数组+链表
  • JDK8之后:数组+链表+红黑树

哈希值

根据hash计算公式: 计算出存储的位置 int index=(数组长度-1)&哈希值

哈希值

  • 根据hashCode方法算出来的int类型的整数
  • 该方法定义在Object类中,所有对象都可以调用,默认使用地址值进行计算
  • 一般情况下,会重写hashCode方法,利用对象内部的属性值计算哈希值

对象的哈希值特点

  • 如果没有重写hashCode方法,不同对象计算出的哈希值是不同的

地址值:0x0011 哈希值:6794651616

地址值:0x0022 哈希值:89476546132

  • 如果已经重写hashcode方法,不同对象只要属性值相同,计算出的哈希值就是一样

  • 在小部分情况下,不同的属性或者不同的地址值计算出来的哈希值有可能一样(哈希碰撞)
    image.png

Map

HashMap

  • hashMAp是map里面的一个实现类
  • 无序、不重复、无索引
  • hashMAp跟hashSet底层原理一模一样,都是采用哈希表结构 1.8之后采用了红黑树

LinkedHashMap

  • 由键决定:有序、不重复、无索引
  • 这里的有序是保证存储和取出的元素顺序一致
  • 原理:底层数据结构是哈希表,只是每个键值对元素又额外的多了一个双连表的机制记录存储的顺序

TreeMap

  • treeMap跟TreeSet底层原理一样、都是红黑树结构

  • 由键决定:可排序、不重复、无索引

  • 可排序:对键进行排序

  • 默认从小到大排序,也可以自己规定键的排序规则,有两种排序规则

    实现comparable接口,指定比较规则

    创建集合时传递Comparator比较器对象,指定比较规则

java数据结构

是先进后出,后进先出

队列

是先进先出,后进后出

数组

数组是一种查询快,增删慢的模型

  • 查询速度快:查询数据通过地址值和索引定位,查询任意数据耗时相同
  • 删除效率低:要将原始数据删除,同时后面每个数据前移

添加效率极低:添加位置后的每个数据后移,在添加元素

链表

  • 链表中的节点是独立的对象,在内存中是不连续的,每个节点包含数据值和狭义和节点的地址
  • 链表查询慢,无论查询哪个数据都要从头开始找
  • 增删相对快

这个是单项链表

image.png

前一个节点记录后一个节点的地址

java基础知识

JVM模型

image.png

一个类什么时候进入JVM

3个时间节点加载

  • 虚拟机启动时,执行main()方法的时候
  • new对象的时候
  • 读取 静态字段或者静态方法的时候

加载器类别

  • [启动类加载器(Bootstrap Class Loader)]
  • [扩展类加载器(Extension Class Loader)]
  • [应用程序类加载器(Application Class Loader)]

应用程序类加载器 是 Java 类加载器中的一种。它负责加载应用程序的类和资源文件

java类加载、一个类进入JVM后,它经历了什么

加载=》验证=》准备=-》解析=》初始化=》使用=》卸载

一个类如何初始化

字节码中clinit 方法: 用于静态变量或者静态块的初始化。当类class初始化的时候,调用该方法 字节码中 init方法 用于对象实例初始化是被调用到。每次new对象的时候,都会调用该方法

什么是类的主动引用

主动引用:A调用B,对B类进行引用时如果没有进行初始化,则先触发其初始化叫做主动引用

主动引用6种定义

  • new一个对象的时候会发生类初始化,调用clinit方法
  • 调用类中的静态成员,除了final字段,final被调用但是没有初始化类
  • 调用某个类的静态方法,那这个类一定先初始化 clint
  • 反射调用, 触发类初始化
  • 先初始化父类,在初始化子类 调用clinit

什么是类的被动引用

被动引用:通过子类引用父类的静态字段,不会导致子类初始化

A类调用B类,不会触发B类的初始化

什么是类加载器classLoader

类装载工作由classLoader及其子类负责,classLoader是一个很重要的java执行时系统组件,他负责在运行时查找和装入class字节码文件

jvm在运行时会产生三个classLoader:boostrap(启动类加载器),ectensionClassLoader(扩展类装载器)和appClassLoader(系统类装载器)

image.png

什么是双亲委派模型

主要是为了解决不安全性问题

image.png

向上委托:指现在自己的加载器,判断是有自己已经加载了,如果加载了直接返回class对象,没有的话,向上委托给父加载器

每个加载器都有自己的内存空间,先看在不在

1、当某个类加载器(例如自定义加载)要加载类时,它先把该类加载请求委派给父类加载器去完成,而不会自己去加载该类

2、每层的类加载器都是如何,因此所有的加载请求最终都应该传送顶层的跟类加载器

3、只有当父类加载反馈自己无法完成该加载请求(它的搜索范围中没有找到所需要的类)时,子加载器才会自己去加载

如何打破双亲委派模型

答案就是重写loadclass方法

java 栈与堆

栈:

  • 基本数据类型、局部变量存放在栈帧内,方法执行完毕(栈帧出栈)立即释放,节约空间
  • 栈帧内的变量,没有 默认值,需要手工设置
  • 栈帧只存留在单个线程中,其他线程访问不了
  • 可以使用-Xss来滴你故意栈内存大小
  • 当推栈内存已满时,java运行时抛出java .lang.stackOverFlowError

堆:

  • 数组和对象存于堆中,用完后(栈帧不在引用),靠垃圾回事算法清除
  • 堆内对应的变量都有默认值
  • 所有的线程共享堆,及所欲的栈帧的变量都能引用堆的内存地址
  • 可以使用-Xms和-Xmx的JVM选项来定义堆内存的启动大小和最大大小
  • 如何堆内存已满,则抛出java.lang.OutOfMemoryEoor java 对空间错误

java长命对应与短命对象

长命对象:一直有人引用的就是长命对象

短命对象:没人引用就是短命对象,也成为垃圾对象,因为没人用它,即没人应用它

java为什么要设计分代模型,年轻代于老年代

年轻代:存储短命对象

老年代:存储长命对象

java有几种锁

  • 无锁

  • 偏向锁

  • 轻量级锁

  • 重量级锁

CAS汇编语言底层 的实现

是采用**lock cmpChg**指令实现的

什么是无锁什么是匿名偏向锁

无锁

当前我们的代码是没有加锁的,当前偏向锁为0,锁类型为01,故无锁的对象头markword格式,偏向锁为,所类型为01

匿名偏向锁

什么是偏向锁

如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况下,则线程是不需要触发同步的,这猴子那个情况下,就会给线程加有个偏向锁

匿名偏向锁与偏向锁的区别

  • 匿名偏向锁无线程id=》代表没人用,没人用就没有线程id
  • 偏向锁有线程id =》代表有人用,有人用就是有线程id

轻量级锁

轻量级锁是发送发生在java虚拟机层面的,不需要经过操作系统,因为是java语言 自身在while循环 一直在循环是要消耗CPU的,消耗系统资源,如果等待太多是不适合采用轻量级的

重量级锁

  • 当轻量级锁处理不了的时候,直接交给操作系统来处理,操作系统os决定,操作系统帮我们协调哪些线程该休息哪些线程该执行,锁轮到谁了交给操作系统老大来决定。

  • 操作系统对于锁的管理过程就是排队,就是在操作系统级别每一把锁都关联一个队列,重量级是进入等待队列等着

在什么情况下升级为轻量级锁

有两条及以上的线程竞争就会产生轻量级锁

多个线程在不同时段获取同一把锁,既不存在锁竞争情况,也就没有线程阻塞。针对这种情况JVM采用轻量级锁来避免线程的阻塞与唤醒

轻量级锁升级为重量级锁

- 耗时过程调用wait()方法
- 早期jvm是自旋次数过多,后来根据jvm自适应什么时候升级为重量级有jvm自己觉得

锁的升级过程

image.png

对象在内存中的内存布局

最终的对象大小必须被8整除,不能被8整除的需要进行填充
java默认是有开启压缩,每个引用占用4字节。没有开启压缩每个引用占用8字节

  • mark world 4字节(保存GC,锁、hashcode等内容)

  • 类型指针 4字节

  • 实例数据 看成员变量是什么类型有多少个,比如Integer占用4个字节

  • 对齐填充 填充到可以被8整除

image.png

  • 例子一

在内存中占用16个字节=》16字节=8字节的mark world+4字节类型指针+4字节填充

private  static  class  T{
    
}

public static void main(String[] args) {
    T t=new T();
    
}

image.png

  • 例子二

在内存中占用16个字节=》16字节=8字节的mark world+4字节类型指针+4字节实例数据

private  static  class  T{
   private Integer  a;//4节点 
}

public static void main(String[] args) {
   T t=new T();

}

image.png

  • 例子三 在内存中占用24个字节=》24字节=8字节的mark world+4字节类型指针+12字节实例数据
private  static  class  T{
   private Integer  a;//4节点
   private  long b;// 8字节
}

public static void main(String[] args) {
   T t=new T();

}

image.png

  • 例子四

在内存中占用32个字节=》32字节=8字节的mark world+4字节类型指针+13字节实例数据+7字节填充

private  static  class  T{
    private Integer  a;//4节点
    private  long b;// 8字节
    private boolean  c;//1字节 
}

public static void main(String[] args) {
    T t=new T();
    System.out.println(ClassLayout.parseInstance(t).toPrintable());


}

image.png

  • 例子五
    在内存中占用32个字节=》32字节=8字节的mark world+4字节类型指针+17字节实例数据+3字节填充
private  static  class  T{
    private Integer  a;//4节点
    private  long b;// 4字节
    private boolean  c;
    private  String d="fdkfhkdfldk";//4字节压缩引用
}

public static void main(String[] args) {
    T t=new T();
    System.out.println(ClassLayout.parseInstance(t).toPrintable());


}

image.png

CAS的ABA问题如何解决

采用版本号

并发的三大要素

- 原子性、有序性、可见性

volatile的五层实现

- java源码=》采用volatile标记某个变量
- ByteCode字节码=》ACC_VOLATILe
- JVM虚拟机规范=》JVM内存屏障
- Hotspot实现=》汇编语言调用
- CPU级别=》MESI、原语支持、总线锁

一个Object对象在JVM内存中占用多大

16字节=8字节的mark world+4字节类型指针+4字节填充

image.png

java四种引用类型

强、软、弱、虚

弱: 体现在threadLoacl的key中

虚: 一般虚引用用在java垃圾回收器,管理堆外内存

ThreadLoacl的原理是什么、使用场景有哪些

Thread类中有两个变量threadLocals和inheritabelThreadLocals,二者都是threadloacal内部类threadLocalMap类型的变量,我们通过查看内部ThreadLocalMap可以发现实际上它类似于一个HashMap,在默认情况下,每个线程中的这个两个变量都是null。

初此之之外,每个线程的本地变量不是存放在ThreadLocal实例中,而是放在调用线程的ThreadLocals变量里面。也就是说ThreadLocal类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载本地变量的载体,通过set方法将value添加到调用线程的threadLocals,当调用get方法适合能够从他的threadLocals中取出比那里。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的ThreadLocas中,所有不使用的时候一定要remove掉,防止出现内存溢出

ThreadLocal有哪些内存泄漏问题,如何避免

每个Thread都有一个threadLocalMap的map,该map的key为Threadcal实例,它为一个弱引用,我们知道弱引用有利于GC回收,当ThreadLocal的key=null,Gc就会回收这部分空间当时value切不一定能过被回收,因为它还与current Thread

存在一个强引用关系

java反射机制

反射其实就是得到类的字节码文件

Java反射机制是指在运行时动态地获取一个类的信息并能够操作该类的属性和方法的能力

反射机制的原理

Java反射机制的原理主要是通过Class类来实现的。Class类是Java中反射机制的核心类,它可以在运行时动态地获取一个类的信息。Class类的实例对象可以通过三种方式获取:

(1)使用Class.forName()方法获取Class对象。Class.forName()方法接受一个字符串参数,该参数为完整类名,它将返回该类的Class对象。

(2)使用类名.class获取Class对象。例如:String.class。

(3)使用对象.getClass()方法获取Class对象。例如:String str = "hello",则str.getClass()将返回String类的Class对象。

java垃圾清除算法

  • 标记清除 (存在问题:内存碎片化)

  • 标记拷贝(存在问题:浪费内存)

  • 标记压缩或者标记整理(存在问题:效率低)

    新生代垃圾回收都是讲存活的复制到survior,然后将整个eden清除掉

java垃圾收集器

  • serial(几兆到几十兆)=》单线程的垃圾回收用于年轻代与老年代采用,工作在年轻代叫这serial,工作在老年代叫serial old

parallel(几十兆到几百兆)=》parallel并行多线程 工作在年轻代叫这parallel,工作在老年代叫parallel old,1.8默认采用parallel old与parallel scavenge、 组合见图表 concurrent mark sweep(cms)

  • seral与seral old都是使用单线程,运行时业务线程会停止
  • 是paraller scavange 与parallel old都是使用多线程,运行时业务线程扔会停止
  • jDK默认采用是paraller scavange 与parallel old结合使用
  • 并发指的是,GC线程与垃圾回收器线程并行执行,原来是iGC线程已运行业务线程就必须停止
    • 其中CMS是工作在老年代,与之工作在年轻代可以用serial或者parNew,parnew与parallel scavagen是一个意思工作在年轻代的多线程,为什么不用parallel scavagen因为它有些增强(官网说的)所有可以与CMS更好的配合使用

CMS从线程角度,分为四个阶段初始标记=》并发标记=》重新标记=》并发清理

image.png

java动态代理

是基于接口的代理和反射机制来实现的,主要的是实现InvocationHandler接口invoke()方法

Java 动态代理作用

1. 扩展目标对象的功能(aop)
2.实现远程方法调用 

java线程池的底层工作原理

//创建线程池,底层代码
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

1. corePoolSize:线程池中的核心线程数,核心线程会一直存在,即使没有任务执行。

2. maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1

3. keepAliveTime:当线程数大于核心数时,这是多余空闲线程在终止前等待新任务的最长时间。(意思就是本来核心线程三个,已占满,扩展到最大线程数到5,解决完后没有线程任务了,这时设置一个时间,过了这个时间被扩展的2个非核心线程会被回收)

4. unit:keepAliveTime的时间单位。

5. workQueue:任务队列,被提交但尚未被执行的任务,相当于去饭店吃饭,餐桌满了,要在外边排队(阻塞队列)

6. threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可。

7. handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数。

image.png

  1. 在创建了线程池后,等待提交过来的任务请求。
  2. 当调用execute()方法添加一个请求任务时,线程池会做如下判断: 2.1 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务; 2.2 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列; 2.3 如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务; 2.4 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。 3.当一个线程完成任务时,它会从队列中取下一个任务来执行。 4.当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断: 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉;所以线程池的所有任务完成后它最终会收缩到corePoolSize 的大小。

SpringMvc

内置的九大组件

  1. HandlerMapping:用于查找处理器(Handler)。
  2. HandlerAdapter:用于适配器模式,将处理器(Handler)转换为实际执行处理的类。
  3. HandlerExceptionResolver:用于处理处理器抛出的异常。
  4. ViewResolver:用于解析ModelAndView对象并返回对应的视图给客户端。
  5. RequestToViewNameTranslator:将请求信息转换成视图名称。
  6. LocaleResolver:用于获取当前用户的语言设置。
  7. ThemeResolver:用于获取当前的主题设置。
  8. MultiPartResolver:用于处理多部分文件上传。
  9. FlashMapManager:管理FlashMap,通常用于在页面之间传递状态信息

Spring

spring是轻量级的开源的javaee框架,可以把spring理解为一个容器,spring的核心技术有:IOC,AOP ,通过spring来管理整个bean的生命周期,从创建到使用到销毁,各个环节都是由容器来帮我们控制的。其中 spring是轻量级的开源的javaee框架

spring架构

image.png

- 1.Core Container(核心容器)

Beans模块:提供BeanFactory,对Java Bean进行管理,Spring将管理对象称为Bean;

JavaBean实际就是一个普通的Java类,为了规范开发,JavaBean具有如下特点:

① 具有一个公共的、无参的构造方法;

② 对应的属性必须提供了setter和getter方法用于外部属性赋值和获取属性值;

  1. Core模块:提供了Spring框架的基本组成部分,包括IOC(控制反转)和DI(依赖注入)功能;
  2. Context上下文模块:建立在Core和Beans的基础上,它是访问定义和配置的任何对象的媒介,提供了邮件服务、任务调度、远程访问、缓存、JNDI等支持。其中ApplicationContext接口是上下文模块的焦点;
  3. SpEL模块:提供了强大的表达式语言去支持运行时查询和操作对象图;

2.Data Access/Integration(数据访问/集成)

  1. JDBC模块:提供了JDBC的抽象层,减少在开发过程中对数据库操作时的编码;
  2. ORM模块:对象关系映射,对流行的对象关系映射API;
  3. OXM模块:提供了一个支持对象/XML映射的抽象层实现;
  4. JMS模块:Java消息传递服务,包含使用和产生信息的特性;
  5. Transactions事务模块:支持对实现特殊接口以及所有POJO类的编程和声明式事务处理;

3.Web

  1. WebSocket模块:提供了WebSocket和SockJS的实现以及STOMP的支持;
  2. Servlet模块:也称为Spring-webmvc模块,包含了Spring的模型-视图-控制器(MVC)和REST WebServices实现的Web应用程序;
  3. Web模块:提供了基本的Web开发集成特性,如:多文件上传功能、使用Servlet监听器来初始化IOC容器以及Web应用上下文。
  4. Portlet模块:提供了在Portlet环境中使用MVC实现,类似Servlet的功能;

4.其他模块

  1. AOP模块:提供了面向切面编程实现,允许定义方法拦截器和切入点,将代码按照功能进行分离,以降低耦合性;
  2. Aspects模块:提供了与AspectJ的集成功能,AspectJ是一个功能强大切成熟的面向切面编程框架;
  3. Instrumentation模块:提供了类工具的支持和类加载器的实现,可以在特定的应用服务器中使用;
  4. Messaging模块:提供了对消息传递体系结构和协议的支持;
  5. Test模块:提供了对单元测试和集成测试的支持;

BeanFactory

可以把BeanFactory理解为是一个入口或者是容器对象,因为所有创建的对象都是放在容器中,而容器就是通过BeanFactory来访问的 BeanFactory的主要作用是提供Bean的创建、配置、初始化和销毁等基本操作,它可以根据配置文件或注解来创建并管理Bean实例,并提供了各种方法来获取和操作Bean实例。 ApplicationContext继承了BeanFactory提供了一序列更加完善的功能

BeanDefinition

BeanDefinition主要是用来描述Bean,存储Bean的相关信息,主要包括:Bean的属性、是否单例、延迟加载、Bean的名称、构造方法等。

image.png

BeanFactoryPostProcessor

BeanFactoryPostProcessor主要用来修改bean的定义信息,比如要实现替换配置文件的某个${jdbc:username}属性时,就可以继承该接口,然后通过反射获取改实例进行替换

public class MyBeanFactoryPostProcessor extends BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        BeanDefinition bean = beanFactory.getBeanDefinition("bean名称");
    }
}

image.png

image.png

BeanPostProcessor

BeanPostProcessor#postProcessBeforeInitialization BeanPostProcessor#postProcessAfterInitialization

该类主要用在初始化bena前后做一些操作

image.png

而这个BeanPostProcessor在spring的体现就是在aop中

Spring IOC

底层原理:是通过工厂模式与反射实现的

  • ioc思想是基于容器完成,而容器底层就是对象工厂

  • spring提供IOC容器实现两种方式(两个接口)beanfactory与applicationContext

    其中:beanfactory是在加载配置文件的时候不会创建对象,在获取对象使用才回去创建对象。而applicationContext加载配置文件的时候就会把对象进行创建

谈谈你对ioc的理解

spring是轻量级的开源的javaee框架,可以把spring理解为一个容器,spring的核心技术有:IOC,AOP等 ,通过spring来管理整个bean的生命周期,从创建到使用到销毁,各个环节都是由容器来帮我们控制的。其中

  • IOC: 控制反转,原来我们使用的时候对象是由使用者控制的,有了spring之后,可以将整个对象交给容器来帮我们进行管理(理论思想)
  • DI:依赖注入,将对应的属性注入到具体的对象中@Autowired,@Resource, populateBean方法来完成属性注入 容器:存储对象,使用map结构在存储对象,在spring中存储对象的时候一般有三级缓存, singletonObjects存放完整对象,earlySingletonObjects存放半成品对象,singletonFactory用来存放lambda表达式和对象名称的映射,
  • 整个bean的生命周期,从创建到使用到销毁,各个环节都是由容器来帮我们控制的。

简单描述bean的生命周期

  1. 实例化(Instantiation)bean:这是Spring容器根据配置信息或注解创建Bean实例的过程。实例化的方式可以是直接通过构造函数实例化,或者是通过工厂方法实例化。(在堆中开辟一块空间,属性都是默认值)
  2. 属性赋值(Population):在实例化之后,Spring容器会向Bean注入配置文件中的属性值或注解中的属性值。这个过程可以通过setter方法注入,也可以通过字段注入。
  3. 执行前置处理方法
  4. 初始化(Initialization):在属性赋值完成后,Spring容器会调用Bean的初始化方法。这个阶段允许进行一些初始化的操作,比如数据加载、资源准备等。
  5. 执行后置处理方法
  6. 使用(In Use):在这个阶段,Bean可以被应用程序正常使用,即被注入到其他Bean中,或者通过Spring容器获取并调用其方法。
  7. 销毁(Destruction):当应用程序不再需要Bean时,Spring容器负责销毁Bean。这个阶段可以进行资源释放、清理操作。

未命名文件.png

beanFactory与FactoryBean的区别

相同点:都是用来创建对象的
不同点:beanFactory创建对象是标准化的流程流水线,而集成FatoryBean接口后主要用于判断是否单例对象(isSingleton)、获取对象的类型(getObjectType)、用户可以按照自己的任意方式来创建对象getObject()

image.png

spring事务传播机制

多个事务方法相互调用时,事务如何在这些方法之间进行传播,spring中提供了7中不同的传播特性,来保证事务的正常执行:

REQUIRED: 默认的传播特性,如果当前没有事务,则新建一个事务,如果当前存在事务,则加入这个事务

SUPPORTS:当前存在事务,则加入当前事务,如果当前没有事务,则以非事务的方式执行

MANDATORY:当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常

REQUIRED_NEW:创建一个新事务,如果存在当前事务,则挂起改事务

NOT_SUPPORTED:以非事务方式执行,如果存在当前事务,则挂起当前事务

NEVER: 不使用事务,如果当前事务存在,则抛出异常

NESTED:如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样

NESTED和REQUIRED_NEW的区别:

REQUIRED_NEW是新建一个事务并且新开始的这个事务与原有事务无关,而NESTED则是当前存在事务时会开启一个嵌套事务,在NESTED情况下,父事务回滚时,子事务也会回滚,而REQUIRED_NEW情况下,原有事务回滚,不会影响新开启的事务

BeanFactory与ApplicationCOntext的区别

image.png

spring框架中使用了哪些设计模式及应用场景

1.工厂模式,在各种BeanFactory以及ApplicationContext创建中都用到了 2.模版模式,在各种BeanFactory以及ApplicationContext实现中也都用到了 3.代理模式,Spring AOP利用了AspectJ AOP实现的! AspectJAOP 的底层用了动态代理 4.策略模式,加载资源文件的方式,使用了不同的方法,比如:ClassPathResourece, FileSystemResource,ServletContextResource,UrlResource但他们都有共同的借口Resource;在Aop的实现中,采用了两种不同的方式,JDK动态代理和CGLIB代理 5.单例模式,比如在创建bean的时候。 6.观察者模式,spring中的ApplicationEvent,ApplicationListener,ApplicationEventPublisher 7.适配器模式,MethodBeforeAdviceAdapter,ThrowsAdviceAdapter,AfterReturningAdapter 8.装饰者模式,源码中类型带Wrapper或者Decorator的都是

AOP

面向切面编程。利用它可以使得业务部分之间的耦合度降低

AOP底层原理:

1、底层使用了动态代理

(1)有两种情况的动态代理

第一种:有接口,使用JDK动态代理

(1)调用newProxyInstance方法

方法三个参数:

第一:类加载器

第二:增强方法所在的类,这个类实现的接口,支持多个接口

第三:实现这个接口InvocationHandler,创建代理对象,写增强方法,示例代码如下:

第二种:没有接口情况,使用CGLIB动态代理

创建子类的代理对象,增强类的方法

术语

  • 连接点 类里面哪些方法可以被增强,这些方法称为连接点
  • 切入点 实际被真正增强的方法,称为切入点
  • 通知(增强) 实际增强的逻辑部分称为通知:前置通知、后置通知、环绕通知、异常通知、最终通知
  • 切面 是动作 把通知应用到切入点过程

应用场景有:日志记录、安全控制、事务处理、异常处理..

spring webflux

  • webflux使用当前一种比较流行的响应式编程框架
  • webflux是一种异步非阻塞的框架

异步和同步的理解

调用者发送请求,如果等着对方回应之后采取做其他事情就是同步,如果发送请求之后不等着对方回应就去做其他事情就是异步

阻塞和非阻塞

被调用者收到请求之后,做完请求任务之后才给出反馈就是阻塞,收到请求之后马上给出反馈然后再去做事情就是非阻塞

MQTT数据包

MQTT协议中,一个MQTT数据包由:固定头(Fixed header)、 可变头(Variable header)、 消息体(payload)三部分构成。

  • 固定头(Fixed header),所有数据包中都有固定头,包含数据包类型及数据包的分组标识。
  • 可变头(Variable header),部分数据包类型中有可变头。
  • 内容消息体(Payload),存在于部分数据包类,是客户端收到的具体消息内容。

图片

如何解决循环依赖

利用三级缓存

spring三级缓存

三个缓存对应,在获取数据的时候是按照什么顺序来获取的?

先获取一级缓存、没有在获取二级缓存,没有在获取三级缓存,说句当前面的缓存中存在了对象那后面就需要把缓存对象清空 主要用来解决循环依赖问题

  • 第一级缓存:singletonObjects,用于保存实例化、注入、初始化完成的 bean 实例;
  • 第二级缓存:earlySingletonObjects,用于保存实例化完成的 bean 实例;
  • 第三级缓存:singletonFactories,用于保存 bean 创建工厂,以便后面有机会创建代理对象。

springboot

Dubbo

dubbo原理

image.png 1)注册中心(registry)

生产者在此注册并发布内容,消费者在此订阅并接收发布的内容。

2)消费者(consumer)

客户端,从注册中心获取到方法,可以调用生产者中的方法。

3)生产者(provider)

服务端,生产内容,生产前需要依赖容器(先启动容器)。

4)容器(container)

生产者在启动执行的时候,必须依赖容器才能正常启动(默认依赖的是spring容器),

5)监控(Monitor)

统计服务的调用次数与时间等。

image.png 对照上面的整体架构图,大致分为以下8大步骤:

1、服务提供者启动,开启Netty服务,创建Zookeeper客户端,向注册中心注册服务;

2、服务消费者启动,通过Zookeeper向注册中心获取服务提供者列表,与服务提供者通过Netty建立长连接;

3、服务消费者通过接口开始远程调用服务,ProxyFactory通过初始化Proxy对象,Proxy通过创建动态代理对象;

4、动态代理对象通过invoke方法,层层包装生成一个Invoker对象,该对象包含了代理对象;

5、Invoker通过路由,负载均衡选择了一个最合适的服务提供者,在通过加入各种过滤器,协议层包装生成一个新的DubboInvoker对象;

6、再通过交换成将DubboInvoker对象包装成一个Reuqest对象,该对象通过序列化通过NettyClient传输到服务提供者的NettyServer端;

7、到了服务提供者这边,再通过反序列化、协议解密等操作生成一个DubboExporter对象,再层层传递处理,会生成一个服务提供端的Invoker对象;

8、这个Invoker对象会调用本地服务,获得结果再通过层层回调返回到服务消费者,服务消费者拿到结果后,再解析获得最终结果。

Dubbo内置负载均衡策略

Dubbo内置了4种负载均衡策略:

  1. RandomLoadBalance:随机负载均衡。随机的选择一个。是Dubbo的默认负载均衡策略。
  2. RoundRobinLoadBalance:轮询负载均衡。轮询选择一个。
  3. LeastActiveLoadBalance:最少活跃调用数,相同活跃数的随机。活跃数指调用前后计数差。使慢的 Provider 收到更少请求,因为越慢的 Provider 的调用前后计数差会越大。
  4. ConsistentHashLoadBalance:一致性哈希负载均衡。相同参数的请求总是落在同一台机器上。

Ribbon的7 种负载均衡策略

Ribbon的7 种负载均衡策略

1.轮询策略

轮询策略:RoundRobinRule,按照一定的顺序依次调用服务实例。比如一共有 3 个服务,第一次调用服务 1,第二次调用服务 2,第三次调用服务3,依次类推。此策略的配置设置如下:

springcloud-nacos-provider: # nacos中的服务id
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #设置负载均衡

2.权重策略

权重策略:WeightedResponseTimeRule,根据每个服务提供者的响应时间分配一个权重,响应时间越长,权重越小,被选中的可能性也就越低。它的实现原理是,刚开始使用轮询策略并开启一个计时器,每一段时间收集一次所有服务提供者的平均响应时间,然后再给每个服务提供者附上一个权重,权重越高被选中的概率也越大。此策略的配置设置如下:

springcloud-nacos-provider: # nacos中的服务id
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule

3.随机策略

随机策略:RandomRule,从服务提供者的列表中随机选择一个服务实例。此策略的配置设置如下:

springcloud-nacos-provider: # nacos中的服务id
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #设置负载均衡

4.最小连接数策略

最小连接数策略:BestAvailableRule,也叫最小并发数策略,它是遍历服务提供者列表,选取连接数最小的⼀个服务实例。如果有相同的最小连接数,那么会调用轮询策略进行选取。此策略的配置设置如下:

springcloud-nacos-provider: # nacos中的服务id
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.BestAvailableRule #设置负载均衡

5.重试策略

重试策略:RetryRule,按照轮询策略来获取服务,如果获取的服务实例为 null 或已经失效,则在指定的时间之内不断地进行重试来获取服务,如果超过指定时间依然没获取到服务实例则返回 null。此策略的配置设置如下:

ribbon:
  ConnectTimeout: 2000 # 请求连接的超时时间
  ReadTimeout: 5000 # 请求处理的超时时间
springcloud-nacos-provider: # nacos 中的服务 id
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #设置负载均衡

6.可用性敏感策略

可用敏感性策略:AvailabilityFilteringRule,先过滤掉非健康的服务实例,然后再选择连接数较小的服务实例。此策略的配置设置如下:

springcloud-nacos-provider: # nacos中的服务id
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.AvailabilityFilteringRule

7.区域敏感策略

区域敏感策略:ZoneAvoidanceRule,根据服务所在区域(zone)的性能和服务的可用性来选择服务实例,在没有区域的环境下,该策略和轮询策略类似。此策略的配置设置如下:

springcloud-nacos-provider: # nacos中的服务id
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.ZoneAvoidanceRule

微服务相关

nacos的服务注册表结构是怎样的?

Namecespace(环境隔离)=》group(服务分组)=》服务=》集群=》实例

领域驱动设计(Domain-Driven Design,DDD)

四层架构

  • 用户接口层

  • 应用层

  • 领域层

  • 基础层

image.png

三大设计原则

  • 单一职责原则
    一个类只负责单一职责,另外一种理解就是一个类应该只有一个引起他变化的原因
  • 开放封闭原则
    对扩展开放,对修改封闭
  • 依赖反转原则
    程序之间应该只有依赖于抽象接口,而不是依赖于具体实现

SkyWalking

启动成功后会启动两个服务,一个是skywalking-oap-serve,一个是skywalking-web-ui
skywalking-oap-serve服务启动后会暴露两个端口:11800与12800,分别为收集监控数据的端口11800和接收前端请求的端口12800,修改端口可以condif/application.yml

docker

什么是docker

Docker是一个快速交付应用、运行应用的技术: 1.可以将程序及其依赖、运行环境一起打包为一个镜像, 2.运行时利用沙箱机制形成隔离容器,各个应用互不干扰 3.启动、移除都可以通过一行命令完成,方便快捷

Docker和虚拟机的差异:

docker是一个系统进程;虚拟机是在操作系统中的操作系统 ·docker体积小、启动速度快、性能好; 虚拟机体积大、启动速度慢、性能一般

Feign

Feign远程调用过程

首先要了解 Feign 是如何进行远程调用的,这里面包括,注册中心、负载均衡、FeignClient 之间的关系,微服务通过不论是 eureka、nacos 也好注册到服务端,Feign 是靠 Ribbon 做负载的,而 Ribbon 需要拿到注册中心的服务列表,将服务进行负载缓存到本地,然后 FeignClient 客户端在进行调用,大概就是这么一个过程

Feign第一次为什么比较慢

Ribbon 对于负载 Client 是在服务启动后,发生调用的时候才会去创建 Client,所以在第一次发生 http 请求调用的时候,不光要算上 http 的请求时间,还要算上 Client 的创建时间,所以第一次调用的时候才会很慢,

简述CAP定理内容?

·分布式系统节点通过网络连接,一定会出现分区问题(P)

·当分区出现时,系统的一致性(C)和可用性(A)就无法同时满足

思考:elasticsearch集群是CP还是AP?

· ES集群出现分区时,故障节点会被剔除集群,数据分片会重 新分配到其它节点,保证数据一致。因此是低可用性,高一 致性,属于CP

Seate

XA模式

XA模式的优点是什么?

事务的强一致性,满足ACID原则。 常用数据库都支持,实现简单,并且没有代码侵入 XA模式的缺点是什么? 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差依赖关系型数据库实现事务 XA模式原理

image.png

AT模式原理

image.png

TCC原理

image.png

简述AT模式与XA模式最大的区别是什么?

·XA模式一阶段不提交事务,锁定资源;

AT模式一阶段直接提交,不锁定资源。

·XA模式依赖数据库机制实现回

AT模式利用数据快照实 现数据回滚。

·XA模式强一致;

AT模式最终一致