自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

隱藏了兩年的Bug,終于連根拔起,悲觀鎖并沒有那么簡單

開發(fā) 前端
接手的新項(xiàng)目,接二連三的出現(xiàn)賬不平的問題,作為程序員中比較執(zhí)著的人,不解決誓不罷休。最終,經(jīng)過兩次,歷時多日終于將其連根拔起。實(shí)屬不易,特寫篇文章記錄一下。

接手的新項(xiàng)目,接二連三的出現(xiàn)賬不平的問題,作為程序員中比較執(zhí)著的人,不解決誓不罷休。最終,經(jīng)過兩次,歷時多日終于將其連根拔起。實(shí)屬不易,特寫篇文章記錄一下。

文章中不僅會講到使用悲觀鎖踩到的坑,以及本人是如何排查問題的,某些思路和方法或許能對大家有所幫助。

事情的起源

運(yùn)營同事時不時就提出查賬調(diào)賬的需求,原因很簡單,賬不平,不查不行。如果你有過財務(wù)相關(guān)系統(tǒng)的工作經(jīng)歷,賬務(wù)問題始終是最難攻克的。

雖然剛接手項(xiàng)目,雖然很多業(yè)務(wù)邏輯還不了解,但出現(xiàn)這樣的技術(shù)挑戰(zhàn),還是要堅決攻克的。

其實(shí),這類問題的原因很簡單:熱點(diǎn)賬戶。當(dāng)很多服務(wù)或線程操作同一個用戶的賬戶時,就會出現(xiàn)一個更新把另外一個更新覆蓋掉的情況。

賬戶不平

上圖可輕易看出,當(dāng)兩個服務(wù)或線程同時查詢數(shù)據(jù)庫的一條數(shù)據(jù)(熱點(diǎn)賬戶),然后內(nèi)存中做修改,最后更新到數(shù)據(jù)庫。如果出現(xiàn)并發(fā)情況,兩個線程都讀取了100,一個計算得80,一個計算得60,后更新的就有可能將前面的覆蓋掉。

解決方案通常有:

  • 單服務(wù)線程鎖;
  • 集群分布式鎖;
  • 集群數(shù)據(jù)庫悲觀鎖;

項(xiàng)目中已采用了悲觀鎖,就基于來進(jìn)行排查追蹤原因。

何謂悲觀鎖

悲觀鎖是在對數(shù)據(jù)被的修改持悲觀態(tài)度,在整個數(shù)據(jù)處理過程中會將數(shù)據(jù)鎖定。

悲觀鎖的實(shí)現(xiàn),往往依靠數(shù)據(jù)庫提供的鎖機(jī)制(也只有數(shù)據(jù)庫層提供的鎖機(jī)制才能真正保證數(shù)據(jù)訪問的排他性,否則,即使在應(yīng)用層中實(shí)現(xiàn)了加鎖機(jī)制,也無法保證外部系統(tǒng)不會修改數(shù)據(jù))。

通常會使用select ... for update語句來實(shí)現(xiàn)對數(shù)據(jù)的枷鎖。

for update僅適用于InnoDB,且必須在事務(wù)塊(BEGIN/COMMIT)中才能生效。在進(jìn)行事務(wù)操作時,通過“for update”語句,MySQL會對查詢結(jié)果集中每行數(shù)據(jù)都添加排他鎖,其他線程對該記錄的更新與刪除操作都會阻塞。排他鎖包含行鎖、表鎖。

如下示例展示了悲觀鎖的基本使用流程:

  1. set autocommit=0;   
  2. //設(shè)置完autocommit后,執(zhí)行正常業(yè)務(wù)。具體如下: 
  3. //0.開始事務(wù) 
  4. begin;/begin work;/start transaction; (三者選一就可以) 
  5. //1.查詢出商品信息 
  6. select status from t_goods where id=1 for update
  7. //2.根據(jù)商品信息生成訂單 
  8. insert into t_orders (id,goods_id) values (null,1); 
  9. //3.修改商品status為2 
  10. update t_goods set status=2; 
  11. //4.提交事務(wù) 
  12. commit;/commit work

因?yàn)殛P(guān)閉了數(shù)據(jù)庫自動提交,這里通過begin/commit來管理事務(wù)。

使用select…for update的方式通過數(shù)據(jù)庫實(shí)現(xiàn)了悲觀鎖。其中,id為1的那條數(shù)據(jù)就被鎖定,其它的事務(wù)必須等本次事務(wù)提交之后才能執(zhí)行。這樣就保證了在操作期間數(shù)據(jù)不會被其它事務(wù)修改。

原因初步分析

在了解了賬不平的原因和悲觀鎖的基本原理之后,就可以進(jìn)行問題的排查了。既然系統(tǒng)已經(jīng)使用了悲觀鎖,竟然還會出現(xiàn)問題,那肯定是哪里漏掉了什么。

于是,排查了所有賬戶(account表)更新的地方,還真找到一處bug。

大多數(shù)地方都使用了悲觀鎖,先for update查詢一下,然后計算新的余額,再進(jìn)行更新數(shù)據(jù)庫。但有一處竟然先查詢到了計算了余額,然后再進(jìn)行加鎖,最后更新。

基本流程如下:

錯誤加鎖

在上述情況中,雖然線程B進(jìn)行了加鎖處理,但由于計算新余額并未在鎖中,導(dǎo)致雖然使用了悲觀鎖,但依舊存在問題。正確的使用方式就是將計算余額的邏輯放在鎖中。

當(dāng)然,如果線程B完全被遺忘加鎖了,也會出現(xiàn)同樣的問題。

在排查解決了上述bug,我開始嘚瑟了,以為徹底解決了賬不平的問題。

一個月之后

結(jié)果一個月之后,運(yùn)營同事又來找了,偶爾依舊會出現(xiàn)賬不平的問題。剛開始我還以為是不是搞錯了,歷史的賬不平導(dǎo)致現(xiàn)在最終的不平。但最終還是下定決心再排查一次。

第一天,把賬不平的賬戶的賬務(wù)流水、涉及到代碼、日志全部捋一遍。這期間還遇到了很多小困難,最終注意克服。

困難一:數(shù)據(jù)查不動

賬務(wù)記錄表數(shù)據(jù)太多,上千萬的數(shù)據(jù),最初的設(shè)計者并沒有創(chuàng)建索引。這就要了老命了,根據(jù)篩選條件根本查不出數(shù)據(jù)來。

這里就用到SQL優(yōu)化的兩個技能點(diǎn):limit限制查詢條數(shù)和高效的分頁策略。

關(guān)于limit限制查詢條件這一點(diǎn)很明顯,不僅減少了結(jié)果集,而且在遇到符合條件的數(shù)據(jù)之后會立馬返回。

高效的分頁策略在列表頁在查詢數(shù)據(jù)經(jīng)常遇到,為了避免一次性返回過多的數(shù)據(jù)影響接口性能,一般會對查詢接口做分頁處理。

在Mysql中分頁一般用的limit關(guān)鍵字:

  1. select id,name,age from user limit 10,20; 

少量數(shù)據(jù)時,limit分頁沒啥問題。但如果表中數(shù)據(jù)量很多,就會出現(xiàn)性能問題。

比如分頁參數(shù)變成了:

  1. select id,name,age from user limit 1000000,20; 

Mysql會查到1000020條數(shù)據(jù),然后丟棄前面的1000000條,只查后面的20條數(shù)據(jù),非常浪費(fèi)資源。

優(yōu)化sql:

  1. select id,name,age from user where id > 1000000 limit 20; 

當(dāng)然還可以使用between優(yōu)化分頁:

  1. select id,name,age 
  2.  
  3. from user where id between 1000000 and 1000020; 

值得慶幸的是那張表的ID是自增的,于是用了id大于的條件,只差了最近的交易記錄,才勉強(qiáng)把數(shù)據(jù)查詢出來。

困難二:日志過多

由于系統(tǒng)日志打的比較詳細(xì),一個項(xiàng)目每天大概幾個G的日志。要在這中間查詢到有用的日志,也是一個調(diào)整。

排查問題時,先使用了grep 命令找到出問題交易的賬號日志:

  1. grep 123 info.log 

當(dāng)大概定位的到日志輸出時間了,再利用區(qū)間縮小日志范圍:

  1. grep '2021-11-17 19:23:23' info.log > temp.log 

這里同樣使用grep命令查找對應(yīng)時間區(qū)間的日志,并將查找到日志輸出到temp.log文件中,然后通過sz命令,下載到本地進(jìn)行篩選分析。

這里大家可以善用grep命令。同時也要善用輸出到新文件,這樣比每次查幾個G的內(nèi)容方便多了。當(dāng)然更方便的就是把篩選之后的日志下載本地,再次比對分析。

其他

關(guān)于代碼篩選這塊,沒有什么訣竅,除了從頭到位的捋一捋,沒有別的好方法。不過這個過程善用IDE的搜索和“Find usages”功能即可。

日終收獲

經(jīng)過上述排查,最終在臨下班時,定位到了問題的原因:一個線程將余額更新之后,另外一個線程將其覆蓋了。在賬務(wù)流水記錄中存在了兩筆緊鄰,且計算前余額一樣的記錄。

得出結(jié)果之后,再排查其他的同類問題就方便多了,比如可采用group by來進(jìn)行快速篩選:

  1. select count(id) as num , balance from account group by balance having num > 1; 

通過上述語句就可以快速查出有同樣計算前余額的記錄。當(dāng)然,上述語句還可以添加條件和結(jié)果維度。

雖然找到的問題發(fā)生的地方,但并未完全找到問題的原因。

更深層次的Bug

本以為找到了問題發(fā)生的點(diǎn),就能快速解決問題的,但的確小覷了這個Bug,又是一整天才排查出根本原因。

模擬高并發(fā)

找到出問題的代碼,看了實(shí)現(xiàn)邏輯,沒問題啊,也加了悲觀鎖,數(shù)據(jù)庫事務(wù)也沒失效,也沒有同Service的方法調(diào)用。怎么就會出現(xiàn)問題呢?

既然肉眼看不出來,那就用程序跑。于是,寫了一個單元測試,創(chuàng)建一個線程池,來調(diào)用對應(yīng)加鎖方法。結(jié)果,依舊沒問題。

由于跑的是測試庫,生產(chǎn)庫用的是云服務(wù),擔(dān)心是數(shù)據(jù)庫的差異,于是在Navicat驗(yàn)證了悲觀鎖是否生效:

  1. START transaction ; 
  2.  
  3. select * from account where id = 1 for update

然后在另外一個查詢窗口執(zhí)行:

  1. select * from account where id = 1 for update

發(fā)現(xiàn),數(shù)據(jù)庫的鎖的確是生效的,在沒有執(zhí)行commit操作之前,是查不到數(shù)據(jù)的。

僵局與希望

此時,完全陷入僵局。于是就開始大量搜索資料,多次閱讀代碼。

最終,在一篇寫得很水,但給了一個Hibernate javadoc文檔鏈接的文中,無意點(diǎn)了一下鏈接,獲得了巨大的啟發(fā)。

在javadoc看了一下session實(shí)現(xiàn)悲觀鎖的方法。項(xiàng)目中用了已經(jīng)廢棄的get方法:

get

  1. @Deprecated 
  2. Object get(Class clazz, 
  3.                       Serializable id, 
  4.                       LockMode lockMode) 

**Deprecated.**LockMode parameter should be replaced with LockOptions

Return the persistent instance of the given entity class with the given identifier, or null if there is no such persistent instance. (If the instance is already associated with the session, return that instance. This method never returns an uninitialized instance.) Obtain the specified lock mode if the instance exists.

其中的“If the instance is already associated with the session, return that instance”讓我眼前一亮。難道是緩存在作祟?

上面的重點(diǎn)是:如果session中已經(jīng)存在這么個對象實(shí)例,會直接返回這個實(shí)例。

感覺回去看代碼,還真是的,偽代碼如下:

  1. Account account = accountService.getAccount(type, userNo); 
  2. if(account == null){ 
  3.  //... 
  4. accountService.getAccountAndLock(account.getId()); 
  5. // ... 

上述代碼首先值得肯定的有兩點(diǎn):第一,在加鎖之前先查了一次對象,這樣能避免因?yàn)閷ο蟛淮嬖?,鎖住全表;第二,就是鎖一條數(shù)據(jù)庫記錄時盡量采用id,精確定位到具體的記錄,避免鎖住其他記錄或整張表。

那么,是不是因?yàn)榍懊娴牟樵儗?dǎo)致后面getAccountAndLock方法的實(shí)效呢?再來驗(yàn)證一下。

于是,在單元測試中添加了前面的查詢,再次執(zhí)行。哈哈,Bug終于復(fù)現(xiàn)了!

為了進(jìn)一步證實(shí),在底層的公共方法中添加了clear操作:

  1. public T findAndLock(Class cls, String primaryKey) throws DataAccessException { 
  2.  Session session = getHibernateTemplate().getSessionFactory().getCurrentSession(); 
  3.  // 添加驗(yàn)證是否緩存問題 
  4.  session.clear(); 
  5.  Object object = session.load(cls, primaryKey, LockOptions.UPGRADE); 
  6.  return (T) object; 

再次執(zhí)行單元測試,可正常加鎖。至此,Bug定位完畢。

問題的解決

既然已經(jīng)定位問題,解決起來就非常方便了。上面使用session.clear()只是為了驗(yàn)證,真實(shí)生產(chǎn)使用這種方法影響太大,而且是事后處理。

解決方案:將基于Hibernate的普通查詢,改為基于原生SQL的查詢。因?yàn)榍懊娴钠胀ú樵冎恍枰猧d,那么只用一條SQL查詢ID即可,如果id為空,則不存在;如果id非空,則再進(jìn)行下一步處理。

至此,問題完美解決。

小結(jié)

在解決上述問題的過程中,看似只是很簡單的悲觀鎖,但在排查的過程中還用到和涉及到了大量的其他知識,比如@Transactional事務(wù)失效場景的排查、事務(wù)的隔離級別、Hibernate的多級緩存、Spring的事物管理、多線程、Linux操作、Navicat手動事務(wù)、SQL優(yōu)化、單元測試、Javadoc查閱等。 

所以,在解決問題之后,覺得十分有必要分享給大家。通過這個案例,你又學(xué)到了什么呢?

 

責(zé)任編輯:武曉燕 來源: 程序新視界
相關(guān)推薦

2016-02-15 09:52:21

虛擬現(xiàn)實(shí)

2009-02-19 20:25:34

SunSolaris發(fā)展趨勢

2018-12-18 09:20:06

2022-07-11 12:37:15

安全運(yùn)營網(wǎng)絡(luò)攻擊

2013-01-06 13:45:14

2015-03-25 17:57:50

JavaJava糟糕

2021-03-18 08:08:16

FedoraLogoFedora 社區(qū)

2009-02-17 09:11:42

Unix時間錯誤

2023-08-28 08:24:07

myloaderMySQLGreatSQL

2021-06-15 16:17:19

Commit報錯事務(wù)

2019-10-24 08:25:44

IPv6互聯(lián)網(wǎng)網(wǎng)絡(luò)協(xié)議

2017-01-17 10:41:19

聯(lián)想企業(yè)網(wǎng)盤

2011-09-23 09:42:25

2020-12-14 09:35:20

CentOSRockyLinux

2020-03-24 09:00:32

企業(yè)征信信用風(fēng)險益博睿

2023-05-18 15:00:06

2020-11-04 10:33:19

數(shù)據(jù)

2022-05-06 08:26:21

babel編譯器

2019-12-19 16:46:50

數(shù)據(jù)恢復(fù)軟件云計算技術(shù)

2013-05-06 09:19:36

云應(yīng)用趨勢云服務(wù)云管理工具
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號