1、許苑–OJ判題系統
技術棧:Spring Boot+Spring Cloud Alibaba+Redis+Mybatis+MQ+Docker
項目地址: https://github.com/xuyuan-upward/xyoj-backend-microservice
1.1、項目介紹:
一個基于微服務的OJ系統,具備能夠根據管理員預設的題目用例對用戶提交的代碼進行執行和評測的能力。此外,還自主實現了代碼沙箱,可作為獨立服務供其他開發者調用。
1.2、主要工作:
1、為?持多種代碼沙箱的創建,使?靜態??模式實現對代碼沙箱調?的擴展,提高了系統可擴展性和可維護性。
1.1、通過靜態工廠模式實現了遠程代碼沙箱或者本地代碼沙箱調用
2、采用了策略模式封裝判題邏輯,以解決不同判題模式的差異,提高系統的靈活性。
2.1、根據獲取到的代碼沙箱返回的配置信息以及輸出結果進行使用Java策略算法判斷還是其他語言判斷代碼的正確性
2.2、根據不同語言選擇算判題策略
3、 使? Java Runtime類的exec?法編譯和執?Java代碼,通過Process類獲取結果。
3.1、Runtime類進行命令的創建以及命令執行:
3.2、Process獲取結果 :
4、為確保宿主機安全,利?Docker Java庫創建隔離的容器環境執行代碼。
4.1、引入docker庫依賴
<dependency><groupId>com.github.docker-java</groupId><artifactId>docker-java</artifactId><version>3.3.0</version></dependency>
4.2、獲取Docker客戶端實例
DockerClient dockerClient = DockerClientBuilder.getInstance("unix:///var/run/docker.sock").build();
通過DockerClientBuilder
創建一個Docker客戶端實例,用于與Docker守護進程通信。
unix:///var/run/docker.sock
是Docker守護進程的Unix套接字路徑。
4.3、拉取Docker鏡像
if (!isPullImag) {PullImageCmd pullImageCmd = dockerClient.pullImageCmd(Image);PullImageResultCallback pullImageResultCallback = new PullImageResultCallback(){@Overridepublic void onNext(PullResponseItem item) {System.out.println("下載鏡像" + item.getStatus());super.onNext(item);}};pullImageCmd.exec(pullImageResultCallback).awaitCompletion();isPullImag = true;
}
如果鏡像尚未拉取,則通過pullImageCmd
拉取指定的Docker鏡像。
PullImageResultCallback
用于監聽鏡像拉取的進度和狀態。
awaitCompletion()
確保鏡像拉取完成后才繼續執行后續代碼。
4.4、創建并啟動Docker容器
CreateContainerCmd containerCmd = dockerClient.createContainerCmd(Image);
HostConfig hostConfig = new HostConfig();
hostConfig.withMemory(150 * 1000 * 1000L); // 限制內存為150MB
hostConfig.withMemorySwap(0L); // 禁用交換內存
hostConfig.withCpuCount(1L); // 限制CPU核數為1
hostConfig.setBinds(new Bind(userCodeParentPath, new Volume("/app"))); // 綁定主機目錄到容器
CreateContainerResponse createContainerResponse = containerCmd.withHostConfig(hostConfig).withNetworkDisabled(true) // 禁用網絡.withReadonlyRootfs(true) // 只讀文件系統.withAttachStdin(true) // 綁定標準輸入.withAttachStderr(true) // 綁定標準錯誤.withAttachStdout(true) // 綁定標準輸出.withTty(true) // 啟用TTY.exec();
String containerId = createContainerResponse.getId();
dockerClient.startContainerCmd(containerId).exec();
創建容器時,配置了資源限制(內存、CPU)和安全性(禁用網絡、只讀文件系統)。
setBinds
將主機目錄綁定到容器內的/app目錄,用于存放用戶代碼。
containerCmd.exec()
創建 Docker 容器,并通過createContainerResponse獲取容器ID,然后通過dockerClient.startContainerCmd(containerId).exec();
啟動容器。
4.5、在容器中執行用戶代碼
String[] cmdArray = ArrayUtil.append(new String[]{"java", "-cp", "/app", "Main"}, inputArgsArray);
ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId).withCmd(cmdArray) // 設置執行的命令.withAttachStderr(true) // 綁定標準錯誤.withAttachStdin(true) // 綁定標準輸入.withAttachStdout(true) // 綁定標準輸出.exec();
構造執行命令,例如java -cp /app Main <inputArgs>
。
通過execCreateCmd在容器中創建執行命令,并獲取命令的ID。
4.6、 捕獲執行結果
ExecStartResultCallback execStartResultCallback = new ExecStartResultCallback() {@Overridepublic void onNext(Frame frame) {StreamType streamType = frame.getStreamType();if (StreamType.STDERR.equals(streamType)) {errorMessage[0] = new String(frame.getPayload()); // 捕獲錯誤輸出} else {message[0] = new String(frame.getPayload()); // 捕獲標準輸出}}
};
dockerClient.execStartCmd(execId).exec(execStartResultCallback).awaitCompletion(TIME_OUT, TimeUnit.SECONDS);
通過ExecStartResultCallback
監聽命令執行的輸出:
- 如果是 STDERR,表示發生錯誤,捕獲錯誤信息。
- 如果是 STDOUT,捕獲程序正常輸出。
4.7、監控內存使用
StatsCmd statsCmd = dockerClient.statsCmd(containerId);
ResultCallback<Statistics> statisticsResultCallback = statsCmd.exec(new ResultCallback<Statistics>() {@Overridepublic void onNext(Statistics statistics) {maxMemory[0] = Math.max(statistics.getMemoryStats().getUsage(), maxMemory[0]); // 獲取內存使用峰值}
});
通過statsCmd
監控容器的內存使用情況,記錄內存使用的峰值。
4.8、刪除容器
dockerClient.stopContainerCmd(containerId).exec();
dockerClient.removeContainerCmd(containerId).exec();
執行完成后,停止并刪除容器,釋放資源。
5、為減少判題與服務模塊之間的耦合,通過使?Rabbitmq技術進?解耦。
5.1、原因
原因:由于判題操作是一個比較重的服務(需要調用代碼沙箱)然后判題服務監聽到該隊列的消息并進行判題處理,并且異步更改題目的判題狀態。改造后的業務流程:用戶提交題目時,由題目服務發送一條消息到隊列
這樣做的好處!
- 對用戶來說:不需要在前端同步等待,優化了體驗。
- 對系統來說:解耦了題目服務和判題服務,兩者不需要相互調用。
使判題服務繁忙或宕機,題目服務依然可以發送判題任務到隊列,等判題服務恢復后繼續處理
5.2、過程
1、首先進行Rabbitmq初始化交換機與隊列根據路由鍵進行綁定,并創建成Spring的一個 bean 對象
2、在提交題目那里調用生產者進行發送消息
生產者發送消息:
進行消費者監聽對應的隊列,然后調用判題方法:
6、為保護服務同時簡化客戶端調用,項目通過Spring Cloud Gateway聚合路由服務。
在第7點下面的gateway
7、微服務體現,以及nacos配置中心。
7.1、引入對應的SpringBoot SpringCloud SpringCloud Alibaba版本依賴管理
<dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>2021.0.5</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>${spring-cloud-alibaba.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>
7.2、引入nacos依賴
<!--nacos 配置和注冊管理--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency>
7.3、進行引入SpringCloud的gateway依賴,以及微服務下knife4j的聚合
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><!--用來實現knife4j文檔聚合--><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-gateway-spring-boot-starter</artifactId><version>4.3.0</version></dependency>
7.4、路由配置規則的設定
2、許苑刷題閣
技術棧:Spring Boot+ElasticSearch+Mybatis+Nacos+Hotkey+SaToken+Sentinel
項目地址: https://github.com/xuyuan-upward/mianshiyuan
2.1、項目介紹:
一個在線刷題平臺,平臺?持管理員創建題庫、批量管理題目,用戶可以通過高效
的搜索引擎進?題目檢索,在線做題。項目核?圍繞性能優化、數據一致性和高并發場景進?
設計, 確保用戶的刷題體驗流暢且穩定。
2.2、主要工作:
1、為實現用戶刷題記錄功能,基于Redis BitMap+Redisson實現用戶年度刷題記錄的統
計,相?數據庫存儲節約?百倍空間。
1.1、使用Bitmap 位圖,是一種使用位(bit)來表示數據的 緊湊 數據結構。每個位可以存儲兩個值:0 或 1,常用于表示某種狀態或標志.。
優點:
1.節約內存空間:因為每個位僅占用1位內存,特別在大規模存儲二值數據(如布爾值)時,節約效果明顯。
2.查詢效率高:通過位運算(如與、或、非等),可以快速判斷某個元素是否存在。這使得查找操作非常高效,時間復雜度為 O(1)。
代碼(簽到):
// 獲取 Redis 的 BitMap// RBisSet是Redisson庫中的一種數據類型,它對應Redis中的位圖RBitSet signInBitSet = redissonClient.getBitSet(key);// 獲取當前日期是一年中的第幾天,作為偏移量(從 1 開始計數)int offset = date.getDayOfYear();// 查詢當天有沒有簽到if (!signInBitSet.get(offset)) {// 如果當前未簽到,則設置signInBitSet.set(offset, true);}
代碼(獲取某年某個用戶的簽到信息):
// 獲取 Redis 的 BitSetRBitSet signInBitSet = redissonClient.getBitSet(key);// 加載 BitSet 到Java內存中,避免后續讀取時發送多次請求BitSet bitSet = signInBitSet.asBitSet();// 統計簽到的日期List<Integer> dayList = new ArrayList<>();// 從索引 0 開始查找下一個被設置為 1 的位int index = bitSet.nextSetBit(0);while (index >= 0) {dayList.add(index);// 繼續查找下一個被設置為 1 的位index = bitSet.nextSetBit(index + 1);}
2、為提高題目搜索性能,采?Elasticsearch替代MySQL進?模糊查詢,并通過定時任務,實現增量同步,保持數據一致性。
2.1、引入依賴
<!-- elasticsearch-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
并通過注入elasticsearchRestTemplate
即可進行Elasticsearch的操作
@Resource
private ElasticsearchRestTemplate elasticsearchRestTemplate;
2.2、集成類似操作數據庫的 Dao 實體類一樣
2.3、繼承ElasticsearchRepository,此時可以類似mybatis那樣操作 es 了
public interface QuestionEsDao extends ElasticsearchRepository<QuestionEsDTO, Long> {List<QuestionEsDTO> findByUserId(Long userId);
2.4、并通過實現CommandLineRunner
接口,項目啟動時,實現MySQL全量同步到 ES。
2.5、開啟定時任務,每分鐘進行對5分中之前修改的數據進行增量數據同步
3、為防止高并發下瞬時流量擊垮數據庫,接?Hotkey緩存熱?題目,提高性能和安全性。
3.1、工作流程:
JD HotKey 是京東提供了一個輕量級通用的熱 key 探測中間件。
首先在 dashboard 中配置了熱點 key的規則,并部署 worker 用于統計 key 的訪問數量,在后端項目中引入jd client 上報熱點 key 的訪問給 worke進行統計訪問數量,一旦達到指定的熱點 key 規定閾值,worker 會推送熱點 key 到client jar包的后端,后端進行caffeine本地緩存。
主要代碼:
String key = "bank_detail_" + id;// 如果是熱點keyif (JdHotKeyStore.isHotKey(key)) {// 從本地緩存中獲取緩存值Object cacheQuestionBankVO = JdHotKeyStore.get(key);if (cacheQuestionBankVO != null) {return ResultUtils.success((QuestionBankVO) cacheQuestionBankVO);}}// 查詢數據庫QuestionBankVO questionBankVO = questionBankService.getQuestionBankVO(questionBank, request);// 設置本地緩存 如果是熱點key了才會設置對應的緩存 否則不做任何處理JdHotKeyStore.smartSet(key, questionBankVO);
分析:基于isHotKey
該方法會返回該 key 是否是熱 key,如果是返回 true,如果不是返回 false,并且會將 key 上報到探測集群進行數量計算。該方法通常用于判斷只需要判斷 key 是否熱、不需要緩存 value 的場景,如刷子用戶、接口訪問頻率等。并基于get
方法獲取緩存,熱key返回緩存,不是熱key返回null。沒有緩存通過此smartSet
方法給熱 key 賦值 value,如果是熱 key,該方法才會賦值,非熱 key,什么也不做。
本地緩存淘汰策略:
- 基于大小的淘汰
最大緩存大小:緩存可以配置最大條目數。一旦條目數超過限制,(LRU)最少使用次數的條目將被淘汰。 - 基于時間的淘汰
過期:緩存條目可以在一定時間后過期。這確保了過時、未使用的緩存條目會被自動移除。例如,可以將條目的過期時間配置為 10 分鐘。 - 基于引用的淘汰
弱引用或軟引用:此策略基于內存壓力淘汰條目,當 JVM 需要更多內存時,會移除不再被強引用的緩存條目。
黑馬Redis教學圖例:
3.2、該JD Hotkey框架組成部分:
1、etcd集群
etcd作為一個高性能的配置中心,可以以極小的資源占用,提供高效的監聽訂閱服務。主要用于存放key規則配置,各worker的ip地址等。
2、client端jar包
就是在服務中添加的引用jar,引入后,就可以以便捷的方式去判斷某key是否熱key。同時,該jar完成了key上報、監聽etcd里key的rule變化、以及拉取worker的ip、對熱key進行本地caffeine緩存等。
3、worker端集群
worker端是一個獨立部署的Java程序,啟動后會連接etcd,并定期上報自己的ip信息,供client端獲取地址并進行長連接。之后,主要就是對各個client發來的待測key進行累加計算,當達到etcd里設定的rule閾值后,將熱key推送到各個client。
4、dashboard控制臺
控制臺是一個帶可視化界面的Java程序,也是連接到etcd,之后在控制臺設置各個APP的key規則,譬如2秒出現20次算熱key。然后當worker探測出來熱key后,會將key發往etcd,dashboard也會監聽熱key信息,進行入庫保存記錄。同時,dashboard也可以手工添加、刪除熱key,供各個client端監聽。
4、為保護系統,通過Sentinel限流和熔斷保護題庫接口,異常時返回緩存數據。
4.1、什么是熔斷?什么是限流?
- 熔斷:熔斷是指當調用的下游服務出現故障時,切斷對該服務的調用,防止系統出現連鎖故障。
工作原理:- 健康檢查:熔斷器監控系統會檢查與其他服務的連接情況,當調用某個服務頻繁失敗時,它會進入 打開 狀態。
- 打開狀態:當熔斷器處于打開狀態時,所有對該服務的請求會切斷,不會繼續向故障的服務發送請求,從而避免進一步加重服務負擔。
- 恢復狀態:熔斷器會在一段時間后進入 半開 狀態,允許少量請求通過,如果這些請求成功,熔斷器會重新恢復到 關閉 狀態,恢復正常調用。如果失敗,熔斷器會重新進入 打開 狀態,繼續拒絕請求。
- 關閉狀態:在沒有問題時,熔斷器保持關閉狀態,正常傳遞請求。
- 限流:限流是指限制單位時間內對某個資源或服務的訪問次數。
- 總結:熔斷與限流可以同時使用,熔斷器用于處理服務不可用的情況,而限流用于控制請求頻率,保證系統的穩定運行。
4.2、項目示例運用:
Sentinel控制臺部署
接入客戶端用于和Sentinel進行通訊,引入依賴(SpringCloud Alibaba已經整合)
<!-- https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-starter-alibaba-sentinel -->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId><version>2021.0.5.0</version>
</dependency>
通過注解對listQuestionBankVOByPage資源進行保護定義:
@SentinelResource(value = "listQuestionBankVOByPage",blockHandler = "handleBlockHandler",fallback = "handleFallback")public BaseResponse<Page<QuestionBankVO>> listQuestionBankVOByPage(@RequestBody QuestionBankQueryRequest questionBankQueryRequest,HttpServletRequest request)
(1)value = "listQuestionBankVOByPage"
定義資源名稱,Sentinel 會監控 listQuestionBankVOByPage 方法的調用情況。因為題庫是經常訪問的,故將它定義為保護資源
可以在 Sentinel 控制臺 配置 限流、熔斷、降級規則。
(2)blockHandler = "handleBlockHandler"
當觸發限流或熔斷時,會執行 handleBlockHandler 方法,而不是直接拋出異常。
這個 handleBlockHandler 方法需要和原方法的參數一致,并且返回類型相同。
該方法不能和 listQuestionBankVOByPage 方法定義在不同的類中(除非是 static 方法)。
(3)fallback = "handleFallback"
當方法發生異常(例如超時、空指針等)時,會執行 handleFallback 方法,提供降級處理邏輯。
handleFallback 方法也需要和 listQuestionBankVOByPage 方法的參數列表一致,返回值類型相同。
5、為防止不同客?端賬號共享,通過UserAgent識別設備,Sa-Token檢測同端登錄沖突。
這行代碼的作用是 用戶登錄 并將當前用戶與指定的設備進行綁定,實現同一用戶在不同設備上的登錄互斥。
StpUtil.login(user.getId(), DeviceUtils.getRequestDevice(request));
這行代碼的作用是 將用戶的登錄狀態存儲到當前會話中,方便后續通過會話獲取和使用用戶信息。
StpUtil.getSession().set(USER_LOGIN_STATE, user);
6、為防止內容盜取,設計分級反爬?策略:使?Redis統計訪問題目頻率,超限時?動報警
和封禁用戶。
對getQuestionVOById
次數進行限制。10拋出訪問頻繁,20次踢下線。
3、許苑園–尋找共同興趣的伙伴
技術棧:Spring Boot+Redis+Mybatis+WebSocket+ChatGPT+Vue3
項目地址: https://github.com/xuyuan-upward/xuyuan-matching
3.1、項目介紹:
一個實時的社交聊天平臺,致力于為用戶尋找共同興趣的學習伙伴。基于目的
實現了伙伴交流聊天室、按共同興趣愛好標簽檢索伙伴、推薦相似伙伴、組隊,聊天,
ChatGPT問答等功能。