一、什么是高并發(fā)
高并發(fā)是指在同一個(gè)時(shí)間點(diǎn)抛人,有大量用戶同時(shí)訪問URL地址弛姜,比如淘寶雙11、定時(shí)領(lǐng)取紅包就會(huì)產(chǎn)生高并發(fā)妖枚;又比如貼吧的爆吧廷臼,就是惡意的高并發(fā)請(qǐng)求,也就是DDOS攻擊(通過大量合法的請(qǐng)求占用大量網(wǎng)絡(luò)資源绝页,以達(dá)到癱瘓網(wǎng)絡(luò)的目的)荠商。
二、高并發(fā)帶來的后果
- 服務(wù)端
??導(dǎo)致站點(diǎn)服務(wù)器续誉、DB服務(wù)器資源被占滿崩潰莱没。
??數(shù)據(jù)的存儲(chǔ)和更新結(jié)果和理想的設(shè)計(jì)不一致。 - 用戶角度
??尼瑪酷鸦,網(wǎng)站這么卡饰躲,刷新了還這樣,垃圾網(wǎng)站井佑,不玩了属铁。
三、并發(fā)下的處理
-
配置數(shù)據(jù)庫連接池C3P0
??配置連接池的原因是因?yàn)槲覀円鲆粋€(gè)高并發(fā)的秒殺系統(tǒng)躬翁,可能一些連接會(huì)被鎖住焦蘑,其他的線程就可能會(huì)拿不到連接的情況,所以我們要調(diào)整一下連接池的屬性來更符合我們的場景
<!-- 數(shù)據(jù)庫連接池 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 配置連接池屬性 -->
<property name="driverClass" value="${driver}"/>
<property name="jdbcUrl" value="${url}"/>
<property name="user" value="${username}"/>
<property name="password" value="${password}"/>
<!-- c3p0連接池的私有屬性 -->
<property name="maxPoolSize" value="30"/>
<property name="minPoolSize" value="10"/>
<property name="autoCommitOnClose" value="false"/>
<!-- 獲取連接超時(shí)時(shí)間 -->
<property name="checkoutTimeOut" value="1000"/>
<!-- 當(dāng)獲取連接失敗重試次數(shù) -->
<property name="acquireRetryAttempsts" value="2"/>
</bean>
-
事務(wù)+鎖來防止并發(fā)導(dǎo)致數(shù)據(jù)錯(cuò)亂
??建議所有的數(shù)據(jù)操作都寫在一個(gè)sql事務(wù)里面盒发。下面舉三個(gè)例子來說明情況例嘱。
①簽到功能:一天一個(gè)用戶只能簽到一次,簽到成功后用戶獲得一個(gè)積分宁舰,我們可以把添加簽到和添加積分放到一個(gè)事務(wù)里面拼卵,這樣在添加失敗,或者編輯用戶積分失敗的時(shí)候可以回滾數(shù)據(jù)蛮艰。
②在高并發(fā)情況下用戶進(jìn)行抽獎(jiǎng)腋腮,很可能會(huì)導(dǎo)致用戶參與抽獎(jiǎng)的時(shí)候積分被扣除,而獎(jiǎng)品實(shí)際上已經(jīng)被抽完了壤蚜。我們可以在事務(wù)里面即寡,通過WITH(UPDLOCK)鎖住商品表,或者update表的獎(jiǎng)品剩余數(shù)量和最后編輯時(shí)間字段袜刷,來把數(shù)據(jù)行鎖住聪富,然后進(jìn)行用戶積分的消耗,都完成后提交事務(wù)著蟹,失敗就回滾墩蔓,這樣就放置數(shù)據(jù)錯(cuò)亂梢莽。
//當(dāng)我們用UPDLOCK來讀取記錄時(shí)可以對(duì)取到的記錄加上更新鎖
//從而加上鎖的記錄在其它的線程中是不能更改的只能等本線程的事務(wù)結(jié)束后才能更改
update commodity with (updlock) set count = count-1 where id=?;
③ 如果要實(shí)現(xiàn)這樣一個(gè)需求:cache里面的數(shù)據(jù)必須每天9點(diǎn)更新一次,其他時(shí)間點(diǎn)緩存每小時(shí)更新一次奸披。并且到9點(diǎn)的時(shí)候昏名,凡是已經(jīng)打開頁面的用戶會(huì)自動(dòng)刷新頁面。
??這里面包含的用戶觸發(fā)緩存更新的邏輯:用戶刷新頁面阵面,當(dāng)緩存存在的時(shí)候葡粒,會(huì)獲取到最后一次緩存更新的時(shí)間。如果當(dāng)前時(shí)間>9點(diǎn)膜钓,并且最后緩存時(shí)間在9點(diǎn)之前,則會(huì)從數(shù)據(jù)庫中重新獲取數(shù)據(jù)保存到cache中卿嘲。如果大量用戶在9點(diǎn)之前已經(jīng)打開了頁面颂斜,而且在9點(diǎn)之后還未關(guān)閉頁面,那么就會(huì)導(dǎo)致在9點(diǎn)的時(shí)候會(huì)有很多并發(fā)請(qǐng)求過來拾枣,數(shù)據(jù)庫服務(wù)器壓力暴增沃疮。
??要解決這個(gè)問題,最好就是只有一個(gè)請(qǐng)求去數(shù)據(jù)庫獲取梅肤,其他都是從緩存中獲取數(shù)據(jù)司蔬。此時(shí),我們就可以用鎖來解決:從數(shù)據(jù)讀取到緩存那段代碼前面加上鎖姨蝴,這樣在并發(fā)的情況下只會(huì)有一個(gè)請(qǐng)求是從數(shù)據(jù)庫里獲取數(shù)據(jù)俊啼,其他都是從緩存中獲取。
但是不是所有的方法都需要加事務(wù)左医,比如讀操作授帕。
事務(wù)時(shí)間要盡可能短
??當(dāng)在高并發(fā)系統(tǒng)進(jìn)行寫入操作的時(shí)候就會(huì)鎖定你寫入的那行代碼,要是寫入時(shí)間很長那么鎖定的時(shí)間也很長浮梢,不利于高并發(fā)的操作跛十。特別是網(wǎng)絡(luò)操作運(yùn)行時(shí)間一般都比較長,所以最好不要穿插進(jìn)來秕硝。利用緩存處理高并發(fā)
把被用戶大量訪問的靜態(tài)資源緩存在CDN中
??在秒殺的時(shí)候芥映,如果秒殺沒有開始,用戶看到喜歡的商品远豺,用戶就會(huì)不停刷新這個(gè)頁面奈偏。所以類似于秒殺詳情頁這些被用戶大量訪問的頁面靜態(tài)資源(如html、css憋飞、js)就應(yīng)該部署到CDN節(jié)點(diǎn)上霎苗,也就是用戶訪問的那些html已經(jīng)不在系統(tǒng)中了,而是在CDN節(jié)點(diǎn)上榛做。
用戶大量刷新→CDN(detail頁靜態(tài)化唁盏,靜態(tài)資源js内狸、css等)→高并發(fā)系統(tǒng)
合理使用nosql緩存數(shù)據(jù)庫
??高并發(fā)接口,比如秒殺地址接口是沒辦法使用CDN緩存的厘擂,因?yàn)镃DN適合我這個(gè)請(qǐng)求對(duì)應(yīng)的資源不變的昆淡,比如JavaScript,JavaScript拿回來在瀏覽器執(zhí)行刽严,它的內(nèi)容是不變的昂灵。但是高并發(fā)接口的返回?cái)?shù)據(jù)是在變化的,比如秒殺接口:一開始沒有秒殺舞萄,隨著時(shí)間推移已經(jīng)開啟秒殺眨补,再往后秒殺已經(jīng)關(guān)閉了。所以高并發(fā)接口不適合放在CDN緩存倒脓,但是適合放在服務(wù)器端緩存撑螺。
??后端緩存可以用應(yīng)用系統(tǒng)來控制,比如先訪問數(shù)據(jù)庫拿到高并發(fā)接口的數(shù)據(jù)崎弃,然后放在redis緩存里面甘晤,下次訪問直接在緩存里面找。
??使用這種方法的好處就是一致性維護(hù)成本低:請(qǐng)求地址要求拿到高并發(fā)接口的數(shù)據(jù)的時(shí)候饲做,先訪問服務(wù)器端緩存线婚,若沒有再訪問數(shù)據(jù)庫。如果高并發(fā)接口的數(shù)據(jù)需要改變的時(shí)候盆均,我們可以等待緩存超時(shí)再更新數(shù)據(jù)塞弊,或者直接穿透到數(shù)據(jù)庫更新,又或者當(dāng)數(shù)據(jù)庫數(shù)據(jù)更新的時(shí)候主動(dòng)更新一下緩存缀踪。使用一級(jí)緩存居砖,減少nosql服務(wù)器壓力
??一級(jí)緩存使用站點(diǎn)服務(wù)器緩存去存儲(chǔ)數(shù)據(jù),注意只存儲(chǔ)部分請(qǐng)求量大的數(shù)據(jù)驴娃,并且緩存的數(shù)據(jù)量要控制奏候,不能過分的使用站點(diǎn)服務(wù)器的內(nèi)存而影響了站點(diǎn)應(yīng)用程序的正常運(yùn)行。善用原子計(jì)數(shù)器
??在秒殺系統(tǒng)中唇敞,熱點(diǎn)商品會(huì)有大量用戶參與進(jìn)來蔗草,然后就產(chǎn)生了大量減庫存競爭。所以當(dāng)執(zhí)行秒殺的時(shí)候系統(tǒng)會(huì)做一個(gè)原子計(jì)數(shù)器(可以通過redis/nosql實(shí)現(xiàn))疆柔,它記錄的是商品的庫存咒精。當(dāng)用戶執(zhí)行秒殺的時(shí)候,就會(huì)去減庫存旷档,也就是減原子計(jì)數(shù)器模叙,保證原子性。當(dāng)減庫存成功之后就回去記錄行為消息(誰去減了庫存)鞋屈,減了會(huì)后作為一個(gè)消息當(dāng)?shù)揭粋€(gè)分布的MQ(消息隊(duì)列)中范咨,然后后端的服務(wù)器會(huì)把其落地到MySQL中故觅。
原子計(jì)數(shù)器:主要是高并發(fā)的統(tǒng)計(jì)的時(shí)候要用到。比如:
increment() 和 decrement() 操作是原子的讀-修改-寫操作渠啊。為了安全實(shí)現(xiàn)計(jì)數(shù)器输吏,必須使用當(dāng)前值,并為其添加一個(gè)值替蛉,或?qū)懗鲂轮倒峤Γ羞@些均視為一項(xiàng)操作,其他線程不能打斷它躲查。
善用redis的消息隊(duì)列
??使用redis的list它浅,當(dāng)用戶參與到高并發(fā)活動(dòng)時(shí),將參與用戶的信息添加到消息隊(duì)列中镣煮,然后再寫個(gè)多線程程序去消耗隊(duì)列(pop數(shù)據(jù))罚缕,這樣能避免服務(wù)器宕機(jī)的危險(xiǎn)。
??通過消息隊(duì)列可以做很多的服務(wù)怎静,比如定時(shí)短信發(fā)送服務(wù),使用sorted set(sset)黔衡,發(fā)送時(shí)間戳作為排序依據(jù)蚓聘,短信數(shù)據(jù)隊(duì)列根據(jù)時(shí)間升序,然后寫個(gè)程序定時(shí)循環(huán)去讀取sset隊(duì)列中的第一條盟劫,當(dāng)前時(shí)間是否超過發(fā)送時(shí)間夜牡,如果超過就進(jìn)行短信發(fā)送。事務(wù)競爭的優(yōu)化
??在高并發(fā)秒殺系統(tǒng)中侣签,第一個(gè)用戶執(zhí)行減庫存操作塘装,在commit/rollback以前,第二個(gè)秒殺用戶也要執(zhí)行減庫存影所,但是因?yàn)橐粋€(gè)用戶得到了鎖蹦肴,其他用戶就必須進(jìn)行等待(因?yàn)楫?dāng)事務(wù)不去提交/回滾的話行級(jí)鎖是沒辦法釋放的)。也就是說后面的線程想減庫存猴娩,必須等到前面的線程釋放哈行鎖阴幌。這就變成了一個(gè)串行的操作:同一個(gè)商品減庫存,大家都要排隊(duì)等卷中,就產(chǎn)生了大量阻塞操作矛双。而且,sql語句發(fā)送給數(shù)據(jù)庫也可能存在網(wǎng)絡(luò)延遲蟆豫,這樣后面的用戶等待時(shí)間就更長了议忽。
解決方案
①M(fèi)ySQL源碼層的修改方案:在update后面加上這樣一句話:/+[auto_commit]/,當(dāng)你執(zhí)行完這條update的時(shí)候它會(huì)自動(dòng)回滾十减≌恍遥回滾的條件是:update影響的記錄數(shù)是1就可以commit愤估,如果為0就會(huì)rollback。也就是不給java客戶端和MySQL之間網(wǎng)絡(luò)延遲侦镇,然后再由java客戶端其控制commit和rollback灵疮,而是直接通過語句發(fā)過去你就告訴我commit和rollback。這個(gè)成本比較高壳繁,需要修改MySQL源碼
②使用存儲(chǔ)過程:存儲(chǔ)過程的本質(zhì)就是讓一組sql組成一組事務(wù)震捣,然后再M(fèi)ySQL端完成,避免客戶端完成事務(wù)造成性能的干擾闹炉。一般情況下蒿赢,spring聲明事務(wù)和手動(dòng)控制事務(wù)都是客戶端控制事務(wù)。這些事務(wù)在行級(jí)鎖沒有那么高的競爭情況下是完全OK的渣触,但是秒殺是一個(gè)特殊的應(yīng)用場景羡棵,它會(huì)在同一行中產(chǎn)生熱點(diǎn),大家都競爭同一行嗅钻,這個(gè)時(shí)候存儲(chǔ)過程就能夠發(fā)揮作用了皂冰,他把整個(gè)sql執(zhí)行過程放在MySQL端完成,MySQL執(zhí)行sql的效率非常高养篓。*簡單的邏輯我們可以使用存儲(chǔ)過程秃流,太過復(fù)雜的就不要依賴了踩麦。
-- 秒殺執(zhí)行存儲(chǔ)過程
DELIMITER $$ -- onsole ; 轉(zhuǎn)換為 $$
-- 定義存儲(chǔ)過程
-- 參數(shù):in 輸入?yún)?shù); out 輸出參數(shù)
-- row_count():返回上一條修改類型sql(delete,insert,upodate)的影響行數(shù)
-- row_count: 0:未修改數(shù)據(jù); >0:表示修改的行數(shù); <0:sql錯(cuò)誤/未執(zhí)行修改sql
CREATE PROCEDURE `seckill`.`execute_seckill`
(IN v_seckill_id bigint, IN v_phone BIGINT,
IN v_kill_time TIMESTAMP, OUT r_result INT)
BEGIN
DECLARE insert_count INT DEFAULT 0;
START TRANSACTION;
INSERT ignore INTO success_killed (seckill_id, user_phone, create_time)
VALUES(v_seckill_id, v_phone, v_kill_time);
SELECT ROW_COUNT() INTO insert_count;
IF (insert_count = 0) THEN
ROLLBACK;
SET r_result = -1;
ELSEIF (insert_count < 0) THEN
ROLLBACK ;
SET r_result = -2;
ELSE
UPDATE seckill SET number = number - 1
WHERE seckill_id = v_seckill_id AND end_time > v_kill_time
AND start_time < v_kill_time AND number > 0;
SELECT ROW_COUNT() INTO insert_count;
IF (insert_count = 0) THEN
ROLLBACK;
SET r_result = 0;
ELSEIF (insert_count < 0) THEN
ROLLBACK;
SET r_result = -2;
ELSE
COMMIT;
SET r_result = 1;
END IF;
END IF;
END;
$$
-- 代表存儲(chǔ)過程定義結(jié)束
DELIMITER ;
SET @r_result = -3;
-- 執(zhí)行存儲(chǔ)過程
call execute_seckill(1001, 13631231234, now(), @r_result);
-- 獲取結(jié)果
SELECT @r_result;
③通常我們的操作是:減庫存(rowLock)→插入購買明細(xì)→commit/rollback(freeLock)逮京。我們可以在這個(gè)基礎(chǔ)上進(jìn)行一些簡單的優(yōu)化,調(diào)換操作的順序:插入購買明細(xì)→減庫存(rowLock)→commit/rollback(freeLock)网梢,我這樣們的延遲就只會(huì)發(fā)生在update語句這個(gè)點(diǎn)上碧注。
腳本合理控制請(qǐng)求
??比如用腳本防止用戶重復(fù)點(diǎn)擊導(dǎo)致多余的請(qǐng)求嚣伐。使用具有高并發(fā)能力的編程語言去開發(fā)
nodejs就是一個(gè)具有高并發(fā)能力的編程語言,它使用單線程異步時(shí)間機(jī)制萍丐,不會(huì)因?yàn)閿?shù)據(jù)邏輯處理問題導(dǎo)致服務(wù)器資源被占用而導(dǎo)致服務(wù)器宕機(jī)轩端,我們可以使用NodeJs寫web接口。
apache模式逝变,以下簡稱A模式船万。一共有三個(gè)點(diǎn)餐窗口,三位服務(wù)人員骨田,三位廚師(請(qǐng)自行腦補(bǔ)畫面耿导,但是別亂想)。顧客在任一窗口點(diǎn)餐[所謂多線程]态贤,點(diǎn)完后服務(wù)員傳達(dá)廚師舱呻,等待廚師出餐,服務(wù)員返給顧客[同步返回響應(yīng)結(jié)果]。顧客本次購物結(jié)束箱吕。服務(wù)員進(jìn)行下一位顧客的點(diǎn)餐[接收下一個(gè)請(qǐng)求]芥驳。
??nodejs模式,以下簡稱N模式茬高。一共只有一個(gè)點(diǎn)餐窗口一位服務(wù)員[單線程]兆旬,一位廚師[CPU]。顧客在窗口點(diǎn)餐怎栽,點(diǎn)完后服務(wù)員傳達(dá)廚師丽猬,廚師進(jìn)行出餐,而服務(wù)員不必等待[不必等待當(dāng)前請(qǐng)求返回結(jié)果]熏瞄,直接進(jìn)行下一位顧客的點(diǎn)餐脚祟,然后繼續(xù)傳達(dá)下一個(gè)顧客的訂單給廚師。廚師挨個(gè)完成后拋出給出餐窗口[異步返回響應(yīng)結(jié)果]强饮,顧客到出餐窗口取餐由桌,本次購物結(jié)束。
比如要統(tǒng)計(jì)用戶通過各種方式(如點(diǎn)擊圖片/鏈接)進(jìn)入到商品詳情的行為次數(shù)邮丰,如果同時(shí)有1w個(gè)用戶同時(shí)在線訪問頁面行您,一次拉動(dòng)滾動(dòng)條屏幕頁面展示10件商品,這樣就會(huì)有10w個(gè)請(qǐng)求過來剪廉,服務(wù)端需要把請(qǐng)求的次數(shù)數(shù)據(jù)入庫邑雅,這樣服務(wù)器分分鐘給跪了。
??要解決這些訪問量大的數(shù)據(jù)統(tǒng)計(jì)接口的問題妈经,我們可以通過nodejs寫一個(gè)數(shù)據(jù)處理接口,把統(tǒng)計(jì)數(shù)據(jù)先存到redis的list中捧书,然后再使用nodejs寫一個(gè)腳本吹泡,腳本的功能就是從redis里取出數(shù)據(jù)保存到mysql數(shù)據(jù)庫中。這個(gè)腳本會(huì)一直運(yùn)行经瓷,當(dāng)redis沒有數(shù)據(jù)需求要同步到數(shù)據(jù)庫中的時(shí)候爆哑,sleep,然后再進(jìn)行數(shù)據(jù)同步操作舆吮。
集群
??集群是一種多服務(wù)器結(jié)構(gòu)揭朝,也就是把同一個(gè)業(yè)務(wù),部署在多個(gè)服務(wù)器上(區(qū)別于分布式色冀,分布式是把個(gè)業(yè)務(wù)分拆多個(gè)子業(yè)務(wù)潭袱,部署在不同的服務(wù)器上),這樣就可以提高單位時(shí)間內(nèi)執(zhí)行的任務(wù)數(shù)來提升效率锋恬,把壓力分擔(dān)到多臺(tái)服務(wù)器上屯换。
??我們可以集群部署Mysql數(shù)據(jù)庫,或者NoSQL DB服務(wù)器(如mongodb服務(wù)器、redis服務(wù)器)彤悔,把一些常用的查詢數(shù)據(jù)嘉抓,并且不會(huì)經(jīng)常變化的數(shù)據(jù)保存到NoSQL DB服務(wù)器,來減少數(shù)據(jù)庫服務(wù)器的壓力晕窑,加快數(shù)據(jù)的響應(yīng)速度抑片。-
構(gòu)建一個(gè)好的服務(wù)器架構(gòu)
大致的服務(wù)器架構(gòu)如下:
服務(wù)器
├負(fù)載均衡
│├Nginx
│└阿里云SLB
├資源監(jiān)控
└分布式數(shù)據(jù)庫
├主從分離、集群
├分布式
└表優(yōu)化杨赤、索引優(yōu)化等
NoSQL
├redis
│├主從分離
│└集群
├m(xù)ongodb
│├主從分離
│└集群
├m(xù)emcache
│├主從分離
│└集群
└...
CDN
├html
├css
├js
└image
高并發(fā)情境中敞斋,更新用戶相關(guān)緩存需要分布式存儲(chǔ),比如使用用戶ID進(jìn)行hash分組望拖,把用戶分不到不用的緩存中渺尘,這樣一個(gè)緩存集合的總量不會(huì)很大,不會(huì)影響查詢效率说敏。