阅读 142

MySql中的事务

Mysql数据库是我们在开发中常用的一个开源数据库,自2001年MySql开始引入InnoDB存储引擎,并在之后一年宣布全面支持事务以来,Mysql的发展进入快车道;Mysql其优良的性能与可靠性,支持它开源数据库中独占鳌头。同时,MySql也有完善,安全的事务控制。

1.存储引擎的选择

同大多数数据库一样,Mysql中有一个存储引擎的概念,针对不同的存储需求可以选择最优的存储引擎 Mysql默认支持多种存储引擎,5.0后,包括:MyISAM、InnoDB、MEMORY、MERGE、BDB等,其中InnoDB和BDB提供事务安全表,其他存储引擎都是非事务安全表;

这里我讲的是事务的控制,所以我们在创建表的时候,都是选择的InnoDB存储引擎,它也是MySql5.5之后的默认存储引擎

2.事务的概念

事务指逻辑上的一组操作,组成这组操作的各个单元,要不全部成功,要不全部不成功。 也就是下面事务的原子性 这里我们一次银行的转账为例:A——B转帐,对应于如下两条sql语句

update from account set money=money+100 where name='B';
update from account set money=money-100 where name='A';
复制代码

每条sql语句可以看做一个独立的单元,这两条语句只能全部执行完成才能算这次事务执行完成

3.事务的四大特性(ACID)

3.1 原子性(Atomicity

  原子性是指事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失败。比如在同一个事务中的SQL语句,要么全部执行成功,要么全部执行失败

3.2 一致性(Consistency)

  官网上事务一致性的概念是:事务必须使数据库从一个一致性状态变换到另外一个一致性状态。以转账为例子,A向B转账,假设转账之前这两个用户的钱加起来总共是2000,那么A向B转账之后,不管这两个账户怎么转,A用户的钱和B用户的钱加起来的总额还是2000,这个就是事务的一致性。

3.3 隔离性(Isolation)

  事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。

3.4 持久性(Durability)

  持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响
 事务的四大特性中最麻烦的是隔离性,也是我们事务控制的主要内容  

 4.MySql数据库中操作事务的命令

默认情况下,MySql事务是自动提交(Autocommit)的,若果需要明确的Commit和Rollback来提交和回滚事务,那么就需要明确的事务控制命令来开始事务。
SET AUTOCOMMIT可以修改当前连接的提交方式,如果设置了set autocommit=0,则设置之后的所有事务都需要通过明确的命令进行提交或者回滚。如果只是对某些语句需要进行事务控制,则使用start transaction语句开始一个事务比较方便,这样事务结束之后可以自动回到自动提交的方式,如果希望所有的事务都不是自动提交的,那么通过修改AUTOCOMMIT来控制事务比较方便,这样不用在每个事务开始的时候再执行start transaction.

4.1. 创建测试sql脚本
//创建账户表
CREATE TABLE `account` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(40) DEFAULT NULL,
  `money` float DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

//插入测试数据
INSERT INTO `account` VALUES ('1', 'A', '1000');
INSERT INTO `account` VALUES ('2', 'B', '1000');
INSERT INTO `account` VALUES ('3', 'C', '1000');


复制代码
4.2 开启事务(start transaction)

使用"start transaction"开启MySQL数据库的事务,如下所示:

mysql> select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
|  1 | A    |  1000 |
|  2 | B    |  1000 |
|  3 | C    |  1000 |
+----+------+-------+
3 rows in set (0.00 sec)

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
复制代码

我们首先在数据库中模拟转账失败的场景,首先执行update语句让A用户的money减少100块钱,如下所示:

mysql> update account set money=money-100 where name='A';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql>  select * from account where name='A';
+----+------+-------+
| id | name | money |
+----+------+-------+
|  1 | A    |   900 |
+----+------+-------+
1 row in set (0.00 sec)
复制代码

然后我们关闭当前操作的dos命令行窗口,这样就导致了刚才执行的update语句的数据库的事务没有被提交,那么我们对A用户的修改就不算是是真正的修改了,下次在查询A用户的money时,依然还是之前的1000,如下所示:

mysql> select * from account where name='A';
+----+------+-------+
| id | name | money |
+----+------+-------+
|  1 | A    |  1000 |
+----+------+-------+
1 row in set (0.01 sec)
复制代码
4.3 提交事务

 下面我们在数据库模拟A——B转账成功的场景:  

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

//让两个更新操作在同一事务中进行
mysql> update account set money=money-100 where name='A';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> update account set money=money+100 where name='B';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
//提交事务
mysql> commit;
Query OK, 0 rows affected (0.03 sec)

//转账完成
mysql> select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
|  1 | A    |   900 |
|  2 | B    |  1100 |
|  3 | C    |  1000 |
+----+------+-------+
3 rows in set (0.00 sec)
复制代码

在事务提交后,A——B转账100块钱的这个业务操作算是真正成功了,A账户中少了100,B账户中多了100。

4.4 回滚事务(rollback)

通过手动回滚事务,使前面执行的操作无效


mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

//对A,B进行两次update操作
mysql> update account set money=money-100 where name='A';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> update account set money=money+100 where name='B';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> update account set money=money-100 where name='A';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> update account set money=money+100 where name='B';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from account where name in('A','B');
+----+------+-------+
| id | name | money |
+----+------+-------+
|  1 | A    |   700 |
|  2 | B    |  1300 |
+----+------+-------+
2 rows in set (0.00 sec)

//手动回滚事务,使前面的update操作无效
mysql> rollback;
Query OK, 0 rows affected (0.03 sec)

//A账户,B账户回到事务开始前状态
mysql> select * from account where name in('A','B');
+----+------+-------+
| id | name | money |
+----+------+-------+
|  1 | A    |   900 |
|  2 | B    |  1100 |
+----+------+-------+
2 rows in set (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)
复制代码

过手动回滚事务,让所有的操作都失效,这样数据就会回到最初的初始状态。
在事务中可以通过定义SAVEPOINT,指定回滚事务的一个部分,语法:


mysql> savepoint test;
Query OK, 0 rows affected (0.00 sec)
复制代码

回滚时,就可以指定回滚点:

mysql> rollback to savepoint test;
Query OK, 0 rows affected (0.00 sec)
复制代码

5.事务的隔离级别

 多个线程开启各自事务操作数据库中数据时,数据库系统要负责隔离操作,以保证各个线程在获取数据时的准确性。

 5.1、事务不考虑隔离性可能会引发的问题  

  如果事务不考虑隔离性,可能会引发如下问题:

  1、脏读

脏读指一个事务读取了另外一个事务未提交的数据。
这是非常危险的,假设A向B转帐100元,对应sql语句如下所示    

1.update account set money=money+100 where name='B';    
2.update account set money=money-100  where name='A
复制代码

当第1条sql执行完,第2条还没执行(A未提交时),如果此时B查询自己的帐户,就会发现自己多了100元钱。如果A等B走后再回滚,如果B再查询账户发现100元并没有到账,B就会损失100元。  

  2、不可重复读

不可重复读指在一个事务内读取表中的某一行数据,多次读取结果不同。
  例如银行想查询A帐户余额,第一次查询A帐户为200元,此时A向帐户内存了100元并提交了,银行接着又进行了一次查询,此时A帐户为300元了。银行两次查询不一致,可能就会很困惑,不知道哪次查询是准的。
不可重复读和脏读的区别是,脏读是读取前一事务未提交的脏数据,不可重复读是重新读取了前一事务已提交的数据
  很多人认为这种情况就对了,无须困惑,当然是后面的为准。我们可以考虑这样一种情况,比如银行程序需要将查询结果分别输出到电脑屏幕和写到文件中,结果在一个事务中针对输出的目的地,进行的两次查询不一致,导致文件和屏幕中的结果不一致,银行工作人员就不知道以哪个为准了。

  3、虚读(幻读)

  虚读(幻读)是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。
  如丙存款100元未提交,这时银行做报表统计account表中所有用户的总额为500元,然后丙提交了,这时银行再统计发现帐户为600元了,造成虚读同样会使银行不知所措,到底以哪个为准。

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

5.2、事务隔离性的设置语句

MySQL数据库共定义了四种隔离级别:

  • Serializable(串行化):可避免脏读、不可重复读、虚读情况的发生。
  • Repeatable read(可重复读):可避免脏读、不可重复读情况的发生。
  • Read committed(读已提交):可避免脏读情况发生。
  • Read uncommitted(读未提交):最低级别,以上情况均无法保证。

mysql数据库查询当前事务隔离级别:select @@tx_isolation

例如:
mysql> select @@tx_isolation
    -> ;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)
复制代码

mysql数据库默认的事务隔离级别是:Repeatable read(可重复读)
mysql数据库设置事务隔离级别:set transaction isolation level 隔离级别名

mysql> set transaction isolation level Read uncommitted;
Query OK, 0 rows affected (0.00 sec)
复制代码
5.3、使用MySQL数据库演示不同隔离级别下的并发问题

同时打开两个窗口模拟2个用户并发访问数据库

1、当把事务的隔离级别设置为read uncommitted时,会引发脏读、不可重复读和虚读

A窗口

//设置隔离级别为未提交
mysql> set transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
|  1 | A    |  1000 |
|  2 | B    |  1000 |
|  3 | C    |  1000 |
+----+------+-------+
3 rows in set (0.01 sec)

//这里读了B事务未提交的脏数据
mysql> select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
|  1 | A    |  1100 |
|  2 | B    |  1000 |
|  3 | C    |  1000 |
+----+------+-------+
3 rows in set (0.00 sec)
复制代码

B窗口   

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> update account set money=money+100 where name='A';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
//这里不要提交,到A窗口查询A账户
mysql>
复制代码

2、当把事务的隔离级别设置为read committed时,会引发不可重复读和虚读,但避免了脏读

A窗口    

set transaction isolation level  read committed;
start transaction;
select * from account;--发现a帐户是1000元,转到b窗口
select * from account; --发现a帐户多了100,这时候,a读到了别的事务提交的数据,两次读取a帐户读到的是不同的结果(不可重复读)
复制代码

B窗口    

start transaction;
update account set money=money+100 where name='aaa';
 commit;--转到a窗口
复制代码

3、当把事务的隔离级别设置为repeatable read(mysql默认级别)时,会引发虚读,但避免了脏读、不可重复读 mysql在执行select操作时,也可以避免幻读,但是在执行update、delete、insert操作时,仍然会有幻读
MySql在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)
具体可看:www.cnblogs.com/chinesern/p…
具体看个例子:
A窗口:


mysql> start transaction; --使用默认隔离级别 repeatable read
Query OK, 0 rows affected (0.00 sec)

//A账户初始状态
mysql> select * from account where name='A';
+----+------+-------+
| id | name | money |
+----+------+-------+
|  1 | A    |  1000 |
+----+------+-------+
1 row in set (0.00 sec)

//B事务提交后再次查看,A账户未发生变化,避免了不可重复读
mysql> select * from account where name='A';
+----+------+-------+
| id | name | money |
+----+------+-------+
|  1 | A    |  1000 |
+----+------+-------+
1 row in set (0.00 sec)

mysql> update account set money=money+100 where name='A';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
//这里只更新了一次,但是结果却不一致,引发了幻读
mysql> select * from account where name='A';
+----+------+-------+
| id | name | money |
+----+------+-------+
|  1 | A    |  1200 |
+----+------+-------+
1 row in set (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.03 sec)

mysql> select * from account where name='A';
+----+------+-------+
| id | name | money |
+----+------+-------+
|  1 | A    |  1200 |
+----+------+-------+
1 row in set (0.00 sec)

mysql>
复制代码

B窗口:

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

//修改A账户
mysql> update account set money=money+100 where name='A';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit;
Query OK, 0 rows affected (0.13 sec)
//提交后转到A窗口查看
mysql>
复制代码

4、当把事务的隔离级别设置为Serializable时,会避免所有问题

A窗口    

set transaction isolation level Serializable;
start transaction;
select * from account;--转到b窗口
复制代码

B窗口    

start transaction;
insert into account(name,money) values('ggg',1000);--发现不能插入,只能等待a结束事务才能插入
复制代码

在选择事务隔离级别时,一般不推荐使用串行化,一是默认的repeatable read以能应付大多数并发情况,二是使用串行化会加大系统开销,执行效率大大降低

关注下面的标签,发现更多相似文章
评论