所谓事务(Transaction)就是通过保证一批次的操作要么完全执行要么根本不执行来维护数据库的完整性。举个烂大街的例子:A给B转账1000元,对应的SQL语句为:(未显式定义交易)UPDATEdeposit_tablesetdepositdeposit=deposit-1000WHEREname='A';UPDATEdeposit_tablesetdepositdeposit=deposit+1000WHEREname='B';运行后结果如下:mysql>SELECT*FROMdeposit_table;+------+--------+|name|deposit|+------+---------+|A|3000||B|5000|+-----+--------+这样可能会遇到问题,比如执行完第一条语句后,数据库崩溃了,最后的结果可能是这样的(待讨论,看接下来的SQL是否写入日志):+------+---------+|name|存款|+------+--------+|A|2000||B|5000|+-----+--------+A的1000元无缘无故消失了,这绝对不合适。创建交易是为了解决类似的问题。如果使用事务处理转账,对应的SQL为:STARTTRANSACTION;UPDATEdeposit_tablesetdepositdeposit=deposit-1000WHEREname='A';UPDATEdeposit_tablesetdepositdeposit=deposit+1000WHEREname='B';COMMIT;在原来的两条SQL语句前后加上STARTTRANSACTION和COMMIT,可以保证即使转账操作失败,A的余额也不会减少。仔细想了想,发现这个例子不是特别合适,因为数据库的故障恢复技术(后面会讲到)会影响到最终的结果,而且模拟这种故障并不容易。最后的结果只能猜测:)但是我想不出其他更合适的例子。接下来,我们将详细讨论交易的一些特征和(一些)实现细节。ACIDA:Atomicity(原子性)C:Consistency(一致性)I:Isolation(隔离)D:Durability(持久性)1.Atomicity(原子性)首先讲两个重要的概念:commit和return回滚(rollback),当我们执行一个提交操作,数据库将被永久修改,而执行回滚操作意味着数据库将撤消所有正在进行的未提交修改。注意,这里的持久化并不是说事务一完成数据就刷盘。即使没有刷入磁盘,M??ySQL也有日志机制保证修改不会丢失。事务是支持提交和回滚的工作单元。原子性是指当一个事务对数据库进行多次更改时,要么在事务提交时所有更改都成功,要么在事务回滚时所有更改都撤消。这是官方文档中的表述,但有些人似乎误解了commit语句。事实上,即使事务中的某条语句出现错误,一旦你执行了commit,之前正常的改动还是会被commit,MySQL不会自动判断事务中的SQL执行是否成功。让我们用一个例子来看看提交和回滚:mysql>SELECT*FROMdeposit_table;+------+--------+|name|deposit|+------+-------+|A|2000||B|6000|+------+--------+2rowsinset(0.04sec)mysql>STARTTRANSACTION;INSERTINTOdeposit_tableVALUES('C',7000);INSERTITOdeposit_tableVALUES('D',8000);#再次插入D,由于主键的唯一性,语句会失败INSERTINTOdeposit_tableVALUES('D',9000);COMMIT;#提交事务QueryOK,0rowsaffected(0.00sec)QueryOK,1rowaffected(0.00sec)QueryOK,1rowaffected(0.00sec)1062-Duplicateentry'D'forkey'PRIMARY'QueryOK,0rowsaffected(0.07sec)mysql>SELECT*FROMdeposit_table;+------+---------+|名称|存款|+------+-----+|A|2000||B|6000||C|7000||D|8000|+------+--------+4rowsinset(0.04sec)我们可以看到在执行INSERTINTOdeposit_tableVALUES('D',9000)时,由于前面的语句已经插入了D,所以此SQL语句的执行失败,并报告了1062-Duplicateentry'D'forkey'PRIMARY'错误,但是执行了COMMIT之后,还是提交了之前的修改,这显然不符合我们的预期。注意:如果使用Navicat的查询界面,将无法执行COMMIT语句,只能执行到报错的地方。推荐使用命令行来执行。所以在实践中,我们需要根据MySQL的错误返回值来判断是使用ROLLBACK还是COMMIT。就像这样:#创建一个存储过程CREATEDEFINER=`root`@`localhost`PROCEDURE`insert_test`()BEGIN#创建一个标识符,如果出错则设置为1DECLAREerr_flgINTEGER;DECLARECONTINUEHANDLERFORSQLEXCEPTIONSETerr_flg=1;STARTTRANSACTION;INSERTINTOdeposit_tableVALUES('C',7000);INSERTINTOdeposit_tableVALUES('D',8000);INSERTINTOdeposit_tableVALUES('D',9000);#发生错误,事务回滚IFerr_flg=1THENSELECT'SQLErrInvoked';#错误信息ROLLBACK;SELECT*FROMdeposit_table;#没有出现Error,直接提交ELSESELECT'TRANSACTIONSuccess';COMMIT;SELECT*FROMdeposit_table;ENDIF;END接下来我们调用存储过程:mysql>callinsert_test();+-------------------+|SQLErrInvoked|+----------------+|SQLErrInvoked|+----------------+1rowinset(0.04sec)+------+--------+|name|deposit|+------+--------+|A|2000||B|6000|+------+--------+2rowsinset(0.09sec)QueryOK,0rowsaffected(0.00sec)结果打印错误信息SQLErrInvoked,内容表的nt没有变化,说明我们ROLLBACK成功回滚了事务,符合我们的预期。如果使用其他语言调用MySQL接口,只需要获取错误标志并相应执行ROLLBACK或COMMIT即可。2、Consistency官网给出的解释是:数据库始终保持一致状态——在每次commit或rollback之后,事务进行时。如果相关数据正在跨多个表更新,查询看到的是Eitheralloldvaluesorallnewvalues,而不是新旧值的混合。翻译过来就是:在每次提交或回滚之后以及正在进行的事务处理过程中,数据库始终处于一致状态,如果跨多个表进行更新如果相关数据存在,则查询将看到要么所有旧值要么所有新值,而不是新旧价值观的混合。例如:#表a,b的定义跳过STARTTRANSACTION;UPDATEaSETname='a_new'WHEREname='a_old';UPDATEbSETname='b_new'WHEREname='b_old';犯罪;本例中的一致性,即如果此时,有查询SELECTa.name,b.nameFROMa,b;结果要么是a_oldb_old(表示事务已经回滚或者正在执行),要么是a_newb_new(表示事务已经成功提交),而不是a_oldb_new和a_newb_old两种情况。一些博客将一致性解释为“数据符合现实世界中的约束,比如唯一性约束等”。个人更倾向于官方文档的解读。这是一个见仁见智的问题。纠结这些概念意义不大。3.隔离(isolation)事务的隔离是指事务之间不能互相干扰,也不能看到对方未提交的数据。这种隔离是通过锁定机制实现的。我们在操作系统中也了解到,使用锁往往意味着并发性能的下降,因为可能会出现阻塞甚至死锁。当然,当用户确定事务不会互相干扰时,可以调整隔离级别,牺牲一些隔离来提高性能和并发。至于使用哪种隔离级别,需要自己权衡。因为隔离涉及的内容很多,我会在下一篇文章中详细讲解。4、持久性(persistence)事务的持久性是指一旦commit操作成功,事务所做的修改不会因为某些意外事件而丢失,比如断电、系统崩溃等潜在的威胁。MySQL提供了很多机制,比如日志技术,doublewritebuffer等等。MySQL的日志恢复技术我会单独写一篇文章,这里说一下doublewritebuffer技术。虽然这种技术叫做缓冲区,但实际上缓冲区并不位于内存中,而是位于磁盘上。这听起来可能很奇怪——既然数据是放到磁盘里的,为什么不直接写到数据文件中而不是多做一些呢?这是因为InnoDB的PageSize一般是16kb,它的数据校验也是针对page计算的,在flush数据到磁盘的过程中,如果出现掉电等故障,可能只有一部分page被写入(部分页面写入)。这种情况是redolog无法解决的,因为redolog记录的是页面的物理操作。如果页面本身损坏,重做是没有意义的。所以当这种情况发生时,我们需要一个副本来恢复页面。而且buffer是顺序写入的,开销比随机读写要小很多,所以doublewrite之后,性能并没有降低到原来的50%。2、事务中常用语句STARTTRANSACTION/BEGIN显式开启一个事务COMMIT提交事务,永久修改数据库SAVEPOINT在事务中创建一个保存点RELEASESAVAPOINT移除一个保存点ROLLBACK回滚事务并撤回所有未提交的更改。事务将被终止ROLLBACKTO[SAVEPOINT]回滚到给定的保存点,但事务不会终止。另外,保存点之后的行锁是不会被释放的。有关详细信息,请参阅SAVEPOINT、ROLLBACKTOSAVEPOINT和RELEASESAVEPOINT语句InnoDB不会释放在保存点之后存储在内存中的行锁。(对于新插入的行,锁信息由该行存储的事务ID携带,锁不单独存储在内存中,此时在undo中释放行锁。)SETTRANSACTION设置事务隔离levelSETautocommit0/1是否自动提交(默认autocommit)强调autocommit参数,默认情况下,如果不显式使用STARTTRANSACTION/BEGIN,MySQL会把每一句SQL视为一个独立的事务,例如:原表结构:mysql>SELECT*FROMdeposit_table;+------+--------+|name|deposit|+------+--------+|A|2000||B|6000|+------+--------+2rowsinset(0.04sec)新建存储过程(删除STARTTRANSACTION):CREATEDEFINER=`root`@`localhost`PROCEDURE`insert_test`()BEGIN#Routinebodygoeshere...DECLAREerr_flgINTEGER;DECLARECONTINUEHANDLERFORSQLEXCEPTIONSETerr_flg=1;#STARTTRANSACTION;INSERTINTOdeposit_tableVALUES('C',7000);INSERTINTOde8('C',7000);INSERTINTOde8;INSERTINTOdeposit_tableVALUES('D',9000);IFerr_flg=1THENSELECT'SQLErrInvoked';ROLLBACK;SELECT*FROMdeposit_table;ELSESELECT'TRANSACTIONSuccess';COMMIT;SELECT*FROMdeposit_table;ENDIF;END调用结果:mysql>callinsert_test();+----------------+|SQLErrInvoked|+----------------+|SQLErrInvoked|+-----------------+1rowinset(0.24sec)+------+--------+|name|deposit|+------+---------+|A|2000||B|6000||C|7000||D|8000|+-----+-----+4rowsinset(0.28sec)QueryOK,0rowsaffected(0.21sec)在这里,我们看到虽然确实执行了ROLLBACK,但是C和D还是被插入到了deposit_table中。这是因为没有显式事务,MySQL会执行隐式事务,每次修改自动提交,所以没有办法回滚。