java實現(xiàn)分布式鎖的常用三種方式
分布式鎖概述
我們的系統(tǒng)都是分布式部署的,日常開發(fā)中,秒殺下單、搶購商品等等業(yè)務場景,為了防?庫存超賣,都需要用到分布式鎖。
分布式鎖其實就是,控制分布式系統(tǒng)不同進程共同訪問共享資源的一種鎖的實現(xiàn)。如果不同的系統(tǒng)或同一個系統(tǒng)的不同主機之間共享了某個臨界資源,往往需要互斥來防止彼此干擾,以保證一致性。
業(yè)界流行的分布式鎖實現(xiàn),一般有這3種方式:
基于數(shù)據(jù)庫實現(xiàn)的分布式鎖
基于Redis實現(xiàn)的分布式鎖
基于Zookeeper實現(xiàn)的分布式鎖
分布式鎖:基于數(shù)據(jù)庫實現(xiàn)
主要有兩種方式:
1、悲觀鎖
2、樂觀鎖
A. 悲觀鎖(排他鎖)
利用select … where xx=yy for update排他鎖
注意:這里需要注意的是where xx=yy,xx字段必須要走索引,否則會鎖表。有些情況下,比如表不大,mysql優(yōu)化器會不走這個索引,導致鎖表問題。
核心思想:以「悲觀的心態(tài)」操作資源,無法獲得鎖成功,就一直阻塞著等待。
注意:該方式有很多缺陷,一般不建議使用。
實現(xiàn):
創(chuàng)建一張資源鎖表:
CREATE TABLE `resource_lock` ( `id` int(4) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `resource_name` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖定的資源名', `owner` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖擁有者', `desc` varchar(1024) NOT NULL DEFAULT '備注信息', `update_time` timestamp NOT NULL DEFAULT '' COMMENT '保存數(shù)據(jù)時間,自動生成', PRIMARY KEY (`id`), UNIQUE KEY `uidx_resource_name` (`resource_name `) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='鎖定中的資源';
注意:resource_name 鎖資源名稱必須有唯一索引
使用事務查詢更新:
@Transaction
public void lock(String name) {
ResourceLock rlock = exeSql("select * from resource_lock where resource_name = name for update");
if (rlock == null) {
exeSql("insert into resource_lock(reosurce_name,owner,count) values (name, 'ip',0)");
}
}使用 for update 鎖定的資源。如果執(zhí)行成功,會立即返回,執(zhí)行插入數(shù)據(jù)庫,后續(xù)再執(zhí)行一些其他業(yè)務邏輯,直到事務提交,執(zhí)行結束;如果執(zhí)行失敗,就會一直阻塞著。
可以在數(shù)據(jù)庫客戶端工具上測試出來這個效果,當在一個終端執(zhí)行了 for update,不提交事務。在另外的終端上執(zhí)行相同條件的 for update,會一直卡著
雖然也能實現(xiàn)分布式鎖的效果,但是會存在性能瓶頸。
優(yōu)點:
簡單易用,好理解,保障數(shù)據(jù)強一致性。
缺點:
1)在 RR 事務級別,select 的 for update 操作是基于間隙鎖(gap lock) 實現(xiàn)的,是一種悲觀鎖的實現(xiàn)方式,所以存在阻塞問題。
2)高并發(fā)情況下,大量請求進來,會導致大部分請求進行排隊,影響數(shù)據(jù)庫穩(wěn)定性,也會耗費服務的CPU等資源。
當獲得鎖的客戶端等待時間過長時,會提示:
[40001][1205] Lock wait timeout exceeded; try restarting transaction
高并發(fā)情況下,也會造成占用過多的應用線程,導致業(yè)務無法正常響應。
3)如果優(yōu)先獲得鎖的線程因為某些原因,一直沒有釋放掉鎖,可能會導致死鎖的發(fā)生。
4)鎖的長時間不釋放,會一直占用數(shù)據(jù)庫連接,可能會將數(shù)據(jù)庫連接池撐爆,影響其他服務。
5)MySql數(shù)據(jù)庫會做查詢優(yōu)化,即便使用了索引,優(yōu)化時發(fā)現(xiàn)全表掃效率更高,則可能會將行鎖升級為表鎖,此時可能就更悲劇了。
6)不支持可重入特性,并且超時等待時間是全局的,不能隨便改動。
B. 樂觀鎖
所謂樂觀鎖與悲觀鎖最大區(qū)別在于基于CAS思想,表中添加一個時間戳或者是版本號的字段來實現(xiàn),update xx set version=new_version where xx=yy and version=Old_version,通過增加遞增的版本號字段實現(xiàn)樂觀鎖。
不具有互斥性,不會產生鎖等待而消耗資源,操作過程中認為不存在并發(fā)沖突,只有update version失敗后才能覺察到。
搶購、秒殺就是用了這種實現(xiàn)以防止超賣。
實現(xiàn):
創(chuàng)建一張資源鎖表:
CREATE TABLE `resource` ( `id` int(4) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `resource_name` varchar(64) NOT NULL DEFAULT '' COMMENT '資源名', `share` varchar(64) NOT NULL DEFAULT '' COMMENT '狀態(tài)', `version` int(4) NOT NULL DEFAULT '' COMMENT '版本號', `desc` varchar(1024) NOT NULL DEFAULT '備注信息', `update_time` timestamp NOT NULL DEFAULT '' COMMENT '保存數(shù)據(jù)時間,自動生成', PRIMARY KEY (`id`), UNIQUE KEY `uidx_resource_name` (`resource_name `) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='資源';
為表添加一個字段,版本號或者時間戳都可以。通過版本號或者時間戳,來保證多線程同時間操作共享資源的有序性和正確性。
偽代碼實現(xiàn):
Resrouce resource = exeSql("select * from resource where resource_name = xxx");
boolean succ = exeSql("update resource set version= 'newVersion' ... where resource_name = xxx and version = 'oldVersion'");
if (!succ) {
// 發(fā)起重試
}實際代碼中可以寫個while循環(huán)不斷重試,版本號不一致,更新失敗,重新獲取新的版本號,直到更新成功。
優(yōu)點:
實現(xiàn)簡單,復雜度低
保障數(shù)據(jù)一致性
缺點:
性能低,并且有鎖表的風險
可靠性差
非阻塞操作失敗后,需要輪詢,占用CPU資源
長時間不commit或者是長時間輪詢,可能會占用較多的連接資源
分布式鎖:基于Redis實現(xiàn)
原理與實現(xiàn)
Redis提供了多種命令支持實現(xiàn)分布式鎖,其中最常用的是SETNX(Set if Not eXists)和GETSET結合使用,或者使用更高級的SET命令配合NX(Only set the key if it does not already exist)和PX或EX(為key設置過期時間)選項。
優(yōu)點:
性能高效,Redis本身為內存數(shù)據(jù)庫,操作速度快。
實現(xiàn)簡單,通過幾個命令即可完成鎖的獲取與釋放。
支持自動過期,降低死鎖風險。
缺點:
單點問題,依賴單一Redis實例可能成為瓶頸。
網絡分區(qū)可能導致鎖的不一致狀態(tài)。
示例代碼(偽代碼):
import redis.clients.jedis.Jedis;
public class RedisDistributedLock {
private Jedis jedis;
private static final String LOCK_SUCCESS = "OK";
private static final Long RELEASE_SUCCESS = 1L;
public RedisDistributedLock(Jedis jedis) {
this.jedis = jedis;
}
public boolean lock(String lockKey, int expireTime) {
String result = jedis.set(lockKey, "locked", "NX", "PX", expireTime * 1000);
return LOCK_SUCCESS.equals(result);
}
public boolean unlock(String lockKey) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, 1, lockKey, "locked");
return RELEASE_SUCCESS.equals(result);
}
}代碼注解:上例中,
lock方法嘗試使用NX(只在鍵不存在時設置)和PX(設置過期時間,單位毫秒)參數(shù)設置鎖,返回OK表示成功獲取鎖。unlock方法使用Lua腳本確保解鎖操作的原子性,只有當鎖的持有者與當前客戶端匹配時才執(zhí)行刪除操作。
分布式鎖:基于Zookeeper實現(xiàn)
在學習Zookeeper分布式鎖之前,我們復習一下Zookeeper的節(jié)點哈。
Zookeeper的節(jié)點Znode有四種類型:
持久節(jié)點:默認的節(jié)點類型。創(chuàng)建節(jié)點的客戶端與zookeeper斷開連接后,該節(jié)點依舊存在。
持久節(jié)點順序節(jié)點:所謂順序節(jié)點,就是在創(chuàng)建節(jié)點時,Zookeeper根據(jù)創(chuàng)建的時間順序給該節(jié)點名稱進行編號,持久節(jié)點順序節(jié)點就是有順序的持久節(jié)點。
臨時節(jié)點:和持久節(jié)點相反,當創(chuàng)建節(jié)點的客戶端與zookeeper斷開連接后,臨時節(jié)點會被刪除。
臨時順序節(jié)點:有順序的臨時節(jié)點。
Zookeeper分布式鎖實現(xiàn)應用了臨時順序節(jié)點。這里不貼代碼啦,來講下zk分布式鎖的實現(xiàn)原理吧。
zk獲取鎖過程
當?shù)谝粋€客戶端請求過來時,Zookeeper客戶端會創(chuàng)建一個持久節(jié)點locks。如果它(Client1)想獲得鎖,需要在locks節(jié)點下創(chuàng)建一個順序節(jié)點lock1.如圖

接著,客戶端Client1會查找locks下面的所有臨時順序子節(jié)點,判斷自己的節(jié)點lock1是不是排序最小的那一個,如果是,則成功獲得鎖。

這時候如果又來一個客戶端client2前來嘗試獲得鎖,它會在locks下再創(chuàng)建一個臨時節(jié)點lock2

客戶端client2一樣也會查找locks下面的所有臨時順序子節(jié)點,判斷自己的節(jié)點lock2是不是最小的,此時,發(fā)現(xiàn)lock1才是最小的,于是獲取鎖失敗。獲取鎖失敗,它是不會甘心的,client2向它排序靠前的節(jié)點lock1注冊Watcher事件,用來監(jiān)聽lock1是否存在,也就是說client2搶鎖失敗進入等待狀態(tài)。

此時,如果再來一個客戶端Client3來嘗試獲取鎖,它會在locks下再創(chuàng)建一個臨時節(jié)點lock3

同樣的,client3一樣也會查找locks下面的所有臨時順序子節(jié)點,判斷自己的節(jié)點lock3是不是最小的,發(fā)現(xiàn)自己不是最小的,就獲取鎖失敗。它也是不會甘心的,它會向在它前面的節(jié)點lock2注冊Watcher事件,以監(jiān)聽lock2節(jié)點是否存在。

釋放鎖
我們再來看看釋放鎖的流程,Zookeeper的客戶端業(yè)務完成或者發(fā)生故障,都會刪除臨時節(jié)點,釋放鎖。如果是任務完成,Client1會顯式調用刪除lock1的指令

如果是客戶端故障了,根據(jù)臨時節(jié)點得特性,lock1是會自動刪除的

lock1節(jié)點被刪除后,Client2可開心了,因為它一直監(jiān)聽著lock1。lock1節(jié)點刪除,Client2立刻收到通知,也會查找locks下面的所有臨時順序子節(jié)點,發(fā)下lock2是最小,就獲得鎖。

同理,Client2獲得鎖之后,Client3也對它虎視眈眈,啊哈哈~
Zookeeper設計定位就是分布式協(xié)調,簡單易用。如果獲取不到鎖,只需添加一個監(jiān)聽器即可,很適合做分布式鎖。
Zookeeper作為分布式鎖也缺點:如果有很多的客戶端頻繁的申請加鎖、釋放鎖,對于Zookeeper集群的壓力會比較大。
到此這篇關于java實現(xiàn)分布式鎖的常用三種方式的文章就介紹到這了,更多相關java 分布式鎖內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
SpringBoot實現(xiàn)日志文件分隔(根據(jù)日期和文件大小)
文章簡要介紹了如何在Spring Boot項目中配置日志文件路徑,包括在resource目錄下創(chuàng)建logback-spring.xml文件,并在yml配置文件中設置logging.file.path屬性來指定日志文件的輸出路徑2026-01-01
SpringBoot訪問web中的靜態(tài)資源的方式小結
這篇文章主要介紹了SpringBoot訪問web中的靜態(tài)資源的方式,本文給大家介紹了兩種方式,通過代碼示例和圖文講解的非常詳細,具有一定的參考價值,需要的朋友可以參考下2024-10-10
SpringBoot導出Excel的四種實現(xiàn)方式
近期接到了一個小需求,要將系統(tǒng)中的數(shù)據(jù)導出為Excel,且能將Excel數(shù)據(jù)導入到系統(tǒng),對于大多數(shù)研發(fā)人員來說,這算是一個最基本的操作了,本文就給大家總結一下SpringBoot導出Excel的四種實現(xiàn)方式,需要的朋友可以參考下2024-01-01
SpringBoot的ResponseEntity類返回給前端具體講解
這篇文章主要給大家介紹了關于SpringBoot的ResponseEntity類返回給前端的相關資料,ResponseEntity是Spring框架中用于封裝HTTP響應的類,可以自定義狀態(tài)碼、響應頭和響應體,常用于控制器方法中返回特定數(shù)據(jù)的HTTP響應,需要的朋友可以參考下2024-11-11

