数据库事务

1、什么是数据库事务

(1)使用场景

  比如下单,会操作订单表,资金表,物流表等等,这个时候需要让这些操作都在一个事务里面完成。当一个业务流程涉及多个表的操作的时候,我们希望它们要么是全部成功的,要么都不成功,这个时候就会启用事务。又比如行内转账的这种操作,如果把它简单地理解为一个账户的余额增加,另一个账户的余额减少的情况(当然实际上要比这复杂),那么这两个动作一定是同时成功或者同时失败的,否则就会造成银行的会计科目不平衡。

  可以通过注解或是配置切面的方式开启事务。

(2)事务的定义

  维基百科的定义:事务是数据库管理系统(DBMS)执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。这里面有两个关键点,第一个,它是数据库最小的工作单元,是不可以再分的。第二个,它可能包含了一个或者一系列的DML语句,包括insert delete update。

2、事务的四大特性——ACID

(1)原子性,Atomicity。意味着对数据库的一系列操作要么全部成功,要么全部失败。在Innodb中,通过undo log来实现原子性,它记录了数据修改之前的值(逻辑日志),一旦发生异常,可以用undo log进行回滚。

(2)一致性,consistent。指的是数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。比如主键必须是唯一的,字段长度符合要求。还有一个是用户自定义的完整性。

  比如说转账的这个场景,A账户余额减少1000,B账户余额只增加了500,这个时候因为两个操作都成功了,按照原子性的定义,它是满足原子性的, 但是它没有满足一致性,因为它导致了两个账户的不平衡。所以也违反了一致性。用户自定义的完整性通常要在代码中控制。

(3)隔离性,Isolation。事务之间彼此是透明的,互不干扰的,通过这种方式,保证业务数据的一致性。通过MVCC和LBCC(基于锁)协调控制

(4)持久性,Durable。对数据库的任意的操作,增删改,只要事务提交成功,那么结果就是永久性的,不会因为系统宕机或者重启了数据库的服务器,数据又恢复到原来的状态。

  持久性是通过redolog和doublewrite双写缓冲来实现的,操作数据的时候会先写到内存的buffer pool里面,同时记录redo log,如果在刷盘之前出现异常,在重启后就可以读取redo log的内容,写入到磁盘,保证数据的持久性。当然,恢复成功的前提是数据页本身没有被破坏,是完整的,这个通过双写缓冲

(double write)保证。

原子性,隔离性,持久性,最后都是为了实现一致性。

3、数据库什么时候会出现事务?

  无论是在 Navicat 这种可视化工具里面去操作,还是在Java 代码里面通过API 去操作,加上@Transactional 注解或者 AOP 配置,最终都是发送一个指令到数据库去执行,Java的JDBC只不过是把这些命令封装起来了。

  开启事务的两种方式:

*  自动提交

  InnoDB里面有一个autocommit的参数(默认开启),当执行更新操作(增删改)时,它会自动开启一个事务,并且自动提交。

*  手动开启

  如果autocommit设置为关闭时,就需要手动开启和结束事务了。手动开启事务有两种方式:一种是用begin;一种是用start transaction。结束事务也有几种方式:一种是提交事务,commit;一种是回滚事务,rollback;还有就是当客户端连接断开时,事务也会结束。

4、事务并发带来的问题——(数据库读一致性问题)

  当很多事务并发地去操作数据库的表或行时,如果没有上面提到的Isolation(隔离性),就会产生很多问题:

  (1)脏读——事务A读到了事务B未提交的数据

    例如:在转账场景中,第一个事务基于读取到的第二个事务未提交的余额进行了操作,但是第二个事务进行了回滚,这个时候就会导致数据不一致。

  (2)不可重复读——由于其他事务修改导致事务A两次读到的数据不一致

    例如:有两个事务,第一个事务通过id=1查询到了一条数据。然后在第二个事务里面执行了一个update操作,执行了update以后它通过一个commit提交了修改。然后第一个事务中再次查询id=1的数据,便读取到了其他事务已提交的数据导致前后两次读取的数据不一致。这种一个事务读取到了其他事务                  已提交的数据导致前后两次读取数据不一致的情况,叫做不可重复读。

  (3)幻读——由于其他事务插入导致事务A两次读到的数据不一致

    幻读和不可重复读的区别:不可重复读是修改或者删除导致的,幻读是插入导致的

  以上三个问题都是数据库读一致性问题,是在同一个事务中前后两次读取出现了不一致的情况。

5、事务隔离级别

(1)SQL92对四种隔离级别定义

  第一种叫:Read Uncommitted(未提交读),一个事务可以读取到其他事务未提交的数据,会出现脏读,它没有解决任何问题。

  第二种叫:Read Committed(已提交读),一个事务只能读取到其他事务已经提交的数据,不能读取到其他事务未提交的数据,它解决了脏读的问题,但是会出现不可重复读问题。

  第三种叫:Repeatable Read(可重复读),在同一个事务里面多次读取同样的数据结果是一样的,它解决了不可重复读问题,但是会出现幻读问题。

  第四种叫:Serializable(串行化),所有事务都是串行执行的,也就是数据的操作需要排队,所以不存在并发,也就不存在事务并发带来的问题。

(2)InnoDB对隔离级别的支持 

         

   RC隔离级别:

    1)没有解决不可重复读和幻读的原因:与其MVCC快照数据创建时机有关,在该级别下,每次select都会创建新的数据快照。

    2)加锁可以解决不可重复读,但是不能解决幻读:通过加共享锁,可以保证数据不被修改,但是因为该级别只有记录锁,没有间隙锁和临键锁,所以不能解决幻读问题。

  RR隔离级别:

    因为MVCC在该级别下只在第一次select时创建数据快照,可以保证只能查找创建时间小于等于当前事务ID的数据,和删除时间大于当前事务ID的行(或未删除),所以能解决不可重读和幻读问题。

 6、解决读一致性问题的实现方案——隔离级别的实现  InnoDB支持的四个隔离级别和SQL92定义的基本一致,隔离级别越高,事务的并发度就越低。唯一的区别就在于,InnoDB在RR的级别就解决了幻读的问题。这个也是InnoDB默认使用RR作为事务隔离级别的原因,既保证了数据的一致性,又支持较高的并发度。

*  LBCC——基于锁的并发控制

  第一种,既然要保证前后两次读取数据一致,那么读取数据的时候,锁定要操作的数据,不允许其他的事务修改就行了。这种方案叫做基于锁的并发控制Lock Based Concurrency Control(LBCC)。如果仅仅是基于锁来实现事务隔离,一个事务读取的时候不允许其他时候修改,那就意味着不持并发的读写操作,这样会极大地影响操作数据的效率。

MVCC——多版本并发控制

  另一种解决方案,如果要让一个事务前后两次读取的数据保持一致,可以在修改数据的时候给它建立一个备份或者叫快照,后面再来读取这个快照就行了。

  MVCC的核心思想是: 可以查到在这个事务开始之前已经存在的数据,生成数据快照,在该事务中就以该快照为依据,即使该数据在后面被修改或者删除了也不会影响当前事务。在这个事务之后新增的数据,也是查不到的。

数据快照何时创建?

  这里要说下数据快照的意思:数据快照是对全局数据设定的一个视图,并不是将数据拷贝一份。

  1)在RR隔离级别下,数据快照是在第一个select请求时创建的,也就是整个整个事务使用统一视图。

  2)在RC隔离级别下,每一个select请求都会创建一个新的数据快照,所以RC级别下普通select不能解决不可重复读问题,但是可以通过加锁的方式来解决。

实现原理:利用undo log实现多版本,InnoDB为每行记录都实现了两个隐藏字段:

  DB_TRX_ID,6字节:插入或更新行的最后一个事务的事务ID,事务编号是自动递增的(可以把它理解为创建版本号,在数据新增或者修改为新数据的时候,记录当前事务ID)

  DB_ROLL_PTR,7字节:回滚指针(可以把它理解为删除版本号,数据被删除或记录为旧数据的时候,记录当前事务ID)

原理如下图演示:    

   可以将事务ID理解为版本号。

  1)第一个事务,初始化数据。此时的数据,创建版本是当前事务ID,删除版本为空。

  2)第二个事务,执行第一次查询,读取到两条原始数据,此时事务ID是2。

  3)第三个事务,插入数据,此时的数据,多了一条tom,它的创建版本号是当前事务ID是3。

                            

  4)此时第二个事务执行第2次查询: 

     MVCC的查找规则:只能查找创建时间小于等于当前事务ID的数据,和删除时间大于当前事务ID的行(或未删除)。也就是不能查到在当前事务开始之后插入的数据,tom的创建ID大于2,所以还是只能查到qingshan和jack两条数据。

  5)第四个事务,删除了id=2 jack这条记录。此时的数据,jack这条记录的删除版本被记录为当前事务ID=4,其他数据不变。

         

  6)此时第二个事务执行第3次查询:

     根据MVCC查找规则,在当前事务开始之后删除的数据依然可以查出来,因为jack的删除版本为4,大于2,所以还是查询出qingshan和jack两条数据。

  7)第五个事务,执行更新操作,这个事务的ID是5。此时的数据,更新数据时,旧数据的删除版本被记录为当前事务ID=5,产生一条新数据

          

   8)此时第二个事务执行第4次查询:

      根据MVCC查找规则,因为更新后的数据 ‘盆鱼宴’ 创建版本大于 2,代表是在事务之后增加的,查不出来。而旧数据qingshan的删除版本大于2,代表是在事务之后删除的,可以查出来。

  通过以上的演示可以看到,通过版本号控制,无论其他事务是插入、修改、删除,第二个事务查询到的数据都没有变化。

7、再来看一下update语句的执行流程

update table_test set num = num+1 where id = 1;

  执行流程:

    (1)MySQL执行器先找InnoDB引擎读取id=1这一行的数据,InnoDB引擎直接用树查找主键id=1那条数据,如果数据所在页直接在内存中,那么直接返回,否则先从磁盘读取到缓存中再返回;

    (2)执行器获得数据后,执行num = num + 1,再调用InnoDB引擎接口写入新的数据;

    (3)InnoDB引擎将新的数据更新到内存中,再将这个更新操作记录到redo log中,此时redo log日志处于prepare状态,并通知执行器已就绪,随时可以提交事务;

    (4)MySQL执行器写入binlog日志并持久化到磁盘,并调用InnoDB引擎的事务提交接口,InnoDB引擎将刚刚写入的redo log日志的状态改为commit状态,至此,事务完成。

  从上面的流程可以看到redo log的写入分为两步:prepare、commit,这就是所谓的两阶段提交。而且两阶段提交一定是成功的写入了两个日志文件:redo log & binlog,只有这样事务才能提交,数据才能满足一致性原则。

 

posted @ 2020-05-11 21:12  jingyi_up  阅读(57)  评论(0编辑  收藏  举报