目實(shí)戰(zhàn):Spring Boot + Redis 構(gòu)建完整O2O訂單系統(tǒng))
做這個(gè)項(xiàng)目之前我在本地跑過不少零散的接口demo但真正動(dòng)手把“蒼穹外賣”這套完整項(xiàng)目從頭到尾捋下來之后才感受到一個(gè)線上級(jí)業(yè)務(wù)系統(tǒng)涉及的東西遠(yuǎn)不止CRUD那么簡單。這套外賣系統(tǒng)很像一個(gè)濃縮版的商業(yè)中臺(tái)用戶端小程序、商家端后臺(tái)、管理端控制臺(tái)外加一整套訂單流轉(zhuǎn)、購物車、支付回調(diào)、配送提醒、數(shù)據(jù)統(tǒng)計(jì)的鏈路。無論你是準(zhǔn)備畢業(yè)設(shè)計(jì)、想寫進(jìn)簡歷的練手項(xiàng)目還是想搞明白“一個(gè)真實(shí)業(yè)務(wù)系統(tǒng)怎么從零搭起來”這個(gè)項(xiàng)目都能給你很完整的參照系。主后端用的是Spring Boot持久層用了MyBatis也有一版是MyBatis Plus緩存交給Redis登錄鑒權(quán)走JWT前后端分離管理端用Vue構(gòu)建。整個(gè)開發(fā)過程里最難的部分不是某個(gè)算法而是“多個(gè)模塊之間怎么把狀態(tài)同步好”——訂單狀態(tài)怎么變、庫存什么時(shí)候扣、超時(shí)單怎么處理、WebSocket怎么把配送狀態(tài)推給用戶。這篇文章我會(huì)把你需要關(guān)注的模塊設(shè)計(jì)、關(guān)鍵實(shí)現(xiàn)、部署細(xì)節(jié)以及我實(shí)際調(diào)試過程中踩過的坑全部拆開講盡量做到你照著往下走就能復(fù)現(xiàn)。1. 項(xiàng)目全貌與技術(shù)選型拆解1.1 為什么叫“蒼穹外賣”它在解決什么問題“蒼穹外賣”是一個(gè)典型的O2O外賣平臺(tái)項(xiàng)目名字本身是代號(hào)你可以把它理解成一套“小美外賣”或“某外賣系統(tǒng)”。它模擬真實(shí)外賣平臺(tái)的核心閉環(huán)用戶瀏覽菜品、加入購物車、下單支付模擬、商家接單、騎手配送、用戶確認(rèn)收貨同時(shí)管理端還承擔(dān)分類管理、菜品管理、套餐管理、員工管理、訂單查詢和數(shù)據(jù)統(tǒng)計(jì)等功能。這個(gè)項(xiàng)目最大的價(jià)值在于覆蓋面廣它不是簡單的“增刪改查”而是把真實(shí)的業(yè)務(wù)狀態(tài)機(jī)、并發(fā)控制、緩存策略、權(quán)限模型都融入進(jìn)來了。比如用戶端需要處理購物車勾選的一堆菜品下單時(shí)要同時(shí)考慮起送價(jià)、配送費(fèi)、訂單總金額和結(jié)算明細(xì)商家端要能看到新訂單并執(zhí)行接單/拒單管理端要能按時(shí)間段查詢營業(yè)額和用戶下單趨勢。任何一個(gè)環(huán)節(jié)如果只寫表面接口都會(huì)在聯(lián)調(diào)時(shí)出問題。對(duì)于開發(fā)者來說它解決了“只會(huì)寫CRUD不知道業(yè)務(wù)怎么串聯(lián)”的痛點(diǎn)。你會(huì)真實(shí)地感受到一個(gè)訂單從創(chuàng)建到完成需要哪些表需要哪些狀態(tài)字段什么時(shí)候用Redis存購物車什么時(shí)候必須落到MySQL什么時(shí)候用定時(shí)任務(wù)去處理超時(shí)未支付訂單。這些經(jīng)驗(yàn)在面試和實(shí)際工作中都非常關(guān)鍵。1.2 技術(shù)棧清單與選型理由先列一份我在項(xiàng)目中實(shí)際使用的技術(shù)棧集合方便你對(duì)照環(huán)境層次技術(shù)選型說明后端Spring Boot 2.x主框架快速搭建RESTful服務(wù)持久層MyBatis含MyBatis Plus封裝靈活編寫SQL應(yīng)對(duì)復(fù)雜報(bào)表緩存Redis緩存店鋪營業(yè)狀態(tài)、購物車數(shù)據(jù)存儲(chǔ)JWT黑名單鑒權(quán)JWT Spring Interceptor無狀態(tài)登錄校驗(yàn)數(shù)據(jù)庫MySQL 5.7/8.0存儲(chǔ)持久化業(yè)務(wù)數(shù)據(jù)前端用戶端微信小程序原生或uni-app貼近真實(shí)C端場景前端管理端Vue Element UI管理后臺(tái)界面接口文檔Knife4j / Swagger前后端聯(lián)調(diào)必備部署Linux Nginx Docker可選模擬線上環(huán)境選型背后的邏輯我需要多說幾句。Spring Boot在Java生態(tài)里就是“約定優(yōu)于配置”的代言人起步快并且社區(qū)資料極其豐富。MyBatis和JPA的爭論一直存在但這個(gè)項(xiàng)目里我選MyBatis是因?yàn)楹芏嗖樵兩婕岸啾黻P(guān)聯(lián)、動(dòng)態(tài)條件、金額統(tǒng)計(jì)MyBatis的SQL可控性更強(qiáng)出現(xiàn)慢查詢時(shí)可以直接在XML里優(yōu)化。Redis在這里不是裝飾品而是真實(shí)承擔(dān)了高頻讀寫業(yè)務(wù)外賣系統(tǒng)里的店鋪營業(yè)狀態(tài)每天被看好幾次購物車數(shù)據(jù)用戶改來改去寫在MySQL里不僅要頻繁建會(huì)話表壓力也大放在Redis里天然帶過期時(shí)間數(shù)據(jù)結(jié)構(gòu)又支持Hash和List非常契合。JWT則解決了分布式場景下的會(huì)話共享問題。雖然單體項(xiàng)目用Session也可以但JWT更接近現(xiàn)在的主流架構(gòu)習(xí)慣登錄后服務(wù)端不保存用戶會(huì)話只需要在攔截器里校驗(yàn)Token的簽名和有效期這樣后續(xù)如果要把服務(wù)拆成多實(shí)例也無須額外同步Session。1.3 系統(tǒng)角色與模塊劃分我從實(shí)際開發(fā)的角度把整個(gè)項(xiàng)目拆成三大塊用戶端小程序/移動(dòng)H5登錄、瀏覽分類與菜品、購物車、下單、支付結(jié)算模擬、查看訂單狀態(tài)、催單。商家端管理后臺(tái)一部分主要是商家或店員視角管理菜品、接收新訂單、訂單狀態(tài)流轉(zhuǎn)比如接單、配送中、完成。管理端平臺(tái)運(yùn)營視角員工管理、分類與菜品管理、套餐管理、訂單綜合查詢、營業(yè)數(shù)據(jù)統(tǒng)計(jì)、工作臺(tái)運(yùn)營概覽。為什么要把商家端和管理端分開真實(shí)外賣平臺(tái)中商家只能管理自己的店鋪平臺(tái)運(yùn)營可以查看全量數(shù)據(jù)。這個(gè)項(xiàng)目里Model劃分雖然沒有搞微服務(wù)那么重但Controller層的包結(jié)構(gòu)、權(quán)限攔截、角色判斷還是要提前設(shè)計(jì)好。我一開始就是把所有接口都堆到同一個(gè)Controller里后面發(fā)現(xiàn)要么是權(quán)限混亂要么是字段輸出到前端時(shí)互相干擾所以項(xiàng)目里凡是標(biāo)識(shí)了admin的路徑都統(tǒng)一管理凡是/user路徑都走用戶身份判斷形成約定后代碼好維護(hù)很多。2. 數(shù)據(jù)庫設(shè)計(jì)與核心表結(jié)構(gòu)解析2.1 業(yè)務(wù)實(shí)體拆解做任何項(xiàng)目數(shù)據(jù)庫設(shè)計(jì)都是地基。我最初設(shè)計(jì)這張表清單花了兩天時(shí)間來回調(diào)整了三版。最終的核心表大概包括員工表employee管理端登錄賬號(hào)包含賬號(hào)、密碼加鹽加密、狀態(tài)。分類表category類型區(qū)分菜品分類和套餐分類名稱、排序、是否刪除。菜品表dish菜名、圖片、價(jià)格、描述、所屬分類、狀態(tài)啟售/停售??谖侗韉ish_flavor很多菜品需要選辣度、規(guī)格一對(duì)多。套餐表setmeal包含套餐價(jià)格、分類、狀態(tài)。套餐與菜品關(guān)聯(lián)表setmeal_dish多對(duì)多關(guān)系。用戶表user微信端用戶信息openid模擬、昵稱、手機(jī)號(hào)。地址簿表address_book收貨人、手機(jī)號(hào)、詳細(xì)地址、標(biāo)簽家/公司。購物車表shopping_cart其實(shí)可以用Redis但落庫版本能把購物車持久化到MySQL表里存用戶ID、菜品ID、口味JSON、數(shù)量、金額。訂單表orders訂單號(hào)、用戶ID、地址快照、金額明細(xì)、狀態(tài)、下單時(shí)間、支付時(shí)間、預(yù)計(jì)送達(dá)時(shí)間。訂單明細(xì)表order_detail一個(gè)訂單對(duì)應(yīng)多個(gè)菜品記錄菜品快照信息。工作臺(tái)相關(guān)統(tǒng)計(jì)表一般實(shí)時(shí)計(jì)算不單獨(dú)建表。這里想強(qiáng)調(diào)兩個(gè)容易被忽略的設(shè)計(jì)點(diǎn)。第一訂單表里一定要存地址快照和菜品快照而不是通過外鍵去關(guān)聯(lián)用戶地址表和菜品表。否則用戶以后改了地址或者菜品下架改名歷史訂單的數(shù)據(jù)就全部亂掉了。第二金額字段要用decimal(10,2)不能用float或double這個(gè)面試?yán)锊戎懈怕蕵O高——二進(jìn)制浮點(diǎn)數(shù)在累加時(shí)會(huì)出現(xiàn)0.10.2不等于0.3的問題訂單金額錯(cuò)了可沒法交代。2.2 訂單狀態(tài)流轉(zhuǎn)與關(guān)聯(lián)設(shè)計(jì)訂單是這個(gè)系統(tǒng)的核心我們把狀態(tài)機(jī)設(shè)計(jì)為狀態(tài)值含義對(duì)應(yīng)的用戶操作1待付款用戶下單后未支付2待接單已支付等待商家確認(rèn)3已接單商家已接受制作中4配送中騎手正在配送5已完成用戶確認(rèn)或自動(dòng)完成6已取消用戶取消或超時(shí)取消7退款售后狀態(tài)模擬狀態(tài)之間絕不能亂跳比如待付款狀態(tài)下是不能直接改成配送中的。我代碼里在Service層寫了一個(gè)統(tǒng)一的狀態(tài)校驗(yàn)方法不允許非法狀態(tài)躍遷。在訂單表里還設(shè)置了一個(gè)字段區(qū)分是用戶取消、商家拒單還是超時(shí)取消便于后續(xù)統(tǒng)計(jì)取消原因。和訂單關(guān)聯(lián)最緊密的就是明細(xì)表。明細(xì)表不需要和菜品表做強(qiáng)關(guān)聯(lián)因?yàn)椴似繁碜侄慰梢噪S便改但明細(xì)要保留當(dāng)時(shí)的價(jià)格。如果你在管理端改了菜品價(jià)格然后回頭查歷史訂單發(fā)現(xiàn)金額變了那就是關(guān)聯(lián)設(shè)計(jì)出了問題。這是我實(shí)際開發(fā)中遇到的真實(shí)問題所以快照思路要嚴(yán)格執(zhí)行。2.3 數(shù)據(jù)庫設(shè)計(jì)的幾個(gè)坑第一個(gè)坑是時(shí)間字段的時(shí)區(qū)問題。MySQL里如果連接參數(shù)沒有設(shè)置serverTimezoneAsia/Shanghai入庫的時(shí)間會(huì)和本地時(shí)間差8小時(shí)。訂單的統(tǒng)計(jì)是按天來的時(shí)區(qū)一亂營業(yè)額統(tǒng)計(jì)就大概率偏移。第二個(gè)坑是邏輯刪除。很多表都加了deleted字段但查詢時(shí)稍不注意就把已刪除的分類也查出來了。我采取的方案是MyBatis配置全局的邏輯刪除字段并在業(yè)務(wù)層通過構(gòu)造查詢條件強(qiáng)制過濾。第三個(gè)坑是索引設(shè)計(jì)。訂單表數(shù)據(jù)量一旦上來如果沒對(duì)user_id和status建聯(lián)合索引按用戶查歷史訂單、按狀態(tài)列訂單都會(huì)走全表掃描。我的建議是order_no做唯一索引user_idstatus做聯(lián)合索引訂單創(chuàng)建時(shí)間的范圍查詢也要考慮到。3. 核心功能實(shí)現(xiàn)與實(shí)操要點(diǎn)3.1 用戶登錄與JWT鑒權(quán)流程外賣項(xiàng)目的用戶端登錄一般走微信登錄真實(shí)場景需要調(diào)用微信的code2session接口換取openid。做模擬項(xiàng)目的時(shí)候不需要真實(shí)的AppID所以我在項(xiàng)目里封裝了一個(gè)類似的登錄服務(wù)前端把模擬的code傳過來后端通過固定的配置類生成一個(gè)openid真實(shí)場景就是請求微信接口獲取再根據(jù)openid查用戶表不存在則自動(dòng)注冊新用戶最后生成JWT返回給前端。代碼層面JWT我用的是jjwt庫核心操作就是三塊生成Token、解析Token、校驗(yàn)過期時(shí)間。實(shí)際項(xiàng)目中我會(huì)自定義一個(gè)攔截器或者HandlerInterceptor對(duì)所有除登錄接口之外的路徑進(jìn)行校驗(yàn)。攔截器里從Header中取token解析成功就把用戶ID放到ThreadLocal或RequestContext里后續(xù)Controller直接取。這里有個(gè)關(guān)鍵點(diǎn)ThreadLocal用完后一定要remove否則線程池復(fù)用時(shí)會(huì)帶來數(shù)據(jù)串臺(tái)的隱患。我踩過的坑是第一次設(shè)計(jì)Header字段時(shí)叫“Authorization”但管理端和用戶端用了同一個(gè)攔截器導(dǎo)致用戶Token被當(dāng)成管理員Token用。后來我給兩個(gè)端分別定義了不同的鑒權(quán)攔截器且路徑前綴區(qū)分開/admin/**走管理端校驗(yàn)/user/**走用戶端校驗(yàn)。這個(gè)設(shè)計(jì)影響到后面所有模塊的聯(lián)調(diào)提前理清很有必要。3.2 購物車與下單鏈路購物車是這個(gè)系統(tǒng)中“高頻寫”的代表。用戶加購時(shí)前端把菜品ID和口味組合傳給后端后端先判斷Redis里有沒有對(duì)應(yīng)數(shù)據(jù)如果有數(shù)量加一如果沒有初始化一個(gè)新的購物車條目。用Hash結(jié)構(gòu)存儲(chǔ)時(shí)key可以用userIdfield用“菜品id_口味hash”這樣同一個(gè)菜加不同辣度可以分開計(jì)數(shù)也方便前端回顯購物車列表時(shí)聚合。下單鏈路我更想強(qiáng)調(diào)“一致性”。下單涉及的操作包括校驗(yàn)購物車非空、計(jì)算總金額、校驗(yàn)余額或模擬支付結(jié)果、生成訂單主表和明細(xì)表、清空購物車、扣減庫存如果做了庫存。這些操作必須放在同一個(gè)事務(wù)中否則一旦中途異常訂單表和庫存表就出現(xiàn)不一致。我在做庫存扣減時(shí)一開始用的是先查庫存再更新結(jié)果并發(fā)下單時(shí)出現(xiàn)超賣問題。后來改了SQL直接通過UPDATE語句“庫存購買數(shù)量”這個(gè)條件來扣減update dish set stock stock - #{num} where id #{id} and stock #{num}。受影響行數(shù)為0說明庫存不足需要回滾。這是我在實(shí)際項(xiàng)目中記憶很深刻的一個(gè)教訓(xùn)雖然蒼穹這套項(xiàng)目有些版本沒有做庫存字段但加上這個(gè)點(diǎn)會(huì)讓項(xiàng)目含金量更高。3.3 訂單狀態(tài)變更與消息推送訂單狀態(tài)變化之后用戶端需要實(shí)時(shí)看到。這里我沒有引入MQ做異步推送而是用WebSocket建立一個(gè)長連接商戶或管理端修改訂單狀態(tài)后服務(wù)端主動(dòng)把最新狀態(tài)推送給對(duì)應(yīng)的用戶端連接。實(shí)現(xiàn)步驟大致是后端引入WebSocket依賴編寫一個(gè)WebSocketServer的單例管理所有在線連接當(dāng)用戶登錄成功后前端通過握手請求把token傳上來服務(wù)端解析出userId后把Session映射到userId的Map中訂單狀態(tài)變更時(shí)根據(jù)訂單里的userId找到對(duì)應(yīng)連接并推送JSON消息比如推送訂單號(hào)、最新狀態(tài)、預(yù)計(jì)送達(dá)時(shí)間。如果不想引入WebSocket也可以前端定時(shí)輪詢訂單接口但這樣實(shí)時(shí)性差且壓力大。WebSocket的難度主要在于連接管理和心跳保活。我在本地測試時(shí)發(fā)現(xiàn)瀏覽器和服務(wù)器之間存在網(wǎng)絡(luò)代理時(shí)會(huì)自動(dòng)斷開連接所以寫了Ping/Pong心跳機(jī)制每30秒發(fā)一次心跳包維持連接。這個(gè)機(jī)制如果你不做線上環(huán)境很容易出現(xiàn)用戶那邊已經(jīng)斷開了服務(wù)端還不知道的情況。3.4 管理端數(shù)據(jù)統(tǒng)計(jì)與報(bào)表管理端有一個(gè)比較亮眼的功能是數(shù)據(jù)統(tǒng)計(jì)按時(shí)間區(qū)間展示營業(yè)額、訂單量、平均客單價(jià)、新增用戶數(shù)同時(shí)畫出訂單趨勢曲線。這里不需要額外引入復(fù)雜的BI工具用SQL聚合即可。我以營業(yè)額為例說明思路篩選條件包括訂單狀態(tài)已完成的訂單才計(jì)算營業(yè)額不能把待付款和已取消算進(jìn)去然后根據(jù)下單時(shí)間或支付時(shí)間所在日期進(jìn)行分組按天求出訂單金額總額。注意MySQL的DATE_FORMAT函數(shù)可以把datetime轉(zhuǎn)成yyyy-MM-dd的字符串再配合GROUP BY就能得到每日的營業(yè)額列表。類似的訂單趨勢統(tǒng)計(jì)是按天統(tǒng)計(jì)訂單數(shù)新增用戶數(shù)是按用戶表create_time日期分組訪問趨勢可能涉及UV/PV統(tǒng)計(jì)這時(shí)候Redis的HyperLogLog就派上用場了PFADD和PFCOUNT可以非常省內(nèi)存地去重計(jì)數(shù)。當(dāng)然如果只是為了演示效果用SQL直接做也行但你能說出HyperLogLog的原理面試官對(duì)你的工程認(rèn)知會(huì)明顯加分。4. 部署與環(huán)境搭建實(shí)錄4.1 本地開發(fā)環(huán)境準(zhǔn)備我先說本地跑起來的必要環(huán)境配置。JDK建議用Java 8或11Maven用3.6以上MySQL用5.7或8.0都可Redis用5.0以上。如果你的電腦還沒裝這些我建議用Docker一鍵跑MySQL和Redis可以省去很多環(huán)境變量的煩惱。比如執(zhí)行docker run -p 3306:3306 --name mysql -e MYSQL_ROOT_PASSWORD123456 -d mysql:5.7Windows上記得檢查端口占用本地開發(fā)時(shí)3306端口很容易被另一個(gè)殘留MySQL進(jìn)程占著連接不上時(shí)先netstat查一下。項(xiàng)目配置文件中需要修改的核心項(xiàng)是數(shù)據(jù)源和Redis地址。Spring Boot里我傾向于用application.yml統(tǒng)一管理。需要注意的一點(diǎn)是密碼和敏感信息不要寫死在git里至少應(yīng)該通過環(huán)境變量引用spring.redis.host${REDIS_HOST:localhost}這樣既方便本地跑也方便上線時(shí)用環(huán)境變量注入。4.2 前后端聯(lián)調(diào)中的協(xié)議約定這個(gè)項(xiàng)目涉及到小程序端和管理端前后端聯(lián)調(diào)是重頭戲。聯(lián)調(diào)之前必須先統(tǒng)一數(shù)據(jù)格式。我用的統(tǒng)一響應(yīng)結(jié)構(gòu)是{ code: 1, msg: success, data: {} }code為1表示成功0表示失敗。有人喜歡用200/500這種HTTP狀態(tài)碼來代表業(yè)務(wù)成功與否但我不太建議因?yàn)镠TTP狀態(tài)碼應(yīng)該表達(dá)傳輸層狀態(tài)而業(yè)務(wù)層狀態(tài)需要code字段來細(xì)分。前端拿到res.code后判斷是否等于1再?zèng)Q定拉取data還是彈出錯(cuò)誤信息。另外一個(gè)非常容易踩雷的是日期格式。后端返回的LocalDateTime默認(rèn)是一長串帶T的格式小程序端直接展示很難看。我在全局配置里加了Jackson的日期格式化統(tǒng)一為“yyyy-MM-dd HH:mm:ss”。還有金額字段返回時(shí)要小心精度丟失不能轉(zhuǎn)成double再傳給前端而是要保留字符串或整數(shù)形式比如以元為單位保留兩位小數(shù)或者前端用整數(shù)分單位解析避免浮點(diǎn)數(shù)精度問題。4.3 打包部署的常見問題本地運(yùn)行成功后要把后端打成jar包部署到Linux服務(wù)器。執(zhí)行mvn clean package后在target目錄下生成xxx.jar。部署時(shí)我用的是systemd服務(wù)或者直接nohup java -jar xxx.jar logs/log.log 21 。最常見的三個(gè)問題第一打包時(shí)test報(bào)錯(cuò)導(dǎo)致打包失敗可以在pom中跳過測試spring-boot-maven-plugin配置maven.test.skiptrue或者命令行加-Dmaven.test.skiptrue。第二端口沒開服務(wù)器安全組或防火墻擋住了8080端口導(dǎo)致前端請求連接超時(shí)檢查firewalld或者iptables規(guī)則。第三靜態(tài)資源路徑問題。管理端打包后的dist文件一般放在Nginx里Nginx配置api反向代理到后端的8080端口前端和后端之間不會(huì)跨域但如果你前端直接用devServer代理本地跑時(shí)也要確認(rèn)proxy表配置正確。5. 問題排查與避坑指南5.1 常見異常速查表我在做這個(gè)項(xiàng)目時(shí)整理過一張異常排錯(cuò)表現(xiàn)在直接分享出來異常表現(xiàn)常見原因解決方案登錄接口返回401JWT過期或簽名不匹配檢查token是否帶Bearer前綴再確認(rèn)攔截器排除路徑是否配錯(cuò)MySQL連接報(bào)CommunicationsException數(shù)據(jù)庫地址/端口/賬號(hào)錯(cuò)誤或連接池耗盡檢查yml配置注意時(shí)區(qū)參數(shù)重啟時(shí)確認(rèn)服務(wù)已啟動(dòng)Redis連接失敗Redis未啟動(dòng)或密碼錯(cuò)誤本地執(zhí)行ping檢查生產(chǎn)環(huán)境用redis-cli -a 密碼 ping驗(yàn)證中文亂碼數(shù)據(jù)庫連接字符集未配置UTF-8URL加useUnicodetruecharacterEncodingutf-8金額對(duì)不上使用了float/double全面改為BigDecimal并配置保留兩位小數(shù)前端報(bào)跨域后端未允許跨域或代理配置錯(cuò)誤開發(fā)環(huán)境用async的proxy線上用Nginx同源解決定時(shí)任務(wù)沒執(zhí)行未啟用EnableScheduling或cron表達(dá)式錯(cuò)誤檢查啟動(dòng)類注解和Cron表達(dá)式注意6位還是7位最隱蔽的問題是數(shù)據(jù)庫連接池連接泄露。如果有長連接查詢特別慢或者偶發(fā)無法獲取連接多半是事務(wù)沒提交或者連接未歸還。我會(huì)在yml里配置連接池監(jiān)控參數(shù)例如maximum-pool-size20minimum-idle5并開啟日志排查的時(shí)候能看清誰拿了連接不還。5.2 我踩過的幾個(gè)坑第一個(gè)坑是事務(wù)失效。在同一個(gè)類里調(diào)用帶Transactional的public方法時(shí)事務(wù)不會(huì)生效因?yàn)镾pring的代理對(duì)象調(diào)用的是this方法而不是代理方法。解決辦法是把需要事務(wù)的邏輯拆到另一個(gè)Service類中或者通過注入自身的代理對(duì)象調(diào)用。為了排查這種問題我會(huì)在配置里加上事務(wù)日志輸出到控制臺(tái)觀察異常后是否有rollback記錄。第二個(gè)坑是JSON序列化循環(huán)引用。菜品類里包含分類對(duì)象分類對(duì)象又包含菜品列表結(jié)果返回JSON時(shí)直接棧溢出。后來把關(guān)聯(lián)屬性用JsonIgnore或設(shè)置DTO只返回需要的字段。經(jīng)驗(yàn)就是實(shí)體類不要直接作為API響應(yīng)體一定要轉(zhuǎn)VO/DTO這既安全又靈活。第三個(gè)坑是MyBatis的動(dòng)態(tài)SQL字段里出現(xiàn)了下劃線映射問題。表字段是create_time實(shí)體屬性是createTime如果沒開啟mapUnderscoreToCamelCase查詢字段時(shí)為null。在配置文件里加上mybatis.configuration.map-underscore-to-camel-casetrue即可這個(gè)點(diǎn)能幫你省半小時(shí)。5.3 性能優(yōu)化與后續(xù)擴(kuò)展方向項(xiàng)目基本跑通后我又做了幾個(gè)方向上的優(yōu)化讓整個(gè)項(xiàng)目的完成度更高。緩存優(yōu)化方面店鋪營業(yè)狀態(tài)、分類列表這些讀多寫少的數(shù)據(jù)可以在每次查詢后寫入Redis并設(shè)置合理的過期時(shí)間例如5到10分鐘。新增或修改時(shí)主動(dòng)刪除對(duì)應(yīng)緩存保證緩存與數(shù)據(jù)庫最終一致。頁面端會(huì)出現(xiàn)數(shù)據(jù)短暫延遲但通??梢越邮?。接口優(yōu)化方面菜品分頁查詢時(shí)如果數(shù)據(jù)量大可以考慮用PageHelper分頁或者在SQL里做limit分頁。管理端訂單列表查詢使用了比較多的時(shí)間范圍篩選我建立了復(fù)合索引避免臨時(shí)文件排序。擴(kuò)展方向上可以給項(xiàng)目加入MQ來處理訂單超時(shí)消息比如用延遲隊(duì)列實(shí)現(xiàn)下單后15分鐘未支付自動(dòng)取消。還可以引入Elasticsearch做菜品搜索提高分類和菜品的查詢速度?,F(xiàn)在很多微服務(wù)架構(gòu)把蒼穹項(xiàng)目拆分成多個(gè)模塊比如認(rèn)證服務(wù)、訂單服務(wù)、商品服務(wù)這其實(shí)也是很好的學(xué)習(xí)路徑但先把單體項(xiàng)目吃透更重要。一點(diǎn)私人總結(jié)做完這個(gè)項(xiàng)目之后我最大的收獲是建立了對(duì)完整業(yè)務(wù)系統(tǒng)的認(rèn)知閉環(huán)。以前寫CRUD只是照著接口文檔填方法體而蒼穹外賣讓我真正思考了訂單在什么時(shí)機(jī)生成、購物車數(shù)據(jù)放在哪里、什么時(shí)候要加鎖、什么時(shí)候要用快照。從數(shù)據(jù)庫設(shè)計(jì)到鑒權(quán)攔截再到WebSocket推送鏈路完整每個(gè)環(huán)節(jié)都不是孤立的。如果你正在動(dòng)手做這個(gè)項(xiàng)目我建議你不要只滿足于“能跑起來”。把每一個(gè)狀態(tài)變更的日志打出來把每一次調(diào)用鏈路梳理清楚甚至可以自己畫一張時(shí)序圖。只要你把訂單主流程的所有細(xì)節(jié)都吃透了下次再去接觸分布式事務(wù)、消息隊(duì)列、微服務(wù)拆分這些主題會(huì)發(fā)現(xiàn)基礎(chǔ)已經(jīng)打好了。最后再分享一個(gè)小經(jīng)驗(yàn)這個(gè)項(xiàng)目做完記得用真實(shí)體驗(yàn)的心態(tài)去測試每一個(gè)按鈕你自己就是最好的測試人員。