ACID
一个数据库事务通常包含了一个序列的对数据库的读/写操作。它的存在包含有以下两个目的:
- 为数据库操作序列提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持一致性的方法。
- 当多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。
上面对数据库事务的定义摘自维基百科。先不用着急的去理解这个定义的具体含义,我们从事务的四个特性来逐步了解什么是事务。
数据库事务拥有以下四个特性,习惯上被称之为ACID特性。
原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。
隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。
用一个转账的例子来解释,这个例子被用烂了,却很经典:
从A账户向B账户转账100元,可能分为以下几个步骤:
读取A账户,将A账户余额减100
A 账户余额写回数据库
读取B账户,将B账户余额加100
B账户余额写回数据库
-
一致性
什么叫数据一致性?通常是由我们自己来定义的。在上面的场景中,就是在转账的前后,A账户和B账户的总额保持不变。
再举一个例子,之前做过一个版本管理系统,用户发布一个的版本(上传若干附件),版本记录表就要插入一行记录,相对应的,附件表也要插入若干的记录(一对多的关系)。对于这两个表的操作,要么全做,要么不做,如果附件表插入了记录,而版本记录表没有操作,不符合一致性定义;如果版本记录表插入了一条记录,而附件表没有插入记录或者插入记录少了几条,也不符合一致性的定义。想要保持一致性,那么需要对这两个表的插入操作都放在同一个事务内进行。
在事务处理的ACID属性中,一致性是最基本的属性,其它的三个属性都为了保证一致性而存在的。
-
原子性
上面的转账的四个步骤要么全做,要么不做。假如做完第一步之后,计算机突然断电了,那么数据库重启之后就需要执行一个crash
recovery的过程,之前的所有操作都应该回滚到执行事务之前的状态。即A向B转账的操作失败了。上面提到,其他三个属性都是为了保持一致性而存在。只要原子性是否就可以保证一致性?答案当然是否定的。
比如,事务1 A向B转账100元,在第一步执行完毕之后,恰好另外一个事务2操作是C向A转账200元,并且已经执行完毕,此时执行事务1的第二步,将A账户余额写回数据库,此时事务2的执行结果就被事务1覆盖掉了,造成了数据的不一致(A + B + C 的账户总额保持一致)。
可见,即使事务1最终执行完毕,满足了原子性,因为另一个事务的影响,还是造成了数据的不一致状态。原子性并不能保证一致性。
那么,为什么会看到网上还有许多人再问原子性和一致性的问题呢?
我认为是程序员很容易从数据库事务原子性联想到做应用时多线程并发时的原子性。多线程并发时的原子性基本靠锁来维持,我们认为,有了锁的保护,临界区的资源就不可以被另一个线程访问了。事实上,数据库事务原子性与锁关系不大,锁涉及到了事务的另一个特性:隔离性。
-
隔离性
就像在上面谈到的,事务1 与事务2 并行发生,造成了数据的不一致状态。隔离性用来解决这个问题。
事务隔离性可以保证:如果在A给B转账的同时,有另外一个事务执行了C给B转账的操作,那么当两个事务都结束的时候,B账户里面的钱应该是A转给B的钱加上C转给B的钱再加上自己原有的钱。
-
持久性
持久性比较容易理解。即,一旦事务提交(转账成功),所有的数据都会被写入数据库,落地到磁盘。账户中的钱就真的发生了变化。
事务隔离性
锁
从上文可以看出,当并发事务同时访问一个资源时,有可能导致数据不一致,因此需要一种机制来将数据访问顺序化,以保证数据库数据的一致性。锁就是其中的一种机制。我们通过使用锁来保证事务隔离性。
为了理解下面提到的隔离级别,我们简单认识一下数据库中的几种锁:
-
从锁粒度划分
表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
-
从锁性质划分
读锁(S 锁):如果事务A对数据T加了该锁之后,其他事务可以并发读取T(获取该数据的读锁),但任何事务都不能对数据T进行修改(获取数据上的写锁),直到已释放所有读锁。
写锁(X 锁):如果事务A对数据T加上写锁后,则其他事务不能再对T加任任何类型的锁。获得写锁的事务既能读数据,又能修改数据。
意向锁: 设计目的主要是为了在一个事务中揭示下一行将要被请求锁的类型。参考
更新锁:引入它是因为多数数据库在实现加X锁时是执行了如下流程:先加S锁,添加成功后尝试更换为X锁。这时如果有两个事务同时加了S锁,尝试换X锁,就会发生死锁。因此增加U锁,U锁代表有更新意向,只允许有一个事务拿到U锁,该事务在发生写后U锁变X锁,未写时看做S锁。
隔离级别
我们知道了并发事务的隔离性靠锁机制来实现,很多DBMS定义了多个不同的"事务隔离等级"来控制锁的程度和并发能力。
SQL定义的标准隔离级别有四种,从高到底依次为:
可序列化(Serializable)
可重复读(Repeatable reads)
提交读(Read committed)
未提交读(Read uncommitted)
随着数据库隔离级别的提高,数据的并发能力也会有所下降。
下面了解一下这几种隔离级别在数据库中可能的实现。注意,下面的实现都是基于传统数据库,而不是MVCC的。
未提交读
-
锁机制:
事务在读数据的时候并未对数据加锁;
事务在修改数据的时候对数据增加行级S锁。
举例:
Transaction 1 | Transaction 2 |
---|---|
select * from users where id = 1 // will read 20 | |
update users set age = 21 where id = 1 | |
select age from users where is = 1 // will read 21 | |
roll back |
事务一在读取某行数据的时候并未加任何锁,事务二也能对这行数据进行读取和更新;
事务二在更新某行数据的时候对这行数据加了S锁,事务一可以对这行数据进行读取,因此看到了事务二未提交的更改;
事务二更新某行数据对这行数据加了S锁,事务一不能对这行数据进行更新,直到事务二结束。
可以看到,事务一第二次查询看到了事务二未提交的更改,之后这些数据被事务二进行了回滚,于是事务一查询到的数据就成了脏数据,这种现象称之为脏读。
未提交读会造成脏读。
提交读
-
锁机制
事务对当前被读取的数据加行级S锁(读到时才加),一旦读完该行就释放S锁;
事务在更新某数据时,必须先对其加行级X锁,直到事务结束才释放。
举例
Transaction 1 | Transaction 2 |
---|---|
select * from users where id =1 //will read 20 | |
update users set age = 21 where id =1; commit | |
select * from users where id = 1 //will read 21 |
事务二在更新数据的时候对数据加了X锁,直到事务结束才释放。所以事务一读取不到事务二未提交的数据。
事务二结束后事务一读取到了与第一次读取中不一致的数据。造成了事务一中两次读取的结果不一致,产生了不可重复读问题。
可重复读
-
锁机制
事务对当前被读取的数据加行级S锁,直到事务结束才释放;
事务在更新某数据时,对其加行级X锁,直到事务结束才释放。
举例
Transaction 1 | Transaction 2 |
---|---|
select * from users where id =1; commit | |
update users set age = 21 where id =1; commit |
事务一在读取数据时,对数据加了S锁,直到事务结束才释放。因此在此期间,事务二只能读取该数据,不能更新。这样保证了事务一在整个事务期间,无论读取多少次该数据,结果都是一致的,解决了不可重复读的问题。
事务二在更新数据时对数据加了X锁,直到事务结束才释放,在此期间事务一都无法访问和更新该数据,解决了脏读的问题。
Transaction 1 | Transaction 2 |
---|---|
select * from users where age between 20 and 30 | |
insert into users values(3, 'bob', 25); commit | |
select * from users where age between 20 and 30 |
上面的例子中:
事务一查询年龄20到30之间的用户,假设取到10条数据,那么对这10条数据加上了行级S锁;
事务二插入一条数据。由于此时没有任何事务对表添加了表级锁,因此顺利插入;
事务一再一次查询年龄20到30之间的用户,发现与第一次读取时的数据不一致了,多出了一条数据。
这种现象就是幻读。这是一种特殊的不可重复读现象。
可序列化
-
锁机制
事务对当前被读取的数据加表级S锁,直到事务结束才释放;
事务在更新某数据时,对其加表级X锁,直到事务结束才释放。
事务一在读取表记录时,事务二也可以读取该表,但不能对表进行更新、删除、插入等操作;
事务一在更新表记录时,事务二不能够读取该表的任何记录,也不能对表进行更新操作。
可序列化隔离级别避免了脏读、不可重复读和幻读,是最高的隔离级别。