SpringBoot使用Redis實現分散式鎖
前言
在單機應用時代,我們對一個共享的物件進行多執行緒訪問的時候,使用java的synchronized關鍵字或者ReentrantLock類對操作的物件加鎖就可以解決物件的執行緒安全問題。
分散式應用時代這個方法卻行不通了,我們的應用可能被部署到多臺機器上,執行在不同的JVM裡,一個物件可能同時存在多臺機器的記憶體中,怎樣使共享物件同時只被一個執行緒處理就成了一個問題。
在分散式系統中為了保證一個物件在高併發的情況下只能被一個執行緒使用,我們需要一種跨JVM的互斥機制來控制共享資源的訪問,此時就需要用到我們的分散式鎖了。
分散式鎖一般有三種實現方式:1.通過資料庫實現分散式鎖;2.通過快取(Redis等)實現分散式鎖;3.通過Zookeeper實現分散式鎖。本篇文章主要介紹第二種通過Redis實現分散式鎖的方式。
分散式鎖的需要具備的條件
為了保證分散式鎖的可用性,需要具備一下五點條件:
1、在同一時間保證只有一臺機器的一個執行緒可以持有鎖。
2、不能發生死鎖,無論何時持有鎖的機器崩潰掛掉了都要能自動釋放鎖。
3、高效的獲取和釋放鎖。
4、具備非阻塞性,一旦獲取不到鎖就立刻返回加鎖失敗。
5、獨佔性,即自己加的鎖只有自己才能釋放。
程式碼實現
元件依賴
首先在pom.xml檔案中新增依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
加鎖程式碼
程式碼如下:
/** * 獲取鎖 * @param lockKey 鎖 * @param identity 身份標識(保證鎖不會被其他人釋放) * @param expireTime 鎖的過期時間(單位:秒) * @return */ public boolean lock(String lockKey,String identity,long expireTime){ boolean lockResult = redisTemplate.opsForValue().setIfAbsent(lockKey,identity,expireTime,TimeUnit.SECONDS); return opsForValue; }
加鎖的方法只需要三個引數:lockKey、identity、expireTime。
- 第一個引數lockKey為key,一個資源對應一個唯一的key。
- 第二個引數identity為身份標識,作為此key對應的value儲存,為了判斷在釋放鎖時是不是和加鎖的身份相同,防止別人釋放鎖。
- 第三個引數expireTime為過期時間,此引數保證程式加鎖後崩潰導致不能主動釋放鎖的時候自動釋放鎖,防止出現死鎖。
為什麼使用setIfAbsent方法呢?這個方法的好處就是,如果redis中已經存在這個key了,就會返回失敗,並且不改變redis中的資料,這樣就不會把別的執行緒的加的鎖給覆蓋掉。
解鎖程式碼
程式碼如下:
/** * 釋放鎖 * @param lockKey 鎖 * @param identity 身份標識(保證鎖不會被其他人釋放) * @return */ public boolean releaseLock(String lockKey,String identity){ String luaScript = "if " + " redis.call('get',KEYS[1]) == ARGV[1] " + "then " + " return redis.call('del',KEYS[1]) " + "else " + " return 0 " + "end"; DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>(); redisScript.setResultType(Boolean.class); redisScript.setScriptText(luaScript); List<String> keys = new ArrayList<>(); keys.add(lockKey); boolean result = redisTemplate.execute(redisScript,keys,identity); return result; }
解鎖的方法只需兩個引數:lockKey、identity。
- 第一個引數lockKey為key,一個資源對應一個唯一的key。
- 第二個引數identity為身份標識,作為此key對應的value儲存,為了判斷在釋放鎖時是不是和加鎖的身份相同,防止別人釋放鎖。
此處使用Lua指令碼來判斷身份,身份相同就刪除,身份不同就不對資料做操作並返回失敗。為什麼要使用Lua指令碼呢?這是為了要保證操作的原子性,redis在執行Lua指令碼的時候是把腳本當作一個命令來執行的,我們都知道redis的命令是都是原子操作,這樣就保證了操作的原子性。
測試程式碼
package com.qixi.lock.demo.lockdemo.controller; import com.qixi.lock.demo.lockdemo.util.RedisLock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * 測試分散式鎖 * @author ZhengNC * @date 2020/5/13 17:27 */ @RestController @RequestMapping("test") public class TestRedisLockController { private final String lockKeyName = "testKey"; @Autowired private RedisLock redisLock; /** * 測試加鎖 * @param id 加鎖的資源id * @param identity 身份標識 * @return */ @GetMapping("lock") public String lock(@RequestParam("id") String id,@RequestParam("identity") String identity){ String lockKey = lockKeyName+":"+id; boolean lockSuccess = redisLock.lock(lockKey,60); String result = "lock failed"; if (lockSuccess){ result = "lock success"; } return result; } /** * 測試釋放鎖 * @param id 釋放鎖的資源id * @param identity 身份標識 * @return */ @GetMapping("release") public String release(@RequestParam("id") String id,@RequestParam("identity") String identity){ String lockKey = lockKeyName+":"+id; boolean releaseSuccess = redisLock.releaseLock(lockKey,identity); String result = "release failed"; if (releaseSuccess){ result = "release success"; } return result; } }
package com.qixi.lock.demo.lockdemo.util; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; /** * 分散式鎖Redis工具類 * @author ZhengNC * @date 2020/5/13 17:27 */ @Component public class RedisLock { @Autowired private RedisTemplate<String,String> redisTemplate; /** * 獲取鎖 * @param lockKey 鎖 * @param identity 身份標識(保證鎖不會被其他人釋放) * @param expireTime 鎖的過期時間(單位:秒) * @return */ public boolean lock(String lockKey,long expireTime){ boolean lockResult = redisTemplate.opsForValue().setIfAbsent(lockKey,TimeUnit.SECONDS); return lockResult; } /** * 釋放鎖 * @param lockKey 鎖 * @param identity 身份標識(保證鎖不會被其他人釋放) * @return */ public boolean releaseLock(String lockKey,String identity){ String luaScript = "if " + " redis.call('get',KEYS[1]) == ARGV[1] " + "then " + " return redis.call('del',KEYS[1]) " + "else " + " return 0 " + "end"; DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>(); redisScript.setResultType(Boolean.class); redisScript.setScriptText(luaScript); List<String> keys = new ArrayList<>(); keys.add(lockKey); boolean result = redisTemplate.execute(redisScript,identity); return result; } }
結語
感謝大家閱讀我的文章,更歡迎大家指出我的問題,希望能在這裡通過討論取得共同的進步。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。