概述
今天我們來講一下如何使用實現分佈式鎖
要了解如何實現分佈式鎖。我們首先要了解什麼是redis分佈式鎖
什麼是redis分佈式鎖?
redis分佈式鎖首先是一個分佈式鎖。而分佈式鎖又是什麼呢?
簡單來說分佈式鎖是在分佈式的環境下,一個方法同一時間只能被一臺機器的一個線程所使用。
舉一個通俗但是有點不太優雅的例子就是:廁所的隔間一次只能有一個人使用。如果太多人來的話。後面的人只有隔一段時間再來。或者離開。
而redis分佈式鎖就是使用redis的技術實現分佈式鎖。
下面我們先來看下redis實現分佈式鎖的遠離。
redis實現分佈式鎖原理
爲了方便講述,我們就假設我們現在需要對給商品A庫存變更進行加鎖。防止併發導致庫存更新錯誤。
並假設商品A
加鎖:把商品A的ID或者是SKU(後面統稱key)這類的唯一數據作爲key存入redis中,並且賦予一個值(一般是唯一的,比如時間戳,後面統稱value)
拿鎖:把key和value進行保存
解鎖:當一個已經拿到鎖的請求並且執行完修改庫存的操作之後,那麼這個請求需要使用key獲取鎖,並檢查鎖的value是不是和自己拿到鎖的value一致如果一致則刪除鎖
知道了原理之後我們就可以看下redis中有那些方法是可以做到這些操作的了。
按照上面原理我們首先很容易可以想到使用先使用get進行檢查值是否存在,然後使用set進行加鎖操作,然後使用del進行解鎖操作。
能這樣想說明年輕人你是有前途的。
但是我們可以想一下,我們使用redis鎖是爲了什麼?是爲了讓一個方法在同一時間只能被一個線程的所訪問 按照我們上面的做法get方法檢查和set 方法設置值是兩個操作。並不是一起執行的,那麼中間就還是有可能出先兩個線程同時都獲取到鎖的現象。所以這種做法還是有點問題的。那麼怎麼樣纔是正確的呢?
我們翻開redis的官方文檔。(因爲本人英語實在捉急,所以使用的是國內翻譯過的文檔,這裏奉上網站redis中國,在這裏可以看到很多的redis命令。小夥伴們可以多來看看)
這裏set方法新加入了兩個命令,分別是px 和 nx 這兩個是什麼意思呢。我們來看一下。
nx:只有key不存在的時候纔會設置key的值
px:設置key的過期時間,單位爲毫秒
那麼我們知道這兩個命令之後,我們就可以組合出這樣一個命令
SET SHOP-A-KEY SHOP-A-VALUE NX
這個命令的意思是:當key爲‘SHOP-A-KEY’沒有值的時候則設置‘SHOP-A-VALUE’爲值,並且過期時間爲50000毫秒,如果‘SHOP-A-KEY’已經有值了則設置失敗。
你們看這樣子是不是我們就使用一個命令完成了加鎖的操作。完美解決了上面的問題
我們來實際看下效果
結果也是符合我們的預期的。
但是這樣加鎖就完美了嗎?
我們再來想一下,假如一個線程獲得了鎖,然後在下面的程序執行的過程中出現了異常,沒有走到解鎖的環節,那會出現什麼情況呢?
答案是後面的所有線程都會取不到鎖。這也就出現了我們說的死鎖了。
那麼要怎麼解決呢?還記的我們上文的兩個命令嗎?其中NX我們已經用到了,那麼接下來自然就可以使用PX進行解決了。
我們從上面可以知道px是爲給值一個時間,時間到了之後自動刪除。那麼我們就可以在加鎖的時候預估一下我們現在執行的程序最長需要多長時間給他設置一個時間。這樣即使程序出錯了。到了時間之後,後面的線程還是可以正常的獲取值。這樣就不會出現死鎖了。
經過上面的操作我們的加鎖是基本上已經可以了。接下來我們來看下解鎖。
有的人會說了。解鎖不就是刪除值不就行了。還需要什麼特殊操作嗎?
答案是一般是不需要的。
我們設想一種情況。
1、假設有線程a和線程b同時進入一個方法a
2、線程a首先取得了鎖,鎖的時間是2s,並且進入方法a。
3、此時線程a執行方法a的時候遇到了一些問題執行的時間變長了,但是呢並沒有報異常。還是繼續執行。並且時間已經超過了2s線程a的鎖已經失效了。
4、然後這是線程b這時也獲取了鎖並且進入了方法a。
5、這時線程a終於執行完了,然後線程a就把鎖取消了,但是線程a此時並不知道,他取消的不是自己的鎖,而是線程b的鎖。這時線程b還在執行,但是鎖已經沒有了。就出現了問題。
爲了防止上面的情況我們就需要在刪除的時候先檢查一下這個鎖是不是本線程的。是再刪,不是則不管。對吧。我們不能亂拿別人的東西。
比如:我們可以使用我們加鎖的時候可以設置一個唯一的值,比如時間戳。之後解鎖的時候先比對一下緩存終的值是不是我們設置的值。
那要怎麼做呢?
這裏有的人要說了。我們先get出來值然後判斷一下,然後再刪掉不就行了嗎?
答案是不完全對。還記的我們上面說的要保證原子性嗎?
這裏我們可以採用執行LUA腳本的方式。
至於LUA腳本是什麼。這裏就不詳細講了~~(其實我也不知道)~~
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
這裏其實就是使用一個語句將檢查和刪除全部解決了。這樣就保證了操作的原子性。這裏我們再後面的抄作業環節中再看效果。。
代碼展示
jedis
jedis是redis官方java使用的鏈接工具。內部集成了很多命令的方法。我們先看下使用jedis如何實現分佈式鎖。
jedis連接池單例
/**
* @Author: buding
* @DateTime: 2020/3/6 3:43 下午
*/
public class JedisUtils {
// 地址
public final static String host = "127.0.0.1";
// 端口
public final static Integer port = 6379;
// 密碼
public final static String auth = "xx";
private Jedis instance = null;
private static JedisPool jedisPool = null;
private JedisUtils() {
}
public static Jedis getInstance() {
if (jedisPool == null) {
JedisPoolConfig config = new JedisPoolConfig();
// 最大空閒數
config.setMaxIdle(10);
// 總數
config.setMaxTotal(20);
// 等待時間
config.setMaxWaitMillis(50000);
jedisPool = new JedisPool(config, host, port, 50000, auth);
}
return jedisPool.getResource();
}
}
加鎖
public static Boolean lock(String key, String value, Long expiredTime) {
Jedis jedis = JedisUtils.getInstance();
String result = jedis.set(key, value, new SetParams().nx().px(expiredTime));
jedis.close();
return null != result ? true : false;
}
解鎖
public static Boolean unLock(String key, String value) {
Jedis jedis = JedisUtils.getInstance();
// lua語句,保證原子性
String lua = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
"then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
List<String> keyList = new ArrayList<String>();
keyList.add(key);
List<String> argvList = new ArrayList<String>();
argvList.add(value);
Object result = jedis.eval(lua, keyList, argvList);
return result.equals(1) ? true : false;
}
使用測試
public static void main(String[] args) {
final String key = "shop-a-key";
final String value = "shop-a-value";
Long expiredTime = 30000L;
System.out.println("========加鎖========");
Boolean result = RedisUtils.lock(key, value, expiredTime);
System.out.println("========加鎖完畢========");
new Thread(new Runnable() {
public void run() {
try {
System.out.println("========執行業務========");
// 模擬業務執行
Thread.sleep(5000);
System.out.println("========業務執行完畢========");
System.out.println("========開始解鎖========");
if (RedisUtils.unLock(key, value)) {
System.out.println("========解鎖完畢========");
} else {
System.out.println("========解鎖失敗========");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
結果
使用redisTemplate實現分佈式鎖
redisTemplate是spring大家族的一部分,提供了在srping應用中通過簡單的配置訪問redis服務,對reids底層開發包(Jedis, JRedis, and RJC)進行了高度封裝,RedisTemplate提供了redis各種操作、異常處理及序列化,支持發佈訂閱
加鎖
@Override
public Boolean lock(String key, String value) {
// 判斷是不是可以加鎖成功
if (redisTemplate.opsForValue().setIfAbsent(key, value, 300L, TimeUnit.SECONDS)) {
return true;
} else {
return false;
}
}
解鎖
@Override
public void unlock(String key, String value) {
String lua = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
"then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(lua,Long.class);
redisScript.setScriptText(lua);
redisTemplate.execute(redisScript, Collections.singletonList(key), value);
}
```
**測試**
```java
@GetMapping("redis")
public Response redisTest() {
final String key = "shop-a-key";
final String value = "shop-a-value";
System.out.println("========加鎖========");
if (redisService.lock(key, value)) {
System.out.println("========加鎖完畢========");
} else {
System.out.println("========加鎖失敗========");
}
new Thread(new Runnable() {
public void run() {
try {
System.out.println("========執行業務========");
// 模擬業務執行
Thread.sleep(5000);
System.out.println("========業務執行完畢========");
System.out.println("========開始解鎖========");
redisService.unlock(key, value);
System.out.println("========解鎖完畢========");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
return Response.success();
}
結果
好了,本文給大家講了一下redis的分佈式鎖的大概原理,和使用jedis和redisTemplete實現redis分佈式鎖的方法。如果有什麼問題,希望大家能指出。謝謝🙏