Redis分布式鎖之紅鎖的實現(xiàn)
一 分布式鎖的概念
1:概念
分布式鎖(多服務(wù)共享鎖) 在分布式的部署環(huán)境下,通過鎖機制來讓多客戶端互斥的對共享資源進行訪問控制分布式系統(tǒng)不同進程共同訪問共享資源的一種鎖的實現(xiàn)。如果不同的系統(tǒng)或同一個系統(tǒng)的不同主機之間共享了某個臨界資源,往往需要互斥來防止彼此干擾,以保證一致性。
2:鎖/分布式鎖/事務(wù)區(qū)別
- 鎖 單進程的系統(tǒng)中,存在多線程同時操作一個公共變量,此時需要加鎖對變量進行同步操作,保證多線程的操作線性執(zhí)行消除并發(fā)修改。解決的是單進程中的多線程并發(fā)問題。
 - 分布式鎖 只要的應(yīng)用場景是在集群模式的多個相同服務(wù),可能會部署在不同機器上,解決進程間安全問題,防止多進程同時操作一個變量或者數(shù)據(jù)庫。解決的是多進程的并發(fā)問題 事務(wù) 解決一個會話過程中,上下文的修改對所有數(shù)據(jù)庫表的操作要么全部成功,要不全部失敗。所以應(yīng)用在service層。解決的是一個會話中的操作的數(shù)據(jù)一致性。
 - 分布式事務(wù) 解決一個聯(lián)動操作,比如一個商品的買賣分為添加商品到購物車、修改商品庫存,此時購物車服務(wù)和商品庫存服務(wù)可能部署在兩臺電腦,這時候需要保證對兩個服務(wù)的操作都全部成功或者全部回退。解決的是組合服務(wù)的數(shù)據(jù)操作的一致性問題
 
3:reddison的公平鎖
reddison公平鎖枷鎖
默認的加鎖邏輯是非公平的。在加鎖失敗時,線程會進入 while 循環(huán),一直嘗試獲得鎖,這時候是多線程進行競爭。就是說誰搶到就是誰的。Redisson 提供了公平鎖機制,使用方式如下
RLock fairLock = redisson.getFairLock("anyLock");
// 最常見的使用方法
fairLock.lock();
看門狗機制是在 RedissonBaseLock#scheduleExpirationRenewal 方法中,這塊公平鎖和非公平鎖并無區(qū)別。前文已經(jīng)了解到,公平鎖加鎖失敗之后,會將當前放到等待隊列中,通過 Java 代碼中的循環(huán)不斷嘗試獲得鎖。
reddison公平鎖釋放
公平鎖的釋放同樣分為主動釋放和超時釋放。
- 主動釋放,即自己調(diào)用釋放鎖。
 - 超時刪除,則分為兩種,一種是持鎖線程超時刪除,這種和非公平鎖沒有任何區(qū)別,因為這個鎖也是含有超時時間+看門狗續(xù)租的。另一種則是等待隊列中的超時刪除,是在每次獲取鎖之前,判斷第一個等待線程的時間戳是否超時,從而移除鎖。
 
二 、使用的案例場景
需求當在打車軟件中,乘客下了訂單。多個司機搶單,此時因為單子只有一個,多個司機對此共享資源進行搶,此處應(yīng)該使用分布式鎖;后臺服務(wù)部署在多臺服務(wù)器上;
controller層代碼
 @GetMapping("/do/{orderId}")
    public String grab(@PathVariable("orderId") int orderId, int driverId){
        System.out.println("order:"+orderId+",driverId:"+driverId);
        //此處調(diào)用鎖控制層代碼
        grabService.grabOrder(orderId,driverId);
        return "";
    }
鎖控制層代碼(使用synchronized 不成功)使用synchronized 不能保證多臺服務(wù)器只有一個搶成功;因為synchronized 只能鎖本服務(wù)的資源;多臺服務(wù)的資源是鎖不住的;
@Autowired
	OrderService orderService;
	
	@Override
	public String grabOrder(int orderId, int driverId) {
		String lock = (orderId+"");
		
		synchronized (lock.intern()) {
			try {
				System.out.println("司機:"+driverId+" 執(zhí)行搶單邏輯");
				//此處調(diào)用訂單業(yè)務(wù)代碼
	            boolean b = orderService.grab(orderId, driverId);
	            if(b) {
	            	System.out.println("司機:"+driverId+" 搶單成功");
	            }else {
	            	System.out.println("司機:"+driverId+" 搶單失敗");
	            }
	            
	        } finally {
	        	
	            
	        }
		}
		
		
		return null;
	}
調(diào)用的訂單業(yè)務(wù)代碼這一層就是寫的偽代碼,后續(xù)并不關(guān)注他

三、Redis解決方案-紅鎖
介紹
紅鎖本質(zhì)上就是使用多個Redis做鎖。例如有5個Redis,一次鎖的獲取,會對每個請求都獲取一遍,如果獲取鎖成功的數(shù)量超過一半(2.5),則獲取鎖成功,反之失敗;
釋放鎖也需要對每個Redis釋放
紅鎖原理
- 在Redis的分布式環(huán)境中,我們假設(shè)有5個Redis master。這些節(jié)點完全互相獨立,沒有主從關(guān)系
 - 線程1向這5個redis加鎖,當加到第三個的時候,4和5加不上了;但是符合紅鎖的n/2+1原則,所以線程1獲取到了鎖;
 - 當redis3掛了,此時線程1獲取到了鎖,正在順序執(zhí)行,
 - 線程2來到了redis搶占鎖,因為3掛了,1,2有鎖,只有4和5可以加鎖,因為我們注冊的時候是5臺,4和5這兩臺不滿足n/2+1原則,搶占鎖失敗;
 
當鎖遇到故障轉(zhuǎn)移
單實例肯定不是很可靠吧?加鎖成功之后,結(jié)果 Redis 服務(wù)宕機了,這不就涼涼~
這時候會提出來將 Redis 主從部署。即使是主從,也是存在巧合的!
主從結(jié)構(gòu)中存在明顯的競態(tài):
客戶端 A 從 master 獲取到鎖在 master 將鎖同步到 slave 之前,master 宕掉了。
slave 節(jié)點被晉級為 master 節(jié)點客戶端 B 取得了同一個資源被客戶端 A 已經(jīng)獲取到的另外一個鎖。安全失效!有時候程序就是這么巧,比如說正好一個節(jié)點掛掉的時候,多個客戶端同時取到了鎖。如果你可以接受這種小概率錯誤,那用這個基于復(fù)制的方案就完全沒有問題。
那我使用集群呢?如果還記得前面的內(nèi)容,應(yīng)該是知道對集群進行加鎖的時候,其實是通過 CRC16 的 hash 函數(shù)來對 key 進行取模,將結(jié)果路由到預(yù)先分配過 slot 的相應(yīng)節(jié)點上。
發(fā)現(xiàn)其實還是發(fā)到單個節(jié)點上的! 這時候 Redis 作者提出了 RedLock 的概念

總結(jié)一下就是對集群的每個節(jié)點進行加鎖,如果大多數(shù)(N/2+1)加鎖成功了,則認為獲取鎖成功。
RedLock 的問題
看著 RedLock 好像是解決問題了:
客戶端 A 鎖住了集群的大多數(shù)(一半以上);
客戶端 B 也要鎖住大多數(shù);
這里肯定會沖突,所以 客戶端 B 加鎖失敗。
那實際解決問題了么?
加鎖 key 的問題
有一個很大的疑問,我加鎖 lock1、lock2、lock3,但是 RedissonRedLock 是如何保證這三個 key 是在歸屬于 Redis 集群中不同的 master 呢?因為按照 RedLock 的理論,是需要在半數(shù)以上的 master 節(jié)點加鎖成功。閱讀完源碼之后,發(fā)現(xiàn) RedissonRedLock 完全是 RedissonMultiLock 的子類,只是重寫了 failedLocksLimit 方法,保證半數(shù)以上加鎖成功即可。所以這三個 key,是需要用戶來保證分散在不同的節(jié)點上的。
紅鎖的爭議
那我使用 5 個單節(jié)點的客戶端,然后再使用紅鎖,聽著好像是可以的,并且 RedissonRedLock 可以這樣使用。但是那和 Redis 集群還有啥關(guān)系?。∷砸廊粵]有解決我的問題,在 redis 集群下 針對master節(jié)點集群,,還是需要用戶自己來“手工定位鎖”,使鎖的節(jié)點分散到不同的master 集群節(jié)點下。 手工定位鎖,這個…… 我考慮了下,還是不用 RedLock 吧!如果master節(jié)點變動則鎖也存在問題,master集群同步等等鎖的同步和鎖失效也是需要考慮的問題;
Redisson 的開發(fā)者認為 Redis 的紅鎖也存在爭議(前文介紹的那個爭議),但是為了保證可用性,RLock 對象執(zhí)行的每個 Redis 命令執(zhí)行都通過 Redis 3.0 中引入的 WAIT 命令進行同步。
Redisson 的開發(fā)者認為 Redis 的紅鎖也存在爭議(前文介紹的那個爭議),但是為了保證可用性,RLock 對象執(zhí)行的每個
Redis 命令執(zhí)行都通過 Redis 3.0 中引入的 WAIT 命令進行同步。

源碼在這一部分。

看源碼,同時發(fā)送了一個 WAIT 1 1000 到 Redis。
結(jié)論:Redisson RedLock 是基于聯(lián)鎖 MultiLock 實現(xiàn)的,但是使用過程中需要自己判斷 key 落在哪個節(jié)點上,對使用者不是很友好。
紅鎖使用說明-官網(wǎng)介紹
基于Redis的Redisson紅鎖RedissonRedLock對象實現(xiàn)了Redlock介紹的加鎖算法。該對象也可以用來將多個RLock對象關(guān)聯(lián)為一個紅鎖,每個RLock對象實例可以來自于不同的Redisson實例。
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同時加鎖:lock1 lock2 lock3
// 紅鎖在大部分節(jié)點上加鎖成功就算成功。
lock.lock();
...
lock.unlock();
大家都知道,如果負責儲存某些分布式鎖的某些Redis節(jié)點宕機以后,而且這些鎖正好處于鎖住的狀態(tài)時,這些鎖會出現(xiàn)鎖死的狀態(tài)。為了避免這種情況的發(fā)生,Redisson內(nèi)部提供了一個監(jiān)控鎖的看門狗,它的作用是在Redisson實例被關(guān)閉前,不斷的延長鎖的有效期。默認情況下,看門狗的檢查鎖的超時時間是30秒鐘,也可以通過修改Config.lockWatchdogTimeout來另行指定。
另外Redisson還通過加鎖的方法提供了leaseTime的參數(shù)來指定加鎖的時間。超過這個時間后鎖便自動解開了。
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3); // 給lock1,lock2,lock3加鎖,如果沒有手動解開的話,10秒鐘后將會自動解開 lock.lock(10, TimeUnit.SECONDS); // 為加鎖等待100秒時間,并在加鎖成功10秒鐘后自動解開 boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS); ... lock.unlock();
紅鎖實戰(zhàn)
1.注冊紅鎖的RedissonClient
@Component
public class RedisConfig {
	@Bean(name = "redissonRed1")
    @Primary
    public RedissonClient redissonRed1(){
        Config config = new Config();
        config.useSingleServer().setAddress("127.0.0.1:6379").setDatabase(0);
        return Redisson.create(config);
    }
    @Bean(name = "redissonRed2")
    public RedissonClient redissonRed2(){
        Config config = new Config();
        config.useSingleServer().setAddress("127.0.0.1:6380").setDatabase(0);
        return Redisson.create(config);
    }
    @Bean(name = "redissonRed3")
    public RedissonClient redissonRed3(){
        Config config = new Config();
        config.useSingleServer().setAddress("127.0.0.1:6381").setDatabase(0);
        return Redisson.create(config);
    }
    @Bean(name = "redissonRed4")
    public RedissonClient redissonRed4(){
        Config config = new Config();
        config.useSingleServer().setAddress("127.0.0.1:6382").setDatabase(0);
        return Redisson.create(config);
    }
    @Bean(name = "redissonRed5")
    public RedissonClient redissonRed5(){
        Config config = new Config();
        config.useSingleServer().setAddress("127.0.0.1:6383").setDatabase(0);
        return Redisson.create(config);
    }
  }
配置方式2
基于 Redis 的 Redisson 分布式聯(lián)鎖 RedissonMultiLock 對象可以將多個 RLock 對象關(guān)聯(lián)為一個聯(lián)鎖,每個 RLock 對象實例可以來自于不同的 Redisson 實例。

按照官方文檔的說法,這里 Redisson 客戶端可以不是同一個。當然,一般工作中也不會說不用一個客戶端吧,可以看出 遍歷所有的鎖,依次加鎖。加鎖邏輯就和可重入鎖加鎖并無區(qū)別了。所以 Lua 腳本就不進行分析了
2. 紅鎖使用
package com.online.taxi.order.service.impl;
import com.online.taxi.order.constant.RedisKeyConstant;
import com.online.taxi.order.service.GrabService;
import com.online.taxi.order.service.OrderService;
import org.redisson.Redisson;
import org.redisson.RedissonRedLock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class GrabRedisRedissonRedLockLockServiceImpl implements GrabService {
    // 紅鎖
    @Autowired
    @Qualifier("redissonRed1")
    private RedissonClient redissonRed1;
    @Autowired
    @Qualifier("redissonRed2")
    private RedissonClient redissonRed2;
    @Autowired
    @Qualifier("redissonRed3")
    private RedissonClient redissonRed3;
    @Autowired
    @Qualifier("redissonRed4")
    private RedissonClient redissonRed4;
    @Autowired
    @Qualifier("redissonRed5")
    private RedissonClient redissonRed5;
    @Autowired
	OrderService orderService;
    @Override
    public String grabOrder(int orderId , int driverId){
        System.out.println("紅鎖實現(xiàn)類");
        //生成key
        String lockKey = ("" + orderId).intern();
        //redisson鎖 單節(jié)點
//        RLock rLock = redissonRed1.getLock(lockKey);
        //紅鎖 redis son
        RLock rLock1 = redissonRed1.getLock(lockKey);
        RLock rLock2 = redissonRed2.getLock(lockKey);
        RLock rLock3 = redissonRed3.getLock(lockKey);
        RLock rLock4 = redissonRed4.getLock(lockKey);
        RLock rLock5 = redissonRed5.getLock(lockKey);
        RedissonRedLock rLock = new RedissonRedLock(rLock1,rLock2,rLock3,rLock4,rLock5);
        try {
             /**紅鎖
		     * waitTimeout 嘗試獲取鎖的最大等待時間,超過這個值,則認為獲取鎖失敗
		     * leaseTime   鎖的持有時間,超過這個時間鎖會自動失效(值應(yīng)設(shè)置為大于業(yè)務(wù)處理的時間,確保在鎖有效期內(nèi)業(yè)務(wù)能處理完)
		     */
            boolean b1 = rLock.tryLock((long)waitTimeout, (long)leaseTime, TimeUnit.SECONDS);
            if (b1){
                System.out.println("加鎖成功");
                // 此代碼默認 設(shè)置key 超時時間30秒,過10秒,再延時
                System.out.println("司機:"+driverId+" 執(zhí)行搶單邏輯");
               
                boolean b = orderService.grab(orderId, driverId);
                if(b) {
                    System.out.println("司機:"+driverId+" 搶單成功");
                }else {
                    System.out.println("司機:"+driverId+" 搶單失敗");
                }
                System.out.println("加鎖成功");
            }else {
                System.out.println("加鎖失敗");
            }
        } finally {
        	rLock.unlock();
        }
        return null;
    }
}
四、單機版redission 使用
 <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.18.0</version>
        </dependency>
1.配置
    @Value("${spring.redis.host}")
    private String url;
    @Value("${spring.redis.port}")
    private Integer port;
    @Value("${spring.redis.password}")
    private String password;
    @Bean
    public RedissonClient redissonClient(){
        // 配置
        Config config = new Config();
        log.info("Redisson 初始化 {}",String.format("redis://%s:%d",url,port));
        config.useSingleServer().setAddress(String.format("redis://%s:%d",url,port) )
                .setPassword(password);
        // 創(chuàng)建RedissonClient對象
        return Redisson.create(config);
    }
2. 注入分布式鎖
此處枷鎖后在釋放鎖是解決了鎖的讀寫機制問題
@Autowired
private RedissonClient redissonClient;
 public List<? extends Object> getCachedConfigList(String ode) {
        List<StructureVo> configuration =null;
        String key = getCacheKey(orgCode, STR_CONFIG_LIST);
        List<Object> cacheConfig = redisCache.getCacheList(key);
        if (!CollectionUtils.isEmpty(cacheConfig)) {
            return cacheConfig;
        }
        RLock lock = redissonClient.getLock(getReddisonEbomCacheKey());
        try {
            lock.lock(DEFAULT_EXPIRE_SECOND,TimeUnit.SECONDS);
            List<Object> secondCacheConfig = redisCache.getCacheList(key);
            if (!CollectionUtils.isEmpty(secondCacheConfig)) {
                log.info("second lock reddison 查詢到緩存釋放鎖!");
                reddisnUnlock(getReddisonEbomCacheKey());
                return cacheConfig;
            }
            log.info("reddison枷鎖成功!存放緩存資源!");
            configuration = this.queryAllConfiguration();
            redisCache.setCacheList(key, configuration);
            redisCache.expire(key, DEFAULT_EXPIRE_HOURS, TimeUnit.HOURS);
        } catch (Exception e) {
            log.error("Happen Exception: " + e.getMessage(),e);
        }finally {
            reddisnUnlock(getReddisonEbomCacheKey());
        }
        return configuration;
    }
釋放鎖方法
    private void reddisnUnlock(String key) {
        log.info(" reddison key {}鎖釋放!",key);
        try {
            RLock unlock = redissonClient.getLock(key);
            if (unlock != null && unlock.isHeldByCurrentThread()) {
                unlock.unlock();
            }
        } catch (IllegalMonitorStateException e) {
            log.info("reddison鎖釋放 Exception: " + e.getMessage(),e);
        }
    }
Redis 單實例版本鎖 NX
單實例加鎖
SET resource_name my_random_value NX PX 30000
對于單實例 Redis 只需要使用這個命令即可。
- NX:僅在不存在 key 的時候才能被執(zhí)行成功;
 - PX:失效時間,傳入 30000,就是 30s 后自動釋放鎖;
 - my_random_value:就是隨機值,可以是線程號之類的。主要是為了更安全的釋放鎖,釋放鎖的時候使用腳本告訴 Redis: 只有 key 存在并且存儲的值和我指定的值一樣才能刪除成功。
 
簡單描述即為
- 指定一個 key 作為鎖標記,存入 Redis 中,指定一個 唯一的用戶標識作為 value。
 - 當 key 不存在時才能設(shè)置值,確保同一時間只有一個客戶端進程獲得鎖,滿足互斥性特性。
 - 設(shè)置一個過期時間,防止因系統(tǒng)異常導(dǎo)致沒能刪除這個 key,滿足防死鎖特性。
 - 當處理完業(yè)務(wù)之后需要清除這個 key 來釋放鎖,清除 key 時需要校驗 value 值,需要滿足只有加鎖的人才能釋放鎖
 
可以通過以下 Lua 腳本實現(xiàn)鎖釋放:
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end為什么要設(shè)置隨機值?
主要是為了防止鎖被其他客戶端刪除。有這么一種情況:
- 客戶端 A 獲得了鎖,還沒有執(zhí)行結(jié)束,但是鎖超時自動釋放了;
 - 客戶端 B 此時過來,是可以獲得鎖的,加鎖成功;
 - 此時,客戶端 A 執(zhí)行結(jié)束了,要去釋放鎖,如果不對比隨機值,就會把客戶端 B 的鎖給釋放了。
當然前面看過 Redisson 的處理,這個 my_random_value 存放的是 UUID:ThreadId 組合成的一個類似 931573de-903e-42fd-baa7-428ebb7eda80:1 的字符串。 
為什么要用lua腳本操作redis數(shù)據(jù)庫?
1.減少開銷–減少向redis服務(wù)器的請求次數(shù)
2.原子操作–redis將lua腳本作為一個原子執(zhí)行
3.可復(fù)用–其他客戶端可以使用已經(jīng)執(zhí)行過的lua腳本
4.增加redis靈活性–lua腳本可以幫助redis做更多的事情
到此這篇關(guān)于Redis分布式鎖之紅鎖的實現(xiàn)的文章就介紹到這了,更多相關(guān)Redis 紅鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
 詳解使用Redis SETNX 命令實現(xiàn)分布式鎖
本篇文章主要介紹了詳解使用Redis SETNX 命令實現(xiàn)分布式鎖,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-01-01
 Redis持久化方式之RDB和AOF的原理及優(yōu)缺點
在Redis中,數(shù)據(jù)可以分為兩類,即內(nèi)存數(shù)據(jù)和磁盤數(shù)據(jù),Redis?提供了兩種不同的持久化方式,其中?RDB?是快照備份機制,AOF?則是追加寫操作機制,本文將詳細給大家介紹Redis?持久化方式RDB和AOF的原理及優(yōu)缺點,感興趣的同學可以跟著小編一起來學習2023-06-06
 從原理到實踐分析?Redis?分布式鎖的多種實現(xiàn)方案
在分布式系統(tǒng)中,為了保證多個進程或線程之間的數(shù)據(jù)一致性和正確性,需要使用鎖來實現(xiàn)互斥訪問共享資源,然而,使用本地鎖在分布式系統(tǒng)中存在問題,這篇文章主要介紹了從原理到實踐分析?Redis?分布式鎖的多種實現(xiàn)方案,需要的朋友可以參考下2024-07-07

