陆续施工中......面试了两家,发现自己没准备好,每天学习一点,为拿到心仪的offer做好准备。同时,也想给在面试中的小伙伴一点帮助。
心得体会
根据投递岗位和工作年限准备
面试题是根据岗位要求和工作年限来提的。面试官的提问会和你的工作经验有关,或是比较基础的知识。按照自身实际情况进行准备就好,不用难为自己。
学习简历涉及到的深层次知识
面试准备要把简历上涉及的技术的深层次知识充分了解,避免一问三不知。要有意识的准备这一点。平常工作时,只是用技术,但学习不深。面试准备也是看别人总结的知识点。我应该根据自己的简历,构建自己的知识点,不然知识无穷无尽,永远也学不完。
面试题
测试
测试之前要问清楚功能要求,如是否允许原地修改。
- 功能测试:正常输入
- 极端测试:[], 只有一个元素的数组,已经有序的数组,特别大的数组
- 非法输入:输入错误参数
算法
准备简单算法,动手敲代码之前先理清思路,考虑时间和空间复杂度,输入值和输出值,用到的变量。把这些和面试官沟通完毕,得到认可后敲代码。注意代码的命名、简练无重复、完整性和鲁棒性。以下是题目的简单分类。无标注的为leetcode的题目。分类标准是数据结构和解题方法,两个分类有交叉。
哈希
O(1)时间找到元素。用于判重,统计次数
物理结构-数组
数组适用于字符集小的情况,如只要小写英文字母,只有ASCII码,字符集大用对象。注意将字符转码charCodeAt后作为数组下标
- 程序员面试金典01.01判定字符是否唯一
物理结构-对象
- 查询元素的出现次数。剑指03. 数组中重复的数字。
-
- 两数之和
- 判断是否重复 程序员面试金典02.01. 移除链表重复节点
字符串
- 字符串操作经常从尾部开始,因为尾部有足够的空间,不用担心会覆盖原有的字符。 剑指试题05 替换空格
链表
注意在循环中的指针移动
删除节点
- 程序员面试金典02.01.移除链表重复节点。每次判断pre.next是否需要删除,那么删除时,pre就是前置节点。
分割链表
- 程序员面试金典02.04. 分割链表。注意思考的完整性,分割得到的before链表如果为空怎么办,after链表的尾节点的next置null
链表求和
- 程序员面试金典02.05 链表求和,注意两个链表长度不同,还有维护进位
快慢指针
递归
递归占用空间:每次递归占用空间*递归次数
- 程序员面试金典02.02. 返回倒数第 k 个节点的递归解法有助于理解递归
树
区分概念
- 二叉查找树(二叉搜索树、二叉排序树)。左子树所有节点的值小于等于根节点,右子树所有节点的值大于根节点。二叉搜索树的每一个节点都在一个区间当中,可以通过递归传递区间端点值,检查每个节点是否在这个区间中来判断树是否是二叉搜索树。程序员面试金典04.05. 合法二叉搜索树
- 平衡二叉树。所有节点的左子树和右子树的深度差不超过1。
- 满二叉树
- 完全二叉树:满二叉树最下层去掉几个叶子节点,并且剩余叶子都集中在该层最左边的位置。
- 镜像…
- 中序遍历,(二叉查找树),先序遍历
- 单词查找树(trie):每个节点存储字符
位操作
- 乘2相当于左移一位
- 获取某一位:将1左移i位,得到00010000形式,与原数位与,其他位都得0,i位是0则结果为0,i位为1则结果不等于0.
- 置位:将1左移i位,得到00010000形式,与原数位或,其他位不受影响,i位会变成1
- 清0:与置位相反。将1左移i位,得到00010000形式,再取反,得到11101111,与原数位与,其他位不受影响,i位会变成0
- 更新:先清0,再置1。写0就不用置1。 以上思路起源于0001000形式。
数学
- 素数
动态规划
45分钟内要解决很难,所以面试考察较少。原理是用缓存避免重复计算。空间换时间。
双指针
双指针方法常用于数组,链表,注意题目的原地,排序关键字。
- 链表中的快慢指针。876.找中点。141.判断是否有 环。142.找环起点。
- 链表中有一个指针先走。19.删除链表的倒数第N个节点。
- 数组中的快慢指针。常见于原地修改数组。26. 删除排序数组中的重复项,注意排序数组的重复元素紧挨的。27.移除元素。283.移动零。
- 数组中的左右指针。167.两数之和-输入有序数组, 注意输入数组是递增的。二分查找。
排序
先问清楚时间和空间复杂度的要求,数据量等题意。
快速排序
每趟归位一个元素。平均logn趟,平均时间复杂度O(nlogn)。最坏情况下数组已经有序,每次都取第一个元素,导致划分极不均匀,时间复杂度为O(n平方)。空间复杂度取决于递归树高度,平均O(logn),最坏退化为冒泡排序,树是斜树,树高O(n)
归并排序
分治法的典型应用。先划分再归并。每次归并用时O(n),归并次数logn。总时间复杂度O(nlogn)
基数排序
利用整数位数有限的特点。先按个位对数字分组,再按十位分组。每次分组用时O(n),整数k位。总时间复杂度O(kn).
next greater number
要求下一个更大的数,就要知道后面元素的信息,所以对输入数组从后往前扫。比我小的都出栈,然后当前元素进栈,从而得到递减栈。对于当前元素,栈顶就是next greater number。当前元素进栈后等待下一轮的比较。
时间优化
字符串拼接时间、空间复杂度都是O(n平方)。 可以模拟java的stringBuffer,用数组不断push,存储结果,最后转为字符串。程序员面试金典01.06. 字符串压缩
网络
响应码
- 301 永久重定向
- 302 临时重定向
- 304 未修改,使用浏览器缓存
- 100 配置
- 200 成功
即时通信技术
即时通信指服务器可以即时地将数据更新反映到客户端。 轮询。客户端每隔一定时间去请求服务器,服务器不管是否有数据更新都会响应。客户端收到响应,比对上次数据看是否更新。没有更新时,请求无效,浪费资源。而且数据更新可能存在延迟。
var xhr = new XMLHttpRequest();
setInterval(function(){
xhr.open('GET','/user');
xhr.onreadystatechange = function(){
};
xhr.send();
},1000)
长轮询。服务器收到请求后,保持住连接。在数据更新时再响应,断开连接。客户端收到响应后再次发出请求。
function ajax(){
xhr.onreadystatechange = function(){
ajax();
};
}
轮询和长轮询,由浏览器请求服务器,服务器被动响应,所以都会有高并发问题。
长连接 实现方式iframe流。在页面里嵌入一个隐蔵iframe,将这个隐蔵iframe的src属性设为对一个长连接的请求或是采用xhr请求,服务器端就能源源不断地往客户端输入数据。进度栏都会显示加载没有完成。
websocket。建立连接后,服务器和客户端地位平等,可以相互发送数据,不存在请求和响应的区别。全双工。
// 向后端发送一个websocket连接请求
let ws = new WebSocket('ws://127.0.0.1:5000/vote');
ws.send(uid)
ws.onmessage = function (event) {
let data = JSON.parse(event.data);
}
websocket 和 socket有什么不同?
websocket是应用层协议,socket编程接口。websocket在七层,tcp在四层。 websocket 多个客户端连接同一个服务器,两个客户端之间的消息需要通过服务器转发。 socket可以之间通过p2p进行两端的交互,不需要通过第三方转发。tcp本身就是全双工的
websocket握手过程:
- 1、HTTP连接建立
- 2、浏览器通过http向服务器发请求 协议变更为websocket
- 3、服务器http响应 ,变更允许。之后按websocket协议通信
- 4、数据通过tcp进行传输
websocket 和 http的区别?
相同点:建立在tcp之上 不同点:http单向,服务器只能被动等待客户端强求数据。无状态的,需要携带身份信息。 websocket全双工,websocket依赖http来握手。连接建立后,避免了http的无状态性。
TCP三次握手的意义
客户端和服务器相互告知起始序列号,保证报文的有序。 如果二次握手,连接就建立了,导致之前无效请求建立连接。而且,服务器消息的起始序列号没有得到确认,导致TCP不可靠。
TCP四次挥手的原因
TCP是全双工的。主动方发起断开连接的请求只表示我不会发送数据,但仍可以接收数据。被动方可能还有数据要发,所以先发ACK,数据发送完毕后,再发起断开连接的请求表示我也不会再发送数据了。
框架
MVVM
Model-View-ViewModel。ViewModel通过双向数据绑定将View和Model联系起来。 实现包括compile模板编译,observer数据劫持,watcher观察者和Dep依赖订阅器。
- compile来编译模板,包括编译文本,编译指令,把更新函数添加到watch的update方法中。
- observer改写get,set,收集依赖(watcher)加入Dep,监听数据变化。触发Dep的notify.
- Dep是observer的一部分,存储watcher,数据更新时通知watch调用update
- watcher接收observer的通知,执行complie中的更新方法。watcher是observer和complie的桥梁,一方面把自身加入Dep依赖订阅器,与observer建立联系,另一方面自身的update方法,会触发compile的更新方法。
vue响应式原理
响应式的意思是输入改变时,输出随着改变。 首先自己实现一个简单的响应式。推荐看视频
let price = 10;
let num = 2;
let total = total = price * num; // 20
num = 3;
console.log(total); // 期待为30
要实现这样的效果,只要在输入改变时,再次执行得到输出值的函数就可以。
let storage = [];
let target = function(){
total = price * num;
}
function record(){
storage.push(target);
}
function replay(){
storage.forEach(run=>run());
}
record();
target();
console.log(total);//20
num = 3;
replay();
console.log(total);//30
用数组storge存储得到输出值的函数,第一次调用前纪录这个函数,输入改变时,再次调用storge中存放的函数。
class Dep{
constructor(){
this.subscribers = [];
}
depend(){
if(target!==null && !this.subscribers.includes(target))
this.subscribers.push(target);
}
notify(){
this.subscribers.forEach(sub => sub());
}
}
let dep = new Dep();
dep.depend();
target();
console.log(total);
num = 3;
dep.notify();
console.log(total);
类的写法,dep.depend()纪录函数,有includes检测,所以不会重复添加。dep.notify()再次调用。
let data = {
price: 10,
num: 2,
}
let target = null;
class Dep{
constructor(){
this.subscribers = [];
}
depend(){
if(target!==null && !this.subscribers.includes(target))
this.subscribers.push(target);
}
notify(){
this.subscribers.forEach(sub => sub());
}
}
Object.keys(data).forEach(key => {
const dep = new Dep();
let internalValue = data[key];
Object.defineProperty(data, key, {
get(){
console.log(`get ${internalValue}`);
dep.depend();
return internalValue;
},
set(newVal){
console.log(`set ${newVal}`);
internalValue = newVal;
dep.notify();
}
})
})
function watcher(func){
target = func;
target();
target = null;
}
watcher(()=>{
data.total = data.price * data.num;
});
console.log(data.total);
data.num = 3;
console.log(data.total);
watcher将函数赋给target,并执行target,这样可以得到第一次的输出值。遍历data的属性,用Object.defineProperty为每个属性改写getter和setter,劫持数据的获取和改动,get时纪录函数,set时设置新值并重新执行。
现在理解vue中的响应式。这部分内容参考自 深入浅出Vue响应式原理 ,加入了一点我自己的理解。vue中的响应式要求源数据改变时,用到数据的地方自动更新。比如模板,计算属性。要实现这个功能需要
- 监听数据变比
- 知道哪些地方用到原数据
- 数据变化时,通知那些地方更新
前两点可以用数据劫持,Object.defineProperty改写getter和setter。用到原数据的地方肯定会获取值、调用get,所以get可以知道有哪些地方用到源数据。set可以监听到数据的改变。
用到源数据的地方就是依赖源数据的地方,知道哪些地方用到原数据就是收集依赖。收集依赖后找个地方存储依赖。引入订阅者Dep类。
class Dep {
constructor () {
this.subs = [];
}
addSub (sub) {
if(sub && !this.subs.includes(sub))//不会重复添加
this.subs.push(sub);
}
notify () {
this.subs.forEach((sub) => {
sub.update();
})
}
}
依赖可能有很多,引入观察者Watcher来管理依赖。每个依赖都是一个watcher实例。 Dep中存储watcher实例。数据变化时,调用Dep的notify方法,由Dep通知所有的watcher更新数据,重新计算输出值,更新视图。所有脏活累活都是watcher干的,Dep只需要存储依赖,通知更新。使用的是观察者模型。
class Watcher {
constructor(obj, key, cb) {
// 将 Dep.target 指向自己
Dep.target = this
this.cb = cb
this.obj = obj
this.key = key
this.value = obj[key]
Dep.target = null
}
update() {
// 获得新值
this.value = this.obj[this.key]
// 定义一个 cb 函数,这个函数用来模拟视图更新
this.cb(this.value)
}
}
function observe (obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
function defineReactive (obj, key, value) {
observe(value) // 递归子属性
let dp = new Dep(); //新增
Object.defineProperty(obj, key, {
get: function reactiveGetter () {
// 将 Watcher 添加到订阅
if (Dep.target === null) {
new Watcher(obj, key, cb);
dp.addSub(Dep.target) // 新增
}
return value;
},
set: function reactiveSetter (newVal) {
observe(newVal) //如果赋值是一个对象,也要递归子属性
if (newVal !== value) {
value = newVal;
// 执行 watcher 的 update 方法
dp.notify() //新增
}
}
})
}
}
class Vue {
constructor(options) {
this._data = options.data;
observer(this._data);
}
}
每个data中的属性都有一个目标者dep,get时将依赖watcher加入订阅,set时设置新值,由dep通知watcher去update,获得新值(通过get),触发视图更新。
当传入一个js对象作为vue 的data选项时,vue通过observer遍历这个对象,对每个属性调用Object.defineProperty(),改写get和set方法,从而劫持数据获取和改动。访问数据会调用get,get中收集依赖加入Dep。数据变化时更新值,由dep通知watcher,watcher拿到新值,触发视图更新。
数据双向绑定
其实是响应式原理和MVVM的总结。响应式:Object.defineProperty()+观察者模式。MVVM:observer,compile和watcher。
vue生命周期
记住四个关键字:创建、挂载、更新、销毁。
- beforeCreate:实例创建之前,data、methods不可用
- created: 实例创建完毕,完成了data observer,事件登记。还未挂载,所以el不可用。
- beforeMount:el值为生成的html,但数据未在dom上渲染
- Mounted:数据在dom上渲染
- beforeUpdate:数据更新,视图并未更新
- updated:DOM重新渲染完毕,可以执行依赖于DOM的操作
- beforeDestroy:实例销毁前,实例仍然可用
- destroyed:实例已销毁。子实例被销毁,事件监听移除 详见vue2.0-vue实例的生命周期
vue非父子组件通信
eventBus
首先new一个vue实例并导出
import Vue from 'vue';
export default new Vue();
传递消息的组件中emit,发布
Bus.$emit('getTarget', event.target);
接收消息的组件中on监听,订阅
Bus.$on('getTarget', target => {
console.log(target);
});
eventBus 对于两个组件来说是全局的vue实例,然后分别调用这个实例的事件触发emit和事件监听on来实现通信。
vuex
当你搞不清楚组件之间的值是怎么传递的时候说明你该用vuex了。Vuex从入门到入门这篇文章讲的很好。简单总结如下:
state可以理解为前端的数据库,存储共享的数据。取数据用getter,存数据用mutation,存数据之前的数据处理用action, 将所有这些综合在一起就是store。
action在methods中用,getter在computed中用。mutation通过action来dispatch.
vue单页应用路由实现原理
参考vue:路由实现原理 路由的意思是路应该怎么走,怎么才能到达目的地,或者给出一条路,想知道这条路通向哪里。对于http请求来说就是由url得到网页。前端的单页应用可以在不发出http请求的情况下更新页面,这要靠前端路由。实现方式包括
- window.location.hash
- history 在H5中的新增方法 可以通过在vue-router中的mode参数传入不同值来控制不同的实现方式。
window.location.hash是url中#和#之后的部分,这部分是用来指导浏览器行为的,不会发送给服务器,所以可以利用这一点,实现不发请求就更改地址,然后更改页面的功能。通过改hash来更新页面就是前端路由的实现了。hash的每次改变会被记录到浏览器历史记录中去。
H5为history提供新方法pushState()和replaceState().当调用它们修改历史记录时,虽然url变了,但不会立即请求,只是触发popstate事件,所以可以监听popstate。
pushState可以修改url,只要与当前url同源即可,hash只能修改#部分。 pushState设置url可以与当前url相同,仍然可用加入历史记录栈中,hash设置的新值必须与当前不一样才能加入历史记录。
history模式也有不足。单页应用的理想场景是,仅在初次进入时加载index.html,之后的网络请求都通过ajax,不会通过url来请求页面。但碰到用户在地址栏键入回车的情况。hash不会修改url,所以对请求无影响。history模式修改了url,而这个url在后端无对应页面,会返回404。可以在后端对所以这种路由情况返回index.html,其余错误url返回404。
虚拟dom不一定快
- 原生DOM操作与框架的封装操作 任何框架封装都不可能比原生DOM操作更快,因为框架封装DOM操作需要应对上层所有Api,具有普适性。任何框架的封装操作,都可以找到手动优化方法。但在实际开发中,不可能针对每一处做手动优化。框架提供的是在不用做手动优化的前提下,提供过的去的性能。这是可维护性与性能的平衡。 就算vue使用了虚拟dom,将一些改动先同步到虚拟对象上,然后去改动真实DOM。这其中,去改动真实DOM还是使用了原生的api去操作DOM,还是会不可避免的去reflow整个页面,如果不能把这些更新操作打包起来集中去更新真实DOM,那其实完全散失了虚拟DOM的作用性,反而变得更加冗余。
这时候框架的价值就体现出来了,vue中,如果在同一次事件循环中如果观察到有多个数据变化,vue会开启一个异步更新队列,并缓冲在同一事件循环中发生的所有数据改变。然后在下一个的事件循环‘tick’中,vue刷新队列并执行实际工作。这样就可以批量的去更新多次数据变化到虚拟dom对象中,diff差异,同步到页面中的真实dom里。
设计模式
发布订阅模式
用公众号来理解,公众号为发布者,订阅者是用户。用户订阅公众号后,公众号有新消息就会发布给用户。当然用户也可以取消订阅。下面是发布订阅模式的简单实现。 参考js实现发布订阅模式 PubSub需要实现三个功能,订阅,发布和取消订阅。
class PubSub{
constructor(){
this.handleBars = {}; // 保存监听事件
}
subscribe(eventName, handle){
if(!this.handleBars.hasOwnProperty(eventName)){
this.handleBars[eventName] = [];
}
if(typeof handle == 'function'){
this.handleBars[eventName].push(handle);
}else {
throw new Error('need callback')
}
}
publish(eventName, ...args){
if(this.handleBars.hasOwnProperty(eventName)){
this.handleBars[eventName].forEach(handle =>{
handle.apply(null, args);
})
}
}
unsubscribe(eventName, handle){
if(this.handleBars.hasOwnProperty(eventName)){
this.handleBars[eventName].forEach((item, index) =>{
if(item === handle){
this.handleBars[eventName].splice(index, 1);
}
})
}
}
}
let sub = new PubSub();
function func1(type){
console.log(`${type} func1`);
}
function func2(type){
console.log(`${type} func2`);
}
function func3(type, data){
console.log(`${type} func3 data ${data}`);
}
sub.subscribe('ready', func1);
sub.subscribe('ready', func2);
sub.subscribe('complete', func3);
setTimeout(()=>{
sub.unsubscribe('ready', func1);
console.log(sub.handleBars)
sub.publish('ready', 'ready');
sub.publish('complete', 'complete', 123);
},1000)
PubSub类维护一个存储各种事件回调的handleBars对象。每次订阅,如果这个对象没有该属性,初始化该属性为数组。之后向这个数组添加回调。以下是handleBars存储的值。发布时,执行该事件对应的回调。取消订阅就是从handleBars中删除事件对应的回调。订阅、发布和取消订阅对应 存储、执行和删除。
{ready: Array[1], complete: Array[1]}
complete: Array[1]
0: ƒ func3()
ready: Array[1]
0: ƒ func2()
订阅时,传入要订阅的事件类型和回调函数。取消订阅类似;
sub.subscribe('ready', func1);
sub.unsubscribe('ready', func1);
发布事件,传参。第一个参数是事件类型,第一个参数是传递数据。
sub.publish('ready', 'ready');
观察者模式
class Observer{
constructor(fn){
this.update = fn;
}
}
class Subject{
constructor(){
this.observers = [];
}
addObserver(observer){
if(!this.observers.includes(Observer)){
this.observers.push(observer);
}
}
removeObserver(observer){
this.observers.forEach((item, index) => {
if(item === observer){
this.observers.splice(index, 1);
}
})
}
notify(){
this.observers.forEach(item => {
item.update();
})
}
}
let subject = new Subject();
function func(){
console.log('update')
}
let observer1 = new Observer(func);
subject.addObserver(observer1);
subject.notify();
subject.removeObserver(observer1);
subject.notify();
console.log(subject.observers); //[]
目标者存储观察者。观察者模式和发布/订阅模式的区别:
- 目标者知道有哪些观察者,发布者不知道也不关心订阅者的存在。
- 事件发生时由目标者通知观察者,观察者进行update()。发布者不会通知订阅者,而由调度中心(handleBars)通知订阅者。 *发布/订阅模式如vue响应式实现中的依赖收集和更新
行为面试
提问
项目组和美国同事是如何合作的?如何沟通? 有机会去美国培训? 组内几个人,如何分工? 现在做什么项目?
系统设计
答题过程:先问清除系统的功能,限制条件,先给出大框架,再给出细节。
可扩展性
性能问题:单个用户缓慢 扩展性问题:对于单个用户服务较快,但在高负载(多用户,大数据量)下缓慢 可扩展性:增加资源可以成比例的提高性能。或者为提高可靠性引入冗余,增加的资源不会影响性能。
垂直扩展
花钱买更好的服务器,但是是有限的。一钱不够,二现在的技术只能提供当前的设备。
水平扩展
与其买一台顶配设备,不如多买几台一般的设备。
延迟与吞吐量
- 延迟:执行操作所用的时间
- 吞吐量:单位时间内执行操作的数量
- 我们的目标是在可接受的延迟内,达到最大的吞吐量
一致性和可用性
- 一致性:读操作可以返回最新的写操作的结果
- 可用性:在响应时间内返回结果
- 分区容错性:网络故障导致网络发生分区时,仍能保证一致性或可用性
根据CAP理论,三者必须牺牲一个,才能保证另外两个。分布式系统肯定会有很多节点,而网络是不可靠的,网络节点发生故障必须考虑,所以必须保证分区容错性。
A向server1,server2写x后,A与server2之间发生网络故障。如要保证可用性,B,C读到的数据是不一致的。如要保证一致性,则B,C需要等待一段时间,在等待期间不能拿到结果。
需要注意的是,牺牲一致性牺牲的是实时一致性,但可以保证数据最终达到一致状态。
一致性模式
- 弱一致性 可能读到新写入的数据,也可能不能。常见于网络电话,视频聊天,实时多人游戏。会有短暂的不同步或几秒延迟。如果在通话中丢失了几秒,重新连接后,不会听到这几秒的语音。
- 最终一致性 在一段时间后可以读到新写入的数据。数据不是实时更新,是异步更新的,比如email,DNS。在追求可用性的系统中,经常使用最终一致性
- 强一致性 可以立即读到新的数据,数据被同步复制。常见于文件系统,关系型数据库。
故障切换
为了可用性,为了避免单点故障,需要冗余。根据冗余的功能不同,故障切换时的操作也不同。
- 工作到备用的切换(主从切换) 备用服务器不工作。工作服务器发心跳信号到备用服务器,心跳中断时,备用服务器切换成工作服务器的IP并提供服务。
- 双工作切换(主主切换) 两台服务器都工作,在他们之间分散负载。切换快。
- 故障切换的缺点 要实现故障切换要额外的硬件。在数据未写入到备用服务器时,工作服务器宕机,数据会丢失。
DNS
- 查浏览器缓存,未命中进行下一步
- 操作系统缓存,host文件,未命中进行下一步
- 路由器缓存,未命中进行下一步
- 本地域名服务器缓存,未命中进行下一步
- 本地域名服务器向根域名服务器请求,根域名服务器返回com.顶级域名服务器的地址。
- 本地域名服务器向顶级域名服务器请求,顶级域名服务器返回baidu.com域名服务器的地址
- 本地域名服务器向baidu.com域名服务器请求,二级域名服务器查到www主机对应的IP地址,返回
- 本地域名服务器缓存结果,返回给客户端
- 客户端缓存,用IP发起请求
CDN
将视频,图片,html,css,js等静态资源缓存到更近的地方,使得用户可以共享资源,缩短响应时间。 网游加速器则是架设高带宽机房,用两端高速路代替一段土路。
- CDN推送。服务器主动将内容推送到CDN,缓解目标服务器的压力,缺点是存在延迟。
- CDN拉取。第一次访问时,从服务器拉取内容,返回,缓存。
反向代理
正向代理隐藏客户端,如某上网工具。 反向代理隐藏服务器,将请求转发给服务器。好处多多:
- 缓存,提供CDN功能
- 安全,用户只知道反向代理的IP
- 可扩展性,用户只知道反向代理的IP,内部服务器可以增加,减少,修改配置
- 内部相对安全,所以可以在这加密,解密。内部服务器不需要进行这样的耗时操作。
负载均衡器
分担单个繁重工作或多个并发请求。 负载均衡器可以隐藏内部服务器。DNS只知道负载均衡器的IP,内部服务器的私有IP外部并不知道。
如果每台服务器都存储相同内容,会存在数据同步问题。可以将资源分类存储。如图片服务器,视频服务器,HTML服务器,这样修改的总发生在同一台服务器,而且这种服务器,如图片服务器可以做相应的优化。另一种方式是内部服务器不存储与用户相关的信息,而把这些内容存储在一个公用的数据库中。
- 四层交换。根据IP地址和端口号转发请求,TCP连接在客户端和服务器之间建立,需要修改报文的目标IP地址。
- 七层交换。根据应用层报文内容选择服务器。所以必须代理,此时负载均衡器与客户端,服务器与负载均衡器之间都会建立TCP连接。
如何实现负载均衡
- DNS返回IP地址,DNS可以用round robin轮询调度,以循环的方式返回IP地址。但可能有个服务器一直分到负载量大的用户。
- load balancer也可以用round robin轮询调度。如果每台服务器的资源一样,这回break session,导致用户重新登录,购物车不一致,因为第二次访问的是另一台服务器。解决方案一:有一台专门的session server,其余所有server去session server取session。解决方案二,load balancer存储用户访问的服务器的ID,对应的cookie,用户下一次来知道去哪一台服务器。
- 负载均衡是为了实现可扩展性和冗余
- 需要在以下三个层次平衡负载,外部请求到web server,web server到平台层,平台层到数据库
平台层
在web server和数据库之间添加平台层,将业务功能从web server分离出,在修改API只需要修改平台层。
RAID
磁盘坏了怎么办。磁盘阵列。RAID1方案提供镜像磁盘做备份,工作磁盘坏了,镜像磁盘接替服务,再插入一个磁盘作为新的镜像磁盘,在过程中服务不会中断。
缓存
缓存便于读,不便于写。写的时候,如缓存一堆HTML文件,修改字体, 修改很多文件。
- 对磁盘的缓存。RAM中缓存文件对应的磁盘地址。第一次访问未命中,访问磁盘慢,将地址记录在哈希表中,下次先查哈希表。时间长哈希表会越来越大,所以需要删除记录,比如用FIFO
- 可以在多层次进行缓存。浏览器,服务器,数据库,CDN,内存缓存
- 内存缓存,如redis,memcached.数据在ram中,比磁盘快。由于内存小,需要使用最近最少未使用算法淘汰数据。
缓存策略
- 缓存模式。缓存不和存储器直接交互。应用执行以下操作:
从缓存中查数据,未命中则查询存储器,查到数据后存到缓存,将数据返回给客户端。存在缓存过期问题,可以设置TTL
- 直写模式。应用把缓存作为主要的数据存储读写缓存。读很快。应用修改数据写到缓存,缓存同步的把数据写入数据库。返回结果。同步写导致操作很慢。
- 回写。数据从cache异步地写入数据库
解决单点故障
工作-备份 双工作 异地备份,一个数据中心故障,通过DNS返回另一个数据中心的IP
数据库
关系型数据库
如sql.用表来存储数据项的。关系型数据库要求ACID,原子操作,强一致性。
- 实现有主从复制:读写master数据库,读slave,可以负载均衡和备份。
主库复制将写入复制到多个从库中,树状的从库将数据复制到更多从库。主库离线,系统可以以只读方式运行。
- 主主复制
两个主库都负责读写。主库存在一致性问题,或者存在同步写延迟
- 两者共同的缺点:复制。主库在将新写入的数据复制到其他节点前挂掉,则数据丢失。读的从库越多,写入代价越高。
- 联合。按功能对数据库进行划分,如产品数据库,用户数据库。可以并行写入,提高负载能力。减少复制延迟。
- 分片
将数据分散到多个数据库。每个数据库只是数据的一个子集。优点与联合相同,减少读写压力,减少复制,提高缓存命中率。
NoSQL非关系型数据库
强调可用性高于一致性,所以是最终一致性。无法实现ACID。包括键值类型数据库,文档类型数据库,图数据库。
结构化数据(用户名,密码,地址),需要JOIN用sql.非结构化,数据量大,不用join用NoSql.如社交数据,埋点数据,日志,购物车(临时数据)。
服务器架构
load balancer连接所有服务器,为避免单点故障,实际应该还有一台。
任务队列
客户端发起耗时操作,不阻塞客户端,把任务放入任务队列,由一个worker异步地进行处理。期间,客户端可以返回一些结果看似任务已经完成。如发推文,推文立即出现在当前账户,但其他人需要等待一些时间才能看到推文。
高并发处理方法
- html静态化
- 各级缓存
- CDN
- 负载均衡
- sql数据库集群,主从模式
- 图片服务器。将图片与页面分离,可以避免服务器因为图片问题崩溃。而且也可以正对图片服务器做针对性优化。
高并发处理可以从客户端和服务端两个层面来处理,前端要做的是请求能不发给服务器就不发,后端策略是分而治之,提高单个请求的处理速度。
前端
- 缓存:5M的localStrong,强缓存和协商缓存
- 合并请求
- 压缩资源,减少流量
- 异步请求,分批获取数据
- 节流,5秒只请求一次
后端
- CDN
- 根据业务拆分到不同的平台服务器
- 负载均衡器,集群,分布式架构
- 专门的图片、视频等静态服务器
- html静态化
数据库
数据库集群,进行读写分离
redis
内存中的数据结构存储系统,所以可以用作缓存,数据库,消息队列。支持键到5种数据类型的映射。
前端异常监控
参考 Tencent CDC(cdc.tencent.com/2018/09/13/…
前端异常监控分四步:前端采集异常上传到服务器,服务器存储异常,机器分析或人工可视化分析,告警或预警。
错误原因
代码错、网络慢、系统错(内存不足)
异常采集
采什么?who did what get which exception in which environment.
- 用户信息:用户状态,权限,在哪个终端登录。
- 行为信息:操作,上传数据,后端返回数据,时间,页面
- 异常信息:异常级别,用户操作的DOM节点
- 环境信息:网络环境,设备型号,客户端版本,API接口版本
使用 requestId 唯一标识每个请求,请求会涉及到请求报文,数据库操作,涉及到的缓存操作,服务请求,响应报文。
异常捕获
全局捕获便于管理,单点捕获作为补充。 window.onerror全局捕获,try…catch单点捕获
异常录制
异常发生时,异常根源可能很远。所以要记录用户的每一个操作。
异常级别
紧急性,重要性区分
上报方案
前端存储,IndexedDB浏览器数据库,类似于noSql的键值对存储 上报方案:
- 即时上报
- 批量上报
- 区块上报,异常相关操作,去除在批量上报中重复的。区块上报比即时上报稍慢
- 用户主动上报,可以慢点上传
压缩方案:gzip
异常存储
后端有专门的日志服务器。接入层负责负责对信息进行甄别和处理后转入消息队列。
异常分析
用户纬度,时间纬度(异常前后),性能纬度,环境纬度,场景回溯。
异常修复
前端代码经过压缩后发布,上传的stack信息需要还原成源码以定位异常。将sourcemap上传到日志服务器,用sourcemap对stack信息进行解码。这就要求sourcemap必须与生产环境代码的版本对应