十年網(wǎng)站開發(fā)經(jīng)驗(yàn) + 多家企業(yè)客戶 + 靠譜的建站團(tuán)隊
量身定制 + 運(yùn)營維護(hù)+專業(yè)推廣+無憂售后,網(wǎng)站問題一站解決
你說的這個version是mysql底層的鎖機(jī)制提供的,并不是java提供的。
創(chuàng)新互聯(lián)是一家專業(yè)提供南崗企業(yè)網(wǎng)站建設(shè),專注與網(wǎng)站制作、網(wǎng)站建設(shè)、H5網(wǎng)站設(shè)計、小程序制作等業(yè)務(wù)。10年已為南崗眾多企業(yè)、政府機(jī)構(gòu)等服務(wù)。創(chuàng)新互聯(lián)專業(yè)網(wǎng)站建設(shè)公司優(yōu)惠進(jìn)行中。
使用數(shù)據(jù)版本(Version)記錄機(jī)制實(shí)現(xiàn),這是mysql樂觀鎖最常用的一種實(shí)現(xiàn)方式。所謂的數(shù)據(jù)版本就是給數(shù)據(jù)增加一個版本標(biāo)識,一般是通過為數(shù)據(jù)庫表增加一個數(shù)字類型的 “version” 字段來實(shí)現(xiàn)。當(dāng)讀取數(shù)據(jù)時,將version字段的值一同讀出,數(shù)據(jù)每更新一次,對此version值加1。當(dāng)我們提交更新的時候,判斷數(shù)據(jù)庫表對應(yīng)記錄的當(dāng)前版本信息與第一次取出來的version值進(jìn)行比對,如果數(shù)據(jù)庫表當(dāng)前版本號與第一次取出來的version值相等,則予以更新,否則認(rèn)為是過期數(shù)據(jù),版本號重新讀取再做更新。
關(guān)于mysql中的樂觀鎖和悲觀鎖面試的時候被問到的概率還是比較大的。
mysql的悲觀鎖:
其實(shí)理解起來非常簡單,當(dāng)數(shù)據(jù)被外界修改持保守態(tài)度,包括自身系統(tǒng)當(dāng)前的其他事務(wù),以及來自外部系統(tǒng)的事務(wù)處理,因此,在整個數(shù)據(jù)處理過程中,將數(shù)據(jù)處于鎖定狀態(tài)。悲觀鎖的實(shí)現(xiàn),往往依靠數(shù)據(jù)庫提供的鎖機(jī)制,但是也只有數(shù)據(jù)庫層提供的鎖機(jī)制才能真正保證數(shù)據(jù)訪問的排他性,否則,即使在自身系統(tǒng)中實(shí)現(xiàn)了加鎖機(jī)制,也無法保證外部系統(tǒng)不會修改數(shù)據(jù)。
來點(diǎn)實(shí)際的,當(dāng)我們使用悲觀鎖的時候我們首先必須關(guān)閉mysql數(shù)據(jù)庫的自動提交屬性,因?yàn)镸ySQL默認(rèn)使用autocommit模式,也就是說,當(dāng)你執(zhí)行一個更新操作后,MySQL會立刻將結(jié)果進(jìn)行提交。
關(guān)閉命令為:set autocommit=0;
悲觀鎖可以使用select…for update實(shí)現(xiàn),在執(zhí)行的時候會鎖定數(shù)據(jù),雖然會鎖定數(shù)據(jù),但是不影響其他事務(wù)的普通查詢使用。此處說普通查詢就是平時我們用的:select * from table 語句。在我們使用悲觀鎖的時候事務(wù)中的語句例如:
//開始事務(wù)
begin;/begin work;/start transaction; (三選一)
//查詢信息
select * from order where id=1 for update;
//修改信息
update order set name='names';
//提交事務(wù)
commit;/commit work;(二選一)
此處的查詢語句for update關(guān)鍵字,在事務(wù)中只有SELECT ... FOR UPDATE 或LOCK IN SHARE MODE 同一條數(shù)據(jù)時會等待其它事務(wù)結(jié)束后才執(zhí)行,一般的SELECT查詢則不受影響。
執(zhí)行事務(wù)時關(guān)鍵字select…for update會鎖定數(shù)據(jù),防止其他事務(wù)更改數(shù)據(jù)。但是鎖定數(shù)據(jù)也是有規(guī)則的。
查詢條件與鎖定范圍:
1、具體的主鍵值為查詢條件
比如查詢條件為主鍵ID=1等等,如果此條數(shù)據(jù)存在,則鎖定當(dāng)前行數(shù)據(jù),如果不存在,則不鎖定。
2、不具體的主鍵值為查詢條件
比如查詢條件為主鍵ID1等等,此時會鎖定整張數(shù)據(jù)表。
3、查詢條件中無主鍵
會鎖定整張數(shù)據(jù)表。
4、如果查詢條件中使用了索引為查詢條件
明確指定索引并且查到,則鎖定整條數(shù)據(jù)。如果找不到指定索引數(shù)據(jù),則不加鎖。
悲觀鎖的確保了數(shù)據(jù)的安全性,在數(shù)據(jù)被操作的時候鎖定數(shù)據(jù)不被訪問,但是這樣會帶來很大的性能問題。因此悲觀鎖在實(shí)際開發(fā)中使用是相對比較少的。
mysql的樂觀鎖:
相對悲觀鎖而言,樂觀鎖假設(shè)數(shù)據(jù)一般情況下不會造成沖突,所以在數(shù)據(jù)進(jìn)行提交更新的時候,才會對數(shù)據(jù)的沖突與否進(jìn)行檢測,如果發(fā)現(xiàn)沖突,則讓返回用戶錯誤的信息,讓用戶決定如何去做。
一般來說,實(shí)現(xiàn)樂觀鎖的方法是在數(shù)據(jù)表中增加一個version字段,每當(dāng)數(shù)據(jù)更新的時候這個字段執(zhí)行加1操作。這樣當(dāng)數(shù)據(jù)更改的時候,另外一個事務(wù)訪問此條數(shù)據(jù)進(jìn)行更改的話就會操作失敗,從而避免了并發(fā)操作錯誤。當(dāng)然,還可以將version字段改為時間戳,不過原理都是一樣的。
例如有表student,字段:
id,name,version
1 a 1
當(dāng)事務(wù)一進(jìn)行更新操作:update student set name='ygz' where id = #{id} and version = #{version};
此時操作完后數(shù)據(jù)會變?yōu)閕d = 1,name = ygz,version = 2,當(dāng)另外一個事務(wù)二同樣執(zhí)行更新操作的時候,卻發(fā)現(xiàn)version != 1,此時事務(wù)二就會操作失敗,從而保證了數(shù)據(jù)的正確性。
悲觀鎖和樂觀鎖都是要根據(jù)具體業(yè)務(wù)來選擇使用,本文僅作簡單介紹。
樂觀鎖與悲觀鎖不同的是,它是一種邏輯上的鎖,而不需要數(shù)據(jù)庫提供鎖機(jī)制來支持
當(dāng)數(shù)據(jù)很重要,回滾或重試一次需要很大的開銷時,需要保證操作的ACID性質(zhì),此時應(yīng)該采用悲觀鎖
而當(dāng)數(shù)據(jù)對即時的一致性要求不高,重試一次不太影響整體性能時,可以采用樂觀鎖來保證最終一致性,同時有利于提高并發(fā)性
通常,樂觀鎖采用版本號/時間戳的形式實(shí)現(xiàn):給數(shù)據(jù)額外增加一個版本號字段進(jìn)行控制;更新時,若提交的數(shù)據(jù)所帶的版本號與當(dāng)前記錄的版本號一致,則允許變更執(zhí)行并更新版本號;若不一致,則意味著產(chǎn)生沖突,根據(jù)業(yè)務(wù)需求直接丟棄并返回失敗,或者嘗試合并
在MySQL的實(shí)踐中,常見的一種使用樂觀鎖的方法,是在需要使用樂觀鎖的表中,新增一個version字段
例如:
create table product_amount (
id int not null primary key auto_increment,
product_name varchar(64) not null,
selling_amount int not null,
storing_amount int not null,
version int not null
);
當(dāng)需要更新銷售中的商品數(shù)量(selling_amount)時,使用如下的SQL語句:
update product_amount set selling_amount = #{selling_amount}, version = #{new_version} where id=#{id} and version = #{old_version};
若該語句返回1,則表示更新成功;若返回0,則表示前后的version不一致,產(chǎn)生沖突,更新失敗
對于更新倉庫中的商品數(shù)據(jù)(storing_amount)時,也是同理
不過,這樣為每行記錄都統(tǒng)一設(shè)置一個version字段的樂觀鎖方式,存在一個問題:上例中,如果同時需要單獨(dú)對selling_amount及storing_amount進(jìn)行update(兩條SQL語句分別單獨(dú)執(zhí)行),那么后執(zhí)行的一條會因?yàn)橄葓?zhí)行的一條更新了version字段而失敗,而這種失敗顯然是沒有必要的,白白浪費(fèi)了開銷
一種比較好的方式是為每個需要樂觀鎖的字段單獨(dú)設(shè)置版本號,例如對上例的改造:
create table product_amount (
id int not null primary key auto_increment,
product_name varchar(64) not null,
selling_amount int not null,
selling_version int not null,
storing_amount int not null,
storing_version int not null
);
selling_amount和storing_amount分別擁有自己的樂觀鎖版本號(selling_version和storing_version),更新時分別只關(guān)注自己的版本號,這樣就不會因?yàn)榘姹咎柋黄渌侄涡薷亩。岣吡瞬l(fā)性
以上不是重點(diǎn),重點(diǎn)是 對事務(wù)控制語句不熟悉。
SAVEPOINT identifier : 在事務(wù)中 創(chuàng)建保存點(diǎn)。一個事務(wù)中 允許有多個保存點(diǎn)。
RELEASE SAVEPOINT identifier : 刪除保存點(diǎn)。當(dāng)事務(wù)中 沒有指定的 保存點(diǎn),執(zhí)行該語句 會拋異常。
ROLLBACK TO identifier : 把事務(wù)回滾到 保存點(diǎn)。
Say you have a table T with a column C with one row in it, say it has the value '1'. And consider you have a simple task like following:
That is a simple task that issue two reads from table T, with a delay of 1 minute between them.
If you follow the logic above you can quickly realize that SERIALIZABLE transactions, while they may make life easy for you, are always completely blocking every possible concurrent operation, since they require that nobody can modify, delete nor insert any row. The default transaction isolation level of the .Net System.Transactions scope is serializable, and this usually explains the abysmal performance that results.
在Repeatable Read隔離級別下,一個事務(wù)可能會遇到幻讀(Phantom Read)的問題。
幻讀是指,在一個事務(wù)中,第一次查詢某條記錄,發(fā)現(xiàn)沒有,但是,當(dāng)試圖更新這條不存在的記錄時,竟然能成功,并且,再次讀取同一條記錄,它就神奇地出現(xiàn)了。
我們?nèi)匀幌葴?zhǔn)備好students表的數(shù)據(jù):
然后,分別開啟兩個MySQL客戶端連接,按順序依次執(zhí)行事務(wù)A和事務(wù)B:
事務(wù)B在第3步第一次讀取id=99的記錄時,讀到的記錄為空,說明不存在id=99的記錄。隨后,事務(wù)A在第4步插入了一條id=99的記錄并提交。事務(wù)B在第6步再次讀取id=99的記錄時,讀到的記錄仍然為空,但是,事務(wù)B在第7步試圖更新這條不存在的記錄時,竟然成功了,并且,事務(wù)B在第8步再次讀取id=99的記錄時,記錄出現(xiàn)了。
可見,幻讀就是沒有讀到的記錄,以為不存在,但其實(shí)是可以更新成功的,并且,更新成功后,再次讀取,就出現(xiàn)了。
在沖突較少的情況下,使用樂觀鎖。樂觀鎖 因?yàn)闆]有 加鎖 釋放鎖,也減少了 加鎖 釋放鎖的開銷。
沖突較多時,如果使用樂觀鎖 需要不停地嘗試,所以 使用悲觀鎖。
如果樂觀鎖 進(jìn)行嘗試時 的花銷較大,也是使用悲觀鎖。