最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Redisson分布式鎖的原理及分析

 更新時間:2026年05月27日 14:38:01   作者:無語堵上西樓  
分布式鎖的關(guān)鍵問題包括續(xù)期、釋放、可重入和互斥,Redisson提供完善的解決方案,通過WatchDog機制自動續(xù)期,支持分布式鎖的重入和高效釋放,避免死鎖,提升資源利用率

分布式鎖就要考慮鎖的續(xù)期、釋放、可重入、互斥等問題。Redisson這個客戶端是目前最完美的一種方案,它在內(nèi)部可以對鎖進行自動續(xù)期,程序執(zhí)行結(jié)束、發(fā)生異常或者整個應(yīng)用掛掉都可以釋放鎖,可重入和互斥也都處理的很好。

有Redisson了,我們沒必要自己手寫分布式鎖了,手寫的分布式鎖不如Redisson考慮的全面的。

Redisson分布式鎖方案優(yōu)點

  • Redisson 通過 Watch Dog(看門狗) 機制很好的解決了鎖的續(xù)期問題。
  • 通過 Redisson 實現(xiàn)分布式可重入鎖,比原生的SET mylock userId NX PX milliseconds + lua 實現(xiàn)的效果更好。
  • 在進程等待申請鎖的實現(xiàn)上也做了一些優(yōu)化,減少了無效的鎖申請,提升了資源的利用率。

分布式鎖要注意的問題

安全屬性(Safety property): 獨享(相互排斥)

  • 在任意一個時刻,只有一個客戶端持有鎖。

活性A(Liveness property A): 無死鎖

  • 即便持有鎖的客戶端崩潰(crashed)或者網(wǎng)絡(luò)被分 裂(gets partitioned),鎖仍然可以被獲取。

活性B(Liveness property B): 容錯

  • 只要大部分Redis節(jié)點都活著,客戶端就可以獲取和釋放鎖

為什么Redisson不用setnx實現(xiàn)分布式鎖?

Redisson沒有使用setnx命令實現(xiàn)分布式鎖,因為雖然setnx命令能夠?qū)崿F(xiàn)分布式鎖,但存在以下幾個問題:

  • 鎖過期時間不能自動續(xù)約。使用setnx命令實現(xiàn)分布式鎖時,如果獲取鎖的客戶端執(zhí)行時間過長,導(dǎo)致鎖過期,其它客戶端就有可能獲取到這個鎖。因此需要加入自動續(xù)約機制,在鎖的持有者自身沒有釋放鎖的情況下,對鎖進行續(xù)約以保證該鎖持續(xù)生效。
  • 不支持可重入。如果某個線程已經(jīng)持有了一個鎖,再次對這個鎖進行加鎖時,setnx命令會認為這個鍵已經(jīng)存在,無法再次進行加鎖。而支持可重入的鎖允許同一線程多次加鎖,且要求解鎖次數(shù)與加鎖次數(shù)相等。
  • 不支持鎖的釋放。setnx命令只能通過手動設(shè)置過期時間或者等待過期時間釋放鎖。如果某個線程異常退出或者未能及時釋放鎖,就有可能導(dǎo)致死鎖的發(fā)生。而支持釋放鎖的鎖允許設(shè)置鎖的自動釋放時間或者手動釋放鎖。

Redisson提供的分布式鎖可以解決以上三個問題,并提供了更為完善的分布式鎖功能,使得使用Redisson實現(xiàn)分布式鎖更加方便和穩(wěn)定。

例如:使用Lua腳本來保證原子性,使用Redis的watch機制來實現(xiàn)分布式鎖的釋放,使用watchdog機制實現(xiàn)分布式鎖的續(xù)期。

原理分析

用法

RLock lock = redisson.getLock("myLock");
lock.lock();
try {
    // do sth.
} finally {
    lock.unlock();
}

獲取鎖,若獲取不成功則訂閱釋放鎖的消息,在收到釋放鎖的消息前阻塞,收到釋放鎖的消息后再去循環(huán)獲取鎖。 

代碼調(diào)用流程

lock()          //org.redisson.RedissonLock.java#lock
    lock(-1, null, false)
        tryAcquire(-1, leaseTime, unit, threadId)
            tryAcquireAsync(waitTime, leaseTime, unit, threadId)
                tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);  //這是最重要的方法

lock(-1, null, false)

總結(jié):獲取鎖,若獲取不成功則訂閱釋放鎖的消息,在收到釋放鎖的消息前阻塞,收到釋放鎖的消息后再去循環(huán)獲取鎖。

private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
		// 獲取鎖
        Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
        // 獲取成功
        if (ttl == null) {
            return;
        }
		// 異步訂閱redis channel
        RFuture<RedissonLockEntry> future = subscribe(threadId);
        if (interruptibly) {
            commandExecutor.syncSubscriptionInterrupted(future);
        } else {
            commandExecutor.syncSubscription(future);
        }
        try {
            while (true) {
                ttl = tryAcquire(-1, leaseTime, unit, threadId);
                // lock acquired
                if (ttl == null) {
                    break;
                }
                // waiting for message
                if (ttl >= 0) {
                    try {
                        future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    } catch (InterruptedException e) {
                        if (interruptibly) {
                            throw e;
                        }
                        future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    }
                } else {
                    if (interruptibly) {
                        future.getNow().getLatch().acquire();
                    } else {
                        future.getNow().getLatch().acquireUninterruptibly();
                    }
                }
            }
        } finally {
			// 取消訂閱
            unsubscribe(future, threadId);
        }
//        get(lockAsync(leaseTime, unit));
    }

tryAcquire(leaseTime, unit, threadId)

private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
    return get(tryAcquireAsync(leaseTime, unit, threadId));// 通過異步獲取鎖,但get(future)實現(xiàn)同步
}

tryAcquireAsync(waitTime, leaseTime, unit, threadId)

總結(jié):用到了Netty的Future-listen模型:給Future一個Promise。 

private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
    if (leaseTime != -1) { //1 如果設(shè)置了超時時間,直接調(diào)用 tryLockInnerAsync
        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
    //2 如果leaseTime==-1,則默認超時時間為30s
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(LOCK_EXPIRATION_INTERVAL_SECONDS, TimeUnit.SECONDS, threadId, RedisCommands.EVAL_LONG);
    //3 監(jiān)聽Future,獲取Future返回值ttlRemaining(剩余超時時間),獲取鎖成功,但是ttlRemaining,則刷新過期時間
    ttlRemainingFuture.addListener(new FutureListener<Long>() {
        @Override
        public void operationComplete(Future<Long> future) throws Exception {
            if (!future.isSuccess()) {
                return;
            }
            Long ttlRemaining = future.getNow();
            // lock acquired
            if (ttlRemaining == null) {
                scheduleExpirationRenewal(threadId);
            }
        }
    });
    return ttlRemainingFuture;
}

tryLockInnerAsync

<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    internalLockLeaseTime = unit.toMillis(leaseTime);
    return commandExecutor.evalWriteAsync(
        getName(),
        LongCodec.INSTANCE,
        command,
          "if (redis.call('exists', KEYS[1]) == 0) then " +
              "redis.call('hset', KEYS[1], ARGV[2], 1); " +
              "redis.call('pexpire', KEYS[1], ARGV[1]); " +
              "return nil; " +
          "end; " +
          "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
              "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
              "redis.call('pexpire', KEYS[1], ARGV[1]); " +
              "return nil; " +
          "end; " +
          "return redis.call('pttl', KEYS[1]);",
        Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}

腳本入?yún)?/h3>
參數(shù)示例值含義
KEY個數(shù)1KEY個數(shù)。為了后面可重入做的計數(shù)統(tǒng)計。
KEYS[1]“myLock”加鎖的 key 名字
ARGV[1]60000持有鎖的有效時間:毫秒。默認 30 秒
ARGV[2]285475da-9152-4c83-822a-67ee2f116a79:52唯一標識:Redisson客戶端ID(UUID)+線程ID

看一下在 Redis 中的存儲結(jié)構(gòu):

127.0.0.1:6379> HGETALL myLock
1) "285475da-9152-4c83-822a-67ee2f116a79:52"
2) "1"

腳本解讀

-- 若鎖不存在:新增鎖,設(shè)置鎖重入計數(shù)為1,設(shè)置鎖過期時間,返回
if (redis.call('exists', KEYS[1]) == 0) then
    redis.call('hset', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;
-- 若鎖存在,且唯一標識也匹配:表明當前加鎖請求為鎖重入請求,故鎖重入計數(shù)+1,再次設(shè)置鎖過期時間,返回
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;
-- 若鎖存在,但唯一標識不匹配:表明鎖是被其他線程占用,當前線程無權(quán)解他人的鎖,直接返回鎖剩余過期時間
return redis.call('pttl', KEYS[1]);

互斥

如果客戶端 2 來嘗試加鎖,會如何呢?

首先,第一個 if 判斷會執(zhí)行exists myLock,發(fā)現(xiàn) myLock 這個鎖 key 已經(jīng)存在了。接著第二個 if 判斷,判斷一下,myLock 鎖 key 的 hash 數(shù)據(jù)結(jié)構(gòu)中,是否包含客戶端 2 的 ID,這里明顯不是,因為那里包含的是客戶端 1 的 ID。所以,客戶端 2 會執(zhí)行:

return redis.call('pttl', KEYS[1]);

返回的一個數(shù)字,這個數(shù)字代表了 myLock 這個鎖 key 的剩余生存時間。

看一下 Redissson tryLock 的主流程:

@Override
    public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        long time = unit.toMillis(waitTime);
        long current = System.currentTimeMillis();
        long threadId = Thread.currentThread().getId();
        // 1.嘗試獲取鎖
        Long ttl = tryAcquire(leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) {
            return true;
        }
        // 申請鎖的耗時如果大于等于最大等待時間,則申請鎖失敗.
        time -= System.currentTimeMillis() - current;
        if (time <= 0) {
            acquireFailed(threadId);
            return false;
        }
        current = System.currentTimeMillis();
        /**
         * 2.訂閱鎖釋放事件,并通過 await 方法阻塞等待鎖釋放,有效的解決了無效的鎖申請浪費資源的問題:
         * 基于信息量,當鎖被其它資源占用時,當前線程通過 Redis 的 channel 訂閱鎖的釋放事件,
         * 一旦鎖釋放會發(fā)消息通知待等待的線程進行競爭.
         *
         * 當 this.await 返回 false,說明等待時間已經(jīng)超出獲取鎖最大等待時間,取消訂閱并返回獲取鎖失敗.
         * 當 this.await 返回 true,進入循環(huán)嘗試獲取鎖.
         */
        RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
        // await 方法內(nèi)部是用 CountDownLatch 來實現(xiàn)阻塞,獲取 subscribe 異步執(zhí)行的結(jié)果(應(yīng)用了 Netty 的 Future)
        if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
            if (!subscribeFuture.cancel(false)) {
                subscribeFuture.onComplete((res, e) -> {
                    if (e == null) {
                        unsubscribe(subscribeFuture, threadId);
                    }
                });
            }
            acquireFailed(threadId);
            return false;
        }
        try {
            // 計算獲取鎖的總耗時,如果大于等于最大等待時間,則獲取鎖失敗.
            time -= System.currentTimeMillis() - current;
            if (time <= 0) {
                acquireFailed(threadId);
                return false;
              }
            /**
             * 3.收到鎖釋放的信號后,在最大等待時間之內(nèi),循環(huán)一次接著一次的嘗試獲取鎖
             * 獲取鎖成功,則立馬返回 true,
             * 若在最大等待時間之內(nèi)還沒獲取到鎖,則認為獲取鎖失敗,返回 false 結(jié)束循環(huán)
             */
            while (true) {
                long currentTime = System.currentTimeMillis();
                // 4.再次嘗試獲取鎖
                ttl = tryAcquire(leaseTime, unit, threadId);
                // lock acquired
                if (ttl == null) {
                    return true;
                }
                // 5.超過最大等待時間則返回 false 結(jié)束循環(huán),獲取鎖失敗
                time -= System.currentTimeMillis() - currentTime;
                if (time <= 0) {
                    acquireFailed(threadId);
                    return false;
                }
                /**
                 * 6.阻塞等待鎖(通過信號量(共享鎖)阻塞,等待解鎖消息):
                 */
                currentTime = System.currentTimeMillis();
                if (ttl >= 0 && ttl < time) {
                    //如果剩余時間(ttl)小于wait time ,就在 ttl 時間內(nèi),從Entry的信號量獲取
                    //一個許可(除非被中斷或者一直沒有可用的許可)。
                    getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                    //則就在wait time 時間范圍內(nèi)等待可以通過信號量
                    getEntry(threadId).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
                }
                // 更新剩余的等待時間(最大等待時間-已經(jīng)消耗的阻塞時間)
                time -= System.currentTimeMillis() - currentTime;
                if (time <= 0) {
                    acquireFailed(threadId);
                    return false;
                }
            }
        } finally {
            // 7.無論是否獲得鎖,都要取消訂閱解鎖消息
            unsubscribe(subscribeFuture, threadId);
        }
//        return get(tryLockAsync(waitTime, leaseTime, unit));
    }

流程分析

  1. 嘗試獲取鎖。若返回 null 則說明加鎖成功;若返回一個數(shù)值,則說明已經(jīng)存在該鎖,ttl 為鎖的剩余存活時間。
  2. 如果此時客戶端 2 進程獲取鎖失敗,那么使用客戶端 2 的線程 id(其實本質(zhì)上就是進程 id)通過 Redis 的 channel 訂閱鎖釋放的事件,。如果等待的過程中一直未等到鎖的釋放事件通知,當超過最大等待時間則獲取鎖失敗,返回 false,也就是第 39 行代碼。如果等到了鎖的釋放事件的通知,則開始進入一個不斷重試獲取鎖的循環(huán)。
  3. 循環(huán)中每次都先試著獲取鎖,并得到已存在的鎖的剩余存活時間。如果在重試中拿到了鎖,則直接返回。如果鎖當前還是被占用的,那么等待釋放鎖的消息,具體實現(xiàn)使用了 JDK 的信號量 Semaphore 來阻塞線程,當鎖釋放并發(fā)布釋放鎖的消息后,信號量的release()方法會被調(diào)用,此時被信號量阻塞的等待隊列中的一個線程就可以繼續(xù)嘗試獲取鎖了。

特別注意

以上過程存在一個細節(jié),這里有必要說明一下,也是分布式鎖的一個關(guān)鍵點:當鎖正在被占用時,等待獲取鎖的進程并不是通過一個 while(true) 死循環(huán)去獲取鎖,而是利用了 Redis 的發(fā)布訂閱機制,通過 await 方法阻塞等待鎖的進程,有效的解決了無效的鎖申請浪費資源的問題。

續(xù)期

客戶端 1 加鎖的鎖 key 默認生存時間才 30 秒,如果超過了 30 秒,客戶端 1 還想一直持有這把鎖,怎么辦呢?

Redisson 提供了一個續(xù)期機制, 只要客戶端 1 一旦加鎖成功,就會啟動一個 Watch Dog??梢赃@樣來使用看門狗(leaseTime不設(shè)置,或者設(shè)置為-1)

lock.tryLock()
lock.tryLock(xxx, -1, xxx)

源碼 

org.redisson.RedissonLock#tryAcquireAsync 

private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
    if (leaseTime != -1) {
        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        if (e != null) {
            return;
        }
        // lock acquired
        if (ttlRemaining == null) {
            scheduleExpirationRenewal(threadId);
        }
    });
    return ttlRemainingFuture;
}

注意:從以上源碼我們看到 leaseTime 必須是 -1 才會開啟 Watch Dog 機制,也就是如果你想開啟 Watch Dog 機制必須使用默認的加鎖時間為 30s。如果你自己自定義時間,超過這個時間,鎖就會自己釋放,并不會延長。

private void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();
    ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
    if (oldEntry != null) {
        oldEntry.addThreadId(threadId);
    } else {
        entry.addThreadId(threadId);
        renewExpiration();
    }
}
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                "return 1; " +
            "end; " +
            "return 0;",
        Collections.<Object>singletonList(getName()),
        internalLockLeaseTime, getLockName(threadId));
}

Watch Dog 機制其實就是一個后臺定時任務(wù)線程。獲取鎖成功之后,會將持有鎖的線程放入到一個 RedissonLock.EXPIRATION_RENEWAL_MAP里面,然后每隔 10 秒(internalLockLeaseTime / 3) 檢查一下,如果客戶端 1 還持有鎖 key(判斷客戶端是否還持有 key,其實就是遍歷 EXPIRATION_RENEWAL_MAP 里面線程 id 然后根據(jù)線程 id 去 Redis 中查,如果存在就會延長 key 的時間),那么就會不斷的延長鎖 key 的生存時間。

注意:這里有一個細節(jié):如果服務(wù)宕機了,Watch Dog 機制的線程也就沒有了,此時就不會延長 key 的過期時間,到了 30s 之后就會自動過期了,其他線程就可以獲取到鎖。

可重入

Redisson 也是支持可重入鎖的,比如下面這種代碼:

@Override
public void lock() {
    RLock lock = redissonSingle.getLock("myLock");
    try {
        lock.lock();
        // 執(zhí)行業(yè)務(wù)
        doBusiness();
        lock.lock();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        // 釋放鎖
        lock.unlock();
        lock.unlock();
        logger.info("任務(wù)執(zhí)行完畢, 釋放鎖!");
    }
}

我們再分析一下加鎖那段 lua 代碼:

if (redis.call('exists', KEYS[1]) == 0) then " +
   "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
   "redis.call('pexpire', KEYS[1], ARGV[1]); " +
   "return nil; " +
   "end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
    "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
    "return nil; " +
    "end; " +
"return redis.call('pttl', KEYS[1]);"

第一個 if 判斷肯定不成立,exists myLock 會顯示鎖 key 已經(jīng)存在。第二個 if 判斷會成立,因為 myLock 的 hash 數(shù)據(jù)結(jié)構(gòu)中包含的那個 ID 即客戶端 1 的 ID,此時就會執(zhí)行可重入加鎖的邏輯,使用:hincrby myLock 285475da-9152-4c83-822a-67ee2f116a79:52 1對客戶端 1 的加鎖次數(shù)加 1。此時 myLock 數(shù)據(jù)結(jié)構(gòu)變?yōu)橄旅孢@樣:

127.0.0.1:6379> HGETALL myLock
1) "285475da-9152-4c83-822a-67ee2f116a79:52"
2) "2"

到這里,小伙伴本就都明白了 hash 結(jié)構(gòu)的 key 是鎖的名稱,field 是客戶端 ID,value 是該客戶端加鎖的次數(shù)。

釋放

鎖的釋放利用了Redis的訂閱功能。

釋放鎖的步驟主要分三步:

  1. 刪除鎖(這里注意可重入鎖,在上面的腳本中有詳細分析)。
  2. 廣播釋放鎖的消息,通知阻塞等待的進程(向通道名為 redisson_lock__channel publish 一條 UNLOCK_MESSAGE 信息)。
  3. 取消 Watch Dog 機制,即將 RedissonLock.EXPIRATION_RENEWAL_MAP 里面的線程 id 刪除,并且 cancel 掉 Netty 的那個定時任務(wù)線程。

源碼

執(zhí)行

lock.unlock()

 就可以釋放分布式鎖。我們來看一下釋放鎖的流程代碼:

@Override
public RFuture<Void> unlockAsync(long threadId) {
    RPromise<Void> result = new RedissonPromise<Void>();
    // 1. 異步釋放鎖
    RFuture<Boolean> future = unlockInnerAsync(threadId);
    // 取消 Watch Dog 機制
    future.onComplete((opStatus, e) -> {
        cancelExpirationRenewal(threadId);
        if (e != null) {
            result.tryFailure(e);
            return;
        }
        if (opStatus == null) {
            IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                    + id + " thread-id: " + threadId);
            result.tryFailure(cause);
            return;
        }
        result.trySuccess(null);
    });
    return result;
}
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            // 判斷鎖 key 是否存在
            "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                "return nil;" +
            "end; " +
            // 將該客戶端對應(yīng)的鎖的 hash 結(jié)構(gòu)的 value 值遞減為 0 后再進行刪除
            // 然后再向通道名為 redisson_lock__channel publish 一條 UNLOCK_MESSAGE 信息
            "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
            "if (counter > 0) then " +
                "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                "return 0; " +
            "else " +
                "redis.call('del', KEYS[1]); " +
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                "return 1; "+
            "end; " +
            "return nil;",
            Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}

總結(jié)

以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • Spring通過攔截器實現(xiàn)多數(shù)據(jù)源切換的示例代碼

    Spring通過攔截器實現(xiàn)多數(shù)據(jù)源切換的示例代碼

    本文主要介紹了Spring攔截器實現(xiàn)多數(shù)據(jù)源動態(tài)切換,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習或者工作具有一定的參考學(xué)習價值,需要的朋友們下面隨著小編來一起學(xué)習學(xué)習吧
    2025-08-08
  • Java前后端分離的在線點餐系統(tǒng)實現(xiàn)詳解

    Java前后端分離的在線點餐系統(tǒng)實現(xiàn)詳解

    這是一個基于SpringBoot+Vue框架開發(fā)的在線點餐系統(tǒng)。首先,這是一個前后端分離的項目。具有一個在線點餐系統(tǒng)該有的所有功能,感興趣的朋友快來看看吧
    2022-01-01
  • Springboot集成restTemplate過程詳解

    Springboot集成restTemplate過程詳解

    這篇文章主要介紹了Springboot集成restTemplate過程詳解,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習或者工作具有一定的參考學(xué)習價值,需要的朋友可以參考下
    2020-04-04
  • 通過IEAD+Maven快速搭建SSM項目的過程(Spring + Spring MVC + Mybatis)

    通過IEAD+Maven快速搭建SSM項目的過程(Spring + Spring MVC + Mybatis)

    這篇文章主要介紹了通過IEAD+Maven快速搭建SSM項目的過程(Spring + Spring MVC + Mybatis),本文給大家介紹的非常詳細,對大家的學(xué)習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2021-01-01
  • 學(xué)習spring事務(wù)與消息隊列

    學(xué)習spring事務(wù)與消息隊列

    這篇文章主要為大家詳細介紹了spring事務(wù)與消息隊列,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2016-10-10
  • Maven 配置中的 <mirror>繞過 HTTP 阻斷機制的方法

    Maven 配置中的 <mirror>繞過 HTTP 阻斷機制的方法

    這篇文章主要介紹了Maven 配置中的 <mirror>繞過 HTTP 阻斷機制的方法,本文給大家分享問題原因及解決方案,感興趣的朋友一起看看吧
    2025-06-06
  • Java Optional<Foo>轉(zhuǎn)換成List<Bar>的實例方法

    Java Optional<Foo>轉(zhuǎn)換成List<Bar>的實例方法

    在本篇內(nèi)容里小編給大家整理的是一篇關(guān)于Java Optional<Foo>轉(zhuǎn)換成List<Bar>的實例方法,有需要的朋友們可以跟著學(xué)習下。
    2021-06-06
  • Java中2個對象字段值比較是否相同

    Java中2個對象字段值比較是否相同

    本文主要介紹了Java中2個對象字段值比較是否相同,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習或者工作具有一定的參考學(xué)習價值,需要的朋友們下面隨著小編來一起學(xué)習學(xué)習吧
    2022-04-04
  • 在SpringBoot項目中利用maven的generate插件

    在SpringBoot項目中利用maven的generate插件

    今天小編就為大家分享一篇關(guān)于在SpringBoot項目中利用maven的generate插件,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧
    2019-01-01
  • 基于SpringBoot實現(xiàn)七天免登錄的完整流程

    基于SpringBoot實現(xiàn)七天免登錄的完整流程

    作為一名Java后端高級開發(fā),我敢說七天免登錄是業(yè)務(wù)系統(tǒng)里最常見的需求之一,這個需求看似簡單,但實現(xiàn)不好很容易踩坑:要么免登錄失效影響用戶體驗,要么出現(xiàn)安全漏洞導(dǎo)致賬號被盜,今天這篇文章,我就結(jié)合實際工作經(jīng)驗,講透七天免登錄的標準實現(xiàn)方案
    2026-01-01

最新評論

武穴市| 甘肃省| 安多县| 多伦县| 平和县| 韶关市| 曲靖市| 白朗县| 九寨沟县| 绥芬河市| 安丘市| 资中县| 司法| 朝阳市| 东方市| 榕江县| 宣汉县| 垫江县| 环江| 岐山县| 高青县| 灌南县| 庆元县| 易门县| 秭归县| 通州市| 台中市| 甘谷县| 监利县| 东平县| 辉南县| 晋州市| 安仁县| 山西省| 灵丘县| 舒城县| 二手房| 武威市| 罗平县| 桃源县| 府谷县|