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: 0mysql> 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: 0mysql> 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: 0mysql> update account set money=money+100 where name='B';Query OK, 1 row affected (0.00 sec)Rows matched: 1 Changed: 1 Warnings: 0mysql> update account set money=money-100 where name='A';Query OK, 1 row affected (0.00 sec)Rows matched: 1 Changed: 1 Warnings: 0mysql> update account set money=money+100 where name='B';Query OK, 1 row affected (0.00 sec)Rows matched: 1 Changed: 1 Warnings: 0mysql> 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) 具体可看: 具体看个例子: A窗口:mysql> start transaction; --使用默认隔离级别 repeatable readQuery 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: 0mysql> 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以能应付大多数并发情况,二是使用串行化会加大系统开销,执行效率大大降低