學(xué)過Java多線程的應(yīng)該都知道什么是鎖蛮浑,沒學(xué)過的也不用擔(dān)心唬党,Java中的鎖可以簡單的理解為多線程情況下訪問臨界資源的一種線程同步機制滑频。
在學(xué)習(xí)或者使用Java的過程中進程會遇到各種各樣的鎖的概念:公平鎖腺逛、非公平鎖著拭、自旋鎖纱扭、可重入鎖、偏向鎖儡遮、輕量級鎖乳蛾、重量級鎖、讀寫鎖鄙币、互斥鎖等肃叶。
蒙了嗎?不要緊十嘿!即使你這些都不會也不要緊因惭,因為這個和今天要探討的關(guān)系不大,不過如果你作為一個愛學(xué)習(xí)的小伙伴绩衷,這里也給你準(zhǔn)備了一份秘籍:《Java多線程核心技術(shù)》蹦魔,一共19篇祝你一臂之力激率!免費版的不過癮,當(dāng)然也有收費版的勿决!
一乒躺、為什么要使用分布式鎖
我們在開發(fā)應(yīng)用的時候,如果需要對某一個共享變量進行多線程同步訪問的時候低缩,可以使用我們學(xué)到的Java多線程的18般武藝進行處理嘉冒,并且可以完美的運行,毫無Bug咆繁!
注意這是單機應(yīng)用讳推,也就是所有的請求都會分配到當(dāng)前服務(wù)器的JVM內(nèi)部,然后映射為操作系統(tǒng)的線程進行處理玩般!而這個共享變量只是在這個JVM內(nèi)部的一塊內(nèi)存空間娜遵!
后來業(yè)務(wù)發(fā)展,需要做集群壤短,一個應(yīng)用需要部署到幾臺機器上然后做負載均衡,大致如下圖:
上圖可以看到慨仿,變量A存在JVM1久脯、JVM2、JVM3三個JVM內(nèi)存中(這個變量A主要體現(xiàn)是在一個類中的一個成員變量镰吆,是一個有狀態(tài)的對象帘撰,例如:UserController控制器中的一個整形類型的成員變量),如果不加任何控制的話万皿,變量A同時都會在JVM分配一塊內(nèi)存摧找,三個請求發(fā)過來同時對這個變量操作,顯然結(jié)果是不對的牢硅!即使不是同時發(fā)過來蹬耘,三個請求分別操作三個不同JVM內(nèi)存區(qū)域的數(shù)據(jù),變量A之間不存在共享减余,也不具有可見性综苔,處理的結(jié)果也是不對的!
如果我們業(yè)務(wù)中確實存在這個場景的話位岔,我們就需要一種方法解決這個問題如筛!
為了保證一個方法或?qū)傩栽诟卟l(fā)情況下的同一時間只能被同一個線程執(zhí)行,在傳統(tǒng)單體應(yīng)用單機部署的情況下抒抬,可以使用Java并發(fā)處理相關(guān)的API(如ReentrantLock或Synchronized)進行互斥控制杨刨。在單機環(huán)境中,Java中提供了很多并發(fā)處理相關(guān)的API擦剑。但是妖胀,隨著業(yè)務(wù)發(fā)展的需要芥颈,原單體單機部署的系統(tǒng)被演化成分布式集群系統(tǒng)后,由于分布式系統(tǒng)多線程做粤、多進程并且分布在不同機器上浇借,這將使原單機部署情況下的并發(fā)控制鎖策略失效,單純的Java API并不能提供分布式鎖的能力怕品。為了解決這個問題就需要一種跨JVM的互斥機制來控制共享資源的訪問妇垢,這就是分布式鎖要解決的問題!
二肉康、分布式鎖應(yīng)該具備哪些條件
在分析分布式鎖的三種實現(xiàn)方式之前闯估,先了解一下分布式鎖應(yīng)該具備哪些條件:
1、在分布式系統(tǒng)環(huán)境下吼和,一個方法在同一時間只能被一個機器的一個線程執(zhí)行涨薪;
2、高可用的獲取鎖與釋放鎖炫乓;
3刚夺、高性能的獲取鎖與釋放鎖;
4末捣、具備可重入特性侠姑;
5、具備鎖失效機制箩做,防止死鎖莽红;
6、具備非阻塞鎖特性邦邦,即沒有獲取到鎖將直接返回獲取鎖失敗安吁。
三、分布式鎖的三種實現(xiàn)方式
目前幾乎很多大型網(wǎng)站及應(yīng)用都是分布式部署的燃辖,分布式場景中的數(shù)據(jù)一致性問題一直是一個比較重要的話題鬼店。分布式的CAP理論告訴我們“任何一個分布式系統(tǒng)都無法同時滿足一致性(Consistency)、可用性(Availability)和分區(qū)容錯性(Partition tolerance)郭赐,最多只能同時滿足兩項薪韩。”所以捌锭,很多系統(tǒng)在設(shè)計之初就要對這三者做出取舍俘陷。在互聯(lián)網(wǎng)領(lǐng)域的絕大多數(shù)的場景中,都需要犧牲強一致性來換取系統(tǒng)的高可用性观谦,系統(tǒng)往往只需要保證“最終一致性”拉盾,只要這個最終時間是在用戶可以接受的范圍內(nèi)即可。
在很多場景中豁状,我們?yōu)榱吮WC數(shù)據(jù)的最終一致性捉偏,需要很多的技術(shù)方案來支持倒得,比如分布式事務(wù)、分布式鎖等夭禽。有的時候霞掺,我們需要保證一個方法在同一時間內(nèi)只能被同一個線程執(zhí)行。
基于數(shù)據(jù)庫實現(xiàn)分布式鎖讹躯;
基于緩存(Redis等)實現(xiàn)分布式鎖菩彬;
基于Zookeeper實現(xiàn)分布式鎖;
盡管有這三種方案潮梯,但是不同的業(yè)務(wù)也要根據(jù)自己的情況進行選型骗灶,他們之間沒有最好只有更適合!
四秉馏、基于數(shù)據(jù)庫的實現(xiàn)方式
基于數(shù)據(jù)庫的實現(xiàn)方式的核心思想是:在數(shù)據(jù)庫中創(chuàng)建一個表耙旦,表中包含方法名等字段,并在方法名字段上創(chuàng)建唯一索引萝究,想要執(zhí)行某個方法免都,就使用這個方法名向表中插入數(shù)據(jù),成功插入則獲取鎖帆竹,執(zhí)行完成后刪除對應(yīng)的行數(shù)據(jù)釋放鎖琴昆。
(1)創(chuàng)建一個表:
DROP TABLE IF EXISTS `method_lock`;
CREATE TABLE `method_lock` (
? `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',
? `method_name` varchar(64) NOT NULL COMMENT '鎖定的方法名',
? `desc` varchar(255) NOT NULL COMMENT '備注信息',
? `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
? PRIMARY KEY (`id`),
? UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';
1
2
3
4
5
6
7
8
9
(2)想要執(zhí)行某個方法,就使用這個方法名向表中插入數(shù)據(jù):
INSERT INTO method_lock (method_name, desc) VALUES ('methodName', '測試的methodName');
1
因為我們對method_name做了唯一性約束馆揉,這里如果有多個請求同時提交到數(shù)據(jù)庫的話,數(shù)據(jù)庫會保證只有一個操作可以成功抖拦,那么我們就可以認為操作成功的那個線程獲得了該方法的鎖升酣,可以執(zhí)行方法體內(nèi)容。
(3)成功插入則獲取鎖态罪,執(zhí)行完成后刪除對應(yīng)的行數(shù)據(jù)釋放鎖:
delete from method_lock where method_name ='methodName';
1
注意:這只是使用基于數(shù)據(jù)庫的一種方法噩茄,使用數(shù)據(jù)庫實現(xiàn)分布式鎖還有很多其他的玩法!
使用基于數(shù)據(jù)庫的這種實現(xiàn)方式很簡單复颈,但是對于分布式鎖應(yīng)該具備的條件來說绩聘,它有一些問題需要解決及優(yōu)化:
1、因為是基于數(shù)據(jù)庫實現(xiàn)的耗啦,數(shù)據(jù)庫的可用性和性能將直接影響分布式鎖的可用性及性能凿菩,所以,數(shù)據(jù)庫需要雙機部署帜讲、數(shù)據(jù)同步衅谷、主備切換;
2似将、不具備可重入的特性获黔,因為同一個線程在釋放鎖之前蚀苛,行數(shù)據(jù)一直存在,無法再次成功插入數(shù)據(jù)玷氏,所以堵未,需要在表中新增一列,用于記錄當(dāng)前獲取到鎖的機器和線程信息盏触,在再次獲取鎖的時候渗蟹,先查詢表中機器和線程信息是否和當(dāng)前機器和線程相同,若相同則直接獲取鎖耻陕;
3拙徽、沒有鎖失效機制,因為有可能出現(xiàn)成功插入數(shù)據(jù)后诗宣,服務(wù)器宕機了膘怕,對應(yīng)的數(shù)據(jù)沒有被刪除,當(dāng)服務(wù)恢復(fù)后一直獲取不到鎖召庞,所以岛心,需要在表中新增一列,用于記錄失效時間篮灼,并且需要有定時任務(wù)清除這些失效的數(shù)據(jù)忘古;
4、不具備阻塞鎖特性诅诱,獲取不到鎖直接返回失敗髓堪,所以需要優(yōu)化獲取邏輯,循環(huán)多次去獲取娘荡。
5干旁、在實施的過程中會遇到各種不同的問題,為了解決這些問題炮沐,實現(xiàn)方式將會越來越復(fù)雜争群;依賴數(shù)據(jù)庫需要一定的資源開銷,性能問題需要考慮大年。
五换薄、基于Redis的實現(xiàn)方式
1、選用Redis實現(xiàn)分布式鎖原因:
(1)Redis有很高的性能翔试;
(2)Redis命令對此支持較好轻要,實現(xiàn)起來比較方便
2、使用命令介紹:
(1)SETNX
SETNX key val:當(dāng)且僅當(dāng)key不存在時垦缅,set一個key為val的字符串伦腐,返回1;若key存在失都,則什么都不做柏蘑,返回0幸冻。
1
(2)expire
expire key timeout:為key設(shè)置一個超時時間,單位為second咳焚,超過這個時間鎖會自動釋放洽损,避免死鎖。
1
(3)delete
delete key:刪除key
1
在使用Redis實現(xiàn)分布式鎖的時候革半,主要就會使用到這三個命令碑定。
3、實現(xiàn)思想:
(1)獲取鎖的時候又官,使用setnx加鎖延刘,并使用expire命令為鎖添加一個超時時間,超過該時間則自動釋放鎖六敬,鎖的value值為一個隨機生成的UUID碘赖,通過此在釋放鎖的時候進行判斷。
(2)獲取鎖的時候還設(shè)置一個獲取的超時時間外构,若超過這個時間則放棄獲取鎖普泡。
(3)釋放鎖的時候,通過UUID判斷是不是該鎖审编,若是該鎖撼班,則執(zhí)行delete進行鎖釋放。
4垒酬、 分布式鎖的簡單實現(xiàn)代碼:
/**
* 分布式鎖的簡單實現(xiàn)代碼
* Created by liuyang on 2017/4/20.
*/
public class DistributedLock {
? ? private final JedisPool jedisPool;
? ? public DistributedLock(JedisPool jedisPool) {
? ? ? ? this.jedisPool = jedisPool;
? ? }
? ? /**
? ? * 加鎖
? ? * @param lockName? ? ? 鎖的key
? ? * @param acquireTimeout 獲取超時時間
? ? * @param timeout? ? ? ? 鎖的超時時間
? ? * @return 鎖標(biāo)識
? ? */
? ? public String lockWithTimeout(String lockName, long acquireTimeout, long timeout) {
? ? ? ? Jedis conn = null;
? ? ? ? String retIdentifier = null;
? ? ? ? try {
? ? ? ? ? ? // 獲取連接
? ? ? ? ? ? conn = jedisPool.getResource();
? ? ? ? ? ? // 隨機生成一個value
? ? ? ? ? ? String identifier = UUID.randomUUID().toString();
? ? ? ? ? ? // 鎖名砰嘁,即key值
? ? ? ? ? ? String lockKey = "lock:" + lockName;
? ? ? ? ? ? // 超時時間,上鎖后超過此時間則自動釋放鎖
? ? ? ? ? ? int lockExpire = (int) (timeout / 1000);
? ? ? ? ? ? // 獲取鎖的超時時間勘究,超過這個時間則放棄獲取鎖
? ? ? ? ? ? long end = System.currentTimeMillis() + acquireTimeout;
? ? ? ? ? ? while (System.currentTimeMillis() < end) {
? ? ? ? ? ? ? ? if (conn.setnx(lockKey, identifier) == 1) {
? ? ? ? ? ? ? ? ? ? conn.expire(lockKey, lockExpire);
? ? ? ? ? ? ? ? ? ? // 返回value值般码,用于釋放鎖時間確認
? ? ? ? ? ? ? ? ? ? retIdentifier = identifier;
? ? ? ? ? ? ? ? ? ? return retIdentifier;
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? // 返回-1代表key沒有設(shè)置超時時間,為key設(shè)置一個超時時間
? ? ? ? ? ? ? ? if (conn.ttl(lockKey) == -1) {
? ? ? ? ? ? ? ? ? ? conn.expire(lockKey, lockExpire);
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? try {
? ? ? ? ? ? ? ? ? ? Thread.sleep(10);
? ? ? ? ? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? ? ? ? ? Thread.currentThread().interrupt();
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? } catch (JedisException e) {
? ? ? ? ? ? e.printStackTrace();
? ? ? ? } finally {
? ? ? ? ? ? if (conn != null) {
? ? ? ? ? ? ? ? conn.close();
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? return retIdentifier;
? ? }
? ? /**
? ? * 釋放鎖
? ? * @param lockName? 鎖的key
? ? * @param identifier 釋放鎖的標(biāo)識
? ? * @return
? ? */
? ? public boolean releaseLock(String lockName, String identifier) {
? ? ? ? Jedis conn = null;
? ? ? ? String lockKey = "lock:" + lockName;
? ? ? ? boolean retFlag = false;
? ? ? ? try {
? ? ? ? ? ? conn = jedisPool.getResource();
? ? ? ? ? ? while (true) {
? ? ? ? ? ? ? ? // 監(jiān)視lock乱顾,準(zhǔn)備開始事務(wù)
? ? ? ? ? ? ? ? conn.watch(lockKey);
? ? ? ? ? ? ? ? // 通過前面返回的value值判斷是不是該鎖,若是該鎖宫静,則刪除走净,釋放鎖
? ? ? ? ? ? ? ? if (identifier.equals(conn.get(lockKey))) {
? ? ? ? ? ? ? ? ? ? Transaction transaction = conn.multi();
? ? ? ? ? ? ? ? ? ? transaction.del(lockKey);
? ? ? ? ? ? ? ? ? ? List<Object> results = transaction.exec();
? ? ? ? ? ? ? ? ? ? if (results == null) {
? ? ? ? ? ? ? ? ? ? ? ? continue;
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? retFlag = true;
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? conn.unwatch();
? ? ? ? ? ? ? ? break;
? ? ? ? ? ? }
? ? ? ? } catch (JedisException e) {
? ? ? ? ? ? e.printStackTrace();
? ? ? ? } finally {
? ? ? ? ? ? if (conn != null) {
? ? ? ? ? ? ? ? conn.close();
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? return retFlag;
? ? }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
5、測試剛才實現(xiàn)的分布式鎖
例子中使用50個線程模擬秒殺一個商品孤里,使用–運算符來實現(xiàn)商品減少伏伯,從結(jié)果有序性就可以看出是否為加鎖狀態(tài)。
模擬秒殺服務(wù)捌袜,在其中配置了jedis線程池说搅,在初始化的時候傳給分布式鎖,供其使用虏等。
/**
* Created by liuyang on 2017/4/20.
*/
public class Service {
? ? private static JedisPool pool = null;
? ? private DistributedLock lock = new DistributedLock(pool);
? ? int n = 500;
? ? static {
? ? ? ? JedisPoolConfig config = new JedisPoolConfig();
? ? ? ? // 設(shè)置最大連接數(shù)
? ? ? ? config.setMaxTotal(200);
? ? ? ? // 設(shè)置最大空閑數(shù)
? ? ? ? config.setMaxIdle(8);
? ? ? ? // 設(shè)置最大等待時間
? ? ? ? config.setMaxWaitMillis(1000 * 100);
? ? ? ? // 在borrow一個jedis實例時弄唧,是否需要驗證适肠,若為true,則所有jedis實例均是可用的
? ? ? ? config.setTestOnBorrow(true);
? ? ? ? pool = new JedisPool(config, "127.0.0.1", 6379, 3000);
? ? }
? ? public void seckill() {
? ? ? ? // 返回鎖的value值候引,供釋放鎖時候進行判斷
? ? ? ? String identifier = lock.lockWithTimeout("resource", 5000, 1000);
? ? ? ? System.out.println(Thread.currentThread().getName() + "獲得了鎖");
? ? ? ? System.out.println(--n);
? ? ? ? lock.releaseLock("resource", identifier);
? ? }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
模擬線程進行秒殺服務(wù):
public class ThreadA extends Thread {
? ? private Service service;
? ? public ThreadA(Service service) {
? ? ? ? this.service = service;
? ? }
? ? @Override
? ? public void run() {
? ? ? ? service.seckill();
? ? }
}
public class Test {
? ? public static void main(String[] args) {
? ? ? ? Service service = new Service();
? ? ? ? for (int i = 0; i < 50; i++) {
? ? ? ? ? ? ThreadA threadA = new ThreadA(service);
? ? ? ? ? ? threadA.start();
? ? ? ? }
? ? }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
結(jié)果如下侯养,結(jié)果為有序的:
若注釋掉使用鎖的部分:
public void seckill() {
? ? // 返回鎖的value值,供釋放鎖時候進行判斷
? ? //String indentifier = lock.lockWithTimeout("resource", 5000, 1000);
? ? System.out.println(Thread.currentThread().getName() + "獲得了鎖");
? ? System.out.println(--n);
? ? //lock.releaseLock("resource", indentifier);
}
1
2
3
4
5
6
7
從結(jié)果可以看出澄干,有一些是異步進行的:
5逛揩、基于ZooKeeper的實現(xiàn)方式
ZooKeeper是一個為分布式應(yīng)用提供一致性服務(wù)的開源組件,它內(nèi)部是一個分層的文件系統(tǒng)目錄樹結(jié)構(gòu)麸俘,規(guī)定同一個目錄下只能有一個唯一文件名辩稽。基于ZooKeeper實現(xiàn)分布式鎖的步驟如下:
(1)創(chuàng)建一個目錄mylock从媚;
(2)線程A想獲取鎖就在mylock目錄下創(chuàng)建臨時順序節(jié)點逞泄;
(3)獲取mylock目錄下所有的子節(jié)點,然后獲取比自己小的兄弟節(jié)點静檬,如果不存在炭懊,則說明當(dāng)前線程順序號最小,獲得鎖拂檩;
(4)線程B獲取所有節(jié)點侮腹,判斷自己不是最小節(jié)點,設(shè)置監(jiān)聽比自己次小的節(jié)點稻励;
(5)線程A處理完父阻,刪除自己的節(jié)點,線程B監(jiān)聽到變更事件望抽,判斷自己是不是最小的節(jié)點加矛,如果是則獲得鎖。
這里推薦一個Apache的開源庫Curator煤篙,它是一個ZooKeeper客戶端斟览,Curator提供的InterProcessMutex是分布式鎖的實現(xiàn),acquire方法用于獲取鎖辑奈,release方法用于釋放鎖苛茂。
優(yōu)點:具備高可用、可重入鸠窗、阻塞鎖特性妓羊,可解決失效死鎖問題。
缺點:因為需要頻繁的創(chuàng)建和刪除節(jié)點稍计,性能上不如Redis方式躁绸。