MySQL编程进阶之:触发器中的事务:触发器如何与父事务共享或独立。

各位靓仔靓女们,欢迎来到今天的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;

运行上面的代码,你会发现:

  • 第一次插入成功,ordersorder_logs表都多了数据。
  • 第二次插入失败,抛出主键冲突的错误。
  • 再次查询ordersorder_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操作,是在一个独立的事务里完成的,它不受外部事务回滚的影响。

代码解释:

  1. DECLARE EXIT HANDLER FOR SQLEXCEPTION: 这部分代码定义了一个异常处理器。 如果在START TRANSACTIONCOMMIT之间的代码块中发生任何SQL异常,这个处理器就会被激活。
  2. BEGIN ... ROLLBACK; RESIGNAL; END: 异常处理器的具体逻辑。
    • ROLLBACK: 回滚触发器内部的事务。虽然触发器内部的事务回滚了,但是外部事务依然会按照其自身的逻辑进行处理。
    • RESIGNAL: 重新抛出异常。这很重要,因为即使触发器内部的操作在一个独立的事务中,外部事务(即触发触发器的SQL语句的事务)仍然需要知道发生了错误。 如果没有RESIGNAL,外部事务可能不会感知到错误,从而导致数据不一致。
  3. START TRANSACTION: 开启一个新的事务。
  4. INSERT INTO order_logs ...: 执行插入日志的操作。
  5. 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 TRANSACTIONCOMMITROLLBACK语句。 务必使用RESIGNAL 重新抛出异常,否则外部事务无法感知到触发器内部的错误。 要注意性能损耗和并发问题。
没有事务 尽量不要在触发器中进行复杂的事务控制。 触发器本身的设计目标是简单、高效,过多的事务逻辑会增加复杂性,降低可维护性。 避免在触发器中进行复杂的业务逻辑处理。 保持触发器的简单性,尽量将复杂的业务逻辑放到应用层或者存储过程里处理。

记住,没有最好的方案,只有最适合的方案。 根据实际情况,选择合适的事务处理方式,才能让你的触发器发挥最大的威力。

今天的课就到这里, 各位靓仔靓女们,下课! 记得点赞,收藏,转发,一条龙服务哦! 拜拜!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注