一、在循环中提交
- 开发人员喜欢在循环中进行事务提交,这种方法有两个缺点:出现错误难处理以及效率低
演示案例
- 创建一个表
create table t1( a int not null, b varchar(80) )engine=innodb;
- 下面一个存储过程
delimiter // create procedure load1(count int unsigned) begin declare s int unsigned default 1; declare c char(80) default repeat('a',80); while s<=count do insert into t1 select NULL,c; commit; set s=s+1; end while; end; // delimiter ;
- 默认情况下,SQL语句都是自动提交的,也就是说在存储过程中,insert语句之后都会有一个隐式的commit操作,因此上面的存储过程也等价于:
delimiter // create procedure load2(count int unsigned) begin declare s int unsigned default 1; declare c char(80) default repeat('a',80); while s<=count do insert into t1 select NULL,c; set s=s+1; end while; end; // delimiter ;
- 这两个存储过程有两个问题:
- ①如果循环执行时发生错误,数据库会停留在一个未知的位置,因此很难进行处理
- ②性能问题:因为事务每提交一次就需要写一次重做日志,因此效率比较低
- 现在我们运行上面的两个存储过程,观察它们的运行的时间,可以看到差不多都为两秒多:
call load1(10000); truncate table t1; call load2(10000);
改进方法①
- 改进方法就是循环中的所有insert放在一个事务中进行提交,这样的话重做日志只需要写一次,因此效率提高了
delimiter // create procedure load3(count int unsigned) begin declare s int unsigned default 1; declare c char(80) default repeat('a',80); start transaction; while s<=count do insert into t1 select NULL,c; set s=s+1; end while; commit; end; // delimiter ;
- 接着运行这个存储过程,然后观察一下运行的时间,可以看到只运行了0.2秒:
truncate table t1; call load3(10000);
改进方法②
- 改进方法就是不在存储过程中开启事务,而是在运行存储过程前显式开启一个事务,如下所示:
truncate table t1; begin; call load2(10000); commit;
- 接着运行这个存储过程,然后观察一下运行的时间,可以看到只运行了0.18秒:
二、使用自动提交
- 自动提交不是一个好的习惯,因为这会是初级DBA容易犯错,另外还可能使一些开发人员产生错误的理解,如我们在上面介绍到的循环提交问题
- MySQL数据库默认设置使用自动提交(autocommit),可以使用下面的语句来关闭自动提交功能
set autocommit=0;
- 当然也可以使用start transaction或begin来显式开启一个事务。在显式开启事务之后,在默认设置下(即参数completion_type=0),MySQL会自动执行SET AUTOCOMMIT=0的命,在使用commit或rollback结束一个事务之后自动执行SET AUTOCOMMIT=1
不同编程语言API的自动提交
- 对于不同语言的API,自动提交也是不同的
- MySQL C API默认的提交方式是自动提交
- MySQL Python API则会自动执行SET AUTOCOMMIT=0,禁用自动提交
三、使用自动回滚
- InnoDB支持通过定义一个HANDLER来进行自动事务的回滚操作,如在一个存储过程中发生了错误会自动对其进行回滚操作
演示案例
- 因此我发现很多开发人员喜欢在应用程序中使用自动回滚,例如:
- 创建一个表
create table b( a int not null default 0, primary key(a) )engine=innodb default charset=latin1;
- 存储过程定义了一个exit类型的HANDLER,当捕获到错误时进行回滚:
delimiter // create procedure sp_auto_rollback_demo() begin declare exit handler for sqlexception rollback; start transaction; insert into b select 1; insert into b select 2; insert into b select 1; insert into b select 3; commit; end; // delimiter ;
- 因为表中a字段为主键,因此第三条insert语句会抛出错误,运行如下,表格没有插入任何数据:
- 但是这个存储过程我们不知道运行的结果是什么,因此我们需要在存储过程中加入一些判断条件,用来查看存储过程的执行结果,更改如下:
-- 发生错误时,先回滚然后返回-1;执行成功返回1 delimiter // create procedure sp_auto_rollback_demo2() begin declare exit handler for sqlexception begin rollback; select -1; end; start transaction; insert into b select 1; insert into b select 2; insert into b select 1; insert into b select 3; commit; select 1; end; // delimiter ;
- 当我们再次运行存储过程时,结果如下,可以看到存储过程返回了-1,存储过程运行失败:
- 但是问题没有解决,上面虽然在错误时返回-1,但是开发人员不知道自动回滚时发生的是什么错误
- 习惯使用自动回滚的人大多数是以前使用SQL Server的开发人员。在SQL Server中可以使用SET XABORT ON来自动回滚一个事务。但是SQL Server数据库不仅自动回滚当前的事务,还会抛出异常,开发人员可以捕获这个异常。因此SQL Server和MySQL数据库在这方面是有所不同的
- 就像前面说的那样,对事务的BEGIN、COMMIT、ROLLBACK操作应该交给程序段来完成,存储过程需要完成的只是一个逻辑的操作,即对逻辑进行封装。下面演示用Python编写的程序调用一个程序过程sp_rollback_demo,这里的存储过程和之前的存储过程sp_auto_rollback_demo在逻辑上完成的内容大致相同:
- 和sp_auto_rollback_demo不同的是,在sp_rollback_demo中去掉了对事务的控制语句,这些操作交给程序来完成,接下来查看test_demo.py的程序源代码:
- 运行这个程序:
- 在程序中控制事务的好处是:用户可以知道错误的原因。例如在这个例子中,我们知道发生了1062这个错误,错误的内容是Duplicate entry '1' for key 'PRIMARY',即发生了主键重复的错误。然后可以根据发生的原因进一步调试程序