面试汇总

自我介绍

JVM

您好,我叫朱会会,来自华东师范大学数据科学与工程学院,现在是一名研二的学生。目前在京东实习,负责组内自研的内容流量平台的维护与研发。

研二作为主力开发学院的在线网课平台-水杉在线,除了负责后端开发之外,还负责整个开发平台的CICD能力和习题推荐系统的搭建,目前水杉在线的用户有接近三千多名本科生和研究生用户,前段时间也是有幸参与了世界人工智能大会的展览。

非常荣幸有机会参加今天的面试。以上就是我的自我介绍,谢谢。

实习项目

项目基于组内自研的内容流量平台-cfc,用于流量统计、链路追踪、降级限流等等功能,我开发的是一个智能兜底的需求,主要是用来兜底一些异常情况,具体是这样的,系统接入cfc后,可以在cfc的管理页面来设置开启使用手动兜底还是智能兜底,手动兜底的功能需要开发者自己把握,因为有些异常可能是业务的异常,不需要进行处理和兜底,这时候填写一个无序兜底的异常白名单,在抛出异常进入兜底窗口的时候,会跳过这些异常,不进行处理。还需要手动填写一个json的兜底方案。智能兜底是这样的,我们在单例的拦截器中维护了一个concurrenthashmap,map以traceName为key,存储了上一次的正常返回结果和时间戳,每半个小时更新一次。这样当用户进入异常处理窗口的时候,就直接从内存中取出数据返回。大概就是这样。(这里可能会问到单例模式和hashmap)。

拦截器

SpringMVC中的interceptor拦截请求主要是通过HandlerInterceptor来实现的,定义拦截器的方式有两种,一是实现或继承了HandlerInterceptor,二是实现或继承WebRequestInterceptor接口。

  • preHandle:返回boolean类型,返回false请求结束,后续的Interceptor和controller不会执行;返回true会继续调用下一个Interceptor的prehandle方法,是最后一个Interceptor的时候调用controller方法。
  • postHandle:controller之后执行,在DispatcherServlet进行视图返回渲染之前被调用,所以可以在这个方法中对controller处理之后的modelAndView对象进行操作。
  • 在afterCompletion:在整个请求结束之后,在DispatcherServlet渲染了对用的视图之后执行。

当preHandle时true的时候,postHandle才会执行,当为false的时候,afterCompletion仍然会执行。

对于过滤器,逻辑需要复写invoke方法,所以就一个invoke方法。

单例模式

在有些系统中,为了节省内存资源、保证数据内容的一致性,对某些类要求只能创建一个实例,这就是所谓的单例模式。

单例模式的使用场景

  • 有频繁实例化然后销毁的情况,也就是频繁的 new 对象,可以考虑单例模式;
  • 创建对象时耗时过多或者耗资源过多,但又经常用到的对象;
  • 频繁访问 IO 资源的对象,例如数据库连接池或访问本地文件;

在 Spring 中的 bean 默认就是单例的,拦截器也是单例的。

懒汉式

优点:简单,只有第一次获取才生成实例,不浪费内存

缺点:线程不安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton {  
// 静态私有变量,确保能够在静态方法中访问到该类变量
private static Singleton instance;
// 将构造函数私有化,不允许外部调用构造函数创建对象实例
private Singleton (){}

// 获取单例是静态的方法,因为不能通过构造函数创建实例
public static Singleton getInstance() {
// 第一次调用时才实例化
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

线程安全的懒汉式

优点:线程安全且第一次调用才生成初始化,节约内存

缺点:加锁影响效率

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {  
private static Singleton instance;
private Singleton (){}

// 为了防止线程不安全,加上synchronized锁,锁的是this.class对象
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

饿汉式

优点:不加锁,获取单例时效率高

缺点:类加载就初始化,浪费内存

1
2
3
4
5
6
7
8
public class Singleton {  
private static Singleton instance = new Singleton();
private Singleton (){}

public static Singleton getInstance() {
return instance;
}
}

双检锁/双重校验锁(DCL,即 double-checked locking)

优点:在多线程环境下能够保存高性能

缺点:实现较为复杂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Singleton { 
// volatile关键字防止指令重排导致的问题
private volatile static Singleton singleton;
private Singleton (){}

public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
// 线程1执行到这,由于指令重排可能导致先将引用(分配内存)给实例,再调用构造方法,
// 如果线程2在调用构造方法之前调用getInstance(),那么此时INSTANCE不为null此时
// 线程2拿到的是没有执行初始化的实例
singleton = new Singleton();
}
}
}
return singleton;
}
}

登记式/静态内部类

优点:只有第一次调用getInstance()后,实例才会创建,而不是只要类一加载就创建,节省内存,同时类加载时JVM会保证线程安全

1
2
3
4
5
6
7
8
9
10
public class Singleton {  
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}

public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

水杉在线

技术选型

水杉在线其实是一个多个子系统组成的平台,包括水杉学堂-类似于mooc形式,还有定制化的类似于gitlab的仓库,和OJ平台,我们做的的工作主要是门户和水杉学堂的开发,并且实现了多个平台登录的打通。

后端使用的是springcloud,前端是vue框架,所有文件都放在对象存储,通过Url引用访问。(估计会问到数据库,引申出和mysql相关的问题)。

CICD

为什么使用CICD

其实我们最开始使用CICD的目的很简单,就是因为不断的打包发布太麻烦了,我们最初开发的时候其实是比较傻瓜式的,初期前端开发人员使用mock模拟数据和后端进度解耦,后端分成多个微服务同时开发,待后端开发者开发完成后在打包发布,进行前后端联调。因为不断的打包发布太麻烦,所以就去探索了一下CICD的相关实现,最终使用gitlab+docker compose实现了CICD的能力。

其实后来我们通过实践发现CICD有更多的好处,比如:

  • 过程比较透明

    通过观察节点日志,服务失败,可以很方便看到哪一环节除了问题,方便开发人员更快的定位和解决问题。

  • 方便自测隔离

    我们现在的做法是,用户都往master分支push代码,当需要发布的时候,将master分支合并到prod分支触发CICD流程,实现新版本的发布。但是后来工具发现,可以以分支为单位,每个分支按自测需要新建一个CICD流程,这样就避免了代码污染的问题,每个开发者在自测的时候,不会影响到线上的环境或者别人的环境。

CICD实践

借助gitlab和docker-compose来实现,这里需要为每个服务编写.gitlab-ci.yaml配置文件来定义流水线阶段分级与每个节点的具体逻辑的文件。还有dockerfile,用来构建镜像的文件,文本包含了一条条构建镜像需要的指令和说明。另外还需要一台runner机器,用来执行集成脚本的机器。我们也使用了阿里云的镜像服务

CICD

习题推荐系统

我们希望赋予他更多的AI元素,例如在录入习题的时候,我们需要给习题添加标签,在前期我们更多的是实验室一起手动打标签,后来注意到百度的easydl有这种多标签分类,我们就用我们的数据在easydl平台训练了一个多标签分类的模型,再有新的数据进来的时候,可以进行自动的打标签。

习题推荐是因为我们平台一开始设计就有考试这块的业务,我们也提供了练习的功能,一开始的时候我们是这样设计的,建立Lucene索引,用户提供标签信息,我们去做匹配,当然也会做记录,例如这次抽到了哪些题目,还有用户的正确率这些信息,后来有了这些数据之后,我们就考虑来做推荐了,因为我们前期收集的数据还算标准,大概就是用户针对某个标签的正确率,这其实和商品的评分是一致的,可以拿来做推荐,这个目前我们更多的是在算法层面的研究。

开源项目推荐

用户 90W

项目 450W

交互数量1200W

map

hashmap

数据结构

注意:拉链法是头插法。

1
2
3
4
5
6
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
}

put操作

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
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 键为 null 单独处理
if (key == null)
return putForNullKey(value);
int hash = hash(key);
// 确定桶下标
int i = indexFor(hash, table.length);
// 先找出是否已经存在键为 key 的键值对,如果存在的话就更新这个键值对的值为 value
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}

modCount++;
// 插入新键值对
addEntry(hash, key, value, i);
return null;
}

对于7行的key是null的情况,是允许的,但是null无法调用hashcode()方法,所以使用第0个下标来存储key是null的键值对。

$therehold = capacity*loadFactor$

hash阶段

  • 由源码可知,hash的计算函数是(h = key.hashCode()) ^ (h >>> 16),为什么有异或操作?

    首先hash的值是一个32位的二进制,在得到hash值之后,需要执行(n-1) & hash操作,当n很小的时候,前位都是0,而0与0或者1执行&运算都是0,如果对于两个hash高位不同而低位相同,则很容易碰撞,所以需要执行一次异或操作,将前边的信息保留在后位。

  • 为什么capcity是2的幂

    因为最后计算桶位的时候,公式是(n-1) & hash,如果n是2的幂,保证n-1是全为1的二进制数,如果不全是1,存在某一位为0,那么0和01的与运算都是0,增加碰撞的可能,本质上和上一个问题都是为了减少hash碰撞。

  • 为什么是与运算而不是模

    • 位运算效率高
    • 避免hashcode为负数的情况。

put操作

  • table是null或者table.length()是0,则resize
  • 计算桶位index,table[i]==null,新建节点添加。添加成功后,判断实际存在的键值对个数是不是超过therehold,超过则resize
  • 如果table[i]不是null,则判断table[i]的首个元素是否和key一样,如果相同则直接覆盖value(相同指hashcode和equals),否则判断table[i]是否为treenode,如果是红黑树,则在树中插入键值对,如果不是,则遍历table[i],判断链表长度是否大于8,大于8则树化,在树中插入操作,否则进行链表的插入操作。遍历过程中若发现key存在则直接覆盖value

扩容操作

用在两处:

  • 因为hashmap是懒加载,在第一次put的时候才会初始化,所以在第一次put进行resize初始化,16或者是传入的参数值。
  • put操作之后,需要检查size是不是已经超过therehold,超过则resize。若此时capacity已经大于了最大值,则把therehold置为Int的最大值,否则对capacity和therehold进行扩容操作。

扩容之后需要元素需要重新散列,但是不需要重新计算所有的哈希,只需要看看原来的哈希值新增的bit是1还是0就行,为什么呢?

因为n右移了一位,最后在计算桶位的时候,得到的hash值肯定也多了一位,如果新增的一个bit是0,和1与运算还是0,不需要变动。如果新增的bit位变成了1,原来是00101是5,现在是100101是21=5+16。

ConcurrentHashMap

实现和hashmap类似,但是引入分段锁(segment),每个锁维护几个桶(HashEntry),多个线程可以同时访问不同分段锁上的桶,并发度就是segment的个数,默认是16.

jdk1.7采用分段锁机制,核心类是segment,继承重入锁ReentrantLock。而jdk8使用CAS(乐观锁)来支持更高的并发度,在CAS操作失败的时候使用内置锁synchronized。

乐观锁?

全称是比较并交换(Compare-and-Swap),CAS指令需要有3个操作数,分别是内存地址V,旧的预期值A和新的值B。执行操作时,只有当V的值等于A,才将V的值更新为B。

LinkedHashMap

继承自hashmap,内部维护了一个双向链表,用来维护插入顺序或者LRU(最近最少使用)顺序,accessOrder决定了顺序,默认是false,此时维护的是插入顺序,如果是true,维护的是查找顺序。

最重要的是两个用于维护顺序的函数:

  • afterNodeAccess

    当第一个节点被访问时,如果accessOrder是true,则将该节点移到链表尾部。也就是说在指定为LRU顺序之后,在每次访问一个节点后,都将节点移到链表尾部,保证链表尾部是最近访问的节点,首部就是最近最久未使用的节点。

  • afterNodeInsertion

    1
    2
    3
    4
    5
    6
    7
    void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
    K key = first.key;
    removeNode(hash(key), key, null, false, true);
    }
    }

    在put操作执行之后,当removeEldestEntry()方法返回true时会移除最晚的节点,也及时链表首部节点first。removeEldestEntry默认是false,需要为true的时候需要继承LinkedHashMap并且覆盖这个方法,这再是实现LRU缓存的时候很有效,通过移除最近未使用的节点,保证缓存空间够用。

  • 使用LinkedHashMap实现LRU缓存

    • 继承LinkedhashMap
    • 设定最大缓存空间 MAX_ENTRIES
    • 使用LinkedHashMap的构造函数设置accessOrder为true,开启LRU缓存。
    • 覆盖 removeEldestEntry() 方法实现,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    static class LRU<K,V> extends LinkedHashMap<K,V>{
    private static final int MAX_ENTRY = 3;
    protected boolean removeEldestEntry(Map.Entry eldest) {
    return size() > MAX_ENTRY;
    }
    LRU(){
    super(MAX_ENTRY,0.75f,true);
    }
    }
    1
    2
    3
    4
    5
    6
    7
    LRU<Integer,String> test = new LRU();
    test.put(1,"1");
    test.put(2, "2");
    test.put(3, "3");
    test.get(1);
    test.put(4, "4");
    System.out.println(test.keySet());

    最后输出:[3,1,4]

WeakhashMap

1
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>

entry继承自弱引用,下一次GC的时候会被回收,主要用来实现缓存。

tomcat中的ConcurrentCache使用W来实现缓存,采用的是分代缓存。

  • 经常使用的对象放在eden中,使用ConcurenthashMap实现,不担心被回收。
  • 不常使用的对象放在longterm中,使用W实现,下一次GC会被回收。
  • get时,先从eden获取,没找到再到longterm获取,获取到放入eden。
  • put时,如果eden的大小超过size,将eden中的对象放入longtrem中,利用JVM进行回收。

Mysql

1、存储引擎

Mysql默认的存储引擎是InnoDB,在5.7版本只有InnoDB是支持事务的。

MyISAM和InnoDB区别?

在5.5之前,默认引擎是MyISAM 。优点有:全文索引,压缩,空间函数,但是不支持事务和行级锁,最大的缺陷是崩溃之后无法安全恢复,在5.5之后,引入InnoDB并成为默认引擎。

两者的对比

MyISAM InnoDB
行级锁 表级锁 表级锁和行级锁(默认)
事务和崩溃后安全恢复 不支持 支持,同时具有回滚,崩溃修复能力
外键 不支持 支持
MVCC 不支持 支持

MVCC多版本控制,比单纯的锁更加高效,只在Read CommittedRepetable read两个隔离级别工作,可以使用乐观锁和悲观锁来实现。

MyISAM更注重性能。但不是首选。

2、字符集与校对规则

字符集指的是一种从而二进制编码到某类字符符号的映射。校对规则是指某种字符集下的排序规则。MySql中每一种字符集都会对应一系列的校对规则。

M采用的是类似继承的方式制定字符集的默认值,每个数据库和每张数据表都有自己的默认值,他们逐层继承。比如:某个库中所有表的默认字符集将是该数据库所指定的字符集(这些表在没有指定字符集的情况下,才会采用摩恩字符集)。

3、索引

参考

索引的数据结构主要有:B树和哈希索引。哈希索引的底层就是哈希表,因为在绝大多数需求为单条记录查询的时候,可以选择哈希索引,其他情况下选择B树索引。

B树使用的是B+树。两种存储引擎的实现方式不同:

  • MyIASM

    叶节点的data区域存放是是数据记录的地址。在索引检索的时候,按照B树算法索引,指定的Key存在,则取出data域的值,然后以data域的值为地址读取对应的数据记录。这被称为“非聚簇索引”。

  • InnoDB

    数据文件本身就是索引文件。树的叶节点data域保存了完整的数据记录,因此InnoDB表数据文件本身就是主索引,这称为“聚簇索引”。其余的索引作为辅助索引,辅助索引的data记录的是相应记录主键的值而不是地址。

    在根据主索引搜索时,直接找到Key所在的节点;在根据辅助索引查找时,先取出主键的值,再走一遍主索引。因此在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,会造成主索引频繁分裂。

4、查询缓存

8.0版本移除,不实用。

缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。 因此,开启查询缓存要谨慎,尤其对于写密集的应用来说更是如此。如果开启,要注意合理控制缓存空间大小,一般来说其大小设置为几十MB比较合适。

5、事务

事务的四大特性

  • 原子性:事务是自小的执行单位。原子性确保动作要么全部完成,要么全部不执行。
  • 一致性:数据库从一个正确状态变化到另一个一致性状态。
  • 隔离性:并发访问数据库,两个用户的事务不干扰。并发事务之间的数据库是独立的。
  • 持久性:对数据库的改变是持久的,即使数据库发生故障也不应该有影响。

并发事务带来的问题

  • 脏读

    一个事务修改数据库,还没提交。另一个事务读数据,因为这个数据还没提交,所以读出的是脏数据。对脏数据的操作可能不正确。

  • 丢失修改

    两个事务读一个数据,第一个事务修改之后第二个事务也修改,这样第一个事务修改的结果就丢失了。保留第二次修改的结果。

  • 不可重复读

    一个事务内多次读用一个数据,在两次读数据之间,由于第二个事务修改数据,导致两次读的数据不同,称为不可重复读。

  • 幻读

    它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

不可重复读的重点是多次读取发现某些列被修改。

幻读重点是多次读取发现记录增多后者减少。

数据库的隔离级别:

  • 读未提交

    允许读取尚未提交的数据变更,可能导致脏读幻读或者不可重复读。

  • 读已提交

    允许读取并发事务已经提交的数据。避免脏读,但是幻读和不可重复读可能发生。

  • 可重复读

    对同一字段的多次读取结果都是一致的,除非被本身事务自己修改。

  • 可串行化

    最高的更隔离级别。服务ACID,所以的事务逐个执行,这样事务之间不产生干扰

InnoDB默认是可重复读。使用的是next-key lock算法,避免幻读,已经达到了可串行化的隔离级别。隔离级别越低,锁越少,所以大部分数据库的隔离级别都是读已提交,但是InnoDB使用可重复读,并且不会有性能损失。InnoDB在分布式事务的情况下使用可串行化的隔离级别。

锁机制与InnoDB锁算法

  • 表级锁:粒度最大的锁,整张表加锁。实现简单,资源消耗少,加锁快,不会出现死锁。出发锁冲突的概率高,并发度最低。
  • 行级锁:粒度最小,当前操作行加锁。大大减少数据库操作的冲突,并发度高。开销大,加锁慢,会出现死锁。

InnoDB存储引擎的算法有三种:

  • Reacord lock:单行记录锁
  • Gap lock:间隙锁,锁定一个范围,不包括记录本身。
  • Next-key lock:record+gap,包含记录本身。

补充:

  1. innodb对于行的查询使用next-key lock
  2. Next-locking keying为了解决Phantom Problem幻读问题
  3. 当查询的索引含有唯一属性时,将next-key lock降级为record key
  4. Gap锁设计的目的是为了阻止多个事务将记录插入到同一范围内,而这会导致幻读问题的产生
  5. 有两种方式显式关闭gap锁:(除了外键约束和唯一性检查外,其余情况仅使用record lock) A. 将事务隔离级别设置为RC B. 将参数innodb_locks_unsafe_for_binlog设置为1

6、大表优化

MYSQL表单过大,数据库的CRUD性能下降,措施如下:

  • 限定数据范围

    禁止不带任何限制数据范围条件范围的查询语句。

  • 读写分离

    主库写,从库读。

  • 垂直分区

    数据表的相关性进行拆分。把一张列比较多的表拆分成多张表。

    • 优点:列数据变小,在查询时减少读取的block数,减少IO次数。简化表结构,易于维护。
    • 缺点:主键冗余,需要管理冗余列,引起join操作,事务变得复杂。
  • 水平分区

    保持数据表结构不变,通过策略存储数据分片,这样每一片数据分散到不同的表或者库中,达到分布式的目的。

    水平拆分支持大的数据量。但是分表仅仅解决了单一表数据量大的问题,但是表数据还在同一台机器上,其实对于并发能力没有太多提升。所以水平拆分最好分库。

    分片支持大的数据量存储,但是分片事务难以解决,逻辑复杂,尽量不分片,非要分片,尽量选择客户端分片,可以减少一次和中间件的网络IO

    数据库分片的两种常见方案:

    • 客户端代理:分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当网的 Sharding-JDBC (推荐) 、阿里的TDDL是两种比较常用的实现。
    • 中间件代理:在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。 我们现在谈的 Mycat 、360的Atlas、网易的DDB等等都是这种架构的实现。

分库分表之后,ID主键如何处理:

  • UUID:太长无序不可读,查询效率低,比较适合用于生成名字唯一的标识比如文件的名字。
  • 自增ID:两台数据库分别设置不同步长,生成不重复ID的策略来实现高可用。这种方式生成的 id 有序,但是需要独立部署数据库实例,成本高,还会有性能瓶颈。
  • redis:性能好,灵活方便,不依赖数据库。
  • 雪花算法

7、池化

这种设计会初始预设资源,解决的问题就是抵消每次获取资源的消耗,如创建线程的开销,获取远程连接的开销等。除了初始化资源,池化设计还包括如下这些特征:池子的初始值、池子的活跃值、池子的最大值等,这些特征可以直接映射到java线程池和数据库连接池的成员属性中。这篇文章对池化设计思想介绍的还不错。

数据库连接本质就是一个 socket 的连接。数据库服务端还要维护一些缓存和用户权限信息之类的 所以占用了一些内存。我们可以把数据库连接池是看做是维护的数据库连接的缓存,以便将来需要对数据库的请求时可以重用这些连接。为每个用户打开和维护数据库连接,尤其是对动态数据库驱动的网站应用程序的请求,既昂贵又浪费资源。在连接池中,创建连接后,将其放置在池中,并再次使用它,因此不必建立新的连接。如果使用了所有连接,则会建立一个新连接并将其添加到池中。 连接池还减少了用户必须等待建立与数据库的连接的时间。

8、常见问题:

行存储与列存储

摘自

主流的(OLTP)数据库大多数采用行存储,随着分析性数据库(OLAP)数据库的兴起,列存储又变的流行。

列存储优势一方面体现在存储上节约空间,减少IO。另一方面依靠列式数据结构做了计算上的优化。

什么是列式存储

img

传统OLTP数据库通常采用行存储,所有的列依次排列成一行,以行为单位存储,再配合B+树或者SS-Table作为索引,就能快速通过主键找到相应的行数据。

行存储对于OLTP场景很自然:大多数操作都是以实体为单位,把一行数据存储在相邻的位置是个很好的选择。

对于OLAP场景来说,一个典型的查询需要遍历整张表,进行分组、排序、聚合等操作,这样一来行存储就没有优势了。更糟糕的是,分新型SQL通常不会用到所有的列,仅仅对某些感兴趣的列运算,一行中的无关列也不得不参与扫描。

列存储就是为这样的需求设计的。如下图所示,同一列的数据被一个一个紧挨着放在一起,表的每列构成一个长数组。

img

列存储对于OLTP的场景不友好,一行数据的写入需要同时修改多个列。但是对于OLAP数据库有着很大的优势。

  • 当查询语句只设计到部分列时,只需要扫描相关的列。
  • 每一列的数据都是相同类型,彼此间的相关性更大,对列数据存储的压缩效率高。

Bigtable(HBase)是列存储吗?

其实不是列存储,是按照key-value pair存储数据,和列存储无关系。

但是BT有列簇概念。列簇可以指定给某个locality group,决定改列簇数据的物理位置,从而让同一主键的各个列簇分别存放在最优的物理节点上,

由于 column family 内的数据通常具有相似性,对它做压缩要比对整个表压缩效果更好。

列式数据库可以是关系型、也可以是 NoSQL,这和是否是列式并无关系。

DSM 分页模式

我们知道,由于机械磁盘受限于磁头寻址过程,读写通常都以一块(block)为单位,故在操作系统中被抽象为块设备,与流设备相对。这能帮助上层应用是更好地管理储存空间、增加读写效率等。这一特性直接影响了数据库储存格式的设计:数据库的 Page 对应一个或几个物理扇区,让数据库的 Page 和扇区对齐,提升读写效率。

大多数服务于在线查询的DBMS采用NSM即安行存储的方式,将完整的行从header开依次存放。页的最后有一个索引,存放了页内各行的起始偏移量。由于每行的长度不一定固定,索引可以帮我们快速找到需要的行,无序逐个扫描。但是缺点在于,如果每次只涉及到很小的一部分列,那多余的列依然浪费内存以及CPU cache,导致更多的IO,为了避免这一问题,分析性数据库采用DSM列存储:将relationan按照列拆分成多个sub-relation。类似的,在页尾部存放一个索引。

NSM可以快速取出某一行的数据,因为一行的数据保存在同一页;DSM能更好的利用CPU cache以及更紧凑的压缩。

分布式储存系统虽然不再有页的概念,但是仍然会将文件切割成分块进行储存,但分块的粒度要远远大于一般扇区的大小(如 HDFS 的 Block Size 一般是 128MB)。更大的读写粒度是为了适应网络 IO 更低的带宽以获得更大的吞吐量,但另一方面也牺牲了细粒度随机读写。

img

列存储与分布式文件系统

在现代的大数据架构中,GFS,HDFS等分布式文件系统已经成为存放按规模数据集的主流方式,。分布式文件系统相比单机的磁盘,具备多副本高可用容量大成本低等优势,但是也有一些单机架构没有的问题。

  • 读写均要经过网络,吞吐量可以追平甚至超过硬盘,但是延迟要比硬盘大得多,且受网络环境影响很大。
  • 可以进行大吞吐量的顺序读写,但随机访问性能很差,大多不支持随机写入。为了抵消网络的 overhead,通常写入都以几十 MB 为单位。

以上缺点对于重度依赖读写的OLTP场景数据库来说是致命的。所以很多定位于OLAP的列存储放弃OLTP能力,从而构建在分布式文件系统之上。

充分发挥分布式文件系统的性能,有以下几种方式:按块读取数据,流式读取,追加写入等。

总结

本文介绍了列式存储的存储结构设计。抛开种种繁复的细节,我们看到,以下这些思想或设计是具有共性的。

  1. 跳过无关的数据。从行存到列存,就是消除了无关列的扫描;ORC 中通过三层索引信息,能快速跳过无关的数据分片。
  2. 编码既是压缩,也是索引。Dremel 中用精巧的嵌套编码避免了大量 NULL 的出现;C-Store 对 distinct 值的编码同时也是对 distinct 值的索引;PowerDrill 则将字典编码用到了极致(见下一篇文章)。
  3. 假设数据不可变。无论 C-Store、Dremel 还是 ORC,它们的编码和压缩方式都完全不考虑数据更新。如果一定要有更新,暂时写到别处、读时合并即可。
  4. 数据分片。处理大规模数据,既要纵向切分也要横向切分,不必多说。

MYSQL复制

摘自简书

保证主服务器(master)和从服务器(Slave)的数据是一致性的,向master插入数据后,slave会自动从master把修改的数据同步过来(有一定延迟),通过这种方式保证数据一致性,就是mysql复制。

复制能解决什么问题?

  • 高可用和故障切换

    master挂掉后可以指定一台slave充当master继续保证服务运行。

  • 负载均衡

    开发中可能会遇到锁表,导致暂时不能使用读的操作,使用主从复制,主库负责写,从库负责读,这样即使主库锁表,读从库也能保证业务的正常运行。

    调查发现一般读写的比例是10:1,所以需要多个slave。保证了系统的高可用。

  • 数据备份

  • 业务模块化

    可以一个业务模块读取slave,再针对不同的业务场景进行数据库的索引创建和根据业务选择mysql引擎,不同的slave可以根据不同需求设置不同的索引和存储引擎。

主从节点需要注意:

(1)主从服务器操作系统版本和位数一致;
(2) Master和Slave数据库的版本要一致;
(3) Master和Slave数据库中的数据要一致;
(4) Master开启二进制日志,Master和Slave的server_id在局域网内必须唯一;

复制的流程

  • master将数据改变写到二进制日志(binary log)中,也就是配置文件login-bin 指定的文件。
  • slave通过线程IO读取日志文件并写入到中继日志(relay log)
  • slave重做中继日志中的事件,把中继日志的事件信息一条条的本地执行,完成数据在本地的存储,从而实现将改变反映到他自己的数据(数据重放)。

复制涉及到三个线程

  • 主节点binary log dump线程(IO线程)

    slave连接master时,master创建log dump线程,发送bin-log内容。在读取bin-log中的操作时,线程会给bin-log加锁。

  • 从节点IO线程‘

    当从节点执行start slave命令之后,从节点创建一个IO线程来连接主节点,请求从主库中更新bin-log。IO线程收到主节点的binlog sump线程发来的更新之后,保存在本地的relay-log中。

  • 从节点SQL线程

    读取relay log中的内容,解析成具体的操作并执行,最终保证主从数据库的一致性。

复制类型

  • 基于语句的复制 statement-base Replication(SBR)

    在master上执行的SQL语句,在slave上会执行相同的语句。Mysql默认采用基于语句的复制,效率比较高。一旦发现没法精准复制时,会自动选基于行的复制。

    优点是只需要记录修改数据的sql语句到binlog,减少binlog日志量,节约IO。

    缺点是语句很复杂的时候,slave执行消耗过多资源,而基于行复制的话,只会记录变更的行记录。

  • 基于行的复制

    把改变的内容复制到slave,而不是把命令在slave执行一遍。

    优点:只会记录变更的行记录,哪怕一个语句很复杂,但是它最后只影响几条记录,那么行的复制,只会把影响到几条记录记录到binlog,降低slave重放日志时的资源消耗。

    缺点:日志庞大,不利于数据库的还原。

  • 混合类型的复制

    默认采用基于语句的复制,当发现基于语句的复制无法精确的复制时,采用基于行的复制。

数据库三范式

  • 第一范式(确保每列保持原子性)

    最基本的范式。如果数据库表中所有字段都是不可分解的原子值,说明满足第一范式。如地址,有时候需要访问地址中的省份部分,有时候访问城市部分,这时候将地址拆分成省份、城市、详细地址等多个部分进行存储,这样设计就满足了第一范式。

  • 第二范式(确保表中的每列都和主键相关)

    在一个数据库表中,一个表中只能保存一种数据,不可以把多种数据保存在同一张数据库表中。

    2012040114063976

    例如这张表,就要拆成,订单信息表,订单项目表,商品信息表。

    2012040114082156

  • 第三范式(确保每列都和主键直接相关,而不是间接相关)

    比如在设计一个订单数据表的时候,可以将客户编号作为一个外键和订单表建立相应的关系。而不可以在订单表中添加关于客户其它信息(比如姓名、所属公司等)的字段。

多线程

使用

三种方法:

  • 实现Runnable:无返回值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    package test;

    public class MyRunnable implements Runnable{
    @Override
    public void run() {
    System.out.println("实现runnable接口的线程");
    }
    }

    1
    2
    3
    MyRunnable myRunnable = new MyRunnable();
    Thread myThread = new Thread(myRunnable);
    myThread.start();
  • 实现Callable:有返回值,通过FutureTask封装

    1
    2
    3
    4
    5
    6
    public class MyCalled implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
    return 2;
    }
    }
    1
    2
    3
    4
    5
    MyCalled myCalled = new MyCalled();
    FutureTask<Integer> futureTask = new FutureTask<>(myCalled);
    Thread thread = new Thread();
    thread.start();
    System.out.println(futureTask.get());
  • 继承thread:实现run接口

    1
    2
    MyThread mt = new MyThread();
    mt.start();

线程机制

Executor(线程池)

管理多个异步程序的执行,无序coder手动管理,主要有如下三种:

  • CachedThreadPool:一个任务创建一个线程

    1
    2
    3
    4
    5
    ExecutorService executorService = Executors.newCachedThreadPool();
    for(int i = 0;i<5;i++){
    executorService.execute(new MyRunnable());
    }
    executorService.shutdown();
  • FixedThreadPool:所有任务使用固定大小的线程

  • SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。

Daemon

守护线程,非守护线程都结束之后也随之终结,main是非守护线程,在线程启动前使用setDaemon可以设置成守护线程。

sleep

线程休眠

yield

当前线程已经完成了最重要的部分,可以切换给其他线程执行。

中断

InterruptedException

中断线程,如果线程处于阻塞、等待状态,抛出InterruptedExecution,从而提前结束。但是不能中断I/O和synchronized阻塞。

interrupted

如果线程的run方法执行无限循环,并且没有执行sleep等抛出InterruptedException的操作,调用interrupt就不会终止。

但是interrupt会设置线程的中断标记,此时调用interrupted返回true,因此可以在循环体中用此方法判断线程是否处于中断。

1
2
3
4
5
6
7
@Override
public void run() {
while (!interrupted()){

}
System.out.println("Thread end");
}

Executor中断操作

shutdown方法会等待线程都执行完毕之后关闭,如果调用的是shutdownNow方法,相当于调用每个线程的interrupt方法。

如果想中断Executor中的一个线程,可以通过submit提交一个线程,会返回一个Future<?>对象,调用对象的cancel方法可以中断线程。

互斥同步

synchronized:JVM实现的锁。

  • 代码块:只作用于同一个对象

    1
    2
    3
    4
    5
    public void func(){
    synchtonized(this){

    }
    }
  • 方法:只作用于同一个对象

    1
    2
    3
    public synchronized void func () {
    // ...
    }
  • 类:不同实例也会锁资源

    1
    2
    3
    4
    5
    public void func() {
    synchronized (SynchronizedExample.class) {
    // ...
    }
    }
  • 静态方法:作用于整个类

    1
    2
    3
    public synchronized static void fun() {
    // ...
    }

ReentrantLock

是JDK实现的锁,在JUC包下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LockExam {
private Lock lock = new ReentrantLock();

public void fun(){
lock.lock();
try {
for(int i = 0;i<10;i++){
System.out.println(i+" ");
}
}finally {
lock.unlock();
}
}
}

区别

  • S是JVM实现,R是JDK实现
  • 性能大致相同
  • R等待可中断,S不行
  • S非公平,R可公平

选择

优先S。JVM实现,和JDK版本无关,天然支持。并且S锁由JVM释放,不用担心因为没有释放锁而导致的死锁问题。

线程协作

join

将当前线程挂起,知道目标线程执行结束。

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
package test.线程;

/**
* @author zhuhuihui15
* @description
* @date 2021/6/23 10:23
*/
public class JoinExam {
private class A extends Thread{
@Override
public void run() {
System.out.println("A");
}
}

private class B extends Thread{
private A a;
B(A a){
this.a = a;
}
@Override
public void run() {
try {
a.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B");
}
}

public void test() {
A a = new A();
B b = new B(a);
b.start();
a.start();
}

}

输出AB。

wait/notify/notifyAll

调用wait线程挂起,其他线程调用notify或者notifyall来唤起线程,属于Object的一部分,不属于Thread。只能应在同步方法或者同步控制块。

必须释放锁,如果不释放,其他方法无法进入对象的同步方法,也就无法执行notify,从而造成死锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class WaitExam {
public synchronized void before(){
System.out.println("before方法");
notifyAll();
}

public synchronized void after() {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after方法");
}
}

await/signal/signalAll

JUC提供了Condition类来实现线程之间的协调,可以在Condition调用await方法使线程等待,相比于wait,await可以指定等待的条件,更加灵活。

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
public class AwaitTest {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();

public void before(){
lock.lock();
try {
System.out.println("before");
condition.signalAll();
}finally {
lock.unlock();
}
}

public void after() throws InterruptedException {
lock.lock();
try {
condition.await();
System.out.println("after");
}finally {
lock.unlock();
}
}

}

JUC-AQS

AQS被认为是JUC的核心。

  • CountDownLatch

    用来控制一个或者多个线程等待多个线程,维护了一个cnt,每次调用countDownLatch都会使得cnt数值减少1,减少到0的时候,那些因为调用await方法等待的线程就会被唤醒。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class test {
    public static void main(String[] args) throws InterruptedException {
    final Integer count = 10;
    ExecutorService executor = Executors.newCachedThreadPool();
    CountDownLatch countDownLatch = new CountDownLatch(count);
    for(int i = 0;i<count; i++){
    executor.execute(()->{
    System.out.println("run..");
    });
    countDownLatch.countDown();
    }
    countDownLatch.await();
    System.out.println("end");
    executor.shutdown();
    }
    }
  • CylicBarrier

    多个线程互相等待,多个线程都到达时,这些线程才会执行。线程执行await方法后计数器会-1,并进行等待,直到计数器为0,所有调用await方法的线程继续执行。

    和CountDownLatch的区别是,Barrier计数器通过调用reset()方法可以循环使用,所以叫循环屏障。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
      public class CylicBarrierTest {
    public static void main(String[] args) {
    final int totalThread = 10;
    ExecutorService executor = Executors.newFixedThreadPool(totalThread);
    CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
    for(int i = 0;i<totalThread; i++){
    executor.execute(()->{
    System.out.println("before");
    try {
    cyclicBarrier.await();
    } catch (InterruptedException e) {
    e.printStackTrace();
    } catch (BrokenBarrierException e) {
    e.printStackTrace();
    }
    System.out.println("after");
    });
    }
    executor.shutdown();
    }
    }

    + Semaphore

    类似于信号量,控制对互斥资源的访问线程数,以下程序模拟并发请求,

    ```java
    public class SemaphoreTest {
    public static void main(String[] args) {
    final int permit = 3;
    final int total = 10;
    Semaphore semaphore = new Semaphore(permit);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for(int i = 0;i<total;i++){
    executorService.execute(()->{
    try {
    semaphore.acquire();
    System.out.println("available permit: "+semaphore.availablePermits());
    } catch (InterruptedException e) {
    e.printStackTrace();
    }finally {
    semaphore.release();
    }
    });
    }
    executorService.shutdown();
    }
    }

JUC-Other

  • FutureTask

    可以用于异步封装,实例如下:

    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
    public class FutureTaskTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    FutureTask future = new FutureTask<Integer>(new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
    int result = 0;
    for(int i = 0;i<100;i++){
    Thread.sleep(10);
    result+=i;
    }
    return result;
    }
    });

    Thread thread = new Thread(future);
    thread.start();

    Thread thread1 = new Thread(()->{
    System.out.println("other thread is running...");
    try {
    Thread.sleep(1000);
    System.out.println("other thread is stop now");
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    });
    thread1.start();
    System.out.println(future.get());
    }
    }

    输出:

    other thread is running…
    other thread is stop now
    4950

  • BlockingQueue

    • FIFO队列:LinkedBlockingQueue、ArrayBlockingQueue(固定长度)
    • 优先队列:PriorityBlockingQueue

    提供了阻塞的get和set方法,队列为空时,get阻塞知道队列中有内容。队列满时,put阻塞,直到队列有空闲。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    public class ThreadQueueTest {
    private static BlockingQueue queue = new ArrayBlockingQueue(5);

    private static class Producer extends Thread{
    @Override
    public void run() {
    try {
    queue.put("producer");
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("producer..");
    }
    }

    private static class Consumer extends Thread{
    @Override
    public void run() {
    try {
    String producer = (String) queue.take();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("consumer..");
    }
    }

    public static void main(String[] args) {
    for (int i = 0;i<2;i++){
    Producer producer = new Producer();
    producer.start();
    }

    for(int i = 0;i<5;i++){
    Consumer consumer = new Consumer();
    consumer.start();
    }
    for(int i = 0;i<3;i++){
    Producer producer = new Producer();
    producer.start();
    }
    }
    }
  • ForkJoin

    用于并行计算,类似于Mapeduce

    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
    public class ForkJoinTest extends RecursiveTask<Integer> {
    private final int threshold = 5;
    private int first;
    private int last;

    public ForkJoinTest(int first,int last){
    this.first = first;
    this.last = last;
    }

    @Override
    protected Integer compute() {
    int result = 0;
    if(last-first<=threshold){
    for(int i = first;i<=last;i++){
    result+=i;
    }
    }else {
    //任务拆分
    int middle = first+(last - first)/2;
    ForkJoinTest left = new ForkJoinTest(first, middle);
    ForkJoinTest right = new ForkJoinTest(middle,last);
    left.fork();
    right.fork();
    result = left.join()+right.join();
    }
    return result;
    }
    }
    1
    2
    3
    4
    5
    6
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    ForkJoinTest exam = new ForkJoinTest(1,10000);
    ForkJoinPool forkJoinPool = new ForkJoinPool();
    Future result = forkJoinPool.submit(exam);
    System.out.println(result.get());
    }

java内存模型

三大特征

  • 原子性

    AtomicInteger保证原子性

    1
    2
    3
    4
    5
    6
    7
    private AtomicInteger count = new AtomicInteger();
    public void add(){
    count.incrementAndGet();
    }
    public int get(){
    return count.get();
    }

    Sychronized保证原子性:

    1
    2
    3
    4
    5
    6
    7
    8
    private int count = 0;
    public synchronized void add(){
    count++;
    }

    public synchronized int get(){
    return count;
    }
  • 可见性:一个线程修改,其余线程可以立即知道这个修改。主要的实现方式有volatile,sychronized,final。final是在未发生this逃逸的情况下,其余线程是可见的。其中volatile只保证可见性,但不保证原子性。

    this逃逸:在构造函数完成构造之前,其余线程就拥有该对象的引用,导致程序发生莫名其妙的问题。

  • 有序性

    线程内有序,但是线程之间无序,因为发生指令重排,volatile通过添加内存屏障的方式禁止指令重排,也可以通过sychronized。

先行发生原则

  • 单线程:前面的操作先于后面的操作
  • 管道锁定规则:unlock先行于同一个锁的Lock操作
  • volatile变量规则:对volatile变量的写操作先行于读操作
  • 线程启动规则:start操作发生于其余操作之前
  • 线程加入原则:Thread结束先行于join返回
  • 线程中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生。
  • 对象终结
  • 传递性

线程安全实现

  • 不可变:final,String,枚举等等。

  • 互斥同步:sychronized,ReentrankLock

  • 非阻塞

    互斥同步是一种悲观策略,而且阻塞会产生性能影响。基于冲突检测的乐观并发策略是这样的:先进行操作,若没有其他线程争共享数据,success,否则采取补偿措施(不断重试,直到成功)

    • CAS

      全称是比较并交换(Compare-and-Swap),CAS指令需要有3个操作数,分别是内存地址V,旧的预期值A和新的值B。执行操作时,只有当V的值等于A,才将V的值更新为B。

    • AtomicInteger

      1
      2
      3
      4
      5
      6
      7
      8
      public final int getAndAddInt(Object var1, long var2, int var4) {
      int var5;
      do {
      var5 = this.getIntVolatile(var1, var2);
      } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

      return var5;
      }
    • ABA

      原本是A,被改成B,然后又改成A,CAS会认为没有被改变过,JUC下有atomicStampedReference,控制变量值的版本。大部分情况下ABA不影响并发程序的正确性,需要解决的话,改成互斥同步会比原子类更高效。

  • 无同步

    • 封闭栈:多个线程访问同一个方法的局部变量,不会出现线程安全问题,因为局部变量在虚拟机栈,线程私有。

    • 线程本地存储

      为每个线程提供一个变量副本,线程之间相互不影响,每个线程都保存了一个ThreadLocalMap的成员变量,存储以ThreadLocal为Key,set方法为值。

      内存泄漏问题:key弱引用,value强引用。系统GC出现key为null的value,尽可能手动remove().

    • 可重入代码:感觉是一种编程思想,依赖程序员主观实现,可重入的解释是:在代码执行的时候中断他,转而去执行另外一段代码,在控制权返回后,原来的程序不会出现任何错误。

锁优化

JVM对synchronized的优化。

自旋锁:

避免阻塞,遇到锁时先自旋。适用于共享数据的锁定状态很短的情况。JDK1.6开始自适应的自旋锁。自适应意味着自旋的次数不是固定,而是由前一次在同一个锁上的自旋次数和锁的拥有者状态决定。

锁消除:

通过逃逸分析来支持,如果Heap上的共享数据不可能逃逸出去被其他线程访问到,当成私有数据对待,消除锁。

例如字符串的拼接sb=s1+s2+s3,因为string不可变,会自动将拼接优化成stringBuffer的append操作,append是synchronized互斥,所以sb的引用不会发生逃逸,因此可以消除sb变量的加锁。

锁粗化

上述例子,两次字符串相加调用append会反复给同一个对象加锁解锁浪费性能,JVM将会把加锁的范围拓展粗化到整个操作序列的外部。上述代码就是第一个操作之前知道最后一个append操作之后,这样只需要加锁一次。

轻量级锁

JDK1.6引入偏向锁和轻量级锁,从而让锁有了四个状态:无锁,偏向锁,轻量级锁,重量级锁。

相对于重量级锁而言,使用CAS操作来避免重量级锁使用互斥量的开销。对于蛋白粉锁,在整个同步周期都是不存在竞争的,因此不需要都使用互斥量进行同步,可以先采用CAS操作进行同步,如果CAS失败了再改用互斥量进行同步。

偏向锁

对象锁第一次被线程获得时,标记为101偏向。同时使用CAS操作将线程ID记录到mark word中,如果CAS操作成功,这个线程每次进入到这个锁相关的同步块就不再需要任何的同步操作。

当有另外一个线程获取锁,偏向锁宣告结束,撤销偏向锁后恢复到为锁定状态或者轻量锁状态。

计算机网络

一、五层协议

一般折中OSI和TCP/IP,采用五层协议。(自上而下)

5 应用层:

[参考](https://krains.gitee.io/blogs/Computer Network/应用层.html#概述)

应用层的任务是通过使用进程间的交互来完成特定网络应用。应用层定义的是应用进程(进程:主机中正在运行的程序)间的交互规则,对于不同的网络应用需要不同的应用协议。应用层协议很多,如DNS,HTTP协议,电子邮件SMTP协议,把应用层交互的数据称为报文

  • DNS

    域名系统,是因特网的一项核心服务,是一个将域名和IP地址映射的一个分布式数据库。

  • HTTP协议

    超文本传输协议。所有的www文件都遵守这个标准。设计Http最初的目的是为了提供一种发布和接收HTML页面的方法。

  • websocket

4 运输层:

[参考](https://krains.gitee.io/blogs/Computer Network/传输层.html#概述)

负责向两台主机进程之间的通信提供通用的数据传输服务。应用进程利用该服务传送应用报文。由于一台主机可以同时运行多个线程,因此传输层具有复用和分用的功能。复用就是指多个应用进程可以同时用下面传输层的服务,分用则是把运输层把收到的消息交付到上面应用层中的相应进程。

运输层主要负责以下两种协议:

  • 传输控制协议TCP:提供面向连接的,可靠的数据传输服务。
  • 用户数据协议UDP:无连接的,尽最大努力的数据传输服务(不保证数据传输的可靠性)。

3 网络层:

[参考](https://krains.gitee.io/blogs/Computer Network/网络层.html#网际协议-ip)

在计算机网络中进行通信的两个计算机可能会讲过很多个数据链路,也可能还要经过很多通信子网。网络层的作用就是选择合适的网间路由和交换节点,确保数据及时传送。在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和包进行传送。在TCP/IP体系结构中,由于网络层使用IP协议,因此分组也叫IP数据报,简称数据报

运输层的数据报UDP和网络层的IP数据报不一样,另外,无论是哪一层的数据单元,都可以笼统的用“分组”来表示。

互联网是大型的异构网络通过路由器连接起来的。互联网使用的网络层协议是无连接的网际协议和许多路由选择协议,因此互联网的网络层也叫作网际层IP层

2 数据链路层:

简称为链路层。两台主机传输数据,是在一段一段的数据链路上传送的,需要专门的链路层协议。在两个相邻的节点传输数据时,链路层将网络层交下来的IP数据报组装成帧,在两个相邻的节点传送帧。每一帧包括数据和必要的信息(同步信息,地址信息,差错控制等)。

在接收数据时,控制信息使得接收端能够知道一个帧从哪个比特开始到哪个比特结束。这样链路层在收到一个帧后就可以从中提取数据部分。控制信息还使得接收端能够检测到所收到的帧中有错误差。发现差错,链路层简单的丢弃这个帧,避免浪费资源。如果需要改正差错,就要采用可靠性传输协议来纠正差错。

1 物理层:

在物理层上传送的数据是比特。物理层的作用是实现相邻计算机节点之间比特流的透明传输,尽可能屏蔽掉具体传输介质和物理设备的差异。使得其上面的链路层不必考虑网络的具体传输介质是什么。“透明传送比特流”表示经实际电路传送后的比特流没有发生变化。

计算机网络复习

二、TCP

0、TCP报文结构

tcp_head

  • 32位序列号:建立连接时由计算机生成的随机数作为初始值,通过SYN包传给接收端主机,每发送一次数据,就累加一次。用来解决网络包乱序的问题。
  • 32位确认号:希望收到的下一个数据报的序列号,表明到序列号 N-1 为止的所有数据已经正确收到。解决不丢包的问题。
  • TCP协议数据报头长:4位长。表明TCP头中包含多少个 4字节
  • 控制位:(6位)
    • ACK:是1的时候,确认应答的字段有效,TCP规定除了最初建立时的SYN包之外必须是1
    • RST:是1的时候,表示TCP连接中出现异常必须强制断开连接
    • SYN:是1的时候,希望建立连接,并在序列号的字段进行序列号初始值设定
    • FIN:是1的时候表示希望断开连接。当通信结束希望断开连接时,通信双方的主机之间可以交换FIN位为1的TCP字段
  • 窗口大小(WIN):16位长。表示从确认号开始,本报文的发送方(数据发送端 or 数据接收端)可以接收的字节数,即接收窗口大小。用于流量控制。
  • 校验和(Checksum):16位长。是为了确保高可靠性而设置的。它校验头部、数据和伪TCP头部之和。
  • 紧急指针:URG=1时才有意义。

TCP最小长度是20字节。

1、常见问题

  • 如何唯一确定一个TCP?

    通过TCP四元组:源地址、源端口、目的地址、目的端口,地址在IP头部,端口在TCP头部。

  • 一个IP的服务器监听了一个端口,它的TCP的最大连接数是多少?

    最大TCP连接数 = 客户端的IP数*客户端的端口数

    对于IPv4,IP数最多是$2^{32}$,客户端端口最多是$2^{16}$,最多可能是$2^{48}$.

  • UDP的头部格式是?

    image-20210820151945323
  • 为什么client和server的序列号不一样?

    网络报文可能延迟、复制重丢失,这样会造成不同连接之前相互影响,不一样可以避免相互影响。

    为了安全性,防止黑客伪造相同的序列号,接收到报文。

  • 序列号是如何产生的?

    最初基于时钟,每4ms+1,转一圈4.55小时。后来 $ISN = M+F(localhost,localport,remotehost,remostport)$

  • SYN攻击?

    短时间伪造不同IP的SYN报文,服务端每次发送ACK应答但是收不到第三次回应,导致沾满SYN接收队列,使得服务器不能正常服务。

    避免?

    修改linux参数,控制队列大小和队列满的处理策略。

2、TCP三次握手和四次挥手

  • 客户端和服务端处于CLOSED状态。先是服务端主动监听某个端口,处于LISTEN状态。
  • 客户端随机初始化序号(client_isn),将序号放入TCP首部的序号字段,同时把SYN置1,之后客户端处于SYN-SENT状态
  • 服务端收到客户端的SYN报文后,首先初始化自己的序号(server_isn),填入序号中;其次把确认号填入client_isn+1,把SYN和ACK标志位置为1,然后把报文发给客户端,此时客户端处于SYN-RCVD状态。
  • 客户端收到后,向服务端回应一个报文,ACK置1,应答号填server_isn+1,最后把报文发送给服务端,这次报文可以携带数据,之后客户端处于ESTABLISHED状态
  • 服务端收到应答报文,也进入ESTABLISHED状态。

如何查看TCP状态

1
netstat -napt

为什么是三次?

  • 避免历史连接(主要原因):客户端发送多次SYN建立连接的报文,在网络拥堵的情况下:

    image-20210822104906714
    • 旧SYN报文比最新的SYN报文早到达了客户端
    • 服务端返回SYN+ACK给客户端
    • 客户端根据自身上下文,判断是历史连接,发送RST给服务端,终止连接。
  • 同步双方初始序列号(这个和确认收发能力的过程类似),其实四次也可以,但是能三次为啥要四次呢?

  • 避免资源浪费:多个SYN阻塞,服务器在收到请求后会建立多个冗余连接。


断开一个TCP连接需要四次挥手:

计算机网络复习-第 3 页

  • 客户端:发送一个FIN报文,进入fin_wait_1状态
  • 服务端:收到FIN,它发回一个ACK。进入closed_wait状态
  • 客户端:收到ACK,进入到fin_wait-2状态。
  • 服务端:关闭与客户端的连接,发送一个FIN给客户端,进入到last_ack状态
  • 客户端:发回ACK报文确认,进入time_wait状态
  • 服务端:收到ACK,进入CLOSED状态,
  • 客户端:经过2MSL一段时间后,自动进入CLOSED状态。

常见问题:

  • 为什么需要四次挥手?

    任何一方都可以发送连接释放的通知,对方确认后进入办关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了TCP连接。更具体一点:AB两人通话,A:我说完了。B:我知道了(但我可能还有话说)。B:我也说完了。A:知道了(挂断)。

  • 为什么有TIME_WAIT状态

    • 防止具有相同四元组的旧数据包被收到
    • 保证被动关闭连接的一方能正确关闭,即保证最后的ack被被动关闭方接收。

3、TCP重传、滑动窗口、流量控制、拥塞机制

1.重传

超时重传

发送数据时设定一个定时器,超出指定时间没有收到ACK,则重发。以下两种情况触发超时重传:

  • 数据包丢失
  • 确认应答丢失

超时重传存在的问题是:超时周期可能比较长。


快速重传

超时重传是时间驱动,而快速重传是数据驱动。比如:

发送1,2,3,4,5五个数据,先传1,ack回2,这时候2没到,3到了,ACK还是回2,然后4到了,2还是没到,ACK依然回2。收到三个ACK=2,在定时器过期之前,重传丢失的2,最后收到了2,因为3,4,5都收到了,于是ACK回6.

有个问题就是,在重传的时候,是重传2呢还是重传2345呢?为了解决不知道重传哪些报文,于是有了SACK方法。


SACK重传(选择性确认)

在TCP头部添加SACK,可以将缓存的地图发送给发送方,就可以只重传丢失的数据。

image-20210822155359858

Duplicate SACK

以下两个场景:

  • ACK丢失

    由于ACK丢失,导致服务端发送重复数据。这时候接收到回复SACK,表示已经接受包,这个SACK就叫做D-SACK。

  • 网络延时

2、滑动窗口

窗口大小就是无需等待确认应答,可以继续发送数据的最大值。

TCP头有个window,也就是窗口的大小,接收方告诉发送方自己还有多少缓冲区,所以窗口的大小一般是接收方决定的。

3、流量控制

一种机制,可以让发送方根据接收方的实际接收能力控制发送的数据量。

连接双方有缓冲空间,只允许发送缓冲区能接纳的数据,接收端来不及处理的时候提示发送方降低发送速率,防止丢包。缓冲区是可改变大小的滑动窗口协议。

4、拥塞控制

网络阻塞时,减少数据的发送

4、TCP,UDP协议的区别

UDP在传送数据之前不需要建立连接,远程主机收到UDP报文后,不需要给出任何确认。虽然UDP不提供可交付,但是比较有效。比如:QQ语音,视屏,直播。

TCP提供面向连接的服务。传送前建立连接,传送后断开连接。不提供广播或者多播服务。(可靠性体现在三次握手,在数据传输时,还有确认,窗口,重传等等,在数据传输之后,还会断开连接来释放资源)。增加很多开销,使得协议数据单元的首部增大很多。一般用于文件传输,发送和接受邮件,远程登陆。

TCP如何保证可靠传输?

分块,编号,校验和,流量控制,拥塞机制,ARQ,超时重传。

  • 数据被分割成TCP认为最适合传输的数据块
  • 给发送的每一个包进行编号,接收方对数据报进行排序,把有序数据传给应用层。
  • 校验和:TCP将保持他首部和数据的校验和。这是一个端到端的检验和,目的是检验数据在传输的过程中的任何变化,有变化则丢弃并不确认收到此报文。
  • TCP接收端会丢弃重复的数据。
  • 流量控制:连接双方有缓冲空间,只允许发送缓冲区能接纳的数据,接收端来不及处理的时候提示发送方降低发送速率,防止丢包。缓冲区是可改变大小的滑动窗口协议。
  • 拥塞控制:网络阻塞时,减少数据的发送
  • ARQ协议:每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。
  • 超时重传: 当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。

4、滑动窗口和流量控制

TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。 接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。

5、拥塞控制

为了进行控制,TCP维持一个拥塞窗口的状态变量。窗口的大小取决于网络的拥塞程度,动态变化,发送方让自己的发送窗口取为拥塞窗口和接收方的接收窗口中较小的一个。

采用了四种算法:慢开始、拥塞避免、快重传、快恢复。

  • 慢开始:因为不知道情况,小到大增加发送窗口,cwnd(拥塞窗口)初始1,每次加倍。
  • 拥塞避免:让拥塞窗口cwnd缓慢增大,即每经过一个往返时间RTT(网络时延)就把发送放的cwnd加1.
  • 快重传与恢复:FRR(快速重传与恢复)能快速恢复丢失的数据包,没有FRR,丢失后,TCP使用定时器要求传输暂停。有了FRR,接收方收到一个不按照顺序的数据段,立即发送于一个重复确认。如果发送方接到三个重复确认,会假定指出的数据丢失,立即重传丢失的数据。有了 FRR,就不会因为重传时要求的暂停被耽误。当有单独的数据包丢失时,快速重传和恢复(FRR)能最有效地工作。当有多个数据信息包在某一段很短的时间内丢失时,它则不能很有效地工作。

6、ARQ协议

自动重传请求是OSI模型汇总数据链路层和传输层的错误纠正协议之一。使用确认和超时连个机制,在不可靠服务的基础上实现可靠的信息传输。如果发送方在发送一段时间没有收到确认帧,通常重新发送。ARQ通常包括:停止等待ARQ和连续ARQ协议。

停止等待ARQ:

  • 为了实现可靠传输,原理是:发送完停止,等待对方确认(回复ACK),超时没有收到ACK,则重新发送。
  • 若接收方收到重复分组,丢弃,但需要发送确认。

优点:简单

缺点:信道利用率低,等待时间长。

  1. 无差错情况:

    收到ACK,再次发送。

  2. 出现差错(超时重传)、

    发送完一个分组需要一个超时计时器。这种重传方式通常称为:自动重传请求ARQ。

  3. 确认丢失和确认迟到

    • 确认丢失:确认消息丢失,客户端再次发送,服务端收到消息采取以下搓手:丢弃消息,不向上层交付。再次发送确认消息。
    • 确认迟到:A发,B确认,但消息迟到。A再发,B第二次确认到达。传输别的。A收到第一次确认到达。处理方式:A收到重复确认直接丢弃,B收到重复的数据直接丢弃。

连续ARQ

可以提高信道利用率。发送方位置控制一个发送窗口,窗口内的分组连续发送,无需ACK,接收方累计确认,对顺序到达的最后一个分组发送ACK,表明到这个分组为止的所有分组都正确收到。

有点:信道利用率高,容易实现,即使确认丢失,也不不必重传。

缺点:不能向发送方反映出接收方已经正确收到的所有分组的信息。 比如:发送方发送了 5条 消息,中间第三条丢失(3号),这时接收方只能对前两个发送确认。发送方无法知道后三个分组的下落,而只好把后三个全部重传一次。这也叫 Go-Back-N(回退 N),表示需要退回来重传已经发送过的 N 个消息。

三、HTTP

HTTP报文结构

请求报文头部

  • User-Agent:产生请求的浏览器类型。
  • Accept:客户端可识别的响应内容类型列表;
  • Accept-Language:客户端可接受的自然语言;
  • Accept-Encoding:客户端可接受的编码压缩格式;
  • Accept-Charset:可接受的应答的字符集;
  • Host:请求的主机名,允许多个域名同处一个IP 地址,即虚拟主机;(必选)
  • Connection:连接方式(close 或 keep-alive);
  • Cookie:存储于客户端扩展字段,向同一域名的服务端发送属于该域的cookie;
  • 请求包体:在POST方法中使用。
  • Referer:包含一个URL,用户从该URL代表的页面出发访问当前请求的页面。
  • If-Modified-Since:文档的最后改动时间

响应头部

  • Allow 服务器支持哪些请求方法(如GET、POST等)。
  • Content-Encoding 文档的编码(Encode)方法。
  • Content-Length 表示内容长度。只有当浏览器使用持久HTTP连接时才需要这个数据。
  • Content-Type 表示后面的文档属于什么MIME类型。
  • Date 当前的GMT时间。你可以用setDateHeader来设置这个头以避免转换时间格式的麻烦。
  • Expires 应该在什么时候认为文档已经过期,从而不再缓存它。
  • Last-Modified 文档的最后改动时间。
  • Refresh 表示浏览器应该在多少时间之后刷新文档,以秒计。
  • Server 服务器名字。
  • Set-Cookie 设置和页面关联的Cookie。
  • ETag:被请求变量的实体值。ETag是一个可以与Web资源关联的记号(MD5值)。
  • Cache-Control:这个字段用于指定所有缓存机制在整个请求/响应链中必须服从的指令。

Q:输入URL地址,显示主页的过程

更详细的参考这里

主要会使用到哪些协议。

过程:

  • 1、浏览器查找域名的IP地址(浏览器缓存,路由器缓存,DNS缓存)
  • 2、浏览器向WEB服务器发送http请求(cookies会随着请求发送给服务器)
  • 3、服务器处理请求
  • 4、服务器发回一个HTML响应。
  • 5、浏览器开始显示HTML

协议:

  • DNS
  • TCP:与服务器建立TCP连接
  • IP:建立TCP协议,需要发送数据,发送数据再网络层使用IP协议
  • OPSF:IP数据包在路由器期间,路由选择使用此协议。
  • ARP:路由器与服务器通信,将IP转化为mac地址
  • HTTP:TCP建立后,通过HTTP访问网页。

Q-状态码

类别 原因短语
1.. 信息性状态码 接收的请求正在处理
2.. 成功状态码 请求正常处理完毕
3.. 重定向状态码 需要进行附加操作完成请求
4.. 客户端错误状态码 服务器无法处理请求
5.. 服务器错误状态码 服务器处理请求出错
  • 3

    重定向,需要进一步的操作以完成请求

    • 301 Moved Permanently。请求的资源已被永久的移动到新URI,返回信息会包括新的URI,浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替
  • 302 Moved Temporarily。与301类似。但资源只是临时被移动。客户端应继续使用原有URI

    • 304 Not Modified。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源
  • 4

    客户端错误,请求包含语法错误或无法完成请求

    • 400 Bad Request 由于客户端请求有语法错误,不能被服务器所理解。
  • 401 Unauthorized 请求未经授权。这个状态代码必须和WWW-Authenticate报头域一起使用

    • 403 Forbidden 服务器收到请求,但是拒绝提供服务。服务器通常会在响应正文中给出不提供服务的原因
    • 404 Not Found 请求的资源不存在,例如,输入了错误的URL
  • 5

    服务器错误,服务器在处理请求的过程中发生了错误

    • 500 Internal Server Error 服务器发生不可预期的错误,导致无法完成客户端的请求。
  • 503 Service Unavailable 服务器当前不能够处理客户端的请求,在一段时间之后,服务器可能会恢复正常。

Q-各种协议与HTTP协议的关系

image-20210320222328026

Q-HTTP长连接、短连接

HTTP/1.0默认短连接,客户端每次访问某个html或者其他类型的web中包含其他的web资源(JS文件,图像,CSS),每次遇到这样一个web资源,浏览器就会重新建立一个HTTP会话。

从HTTP/1.1默认使用长连接,会在响应头加入

1
COPYconnection:keep-alive

长连接,每次一个网页打开,客户端和服务器之间用于传输HTTP数据的TCP连接不关闭。不会持久保持,有保持时间,可以在不同的服务器软件设定。实现长连接的客户端和服务器都需要支持长连接。

HTTP协议的长连接和短连接,实质上是TCP协议的长连接短连接。

Q-HTTP如何保存用户状态

HTTP是无状态协议,自身不对请求体和响应体之间的通信状态进行保存。Session机制,通过服务端记录用户状态。典型的场景是购物车,当你添加商品到购物车时,系统不知道是哪个用户,服务器给特定的用户创建特定的Session来标识这个用户并跟踪。(过期销毁)。

在服务端保存session方法:内存和数据库(redis),既然session在服务端,那如何实现session跟踪呢?通过在cookie中添加一个session ID 的方式来实现追踪。

cookie被禁用了怎么办?

URL重写,直接把Session ID附加在URL路径的后面。

Cookie和Session都用来跟踪浏览器用户身份的会话方式,但是场景不太一样。

Cookie一般保存用户信息:

  • 在cookie中保存已经登录过的用户信息,下次访问网站可以自动填写基本信息。
  • 保持登录,在cookie中存放了token。
  • 登录一次网站后访问网站其他页面不需要登陆。

Cookie数据保存在客户端,Session保存在服务器端。

Session安全性更高。

Q-HTTP/1.0和HTTP/1.1

  • 1.0默认长连接。1.1的持续连接有流水线和非流水线方式。流水线是客户端收到HTTP的响应报文之前能接着发送新的请求报文。非流水线是客户端在收到前一个响应后才能发送下一个请求。
  • 错误状态响应码:新增了24个状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。
  • 缓存处理:在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。
  • 带宽优化以及网络连接的使用:1.0存在浪费带宽,例如客户端要对象的一部分,但是服务端把整个对象都穿过来,不支持断点续传。1.1在请求头引入range头域,允许只请求资源的某个部分,返回206。

Q-URL和URI

URI:统一资源标志符,唯一表示一个资源

URL:统一资源定位符,可以提供资源的路径。是具体的URI。

URI像身份证,URL像家庭住址。不仅标识资源,还提供定位资源的信息。

Q-HTTP和HTTPS

  1. 有哪些区别:
    • HTTP明文传输。HTTPS解决了HTTP不安全的缺陷,在TCP和HTTP网络层之间加入了SSL/TLS安全协议,使得报文加密传输。
    • HTTP在TCP三握只有就可以报文传输,HTTPS在TCP的三握之后还需要进行TLS的握手
    • HTTTP端口80,S是443
    • HTTPS需要向CA(证书权威机构)申请数字证书,保证服务器的身份是可靠的。
  2. HTTPS解决了哪些问题
  • 混合加密的方式实现了信息的机密性,解决了窃听的风险。

  • 摘要算法实现完整性,为数据生成独一无二的指纹,指纹用于校验数据的完整性,解决了篡改的风险。

  • 将服务器公钥放入到数字证书中,解决了冒充的风险。

    混合加密

    HTTPS采用对称加密和非对称加密结合,在通信建立前使用非对称加密的方式交换会话秘钥,通信过程采用对称加密的会话秘钥加密明文数据。

    为什么采用混合?

    • 对称加密只有一个秘钥,运算速度快,秘钥必须加密,无法做到安全的密钥交换。
    • 非对称加密使用公钥和私钥,公钥可以任意分发而私钥保密。

    摘要算法

    实现完整性,为数据生成独一无二的指纹,防止篡改。

    客户端在发送明文之前通过摘要算法算出明文的指纹,发送的时候指纹+明文一起加密发送,服务器解密后,用相同的摘要算法算出发送过来的明文,通过比较客户端携带的指纹和当前算出的指纹进行比较,指纹相同说明数据是完整的。

    数字证书

    客户端索取公钥的时候如何防止不被篡改?需要借助第三方权威机构CA(数字证书认证机构),将服务器公钥放在数字证书中,只要证书是可信的,公钥就是可信的。

  1. HTTPS如何建立连接

    基本流程是:客户端索要公钥,双方协商产生会话秘钥,双方使用会话秘钥进行加密通信。前两步是SSL/TLS建立过程,是握手阶段。

    • clientHello

      客户端向服务器发起加密通信请求,主要发送以下信息:

      • 客户端支持的SSL/TLS协议斑版本
      • 客户端生产的随机数
      • 客户端支持的密码套件列表,如RSA加密算法
    • ServerHello

      服务器收到请求后,向客户端发送响应。有以下内容:

      • 确认SSL/TLS协议版本,如果浏览器不支持,则关闭加密通信
      • 服务器产生的随机数
      • 确认的密码套件列表,如RSA
      • 服务器的数字证书
    • 客户端回应

      客户端在收到服务器的回应后,通过浏览器或者OS中的CA公钥,确认服务器数字证书的真实性,没问题的话从证书抽取公钥,使用公钥加密报文,发送如下信息:

      • 一个随机数,会被公钥加密
      • 加密通信算法改变通知,表示随后的信息使用会话秘钥加密通信
      • 客户端握手结束通知,同时把之前所有内容的发生的数据做个摘要,用来服务端校验。

      现在一共有了三个随机数,接着用双方协商的加密算法,各自生成本次通信的会话秘钥。

    • 服务器最后回应

      服务端收到客户端的第三个随机数,通过协商的加密算法,计算本次通信的会话秘钥,然后向客户端发生最后的信息:

      • 加密通信算法改变的通知,随后都使用会话秘钥加密通信
      • 服务器握手结束,表示服务器的握手阶段结束,把之前所有内容发生的数据做个摘要,用来客户端校验。
  2. HTTP1.1、HTTP2、HTTP3的演变

    1.1相比于1.0多了长连接和管道,但是还是有性能瓶颈,HTTP2有以下改进:

    • 头部压缩

      同时发出多个请求,头是一样的或者相似的,协议会消除重复的部分,这就是HPACK算法:在客户端和服务器同时维护⼀张头信息表,所有字段都会存⼊这个表,⽣成⼀个索引号,以后就不发送同样字段了,只发送索引号,这样就提⾼速度了。

    • 二进制格式:不传输明文,直接传输二进制,统称为帧

    • 数据流:HTTP2不是按照顺序发送,同一个连接里面连续的数据报,可能属于不同响应。因此要对数据做标记,指出属于哪个回应。

      客户端还能指定数据流的优先级。

    • 多路复用:移除了1.1中的串行请求,不需要排队等待,不会出现队头阻塞问题。

    • 服务器推送:可以主动发送信息,例如在浏览器刚请求html页面的时候,提前吧可能用到的JS ,CSS 文件返回。

  3. HTTP2有哪些缺陷?HTTP3有哪些优化

    缺陷:

    • 管道传输中有一个请求阻塞了,队列后的请求也阻塞
    • 2中多路复用,一旦丢包,就会阻塞所有的HTTP请求。

    所以在3中把http下层的TCP协议改成了UDP,UDP不管顺序和丢包,不会出现1.1队头阻塞和2中的丢包全部重传问题。UDP不可靠,但是基于UDP的QUIC可以实现类似TCP的可靠性传输。QUIC是一个在UDP之上的伪TCP+TLS+HTTP/2的多路复用协议。

  4. HTTP1.1如何优化

    • keep_alive长连接

    • 避免http请求-缓存,服务端会返回预估的过期时间。

    • 减少HTTP请求次数

      • 减少重定向次数:重定向工作让代理服务器完成

        image-20210820132932346

        在知道重定向规则后,可以进一步减少消息传递,如下:

        image-20210820133050808
      • 合并请求

        多个访问小文件的请求合并成一个大的请求,减少了请求次数,也就减少了重复发送的http头部。比如用CSS Image Sprites技术把他们合成一个大图片,再根据CSS数据切割。或者使用webpack将js,css资源合并打包成大文件。存在的问题就是,当大资源的某个小资源发生变化后,客户端必须下载整个完整的大资源文件,带来额外的网络消耗。

      • 延迟发送请求:请求⽹⻚的时候,没必要把全部资源都获取到,⽽是只获取当前⽤户所看到的⻚⾯资源,当⽤户向下滑动⻚⾯的时

        候,再向服务器获取接下来的资源,这样就达到了延迟发送请求的效果。

    • 减少HTTP响应的数据大小:对返回的资源进行有损压缩或者无损压缩

      content-encoding: gzip

      Accept: audio/*;q=0.2,audio/basic

  5. HTTPS如何优化

    性能消耗主要是两个环节,一是TLS协议握手的过程,二是握手后的加密报文传输。对于2来说,主流的对称加密算法如AES,ChaCha20性能都很好,主要是解决1的性能消耗。

    • 硬件优化,计算密集型,考虑更好的CPU

    • 软件优化:软件优化方向,可以吧软件升级成比较新的版本,例如将linux内核2.X升级成4.X等等

      对于协议优化,密钥交换选择ECDHE算法,不用RSA算法 ;将TSL1.2升级成TSL1.3.

    • 证书优化:

      • 选择ECDSA,因为更短
      • 开启OCSP Stapling功能,

五、websocket

一种与HTTP不同的协议。两者都位于OSI模型的应用层,依赖TCP协议。虽然不同,但是RFC规定:WebSocket设计为通过80和443端口工作,支持HTTP代理和中介,从而和HTTP兼容,为了实现兼容,W握手使用HTTP ,Upgrade头从HTTP协议更改为W协议。

与H不同,W提供权全双工通信。此外,W还可以在TCP之上启用消息流。TCP单独处理字节流,没有固定的消息概念。

WebSocket协议规范将 ws(WebSocket)和 wss (WebSocket Secure)定义为两个新的统一资源标识符(URI)方案,分别对应明文和加密连接。

优点:

  • 较小的控制开销:
  • 更强的实时性:由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;
  • 保持连接状态:与 HTTP 不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
  • 更好的二进制支持:Websocket 定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
  • 可以支持拓展:Websocket 定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。
  • 更好的压缩效果:相对于HTTP压缩,Websocket 在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率

连接过程:

W是独立的、创建在TCP上的协议,W通过HTTP/1.1协议的101状态码进行握手。为了创建W连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为握手。

  • 客户端请求

    1
    2
    3
    4
    5
    6
    7
    COPYGET / HTTP/1.1
    Upgrade: websocket
    Connection: Upgrade
    Host: example.com
    Origin: http://example.com
    Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
    Sec-WebSocket-Version: 13
  • 服务器回应

    1
    2
    3
    4
    5
    COPYHTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
    Sec-WebSocket-Location: ws://example.com/

JVM

1、JVM架构

java源码通过javac编译为java字节码,java字节码是java虚拟机执行的一套代码格式,抽象了计算机的基本操作。大多数指令只有一个字节,有些操作符需要参数,导致多使用了一些字节。

java虚拟机

JVM的基础架构如上图所示:主要包含三大块:

  • 类加载器:负责动态加载JAVA类到JAVA虚拟机的内存空间。
  • 运行时数据区:存储JVM运行时的所有数据。
  • 执行引擎:提供JVM在不同平台的运行能力。

线程

在JVM中运行着很多线程,一部分是程序创建来执行代码逻辑的应用线程,剩下的是JVM创建来执行后台任务的系统线程

主要的系统线程:

  • Compile:运行时将字节码编译为本地代码使用的线程。
  • GC:包含所有和GC有关的操作。
  • Period Task:JVM周期性任务调度的线程,主要包含JVM内部的采样分析。
  • Singal Dispatcher:处理OS发来的信号。
  • VM:某些操作需要等待 JVM 到达 安全点(Safe Point),即堆区没有变化。比如:GC 操作、线程 Dump、线程挂起 这些操作都在 VM Thread 中进行。

类型来分,JVM内部有两种线程:

  • 守护线程:JVM自己使用,程序也可以把自己的线程标记为守护线程(public final void setDaemon(boolean on),必须在start()方法之前调用。
  • 非守护线程:main方法执行的线程,也称为用户线程。

只要有非守护进程运行,java程序就会继续运行,非守护都终止时,虚拟机也会自动退出。

守护进程不适合进行IO,计算等操作,因为守护进程在非守护进程结束自动释放,不能判断该守护进程是否完成了操作。

2、类加载器

负责动态加载类到虚拟机的内存空间。通常是按需加载,类第一次使用才加载。有了类加载器,java在运行时系统不需要知道文件与文件系统。每个java类都要由类加载器装入到内存。

除了定位和导入二进制文件,还验证类的正确性,为变量分配初始化内存,帮助解析符号引用。按照以下顺序完成:

  • 装载:查找并装载二进制数据。
  • 链接:执行验证、准备、解析。
    • 验证:确保被导入类型的正确性。
    • 准备:为类变量分配内存,并将其初始化为默认值。
    • 解析:把类型中的符号引用转化为直接引用。
  • 初始化:把类变量初始化为正确的初始值。

1.装载

多个类装载器,应用程序可以使用两种类装载器:

  • Bootstrap ClassLoader:原生C编写,不继承自java.lang.ClassLoder。加载核心类库,启动类加载器通常使用某种默认的方式从本地磁盘中加载,包括java API。
  • Extention Classloader:用来在<JAVA_HOME>/jre/lib/ext ,或 java.ext.dirs 中指明的目录中加载 Java 的扩展库。 Java 虚拟机的实现会提供一个扩展库目录。
  • Application Classloader:根据应用程序的类路径java.class.path或者CLASSPATH加载类。一般来说,java应用的类它加载。可以通过ClassLoader.getSystemClassLoader() 来获取它。
  • 自定义类加载器:继承java.lang.ClassLoder来实现自己的类加载器。

全盘负责双亲委托机制:

一个JVM系统至少3种类加载器,如何平配合工作?通过全盘负责双亲委托机制来协调类加载器。

  • 全盘负责:当一个ClassLoder装载一个类时,除非显示的使用另一个,该类及其所依赖的类都用这个装载。
  • 双亲委托机制:指先委托父装载器寻找目标类,只有在找不到的情况下才从自己的类路径中查找并装载目标类。

全盘负责双亲委托是java推荐的机制,不是强制。实现自己的类加载器,保持机制,重写findClass(name)方法;破坏此机制,重写loadClass(name)方法。

装载入口:

所有java虚拟机实现必须在每个类或者接口首次主动使用时初始化。以下情况符合主动使用的要求:

  • 创建某个类的新实例(new、反射、克隆、序列化)
  • 调用某个类的静态方法
  • 使用类或者接口的静态字段,或对该字段赋值。final
  • 调用java API 的某些反射方法时。
  • 初始化某个类的子类时、
  • 当虚拟机启动时被表明为自动类的类。

除了以上全是被动,不会导致java类型的初始化。

对于接口来说,只有在此接口声明的非常量字段被使用,才会初始化,不会因为事先这个接口的子接口或者类要初始化而被初始化。

父类需要在子类之前初始化。当实现了接口的类被初始化时,不需要初始化父接口。然而,当实现了父接口的子类(或者拓展了父接口的子接口)被装载时,父接口也要被装载(装载但不实例化)。

2.链接

验证

确保装载后的类型符合java语言的语义,并且不会危害整个虚拟机的完整性。

  • 装载时验证:检查二进制数据保证是预期格式。确保除object外的每个类都有父类,确保该类的父类已经被装载。
  • 正式验证阶段:检查final类不能有子类,确保 final 方法不被覆盖、确保在类型和超类型之间没有不兼容的方法声明(比如拥有两个名字相同的方法,参数在数量、顺序、类型上都相同,但返回类型不同)。
  • 符号引用的验证:当虚拟机搜寻一个被符号引用的元素(类型、字段或方法)时,必须首先确认该元素存在。如果虚拟机发现元素存在,则必须进一步检查引用类型有访问该元素的权限。

准备

虚拟机为类变量分配内存,设置默认初始值。初始化阶段之前,类变量都没有被初始化为真正的初始值。

类型 默认值
int 0
long 0L
short (short)0
char ‘\u0000’
byte (byte)0
blooean false
float 0.0f
double 0.0d
reference null

解析

直白一点就是,将符号引用转化为直接引用,比如,将package com.source….转化为物理地址。

在类型的常量池中寻找类、接口、字段和方法的符号占用,把这些符号引用替换成直接引用的过程。

  • 类、接口的解析:判断所要转化成的直接引用是数组类型,还是普通的对象类型的引用,从而进行不同的解析。
  • 字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束。

3.初始化

所有类变量(静态量)初始化语句和类型的静态初始化器都被java编译器收集在一起,放到一个特殊的方法。对于类来说,方法叫初始化方法;对于接口来说,称为接口初始化方法。在类和接口的class文件中,称为<clinit>

  1. 存在直接父类且父类没被初始化,先初始化直接父类。
  2. 类存在一个类初始化方法, 执行此方法。

递归执行,所以第一个初始化的类一定是object。

虚拟机须确保初始化过程正确同步。如果多个县城需要初始化一个类,仅仅允许一个,其余的等待。

Clinit方法

  • 对于静态变量和静态初始化语句来说:执行的顺序和它们在类或接口中出现的顺序有关。
  • 并非所有的类都需要在它们的class文件中拥有<clinit>()方法, 如果类没有声明任何类变量,也没有静态初始化语句,那么它就不会有<clinit>()方法。如果类声明了类变量,但没有明确的使用类变量初始化语句或者静态代码块来初始化它们,也不会有<clinit>()方法。如果类仅包含静态final常量的类变量初始化语句,而且这些类变量初始化语句采用编译时常量表达式,类也不会有<clinit>()方法。**只有那些需要执行Java代码来赋值的类才会有<clinit>()**’
  • final常量:Java虚拟机在使用它们的任何类的常量池或字节码中直接存放的是它们表示的常量值。

3、内存模型

运行时数据区保存JVM在运行过程中产生的数据。

java虚拟机-第 2 页

Heap

各线程共享的内存区域,是虚拟机管理内存区域最大的一块。几乎所有的对象实例和数组实例都是在堆上分配,但是对着JIT编译器以及逃逸分析技术的发展,也可能被优化为在栈上分配。

还包含字符串字面量常量池。包含一个新生代,一个老年代。

新生代三个区,大部分对象在Eden去生成,survivor总有一个是空的。

老年代保存一些生命周期比较长的对象,当一个对象经过对此GC还没被回收,将移动到老年代。

Method Area

方法区的数据所有线程共享,为安全使用方法区的数据,需要注意线程安全问题。

方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

主要保存类级别的数据,包括:

  • ClassLoader Reference
  • Runtime Constant Pool
    • 数字常量
    • 类属性引用
    • 方法引用

在JVM1.8之前,方法区的实现为永久代。经常内存溢出。在JVM1.8方法区的实现改为元空间,元空间是在Native的一块内存空间。

Stack

JVM线程启动时,分配独立的运行时栈,来保存方法调用。每个方法调用,都会入栈一个栈帧。

栈帧保存三个引用:本地变量表操作数帧和当前方法所属类的运行时常量池。由于本地变量表和操作数栈的大小都在编译时确定,所以栈帧的大小是固定的。

被调用的方法返回或抛出异常,栈帧弹出。栈帧内的数据是线程安全的。

栈大小可以动态拓展,但是如果一个线程需要栈的大小超出允许的大小,抛出StackOverflowError

PC Register

对于每个JVM线程,线程启动时有一个独立的PC计数器,保存当前执行的代码地址。如果是Native方法, PC的值是NULL。一旦执行完成,PC计数器会被更新为下一个需要执行代码的地址。

Native Method Stack

本地方法栈执行的是native方法。native方法就是C++写的方法。

Direct Memory

可以直接调用的内存。堆外内存。D可以使用堆外内存。

在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为 避免了在 Java 堆和 Native 堆中来回复制数据

4、垃圾收集

对象存活检测

垃圾回收器回收内存之前,先要确定哪些对象是活的,哪些对象可以回收。

  • 引用计数算法

    对象添加引用计数器,引用时+1,引用失效-1。致命缺陷就是两个对象相互引用导致两个都无法回收。

  • 根搜索算法

    实际上是追踪从根节点开始的引用图。通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

    在根搜索算法追踪的过程,起点是GC Root,根据JVM的实现不同而不同,但是总会包含以下几个方面(堆外引用):

    • 虚拟机栈中引用的对象
    • 方法区中的类静态属性引用的变量
    • 方法区中的常量引用的变量
    • 本地方法JNI的引用对象

    从GC Root开始的引用图,有向图,节点时对象,边是引用类型。JVM分为四种引用类型:强引用,软引用,弱引用,虚引用。

    一个对象的引用类型有多个,如何确定回收策略:

    • 单条引用链以链上最弱的一个引用类型来决定;
    • 多条引用链以多个单条引用链中最强的一个引用类型来决定;

    在引用图,如果一个节点没有任何路径可达,确定回收。

    强引用:

    在java中普遍存在,类似于Object o = new Object;和其他引用的区别是:强引用禁止引用目标被GC收集,其他引用不禁止。

    软引用:

    JVM 的实现需要在抛出 OutOfMemoryError 之前清除 SoftReference,但在其他的情况下可以选择清理的时间或者是否清除它们。

    有用但不是必须,使用java.lang.red.SoftReferencr类表示,这个特性很适合做缓存,比如:网页缓存,图片缓存。

    弱引用:

    垃圾收集器在GC的时候会回收所有的弱引用,如果该弱引用和引用队列相关联,他会把该弱引用加入到队列。

    虚引用:

    “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

垃圾回收算法

1、复制回收

可用内存等分两份,同一时刻值使用其中一份。用完后将还存活的对象赋值到另一份,然后将这一份清空。能够有效避免内存碎片,但是减低了内存使用率。

解决了标记清除回收可能会产生大量不连续内存的问题。

2、标记清除算法

先暂停整个程序的全部运行线程,让回收线程以单线程进行扫描标记,并进行直接清除回收,然后回收完成后恢复运行线程。标记清除后会产生大量不连续的内存碎片,造成空间浪费。

3、标记整理算法

和标记清除相似,不同的是,回收期间同时会将保留的存储对象搬运汇集到连续的内存空间,从而集成空闲时间。

4、增量回收

将程序拥有的内存空间分成若干区。程序的存储对象会分布在分区中,每次只对其中一个分区进行回收,从而避免程序全部运行线程暂停来进行回收,允许部分线程在不影响回收行为的情况下保持运行,降低回收时间,增加线程响应速度。

5、分代回收

对象拥有不同的生命周期,不同生命周期的对象采用不同的回收算法,以提高效率。

记忆集:

对象C在新生代,只被一个在老年代的D引用。如果运行新生代GC,要确定C是否被堆外引用,需要遍历老年代,代价比较大。JVM在对象引用的时候,会有个记忆集,记录从老年代到新生代的引用关系,并把记忆集中的老年代作为GC ROOT构建索引图,这样在新生代GC的时候就不需要遍历老年代。

但是记忆集有缺点:C & D 其实都可以进行回收,但是由于记忆集的存在,不会将 C 回收。这里其实有一点 空间换时间 的意思。不过无论如何,它依然确保了垃圾回收所遵循的原则:垃圾回收确保回收的对象必然是不可达对象,但是不确保所有的不可达对象都会被回收

垃圾回收触发条件

1、堆内内存

对于HotSpot VM实现,GC只有两大种:

  1. Partial GC:并不收集整个GC堆
    • Young GC:只收集新生代的GC
    • Old GC:只收集老年代的GC。只有 CMS的 Concurrent Collection 是这个模式
    • Mixed GC:收集整个 Young Gen 以及部分 Old Gen 的 GC。只有 G1 有这个模式
  2. Full GC:收集整个堆,包括 Young Gen、Old Gen、Perm Gen(如果存在的话)等所有部分的 GC 模式。

最简单的分代式GC策略,按照HotSpot的serial GC的实现看,触发条件是:

  • Young GC:新生代的eden区分配满的时候触发,把eden区存货的对象复制到一个Survivor区,当这个Survivor区满时,存活的对象被复制到另一个survivor区。
  • Full GC
    • 当准备要触发一次 Young GC 时,如果发现之前 Young GC 的平均晋升大小比目前 Old Gen剩余的空间大,则不会触发 Young GC 而是转为触发 Full GC
    • 如果有 Perm Gen 的话,要在 Perm Gen分配空间但已经没有足够空间时
    • System.gc()
    • Heap dump

并发 GC 的触发条件就不太一样。以 CMS GC 为例,它主要是定时去检查 Old Gen 的使用量,当使用量超过了触发比例就会启动一次 GC,对 Old Gen做并发收集。

2、堆外内存

DirectByteBuffer的引用是直接分配在堆的old区,回收时机是在FULLGC,因此需要避免频繁的分配directByteBuffer,这样就很容易导致Native Memory溢出。

DirectByteBuffer申请的直接内存,不在G范围内,无法自动回收。JDK提供一种机制,可以为堆内存对象注册一个钩子函数(其实就是实现 Runnable 接口的子类),当堆内存对象被GC回收的时候,会回调run方法,我们可以在这个方法中执行释放 DirectByteBuffer 引用的直接内存,即在run方法中调用 UnsafefreeMemory 方法。注册是通过sun.misc.Cleaner 类来实现的。

垃圾收集器

内存回收的具体实现,以下为7种不同分代的收集器,有连线表示可以搭配使用。

java虚拟机-第 3 页

1、Serial 收集器

最基本的收集器,单线程。是JVM在client模式下的默认新生代收集器。优点是:简单高效。此收集器没有线程交互的开销,效率高。在用户桌面的场景下,分配给JVM的内存不会太多,停顿时间在几十到一百多毫秒之间,只要不频繁收集,完全可以接受。

2、Serial Old收集器

Serial的老年代版本,单线程,采用“标记-整理算法”进行垃圾回收。

3、ParNew 收集器

Serial的多线程版本,是许多运行在Server模式下的默认新生代GC,主要与CMS收集器配合工作。

4、Parallel Scavenge 收集器

新生代GC,多线程收集器。更关注可控制的吞吐量,吞吐量等于运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)。

5、Parallel Old

是Parallel Scavenge的老年代版本,多线程,通常与Parallel Scavenge配合使用。

6、CMS 收集器

目标是获取最短停顿时间,采用“标记-清除”算法,运行在老年代,包含以下步骤:

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除

其中初始标记和重新标记仍然需要stop the world。初始标记仅仅标记GC ROOT能直接关联的对象,并发标记是为了记性GC ROOT Tracing过程,重新标记是为了修正并发标记期间,因为用户程序继续运行而导致标记变动的那部分对象的标记记录。

最耗时的是并发表标记和并发清除,收集线程和用户线程一起工作,总体来说,CMS GC回收和用户程序并行。优点是并发收集、低停顿,但是有三个缺点:

  • 对CPU资源很敏感:并发阶段不停用用户进程,但是占用线程导致程序变慢。
  • 不能处理浮动垃圾:浮动垃圾就是在并发标记阶段,用户程序运行产生新的垃圾,这部分垃圾在标记后,CMS无法在当次集中处理,只能在下一次GC处理,这部分垃圾就称为浮动垃圾。

正是由于在垃圾收集阶段程序还需要运行,即还需要预留足够的内存空间供用户使用,因此 CMS 收集器不能像其他收集器那样等到老年代几乎填满才进行收集,需要预留一部分空间提供并发收集时程序运作使用。要是 CMS 预留的内存空间不能满足程序的要求,这是 JVM 就会启动预备方案:临时启动 Serial Old 收集器来收集老年代,这样停顿的时间就会很长。

6、G1收集器

比GMS有很大改进:

  • 标记整理算法:采用标记整理算法实现
  • 增量回收模式:将Heap分割成多个Region,并在后台维护一个优先列表,每次根据允许的时间,优先回收垃圾最多的区域。

因此G1收集器可以实现在基本不牺牲吞吐量的情况下完成低停顿的内存回收,这是正式由于他极力的避免全区域回收。

总结

垃圾收集器 特性 算法 优点 缺点
Serial 串行 复制 高效:无线程切换 无法利用多核CPU
ParNew 并行 复制 可利用多核CPU、唯一能与CMS配合的并行收集器
Parallel Scavenge 并行 复制 高吞吐量
Serial Old 串行 标记整理 高效 无法利用多核CPU
Parallel Old 并行 标记整理 高吞吐量
CMS 并行 标记清除 低停顿 CPU敏感,浮动垃圾,内存碎片
G1 并行 增量回收 低停顿。高吞吐量 内存使用效率低:分区导致内存不能充分使用

5、Java分配机制

java中符合“编译时可知,运行时不可变”要求的主要是静态方法和私有方法。因此适合在类加载时进行解析。

java虚拟机中有四种方法调用指令:

  • invokestatic:调用静态方法。
  • invokespecial:调用实例构造方法,私有方法和super
  • invokeinterface:调用接口方法
  • invokevirtual:调用以上指令不能调用的方法(虚方法)。

只要能被static和special指令调用的方法,都可以在解析阶段唯一确定调用版本,符合条件的有:静态方法,私有方法,实例构造器,父类方法,他们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法称为非虚方法, 反之称为虚方法,(final除外)。

虽然final方法是使用virtual指令来调用,但是无法被覆盖,多态的选择是唯一的,所以是一种虚方法。

静态分派

对于类字段的访问也采用静态分派

People man = new Man()

静态分派主要针对重载,方法调用时如何选择。在上面的代码,Peopel被称为变量的引用类型,Man称为变量的实际类型。静态类型在编译时可知,动态类型在运行时可知。

编译器在重载时候通过参数的静态类型而不是实际类型作为判断依据。并且静态类型咋编译时可知,所以编译器根据重载的参数的静态类型进行方法选择。

在某些情况下有多个重载,那编译器如何选择呢? 编译器会选择”最合适”的函数版本,那么怎么判断”最合适“呢?越接近传入参数的类型,越容易被调用。

动态分派

主要针对重写,使用virtual指令调用,virtual指令多态查找过程:

  • 找到操作数栈顶的第一个元素所指向的对象的实际类型,记为C
  • 如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果权限校验不通过,返回java.lang.IllegalAccessError异常。
  • 否则,按照继承关系从下往上一次对C的各个父类进行第2步的搜索和验证过程。
  • 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError异常。

虚拟机动态分派的实现

动态分配很繁琐,而且动态分派的方法版本的选择需要考虑运行时咋类的方法数据中搜索合适的目标方法,因为在虚拟机的实现中基于性能的考虑,在方法区中建立一个虚方法表来提高性能。

image-20210324125440920

虚方法表中存放各个方法的实际入口地址。如果某个方法在子类中没有重写,那么子类的虚方法表里的入口和父类入口一致,如果子类重写了这个方法,那么子类方法表中的地址会被替换成子类实现版本的入口地址。

6、String常量池

常量池类似于一个JAVA系统级别提供的缓存。

String类型的常量池比较特殊,使用方法:

  • 直接使用双引号声明的String对象会直接存储在常量池。
  • 不是使用双引号,可以使用String提供的inter方法,该方法会从字符串常量池中查询当前字符串是否存在,不存在则放入常量池。

Intern

java使用Jni调用C++实现的StringTable的Intern方法,StringTable跟java中的HashMap实现差不多,但是不能扩容。默认大小是1009

要注意的是, StringString Pool 是一个固定大小的 Hashtable ,默认值大小长度是 1009 ,如果放进 String PoolString 非常多,就会造成 Hash 冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用 String.intern 时性能会大幅下降。

JDK6中StringTable固定,但是JDK7可以通过参数指定:

1
-XX:StringTableSize=99991

在6和以前的版本,字符串的常量池存放在Perm区,7的版本中,转移到正常的Heap区

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);

String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
}

执行结果:

  • JDK6:false false
  • JDK7:fale true
1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
String s = new String("1");
String s2 = "1";
s.intern();
System.out.println(s == s2);

String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);
}

执行结果:

  • 6:false false
  • 7:false false

由于7将字符串常量池放到Heap中,导致差异。

JDK 6

1

黑色线表示String对象的内容指向,红色代表地址指向。

在jdk6中所有打印都是false,因为常量池在Perm中,Perm和正常的Heap是分开的,引号声明的字符串直接在常量池生成,而new出来的String存放在Heap,地址肯定不同,及时调用String.intern()方法也是没有很合关系。

JDK 7

java虚拟机-第 5 页

  • 第一段代码,String s3 = new String("1")+new String("1");,代码生成两个最终对象,是常量池中的“1”和Heap中S3引用指向的对象。此时S3引用的对象是“11”,但是常量池没有“11”对象。
  • 接下来S3.intern()是讲S3中的“11”放入常量池。但是7中的常量池不在Perm区域了,常量池中不需要再存储一份对象,可以直接存储占中的引用。这份引用指向S3的对象。引用地址是相同的。
  • 最后 String s4 = "11"; 这句代码中 ”11” 是显示声明的,因此会直接去常量池中创建,创建的时候发现已经有这个对象了,此时也就是指向 s3 引用对象的一个引用。所以 s4 引用就指向和 s3 一样了。因此最后的比较 s3 == s4true
  • 再看 ss2 对象。 String s = new String("1"); 第一句代码,生成了2个对象。常量池中的 “1”JAVA Heap 中的字符串对象。s.intern(); 这一句是 s 对象去常量池中寻找后发现 “1” 已经在常量池里了。
  • 接下来 String s2 = "1"; 这句代码是生成一个 s2 的引用指向常量池中的 “1” 对象。 结果就是 ss2 的引用地址明显不同。

接下来是第二段代码:

java虚拟机-第 6 页

  • 第一段代码和第二段代码的改变就是 s3.intern(); 的顺序是放在 String s4 = "11"; 后了。这样,首先执行 String s4 = "11"; 声明 s4 的时候常量池中是不存在 “11” 对象的,执行完毕后, “11“ 对象是 s4 声明产生的新对象。然后再执行 s3.intern(); 时,常量池中 “11” 对象已经存在了,因此 s3s4 的引用是不同的。
  • 二段代码中的 ss2 代码中,s.intern();,这一句往后放也不会有什么影响了,因为对象池中在执行第一句代码String s = new String("1"); 的时候已经生成 “1” 对象了。下边的 s2 声明都是直接从常量池中取地址引用的。 ss2 的引用地址是不会相等的。

小节

对intern和常量池都做了一定的修改,主要包括:

  • 将String常量池从Perm区移动到了Heap区
  • String.intern方法时,如果heap中存在对象,将会直接保存对象的引用,而不是重新创建对象。

使用范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static final int MAX = 1000 * 10000;
static final String[] arr = new String[MAX];

public static void main(String[] args) throws Exception {
Integer[] DB_DATA = new Integer[10];
Random random = new Random(10 * 10000);
for (int i = 0; i < DB_DATA.length; i++) {
DB_DATA[i] = random.nextInt();
}
long t = System.currentTimeMillis();
for (int i = 0; i < MAX; i++) {
//arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length]));
arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern();
}

System.out.println((System.currentTimeMillis() - t) + "ms");
System.gc();
}

一条是使用 intern,一条是未使用 intern。

通过上述结果,我们发现不使用 intern 的代码生成了 1000w 个字符串,占用了大约 640m 空间。 使用了 intern 的代码生成了 1345 个字符串,占用总空间 133k 左右。其实通过观察程序中只是用到了 10 个字符串,所以准确计算后应该是正好相差 100w 倍。虽然例子有些极端,但确实能准确反应出 intern 使用后产生的巨大空间节省。

intern 方法后时间上有了一些增长。这是因为程序中每次都是用了 new String 后,然后又进行 intern 操作的耗时时间,这一点如果在内存空间充足的情况下确实是无法避免的,但我们平时使用时,内存空间肯定不是无限大的,不使用 intern 占用空间导致 jvm 垃圾回收的时间是要远远大于这点时间的。

7、对象生命周期

类实例化

实例化类的四个途径:

  • new
  • 调用class或者java.lang.reflect.Constructor对象的NewInstance
  • 调用任何现有对象的clone
  • 通过java.io.ObjectInputStream.getObject()反序列化。

隐含的实例化:

  • 保存命令行参数的String对象
  • 虚拟机装载的每个类,都会暗中实例化一个class对象来代表这个类型。
  • 当Java虚拟机装载了在常量池中包含CONSTANT_String_info入口的类的时候,它会创建新的String对象来表示这些常量字符串。
  • 执行包含字符串连接操作符的表达式会产生新的对象。

垃圾收集和对象的终结

程序可以分配内存但不能释放内存,一个对象不再为程序引用,虚拟机必须回收那部分内存。

卸载类

虚拟机中类的生命周期和对象的生命周期相似。当程序不再使用某个类的时候,可以选择卸载他们。

虚拟机通过判断类是否在被引用来实现垃圾收集。判断动态装载类的Class实例在正常的垃圾收集过程中是否可触及有两种方式:

  • 如果实例非Class实例的明确引用。
  • 如果在堆中还存在一个可触及对象,在方法区中它的类型数据指向一个Class实例。

image-20210324173320715

Spring

1、Spring

1.1 简介

  • Spring:春天—->给软件行业带来了春天!
  • 2002,首次推出了Spring框架的雏形:interface21框架!
  • Spring框架即以interface21框架为基础,经过重新设计,并不断丰富内涵,于2004年3月24日,发布了1.0正式版。
  • Rod Johnson,Spring Framework创始人,著名作者。很难想象其学历,真的让好多人大吃一惊,他是悉尼大学的博士,然而他的专业不是计算机,而是音乐学。
  • spring理念:使现有的技术更加容易使用,本身是一个大杂烩,整合了现有的技术框架。
  • SSH:Struct2+Spring+Hibernate!
  • SSM:SpringMVC+Spring+Mybatis!

官网:https://spring.io/projects/spring-framework#overview

官方下载地址:https://repo.spring.io/release/org/springframework/spring/

Github:https://github.com/spring-projects/spring-framework

Maven仓库:导入webmvc包会自动导入相关依赖;jdbc用于和Mybatis整合。

1
2
3
4
5
6
7
8
9
10
11
12
<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>

1.2 优点

  • Spring是一个开源的免费的框架(容器)!

  • Spring是一个轻量级的、非入侵式的框架!

  • 控制反转(IOC)、面向切面编程(AOP)!

  • 支持事务的处理,对框架整合的支持!

    总结一句话:Spring就是一个轻量级的控制反转(IOC)和面向切面编程的框架!

1.3 组成

image-20200102001447503

1.4 拓展

在Spring的官网有这个介绍:现代化的java开发!说白了就是基于Spring的开发!

image-20200102001823229

  • Spring Boot
    • 一个快速开发的脚手架。
    • 基于Spring Boot可以快速的开发单个微服务。
    • 约定大于配置!
  • Spring Cloud
    • SpringCloud是基于SpringBoot实现的。

因为现在大多数公司都在使用SpringBoot进行快速开发,学习SpringBoot的前提,需要完全掌握Spring以及SpringMVC!承上启下的作用。

弊端:发展了太久之后,违背了原来的理念!配置十分繁琐,人称:“配置地狱”。

2、 IOC理论推导

1.UserDao接口

2.UserDaoImpl实现类

3.UserService业务接口

4.UserServiceImpl业务实现类

在我们之前的业务中,用户的需求可能会影响我们原来的代码,我们需要根据用户的需求去修改源代码!如果程序代码量十分大,修改一次的成本代价十分昂贵!

我们使用一个Set接口实现,已经发生了革命性的变化!

1
2
3
4
5
6
private UserDao userDao;

//利用set进行动态实现值的注入!
public void setUserDao(UserDao userDao){
this.userDao = userDao;
}
  • 之前,程序是主动创建对象!控制权在程序员手上!
  • 使用了set注入后,程序不再具有主动性,而是变成了被动的接收对象!

这种思想,从本质上解决了问题,我们程序员不用再去管理对象的创建了。系统的耦合性大大降低,可以更加专注在业务的实现上。这是IOC的原型!

image-20200102111735712

image-20200102111753076

IOC本质

控制反转IOC(Inversion of Control),是一种设计思想,DI(依赖注入)是实现IOC的一种方法,也有人认为DI是IoC的另一种说法。没有IoC的程序中,我们使用面向对象编程,对象的创建与对象间的依赖关系完全硬编码在程序中,对象的创建由程序自己控制,控制反转后将对象的创建转移给第三方,个人认为所谓控制反转就是:获得依赖对象的方式反转了。

采用XML方式配置Bean的时候,Bean的定义信息是和实现分离的,而采用注解的方式可以把两者合为一体,Bean的定义信息直接以注解的形式定义在实现类中,从而达到了零配置的目的。

控制反转是一种通过描述(XML或注解)并通过第三方去生产或获取特定对象的方式。在Spring中实现控制反转的是IoC容器,其实现方式是依赖注入(Dependency Injection,DI)

3、 Hello Spring

beans.xml官网配置文件:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

</beans>

bean对象添加:

1
2
3
4
5
6
7
8
9
10
<bean id="mysqlImpl" class="com.kuang.dao.UserDaoMysqlImpl"></bean>
<bean id="oracleImpl" class="com.kuang.dao.UserDaoOracleImpl"></bean>

<bean id="UserServiceImpl" class="com.kuang.service.UserServiceImpl">
<!--
ref:引用Spring容器中已经创建好的对象
value:具体的值,基本数据类型
-->
<property name="userDao" ref="mysqlImpl"></property>
</bean>

Test方法:

1
2
3
4
5
//解析beans.xml文件,生成管理相应的Bean对象
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
//getBean:参数即为spring配置文件中bean的id
Hello hello = (Hello) context.getBean("hello");
System.out.println(hello.toString());

思考问题

  • Hello对象是谁创建的?

    hello对象是由Spring创建的。

  • Hello对象的属性是怎么设置的?

    hello对象的属性是由Spring容器设置的。

这个过程就叫做控制反转:

控制:谁来控制对象的创建,传统应用程序的对象是由程序本身控制创建的,使用Spring后,对象是由Spring来创建的。

反转:程序本身不创建对象,而变成被动的接收对象。

依赖注入:就是利用set方法来进行注入。

IoC是一种编程思想,由主动的编程编程被动的接收。

可以通过new ClassPathXmlApplicationContext去浏览一下底层源码。

OK,到了现在,我们彻底不用在程序中去改动了,要实现不同的操作,只需要在xml配置文件中进行修改,所谓的IoC,一句话搞定:对象由Spring来创建,管理,装配!

IDEA快捷创建beans.xml文件,自动导入spring配置信息:

image-20200102152125077

4、 IoC创建对象的方式

  1. 使用无参构造创建对象,默认方式!

  2. 假设我们要使用有参构造创建对象。

    1.下标赋值。

    1
    2
    3
    4
    <!--第一种,下标赋值!-->
    <bean id="user" class="com.kuang.pojo.User">
    <constructor-arg index="0" value="憨批" />
    </bean>

    2.类型赋值。

    1
    2
    3
    4
    <!--第二种,通过类型创建,不建议使用,重复类型难以分辨-->
    <bean id="user" class="com.kuang.pojo.User">
    <constructor-arg type="java.lang.String" value="大憨批" />
    </bean>

    3.参数名赋值。

    1
    2
    3
    4
    <!--第三种,直接通过参数名来设置-->
    <bean id="user" class="com.kuang.pojo.User">
    <constructor-arg name="name" value="臭憨批" />
    </bean>

总结:在配置文件加载的时候,容器中管理的对象就已经初始化了!

5、 Spring配置

5.1 别名

1
2
<!--别名,如果添加了别名,我们也可以使用别名获取到-->
<alias name="user" alias="userNew"></alias>

5.2 Bean的配置

1
2
3
4
5
6
7
<!--
id:bean的唯一标识符,相当于我们学的对象名;
class:bean对象所对应的全限定名:包名+类名;
name:也是别名,可以同时取多个别名,逗号分割
-->
<bean id="userT" class="com.kuang.pojo.UserT" name="user2,u2">
</bean>

5.3 import

这个import,一般用于团队开发使用,他可以将多个配置文件,导入合并为一个。

假设,现在项目中有多个人开发,这三个人负责不同的类开发,不同的类需要注册在不同的bean中,我们可以利用import将所有人的beans.xml合并为一个总的!

  • 张三
  • 李四
  • 王五
  • applicationContext.xml
1
2
3
<import resource="beans.xml"/>
<import resource="beans2.xml"/>
<import resource="beans3.xml"/>

使用的时候,直接使用总的配置就可以了。

6、 依赖注入

6.1 构造器注入

之前已经介绍过。

6.2 Set方式注入【重点】

  • 依赖注入:Set注入!
    • 依赖:bean对象的创建依赖于容器!
    • 注入:bean对象中的所有属性,由容器来注入!

【环境搭建】

  1. 复杂类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Address {
    private String address;

    public String getAddress() {
    return address;
    }

    public void setAddress(String address) {
    this.address = address;
    }
    }
  2. 真实测试对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class Student {
    private String name;
    private Address address;
    private String[] books;
    private List<String> hobbies;
    private Map<String,String> card;
    private Set<String> games;
    private String wife;
    private Properties info;
    }
    1. beans.xml

      1
      2
      3
      4
      5
      6
      7
      8
      9
      <?xml version="1.0" encoding="UTF-8"?>
      <beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
      <bean id="student" class="com.kuang.pojo.Student">
      <!--第一种,普通值注入-->
      <property name="name" value="憨批"/>
      </bean>
      </beans>
    2. 测试类

      1
      2
      3
      4
      5
      6
      7
      public class MyTest {
      public static void main(String[] args) {
      ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
      Student student = (Student) context.getBean("student");
      System.out.println(student.getName());
      }
      }

    完善注入:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="address" class="com.kuang.pojo.Address"/>

    <bean id="student" class="com.kuang.pojo.Student">
    <!--第一种,普通值注入,value-->
    <property name="name" value="憨批"/>
    <!--第二种,Bean注入,ref-->
    <property name="address" ref="address"/>
    <!--数组注入-->
    <property name="books">
    <array>
    <value>红楼梦</value>
    <value>西游记</value>
    <value>水浒传</value>
    <value>三国演义</value>
    </array>
    </property>
    <!--List注入-->
    <property name="hobbies">
    <list>
    <value>听歌</value>
    <value>敲代码</value>
    <value>看电影</value>
    </list>
    </property>
    <!--Map-->
    <property name="card">
    <map>
    <entry key="身份证" value="1555555555"/>
    <entry key="银行卡" value="5555555555"/>
    </map>
    </property>
    <!--Set-->
    <property name="games">
    <set>
    <value>lol</value>
    <value>wow</value>
    </set>
    </property>
    <!--null-->
    <property name="wife">
    <null/>
    </property>
    <!--Properties-->
    <property name="info">
    <props>
    <prop key="driver">com.mysql.jdbc.Driver</prop>
    <prop key="url">jdbc:mysql://localhost:3306/news</prop>
    <prop key="root">root</prop>
    <prop key="password">123456</prop>
    </props>
    </property>

    </bean>

    </beans>

6.3 拓展方式注入

我们可以使用c和p命令空间进行注入:

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<!--p命名空间注入,可以直接注入属性的值:property-->
<bean id="user" class="com.kuang.pojo.User" p:name="憨批" p:age="18"/>

<!--c命名空间注入,通过构造器注入:construct-args-->
<bean id="user2" class="com.kuang.pojo.User" c:age="18" c:name="憨批"/>

</beans>

测试:

1
2
3
4
5
6
@Test
public void test2(){
ApplicationContext context = new ClassPathXmlApplicationContext("userBeans.xml");
User user = context.getBean("user2", User.class);
System.out.println(user);
}

注意点:p和c命名空间不能直接使用,需要导入xml约束!

1
2
xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"

6.4 bean的作用域

  1. 代理模式(Spring默认机制):get到的都是同一个对象!

    1
    <bean id="user2" class="com.kuang.pojo.User" c:age="18" c:name="憨批" scope="singleton"/>
  2. 原型模式:每次从容器中get的时候,都会产生一个新的对象!

    1
    <bean id="user2" class="com.kuang.pojo.User" c:age="18" c:name="憨批" scope="prototype"/>
  3. 其余的request、session、application、这些个只能在web开发中使用。

7、 Bean的自动装配

  • 自动装配是Spring满足bean依赖的一种方式!
  • Spring会在上下文中自动寻找,并自动给bean装配属性!

在Spring中有三种装配的方式:

  1. 在xml中显式配置;
  2. 在java中显式配置;
  3. 隐式的自动装配bean

7.1 测试

环境搭建:一个人有两个宠物!

7.2 ByName自动装配

1
2
3
4
5
6
<!--
byName:会自动在容器上下文中查找和自己对象set方法后面的值对应的beanid!
-->
<bean id="people" class="com.kuang.pojo.People" autowire="byName">
<property name="name" value="憨批"/>
</bean>

7.3 ByType自动装配

1
2
3
4
5
6
<!--
byType:会自动在容器上下文中查找,和自己对象属性类型相同的bean!必须保证类型全局唯一。
-->
<bean id="people" class="com.kuang.pojo.People" autowire="byType">
<property name="name" value="憨批"/>
</bean>

小结:

  • byName的时候,需要保证所有bean的id唯一,并且这个bean需要和自动注入的属性的set方法的值一致!
  • byType的时候,需要保证所有bean的class唯一,并且这个bean需要和自动注入的属性的类型一致!

7.4 使用注解实现自动装配

jdk1.5支持注解,Spring2.5开始支持注解。

要使用注解须知:

  1. 导入约束:context约束。

  2. 配置注解的支持:context:annot-config/

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    https://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

    </beans>

@Autowired

直接在属性上使用即可!也可以在set方式上使用!

使用Autowired我们可以不用编写Set方法了,前提是你这个自动装配的属性在IoC(Spring)容器中存在,且符合名字byName!

科普:

1
2
3
4
@Nullable	字段标记了这个注解,说明这个字段可以为null
public People(@Nullable String name){
this.name = name;
}
1
2
3
public @interface Autowired {
boolean required() default true;
}

测试代码:

1
2
3
4
5
6
7
8
public class People {
//如果显式定义了Autowired的required属性为false,说明这个对象可以为null,否则不允许为空
@Autowired(required = false)
private Dog dog;
@Autowired
private Cat cat;
private String name;
}

如果@Autowired自动装配的环境比较复杂,自动装配无法通过一个注解@Autowired完成的时候,我们可以使用@Qualifier(value=”xxx”)去配置@Autowired的使用,指定一个唯一的bean对象注入!

1
2
3
4
5
6
7
8
9
public class People {
@Autowired
@Qualifier(value="dog11")
private Dog dog;
@Autowired
@Qualifier(value="cat11")
private Cat cat;
private String name;
}

@Resource注解

1
2
3
4
public class People {
@Resource(name = "cat2")
private Cat cat;
}

小结:

@Resource和@Autowired的区别:

  • 都是用来自动装配的,都可以放在属性字段上;

  • @Autowired通过byType的方式实现,而且必须要求这个对象存在!【常用】

  • @Resource默认通过byName的方式实现,如果找不到名字,则通过byType实现!如果两个都找不到的情况下,就报错!

  • 执行顺序不同:@Autowired通过byType的方式实现,@Resource默认通过byName的方式实现。

8、 使用注解开发

在spring4之后,要使用注解开发,必须要保证aop的包导入了。

image-20200103105725321

使用注解需要导入context约束,增加注解的支持!

1
2
<!--指定要扫描的包,这个包下的注解会生效-->
<context:component-scan base-package="com.kuang.pojo"/>
  1. bean

  2. 属性如何注入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //等价于<bean id="user" class="com.kuang.pojo.User"/>
    //@Component 组件
    @Component
    public class User {
    //相当于<property name="name" value="小憨批"/>
    public String name;
    @Value("小憨批")
    public void setName(String name){
    this.name = name;
    }
    }
  3. 衍生的注解

    @Component有几个衍生注解,我们在web开发中,会按照mvc三层架构分层!

    • dao【@Repository】
    • service【@Service】
    • controller【@Controller】

    这四个注解功能都是一样的,都是代表将某个类注册到Spring中,装配Bean!

  4. 自动装配

    1
    2
    3
    4
    -@Autowired:自动装配通过类型,名字
    如果Autowired不能唯一自动装配上属性,则需要通过@Qualifier(value="xxx")
    -@Nullable:字段标记了这个注解,说明这个字段可以为null
    -@Resource:自动装配通过名字,类型
  5. 作用域

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Scope("singleton")
    public class User {
    //相当于<property name="name" value="小憨批"/>
    public String name;
    @Value("小憨批")
    public void setName(String name){
    this.name = name;
    }
    }
  6. 小结

    xml与注解:

    • xml更加万能,适用于任何场合!维护简单方便。
    • 注解,不是自己的类使用不了,维护相对复杂!

    xml与注解最佳实践:

    • xml用来管理bean;
    • 注解只负责完成属性的注入;
    • 我们在使用的过程中,只需要注意一个问题:必须让注解生效,就需要开启注解的支持。
    1
    2
    3
     <!--指定要扫描的包,这个包下的注解会生效-->
    <context:component-scan base-package="com.kuang"/>
    <context:annotation-config/>

9、 使用java的方式配置Spring

我们现在要完全不适用Spring的xml配置了,全权交给java来做!

javaConfig是Spring的一个子项目,在Spring4之后,它成为了一个核心功能。

实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
public class User {
private String name;

public String getName() {
return name;
}

@Value("小笨蛋")
public void setName(String name) {
this.name = name;
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.kuang.pojo.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

//这个也会被Spring容器托管,注册到容器中,因为本来就是一个@Component
//@Configuration代表这是一个配置类,就和我们之前看的beans.xml
@Configuration
@ComponentScan("com.kuang.pojo")
@Import(KuangConfig2.class )
public class KuangConfig {
//注册一个bean,就相当于我们之前写的一个bean标签
//这个方法的名字,就相当于bean标签中的id属性
//这个方法的返回值,就相当于bean标签中的class属性
@Bean
public User getUser(){
return new User();//就是返回要注入到bean的对象
}
}

测试类:

1
2
3
4
5
6
7
8
public class MyTest {
public static void main(String[] args) {
//如果完全使用了配置类方式去做,我们就只能通过AnnotationConfig上下文来获取容器,通过配置类的class对象加载!
ApplicationContext context = new AnnotationConfigApplicationContext(KuangConfig.class);
User getUser = (User) context.getBean("getUser");
System.out.println(getUser.getName());
}
}

这种纯java的配置方式,在SpringBoot中随处可见!

10、 代理模式

为什么要学习代理模式?因为这就是SpringAOP的底层!【SpringAOP 和 SpringMVC 面试必问】

代理模式的分类:

  • 静态代理
  • 动态代理

image-20200104125508118

10.1 静态代理

角色分析:

  • 抽象角色:一般会使用接口或者抽象类来解决
  • 真实角色:被代理的角色
  • 代理角色:代理真是角色,代理真实角色后,我们一般会做一些附属操作。
  • 客户:访问代理对象的人!

代码步骤:

  1. 接口

    1
    2
    3
    4
    //租房
    public interface Rent {
    public void rent();
    }
  2. 真实角色

    1
    2
    3
    4
    5
    6
    //房东
    public class Host implements Rent {
    public void rent(){
    System.out.println("房东要出租房子!");
    }
    }
  3. 代理角色

    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
    public class Proxy implements Rent {
    private Host host;

    public Proxy() {
    }
    public Proxy(Host host) {
    this.host = host;
    }

    public void rent(){
    seeHouse();
    host.rent();
    hetong();
    fee();
    }
    //看房
    public void seeHouse(){
    System.out.println("中介带你看房");
    }
    //签合同
    public void hetong(){
    System.out.println("签合同");
    }
    //收费
    public void fee(){
    System.out.println("收取中介费用");
    }
    }
  4. 客户端访问

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Client {
    public static void main(String[] args) {
    //房东要租房子
    Host host = new Host();
    //代理,中介帮房东租房子,但是呢?代理角色一般会有一些附属操作!
    Proxy proxy = new Proxy(host);
    proxy.rent();
    }
    }

代理模式的好处:

  • 可以使真实角色的操作更加纯粹!不用去关注一些公共的业务
  • 公共也就交给代理角色!实现了业务的分工!
  • 公共业务发生扩展的时候,方便集中管理!

缺点:

  • 一个真实角色就会产生一个代理角色,代码量会翻倍,开发效率会变低

10.2 加深理解

代码:对应08-demo02

聊聊AOP

image-20200105210505898

10.3 动态代理

  • 动态代理和静态代理角色一样
  • 动态代理的代理类是动态生成的,不是我们直接写好的。
  • 动态代理分为两大类:基于接口的动态代理,基于类的动态代理
    • 基于接口——JDK动态代理
    • 基于类:cglib
    • java字节码实现:javasisit

需要了解两个类:Proxy:代理;InvocationHandler:调用处理程序

动态代理的好处:

  • 可以使真实角色的操作更加纯粹!不用去关注一些公共的业务
  • 公共也就交给代理角色!实现了业务的分工!
  • 公共业务发生扩展的时候,方便集中管理!
  • 一个动态代理类代理类代理的是一个接口,一般就是对应的一类业务
  • 一个动态代理类可以代理多个类,只要是实现了同一个接口即可!

11、 AOP

11.1 什么是AOP

AOP(Aspect Oriented Programming)意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生泛型,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的频率。

image-20200106085441897

11.2 AOP在Spring中的作用

==提供声明式事务;允许用户自定义切面==

  • 横切关注点:跨越应用程序多个模块的方法或功能。即是,与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点,如日志、安全、缓存、事务等等……
  • 切面(ASPECT):横切关注点被模块化的特殊对象,即是一个类。
  • 通知(Advice):切面必须要完成的工作,即是类中的一个方法。
  • 目标(Target):被通知对象。
  • 代理(Proxy):向目标对象应用通知之后创建的对象。
  • 切入点(PointCut):切面通知执行的“地点”的定义。
  • 连接点(jointPoint):与切入点匹配的执行点。

image-20200106090325307

SpringAOP中,通过Advice定义横切逻辑,Spring中支持5种类型的Advice:

image-20200106090428369

即AOP在不改变原有代码的情况下,去增加新的功能。

11.3 使用Spring实现AOP

【重点】使用AOP织入,需要导入一个依赖包。

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>

方式一:使用Spring的API接口【主要SpringAPI接口实现】

方式二:自定义来实现AOP【主要是切面定义】

方式三:使用注解实现

12、 整合Mybatis

步骤:

  1. 导入相关jar包
    • junit
    • Mybatis
    • mysql数据库
    • spring相关的
    • aop织入
    • mybatis-spring【new知识点】
  2. 编写配置文件
  3. 测试

12.1 回忆mybatis

  1. 编写实体类
  2. 编写核心配置文件
  3. 编写接口
  4. 编写Mapper.xml
  5. 测试

12.2 Mybatis-Spring

  1. 编写数据源配置
  2. sqlSessionFactory
  3. sqlSessionTemplate
  4. 需要给接口加实现类
  5. 将自己写的实现类,注入到Spring中
  6. 测试

13、 声明式事务

13.1 回顾事务

  • 把一组业务当成一个业务来做:要么都成功,要么都失败。
  • 事务在项目开发中,十分的重要,涉及到数据的一致性问题,不能马虎。
  • 确保完整性和一致性。

事务ADID原则:

  • 原子性
  • 一致性
  • 隔离性
    • 多个业务可能操作同一个资源,防止数据损坏
  • 持久性
    • 事务一旦提交,无论系统发生什么问题,结果都不会再被影响,被持久化的写到存储器中。

13.2 Spring中的事务管理

  • 声明式事务:AOP
  • 编程式事务:需要在代码中,进行事务的管理

思考:

为什么需要事务?

  • 如果不配置事务,可能存在数据提交不一致的情况
  • 如果我们不在Spring中去配置声明式事务,我们就需要在代码中手动配置事务
  • 事务在项目的开发中十分重要,涉及到数据的一致性和完整性问题,不容马虎

总结

参考自javaGuide

什么是Spring框架

我们一般说 Spring 框架指的都是 Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是:核心容器、数据访问/集成,、Web、AOP(面向切面编程)、工具、消息和测试模块。比如:Core Container 中的 Core 组件是Spring 所有组件的核心,Beans 组件和 Context 组件是实现IOC和依赖注入的基础,AOP组件用来实现面向切面编程。

列举一些重要的Spring模块

  • core:基础,主要提供IOC依赖注入功能。
  • AOP:面向切面编程的实现
  • JDBC :java数据库连接
  • JMS:消息服务
  • WEB:创建web应用程序提供支持
  • Test:提供了对JUnit和TestNG测试的支持。

@RestController和@Controller

restcontroller在spring4之后才有,之前必须使用controller+responsebody

IOC和AOP

  • 对IOC和AOP的理解

    IOC容器实际上就是一个Map(key,value),存放的是各种对象。spring时代通过xml配置bean,在boot时代通过注解配置bean。

    AOP能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码降低模块间的耦合度,并有利于未来的可拓展性和可维护性。基于动态代理的。如果代理的对象,实现了某个接口,使用JDK,没有实现的使用CGLAB代理。

Spring Bean

  • 作用域

    • singleton:唯一bean实例,默认是单例。
    • protype:每次请求都会创建一个新的bean实例
    • request:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。
    • session :每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效。
    • global-session: 全局session作用域,仅仅在基于portlet的web应用中才有意义,Spring5已经没有了。
  • 单例bean的线程安全

    一般情况下,我们常用的 ControllerServiceDao 这些 Bean 是无状态的。无状态的 Bean 不能保存数据,因此是线程安全的。

    常见的两种解决办法:

    • 在类中定义threadLocal成员变量。
    • 改变 Bean 的作用域为 “prototype”:每次请求都会创建一个新的 bean 实例,自然不会存在线程安全问题。
  • @Compnen和@Bean的区别

  • 将一个类声明为Sprng的bean的注解

    一般使用@Autowired注解自动装配bean,想把类标识成可用于autowired自动装配的类,可以使用以下注解:

    • @Component :通用的注解,可标注任意类为 Spring 组件。如果一个Bean不知道属于哪个层,可以使用@Component 注解标注。
    • @Repository : 对应持久层即 Dao 层,主要用于数据库相关操作。
    • @Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao层。
    • @Controller : 对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。
  • bean的生命周期

    Spring Bean 生命周期

SpringMVC

  • 对MVC的理解

    是一种设计模式。

    img

    • Model1时代JSP
    • Model2时代Bean(model)+JSP(View)+Servlet(controller)
  • MVC的工作原理

    1. 客户端(浏览器)发送请求,直接请求到 DispatcherServlet
    2. DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler
    3. 解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由 HandlerAdapter 适配器处理。
    4. HandlerAdapter 会根据 Handler 来调用真正的处理器开处理请求,并处理相应的业务逻辑。
    5. 处理器处理完业务后,会返回一个 ModelAndView 对象,Model 是返回的数据对象,View 是个逻辑上的 View
    6. ViewResolver 会根据逻辑 View 查找实际的 View
    7. DispaterServlet 把返回的 Model 传给 View(视图渲染)。
    8. View 返回给请求者(浏览器)

Spring框架中用到的设计模式

  • 工厂模式:创建Bean
  • 代理模式:AOP
  • 单例模式:Bean单例模式
  • 包装器设计模式:Spring 中 jdbcTemplatehibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
  • 观察者模式:事件驱动

Spring管理事务

  • 管理事务的方式

    • 编程式,不推荐。

    • 声明式事务

      • 基于XML
      • 基于注解
  • 事务的隔离级别

  • 哪几种事务传播行为五种

  • Transactional(rollback=Exception.class)

    @Transactional注解中如果不配置rollbackFor属性,那么事务只会在遇到RuntimeException的时候才会回滚,加上rollbackFor=Exception.class,可以让事务在遇到非运行时异常时也回滚。

使用JPA在数据库中非持久化一个字段。

@Transient

redis

简单介绍

使用C语言开发的k-v数据库,数据存储在内存,读写速度很快,一般用于缓存方向,也可以用来做分布式锁,消息队列。提供了五中数据类型String,hash,list,set,zset。支持事务,持久化,lua脚本,多重集群方案。

分布式缓存常见的技术选型

Memcached和Redis。

前者是分布式缓存刚兴起,后来随着Redis发展,都是用redis了。分布式缓存主要解决的问题是,单击缓存的容量收到服务器限制且无法保存通用的消息。因为本地缓存只在当前服务有效。

redis和Memcached的区别和共同点

共同点:

  • 基于内存
  • 过期策略
  • 高性能

区别:

  • redis数据类型更丰富-五种,后者只是简单的k-v
  • redis数据持久化,重启的时候可以再次加载使用。后者单纯放在内存。
  • redis灾难恢复机制。因为可以持久化
  • redis在内存用完之后,可以将数据放磁盘,后者直接报异常。
  • M没有原生的集群模式。redis原生支持cluster模式
  • M多线程非阻塞IO复用的网络模型;R单线程多路IO复用模型(R6.0之后引入多线程)
  • R支持发布订阅模型,Lua脚本,事务。M不支持。
  • M过期数据只用了惰性删除,R使用了惰性删除和定期删除。

缓存数据的处理流程

  • 数据在缓存直接返回
  • 不在缓存查数据库
  • 数据库存在更新缓存
  • 数据库不在返回空

为什么用R缓存

高性能:

直接访问数据库会比较慢,因为要从次硬盘读取。对于某些不常改变的高频数据,可以放在缓存中,访问时直接存缓存读取,这样比较快。需要保证数据一致性,当数据库的数据改变时,缓存的数据也要同步更新。

高并发:

MYSQL的QPS在1W左右(4core 8G),使用R之后可以达到10W-30W+。

QPS:服务器每秒可以执行的查询次数。

R单线程模型

Redis 基于 Reactor 模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler)。文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据 套接字目前执行的任务来为套接字关联不同的事件处理器。

当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关 闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。

可以看出,文本事件处理器主要是4个部分:

  • 多个socket(客户端连接)
  • IO多路复用(支持多个客户端连接的关键)
  • 文本事件分派器(将socket关联到相应的时间处理器)
  • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

image-20210325164739624

R为什么不使用多线程

单线程模型,但是在4.0之后就加入了对多线程的支持。但是多线程主要是针对一些大键值对的删除操作的命令。

大体来说R还是单线程处理,为何不使用多线程?

  • 单线程编程容易且易维护
  • 性能瓶颈不在CPU,在于内存和网络
  • 多线程存在死锁,线程上下文切换问题,甚至影响性能。

为什么6.0引入多线程。

主要是为了提高网络IO性能瓶颈。

虽然6.0引入多线程,但是只在网路数据的读写这类耗时操作使用,执行命令仍然是单线程。默认是禁用的。

开多线程之后,还需要设置线程数,否则不生效。

1
COPYio-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程

缓存数据设置过期时间有啥用

内存有限。

业务场景需要。(token,短信验证码)

R如何判断数据是否过期呢?

过期字典来保存过期时间。过期字典的k指向数据库的某个key,保存了数据库键的过期时间(毫秒精度的unix时间戳)

过期数据的删除策略

  • 惰性删除:取出key的时候过期检查。CPU友好,但是可能太多过期K没删除
  • 定期删除:每隔一段时间抽取K删除过期K。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。

一种是内存友好,一种CPU友好,所以R是定期+惰性。

仅仅通过K的过期时间有问题,因为可能存在定期删除和惰性漏掉的问题,这样导致大量过期K堆积在内存。–内存淘汰机制。

内存淘汰机制

  • volatile-lru:从已设置过期时间的数据集挑选最少使用的数据淘汰
  • V-ttl:从已设置过期时间的数据集挑选将要过期的数据淘汰。
  • V-random:字面意思
  • allkeys-lru:当内存不足以容纳写入新数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
  • a-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  • no-evition:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!

4.0版本以后增加了两种:

  • v-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
  • a-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

持久化机制

快照和只追加文件

  • 快照持久化

    通过创建快照的方式获得存储在内存里面的数据的某个时间节点的副本。创建快照后可以进行备份,或者复制到其他服务器从而常创建具有相同数据的服务器副本(主从结构)。

    快照是默认的持久化方式

  • AOF持久化

    实时性更好,成为主流的方案,默认不开启,通过appendonly yes开启。

    开启后,每次执行一条更改Redis的命令,redis就会将命写入AOF文件。默认的文件时appendonly.aof。

    在redis存在三种不同的AOF持久化方式,分别是:

    • appendsync always # 每次更细都写AOF
    • appendsync everysec # 每秒钟同步一次
    • appendsync no # OS决定什么时候同步

    兼顾数据和写入性能,选择everysec。对性能几乎没影响。

Redis事务

通过MULTI,EXEC,DISCARD,WATCH命令实现事务。使用 MULTI命令后可以输入多个命令。Redis不会立即执行这些命令,而是将它们放到队列,当调用了EXEC命令将执行所有命令。

R的事务和数据库的事务不太一样,事务四大特性:

  • 原子性:动作要么全部完成,要么不起作用。
  • 一致性:事务提交后,数据保持一致,多个事务对同一个数据库读取的结果是相同的。
  • 隔离性:并发访问数据库,一个用户的事务不被打扰,并发事务之间数据库是独立的。
  • 持久性:一个事物提交后,对数据的改变是持久的,及时数据库发生故障也不能有任何影响。

Redis不支持回滚,不满足原子性。而且不满足持久性。为啥呢?因为他们认为没必要回滚,这样更简单且性能更好。Redis开发者觉得即使命令执行错误也应该在开发过程中就被发现而不是生产过程中。

R的事务可以理解为:提供了一种将多个命令请求打包的功能,然后再按照顺序执行所有命令,并且不会被中途打断。

缓存穿透

  • 是啥?

    大量请求的Key不在缓存,直接到数据库。黑客故意制造缓存中不存在的Key请求,导致大量请求落到数据库。

  • 如何解决?

    最基本的是做好参数校验,不合法的参数直接返回客户端,例如ID不小于0,邮箱格式等。

    • 缓存无效key

      缓存和数据库都查不到,就缓存这个无效key,适用于key变化不频繁的情况。会导致缓存大量无效key,可以尽量将key的过期时间设置短一点,例如1min。

    • 布隆过滤器

      通过它可以方便的判断一个给定数据是否存在于海量数据中。具体的做法是:把所有可能的请求的值放在过滤器,请求过来,先判断请求的值是否在过滤器,不存在的话,返回参数信息错误。存在的话,再去走缓存的流程。

      过滤器时候存在,小概率误判;说不存在,一定不存在。

      • 元素加入过滤器

        1.使用过滤器中的哈希函数对元素值计算,得到哈希值。(几个哈希函数得到几个哈希值)

        2.根据得到的哈希值,数组中对应的下标标记为1

      • 判断是否存在

        1.对给定元素计算哈希值。

        2.得到值之后判断数组中的每个元素位置是否都为1,是,说明在过滤器,存在一个值不是1,说明不在。

      有一种情况:不同字符串可能哈希出来的位置相同。(哈希冲突,最原始的问题)

布隆过滤器

就是一个很长的数组,在存入数据的过程中,先通过三次(也可能是更多次)的哈希计算,然后把这些hash的值都标记为1,在查询的时候,先通过三次hash值,然后去查数组,有一个不是1,则数据一定不存在。但是过滤器很难实现删除操作,本质原因是没有解决哈希冲突。

有点是插入和删除很快。保密性好,因为只存0和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
COPYpublic class test{
private static int size = 1000000;
//期望的误判率
private static double fpp = 0.01;
//布隆过滤器
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(),size,fpp);
private static int total = 1000000;
psvm{
//插入10万样本数据
for(int i = 0;i<total;i++){
bloomFilter.put(i);
}

//用另外十万测试数据,测试误判率
int count = 0;
for(int i = total;i<total+100000;i++){
if(bloomFilter.mightContain(i)){
count++;
sout(i+"误判了");
}
}
sout("总的误判数是:"+count);
}
}

但是误判率不能设置成无线小,这会拖慢过滤器的计算速度。

误判率的底层原理:

给出更多的哈希函数和数组空间,使用多个哈希函数算出来的哈希位置也不一样,多对应的二进制数据也就越多,这样就可以减少重复的概率。

使用布隆过滤器解决redis缓存穿透的问题:

将数据库的数据全部存放在布隆过滤器上,先查过滤器。没有返回,有的哈,从redis查找。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
COPYpublic class RedisBloomFilter{
psvm{
Config config = new Config();
config.useSingleServer.setSddress("redis://127.0.0.1:6379");
config.useSingleServer().setPassword("1234");
//构造过滤器
RedissonClient redisson = Redisson.create(config);
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("phoneList");
//初始化过滤器,预计元素是1000000L,误差率是3%
bloomFilter.tryInit(10000L,0.03);
//将号码10086插入到过滤器
bloomFilter.put("10086");

//判断号码是否在过滤器
sout(bloomFilter.contains("123456"));
}
}

缓存雪崩

缓存在同一时间大面积失效,后面的请求落在数据库,造成短时间内数据库承受大量请求。(缓存模块宕机)

还有一种是有一些被大量访问的数据在某一时刻失效,导致请求落在数据库。

解决办法:

  • 缓存服务宕机
    • redis集群
    • 限流,避免同时处理大量请求。
  • 热点缓存失效:
    • 设置不同的失效时间比如随机设置缓存的失效时间。
    • 缓存永不失效。

如何保证缓存和数据库的一致性

旁路缓存模式

遇到写请求是这样的,更新DB,直接删除缓存。

数据库更新成功缓存删除失败。两个方案:

  • 缓存失效时间变短(不推荐):我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。
  • 增加cache更新重试机制(常用):如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将 缓存中对应的 key 删除即可。

操作系统

进程管理

进程与线程

进程状态的切换

进程调度算法

  • 批处理系统:扫盲

    没有太多用户操作,调度算法的目的是保证吞吐量和周转时间

    • 先来先服务FCFS:不利于短作业
    • 短作业优先SJF:长作业可能饿死
    • 最短剩余时间
  • 交互式系统

    有大量的用户交互操作,调度算法的目标是快速的进行响应。

    • 时间片轮转

      就绪进程FCFS进队,给队首分配时间片,用完之后计时器发出时钟中断,送至队尾,然胡队首分时间片。

      效率和时间片大小有关系,时间片太小进程切换频繁,太长则不能保证实时性。

    • 优先级调度

      为进程分配优先级,为防止优先级低的进程饿死,可以随着时间推移增加进程的优先级。

    • 多级反馈队列

      采用时间片轮转,一个需要100次时间片的线程需要切换100次。在多级队列中,设置多个队列,时间片大小不太能,例如是1,2,4,8,第一个进程没执行完就移入第二个队列,这样只需要交换7次。

  • 实时系统:要求一个请求在确定时间内得到响应。分为硬实时和软实时,前者必须满足绝对时间,后者有一定的容忍时间。

进程同步

  • 临界区

  • 同步与互斥

  • 信号量:一个整型变量,可以进行down和up操作,也就是P和V操作。

    down:如果信号量大大于0,执行-1操作;等于0,进程睡眠,等待信号量大于0.

    up:信号量执行+1操作,唤醒睡眠的进程完成down操作。

    PV操作需要设计成原语,通常的做法是在执行操作的时候屏蔽中断。

    如果信号量只能取值0和1,就成了互斥量,0表示临界区加锁,1表示临界区解锁。

    使用信号量解决生产者-消费者问题:

    信号量记录缓冲区物品的数量,empty记录缓冲区空的信号量,full记录缓冲区满的信号量。empty在生产过程使用,不是0的时候可以放入,full在消费过程使用,不是0才可以消费。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #define N 100
    typedef int semaphore;
    //缓冲区,down是申请锁操作,up是释放锁操作
    semaphore mutex = 1;
    semaphore empty = N;
    semaphore full = 0;

    void producer(){
    while(true){
    int item = produce_item();
    down(&empty);
    down(&mutex);
    insert_item(item);
    up(&mutex);
    up(&full);
    }
    }
  • 管程:使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制,而管程把控制的代码独立出来,不仅不容易出错,也使得客户端代码调用更容易。

经典同步问题

  • 哲学家进餐问题
  • 读者-写者问题

进程通信

进程通信是手段,进程同步是目的。

  • 管道:通过pipe函数创建,fd[0]用于读,fd[1]用于写,具有以下限制:

    1
    2
    #include<unistd.h>
    int pipe(int fd[2]);
    • 只支持半双工协议(单向交替传输)
    • 只能在父子进程或兄弟进程中使用
  • FIFO:命名管道,去除了管道只能在父子进程中使用的限制。

    1
    2
    3
    #include<sys/stat.h>
    int mkfifo(const char *path,mode_t mode);
    int mkfifoat(int fd,const char *path,mode_t mode);

    常用于客户-服务器应用程序中,FIFO作为汇聚点,在客户进程和服务器进程之间传递数据。

    image-20210630105001363
  • 消息队列:相比于FIFO有以下优点

    • 可以独立于读写进程存在,避免了FIFO中同步管道的打开和关闭时可能产生的困难。
    • 避免了FIFO的同步阻塞问题
    • 读进程可以根据消息类型有选择的接收消息,不像FIFO只能默认接收。
  • 信号量:为多个进程提供对共享数据的访问。

  • 共享存储:多个进程共享一个给定的存储区,数据不需要在进程之间进行赋值,也是最快的一种IPC。

    使用信号量同步对共享存储的访问。

  • 套接字:可以用于不同机器间的进程通信。

死锁

产生条件:

  • 互斥:资源任一时刻只能一个线程占用
  • 请求与保持:进程因为资源阻塞,不释放已持有资源
  • 不剥夺:进程已获得的资源,在末使用完之前,不能强行剥夺。
  • 循环等待:若干进程之间形成一种头尾相接的循环等待资源关系。

处理方法:

主要有:鸵鸟策略,死锁检测与死锁恢复,死锁预防,死锁避免。

  • 鸵鸟策略:假装没发生。

  • 检测与恢复:不阻止死锁,而是检测到时,采取措施进行恢复。

    • 每种类型一个资源的检测:检测有向图是否存在环。

    • 每种类型多个资源的检测:

      E是资源总量,A是资源剩余,C是进程拥有,R是进程需要。

      image-20210630114906686

      每个进程开始时都不标记,执行过程可能标记,算法结束时,任何没有被标记的进程都是死锁进程,

      • 找到一个没有标记的进程Pi,请求的资源小于等于A
      • 找到了就把C中的i行向量加到A中,标记进程,并转回1
      • 没有这样的进程,算法终止。

    而恢复主要是:抢占,回滚,杀死进程。

  • 死锁预防

    • 破坏互斥条件
    • 破坏占有和等待条件
    • 破坏不可抢占条件
    • 破坏环路等待
  • 死锁避免

    • 安全状态:

      image-20210630120412962

      如果没有死锁发生,并且即使所有进程突然请求对资源的最大需求,也依然存在某种调度程序能够使得每一个进程运行完毕,则称该状态是安全的的。

    • 单个资源的银行家算法

      image-20210630120447059

      C不是安全状态,算法会拒绝之前的请求,从而避免进入C

    • 多个资源的银行家算法

      image-20210630120733895

      E总资源,P已分配资源,A可用资源,是向量而不是数值,算法如下:

      • 检查右边的矩阵是否有一行小于A,不存在则死锁,不安全状态
      • 找到则进程标记终止,已分配资源加到A
      • 知道所有进程都标记,则状态安全。

      如果任何一个状态是不安全的,则拒绝进入这个状态。

内存管理

虚拟内存

OS将内存抽象成地址空间,每个程序拥有自己的地址空间,地址空间被分为多个块,每个块称为一个页,页映射到物理内存。当程序引用到不在物理内存中的页时,硬件执行必要的映射,将缺失的部分装入物理内存并重新计算失败的指令。虚拟内存允许程序不用将地址空间中的每一页都映射到物理内存,使得运行大程序成为可能。

分页系统地址映射

内存管理单元管理地址空间和物理内存的转换,页表存储着页(地址空间)和页框(物理内存空间)的映射表。一个虚拟地址分成两个部分,一部分存储页面号,一部分存储偏移量。

image-20210630153409898

页表存放16个页,需要用4个bit位来索引定位,对于逻辑地址,前4位找到页面号是2,2的内容是1101,最后一位表示是否在内存中,1表示在内存中。后12位存储偏移量。最后得到物理地址。

页面置换算法

虚拟内存的存在,缺页中断将页调入内存的时候,如果内存中没有空闲,必须从内存中调出一个页面到磁盘对换区中腾出空间。页面置换算法的主要目标是使得页面置换频率最低,也就是说缺页率最低。

  • 最佳(OPT):最长时间不再被访问,是一种理论算法,因为无法知道一个页面多长时间不再被访问到。

  • 最近最久未使用(LRU):实现LRU需要维护链表,参考LinkedHashMap。因为每次访问都需要更新链表,所以代价比较高。

  • 最近未使用(NRU):页面有两个状态位,R与M。页面被访问时设置页面的R=1,页面被修改M=1。R位会定时清0,所以可能有:

    R M
    0 1
    0 0
    1 0
    1 1

    优先置换出已经被修改的脏页面(0,1),而不是频繁使用的(1,0)。

  • 先进先出:会将经常访问的页面置换,缺页率高。

  • 第二次机会算法:

    image-20210630160729702

    为了弥补FIFO的不足,页面访问时设置R位为1,替换的时候,检查最老页面的R位,是0则立刻替换;是1,R清零,放到链表末尾,修改装入时间,然后吃那个链表头开始搜索。

  • 时钟:第二次机会算法需要在链表中移动页面,降低效率。始终使用环形链表将页面连接起来,使用指针指向最老的页面。

  • image-20210630161110256

分段

页大小固定,下图是一个编译器在编译过程中建立的多个表,有4个表示动态增长的,如果使用分页系统的一维地址空间,动态增长的特点会导致覆盖问题的出现。

image-20210630161556159

分段的做法是把每个表分成段,一个段构成一个独立的地址空间。段的长度可以不同,可以动态增长。

段页式

地址空间被划分成多个拥有独立地址空间的段,每个段上的地址空间划分成大小相同的页,这样既拥有分段系统的共享和保护,也拥有分页系统的虚拟内存的功能。

分段与分页的比较

  • 分页透明,分段需要coder显式划分
  • 分页是一维地址空间,分段是二维
  • 页大小不变,段大小可以动态变化
  • 分页主要用于实现虚拟存储;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。

设备管理

磁盘结构

image-20210630162721693

  • 盘面(Platter):一个磁盘有多个盘面;
  • 磁道(Track):盘面上的圆形带状区域,一个盘面可以有多个磁道;
  • 扇区(Track Sector):磁道上的一个弧段,一个磁道可以有多个扇区,它是最小的物理储存单位,目前主要有 512 bytes 与 4 K 两种大小;
  • 磁头(Head):与盘面非常接近,能够将盘面上的磁场转换为电信号(读),或者将电信号转换为盘面的磁场(写);
  • 制动手臂(Actuator arm):用于在磁道之间移动磁头;
  • 主轴(Spindle):使整个盘面转动。

磁盘调度算法

读写一个磁盘块的时间的影响因素有:旋转时间(主轴转动盘面,使得磁头移动到适当的扇区);寻道时间(制动手臂移动,使得磁头移动到适当的磁道);实际的数据传输时间。其中寻道时间最长,调度的主要目标就是使得平均寻道时间最短。

  • 先来先服务:公平但平均寻道时间可能长。
  • 最短寻道时间:优先调度与当前磁头所在磁道最近的磁道,两端的磁道请求更容器出现饥饿现象。
  • 电梯算法:朝着一个方向调度,知道该方向没有未完成的磁盘请求,然后改变方向,解决了饥饿问题。

链接

编译系统

gcc -o hello hello.c的过程大致如下:

image-20210630164718917
  • 预处理阶段:处理以#开头的预处理命令
  • 编译阶段:翻译成汇编文件
  • 汇编阶段:将汇编文件编译成可重定位目标文件
  • 链接阶段:将可重定位目标文件和printf.o等单独编译好的目标文件进行合并,得到最终的可执行目标文件。

静态链接

以一组可重定位目标文件为输入,生成一个完全链接的可执行目标文件作为输出。连接器主要完成以下工作:

  • 符号解析:每个符号对应一个函数、全局变量或者静态变量,目的是把每个符号引用与一个符号定义关联起来。
  • 重定位:连接器通过每个符号定义与一个内存位置关联起来,然后修改所有对这些符号的引用,使得他们指向这个内存位置。
image-20210630165915208

目标文件

  • 可执行目标文件:可以直接在内存中运行。
  • 可重定位目标文件:可与其他可重定位目标文件在连接阶段合并,创建一个可执行文件(好家伙,递归定义)
  • 共享目标文件:特殊的可重定位目标文件,可以在运行时被动态加载到内存并链接。

动态链接

静态库有以下两个问题:

  • 静态库更新时,整个程序都要重新进行链接
  • 对于printf这种标准函数库,如果每个程序都要有代码,会造成浪费。

共享库解决这个问题,在linux中用.so后缀表示,windows称为DLL。有以下特点:

  • 在给定的文件系统中一个库只有一个文件,所有引用该库的可执行目标文件都共享这个文件,不会被复制到引用他的可执行文件中。

  • 在内存中,一个共享库.text节(已编译程序的机器码)的一个副本可以被不同的正在运行的进程共享。

    image-20210630170757139

tidb

接下来来谈谈这个contribute–表达式向量化

TIDB为了鼓励参与,在很简单的向量化这里留下了很多口子,对于小白来说非常友好。

如何访问和修改向量

在TIDB中有变长类型和定长类型,有如下读写方式:

  • 定长类型(int64为例)
    • ResizeInt64s(size, isNull):预分配 size 个元素的空间,并把所有位置的 null 标记都设置为 isNull
    • Int64s():返回一个 []int64 的 Slice,用于直接读写数据;
    • SetNull(rowID, isNull):标记第 rowID 行为 isNull
  • 变长类型(String为例)
    • ReserveString(size):预估 size 个元素的空间,并预先分配内存;
    • AppendNull():追加一个 null 到向量末尾;
    • GetString(rowID):读取下标为 rowID 的 string 数据。

向量表达式计算框架

向量化的计算接口

1
2
COPYvectorized() bool
vecEvalXType(input *Chunk, result *Column) error
  • xType:类型
  • input:输入数据,类型为\*Chunk
  • result存放结果数据。

对于任意表达式,只有在其中所有的函数都支持向量化后,才认为表达式支持向量化。

为函数实现向量化接口

向量化需要实现vecEvalXType()vectorized接口

  • vectorized() 接口中返回 true ,表示该函数已经实现向量化计算;
  • vecEvalXType() 实现此函数的计算逻辑。

向量化的代码存放在_vec.go中,文件头加上licence说明。

以下是代码:

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
COPYfunc (b *builtinMicroSecondSig) vectorized() bool {
return false
return true
}

func (b *builtinMicroSecondSig) vecEvalInt(input *chunk.Chunk, result *chunk.Column) error {
return errors.Errorf("not implemented")
n := input.NumRows()
buf, err := b.bufAllocator.get(types.ETDuration, n)
if err != nil {
return err
}
defer b.bufAllocator.put(buf)
if err = b.args[0].VecEvalDuration(b.ctx, input, buf); err != nil {
return err
}

result.ResizeInt64(n, false)
result.MergeNulls(buf)
i64s := result.Int64s()
ds := buf.GoDurations()
for i := 0; i < n; i++ {
if result.IsNull(i) {
continue
}
i64s[i] = int64((ds[i] % time.Second) / time.Microsecond)
}
return nil
}
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.

扫一扫,分享到微信

微信分享二维码

请我喝杯咖啡吧~

支付宝
微信