MySQL 系列:第11篇 触发器与事件调度器

📅 2026/6/17 6:14:08
MySQL 系列:第11篇 触发器与事件调度器
IT策士 10余年一线大厂经验专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章助你少走弯路。前面的存储过程和自定义函数让我们学会了如何“封装”逻辑供主动调用。但还有一种需求当某件事发生时自动执行一段逻辑——比如插入订单后自动更新库存、每天凌晨自动清理过期数据。这就是 MySQL 的触发器Trigger和事件调度器Event Scheduler。今天用 Python 配合实战把它们彻底讲透并警示你生产环境中的坑。1. 准备数据订单 库存 日志三张表我们用“下单自动扣库存、记日志”这一经典场景来演示触发器。importmysql.connector connmysql.connector.connect(host127.0.0.1,port3306,userroot,passwordMyNewPass123!,databaseshop)cursorconn.cursor()# 商品表cursor.execute( CREATE TABLE IF NOT EXISTS products(idINT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(100)NOT NULL, stock INT NOT NULL DEFAULT0, sales INT NOT NULL DEFAULT0)ENGINEInnoDB)# 订单表cursor.execute( CREATE TABLE IF NOT EXISTS orders(idINT AUTO_INCREMENT PRIMARY KEY, product_id INT NOT NULL, quantity INT NOT NULL, total_price DECIMAL(10,2)NOT NULL, customer_name VARCHAR(50), status VARCHAR(20)DEFAULTpending, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)ENGINEInnoDB)# 日志表cursor.execute( CREATE TABLE IF NOT EXISTS operation_log(idINT AUTO_INCREMENT PRIMARY KEY, table_name VARCHAR(50), operation VARCHAR(20), record_id INT, detail TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)ENGINEInnoDB)# 清空并灌入测试数据cursor.execute(TRUNCATE products)cursor.execute(TRUNCATE orders)cursor.execute(TRUNCATE operation_log)products[(1,机械键盘,100,0),(2,蓝牙耳机,50,0),(3,显示器,30,0),]cursor.executemany(INSERT INTO products (id, title, stock, sales) VALUES (%s,%s,%s,%s), products)conn.commit()print(✅ 测试环境准备完毕)2. 触发器Trigger数据变更时的自动响应触发器绑定在表上在INSERT / UPDATE / DELETE之前或之后自动执行。MySQL 5.7 之前一张表同事件同时机只能有一个触发器8.0 放宽此限制可多个按创建顺序执行。2.1 触发器六种时机2.2 实战订单创建后自动扣库存create_trigger_sql CREATE TRIGGER after_order_insert AFTER INSERT ON orders FOR EACH ROW BEGIN -- 扣减库存 UPDATE products SET stockstock - NEW.quantity, salessales NEW.quantity WHEREidNEW.product_id;-- 记录日志 INSERT INTO operation_log(table_name, operation, record_id, detail)VALUES(orders,INSERT, NEW.id, CONCAT(客户 , NEW.customer_name, 购买了 product_id, NEW.product_id, x, NEW.quantity));END cursor.execute(create_trigger_sql)print(✅ 触发器 after_order_insert 创建成功)核心关键字NEW引用插入或更新后的新行INSERT 和 UPDATE 中使用OLD引用删除或更新前的旧行DELETE 和 UPDATE 中使用2.3 用 Python 测试触发器# 查询触发前状态cursor.execute(SELECT id, title, stock, sales FROM products WHERE id 1)rowcursor.fetchone()print(f 下单前 - {row[1]}: 库存{row[2]}, 销量{row[3]})# 插入订单触发器自动扣库存cursor.execute( INSERT INTO orders(product_id, quantity, total_price, customer_name)VALUES(1,3,1197.00,张三))conn.commit()print(✅ 订单插入成功触发器已自动执行)# 查询触发后状态cursor.execute(SELECT id, title, stock, sales FROM products WHERE id 1)rowcursor.fetchone()print(f 下单后 - {row[1]}: 库存{row[2]}, 销量{row[3]})# 查看日志cursor.execute(SELECT * FROM operation_log ORDER BY id DESC LIMIT 1)logcursor.fetchone()print(f\n 自动日志: {log[4]} {log[5]})预期输出✅ 触发器 after_order_insert 创建成功 下单前 - 机械键盘: 库存100, 销量0✅ 订单插入成功触发器已自动执行 下单后 - 机械键盘: 库存97, 销量3 自动日志: 客户 张三 购买了product_id1x3 2025-07-2212:00:00触发器的效果透明且强制——无论通过哪种方式插入订单应用代码、命令行、其他存储过程库存都会自动扣减。2.4 BEFORE 触发器数据校验在插入前检查库存是否充足如果不足则拒绝cursor.execute( CREATE TRIGGER before_order_insert BEFORE INSERT ON orders FOR EACH ROW BEGIN DECLARE current_stock INT;SELECT stock INTO current_stock FROM products WHEREidNEW.product_id;IF current_stock IS NULL THEN SIGNAL SQLSTATE45000SET MESSAGE_TEXT商品不存在;ELSEIF current_stockNEW.quantity THEN SIGNAL SQLSTATE45000SET MESSAGE_TEXT库存不足;END IF;END)print(✅ BEFORE 触发器创建成功)# 测试库存不足try: cursor.execute(INSERT INTO orders (product_id, quantity, total_price, customer_name) VALUES (1, 999, 9999.00, 李四))conn.commit()except mysql.connector.Error as e: print(f❌ 触发器拦截: {e})预期输出✅ BEFORE 触发器创建成功 ❌ 触发器拦截:1644(45000): 库存不足SIGNAL SQLSTATE 45000是 MySQL 抛出自定义异常的标准方式会让插入操作直接失败。2.5 AFTER UPDATE 触发器价格变动告警当商品价格大幅波动时自动记录告警日志# 先给 products 表加 price 列cursor.execute(ALTER TABLE products ADD COLUMN IF NOT EXISTS price DECIMAL(10,2) DEFAULT 0)cursor.execute( CREATE TRIGGER after_product_price_change AFTER UPDATE ON products FOR EACH ROW BEGIN IF NEW.price!OLD.price THEN INSERT INTO operation_log(table_name, operation, record_id, detail)VALUES(products,UPDATE, NEW.id, CONCAT(价格变动: , OLD.price, - , NEW.price, (变动幅度: , ROUND((NEW.price-OLD.price)/OLD.price*100,1),%)));END IF;END)print(✅ 价格监控触发器创建成功)# 测试改价cursor.execute(UPDATE products SET price 399.00 WHERE id 1)cursor.execute(UPDATE products SET price 499.00 WHERE id 1)conn.commit()cursor.execute(SELECT detail FROM operation_log WHERE operation UPDATE AND table_name products)print(\n 价格变动日志)forrowincursor.fetchall(): print(f {row[0]})预期输出✅ 价格监控触发器创建成功 价格变动日志 价格变动:0.00-399.00(变动幅度:0.0%)价格变动:399.00-499.00(变动幅度:25.1%)注意OLD.price和NEW.price的对比UPDATE 触发器中既能访问旧值也能访问新值。2.6 查看与删除触发器# 查看所有触发器cursor.execute(SHOW TRIGGERS LIKE orders)print(\n orders 表上的触发器)forrowincursor.fetchall(): print(f {row[0]} - {row[4]} {row[5]})# 删除触发器cursor.execute(DROP TRIGGER IF EXISTS after_product_price_change)print(✅ 触发器已删除)3. 事件调度器Event Scheduler数据库里的 Cron触发器是被动的等数据变更而事件调度器是主动的——它按照预设的时间计划自动执行任务类似于 Linux 的crontab。3.1 开启事件调度器# 查看调度器状态cursor.execute(SHOW VARIABLES LIKE event_scheduler)print(f事件调度器状态: {cursor.fetchone()[1]})# 开启需要 SUPER 权限cursor.execute(SET GLOBAL event_scheduler ON)print(✅ 事件调度器已开启)3.2 创建定时任务每天清理过期日志cursor.execute( CREATE EVENT IF NOT EXISTS clean_old_logs ON SCHEDULE EVERY1DAY STARTS CURRENT_TIMESTAMP DO DELETE FROM operation_log WHERE created_atDATE_SUB(NOW(), INTERVAL90DAY))print(✅ 定时清理事件创建成功)EVERY 1 DAY每天执行一次STARTS CURRENT_TIMESTAMP从现在开始ENDS可选指定结束时间3.3 创建复杂事件每日销售统计cursor.execute( CREATE TABLE IF NOT EXISTS daily_stats(dateDATE PRIMARY KEY, order_count INT DEFAULT0, total_revenue DECIMAL(12,2)DEFAULT0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)ENGINEInnoDB)cursor.execute( CREATE EVENT IF NOT EXISTS generate_daily_report ON SCHEDULE EVERY1DAY STARTS CONCAT(CURDATE(), 23:59:00)DO BEGIN INSERT INTO daily_stats(date, order_count, total_revenue)SELECT CURDATE(), COUNT(*), COALESCE(SUM(total_price),0)FROM orders WHERE DATE(created_at)CURDATE()ON DUPLICATE KEY UPDATE order_countVALUES(order_count), total_revenueVALUES(total_revenue);END)print(✅ 每日报表事件创建成功)3.4 用 Python 管理事件# 查看所有事件cursor.execute(SHOW EVENTS FROM shop)print(\n shop 数据库的事件)forrowincursor.fetchall(): print(f {row[1]} - 每 {row[3]} 执行 - 状态: {row[5]})# 临时禁用事件cursor.execute(ALTER EVENT clean_old_logs DISABLE)print(✅ clean_old_logs 已禁用)# 手动触发一次事件方便测试cursor.execute(ALTER EVENT clean_old_logs ENABLE)cursor.execute(ALTER EVENT clean_old_logs ON COMPLETION PRESERVE)# 删除事件cursor.execute(DROP EVENT IF EXISTS generate_daily_report)print(✅ generate_daily_report 已删除)4. 性能陷阱与替代方案触发器和事件看似强大但生产环境中经常被称为“隐式炸弹”4.1 触发器的致命缺陷4.2 事件调度器的注意事项事件由单线程执行一个事件阻塞会拖累其他事件大量事件或长事务事件会导致事件延迟事件执行失败不会自动重试需要自行监控4.3 替代方案我的建议99% 的触发器需求都可以用应用层逻辑替代。如果你正在设计一个新系统优先在 Python 服务中显式实现业务逻辑把 MySQL 当成纯粹的数据存储引擎。只有在遗留系统维护或者对数据库层的透明性有硬性要求时才考虑触发器。5. 动手试试完善自动化体系基于现有表结构完成以下挑战创建一个 AFTER DELETE 触发器当订单被删除时自动回退库存stock OLD.quantity, sales - OLD.quantity并记录日志。创建一个事件每小时执行一次将statuspending且创建超过 24 小时的订单自动取消设为statusexpired。用 Python 插入一条订单后删除它验证库存是否回到原始值。思考题如果一条订单插入触发了 3 个 AFTER INSERT 触发器它们的执行顺序是怎样的如何指定参考代码# 1. AFTER DELETE 触发器cursor.execute( CREATE TRIGGER after_order_delete AFTER DELETE ON orders FOR EACH ROW BEGIN UPDATE products SET stockstock OLD.quantity, salessales - OLD.quantity WHEREidOLD.product_id;INSERT INTO operation_log(table_name, operation, record_id, detail)VALUES(orders,DELETE, OLD.id, CONCAT(退回库存 x, OLD.quantity));END)print(✅ 删除回退触发器创建成功)6. 总结今天我们掌握了 MySQL 的两种自动机制触发器数据变更时自动执行适合审计日志、强制校验但性能开销大、调试困难。事件调度器定时执行任务适合数据清理、报表生成但不应替代专业调度系统。核心原则能用应用层逻辑解决的问题不要下沉到数据库。触发器是“隐式魔法”在团队协作中往往带来更多麻烦。下一篇我们将进入权限与安全管理学习如何精细化控制数据库访问。下次见想了解更多还可以去各个平台搜索「IT策士」一起升级 IT 思维