單線程模型nginx
Redis客戶端對服務端的每次調用都經歷了發送命令,執行命令,返回結果三個過程。其中執行命令階段,因爲Redis是單線程來處理命令的,全部每一條到達服務端的命令不會馬上執行,全部的命令都會進入一個隊列中,而後逐個被執行。而且多個客戶端發送的命令的執行順序是不肯定的。可是能夠肯定的是不會有兩條命令被同時執行,不會產生併發問題,這就是Redis的單線程基本模型。redis
1. redis單線程問題數據庫
單線程指的是網絡請求模塊使用了一個線程(因此不需考慮併發安全性),即一個線程處理全部網絡請求,其餘模塊仍用了多個線程。編程
2. 爲何說redis可以快速執行api
(1) 絕大部分請求是純粹的內存操做(很是快速)安全
(2) 採用單線程,避免了沒必要要的上下文切換和競爭條件服務器
(3) 非阻塞IO - IO多路複用,Redis採用epoll作爲I/O多路複用技術的實現,再加上Redis自身的事件處理模型將epoll中的鏈接,讀寫,關閉都轉換爲了時間,不在I/O上浪費過多的時間。網絡
Redis採用單線程模型,每條命令執行若是佔用大量時間,會形成其餘線程阻塞,對於Redis這種高性能服務是致命的,因此Redis是面向高速執行的數據庫。多線程
3. redis的內部實現併發
內部實現採用epoll,採用了epoll+本身實現的簡單的事件框架。epoll中的讀、寫、關閉、鏈接都轉化成了事件,而後利用epoll的多路複用特性,毫不在io上浪費一點時間 這3個條件不是相互獨立的,特別是第一條,若是請求都是耗時的,採用單線程吞吐量及性能可想而知了。應該說redis爲特殊的場景選擇了合適的技術方案。
4. Redis關於線程安全問題
redis其實是採用了線程封閉的觀念,把任務封閉在一個線程,天然避免了線程安全問題,不過對於須要依賴多個redis操做的複合操做來講,依然須要鎖,並且有多是分佈式鎖。
另外一篇對redis單線程的理解:Redis單線程理解
redis分客戶端和服務端,一次完整的redis請求事件有多個階段(客戶端到服務器的網絡鏈接-->redis讀寫事件發生-->redis服務端的數據處理(單線程)-->數據返回)。平時所說的redis單線程模型,本質上指的是服務端的數據處理階段,不牽扯網絡鏈接和數據返回,這是理解redis單線程的第一步。接下來,針對不一樣階段分別闡述我的的一些理解。
首先,客戶端和服務器是socket通訊方式,socket服務端監聽可同時接受多個客戶端請求,這點很重要,若是不理解可先記住。注意這裏能夠理解爲本質上與redis無關,這裏僅僅作網絡鏈接,或者能夠理解爲,爲redis服務端提供網絡交互api。
假設創建網絡鏈接須要30秒(爲了更容易理解,因此時間上擴大了N倍)
首先肯定一點,redis的客戶端與服務器端通訊是基於TCP鏈接(不懂去看,基礎很重要),第一階段僅僅是創建了客戶端到服務器的網絡鏈接,而後纔是發生第二階段的讀寫事件。
完成了上一個階段的網絡鏈接,redis客戶端開始真正向服務器發起讀寫事件,假設是set(寫)事件,此時redis客戶端開始向創建的網絡流中送數據,服務端能夠理解爲給每個網絡鏈接建立一個線程同時接收客戶端的請求數據。
假設從客戶端發數據,到服務端接收完數據須要10秒。
服務端完成了第二階段的數據接收,接下來開始依據接收到的數據作邏輯處理,而後獲得處理後的數據。數據處理能夠理解爲一次方法調用,帶參調用方法,最終獲得方法返回值。不要想複雜,重在理解流程。
假設redis服務端處理數據須要0.1秒
這一階段很簡單,當reids服務端數據處理完後 就會當即返回處理後的數據,沒什麼特別須要強調的。
假設服務端把處理後的數據回送給客戶端須要5秒。
第一階段說過,redis是以socket方式通訊,socket服務端可同時接受多個客戶端請求鏈接,也就是說,redis服務同時面對多個redis客戶端鏈接請求,而redis服務自己是單線程運行。
假設,如今有A,B,C,D,E五個客戶端同時發起redis請求,A優先稍微一點點第一個到達,而後是B,C,D,E依次到達,此時redis服務端開始處理A請求,創建鏈接須要30秒,獲取請求數據須要10秒,而後處理數據須要0.1秒,回送數據給客戶端須要5秒,總共大概須要45秒。也就是說,下一個B請求須要等待45秒,這裏注意,也許這五個幾乎同時請求,因爲socket能夠同時處理多個請求,因此創建網絡鏈接階段時間差可忽略,可是在第二階段,服務端須要什麼事都不幹,坐等10秒中,對於CPU和客戶端來講是沒法忍受的。因此說單線程效率很是,很是低,可是正是由於這些相似問題,Redis單線程本質上並非如此運行。接下來討論redis真正的單線程運行方式。
客戶端與服務端創建鏈接交由socket,能夠同時創建多個鏈接(這裏應該是多線程/多進程),創建的鏈接redis是知道的(爲何知道,去看socket編程,再次強調基礎很重要),而後redis會基於這些創建的鏈接去探測哪一個鏈接已經接收完了客戶端的請求數據(注意:不是探測哪一個鏈接創建好了,而是探測哪一個接收完了請求數據),並且這裏的探測動做就是單線程的開始,一旦探測到則基於接收到的數據開始數據處理階段,而後返回數據,再繼續探測下一個已經接收完請求數據的網絡鏈接。注意,從探測到數據處理再到數據返回,全程單線程。這應該就是所謂的redis單線程。至於內部有多複雜咱們無需關心,咱們追求的是理解流程,苛求原理,但不能把內臟都挖出來。
從探測到接受完請求數據的網絡鏈接到最終的數據返回,服務器只須要5.1秒,這個時間是我放大N倍後的數據,實際時間遠遠小於這個,多是5.1的N萬分之一時間,爲何這麼說,由於數據的處理是在本地內存中,速度有多快任你想象,最終的返回數據雖然牽扯到網絡,可是網絡鏈接已經創建,這個速度也是很是很是快的,只是比數據處理階段慢那麼一點點。所以單線程方式在效率上其實並不須要擔憂。
IO多路複用
參考:https://www.zhihu.com/question/32163005
要弄清問題先要知道問題的出現緣由
緣由:
因爲進程的執行過程是線性的(也就是順序執行),當咱們調用低速系統I/O(read,write,accept等等),進程可能阻塞,此時進程就阻塞在這個調用上,不能執行其餘操做.阻塞很正常.
接下來考慮這麼一個問題:一個服務器進程和一個客戶端進程通訊,服務器端read(sockfd1,bud,bufsize),此時客戶端進程沒有發送數據,那麼read(阻塞調用)將阻塞,直到客戶端調用write(sockfd,but,size)發來數據.在一個客戶和服務器通訊時這沒什麼問題;
當多個客戶與服務器通訊時當多個客戶與服務器通訊時,若服務器阻塞於其中一個客戶sockfd1,當另外一個客戶的數據到達套接字sockfd2時,服務器不能處理,仍然阻塞在read(sockfd1,...)上;此時問題就出現了,不能及時處理另外一個客戶的服務,咋麼辦?
I/O多路複用來解決!
I/O多路複用:
繼續上面的問題,有多個客戶鏈接,sockfd1,sockfd2,sockfd3..sockfdn同時監聽這n個客戶,當其中有一個發來消息時就從select的阻塞中返回,而後就調用read讀取收到消息的sockfd,而後又循環回select阻塞;這樣就不會由於阻塞在其中一個上而不能處理另外一個客戶的消息
「I/O多路複用」的英文是「I/O multiplexing」,能夠百度一下multiplexing,就能獲得這個圖:
Q:
那這樣子,在讀取socket1的數據時,若是其它socket有數據來,那麼也要等到socket1讀取完了才能繼續讀取其它socket的數據吧。那不是也阻塞住了嗎?並且讀取到的數據也要開啓線程處理吧,那這和多線程IO有什麼區別呢?
A:
1.CPU原本就是線性的不論什麼都須要順序處理並行只能是多核CPU
2.io多路複用原本就是用來解決對多個I/O監聽時,一個I/O阻塞影響其餘I/O的問題,跟多線程不要緊.
3.跟多線程相比較,線程切換須要切換到內核進行線程切換,須要消耗時間和資源.而I/O多路複用不須要切換線/進程,效率相對較高,特別是對高併發的應用nginx就是用I/O多路複用,故而性能極佳.但多線程編程邏輯和處理上比I/O多路複用簡單.而I/O多路複用處理起來較爲複雜.
I/O 指的是網絡I/O。
多路指的是多個TCP 鏈接(Socket 或Channel)。
複用指的是複用一個或多個線程。
它的基本原理就是再也不由應用程序本身監視鏈接,而是由內核替應用程序監視文件描述符。
客戶端在操做的時候,會產生具備不一樣事件類型的socket。在服務端,I/O 多路複用程序(I/O Multiplexing Module)會把消息放入隊列中,而後經過文件事件分派器(Fileevent Dispatcher),轉發到不一樣的事件處理器中。
多路複用有不少的實現,以select 爲例,當用戶進程調用了多路複用器,進程會被阻塞。內核會監視多路複用器負責的全部socket,當任何一個socket 的數據準備好了,多路複用器就會返回。這時候用戶進程再調用read 操做,把數據從內核緩衝區拷貝到用戶空間。
I/O 多路複用的特色是經過一種機制一個進程能同時等待多個文件描述符,而這些文件描述符(套接字描述符)其中的任意一個進入讀就緒(readable)狀態,select()函數就能夠返回。
import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; class Demo extends Thread { public void run() { Jedis jedis1 = new Jedis(); for (int i=0;i<100;i++){ int num = Integer.parseInt(jedis1.get("num"));// 1: 代碼行1 num = num + 1; // 2: 代碼行2 jedis1.set("num",num+""); System.out.println(jedis1.get("num")); } } } public class test{ public static void main(String... args){ Jedis jedis = new Jedis(); jedis.set("num","1"); new Demo().start(); new Demo().start(); } }
如代碼所示,例如當線程1在代碼行讀取數值爲99時候,此時線程2頁執行讀取操做也是99,隨後同時執行num=num+1,以後更新,致使一次更新丟失,這就是這個代碼測試的錯誤之處。因此Redis自己是線程安全的,可是你還須要保證你的業務必須也是線程安全的。
注意:千萬不要覺得原子操做是線程安全的,原子操做只能保證命令全執行或者全不執行,並不會保證線程安全操做。例如數據庫中的事務就是原子的,依舊還須要提供併發控制!!!!
原子性操做是否線程安全?
原文:https://stackoverflow.com/questions/14370575/why-are-atomic-operations-considered-thread-safe
1. 原子操做是針對訪問共享變量的操做而言的。涉及局部變量訪問的操做無所謂是否原子的。
2. 原子操做是從該操做的執行線程之外的線程來描述的,也就是說它只有在多線程環境下才有意義。
原子操做得「不可分割」包括兩層含義
1.訪問(讀、寫)某個共享變量的操做從其執行線程之外的任何線程來看,該操做要麼已經執行結束要麼還沒有發生,即其餘線程不會「看到」該操做執行了部分的中間效果。
2.訪問同一組共享變量的原子操做是不可以被交錯的。
此原子性與數據庫原子性有區別:最主要區別是數據庫的原子性,能夠被其餘線程看見中間狀態,不然就不會有隔離級別的事了。