# 事务处理

事务是保证多个SQL操作的一致性,如果一条失败全部SQL也将失效。

学习事务时建议开启两个客户端(或GUI软件)来体验

# 业务分析

  • 事务是保证多个SQL操作的一致性,如果一条失败全部SQL也将失效。

  • 实际业务中大多数是对多个表操作,比如当发表文章时需要将文章的基本信息发到文章基础表和文章内容添加到文章内容表,这种情况不使用事务也没有关系,如果出现数据异常重新添加就可以了。

  • 但牵涉到货币的情况就必须使用事务了,必须保证货币处理是准确的。

  • 当然有些公司要求所有查询都使用事务,这就遵照公司要求完成就可以了。

# 储存引擎

查看引擎

SHOW ENGINES;

最新版本的Mysql中已经不建议使用 MyISAM引擎了,所以我们也不用讨论他了。

# InnoDB

支持事务的引擎建议使用 InnoDB。如果旧表是其他引擎,使用下面语句更改为InnoDB引擎。

ALTER TABLE stu ENGINE=InnoDB;

# 提交模式

# 自动提交

Mysql的提交默认是自动提交,即发送一条执行一条。

DBeaver 执行以下SQL后,在别一个 Sequel Pro 会立刻看到结果。

INSERT INTO stu (class_id,sname,sex)VALUES(2,'张帝','女');

image-20190722173405511

Sequel Pro 里即可看到结果,这为自动提交。

image-20190722173522545

# 事务提交

# 事务单独开启

执行 START TRANSACTIONBEGIN 语句后,表示要开启一项事务处理。

  • COMMIT 提交事务
  • ROLLBACK 回滚事务
START TRANSACTION;
INSERT INTO stu (class_id,sname,sex)VALUES(2,'张帝','女');
COMMIT;

# 全局开启事务

如果所有SQL都使用事务操作,我们可以通过 SET AUTOCOMMIT=0 关闭自动提交来开启事务机制,这样所有语句都是事务类型。

-- 关闭自动提交
SET AUTOCOMMIT = 0;

INSERT INTO stu (class_id,sname,sex)VALUES(2,'李清','女');
COMMIT;

-- 开启自动提交
SET AUTOCOMMIT = 1;

# 程序控制

程序语言基本上都支持事务的处理。如果对以下代码不清楚,请在后盾人 查看PHP的PDO章节。

<?php
header("Content-type:text/html;charset=utf8");
try {
    $config = [
        'host' => '127.0.0.1',
        'user' => 'root',
        'password' => 'root',
        'database' => 'test',
        'charset' => 'utf8'
    ];
    $dsn = sprintf(
        "mysql:host=%s;dbname=%s;charset=%s",
        $config['host'],
        $config['database'],
        $config['charset']
    );
    $pdo = new PDO($dsn, $config['user'], $config['password']);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    die($e->getMessage());
}
try {
    # 开启事务
    $pdo->beginTransaction();
    $d = $pdo->exec("INSERT INTO stu (class_id,sname,sex)
     VALUES(2,'后盾人','男')");

    $pdo->exec("UPDATE class SET 
     stu_count = (SELECT count(*) FROM stu WHERE class_id=2) WHERE id=2");
    # 提交事务
    $pdo->commit();
} catch (PDOException $e) {
    # 事务回滚
    $pdo->rollBack();
    die("Exception:" . $e->getMessage());
}

# 事务隔离

# 并发问题

当高并发访问会遇到多个事务的隔离问题,可能会出现以下:

  1. 脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据

  2. 不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。

  3. 幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

# 隔离级别

事务隔离级别 脏读 不可重复读 幻读 说明
读未提交(read-uncommitted) 最低的事务隔离级别,一个事务还没提交时,它做的变更就能被别的事务看到
不可重复读(read-committed) 保证一个事物提交后才能被另外一个事务读取。另外一个事务不能读取该事物未提交的数据。
可重复读(repeatable-read) 多次读取同一范围的数据会返回第一次查询的快照,即使其他事务对该数据做了更新修改。事务在执行期间看到的数据前后必须是一致的。
串行化(serializable) 事务 100% 隔离,可避免脏读、不可重复读、幻读的发生。花费最高代价但最可靠的事务隔离级别。

# 查询与设置

查询隔离级别

select @@tx_isolation;

设置隔离级别

set session transaction isolation level read uncommitted;

# 脏读

为了演示效果将隔离级别设置为最低级 read uncommitted。脏读是一个事务没有提交时可被其他事务读取到。

  1. 事务A执行更新操作

    set session transaction isolation level read UNCOMMITTED;
    BEGIN;
    UPDATE stu SET sname = '后盾人' WHERE id=1;
    
  2. 因为使用了最低级别read uncommitted,事务B在事务A没有提交时就可以看到更新的数据,如果事务A执行ROLLBACK 事务B的读到的数据就为脏数据。

    SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
    BEGIN;
    SELECT * FROM `stu`;
    
  3. 将隔离级别设置为除 read uncommitted以外的,再重复上面的例子,都可以有效避免脏读的问题。

# 不可重复读

为了演示效果将隔离级别设置为最低级 read committed

事务A在执行过程中更新数据,事务B同时读取的数据没有脏数据。但当事务A提交了事务后,事务B再读取时得到了最新的数据,这种情况为不可重复读, 要保证事务过程中的数据是一致的,不受其他事务影响。

  1. 事务A执行以下代码,但没有提交
	set session transaction isolation level REPEATABLE READ;
	BEGIN;
	UPDATE stu SET sname = '后盾人9' WHERE id=1;
  1. 因为使用了 read committed级别,所以事务B不会读到脏数据

    SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
    BEGIN;
    SELECT * FROM stu;
    
  2. 事务A提交事务

    ...
    commit
    ...
    
  3. 此时事务B可以读取到事务A提交的数据,这就是不能重复读取到同一个数据,即事务B读取结果受事务A影响。

  4. 将隔离机制设置为 repeatable-read 就可以解决这类不可重复读的问题。

# 幻读

幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体。

  1. 事务A执行查询,假如查询结果是6条

    set session transaction isolation level REPEATABLE READ;
    BEGIN;
    SELECT * FROM stu;
    
  2. 事务B执行添加

    set session transaction isolation level REPEATABLE READ;
    insert into stu (class_id,sname,sex) values(2,'后盾人',1);
    commit;
    
  3. 事务A执行更新,发现更新了7条(刚才查询时6条,但更新了七条,感觉像出现了幻觉)

    ...
    UPDATE stu SET balance = 200;
    ...
    
  4. 切换隔离级别为 serializable 后,在事务A没有提交时,事务B是不能插入数据的(表现形式为等待)。