<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          面試官:MySQL如何實(shí)現(xiàn)分布式讀寫鎖?

          共 13151字,需瀏覽 27分鐘

           ·

          2021-03-06 01:03


          1、先看個(gè)業(yè)務(wù)場景

          對 X 資源,可以執(zhí)行 2 種操作:W 操作、R 操作,2 種操作需要滿足下面條件

          (1)、執(zhí)行操作的機(jī)器分布式在不同的節(jié)點(diǎn)中,也就是分布式的

          (2)、W 操作是獨(dú)享的,也就是說同一時(shí)刻只允許有一個(gè)操作者對 X 執(zhí)行 W 操作

          (3)、R 操作是共享的,也就是說同時(shí)可以有多個(gè)執(zhí)行者對 X 資源執(zhí)行 R 操作

          (4)、W 操作和 R 操作是互斥的,什么意思呢?也就是說 W 操作和 R 操作不能同時(shí)存在

          通俗點(diǎn)說:

          如果當(dāng)前 W 操作正在執(zhí)行,此時(shí)有 R 操作請求過來,那么這個(gè) R 請求只能等待或者執(zhí)行失敗

          如果前有 R 操作正在執(zhí)行,此時(shí)有 W 操作請求過來,那么這個(gè) W 請求只能等待或者執(zhí)行失敗。

          這種業(yè)務(wù)場景如果是單臺(tái)虛擬機(jī),在 java 中可以使用 ReadWriteLock 讀寫鎖就可以實(shí)現(xiàn)了,但是今天我們要討論的是操作者不在同一個(gè) jvm 中,而是分布式在不同的節(jié)點(diǎn),服務(wù)中。

          大家可能在思考,哪里有這樣的業(yè)務(wù)場景?

          我之前做過 p2p,這里給大家舉個(gè) p2p 中的例子。

          可能大家對 p2p 不了解,這里先介紹一下 p2p 的業(yè)務(wù)。

          比如小明需要 10 萬買車,但是手頭上沒錢,此時(shí)可以在 p2p 平臺(tái)上申請一個(gè) 10 萬的借款,然后 p2p 平臺(tái)會(huì)發(fā)布一個(gè)借款項(xiàng)目,開始募集資金。

          其他網(wǎng)民可以去投資這個(gè)項(xiàng)目,每個(gè)月借款人會(huì)進(jìn)行還款,投資人會(huì)拿到收益。

          當(dāng)投資人每次投資的時(shí)候,會(huì)產(chǎn)生一份債權(quán),可以把債權(quán)理解為借款人欠你錢的一個(gè)憑證。

          如果投資人急著用錢,但是此時(shí)投資還未到期,此時(shí)你可以發(fā)起債權(quán)轉(zhuǎn)讓,將你的債權(quán)賣給給其他人,這樣你就可以及時(shí)拿到本金了。

          這里面涉及到 2 個(gè)關(guān)鍵的業(yè)務(wù):借款人執(zhí)行還款、投資人發(fā)起債權(quán)轉(zhuǎn)讓

          借款人還款:借款人執(zhí)行還款的時(shí)候,會(huì)將資金發(fā)到投資人賬戶中,涉及到投資人賬戶資金的變動(dòng),還有債權(quán)信息的變化等,整個(gè)還款過程涉及到調(diào)用銀行系統(tǒng),過程比較復(fù)雜,耗時(shí)相對比較長。

          債權(quán)轉(zhuǎn)讓:投資人發(fā)起債權(quán)轉(zhuǎn)讓,也涉及到債權(quán)的編號和投資人賬戶的資金的變動(dòng)。

          由于這 2 個(gè)業(yè)務(wù)都會(huì)操作債權(quán)記錄和投資人賬戶資金,為了保證資金的正確性,降低系統(tǒng)的復(fù)雜度,我們是這么做的,讓這 2 種業(yè)務(wù)互斥

          • 某筆借款執(zhí)行還款的過程中,那么這筆借款關(guān)聯(lián)的所有債權(quán)記錄不允許發(fā)起轉(zhuǎn)讓
          • 如果某筆借款記錄當(dāng)前沒有在還款處理中,那么這筆借款記錄關(guān)聯(lián)的債權(quán)都可以同時(shí)發(fā)起債權(quán)轉(zhuǎn)讓

          開頭提到的 X、W、R 三個(gè)對象,和我們這個(gè)業(yè)務(wù)場景對標(biāo)一下,如下

          X 表示資源W 操作R 操作
          標(biāo)的 id還款操作債權(quán)轉(zhuǎn)讓

          2、解決問題的思路

          mysql 大家都用過,mysql 中同時(shí)對一筆記錄發(fā)起 update 操作的時(shí)候,mysql 會(huì)確保整個(gè)操作會(huì)排隊(duì)執(zhí)行,內(nèi)部是互斥鎖實(shí)現(xiàn)的,從而可以確保在并發(fā)修改數(shù)據(jù)的時(shí)候,數(shù)據(jù)的正確性,執(zhí)行 update 的時(shí)候,會(huì)返回被更新的行數(shù),這里我們就利用 mysql 這個(gè)特性來實(shí)現(xiàn)讀寫鎖的功能。

          2.1、創(chuàng)建讀寫鎖表

          在業(yè)務(wù)庫創(chuàng)建一個(gè)鎖表,如下:

          create table t_read_write_lock(
              resource_id varchar(50) primary key not null comment '互斥資源id',
              w_count int not null default 0 comment '目前執(zhí)行中的W操作數(shù)量' ,
              r_count int not null default 0 comment '目前執(zhí)行中的R操作數(shù)量',
              version bigint not null default 0 comment '版本號,每次執(zhí)行update的時(shí)候+1'
          );

          這里主要關(guān)注 3 個(gè)字段:

          1、resource_id:互斥資源 id,比如上面的借款記錄 id

          2、w_count:當(dāng)前執(zhí)行 W 操作的數(shù)量

          3、r_count:當(dāng)前執(zhí)行 R 操作的數(shù)量

          下面來看 W 操作和 R 操作的實(shí)現(xiàn)。

          2.2、W 操作過程

          1、通過resource_id去t_read_write_lock查詢,如果不存在,則插入一條記錄,這里由于resource_id是主鍵,所以對于同一個(gè)resource_id只會(huì)有一個(gè)插入成功,這里用 $lock_record表示t_read_write_lock記錄
          2、判斷l(xiāng)ock_record.w_count ==0 && lock_record.r_count==0,如果為true繼續(xù)向下,否則返回false,業(yè)務(wù)終止
          3、獲取鎖,過程如下
              {
                  3.1、開啟事務(wù)
                  3.2int count = (update t_read_write_lock set w_count=1 where r_count = 0)
                  3.3、提交事務(wù);
              }
          4、如果3.2的count==1,繼續(xù)向下執(zhí)行,否則終止業(yè)務(wù)
          5、執(zhí)行業(yè)務(wù)操作
          6、釋放鎖,過程如下
              {
                  6.1、開啟事務(wù)
                  6.2、update t_read_write_lock set w_count=0 where w_count = 1
                  6.3、提交事務(wù)
              }

          整個(gè)過程有個(gè)問題,不知道大家發(fā)現(xiàn)沒有,如果執(zhí)行到 5 之后,系統(tǒng)掛了,會(huì)出現(xiàn)什么情況?

          業(yè)務(wù)執(zhí)行完畢了,但是 w 鎖卻沒有釋放,這種后果就是死鎖了,以后 r 操作就沒法執(zhí)行了。

          我們來看看,如何改進(jìn)?

          需要添加一下上鎖日志表,每次上鎖成功,則記錄一條日志,表結(jié)構(gòu)如下

          create table t_lock_log(
              id bigint primary key auto_increment comment '主鍵,自動(dòng)增長'
              resource_id varchar(50) primary key not null comment '互斥資源id',
              lock_type smallint default 0 comment '鎖類型,0:W鎖,1:R鎖',
              status smallint default 0 comment '狀態(tài),0:獲取鎖成功,1:業(yè)務(wù)執(zhí)行完畢,2:鎖被釋放'
              create_time bigint default 0 comment '記錄創(chuàng)建時(shí)間',
              version bigint not null default 0 comment '版本號,每次執(zhí)行update的時(shí)候+1'
          );

          如何使用呢?

          下面看 W 過程的改進(jìn)

          1、通過resource_id去t_read_write_lock查詢,如果不存在,則插入一條記錄,這里由于resource_id是主鍵,所以對于同一個(gè)resource_id只會(huì)有一個(gè)插入成功,這里用 $lock_record表示t_read_write_lock記錄
          2、判斷l(xiāng)ock_record.w_count ==0 && lock_record.r_count==0,如果為true繼續(xù)向下,否則返回false,業(yè)務(wù)終止
          3、獲取鎖,過程如下
              {
                  3.1、開啟事務(wù)
                  3.2int count = (update t_read_write_lock set w_count=1 where r_count = 0)
                  3.3、如果count==1,則插入一條上鎖日志,鎖類型是0,狀態(tài)是0insert t_lock_log (resource_id,lock_type,status,create_time) values (#{resource_id},0,0,'當(dāng)前時(shí)間');
                  3.4、提交事務(wù);
              }
          4、如果3.2的count==1,繼續(xù)向下執(zhí)行,否則終止業(yè)務(wù)
          5、執(zhí)行業(yè)務(wù)操作,業(yè)務(wù)操作過程如下
              {
                  5.1、業(yè)務(wù)庫開啟事務(wù)
                  5.2、執(zhí)行業(yè)務(wù)
                  5.3、更新鎖日志記錄的狀態(tài)為1,條件中必須帶上status=0int updateLogCount = (update t_lock_log set status=1 where id=#{日志記錄id} and status = 0)
                  5.4if(updateLogCount==1){
                          5.5、提交事務(wù)
                      }else{
                          5.6、回滾事務(wù)【走到這里說明更新鎖日志記錄失敗了,說明t_lock_log的status被其他地方改掉了,被防止死鎖的job修改了】
                      }
                  }
          6、釋放鎖,過程如下
              {
                  6.1、開啟事務(wù)
                  6.2、釋放鎖:update t_read_write_lock set w_count=0 where w_count = 1 and resource_id = #{resource_id}
                  6.3、更新鎖日志狀態(tài)為2:update t_lock_log set status=2 where id = #{日志記錄id}
                  6.4、提交事務(wù)
           }

          2.3、死鎖的處理

          上面這個(gè)是正常流程,如果第 3 步執(zhí)行完了,也就是上鎖 W 鎖成功,但是執(zhí)行到第 6 步之前,系統(tǒng)掛了,此時(shí) W 鎖沒有釋放,會(huì)出現(xiàn)死鎖。

          此時(shí)我們需要一個(gè) job,通過這個(gè) job 來釋放長時(shí)間還未釋放的鎖,比如過了 10 分鐘,鎖還未被釋放的,job 的邏輯如下

          1、獲取10分鐘之前鎖未釋放的鎖日志列表:select * from t_lock_log where status in (0,1) and create_time+10分鐘<=當(dāng)前時(shí)
          間的;
          2、輪詢獲取的日志列表,釋放鎖,操作如下
              {
                  2.1、開啟事務(wù)
                  2.2if(t_lock_log.lock_type==0){
                          //lock_type為0表示是W鎖,下面準(zhǔn)備釋放W鎖
                          //先將日志狀態(tài)更新為2,注意條件中帶上version作為條件,這里使用到了樂觀鎖,可以確保并發(fā)修改時(shí)只有一個(gè)count的值為1
                          int count = (update t_lock_log set status=2 where id = #{日志記錄id} and version = #{日志記錄.version})
                      if(count==1){
                          //將w_count置為0
                          update t_read_write_lock set w_count=0 where w_count = 1 and resource_id = #{resource_id}
                      }
                  }else{
                      //準(zhǔn)備釋放R鎖
                      //先將日志狀態(tài)置為2
                      int count = (update t_lock_log set status=2 where id = #{日志記錄id} and version = #{日志記錄.version})
                      if(count==1){
                      //將r_count置為r_count-1,注意條件中帶上r_count - 1>=0
                      update t_read_write_lock set r_count=r_count-1 where r_count - 1>=0 and resource_id = #{resource_id}
                  }
              }
              2.3、提交事務(wù)
          }

          2.4、R 鎖的過程

          1、通過resource_id去t_read_write_lock查詢,如果不存在,則插入一條記錄,這里由于resource_id是主鍵,所以對于同一個(gè)resource_id只會(huì)有一個(gè)插入成功,這里用 $lock_record表示t_read_write_lock記錄
          2、判斷l(xiāng)ock_record.w_count ==0,如果為true繼續(xù)向下,否則返回false,業(yè)務(wù)終止
          3、獲取鎖,過程如下
              {
                  3.1、開啟事務(wù)
                  3.2int count = (update t_read_write_lock set r_count=r_count+1 where w_count = 0)
                  3.3、如果count==1,則插入一條上鎖日志,鎖類型是1【表示R鎖】,狀態(tài)是0insert t_lock_log (resource_id,lock_type,status,create_time) values (#{resource_id},1,0,'當(dāng)前時(shí)間');
                  3.4、提交事務(wù);
              }
          4、如果3.2的count==1,繼續(xù)向下執(zhí)行,否則終止業(yè)務(wù)
          5、執(zhí)行業(yè)務(wù)操作,業(yè)務(wù)操作過程如下
              {
                  5.1、業(yè)務(wù)庫開啟事務(wù)
                  5.2、執(zhí)行業(yè)務(wù)
                  5.3、更新鎖日志記錄的狀態(tài)為1,條件中必須帶上status=0int updateLogCount = (update t_lock_log set status=1 where id=#{日志記錄id} and status = 0)
                  5.4if(updateLogCount==1){
                      5.5、提交事務(wù)
                  }else{
                      5.6、回滾事務(wù)【走到這里說明更新鎖日志記錄失敗了,說明t_lock_log的status被其他地方改掉了,被防止死鎖的job修改了】
                  }
              }
          6、釋放鎖,過程如下
              {
                  6.1、開啟事務(wù)
                  6.2、釋放鎖:update t_read_write_lock set r_count=r_count-1 where r_count - 1 >= 0 and resource_id = #{resource_id}
                  6.3、更新鎖日志狀態(tài)為2:update t_lock_log set status=2 where id = #{日志記錄id}
                  6.4、提交事務(wù)
              }

          3、總結(jié)

          本文主要介紹了如何使用 mysql 來實(shí)現(xiàn)讀寫鎖,如何防止死鎖,重點(diǎn)就是 2 張表,鎖表和日志表,2 個(gè)表配合一個(gè) job,就把問題解決了。

          大家可以將上面代碼轉(zhuǎn)換為程序,結(jié)合 spring 的 aop 可以實(shí)現(xiàn)一個(gè)通用的 db 讀寫鎖,有興趣的可以試試,有問題歡加我微信 itsoku 交流。

          4、Spring 系列

          1. Spring 系列第 1 篇:為何要學(xué) spring?
          2. Spring 系列第 2 篇:控制反轉(zhuǎn)(IoC)與依賴注入(DI)
          3. Spring 系列第 3 篇:Spring 容器基本使用及原理
          4. Spring 系列第 4 篇:xml 中 bean 定義詳解(-)
          5. Spring 系列第 5 篇:創(chuàng)建 bean 實(shí)例這些方式你們都知道?
          6. Spring 系列第 6 篇:玩轉(zhuǎn) bean scope,避免跳坑里!
          7. Spring 系列第 7 篇:依賴注入之手動(dòng)注入
          8. Spring 系列第 8 篇:自動(dòng)注入(autowire)詳解,高手在于堅(jiān)持
          9. Spring 系列第 9 篇:depend-on 到底是干什么的?
          10. Spring 系列第 10 篇:primary 可以解決什么問題?
          11. Spring 系列第 11 篇:bean 中的 autowire-candidate 又是干什么的?
          12. Spring 系列第 12 篇:lazy-init:bean 延遲初始化
          13. Spring 系列第 13 篇:使用繼承簡化 bean 配置(abstract & parent)
          14. Spring 系列第 14 篇:lookup-method 和 replaced-method 比較陌生,怎么玩的?
          15. Spring 系列第 15 篇:代理詳解(Java 動(dòng)態(tài)代理&cglib 代理)?
          16. Spring 系列第 16 篇:深入理解 java 注解及 spring 對注解的增強(qiáng)(預(yù)備知識(shí))
          17. Spring 系列第 17 篇:@Configration 和@Bean 注解詳解(bean 批量注冊)
          18. Spring 系列第 18 篇:@ComponentScan、@ComponentScans 詳解(bean 批量注冊)
          19. Spring 系列第 18 篇:@import 詳解(bean 批量注冊)
          20. Spring 系列第 20 篇:@Conditional 通過條件來控制 bean 的注冊
          21. Spring 系列第 21 篇:注解實(shí)現(xiàn)依賴注入(@Autowired、@Resource、@Primary、@Qulifier)
          22. Spring 系列第 22 篇:@Scope、@DependsOn、@ImportResource、@Lazy 詳解
          23. Spring 系列第 23 篇:Bean 生命周期詳解
          24. Spring 系列第 24 篇:父子容器詳解
          25. Spring 系列第 25 篇:@Value【用法、數(shù)據(jù)來源、動(dòng)態(tài)刷新】
          26. Spring 系列第 26 篇:國際化詳解
          27. Spring 系列第 27 篇:spring 事件機(jī)制詳解
          28. Spring 系列第 28 篇:Bean 循環(huán)依賴詳解
          29. Spring 系列第 29 篇:BeanFactory 擴(kuò)展(BeanFactoryPostProcessor、BeanDefinitionRegistryPostProcessor)
          30. Spring 系列第 30 篇:jdk 動(dòng)態(tài)代理和 cglib 代理
          31. Spring 系列第 31 篇:aop 概念詳解
          32. Spring 系列第 32 篇:AOP 核心源碼、原理詳解
          33. Spring 系列第 33 篇:ProxyFactoryBean 創(chuàng)建 AOP 代理
          34. Spring 系列第 34 篇:@Aspect 中@Pointcut 12 種用法
          35. Spring 系列第 35 篇:@Aspect 中 5 中通知詳解
          36. Spring 系列第 36 篇:@EnableAspectJAutoProxy、@Aspect 中通知順序詳解
          37. Spring 系列第 37 篇:@EnableAsync & @Async 實(shí)現(xiàn)方法異步調(diào)用
          38. Spring 系列第 38 篇:@Scheduled & @EnableScheduling 定時(shí)器詳解
          39. Spring 系列第 39 篇:強(qiáng)大的 Spel 表達(dá)式
          40. Spring 系列第 40 篇:緩存使用(@EnableCaching、@Cacheable、@CachePut、@CacheEvict、@Caching、@CacheConfig)
          41. Spring 系列第 41 篇:@EnableCaching 集成 redis 緩存
          42. Spring 系列第 42 篇:玩轉(zhuǎn) JdbcTemplate
          43. Spring 系列第 43 篇:spring 中編程式事務(wù)怎么用的?
          44. Spring 系列第 44 篇:詳解 spring 聲明式事務(wù)(@Transactional)
          45. Spring 系列第 45 篇:帶你吃透 Spring 事務(wù) 7 種傳播行為
          46. Spring 系列第 46 篇:Spring 如何管理多數(shù)據(jù)源事務(wù)?
          47. Spring 系列第 47 篇:spring 編程式事務(wù)源碼解析
          48. Spring 系列第 48 篇:@Transaction 事務(wù)源碼解析
          49. Spring 系列第 49 篇:通過 Spring 事務(wù)實(shí)現(xiàn) MQ 中的事務(wù)消息
          50. Spring 系列第 50 篇:spring 事務(wù)攔截器順序如何控制?
          51. Spring 系列第 51 篇:導(dǎo)致 Spring 事務(wù)失效常見的幾種情況
          52. Spring 系列第 52 篇:Spring 實(shí)現(xiàn)數(shù)據(jù)庫讀寫分離
          53. Spring 系列第 53 篇:Spring 集成 MyBatis
          54. Spring 系列第 54 篇:集成 junit
          55. Spring 系列第 55 篇:spring 上下文生命周期
          56. Spring 系列第 56 篇:面試官:循環(huán)依賴不用三級緩存可以么?

          5、更多好文章

          1. Java 高并發(fā)系列(共 34 篇)
          2. MySql 高手系列(共 27 篇)
          3. Maven 高手系列(共 10 篇)
          4. Mybatis 系列(共 12 篇)
          5. 聊聊 db 和緩存一致性常見的實(shí)現(xiàn)方式
          6. 接口冪等性這么重要,它是什么?怎么實(shí)現(xiàn)?
          7. 泛型,有點(diǎn)難度,會(huì)讓很多人懵逼,那是因?yàn)槟銢]有看這篇文章!


          瀏覽 31
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  色情视频免费观看 | 亚洲sv剧情在线 | 国产视频一区二区三区四区 | 尻屄视频在线观看 | 免费无码又爽又刺激A片视频男男 |