溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

Java基于redis實(shí)現(xiàn)分布式鎖的方法

發(fā)布時(shí)間:2020-08-04 14:51:27 來(lái)源:億速云 閱讀:295 作者:小豬 欄目:編程語(yǔ)言

這篇文章主要講解了Java基于redis實(shí)現(xiàn)分布式鎖的方法,內(nèi)容清晰明了,對(duì)此有興趣的小伙伴可以學(xué)習(xí)一下,相信大家閱讀完之后會(huì)有幫助。

為什么會(huì)有這個(gè)需求:

例如一個(gè)簡(jiǎn)單用戶(hù)的操作,一個(gè)線(xiàn)程去修改用戶(hù)狀態(tài),首先在在內(nèi)存中讀出用戶(hù)的狀態(tài),然后在內(nèi)存中進(jìn)行修改,然后在存到數(shù)據(jù)庫(kù)中。在單線(xiàn)程中,這是沒(méi)有問(wèn)題的。但是在多線(xiàn)程中由于讀取,修改,寫(xiě)入是三個(gè)操作,不是原子操作(同時(shí)成功或失?。?,因此在多線(xiàn)程中會(huì)存在數(shù)據(jù)的安全性問(wèn)題。

這個(gè)問(wèn)題的話(huà),就可以用分布式鎖在限制程序的并發(fā)執(zhí)行。

實(shí)現(xiàn)思路:

就是進(jìn)來(lái)一個(gè)先占位,當(dāng)別的線(xiàn)程進(jìn)來(lái)操作的時(shí)候,發(fā)現(xiàn)有人占位了,就會(huì)放棄或者稍后再試。

占位的實(shí)現(xiàn):

在redis中的setnx命令來(lái)實(shí)現(xiàn),redis命令可以參考我這篇博客https://www.cnblogs.com/javazl/p/12657280.html,默認(rèn)set命令就是存值,當(dāng)key存在的時(shí)候,set就會(huì)覆蓋key的value值,而setnx則不會(huì)。當(dāng)沒(méi)有key的時(shí)候,setnx就會(huì)進(jìn)來(lái)先占位,當(dāng)key存在了,其他的setnx就進(jìn)不來(lái)了。。等到第一個(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類(lèi)型
      if (setnx == 1) {
        //沒(méi)人占位
        jedis.set("name", "zl");
        String name = jedis.get("name");
        System.out.println(name);
        //釋放資源
         jedis.del("k1");
      }else{
        //有人占位,停止/暫緩 操作
      }
    });
  }
}

上邊代碼中,就是一個(gè)簡(jiǎn)易的分布式鎖的實(shí)現(xiàn),但是有一個(gè)問(wèn)題。就是如果在占位后釋放前掛了。那么這個(gè)線(xiàn)程會(huì)一直釋放不了,也就是del命令沒(méi)有調(diào)用,后面的全部請(qǐng)求都阻塞到這里,鎖就變成了死鎖。因此這里需要去優(yōu)化。

優(yōu)化的方法就是加過(guò)期時(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è)過(guò)期時(shí)間,防止應(yīng)用在運(yùn)行過(guò)程中拋出異常導(dǎo)致鎖無(wú)法及時(shí)得到釋放
        jedis.expire("k1", 5);
        //沒(méi)人占位
        jedis.set("name", "zl");
        String name = jedis.get("name");
        System.out.println(name);
        jedis.del("k1");
      }else{
        //有人占位,停止/暫緩 操作
      }
    });
  }

這樣處理后,就可以保證鎖可以正常的釋放。但是會(huì)有一個(gè)新的問(wèn)題,就是如果在取鎖和設(shè)置過(guò)期時(shí)間服務(wù)器掛掉了,因?yàn)槿℃i,也就是setnx和設(shè)置過(guò)期時(shí)間是兩個(gè)操作,不具備原子性所以不可能同時(shí)完成。這個(gè)鎖就會(huì)被一直占用,無(wú)法得到釋放,成為死鎖。那么如何解決呢?

在redis2.8之后,setnx和expireke可以通過(guò)一個(gè)命令一起執(zhí)行,讓兩個(gè)操作變成一個(gè),就會(huì)解決這個(gè)問(wèn)題。

優(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è)過(guò)期時(shí)間,防止應(yīng)用在運(yùn)行過(guò)程中拋出異常導(dǎo)致鎖無(wú)法及時(shí)得到釋放
        jedis.expire("k1", 5);
        //沒(méi)人占位
        jedis.set("name", "zl);
        String name = jedis.get("name");
        System.out.println(name);
      //釋放資源
        jedis.del("k1");
      }else{
        //有人占位,停止/暫緩 操作
      }
    });
  }
}

用過(guò)期時(shí)間優(yōu)化后,雖然解決了死鎖的問(wèn)題,但是又有一個(gè)新的問(wèn)題產(chǎn)生,就是超時(shí)問(wèn)題:

舉個(gè)例子:如果要執(zhí)行的業(yè)務(wù)很耗時(shí),可能會(huì)出現(xiàn)紊亂,當(dāng)?shù)匾粋€(gè)線(xiàn)程獲取到鎖的時(shí)候,開(kāi)始執(zhí)行業(yè)務(wù)代碼,但是業(yè)務(wù)代碼很耗時(shí),假如過(guò)期時(shí)間是3秒,而業(yè)務(wù)執(zhí)行需要5秒,這樣,鎖就會(huì)提前釋放,然后第二個(gè)線(xiàn)程獲取到鎖并開(kāi)始執(zhí)行。當(dāng)執(zhí)行到第2秒的時(shí)候,第一個(gè)鎖也執(zhí)行完了,此時(shí)第一個(gè)線(xiàn)程會(huì)釋放第二個(gè)線(xiàn)程的鎖,然后第三個(gè)線(xiàn)程繼續(xù)獲取鎖并執(zhí)行,當(dāng)?shù)降?秒的時(shí)候第二個(gè)線(xiàn)程執(zhí)行完了,那么又會(huì)提前釋放鎖,一直如此循環(huán),會(huì)造成線(xiàn)程的紊亂。

那么解決的思路主要有兩種

盡量避免耗時(shí)操作。

去處理鎖,給鎖的value設(shè)置隨機(jī)數(shù)或隨機(jī)字符串,每當(dāng)要釋放的時(shí)候去判斷這個(gè)value的值,如果是的話(huà)就去釋放,如果不是就不釋放,舉個(gè)例子,假設(shè)第一個(gè)線(xiàn)程進(jìn)來(lái),它獲取鎖的value是1,如果發(fā)生超時(shí)就會(huì)進(jìn)入下一個(gè)線(xiàn)程,下一個(gè)線(xiàn)程會(huì)獲取新的value為

3,在釋放第二個(gè)所之前先去獲取value并比較,發(fā)現(xiàn)1不等于三,那么就不去釋放鎖。
第一種的話(huà)沒(méi)啥說(shuō)的,但是第二種的話(huà)會(huì)有一個(gè)問(wèn)題,就是釋放鎖會(huì)查看value,然后比較,然后釋放,會(huì)有三個(gè)操作,那么就不具備原子性,這樣操作的話(huà),會(huì)出現(xiàn)死鎖。這里我們可以使用Lua腳本去處理。

Lua腳本的特點(diǎn):

1.使用方便,redis內(nèi)置了對(duì)Lua腳本的支持。

2.Lua可以在redis服務(wù)端原子性的執(zhí)行多個(gè)redis命令

3.由于網(wǎng)絡(luò)的原因會(huì)影響到redis的性能,因此,使用Lua可以讓多個(gè)命令同時(shí)執(zhí)行,降低了網(wǎng)絡(luò)給redis帶來(lái)的性能問(wèn)題。

在redis中如何使用Lua腳本:

1.在redis服務(wù)端寫(xiě)好,然后在java業(yè)務(wù)中調(diào)用腳本

2.可以直接在java中直接去寫(xiě),寫(xiě)好后,需要執(zhí)行時(shí),每次將腳本發(fā)送到redis中去執(zhí)行。

創(chuàng)建Lua腳本:

//用redis.call調(diào)用一個(gè)redis命令,調(diào)的是get命令,這個(gè)key是從外面?zhèn)鬟M(jìn)來(lái)的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è)命令會(huì)在Redis中緩存Lua腳本,并返回腳本內(nèi)容的SHA1校驗(yàn)和,然后在java中調(diào)用時(shí),傳入SHA1校驗(yàn)和作為參數(shù),這樣redis服務(wù)端就知道執(zhí)行那個(gè)腳本了。

接下來(lái)在java中編寫(xiě)

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("沒(méi)拿到鎖");
        }
      });
    }
  }
}

這樣處理的話(huà),就解決了死鎖的問(wèn)題。

看完上述內(nèi)容,是不是對(duì)Java基于redis實(shí)現(xiàn)分布式鎖的方法有進(jìn)一步的了解,如果還想學(xué)習(xí)更多內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI