mysql 数据库的四种隔离级别

mysql 数据库的四种隔离级别

1、四种隔离级别

1.1、四种隔离级别

  • 读未提交(read uncommitted)
    可以从字面意思理解为,读取到未提交的数据。如果在系统中读取到未提交的数据,会出现什么问题?首先,系统读取到未提交的数据之后,修改的数据的信息,进行了提交。这时候就会产生脏读的情况,出现这种问题,系统的数据肯定是不能正常使用了。
  • 读已提交(read committed)
    从字面理解就是读取已提交的数据,正好是解决上面的问题,避免脏读的情况发生,你读取已经提交的数据,肯定是事物已经结束,这条数据可以正常操作了。但是也会出现一种情况,就是我在这个数据提交之前读取过一次数据,然后在提交之后又读取一次数据,这两次读取的结果肯定是出现比较大的差异。这种情况是“不可重复读”。这种情况一般经常会出现在,并发场景或者是前后操作时间比较相近,或者是系统之间的前后调用等。考虑到系统的安全性,这种情况肯定也需要避免掉
  • 可重复读(repeatable read)|mysql系统默认的事物隔离级别
    这个意思就是可重复读取数据,但是提交事物之后才能正常读取数据。如果A、B都开启事物对表a进行操作的话,则需要A、B都提交之后才能正常访问a表的数据,这个时候也会出现一个问题,就是B修改的数据提交之后,A也没有办法看到,等到A提交之后,重新查询了这一行数据发现和A原来修改的数据不一致。这种情况被称为幻读,如果出现幻读的情况的,很简单一个问题跟不可重复读一样,就是我在操作的时候读取到的数据信息不一致,导致修改数据出现问题。
  • 串行化(serializable)
    这个是为了解决幻读问题。串行化读取的时候,是不允许第二个用户进行操作的,如果有第二个用户进行操作,则对该用户进行等待操作,只有在第一个用户执行完成之后才能执行第二个用户执行的操作,可见这个事物虽然隔离级别上能保证数据的一致性,但是执行效率很低。

1.2、什么是脏读、不可重复读和幻读?

mysql数据库默认的隔离级别是可重复读,但是这种事物控制不能解决幻读的问题,下面我们来看一下隔离级别支持情况:

  • 脏读:读取到另一个事务未提交的数据
  • 不可重复读:在同一个事务内多次读取同一行数据,读取的结果不同
  • 幻读:在同一事务内操作的数据,在进行查询的时候,发现跟原来操作的数据不一致。实际上幻读比较难理解,什么是幻读。我们那人的幻觉来说明,比如一个人出现幻觉了,是在什么情况会出现幻觉:我们看到的东西跟现实的场景不符合肯定是出现幻觉了,比如说现实生活中我看到了一把椅子,椅子上放了一条毛巾,但是真实情况是椅子上什么都没有,这个时候我们有可能就出现了幻觉,拿这个事情类比一下幻读。我们读取的信息跟实际的信息不符合就是幻读对不对。就是本来存在,但是我们没读到,或者是本来不存在,但是我们读到了,都会产生幻读的情况。
脏读 不可重复读 幻读
读未提交
读已提交
可重复读
串行化

2、事务

事务的四个特性:ACID(原子性、一致性、隔离性和持久性)

  • A,atomicity 原子性:原子性是不可分割的,是一个整体。在对事务进行操作的时候,只能维持事务唯一的特性进行操作,不可能出现同时操作多个相同事务的情况。
  • C,consistency 一致性:我首先要保证原子性的同时,还要保证事务的一致性。在进行事物操作的时候,已经对事务的数据进行修改提交的情况下,在进行事物操作的时候,是不能在修改之前未提交的数据的。只能修改我提交之后的数据。这样才能保证我的数据是经过A修改之后B才进行修改的,这就保证的数据的一致性。
    一致性分为三类:
    • 强一致性:在各个环节,数据复制是同步的,这个就是要在各个环节都要保证数据的一致性。一般系统操作,都分为,数据库,缓存,接口、web端等,都会有数据的展示,强一致性就体现为,在各个端都要保证数据是一致的才能满足强一致性,但是这个有点困难
    • 弱一致性:在各个环节,数据复制是异步的,在数据同步这个过程中,进行异步操作,比如,缓存数据跟数据库数据不一致,我们可以在进行数据库操作的时候,同时更新缓存数据。数据库主从设计中,数据同步也是通过异步完成的。
    • 最终一致性:在不保证各个环节的数据一致的前提下,对最终的数据保持一致。这种情况比较类似于弱一致性。比如数据同步,A数据库同步B数据库,不保证当时时间点的数据一致,但是在经过数据同步之后,A数据库和B数据库能保证数据的一致性。
  • I,Isolation 隔离性:在对数据进行操作的时候,首先A事务操作不能影响B事务的操作,上面我们讲到了事物的四种隔离级别,就是讲的隔离性,通过不同的隔离级别达到事务的不影响操作,来保证数据的原子性及最终一致性
  • D,Durability 持久性:在操作完数据之后,不会因为其他因素而丢失数据。比如说,我往数据库插入一条已提交的数据,但是因为数据库异常导致数据丢失,这种情况就不是的持久性。

3、Mysql如何设置事务

  • 查看当前会话隔离级别:select @@tx_isolation
  • 查看系统隔离级别:select @@global.tx_isolation
  • 设置当前会话的隔离级别:set session transaction isolation level xxx
  • 设置系统会话的隔离级别: set global transaction isolation level xxx
设置 描述
Serializable 可避免脏读、不可重复读、虚读情况的发生。(串行化)
Repeatable read 可避免脏读、不可重复读情况的发生。(可重复读)
Read committed 可避免脏读情况发生(读已提交)。
Read uncommitted 最低级别,以上情况均无法保证。(读未提交)

4、java在事务中的应用

在java的 Connection 方法,比如 setAutoCommit 或 setTransactionIsolation:

  • TRANSACTION_SERIALIZABLE指示不可以发生脏读、不可重复读和幻读的常量。
  • TRANSACTION_REPEATABLE_READ指示不可以发生脏读和不可重复读的常量;幻读可以发生。
  • TRANSACTION_READ_UNCOMMITTED指示可以发生脏读 、不可重复读和幻读的常量。
  • TRANSACTION_READ_COMMITTED指示不可以发生脏读的常量;不可重复读和幻读可以发生。
    通过对java JDBC的connection的设置,可以发现java同样是有4种隔离级别,对应数据库的四种隔离级别。

5、隔离级别的底层实现

5.1、mysql底层实现隔离级别的版本链控制,既MVCC,什么是MVCC呢?

MVCC(multi-version-concurrent-control):MVCC即多版本并发控制,MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
MVCC就是由于事务的并发与隔离级别的存在,导致脏读,不可重复度,幻读等问题的一种解决策略。就是在操作数据的时候,比如进行增删改查的时候,mysql会在日志中把这个操作记录下来,同时生成版本信息,第一次操作版本为1,如果操作次数比较多,就会产生多个版本信息。然后这条记录还会在原来的数据基础上增加trx_id与db_roll_ptr,用来记录当前事务的id和上一条操作记录。当对同一条数据进行多次修改之后,这个日志就会形成一条日志链,而隔离级别的实现就是根据条件,读取某个对应版本的数据。它的实现原理主要是依赖记录中的隐式字段、undo日志、read view 来实现的。

隐式字段包括:

  • DB_ROW_ID:6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID生成一个聚簇索引。
  • DB_TRX_ID:6byte,最近修改(修改、插入)事务ID:记录创建这条记录以及最后一次修改该记录的事务的ID,是一个指针。
  • DB_ROLL_PTR:7byte,回滚指针,指向这条记录的上一个版本(上一个版本存储于,rollback segment里)。
  • DELETED_BIT:1byte,记录被更新或删除并不代表真的删除,而是删除flag变了,相当于记录一次逻辑删除。

快照读和当前读

  • 当前读 : 读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。如共享锁select for share, 排他锁select for update,update,insert,delete等操作。
  • 快照读 : 不加锁的简单select都属于快照读,即非阻塞读。它是基于mvcc的,所以读取到的不一定是数据的最新版本,可能是之前的历史版本。快照读的前提是隔离级别不是串行级别,在串行级别下的快照会退化成当前读。

undo日志:

InnoDB把这些为了回滚而记录的这些东西称之为 undo log。值得注意的是,由于查询操作(SELECT)并不会修改任何用户记录,所以在查询操作时,并不需要记录相应的 undo log。

undo log 主要分为以下三种:

  • insert undo log:插入一条记录时,至少把这条记录的主键记录下来,之后回滚的时候只需要把主键对应的记录删除即可。
  • update undo log:修改一条记录时,至少要把修改这条记录前的旧值都记录下来,在回滚的时候再把这条记录的值更新为旧值就好了。
  • delete undo log:删除一条记录时,至少要把这条记录中的全部内容都记录下来,这样在之后回滚的时候再重新将这些内容组成的记录插入到表中就好了。
    • 删除操作都只是设置一下老记录的 DELETE_BIT,并不是真正将其删除,类似于数据库提供的专门的逻辑删除。为了节省磁盘空间,InnoDB有专门的 purge(清除)线程来清理 DELETED_BIT 为 true 的记录。为了不影响MVCC的正常工作,purge线程自己也维护了一个 read view(这个 read view 相当于当前系统中最老活跃的事务的 read view)。如果某个记录的 DELETED_BIT 为 true,并且 DB_TRX_ID(最后一个操作的事务ID) 相对于 purge线程的 read view 可见,那么这条记录一定是可以被安全清除的。

对 MVCC 有实质上帮助的是 update undo log,undo log 实际上就是存在于 rollback segment 中的旧纪录链。

readview:

ReadView就是事务在使用mvcc进行快照读操作时产生的读视图。该视图记录了一些信息去保证在后续判断中该读到那个版本的数据。

ReadView中主要包含4个比较重要的内容:

  • creator_trx_id 创建该ReadView的事务id(只有在对表中记录改动时,才会为该事务分配id)
  • trx_ids 在生成ReadView时当前系统中活跃的读写事务id列表
  • up_limit_id 活跃的事务中最小事务id
  • low_limit_id 系统中最大的事务id+1

当生成了ReadView后,在通过mvcc访问某条记录时,按下边步骤来判断记录的那个版本是可见的:

  • 如果被访问版本的trx_id和ReadView的creator_trx_id相同,意味当前事务在访问自己修改过的记录,所以当前版本可以被事务访问。
  • 如果被访问版本的trx_id小于ReadView的up_limit_id,表明生成该版本的事务在当前事务之前就已经提交了,所以该版本可以看到。
  • 如果被访问版本的trx_id大于ReadView的low_limit_id,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被访问。
  • 如果被访问版本的trx_id在up_limit_id 和low_limit_id之间,那么就要判断在不在trx_ids中。如果在,就说明该版本的事务还是活跃的不可被访问。如果不在,说明该版本的事务已经被提交,记录可以被访问。

当不能访问当前版本的数据时,就会顺着版本链找到历史版本进行判断。如果最后一个版本也不可见,那么查询结果就查不出该记录。

5.2、隔离级别的实现

  • 读未提交的实现:直接读取数据版本中最新的一条数据,这种情况肯定会产生脏读、不可重复读和幻读的情况
  • 读已提交的实现:利用快照读,只能读到相对当前快照可见的版本,由于快照是每一次读的时候都会重新生成,因此每一次相对于快照的版本都不同,也就有可能出现幻读及不可重复读的情况
    • 快照读:每一次读取的时候都会生成一次ReadVIew快照(RC和RR级别的),该快照详细记录了当前活跃的读写事务(没有提交的事务),然后读取数据时,根据ReadView里面记录的事务id(可能有多个),在版本链中找到可见的版本,返回数据记录。如果最新版本不可见,则根据 db_roll_ptr一直向前找,直到找到可见的版本
    • 版本相对与当前快照可见与不可见的条件:
      • 版本记录中的trx_id小于ReadView中最小的事务id时可见:因为生成该版本记录的事务在生成当前快照前就已经被提交了,因此该版本记录是当前可见的
      • 版本记录中的trx_id大于ReadView中最大的事务id时不可见:因为生成该版本记录的事务在当前快照生成之后启动的,因此该版本记录是相对于当前快照不可见的
      • 版本记录中的trx_id界与ReadView最小事务id与最大事务id之间:需要判断一下版本记录中trx_id是否在ReadView中,如果在,则表示生成该记录的事务还活跃(未提交),则不可见;反之则表示事务已经被提交,记录相对于当前快照是可见的
  • 可重复读:与读已提交不同的是,可重复读只在事务开始后第一次读的时候生成快照,因此在读已提交隔离级别的基础上,解决了不可重复读与幻读的问题
  • 串行化隔离级别的实现:直接使用加锁的方式来访问记录。隔离级别最高,什么问题也不会出现,但是代价就是性能差

5.3、关于读取数据的设计

实际上mysql的select读有两种类型,分别是快照读与当前读。默认是快照读。快照读通过设置可重复读隔离级别即可解决幻读问题。当前读是一种对记录进行加锁的读类型。该锁分为读锁与写锁

  • 读锁是共享锁,用来读取数据。一个事务获取了读锁,则其他可以共享该读锁,但是不可以再获取写锁
    获取读锁的读语句:select * from xxx lock in share mode;
  • 写锁是排他锁,用来写数据。一个事务获取了写锁,则其他事务不可以共享该写锁,同时也不可以获取该记录的读锁
    获取写锁的读语句:select * from xxx for update

简而言之:读读共享,读写互斥,写写互斥。另外,当前读因为获取了记录的锁,一样不会出现幻读问题

  • 读-读:不存在任何问题,也不需要并发控制。
  • 读-写:有线程安全问题,可能会造成事务隔离性问题,也就是可能遇到,脏读,不可重复读,幻读等。
  • 写-写:有线程安全问题,可能会存在更新丢失问题。

注意:两个事务是不可以同时update的,因为一执行update语句,就会获取该数据的写锁,直到事务被提交才会释放锁(如果不是事务操作,update语句执行完就会释放锁)。但是另外一个事务是可以通过快照读类型读数据的!(不获取读锁或者写锁就行)

你可能感兴趣的:(mysql,数据操作,mysqldb,mysql,MVCC)