Spring Boot高可用限流三種實(shí)現(xiàn)解決方案
1.什么是限流
限流是對(duì)某一時(shí)間窗口內(nèi)的請(qǐng)求數(shù)進(jìn)行限制,保持系統(tǒng)的可用性和穩(wěn)定性,防止因流量暴增而導(dǎo)致的系統(tǒng)運(yùn)行緩慢或宕機(jī)。
為什么需要限流
其實(shí)限流思想在生活中隨處可見(jiàn),例如景區(qū)限流,防止人滿(mǎn)為患。熱門(mén)餐飲需要排隊(duì)就餐等。回到互聯(lián)網(wǎng)網(wǎng)絡(luò)上,同樣也是這個(gè)道理,例如某某明星公布了戀情,訪(fǎng)問(wèn)從平時(shí)的50萬(wàn)增加到了500萬(wàn),系統(tǒng)最多可以支撐200萬(wàn)訪(fǎng)問(wèn),那么就要執(zhí)行限流規(guī)則,保證系統(tǒng)是一個(gè)可用的狀態(tài),不至于服務(wù)器崩潰導(dǎo)致所有請(qǐng)求不可用。還有12306購(gòu)票系統(tǒng),每年的618,雙11等場(chǎng)景都需要限流來(lái)抗住高并發(fā)的瞬時(shí)流量,保證系統(tǒng)能正常服務(wù),不被沖垮宕機(jī)癱瘓。
2.限流算法
計(jì)數(shù)器算法
計(jì)數(shù)器限流算法是最為簡(jiǎn)單粗暴的解決方案,主要用來(lái)限制總并發(fā)數(shù),比如數(shù)據(jù)庫(kù)連接池大小、線(xiàn)程池大小、接口訪(fǎng)問(wèn)并發(fā)數(shù)等都是使用計(jì)數(shù)器算法。一般我們會(huì)限制一秒鐘能夠通過(guò)的請(qǐng)求數(shù)。比如我們規(guī)定,對(duì)于A接口來(lái)說(shuō),我們1分鐘的訪(fǎng)問(wèn)次數(shù)不能超過(guò)1000個(gè)。那么我們可以這么做:在一開(kāi)始的時(shí)候,我們可以設(shè)置一個(gè)計(jì)數(shù)器counter,每當(dāng)一個(gè)請(qǐng)求過(guò)來(lái)的時(shí)候, counter就加1,如果counter的值大于1000并且該請(qǐng)求與第一個(gè)請(qǐng)求的間隔時(shí)間還在1分鐘之內(nèi),那么說(shuō)明請(qǐng)求數(shù)過(guò)多; 如果該請(qǐng)求與第一個(gè)請(qǐng)求的間隔時(shí)間大于1分鐘,且counter的值還在限流范圍內(nèi),那么就重置 counter。
代碼實(shí)現(xiàn)如下:
/** * 固定窗口時(shí)間算法 * @return */ public class Counter { public long timeStamp = System.currentTimeMillis(); // 當(dāng)前時(shí)間 public int reqCount = 0; // 初始化計(jì)數(shù)器 public final int limit = 1000; // 時(shí)間窗口內(nèi)最大請(qǐng)求數(shù) public final long interval = 1000 * 60; // 時(shí)間窗口ms ? public boolean limit() { long now = System.currentTimeMillis(); if (now < timeStamp + interval) { // 在時(shí)間窗口內(nèi) reqCount++; // 判斷當(dāng)前時(shí)間窗口內(nèi)是否超過(guò)最大請(qǐng)求控制數(shù) return reqCount <= limit; } else { timeStamp = now; // 超時(shí)后重置 reqCount = 1; return true; } } }
但是,這種上面的固定時(shí)間窗口算法有一個(gè)很明顯的臨界問(wèn)題:假設(shè)限流閥值為5個(gè)請(qǐng)求,單位時(shí)間窗口是1s,如果我們?cè)趩挝粫r(shí)間內(nèi)的前0.8-1s和1-1.2s,分別并發(fā)5個(gè)請(qǐng)求。雖然都沒(méi)有超過(guò)閥值,但是如果算0.8-1.2s,則并發(fā)數(shù)高達(dá)10,已經(jīng)超過(guò)單位時(shí)間1s不超過(guò)5閥值的定義啦。
滑動(dòng)窗口限流算法
滑動(dòng)窗口限流解決固定窗口臨界值的問(wèn)題。它將單位時(shí)間周期分為n個(gè)小周期,分別記錄每個(gè)小周期內(nèi)接口的訪(fǎng)問(wèn)次數(shù),并且根據(jù)時(shí)間滑動(dòng)刪除過(guò)期的小周期。如下圖所示:展示了滑動(dòng)窗口算法對(duì)時(shí)間區(qū)間的劃分
假設(shè)單位時(shí)間還是1s,滑動(dòng)窗口算法把它劃分為5個(gè)小周期,也就是滑動(dòng)窗口(單位時(shí)間)被劃分為5個(gè)小格子。每格表示0.2s。每過(guò)0.2s,時(shí)間窗口就會(huì)往右滑動(dòng)一格。然后呢,每個(gè)小周期,都有自己獨(dú)立的計(jì)數(shù)器,如果請(qǐng)求是0.83s到達(dá)的,0.8~1.0s對(duì)應(yīng)的計(jì)數(shù)器就會(huì)加1。
假設(shè)我們1s內(nèi)的限流閥值還是5個(gè)請(qǐng)求,0.81.0s內(nèi)(比如0.9s的時(shí)候)來(lái)了5個(gè)請(qǐng)求,落在黃色格子里。時(shí)間過(guò)了1.0s這個(gè)點(diǎn)之后,又來(lái)5個(gè)請(qǐng)求,落在紫色格子里。如果是固定窗口算法,是不會(huì)被限流的,但是滑動(dòng)窗口的話(huà),每過(guò)一個(gè)小周期,它會(huì)右移一個(gè)小格。過(guò)了1.0s這個(gè)點(diǎn)后,會(huì)右移一小格,當(dāng)前的單位時(shí)間段是0.21.2s,這個(gè)區(qū)域的請(qǐng)求已經(jīng)超過(guò)限定的5了,已觸發(fā)限流啦,實(shí)際上,紫色格子的請(qǐng)求都被拒絕啦。
TIPS: 當(dāng)滑動(dòng)窗口的格子周期劃分的越多,那么滑動(dòng)窗口的滾動(dòng)就越平滑,限流的統(tǒng)計(jì)就會(huì)越精確。
/** * 單位時(shí)間劃分的小周期(單位時(shí)間是1分鐘,10s一個(gè)小格子窗口,一共6個(gè)格子) */ private int SUB_CYCLE = 10; /** * 每分鐘限流請(qǐng)求數(shù) */ private int thresholdPerMin = 100; /** * 計(jì)數(shù)器, k-為當(dāng)前窗口的開(kāi)始時(shí)間值秒,value為當(dāng)前窗口的計(jì)數(shù) */ private final TreeMap<Long, Integer> counters = new TreeMap<>(); /** * 滑動(dòng)窗口時(shí)間算法實(shí)現(xiàn) */ boolean slidingWindowsTryAcquire() { long currentWindowTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) / SUB_CYCLE * SUB_CYCLE; //獲取當(dāng)前時(shí)間在哪個(gè)小周期窗口 int currentWindowNum = countCurrentWindow(currentWindowTime); //當(dāng)前窗口總請(qǐng)求數(shù) //超過(guò)閥值限流 if (currentWindowNum >= thresholdPerMin) { return false; } //計(jì)數(shù)器+1 counters.get(currentWindowTime)++; return true; } /** * 統(tǒng)計(jì)當(dāng)前窗口的請(qǐng)求數(shù) */ private int countCurrentWindow(long currentWindowTime) { //計(jì)算窗口開(kāi)始位置 long startTime = currentWindowTime - SUB_CYCLE* (60s/SUB_CYCLE-1); int count = 0; //遍歷存儲(chǔ)的計(jì)數(shù)器 Iterator<Map.Entry<Long, Integer>> iterator = counters.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<Long, Integer> entry = iterator.next(); // 刪除無(wú)效過(guò)期的子窗口計(jì)數(shù)器 if (entry.getKey() < startTime) { iterator.remove(); } else { //累加當(dāng)前窗口的所有計(jì)數(shù)器之和 count =count + entry.getValue(); } } return count; }
滑動(dòng)窗口算法雖然解決了固定窗口的臨界問(wèn)題,但是一旦到達(dá)限流后,請(qǐng)求都會(huì)直接暴力被拒絕
漏桶算法
漏桶算法思路很簡(jiǎn)單,我們把水比作是請(qǐng)求,漏桶比作是系統(tǒng)處理能力極限,水先進(jìn)入到漏桶里,漏桶里的水按一定速率流出,當(dāng)流出的速率小于流入的速率時(shí),由于漏桶容量有限,后續(xù)進(jìn)入的水直接溢出(拒絕請(qǐng)求),以此實(shí)現(xiàn)限流。
令牌桶算法
令牌桶算法則是一個(gè)存放固定容量令牌的桶,按照固定速率往桶里添加令牌。桶中存放的令牌數(shù)有最大上限,超出之后就被丟棄或者拒絕。當(dāng)流量或者網(wǎng)絡(luò)請(qǐng)求到達(dá)時(shí),每個(gè)請(qǐng)求都要獲取一個(gè)令牌,如果能夠獲取到,則直接處理,并且令牌桶刪除一個(gè)令牌。如果獲取不同,該請(qǐng)求就要被限流,要么直接丟棄,要么在緩沖區(qū)等待。
令牌桶和漏桶算法區(qū)別:
1)令牌桶是按照固定速率往桶中添加令牌,請(qǐng)求是否被處理需要看桶中令牌是否足夠,當(dāng)令牌數(shù)減為零時(shí)則拒絕新的請(qǐng)求;漏桶則是按照常量固定速率流出請(qǐng)求,流入請(qǐng)求速率任意,當(dāng)流入的請(qǐng)求數(shù)累積到漏桶容量時(shí),則新流入的請(qǐng)求被拒絕;
2)令牌桶限制的是平均流入速率,允許突發(fā)請(qǐng)求,只要有令牌就可以處理,支持一次拿3個(gè)令牌,4個(gè)令牌;漏桶限制的是常量流出速率,即流出速率是一個(gè)固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2,從而平滑突發(fā)流入速率;
3)令牌桶允許一定程度的突發(fā),而漏桶主要目的是平滑流出速率;
3.guava實(shí)現(xiàn)限流
Google開(kāi)源工具包Guava提供了限流工具類(lèi)RateLimiter,該類(lèi)基于令牌桶算法實(shí)現(xiàn)流量限制,使用十分方便,而且十分高效。RateLimiter提供了令牌桶算法實(shí)現(xiàn):平滑突發(fā)限流(SmoothBursty)和平滑預(yù)熱限流(SmoothWarmingUp)。
依賴(lài)包
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>29.0-jre</version> </dependency>
使用示例
/** * @author fjzheng * @version 1.0 * @date 2021/9/1 10:38 */ @Slf4j @RestController @RequestMapping("/test") @ResponseResultBody public class TestController { /** * 限流策略 :1秒鐘2個(gè)請(qǐng)求 */ private final RateLimiter limiter = RateLimiter.create(2.0); private DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @GetMapping("/rateLimit") public String testLimiter() { //500毫秒內(nèi),沒(méi)拿到令牌,就直接進(jìn)入服務(wù)降級(jí) boolean tryAcquire = limiter.tryAcquire(50, TimeUnit.MILLISECONDS); if (!tryAcquire) { log.warn("進(jìn)入服務(wù)降級(jí),時(shí)間{}", LocalDateTime.now().format(dtf)); return "當(dāng)前排隊(duì)人數(shù)較多,請(qǐng)稍后再試!"; } ? log.info("獲取令牌成功,時(shí)間{}", LocalDateTime.now().format(dtf)); return "請(qǐng)求成功"; } ? }
以上用到了RateLimiter的2個(gè)核心方法:create()、tryAcquire(),以下為詳細(xì)說(shuō)明
- acquire() 獲取一個(gè)令牌, 該方法會(huì)阻塞直到獲取到這一個(gè)令牌, 返回值為獲取到這個(gè)令牌花費(fèi)的時(shí)間
- acquire(int permits) 獲取指定數(shù)量的令牌, 該方法也會(huì)阻塞, 返回值為獲取到這 permits 個(gè)令牌花費(fèi)的時(shí)間
- tryAcquire() 判斷時(shí)候能獲取到令牌, 如果不能獲取立即返回 false
- tryAcquire(int permits) 獲取指定數(shù)量的令牌, 如果不能獲取立即返回 false
- tryAcquire(long timeout, TimeUnit unit) 判斷能否在指定時(shí)間內(nèi)獲取到令牌, 如果不能獲取立即返回 false
- tryAcquire(int permits, long timeout, TimeUnit unit) 判斷能否在指定時(shí)間內(nèi)獲取到permits個(gè)令牌,如果不能則返回false。
這時(shí)候調(diào)用上面的測(cè)試接口,一秒鐘只能通過(guò)2個(gè)接口,同時(shí)每隔0.5s產(chǎn)生一個(gè)令牌,調(diào)用過(guò)于頻繁賊會(huì)被限制。但是上面的使用方式不夠優(yōu)雅,因?yàn)槲覀冃枰诿總€(gè)需要限流的接口重復(fù)使用tryAcquire()方法,然后根據(jù)是否獲取到令牌做邏輯判斷
優(yōu)雅使用
所謂優(yōu)雅就是統(tǒng)一處理,使用aop實(shí)現(xiàn)一個(gè)攔截器即可,如下所示:
首先我們的創(chuàng)建一個(gè)注解,該注解可以配置限流信息
/** * @author fjzheng * @version 1.0 * @date 2021/10/25 00:06 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) @Documented public @interface RateLimit { /** * 資源的key,唯一 * 作用:不同的接口,不同的流量控制 */ String key() default ""; ? /** * 最多的訪(fǎng)問(wèn)限制次數(shù) */ double permitsPerSecond () ; ? /** * 獲取令牌最大等待時(shí)間 */ long timeout(); ? /** * 獲取令牌最大等待時(shí)間,單位(例:分鐘/秒/毫秒) 默認(rèn):毫秒 */ TimeUnit timeunit() default TimeUnit.MILLISECONDS; ? /** * 得不到令牌的提示語(yǔ) */ String msg() default "系統(tǒng)繁忙,請(qǐng)稍后再試."; ? }
然后使用aop攔截帶有RateLimit注解的方法,進(jìn)行統(tǒng)一處理即可。
/** * @author fjzheng * @version 1.0 * @date 2021/10/25 00:10 */ @Slf4j @Component @Aspect public class RateLimitAop { /** * 不同的接口,不同的流量控制 * map的key為 Limiter.key */ private final Map<String, RateLimiter> limitMap = Maps.newConcurrentMap(); ? @Around("@annotation(com.shepherd.mall.seckill.annotation.RateLimit)") public Object around(ProceedingJoinPoint joinPoint) throws Throwable{ MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); //拿limit的注解 RateLimit limit = method.getAnnotation(RateLimit.class); if (limit != null) { //key作用:不同的接口,不同的流量控制 String key=limit.key(); RateLimiter rateLimiter = null; //驗(yàn)證緩存是否有命中key if (!limitMap.containsKey(key)) { // 創(chuàng)建令牌桶 rateLimiter = RateLimiter.create(limit.permitsPerSecond()); limitMap.put(key, rateLimiter); log.info("新建了令牌桶={},容量={}",key,limit.permitsPerSecond()); } rateLimiter = limitMap.get(key); // 拿令牌 boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeunit()); // 拿不到命令,直接返回異常提示 if (!acquire) { log.debug("令牌桶={},獲取令牌失敗",key); this.responseFail(limit.msg()); return null; } } return joinPoint.proceed(); } ? /** * 直接向前端拋出異常 * @param msg 提示信息 */ private void responseFail(String msg) { HttpServletResponse response=((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); ResponseVO<Object> responseVO = ResponseVO.failure(400, msg); WebUtil.writeJson(response, responseVO); } }
優(yōu)雅使用示例
/** * @author fjzheng * @version 1.0 * @date 2021/9/1 10:38 */ @Slf4j @RestController @RequestMapping("/test") @ResponseResultBody public class TestController { @GetMapping("/limit2") @RateLimit(key = "limit2", permitsPerSecond = 1, timeout = 50, timeunit = TimeUnit.MILLISECONDS, msg = "當(dāng)前排隊(duì)人數(shù)較多,請(qǐng)稍后再試!") public String limit2() { log.info("令牌桶l(fā)imit2獲取令牌成功"); return "ok"; } }
其測(cè)試結(jié)果和上面的示例是差不多的。
guava的rateLimit限流只能使用與單機(jī)版,如果是分布式系統(tǒng),部署多個(gè)節(jié)點(diǎn)就不行了
4.分布式限流
分布式限流服務(wù)最關(guān)鍵是將限流服務(wù)做成原子化的,防止redis在分步執(zhí)行命令時(shí)部分成功執(zhí)行部分失敗問(wèn)題導(dǎo)致邏輯錯(cuò)誤。lua腳本可以保證操作的原子性
redis+lua實(shí)現(xiàn)
local key = "rate.limit:" .. KEYS[1] local limit = tonumber(ARGV[1]) local expire_time = ARGV[2] ? local is_exists = redis.call("EXISTS", key) if is_exists == 1 then if redis.call("INCR", key) > limit then return 0 else return 1 end else redis.call("SET", key, 1) redis.call("EXPIRE", key, expire_time) return 1 end
nginx+lua實(shí)現(xiàn)
local locks = require "resty.lock" ? local function acquire() local lock =locks:new("locks") local elapsed, err =lock:lock("limit_key") --互斥鎖 保證原子特性 local limit_counter =ngx.shared.limit_counter --計(jì)數(shù)器 ? local key = "ip:" ..os.time() local limit = 5 --限流大小 local current =limit_counter:get(key) ? if current ~= nil and current + 1> limit then --如果超出限流大小 lock:unlock() return 0 end if current == nil then limit_counter:set(key, 1, 1) --第一次需要設(shè)置過(guò)期時(shí)間,設(shè)置key的值為1, --過(guò)期時(shí)間為1秒 else limit_counter:incr(key, 1) --第二次開(kāi)始加1即可 end lock:unlock() return 1 end ngx.print(acquire())
對(duì)于Nginx接入層限流可以使用Nginx自帶了兩個(gè)模塊:連接數(shù)限流模塊ngx_http_limit_conn_module和漏桶算法實(shí)現(xiàn)的請(qǐng)求限流模塊ngx_http_limit_req_module。
控制并發(fā)數(shù):ngx_http_limit_conn_module
http { include mime.types; default_type application/octet-stream; ? #cache lua_shared_dict dis_cache 128m; ? #限流設(shè)置 limit_req_zone $binary_remote_addr zone=contentRateLimit:10m rate=2r/s; ? #根據(jù)IP地址來(lái)限制,存儲(chǔ)內(nèi)存大小10M limit_conn_zone $binary_remote_addr zone=addr:1m; ? sendfile on; #tcp_nopush on; ? #keepalive_timeout 0; keepalive_timeout 65; ? #gzip on; ? server { listen 80; server_name localhost; location /brand { limit_conn addr 2; proxy_pass http://192.168.211.1:18081; } ? location /update_content { content_by_lua_file /root/lua/update_content.lua; } ? location /read_content { limit_req zone=contentRateLimit burst=4 nodelay; content_by_lua_file /root/lua/read_content.lua; } } }
- limit_conn_zone $binary_remote_addr zone=addr:10m; 表示限制根據(jù)用戶(hù)的IP地址來(lái)顯示,設(shè)置存儲(chǔ)地址為的內(nèi)存大小10M
- limit_conn addr 2; 表示 同一個(gè)地址只允許連接2次。
控制速率:ngx_http_limit_req_module
user root root; worker_processes 1; ? events { worker_connections 1024; } ? http { include mime.types; default_type application/octet-stream; ? #cache lua_shared_dict dis_cache 128m; ? #限流設(shè)置 limit_req_zone $binary_remote_addr zone=contentRateLimit:10m rate=2r/s; ? sendfile on; #tcp_nopush on; ? #keepalive_timeout 0; keepalive_timeout 65; ? #gzip on; ? server { listen 80; server_name localhost; ? location /update_content { content_by_lua_file /root/lua/update_content.lua; } ? location /read_content { limit_req zone=contentRateLimit burst=4 nodelay; content_by_lua_file /root/lua/read_content.lua; } } }
burst 譯為突發(fā)、爆發(fā),表示在超過(guò)設(shè)定的處理速率后能額外處理的請(qǐng)求數(shù),當(dāng) rate=10r/s 時(shí),將1s拆成10份,即每100ms可處理1個(gè)請(qǐng)求。
此處,burst=4 ,若同時(shí)有4個(gè)請(qǐng)求到達(dá),Nginx 會(huì)處理第一個(gè)請(qǐng)求,剩余3個(gè)請(qǐng)求將放入隊(duì)列,然后每隔500ms從隊(duì)列中獲取一個(gè)請(qǐng)求進(jìn)行處理。若請(qǐng)求數(shù)大于4,將拒絕處理多余的請(qǐng)求,直接返回503.
不過(guò),單獨(dú)使用 burst 參數(shù)并不實(shí)用。假設(shè) burst=50 ,rate依然為10r/s,排隊(duì)中的50個(gè)請(qǐng)求雖然每100ms會(huì)處理一個(gè),但第50個(gè)請(qǐng)求卻需要等待 50 * 100ms即 5s,這么長(zhǎng)的處理時(shí)間自然難以接受。
因此,burst 往往結(jié)合 nodelay 一起使用。
到此這篇關(guān)于Spring Boot高可用限流三種實(shí)現(xiàn)解決方案的文章就介紹到這了,更多相關(guān)Spring Boot高可用限流內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 基于SpringBoot+Redis實(shí)現(xiàn)一個(gè)簡(jiǎn)單的限流器
- SpringBoot單機(jī)限流的實(shí)現(xiàn)
- SpringBoot使用Redis對(duì)用戶(hù)IP進(jìn)行接口限流的項(xiàng)目實(shí)踐
- SpringBoot使用Redis對(duì)用戶(hù)IP進(jìn)行接口限流的示例詳解
- SpringBoot Redis用注釋實(shí)現(xiàn)接口限流詳解
- SpringBoot如何使用自定義注解實(shí)現(xiàn)接口限流
- 使用SpringBoot?+?Redis?實(shí)現(xiàn)接口限流的方式
- Springboot+Redis實(shí)現(xiàn)API接口限流的示例代碼
相關(guān)文章
解決springboot文件上傳提示臨時(shí)文件夾不存在問(wèn)題
這篇文章主要介紹了解決springboot文件上傳提示臨時(shí)文件夾不存在問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05Springboot AOP對(duì)指定敏感字段數(shù)據(jù)加密存儲(chǔ)的實(shí)現(xiàn)
本篇文章主要介紹了利用Springboot+AOP對(duì)指定的敏感數(shù)據(jù)進(jìn)行加密存儲(chǔ)以及對(duì)數(shù)據(jù)中加密的數(shù)據(jù)的解密的方法,代碼詳細(xì),具有一定的價(jià)值,感興趣的小伙伴可以了解一下2021-11-11PowerJob的TimingStrategyHandler工作流程源碼解讀
這篇文章主要為大家介紹了PowerJob的TimingStrategyHandler工作流程源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01Java線(xiàn)程狀態(tài)及切換、關(guān)閉線(xiàn)程的正確姿勢(shì)分享
這篇文章主要給大家介紹了關(guān)于Java線(xiàn)程狀態(tài)及切換、關(guān)閉線(xiàn)程的正確姿勢(shì),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者使用Java具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10SpringBoot整合JWT實(shí)戰(zhàn)教程
JWT(JSON?Web?Token)是一種用于身份驗(yàn)證和授權(quán)的開(kāi)放標(biāo)準(zhǔn)(RFC?7519),它使用JSON格式傳輸信息,可以在不同系統(tǒng)之間安全地傳遞數(shù)據(jù),這篇文章主要介紹了SpringBoot整合JWT實(shí)戰(zhàn)教程,需要的朋友可以參考下2023-06-06Springboot項(xiàng)目啟動(dòng)不加載resources目錄下的文件問(wèn)題
這篇文章主要介紹了Springboot項(xiàng)目啟動(dòng)不加載resources目錄下的文件問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08