简介:西北工业大学《数据库原理》课程实验报告五,围绕数据库基本操作与安全管理展开,适用于正在学习数据库原理、需要实验参考的本科生。报告完整覆盖了视图重命名、带参数存储过程、加密存储过程以及多种触发器的创建与验证,包括插入、删除、更新触发器对数据一致性的维护,并涉及自动更新统计表的联动机制,能够帮助读者深入理解T-SQL语法和数据库对象的管理方法。压缩包内为单个doc文档,大小约231KB,内含具体实验步骤、SQL语句及运行验证截图,可以按步骤复现实验或借鉴撰写实验报告。已有712人学习浏览,适合数据库初学者及需要系统练习存储过程与触发器开发的人群参考。整体结构清晰,步骤与注释完整,所附存储过程及触发器样例均可直接运行,方便边学边练。
1. 数据库实验报告5:这节实验不以增删改查为主,考察的是数据一致性的边界
数据库课程实验前几节一般是建库、单表查询、多表连接和视图索引,做到第五次实验报告时,话题往往已经推进到数据库并发控制和完整性约束。事务提交、回滚、脏读、不可重复读、死锁,这些词在《西北工业大学数据库实验报告5.doc》这类文档里,要求的不再是背定义,而是在两个会话里用 SQL 亲手把现象复现出来。很多人把这节实验当成“把课堂上的语句再写一遍”,实际它考察的是另一个维度:当两个连接同时操作同一条记录,数据库的锁和隔离级别到底拦下了什么、又放过了什么。下面这条线,正好能作为一份数据库实验报告5的完整展开。
2. 事务隔离与锁:把实验5背后的概念先放在一张表里
2.1 从 ACID 说起,实验报告该解释到什么程度
实验报告5如果涉及事务,第一页逃不开 ACID。许多学生把原子性、一致性、隔离性、持久性四行字抄进报告就算完,但数据库实验5的答辩通常会追问一句:原子性和持久性靠什么机制保证?答案是日志。redo log 负责崩溃恢复时把已提交事务重放,undo log 负责回滚时把旧值恢复;InnoDB 里这两个日志配合 change buffer、doublewrite buffer 一起工作,才是完整的持久性和原子性链条。
一致性不是数据库单独保证的,它由应用层约束、外键、CHECK 约束和触发器共同兜底。实验报告里如果出现“数据库保证一致性”这种结论,严谨程度是不够的。更准确的说法是:原子性和隔离性为一致性创造条件,但业务规则最终由约束来执行。把这句话写进实验报告,比空谈 ACID 更站得住。
2.2 隔离级别和异常现象:报告里最好带一张对照表
隔离级别是实验5最容易出彩的部分,也是最能体现“动手验证”价值的地方。SQL 标准定义了四个级别,不同数据库实现程度不同:MySQL 的默认隔离级别是 REPEATABLE READ,Oracle 和达梦默认是 READ COMMITTED,这个差异经常被实验者忽略,导致在两个不同数据库上跑出完全不同的现象。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 加锁实现 |
|---|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 | 读不加锁 |
| READ COMMITTED | 避免 | 可能 | 可能 | 行级锁,读已提交快照 |
| REPEATABLE READ | 避免 | 避免 | 可能(InnoDB 用间隙锁解决) | 事务内快照一致 |
| SERIALIZABLE | 避免 | 避免 | 避免 | 所有读加锁 |
表格里的“可能”和“避免”是实验报告里必须亲手验证的。认真做实验5的人会发现,InnoDB 的 REPEATABLE READ 下,普通的快照读已经解决了幻读,只有当前读(带 FOR UPDATE 或 LOCK IN SHARE MODE)配合间隙锁才能彻底锁住范围。实验报告中如果能把“标准定义”和“InnoDB 实际行为”区分开,这个实验基本就是高分。
2.3 锁的类型:读锁、写锁、间隙锁分别在挡住什么
事务隔离级别的底层是锁和 MVCC。读锁(共享锁)之间不互斥,读锁和写锁(排他锁)互斥,写锁之间也互斥,这是数据库并发控制的基本盘。实验5里最常见的错误是把“行锁”理解成“锁住一行”,实际上 InnoDB 的行锁锁的是索引记录,不是行本身。如果更新条件没有走索引,InnoDB 会升级为锁住所有扫描到的记录,表现为锁表。
间隙锁是 REPEATABLE READ 隔离级别下 InnoDB 特有的机制。两个事务同时往一个范围的间隙里插入数据时,间隙锁会让后到的插入阻塞,从而解决幻读。但这也有反作用:间隙锁扩大了锁范围,死锁概率随之上升。实验报告里如果能把死锁发生的原因定位到“两个事务以不同顺序请求同一组索引记录”,就已经超出课程要求,直接到了生产环境排查的思路。
3. 把实验环境搭起来:表结构、事务脚本和完整性约束
3.1 建表语句:实验5里最容易被忽略的约束设计
很多实验指导书的建表语句只留主键和外键,其他约束一律省略。但实验5考察完整性约束时,表结构本身就需要精心设计。常见做法是模拟一个教务或订单场景,这里以订单和库存两个表为基线,它们之间的外键、非空、默认值、CHECK 约束都要体现出来。
CREATE TABLE product ( product_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '商品ID', product_name VARCHAR(64) NOT NULL COMMENT '商品名称', stock_qty INT NOT NULL DEFAULT 0 COMMENT '库存数量,不允许负库存', price DECIMAL(10,2) NOT NULL COMMENT '单价', CONSTRAINT ck_product_stock CHECK (stock_qty >= 0) ) ENGINE=InnoDB COMMENT='商品表'; CREATE TABLE order_detail ( order_id INT NOT NULL COMMENT '订单号', product_id INT NOT NULL, quantity INT NOT NULL COMMENT '购买数量', order_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (order_id, product_id), CONSTRAINT fk_order_product FOREIGN KEY (product_id) REFERENCES product(product_id) ON UPDATE CASCADE ON DELETE RESTRICT ) ENGINE=InnoDB COMMENT='订单明细表';这段 DDL 的逻辑说明:NOT NULL 用 product_name 和 quantity 上,确保业务核心字段不出现空值;CHECK (stock_qty >= 0) 把负库存挡在数据库入口,任何应用层绕过校验的负数写入都会直接报错;外键的 ON UPDATE CASCADE 配合 ON DELETE RESTRICT,则保证商品主键变动时订单明细跟着更新,但商品删除若还有订单引用,会被拒绝。参数说明:ENGINE=InnoDB 是必须的,MyISAM 不支持外键约束和事务,实验5跑事务的环节在 MyISAM 下完全不成立。
3.2 第一个事务脚本:提交、回滚、保存点怎么演示
事务实验的最小复现,是同一事务里先扣库存再写订单明细,中间故意制造失败,观察回滚。下面的脚本在两个会话里都需要执行,为了区分建议在命令行或 Navicat 中分别开两个查询窗口。
-- 会话A:开启事务,扣减库存,然后暂停等待输入 START TRANSACTION; UPDATE product SET stock_qty = stock_qty - 1 WHERE product_id = 1; SELECT * FROM product WHERE product_id = 1; -- 此时先不提交,切到会话B执行查询,观察B能否读到stock_qty=99 -- 继续会话A:插入订单明细,然后回滚 INSERT INTO order_detail(order_id, product_id, quantity) VALUES (1001, 1, 1); ROLLBACK; -- 回滚后验证:会话B重新查询,看到stock_qty仍为100,且order_detail无1001记录脚本逻辑说明:UPDATE 之后的 SELECT 能读到修改后的值,是因为当前会话读自己未提交的修改,这是数据库的事务内可见性规则,不叫脏读;脏读要发生在另一个事务。ROLLBACK 之后,UPDATE 和 INSERT 的效果一起撤销,两个表回到事务前状态,用户能观察到原子性如何体现在“库存在,订单也写;库存不在,订单跟着不写”上。
命令参数说明:如果希望只撤销部分操作,用 SAVEPOINT。在 INSERT 之后设置保存点,再执行一条会失败的 SQL,最后 ROLLBACK TO SAVEPOINT,这样订单明细可以保留,库存扣减仍然生效。这种写法在报告里可以解释为“部分回滚用于长事务中保留已完成步骤”。
3.3 完整性约束触发实验:违反约束时数据库的动作
实验5另一类必写内容,是让数据库主动拒绝非法数据。把 PRODUCT 表里的商品库存改成 -5,看 CHECK 约束如何报错;再删除一条已经被 order_detail 引用的 product_id,看 RESTRICT 行为。这两步在实验报告里都要写清楚“执行语句 + 报错信息 + 违背哪种约束”。
-- 违反CHECK约束:期望报错 UPDATE product SET stock_qty = -5 WHERE product_id = 1; -- 期望看到类似错误: Check constraint 'ck_product_stock' is violated -- 违反外键约束:期望被拒绝 DELETE FROM product WHERE product_id = 1; -- 期望看到类似错误: Cannot delete or update a parent row: a foreign key constraint fails这里的价值在于让实验者理解:应用层可以做校验,但数据库约束是最后一道关卡。实验报告里常犯的错是只贴报错截图,不写约束名。正确做法是把约束名写进报告,如 ck_product_stock、fk_order_product,并能解释约束名如何帮助快速定位是哪个字段、哪条规则出了问题。
4. 并发实验:脏读、不可重复读与死锁的复现和排查
4.1 不同隔离级别下的脏读:只在 READ UNCOMMITTED 下能看见
并发实验是实验5的重头戏,需要两个会话配合,一个写,一个读。复现脏读的条件是两个事务都处于 READ UNCOMMITTED,这样会话B能读到会话A未提交的修改。
-- 会话A和会话B都先设置隔离级别 SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -- 会话A:更新库存,不提交 START TRANSACTION; UPDATE product SET stock_qty = stock_qty - 1 WHERE product_id = 1; -- 会话B:在未提交的情况下查询 SELECT stock_qty FROM product WHERE product_id = 1; -- 结果为49(假设原来50),读到的是会话A的未提交修改,产生脏读 -- 会话A:回滚 ROLLBACK; -- 会话B再次查询,结果恢复为50,证明之前读到的是脏数据实验逻辑说明:脏读的本质是读到临时中间状态,这个状态可能因回滚而消失。报告中要在查询结果旁边标注“此时会话A未提交”和“回滚后数值恢复”,才能证明观察到了脏读,而不是简单地把两个查询结果贴在一起。如果默认隔离级别为 REPEATABLE READ,上面的步骤不会出现脏读,B 读到的是事务开始前的那一版数据,这就是 MVCC 快照读的体现。
4.2 不可重复读:同一事务内两次查询结果不一致
不可重复读的复现比脏读多一步前提:两个会话都要在 READ COMMITTED 或更低级别。会话B在一个事务内先查一次,会话A修改并提交,会话B再查第二次,前后不一致就产生了。它的意义在于,事务B第一次读取过这条记录后,第二次读取时它已经变成另一个值,如果业务基于这一行做两次运算,就会拿旧值和新值分别计算,结果不稳定。
-- 会话B:设置为READ COMMITTED,开启事务并第一次查询 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; START TRANSACTION; SELECT stock_qty FROM product WHERE product_id = 1; -- 读到50 -- 会话A:更新并提交 UPDATE product SET stock_qty = 49 WHERE product_id = 1; COMMIT; -- 会话B:同一事务内第二次查询 SELECT stock_qty FROM product WHERE product_id = 1; -- 读到49,两次不一致 COMMIT;这里在 REPEATABLE READ 下重复相同步骤,会话B两次都读到 50,因为第一次 SELECT 产生的快照被整个事务复用。实验5报告中建议把同一段实验在 READ COMMITTED 和 REPEATABLE READ 下各跑一遍,两张结果对比,才算把隔离级别的差异讲透。注意快照读靠 MVCC,不阻塞其他事务的写操作,这是它和加锁读的本质区别。
4.3 死锁制造:两个事务按相反顺序更新同一组记录
死锁的经典复现是事务A先更新 product 表的 1 号记录,再更新 2 号记录;事务B正好相反,先更新 2 号再更新 1 号。两边各持一把锁等待对方的锁,僵持几秒后 InnoDB 会自动检测并回滚一个事务,另一个事务继续执行。
-- 会话A START TRANSACTION; UPDATE product SET price = price + 1 WHERE product_id = 1; -- 此时持有product_id=1的排他锁,等待product_id=2的锁 -- 会话B START TRANSACTION; UPDATE product SET price = price + 1 WHERE product_id = 2; -- 此时持有product_id=2的排他锁,等待product_id=1的锁 -- 返回会话A继续执行 UPDATE product SET price = price + 1 WHERE product_id = 2; -- 阻塞,等待会话B释放 -- 返回会话B继续执行 UPDATE product SET price = price + 1 WHERE product_id = 1; -- 触发死锁检测,其中一个会话报错并回滚实验报告需要记录死锁报错文本,例如“Deadlock found when trying to get lock; try restarting transaction”,并写明会话 B 被回滚、会话 A 成功提交,最后两个商品的价格都只加了 1 而不是各加 2 或加 3,说明死锁回滚保证了数据没有出现叠加错误。生产环境排查死锁时,用 SHOW ENGINE INNODB STATUS 查看 LATEST DETECTED DEADLOCK 段,能读到两个事务各自执行了哪条语句、等待了哪把锁,这也是实验5可以延伸掌握的排查手段。
5. 实验报告的最后一步:把现象变成可复现的证据
5.1 验证隔离级别的自证手段
写实验报告时,不能只贴“我执行了 SET TRANSACTION ISOLATION LEVEL”。一种可靠的验证方法是利用 information_schema 查询事务和锁信息,证明当前会话处于什么隔离级别、事务里持有多少把锁。
-- 查看当前会话的隔离级别 SELECT @@transaction_isolation; -- 查看正在运行的事务和锁等待关系 SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id FROM information_schema.innodb_trx; -- 查看锁等待发生时的阻塞源头 SELECT * FROM sys.innodb_lock_waits\G这三条 SQL 的用途分别是:第一条记录会话的隔离级别,配合实验现象一起放进报告,避免只写结论没有环境参数;第二条确认事务是在 RUNNING 还是 LOCK WAIT 状态,尤其是死锁实验前可以确认两个事务都已开启;第三条查看锁等待的来源表、来源线程和阻塞线程,这是排查死锁的完整证据链。使用时要说明版本差异:MySQL 5.7 用 @@tx_isolation,8.0 后才换成 @@transaction_isolation,达梦数据库的对应视图则是 v$trx 和相关动态视图。
5.2 报告里要写全的三类参数
一份有说服力的实验5报告,最少包含三类参数:数据库品牌和版本、隔离级别、事务操作顺序。版本对实验结果影响很大,例如 MySQL 8.0 的 REPEATABLE READ 行为和 5.7 基本一致,但锁信息和系统视图名称变了;达梦的默认隔离级别是 READ COMMITTED,复现不可重复读时不需要额外设置,直接跑 4.2 节的脚本即可。操作顺序用 A、B 会话分别列出,每条 SQL 前面标注“A1、A2、B1、B2”这样的步骤号,比对结果时只按行号引用,避免大段文字描述造成混乱。
5.3 收尾时留一个快照验证脚本
最后一个技巧是给报告附一段验证脚本,把实验后的数据状态和预期结果写成对照关系:product 表最终库存应为多少、order_detail 里是否包含某条记录。这样做的好处是让评审者快速确认事务是全部提交还是部分回滚。如果实验环境允许定时器,还可以在死锁实验后间隔 10 秒再查一次两张表的状态,证明死锁回滚后数据回到一致起点。实验5的全部意义就在于用数据变化证明机制,而不是用文字说明机制。
本文还有配套的精品资源,点击获取