后端工程师技能图谱
2017-11-16 16:51:13
登录查看完整内容
举报
猜你喜欢
大纲/内容
后端工程师技能知识点图谱
架构
现象与概念
服务雪崩效应
服务A的不可用导致调用者B服务不可用,最终导致更大范围的服务不可用
缓存穿透
降级
预案
方式
自动降级
人工降级
读服务降级
DB -> Cache
Cache -> 兜底数据
写服务降级
多级降级
异步
异步Future
异步Callback
异步编排
限流
算法
令牌桶算法
可以应对突发情况
漏桶算法
应用级限流
分布式限流
Redis+Lua
Nginx+Lua
节流
某个时间段内相同请求只处理一次
高并发设计原则
应用无状态
服务拆分
服务化
消息队列
数据异构
缓存
并发化
高可用原则
切流量
可回滚
后台系统操作可反馈
后台系统审批化
文档和注释
备份
业务设计
原则
防重设计
幂等设计
流程可定义
状态与状态机
分布式系统
数据一致性
CAP定理
一致性(Consistence)等同于所有节点访问同一份最新的数据副本
可用性(Availability)每次请求都能获取到非错的响应,但是不保证获取的数据为最新数据
分区容错性(Network partitioning) 以实际效果而言,分区相当于对通信的时限要求。 系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况, 必须就当前操作在C和A之间做出选择。
放弃项
放弃C
放弃一致性不代表完全放弃数据一致性,这样的话系统就没有意义了。而是放弃数据的强一致性,保留最终一致性。这样的系统无法保证数据保持实时的一致性,但能够承诺数据最终会达到一个一致的状态。
放弃A
放弃可用性就是在系统遇到网络分区或其他故障时,受影响的服务可以暂时不对外提供,等到系统恢复后再对外提供服务。
放弃P
如果希望能够避免出现分区容错性问题,一种较为简单的做法是将所有数据放在一个节点上。这样肯定不会受网络分区影响。但此时分布式系统也失去了意义。
数据强一致性
实现CP
系统中的某个数据被成功更新后,后续任何对该数据的读取操作都将得到更新后的值
数据弱一致性
实现CA
数据最终一致性
分布式事务
协议
2PC
过程
提交
确认提交
角色
协调者
参与者
存在的问题
单点问题
事务管理器执行第一阶段后,如果发生宕机,则事务被阻塞
同步阻塞
在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源
数据不一致
第二阶段中如果发生部分参与者因为网络等原因没有收到COMMIT,则集群的数据一致性会出现问题
解决方法
超时机制
问询机制
让参与者 A 去询问其他参与者 B 的执行情况
场景
如果 B 执行了 rollback 或 commit 操作,则 A 可以大胆的与 B 执行相同的操作
如果 B 此时还没有到达 READY 状态,则可以推断出协调者发出的肯定是 rollback 通知
如果 B 同样位于 READY 状态,则 A 可以继续询问另外的参与者。
只有当所有的参与者都位于 READY 状态时,此时两阶段提交协议无法处理,将陷入长时间的阻塞状态。
3PC
比2PC增加一个预提交过程
协调者需要收到所有的确认才会进入下一步,否则abort
预提交
参与者在这个阶段未收到 pre commit 会自动 abort
参与者在这个阶段未收到 commit ,会自动提交
存在问题
数据不一致问题仍然是存在的,比如第三阶段协调者发出了 abort 请求,然后有些参与者没有收到 abort,那么就会自动 commit,造成数据不一致。
实现
AT模式 (Automatic Transaction Mode)
SQL代理与回滚日志
无侵入的分布式事务解决方案,适用于不希望对业务进行改造的场景,几乎0学习成本
TCC模式
预留与释放
高性能分布式事务解决方案,适用于核心系统等对性能有很高要求的场景。
saga模式
长事务
是长事务解决方案,适用于业务流程长且需要保证事务最终一致性的业务系统,Saga模式一阶段就会提交本地事务,无锁,长流程情况下可以保证性能,多用于渠道层、集成层业务系统。事务参与者可能是其它公司的服务或者是遗留系统的服务,无法进行改造和提供 TCC 要求的接口,也可以使用 Saga 模式。
XA模式
分布式强一致性的解决方案,但性能低而使用较少
分布式数据一致性
Paxos
Leader
Learn
Follower
算法过程
开源实现
zookeeper
ZAB协议
Zab 协议必须确保提交已经被 Leader 提交的事务提案,同时舍弃被跳过的提案,这也就是说当前集群中最新 ZXID 最大的服务器会被选举成为 Leader 节点
是Paxos协议变种,与Paxos协议不同之处在于主要是为构建高可用的主备系统设计的,而 Paxos 能够帮助工程师搭建具有一致性的状态机系统。
文件系统
事件通知
会话
应用
发布订阅
命名服务
分布式锁
配置管理
Raft
三种角色
Candidate
发送投票请求到各个节点
未收到来自Leader的消息就会成为Candidate
会为节点投票
最开始默认的角色
Leader Election
Candidate设置一个随机的Election Timeout:150ms-300ms
超时结束,发起投票,会为自己投一票
Follower节点如果还没投票,会进行投票
获得大部分选票,成为Leader,发送append entries
发送append entries 其他节点存在一个heartbeat timeout
Log Replication
客户端向Raft的Leader发送数据
Leader写入entry
Leader发送log entry到Follower
Follower确认,返回消息给Leader
Leader提交并返回客户端,并发送提交确认给Follower
Follower提交
网络分区
两个分区合并,比较term高的,回滚低的一方所有没被commit的log
集群脑裂
Leader lease(租期)
算法演示
Raft算法演示
etcd
分布式ID
UUID
算法的核心思想是结合机器的网卡、当地时间、一个随记数来生成UUID。
数据库自增ID
使用数据库的id自增策略,如 MySQL 的 auto_increment。并且可以使用两台数据库分别设置不同步长,生成不重复ID的策略来实现高可用
批量生成ID
一次按需批量生成多个ID,每次生成都需要访问数据库,将数据库修改为最大的ID值,并在内存中记录当前值及最大值
Redis生成ID
Redis的所有命令操作都是单线程的,本身提供像 incr 和 increby 这样的自增原子命令,所以能保证生成的 ID 肯定是唯一有序的
snowflake
数据格式
优点
速度快
无依赖
缺点
时间回拨问题
原因
运维人为原因
时间差距小可以等时间追上来
时间差距大需要修改 workerID
CDN
主体对象
静态资源,如网站上面上传的图片、媒体,以及引入的一些Js、css等文件
防止CDN被绕过
禁止IP直接访问
微服务
对比
单体应用
优势
开发
稳定
性能
减少网络开销和传输序列化
部署
劣势
中心化
耦合
学习成本
伸缩
持续交付
SOA
类同
面向服务
松耦合
模块化
分布式计算
平台无关性
差异
原子性
微服务粒度更小
领域驱动设计
DDD
开发运维体系
微服务挑战
基础组件
网关
职责
路由
流控
监控点
qps
状态码
连接数
响应时间
负载均衡
客户端负载均衡
服务端负载均衡
服务发现和注册
consul
通讯协议
HTTP Restful
RPC
自实现协议
流量控制
熔断器
实践
Java Spring体系
Golang Go-Micro体系
设计
框架
Netflix OSS
Zuul网关
Ribbon负载均衡
日志
检索
ElasticSearch
界面
Kibana
收集&过滤器
Logstash
Fluentd
收集器
filebeat
中间件
数据库分库分表(能用分区绝对别用分表)
Mycat
高可用服务组件
Keepalived
VRRP(Virtual Router Redundancy Protocol,虚拟路由器冗余协议)
IP漂移
当主服务器不能正常工作时,Keepalived 会将虚拟地址IP(VIP)绑定到备用服务端上
分类
四层(TCP传输层)负载均衡
对IP和端口
七层(HTTP应用层)负载均衡
对URL
软件
物理负载均衡器
F5
软件负载均衡器
Nginx
四层负载均衡
七层负载均衡
HAProxy
LVS
LVS DR
改写二层中的Mac地址
由real-server直接返回给客户端
LVS IP隧道
改写IP地址
直接返回
LVS Net
由LB返回
round-robin
ip-hash
记录请求分配的IP
key-hash
通过URL算出key
least-conn
商业版本的nginx支持,使用最少连接的backend
隔离
类型
线程隔离
进程隔离
集群隔离
机房隔离
读写隔离
动静隔离
爬虫隔离
热点隔离
资源隔离
典型例子
Hystrix
对不同的HTTP服务分配线程池
某个服务线程池耗尽则自动降级
不会影响其他服务
方法1:取号处理
订单下单前生成订单号,利用订单号下单
方法2:写入幂等
点赞不对数量直接+1,而是采取写唯一记录来统计
中台
是什么
数据库
背景知识
事务
由一组SQL语句组成的逻辑处理单元
ACID模型
A 原子性
整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
C 一致性
在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。
I 隔离性
数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致
事务隔离等级
未提交读(Read uncommitted)
已提交读(read committed)
可重复读(repeatable read)
可序列化(Serializable)
隔离级别对应解决的并发读问题
D 持久性
事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
并发事务
问题
更新丢失
两个并发更新事务,后者的更新覆盖前者
因为行锁的存在,数据库阻止了更新丢失
脏读
脏读指的就是在不同的事务下,当前事务可以读到另外事务未提交的数据
脏读发生的条件是需要事务的隔离级别为READ UNCOMMITTED
幻读
一个事务内两次读到的数据是不一样的情况
一般来说,幻读的问题是可以接受的,因为其读到的是已经提交的数据,本身并不会带来很大的问题。
数据库事务的默认隔离级别设置为READ COMMITTED,在这种隔离级别下允许幻读的现象
不可重复读
一个事务读取了一行数据,一段时间后再次读取该行数据发现该行数据已经被删除
解决
应用层面解决
数据库层面解决
加锁
MVCC:数据多版本并发控制
不用加任何锁,通过一定机制生成一个数据请求时间点的一致性快照,并用这个快照来提供一定级别的一致性读取
从用户角度看,好像数据库提供了数据的多个版本
范式
第一范式
列的原子性
第二范式
主键和非主键之间唯一对应,不出现多个主键
第三范式
对于主键的依赖关系不能存在传递
关系型数据库
Mysql(MariaDB)数据库
基础
安装
Linux
Docker
version: '2'services: mysql1: container_name: mysql1 image: mysql restart: always ports: - 3306:3306 environment: MYSQL_ROOT_PASSWORD: 111111 volumes: - mysql-data1:/var/lib/mysql - ./etc1:/etc/mysql/conf.d networks: - mysqlnet mysql2: container_name: mysql2 image: mysql restart: always ports: - 3307:3306 environment: MYSQL_ROOT_PASSWORD: 111111 volumes: - mysql-data2:/var/lib/mysql - ./etc2:/etc/mysql/conf.d networks: - mysqlnet mysql3: container_name: mysql3 image: mysql restart: always ports: - 3308:3306 environment: MYSQL_ROOT_PASSWORD: 111111 volumes: - mysql-data3:/var/lib/mysql - ./etc3:/etc/mysql/conf.d networks: - mysqlnet adminer: image: adminer restart: always ports: - 8080:8080 networks: - mysqlnetvolumes: mysql-data1: driver: local mysql-data2: driver: local mysql-data3: driver: localnetworks: mysqlnet:
体系结构
连接池管理
管理服务和工具组件
SQL接口组件
查询分析器组件
优化器组件
缓冲组件
插件式存储引擎
存储引擎是基于表的,而不是数据库
物理文件
SQL基础
DCL
GRANT语句
REVOKE语句
DML
INSERT语句
UPDATE语句
DELETE语句
SELECT语句
DDL
CREATE语句
ALTER语句
DROP语句
TRUNCATE
管理语句
工具语句
EXPLAIN
DESCRIBE
SHOW
数据类型
数值类型
整数类型
TINYINT (1字节)
有符号:-128-127
无符号:0-255
SMALLINT(2)
MEDIUMINT(3)
INT、INTEGER(4)
BIGINT(8)
对于整数类型,括号后的数字表示『显示宽度』,配合zerofill使用
浮点数类型
FLOAT(4)
DOUBLE(8)
不指定精度按照计算机硬件默认限制
定点数类型
MySQL内部是以字符串存放,高精度
不指定精度,默认整数位10位,小数位0位
位类型
时间日期类型
DATE(4)
最小值:1000-01-01
最大:9999-12-31
DATETIME (8)
最小:1000-01-01 00:00:00
最大:9999-12-31 23:59:59
TIMESTAMP(4)
最大:2038年某个时刻
返回值显示为 YYYY-MM-DD HH:MM:SS
显示宽度为19个字符
如果想要获取数字值,应在TIMESTAMP列添加『+0』
系统会自动给第一个TIMESTAMP字段自动添加CURRENT_TIMESTAMP,第二个开始默认设置为0
不能存在多个CURRENT_TIMESTAMP
与本地时区相关
TIME(3)
YEAR(1)
最小:1901
最大:2155
超出有效范围均使用 0 值
字符串类型
CHAR(M)
M为0-255字节
select会删除尾部空白字符
VARCHAR(M)
BINARY
VARBINARY
TINYBLOB
BLOB
MEDIUMBLOB
LONGBLOB
TINYTEXT
TEXT
M为0-65535,值的长度+2个字节存储
ENUM
1-255需要1字节
256-65535需要两个字节
SET
可以包含0-64个成员
NULL
对于NULL值,仍然可以使用添加索引
ORDER BY ... AES, NULL值会被放到前面
COUNT,MIN,SUM会忽略NULL值
COUNT(*)例外
运算符
算术运算符
比较运算符
位运算符
常用函数
字符串函数
concat()
insert()
将字符串从x位置开始,y个字符长的子串替换为字符串str
数值函数
日期和时间函数
流程函数
其他常用函数
工具
phpmyadmin
sequelPro
workbench
存储引擎
MyISAM
特点
不支持事务
不支持外键
支持表锁
支持全文索引
适合读场景,速度快
表可能会损坏
会去除数据尾部空格
存储限制 256T
无数据缓存,有索引缓存
允许没有任何索引和主键的表存在,索引都是保存行的地址
保存有表的总行数,如果select count(*) from table;会直接取出出该值
auto_increatement:可以和其他字段一起建立联合索引。引擎的自动增长列必须是索引,如果是组合索引,自动增长可以不是第一列,他可以根据前面几列进行排序后递增
数据可压缩
静态(固定长度)表
动态表
压缩表(✌️)
空间,内存使用低
不支持MVCC(多版本并发控制)
table summary=\"MyISAM Storage EngineFeatures\" border=\"1\"Storage limits256TBTransactionsNoLocking granularityTableMVCCNoGeospatial data type supportYesGeospatial indexing supportYesB-tree indexesYesT-tree indexesNoHash indexesNoFull-text search indexesYesClustered indexesNoData cachesNoIndex cachesYesCompressed dataYes[a]Encrypted data[b]YesCluster database supportNoReplication support[c]YesForeign key supportNoBackup / point-in-time recoverya name=\"idm140295516992432\
存储格式
.frm 存储表定义
.MYD 存储数据
.MYI 存储索引
InnoDB
概览
Storage limits64TBTransactionsYesLocking granularityRowMVCCYesGeospatial data type supportYesGeospatial indexing supportYes[a]B-tree indexesYesT-tree indexesNoHash indexesNo[b]Full-text search indexesYes[c]Clustered indexesYesData cachesYesIndex cachesYesCompressed dataYes[d]Encrypted data[e]YesCluster database supportNoReplication support[f]YesForeign key supportYesBackup / point-in-time recoverya name=\"idm140295535622800\
InnoDB的ACID模型
Atomicity
自动提交
COMMIT语句
ROLLBACK语句
Consistency
双写缓存
奔溃恢复机制
Isolation
需要原子性的支持
SET ISOLATION LEVEL 语句
底层InnoDB locking
Durability
innodb_flush_log_at_trx_commit设置
控制重做日志刷新到磁盘的策略
InnoDB是事务的存储引擎,其通过Force Log at Commit机制实现事务的持久性,即当事务提交(COMMIT)时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的COMMIT操作完成才算完成
sync_binlog设置
innodb_file_per_table设置
存储设备中的写缓存
存储设备的Battery-backed cache
操作系统的fsync系统调用
为了确保每次日志都写入重做日志文件,在每次将重做日志缓冲写入重做日志文件后,InnoDB存储引擎都需要调用一次fsync操作。
UPS电力保障
备份策略
分布式集群
体系架构
后台线程
Master线程
负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性。包括合并插入缓冲,UNDO页的回收
IO线程
InnoDB大量使用AIO来处理IO请求,IO线程就是负责这些IO请求的回调处理
读写线程
read thread
受 innodb_read_io_threads 参数影响
读线程ID总是小于写线程
write thread
受 innodb_write_io_threads 参数影响
日志线程
INSERT BUFFER线程
Purge线程
回收事务被提交后不再需要的undolog
Page Cleaner线程
将脏页的刷新操作放入这个线程完成
减轻Master线程的工作和对于用户查询线程的阻塞
内存
缓冲池
将磁盘页缓存在内存池中
缓存页大小默认为16K
缓存的对象
索引页
数据也
undo页
插入缓冲
自适应哈希索引
InnoDB存储的锁信息
数据字典信息
修改页数据先更新缓存页,然后以一定频率刷新回磁盘
checkpoint机制
大小可以通过参数 innodb_buffer_pool_size 来设置
InnoDB 1.0 版本开始允许有多个缓冲池实例
可以通过参数 innodb_buffer_pool_instances 修改
淘汰算法
改进的LRU
增加midpoint位置
新页面插入到这个位置而不是首部
midpoint之前的叫做new列表,之后的叫做old列表
把页面从old加入new的过程叫做 Pages made young
默认是LRU列表长度的5/8处
可以通过参数 innodb_old_blocks_pct 修改
参数 innodb_old_blocks_time 用于表示页读取到mid位置后需要等待多久才会被加入到LRU列表的热端
Free List
数据库启动时,LRU列表是空的,页都放在FreeList中
当需要从缓冲池分页时,先查询FreeList是否有空闲的页,有的话从FreeList删除,加入LRU列表。没有执行LRU淘汰算法
页压缩
将原本16k的页分别压缩到 1k 2k 4k 8k
对于非16K的页面,使用unzip_LRU列表管理
脏页
在LRU列表中的页面被修改
缓冲页和磁盘数据不一致
脏页即存在于LRU列表,也存在Flush列表
重做日志缓冲
InnoDB存储引擎现将重做日志存放到这个缓存区,然后按一定频率将其刷新到重做日志文件
重做日志一般不需要设置很大,默认是1s刷新一次,保证则沟容纳1s内的事务量即可
参数 innodb_log_buffer_size 默认 8M
重做日志缓冲刷新时机
Master线程一秒刷新一次
每个事务提交时会将重做日志缓冲刷新到重做日志文件
重做日志缓冲池剩余空间小于一半
额外内存池
对一些数据结构的内存分配,需要从这个内存池获取
checkpoint技术
MASTER 线程工作方式
关键特性
对于非聚集索引的插入更新操作,先判断插入的非聚集索引页是否在缓冲池中,在则直接插入,不在则放入InsertBuffer中,然后以一定的频率进行InsertBuffer和辅助索引页子节点的merge操作
条件
索引是辅助索引
索引不是唯一的
Change Buffer
内部实现
Merge Insert Buffer
两次写
在应用(apply)重做日志前,用户需要一个页的副本,当写入失效发生时,先通过页的副本来还原该页,再进行重做,这就是doublewrite
自适应哈希索引(AHI)
查询复杂度O(1)
InnoDB会监控对表上各索引页的查询,观察到建立哈希索引可以带来速度提升,则建立
通过缓冲池的B+树页构造而来
异步IO
刷新邻接页
文件
参数文件
日志文件
错误日志
慢查询日志
查询日志
二进制日志
逻辑日志
套接字文件
pid文件
表结构定义文件
frm文件
存储引擎文件
表空间文件
InnoDB采用将存储的数据按表空间进行存放的设计
默认会有一个初始大小10M的ibdata1文件
设置innodb_data_file_path参数后,所有基于InnoDB存储引擎的表的数据都会记录到该共享空间中
用户可以通过多个文件组成一个表空间
设置参数 innodb_file_per_table 后,用户可以将每个基于InnoDB存储引擎的表产生一个独立表空间
表名.idb
仅存放数据索引和插入缓冲BitMap信息,其他信息还是放在默认的表空间
重做日志文件
重做日志记录的是事务中页修改的物理情况
datadir目录下默认会有两个表名:ib_logfile0和ib_logfile1
记录了对于InnoDB存储引擎的事务日志
保证数据完整性
每个InnoDB存储引擎至少有一个重做日志文件组
格式
redo_log_type占1字节
space表示表空间的ID,但采用压缩的方式,因此占用的空间可能小于四字节
page_no表示页的偏移量,同样采用压缩的方式
redo_log_body标识每个重做日志的数据部分,恢复时需要调用相应的函数进行解析
表
索引组织表
表都是根据主键顺序组织存放的,这种存储方式叫做索引组织表
InnoDB逻辑存储结构
表空间
InnoDB存储引擎结构的最高层
段
数据段
B+树的叶子节点
索引段
非B+树的叶子节点
回滚段
区
由连续页组成的空间
在任何情况下都是1M
为了保证区中页的连续性,InnoDB一次从磁盘申请4-5个区
默认情况下,InnoDB存储引擎页的大小16K,即一个区一共有64个连续的页
刚开始可能会使用32个页大小的碎片页
不同格式的压缩页对应的页的数量不同
页
InnoDB磁盘管理的最小单位
数据页(B-tree Node)
系统页
行
文件格式
Antelope
Barracuda
InnoDB 1.0.x 开始
InnoDB行记录格式
Compact
MySQL 5.0 中引入
变长字段长度列表
若列的长度小于255个字节,用1字节表示
若列的长度大于255个字节。用2字节表示
最大不能超过2字节,因为varchar最大长度限制在65535字节
NULL 标志位
指示了该行数据中是否有NULL值
记录头信息
固定占用5字节
含义
列数据
NULL除了占用NULL标志位,其他字段不占存储
两个隐藏列
事务ID
6字节
回滚指针列
7字节
如果没有定义主键
增加6字节的rowid
Redundant
MySQL5.0以前
Compressed
新的两种记录格式对于存放在BLOB中的数据采用了完全的行溢出的方式
Dynamic
行溢出数据
数据都是存放在页类型为B-tree Node中,如果发生行溢出才会把数据存放在页类型为Uncompress BLOB页中,保留768字节前缀在数据页
InnoDB数据页结构
索引与算法
B+树
插入
第一种情况,将28插入
第二种情况,插入70
实际上并不着急分裂页,做调整即可
第三种情况,插入95
删除
填充因子
50%是可设的最小值
第一种情况,删除70
第二种情况,删除25
第三种情况,删除60
InnoDB支持几种常见的索引
B+树索引
高扇出性,所以高度一般为2-4层
聚簇索引(clustered index)
按照每张表的主键构造的B+树,叶子节点存放的是整张表的行记录,也就是数据页
每个数据页通过一个双向链表连接
数据页上存放的是完整的每行的记录
非数据页的索引页中,存放的仅仅是键值及指向数据页的偏移量,而不是一个完整的行记录
辅助索引(secondary index,非聚簇索引)
叶子节点并不包含行记录的全部数据
叶子节点除了包含键值以外,每个叶子节点中的索引行中还包含了一个书签(bookmark)。该书签用来告诉InnoDB存储引擎哪里可以找到与索引相对应的行数据。
每张表上可以有多个辅助索引
当通过辅助索引来寻找数据时,InnoDB存储引擎会遍历辅助索引并通过叶级别的指针获得指向主键索引的主键,然后再通过主键索引来找到一个完整的行记录
管理
关注Cardinality
Cardinality表的行数应尽可能接近1,如果非常小,那么用户需要考虑是否可以删除此索引
如果需要更新索引Cardinality的信息,可以使用ANALYZE TABLE命令
避免线上修改索引导致服务不可用
快速索引创建(Fast Index Creation)
对于辅助索引的创建,InnoDB存储引擎会对创建索引的表加上一个S锁
删除辅助索引操作InnoDB存储引擎只需更新内部视图,并将辅助索引的空间标记为可用,同时删除MySQL数据库内部视图上对该表的索引定义即可
由于FIC在索引的创建的过程中对表加上了S锁,因此在创建的过程中只能对该表进行读操作
Online DDL
MySQL 5.6版本开始支持
允许辅助索引创建的同时,还允许其他诸如INSERT、UPDATE、DELETE这类DML操作
PT工具
使用
联合索引
对于b列的查询SELECT*FROM TABLE WHERE b=xxx,则不可以使用这棵B+树索引。
覆盖索引
从辅助索引中就可以得到查询的记录,而不需要查询聚集索引中的记录
Multi-Range Read
MySQL5.6版本开始支持
MRR使数据访问变得较为顺序。在查询辅助索引时,首先根据得到的查询结果,按照主键进行排序,并按照主键排序的顺序进行书签查找
减少缓冲池中页被替换的次数
批量处理对键值的查询操作
工作过程
将查询得到的辅助索引键值存放于一个缓存中,这时缓存中的数据是根据辅助索引键值排序的
将缓存中的键值根据RowID进行排序
根据RowID的排序顺序来访问实际的数据文件
Index Condition Pushdown(ICP)
在支持Index Condition Pushdown后,MySQL数据库会在取出索引的同时,判断是否可以进行WHERE条件的过滤,也就是将WHERE的部分过滤操作放在了存储引擎层。
全文索引
倒排索引
哈希索引
自适应,不能人为干预
锁
Next-Key锁
InnoDB默认支持 Repeatable Read
InnoDB存储引擎在默认的REPEATABLE READ的事务隔离级别下已经能完全保证事务的隔离性要求,即达到SQL标准的SERIALIZABLE隔离级别。
内部事务
事务在存储引擎与插件之间,又或者在存储引擎与存储引擎之间,称之为内部XA事务。
最为常见的内部XA事务存在于binlog与InnoDB存储引擎之间
在事务提交时,先写二进制日志,再写InnoDB存储引擎的重做日志。对上述两个操作的要求也是原子的,即二进制日志和重做日志必须同时写入。
外部事务
备份与恢复
Memory
把所有的数据放在内存中,如果发生宕机,数据会丢失
MERGE
做数据库分表
NDB
一个集群存储引擎
数据全部放在内存中
BLACKHOLE
只写操作的binlog,数据不写进磁盘
选择合适的数据类型
char与varchar
char是固定长度的,char(4)都会占用4个字节
varchar会比保存的内存多加一个字节
char会删除尾部空格
char检索速度更快
innodb内部行存储没有区分固定长度和可变长度,建议用varchar
TEXT与BLOB
会引起性能问题,删除大数据量的行会带来『空洞』,需要optimize
利用合成的索引来提高检索性能
合成索引就是根据大文本内容建立一个散列值
避开检索TEXT和BLOB字段
把大文本字段单独成表
浮点数与定点数
货币要用定点数
浮点数不能直接做比较
日期选择
按需使用
长日期建议用DATETIME,不要用TIMESTAMP
如果需要多时区用户使用,建议用TIMESTAMP
字符集
Unicode
为了解决ASCII无法容纳多国语言的问题
ISO发布UCS标准,,采用4字节编码,又称UCS-4
反对UCS-4,成立Unicode小组
将Unicode编入UCS的0组0面 => BMP基本多语言文字面 => UCS-2
UTF-8
中文占3字节
Unicode只是规定了码点,没有说明存储方式,UTF8就是存储的实现
UTF8:通过一定规则将Unicode转换成1-4字节的编码
MySQL
级别
服务器级别
库级别
表级别
字段级别
字符的编码
Collection
提供给字符集做比较操作的一组规则
索引设计与使用
概念
所有类型都可以被索引
提高SELECT的性能
每种存储引擎对表至少支持16个索引,总索引长度至少256字节
MyISAM和InnoDB默认创建的是Btree索引
目前还不支持函数索引,但是支持前缀索引
前缀索引:对索引字段的前N个字符创建索引
InnoDB存储引擎使用REDUNDANT或者COMPACT行格式下,最长限制在767字节
InnoDB存储引擎使用DYNAMIC或者COMPRESSED行格式下,最长限制在3072字节
MyISAM限制1000字节
CREATE [UNIQUE|FULLTEXT|SPATIAL|] INDEX index_name USING index_type ON tablename (index_col_name)
数据结构区分
B+Tree索引
Hash索引
R+Tree索引
物理存储区分
聚簇索引
聚簇索引也叫簇类索引,是一种对磁盘上实际数据重新组织以按指定的一个或多个列的值排序
只能有一个,一般是主键或者唯一键,没有指定的话数据库会生成
物理上连续,可以理解为叶节点就是数据节点
非聚簇索引
二级索引,叶节点还是个索引,逻辑上连续物理上不连续,InnoDB的二级索引在叶子节点中存储了 primary key的值
聚簇索引的叶节点就是数据节点,而非聚簇索引的叶节点仍然是索引节点,并保留一个链接指向对应数据块。
逻辑上区分
主键索引
普通索引(单列索引)
复合索引
唯一索引
空间索引
设计原则
索引最适合出现在where字句中的field,而不是select 中的field
使用唯一索引,基数越大索引效果越好
对于字符串字段,使用短索引(前缀索引)
利用最左前缀匹配原则
不要过度索引,索引会占据磁盘空间,降低写操作性能
对于InnoDB的索引,记录默认会按照一定顺序记录,顺序使用的优先级:主键 -> 唯一键 -> 内部生成所以选择主键要经常出现在where中,且短小,因为InnoDB中普通索引也会保存主键值
BTREE索引与Hash索引
BTREE
InnoDB利用倒排索引存储FULLTEXT
Hash
视图
视图是一种虚拟存在的表
存储过程和函数
概念:存储过程和函数是事先经过编译并存储在数据库中的一段SQL语句集合,函数必须有返回值,存储过程没有
DELIMITER 切换分隔符
定义语法
触发器
触发器是与表有关的数据库对象,在满足定义条件时触发,并执行触发器
事务控制与锁定语句
锁类型
表级锁
MEMORY
行级锁
锁表操作
lock table xxx read|write
unlock table
扁平事务(Flat Transactions)
带有保存点的扁平事务(Flat Transactions with Savepoints)
链事务(Chained Transactions)
嵌套事务(Nested Transactions)
分布式事务(Distributed Transactions)
控制语法
START TRANSACTION
会造成一个隐含的unlock table
COMMIT&ROLLBACK
CHAIN&RELEASE
SET AUTOCOMMIT
SAVEPOINT
redo
作用
用来实现事务的持久性
组成
内存中的重做日志缓冲(redo log buffer),其是易失的
重做日志文件(redo log file),其是持久的
undo
如果用户执行的事务或语句由于某种原因失败了,又或者用户用一条ROLLBACK语句请求回滚,就可以利用这些undo信息将数据回滚到修改之前的样子
在InnoDB存储引擎中MVCC的实现是通过undo来完成。当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过undo读取之前的行版本信息,以此实现非锁定读取。
undo存放在数据库内部的一个特殊段(segment)中,这个段称为undo段(undo segment)
undo段位于共享表空间内
undo log会产生redo log
purge
purge用于最终完成delete和update操作
delete语句只是将记录标记为删除状态,实际的删除是在purge中执行的
group commit
分布式事务的使用
XA 分布式事务
只支持InnoDB
组件
多个资源管理器(RM)
Mysql实例
一个事务管理器(TM)
连接MySQL的客户端
两阶段提交
分布式事务通常采用2PC协议,全称Two Phase Commitment Protocol。
准备阶段
即所有的参与者准备执行事务并锁住需要的资源。参与者ready时,向transaction manager报告已准备就绪。
提交阶段
当transaction manager确认所有参与者都ready后,向所有参与者发送commit命令
语法
XA {START|BEGIN} xid [JOIN|RESUME] 启动xid事务 (xid 必须是一个唯一值; 不支持[JOIN|RESUME]子句) XA END xid [SUSPEND [FOR MIGRATE]] 结束xid事务 ( 不支持[SUSPEND [FOR MIGRATE]] 子句) XA PREPARE xid 准备、预提交xid事务 XA COMMIT xid [ONE PHASE] 提交xid事务 XA ROLLBACK xid 回滚xid事务 XA RECOVER 查看处于PREPARE 阶段的所有事务
PHP使用MySQL XA实例代码
安全问题
SQL注入
利用数据库外部接口将用户数据插入到实际数据库操作语言中,从而达到入侵数据库的目的
程序对用户输入没有严格过滤
sqlmap
Sqlmap 是一个自动化的 SQL 注入工具,其主要功能是扫描、发现并利用给定的 Url 的 Sql 注入漏洞
SQL Mode
MySQL可以运行在不同的Mode下
完成不同严格程度的数据校验,有效保障数据准确性
设置SQLMode为ANSI,保障大多数SQL符合SQL标准
方便数据迁移
SET [GLOBAL| SESSION] sql_mode = 'modes';
常用功能
校验日期数据合法性
NO_BACKSLASH_ESCAPES使反斜杠变成普通字符
启用PIPES_AS_CONCAT,将||变成字符串连接符
常用模式
ANSI
STRICT_TRANS_TABLES
TRANDITIONAL
MySQL分区
MySQL 5.1 加入
5.6之前分区表限制1024个分区
5.6后限制为
分区是指根据一定的规则,数据库把一个表分解成多个更小的,更容易管理的部分
分区不支持Merge,CSV引擎
概述
分而治之
分区键:用于根据某个区间值,特定值列表或者Hash函数值执行数据的聚集,让数据根据规则分布在不同的分区中,让一个大对象变成一些小对象
和单个磁盘或者文件系统分区相比,可以存储更多的数据
优化查询,在where字句中包含分区条件时,可以只扫描必要的一个或多个分区来提高查询效率
对于过期或者不需要保存的数据,可以通过删除与这些数据有关的分区来 快速删除数据
跨多个磁盘分散数据查询,以获得更大的查询吞吐量
不像分表那样,对于统计查询能够更容易实现,不需要借助应用层
创建
RANGE分区
基于一个给定连续区间范围,把数据分配到不同的分区
如果值为NULL会被当做最小值处理
删除分区数据比运行DELETE更高效
查询更有效率
LIST分区
类似Range分区,区别在List分区是基于枚举出的值列表分区
COLUMNS分区
解决Range和List只能支持整数分区
Range Columns
基于元组的比较
List Columns
HASH分区
支持整数
基于给定的分区个数,把数据分配到不同的分区
两种Hash分区
常规Hash分区
取模算法
平均分布,但是不利于扩容
线性Hash分区
线性的2的幂的算法
分布不均衡,但是易于扩展
KEY分区
类似Hash分区
支持其他类型
子分区
MySQL分区处理NULL值
不禁止在分区键上使用NULL
Range中NULL当做最小值处理
LIST分区中NULL必须出现在枚举中
分区管理操作
优化
SQL优化
一般步骤
show status了解各个SQL的执行频率
查看慢查询日志定位执行效率低的SQL
通过EXPLAIN分析低效SQL的执行计划
select_type:表示SELECT类型
SIMPLE简单表,不使用表连接和子查询
PRIMARY:主查询,外层查询
UNION:UNION中第二个或者后面的查询语句
SUBQUERY:子查询中的第一个SELECT
table:输出结果集的表
type:表示mysql在表中找到所需行的方式,也叫访问类型
ALL:性能最差,遍历全表查找
index:遍历整个索引
range:范围查找
ref:使用非唯一索引或唯一索引的前缀扫描,返回匹配某个单独值的记录行
const/system:单表中最多有一个匹配行
NULL:mysql不用访问表或者索引,效果最佳
possible_keys:可能用到的索引
key:实际使用的索引
key_len:使用到的索引字段长度
rows:扫描行的数据
show profile 分析SQL
SELECT @@profiling 查看配置
需要开启
show profiles
show profile for query x
mysql > show profile for query 4+----------------------+------------+| Status | Duration ||----------------------+------------|| starting | 0.000123 || checking permissions | 0.000018 || Opening tables | 0.000039 || init | 0.000112 || System lock | 0.000029 || optimizing | 0.000011 || statistics | 0.000074 || preparing | 0.000032 || executing | 0.000009 || Sending data | 0.000344 || end | 0.000013 || query end | 0.000018 || closing tables | 0.000018 || freeing items | 0.000120 || cleaning up | 0.000031 |+----------------------+------------+
通过trace分析优化器如何执行计划
索引
索引是在存储引擎层实现,不是在服务器层实现
B-Tree
大部分存储引擎支持
只在Memory引擎支持
R-Tree
空间索引,是MyISAM一个特殊索引,少用
Full-text
*前缀索引
对列的前面一部分进行索引,不能再orderby和groupby中使用
MySQL如何使用索引
平衡树
能够根据键值提供一行或者一行集的快速访问
能够使用索引场景
全值匹配:对索引中所有列都指定了具体的值
type=const
范围查询
type=range
最左前缀匹配
索引查询
匹配列前缀
能够实现索引匹配部分精确而其他部分进行范围匹配
MySQL5.6+使用ICP(Index Condition Pushdown)
不能使用索引的场景
%开头的LIKE
推荐用全文索引
数据类型出现隐式转换,运算的时候也不会使用索引
复合索引的情况下,假如查询条件不包含索引列最左边部分,不会使用复合索引
如果MySQL估计使用索引比扫描全表慢,也不会使用索引
常出现在范围查找和 join
当访问的数据占整个表中数据的蛮大一部分时(一般是20%左右),优化器会选择通过聚集索引来查找数据
OR查询中如果前面的条件中有索引而后面的列没有
查看索引使用情况
Handler_*
show status like \"Handler_read%\"
定期检查和优化表
常用SQL技巧
正则
ORDER BY rand() 随机排序数据
WITH ROLLUP
常用SQL的优化
大批量插入数据LOAD
提高InnoDB
将数据按主键顺序排好
关闭唯一性检查 SET UNIQUE_CHECKS=0
关闭自动提交,导入结束后开启
优化INSERT
使用多值插入
优化ORDER BY
通过有序索引顺序扫描直接返回有序数据
Filesort排序
在sort buffer内存区完成排序
内存不够就在磁盘进行排序
尽量减少额外的排序,通过索引直接返回有序数据
优化GROUP BY
GROUP BY 默认是排序,禁用ORDER BY
ORDER BY NULL
避免了Filesort
优化嵌套查询
优化OR条件
OR实际上是对各个条件查询后做UNION操作
优化分页查询
1)在索引上完成分页的操作
2)用ID查询
join语句的优化
用小结果集驱动大结果集,尽量减少join语句中的Nested Loop的循环总次数
优先优化Nested Loop的内层循环,因为内层循环是循环中执行次数最多的,每次循环提升很小的性能都能在整个循环中提升很大的性能
对被驱动表的join字段上建立索引
当被驱动表的join字段上无法建立索引的时候,设置足够的Join Buffer Size
优化数据库对象
优化表的数据类型:PROCEDURE ANALYSE()
读写分离
解决锁争用和IO问题
分库分表
分库
提高单机IO
单机磁盘存储
分表
解决大表带来的索引膨胀
表拆分
垂直拆分
水平拆分
分表分库原则
能不分就不分,1000 万以内的表,不建议分片,通过合适的索引,读写分离等方式,可以很好的解决性能问题。
分片数量尽量少,分片尽量均匀分布在多个 DataHost 上,因为一个查询 SQL 跨分片越多,则总体性能越差,虽然要好于所有数据在一个分片的结果,只在必要的时候进行扩容,增加分片数量。
分片规则需要慎重选择,分片规则的选择,需要考虑数据的增长模式,数据的访问模式,分片关联性问题,以及分片扩容问题,最近的分片策略为范围分片,枚举分片,一致性 Hash 分片,这几种分片都有利于扩容。
尽量不要在一个事务中的 SQL 跨越多个分片,分布式事务一直是个不好处理的问题。
查询条件尽量优化,尽量避免 Select * 的方式,大量数据结果集下,会消耗大量带宽和 CPU 资源,查询尽量避免返回大量结果集,并且尽量为频繁使用的查询语句建立索引。
数据拆分原则
达到一定数量级才拆分(800 万)
不到 800 万但跟大表(超 800 万的表)有关联查询的表也要拆分,在此称为大表关联表
大表关联表如何拆:小于 100 万的使用全局表;大于 100 万小于 800 万跟大表使用同样的拆分策略;无法跟大表使用相同规则的,可以考虑从 java 代码上分步骤查询,不用关联查询,或者破例使用全局表。
反范式
树数据结构存储
邻接表
树深度过深,查询某个节点下的所有子节点会困难
路径枚举
不能确保路径的格式总是正确或者路径中的节点确实存在
嵌套集
确定这三个值(nsleft,comment_id,nsrigh)的简单方法是对树进行一次深度优先遍历, 在逐层深入的过程中依次递增地分配 nsleft 的值,并在返回时依次递增地分配 nsright 的值。
嵌套集的插入和移动节点是比较复杂的,因为需要重新分配左右值, 如果你的应用程序需要频繁的插入、删除节点,那么嵌套集可能并不适合。
闭包
使用中间表提高查询速度
锁问题
表锁
开销小,加锁快,不会出现死锁
粒度大,发生锁冲突的概率高,并发度低
适合查询为主
行锁
粒度小,发生锁冲突的概率低,并发度高
开销大,加锁慢,会出现死锁
适合有大量按索引条件并发更新少量数据,同时并发查询数据的应用
页锁(DBD引擎)
开销和加锁时间介于前两者
会出现死锁
粒度介于前两者,并发度一般
MyISAM表锁
查询表锁争用情况
show status like \"table%\"
如果 Table_locks_waited 大,则说明表锁争占严重
锁模式
表共享读锁
表独占写锁
如何加表锁
执行查询语句前,会对涉及的表加读锁执行更新语句前,会对涉及的表加写锁整个过程无需用户干预
显式加锁:lock table xxx read [local]local:满足MyISAM表并发插入条件下,允许其他用户在表尾部并发插入数据
MyISAM表不会死锁的原因:锁不支持升级,如果在读锁内,不能做更新操作如果在写锁内,只能访问加锁了的表MyISAM一次性取得所有要用到的表,所以不会存在死锁
并发插入
MyISAM引擎有一个concurrent_insert变量
0:不允许并发插入
1:如果表没有空洞,则允许尾部并发插入
2:不管有没有空洞,都允许尾部并发插入
MyISAM锁调度
写锁优先
有读请求先到锁等待队列,后有写请求进来,写锁也会优先
这就是MyISAM不适合大量更新操作和查询操作的原因
InnoDB锁
获取InnoDB行锁争用情况
show status like \"innodb_row_lock%\"
Innodb_row_lock_waits、Innodb_row_lock_time_avg 的值比较高证明争抢严重
从InnoDB1.0开始,在INFORMATION_SCHEMA架构下添加了表INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITS。通过这三张表,用户可以更简单地监控当前事务并分析可能存在的锁问题。
表 INNODB_TRX 结构说明
表 INNODB_LOCKS 结构
表 INNODB_LOCK_WAITS 结构
设置InnoDB Monitor观察锁冲突
show engine innodb status
InnoDB的行锁模式和加锁方法
意向锁
为了允许行锁和表锁共存,实现多粒度锁机制
意向共享锁(IS)
事务在取得行的共享锁前,必须取到对应表的意向共享锁
意向排他锁(IX)
事务在取得行的排他锁前,必须取到对应表的意向排他锁
两种类型的行锁
共享锁(S)
允许一个事务读一行,阻止其他事务获得数据集的排他锁
SELECT ... LOCK IN SHARE MODE;
排他锁(X)
允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁
SELECT ... FOR UPDATE;
意向锁和行锁兼容性
行锁实现方式
通过给索引上的索引项加锁来实现
如果没有索引,InnoDB会通过隐藏的聚簇索引来对记录加锁
3种情形
Recore Lock:对索引项加锁
Gap lock:对索引项之间的间隙加锁,锁定一个范围但不包含本身
Next-key Lock:前两种组合,锁定一个范围,并且锁定记录本身
当查询的索引含有唯一属性时,InnoDB存储引擎会对Next-Key Lock进行优化,将其降级为Record Lock
当范围条件不是相等条件并请求锁时,InnoDB会给符合条件的已存在的数据加锁,对于条件范围内不存在的数据(间隙)也会加锁
对不存在的数据,条件时等值也是使用这种锁
为了解决 Phantom Problem (幻象问题)
防止幻读
满足其恢复和复制的需要
如果不通过索引条件检索数据,那么InnoDB将对所有记录加锁(效果相当于表锁)
不通过索引条件查询,InnoDB会锁定表中的记录
MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同的记录,但是如果是使用相同的索引键,是会出现锁冲突的
当表有多个索引,不同的事务使用不同的索引锁定不同行,不论是使用主键索引、唯一索引或者普通索引,InnoDB都会使用行锁来对数据加锁
即使在条件中使用了索引字段,但是是否使用索引来检索数据是有MySQL通过判断不同执行计划的代价来决定的
乐观锁与悲观锁
悲观锁
乐观锁
加_version字段并使用CAS
什么时候使用表锁
事务需要更新大部分数据
事务涉及多个表
SQL语句锁分析
InnoDB不存在锁升级问题
采用BITMAP记录页上的记录锁
死锁
InnoDB中锁是逐步获得的
会自动检测,并使一个事务释放锁并回退
参数innodb_lock_wait_timeout用来设置超时的时间
使用 wait-for graph(等待图)的方式来进行死锁检测
应用开发中注意点
不同程序并发取多个表,应该尽量约定以相同的顺序来访问表
批量取大量数据,要先对数据排序,保证线程按顺序处理
在事务中,要更新数据要先申请排他锁
repeatable-read隔离级别下,两个线程同时对相同记录用SELECT For UPDATE加排他锁在没有数据的情况下两个线程都会加锁成功,后面更新就会死锁
read committed隔离级别下,两个线程都先执行SELECT For UPDATE,如果没有数据,其中一个线程会成功插入,提交后另外一个线程会出现主键冲突但是仍有一个排他锁,后面的线程请求会死锁
优化Server
MySQL体系结构
MySQL内存管理及优化
磁盘IO问题
应用优化
使用连接池
减少对MySQL的访问
避免同一数据做重复检索
使用查询缓存
概念:存储Select语句和对应的结果
适用对象是不经常更新的表
增加Cache
memcache等等服务
主从架构
分布式的数据库架构
可以使用MySQL Cluster
其他优化建议
对于没有删除操作的MyISAM,查询和插入操作可以并行
应用实现字段ID自增长
利用字段默认值
维护
安装与升级
常用工具
mysql客户端
my-cli
myisampack
mysqladmin
mysqlbinlog
mysqlcheck
mysqldump
mysqlhostcopy
mysqlimport
mysqlshow
perror
replace
MySQL日志
记录所有的DDL和DML,但是不包括查询语句
STATEMENT
ROW
MIXED
恢复
复制
审计
检查是否有SQL注入
记录了所有MySQL数据库请求的信息,无论这些请求是否得到正确的执行
默认是关闭的
日志查看工具:mysqlsla
MySQL权限与安全
MySQL监控
MySQL常见问题和应用技巧
MySQL复制
三个线程
Master
Binlog Dump 线程
Slave
Relay线程
异步复制
半同步复制
主库事务提交前写入binlog,并等待某一个从库接收到binlog后才返回客户端
当网络发生故障,退化为异步复制,数据一致性无法保证
主库事务提交前会写入binlog,然后直接返回客户端
主从库
改善方案
MySQL Cluster
高可用架构
MMM(Master-Master Replication Manager for MySQL)
数据完整性无法保证
MHA(Master Hight Availability)
其他知识
auto_increment原理
传统auto_increment模式:内存中维护了一个计数器
innodb_autoinc_lock_mode=1 针对bulk inserts才会采用AUTO-INC锁这种方式,而针对simple inserts,则采用了一种新的轻量级的互斥锁来分配auto_increment列的值
innodb_autoinc_lock_mode=2(interleaved lock mode)这种模式下任何类型的inserts都不会采用AUTO-INC锁,性能最好,但是在同一条语句内部产生auto_increment值间隙。
读写场景优化
读多写少
读少写多
SQL执行流程
TiDB 数据库
时间序列数据库
influxdb
非关系型数据库(NoSQL Not only SQL)
Redis
版本特性
Redis 3
Redis 4
模块系统
PSYNC 2.0
在旧版本 Redis 中, 如果一个从服务器在 FAILOVER 之后成为了新的主节点, 那么其他从节点在复制这个新主的时候就必须进行全量复制。 在 Redis 4.0 中, 新主和从服务器在处理这种情况时, 将在条件允许的情况下使用部分复制。
在旧版本 Redis 中, 一个从服务器如果重启了, 那么它就必须与主服务器重新进行全量复制, 在 Redis 4.0 中, 只要条件允许, 主从在处理这种情况时将使用部分复制。
缓存驱逐策略优化
新添加LFU
非阻塞 DEL 、 FLUSHDB 和 FLUSHALL
交换数据库 SWAPDB
混合 RDB-AOF 持久化格式
内存命令 MEMORY
兼容NAT和Docker
Redis 5
新的流数据类型(Stream data type)
RDB 增加 LFU 和 LRU 信息
集群管理器从 Ruby (redis-trib.rb) 移植到了redis-cli 中的 C 语言代码
新的有序集合(sorted set)命令:ZPOPMIN/MAX 和阻塞变体(blocking variants)
升级 Active defragmentation 至 v2 版本
增强 HyperLogLog 的实现
更好的内存统计报告
许多包含子命令的命令现在都有一个 HELP 子命令
客户端频繁连接和断开连接时,性能表现更好
许多错误修复和其他方面的改进
升级 Jemalloc 至 5.1 版本
引入 CLIENT UNBLOCK 和 CLIENT ID
新增 LOLWUT 命令
在不存在需要保持向后兼容性的地方,弃用 \"slave\" 术语
网络层中的差异优化
Lua 相关的改进
引入动态的 HZ(Dynamic HZ) 以平衡空闲 CPU 使用率和响应性
对 Redis 核心代码进行了重构并在许多方面进行了改进
Redis 6
Connection
AUTH password
ECHO message
PING [message]
QUIT
SELECT index
SWAPDB index index
Server
BGREWRITEAOF
描述
异步重写AOF
BGSAVE
后台保存数据库
redis会fork一个子进程保存数据到磁盘,父进程继续处理请求
CLIENT KILL [ip:port] [ID client-id] [TYPE normal|master|slave|pubsub] [ADDR ip:port] [SKIPME yes/no]
CLIENT LIST
统计和返回可读的客户端连接列表信息
字段
id: 唯一的64位ID
addr:地址
fd: socket的文件描述符
age:连接持续了多少秒
idle:连接闲置了多少秒
flags: 客户端标示
db:当前的数据库ID
sub: 通道订阅的数量
psub: 模式匹配订阅的数量
multi:在MULTI/EXEC的命令数
qbuf: 查询缓冲区长度,0代表没有查询进行中
qbuf-free:查询缓冲区的空闲空间大小,0代表已经满了
obl:output buffer length
oll:output list length
omem:output buffer memory usage
cmd:last command played
CLIENT GETNAME
获取当前连接的客户端名称
CLIENT PAUSE timeout
让所有的client暂停多少毫秒
在切换redis实例场景有用
CLIENT REPLY ON|OFF|SKIP
CLIENT SETNAME
设置客户端名称
COMMAND
返回一个包含所有redis命令详情的数组
COMMAND COUNT
COMMAND GETKEYS
COMMAND INFO
CONFIG GET parameter
CONFIG REWRITE
CONFIG REWRITE 命令对启动 Redis 服务器时所指定的 redis.conf 文件进行改写
CONFIG SET parameter value
CONFIG SET 命令可以动态地调整 Redis 服务器的配置(configuration)而无须重启。
CONFIG RESETSTAT
重置 INFO 命令中的某些统计数据
DBSIZE
返回当前数据库的 key 的数量
DEBUG OBJECT key
DEBUG OBJECT 是一个调试命令,它不应被客户端所使用
DEBUG SEGFAULT
执行一个不合法的内存访问从而让 Redis 崩溃,仅在开发时用于 BUG 模拟
FLUSHALL [ASYNC]
删除所有的数据库的数据
INFO [section]
以一种易于解释(parse)且易于阅读的格式,返回关于 Redis 服务器的各种信息和统计数值
LASTSAVE
返回最近一次 Redis 成功将数据保存到磁盘上的时间,以 UNIX 时间戳格式表示。
MONITOR
监视redis server命令的操作情况
管理命令不会记录
ROLE
SAVE
SAVE 命令执行一个同步保存操作,将当前 Redis 实例的所有数据快照(snapshot)以 RDB 文件的形式保存到硬盘。
SHUTDOWN [NOSAVE|SAVE]
关闭redis server
SLAVEOF host port
SLAVEOF 命令用于在 Redis 运行时动态地修改复制(replication)功能的行为
SLOWLOG subcommand [argument]
查询慢操作
slowlog-log-slower-than 记录查询时间大于等于多少微妙的
slowlog-max-len 最多保存多少条记录
SYNC
TIME
数据库相关
DEL key
删除给定的一个或多个 key
返回值
被删除 key 的数量
DUMP key
序列化给定 key ,并返回被序列化的值
生成的值
它带有 64 位的校验和,用于检测错误
值的编码格式和 RDB 文件保持一致
RDB 版本会被编码在序列化值当中,如果因为 Redis 的版本不同造成 RDB 格式不兼容,那么 Redis 会拒绝对这个值进行反序列化操作。
如果 key 不存在,那么返回 nil
存在返回序列化之后的值
EXISTS key
测试某个键是否存在
存在 1 不存在 0
EXPIRE/PEXPIRE key seconds/milliseconds
为给定 key 设置生存时间,当 key 过期时(生存时间为 0 ),它会被自动删除
如果一个命令只是修改(alter)一个带生存时间的 key 的值而不是用一个新的 key 值来代替(replace)它的话,那么生存时间不会被改变。
对一个 key 执行 INCR 命令,对一个列表进行 LPUSH 命令,或者对一个哈希表执行 HSET 命令,这类操作都不会修改 key 本身的生存时间
如果使用 RENAME 对一个 key 进行改名,那么改名后的 key 的生存时间和改名前一样
过期时间的精确度:Redis 2.4之前是1秒,2.6中延迟被降低到1ms
2.1.3之前,修改一个带有生存时间的key会导致整个key被删除
成功返回1,key不存在返回0
模式
导航会话
EXPIREAT/PEXPIREAT key timestamp/milliseconds-timestamp
不同在于 EXPIREAT 命令接受的时间参数是 UNIX 时间戳
KEYS pattern
查找所有符合给定模式 pattern 的 key
MIGRATE host port key destination-db timeout [copy] [replace]
将 key 原子性地从当前实例传送到目标实例的指定数据库上,一旦传送成功, key 保证会出现在目标实例上,而当前实例上的 key 会被删除。
timeout 参数以毫秒为格式,指定当前实例和目标实例进行沟通的最大间隔时间
需要在给定的时间规定内完成 IO 操作
超时或者IO错误会返回IOERR
MOVE key db
将当前数据库的 key 移动到给定的数据库 db 当中。
OBJECT
OBJECT 命令允许从内部察看给定 key 的 Redis 对象
PERSIST key
移除给定 key 的生存时间
PEXPIRE key milliseconds
PEXPIREAT key milliseconds
TTL/PTTL key
以秒为单位,返回给定 key 的剩余生存时间
key不存在返回-2
key没有设置生存时间返回-1
返回时间
RANDOMKEY
从当前数据库中随机返回(不删除)一个 key
RENAME key newkey
将 key 改名为 newkey
RENAMENX
当且仅当 newkey 不存在时,将 key 改名为 newkey
RESTORE key ttl serialized-value [REPLACE]
反序列化给定的序列化值,并将它和给定的 key 关联
SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC | DESC] [ALPHA] [STORE destination]
返回或保存给定列表、集合、有序集合 key 中经过排序的元素
TOUCH
修改key的最后访问时间
TYPE
返回 key 所储存的值的类型
none (key不存在)string (字符串)list (列表)set (集合)zset (有序集)hash (哈希表)
UNLINK
WAIT
SCAN cursor [MATCH pattern] [COUNT count]
Strings
位操作
BITCOUNT key [start end]
计算给定字符串中,被设置为 1 的比特位的数量
不存在的key总是认为是空字符串
被设置为 1 的位的数量
bitmap
BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]
BITFIELD 命令可以将一个 Redis 字符串看作是一个由二进制位组成的数组, 并对这个数组中储存的长度不同的整数进行访问 (被储存的整数无需进行对齐)
BITOP operation destkey key [key ...]
operation 可以是 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种
保存到 destkey 的字符串的长度,和输入 key 中最长的字符串长度相等
BITPOS key bit [start] [end]
返回第一位0/1的位置
如果找到返回对应位置
找不到返回-1
对于查找1但是整个二进制串为0,返回len+1
GETBIT key offset
SETBIT
字符串操作
GET key
获取值
时间复杂度
O(1)
返回对应的值
返回nil代表key不存在
SET key value [EX seconds] [PX milliseconds] [NX|XX]
EX seconds
PX milliseconds
NX 只在值不存在的情况下
XX 只在值存在的情况下
对于某个原本带有生存时间(TTL)的键来说, 当 SET 命令成功在这个键上执行时, 这个键原有的 TTL 将被清除。
O(1)
返回
OK
返回nil,因为用户设置了XX或者是NX
实现锁
如果服务器返回 OK ,那么这个客户端获得锁。如果服务器返回 NIL ,那么客户端获取锁失败,可以在稍后再重试。
过期时间到达后自动释放锁
不使用固定的字符串作为键的值,而是设置一个不可猜测(non-guessable)的长随机字符串,作为口令串(token)
不使用 DEL 命令来释放锁,而是发送一个 Lua 脚本,这个脚本只在客户端传入的值和键的口令串相匹配时,才对键进行删除。
APPEND key value
追加字符串
追加后的字符串长度
做时间序列
GETSET key value
设置新值返回旧值
如果有值返回旧值
键不存在返回nil
GETSET 可以和 INCR 组合使用,实现一个有原子性(atomic)复位操作的计数器(counter)。
GETRANGE key start end
返回 key 中字符串值的子字符串,字符串的截取范围由 start 和 end 两个偏移量决定(包括 start 和 end 在内)。
O(N),其中 N 为被返回的字符串的长度。
截取得出的子字符串
MGET
返回所有(一个或多个)给定 key 的值。
一个包含所有给定 key 的值的列表
MSET
同时设置一个或多个 key-value 对
OK,不可能失败
MSETNX
同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。
即使只有一个给定 key 已存在, MSETNX 也会拒绝执行所有给定 key 的设置操作。
原子性操作,要么全部成功,要么全部失败
当所有 key 都成功设置,返回 1 。
如果所有给定 key 都设置失败(至少有一个 key 已经存在),那么返回 0 。
PSETEX key milliseconds value
和SETEX相似,但是指定的过期时间间隔的单位是毫秒
SETEX key seconds value
将值value关联到key,并将key的生存时间设为seconds
设置成功返回OK
当seconds不合法返回错误
SETNX
将key的值设为value,当且仅当key不存在
成功返回1
失败返回0
SETRANGE key offset value
用 value 参数覆写(overwrite)给定 key 所储存的字符串值,从偏移量 offset 开始。
不存在的 key 当作空白字符串处理。
如果原字符串长度小于offset,两者之间用\"\\x00\"填充
redis字符串限制在512M内
对于长度较短的字符串,命令的平摊复杂度O(1)
对于长度较大的字符串,命令的复杂度为 O(M) ,其中 M 为 value 的长度。
修改后的字符串长度
STRLEN key
返回key中的值的长度
如果value不是字符串,返回错误
key不存在返回0
数字增减
DECR key
如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 DECR 操作
如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误
限制范围:64位有符号整形
执行 DECR 命令之后 key 的值。
DECRBY key decrement
将 key 所储存的值减去减量 decrement
参见DECR
减去 decrement 之后,key 的值
INCR key
将key中存储的数值加一
如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。
如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。
本操作的值限制在 64 位(bit)有符号数字表示之内。
执行 INCR 命令之后 key 的值。
计数器
每当某个操作发生时,向 Redis 发送一个 INCR 命令。
限速器
用于限制一个操作可以被执行的速率(rate)。
INCRBY key increment
参见INCR
INCRBYFLOAT
可以使用像 2.0e7 、 3e5 、 90e-2 那样的指数符号
小数尾部的0会被清除
保留小数点后7位
List
BLPOP/BRPOP key [key ...] timeout
BLPOP 是列表的阻塞式(blocking)弹出原语
如果所有给定 key 都不存在或包含空列表,那么 BLPOP 命令将阻塞连接,直到等待超时,或有另一个客户端对给定 key 的任意一个执行 LPUSH 或 RPUSH 命令为止
timeout单位为秒
如果列表为空,超时返回nil
返回两个元素的列表,第一个是key,第二个是value
事件提醒
BRPOPLPUSH source destination timeout
返回两个元素的列表,第一个是value,第二个是等待时长
安全队列
循环列表
LINDEX key index
返回列表 key 中,下标为 index 的元素
列表中下标为 index 的元素。
如果找不到返回nil
LINSERT key BEFORE|AFTER pivot value
将值 value 插入到列表 key 当中,位于值 pivot 之前或之后
如果命令执行成功,返回插入操作完成之后,列表的长度
当pivot不存在列表中,不执行操作,返回-1
如果 key 不存在或为空列表,不执行操作,返回 0
LLEN key
返回列表 key 的长度。
如果key不存在,则key被认为是一个空列表,返回0
如果key不是列表类型,返回错误
LPOP/RPOP key
移除并返回列表 key 的头(L)元素或者尾元素(R)
列表的头或者尾元素
key不存在返回nil
LPUSH/RPUSH key value [value...]
将一个或多个值 value 插入到列表 key 的表头(L,最左边)或者表尾(R,最右边)
表的长度
LPUSHX/RPUSHX key value
将值 value 插入到列表 key 的表头或者表尾,当且仅当 key 存在并且是一个列表
表的长度。
LRANGE key start stop
返回列表 key 中指定区间内的元素,区间以偏移量 start 和 stop 指定
列表
LREM key count value
根据参数 count 的值,移除列表中与参数 value 相等的元素
被移除元素的数量
如果key不存在,返回0
LSET key index value
将列表 key 下标为 index 的元素的值设置为 value
LTRIM key start stop
让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除
RPOPLPUSH source destination
在一个原子操作内
将列表 source 中的最后一个元素(尾元素)弹出,并返回给客户端
将 source 弹出的元素插入到列表 destination ,作为 destination 列表的的头元素
被弹出的元素
LPUSH-RPOP:如果客户端取出数据后奔溃,该数据无法处理
RPOPLPUSH:相当于增加了一个备份,正常处理后可以用LREM从备份表删除
Set
SADD
SCARD
返回集合 key 的基数(集合中元素的数量)
集合的基础,不存在则返回0
SDIFF
返回一个集合的全部成员,该集合是所有给定集合之间的差集
不存在的 key 被视为空集
一个包含差集成员的列表
SDIFFSTORE
这个命令的作用和 SDIFF 类似,但它将结果保存到 destination 集合,而不是简单地返回结果集。
结果集中的元素数量
SINTER
返回一个集合的全部成员,该集合是所有给定集合的交集
交集成员的列表
SINTERSTORE destination key [key ...]
这个命令类似于 SINTER 命令,但它将结果保存到 destination 集合,而不是简单地返回结果集
结果集中的成员数量
SISMEMBER
判断 member 元素是否集合 key 的成员
SMEMBERS
返回集合 key 中的所有成员
SMOVE
将 member 元素从 source 集合移动到 destination 集合
SPOP
移除并返回集合中的一个随机元素
SRANDMEMBER
如果命令执行时,只提供了 key 参数,那么返回集合中的一个随机元素
只提供 key 参数时,返回一个元素
如果集合为空,返回 nil
如果提供了 count 参数,那么返回一个数组
如果集合为空,返回空数组
SREM
移除集合 key 中的一个或多个 member 元素
SUNION
返回一个集合的全部成员,该集合是所有给定集合的并集
SUNIONSTORE
SSCAN
Sorted Set
ZADD key score member
将一个或多个 member 元素及其 score 值加入到有序集 key 当中
被成功添加的新成员的数量,不包括那些被更新的、已经存在的成员
ZCARD
ZCOUNT key min max
返回有序集 key 中, score 值在 min 和 max 之间(默认包括 score 值等于 min 或 max )的成员的数量
ZINCRBY
ZINTERSTORE
ZLEXCOUNT
ZRANGE key start stop [WITHSCORES]
返回有序集 key 中,指定区间内的成员
ZRANGEBYLEX
ZREVRANGEBYLEX
ZRANGEBYSCORE
ZRANK key member
返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递增(从小到大)顺序排列。
ZREM
ZREMRANGEBYLEX
ZREMRANGEBYRANK
ZREMRANGEBYSCORE
ZREVRANGE
ZREVRANGEBYSCORE
HDEL key file
HEXISTS key field
HGET key field
HGETALL key
HINCRBY key field increment
HINCRBYFLOAT key field increment
HKEYS key
HLEN key
HMGET key field
HMSET key field [key field]
HSET key field value
HSETNX key field value
HSTRLEN key field
HVALS key
获取hash所有的value
HSCAN key cursor
Pub/Sub
PSUBSCRIBE pattern
订阅一个或者多个符合给定模式的频道
接收到的信息
PUBSUB <subcommand> [argument...]
查看订阅与发布系统状态的内省命令
PUBSUB CHANNELS [pattern]:列出当前的活跃频道
PUBSUB NUMSUB [channel-1 ... channel-N]:返回给定频道的订阅者数量, 订阅模式的客户端不计算在内
PUBSUB NUMPAT:返回订阅模式的数量
PUBLISH channel message
将信息message发送到指定的频道channel
接受到的信息message的订阅者数量
SUBSCRIBE
订阅给定的一个或多个频道的信息
PUNSUBSCRIBE
UNSUBSCRIBE
Transaction
DISCARD
取消事务,放弃执行事务块内的所有命令
EXEC
执行所有事务块内的命令
假如某个(或某些) key 正处于 WATCH 命令的监视之下,且事务块中有和这个(或这些) key 相关的命令,那么 EXEC 命令只在这个(或这些) key 没有被其他命令所改动的情况下执行并生效,否则该事务被打断(abort)。
MULTI
标记一个事务块的开始
UNWATCH
取消 WATCH 命令对所有 key 的监视
WATCH key
监视一个或者多个key,如果在事务执行之前key被改动,事务打断
键空间通知
可以通过订阅,当redis发生操作时出现通知
事务中的错误
入队的命令可能会出错
EXEC调用失败
Redis不支持事务回滚
CAS 乐观锁
WATCH
如果你使用 WATCH 监视了一个带过期时间的键, 那么即使这个键过期了, 事务仍然可以正常执行
执行EXEC后不管事务是否成功,WATCH都会取消
当客户端断开连接时, 该客户端对键的监视也会被取消
PUBSUB
Sentinel
Redis 的 Sentinel 系统用于管理多个 Redis 服务器(instance)
监控
提醒
自动故障转移
三个定时任务
每隔10秒,每个Sentinel节点会向主节点和从节点发送info命令获取 最新的拓扑结构
通过向主节点执行info命令,获取从节点的信息,这也是为什么 Sentinel节点不需要显式配置监控从节点
当有新的从节点加入时都可以立刻感知出来
节点不可达或者故障转移后,可以通过info命令实时更新节点拓扑信息
每隔2秒,每个Sentinel节点会向Redis数据节点的__sentinel__:hello频道上发送该Sentinel节点对于主节点的判断以及当前Sentinel节点的信息 ,同时每个Sentinel节点也会订阅该频道,来了解其他 Sentinel节点以及它们对主节点的判断
每隔1秒,每个Sentinel节点会向主节点、从节点、其余Sentinel节点 发送一条ping命令做一次心跳检测,来确认这些节点当前是否可达。
主观下线
客观下线
故障转移
持久化
redis持久化
RDB持久化可以在指定的时间间隔内生成数据集的时间点快照
AOF记录服务器执行的所有写操作命令,并在服务器启动时,通过重做还原数据
AOF文件中的命令使用Redis协议的格式保存
redis可以对AOF重写
RDB
非常紧凑的文件,保存某个时间点上的数据集
适合进行备份
适合灾难恢复
可以最大化Redis性能,父进程fork一个子进程来处理保存工作
RDB在回复大数据集比AOF快
RDB要保存整个数据集状态,所以可能会丢失一段数据
需要fork,消耗资源
AOF
可以设置不同的fsync策略,默认每秒一次fsync,发生故障也只会损失1s的数据
追加写入,不需要seek
redis会自己在AOF文件过大时,在后台重写
AOF易读性高
体积较大
AOF速度可能慢
RDB快照
Redis 将数据库快照保存在名字为 dump.rdb 的二进制文件中
写时复制
AOF重写
执行 BGREWRITEAOF 命令, Redis 将生成一个新的 AOF 文件, 这个文件包含重建当前数据集所需的最少命令
当 Redis 启动时, 如果 RDB 持久化和 AOF 持久化都被打开了, 那么程序会优先使用 AOF 文件来恢复数据集, 因为 AOF 文件所保存的数据通常是最完整的。
备份数据
创建一个定期任务(cron job), 每小时将一个 RDB 文件备份到一个文件夹, 并且每天将一个 RDB 文件备份到另一个文件夹
确保快照的备份都带有相应的日期和时间信息, 每次执行定期任务脚本时, 使用 find 命令来删除过期的快照: 比如说, 你可以保留最近 48 小时内的每小时快照, 还可以保留最近一两个月的每日快照
至少每天一次, 将 RDB 备份到你的数据中心之外, 或者至少是备份到你运行 Redis 服务器的物理机器之外
通信协议
网络层
TCP 6379
客户端和服务器发送的命令或数据一律以 \\ (CRLF)结尾
请求协议
*<参数数量> CR LF$<参数 1 的字节数量> CR LF<参数 1 的数据> CR LF...$<参数 N 的字节数量> CR LF<参数 N 的数据> CR LF
回复协议
状态回复
一个状态回复(或者单行回复,single line reply)是一段以 \"+\" 开始、 \"\\\" 结尾的单行字符串
错误回复
一个错误回复是一段以 \"-\" 开始、 \"\\\" 结尾的单行字符串
整数回复
整数回复(integer reply)的第一个字节是 \":\"
批量回复
批量回复(bulk reply)的第一个字节是 \"$\"
字符串长度为512M
二进制安全
多条批量回复
多条批量回复的第一个字节为 \"*\" , 后跟一个字符串表示的整数值, 这个值记录了多条批量回复所包含的回复数量, 再后面是一个 CRLF
空元素
$-1
集群
Redis 集群中不存在中心(central)节点或者代理(proxy)节点
Redis 集群不支持那些需要同时处理多个键的 Redis 命令
Redis 集群通过分区(partition)来提供一定程度的可用性
Redis 集群提供了以下两个好处
将数据自动切分(split)到多个节点的能力
当集群中的一部分节点失效或者无法进行通讯时, 仍然可以继续处理命令请求的能力
集群只使用默认的 0 号数据库
集群责任
持有键值对数据
记录集群的状态,包括键到正确节点的映射
自动发现其他节点,识别工作不正常的节点,并在有需要时,在从节点中选举出新的主节点
满足CA,牺牲了P
集群通信
每个节点都与其他节点建立起了“集群连接(cluster bus)”, 该连接是一个 TCP 连接, 使用二进制协议进行通讯
节点间使用Gossip协议
传播(propagate)关于集群的信息,以此来发现新的节点
向其他节点发送 PING 数据包,以此来检查目标节点是否正常运作
在特定事件发生时,发送集群信息
MOVED转向
如果所查找的槽不是由该节点处理的话, 节点将查看自身内部所保存的哈希槽到节点 ID 的映射记录, 并向客户端回复一个 MOVED 错误
ASK转向
当节点需要让客户端仅仅在下一个命令请求中转向至另一个节点时, 节点向客户端返回 ASK 转向
数据共享
Redis集群使用分片,而不是一致性Hash
一个Redis包含16384个哈希槽
数据库中的每个key都属于这 16384 个哈希槽的其中一个, 集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽
CRC16(key) 语句用于计算键 key 的 CRC16 校验和
主从复制
不保证强一致性
会丢失写命令
出现网络脑裂的情况会丢失数据
对于大多数一方来说, 如果一个主节点未能在节点超时时间所设定的时限内重新联系上集群, 那么集群会将这个主节点视为下线, 并使用从节点来代替这个主节点继续工作
对于少数一方, 如果一个主节点未能在节点超时时间所设定的时限内重新联系上集群, 那么它将停止处理写命令, 并向客户端报告错误
其他开源方案
Codis
模型
单进程单线程
子主题
源码剖析
基础数据结构
动态字符串SDS
结构
常数复杂度获取字符串长度
杜绝缓冲区溢出
减少修改字符串时带来的内存重分配次数
空间预分配
如果对SDS进行修改之后,SDS的长度(也即是len属性的值)将小于1MB,那么程序分配和len属性同样大小的未使用空间,这时SDS len属性的值将和free属性的值相同
如果对SDS进行修改之后,SDS的长度将大于等于1MB,那么程序会分配1MB的 未使用空间
惰性空间释放
使用free属性将字符串缩短的字节的数量记录起来,并等待将来使用。
因为SDS使用 len属性的值而不是空字符来判断字符串是否结束
SDS的API兼容部分C字符串函数
API
双端链表
整体结构
节点结构
字典
哈希表
节点
哈希算法
Redis使用MurmurHash2算法来计算键的哈希值
hash冲突
通过链式解决,节点的next位置
渐进式rehash
触发条件
服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1
服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5
当哈希表的负载因子小于0.1时,程序自动开始对哈希表执行收缩操作
rehash步骤
为字典的ht[1]哈希表分配空间
扩展操作
大小为第一个大于等于ht[0].used*2的2^n
收缩操作
大小为第一个大于等于ht[0].used的2^n
将保存在ht[0]中的所有键值对rehash到ht[1]上面
当ht[0]包含的所有键值对都迁移到了ht[1]之后释放 ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表
步骤
为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示 rehash工作正式开始
在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx属性的值增 一
跳跃表
层(level)
节点中用L1、L2、L3等字样标记节点的各个层,L1代表第一 层,L2代表第二层,以此类推。
两个属性
前进指针
跨度
后退(backward)指针
节点中用BW字样标记节点的后退指针,它指向位于当 前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。
分值(score)
各个节点中的1.0、2.0和3.0是节点所保存的分值。在跳跃 表中,节点按各自所保存的分值从小到大排列
成员对象(obj)
整数集合
编码方式
INTSET_ENC_INT16
INTSET_ENC_INT32
INTSET_ENC_INT64
升级
根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间
将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后 的元素放置到正确的位上,而且在放置元素的过程中,需要继续维持底层数组的有 序性质不变
将新元素添加到底层数组里面
不支持降级操作
压缩列表
压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组 成的顺序型(sequential)数据结构。
结构说明
存储数据格式
字符数组
长度小于等于63(2 –1)字节的字节数组
长度小于等于16383(2 –1)字节的字节数组
长度小于等于4294967295(2 –1)字节的字节数组
整型数组
4位长,介于0至12之间的无符号整数
1字节长的有符号整数
3字节长的有符号整数
int16_t类型整数
int32_t类型整数
int64_t类型整数
对象
REDIS_SET
REDIS_ENCODING_INTSET
使用整数实现集合对象
REDIS_ENCODING_HT
使用字典实现集合对象
REDIS_ZSET
REDIS_ENCODING_ZIPLIST
REDIS_ENCODING_SKIPLIST
字符串对象 REDIS_STRING
底层类型
REDIS_ENCODING_INT
如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示时使用
REDIS_ENCODING_EMBSTR
如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于32 字节时使用
REDIS_ENCODING_RAW
如果字符串对象保存的是一个字符串值,并且这个字符串值的长度大于32字节时使用
类型对比
raw
embstr
如果字符串小于39字节,使用embstr
embstr相比于raw只需要一次内存分配和一次内存释放
embstr的obj和sds是一块连续的内存空间
不可修改
编码转换
当int不再是整型时,转为raw
embstr 一般是只读,遇到字符串操作需要转换成raw
列表对象 REDIS_LIST
REDIS_ENCODING_LINKEDLIST
ziplist
linkedlist
使用ziplist
列表对象保存的所有字符串元素的长度都小于64字节
列表对象保存的元素数量小于512个
这两个条件可以通过配置手动修改
使用linkedlist
哈希对象 REDIS_HASH
hashtable
集合对象 REDIS_SET
intset
使用intset
集合对象保存的所有元素都是整数值
集合对象保存的元素数量不超过512个
可以修改
以上任意一个条件不满足
有序集合对象 REDIS_ZSET
skiplist
同时使用skiplist和hashtable存储
保存的元素数量小于128个
有序集合保存的所有元素成员的长度都小于64字节
类型检查
redisObject中的type字段
内存管理
对象共享
指向一个现有的对象
对象引用计数+1
Redis只对包含整数值的字符串对象进行共享
复杂的对象比较需要消耗过多CPU资源
内存回收
redisObject中的refcount字段
创建对象,引用计数被初始化为1
对象被新程序使用,+1
对象不在被使用,-1
引用计数为0,则释放内存
对象的idle
redisObject中的lru属性记录对象最后一次被使用的时间
用当前时间减去lru时间得出空转时间
示意图
键空间
RedisDB结构中的dict字典保存了数据库中的所有键值对
生存时间和过期时间
RedisDB结构维护了一张过期表,键为指向StringObject的指针,值为过期的Unix时间戳
过期key删除策略
定时删除
在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作
CPU不友好
惰性删除
每次取值时判断,过期删除,有效则返回
内存不友好
定期删除
每隔一段时间检查一次,由算法决定
Redis使用惰性和定期删除策略
expireIfNeeded
activeExpireCycle
随机抽取一批数据检查,有时间限制和个数限制
淘汰策略
AOF、RDB和复制功能对过期键的处理
生成RDB文件
在执行SAVE命令或者BGSAVE命令创建一个新的RDB文件时
已过期的键不会被保存到新创建的RDB文件中
载入RDB文件
如果服务器开启了RDB功能,那么服务器将对RDB文件 进行载入
Master模式
在载入RDB文件时,程序会对文件中保 存的键进行检查,未过期的键会被载入到数据库中,而过期键则会被忽略
Slave模式
在载入RDB文件时,文件中保存的所有 键,不论是否过期,都会被载入到数据库中
AOF文件写入
数据库中的某个键已经过期,但它还没有被惰性删除或者定期删除,那么AOF文件不会因为这个过期键而产生任何影响
当过期键被惰性删除或者定期删除之后,程序会向AOF文件追加(append)一 条DEL命令,来显式地记录该键已被删除
在执行AOF重写的过程中,程序会对数据库中的键进行 检查,已过期的键不会被保存到重写后的AOF文件中
当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制
主服务器在删除一个过期键之后,会显式地向所有从服务器发送一个DEL命 令,告知从服务器删除这个过期键
从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删 除,而是继续像处理未过期的键一样来处理过期键
从服务器只有在接到主服务器发来的DEL命令之后,才会删除过期键
Master过期但是可以从Slave读取的问题
Redis 3.2 之前存在
Redis3.2后官方解决方案
slave 不会让 key 过期,而是等待 master 让 key 过期
slave 使用它的逻辑时钟来判断并返回只有在不违反数据集的一致性的读取操作(从主机的新命令到达)中才存在 key
在Lua脚本执行期间,不执行任何 key 过期操作
Redis启动加载策略
如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据 库状态
只有在AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据库 状态
命令
阻塞
启动一个子进程去执行
自动隔离性保存
事件
文件事件
构成
IO多路复用模型
Redis为每个IO复用库实现了相同的API
Redis先处理读请求,再处理写请求
处理器
连接应答处理器
命令请求处理器
命令回复处理器
时间事件
定时事件
周期事件
id,全局唯一id,递增
when,毫秒精度的UNIX时间戳
timeProc,时间事件处理器
无序链表,不按when排序
无序链表不影响时间事件处理器的性能
serverCron
更新服务器的各类统计信息
清理数据库过期的键值对
关闭和清理无效客户端
尝试进行AOF和RDB保存
对从服务器定期同步
对集群定期同步和连接测试
Couchbase
MonogoDB
图数据库
原理
一种socket通信
高效传输
模块
服务注册和发现
序列化协议
JSON
Protobuf
定义
是一种语言无关,跨平台可扩展的序列化数据格式
优缺点
更简单
类型严谨
数据传输效率高
序列化、反序列化速度快
能够生成对应可直接在代码中使用的类和对象
兼容性好
可读性差
依赖 protc 和 对应语言的代码生成器
编写.proto文件
第一行不能为空也不能为注释
Message
syntax
指定协议版本,默认是 proto2
唯一数字
定义字段在序列化后
范围:1 ~ (2^29)-1
19000 ~ 19999 保留给protobuf内部实现,如果使用编译器会报错
不能使用声明为保留的字段
一旦使用,不能更改
1-15 字段编码后占一个字节
用于高频率出现的字段
保留一些留给未来使用
16-2047字段编码后占用两个字节
指定字段规则
singular
声明字段能够出现0或者1次
proto3中的字段默认规则
repeated
在proto3,如果数值类型字段被标记为repeated,将会默认使用packed进行编码
多个message可以写在同一个proto文件中
注释
/**/ or //
保留字段(reserved)
防止同个proto文件,字段删除后,旧版本的数据在新版本proto中出现错误的问题
不能再同个reserved语句中同时使用字段名和字段编号
double
float
int32
int64
uint32
uint64
sint32
可变长编码,负数编码比int32效率高
sint64
fixed32
永远使用4字节
编码效率如果数值大于2^28比uint32高
fixed64
sfixed32
sfixed64
bool
string
bytes
枚举
Any
Oneof
Maps
嵌套类型
生成RPC使用的代码接口
可选项
java_package
optimize_for
影响C++ Java的代码生成
风格
修改
不要改变现有的任何字段序号
如果新增字段,旧数据反序列化后,新增字段会被赋予默认值
字段可以被删除,但是不能复用他的序号
编译器
由C++编写的 protoc
代码生成器
protoc-gen-go
技巧
多消息传输
大数据集
protobuf没有对大消息
自描述信息
如果没有对应的.proto就没法反序列化出正确的数据
编码
Base 128 Varints
Dubbo 协议
Msgpack
网络传输
TCP传输
HTTP传输
Restful HTTP
Yar
PHP 常用的 RPC 框架
Apache Thrift
Dubbo
对服务治理支持较好
自描述文件简单,就是一个接口,通过动态代理实现
gRPC
得益于HTTP2特性
服务
容器平台
容器运行时CRI
基础部分
docker三件套
docker swarm
docker compose
docker machine
一个工具,可以用来管理多个虚拟主机上的docker
镜像仓库服务Harbor
Mesos
Kubernetes
概念与术语
集群控制节点
每个集群里面需要一个Master来负责整个集群的管理和控制
通常会占据一个独立的机器
关键进程
Kubernetes API Server(kube-apiserver)
提供 HTTP REST 接口
Kubernetes Controller Manager(kube-controller-manager)
k8s里所有资源对象的自动化控制中心
大总管
Kubernetes Scheduler(kube-scheduler)
负责资源调度(Pod调度)的进程
调度室
额外还有一个 etcd
Kubernetes 集群的资源对象数据都是存储在这里
Node
除 Master 外,Kubernetes 集群中
早期版本叫做 Monion
kubelet
负责Pod对应的容器的创建、启停等任务
向 Master 注册自己
定时向Master汇报自己的资源情况
系统信息
Docker版本
CPU、内存等资源使用量
kube-proxy
实现Kubenetes Service的通信与负载均衡机制的重要组件
Docker Engine
负责本机的容器创建和管理工作
Pod
Pod 是 Kubernetes 应用程序的基本执行单元,即它是 Kubernetes 对象模型中创建或部署的最小和最简单的单元。
每个 Pod 表示运行给定应用程序的单个实例
主要用途
运行单个容器的 Pod
运行多个协同工作的容器的 Pod
pause容器
在pod中担任Linux命名空间共享的基础;
启用pid命名空间,开启init进程。
业务容器
Pod 为其组成容器提供了两种共享资源:网络和存储。
网络
每个 Pod 分配一个唯一的 IP 地址
Pod 中的每个容器共享网络命名空间,包括 IP 地址和网络端口
Pod 内的容器 可以使用 localhost 互相通信
当 Pod 中的容器与 Pod 之外 的实体通信时,它们必须协调如何使用共享的网络资源(例如端口)。
虚拟二层网络技术实现
存储
一个 Pod 可以指定一组共享存储卷
Pod 中的所有容器都可以访问共享卷,允许这些容器共享数据。
两种类型Pod
普通Pod
一旦创建,就会被放入etcd中,随后被Kubenetes Master调度到具体的Node上进行绑定,再被该Node上的kubelet实例成一组Docker Container
如果Pod停止,Kubenetes会重新启动起来
如果Node宕机,Kubenetes会在其他Node上重启该Pod
静态Pod
不存放在etcd中,而是放在某个具体Node的文件上,并且只在此Node上运行
资源限制
CPU
千分之一的CPU配额,单位m
单位字节数
配置
requests
该资源的最小申请数
limits
该资源的最大使用量
Endpoint
容器端口
Pod IP
生命周期
随Pod的创建和销毁改变
Event
一个事件的记录
是排查故障的重要信息
Label
是一个key=value键值对
可以添加到任意数量的不同的资源对象上
一个资源对象可以定义任意数量的label
通过 Label Selector 查询和筛选拥有某些Label的资源对象
使用场景
kebe-controller进程通过资源对象RC上的Label Selector来筛选要监控的Pod副本的数量,从而实现Pod副本的数量始终符合预期设定的全自动控制流程
kube-proxy进程通过Service的Label Selector来选择对应的Pod,自动建立每个Service到对应的Pod的请求转发路由表,从而实现Service的智能负债均衡机制
通过对某些Node定义特定的Label,并且在Pod定义文件中使用Node Selector这种标签调度策略,kube-scheduler进程可以实现Pod定向调度的特性
Replication Controller
ReplicationController 确保在任何时候都有特定数量的 pod 副本处于运行状态
Pod期待的副本数(replicas)
用于筛选目标Pod的Label Selector
当Pod的副本数量小于预期数量的时候,用于创建新Pod的Pod模板
提交RC到集群中,Master节点上的Controller Manager组件就得到通知,定期巡检系统中存活的目标Pod
我们可以通过修改RC的副本数量,实现Pod的动态缩放(Scaling)功能
kubectl scale
Replica Set
“下一代的RC”
区别
RS 支持基于集合的Label Selector
RC 只支持基于等式的Label Selector
Deployment
为了更好解决Pod的编排问题
一个 Deployment 控制器为 Pods和 ReplicaSets提供描述性的更新方式。
创建一个Deployment对象来生成对应的Replica Set并完成Pod副本的创建过程
检查Deployment的状态来看部署动作是否完成
更新Deployment以创建新的Pod
如果当前的Deployment不稳定,可以回滚到上个版本
Horizontal Pod Autoscaler
Pod 横向自动扩容,属于一种kubernetes资源
指标
CPUUtilizationPercentage
需要安装部署Heapster
自定义度量指标
比如qps
Service
微服务架构中的一个“微服务”
定义了一个服务访问的入口地址
Service与后端Pod通过Label Selector实现无缝对接
负载均衡器
kube-proxy: 负责将请求从Service转发到后端Pod上,并内部实现了负债均衡算法
一旦创建,k8s会自动为Service分配一个Cluster IP
在Service生命周期内不会改变
服务发现
用Service的Name和Cluster IP做一个DNS域名映射
旧版是利用Linux的ENV
新版通过DNS系统实现
属性
kind: Service
spec.selector
spec.ports.targetPort
多端口问题
spec.ports.name
外部访问
NodePort
k8s在每个Node上开启对应的端口
Load Balancer
Ingress
Ingress 是对集群中服务的外部访问进行管理的 API 对象,典型的访问方式是 HTTP。
Ingress 可以提供负载均衡、SSL 终结和基于名称的虚拟托管
Stateful
Volume
Persistent Volume
Namespace
Annotation
k8s中的几种IP
Node IP
节点IP,节点的物理网卡IP
Docker引擎通过docker0网桥分配
Cluster IP
Service 的 IP
minikube
helm 安装 k8s 应用
kebuctl 使用
get
apply
delete
describe
深入 Pod
深入 Service
核心原理
运维
源码分析
Nginx系
目录结构
指令
Teginx
OpenRestry
Lua语言基础
应用场景
构建OR服务
Apache
MPM(多进程处理模块)
prefork
worker
event
ElasticSearch搜索
主节点
它将负责管理集群范围内的所有变更,例如增加、删除索引,或者增加、删除节点等。
分片
一个 分片 是一个底层的 工作单元 ,它仅保存了 全部数据中的一部分
相当于数据库
相当于表
已经废弃
文档
相当于行记录
ES使用JSON作为文档序列化格式
集群原理
搜索
轻量查询
结构化查询
语句
精确搜索
term
查询数字
查询文本
组合过滤器
布尔过滤器
must
所有的语句都 必须(must) 匹配,与 AND 等价
should
至少有一个语句要匹配,与 OR 等价
must_not
查找多个精确值
terms
范围
range
处理NULL值
exists
missing
数据统计
FastDFS
版本管理
Git
持续集成 CI
Jenkins
Gitlab CI Plugins
自动化测试
渐进表示法
数据结构
线性表
数组
链表
栈
队列
FIFO(先进先出)
操作集
生成长度为MaxSize的空队列
判断队列Q是否已满
将数据元素item插入队列Q中
判断队列Q是否为空
将队头数据元素从队列中删除并返回
树
结点的度
结点的子树个数
树的度
树的所有结点中最大的度数
树的深度
树中所有结点中的最大层次是这棵树的深度
表示
双亲表示法
孩子表示法
孩子兄弟表示法
旋转45度,变为二叉树
儿子兄弟表示法
二叉树
斜二叉树
完美二叉树(满二叉树)
完全二叉树
有n个结点的二叉树,对树中结点按 从上至下、从左到右顺序进行编号, 编号为i(1 ≤ i ≤ n)结点与满二叉树 中编号为 i 结点在二叉树中位置相同
性质
深度为k的二叉树有最大节点总数:2^k-1
二叉搜索树
非空左子树的所有键值小于其根结点的键值
非空右子树的所有键值大于其根结点的键值
左、右子树都是二叉搜索树
递归
循环
最大最小值
分别落在树的最右最左叶节点上
方法
Find
FindMax
FindMin
Insert
Delete
平衡二叉树(AVL)
空树,或者任一结点左、右子树高度差的绝对值不超过1,即|BF(T) |≤ 1
给定结点数为 n的AVL树的 最大高度为O(log2 n)
调整
RR 旋转
LL 旋转
LR 旋转
RL 旋转
红黑树(RBT)
哈夫曼树
树的遍历
先序遍历
根、左子树、右子树
中序遍历
左子树、根、右子树
后序遍历
左子树、右子树、根
层次遍历
从上到下、从左到右(队列实现)
树的高度
左右子树高度最大值+1
两种遍历顺序确定一个树
必须包含中序遍历
图
矩阵
最小路径
拓扑
堆
优先队列:特殊的“队列”,取出元素的顺序是 依照元素的优先权(关键字)大小,而不是元素进入队列的先后顺序。
最大堆
最小堆
操作
构建堆
取值
散列表
基础算法
排序
时间复杂度下界
逆序对
如果 i<j,则a[i]>a[j]
定理
任意N个不同元素组成的序列平均具有 N ( N - 1 ) / 4 个逆序对
任何仅以交换相邻两元素来排序的算 法,其平均时间复杂度为 Ω ( N^2 )
排序算法复杂度
冒泡排序
相邻位置两两比较
选择排序
选择后方区域中最小值来和当前交换
插入排序
将当前位置插入到后方合适的位置
归并排序
两个子序列的归并
算法稳定
最好 O(nlogn)
平均 O(nlogn)
最坏 O(nlogn)
快速排序
选主元做交换
算法不稳定
平均 O(N logN)
最好 O(logN)
最坏 O(N^2)
希尔排序
带有步进的插入排序
原始希尔排序
定义增量序列 D
带步进的插入排序
增量序列希尔排序
Hibbard 增量序列
D(k) = 2^k – 1
Sedgewick增量序列
堆排序
解决选择排序中如何找到最小元的问题
基数排序
主位优先
次位优先
桶排序
经典问题
非递归中序遍历
非递归后序遍历
遍历子节点
挑选一种遍历方法,遇到左右子树为空就打印
菲波那切数列
TopK
先建立Hash表记录频率,用最大堆排序
回文数
字符串
用半边大小的栈
数字
串的模式匹配
Hash算法
判断哈希算法好坏的四个点
平衡性
哈希的结果能够尽可能分布到所有的缓冲中去
单调性
如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区
Hash(key)%(N+1) 无法满足单调性
分散性
负载
一致性Hash算法
将 value 映射到一个 32 位的 key 值,也即是 0~2^32-1 次方的数值空间
环
将需要缓存的对象通过hash转成数值空间某个点
将Cache服务器做虚拟节点,虚拟节点的key可以是ip+序列或者主机名+序列,通过同样的hash算法映射到数值空间上
将 key <= nodeKey上的值放在node上
增加大量虚拟节点解决了平衡性问题
PHP实现的一致性Hash算法
常见问题
大数据处理
布隆过滤器
计算机组成原理
编程语言
C语言
gcc
调试器
gdb
变量
指针
const和指针
const 可以和指针变量一起使用,这样可以限制指针变量本身,也可以限制指针指向的数据。
int * const p3;
修饰变量名,p3本身的值不能被修改
const int *p1;int const *p2;
修饰指针,p1、p2指向不能被修改
结构体
文件操作
进阶
模块编程
extern关键字
声明引用一个模块外的函数
static关键字
只在当前源码范围内使用
编译
预处理
gcc -E
产出 main.i
将所有的#define删除,并展开所有的宏定义
处理所有条件编译命令,比如 #if、#ifdef、#elif、#else、#endif 等
处理#include命令,将被包含文件的内容插入到该命令所在的位置,这与复制粘贴的效果一样。注意,这个过程是递归进行的,也就是说被包含的文件可能还会包含其他的文件
删除所有的注释//和/* ... */
添加行号和文件名标识,便于在调试和出错时给出具体的代码位置
保留所有的#pragma命令,因为编译器需要使用它们
gcc -S
将预处理过的C语言代码转为汇编代码
产出 main.s
汇编
gcc -c
将汇编代码转为机器码,产生ELF可重定位目标文件
产出 main.o
连接
gcc -o
产出二进制可执行程序
项目化工具链
autoscan
aclocal
automake
autoconf
configure
目标文件和可执行文件
Windows下的PE(Portable Executable)格式
Linux下的ELF(Executable Linkable Format)格式
文件类型
可重定位文件
这类文件包含了代码和数据,可以被用来链接成为可执行文件或动态链接库。静态链接库其实也是可重定位文件。
.o文件
可执行文件
共享目标文件
核心转储文件
core dump
ELF文件结构
ELF Header
文件头,描述了整个目标文件的属性,包括是否可执行、是动态链接还是静态链接、入口地址是什么、目标硬件、目标操作系统、段表偏移等信息。
.text
代码段,存放编译后的机器指令,也即各个函数的二进制代码。一个C语言程序由多个函数构成,C语言程序的执行就是函数之间的相互调用。
.data
数据段,存放全局变量和静态变量。
.rodata
只读数据段,存放一般的常量、字符串常量等。
.rel.text.rel.data
重定位段,包含了目标文件中需要重定位的全局符号以及重定位入口
.comment
注释信息段,存放的是编译器的版本信息,比如“GCC:(GUN) 4.2.0”。
.debug
调试信息
.line
调试时的行号表,即源代码行号与编译后指令的对应表。
Section Table
段表,描述了 ELF 文件包含的所有段的信息,比如段的名字、段的长度、在文件中的偏移、读写权限以及其他属性。可以说,ELF 文件的段结构是由段表来决定的,编译器、链接器和装载器都是依靠段表来定位和访问各个段的。
内存模型
PHP
语言基础
会话控制
增强组件
swoole
项目
swoole-src
TSF(Tencent Server Framework)
微服务框架 php-msf
Symfony
协程框架
Laravel
最出名的PHP框架,富含各种设计模式
PHP中的『SpringMVC』
Yii
CI
Phalcon
C语言实现的PHP框架,以高Web性能宣称
ThinkPHP
内核
安装与调试
PHP源码:https://github.com/php/php-src.git
进入源码目录,git切换到需要的分支
./configure --enable-debug --enable-fpm
调试工具:gdb
PHP7的变化
抽象语法树
PHP5中,PHP代码在语法解析阶段直接生成了ZendVM指令
PHP7中首先生成抽象语法树,然后生成ZendVM指令
64位int改进
统一变量语法
一致性foreach行为
<=>,?? 操作符
Native TLS(线程局部存储)
指定函数参数,返回值类型
zval结构变化
将refcount_gc移到具体的value中,zval从24字节减少到16字节
异常处理
HashTable的变化
HT从72字节减少到56字节
Bucket从72字节减少到32字节
执行器
execute_data,opline采用寄存器变量存储
新的参数解析方式
PHP组件
SAPI
一般是一个独立的程序,可认为是PHP的宿主环境,是外界与ZendVM的交互接口
main
PHP主要代码,负责IO,通信,框架初始化,协议解析等工作
Zend
将PHP编译成执行器可以理解的指令
负责执行指令
ext
PHP扩展开发目录
TSRM
PHP线程安全组件
模块初始化(module startup)[php_module_startup]
请求初始化(request startup)[php_request_startup]
脚本执行阶段(execute script)[php_execute_script]
zend_execute_script
zend_compile_file
compile_file
zendparse(yyparse)【语法分析】
zendlex【词法解析】
zend_compile_top_stmt
zend_execute【指令执行】
execute_ex
请求关闭(request shutdown)[php_requset_shutdown]
模块关闭(module shutdown)[php_module_shutdown]
sapi_flush()
zend_shutdown
Apache Handler
cli
流程
main()->php_cli_startup()->do_cli()->php_module_shutdown()
CGI
CGI “通用网关接口”(Common Gateway Interface)
CGI描述了服务器和请求处理程序之间传输数据的一种标准。
CGI最为人诟病的fork-and-execute模式,CGI 程序反复加载是 CGI 性能低下的主要原因
FastCGI 快速通用网关接口(Fast Common Gateway Interface/FastCGI)
基本概念
是一种让交互程序与Web服务器通信的协议。
FastCGI是早期通用网关接口(CGI)的增强版本。
FastCGI像是一个常驻(long-lived)型的CGI, 它可以一直执行,在请求到达时不会花费时间去fork一个进程来处理
FastCGI 是与语言无关的、可伸缩架构的 CGI 开放扩展,将 CGI 解释器进程保持在内存中
致力于减少Web服务器和CGI程序之间服务器开销,使其可以处理更多的请求
CGI 程序保持在内存中并接受 FastCGI 进程管理器调度, 则可以提供良好的性能、伸缩性、Fail-Over 特性等
运行原理
客户端访问某个 URL 地址之后,通过 GET/POST/PUT 等方式提交数据,并通过 HTTP 协议向 Web 服务器发出请求。
服务器端的 HTTP Daemon(守护进程)启动一个子进程。然后在子进程中,将 HTTP 请求里描述的信息通过标准输入 stdin 和环境变量传递给 URL 指定的 CGI 程序,并启动此应用程序进行处理,处理结果通过标准输出 stdout 返回给 HTTP Daemon 子进程。
再由 HTTP Daemon 子进程通过 HTTP 协议返回给客户端
流程图
Web 服务器不是直接执行 CGI 程序了,而是通过 Socket 与 FastCGI 响应器(FastCGI 进程管理器)进行交互,也正是由于 FastCGI 进程管理器是基于 Socket 通信的,所以也是分布式的,Web 服务器可以和 CGI 响应器服务器分开部署。
Web 服务器需要将数据 CGI/1.1 的规范封装在遵循 FastCGI 协议包中发送给 FastCGI 响应器程序
FastCGI协议
消息类型
消息发送流程
FastCGI 消息传递流程示意图
最先发送的是FCGI_BEGIN_REQUEST,然后是FCGI_PARAMS和FCGI_STDIN,由于每个消息头(下面将详细说明)里面能够承载的最大长度是65535,所以这两种类型的消息不一定只发送一次,有可能连续发送多次。
FastCGI 响应体处理完毕之后,将发送FCGI_STDOUT、FCGI_STDERR,同理也可能多次连续发送。最后以FCGI_END_REQUEST表示请求的结束。
需要注意的一点,FCGI_BEGIN_REQUEST和FCGI_END_REQUEST分别标识着请求的开始和结束,与整个协议息息相关,所以他们的消息体的内容也是协议的一部分,因此也会有相应的结构体与之对应(后面会详细说明)。
而环境变量、标准输入、标准输出、错误输出,这些都是业务相关,与协议无关,所以他们的消息体的内容则无结构体对应
由于整个消息是二进制连续传递的,所以必须定义一个统一的结构的消息头,这样以便读取每个消息的消息体,方便消息的切割。这在网络通讯中是非常常见的一种手段。
消息头
typedef struct _fcgi_header { unsigned char version; unsigned char type; unsigned char requestIdB1; unsigned char requestIdB0; unsigned char contentLengthB1; unsigned char contentLengthB0; unsigned char paddingLength; unsigned char reserved;} fcgi_header;
解释
version:标识FastCGI协议版本
type:标识FastCGI记录类型
requestId:标识记录所属的FastCGI请求
contentLength:记录的contentData组件的字节数
关于上面的xxB1和xxB0的协议说明
两个相邻的结构组件除了后缀“B1”和“B0”之外命名相同
表示这两个组件可视为估值为B1<<8 + B0的单个数字
单个数字的名字是这些组件减去后缀的名字
这个约定归纳了一个由超过两个字节表示的数字的处理方式
#include <stdio.h>#include <stdlib.h>#include <limits.h> int main(){ unsigned char requestIdB1 = UCHAR_MAX; unsigned char requestIdB0 = UCHAR_MAX; printf(\"%d\\
如果一个消息体长度超过65535,则分割为多个相同类型的消息发送即可
FCGI_BEGIN_REQUEST
typedef struct _fcgi_begin_request { unsigned char roleB1; unsigned char roleB0; unsigned char flags; unsigned char reserved[5];} fcgi_begin_request;
role:表示Web服务器期望应用扮演的角色
flags:包含一个控制线路关闭位
flags & FCGI_KEEP_CONN 为 0
应用在对本次请求响应后关闭线路
flags & FCGI_KEEP_CONN 不为0
应用在对本次请求响应后不会关闭线路
FCGI_END_REQUEST
typedef struct _fcgi_end_request { unsigned char appStatusB3; unsigned char appStatusB2; unsigned char appStatusB1; unsigned char appStatusB0; unsigned char protocolStatus; unsigned char reserved[3];} fcgi_end_request;
appStatus:组件是应用级别的状态码
protocolStatus:组件是协议级别的状态码
FCGI_REQUEST_COMPLETE:请求的正常结束。FCGI_CANT_MPX_CONN:拒绝新请求。这发生在Web服务器通过一条线路向应用发送并发的请求时,后者被设计为每条线路每次处理一个请求。FCGI_OVERLOADED:拒绝新请求。这发生在应用用完某些资源时,例如数据库连接。FCGI_UNKNOWN_ROLE:拒绝新请求。这发生在Web服务器指定了一个应用不能识别的角色时。
需要注意dcgi_protocol_status和fcgi_role各个元素的值都是 FastCGI 协议里定义好的,而非 PHP 自定义的。
消息通讯样例
配合上面各个结构体,则可以大致想到 FastCGI 响应器的解析和响应流程: 首先读取消息头,得到其类型为FCGI_BEGIN_REQUEST,然后解析其消息体,得知其需要的角色就是FCGI_RESPONDER,flag为0,表示请求结束后关闭线路。然后解析第二段消息,得知其消息类型为FCGI_PARAMS,然后直接将消息体里的内容以回车符切割后存入环境变量。与之类似,处理完毕之后,则返回了FCGI_STDOUT消息体和FCGI_END_REQUEST消息体供 Web 服务器解析。
PHP中的CGI实现
PHP中的CGI实现了FastCGI协议
是一个TCP或者是UDP协议的服务器接受来自Web服务器的请求
当启动时创建TCP/UDP协议的服务器的socket监听
随后就进入了PHP的生命周期: 模块初始化,sapi初始化,处理PHP请求,模块关闭,sapi关闭等就构成了整个CGI的生命周期
fpm
fpm是一个多进程模型,由master和多个worker组成
fpm可以配置多个端口,每个端口一个worker_pool
main()->fpm_init()
master
管理模式
静态管理
动态管理
按需管理 ondemand
事件循环
信号事件
进程检查定时器
执行超时检查定时器
变量类型(8种)
标量类型
整型
浮点型
布尔型
复合类型
特殊类型
资源
zval
zend_value
子主题/* zend_value结构 */typedef union _zend_value {\tzend_long lval;\t\t\t\t/* long value 长整型值 */\tdouble dval;\t\t\t\t/* double value 双精度浮点值 */\tzend_refcounted *counted; \t\t/* 引用计数 */\tzend_string *str; \t\t\t\t/* 字符串值 */\tzend_array *arr; \t\t\t\t/* 数组 */\tzend_object *obj;\t\t\t\t/* 对象指针,指向对象类型 */\tzend_resource *res;\t\t\t\t/* 资源类型 */\tzend_reference *ref;\t\t\t\t/* 引用类型 */\tzend_ast_ref *ast; \t\t\t\t/* 抽象语法树 */\tzval *zv;\tvoid *ptr;\tzend_class_entry *ce;\tzend_function *func;\tstruct {\t\tuint32_t w1;\t\tuint32_t w2;\t} ww;} zend_value;
zend_string
子主题/* 字符串 */struct _zend_string {\tzend_refcounted_h gc;\t\t\t\t/* gc信息 */\tzend_ulong h; /* hash value 哈希值 */\tsize_t len;\t\t\t\t/* 字符串长度 */\tchar val[1]; \t\t\t/* 字符串的值 */};
基本实现
zend_array
Bucket
/* 哈希表中实际存放值的『桶』 */typedef struct _Bucket {\tzval val;\t\t\t\t/* 存放实际的值 */\tzend_ulong h; /* hash value (or numeric index) 哈希值或者是整数值索引 */\tzend_string *key; /* string key or NULL for numerics 指向字符串,如果是数字,则为NULL */} Bucket;
散列函数
nIndex = h | ht->nTableMask; // 计算出映射表的index
数组的初始化
哈希冲突
查找
扩容
引用
引用的示例图
类型转换
转NULL
转布尔
转整形
转浮点数
转字符串
转数组
转对象
GC机制
引用计数
zend_refcounted_h结构
不需要引用计数的类型
布尔
内部字符串
不可变的字符串,例如\"HEllo\"
不可变数组
由opcode优化出来的
需要引用计数的类型
copy-on-write
只有字符串和数组才会分离
回收时机
在zval断开value指向后,如果发现refcount=0,则回收
垃圾回收
垃圾
只有array和object会出现这种情况
回收算法
内存池
PHP拥有自己的ZendMM(Zend Memory Manager)
替换glibc的malloc和free
三种粒度的内存块
chunk(2MB)
内存申请大于2MB,直接调用系统分配,分配若干个chunk
一个chunk包含512个page,其中第一个chunk被保留用于存放chunk header
page(4K)
申请内存大于3092B(3/4page大小),小于511个page
solt
申请内存小于3092B
内存池提前定义好30种不同粒度的solt
内存释放
_efree
对ptr % 2M,如果能整除,证明ptr指向一个chunk
直接归还系统
如果有余数,对余数%4K,如果整除证明ptr指向page
标记对应的page
如果chunk变空了,加入cache
cache满了,归还系统
剩下为solt
标记即可
线程安全
Apache Worker模式和Event模式
注册
获取
编译与执行
编译型
参考C语言模块编程一节
解释型
多了个解释器(虚拟机)
解释器本身定义好了一些具体的操作,这些操作被编译成机器指令
ZendVM
PHP的虚拟机
PHP代码被编译成Zend可识别的指令
把PHP变成Zend可以识别的指令opline
负责执行opcode对应的机器指令
opline指令
opline是Zend的执行指令,每条指令的编码为opcode
struct _zend_op {\tconst void *handler; \t\t// 指令执行的handler,就是一个处理函数\tznode_op op1; \t\t\t\t// 操作数1\tznode_op op2; \t\t\t\t// 操作数2\tznode_op result; \t\t\t// 返回值\tuint32_t extended_value;\t// \tuint32_t lineno;\t\t\t// 保存行号\tzend_uchar opcode;\t\t\t// opcode指令\tzend_uchar op1_type;\t\t// 操作数1的类型\tzend_uchar op2_type; \t\t// 操作数2的类型\tzend_uchar result_type;\t\t// 返回值类型};
opcode(指令编码)
173条
定义在 zend_vm_opcode.h中
操作数
typedef union _znode_op {\tuint32_t constant;\tuint32_t var;\tuint32_t num;\tuint32_t opline_num; /* Needs to be signed */\tuint32_t jmp_offset;} znode_op;
#define IS_CONST\t(1<<0) /* 字面量,123,\"Hello\"... */#define IS_TMP_VAR\t(1<<1)\t/* 临时变量,$a = \"Hello\" . time() 这里\"Hello\
handler
每条opcode对应的实际的处理函数
zend_op_array
编译器的输出,执行器的输入
struct _zend_op_array {\t/* Common elements */\tzend_uchar type;\tzend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */\tuint32_t fn_flags;\tzend_string *function_name;\tzend_class_entry *scope;\tzend_function *prototype;\tuint32_t num_args;\tuint32_t required_num_args;\tzend_arg_info *arg_info;\t/* END of common elements */\tuint32_t *refcount;\tuint32_t this_var;\tuint32_t last;\tzend_op *opcodes; // opcode的指令数组\tint last_var; // 代码中定义的变量数(CV),编译前值为0,发现一个新变量+1\tuint32_t T;\t\t// 临时变量数,IS_TMP_VAR和IS_VAR\tzend_string **vars; //PHP变量名数组,这个数组在ast编译期间配合last_var来确定各个变量的编号\tint last_brk_cont;\tint last_try_catch;\tzend_brk_cont_element *brk_cont_array;\tzend_try_catch_element *try_catch_array;\t/* static variables support */\tHashTable *static_variables; // 静态变量符号表,static声明的\tzend_string *filename;\tuint32_t line_start;\tuint32_t line_end;\tzend_string *doc_comment;\tuint32_t early_binding; /* the linked list of delayed declarations */\tint last_literal;\t// 字面量数量\tzval *literals;\t\t// 字面量数组\tint cache_size;\t// 运行时缓存数组大小\tvoid **run_time_cache; // 运行时缓存数组\tvoid *reserved[ZEND_MAX_RESERVED_RESOURCES];};
zend_execute_data
struct _zend_execute_data {\tconst zend_op *opline; /* executed opline,当前执行中的指令,可以理解为eip指令寄存器的作用 */\tzend_execute_data *call; /* current call, */\tzval *return_value;\tzend_function *func; /* executed funcrion */\tzval This; /* this + call_info + num_args */\tzend_class_entry *called_scope;\tzend_execute_data *prev_execute_data; // 调用上下文,当调用函数或者include时,会把当前zend_execute_data保存在被调用函数的zend_execute_data的这个指针中\tzend_array *symbol_table;\tvoid **run_time_cache; /* cache op_array->run_time_cache */\tzval *literals; /* cache op_array->literals */};
zendvm会根据zend_op_array信息分配一个结构来保存运行时信息
opline
相当于eip
return_value
返回值,执行完以后会把返回值设置到这个地址
symbol_table
全局符号表
prev_execute_data
调用上下文,当调用函数或者include时,会把当前zend_execute_data保存在被调用函数的zend_execute_data的这个指针中
literals
就是zend_op_array->literals
zend_executor_globals
是一个全局符号表,在main执行前分配,知道PHP退出
EG宏
保存着类、函数符号表
编译 zend_compile
词法,语法分析
词法分析器
源码=>token
PHP中使用re2c
re2c最后生成.c文件,其实.l中的代码就是夹杂了/*!re2c .... */这样一段注释的c代码,re2c编译的时候把注释成c代码
词法文件zend_language_scanner.l
使用re2c生成对应的C语言代码
语法分析器
token组合 => 抽象语法树
PHP中使用yacc
语法文件zend_languague_parser.y
使用yacc生成对应的C语言代码
抽象语法树AST
PHP7之前,语法分析后直接生成opcode,解释器和执行器不能解耦
普通节点
非子叶节点,常用于语法的根
list节点
typedef struct _zend_ast_list {\tzend_ast_kind kind;\tzend_ast_attr attr;\tuint32_t lineno;\tuint32_t children; /* 动态变化 */\tzend_ast *child[1];} zend_ast_list;
数据节点
typedef struct _zend_ast_zval {\tzend_ast_kind kind;\tzend_ast_attr attr;\tzval val;} zend_ast_zval;
声明节点
typedef struct _zend_ast_decl {\tzend_ast_kind kind;\tzend_ast_attr attr; /* Unused - for structure compatibility */\tuint32_t start_lineno;\tuint32_t end_lineno;\tuint32_t flags;\tunsigned char *lex_pos;\tzend_string *doc_comment;\tzend_string *name;\tzend_ast *child[4];} zend_ast_decl;
打开PHP文件
调用zendparse()分析语法
zendparse就是yyparse的红替换
zendparse中使用zendlex切割token,然后匹配语法,生成抽象语法树
抽象语法树存放在zend_compiler_globals中,即CG(ast)
抽象语法树编译
ZendVM会把抽象语法树变为zend_op_array
zend_emit_op,生成一条op
分别对op1 op2 result处理
pass_two()
处理一些无法在编译过程确定的数据
添加RETURN
将CV VAL TMP_VAR 由编号转为zend_execute_data上的内存偏移量
先分配CV变量
opcode对应的handler绑定
执行 zend_execute
执行
handler定义
zend_vm_def.h
然后由zend_vm_gen.php 生成 zend_vm_execute.h
调度方式
种类
CALL
这种方式是将各opcode定义的handler封装为独立的C语言函数
SWITCH
所有的opcode定义在一个函数内,利用switch语句跳转
GOTO (最快)
类似switch,但是是使用goto跳转
执行流程
执行开始之前,分配一个zend_execute_data
zend_execute_data->opline指向op_array的第一条opcodes
执行完成后把zend_execute_data释放掉
这个过程类似C语言的进程中的栈
全局execute_data和opline
运行时缓存
opcache
将zend_compile_file函数替换为persistent_compile_file函数
为了解决当源码文件没有变化的情况下,多个请求能够复用编译好的opcode
persistent_compile_file会首先检查是否有该文件的缓存
opcache通过accel_startup启动扩展
扩展开发
利用官方ext_skel工具
PHP-X
PSR
PSR-0
废弃
由PSR-4代替
由PSR-4替代
PSR-1
基础编码规范
PHP代码文件 必须 以 <?php 或 <?= 标签开始
PHP代码文件 必须 以 不带 BOM 的 UTF-8 编码
PHP代码中 应该 只定义类、函数、常量等声明,或其他会产生 副作用 的操作(如:生成文件输出以及修改 .ini 配置文件等),二者只能选其一
命名空间以及类 必须 符合 PSR 的自动加载规范:PSR-4 中的一个
类的命名 必须 遵循 StudlyCaps 大写开头的驼峰命名规范
类中的常量所有字母都 必须 大写,单词间用下划线分隔
方法名称 必须 符合 camelCase 式的小写开头驼峰命名规范
PHP代码文件必须使用<?php或者<?=开始
PHP代码文件必须以不带BOM的UTF-8编码
PHP代码中应该只定义类、函数、常量等声明,或者其他会产生副作用的操作(生产文件输出,修改php.ini配置文件),两者选其一
命名空间及类必须符合PSR的自动加载规范(PSR-4)
类名必须大写驼峰
类内常量所有字母必须大写
方法名称必须小写驼峰
PSR-2
编码风格规范
代码 必须 遵循 PSR-1 中的编码规范
代码 必须 使用4个空格符而不是「Tab 键」进行缩进
每行的字符数 应该 软性保持在 80 个之内,理论上 一定不可 多于 120 个,但 一定不可 有硬性限制
每个 namespace 命名空间声明语句和 use 声明语句块后面,必须 插入一个空白行
类的开始花括号({) 必须 写在函数声明后自成一行,结束花括号(})也 必须 写在函数主体后自成一行
方法的开始花括号({) 必须 写在函数声明后自成一行,结束花括号(})也 必须 写在函数主体后自成一行。
类的属性和方法 必须 添加访问修饰符(private、protected 以及 public),abstract 以及 final 必须 声明在访问修饰符之前,而 static 必须 声明在访问修饰符之后
控制结构的开始花括号({) 必须 写在声明的同一行,而结束花括号(}) 必须 写在主体后自成一行
控制结构的开始左括号后和结束右括号前,都 一定不可 有空格符
必须遵循PSR-1规范
4个空格符替代tab
代码每行字符数控制在80-120之间
namespace和using语句后必须插入一个空白行
类的{}必须自成一行
方法{}必须声明后自成一行
PSR-3
日志规范接口
八个方法:八个等级的日志:debug、 info、 notice、 warning、 error、 critical、 alert 以及 emergency
日志规范
PSR-4
自动加载
PSR-6
缓存规范接口
创建一套通用的接口规范,能够让开发人员整合到现有框架和系统,而不需要去 开发框架专属的适配器类
缓存接口规范
PSR-7
HTTP消息规范接口
HTTP消息接口
composer
映射关系
autoload
classmap
base
PHP7
Native TLS
指定函数参数和返回值类型
zval结构
24字节缩减到16字节
增加Throwable
HashTable从72字节缩减到56字节
Bucket从72字节缩减到32字节
execute_data、opline采用寄存器存储
扩展编写中,新的参数解析方式
Golang
Go环境、命令与工具
环境变量(go module 不再需要)
GOPATH
工作目录,编码目录
pkg
bin
src
如果在GOPATH项目中使用module,需要设置环境变量 GO111MODULE=on
GOROOT
go官方源码的安装目录
GOBIN
可执行二进制目录
go build
go build [-o output] [-i] [build flags] [packages]
说明
构建模式 -buildmode
archive
编译构建所列出的非Main包代码成为 .a 文件
main 包会被忽略
c-archive
c-shared
default
main包会被编译为可执行文件
非main包会被编译为.a文件
shared
exe
pie
plugin
把main包和他导入的其他包编译为Go Plugins,非main包会被忽略
go get
go mod
inti
初始化一个项目
vendor
复制对应的依赖到当前项目下并生成vendor目录
tidy
go install
go tool
二进制实际位置
存放在 src/cmd 目录中
compile
历史遗留(1.5以前)
Go 原生编译器 gc
主要基于 Ken Thompson 先前在 Plan 9 操作系统上使用的C工具链
Go 语言的编译器和链接器都是使用C语言编写并产生本地代码,没有实现自举
gccgo编译器
使用 GCC 作为后端
编译速度相对 gc 较慢,但产生的本地代码运行要稍微快一点。
同时也提供一些与 C 语言之间的互操作性。
不同平台不同的编译器汇编器链接器(这些命名都是来自于 Plan 9 项目)
词法分析
词法分析程序是 GNU bison
语法分析
$GOROOT/src/cmd/gc/go.y 的 yacc 文件
1.5开始
编译工具都由Go实现自举
编译器和运行时优化
interface类型
在接口值中放置零宽度类型不会分配
gc: 1.0+
gccgo: ?
在接口值中放置一个字大小或更小的非指针类型不会被分配
gccgo: never
string和[]byte
逃逸分析和内联
原语
非可遍历对象
link
pprof
profile 文件
gzip 压缩过的 profile.proto 文件格式
通过 Linux 的 setitimer 设置定时器
发送 SIGPROF 信号
处理者
被监控的程序调用 StartCPUProfile & StopCPUProfile
利用 pprof 工具查看
vet
go env
查看go已设置的环境变量
go vet
检查可能出现的错误
go vet [-n] [-x] [-vettool prog] [build flags] [vet flags] [packages]
go doc
govendor
glide
第三方出品
dep
官方出品,放弃
go module
2018年提出vgo(Versioned Go)
实验性特性
Go 1.11 中提供使用
Go 1.
命名
25个关键字
built-in(再定义中重新使用它们)
Go语言程序员推荐使用 驼峰式 命名
变量名字的开头字母的大小写决定了名字在包外的可见性
声明
四种声明语句
var
变量声明
const
常量声明
type
类型声明
func
函数声明
包的声明语句
声明package
import语句
包一级的类型、变量、常量、函数的声明语句
顺序无关
包内访问
局部声明
一个函数的声明由一个函数名字、参数列表、一个可选的返回值列表和包含函数定义的函数体组成。
执行函数从函数的第一个语句开始,依次顺序执行直到遇到renturn返回语句
如果没有返回语句则是执行到函数末尾
var 变量名字 类型 = 表达式
var a string = \"\"
var a string
var a = \"\"
简短变量声明
anim := gif.GIF{LoopCount: nframes}freq := rand.Float64() * 3.0t := 0.0
简短变量声明语句中必须至少要声明一个新的变量
变量的别名
一个指针的值是另一个变量的地址
var i *int
比较
只有当它们指向同一个变量或全部是nil时才相等
返回函数中局部变量的地址也是安全的
区别于C语言
指针不可运算
通过new创建变量
表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为*T
new只是一个预定义的函数。不是关键字
可以被覆盖定义
包一级声明变量
整个程序运行期间
局部变量
从每次创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。
函数的参数变量和返回值变量都是局部变量
编译器会自动选择在栈上还是在堆上分配局部变量的存储空间
逃逸的变量需要额外分配内存
堆上分配
对性能的优化可能会产生细微的影响
赋值
普通赋值
新值的表达式放在=的右边
数值变量也可以支持++递增和--递减语句
x = i++之类的表达式是错误的
元组赋值
我们可以用下划线空白标识符_来丢弃不需要的值
可赋值性
类型要相同
nil可以赋值给任何指针或引用类型的变量
==或!=进行相等比较的能力
两者类型要相等
map不能直接寻址struct中的属性进行赋值
type 类型名字 底层类型
属于不同类型,不能比较
包和文件
一个包所在目录路径的后缀是包的导入路径
gopl.io/ch1/helloworld对应的目录路径是$GOPATH/src/gopl.io/ch1/helloworld
如果一个名字是大写字母开头的,那么该名字是导出的
导入包
import \"xxxx\"
Go语言的规范并没有定义这些字符串的具体含义或包来自哪里,它们是由构建工具来解释的
每个包还有一个包名,包名一般是短小的名字
例如gopl.io/ch2/tempconv包的名字一般是tempconv
如果导入了一个包,但是又没有使用该包将被当作一个编译错误处理
goimports工具
自动添加或删除导入的包
包的初始化
顺序
解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化
init函数
func init() { /* ... */ }
每个文件可以有多个init函数
不能被调用或引用
作用域
语法块是由花括弧所包含的一系列语句
语法块内部声明的名字是无法被外部语法块访问的
全局语法块
包语法块
分支语法块
显式语法块
下标索引
表达式
a[x]
非map
下标x必须为整型或者无类型常量
常量下标必须是非负数并且能被int替代的
常量下标可以是由int转换来的无类型常量
索引范围应该落在0 <= x < len(a),否则会造成越界
对于数组类型
常量索引必须在范围内
不在范围内的索引会引发运行时异常
a[x]是数组索引下标为x的元素,类型和数组的元素类型一致
对于指向数组的指针
a[x] 和 (*a)[x] 等价
slice
a[x]是其索引下标为x的元素,类型和slice的元素类型一致
当string是常量时,常量索引要在string范围内
如果x越界,会引发运行时异常
a[x]是一个不固定的byte,类型就是byte
a[x] 不能被赋值
map
其他类型下标索引都不符合语法
Blocks 语法块 作用域
Block = \"{\" StatementList \"}\" .StatementList = { Statement \";\" } .
源码中含蓄的作用域
所有Go源码都处于一个全局作用域
每个包都有一个包含其下所有源码的包作用域
每个文件都有它自己的文件作用域
switch和select的子句部分也是一个作用域
变量类型
值类型
布尔类型
true: 1 == 1
false: 1 != 1
整型类型
有符号整数
int8、int16、int32和int64
无符号整数
uint8、uint16、uint32和uint64
特定CPU平台机器字大小(32或64bit,不能对此做任何的假设)
有符号:int
无符号:uint
Unicode字符rune类型
和int32等价
通常用于表示一个Unicode码点
byte类型
和uint8等价
一般用于强调数值是一个原始的数据而不是一个小的整数
uintptr类型
没有指定具体的bit大小但是足以容纳指针
浮点类型
float32,float64(默认)
复数
complex128(64位实数+64位虚数)【默认】、complex64(32位实数+32位虚数)
不可改变的字节序列
内置的len函数可以返回一个字符串中的字节数目
常量
值在编译期计算
只能是布尔型、数字型(整数型、浮点型和复数)和字符串型
const pi = 3.14159
const (...)
常量间的所有算术运算、逻辑运算和比较运算的结果也是常量
一个常量的声明也可以包含一个类型和一个值,但是如果没有显式指明类型,那么将从右边的表达式推断类型
iota 常量生成器
用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式
在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0
每一个有常量声明的行加一
无类型常量
编译器为没有明确的基础类型的数字常量提供比基础类型更高精度的算术运算
六种类型
数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成
数组的每个元素都被初始化为元素类型对应的零值
如果在数组的长度位置出现的是“...”省略号,则表示数组的长度是根据初始化值的个数来计算
数组的长度是数组类型的一个组成部分
引用数据类型
Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。
channal
interface
make 与 new
make用于内建类型(map、slice 和channel)的内存分配,会初始化内存
new用于各种类型的内存分配,并返回 *T
对于slice、map等,相当于构建了指针,但是没有初始化底层的结构
零值初始化
数值类型 => 0
字符串类型 => 空字符串
布尔类型 => false
接口或引用类型(包括slice、map、chan和函数)=> nil
函数
函数申明
接口
协程
channel
声明 ch := make(chan int)
对应一个make创建的底层数据结构的引用
不带缓存的Channels
带缓存的Channels
反射
if err != nil {}
测试
*_test.go文件
go test
生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件
测试函数
以Test为函数名前缀的函数
func TestName(t *testing.T) { // ...}
测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头
基准测试函数
以Benchmark为函数名前缀
示例函数
以Example为函数名前缀
编程陷阱
协程参数问题(v共享了变量)
接口比较问题
数值比较问题
Map元素不可寻址问题
for问题
内存泄露
入门
实现和声明
Go汇编代码必须以Go包的方式组织,同时包中至少要有一个Go语言文件用于指明当前包名等基本包信息。
用于变量的定义和函数的定义Go汇编文件类似于C语言中的.c文件
用于导出汇编中定义符号的Go源文件类似于C语言的.h文件
Go汇编语言提供了GLOBL命令用于将符号导出
Go汇编语言提供了DATA命令用于初始化包变量
预定义标志
#include \"textflag.h\"
计算机结构
x86-64架构
Go汇编中的伪寄存器
MOV指令
还可以用于处理数据的扩展和截断操作
基础算术指令
控制流指令
CMP、JMP-if-x、JMP、CALL、RET等指令
常量和全局变量
Go汇编语言中常量以$美元符号为前缀
常量的类型有整数常量、浮点数常量、字符常量和字符串常量等几种类型
对于数值型常量,可以通过常量表达式构成新的常量
$2+2 // 常量表达式$3&1<<2 // == $4$(3&1)<<2 // == $4
全局变量
在Go汇编中全局变量和全局函数更为相似,都是通过一个人为定义的符号来引用对应的内存,区别只是内存中存放是数据还是要执行的指令。
在Go汇编语言中,内存是通过SB伪寄存器定位。
SB是Static base pointer的缩写,意为静态内存的开始地址。
GLOBL汇编指令用于定义名为symbol的变量,变量对应的内存宽度为width,内存宽度部分必须用常量初始化。
数组类型
bool型变量
int型变量
float型变量
string类型变量
framesize部分表示函数的局部变量需要多少栈空间
函数参数和返回值
控制流
调试
Web 开发
通信
TCP
HTTP
net/rpc包
RPC方法
T1 T2 都可被 encoding/gob 序列化
规则
方法类型是公开的
方法是公开的
方法有两个参数,都是公开的
方法的第二个参数是指针类型
方法返回一个 error 类型
Beego
gin
database/sql
提供了一套操作数据库的接口和规范,例如抽象好的SQL预处理(prepare),连接池管理,数据绑定,事务,错误处理等等
github.com/go-sql-driver/mysql
MySQL的驱动
Effective Go
格式化
使用gofmt格式化代码
缩进
tab
行的长度
没有限制
括号
Go语言支持C风格的块注释 /* */ 和C++风格的行注释 //
godoc 既是一个程序,又是一个Web服务器,它对Go的源码进行处理,并提取包中的文档内容。
每个包都应包含一段包注释,即放置在包子句前的一个块注释。
包名
当一个包被导入后,包名就会成了内容的访问器
import \"bytes\
包应当以小写的单个单词来命名,且不应使用下划线或驼峰记法
在少数发生冲突的情况下, 可为导入的包选择一个别名来局部使用
getter和setter
Go并不对获取器(getter)和设置器(setter)提供自动支持
若你有个名为 owner (小写,未导出)的字段,其获取器应当名为 Owner(大写,可导出)而非 GetOwner。
大写字母即为可导出的这种规定为区分方法和字段提供了便利。 若要提供设置器方法,SetOwner 是个不错的选择。
接口命名
如果只包含一个方法的接口,使用-er后缀
驼峰记法
Go中约定使用驼峰记法
分号
词法分析器会使用一条简单的规则来自动插入分号,因此因此源码中基本就不用分号了。
控制结构
If语句
强制大括号
省略不必要的else
重新声明与再次赋值
在满足下列条件时,已被声明的变量 v 可出现在:= 声明中
本次声明与已声明的 v 处于同一作用域中(若 v 已在外层作用域中声明过,则此次声明会创建一个新的变量)
在初始化中与其类型相应的值才能赋予 v
在此次声明中至少另有一个变量是新声明的
即便Go中的函数形参和返回值在词法上处于大括号之外, 但它们的作用域和该函数体仍然相同。
For语句
统一了 for 和 while,不再有 do-while 了
range 子句遍历数组、切片、字符串或者映射
对于字符串,range 能够提供更多便利。它能通过解析UTF-8, 将每个独立的Unicode码点分离出来
错误的编码将占用一个字节,并以符文U+FFFD来代替
Switch语句
不会自动下溯,但 case 可通过逗号分隔来列举相同的处理条件
break 语句可以使 switch 提前终止
能接受一个可选的标签实现跳转
类型选择
switch 可用于判断接口变量的动态类型
多值返回
可命名结果形参
Defer
Go的 defer 语句用于预设一个函数调用(即推迟执行函数), 该函数会在执行 defer 的函数返回之前立即执行
被推迟函数的实参(如果该函数为方法则还包括接收者)在推迟执行时就会求值, 而不是在调用执行时才求值。
被推迟的函数按照后进先出(LIFO)的顺序执行
例子
数据
new分配
为类型为 T 的新项分配已置零的内存空间, 并返回它的地址
构造函数与复合字面
复合字面的字段必须按顺序全部列出。
但如果以 字段:值 对的形式明确地标出元素,初始化字段时就可以按任何顺序出现,未给出的字段值将赋予零值。
复合字面同样可用于创建数组、切片以及映射,字段标签是索引还是映射键则视情况而定。
make分配
只用于创建切片、映射和信道,并返回类型为 T(而非 *T)的一个已初始化 (而非置零)的值。
出现这种用差异的原因在于,这三种类型本质上为引用数据类型,它们在使用前必须初始化。
数组是值。将一个数组赋予另一个数组会复制其所有元素。
特别地,若将某个数组传入某个函数,它将接收到该数组的一份副本而非指针。
数组的大小是其类型的一部分
切片
切片保存了对底层数组的引用,若你将某个切片赋予另一个切片,它们会引用同一个数组
若某个函数将一个切片作为参数传入,则它对该切片元素的修改对调用者而言同样可见, 这可以理解为传递了底层数组的指针。
切片的容量可通过内建函数 cap 获得
二维切片
循环分配内存
一次性分配内存
Map
其键可以是任何相等性操作符支持的类型, 如整数、浮点数、复数、字符串、指针、接口(只要其动态类型支持相等性判断)、结构以及数组。
切片不能用作映射键,因为它们的相等性还未定义。
若将映射传入函数中,并更改了该映射的内容,则此修改对调用者同样可见。
赋值和获取映射值的语法类似于数组,不同的是映射的索引不必为整数。
若试图通过映射中不存在的键来取值,就会返回与该映射中项的类型对应的零值。
打印
改进的格式 %+v 会为结构体的每个字段添上字段名,而另一种格式 %#v 将完全按照Go的语法打印值。
追加
追加记得要返回,因为底层结构可能会改变
初始化
枚举常量使用枚举器 iota 创建
每个源文件都可以通过定义自己的无参数 init 函数来设置一些必要的状态
其实每个文件都可以拥有多个 init 函数
指针 vs. 值
值方法可通过指针和值调用, 而指针方法只能通过指针来调用。
接口与其它类型
接口转换与类型断言
通用性
若某种现有的类型仅实现了一个接口,且除此之外并无可导出的方法,则该类型本身就无需导出。 仅导出该接口能让我们更专注于其行为而非实现
空白标识符
多重赋值中的空白标识符
未使用的导入和变量
为副作用而导入
接口检查
var _ json.Marshaler = (*RawMessage)(nil)
以此来要求 *RawMessage 实现 Marshaler
内嵌
命名冲突
字段或方法 X 会隐藏该类型中更深层嵌套的其它项 X
若相同的嵌套层级上出现同名冲突,通常会产生一个错误。
若重名永远不会在该类型定义之外的程序中使用,那就不会出错。
并发
通过通信共享内存
goroutine
它是与其它goroutine并发运行在同一地址空间的函数
无缓冲信道在通信时会同步交换数据
包
time
After
Sleep
Duration
Parse
sync
Once
WaitGroup
Mutux
unsafe
Alignof
返回对应参数类型的首地址需要对齐的倍数.
对齐系数
Sizeof
返回操作数在内存中的字节大小
Sizeof函数返回的大小只包括数据结构中固定的部分,例如字符串对应结构体中的指针和字符串长度部分,但是并不包含指针指向的字符串的内容。
Offsetof
Pointer
可以包含任意类型变量的地址,类似C语言中的void*类型的指针
不可以直接通过*p来获取unsafe.Pointer指针指向的真实变量的值
转换规则
*T 与 Pointer 可以互转
uintptr 与 Pointer 可以互转
一些转换技巧
把*T1转成*T2
将Pointer转换为uintptr将产生指向值的整数型内存地址
将Pointer转为uintptr后参与运算并返回
在使用 stscall.Syscall 中,将 Pointer 转为 uintptr
Conversion of the result of reflect.Value.Pointer or reflect.Value.UnsafeAddr from uintptr to Pointer.
Conversion of a reflect.SliceHeader or reflect.StringHeader Data field to or from Pointer.
CGO
开启CGO
安装gccgo
Linux & MacOS 安装 gcc
windows 安装 MinGW
CGO_ENABLED 设置为1
示例
import \"C\" 语句
如果在Go代码中出现该语句,则表示使用了CGO特性
紧邻import \"C\"语句前面的注释是一种特殊语法,里面包含正常的C语言代码
C是一个虚拟包
需要独占一行,不能和其他包一同import
当确保CGO启动的情况下,还可以在当前目录中包含C/C++对应的源文件
CGO中的传递的参数要和声明的参数类型保持一致,并使用C包中的转换函数进行转换,不能直接传入Go的参数
通过C包代入的函数不需要大写字母开头,不受Go的代码规则限制
不同的Go包中引入的C包中的类型是不相同的
main.C.CChar 和 Other.C.CChar 不同
#cgo 语句
在 import \"C\" 语句前的注释中可以用来设置编译阶段和链接阶段的相关参数
编译阶段的参数主要用于定义相关宏和指定头文件检索路径
链接阶段的参数主要是指定库文件检索路劲和要链接的库文件
主要影响CFLAGS、CPPFLAGS、CXXFLAGS、FFLAGS和LDFLAGS几个编译器环境变量。
#cgo
CFLAGS部分,-D部分定义了宏PNG_DEBUG,值为1;-I定义了头文件包含的检索目录
LDFLAGS部分,-L指定了链接时库文件检索目录,-l指定了链接时需要链接png库
#cgo指令还支持条件选择,当满足某个操作系统或某个CPU架构类型时后面的编译或链接选项生效。
// #cgo windows CFLAGS: -DX86=1// #cgo !windows LDFLAGS: -lm
build标志条件编译
build标志是在Go或者CGO环境下的C/C++文件开头的一种特殊的注释
类似于 #cgo 中定义的宏,但是能够自定义,不受限与系统定义的宏
实例
代码
构建命令
可以通过 -tags 同时制定多个build标志,他们之间用空格隔开
C代码模块化
hello.h
hello.c
main.go
用Go重新实现C函数
hello.go
通过CGO的//export SayHello指令将Go语言实现的函数SayHello导出为C语言函数。
C语言的代码最终会通过桥接调用Go语言的代码
字符串和切片
其中只有字符串和切片在CGO中有一定的使用价值
如果使用了GoString类型则会对_cgo_export.h头文件产生依赖,而这个头文件是动态输出的
结构体、联合、枚举类型
但是如果有2个成员:一个是以Go语言关键字命名,另一个刚好是以下划线和Go语言关键字命名,那么以Go语言关键字命名的成员将无法访问(被屏蔽)
struct
在Go语言中,我们可以通过C.struct_xxx来访问C语言中定义的struct xxx结构体类型。
如果结构体的成员名字中碰巧是Go语言的关键字,可以通过在成员名开头添加下划线来访问
C语言结构体中位字段对应的成员无法在Go语言中访问,如果需要操作位字段成员,需要通过在C语言中定义辅助函数来完成。对应零长数组的成员,无法在Go语言中直接访问数组的元素,但其中零长的数组成员所在位置的偏移量依然可以通过unsafe.Offsetof(a.arr)来访问。
在C语言中,我们无法直接访问Go语言定义的结构体类型。
union
我们可以通过C.union_xxx来访问C语言中定义的union xxx类型
但是Go语言中并不支持C语言联合类型,它们会被转为对应大小的字节数组。
如果需要操作C语言的联合类型变量,一般有三种方法
在C语言中定义辅助函数
通过Go语言的\"encoding/binary\"手工解码成员(需要注意大端小端问题)
使用unsafe包强制转型为对应类型(这是性能最好的方式)。
对于复杂的联合类型,推荐通过在C语言中定义辅助函数的方式处理。
enum
通过C.enum_xxx来访问C语言中定义的enum xxx结构体类型
数组、字符串和切片
指针间的转换
数值和指针的转换
切片间的转换
函数调用
Go调用C函数
对于一个启用CGO特性的程序,CGO会构造一个虚拟的C包。通过这个虚拟的C包可以调用C语言函数。
C函数的返回值
CGO也针对<errno.h>标准库的errno宏做的特殊支持:在CGO调用C函数时如果有两个返回值,那么第二个返回值将对应errno错误状态。
void函数的返回值
C语言的void类型对应的是当前的main包中的_Ctype_void类型
在CGO生成的代码中,_Ctype_void类型对应一个0长的数组类型[0]byte
C调用Go导出函数
//export
对于C语言,导出的函数是一个可全局访问的C语言函数
如果在两个不同的Go语言包内,都存在一个同名的要导出为C语言函数的函数,那么在最终的链接阶段将会出现符号重名的问题。
内部机制
CGO生成的中间文件(通过生成中间桥接代码)
Go 调用 C 代码
C调用Go代码
CGO内存模型
C++类包装
静态库和动态库
使用C静态库
静态库因为是静态链接,最终的目标程序并不会产生额外的运行时依赖,也不会出现动态库特有的跨运行时资源管理的错误。不过静态库对链接阶段会有一定要求:静态库一般包含了全部的代码,里面会有大量的符号,如果不同静态库之间出现了符号冲突则会导致链接的失败。
GCC使用静态库编译:gcc -o a.out main.c file.a
可以用go generate工具来生成静态库,或者是通过Makefile来构建静态库
因为多了一个静态库的构建步骤,这种使用了自定义静态库并已经包含了静态库全部代码的Go包无法直接用go get安装。不过我们依然可以通过go get下载,然后用go generate触发静态库构建,最后才是go install来完成安装。
为了支持go get命令直接下载并安装,我们C语言的#include语法可以将number库的源文件链接到当前的包。
使用C动态库
动态库可以隔离不同动态库之间的关系,减少链接时出现符号冲突的风险
对于CGO来说,使用动态库和静态库是一样的,因为动态库也必须要有一个小的静态导出库用于链接动态库(Linux下可以直接链接so文件,但是在Windows下必须为dll创建一个.a文件用于链接)
导出C静态库
使用 //export 注释
Go命令行生成静态库:go build -buildmode=c-archive -o number.a
GCC命令行生成静态库:gcc -c -o file.o file.c && ar rcs libfile.a file.o
导出C动态库
gcc环境创建动态库:gcc -shared -o libfile.so file.c
Go命令行创建动态库:go build -buildmode=c-shared -o file.so
导出非main包的函数
要实现从是从非main包导出C函数,或者是多个包导出C函数(因为只能有一个main包),我们需要自己提供导出C函数对应的头文件(因为CGO无法为非main包的导出函数生成头文件)。
编译和链接参数
编译参数:CFLAGS/CPPFLAGS/CXXFLAGS
编译参数主要是头文件的检索路径,预定义的宏等参数。
CFLAGS对应C语言编译参数(以.c后缀名)
链接参数:LDFLAGS
链接参数主要包含要链接库的检索目录和要链接库的名字
不支持相对路径
经过编译后的C和C++目标文件格式是一样的,因此LDFLAGS对应C/C++共同的链接参数。
pkg-config
可以通过#cgo pkg-config xxx命令来生成xxx库需要的编译和链接参数,其底层通过调用 pkg-config xxx --cflags生成编译参数,通过pkg-config xxx --libs命令生成链接参数。
对于非标准的C/C++库没有实现pkg-config支,可以手工为pkg-config工具创建对应库的编译和链接参数实现支持
手工创建/usr/local/lib/pkgconfig/xxx.pc文件
通过PKG_CONFIG 环境变量可指定自定义的pkg-config程序
go get 链
链条断裂
如果依赖的C/C++包比较小并且有源代码的前提下,可以优先选择从代码构建。
多个非main包中导出C函数
非main包的Go导出函数也是有效的
不同包导出的Go函数将在同一个全局的名字空间,因此需要小心避免重名的问题
底层原理
编译原理
src/cmd/compile
前端
当拿到一组文件的抽象语法树之后,Go 语言的编译器会对语法树中定义和使用的类型进行检查
1. 常量、类型和函数名及类型
2. 变量的赋值和初始化
3. 函数和闭包的主体
4. 哈希键值对的类型
5. 导入函数体
6. 外部的声明
类型检查阶段不止会对节点的类型进行验证,还会展开和改写一些内建的函数
例如 make 关键字在这个阶段会根据子树的结构被替换成 makeslice 或者 makechan 等函数
强弱类型
静态与动态类型
执行过程
切片 OTARRAY
哈希 OTMAP
OMAKE
后端
中间代码生成
将编程语言直接翻译成机器码的过程拆成两个简单步骤 —— 中间代码生成和机器码生成
中间代码是一种更接近机器语言的表示形式,对中间代码的优化和分析相比直接分析高级编程语言更容易;
在类型检查之后,就会通过一个名为 compileFunctions 的函数开始对整个 Go 语言项目中的全部函数进行编译
这些函数会在一个编译队列中等待几个后端工作协程的消费
这些并发执行的 Goroutine 会将所有函数对应的抽象语法树转换成中间代码
配置初始化
这个过程中我们会缓存可能用到的类型指针、初始化 SSA 配置和一些之后会调用的运行时函数,还会根据当前的目标设备初始化特定的 ABI
initssaconfig函数
调用 NewTypes 初始化一个新的 Types 结构体并调用 NewPtr 函数缓存类型的信息,Types 结构体中存储了所有 Go 语言中基本类型对应的指针
根据当前的 CPU 架构初始化 SSA 配置 ssaConfig
会初始化一些编译器会用到的 Go 语言运行时的函数
遍历和替换
编译器会将 Go 语言关键字转换成 runtime 包中的函数
在生成中间代码之前,我们还需要对抽象语法树中节点的一些元素进行替换,这个替换的过程就是通过 walk 和很多以 walk 开头的相关函数实现的
会将一些关键字和内建函数转换成函数调用
runtime 函数定义
src/cmd/compile/internal/gc/builtin/runtime.go 文件中找到函数对应的签名和定义
作用只是让编译器能够找到对应符号的函数定义而已
真正的函数实现都在另一个 src/runtime 包中
SSA
特性
在中间代码中使用 SSA 的特性能够为整个程序实现以下的优化
常数传播(constant propagation)
值域传播(value range propagation)
稀疏有条件的常数传播(sparse conditional constant propagation)
消除无用的程式码(dead code elimination)
全域数值编号(global value numbering)
消除部分的冗余(partial redundancy elimination)
强度折减(strength reduction)
寄存器分配(register allocation)
SSA是编译器后端的一部分
SSA生成
经过 walk 系列函数的处理之后,AST 的抽象语法树就不再会改变了,Go 语言的编译器会使用 compileSSA 函数将抽象语法树转换成中间代码
AST到SSA
多轮转换
机器码生成
src/cmd/compile/internal 目录中包含了很多机器码生成相关的包
不同类型的 CPU 分别使用了不同的包生成机器码
其中包括 amd64、arm、arm64、mips、mips64、ppc64、s390x、x86 和 wasm
调度器
历史版本
单线程调度
多线程调度
任务窃取调度(Go1.1)
改进
在当前的 G-M 模型中引入了处理器 P,增加中间层
处理器 P 的基础上实现基于工作窃取的调度器
工作原理
抢占式调度(Go1.2-Go1.13)
解决问题
某些 Goroutine 可以长时间占用线程,造成其它 Goroutine 的饥饿
垃圾回收需要暂停整个程序(Stop-the-world,STW),最长可能需要几分钟的时间,导致整个程序无法工作
基于协作的抢占式调度
需要函数调用作为入口才能触发抢占
编译器会在调用函数前插入 runtime.morestack
Go 语言运行时会在垃圾回收暂停程序、系统监控发现 Goroutine 运行超过 10ms 时发出抢占请求 StackPreempt
当发生函数调用时,可能会执行编译器插入的 runtime.morestack 函数,它调用的 runtime.newstack 会检查 Goroutine 的 stackguard0 字段是否为 StackPreempt
如果 stackguard0 是 StackPreempt,就会触发抢占让出当前线程
缺陷
无法被抢占例子
基于信号的抢占式调度
程序启动时,在 runtime.sighandler 函数中注册 SIGURG 信号的处理函数 runtime.doSigPreempt
在触发垃圾回收的栈扫描时会调用 runtime.suspendG 挂起 Goroutine
将 _Grunning 状态的 Goroutine 标记成可以被抢占,即将 preemptStop 设置成 true
调用 runtime.preemptM 触发抢占
GPM调度模型
G协程
是 Go 语言调度器中待执行的任务,占用了更小的内存空间,也降低了上下文切换的开销,用户态线程
在 Go 语言运行时使用私有结构体 runtime.g 表示
stack 字段
stackguard0 字段
可以用于调度器抢占式调度
preempt
抢占信号
preemptStop
抢占时将状态修改成 `_Gpreempted`
preemptShrink
在同步安全点收缩栈
_panic
最内侧的 panic 结构体
_defer
最内侧的延迟函数结构体
m
当前 Goroutine 占用的线程,可能为空
sched
存储 Goroutine 的调度相关的数据
gobuf 结构:调度器保存或者恢复上下文使用
sp:栈指针
pc:程序计数器
g:持有 runtime.gobuf 的 Goroutine
ret:系统调用的返回值
atomicstatus
Goroutine 的状态
goid
Goroutine 的 ID,该字段对开发者不可见
状态
_Gidle
刚刚被分配并且还没有被初始化
_Grunnable
没有执行代码,没有栈的所有权,存储在运行队列中
_Grunning
可以执行代码,拥有栈的所有权,被赋予了内核线程 M 和处理器 P
_Gsyscall
正在执行系统调用,拥有栈的所有权,没有执行用户代码,被赋予了内核线程 M 但是不在运行队列上
_Gwaiting
由于运行时而被阻塞,没有执行用户代码并且不在运行队列上,但是可能存在于 Channel 的等待队列上
_Gdead
没有被使用,没有执行代码,可能有分配的栈
_Gcopystack
栈正在被拷贝,没有执行代码,不在运行队列上
_Gpreempted
由于抢占而被阻塞,没有执行用户代码并且不在运行队列上,等待唤醒
_Gscan
GC 正在扫描栈空间,没有执行代码,可以与其他状态同时存在
切换
等待中
Goroutine 正在等待某些条件满足,例如:系统调用结束等,包括 _Gwaiting、_Gsyscall 和 _Gpreempted 几个状态
可运行
Goroutine 已经准备就绪,可以在线程运行,如果当前程序中有非常多的 Goroutine,每个 Goroutine 就可能会等待更多的时间,即 _Grunnable
运行中
Goroutine 正在某个线程上运行,即 _Grunning
P上下文
提供线程需要的上下文环境,也会负责调度线程上的等待队列
Go 语言程序的处理器数量一定会等于 GOMAXPROCS
维护着线程与处理器之间的关系
runhead、runqtail 和 runq
表示处理器持有的运行队列,其中存储着待执行的 Goroutine 列表
runnext
线程下一个要执行的goroutine
_Pidle
处理器没有运行用户代码或者调度器,被空闲队列或者改变其状态的结构持有,运行队列为空
_Prunning
被线程 M 持有,并且正在执行用户代码或者调度器
_Psyscall
没有执行用户代码,当前线程陷入系统调用
_Pgcstop
被线程 M 持有,当前处理器由于垃圾回收被停止
_Pdead
当前P不再被使用
M系统线程
调度器最多可以创建 10000 个线程,但是其中大多数的线程都不会执行用户代码(可能陷入系统调用),最多只会有 GOMAXPROCS 个活跃线程能够正常运行。
每一个活跃线程都对应一个运行时中的 runtime.m 结构体
g0
持有调度栈的 Goroutine
会深度参与运行时的调度过程,包括 Goroutine 的创建、大内存分配和 CGO 函数的执行
curg
在当前线程上运行的用户 Goroutine
p
正在运行代码的处理器
nextp
暂存的处理器
oldp
执行系统调用之前的使用线程的处理器
调度器启动
schedinit函数
获取CPU、线程数量
在调度器初始函数执行的过程中会将 maxmcount 设置成 10000
但是可以同时运行的线程还是由 GOMAXPROCS 变量控制
runtime.procresize 更新程序中处理器的数量
整个程序不会执行任何用户 Goroutine,调度器也会进入锁定状态
如果全局变量 allp 切片中的处理器数量少于期望数量,就会对切片进行扩容
使用 new 创建新的处理器结构体并调用 runtime.p.init 方法初始化刚刚扩容的处理器;
通过指针将线程 m0 和处理器 allp[0] 绑定到一起;
调用 runtime.p.destroy 方法释放不再使用的处理器结构;
通过截断改变全局变量 allp 的长度保证与期望处理器数量相等;
将除 allp[0] 之外的处理器 P 全部设置成 _Pidle 并加入到全局的空闲队列中;
创建goroutine
关键字会在编译期间通过以下方法 cmd/compile/internal/gc.state.stmt 和 cmd/compile/internal/gc.state.call 两个方法将该关键字转换成 runtime.newproc 函数调用
newproc1函数
根据传入参数初始化一个 g 结构体
获取或者创建新的 Goroutine 结构体
将传入的参数移到 Goroutine 的栈上
调用 runtime.memmove 函数将 fn 函数的全部参数拷贝到栈上,argp 和 narg 分别是参数的内存空间和大小,我们在该方法中会直接将所有参数对应的内存空间整片的拷贝到栈上
更新 Goroutine 调度相关的属性
将 Goroutine 加入处理器的运行队列
初始化结构体
两种不同的方式获取新的 runtime.g 结构体
从 Goroutine 所在处理器的 gFree 列表或者调度器的 sched.gFree 列表中获取 runtime.g 结构体
当处理器的 Goroutine 列表为空时,会将调度器持有的空闲 Goroutine 转移到当前处理器上,直到 gFree 列表中的 Goroutine 数量达到 32
当处理器的 Goroutine 数量充足时,会从列表头部返回一个新的 Goroutine
调用 runtime.malg 函数生成一个新的 runtime.g 函数并将当前结构体追加到全局的 Goroutine 列表 allgs 中
运行队列
runtime.runqput 函数会将新创建的 Goroutine 运行队列上,这既可能是全局的运行队列,也可能是处理器本地的运行队列
当 next 为 true 时,将 Goroutine 设置到处理器的 runnext 上作为下一个处理器执行的任务
当 next 为 false 并且本地运行队列还有剩余空间时,将 Goroutine 加入处理器持有的本地运行队列
当处理器的本地运行队列已经没有剩余空间时就会把本地队列中的一半 Goroutine 和待加入的 Goroutine 通过 runqputslow 添加到调度器持有的全局运行队列上
调度信息
分别将程序计数器和 Goroutine 设置成 runtime.goexit 函数和新创建的 Goroutine
pc
存储了传入函数的程序计数器
sp
存放了 goexit 函数
调度循环
mstart
初始化 g0 的 stackguard0 和 stackguard1 字段
mstart1
初始化线程并调用 runtime.schedule 进入调度循环
为了保证公平,当全局运行队列中有待执行的 Goroutine 时,通过 schedtick 保证有一定几率会从全局的运行队列中查找对应的 Goroutine
从处理器本地的运行队列中查找待执行的 Goroutine
如果前两种方法都没有找到 Goroutine,就会通过 runtime.findrunnable 进行阻塞地查找 Goroutine
从本地运行队列、全局运行队列中查找
从网络轮询器中查找是否有 Goroutine 等待运行
通过 runtime.runqsteal 函数尝试从其他随机的处理器中窃取待运行的 Goroutine,在该过程中还可能窃取处理器中的计时器
runtime.execute 函数执行获取的 Goroutine,做好准备工作后,它会通过 runtime.gogo 将 Goroutine 调度到当前线程上
runtime.gogo 在不同平台的实现不同,由汇编编写
runtime.goexit 的程序计数器被放到了栈 SP 上
返回地址
待执行函数的程序计数器被放到了寄存器 BX 上
触发调度
调度点
主动挂起
runtime.gopark -> runtime.park_m
触发调度最常见的方法
该函数会将当前 Goroutine 暂停
被暂停的任务不会放回运行队列
会通过 runtime.mcall 在切换到 g0 的栈上调用 runtime.park_m 函数
将当前 Goroutine 的状态从 _Grunning 切换至 _Gwaiting
调用 runtime.dropg 移除线程和 Gorooutine 之间的关联
调用 runtime.schedule 触发新一轮的调度了
当 Goroutine 等待的特定条件满足后,运行时会调用 runtime.goready 将因为调用 runtime.gopark 而陷入休眠的 Goroutine 唤醒
系统调用
runtime.exitsyscall -> runtime.exitsyscall0
Go 语言通过 syscall.Syscall 和 syscall.RawSyscall 等使用汇编语言编写的方法封装了操作系统提供的所有系统调用
INVOKE_SYSCALL
runtime.entersyscall 和 runtime.exitsyscall
出于性能考虑,如果这次系统调用不需要运行时参与,就会使用 syscall.RawSyscall 简化这一过程
只有可以立刻返回的系统调用才可能会被设置成 RawSyscall 类型
准备工作
runtime.entersyscall 函数会在获取当前程序计数器和栈位置之后调用 runtime.reentersyscall
禁止线程上发生的抢占,防止出现内存不一致的问题
保证当前函数不会触发栈分裂或者增长
保存当前的程序计数器 PC 和栈指针 SP 中的内容
将 Goroutine 的状态更新至 _Gsyscall
将 Goroutine 的处理器和线程暂时分离并更新处理器的状态到 _Psyscall
释放当前线程上的锁
runtime.reentersyscall 方法会使处理器和线程的分离,当前线程会陷入系统调用等待返回
结束恢复
当系统调用结束后,会调用退出系统调用的函数 runtime.exitsyscall 为当前 Goroutine 重新分配资源
调用 exitsyscallfast 函数
如果 Goroutine 的原处理器处于 _Psyscall 状态,就会直接调用 wirep 将 Goroutine 与处理器进行关联
_Psyscall 状态会被 sysmon 定期扫描重置
如果调度器中存在闲置的处理器,就会调用 acquirep 函数使用闲置的处理器处理当前 Goroutine
切换至调度器的 Goroutine 并调用 exitsyscall0 函数
当我们通过 pidleget 获取到闲置的处理器时就会在该处理器上执行 Goroutine
在其它情况下,我们会将当前 Goroutine 放到全局的运行队列中,等待调度器的调度
协作式调度
runtime.Gosched -> runtime.gosched_m -> runtime.goschedImpl
g0 的栈上调用 runtime.goschedImpl 函数,运行时会更新 Goroutine 的状态到 _Grunnable,让出当前的处理器并将 Goroutine 重新放回全局队列
该函数会调用 runtime.schedule 重新触发调度
系统监控
runtime.sysmon -> runtime.retake -> runtime.preemptone
线程管理
提供了 runtime.LockOSThread 和 runtime.UnlockOSThread 让我们有能力绑定 Goroutine 和线程完成一些比较特殊的操作
Goroutine 应该在调用操作系统服务或者依赖线程状态的非 Go 语言库时调用 runtime.LockOSThread 函数
LockOSThread
runtime.dolockOSThread 会分别设置线程的 lockedg 字段和 Goroutine 的 lockedm 字段,这两行代码会绑定线程和 Goroutine
UnlockOSThread
Go 程序启动流程
汇编代码位置
src/runtime/asm_amd64.s
runtime·rt0_go
m0
启动程序后的主线程
负责执行初始化操作和启动第一个g,之后和其他的m没有区别
全局变量的g0是m0的g0
runtime.rt0_go
把传入的argc和argv保存到栈上
获取当前cpu的信息并保存到各个全局变量
调用_cgo_init如果函数存在
测试TLS是否工作
设置m0.g0 = g0
设置g0.m = m0
调用runtime.check做一些检查
调用runtime.args保存传入的argc和argv到全局变量
调用runtime.osinit根据系统执行不同的初始化
设置了全局变量ncpu等于cpu核心数量
调用runtime.schedinit执行共同的初始化
调用runtime·mstart启动m0
runtime.mstart这个函数是m的入口点(不仅仅是m0)
runtime.main
锁机制
Mutex
state表示当前互斥锁的状态
mutexLocked 表示互斥锁的锁定状态
mutexWoken 表示从正常模式被从唤醒
mutexStarving 当前的互斥锁进入饥饿状态
waitersCount 当前互斥锁上等待的 Goroutine 个数
sema是用于控制锁状态的信号量
正常模式
锁的等待者会按照先进先出的顺序获取锁
刚被唤起的 Goroutine 与新创建的 Goroutine 竞争时,大概率会获取不到锁
一旦 Goroutine 超过 1ms 没有获取到锁,它就会将当前互斥锁切换饥饿模式,防止部分 Goroutine 被『饿死』
性能更好
饥饿模式
Go1.9推出
如果一个 Goroutine 获得了互斥锁并且它在队列的末尾或者它等待的时间少于 1ms,那么当前的互斥锁就会被切换回正常模式
加锁和解锁
互斥锁的加锁是靠 sync.Mutex.Lock 完成的
当锁的状态是 0 时,将 mutexLocked 位置成 1
自旋锁
如果互斥锁的状态不是 0 时就会调用 sync.Mutex.lockSlow 尝试通过自旋(Spinnig)等方式等待锁的释放
判断当前 Goroutine 能否进入自旋
通过自旋等待互斥锁的释放
计算互斥锁的最新状态
更新互斥锁的状态并获取锁
互斥锁只有在普通模式才能进入自旋
sync.runtime_canSpin 需要返回 true
运行在多 CPU 的机器上
当前 Goroutine 为了获取该锁进入自旋的次数小于四次
当前机器上至少存在一个正在运行的处理器 P 并且处理的运行队列为空
不阻塞,用户态
没有上下文切换带来的开销
浪费CPU资源
信号量
解锁
RWMutex
Cond
内存分配
Go 语言的内存分配器就借鉴了 TCMalloc 的设计实现高速的内存分配,它的核心理念是使用多级缓存根据将对象根据大小分类,并按照类别实施不同的分配策略。
虚拟内存布局
在 Go 语言 1.10 以前的版本,堆区的内存空间都是连续的;但是在 1.11 版本,Go 团队使用稀疏的堆内存空间替代了连续的内存,解决了连续内存带来的限制以及在特殊场景下可能出现的问题
线性内存
spans 区域存储了指向内存管理单元 runtime.mspan 的指针,每个内存单元会管理几页的内存空间,每页大小为 8KB
bitmap 用于标识 arena 区域中的那些地址保存了对象,位图中的每个字节都会表示堆区中的 32 字节是否包含空闲
arena 区域是真正的堆区,运行时会将 8KB 看做一页,这些内存页中存储了所有在堆上初始化的对象
稀疏内存
运行时使用二维的 runtime.heapArena 数组管理所有的内存,每个单元都会管理 64MB 的内存空间
整个堆区最多可以管理 256TB 的内存
地址空间
四种状态
None
内存没有被保留或者映射,是地址空间的默认状态
Reserved
运行时持有该地址空间,但是访问该内存会导致错误
Prepared
内存被保留,一般没有对应的物理内存访问该片内存的行为是未定义的可以快速转换到 Ready 状态
Ready
可以被安全访问
转换方法
runtime.sysAlloc 会从操作系统中获取一大块可用的内存空间,可能为几百 KB 或者几 MB
runtime.sysFree 会在程序发生内存不足(Out-of Memory,OOM)时调用并无条件地返回内存
runtime.sysReserve 会保留操作系统中的一片内存区域,对这片内存的访问会触发异常
runtime.sysMap 保证内存区域可以快速转换至准备就绪
runtime.sysUsed 通知操作系统应用程序需要使用该内存区域,需要保证内存区域可以安全访问
runtime.sysUnused 通知操作系统虚拟内存对应的物理内存已经不再需要了,它可以重用物理内存
runtime.sysFault 将内存区域转换成保留状态,主要用于运行时的调试
内存管理组件
内存管理单元 SPAN
runtime.mspan 是 Go 语言内存管理的基本单元,该结构体中包含 next 和 prev 两个字段,它们分别指向了前一个和后一个 runtime.mspan
每个 runtime.mspan 都管理 npages 个大小为 8KB 的页
startAddr 和 npages — 确定该结构体管理的多个页所在的内存,每个页的大小都是 8KB
freeindex — 扫描页中空闲对象的初始索引
allocBits 和 gcmarkBits — 分别用于标记内存的占用和回收情况
allocCache — allocBits 的补码,可以用于快速查找内存中未被使用的内存
当结构体管理的内存不足时,运行时会以页为单位向堆申请内存
当用户程序或者线程向 runtime.mspan 申请内存时,该结构会使用 allocCache 字段以对象为单位在管理的内存中快速查找待分配的空间
运行时会使用 runtime.mSpanStateBox 结构体存储内存管理单元的状态 runtime.mSpanState
该状态可能处于 mSpanDead、mSpanInUse、mSpanManual 和 mSpanFree 四种情况
当 runtime.mspan 在空闲堆中,它会处于 mSpanFree 状态;当 runtime.mspan 已经被分配时,它会处于 mSpanInUse、mSpanManual 状态
在垃圾回收的任意阶段,可能从 mSpanFree 转换到 mSpanInUse 和 mSpanManual
在垃圾回收的清除阶段,可能从 mSpanInUse 和 mSpanManual 转换到 mSpanFree
在垃圾回收的标记阶段,不能从 mSpanInUse 和 mSpanManual 转换到 mSpanFree
跨度类 runtime.spanClass
Go 语言的内存管理模块中一共包含 67 种跨度类(包含一个0跨度),每一个跨度类都会存储特定大小的对象并且包含特定数量的页数以及对象,所有的数据都会被预选计算好并存储在 runtime.class_to_size 和 runtime.class_to_allocnpages 等变量中
除了上述 66 个跨度类之外,运行时中还包含 ID 为 0 的特殊跨度类,它能够管理大于 32KB 的特殊对象
runtime.spanClass 是一个 uint8 类型的整数,它的前 7 位存储着跨度类的 ID,最后一位表示是否包含指针,垃圾回收会对包含指针的 runtime.mspan 结构体进行扫描
线程缓存(Thread Cache)
线程缓存属于每一个独立的线程,它能够满足线程上绝大多数的内存分配需求
不涉及多线程,不需要使用互斥锁来保护内存,这能够减少锁竞争带来的性能损耗
每一个处理器都会被分配一个线程缓存 runtime.mcache 用于处理微对象和小对象的分配,它们会持有内存管理单元 runtime.mspan
每一个线程缓存都持有 67 * 2 个 runtime.mspan ,这些内存管理单元都存储在结构体的 alloc 字段中
初始化后的 runtime.mcache 中的所有 runtime.mspan 都是空的占位符 emptymspan
替换
runtime.mcache.refill 方法会为线程缓存获取一个指定跨度类的内存管理单元
微分配器
这三个字段组成了微对象分配器,专门为 16 字节以下的对象申请和管理内存
中心缓存(Central Cache)
当线程缓存不能满足需求时,就会使用中心缓存作为补充解决小对象的内存分配问题
当内存管理单元中不存在空闲对象时,它们会从 runtime.mheap 持有的 134 个中心缓存 runtime.mcentral 中获取新的内存单元,中心缓存属于全局的堆结构体 runtime.mheap,它会从操作系统中申请内存
每一个中心缓存都会管理某个跨度类的内存管理单元,它会同时持有两个 runtime.mSpanList,分别存储包含空闲对象的列表和不包含空闲对象的链表
该结构体在初始化时,两个链表都不包含任何内存,程序运行时会扩容结构体持有的两个链表,nmalloc 字段也记录了该结构体中分配的对象个数
SPAN 获取
从有空闲对象的 runtime.mspan 链表中查找可以使用的内存管理单元
从没有空闲对象的 runtime.mspan 链表中查找可以使用的内存管理单元
调用 runtime.mcentral.grow 从堆中申请新的内存管理单元
更新内存管理单元的 allocCache 等字段帮助快速分配内存
中心缓存的扩容方法 runtime.mcentral.grow 会根据预先计算的 class_to_allocnpages 和 class_to_size 获取待分配的页数以及跨度类并调用 runtime.mheap.alloc 获取新的 runtime.mspan 结构
页堆(Page Heap)
该结构体中包含两组非常重要的字段,其中一个是全局的中心缓存列表 central,另一个是管理堆区内存区域的 arenas 以及相关字段
页堆中包含一个长度为 134 的 runtime.mcentral 数组,其中 67 个为跨度类需要 scan 的中心缓存,另外的 67 个是 noscan 的中心缓存
在遇到 32KB 以上的对象时,内存分配器就会选择页堆直接分配大量的内存。
堆上所有的对象都会通过调用 runtime.newobject 函数分配内存
该函数会调用 runtime.mallocgc 分配指定大小的内存空间
对象大小
较小的字符串以及逃逸的临时变量
先使用微型分配器,再依次尝试线程缓存、中心缓存和堆分配内存
微分配器可以将多个较小的内存分配请求合入同一个内存块中,只有当内存块中的所有对象都需要被回收时,整片内存才可能被回收。
依次尝试使用线程缓存、中心缓存和堆分配内存
确定分配对象的大小以及跨度类 runtime.spanClass;
从线程缓存、中心缓存或者堆中获取内存管理单元并从内存管理单元找到空闲的内存空间;
调用 runtime.memclrNoHeapPointers 清空空闲内存中的所有数据;
直接在堆上分配内存
GC
标记清除
两个阶段
标记
从根对象出发查找并标记堆中所有存活的对象
清除
遍历堆中的全部对象,回收未被标记的垃圾对象并将回收的内存加入空闲链表
用户程序在垃圾收集的过程中也不能执行
STW
三色抽象
三类对象
白色
潜在的垃圾,其内存可能会被垃圾收集器回收
黑色
活跃的对象,包括不存在任何引用外部指针的对象以及从根对象可达的对象
灰色
活跃的对象,因为存在指向白色对象的外部指针,垃圾收集器会扫描这些对象的子对象
从灰色对象的集合中选择一个灰色对象并将其标记成黑色
将黑色对象指向的所有对象都标记成灰色,保证该对象和被该对象引用的对象都不会被回收
重复上述两个步骤直到对象图中不存在灰色对象
三色标记清除算法本身是不可以并发或者增量执行的,它仍然需要 STW
屏障技术
三色不变性
强三色不变性
黑色对象不会指向白色对象,只会指向灰色对象或者黑色对象
弱三色不变性
黑色对象指向的白色对象必须包含一条从灰色对象经由多个白色对象的可达路径
想要在并发或者增量的标记算法中保证正确性,需要达成两种三色不变性(Tri-color invariant)中的任意一种
垃圾收集中的屏障技术更像是一个钩子方法,它是在用户程序读取对象、创建新对象以及更新对象指针时执行的一段代码
读屏障
需要在读操作中加入代码片段,对用户程序的性能影响很大
写屏障
插入写屏障
标记过程
垃圾收集器将根对象指向 A 对象标记成黑色并将 A 对象指向的对象 B 标记成灰色
用户程序修改 A 对象的指针,将原本指向 B 对象的指针指向 C 对象,这时触发写屏障将 C 对象标记成灰色
垃圾收集器依次遍历程序中的其他灰色对象,将它们分别标记成黑色
有存活可能的对象都标记成灰色
本轮漏回收
因为栈上的对象在垃圾收集中也会被认为是根对象,所以为了保证内存的安全,Dijkstra 必须为栈上的对象增加写屏障或者在标记阶段完成重新对栈上的对象对象进行扫描
删除写屏障
用户程序将 A 对象原本指向 B 的指针指向 C,触发删除写屏障,但是因为 B 对象已经是灰色的,所以不做改变
用户程序将 B 对象原本指向 C 的指针删除,触发删除写屏障,白色的 C 对象被涂成灰色
混合写屏障
v1.7 版本之前
使用插入写屏障
在标记阶段完成时暂停程序、将所有栈对象标记为灰色并重新扫描
大量 goroutine 下,重新扫描的过程需要占用 10 ~ 100ms 的时间
v1.8 +
使用混合写屏障
将被覆盖的对象标记成灰色并在当前栈没有扫描时将新对象也标记成灰色
在垃圾收集的标记阶段,我们还需要将创建的所有新对象都标记成黑色,防止新分配的栈内存和堆内存中的对象被错误地回收
增量和并发
增量垃圾收集
增量地标记和清除垃圾,降低应用程序暂停的最长时间
GC周期变长
写屏障会影响到应用开销
并发垃圾收集
利用多核的计算资源,在用户程序执行时并发标记和清除垃圾
并不是所有阶段都可以与用户程序一起运行,部分阶段还是需要暂停用户程序的
垃圾收集调步(Pacing)算法
并发收集器的堆内存
因为并发垃圾收集器会与程序一起运行,所以它无法准确的控制堆内存的大小,并发收集器需要在达到目标前触发垃圾收集,这样才能够保证内存大小的可控,并发收集器需要尽可能保证垃圾收集结束时的堆内存与用户配置的 GOGC 一致
减小红色部分
随 v1.5 引入, v1.10 优化
阶段
清除终止
暂停程序,所有的处理器在这时会进入安全点(Safe point)
如果当前垃圾收集循环是强制触发的,我们还需要处理还未被清理的内存管理单元
将状态切换至 _GCmark、开启写屏障、用户程序协助(Mutator Assiste)并将根对象入队
恢复执行程序,标记进程和用于协助的用户程序会开始并发标记内存中的对象,写屏障会将被覆盖的指针和新指针都标记成灰色,而所有新创建的对象都会被直接标记成黑色
开始扫描根对象,包括所有 Goroutine 的栈、全局对象以及不在堆中的运行时数据结构,扫描 Goroutine 栈期间会暂停当前处理器
依次处理灰色队列中的对象,将对象标记成黑色并将它们指向的对象标记成灰色
使用分布式的终止算法检查剩余的工作,发现标记阶段完成后进入标记终止阶段
标记终止
暂停程序、将状态切换至 _GCmarktermination 并关闭辅助标记的用户程序
清理处理器上的线程缓存
将状态切换至 _GCoff 开始清理阶段,初始化清理状态并关闭写屏障
恢复用户程序,所有新创建的对象会标记成白色
后台并发清理所有的内存管理单元,当 Goroutine 申请新的内存管理单元时就会触发清理
runtime.gcphase 是垃圾收集器当前处于的阶段,可能处于 _GCoff、_GCmark 和 _GCmarktermination,Goroutine 在读取或者修改该阶段时需要保证原子性
runtime.gcBlackenEnabled 是一个布尔值,当垃圾收集处于标记阶段时,该变量会被置为 1,在这里辅助垃圾收集的用户程序和后台标记的任务可以将对象涂黑
runtime.gcController 实现了垃圾收集的调步算法,它能够决定触发并行垃圾收集的时间和待处理的工作
runtime.gcpercent 是触发垃圾收集的内存增长百分比,默认情况下为 100,即堆内存相比上次垃圾收集增长 100% 时应该触发 GC,并行的垃圾收集器会在到达该目标前完成垃圾收集
runtime.writeBarrier 是一个包含写屏障状态的结构体,其中的 enabled 字段表示写屏障的开启与关闭
runtime.worldsema 是全局的信号量,获取该信号量的线程有权利暂停当前应用程序
runtime.work 变量
该结构体中包含大量垃圾收集的相关字段,例如:表示完成的垃圾收集循环的次数、当前循环时间和 CPU 的利用率、垃圾收集的模式等
触发时机
运行时会通过如下所示的 runtime.gcTrigger.test 方法决定是否需要触发垃圾收集
gcTriggerHeap
堆内存的分配达到达控制器计算的触发堆大小
gcTriggerTime
如果一定时间内没有触发,就会触发新的循环,该出发条件由 runtime.forcegcperiod 变量控制,默认为 2 分钟
gcTriggerCycle
如果当前没有开启垃圾收集,则触发新的循环
调用 gcstart
后台监控线程 runtime.sysmon 和 runtime.forcegchelper
后台运行定时检查和垃圾收集
用户程序手动触发垃圾收集 runtime.GC
申请内存时根据堆大小触发垃圾收集 runtime.mallocgc
触发
后台触发 forcegchelper
运行时会在应用程序启动时在后台开启一个用于强制触发垃圾收集的 Goroutine
通过调用 runtime.gcStart 方法尝试启动新一轮的垃圾收集
在大多数时间都是陷入休眠的,但是它会被系统监控器 runtime.sysmon 在满足垃圾收集条件时唤醒
系统监控在每个循环中都会主动构建一个 runtime.gcTrigger 并检查垃圾收集的触发条件是否满足
如果满足条件,系统监控会将 runtime.forcegc 状态中持有的 Goroutine 加入全局队列等待调度器的调度
手动触发 runtime.GC
用户程序会通过 runtime.GC 函数在程序运行期间主动通知运行时执行,该方法在调用时会阻塞调用方直到当前垃圾收集循环完成
在垃圾收集期间也可能会通过 STW 暂停整个程序
在正式开始垃圾收集前,运行时需要通过 runtime.gcWaitOnMark 函数等待上一个循环的标记终止、标记和标记终止阶段完成
调用 runtime.gcStart 触发新一轮的垃圾收集并通过 runtime.gcWaitOnMark 等待该轮垃圾收集的标记终止阶段正常结束
持续调用 runtime.sweepone 清理全部待处理的内存管理单元并等待所有的清理工作完成,等待期间会调用 runtime.Gosched 让出处理器
完成本轮垃圾收集的清理工作后,通过 runtime.mProf_PostSweep 将该阶段的堆内存状态快照发布出来,我们可以获取这时的内存状态
申请内存 runtime.mallocgc
当前线程的内存管理单元中不存在空闲空间时,创建微对象和小对象需要调用 runtime.mcache.nextFree 方法从中心缓存或者页堆中获取新的管理单元,在这时就可能触发垃圾收集
当用户程序申请分配 32KB 以上的大对象时,一定会构建 runtime.gcTrigger 结构体尝试触发 垃圾收集;
垃圾收集启动
调用 runtime.gcStart 函数
修改全局的垃圾收集状态到 _GCmark 并做一些准备工作
两次调用 runtime.gcTrigger.test 方法检查是否满足垃圾收集条件
验证垃圾收集条件的同时,该方法还会在循环中不断调用 runtime.sweepone 清理已经被标记的内存单元,完成上一个垃圾收集循环的收尾工作
暂停程序、在后台启动用于处理标记任务的工作 Goroutine、确定所有内存管理单元都被清理以及其他标记阶段开始前的准备工作
进入标记阶段、准备后台的标记工作、根对象的标记工作以及微对象、恢复用户程序,进入并发扫描和标记阶段
调用 runtime.gcBgMarkPrepare 函数初始化后台扫描需要的状态
调用 runtime.gcMarkRootPrepare 函数扫描栈上、全局变量等根对象并将它们加入队列
设置全局变量 runtime.gcBlackenEnabled,用户程序和标记任务可以将对象涂黑
调用 runtime.startTheWorldWithSema 启动程序,后台任务也会开始标记堆中的对象
暂停与恢复程序
runtime.stopTheWorldWithSema
依次停止当前处理器、等待处于系统调用的处理器以及获取并抢占空闲的处理器,处理器的状态在该函数返回时都会被更新至 _Pgcstop,等待垃圾收集器的重新唤醒
runtime.startTheWorldWithSema
调用 runtime.netpoll 从网络轮询器中获取待处理的任务并加入全局队列
调用 runtime.procresize 扩容或者缩容全局的处理器
调用 runtime.notewakeup 或者 runtime.newm 依次唤醒处理器或者为处理器创建新的线程
如果当前待处理的 Goroutine 数量过多,创建额外的处理器辅助完成任务
后台标记模式
在垃圾收集启动期间,运行时会调用 runtime.gcBgMarkStartWorkers 为全局每个处理器创建用于执行后台标记任务的 Goroutine
每一个 Goroutine 都会运行 runtime.gcBgMarkWorker
所有运行 runtime.gcBgMarkWorker 的 Goroutine 在启动后都会陷入休眠等待调度器的唤醒
三种不同的模式 runtime.gcMarkWorkerMode
gcMarkWorkerDedicatedMode
处理器专门负责标记对象,不会被调度器抢占
gcMarkWorkerFractionalMode
当垃圾收集的后台 CPU 使用率达不到预期时(默认为 25%),启动该类型的工作协程帮助垃圾收集达到利用率的目标,因为它只占用同一个 CPU 的部分资源,所以可以被调度
gcMarkWorkerIdleMode
当处理器没有可以执行的 Goroutine 时,它会运行垃圾收集的标记任务直到被抢占
并发扫描与标记辅助
runtime.gcBgMarkWorker 是后台的标记任务执行的函数
获取当前处理器以及 Goroutine 打包成 parkInfo 类型的结构体并主动陷入休眠等待唤醒
当我们调用 runtime.gopark 触发休眠时,运行时会在系统栈中安全地建立处理器和后台标记任务的绑定关系
根据处理器上的 gcMarkWorkerMode 模式决定扫描任务的策略
所有标记任务都完成后,调用 runtime.gcMarkDone 方法完成标记阶段
工作池 gcWork
这个结构体是垃圾收集器中工作池的抽象
实现了一个生产者和消费者的模型
写屏障、根对象扫描和栈扫描都会向工作池中增加额外的灰色对象等待处理
对象的扫描过程会将灰色对象标记成黑色,同时也可能发现新的灰色对象
当工作队列中不包含灰色对象时,整个扫描过程就会结束
扫描对象
运行时会使用 runtime.gcDrain 函数扫描工作缓冲区中的灰色对象,它会根据传入 gcDrainFlags 的不同选择不同的策略
策略
gcDrainUntilPreempt
当 Goroutine 的 preempt 字段被设置成 true 时返回
gcDrainIdle
调用 runtime.pollWork 函数,当处理器上包含其他待执行 Goroutine 时返回
gcDrainFractional
调用 runtime.pollFractionalWorkerExit 函数,当 CPU 的占用率超过 fractionalUtilizationGoal 的 20% 时返回
gcDrainFlushBgCredit
调用 runtime.gcFlushBgCredit 计算后台完成的标记任务量以减少并发标记期间的辅助垃圾收集的用户程序的工作量
写屏障的实现需要编译器和运行时的共同协作
在 SSA 中间代码生成阶段,编译器会使用 cmd/compile/internal/ssa.writebarrier 函数在 Store、Move 和 Zero 操作中加入写屏障
当所有处理器的本地任务都完成并且不存在剩余的工作 Goroutine 时,后台并发任务或者辅助标记的用户程序会调用 runtime.gcMarkDone 通知垃圾收集器
将垃圾收集的状态切换至 _GCmarktermination
如果本地队列中仍然存在待处理的任务,当前方法会将所有的任务加入全局队列并等待其他 Goroutine 完成处理
调用 runtime.gcMarkTermination 进入标记终止阶段
数据统计帮助控制器决定下一轮触发垃圾收集的堆大小
调用 runtime.gcSweep 重置清理阶段的相关状态并在需要时阻塞清理所有的内存管理单元
内存清理
对象回收器(Reclaimer)
内存单元回收器
会在内存中查找所有的对象都未被标记的 runtime.mspan
该过程会被 runtime.mheap.reclaim 触发
所有的回收工作最终都是靠 runtime.mspan.sweep 完成的
在内存管理单元中查找并释放未被标记的对象,但是如果 runtime.mspan 中的所有对象都没有被标记,整个单元就会被直接回收
该过程会被 runtime.mcentral.cacheSpan 或者 runtime.sweepone 异步触发
Stack内存管理
源码位置
runtime/stack.go
协程初始化栈大小
// The minimum size of stack used by Go code\t_StackMin = 2048
Go 1.2: goroutine stack has been increased from 4Kb to 8Kb.Go 1.4: goroutine stack has decreased from 8Kb to 2Kb.
\t_FixedStack0 = _StackMin + _StackSystem
栈分裂(stack split)
Segmented stacks 机制(旧)
当一个协程被创建时,默认8k栈内存
每个function都有前置的检测代码,检测是否用完了栈内存,如果是的话,调用morestack函数
Contiguous stacks 机制(新)
go 1.3推出了连续栈,连续栈使用了另外一种策略,不再把栈分成一段一段的,当栈空间不够时,直接new一个2倍大的栈空间,并将原先栈空间中的数据拷贝到新的栈空间中,而后销毁旧栈。这样当出现栈空间触及边界时,不会产生栈分裂的情况。
源码方法:newstack
newsize := oldsize * 2
栈收缩
源码方法:shrinkstack
newsize := oldsize / 2
复用原来的栈地址
类型底层实现
*reflect.StringHeader
array
不可变
两种声明方式在运行期间得到的结果是完全相同的
类型检查阶段会提取数组大小
随后会使用 cmd/compile/internal/types.NewArray 函数创建包含数组大小的 Array 类型
第二种声明方式在编译期间就会被『转换』成为前一种
推导
上限推导
语句转换
读写
内部结构
hmap
count 表示当前哈希表中的元素数量
B 表示当前哈希表持有的 buckets 数量,但是因为哈希表中桶的数量都 2 的倍数,所以该字段会存储对数,也就是 len(buckets) == 2^B;
hash0 是哈希的种子,它能为哈希函数的结果引入随机性,这个值在创建哈希表时确定,并在调用哈希函数时作为参数传入
oldbuckets 是哈希在扩容时用于保存之前 buckets 的字段,它的大小是当前 buckets 的一半
当哈希表中存储的数据过多,单个桶无法装满时就会使用 extra.overflow 中桶存储溢出的数据
溢出桶能够减少扩容的频率
正常桶和溢出桶在内存中是连续存储的
bmap
哈希表 hmap 的桶就是 bmap
每一个 bmap 都能存储 8 个键值对
tophash 存储了键的哈希的高 8 位
编译时会自动根据类型添加字段
keys
elems
overflow
字面量
使用字面量初始化的方式最终都会通过 cmd/compile/internal/gc.maplit 函数初始化
当哈希表中的元素数量少于或者等于 25 个时,编译器会直接调用 addMapEntries 将字面量初始化的结构体转换成图中的代码,将所有的键值对一次加入到哈希表中
超过了 25 个,就会在编译期间创建两个数组分别存储键和值的信息,这些键值对会通过一个 for 循环加入目标的哈希
两者最后都会使用 runtime.makemap 构建hash
运行时
使用make调用
Go 语言编译器都会在类型检查期间将它们转换成对 runtime.makemap 的调用
计算哈希占用的内存是否溢出或者超出能分配的最大值
调用 fastrand 获取一个随机的哈希种子
根据传入的 hint 计算出需要的最小需要的桶的数量
在哈希函数的选择上,会在程序启动时,检测 cpu 是否支持 aes,如果支持,则使用 aes hash,否则使用 memhash。
使用 runtime.makeBucketArray 创建用于保存桶的数组
根据传入的 B 计算出的需要创建的桶数量在内存中分配一片连续的空间用于存储数据
当桶的数量小于 2^4 时,由于数据较少、使用溢出桶的可能性较低,这时就会省略创建的过程以减少额外开销
当桶的数量多于等于 2^4 时,就会额外创建 2^(B-4) 个溢出桶
当溢出桶数量较多时会通过 runtime.newobject 创建新的溢出桶
读写操作
读取
在编译的类型检查期间,hash[key] 以及类似的操作都会被转换成对哈希的 OINDEXMAP 操作
中间代码生成阶段会在 cmd/compile/internal/gc.walkexpr 函数中将这些 OINDEXMAP 操作转换成相应代码
当接受参数仅为一个时,会使用 runtime.mapaccess1,该函数仅会返回一个指向目标值的指针
当接受两个参数时,会使用 runtime.mapaccess2,除了返回目标值之外,它还会返回一个用于表示当前键对应的值是否存在的布尔值
查找过程
runtime.mapaccess1 函数会先通过哈希表设置的哈希函数、种子获取当前键对应的哈希,再通过 bucketMask 和 add 函数拿到该键值对所在的桶序号和哈希最上面的 8 位数字
在 bucketloop 循环中,哈希会依次遍历正常桶和溢出桶中的数据,它会比较这 8 位数字和桶中存储的 tophash,每一个桶都存储键对应的 tophash,每一次读写操作都会与桶中所有的 tophash 进行比较,用于选择桶序号的是哈希的最低几位,而用于加速访问的是哈希的高 8 位,这种设计能够减少同一个桶中有大量相等 tophash 的概率。
每一个桶都是一整片的内存空间,当发现桶中的 tophash 与传入键的 tophash 匹配之后,我们会通过指针和偏移量获取哈希中存储的键 keys[0] 并与 key 比较,如果两者相同就会获取目标值的指针 values[0] 并返回。
图示
写入
当形如 hash[k] 的表达式出现在赋值符号左侧时,该表达式也会在编译期间转换成调用 runtime.mapassign 函数
首先是函数会根据传入的键拿到对应的哈希和桶
通过遍历比较桶中存储的 tophash 和键的哈希,如果找到了相同结果就会获取目标位置的地址并返回,其中 inserti 表示目标元素的在桶中的索引,insertk 和 elem 分别表示键值对的地址,获得目标地址之后会直接通过算术计算进行寻址获得键值对 k 和 elem
在上述的 for 循环中会依次遍历正常桶和溢出桶中存储的数据,整个过程会依次判断 tophash 是否相等、key 是否相等,遍历结束后会从循环中跳出。
如果当前桶已经满了,哈希会调用 newoverflow 函数创建新桶或者使用 hmap 预先在 noverflow 中创建好的桶来保存数据,新创建的桶不仅会被追加到已有桶的末尾,还会增加哈希表的 noverflow 计数器。
runtime.mapassign 函数会在以下两种情况发生时触发哈希的扩容
装载因子已经超过 6.5
哈希使用了太多溢出桶
溢出桶中的Bucket使用量超过 2 ^ 16
扩容不是一个原子的过程
机制
扩容的入口是 runtime.hashGrow 函数
如果没有超过 loadFactor,则等量扩容
将old bucket pointer 指向旧的bucket
重新生成新的bucket
如果超过了 loadFactor,双倍扩容
通过 runtime.makeBucketArray 创建一组新桶和预创建的溢出桶
随后将原有的桶数组设置到 oldbuckets 上并将新的空桶设置到 buckets 上
溢出桶也使用了相同的逻辑进行更新
数据迁移
哈希表的数据迁移的过程在是 runtime.evacuate 函数中完成的,它会对传入桶中的元素进行『再分配』
runtime.evacuate 函数会将一个旧桶中的数据分流到两个新桶,所以它会创建两个用于保存分配上下文的 evacDst 结构体,这两个结构体分别指向了一个新桶
如果发生双倍扩容,两个Dst会由Hash结果的高一位决定使用哪个
新旧Hash结果可能一致
当哈希表的 oldbuckets 存在时,就会先定位到旧桶并在该桶没有被分流时从中获取键值对。
如果发生等量扩容,则evacDst只需要一个
迁移过程都会使用新旧Hash计算Hash Code
最后会调用 runtime.advanceEvacuationMark 增加哈希的 nevacuate 计数器,在所有的旧桶都被分流后清空哈希的 oldbuckets 和 oldoverflow 字段
chan
ECMAScript
历史
EcmaScript6
JavaScript
C++
编译器g++
标准库
兼容C
#include <stdio.h>
替换C
#include <cstdio>
#include <iostream>
省略.h
替代#define
内存申请符号
new
内联函数
默认参数
类与对象
继承与派生
多态与虚函数
运算符重载
模板
异常
Effective C++
用const替代#define
常量指针:const *char const authorName = \"Bruce\"
常定义在头文件中
更好的方式:const std::string authorName(\"Bruce\")
类内常量:static const int NumTurns = 5;
Shell
Bash基础
使用多个命令
date;whoami
创建shell脚本文件
#!/bin/bash
显示消息
echo命令可用单引号或双引号来划定文本字符串
如果想把文本字符串和命令输出显示在同一行中,可以用echo语句的-n参数。
使用变量
环境变量
set;
用户变量
Var1=123
echo $Var1
命令替换
Var1=`pwd`
Var1=$(pwd)
重定向输入和输出
输出重定向
command > outputfile
可以用双大于号(>>)来追加数据
输入重定向
command < inputfile
Lua
Python
Java
JAVA_HOME
JDK的存储路径
CLASSPATH
类的搜索路径
-classpath 参数指定
数据类型与运算符
基本语法
基础语法
大小写敏感
类名
类名的首字母应该大写
方法名
以小写字母开头
源文件名
源文件名必须和类名相同
主方法入口
所有的Java 程序由public static void main(String []args)方法开始执行
标识符
关键字不能用作标识符,标识符是大小写敏感的
修饰符
访问控制修饰符
public
protected
private
非访问控制修饰符
final
abstract
strictfp
类变量
成员变量
关键字
继承
接口可理解为对象间相互通信的协议
接口只定义派生要用到的方法,但是方法的具体实现完全取决于派生类
内置数据类型
byte
8位(1字节),有符号,二进制补码表示的整数
最小值-128
最大值127
默认值是0
byte类型用于大型数组中节约空间,主要替代整数
short
数据类型是16位(2字节),有符号的,二进制补码表示的整数
最小值-32768
最大值32767
默认值为0
int
32位(4字节),有符号,二进制补码表示的整数
一般地整型变量默认为 int 类型
long
64位(8字节),有符号,二进制补码表示的整数
默认值是 0L
\"L\"理论上不分大小写,但是若写成\"l\"容易与数字\"1\"混淆,不容易分辩。所以最好大写
boolean
char
引用类型
流程控制与数组
面向对象
Java基础类库
用户互动
系统相关
System类
Runtime类
常用类
Object类
Java集合
泛型
MySQL与JDBC编程
基本的Annotation
@Override
检查子类重写的方法是否在父类中存在
@Deprecated
@SuppressWarnings(value = \"unchecked\")
抑制错误
@SafeVarargs
抑制堆污染警告
@FunctionalInterface
检查接口是否符合函数式接口规范
元Annotation,修饰Annotation
@Retention
RetentionPolicy.SOURCE
保留在源码中,jvm直接丢弃
RetentionPolicy.CLASS
编译器将其保留在class文件中,jvm对其不可见
默认值
RetentionPolicy.RUNTIME
编译器将其保留在class文件中,jvm通过反射获取
@Target
@Documented
@Inherited
@Inheritable
修饰某个注解,表明被这个注解修饰的类的子类也被此注解修饰
自定义Annotation
定义Annotation
获取注解
重复注解
@Repeatation
类型注解
编译时处理
APT java的注解处理工具
APT对源码中的java注解生成新的源码文件和其他文件
javac -processor
输入输出
File类
访问文件和目录
文件过滤器
IO流
输入流和输出流
输入流
输出流
字节流和字符流
字节流
InputStream
OutputStream
Reader
Writer
字符流
节点流和处理流
概念模型
输入输出流体系
多线程
网络编程
类加载机制与反射
类的加载、连接和初始化
日志框架
记录框架
JDKLog
log4j2
LogBack
适配器
Apache Commons Logging
SLF4J
JVM
内存区域
GC和内存管理
类文件结构
类加载机制
字节码执行引擎
设计模式
工厂模式
定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行
抽象工厂模式
提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类
单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点
懒汉式
线程不安全
饿汉式
双检锁/双重校验锁
登记式/静态内部类
建造者模式
将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示
策略模式
原型模式
外观模式
为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用
观察者模式
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新
装饰器模式
动态地给一个对象添加一些额外的职责
责任链模式
避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止
代理模式
为其他对象提供一种代理以控制对这个对象的访问
桥接模式
将抽象部分与实现部分分离,使它们都可以独立的变化
主流编程语言比较与选型
强大的spring生态
语言成熟但繁琐
适合Web服务开发
弱类型解释型语言
性能差,未能很好支持线程
适合进行后台开发
代码简洁
性能优越,支持协程
编译速度快
适合开发服务,中间件,跨平台工具,涉及底层操作的功能
继承了C、C++的遗产
C
并发安全
可见性
有序性
操作系统
现代操作系统
什么是操作系统
一种运行在内核态的软件
提供资源抽象
管理硬件资源
四个特性
共享
虚拟
计算机硬件介绍
从内存中取指令并执行
寄存器用来保存临时数据和关键变量
取指寄存器
堆栈指针寄存器
指向内存当前栈的顶端
程序状态字寄存器PSW
系统调用中很重要
允许CPU保持两个不同的线程状态,在纳秒级别的时间尺度上来回切换
存储器
寄存器
CPU Cache
磁盘
磁带
IO设备
实现输入输出的方式
忙等待
中断
中断向量
设备编号成为部分内存的引用,可以用于寻找设备中断处理程序的地址
直接存储器访问
总线
启动计算机
操作系统概念
进程
本质是一个正在执行的程序
是系统进行资源分配和调度的一个独立单位
进程的地址空间
进程表
数组或链表结构
当前存在的每个进程都要占用其中的一项
进程树
进程间通信
软件模拟的硬件中断
硬件中断
多道程序设计
0-最大值
每个进程都有一个工作目录
IO
保护
shell
分析read系统调用过程
用于进程管理的系统调用
微内核
虚拟机
进程与线程
操作系统最核心的概念
进程模型
进程是某种类型的一个活动,他有程序、输入、输出及状态
单个处理器可以被若干个进程共享,他采用某种调度算法决定何时停止一个进程的工作并转而为另一个进程提供服务
创建进程
4种事件导致进程的创建
系统初始化
正在运行的进程通过系统调用创建新的进程
用户请求创建一个新的进程
点击桌面图标等等操作
一个批处理作业的初始化
Unix系统中只有fork可以创建进程
进程fork后面一般会跟exec函数,将子进程替换为新程序
子进程创建后,父子进程拥有不同的地址空间,他们之间会共享正文段
进程终止
四种退出
正常退出(自愿)
出错退出(自愿)
严重错误
被其他进程杀死
exit函数
kill
进程的层次结构
进程和他的子进程共同组成一个进程组
子进程可以创建更多子进程
只能拥有一个父进程
整个系统中,所有的进程都属于init为根的一棵进程树
windows没有进程层次结构
进程状态
三种状态
运行态
此时进程实际占用CPU
就绪态
可运行,未获得CPU使用权
阻塞态
逻辑上不能运行,等待外部某个事件发生
四种可能的转换
进程调度程序是操作系统的一部分
决定运行哪个进程,何时运行,运行多久
进程的实现
进程表(进程控制块)
操作系统维护的一张进程表(数组),每个正在运行的进程都会占用一个进程表项
表项包含进程的信息
程序计数器
堆栈指针
文件打开状态
……
中断服务程序的入口地址,或中断向量表(它是一个数组)的下标,中断服务程序的入口地址存放在该数组中,靠近内存底部
中断相应示意图
多道程序设计模型
CPU利用率=1-p^n
n个进程
p: IO等待与停留在内存的时间比
线程
并行实体需要共享同一个地址空间和所有可用数据的能力
线程比进程更加轻量,更容易创建和销毁
多个线程如果是CPU密集并不会带来性能上的提升,如果是IO密集的话可以加快程序执行的速度
经典线程模型
CPU调度单位
多线程共享同一地址空间和其他资源
CPU在线程间快速切换达到并行
线程是没有保护的
所有线程共享同一个打开文件集、子进程、报警已经相关信号
线程也有三种状态
线程有自己的堆栈
进程从一个主线程开始,主线程通过pthread_create创建新线程
线程是平等的
线程共享的环境
进程代码段
进程公有数据
进程打开的文件描述符
信号处理器
进程目录
进程用户ID和进程组ID
POXIS线程
为了实现可移植的线程程序,定义了这个标准
在用户空间实现线程
内核对线程无知,按照单线程进程管理
每个进程有一张线程表
比内核实现要快
允许每个进程有自己的线程调度算法
不需要系统调用,程序计数器和堆栈指针一切就可以完成线程切换
线程阻塞系统调用而不影响其他线程
页面故障
如果有一个线程页面故障,内核需要阻塞整个进程
在进程内没有轮转调度算法
在内核空间实现线程
不需要运行时系统
线程表在内核中
所有能够阻塞线程的调用都以系统调用的形式实现
在内核创建线程代价巨大,某些系统实现线程回收
内核不需要新的、非阻塞的系统调用
如果线程有页面故障,内核可以检查这个进程有没有其他线程可运行
混合实现
内核线程和用户级线程共存,多路复用
调度程序激活机制
模拟内核线程的功能
内核给每个进程分配一定数量的虚拟处理器
当内核了解到有线程阻塞之后,通知进程的运行时系统,并在堆栈中以参数形式传递有问题的线程编号和所发生时间的一个描述
上行调用进程运行时系统
弹出式线程
比如服务请求,一个请求到达启动一个线程处理
单线程代码多线程化
线程的全局变量问题
Unix的error
解决方案
为每个线程赋予私有全局变量
引入库过程
许多库过程不是可重入
信号
堆栈管理
协程可以理解为纯用户态的线程,其通过协作而不是抢占来进行切换。
在用户态完成创建,切换和销毁
从编程角度上看,协程的思想本质上就是控制流的主动让出(yield)和恢复(resume)机制
迭代器经常用来实现协程
进程、线程和协程的区别
资源分配的最小单位
进程拥有独立的地址空间
进程间通信通过信号量,信号、共享内存、消息传递、管道、队列
进程由操作系统调度
多进程方式比多线程稳定
程序执行的最小单元。CPU调度的基本单位
线程来自于进程,一个进程下可以产生多个线程
每个线程都有自己一个栈,不共享栈,但多个线程能共享同一个属于进程的堆
线程共享内存
线程消耗低于进程
某个线程产生致命错误会导致进程奔溃
线程间读写变量存在锁的问题处理起来相对麻烦
线程共享进程内的资源,比如静态变量,代码块,常量区,堆内存
协程的控制由应用程序显式调度
协程的执行最终靠的还是线程,应用程序来调度协程选择合适的线程来获取执行权
切换非常快,成本低。一般占用栈大小远小于线程(协程KB级别,线程MB级别),所以可以开更多的协程
协程比进程更轻量级
竞争条件
两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精准时序
临界区
互斥
确保当一个进程在使用一个共享变量或者文件时,其他进程不能做同样的事情
对共享内存进行访问的程序片段叫做临界区域或临界区
忙等待的互斥
当一个进程在临界区更新共享内存时,其他进程将不会进入其临界区
本质
当一个进程想进入临界区,先检查是否允许进入,若不允许,则该进程将原地等待,直到允许为止
用于忙等待的锁,叫做自旋锁
方案
屏蔽中断
单处理器系统中,进入临界区时屏蔽所有中断,出临界区打开中断,保证进程能在临界区不会被CPU切换
多处理器系统不适用
锁变量
共享一个锁变量
严格轮换法
连续测试一个变量直到某个值出现
浪费CPU
A进程
while(true){ while(turn != 0);//循环等待 critical_region();//临界区 turn = 1; noncritical_region(); }
B进程
while(true){ while(turn != 1);//循环等待 critical_region();//临界区 turn = 0; noncritical_region(); }
Peterson解法
#define TRUE 1#define FALSE 0#define N 2int interested[N] = {0}; //所有值的初始化为0(FALSE)int turn; //现在轮到谁?void enter_region(int id) //进程是0或1{ int other; //其他进程号other=1-id;另一进程 interested[id] = TRUE; //表明所感兴趣的 turn = id; //设置标志 while (turn == id && interested[other] == TRUE) ; //空语句}void leave_region(int id) //进程:谁离开?{ interested[id] = FALSE; //表示离开临界区}
TSL指令
测试并加锁
睡眠与唤醒
生产者消费者问题(界缓冲区)
两个进程共享内存区域
一个往里写,一个往外读
缓冲区满,生产者休眠
缓冲区空,消费者休眠
存在问题,wakeup信号丢失
缓冲区空,消费者刚刚读空后被调度程序切换为生产者
生产者写入,并检查之前缓冲区为空,所以wakeup消费者
消费者逻辑上并未休眠,所以wakeup信号丢失,继续运行消费者时,消费者开始休眠
生产者填满缓冲区后也休眠
解决wakeup丢失问题
加上唤醒等待位
wakeup信号发给一个清醒的进程时,唤醒等待位为1
随后进程要休眠检查到唤醒等待位为1,清除休眠
管程
一个由过程、变量、数据结构组成的一个集合
任意时刻管程中只有一个活跃的进程
进入管程的互斥是由编译器负责
使用一个整型变量来累计唤醒次数
down操作:对次数-1,如果是0则休眠
up操作:对次数+1
检查唤醒次数和修改值以及休眠这几个操作合为一个原子操作
多处理器中每个信号量应该用一个锁变量保护
通过TSL或XCHG来防止多个CPU同时访问一个锁变量
实现同步
互斥量(mutex)
信号量的简化版本
本质上是一把锁,在访问共享资源前加锁,访问结束释放锁
一个可以处于两态之一的变量,只需要一个二进制位表示
没有忙等待过程,如果得不到锁,CPU yield给其他线程
进程间共享turn变量
pthread中的互斥
pthread的另一种同步机制:条件变量
条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。
消息传递
send
未收到对方确认会重发
receive
接受成功回复确认
区分是否重发:序号
屏障
进程组
基本方式
共享内存
管道
高级方式
Socket通信
调度
操作系统中选择工作的程序
内核管理线程的调度是线程级别的
进程行为
计算密集型
长CPU突发
小频度的IO等待
IO密集型
短CPU突发
频发的IO等待
随着CPU越来越快,更多的进程趋向IO密集型
何时调度
创建新进程后
进程退出后
进程阻塞
IO发生中断
调度算法分类
抢占式调度算法
进程运行某个固定时段的最大值,超过就被挂起
CPU挑选其他进程执行
需要时间间隔发生时钟中断
非抢占式调度算法(协同)
进程运行至阻塞或放弃CPU
处理中断然后继续运行该进程
环境
批处理系统
用户交互系统
实时处理系统
调度算法目标
吞吐量
系统每小时完成的作业数量
周转周期
CPU利用率
批处理系统中的调度算法
先来先服务
维持一个队列,顺序执行进程期望的时长,不会打断遇到阻塞就选取队列中的第一个继续执行阻塞进程插到队列尾部
非抢占
最短作业优先
最短剩余时间优先
是『最短作业优先』的抢占版本
交互式系统的调度算法
轮转调度
时间片
20ms-50ms
维护一张可运行的进程列表
优先级调度
每个时钟中断后降低当前进程的优先级
对各个优先级分类,内部采用轮转调度
高响应比优先调度算法
多级队列(重要)
多级队列
最短进程优先
保证调度
彩票调度
公平分享调度
实时系统的调度算法
策略与机制
调度机制与调度策略分离
调度算法以某种形式参数化,参数由用户进程填写
线程调度
经典IPC问题
读写问题
读写锁
生产者消费者问题
哲学家就餐问题
对资源顺序获取
互斥锁
理发师问题
吸烟者问题
存储管理
无存储器抽象
内存只能运行一道程序
运行多道程序的问题
存储器抽象(地址空间)
解决两个问题
重定位
一个进程可用于寻址内存的一套地址集合
每个进程都有自己的地址空间
独立
基址寄存器与界限寄存器
受保护
交换技术
内存无法满足所有进程运行
处理内存超载的两种方法
把一个进程完整调入内存运行
虚拟内存
部分调入内存运行
空闲区(内存空洞)
内存紧缩
为进程的内存增长分配额外的内存空间
空闲内存管理
位图 图中的b)
内存大小和分配单元决定了位图大小
空闲链表 图中的c)
按照地址排序
双向链表
分配算法
首次分配
沿着链路搜索,直到第一个满足存储需求
下次适配
记录第一次搜索
最佳设配
搜索整个链表,找到最适合的节点
会产生大量小块
最差适配
总是分配最大的可用空闲区
快速设配
将常用大小的空闲区维护单独的链表
覆盖
把程序分割成多个片段
每个程序拥有自己的地址空间,这个空间被分割成多个块,每一块称为一页或者页面,每一页都有连续的地址范围
不是所有的页都在内存
需要到从磁盘换如内存
分页
虚拟地址
虚拟地址不是被直接送到总线,而是送往MMU内存管理单元
虚拟地址空间按照固定大小分成页面,物理地址对应是页框
缺页中断
16位虚拟地址:4位页号和12位偏移量
页表项结构
页框号
在不在位
保护位
访问位
高速缓存禁止位
加速分页过程
两个问题
大页表
加速页表
转换检测缓冲区 TLB
只有很少页表项会被反复读取,其他页表很少被访问
存在MMU中,TLB的管理由MMU
虚拟地址 -> MMU -> TLB -> 内存中的页表
软件TLB管理
TLB失效由操作系统管理,不再是MMU
预先加载可能用到的页表项
软失效
页表项不在TLB中
硬失效
页表项不在内存中
针对大内存的页表
多级页表
倒排页表
64位
页面置换算法
最优页面置换算法
不可实现,因为对未来不可预测
最近未使用页面置换算法
先进先出页面置换算法
第二次机会页面置换算法
时钟页面置换算法
最近最少使用页面置换算法
工作集页面置换算法
工作集时钟置换算法
分页系统中的设计问题
分配策略
局部
全局
页面大小
分离地址空间中数据与代码段
共享页面
读可以共享
共享库
内存映射文件
清除策略
分页守护进程
定期检查内存状态
虚拟内存接口
与分页有关的工作
系统需要分配磁盘的交换空间
调度一个进程时需要刷新MMU和TLB
缺页中断处理
指令备份
锁定内存中的页面
后备存储
策略和机制的分离
分段
需要排他性使用的对象
可抢占与不可抢占资源
死锁与不可抢占资源有关
资源获取
资源抢占
资源不足
顺序非法
满足死锁产生的必要条件
死锁条件
互斥条件
进程对所分配的资源进行排他性的使用
占有和等待条件
进程被阻塞的时候并不释放锁申请到的资源
不可抢占条件
进程对于已经申请到的资源在使用完成之前不可以被剥夺
环路等待条件
发生死锁的时候存在的一个 进程-资源 环形等待链
死锁处理策略
鸵鸟算法
直接忽略不处理
死锁检查与恢复
检查
利用抢占恢复
利用回滚恢复
通过杀死进程恢复
死锁避免
在资源的动态分配过程中,用某种方法防止系统进入不安全状态,从而避免死锁。
银行家算法
对每个请求检查,检查如果满足这一请求是否会达到安全状态
死锁预防
设置某些限制条件,破坏产生死锁的四个必要条件中的一个或几个,以防止发生死锁。
多媒体操作系统
多处理机系统
安全
Unix
UNIX标准和实现
UNIX标准化
ISO C
ISO C 标准的意图是提供 C 程序的可移植性,使其能适合于 大量不同的操作系统,而不只是适合 UNIX 系统。
此标准不仅定义了 C 程序设计语言的语法和语 义,还定义了其标准库
ISO C 头文件依赖于操作系统所配置的 C 编译器的版本
头文件
ISO C 标准定义的头文件
POSIX.1 标准包括这些 头文件以及另外一些头文件
IEEE POSIX
POSIX 指的是可移植操作系统接口
Single UNIX Specification
FIPS
UNIX系统实现
限制
选项
启动流程
硬件启动
OS启动
常用命令
cat
ls
dd
运维相关
top
free
df
ifconfig
netstat
tcpdump
strace
端口
21
ftp
22
ssh
25
smtp
53
DNS
80
http
443
https
GCC
高并发TCP优化
现象
too many connection
大量 TIME_WAIT
主要是调用方产生的,等待2MSL(1Min)
大量 CLOSE_WAIT
主要是代码问题,无法结束并发出FIN包
文件句柄
查看
ulimit -n
vim /etc/security/limits.conf
TCP连接数
采用TCP连接池
空闲连接心跳保活
连接失效
重试
关闭重新创建连接,使用后加入连接池
调整内核参数
IO模型
IO分为两个阶段
准备数据
数据从内核拷贝到用户态
同步IO
BIO 阻塞IO
同步阻塞IO
NIO 非阻塞IO
同步非阻塞IO
多路复用IO
select
select 函数监视的文件描述符分3类
writefds
readfds
exceptfds
函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符
几乎在所有的平台上支持
select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,默认值是1024
对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低
需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
poll
poll本质上和select没有区别
它没有最大连接数的限制,原因是它是基于链表来存储的
大量的fd的数组被整体复制于用户态和内核地址空间之间
poll还有一个特点是“水平触发”
kqueue
epoll
epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次
epoll对文件描述符的操作有两种模式
LT(level trigger)
当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET(edge trigger)
当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
没有最大并发连接的限制
效率提升,不是轮询的方式,不会随着FD数目的增加效率下降
跨平台支持不完全,例如 Darwin 内核就不支持
Reactor单线程模型
Reactor多线程模型
Reactor主从模型
信号量IO
AIO 异步IO
用户态程序向内核提交获取数据申请,立刻返回
内核准备数据后,将数据拷贝到用户态后通知应用来获取
网络与协议
网络概念
数据交换
实现数据通过网络核心从源主机到目的主机
电路交换
电话网络
建立连接
断开连接
独占资源
报文交换
报文
应用发送信息的一个整体
早期应用
电报
存储转发
以报文为单位
分组交换(主要)
分组
把报文拆分成一系列相对较小的数据包
加上头
需要拆分和重组
产生额外开销
以分组为单位
路由器和交换机对数据进行暂存,转发给下游
传输延迟
用L比特表示分组的长度,用R bps表示从路由器A到路由器B的链路传输速率。传输时延是L/R
报文交付时间
带宽
数字信道能传输的最高数据率
时延
四种分组延迟
节点处理延迟
排队延迟
传播延迟
排队
等待输出链路可用
丢包
路由缓存满了
RTT(Round Trip Time)
往返时间:从客户端发送一个很小的数据报到服务器并返回所经历的时间
链路多路复用
链路和网络资源被划分为资源片
独占分配到的资源片
资源片可能闲置(idle)
典型的方法
频分多路复用
时分多路复用
波分多路复用
码分多路复用
统计多路复用
分组交换
按需共享链路
顺序不确定
消息交换
操作系统提供的socket抽象
寻址
IP+PORT
公开协议
RFC文档
私有协议
内容
语法、格式
语义
进程何时发送和响应
OSI模型(7层网络模型)
主机
应用层
网络进程到应用程序。针对特定应用规定各层协议、时序、表示等,进行封装 。在端系统中用软件来实现,如HTTP等
典型协议
HTTP协议
P2P协议
表示层
该层被弃用。应用层的HTTP、FTP、Telnet等协议有类似的功能。传输层的TLS/SSL也有类似功能。
会话层
该层被弃用。应用层的HTTP、RPC、SDP、RTCP等协议有类似的功能。
传输层
提供应用进程之间的逻辑通信机制在网络的各个节点之间可靠地分发数据包。所有传输遗留问题;复用;流量;可靠
媒介
提供主机之间的逻辑通信机制
链路层
物理层
一个(不一定可靠的)点对点数据直链。定义机械特性;电气特性;功能特性;过程特性
TCP/IP协议栈(五层)
应用层详解
超文本传输协议
版本
HTTP 1.0
未得到良好的说明
HTTP 1.0+
keep-alive连接
虚拟主机支持
HTTP 1.1
目前使用的HTTP版本
HTTP 2.0(HTTP-NG)
增强性能
媒体类型
Web服务器会为HTTP对象数据附加一个MIME类型
application/*
应用程序特有的内容格式(离散类型)
audio/*
音频格式(离散类型)
chemical/*
化学数据集(离散 IETF 扩展类型)
image/*
图片格式(离散类型)
message/*
报文格式(复合类型)
model/*
三维模型格式(离散 IETF 扩展类型)
multipart/*
多部分对象集合(复合类型)
text/*
文本格式(离散类型)
video/*
视频电影格式(离散类型)
常见类型
HTML格式
text/html
普通文本
text/plain
JPEG
image/jpeg
GIF
image/gif
CSS
text/css
application/json
JS
application/javascript
JSONP
表单提交
multipart/formdata
URI
统一资源标识符
资源的标识符
URL
统一资源定位符
描述一台特定服务器上的某个资源的特定位置
请求
响应
GET
从服务器获取命名资源
POST
将客户端数据发送到一个服务器网关应用程序中
DELETE
从服务器中删除命名资源
HEAD
仅发送命名资源响应中的HTPP首部
PUT
将来自客户端的数据存储到一个命名的服务器资源中去
每个HTTP响应都携带3位数的状态码
100-199
信息
200-299
成功
300-399
重定向
400-499
客户端错误
500-599
服务端错误
常见状态码
100 Continue 收到请求的起始部分,客户端应该继续请求
101 Switching Protocals 切换协议
http通过101upgrade到websocket
200 OK 正确
201 Create 资源创建成功
301 Moved Permanently 永久移除
302 Found 暂时重定向,不带缓存
与状态码 301 类似,但这里的移除是临时的。客户端应该用 Location 首部给出的 URL 对资源进行临时定位
304 Not Modified 未修改
400 Bad request 坏请求
401 unauthorized 未授权
403 Forbidden 禁止访问
404 Not Found 无法找到资源
405 Method Not Allowed 请求的方法不被支持
408 request timeout 请求超时
409 Conflict 冲突
413 Request Entity too large 请求实体太大
500 internal server error 内部服务器错误
php开发中一般是代码出问题了
502 bad gateway 错误网关
php开发中一般是php进程已经退出,Nginx返回这个错误,一般与php-fpm.conf配置有关
PHP-fpm未启动
503 Service unavailable 服务不可用,过段时间就好啦
504 gateway timeout 网关超时
PHP开发中一般是Nginx超时配置问题,同时检查PHP代码是否存在长时间处理代码
网页通常需要获取多个对象,一个对象都是一个单独的HTTP事务
HTTP 报文都是纯文本
HTTP报文
起始行
首部字段
起始行后面有零个或多个首部字段
每个首部字段都包含一个名字和一个值,为了便于解析,两者之间用冒号(:)来分隔
首部以一个空行结束。添加一个首部字段和添加新行一样简单
主体
文本
二进制数据
应用层协议,下层是传输层
浏览器从URL解析出服务器的主机名
浏览器将服务器的主机名转换成IP地址
浏览器将端口号从URL解析出来
浏览器与服务器建立TCP连接
浏览器发送HTTP请求报文
服务器响应HTTP响应报文
关闭连接,浏览器显示文档
Telnet 程序可以将键盘连接到某个目标 TCP 端口,并将此 TCP 端口的输出回送到显示屏上
URL与资源
<scheme>://<user>:<password>@<host>:<port>/<path>;<params>?<query>#<frag>
将相对 URL 转换成绝对 URL
字符
ASCII
编码机制
为了避开安全字符集
转义
对不安全字符使用%两个表示ASCII码的十六进制数
字符限制
部分字符被保留
报文组成
请求报文格式
<method> <request-URL> <version><headers><entity-body>
响应报文格式
<version> <status> <reason-phrase><headers><entity-body>
请求行
GET、POST等
请求URL
HTTP/1.1
200,404等
原因短语
200 OK中OK就是原因短语
首部
首部最后用<CRLF>结尾
将长的首部行分为多行可以提高可读性,多出来的每行前面至少要有一个空格或制表符(tab)
通用首部
请求首部
响应首部
实体首部
扩展首部
安全方法(幂等)
服务器只返回首部
用于确认资源是否存在,修改,过期
非安全方法
其他不常用方法
OPTIONS
TRACE
GET vs POST
GET是幂等操作,POST不是
GET请求的资源能被缓存,POST不能
GET参数有长度限制(1M),POST没有
100 Continue
HTTP 客户端应用程序有一个实体的主体部分要发送给服务器,但希望在发送之前查看一下服务器是否会接受这个实体
客户端
客户端应用程序只有在避免向服务器发送一个服务器无法处理或使用的大实体时,才应该使用 100 Continue
发送了值为 100 Continue 的 Expect 首部的客户端不应该永远在那儿等待服务器发送 100 Continue 响应超时一定时间之后,客户端应该直接将实体发送出去
Expect: 100-continue
服务器
如果服务器收到了一条带有值为 100 Continue 的 Expect 首部的请求,它会用 100 Continue 响应或一条错误码来进行响应
如果服务器无法理解 Expect 首部的值,就应该以状态码 417 Expectation Failed 进行响应
代理
如果代理从客户端收到了一条带有 100 Continue 期望的请求,它需要做几件事情。如果代理知道下一跳服务器是 HTTP/1.1 兼容的,或者并不知道下一跳服务器与哪个版本兼容,它都应该将 Expect 首部放在请求中向下转发。如果它知道下一跳服务器只能与 HTTP/1.1 之前的版本兼容,就应该以 417 Expectation Failed 错误进行响应。
30x 重定向
301 永久重定向
http 升级到 https
带有缓存
302 临时重定向
不带缓存
303实现了302同样的功能
POST请求后被重定向后,用GET获取重定向资源
307替代302
POST请求后被重定向后,用POST获取重定向资源
通用的信息性首部
Date
报文构建日期
允许客户端和服务器指定与请求/响应连接有关的选项
Update
给出了发送端可能想要“升级”使用的新版本或协议
Via
显示了报文经过的中间节点(代理、网关)
通用缓存首部
Cache-Control
用于随报文传送缓存指示
Pragma
另一种随报文传送指示的方式,但并不专用于缓存
任何情况下 Cache-Control 的使用都优于 Pragma
请求的信息性首部
Host
给出了接收请求的服务器的主机名和端口号
Referer
提供了包含当前请求URI的文档的URL
User-Agent
将发起请求的应用程序名称告知服务器
Accept首部
Accept
客户端希望接受什么类型的数据
Accept-Charset
告诉服务器能够发送哪些字符集
Accept-Encoding
告诉服务器能够发送哪些编码方式
Accept-Language
告诉服务器能够发送哪些语言
条件请求首部
Expect
允许客户端列出某请求所要求的服务器行为
If-Match
如果实体标记与文档当前的实体标记相匹配,就获取这份文档
If-Modified-Since
除非在某个指定的日期之后资源被修改过,否则就限制这个请求
If-Range
允许对文档的某个范围进行条件请求
安全请求首部
Authorization
包含了客户端提供给服务器,以便对其自身进行认证的数据
Cookie
客户端用它向服务器传送一个令牌
代理请求首部
Proxy-Authorization
与Authorization首部相同,但这个首部是在与代理进行认证时使用的
Max-Forward
在通往源端服务器的路径上,将请求转发给其他代理或网关的最大次数
响应的信息性首部
服务器信息
Age
从最初创建开始,响应持续时间
协商首部
Accept-Ranges
对此资源来说,服务器可接受的范围类型
Vary
服务器查看的其他首部的列表,可能会使响应发生变化;也就是说,这是一个首部列表,服务器会根据这些首部的内容挑选出最适合的资源版本发送给客户端
安全响应首部
Set-Cookie
可以在客户端设置一个令牌
实体的信息性首部
Allow
列出了可以对此实体执行的请求方法
Location
告知客户端实体实际上位于何处
内容首部
Content-Type
这个主体的对象类型
Content-Encoding
对主体执行的任意编码方式
Content-Language
理解主体时最适宜使用的自然语言
Content-Length
主体的长度或尺寸
实体缓存首部
ETag
与此实体相关的实体标记
Expires
实体不再有效,要从原始的源端再次获取此实体的日期和时间
Last-Modified
这个实体最后一次被修改的日期和时间
连接管理
HTTP/1.1(以及 HTTP/1.0 的各种增强版本)允许 HTTP 设备在事务处理结束之后将 TCP 连接保持在打开状态,以便为未来的 HTTP 请求重用现存的连接
TCP连接
HTTP由TCP承载
TCP提供可靠数据管道
HTTP 要传送一条报文时,会以流的形式将报文数据的内容通过一条打开的 TCP 连接按序传输
TCP性能的考虑
与建立 TCP 连接,以及传输请求和响应报文的时间相比,事务处理时间可能是很短的。HTTP时延是由TCP时延构成的
HTTP 事务的时延
DNS解析
TCP建立连接
因特网传输请求报文,以及服务器处理请求报文都需要时间
Web 服务器会回送 HTTP 响应,这也需要花费时间
性能关注点
TCP握手
慢开始拥塞控制
数据聚集的Nagle算法
用于捎带确认的 TCP 延迟确认算法
TIME_WAIT 时延和端口耗尽
客户端 2MSL
HTTP连接处理
connection首部
HTTP 的 Connection 首部字段中有一个由逗号分隔的连接标签列表,这些标签为此连接指定了一些不会传播到其他连接中去的选项。
承载 3 种不同类型的标签
HTTP 首部字段名,列出了只与此连接有关的首部
任意标签值,用于描述此连接的非标准选项
值 close,说明操作完成之后需关闭这条持久连接。
串行事务处理时延
如果只对连接进行简单的管理,TCP 的性能时延可能会叠加起来
提高 HTTP 的连接性能
串行连接
并行连接
HTTP 允许客户端打开多条连接,并行地执行多个 HTTP 事务
浏览器确实使用了并行连接,但它们会将并行连接的总数限制为一个较小的值(通常是 4 个)。服务器可以随意关闭来自特定客户端的超量连接
利用子域名来突破限制
持久连接
HTTP/1.1(以及 HTTP/1.0 的各种增强版本)允许 HTTP 设备在事务处理结束之后将 TCP 连接保持在打开状态
HTTP1.0+ keep-alive
参数 timeout 是在 Keep-Alive 响应首部发送的。它估计了服务器希望将连接保持在活跃状态的时间。这并不是一个承诺值。
参数 max 是在 Keep-Alive 响应首部发送的。它估计了服务器还希望为多少个事务保持此连接的活跃状态。这并不是一个承诺值
如果客户端正在与一台 Web 服务器对话,客户端可以发送一个 Connection: Keep-Alive 首部来告知服务器它希望保持连接的活跃状态。如果服务器支持 keep-alive,就回送一个 Connection: Keep-Alive 首部,否则就不回送。
限制与规则
在 HTTP/1.0 中,keep-alive 并不是默认使用的
实体的主体部分必须有正确的 Content-Length
代理和网关必须执行 Connection 首部的规则
不应该与无法确定是否支持 Connection 首部的代理服务器建立 keep-alive 连接,以防止出现哑代理
哑代理
为避免此类代理通信问题的发生,现代的代理都绝不能转发 Connection 首部和所有名字出现在 Connection 值中的首部
还有几个不能作为 Connection 首部值列出,也不能被代理转发或作为缓存响应使用的首部。其中包括 Proxy-Authenticate、Proxy-Connection、Transfer-Encoding 和 Upgrade
Proxy-Connection
哑代理和服务器不识别这个首部
智能代理能够识别,并转成Connection
HTTP/1.1 持久连接
HTTP/1.1 逐渐停止了对 keep-alive 连接的支持,用一种名为持久连接(persistent connection)的改进型设计取代了它
HTTP/1.1 持久连接在默认情况下是激活的
用Connection: close关闭
发送了 Connection: close 请求首部之后,客户端就无法在那条连接上发送更多的请求了
如果客户端不想在连接上发送其他请求了,就应该在最后一条请求中发送一个 Connection: close 请求首部
实体主体部分的长度都和相应的 Content-Length 一致,或者是用分块传输编码方式编码的连接才能持久保持
HTTP/1.1 的代理必须能够分别管理与客户端和服务器的持久连接
HTTP/1.1 的代理服务器不应该与 HTTP/1.0 客户端建立持久连接
尽管服务器不应该试图在传输报文的过程中关闭连接,而且在关闭连接之前至少应该响应一条请求,但不管 Connection 首部取了什么值,HTTP/1.1 设备都可以在任意时刻关闭连接。
HTTP/1.1 应用程序必须能够从异步的关闭中恢复出来。只要不存在可能会累积起来的副作用,客户端都应该重试这条请求。
除非重复发起请求会产生副作用,否则如果在客户端收到整条响应之前连接关闭了,客户端就必须要重新发起请求。
一个用户客户端对任何服务器或代理最多只能维护两条持久连接,以防服务器过载
管道化连接
HTTP/1.1允许在持久连接中使用请求管道
接二连三的发出请求,无需等待响应
如果 HTTP 客户端无法确认连接是持久的,就不应该使用管道。
必须按照与请求相同的顺序回送 HTTP 响应。HTTP 报文中没有序列号标签,因此如果收到的响应失序了,就没办法将其与请求匹配起来了。
HTTP 客户端必须做好连接会在任意时刻关闭的准备
HTTP 客户端不应该用管道化的方式发送会产生副作用的请求(比如 POST)
被很多人关闭,因为存在很多问题
pipelining只能适用于http1.1,一般来说,支持http1.1的server都要求支持pipelining。
只有幂等的请求(GET,HEAD)能使用pipelining,非幂等请求比如POST不能使用,因为请求之间可能会存在先后依赖关系。
head of line blocking 线头阻塞 并没有完全得到解决,server的response还是要求依次返回,遵循FIFO(first in first out)原则。也就是说如果请求1的response没有回来,2,3,4,5的response也不会被送回来。
绝大部分的http代理服务器不支持pipelining。
和不支持pipelining的老服务器协商有问题。
可能会导致新的Front of queue blocking问题。
复用的连接
HTTP/1.1不支持,在2.0中被支持
关闭连接
『任意』解除连接
服务器永远都无法确定在它关闭“空闲”连接的那一刻,在线路那一头的客户端有没有数据要发送
如果出现这种情况,客户端就会在写入半截请求报文时发现出现了连接错误。
Content-Length 截尾操作
客户端或代理收到一条随连接关闭而结束的 HTTP 响应,且实际传输的实体长度与 Content-Length 并不匹配(或没有 Content-Length)时,接收端就应该质疑长度的正确性。
连接关闭容限,重试以及幂等性
幂等
如果一个事务,不管是执行一次还是很多次,得到的结果都相同,这个事务就是幂等的
要发送一条非幂等请求,就需要等待来自前一条请求的响应状态。
尽管用户 Agent 代理可能会让操作员来选择是否对请求进行重试,但一定不能自动重试非幂等方法或序列
大多数浏览器都会在重载一个缓存的 POST 响应时提供一个对话框,询问用户是否希望再次发起事务处理。
正常关闭连接
TCP全关闭和半关闭
close 全关闭
shutdown 半关闭
TCP关闭与重置
对方返回重置会使操作系统清空缓存区数据
正常关闭
实现正常关闭的应用程序首先应该关闭它们的输出信道,然后等待连接另一端的对等实体关闭它的输出信道。当两端都告诉对方它们不会再发送任何数据(比如关闭输出信道)之后,连接就会被完全关闭,而不会有重置的危险
web服务器
接受客户端连接
创建新连接
客户端主机识别
反向DNS
耗时
一般禁用
ident协议
TCP 113端口
有隐私风险
接受请求报文
解析请求行
读取以 CRLF 结尾的报文首部
检测到以 CRLF 结尾的、标识首部结束的空行
如果有的话(长度由 Content-Length 首部指定),读取请求主体
报文内部数据结构
连接的输入/输出处理结构
Web 服务器输入 / 输出结构
单线程
一次只处理一个请求
多进程/多线程
服务器根据需要创建进程/线程,或者预先pre-fork
一个请求分配一个进程/线程处理
对进程/线程有最大限制
复用IO结构
为了支持大量的连接,很多 Web 服务器都采用了复用结构
在复用结构中,要同时监视所有连接上的活动
当连接的状态发生变化时(比如,有数据可用,或出现错误时),就对那条连接进行少量的处理
处理结束之后,将连接返回到开放连接列表中,等待下一次状态变化
Linux系统提供的IO复用方式
复用多线程IO结构
利用CPU的多核心能力
处理请求
对资源映射及访问
docroot
服务器专门用来存放web资源的目录
动态内容资源的映射
构建响应
响应实体
主体内容
MIME类型
永久搬移 301
临时搬移 302
URL重写
服务器关联
服务器重定向请求到包含资源的另外服务器上
303
307
规范目录名称
发送响应
记录日志
背景
冗余的数据传输
带宽瓶颈
瞬间拥塞
传输时延
客户端发送请求,服务器响应304
命中率
缓存拓扑结构
私有缓存
公有缓存
处理步骤
保持副本新鲜
文档过期
在缓存文档过期之前,缓存可以以任意频率使用这些副本,而无需与服务器联系
过期日期和使用期
HTTP/1.0+
指定一个绝对的过期日期。如果过期日期已经过了,就说明文档不再新鲜了
Cache-Control:max-age
max-age 值定义了文档的最大使用期——从第一次生成文档到文档不再新鲜、无法使用为止,最大的合法生存时间(以秒为单位)
相对时间
服务器再验证
对于可能过期的资源要重新验证
不一定是说本地的缓存就一定过期了
如果再验证,内容发生变化,就获取新的数据
没有变化则缓存只需要获取新的首部,包括一个新的过期日期,并对缓存中的首部进行更新就行了
HTTP协议要求行为正确的缓存返回的内容
足够新鲜的缓存副本
与服务器确认过,确认其仍然新鲜的已缓存副本
服务再验证时,原始服务器出错,要返回一条错误报文
附有警告信息说明内容可能不正确的已缓存副本
条件方法验证
如果从指定日期之后文档被修改过了,就执行请求的方法。
可以与Last-Modified服务器响应首部配合使用
If-None-Match:实体标签再验证
服务器可以为文档提供特殊的标签(ETag),而不是将其与最近修改日期相匹配,这些标签就像序列号一样。如果已缓存标签与服务器文档中的标签有所不同,If-None-Match 首部就会执行所请求的方法
强弱验证器
强验证器
只要内容发生了变化,强验证器就会变化
弱验证器
内容发生少量变化,则服务器认为内容足够新鲜
服务器会用前缀“W/”来标识弱验证器
什么时候应该使用实体标签和最近修改日期
HTTP/1.1中,如果服务器回复了ETag,客户端必须使用If-None-Match
如果两者都提供,需要满足两者
缓存控制能力
服务器可以通过 HTTP 定义的几种方式来指定在文档过期之前可以将其缓存多长时间
附加一个 Cache-Control: no-store 首部到响应中去;附加一个 Cache-Control: no-cache 首部到响应中去;附加一个 Cache-Control: must-revalidate 首部到响应中去;附加一个 Cache-Control: max-age 首部到响应中去;附加一个 Expires 日期首部到响应中去;不附加过期信息,让缓存确定自己的过期日期。
no-Store 与 no-Cache 响应首部
标识为 no-store 的响应会禁止缓存对响应进行复制
标识为 no-cache 的响应实际上是可以存储在本地缓存区中的。只是在与原始服务器进行新鲜度再验证之前,缓存不能将其提供给客户端使用。
Pragma: no-cache 首部是为了兼容于 HTTP/1.0+
max-age 响应首部
Cache-Control: max-age 首部表示的是从服务器将文档传来之时起,可以认为此文档处于新鲜状态的秒数。
Expires 响应首部
不推荐使用
must-revalidate 响应首部
Cache-Control: must-revalidate 响应首部告诉缓存,在事先没有跟原始服务器进行再验证的情况下,不能提供这个对象的陈旧副本。
如果在缓存进行 must-revalidate 新鲜度检查时,原始服务器不可用,缓存就必须返回一条 504 Gateway Timeout 错误
试探性过期
如果响应中没有 Cache-Control: max-age 首部,也没有 Expires 首部,缓存可以计算出一个试探性最大使用期。
LM-Factor 算法
客户端的新鲜度限制
客户端可以用 Cache-Control 请求首部来强化或放松对过期时间的限制。
网关、隧道和中继
协议网关
资源网关
通用网关接口CGI
Web服务器可以用它来装载程序以响应对特定 URL 的 HTTP 请求
服务器扩展API
应用程序接口和Web服务
隧道
可以使用HTTP携带非HTTP协议的内容
HTTP+SSL
不同于HTTPS
仍然是HTTP明文传输SSL加密内容
中继
Web爬虫
HTTP发展
HTTP发展存在的4个问题
复杂性
可扩展性
传输依赖性
模块化及功能增强
分成三层
报文传输层
不考虑报文,专注于端点间报文的不透明传输
WebMUX协议
远程调用层
定义了请求响应的功能
Web应用层
提供了大部分的内容管理功能
报文传输
管道化和批量化传输
重用连接
在一个连接上复用多个报文流
对报文分段
远程操作调用
二进制协议
Web应用功能
Web 应用层的基本思想是提供与 HTTP/1.1 等价的功能和一些扩展接口,同时将其映射到一个可扩展的分布式对象框架中去。
WebMUX
可以在一个复用的 TCP 连接上并行地传输报文
可以对以不同速度产生和消耗的独立报文流进行高效的分组,并将其复用到一条或少数几条 TCP 连接上去
HTTP 2.0
HTTP/1.1缺陷
过于庞大
过多的可选项
未能充分利用TCP
传输大小和资源数量的需求日益增加
延迟
原因前面分析过,大部分来自DNS和TCP
线头阻塞(Head of line blocking)
服务器按序处理请求,前一个请求非常耗时,后面的请求就收到影响
HTTP 2.0 特性
复用
HTTP/1.1不支持,即使keep-alive上的TCP同时只能处理一个HTTP请求
二进制分帧
保留HTTP1.1语义方法状态码等
在应用层与传输层之间增加二进制分帧层
HTTP2.0将信息切分为更小的消息和帧,并采用二进制编码
帧Frame
数据通信的最小单位,用来承载特定类型的数据
每个帧都包含帧首部,其中会标识出当前帧所属的流
多个帧之间在连接中可以乱序发送,因为根据帧首部的流标识可以重新组装
解决了HTTP/1.1中管道化的乱序问题
消息Message
逻辑上HTTP消息,例如请求和响应,消息由一个或者多个帧组成
流Stream
存在于连接中的一个虚拟通道,可以承载双向消息,每个流都已一个唯一整数ID
流中每一帧的发送顺序非常重要,接收方会按照收到帧的顺序来进行处理
流有优先级
借助于PRIORITY帧
连接Connection
对应TCP连接
首部压缩
像Cookies这样的头部,信息很大
压缩算法容易收到Breach攻击,在安全一项中我们有提及
HPACK算法
维持一个字典
服务端推送
当一个客户端请求资源X,而服务器知道它很可能也需要资源Z的情况下,服务器可以在客户端发送请求前,主动将资源Z推送给客户端
重置
解决HTTP/1.1中一个含有确切Content-Length消息发出去后无法中断的问题
TCP中断可以解决,但是消耗太大
引入RST_STREAM帧
每个http2流都拥有自己的公示的流量窗口
对于每个流来说,两端都必须告诉对方自己还有足够的空间来处理新的数据,而在该窗口被扩大前,另一端只被允许发送这么多数据
只有数据帧会受到流量控制
cookie机制
承载用户信息的HTTP首部
cookie
会话cookie
持久cookie
服务端通过set-cookie
客户端每次请求会自动带上cookie
domain
path
secure
是否只有在使用 SSL 连接时才发送这个 cookie。
expiration
name
value
httponly
禁止js等操作cookie
基本认证机制
质询:WWW-Authenticate
服务器用 401 状态拒绝了请求
认证算法在返回的WWW-Authenticate首部中指明
认证区域也在该首部指明
实例:WWW-Authenticate: Basic realm=quoted-realm
认证:Authorization
查看『安全』节点
Base64用户名密码编码
摘要认证
密码不通过明文传输
单向摘要算法
MD5
SHA1
加入随机数防止重放攻击
由服务端产生随机数加入WWW-Authorization
握手协议
思考下,HTTP前端登录真的有必要加密么?
有必要,只能抵挡低端攻击和避免明文
尽快实现HTTPS
摘要计算
安全HTTP(HTTPS)
使用 HTTPS 时,所有的 HTTP 请求和响应数据在发送到网络之前,都要进行加密
HTTPS 在 HTTP 下面提供了一个传输级的密码安全层
SSL
TLS
SSL升级版本
解决的问题
窃听
篡改
冒充
数字加密
密码
对文本进行编码,使偷窥者无法识别的算法
密钥
改变密码行为的数字化参数
对称密钥加密
双方使用相同密钥加解密
不对称密钥加密
双方使用不相同密钥加解密
数字签名
用来验证报文未被伪造或篡改的校验和
数字证书
由一个可信的组织验证和签发的识别信息
将 HTTP 报文发送给 TCP 之前,先将其发送给了一个安全层,对其进行加密
HTTP 安全层是通过 SSL 及其现代替代协议 TLS 来实现的
用443端口
SSL握手
客户端发出加密通信的请求
支持的TLS协议版本
客户端产生的随机数用于对话密钥的生成
支持的非对称加密方法
支持的压缩方法
一台服务器只能有一个证书,所以虚拟主机比较麻烦
服务端返回
确认TLS协议版本
产生随机数用于对话密钥
确认接下来要用的非对称加密的方法
服务器证书
服务端有权威机构签名的数字证书,证书中带有公钥
窜改公钥会破坏数字证书,客户端验证证书有效性便可确认
就算窃取到证书后伪装成服务端与用户通信,没有私钥就无法解密出随机数n3,也就无法利用n1,n2,n3生成对话秘钥,没有对话秘钥也就无法解密用户发送的数据包。
客户端回应
验证证书
SSL不检查网站证书,由浏览器检查
服务端公钥
CA对服务端公钥的数字签名
拿浏览器内置的CA公钥解开数字签名并校验
生成pre-master key,用服务端的公钥加密
为什么要使用前面2个随机数和pre-master key?
为了防止pre-master key 被规律性猜出
同时,两个随机数是为了防止单随机数导致不够随机的发生
根据前面三个随机数计算生成本次会话所用的\"会话密钥\"
编码改变通知
结束握手,将前面所有信息hash,供服务端校验
服务端处理
结束握手,将前面所有信息hash,供客户端校验
对等加密过程
有了会话密钥后,双方采用对等加密
证书
RSA证书
RSA 证书可以用于 RSA 密钥交换(RSA 非对称加密)或 ECDHE 密钥交换(RSA 非对称签名)
ECC证书
ECC 证书只能用于 ECDHE 密钥交换(ECDSA 非对称签名)
256 位 ECC Key 在安全性上等同于 3072 位 RSA Key,并且算法运算速度更快
HTTPS优化
TLS握手优化
session复用(简化握手)
Session Identifier
客户端发起请求可以带上服务端能够识别的session id
分布式session cache
Session Ticket
用只有服务端知道的安全密钥加密过的会话信息,最终保存在浏览器端
False Start
抢跑,应用在第一个握手RTT(Server Hello Done)后发送数据
前提
使用支持前向安全性(Forward Secrecy)的加密算法保证数据安全
使用NPN/ALPN(应用层应用层协议协商),用来表明自己支持的 HTTP 协议(http/1.1 http/2.0)
证书验证
ECC减少证书大小
证书链
把中间证书也放进去,减少浏览器去获取中间证书的耗时
OCSP(在线证书状态协议) Stapling
服务端周期性去查询证书是否被吊销结果,将证书查询结果(CA已经加密)带给客户端,减少客户端查询
RSA异步代理计算
算法分离
代理计算
异步执行
对称加密优化
块式对称
AES-GCM
流式对称
ChaCha20-Poly1305
HSTS
HSTS的作用是强制客户端(如浏览器)使用HTTPS与服务器创建连接。服务器开启HSTS的方法是,当客户端通过HTTPS发出请求时,在服务器返回的超文本传输协议响应头中包含Strict-Transport-Security字段。非加密传输时设置的HSTS字段无效。
实体和编码
国际化
内容协商与转码
Websocket
一个Go实现的WebSocket服务(包含协议)
传输层详解
传输层向上层应用提供通信服务
通信部分的最高层
用户功能的最低层
主机才拥有传输层,网络核心中的路由器均工作在下三层
端对端通信:进程之间的通信
发送方多个应用进程都可以使用同一个传输层协议传输数据
分用
接收方的运输层在剥离报文的首部后能把数据正确的交付给进程
差错检查
面向连接:TCP
传输控制协议
TCP报文
面向无连接:UDP
用户数据报协议
UDP用户数据报
源端口
目的端口
用16位表示
最多能有65535
服务器端口号
系统端口(0-1023)
FTP 21
TELNET 23
SMTP 25
DNS 53
HTTP 80
HTTPS 443
登记端口(1024-49151)
客户端端口号(49152-65535)
短暂端口号
提供客户端暂时性使用,通信结束就被回收
UDP
面向报文传输协议
对应用层交付下来的数据追加首部后就交付给网络层,既不合并,也不拆分
无连接
减少延迟
首部开销少
只有8字节(64位)
实现简单
支持多对多通信
不提供可靠数据传输
尽力而为
丢失
非按序到达
不提供拥塞控制
应用可以更好的控制发送时间和速率
不提供流量控制
不提供延迟保障
不提供带宽保障
上层协议
SNMP
NFS
RIP
DHCP
用二元组表示一个socket
(目的IP地址,目的端口号)
首部结构
红色为『伪头部』(为了计算校验和),灰色是协议结构
源端口(16位)
目的端口(16位)
如果接收方发现端口不存在,则丢弃该包,并使用ICMP向发送方发送端口不可达
长度(16位)
UDP用户数据报的长度,其最小值是8字节
检验和(16位)
错误校验
发送方
计算校验和
需要添加12字节的伪首部
伪首部不上传也不下传
把首部和内容一起校验
接收方
计算所收到段的校验和
将其与校验和字段进行比较
流媒体
可以利用应用实现可靠数据传输
由程序员来实现
面向连接的传输协议
提供点对点,一对一的通信
提供可靠、按序的传输
全双工
双端都设置有缓存
面向字节流
TCP不保证收方应用收到的数据块和发送方发出的数据块具有对应大小的关系
发送方发送10个块,接收方可能收到4个数据块就交付给上层应用
拥塞控制
不提供时间/延迟保障
不提供最小带宽保障
SMTP
FTP
TELNET
端点
套接字Socket:端口号拼接到IP地址就是套接字
每一条TCP连接唯一地被通信两端的两个套接字所确定
同一个地址可以有不同的连接,同一个端口号也可以出现不同的TCP连接
Linux 中每个 TCP 连接最少占用多少内存?
用四元组表示socket
(源地址,源端口号,目的IP地址,目的端口号)
TCP首部最少20字节(160位),不能超过60字节(为什么不能超过60字节)
端口号(16位)
来源端口号
辨识发送连接端口
目的端口号
辨识接收连接端口
序列号(seq,32位长)
mod 2的32次方
字节流中每一字节都要按顺序编号
起始序号在建立连接设置
首部序号字段值为本报文段所发送的数据的第一个字节的序号
如果含有同步化flag(SYN),则此为最初的序列号
第一个数据比特的序列码为本序列号加一
如果没有同步化旗标(SYN),则此为第一个数据比特的序列码。
确认号(ack,32位长)
期望收到的数据的开始序列号。也即已经收到的数据的字节长度加1。
报头长度/数据偏移(4位,4字节长的字为计算单位)
以4字节为单位计算出的数据段开始地址的偏移值
实际就是指出首部长度
TCP首部不能超过60字节的原因:4位能够表示15,15*4=60字节
保留位
占6位
占时用不到,置0
标志位
URG
标明紧急指针字段有效,该报文段中有紧急数据需要优先传输
SYN
为1表示这是连接请求或是连接接受请求,用于创建连接和使顺序号同步
ACK
为1表示确认号字段有效
TCP规定,在建立连接后所有的报文段都必须把ACK设置为1
PSH
推送,要求接收方尽快的将报文交付给应用
很少使用
RST
复位,TCP连接中出现严重差错,必须释放连接
FIN
为1表示发送方没有数据要传输了,要求释放连接。
窗口(WIN,16位)
表示从确认号开始,本报文的接受方可以接收的字节数,即接收窗口大小。用于流量控制。
动态变化
校验和(16位)
对整个的TCP报文段,包括TCP头部和TCP数据,以16位字进行计算所得。这是一个强制性的字段
紧急指针(16位)
本报文段中的紧急数据的最后一个字节的序号。
除了紧急数据,其他都是普通数据
选项字段(最多40位)
MSS:最大报文段长度
数据段的最大长度
尽量大,但是不要让IP层传输进行分片
默认是536字节
窗口扩大选项
3字节
时间戳选项
10字节
时间戳值
4字节
时间戳回送回答字段
用来计算RTT
处理TCP序号超过2的32次方问题
防止序号绕回 PAWS
状态编码
可靠数据传输原理
停止等待协议
每发送一个分组就停止发送,等待对方确认对方确认收到后再发送下一个分组
自动重传请求 ARQ
无差错情况
出现差错的情况
超时重传
超时计数器
注意点
发送者必须保存发送的分组副本,直到被确认
分组和确认分组都需要编号
超时时间应该要比平均往返时间长
确认丢失
确认迟到
信道利用率
停滞等待协议耗时
当RTT远大于Td,效率非常低
连续ARQ协议(停止等待协议的连续版本)
连续ARQ协议工作原理
累计确认
接收方收到几个分组后,对按序到达的最后一个分组发送确认
Go-Back-N问题:丢失中间一个包会造成该包后面几个包重传
滑动窗口协议
TCP可靠传输的实现
以字节为单位的滑动窗口
以字节为单位
发送窗口表示:在没有收到B的确认下,A可以连续把窗口内的数据都发送出去
发送窗口不能超过对方的接受窗口值
滑动窗口演示
后沿
没有收到新的确认,不动
收到确认,前移
前沿
没有收到确认,对方通知的窗口大小不变,不动
收到确认,对方通知的窗口大小变小,窗口正好不动
收到确认,窗口变小,向后伸缩
强烈不建议这么做
可用窗口
允许发送但是尚未发送
窗口满了,停止发送
超时重试
TCP缓存、进程写入缓存和窗口的关系
缓存空间和序号空间都是有限的,循环使用
发送缓存
发送应用程序传送给发送方TCP准备发送的数据
TCP已发送出但尚未收到确认的数据
接收缓存
按序到达但是未被应用程序接受的数据
未按序到达的数据
注意四点
发送窗口不总是和接收窗口一样大
TCP协议没有规定对于不按序到达的数据怎么处理
TCP协议要求接收方有累计确认功能,确认推迟不能超过0.5s
TCP是全双工的
超时重传时间的选择
TCP采用了一种自适应算法
记录一个报文段发出时间和确认时间,两者之差就是报文段的往返时间RTT
TCP保留了一个加权平均往返时间 RTTs
平滑的往返时间
0≤α≤1,建议取1/8
超时重传时间RTO略大于RTTs
计算RTTs,不算重传报文段,并把RTO增大一倍
选择确认 SACK
在建立连接的时候协商SACK
解决不按序到达的确认问题
避免已经到达的数据重传
TCP流量控制
利用滑动窗口实现流量控制
解决非零窗口通知丢失问题
当收到零窗口通知,设置持续计时器,超时则发送探测报文段(1字节)
TCP传输效率
发送时机
缓存中数据达到MSS,则组装TCP包发出
应用程序主动要求发出,即使用推送操作
计时器时限到了,把当前已有的缓存发出
Nagle算法
先把第一个字节发出,缓存后面的数据,第一个字节确认,发出后面的数据
缓存数据达到一半或报文段的最大长度,立即发出
糊涂窗口综合征
应用程序每次读一个字节
发送端每次补发一个字节
两种解决方法
让接收方等待一段时间,使得缓存有足够的空间容纳一个最长报文段,发出确认
等到缓存有一半的空间,发出确认
TCP拥塞控制
防止过多的数据注入到网络,这样可以使网络中的路由器和链路不过载
区别于流量控制,拥塞控制是一个全局性的过程,是对点对点的通信量控制
开环控制
设计网络时考虑拥塞因素,一旦运行,不再更改
闭环控制
基于反馈环路
措施
监测网络系统何时何处发生拥塞
由于缺少缓存空间被丢弃的分组百分比
平均队列长度
超时重传的分组数
平均分组时延
分组时延的标准差
把拥塞发生的信息传送到可采取行动的地方
调整网络系统的运行以解决出现的问题
基于窗口的拥塞控制
发送方维持一个拥塞窗口
发送方让自己的发送窗口等会拥塞窗口
拥塞窗口动态变化
判断网络出现拥塞的依据是出现了超时
拥塞控制的四种算法
慢开始
开始发送数据时,有小到大逐渐增大发送窗口
开始cwnd不超过
拥塞避免
加法增大
让拥塞窗口缓慢增大,每经过一个往返时间RTT就把发送方的拥塞窗口+1
将窗口增长变为线性
3个ACK对同一报文确认,进入快恢复
快重传
接收方接收到数据立即确认,即使是失序的报文段
避免让发送方将丢包误认为是网络拥塞
快恢复
乘法减小
拥塞控制演示
ssthresh 不小于 2*SMSS (发送端最大报文段大小)
拥塞控制流程图
主动队列管理 AQM
尾部丢失策略
路由器队列到达一定的值就对后面的数据进行丢弃
随机早期检测
超过最大门限,丢弃
小于最小门限,进入队列
介于两者中间,按概率随机丢弃
TCP连接管理
TCP连接建立
三次握手的目的是同步连接双方的序列号和确认号并交换 TCP 窗口大小信息
三次握手
中间一步确认也可以分成两个包
最后A还需要确认是为了避免无效的连接请求报文段让B重复建立连接,浪费资源
TCP连接释放
四次挥手
保活计时器
保活机制是由一个保活计时器实现的。当计时器被激发,连接一段将发送一个保活探测报文,另一端接收报文的同时会发送一个ACK作为响应。
保活功能在默认情况下是关闭的,TCP连接的任何一端都可以请求打开这一功能
保活探测报文为一个空报文段(或1个字节),序列号等于对方主机发送的ACK报文的最大序列号减1。
接收方会丢弃,然后发送一个ACK
在出现短暂的网络错误的时候,保活机制会使一个好的连接断开
保活机制会占用不必要的带宽
TCP有限状态机
TCP优化
keepalive
减少握手
开启TCP Fast Open
启用TCP_NODELAY
如果client启用Nagle,并且server端启用了delay ack会有什么后果呢?
现代网络带宽较大
更换拥塞控制算法,例如Google的BBR
KCP
KCP是一个快速可靠协议,能以比 TCP浪费10%-20%的带宽的代价,换取平均延迟降低 30%-40%,且最大延迟降低三倍的传输效果。
RTO不翻倍
TCP超时计算是RTOx2,这样连续丢三次包就变成RTOx8了,十分恐怖,而KCP启动快速模式后不x2,只是x1.5(实验证明1.5这个值相对比较好),提高了传输速度。
快速重传
选择性重传
TCP丢包时会全部重传从丢的那个包开始以后的数据,KCP是选择性重传,只重传真正丢失的数据包。
非延迟ACK
TCP为了充分利用带宽,延迟发送ACK(NODELAY都没用),这样超时计算会算出较大 RTT时间,延长了丢包时的判断过程。KCP的ACK是否延迟发送可以调节。
ACK+UNA
ARQ模型响应有两种,UNA(此编号前所有包已收到,如TCP)和ACK(该编号包已收到),光用UNA将导致全部重传,光用ACK则丢失成本太高,以往协议都是二选其一,而 KCP协议中,除去单独的 ACK包外,所有包都有UNA信息。
非退让流控
KCP正常模式同TCP一样使用公平退让法则,即发送窗口大小由:发送缓存大小、接收端剩余接收缓存大小、丢包退让及慢启动这四要素决定。但传送及时性要求很高的小数据时,可选择通过配置跳过后两步,仅用前两项来控制发送频率。以牺牲部分公平性及带宽利用率之代价,换取了开着BT都能流畅传输的效果。
攻击
XSS(跨站脚本攻击)
目的
为了盗取存储在客户端的 cookie 或者其他网站用于识别客户端身份的敏感信息
存储型XSS
黑客输入带有恶意代码的用户输入内容,网站将其展示到页面上
反射型XSS
在URL插入代码,用户打开URL进行攻击
可能场景
标签中直接输出内容
<div>$var</div>
在HTML属性中输出
<div name=\"$var\"></div>
在<script>标签中输出
在事件中输出
在CSS中输出
在地址中输出
防御
cookie设置为HttpOnly
输入检查
富文本
事件是全部禁止的
标签和属性使用白名单
输出检查
就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令
请查看『数据库节点』
彩虹表攻击
重放攻击
撞库攻击
中间人攻击
会话劫持
CSRF(跨站请求伪造)
钓鱼网站通过表单等方式诱导用户发起针对保存有用户登录状态的其他网站敏感请求的攻击
使用JSON API
AJAX 有跨域限制
禁用CORS
或者使用幂等HTTP方法
检查referer头部
保证GET操作的幂等
不能用GET替代POST
避免使用POST
不要复写方法
拒绝不安全的旧浏览器
CSRF Token
服务器发送给客户端一个Token
客户端提交表单带上这个Token
如果Token不合法,拒绝请求
注意
不能使用AJAX来获取Token
对CSRF加盐处理,避免Breach攻击
把salt和token发给客户端,不需要加密
服务端通过验证 secret + salt = token
Breach攻击
如果服务器通过HTTPS+gzip多次发送相同或者相似的响应,攻击者就可以猜测响应的内容(使得HTTPS完全无用)。
DDOS
SYN Flood 攻击
利用三次握手的最后一步:客户端发送ACK确认
给服务器发了一个SYN后,就下线了,于是服务器需要默认等63s才会断开连接,这样,攻击者就可以把服务器的syn连接的队列耗尽,让正常的连接请求不能处理。
对策
修改内核的半连接队列大小
Linux下可以使用tcp_syncookies
当SYN队列满了后,TCP会通过源地址端口、目标地址端口和时间戳打造出一个特别的Sequence Number发回去(又叫cookie),如果是攻击者则不会有响应,如果是正常连接,则会把这个 SYN Cookie发回来,然后服务端可以通过cookie建连接(即使你不在SYN队列中)。
半连接队列保存着第一次握手的信息,例如四元组,窗口,序号等信息,现在利用cookies来保存
缺点就是消耗了CPU资源,携带的信息也非常有限
密码学
信息摘要算法
加密过程不需要密钥,并且经过加密的数据无法被解密
常见算法
不安全,能被破解
SHA
对称加密
编码时使用的密钥值和解码时一样
典型算法
DES、Triple-DES、RC2 和 RC4
枚举攻击猜解密钥
所以密钥要尽量长
密钥首先要通过网络传输给对方
非对称加密
产生公钥和私钥
公钥可以分发给对方
私钥必须自己保存
RSA
得到其中一个都无法推出另外一个
使用其中一个密钥把明文加密后所得的密文,只能用相对应的另一个密钥才能解密得到原本的明文
加密过程
服务端保存私钥,将公钥分发给要通信的客户端
客户端使用该公钥加密后发给服务端,只有服务端私钥能解开
服务端用私钥加密内容,发给客户端,客户端用公钥解开,由于私钥只有服务端有,可以确认消息来自服务端
公钥可以通过数字证书认证机构签授的电子证书形式公布
将内容Hash后,用公钥/密钥加密生成数字签名
保证内容没有被篡改
CA 证书中心
对公钥的可靠性做验证
CA公钥不存在伪造问题,可以查询
用CA的私钥对被认证人的公钥做加密得到
服务端向客户端发送数据时需要附上密文,数字签名,数字证书
客户端用CA公钥对数字证书解密得到服务端的公钥,再执行解密过程
认证
HTTP Basic Authorization(不安全,HTTP1.0产物)
base64Encode(username + \":\" + password)
一定要在https去实现,否则明文下可以被破解
HTTP Header中添加:Authorization: Basic [加密后的字符串]
OAuth2.0
授权码模式
用户请求第三方,第三方重定向到认证服务器,并携带state、redirectUrl和clientId
用户认证授权,认证成功后跳转到redirectUrl并携带code(有过期时间)和之前的state
利用access_token请求资源服务器
一定要用HTTPS,存在中间人攻击,state参数
简化模式
去掉Code阶段
由客户端存储access_token
JWT(json web token)
这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息
一个JWT实际上就是一个字符串
无状态的HTTP中使用
头部
例子:{ \"alg\": \"HS256\
使用Base64Url加密后组成JWT的第一部分
载荷
包含Claims
一种实体的表述
保留
iss
sub
aud
exp
iat
公共
私有
签名
HMACSHA256( base64UrlEncode(header) + \".\
作用是校验来源和内容是否有被篡改
在认证过程中,用户成功登录后返回JWT token给客户端存放在cookies里
代替传统使用session并返回cookies
客户端想要访问权限资源的时候,在请求头部中携带Token
Authorization: Bearer <token>
减少了请求数据库和认证的消耗
跨站脚本攻击不会发生,因为没有cookies
解决了跨域问题
JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
JWT 不加密的情况下,不能将秘密数据写入 JWT。
JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
解耦
最终一致性
广播
错峰与流控
系统可用性降低
系统复杂度提升
一致性无法保证
gearman
ssdb
rabbitmq
RabbitQM是一款基于AMQP协议的消息中间件
Producer
生产消息的角色
Exchange
消息交换机
Queue
Customer
消息消费者
AMQP协议
AMQP是一个异步消息传递所使用的应用层协议规范
Broker
生产消费
Round-robin 任务分发
ack确认
持久
QOS防止mq分发过多的任务
kafka
是什么?
LinkedIn用于日志处理的分布式消息队列
支持离线和在线日志处理
对消息保存时根据Topic进行归类
Producer-Consumer模型
集群由多个Kafka实例组成,每个实例(Server)成为Broker
依赖Zookeeper来保证系统可用性,保存一些集群的meta信息
日志收集
日志聚合
从服务器上收集日志,存在到集中的位置(HDFS和文件服务器)
抽象
忽略掉文件细节,抽象成一个个日志或事件的消息流,处理过程延迟更低,更容易支持多数据源和分布式数据处理
对比产品:Scribe和Apache Flume
行为跟踪
跟踪用户浏览页面、搜索、和其他行为
以发布-订阅模式实时记录到对应的Topic里
订阅者可以进一步实时处理,或者实时监控,或者放到Hadoop离线数据仓库里处理
持久性日志(commit log)
可以为外部持久性日志分布式系统提供服务
这种日志可以在节点间备份数据
为故障节点数据恢复提供重新同步机制
日志压缩功能
Apache BookKeeper
Topic
特指Kafka处理的消息源(Feeds of message)的不同分类
Partition
Topic物理上的分组,一个Topic可以分为多个partition
每个Partition都是一个有序的队列
每条消息都会分配一个有序的ID(offset)
消息,通信的基本单位
每个producer可以向一个topic(主题)发布一些消息
Producers
消息和数据生产者
向Kafka的一个topic发布消息的过程叫做producers
Consumers
消息和数据消费者
订阅Topic并处理其发布的消息的过程叫做consumers
缓存代理,Kafka集群中的一台或多台服务器统称为broker
设计原理
初衷
成为一个统一的信息收集平台
实时收集反馈的信息
能够支撑大量的数据
具有良好的容错能力
Topic/Log
一个Topic可以认为是一类消息,每个Topic将被分成多个Partition(区),每个Partition是在存储层面是append log文件
发布到此Partition的消息都会被直接追加到log文件的尾部
每条消息在文件中的位置称为offset(偏移量),partition是以文件的形式存储在文件系统中
Logs文件根据broker的配置要求,保留一定的时间后删除
储存策略
以Topic进行消息管理,每个Topic包含多个Partition,每个Partition对应一个逻辑的log,由多个segment组成
每个segment存储多条消息,消息ID由其逻辑位置决定,即从消息ID可以直接定位到消息的存储位置,避免ID到位置的额外映射
broker收到发布消息往对应partition的最后一个segment上添加该消息
每个partition在内存中对应一个index,记录每个segment的第一条消息偏移
发布者发到某个 topic 的 消息会被均匀的分布到多个 part 上(随机或根据用户指定的回调函数进行分布)
broker 收到发布消息往对应 part 的最后一个 segment 上添加该消息,当某个 segment 上的消息条数达到配置值或消息发布时间超过阈值时,segment 上的消息会被 flush 到磁盘
只有 flush 到磁盘上的消息订阅者才能订阅到
segment 达到一定的大小后将不会再往该 segment 写数据,broker 会创建新的 segment
broker可以持久化数据,无内存压力
consumer采用pull,producer采用push
Zookeeper协调流程
管理broker与consumer的动态加入与离开
出发负载均衡:当broker或consumer加入或离开时会触发负载均衡算法,使得一个consumer group内的多个consumer的订阅负载均衡
维护消费关系以及每个partition的消费信息
Zookeeper的细节
每个broker启动后会在zookeeper上注册一个临时的broker registry,包含broker的IP和端口号,所存储的topics和partitions信息
每个consumer启动后会在zookeeper上注册一个临时的sonsumer registry,包含consumer所属的consumer group以及订阅的topics
每个 consumer group 关 联一个临时的 owner registry 和一个持久的 offset registry。对于被订阅的每个 partition 包含一个 owner registry,内容为订阅这个 partition 的 consumer id;同时包含一个 offset registry,内容为上一次订阅的 offset。
ActiveMQ
早期MQ
目前比较少用
单机吞吐量
万级
RabbitMQ
Erlang实现
社区比较稳定
RocketMQ
阿里出品
Java语言实现
十万级,支持高吞吐
Kafka
支持顺序消费
如何保证消息不被重复消费
消息幂等
消费者确认
如何解决丢数据的问题
生产者丢数据
发送消息后由mq做ack确认
MQ丢数据
确保队列和消息持久化
先持久化数据,再提供给消费者
消费者丢数据
消费完成后消费者做ack确认
监听死信队列
如何保证消息的顺序性
将需要保持先后顺序的消息放到同一个消息队列中。后续顺序保证交给消费者去保证,比如只用一个消费者去消费该队列。
Kafka 能够保障同个partition的顺序
业务应用把要发送的消息给协调器
协调器在他自己的DB中记录下这条消息
协调器返回对应的msgid
业务应用自行本地事务更新DB
业务系统如果本地事务执行成功,告诉协调器这条消息Commit,如果本地事务执行失败,则rollback
协调器更新自己的数据库,标记消息已经Commit/Rollback
协调器对于成功submit的消息,开始往MQ进行投递,等待ack,如果一段时间没有收到ack,会继续投递该消息
MQ将消息投递给接收方,接收方执行事务更新DB
接收方应用给协调器做ack,告诉这条消息消费成功.如果长时间没有收到ACK,协调器重投到MQ,这里需要接受方做幂等实现
如果协调器没有收到Commit/rollback,则会询问业务应用消息的状态,是要Commit还是Rollback
Cache Aside
由调用者自己管理缓存
Read/Write Through
Read Throught
在查询操作中更新缓存,由缓存服务维护过程而不是应用
Write Throught
当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由Cache自己更新数据库
Write Back
先更新缓存,异步批量同步到DB
缓存雪崩
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩
解决办法
对同个时间点失效的缓存加上一个随机数,不要再同一时间点全部失效
缓存击穿
对于某些key设置了过期时间,但是其是热点数据,如果某个key失效,可能大量的请求打过来,缓存未命中,然后去数据库访问,此时数据库访问量会急剧增加
加分布式锁、或者进程锁
对这部分热点数据采取到期自动刷新的策略,而不是到期自动淘汰
查询的数据在数据库和缓存都是NULL,但是仍然造成查库操作
对于返回为NULL的依然缓存
对访问数据库数据的边界做好限制,比如数据库数据只有0-100,对于101的请求直接返回NULL
增减字段导致的序列化问题
增减字段导致新旧缓存不一致,有可能导致序列化异常
对缓存增加版本号
如果序列化失败,或者获取对象成功但是字段null,删除缓存重新构建
Memcache
未支持持久化
集群方案成熟
支持SQL
有持久化
性能较差
集群化支持较差
单进程多路复用,性能优越
HTML
HTML5新特性
基础知识
CSS和文档
可替换元素:如img等
不可替换元素
块级元素:block-level
行内元素:inline-level
CSS属性:display的主要值
block:
inline(默认)
list-item
link标记:
利用link外接外部样式表
type:text/css
herf:url
media:all
一个文件可以多个link标记。
style元素
<style tye="text/css"></style>
@import指令
与link类似,加载外部样式表
可以在URL之后加入样式的媒体:@import url(ss.css) all;
@import 只能存在于样式表的开头
css的注释: /* 注释 */
选择器
每个CSS规则
h1{color:red;background:#000;}
h1:就是选择器
{}中间的就是声明块
color:red 就是声明,声明块可以多个声明
color为属性
reg为值
如果一个属性可以多个值p{ font:medium helvetica;}
通配选择器:*{color:#fff;}
结合选择器和声明的分组
类选择器和ID选择器
类选择器:
p.warning{font-weight:bold}
多类选择器:p.warning.hel{font-weight:bold}
ID选择器:
#para {font-weight:bold}
使用ID有什么不同?
唯一
属性选择器:
简单属性选择器:h1[class]{font-size:12px;}
根据具体属性值选择:a[href="www.css.com"]{font-weight:bold;}
根据部分属性值选择:
[foo^="bar"]表示一bar开头的所有元素
[foo$="bar"]表示一bar结尾的所有元素
[foo^="bar"]表示包含有bar的所有元素
特定属性的选择类型:img[src|="mm"]{border:1px solid gray;}
父子关系
后代选择器
h1 em{color:#333;} 作为h1元素后代任何em元素
两个元素之间的层次间隔可以是无限的
选择子元素
h1>em{color:#333;}
p>a>strong的父子结构,写成p>strong是不行的
选择相邻的兄弟元素
h1+p{color:#333;}
伪类和伪元素
伪类选择器
连接伪类
:link
:visited
动态伪类
:focus
:hover
:active
伪类顺序
1:link
2:visited
3:hover
4:active
第一个子元素
first-child
根据语言选择
:lang(cn){color:#333;}
综合伪类
a:link:hover{color:red;}
伪元素选择器
设置首字母样式
p:first-letter{color:red;}
设置第一行的样式
p:first-line{color:red;}
:first-letter 和:first-line的限制
不能应用于超链接行内元素
所有的伪元素都必须放在该伪元素的最后面。p:first-line em{}不合法
:before和:after
body:after{content:"The End."}
结构和层叠
特殊性
如果选择器多个属性发生冲突,最高特殊性的声明才会胜出
声明特殊性
通配选择器特殊性:选择器中包括通配选择器和其他选择器,该选择器的特殊性不会因为通配选择器的出现而改变。
重要性
!important标志
标志总是放在声明最后,分号之前,否则无效。
样式不仅会应用到指定元素,还会应用到它的后代元素。
有些属性不能继承
border
层叠
按权重和来源排序
两个规则应用到同一元素,有!important标志胜出
按特殊性排序
p#ii{color:red:}大于p{color:#333;}
按顺序排序
如果权重、特殊性、来源都一致,最后只能排在文件后面的获胜
值和单位
整数
实数
百分比
颜色
命名颜色:gray、black等
RGB颜色:
十六进RGB颜色 color:#EF0000;
长度单位
绝对长度单位
in英尺
cm厘米
mm毫米
pt点
pc 派卡
相对长度单位
em和ex单位
像素长度px
如何取舍
相对路径
绝对路径
关键字:有时一些值需要用关键词来描述
CSS2单位
角度值
度:deg
梯度:grad
弧度:rad
时间值
毫秒 ms
秒 s
频率值
字体
指定字体:font-family:
字体加粗:font-weight:bold
字体大小:font-size
绝对大小:
相对大小:
百分比和大小
p{font-size:12px}
em{font-size:120%;}
字体风格:font-style
斜体:italic
倾斜:obliqique
字体变形:font-variant
small-caps:小型大写
font属性,一种缩写方式。
行高:line-height:
文本属性
缩进和水平对齐
text-indent:3em;段落首行缩进3em
text-align水平对齐
垂直对齐
行高:line-height
行高和继承
垂直对齐文本:vertical-align
基线对齐:baseline
上标:super 下标:sub
底端对齐:bottom
居中对齐:middle
百分比:vertical-align:50%;
长度对齐:vertical-align:5px;
字间隔和字母间隔
word-spacing:3px;
letter-spacing:3px;
文本转换:text-transform
uppercase:全大写
lowercase:全小写
capitalize:每个单词首字大写
文本装饰:text-decoration
underline 下划线
line-through 中间贯穿线
blink会闪烁文本
none会关闭所有的装饰
文本阴影text-shadow:green 5px 0.5em;
处理空白符:white-space
特殊用法:nowrap 禁止文本换行
基本视觉格式化
布局
盒子模型
FlexBox
less
sass/scss
响应式布局
比较好用的CSS框架
JS框架
AngularJS
ReactJS
Vuejs
Vue
vue-router
Vuex
webpack
gulp
grunt
fis
前端技术
CORS(跨域资源共享)
流量器的同源策略
如果两个 URL 的 protocol、port (如果有指定的话)和 host 都相同的话,则这两个 URL 是同源。
设置document.domain=my.com
websocket
图片Ping
图像Ping跨域请求技术是使用<img>标签
一个网页可以从任何网页中加载图像,不用担心跨域不跨域。
callback方法作为参数传给服务端
服务端返回callback({\"hello\": \"world\"})
HTTP CORS
理解CORS
Ajax & Comet
Ajax由浏览器发起
短轮询
Comet让浏览器与服务器全双工通信
长轮询
服务器保持连接,直到有数据就返回(for循环)
长连接
汇编语言
语言处理器
解释器
JIT
字节码直接转为机器语言
编译器结构
重点内容标记
0 条评论
回复 删除
下一页
职业:本科
作者其他创作: