動(dòng)的智能選課系統(tǒng):事務(wù)級課程沖突與先修校驗(yàn)實(shí)現(xiàn))
簡介本資源是一套基于SSM框架開發(fā)的MySQL學(xué)生智能選課系統(tǒng)完整畢業(yè)設(shè)計(jì)資料面向計(jì)算機(jī)類本科生、Java初學(xué)者及數(shù)據(jù)庫課程實(shí)踐者聚焦校園教務(wù)管理中的選課效率低、信息交互滯后等現(xiàn)實(shí)問題。系統(tǒng)實(shí)現(xiàn)學(xué)生端課程智能推薦與自主選課、教師端課程發(fā)布與課表管理、管理員端用戶與數(shù)據(jù)維護(hù)等核心功能兼顧界面直觀性、操作便捷性與跨終端訪問能力。壓縮包為RAR格式大小52.04MB包含源碼JavaJSPSpringMyBatis、MySQL數(shù)據(jù)庫腳本含建表語句與初始數(shù)據(jù)、畢業(yè)論文含需求分析、系統(tǒng)設(shè)計(jì)、測試用例與答辯要點(diǎn)三大主體內(nèi)容結(jié)構(gòu)清晰、開箱即用。目前已有52人學(xué)習(xí)下載讀者可直接導(dǎo)入IDE運(yùn)行調(diào)試復(fù)現(xiàn)完整業(yè)務(wù)流程結(jié)合論文深入理解MVC分層設(shè)計(jì)思想通過SQL腳本快速搭建本地?cái)?shù)據(jù)庫環(huán)境是開展課程設(shè)計(jì)、畢設(shè)開發(fā)與Java Web綜合實(shí)訓(xùn)的高實(shí)用性參考方案。1. 學(xué)生智能選課系統(tǒng)不是“加個(gè)推薦按鈕”就叫智能而是用 MySQL 把課程沖突、先修關(guān)系、容量閾值全壓進(jìn)事務(wù)里跑通你見過那種“智能選課系統(tǒng)”嗎前端彈個(gè)“為您推薦”的下拉框后端查個(gè)SELECT * FROM course WHERE statusopen就完事——這不叫智能這叫帶搜索的課程列表。真正的學(xué)生智能選課系統(tǒng)核心不在算法多炫而在業(yè)務(wù)邏輯能不能在數(shù)據(jù)庫層被原子化、可驗(yàn)證、不可繞過地執(zhí)行。它要實(shí)時(shí)攔住學(xué)生選已滿員的課、跨學(xué)院未授權(quán)的課、沒修完先修課的課要讓教務(wù)老師調(diào)一個(gè)參數(shù)比如某課限選人數(shù)從60調(diào)到80系統(tǒng)立刻生效且不引發(fā)并發(fā)選課時(shí)的超賣還要支撐導(dǎo)出符合教務(wù)規(guī)范的選課結(jié)果報(bào)表字段對得上、時(shí)間戳準(zhǔn)、狀態(tài)鏈可追溯。這個(gè).rar包里的 MySQL 實(shí)現(xiàn)恰恰是用原生約束、存儲(chǔ)過程和事務(wù)隔離級別把“選課”這件事從應(yīng)用層黑匣子拉回?cái)?shù)據(jù)庫可審計(jì)、可壓測、可 rollback 的確定性世界。適合正在做課程設(shè)計(jì)、畢設(shè)或教務(wù)系統(tǒng)二次開發(fā)的開發(fā)者——尤其當(dāng)你發(fā)現(xiàn) Spring Boot 里寫一堆Transactional還總在搶課高峰出錯(cuò)時(shí)該回頭看看 MySQL 本身能扛多少。2. 用 MySQL 建模選課核心實(shí)體從 ER 圖到帶約束的建表語句為什么course_prerequisite必須是復(fù)合主鍵2.1 業(yè)務(wù)實(shí)體拆解哪些表不能少哪些字段必須帶約束一個(gè)能落地的選課系統(tǒng)MySQL 表結(jié)構(gòu)必須直擊三個(gè)剛性需求身份強(qiáng)綁定、依賴可追溯、狀態(tài)可凍結(jié)。我們不建user表復(fù)用學(xué)校統(tǒng)一認(rèn)證但必須有student含學(xué)號主鍵、學(xué)院ID外鍵、年級、是否畢業(yè)班影響選課窗口期course課程號主鍵、課名、學(xué)分、開課學(xué)院ID、最大容量、當(dāng)前已選人數(shù)TINYINT UNSIGNED非計(jì)算字段、狀態(tài)ENUM(open,closed,audit)course_prerequisite課程號 先修課程號聯(lián)合主鍵無自增ID——這是關(guān)鍵避免同一對課程重復(fù)錄入student_course_selection學(xué)號課程號聯(lián)合主鍵、選課時(shí)間DATETIME(3)、狀態(tài)ENUM(selected,dropped,waitlisted)、審核人可為空提示current_enrollment_count字段必須存在且為TINYINT UNSIGNED。別信“用COUNT(*)實(shí)時(shí)查”高并發(fā)下必然超賣。我們靠事務(wù)內(nèi)UPDATE course SET current_enrollment_count current_enrollment_count 1原子更新再配合唯一索引攔截重復(fù)插入。2.2 建表腳本帶注釋的最小可行集含外鍵與檢查約束-- 學(xué)院表簡化版實(shí)際應(yīng)關(guān)聯(lián)學(xué)校組織架構(gòu) CREATE TABLE department ( dept_id CHAR(4) PRIMARY KEY COMMENT 學(xué)院代碼如CS01, dept_name VARCHAR(50) NOT NULL ); -- 學(xué)生表 CREATE TABLE student ( stu_id CHAR(10) PRIMARY KEY COMMENT 學(xué)號全局唯一, dept_id CHAR(4) NOT NULL, grade YEAR NOT NULL COMMENT 入學(xué)年份, is_graduate TINYINT(1) DEFAULT 0 COMMENT 1畢業(yè)班選課期提前結(jié)束, FOREIGN KEY (dept_id) REFERENCES department(dept_id) ); -- 課程表 CREATE TABLE course ( course_id CHAR(8) PRIMARY KEY COMMENT 課程號如CS101001, course_name VARCHAR(100) NOT NULL, credit TINYINT UNSIGNED NOT NULL COMMENT 學(xué)分1-6, dept_id CHAR(4) NOT NULL COMMENT 開課學(xué)院, max_capacity TINYINT UNSIGNED NOT NULL DEFAULT 60, current_enrollment_count TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 實(shí)時(shí)計(jì)數(shù)非計(jì)算字段, status ENUM(open,closed,audit) NOT NULL DEFAULT open, CHECK (current_enrollment_count max_capacity), FOREIGN KEY (dept_id) REFERENCES department(dept_id) ); -- 先修關(guān)系表必須用聯(lián)合主鍵禁止冗余 CREATE TABLE course_prerequisite ( course_id CHAR(8) NOT NULL COMMENT 本課程號, prereq_id CHAR(8) NOT NULL COMMENT 先修課程號, PRIMARY KEY (course_id, prereq_id), FOREIGN KEY (course_id) REFERENCES course(course_id) ON DELETE CASCADE, FOREIGN KEY (prereq_id) REFERENCES course(course_id) ON DELETE RESTRICT ); -- 選課主表聯(lián)合主鍵確保一人一課只存一條記錄 CREATE TABLE student_course_selection ( stu_id CHAR(10) NOT NULL, course_id CHAR(8) NOT NULL, selection_time DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3), status ENUM(selected,dropped,waitlisted) NOT NULL DEFAULT selected, approved_by CHAR(10) NULL COMMENT 審核人學(xué)號僅用于audit狀態(tài), PRIMARY KEY (stu_id, course_id), FOREIGN KEY (stu_id) REFERENCES student(stu_id) ON DELETE CASCADE, FOREIGN KEY (course_id) REFERENCES course(course_id) ON DELETE RESTRICT );關(guān)鍵參數(shù)說明DATETIME(3)毫秒級精度解決同一毫秒內(nèi)多人提交的排序問題TINYINT UNSIGNED課程容量通?!?00用TINYINT節(jié)省內(nèi)存UNSIGNED防負(fù)數(shù)ON DELETE RESTRICT在course_prerequisite上刪課程前必須手動(dòng)清空其先修關(guān)系防邏輯斷裂CHECK (current_enrollment_count max_capacity)MySQL 8.0.16 支持是超賣的第一道防線。3. 選課核心邏輯落地用存儲(chǔ)過程封裝“選課”原子操作為什么SELECT ... FOR UPDATE不能少3.1 選課全流程的四步原子事務(wù)含先修校驗(yàn)與容量控制真實(shí)選課不是簡單插一條記錄。它必須在一個(gè)事務(wù)內(nèi)完成① 檢查學(xué)生是否在選課開放期查student.grade和系統(tǒng)配置表② 檢查課程狀態(tài)是否為open③檢查先修課是否已通過查student_course_selection中該課狀態(tài)為selected或approved_by非空④檢查容量是否充足并原子更新current_enrollment_count⑤ 插入選課記錄。這五步若拆成應(yīng)用層多次查詢更新必然在步驟③和④之間被并發(fā)請求穿透。正確做法是全部塞進(jìn)一個(gè)存儲(chǔ)過程中用SELECT ... FOR UPDATE鎖住目標(biāo)課程行。3.2 核心存儲(chǔ)過程proc_select_course含詳細(xì)注釋DELIMITER $$ CREATE PROCEDURE proc_select_course( IN p_stu_id CHAR(10), IN p_course_id CHAR(8), OUT p_result_code INT, OUT p_result_msg VARCHAR(200) ) BEGIN DECLARE v_course_status ENUM(open,closed,audit); DECLARE v_max_cap TINYINT UNSIGNED; DECLARE v_curr_count TINYINT UNSIGNED; DECLARE v_prereq_count INT DEFAULT 0; DECLARE v_is_graduate TINYINT(1) DEFAULT 0; -- 初始化返回值 SET p_result_code -1; SET p_result_msg 未知錯(cuò)誤; -- 步驟1檢查學(xué)生是否存在且未畢業(yè)簡化畢業(yè)班邏輯 SELECT is_graduate INTO v_is_graduate FROM student WHERE stu_id p_stu_id; IF v_is_graduate IS NULL THEN SET p_result_msg 學(xué)生不存在; LEAVE proc_body; END IF; -- 步驟2用 FOR UPDATE 鎖住課程行防止并發(fā)修改 SELECT status, max_capacity, current_enrollment_count INTO v_course_status, v_max_cap, v_curr_count FROM course WHERE course_id p_course_id FOR UPDATE; -- 關(guān)鍵鎖住這一行其他事務(wù)必須等待 -- 步驟3檢查課程狀態(tài) IF v_course_status ! open THEN SET p_result_msg CONCAT(課程狀態(tài)不可選, v_course_status); LEAVE proc_body; END IF; -- 步驟4檢查先修課查 student_course_selection 中 prereq_id 對應(yīng)記錄 SELECT COUNT(*) INTO v_prereq_count FROM course_prerequisite cp INNER JOIN student_course_selection scs ON cp.prereq_id scs.course_id AND scs.stu_id p_stu_id WHERE cp.course_id p_course_id AND scs.status IN (selected, waitlisted); -- waitlisted 視為已滿足先修 -- 若存在先修關(guān)系但學(xué)生未選任何先修課則失敗 IF v_prereq_count 0 THEN SELECT COUNT(*) INTO v_prereq_count FROM course_prerequisite WHERE course_id p_course_id; IF v_prereq_count 0 THEN SET p_result_msg 未滿足先修課程要求; LEAVE proc_body; END IF; END IF; -- 步驟5檢查容量并原子更新 IF v_curr_count v_max_cap THEN SET p_result_msg 課程已滿員; LEAVE proc_body; END IF; -- 步驟6更新課程計(jì)數(shù)原子操作 UPDATE course SET current_enrollment_count current_enrollment_count 1 WHERE course_id p_course_id; -- 步驟7插入選課記錄 INSERT INTO student_course_selection (stu_id, course_id) VALUES (p_stu_id, p_course_id); -- 成功 SET p_result_code 0; SET p_result_msg 選課成功; END$$ DELIMITER ;邏輯說明與參數(shù)說明FOR UPDATE是靈魂它鎖住course表中p_course_id對應(yīng)的整行后續(xù)所有對該行的SELECT ... FOR UPDATE或UPDATE都會(huì)阻塞直到本事務(wù)提交或回滾先修檢查用COUNT(*)而非EXISTS因需區(qū)分“無先修要求”v_prereq_count0和“有先修但未滿足”v_prereq_count0但course_prerequisite中存在記錄OUT參數(shù)p_result_code0成功-1失敗方便應(yīng)用層直接判斷避免解析錯(cuò)誤消息未顯式START TRANSACTIONMySQL 存儲(chǔ)過程默認(rèn)在自動(dòng)提交關(guān)閉時(shí)開啟隱式事務(wù)此處安全。4. 避坑指南選課系統(tǒng)上線前必踩的 4 個(gè) MySQL 坑第 3 個(gè)讓某高校系統(tǒng)凌晨三點(diǎn)熔斷4.1 現(xiàn)象選課高峰期大量“課程已滿員”誤報(bào)但后臺(tái)查current_enrollment_count明顯小于max_capacity原因應(yīng)用層用了READ COMMITTED隔離級別而proc_select_course中SELECT ... FOR UPDATE在REPEATABLE READ下才保證鎖行有效。READ COMMITTED下FOR UPDATE只鎖索引記錄不鎖間隙gap lock導(dǎo)致幻讀——兩個(gè)事務(wù)同時(shí)查到v_curr_count59都以為能進(jìn)結(jié)果都執(zhí)行UPDATE最終current_enrollment_count變成 61。解決MySQL 配置文件中強(qiáng)制設(shè)置transaction_isolation REPEATABLE-READ或在連接池初始化 SQL 中執(zhí)行SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ。4.2 現(xiàn)象學(xué)生退課后current_enrollment_count沒減導(dǎo)致后續(xù)無法選同課程原因退課邏輯沒走存儲(chǔ)過程而是應(yīng)用層直接DELETE FROM student_course_selection漏掉了對course.current_enrollment_count的UPDATE。解決退課也必須封裝為存儲(chǔ)過程proc_drop_course且內(nèi)部用UPDATE course SET current_enrollment_count current_enrollment_count - 1 WHERE course_id ?同樣加FOR UPDATE。4.3 現(xiàn)象某學(xué)院批量導(dǎo)入新課后所有選課請求卡死SHOW PROCESSLIST顯示大量Waiting for table metadata lock原因?qū)肽_本用ALTER TABLE course ADD COLUMN xxx在線加字段觸發(fā)了 MDLMetadata Lock全表鎖。而選課事務(wù)中的SELECT ... FOR UPDATE需要獲取表的MDL_SHARED_WRITE鎖與ALTER的MDL_EXCLUSIVE沖突所有選課請求排隊(duì)等待。解決新課導(dǎo)入改用INSERT INTO course (...) VALUES (...)批量插入若真需加字段用pt-online-schema-change工具或安排在選課低峰期執(zhí)行。4.4 現(xiàn)象course_prerequisite表數(shù)據(jù)異常出現(xiàn)(CS101001, CS101001)自循環(huán)先修原因應(yīng)用層插入先修關(guān)系時(shí)沒做course_id ! prereq_id校驗(yàn)且表上沒加CHECK約束。解決立即執(zhí)行ALTER TABLE course_prerequisite ADD CONSTRAINT chk_no_self_loop CHECK (course_id ! prereq_id);并清理臟數(shù)據(jù)。MySQL 8.0.16 支持此語法。注意所有FOR UPDATE語句必須出現(xiàn)在事務(wù)的最開始且鎖的行要盡可能少——不要SELECT * FROM course FOR UPDATE而要SELECT status, max_capacity, current_enrollment_count FROM course WHERE course_id ? FOR UPDATE減少鎖粒度。5. 用視圖定時(shí)事件實(shí)現(xiàn)教務(wù)看板不用寫一行 JavaMySQL 自動(dòng)算出“各學(xué)院選課熱度TOP5”5.1 教務(wù)剛需實(shí)時(shí)看板要什么不是“總選課人數(shù)”而是“哪些課快滿了、哪些課沒人選、哪個(gè)學(xué)院的學(xué)生最愛搶XX課”教務(wù)老師不需要技術(shù)指標(biāo)他們要的是三類數(shù)字預(yù)警類max_capacity - current_enrollment_count 5的課程紅色標(biāo)出冷門類開課3天后current_enrollment_count 0的課程需教學(xué)督導(dǎo)介入交叉分析類SELECT dept_name, COUNT(*) FROM student s JOIN student_course_selection scs ON s.stu_id scs.stu_id JOIN course c ON scs.course_id c.course_id WHERE c.course_name LIKE %人工智能% GROUP BY dept_name ORDER BY COUNT(*) DESC LIMIT 5—— 看哪些學(xué)院學(xué)生最熱衷AI課。這些不該讓 Java 后端每秒查一遍而應(yīng)由 MySQL 用物化視圖View 定時(shí)事件Event推送給前端。5.2 創(chuàng)建可查詢的業(yè)務(wù)視圖vw_course_hotness含計(jì)算字段與索引建議-- 創(chuàng)建視圖課程熱度快照注意MySQL 視圖不物化但查詢快 CREATE VIEW vw_course_hotness AS SELECT c.course_id, c.course_name, c.dept_id, d.dept_name, c.credit, c.max_capacity, c.current_enrollment_count, ROUND(c.current_enrollment_count / c.max_capacity * 100, 1) AS occupancy_rate, CASE WHEN c.current_enrollment_count 0 THEN 冷門 WHEN c.max_capacity - c.current_enrollment_count 5 THEN 熱門預(yù)警 WHEN c.current_enrollment_count / c.max_capacity 0.8 THEN 熱門 ELSE 正常 END AS hot_level, c.status FROM course c INNER JOIN department d ON c.dept_id d.dept_id; -- 為高頻查詢字段加索引視圖本身不存數(shù)據(jù)但底層表需索引 -- 在 course 表上確保有INDEX idx_dept_status (dept_id, status) -- 在 student_course_selection 表上確保有INDEX idx_stu_course (stu_id, course_id)為什么用視圖不用臨時(shí)表視圖是邏輯層不占額外磁盤空間查詢SELECT * FROM vw_course_hotness WHERE hot_level 熱門預(yù)警會(huì)被優(yōu)化器重寫為對course和department的高效 JOIN前端輪詢/api/hot-courses接口時(shí)后端只需SELECT * FROM vw_course_hotness WHERE ...SQL 干凈無拼接風(fēng)險(xiǎn)。5.3 用 MySQL Event 每5分鐘刷新一次“選課趨勢統(tǒng)計(jì)表”視圖實(shí)時(shí)但不存歷史。教務(wù)需要看“過去24小時(shí)各時(shí)段選課峰值”就得建一張物理表用定時(shí)事件驅(qū)動(dòng)更新-- 創(chuàng)建趨勢統(tǒng)計(jì)表 CREATE TABLE course_selection_trend ( trend_date DATE NOT NULL, hour_of_day TINYINT UNSIGNED NOT NULL COMMENT 0-23, selected_count INT UNSIGNED NOT NULL DEFAULT 0, dropped_count INT UNSIGNED NOT NULL DEFAULT 0, PRIMARY KEY (trend_date, hour_of_day) ); -- 創(chuàng)建事件每天凌晨1點(diǎn)清空昨日數(shù)據(jù)每5分鐘追加最新5分鐘統(tǒng)計(jì) DELIMITER $$ CREATE EVENT evt_update_trend_daily ON SCHEDULE EVERY 1 DAY DO DELETE FROM course_selection_trend WHERE trend_date CURDATE() - INTERVAL 7 DAY; -- 只保留7天 $$ CREATE EVENT evt_update_trend_5min ON SCHEDULE EVERY 5 MINUTE DO BEGIN INSERT INTO course_selection_trend (trend_date, hour_of_day, selected_count, dropped_count) SELECT CURDATE(), HOUR(selection_time), SUM(CASE WHEN status selected THEN 1 ELSE 0 END), SUM(CASE WHEN status dropped THEN 1 ELSE 0 END) FROM student_course_selection WHERE selection_time NOW() - INTERVAL 5 MINUTE GROUP BY trend_date, HOUR(selection_time) ON DUPLICATE KEY UPDATE selected_count selected_count VALUES(selected_count), dropped_count dropped_count VALUES(dropped_count); END$$ DELIMITER ;關(guān)鍵細(xì)節(jié)ON DUPLICATE KEY UPDATE避免因網(wǎng)絡(luò)延遲導(dǎo)致同一分鐘數(shù)據(jù)被重復(fù)插入HOUR(selection_time)MySQL 的HOUR()函數(shù)直接提取小時(shí)比應(yīng)用層解析快事件名evt_update_trend_5min清晰表明用途便于 DBA 監(jiān)控刪除策略7 DAY平衡存儲(chǔ)與分析需求教務(wù)極少看超過一周的趨勢。6. 我的血淚經(jīng)驗(yàn)上線前必須做的三件事少做一件教務(wù)處電話能打爆你手機(jī)上線前最后一步不是測功能而是用真實(shí)數(shù)據(jù)壓測邊界。我經(jīng)歷過某模擬項(xiàng)目X測試時(shí)用100個(gè)賬號跑通上線后教務(wù)發(fā)通知“全校選課”瞬間5000并發(fā)系統(tǒng)直接雪崩。后來總結(jié)出三條鐵律現(xiàn)在每個(gè)新系統(tǒng)部署必做6.1 用sysbench模擬真實(shí)選課流量重點(diǎn)壓proc_select_course別用 JMeter 模擬 HTTP 請求——那測的是你的 Web 框架不是 MySQL。直接用sysbench跑自定義 Lua 腳本調(diào)用存儲(chǔ)過程# 編寫 select_course.lua核心是 -- mysql:query(CALL proc_select_course(S202300001, CS101001, code, msg)) # 執(zhí)行壓測16線程持續(xù)300秒 sysbench --db-drivermysql \ --mysql-host127.0.0.1 \ --mysql-port3306 \ --mysql-userroot \ --mysql-passwordxxx \ --mysql-dbcourse_db \ --time300 \ --threads16 \ --report-interval10 \ select_course.lua run要看的關(guān)鍵指標(biāo)queries每秒查詢數(shù)應(yīng)穩(wěn)定在 300單機(jī) MySQL 8.0latency95% 延遲 ≤ 200mserrors必須為 0若有Deadlock found說明FOR UPDATE鎖順序不一致需檢查所有存儲(chǔ)過程鎖表順序是否統(tǒng)一永遠(yuǎn)先鎖course再鎖student_course_selection。6.2 手動(dòng)觸發(fā)一次“極端場景”用SELECT ... FOR UPDATE卡住課程驗(yàn)證超時(shí)機(jī)制在生產(chǎn)庫或預(yù)發(fā)庫執(zhí)行-- 開啟事務(wù)鎖住熱門課 START TRANSACTION; SELECT * FROM course WHERE course_id CS101001 FOR UPDATE; -- 不 COMMIT保持鎖然后讓應(yīng)用發(fā)起10次選該課的請求。觀察第1次應(yīng)成功拿到鎖后9次應(yīng)在innodb_lock_wait_timeout默認(rèn)50秒后報(bào)錯(cuò)Lock wait timeout exceeded應(yīng)用層必須捕獲此錯(cuò)誤返回“系統(tǒng)繁忙請稍后再試”絕不能拋 500 給前端。這是檢驗(yàn)?zāi)闳蒎e(cuò)能力的試金石。很多團(tuán)隊(duì)只測“成功路徑”卻忘了數(shù)據(jù)庫鎖是分布式系統(tǒng)里最真實(shí)的“雪崩源頭”。6.3 導(dǎo)出一份《MySQL 選課系統(tǒng)健康檢查清單》交給 DBA 逐項(xiàng)簽字這不是甩鍋而是建立責(zé)任閉環(huán)。清單包含12項(xiàng)例如檢查項(xiàng)預(yù)期值實(shí)際值DBA 簽字innodb_buffer_pool_size≥ 總數(shù)據(jù)量 × 1.512G□max_connections≥ 500600□slow_query_log是否開啟ONON□course表current_enrollment_count索引有INDEX idx_status_count (status, current_enrollment_count)有□所有存儲(chǔ)過程DEFINER是否為rootlocalhost是是□為什么必須簽字DBA 是最后一道防線。當(dāng)某天current_enrollment_count被手動(dòng)UPDATE錯(cuò)了只有他能從 binlog 里撈回?cái)?shù)據(jù)——前提是他知道這個(gè)字段有多關(guān)鍵。我?guī)н^的每個(gè)學(xué)生團(tuán)隊(duì)上線前都逼他們手抄這份清單三遍。不是形式主義是讓“MySQL 不只是個(gè)存數(shù)據(jù)的地方而是業(yè)務(wù)邏輯的裁判員”這個(gè)認(rèn)知刻進(jìn)肌肉記憶。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取