高並發
高並發(High Concurrency)是互聯網分布式系統架構設計中必須考慮的因素之一,它通常是指,通過設計保證系統能夠同時並行處理很多請求。高並發相關常用的一些指標有響應時間(Response Time),吞吐量(Throughput),每秒查詢率QPS(Query Per Second),並發用戶數等。
響應時間:系統對請求做出響應的時間。例如系統處理一個HTTP請求需要200ms,這個200ms就是系統的響應時 間。
吞吐量:單位時間內處理的請求數量。
QPS:每秒響應請求數。在互聯網領域,這個指標和吞吐量區分的沒有這么明顯。
並發用戶數:同時承載正常使用系統功能的用戶數量。例如一個即時通訊系統,同時在線量一定程度上代表了系統 的並發用戶數。
高並發一直是網站上線后會遇到的一個嚴峻的考驗,渡過了一切都好,渡不過就是宕機。
在電商時代如此發達的今天,高並發無此不在雙十一 、618、雙十二,還有雷猴王的某米手機搶購。首先我們要分析高並發究竟會給我們開發者帶來什么樣的挑戰
大量的請求,如果僅僅只有一台服務器肯定是吃不消的,通常一些公司都是一台服務器上部署了很多個網站也充當了數據庫服務器、redis服務器。如果要應用高並發沒有足夠的硬件支持是不行的。我們需要進行 分布式集群 以及 負載均衡
硬件支持有了過后,我們就需要下一步的分析
這時我們還需要提高網站的吞吐量,怎么提高呢?首先我們需要針對IO密集型做異步化操作,搶單的頁面不只是有搶單按鈕,還有商品的介紹,圖片,文字描述等。對於這些數據我們要進行緩存,一萬個用戶一萬次請求都從數據庫中取數據與只取一次剩下9999次從緩存中取效率自然是不一樣的
上面說的都是為了解決一個 高 字,而並發才是我們真正需要准備的,假如兩個用戶同時請求,這時庫存還有1,程序里先判斷庫存是不是1,現在都符合條件,然后進行生成訂單等操作。就發生了資源共享的問題,明明只有一個訂單,但是兩個用戶都完成了訂單,那么這個商品應該給誰呢?
並發
假設現在是一個電商網站,今天要舉辦活動,有10個商品低價銷售,但是會來搶購的人會特別多,最后只有十個人可以成功的買到商品
假設的邏輯,我們用戶進行了請求,我們把他們的信息放到庫里,但是只有前十個人是可以購買商品的,因為庫存只有10個
也許我們可以用鎖來解決並發的問題,但是鎖無疑帶來的是效率的低下,用戶體驗也極低。我們想要的是快速返回,但是后面那一堆的邏輯怎么辦呢?我們可以使用RabbitMq隊列,用戶的請求到達了搶單接口,我們只向隊列中丟一條數據后就立即返回
這時又來了一個問題,會有同一個用戶多次進行請求的情況,如果像之前的邏輯,前10條信息有二條是屬於一個人的呢,(這里假設每個人只可以購買一次)我們就需要進行判斷了,同一個賬戶發送的多次請求,我們只認為第一次請求是有效的,剩下的都請都直接返回。因為是並發,我們又怎么做到第一次請求有效呢?這時我們可以使用Redis incr存儲用戶的標識,Redis是單線程的,不存在並發的問題。incr返回為1那么是第一次請求,為N則是第N請求那么它就是無效的。這是請求標識
請求標識我們可以在搶單接口就進行判斷,也就是先拿用戶的標識去Incr,返回為1則丟到隊列,不為1則不丟到隊列。
也可以在rabbitmq的消費端進行處理,從rabbitmq消息隊列中拿到用戶信息后,進行incr。再進行下一步操作
丟到了消息隊列中,我們還需要去處理,consumer我們肯定是要有多個的,我們可以使用平分分發與手動交付。在這里我們把用戶的信息進行入庫,當然入庫后我們再向Redis中存入一條入庫標識
上面都是在后端,客戶端這里點擊了搶單按鈕后可以立即導向排隊界面(是不是很熟悉,某米。。。)在這個界面進行輪詢五秒一次,判斷當前用戶在庫中的位置,如果是前十,那么就進行訂單操作,不是。。。那就再等,看看會不會有其他用戶放棄購買資格。
也可以使用Memcache鎖
product_lock_key 為票鎖key
當product_key存在於memcached中時,所有用戶都可以進入下單流程。
當進入支付流程時,首先往memcached存放add(product_lock_key, “1″),如果返回成功,進入支付流程。如果不成,則說明已經有人進入支付流程,則線程等待N秒,遞歸執行add操作。
測試方法:
本地模擬測試網站高訪問高並發采用的測試工具是大名鼎鼎的Loadrunner,這個工具做測試的一般都知道。
秒殺解決
頁面靜態化(CDN)、限流、引入Redis
1、在秒殺的情況下,肯定不能如此高頻率的去讀寫數據庫,會嚴重造成性能問題的
必須使用緩存,將需要秒殺的商品放入緩存中,並使用鎖來處理其並發情況。當接到用戶秒殺提交訂單的情況下,先將商品數量遞減(加鎖/解鎖)后再進行其他方面的處理,處理失敗在將數據遞增1(加鎖/解鎖),否則表示交易成功。
當商品數量遞減到0時,表示商品秒殺完畢,拒絕其他用戶的請求。
2、這個肯定不能直接操作數據庫的,會掛的。直接讀庫寫庫對數據庫壓力太大,要用緩存。
把你要賣出的商品比如10個商品放到緩存中;然后在memcache里設置一個計數器來記錄請求數,這個請求書你可以以你要秒殺賣出的商品數為基數,比如你想賣出10個商品,只允許100個請求進來。那當計數器達到100的時候,后面進來的就顯示秒殺結束,這樣可以減輕你的服務器的壓力。然后根據這100個請求,先付款的先得后付款的提示商品以秒殺完。
3、首先,多用戶並發修改同一條記錄時,肯定是后提交的用戶將覆蓋掉前者提交的結果了。
這個直接可以使用加鎖機制去解決,樂觀鎖或者悲觀鎖。
樂觀鎖,就是在數據庫設計一個版本號的字段,每次修改都使其+1,這樣在提交時比對提交前的版本號就知道是不是並發提交了,但是有個缺點就是只能是應用中控制,如果有跨應用修改同一條數據樂觀鎖就沒辦法了,這個時候可以考慮悲觀鎖。
悲觀鎖,就是直接在數據庫層面將數據鎖死,類似於oralce中使用select xxxxx from xxxx where xx=xx for update,這樣其他線程將無法提交數據。
除了加鎖的方式也可以使用接收鎖定的方式,思路是在數據庫中設計一個狀態標識位,用戶在對數據進行修改前,將狀態標識位標識為正在編輯的狀態,這樣其他用戶要編輯此條記錄時系統將發現有其他用戶正在編輯,則拒絕其編輯的請求,類似於你在操作系統中某文件正在執行,然后你要修改該文件時,系統會提醒你該文件不可編輯或刪除。
4、不建議在數據庫層面加鎖,建議通過服務端的內存鎖(鎖主鍵)。當某個用戶要修改某個id的數據時,把要修改的id存入memcache,若其他用戶觸發修改此id的數據時,讀到memcache有這個id的值時,就阻止那個用戶修改。
5、實際應用中,並不是讓mysql去直面大並發讀寫,會借助“外力”,比如緩存、利用主從庫實現讀寫分離、分表、使用隊列寫入等方法來降低並發讀寫。
6.隨機拋棄請求 看運氣了 redis的set 有個方法srandmember
實際上硬件才是根本
負載均衡 :多台服務器
讀寫分離:數據庫作為數據持久化工具,必定在並發訪問頻繁且負載壓力較大的情況下成為系統性能的‘瓶頸’。即使使用上面的本地緩存等方式來解決頻繁訪問數據庫的問題,但仍舊會有大量的並發請求要訪問動態數據, 其中的‘讀寫分離’方案就是一種被廣泛采用的方案
數據庫鎖的介紹(一般不建議用)
悲觀鎖(Pessimistic Lock), 顧名思義,就是很悲觀,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會block直到它拿到鎖。傳統的關系型數據庫里邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。 通過 jdbc 實現時 sql 語句只要在整個語句之后加 for update 即可。例如: select …for update
樂觀鎖(Optimistic Lock), 顧名思義,就是很樂觀,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制。樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,像數據庫如果提供類似於write_condition機制的其實都是提供的樂觀鎖。
兩種鎖各有優缺點,不可認為一種好於另一種,像樂觀鎖適用於寫比較少的情況下,即沖突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。但如果經常產生沖突,上層應用會不斷的進行retry,這樣反倒是降低了性能,所以這種情況下用悲觀鎖就比較合適
共享鎖:(讀取)操作創建的鎖。其他用戶可以並發讀取數據,但任何事物都不能獲取數據上的排它鎖,直到已釋放所有共享鎖。
共享鎖(S鎖)又稱為讀鎖,若事務T對數據對象A加上S鎖,則事務T只能讀A;其他事務只能再對A加S鎖,而不能加X鎖,直到T釋放A上的S鎖。這就保證了其他事務可以讀A,但在T釋放A上的S鎖之前不能對A做任何修改。
排它鎖:排它鎖又稱為寫鎖((eXclusive lock,簡記為X鎖)),若事物T對數據對象A加上X鎖,則只允許T讀取和修改A,其它任何事務都不能再對A加任何類型的鎖,直到T釋放A上的鎖。它防止任何其它事務獲取資源上的鎖,直到在事務的末尾將資源上的原始鎖釋放為止。
互斥鎖是一個互斥的同步對象,意味着同一時間有且僅有一個線程可以獲取它。
互斥鎖可適用於一個共享資源每次只能被一個線程訪問的情況
小結:
悲觀鎖:查詢加鎖 【select ...... for update】
樂觀鎖:修改加鎖 【版本號控制】
排它鎖:事務A可以查詢、修改,直到事務A釋放為止才可以執行下一個事務
共享鎖:事務A可以查詢、修改,同時事務B也可以查詢但不能修改
互斥鎖:同一資源同一時間只能被一個線程訪問
大量用戶訪問解決
大型網站,比如門戶網站,在面對大量用戶訪問、高並發請求方面帶來的問題
1大並發:在同一個時間點,有大量的客戶來訪問我們的網站,如果訪問量過大,就可能造成網站癱瘓。
2大流量:當網站大后,有大量的圖片,視頻, 這樣就會對流量要求高,需要更多更大的帶寬。
3大存儲:你的數據量會成海量的數據,如果我們的數據放入一張表,是無法應對的。可能對數據保存和查詢出現問題。
基本的解決方案集中在這樣幾個環節:使用高性能的服務器、高性能的數據庫、高效率的編程語言、還有高性能的Web容器,(對架構分層+負載均衡+集群)這幾個解決思路在一定程度上意味着更大的投入。
解決方案:
一、提高硬件能力、增加系統服務器。(當服務器增加到某個程度的時候系統所能提供的並發訪問量幾乎不變,所以不能根本解決問題)
二、使用緩存(本地緩存:本地可以使用JDK自帶的 Map、Guava Cache.分布式緩存:Redis、Memcache.本地緩存不適用於提高系統並發量,一般是用處用在程序中。比如Spring是如何實現單例的呢?大家如果看過源碼的話,應該知道,Spiring把已經初始過的變量放在一個Map中,下次再要使用這個變量的時候,先判斷Map中有沒有,這也就是系統中常見的單例模式的實現。
分布式緩存利器Redis集群,Redis集群的搭建至少需要三主三從。 1. 所有的redis節點彼此互聯(PING-PONG機制),內部使用二進制協議優化傳輸速度和帶寬。 2. 節點的fail是通過集群中超過半數的節點檢測失效時才生效(所以一個集群中至少要有三個節點)。 3. 客戶端與redis節點直連,不需要中間proxy層.客戶端不需要連接集群所有節點,連接集群中任何一個可用節點即可。 4. 集群中每一個節點都存放不同的內容,每一個節點都應有備份機。 5. redis-cluster把所有的物理節點映射到[0-16383]slot上,cluster 負責維護node<->slot<->value
Redis 集群中內置了16384 個哈希槽,當需要在Redis 集群中放置一個key-value 時,redis先對 key 使用 crc16 算法算出一個結果,然后把結果對16384 求余數,這樣每個key 都會對應一個編號在0-16383 之間的哈希槽,redis會根據節點數量大致均等的將哈希槽映射到不同的節點。
三 、消息隊列 (解耦+削峰+異步)通過異步處理提高系統性能,降低系統耦合性
在不使用消息隊列服務器的時候,用戶的請求數據直接寫入數據庫,在高並發的情況下數據庫壓力劇增,使得響應速度變慢。但是在使用消息隊列之后,用戶的請求數據發送給消息隊列之后立即 返回,再由消息隊列的消費者進程從消息隊列中獲取數據,異步寫入數據庫。由於消息隊列服務器處理速度快於數據庫(消息隊列也比數據庫有更好的伸縮性),因此響應速度得到大幅改善。
通過使用消息中間件對Dubbo服務間的調用進行解耦, 消息中間件可利用高效可靠的消息傳遞機制進行平台無關的數據交流,並基於數據通信來進行分布式系統的集成。通過提供消息傳遞和消息排隊模型,可以在分布式環境下擴展進程間的通信。通過消息中間件,應用程序或組件之間可以進行可靠的異步通訊,從而降低系統之間的耦合度,提高系統的可擴展性和可用性。
四 、采用分布式開發 (不同的服務部署在不同的機器節點上,並且一個服務也可以部署在多台機器上,然后利用 Nginx 負載均衡訪問。這樣就解決了單點部署(All In)的缺點,大大提高的系統並發量)
五 、數據庫分庫(讀寫分離)、分表(水平分表、垂直分表)
PXC高可用集群與Replication集群結合方案
這種的集群在遇到單表數據量超過2000萬的時候,mysql性能會受損,所以一個集群還不夠,我們需要把數據分到另一個集群,這個稱為“切片”,就是把大量的數據拆分到不同的集群中,每個集群的數據都是不一樣的,通過MyCat這個阿里巴巴的開源中間件,可以把sql分到不同的集群里面去。
六、 采用集群 (多台機器提供相同的服務)系統架構方案
七、CDN 加速 (將一些靜態資源比如圖片、視頻等等緩存到離用戶最近的網絡節點)
八、瀏覽器緩存 頁面靜態化(使用php自己的ob緩存技術實現, 主流的mvc框架(tp,yii,laravel)模板引擎一般都自帶頁面靜態化 )
九、使用合適的連接池(數據庫連接池、線程池等等)
十、適當使用多線程進行開發。
十一、使用鏡像
鏡像是大型網站常采用的提高性能和數據安全性的方式,鏡像的技術可以解決不同網絡接入商和地域帶來的用戶訪問速度差異,比如ChinaNet和EduNet之間的差異就促使了很多網站在教育網內搭建鏡像站點,數據進行定時更新或者實時更新。有很多專業的現成的解決架構和產品可選。也有廉價的通過軟件實現的思路,比如Linux上的rsync等工具。
十二、圖片服務器分離
大家知道,對於Web服務器來說,不管是Apache、IIS還是其他容器,圖片是最消耗資源的,於是我們有必要將圖片與頁面進行分離,這是基本上大型網站都會采用的策略,他們都有獨立的、甚至很多台的圖片服務器。這樣的架構可以降低提供頁面訪問請求的服務器系統壓力,並且可以保證系統不會因為圖片問題而崩潰。
在應用服務器和圖片服務器上,可以進行不同的配置優化,比如apache在配置ContentType的時候可以盡量少支持、盡可能少的LoadModule,保證更高的系統消耗和執行效率。