• home > DB > mysql >

    再谈Transaction——MySQL事务处理分析

    Author:[email protected] Date:

    在商务级的应用中,都必须考虑事务处理的!事物的定义、事务都应该具备ACID特征、事物的隔离级别、事物解决的问题、MYSQL的事务处理主要方法、Mysql默认使用REPEATABLE READ理由、事务开启与结束的标志、transaction及其控制

    MySQL 事务基础概念/Definition of Transaction

    事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个 sql 语句,这些语句要么都执行,要么都不执行

    事务处理在各种管理系统中都有着广泛的应用,比如人员管理系统,很多同步数据库操作大都需要用到事务处理。比如说,在人员管理系统中,你删除一个人员,你即需要删除人员的基本资料,也要删除和该人员相关的信息,如信箱,文章等等,这样,这些数据库操作语句就构成一个事务!

    删除的SQL语句
    delete from userinfo where ~~~
    delete from mail where ~~
    delete from article where~~
    ~~ 
    如果没有事务处理,在你删除的过程中,假设出错了,只执行了第一句,那么其后果是难以想象的!

    但用事务处理。如果删除出错,你只要rollback就可以取消删除操作(其实是只要你没有commit你就没有确实的执行该删除操作)

    一般来说,在商务级的应用中,都必须考虑事务处理的

    mysql逻辑架构和存储引擎

    MySQL 支持事务的存储引擎有 InnoDB、NDB Cluster 等,其中 InnoDB 的使用最为广泛;其他存储引擎不支持事务,如 MyIsam、Memory 等

    事务的特征

    • 一个最小的不可再分的工作单元;通常一个事务对应一个完整的业务(例如银行账户转账业务,该业务就是一个最小的工作单元)

    • 一个完整的业务需要批量的DML(insert、update、delete)语句共同联合完成

    • 事务只和DML语句有关,或者说DML语句才有事务。这个和业务逻辑有关,业务逻辑不同,DML语句的个数不同

    先来明确一下事务涉及的相关知识:

    事务都应该具备ACID特征

    所谓ACID是Atomic(原子性)/Consistent(一致性)/Isolated(隔离性)/Durable(持续性)四个词的首字母所写。

    一般来说,事务是必须满足4个条件(ACID)
    • 原子性(Autmic):事务是最小单位,不可再分。即:组成事务处理的语句形成了一个逻辑单元,不能只执行其中的一部分。事务在执行性,要做到"要么不做,要么全做!",就是说不允许事务部分得执行。即使因为故障而使事务不能完成,在rollback时也要消除对数据库得影响!

      比如:银行转帐过程中,必须同时从一个帐户减去转帐金额,并加到另一个帐户中,只改变一个帐户是不合理的。

      原子性由 Undo log 保证。Undo Log 会保存每次变更之前的记录,从而在发生错误时进行回滚

    • 一致性(Consistency)在事务处理执行前后,数据库是一致的。事务要求所有的DML语句操作的时候,必须保证同时成功或者同时失败。事务得操作应该使使数据库从一个一致状态转变倒另一个一致得状态!

      比如:网上购物,你只有即让商品出库,又让商品进入顾客得购物篮才能构成事务!

      比如:银行转帐过程中,要么转帐金额从一个帐户转入另一个帐户,要么两个帐户都不变,没有其他的情况。

      一致性由原子性,隔离性,持久性来保证

    • 隔离性(Isolation)一个事务处理对另一个事务处理没有影响。即:事务A和事务B之间具有隔离性。如果多个事务并发执行,应象各个事务独立执行一样!也就是说任何事务都不可能看到一个处在不完整状态下的事务。

      隔离性追求的是并发情形下事务之间互不干扰。

      比如:银行转帐过程中,在转帐事务没有提交之前,另一个转帐事务只能处于等待状态。

      隔离性由 MVCC(Multi-Version Concurrency Control) 和 Lock(锁机制) 保证

    • 持久性(Durability)事务处理的效果能够被永久保存下来。反过来说,事务应当能够承受所有的失败,包括服务器、进程、通信以及媒体失败等等。一个成功执行得事务对数据库得作用是持久得,即使数据库应故障出错,也应该能够恢复!持久性是事务的保证,事务终结的标志(内存的数据持久到硬盘文件中)。

      比如:银行转帐过程中,转帐后帐户的状态要能被保存下来。

      持久性由 Redo Log 保证。每次真正修改数据之前,都会将记录写到 Redo Log 中,只有 Redo Log 写入成功,才会真正的写入到 B+ 树中,如果提交之前断电,就可以通过 Redo Log 恢复记录

    InnoDB存储引擎两种事务日志:

    • redo log(重做日志): 用于保证事务持久性

    • undo log(回滚日志):事务原子性和隔离性实现的基础

    undo log是现原子性的关键,是当事务回滚时能够撤销所有已经成功执行的 sql 语句。

    InnoDB 实现回滚,靠的是 undo log:

    • 当事务对数据库进行修改时,InnoDB 会生成对应的 undo log。

    • 如果事务执行失败或调用了 rollback,导致事务需要回滚,便可以利用 undo log 中的信息将数据回滚到修改之前的样子。

    undo log 属于逻辑日志,它记录的是 sql 执行相关的信息。

    当发生回滚时,InnoDB 会根据 undo log 的内容做与之前相反的工作:

    • 对于每个 insert,回滚时会执行 delete。

    • 对于每个 delete,回滚时会执行 insert。

    • 对于每个 update,回滚时会执行一个相反的 update,把数据改回去。

    以 update 操作为例:当事务执行 update 时,其生成的 undo log 中会包含被修改行的主键(以便知道修改了哪些行)、修改了哪些列、这些列在修改前后的值等信息,回滚时便可以使用这些信息将数据还原到 update 之前的状态。

    redo log 存在的背景

    InnoDB 作为 MySQL 的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘 IO,效率会很低。

    为此,InnoDB 提供了缓存(Buffer Pool),Buffer Pool 中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:

    • 当从数据库读取数据时,会首先从 Buffer Pool 中读取,如果 Buffer Pool 中没有,则从磁盘读取后放入 Buffer Pool。

    • 当向数据库写入数据时,会首先写入 Buffer Pool,Buffer Pool 中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。

    Buffer Pool 的使用大大提高了读写数据的效率,但是也带来了新的问题:如果 MySQL 宕机,而此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。

    于是,redo log 被引入来解决这个问题:当数据修改时,除了修改 Buffer Pool 中的数据,还会在 redo log 记录这次操作;当事务提交时,会调用 fsync 接口对 redo log 进行刷盘。

    如果 MySQL 宕机,重启时可以读取 redo log 中的数据,对数据库进行恢复。

    redo log 采用的是 WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到 Buffer Pool,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求

    既然 redo log 也需要在事务提交时将日志写入磁盘,为什么它比直接将 Buffer Pool 中修改的数据写入磁盘(即刷脏)要快呢?

    主要有以下两方面的原因:

    • 刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是追加操作,属于顺序 IO。

    • 刷脏是以数据页(Page)为单位的,MySQL 默认页大小是 16KB,一个 Page 上一个小修改都要整页写入;而 redo log 中只包含真正需要写入的部分,无效 IO 大大减少。

    redo log 与 binlog

    我们知道,在 MySQL 中还存在 binlog(二进制日志)也可以记录写操作并用于数据的恢复,但二者是有着根本的不同的。

    作用不同:

    • redo log 是用于 crash recovery 的,保证 MySQL 宕机也不会影响持久性;

    • binlog 是用于 point-in-time recovery 的,保证服务器可以基于时间点恢复数据,此外 binlog 还用于主从复制。

    层次不同:

    • redo log 是 InnoDB 存储引擎实现的,

    • 而 binlog 是 MySQL 的服务器层实现的,同时支持 InnoDB 和其他存储引擎。

    内容不同:

    • redo log 是物理日志,内容基于磁盘的 Page。

    • binlog 是逻辑日志,内容是一条条 sql。

    写入时机不同:

    • redo log 的写入时机相对多元。前面曾提到,当事务提交时会调用 fsync 对 redo log 进行刷盘;这是默认情况下的策略,修改 innodb_flush_log_at_trx_commit 参数可以改变该策略,但事务的持久性将无法保证。

    除了事务提交时,还有其他刷盘时机:如 master thread 每秒刷盘一次 redo log 等,这样的好处是不一定要等到 commit 时刷盘,commit 速度大大加快。

    • binlog 在事务提交时写入。

    事务的隔离四种级别

    ACID四大特征中,最难理解的不是一致性,而是事务的隔离性,数据库权威专家针对事务的隔离性研究出来了事务的隔离四种级别,四种事务隔离级别就是为了解决数据在高并发下产生的问题(脏读、不可重复读、幻读)

    并发一致性问题

    • 脏读:比如有两个事务并行执行操作同一条数据库记录,A事务能读取到B事务未提交的数据。比如:事务B操作了数据库但是没有提交事务,此时A读取到了B没有提交事务的数据。这就是脏读的体现。

    • 不可重复读:比如事务A在同一事务中多次读取同一记录,此时事务B修改了事务A正在读的数据并且提交了事务,但是事务A读取到了事务B所提交的数据,导致两次读取数据不一致。

    • 幻读:事务A将数据表中所有数据都设置为100,此时事务B插入了一条值为200的数据并提交事务,当事务A修改完成提交事务后,发现还有一条数据的值不为100.这就是幻读的一种体现方式。

    脏读示例不可重复读示例幻读示例

    脏读情况不可重复读(Non-Repeatable Reads)幻读流传展示

    "脏读"、"不可重复读"和"幻读",其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决

    事务隔离级别

    • 未提交读(RU),一个事务还没提交时,它做的变更就能被别的事务看到。

      发生脏读的原因:RU 原理是对每个更新语句的行记录进行加锁,而不是对整个事务进行加锁,所以会发生脏读。而 RC 和 RR 会对整个事务加锁。

    • 已提交读(RC),一个事务提交之后,它做的变更才会被其他事务看到。

      不能重复读的原因:RC 每次执行 SQL 语句都会生成一个新的 Read View,每次读到的都是不同的。而 RR 的事务从始至终都是使用同一个 Read View。

    • 可重复读(RR), 一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。

      默认是 MVCC 机制(“一致性非锁定读”)保证 RR 级别的隔离正确性,是不上锁的。可以选择手动上锁:select xxxx for update (排他锁); select xxxx lock in share mode(共享锁),称之为“一致性锁定读”。使用锁之后,就能在 RR 级别下,避免幻读。当然,默认的 MVCC 读,也能避免幻读。

    • 串行化(serializable),所有事务一个接着一个的执行,这样可以避免幻读 (phantom read),对于基于锁来实现并发控制的数据库来说,串行化要求在执行范围查询的时候,需要获取范围锁,如果不是基于锁实现并发控制的数据库,则检查到有违反串行操作的事务时,需回滚该事务。

      顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

    数据库权威专家针对事务的隔离性研究出来了事务的隔离四种级别

    事务隔离级别越严格,越消耗计算机性能,效率也越低

    数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使事务在一定程度上 “串行化”进行,这显然与“并发”是矛盾的。同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对“不可重复读”和“幻读”并不敏感,可能更关心数据并发访问的能力。

    Mysql默认使用的数据隔离级别是REPEATABLE READ(可重复读,允许幻读)。MySql 的 REPEATABLE READ 级别不会导致幻读。

    REPEATABLE READ级别会导致幻读,但是,由于 Mysql 的优化,在使用默认的 select 时,MySql 使用 MVCC 机制保证不会幻读

    也可以使用锁,在使用锁时,例如 for update(X 锁),lock in share mode(S 锁),MySql 会使用 Next-Key Lock 来保证不会发生幻读。

    前者称为快照读,后者称为当前读。

    Mysql默认使用REPEATABLE READ理由

    Mysql在5.0这个版本以前,binlog只支持STATEMENT这种格式!而这种格式在读已提交(Read Commited)这个隔离级别下主从复制是有bug的,因此Mysql将可重复读(Repeatable Read)作为默认的隔离级别!

    有什么bug呢?具体阅读《互联网项目中mysql应该选什么事务隔离级别

    mysql为什么不采默认用串行化(Serializable)?

    因为每个次读操作都会加锁,快照读失效,一般是使用mysql自带分布式事务功能时才使用该隔离级别

    主从复制,是基于什么复制的? 

    是基于binlog复制的!

    binlog有几种格式?

    1. statement:记录的是修改SQL语句

    2. row:记录的是每行实际数据的变更

    3. mixed:statement和row模式的混合


    事务开启的标志?事务结束的标志?

    开启标志:任何一条DML语句(insert、update、delete)执行,标志事务的开启

    结束标志(提交或者回滚)

                提交:成功的结束,将所有的DML语句操作历史记录和底层硬盘数据来一次同步

                回滚:失败的结束,将所有的DML语句操作历史记录全部清空

    事物与数据库底层数据:

    在事物进行过程中,未结束之前,DML语句是不会更改底层数据,只是将历史操作记录一下,在内存中完成记录。只有在事物结束的时候,而且是成功的结束的时候,才会修改底层硬盘文件中的数据

    什么是transaction及其控制?

    Transaction包含了一系列的任务操作。这些操作可能是创建更新删除等等操作,是作为一个特定的结果来表现的。要么成功,要么不成功

    transaction有4种控制

    1. commit,也就是提交

    2. rollback,也就是回调

    3. set transaction,对这个事务设定一个名字

    4. save point。这个控制是用来设定某个点用来回退的

    MVCC解析

    MVCC 全称 Multi-Version Concurrency Control,即多版本的并发控制协议。

    一致性非锁定读

    一致性非锁定读(consistent nonlocking read)是指InnoDB存储引擎通过多版本控制(MVCC)的方式来读取当前执行时间数据库中行的数据。如果读取的这行正在执行DELETE或UPDATE操作,这时读取操作不会向XS锁一样去等待锁释放,而是会去读一个快照数据。

    MVCC相关的知识

    在事务隔离级别RC和RR下,InnoDB存储引擎使用非锁定的一致性读。然而对于快照数据的定义却不同,在RC级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据。而在RR级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。

    MVCC过程图示

    可以看到,第1步和第2步是非常容易理解的,而在第3步事务B插入一条新的数据后,在第4步事务A还是查不到,也就是利用了MVCC的特性来实现。当事务B提交后,第5步的查询在RC和RR隔离级别下的输出是不同的,这个的原因在另一篇博客中也说到了,是因为他们创建ReadView的时机不同。

    但是很诡异的是在第6步的时候,事务A更新了一条它看不见的记录,然后查询就能够查询出来了。这里很多人容易迷惑,不可见不代表记录不存在,它只是利用了可见性判断忽略了而已。更新成功之后,事务A顺其自然的记录了这条记录的Undo log,在随后的查询中,因为它能够看见自己的改动这一个可见性的判断,自然就能够查询出来了。这里很多名词需要去深入读一下此文:谈谈MySQL InnoDB存储引擎事务的ACID特性

    RR 解决脏读、不可重复读、幻读等问题,使用的是 MVCC

    高性能MySQL MVCC

    MVCC是个 行级锁 的变种,它在 普通读情况下避免了加锁操作,因此开销更低 。虽然实现不同,但通常都是实现非阻塞读,写操作只锁定必要的行。

    MVCC 的特点

    在同一时刻,不同的事务读取到的数据可能是不同的(即多版本)

    MVCC 最大的优点是读不加锁,因此读写不冲突,并发性能好。InnoDB 实现 MVCC,多个版本的数据可以共存,主要是依靠数据的隐藏列(也可以称之为标记位)和 undo log。

    其中数据的隐藏列包括了该行数据的版本号、删除时间、指向 undo log 的指针等等。

    当读取数据时,MySQL 可以通过隐藏列判断是否需要回滚并找到回滚需要的 undo log,从而实现 MVCC;隐藏列的详细格式不再展开。

    一致性锁定读

    前面说到,在默认隔离级别RR下,InnoDB存储引擎的SELECT操作使用一致性非锁定读。但是在某些情况下,用户需要显式地对数据库读取操作进行加锁以保证数据逻辑的一致性。InnoDB存储引擎对于SELECT语句支持两种一致性的锁定读(locking read)操作。

    • SELECT … FOR UPDATE (X锁)

    • SELECT … LOCK IN SHARE MODE (S锁)

    MySQL InnoDB 脏读 不可重复读 幻读 

    脏读

    脏读情景分析

    当事务 A 在 T3 时间节点读取 zhangsan 的余额时,会发现数据已被其他事务修改,且状态为未提交。

    此时事务 A 读取最新数据后,根据数据的 undo log 执行回滚操作,得到事务 B 修改前的数据,从而避免了脏读。

    不可重复读

    不可重复读情景分析

    当事务 A 在 T2 节点第一次读取数据时,会记录该数据的版本号(数据的版本号是以 row 为单位记录的),假设版本号为 1;当事务 B 提交时,该行记录的版本号增加,假设版本号为 2。

    当事务 A 在 T5 再一次读取数据时,发现数据的版本号(2)大于第一次读取时记录的版本号(1),因此会根据 undo log 执行回滚操作,得到版本号为 1 时的数据,从而实现了可重复读。

    幻读

    InnoDB 实现的 RR 通过 next-keylock 机制避免了幻读现象。

    next-keylock 是行锁的一种,实现相当于 record lock(记录锁) + gap lock(间隙锁);其特点是不仅会锁住记录本身(record lock 的功能),还会锁定一个范围(gap lock 的功能)。

    当然,这里我们讨论的是不加锁读:此时的 next-key lock 并不是真的加锁,只是为读取的数据增加了标记(标记内容包括数据的版本号等);准确起见姑且称之为类 next-key lock 机制。

    幻读情景分析

    这样当 T5 时刻再次读取 0

    小结:概括来说,InnoDB 实现的 RR,通过锁机制、数据的隐藏列、undo log 和类 next-key lock,实现了一定程度的隔离性,可以满足大多数场景的需要。

    不过需要说明的是,RR 虽然避免了幻读问题,但是毕竟不是 Serializable,不能保证完全的隔离


    MYSQL的事务处理主要方法

    用begin,rollback,commit来实现

    • begin 开始一个事务
    • rollback 事务回滚
    • commit  事务确认

    改变mysql的自动提交模式

    MYSQL默认事务是自动提交的,也就是你提交一个QUERY,它就直接执行!也就是说,只要执行一条DML语句就开启了事物,并且提交了事务。

    MySQL自动提交模式

    我们可以通过设置autocommit来实现事务的处理。

    • set autocommit=0 禁止自动提交

    • set autocommit=1 开启自动提交

    但注意当你用 set autocommit=0 的时候,你以后所有的SQL都将做为事务处理,直到你用commit确认或rollback结束,注意当你结束这个事务的同时也开启了个新的事务!按第一种方法只将当前的作为一个事务!
    个人推荐使用第一种方法!
    MYSQL中只有INNODB和BDB类型的数据表才能支持事务处理!其他的类型是不支持的!(切记!)

    再来看看哪些问题会用到事务处理:

    先假设一下问题的背景:网上购书,某书(数据库编号为123)只剩最后一本,而这个时候,两个用户对这本书几乎同时发出了购买请求,让我们看看整个过程:

    在具体分析之前,先来看看数据表的定义:

    -------------------------------------------------------------------------------

    create table book
    (
        book_id unsigned int(10) not null auto_increment,
        book_name varchar(100) not null,
        book_price float(5, 2) not null, #我假设每本书的价格不会超过999.99元
        book_number int(10) not null,
        primary key (book_id)
    )
    type = innodb; #engine = innodb也行

    -------------------------------------------------------------------------------

    对于用户甲来说,他的动作稍微比乙快一点点,其购买过程所触发的动作大致是这样的:

    -------------------------------------------------------------------------------

    1. SELECT book_number FROM book WHERE  book_id = 123;

    book_number大于零,确认购买行为并更新book_number

    2. UPDATE book SET book_number = book_number - 1 WHERE  book_id = 123;

    购书成功

    -------------------------------------------------------------------------------

    而对于用户乙来说,他的动作稍微比甲慢一点点,其购买过程所触发的动作和甲相同:

    -------------------------------------------------------------------------------

    1. SELECT book_number FROM book WHERE  book_id = 123;

    这个时候,甲刚刚进行完第一步的操作,还没来得及做第二步操作,所以book_number一定大于零

    2. UPDATE book SET book_number = book_number - 1 WHERE  book_id = 123;

    购书成功

    -------------------------------------------------------------------------------

    表面上看甲乙的操作都成功了,他们都买到了书,但是库存只有一本,他们怎么可能都成功呢?再看看数据表里book_number的内容,已经变成 -1了,这当然是不能允许的(实际上,声明这样的列类型应该加上unsigned的属性,以保证其不能为负,这里是为了说明问题所以没有这样设置)

    好了,问题陈述清楚了,再来看看怎么利用事务来解决这个问题,打开MySQL手册,可以看到想用事务来保护你的SQL正确执行其实很简单,基本就是三个语句:开始,提交,回滚。

    • 开始:START TRANSACTION或BEGIN语句可以开始一项新的事务

    • 提交:COMMIT可以提交当前事务,是变更成为永久变更

    • 回滚:ROLLBACK可以回滚当前事务,取消其变更

    此外,SET AUTOCOMMIT = {0 | 1}可以禁用或启用默认的autocommit模式,用于当前连接。

    -------------------------------------------------------------------------------

    那是不是只要用事务语句包一下我们的SQL语句就能保证正确了呢?比如下面代码:

    -------------------------------------------------------------------------------

    BEGIN;

    SELECT book_number FROM book WHERE  book_id = 123;

    // ...

    UPDATE book SET book_number = book_number - 1 WHERE  book_id = 123;

    COMMIT;

    -------------------------------------------------------------------------------

    答案是否定了,这样依然不能避免问题的发生,如果想避免这样的情况,实际应该如下:

    -------------------------------------------------------------------------------

    BEGIN;

    SELECT book_number FROM book WHERE  book_id = 123 FOR UPDATE ;

    // ...

    UPDATE book SET book_number = book_number - 1 WHERE  book_id = 123;

    COMMIT;

    -------------------------------------------------------------------------------

    由于加入了FOR UPDATE,所以会在此条记录上加上一个行锁,如果此事务没有完全结束,那么其他的事务在使用SELECT ... FOR UPDATE请求的时候就会处于等待状态,直到上一个事务结束,它才能继续,从而避免了问题的发生,需要注意的是,如果你其他的事务使用的是不带FOR UPDATE的SELECT语句,将得不到这种保护。

    MySQL事务操作

    mysql> use RUNOOB;
    Database changed
    mysql> CREATE TABLE runoob_transaction_test( id int(5)) engine=innodb;  # 创建数据表
    Query OK, 0 rows affected (0.04 sec)
    
    mysql> select * from runoob_transaction_test;
    Empty set (0.01 sec)
     
    mysql> begin;  # 开始事务
    Query OK, 0 rows affected (0.00 sec)
     
    mysql> insert into runoob_transaction_test value(5);
    Query OK, 1 rows affected (0.01 sec)
     
    mysql> insert into runoob_transaction_test value(6);
    Query OK, 1 rows affected (0.00 sec)
     
    mysql> commit; # 提交事务
    Query OK, 0 rows affected (0.01 sec)
     
    mysql>  select * from runoob_transaction_test;
    +------+
    | id   |
    +------+
    | 5    |
    | 6    |
    +------+
    2 rows in set (0.01 sec)
     
    mysql> begin;    # 开始事务
    Query OK, 0 rows affected (0.00 sec)
     
    mysql>  insert into runoob_transaction_test values(7);
    Query OK, 1 rows affected (0.00 sec)
     
    mysql> rollback;   # 回滚
    Query OK, 0 rows affected (0.00 sec)
     
    mysql>   select * from runoob_transaction_test;   # 因为回滚所以数据没有插入
    +------+
    | id   |
    +------+
    | 5    |
    | 6    |
    +------+
    2 rows in set (0.01 sec)

    PHP + MySQL事务操作的代码演示:

    <?php
    $dbhost = 'localhost:3306';  // mysql服务器主机地址
    $dbuser = 'root';            // mysql用户名
    $dbpass = '123456';          // mysql用户名密码
    $conn = mysqli_connect($dbhost, $dbuser, $dbpass);
    if(! $conn )
    {
        die('连接失败: ' . mysqli_error($conn));
    }
    // 设置编码,防止中文乱码
    mysqli_query($conn, "set names utf8");
    mysqli_select_db( $conn, 'RUNOOB' );
    mysqli_query($conn, "SET AUTOCOMMIT=0"); // 设置为不自动提交,因为MYSQL默认立即执行
    mysqli_begin_transaction($conn);            // 开始事务定义
    
    if(!mysqli_query($conn, "insert into runoob_transaction_test (id) values(8)"))
    {
        mysqli_query($conn, "ROLLBACK");     // 判断当执行失败时回滚
    }
    
    if(!mysqli_query($conn, "insert into runoob_transaction_test (id) values(9)"))
    {
        mysqli_query($conn, "ROLLBACK");      // 判断执行失败时回滚
    }
    mysqli_commit($conn);            //执行事务
    mysqli_close($conn);
    ?>


    PHP使用AdoDB操作MySQL事务的代码演示:

    实际LAMP应用中,一般PHP使用AdoDB操作MySQL,下面给出AdoDB相应的代码方便大家查阅:

    -------------------------------------------------------------------------------

    // ...
    $adodb -> startTrans
    ();

    //实际,getOne所调用的查询也可以直接放到rowLock来进行,这里只是为了演示效果能更明显些。
    $adodb -> rowLock ( 'book' 'book_id = 123'
    );
    $bookNumber  $adodb -> getOne ( "SELECT book_number FROM book WHERE  book_id = 123"
    );
    $adodb -> execute ( "UPDATE book SET book_number = book_number - 1 WHERE  book_id = 123"
    );
    $adodb -> completeTrans
    ();
    // ...
    ?>

    -------------------------------------------------------------------------------

    其中,rowLock的方法就是调用的FOR UPDATE来实现的行锁,你可能会想把&"FOR UPDATE&直接写到$adodb->getOne()调用的那条SQL语句里面去实现行锁的功能,不错,那样确实可以,但是并不是所有的数据库 都使用&"FOR UPDATE&"语法来实现行锁功能,比如Sybase使用&"HOLDLOCK&"的语法来实现行锁功能,所以为了你的数据库抽象层保持可移植性,我还是劝你 用rowLock来实现行锁功能,至于可移植性就交给AdoDB好了,嗯,有点扯远了,今儿就说到这里了。

    -------------------------------------------------------------------------------

    附:

    AdoDB中存在一个setTransactionMode()方法,能够设置事务的隔离级别,如下:

    SetTransactionMode allows you to pass in the transaction mode to use for all subsequent transactions for that connection session. Note: if you have persistent connections and using mysql or mssql, you might have to explicitly reset your transaction mode at the beginning of each page request. This is only supported in postgresql, mssql, mysql with InnoDB and oci8 currently. For example:

    $db->SetTransactionMode("SERIALIZABLE");
    $db->BeginTrans();
    $db->Execute(...); $db->Execute(...);
    $db->CommiTrans();

    $db->SetTransactionMode(""); // restore to default
    $db->StartTrans();
    $db->Execute(...); $db->Execute(...);
    $db->CompleteTrans();

    Supported values to pass in:

        * READ UNCOMMITTED (allows dirty reads, but fastest)
        * READ COMMITTED (default postgres, mssql and oci8)
        * REPEATABLE READ (default mysql)
        * SERIALIZABLE (slowest and most restrictive)

    You can also pass in database specific values such as 'SNAPSHOT' for mssql or 'READ ONLY' for oci8/postgres.


    参考链接:

    MySQL——事务(Transaction)详解 https://blog.csdn.net/w_linux/article/details/79666086

    MySql 三大知识点——索引、锁、事务 https://zhuanlan.zhihu.com/p/59764376

    面试官问你:MYSQL事务和隔离级别,该如何回答 https://zhuanlan.zhihu.com/p/70701037

    谈谈MySQL的锁 https://zhuanlan.zhihu.com/p/65721606

    一文说尽MySQL事务及ACID特性的实现原理 https://zhuanlan.zhihu.com/p/56874694

    MySQL 事务 https://www.runoob.com/mysql/mysql-transaction.html


    转载本站文章《再谈Transaction——MySQL事务处理分析》,
    请注明出处:https://www.zhoulujun.cn/html/DB/mysql/2015_1022_324.html