ARTICLE DETAIL

资讯详情

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

数据库课程设计报刊订阅管理系统:从关系模式到存储过程

数据库课程设计报刊订阅管理系统:从关系模式到存储过程 简介这是一份完整的数据库课程设计报告以报刊订阅管理系统为实战案例面向需要完成数据库系统开发课程设计的高校学生也适合作为SQL Server与C#项目开发的参考范本。报告按课程设计标准流程展开包含需求分析、概要设计、数据库逻辑结构设计、功能模块设计、核心算法源码、调试运行结果及心得体会系统涵盖用户注册登录、报刊信息管理、订阅退订、支付接口等功能模块并给出了具体的表结构设计与外键约束说明。资源共1个PDF文件压缩包大小约970KB内容为36页图文并茂的设计文档层次分明、目录完整便于对照学习。目前已有893人学习浏览适合正在撰写课程设计报告或准备答辩的学生参考其整体框架与细节实现也可帮助开发者快速理解报刊订阅类管理系统的数据库设计思路与开发流程。1. 数据库课程设计“报刊订阅管理系统”别做成普通增删改查数据库课程设计拿到《报刊订阅管理系统的设计与实现》这个题目最容易翻车的不是写不出代码而是把系统做成订阅信息的增删改查页面交差时被老师一问业务规则就卡住。这个题目真正的考察点是数据库设计的完整链路从订阅、续订、投递这些业务规则里抽出实体设计满足3NF的关系模式再落成建表、索引、视图、存储过程和统计查询最后用数据验证设计。这篇笔记按这条链路讲清楚怎么做适合正在赶课程设计、需要写出完整设计文档的同学也适合想补数据库设计短板的在职新人。2. 先把订阅业务模型立住从需求拆出五张表和完整约束拿到题目别急着进 Navicat 点建表。数据建模之前要先回答几个业务问题一个订阅者能不能同时订多种报刊一次下单能不能包含多个报刊订阅期是从哪天到哪天报刊中途停刊了已生成的投递记录怎么处理投递记录按什么粒度生成常见做法是把整个系统拆成五张表订阅者表、报刊表、订阅订单主表、订阅订单明细表、投递记录表。五张表不是凭空拍出来的而是从业务关系一层层推出来的。下面把推理过程走一遍这部分内容也是课程设计文档里“需求分析”和“概念设计”章节的核心素材。2.1 实体与关系识别订单主表、明细表和投递表为什么缺一不可先识别实体。订阅者和报刊是最明显的两个实体但只有这两张表远远不够因为“订阅”这个行为本身有属性什么时候下的单、有效起止日期、订了几份、当时单价是多少。所以需要一个中间实体把订阅者和报刊关联起来。如果只做一张“订阅表”字段大致是订阅者ID、报刊ID、起止日期、份数。一次下单同时订三种报刊时需要插三行三行里的订阅者姓名、电话、地址会重复三遍。万一订阅者改地址得同时改三行才能保持一致少改一行就产生数据不一致——这就是典型的更新异常也是课堂上讲的范式问题。所以正确的拆法是把一次下单动作抽成订阅订单主表把每种报刊的订阅细节抽成订阅订单明细表。主表一条记录对应一次完整下单明细表一条记录对应单种报刊的订阅期主表和明细表是 1:N。投递记录表的粒度更特殊。日报每天产生一个期次周刊每周一个月刊每月一个。一份订阅明细从 2024-01-01 到 2024-06-30如果是日报理论上会产生约 181 条投递记录。投递记录的作用是记录每一期有没有实际送到、有没有退订它是后续“投递完成率”“漏投统计”这些报表的数据来源。很多课程设计把这一块漏掉做成只有“订阅管理”没有“投递管理”答辩时被问到“系统怎么体现投递过程”就答不上来。E-R 关系最终可以表述为订阅者 1:N 订阅订单订阅订单 1:N 订阅明细报刊 1:N 订阅明细订阅明细 1:N 投递记录。每一层都是清晰的父子关系删除策略也由此确定。2.2 关系模式与3NF检查该不该冗余价格和金额关系模式按下列方式定义主键加粗表名主键关键字段说明subscriberidname, phone, address, status订阅者基本信息newspaperidtitle, category, period, price报刊目录period 区分日报/周刊/月刊subscription_orderidsubscriber_id, order_date, total_amount, status一次订阅行为的订单头subscription_itemidorder_id, newspaper_id, start_date, end_date, copies, unit_price, amount每种报刊一条明细含订阅期与金额快照delivery_recordiditem_id, issue_no, deliver_date, status每期一条投递流水逐个做 3NF 检查subscriber 表里 address 依赖于 id没有传递依赖newspaper 表里 period 和 price 都是报刊自身属性subscription_order 表里 total_amount 理论上可以由明细表 SUM 得到这里保留是为了查询订单列表不用每次都做聚合属于可控冗余subscription_item 表要重点说。subscription_item 里的 unit_price 是“下单时的报刊单价快照”amount 是 copies 乘 unit_price。有人会质疑单价不是应该在 newspaper 表里吗这里冗余了。确实冗余但是这个冗余必须做。如果只存 newspaper_id订阅三个月后报刊调价历史订单的金额就会被改掉财务报表全乱。课程设计文档里写清楚这一条“反范式取舍”比空谈第三范式更能拿分。还要注意不要把订阅者地址、电话复制到明细表。地址属于订阅者投递时如果需要地址通过订阅者表 JOIN 取不要冗余否则改地址又会出现多处更新。2.3 建表SQL主键、外键、级联策略一次定清楚逻辑结构定好后下面的建表脚本可以直接跑数据库用 MySQL 5.7 或 8.0 均可。CREATE DATABASE IF NOT EXISTS newspaper_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE newspaper_db; -- 订阅者表 CREATE TABLE subscriber ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT 订阅者ID, name VARCHAR(50) NOT NULL COMMENT 姓名, phone VARCHAR(20) NOT NULL COMMENT 联系电话, address VARCHAR(200) NOT NULL COMMENT 投递地址, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0停用 ) ENGINEInnoDB COMMENT订阅者表; -- 报刊表 CREATE TABLE newspaper ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT 报刊ID, title VARCHAR(100) NOT NULL COMMENT 报刊名称, category VARCHAR(20) NOT NULL COMMENT 类别时政/财经/体育/娱乐, period ENUM(日报,周刊,月刊) NOT NULL COMMENT 出版周期, price DECIMAL(8,2) NOT NULL COMMENT 每期单价, status TINYINT NOT NULL DEFAULT 1 COMMENT 1在售 0停刊 ) ENGINEInnoDB COMMENT报刊表; -- 订阅订单主表 CREATE TABLE subscription_order ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT 订单ID, subscriber_id INT UNSIGNED NOT NULL COMMENT 订阅者ID, order_date DATE NOT NULL COMMENT 下单日期, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总金额, status TINYINT NOT NULL DEFAULT 1 COMMENT 1已下单 0已取消, CONSTRAINT fk_order_subscriber FOREIGN KEY (subscriber_id) REFERENCES subscriber(id) ON DELETE RESTRICT ) ENGINEInnoDB COMMENT订阅订单主表; -- 订阅订单明细表 CREATE TABLE subscription_item ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT 明细ID, order_id INT UNSIGNED NOT NULL COMMENT 所属订单ID, newspaper_id INT UNSIGNED NOT NULL COMMENT 报刊ID, start_date DATE NOT NULL COMMENT 订阅开始日期, end_date DATE NOT NULL COMMENT 订阅结束日期, copies INT NOT NULL DEFAULT 1 COMMENT 订阅份数, unit_price DECIMAL(8,2) NOT NULL COMMENT 下单时单价快照, amount DECIMAL(10,2) NOT NULL COMMENT 金额份数*单价, CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES subscription_order(id) ON DELETE CASCADE, CONSTRAINT fk_item_newspaper FOREIGN KEY (newspaper_id) REFERENCES newspaper(id) ON DELETE RESTRICT, CONSTRAINT chk_item_date CHECK (end_date start_date) ) ENGINEInnoDB COMMENT订阅订单明细表; -- 投递记录表 CREATE TABLE delivery_record ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT 投递记录ID, item_id INT UNSIGNED NOT NULL COMMENT 订阅明细ID, issue_no VARCHAR(30) NOT NULL COMMENT 期次编号如20240105, deliver_date DATE NOT NULL COMMENT 投递日期, status TINYINT NOT NULL DEFAULT 0 COMMENT 0未投递 1已投递 2退订, CONSTRAINT fk_delivery_item FOREIGN KEY (item_id) REFERENCES subscription_item(id) ON DELETE CASCADE, UNIQUE KEY uk_delivery (item_id, issue_no) ) ENGINEInnoDB COMMENT投递记录表;逐表说明几个决策点。字符集用 utf8mb4 而不是 utf8否则插入生僻字或 emoji 会报“Incorrect string value”这是课程设计里最容易出现的乱码事故。period 用 ENUM 枚举写入非法值直接被拒比在应用层判断更可靠。金额字段全部用 DECIMAL不要用 FLOAT 或 DOUBLE浮点累计会差几分钱财务报表一查就露馅。级联策略值得单独解释subscription_order 的外键 subscriber_id 用 ON DELETE RESTRICT表示有订单的订阅者不允许被物理删除防止误删历史数据subscription_item 的 order_id 用 ON DELETE CASCADE删除订单时明细自动清掉delivery_record 的 item_id 同样 CASCADE。这个“上层限制、下层级联”的组合既保护历史数据又让删除操作不用手工逐条清理子表。建表脚本里所有 COMMENT 都写完整后面导出数据字典直接可用。提示调试阶段如果确实要临时绕过外键检查可以用SET FOREIGN_KEY_CHECKS0但只在本地开发环境用演示系统里关了外键检查被老师看到会比较被动。3. 让核心查询快起来索引、视图与下单存储过程表建好只是第一步“系统的设计与实现”里“实现”二字的主体是让业务查询在数据量上来之后仍然能用。课程设计规模一般不大但答辩老师很爱问一句话数据量到十万条你这个查询还快吗这一章把索引、视图、存储过程三件事做扎实就是对这个问题的正面回答。3.1 索引规划外键与状态列优先复合索引避免回表索引不是越多越好但要覆盖常用查询路径。这个系统的高频查询有按订阅者查订单、按报刊汇总订阅量、按时间范围查订阅明细、按月统计投递量。对应的索引脚本如下-- 外键列MySQL会默认建索引这里补的是查询列和组合列 -- 支持“某个订阅者时间范围”查订单 CREATE INDEX idx_order_subscriber_date ON subscription_order(subscriber_id, order_date); -- 支持按报刊汇总订阅量 CREATE INDEX idx_item_newspaper ON subscription_item(newspaper_id); -- 支持按月/按年统计订阅以及续订提醒查询 CREATE INDEX idx_item_start_date ON subscription_item(start_date); -- 支持投递日期范围的统计 CREATE INDEX idx_delivery_date ON delivery_record(deliver_date);说明一下选择逻辑。subscription_order 上建的是复合索引 (subscriber_id, order_date)因为查询模式是“某个人的订单按时间倒序”两列组合能直接覆盖不用回表再排一次序如果只给 order_date 建单列索引按人过滤后还是要回表。subscription_item 上的 newspaper_id 和 start_date 分别对应两类查询按报刊维度汇总、按时间维度统计。delivery_record 上的 deliver_date 是报表查询的主要过滤条件。反过来不要给 status 这类区分度极低的列单独建索引。status 只有 0、1、2 三种取值全表差不多均匀分布MySQL 优化器大概率放弃索引直接全表扫建了也白建还拖慢写入速度。课程设计文档里写一句“索引选择性分析”比罗列十个索引更像回事。3.2 视图封装常用查询有效订阅、待续订提醒视图的价值是让业务层只面对一个逻辑表不用每次 JOIN 四张表。下面这个视图查询所有当前有效订阅包含订阅者姓名电话、报刊名、起止日期CREATE VIEW v_active_subscription AS SELECT s.name AS subscriber_name, s.phone AS subscriber_phone, n.title AS newspaper_title, i.start_date, i.end_date FROM subscriber s JOIN subscription_order o ON s.id o.subscriber_id JOIN subscription_item i ON o.id i.order_id JOIN newspaper n ON i.newspaper_id n.id WHERE i.end_date CURDATE() AND o.status 1;这个视图可以直接给前端“我的订阅”页面用也可以在此基础上做续订提醒查 end_date 距今天不足 30 天的记录把名单导给客服做电话回访。使用视图时有一个习惯要养成业务 SQL 只从视图取数不再手工 JOIN这样底层表结构调整时只要视图不变业务层代码可以不动。但视图并不天然比直接 SQL 快它只是封装实际执行计划还是要用 EXPLAIN 验证。3.3 存储过程实现订阅下单事务边界放在数据库端下单是个典型的多步操作写订单主表、写订单明细、汇总金额回填主表任何一步失败都不能让主表明细表出现半截数据。把事务边界放在存储过程里对课程设计而言是体现“数据库程序设计能力”的加分项。下面用 MySQL 8.0 的 JSON_TABLE 实现一条 SQL 批量插入明细DELIMITER $$ CREATE PROCEDURE sp_create_subscription( IN p_subscriber_id INT, IN p_items JSON ) BEGIN DECLARE v_order_id INT; START TRANSACTION; -- 1. 锁定订阅者行防止并发下重复下单 SELECT status INTO sub_status FROM subscriber WHERE id p_subscriber_id FOR UPDATE; IF sub_status 1 THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 订阅者已停用; END IF; -- 2. 插入订单主表 INSERT INTO subscription_order(subscriber_id, order_date, total_amount, status) VALUES (p_subscriber_id, CURDATE(), 0, 1); SET v_order_id LAST_INSERT_ID(); -- 3. 解析JSON数组批量插入明细单价取newspaper表的当前价格 INSERT INTO subscription_item (order_id, newspaper_id, start_date, end_date, copies, unit_price, amount) SELECT v_order_id, t.newspaper_id, t.start_date, t.end_date, t.copies, n.price, n.price * t.copies FROM JSON_TABLE(p_items, $[*] COLUMNS ( newspaper_id INT PATH $.newspaper_id, start_date DATE PATH $.start_date, end_date DATE PATH $.end_date, copies INT PATH $.copies )) AS t JOIN newspaper n ON t.newspaper_id n.id WHERE n.status 1; -- 4. 汇总明细金额回填主表 UPDATE subscription_order o JOIN ( SELECT order_id, SUM(amount) AS amt FROM subscription_item WHERE order_id v_order_id GROUP BY order_id ) s ON o.id s.order_id SET o.total_amount s.amt WHERE o.id v_order_id; COMMIT; END$$ DELIMITER ;调用方式是把订阅明细以 JSON 数组传进去CALL sp_create_subscription( 1, [{newspaper_id:2,start_date:2024-01-01,end_date:2024-06-30,copies:1}] );参数和边界说明p_subscriber_id 对应订阅者主键p_items 是 JSON 数组每个元素表示一种报刊的订阅区间。第 1 步的 SELECT FOR UPDATE 是并发控制的关键两个会话同时为同一个订阅者下单时第二个会话会等第一个提交后再执行避免创建出两份相同订阅。第 3 步里单价不从前端传而是从 newspaper 表现取防止用户自己改价钱。事务内任何一步报错未到 COMMIT 就自动回滚主表不会留下金额为 0 的脏订单。如果你用的是 MySQL 5.7JSON_TABLE 不可用常见替代是把明细解析放在应用层循环插入存储过程只负责开事务和回滚。效果一样只是网络交互多几次。另外如果上层用 Java 或 Python 连接数据库连接池的 initialSize 和 maxActive 分别设 5 和 20 足够课程设计使用开太大反而容易把本地数据库连接数打满。4. 统计报表与数据导出课程设计答辩的加分项课程设计真正拉开差距的地方是报表。大多数同学提交的系统只能录入和查询而“报刊订阅管理系统”天然有订阅量统计、收入统计、投递完成率统计这些分析需求。把这些 SQL 写出来再配一个 Excel 导出整个系统的完整度立刻不同。4.1 订阅量按月与按类别统计最常被问的报表是“每个月的订阅量和订阅金额”。按明细表的 start_date 分组SELECT DATE_FORMAT(i.start_date, %Y-%m) AS ym, COUNT(DISTINCT i.id) AS item_count, COUNT(DISTINCT i.order_id) AS order_count, SUM(i.amount) AS revenue FROM subscription_item i GROUP BY DATE_FORMAT(i.start_date, %Y-%m) ORDER BY ym;这里几个细节要注意。COUNT(DISTINCT i.id) 统计的是订阅笔数COUNT(DISTINCT i.order_id) 统计的是订单数两者不一样一个订单可能包含三种报刊所以笔数会大于订单数。SUM(i.amount) 直接用明细表的金额明细里有 unit_price 快照历史收入不会因报刊调价而变化。如果只用 newspaper 表的当前价格去乘统计出来的收入会忽高忽低。按报刊类别看市场分布又是一条SELECT n.category, COUNT(i.id) AS subscribe_count, SUM(i.amount) AS revenue FROM subscription_item i JOIN newspaper n ON i.newspaper_id n.id GROUP BY n.category ORDER BY revenue DESC;4.2 投递完成率与异常单据分析投递表是整个系统里最容易被忽略、也最有话可说的部分。统计每种报刊的投递完成率SELECT n.title, COUNT(d.id) AS total_issues, SUM(d.status 1) AS delivered, SUM(d.status 0) AS pending, 100 * SUM(d.status 1) / COUNT(d.id) AS done_rate FROM delivery_record d JOIN subscription_item i ON d.item_id i.id JOIN newspaper n ON i.newspaper_id n.id GROUP BY n.title;SQL 里 SUM(d.status 1) 是 MySQL 的条件计数写法表达式成立返回 1不成立返回 0求和即计数。不用整段 CASE WHEN可读性更好。这个报表的价值在于如果某份日报的投递完成率长期低于 95%说明投递环节有问题系统就不只是记录工具而是能辅助运营判断。再给一个“该投未投”的异常查询订阅还在有效期内但投递记录里找不到任何已投递状态的期次。逻辑用 NOT EXISTS 表达SELECT s.name AS subscriber_name, n.title AS newspaper_title, i.start_date, i.end_date FROM subscription_item i JOIN subscription_order o ON i.order_id o.id JOIN subscriber s ON o.subscriber_id s.id JOIN newspaper n ON i.newspaper_id n.id WHERE i.end_date CURDATE() AND o.status 1 AND NOT EXISTS ( SELECT 1 FROM delivery_record d WHERE d.item_id i.id AND d.status 1 );NOT EXISTS 在这里比 LEFT JOIN ... WHERE d.id IS NULL 更容易读执行计划也差不多。课程设计文档里把这个查询写成“投递异常检测”是一个很好的功能亮点。4.3 报表结果导出ExcelPython连接MySQL的推荐姿势统计 SQL 写得再好展示时拿不出能看的图表也吃亏。最简单的方式是写一个 Python 脚本把统计结果导出成 Excel。依赖就三个库pymysql、pandas、openpyxl。import pandas as pd import pymysql conn pymysql.connect( host127.0.0.1, port3306, userroot, passwordyour_password, databasenewspaper_db, charsetutf8mb4 ) sql SELECT DATE_FORMAT(i.start_date, %Y-%m) AS 月份, COUNT(DISTINCT i.id) AS 订阅笔数, SUM(i.amount) AS 订阅金额 FROM subscription_item i GROUP BY DATE_FORMAT(i.start_date, %Y-%m) ORDER BY 月份 df pd.read_sql(sql, conn) df.to_excel(subscribe_stat.xlsx, indexFalse) conn.close() print(导出完成subscribe_stat.xlsx)注意几个参数。charsetutf8mb4 必须和数据库一致否则中文列名和内容导出后是乱码to_excel 需要 openpyxl 库没装的话执行pip install openpyxl导出的 Excel 里中文列名直接由 SQL 别名决定。如果环境不允许装 pandas也有一个纯 MySQL 方案SELECT ... INTO OUTFILE /tmp/stat.csv但需要 FILE 权限且 MySQL 8 默认开启 secure_file_priv 限制导出目录折腾起来比 Python 方案麻烦不推荐课程设计用。5. 常见问题与避坑从建库到并发踩过的五种典型教训这一章把我在类似课程设计和真实项目中遇到的五类问题按“现象→原因→解决”记录下来。这些问题只要提前知道都是可以避免的。5.1 自增主键回滚只增不减业务单号不能拿自增ID硬扛现象下单事务里先插入订单主表后面某一步校验失败事务回滚。再次下单成功后发现订单表 id 不是 1、2、3 连续递增而是跳到了 5 或 6。有人觉得这是 bug去网上找“重置自增”的偏方。原因InnoDB 的自增计数器不会随事务回滚回退。事务里每执行一次 INSERT自增计数器就加一哪怕事务最终回滚这个值也已经消耗掉了。这是 InnoDB 的既定行为不是故障。解决自增 id 只做代理主键不在业务上对外展示。对外展示的单号可以用CONCAT(SUB, DATE_FORMAT(order_date, %Y%m%d), LPAD(id, 4, 0))拼出来比如 SUB202401050001。这样内部关联用 id页面展示用单号自增不连续根本看不出来更不存在“必须连续”的需求。课程设计文档里把这个单号生成逻辑写清楚等于提前挡掉一个答辩问题。5.2 外键删除被拒先想清楚ON DELETE策略再建表现象界面上删了一个订阅者程序报错Cannot delete or update a parent row: a foreign key constraint fails。第一次遇到的人通常很懵觉得外键是个麻烦。原因建表时 subscription_order 的 subscriber_id 外键设的是 ON DELETE RESTRICT意思是有订单引用的订阅者不允许删除。这是保护历史数据的正确策略不是配置错误。解决删除订阅者前先处理它名下的订单。常见做法是业务上不物理删除只把 subscriber.status 改成 0实现“停用”而非“删除”如果确实要删就先删明细和投递记录再删订单最后删订阅者顺序不能乱。调试时临时关外键检查可以用 SET FOREIGN_KEY_CHECKS0但只是开发者自己的后悔药演示环境千万别这么干。5.3 索引失效三种写法函数包裹、前置通配符与隐式转换现象数据量到几万条后某些查询慢得离谱EXPLAIN 一看 type 是 ALL明明有索引却不走。原因常见三种写法让索引失效。第一种是索引列被函数包裹比如WHERE DATE_FORMAT(start_date, %Y-%m) 2024-01MySQL 没法对函数计算结果走索引第二种是 LIKE 前置通配符WHERE title LIKE %日报%索引要按前缀匹配前导通配符等于放弃索引第三种是隐式类型转换phone 字段是 VARCHAR写成WHERE phone 13812345678MySQL 会把字段转成数字比较索引失效。解决范围查询改写成区间形式WHERE start_date 2024-01-01 AND start_date 2024-02-01这个条件下 start_date 上的索引能正常生效模糊搜索如果确实需要包含式匹配考虑全文索引或单独拆关键词字段字符串字段查询时记得加引号。这个坑在数据库面试题里也经常出现搞懂了不亏。5.4 中文乱码三处不一致建库、连接、表都要utf8mb4现象插入中文时正常SELECT 查出来是一串问号或者插入时报Incorrect string value。原因字符集问题出在三处不一致。建库时的 DEFAULT CHARACTER SET、客户端连接时的 charset、数据表字段的字符集只要有一处是 latin1 或老版 utf8就会出现乱码。解决首先是建库时统一DEFAULT CHARACTER SET utf8mb4其次连接串或连接工具要指定 utf8mb4JDBC 的连接串里加characterEncodingutf8Python 的 pymysql 参数里加charsetutf8mb4Navicat 连接属性里把编码设为 UTF-8最后检查已有表的字符集不一致时用ALTER TABLE table_name CONVERT TO CHARACTER SET utf8mb4修正。注意这个操作在数据量大时会重写整张表并锁表生产环境要放在维护窗口执行。5.5 并发下单重复订阅唯一约束与锁的选择现象压测或多人同时操作时同一个订阅者同一天对同一份日报生成了两条有效订阅后台数据重复。原因应用层常规写法是“先查有没有有效订阅没有就插入”。两个并发请求同时通过查询都认为没有有效订阅然后都执行插入重复数据就产生了。问题不在查询本身而在“查”和“插”两步之间没有锁。解决两个层面兜底。数据库层面给订阅明细加唯一索引配一个“同一订单内不重复”的约束同时在下单存储过程里用 SELECT ... FOR UPDATE 锁定订阅者行把查重和插入变成同一个事务内的原子操作。多个事务并发时InnoDB 会自动让后到的事务等锁如果发生死锁MySQL 会回滚其中一个事务应用层要捕获死锁异常并重试一次。编程习惯上尽量固定访问顺序先锁订阅者、再操作明细能明显减少死锁概率。提示FOR UPDATE 只对 InnoDB 事务内有效并且一定要让查询走索引否则锁定范围可能放大到全表并发能力直线下降。6. 验收方法用造数脚本和EXPLAIN验证设计是否过关设计得好不好不能只看能不能跑通还要用数据和执行计划来验收。我的习惯是给课程设计做三件事造十万级数据、看 EXPLAIN、做更新异常反向验证。6.1 快速造数十万级投递记录的压力自测用递归 CTE 给所有订阅明细批量生成 365 天的投递记录数据量一步到位WITH RECURSIVE seq AS ( SELECT 1 AS n UNION ALL SELECT n 1 FROM seq WHERE n 365 ) INSERT INTO delivery_record (item_id, issue_no, deliver_date, status) SELECT i.id, DATE_FORMAT(2024-01-01 INTERVAL s.n DAY, %Y%m%d), 2024-01-01 INTERVAL s.n DAY, 1 FROM subscription_item i, seq s;说明这条语句给每条订阅明细生成从 2024-01-01 起连续 365 天的投递记录issue_no 按日期格式生成保证唯一键不冲突。生成后 select count(*) 看总量再跑一遍第 4 章的统计 SQL感觉一下执行速度。6.2 EXPLAIN确认执行计划对慢查询执行前加 EXPLAIN看 type 列和 key 列。key显示 idx_delivery_date说明范围查询用上了索引type 从 ALL 变成 range 或 ref说明过滤生效rows 列估算扫描行数大幅减少才是真正有优化效果。6.3 用更新异常反推设计缺陷最后一步是做反向验证把 newspaper 表某报刊的价格改掉再看历史订单金额是否跟着变。如果历史金额变了说明明细表没做价格快照设计有缺陷如果没变说明 unit_price 快照是生效的。同理修改订阅者地址不应影响任何历史投递记录的归属。养成这个验证习惯后答辩时被问到范式设计可以直接拿这次的验证过程举例。希望帮到你。本文还有配套的精品资源点击获取
返回列表