MySQL之事务


 为什么要有事务?

        事务广泛的运用于订单系统、银行系统等多种场景。如果有以下一个场景:A用户和B用户是银行的储户。现在A要给B转账500元。那么需要做以下几件事:

            1. 检查A的账户余额>500元;

            2. A账户扣除500元;

            3. B账户增加500元;

        正常的流程走下来,A账户扣了500,B账户加了500,皆大欢喜。那如果A账户扣了钱之后,系统出故障了呢?A白白损失了500,而B也没有收到本该属于他的500。以上的案例中,隐藏着一个前提条件:A扣钱和B加钱,要么同时成功,要么同时失败。事务的需求就在于此。

  解决方案:当 A 账户的余额减少之后,不要立即修改数据表,而是在确认 B 账户的余额增加之后,同时修改数据表。

  事务安全,是一种保护连续操作同时实现(完成)的机制。事务安全的意义就是,保证数据操作的完整性。

一、事务的简介:

  1、事务的定义

  事务(Transaction):一个最小的不可再分的工作单元;

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

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

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

  2、存储引擎(为什么要提MySQL的存储引擎,因为不是所有的数据库存储引擎都支持事务)。

  在实际工作中,选择一个合适的存储引擎是一个比较复杂的问题。每种存储引擎都有自己的优缺点,不能笼统地说谁比谁好。

   

  InnoDB:支持事务处理,支持外键,支持崩溃修复能力和并发控制。如果需要对事务的完整性要求比较高(比如银行),要求实现并发控制(比如售票),那选择InnoDB有很大的优势。如果需要频繁的更新、删除操作的数据库,也可以选择InnoDB,因为支持事务的提交(commit)和回滚(rollback)。 

  MyISAM:插入数据快,空间和内存使用比较低。如果表主要是用于插入新记录和读出记录,那么选择MyISAM能实现处理高效率。如果应用的完整性、并发性要求比 较低,也可以使用。

  MEMORY:所有的数据都在内存中,数据的处理速度快,但是安全性不高。如果需要很快的读写速度,对数据的安全性要求较低,可以选择MEMOEY。它对表的大小有要求,不能建立太大的表。所以,这类数据库只使用在相对较小的数据库表。

  注意:同一个数据库也可以使用多种存储引擎的表。如果一个表要求比较高的事务处理,可以选择InnoDB。这个数据库中可以将查询要求比较高的表选择MyISAM存储。如果该数据库需要一个用于查询的临时表,可以选择MEMORY存储引擎。

  3、事务的四大特性

  •   A(atomicity) 原子性。一个事务的执行被视为一个不可分割的最小单元。事务里面的操作,要么全部成功执行,要么全部失败回滚,不可以只执行其中的一部分。
  •        C(consistency) 一致性。一个事务的执行不应该破坏数据库的完整性约束。如果上述例子中第2个操作执行后系统崩溃,保证A和B的金钱总计是不会变的。
  •        I(isolation) 隔离性。通常来说,事务之间的行为不应该互相影响。然而实际情况中,事务相互影响的程度受到隔离级别的影响。文章后面会详述。
  •        D(durability) 持久性。事务提交之后,需要将提交的事务持久化到磁盘。即使系统崩溃,提交的数据也不应该丢失。

  4、事务的并发访问问题

1)脏读:脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
例如:
  张三的工资为5000,事务A中把他的工资改为8000,但事务A尚未提交。
  与此同时,
  事务B正在读取张三的工资,读取到张三的工资为8000。
  随后,
  事务A发生异常,而回滚了事务。张三的工资又回滚为5000。
  最后,
  事务B读取到的张三工资为8000的数据即为脏数据,事务B做了一次脏读。

2)不可重复读:是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
例如:
  在事务A中,读取到张三的工资为5000,操作没有完成,事务还没提交。
  与此同时,
  事务B把张三的工资改为8000,并提交了事务。
  随后,
  在事务A中,再次读取张三的工资,此时工资变为8000。在一个事务中前后两次读取的结果并不致,导致了不可重复读。
3)幻读:是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。
例如:
  目前工资为5000的员工有10人,事务A读取所有工资为5000的人数为10人。
  此时,
  事务B插入一条工资也为5000的记录。
  这是,事务A再次读取工资为5000的员工,记录为11人。此时产生了幻读。

  5、事务的隔离级别

  事务的隔离性受到隔离级别的影响。那么事务的隔离级别是什么呢?事务的隔离级别可以认为是事务的"自私"程度,它定义了事务之间的可见性。隔离级别分为以下几种:

          1).READ UNCOMMITTED(未提交读)。在RU的隔离级别下,事务A对数据做的修改,即使没有提交,对于事务B来说也是可见的,这种问题叫脏读。这是隔离程度较低的一种隔离级别,在实际运用中会引起很多问题,因此一般不常用。

          2).READ COMMITTED(提交读)。在RC的隔离级别下,不会出现脏读的问题。事务A对数据做的修改,提交之后会对事务B可见,举例,事务B开启时读到数据1,接下来事务A开启,把这个数据改成2,提交,B再次读取这个数据,会读到最新的数据2。在RC的隔离级别下,会出现不可重复读的问题。这个隔离级别是许多数据库的默认隔离级别。

          3).REPEATABLE READ(可重复读)。在RR的隔离级别下,不会出现不可重复读的问题。事务A对数据做的修改,提交之后,对于先于事务A开启的事务是不可见的。举例,事务B开启时读到数据1,接下来事务A开启,把这个数据改成2,提交,B再次读取这个数据,仍然只能读到1。在RR的隔离级别下,会出现幻读的问题。幻读的意思是,当某个事务在读取某个范围内的值的时候,另外一个事务在这个范围内插入了新记录,那么之前的事务再次读取这个范围的值,会读取到新插入的数据。Mysql默认的隔离级别是RR,然而mysql的innoDB引擎间隙锁成功解决了幻读的问题。

          4).SERIALIZABLE(可串行化)。可串行化是最高的隔离级别。这种隔离级别强制要求所有事物串行执行,在这种隔离级别下,读取的每行数据都加锁,会导致大量的锁征用问题,性能最差。

   设置事务的隔离级别:

  方式一:可以在my.ini文件中使用transaction-isolation选项来设置服务器的缺省事务隔离级别

  该选项值可以是:

  – READ-UNCOMMITTED

  – READ-COMMITTED

  – REPEATABLE-READ

  – SERIALIZABLE

  ? 例如: [mysqld] transaction-isolation = READ-COMMITTED

  方式二:通过命令动态设置隔离级别 

  隔离级别也可以在运行的服务器中动态设置,应使用SET TRANSACTION ISOLATION LEVEL语句。 

  其语法模式为:

  SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL
  其中的可以是:
  – READ UNCOMMITTED
  – READ COMMITTED
  – REPEATABLE READ
  – SERIALIZABLE
  ? 例如: SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

 6、事务的一些术语及两条重要的SQL语句
  • 开启事务:Start Transaction
  • 事务结束:End Transaction
  • 提交事务:Commit Transaction
  • 回滚事务:Rollback Transaction

在MySQL中,默认情况下,事务是自动提交的,也就是说,只要执行一条DML语句就开启了事物,并且提交了事务(当然,自动提交机制也是可以关闭的)。

二、事务操作实例

  事务操作,分为两种:自动事务(默认的),手动事务。

  以银行账户的余额增减为例,我们来了解手动事务的操作流程。

  执行如下 SQL 语句,创建银行账户表并插入数据:

-- 创建银行账户表
create table bank_account(
    id int primary key auto_increment,
    cardno varchar(16) not null unique comment 'bank card number',
    name varchar(20) not null,
    money decimal(10,2) default 0.0 comment 'account balance' 
)charset utf8;

-- 插入数据
insert into bank_account values
(null, '0000000000000001', 'Charies', 8000),
(null, '0000000000000002', 'Gavin', 6000);

  操作事务:

第 1 步:开启事务,告诉系统以下所有操作,不要直接写入数据库,先存到事务日志。

  • 基本语法:start transaction;

执行如上 SQL 语句,开启事务:

第 2 步:减少 Charies 账户的余额

-- 更新 Charies 账户余额
update bank_account set money = money - 1000 where id = 1;
-- 查询 bank_account 表数据
select * from bank_account;

如图所示:

  Charies 账户的余额显示减少1000,但实际上,由于我们开启了事务,数据表真实的数据,并没有同步更新。为了验证这个论断,我们重新打开一个数据库客户端,查询bank_account表的数据:

如上图所示:

  显然数据库的事务安全机制起了作用,当我们开启(手动)事务之后,其后一系列操作并没有直接写入数据库,而是存入了事务日志。在这里,我们并没有打开数据库事务的日志进行验证,因为事务日志存储的是经过编译之后的字节码文件。

 

第 3 步:增加 Gavin 账户的余额

-- 更新 Gavin 账户余额
update bank_account set money = money + 1000 where id = 2;
-- 查询 bank_account 表数据
select * from bank_account;

如上图所示:

  Gavin 账户的余额显示增加1000,但是,由于我们开启了事务,数据表真实的数据,仍然没有同步更新。

第 4 步:提交事务或回滚事务

  •   提交事务基本语法:commit;
  •   回滚事务基本语法:rollback;

如果我们选择提交事务,则将事务日志存储的记录直接更新到数据库,并清除事务日志;如果我们选择回滚事务,则直接将事务日志清除,所有在开启事务至回滚事务之间的操作失效,保持原有的数据库记录不变。在这里,我们以提交事务为例:

-- 提交事务
commit;
-- 查询 bank_account 表数据
select * from bank_account;

如上图所示:

  当我们提交事务之后,数据库的真实记录更新,两个客户端的数据一致。

值得我们注意的是:

  当我们提交事务之后,在进行回滚事务是不起作用的,因为事务日志在提交事务的同时已经被清除了!

 参考博客:https://blog.csdn.net/qq_35246620/article/details/78305872