数据库事务的特性以及MySQL数据库的隔离级别
前言
了解数据库事务的特性,能够让我们更好的理解编程的时候对数据库事务的控制。本篇文章主要讲解了数据库事务的特性以及MySQL数据库在不同的事务隔离级别下遇到的不同情况。
数据库事务的概念
数据库事务是
访问并可能更新数据库中各个数据项的一个程序执行单元,通常包含对数据库进行读或者写的一个操作序列
。
一个典型的数据库事务如下:
begin transaction //事务开始
SQL1
SQL2
commit/rollback//事务结束或者回滚
数据库事务的特性
数据库事务的特性有四个,简称ACID,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。下面我们来说明一下这四个特性。
原子性(Atomicity)
原子性是指,同一个事务中的多个数据库的操作是一起完成的,要么所有的数据库操作都操作成功,如果有一个数据库的操作失败,则其他的操作也都会回滚。也就是说,要么全部成功,要么全部回滚。
一致性(Consistency)
一致性是指,一个事务或者多个事务在开始前的数据的状态和结束后的数据的状态要保持一致。这里的一致性有点抽象,我们举个例子来说明:假设在数据库中库存表中的某商品的库存总量为10,购买量为0,剩余量为10。现在有两个用户购买了1件该商品,那么在这两个用户购买完成后,该商品的购买量应该为2,剩余量应该为8,它们的总和等于库存量10,而不应该为其他的数值。
隔离性(Isolation)
隔离性是指,不同事务之间,他们是互相独立的,不会相互的干扰。就是说,一个事务里对某一数据进行操作,不会影响其他事务对同样数据的操作。他们操作完成以后对数据库的影响和他们串行执行时的结果一样
。
持久性(Durability)
持久性是指,一个事务一旦提交成功,它对数据库的修改是持久的。任何事务或者系统故障都不会导致数据的丢失。
事务并发会产生的异常
事务的四大特性,可以很好的规范程序员根据不同的业务场景以及数据库不同的隔离级别,对数据库的事务进行合理的操作,从而来避免一些多个事务并发执行时所产生的异常情况。事务并发执行时,产生以下几种异常情况。
丢失更新
这里我们来介绍一下丢失更新这种异常现象。丢失更新是指如果多个线程(事务)进行操作,
基于同一个查询结构(也就是说它们刚开始读取到的数据都是一样的)
对表中的数据进行修改,那么后修改的记录会覆盖前面修改的记录,前面修改的就丢失掉了。
具体的分可以分为两种丢失更新:一种是回滚丢失更新,一种是覆盖丢失更新。
回滚丢失更新
假设有两个事务,事务A在T1时间点(之后的时间随着数字的增大而增大),开始了事务,事务B在T2时间开启了事务。事务A在T3时间读取了账户余额为1000元,事务B在T4时间读取到了账户余额为1000元,然后事务B在T5时间汇入了100元,此时账户余额为1100元,事务B在T6时刻提交了事务。事务A在T7的时刻取出了100元,余额变为了900元,在T8时刻撤销了事务,事务A在T9时刻恢复了原来的余额1000元。那么此时,由于事务A的撤销回滚操作使事务B提交1100元的操作丢失,就叫做回滚丢失更新。
这里要注意了,由于SQL92没有定义这种现象,标准定义的所有隔离级别中都不允许回滚丢失的发生
。
覆盖更新操作
假设有两个事务,事务B在T1时刻开启了事务,事务A在T2时刻开启了事务。事务B在T3时刻读取了余额1000元,事务A在T4时刻读取了余额1000元。事务B在T5时刻取出100元,余额变为900元,在T6时刻提交事务。事务A在T7时刻汇入100元,余额变为1100元,在T8时刻提交事务。那么此时余额最终为1100元,而事务B取出100元的事务操作被覆盖掉了,这种就被称作覆盖丢失更新。
下图是两种丢失更新操作的时间流程图
那么如何来解决更新丢失的情况呢?
丢失更新的解决方案
解决丢失更新有两种解决方案:一种是悲观锁机制
,一种是乐观锁机制
。
悲观锁机制
悲观锁机制是指这种丢失更新的发生概率很高,最好一开始就加上锁,免得更新时出错。
悲观锁有两种实现方式:添加共享锁方式
和添加排它锁方式
。
共享锁方式是指当事务A使用了共享锁后,其他的事务对该表只能进行读操作而不能进行写操作。排它锁方式是指当事务A使用了排它锁,其他的事务必须等待事务A执行完毕后才能进行操作。
共享锁方式代码:
select * from tablename lock in share mode;
排它锁方式代码:
select * from tablename for update;
乐观锁机制
乐观锁机制是指假设发生这种问题的概率很小,最后一步做更新的时候再锁住,免得锁住时间太长影响其他的事务操作。
乐观锁的实现一般是在数据库表中增加一个timestamp字段,并将其设置为只要该表进行插入或修改操作时都会更新该字段为最新时间。当其他线程进行修改的时候通过检查timestamp是否改变,从而判断当前更新基于的查询是否已是过时的版本,如果过时,则更新操作失败。
。
这里我们来举个例子说明一下:
这四种异常会因为数据库的隔离级别不同而产生,接下来我们先来讲一下MySQL数据的四种隔离级别。
MySQL数据库的四个隔离级别
MySQL数据库的四个隔离级别从低到高依次为:1.
读未提交(read uncommitted)
;2.读已提交(read committed)
;3.可重复读(repeatable read)--MySQL默认的隔离级别
;4.串行化(serializable)
。
以上四种隔离级别,分别会产生的异常情况如下图所示:
读未提交(read uncommitted)
如果有两个事务,都在操作同一条数据的一个整型字段paramA的值(假设两个事务没操作之前值为0),其中一个事务A正在读取变量paramA的值的时候,而另一个事务B更改了变量paramA的值(更改为1),但是还没有提交paramA的值,那么此时事务A读取到的paramA的值为1并且之后进行了paramA+2运算操作后paramA的值变为了3最后提交了事务,那么此时事务A得到的paramA的值为3。而此时B事务由于由于某种原因回滚了事务,但是事务A这里已经提交并读取了B事务更改过的paramA的脏数据,这里就发生了脏读,如果事务B提交失败的话,那么事务A拿到的变量paramA的值与数据库本来的值是不一致的。此级别同样会产生幻读和不可重复读的现象(同一事务不同时间读取的记录条数不一致)。
读已提交(read committed)
如果有两个事务,都在操作同一条数据的一个整型字段paramA的值(假设两个事务没操作之前值为0),其中一个事务A正在读取paramA的值的时候,而另一个事务B更改了paramA的值(更改为1),但是还没有提交paramA的值,那么此时事务A读取到的paramA的值为0;如果事务A在读取变量paramA的值时,事务B已经更改了paramA的值为1,并且提交了事务,那么事务A读取到的值就为1。这个隔离级别会导致同一事务在多次读取一条记录的某个值时可能会读取不同的结果,也即不可重复读的情况;当然,也会产生幻读的现象。
可重复读(repeatable read)
还是上述场景,如果事务A在第一次读取paramA的时候,事务B没有更改paramA的值,那么事务A读取到的paramA的值为0,在事务A第二次读取paramA的时候,事务B更改了paramA的值并提交了事务,则事务A第二次读取到的paramA的值仍为0。此隔离级别为MySQL数据库的默认隔离界别,但这种隔离级别也会产生幻读。
串行化(serializable)
仍以上述两个事务为例,此时如果事务A读取某一表的一条数据中字段paramA的值(此时为0),此时如果事务B如果想修改该表中该记录字段paramA的值时,则修改不了,处于等待状态;但是事务B如果是读取字段paramA的值则允许读取。如果事务A在更改该表该记录的字段paramA的值,则事务B不能做任何操作(读取、增加、修改、删除)的操作,只有等事务A结束后才允许进行操作。这种隔离级别容易产生超时和锁竞争情况。