一、redis鎖處理冪等性失效
上面代碼中,鎖起不了作用;
——count方法,和insert方法在同一事務(wù)中,事務(wù)中包含鎖,鎖沒有作用,鎖的范圍內(nèi),事務(wù)沒提交,但釋放鎖后,事務(wù)提交前,被另一線程搶了執(zhí)行權(quán)后,因?yàn)槭聞?wù)還沒提交,另一線程拿到的count還是0。
以上代碼問題:
- 對(duì)事物的理解使用有問題,冪等設(shè)計(jì)bug;
- redis鎖使用有問題(單獨(dú)案例講述);
mysql默認(rèn)事務(wù)級(jí)別——可重復(fù)讀;
鎖加錯(cuò)位置了,鎖應(yīng)該加在這個(gè)事務(wù)方法的外面;
正例:
stop the world:
學(xué)會(huì)用stop the world注釋代碼。
1.1 擴(kuò)展:
事務(wù)在生產(chǎn)實(shí)踐中經(jīng)常犯的錯(cuò)誤:
- 事務(wù)范圍:應(yīng)該加入事務(wù)的代碼未加入到事務(wù)中
1.1.1 圖是另一個(gè)真實(shí)生產(chǎn)當(dāng)中的事故-僅供參考:
? IdGenerator 是一個(gè)生成唯一標(biāo)識(shí)符的工具類。它通常用于生成數(shù)據(jù)庫表中的主鍵值,例如AUTO_INCREMENT 字段。
- 事務(wù)大?。菏聞?wù)過大,是否有必要拆解小事務(wù)(如何優(yōu)化),拆解后一致性問題。
傳播范圍(異常標(biāo)注):
- 多線程中不可傳播;
- 多個(gè)方法內(nèi)如果異常被捕獲將要被標(biāo)記為異常事務(wù),不可以再次提交(雖然不影響數(shù)據(jù),但是有報(bào)錯(cuò)信息);
二、Transaction rolled back bacause it has been marked as rollback-only問題原因復(fù)盤
2.1 復(fù)盤
錯(cuò)誤原因:
提交了一個(gè)被標(biāo)記為異常的事務(wù),會(huì)報(bào)這個(gè)錯(cuò)。
解決方法:
- a處try-catch代碼去掉;
- 或者,b處@Transactional注解去掉;
無論是哪種解決方法,具體看業(yè)務(wù)。
三、mysql死鎖場景
- 問題1:jvm如果死鎖了,java進(jìn)程還在嗎?——一直鎖著。
- 問題2:mysql如果死鎖了,其他連接還能正常運(yùn)行嗎?——死鎖一段時(shí)間后會(huì)自動(dòng)釋放,可配置;
3.1 mysql死鎖復(fù)盤
在 MySQL 中,F(xiàn)OR UPDATE 子句用于在讀取數(shù)據(jù)時(shí)鎖定該記錄,以防止其他事務(wù)同時(shí)更新或刪除該記錄。當(dāng)多個(gè)事務(wù)試圖同時(shí)鎖定同一記錄時(shí),可能會(huì)導(dǎo)致死鎖。
下面是一個(gè)可能導(dǎo)致死鎖的場景:
假設(shè)有兩個(gè)事務(wù) T1 和 T2,它們都試圖更新同一行數(shù)據(jù)。
- 事務(wù) T1 執(zhí)行以下操作:
- 讀取一行數(shù)據(jù)并加上 FOR UPDATE 鎖。
- 等待一段時(shí)間(例如,進(jìn)行一些計(jì)算或等待其他資源)。
- 事務(wù) T2 執(zhí)行以下操作:
- 讀取同一行數(shù)據(jù)并加上 FOR UPDATE 鎖。
- 試圖更新該記錄,但由于 T1 已經(jīng)鎖定了該記錄,因此事務(wù) T2 被阻塞。 現(xiàn)在,事務(wù) T1 等待一>段時(shí)間后準(zhǔn)備更新記錄,但由于事務(wù) T2 已經(jīng)鎖定了該記錄,因此事務(wù) T1 也被阻塞。
這就形成了一個(gè)死鎖,因?yàn)閮蓚€(gè)事務(wù)都在等待對(duì)方釋放鎖,而它們都無法繼續(xù)執(zhí)行下去。
為了避免死鎖,可以采取以下措施:
- 盡量減少鎖定的時(shí)間,以避免其他事務(wù)長時(shí)間等待。
- 按照相同的順序訪問數(shù)據(jù),以避免沖突。
mysql死鎖時(shí)間長好還是短好?
——短的話,不好控制長事務(wù);長的話,發(fā)生死鎖時(shí),時(shí)間等待太久;
四、手動(dòng)模擬死鎖
mysql默認(rèn)級(jí)別,可重復(fù)讀(rr)。阿里設(shè)置的級(jí)別:讀已提交(rc);
rr會(huì)有間隙鎖出現(xiàn)死鎖的可能性更大;
五、冪等性設(shè)計(jì)方法
5.1 冪等性設(shè)計(jì):
- 有時(shí)我們?cè)谔顚懩承?form表單 時(shí),保存按鈕不小心快速點(diǎn)了兩次,表中竟然產(chǎn)生了兩條重復(fù)的數(shù)據(jù),只是id不一樣;
- 我們?cè)陧?xiàng)目中為了解決 接口超時(shí) 問題,通常會(huì)引入了 重試機(jī)制 。第一次請(qǐng)求接口超時(shí)了,請(qǐng)求方?jīng)]能及時(shí)獲取返回結(jié)果(此時(shí)有可能已經(jīng)成功了),為了避免返回錯(cuò)誤的結(jié)果(這種情況不可能直接返回失敗吧?),于是會(huì)對(duì)該請(qǐng)求重試幾次,這樣也會(huì)產(chǎn)生重復(fù)的數(shù)據(jù);
- mq消費(fèi)者在讀取消息時(shí),有時(shí)候會(huì)讀取到 重復(fù)消息 ,如果處理不好,也會(huì)產(chǎn)生重復(fù)的數(shù)據(jù);
這些都是冪等性問題。
接口冪等性: 是指用戶對(duì)于同一操作發(fā)起的一次請(qǐng)求或者多次請(qǐng)求的結(jié)果是一致的,不會(huì)因?yàn)槎啻吸c(diǎn)擊而產(chǎn)生了副作用。
這類問題多發(fā)于接口的:
- insert 操作,這種情況下多次請(qǐng)求,可能會(huì)產(chǎn)生重復(fù)數(shù)據(jù);
- update 操作,如果只是單純的更新數(shù)據(jù),比如: update user set status=1 where id=1 ,是沒有問題的。如果還有計(jì)算,比如: update user set status=status+1where id=1 ,這種情況下多次請(qǐng)求,可能會(huì)導(dǎo)致數(shù)據(jù)錯(cuò)誤;
那么我們要如何保證接口冪等性?請(qǐng)往下看。
5.1.1 insert前先select
?通常情況下,在保存數(shù)據(jù)的接口中,我們?yōu)榱朔乐巩a(chǎn)生重復(fù)數(shù)據(jù),一般會(huì)在 insert 前,先根據(jù) name 或 code 字段 select 一下數(shù)據(jù)。如果該數(shù)據(jù)已存在,則執(zhí)行 update 操作,如果不存在,才執(zhí)行 insert 操作。
?該方案可能是我們平時(shí)在防止產(chǎn)生重復(fù)數(shù)據(jù)時(shí),使用最多的方案。但是該方案不適用于并發(fā)場景,在并發(fā)場景中,要配合其他方案一起使用,否則同樣會(huì)產(chǎn)生重復(fù)數(shù)據(jù)。
5.1.2 加悲觀鎖
5.1.2.1 支付場景
?支付場景在加減庫存場景中,用戶A的賬號(hào)余額有150元,想轉(zhuǎn)出100元,正常情況下用戶A的余額只剩50元。一般情況下,sql是這樣的:
update user amount = amount-100 where id=123;
?如果出現(xiàn)多次相同的請(qǐng)求,可能會(huì)導(dǎo)致用戶A的余額變成負(fù)數(shù)。這種情況,用戶A來可能要哭了。于此同時(shí),系統(tǒng)開發(fā)人員可能也要哭了,因?yàn)檫@是很嚴(yán)重的系統(tǒng)bug。
?為了解決這個(gè)問題,可以加悲觀鎖,將用戶A的那行數(shù)據(jù)鎖住,在同一時(shí)刻只允許一個(gè)請(qǐng)求獲得
鎖,更新數(shù)據(jù),其他的請(qǐng)求則等待。
通常情況下通過如下sql鎖住單行數(shù)據(jù):
select * from user id=123 for update;
條件:數(shù)據(jù)庫引擎為innoDB
操作位于事務(wù)中
具體流程如下:
具體步驟:
- 多個(gè)請(qǐng)求同時(shí)根據(jù)id查詢用戶信息。
- 判斷余額是否不足100,如果余額不足,則直接返回余額不足。
- 如果余額充足,則通過for update再次查詢用戶信息,并且嘗試獲取鎖。
- 只有第一個(gè)請(qǐng)求能獲取到行鎖,其余沒有獲取鎖的請(qǐng)求,則等待下一次獲取鎖的機(jī)會(huì)。
- 第一個(gè)請(qǐng)求獲取到鎖之后,判斷余額是否不足100,如果余額足夠,則進(jìn)行update操作。
- 如果余額不足,說明是重復(fù)請(qǐng)求,則直接返回成功。
5.1.2.1 操作庫場景
select* from stock_info where goods_id=12312 and storage_id=1 for update;
具體流程:
a:單件貨品操作流程:
b:(同一個(gè)goodsId)多個(gè)單件貨品,批量操作出庫流程:
具體步驟:
- 多個(gè)請(qǐng)求同時(shí)根據(jù)goodsId和storageId操作貨品的上下架,或者其他渠道訂單批量下架操作;
- 判斷當(dāng)前貨品是否有倉庫貨品;
- 如果貨品庫存充足,則通過for update再次查詢貨品庫存信息,并且嘗試獲取鎖;
- 只有第一個(gè)請(qǐng)求能獲取到行鎖,其余沒有獲取鎖的請(qǐng)求,則等待下一次獲取鎖的機(jī)會(huì);
- 第一個(gè)請(qǐng)求獲取到鎖之后,進(jìn)行貨品單件明細(xì)狀態(tài)變更,成功后操作,則進(jìn)行update操作加減庫存;
- 如果庫存不足或者單件不滿足操作,則直接返回成功或者冪等狀態(tài)。
?需要特別注意的是:如果使用的是mysql數(shù)據(jù)庫,存儲(chǔ)引擎必須用innodb,因?yàn)樗胖С质?務(wù)。此外,這里id字段一定要是主鍵或者唯一索引,不然會(huì)鎖住整張表。
?悲觀鎖需要在同一個(gè)事務(wù)操作過程中鎖住一行數(shù)據(jù),如果事務(wù)耗時(shí)比較長,會(huì)造成大量的請(qǐng)求等待,影響接口性能。此外,每次請(qǐng)求接口很難保證都有相同的返回值,所以不適合冪等性設(shè)計(jì)場景,但是在防重場景中是可以的使用的。在這里順便說一下, 防重設(shè)計(jì) 和冪等設(shè)計(jì) ,其實(shí)是有區(qū)別的。防重設(shè)計(jì)主要為了避免產(chǎn)生重復(fù)數(shù)據(jù),對(duì)接口返回沒有太多要求。而冪等設(shè)計(jì)除了避免產(chǎn)生重復(fù)數(shù)據(jù)之外,還要求每次請(qǐng)求都返回一樣的結(jié)果。
5.1.3 加樂觀鎖
?既然悲觀鎖有性能問題,為了提升接口性能,我們可以使用樂觀鎖。需要在表中增加一個(gè)timestamp 或者 version 字段,這里以 version 字段為例。
在更新數(shù)據(jù)之前先查詢一下數(shù)據(jù):
select id,amount,version from user id=123;
中間就省略了,相信大家也知道。直接貼出sql中的樂觀鎖代碼了:
update user set amount=amount+100,version=version+1where id=123 and version=1;
需要注意的是,如果影響行數(shù)為0:
?該 update 操作不會(huì)真正更新數(shù)據(jù),最終sql的執(zhí)行結(jié)果影響行數(shù)是 0 ,因?yàn)?version 已經(jīng)變成 2了, where
中的 version=1 肯定無法滿足條件。但為了保證接口冪等性,接口可以直接返回成功,因?yàn)?version
值已經(jīng)修改了,那么前面必定已經(jīng)成功過一次,后面都是重復(fù)的請(qǐng)求。
具體步驟:
- 先根據(jù)id查詢用戶信息,包含version字段;
- 根據(jù)id和version字段值作為where條件的參數(shù),更新用戶信息,同時(shí)version+1;
- 判斷操作影響行數(shù),如果影響1行,則說明是一次請(qǐng)求,可以做其他數(shù)據(jù)操作;
- 如果影響0行,說明是重復(fù)請(qǐng)求,則直接返回成功;
5.1.4 加唯一索引
?常規(guī)的創(chuàng)建唯一索引,和唯一聯(lián)合索引的思路就不寫了。
5.1.4.1 軟刪除可能引發(fā)的問題:
?在很多業(yè)務(wù)場景中,都使用“軟刪除”即使用flag或is_deleted等字段表示記錄是否被刪除,這種方式能很好地保存“歷史記錄”,但由于”歷史記錄”的存在,導(dǎo)致無法在表上建立唯一索引,需要通過程序來控制”數(shù)據(jù)唯一性”,其中一種程序?qū)崿F(xiàn)邏輯就是“先嘗試更新,更新失敗則插入”,該方式在高并發(fā)下死鎖頻發(fā)。(select for update ;為什么?你能復(fù)現(xiàn)么?如何避免?)
?盡管可以通過程序來控制”數(shù)據(jù)唯一性”,但仍建議使用數(shù)據(jù)庫級(jí)別的唯一約束來確保數(shù)據(jù)在表級(jí)別的”唯一”,對(duì)于”硬刪除”方式,直接在唯一索引列上建立為唯一索引即可,對(duì)于”軟刪除”方式,可以通過 復(fù)合索引 方式來處理。
?假設(shè)當(dāng)前有訂單相關(guān)的表tb_order_worker,表中有order_id字段需要唯一約束,使用is_delete字段來標(biāo)識(shí)記錄是否被”軟刪除”,is_delete=1時(shí)表示記錄被刪除,is_delete=0時(shí)表示記錄未被刪除,需要控制滿足is_delete=0時(shí)的記錄中order_id唯一,如果對(duì)(order_id,is_delete)的建唯一索引,那么當(dāng)同一訂單被多次”軟刪除”時(shí)就會(huì)出現(xiàn)唯一索引沖突的問題。
解決方式一:
?提升is_delete列的取值范圍,當(dāng)is_delete=0時(shí)表示記錄有效,當(dāng)is_delete>0時(shí)表示記錄被刪除,在刪除記錄時(shí)將is_delete值設(shè)置為不同數(shù)值,只要確保相同order_id的記錄使用不同數(shù)值即可(很多表都使用自增主鍵,可以取自增主鍵的值來作為is_delete值)。
解決方式二:
?新增列order_rid來保持方式一中is_delete的原有取值范圍,當(dāng)is_delete時(shí)設(shè)置order_rid=0,當(dāng)is_delete=1時(shí)設(shè)置order_rid為任意非0值,只要確保相同order_id的記錄使用不同值即可(同樣建議參照自增主鍵值來設(shè)置),然后對(duì)(order_id,yn,order_rid)建唯一索引。
5.1.4.2 唯一索引和普通索引的區(qū)別?
5.1.4.2.1 查詢
select * from t_user where id_card =1000;
- 對(duì)于普通索引來說,查找到滿足條件的第一個(gè)記錄(1,1000)后,需要查找下一個(gè)記錄,直到碰到第一個(gè)不滿足id_card=1000條件的 記錄;
- 對(duì)于唯一索引來說,由于索引定義了唯一性,查找到第一個(gè)滿足條件的記錄后,就會(huì)停止繼續(xù)檢索。
?性能差距微乎其微,因?yàn)閙ysql 數(shù)據(jù)是按照數(shù)據(jù)頁為單位的,也就是說,當(dāng)讀取一條數(shù)據(jù)的時(shí)候,會(huì)將當(dāng)前數(shù)據(jù)所在頁都讀入到內(nèi)存,普通索引無非多了一次判斷是否等于 的操作,相當(dāng)于指針的尋找和一次計(jì)算,當(dāng)然,如果該頁碼上,id_card=1000是最后一個(gè)數(shù)據(jù),那么就需要取下一個(gè)頁了,但是這種概率并不大。
?總結(jié)說,查詢上,普通索引和唯一索引性能是沒什么差異的。
5.1.4.2.2 更新
?當(dāng)需要更新一個(gè)數(shù)據(jù)頁時(shí),如果數(shù)據(jù)頁在內(nèi)存中就直接更新,而如果這個(gè)數(shù)據(jù)頁還沒有在內(nèi)存中的話,在不影響數(shù)據(jù)一致 性的前提下,InooDB會(huì)將這些更新操作緩存在change buffer中,這樣就不需要從磁盤中讀入這個(gè)數(shù)據(jù)頁了。在下次查詢 需要訪問這個(gè)數(shù)據(jù)頁的時(shí)候,將數(shù)據(jù)頁讀入內(nèi)存,然后執(zhí)行change buffer中與這個(gè)頁有關(guān)的操作。通過這種方式就能保證 這個(gè)數(shù)據(jù)邏輯的正確性。
這個(gè)change buffer通常被稱為InnoDB的寫緩沖?
?在MySQL5.5之前,叫插入緩沖(insert buffer),只針對(duì)insert做了優(yōu)化;現(xiàn)在對(duì)delete和update也有效,叫做寫緩沖(change buffer)。 它是一種應(yīng)用在非唯一普通索引頁(non-unique secondary index page)不在緩沖池中,對(duì)頁進(jìn) 行了寫操作,并不會(huì)立刻將磁盤頁加載到緩沖池,而僅僅記錄緩沖變更(buffer changes),等未來數(shù)據(jù)被讀取時(shí),再將數(shù)據(jù)合并(merge)恢復(fù)到緩沖池中的技術(shù)。
寫緩沖的目的是降低寫操作的磁盤IO,提升數(shù)據(jù)庫性能:
? 對(duì)于唯一索引來說,所有的更新操作都要先判斷這個(gè)操作是否違反唯一性約束。比如,要插入 (1,1000)這個(gè)記錄,就要先判 斷現(xiàn)在表中是否已經(jīng)存在id_card=1000的記錄,而這必須要將數(shù)據(jù) 頁讀入內(nèi)存才能判斷。如果都已經(jīng)讀入到內(nèi)存了,那直接更新內(nèi)存會(huì)更快,就沒必要使用change buffer了。 因此,唯一索引的更新就不能使用change buffer,實(shí)際上也只有普通索引可以使用。
接著分析InnoDB更新流程:
處理流程如下:
- 對(duì)于唯一索引來說,找到999和1001之間的位置,判斷到?jīng)]有沖突,插入這個(gè)值,語句執(zhí)行結(jié) 束;
- 對(duì)于普通索引來說,找到999和1001之間的位置,插入這個(gè)值,語句執(zhí)行結(jié)束。
這樣看來,普通索引和唯一索引對(duì)更新語句性能影響的差別,只是一個(gè)判斷,只會(huì)耗費(fèi)微小的 CPU時(shí)間。
真正影響性能的是第二種情況是,這個(gè)記錄要更新的目標(biāo)頁不在內(nèi)存中。處理流程如下:
- 對(duì)于唯一索引來說,需要將數(shù)據(jù)頁讀入內(nèi)存,判斷到?jīng)]有沖突,插入這個(gè)值,語句執(zhí)行結(jié)束;
- 對(duì)于普通索引來說,則是將更新記錄在change buffer,語句執(zhí)行就結(jié)束了。
5.1.4.2.3 總結(jié)
?將數(shù)據(jù)從磁盤讀入內(nèi)存涉及隨機(jī)IO的訪問,是數(shù)據(jù)庫里面成本最高的操作之一。change buffer因?yàn)闇p少了隨機(jī)磁盤訪問, 所以對(duì)更新性能的提升是會(huì)很明顯的。
?因此,對(duì)于寫多讀少的業(yè)務(wù)來說,頁面在寫完以后馬上被訪問到的概率比較小,此時(shí)change buffer的使用效果最好。
?這種 業(yè)務(wù)模型常見的就是賬單類、日志類的系統(tǒng)。
?反過來,假設(shè)一個(gè)業(yè)務(wù)的更新模式是寫入之后馬上會(huì)做查詢,那么即使?jié)M足了條件,將更新先記錄在change buffer,但之 后由于馬上要訪問這個(gè)數(shù)據(jù)頁,會(huì)立即觸發(fā)merge過程。這樣隨機(jī)訪問IO的次數(shù)不會(huì)減少,反而增加了change buffer的維 護(hù)代價(jià)。所以,對(duì)于這種業(yè)務(wù)模式來說,change buffer反而起到了副作用。
?redo log主要節(jié)省的是隨機(jī)寫磁盤的IO消耗(轉(zhuǎn)成 順序?qū)懀?,而change buffer主要節(jié)省的則是隨機(jī)讀磁盤的IO消耗。
5.1.4.2.4 Change buffer為什么只對(duì)非唯一普通索引頁有效
-
主鍵索引,唯一索引
實(shí)際上對(duì)于【唯一索引】的更新,插入操作都會(huì)先判斷當(dāng)前操作是否違反唯一性約束,而這個(gè)操作就必須要將索引頁讀取到內(nèi)存中,此時(shí)既然已經(jīng)讀取到內(nèi)存了,那直接更新即可,沒有需要在用Change buffer了。 -
非唯一普通索引
不需要判斷當(dāng)前操作是否違反唯一性約束,也就不需要將數(shù)據(jù)頁讀取到內(nèi)存,因此可以直接使用 change buffer 更新。
基于此,Change buffer只有對(duì)普通索引可以使用,對(duì)唯一索引的更新無法生效。
change buffer參考文章:MySQL十七:Change Buffer
六、mysql 一頁大約能存多少個(gè)索引
MySQL 中,一頁的大小通常是 16KB,其中大約能存 1000 個(gè)索引。文章來源:http://www.zghlxwxcb.cn/news/detail-738113.html
? 在 MySQL 中,索引的大小取決于多種因素,例如索引的數(shù)據(jù)類型、索引列的數(shù)量、數(shù)據(jù)行的大小等等。通常情況下,一個(gè) B 樹索引的大小可以通過以下公式來估算:
?索引大小 = 索引列數(shù) * 每個(gè)列的平均字節(jié)數(shù) + 每個(gè)索引節(jié)點(diǎn)的開銷其中,每個(gè)索引節(jié)點(diǎn)的開銷通常是固定的,大約為 16 字節(jié)。
例如,如果一個(gè)索引有 4 個(gè)列,每個(gè)列的平均字節(jié)數(shù)為 10 字節(jié),那么該索引的大小大約為:
4 * 10 + 16 = 56 字節(jié)
?需要注意的是,這只是一個(gè)粗略的估算值,實(shí)際的索引大小可能會(huì)因?yàn)閿?shù)據(jù)的分布、數(shù)據(jù)行的大小等因素而有所不同。此外,不>同類型的索引(如 B 樹索引、哈希索引等)的大小也會(huì)有所不同。在實(shí)際應(yīng)用中,為了獲得最佳的性能和存儲(chǔ)效率,需要根據(jù)>具體情況進(jìn)行調(diào)整和優(yōu)化。文章來源地址http://www.zghlxwxcb.cn/news/detail-738113.html
到了這里,關(guān)于冪等性設(shè)計(jì),及案例分析的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!