ARTICLE DETAIL

资讯详情

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

校园卡管理系统数据库设计实战:从ER模型到并发控制的完整方案

校园卡管理系统数据库设计实战:从ER模型到并发控制的完整方案 简介这是一份《数据库原理与应用》课程中校园卡管理系统数据库设计的完整文档面向需要完成数据库课程设计或初学数据库原理的高校学生。文档以校园卡日常管理、电子钱包、身份认证三个子系统为背景完整展示了需求分析、数据入库、存储过程创建、系统调试与测试等核心环节并针对食堂消费、超市消费、课程考勤、宿舍归宿等场景给出数据字典、逻辑结构定义和存储过程定义。附录收录了全部SQL运行语句及功能验证方法可直接对照实现或作为答辩参考。资源为单个PDF文件大小1.28MB已有119人学习下载。内容还兼顾安全性、完整性约束、性能优化与故障恢复配合校园卡办理、充值、挂失等业务流程图能帮助读者系统理解数据库设计从分析到实施的完整思路。1. 校园卡管理系统数据库设计从一张卡到一组表课程设计最容易翻车的地方校园卡丢了要挂失补卡后旧卡流水还得查得着食堂高峰上千人同时刷卡余额不能被扣成负数月底对账差五毛钱不该查一整夜——这些场景的背后是一套数据库设计。很多学生拿到「数据库原理与应用校园卡管理系统数据库设计」这类课程设计题目第一反应是画张 ER 图、建几张表交差结果一上真数据就翻车并发扣款把余额扣成负数、补卡后历史流水对不上、日终对账差几分钱找不出原因。这里按一套完整设计往下拆业务怎么建模、实体和联系怎么定、关系模式怎么做范式取舍、建表 SQL 怎么落、并发怎么控、对账怎么验证让新手能照着复现让熟手能拿去答辩和压测。2. 需求分析与概念结构设计先拆业务边界再画 ER 图评审一份数据库课程设计最先看的就是需求分析和 ER 模型。这块如果立不住后面的表结构就是空中楼阁。很多同学为了凑实体数量把「充值方式」「食堂窗口」这种纯属性硬抬成实体E-R 图变得又臃肿又难辩护。正确顺序是先讲清业务用例再从用例里圈实体、连联系最后才落 ER 图。2.1 从卡片生命周期倒推业务用例增删改查不是凭空来的把校园卡的一生列出来发卡、充值、消费、挂失、解挂与补卡、销户退款。每个环节都对应一组明确的数据库动作发卡是新增持卡人、新增卡片并同时创建资金账户充值要更新账户余额并写入充值流水消费是条件扣款加消费流水挂失要同时改卡状态和账户冻结标记补卡只新增一张卡不允许改动原账户余额销户则要查余额、走退款流程、再置账户销户。课程设计文档里最常被问到的「增删改查」就是从这些动作来的不是凭空列出来给老师凑数的。这些业务动作里藏着关键约束应整理成一张用例表写清楚每个动作的输入、输出和不允许出现的情况业务动作涉及对象主要数据库操作核心约束发卡持卡人、卡片、账户INSERT 三张表同一持卡人可有多张历史卡充值账户、流水UPDATE 余额 INSERT 流水充值流水号唯一消费账户、卡片、商户、流水UPDATE 余额 INSERT 流水余额不足时禁止扣款挂失卡片、账户UPDATE 卡状态 UPDATE 冻结标记挂失后账户不可消费补卡卡片INSERT 新卡 UPDATE 旧卡状态不改变账户余额与流水销户账户、流水UPDATE 账户状态 INSERT 退款流水余额不为负退款有据实际操作时我一般会按四步走从需求描述里圈出所有名词这是候选实体把纯属性剔除姓名、余额、时间是属性不是实体为保留独立行为或独立状态的对象单独建实体卡有挂失状态账户有冻结状态二者必须分开最后用动词连线充值、消费、挂失都是联系联系两端各是什么实体一目了然。2.2 实体清单持卡人、校园卡、账户、商户、流水为什么必须分开实体识别的核心原则是独立状态和独立行为。卡片有「正常、挂失、注销」三种状态账户有「正常、冻结、销户」三种状态两种状态变更并不是同步发生的挂失卡片的瞬间账户余额要冻结但账户本身并没有销户补卡后新卡正常使用账户状态仍然继承。把卡和账户合成一张表挂失和补卡时就要反复改同一行的同一批字段稍微一个事务漏提交数据就互相污染。校园卡管理系统里最少要有五个核心实体。持卡人统一建模学生和教职工都放进同一张表用身份类型区分校园卡一张实体表存物理卡号、卡面号、状态账户是资金实体余额和冻结标记与实体卡分离商户是收款方终端挂靠在商户下交易流水记录每一笔充值、消费、退款是系统里增量最大的一张表。把这五个实体列成清单属性边界就清楚了实体核心属性备注持卡人学号、姓名、证件类型、证件号、手机号、状态学生与教职工共用校园卡卡面号、物理卡号、持卡人ID、账户ID、状态一人可多卡含历史卡账户持卡人ID、余额、冻结标记、乐观锁版本号一持卡人一个活跃账户商户商户名称、类型、状态含食堂、超市、门禁维护方终端终端编号、所属商户ID、位置消费 POS、门禁读卡器交易流水业务流水号、卡ID、账户ID、商户ID、金额、类型只追加不修改不删除交易流水必须独立建模而不是当卡片的属性集合一笔流水同时关联卡片、商户、终端三个对象而且流水只有「插入」和「查询」两种操作天然要单独成表。很多课程设计把流水设计成卡片表的一个 JSON 字段或一张弱表后续做对账、日结、审计时全部卡壳。2.3 联系与基数1:1、1:N、M:N 的三个决策检查点实体之间的联系方式是 ER 图里最容易扣分的地方。校园卡系统里的基数关系不复杂但每个决策点背后都对应真实业务约束持卡人与校园卡是 1:N因为补卡后旧卡仍在历史数据里存在卡片与账户是 1:1业务上一个活跃持卡人只对应一个资金账户补卡不换账户卡片与流水是 1:N商户与流水是 1:N终端与流水是 1:N如果需求包含门禁授权卡片与门禁点是 M:N必须建授权中间表。检查基数设计时我习惯用三个问题自测。第一挂失时能不能只改卡状态、不碰账户余额如果答案是不能说明卡和账户的职责没有拆干净。第二一个商户有多个终端报表是按商户汇总还是按终端汇总消费流水必须能同时回溯「哪个商户、哪个终端」所以终端 ID 和商户 ID 都要出现在流水里。第三学校大门门禁和宿舍门禁是同一个实体吗如果两个系统的授权时间、授权范围不一致就应当拆成两类门禁点而不是在一个实体上堆一堆用不到的属性。ER 图画到这里已经能看出这套设计的骨架五张核心实体表加若干联系表后面逻辑结构设计就是把这张图翻译成关系模式。这一步不要省评审老师问「为什么有这个表、为什么这样关联」时ER 图就是你最直接的答辩证据。3. 逻辑结构设计ER 转关系模式范式检查别只停留在理论概念结构定了下一步是把 ER 模型转成关系模式。这一步教材里叫逻辑结构设计落到课程设计里就是「定表结构、定主外键、查范式」。范式不是用来背名词的流水表、账户表里那几个典型问题能用范式理论解释清楚比堆一堆教科书原话更有说服力。3.1 转换规则与第一版关系模式清单ER 转关系模式有三条通用规则。1:1 联系尽量把一方的主键放进另一方用作外键比如卡片表里直接放 account_id1:N 联系把 1 方主键放进 N 方流水表里放 card_id、merchant_idM:N 联系必须建独立中间表卡片与门禁点的授权关系就落在授权表上。按照这套规则上一章的 ER 图落成关系模式后是这样一组表关系模式主键外键 / 关联字段持卡人表holder_id无校园卡表card_idholder_id, account_id账户表account_idholder_id商户表merchant_id无终端表terminal_idmerchant_id交易流水表trans_idholder_id, card_id, account_id, merchant_id, terminal_id授权表可选auth_idcard_id, gate_id门禁点表可选gate_id无这版表清单能覆盖校园卡系统的核心场景消费、充值、挂失、补卡、商户日结、流水查询。是不是还需要扩展表要看选题范围。如果题目给了图书馆借阅或考勤场景就再往里加实体如果只要求校园卡消费和充值这八张表已经足够撑起一篇课程设计。3.2 用流水表做范式体检两个必改的反例范式检查不要对着每一张表空喊「这符合 3NF」要拿着具体字段做判断。流水表是最容易出问题的举两个典型反例。第一个反例流水表里直接存了商户名称而不是 merchant_id。这违反第三范式。商户名称属于商户表如果商户改名叫「第一食堂」历史流水里的旧名称不会跟着变日终报表里就会出现两张名字不同的同一商户数据。正确做法是流水表只存 merchant_id需要商户名时通过 JOIN 关联查询。第二个反例流水表以 (card_id, trans_id) 做联合主键。这种设计问题更隐蔽。联合主键意味着表的主键由两个字段共同构成但 trans_time、trans_type 这些字段只依赖于 trans_id不依赖于 card_id这就产生了部分函数依赖表停留在第一范式到第二范式之间。实际业务里卡片丢失补办后 card_id 变化历史流水的主键也跟着变化整个关联全乱。正确设计是流水表用单列自增主键 trans_idcard_id 作为普通字段加索引。单列主键消灭部分依赖流水类型、交易时间都完全依赖于主键整个表天然符合 2NF 和 3NF。这是答辩时最值得主动展示的一个分析过程。3.3 合法反范式与冗余字段的答辩口径范式检查通过后还有一个绕不开的问题流水表里要不要存 balance_after交易后余额从第三范式角度看交易后余额可以由账户表的 balance 反推属于冗余字段。但从账务系统角度看这个字段又必须存在因为流水表承担审计职责要还原交易发生那一刻的账户状态如果账户余额后来被别的交易修改再查流水就看不到「当时剩多少」了。这种冗余属于可控反范式课程设计文档里应当主动写一段设计说明balance_after 字段的取值由同一事务内的扣款语句写入来源是账户表更新后的余额不存在多处维护的问题它服务的是对账和审计需求不参与日常余额计算。同样道理流水表冗余 holder_id 也是为补卡后按人查流水服务的。把「为什么冗余、谁来保证一致」写清楚评审老师不但不会扣分反而会觉得你有工程意识。4. 物理设计与建表落地一套可以直接抄的 MySQL DDL逻辑结构设计完成后进入物理设计选引擎、定字符集、写建表语句、设计索引。这部分是「数据库 SQL」和「数据库优化」最集中的体现也是整套设计能不能跑起来的关键。课程设计答辩时要现场演示SQL 写得不好不仅在 MySQL 里报错性能演示时还会卡得尴尬。4.1 引擎、字符集与金额字段的三个硬性选择建库之前先定三个全局参数。存储引擎选 InnoDB理由是事务安全和行级锁。校园卡消费是典型的小事务高并发场景扣款和写流水必须在一个事务里完成MyISAM 不支持事务直接排除。字符集选 utf8mb4排序规则用 utf8mb4_general_ci。utf8mb4 能存生僻字和 emoji商户名里出现特殊符号也不会写入失败只存中文姓名的老设计用 utf8 够用但课程设计没必要冒这个风险。金额字段必须用 DECIMAL这是血泪经验。FLOAT 和 DOUBLE 是浮点数二进制无法精确表示所有十进制小数0.1 加 0.2 都可能出现 0.30000000000000004。校园卡余额和流水金额涉及钱累计误差到月底对账时就是翻车现场。余额字段用 DECIMAL(10,2)能表示到千万级别的金额校园卡场景绰绰有余。建库语句收在这里后面所有表都默认落在这个库下CREATE DATABASE campus_card DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;逻辑说明数据库级的字符集设置决定了后续建表不指定字符集时的默认值避免每张表单独声明还出现遗漏。排序规则选 general_ci 足够支持中文与英文字符的等值查询和排序性能比 utf8mb4_unicode_ci 略好。4.2 四张核心表的建表 SQL账户、卡片、商户、流水建表时优先建被引用的基础表。这里按「先账户、再卡片、再商户、最后流水」的顺序给出四张主表。每张表的注释字段和业务约束都写进 DDL方便课程设计文档直接截图。CREATE TABLE t_account ( account_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT 账户ID, holder_id BIGINT UNSIGNED NOT NULL COMMENT 持卡人ID, balance DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 账户余额单位元, freeze_flag TINYINT NOT NULL DEFAULT 0 COMMENT 冻结标记0正常 1冻结, version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, UNIQUE KEY uk_holder (holder_id) ) ENGINEInnoDB COMMENT资金账户表;逻辑说明balance 用 DECIMAL(10,2)严禁换成 DOUBLEfreeze_flag 用 TINYINT 存字典值不存字符串节省空间且状态扩展方便version 字段为并发扣款做乐观锁准备holder_id 上有唯一键保证一个持卡人只有一个活跃账户。CREATE TABLE t_card ( card_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT 卡ID, holder_id BIGINT UNSIGNED NOT NULL COMMENT 持卡人ID, account_id BIGINT UNSIGNED NOT NULL COMMENT 账户ID, card_no VARCHAR(32) NOT NULL COMMENT 卡面号, chip_no VARCHAR(32) NOT NULL COMMENT 芯片物理号, status TINYINT NOT NULL DEFAULT 1 COMMENT 卡状态0注销 1正常 2挂失, issue_time DATETIME NOT NULL COMMENT 发卡时间, expire_time DATETIME NULL COMMENT 有效期NULL为长期有效, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, UNIQUE KEY uk_card_no (card_no), KEY idx_holder (holder_id) ) ENGINEInnoDB COMMENT校园卡表;逻辑说明chip_no 是物理卡号卡面号可以补印物理号不可以所以两者分开存status 用 0/1/2 字典编码挂失是状态更新不是删行idx_holder 支撑按持卡人查所有历史卡。CREATE TABLE t_merchant ( merchant_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT 商户ID, merchant_name VARCHAR(64) NOT NULL COMMENT 商户名称, category TINYINT NOT NULL DEFAULT 0 COMMENT 商户类型0食堂 1超市 2门禁, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态0停用 1正常, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB COMMENT商户表;逻辑说明商户类型用字典值而不是字符串日结报表要按类型分组统计TINYINT 更高效停用商户用状态位标记不删除历史数据流水表里的关联才不会断。CREATE TABLE t_transaction ( trans_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT 流水ID, trans_no VARCHAR(40) NOT NULL COMMENT 业务流水号全局唯一, holder_id BIGINT UNSIGNED NOT NULL COMMENT 持卡人ID冗余用于按人查询, card_id BIGINT UNSIGNED NOT NULL COMMENT 卡ID, account_id BIGINT UNSIGNED NOT NULL COMMENT 账户ID, merchant_id BIGINT UNSIGNED NULL COMMENT 商户ID充值场景为NULL, terminal_id BIGINT UNSIGNED NULL COMMENT 终端ID, trans_type TINYINT NOT NULL COMMENT 流水类型1消费 2充值 3退款 4圈存, amount DECIMAL(10,2) NOT NULL COMMENT 变动金额充值正数消费负数, balance_after DECIMAL(10,2) NOT NULL COMMENT 交易后账户余额, status TINYINT NOT NULL DEFAULT 1 COMMENT 流水状态0失败 1成功 2冲正, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 交易时间, UNIQUE KEY uk_trans_no (trans_no), KEY idx_card_time (card_id, create_time), KEY idx_merchant_time (merchant_id, create_time), KEY idx_holder_time (holder_id, create_time) ) ENGINEInnoDB COMMENT交易流水表;逻辑说明trans_no 由业务系统生成例如「时间戳随机数」或「日期终端号序号」数据库里加唯一键防止重复记账amount 用正负号区分入账和出账对账时 SUM(amount) 直接出净变动额balance_after 是反范式字段作用已在上一章说明。这四张表建完校园卡的核心数据模型已经可以支撑消费、充值、挂失、补卡和日终对账。4.3 索引设计流水表的查询路径决定了联合索引索引不是建得越多越好。流水表是增量最大、查询最频繁的表索引要围绕真实查询路径设计。校园卡场景里高频查询有三种按卡查某张卡的历史流水、按商户查某天营业额、按持卡人查全部卡的流水。对应到索引就是三个联合索引。ALTER TABLE t_transaction ADD KEY idx_card_time (card_id, create_time), ADD KEY idx_merchant_time (merchant_id, create_time), ADD KEY idx_holder_time (holder_id, create_time);逻辑说明联合索引把筛选字段和排序字段揉在一起走索引就能同时完成 WHERE 过滤和 ORDER BY 排序避免 filesort。不要给 trans_type、status 这类低基数字段单独建单列索引区分度太低优化器大概率不走索引反而增加写入开销。数据量大了以后流水表要按时间归档。常见做法是保留最近一年数据在在线表里历史流水迁移到备份库或归档表也可以在建表时就按月份做 RANGE 分区比如 PARTITION BY RANGE (TO_DAYS(create_time))每个月一个分区查询只扫对应分区。课程设计阶段插入几万条测试数据验证索引效果即可分区方案写在文档里作为扩展方案。4.4 落库规范隐私加密、状态字典与并发版本号除了主表 DDL还有三条落库规范建议写进课程设计文档。证件号属于隐私字段不要明文落库应用层用加密算法处理后再写入展示时再解密数据库里被拖库也不会直接泄露明文。所有状态字段统一用 TINYINT 字典编码并在 COMMENT 里写明每个值的含义这比在 Java 代码里定义魔法数字可维护得多。账户表保留 version 字段配合条件 UPDATE 实现乐观锁这是下一条避坑内容里的关键依赖。5. 避坑校园卡设计里五个典型的翻车现场课程设计答辩翻车几乎都翻在数据一致性问题上。这里列五个我在实际排错中见过最多的问题每一条都按「现象到原因再到解决」的顺序讲后面压测时能直接验证。5.1 先查余额再扣款并发场景把余额扣成负数现象模拟两个终端同时消费同一张卡账户余额变成负数或者余额明明够第二次扣款却失败。原因应用程序先执行 SELECT balance再在 Java 或 Python 里判断余额是否足够最后执行 UPDATE balance balance - amount。两个事务同时读到余额 50 元各自判断「够扣 30 元」先后更新后余额变成 -10 元。这是数据库并发锁没控制好不是 MySQL 的问题。解决把「判断余额」和「扣款」合并成一条原子 UPDATE由数据库的行锁保证并发安全UPDATE t_account SET balance balance - 30.00, version version 1 WHERE account_id 1001 AND balance 30.00 AND freeze_flag 0;逻辑说明这条语句执行时InnoDB 对 account_id 1001 的行加行级锁余额判断和扣款在同一原子操作里完成如果受影响行数为 0说明余额不足或账户冻结应用程序再返回对应提示。业务侧不要先 SELECT 再 UPDATE这是并发扣款的第一原则。5.2 补卡后历史流水查不到问题出在按卡号关联现象学生丢卡补办后在查询界面按新卡号只能看到补卡后的流水旧卡消费记录全部消失。原因流水表与卡片表通过 card_id 关联查询界面把「当前卡号」传进去过滤流水。旧卡已经被标记注销新卡和旧卡的 card_id 不同按新卡查自然查不到旧卡的数据。解决流水表冗余 holder_id查询按持卡人维度过滤而不是按卡号过滤。补卡不改 holder_id同一持卡人的新卡旧卡流水都能查到。如果还想区分「这张交易是用哪张实体卡刷的」列表里展示 card_no 即可关联关系由流水表里的 card_id 保证。5.3 挂失的卡还能刷成功状态没参与扣款条件现象卡片挂失后门禁或消费仍然产生成功流水资金被扣走。原因挂失业务更新了 t_card.status但扣款 SQL 只校验了账户余额没有校验卡片状态和账户冻结标记。应用代码里虽然先查了卡状态但查状态和扣款是两个独立操作中间卡被挂失也拦不住。解决扣款条件里同时带上卡片状态和账户冻结标记并把它们写进同一条 SQL。扣款时 JOIN t_card 校验 status 1账户侧校验 freeze_flag 0。挂失事务要把「卡状态置为 2」和「账户 freeze_flag 置为 1」放进同一个数据库事务任何一个失败都整体回滚。5.4 日终对账永远差几分钱流水表分散是根源现象日终把消费流水、充值流水分别汇总和 t_account 的余额总额对不上差额不是整数怎么查都查不出来。原因两个问题叠加。第一消费流水和充值流水被拆成两张表某张表漏记或记错一笔对账时只能靠肉眼比对第二余额变动和流水写入不在同一事务里扣款成功但插流水失败账户余额变了流水没记录。解决统一为一张流水表用 trans_type 区分业务类型余额变动和流水插入放进同一个事务要么都成功要么都回滚。对账时一条 SQL 就能完成SELECT account_id, SUM(amount) FROM t_transaction WHERE status 1 GROUP BY account_id再与 t_account.balance 逐行比对。这个设计在第 4 章建表时已经预留好了。5.5 emoji 商户名写入报错建库字符集选错了现象插入商户名「奶茶店」的 emoji 变体时报 Incorrect string value但中文插入一切正常。原因数据库字符集是 utf8MySQL 的 utf8 实际最多 3 字节而 emoji 需要 4 字节存储。建库时没选 utf8mb4等于把 4 字节字符直接拒之门外。解决建库时指定 utf8mb4 和 utf8mb4_general_ci已经建好的库用 ALTER DATABASE 和 ALTER TABLE 转成 utf8mb4。应用层连接串也要加 useUnicodetruecharacterEncodingutf8mb4否则程序层转码仍会失败。字符串字段长度也要注意utf8mb4 下 VARCHAR(64) 存 emoji 占用的字节数比中文多长度评估要留余量。6. 验证与进阶对账视图、并发压测和答辩的几个关键口径设计写完怎么证明它站得住这里给三样东西两张对账视图、一个并发压测脚本、三个答辩口径。对账视图把「账户余额」和「流水汇总」拉到同一张表里对比日终对账从跑批变成一次查询CREATE VIEW v_balance_check AS SELECT a.account_id, a.balance AS account_balance, COALESCE(SUM(t.amount), 0) AS flow_sum FROM t_account a LEFT JOIN t_transaction t ON t.account_id a.account_id AND t.status 1 GROUP BY a.account_id, a.balance;逻辑说明视图把账户余额与流水净变动额并列展示两者不一致的行就是要排查的差异行。商户日结同理按商户和日期分组统计营业额CREATE VIEW v_merchant_daily AS SELECT merchant_id, DATE(create_time) AS trade_date, SUM(CASE WHEN trans_type 1 THEN amount ELSE 0 END) AS sale_amount, COUNT(*) AS trans_count FROM t_transaction WHERE status 1 GROUP BY merchant_id, DATE(create_time);并发扣款验证用 Python 多线程循环跑同一账户的扣款 SQL观察最终余额是否等于初始余额减去成功扣款总额import threading import pymysql config dict(host127.0.0.1, userroot, password123456, databasecampus_card, charsetutf8mb4) def deduct(account_id, amount): conn pymysql.connect(**config) try: with conn.cursor() as cur: affected cur.execute( UPDATE t_account SET balance balance - %s, version version 1 WHERE account_id %s AND balance %s AND freeze_flag 0, (amount, account_id, amount)) if affected 0: print(扣款失败余额不足或账户冻结) return cur.execute( INSERT INTO t_transaction (trans_no, holder_id, card_id, account_id, merchant_id, terminal_id, trans_type, amount, balance_after, status) VALUES (%s, 1, 1, %s, 1, 1, 1, %s, (SELECT balance FROM t_account WHERE account_id %s), 1), (fTEST{account_id}{amount}, account_id, amount, account_id)) conn.commit() finally: conn.close() threads [threading.Thread(targetdeduct, args(1, 10)) for _ in range(100)] for t in threads: t.start() for t in threads: t.join()逻辑说明100 个线程同时扣同一个账户 10 元初始余额 1000 元时成功扣款次数应为 100最终余额 0如果出现负数或扣款次数超过 100说明并发控制失效。balance_after 用 UPDATE 后的账户余额查询写入避免应用层读到旧值。答辩时三个关键口径要提前准备好。第一范式问题怎么解释流水表里的 balance_after 和 holder_id 是可控冗余服务于审计和按人查询由事务保证一致不是设计疏漏。第二为什么用 DECIMAL 而不用 FLOAT金额精度是账务系统的底线浮点数累加会产生误差。第三为什么扣款不用存储过程业务扣款逻辑涉及多表状态校验放在应用层用事务控制更灵活触发器能自动记账但会放大锁竞争高并发场景下性能不划算。我自己的习惯是课程设计交付前一定跑一遍这个压测脚本并保留执行结果截图放进文档。答辩时直接展示「100 并发扣款后余额不为负、流水总数正确」比背十页范式定义都管用。希望帮到你。本文还有配套的精品资源点击获取
返回列表