正文
博主原本以爲,《分佈式之數據庫和緩存雙寫一致性方案解析》,一文已經十分清晰。然而這一兩天,有人在微信上私聊我,以爲應該要採用javascript
先刪緩存,再更新數據庫,再刪緩存
這一方案做爲緩存更新策略,而不是先更新數據庫,再刪緩存。而且搬出了兩篇大佬的文章,《Cache Aside Pattern》,《緩存與數據庫不一致,咋辦?》,但願博主能加以說明。由於問的人太多了,因此纔有了這篇文章的誕生。php
正文
在開始這篇文章以前,咱們先本身思考一下如下兩個更新策略html
方案一
(1)刪緩存
(2)更數據庫
(3)刪緩存前端
方案二
(1)更數據庫
(2)刪緩存vue
你們看下面的文章前,本身先思考一下,方案一的步驟(1)有沒有存在的必要?
先上一個結論:方案二存在的缺點,方案一所有存在,且方案一比方案二多一個步驟,因此應該選方案二。java
下面,針對《Cache Aside Pattern》,《緩存與數據庫不一致,咋辦?》這兩篇文章提出的論點,提出小小的質疑。這兩篇文章認爲方案二不行的緣由,主要有如下兩點
(1)方案二在步驟(2),出現刪緩存失敗的狀況下,會出現數據不一致的情形,以下圖所示
(2)方案二存在下面的主從同步,致使cache不一致問題,以下圖所示
大體流程就是,線程A寫,線程B讀,會有如下流程出現
(1)緩存恰好失效
(2)線程A寫入master數據庫,slave還沒同步
(3)線程B發現緩存失效,去slave讀到舊值
(4)線程A刪除緩存
(5)線程B把舊值放入緩存node
然而你們發現了麼,這兩篇文章提出的反對意見,在該文做者本身所提出的方案一里頭也是存在的?
(1)針對刪緩存失敗問題
方案一的步驟(3)也會可能出現刪除緩存失敗問題,但是做者沒有加以詳細說明。
(2)針對數據不一致問題
線程A寫,線程B讀,會有如下流程出現
(1)線程A刪除緩存
(2)線程A寫入master數據庫,slave還沒同步
(3)線程B發現緩存失效,去slave讀到舊值
(4)線程A刪除緩存
(5)線程B把舊值放入緩存mysql
綜上所述,咱們應該選擇方案二,而不是方案一。方案二存在的缺點,方案一所有存在,且方案一步驟上多了一步,增長了不穩定因素。react
總結
該文章只是糾正了一下目前流傳的觀點的正確性,並無針對任何人。技術的世界,只論技術。ios
前言
本篇文章適合前端架構師,或者進階的前端開發人員;我在面試vmware前端架構師的時候,被問到關於callback,promise,generator,async-await的問題。
首先咱們回顧一下javascript異步的發展歷程。
ES6 之前:
回調函數(callback);nodejs express 中經常使用,ajax中經常使用。
ES6:
promise對象; nodejs最先有bluebird promise的雛形,axios中經常使用。
generator函數;nodejs koa框架使用率很高。
ES7:
async/await語法; 當前最經常使用的異步語法,nodejs koa2 徹底使用該語法。
回調函數callback
回調函數實際就是一個參數;將一個函數當作參數傳到另外一個函數裏,當那個函數執行完後,再執行傳進去的這個函數;這個過程就叫作回調。
回調字面也好理解,就是先處理本體函數,再處理回調的函數,舉個例子,方便你們理解。
function A(callback){ console.log("我是主體函數"); callback(); } function B(){ console.log("我是回調函數"); } A(B); /*輸出結果 我是主體函數 我是回調函數 */
上面的例子很好理解,首先執行主體函數A,打印結果:我是主題函數;而後執行回調函數callback 也就是B,打印結果:我是回調函數。
promise對象
promise 對象用於一個異步操做的最終完成(或最終失敗)及其結果的表示。
簡單地說就是處理一個異步請求。咱們常常會作些斷言,若是我贏了你就嫁給我,若是輸了我就嫁給你之類的斷言。這就是promise的中文含義:斷言,一個成功,一個失敗。
舉個例子,方便你們理解:
promise構造函數的參數是一個函數,咱們把它稱爲處理器函數,處理器函數接收兩個函數reslove和reject做爲其參數,當異步操做順利執行則執行reslove函數, 當異步操做中發生異常時,則執行reject函數。經過resolve傳入得的值,能夠在then方法中獲取到,經過reject傳入的值能夠在chatch方法中獲取到。
由於then和catch都返回一個相同的promise對象,因此能夠進行鏈式調用。
function readFileByPromise("a.txt"){ //顯示返回一個promise對象 return new Promise((resolve,reject)=>{ fs.readFile(path,"utf8",function(err,data){ if(err) reject(err); else resolve(data); }) }) } //書寫方式二 readFileByPromise("a.txt").then( data =>{ //打印文件中的內容 console.log(data); }).catch( error =>{ //拋出異常, console.log(error); })
generator函數
ES6的新特性generator函數(面試的時候掛在這裏),中文譯爲生成器,在之前一個函數中的代碼要麼被調用,要麼不被調用,還不存在能暫停的狀況,generator讓代碼暫停成待執行,定義一個生成器很簡單,在函數名前加個*號,使用上也與普通函數有區別。
舉個例子,方便你們理解:
function *Calculate(a,b){ let sum=a+b; console.log(sum); let sub=a-b; console.log(sub); }
上面即是一個簡單的generator聲明例子。
generator函數不能直接調用,直接調用generator函數會返回一個對象,只有調用該對象的next()方法才能執行函數裏的代碼。
let gen=Calculate(2,7);
執行該函數:
gen.next(); /*打印 9 -5 */
其實單獨介紹generator並無太大的價值,要配合key yield,才能真正發揮generator的價值。yield能將生Generator函數的代碼邏輯分割成多個部分,下面改寫上面的生成器函數。
function *Calculate(a,b){ let sum=a+b; yield console.log(sum); let sub=a-b; yield console.log(sub); } let gen=Calculate(2,7); gen.next(); /*輸出 9*/
能夠看到這段代碼執行到第一個yield處就中止了,若是要讓裏邊全部的代碼都執行完就得反覆調用next()方法
let gen=Calculate(2,7); gen.next(); gen.next(); /*輸出 9 -5*/
在用一個例子,來講明generator函數與回調函數的區別:
回調函數:
fs.readFile("a.txt",(err,data)=>{ if(!err){ console.log(data); fs.readFile("b.txt",(err,data)=>{ if(!err) console.log(data); }) } })
這是一個典型的回調嵌套,過多的回調嵌套形成代碼的可讀性和可維護性大大下降,造成了使人深惡痛絕的回調地獄,試想若是有一天讓你按順序讀取10個文件,那就得嵌套10層,再或者需求變動,讀取順序要變了先讀b.txt,再度a.txt那改來真的不要太爽。
generator函數:
function readFile(path) { fs.readFile(path,"utf8",function(err,data){ it.next(data); }) } function *main() { var result1 = yield readFile("a.txt"); console.log(result1); var result2 = yield readFile("b.txt"); console.log(result2); var result3 = yield readFile("c.txt"); console.log(result3); } var it = main(); it.next();
generator函數的強大在於容許你經過一些實現細節來將異步過程隱藏起來,依然使代碼保持一個單線程、同步語法的代碼風格。這樣的語法使得咱們可以很天然的方式表達咱們程序的步驟/語句流程,而不須要同時去操做一些異步的語法格式。
async-await
async函數返回一個promise對象,若是在async函數中返回一個直接量,async會經過Promise.resolve封裝成Promise對象。
咱們能夠經過調用promise對象的then方法,獲取這個直接量。
async function test(){ return "Hello World"; } var result=test(); console.log(result); //打印Promise { 'Hello World' }
那如過async函數不返回值,又會是怎麼樣呢?
async function test(){ } var result=test(); console.log(result); //打印Promise { undefined }
await會暫停當前async的執行,await會阻塞代碼的執行,直到await後的表達式處理完成,代碼才能繼續往下執行。
await後的表達式既能夠是一個Promise對象,也能夠是任何要等待的值。
若是await等到的是一個 Promise 對象,await 就忙起來了,它會阻塞後面的代碼,等着 Promise 對象 resolve,而後獲得 resolve 的值,做爲 await 表達式的運算結果。
上邊你看到阻塞一詞,不要驚慌,async/await只是一種語法糖,代碼執行與多個callback嵌套調用沒有區別,本質並非同步代碼,它只是讓你思考代碼邏輯的時候可以以同步的思惟去思考,避開回調地獄,簡而言之-async/await是以同步的思惟去寫異步的代碼,因此async/await並不會影響node的併發數,你們能夠大膽的應用到項目中去!
若是它等到的不是一個 Promise 對象,那 await 表達式的運算結果就是它等到的東西。
舉個例子,方便你們理解:
function A() { return "Hello "; } async function B(){ return "World"; } async function C(){ //等待一個字符串 var s1=await A(); //等待一個promise對象,await的返回值是promise對象resolve的值,也就是"World" var s2=await B(); console.log(s1+s2); } C(); //打印"Hello World"
前言
你真的瞭解JS嗎,看徹底篇,你可能對人生產生疑問。
typeof
typeof運算符,把類型信息當作字符串返回。
//正則表達式 是個什麼 ? typeof /s/ // object //null typeof null // object
正則表達式並非一個‘function’,而是一個object。在大多數語言中,null 表明的是一個空指針(0x00),可是在js中,null爲一個object。
instanceof
instanceof運算符,用來測試一個對象在其原型鏈中是否存在一個構造函數:prototype
//語法 object instanceof constructor function Person(){}; var p =new Person(); p instanceof Person; //true
但
[] instanceof window.frames[0].Array // false
由於 Array.prototype !== window.frames[0].Array.prototype ,所以,咱們必須使用Array.isArray(obj)或者Object.prototype.toString.call(obj) === "[object Array]" 來判斷obj是否爲數組。
Object.prototype.toString
根據上面提到的,可使用該方法獲取對象類型的字符串。
//call的使用能夠看博主前面的文章。(使用apply亦可) var toString = Object.prototype.toString; toString.call(new Date); // [object Date] toString.call(new String); // [object String] toString.call(Math); // [object Math] toString.call(/s/); // [object RegExp] toString.call([]); // [object Array] toString.call(undefined); // [object Undefined] toString.call(null); // [object Null]
做用域安全與構造函數
構造函數:使用new調用的函數,當使用new調用時,構造函數內的this會指向新建立的對象實例。
function Dog(name, age){ this.name = name; this.age = age; } let dog = new Dog("柴犬", 5); dog.name // 柴犬
若是咱們沒有使用new又會如何?
let dog = Dog("柴犬", 5); dog.name // undefined window.name // 柴犬
這是由於在沒有使用new關鍵字時,this在當前狀況被解析成window,因此屬性就被分配到window上了。
function Dog(name, age){ if(this instanceof Dog){ this.name = name; this.age = age; }else{ return new Dog(name, age); } } let dog1 = new Person("柴犬", 5); dog1.name // 柴犬 let dog2 = Dog("柯基犬", 20); dog2 .name // 柯基犬
使用上面的方法,就能夠再不使用new的狀況下,正常構造函數。
惰性載入函數
一個函數以下:
function foo(){ if(a != b){ console.log('111') //返回結果1 }else{ console.log('222') //返回結果2 } }
a和b是不變的,那麼不管執行多少次,結果都是不變的,可是每一次都要執行if判斷語句,這樣就形成了資源浪費。
而惰性載入函數,即可以解決這個問題。
function foo(){ if(a != b){ foo = function(){ console.log('111') } }else{ foo = function(){ console.log('222') } } return foo(); }
var foo = (function foo(){ if(a != b){ return function(){ console.log('111') } }else{ return function(){ console.log('222') } } })();
如上函數所示:第一次執行後便會對foo進行賦值,覆蓋以前的函數,因此再次執行,便不會在執行if判斷語句。
fun.bind(thisarg[,arg1[,arg2[,....]]])綁定函數
thisarg:當綁定函數被調用時,該參數會做爲原函數運行時的this指向。當使用new時,該參數無效。
arg:當綁定時,這些參數將傳遞給被綁定的方法。
例子:
let person = { name: 'addone', click: function(e){ console.log(this.name) } } let btn = document.getElementById('btn'); EventUtil.addHandle(btn, 'click', person.click);
這裏建立了一個person對象,而後將person.click方法分配給DOM,可是當你按按鈕時,會打印undefied,緣由是this指向了DOM而不是person。
解決方法,當時是使用綁定函數了:
EventUtil.addHandle(btn, 'click', person.click.bind(person));
函數柯里化
柯里化是把接受多個參數的函數轉變成接受單一參數的函數。
//簡單例子,方便你明白柯里化 function add(num1, num2){ return num1 + num2; } function curryAdd(num2){ return add(1, num2); } add(2, 3) // 5 curryAdd(2) // 3
下面是柯里化函數的通用方法:
function curry(fn){ var args = Array.prototype.slice.call(arguments, 1); return function(){ let innerArgs = Array.prototype.slice.call(arguments); let finalArgs = args.concat(innerArgs); return fn.apply(null, finalArgs); } }
Array.prototype.slice.call(arguments, 1)來獲取第一個參數後的全部參數。在函數中,一樣調用 Array.prototype.slice.call(arguments)讓innerArgs存放全部的參數, 而後用contact將內部外部參數組合起來,用apply傳遞函數。
function add(num1, num2){ return num1 + num2; } let curryAdd1 = curry(add, 1); curryAdd1(2); // 3 let curryAdd2 = curry(add, 1, 2); curryAdd2(); // 3
不可擴展對象
默認狀況下對象都是可擴展的,不管是擴展屬性或是方法。
let dog = { name: '柴犬' }; dog.age = 5;
如第二行,咱們爲dog擴展了age屬性。
使用Object.preventExtensions()能夠阻止擴展行爲。
let dog = { name: '柴犬' }; Object.preventExtensions(dog); dog.age = 20; dog.age // undefined
還可使用 Object.isExtensible()來判斷對象是否支持擴展。
let dog = { name: 'addone' }; Object.isExtensible(dog); // true Object.preventExtensions(dog); Object.isExtensible(dog); // false。
密封的對象
密封后的對象不可擴展,且不能刪除屬性和方法。
使用Object.seal()來進行密封。
let dog = { name: '柴犬' }; Object.seal(dog); dog.age = 20; delete dog.name; dog.age // undefined dog.name // 柴犬
固然也有Object.isSealed()來判斷是否密封
let dog = { name: '柴犬' }; Object.isExtensible(dog); // true Object.isSealed(dog); // false Object.seal(dog); Object.isExtensible(dog); // false Object.isSealed(dog); // true
凍結對象
凍結對象爲防篡改級別最高的,密封,且不能修改。
使用Object.freeze()來進行凍結。
let dog= { name: '柴犬' }; Object.freeze(dog); dog.age = 20; delete dog.name; dog.name = '吉娃娃' dog.age // undefined dog.name // 柴犬
固然也有Object.isFrozen()來判斷是否凍結
let dog = { name: '柴犬' }; Object.isExtensible(dog); // true Object.isSealed(dog); // false Object.isFrozen(dog); // false Object.freeze(dog); Object.isExtensible(dog); // false Object.isSealed(dog); // true Object.isFrozen(dog); // true
數組分塊
瀏覽器對長時間運行的腳本進行了制約,若是運行時間超過特定時間或者特定長度,便不會繼續執行。
若是發現某個循環佔用了大量的時間,那麼就要面對下面兩個問題:
1.該循環是否必須同步完成?
2.數據是否必須按順序完成?
若是是否,那麼咱們可使用一種叫作數組分塊的技術。基本思路是爲要處理的項目建立一個列隊,而後使用定時取出一個要處理的項目進行處理,以此類推。
function chunk(array, process, context){ setTimeout(function(){ // 取出下一個項目進行處理 let item = array.shift(); process.call(item); if(array.length > 0){ setTimeout(arguments.callee, 100); } }, 100) }
這裏設置三個參數,要處理的數組,處理的函數,運行該函數的環境。
節流函數
節流函數目的是爲了防止某些操做執行的太快,好比onresize,touch等事件。這種高頻率的操做可能會使瀏覽器崩潰,爲了不這種狀況,能夠採用節流的方式。
function throttle(method, context){ clearTimeout(method.tId); method.tId = setTimeout(function(){ method.call(context); }, 100) }
這裏接收兩個參數,要執行的函數,和執行的環境。執行時先clear以前的定時器,而後將當前定時器賦值給方法的tId
,以後調用call
來肯定函數的執行環境。
function resizeDiv(){ let div = document.getElementById('div'); div.style.height = div.offsetWidth + "px"; } window.onresize = function(){ throttle(resizeDiv); }
前言
本篇文章比較適合3年以上的前端工做者,JS三座大山分別指:原型與原型鏈,做用域及閉包,異步和單線程。
原型與原型鏈
說到原型,就不得不提一下構造函數,首先咱們看下面一個簡單的例子:
function Dog(name,age){ this.name = name; this.age = age; } let dog1 = new Dog("哈士奇",3); let dog2 = new Dog("泰迪",2);
首先創造空的對象,再讓this指向這個對象,經過this.name進行賦值,最終返回this,這其實也是new 一個對象的過程。
其實: let obj = {} 是 let obj = new Object()的語法糖; let arr = [] 是 let arr = new Array()的語法糖; function Dog(){...} 是 let Dog = new Fucntion()的語法糖。
那什麼是原型那?在js中,全部對象都是Object的實例,並繼承Object.prototype的屬性和方法,可是有一些是隱性的。
咱們來看一下原型的規則:
1.全部的引用類型(包括數組,對象,函數)都具備對象特性;可自由擴展屬性。
var obj = {}; obj.attribute = "三座大山"; var arr = []; arr.attribute = "三座大山"; function fn1 () {} fn1.attribute = "三座大山";
2.全部的引用類型(包括數組,對象,函數)都有隱性原型屬性(__proto__),值也是一個普通的對象。
console.log(obj.__proto__);
3.全部的函數,都有一個prototype屬性,值也是一個普通的對象。
console.log(obj.prototype);
4.全部的引用類型的__proto__屬性值都指向構造函數的prototype屬性值。
console.log(obj.__proto__ === Object.prototype); // true
5.當試圖獲取對象屬性時,若是對象自己沒有這個屬性,那就會去他的__proto__(prototype)中去尋找。
function Dog(name){ this.name = name; } Dog.prototype.callName = function (){ console.log(this.name,"wang wang"); } let dog1 = new Dog("Three Mountain"); dog1.printName = function (){ console.log(this.name); } dog1.callName(); // Three Mountain wang wang dog1.printName(); // Three Mountain
原型鏈:以下圖。
我找一個屬性,首先會在f.__proto__中去找,由於屬性值爲一個對象,那麼就會去f.__proto__.__proto__去找,同理若是還沒找到,就會一直向上去查找,直到結果爲null爲止。這個串起來的鏈即爲原型鏈。
做用域及閉包
講到做用域,你會想到什麼?固然是執行上下文。每一個函數都有本身的excution context,和variable object。這些環境用於儲存上下文中的變量,函數聲明,參數等。只有函數才能製造做用域。
PS:for if else 不能創造做用域。
console.log(a) ; // undefined var a = 1;
//可理解爲
var a;
console.log(a); // undefined
a = 1;
執行console.log時,a只是被聲明出來,並無賦值;因此結果固然是undefined。
this
本質上來講,在js裏this是一個指向函數執行環境的指針。this永遠指向最後調用它的對象,而且在執行時才能獲取值,定義是沒法確認他的值。
var a = { name : "A", fn : function (){ console.log (this.name) } } a.fn() // this === a
a 調用了fn() 因此此時this爲a
a.fn.call ({name : "B"}) // this === {name : "B"}
使用call(),將this的值指定爲{name:"B"}
var fn1 = a.fn fn1() // this === window
雖然指定fn1 = a.fn,可是調用是有window調用,因此this 爲window
this有多種使用場景,下面我會主要介紹4個使用場景:
1.做爲構造函數執行
function Student(name,age) { this.name = name // this === s this.age = age // this === s //return this } var s = new Student("py1988",30)
首先new 字段會建立一個空的對象,而後調用apply()函數,將this指向這個空對象。這樣的話,函數內部的this就會被空對象代替。
2.做爲普通函數執行
function fn () { console.log (this) // this === window } fn ()
3.做爲對象屬性執行
var obj = { name : "A", printName : function () { console.log (this.name) // this === obj } } obj.printName ()
4.call(),apply(),bind()
三個函數能夠修改this的指向,具體請往下看:
var name = "小明" , age = "17" var obj = { name : "安妮", objAge : this.age, fun : function () { console.log ( this.name + "今年" + this.age ) } } console.log(obj.objAge) // 17 obj.fun() // 安妮今年undefined
var name = "小明" , age = "17" var obj = { name : "安妮", objAge :this.age, fun : function (like,dislike) { console.log (this.name + "今年" + this.age ,"喜歡吃" + like + "不喜歡吃" + dislike) } } var a = { name : "Jay", age : 23 } obj.fun.call(a,"蘋果","香蕉") // Jay今年23 喜歡吃蘋果不喜歡吃香蕉
obj.fun.apply(a,["蘋果","香蕉"]) // Jay今年23 喜歡吃蘋果不喜歡吃香蕉
obj.fun.bind(a,"蘋果","香蕉")() // Jay今年23 喜歡吃蘋果不喜歡吃香蕉
首先call,apply,bind第一個參數都是this指向的對象,call和apply若是第一個參數指向null或undefined時,那麼this會指向windows對象。
call,apply,bind的執行方式如上例所示。call,apply都是改變上下文中的this,而且是當即執行的。bind方法可讓對應的函數想何時調用就何時調用。
閉包
閉包的概念很抽象,看下面的例子你就會理解什麼叫閉包了:
function a(){ var n = 0; this.fun = function () { n++; console.log(n); }; } var c = new a(); c.fun(); //1 c.fun(); //2
閉包就是可以讀取其餘函數內部變量的函數。在js中只有函數內部的子函數才能讀取局部變量。因此能夠簡單的理解爲:定義在內部函數的函數。
用途主要有兩個:
1)前面提到的,讀取函數內部的變量。
2)讓變量值始終保持在內存中。
異步和單線程
咱們先感覺下異步。
console.log("start"); setTimeout(function () { console.log("medium"); }, 1000); console.log("end");
使用異步後,打印的順序爲 start-> end->medium。由於沒有阻塞。
爲何會產生異步呢?
首先由於js爲單線程,也就是說CPU同一時間只能處理一個事務。得按順序,一個一個處理。
如上例所示,第一步:執行第一行打印 「start」;第二步:執行setTimeout,將其中的函數分存起來,等待時間結束後執行;第三步:執行最後一行,打印「end」;第四部:處於空閒狀態,查看暫存中,是否有可執行的函數;第五步:執行分存函數。
爲何js引擎是單線程?
js的主要用途是與用戶互動,以及操做DOM,這決定它只能是單線程。例:一個線程要添加DOM節點,一個線程要刪減DOM節點,容易形成分歧。
爲了更好使用多CPU,H5提供了web Worker 標準,容許js建立多線程,可是子線程受到主線程控制,並且不得操做DOM。
任務列隊
單線程就意味着,全部的任務都要排隊,前一個結束,纔會執行後面的任務。若是列隊是由於計算量大,CPU忙不過來,倒也算了。可是更多的時候,CPU是閒置的,由於IO設備處理得很慢,例如 ajax讀取網絡數據。js設計者便想到,主線程徹底能夠無論IO設備,將其掛起,而後執行後面的任務。等後面的任務結束掉,在反過頭來處理掛起的任務。
好,咱們來梳理一下:
1)全部的同步任務都在主線程上執行,行程一個執行棧。
2)除了主線程以外,還存在一個任務列隊,只要一步任務有了運行結果,就在任務列隊中植入一個時間。
3)主線程完成全部任務,就會讀取列隊任務,並將其執行。
4)重複上面三步。
只要主線程空了,就會讀取任務列隊,這就是js的運行機制,也被稱爲 event loop(事件循環)。
前言
Nodejs目前處境稍顯尷尬,不少語言都已經擁有異步非阻塞的能力。阿里的思路是比較合適的,可是必需要注意,絕對不能讓node作太多的業務邏輯,他只適合接收生成好的數據,而後或渲染後,或直接發送到客戶端。
爲何nodejs 還能夠成爲主流技術哪?
是由於nodejs 對於大前端來講仍是很是重要的技術!!!若是你理解nodejs 的編程原理,很容易就會理解angularjs,reactjs 和vuejs 的設計原理。
NodeJS
Node是一個服務器端JavaScript解釋器,用於方便地搭建響應速度快、易於擴展的網絡應用。Node使用事件驅動,非阻塞I/O 模型而得以輕量和高效,很是適合在分佈式設備上運行數據密集型的實時應用。
Node是一個可讓JavaScript運行在瀏覽器以外的平臺。它實現了諸如文件系統、模塊、包、操做系統 API、網絡通訊等Core JavaScript沒有或者不完善的功能。歷史上將JavaScript移植到瀏覽器外的計劃不止一個,但Node.js 是最出色的一個。
V8引擎
V8 JavaScript引擎是Google用於其Chrome瀏覽器的底層JavaScript引擎。不多有人考慮JavaScript在客戶機上實際作了些什麼!
實際上,JavaScript引擎負責解釋並執行代碼。Google使用V8建立了一個用C++編寫的超快解釋器,該解釋器擁有另外一個獨特特徵;您能夠下載該引擎並將其嵌入任何應用程序。V8 JavaScript引擎並不只限於在一個瀏覽器中運行。
所以,Node實際上會使用Google編寫的V8 JavaScript引擎,並將其重建爲可在服務器上使用。
事件驅動
在咱們使用Java,PHP等語言實現編程的時候,咱們面向對象編程是完美的編程設計,這使得他們對其餘編程方法不屑一顧。殊不知大名鼎鼎Node使用的倒是事件驅動編程的思想。那什麼是事件驅動編程。
事件驅動編程,爲須要處理的事件編寫相應的事件處理程序。代碼在事件發生時執行。
爲須要處理的事件編寫相應的事件處理程序。要理解事件驅動和程序,就須要與非事件驅動的程序進行比較。實際上,現代的程序大可能是事件驅動的,好比多線程的程序,確定是事件驅動的。早期則存在許多非事件驅動的程序,這樣的程序,在須要等待某個條件觸發時,會不斷地檢查這個條件,直到條件知足,這是很浪費cpu時間的。而事件驅動的程序,則有機會釋放cpu從而進入睡眠態(注意是有機會,固然程序也可自行決定不釋放cpu),當事件觸發時被操做系統喚醒,這樣就能更加有效地使用cpu。
來看一張簡單的事件驅動模型(uml):
事件驅動模型主要包含3個對象:事件源、事件和事件處理程序。
事件源:產生事件的地方(html元素)
事件:點擊/鼠標操做/鍵盤操做等等
事件對象:當某個事件發生時,可能會產生一個事件對象,該時間對象會封裝好該時間的信息,傳遞給事件處理程序
事件處理程序:響應用戶事件的代碼
運行原理
當咱們搜索Node.js時,奪眶而出的關鍵字就是 「單線程,異步I/O,事件驅動」,應用程序的請求過程能夠分爲倆個部分:CPU運算和I/O讀寫,CPU計算速度一般遠高於磁盤讀寫速度,這就致使CPU運算已經完成,可是不得不等待磁盤I/O任務完成以後再繼續接下來的業務。
因此I/O纔是應用程序的瓶頸所在,在I/O密集型業務中,假設請求須要100ms來完成,其中99ms化在I/O上。若是須要優化應用程序,讓他能同時處理更多的請求,咱們會採用多線程,同時開啓100個、1000個線程來提升咱們請求處理,固然這也是一種可觀的方案。
可是因爲一個CPU核心在一個時刻只能作一件事情,操做系統只能經過將CPU切分爲時間片的方法,讓線程能夠較爲均勻的使用CPU資源。但操做系統在內核切換線程的同時也要切換線程的上線文,當線程數量過多時,時間將會被消耗在上下文切換中。因此在大併發時,多線程結構仍是沒法作到強大的伸縮性。
那麼是否能夠另闢蹊徑呢?!咱們先來看看單線程,《深刻淺出Node》一書提到 「單線程的最大好處,是不用像多線程編程那樣到處在乎狀態的同步問題,這裏沒有死鎖的存在,也沒有線程上下文切換所帶來的性能上的開銷」,那麼一個線程一次只能處理一個請求豈不是無稽之談,先讓咱們看張圖:
Node.js的單線程並非真正的單線程,只是開啓了單個線程進行業務處理(cpu的運算),同時開啓了其餘線程專門處理I/O。當一個指令到達主線程,主線程發現有I/O以後,直接把這個事件傳給I/O線程,不會等待I/O結束後,再去處理下面的業務,而是拿到一個狀態後當即往下走,這就是「單線程」、「異步I/O」。
I/O操做完以後呢?Node.js的I/O 處理完以後會有一個回調事件,這個事件會放在一個事件處理隊列裏頭,在進程啓動時node會建立一個相似於While(true)的循環,它的每一次輪詢都會去查看是否有事件須要處理,是否有事件關聯的回調函數須要處理,若是有就處理,而後加入下一個輪詢,若是沒有就退出進程,這就是所謂的「事件驅動」。這也從Node的角度解釋了什麼是」事件驅動」。
在node.js中,事件主要來源於網絡請求,文件I/O等,根據事件的不一樣對觀察者進行了分類,有文件I/O觀察者,網絡I/O觀察者。事件驅動是一個典型的生產者/消費者模型,請求到達觀察者那裏,事件循環從觀察者進行消費,主線程就能夠快馬加鞭的只關注業務不用再去進行I/O等待。
優勢
Node 公開宣稱的目標是 「旨在提供一種簡單的構建可伸縮網絡程序的方法」。咱們來看一個簡單的例子,在 Java和 PHP 這類語言中,每一個鏈接都會生成一個新線程,每一個新線程可能須要 2 MB的配套內存。在一個擁有 8 GB RAM 的系統上,理論上最大的併發鏈接數量是 4,000 個用戶。隨着您的客戶羣的增加,若是但願您的 Web 應用程序支持更多用戶,那麼,您必須添加更多服務器。因此在傳統的後臺開發中,整個 Web 應用程序架構(包括流量、處理器速度和內存速度)中的瓶頸是:服務器可以處理的併發鏈接的最大數量。這個不一樣的架構承載的併發數量是不一致的。
而Node的出現就是爲了解決這個問題:更改鏈接到服務器的方式。在Node 聲稱它不容許使用鎖,它不會直接阻塞 I/O 調用。Node在每一個鏈接發射一個在 Node 引擎的進程中運行的事件,而不是爲每一個鏈接生成一個新的 OS 線程(併爲其分配一些配套內存)。
缺點
如上所述,nodejs的機制是單線程,這個線程裏面,有一個事件循環機制,處理全部的請求。在事件處理過程當中,它會智能地將一些涉及到IO、網絡通訊等耗時比較長的操做,交由worker threads去執行,執行完了再回調,這就是所謂的異步IO非阻塞吧。可是,那些非IO操做,只用CPU計算的操做,它就本身扛了,好比算什麼斐波那契數列之類。它是單線程,這些本身扛的任務要一個接着一個地完成,前面那個沒完成,後面的只能乾等。所以,對CPU要求比較高的CPU密集型任務多的話,就有可能會形成號稱高性能,適合高併發的node.js服務器反應緩慢。
適合場景
一、RESTful API
這是適合 Node 的理想狀況,由於您能夠構建它來處理數萬條鏈接。它仍然不須要大量邏輯;它本質上只是從某個數據庫中查找一些值並將它們組成一個響應。因爲響應是少許文本,入站請求也是少許的文本,所以流量不高,一臺機器甚至也能夠處理最繁忙的公司的 API 需求。
二、實時程序
好比聊天服務
聊天應用程序是最能體現 Node.js 優勢的例子:輕量級、高流量而且能良好的應對跨平臺設備上運行密集型數據(雖然計算能力低)。同時,聊天也是一個很是值得學習的用例,由於它很簡單,而且涵蓋了目前爲止一個典型的 Node.js 會用到的大部分解決方案。
三、單頁APP
ajax不少。如今單頁的機制彷佛很流行,好比phonegap作出來的APP,一個頁面包打天下的例子比比皆是。
總而言之,NodeJS適合運用在高併發、I/O密集、少許業務邏輯的場景
前言
在一個項目中,技術的統一性是最重要的,數據庫的設計則是重點中的重點。NoSQL 是目前最流行的數據庫,可是其實用性和功能性遠不如sql數據庫。
實際不少SQL數據庫被詬病的性能問題大可能是源於程序員的不合理設計,一個好的設計可使sql類數據庫提升幾倍的性能。
1.細節的優化
字段儘可能設置爲not null 。
規範字段大小,越小越好。
表名規範前綴。
一個表儘可能儲存一個對象。
char永遠比varchar更高效。
timestamp 比datetime小一倍。
避免字串ID。
單條查詢最後用limit 1。
不用mysql內置函數,由於不會創建查詢緩存。
使用ip而不是域名做爲數據庫的路徑,避免dns解析。
2.使用sql內置功能
例如trigger,procedure,event...等等,能夠有效減小後端代碼的運用,可是不適合處理高觸發的項目。
3.選擇適合的存儲引擎
最多見的就是InnoDB 與 MyISAM. 二者區別請自行百度。
4.將數據保存至內存中
從內存中讀取數據,最大限度減小磁盤的操做,相關內容會在後面詳細解釋。
5.提升磁盤讀寫速度
6.充分使用索引 INDEX
1
2
|
mysql> DROP INDEX index_name ON tab;
//添加index
mysql> ALTER TABLE tab DROP INDEX index_name ;
//刪除index
|
7.使用內存磁盤
如今基礎設施都過硬,因此能夠將sql 目錄遷移到內存磁盤上。
8.讀寫分離設計
隨着系統變得愈來愈龐大,特別是當它們擁有不好的SQL時,一臺數據庫服務器一般不足以處理負載。可是多個數據庫意味着重複,除非你對數據進行了分離。更通常地,這意味着創建主/從副本系統,其中程序會對主庫編寫全部的Update、Insert和Delete變動語句,而全部Select的數據都讀取自從數據庫(或者多個從數據庫)。
儘管概念上很簡單,可是想要合理、精確地實現並不容易,這可能須要大量的代碼工做。所以,即使在開始時使用同一臺數據庫服務器,也要儘早計劃在php中使用分離的DB鏈接來進行讀寫操做。若是正確地完成該項工做,那麼系統就能夠擴展到2臺、3臺甚至12臺服務器,並具有高可用性和穩定性。
9.使用memcache或者redis
以前的博客有相關的介紹。
10.SQL數據庫分散式佈局
將數據庫分散到多個服務器上,分擔數據庫工做壓力。
在簡單理解cookie/session機制這篇文章中,簡要闡述了cookie和session的原理。本文將要簡單闡述另外一個同cookie/session一樣重要的技術術語:token。
什麼是token
token的意思是「令牌」,是服務端生成的一串字符串,做爲客戶端進行請求的一個標識。
當用戶第一次登陸後,服務器生成一個token並將此token返回給客戶端,之後客戶端只需帶上這個token前來請求數據便可,無需再次帶上用戶名和密碼。
簡單token的組成;uid(用戶惟一的身份標識)、time(當前時間的時間戳)、sign(簽名,token的前幾位以哈希算法壓縮成的必定長度的十六進制字符串。爲防止token泄露)。
身份認證概述
因爲HTTP是一種沒有狀態的協議,它並不知道是誰訪問了咱們的應用。這裏把用戶當作是客戶端,客戶端使用用戶名還有密碼經過了身份驗證,不過下次這個客戶端再發送請求時候,還得再驗證一下。
通用的解決方法就是,當用戶請求登陸的時候,若是沒有問題,在服務端生成一條記錄,在這個記錄裏能夠說明登陸的用戶是誰,而後把這條記錄的id發送給客戶端,客戶端收到之後把這個id存儲在cookie裏,下次該用戶再次向服務端發送請求的時候,能夠帶上這個cookie,這樣服務端會驗證一下cookie裏的信息,看能不能在服務端這裏找到對應的記錄,若是能夠,說明用戶已經經過了身份驗證,就把用戶請求的數據返回給客戶端。
以上所描述的過程就是利用session,那個id值就是sessionid。咱們須要在服務端存儲爲用戶生成的session,這些session會存儲在內存,磁盤,或者數據庫。
基於token機制的身份認證
使用token機制的身份驗證方法,在服務器端不須要存儲用戶的登陸記錄。大概的流程:
客戶端使用用戶名和密碼請求登陸。服務端收到請求,驗證用戶名和密碼。驗證成功後,服務端會生成一個token,而後把這個token發送給客戶端。客戶端收到token後把它存儲起來,能夠放在cookie或者Local Storage(本地存儲)裏。客戶端每次向服務端發送請求的時候都須要帶上服務端發給的token。服務端收到請求,而後去驗證客戶端請求裏面帶着token,若是驗證成功,就向客戶端返回請求的數據。
利用token機制進行登陸認證,能夠有如下方式:
a.用設備mac地址做爲token
客戶端:客戶端在登陸時獲取設備的mac地址,將其做爲參數傳遞到服務端
服務端:服務端接收到該參數後,便用一個變量來接收,同時將其做爲token保存在數據庫,並將該token設置到session中。客戶端每次請求的時候都要統一攔截,將客戶端傳遞的token和服務器端session中的token進行對比,相同則登陸成功,不一樣則拒絕。
此方式客戶端和服務端統一了惟一的標識,而且保證每個設備擁有惟一的標識。缺點是服務器端須要保存mac地址;優勢是客戶端無需從新登陸,只要登陸一次之後一直可使用,對於超時的問題由服務端進行處理。
b.用sessionid做爲token
客戶端:客戶端攜帶用戶名和密碼登陸
服務端:接收到用戶名和密碼後進行校驗,正確就將本地獲取的sessionid做爲token返回給客戶端,客戶端之後只需帶上請求的數據便可。
此方式的優勢是方便,不用存儲數據,缺點就是當session過時時,客戶端必須從新登陸才能請求數據。
固然,對於一些保密性較高的應用,能夠採起兩種方式結合的方式,將設備mac地址與用戶名密碼同時做爲token進行認證。
APP利用token機制進行身份認證
用戶在登陸APP時,APP端會發送加密的用戶名和密碼到服務器,服務器驗證用戶名和密碼,若是驗證成功,就會生成相應位數的字符產做爲token存儲到服務器中,而且將該token返回給APP端。
之後APP再次請求時,凡是須要驗證的地方都要帶上該token,而後服務器端驗證token,成功返回所須要的結果,失敗返回錯誤信息,讓用戶從新登陸。其中,服務器上會給token設置一個有效期,每次APP請求的時候都驗證token和有效期。
token的存儲
token能夠存到數據庫中,可是有可能查詢token的時間會過長致使token丟失(其實token丟失了再從新認證一個就好,可是別丟太頻繁,別讓用戶沒事兒就去認證)。
爲了不查詢時間過長,能夠將token放到內存中。這樣查詢速度絕對就不是問題了,也不用太擔憂佔據內存,就算token是一個32位的字符串,應用的用戶量在百萬級或者千萬級,也是佔不了多少內存的。
token的加密
token是很容易泄露的,若是不進行加密處理,很容易被惡意拷貝並用來登陸。加密的方式通常有:
在存儲的時候把token進行對稱加密存儲,用到的時候再解密。文章最開始提到的簽名sign:將請求URL、時間戳、token三者合併,經過算法進行加密處理。
最好是兩種方式結合使用。
還有一點,在網絡層面上token使用明文傳輸的話是很是危險的,因此必定要使用HTTPS協議。
總結
以上就是對於token在用戶身份認證過程當中的簡單總結。但願沒有技術背景的產品經理們在和開發哥哥溝通的時候不要再被這些技術術語問住了。