汇总的汇总
todo项
- volatile的加屏障具体
- innodb的几章
- 汇总完这一篇,复习、整理下框架,说清楚主要架构和流程
- 算法还剩一些
- 设计模式
- 新的技术
数据库
MySql
查询中哪些情况不会使用索引?
- 使用or
- like以”%xx”开始匹配
- 联合(复合)索引,不符合最左匹配
- 索引列数据类型隐形转换,比如列是字符串,但用数值来查询就用不上索引
- 在where子句中,对索引列有数学运算、或者使用函数,用不了索引
- MySQL估计全表扫描比查询索引快时(比如数据量非常少)
MYSQL 索引类型、什么情况下用不上索引、什么情况下不推荐使用索引
MySQL性能优化的最佳21条经验 – 没大用
mysql explain执行计划详解 – 有错字之类的
type: const 命中唯一索引或主键的时候
数据库隔离级别
- 读未提交
- 读提交
- 可重复读
- 可串行化
数据库索引,底层是怎样实现的,为什么要用B+树索引?
MySQL底层使用B+树实现的。
MyISAM引擎,B+树主索引、辅助索引叶节点是数据记录的地址,称为非聚集索引(与InnoDB区分)
InnoDB的主键索引是聚集索引,叶节点存的完整的数据记录;辅助索引,叶节点存的是主键的值。
为什么用B+树索引?
数据文件比较大,一般存储在磁盘上
索引的组织结构要尽量减少查找过程中磁盘IO次数。
数据库系统利用磁盘预读原理,将一个节点的大小设为一个页的大小,则只需要一次IO就可以将一个节点的数据都读入
B+树只有叶子节点存放数据,非叶子节点作为索引,这样树出度大,树高小,一般3层,查询目标数据的io次数比较少,效率高。
使用节点大小正好等于磁盘一页大小的B+树,可以减少io操作次数,提高查询效率。
从数组、哈希表、二叉树等数据结构的对比来回答,见下面这篇文章
MySQL为什么不用数组、哈希表、二叉树等数据结构作为索引呢
Mysql主从同步的实现原理?
原理:在主库上记录二进制日志,在备库重放日志的方式实现异步数据复制。
复制有三个步骤:
- 主库记录二进制日志,每次准备提交事务(完成数据库更新)前先记录二进制日志(记录日志完后,再执行数据库更新)
- 备库将主库的二进制文件复制到本地的中继日志中。
- 备库会启动一个工作线程,称为IO工作线程,负责和主库建立一个普通的客户端连接
- 如果该进程追赶上了主库,它将进入睡眠状态,直到主库有新的事件产生,会被唤醒,将接收到的事件记录到中继日志中
- 备库的SQL线程读取中继日志并在备库执行
- 中继日志一般在系统缓存中,开销低,也可以根据配置选项来决定是否写入自己的二进制日志中
常见复制架构:
一主多从
主主
环型复制
MySQL是怎么用B+树?
innodb引擎用B+树当索引,索引文件同时是数据文件。聚集索引,也就是主键索引,叶节点存储的完整行数据;辅助索引,也称为非聚集索引,叶节点存对应行记录的主键。
MyISAM引擎也是用B+树当索引,为非聚集索引,索引不是数据文件,叶节点存的是行记录的地址。
谈谈数据库乐观锁与悲观锁?
- 悲观锁,认为操作会发生冲突,提前加锁,直到自己操作结束再释放锁。
- MySQL的显式锁定 写锁
select .. for update
& 读锁select .. lock in share mode
- 乐观锁,认为不会发生冲突,在提交更新的时候会判断一下期间数据有没有被修改。类似于CAS操作,常用方式有版本号、时间戳。
mvcc,怎么实现rr rc todo
mysql间隙锁有没有了解,死锁有没有了解,写一段会造成死锁的sql语句,死锁发生了如何解决,mysql有没有提供什么机制去解决死锁
gap lock
MySQL几种常用的存储引擎区别
InnoDB与MyISAM比较典型的几个区别:
- innodb支持事务、MVCC快照读、行级锁粒度、hash索引、聚集索引、支持外键
- myisam支持全文索引、空间索引、数据压缩
- innodb存储成本高、内存成本高、插入速度低,myisam反过来
来源:MySQL技术内幕
explain 可以看到哪些信息,什么信息说明什么,explain的结果列讲一下
https://dev.mysql.com/doc/refman/8.0/en/explain-output.html
Column | JSON Name | Meaning |
---|---|---|
id |
select_id |
The SELECT identifier select标识 |
select_type |
None | The SELECT type select类型 |
table |
table_name |
The table for the output row 表名 |
partitions |
partitions |
The matching partitions 使用的分区 |
type |
access_type |
The join type join类型 |
possible_keys |
possible_keys |
The possible indexes to choose 可能使用的索引 |
key |
key |
The index actually chosen 实际使用的索引 |
key_len |
key_length |
The length of the chosen key 实际使用的索引的长度 |
ref |
ref |
The columns compared to the index 与索引进行对比的列 |
rows |
rows |
Estimate of rows to be examined 预估要检查的行数 |
filtered |
filtered |
Percentage of rows filtered by table condition 符合条件的数据的百分比 |
Extra |
None | Additional information 额外的信息 |
select_type
常见的有SIMPLE(简单查询,无union、subqueries)、PRIMARY(子查询的外层)、SUBQUERY、UNION等
type
system:表中只有一行数据,const的特殊情况
const:至多有一行matching,可以理解为主键或唯一索引的= (单表,对tbl_name来说,1是const)
1
2
3
4SELECT * FROM tbl_name WHERE primary_key=1;
SELECT * FROM tbl_name
WHERE primary_key_part1=1 AND primary_key_part2=2;eq_ref:主键或唯一索引的= (多表关联,other_table的结果不定,所以对ref_table来说,选择不是const)
1
2
3
4
5
6SELECT * FROM ref_table,other_table
WHERE ref_table.key_column=other_table.column;
SELECT * FROM ref_table,other_table
WHERE ref_table.key_column_part1=other_table.column
AND ref_table.key_column_part2=1;ref:(非主键与非唯一索引的)其他索引的=和<=>(等和不等)
1
2
3
4
5
6
7
8SELECT * FROM ref_table WHERE key_column=expr;
SELECT * FROM ref_table,other_table
WHERE ref_table.key_column=other_table.column;
SELECT * FROM ref_table,other_table
WHERE ref_table.key_column_part1=other_table.column
AND ref_table.key_column_part2=1;fulltext
用到了全文索引
ref_or_null
类似ref,会额外检索包含null的行
index_merge
用到了多个索引,索引合并优化
unique_subquery
替换下面的in子查询,子查询返回不重复的集合
1
value IN (SELECT primary_key FROM single_table WHERE some_expr)
index_subquery
区别于unique_subquery,用于非唯一索引,可以返回重复值
1
value IN (SELECT key_column FROM single_table WHERE some_expr)
range
索引范围查找,包括主键、唯一索引、其他索引——即,所有key
=
,<>
,>
,>=
,<
,<=
,IS NULL
,<=>
,BETWEEN
,LIKE
, orIN()
1
2
3
4
5
6
7
8
9
10
11SELECT * FROM tbl_name
WHERE key_column = 10;
SELECT * FROM tbl_name
WHERE key_column BETWEEN 10 and 20;
SELECT * FROM tbl_name
WHERE key_column IN (10,20,30);
SELECT * FROM tbl_name
WHERE key_part1 = 10 AND key_part2 IN (10,20,30);index
类似all,但是只扫描索引,有两种情况
- 覆盖索引,select中的列都在索引中,extra中显示using index
- 利用索引的顺序进行全表扫描(比如有order by),extra中不宣誓using index
all
全表扫描
rows和filtered
- rows:MySQL认为需要检查的行数
- filtered:rows中会被过滤出来的——即符合条件的——的数据的百分比
- rows*filtered=查询出的结果数
extra 常见的有
using index 列信息只从索引出,不用再从实际行取。使用了覆盖索引
using where 没有可用的索引,通过where条件过滤
using filesort 需要额外排序
….还有好多
索引优化 todo 看看高性能书,有硬件层面和开发层面?
https://juejin.im/post/5b68e3636fb9a04fd343ba99#heading-3
如果MySQL评估使用索引比全表扫描还慢,则不会使用索引
前导模糊查询(like ‘%xx’)不会使用索引,可以优化为非前导模糊查询(like ‘xx%’)
数据类型出现隐式转换的时候不会命中索引,特别是当列类型是字符串,一定要将字符常量值用引号引起来
复合索引,要满足最左匹配原则
union、in、or 都能够命中索引,建议使用 in
查询的CPU消耗:or (id=1 or id=2)> in (id in (1,2)) >union(id = 1 union id = 2)
用or分割开的条件,如果or前的条件中列有索引,而后面的列中没有索引,那么涉及到的索引都不会被用到
因为or后面的条件列中没有索引,那么后面的查询肯定要走全表扫描,在存在全表扫描的情况下,就没有必要多一次索引扫描增加IO访问。
负向条件查询不能使用索引,可以优化为 in 查询
负向条件有:!=、<>、not in、not exists、not like 等。
范围条件查询可以命中索引
范围条件有:<、<=、>、>=、between等(返回数据的比例超过30%,会不使用索引)
查询条件(带有计算函数)执行计算不会命中索引
利用覆盖索引进行查询,避免回表
建议索引的列设置为非null
更新十分频繁的字段上不宜建立索引
区分度不大的字段上不宜建立索引
业务上具有唯一特性的字段,建议建立唯一索引
多表关联时,关联字段建议有索引
创建索引时避免以下错误观念
索引越多越好,认为一个查询就需要建一个索引。
宁缺勿滥,认为索引会消耗空间、严重拖慢更新和新增速度。
抵制唯一索引,认为业务的唯一性一律需要在应用层通过“先查后插”方式解决。
过早优化,在不了解系统的情况下就开始优化。
其他数据库
有使用过哪些NoSQL数据库?MongoDB和Redis适用哪些场景?
工程中用过Redis,主要是小部分数据的缓存 其他不太了解
NoSql not only sql 非关系型数据库
Redis和memcache有什么区别?Redis为什么比memcache有优势?
不太了解
考虑redis的时候,有没有考虑容量?大概数据量会有多少?
没有,公司维护的Redis组件 – redis & nosql 需要再深入一点呀
Redis的缓存淘汰策略、更新策略
过期策略
- 定期删除:默认每隔100ms随机抽取一些设置了过期时间的key,检查是否过期,如果过期就删除(因为全表扫描非常耗时、耗性能,所以是随机,也因此要配合惰性删除)
- 惰性删除:在客户端要获取某个key时,判断key是否设置过期以及是否过期,如果过期先删除
内存淘汰策略
Redis在使用内存达到某个阈值(通过maxmemory配置)的时候,就会触发内存淘汰机制,选取一些key来删除。
1
2maxmemory <bytes> 配置内存阈值
maxmemory-policy noeviction- noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。默认策略
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
如何选取合适的策略?比较推荐的是两种lru策略。根据自己的业务需求。如果你使用Redis只是作为缓存,不作为DB持久化,那推荐选择allkeys-lru;如果你使用Redis同时用于缓存和数据持久化,那推荐选择volatile-lru。
redis的数据结构
Redis 键值(Key-Value)存储数据库
- string 字符类型
- map 散列类型
- list 列表类型
- set 集合类型
- sortedset 有序集合类型
redis如何实现分布式锁,zk如何实现分布式锁,两者的区别。如果service还没执行完,分布式锁在redis中已经过期了,怎么解决这种问题
redis实现分布式锁:setNX,创建成功表明获得了锁(要注意设置超时、谁加锁谁解锁、解锁的原子性)
zk实现分布式锁:在路径下创建临时顺序节点,序号最小的节点表示获得了锁,其他竞争者监听自己的前一个节点
redisson给的答案是锁获取成功后,注册一个定时任务,每隔一定时间(this.internalLockLeaseTime / 3L, 10s)就去续约
加一个监听器,如果key快要超时了,就进行续约(重置成30s)
Java
Java基础
Java反射原理, 注解原理?
反射原理:在运行状态下,对于任何一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能调用它的任意方法和属性,并能改变它的属性。总结来说,反射把Java类中的各个成分映射成为一个个Java对象,并且可以进行操作。
注解原理:注解的本质是一个继承了Annotation接口的接口。
解析一个类或者方法的注解有两种形式,一是编译期扫描,如@Override,编译器会检查方法是否真的重写了父类的某个方法;二是运行期发射,虚拟机规范定义了一系列和注解相关的属性表,字段、属性或类上有注解时(被注解修饰了),会写相应信息进字节码文件,Class类中提供了一些接口用于获取注解或判断是否被某个注解修饰。
延伸阅读:JAVA 注解的基本原理
ps: Java类执行的过程/类加载过程(2-6)/类的生命周期(2-8) – tbc 更准确的说法
- 编译:Java文件编译成.class字节码文件
- 加载:类加载器通过全限定名,将字节码加载进JVM,存储在方法区,将其转换为一个与目标类型对应的Class对象实例
- 验证:格式(.class文件规范)验证和语义(final不能继承等)验证?
- 准备:静态变量赋初值与内存空间,final修饰的内存空间直接赋原值(?),不是开发人员赋的初值
- 解析:符号引用转换为直接引用,分配地址(?)
- 初始化:先初始化父类,再初始化自身;静态变量赋值,静态代码块执行。
- 使用
- 卸载
Java中==、equals与hashCode的区别和联系
https://juejin.im/entry/59b3897b5188257e733c24eb – 后面写的比较乱
https://juejin.im/post/5a4379d4f265da432003874c – equals与hashCode
Java数据类型
- 8种基本数据类型
- (整型)数值类型 byte short int long 1.2.4.8
- (浮点)数值类型 float double 4.8
- 字符型 char 2 存储 Unicode 码,用单引号赋值。
- 布尔类型 boolean 1
- 3种引用类型:类、接口、数组
==
比较两个数据是否相等,基本类型比较数值是否相等,引用类型比较地址是否相等。
equals()方法
Object类型定义的,比较二者==
1 | //object的equals方法 |
想自定义对象逻辑“相等”(值相等、或内容相等)的含义时,重写equals方法。
重写equals准则:
自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true。
对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true。
传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true, 并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true。
一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false, 前提是对象上 equals 比较中所用的信息没有被修改。
非空性:对于任何非空引用值 x,x.equals(null) 都应返回 false。
一般只判断同类型的对象
主要不要违反了对称性、传递性
hashCode()方法
1 | public native int hashCode(); |
equals的对象hashcode一定相等,hashcode相同的对象不一定equals
为什么对象的hashcode会相同?
hashcode的实现取决于jvm,比较典型的一种是基于内存地址进行哈希计算,也有基于伪随机的实现。
哈希计算会存在哈希碰撞。
https://juejin.im/entry/597937cdf265da3e114cd300
谈谈final、finally、finalize的区别 – 放一起有点奇怪
final
- 修饰类、方法或变量
- 修饰类:表明类不能被继承
- 方法:禁止在子类中被覆盖(private方法会隐式被指定为final)
- 变量:
- 基本数据类型的变量:数值在初始化后不能更改
- 引用类型的变量:初始化后不能再指向另一个对象(指向的地址不可变)
finally:
一般与try catch一起使用,无论程序抛出异常或正常执行,finally块的内容一定会被执行。
最常用的地方:通过try-catch-finally来进行类似资源释放、保证解锁等动作。
finalize
Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法。
日常开发中基本不用,也不推荐使用。Java9中被标记为deprecated! – 不想多说
https://juejin.im/post/5b9bb81ef265da0ac2565a0f
java如何实现序列化的,Serialization底层如何实现的
简单说来,是将类信息和数据信息递归写成字节信息
java中的反射
field的赋值底层实现
以UnsafeBooleanFieldAccessorImpl为例,也是利用unsafe 偏移
ps: Unsafe工具类 static final Unsafe unsafe = Unsafe.getUnsafe();
1 | // set |
Java容器
1. Java容器有哪些?哪些是同步容器,哪些是并发容器?
容器分两个大类,Collection和Map。Collection又分List、Set、Queue、Vector几个大类,Map有HashMap、TreeMap、LinkedHashMap、HashTable,其中,Vector、HashTable是同步容器。
并发容器一般在juc包下,有ConcurrentHashMap、CopyOnWriteArrayList等。
ps:
List: ArrayList、LinkedList
Set: HashSet、LinkedHashSet、TreeSet
Queue: LinkedList、PriorityQueue
引申:几个容器的主要方法的操作流程,容器体系结构
2. ArrayList和LinkedList的插入和访问的时间复杂度?
ArrayList:插入O(n) 访问O(1)
LinkedList:插入O(1) 访问O(n)
HashMap在什么情况下会扩容,或者有哪些操作会导致扩容?
java8中
- 放入新值(putValue–put/putMapEntries)后,元素个数size大于阈值threshold,会触发扩容。
- 链表树化时,如果表长table.length小于64,会用扩容代替树化。
- put值前,如果表长为0,会触发扩容
HashMap put方法的执行过程?
- 如果table为空,或长度为0,初始化。默认loadFactor为0.75,默认capacity为16(capacity是table的长度),threshold一般为capacity*loadFactor。
- 通过hash定位槽,如果槽为空,构造新节点赋值给槽
- 若槽不为空,则在槽的链表或树中找到key相同的节点,替换节点值为新值;或是没有key相同的节点,就在树中或链表尾部加入新节点;若链表加入新节点后长度达到8(槽不算,aka槽下原有7个节点),则进行红黑树转化
- 如果是新加入节点,modCount、元素个数size自增1,如果元素个数超过阈值,则进行扩容
Java8扩容的执行过程?
计算新容量newCap和新阈值newThr(ps: 当容量已到最大值时,不再扩容;2倍扩容;)
创建新的数组,赋值给table
- 将键值对重新映射到新数组上
- 如果无链表,直接根据
hash&(newCap-1)
定位
- 如果无链表,直接根据
- 如果是树节点,委托红黑树来拆分和重新映射
- 为链表,根据
hash&oldCap
的值分成0、1两组,映射到j和j+oldCap(0低位,1高位)(链表顺序不变)
- 将键值对重新映射到新数组上
HashMap概述
查找
根据hash定位槽
在槽中查找给定key(hash相等、key相等),找到直接返回,否则最后返回null
若槽节点key相等,返回槽节点
若槽节点为树节点,委托给树查找
遍历链表查找
遍历
从
index = 0, table[index]
开始,找到一个不为null的槽,遍历链表
插入
如果table为空,或长度为0,初始化。(默认loadFactor为0.75,默认capacity为16(capacity是table的长度),threshold一般为capacity*loadFactor。)
通过hash定位槽,如果槽为空,构造新节点赋值给槽
若槽不为空,则在槽的链表或树中找到key相同的节点,替换节点值为新值;或是没有key相同的节点,就在树中或链表尾部加入新节点;若链表加入新节点后长度达到8(槽不算,aka槽下原有7个节点),则进行红黑树转化
如果是新加入节点,modCount、元素个数size自增1,如果元素个数超过阈值,则进行扩容
扩容
计算新容量newCap和新阈值newThr(ps: 当容量已到最大值时,不再扩容;2倍扩容;)
创建新的数组,赋值给table
将键值对重新映射到新数组上
如果无链表,直接根据
hash&(newCap-1)
定位如果是树节点,委托红黑树来拆分和重新映射
为链表,根据
hash&oldCap
的值分成0、1两组,映射到j和j+oldCap(0低位,1高位)(链表顺序不变)
删除
定位到槽
找到删除节点
删除节点,并修复链表或红黑树
链表树化
- 链表树化有两个条件,不满足采用扩容,满足再扩容
- 树化时,将Node节点替换为TreeNode,保留next信息
- 替换后,再从head开始,进行红黑树化(标记红黑节点、父子节点,如果root节点不是first节点,再修正next和prev?)【链表转成红黑树后,原链表的顺序仍然会被引用仍被保留了(红黑树的根节点会被移动到链表的第一位)】
在扩容过程中,树化要满足两个条件:
链表长度大于等于 TREEIFY_THRESHOLD 8
桶数组容量大于等于 MIN_TREEIFY_CAPACITY 64
红黑树拆分(扩容时候)
红黑树中保留了next引用,拆分原理和链表相似
根据hash拆分成两组(这时候会生成新的next关系)
各组内根据情况,链化或者重新红黑树化
红黑树链化
将TreeNode替换为Node
ConcurrentHashMap概述
相比较HashMap,主要是增加了写操作时候的同步处理。扩容迁移时,多个线程帮助迁移。
为什么要用synchronized代替ReentrantLock?
优化后的synchronized性能与ReentrantLock差不多,基于JVM也保证synchronized在各平台上的使用一致。
锁粒度降低了;在大量数据操作下,基于api的ReentrantLock会有更大的内存开销。
sizeCtl
默认为0
当table为null时,持有一个initial table size用于初始化
当sizeCtl<0时
-1表示正在初始化
非-1的负数
1
2
3(sizeCtl的低16位-1)表示有多少个线程参与扩容迁移
sizeCtl的高16位
-(1 + the number of active resizing threads)
sizeCtl>0时,
(n << 1) - (n >>> 1)
= 0.75n (表示阈值,超过阈值需要扩容)
插入
计算hash
循环执行
- 如果数组为空,初始化initTable
- 如果hash定位到的槽为空,CAS替换为新节点,退出循环
- 如果槽不为空,节点hash为-1,说明正在迁移,helpTransfer
- 槽不为空,且不在迁移,那么,对头节点加锁,链表或红黑树形式插入或更新节点
- 如果数组为空,初始化initTable
addCount
迁移
transfer的第二个参数为空的时候,触发扩容,创建nextTable,在addCount和tryPresize中有这样的调用。
addCount是size不精确情况下,可能触发扩容;tryPresize是已知精确size的情况下做扩容。
- 计算步长stride
- 如果nextTab未创建,则创建之,并赋给nextTable
- 循环迁移
- 分配迁移区间i
和
bound(
i从前往后,
bound = i - stride + 1`,总之就是stride) - 如果区间已达边界,将sc减1,表示本线程退出迁移。如果是最后一个迁移线程,标记finish和advance为true,进入下一循环recheck;非最后线程,直接退出方法。
- 如果finish为true,
table = nextTab; sizeCtl = (n << 1) - (n >>> 1);
,退出 - 若未达边界,且槽为空,CAS槽为fwd,进入下一循环
- 槽不为空,且槽已经是fwd,进入下一循环
- 最后一种情形,进行迁移
- 为链表,根据节点hash二进制第k位为0或1分成两组(n=2^k),1连接到高位槽上
- 为红黑树,分组同链表,分好的组根据节点个数判断是否链化或新生成红黑树
- 分配迁移区间i
- 计算步长stride
HashMap检测到hash冲突后,将元素插入在链表的末尾还是开头?
Java8是加载链表末尾
Java7是开头
头插法会改变链表中元素原本的顺序,在并发情况下可能会产生链表成环的问题。
Java7到Java8的改变HashMap为何从头插入改为尾插入
java7的问题老生常谈,HashMap的死循环
1.8还采用了红黑树,讲讲红黑树的特性,为什么人家一定要用红黑树而不是AVL、B树之类的?
插入、删除、查找的最坏时间复杂度都为 O(logn)。
红黑树特性:
- 每个节点要么是黑色,要么是红色
- 根节点是黑色的
- 每个叶节点是黑色的(Java实现中,叶子节点是null,遍历时看不到黑色的叶子节点,反而每个叶子节点是红色的)
- 如果一个节点是红色的,那么它的两个子节点是黑色的(意味着可以有连续的黑色节点,但不能有连续的红色节点。若给定N个黑色节点,最短路径情况是连续N个黑色,树高为N-1;最长路径情况是红黑相间,树高为2N-2)
- 对任一节点,从节点到它每个叶子节点的路径包含相同数量的黑色节点(最主要特性,插入、删除要调整以遵守这个规则)
为什么用红黑树?
红黑树的统计性能(理解为增删查平均性能)优于AVL树。
AVL:名字来源发明者G. M. Adelson-Velsky和E. M. Landis。本质是平衡二叉搜索树(查找树),任何节点的左右子树高度差不超过1,是高度平衡的二叉查找树。
B树:重温数据结构:理解 B 树、B+ 树特点及使用场景 平衡二叉树节点最多有两个子树,而 B 树每个节点可以有多个子树,M 阶 B 树表示该树每个节点最多有 M 个子树
AVL树高度平衡,查找效率高,但维护这个平衡的成本比较大,插入、删除要做的调整比较耗时。
红黑树的插入、删除、查找各种操作的性能比较平衡。
B树和B+树多用于数据存储在磁盘上的场景,比较矮胖,一次读取较多数据,减少IO。节点内是有序列表。列表的插入、删除成本比较高,如果是链表形式,则查找效率比较低(不能用二分查找提高查询效率)。
【自己的理解:B树节点内是有序列表,通过二分查找提高效率】
为什么STL和linux都使用红黑树作为平衡树的实现? - Acjx的回答 - 知乎 https://www.zhihu.com/question/20545708/answer/58717264
谈谈Java容器ArrayList、LinkedList、HashMap、HashSet的理解,以及应用场景
ArrayList | LinkedList | HashMap | HashSet | |
---|---|---|---|---|
数据结构 | (可变)数组 | (双向)链表 | 数组+红黑树 | 底层实现是HashMap |
插入时间复杂度 | o(n) | o(1) | ||
删除时间复杂度 | o(n) | o(1) | ||
访问时间复杂度 | o(1) 支持随机访问 | o(n) 不支持随机.. | ||
应用场景 | 经常访问 | 经常修改 | 映射..? | 去重 |
sortset底层,原理,怎么保证有序
TreeSet具体实现是TreeMap,底层是红黑树
containsKey、get、put、remove 时间复杂度log(n)
红黑树
通过对任何一条(根到叶子的)路径上的各个节点的着色方式的限制,确保没有一条路径会比其他路径长出2倍,因而近乎是平衡的
性质:
- 每个节点是红色的,或是黑色的
- 根节点是黑色的
- 每个叶子节点(Nil)是黑色的
- 如果一个节点是红色的,则它的两个子节点是黑色的
- 对每个节点,从该节点到其子孙节点的所有路径上包含相同个数的黑色节点。(红节点不能有红孩子)(从该节点出发的所有下降路径,有相同的黑节点个数)
黑高度:从一个节点到达一个叶子节点的任意一条路径上黑色节点的个数
红黑树的黑高度定义为根节点的黑高度
优先级队列的底层原理?
堆,默认是小顶堆
入队
1 | public boolean offer(E e) { |
出队
1 | public E poll() { |
DelayQueue
https://www.cnblogs.com/jobs/archive/2007/04/27/730255.html
DelayQueue = BlockingQueue + PriorityQueue + Delayed
Java并发
线程池的工作原理,几个重要参数,然后给了具体几个参数分析线程池会怎么做,最后问阻塞队列的作用是什么?
线程池解决两个问题:
- 由于减少了每个任务的调度开销,通常在执行大量异步任务时提供优秀的性能。
- 提供了管理、调控资源的方式
Executors工厂方法:
- newFixedThreadPool 固定size的线程池。为了满足资源管理的需求,需要限制当前线程数量的场景。适用于负载比较重的服务器。
- corePoolSize == maximumPoolSize
- keepAliveTimes = 0
- LinkedBlockingQueue 队列大小Integer.MAX_VALUE,等价于无界
- 当线程池中线程数达到corePoolSize后,新任务将在队列中等待
- 由于使用无界队列,运行中的线程池不会拒绝任务
- newSingleThreadExecotor 单个线程的线程池。需要保证顺序执行任务的场景,并且在任意时间点不会有多个线程是活动的。
- corePoolSize = maximumPoolSize = 1
- keepAliveTimes = 0
- LinkedBlockingQueue
- 如果当前线程池无线程,就创建一个线程来运行任务
- 当线程数达到1后,新的任务都加入到队列中
- newCachedThreadPool 大小无界的线程池(自动资源回收?),适用于有很多短期异步执行任务的小程序,或者是负载比较轻的服务器。
- corePoolSize = 0, maximumPoolSize = Integer.MAX_VALUE
- keepAliveTimes = 60s
- SynchronousQueue 是一个没有容量的阻塞队列,一个插入操作必须等待另一个线程对应的移除操作
- 提交任务时如果有空闲线程,就空闲线程取到这个任务执行;否则创建一个线程来执行任务
- 适用于将主线程的任务传递给空闲线程执行
重要参数:
- core and maximum pool sizes
- corePoolSize 核心最大线程:新任务加入时,如果运行线程个数小于核心线程数,即使有其他工作线程是空闲的,也会创建新线程 – 线程池预热
- maximumPoolSize 线程池最大线程:阻塞队列满时,如果运行线程数小于maximumPoolSize,才可创建新线程运行任务
- corePoolSize=maximumPoolSize时,等价于newFixedThreadPool
- maximumPoolSize=本质上无限的数(比如Integer.MAX_VALUE),等价于newCachedThreadPool ?
- 一般只在构造时设置这两个参数,但也可以通过两个set方法改变
- 这两个参数会自动调整么?
- On-demand construction
- 默认情况下,只有任务提交时才会创建线程(包括核心线程)
- 也可以通过prestartCoreThread或者prestartAllCoreTheads来预先创建线程。比如构建了一个阻塞队列不为空的线程池时,会想要这么做(预先创建线程)。
- Creating new threads
- 默认使用defaultThreadFactory来创建线程,相同的线程组ThreadGroup、优先级priority和非守护线程状态non-daemon status.
- 也可以使用自定义的threadFactory,自定义线程名称、线程组、优先级等。
- threadFactory创建线程失败的什么东西没看懂
- Keep-alive times
- keepAliveTime 如果线程数多于核心线程数,超过这个时间的空闲线程将会被停掉(指销毁掉?)
- queuing
- 入队规则
- rejected tasks 四个拒绝策略 RejectedExecutionHandler
- ThreadPoolExecutor.AbortPolicy 抛出RejectedExecutionException
- CallerRunsPolicy 调用者自身来执行
- DiscardPolicy 丢弃任务,任务不会被执行
- DiscardOldestPolicy work queue的首个任务将会被丢弃,重试添加当前任务(可能再次失败,自旋执行)
- hook methods
- beforeExecute afterExecute 可用来设置运行环境,重新初始化本地线程,获取统计数据,添加日志。
- terminated executor终止时提供的钩子方法
- queue maintenance getQueue可用于监控和调试当前work queue,其他用途不建议。remove和purge可用于大量任务取消时候的存储清理。
- reclamation (清除?)一个在程序中无引用、并且无剩余线程的线程池,即使无显式shutdown关闭,也可以被清除回收。可以通过这些方式设置线程池的线程在无使用时(最终)销毁:设置keep-alivet times;使用小的核心线程数比如0,或者设置allowCoreThreadTimeOut。
ScheduledThreadPoolExcutor
延迟运行命令,或周期执行命令
LinkedBlockingQueue和DelayQueue的实现原理
- LinkedBlockingQueue 就是生产者消费者的实现
- 应用了ReentrantLock(putLock & tackLock)和lock的Condition(notEmpty & notFull)
- DelayQueue
- 应用了PriorityQueue,时间小的在队头
- ReentrantLock(lock)和Condition(available)
FutureTask是用AQS实现的 get=acquireShared,run/cancel后=release
谈谈Java线程的基本状态,其中的wait() sleep() yield()方法的区别。
新建、运行(运行中、就绪)、等待、超时等待、阻塞、终止
wait()
Object的方法,在某个对象上等待,等待这个对象将它唤醒,释放锁。运行->等待/超时等待
sleep()
Thread的静态方法,当前线程睡眠,不释放锁。运行->超时等待
yield()
Thread的方法,让出当前cpu。还是运行这个大状态,从运行中变成就绪状态。
简单谈谈JVM内存模型,以及volatile关键字
运行时数据区域包括堆、方法区(包括运行时常量池)、Java虚拟机栈、本地方法栈、程序计数器、直接内存。
- 堆:所有对象在这里分配内存【所有线程共享】
- 方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等信息【所有线程共享】
- Java虚拟机栈:生命周期与线程相同,描述的是Java方法执行时候的内存模型,每个方法被执行的时候都会创建一个栈帧,存储局部变量表、操作数栈、常量池引用(动态链接)、方法出口等信息。【线程私有】
- 本地方法栈:与虚拟机栈类似,只不过方法是本地方法【线程私有】
- 程序计数器:记录正在执行的虚拟机字节码指令的地址(如果是本地方法则为空)【线程私有】
- 直接内存:JDK1.4引入NIO,可以使用native函数库分配堆外内存,然后通过堆内的DirectByteBuffer作为这部分内存的引用、进行操作。可以提高性能,避免堆外内存和堆内内存的来回拷贝。
Java内存模型 JMM
Java memory model
用来屏蔽不同硬件和操作系统的内存访问差异,实现Java在各平台上一致的内存访问效果。
JMM规定,所有变量都存在主内存中(类似于操作系统的普通内存);每个线程有自己的工作内存(=CPU的寄存器或高速缓存),保存了该线程使用的变量的主内存副本拷贝。线程只能操作工作内存。
存在缓存不一致问题。
主内存与工作内存交互操作
内存模型三大特性
原子性:上述8个操作是原子的(double&long等64位变量的操作,JVM允许非原子),一系列操作合起来其实是非原子的
可见性:指一个线程修改了共享变量的值,其他线程可以立即得知这个修改。JMM是通过变量修改后将新值同步到主内存(并使其他工作内存中的这个变量副本无效)、在变量读取前从主内存刷新变量值来实现的。
有序性:从本线程来看,所有操作都是有序的;从线程外看,操作是无序的,因为发生了指令重排。JMM允许编译器和处理器进程指令重排,重排不会影响到单线程的执行结果,但会影响多线程的执行正确性。
volatile关键字通过添加内存屏障的方式来禁止指令重排(重排序时不能把屏障后的指令重排到屏障前)
先行发生原则
- 单一线程原则:在一个线程内,在程序前面的操作先行发生于后面的操作。
- 管程锁定原则:一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
- volatile变量规则:对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
- 线程启动规则:Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。
- 线程加入规则:Thread 对象的结束先行发生于 join() 方法返回。
- 线程中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
- 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
- 传递性:如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。
volatile关键字
- 保证了不同线程对该变量操作的内存可见性
- 禁止指令重排序,保证(volatile读写)有序性
volatile的底层如何实现,怎么就能保住可见性了?
具体在👆
在缓存行和主内存之间,利用的是缓存一致性协议。
在写入缓存和缓存行之间,利用的是内存屏障。
从规范上讲是内存屏障,x86实现上是lock前缀指令,既有原子性的效果,也有StoreLoad内存屏障的效果。
内存屏障的保守插入方式,为了使写操作一定刷新到缓存行,(因为缓存一致性和禁止重排序)读操作一定读到最新值:
- 在每个volatile读后面,插入LoadLoad和LoadStore
- 在每个volatile写前面插入StoreStore,写后面插入StoreLoad
1 | public class VolatileExample { |
线程之间的交互方式有哪些?有没有线程交互的封装类?
线程之间的协作
- join() 在线程中调用另一个线程的join()方法,会将本线程挂起,直到目标线程结束
- wait() notify() notifyAll()
- 调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。
- 属于Object(不是Thread)
- await() signal() signalAll()
- java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用Condition的 signal() 或 signalAll() 方法唤醒等待的线程。
线程交互的封装类
CountDownLatch
用来控制一个线程等待多个线程。
维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方 法而在等待的线程就会被唤醒。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class CountdownLatchExample {
public static void main(String[] args) throws InterruptedException {
final int totalThread = 10;
CountDownLatch countDownLatch = new CountDownLatch(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("run..");
countDownLatch.countDown();
}); }
countDownLatch.await();
System.out.println("end");
executorService.shutdown();
} }
run..run..run..run..run..run..run..run..run..run..end等待所有run结束
CyclicBarrier
用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。
和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1,并进行等待,直到计数器为 0,所有调用 await() 方法而在等待的线程才能继续执行。
CyclicBarrier 和 CountdownLatch 的一个区别是,CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用,所以它才叫做循环屏障。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class CyclicBarrierExample {
public static void main(String[] args) {
final int totalThread = 10;
CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("before..");
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.print("after..");
});
}
executorService.shutdown();
}
}
before..before..before..before..before..before..before..before..before..before..after..after..after..after..after..after..after..after..after..after..等待所有before结束
Semaphore
Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class SemaphoreExample {
public static void main(String[] args) {
final int clientCount = 3;
final int totalRequestCount = 10;
Semaphore semaphore = new Semaphore(clientCount);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalRequestCount; i++) {
executorService.execute(()->{
try {
semaphore.acquire();
System.out.print(semaphore.availablePermits() + " ");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}); }
executorService.shutdown();
}
}
1 0 1 1 1 2 2 2 0 1有限个资源
Java的锁机制 – 内容巨多
背景知识
- 指令流水线:现代处理器的体系结构中,采用流水线的方式对指令进行处理。每个指令的工作可分为5个阶段:取指令、指令译码、执行指令、访存取数和结果写回。
- CPU多级缓存:计算机系统中,存在CPU高速缓存,用于减少处理器访问内存所需平均时间。当处理器发出内存访问请求时,会先查看缓存中是否有请求数据,若命中则直接返回该数据;若不存在,则先从内存中将数据载入缓存,再将其返回处理器。
问题引入
- 原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。(比如i++,如果对实例变量i的操作不做额外的控制,那么多个线程同时调用,就会出现覆盖现象,丢失部分更新。) – 因为指令流水线
- 可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值(存在可见性问题的根本原因是由于缓存的存在)– 因为存在缓存
- 顺序性:即程序执行的顺序按照代码的先后顺序执行 – 因为存在指令重排
JMM内存模型
主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。这里的变量指共享变量(存在竞争问题的变量),如实例字段、静态字段、数组对象元素等。不包括线程私有的局部变量、方法参数等。
内存划分:分为主内存和工作内存,【每个线程都有自己的工作内存,它们共享主内存。】【线程对共享变量的所有读写操作都在自己的工作内存中进行,不能直接读写主内存中的变量。】【不同线程间也无法直接访问对方工作内存中的变量,线程间变量值的传递必须通过主内存完成。】
主内存(Main Memory)存储所有共享变量的值。
工作内存(Working Memory)存储该线程使用到的共享变量在主内存的的值的副本拷贝。
内存间交互规则【一个变量如何从主内存拷贝到工作内存,如何从工作内存同步到主内存中】
8种原子操作
- lock: 将一个变量标识为被一个线程独占状态
- unclock: 将一个变量从独占状态释放出来,释放后的变量才可以被其他线程锁定
- read: 将一个变量的值从主内存传输到工作内存中,以便随后的load操作
- load: 把read操作从主内存中得到的变量值放入工作内存的变量的副本中
- use: 把工作内存中的一个变量的值传给执行引擎,每当虚拟机遇到一个使用到变量的指令时都会使用该指令
- assign: 把一个从执行引擎接收到的值赋给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时,都要使用该操作
- store: 把工作内存中的一个变量的值传递给主内存,以便随后的write操作
- write: 把store操作从工作内存中得到的变量的值写到主内存中的变量
原子操作的使用规则
read、load、use必须成对顺序出现,但不要求连续出现。assign、store、write同之;
变量诞生和初始化:变量只能从主内存“诞生”,且须先初始化后才能使用,即在use/store前须先load/assign;
lock一个变量后会清空工作内存中该变量的值,使用前须先初始化;unlock前须将变量同步回主内存;
一个变量同一时刻只能被一线程lock,lock几次就须unlock几次;未被lock的变量不允许被执行unlock,一个线程不能去unlock其他线程lock的变量。
对于double和long,虽然内存模型允许对非volatile修饰的64位数据的读写操作分为两次32位操作来进行,但商用虚拟机几乎把64位数据的读写实现为了原子操作,可以忽略这个问题。
先行发生原则
【Java内存模型具备一些先天的“有序性”,即不需要通过任何同步手段(volatile、synchronized等)就能够得到保证的有序性,这个通常也称为happens-before原则。】
如果两个操作的执行次序不符合先行原则且无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
- 程序次序规则(Program Order Rule):一个线程内,逻辑上书写在前面的操作先行发生于书写在后面的操作。
- 监视器锁规则(Monitor Lock Rule):一个unLock操作先行发生于后面对同一个锁的lock操作。“后面”指时间上的先后顺序。
- volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作。“后面”指时间上的先后顺序。
- 传递规则(Transitivity):如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。
- 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每个一个动作。
- 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生(通过Thread.interrupted()检测)。
- 线程终止规则(Thread Termination Rule):线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。
- 对象终结规则(Finaizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于他的finalize()方法的开始。
问题解决
原子性
- 由JMM保证的原子性变量操作
- 基本数据类型的读写(工作内存)是原子的
- JMM的lock和unlock指令可以实现更大范围的原子性保证,虚拟机提供synchronized关键字和Lock锁来保证原子性。
可见性
- volatile关键字修饰的变量,被线程修改后会立即同步回主内存,其他线程要读取这个变量会从主内存刷新值到工作内存。(因为缓存一致性协议会让其他工作内存中的该变量拷贝无效,必须得从主内存再读取)即read、load、use三者连续顺序执行,assign、store、write连续顺序执行。
synchronized/Lock
由lock和unlock的使用规则保证【这里有疑问啊,synchronized有lock和unlock,但是Lock没有吧…Lock怎么保证可见性?还是说Lock保证不了可见性。可见性只能由volatile保证?–参见ConcurrentHashMap,有synchronized,还配合volatile使用—ConcurrentHashMap有些是不加锁的操作,比如get,所以还是用volatile保证可见性。synchronized 锁的是某个node节点,对这个node节点的】synchronized有语义规定,说是通过内存屏障实现的
线程解锁前,必须把共享变量的最新值刷新到主内存中
线程加锁前,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值- Lock用了cas,有
lock cmpxchg
,lock前缀指令保证了可见性,同时有内存屏障的作用
同时,这俩还能保证临界区操作的所有变量的可见性因为内存屏障
LOCK前缀的指令具有如下效果:
- 把写缓冲区中所有的数据刷新到内存中
注意,是所有的数据,可不仅仅是对state的修改
All threads will see the most recent write to a volatile field, along with any writes which preceded that volatile read/write. Reentrantlock的lock和unlock方法实际上会cas一个state的变量,state是volatile的,因此夹在两次state之间的操作都能保证可见性。这应该算是happen before的传递性…
- Lock用了cas,有
顺序性
- volatile 禁止指令重排序
- synchronized/Lock “一个变量在同一个时刻只允许一条线程对其执行lock操作” – 感觉这个也没用,不然双重检查的单例怎么还用volatile关键字来防止重排序 – 最多保证原子性,被加锁内容按照顺序被多个线程执行
锁机制
volatile:
保证可见性和顺序性【实现方式:lock前缀指令+依赖MESI缓存一致性协议】
- volatile修饰的变量,在进行写操作的时候会多一行汇编代码,lock指令,做两件事:
- 将当前处理器缓存行的数据写回系统内存
- 引起其他处理器里缓存了该内存地址的数据无效。【实现缓存一致性协议,处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了(处理器发现自己缓存行对应的内存地址被修改,就会将自己的缓存设置成无效状态)】
- volatile修饰的变量,在进行写操作的时候会多一行汇编代码,lock指令,做两件事:
final:有两个重排序规则 – 不甚了解
- 写final域的重排序规则:在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
- 读final域的重排序规则:初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
synchronized关键字
使用哪个对象的监视器:
- 修饰对象方法时,使用当前对象的监视器
- 修饰静态方法时,使用类类型(Class 的对象)监视器
- 修饰代码块时,使用括号中的对象的监视器
- 必须为 Object 类或其子类的对象
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
简单理解,只有一个线程CAS时,如果CAS成功,表示没有锁竞争,保持偏向锁状态,如果CAS失败,说明有竞争,(先撤销偏向锁,将对象头设置成无锁状态,并设置为不可偏向)升级为轻量级锁。
几种锁的适用场景
- 偏向锁:锁不仅不存在线程竞争,而且总是由同一个线程多次获得,这时候偏向锁的代价最低。适用只有一个线程访问同步块的场景。(如果有别的线程来获取锁,发现)
- 轻量级锁:同步块执行时间非常快的,执行完就替换回mark word,别的线程要加锁也很快,CAS。(如果同步块执行很久,竞争线程自旋cas非常久,就很耗cpu,所以会升级到重量级锁,竞争线程阻塞挂起)
- 重量级锁:同步块执行时间比较长的,原因如2
锁升级机制
1. 偏向锁:线程检查锁对象的状态是否是可偏向的,是的话,检查mark word中的线程ID是不是自己,是的话进入代码块,不是的话,将线程ID cas进mark word。cas失败的话,说明之前是别的线程(假设A)取到的了,等待全局安全点,JVM暂停线程A,检查线程A的状态:如果A不在活动中,将锁对象的mark word中的线程ID置空,再cas成自己的线程ID;如果A在活动中(未退出代码块),升级为轻量级锁:JVM在线程A中分配锁记录,拷贝锁对象mark word,并将锁对象mark word指向这个锁记录;在线程B中分配锁记录,拷贝锁对象mark word,并持续自旋cas(如果自旋n次还失败,就要再次升级成重量级锁了..)...
2. 轻量级锁:如果不止一个线程尝试获取锁,就会升级到轻量级锁。**通过自适应自旋CAS的方式获取锁。**如果获取失败,说明存在竞争,膨胀为重量级锁,线程阻塞。默认自旋10次。**将对象头中的Mark Word复制到栈帧(一块空间,称为锁记录)中,然后用CAS将对象头中的Mark Word替换为指向栈帧中锁记录的指针。**
3. 重量级锁:通过系统的线程互斥锁来实现的,未获取到锁的线程会阻塞挂起
大佬的图,来源见水印
右下角的轻量级锁释放的补充说明:
在某个线程A正持有轻量级锁的时候(还在代码块内运行,时间比较长),某个线程B自旋cas竞争锁(肯定是cas失败了)失败了,这时候就会升级成重量级锁了,mark word指向了互斥量的指针,这和线程A中锁记录的值不同,线程A后续释放锁就失败了(意识到已经升级成重量级锁,唤醒其他挂起的线程)

- AQS
【内存屏障和”lock”前缀指令】理解
volatile通过编译器,既会增加”lock”前缀指令,也会加上内存屏障(mfence等)
内存屏障是抽象概念,各个硬件、处理器实现不同
lock前缀指令和mfence等是具体实现
mesi协议保证缓存和主存间的一致性
有了msei协议,为什么汇编层面还需要lock(volatile)来实现可见性? - Rob Zhang的回答 - 知乎 https://www.zhihu.com/question/334662600/answer/747038084
内存屏障能保证从storebuffer到缓存再到主存的一致性,在多线程运行中可以作为mesi的补充(因为mesi管不到那么多),但内存屏障
lock前缀主要是为了提供原子操作,虽然它也包含了内存屏障功能(强制将寄存器、缓存(、storebuffer/invalid queue或类似的东西)等强制同步到主存)
关于内存屏障的几个问题? - cao的回答 - 知乎 https://www.zhihu.com/question/47990356/answer/108650501
x86在Windows下的内存屏障是用lock前缀指令来达到效果的
简单理解:
内存屏障保证了寄存器和缓存之间的一致性
lock前缀保证操作原子性
二者都能保证可见性
x86架构的内存屏障
sfence: Store Barrier = StoreStore Barriers 写屏障
所有sfence之前的store指令都在sfence之前被执行,并刷出到CPU的L1 Cache中;
所有在sfence之后的store指令都在sfence之后执行,禁止重排序到sfence之前。
所以,所有Store Barrier之前发生的内存更新都是可见的。
lfence: Load Barrier = LoadLoad Barriers 读屏障
所有在lfence之后的load指令,都在lfence之后执行,并且一直等到load buffer被该CPU读完才能执行之后的load指令(即要刷新失效的缓存)。配合sfence,使所有sfence之前发生的内存更新,对lfence之后的load操作都可见。
mfence: Full Barrier = StoreLoad Barriers 全屏障
综合了sfence和lfence的作用,强制所有在mfence之前的store/load指令都在mfence之前被执行,之后的store/load指令都在之后执行,禁止跨越mfence重排序。并且都刷新到缓存&重新载入无效缓存。
Mark Word 对象头【见JMM】 todo
主要有锁标志位,根据不同的锁状态其他位上存有不同的值,比如
- 偏向锁:拥有锁的线程ID,偏向状态
- 轻量级锁:拥有锁的锁记录地址
- 重量级锁:监视器锁的地址
synchronized底层实现
加在方法上和加在同步代码块中编译后的区别、类锁、对象锁
编译时候加入监视器锁
1 | public class SyncTest { |
加在方法上:方法上有synchronized关键字,flags里有ACC_SYNCHRONIZED
https://blog.csdn.net/hosaos/java/article/details/100990954
ACC_SYNCHRONIZED是获取监视器锁的一种隐式实现(没有显示的调用monitorenter,monitorexit指令)
如果字节码方法区中的ACC_SYNCHRONIZED标志被设置,那么线程在执行方法前会先去获取对象的monitor对象,如果获取成功则执行方法代码,执行完毕后释放monitor对象
1 | public synchronized void syncMethod(); |
加在同步块上:monitorenter / monitorexit 关键字
1 | public void syncBlock(); |
volatile在编译上的体现
1 | public class VolatileTest { |
字节码
网上查到的是变量上flags有ACC_VOLATILE标识,自己编译出来没看到…
1 | public void plus(); |
看文章说还是lock前缀指令
http://gee.cs.oswego.edu/dl/jmm/cookbook.html – x86架构下,实现是lock前缀指令,支持”SSE2”扩展 (Pentium4 and later)的版本支持mfence指令(比lock前缀更推荐),cas的cmpxchg的实现需要lock前缀
https://www.cnblogs.com/xrq730/p/7048693.html
- 锁总线,其它CPU对内存的读写请求都会被阻塞,直到锁释放,不过实际后来的处理器都采用锁缓存替代锁总线,因为锁总线的开销比较大,锁总线期间其他CPU没法访问内存
- lock后的写操作会回写已修改的数据,同时让其它CPU相关缓存行失效,从而重新从主存中加载最新的数据
- 不是内存屏障却能完成类似内存屏障的功能,阻止屏障两边的指令重排序
整理一下最终的实现:
- lock前缀指令会引起处理器缓存回写到内存
- 一个处理器的缓存回写到内存会导致其他处理器的缓存无效,这是MESI实现的(缓存一致性协议)
- 另外,lock前缀指令能完成内存屏障的功能,阻止屏障前后的指令重排序
这篇文章https://juejin.im/post/5ea938426fb9a043856f2f6a提到,x86下使用`lock`来实现`StoreLoad`,并且只有 StoreLoad
有效果。x86 上怎么使用 Barrier 的说明可以在 openjdk 的代码中看到,在这里src/hotspot/cpu/x86/assembler_x86.hpp。
3种重排序类型
1是编译器重排序,2和3是处理器重排序。会导致多线程程序出现内存可见性问题。
编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
aqs,countDownLatch如何实现 todo
计算密集型/IO密集型 任务 分别如何设置线程池的核心线程数和最大线程数,为什么这么设置
https://blog.csdn.net/weixin_40151613/java/article/details/81835974
计算密集型:
CPU使用率比较高,(也就是一些复杂运算,逻辑处理)
线程数设置为CPU核数
IO密集型:
cpu使用率较低,程序中会存在大量I/O操作占据时间,导致线程空余出来
一般设置线程数为CPU核数的2倍
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
线程等待时间越长,需要越多的线程
补充
高并发、任务执行时间短的业务:线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
并发不高、任务执行时间长的业务:
- 假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以适当加大线程池中的线程数目,让CPU处理更多的业务
- 假如是业务时间长集中在计算操作上,也就是计算密集型任务,和(1)一样,线程池中的线程数设置得少一些,减少线程上下文的切换
并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计
- 数据能否做缓存
- 增加服务器
- 业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件(任务时间过长的可以考虑拆分逻辑放入队列等操作)对任务进行拆分和解耦。
死锁
死锁定义:多个进程循环等待它方占有的资源而无限期地僵持下去的局面。
产生死锁的必要条件:
- 互斥(mutualexclusion),一个资源每次只能被一个进程使用
- 不可抢占(nopreemption),进程已获得的资源,在未使用完之前,不能强行剥夺
- 占有并等待(hold andwait),一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 环形等待(circularwait),若干进程之间形成一种首尾相接的循环等待资源关系。
对待死锁的策略主要有:
死锁预防:破坏导致死锁必要条件中的任意一个就可以预防死锁。例如,要求用户申请资源时一次性申请所需要的全部资源,这就破坏了保持和等待条件;将资源分层,得到上一层资源后,才能够申请下一层资源,它破坏了环路等待条件。预防通常会降低系统的效率。
死锁避免:避免是指进程在每次申请资源时判断这些操作是否安全,例如,使用银行家算法。死锁避免算法的执行会增加系统的开销。
死锁检测:死锁预防和避免都是事前措施,而死锁的检测则是判断系统是否处于死锁状态,如果是,则执行死锁解除策略。
死锁解除:这是与死锁检测结合使用的,它使用的方式就是剥夺。即将某进程所拥有的资源强行收回,分配给其他的进程。
避免死锁的几个常见方法
避免一个线程同时获取多个锁
避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
尝试使用定时锁,使用
lock.tryLock(timeout)
来代替使用内部锁机制。对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
Java虚拟机
虚拟机的几大问题
- 运行时数据区域
- 垃圾收集
- 对象可达
- 引用类型
- GC Roots
- 算法
- 收集器
- 内存分配与回收策略(回收主要是老年代的触发条件)
- 类加载机制
新生代分为几个区?使用什么算法进行垃圾回收?为什么使用这个算法?
新生代有三个区,一个较大的Eden区,两个小的Survivor区。
使用复制算法。(也有标记过程,标记-复制)
一方面,针对算法本身,相对于标记-清除算法,不会有内存碎片的问题;相对于标记-整理算法,处理效率高很多(在整理时,还未进行对象清理,移动存活对象时需要将存活对象插入到待清理对象之前,有大量的移动操作,时间复杂度很高)。
复制算法主要问题在于内存利用率,而HotSpot的Eden和Survivor的默认比例是8:1,保证内存利用率达到了90%,所以影响也不是太大。
另一方面,新生代minor gc比较频繁,对gc效率有比较高的要求;对象生命周期比较短,小的survivor空间即可容纳大部分情况下的存活对象。
引申:jvm的几个知识点,算法,判断对象存活,GC roots有哪些,内存分配与回收策略,类加载机制
垃圾收集算法【见Java虚拟机】
- 标记-清除
- 标记-整理
- 复制
- 分代收集
- 新生代:复制算法
- 老年代:标记-清除 or 标记整理
垃圾收集器与内存分配策略【祥见JVM的几个大知识点】
垃圾收集器
内存分配策略
Minor GC 和 Full GC
- Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比 较快。
- Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。
分配策略
对象优先在 Eden 分配
大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。
大对象直接进入老年代
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。
长期存活的对象进入老年代
为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁, 增加到一定年龄则移动到老年代中。
-XX:MaxTenuringThreshold 用来定义年龄的阈值。
动态对象年龄判定
虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄 所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
空间分配担保
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代 最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小 于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。
Full GC 的触发条件
对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
调用 System.gc()
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数 调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对 象进入老年代的年龄,让对象在新生代多存活一段时间。
空间分配担保失败
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。JDK 1.7 及以前的永久代空间不足
在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静 态变量等数据。
当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也 会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
Concurrent Mode Failure
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时 性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
ClassLoader原理和应用
ClassLoader的作用
- 加载class字节码文件到jvm
- 确认每个类应由那个类加载器加载,这也影响到两个类是否相等的判断,影响的方法有equals()、isAssignableFrom()、isInstance()以及instanceof关键字
加载的类存放在哪里?
jdk8之前在方法区,8之后在元数据区。
什么时候触发类加载?
- 隐式加载
- 遇到new、getstatic、putstatic、invokestatic4条字节码指令时
- 对类进行反射调用时
- 当初始化一个类时,如果父类还没初始化,优先加载父类并初始化
- 虚拟机启动时,需指定一个包含main函数的主类,优先加载并初始化这个主类
- 显式加载
- 通过ClassLoader的loadClass方法
- 通过Class.forName
- 通过ClassLoader的findClass方法
- 隐式加载
有哪些类加载器ClassLoader?
- Bootstrap ClassLoader:加载JVM自身工作需要的类,由JVM自己实现。加载JAVA_HOME/jre/lib下的文件
- ExtClassLoader:是JVM的一部分,由
sun.misc.Launcher$ExtClassLoader
实现,会加载JAVA_HOME/jre/lib/ext下的文件,或由System.getProperty("java.ext.dirs")
指定的目录下的文件 - AppClassLoader:应用类加载器,由
sun.misn.Launcher$AppClassLoader
实现,加载System.getProperty("java.class.path")
目录下的文件,也就是classpath路径。
双亲委派模型
原理:当一个类加载器收到类加载请求时,如果存在父类加载器,会先由父类加载器进行加载,当父类加载器找不到这个类时(根据类的全限定名称。找不到是由于,每个类有自己的加载路径。),当前类加载器才会尝试自己去加载。
为什么使用双亲委派模型?它可以解决什么问题?
双亲委派模型能够保证类在内存中的唯一性。
假如没有双亲委派模型,用户自己写了个全限定名为
java.lang.Object
的类,并用自己的类加载器去加载,同时BootstrapClassLoader加载了rt.jar包中的jdk本身的java.lang.Object
,这样内存中就存在两份Object类了,会出现很多问题,例如根据全限定名无法定位到具体的类。
高吞吐量的话用哪种gc算法
高吞吐量,如果指cpu多用于用户程序,需要停顿时间比较短的收集器,新生代在服务端一般用Parallel Scavenge,算法也是复制算法。
复制算法的性能比较高。
jvm参数调优详细过程,到为什么这么设置,好处,一些gc场景,如何去分析gc日志
jvm调优的基本原则:
- 大多数Java应用不需要进行JVM优化
- 大多数导致GC频繁、内存使用率高的问题的原因是代码层面的问题(代码层面)
- 上线前应考虑将JVM参数设置最优
- 减少创建对象的数量(代码层面)
- 较少使用全局变量和大对象(代码层面)
- 优先架构调优和代码调优,JVM优化是不得已的手段,或者说是发现问题
- 分析gc情况优化代码比优化JVM参数更好(代码层面)
https://juejin.im/post/5dea4cb46fb9a01626644c36
新生代配置原则:
1.追求响应时间优先 这种需求下,新生代尽可能设置大一些,并通过实际情况调整新生代大小,直至接近系统的最小响应时间。因为新生代比较大,发生垃圾回收的频率会比较低,响应时间快速。
2.追求吞吐量优先 吞吐量优先的应用,在新生代中的大部分对象都会被回收,所以,新生代尽可能设置大。此时不追求响应时间,垃圾回收可以并行进行。
3.避免设置过小新生代 设置过小,YGC会很频繁,同时,很可能导致对象直接进入老年代中,老年代空间不足发生FullGC。
老年代配置原则:
1.追求响应时间优先 这种情况下,可以使用CMS收集器,以获取最短回收停顿时间,但是其内存分配需要注意,如果设置小了会造成回收频繁并且碎片变多;如果设置大了,回收的时间会很长。所以,最优的方案是根据GClog分析垃圾回收信息,调整内存大小。
2.追求吞吐量优先 吞吐量优先通常需要分配一个大新生代、小老年代,将短期存活的对象在新生代回收掉。
JVM性能调优的监控工具了解那些?
jps jstack jmap
jps [option]
输出Java进程信息
1 | jps -ml |
jstack [option] pid
输出某个进行内的线程栈信息
1 | jstack 111957 | grep 1b6d0 |
1 | -l long listings,会打印出额外的锁信息,在发生死锁时可以用<strong>jstack -l pid</strong>来观察锁持有情况 |
jmap [option] pid
输出某个进程内的堆信息:JVM版本、使用的GC算法、堆配置、堆内存使用情况
1 | jmap -heap 111957 |
输出堆内存中对象个数、大小统计直方图
1 | jmap -histo:live 111957 | less |
1 | B byte |
dump出堆信息,再使用jhat或其他工具分析
1 | jmap -dump:format=b,file=dump.dat 111957 |
jstat [ generalOption | outputOptions vmid [interval[s|ms] [count]] ]
jvm统计信息
vmid是Java虚拟机ID,在Linux/Unix系统上一般就是进程ID。interval是采样时间间隔。count是采样数目。
1 | jstat -gc 111957 250 6 # gc信息 |
1 | S0C、S1C、S0U、S1U:Survivor 0/1区容量(Capacity)和使用量(Used) |
Java IO
Java中的NIO,BIO,AIO分别是什么
- BIO:同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
- NIO:同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
- AIO:异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理.AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
网络
计算机网络
TCP/UDP的区别
UDP:用户数据报协议 UDP(User Datagram Protocol)是无连接的,尽最大可能交付,没有拥塞控制,面向报文 (对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部),支持一对一、一对多、多对一和多对多 的交互通信。
TCP:传输控制协议 TCP(Transmission Control Protocol)是面向连接的,提供可靠交付,有流量控制,拥塞控 制,提供全双工通信,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据 块),每一条 TCP 连接只能是点对点的(一对一)。
UDP首部格式
首部字段只有 8 个字节,包括源端口、目的端口、长度、检验和。12 字节的伪首部是为了计算检验和临时添加的。
TCP首部格式
序号 :用于对字节流进行编号,例如序号为 301,表示第一个字节的编号为 301,如果携带的数据长度为 100 字节,那么下一个报文段的序号应为 401。
确认号 :期望收到的下一个报文段的序号。例如 B 正确收到 A 发送来的一个报文段,序号为 501,携带的数据 长度为 200 字节,因此 B 期望下一个报文段的序号为 701,B 发送给 A 的确认报文段中确认号就为 701。
数据偏移 :指的是数据部分距离报文段起始处的偏移量,实际上指的是首部的长度。
确认 ACK :当 ACK=1 时确认号字段有效,否则无效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置 1。
同步 SYN :在连接建立时用来同步序号。当 SYN=1,ACK=0 时表示这是一个连接请求报文段。若对方同意建 立连接,则响应报文中 SYN=1,ACK=1。
终止 FIN :用来释放一个连接,当 FIN=1 时,表示此报文段的发送方的数据已发送完毕,并要求释放连接。
窗口 :窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制,是因为接收方的数据缓存空 间是有限的。
TCP如何保证传输的有效性。
使用超时重传来实现可靠传输:如果一个已经发送的报文段在超时时间内没有收到确认,那么就重传这个报文段。
TCP滑动窗口
暂时存放字节流。发送方和接收方各有一个窗口,接收方通过TCP报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其他信息设置自己的窗口大小。
发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。
接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {34, 35} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前 的所有字节都已经被接收。
TCP的拥塞控制
与流量控制的区别:
- 流量控制是上一题里窗口,接收方发送窗口值来控制发送方的窗口大小,从而影响发送方的发送速率。将窗口值设置为0,则发送方不能发送数据。
- 控制发送方的发送速率,保证接收方来得及接收。
- 流量控制是上一题里窗口,接收方发送窗口值来控制发送方的窗口大小,从而影响发送方的发送速率。将窗口值设置为0,则发送方不能发送数据。
拥塞控制
是为了降低整个网络的拥塞程度
主要通过四个算法进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。
发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量(只是一个状态变量,不是发送方窗口。再区别一下,拥塞窗口讨论的是报文段数量,发送窗口讨论的是字节数量)
慢开始与拥塞避免
发送的最初是慢开始,cwnd=1,发送方只能发送一个报文段;接收到确认后,将cwnd加倍,之后能发送的报文段数量是2、4、8..
ssthresh是慢开始门限(初始值自己定),当cwnd >= ssthresh 时,进入拥塞避免,每个轮 次只将 cwnd 加 1。
如果出现超时,则另ssthresh = cwnd / 2,并重新执行慢开始。
见图1、2、3
快重传与快恢复
【在接收方,要求每次接收到报文段都应该对最后一个已收到的有序报文段进行确认。例如已经接收到 M1 和 M2,此时收到 M4,应当发送对 M2 的确认。】
在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传,立即重传下一个报文段。【例如收到三个 M2,则 M3 丢失,立即重传 M3。】
同时执行快恢复,令 ssthresh = cwnd / 2 ,cwnd = ssthresh,并直接进入拥塞避免。
见上图4、5
TCP建立连接的三次握手
假设A为客户端,B为服务端
- 首先B处于监听(listen)状态,等待客户的连接请求
- A向B发送连接(SYN,同步)请求报文,SYN=1,ACK=0,seq=x(选择一个初始的序号x)
- B收到连接请求报文,如果同意建立连接,则向A发送连接确认报文,SYN=1,ACK=1,ack=x+1(确认号为x+1),seq=y(同时也选择一个初始的序号y)
- A收到B的连接确认报文后,还要向B发出确认,seq=x+1(序号为x+1),ack=y+1(确认号为y+1)
为什么要三次握手?
三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。
客户端发送的连接请求如果在网络中滞留,那么隔很长时间才能收到服务器发回的连接确认,在这段时间内,客户端等待一个超时重传时间后,就会重新发送连接请求。同时滞留的连接请求最后还是会到达服务器,如果只是两次握手,那么服务器会打开两个连接。如果有第三次握手,客户端会忽略服务器之后发送的对滞留连接请求的连接确认,不进行第三次握手,因此就不会再次打开连接。
TCP四次挥手断开连接
ack都为1.
A 发送连接释放报文,FIN=1。
B 收到之后发出确认,此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据。
当 B 不再需要连接时,发送连接释放报文,FIN=1。
A 收到后发出确认,进入 TIME-WAIT 状态,等待 2 MSL(最大报文存活时间)后释放连接。
B 收到 A 的确认后释放连接。
四次挥手的原因
客户端发送FIN连接释放报文后,服务器收到这个报文就进入CLOSE_WAIT状态,这个状态是为了让服务器端发送未传送完毕的数据,发完后服务器就会发送FIN连接释放报文。
TIME_WAIT
客户端收到服务端的FIN报文后进入此状态,并不是直接进入CLOSED状态,还需要等待一个时间计时器设置的时间2MSL。有两个理由:
- 确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文,那么就会重新发送连接释放请求报文, A 等待一段时间就是为了处理这种情况的发生。
- 等待一段时间是为了让本次连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文。
哪些典型的应用用的是udp
dns: Domain Name System,域名系统 域名解析
TFTP: Trivial File Transfer Protocol,简单文件传输协议
1.包总量较少的通信(DNS、SNMP等)
2.视频、音频等多媒体通信(即时通信)
3.限定于 LAN 等特定网络中的应用通信
4.广播通信(广播、多播)
HTTP
https和http区别,有没有用过其他安全传输手段?
区别:
- http明文传输,安全性低;HTTPS数据加密传输,安全性高
- 使用https协议需要到CA(Certificate Authority,数字证书认证机构)申请证书
- http的响应速度比HTTPS快,因为HTTPS除了http三次握手的包,还要加上ssl的交互–具体是?
- 端口不同,http80端口,https443端口
- https本质是构建在ssl/tls之上的http协议
Http协议
基础概念
URI:uniform resource identifier 统一资源标识符
- URL:uniform resource locator 统一资源定位符
- URN:uniform resource name 统一资源名称
- URI包括URL和URN
请求报文的格式
request line 请求行:请求方法,URL,协议
request headers 请求头:各种header
请求行和请求头合称为请求消息头
空行分隔开请求头和请求消息体
request message body 请求消息体:key-value形式或者raw格式等等
响应报文的格式
status line 状态行:协议,状态码
response headers 响应头
状态行和响应头合称为响应消息头
空行分隔开消息头和消息体
response message body 响应消息体
HTTP方法
- get 主要用来获取资源
- head 获取报文首部,主要用于确认 URL 的有效性以及资源更新的日期时间等。
- post 主要用来传输数据
- put 上传文件,不带验证机制存在安全问题,一般不使用
- patch 对资源进行部分修改 – 也不常用
- delete 删除文件,与put功能相反,同样不带验证机制
- options 查询支持的方法,会返回
Allow: GET, POST, HEAD, OPTIONS
这样的内容 - connect 要求在与代理服务器通信时建立隧道。使用 SSL(Secure Sockets Layer,安全套接层)和 TLS(Transport Layer Security,传输层安全)协议把通信内容 加密后经网络隧道传输。
- trace 追踪路径,一般也不用…
HTTP状态码
简要记一下
- 1XX 信息性状态码,接收的请求正在处理
- 2XX 请求正常处理完毕
- 3XX 重定向
- 4XX 客户端错误
- 5XX 服务端错误
再关注下前面的http和HTTPS的比较
其他安全传输手段:SSH
延伸
https的特性:加密保证安全性防窃听、认证防伪装、完整性防篡改
加密方式:混合加密,用非对称加密传输对称秘钥,用对称秘钥进行要传输的数据的加解密
认证:使用证书来对通信双方认证。
完整性:ssl提供报文摘要功能来进行完整性保护。
http也可以通过md5验证完整性,但数据篡改后也可重新生成md5,因为是明文的。https是通过ssl的报文摘要来保证完整性的,结合了加密与认证,即使加密后数据被篡改,也很难再生成报文摘要,因为不知道明文是什么。
cookie session介绍一下
cookie
- 是服务器发送到用户浏览器并保持在本地的一小块数据,会在浏览器向同一服务器再次发起请求时被带上。
- 用途:
- 会话状态管理(比如用户登录状态、购物车等)
- 个性化设置(比如用户自定义设置、主题等)
- 浏览器行为分析
- 生成方式
- 服务器发送
Set-Cookie: yummy_cookie=choco
这样的header,客户端得到响应报文后把cookie存在浏览器 - 浏览器通过
document.cookie
属性可创建新的cookie
- 服务器发送
- HttpOnly 标记为 HttpOnly 的 Cookie 不能被 JavaScript 脚本调用。
- Secure 标记为 Secure 的 Cookie 只能通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信 息也不应该通过 Cookie 传输,因为 Cookie 有其固有的不安全性,Secure 标记也无法提供确实的安全保障。
session
- 存储在服务端,可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中
- 使用 Session 维护用户登录状态的过程如下:
- 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中;
- 服务器验证该用户名和密码,如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为 Session ID;
- 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中;
- 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取 出用户信息,继续之前的业务操作。
cookie和session的选择
cookie只能存储ASCII码字符串,session可以存储任何类型的数据
cookie存储在浏览器中,安全性较低
对于大型网址,如果所有用户信息都存储在session中,开销比较大 – 【感觉不是个问题…】
session表结构怎么设计,储存在哪里?
- 我们项目里没有直接使用session,用的是商城统一单点登录
- 如果我设计
- 首先一个用户请求过来,如果没有带session id,先重定向到登录页
- 收到登录请求,身份验证通过后,生成一个session,key为唯一ID,即session id,value为需要存储的信息,比如用户名、生成时间等,将session id作为cookie响应发回浏览器
- 众多的session是key-value结构,session本身也是key-value结构
- 存储在Redis
你们的session cookie在项目里运用到哪里?
- session是SSO用的,cookie也主要是SSO用的
- 偶尔用的cookie是虚拟登录这样的场景
- 比如超级账号:员工的erp账号以只读的形式登录到用户账号,主要用于排查问题
- 比如账号管家:系统中,账号体系中的主账号可以登录到子账号上,一般也只读
- 再如虚拟登录,业务范畴上,两个账号建立授权关系,B账号可以虚拟登录到A账号上,代为操作系统
- 实现:被登录人一般是sso中的session对应的用户,属于资源所属者;操作者是erp账号、主账号、虚拟登录账号等,会有登录类型区分,这些信息会先加密,再存入cookie中(还会有不同的拦截器,进行身份和权限验证)
单点登录的实现
CAS
TGT:Ticket Granted Ticket(俗称大令牌,或者说票根,他可以签发ST)。【类似session】
TGC:Ticket Granted Cookie(cookie中的value),存在Cookie中,根据他可以找到TGT。【类似session id】
ST:Service Ticket (小令牌),是TGT生成的,默认是用一次就生效了。也就是上面的ticket值。
ps: 未登录状态下,访问app1时,展示登录页,浏览器会写入cas服务器的TGC;第二次访问app2,(因为app2本身校验当前请求未登录)重定向到cas服务器时,会带上TGC,cas服务器根据TGC判断用户已登录,签发新的ST再重定向到app2,这时候app2用ST校验通过,记录下自己的session cookie,提供请求内容。
OAuth【不看了不看了!】https://juejin.im/post/5cc81d5451882524f72cd32c
https://juejin.im/post/5b3b3b61f265da0f955ca780
操作系统
冯诺依曼计算机的结构
运算器(算术逻辑单元,处理寄存器)
控制器(指令寄存器,程序计数器)
存储器(存储数据和指令)
输入设备
输出设备
Linux怎么查看系统负载情况?
- uptime
- w
- top
线上服务器cpu飙高,如何处理这个问题
- 定位进程:top 查看cpu占用情况
- 定位线程:如果是Java应用,top -Hp pid
- 定位代码`
printf %x tid
打印出线程ID对应的16进制数 0xtidjstack pid |grep -A 200 0xtid
内核态 和 用户态、cas 和 sout 哪个用到了内核态和用户态的切换
sout用到了切换
进程的调度
进程间的通讯方式
线程间的同步方式
进程和线程的区别
框架
Spring&SpringMVC
请详细描述springmvc处理请求全流程?
通用的流程:
客户端提交请求到DispatcherServlet
DispatcherServlet寻找Handler(HandlerExecutionChain)(包括handler , common interceptors和MappedInterceptor)
DispatcherServlet调用controller
controller调用业务逻辑,返回ModelAndView
DispatcherServlet寻找ViewResolver,找到对应视图
渲染视图显示到客户端
restful的一些细节(上述2、3、4过程的细化,restful的mav一般是空的):
- getHandler取到一个HandlerExecutionChain mappedHandler,包含URL对应的controller方法HandlerMethod,和一些interceptors
- HandlerMethod取到对应的handlerAdapter,数据绑定就再这个ha中做的
- mappedHandler执行拦截器的preHandle
- handlerAdapter执行controller方法,包含请求前的数据绑定(数据转换),和请求后的数据转换(转换后将数据按需要的格式写入response)
- mappedHandler执行拦截器的postHandle
- 以上过程如果有抛出异常,由全局异常处理器来处理
- mappedHandler触发拦截器的afterCompletion
- 讲一讲AtomicInteger,为什么要用CAS而不是synchronized?
ioc原理、aop原理和应用
ioc原理 控制反转(依赖注入)
- 本质是,spring维护了一个实例的容器,在需要使用某个实例的地方,自动注入这个实例
- 主要运用了反射机制,通过反射来创建约定的实例,并维护在容器中
aop原理 面向切面编程
原理是动态代理。代理模式的定义:给某一个对象提供一个代理,并由代理对象控制对原对象的引用。实现方式:
首先有接口A,类a实现接口A
接着创建一个bInvocationHandler类,实现InvocationHandler接口,持有一个被代理对象的实例target,invoke方法中触发method
1
2
3
4
5
6
7
8
9
10/**
* proxy: 代表动态代理对象,编译时候生成的
* method:代表正在执行的方法
* args:代表调用目标方法时传入的实参
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代理执行" +method.getName() + "方法");
Object result = method.invoke(target, args);
return result;
}创建代理对象
1
A a = (A) Proxy.newProxyInstance(A.class.getClassLoader(), new Class<?>[]{A.class}, handler)
比如日志、监控等公共行为可以通过AOP来实现,避免大量重复代码
元素
- 切面:拦截器类,定义切点以及通知
- 切点:具体拦截的某个业务点
- 通知:切面当中的方法,声明通知方法在目标业务层的执行位置,通知类型如下:
- 前置通知:@Before 在目标业务方法执行之前执行
- 后置通知:@After 在目标业务方法执行之后执行
- 返回通知:@AfterReturning 在目标业务方法返回结果之后执行
- 异常通知:@AfterThrowing 在目标业务方法抛出异常之后
- 环绕通知:@Around 功能强大,可代替以上四种通知,还可以控制目标业务方法是否执行以及何时执行
aspectj切面扫描的细节再看下
spring 事务实现
Spring事务的底层依赖MySQL的事务,代码层面上利用AOP实现。
常用的是@Transactional
注解,会被解析生成一个代理服务,TransactionInterceptor对它进行拦截处理,进行事务开启、 commit或者rollback的操作。
另外,spring还定义了事务传播行为,有7种类型,项目中常见的是PROPAGATION_REQUIRED。如果没有事务就新建事务,如果存在事务,就加入这个事务。
执行事务的时候使用TransactionInterceptor进行拦截,然后处理
事务传播行为类型 | 说明 |
---|---|
PROPAGATION_REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。(如果父方法有事务,加入父方法的事务;父方法没有事务,则自己新建一个事务) |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。(如果父方法有事务,加入父方法的事务;父方法没有事务,则以非事务执行) |
PROPAGATION_MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常。(依赖父方法事务) |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。(如果父方法有事务,把父方法事务挂起,自己新建事务;父方法没有事务,则自己新建一个事务) |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。(如果父方法有事务,把父方法事务挂起,以非事务执行自己的操作;父方法没有事务,则以非事务执行)(总是以非事务执行,不报错) |
PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。(总是以非事务执行,如果父方法存在事务,抛异常) |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 |
REQUIRED、REQUIRES_NEW、NESTED的对比
REQUIRED共用一个事务。
REQUIRES_NEW 有独立的子事务,子事务异常不会导致父事务回滚,父事务异常也不会导致子事务回滚,相互独立。
NESTED 子事务嵌套在父事务中,父事务回滚会引起子事务回滚;父事务正常、子事务异常,子事务可以单独回滚。
- txNamespaceHandle注册的
InfrastructureAdvisorAutoProxyCreator
是一个BeanPostProcessor,主要是为了创建动态代理(wrapIfNecessary)
这几个类是可以自动创建代理的
在创建代理的时候,获取切面
txNamespaceHandler注册了一个Advisor(BeanFactoryTransactionAttributeSourceAdvisor),再在这个advisor中判断是否当前bean符合这个切面(主要实现就是看有没有@Transactional注解)
TransactionInterceptor
是advice,增强,执行切面工作
摘录:https://my.oschina.net/fifadxj/blog/785621
spring-jdb的事务流程:
1 | DefaultTransactionDefinition def = new DefaultTransactionDefinition(); |
PlatformTransactionManager的getTransaction(), rollback(), commit()是spring处理事务的核心api,分别对应事务的开始,提交和回滚。
- TransactionSynchronizationManager负责从ThreadLocal中存取jdbc connection
- 创建事务的时候会通过dataSource.getConnection()获取一个新的jdbc connection,然后绑定到ThreadLocal
- 在业务代码中执行sql时,通过DataSourceUtils.getConnection()从ThreadLocal中获取当前事务的jdbc connection, 然后在该jdbc connection上执行sql
- commit和rollback事务时,从ThreadLocal中获取当前事务的jdbc connection,然后对该jdbc connection进行commit和rollback
mybatis-spring的事务流程:
配置
1 | <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> |
- mybatis-spring依赖DataSourceTransactionManager来处理事务,并没有创建自己的PlatformTransactionManager实现。
- mybatis通过SqlSessionFactoryBuilder创建SqlSessionFactory,而mybatis-spring通过SqlSessionFactoryBean创建SqlSessionFactory。
- 配置使用SpringManagedTransactionFactory来创建MyBatis的Transaction实现SpringManagedTransaction
- 配置使用SqlSessionTemplate代替通过SqlSessionFactory.openSession()获取SqlSession
调用过程
可以看到mybatis-spring处理事务的主要流程和spring jdbc处理事务并没有什么区别,都是通过DataSourceTransactionManager的getTransaction(), rollback(), commit()完成事务的生命周期管理,而且jdbc connection的创建也是通过DataSourceTransactionManager.getTransaction()完成,mybatis并没有参与其中,mybatis只是在执行sql时通过DataSourceUtils.getConnection()获得当前thread的jdbc connection,然后在其上执行sql。
sqlSessionTemplate是DefaultSqlSession的一个代理类,它通过SqlSessionUtils.getSqlSession()试图从ThreadLocal获取当前事务所使用的SqlSession。如果是第一次获取时会调用SqlSessionFactory.openSession()创建一个SqlSession并绑定到ThreadLocal,同时还会通过TransactionSynchronizationManager注册一个SqlSessionSynchronization。
SqlSessionSynchronization是一个事务生命周期的callback接口,mybatis-spring通过SqlSessionSynchronization在事务提交和回滚前分别调用DefaultSqlSession.commit()和DefaultSqlSession.rollback()
这里的DefaultSqlSession只会进行一些自身缓存的清理工作,并不会真正提交事务给数据库,原因是这里的DefaultSqlSession使用的Transaction实现为SpringManagedTransaction,SpringManagedTransaction在提交事务前会检查当前事务是否应该由spring控制,如果是,则不会自己提交事务,而将提交事务的任务交给spring,所以DefaultSqlSession并不会自己处理事务。
DefaultSqlSession执行sql时,会通过SpringManagedTransaction调用DataSourceUtils.getConnection()从ThreadLocal中获取jdbc connection并在其上执行sql。
mybatis-spring做的最主要的事情是:
在SqlSession执行sql时通过用SpringManagedTransaction代替mybatis的JdbcTransaction,让SqlSession从spring的ThreadLocal中获取jdbc connection。
通过注册事务生命周期callback接口SqlSessionSynchronization,让SqlSession有机会在spring管理的事务提交或回滚时清理自己的内部缓存。
spring的循环依赖如何解决?为什么要三级缓存?
https://juejin.im/post/5c98a7b4f265da60ee12e9b2
https://juejin.im/post/5e927e27f265da47c8012ed9
spring对循环依赖的处理有三种情况:
- 构造器的循环依赖:这种依赖spring是处理不了的,直 接抛出BeanCurrentlylnCreationException异常。
- 单例模式下的setter循环依赖:通过“三级缓存”处理循环依赖。
- 非单例循环依赖:无法处理。
如何解决的?
只能解决单例的属性循环依赖的情况。本质上是通过将创建好的、或正在创建中的bean缓存起来。比如A和B循环依赖,创建A时先将A的实例放入缓存,自动注入属性B时,发现缓存中没有B,那么来创建B的实例,将B实例化放入缓存,注入属性A,发现A在缓存中,取出来赋值给A。bean B创建完成返回,赋值给A的属性B。这时候A和B的bean就都创建好了。
为什么要三级?看起来一级就可以实现呀?
为什么要三级缓存:循环依赖的关键点:提前暴露绑定A原始引用的工厂类到工厂缓存。等需要时触发后续操作处理A的早期引用,将处理结果放入二级缓存
只有一级singeltonObjects肯定是不行的,需要一个放半成品的地方
实际上二级就够了,可以解决循环依赖的问题
考虑到代理的情况,就需要objectFactories这个三级缓存了,因为代理的创建是在第三步,这时候动态代理还没产生,注入了也不是最终的实例。放入三级缓存时,重写了getObject方法,会调用BeanPostProcessor的getEarlyBeanReference,这时候取到的就会是动态代理后的。
Zookeeper
zk挂了怎么办? todo
指zk集群挂了其中一台机器? – 集群自己可以处理
- 挂的是master
- 挂的是follower
- 挂的是..
集群全挂了?—那就是全挂了啊 趁早加入监控和降级策略
Dubbo&Netty&RPC
https://juejin.im/post/5e215783f265da3e097e9679
RPC
remote procedure call 远程过程调用,是一种进程间的通信方式,是一种技术思想,而不是规范
一次完整的rpc调用流程。RPC的目标是把2-8封装起来,对用户透明。
(1):服务消费方(client)以本地调用方式调用服务。
(2):client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体。
(3):client stub找到服务地址,并将消息发送到服务端。
(4):server stub收到消息后进行解码。
(5):server stub根据解码结果调用本地的服务。
(6):本地服务执行并将结果返回给server stub。
(7):server stub将返回结果打包成消息并发送至消费方。
(9):client stub接收到消息,并进行解码。
(9):服务消费方得到最终结果。
决定rpc效率的两个重要因素:通信效率,序列化和反序列化效率
常见rpc框架:dubbo、gRPC、Thrift、HSF(high speed service framework)
netty 理解netty
netty是一个异步事件驱动的网络应用程序框架,是基于NIO的多路复用模型实现的。
传统HTTP服务
【HTTP服务器之所以称为HTTP服务器,是因为编码解码协议是HTTP协议,如果协议是Redis协议,那它就成了Redis服务器,如果协议是WebSocket,那它就成了WebSocket服务器,等等。 使用Netty可以定制编解码协议,实现自己的特定协议的服务器。】
- 创建一个ServerSocket,监听并绑定一个端口
- 一系列客户端来请求这个端口
- 服务器使用Accept,获得一个来自客户端的Socket连接对象
- 启动一个新线程处理连接
- 读Socket,得到字节流
- 解码协议,得到HTTP请求对象
- 处理HTTP请求,得到一个结果,封装成一个HTTPResponse对象
- 编码协议,将结果序列化字节流写入Socket,发给客户端
- 循环步骤3
NIO
不是Java独有的概念,NIO代表IO多路复用。
由操作系统提供的功能,早期select,后期linux-epoll/max-kqueue。一般就说是epoll(没人用mac当服务器)
Netty基于Java NIO进行了封装,提供易于操作的使用模式和接口。
BIO (Blocking IO),如何理解blocking
- 服务端监听时,accept是阻塞的,只有新连接来了,accept才会返回,主线程才能继续
- 读写Socket时,read是阻塞的,只有请求消息来了(需要读完吗?),read才能返回,子线程才能继续处理
- 读写Socket时,write是阻塞的,只有客户端把消息接收了(客户端把消息接收了是什么表现?),write才能返回,子线程才能继续
NIO利用事件机制(=事件驱动机制)实现非阻塞。【可以用一个线程把Accept,读写操作,请求处理的逻辑全干了。如果什么事都没得做,它也不会死循环,它会将线程休眠起来,直到下一个事件来了再继续干活,这样的一个线程称之为NIO线程。】
伪代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15while true {
events = takeEvents(fds) // 获取事件,如果没有事件,线程就休眠
for event in events {
if event.isAcceptable {
doAccept() // 新链接来了
} elif event.isReadable {
request = doRead() // 读消息
if request.isComplete() {
doProcess()
}
} elif event.isWriteable {
doWrite() // 写消息
}
}
}
Reactor(基于事件驱动)线程模型
【netty可以基于以下模型灵活配置,比较常见的是用第三种。】
【在Netty里面,Accept连接可以使用单独的线程池去处理,读写操作又是另外的线程池来处理。】
【Accept连接和读写操作也可以使用同一个线程池来进行处理。请求处理逻辑既可以使用单独的线程池进行处理,也可以跟读写线程放在一块处理。】
【线程池中的每一个线程都是NIO线程。用户可以根据实际情况进行组装,构造出满足系统需求的高性能并发模型。】
Reactor单线程模型。一个NIO线程+一个accept线程。reactor线程负责分发,read、decode等操作都由其他线程处理。就和上面的伪代码差不多。
Reactor多线程模型。相比上一种,【其他线程】由线程池来托管。
Reactor主从模型。多个acceptor的NIO线程池用于接收客户端的连接。
TCP粘包拆包
现象
- 假设使用netty在客户端重复写100次数据”你好,我的名字是xxx!”给服务端,用ByteBuf存放这个数据
- 服务端接收后输出,一般存在三种情况
- 完整的一个字符串
- 字符串多了
- 字符串少了
原因:尽管client按照ByteBuf为单位发送数据,server按照ByteBuf读取,但操作系统底层是tcp协议,按照字节发送和接收数据,在netty应用层,重新拼装成的ByteBuf与客户端发送过来的ByteBuf可能不是对等的。
因此,我们需要自定义协议来封装和解封应用层的数据包。
netty中定义好的拆包器
- 固定长度的拆包器 FixedLengthFrameDecoder
- 行拆包器 LineBasedFrameDecoder
- 分隔符拆包器 DelimiterBasedFrameDecoder (行拆包器的通用版本,可自定义分隔符)
- 长度域拆包器 LengthFieldBasedFrameDecoder (最通用,在协议中包含长度域字段)
零拷贝
传统方式的拷贝
File.read(bytes)
Socket.send(bytes)
需要四次数据拷贝和四次上下文切换
数据从磁盘读取到内核的read buffer
数据从内核缓冲区拷贝到用户缓冲区
数据从用户缓冲区拷贝到内核的socket buffer
数据从内核的socket buffer拷贝到网卡接口(硬件)的缓冲区
零拷贝的概念
上面的第二步和第三步是没有必要的,通过java的FileChannel.transferTo方法,可以避免上面两次多余的拷贝(需要操作系统支持)
调用transferTo,数据从文件由DMA引擎拷贝到内核read buffer
接着DMA从内核read buffer将数据拷贝到网卡接口buffer
上面的两次操作都不需要CPU参与,达到了零拷贝。
Netty中的零拷贝
体现在三个方面:
bytefuffer
Netty发送和接收消息主要使用bytebuffer,bytebuffer使用直接内存(DirectMemory)直接进行Socket读写。
原因:如果使用传统的堆内存进行Socket读写,JVM会将堆内存buffer拷贝一份到直接内存中然后再写入socket,多了一次缓冲区的内存拷贝。DirectMemory中可以直接通过DMA发送到网卡接口
Composite Buffers
传统的ByteBuffer,如果需要将两个ByteBuffer中的数据组合到一起,需要先创建一个size=size1+size2大小的新的数组,再将两个数组中的数据拷贝到新的数组中。
使用Netty提供的组合ByteBuf,就可以避免这样的操作。CompositeByteBuf并没有真正将多个Buffer组合起来,而是保存了它们的引用,从而避免了数据的拷贝,实现了零拷贝。
对FileChannel.transferTo的使用
Netty中使用了FileChannel的transferTo方法,该方法依赖于操作系统实现零拷贝。
dubbo
简介与特性:dubbo是一款高性能、轻量级的开元Java RPC框架,提供三大核心能力:面向接口的远程方法调用、智能容错和负载均衡、服务自动注册和发现。
- 【以下几点是官网上的特性介绍…】
- 面向接口的远程方法调用:提供高性能的基于代理的远程调用能力,服务以接口为粒度,为开发者屏蔽远程调用底层细节。
- 智能负载均衡:内置多种负载均衡策略(有哪些?),感知下游节点的健康状况,显著减少调用延迟,提高系统吞吐量。
- 服务自动注册于发现:支持多种注册中心服务(有哪些?),服务实例上下线实时感知(具体实现是什么?)。
- 高度可扩展能力:遵循微内核+插件的设计原则,所有核心能力如Protocol、Transport、Serialization被设计为可扩展点,平等的对待内置实现和第三方实现。(SPI设计模式?)
- 运行期流量调度:内置条件、脚本等路由策略,通过配置不同的路由规则,实现灰度发布、同机房优先等功能。
- 可视化的服务治理与运维:提供丰富服务治理、运维工具:随时查看服务元数据、服务健康状态以及调用统计,实时下发路由策略、调度配置参数。
dubbo架构
以上两张图说明dubbo执行流程:
- dubbo容器启动后,provider将自己提供的服务注册到注册中心(注册中心便知道有哪些服务上线了)
- consumer启动后,从注册中心订阅需要的服务。
- 注册中心以长连接的方式向consumer发送服务变更通知。
- consumer同步调用provider的服务(如果服务有多个节点,可通过负载均衡算法选择一个节点进行调用)
- consumer和provider会定期将调用信息(调用时间、调用服务信息)发送给监控中心
- Dubbo容器启动、服务生产者注册自己的服务、服务消费者从注册中心中订阅服务是在Dubbo应用启动时完成的;consumer调用provider是同步过程;注册中心向consumer发送服务变更通知是异步的;consumer和provider向监控中心发送信息是异步的。
调用链整体展开:
下面这张图看起来有点复杂了..
Dubbo配置的覆盖关系 (1):方法级优先、接口级次之,全局配置优先级最低。 (2):如果级别一样,则消费者优先,提供方次之。
dobbo高可用
- 注册中心Zookeeper宕机,还可以消费Dubbo暴露的服务。
- Dubbo的监控中心宕机,不会影响Dubbo的正常使用,只是丢失了部分采样数据。
- 数据库宕机后,注册中心仍然可以通过缓存提供服务列表查询,但是不能注册新的服务。
- 注册中心集群的任意一个节点宕机,将自动切换到另外一台。
- 注册中心全部宕机,服务提供者和消费者可以通过本地缓存通讯。
- 服务提供者无状态,任意一台宕机后,不影响使用。
- 服务提供者全部宕机,服务消费者应用将无法使用,并且会无限次重连等待服务提供者恢复。
负载均衡策略
【默认为随机】
基于权重的随机负载均衡:Random LoadBalance,比如orderService想要远程调用userService,而userService分别在三台机器上,我们可以给每台机器设置权重,比如三台机器的权重依次为100、200、50,则总权重为350,则选择第一台的概率就是100/350.
基于权重的轮询负载均衡:RoundRobin LoadBalance(可以理解为按照权重占比进行轮询。占比少的,当权重比较低时就不会再去权重低的机器上请求。如果某台机器性能一般,但权重占比高,就很可能卡在这里)
最少活跃数负载均衡:LeastActive LoadBalance,比如三台服务器上一次处理请求所花费的时间分别为100ms、1000ms、300ms,则这一次请求回去上一次处理请求时间最短的机器,所以这次一号服务器处理这次请求。
一致性Hash负载均衡:ConsistentHash LoadBalance
原文:https://blog.csdn.net/revivedsun/java/article/details/71022871
一致性Hash负载均衡涉及到两个主要的配置参数为hash.arguments 与hash.nodes。
hash.arguments : 当进行调用时候根据调用方法的哪几个参数生成key,并根据key来通过一致性hash算法来选择调用结点。例如调用方法invoke(String s1,String s2); 若hash.arguments为1(默认值),则仅取invoke的参数1(s1)来生成hashCode。
hash.nodes: 为结点的副本数。
1
2
3
4
5缺省只对第一个参数Hash,如果要修改,请配置
<dubbo:parameter key="hash.arguments" value="0,1" />
缺省用160份虚拟节点,如果要修改,请配置
<dubbo:parameter key="hash.nodes" value="320" />降级服务
当服务器压力剧增的情况下,根据实际业务及流量,对一些服务和页面有策略地不处理或者换种简单的方式处理,从而释放服务器资源以保证核心交易正常或高效运行。
mock=force:return+null:表示消费方对该服务的方法都返回null值,不发起远程调用。用来屏蔽不重要的服务不可用时对调用方的影响,可以直接在Dubbo客户端(localhost:7001)对服务消费者设置,屏蔽掉即可。
mock=fall:return+null:表示消费方对该服务的方法调用在失败后,再返回null,不抛出异常。用来容忍不重要服务不稳定时对调用方的影响,可以直接在Dubbo客户端(localhost:7001)对服务消费者设置,容错掉即可。
集群容错
- Failover Cluster:失败自动切换,当出现失败,重试其他服务器。通常用于读操作,但重试会带来更长延迟。可通过retries=n来设置重试次数。
Failfast Cluster:快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增操作。
Forking Cluster:并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多的服务资源。通过过fork=n设置最大并行数。
Broadcast Cluster:广播调用所有提供者,逐个调用,任意一台报错则报错,通常用于通知所有服务提供者更新缓存或日志等本地资源信息。
消息队列
作用
- 解耦
- 异步
- 削峰/限流
原理介绍 todo
如何保证RocketMQ 消息的顺序性,如何解决重复消费问题
针对kafka来说
如何保证消息的顺序性:
一个分区内的消息是顺序的
一个主题的不同分区之间,消息不能保证有序
– 对同一类消息指定相同的key,相同的key会哈希到同一个分区,这样可以保证这部分消息的有序性
https://www.cnblogs.com/756623607-zhang/p/10506909.html
如何解决重复消费:
kafka自带的消费机制
consumer消费后,会定期将消费过的offset偏移量提交给broker。如果consumer重启,会继续上次的offset开始消费。
业务上保证幂等性
如果进程挂了或机器宕机,没来得及提交offset,需要业务上进行幂等。
比如建立一张消息表。
生产者,发送消息前判断库中是否有记录(有记录说明已发送),没有记录,先入库,状态为待消费,然后发送消息并把主键id带上。
消费者,接收消息,通过主键ID查询记录表,判断消息状态是否已消费。若没消费过,则处理消息,处理完后,更新消息记录的状态为已消费。
MyBatis
MyBatis,Mybatis与Spring
MyBatis 消除了大部分 JDBC 的样板代码、手动设置参数以及检索结果。通过简洁的设计最大限度地简化开发和提升性能。
解除SQL与程序代码的耦合,通过提供dao层,将业务逻辑和数据访问逻辑分离开。设计更清晰,更易维护。
MyBatis整体架构
MyBatis层级结构
裸用sqlSession
是上面的红框
spring用mapper/dao接口代理,本质上是一个MapperProxy,从下面的红框开始执行
spring事务是在哪个环节起作用?
https://mybatis.org/spring/zh/transactions.html
一个使用 MyBatis-Spring 的其中一个主要原因是它允许 MyBatis 参与到 Spring 的事务管理中。而不是给 MyBatis 创建一个新的专用事务管理器,MyBatis-Spring 借助了 Spring 中的 DataSourceTransactionManager 来实现事务管理。
一旦配置好了 Spring 的事务管理器,你就可以在 Spring 中按你平时的方式来配置事务。并且支持 @Transactional 注解和 AOP 风格的配置。在事务处理期间,一个单独的
SqlSession
对象将会被创建和使用。当事务完成时,这个 session 会以合适的方式提交或回滚。事务配置好了以后,MyBatis-Spring 将会透明地管理事务。
所以,最外层是事务,每个事务会起一个SqlSession
。
几篇文章:
入门,裸用mybatis:https://juejin.im/post/5aa5c6fb5188255587232e5a#heading-0
mybatis执行,包括整合spring后的流程:https://juejin.im/post/5e350d895188254dfd43def5#heading-9
关于JDBC:https://juejin.im/post/5c75e6666fb9a049cd54dc88
Mybatis和spring整合的使用:https://juejin.im/post/5cdfed6ef265da1b6720dcaf
mybatis框架说明:
整体执行流程说明:
sqlSession执行流程说明:
关键流程(以下整个可以看成裸用MyBatis的执行流程)
config文件加载:解析xml文件配置项
mapper文件加载:上一个流程中的一个环节,解析完后封装成MappedStatement,存入configuration
SqlSource创建流程:上一流程的一个环节,SqlSource是MappedStatement的一部分,主要存放sql和占位的参数名称
– 解析环节结束
SqlSession执行流程:sqlSessionFactory.openSession
主要是建立了一个和数据库的连接connection
获取BoundSql流程:sqlSession.xx
方法执行时,需要获取BoundSql,BoundSql本质上是SqlSource和执行请求的入参的一个组合
参数映射流程:根据顺序,或者根据名称(只是大略看了一眼)
结果集映射流程:根据名称(只是大略看了一眼)
mybatis的openSession默认开启事务,autocommit为false,隔离级别为null
mybatis的JdbcTransaction
整合spring的几个组件
org.mybatis.spring.SqlSessionFactoryBean
注入sqlSessionFactory
org.mybatis.spring.mapper.MapperScannerConfigurer
扫描指定包
- 将包下class文件加入到beanDefinition中,bean类型指定为MapperFactoryBean
- SqlSessionFactoryBean构建sqlSessionFactory时,扫描mapper xml文件,根据namespace在MapperRegistry中注入对应mapper接口的MapperProxyFactory
- MapperFactoryBean->getObject中生成mapper的代理类MapperProxy(通过MapperFactoryBean中的interface,即mapper的namespace找到MapperProxyFactory,再生产出代理类)
以下大概知道了
现在差一个中间环节,mapper的beanDefinition怎么变成MapperProxy..以及MapperFactoryBean的作用
还有个SqlSessionTemplate:https://juejin.im/post/5cea1f386fb9a07ea803a70e
还有MapperProxyFactory – 来创建MapperProxy
Java动态代理:https://juejin.im/post/5c1ca8df6fb9a049b347f55c
MapperFactoryBean
MapperProxy
MapperMethod – 到这里之后,流程就转到sqlSession.selectOne之类的了
Mybatis缓存
https://juejin.im/post/5e81fb126fb9a03c546c22bb
MyBatis 系统中默认定义了两级缓存:一级缓存和二级缓存
- 默认情况下,只有一级缓存开启。(SqlSession级别的缓存,也称为本地缓存)
- 二级缓存需要手动开启和配置,它是基于 namespace 级别的缓存,缓存只作用于 cache 标签所在的映射文件中的语句。
系统设计
分布式
谈谈分布式锁、以及分布式全局唯一ID的实现比较?
分布式锁实现方式及比较
为什么需要分布式锁?
- 效率:避免不同节点重复相同的工作,这些工作会浪费资源。比如针对一个操作发多封邮件。
- 正确性:避免破坏正确性的发生,比如多个节点操作了同一条数据,其中一个操作结果被覆盖了,造成数据丢失。
常见实现方式
- 数据库
- 表的一行数据表示一个资源,
select..for update
来加锁,可同时存节点信息,支持重入。 - 理解简单,但需要自己实现,以及维护超时、事务和异常处理等,性能局限于数据库,性能相对比较低,不适合高并发场景。
- 表的一行数据表示一个资源,
- zookeeper
- curator封装 – 具体怎么用还没看过呢
- Zk 排他锁和共享锁有区别。排他锁,利用zk有序节点,序号最小的节点表示获取到锁,其他未获取到锁的节点监听自己的前一个节点。 (共享锁,能获取到资源都算?回家再看看。)还有个读写锁,一个节点获取读锁,只要序号小于他的都为读锁,就表示获取到读锁;一个节点获取写锁,需要自己的序号最小,才表示获取到写锁。可重入锁之类的,zk节点写值吧,原理和Java的reentrantLock类似,获取多少次,state自加多少次,解锁再一次次自减,直到state为0表示完全释放。
- (文章里说zk分布式锁和数据库mysql差不多。。真的么)
- redis setNX
- 自己参照一篇文章实现的比较简单,主要利用setNX,不支持重入,非公平。有超时释放,有加锁身份,解锁原子性
- 下面的文章里介绍的Redission。不太了解,加锁原子性lua脚本,用了hset,hashmap的结构,key是资源,value是锁定次数,可重入。。还有公平锁的实现。。。
- 数据库
分布式全局唯一ID/分布式ID生成器 实现方式及比较
uuid
- 缺点:长,占用空间大,间接导致数据库性能下降
- 缺点:不是有序的,导致索引在写的时候有过多的随机写
数据库自增主键
- 缺点:完全依赖于数据库,有性能瓶颈
- 缺点:不易扩展
snowflake Twitter,Scala实现的…
雪花算法,带有时间戳的全局唯一ID生成算法
固定ID格式:
1
2
341位的时间戳(精确到毫秒,41位的长度可使用69年)
10位的机器ID(10位长度最多支持1024个服务器节点部署)
12位的计数序列号(12位支持每节点每毫秒最多生成4096个序列号)
分布式锁的实现方式,zk实现和Redis实现的比较
实现方式:CAP的应用
MySQL唯一索引
- 实现:锁名称建立唯一索引,先插入数据的线程获得锁
- 缺点:完全依赖数据库的可用性(单点问题,无主从切换)和性能
Redis
- 实现:
setnx key value expire_time
- 优缺点:为解决无主从切换的问题,可以使用Redis集群,或者sentinel哨兵模型。当master节点出现故障,哨兵从slave中选取节点称为新master节点。文章说,Redis集群的复制是AP模式,可能存在数据不一致,导致存在两个线程获得到锁的情况。(一个线程在原master获得锁,另一个线程在新master获得锁)
- 对数据一致性非常敏感的场景,建议使用CP模型(比如zk)
- 实现:
zk
实现:
- 线程向zk的锁目录,申请创建有序的临时节点
- 如果建成的节点序号最小,表明获得到锁
- 如果序号非最小,监听自己的前一个节点
- 删除节点表示释放锁;当获取锁的客户端异常、无心跳,临时节点会被删除,也表示释放锁
优缺点:CP模式,zk的分布式锁比Redis的可靠,但Redis的性能更高。要根据自己的业务场景,再选型。
对一致性哈希的理解
求出各节点的哈希值,将其配置到0~2^32的圆(continuum)上
用同样方法求出存储数据的哈希值,映射到相同的圆上
从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个节点上。如果超过2^32仍然找不到节点,就保存到第一个节点上
【如果添加一个节点node5,只会影响该节点的逆时针方向的第一个节点node4会受到影响(原来在node4上的数据要重新分配一些到node5上)】
一致性哈希算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性
在服务节点太少时,容易因节点分部不均匀而造成数据倾斜问题,可引入虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射。
对分布式数据一致性的理解
https://juejin.im/post/5ce7b325e51d45772a49ac9d
数据不一致性的情形
- 主库、从库和缓存的数据一致性:相同数据冗余。为保证数据库的高可用和高性能,会采用主从(备)架构并引入缓存。数据不一致存在于数据冗余的时间窗口内。
- 多副本数据之间的数据一致性:相同数据副本。一份数据有多个副本存储到不同节点上,客户端可以访问任一节点进行读写。常用协议包括Paxos、ZAB、Raft、Quorum、Gossip等。
- 分布式服务之间的数据一致性:微服务架构下,不同服务操作不同的库表,要求库表间要保持一致(等价于分布式事务) – 【感觉题目问的是这个】
对CAP理论的理解 https://www.zhihu.com/question/54105974/answer/139037688
C代表一致性,A代表可用性(在一定时间内,用户的请求都会得到应答),P代表分区容忍性。
一个分布式系统里面,节点组成的网络本来应该是连通的。然而可能因为一些故障,使得有些节点之间不连通了,整个网络就分成了几块区域。数据就散布在了这些不连通的区域中。这就叫分区。
当你一个数据项只在一个节点中保存,那么分区出现后,和这个节点不连通的部分就访问不到这个数据了。这时分区就是无法容忍的。
提高分区容忍性的办法就是一个数据项复制到多个节点上,那么出现分区之后,这一数据项就可能分布到各个区里。容忍性就提高了。
然而,要把数据复制到多个节点,就会带来一致性的问题,就是多个节点上面的数据可能是不一致的。要保证一致,每次写操作就都要等待全部节点写成功,而这等待又会带来可用性的问题。
总的来说就是,数据存在的节点越多,分区容忍性越高,但要复制更新的数据就越多,一致性就越难保证。为了保证一致性,更新所有节点数据所需要的时间就越长,可用性就会降低。
理解数据库本地事务 分布式事务
- ACID
- 原子性 atomicity:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
- 一致性 consistency:事务的执行前后,是从一个一致性状态转移到另一个一致性状态。【是通过原子性和隔离性保证的。】
- 隔离性 isolation:事务并发执行时,每个事务有各自完整的数据空间。有不同的隔离级别,大部分通过锁实现。
- 持久性 durability:事务只要成功执行,对数据库所做的更新会永久保存下来。
- 隔离级别
- innodb实现原理:主要通过锁和日志来保证ACID
- 通过锁机制和mvcc实现隔离性
- redo log(重做日志)实现持久性
- undo log实现原子性和一致性【可以回滚】
- ACID
分布式事务 – 主要是要保证原子性
分布式事务
指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
一次大操作由不同的小操作组成,小操作分布在不同的服务器上,且属于不同的应用,分布式服务需要保证这些小操作要么全部成功,要么全部失败。
分布式事务的场景
- Service多个节点 – 指微服务等,比如一个交易平台,订单、库存、余额等在不同的服务下,一次交易需要原子性得更新。
- resource多个节点 – 指分库分表了,比如转账双方的余额在不同的表里,一次转账双方都要正确更新。
理论
- CAP
- BASE
解决方案
- 2PC
- 第一阶段:预提交,并反映是否可以提交。【事务管理器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交.】
- 第二阶段:提交,或者回滚。【事务协调器要求每个数据库提交数据,或者回滚数据。】
- 优点:实现成本低
- 缺点:单点问题(事务管理器单点,可能引起资源管理器一直阻塞),同步阻塞(precommit后,资源管理器一直处于阻塞中,直到提交、释放资源),可能存在数据不一致(比如协调者发出了commit通知,但只有部分参与者收到通知并执行了commit,其余参与者则没收到通知处于阻塞状态,就产生不一致了)
- TCC
- try - confirm - cancel
- 协调者变成多点,引入进群
- 引入超时,超时后进行补偿,并且不会锁定整个资源,缓解同步阻塞
- 数据一致性:通过补偿机制,由业务活动管理器控制一致性
- 本地消息表:核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。
- MQ事务
- SAGA
- seata
- 2PC
分布式事务
描述分布式事务之TCC服务设计?
不了解
TCC分布式事务 try - confirm - cancel
高并发&高性能&高可用系统设计
高并发&高性能&高可用
高并发:系统能够同时、并行处理很多请求,常用指标响应时间、吞吐量、并发用户数、QPS
如何提高:
- 垂直扩展
- 增加单机硬件性能,比如CPU增加核数、升级更好的网卡,换好的硬盘,扩充内存
- 提升单机架构性能。比如使用缓存,使用异步增加单服务吞吐量
- 水平扩展
- 增加服务器数量
- 垂直扩展
高性能:程序处理速度快,占用内存少,CPU占用率低
高并发和高性能紧密相关,提供性能大部分可以提高并发
增加服务器资源(内存、CPU、服务器数量)绝大部分能提高并发和性能
注意事项:
避免因为io阻塞让CPU闲置,浪费CPU
避免频繁创建、销毁线程,导致浪费资源在调度上
高可用:减少停工时间,保持服务的高度可用性(一直能用)
全年停机不超过31.5秒
6个9:一直能用的概率为99.9999%
注意事项:
- 避免单点
- 使用集群
- 心跳机制,监控服务器状态,挂了就进行故障修复
限流算法
缓存:提升系统访问速度和增大系统处理容量
降级:当服务器压力剧增的情况下,根据当前业务情况对一些服务和页面有策略地降级,以此释放服务器资源以保证核心任务的正常运行
限流:限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理
限流算法:
固定窗口法
- 实现:固定时间内限定个数,比如限定每分钟100个请求
- 缺点:无法应对两个时间边界内的突发流量。比如在计数器清零的前一秒和后一秒都进来100个请求,那么系统短时间内就收到了两倍(200个)请求,有可能超负荷。
- 原因:统计精度不够
滑动窗口法
- 实现:简单来说就是随着时间的推移,时间窗口也会持续移动,有一个计数器不断维护着窗口内的请求数量,这样就可以保证任意时间段内,都不会超过最大允许的请求数。例如当前时间窗口是0s~60s,请求数是40,10s后时间窗口就变成了10s~70s,请求数是60。
- 可以用Redis有序集合实现..
- 缺点:还是不能解决细粒度请求过于集中的问题,比如限制一分钟60个请求,但在59s时发送了60个请求过来。
漏桶算法
- 算法思想:与令牌桶算法有点相反。不限制流入速率,但以固定的速度将水流出。如果流入速度太大会导致水满溢出,溢出的请求被丢弃。
- 实现一:基于queue。queue的大小表示桶的大小,queue满了请求会被拒绝;另维护一个定时器,根据设定的出水速度去queue中取一个任务,比如限定一秒钟5个请求,就200ms去取一个任务,取到就执行,取不到就轮空。
- 实现二:基于meter,计数器。【…写的不是很清楚,看起来和固定窗口法很像了,没有体现固定的出水,只表示时间粒度比较细】
令牌桶法
- 算法思路:以固定的速率生成令牌,把令牌放到固定容量的桶里,超过桶容量的令牌则丢弃。每来一个请求获取一次令牌,只有获得令牌的请求才能放行,没有获得令牌的请求丢弃。
- 【令牌是匀速生成的,如果请求超高频,则完全被限制成令牌的生成速率;如果请求突发,也最多只允许令牌数的上限。】
- Guava RateLimiter
令牌桶与漏桶的比较
漏桶能够强行限制数据的传输速率
令牌桶限制数据的平均传输速率,允许某种程度的突发传输
【看起来比较喜欢令牌桶】
两次点击,怎么防止重复下订单?
两次点击的场景
- 没有刷新和前端控制,同一个按钮点了两次
- 网络问题以为失败(其实成功了)又提交了一次
- rpc等重试服务
- 刷新前后各点一次(或者表单刷新又提交了一次)
- 点了后退按钮,再前进
处理方案:
- 前端
- 弹出确认界面,或disable入口并倒计时等
- 后端
- 约定【所谓重复订单,需要定义这是相同的订单】,需要和客户端配合实现
- 比如支付可以用订单ID作判断
- 如果是下单,可以用uuid或服务端先生成一个全局唯一的订单ID,客户端如果未接收到下单成功的响应,多次重试都用这一个订单ID来提交。(如果是刷新,需要客户端去服务端请求最新购物车数据,已成功下单的商品已被移除;如果是未刷新页面的重试,则使用同一个订单ID;或者提示用户刷新、提示是否重试)
- 后端的去重判断方式 https://www.cnblogs.com/jett010/articles/9056567.html – 本质上分布式锁的应用
- 基于数据库中对应订单ID的状态做判断,ID已存在(下单),或者状态已变更(修改订单,比如取消、退款等)。如果查询和更新是分开的两个操作,会存在时间差,比如查询完后状态被别的线程修改了,可以用加数据库锁的方式解决这个问题(悲观锁或乐观锁)
- 利用数据库唯一性索引,性能比较低
- Redis分布式锁,key是订单ID,要点是加锁和解锁的原子性
- ps redis的计数器是原子操作 https://redis.io/commands/incr
- 约定【所谓重复订单,需要定义这是相同的订单】,需要和客户端配合实现
秒杀场景设计,应付突然的爆发流量
两个核心问题:并发读、并发写
对应到架构设计,就是高可用、一致性和高性能的要求
高性能:涉及高读和高写。
核心理念:高读->尽量“少读”或“读少”,高写->数据拆分
动静分离:将页面拆分,静态部分做CDN缓存(秒级失效,若干CDN节点),动态部分异步请求。
数据拆分、静态缓存、数据整合(指动态数据、静态数据怎么整合在一起,一种服务端将动态数据插入到静态页面,另一种前端异步调用)
热点优化
热点操作:零点刷新、零点下单、零点加购物车等,属于用户行为不好改变,但可以做一些限制,比如用户频繁刷新页面时进行提示阻断。
热点数据:
热点识别:分为静态热点(可以提前预测的。大促前夕,可以根据大促的行业特点、活动商家等纬度信息分析出热点商品,或者通过卖家报名的方式提前筛选;另外,还可以通过技术手段提前预测,例如对买家每天访问的商品进行大数据计算,然后统计出 TOP N 的商品,即可视为热点商品)和动态热点(无法提前预测的。比如直播临时做了个广告可能导致一件商品短期内被大量购买)。
动态热点的识别实现思路:1. 异步采集交易链路各个环节的热点key信息,比如nginx采集访问url或agent采集热点日志(一些中间件本身已具备热点发现能力),提前识别潜在的热点数据。2. 聚合分析热点数据,达到一定规则的热点数据,通过订阅分发推送到链路系统,各系统根据自身需求决定如何处理热点数据,或限流或缓存,从而实现热点保护
最好做到秒级实时,这样动态发现才有意义。实际上也是对核心节点的数据采集和分析能力提出了较高的要求。
热点隔离:将热点数据隔离出来,不影响非热点数据的访问。 – 【我怎么觉得参与秒杀的商品都可以直接作为热点呢??】
- 业务隔离。秒杀作为一种营销活动,卖家需要单独报名,从技术上来说,系统可以提前对已知热点做缓存预热 – 【静态热点吧..】
- 系统隔离。系统隔离是运行时隔离,通过分组部署和另外 99% 进行分离,另外秒杀也可以申请单独的域名,入口层就让请求落到不同的集群中 – 【也是静态热点吧..】
- 热点数据,可以启用单独的缓存集群或者DB服务组,从而更好的实现横向或纵向能力扩展 – 【可以是动态的,假如一个商品被动态标记为热点后】
热点优化:对这1%的数据做针对性的优化
- 缓存:热点缓存是最为有效的办法。
- 限流:流量限制更多是一种保护机制。–属于有损服务。
系统优化:提升硬件水平、调优JVM性能、代码层面优化
- 代码层面优化:1. 减少序列化(减少RPC调用,一种可行的方案是将多个关联性较强的应用进行 “合并部署”,从而减少不同应用之间的 RPC 调用(微服务设计规范))2. 直接输出流数据(只要涉及字符串的I/O操作,无论是磁盘 I/O 还是网络 I/O,都比较耗费 CPU 资源,因为字符需要转换成字节,而这个转换又必须查表编码。所以对于常用数据,比如静态字符串,推荐提前编码成字节并缓存,具体到代码层面就是通过 OutputStream() 类函数从而减少数据的编码转换;另外,热点方法toString()不要直接调用ReflectionToString实现,推荐直接硬编码,并且只打印DO的基础要素和核心要素– 这整段不是很懂,toString懂啊哈哈)3. 裁剪日志异常堆栈,超大流量下频繁地输出完整堆栈,会加剧系统当前负载(可以通过日志配置文件控制异常堆栈输出的深度)4. 去组件框架:极致优化要求下,可以去掉一些组件框架,比如去掉传统的 MVC 框架,直接使用 Servlet 处理请求。这样可以绕过一大堆复杂且用处不大的处理逻辑,节省毫秒级的时间,当然,需要合理评估你对框架的依赖程度
一致性:秒杀场景下的一致性问题,主要是库存扣减的准确性问题
减库存的方式:
下单减库存(用户体验好,但存在恶意下单不付款的问题)
付款减库存(用户体验差,很多人下单成功但有人不能付款)
预扣库存:缓解了以上两种方式的问题。预扣库存实际就是“下单减库存”和 “付款减库存”两种方式的结合,将两次操作进行了前后关联,下单时预扣库存,付款时释放库存。
劣势:并没有彻底解决以上问题。比如针对恶意下单的场景,虽然可以把有效付款时间设置为 10 分钟,但恶意买家完全可以在 10 分钟之后再次下单。
实际业界也多用这种方式,下单后一般都有个 “有效付款时间”,超过该时间订单自动释放,是典型的预扣库存方案。
恶意下单的解决方案主要还是结合安全和反作弊措施来制止。比如,识别频繁下单不付款的买家并进行打标,这样可以在打标买家下单时不减库存;再比如为大促商品设置单人最大购买件数,一人最多只能买 N 件商品;又或者对重复下单不付款的行为进行次数限制阻断等
超卖分为两种:1. 商家可以补货的,允许超卖;2. 不允许超卖,限定库存字段不能为负数:1)一是在通过事务来判断,即保证减后库存不能为负,否则就回滚;2)直接设置数据库字段类型为无符号整数,这样一旦库存为负就会在执行 SQL 时报错
一致性性能的优化
高并发读:“分层校验”。即在读链路时,不做一致性校验,只做不影响性能的检查(如用户是否具有秒杀资格、商品状态是否正常、用户答题是否正确、秒杀是否已经结束、是否非法请求等),在写链路的时候,才对库存做一致性检查,在数据层保证最终准确性。
不同层次尽可能过滤掉无效请求,只在“漏斗” 最末端进行有效处理,从而缩短系统瓶颈的影响路径。
高并发写
更换DB选型:换用缓存系统(带有持久化功能的缓存,如Redis,适合减库存操作逻辑单一的,无事务要求的)
优化DB性能:库存数据落地到数据库实现其实是一行存储(MySQL),因此会有大量线程来竞争 InnoDB 行锁。但并发越高,等待线程就会越多,TPS 下降,RT 上升,吞吐量会受到严重影响。
两种方法:
- 应用层排队。加入分布式锁,控制集群对同一行记录进行操作的并发度,也能控制单个商品占用数据库连接的数量
- 数据层排队。(应用层排队是有损性能的,数据层排队是最为理想的。)业界中,阿里的数据库团队开发了针对InnoDB 层上的补丁程序(patch),可以基于DB层对单行记录做并发排队,从而实现秒杀场景下的定制优化。另外阿里的数据库团队还做了很多其他方面的优化,如 COMMIT_ON_SUCCESS 和 ROLLBACK_ON_FAIL 的补丁程序,通过在 SQL 里加入提示(hint),实现事务不需要等待实时提交,而是在数据执行完最后一条 SQL 后,直接根据 TARGET_AFFECT_ROW 的结果进行提交或回滚,减少网络等待的时间(毫秒级)。目前阿里已将包含这些补丁程序的 MySQL 开源:AliSQL。
高可用
流量削峰
- 答题:通过提升购买的复杂度,达到两个目的:防止作弊&延缓请求。本质是通过在入口层削减流量,从而让系统更好地支撑瞬时峰值。
- 排队:最为常见消息队列,通过把同步的直接调用转换成异步的间接推送,缓冲瞬时流量。(弊端:请求积压、用户体验)(排队本质是在业务层将一步操作转变成两步操作,从而起到缓冲的作用,但鉴于此种方式的弊端,最终还是要基于业务量级和秒杀场景做出妥协和平衡。)
- 过滤:过滤的核心目的是通过减少无效请求的数据IO 保障有效请求的IO性能。
- 读限流:对读请求做限流保护,将超出系统承载能力的请求过滤掉
- 读缓存:对读请求做数据缓存,将重复的请求过滤掉
- 写限流:对写请求做限流保护,将超出系统承载能力的请求过滤掉
- 写校验:对写请求做一致性校验,只保留最终的有效数据
Plan B
架构阶段:考虑系统的可扩展性和容错性,避免出现单点问题。例如多地单元化部署,即使某个IDC甚至地市出现故障,仍不会影响系统运转
编码阶段:保证代码的健壮性,例如RPC调用时,设置合理的超时退出机制,防止被其他系统拖垮,同时也要对无法预料的返回错误进行默认的处理
测试阶段:保证CI的覆盖度以及Sonar的容错率,对基础质量进行二次校验,并定期产出整体质量的趋势报告
发布阶段:系统部署最容易暴露错误,因此要有前置的checklist模版、中置的上下游周知机制以及后置的回滚机制
运行阶段:系统多数时间处于运行态,最重要的是运行时的实时监控,及时发现问题、准确报警并能提供详细数据,以便排查问题
故障发生:首要目标是及时止损,防止影响面扩大,然后定位原因、解决问题,最后恢复服务
预防:建立常态压测体系,定期对服务进行单点压测以及全链路压测,摸排水位
管控:做好线上运行的降级、限流和熔断保护。需要注意的是,无论是限流、降级还是熔断,对业务都是有损的,所以在进行操作前,一定要和上下游业务确认好再进行。就拿限流来说,哪些业务可以限、什么情况下限、限流时间多长、什么情况下进行恢复,都要和业务方反复确认
监控:建立性能基线,记录性能的变化趋势;建立报警体系,发现问题及时预警
恢复:遇到故障能够及时止损,并提供快速的数据订正工具,不一定要好,但一定要有
在系统建设的整个生命周期中,每个环节中都可能犯错,甚至有些环节犯的错,后面是无法弥补的或者成本极高的。所以高可用是一个系统工程,必须放到整个生命周期中进行全面考虑。同时,考虑到服务的增长性,高可用更需要长期规划并进行体系化建设。
监控
集群监控的时候,重点需要关注哪些技术指标?这些指标如何优化?
- 系统指标:cpu使用率、内存使用率、机器连通性、系统负载(cpu.load)
- 网络指标:网络流入速度、网络流出速度、网络流入包数、网络流出包数、TCP连接数、TCP Active Opens(主动打开数)、IP接收包数、IP丢包数、TCP接收包数、TCP发送包数、TCP包传输错误数、TCP重传数
- 磁盘指标:磁盘使用率、iNode使用率、磁盘繁忙占比、磁盘读速度、磁盘写速度、磁盘读次数、磁盘写次数
- 容器指标?:线程数[容器指标]/平均到每核、进程数[容器指标]/平均到每核….
[小米监控](
面向对象
简单说一下面向对象的特征以及六大原则
特征:
封装:把客观事物封装成抽象的类
抽象继承!:(实现继承or接口继承)让某个类型的对象获得另一个类型的对象的属性和方法多态:一个类实例的相同方法在不同情形有不同的表现形式。(编译多态与运行时多态)一般指运行时多态..?
多态存在的必要条件:继承、重写、父类引用指向子类对象
原则:
单一职责:一个类的功能要单一,不能包罗万象
开放封闭:一个模块,在扩展性方面应该是开发的,在更改性方面是封闭的
里氏替换:子类应当可以替换父类,并出现在父类能够出现的任何地方
依赖倒置:高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象;抽象不应该依赖于具体实现,具体实现应该依赖于抽象。
接口分离:模块间要通过抽象接口隔离开,而不是通过具体的类强耦合起来,即面向接口编程。
迪米特原则:一个类对自己依赖的类知道的越少越好。类间解耦,低耦合、高内聚。
亿级数据
从千万的数据到亿级的数据,会面临哪些技术挑战?你的技术解决思路?
https://zhuanlan.zhihu.com/p/51081227
最常见的数据库,如MySql、Oracle等,都采用行式存储,比较适合OLTP。如果用MySql等行数据库来实现OLAP,一般都会碰到两个瓶颈:
- 数据量瓶颈:mysql比较适合的数据量级是百万级,再多的话,查询和写入性能会明显下降。因此,一般会采用分库分表的方式,把数据规模控制在百万级。
- 查询效率瓶颈:mysql对于常用的条件查询,需要单独建立索引或组合索引。非索引字段的查询需要扫描全表,性能下降明显。
分表
垂直业务拆分=分库+微服务(可分库基础上再分表)
https://zhuanlan.zhihu.com/p/54594681
1亿个手机号码,判断重复
不允许有误差的:
- hash到n个小文件中
- 每个文件做统计
- 个数大于1的是重复的
允许有误差的:
布隆过滤器
算法
海量数据
5亿整数的大文件,怎么排?
内存估算
假设每个数最多64位,8字节
5,0000,0000x8 ~ 500MBx8 = 4000MB ~ 4G
假设5亿数不重复
内存装的下:
直接快排,得算好久吧..
内存装不下:
- 读文件,数取模%4000存入4000个小文件,每个文件约1M
- 读每个小文件,快排
- 多路归并排序输出
分治/hash映射 + hash统计 + 堆/快排/归并排序
- hash分成n个小文件,满足内存要求:好处是,可以让相同的数或字符串进入同一个小文件
- 小文件排序或统计,或没有本步骤,输出另一份小文件
- 最终要求
- 全排序:使用多路归并
- 找top k:直接小顶堆(找最大top k)or大顶堆;或者每个小文件先找top k,再对比n个top k
- 找两文件不同:两两小文件set对比
数据结构
- bitmap 可用于整数去重等
- trie树 名字来源retrieval
排序
常见的排序算法
冒泡排序-复杂度O(n^2)-交换排序
对所有相邻记录的关键码值进行比较,如果是逆序(L.r[1].key > L.r[2].key),则将其交换,最终达到有序化。
对无序区从前向后依次将相邻记录的关键码进行比较,若逆序,则将其交换,从而使得关键码值小的记录向上“飘浮”(左移),关键码值大的记录向下“坠落”(右移)。
每经过一趟冒泡排序,都使无序区中关键码值最大的记录进入有序区,对于由n个记录组成的记录序列,最多经过n-1趟冒泡排序,就可将这n个记录重新按关键码顺序排列。可看出,若“在一趟排序过程中没有进行过交换记录的操作”,则可结束整个排序过程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21/**
* 冒泡排序--更像坠落排序
*
* @param nums
*/
public void sort(T[] nums) {
int len = nums.length;
boolean isSorted = false;
// i区分无序区和有序区
for (int i = len - 1; i >= 0 && !isSorted; i--) {
isSorted = true;
// j将大元素右移
for (int j = 0; j < i; i++) {
if (less(nums[j + 1], nums[j])) {
isSorted = false;
swap(nums, j, j + 1);
}
}
}
}
选择排序-复杂度O(n^2)-选择排序
每一趟从待排序的记录中选出关键码最小的记录,顺序放在已排好序的子序列后面,直到全部记录排序完毕。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/**
* 选择排序
*
* @param nums
*/
public void sort(T[] nums) {
for (int i = 0; i < nums.length; i++) {
// index指向每轮最小的数
int index = i;
for (int j = i + 1; j < nums.length; j++) {
if (less(nums[j], nums[index])) {
index = j;
}
}
swap(nums, i, index);
}
}
插入排序-复杂度O(n^2) -插入排序
基本思想是,将待排序的记录,按其关键码的大小插入到已经排好序的有序子表中,直到全部记录插入完成为止。
1
2
3
4
5
6
7
8
9
10
11
12
13/**
* 插入排序
*
* @param nums
*/
public void sort(T[] nums) {
for (int i = 1; i < nums.length; i++) {
for (int j = i; j > 0 && less(nums[j], nums[j - 1]); j--) {
swap(nums, j, j - 1);
}
}
}
归并排序-复杂度O(nlogn)-归并排序
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/**
* 归并排序
*
* @param nums
*/
public void sort(T[] nums, Class<T> clazz) {
T[] copy = (T[]) Array.newInstance(clazz, nums.length);
System.arraycopy(nums, 0, copy, 0, nums.length);
sort(nums, copy, 0, nums.length);
}
private void sort(T[] nums, T[] copy, int begin, int end) {
if (begin + 1 == end) {
return;
}
int half = (end - begin) / 2;
sort(nums, copy, begin, begin + half);
sort(nums, copy, begin + half, end);
merge(copy, nums, begin, begin + half, end);
}
private void merge(T[] nums, T[] copy, int begin, int mid, int end) {
int i = begin, j = mid, k = begin;
while (i < mid && j < end) {
if (nums[i].compareTo(nums[j]) < 0) {
copy[k] = nums[i++];
} else {
copy[k] = nums[j++];
}
k++;
}
while (i < mid) {
copy[k++] = nums[i++];
}
while (j < end) {
copy[k++] = nums[j++];
}
}
快速排序-复杂度O(nlogn)-交换排序
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/**
* 快速排序
*
* @param nums
*/
public void sort(T[] nums) {
sort(nums, 0, nums.length - 1);
}
private void sort(T[] nums, int begin, int end) {
int left = begin + 1, right = end;
while (left < right) {
while (left <= end && less(nums[left], nums[begin])) {
left++;
}
while (right >= begin && less(nums[begin], nums[right])) {
right--;
}
if (left < right) {
swap(nums, left, right);
}
}
if (right <= end && right >= begin) {
swap(nums, begin, right);
sort(nums, begin, right - 1);
sort(nums, right + 1, end);
}
}
堆排序-复杂度O(nlogn)-堆排序
位置 k 的节点的父节点位置 为 k/2,而它的两个子节点的位置分别为 2k 和 2k+1。
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/**
* 堆排序 排成最大堆
* 数组第 0 个位置不能有元素
*
* @param nums
*/
public void sort(T[] nums) {
int cnt = nums.length - 1;
for (int k = cnt / 2; k >= 1; k--) {
sink(nums, k, cnt);
}
while (cnt > 1) {
swap(nums, 1, cnt);
cnt--;
sink(nums, 1, cnt);
}
}
/**
* 下沉
*
* @param nums
* @param k
*/
private void sink(T[] nums, int k, int len) {
while (k * 2 <= len) {
int child = k * 2;
// 判断child + 1未越界
if (child + 1 < len && less(nums[child], nums[child + 1])) {
child++;
}
// 如果子节点比k小,退出循环
if (less(nums[child], nums[k])) {
break;
}
swap(nums, k, child);
k = child;
}
}
https://book.open-falcon.org/zh_0_2/intro/)
其他
最近两年遇到的最大的挫折,从挫折中学到了什么?
胃炎?失眠?
生病要看病,不要自己琢磨;看病也是要厚脸皮的,多问;运动,保持良好生活习惯。
– 这么回答..太坑了吧
最近有没有学习过新技术?
zookeeper
kafka
json:Gson/jackson/fastjson
设计模式:尽量在工作中合适地应用
docker简单使用