国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

通用分布式鎖組件

這篇具有很好參考價(jià)值的文章主要介紹了通用分布式鎖組件。希望對大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

自定義注解實(shí)現(xiàn)通用分布式鎖組件。

1 Redisson

Redisson官網(wǎng):https://redisson.org/

1.1介紹

通用分布式鎖組件,分布式與微服務(wù),Redis,分布式,redis

Redisson是一個(gè)基于Redis的工具包,可以幫助開發(fā)人員更輕松地使用Redis,功能非常強(qiáng)大。將JDK中很多常見的隊(duì)列、鎖、對象都基于Redis實(shí)現(xiàn)了對應(yīng)的分布式版本并提供高級的分布式鎖,分布式集合,分布式對象,以及其他的高級Redis功能。

1.2 為什么要使用Redisson實(shí)現(xiàn)分布式鎖

1.2.1 鎖續(xù)期的問題

當(dāng)對業(yè)務(wù)進(jìn)行加鎖時(shí),鎖的過期時(shí)間,絕對不能想當(dāng)然的設(shè)置一個(gè)值。

假設(shè)線程A在執(zhí)行某個(gè)業(yè)務(wù)時(shí)加鎖成功并設(shè)置鎖過期時(shí)間。但該業(yè)務(wù)執(zhí)行時(shí)間過長,業(yè)務(wù)的執(zhí)行時(shí)間超過了鎖過期時(shí)間,那么在業(yè)務(wù)還沒執(zhí)行完時(shí),鎖就自動釋放了。

接著后續(xù)線程就可以獲取到鎖,又來執(zhí)行該業(yè)務(wù)。就會造成線程A還沒執(zhí)行完,后續(xù)線程又來執(zhí)行,導(dǎo)致同一個(gè)業(yè)務(wù)邏輯被重復(fù)執(zhí)行。因此對于鎖的超時(shí)時(shí)間,需要結(jié)合著業(yè)務(wù)執(zhí)行時(shí)間來判斷,讓鎖的過期時(shí)間大于業(yè)務(wù)執(zhí)行時(shí)間。

業(yè)務(wù)執(zhí)行時(shí)間的影響因素太多了,無法確定一個(gè)準(zhǔn)確值,只能是一個(gè)估值。無法百分百保證業(yè)務(wù)執(zhí)行期間,鎖只能被一個(gè)線程占有。

如想保證的話,可以在創(chuàng)建鎖的同時(shí)創(chuàng)建一個(gè)守護(hù)線程,同時(shí)定義一個(gè)定時(shí)任務(wù)每隔一段時(shí)間去為未釋放的鎖增加過期時(shí)間。當(dāng)業(yè)務(wù)執(zhí)行完,釋放鎖后,再關(guān)閉守護(hù)線程。 這種實(shí)現(xiàn)思想可以用來解決鎖續(xù)期。

通用分布式鎖組件,分布式與微服務(wù),Redis,分布式,redis

1.2.2 獲取鎖嘗試的問題

在我們的項(xiàng)目中, 可能會有這樣的情況:

多個(gè)線程競爭獲得鎖, 同一時(shí)刻只有一個(gè)線程獲得到鎖, 其它線程應(yīng)該嘗試獲得鎖。而我們在使用Redis實(shí)現(xiàn)分布式鎖的時(shí)候,獲得不到鎖了,就不再嘗試獲得鎖了,而是直接放棄了。

如果要實(shí)現(xiàn),我們可以采取自旋的方式,同時(shí)設(shè)置一個(gè)超時(shí)時(shí)間。

1.2.3 可重入問題

當(dāng)一個(gè)線程擁有一個(gè)鎖時(shí),它可以重復(fù)獲取該鎖而不會被自己所持有的鎖阻塞??芍厝腈i通常用于高并發(fā)環(huán)境中,以保證線程安全性和避免死鎖的發(fā)生。而我們在使用Redis實(shí)現(xiàn)分布式鎖的時(shí)候,根本沒辦法重入。

像這樣的問題還有很多,如果要實(shí)現(xiàn)一個(gè)生產(chǎn)級別,比較完美的分布式鎖,是個(gè)很耗時(shí)耗力的工作。所以工作里面一般不會自己封裝分布式鎖,如果使用Redis實(shí)現(xiàn)分布式鎖,一般選擇Redisson來實(shí)現(xiàn)。

1.3 Wath Dog的自動延期機(jī)制

剛才提到過,自己實(shí)現(xiàn)的鎖可能存在鎖續(xù)期的問題,但是Redission就提供了一種自動延期機(jī)制解決了這個(gè)問題。

通用分布式鎖組件,分布式與微服務(wù),Redis,分布式,redis

如果拿到分布式鎖的節(jié)點(diǎn)(微服務(wù))宕機(jī),且這個(gè)鎖正好處于鎖住的狀態(tài)時(shí),會出現(xiàn)鎖死的狀態(tài),為了避免這種情況的發(fā)生,鎖都會設(shè)置一個(gè)過期時(shí)間。這樣也存在一個(gè)問題,加入一個(gè)線程拿到了鎖設(shè)置了30s超時(shí),在30s后這個(gè)線程還沒有執(zhí)行完畢,鎖超時(shí)釋放了,就會導(dǎo)致問題,Redisson給出了自己的答案,就是 watch dog 自動延期機(jī)制。

Redisson提供了一個(gè)監(jiān)控鎖的看門狗,它的作用是在Redisson實(shí)例被關(guān)閉前,不斷的延長鎖的有效期,也就是說,如果一個(gè)拿到鎖的線程一直沒有完成邏輯,那么看門狗會幫助線程不斷的延長鎖超時(shí)時(shí)間,鎖不會因?yàn)槌瑫r(shí)而被釋放。

默認(rèn)情況下,看門狗的續(xù)期時(shí)間是30s,也可以通過修改config.lockWatchdogTimeout來另行指定。

另外Redisson 還提供了可以指定leaseTime參數(shù)的加鎖方法來指定加鎖的時(shí)間。超過這個(gè)時(shí)間后鎖便自動解開了,不會延長鎖的有效期。

  1. watch dog 在當(dāng)前節(jié)點(diǎn)存活時(shí)每10s給分布式鎖的key續(xù)期 30s;
  2. watch dog 機(jī)制啟動,且代碼中沒有釋放鎖操作時(shí),watch dog 會不斷的給鎖續(xù)期;

1.4 快速了解

首先引入依賴:

<!--redisson-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
</dependency>

然后是配置:

 @Configuration
 public class RedisConfig {
    @Bean
    public RedissonClient redissonClient() {
        // 配置類
        Config config = new Config();
        // 添加redis地址,這里添加了單點(diǎn)的地址,也可以使用config.useClusterServers()添加集群地址 
        config.useSingleServer()
            .setAddress("redis://192.168.150.101:6379")
            .setPassword("123456");
        // 創(chuàng)建客戶端
        return Redisson.create(config);
    }
 }

最后是基本用法:

 @Autowired
 private RedissonClient redissonClient;

 @Test
 void testRedisson() throws InterruptedException {
    // 1.獲取鎖對象,指定鎖名稱
    RLock lock = redissonClient.getLock("anyLock");
    try {
        // 2.嘗試獲取鎖,參數(shù):waitTime、leaseTime、時(shí)間單位
        boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
        if (!isLock) {
            // 獲取鎖失敗處理 ..
        } else {
            // 獲取鎖成功處理
        }
    } finally {
        // 4.釋放鎖
        lock.unlock();
    }
 }

利用Redisson獲取鎖時(shí)可以傳3個(gè)參數(shù):

  • waitTime:獲取鎖的等待時(shí)間。當(dāng)獲取鎖失敗后可以多次重試,直到waitTime時(shí)間耗盡。waitTime默認(rèn)-1,即失敗后立刻返回,不重試。
  • leaseTime:鎖超時(shí)釋放時(shí)間。默認(rèn)是30,同時(shí)會利用WatchDog來不斷更新超時(shí)時(shí)間。需要注意的是,如果手動設(shè)置leaseTime值,會導(dǎo)致WatchDog失效。
  • TimeUnit:時(shí)間單位

1.5 項(xiàng)目集成

關(guān)鍵基礎(chǔ)配置:

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.tianji.common.autoconfigure.redisson.aspect.LockAspect;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

@Slf4j
@ConditionalOnClass({RedissonClient.class, Redisson.class})
@Configuration
@EnableConfigurationProperties(RedisProperties.class)
public class RedissonConfig {
    private static final String REDIS_PROTOCOL_PREFIX = "redis://";
    private static final String REDISS_PROTOCOL_PREFIX = "rediss://";

    @Bean
    @ConditionalOnMissingBean
    public LockAspect lockAspect(RedissonClient redissonClient){
        return new LockAspect(redissonClient);
    }

    @Bean
    @ConditionalOnMissingBean
    public RedissonClient redissonClient(RedisProperties properties){
        log.debug("嘗試初始化RedissonClient");
        // 1.讀取Redis配置
        RedisProperties.Cluster cluster = properties.getCluster();
        RedisProperties.Sentinel sentinel = properties.getSentinel();
        String password = properties.getPassword();
        int timeout = 3000;
        Duration d = properties.getTimeout();
        if(d != null){
            timeout = Long.valueOf(d.toMillis()).intValue();
        }
        // 2.設(shè)置Redisson配置
        Config config = new Config();
        if(cluster != null && !CollectionUtil.isEmpty(cluster.getNodes())){
            // 集群模式
            config.useClusterServers()
                    .addNodeAddress(convert(cluster.getNodes()))
                    .setConnectTimeout(timeout)
                    .setPassword(password);
        }else if(sentinel != null && !StrUtil.isEmpty(sentinel.getMaster())){
            // 哨兵模式
            config.useSentinelServers()
                    .setMasterName(sentinel.getMaster())
                    .addSentinelAddress(convert(sentinel.getNodes()))
                    .setConnectTimeout(timeout)
                    .setDatabase(0)
                    .setPassword(password);
        }else{
            // 單機(jī)模式
            config.useSingleServer()
                    .setAddress(String.format("redis://%s:%d", properties.getHost(), properties.getPort()))
                    .setConnectTimeout(timeout)
                    .setDatabase(0)
                    .setPassword(password);
        }
        // 3.創(chuàng)建Redisson客戶端
        return Redisson.create(config);
    }

    private String[] convert(List<String> nodesObject) {
        List<String> nodes = new ArrayList<>(nodesObject.size());
        for (String node : nodesObject) {
            if (!node.startsWith(REDIS_PROTOCOL_PREFIX) && !node.startsWith(REDISS_PROTOCOL_PREFIX)) {
                nodes.add(REDIS_PROTOCOL_PREFIX + node);
            } else {
                nodes.add(node);
            }
        }
        return nodes.toArray(new String[0]);
    }
}

幾個(gè)關(guān)鍵點(diǎn):

  • 這個(gè)配置上添加了條件注解@ConditionalOnClass({RedissonClient.class, Redisson.class}) 也就是說,只要引用了配置所在模塊,并且引用了Redisson依賴,這套配置就會生效。不引入Redisson依賴,配置自然不會生效,從而實(shí)現(xiàn)按需引入。
  • RedissonClient的配置無需自定義Redis地址,而是直接基于SpringBoot中的Redis配置即可。而且不管是Redis單機(jī)、Redis集群、Redis哨兵模式都可以支持

2 定義通用分布式鎖組件

Redisson的分布式鎖使用并不復(fù)雜,基本步驟包括:

  • 1)創(chuàng)建鎖對象
  • 2)嘗試獲取鎖
  • 3)處理業(yè)務(wù)
  • 4)釋放鎖

但是,除了第3步以外,其它都是非業(yè)務(wù)代碼,對業(yè)務(wù)的侵入較多:

通用分布式鎖組件,分布式與微服務(wù),Redis,分布式,redis

可以發(fā)現(xiàn),非業(yè)務(wù)代碼格式固定,每次獲取鎖總是在重復(fù)編碼。我們可不可以對這部分代碼進(jìn)行抽取和簡化呢?

2.1 實(shí)現(xiàn)思路分析

要優(yōu)化這部分代碼,需要通過整個(gè)流程來分析:

通用分布式鎖組件,分布式與微服務(wù),Redis,分布式,redis
可以發(fā)現(xiàn),只有紅框部分是業(yè)務(wù)功能,業(yè)務(wù)前、后都是固定的鎖操作。既然如此,我們完全可以基于AOP的思想,將業(yè)務(wù)部分作為切入點(diǎn),將業(yè)務(wù)前后的鎖操作作為環(huán)繞增強(qiáng)。

但是,我們該如何標(biāo)記這些切入點(diǎn)呢?
不是每一個(gè)service方法都需要加鎖,因此我們不能直接基于類來確定切入點(diǎn);另外,需要加鎖的方法可能也較多,我們不能基于方法名作為切入點(diǎn),這樣太麻煩。因此,最好的辦法是把加鎖的方法給標(biāo)記出來,利用標(biāo)記來確定切入點(diǎn)。如何標(biāo)記呢?

最常見的辦法就是基于注解來標(biāo)記了。同時(shí),加鎖時(shí)還有一些參數(shù),比如:鎖的key名稱、鎖的waitTime、releaseTime等等,都可以基于注解來傳參。

因此,注解的核心作用是兩個(gè):

  • 標(biāo)記切入點(diǎn)
  • 傳遞鎖參數(shù)

綜上,我們計(jì)劃利用注解來標(biāo)記切入點(diǎn),傳遞鎖參數(shù)。同時(shí)利用AOP環(huán)繞增強(qiáng)來實(shí)現(xiàn)加鎖、釋放鎖等操作。

2.2 定義注解

注解本身起到標(biāo)記作用,同時(shí)還要帶上鎖參數(shù):

  • 鎖名稱
  • 鎖等待時(shí)間
  • 鎖超時(shí)時(shí)間
  • 時(shí)間單位
  • 方法結(jié)束是否釋放鎖
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyLock {
    /**
     * 加鎖key的表達(dá)式,支持SPEL表達(dá)式
     */
    String name();

    /**
     * 阻塞超時(shí)時(shí)長,不指定 waitTime 則按照Redisson默認(rèn)時(shí)長
     */
    long waitTime() default 1;

    /**
     * 鎖自動釋放時(shí)長,默認(rèn)是-1,其實(shí)是30秒 + watchDog模式
     */
    long leaseTime() default -1;

    /**
     * 時(shí)間單位,默認(rèn)為秒
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;

    /**
     * 如果設(shè)定了false,則方法結(jié)束不釋放鎖,而是等待leaseTime后自動釋放
     */
    boolean autoUnlock() default true;
}

2.3 定義切面

接下來,我們定義一個(gè)環(huán)繞增強(qiáng)的切面,實(shí)現(xiàn)加鎖、釋放鎖:

package com.tianji.promotion.utils;

import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

@Component
@Aspect
@RequiredArgsConstructor
public class MyLockAspect implements Ordered{

    private final RedissonClient redissonClient;

    @Around("@annotation(myLock)")
    public Object tryLock(ProceedingJoinPoint pjp, MyLock myLock) throws Throwable {
    	if (!myLock.autoUnlock() && myLock.leaseTime() <= 0) {
            // 不手動釋放鎖時(shí),必須指定leaseTime時(shí)間
            throw new BizIllegalException("leaseTime不能為空");
        }
        // 1.創(chuàng)建鎖對象
        RLock lock = redissonClient.getLock(myLock.name());
        // 2.嘗試獲取鎖
        boolean isLock = lock.tryLock(myLock.waitTime(), myLock.leaseTime(), myLock.unit());
        // 3.判斷是否成功
        if(!isLock) {
            // 3.1.失敗,快速結(jié)束
            throw new BizIllegalException("請求太頻繁");
        }
        try {
            // 3.2.成功,執(zhí)行業(yè)務(wù)
            return pjp.proceed();
        } finally {
            // 4.釋放鎖
            if (myLock.autoUnlock()) {
                lock.unlock();
            }
        }
    }
    /**
     * 指定切面注解的優(yōu)先執(zhí)行順序
     * 這里設(shè)置鎖注解要優(yōu)先于其他注解執(zhí)行
     * (先加鎖,再執(zhí)行事務(wù))
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

2.4 使用鎖

定義好了鎖注解和切面,接下來使用直接加上注解就行了:

通用分布式鎖組件,分布式與微服務(wù),Redis,分布式,redis

可以看到,業(yè)務(wù)中無需手動編寫加鎖、釋放鎖的邏輯了,沒有任何業(yè)務(wù)侵入,使用起來也非常優(yōu)雅。

不過呢,現(xiàn)在還存在幾個(gè)問題:

  • Redisson中鎖的種類有很多,目前的代碼中把鎖的類型寫死了
  • Redisson中獲取鎖的邏輯有多種,比如獲取鎖失敗的重試策略,目前都沒有設(shè)置
  • 鎖的名稱目前是寫死的,并不能根據(jù)方法參數(shù)動態(tài)變化

所以呢,我們接下來還要對鎖的實(shí)現(xiàn)進(jìn)行優(yōu)化,注意解決上述問題。

2.5.工廠模式切換鎖類型

Redisson中鎖的類型有多種,例如:

通用分布式鎖組件,分布式與微服務(wù),Redis,分布式,redis

因此,我們不能在切面中把鎖的類型寫死,而是交給用戶自己選擇鎖類型。

那么問題來了,如何讓用戶選擇鎖類型呢?
鎖的類型雖然有多種,但類型是有限的幾種,完全可以通過枚舉定義出來。然后把這個(gè)枚舉作為MyLock注解的參數(shù),交給用戶去選擇自己要用的類型。

而在切面中,我們則需要根據(jù)用戶選擇的鎖類型,創(chuàng)建對應(yīng)的鎖對象即可。但是這個(gè)邏輯不能通過if-else來實(shí)現(xiàn),太low了。
這里我們的需求是根據(jù)用戶選擇的鎖類型,創(chuàng)建不同的鎖對象。有一種設(shè)計(jì)模式剛好可以解決這個(gè)問題:簡單工廠模式

2.5.1 鎖類型枚舉

我們首先定義一個(gè)鎖類型枚舉:

public enum MyLockType {
    RE_ENTRANT_LOCK, // 可重入鎖
    FAIR_LOCK, // 公平鎖
    READ_LOCK, // 讀鎖
    WRITE_LOCK, // 寫鎖
    ;
}

然后在自定義注解中添加鎖類型這個(gè)參數(shù):

	/**
     * 使用的鎖類型,默認(rèn)可重入鎖
     * @return
     */
    MyLockType lockType() default MyLockType.RE_ENTRANT_LOCK;

通用分布式鎖組件,分布式與微服務(wù),Redis,分布式,redis

2.5.2 鎖對象工廠

然后定義一個(gè)鎖工廠,用于根據(jù)鎖類型創(chuàng)建鎖對象:

import com.xxx.enums.MyLockType;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

import java.util.EnumMap;
import java.util.Map;
import java.util.function.Function;

import static com.tianji.promotion.enums.MyLockType.*;


@Component
public class MyLockFactory {

    //封裝的是方法引用
    private final Map<MyLockType, Function<String, RLock>> lockHandlers;

    public MyLockFactory(RedissonClient redissonClient) {
        this.lockHandlers = new EnumMap<>(MyLockType.class);
        this.lockHandlers.put(RE_ENTRANT_LOCK, redissonClient::getLock);
        this.lockHandlers.put(FAIR_LOCK, redissonClient::getFairLock);
        this.lockHandlers.put(READ_LOCK, name -> redissonClient.getReadWriteLock(name).readLock());
        this.lockHandlers.put(WRITE_LOCK, name -> redissonClient.getReadWriteLock(name).writeLock());
    }

    public RLock getLock(MyLockType lockType, String name){
        //.apply調(diào)用方法引用封裝的方法
        return lockHandlers.get(lockType).apply(name);
    }
}

說明:

  • MyLockFactory內(nèi)部持有了一個(gè)Map,key是鎖類型枚舉,值是創(chuàng)建鎖對象的Function。注意這里不是存鎖對象,因?yàn)殒i對象必須是多例的,不同業(yè)務(wù)用不同鎖對象;同一個(gè)業(yè)務(wù)用相同鎖對象。
  • MyLockFactory內(nèi)部的Map采用了EnumMap。只有當(dāng)Key是枚舉類型時(shí)可以使用EnumMap,其底層不是hash表,而是簡單的數(shù)組。由于枚舉項(xiàng)數(shù)量固定,因此這個(gè)數(shù)組長度就等于枚舉項(xiàng)個(gè)數(shù),然后按照枚舉項(xiàng)序號作為角標(biāo)依次存入數(shù)組。這樣就能根據(jù)枚舉項(xiàng)序號作為角標(biāo)快速定位到數(shù)組中的數(shù)據(jù)。

2.5.3 改造切面代碼

我們將鎖對象工廠注入MyLockAspect,然后就可以利用工廠來獲取鎖對象了:

private final MyLockFactory myLockFactory;

RLock lock = myLockFactory.getLock(myLock.lockType(),myLock.name());

通用分布式鎖組件,分布式與微服務(wù),Redis,分布式,redis

此時(shí),在業(yè)務(wù)中,就能通過注解來指定自己要用的鎖類型了:

通用分布式鎖組件,分布式與微服務(wù),Redis,分布式,redis

2.6 鎖失敗策略

多線程爭搶鎖,大部分線程會獲取鎖失敗,而失敗后的處理方案和策略是多種多樣的。目前,我們獲取鎖失敗后就是直接拋出異常,沒有其它策略,這與實(shí)際需求不一定相符。

2.6.1 策略分析

接下來,我們就分析一下鎖失敗的處理策略有哪些。

大的方面來說,獲取鎖失敗要從兩方面來考慮:

  • 獲取鎖失敗是否要重試?有三種策略:
    • 不重試,對應(yīng)API:lock.tryLock(0, 10, SECONDS),也就是waitTime小于等于0
    • 有限次數(shù)重試:對應(yīng)API:lock.tryLock(5, 10, SECONDS),也就是waitTime大于0,重試一定waitTime時(shí)間后結(jié)束
    • 無限重試:對應(yīng)API lock.lock(10, SECONDS) , lock就是無限重試
  • 重試失敗后怎么處理?有兩種策略:
    • 直接結(jié)束
    • 拋出異常

對應(yīng)的API和策略名如下:

通用分布式鎖組件,分布式與微服務(wù),Redis,分布式,redis

重試策略 + 失敗策略組合,總共以下幾種情況:

通用分布式鎖組件,分布式與微服務(wù),Redis,分布式,redis

那么該如何用代碼來表示這些失敗策略,并讓用戶自由選擇呢?

相信大家應(yīng)該能想到一種設(shè)計(jì)模式:策略模式。同時(shí),我們還需要定義一個(gè)失敗策略的枚舉。在MyLock注解中定義這個(gè)枚舉類型的參數(shù),供用戶選擇。

注意:

一般的策略模式大概是這樣:

  • 定義策略接口
  • 定義不同策略實(shí)現(xiàn)類
  • 提供策略工廠,便于根據(jù)策略枚舉獲取不同策略實(shí)現(xiàn)

而在策略比較簡單的情況下,我們完全可以用枚舉代替策略工廠,簡化策略模式。

綜上,我們可以定義一個(gè)基于枚舉的策略模式,簡化開發(fā)。

2.6.2 策略實(shí)現(xiàn)

我們定義一個(gè)失敗策略枚舉,直接將失敗策略定義到枚舉中:

package com.xxx.utils;

import com.xxx.common.exceptions.BizIllegalException;//自定義業(yè)務(wù)異常
import org.redisson.api.RLock;

public enum MyLockStrategy {
    SKIP_FAST(){
        @Override
        public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
            return lock.tryLock(0, prop.leaseTime(), prop.unit());
        }
    },
    FAIL_FAST(){
        @Override
        public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
            boolean isLock = lock.tryLock(0, prop.leaseTime(), prop.unit());
            if (!isLock) {
                throw new BizIllegalException("請求太頻繁");
            }
            return true;
        }
    },
    KEEP_TRYING(){
        @Override
        public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
            lock.lock( prop.leaseTime(), prop.unit());
            return true;
        }
    },
    SKIP_AFTER_RETRY_TIMEOUT(){
        @Override
        public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
            return lock.tryLock(prop.waitTime(), prop.leaseTime(), prop.unit());
        }
    },
    FAIL_AFTER_RETRY_TIMEOUT(){
        @Override
        public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
            boolean isLock = lock.tryLock(prop.waitTime(), prop.leaseTime(), prop.unit());
            if (!isLock) {
                throw new BizIllegalException("請求太頻繁");
            }
            return true;
        }
    },
    ;

    public abstract boolean tryLock(RLock lock, MyLock prop) throws InterruptedException;
}

然后,在MyLock注解中添加枚舉參數(shù):

/**
* 定義鎖失敗后的策略
* @return
*/
MyLockStrategy lockStrategy() default MyLockStrategy.FAIL_AFTER_RETRY_TIMEOUT;

通用分布式鎖組件,分布式與微服務(wù),Redis,分布式,redis

最后,修改切面代碼,基于用戶選擇的策略來處理:

boolean isLock = myLock.lockStrategy().tryLock(lock, myLock);

通用分布式鎖組件,分布式與微服務(wù),Redis,分布式,redis

最后,修改切面代碼,基于用戶選擇的策略來處理:

通用分布式鎖組件,分布式與微服務(wù),Redis,分布式,redis

這個(gè)時(shí)候,我們就可以在使用鎖的時(shí)候自由選擇鎖類型、鎖策略了:

通用分布式鎖組件,分布式與微服務(wù),Redis,分布式,redis

2.7 基于SPEL的動態(tài)鎖名

現(xiàn)在還剩下最后一個(gè)問題,就是鎖名稱的問題。
在當(dāng)前業(yè)務(wù)中,我們的鎖對象本來應(yīng)該是當(dāng)前登錄用戶,是動態(tài)獲取的。而加鎖是基于注解參數(shù)添加的,在編碼時(shí)就需要指定。怎么辦?

Spring中提供了一種表達(dá)式語法,稱為SPEL表達(dá)式,可以執(zhí)行java代碼,獲取任意參數(shù)。

思路:
我們可以讓用戶指定鎖名稱參數(shù)時(shí)不要寫死,而是基于SPEL表達(dá)式。在創(chuàng)建鎖對象時(shí),解析SPEL表達(dá)式,動態(tài)獲取鎖名稱。

思路很簡單,不過SPEL表達(dá)式的解析還是比較復(fù)雜的。不推薦自己編寫。

2.7.1 SPEL表達(dá)式

SPEL的表達(dá)式語法可以參考官網(wǎng)文檔:https://docs.spring.io/spring-framework/docs/3.0.x/reference/expressions.html

中文文檔:https://itmyhome.com/spring/expressions.html

首先,在使用鎖注解時(shí),鎖名稱可以利用SPEL表達(dá)式,例如我們指定鎖名稱中要包含參數(shù)中的用戶id,則可以這樣寫:

通用分布式鎖組件,分布式與微服務(wù),Redis,分布式,redis

而如果是通過UserContext.getUser()獲取,則可以利用下面的語法:

@MyLock(name="lock:coupon:#{T(com.common.util.UserContext).getUser()}")

這里T(類名).方法名()就是調(diào)用靜態(tài)方法。

2.7.2 解析SPEL

在切面中,我們需要基于注解中的鎖名稱做動態(tài)解析,而不是直接使用名稱:

通用分布式鎖組件,分布式與微服務(wù),Redis,分布式,redis

其中獲取鎖名稱用的是getLockName()這個(gè)方法:


/**
 * SPEL的正則規(guī)則
 */
private static final Pattern pattern = Pattern.compile("\\#\\{([^\\}]*)\\}");
/**
 * 方法參數(shù)解析器
 */
private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

/**
 * 解析鎖名稱
 * @param name 原始鎖名稱
 * @param pjp 切入點(diǎn)
 * @return 解析后的鎖名稱
 */
private String getLockName(String name, ProceedingJoinPoint pjp) {
    // 1.判斷是否存在spel表達(dá)式
    if (StringUtils.isBlank(name) || !name.contains("#")) {
        // 不存在,直接返回
        return name;
    }
    // 2.構(gòu)建context,也就是SPEL表達(dá)式獲取參數(shù)的上下文環(huán)境,這里上下文就是切入點(diǎn)的參數(shù)列表
    EvaluationContext context = new MethodBasedEvaluationContext(
            TypedValue.NULL, resolveMethod(pjp), pjp.getArgs(), parameterNameDiscoverer);
    // 3.構(gòu)建SPEL解析器
    ExpressionParser parser = new SpelExpressionParser();
    // 4.循環(huán)處理,因?yàn)楸磉_(dá)式中可以包含多個(gè)表達(dá)式
    Matcher matcher = pattern.matcher(name);
    while (matcher.find()) {
        // 4.1.獲取表達(dá)式
        String tmp = matcher.group();
        String group = matcher.group(1);
        // 4.2.這里要判斷表達(dá)式是否以 T字符開頭,這種屬于解析靜態(tài)方法,不走上下文
        Expression expression = parser.parseExpression(group.charAt(0) == 'T' ? group : "#" + group);
        // 4.3.解析出表達(dá)式對應(yīng)的值
        Object value = expression.getValue(context);
        // 4.4.用值替換鎖名稱中的SPEL表達(dá)式
        name = name.replace(tmp, ObjectUtils.nullSafeToString(value));
    }
    return name;
}

private Method resolveMethod(ProceedingJoinPoint pjp) {
    // 1.獲取方法簽名
    MethodSignature signature = (MethodSignature)pjp.getSignature();
    // 2.獲取字節(jié)碼
    Class<?> clazz = pjp.getTarget().getClass();
    // 3.方法名稱
    String name = signature.getName();
    // 4.方法參數(shù)列表
    Class<?>[] parameterTypes = signature.getMethod().getParameterTypes();
    return tryGetDeclaredMethod(clazz, name, parameterTypes);
}

private Method tryGetDeclaredMethod(Class<?> clazz, String name, Class<?> ... parameterTypes){
    try {
        // 5.反射獲取方法
        return clazz.getDeclaredMethod(name, parameterTypes);
    } catch (NoSuchMethodException e) {
        Class<?> superClass = clazz.getSuperclass();
        if (superClass != null) {
            // 嘗試從父類尋找
            return tryGetDeclaredMethod(superClass, name, parameterTypes);
        }
    }
    return null;
}

2.8 完整代碼

MyLockAspect 經(jīng)過一步步修改與最開始在文章中出現(xiàn)有差異這里給出完整版。文章來源地址http://www.zghlxwxcb.cn/news/detail-845019.html

import com.common.utils.StringUtils;
import com.promotion.anno.MyLock;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.Ordered;
import org.springframework.core.ParameterNameDiscoverer;

import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import java.lang.reflect.Method;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Component
@Aspect
@RequiredArgsConstructor
public class MyLockAspect implements Ordered {

    //    private final RedissonClient redissonClient;
    private final MyLockFactory myLockFactory;

    @Around("@annotation(myLock)")
    public Object tryLock(ProceedingJoinPoint pjp, MyLock myLock) throws Throwable {
    	if (!myLock.autoUnlock() && myLock.leaseTime() <= 0) {
            // 不手動釋放鎖時(shí),必須指定leaseTime時(shí)間
            throw new BizIllegalException("leaseTime不能為空");
        }
        // 1.創(chuàng)建鎖對象
        //RLock lock = redissonClient.getLock(myLock.name());//獲取可重入鎖
        String lockName = getLockName(myLock.name(), pjp);
        RLock lock = myLockFactory.getLock(myLock.lockType(),lockName);
        // 2.嘗試獲取鎖
//        boolean isLock = lock.tryLock(myLock.waitTime(), myLock.leaseTime(), myLock.unit());
        //使用策略模式獲取鎖
        boolean isLock = myLock.lockStrategy().tryLock(lock, myLock);
        // 3.判斷是否成功
        if (!isLock) {
            // 3.1.失敗,快速結(jié)束(使用策略模式后內(nèi)部會自己拋異常)
            return null;
        }
        try {
            // 3.2.成功,執(zhí)行業(yè)務(wù)
            return pjp.proceed();
        } finally {
            // 4.釋放鎖
            if (myLock.autoUnlock()) {
                lock.unlock();
            }
        }
    }

    /**
     * 指定切面注解的優(yōu)先執(zhí)行順序
     * 這里設(shè)置要高于其他注解
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }



    /**
     * SPEL的正則規(guī)則
     */
    private static final Pattern pattern = Pattern.compile("\\#\\{([^\\}]*)\\}");
    /**
     * 方法參數(shù)解析器
     */
    private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

    /**
     * 解析鎖名稱
     * @param name 原始鎖名稱
     * @param pjp 切入點(diǎn)
     * @return 解析后的鎖名稱
     */
    private String getLockName(String name, ProceedingJoinPoint pjp) {
        // 1.判斷是否存在spel表達(dá)式
        if (StringUtils.isBlank(name) || !name.contains("#")) {
            // 不存在,直接返回
            return name;
        }
        // 2.構(gòu)建context,也就是SPEL表達(dá)式獲取參數(shù)的上下文環(huán)境,這里上下文就是切入點(diǎn)的參數(shù)列表
        EvaluationContext context = new MethodBasedEvaluationContext(
                TypedValue.NULL, resolveMethod(pjp), pjp.getArgs(), parameterNameDiscoverer);
        // 3.構(gòu)建SPEL解析器
        ExpressionParser parser = new SpelExpressionParser();
        // 4.循環(huán)處理,因?yàn)楸磉_(dá)式中可以包含多個(gè)表達(dá)式
        Matcher matcher = pattern.matcher(name);
        while (matcher.find()) {
            // 4.1.獲取表達(dá)式
            String tmp = matcher.group();
            String group = matcher.group(1);
            // 4.2.這里要判斷表達(dá)式是否以 T字符開頭,這種屬于解析靜態(tài)方法,不走上下文
            Expression expression = parser.parseExpression(group.charAt(0) == 'T' ? group : "#" + group);
            // 4.3.解析出表達(dá)式對應(yīng)的值
            Object value = expression.getValue(context);
            // 4.4.用值替換鎖名稱中的SPEL表達(dá)式
            name = name.replace(tmp, ObjectUtils.nullSafeToString(value));
        }
        return name;
    }

    private Method resolveMethod(ProceedingJoinPoint pjp) {
        // 1.獲取方法簽名
        MethodSignature signature = (MethodSignature)pjp.getSignature();
        // 2.獲取字節(jié)碼
        Class<?> clazz = pjp.getTarget().getClass();
        // 3.方法名稱
        String name = signature.getName();
        // 4.方法參數(shù)列表
        Class<?>[] parameterTypes = signature.getMethod().getParameterTypes();
        return tryGetDeclaredMethod(clazz, name, parameterTypes);
    }

    private Method tryGetDeclaredMethod(Class<?> clazz, String name, Class<?> ... parameterTypes){
        try {
            // 5.反射獲取方法
            return clazz.getDeclaredMethod(name, parameterTypes);
        } catch (NoSuchMethodException e) {
            Class<?> superClass = clazz.getSuperclass();
            if (superClass != null) {
                // 嘗試從父類尋找
                return tryGetDeclaredMethod(superClass, name, parameterTypes);
            }
        }
        return null;
    }
}

到了這里,關(guān)于通用分布式鎖組件的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • 9.4. 分布式與微服務(wù)架構(gòu)

    9.4. 分布式與微服務(wù)架構(gòu)

    在本章節(jié)中,我們將介紹分布式系統(tǒng)和微服務(wù)架構(gòu)的基本概念。分布式系統(tǒng)解決了單體應(yīng)用面臨的可擴(kuò)展性、高可用性等問題,而微服務(wù)架構(gòu)進(jìn)一步提升了系統(tǒng)的可維護(hù)性和靈活性。 9.4.1. 分布式系統(tǒng)基本概念 分布式系統(tǒng)是由多個(gè)獨(dú)立的計(jì)算節(jié)點(diǎn)組成的系統(tǒng),這些節(jié)點(diǎn)通過網(wǎng)

    2024年02月08日
    瀏覽(88)
  • 微服務(wù)技術(shù)棧SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式(五):分布式搜索 ES-下

    微服務(wù)技術(shù)棧SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式(五):分布式搜索 ES-下

    聚合(aggregations)可以實(shí)現(xiàn)對文檔數(shù)據(jù)的統(tǒng)計(jì)、分析、運(yùn)算。聚合常見的有三類: 桶(Bucket)聚合:用來對文檔做分組 TermAggregation:按照文檔字段值分組 Date Histogram:按照日期階梯分組,例如一周為一組,或者一月為一組 度量(Metric)聚合:用以計(jì)算一些值,比如:最大值

    2024年03月26日
    瀏覽(37)
  • 微服務(wù)學(xué)習(xí):SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式

    微服務(wù)學(xué)習(xí):SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式

    目錄 一、高級篇 二、面試篇 ==============實(shí)用篇============== day05-Elasticsearch01 1.初識elasticsearch 1.4.安裝es、kibana 1.4.1.部署單點(diǎn)es 1.4.2.部署kibana 1.4.3.安裝IK分詞器 1.4.4.總結(jié) 2.索引庫操作 2.1.mapping映射屬性 2.2.索引庫的CRUD 2.2.1.創(chuàng)建索引庫和映射 2.2.2.查詢索引庫 2.2.3.修改索引庫 2.

    2024年02月02日
    瀏覽(25)
  • 分布式系統(tǒng)與微服務(wù)的區(qū)別是什么?

    分布式系統(tǒng)和微服務(wù)是兩個(gè)相關(guān)但不同的概念,它們都是在構(gòu)建復(fù)雜的軟件應(yīng)用時(shí)使用的架構(gòu)思想。 分布式系統(tǒng): 分布式系統(tǒng)是指由多個(gè)獨(dú)立的計(jì)算機(jī)或服務(wù)器通過網(wǎng)絡(luò)連接共同工作,協(xié)同完成一個(gè)任務(wù)或提供一個(gè)服務(wù)。在分布式系統(tǒng)中,各個(gè)計(jì)算機(jī)節(jié)點(diǎn)可以分擔(dān)任務(wù)的負(fù)

    2024年02月11日
    瀏覽(24)
  • 微服務(wù) - Redis緩存 · 數(shù)據(jù)結(jié)構(gòu) · 持久化 · 分布式 · 高并發(fā)

    微服務(wù) - Redis緩存 · 數(shù)據(jù)結(jié)構(gòu) · 持久化 · 分布式 · 高并發(fā)

    系列目錄 微服務(wù) - 概念 · 應(yīng)用 · 架構(gòu) · 通訊 · 授權(quán) · 跨域 · 限流 微服務(wù) - Consul集群化 · 服務(wù)注冊 · 健康檢測 · 服務(wù)發(fā)現(xiàn) · 負(fù)載均衡 微服務(wù) - Redis緩存 · 數(shù)據(jù)結(jié)構(gòu) · 持久化 · 分布式 · 高并發(fā) 微服務(wù) - Nginx網(wǎng)關(guān) · 進(jìn)程機(jī)制 · 限流熔斷 · 性能優(yōu)化 · 動態(tài)負(fù)載 · 高可用

    2023年04月18日
    瀏覽(23)
  • 【業(yè)務(wù)功能篇87】微服務(wù)-springcloud-本地緩存-redis-分布式緩存-緩存穿透-雪崩-擊穿

    【業(yè)務(wù)功能篇87】微服務(wù)-springcloud-本地緩存-redis-分布式緩存-緩存穿透-雪崩-擊穿

    ??緩存的作用是減低對數(shù)據(jù)源的訪問頻率。從而提高我們系統(tǒng)的性能。 緩存的流程圖 2.1 本地緩存 ??其實(shí)就是把緩存數(shù)據(jù)存儲在內(nèi)存中(Map String,Object ).在單體架構(gòu)中肯定沒有問題。 單體架構(gòu)下的緩存處理 2.2 分布式緩存 ??在分布式環(huán)境下,我們原來的本地緩存就不是

    2024年02月10日
    瀏覽(31)
  • SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式,系統(tǒng)詳解springcloud微服務(wù)技術(shù)棧

    SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式,系統(tǒng)詳解springcloud微服務(wù)技術(shù)棧

    我們發(fā)現(xiàn)在微服務(wù)中有一個(gè)令人頭疼的問題——部署,用Docker去解決這個(gè)部署難題 1、項(xiàng)目部署的問題 2、Docker 扔到一臺機(jī)器上,它們的依賴難道沒有干擾嗎?不會,docker將打包好的程序放到一個(gè)隔離容器去運(yùn)行,使用沙箱機(jī)制,避免互相干擾,之間不可見,這樣就解決了混

    2023年04月24日
    瀏覽(18)
  • 通用分布式鎖組件

    通用分布式鎖組件

    自定義注解實(shí)現(xiàn)通用分布式鎖組件。 Redisson官網(wǎng):https://redisson.org/ Redisson是一個(gè)基于Redis的工具包,可以幫助開發(fā)人員更輕松地使用Redis,功能非常強(qiáng)大。將JDK中很多常見的隊(duì)列、鎖、對象都基于Redis實(shí)現(xiàn)了對應(yīng)的分布式版本并提供高級的分布式鎖,分布式集合,分布式對象,

    2024年04月09日
    瀏覽(16)
  • 微服務(wù)---分布式多級緩存集群實(shí)現(xiàn)方案(Caffeine+redis+nginx本地緩存+Canal數(shù)據(jù)同步)

    微服務(wù)---分布式多級緩存集群實(shí)現(xiàn)方案(Caffeine+redis+nginx本地緩存+Canal數(shù)據(jù)同步)

    傳統(tǒng)的緩存策略一般是請求到達(dá)Tomcat后,先查詢Redis,如果未命中則查詢數(shù)據(jù)庫,如圖: 存在下面的問題: ?請求要經(jīng)過Tomcat處理,Tomcat的性能成為整個(gè)系統(tǒng)的瓶頸 ?Redis緩存失效時(shí),會對數(shù)據(jù)庫產(chǎn)生沖擊 多級緩存就是充分利用請求處理的每個(gè)環(huán)節(jié),分別添加緩存,減輕T

    2024年02月12日
    瀏覽(31)
  • 【業(yè)務(wù)功能100】補(bǔ)充代碼【業(yè)務(wù)功能88】微服務(wù)-springcloud-分布式鎖-redis-redisson-springcache

    采用redisson做分布式鎖,完成數(shù)據(jù)的查詢接口功能getCatelog2JSONRedis 原先從mysql數(shù)據(jù)庫查詢的效率較低,現(xiàn)在將部分固定數(shù)據(jù)展示比如頁面的樹形欄目信息等,存儲到 redis緩存 ,然后基于分布式集群,需要結(jié)合本地鎖(synchronized )與分布式鎖(redissonClient.getLock(“catelog2JSON-lock”

    2024年02月09日
    瀏覽(26)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包