事务特性及隔离级别

一、事务特性

事务四大特性分别是原子性、一致性、隔离性、持久性。

A(Atomicity):原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。

C(Consistency):一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。举例来说,假设用户A和用户B两者的钱加起来一共是1000,那么不管A和B之间如何转账、转几次账,事务结束后两个用户的钱相加起来应该还得是1000,这就是事务的一致性。

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

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

二、隔离级别

1、为什么要设置隔离级别

设置隔离级别是为了在并发访问数据库时避免以下问题。

(1)、更新丢失(Lost update) 

如果多个线程操作,基于同一个查询结构对表中的记录进行修改,那么后修改的记录将会覆盖前面修改的记录,前面的修改就丢失掉了,这就叫做更新丢失。这是因为系统没有执行任何的锁操作,因此并发事务并没有被隔离开来。

第1类丢失更新:事务A撤销时,把已经提交的事务B的更新数据覆盖了。

时间

事务A

事务B

T1

开始事务

 

T2

 

开始事务

T3

查询余额为1000元

 

T4

 

查询余额为1000元

T5

 

转入100元修改余额为1100元

T6

 

提交事务

T7

取出100元将余额修改为900

 

T8

撤销事务

 

T9

余额恢复为1000元(丢失事务B的更新)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

第2类丢失更新:事务A覆盖事务B已经提交的数据,造成事务B所做的操作丢失

时间

事务A

事务B

T1

 

开始事务

T2

开始事务

 

T3

 

查询余额为1000元

T4

查询余额为1000元

 

T5

 

转入100元修改余额为1100元

T6

 

提交事务

T7

取出100元将余额修改为900元

 

T8

提交事务

 

T9

查询余额为900元(丢失事务B的更新)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

解决方法:对行加锁,只允许一个事务并发修改。或者version字段校验。

(2)、脏读(Dirty Read)

A事务读取B事务尚未提交的数据并在此基础上操作,而B事务执行回滚,那么A读取到的数据就是脏数据。

时间

事务A

事务B

T1

 

开始事务

T2

开始事务

 

T3

 

查询余额为1000元

T4

 

取出500元余额修改为500元

T5

查询账户余额500元(脏读)

 

T6

 

撤销事务余额恢复为1000元

T7

转入100元余额修改为600元

 

T8

提交事务

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

解决办法:如果在第一个事务提交前,任何其他事务不可读取其修改过的值,则可以避免该问题。

(3)、不可重复读(Non-repeatable Reads)

一个事务对同一行数据重复读取两次,但是却得到了不同的结果。事务T1读取某一数据后,事务T2对其做了修改,当事务T1再次读该数据时得到与前一次不同的值。

时间

事务A

事务B

T1

 

开始事务

T2

开始事务

 

T3

 

查询账户余额为1000元

T4

查询账户余额为1000元

 

T5

 

取出100元修改余额为900元

T6

 

提交事务

T7

查询账户余额为900元(不可重复读)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

解决办法:如果只有在修改事务完全提交之后才可以读取数据,则可以避免该问题。

(4)、幻读

指两次执行同一条 select 语句会出现不同的结果,第二次读的数据行数与第一次不一致,并没有说这两次执行是在同一个事务中。例如:目前工资为1000元的员工有10人。那么事务A中读取所有工资为1000元的员工,得到了10条记录;这时事务B向员工表插入了一条员工记录,工资也为1000元;那么事务A再次读取所有工资为1000元的员工共读取到了11条记录。

时间

事务A

事务B

T1

开始事务

 

T2

 

开始事务

T3

查询薪资为1000元的员工数为10个

 

T4

 

1000元薪资招聘了一个新员工

T5

 

提交事务

T6

查询薪资为1000元的员工数为11个

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

2、事务隔离级别

数据库事务的隔离级别有4个,由低到高依次为读未提交(Read uncommitted)、读已提交(Read committed)、可重复读(Repeatable read)、串行化(Serializable),这四个级别可以逐个解决脏读、不可重复读、幻象读这几类问题。

(1)、读未提交(Read uncommitted)

如果一个事务已经开始写数据,另外一个事务则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现。这样就避免了更新丢失,却可能出现脏读。也就是说事务B读取到了事务A未提交的数据。

(2)、读已提交(Read committed)

读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。该隔离级别避免了脏读,但是却可能出现不可重复读。事务A事先读取了数据,事务B紧接了更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。

(3)、可重复读(Repeatable read)

可重复读是指在一个事务内,多次读同一数据。在事务A还没有结束时,事务B也访问同一数据。那么,在事务A中的两次读数据之间,即使事务B对数据进行修改,事务A两次读到的的数据是一样的。这样就发生了在一个事务内两次读到的数据是一样的,因此称为是可重复读。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。这样避免了不可重复读取和脏读,但是有时可能出现幻象读。(读取数据的事务)这可以通过“共享读锁”和“排他写锁”实现。

(4)、串行化(Serializable)

提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行。如果仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。序列化是最高的事务隔离级别,同时代价也花费最高,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻读。

(5)、隔离级别总结

隔离级别

脏读

不可重复读

幻读

第一类丢失更新

第二类丢失更新

读未提交

允许

允许

允许

不允许

允许

读已提交

不允许

允许

允许

不允许

允许

可重复读

不允许

不允许

允许

不允许

不允许

串行化

不允许

不允许

不允许

不允许

不允许

 

 

 

 

 

 

 

 

 

 

 

隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。大多数数据库的默认级别就是Read committed,比如Sql Server , Oracle。MySQL的默认隔离级别就是Repeatable read。

三、悲观锁与乐观锁

1、悲观锁

悲观锁,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制。也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统的数据访问层中实现了加锁机制,也无法保证外部系统不会修改数据。

(1)、行锁与表锁

使用select ... for update会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认Row-Level Lock,所以只有明确地指定主键或者索引,MySQL 才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock (将整个数据表给锁住)。举例如下:

  • select * from t_items where id=1 for update;

这条语句明确指定主键(id=1),并且有此数据(id=1的数据)存在,则采用row lock。只锁定当前这条数据行。

  • select * from t_items where id=3 for update;

这条语句明确指定主键,但是却查无此数据,此时不会产生lock(没有元数据,又去lock谁呢?)。

  • select * from t_items where name='手机' for update;

这条语句没有指定数据的主键,name也不是索引,那么此时产生table lock,即在当前事务提交前整张数据表的所有字段将无法被查询。

  • select * from t_items where id>0 for update;或者select * from t_items where id<>1 for update;

上述两条语句的主键都不明确,也会产生table lock。

  • select * from t_items where status=1 for update;(假设status字段有索引)

这条语句明确指定了索引,并且有此数据,则产生row lock。

  • select * from t_items where status=3 for update;(假设status字段有索引)

这条语句明确指定索引,但是根据索引查无此数据,也就不会产生lock。

(2)、悲观锁小结

悲观锁并不是适用于任何场景,它也有它存在的一些不足,因为悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。如果加锁的时间过长,其他用户长时间无法访问,影响了程序的并发访问性,同时这样对数据库性能开销影响也很大,特别是对长事务而言,这样的开销往往无法承受。所以与悲观锁相对的,我们有了乐观锁。

2、乐观锁

乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以只会在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回用户错误的信息,让用户决定如何去做。实现乐观锁一般来说有以下2种方式:

(1)、版本号

使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。

(2)、时间戳

与第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。

posted @ 2020-12-07 17:18  西北-孤狼  阅读(166)  评论(0)    收藏  举报