MySQL中的事务

转自:https://www.cnblogs.com/hebao0514/category/719525.html

一、事务的四大特性(ACID)

1. 原子性(atomicity):一个事务必须视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作,这就是事务的原子性。

2. 一致性(consistency):数据库总是从一个一致性的状态转换到另一个一致性的状态。
拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。

3. 隔离性(isolation):一个事务所做的修改在最终提交以前,对其他事务是不可见的。
比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。

4. 持久性(durability):一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。

二、事务的简单使用
1. 在使用数据库时候需要使用事务,必须先开启事务,开启事务的语句具体如下:

start transaction

2. 事务开启之后就可以执行SQL语句
3. SQL语句执行成功之后,需要提交事务,提交事务的语句如下:

commit

note:
在MySQL中直接书写的SQL语句都是自动提交的,而事务中的操作语句需要使用commit语句手动提交,只有事务提交后其中的操作才会生效。
如果不想提交事务,我们还可以使用相关语句取消事务(也称回滚),具体语句如下:

rollback

需要注意的是,rollback语句只能针对未提交的事务执行的回滚操作,已经提交的事务是不能回滚的。

例子:通过一个转账的案例演示如何使用事务。
1. 创建表

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);

2. 使用事务来实现转账:首先开启一个事务,然后通过update语句将 a账户的100元 转给 b 账户,然后提交事务

start transaction;
update account set money=money-100 where name='a';
update account set money=money+100 where name='b';
commit;

1). 在命令行中执行,执行效果如下:

事务提交和事务回滚的具体例子参考这里:事务提交事务回滚

2). 在Navicat中执行,执行结果如下:

 

参考:

https://www.cnblogs.com/hebao0514/p/5490698.html

 三、脏读

(一)、什么是脏读

 脏读就是指一个事务读取了另一个事务未提交的数据。

(二)、脏读的例子及避免

(1). 脏读例子:

脏读例子:https://www.cnblogs.com/hebao0514/p/5490764.html  Read Uncommitted(读未提交)(个人理解:一个事务读到另一个事务还未提交的数据)

例子说明:开启两个线程,分别模拟a账户和b账户,
MySQL的默认隔离级别是Repeatable Read(可重复读),该级别是可以避免脏读的,因此需要将b账户中事务的隔离级别设置为Read Uncommitted(读未提交)。
在a账户中开启一个事务,执行转账功能,但未提交(commit);在b账户中开启一个事务,执行查询功能,因为b账户的事务隔离级别低,
就读到了a账户还没提交的数据(即出现脏读),这时候,b误以为a账户已经转账成功,便会给a发货,当b发货之后,a如果不提交事务而将事务回滚,b就会受到损失。

(2). 避免脏读:Read Committed 隔离级别来避免脏读的例子:

1. a账户(左)和b账户(右)当前余额:

   

2. 开启事务,转账给b账户:

3. 此时a账户的事务并未提交,此时b账户查看余额:

 

可以看出,b账户中仍为1000,没有读到a账户中没有提交的信息。说明Read Committed 隔离级别可以避免脏读

 四、事务隔离级别

https://www.cnblogs.com/hebao0514/p/5492108.html

1). Read Uncommitted   2). Read Committed   3). Repeatable Read   4). Serializable
(1)Read Uncommitted
Read UnCommitted(读未提交)是事务中最低的级别,该级别下的事务可以读取到另一个事务中未提交的数据,也被称为脏读(Dirty Read),这是相当危险的。由于该级别较低,在实际开发中避免不了任何情况,所以一般很少使用。
 
(2)Read Committed
大多数的数据库管理系统的默认隔离级别都是Read Committed(读提交),该级别下的事务只能读取其他事务中已经提交的内容,可以避免脏读,但是不能避免重复读和幻读的情况。
重复读:在事务内读取了别的线程已经提交的数据,但是两次查询读取结果不一样,原因是查询的过程中其他事务做了更新操作
幻读:在事务内两次查询的数据条数不一样,原因是查询的过程中其他事务做了添加操作
 
(3)Repeatable Read
Repeatable Read(可重复读)是MySQL默认的事务隔离级别,它可以避免脏读、不可重复读的问题,确保同一个事务的多个实例在并发操作数据的时候,会看到相同的数据行。但是理论上,该级别会出现幻读情况,不过MySQL的存储引擎通过多版本并发控制机制解决了该问题,因此该级别是可以避免幻读的。
 
(4)Serializable
Serializable(可串行化)是事务的最高隔离级别,它会强制对事务进行排序,使它们彼此之间不会发生冲突,从而解决脏读、幻读、重复读的问题。实际上,就是在每个读的数据行上加上锁。这个级别,可能导致大量超时现象和锁竞争,实际应用中很少使用。

五、不可重复读

(一)、什么是不可重复读

不可重复读(Non-Repeatable Read)是指事务中两次查询的结果不一致,原因是在查询的过程中其他事务做了更新的操作。

例如,银行在做统计报表的时候,第一次查询a账户有1000元,第二次查询a账户有900元,原因是统计期间a账户取出了100元,这样导致多次查询中,查询结果不一致。

不可重复读和脏读有点类似,但是脏读是读取了另一个事务未提交的脏数据,不可重复读是在事务内重复读取了别的线程已提交的数据。

note:MySQL的默认事务隔离级别是:Repeatable Read(可重复读)

(二)、不可重复读的例子及避免

(1). 不可重复读的例子

不可重复读的例子:https://www.cnblogs.com/hebao0514/p/5494442.html   Repeatable Read(可重复读)(个人理解:一个事务中,重复查询的结果是一样的,哪怕在第一次查询之后,数据库已经变了,查询结果仍与第一次查询结果一致,而不是查询数据库中的最新数据)

1. 线程2:查看线程2中事务隔离级别:

1. 线程2:首先在线程2中开启一个事务,然后在当前事务中查询各个账户的余额信息:

3. 线程1:线程1中不用开启事务,直接使用update更新a账户,并查询余额:

note:由于线程1只需要执行修改的操作,不需要保证同步性,因此直接执行SQL语句就可以
4. 线程2:当线程1更新操作执行完成后,在线程2中再次查询各账户余额,发现a账户变为900:

线程2中,a账户两次的查询结果不一致,实际上这种操作是没有错的(虽然没错,但应该避免这种情况?)

例子说明:开启两个线程1和2,线程2的隔离级别为Read Committed。在线程2中开启事务,查询a账户为1000,此时还不提交事务;在线程1中不用开启事务,更新a账务为900;返回线程2(此时事务还没有提交),查询a账户变为900。即:线程2在同一个事务中,两次查询结果不一致。(博客例子中的a账户和b账户应该为表述错误,应为:线程1和线程2,操作的账户都是a账户。另外博客的线程2(即b账户中)继前一篇博客的操作,已经改成了Read Committed 隔离级别)

(2). 避免不可重复读:Repeatable Read 隔离级别来避免不可重复读的例子:

1. 查看线程1中事务隔离级别:

2. 在线程1中开启一个事务,然后在当前事务中查询各个账户的余额信息:

3. 线程2中不用开启事务,直接使用update语句执行更新操作,并查询余额:

4. 返回线程1:当线程2更新操作执行完成后,在线程1中再次查询账户余额,发现a账户仍未1000:

5. 如果此时在线程1中修改a账户,会报错:ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

六、幻读

 (一)、什么是幻读

幻读(Phantom Read)又称为虚读,是指在一个事务内两次查询中数据条数不一致,幻读和不重复读有些不同,同样是在两次查询过程中,不同的是,幻读是由于其他事务做了插入记录的操作,导致记录数有所增加。

例如:银行在做统计报表时统计account表中所有用户的总金额时候,此时总共有两个账户,总共金额为2000元,这时候新增了一个用户账户,并且存入1000元,这时候银行再次统计就会发现账户总金额为3000,造成了幻读情况

(二)、幻读的例子及避免

(1). 幻读的例子

幻读的例子:https://www.cnblogs.com/hebao0514/p/5494588.html  Phantom Read(幻读、虚读)(个人理解:一个事务中,重复查询的结果是一样的,哪怕在第一次查询之后,数据库已经变了,查询结果仍与第一次查询结果一致,而不是查询数据库中的最新数据。幻读是针对insert操作)

1. 线程2:首先设置线程2的隔离级别为Read Committed,并查询。(可重复读隔离级别是可以避免幻读的出现,因此需要将事务的隔离级别设置为更低)

2. 线程2:开启一个事务,然后在当前事务中查询账户的余额信息:

3. 线程1:先查询account表中的信息,然后进行添加操作:(线程1不用开启事务,直接执行添加操作即可)

4. 线程2:线程1添加完记录后,在线程2中查询余额信息:

可以发现,在Read Committed隔离级别下,线程2中第二次查询数据比第一查询数据的时候多一条记录,这种情况并不是错误的,但可能不符合实际需求。

例子说明:开启两个线程1和2,线程2的隔离级别为Read Committed。在线程2中开启事务,查询account表的结果为共有两条记录;在线程1中不用开启事务,在线程1中添加一条记录(c,1000);返回线程2,查询account表,查询结果变为共有三条记录。即:线程2在同一个事务中,两次查询结果不一致。

幻读和不重复读的区别:
同样在两次查询过程中,不同的是,幻读是由于其他事务做了插入操作(insert),导致记录数有所增加。而不重复读是由于其他事务做了更新操作(update),导致同一条记录的查询结果不同。

(2). 避免幻读:Repeatable Read 隔离级别来避免幻读的例子:

先删除刚才添加的(c,1000)这一条记录

1. 线程2:为了防止出现幻读,可以将线程2的隔离级别设置为Repeatable Read

2. 线程2:开启一个事务,然后在当前事务中查询账户的余额信息:

3. 线程1:先查询account表中的信息,然后进行添加操作:(线程1不用开启事务,直接执行添加操作即可)

4. 线程2:线程1添加完记录后,在线程2中查询余额信息

可以发现,在Repeatable Read隔离级别下,线程2中两次查询结果是一样的。
5. 线程2:使用commit提交当前事务,再查询account表,查询到三条记录

Repeatable Read从理论的角度是会出现幻读的,但是MySQL内部通过多版本控制机制【实际上就是对读取到的数据加锁】解决这个问题。
因此,用户才可以放心大胆使用Repeatable Read这个事务隔离级别。

note:Serializable 和 Repeatable Read都可以防止幻读。但是Serializable 事务隔离级别效率低下,比较耗数据库性能,一般不使用。

posted @ 2018-11-29 15:00  zeroingToOne  阅读(924)  评论(0编辑  收藏  举报