以前对于数据库事务概念的理解有不少不到位的地方,今天用简单的实例再来阐述一下数据库事务和隔离级别的概念,也方便之后温故而知新。mysql
事务(Transaction)是并发控制的基本单位。所谓的事务,它是一个操做序列,这些操做要么都执行,要么都不执行,它是一个不可分割的工做单位。例如,银行转帐工做:从一个帐号扣款并使另外一个帐号增款,这两个操做要么都执行,要么都不执行。因此,应该把它们当作一个事务。事务是数据库维护数据一致性的单位,在每一个事务结束时,都能保 持数据一致性。
咱们以Msql数据库的操做为例,再进一步解释一下数据库事务: 首先咱们用如下命令查看该Mysql会话的事务隔离级别,关于事务隔离级别及其做用,咱们在后面的章节中会进行详细介绍,这里只要简单知道数据库能够设置不一样的事务隔离级别,不一样的隔离级别会对事务的操做产生不一样的效果便可。使用如下命令能够查询当前Mysql会话的事务隔离级别,能够看到,Mysql默认的事务隔离级别是REPEATABLE-READ。sql
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
复制代码
为了用实例来解释事务,咱们建立了以下的bank数据表,并插入一条数据,数据库
mysql> describe bank;
+---------+---------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------+---------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | varchar(40) | NO | | NULL | |
| balance | decimal(10,2) | YES | | NULL | |
+---------+---------------+------+-----+---------+----------------+
mysql> select * from bank;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 3 | fufu | 2000.00 |
+----+------+---------+
复制代码
使用start transaction命令开启数据库事务,安全
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
复制代码
更新id为3的行的balance值为3000.00,bash
mysql> update bank set balance = 3000 where id = 3; Query OK, 1 row affected (0.09 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> select * from bank; +----+------+---------+ | id | name | balance | +----+------+---------+ | 3 | fufu | 3000.00 | +----+------+---------+ 1 row in set (0.00 sec) 复制代码
此时咱们能够看到,select语句查询到的id为3的行的balance值已经修改成3000.00,接下来咱们再尝试插入一条新数据,markdown
mysql> insert into bank (name, balance) values ('melo', 1000); Query OK, 1 row affected (0.06 sec) mysql> select * from bank; +----+------+---------+ | id | name | balance | +----+------+---------+ | 3 | fufu | 3000.00 | | 4 | melo | 1000.00 | +----+------+---------+ 2 rows in set (0.00 sec) 复制代码
因为以上的update和insert操做都是在start transaction命令开启事务以后,因此直到事务结束,这些操做都属于同一事务,假设咱们在insert操做时产生了错误,能够根据事务的定义得知,这些属于同一事务的全部操做要么都执行要么都不执行,咱们能够验证一下,使用rollback命令,模拟事务失败回滚,并发
mysql> rollback;
Query OK, 0 rows affected (0.01 sec)
复制代码
此时咱们在查询数据库中的全部数据,发现数据恢复到了update命令执行前的状态,id为3的行的balance值等于2000没有变化。性能
mysql> select * from bank; +----+------+---------+ | id | name | balance | +----+------+---------+ | 3 | fufu | 2000.00 | +----+------+---------+ 1 row in set (0.00 sec) 复制代码
到此,咱们阐述了数据库事务的定义并用简单的Mysql操做说明了事务的操做方式,咱们能够总结出数据库事务的生命周期以下:
spa
如今咱们回过头来思考一下上述示例,示例中的全部操做都是在一个Mysql会话中进行的,也就是没有其余用户在同时链接数据库进行操做,在这种没有并发会话的使用场景中,不管事务是正常结束仍是异常结束,对于该单独用户读取数据不会形成任何影响,由于他的全部操做都是串行的。可是在实际应用场景中,数据库每时每刻都服务于不少会话,假设用户A的事务A开始后更新了数据库数据,此时用户B开始读取该数据,用户B将会读取到了新的值。可是若是紧接着事务A在下一条SQL语句操做时产生了错误,将事务A回滚了,那么用户B读取到的数据就是错误的无效数据了。这只是数据库事务在并发环境下会产生的一个简单的问题,因此接下来详细阐述并发事务会产生的问题。日志
这节咱们主要说明并发事务时可能会出现的问题,咱们用时间点和事务操做表格的方式来举例。
定义:A事务撤销时,把已经提交的B事务的更新数据覆盖了。
时间点 | 事务A | 事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询帐户余额为1000元 | |
T4 | 查询帐户余额为1000元 | |
T5 | 存入100元把余额改成1100元 | |
T6 | 提交事务 | |
T7 | 取出100元把余额改成900元 | |
T8 | 撤销事务 | |
T9 | 余额恢复为1000元(丢失更新) |
以上的示例演示了第一类丢失更新问题,事务B虽然成功了,可是它所作的更新没有被永久存储,这种并发问题是因为彻底没有隔离事务形成的。当两个事务更新相同的数据时,若是一个事务被提交,另外一个事务却撤销,那么会连同第一个事务所作的更新也被撤销了。(这是绝对避免出现的事情) 事务A的开始时间和结束时间包含事务B的开始和结束时间,事务A回滚事务的同时,把B的已经提交的事务也回滚的,这是避免的,这就是第一类丢失更新.
定义:A事务提交时,把已经提交的B事务的更新数据覆盖了。
时间点 | 事务A | 事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询帐户余额为1000元 | |
T4 | 查询帐户余额为1000元 | |
T5 | 取出100元把余额改成900元 | |
T6 | 提交事务 | |
T7 | 存入100元把余额改成1100 | |
T8 | 提交事务 | |
T9 | 余额恢复为1100元(丢失更新) |
第二类丢失更新和第一类的区别其实是对数据的影响是由A事务的撤销仍是提交形成的,它和不可重复读(下面介绍)本质上是同一类并发问题,一般把它看作是不可重复读的一个特例。两个或多个事务查询同一数据。而后都基于本身的查询结果更新数据,这时会形成最后一个提交的更新事务,将覆盖其它已经提交的更新事务。
定义:读到未提交更新的数据
时间点 | 事务A | 事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询帐户余额为1000元 | |
T4 | 取出500元把余额改成500元 | |
T5 | 查询帐户余额为500元(脏读) | |
T6 | 撤销事务,余额恢复为1000元 | |
T7 | 存入100元把余额改成600元 | |
T8 | 提交事务 |
A事务查询到了B事务未提交的更新数据,A事务依据这个查询结果继续执行相关操做。可是接着B事务撤销了所作的更新,这会致使A事务操做的是脏数据,以上的示例中T5时刻产生了脏读,最终致使A事务提交时帐户余额的不正确,可能有人会有疑问,B事务尚未提交或撤销,T5时刻A事务为何能读到已经改变的数据,这里要说的是,数据表中的数据是实时改变的,事务只是控制数据的最终状态,也就是说若是没有正确的隔离级别,在更新操做语句结束后,即便事务未完成,其余事务就已经能够读取到改变的数据值了。
如今为止:全部的数据库都避免脏读操,能够用两个Mysql会话试验一下以上的操做,在默认的隔离级别下(REPEATABLE-READ),A事务在T5时刻读取到的余额为1000元,不会是500元。
定义:读到已经提交更新的数据,但一个事务范围内两个相同的查询却返回了不一样数据。
时间点 | 事务A | 事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询帐户余额为1000元 | |
T4 | 查询帐户余额为1000元 | |
T5 | 取出100元把余额改成900元 | |
T6 | 提交事务 | |
T7 | 查询帐户余额为900元(与T4读取的一不一致,不可重复读) |
定义:读到已提交插入数据,幻读与不可重复读相似,幻读是查询到了另外一个事务已提交的新插入数据,而不可重复读是查询到了另外一个事务已提交的更新数据。
时间点 | 事务A | 事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 统计用户Z总存款数为1000元 | |
T4 | 新增Z的一个存款帐号,存款100元 | |
T5 | 提交事务 | |
T6 | ||
T7 | 再次统计用户Z总存款数为1100元(与T4读取的一不一致,幻读) |
A事务第一次查询时,没有问题,第二次查询时查到了B事务已提交的新插入数据,这致使两次查询结果不一样。
不可重复读和幻读的区别: 简单来讲,不可重复读是因为数据修改引发的,幻读是由数据插入或者删除引发的。
不可重复读,是指在数据库访问中,一个事务范围内两个相同的查询却返回了不一样数据。这是因为查询时系统中其余事务修改的提交而引发的。好比事务T1读取某一数据,事务T2读取并修改了该数据,T1为了对读取值进行检验而再次读取该数据,便获得了不一样的结果。
一种更易理解的说法是:在一个事务内,屡次读同一个数据。在这个事务尚未结束时,另外一个事务也访问该同一数据。那么,在第一个事务的两次读数据之间。因为第二个事务的修改,那么第一个事务读到的数据可能不同,这样就发生了在一个事务内两次读到的数据是不同的,所以称为不可重复读,即原始读取不可重复。
所谓幻读,是指事务A读取与搜索条件相匹配的若干行。事务B以插入或删除行等方式来修改事务A的结果集,而后再提交。
幻读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,好比这种修改涉及到表中的“所有数据行”。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。那么,之后就会发生操做第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉同样.通常解决幻读的方法是增长范围锁RangeS,锁定检锁范围为只读,这样就避免了幻读。
以上就是数据库并发事务致使的五大问题,总结来讲其中两类是更新问题,三类是读问题,数据库是如何避免这种并发事务问题的呢?答案就是经过不一样的事务隔离级别,在不一样的隔离级别下,并发事务读取数据的结果是不同的,好比在脏读小节里介绍的,若是是在REPEATABLE-READ隔离级别下,A事务在T5时刻读取是读取不到B事务未提交的数据的。咱们须要根据业务的要求,设置不一样的隔离级别,在效率和数据安全性中找到平衡点。
SQL标准定义了4类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级通常支持更高的并发处理,并拥有更低的系统开销。
当数据库系统使用SERIALIZABLE隔离级别时,一个事务在执行过程当中彻底看不到其余事务对数据库所作的更新。当两个事务同时操做数据库中相同数据时,若是第一个事务已经在访问该数据,第二个事务只能停下来等待,必须等到第一个事务结束后才能恢复运行。所以这两个事务其实是串行化方式运行。
当数据库系统使用REPEATABLE READ隔离级别时,一个事务在执行过程当中能够看到其余事务已经提交的新插入的记录,可是不能看到其余事务对已有记录的更新。
当数据库系统使用READ COMMITTED隔离级别时,一个事务在执行过程当中能够看到其余事务已经提交的新插入的记录,并且还能看到其余事务已经提交的对已有记录的更新。
当数据库系统使用READ UNCOMMITTED隔离级别时,一个事务在执行过程当中能够看到其余事务没有提交的新插入的记录,并且还能看到其余事务没有提交的对已有记录的更新。
以上的四种隔离级别按从高到底排序,你可能会说,选择SERIALIZABLE,由于它最安全!没错,它是最安全,但它也是最慢的!四种隔离级别的安全性与性能成反比!最安全的性能最差,最不安全的性能最好!
经过以上的四种隔离级别的定义,咱们已经能够分析出,每一个隔离级别能够避免哪些并发问题了,总结一下以下表:
隔离级别 | 第一类丢失更新 | 第二类丢失更新 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|---|
SERIALIZABLE (串行化) | 避免 | 避免 | 避免 | 避免 | 避免 |
REPEATABLE READ(可重复读) | 避免 | 避免 | 避免 | 避免 | 容许 |
READ COMMITTED (读已提交) | 避免 | 容许 | 避免 | 容许 | 容许 |
READ UNCOMMITTED(读未提交) | 避免 | 容许 | 容许 | 容许 | 容许 |
咱们经过隔离级别的定义很容易本身分析出这张表,好比可重复读隔离级别的定义是一个事务在执行过程当中能够看到其余事务已经提交的新插入的记录,可是不能看到其余事务对已有记录的更新。因此,在这种隔离级别下,在脏读示例的T5时刻和不可重复读的T7时刻,事务A都是不管事务B是否提交,事务A都是没法读取到事务B对已有记录的更新的,因此不会产生脏读和不可重复读,而又因为这种隔离级别下能够看到其余事务已经提交的新插入记录,天然是没法避免幻读的产生。另外,值得注意的是全部隔离级别均可以免第一类丢失更新的问题。 大多数关系数据库默认使用Read committed的隔离级别,Mysql InnoDB默认使用Read repeatable的隔离级别,这和Mysql replication 机制使用Statement日志格式有关。各数据库隔离级别的实现也是有差异的,例如Oracle支持Read committed 和Serializable两种隔离级别,另外能够经过使用读快照在Read committed级别上禁止不可重复读问题;MySQL默认采用RR隔离级别,SQL标准是要求RR解决不可重复读的问题,可是由于MySQL采用了gap lock,因此实际上MySQL的RR隔离级别也解决了幻读的问题,也就是Mysql InnoDB在Read repeatable级别上使用next-key locking 策略来避免幻读现象的产生。