在 Java 中調用 ChatGPT API 并實現流式接收(Server-Sent Events, SSE)

文章目錄

  • 簡介
  • OkHttp 流式獲取 GPT 響應
  • 通過 SSE 流式推送前端
    • 后端代碼
      • 消息實體
      • 接口
      • 接口實現
      • 數據推送給前端
    • 前端代碼
      • 創建 `sseClient.js`
      • vue3代碼
    • 優化后端代碼

簡介

用過 ChatGPT 的伙伴應該想過自己通過調用ChatGPT官網提供的接口來實現一個自己的問答機器人,但是在調用的時候發現,請求總是以傳統的HTTP請求/響應模式進行,這意味著我們沒發送一個請求后需要等待 ChatGPT 服務器返回完整的響應。這種方式在生成文本時并不不是我們理想的,因為用戶體驗不夠流暢。

為了提供更好的用戶體驗,我們可以使用Server-Sent Events(SSE)技術來實現流式接收。這樣,當ChatGPT 服務器可以在生成響應的同時逐步將內容推送給我們,我們在通過 SSE 流式推送到前端頁面,讓用戶能夠實時看到生成的內容。我將詳細介紹如何在Java中實現這一功能。

OkHttp 流式獲取 GPT 響應

其實市面上已經有很多現成的框架支持,但我們這里使用 okHttp 這個輕量級的HTTP客戶端庫來實現。

需要先引用相關maven:

    <dependency><groupId>com.squareup.okhttp3</groupId><artifactId>okhttp</artifactId></dependency><dependency><groupId>com.squareup.okhttp3</groupId><artifactId>okhttp-sse</artifactId></dependency>

構建請求體,必須加上參數 stream 值為true

 //構建發送內容String messageStr = StrUtil.format(prompt, params);// 創建一個Message對象,該對象表示一個消息,并設置其屬性Message message = new Message(Message.Role.USER.getRole(), messageStr);// 創建一個ChatCompletion對象,表示聊天完成請求,并將剛創建的消息添加到其中ChatCompletionRequest request = ChatCompletionRequest.builder().model(ChatCompletionRequest.Model.GPT_3_5_TURBO.getName()).messages(Arrays.asList(message)).stream(true).build();
       
// 定義see接口
Request request = new Request.Builder().url("https://api.openai.com/v1/chat/completions").header("Authorization","xxx").post(okhttp3.RequestBody.create(okhttp3.MediaType.parse("application/json; charset=utf-8"),param.toJSONString())).build();
OkHttpClient okHttpClient = new OkHttpClient.Builder().connectTimeout(10, TimeUnit.MINUTES).readTimeout(10, TimeUnit.MINUTES)//這邊需要將超時顯示設置長一點,不然剛連上就斷開,之前以為調用方式錯誤被坑了半天.build();// 實例化EventSource,注冊EventSource監聽器
RealEventSource realEventSource = new RealEventSource(request, new EventSourceListener() {@Overridepublic void onOpen(EventSource eventSource, Response response) {log.info("onOpen");}@SneakyThrows@Overridepublic void onEvent(EventSource eventSource, String id, String type, String data) {// log.info("onEvent");// 在實際應用中,你可以在這里將數據推送給前端log.info(data);//請求到的數據}@Overridepublic void onClosed(EventSource eventSource) {log.info("onClosed");
//                emitter.complete();}@Overridepublic void onFailure(EventSource eventSource, Throwable t, Response response) {log.error("onFailure 出現異常,response={}", response, t);//這邊可以監聽并重新打開
//                emitter.complete();}
});
realEventSource.connect(okHttpClient);//真正開始請求的一步

通過 SSE 流式推送前端

sse(Server Sent Event),直譯為服務器發送事件,顧名思義,也就是客戶端可以獲取到服務器發送的事件

我們常見的 http 交互方式是客戶端發起請求,服務端響應,然后一次請求完畢;但是在 sse 的場景下,客戶端發起請求,連接一直保持,服務端有數據就可以返回數據給客戶端,這個返回可以是多次間隔的方式

原理是先建立鏈接,然后不斷發消息就可以

我們利用 springboot 封裝的 SseEmitter 來完成推送,需要用到以下依賴:

<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.7.16</version>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId>
</dependency>

后端代碼

消息實體

其中客戶端 ID 是每個 SSE 鏈接的唯一標識,拿到 ID 可以精準的給唯一的用戶推送消息,消息通過字符串的方式進行傳遞

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;/*** 消息體*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MessageVo {/*** 客戶端id*/private String clientId;/*** 傳輸數據體(json)*/private String data;
}

接口

import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;public interface SseEmitterService {/*** 創建連接** @param clientId 客戶端ID*/SseEmitter createConnect(String clientId);/*** 根據客戶端id獲取SseEmitter對象** @param clientId 客戶端ID*/SseEmitter getSseEmitterByClientId(String clientId);/*** 發送消息給所有客戶端** @param msg 消息內容*/void sendMessageToAllClient(String msg);/*** 給指定客戶端發送消息** @param clientId 客戶端ID* @param msg      消息內容*/void sendMessageToOneClient(String clientId, String msg);/*** 關閉連接** @param clientId 客戶端ID*/void closeConnect(String clientId);
}

接口實現

@Slf4j
@Service
public class SseEmitterServiceImpl implements SseEmitterService {/*** 容器,保存連接,用于輸出返回 ;可使用其他方法實現*/private static final Map<String, SseEmitter> sseCache = new ConcurrentHashMap<>();/*** 根據客戶端id獲取SseEmitter對象** @param clientId 客戶端ID*/@Overridepublic SseEmitter getSseEmitterByClientId(String clientId) {return sseCache.get(clientId);}/*** 創建連接** @param clientId 客戶端ID*/@Overridepublic SseEmitter createConnect(String clientId) {// 設置超時時間,0表示不過期。默認30秒,超過時間未完成會拋出異常:AsyncRequestTimeoutExceptionSseEmitter sseEmitter = new SseEmitter(0L);// 是否需要給客戶端推送IDif (StrUtil.isBlank(clientId)) {clientId = IdUtil.simpleUUID();}// 注冊回調sseEmitter.onCompletion(completionCallBack(clientId));     // 長鏈接完成后回調接口(即關閉連接時調用)sseEmitter.onTimeout(timeoutCallBack(clientId));        // 連接超時回調sseEmitter.onError(errorCallBack(clientId));          // 推送消息異常時,回調方法sseCache.put(clientId, sseEmitter);log.info("創建新的sse連接,當前用戶:{}    累計用戶:{}", clientId, sseCache.size());try {// 注冊成功返回用戶信息sseEmitter.send(SseEmitter.event().id(String.valueOf(HttpStatus.HTTP_CREATED)).data(clientId, MediaType.APPLICATION_JSON));} catch (IOException e) {log.error("創建長鏈接異常,客戶端ID:{}   異常信息:{}", clientId, e.getMessage());}return sseEmitter;}/*** 發送消息給所有客戶端** @param msg 消息內容*/@Overridepublic void sendMessageToAllClient(String msg) {if (MapUtil.isEmpty(sseCache)) {return;}// 判斷發送的消息是否為空for (Map.Entry<String, SseEmitter> entry : sseCache.entrySet()) {MessageVo messageVo = new MessageVo();messageVo.setClientId(entry.getKey());messageVo.setData(msg);sendMsgToClientByClientId(entry.getKey(), messageVo, entry.getValue());}}/*** 給指定客戶端發送消息** @param clientId 客戶端ID* @param msg      消息內容*/@Overridepublic void sendMessageToOneClient(String clientId, String msg) {MessageVo messageVo = new MessageVo(clientId, msg);sendMsgToClientByClientId(clientId, messageVo, sseCache.get(clientId));}/*** 關閉連接** @param clientId 客戶端ID*/@Overridepublic void closeConnect(String clientId) {SseEmitter sseEmitter = sseCache.get(clientId);if (sseEmitter != null) {sseEmitter.complete();removeUser(clientId);}}/*** 推送消息到客戶端* 此處做了推送失敗后,重試推送機制,可根據自己業務進行修改** @param clientId  客戶端ID* @param messageVo 推送信息,此處結合具體業務,定義自己的返回值即可**/private void sendMsgToClientByClientId(String clientId, MessageVo messageVo, SseEmitter sseEmitter) {if (sseEmitter == null) {log.error("推送消息失敗:客戶端{}未創建長鏈接,失敗消息:{}",clientId, messageVo.toString());return;}SseEmitter.SseEventBuilder sendData = SseEmitter.event().id(String.valueOf(HttpStatus.HTTP_OK)).data(messageVo, MediaType.APPLICATION_JSON);try {sseEmitter.send(sendData);} catch (IOException e) {// 推送消息失敗,記錄錯誤日志,進行重推log.error("推送消息失敗:{},嘗試進行重推", messageVo.toString());boolean isSuccess = true;// 推送消息失敗后,每隔10s推送一次,推送5次for (int i = 0; i < 5; i++) {try {Thread.sleep(10000);sseEmitter = sseCache.get(clientId);if (sseEmitter == null) {log.error("{}的第{}次消息重推失敗,未創建長鏈接", clientId, i + 1);continue;}sseEmitter.send(sendData);} catch (Exception ex) {log.error("{}的第{}次消息重推失敗", clientId, i + 1, ex);continue;}log.info("{}的第{}次消息重推成功,{}", clientId, i + 1, messageVo.toString());return;}}}/*** 長鏈接完成后回調接口(即關閉連接時調用)** @param clientId 客戶端ID**/private Runnable completionCallBack(String clientId) {return () -> {log.info("結束連接:{}", clientId);removeUser(clientId);};}/*** 連接超時時調用** @param clientId 客戶端ID**/private Runnable timeoutCallBack(String clientId) {return () -> {log.info("連接超時:{}", clientId);removeUser(clientId);};}/*** 推送消息異常時,回調方法** @param clientId 客戶端ID**/private Consumer<Throwable> errorCallBack(String clientId) {return throwable -> {log.error("SseEmitterServiceImpl[errorCallBack]:連接異常,客戶端ID:{}", clientId);// 推送消息失敗后,每隔10s推送一次,推送5次for (int i = 0; i < 5; i++) {try {Thread.sleep(10000);SseEmitter sseEmitter = sseCache.get(clientId);if (sseEmitter == null) {log.error("SseEmitterServiceImpl[errorCallBack]:第{}次消息重推失敗,未獲取到 {} 對應的長鏈接", i + 1, clientId);continue;}sseEmitter.send("失敗后重新推送");} catch (Exception e) {e.printStackTrace();}}};}/*** 移除用戶連接** @param clientId 客戶端ID**/private void removeUser(String clientId) {sseCache.remove(clientId);log.info("SseEmitterServiceImpl[removeUser]:移除用戶:{}", clientId);}
}

數據推送給前端

在 onEvent 回調中添加代碼,每接收到消息后就推送到前端

       
// 定義see接口
Request request = new Request.Builder().url("https://api.openai.com/v1/chat/completions").header("Authorization","xxx").post(okhttp3.RequestBody.create(okhttp3.MediaType.parse("application/json; charset=utf-8"),param.toJSONString())).build();
OkHttpClient okHttpClient = new OkHttpClient.Builder().connectTimeout(10, TimeUnit.MINUTES).readTimeout(10, TimeUnit.MINUTES)//這邊需要將超時顯示設置長一點,不然剛連上就斷開,之前以為調用方式錯誤被坑了半天.build();// 實例化EventSource,注冊EventSource監聽器
RealEventSource realEventSource = new RealEventSource(request, new EventSourceListener() {@Overridepublic void onOpen(EventSource eventSource, Response response) {log.info("onOpen");}@Overridepublic void onEvent(EventSource eventSource, String id, String type, String data) {if ("[DONE]".equals(data)) {System.out.println("收到 [DONE] 信號");return;}ChatCompletionResp chatCompletionResp = JSON.parseObject(data, ChatCompletionResp.class);// 獲得生成的文章內容if (CollUtil.isEmpty(chatCompletionResp.getChoices())){return;}Message delta = chatCompletionResp.getChoices().get(0).getDelta();if (delta == null || delta.getContent() == null){return;}sseEmitterService.sendMessageToOneClient(clientId , delta.getContent());log.info(data);//請求到的數據}@Overridepublic void onClosed(EventSource eventSource) {log.info("onClosed");
//                emitter.complete();}@Overridepublic void onFailure(EventSource eventSource, Throwable t, Response response) {log.error("onFailure 出現異常,response={}", response, t);//這邊可以監聽并重新打開
//                emitter.complete();}
});
realEventSource.connect(okHttpClient);//真正開始請求的一步

前端代碼

由于 EventSource 不允許直接配置請求頭,普通的 EventSource 如果需要攜帶token請求,那就需要引入一個插件

安裝 EventSourcePolyfill

你可以通過npm安裝 event-source-polyfill:

npm install event-source-polyfill

引入 EventSourcePolyfill 后,它會自動替換瀏覽器中的原生 EventSource,其用法與原生的 API 一致。你可以像使用 EventSource 一樣使用它:

創建 sseClient.js

封裝一下, sse 最佳實踐,

// utils/sseClient.js
import { EventSourcePolyfill } from 'event-source-polyfill'
import { baseURL } from '../config';// 封裝一個創建 SSE 連接的方法
export function newEventSource({ clientId = '', headers = {}, onMessage, onError, onOpen }) {const token = sessionStorage.getItem('token') || ''const es = new EventSourcePolyfill(baseURL + 'p/sse/createConnect?clientId=' + clientId  , {headers: {'Authorization': `Bearer ${token}`...headers},heartbeatTimeout: 60 * 1000, // 心跳超時(可選)})es.onopen = (event) => {console.log('SSE 連接已開啟')onOpen && onOpen(event)}es.onmessage = (event) => {//前端:在接收到結束標識后立即銷毀if (event.data === '[DONE]') {console.log('SSE 連接已關閉')es.close()}onMessage && onMessage(event)}es.onerror = (event) => {console.error('SSE 錯誤:', event)onError && onError(event)es.close() // 出錯時自動關閉}return es // 返回實例,方便外部主動關閉
}

vue3代碼

import { newEventSource } from '@/utils/sseClient.js'const createSseConnection = () => {return newEventSource({clientId: 'xxx',onMessage: (event) => {console.log('Received SSE message:', event.data);}});
};

優化后端代碼

按需建立連接并及時關閉 是非常關鍵的實踐策略,每一個 SseEmitter 在服務端都是一個線程或者任務掛起的狀態,太多不關閉會導致資源消耗(線程、連接、內存等);

如果每個用戶長時間掛一個 SSE,不及時關閉,可能造成內存泄露或線程池耗盡,所以我們優化一下后端代碼,在完成輸出后及時關閉連接.

在關閉和異常的回調方法中添加:

sseEmitterService.sendMessageToOneClient(clientId, "[DONE]");
sseEmitterService.closeConnect(clientId);

修改后:

       
// 定義see接口
Request request = new Request.Builder().url("https://api.openai.com/v1/chat/completions").header("Authorization","xxx").post(okhttp3.RequestBody.create(okhttp3.MediaType.parse("application/json; charset=utf-8"),param.toJSONString())).build();
OkHttpClient okHttpClient = new OkHttpClient.Builder().connectTimeout(10, TimeUnit.MINUTES).readTimeout(10, TimeUnit.MINUTES)//這邊需要將超時顯示設置長一點,不然剛連上就斷開,之前以為調用方式錯誤被坑了半天.build();// 實例化EventSource,注冊EventSource監聽器
RealEventSource realEventSource = new RealEventSource(request, new EventSourceListener() {@Overridepublic void onOpen(EventSource eventSource, Response response) {log.info("onOpen");}@SneakyThrows@Overridepublic void onEvent(EventSource eventSource, String id, String type, String data) {// log.info("onEvent");// 在實際應用中,你可以在這里將數據推送給前端log.info(data);//請求到的數據}@Overridepublic void onClosed(EventSource eventSource) {log.info("onClosed");sseEmitterService.sendMessageToOneClient(clientId, "[DONE]");sseEmitterService.closeConnect(clientId);
//                emitter.complete();}@Overridepublic void onFailure(EventSource eventSource, Throwable t, Response response) {log.error("onFailure 出現異常,response={}", response, t);//這邊可以監聽并重新打開sseEmitterService.sendMessageToOneClient(clientId, "[DONE]");sseEmitterService.closeConnect(clientId);
//                emitter.complete();}
});
realEventSource.connect(okHttpClient);//真正開始請求的一步

輸出效果如下:

在這里插入圖片描述

參考文章:

java模擬GPT流式問答

Springboot 集成 SSE 向前端推送消息

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/77110.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/77110.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/77110.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

硬盤分區格式之GPT(GUID Partition Table)筆記250407

硬盤分區格式之GPT&#xff08;GUID Partition Table&#xff09;筆記250407 GPT&#xff08;GUID Partition Table&#xff09;硬盤分區格式詳解 GPT&#xff08;GUID Partition Table&#xff09;是替代傳統 MBR 的現代分區方案&#xff0c;專為 UEFI&#xff08;統一可擴展固…

Vite環境下解決跨域問題

在 Vite 開發環境中&#xff0c;可以通過配置代理來解決跨域問題。以下是具體步驟&#xff1a; 在項目根目錄下找到 vite.config.js 文件&#xff1a;如果沒有&#xff0c;則需要創建一個。配置代理&#xff1a;在 vite.config.js 文件中&#xff0c;使用 server.proxy 選項來…

交換機與ARP

交換機與 ARP&#xff08;Address Resolution Protocol&#xff0c;地址解析協議&#xff09; 的關系主要體現在 局域網&#xff08;LAN&#xff09;內設備通信的地址解析與數據幀轉發 過程中。以下是二者的核心關聯&#xff1a; 1. 基本角色 交換機&#xff1a;工作在 數據鏈…

【Spring】小白速通AOP-日志記錄Demo

這篇文章我將通過一個最常用的AOP場景-方法調用日志記錄&#xff0c;帶你徹底理解AOP的使用。例子使用Spring BootSpring AOP實現。 如果對你有幫助可以點個贊和關注。謝謝大家的支持&#xff01;&#xff01; 一、Demo實操步驟&#xff1a; 1.首先添加Maven依賴 <!-- Sp…

git功能點管理

需求&#xff1a; 功能模塊1 已經完成&#xff0c;已經提交并推送到遠程&#xff0c;準備交給測試。功能模塊2 已經完成&#xff0c;但不提交給測試&#xff0c;繼續開發。功能模塊3 正在開發中。 管理流程&#xff1a; 創建并開發功能模塊1&#xff1a; git checkout main…

QGIS實戰系列(六):進階應用篇——Python 腳本自動化與三維可視化

歡迎來到“QGIS實戰系列”的第六期!在前幾期中,我們從基礎操作到插件應用逐步提升了 QGIS 技能。這一篇,我們將邁入進階領域,探索如何用 Python 腳本實現自動化,以及如何創建三維可視化效果,讓你的 GIS 項目更高效、更立體。 第一步:Python 腳本自動化 QGIS 內置了 Py…

高德地圖 3D 渲染-區域紋理圖添加

引入-初始化地圖&#xff08;關鍵代碼&#xff09; // 初始化頁面引入高德 webapi -- index.html 文件 <script src https://webapi.amap.com/maps?v2.0&key您申請的key值></script>// 添加地圖容器 <div idcontainer ></div>// 地圖初始化應該…

ffmpeg視頻轉碼相關

ffmpeg視頻轉碼相關 簡介參數 實戰舉栗子獲取視頻時長視頻轉碼mp4文件轉為hls m3u8 ts等文件圖片轉視頻抽取視頻第一幀獲取基本信息 轉碼日志輸出詳解轉碼耗時測試 簡介 FFmpeg 是領先的多媒體框架&#xff0c;能夠解碼、編碼、 轉碼、復用、解復用、流、過濾和播放 幾乎所有人…

【ISP】HDR技術中Sub-Pixel與DOL的對比分析

一、原理對比 Sub-Pixel&#xff08;空間域HDR&#xff09; ? 核心機制&#xff1a;在單個像素內集成一大一小兩個子像素&#xff08;如LPD和SPD&#xff09;&#xff0c;利用其物理特性差異&#xff08;靈敏度、滿阱容量&#xff09;同時捕捉不同動態范圍的信號。 ? 大像素&…

Vulnhub-IMF靶機

本篇文章旨在為網絡安全滲透測試靶機教學。通過閱讀本文&#xff0c;讀者將能夠對滲透Vulnhub系列IMF靶機有一定的了解 一、信息收集階段 靶機下載地址&#xff1a;https://www.vulnhub.com/entry/imf-1,162/ 因為靶機為本地部署虛擬機網段&#xff0c;查看dhcp地址池設置。得…

Linux內核中TCP協議棧的實現:tcp_close函數的深度剖析

引言 TCP(傳輸控制協議)作為互聯網協議族中的核心協議之一,負責在不可靠的網絡層之上提供可靠的、面向連接的字節流服務。Linux內核中的TCP協議棧實現了TCP協議的全部功能,包括連接建立、數據傳輸、流量控制、擁塞控制以及連接關閉等。本文將深入分析Linux內核中tcp_close…

java+postgresql+swagger-多表關聯insert操作(七)

入參為json&#xff0c;然后根據需要對多張表進行操作&#xff1a; 入參格式&#xff1a; [{"custstoreName":"swagger-測試經銷商01","customerName":"swagger-測試客戶01","propertyNo":"swaggertest01",&quo…

R語言——繪制生命曲線圖(細胞因子IL5)

繪制生命曲線圖&#xff08;根據細胞因子&#xff09; 說明流程代碼加載包讀取Excel文件清理數據重命名列名處理IL-5中的"<"符號 - 替換為檢測下限的一半首先找出所有包含"<"的值檢查缺失移除缺失值根據IL-5中位數將患者分為高低兩組 創建生存對象擬…

Python----計算機視覺處理(Opencv:道路檢測完整版:透視變換,提取車道線,車道線擬合,車道線顯示,)

Python----計算機視覺處理&#xff08;Opencv:道路檢測之道路透視變換) Python----計算機視覺處理&#xff08;Opencv:道路檢測之提取車道線&#xff09; Python----計算機視覺處理&#xff08;Opencv:道路檢測之車道線擬合&#xff09; Python----計算機視覺處理&#xff0…

【Oracle篇】跨字符集遷移:基于數據泵的ZHS16GBK轉AL32UTF8全流程遷移

&#x1f4ab;《博主主頁》&#xff1a;奈斯DB-CSDN博客 &#x1f525;《擅長領域》&#xff1a;擅長阿里云AnalyticDB for MySQL(分布式數據倉庫)、Oracle、MySQL、Linux、prometheus監控&#xff1b;并對SQLserver、NoSQL(MongoDB)有了解 &#x1f496;如果覺得文章對你有所幫…

【C++算法】50.分治_歸并_翻轉對

文章目錄 題目鏈接&#xff1a;題目描述&#xff1a;解法C 算法代碼&#xff1a;圖解 題目鏈接&#xff1a; 493. 翻轉對 題目描述&#xff1a; 解法 分治 策略一&#xff1a;計算當前元素cur1后面&#xff0c;有多少元素的兩倍比我cur1小&#xff08;降序&#xff09; 利用單…

深入講解:智能合約中的讀寫方法

前言 在探秘區塊鏈開發:智能合約在 DApp 中的地位及與傳統開發差異一文中我提到對于智能合約中所有的寫入其實都算是交易。而在一個完整的智能合約代碼中最大的兩個組成部分就是讀取和寫入。 本文將為你深入探討該兩者方法之間的區別。 寫方法 寫方法其實就是對區塊鏈這一…

Go語言類型捕獲及內存大小判斷

代碼如下&#xff1a; 類型捕獲可使用&#xff1a;reflect.TypeOf()&#xff0c;fmt.Printf在的%T。 內存大小判斷&#xff1a;len()&#xff0c;unsafe.Sizeof。 package mainimport ("fmt""unsafe""reflect" )func main(){var i , j 1, 2f…

MyBatis Plus 在 ZKmall開源商城持久層的優化實踐

ZKmall開源商城作為基于 Spring Cloud 的高性能電商平臺&#xff0c;其持久層通過 MyBatis Plus 實現了多項深度優化&#xff0c;涵蓋分庫分表、緩存策略、分頁性能、多租戶隔離等核心場景。以下是具體實踐總結&#xff1a; 一、分庫分表與插件集成優化 1. 分庫分表策略 ?Sh…

學習MySQL第七天

夕陽無限好 只是近黃昏 一、子查詢 1.1 定義 將一個查詢語句嵌套到另一個查詢語句內部的查詢 我們通過具體示例來進行演示&#xff0c;這一篇博客更側重于通過具體的小問題來引導大家獨立思考&#xff0c;然后熟悉子查詢相關的知識點 1.2 問題1 誰的工資比Tom高 方…