0%

ORM 相关

Object·/Relation Mapping 对象/关系数据库映射,他完成的是面向对象的编程语言到关系型数据库的映射,所以你可以理解为 ORM 其实就是应用程序和数据库的桥梁。

JPA 规范

JPA 规范本质上就是一种 ORM 规范。Hibernate 实现了这套规范。

JPQL 比较 SQL

jpql 与 SQL 的区别就是 SQL 是面向对象关系数据库,他操作的是数据表和数据列,而 jpql 操作的对象是实体对象和实体属性。
下面是 JPQL 的基本格式,根据基本格式我们执行 jpql 语句

1
2
select 实体别名.属性名, 实体别名.属性名 from 实体名 as 实体别名 where 实体别名.实体属性 op 比较值;
select p.name from book as p ;

Mybatis 对比 Hibernate

国人用 Mybatis 的原因:

  1. 大厂带节奏 国内做互联网的 Java 程序很多都是拷贝阿里的,阿里一开始用例 iBatis(日本韩国是怎么回事呢)。大量的老系统都是基于 iBatis/MyBatis 的,市场上对 MyBatis 熟悉的人才更多,招聘和培训更容易,有的青年程序员以为“MyBatis 早已统一全球了”就是一个很好的证明。
  1. 简单,学习成本低 小公司需要大量入门级的程序员,像大神甚至一个都请不起,请问大神们那些牛 b 框架哪个更快让菜鸟们上手,降低公司学习成本。注意这个成本会一直跟随公司,想必大神们创业直接前后端分离了,毕竟钱嘛多的是。
  1. 对于复杂性需求的灵活性高 国内绝大部分项目都是面向表结构编程的,把 java 对象仅当成数据容器,查询和模型变更都设计在一张表上,所谓业务逻辑就是一堆增删改查的 sql 集合,当然用 mybatis 方便。在逻辑不复杂,或者你判断软件生命周期不会超过一年的时候,直接用表结构编程是最方便快捷的。国内普遍都是分布式,流量和性能决定了需要经常进行优化,而是用 Mybatis 对复杂需求的优化很方便。
  1. 政治环境 国内好多项目都是应付领导的某些奇葩需求。需要面向领导编程。一大半时间其实都是在解决领导的需求。国内项目需要大量报表统计(看看帆软卖的这么好就知道了),需要提供给领导作为决策。看到这里,各位领导不要骂我 ,真的不是黑领导的。
  1. Hibernate 学习成本高 虽然,实际上 SpringDataJPA 是非常简单的,但是,但是,JPA/Hibernate 后期调试跟踪问题很麻烦,改起来也麻烦。别忘了,牛逼如你的人全公司甚至一个都没。还有什么缓存什么 Criteria 什么 Lazy,虽然这些你学了也不见得能用上,但一个框架,你不学还是不行的。而且,JPA 对于增删改很方便,复杂查询却是软肋,有同学会说,JPA 也能写 SQL 语句啊,我想说的是,既然都用 orm 了,你再写 sql,那不就失去了 oop 的内涵了吗?不优雅好吧。

老外喜欢 JPA 的原因:

  1. 很多老外对 Mybatis 的认知还停留在 iBatis 阶段 实际上在 Mybatis 的应用场景里面,开发者要的就是自动封装,把 sql 查询结果转化为指定的 java 对象。这个在 iBatis 阶段,需要开发者自己定义大量的 xml 配置,去指定数据库表字段与 Java 实体类之间的关系。并且,对于每一条 sql,都需要在 xml 中写相应的语句,虽然有代码生成器,带开发量还是不小的。但 Mybatis 发展到今天,已经非常完美地做好了自动封装数据对象这件事,支持的插件也比较丰富。对于常见的增删改查,也不需要自己写一行代码,这已经无限接近于 Hibernate 的能力了。

  2. 喜欢 OOP、DDD,认为写 SQL 不优雅 用 jpa 的核心是让我们关注对象建模,而不是关心底层数据库映射。只有你在考虑数据和行为在一起的充血模型、贴身职责,聚合根状态变迁,值对象不变性的情况下,你才会发现 jpa 给你提供了很多便利,根本不需要关注底层存储模型。在复杂的逻辑、超长的软件生命周期。使用 DDD 的设计方法是目前看比较合理的选择,维护的成本比较低。
    DDD 全称是(Domain-Driven Design)这是 2004 年就出来的理论,复杂逻辑的应对之道。DDD 大会在欧洲等地办了一届又一届,CQRS、Event Sourcing 等探索层出不穷,这也是为什么国外比较流行 JPA 原因。
    不过,国内主要是随着这两年随着微服务火爆也有人谈起来 DDD 了。
    但其实 DDD 也不是银弹,需要大拿能把控全局,国内缺的就是这种大拿,搬砖的太多。

  3. 有些老外在技术选型时,不会考虑除 Spring 这种知名框架外的其他技术 无他,唯手熟尔。国外一个项目,做了几年十几年都是很正常的。我以前接触过一个巴基斯坦的电商项目,做了十几年,也跑的好好的,这就是证据。使用技术也是有惯性的。

  4. 数据体量和种类没有达到 个人感觉,也咨询了国际友人。老外的项目,在数据体量和种类上完全达不到国内的水平。所以,他们对于性能上的渴求度没有那么高。追求的是稳定,可维护性好。国内一个双 11,如果用 hibernate,那只能死掉了。

Mybatis 优势

  • MyBatis 可以进行更为细致的 SQL 优化,可以减少查询字段。
  • MyBatis 容易掌握,而 Hibernate 门槛较高。

Hibernate 优势

  • Hibernate 的 DAO 层开发比 MyBatis 简单,不需要写 sql,Mybatis 需要维护 SQL 和结果映射。
  • Hibernate 对对象的维护和缓存要比 MyBatis 好,对增删改查的对象的维护要方便。
  • Hibernate 数据库移植性很好,MyBatis 的数据库移植性不好,不同的数据库需要写不同 SQL。
  • Hibernate 有更好的二级缓存机制,可以使用第三方缓存。MyBatis 本身提供的缓存机制不佳。
    hibernate 让你不用写 sql 了,这不单可以让你的应用更好移植其它数据库,更主要的是让程序员更专注业务逻辑、数据关系、对象关系等。
    参考连接:https://zhuanlan.zhihu.com/p/34298019

Redis 中使用 lua

一、简介

  1. Redis 中为什么引入 Lua 脚本?

    Redis 是高性能的 key-value 内存数据库,在部分场景下,是对关系数据库的良好补充。
    Redis 提供了非常丰富的指令集,官网上提供了 200 多个命令。但是某些特定领域,需要扩充若干指令原子性执行时,仅使用原生命令便无法完成。
    Redis 为这样的用户场景提供了 lua 脚本支持,用户可以向服务器发送 lua 脚本来执行自定义动作,获取脚本的响应数据。
    Redis 服务器会单线程原子性执行 lua 脚本,保证 lua 脚本在处理的过程中不会被任意其它请求打断。

  2. Redis 意识到上述问题后

    在 2.6 版本推出了 lua 脚本功能,允许开发者使用 Lua 语言编写脚本传到 Redis 中执行。使用脚本的好处如下:减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延。
    原子操作。Redis 会将整个脚本作为一个整体执行,中间不会被其他请求插入。因此在脚本运行过程中无需担心会出现竞态条件,无需使用事务。
    复用。客户端发送的脚本会永久存在 redis 中,这样其他客户端可以复用这一脚本,而不需要使用代码完成相同的逻辑。

  3. 什么是 Lua?

    Lua 是一种轻量小巧的脚本语言,用标准 C 语言编写并以源代码形式开放。
    其设计目的就是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。因为广泛的应用于:游戏开发、独立应用脚本、Web 应用脚本、扩展和数据库插件等。
    比如:Lua 脚本用在很多游戏上,主要是 Lua 脚本可以嵌入到其他程序中运行,游戏升级的时候,可以直接升级脚本,而不用重新安装游戏。

二、Redis 中 Lua 的常用命令

命令不多,就下面这几个:

  • EVAL

  • EVALSHA

  • SCRIPT LOAD - SCRIPT EXISTS

  • SCRIPT FLUSH

  • SCRIPT KILL

    2.1 EVAL 命令
    命令格式:EVAL script numkeys key [key …] arg [arg …]

  • script 参数是一段 Lua5.1 脚本程序。脚本不必(也不应该[^1])定义为一个 Lua 函数

  • numkeys 指定后续参数有几个 key,即:key [key …]中 key 的个数。如没有 key,则为 0

  • key [key …] 从 EVAL 的第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key)。在 Lua 脚本中通过 KEYS[1], KEYS[2]获取。

  • arg [arg …] 附加参数。在 Lua 脚本中通过 ARGV[1],ARGV[2]获取。

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
// 例1:numkeys=1,keys数组只有1个元素key1,arg数组无元素
127.0.0.1:6379> EVAL "return KEYS[1]" 1 key1
"key1"

// 例2:numkeys=0,keys数组无元素,arg数组元素中有1个元素value1
127.0.0.1:6379> EVAL "return ARGV[1]" 0 value1
"value1"

// 例3:numkeys=2,keys数组有两个元素key1和key2,arg数组元素中有两个元素first和second
// 其实{KEYS[1],KEYS[2],ARGV[1],ARGV[2]}表示的是Lua语法中“使用默认索引”的table表,
// 相当于java中的map中存放四条数据。Key分别为:1、2、3、4,而对应的value才是:KEYS[1]、KEYS[2]、ARGV[1]、ARGV[2]
// 举此例子仅为说明eval命令中参数的如何使用。项目中编写Lua脚本最好遵从key、arg的规范。
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"


// 例4:使用了redis为lua内置的redis.call函数
// 脚本内容为:先执行SET命令,在执行EXPIRE命令
// numkeys=1,keys数组有一个元素userAge(代表redis的key)
// arg数组元素中有两个元素:10(代表userAge对应的value)和60(代表redis的存活时间)
127.0.0.1:6379> EVAL "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;" 1 userAge 10 60
(integer) 1
127.0.0.1:6379> get userAge
"10"
127.0.0.1:6379> ttl userAge
(integer) 44

通过上面的例 4,我们可以发现,脚本中使用 redis.call()去调用 redis 的命令。
在 Lua 脚本中,可以使用两个不同函数来执行 Redis 命令,它们分别是: redis.call() 和 redis.pcall()
这两个函数的唯一区别在于它们使用不同的方式处理执行命令所产生的错误,差别如下:
当 redis.call() 在执行命令的过程中发生错误时,脚本会停止执行,并返回一个脚本错误,错误的输出信息会说明错误造成的原因:

1
2
3
4
5
127.0.0.1:6379> lpush foo a
(integer) 1

127.0.0.1:6379> eval "return redis.call('get', 'foo')" 0
(error) ERR Error running script (call to f_282297a0228f48cd3fc6a55de6316f31422f5d17): ERR Operation against a key holding the wrong kind of value

和 redis.call() 不同, redis.pcall() 出错时并不引发(raise)错误,而是返回一个带 err 域的 Lua 表(table),用于表示错误:

1
2
127.0.0.1:6379> EVAL "return redis.pcall('get', 'foo')" 0
(error) ERR Operation against a key holding the wrong kind of value

2.2 SCRIPT LOAD 命令 和 EVALSHA 命令

SCRIPT LOAD 命令格式:SCRIPT LOAD script
EVALSHA 命令格式:EVALSHA sha1 numkeys key [key …] arg [arg …]

这两个命令放在一起讲的原因是:EVALSHA 命令中的 sha1 参数,就是 SCRIPT LOAD 命令执行的结果。

SCRIPT LOAD 将脚本 script 添加到 Redis 服务器的脚本缓存中,并不立即执行这个脚本,而是会立即对输入的脚本进行求值。并返回给定脚本的 SHA1 校验和。如果给定的脚本已经在缓存里面了,那么不执行任何操作。

在脚本被加入到缓存之后,在任何客户端通过 EVALSHA 命令,可以使用脚本的 SHA1 校验和来调用这个脚本。脚本可以在缓存中保留无限长的时间,直到执行 SCRIPT FLUSH 为止。

1
2
3
4
5
6
7
8
9
10
11
## SCRIPT LOAD加载脚本,并得到sha1值
127.0.0.1:6379> SCRIPT LOAD "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;"
"6aeea4b3e96171ef835a78178fceadf1a5dbe345"

## EVALSHA使用sha1值,并拼装和EVAL类似的numkeys和key数组、arg数组,调用脚本。
127.0.0.1:6379> EVALSHA 6aeea4b3e96171ef835a78178fceadf1a5dbe345 1 userAge 10 60
(integer) 1
127.0.0.1:6379> get userAge
"10"
127.0.0.1:6379> ttl userAge
(integer) 43

2.3 SCRIPT EXISTS 命令
命令格式:SCRIPT EXISTS sha1 [sha1 …]
作用:给定一个或多个脚本的 SHA1 校验和,返回一个包含 0 和 1 的列表,表示校验和所指定的脚本是否已经被保存在缓存当中

1
2
3
4
5
6
7
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345
1) (integer) 1
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe346
1) (integer) 0
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345 6aeea4b3e96171ef835a78178fceadf1a5dbe366
1) (integer) 1
2) (integer) 0

2.4 SCRIPT FLUSH 命令

命令格式:SCRIPT FLUSH
作用:清除 Redis 服务端所有 Lua 脚本缓存

1
2
3
4
5
6
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345
1) (integer) 1
127.0.0.1:6379> SCRIPT FLUSH
OK
127.0.0.1:6379> SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345
1) (integer) 0

2.5 SCRIPT KILL 命令
命令格式:SCRIPT FLUSH
作用:杀死当前正在运行的 Lua 脚本,当且仅当这个脚本没有执行过任何写操作时,这个命令才生效。 这个命令主要用于终止运行时间过长的脚本,比如一个因为 BUG 而发生无限 loop 的脚本,诸如此类。

假如当前正在运行的脚本已经执行过写操作,那么即使执行 SCRIPT KILL,也无法将它杀死,因为这是违反 Lua 脚本的原子性执行原则的。在这种情况下,唯一可行的办法是使用 SHUTDOWN NOSAVE 命令,通过停止整个 Redis 进程来停止脚本的运行,并防止不完整(half-written)的信息被写入数据库中。

三、Redis 执行 Lua 脚本文件

在第二章中介绍的命令,是在 redis 客户端中使用命令进行操作。该章节介绍的是直接执行 Lua 的脚本文件。
3.1 编写 Lua 脚本文件

1
2
3
4
5
6
7
8
9
10
local key = KEYS[1]
local val = redis.call("GET", key);

if val == ARGV[1]
then
redis.call('SET', KEYS[1], ARGV[2])
return 1
else
return 0
end

3.2 执行 lua 脚本文件

1
2
执行命令: redis-cli -a 密码 --eval Lua脚本路径 key [key …] ,  arg [arg …]
如:redis-cli -a 123456 --eval ./Redis_CompareAndSet.lua userName , zhangsan lisi

此处敲黑板,注意啦!!!
“–eval”而不是命令模式中的”eval”,一定要有前端的两个-
脚本路径后紧跟 key [key …],相比命令行模式,少了 numkeys 这个 key 数量值
key [key …] 和 arg [arg …] 之间的“ , ”,英文逗号前后必须有空格,否则死活都报错

1
2
3
4
5
6
7
8
9
10
11
12
13
## Redis客户端执行
127.0.0.1:6379> set userName zhangsan
OK
127.0.0.1:6379> get userName
"zhangsan"

## linux服务器执行
## 第一次执行:compareAndSet成功,返回1
## 第二次执行:compareAndSet失败,返回0
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_CompareAndSet.lua userName , zhangsan lisi
(integer) 1
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_CompareAndSet.lua userName , zhangsan lisi
(integer) 0

四、实例:使用 Lua 控制 IP 访问频率

需求:实现一个访问频率控制,某个 IP 在短时间内频繁访问页面,需要记录并检测出来,就可以通过 Lua 脚本高效的实现。
小声说明:本实例针对固定窗口的访问频率,而动态的非滑动窗口。即:如果规定一分钟内访问 10 次,记为超限。在本实例中前一分钟的最后一秒访问 9 次,下一分钟的第 1 秒又访问 9 次,不计为超限。
脚本如下:

1
2
3
4
5
6
7
8
9
10
11
local visitNum = redis.call('incr', KEYS[1])

if visitNum == 1 then
redis.call('expire', KEYS[1], ARGV[1])
end

if visitNum > tonumber(ARGV[2]) then
return 0
end

return 1;

演示如下:

1
2
3
4
5
6
7
8
9
10
11
12
## LimitIP:127.0.0.1为key, 10 3表示:同一IP在10秒内最多访问三次
## 前三次返回1,代表未被限制;第四、五次返回0,代表127.0.0.1这个ip已被拦截
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_LimitIpVisit.lua LimitIP:127.0.0.1 , 10 3
(integer) 1
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_LimitIpVisit.lua LimitIP:127.0.0.1 , 10 3
(integer) 1
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_LimitIpVisit.lua LimitIP:127.0.0.1 , 10 3
(integer) 1
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_LimitIpVisit.lua LimitIP:127.0.0.1 , 10 3
(integer) 0
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_LimitIpVisit.lua LimitIP:127.0.0.1 , 10 3
(integer) 0

五、总结
通过上面一系列的介绍,对 Lua 脚本、Lua 基础语法有了一定了解,同时也学会在 Redis 中如何去使用 Lua 脚本去实现 Redis 命令无法实现的场景
回头再思考文章开头提到的 Redis 使用 Lua 脚本的几个优点:减少网络开销、原子性、复用

Sentinel

http://redisdoc.com/topic/sentinel.html

MAT 分析

链接: https://zhuanlan.zhihu.com/p/56110317
链接: https://zhuanlan.zhihu.com/p/57347496
链接: https://www.eclipse.org/mat

JAVA 性能调优

jvm 之 AccessController.doPrivileged

AccessController.doPrivileged 意思是这个是特别的,不用做权限检查.

在什么地方会用到呢:加入 1.jar 中有类可以读取一个文件,现在我们要使用 1.jar 去做这个事情.但是我们的类本生是没有权限去读取那个文件的,一般情况下就是眼睁睁的看着了.

但是 jiava 提供了 doPrivileged.在 1.jar 中如果读取文件的方法是通过 doPrivileged 来实现的.就不会有后面的检查了,现在我们就可以使用 1.jar 去读取那个文件了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package huangyunbin.client;

import java.io.FilePermission;
import java.security.AccessController;
import java.security.Permission;
import java.security.PrivilegedAction;

public class Client
{
public void doCheck() {
AccessController.doPrivileged( new PrivilegedAction() {
public Object run() {
check();
return null ;
}
} );
}

private void check() {
Permission perm = new FilePermission( "/1.txt" , "read" );
AccessController.checkPermission(perm);
System.out.println( " TestService has permission " );
}
}

把这个类打包成 client.jar 放到/home/h/client/下,我们建立个 my.policy 文件,文件内容是:

1
2
3
grant codeBase  "file:/home/h/client/*"   {
permission java.io.FilePermission "/1.txt","read";
};

配置文件的意思是 /home/h/client/下面的 jar 包或 class 类 可以读取/1.txt.

现在我们再创建一个项目:创建一个类来调用前面的 Client

1
2
3
4
5
6
7
8
public class server
{
public static void main(String[] args)
{
Client c =new Client();
c.doCheck();
}
}

运行这个 server 类.注意这里要用上之前的 my.policy 文件
在 vm 参数中写上这样的:

1
2
-Djava.security.manager
-Djava.security.policy=/home/h/my.policy

运行,结果是
TestService has permission

在配置文件 my.policy 中我们没有允许 server 去读取/1.txt,但是现在却可以正常访问.这个就是 AccessController.doPrivileged 的作用.

首先,大家需要明确一点,Docker 容器不是虚拟机。2014 年,当我第一次接触 Docker 的时候,我把它比做一种轻量级的虚拟机。这样做无可厚非,因为 Docker 最初的成功秘诀,正是它比虚拟机更节省内存,启动更快。Docker 不停地给大家宣传,”虚拟机需要数分钟启动,而 Docker 容器只需要 50 毫秒”。然而,Docker 容器并非虚拟机,我们不妨来比较一下它们。

理解虚拟机

使用虚拟机运行多个相互隔离的应用时,如下图:

从下到上理解上图:

  • 基础设施(Infrastructure)。它可以是你的个人电脑,数据中心的服务器,或者是云主机。
  • 虚拟机管理系统(Hypervisor)。利用 Hypervisor,可以在主操作系统之上运行多个不同的从操作系统。类型 1 的 Hypervisor 有支持 MacOS 的 HyperKit,支持 Windows 的 Hyper-V、Xen 以及 KVM。类型 2 的 Hypervisor 有 VirtualBox 和 VMWare workstation。
  • 客户机操作系统(Guest Operating System)。假设你需要运行 3 个相互隔离的应用,则需要使用 Hypervisor 启动 3 个客户机操作系统,也就是 3 个虚拟机。这些虚拟机都非常大,也许有 700MB,这就意味着它们将占用 2.1GB 的磁盘空间。更糟糕的是,它们还会消耗很多 CPU 和内存。
  • 各种依赖。每一个客户机操作系统都需要安装许多依赖。如果你的应用需要连接 PostgreSQL 的话,则需要安装 libpq-dev;如果你使用 Ruby 的话,应该需要安装 gems;如果使用其他编程语言,比如 Python 或者 Node.js,都会需要安装对应的依赖库。
  • 应用。安装依赖之后,就可以在各个客户机操作系统分别运行应用了,这样各个应用就是相互隔离的。理解 Docker 容器使用 Docker 容器运行多个相互隔离的应用时,如下图:

理解 Docker 容器

使用 Docker 容器运行多个相互隔离的应用时,如下图:

不难发现,相比于虚拟机,Docker 要简洁很多。因为我们不需要运行一个臃肿的客户机操作系统了。
从下到上理解上图:

  • 基础设施(Infrastructure)。
  • 主操作系统(Host Operating System)。所有主流的 Linux 发行版都可以运行 Docker。对于 MacOS 和 Windows,也有一些办法”运行”Docker。
  • Docker 守护进程(Docker Daemon)。Docker 守护进程取代了 Hypervisor,它是运行在操作系统之上的后台进程,负责管理 Docker 容器。
  • 各种依赖。对于 Docker,应用的所有依赖都打包在 Docker 镜像中,Docker 容器是基于 Docker 镜像创建的。
  • 应用。应用的源代码与它的依赖都打包在 Docker 镜像中,不同的应用需要不同的 Docker 镜像。不同的应用运行在不同的 Docker 容器中,它们是相互隔离的。

对比虚拟机与 Docker

Docker 守护进程可以直接与主操作系统进行通信,为各个 Docker 容器分配资源;它还可以将容器与主操作系统隔离,并将各个容器互相隔离。虚拟机启动需要数分钟,而 Docker 容器可以在数毫秒内启动。由于没有臃肿的从操作系统,Docker 可以节省大量的磁盘空间以及其他系统资源。

说了这么多 Docker 的优势,大家也没有必要完全否定虚拟机技术,因为两者有不同的使用场景。虚拟机更擅长于彻底隔离整个运行环境。例如,云服务提供商通常采用虚拟机技术隔离不同的用户。而 Docker 通常用于隔离不同的应用,例如前端,后端以及数据库。

服务器虚拟化 vs Docker

  • 服务器好比运输码头:拥有场地和各种设备(服务器硬件资源)
  • 服务器虚拟化好比作码头上的仓库:拥有独立的空间堆放各种货物或集装箱(仓库之间完全独立,独立的应用系统和操作系统)
  • Docker 比作集装箱:各种货物的打包(将各种应用程序和他们所依赖的运行环境打包成标准的容器,容器之间隔离)

Docker 有着小巧、迁移部署快速、运行高效等特点,但隔离性比服务器虚拟化差:不同的集装箱属于不同的运单(Docker 上运行不同的应用实例),相互独立(隔离)。但由同一个库管人员管理(主机操作系统内核),因此通过库管人员可以看到所有集装箱的相关信息(因为共享操作系统内核,因此相关信息会共享)。

服务器虚拟化就好比在码头上(物理主机及虚拟化层),建立了多个独立的“小码头”—仓库(虚拟机)。其拥有完全独立(隔离)的空间,属于不同的客户(虚拟机所有者)。每个仓库有各自的库管人员(当前虚拟机的操作系统内核),无法管理其它仓库。不存在信息共享的情况.

因此,我们需要根据不同的应用场景和需求采用不同的方式使用 Docker 技术或使用服务器虚拟化技术。例如一个典型的 Docker 应用场景是当主机上的 Docker 实例属于单一用户的情况下,在保证安全的同时可以充分发挥 Docker 的技术优势。对于隔离要求较高的环境如混合用户环境,就可以使用服务器虚拟化技术。正则科技提供了丰富的 Docker 应用实例,满足您的各种应用需求,并且支持在已经安装了自在(Isvara)服务器虚拟化软件的主机上同时使用服务器虚拟化技术和 Docker 技术提供不同技术场景

Docker 基本命令

参考:https://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html

K8S 的 kubectl 命令

参考:https://zhuanlan.zhihu.com/p/85810571

链接:https://www.zhihu.com/question/22916306/answer/1661734440

什么是 CNAME 记录?CNAME 记录如何使用  

CName 记录是 Canonical Name 的简称,通常称别名指向,CNAME 记录可用于将一个域名别名为另一个规范名称的域名系统(DNS)资源记录。

网站是由一组由一组唯一标识的位置(称为 IP 地址)提供服务的;但是要访问这些站点(例如:晓得博客),我们通常会键入它们对应的域名,这些域名更容易记住。要找到正确的 IP 地址,您的浏览器将联系域名服务器(DNS),并在其数据库中查询 IP 地址。

CNAME 记录如何使用  

例如,假设您有几个子域,例如http://www.mydomain.com,http://ftp.mydomain.com,http://mail.mydomain.com等,并且您希望这些子域指向您的主域名http://mydomain.com。您可以创建CNAME记录,而不是为每个子域创建A记录并将其绑定到您域的IP地址。

如下表所示,如果服务器的 IP 地址发生更改,则只需更新一个 A 记录,并且所有子域都会自动更新,因为所有 CNAMES 都指向带有 A 记录的主域:
|(子)域名/主机名 | 记录类型 | 目标/目的地 |
|——————-|—|—————–|
|http://mydomain.com |A |111.222.333.444|
|http://www.mydomain.com| CNAME |http://mydomain.com|
|http://ftp.mydomain.com| CNAME |http://mydomain.com|
|http://mail.mydomain.com |CNAME |http://mydomain.com|
http://www.mydomain.com | CNAME | http://mydomain.com|

http://mydomain.com指向服务器IP地址,并通过http://www.mydomain.com指向相同的地址http://mydomain.com。如果IP地址发生更改,则只需要在一个地方进行更新即可:只需为修改A记录http://mydomain.com,那么http://www.mydomain.com自动继承更改。也就是做别名(即CNAME)的那些域名的指向将自动更改到新的IP地址上(以上操作均需要在DNS处执行)。

CNAME 记录必须始终指向另一个域名,永远不要直接指向 IP 地址。如果您尝试将 CNAME 记录指向 IP 地址,DNSimple 的记录编辑器会警告您。CNAME 对其他记录必须是唯一的。

CNAME 记录局限性  

CNAME 记录必须始终指向另一个域名,并且永远不要直接指向 IP 地址。

您不能为主域名(http://mydomain.com)本身创建CNAME记录,该记录必须是A记录。

例如,您不能将http://mydomain.com映射到http://google.com,但是可以将http://google.mydomain.com映射到http://google.com。

使用 CNAME 记录意味着有一个额外的请求发送到 DNS 服务器,这可能会导致几毫秒的延迟

一个 CNAME 记录不能与另一个具有相同名称的记录共存。不能同时有 CNAME 和 TXT 记录http://www.example.com。

一个 CNAME 可以指向另一个 CNAME,尽管出于性能原因通常不建议使用此配置。如果适用,CNAME 应该尽可能地指向目标名称,以避免不必要的性能开销。

Gradle

简介

Gradle是一门基于groovy的DSL(特定领域语言)语言,也就是说它只能在某一特定领域使用,而不通用。

项目中gradle相关文件

  • .gradle gradle项目产生文件(自动编译工具产生的文件)
  • build 自动构建时生成文件的地方
  • gradle 自动完成gradle环境支持文件夹
  • build.gradle gradle 项目自动编译的配置文件
  • gradle.properties gradle 运行环境配置文件
  • setting.gradle gradle 项目的子项目包含文件
  • gradlew 自动完成 gradle 环境的linux mac 脚本,配合gradle 文件夹使用 代替gradle 命令实现自动完成gradle环境搭建,配合gradle文件夹的内容
  • gradlew.bat 自动完成 gradle 环境的windows 脚本,配合gradle 文件夹使用
    上面两个文件会做如下几件事情
  1. 解析 gradle/wrapper/gradle-wrapper.properties 文件,获取项目需要的 gradle 版本下载地址
  2. 判断本地用户目录下的 ./gradle 目录下是否存在该版本,不存在该版本,走第3点,存在走第4点
  3. 下载 gradle-wrapper.properties 指定版本,并解压到用户目录的下 ./gradle 文件下
  4. 利用 ./gradle 目录下对应的版本的 gradle 进行相应自动编译操作

gradle版本问题

gradle版本对于项目是否能够成功构建影响很大,所以一般都是在gradle文件夹中指定版本,然后gradlew脚本配合完成环境的搭建,以保证项目在任何地点构建时使用的gradle版本保持一致。

对比maven

Java世界中主要有三大构建工具:Ant、Maven和Gradle。经过几年的发展,Ant几乎销声匿迹、Maven也日薄西山,而Gradle的发展则如日中天。总的说来Gradle在延续了Maven的优点上,增添了更多的灵活性,如多模块构建,依赖管理等。
参考 https://www.cnblogs.com/huang0925/p/5209563.html

MIME(Multipurpose Internet Mail Extensions)多用途互联网邮件扩展类型。
MIME 意为多目 Internet 邮件扩展,它设计的最初目的是为了在发送电子邮件时附加多媒体数据,让邮件客户程序能根据其类型进行处理。然而当它被 HTTP 协议支持之后,它的意义就更为显著了。它使得 HTTP 传输的不仅是普通的文本,而变得丰富多彩。

我们要了解浏览器是如何处理内容的。在浏览器中显示的内容有 HTML、有 XML、有 GIF、还有 Flash ……那么,浏览器是如何区分它们,决定什么内容用什么形式来显示呢?答案是 MIME Type,也就是该资源的媒体类型。
媒体类型通常是通过 HTTP 协议,由 Web 服务器告知浏览器的,更准确地说,是通过 Content-Type 来表示的,例如:

Content-Type: text/HTML

表示内容是 text/HTML 类型,也就是超文本文件。为什么是“text/HTML”而不是“HTML/text”或者别的什么?MIME Type 不是个人指定的,是经过 ietf 组织协商,以 RFC 的形式作为建议的标准发布在网上的,大多数的 Web 服务器和用户代理都会支持这个规范 (顺便说一句,Email 附件的类型也是通过 MIME Type 指定的)。

通常只有一些在互联网上获得广泛应用的格式才会获得一个 MIME Type,如果是某个客户端自己定义的格式,一般只能以 application/x- 开头。

在把输出结果传送到浏览器上的时候,浏览器必须启动适当的应用程序来处理这个输出文档。这可以通过多种类型 MIME(多功能网际邮件扩充协议)来完成。在 HTTP 中,MIME 类型被定义在 Content-Type header 中。

例如,架设你要传送一个 Microsoft Excel 文件到客户端。那么这时的 MIME 类型就是“application/vnd.ms-excel”。在大多数实际情况中,这个文件然后将传送给 Execl 来处理(假设我们设定 Execl 为处理特殊 MIME 类型的应用程序)

每个 MIME 类型由两部分组成,前面是数据的大类别,例如声音 audio、图象 image 等,后面定义具体的种类。

常见的 MIME 类型

  • 超文本标记语言文本 .html,.html text/html
  • 普通文本 .txt text/plain
  • RTF 文本 .rtf application/rtf
  • GIF 图形 .gif image/gif
  • JPEG 图形 .ipeg,.jpg image/jpeg
  • au 声音文件 .au audio/basic
  • MIDI 音乐文件 mid,.midi audio/midi,audio/x-midi
  • RealAudio 音乐文件 .ra, .ram audio/x-pn-realaudio
  • MPEG 文件 .mpg,.mpeg video/mpeg
  • AVI 文件 .avi video/x-msvideo
  • GZIP 文件 .gz application/x-gzip
  • TAR 文件 .tar application/x-tar

ORACLE 对比 MYSQL

行业比对

  1. Oracle 能背锅,MySQL 不能背锅。
  2. Oracle 发展于传统行业,行业特点是数据量少,但是每条数据价值很高(银行账户的一笔交易记录)。Mysql 发展于互联网行业,业务特点是数量巨大,但是数据价值小(一次点击操作)。因为单条价值高,随意使用付费 ORACLE 软件也合理。
  3. 企业应用中使用 Oracle 时大多数业务都在数据库的存储过程中实现,而 Web 应用使用 Mysql 时很多业务逻辑是在 web 服务端上实现。
  4. oracle 目前来说还是占据着主流。除了互联网之外的传统行业基本上还是用的 oracle,无论从规模还是数量上,比 sql 有着大不知道多少倍的基数,想要改弦易辙,何其难也?
  5. 传统行业讲究“稳”字。传统行业的数据生态是基于数据库架构搭建起来的,很多核心业务都已经牢牢绑定了 oracle 这样的重型系统,一旦改用 sql 触动了核心业务,就势必会带来安全问题,传统行业没有必要冒这个风险。(另外,这么牛叉的开源技术也不是几句话就能有的)
  6. 互联网行业讲究“快”字。互联网就是一场赛跑游戏,谁跑的慢谁就要挨打,所以互联网企业可以在更加纯粹的开源环境里不断试错,sql 明显更适合互联网企业的开发和创新。
  7. 数据完整性要求不一致。传统行业不会允许数据出现完整性、一致性的错误,比如对于互联网企业来说,微博上的评论丢了几条没有关系,但是对于传统行业来说哪怕账户上少了一分钱,就足以引起传统行业的重视。
  8. 数据重心不一样。Oracle 的业务数据库规模都不是很大,如果有上百台就是很大的规模了,而互联网行业里面的 MySQL 使用广泛,几百台都是很平常的事情。
  9. Oracle 的产品已经做得足够好了,满足了传统行业的要求。Oracle DBA 的管理模式主要是集中式,因为业务面大,出问题的概率会更高,高级人才在性能优化这方面投入的精力更多,但对于传统行业来说并不是什么问题。
  10. 国内对于 MySQL 和 Oracle 使用的一个误区就是把 MySQL 当 Oracle 用,把 Oracle 当 MySQL 用。单纯比性能其实意义不大,Oracle 肯定完胜 MySQL,要比较水平扩展能力,那还是 MySQL 更加轻量。当然啦。

技术比对

  1. hash join 。Oracle 引进这的时候应该是 90 年代,而今天,Mysql 还没有。这样连接性能肯定不行。当然,这个用变通解决可以绕过去。

  2. with as 。Oracle 中可以 select 查询作为一个中间结果,然后多处引用。这个 mysql 好像最近的 8.X 才有。多土啊,这可是最有用的功能之一。

  3. rownumber。到目前为止,mysql 还没有 row_number。这可是程序员最常用到的功能之一。当然,可以变通解决。

  4. Ananlytic function 和 window function。 这个 Oracle 很强大(比如当前行引用前一行的值)。Mysql 基本可以说是没有(当然也可以变通解决)

  5. Stored procedure 这个 mysql 差太远太远,基本上是步枪对机枪。Mysql 没有 role 吧?程序员得造轮子。(查了下,8.0 有了。)

  6. ORACLE 数据库自带 oracle apex 开发。好像数据库里只有 oracle 愿意投钱搞这个。

  7. vpd,materialized view ,workflow,geological…… 这些 mysql 或者没有或者菜,当然 oracle 也不是每个版本都有。不过这些功能并不是日常必然用到的,所以还好啦。

总之,mysql 短处不少,不过,用熟了之后,大多数是可以找到变通解决方法的。比如大家诟病很多的 Hashjoin 连接,用冗余字段解决,效果肯定比 hashjoin 好。

总结

两种数据库出生在不同时代,ORACLE 作为商用产品是面向企业用户的,mysql 作为免费开源产品是保证一定程度上满足应用对数据库的需要,在 mysql 被 ORACLE 收购之前,功能比较简单,性能也比较一般。但在收购之后,mysql 从功能和性能上都获得了很大的提升,很多方面也在想 ORACLE 数据库看齐。

浏览器原理

参考:https://zhuanlan.zhihu.com/p/47407398
这里以 chrome 为例进行展示说明。

架构

Chrome 采用多进程架构,其顶层存在一个 Browser process 用以协调浏览器的其它进程。

具体说来,Chrome 的主要进程及其职责如下:

  • Browser Process:
    负责包括地址栏,书签栏,前进后退按钮等部分的工作;
    负责处理浏览器的一些不可见的底层操作,比如网络请求和文件访问;
  • Renderer Process:
    负责一个 tab 内关于网页呈现的所有事情
  • Plugin Process:
    负责控制一个网页用到的所有插件,如 flash
  • GPU Process
    负责处理 GPU 相关的任务
    Chrome 还为我们提供了「任务管理器」,供我们方便的查看当前浏览器中运行的所有进程及每个进程占用的系统资源,右键单击还可以查看更多类别信息。

Chrome 多进程架构的优缺点

优点

  • 某一渲染进程出问题不会影响其他进程
  • 更为安全,在系统层面上限定了不同进程的权限
    缺点
  • 由于不同进程间的内存不共享,不同进程的内存常常需要包含相同的内容。
  • 为了节省内存,Chrome 限制了最多的进程数,最大进程数量由设备的内存和 CPU 能力决定,当达到这一限制时,新打开的 Tab 会共用之前同一个站点的渲染进程。

Chrome 把浏览器不同程序的功能看做服务,这些服务可以方便的分割为不同的进程或者合并为一个进程。以 Broswer Process 为例,如果 Chrome 运行在强大的硬件上,它会分割不同的服务到不同的进程,这样 Chrome 整体的运行会更加稳定,但是如果 Chrome 运行在资源贫瘠的设备上,这些服务又会合并到同一个进程中运行,这样可以节省内存,示意图如下。

Site Isolation 机制从 Chrome 67 开始默认启用。这种机制允许在同一个 Tab 下的跨站 iframe 使用单独的进程来渲染,这样会更为安全。
Site Isolation 被大家看做里程碑式的功能, 其成功实现是多年工程努力的结果。Site Isolation 不是简单的叠加多个进程。这种机制在底层改变了 iframe 之间通信的方法,Chrome 的其它功能都需要做对应的调整,比如说 devtools 需要相应的支持,甚至 Ctrl + F 也需要支持。

导航过程发生了什么

也许大多数人使用 Chrome 最多的场景就是在地址栏输入关键字进行搜索或者输入地址导航到某个网站,我们来看看浏览器是怎么看待这个过程的。

我们知道浏览器 Tab 外的工作主要由 Browser Process 掌控,Browser Process 又对这些工作进一步划分,使用不同线程进行处理:

  • UI thread : 控制浏览器上的按钮及输入框;
  • network thread: 处理网络请求,从网上获取数据;
  • storage thread: 控制文件等的访问;

分为以下几步:

  1. 处理输入
    UI thread 需要判断用户输入的是 URL 还是 query;

  2. 开始导航
    当用户点击回车键,UI thread 通知 network thread 获取网页内容,并控制 tab 上的 spinner 展现,表示正在加载中。
    network thread 会执行 DNS 查询,随后为请求建立 TLS 连接。如果 network thread 接收到了重定向请求头如 301,network thread 会通知 UI thread 服务器要求重定向,之后,另外一个 URL 请求会被触发。

  3. 读取响应

    当请求响应返回的时候,network thread 会依据 Content-Type 及 MIME Type sniffing 判断响应内容的格式如果响应内容的格式是 HTML ,下一步将会把这些数据传递给 renderer process,如果是 zip 文件或者其它文件,会把相关数据传输给下载管理器。
    Safe Browsing 检查也会在此时触发,如果域名或者请求内容匹配到已知的恶意站点,network thread 会展示一个警告页。此外 CORB 检测也会触发确保敏感数据不会被传递给渲染进程。

  4. 查找渲染进程

    当上述所有检查完成,network thread 确信浏览器可以导航到请求网页,network thread 会通知 UI thread 数据已经准备好,UI thread 会查找到一个 renderer process 进行网页的渲染。
    由于网络请求获取响应需要时间,这里其实还存在着一个加速方案。当 UI thread 发送 URL 请求给 network thread 时,浏览器其实已经知道了将要导航到那个站点。UI thread 会并行的预先查找和启动一个渲染进程,如果一切正常,当 network thread 接收到数据时,渲染进程已经准备就绪了,但是如果遇到重定向,准备好的渲染进程也许就不可用了,这时候就需要重启一个新的渲染进程。

  5. 确认导航

    进过了上述过程,数据以及渲染进程都可用了, Browser Process 会给 renderer process 发送 IPC 消息来确认导航,一旦 Browser Process 收到 renderer process 的渲染确认消息,导航过程结束,页面加载过程开始。

    此时,地址栏会更新,展示出新页面的网页信息。history tab 会更新,可通过返回键返回导航来的页面,为了让关闭 tab 或者窗口后便于恢复,这些信息会存放在硬盘中。

  6. 额外的步骤

    一旦导航被确认,renderer process 会使用相关的资源渲染页面,下文中我们将重点介绍渲染流程。当 renderer process 渲染结束(渲染结束意味着该页面内的所有的页面,包括所有 iframe 都触发了 onload 时),会发送 IPC 信号到 Browser process, UI thread 会停止展示 tab 中的 spinner。当然上面的流程只是网页首帧渲染完成,在此之后,客户端依旧可下载额外的资源渲染出新的视图。

    在这里我们可以明确一点,所有的 JS 代码其实都由 renderer Process 控制的,所以在你浏览网页内容的过程大部分时候不会涉及到其它的进程。不过也许你也曾经监听过 beforeunload 事件,这个事件再次涉及到 Browser Process 和 renderer Process 的交互,当当前页面关闭时(关闭 Tab ,刷新等等),Browser Process 需要通知 renderer Process 进行相关的检查,对相关事件进行处理。

    如果导航由 renderer process 触发(比如在用户点击某链接,或者 JS 执行 window.location = "http://newsite.com" ) renderer process 会首先检查是否有 beforeunload 事件处理器,导航请求由 renderer process 传递给 Browser process

    如果导航到新的网站,会启用一个新的 render process 来处理新页面的渲染,老的进程会留下来处理类似 unload 等事件。

    除了上述流程,有些页面还拥有 Service Worker (服务工作线程),Service Worker 让开发者对本地缓存及判断何时从网络上获取信息有了更多的控制权,如果 Service Worker 被设置为从本地 cache 中加载数据,那么就没有必要从网上获取更多数据了。

    值得注意的是 service worker 也是运行在渲染进程中的 JS 代码,因此对于拥有 Service Worker 的页面,上述流程有些许的不同。

    当有 Service Worker 被注册时,其作用域会被保存,当有导航时,network thread 会在注册过的 Service Worker 的作用域中检查相关域名,如果存在对应的 Service worker,UI thread 会找到一个 renderer process 来处理相关代码,Service Worker 可能会从 cache 中加载数据,从而终止对网络的请求,也可能从网上请求新的数据。

    如果 Service Worker 最终决定通过网上获取数据,Browser 进程 和 renderer 进程的交互其实会延后数据的请求时间 。Navigation Preload 是一种与 Service Worker 并行的加速加载资源的机制,服务端通过请求头可以识别这类请求,而做出相应的处理。

渲染进程是如何工作的

渲染进程几乎负责 Tab 内的所有事情,渲染进程的核心目的在于转换 HTML CSS JS 为用户可交互的 web 页面。渲染进程中主要包含以下线程:

  • 主线程 Main thread
  • 工作线程 Worker thread
  • 排版线程 Compositor thread
  • 光栅线程 Raster thread
  1. 构建 DOM
    当渲染进程接收到导航的确认信息,开始接受 HTML 数据时,主线程会解析文本字符串为 DOM。

渲染 html 为 DOM 的方法由 HTML Standard 定义。

  1. 加载次级的资源

    网页中常常包含诸如图片,CSS,JS 等额外的资源,这些资源需要从网络上或者 cache 中获取。主进程可以在构建 DOM 的过程中会逐一请求它们,为了加速 preload scanner 会同时运行,如果在 html 中存在 <img> <link> 等标签,preload scanner 会把这些请求传递给 Browser process 中的 network thread 进行相关资源的下载。

  2. JS 的下载与执行

    当遇到 <script> 标签时,渲染进程会停止解析 HTML,而去加载,解析和执行 JS 代码,停止解析 html 的原因在于 JS 可能会改变 DOM 的结构(使用诸如 document.write()等 API)。

    不过开发者其实也有多种方式来告知浏览器应对如何应对某个资源,比如说如果在<script> 标签上添加了 asyncdefer 等属性,浏览器会异步的加载和执行 JS 代码,而不会阻塞渲染。

  3. 样式计算

    仅仅渲染 DOM 还不足以获知页面的具体样式,主进程还会基于 CSS 选择器解析 CSS 获取每一个节点的最终的计算样式值。即使不提供任何 CSS,浏览器对每个元素也会有一个默认的样式。

  4. 获取布局

    想要渲染一个完整的页面,除了获知每个节点的具体样式,还需要获知每一个节点在页面上的位置,布局其实是找到所有元素的几何关系的过程。其具体过程如下:

    通过遍历 DOM 及相关元素的计算样式,主线程会构建出包含每个元素的坐标信息及盒子大小的布局树。布局树和 DOM 树类似,但是其中只包含页面可见的元素,如果一个元素设置了 display:none ,这个元素不会出现在布局树上,伪元素虽然在 DOM 树上不可见,但是在布局树上是可见的。

  5. 绘制各元素

    即使知道了不同元素的位置及样式信息,我们还需要知道不同元素的绘制先后顺序才能正确绘制出整个页面。在绘制阶段,主线程会遍历布局树以创建绘制记录。绘制记录可以看做是记录各元素绘制先后顺序的笔记。

  6. 合成帧

    熟悉 PS 等绘图软件的童鞋肯定对图层这一概念不陌生,现代 Chrome 其实利用了这一概念来组合不同的层。

    复合是一种分割页面为不同的层,并单独栅格化,随后组合为帧的技术。不同层的组合由 compositor 线程(合成器线程)完成。

    主线程会遍历布局树来创建层树(layer tree),添加了 will-change CSS 属性的元素,会被看做单独的一层

    你可能会想给每一个元素都添加上 will-change,不过组合过多的层也许会比在每一帧都栅格化页面中的某些小部分更慢。为了更合理的使用层,可参考 坚持仅合成器的属性和管理层计数 。

    一旦层树被创建,渲染顺序被确定,主线程会把这些信息通知给合成器线程,合成器线程会栅格化每一层。有的层的可以达到整个页面的大小,因此,合成器线程将它们分成多个磁贴,并将每个磁贴发送到栅格线程,栅格线程会栅格化每一个磁贴并存储在 GPU 显存中。

    一旦磁贴被光栅化,合成器线程会收集称为绘制四边形的磁贴信息以创建合成帧。

    合成帧随后会通过 IPC 消息传递给浏览器进程,由于浏览器的 UI 改变或者其它拓展的渲染进程也可以添加合成帧,这些合成帧会被传递给 GPU 用以展示在屏幕上,如果滚动发生,合成器线程会创建另一个合成帧发送给 GPU。

重排 重绘 动画

  • 当改变一个元素的尺寸,位置,属性时发生重排。触发样式计算,布局,绘制记录,图层的步骤都要进行。

  • 当改变一个元素的颜色时发生重绘。触发样式计算和绘制记录。

动画与 JS 的矛盾

渲染进程中的主线程,除了执行上图的那五个操作之后,还会有 JavaScript 占用主线程。 如果主线程中有每帧都需要重排重绘的动画,那么主线程在每帧都要分一部分时间执行重排重绘。如果此时还有一个 JavaSript 来抢占主线程,那么有可能发生动画卡顿。

  • 方法一
    通过使用 requestAnimationFrame()方法,可以把 js 分隔成若干片段,在每帧运行。避免了与重排重绘占用时间。React Fiber 使用了这个 API 做了很多优化。
  • 方法二
    通过 Transform 属性,这样动画只占用合成器线程和栅格线程,不会与主线程上的 JS 冲突。

浏览器对事件的处理

浏览器通过对不同事件的处理来满足各种交互需求,这一部分我们一起看看从浏览器的视角,事件是什么,在此我们先主要考虑鼠标事件。

在浏览器的看来,用户的所有手势都是输入,鼠标滚动,悬置,点击等等都是。
当用户在屏幕上触发诸如 touch 等手势时,首先收到手势信息的是 Browser process, 不过 Browser process 只会感知到在哪里发生了手势,对 tab 内内容的处理是还是由渲染进程控制的。

事件发生时,浏览器进程会发送事件类型及相应的坐标给渲染进程,渲染进程随后找到事件对象并执行所有绑定在其上的相关事件处理函数。
前文中,我们提到过合成器可以独立于主线程之外通过合成栅格化层平滑的处理滚动。如果页面中没有绑定相关事件,组合器线程可以独立于主线程创建组合帧。如果页面绑定了相关事件处理器,主线程就不得不出来工作了。这时候合成器线程会怎么处理呢?

这里涉及到一个专业名词「理解非快速滚动区域(non-fast scrollable region)」由于执行 JS 是主线程的工作,当页面合成时,合成器线程会标记页面中绑定有事件处理器的区域为 non-fast scrollable region ,如果存在这个标注,合成器线程会把发生在此处的事件发送给主线程,如果事件不是发生在这些区域,合成器线程则会直接合成新的帧而不用等到主线程的响应。

** 事件的优化 **

一般我们屏幕的刷新速率为 60fps,但是某些事件的触发量会不止这个值,出于优化的目的,Chrome 会合并连续的事件(如 wheel, mousewheel, mousemove, pointermove, touchmove ),并延迟到下一帧渲染时候执行 。

而如 keydown, keyup, mouseup, mousedown, touchstart, 和 touchend 等非连续性事件则会立即被触发。