分布式鎖的實(shí)現(xiàn)之redis篇
點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號”
優(yōu)質(zhì)文章,第一時間送達(dá)
作者 | Virtuals
來源 | urlify.cn/7FJ7Rr
76套java從入門到精通實(shí)戰(zhàn)課程分享
為什么需要分布式鎖
引入經(jīng)典的秒殺情景,100件商品供客戶搶。如果是單機(jī)版的話,我們使用synchronized 或者 lock 都可以實(shí)現(xiàn)線程安全。但是如果多個服務(wù)器的話,synchronized 和 lock 就不管用了(廢話,怎么可能管用,都不在同一段代碼了)。
分布式鎖就是被設(shè)計出來實(shí)現(xiàn)多個服務(wù)器的線程安全。
很容易想到的方案是把共享變量(鎖)抽取出來放在一個公共的數(shù)據(jù)庫里(Redis、Memchhed)里,所有的服務(wù)器通過這個公共的資源實(shí)現(xiàn)數(shù)據(jù)的一致性,防止超賣。
具體實(shí)現(xiàn)
分布式鎖的實(shí)現(xiàn)方式有:Memchched分布式鎖、Redis分布式鎖、Zookeeper分布式鎖,這里我們以Redis分布式鎖為例,Redis分布式鎖也是現(xiàn)在使用得最多的
1. 思路
setnx加鎖
setnx是實(shí)現(xiàn)分布式的核心,意思是只有當(dāng)前key不存在才返回1,當(dāng)前key存在返回0
這個key就是我們的“鎖”,只有線程獲得鎖才能繼續(xù)執(zhí)行,執(zhí)行完del這個key相當(dāng)于解鎖操作。這個就是redis實(shí)現(xiàn)分布式鎖的核心,怎么樣,很好理解吧
del解鎖
2. 第一個問題:鎖無法被釋放
試想一下,如果你執(zhí)行完set命令服務(wù)器宕機(jī)了,來不及del解鎖,那么這個鎖永遠(yuǎn)無法被釋放,其他線程無法執(zhí)行。
解決方法是key必須設(shè)置一個超時時間,即使沒有被顯示釋放,也在超時后自動釋放。
redis為我們提供了這個命令設(shè)置超時時間
expire key ttl 秒為單位
pexpire key ttl 毫秒為單位
expireat key timestamp
pexpireat key timestamp
因此加鎖的操作變成:
setnx lock 1
expire lock 10
但是這兩個操作不保證原子性(Redis單條操作保證原子性),如果加完鎖還沒設(shè)置過期時間服務(wù)器就宕機(jī)了,同樣會導(dǎo)致死鎖,因此加鎖整個操作必須保證原子性。
redis提供了set+過期時間的原子操作
set lock 1 EX 10 NX
// 最終的加鎖命令
3. 第二個問題:錯誤釋放鎖
第二個問題,如果線程執(zhí)行時間超過TTL,當(dāng)前鎖被自動銷毀
但是等線程執(zhí)行完了,原來的del方法還會執(zhí)行,它就會去執(zhí)行解鎖操作,把其他線程占用的鎖給del了,這會產(chǎn)生非常嚴(yán)重的問題
String REDIS_Lock="lock";
String value=1;
try{
redisUtil.setLockDistribute(REDIS_LOCK,1,10);
......業(yè)務(wù)邏輯
}finally{
// 這個操作有可能會誤刪鎖
redisUtil.del(REDIS_LOCK);
}
解決方案是key的value不再是默認(rèn)的了
String REDIS_Lock="lock";
String value=UUID.randomUUID().toString()+Thread.currentThread().getname();
try{
redisUtil.setLockDistribute(REDIS_LOCK,1,10);
......業(yè)務(wù)邏輯
}finally{
// 先判斷后刪除
if(redisUtil.get(REDIS_LOCK).equals(value)){
redisUtil.del(REDIS_LOCK);
}
}
這樣寫其實(shí)還有個問題,判斷和刪除無法保證原子性,還是有可能誤刪。因此解鎖我們使用lua腳本來保證原子性:工具類有實(shí)現(xiàn)lua腳本的方法。
//lua腳本刪除key原子操作
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
(解鎖操作也可用事務(wù)來保證原子性,應(yīng)付面試,實(shí)戰(zhàn)還是lua腳本)
4. 第三個問題:超時解鎖導(dǎo)致并發(fā)
加鎖和解鎖操作我們都搞定了,但是還有一個問題:如果你的線程執(zhí)行時間超過ttl過期時間,鎖還是被釋放了,其他線程可以和次線程并發(fā)執(zhí)行,這是我們并不想看到的。
因此我們要為ttl延時
我們可以讓獲得鎖的線程開啟一個守護(hù)線程,用來給快要過期的鎖“續(xù)航”。
5. 集群環(huán)境下可能出現(xiàn)的問題
redis集群環(huán)境,多個master,多個slave的情況下:
當(dāng)主節(jié)點(diǎn)掛掉時,從節(jié)點(diǎn)會取而代之,但客戶端無明顯感知。當(dāng)客戶端 A 成功加鎖,指令還未同步,此時主節(jié)點(diǎn)掛掉,從節(jié)點(diǎn)提升為主節(jié)點(diǎn),新的主節(jié)點(diǎn)沒有鎖的數(shù)據(jù),當(dāng)客戶端 B 加鎖時就會成功。
也就是主結(jié)點(diǎn)加了鎖就宕機(jī)了,從節(jié)點(diǎn)還沒同步,當(dāng)該從節(jié)點(diǎn)提升為主節(jié)點(diǎn)時就會出錯。
解決方案我也不清楚....以后碰到再找資料
開源框架Redisson
上面的流程如果手寫的話會要人老命,開源框架Redisson幫我們擺平一切,現(xiàn)在用得十分多
直接上代碼:
// 注入redisson
public Redisson redisson(){
Config config=new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
return Redisson.create(config);
}
@Autowired
Redisson son;
String REDIS_Lock="lock";
String value=UUID.randomUUID().toString()+Thread.currentThread().getname();
RLock lock=son.getLock();
try{
lock.lock();
......業(yè)務(wù)邏輯
}finally{
lock.unlock();
}
// 這段代碼會解決上述三個問題,集群環(huán)境下redis分布式鎖的實(shí)現(xiàn)
結(jié)語
分布式鎖看起來難其實(shí)原理還是很簡單的,沒事多看看官方文檔,講得挺細(xì)致的
粉絲福利:Java從入門到入土學(xué)習(xí)路線圖
??????

??長按上方微信二維碼 2 秒
感謝點(diǎn)贊支持下哈 
