準備環境
- 虛擬機或者服務器上有Redis
- Maven
- eclipse或者idea
- JDK8
添加依賴
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
為什么要做分布式鎖
例如一個簡單用戶的操作,一個執行緒去修改用戶狀態,首先在在記憶體中讀出用戶的狀態,然后在記憶體中進行修改,然后在存到資料庫中,在單執行緒中,這是沒有問題的,但是在多執行緒中由于讀取,修改,寫入是三個操作,不是原子操作(同時成功或失敗),因此在多執行緒中會存在資料的安全性問題,
實作思路
就是進來一個先占位,當別的執行緒進來操作的時候,發現有人占位了,就會放棄或者稍后再試,
在redis中的setnx命令來實作,默認set命令就是存值,當key存在的時候,set就會覆寫key的value值,而setnx則不會,當沒有key的時候,setnx就會進來先占位,當key存在了,其他的setnx就進不來了,等到第一個執行完成后,在del命令釋放位子,
實作代碼
CallWithJedis
package com.leo.study.utils;
import redis.clients.jedis.Jedis;
public interface CallWithJedis {
void call(Jedis jedis);
}
Redis
package com.leo.study.utils;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class Redis {
private JedisPool jedisPool;
public Redis() {
GenericObjectPoolConfig<Object> config = new GenericObjectPoolConfig<>();
// 設定最大空閑數
config.setMaxIdle(300);
// 設定最大連接數
config.setMaxTotal(1000);
// 設定最大等待時間
config.setMaxWaitMillis(30000);
// 空閑時檢查有效性
config.setTestOnBorrow(true);
jedisPool = new JedisPool(config, "192.168.0.118", 6379);
// jedisPool = new JedisPool(config, "127.0.0.1", 6379);
}
public void execute(CallWithJedis callWithJedis) {
try (Jedis jedis = jedisPool.getResource()) {
callWithJedis.call(jedis);
}
}
}
網上好些例子,都缺少這兩段代碼,只有關鍵實作的部分,可以參考https://www.cnblogs.com/javazl/p/12661730.html
這篇文章比較具有參考性,我這邊就直接寫最后的解決方法了
在測驗類里面寫
public void testLock() {
Redis redis = new Redis();
redis.execute(jedis -> {
String set = jedis.set("k1", "v1", new SetParams().nx().ex(5));
System.out.println(set);
if (set != null && "OK".equals(set)) {
// 給鎖添加一個過期時間,防止應用在運行程序中拋出例外導致鎖無法及時得到釋放
jedis.expire("k1", 5);
jedis.set("name", "javaboy");
String name = jedis.get("name");
System.out.println(name);
jedis.del("k1");// 釋放資源
}
});
}
注意OK是大寫的
用過期時間優化后,雖然解決了死鎖的問題,但是又有一個新的問題產生,就是超時問題:
舉個例子:如果要執行的業務很耗時,可能會出現紊亂,當地一個執行緒獲取到鎖的時候,開始執行業務代碼,但是業務代碼很耗時,假如過期時間是3秒,而業務執行需要5秒,這樣,鎖就會提前釋放,然后第二個執行緒獲取到鎖并開始執行,當執行到第2秒的時候,第一個鎖也執行完了,此時第一個執行緒會釋放第二個執行緒的鎖,然后第三個執行緒繼續獲取鎖并執行,當到第3秒的時候第二個執行緒執行完了,那么又會提前釋放鎖,一直如此回圈,會造成執行緒的紊亂,
那么解決的思路主要有兩種
盡量避免耗時操作,
去處理鎖,給鎖的value設定亂數或隨機字串,每當要釋放的時候去判斷這個value的值,如果是的話就去釋放,如果不是就不釋放,舉個例子,假設第一個執行緒進來,它獲取鎖的value是1,如果發生超時就會進入下一個執行緒,下一個執行緒會獲取新的value為3,在釋放第二個所之前先去獲取value并比較,發現1不等于三,那么就不去釋放鎖,
第一種的話沒啥說的,但是第二種的話會有一個問題,就是釋放鎖會查看value,然后比較,然后釋放,會有三個操作,那么就不具備原子性,這樣操作的話,會出現死鎖,這里我們可以使用Lua腳本去處理,
Lua腳本的特點:
1.使用方便,redis內置了對Lua腳本的支持,
2.Lua可以在redis服務端原子性的執行多個redis命令
3.由于網路的原因會影響到redis的性能,因此,使用Lua可以讓多個命令同時執行,降低了網路給redis帶來的性能問題,
在redis中如何使用Lua腳本:
1.在redis服務端寫好,然后在java業務中呼叫腳本
2.可以直接在java中直接去寫,寫好后,需要執行時,每次將腳本發送到redis中去執行,
創建release.lua腳本:
//用redis.call呼叫一個redis命令,調的是get命令,這個key是從外面傳進來的key
if redis.call("get",KEYS[1])==ARGV[1] then//如果相等就去操作釋放命令
return redis.call("del",KEYS[1])
else
return 0
end
腳本建議創在/usr/local/redis/lua,也就是redis目錄的下一級,創建一個lua的檔案夾,里面存放lua腳本
可以給Lua腳本求一個SHA1和:
cat lua/release.lua | redis-cli -a root script load --pipe
b8059ba43af6ffe8bed3db65bac35d452f8115d8
在測驗類里面寫
public void luaTest() {
Redis redis = new Redis();
for (int i = 0; i < 2; i++) {
redis.execute(jedis -> {
// 1.先獲取一個隨機字串
String value = UUID.randomUUID().toString();
String set = jedis.set("k1", value , new SetParams().nx().ex(5));
System.out.println("luaTest-->" + set);
if (set != null && "OK".equals(set)) {
// 4. 具體的業務操作
jedis.set("site", "www.javaboy.org");
String site = jedis.get("site");
System.out.println(site);
// 5.釋放鎖
jedis.evalsha("b8059ba43af6ffe8bed3db65bac35d452f8115d8", Arrays.asList("k1"),
Arrays.asList(value));
} else {
System.out.println("沒拿到鎖!!!");
}
});
}
}
資料來自江南一點雨,本人已付費購買!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/67905.html
標籤:其他
上一篇:深入淺出JUC并發編程
