Redis分布式鎖解決超賣問題
一、使用redisTemplate中的setIfAbsent方法。
// setIfAbsent就是對應(yīng)redis的setnx
Boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent(lockKey, lockKey, 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(setIfAbsent)) {
LOG.info("恭喜,搶到鎖了!lockKey:{}", lockKey);
} else {
LOG.info("很遺憾,沒搶到鎖!lockKey:{}", lockKey);
}缺點:
- 性能不高,可能會導(dǎo)致少買。
- 假如業(yè)務(wù)執(zhí)行時間長,設(shè)置的鎖的過期時間短的話,可能出現(xiàn)超賣問題。
二、使用Redisson解決(看門狗方式)
2.1、實現(xiàn)原理
redisson在獲取鎖之后,會開啟一個守護線程(看門狗線程),當(dāng)鎖即將過期還沒有釋放時,不斷的延長鎖key的生存時間
2.2、SpringBoot集成Redisson
2.2.1、添加pom.xml依賴
<!--至少3.18.0版本,才支持spring boot 3-->
<!--升級到3.20.0,否則打包生產(chǎn)會報錯:Could not initialize class org.redisson.spring.data.connection.RedissonConnection-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.21.0</version>
</dependency>2.2.2、注入RedissonClient對象
@Autowired private RedissonClient redissonClient;
2.2.3、使用Redisson
RLock lock = null;
try {
// 使用redisson,自帶看門狗
lock = redissonClient.getLock(lockKey);
/**
waitTime – the maximum time to acquire the lock 等待獲取鎖時間(最大嘗試獲得鎖的時間),超時返回false
leaseTime – lease time 鎖時長,即n秒后自動釋放鎖
time unit – time unit 時間單位
*/
// boolean tryLock = lock.tryLock(30, 10, TimeUnit.SECONDS); // 不帶看門狗
boolean tryLock = lock.tryLock(0, TimeUnit.SECONDS); // 帶看門狗
if (tryLock) {
LOG.info("恭喜,搶到鎖了!");
} else {
LOG.info("很遺憾,沒搶到鎖");
}
} catch (InterruptedException e) {
LOG.error("發(fā)生異常", e);
} finally {
LOG.info("釋放鎖!");
//當(dāng)lock不等于null并且lock是當(dāng)前線程的時候去釋放鎖
if (null != lock && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}缺點:
- 看門狗啟動后,對整體性能也會有一定影響
- 當(dāng)redis宕機后,獲取不到鎖,業(yè)務(wù)中斷。
- 正常情況下,如果加鎖成功了,那么master節(jié)點會異步復(fù)制給對應(yīng)的slave節(jié)點。但是如果在這個過程中發(fā)生master節(jié)點宕機,主備切換,slave節(jié)點從變?yōu)榱?master節(jié)點,而鎖還沒從舊master節(jié)點同步過來,這就發(fā)生了鎖丟失,會導(dǎo)致多個客戶端可以同時持有同一把鎖的問題
三、Redis紅鎖
2.1、Redis紅鎖名稱來源
Redis 紅鎖的名稱來源于 Redis 的logo,Redis 的 logo 是一個紅色熱氣球,而紅色的熱氣球上有一把鎖的圖案,因此這種分布式鎖解決方案也被稱為"Redlock",中文翻譯為"紅鎖"。
2.2、原理
現(xiàn)在假設(shè)有5個Redis主節(jié)點(大于3的奇數(shù)個),這樣基本保證他們不會同時都宕掉,獲取鎖和釋放鎖的過程中,客戶端會執(zhí)行以下操作:
- 獲取當(dāng)前Unix時間,以毫秒為單位,并設(shè)置超時時間過期時間(過期時間要大于正常業(yè)務(wù)執(zhí)行的時間 + 獲取所有redis服務(wù)消耗時間 + 時鐘漂移)
- 依次嘗試從5個實例,使用相同的key和具有唯一性的value獲取鎖,當(dāng)向Redis請求獲取鎖時,客戶端應(yīng)該設(shè)置一個網(wǎng)絡(luò)連接和響應(yīng)超時時間,這個超時時間應(yīng)該小于鎖的失效時間TTL,這樣可以避免客戶端死等。比如:TTL為5s,設(shè)置獲取鎖最多用1s,所以如果一秒內(nèi)無法獲取鎖,就放棄獲取這個鎖,從而嘗試獲取下個鎖
- 客戶端 獲取所有能獲取的鎖后的時間 減去 第(1)步的時間,就得到鎖的獲取時間。鎖的獲取時間要小于鎖失效時間TTL,并且至少從半數(shù)以上的Redis節(jié)點取到鎖,才算獲取成功鎖
- 如果成功獲得鎖,key的真正有效時間 = TTL - 鎖的獲取時間 - 時鐘漂移。比如:TTL 是5s,獲取所有鎖用了2s,則真正鎖有效時間為3s
- 如果因為某些原因,獲取鎖失?。]有在半數(shù)以上實例取到鎖或者取鎖時間已經(jīng)超過了有效時間),客戶端應(yīng)該在所有的Redis實例上進(jìn)行解鎖,無論Redis實例是否加鎖成功,因為可能服務(wù)端響應(yīng)消息丟失了但是實際成功了。
2.3、代碼實現(xiàn)
2.3.1、注冊紅鎖的RedissonClient
@Component
public class RedisConfig {
@Bean(name = "redissonClient1")
@Primary
public RedissonClient redissonRed1(){
Config config = new Config();
config.useSingleServer().setAddress("127.0.0.1:6379").setDatabase(0);
return Redisson.create(config);
}
@Bean(name = "redissonClient2")
public RedissonClient redissonRed2(){
Config config = new Config();
config.useSingleServer().setAddress("127.0.0.1:6380").setDatabase(0);
return Redisson.create(config);
}
@Bean(name = "redissonClient3")
public RedissonClient redissonRed3(){
Config config = new Config();
config.useSingleServer().setAddress("127.0.0.1:6381").setDatabase(0);
return Redisson.create(config);
}
@Bean(name = "redissonClient4")
public RedissonClient redissonRed4(){
Config config = new Config();
config.useSingleServer().setAddress("127.0.0.1:6382").setDatabase(0);
return Redisson.create(config);
}
@Bean(name = "redissonClient5")
public RedissonClient redissonRed5(){
Config config = new Config();
config.useSingleServer().setAddress("127.0.0.1:6383").setDatabase(0);
return Redisson.create(config);
}
}
2.3.2、注入Redis紅鎖對象
// 紅鎖
@Autowired
@Qualifier("redissonClient1")
private RedissonClient redissonClient1;
@Autowired
@Qualifier("redissonClient2")
private RedissonClient redissonClient2;
@Autowired
@Qualifier("redissonClient3")
private RedissonClient redissonClient3;
@Autowired
@Qualifier("redissonClient4")
private RedissonClient redissonClient4;
@Autowired
@Qualifier("redissonClient5")
private RedissonClient redissonClient5;2.3.3、使用Redis紅鎖
/*
假設(shè)有五臺redis機器, A B C D E
線程1: A B C D E(獲取到鎖)
線程2: C D E(獲取到鎖)
線程3: C(未獲取到鎖)
*/
RLock lock1 = null;
RLock lock2 = null;
RLock lock3 = null;
RLock lock4 = null;
RLock lock5 = null;
try {
lock1 = redissonClient1.getLock(lockKey);
lock2 = redissonClient2.getLock(lockKey);
lock3 = redissonClient3.getLock(lockKey);
lock4 = redissonClient4.getLock(lockKey);
lock5 = redissonClient5.getLock(lockKey);
// 紅鎖的寫法
RedissonRedLock redissonRedLock = new RedissonRedLock(lock1, lock2, lock3, lock4, lock5);
boolean tryLock = redissonRedLock.tryLock(0, TimeUnit.SECONDS);
if (tryLock) {
LOG.info("恭喜,搶到鎖了!");
} else {
LOG.info("很遺憾,沒搶到鎖");
}
} catch (InterruptedException e) {
LOG.error("發(fā)生異常", e);
} finally {
LOG.info("釋放鎖!");
//當(dāng)lock不等于null并且lock是當(dāng)前線程的時候去釋放鎖
if (null != lock && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}注意:
- 按照順序獲取鎖、不然會出現(xiàn)每個線程都拿不到鎖的情況。
- 當(dāng)redis宕機后,切換主備或者重啟時間需大于鎖的時間,不然會有線程同時獲取到鎖(比如線程1獲取到a,b,c,獲得了鎖后c宕機重啟了,如果數(shù)據(jù)沒有備份,c中沒有key,線程2有可能獲取到了c,d,e,也獲取到了鎖)。
- 盡可能的獲取到更多redis實例的鎖。
- 獲取鎖的時間要注意,需要設(shè)置超時時間,如果超時時間內(nèi)拿不到鎖,結(jié)束線程
到此這篇關(guān)于Redis分布式鎖解決超賣問題的文章就介紹到這了,更多相關(guān)Redis分布式鎖超賣內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis和數(shù)據(jù)庫的一致性(Canal+MQ) 的實現(xiàn)
本文主要介紹了Redis和數(shù)據(jù)庫的一致性(Canal+MQ),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-06-06
Redis內(nèi)存碎片產(chǎn)生原因及Pipeline管道原理解析
這篇文章主要為大家介紹了Redis內(nèi)存碎片產(chǎn)生原因及Pipeline管道原理解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03

