基于springboot實(shí)現(xiàn)redis分布式鎖的方法
在公司的項(xiàng)目中用到了分布式鎖,但只會(huì)用卻不明白其中的規(guī)則
所以寫一篇文章來(lái)記錄
使用場(chǎng)景:交易服務(wù),使用redis分布式鎖,防止重復(fù)提交訂單,出現(xiàn)超賣問(wèn)題
分布式鎖的實(shí)現(xiàn)方式
- 基于數(shù)據(jù)庫(kù)樂(lè)觀鎖/悲觀鎖
- Redis分布式鎖(本文)
- Zookeeper分布式鎖
redis是如何實(shí)現(xiàn)加鎖的?
在redis中,有一條命令,實(shí)現(xiàn)鎖
SETNX key value
該命令的作用是將 key 的值設(shè)為 value ,當(dāng)且僅當(dāng) key 不存在。若給定的 key 已經(jīng)存在,則 SETNX 不做任何動(dòng)作。設(shè)置成功,返回 1 ;設(shè)置失敗,返回 0
使用 redis 來(lái)實(shí)現(xiàn)鎖的邏輯就是這樣的
線程 1 獲取鎖 -- > setnx lockKey lockvalue
-- > 1 獲取鎖成功
線程 2 獲取鎖 -- > setnx lockKey lockvalue
-- > 0 獲取鎖失敗 (繼續(xù)等待,或者其他邏輯)
線程 1 釋放鎖 -- >
線程 2 獲取鎖 -- > setnx lockKey lockvalue
-- > 1 獲取成功
接下來(lái)我們將基于springboot實(shí)現(xiàn)redis分布式鎖
1. 引入redis、springmvc、lombok依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.miao.redis</groupId>
<artifactId>springboot-caffeine-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-redis-lock-demo</name>
<description>Demo project for Redis Distribute Lock</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>
<!--springMvc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2. 新建RedisDistributedLock.java并書寫加鎖解鎖邏輯
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import java.nio.charset.StandardCharsets;
/**
* @author miao
* redis 加鎖工具類
*/
@Slf4j
public class RedisDistributedLock {
/**
* 超時(shí)時(shí)間
*/
private static final long TIMEOUT_MILLIS = 15000;
/**
* 重試次數(shù)
*/
private static final int RETRY_TIMES = 10;
/***
* 睡眠時(shí)間
*/
private static final long SLEEP_MILLIS = 500;
/**
* 用來(lái)加鎖的lua腳本
* 因?yàn)樾掳娴膔edis加鎖操作已經(jīng)為原子性操作
* 所以放棄使用lua腳本
*/
private static final String LOCK_LUA =
"if redis.call(\"setnx\",KEYS[1],ARGV[1]) == 1 " +
"then " +
" return redis.call('expire',KEYS[1],ARGV[2]) " +
"else " +
" return 0 " +
"end";
/**
* 用來(lái)釋放分布式鎖的lua腳本
* 如果redis.get(KEYS[1]) == ARGV[1],則redis delete KEYS[1]
* 否則返回0
* KEYS[1] , ARGV[1] 是參數(shù),我們只調(diào)用的時(shí)候 傳遞這兩個(gè)參數(shù)就可以了
* KEYS[1] 主要用來(lái)傳遞在redis 中用作key值的參數(shù)
* ARGV[1] 主要用來(lái)傳遞在redis中用做 value值的參數(shù)
*/
private static final String UNLOCK_LUA =
"if redis.call(\"get\",KEYS[1]) == ARGV[1] "
+ "then "
+ " return redis.call(\"del\",KEYS[1]) "
+ "else "
+ " return 0 "
+ "end ";
/**
* 檢查 redisKey 是否上鎖
*
* @param redisKey redisKey
* @param template template
* @return Boolean
*/
public static Boolean isLock(String redisKey, String value, RedisTemplate<Object, Object> template) {
return lock(redisKey, value, template, RETRY_TIMES);
}
private static Boolean lock(String redisKey,
String value,
RedisTemplate<Object, Object> template,
int retryTimes) {
boolean result = lockKey(redisKey, value, template);
while (!(result) && retryTimes-- > 0) {
try {
log.debug("lock failed, retrying...{}", retryTimes);
Thread.sleep(RedisDistributedLock.SLEEP_MILLIS);
} catch (InterruptedException e) {
return false;
}
result = lockKey(redisKey, value, template);
}
return result;
}
private static Boolean lockKey(final String key,
final String value,
RedisTemplate<Object, Object> template) {
try {
RedisCallback<Boolean> callback = (connection) -> connection.set(
key.getBytes(StandardCharsets.UTF_8),
value.getBytes(StandardCharsets.UTF_8),
Expiration.milliseconds(RedisDistributedLock.TIMEOUT_MILLIS),
RedisStringCommands.SetOption.SET_IF_ABSENT
);
return template.execute(callback);
} catch (Exception e) {
log.info("lock key fail because of ", e);
}
return false;
}
/**
* 釋放分布式鎖資源
*
* @param redisKey key
* @param value value
* @param template redis
* @return Boolean
*/
public static Boolean releaseLock(String redisKey,
String value,
RedisTemplate<Object, Object> template) {
try {
RedisCallback<Boolean> callback = (connection) -> connection.eval(
UNLOCK_LUA.getBytes(),
ReturnType.BOOLEAN,
1,
redisKey.getBytes(StandardCharsets.UTF_8),
value.getBytes(StandardCharsets.UTF_8)
);
return template.execute(callback);
} catch (Exception e) {
log.info("release lock fail because of ", e);
}
return false;
}
}
補(bǔ)充:
1. spring-data-redis 有StringRedisTempla和RedisTemplate兩種,但是我選擇了RedisTemplate,因?yàn)樗容^萬(wàn)能。他們的區(qū)別是:當(dāng)你的redis數(shù)據(jù)庫(kù)里面本來(lái)存的是字符串?dāng)?shù)據(jù)或者你要存取的數(shù)據(jù)就是字符串類型數(shù)據(jù)的時(shí)候,那么你就使用StringRedisTemplate即可, 但是如果你的數(shù)據(jù)是復(fù)雜的對(duì)象類型,而取出的時(shí)候又不想做任何的數(shù)據(jù)轉(zhuǎn)換,直接從Redis里面取出一個(gè)對(duì)象,那么使用RedisTemplate是 更好的選擇。
2. 選擇lua腳本是因?yàn)?,腳本運(yùn)行是原子性的,在腳本運(yùn)行期間沒(méi)有客戶端可以操作,所以在釋放鎖的時(shí)候用了lua腳本,
而redis最新版加鎖時(shí)保證了Redis值和自動(dòng)過(guò)期時(shí)間的原子性,所用沒(méi)用lua腳本
3. 創(chuàng)建測(cè)試類 TestController
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @author miao
*/
@RestController
@Slf4j
public class TestController {
@Resource
private RedisTemplate<Object, Object> redisTemplate;
@PostMapping("/order")
public String createOrder() throws InterruptedException {
log.info("開(kāi)始創(chuàng)建訂單");
Boolean isLock = RedisDistributedLock.isLock("testLock", "456789", redisTemplate);
if (!isLock) {
log.info("鎖已經(jīng)被占用");
return "fail";
} else {
//.....處理邏輯
}
Thread.sleep(10000);
//一定要記得釋放鎖,否則會(huì)出現(xiàn)問(wèn)題
RedisDistributedLock.releaseLock("testLock", "456789", redisTemplate);
return "success";
}
}
4. 使用postman進(jìn)行測(cè)試



5. redis分布式鎖的缺點(diǎn)
上面我們說(shuō)的是redis,是單點(diǎn)的情況。如果是在redis sentinel集群中情況就有所不同了。在redis sentinel集群中,我們具有多臺(tái)redis,他們之間有著主從的關(guān)系,例如一主二從。我們的set命令對(duì)應(yīng)的數(shù)據(jù)寫到主庫(kù),然后同步到從庫(kù)。當(dāng)我們申請(qǐng)一個(gè)鎖的時(shí)候,對(duì)應(yīng)就是一條命令 setnx mykey myvalue ,在redis sentinel集群中,這條命令先是落到了主庫(kù)。假設(shè)這時(shí)主庫(kù)down了,而這條數(shù)據(jù)還沒(méi)來(lái)得及同步到從庫(kù),sentinel將從庫(kù)中的一臺(tái)選舉為主庫(kù)了。這時(shí),我們的新主庫(kù)中并沒(méi)有mykey這條數(shù)據(jù),若此時(shí)另外一個(gè)client執(zhí)行 setnx mykey hisvalue , 也會(huì)成功,即也能得到鎖。這就意味著,此時(shí)有兩個(gè)client獲得了鎖。這不是我們希望看到的,雖然這個(gè)情況發(fā)生的記錄很小,只會(huì)在主從failover的時(shí)候才會(huì)發(fā)生,大多數(shù)情況下、大多數(shù)系統(tǒng)都可以容忍,但是不是所有的系統(tǒng)都能容忍這種瑕疵。
6.redis分布式鎖的優(yōu)化
為了解決故障轉(zhuǎn)移情況下的缺陷,Antirez 發(fā)明了 Redlock 算法,使用redlock算法,需要多個(gè)redis實(shí)例,加鎖的時(shí)候,它會(huì)想多半節(jié)點(diǎn)發(fā)送 setex mykey myvalue 命令,只要過(guò)半節(jié)點(diǎn)成功了,那么就算加鎖成功了。釋放鎖的時(shí)候需要想所有節(jié)點(diǎn)發(fā)送del命令。這是一種基于【大多數(shù)都同意】的一種機(jī)制。感興趣的可以查詢相關(guān)資料。在實(shí)際工作中使用的時(shí)候,我們可以選擇已有的開(kāi)源實(shí)現(xiàn),python有redlock-py,java 中有Redisson redlock。
redlock確實(shí)解決了上面所說(shuō)的“不靠譜的情況”。但是,它解決問(wèn)題的同時(shí),也帶來(lái)了代價(jià)。你需要多個(gè)redis實(shí)例,你需要引入新的庫(kù) 代碼也得調(diào)整,性能上也會(huì)有損。所以,果然是不存在“完美的解決方案”,我們更需要的是能夠根據(jù)實(shí)際的情況和條件把問(wèn)題解決了就好。
我大致講清楚了redis分布式鎖方面的問(wèn)題(日后如果有新的領(lǐng)悟就繼續(xù)更新)。更多相關(guān)springboot redis分布式鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 關(guān)于SpringBoot 使用 Redis 分布式鎖解決并發(fā)問(wèn)題
- SpringBoot整合Redisson實(shí)現(xiàn)分布式鎖
- Springboot整合Redis實(shí)現(xiàn)超賣問(wèn)題還原和流程分析(分布式鎖)
- springboot 集成redission 以及分布式鎖的使用詳解
- SpringBoot之使用Redis實(shí)現(xiàn)分布式鎖(秒殺系統(tǒng))
- Redis分布式鎖升級(jí)版RedLock及SpringBoot實(shí)現(xiàn)方法
- SpringBoot集成redis實(shí)現(xiàn)分布式鎖的示例代碼
- SpringBoot3+Redis實(shí)現(xiàn)分布式鎖的配置方法
相關(guān)文章
解決程序包org.springframework.test.context不存在
這篇文章主要介紹了解決程序包org.springframework.test.context不存在的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09
Java Maven settings.xml中私有倉(cāng)庫(kù)配置詳解
這篇文章主要介紹了詳解Maven settings.xml配置,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2021-10-10
Java Swing實(shí)現(xiàn)坦克大戰(zhàn)游戲
這篇文章主要介紹了Java Swing實(shí)現(xiàn)坦克大戰(zhàn)游戲,文中有非常詳細(xì)的代碼示例,對(duì)正在學(xué)習(xí)java的小伙伴們有很大的幫助喲,需要的朋友可以參考下2021-05-05
詳解servlet的url-pattern匹配規(guī)則
本篇文章主要介紹了=servlet的url-pattern匹配規(guī)則,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-12-12
反射機(jī)制:getDeclaredField和getField的區(qū)別說(shuō)明
這篇文章主要介紹了反射機(jī)制:getDeclaredField和getField的區(qū)別說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06
java 中用split分割字符串,最后的空格等不被拆分的方法
下面小編就為大家?guī)?lái)一篇java 中用split分割字符串,最后的空格等不被拆分的方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-02-02

