理解事务 - MySQL 事务处理机制

3,155 阅读10分钟
原文链接: www.jianshu.com

事务

数据库事务

数据库事务(Database Transaction),是指作为单个逻辑工作单元执行的一系列操作,要么完全执行,要么完全地不执行。

ACID

事务必须具备ACID四个特性

原子性(Atomicity)

原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚。

一致性(Consistency)

一致性是指事务必须使数据库从一个一致的状态变到另外一个一致的状态,也就是执行事务之前和之后的状态都必须处于一致的状态。

在事务T开始时,此时数据库有一种状态,这个状态是所有的MySQL对象处于一致的状态,例如数据库完整性约束正确,日志状态一致等,当事务T提交后,这时数据库又有了一个新的状态,不同的数据,不同的索引,不同的日志等,但此时,约束,数据,索引,日志等MySQL各种对象还是要保持一致性(正确性)。 这就是 从一个一致性的状态,变到另一个一致性的状态。也就是事务执行后,并没有破坏数据库的完整性约束(一切都是对的)。知乎

隔离性(Isolation)

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

即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。

持久性(Durability)

持久性是指一个事务一旦被提交了,那么对于数据库中的数据改变就是永久性的,即便是在数据库系统遭遇到故障的情况下也不会丢失提交事务的操作。

例如我们在使用JDBC操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务以及正确提交,即使这时候数据库出现了问题,也必须要将我们的事务完全执行完成,否则就会造成我们看到提示事务处理完毕,但是数据库因为故障而没有执行事务的重大错误。

MySQL中使用事务

提交事务:

>set autocommit = 0 禁止自动提交
>start transaction;
>update accout set money=money+100 where name="Jason";
>commit;

回滚事务:

>set autocommit = 0 禁止自动提交
>start transaction;
>update account set money=money-100 where name="justin";
>rollback;

事务隔离性及在MySQL中实践

当多个线程都开启事务操作数据库中数据时,数据库系统要能进行隔离操作,以保证各个线程获取数据的准确性。

如果没有隔离,会发生的几种问题

脏读(Dirty Read)

一个事务处理过程里读取了另一个未提交的事务中的数据

不可重复读(NonRepeatable Read)

对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询的间隔期间,另外一个事务修改并提交了该数据

  不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。
  在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。但在另一些情况下就有可能发生问题,例如对于同一个数据A和B依次查询就可能不同,A和B就可能打起来了……

幻读(Phantom Read)

在一个事务中读取到了别的事务插入的数据,导致前后不一致

  如丙存款100元未提交,这时银行做报表统计account表中所有用户的总额为500元,然后丙提交了,这时银行再统计发现帐户为600元了,造成虚读同样会使银行不知所措,到底以哪个为准。

不同隔离级别的问题


MySQL中的隔离级别设置

查看当前事务的隔离级别

>select @@tx_isolation;


修改事务的隔离级别
>set  [global | session]  transaction isolation level 隔离级别名称;
>set tx_isolation=’隔离级别名称;’


隔离级别:Serializable | Repeatable read | Read committed |
Read uncommitted
注意:
设置默认级别是指当前session的下一个事务
设置session级别是指当前session以后的所有事务
设置global级别是指对之后的所有session,不包括当前session

MySQL事务隔离级别实践

MySQL中的锁机制

数据库为了维护ACID,尤其是一致性和隔离性,一般使用加锁的方式。同时由于数据库是个高并发的应用,同一时间有大量的并发访问,如果加锁过度,会极大的降低并发处理的能力。所以对于加锁的处理,是数据库对于事务处理的精髓所在

锁方案

一次封锁

因为有大量的并发访问,为了预防死锁,一般应用中采用的是一次封锁的方案:就是在方法的开始阶段,已经预先知道需要用到那些数据,然后全部锁住,在方法执行之后,再全部解锁。
这种方案可以有效避免死锁发生,当时由于数据库操作在事务开始阶段并不知道具体会用到哪些数据,所以该方案不合适在数据库中使用。

两段锁

两段锁协议将事务分成两个阶段:加锁阶段和解锁阶段

  • 加锁阶段:在该阶段可以进行加锁操作。读数据前需要申请获取S锁(共享锁:其他事务可以继续加共享锁,但不能加排他锁);写数据前需要申请获取X锁(排他锁:其他事务不能获取任何锁)。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。
  • 解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能进行加锁操作。


    备注:这个方案无法避免死锁,但是可以保证事务调度的串行化(串行化在数据库恢复和备份时候很重要)。

    MySQL的锁类型

    表锁
    对一整张表加锁,并发能力低下(即使有分读锁、写锁),一般在DDL处理时使用
    行锁
    只锁住特定行的数据,并发能力强,MySQL一般都是用行锁来处理并发事务。

    如果用到无索引的字段,那么MySQL会在存储引擎层面将所有的记录加锁,然后由MySQL Server过滤,如果不满足会调用unlock_row把不满足条件的记录释放锁(这里违背了二段锁协议)。

这种情况同样适用于MySQL的默认隔离级别RR。所以对一个数据量很大的表做批量修改的时候,如果无法使用相应的索引,MySQL Server过滤数据的的时候特别慢,就会出现虽然没有修改某些行的数据,但是它们还是被锁住了的现象。

GAP锁(间隙锁)

是MySQL使用索引对行锁两边的区间进行加锁,避免其他事务在这两个区间insert的一种锁。


GAP锁示例


如图所示:数据库中存在值5,30。那么数据库会将数据段切分以下几个区间:
(negative infinity, 5],
(5,30],
(30,positive infinity)
当对值为30这一行加行锁的时候,会同时对(5,30]和(30,positive infinity)加GAP锁。这样其他事务如果想在这两个区间进行insert操作的时候,需要等待本次事务完成。

如果对不存在的数据进行更新,比如更新20(不存在)对应数据行,那么数据库也会对其存在的区间(5,30]加GAP锁。这样,如果有其他事务想插入值为10的数据,需要等待20这个事务完成。

如果使用的是没有索引的字段,那么会给全表加入GAP锁。

Next-Key锁

Next-Key锁是行锁和GAP锁的合并(MySQL使用它来避免幻读)

MVVC(多版本并发控制)

Innodb中的乐观锁实现。通过它提高MySQL的读取操作的性能。并能解决MySQL的重复读问题。
MVVC在每一行记录的后面加两个隐含列(记录创建版本号和删除版本号)。这里的版本号指的是事务的版本号(每个事务启动的时候,都有一个递增的版本号)。

  • 比如插入一条记录(事务id为1)
id name create_version delete_version
1 bird 1 -
  • 如果把这条记录name更新为dog
id name create_version delete_version
1 dog 2 -
1 bird 1 2

在更新操作的时候,采用的是先标记旧的那行记录为已删除,并且删除版本号是事务版本号,然后插入一行新的记录的方式。

  • 删除这条记录时
id name create_version delete_version
1 dog 2 3
1 bird 1 2

删除操作的时候,就把事务版本号作为删除版本号

  • 执行查询操作需要符合如下规则才能被查出来
    1) 删除版本号 大于 当前事务版本号,就是说删除操作是在当前事务启动之后做的。
    2) 创建版本号 小于或者等于 当前事务版本号 ,就是说记录创建是在事务中(等于的情况)或者事务启动之前。

在RR级别中,通过MVCC机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,是不及时的数据,不是数据库当前的数据!这在一些对于数据的时效特别敏感的业务中,就很可能出问题。
对于这种读取历史数据的方式,我们叫它快照读 (snapshot read),而读取数据库当前版本数据的方式,叫当前读 (current read)。很显然,在MVCC中:
快照读:就是select
>select * from table ....;
当前读:特殊的读操作,插入/更新/删除操作,属于当前读,处理的都是当前的数据,需要加锁。
>select * from table where ? lock in share mode;
>select * from table where ? for update;
>insert;
>update ;
>delete;

Innodb中事务隔离级别和锁的关系

Innodb通过使用不同的锁来实现事务隔离

避免脏读

通过对数据加行锁或则表锁,使对同一数据进行操作的事务处于等待状态,来避免同时操作

事务A 事务B
begin; begin;
update class_teacher set class_name='初三二班' where teacher_id=1; update class_teacher set class_name='初三三班' where teacher_id=1;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
commit;

避免不可重复读

通过MVVC实现事务的可重复读


避免幻读

通过Next-Key锁避免产生幻读现象
MySQL在RC和RR中都避免了幻读现象。

本文参考以下文章,并摘用部分内容(感谢各位作者):
数据库事务的四大特性以及事务的隔离级别
mysql 四种事物隔离级别的小总结
Innodb中的事务隔离级别和锁的关系