對于分布式鎖可以從幾個(gè)問題入手:
- 分布式鎖應(yīng)用的場景是什么樣的呢?
- 分布式鎖的實(shí)現(xiàn)是怎么樣的呢?
- 分布式鎖的實(shí)現(xiàn)方案應(yīng)用有什么優(yōu)劣勢?
- 分布式鎖的方案如何選擇?等
一、分布式鎖概念
1、什么是分布式鎖
- 分布式鎖在分布式環(huán)境中,針對多個(gè)進(jìn)程訪問共享資源而提供的同步方案。相對于單進(jìn)程的鎖,分布式鎖更加復(fù)雜,分布式的不可靠性,需要分布式鎖考慮更多的東西。
- 分布式鎖作用:保證共享資源的正確性,某一時(shí)刻鎖定全局資源,讓進(jìn)程串行化執(zhí)行,能有效避免共享資源被多個(gè)進(jìn)程同時(shí)訪問時(shí)候出現(xiàn)的數(shù)據(jù)正確性問題。比如防止重復(fù)下單、解決業(yè)務(wù)層冪等問題、解決消息隊(duì)列重復(fù)消費(fèi)問題、解決數(shù)據(jù)不一致問題等等
2、分布式鎖的設(shè)計(jì)目標(biāo)
1、可以保證在分布式部署的應(yīng)用集群中,同一個(gè)方法在同一時(shí)間只能被一臺(tái)機(jī)器上的一個(gè)線程執(zhí)行。
2、這把鎖要是一把可重入鎖(避免死鎖)
3、這把鎖最好是一把阻塞鎖(根據(jù)業(yè)務(wù)需求考慮要不要這條)
4、這把鎖最好是一把公平鎖(根據(jù)業(yè)務(wù)需求考慮要不要這條)
5、有高可用的獲取鎖和釋放鎖功能(服務(wù)高可用,系統(tǒng)穩(wěn)健)
6、獲取鎖和釋放鎖的性能要好
7、鎖的自動(dòng)續(xù)約與自動(dòng)釋放
8、代碼高度抽象,業(yè)務(wù)接入非常簡單
9、可視化的管理后臺(tái),監(jiān)控與管理
3、常見分布式鎖解決方案
MySql
Zk
Redis
自研分布式鎖:如谷歌的Chubby。
etcd
二、基于mysql實(shí)現(xiàn)分布式鎖
1、基于表主鍵唯一做分布式鎖
原理:利用主鍵唯一的特性,如果有多個(gè)請求同時(shí)提交到數(shù)據(jù)庫的話,數(shù)據(jù)庫會(huì)保證只有一個(gè)操作可以成功,那么我們就可以認(rèn)為操作成功的那個(gè)線程獲得了該方法的鎖,當(dāng)方法執(zhí)行完畢之后,想要釋放鎖的話,刪除這條數(shù)據(jù)庫記錄即可。(聯(lián)合主鍵或者唯一主鍵但是不能使自增主鍵)
- 存在的問題
1、這把鎖強(qiáng)依賴數(shù)據(jù)庫的可用性,數(shù)據(jù)庫是一個(gè)單點(diǎn),一旦數(shù)據(jù)庫掛掉,會(huì)導(dǎo)致業(yè)務(wù)系統(tǒng)不可用。
2、這把鎖沒有失效時(shí)間,一旦解鎖操作失敗,就會(huì)導(dǎo)致鎖記錄一直在數(shù)據(jù)庫中,其他線程無法再獲得到鎖。
3、這把鎖只能是非阻塞的,因?yàn)閿?shù)據(jù)的insert操作,一旦插入失敗就會(huì)直接報(bào)錯(cuò)。沒有獲得鎖的線程并不會(huì)進(jìn)入排隊(duì)隊(duì)列,要想再次獲得鎖就要再次觸發(fā)獲得鎖操作。
4、這把鎖是非重入的,同一個(gè)線程在沒有釋放鎖之前無法再次獲得該鎖。因?yàn)閿?shù)據(jù)中數(shù)據(jù)已經(jīng)存在了。
5、這把鎖是非公平鎖,所有等待鎖的線程憑運(yùn)氣去爭奪鎖。
- 當(dāng)然,我們也可以有其他方式解決上面的問題。
1、數(shù)據(jù)庫是單點(diǎn)?搞兩個(gè)數(shù)據(jù)庫,數(shù)據(jù)之前雙向同步。一旦掛掉快速切換到備庫上。
2、沒有失效時(shí)間?只要做一個(gè)定時(shí)任務(wù),每隔一定時(shí)間把數(shù)據(jù)庫中的超時(shí)數(shù)據(jù)清理一遍。
3、非阻塞的?搞一個(gè)while循環(huán),直到insert成功再返回成功。
4、非重入的?在數(shù)據(jù)庫表中加個(gè)字段,記錄當(dāng)前獲得鎖的機(jī)器的主機(jī)信息和線程信息,那么下次再獲取鎖的時(shí)候先查詢數(shù)據(jù)庫,如果當(dāng)前機(jī)器的主機(jī)信息和線程信息在數(shù)據(jù)庫可以查到的話,直接把鎖分配給他就可以了。
5、非公平的?再建一張中間表,將等待鎖的線程全記錄下來,并根據(jù)創(chuàng)建時(shí)間排序,只有最先創(chuàng)建的允許獲取鎖
- 這種方案總體來說性能依靠于mysql,且會(huì)產(chǎn)生鎖表等現(xiàn)象,在高并發(fā)場景是不適用的。
2、基于數(shù)據(jù)庫排他鎖做分布式鎖
在查詢語句后面增加for update,數(shù)據(jù)庫會(huì)在查詢過程中給數(shù)據(jù)庫表增加排他鎖 (注意: InnoDB 引擎在加鎖的時(shí)候,只有通過索引進(jìn)行檢索的時(shí)候才會(huì)使用行級(jí)鎖,否則會(huì)使用表級(jí)鎖。這里我們希望使用行級(jí)鎖,就要給要執(zhí)行的方法字段名添加索引,值得注意的是,這個(gè)索引一定要?jiǎng)?chuàng)建成唯一索引,否則會(huì)出現(xiàn)多個(gè)重載方法之間無法同時(shí)被訪問的問題。重載方法的話建議把參數(shù)類型也加上。)。當(dāng)某條記錄被加上排他鎖之后,其他線程無法再在該行記錄上增加排他鎖。
思路:我們可以認(rèn)為獲得排他鎖的線程即可獲得分布式鎖,當(dāng)獲取到鎖之后,可以執(zhí)行方法的業(yè)務(wù)邏輯,執(zhí)行完方法之后,通過connection.commit()操作來釋放鎖。
示例:
BEGIN;(確保以下2步驟在一個(gè)事務(wù)中:)
SELECT * FROM tb_product_stock WHERE product_id=1 FOR UPDATE--->product_id有索引,鎖行,無索引,表鎖(加鎖階段)
UPDATE tb_product_stock SET number=number-1 WHERE product_id=1--->更新庫存
COMMIT;( 解鎖階段)
- 存在的問題
性能依賴于mysql,同時(shí)雖然使用了索引,在查詢優(yōu)化的時(shí)候未必使用行鎖有可能使用了表鎖。也有可能引發(fā)死鎖等其他意外發(fā)生
3、基于版本控制實(shí)現(xiàn)的樂觀鎖
- 這個(gè)策略源于 mysql 的 mvcc 機(jī)制,使用這個(gè)策略其實(shí)本身沒有什么問題,唯一的問題就是對數(shù)據(jù)表侵入較大,我們要為每個(gè)表設(shè)計(jì)一個(gè)版本號(hào)字段,然后寫一條判斷 sql 每次進(jìn)行判斷,增加了數(shù)據(jù)庫操作的次數(shù),在高并發(fā)的要求下,對數(shù)據(jù)庫連接的開銷也是無法忍受的。
思路:我們可以對我們的表加一個(gè)版本號(hào)字段,那么我們查詢出來一個(gè)版本號(hào)之后,update或者delete的時(shí)候需要依賴我們查詢出來的版本號(hào),判斷當(dāng)前數(shù)據(jù)庫和查詢出來的版本號(hào)是否相等,如果相等那么就可以執(zhí)行,如果不等那么就不能執(zhí)行。這樣的一個(gè)策略很像我們的CAS(Compare And Swap),比較并交換是一個(gè)原子操作
示例:
BEGIN;(確保以下2步驟在一個(gè)事務(wù)中:)
SELECT number FROM tb_product_stock WHERE product_id=1--》查詢庫存總數(shù),不加鎖
UPDATE tb_product_stock SET number=number-1 WHERE product_id=1 AND number=第一步查詢到的庫存數(shù)--》number字段作為版本控制字段(這里版本的控制可以根據(jù)業(yè)務(wù)調(diào)整)
COMMIT;
4、mysql作為分布式鎖總結(jié)
- 優(yōu)點(diǎn):直接借助數(shù)據(jù)庫,容易理解。缺點(diǎn):會(huì)有各種各樣的問題,在解決問題的過程中會(huì)使整個(gè)方案變得越來越復(fù)雜。操作數(shù)據(jù)庫需要一定的開銷,性能問題需要考慮。也就是說在高并發(fā)場景數(shù)據(jù)庫作為分布式鎖并不適用。
三、redis作為分布式鎖
- redis實(shí)現(xiàn)分布式鎖原理主要是利用redis中setNX命令的原子性操作來實(shí)現(xiàn)的。
1、基于jedis實(shí)現(xiàn)分布式鎖
/**
* 基于jedis實(shí)現(xiàn)的分布式鎖
*/
public class RedisDistributeLockUtils {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
private static final Long RELEASE_SUCCESS = 1L;
/**
* @param jedis
* @param lockKey
* @param requestId 通過給value賦值為requestId,我們就知道這把鎖是哪個(gè)請求加的了,在解鎖的時(shí)候就可以有依據(jù)。
* requestId可以使用UUID.randomUUID().toString()方法生成。也就是鎖誰加的鎖誰來釋放
* @param expiredTime
* @return
*/
public boolean getDistributeLock(Jedis jedis, String lockKey, String requestId, Long expiredTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expiredTime);
if (LOCK_SUCCESS.equalsIgnoreCase(result)) {
return true;
}
return false;
}
public boolean releaseDistributeLock(Jedis jedis, String lockKey, String requestId) {
/**
* 1、過程:將Lua代碼傳到j(luò)edis.eval()方法里,并使參數(shù)KEYS[1]賦值為lockKey,
* ARGV[1]賦值為requestId。eval()方法是將Lua代碼交給Redis服務(wù)端執(zhí)行。
* 2、原理:首先獲取鎖對應(yīng)的value值,檢查是否與requestId相等,如果相等則刪除鎖(解鎖)。
* 那么為什么要使用Lua語言來實(shí)現(xiàn)呢?因?yàn)橐_保上述操作是原子性的。
*/
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, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
- 上面沒有考慮使用高可用情況,在通常的互聯(lián)網(wǎng)架構(gòu)中都是使用主從或者集群模式來實(shí)現(xiàn)redis的高可用。而集群模式是基于redis分片技術(shù),并不適合作為redis分布式鎖高可用方案,因此常用的是redis主從模式,在通常99.9999%的情況下面,redis是沒問題,但是當(dāng)主redis掛掉時(shí)候,而主從數(shù)據(jù)異步進(jìn)行時(shí)候,訪問此時(shí)從服務(wù)器中還沒有數(shù)據(jù),就會(huì)存在鎖失效問題。也就是說在極端情況下面會(huì)存在數(shù)據(jù)不一致問題。這也和redis集群是AP模型吻合。
- 在redis官方文檔中,推薦使用redlock來保證一致性問題。但是至少需要三個(gè)redis主從實(shí)例來完成,下面說說redlock算法實(shí)現(xiàn)。
2、redlock算法實(shí)現(xiàn)分布式鎖
- 在java中,實(shí)現(xiàn)了redlock鎖的開源框架是redisson,redLock實(shí)現(xiàn)分布式鎖的原理是:假設(shè)我們有N個(gè)Redis master節(jié)點(diǎn),這些節(jié)點(diǎn)都是完全獨(dú)立的,我們不用任何復(fù)制或者其他隱含的分布式協(xié)調(diào)算法。我們已經(jīng)描述了如何在單節(jié)點(diǎn)環(huán)境下安全地獲取和釋放鎖。因此我們理所當(dāng)然地應(yīng)當(dāng)用這個(gè)方法在每個(gè)單節(jié)點(diǎn)里來獲取和釋放鎖。在我們的例子里面我們把N設(shè)成5,這個(gè)數(shù)字是一個(gè)相對比較合理的數(shù)值,因此我們需要在不同的計(jì)算機(jī)或者虛擬機(jī)上運(yùn)行5個(gè)master節(jié)點(diǎn)來保證他們大多數(shù)情況下都不會(huì)同時(shí)宕機(jī)。一個(gè)客戶端需要做如下操作來獲取鎖:
1、獲取當(dāng)前時(shí)間(單位是毫秒)。
2、輪流用相同的key和隨機(jī)值在N個(gè)節(jié)點(diǎn)上請求鎖,在這一步里,客戶端在每個(gè)master上請求鎖時(shí),會(huì)有一個(gè)和總的鎖釋放時(shí)間相比小的多的超時(shí)時(shí)間。比如如果鎖自動(dòng)釋放時(shí)間是10秒鐘,那每個(gè)節(jié)點(diǎn)鎖請求的超時(shí)時(shí)間可能是5-50毫秒的范圍,這個(gè)可以防止一個(gè)客戶端在某個(gè)宕掉的master節(jié)點(diǎn)上阻塞過長時(shí)間,如果一個(gè)master節(jié)點(diǎn)不可用了,我們應(yīng)該盡快嘗試下一個(gè)master節(jié)點(diǎn)。
3、客戶端計(jì)算第二步中獲取鎖所花的時(shí)間,只有當(dāng)客戶端在大多數(shù)master節(jié)點(diǎn)上成功獲取了鎖(在這里是3個(gè)),而且總共消耗的時(shí)間不超過鎖釋放時(shí)間,這個(gè)鎖就認(rèn)為是獲取成功了。
4、如果鎖獲取成功了,那現(xiàn)在鎖自動(dòng)釋放時(shí)間就是最初的鎖釋放時(shí)間減去之前獲取鎖所消耗的時(shí)間。
5、如果鎖獲取失敗了,不管是因?yàn)楂@取成功的鎖不超過一半(N/2+1)還是因?yàn)榭傁臅r(shí)間超過了鎖釋放時(shí)間,客戶端都會(huì)到每個(gè)master節(jié)點(diǎn)上釋放鎖,即便是那些他認(rèn)為沒有獲取成功的鎖。
- springboot+redisson實(shí)現(xiàn)分布式鎖:
(1)、導(dǎo)入包
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.5.0</version>
</dependency>
(2)、配置文件
spring.redisson.password=123456
spring.redisson.clusters=127.0.0.1:6370,127.0.0.1:6371,127.0.0.1:6372
(3)配置redisson
@Configuration
public class RedissonConfig {
@Value("${spring.redisson.clusters}")
private String cluster;
@Value("${spring.redisson.password}")
private String password;
@Bean
public RedissonClient getRedisson(){
String[] nodes = cluster.split(",");
//redisson版本是3.5,集群的ip前面要加上“redis://”,不然會(huì)報(bào)錯(cuò),3.2版本可不加
for(int i=0;i<nodes.length;i++){
nodes[i] = "redis://"+nodes[i];
}
RedissonClient redisson = null;
Config config = new Config();
config.useClusterServers() //這是用的集群server
.setScanInterval(2000) //設(shè)置集群狀態(tài)掃描時(shí)間
.addNodeAddress(nodes)
.setPassword(password);
redisson = Redisson.create(config);
return redisson;
}
}
(4)分布式鎖使用
@Service
public class DistributeLockServiceImpl{
@Autowired
private RedissonClient redissonClient;
private final static String LOCK_KEYP_PREFIX="redisson:lock:test:";
@overiide
public void redissonTest(Long userId) {
//獲取key
String redisLockKey=LOCK_KEYP_PREFIX.concat(String.ValueOf(userId));
//獲取redisson鎖
RLock rlock = redissonClient.getLock(redisLockKey);
//設(shè)置鎖超時(shí)時(shí)間,防止異常造成死鎖
rlock.lock(20, TimeUnit.SECONDS);
try{
//todo 執(zhí)行業(yè)務(wù)邏輯
} catch(Exception e){
//異常處理
}finally{
//釋放鎖
rlock.unlock();
}
}
}
5、redLock算法實(shí)現(xiàn)的分布式鎖總結(jié)
- redLock實(shí)現(xiàn)的分布式鎖真的好的。關(guān)于redLock實(shí)現(xiàn)的鎖的論戰(zhàn)可以參考:https://juejin.im/post/59f592c65188255f5c5142d2
三、zookeeper實(shí)現(xiàn)的分布式鎖
- 關(guān)于zookeeper實(shí)現(xiàn)的分布式鎖可以參考博主的另一篇文章:
http://www.itdecent.cn/p/fc7ced6357f7
四、基于etcd實(shí)現(xiàn)的分布式鎖
- etcd和zk一樣作為分布式一致性的解決方案,具有zk的所有功能,就是說zk能做的etcd也能做。etcd是基于raft協(xié)議的一致性框架,相對paxos算法,更加容易理解,而且etcd提供了http+json的調(diào)用格式,更加符合restful風(fēng)格,還有一點(diǎn)重點(diǎn)的,基于etcd相對zk,性能會(huì)更加好。
- 由于對于etcd不是很熟悉,后續(xù)學(xué)習(xí)了在動(dòng)手寫個(gè)demo。
五、幾種主流的分布鎖對比
-
在生產(chǎn)上面對高并發(fā)基本不會(huì)使用數(shù)據(jù)作為分布式鎖的解決方法,絕大數(shù)情況是用redis作為分布式鎖的解決方案,但是對于一致性要求特別高的比如交易等系統(tǒng)內(nèi)就要考慮使用zk或者etcd實(shí)現(xiàn)的分布式鎖。具體三者的比較如下:
分布式鎖實(shí)現(xiàn)的主流方案對比
參考:
http://www.importnew.com/27477.html
https://juejin.im/post/59f592c65188255f5c5142d2
https://zhuanlan.zhihu.com/p/42056183
https://juejin.im/post/5bbb0d8df265da0abd3533a5
http://www.spring4all.com/question/158
