大數據下高併發的處理詳解

對於咱們開發的網站,若是網站的訪問量很是大的話,那麼咱們就須要考慮相關的併發訪問問題了。而併發問題是絕大部分的程序員頭疼的問題,但話又說回來了,既然逃避不掉,那咱們就要想一想應對措施,今天咱們就一塊兒討論一下常見的併發和同步吧。
首先爲了更好的理解併發和同步,咱們須要首先明白兩個重要的概念:同步和異步java

同步和異步的區別和聯繫


所謂同步,就是一個線程執行一個方法或函數的時候,會阻塞其它線程,其餘線程要等待它執行完畢才能繼續執行。
異步,就是多個線程之間沒有阻塞,多個線程同時執行。
通俗一點來講,同步就是一件事一件事的作,異步就是作一件事,不影響作其餘事情。
例如:吃飯和說話,只能一件一件的來,由於只有一張嘴。
可是吃飯和聽音樂是異步的,能夠一塊兒進行,由於聽音樂並不影響咱們吃飯。mysql

對於Java程序員來講,Synchronized最爲熟悉了,若是它做用於一個類的話,那麼就是一個線程訪問類的方法時,其餘線程就會阻塞,相反,若是沒有這個關鍵字來修飾的話,不一樣線程就能夠在同一時間訪問同一個方法,這就是異步。程序員

髒讀和不可重複讀


髒讀
髒讀就是指當一個事務正在訪問數據,而且對數據進行了修改,而這種修改尚未提交到數據庫中,這是,另一個事務也訪問這個數據,而後使用了這個數據。由於這個數據是尚未提交的數據,那麼另一個事務讀取的這個數據是髒數據(Dirty Data),依據髒數據所作的操做多是不正確的。sql

不可重複讀
在第一個事務讀取數據後,第二個事務對數據進行了修改,致使第一個事務結束前再訪問這個數據的時候,會發現兩次讀取到的數據是不同的,所以稱爲不可重複讀。數據庫

如何處理併發和同步


今天講的如何處理併發和同同步問題主要是經過鎖機制。
咱們須要明白,鎖機制有兩個層面。
一種是代碼層次上的,若是Java中的同步鎖Synchronized,另外一種是數據庫層次上的,比較典型的就是悲觀鎖(傳統的物理鎖)樂觀鎖網絡

悲觀鎖
悲觀鎖,正如其名,它指的是對數據被外界(包括本系統當前的其餘事務,以及來自外部系統的事務處理)修改持保守態度。所以,在這個數據處理過程當中,將數據處於鎖定狀態。
悲觀鎖的實現,每每依靠數據庫提供的鎖機制(也只有數據庫層提供的鎖機制才能真正保證數據訪問的排他性,不然,即便在本系統中實現了加鎖機制,也沒法保證外部系統不會修改數據)。
一個典型的倚賴數據庫的悲觀鎖調用:session

select * from account where name=」Erica」 for update 

這條 sql 語句鎖定了 account 表中全部符合檢索條件( name=」Erica」 )的記錄。
本次事務提交以前(事務提交時會釋放事務過程當中的鎖),外界沒法修改這些記錄。
Hibernate 的悲觀鎖,也是基於數據庫的鎖機制實現。
下面的代碼實現了對查詢記錄的加鎖:併發

1
2
3
4
String hqlStr ="from TUser as user where user.name='Erica'";
Query query = session.createQuery(hqlStr);
query.setLockMode("user",LockMode.UPGRADE); // 加鎖
List userList = query.list();// 執行查詢,獲取數據

觀察運行期 Hibernate 生成的 SQL 語句:異步

1
select tuser0_.id as id, tuser0_.name as name, tuser0_.group_id as group_id, tuser0_.user_type as user_type, tuser0_.sex as sex from t_user tuser0_ where (tuser0_.name='Erica' ) for update

這裏 Hibernate 經過使用數據庫的 for update 子句實現了悲觀鎖機制。
Hibernate 的加鎖模式有:分佈式

1
2
3
4
5
6
7
8
9
10
11
12
LockMode.NONE : 無鎖機制。 
LockMode.WRITE : Hibernate 在 Insert 和 Update 記錄的時候會自動獲取
LockMode.READ : Hibernate 在讀取記錄的時候會自動獲取。
以上這三種鎖機制通常由 Hibernate 內部使用,如 Hibernate 爲了保證 Update過程當中對象不會被外界修改,會在 save 方法實現中自動爲目標對象加上 WRITE 鎖。

LockMode.UPGRADE :利用數據庫的 for update 子句加鎖。
LockMode.UPGRADE_NOWAIT : Oracle 的特定實現,利用 Oracle 的 for update nowait 子句實現加鎖。
上面這兩種鎖機制是咱們在應用層較爲經常使用的,加鎖通常經過如下方法實現:

Criteria.setLockMode
Query.setLockMode
Session.lock

注意,只有在查詢開始以前(也就是 Hiberate 生成 SQL 以前)設定加鎖,纔會真正經過數據庫的鎖機制進行加鎖處理,不然,數據已經經過不包含 for update子句的 Select SQL 加載進來,所謂數據庫加鎖也就無從談起。

爲了更好的理解select… for update的鎖表的過程,本人將要以mysql爲例,進行相應的講解
開啓兩個測試窗口,其中一個窗口A執行命令:

1
2
3
4
5
6
7
8
9
10
11
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from empinfo for update;
+--------+----------+------+---------+
| Fempno | Fempname | Fage | Fsalary |
+--------+----------+------+---------+
| 1233 | sdfs | NULL | NULL |
| 324234 | sdf | 38 | 12121 |
+--------+----------+------+---------+
2 rows in set (0.00 sec)

這個時候打開窗口B執行更新或插入操做:

1
mysql> update empinfo set Fage=12 where Fempno=1233;

這個時候窗口B的更新或插入操做不會執行,會一直在等待,直到A窗口的事務提交了:

1
2
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

B窗口的更新纔開始執行。
那麼for update到底鎖定表仍是行呢?

因爲InnoDB預設是Row-Level Lock,因此只有「明確」的指定主鍵,MySQL纔會執行Row lock (只鎖住被選取的資料例) ,不然MySQL將會執行Table Lock (將整個資料表單給鎖住)。
例1: (明確指定主鍵,而且有此筆資料,row lock)

1
2
3
SELECT * FROM products WHERE id='3' FOR UPDATE;

SELECT * FROM products WHERE id='3' and type=1 FOR UPDATE;

例2: (明確指定主鍵,若查無此筆資料,無lock)

SELECT * FROM products WHERE id='-1' FOR UPDATE; 

例3: (無主鍵,table lock)

SELECT * FROM products WHERE name='Mouse' FOR UPDATE; 

例4: (主鍵不明確,table lock)

SELECT * FROM products WHERE id<>'3' FOR UPDATE; 

例5: (主鍵不明確,table lock)

SELECT * FROM products WHERE id LIKE '3' FOR UPDATE; 

注1: FOR UPDATE僅適用於InnoDB,且必須在交易區塊(BEGIN/COMMIT)中才能生效。
注2: 要測試鎖定的情況,能夠利用MySQL的Command Mode ,開二個視窗來作測試。在MySql 5.0中測試確實是這樣的
另外:MyAsim 只支持表級鎖,InnerDB支持行級鎖 添加了(行級鎖/表級鎖)鎖的數據不能被其它事務再鎖定,也不被其它事務修改(修改、刪除) 。是表級鎖時,不論是否查詢到記錄,都會鎖定表。
到這裏,悲觀鎖機制你應該瞭解一些了吧~

樂觀鎖
相對悲觀鎖而言,樂觀鎖機制採起了更加寬鬆的加鎖機制。悲觀鎖大多數狀況下依 靠數據庫的鎖機制實現,以保證操做最大程度的獨佔性。但隨之而來的就是數據庫 性能的大量開銷,特別是對長事務而言,這樣的開銷每每沒法承受。如一個金融系統,當某個操做員讀取用戶的數據,並在讀出的用戶數據的基礎上進 行修改時(如更改用戶賬戶餘額),若是採用悲觀鎖機制,也就意味着整個操做過 程中(從操做員讀出數據、開始修改直至提交修改結果的全過程,甚至還包括操做 員中途去煮咖啡的時間),數據庫記錄始終處於加鎖狀態,能夠想見,若是面對幾 百上千個併發,這樣的狀況將致使怎樣的後果。樂觀鎖機制在必定程度上解決了這個問題。樂觀鎖,大可能是基於數據版本 Version )記錄機制實現。何謂數據版本?即爲數據增長一個版本標識,在基於數據庫表的版本解決方案中,通常是經過爲數據庫表增長一個 「version」 字段來 實現。 讀取出數據時,將此版本號一同讀出,以後更新時,對此版本號加一。此時,將提 交數據的版本數據與數據庫表對應記錄的當前版本信息進行比對,若是提交的數據 版本號大於數據庫表當前版本號,則予以更新,不然認爲是過時數據。
假如數據庫中帳戶餘額爲100,version爲1,操做員A讀出餘額,並修改成50,而在A操做的同時操做員B也讀出了帳戶餘額100,並修改成80,A完成了操做錄入系統,version從1加上1變爲2,餘額修改成50,操做員B也提交了記錄,version也變爲2,餘額則是80,可是此時數據庫發現,B提交的version爲2,當前版本也是2,不知足 「 提交版本必須大於記 錄當前版本才能執行更新 「 的樂觀鎖策略。所以,操做員 B 的提交被駁回。 這樣,就避免了操做員 B 用基於version=1 的舊數據修改的結果覆蓋操做 員 A 的操做結果的可能。 從上面的例子能夠看出,樂觀鎖機制避免了長事務中的數據庫加鎖開銷(操做員 A和操做員 B 操做過程當中,都沒有對數據庫數據加鎖),大大提高了大併發量下的系 統總體性能表現。 須要注意的是,樂觀鎖機制每每基於系統中的數據存儲邏輯,所以也具有必定的局 限性,如在上例中,因爲樂觀鎖機制是在咱們的系統中實現,來自外部系統的用戶 餘額更新操做不受咱們系統的控制,所以可能會形成髒數據被更新到數據庫中。在 系統設計階段,咱們應該充分考慮到這些狀況出現的可能性,並進行相應調整(如 將樂觀鎖策略在數據庫存儲過程當中實現,對外只開放基於此存儲過程的數據更新途 徑,而不是將數據庫表直接對外公開)。 Hibernate 在其數據訪問引擎中內置了樂觀鎖實現。若是不用考慮外部系統對數 據庫的更新操做,利用 Hibernate 提供的透明化樂觀鎖實現,將大大提高咱們的 生產力。

Hibernate使用樂觀鎖我只說一下註解的方式:
在Entity中加入如下代碼

1
2
3
4
5
6
7
8
9
10
private int version;

@Version
@Column(name = "version",length = 11)
public int getVersion() {
return version;
}
public void setVersion(int version) {
this.version = version;
}

這樣就能夠輕鬆實現hibernate樂觀鎖方式。

常見併發同步案例分析


案例一:訂票系統案例
某航班只有一張機票,假定有1w我的打開你的網站來訂票,問你如何解決併發問題(可擴展到任何高併發網站要考慮的併發讀寫問題)
問題,1w我的來訪問,票沒出去前要保證你們都能看到有票,不可能一我的在看到票的時候別人就不能看了。到底誰能搶到,那得看這我的的「運氣」(網絡快慢等)
其次考慮的問題,併發,1w我的同時點擊購買,到底誰能成交?總共只有一張票。
首先咱們容易想到和併發相關的幾個方案 :
鎖同步同步更多指的是應用程序的層面,多個線程進來,只能一個一個的訪問,java中指的是syncrinized關鍵字。鎖也有2個層面,一個是java中談到的對象鎖,用於線程同步;另一個層面是數據庫的鎖;若是是分佈式的系統,顯然只能利用數據庫端的鎖來實現。
假定咱們採用了同步機制或者數據庫物理鎖機制,如何保證1w我的還能同時看到有票,顯然會犧牲性能,在高併發網站中是不可取的。使用hibernate後咱們提出了另一個概念:樂觀鎖、悲觀鎖(即傳統的物理鎖);
採用樂觀鎖便可解決此問題。樂觀鎖意思是不鎖定表的狀況下,利用業務的控制來解決併發問題,這樣即保證數據的併發可讀性又保證保存數據的排他性,保證性能的同時解決了併發帶來的髒數據問題。
hibernate中如何實現樂觀鎖:
前提:在現有表當中增長一個冗餘字段,version版本號, long類型
原理:
1)只有當前版本號》=數據庫表版本號,才能提交
2)提交成功後,版本號version ++

閱讀全文:http://click.aliyun.com/m/14341/  

相關文章
相關標籤/搜索