各位靓仔靓女们,欢迎来到今天的MySQL进阶小课堂! 今天咱们聊点刺激的——触发器中的事务,看看这小小的触发器,如何跟它“爸爸”的事务玩耍,是抱紧大腿一起飞,还是独立自主搞事情。
一、开场白:触发器这玩意儿,到底算谁的?
想象一下,你写了个触发器,监视着orders
表,一旦有新订单插入,它就自动更新inventory
表。 这时候,如果插入orders
表的SQL失败了,你的触发器要不要跟着回滚? 这就是触发器和事务关系的灵魂拷问。
MySQL的触发器,默认情况下,就像一个乖宝宝,紧紧依偎在它“爸爸”的事务里。 也就是说,触发器和触发它的SQL语句,共享同一个事务。 如果触发语句失败了,触发器里的操作也会跟着回滚,反之亦然。
二、默认的“一家人”模式:隐式事务
先来个例子,直观感受一下:
-- 创建两个表
CREATE TABLE orders (
order_id INT PRIMARY KEY AUTO_INCREMENT,
customer_id INT,
amount DECIMAL(10, 2)
);
CREATE TABLE order_logs (
log_id INT PRIMARY KEY AUTO_INCREMENT,
order_id INT,
message VARCHAR(255)
);
-- 创建触发器,在orders表插入数据后,记录日志
DELIMITER //
CREATE TRIGGER after_order_insert
AFTER INSERT ON orders
FOR EACH ROW
BEGIN
INSERT INTO order_logs (order_id, message)
VALUES (NEW.order_id, 'New order created!');
END//
DELIMITER ;
-- 插入一条数据
INSERT INTO orders (customer_id, amount) VALUES (1, 100.00);
-- 查询数据
SELECT * FROM orders;
SELECT * FROM order_logs;
-- 模拟插入orders表失败的情况 (故意插入重复主键)
INSERT INTO orders (order_id, customer_id, amount) VALUES (1, 2, 200.00); -- 报错:Duplicate entry '1' for key 'orders.PRIMARY'
-- 再次查询数据,看order_logs表有没有留下痕迹
SELECT * FROM orders;
SELECT * FROM order_logs;
运行上面的代码,你会发现:
- 第一次插入成功,
orders
和order_logs
表都多了数据。 - 第二次插入失败,抛出主键冲突的错误。
- 再次查询
orders
和order_logs
,你会发现order_logs
表没有任何新增记录。 这是因为第一次插入orders
表触发了after_order_insert
触发器,而触发器执行的INSERT
操作和触发语句共享同一个事务。 第二次插入语句失败,整个事务回滚,包括触发器里的INSERT
操作。
这就是默认情况下的“一家人”模式:触发器和触发它的语句,同生共死,荣辱与共。
三、谁说触发器不能单飞?显式事务的妙用
但是,现实世界总是充满了“叛逆”因子。 有时候,我们希望触发器里的操作,即使触发它的语句失败了,也能保持住。 比如,记录操作日志,即使主业务失败了,日志还是要留存的。
这时候,就需要用到显式事务。 在触发器里,开启一个新的事务,让它独立于触发它的语句。
注意: MySQL 8.0.17及更高版本,才支持在触发器中使用显式事务。 低版本的MySQL不支持。
-- 假设我们已经有上面的orders和order_logs表
-- 修改触发器,在触发器中使用显式事务
DELIMITER //
CREATE TRIGGER after_order_insert
AFTER INSERT ON orders
FOR EACH ROW
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL; -- 重新抛出异常,让外部事务感知到错误
END;
START TRANSACTION;
INSERT INTO order_logs (order_id, message)
VALUES (NEW.order_id, 'New order created!');
COMMIT;
END//
DELIMITER ;
-- 再次模拟插入orders表失败的情况 (故意插入重复主键)
INSERT INTO orders (order_id, customer_id, amount) VALUES (1, 3, 300.00); -- 报错:Duplicate entry '1' for key 'orders.PRIMARY'
-- 再次查询数据
SELECT * FROM orders;
SELECT * FROM order_logs;
这次,即使插入orders
表失败了,order_logs
表里依然会留下日志记录。 因为触发器里的INSERT
操作,是在一个独立的事务里完成的,它不受外部事务回滚的影响。
代码解释:
DECLARE EXIT HANDLER FOR SQLEXCEPTION
: 这部分代码定义了一个异常处理器。 如果在START TRANSACTION
和COMMIT
之间的代码块中发生任何SQL异常,这个处理器就会被激活。BEGIN ... ROLLBACK; RESIGNAL; END
: 异常处理器的具体逻辑。ROLLBACK
: 回滚触发器内部的事务。虽然触发器内部的事务回滚了,但是外部事务依然会按照其自身的逻辑进行处理。RESIGNAL
: 重新抛出异常。这很重要,因为即使触发器内部的操作在一个独立的事务中,外部事务(即触发触发器的SQL语句的事务)仍然需要知道发生了错误。 如果没有RESIGNAL
,外部事务可能不会感知到错误,从而导致数据不一致。
START TRANSACTION
: 开启一个新的事务。INSERT INTO order_logs ...
: 执行插入日志的操作。COMMIT
: 提交事务。
四、触发器里的事务:一些需要注意的坑
虽然触发器里使用显式事务很强大,但也有些坑需要注意:
- 版本限制: 前面已经说过,MySQL 8.0.17之前版本不支持。
- 性能损耗: 开启新的事务,会增加数据库的开销,影响性能。 所以,要谨慎使用,只在必要的时候才用。
- 并发问题: 如果多个事务同时触发同一个触发器,可能会导致死锁。 需要仔细设计,避免并发冲突。
- 逻辑复杂性: 在触发器里处理事务,会增加代码的复杂性,降低可维护性。 要权衡利弊,尽量保持触发器的简单性。
RESIGNAL
的重要性: 务必使用RESIGNAL
重新抛出异常,否则外部事务无法感知到触发器内部的错误。
五、触发器的事务隔离级别:你说了不算
触发器没有自己的事务隔离级别。 触发器总是运行在与触发它的语句相同的事务隔离级别下。 你不能在触发器里设置SET TRANSACTION ISOLATION LEVEL
。 试图这样做会导致语法错误。
六、实战案例:审计日志的正确打开方式
说了这么多,来个实际的例子。 假设我们需要记录用户对products
表的修改,包括修改前后的值。
-- 创建products表
CREATE TABLE products (
product_id INT PRIMARY KEY AUTO_INCREMENT,
product_name VARCHAR(255),
price DECIMAL(10, 2)
);
-- 创建审计日志表
CREATE TABLE product_audit_logs (
log_id INT PRIMARY KEY AUTO_INCREMENT,
product_id INT,
old_product_name VARCHAR(255),
new_product_name VARCHAR(255),
old_price DECIMAL(10, 2),
new_price DECIMAL(10, 2),
modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 创建触发器,记录products表的更新
DELIMITER //
CREATE TRIGGER after_product_update
AFTER UPDATE ON products
FOR EACH ROW
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
INSERT INTO product_audit_logs (product_id, old_product_name, new_product_name, old_price, new_price)
VALUES (OLD.product_id, OLD.product_name, NEW.product_name, OLD.price, NEW.price);
COMMIT;
END//
DELIMITER ;
-- 更新products表的数据
UPDATE products SET price = 120.00 WHERE product_id = 1;
-- 模拟更新products表失败的情况 (违反唯一约束,假设product_name是唯一约束)
ALTER TABLE products ADD UNIQUE INDEX idx_product_name (product_name);
INSERT INTO products (product_name, price) VALUES ('Product A', 100.00);
UPDATE products SET product_name = 'Product A' WHERE product_id = 2; -- 违反唯一约束
-- 查询数据
SELECT * FROM products;
SELECT * FROM product_audit_logs;
这个例子中,即使更新products
表失败,product_audit_logs
表里也会留下审计记录。 保证了审计日志的完整性。
七、总结:触发器与事务,爱恨情仇一线牵
总结一下,触发器和事务的关系,就像一对欢喜冤家,既可以抱团取暖,也可以各自为战。 关键在于你如何选择。
模式 | 特点 | 适用场景 | 注意事项 |
---|---|---|---|
隐式事务 | 触发器和触发它的语句共享同一个事务。 如果触发语句失败,触发器里的操作也会回滚。 | 对数据一致性要求极高,触发器里的操作必须和触发语句保持原子性。 例如:订单系统,如果订单创建失败,库存不能减少。 | 无需额外配置,但要注意错误处理,确保触发器里的操作不会导致事务失败。 |
显式事务 | 触发器里开启一个新的事务,独立于触发它的语句。 即使触发语句失败,触发器里的操作也能保持住。 | 需要记录操作日志,审计信息等,即使主业务失败,日志也要留存。 例如:记录用户登录失败的日志,即使登录失败,也要记录登录时间和IP地址。 | MySQL版本必须是8.0.17及更高版本。 需要使用START TRANSACTION 、COMMIT 和ROLLBACK 语句。 务必使用RESIGNAL 重新抛出异常,否则外部事务无法感知到触发器内部的错误。 要注意性能损耗和并发问题。 |
没有事务 | 尽量不要在触发器中进行复杂的事务控制。 触发器本身的设计目标是简单、高效,过多的事务逻辑会增加复杂性,降低可维护性。 | 避免在触发器中进行复杂的业务逻辑处理。 | 保持触发器的简单性,尽量将复杂的业务逻辑放到应用层或者存储过程里处理。 |
记住,没有最好的方案,只有最适合的方案。 根据实际情况,选择合适的事务处理方式,才能让你的触发器发挥最大的威力。
今天的课就到这里, 各位靓仔靓女们,下课! 记得点赞,收藏,转发,一条龙服务哦! 拜拜!