事务特性原理及其原理、隔离级别和传播属性

一、前言

2020.1.8
对博客内容进行扩展和修改时,写了半天的内容在点击保存的时候突然让我重新登录,重新登录后对博客内容的修改都没了,只好重新写(新写的感觉没有第一遍写的顺畅 ),好气啊!!!!!!!!

二、事务的四大特性

事务(Transaction)是并发控制单位,是用户定义的一个操作序列,这些操作要么都做,要么都不做,是一个不可分割的工作单位。

1. 介绍

1.1. 原子性

一个事务中所有对数据库的操作是一个不可分割的操作序列,要么全做要么全不做

即事务是执行的最小单元,执行结果只有两个,要么成功,要么失败回滚。

1.2. 一致性

一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。数据不会因为事务的执行而遭到破坏。

比如,当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统在运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是不一致的状态。

1.3. 持久性

持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,接下来的任何操作和故障都不应该影响到已提交的事务执行结果。

例如我们在使用JDBC操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务以及正确提交,即使这时候数据库出现了问题,也必须要将我们的事务完全执行完成,否则就会造成我们看到提示事务处理完毕,但是数据库因为故障而没有执行事务的重大错误。

1.4. 隔离性

一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。

隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
 关于事务的隔离性数据库提供了多种隔离级别,稍后会介绍到。

2. MySql 中四大特性实现原理

Mysql 在5.7版本后默认使用 InnoDB 作为存储引擎,所以下面介绍 InnoDB中的实现方式。

2.1 原子性

InnoDB 引擎使用 undo log(归滚日志)来保证原子性操作

事务对数据库的每一条数据的改动(INSERT、DELETE、UPDATE)都会被记录到 undo log 中,

  • insert : undo log 中将插入记录的主键记录下来,事务回滚时将这条主键记录删除即可。
  • delete :undo log 中将这条记录的所有字段内容记录下来,事务回滚时将由这些内容组成的记录插入到表中即可。
  • update:undo log 中将这条记录的旧值都记录下来,事务回滚时将这条记录更新为旧值即可。

当事务执行失败或者调用了 rollback 方法时,就会触发回滚事件,利用 undo log 中记录将数据回滚到修改之前的样子。

关于undo log 的具体工作流程,详参: https://blog.51cto.com/qiangmzsx/1768263

2.2 持久性

InnoDB 引擎使用 redo log(归档日志)来保证原子性操作。

由于CPU和磁盘速度不一致问题,Mysql是将磁盘上的数据加载到内存,对内存进行操作,然后再回写磁盘。假设此时宕机了,在内存中修改的数据全部丢失了,持久性就无法保证。
可能有人会提出将数据写回磁盘后再将事务提交即可。如果在写回磁盘前机器宕机,则事务未提交,下次启动会回滚操作。

但实际上,根据局部性原理和磁盘预读的特性,磁盘每次IO并不是严格按需读取,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。磁盘的预读一般是页的整数倍(页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页。不同操作系统的页大小可能不一样,一般为8k)。简单来说,一次IO读取,不光把当前磁盘地址的数据,而是把相邻的数据也都读取到内存缓冲区内。
而上述方案则会出现如下问题:

  • 每次更新可能只是更新一两个字节,但是却需要IO整个页的大小
  • 一个事务中的SQL可能牵扯多个磁盘页的数据修改,而这些数据物理上可能无限远,即会出现随机IO,速度比较慢。

所以,InnoDB 引擎引入了一个中间层来解决这个持久性的问题,我们把这个叫做 redo log(归档日志)。

当事务中对数据修改时,InnoDB会先将记录写入到redo log中,随后更新内存,这时候更新就算结束了,当事务提交的时候,会将redo log日志进行刷盘(redo log一部分在内存中,一部分在磁盘上)。这时候即使数据库宕机重启,也可以从 redo log中读取未写入磁盘中的数据,再根据 undo log 和 bin log 内容决定是回滚数据还是提交数据。

使用 redo log 有以下两个优势:

redo log只记录了修改哪一页修改的内容,因此体积小,刷盘快。
redo log使用末尾进行追加,属于顺序IO。相较于随机IO效率更高。

关于redo log 的具体工作流程,详参: https://mp.weixin.qq.com/s/vmB7Gsr9N3ZfF805wy6eHw

2.3 隔离性

InnnDB利用锁和 MVCC 机制来保证隔离性。

根据 https://www.cnblogs.com/moershiwei/p/9766916.html 的介绍,我们可以理解为锁的实现方式是悲观锁的方式(即每次操作先加锁,再操作,提交后释放锁),MVCC是一种乐观锁的方式(即每次操作不加锁,最后提交前对比一下版本号或者其他标志,如果没有过程中没有被修改,就提交)。

2.3.1 数据库锁(悲观锁)

1. 悲观锁按照使用性质划分:

  • 共享锁(Share locks简记为S锁):也称读锁,事务A对对象T加s锁,其他事务也只能对T加S,多个事务可以同时读,但不能有写操作,直到A释放S锁。

  • 排它锁(Exclusive locks简记为X锁):也称写锁,事务A对对象T加X锁以后,其他事务不能对T加任何锁,只有事务A可以读写对象T直到A释放X锁。

  • 更新锁(简记为U锁):用来预定要对此对象施加X锁,它允许其他事务读,但不允许再施加U锁或X锁;当被读取的对象将要被更新时,则升级为X锁,主要是用来防止死锁的。因为使用共享锁时,修改数据的操作分为两步,首先获得一个共享锁,读取数据,然后将共享锁升级为排它锁,然后再执行修改操作。这样如果同时有两个或多个事务同时对一个对象申请了共享锁,在修改数据的时候,这些事务都要将共享锁升级为排它锁。这些事务都不会释放共享锁而是一直等待对方释放,这样就造成了死锁。如果一个数据在修改前直接申请更新锁,在数据修改的时候再升级为排它锁,就可以避免死锁。

2. 悲观锁按照作用范围划分

  • Record Locks(行锁) : 行锁,顾名思义,是加在索引行(对!是索引行!不是数据行!)上的锁。比如select * from user where id=1 and id=10 for update,就会在id=1和id=10的索引行上加Record Lock。

  • Gap Locks(间隙锁) : 间隙锁,它会锁住两个索引之间的区域。比如select * from user where id>1 and id<10 for update,就会在id为(1,10)的索引区间上加Gap Lock。

  • Next-Key Locks(间隙锁) : 也叫间隙锁,它是Record Lock + Gap Lock形成的一个闭区间锁。比如select * from user where id>=1 and id<=10 for update,就会在id为[1,10]的索引闭区间上加Next-Key Lock。

  • 表锁: 锁住整张表

组合起来就会有:行级共享锁,表级共享锁,行级排它锁,表级排它锁

InnoDB 默认支持表级锁和行级锁,但是需要注意的是,如果检索项(可以简单理解是where后面的过滤项)如果是索引的话则使用行级锁,否则使用表级锁。

2.3.2 MVCC : 多版本并发控制 (乐观锁)

InnoDB 默认的隔离级别REPEATABLE READ(可重复读)需要两个不同的事务相互之间不能影响,而且还能支持并发,这点悲观锁是达不到的,所以REPEATABLE READ(可重复读)采用的就是乐观锁,而乐观锁的实现采用的就是MVCC。

虽然使用锁机制也可以控制并发,但是其系统开销较大,而MVCC可以在大多数情况下代替行级锁,使用MVCC,能降低其系统开销。

InnoDB 通过在每条记录上创建两个隐藏的列来实现,这两列分别是数据版本号db_trx_id(最后更新数据的事务id, 默认是1) 删除版本号 db_roll_pt (数据删除的事务id,默认null。 事务id由mysql数据库自动生成,且递增。db_trx_id记录着最近更新这条记录的事务的id,db_roll_pt 记录着删除这条记录的事务的id。

当查询时需要同时满足以下两个条件
  1、查找数据版本号 db_trx_id,早于(小于等于)当前事务id的数据行。 这样可以确保事务读取的数据是事务之前已经存在的。或者是当前事务插入或修改的。
  2、查找删除版本号 db_roll_pt 为null 或者大于当前事务版本号的记录。 这样确保取出来的数据在当前事务开启之前没有被删除。

更新时: 会生成新的一行。先复制数据,新数据数据版本号为当前事务id,删除版本号为 null 。然后更新 原来数据的删除版本号为 当前事务id


下面的a(1, null) 代表a(db_trx_id, db_roll_pt ) 的值:

比如:

  1. 事务A,id 为1。 插入了记录a(1, null),b(1, null),并提交。
  2. 事务B,id为 2 。查询三遍表记录(即三遍 select 语句),当查询结束第一遍时,返回 a(1, null),b(1, null)。此时事务C开始执行。
  3. 事务C,id为 3。删除了记录a(1, 3)并提交。此时表中记录为a(1, 3),b(1, null)
  4. 此时事务B执行第二次查询,a记录仍满足查询条件,仍会被查询出来,此时查询结果和第一次查询结果相同 : a(1, 3),b(1, null)
  5. 事务D,id为4 。更新操作,更新b记录,尚未提交。过程是会先复制一条b记录,此时表中记录为a(1, 3), b(1, null), b副本(4, null)。
  6. 此时事务B执行第三次查询。根据查询条件,结果仍为a(1, 3), b(1, null)。 – 此时可以看到,事务B的三次查询返回结果相同,并未受到事务C、D的影响。
  7. 事务D继续操作,更新数据b,原来数据的删除版本号为 当前事务id。即这时库中记录数据为a(1, 3), b(1, 4), b副本(4, null), 然后提交。
  8. 假设步骤1中事务A,插入记录a(1, null) 后未插入b便提交,事务B进行了第一次查找,会查找出来a(1, null)。之后事务C插入了数据b(1, null)并提交。事务B进行第二次查找则会查找出来a(1, null), b(1, null)。出现了幻读。

可以看到MVCC实现了可重复读的隔离级别,避免了脏读、不可重复读,但是可能会出现幻读。

上面的一个流程只是简单的文字叙述,详细内容参看: https://www.cnblogs.com/luchangyou/p/11321607.html


2.4 一致性

数据库是无法保证一致性的,一致性的保证需要业务代码来确保业务的一致性。
比如转账业务,A扣钱,B加钱,代码中没有写B加钱的逻辑,数据库自然无法保证,一致性自然无法保证。

二、事务的七种传播属性

1. 介绍

在这里插入图片描述

事务传播属性解释
PROPAGATION_REQUIRED支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。即如果上级具有事务,则使用上级的事务,不具备则自己新建一个事务
PROPAGATION_REQUIRES_NEW新建事务,如果当前存在事务,把当前事务挂起。即如果上级存在事务,则挂起上级事务,使用自己新创建的事务
PROPAGATION_MANDATORY支持当前事务,如果当前没有事务,就抛出异常。即如果上级具有事务,则使用上级的事务,上级没有事务,则抛出异常
PROPAGATION_SUPPORTS支持当前事务,如果当前没有事务,就以非事务方式执行。即如果上级具有事务,则使用上级的事务,如果上级没有事务,则不开启事务
PROPAGATION_NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。即如果上级具有事务,则使用挂起上级事务,使用非事务方式。
PROPAGATION_NEVER以非事务方式执行,如果当前存在事务,则抛出异常
PROPAGATION_NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。

这里解释一下 PROPAGATION_NESTEDPROPAGATION_REQUIRES_NEW 的区别:
最容易弄混淆的其实是PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED

PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 “内部” 事务. 这个事务将被完全
commited 或 rolled back 而不依赖于外部事务,它拥有自己的隔离范围, 自己的锁, 等等.
当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行.
PROPAGATION_REQUIRES_NEW常用于日志记录,或者交易失败仍需要留痕

另一方面, PROPAGATION_NESTED 开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正
的子事务. 潜套事务开始执行时, 它将取得一个 savepoint.
如果这个嵌套事务失败, 我们将回滚到此 savepoint. 潜套事务是外部事务的一部分,
只有外部事务结束后它才会被提交.

由此可见, PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于,
PROPAGATION_REQUIRES_NEW 完全是一个新的事务, 而 PROPAGATION_NESTED
则是外部事务的子事务, 如果外部事务 commit, 潜套事务也会被 commit,
这个规则同样适用于 roll back.

2. 解释

上面的解释比较官方,什么意思呢?具体以下面的一个小例子来解释。

Spring 中 @Transactional 注解默认的隔离级别是 PROPAGATION_REQUIRED

  1. 首先程序结构如下(其实很好理解,所以写的比较简单)
    UserService 调用 insertUser() 方法。 insertUser() 调用 RoleService .insertRole() 中来向数据库中添加一个角色。我们的目的就是当用户调用insertUser() 方法时会调用RoleService.insertRole方法.

  2. 首先验证一个 @Transactional 默认的传播属性 PROPAGATION_SUPPORTS
    代码如下,insertUser 开启 传播属性为 REQUIRED 的事务,insertRole 开始传播属性为SUPPORTS 的事务。我们需要验证 inserRole 方法上的事务是否会生效?

    	******************** UserService *************************
        /**
         * 加入两个角色
         */
        @Override
        @Transactional(propagation = REQUIRED)   // 开启事务,传播属性为 REQUIRED
        public void insertUser() {
            User user = new User();
            user.setName("陈七");
            user.setPwd("999999");
            user.setParentId(3);
            userMapper.insertUser(user);
            roleService.insertRole();
            int i = 10 / 0;
        }
    
    	******************** UserService *************************
    	
    	******************** RoleService *************************
    	@Override
    	@Transactional(propagation = SUPPORTS)	 // 开启事务,传播属性为 SUPPORTS
        public void insertRole() {
            roleMapper.insertRole();	// 如果有一条数据,则事务未生效
            int i = 10 / 0;
        }
        ******************** RoleService *************************
    
  3. 执行结果是User表和Role表中都没有数据,说明insertRole 方法上的事务生效了。

  4. 再回头 看一下 PROPAGATION_SUPPORTS 传播属性的描述:支持当前事务,如果当前没有事务,就以非事务方式执行。。也就是说本例中 insertRole 方法的事务是否生效是看他的上级inserUser方法是否具有事务的,所以我们这里去掉了insertUser 方法的事务注解。重新运行。

    	******************** UserService *************************
        /**
         * 加入两个角色
         */
        @Override
        public void insertUser() {
            User user = new User();
            user.setName("陈七");
            user.setPwd("999999");
            user.setParentId(3);
            userMapper.insertUser(user);
            roleService.insertRole();
            int i = 10 / 0;
        }
    
    	******************** UserService *************************
    	
    	******************** RoleService *************************
    	@Override
    	@Transactional(propagation = SUPPORTS)	 // 开启事务,传播属性为 SUPPORTS
        public void insertRole() {
            roleMapper.insertRole();	// 如果有一条数据,则事务未生效
            int i = 10 / 0;
        }
        ******************** RoleService *************************
    
  5. 运行结果如下, User 表和Role表中都插入了数据。因为inserUser 方法没有事务,所以User表中插入数据应该的,但是role表中也插入了数据,说明insertRole 方法上面的事务未生效。因为其调用者并没有开始事务,而insertRole 方法在开始事务的时候定义了传播属性是PROPAGATION_SUPPORTS。上级没有事务则不会自动生成事务。所以造成了如下结果 :
    . 在这里插入图片描述
    在这里插入图片描述

  6. 为了更好的验证,我们将 insertRole 方法上的事务传播属性改为 PROPAGATION_MANDATORYPROPAGATION_MANDATORY 的描述支持当前事务,如果当前没有事务,就抛出异常。即如果上级具有事务,则使用上级的事务,上级没有事务,则抛出异常 。再次运行。可以看到因为insertUser方法没有开启事务,所以抛出了异常。
    在这里插入图片描述

三、事务的五种隔离级别

1. 什么是脏读、不可重复读、幻读。

  • 脏读:脏读又称无效数据读出。一个事务读取另外一个事务还没有提交的数据叫脏读。

    例如:事务T1修改了一行数据,但是还没有提交,这时候事务T2读取了被事务T1修改后的数据,之后事务T1因为某种原因Rollback了,那么事务T2读取的数据就是脏的。

  • 不可重复读:不可重复读是指在同一个事务内,两个相同的查询返回了不同的结果。

    例如:事务T1读取某一数据,事务T2读取并修改了该数据,T1为了对读取值进行检验而再次读取该数据,便得到了不同的结果。

    解决:使用行级锁,锁定该行,事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据

  • 幻读:在同一事务内,两次相同的查询返回的数据条目数量不同,在RU、RC、RR级别下,都会出现幻读

    例如:事务T1读取一次表中数据总量,事务T2修改了表中数据总量(插入或者删除了数据)。这是事务T1再次读取表中数据总量,发现和第一次读取的总量不同,好像产生了幻觉。
    亦或者,在可重复读的隔离级别下,事务T1查询了记录a,发现不存在,准备插入记录a,但是此时事务T2开启并插入了记录a,此时事务T1才开始准备插入,但是T1插入会失败,因为库里已经存在记录a,此时T1即便再次查询记录a也无法查询到。这是因为mvcc的特性,由于T2比T1晚开启,T1是不会读取到T2修改的记录。

    解决:使用表级锁,锁定整张表,事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。

注意:
1. 脏读、不可重复读、幻读,解决该问题所需要的隔离级别
高低是:脏读 (读已提交)< 不可重复读(可重复读) < 幻读(可串行化)

2. 不可重复读针对的是多次读取内容不同,幻读针对的是多次读取,内容条数不同
3. 快照读:普通的select操作,是从read view中读取数据,读取的可能是历史数据
4. 当前读:insert、update、delete、select…for update这种操作,读取的总是当前的最新数据

2. 事务的隔离级别

这里说的五种隔离级别,是指在Spring中,而在数据库中,并不包含Sping默认这一级别

隔离级别解释
Spring默认(DEFAULT)这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别.另外四个与JDBC的隔离级别相对应;
读未提交(READ_UNCOMMITTED)这是事务最低的隔离级别,它允许事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻读。
读已提交 (READ_ COMITTED)保证一个事务修改的数据提交后才能被另外一个事务读取。 另外一个事务不能读取该事务未提交的数据。这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻读。
可重复读(REPEATABLE READ)这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻读。它除了保证-一个事务不能读取另一个事务未提交的数据外,还保证了避免下面的情况产生(不可重复读) (MySql 默认就是这个级别)
可串行化 (SERIALIZABLE)这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,不可重复读外,还避免了幻像读。但是不建议使用,他将事务完全按照串行处理。

以上:内容部分参考网络
https://www.cnblogs.com/daofaziran/p/10933302.html
https://blog.csdn.net/star1210644725/article/details/96829608
https://mp.weixin.qq.com/s/uMBPfM7zx0Rp_brP4cnKaA
https://www.cnblogs.com/luchangyou/p/11321607.html
https://www.cnblogs.com/moershiwei/p/9766916.html
https://www.jianshu.com/p/563612576e6e
https://blog.csdn.net/z69183787/article/details/76208998
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
事务是数据库操作的一个逻辑单元,它保证了数据的一致性和完整性。事务有四个基本特性,也称为ACID特性: 1. **原子性** (Atomicity):事务中的所有操作要么全部完成,要么全部不做,不会部分执行。 2. **一致性** (Consistency):事务开始前和结束后,数据库的数据都要保持在一致的状态。 3. **隔离性** (Isolation):确保并发环境下的事务看起来像是独自运行,互不影响,包括读未提交、已提交和序列化等隔离级别。 4. **持久性** (Durability):一旦事务提交,其对数据库的影响是永久的,即使系统崩溃也能恢复。 在并发环境下,隔离级别用于解决可能出现的问题,常见的隔离级别有: - **读未提交(Read Uncommitted)**:允许读取其他事务未提交的数据,可能导致脏读。 - **已提交(Read Committed)**:只读取已经提交的数据,避免脏读,但仍存在幻读。 - **可重复读(Repeatable Read)**:在此级别下,一次事务的多次读取结果不变,但可能会看到幻灭现象(因为其他事务的插入)。 - **串行化(Serializable)**:最严格的级别,强制事务按照某种顺序执行,完全消除幻读。 事务传播属性定义了当在一个事务中调用另一个事务时的行为,如: - **支持(Support)**:默认值,如果目标操作在当前事务中,就执行。 - ** Requires**:只有当目标操作成功才会继续。 - ** Not Supported**:禁止嵌套事务。 - ** Requires New**:在新的事务上下文中执行。 - ** MANDATORY**:如果目标操作在事务中,强制其成为新事务的一部分。 - ** REQUIRES NEW**:总是新建一个新的事务来执行。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫吻鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值