事务概述
现实生活中我们经常会进行银行转账操作,该操作可分为两部分来完成:账户A转出和账户B转入, 只有这两个部分都无误完成才认为转账成功。在数据库中,可通过两条语句完成该转账操作,如果其中任意一条语句出现异常没有正常执行则会导致两个账户的金额不同步造成错误。 为了防止上述情况的发生,MySQL中引人了事务。所谓事务就是针对数据库的一组操作(由一条或多条SQL语句组成)进行管控。如果其中任一条语句无法执行,那么所有的语句都不会执行。也就是说,事务中的语句要么都执行,要么都不执行。
在数据库中使用事务时必须先开启事务,代码如下:
START TRANSACTION;
事务开启之后就可以执行SQL语句,SQL语句执行成功后,需要使用相应语句提交事务,代码如下:
COMMIT;
请注意:平常我们在MySQL中直接书写的SQL语句都是自动提交的它会立即生效;但是,事务中的操作语句都需要使用COMMIT语句手动提交,否则不会生效。
如果不想提交当前事务还可以使用相关语句取消事务(也称回滚),代码如下:
ROLLBACK;
ROLLBACK语句只能针对未提交的事务执行回滚操作,已提交的事务是不能回滚的。
事务入门示例
在此,创建一张account表用于表示账户信息并进行相关操作。
先来看事务的提交,代码如下:
DROP TABLE IF EXISTS account;
-- 创建account表
CREATE TABLE account(
id INT primary key auto_increment,
name VARCHAR(40),
money FLOAT
);
-- 向表中插入数据
INSERT INTO account(name, money) VALUES ('A',1000);
INSERT INTO account(name, money) VALUES ('B',1000);
-- 开始事务
START TRANSACTION;
-- 转账
UPDATE account SET money=money-100 WHERE NAME= 'A';
UPDATE account SET money=money+100 WHERE NAME= 'B';
-- 提交事务
COMMIT;
再来看事务的回滚,代码如下:
DROP TABLE IF EXISTS account;
-- 创建account表
CREATE TABLE account(
id INT primary key auto_increment,
name VARCHAR(40),
money FLOAT
);
-- 向表中插入数据
INSERT INTO account(name, money) VALUES ('A',1000);
INSERT INTO account(name, money) VALUES ('B',1000);
-- 开始事务
START TRANSACTION;
-- 转账
UPDATE account SET money=money-100 WHERE NAME= 'A';
UPDATE account SET money=money+100 WHERE NAME= 'B';
-- 回滚事务
ROLLBACK;
事务的特性
事务有着非常严格的定义,它必须同时满足4个特性,即:原子性(Atomicity)、一致性 (Consistency)、隔离性(Isolation)、持久性(Durability),简称事务ACID标准。
原子性
事务必须被视为一个不可分割的最小工作单元,只有事务中所有的数据库操作都执行成功才算整个事务执行成功。如果事务中有任何一个SQL语句执行失败,则已经执行成功的SQL语句也必须撤销,数据库的状态退回到执行事务前的状态。
—致性
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。 例如:假设用户A和用户B两者的钱加起来一共为3000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还是3000。
隔离性
数据库为每一个用户开启的事务不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。例如:多个用户操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。 例如:对于任意两个并发的事务T1和T2。从事务T1的角度看来:T2要么在T1开始之前就已经结束,要么T2在T1结束之后才开始。也就是说:每个事务都感觉不到有其它事务在并发地执行。
持久性
事务一旦提交,其所做的修改就会永久保存到数据库中,即使数据库发生故障也不 应该对其有任何影响。需要注意的是,事务的持久性不能做到100%的持久,只能从事务本身的角度来保证永久性,而一些外部原因导致数据库发生故障,如硬盘损坏,那么所提交的数据可能都会丢失。
事务的隔离级别
通常情况下数据库是多线程并发访问的,所以很容易出现多个线程同时开启事务的情况。在该情况下和可能出现脏读、重复读以及幻读的情况,为了避免这种情况的发生,就需要为事务设置隔离级别。在此,我们分别介绍在MySQL中事务的4种隔离级别。
READ UNCOMMITTED
READ UNCOMMITTED(读未提交)是事务中最低的级别。在该级别下的事务可以读 取到另一个事务中未提交的数据也被称为脏读(Dirty Read)。由于该级别太低,所以在实际开发中避免不了任何情况,所以极少使用。
脏读
在此演示脏读,详细步骤如下:
第一步
准备数据,代码如下:
DROP TABLE IF EXISTS account;
-- 创建account表
CREATE TABLE account(
id INT primary key auto_increment,
name VARCHAR(40),
money FLOAT
);
-- 向表中插入数据
INSERT INTO account(name, money) VALUES ('A',1000);
INSERT INTO account(name, money) VALUES ('B',1000);
第二步
打开两个Dos中模拟两个操作数据的不同线程甲和乙,图示如下:
第三步
在线程乙中设置事务的隔离级别为READ UNCOMMITTE代码如下:
-- 设置事务隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- 查询事务隔离级别
SELECT @@tx_isolation;
图示如下: 从上图可以看出:线程乙的事务的隔离级别已经被修改为READ UNCOMMITTED
第四步
在线程乙中开启事务并查询目前所有账户的信息。
代码如下:
-- 开启事务
START TRANSACTION;
-- 查询所有账户信息
SELECT * FROM account;
图示如下: 第五步
在线程甲中开启事务并执行转账。
代码如下:
-- 开启事务
START TRANSACTION;
-- 转账
UPDATE account SET money=money-100 WHERE name='A';
UPDATE account SET money=money+100 WHERE name='B';
图示如下: 在线程甲中开启了事务并执行转账但是没有提交事务。
第六步
在线程乙中查询所有账户信息 代码如下:
-- 查询所有账户信息
SELECT * FROM account;
图示如下: 从上图可以看出:在线程乙中读到了线程甲在事务操作中未提交的数据!!此时,线程甲的事务可以使用回滚ROLLBACK
撤销之前的操作!这是相当危险的!这就好比:线程甲是客户,线程乙是卖家;因为脏读卖家误以为收到了客户的钱款于是立马发货,但是客户却通过回滚撤销了之前的转账。
图示如下: 在图示的操作中:线程甲执行了回滚撤销之前的转账操作后线程乙再次查询时发现数据发生了变化!
READ COMMITTED
为了避免脏读的发生,可将线程乙的隔离级别设置为READ COMMITTED,代码如下:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
大多数的数据库管理系统(例如Oracle)的默认隔离级别都是READ COMMITTED(读提交)。该级别下的事务只能读取其它事务已经提交的内容,可以避免脏读但不能避免重复读和幻读的情况。
重复读就是在事务内重复读取别的线程已经提交的数据的时读取的结果不一致。导致该问题的原因是查询的过程中其它事务做了更新操作。
幻读是指在一个事务内两次查询中数据条数不一致。导致该问题的原因是查询的过程中其它的事务做了添加操作。
详细步骤如下:
第一步
准备数据,代码如下:
DROP TABLE IF EXISTS account;
-- 创建account表
CREATE TABLE account(
id INT primary key auto_increment,
name VARCHAR(40),
money FLOAT
);
-- 向表中插入数据
INSERT INTO account(name, money) VALUES ('A',1000);
INSERT INTO account(name, money) VALUES ('B',1000);
第二步
打开两个Dos中模拟两个操作数据的不同线程甲和乙,
第三步
在线程乙中设置事务的隔离级别为READ COMMITTED代码如下:
-- 设置事务隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 查询事务隔离级别
SELECT @@tx_isolation;
图示如下: 从上图可以看出:线程乙的事务的隔离级别已经被修改为READ COMMITTED
第四步
在线程乙中开启事务并查询目前所有账户的信息。
代码如下:
-- 开启事务
START TRANSACTION;
-- 查询所有账户信息
SELECT * FROM account;
图示如下: 第五步
在线程甲中开启事务并执行转账。
代码如下:
-- 开启事务
START TRANSACTION;
-- 转账
UPDATE account SET money=money-100 WHERE name='A';
UPDATE account SET money=money+100 WHERE name='B';
图示如下: 在线程甲中开启了事务并执行转账但是没有提交事务。
第六步
在线程乙中查询所有账户信息 代码如下:
-- 查询所有账户信息
SELECT * FROM account;
图示如下: 在为线程乙设置隔离级别为READ COMMITTED后再重复刚才示例的操作可以发现:线程乙在同一个事务中的查询结果是一致的,并没有查询到线程甲中事务未提交的内容,因此可以说明READ COMMITTED隔离级别可以有效避免脏读。
REPEATABLE READ
REPEATABLE READ(可重复读)是MySQL默认的事务隔离级别。它可以避免脏读和不可重复读。但理论上,该级别会出现幻读的情况。不过,MySQL的存储引擎通过多版本并发控制机制解决了该问题,因此该级别是可以避免幻读的。在学习REPEATABLE READ之前,我们先来了解不可重复读。
不可重复读
不可重复读是指事务中两次査询的结果不一致,原因是在査询的过程中其它事务做了更新等操作。例如,银行在做统计报表时,第一次查询编号为9527的账户中有1000元钱,第二次查询9527账户时有900元钱;原因是银行做报表的统计期间9527账户的主人取出了100元,这样就会导致多次统计报表的结果不一致。
不可重复读和脏读有点类似,但是脏读是读取其它线程的事务未提交的脏数据,不可重复读是在该事务内重复读取其他线程已提交的数据但数据并不一致的情况。
在此演示不可重复,详细步骤如下:
第一步
准备数据,代码如下:
DROP TABLE IF EXISTS account;
-- 创建account表
CREATE TABLE account(
id INT primary key auto_increment,
name VARCHAR(40),
money FLOAT
);
-- 向表中插入数据
INSERT INTO account(name, money) VALUES ('A',1000);
INSERT INTO account(name, money) VALUES ('B',1000);
第二步
打开两个Dos中模拟两个操作数据的不同线程甲和乙
第三步
在线程乙中设置事务隔离级别为READ COMMITTED
-- 设置事务隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 查询事务隔离级别
SELECT @@tx_isolation;
图示如下:
第四步
在线程乙中开启事务并查询目前所有账户的信息。
代码如下:
-- 开启事务
START TRANSACTION;
-- 查询所有账户信息
SELECT * FROM account;
图示如下:
第五步
在线程甲中利用update语句更改账户A的money字段的值
代码如下:
-- 更新账户A的money
UPDATE account SET money=money-100 WHERE name='A';
-- 查询账户信息
SELECT * FROM account;
图示如下:
第六步
在线程乙中在此查询账户信息
代码如下:
-- 查询所有账户信息
SELECT * FROM account;
图示如下: 从上图可以看出:在线程乙中的同一个事务中两次查询的结果竟然不同!这就是数据的不可重复读!
为了避免不可重复读的发生,可将线程乙的隔离级别设置为REPEATABLE READ,代码如下:
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
在为线程乙设置隔离级别为REPEATABLE READ后再重复刚才示例的操作可以发现:线程乙在同一个事务中的查询结果是一致的,并没有查询到线程甲中update修改的内容,因此可以说明REPEATABLE READ隔离级别可以有效避免不可重复读。
详细步骤如下:
第一步
准备数据,代码如下:
DROP TABLE IF EXISTS account;
-- 创建account表
CREATE TABLE account(
id INT primary key auto_increment,
name VARCHAR(40),
money FLOAT
);
-- 向表中插入数据
INSERT INTO account(name, money) VALUES ('A',1000);
INSERT INTO account(name, money) VALUES ('B',1000);
第二步
打开两个Dos中模拟两个操作数据的不同线程甲和乙,
第三步
在线程乙中设置事务隔离级别为REPEATABLE READ
-- 设置事务隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 查询事务隔离级别
SELECT @@tx_isolation;
图示如下:
第四步
在线程乙中开启事务并查询目前所有账户的信息。
代码如下:
-- 开启事务
START TRANSACTION;
-- 查询所有账户信息
SELECT * FROM account;
图示如下:
第五步
在线程甲中利用update语句更改账户A的money字段的值
代码如下:
-- 更新账户A的money
UPDATE account SET money=money-100 WHERE name='A';
-- 查询账户信息
SELECT * FROM account;
图示如下:
第六步
在线程乙中在此查询账户信息
代码如下:
-- 查询所有账户信息
SELECT * FROM account;
图示如下: 从上图可见:隔离级别REPEATABLE READ可有效防止不可重复读的发生。
幻读
幻读(PHANTOM READ)又被称为虚读,是指在一个事务内两次查询中数据条数不一致。例如,银行在做统计报表时统计account表中所有用户的总额时,总共有三个账户总共金额有3000。此时,新增了一个账户并且存人了 1000元;在该情况下,银行再统计时发现账户的总金额变为4000造成了幻读。
在此演示幻读,步骤如下:
第一步
准备数据,代码如下:
DROP TABLE IF EXISTS account;
-- 创建account表
CREATE TABLE account(
id INT primary key auto_increment,
name VARCHAR(40),
money FLOAT
);
-- 向表中插入数据
INSERT INTO account(name, money) VALUES ('A',1000);
INSERT INTO account(name, money) VALUES ('B',1000);
第二步
打开两个Dos中模拟两个操作数据的不同线程甲和乙,图示如下
第三步
在线程乙中设置事务隔离级别为READ COMMITTED
-- 设置事务隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 查询事务隔离级别
SELECT @@tx_isolation;
图示如下: 第四步
在线程乙中开启事务并查询目前所有账户的信息。
代码如下:
-- 开启事务
START TRANSACTION;
-- 查询所有账户信息
SELECT * FROM account;
图示如下:
第五步
在线程甲中插入新的账户C
代码如下:
-- 查询账户信息
SELECT * FROM account;
-- 插入新用户C
INSERT INTO account(name, money) VALUES ('C',1000);
-- 查询账户信息
SELECT * FROM account;
图示如下:
第六步
在线程乙查询所有账户信息
代码如下:
-- 查询账户信息
SELECT * FROM account;
图示如下:
此时,查询出了三个账户,其中账户C是新增的账户,出现了幻读。这种情况并不是错误的,但可能不符合实际需求。
为了防止出现幻读,可以将线程乙的事务隔离级别设置为REPEATABLE READ,代码如下:
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
详细步骤如下:
第一步
准备数据,代码如下:
DROP TABLE IF EXISTS account;
-- 创建account表
CREATE TABLE account(
id INT primary key auto_increment,
name VARCHAR(40),
money FLOAT
);
-- 向表中插入数据
INSERT INTO account(name, money) VALUES ('A',1000);
INSERT INTO account(name, money) VALUES ('B',1000);
第二步
打开两个Dos中模拟两个操作数据的不同线程甲和乙,
第三步
在线程乙中设置事务隔离级别为REPEATABLE READ
-- 设置事务隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 查询事务隔离级别
SELECT @@tx_isolation;
图示如下: 第四步
在线程乙中开启事务并查询目前所有账户的信息。
代码如下:
-- 开启事务
START TRANSACTION;
-- 查询所有账户信息
SELECT * FROM account;
图示如下:
第五步
在线程甲中插入新的账户C
代码如下:
-- 查询账户信息
SELECT * FROM account;
-- 插入新用户C
INSERT INTO account(name, money) VALUES ('C',1000);
-- 查询账户信息
SELECT * FROM account;
图示如下:
第六步
在线程乙查询所有账户信息
代码如下:
-- 查询账户信息
SELECT * FROM account;
图示如下: 从上图可以看出:在线程乙中设置事务隔离级别为REPEATABLE READ后避免了幻读的出现。
SERIALIZABLE
SERIALIZABLE(可串行化)是事务的最高隔离级别,它会强制对事务进行排序,使之不会发生冲突,从而解决脏读、幻读、重复读的问题。但是,该级别可能导致大量的超时现象和锁竞争,实际应用中很少使用。
在此介绍该隔离级别的使用,步骤如下:
第一步
准备数据,代码如下:
DROP TABLE IF EXISTS account;
-- 创建account表
CREATE TABLE account(
id INT primary key auto_increment,
name VARCHAR(40),
money FLOAT
);
-- 向表中插入数据
INSERT INTO account(name, money) VALUES ('A',1000);
INSERT INTO account(name, money) VALUES ('B',1000);
第二步
打开两个Dos中模拟两个操作数据的不同线程甲和乙,
第三步
在线程乙中设置事务隔离级别为SERIALIZABLE
-- 设置事务隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 查询事务隔离级别
SELECT @@tx_isolation;
图示如下: 第四步
在线程乙中开启事务并查询目前所有账户的信息。
代码如下:
-- 开启事务
START TRANSACTION;
-- 查询所有账户信息
SELECT * FROM account;
图示如下: 第五步
在线程甲中开启事务并插入新的账户C
代码如下:
-- 开启事务
START TRANSACTION;
-- 插入新用户C
INSERT INTO account(name, money) VALUES ('C',1000);
图示如下: 从上图可见:插入新用户C的操作未能得到执行,而且随后出现提示:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
图示如下: 原因分析:SERIALIZABLE作为事务的最高隔离级别引入了锁的机制,强制对事务进行排序;故,很容易导致大量的超时现象和锁竞争。所以,当线程乙中开启事务执行查询并执行COMMIT;
后其它线程的事务中的操作才能得到执行。
图示如下: 从上图的操作我们可以看出:线程乙中的事务执行了COMMIT;
后线程甲事务中的插入操作得到了响应并生效。