mysql居然還能實(shí)現(xiàn)分布式鎖的方法
之前的文章中通過(guò)電商場(chǎng)景中秒殺的例子和大家分享了單體架構(gòu)中鎖的使用方式,但是現(xiàn)在很多應(yīng)用系統(tǒng)都是相當(dāng)龐大的,很多應(yīng)用系統(tǒng)都是微服務(wù)的架構(gòu)體系,那么在這種跨jvm的場(chǎng)景下,我們又該如何去解決并發(fā)。
單體應(yīng)用鎖的局限性在進(jìn)入實(shí)戰(zhàn)之前簡(jiǎn)單和大家粗略聊一下互聯(lián)網(wǎng)系統(tǒng)中的架構(gòu)演進(jìn)。
在互聯(lián)網(wǎng)系統(tǒng)發(fā)展之初,消耗資源比較小,用戶(hù)量也比較小,我們只部署一個(gè)tomcat應(yīng)用就可以滿(mǎn)足需求。一個(gè)tomcat我們可以看做是一個(gè)jvm的進(jìn)程,當(dāng)大量的請(qǐng)求并發(fā)到達(dá)系統(tǒng)時(shí),所有的請(qǐng)求都落在這唯一的一個(gè)tomcat上,如果某些請(qǐng)求方法是需要加鎖的,比如上篇文章中提及的秒殺扣減庫(kù)存的場(chǎng)景,是可以滿(mǎn)足需求的。但是隨著訪(fǎng)問(wèn)量的增加,一個(gè)tomcat難以支撐,這時(shí)候我們就需要集群部署tomcat,使用多個(gè)tomcat支撐起系統(tǒng)。
在上圖中簡(jiǎn)單演化之后,我們部署兩個(gè)Tomcat共同支撐系統(tǒng)。當(dāng)一個(gè)請(qǐng)求到達(dá)系統(tǒng)的時(shí)候,首先會(huì)經(jīng)過(guò)nginx,由nginx作為負(fù)載均衡,它會(huì)根據(jù)自己的負(fù)載均衡配置策略將請(qǐng)求轉(zhuǎn)發(fā)到其中的一個(gè)tomcat上。當(dāng)大量的請(qǐng)求并發(fā)訪(fǎng)問(wèn)的時(shí)候,兩個(gè)tomcat共同承擔(dān)所有的訪(fǎng)問(wèn)量。這之后我們同樣進(jìn)行秒殺扣減庫(kù)存的時(shí)候,使用單體應(yīng)用鎖,還能滿(mǎn)足需求么?
之前我們所加的鎖是JDK提供的鎖,這種鎖在單個(gè)jvm下起作用,當(dāng)存在兩個(gè)或者多個(gè)的時(shí)候,大量并發(fā)請(qǐng)求分散到不同tomcat,在每個(gè)tomcat中都可以防止并發(fā)的產(chǎn)生,但是多個(gè)tomcat之間,每個(gè)Tomcat中獲得鎖這個(gè)請(qǐng)求,又產(chǎn)生了并發(fā)。從而扣減庫(kù)存的問(wèn)題依舊存在。這就是單體應(yīng)用鎖的局限性。那我們?nèi)绻鉀Q這個(gè)問(wèn)題呢?接下來(lái)就要和大家分享分布式鎖了。
分布式鎖什么是分布式鎖?那么什么是分布式鎖呢,在說(shuō)分布式鎖之前我們看到單體應(yīng)用鎖的特點(diǎn)就是在一個(gè)jvm進(jìn)行有效,但是無(wú)法跨越j(luò)vm以及進(jìn)程。所以我們就可以下一個(gè)不那么官方的定義,分布式鎖就是可以跨越多個(gè)jvm,跨越多個(gè)進(jìn)程的鎖,像這樣的鎖就是分布式鎖。
設(shè)計(jì)思路由于tomcat是java啟動(dòng)的,所以每個(gè)tomcat可以看成一個(gè)jvm,jvm內(nèi)部的鎖無(wú)法跨越多個(gè)進(jìn)程。所以我們實(shí)現(xiàn)分布式鎖,只能在這些jvm外去尋找,通過(guò)其他的組件來(lái)實(shí)現(xiàn)分布式鎖。
上圖兩個(gè)tomcat通過(guò)第三方的組件實(shí)現(xiàn)跨jvm,跨進(jìn)程的分布式鎖。這就是分布式鎖的解決思路。
實(shí)現(xiàn)方式那么目前有哪些第三方組件來(lái)實(shí)現(xiàn)呢?目前比較流行的有以下幾種:
數(shù)據(jù)庫(kù),通過(guò)數(shù)據(jù)庫(kù)可以實(shí)現(xiàn)分布式鎖,但是高并發(fā)的情況下對(duì)數(shù)據(jù)庫(kù)的壓力比較大,所以很少使用。 Redis,借助redis可以實(shí)現(xiàn)分布式鎖,而且redis的java客戶(hù)端種類(lèi)很多,所以使用方法也不盡相同。 Zookeeper,也可以實(shí)現(xiàn)分布式鎖,同樣zk也有很多java客戶(hù)端,使用方法也不同。針對(duì)上述實(shí)現(xiàn)方式,老貓還是通過(guò)具體的代碼例子來(lái)一一演示。
基于數(shù)據(jù)庫(kù)的分布式鎖思路:基于數(shù)據(jù)庫(kù)悲觀(guān)鎖去實(shí)現(xiàn)分布式鎖,用的主要是select ... for update。select ... for update是為了在查詢(xún)的時(shí)候就對(duì)查詢(xún)到的數(shù)據(jù)進(jìn)行了加鎖處理。當(dāng)用戶(hù)進(jìn)行這種行為操作的時(shí)候,其他線(xiàn)程是禁止對(duì)這些數(shù)據(jù)進(jìn)行修改或者刪除操作,必須等待上個(gè)線(xiàn)程操作完畢釋放之后才能進(jìn)行操作,從而達(dá)到了鎖的效果。
實(shí)現(xiàn):我們還是基于電商中超賣(mài)的例子和大家分享代碼。
咱們還是利用上次單體架構(gòu)中的超賣(mài)的例子和大家分享,針對(duì)上次的代碼進(jìn)行改造,我們新鍵一張表,叫做distribute_lock,這張表的目的主要是為了提供數(shù)據(jù)庫(kù)鎖,我們來(lái)看一下這張表的情況。
由于我們這邊模擬的是訂單超賣(mài)的場(chǎng)景,所以在上圖中我們有一條訂單的鎖數(shù)據(jù)。
我們將上一篇中的代碼改造一下抽取出一個(gè)controller然后通過(guò)postman去請(qǐng)求調(diào)用,當(dāng)然后臺(tái)是啟動(dòng)兩個(gè)jvm進(jìn)行操作,分別是8080端口以及8081端口。完成之后的代碼如下:
/** * @author kdaddy@163.com * @date 2021/1/3 10:48 * @desc 公眾號(hào)“程序員老貓” */@Service@Slf4jpublic class MySQLOrderService { @Resource private KdOrderMapper orderMapper; @Resource private KdOrderItemMapper orderItemMapper; @Resource private KdProductMapper productMapper; @Resource private DistributeLockMapper distributeLockMapper; //購(gòu)買(mǎi)商品id private int purchaseProductId = 100100; //購(gòu)買(mǎi)商品數(shù)量 private int purchaseProductNum = 1; @Transactional(propagation = Propagation.REQUIRED) public Integer createOrder() throws Exception{ log.info('進(jìn)入了方法'); DistributeLock lock = distributeLockMapper.selectDistributeLock('order'); if(lock == null) throw new Exception('該業(yè)務(wù)分布式鎖未配置'); log.info('拿到了鎖'); //此處為了手動(dòng)演示并發(fā),所以我們暫時(shí)在這里休眠1分鐘 Thread.sleep(60000); KdProduct product = productMapper.selectByPrimaryKey(purchaseProductId); if (product==null){ throw new Exception('購(gòu)買(mǎi)商品:'+purchaseProductId+'不存在'); } //商品當(dāng)前庫(kù)存 Integer currentCount = product.getCount(); log.info(Thread.currentThread().getName()+'庫(kù)存數(shù)'+currentCount); //校驗(yàn)庫(kù)存 if (purchaseProductNum > currentCount){ throw new Exception('商品'+purchaseProductId+'僅剩'+currentCount+'件,無(wú)法購(gòu)買(mǎi)'); } //在數(shù)據(jù)庫(kù)中完成減量操作 productMapper.updateProductCount(purchaseProductNum,'kd',new Date(),product.getId()); //生成訂單 ...次數(shù)省略,源代碼可以到老貓的github下載:https://github.com/maoba/kd-distribute return order.getId(); }}
SQL的寫(xiě)法如下:
select * from distribute_lock where business_code = #{business_code,jdbcType=VARCHAR} for update
以上為主要實(shí)現(xiàn)邏輯,關(guān)于代碼中的注意點(diǎn):
createOrder方法必須要有事務(wù),因?yàn)橹挥性谑聞?wù)存在的情況下才能觸發(fā)select for update的鎖。 代碼中必須要對(duì)當(dāng)前鎖的存在性進(jìn)行判斷,如果為空的情況下,會(huì)報(bào)異常我們來(lái)看一下最終運(yùn)行的效果,先看一下console日志,
8080的console日志情況:
11:49:41 INFO 16360 --- [nio-8080-exec-2] c.k.d.service.MySQLOrderService : 進(jìn)入了方法11:49:41 INFO 16360 --- [nio-8080-exec-2] c.k.d.service.MySQLOrderService : 拿到了鎖
8081的console日志情況:
11:49:48 INFO 17640 --- [nio-8081-exec-2] c.k.d.service.MySQLOrderService : 進(jìn)入了方法
通過(guò)日志情況,兩個(gè)不同的jvm,由于第一個(gè)到8080的請(qǐng)求優(yōu)先拿到了鎖,所以8081的請(qǐng)求就處于等待鎖釋放才會(huì)去執(zhí)行,這說(shuō)明我們的分布式鎖生效了。再看一下完整執(zhí)行之后的日志情況:
8080的請(qǐng)求:
11:58:01 INFO 15380 --- [nio-8080-exec-1] c.k.d.service.MySQLOrderService : 進(jìn)入了方法11:58:01 INFO 15380 --- [nio-8080-exec-1] c.k.d.service.MySQLOrderService : 拿到了鎖11:58:07 INFO 15380 --- [nio-8080-exec-1] c.k.d.service.MySQLOrderService : http-nio-8080-exec-1庫(kù)存數(shù)1
8081的請(qǐng)求:
11:58:03 INFO 16276 --- [nio-8081-exec-1] c.k.d.service.MySQLOrderService : 進(jìn)入了方法11:58:08 INFO 16276 --- [nio-8081-exec-1] c.k.d.service.MySQLOrderService : 拿到了鎖11:58:14 INFO 16276 --- [nio-8081-exec-1] c.k.d.service.MySQLOrderService : http-nio-8081-exec-1庫(kù)存數(shù)011:58:14 ERROR 16276 --- [nio-8081-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.Exception: 商品100100僅剩0件,無(wú)法購(gòu)買(mǎi)] with root cause
java.lang.Exception: 商品100100僅剩0件,無(wú)法購(gòu)買(mǎi) at com.kd.distribute.service.MySQLOrderService.createOrder(MySQLOrderService.java:61) ~[classes/:na]
很明顯第二個(gè)請(qǐng)求由于沒(méi)有庫(kù)存,導(dǎo)致最終購(gòu)買(mǎi)失敗的情況,當(dāng)然這個(gè)場(chǎng)景也是符合我們正常的業(yè)務(wù)場(chǎng)景的。最終我們數(shù)據(jù)庫(kù)的情況是這樣的:
很明顯,我們到此數(shù)據(jù)庫(kù)的庫(kù)存和訂單數(shù)量也都正確了。到此我們基于數(shù)據(jù)庫(kù)的分布式鎖實(shí)戰(zhàn)演示完成,下面我們來(lái)歸納一下如果使用這種鎖,有哪些優(yōu)點(diǎn)以及缺點(diǎn)。
優(yōu)點(diǎn):簡(jiǎn)單方便、易于理解、易于操作。 缺點(diǎn):并發(fā)量大的時(shí)候?qū)?shù)據(jù)庫(kù)的壓力會(huì)比較大。 建議:作為鎖的數(shù)據(jù)庫(kù)和業(yè)務(wù)數(shù)據(jù)庫(kù)分開(kāi)。寫(xiě)在最后對(duì)于上述數(shù)據(jù)庫(kù)分布式鎖,其實(shí)在我們的日常開(kāi)發(fā)中用的也是比較少的。基于redis以及zk的鎖倒是用的比較多一些,本來(lái)老貓想把redis鎖以及zk鎖放在這一篇中一起分享掉,但是再寫(xiě)在同一篇上面的話(huà),篇幅就顯得過(guò)長(zhǎng)了,因此本篇就和大家分享這一種分布式鎖。源碼大家可以在老貓的github中下載到。地址是:https://github.com/maoba/kd-distribute
到此這篇關(guān)于mysql居然還能實(shí)現(xiàn)分布式鎖的方法的文章就介紹到這了,更多相關(guān)mysql 分布式鎖內(nèi)容請(qǐng)搜索好吧啦網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持好吧啦網(wǎng)!
相關(guān)文章:
1. MySQL 字符串函數(shù):字符串截取2. DB2高可用性災(zāi)難恢復(fù)(HADR)的限制3. Microsoft Office Access調(diào)整字段位置的方法4. MySQL之mysqldump的使用詳解5. DB2數(shù)據(jù)庫(kù)導(dǎo)出表結(jié)構(gòu)與導(dǎo)入導(dǎo)出表數(shù)據(jù)6. RHAS 3.0上的Oracle 9i的安裝7. MySQL雙主(主主)架構(gòu)配置方案8. MySQL CHAR和VARCHAR該如何選擇9. mysql group by 對(duì)多個(gè)字段進(jìn)行分組操作10. 一文帶你學(xué)會(huì)Mysql表批量添加字段
