ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LinuxC+QT+MySQL 票务模拟系统:从数据库设计到并发控制的毕设实践

LinuxC+QT+MySQL 票务模拟系统:从数据库设计到并发控制的毕设实践 简介一份基于Linux C与Qt框架、结合MySQL数据库实现的机票网络售票模拟系统源码主要面向毕业设计、课程设计与项目开发场景适合具备一定C/C与数据库基础的学习者参考。包内共104个文件涵盖C源码.cpp/.h、界面文件.ui、Qt项目配置.pro/.qrc、MySQL建库脚本与自动部署脚本.sh并附有PDF说明文档和设计截图.png便于对照理解系统结构。资源包约10.01MB整体轻量已通过测试可帮助快速复现航班查询、购票与后台管理等核心流程。目前已有31人学习下载适合用来梳理客户端/服务器端分离开发思路以及Qt与MySQL联调时的环境配置要点。需要说明的是资源来源于网络分享仅供学习交流勿用于商业用途。1. 为什么这套 LinuxCQTMySQL 票务模拟系统在毕设里最不容易翻车毕业设计选题最怕的不是技术太新而是“各模块看着都会合在一起却谁也不理谁”。基于 LinuxCQTMySQL 开发的机票网络售票模拟系统恰恰是把三件老技术按一条完整数据链路串起来LinuxC 做网络服务端扛住并发购票请求QT 做桌面客户端负责展示和操作MySQL 做持久化层存航班、用户和订单。这个组合在答辩时是最稳的因为每一层都能被单独追问C 的线程和锁、QT 的信号槽、MySQL 的事务和索引全是课程里教过的知识点没有一个是黑匣子。适合计算机科学与技术、软件工程专业的毕业设计或课程设计尤其适合想同时展示网络编程和数据库设计能力、又不想冒险用陌生框架的读者。2. 先用 MySQL 把底子铺好机票、订单、用户三张表与初始化数据2.1 业务梳理一张机票订单要拆成几张表做这个系统之前先把购票场景压成三句话用户登录后查航班看到余票和价格下单扣减余票并生成订单。核心数据就三类用户、航班、订单。很多初学者习惯一张大表把字段全塞进去结果到了联调阶段查一个航班要把用户信息也捞出来性能难看代码也绕。我把表拆成三张user 维护账号和密码摘要flight 维护航班静态信息和动态余票ticket_order 维护订单状态。之所以把余票字段放进 flight 而不是单独一张库存表是因为毕设场景里航班量不大余票随订单实时变化放在同一行里可以用一条 UPDATE 原子扣减避免两张表之间的强一致问题。价格也直接放在 flight 表里订单创建时从航班行取值不额外维护价格历史模拟系统不需要做价格快照。这里有个选型细节值得展开一定用 InnoDB 引擎不要用 MyISAM。MyISAM 不支持行级锁也不支持事务两个客户端同时买同一航班扣减余票时可能出现覆盖写余票明明剩 1 张却能卖出 3 张。InnoDB 配合事务和行锁才是这个系统正确的底子。字符集统一用 utf8mb4不要用 utf8MySQL 的 utf8 实际是 utf8mb3存不了生僻字和特殊符号机票订单里出现乘客名字带生僻字会直接报错。2.2 用 SQL 脚本建库建表、索引和演示航班一次跑完我习惯把初始化脚本写成可重复执行的版本用 CREATE TABLE IF NOT EXISTS再单独放一批演示数据。这样 MySQL 装好后跑一遍就能进入开发状态不用每次手动敲。CREATE DATABASE IF NOT EXISTS airticket DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE airticket; CREATE TABLE IF NOT EXISTS user ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, username VARCHAR(32) NOT NULL, password_md5 CHAR(32) NOT NULL, real_name VARCHAR(16) DEFAULT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE IF NOT EXISTS flight ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, flight_no VARCHAR(16) NOT NULL, origin VARCHAR(16) NOT NULL, dest VARCHAR(16) NOT NULL, departure_time DATETIME NOT NULL, arrival_time DATETIME NOT NULL, economy_price DECIMAL(10,2) NOT NULL, business_price DECIMAL(10,2) NOT NULL, economy_left INT UNSIGNED NOT NULL DEFAULT 0, business_left INT UNSIGNED NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1 COMMENT 1可售 0停售, PRIMARY KEY (id), UNIQUE KEY uk_flight_no (flight_no), KEY idx_origin_dest (origin, dest) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE IF NOT EXISTS ticket_order ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL, user_id INT UNSIGNED NOT NULL, flight_id INT UNSIGNED NOT NULL, seat_class ENUM(economy,business) NOT NULL DEFAULT economy, price DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已出票 2已取消, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_flight_id (flight_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表逻辑里有几个点要解释user.password_md5 定长 32 字符比 VARCHAR 存储更紧凑也避开用户密码明文落库的问题模拟系统足够用了。flight 表的 UNIQUE KEY uk_flight_no 保证同一航班号不会重复录入演示数据多跑一次不会堆积脏数据。ticket_order 的 seat_class 用 ENUM 而不是字符串既节省存储又让 C 和 QT 两端传入的舱位值必须有固定枚举联调时少一类脏数据。插入演示数据时注意起飞时间要用 DATETIME 并且是未来的时间方便后面按 NOW() 过滤。INSERT INTO flight (flight_no, origin, dest, departure_time, arrival_time, economy_price, business_price, economy_left, business_left) VALUES (CA1801, 北京, 上海, 2026-01-10 08:00:00, 2026-01-10 10:20:00, 680.00, 1880.00, 20, 6), (MU2703, 上海, 广州, 2026-01-10 09:30:00, 2026-01-10 12:00:00, 520.00, 1520.00, 15, 4), (CZ3502, 广州, 成都, 2026-01-10 13:20:00, 2026-01-10 15:40:00, 780.00, 1980.00, 12, 2), (HU7605, 北京, 深圳, 2026-01-11 07:40:00, 2026-01-11 10:50:00, 920.00, 2380.00, 25, 5);余票字段初始值就是模拟库存20、15、12 这些数字没有实际意义但建议不要填 0 或 1因为后面压测并发时会用到足够余量。真实系统里余票来自航司接口模拟系统写死没问题。2.3 在 Linux 端装好 MySQL 并给程序一个最小权限账号开发阶段最常见的翻车点是直接用 root 账号连业务代码。root 权限太大C 程序里拼错一条 SQL 可能直接把系统表删了。正确做法是单独建一个业务账号只给 airticket 库的增删改查权限。以 Ubuntu 22.04 为例装好 MySQL 后执行sudo apt update sudo apt install mysql-server -y sudo systemctl enable --now mysql systemctl status mysql能看见 active (running) 就进入下一步。有的发行版服务名是 mysqld用 systemctl status mysqld 也能确认。接着不要急着用 root 跑业务执行CREATE USER IF NOT EXISTS ticket_userlocalhost IDENTIFIED BY ticket_pass; GRANT SELECT, INSERT, UPDATE, DELETE ON airticket.* TO ticket_userlocalhost; FLUSH PRIVILEGES;主机部分写成 localhost 意味着只允许本机连接毕设演示完全够用。如果还想用 MySQL Workbench 从开发机远程连就要建 ticket_user%并在 my.cnf 里确认 bind-address 不是 127.0.0.1 而是 0.0.0.0。我一般建议演示时直接用 127.0.0.1 跑 TCP 连接省掉 socket 路径的麻烦后面避坑章节会展开。2.4 一个小设计决定把“模拟现实”留在数据字段里航班表里已经有 departure_time前端显示时直接格式化不要在 QT 里硬编码字符串。常见错误是把起飞时间拼在界面 label 里导致换演示时间要重新编译客户端。更自然的做法是在显示层格式化C 服务端查询时也可以顺手用 DATE_FORMAT 输出。想让航班数据更接近真实还可以写一条 MySQL 存储过程根据当前时间自动生成未来三天的航班每天演示前调用一次效果比死数据好很多。不过这是加分项不做也不影响主体功能。3. LinuxC 服务端多线程接收购票请求用事务把“超卖”压住3.1 socket pthread先让客户端连得上服务端这一层的目标是QT 客户端发来一个 “BUY 航班ID 舱位” 的文本命令经过 LinuxC 处理返回 OK 或错误信息。先看最小骨架它把起服务、收连接、开线程做业务的流程走通。#include stdio.h #include stdlib.h #include string.h #include unistd.h #include time.h #include pthread.h #include arpa/inet.h #include sys/socket.h #include mysql/mysql.h #define PORT 9999 static MYSQL *db_connect(void) { MYSQL *conn mysql_init(NULL); if (mysql_real_connect(conn, 127.0.0.1, ticket_user, ticket_pass, airticket, 3306, NULL, 0) NULL) { fprintf(stderr, mysql_real_connect failed: %s\n, mysql_error(conn)); mysql_close(conn); return NULL; } mysql_set_character_set(conn, utf8mb4); return conn; } static void *handle_client(void *arg) { long fd (long)arg; char buf[256]; ssize_t n read(fd, buf, sizeof(buf) - 1); if (n 0) { close(fd); return NULL; } buf[n] \0; buf[strcspn(buf, \r\n)] \0; /* 业务解析和数据库操作在 3.2 里补全 */ printf(recv: %s\n, buf); write(fd, OK\n, 3); close(fd); return NULL; } int main(void) { srandom(time(NULL)); int server_fd socket(AF_INET, SOCK_STREAM, 0); if (server_fd 0) { perror(socket); return 1; } int opt 1; setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); struct sockaddr_in addr {0}; addr.sin_family AF_INET; addr.sin_addr.s_addr htonl(INADDR_ANY); addr.sin_port htons(PORT); if (bind(server_fd, (struct sockaddr *)addr, sizeof(addr)) 0) { perror(bind); return 1; } listen(server_fd, 16); while (1) { int client_fd accept(server_fd, NULL, NULL); if (client_fd 0) continue; pthread_t tid; pthread_create(tid, NULL, handle_client, (void *)(long)client_fd); pthread_detach(tid); } }注意几点参数细节mysql_real_connect 的第四个参数传的是数据库名 airticket第六个参数传 3306 端口配合 127.0.0.1 走 TCP不走本地 socket。pthread_detach 是必须的否则每个线程退出后不会自动释放资源跑一会儿服务端内存就被僵尸线程吃光。listen 的第二个参数 16 是等待队列长度毕设压力不大16 足够改太大反而没用。3.2 订票的核心一条带条件的 UPDATE 比“先查再改”更稳这里有一个很多新手会踩的坑先 SELECT 查余票如果大于 0 再 UPDATE 减一。这个“先查再改”在并发场景下必然出问题两个请求同时查到余票为 1都会进入更新最后余票变成负数。要压住这个并发问题得靠一条带条件的 UPDATE 和事务把“判断余票”和“扣减余票”放进同一个原子操作。static int buy_ticket(MYSQL *conn, int user_id, int flight_id, int is_business) { const char *left_col is_business ? business_left : economy_left; const char *price_col is_business ? business_price : economy_price; char sql[512]; mysql_autocommit(conn, 0); snprintf(sql, sizeof(sql), UPDATE flight SET %s %s - 1 WHERE id %d AND %s 0, left_col, left_col, flight_id, left_col); if (mysql_real_query(conn, sql, strlen(sql)) ! 0) { mysql_rollback(conn); mysql_autocommit(conn, 1); return -1; } if (mysql_affected_rows(conn) ! 1) { mysql_rollback(conn); mysql_autocommit(conn, 1); return 0; } char order_no[64]; snprintf(order_no, sizeof(order_no), TS%ld%05ld, (long)time(NULL), random() % 100000); snprintf(sql, sizeof(sql), INSERT INTO ticket_order (order_no, user_id, flight_id, seat_class, price) SELECT %s, %d, %d, %s, %s FROM flight WHERE id %d, order_no, user_id, flight_id, is_business ? business : economy, price_col, flight_id); if (mysql_real_query(conn, sql, strlen(sql)) ! 0 || mysql_affected_rows(conn) ! 1) { mysql_rollback(conn); mysql_autocommit(conn, 1); return -1; } mysql_commit(conn); mysql_autocommit(conn, 1); return 1; }关键判断在 mysql_affected_rows(conn) ! 1。MySQL 的 UPDATE 即使匹配到行如果值没有变化返回的受影响行数也可能为 0所以 WHERE 条件里必须有“余票大于 0”的判断。affected_rows 为 1说明这条 UPDATE 真的扣掉了一张票为 0说明当前没有可卖余票直接回滚并返回“无票”。订单插入放在同一个事务里用 SELECT 从 flight 表取实时价格避免代码里重复维护一份价格。这里还有一个必须养成的习惯用户输入的任何舱位文本不要直接拼进 SQL先把字符串翻译成整型枚举。代码里的 is_business 是 intleft_col 和 price_col 来自编译期常量用户输入根本进不了 SQL 文本从源头掐掉注入风险。3.3 协议格式和联调时用的简单文本约定客户端和服务端之间用什么协议直接影响调试效率。我建议用最简单的文本行协议每行一个命令例如 BUY 1 economy、QUERY 1回应也一行搞定OK 或 FAIL:NOSTOCK。别一上来就搞 JSON 或二进制协议毕设时间有限文本协议用 netcat 就能手动测出了问题一眼看得见。关于 C 服务端的具体做法将 handle_client 中的消息用 strtok_r 按空格切分。第一个字段是 BUY第二个是航班 ID第三个是舱位。把舱位字符串转成 is_business 之后调用 buy_ticket再根据返回值写出不同提示。协议里不建议让客户端传价格价格必须以数据库为准否则客户端改报文就能买到便宜票了。3.4 单独连接还是连接池毕设阶段别过度设计每个请求都 mysql_real_connect 一次确实低效但毕设演示没有真实并发压力单独连接足够。热搜里常看到“mysql 的数据库连接池”真实项目那是必须的但在 LinuxC 这个项目里做连接池会把造轮子时间拉长。折中做法是每个线程建立一次连接、线程退出时释放并在 handle_client 开头调用 db_connect结尾 mysql_close。这样既不会全局共享一条连接导致线程互相踩实现成本也只有几行。4. QT 客户端Qt Designer 搭界面QMYSQL 驱动接数据库4.1 Qt Designer 里放哪几块控件QT 客户端的主要工作是展示航班列表、发起购票、反馈结果。界面用 Qt Designer 画非常快不需要手工写布局代码。我一般放一个 QTableView 做航班列表左边放查询条件 ComboBox右边放两个按钮一个“查询航班”一个“购买当前航班”底部放状态栏和刷新定时器。设计原则是只放必要控件。很多课程设计喜欢堆 QTabWidget、QFormLayout、QGroupBox把界面搞得很满答辩时反而讲不清。这个系统的核心动作只有“查”和“买”界面简洁逻辑反而好讲。Qt Designer 里把控件拖好后注意给按钮和列表起可读的 objectName比如 buyButton、flightTable后面连接槽函数不会认错对象。4.2 建立 MySQL 连接与 QSqlQueryModel 刷新航班列表QT 直连 MySQL 用的是 QSqlDatabase关键是选择合适的驱动名和连接名。同一进程里如果要开多条连接必须给每条连接起不同名字否则后一个 open 会覆盖前一个。QSqlDatabase db QSqlDatabase::addDatabase(QMYSQL, ticket_query); db.setHostName(127.0.0.1); db.setPort(3306); db.setDatabaseName(airticket); db.setUserName(ticket_user); db.setPassword(ticket_pass); if (!db.open()) { QMessageBox::critical(this, 数据库连接失败, db.lastError().text()); return; }参数说明addDatabase 第一个参数 QMYSQL 表示用 MySQL 驱动第二个参数 ticket_query 是这条连接的句柄如果本机同时开着 MySQL 和别的数据库这个命名就能区分连接。连接成功之后查询航班列表用 QSqlQueryModel它可以直接把查询结果映射到表格视图。QSqlQueryModel *model new QSqlQueryModel(this); model-setQuery( SELECT id, flight_no, origin, dest, DATE_FORMAT(departure_time, %Y-%m-%d %H:%i) AS dep_time, economy_price, economy_left FROM flight WHERE status 1, db); if (model-lastError().type() ! QSqlError::NoError) { QMessageBox::warning(this, 查询失败, model-lastError().text()); return; } model-setHeaderData(1, Qt::Horizontal, 航班号); model-setHeaderData(2, Qt::Horizontal, 起飞城市); model-setHeaderData(3, Qt::Horizontal, 到达城市); model-setHeaderData(4, Qt::Horizontal, 起飞时间); model-setHeaderData(5, Qt::Horizontal, 经济舱价格); model-setHeaderData(6, Qt::Horizontal, 经济舱余票); ui-flightTable-setModel(model);注意第一个字段 id 没有表头它只是给后面购票时取航班 ID 用。DATE_FORMAT 在 SQL 层就把日期显示格式固定了省得 QT 再写 QDateTime 转换。刷新数据时重新调用一次 model-setQuery 即可这是 QSqlQueryModel 和 QSqlTableModel 的常见做法。4.3 购票动作不直接写数据库发一个短命令给 LinuxC 服务端如果你是直接把购票 UPDATE 写在 QT 客户端里那 LinuxC 服务端就失去存在意义了。网络售票模拟系统里QT 客户端查数据库拿展示数据但下单这个写事务交给 C 服务端用 QTcpSocket 发一行命令。socket new QTcpSocket(this); socket-connectToHost(127.0.0.1, 9999); connect(socket, QTcpSocket::readyRead, this, [this]() { QString line QString::fromUtf8(socket-readLine()); if (line.startsWith(OK)) { ui-statusLabel-setText(出票成功稍后刷新余票); } else { ui-statusLabel-setText(line.trimmed()); } });点击“购买当前航班”时从当前选中的行里取出航班 ID拼命令发出int row ui-flightTable-currentIndex().row(); int flightId model-index(row, 0).data().toInt(); QString cmd QString(BUY %1 economy\n).arg(flightId); socket-write(cmd.toUtf8());把写操作放到服务端一个直接好处是事务边界只在 C 代码里出现QT 客户端不会因为误操作把事务开一半。另一个好处是答辩演示时你可以先开一个终端显示 C 服务端日志再点客户端按钮评审能清楚看到一次购票请求从界面走到数据库的完整路径这比所有逻辑都堆在客户端“有说服力得多”。4.4 刷新动作要用 QTimer 而不是在回调里立刻重查出票成功之后航班余票已经变了但 QT 的表格还停留在旧数据。刷新必须触发一次新的 setQuery。问题在于刚收到 OK 就立刻去查C 服务端的事务可能还没来得及提交完成查到的还是旧值。稳妥做法是收到 OK 后用 QTimer::singleShot 延时 500 毫秒再刷新或者直接点“查询航班”按钮手动刷新。这个细节很多人忽略演示时容易出现“买完票余票不变”的尴尬其实不是逻辑错了是刷新时机不对。5. LinuxCQTMySQL 联调避坑现象、原因、解决5.1 QSqlDatabase: QMYSQL driver not loaded现象QT 程序启动时数据库 open 失败输出窗口出现 QSqlDatabase: QMYSQL driver not loaded。原因绝大多数情况是 Qt 的 SQL 驱动插件没装上。Debian 系发行版Qt 主包和 MySQL 驱动是分开的只装了 libqt5sql5 不会自动带 MySQL 插件。也有可能是 Qt 源码编译时没把 sql-mysql 插件编进去。解决先装发行版驱动包Ubuntu 下执行 sudo apt install libqt5sql5-mysql装完查看 /usr/lib/x86_64-linux-gnu/qt5/plugins/sqldrivers/ 下是否出现 libqsqlmysql.so。如果还不行用 QT_DEBUG_PLUGINS1 ./air_client 启动QT 会打印插件加载失败的详细原因这个环境变量是排查 QT 插件问题的后悔药能直接看到是路径问题还是库依赖缺失。5.2 Fatal: cannot mix incompatible Qt library version ex50601现象程序一启动就 abort终端输出 Fatal: cannot mix incompatible Qt library (version ex50601) with this library有些机器上还会伴随段错误。原因运行环境里存在多个 Qt 版本Qt Creator 自带的库和系统库混在一起。最常见的是 LD_LIBRARY_PATH 里写了一个 Qt 路径而 qmake 或 CMake 编译时用的是另一个版本的库程序加载到两个相互冲突的 Qt 核心库。解决先执行 ldd ./air_client | grep Qt看清楚程序到底加载了哪些 Qt 库。再执行 qmake -query QT_INSTALL_LIBS把 LD_LIBRARY_PATH 指向这个输出路径。如果装了多个 Qt我建议 unset LD_LIBRARY_PATH 然后重新跑一次让程序走系统默认的 rpath能解决 90% 的混用问题。这个错误在 QT 开发里几乎每个人都会翻车一次记住先查 ldd 再动环境变量。5.3 ERROR 2002 (HY000) 连不上 MySQL任何程序都连接失败现象C 服务端启动时报 ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sockQT 客户端同样连不上命令行 mysql 也提示类似错误。原因报错里出现 socket 路径说明客户端走了 UNIX socket 连接而不是 TCP。MySQL 在 Ubuntu 上的 socket 文件默认放在 /var/run/mysqld/mysqld.sock程序却试图连接 /tmp/mysql.sock。另外也可能是 mysql.server 没启动systemctl status mysql 一看是 inactive。解决先确认 MySQL 服务在运行再在连接参数里强制走 TCP。C 端把 mysql_real_connect 的 host 写成 127.0.0.1端口写成 3306QT 端 setHostName(127.0.0.1) 和 setPort(3306) 同样处理。TCP 连接不依赖 socket 文件天然避开路径不匹配问题。如果非要走 socket就在 my.cnf 里把 socket 路径改成 /tmp/mysql.sock 并重启服务。5.4 中文乱码数据库正常客户端显示问号现象用命令行插入中文数据正常但 C 服务端打印出来的航班信息是问号QT 表格里也是乱码。原因数据库表是 utf8mb4但客户端连接 MySQL 时没有声明字符集连接层默认是 latin1数据在传输过程中被转码破坏。解决C 端连接建立后立即执行 mysql_set_character_set(conn, utf8mb4)QT 端在 db.open() 之后执行一次 QSqlQuery(SET NAMES utf8mb4, db)。这两个设置能保证客户端、连接、存储三层字符集一致。还要检查编译器对 C 源码的编码Linux 下 gcc 默认按 UTF-8 处理但如果代码是 GBK 保存的也会出现源头乱码统一把源文件编码改成 UTF-8 是最稳的。5.5 余票成负数或“明明有票却下单失败”现象并发买了二三十张票后flight 表里 economy_left 变成负数或者收到 FAIL:NOSTOCK 但数据库余票还剩很多。原因前者是没做条件 UPDATE先查再改导致超卖后者是查询时没有在事务里或者提交前被另一个连接改了数据。还有一个隐蔽问题QT 端直接用自己的数据库连接改了余票C 服务端的连接缓存了旧数据两边互相不知道。解决所有扣减必须走 mysql_affected_rows 判断UPDATE ... WHERE economy_left 0只有影响行数为 1 才插入订单并提交。QT 端不参与任何写事务只做查询展示。遇到“有票却买不了”去数据库执行 SELECT * FROM flight WHERE id1确认余票是不是小于订单数。如果代码里把“扣减”和“下单”分开提交有机会出现票扣了但订单没生成所以一定要放进同一个事务。6. 给“能跑起来”的模拟系统补上三个进阶能力6.1 把裸 SQL 换成预编译语句C 端用 mysql_stmt_prepare 替代 snprintf 拼 SQLQT 端用 QSqlQuery::prepare 绑定参数。这样不但防 SQL 注入也让 MySQL 端到端的执行计划可以复用。C 端代码改动不大但答辩时被问到“你这个系统安全性怎么保证”这一条就是直接回应。6.2 给余票字段加版本号做乐观锁在 flight 表增加 version 列为整数扣减余票时 UPDATE 条件多带一个 version 判断成功后再 version1。这样即使事务里 UPDATE 影响行数为 1也能确认真实读到的是最新版本配合 InnoDB 行锁更稳。对于模拟系统这个设计能体现你对并发控制的理解。6.3 用并发脚本验证“超卖”逻辑是否真的被压住联调时不要只在 QT 界面点按钮那只能验证单线程路径。写一个并发脚本20 个进程同时买同一趟航班的票观察最终订单数和余票数是否一致。for i in $(seq 1 20); do printf BUY 1 economy\n | nc 127.0.0.1 9999 buy_result.log done wait echo OK 数$(grep -c ^OK buy_result.log) echo FAIL 数$(grep -c ^FAIL buy_result.log)脚本里的地址和端口要与 C 服务端一致航班 ID 1 的初始余票数决定预期结果。跑完后去 MySQL 执行 SELECT economy_left FROM flight WHERE id1如果初始是 20脚本打出 20 个 OK 且余票为 0说明事务和条件 UPDATE 都生效。如果 OK 数超过 20回去检查 buy_ticket 里是否漏了 mysql_affected_rows 判断。我现在每次联调都会按固定顺序走一遍先看 MySQL 服务状态再启动 C 服务端最后开 QT 客户端。出问题先查数据库日志和线程日志而不是盯着界面猜。这个顺序帮我避开了绝大多数低级错误也建议你把它养成习惯。希望帮到你。本文还有配套的精品资源点击获取
返回列表