SpringBoot?Loki安裝簡介及實戰(zhàn)思路
前言
因為網(wǎng)上好多都沒有通過Loki的API自己實現(xiàn)對日志監(jiān)控系統(tǒng),所以我就下定決心自己出一版關(guān)于loki與springboot的博文供大家參考,這個可以說是比較實用,很適合中小型企業(yè)。因此我醞釀了挺久了,對于loki的研究也比較久,希望各位讀者能有新的收獲。
簡介
Loki是Grafana Labs團隊的開源項目,可以組成一個功能齊全的日志堆棧。Loki是一個水平可擴展,高可用性,多租戶的日志聚合系統(tǒng)。它的設(shè)計非常經(jīng)濟高效且易于操作,因為它不會為日志內(nèi)容編制索引,而是為每個日志流編制一組標簽。Loki是用來存儲日志和處理查詢,需要通過promtail來收集日志,也可是通過后端的logback等日志框架來收集日志,通過grafana提供的loki可視化查看日志,當然了loki也提供了API,可以根據(jù)自己的需求來自己實現(xiàn)可視化界面,能夠減少三方插件的使用。
安裝
上一篇文章已經(jīng)介紹了如何安裝以及使用Grafana+loki+promtail進行搭建日志系統(tǒng),http://www.dbjr.com.cn/article/248318.htm可以看看這篇文章。接下來筆者要介紹的是通過Loki的API編寫自己可視化界面,并且通過logback來實現(xiàn)收集日志。 大致的結(jié)構(gòu)如圖
簡單介紹一下,主要就是通過springboot后端的logback日志框架來收集日志,在推送到loki中存儲,loki執(zhí)行對日志的查詢,通過API根據(jù)標簽等信息去查詢?nèi)罩静⑶以谧远x的前端界面中展示。
整體思路
其實宏觀來看,要達成這個需求說起來是十分簡單的,只需配置logback配置,在通過MDC寫入、收集日志,這里可以好多的寫法,可以是通過反射寫入日志,也可以是在需要打印的地方寫入日志,并且是將日志區(qū)分為不同的標簽。在前端就可以根據(jù)所定義的標簽來查看相應(yīng)的日志。前端獲取日志信息邏輯也很簡單,就只是通過Loki提供的API獲取每行的日志。接下來我就一一詳細的介紹SpringBoot與Loki的那些事。 可以查看此圖便于理解:
Loki實戰(zhàn)開發(fā)
接下來就詳細講解筆者在實戰(zhàn)開發(fā)中是如何編寫的,本次介紹只是對編寫的代碼進行詳講,對于代碼可能不會全部粘貼,不然冗余起來效果不好,各位讀者可以各自發(fā)揮,更加完善。其實整個業(yè)務(wù)也不難,基本都是loki自身提供的API,讀者可以通過Loki官方網(wǎng)站grafana.com/docs/loki/l… 去進一步對Loki的API進行查閱,后面筆者可能也會出一篇來專門對Loki的API以及配置進行介紹。好了,廢話不多說,馬上進入正題。
springboot中的配置
首先需要配置向Loki推送日志,也就是需要通過Loki的API:POST /loki/api/v1/push ,可以直接將地址通過appender寫死在logback日志框架中,但是在項目開發(fā)中,要考慮到環(huán)境的不同,應(yīng)該是能夠根據(jù)需要來修改loki服務(wù)器的地址,因此將loki的服務(wù)器地址配置在application-dev.yml中。
loki: url: http://localhost:3100/loki/api/v1
配置logback日志框架
先獲取yml配置的地址,通過appender添加到日志框架中,當然,配置客戶端也不一定是LogBack框架,還有Log4j2框架也是能夠使用的,具體配置可以看官網(wǎng)github.com/loki4j/loki… 和 github.com/tkowalcz/tj… ,本章只對loki進行講解,對于日志框架,后期也會一一列出,各位讀者有什么不了解的,可以先到網(wǎng)上查閱資料。因為筆者不是部署多臺Loki服務(wù)器,不同的系統(tǒng)采用system這個標簽來進行區(qū)分。
<springProperty scope="context" name="lokiUrl" source="loki.url"/> <property name="LOKI_URL" value="${lokiUrl}"/> <!--添加loki--> <appender name="lokiAppender" class="com.github.loki4j.logback.Loki4jAppender"> <batchTimeoutMs>1000</batchTimeoutMs> <http class="com.github.loki4j.logback.ApacheHttpSender"> <url>${LOKI_URL}/push</url> </http> <format> <label> <pattern>system=${SYSTEM_NAME},level=%level,logType=%X{log_file_type:-logType}</pattern> </label> <message> <pattern>${log.pattern}</pattern> </message> <sortByTime>true</sortByTime> </format> </appender>
注解與切面寫入日志
自定義注解,并且設(shè)置日志標簽值。
/** * @author: lyd * @description: 自定義日志注解,用作LOKI日志分類 * @Date: 2022/10/10 */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD}) @Documented public @interface LokiLog { LokiLogType type() default LokiLogType.DEFAULT; }
通過枚舉的方式來定義日志類型的標簽值
/** * @author: lyd * @description: 枚舉便簽值 - 類型自己定義 * @Date: 2022/10/11 */ public enum LokiLogType { DEFAULT("默認"), A("A"), B("B"), C("C"); private String desc; LokiLogType(String desc) { this.desc=desc; } public String getDesc() { return desc; } }
編寫切面,寫入日志(詳情可以參照這篇文章http://www.dbjr.com.cn/article/230135.htm,內(nèi)部通過MDC.put("log_file_type", logType.getDesc());(MDC ( Mapped Diagnostic Contexts ),它是一個線程安全的存放診斷日志的容器??梢詤⒄?http://www.dbjr.com.cn/article/232986.htm可以理解為log_file_type是標簽名,logType.getDesc()是標簽值。
/** * @author: lyd * @description: 自定義日志切面:https://cloud.tencent.com/developer/article/1655923 * @Date: 2022/10/10 */ @Aspect @Slf4j @Component public class LokiLogAspect { /** * 切到所有OperatorLog注解修飾的方法 */ @Pointcut("@annotation(org.nl.wms.log.LokiLog)") public void operatorLog() { // 空方法 } /** * 利用@Around環(huán)繞增強 * * @return */ @Around("operatorLog()") public synchronized Object around(ProceedingJoinPoint pjp) throws Throwable { // ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); // HttpServletRequest request = attributes.getRequest(); // HttpServletResponse response = attributes.getResponse(); Signature signature = pjp.getSignature(); MethodSignature methodSignature = (MethodSignature) signature; Method method = methodSignature.getMethod(); LokiLog lokiLog = method.getAnnotation(LokiLog.class); // 獲取描述信息 LokiLogType logType = lokiLog.type(); MDC.put("log_file_type", logType.getDesc()); log.info("輸入?yún)?shù):" + JSONObject.toJSONString(pjp.getArgs())); Object proceed = pjp.proceed(); log.info("返回參數(shù):" + JSONObject.toJSONString(proceed)); MDC.remove("log_file_type"); return proceed; } }
使用注解,在方法中引用注解即可
@LokiLog(type = LokiLogType.A)
前端界面與后端接口
前端界面介紹起來可能比較麻煩,畢竟寫的代碼也比較多,這里就選取講解,代碼量比較多,也不會是全部代碼粘貼,樣式之類的,我相信讀者會根據(jù)自己的需求去實現(xiàn),這里主要的是記錄開發(fā)的思路。
日志的初步獲取
前端的界面就如圖,本次是以el-admin這個為基礎(chǔ)制作的demo。
查找日志是需要通過標簽與標簽值來獲取日志信息,因此首先需要的是攜帶標簽對到后端訪問Loki的API拿到數(shù)據(jù),讀者可以查閱官網(wǎng)的API,結(jié)合著學(xué)習(xí)。
一開始當vue視圖渲染的時候,就會從后端獲取loki日志標簽,具體后端接口的業(yè)務(wù)代碼如下:
/** * 獲取labels和values樹 * * @return */ @Override public JSONArray getLabelsValues() { JSONArray result = new JSONArray(); // 獲取所有標簽 String labelString = HttpUtil.get(lokiUrl + "/labels", CharsetUtil.CHARSET_UTF_8); JSONObject parse = (JSONObject) JSONObject.parse(labelString); JSONArray labels = parse.getJSONArray("data"); for (int i=0; i<labels.size(); i++) { // 獲取標簽下的所有值 String valueString = HttpUtil.get(lokiUrl + "/label/" + labels.getString(i) + "/values", CharsetUtil.CHARSET_UTF_8); JSONObject parse2 = (JSONObject) JSONObject.parse(valueString); JSONArray values = parse2.getJSONArray("data"); JSONArray children = new JSONArray(); // 組成樹形狀態(tài) 兩級 for (int j=0; j<values.size(); j++) { JSONObject leaf = new JSONObject(); leaf.put("label", values.getString(j)); leaf.put("value", values.getString(j)); children.add(leaf); } JSONObject node = new JSONObject(); node.put("label", labels.getString(i)); node.put("value", labels.getString(i)); node.put("children", children); result.add(node); } return result; }
核心代碼就只有通過Hutool工具包去訪問API獲取標簽HttpUtil.get(lokiUrl + "/labels", CharsetUtil.CHARSET_UTF_8); 以及 獲取標簽值HttpUtil.get(lokiUrl + "/label/" + labels.getString(i) + "/values", CharsetUtil.CHARSET_UTF_8); 因為我的前端是用elment-ui的樹來接收的,因此我就將返回的數(shù)據(jù)設(shè)計成相應(yīng)的形式。
<el-form-item label="日志標簽"> <el-cascader v-model="labelAndValue" :options="labelsOptions" placeholder="請選擇標簽" @change="queryData" /> </el-form-item>
模糊查找與更多參數(shù)
loki提供了相應(yīng)的API來進行模糊查找日志,無非就是通過loki的API攜帶關(guān)鍵字進行模糊查找日志,筆者的做法是獲取含有關(guān)鍵字的日志內(nèi)容。
"/query_range?query={system=\"" + systemName + "\", " + logLabel + "=\"" + logLabelValue + "\"} |= `" + text + "`"
并且還能夠通過時間段來查詢,筆者實現(xiàn)了的效果如圖
不僅可以通過關(guān)鍵字,還有時間段時間范圍以及查找的方向和一次性顯示的條數(shù),最好是建議不要超過1000條數(shù)據(jù),滾動步數(shù)是實現(xiàn)滾動下拉的時候獲取新的日志數(shù)據(jù)的條目數(shù)。 后端代碼如下,簡單介紹一下,就是提供所需要的查詢條件來對日志進行篩選。不管是獲取日志數(shù)據(jù)還是滾動下拉獲取的日志數(shù)據(jù)都可以通用這個接口,然而主要的參數(shù)設(shè)置可以在前端進行打磨,以下代碼還有優(yōu)化的空間,畢竟當時剛開始寫的時候沒考慮這么多。
@Override public JSONObject getLogData(JSONObject json) { String logLabel = ""; String logLabelValue = ""; Long start = 0L; Long end = 0L; String text = ""; String limit = "100"; String direction = "backward"; if (json.get("logLabel") != null) logLabel = json.getString("logLabel"); if (json.get("logLabelValue") != null) logLabelValue = json.getString("logLabelValue"); if (json.get("text") != null) text = json.getString("text"); if (json.get("start") != null) start = json.getLong("start"); if (json.get("end") != null) end = json.getLong("end"); if (json.get("limits") != null) limit = json.getString("limits"); if (json.get("direction") != null) direction = json.getString("direction"); /** * 組織參數(shù) * 納秒數(shù) * 1660037391880000000 * 1641453208415000000 * http://localhost:3100/loki/api/v1/query_range?query={host="localhost"} |= ``&limit=1500&start=1641453208415000000&end=1660027623419419002 */ JSONObject parse = null; String query = lokiUrl + "/query_range?query={system=\"" + systemName + "\", " + logLabel + "=\"" + logLabelValue + "\"} |= `" + text + "`"; String result = ""; if (start==0L) { result = HttpUtil.get(query + "&limit=" + limit + "&direction=" + direction, CharsetUtil.CHARSET_UTF_8); } else { result = HttpUtil.get(query + "&limit=" + limit + "&start=" + start + "&end=" + end + "&direction=" + direction, CharsetUtil.CHARSET_UTF_8); } try { parse = (JSONObject) JSONObject.parse(result); } catch (Exception e) { // reslut的值可能為:too many outstanding requests,無法轉(zhuǎn)化成Json System.out.println("reslut:" + result); // e.printStackTrace(); } return parse; }
前端的邏輯是比較復(fù)雜的,因為需要做大量的賦值與設(shè)置。 前端js方法代碼,主要是對參數(shù)數(shù)據(jù)的組織,這里需要注意的是,因為loki需要的是納秒級別的時間戳,這里就需要十分注意前端js的精度。還有一點就是,如果后端日志是有顏色標簽的,那么前端直接渲染就會顯示標簽,所以這里需要進行相應(yīng)的處理,就是用過AnsiUp插件進行操作,詳細看此篇文章:http://www.dbjr.com.cn/article/266641.htm
queryData() { console.log(this.labelAndValue) // 清空查詢數(shù)據(jù) this.clearParam() if (this.labelAndValue.length > 0) { queryParam.logLabel = this.labelAndValue[0] queryParam.logLabelValue = this.labelAndValue[1] } if (queryParam.logLabelValue === null) { // 判空 this.$message({ showClose: true, message: '請選擇標簽', type: 'warning' }) this.showEmpty = true this.emptyText = '請選擇標簽' return } if (this.timeRange.length !== 0) { // 如果是輸入時間范圍 queryParam.start = (new Date(this.timeRange[0]).getTime() * 1000000).toString() queryParam.end = (new Date(this.timeRange[1]).getTime() * 1000000).toString() } if (this.timeZoneValue) { const time = new Date() queryParam.start = ((time.getTime() - this.timeZoneValue) * 1000000).toString() queryParam.end = (time.getTime() * 1000000).toString() } if (this.text) { queryParam.text = this.text.replace(/^\s*|\s*$/g, '') // 去空 } if (this.limits) { queryParam.limits = this.limits } queryParam.direction = this.direction var ansi_up = new AnsiUp() logOperation.getLogData(queryParam).then(res => { this.showEmpty = false if (res.data.result.length === 1) { this.logs = res.data.result[0].values for (const i in res.data.result[0].values) { this.logs[i][1] = ansi_up.ansi_to_html(res.data.result[0].values[i][1]) } } else if (res.data.result.length > 1) { // 清空 this.logs = [] for (const j in res.data.result) { // 用push的方式將所有日志數(shù)組添加進去 for (const values_index in res.data.result[j].values) { this.logs.push(res.data.result[j].values[values_index]) } } for (const k in this.logs) { this.logs[k][1] = ansi_up.ansi_to_html(this.logs[k][1]) } if (this.direction === 'backward') { // 由于使用公共標簽會導(dǎo)致時間順序錯亂,因此對二維數(shù)組進行排序 this.logs.sort((a, b) => b[0] - a[0]) } else { this.logs.sort((a, b) => a[0] - b[0]) } } else { this.showEmpty = true this.emptyText = '暫無日志信息,請選擇時間段試試' } }) },
通過AnsiUp插件可以將帶有顏色標簽的日志以顏色展示,代碼如下:
<div style="margin: 3px; min-height: 80vh;"> <!--數(shù)據(jù)判空--> <el-empty v-if="showEmpty" :description="emptyText" /> <!--數(shù)據(jù)加載--> <el-card v-else shadow="hover" style="width: 100%" class="log-warpper"> <div style="width: 100%"> <div v-for="(log, index) in logs" :key="index"> <div style="margin-bottom: 5px; font-size: 12px;" v-html="log[1]" /> </div> </div> </el-card> </div>
向后端請求日志返回的結(jié)果是如下圖所示
滾動追加日志
其實下拉滾動的代碼與上面直接獲取日志的是差不多的,只是在數(shù)據(jù)的追加是不一樣的做法,這里需要注意的是要考慮日志的展示是正序還是逆序,不同的順序計算時間范圍是不一樣的,就如下代碼
if (this.direction === 'backward') { // 設(shè)置時間區(qū)間 queryParam.start = (this.logs[this.logs.length - 1][0] - zone).toString() queryParam.end = this.logs[this.logs.length - 1][0] } else { queryParam.start = this.logs[this.logs.length - 1][0] queryParam.end = (parseFloat(this.logs[this.logs.length - 1][0]) + parseFloat(zone.toString())).toString() }
在滾動獲取日志的思路是獲取最后一條數(shù)據(jù)的時間,往后推一定的時間差,所以需要考慮是正序還是倒序,默認是6小時。
mounted() { window.addEventListener('scroll', this.handleScroll) } methods: { handleScroll() { // 滾動事件 const scrollTop = document.documentElement.scrollTop// 滾動高度 const clientHeight = document.documentElement.clientHeight// 可視高度 const scrollHeight = document.documentElement.scrollHeight// 內(nèi)容高度 const bottomest = Math.ceil(scrollTop + clientHeight) if (bottomest >= scrollHeight) { // 加載新數(shù)據(jù) queryParam.limits = this.scrollStep queryParam.direction = this.direction // 獲取時間差 let zone = queryParam.end - queryParam.start if (this.timeRange.length) { // 如果是輸入時間范圍 zone = ((new Date(this.timeRange[1]).getTime() - new Date(this.timeRange[0]).getTime()) * 1000000).toString() } if (this.timeZoneValue) { zone = this.timeZoneValue * 1000000 } if (zone === 0) { zone = 3600 * 1000 * 6 } if (this.direction === 'backward') { // 設(shè)置時間區(qū)間 queryParam.start = (this.logs[this.logs.length - 1][0] - zone).toString() queryParam.end = this.logs[this.logs.length - 1][0] } else { queryParam.start = this.logs[this.logs.length - 1][0] queryParam.end = (parseFloat(this.logs[this.logs.length - 1][0]) + parseFloat(zone.toString())).toString() } var ansi_up = new AnsiUp() logOperation.getLogData(queryParam).then(res => { console.log(res) this.showEmpty = false if (res.data.result.length === 1) { // 如果返回的日志是一樣的就不顯示 if (res.data.result[0].values.length === 1 && ansi_up.ansi_to_html(res.data.result[0].values[0][1]) === this.logs[this.logs.length - 1][1]) { this.$notify({ title: '警告', duration: 1000, message: '當前時間段日志已最新!', type: 'warning' }) return } const log = res.data.result[0].values for (const i in res.data.result[0].values) { log[i][1] = ansi_up.ansi_to_html(res.data.result[0].values[i][1]) this.logs.push(log[i]) } } else if (res.data.result.length > 1) { const tempArray = [] // 數(shù)據(jù)需要處理,由于是追加數(shù)組,所以需要用額外變量來存放 // 刷新就是添加,不清空原數(shù)組 for (const j in res.data.result) { // 用push的方式將所有日志數(shù)組添加進去 for (const values_index in res.data.result[j].values) { tempArray.push(res.data.result[j].values[values_index]) } } if (this.direction === 'backward') { // 由于使用公共標簽會導(dǎo)致時間順序錯亂,因此對二維數(shù)組進行排序 tempArray.sort((a, b) => b[0] - a[0]) } else { tempArray.sort((a, b) => a[0] - b[0]) } for (const k in tempArray) { tempArray[k][1] = ansi_up.ansi_to_html(tempArray[k][1]) // 數(shù)據(jù)轉(zhuǎn)換 this.logs.push(tempArray[k]) // 追加數(shù)據(jù) } } else { this.$notify({ title: '警告', duration: 1000, message: '暫無以往日志數(shù)據(jù)!', type: 'warning' }) } }) } } }
定時刷新日志
當然,日志的獲取也是需要實時刷新的,這種不僅可以使用定時器還能夠使用websocket,筆者使用的是定時器,因為這個寫起來比較簡單。相關(guān)的代碼以及解析如下: 視圖
<el-form-item> <el-dropdown split-button type="primary" size="mini" @click="queryData"> 查詢{{ runStatu }} <el-dropdown-menu slot="dropdown"> <el-dropdown-item v-for="(item, index) in runStatuOptions" :key="index" @click.native="startInterval(item)">{{ item.label }}</el-dropdown-item> </el-dropdown-menu> </el-dropdown> </el-form-item>
方法代碼 代碼大致也和上面兩種情況是類似的,思路是獲取當前時間前(時間差)的時間到當前時間的日志信息。這里不需要管日志的時序方向,只需要做好始終時間,注意納秒級別,還有定時器不要忘記銷毀。
startInterval(item) { this.runStatu = item.label console.log(item.value) if (item.value !== 0) { this.timer = setInterval(() => { // 定時刷新 this.intervalLogs() }, item.value) } else { console.log('銷毀了') clearInterval(this.timer) } }, intervalLogs() { // 定時器的方法 // 組織參數(shù) // 設(shè)置開始時間和結(jié)束時間 // 開始為現(xiàn)在時間 const start = new Date() const end = new Date() // 時差判斷 let zone = queryParam.end - queryParam.start if (this.timeRange.length) { // 如果是輸入時間范圍 zone = ((new Date(this.timeRange[1]).getTime() - new Date(this.timeRange[0]).getTime()) * 1000000).toString() } if (this.timeZoneValue) { zone = this.timeZoneValue * 1000000 } if (zone === 0) { // 防止空指針 start.setTime(start.getTime() - 3600 * 1000 * 6) queryParam.start = (start.getTime() * 1000000).toString() } else { queryParam.start = (start.getTime() * 1000000 - zone).toString() } queryParam.end = (end.getTime() * 1000000).toString() queryParam.limits = this.limits console.log('定時器最后參數(shù):', queryParam) var ansi_up = new AnsiUp() // 后端日志格式轉(zhuǎn)化 logOperation.getLogData(queryParam).then(res => { console.log('res', res) this.showEmpty = false debugger if (res.data.result.length === 1) { this.logs = res.data.result[0].values for (const i in res.data.result[0].values) { // 格式轉(zhuǎn)換 this.logs[i][1] = ansi_up.ansi_to_html(res.data.result[0].values[i][1]) } } else if (res.data.result.length > 1) { // 清空 this.logs = [] for (const j in res.data.result) { // 用push的方式將所有日志數(shù)組添加進去 for (const values_index in res.data.result[j].values) { this.logs.push(res.data.result[j].values[values_index]) } } for (const k in this.logs) { this.logs[k][1] = ansi_up.ansi_to_html(this.logs[k][1]) } if (this.direction === 'backward') { // 由于使用公共標簽會導(dǎo)致時間順序錯亂,因此對二維數(shù)組進行排序 this.logs.sort((a, b) => b[0] - a[0]) } else { this.logs.sort((a, b) => a[0] - b[0]) } } else { this.showEmpty = true this.emptyText = '暫無日志信息,請選擇時間段試試' } }) },
最后粘一小段展示的界面
總結(jié)
loki是輕量級的分布式日志查詢框架,特別適合中小型企業(yè),尤其是工業(yè)項目,在項目上線的時候可以通過這樣的一個界面來觀察日志,確實能夠得到很大的幫助,但是這個loki不是特別的穩(wěn)定,最為常見的是會出現(xiàn)ERP ERROR,這種錯誤是最頭疼的,個人感覺可能是計算機或者網(wǎng)絡(luò)的因素造成。
以上就是SpringBoot Loki安裝簡介及實戰(zhàn)思路的詳細內(nèi)容,更多關(guān)于SpringBoot Loki安裝的資料請關(guān)注腳本之家其它相關(guān)文章!
- 使用?Loki?實現(xiàn)?Kubernetes?容器日志監(jiān)控的方法
- JavaScript實現(xiàn)的內(nèi)存數(shù)據(jù)庫LokiJS介紹和入門實例
- springboot使用小工具之Lombok、devtools、Spring Initailizr詳解
- SpringBoot文件上傳與下載功能實現(xiàn)詳解
- springboot如何設(shè)置請求參數(shù)長度和文件大小限制
- springboot中報錯Invalid character found in the request的解決
- springboot中關(guān)于classpath:路徑使用及說明
相關(guān)文章
RocketMQ4.5.2 修改mqnamesrv 和 mqbroker的日志路徑操作
這篇文章主要介紹了RocketMQ 4.5.2 修改mqnamesrv 和 mqbroker的日志路徑操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07redis實現(xiàn)隊列的阻塞、延時、發(fā)布和訂閱
本文主要介紹了redis實現(xiàn)隊列的阻塞、延時、發(fā)布和訂閱,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06org.apache.zookeeper.KeeperException.BadVersionException異常的解
在使用Apache ZooKeeper進行分布式協(xié)調(diào)時,你可能會遇到org.apache.zookeeper.KeeperException.BadVersionException異常,本文就來介紹一下解決方法,感興趣的可以了解一下2024-03-03SpringCloud Zuul在何種情況下使用Hystrix及問題小結(jié)
這篇文章主要介紹了SpringCloud Zuul在何種情況下使用Hystrix 及問題小結(jié),感興趣的朋友跟隨小編一起看看吧2018-11-11