:從數(shù)據(jù)結構選型到撮合引擎設計)
簡介這是基于Java Swing開發(fā)的股票交易模擬系統(tǒng)課程設計資源包面向需要完成數(shù)據(jù)結構課程設計、Java實訓或畢業(yè)設計的本科/高職學生。系統(tǒng)采用面向對象與MVC分層思想綜合運用數(shù)據(jù)庫存儲技術、鏈表與隊列數(shù)據(jù)結構、JFreechart開源圖表庫以及新浪股票行情接口實現(xiàn)行情揭示、賬戶管理、模擬買賣、走勢圖繪制、技術指標與策略分析等完整功能同時以隨機數(shù)模擬實時數(shù)據(jù)便于脫離真實行情環(huán)境獨立運行調試。資源包共187個文件約10.79MB包含54個Java源碼、20個依賴jar包、87個編譯后的class文件以及PPT答辯演示文稿、數(shù)據(jù)庫配置腳本、界面截圖和設計說明文檔代碼分層清晰可直接導入主流IDE學習與二次開發(fā)。已有758人學習下載適合用來掌握Swing圖形界面設計、數(shù)據(jù)庫連接、隨機數(shù)仿真、事件驅動編程等核心技能也可作為課程設計報告與答辯PPT的參考素材。1. Java Swing 股票交易模擬系統(tǒng)這個課程設計包真正值得花時間的部分每年數(shù)據(jù)結構課設都有人拿到類似「Java Swing 股票交易模擬系統(tǒng)源碼數(shù)據(jù)庫ppt」的壓縮包第一反應是裝上跑通、截圖交差。但這類 java 課程設計案例源碼遍地都是真正拉開分數(shù)差距的不是界面多好看而是你能不能把訂單排隊、行情緩存、持倉映射、賬戶并發(fā)扣款這些業(yè)務場景和數(shù)據(jù)結構課上的堆、隊列、哈希表、平衡樹逐一對上號。題目里帶著「數(shù)據(jù)結構」三個字評分點基本落在選型和復雜度上。這篇文章我會從整體設計拆到數(shù)據(jù)庫建表再講清楚撮合、行情刷新和線程安全怎么落地最后給你一套答辯前能直接用的驗證方法和避坑順序。2. 先搭框架再看代碼四層架構與每層該選什么數(shù)據(jù)結構2.1 依賴方向決定改代碼的位置UI、服務、數(shù)據(jù)三層為什么要物理拆開很多課設包的原始代碼是「一個 MainFrame 包天下」JTable 的渲染、行情生成循環(huán)、下單邏輯全塞在一個類里甚至監(jiān)聽器里直接寫 SQL。這種寫法不是不能跑而是你一旦被問到「某個功能改了會不會影響另一個功能」根本答不清楚依賴關系。我一般會先把代碼按四層拆開按下面的包結構落com.course.stock ├── view # JFrame、JTable、監(jiān)聽器只做展示和輸入收集 ├── service # 賬戶、撮合引擎、行情生成業(yè)務規(guī)則都在這里 ├── dao # JDBC 數(shù)據(jù)訪問負責落庫和查詢 └── domain # User、TradeOrder、DealRecord、Position 等實體依賴方向是單向的view 調 serviceservice 調 daodomain 是公共數(shù)據(jù)載體。service 層絕對不 import javax.swingdao 層也絕對不出現(xiàn)業(yè)務判斷。這樣分完之后你在答辯時可以說「UI 層的登錄按鈕只負責把用戶名密碼交給 service 去校驗數(shù)據(jù)庫連接方式換成連接池UI 層不需要改動」。這句話比功能演示更能證明你理解了分層。一個額外收益是調試成本下降。比如行情價格異常先看 service 層的生成邏輯不用在監(jiān)聽器里翻幾千行找數(shù)據(jù)到底從哪來的。對于課設這種「交付時可能被抽查代碼」的場景類之間職責清晰比代碼寫得花哨重要得多。2.2 訂單簿用優(yōu)先隊列而不是普通 List撮合復雜度差了一個量級股票模擬系統(tǒng)里最核心的數(shù)據(jù)結構是訂單簿。買盤按價格從高到低排隊賣盤按價格從低到高排隊同價按時間先后排隊。這個「按優(yōu)先級出隊」的語義天然是堆的活。用 ArrayList 存訂單每次撮合都要線性掃描找最優(yōu)價用 PriorityQueueJava 的優(yōu)先隊列底層是二叉堆插入和取最值都是 O(log n)。一次撮合要取買家最高價和賣家最低價各一次n 筆訂單下整體復雜度從 O(n2) 降到 O(n log n)。這個對比放在答辯里就是實打實的加分點。// 買盤價格高者優(yōu)先同價先到先得 QueueOrder buyOrders new PriorityQueue( (o1, o2) - o2.getPrice().compareTo(o1.getPrice()) ! 0 ? o2.getPrice().compareTo(o1.getPrice()) : o1.getTime().compareTo(o2.getTime()) ); // 賣盤價格低者優(yōu)先同價先到先得 QueueOrder sellOrders new PriorityQueue( (o1, o2) - o1.getPrice().compareTo(o2.getPrice()) ! 0 ? o1.getPrice().compareTo(o2.getPrice()) : o1.getTime().compareTo(o2.getTime()) );這里有兩個細節(jié)值得在代碼注釋里寫清楚。第一價格比較用 BigDecimal 的 compareTo 而不是 equals原因是 equals 還比較精度1.0 和 1.00 會被判為不同但它們在訂單簿里必須是一個價位。第二時間戳字段建議直接存 long 型的 System.currentTimeMillis()不需要在隊列里存 Date 對象做比較排序更快落庫時再轉 DATETIME。隊列初始化后撮合循環(huán)就很簡潔同時看兩個隊頭能成交就處理剩余量不能成交就退出。這個邏輯放到第 4 章展開這里先記住一個原則——PriorityQueue 只在內存里維護訂單順序訂單本身的持久化還在數(shù)據(jù)庫兩者不沖突。2.3 行情環(huán)存、持倉映射與自選列表三種典型數(shù)據(jù)結構的使用位置訂單簿之外還有三處數(shù)據(jù)結構選型容易在答辯時被追問。第一是行情快照。實時價格要被表格、K 線圖、持倉盈虧同時讀取用 ConcurrentHashMapString, Stock 存代碼到行情對象的映射讀多寫少比用 Hashtable 鎖整個表要合理。更新行情時只 put 單只股票其他股票的讀操作不會受阻塞。第二是 K 線滑動窗口。只保留最近 N 根分鐘 K 線新數(shù)據(jù)進來把最老的擠掉用數(shù)組實現(xiàn)的環(huán)形緩沖最合適。public class RingBuffer { private final double[] data; private int head; private int size; public RingBuffer(int capacity) { this.data new double[capacity]; } public void add(double value) { data[(head size) % data.length] value; if (size data.length) { head (head 1) % data.length; // 滿了就覆蓋最老的一格 } else { size; } } public double avg() { double sum 0; for (int i 0; i size; i) { sum data[(head i) % data.length]; } return size 0 ? 0 : sum / size; } }用它算最近 N 分鐘的均價復雜度是 O(1)不需要每次重新遍歷整個歷史表。如果把「均價高于實時價提示風險」做成一個小標簽放在界面里這一處設計足夠支撐一個追問「你這里為什么不用 LinkedList」答案就是數(shù)組索引定位 O(1)、GC 壓力小且容量固定。第三是持倉映射。默認用 HashMapString, Position 以股票代碼為鍵取持倉 O(1)。但如果自選股面板需要按漲跌幅排序展示不要在 HashMap 上強行排序而是排序時把 entrySet 拉出來丟進臨時列表用 Collections.sort。原因是持倉數(shù)量通常是個位數(shù)到幾十個數(shù)量級排序成本可忽略沒必要為了排序引入一個始終維護有序性的 TreeMap 把寫操作拖慢。3. 數(shù)據(jù)庫表設計與 JDBC 落地把行情、訂單、持倉變成可對賬的賬3.1 建表腳本六張表和它們的字段語義模擬交易系統(tǒng)再怎么簡化「用戶、賬戶、股票、委托、成交、K 線」這六類數(shù)據(jù)一樣都不能少。賬戶和用戶拆開是為了以后擴展「一個用戶多個模擬賬戶」留余地。委托表和成交表拆開是因為一筆委托可能被拆成多筆成交這是真實撮合語義。CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(64) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_account ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, available_balance DECIMAL(18,2) DEFAULT 1000000.00, frozen_balance DECIMAL(18,2) DEFAULT 0.00, CONSTRAINT fk_account_user FOREIGN KEY (user_id) REFERENCES t_user(id) ); CREATE TABLE t_stock ( code VARCHAR(10) PRIMARY KEY, name VARCHAR(20) NOT NULL, last_price DECIMAL(10,2) NOT NULL, updated_at DATETIME ); CREATE TABLE t_trade_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, stock_code VARCHAR(10) NOT NULL, side TINYINT NOT NULL COMMENT 1買 2賣, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, status TINYINT NOT NULL COMMENT 0未成交 1部分成交 2全部成交 3已撤單, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_deal_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, stock_code VARCHAR(10) NOT NULL, deal_price DECIMAL(10,2) NOT NULL, deal_quantity INT NOT NULL, deal_amount DECIMAL(18,2) NOT NULL, deal_time DATETIME DEFAULT CURRENT_TIMESTAMP );DECIMAL(18,2) 是故意選的。模擬盤初始資金 100 萬單股價格按 10200 元算買一萬股剛好 200 萬DECIMAL(18,2) 足夠覆蓋且不會出現(xiàn) double 那種 0.10.2 不等于 0.3 的精度問題。t_trade_order 要建索引在 (user_id, status) 上因為「查我的未成交委托」是最頻繁的查詢。t_deal_record 的 order_id 保留是為了演示時點開一筆委托能看到它被拆成了幾次成交。K 線表按分鐘落庫就夠了字段是 stock_code、kline_time、open、high、low、close、volume聯(lián)合主鍵 (stock_code, kline_time)。這筆數(shù)據(jù)不用實時寫每 N 秒由內存里的環(huán)形緩沖聚合一次再批插入避免高頻行情把數(shù)據(jù)庫拖垮。3.2 JDBC 工具類、事務提交與連接參數(shù)先能跑再談優(yōu)化課設階段不需要上數(shù)據(jù)庫連接池一個簡單的 JDBC 工具類足夠。但連接參數(shù)里有兩個坑必須提前避開MySQL 8 的驅動類名是 com.mysql.cj.jdbc.Driver不再是 com.mysql.jdbc.DriverserverTimezone 必須顯式指定否則會拋時區(qū)異常。public class DbUtil { private static final String URL jdbc:mysql://localhost:3306/stock_sim ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement st, ResultSet rs) { if (rs ! null) try { rs.close(); } catch (SQLException ignored) {} if (st ! null) try { st.close(); } catch (SQLException ignored) {} if (conn ! null) try { conn.close(); } catch (SQLException ignored) {} } }寫入操作一定要手動控制事務。最典型的場景是「買入成交」涉及三張表成交記錄插入、賬戶余額扣減、訂單狀態(tài)更新。任何一個失敗賬就對不上了。Connection conn DbUtil.getConnection(); try { conn.setAutoCommit(false); // 1. 扣減賬戶余額 // 2. 更新訂單狀態(tài)為全部成交 // 3. 插入成交記錄 conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); DbUtil.close(conn, null, null); }連接池的話題可以在答辯時提一句「生產環(huán)境會用連接池管理連接課設里為了讓你看清鏈路所以用 DriverManager」。但前提是你在配置文件里留了 db.properties 而不是把用戶名密碼寫死在常量類里。常見的做法是 Properties.load 讀取這里就不再貼完整代碼知道這個方向即可。3.3 拿到 zip 后從導入到第一條記錄落庫的完整路徑壓縮包解壓后先用一小時做環(huán)境核對而不是直接雙擊運行腳本。我見過的翻車大半發(fā)生在「IDEA 里 Run 起來報 ClassNotFoundException」或者「表結構對不上」。按下面順序操作用 Navicat 或命令行執(zhí)行建庫腳本庫名用 stock_sim核對 SQL 腳本里的 CREATE DATABASE 語句是否包含字符集設置打開 db.properties把 jdbc.url、username、password 改為本機實際值在 IDEA 中打開項目確認 Project SDK 是 1.8 或更高Language level 與編譯版本一致在 Project Structure 的 Libraries 里把 mysql-connector-java 的 jar 包加進來確認它出現(xiàn)在最終 artifact 里找到帶 main 方法的入口類通常是 LoginFrame 或 MainFrame先跑一次跑通后做一次冒煙驗證注冊一個用戶、查看初始資金是否為 100 萬、下買入單、去數(shù)據(jù)庫查 t_trade_order 是否多了一行。這條鏈路通了后面所有演示環(huán)節(jié)才談得上。4. 核心交易邏輯實現(xiàn)撮合、行情刷新與線程安全4.1 撮合引擎價格優(yōu)先、時間優(yōu)先的完整成交流程撮合引擎是整個系統(tǒng)里最有講頭的一段代碼。它的輸入是兩個優(yōu)先隊列——買盤隊列和賣盤隊列循環(huán)邏輯一句話概括買一價格 賣一價格就能成交成交量取兩者剩余量的較小值成交價取賣一價成交后把剩余量放回原隊列繼續(xù)比對。public ListDeal match(QueueOrder buys, QueueOrder sells) { ListDeal deals new ArrayList(); while (!buys.isEmpty() !sells.isEmpty()) { Order buy buys.peek(); Order sell sells.peek(); if (buy.getPrice().compareTo(sell.getPrice()) 0) { break; // 買方最高價夠不到賣方最低價 } int quantity Math.min(buy.getRemaining(), sell.getRemaining()); Deal deal new Deal(); deal.setBuyOrderId(buy.getId()); deal.setSellOrderId(sell.getId()); deal.setStockCode(buy.getStockCode()); deal.setPrice(sell.getPrice()); // 成交價按賣一價 deal.setQuantity(quantity); deals.add(deal); buy.setRemaining(buy.getRemaining() - quantity); sell.setRemaining(sell.getRemaining() - quantity); if (buy.getRemaining() 0) buys.poll(); if (sell.getRemaining() 0) sells.poll(); // 沒成交完的訂單留在隊里下一輪繼續(xù)比較 } return deals; }成交價按賣一價而不是買一價這是模擬盤常用的簡化規(guī)則真實交易所的撮合中間價規(guī)則更復雜。你在答辯時主動說出「我這個模型做了簡化真實場景要考慮中間價和最小變動價位」比等評委來問要主動得多。要注意的是這里的 peek 不會移除訂單只有剩余量歸零才 poll。如果買單一筆很大可能連續(xù)吃掉好幾檔賣單每吃一檔就產生一條 Deal 記錄正好對應數(shù)據(jù)庫里 t_deal_record 的一行。將這次循環(huán)產生的 List 批量入庫不要一條條 commit。4.2 行情生成與界面回顯SwingWorker 把價格刷新的結果送回 EDT行情數(shù)據(jù)在模擬系統(tǒng)里通常是一個后臺線程循環(huán)更新。這里最容易犯的錯誤是直接在守護線程里調用 table.setValueAt——Swing 組件不是線程安全的任何 UI 操作必須在事件分發(fā)線程EDT上執(zhí)行。直接別處改 UI 的后果是界面偶發(fā)閃爍、半刷新甚至直接拋異常。class PriceFeedWorker extends SwingWorkerVoid, PriceTick { private final Random random new Random(); Override protected Void doInBackground() throws Exception { while (!isCancelled()) { Thread.sleep(1000); ListStock stocks stockDao.findAll(); ListPriceTick ticks new ArrayList(); for (Stock s : stocks) { double change (random.nextDouble() - 0.5) * 2 * 0.02; BigDecimal newPrice s.getLastPrice() .multiply(BigDecimal.valueOf(1 change)) .setScale(2, RoundingMode.HALF_UP); PriceTick tick new PriceTick(s.getCode(), newPrice); ticks.add(tick); latestPrices.put(s.getCode(), newPrice); } publish(ticks.toArray(new PriceTick[0])); } return null; } Override protected void process(ListPriceTick chunks) { // 此方法一定運行在 EDT 上可以安全更新表格 for (PriceTick tick : chunks) { stockTableModel.updatePrice(tick.getCode(), tick.getPrice()); } } }SwingWorker 的 publish/process 機制就是設計出來解決這個問題的。doInBackground 在后臺線程跑循環(huán)process 匯總后切回 EDT 批量更新界面。注意 setScale(2, RoundingMode.HALF_UP) 保證價格只保留兩位小數(shù)。還有一點我踩過坑后臺線程里每秒查一次全表股票雖然不是大問題但明顯浪費。更合理的做法是啟動時把股票列表加載進內存的 ConcurrentHashMap后臺線程只更新內存對象定時任務再批量把快照寫回數(shù)據(jù)庫。數(shù)據(jù)庫是模擬系統(tǒng)的「存檔」不是行情數(shù)據(jù)的實時通道。4.3 賬戶與持倉的并發(fā)控制synchronized 鎖在哪個對象上模擬盤允許同時開啟多個窗口下單或者一個窗口里連續(xù)快速點擊買入。如果賬戶扣款和持倉變更不清理并發(fā)「余額扣了但持倉沒到賬」「賣出數(shù)量超過持倉」這些問題就會冒出來。這其實是 java 面試八股文里經典的并發(fā)問題換了個皮膚。public synchronized TradeResult buy(Order order, Account account) { BigDecimal cost order.getPrice().multiply(BigDecimal.valueOf(order.getQuantity())); if (account.getAvailableBalance().compareTo(cost) 0) { return TradeResult.fail(可用余額不足需要 cost 實際 account.getAvailableBalance()); } account.setAvailableBalance(account.getAvailableBalance().subtract(cost)); Position position positions.get(order.getStockCode()); if (position null) { position new Position(order.getStockCode(), BigDecimal.ZERO, BigDecimal.ZERO); positions.put(order.getStockCode(), position); } position.setQuantity(position.getQuantity() order.getQuantity()); position.setCostAmount(position.getCostAmount().add(cost)); return TradeResult.ok(); }把整個買入動作鎖在方法上聽著粗暴但在單機模擬盤這個場景下是正確的取舍。課設系統(tǒng)沒有微服務沒有分布式業(yè)務都在一個 JVM 里方法級鎖足夠保證賬戶余額和持倉的一致性。如果你用鎖對象注意別鎖字符串常量兩個不同的 key 如果值相同會互相同步造成無意義的等待。余額不足的校驗一定要放在扣款之前并且校驗和扣款必須處于同一把鎖下。先校驗再放鎖再扣款中間另一個線程插進來余額就被透支了。這個「檢查和動作之間要有原子性」的結論值得在答辯論證時反復強調。5. 翻車點避坑與排查五個常見問題的現(xiàn)象、原因和解決順序5.1 界面卡死登錄按鈕點下去整個窗口白屏現(xiàn)象點擊登錄后 JFrame 無響應轉圈或者白屏過幾秒恢復嚴重時直接 NotResponding。原因登錄按鈕的監(jiān)聽器里直接執(zhí)行了數(shù)據(jù)庫查詢而鼠標事件回調運行在 EDT 上一條慢 SQL 或網(wǎng)絡抖動就把界面線程堵住了。數(shù)據(jù)量一大這個卡頓就越明顯。解決所有可能耗時超過幾十毫秒的操作丟進 SwingWorker 或普通線程結果回來后再用 SwingUtilities.invokeLater 或 worker 的 process 刷新界面。登錄按鈕里只做「啟動登錄任務」這一個動作不要在里面查庫。把這條寫好評委的「你的界面為什么流暢」就有答案了。5.2 連不上數(shù)據(jù)庫ClassNotFoundException 或 CommunicationsException現(xiàn)象點登錄立刻報錯控制臺出現(xiàn) java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver或者 Communications link failure。原因前者是驅動 jar 沒被正確打進運行環(huán)境后者八成是 MySQL 版本和 URL 參數(shù)不匹配。MySQL 5.7 用 com.mysql.jdbc.Driver 還能過MySQL 8 必須用 com.mysql.cj.jdbc.Driver 且 URL 里加 serverTimezone。解決先確認用的驅動 jar 版本。IDE 里添加完 Library 后去 Artifacts 里勾選 include in project build很多 IDEA 項目里 Library 存在但打包時沒帶上。連接參數(shù)統(tǒng)一走 db.properties排查時只需要改一個文件不要在代碼里到處找硬編碼的 URL。5.3 賣出金額和買入成本對不上賬持倉成本越算越亂現(xiàn)象演示買入 100 股、賣出 100 股后賬戶總資產不等于初始資金差額正好是買入賣出的某個百分比。原因用 double 存價格和金額累加多次后精度丟失。比如 19.99 元的價格在 double 里實際是 19.989999999999998乘上股數(shù)放大后誤差變成幾毛錢再經過幾筆交易就完全對不上。解決價格、金額、余額全部用 BigDecimal構造時用字符串構造器 new BigDecimal(19.99)不要用 new BigDecimal(19.99)。JDBC 讀取 DECIMAL 字段時用 getBigDecimal 拿到就是 BigDecimal不要轉成 double 再轉回來。這是所有對賬問題的根源。5.4 注冊成功后列表不刷新偶爾空指針現(xiàn)象新用戶注冊完成頁面上的用戶列表沒有新數(shù)據(jù)點擊某些行還會報 NullPointerException。原因注冊成功后的數(shù)據(jù)加載仍然執(zhí)行在舊邏輯里拿的是注冊前的列表引用或者界面刷新和后臺數(shù)據(jù)加載的時序不對窗口還沒完成初始化數(shù)據(jù)觸達 UI 時表格模型還是 null。解決數(shù)據(jù)變更后統(tǒng)一走「重新查詢→構建新的 TableModel→setModel 刷新」這一條路徑。空指針排查時先打印堆??词悄膫€對象為 null八成是領域對象沒有初始化例如 new ArrayList() 沒賦值就 add。不要在 getter 里返回 null集合屬性在聲明處直接初始化。5.5 快速雙擊下單持倉出現(xiàn)負數(shù)現(xiàn)象快速點擊買入按鈕明明余額足夠買 100 股最終持倉變成 -20 股或者余額變負數(shù)。原因兩次點擊事件都在余額校驗完成前進入了扣款邏輯或者賣出時沒有檢查當前持倉量。數(shù)據(jù)庫層面沒有約束程序層面也沒有加鎖負庫存就出現(xiàn)了。解決下單入口加 synchronized 見 4.3 的寫法賣出在下單前先查 position.getQuantity()不足直接拒絕。另外可以在 t_account 表加 CHECK (available_balance 0)MySQL 8.0.16 以后會強制生效作為最后一層防線。程序鎖加數(shù)據(jù)庫約束雙層保護演示時再怎么點都不怕。6. 答辯前只練這三件事數(shù)據(jù)驗證、演示節(jié)奏與加分說辭答辯演示前我會先跑一遍對賬 SQL確認賬是平的。賬不平的模擬盤演示出去功能再多也圓不回來。對賬思路不復雜用戶的「當前可用余額 各持倉市值 所有成交扣費」應該等于初始資金。SQL 寫成這樣SELECT u.username, a.available_balance, SUM(p.quantity * s.last_price) AS market_value, a.available_balance SUM(p.quantity * s.last_price) AS total_assets FROM t_user u JOIN t_account a ON u.id a.user_id LEFT JOIN t_position p ON p.user_id u.id LEFT JOIN t_stock s ON s.code p.stock_code GROUP BY u.id;如果每個用戶的 total_assets 都等于初始資金說明撮合、扣款、持倉更新是一致的。這條 SQL 跑通你演示時心里有底。演示節(jié)奏我習慣控制在十分鐘第一分鐘界面行情刷新動起來讓評委看到價格在每秒跳動中間五分鐘做一筆完整買入和一筆賣出強調撮合隊列的價格優(yōu)先和時間優(yōu)先最后兩分鐘打開數(shù)據(jù)庫對著 t_trade_order 和 t_deal_record 展示買賣數(shù)據(jù)是怎么落庫的。這比反復切窗口講功能更能體現(xiàn)數(shù)據(jù)結構課程的訓練痕跡。加分說辭記得準備兩個。一是撮合隊列為什么用堆而不用數(shù)組線性掃描答「ArrayDeque 和 List 得 O(n) 掃描PriorityQueue 插入和取最值 O(log n)」二是 ConcurrentHashMap 和 Hashtable 的差別答「鎖粒度不同前者鎖桶后者鎖整表」。這類問題就是常見的 java 基礎面試題變體提前背熟被追問不慌。我自己的習慣是正式演示前至少完整過三遍流程中間故意制造一次「余額不足」的失敗彈窗證明異常路徑也處理了。這一手比單純展示順利路徑更讓評委相信系統(tǒng)是你自己寫的。希望幫到你。本文還有配套的精品資源點擊獲取