SpringCloud?分布式鎖的多種實(shí)現(xiàn)
前言
今天跟大家探討一下分布式鎖的設(shè)計(jì)與實(shí)現(xiàn)。
- 分布式鎖概述
- 數(shù)據(jù)庫(kù)分布式鎖
- Redis分布式鎖
- Zookeeper分布式鎖
- 三種分布式鎖對(duì)比
1. 分布式鎖概述
我們的系統(tǒng)都是分布式部署的,日常開發(fā)中,秒殺下單、搶購(gòu)商品等等業(yè)務(wù)場(chǎng)景,為了防?庫(kù)存超賣,都需要用到分布式鎖。
分布式鎖其實(shí)就是,控制分布式系統(tǒng)不同進(jìn)程共同訪問共享資源的一種鎖的實(shí)現(xiàn)。如果不同的系統(tǒng)或同一個(gè)系統(tǒng)的不同主機(jī)之間共享了某個(gè)臨界資源,往往需要互斥來防止彼此干擾,以保證一致性。
業(yè)界流行的分布式鎖實(shí)現(xiàn),一般有這3種方式:
- 基于數(shù)據(jù)庫(kù)實(shí)現(xiàn)的分布式鎖
- 基于Redis實(shí)現(xiàn)的分布式鎖
- 基于Zookeeper實(shí)現(xiàn)的分布式鎖
2. 基于數(shù)據(jù)庫(kù)的分布式鎖
2.1 數(shù)據(jù)庫(kù)悲觀鎖實(shí)現(xiàn)的分布式鎖
可以使用select ... for update
來實(shí)現(xiàn)分布式鎖。我們自己的項(xiàng)目,分布式定時(shí)任務(wù),就使用類似的實(shí)現(xiàn)方案,我給大家來展示個(gè)簡(jiǎn)單版的哈
表結(jié)構(gòu)如下:
CREATE TABLE `t_resource_lock` ( `key_resource` varchar(45) COLLATE utf8_bin NOT NULL DEFAULT '資源主鍵', `status` char(1) COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT 'S,F,P', `lock_flag` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '1是已經(jīng)鎖 0是未鎖', `begin_time` datetime DEFAULT NULL COMMENT '開始時(shí)間', `end_time` datetime DEFAULT NULL COMMENT '結(jié)束時(shí)間', `client_ip` varchar(45) COLLATE utf8_bin NOT NULL DEFAULT '搶到鎖的IP', `time` int(10) unsigned NOT NULL DEFAULT '60' COMMENT '方法生命周期內(nèi)只允許一個(gè)結(jié)點(diǎn)獲取一次鎖,單位:分鐘', PRIMARY KEY (`key_resource`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin
加鎖lock
方法的偽代碼如下:
@Transcational //一定要加事務(wù) public boolean lock(String keyResource,int time){ resourceLock = 'select * from t_resource_lock where key_resource ='#{keySource}' for update'; try{ if(resourceLock==null){ //插入鎖的數(shù)據(jù) resourceLock = new ResourceLock(); resourceLock.setTime(time); resourceLock.setLockFlag(1); //上鎖 resourceLock.setStatus(P); //處理中 resourceLock.setBeginTime(new Date()); int count = "insert into resourceLock"; if(count==1){ //獲取鎖成功 return true; } return false; } }catch(Exception x){ return false; } //沒上鎖并且鎖已經(jīng)超時(shí),即可以獲取鎖成功 if(resourceLock.getLockFlag=='0'&&'S'.equals(resourceLock.getstatus) && new Date()>=resourceLock.addDateTime(resourceLock.getBeginTime(,time)){ resourceLock.setLockFlag(1); //上鎖 resourceLock.setStatus(P); //處理中 resourceLock.setBeginTime(new Date()); //update resourceLock; return true; }else if(new Date()>=resourceLock.addDateTime(resourceLock.getBeginTime(,time)){ //超時(shí)未正常執(zhí)行結(jié)束,獲取鎖失敗 return false; }else{ return false; } }
解鎖unlock
方法的偽代碼如下:
public void unlock(String v,status){ resourceLock.setLockFlag(0); //解鎖 resourceLock.setStatus(status); S:表示成功,F(xiàn)表示失敗 //update resourceLock; return ; }
整體流程:
try{ if(lock(keyResource,time)){ //加鎖 status = process();//你的業(yè)務(wù)邏輯處理。 } } finally{ unlock(keyResource,status); //釋放鎖 }
其實(shí)這個(gè)悲觀鎖實(shí)現(xiàn)的分布式鎖,整體的流程還是比較清晰的。就是先select ... for update
鎖住主鍵key_resource
那個(gè)記錄,如果為空,則可以插入一條記錄,如果已有記錄判斷下狀態(tài)和時(shí)間,是否已經(jīng)超時(shí)。這里需要注意一下哈,必須要加事務(wù)哈。
2.2 數(shù)據(jù)庫(kù)樂觀鎖實(shí)現(xiàn)的分布式鎖
除了悲觀鎖,還可以用樂觀鎖實(shí)現(xiàn)分布式鎖。樂觀鎖,顧名思義,就是很樂觀,每次更新操作,都覺得不會(huì)存在并發(fā)沖突,只有更新失敗后,才重試。它是基于CAS思想實(shí)現(xiàn)的。我以前的公司,扣減余額就是用這種方案。
搞個(gè)version字段,每次更新修改,都會(huì)自增加一,然后去更新余額時(shí),把查出來的那個(gè)版本號(hào),帶上條件去更新,如果是上次那個(gè)版本號(hào),就更新,如果不是,表示別人并發(fā)修改過了,就繼續(xù)重試。
大概流程如下:
查詢版本號(hào)和余額
select version,balance from account where user_id ='666';
假設(shè)查到版本號(hào)是oldVersion=1.
邏輯處理,判斷余額
if(balance<扣減金額){ return; } left_balance = balance - 扣減金額;
進(jìn)行扣減余額
update account set balance = #{left_balance} ,version = version+1 where version = #{oldVersion} and balance>= #{left_balance} and user_id ='666';
大家可以看下這個(gè)流程圖哈:
這種方式適合并發(fā)不高的場(chǎng)景,一般需要設(shè)置一下重試的次數(shù)
3.基于Redis實(shí)現(xiàn)的分布式鎖
Redis分布式鎖一般有以下這幾種實(shí)現(xiàn)方式:
- setnx + expire
- setnx + value值是過期時(shí)間
- set的擴(kuò)展命令(set ex px nx)
- set ex px nx + 校驗(yàn)唯一隨機(jī)值,再刪除
- Redisson
- Redisson + RedLock
3.1 setnx + expire
聊到Redis分布式鎖,很多小伙伴反手就是setnx + expire
,如下:
if(jedis.setnx(key,lock_value) == 1){ //setnx加鎖 expire(key,100); //設(shè)置過期時(shí)間 try { do something //業(yè)務(wù)處理 }catch(){ } finally { jedis.del(key); //釋放鎖 } }
這段代碼是可以加鎖成功,但是你有沒有發(fā)現(xiàn)問題,加鎖操作和設(shè)置超時(shí)時(shí)間是分開的。假設(shè)在執(zhí)行完setnx
加鎖后,正要執(zhí)行expire
設(shè)置過期時(shí)間時(shí),進(jìn)程crash
掉或者要重啟維護(hù)了,那這個(gè)鎖就長(zhǎng)生不老了,別的線程永遠(yuǎn)獲取不到鎖啦,所以分布式鎖不能這么實(shí)現(xiàn)!
3.2 setnx + value值是過期時(shí)間
long expires = System.currentTimeMillis() + expireTime; //系統(tǒng)時(shí)間+設(shè)置的過期時(shí)間 String expiresStr = String.valueOf(expires); // 如果當(dāng)前鎖不存在,返回加鎖成功 if (jedis.setnx(key, expiresStr) == 1) { return true; } // 如果鎖已經(jīng)存在,獲取鎖的過期時(shí)間 String currentValueStr = jedis.get(key); // 如果獲取到的過期時(shí)間,小于系統(tǒng)當(dāng)前時(shí)間,表示已經(jīng)過期 if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) { // 鎖已過期,獲取上一個(gè)鎖的過期時(shí)間,并設(shè)置現(xiàn)在鎖的過期時(shí)間(不了解redis的getSet命令的小伙伴,可以去官網(wǎng)看下哈) String oldValueStr = jedis.getSet(key, expiresStr); if (oldValueStr != null && oldValueStr.equals(currentValueStr)) { // 考慮多線程并發(fā)的情況,只有一個(gè)線程的設(shè)置值和當(dāng)前值相同,它才可以加鎖 return true; } } //其他情況,均返回加鎖失敗 return false; }
日常開發(fā)中,有些小伙伴就是這么實(shí)現(xiàn)分布式鎖的,但是會(huì)有這些缺點(diǎn):
- 過期時(shí)間是客戶端自己生成的,分布式環(huán)境下,每個(gè)客戶端的時(shí)間必須同步。
- 沒有保存持有者的唯一標(biāo)識(shí),可能被別的客戶端釋放/解鎖。
- 鎖過期的時(shí)候,并發(fā)多個(gè)客戶端同時(shí)請(qǐng)求過來,都執(zhí)行了
jedis.getSet()
,最終只能有一個(gè)客戶端加鎖成功,但是該客戶端鎖的過期時(shí)間,可能被別的客戶端覆蓋。
3.3 set的擴(kuò)展命令(set ex px nx)
這個(gè)命令的幾個(gè)參數(shù)分別表示什么意思呢?跟大家復(fù)習(xí)一下:
SET key value [EX seconds] [PX milliseconds] [NX|XX]
- EX second :設(shè)置鍵的過期時(shí)間為
second
秒。 - PX millisecond :設(shè)置鍵的過期時(shí)間為
millisecond
毫秒。 - NX :只在鍵不存在時(shí),才對(duì)鍵進(jìn)行設(shè)置操作。
- XX :只在鍵已經(jīng)存在時(shí),才對(duì)鍵進(jìn)行設(shè)置操作。
if(jedis.set(key, lock_value, "NX", "EX", 100s) == 1){ //加鎖 try { do something //業(yè)務(wù)處理 }catch(){ } finally { jedis.del(key); //釋放鎖 } }
這個(gè)方案可能存在這樣的問題:
- 鎖過期釋放了,業(yè)務(wù)還沒執(zhí)行完。
- 鎖被別的線程誤刪。
有些伙伴可能會(huì)有個(gè)疑問,就是鎖為什么會(huì)被別的線程誤刪呢?假設(shè)并發(fā)多線程場(chǎng)景下,線程A獲得了鎖,但是它沒釋放鎖的話,線程B是獲取不到鎖的,所以按道理它是執(zhí)行不到加鎖下面的代碼滴,怎么會(huì)導(dǎo)致鎖被別的線程誤刪呢?
假設(shè)線程A和B,都想用
key
加鎖,最后A搶到鎖加鎖成功,但是由于執(zhí)行業(yè)務(wù)邏輯的耗時(shí)很長(zhǎng),超過了設(shè)置的超時(shí)時(shí)間100s
。這時(shí)候,Redis就自動(dòng)釋放了key
鎖。這時(shí)候線程B就可以加鎖成功了,接下啦,它也執(zhí)行業(yè)務(wù)邏輯處理。假設(shè)碰巧這時(shí)候,A執(zhí)行完自己的業(yè)務(wù)邏輯,它就去釋放鎖,但是它就把B的鎖給釋放了。
3.4 set ex px nx + 校驗(yàn)唯一隨機(jī)值,再刪除
為了解決鎖被別的線程誤刪問題??梢栽?code>set ex px nx的基礎(chǔ)上,加上個(gè)校驗(yàn)的唯一隨機(jī)值,如下:
if(jedis.set(key, uni_request_id, "NX", "EX", 100s) == 1){ //加鎖 try { do something //業(yè)務(wù)處理 }catch(){ } finally { //判斷是不是當(dāng)前線程加的鎖,是才釋放 if (uni_request_id.equals(jedis.get(key))) { jedis.del(key); //釋放鎖 } } }
在這里,判斷當(dāng)前線程加的鎖和釋放鎖不是一個(gè)原子操作。如果調(diào)用jedis.del()
釋放鎖的時(shí)候,可能這把鎖已經(jīng)不屬于當(dāng)前客戶端,會(huì)解除他人加的鎖。
一般可以用lua腳本來包一下。lua腳本如下:
if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end;
這種方式比較不錯(cuò)了,一般情況下,已經(jīng)可以使用這種實(shí)現(xiàn)方式。但是還是存在:鎖過期釋放了,業(yè)務(wù)還沒執(zhí)行完的問題。
3.5 Redisson
對(duì)于可能存在鎖過期釋放,業(yè)務(wù)沒執(zhí)行完的問題。我們可以稍微把鎖過期時(shí)間設(shè)置長(zhǎng)一些,大于正常業(yè)務(wù)處理時(shí)間就好啦。如果你覺得不是很穩(wěn),還可以給獲得鎖的線程,開啟一個(gè)定時(shí)守護(hù)線程,每隔一段時(shí)間檢查鎖是否還存在,存在則對(duì)鎖的過期時(shí)間延長(zhǎng),防止鎖過期提前釋放。
當(dāng)前開源框架Redisson解決了這個(gè)問題。可以看下Redisson底層原理圖:
只要線程一加鎖成功,就會(huì)啟動(dòng)一個(gè)watch dog
看門狗,它是一個(gè)后臺(tái)線程,會(huì)每隔10秒檢查一下,如果線程1還持有鎖,那么就會(huì)不斷的延長(zhǎng)鎖key的生存時(shí)間。因此,Redisson就是使用watch dog
解決了鎖過期釋放,業(yè)務(wù)沒執(zhí)行完問題。
3.6 Redisson + RedLock
前面六種方案都只是基于Redis單機(jī)版的分布式鎖討論,還不是很完美。因?yàn)?strong>Redis一般都是集群部署的:
如果線程一在Redis
的master
節(jié)點(diǎn)上拿到了鎖,但是加鎖的key
還沒同步到slave
節(jié)點(diǎn)。恰好這時(shí),master
節(jié)點(diǎn)發(fā)生故障,一個(gè)slave
節(jié)點(diǎn)就會(huì)升級(jí)為master
節(jié)點(diǎn)。線程二就可以順理成章獲取同個(gè)key
的鎖啦,但線程一也已經(jīng)拿到鎖了,鎖的安全性就沒了。
為了解決這個(gè)問題,Redis作者antirez提出一種高級(jí)的分布式鎖算法:Redlock。它的核心思想是這樣的:
部署多個(gè)Redis master,以保證它們不會(huì)同時(shí)宕掉。并且這些master節(jié)點(diǎn)是完全相互獨(dú)立的,相互之間不存在數(shù)據(jù)同步。同時(shí),需要確保在這多個(gè)master實(shí)例上,是與在Redis單實(shí)例,使用相同方法來獲取和釋放鎖。
我們假設(shè)當(dāng)前有5個(gè)Redis master節(jié)點(diǎn),在5臺(tái)服務(wù)器上面運(yùn)行這些Redis實(shí)例。
RedLock的實(shí)現(xiàn)步驟:
- 獲取當(dāng)前時(shí)間,以毫秒為單位。
- 按順序向5個(gè)master節(jié)點(diǎn)請(qǐng)求加鎖。客戶端設(shè)置網(wǎng)絡(luò)連接和響應(yīng)超時(shí)時(shí)間,并且超時(shí)時(shí)間要小于鎖的失效時(shí)間。(假設(shè)鎖自動(dòng)失效時(shí)間為10秒,則超時(shí)時(shí)間一般在5-50毫秒之間,我們就假設(shè)超時(shí)時(shí)間是50ms吧)。如果超時(shí),跳過該master節(jié)點(diǎn),盡快去嘗試下一個(gè)master節(jié)點(diǎn)。
- 客戶端使用當(dāng)前時(shí)間減去開始獲取鎖時(shí)間(即步驟1記錄的時(shí)間),得到獲取鎖使用的時(shí)間。當(dāng)且僅當(dāng)超過一半(N/2+1,這里是5/2+1=3個(gè)節(jié)點(diǎn))的Redis master節(jié)點(diǎn)都獲得鎖,并且使用的時(shí)間小于鎖失效時(shí)間時(shí),鎖才算獲取成功。(如上圖,10s> 30ms+40ms+50ms+4m0s+50ms)
- 如果取到了鎖,key的真正有效時(shí)間就變啦,需要減去獲取鎖所使用的時(shí)間。
- 如果獲取鎖失?。]有在至少N/2+1個(gè)master實(shí)例取到鎖,有或者獲取鎖時(shí)間已經(jīng)超過了有效時(shí)間),客戶端要在所有的master節(jié)點(diǎn)上解鎖(即便有些master節(jié)點(diǎn)根本就沒有加鎖成功,也需要解鎖,以防止有些漏網(wǎng)之魚)。
簡(jiǎn)化下步驟就是:
- 按順序向5個(gè)master節(jié)點(diǎn)請(qǐng)求加鎖
- 根據(jù)設(shè)置的超時(shí)時(shí)間來判斷,是不是要跳過該master節(jié)點(diǎn)。
- 如果大于等于3個(gè)節(jié)點(diǎn)加鎖成功,并且使用的時(shí)間小于鎖的有效期,即可認(rèn)定加鎖成功啦。
- 如果獲取鎖失敗,解鎖!
Redisson實(shí)現(xiàn)了redLock版本的鎖,有興趣的小伙伴,可以去了解一下哈~
4. Zookeeper分布式鎖
在學(xué)習(xí)Zookeeper分布式鎖之前,我們復(fù)習(xí)一下Zookeeper的節(jié)點(diǎn)哈。
Zookeeper的節(jié)點(diǎn)Znode有四種類型:
- 持久節(jié)點(diǎn):默認(rèn)的節(jié)點(diǎn)類型。創(chuàng)建節(jié)點(diǎn)的客戶端與zookeeper斷開連接后,該節(jié)點(diǎn)依舊存在。
- 持久節(jié)點(diǎn)順序節(jié)點(diǎn):所謂順序節(jié)點(diǎn),就是在創(chuàng)建節(jié)點(diǎn)時(shí),Zookeeper根據(jù)創(chuàng)建的時(shí)間順序給該節(jié)點(diǎn)名稱進(jìn)行編號(hào),持久節(jié)點(diǎn)順序節(jié)點(diǎn)就是有順序的持久節(jié)點(diǎn)。
- 臨時(shí)節(jié)點(diǎn):和持久節(jié)點(diǎn)相反,當(dāng)創(chuàng)建節(jié)點(diǎn)的客戶端與zookeeper斷開連接后,臨時(shí)節(jié)點(diǎn)會(huì)被刪除。
- 臨時(shí)順序節(jié)點(diǎn):有順序的臨時(shí)節(jié)點(diǎn)。
Zookeeper分布式鎖實(shí)現(xiàn)應(yīng)用了臨時(shí)順序節(jié)點(diǎn)。這里不貼代碼啦,來講下zk分布式鎖的實(shí)現(xiàn)原理吧。
4.1 zk獲取鎖過程
當(dāng)?shù)谝粋€(gè)客戶端請(qǐng)求過來時(shí),Zookeeper客戶端會(huì)創(chuàng)建一個(gè)持久節(jié)點(diǎn)locks
。如果它(Client1)想獲得鎖,需要在locks
節(jié)點(diǎn)下創(chuàng)建一個(gè)順序節(jié)點(diǎn)lock1
.如圖
接著,客戶端Client1會(huì)查找locks
下面的所有臨時(shí)順序子節(jié)點(diǎn),判斷自己的節(jié)點(diǎn)lock1
是不是排序最小的那一個(gè),如果是,則成功獲得鎖。
這時(shí)候如果又來一個(gè)客戶端client2前來嘗試獲得鎖,它會(huì)在locks下再創(chuàng)建一個(gè)臨時(shí)節(jié)點(diǎn)lock2
客戶端client2一樣也會(huì)查找locks下面的所有臨時(shí)順序子節(jié)點(diǎn),判斷自己的節(jié)點(diǎn)lock2是不是最小的,此時(shí),發(fā)現(xiàn)lock1才是最小的,于是獲取鎖失敗。獲取鎖失敗,它是不會(huì)甘心的,client2向它排序靠前的節(jié)點(diǎn)lock1注冊(cè)Watcher事件,用來監(jiān)聽lock1是否存在,也就是說client2搶鎖失敗進(jìn)入等待狀態(tài)。
此時(shí),如果再來一個(gè)客戶端Client3來嘗試獲取鎖,它會(huì)在locks下再創(chuàng)建一個(gè)臨時(shí)節(jié)點(diǎn)lock3
同樣的,client3一樣也會(huì)查找locks下面的所有臨時(shí)順序子節(jié)點(diǎn),判斷自己的節(jié)點(diǎn)lock3是不是最小的,發(fā)現(xiàn)自己不是最小的,就獲取鎖失敗。它也是不會(huì)甘心的,它會(huì)向在它前面的節(jié)點(diǎn)lock2注冊(cè)Watcher事件,以監(jiān)聽lock2節(jié)點(diǎn)是否存在。
4.2 釋放鎖
我們?cè)賮砜纯瘁尫沛i的流程,Zookeeper的客戶端業(yè)務(wù)完成或者發(fā)生故障,都會(huì)刪除臨時(shí)節(jié)點(diǎn),釋放鎖。如果是任務(wù)完成,Client1會(huì)顯式調(diào)用刪除lock1的指令
如果是客戶端故障了,根據(jù)臨時(shí)節(jié)點(diǎn)得特性,lock1是會(huì)自動(dòng)刪除的
lock1節(jié)點(diǎn)被刪除后,Client2可開心了,因?yàn)樗恢北O(jiān)聽著lock1。lock1節(jié)點(diǎn)刪除,Client2立刻收到通知,也會(huì)查找locks下面的所有臨時(shí)順序子節(jié)點(diǎn),發(fā)下lock2是最小,就獲得鎖。
同理,Client2獲得鎖之后,Client3也對(duì)它虎視眈眈,啊哈哈~
- Zookeeper設(shè)計(jì)定位就是分布式協(xié)調(diào),簡(jiǎn)單易用。如果獲取不到鎖,只需添加一個(gè)監(jiān)聽器即可,很適合做分布式鎖。
- Zookeeper作為分布式鎖也缺點(diǎn):如果有很多的客戶端頻繁的申請(qǐng)加鎖、釋放鎖,對(duì)于Zookeeper集群的壓力會(huì)比較大。
5. 三種分布式鎖對(duì)比
5.1 數(shù)據(jù)庫(kù)分布式鎖實(shí)現(xiàn)
優(yōu)點(diǎn):
簡(jiǎn)單,使用方便,不需要引入Redis、zookeeper
等中間件。
缺點(diǎn):
- 不適合高并發(fā)的場(chǎng)景
- db操作性能較差;
5.2 Redis分布式鎖實(shí)現(xiàn)
優(yōu)點(diǎn):
- 性能好,適合高并發(fā)場(chǎng)景
- 較輕量級(jí)
- 有較好的框架支持,如Redisson
缺點(diǎn):
- 過期時(shí)間不好控制
- 需要考慮鎖被別的線程誤刪場(chǎng)景
5.3 Zookeeper分布式鎖實(shí)現(xiàn)
缺點(diǎn):
- 性能不如redis實(shí)現(xiàn)的分布式鎖
- 比較重的分布式鎖。
優(yōu)點(diǎn):
- 有較好的性能和可靠性
- 有封裝較好的框架,如Curator
5.4 對(duì)比匯總
- 從性能角度(從高到低)Redis > Zookeeper >= 數(shù)據(jù)庫(kù);
- 從理解的難易程度角度(從低到高)數(shù)據(jù)庫(kù) > Redis > Zookeeper;
- 從實(shí)現(xiàn)的復(fù)雜性角度(從低到高)Zookeeper > Redis > 數(shù)據(jù)庫(kù);
- 從可靠性角度(從高到低)Zookeeper > Redis > 數(shù)據(jù)庫(kù)。
到此這篇關(guān)于SpringCloud 分布式鎖的多種實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)SpringCloud 分布式鎖 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Netty分布式server啟動(dòng)流程N(yùn)io創(chuàng)建源碼分析
這篇文章主要介紹了Netty分布式server啟動(dòng)流程N(yùn)io創(chuàng)建源碼分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-03-03Java使用Freemarker頁(yè)面靜態(tài)化生成的實(shí)現(xiàn)
這篇文章主要介紹了Java使用Freemarker頁(yè)面靜態(tài)化生成的實(shí)現(xiàn),頁(yè)面靜態(tài)化是將原來的動(dòng)態(tài)網(wǎng)頁(yè)改為通過靜態(tài)化技術(shù)生成的靜態(tài)網(wǎng)頁(yè),FreeMarker?是一個(gè)用?Java?語言編寫的模板引擎,它基于模板來生成文本輸,更多相關(guān)內(nèi)容需要的小伙伴可以參考一下2022-06-06spring boot實(shí)現(xiàn)圖片上傳和下載功能
這篇文章主要為大家詳細(xì)介紹了spring boot實(shí)現(xiàn)圖片上傳和下載功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-02-02關(guān)于log4j日志擴(kuò)展---自定義PatternLayout
這篇文章主要介紹了關(guān)于log4j日志擴(kuò)展---自定義PatternLayout,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12SpringBoot中使用Quartz設(shè)置定時(shí)任務(wù)的實(shí)例詳解
Quartz是OpenSymphony開源組織在任務(wù)調(diào)度領(lǐng)域的一個(gè)開源項(xiàng)目,完全基于 Java 實(shí)現(xiàn),本文小編給大家介紹了SpringBoot中如何使用Quartz設(shè)置定時(shí)任務(wù),文中通過代碼示例給大家講解的非常詳細(xì),需要的朋友可以參考下2023-12-12