Java基于redis實(shí)現(xiàn)分布式鎖代碼實(shí)例
為什么會有這個(gè)需求:
例如一個(gè)簡單用戶的操作,一個(gè)線程去修改用戶狀態(tài),首先在在內(nèi)存中讀出用戶的狀態(tài),然后在內(nèi)存中進(jìn)行修改,然后在存到數(shù)據(jù)庫中。在單線程中,這是沒有問題的。但是在多線程中由于讀取,修改,寫入是三個(gè)操作,不是原子操作(同時(shí)成功或失?。虼嗽诙嗑€程中會存在數(shù)據(jù)的安全性問題。
這個(gè)問題的話,就可以用分布式鎖在限制程序的并發(fā)執(zhí)行。
實(shí)現(xiàn)思路:
就是進(jìn)來一個(gè)先占位,當(dāng)別的線程進(jìn)來操作的時(shí)候,發(fā)現(xiàn)有人占位了,就會放棄或者稍后再試。
占位的實(shí)現(xiàn):
在redis中的setnx命令來實(shí)現(xiàn),redis命令可以參考我這篇博客https://www.cnblogs.com/javazl/p/12657280.html,默認(rèn)set命令就是存值,當(dāng)key存在的時(shí)候,set就會覆蓋key的value值,而setnx則不會。當(dāng)沒有key的時(shí)候,setnx就會進(jìn)來先占位,當(dāng)key存在了,其他的setnx就進(jìn)不來了。。等到第一個(gè)執(zhí)行完成后,在del命令釋放位子。
代碼實(shí)現(xiàn):
public class LockTest {
public static void main(String[] args) {
Redis redis = new Redis();
redis.execute(jedis->{
Long setnx = jedis.setnx("k1", "v1");
//setnx的返回值為long類型
if (setnx == 1) {
//沒人占位
jedis.set("name", "zl");
String name = jedis.get("name");
System.out.println(name);
//釋放資源
jedis.del("k1");
}else{
//有人占位,停止/暫緩 操作
}
});
}
}
上邊代碼中,就是一個(gè)簡易的分布式鎖的實(shí)現(xiàn),但是有一個(gè)問題。就是如果在占位后釋放前掛了。那么這個(gè)線程會一直釋放不了,也就是del命令沒有調(diào)用,后面的全部請求都阻塞到這里,鎖就變成了死鎖。因此這里需要去優(yōu)化。
優(yōu)化的方法就是加過期時(shí)間,確保鎖在一定時(shí)間后能夠釋放.
public class LockTest {
public static void main(String[] args) {
Redis redis = new Redis();
redis.execute(jedis->{
Long setnx = jedis.setnx("k1", "v1");
if (setnx == 1) {
//給鎖添加一個(gè)過期時(shí)間,防止應(yīng)用在運(yùn)行過程中拋出異常導(dǎo)致鎖無法及時(shí)得到釋放
jedis.expire("k1", 5);
//沒人占位
jedis.set("name", "zl");
String name = jedis.get("name");
System.out.println(name);
jedis.del("k1");
}else{
//有人占位,停止/暫緩 操作
}
});
}
這樣處理后,就可以保證鎖可以正常的釋放。但是會有一個(gè)新的問題,就是如果在取鎖和設(shè)置過期時(shí)間服務(wù)器掛掉了,因?yàn)槿℃i,也就是setnx和設(shè)置過期時(shí)間是兩個(gè)操作,不具備原子性所以不可能同時(shí)完成。這個(gè)鎖就會被一直占用,無法得到釋放,成為死鎖。那么如何解決呢?
在redis2.8之后,setnx和expireke可以通過一個(gè)命令一起執(zhí)行,讓兩個(gè)操作變成一個(gè),就會解決這個(gè)問題。
優(yōu)化實(shí)現(xiàn):
public class LockTest {
public static void main(String[] args) {
Redis redis = new Redis();
redis.execute(jedis->{
//將兩個(gè)操作合并成一個(gè),nx就是setnx,ex就是expire
String set = jedis.set("k1", "v1", new SetParams().nx().ex(5));
//操作結(jié)果為okhuo或者error
if (set !=null && "OK".equals(set)) {
//給鎖添加一個(gè)過期時(shí)間,防止應(yīng)用在運(yùn)行過程中拋出異常導(dǎo)致鎖無法及時(shí)得到釋放
jedis.expire("k1", 5);
//沒人占位
jedis.set("name", "zl);
String name = jedis.get("name");
System.out.println(name);
//釋放資源
jedis.del("k1");
}else{
//有人占位,停止/暫緩 操作
}
});
}
}
用過期時(shí)間優(yōu)化后,雖然解決了死鎖的問題,但是又有一個(gè)新的問題產(chǎn)生,就是超時(shí)問題:
舉個(gè)例子:如果要執(zhí)行的業(yè)務(wù)很耗時(shí),可能會出現(xiàn)紊亂,當(dāng)?shù)匾粋€(gè)線程獲取到鎖的時(shí)候,開始執(zhí)行業(yè)務(wù)代碼,但是業(yè)務(wù)代碼很耗時(shí),假如過期時(shí)間是3秒,而業(yè)務(wù)執(zhí)行需要5秒,這樣,鎖就會提前釋放,然后第二個(gè)線程獲取到鎖并開始執(zhí)行。當(dāng)執(zhí)行到第2秒的時(shí)候,第一個(gè)鎖也執(zhí)行完了,此時(shí)第一個(gè)線程會釋放第二個(gè)線程的鎖,然后第三個(gè)線程繼續(xù)獲取鎖并執(zhí)行,當(dāng)?shù)降?秒的時(shí)候第二個(gè)線程執(zhí)行完了,那么又會提前釋放鎖,一直如此循環(huán),會造成線程的紊亂。
那么解決的思路主要有兩種
盡量避免耗時(shí)操作。
去處理鎖,給鎖的value設(shè)置隨機(jī)數(shù)或隨機(jī)字符串,每當(dāng)要釋放的時(shí)候去判斷這個(gè)value的值,如果是的話就去釋放,如果不是就不釋放,舉個(gè)例子,假設(shè)第一個(gè)線程進(jìn)來,它獲取鎖的value是1,如果發(fā)生超時(shí)就會進(jìn)入下一個(gè)線程,下一個(gè)線程會獲取新的value為
3,在釋放第二個(gè)所之前先去獲取value并比較,發(fā)現(xiàn)1不等于三,那么就不去釋放鎖。
第一種的話沒啥說的,但是第二種的話會有一個(gè)問題,就是釋放鎖會查看value,然后比較,然后釋放,會有三個(gè)操作,那么就不具備原子性,這樣操作的話,會出現(xiàn)死鎖。這里我們可以使用Lua腳本去處理。
Lua腳本的特點(diǎn):
1.使用方便,redis內(nèi)置了對Lua腳本的支持。
2.Lua可以在redis服務(wù)端原子性的執(zhí)行多個(gè)redis命令
3.由于網(wǎng)絡(luò)的原因會影響到redis的性能,因此,使用Lua可以讓多個(gè)命令同時(shí)執(zhí)行,降低了網(wǎng)絡(luò)給redis帶來的性能問題。
在redis中如何使用Lua腳本:
1.在redis服務(wù)端寫好,然后在java業(yè)務(wù)中調(diào)用腳本
2.可以直接在java中直接去寫,寫好后,需要執(zhí)行時(shí),每次將腳本發(fā)送到redis中去執(zhí)行。
創(chuàng)建Lua腳本:
//用redis.call調(diào)用一個(gè)redis命令,調(diào)的是get命令,這個(gè)key是從外面?zhèn)鬟M(jìn)來的keyif redis.call("get",KEYS[1])==ARGV[1] then//如果相等就去操作釋放命令
return redis.call("del",KEYS[1])
else
return 0
end
可以給Lua腳本求一個(gè)SHA1和:
cat lua/equal.lua | redis-cli -a root script load --pipe
script load這個(gè)命令會在Redis中緩存Lua腳本,并返回腳本內(nèi)容的SHA1校驗(yàn)和,然后在java中調(diào)用時(shí),傳入SHA1校驗(yàn)和作為參數(shù),這樣redis服務(wù)端就知道執(zhí)行那個(gè)腳本了。
接下來在java中編寫
public static void main(String[] args) {
Redis redis = new Redis();
for (int i = 0; i < 2; i++) {
redis.execute(jedis -> {
//1.先獲取一個(gè)隨機(jī)字符串
String value = UUID.randomUUID().toString();
//2.獲取鎖
String k1 = jedis.set("k1", value, new SetParams().nx().ex(5));
//3.判斷是否成功拿到鎖
if (k1 != null && "OK".equals(k1)) {
//4. 具體的業(yè)務(wù)操作
jedis.set("site", "zl");
String site = jedis.get("site");
System.out.println(site);
//5.釋放鎖
jedis.evalsha("b8059ba43af6ffe8bed3db65bac35d452f8115d8",
Arrays.asList("k1"), Arrays.asList(value));
} else {
System.out.println("沒拿到鎖");
}
});
}
}
}
這樣處理的話,就解決了死鎖的問題。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
在idea中使用JaCoCo插件統(tǒng)計(jì)單元測試覆蓋率的實(shí)現(xiàn)
這篇文章主要介紹了在idea中使用JaCoCo插件統(tǒng)計(jì)單元測試覆蓋率的實(shí)現(xiàn),具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-01-01
SpringBoot集成Flyway進(jìn)行數(shù)據(jù)庫版本遷移管理的步驟
這篇文章主要介紹了SpringBoot集成Flyway進(jìn)行數(shù)據(jù)庫版本遷移管理的步驟,幫助大家更好的理解和學(xué)習(xí)使用SpringBoot框架,感興趣的朋友可以了解下2021-03-03
用Java連接sqlserver數(shù)據(jù)庫時(shí)候幾個(gè)jar包的區(qū)別分析
這篇文章主要介紹了用Java連接sqlserver數(shù)據(jù)庫時(shí)候幾個(gè)jar包的區(qū)別分析,需要的朋友可以參考下2014-10-10
Spring Boot JDBC 連接數(shù)據(jù)庫示例
本篇文章主要介紹了Spring Boot JDBC 連接數(shù)據(jù)庫示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-02-02
SpringMVC?RESTFul實(shí)戰(zhàn)案例訪問首頁
這篇文章主要為大家介紹了SpringMVC?RESTFul實(shí)戰(zhàn)案例訪問首頁,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05
Java如何將任意類型的Object對象轉(zhuǎn)換為相應(yīng)的實(shí)體對象
這篇文章主要介紹了Java如何將任意類型的Object對象轉(zhuǎn)換為相應(yīng)的實(shí)體對象問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01

