数据库 —— 事务的隔离级别与传播机制|8月更文挑战

165 阅读15分钟

这是我参与8月更文挑战的第20天,活动详情查看:8月更文挑战

什么叫事务?

事务是一系列对系统中数据进行访问与更新的操作组成的一个程序逻辑单元。即不可分割的许多基础数据库操作。

1 事务特性(ACID)

  • 原子性(Atomicity):事务是最小的执行单位,不允许分割。原子性确保动作要么全部完成,要么完全不起作用。

  • 一致性(Consistency):执行事务前后,数据保持一致。

    一个事务执行前后,应该使数据库从一个一致性状态转换为另一个一致性状态。比方说假设A、B两个人,共有5000元。那么无论A给B转多少钱,转多少次,总数仍然是5000没有改变。

  • 隔离性(Isolation):并发访问数据库时,一个事务不被其他事务所干扰。

  • 持久性(Durability):一个事务被提交之后。对数据库中数据的改变是持久的,即使数据库发生故障。

2 隔离级别

数据库事务隔离级别有4种,由低到高为:Read uncommittedRead committedRepeatable readSerializable 。而且,在事务的并发操作中可能会出现 脏读、不可重复读、幻读 问题。不做隔离操作则会出现:

  • 脏读:事务A中读到了事务B中未提交的更新数据内容
  • 不可重复读:读到其它事务已经提交后的更新数据,即一个事务范围内两个相同的查询却返回了不同数据
  • 幻读:事物A执行select后,事物B增或删了一条数据,事务A再执行同一条SQL后发现多或少了一条数据
  • 第一类丢失更新:A事务撤销时,把已经提交的B事务的更新数据覆盖了
  • 第二类丢失更新:A事务提交时,把已经提交的B事务的更新数据覆盖了

默认隔离级别

  • Oracle仅有Serializable(串行化)和Read Committed(读已提交)两种隔离方式,默认选择读已提交的方式
  • MySQL默认为Repeatable Read(可重读)

数据库中的锁

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

InnoDB存储引擎下的四种隔离级别发生问题的可能性如下:

隔离级别第一类丢失更新第二类丢失更新脏读不可重复读幻读
Read Uncommitted(读未提交)不可能可能可能可能可能
Read Committed(读已提交)不可能可能不可能可能可能
Repeatable Read(可重复读)不可能不可能不可能不可能可能
Serializable(串行化)不可能不可能不可能不可能不可能

2.1 Read Uncommitted(读未提交)

即读取到了其它事务未提交的内容。在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)

特点:最低级别,任何情况都无法保证

读未提交的数据库锁情况

  • 事务中读取数据:未加锁
  • 事务中更新数据:只对数据增加行级共享锁

2.2 Read Committed(读已提交)

即读取到了其它事务已提交的内容。一个事务只能看见已经提交事务所做的改变。这种隔离级别也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理期间可能会有新的commit,所以同一select可能返回不同结果。

特点:避免脏读

读已提交的数据库锁情况

  • 事务中读取数据:加行级共享锁(读到时才加锁),读完后立即释放
  • 事务中更新数据:在更新时的瞬间对其加行级排它锁,直到事务结束才释放

Read Committed隔离级别下的加锁分析

隔离级别的实现与锁机制密不可分,所以需要引入锁的概念,首先我们看下InnoDB存储引擎提供的两种标准的行级锁:

  • 共享锁(S Lock):又称为读锁,可以允许多个事务并发的读取同一资源,互不干扰。即如果一个事务T对数据A加上共享锁后,其他事务只能对A再加共享锁,不能再加排他锁,只能读数据,不能修改数据
  • 排他锁(X Lock): 又称为写锁,如果事务T对数据A加上排他锁后,其他事务不能再对A加上任何类型的锁,获取排他锁的事务既能读数据,也能修改数据

注意: 共享锁和排他锁是不相容的。

2.3 Repeatable Read(可重复读)

它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。但会导致**幻读 (Phantom Read)**问题。

幻读 是户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当该用户再读取该范围的数据行时,会发现有新的“幻影” 行。

特点:避免脏读、不可重复读。MySQL默认事务隔离级别

可重复读的数据库锁情况

  • 事务中读取数据:开始读取的瞬间对其增加行级共享锁,直到事务结束才释放
  • 事务中更新数据:开始更新的瞬间对其增加行级排他锁,直到事务结束才释放

2.4 Serializable(可串行化)

指一个事务在执行过程中完全看不到其他事务对数据库所做的更新。当两个事务同时操作数据库中相同数据时,如果第一个事务已经在访问该数据,第二个事务只能停下来等待,必须等到第一个事务结束后才能恢复运行。因此这两个事务实际上是串行化方式运行。

特点:避免脏读、不可重复读、幻读

可序列化的数据库锁情况

  • 事务中读取数据:先对其加表级共享锁 ,直到事务结束才释放
  • 事务中更新数据:先对其加表级排他锁 ,直到事务结束才释放

3 SpringBoot Transaction

查看 mysql 事务隔离级别:show variables like 'tx_iso%';

3.1 实现方式

在Spring中事务有两种实现方式:

  • 编程式事务管理: 编程式事务管理使用TransactionTemplate或直接使用底层的PlatformTransactionManager
  • 声明式事务管理: 建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务管理不需要入侵代码,通过@Transactional就可以进行事务操作,更快捷而且简单

3.2 提交方式

默认情况下,数据库处于自动提交模式。每一条语句处于一个单独的事务中,在这条语句执行完毕时,如果执行成功则隐式的提交事务,如果执行失败则隐式的回滚事务。 对于正常的事务管理,是一组相关的操作处于一个事务之中,因此必须关闭数据库的自动提交模式。不过,这个我们不用担心,spring会将底层连接的自动提交特性设置为false。也就是在使用spring进行事物管理的时候,spring会将是否自动提交设置为false,等价于JDBC中的 connection.setAutoCommit(false);,在执行完之后在进行提交,connection.commit();

3.3 事务隔离级别

隔离级别是指若干个并发的事务之间的隔离程度。

@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void addGoods(){
	......
}

枚举类Isolation中定义了五种隔离级别:

  • DEFAULT:默认值。表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是READ_COMMITTED
  • READ_UNCOMMITTED:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别
  • READ_COMMITTED:该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值
  • REPEATABLE_READ:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读
  • SERIALIZABLE:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别

3.4 事务传播行为

事务的传播性一般用在事务嵌套的场景,如一个事务方法里面调用了另外一个事务方法,那两个方法是各自作为独立的方法提交还是内层事务合并到外层事务一起提交,这就需要事务传播机制配置来确定怎么样执行。

@Transactional(propagation=Propagation.REQUIRED)
public void addGoods(){
	......
}

枚举类Propagation中定义了七种事务传播机制如下:

  • REQUIRED(required)

    Spring默认的传播机制,能满足绝大部分业务需求,如果外层有事务,则当前事务加入到外层事务,一块提交,一块回滚。如果外层没有事务,新建一个事务执行

  • REQUIRES_NEW(requires_new,新创建事务)

    该事务传播机制是每次都会新开启一个事务,同时把外层事务挂起,当当前事务执行完毕,恢复上层事务的执行。如果外层没有事务,执行当前新开启的事务即可

  • SUPPORTS(supports)

    如果外层有事务,则加入外层事务,如果外层没有事务,则直接使用非事务方式执行。完全依赖外层的事务

  • NOT_SUPPORTED(not_supported,传播机制不支持事务)

    该传播机制不支持事务,如果外层存在事务则挂起,执行完当前代码,则恢复外层事务,无论是否异常都不会回滚当前的代码

  • NEVER(never)

    该传播机制不支持外层事务,即如果外层有事务就抛出异常

  • MANDATORY(mandatory)

    与NEVER相反,如果外层没有事务,则抛出异常

  • NESTED(nested,嵌套事务)

    该传播机制的特点是可以保存状态保存点,当前事务回滚到某一个点,从而避免所有的嵌套事务都回滚,即各自回滚各自的,如果子事务没有把异常吃掉,基本还是会引起全部回滚的。

3.5 事务回滚规则

指示spring事务管理器回滚一个事务的推荐方法是在当前事务的上下文内抛出异常。spring事务管理器会捕捉任何未处理的异常,然后依据规则决定是否回滚抛出异常的事务。 默认配置下,spring只有在抛出的异常为运行时unchecked异常时才回滚该事务,也就是抛出的异常为RuntimeException的子类(Errors也会导致事务回滚),而抛出checked异常则不会导致事务回滚。 可以明确的配置在抛出那些异常时回滚事务,包括checked异常。也可以明确定义那些异常抛出时不回滚事务。

3.6 事务常用配置

  • readOnly

    该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。例如:@Transactional(readOnly=true)

  • rollbackFor

    该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。例如:指定单一异常类:@Transactional(rollbackFor=RuntimeException.class)指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class})

  • rollbackForClassName

    该属性用于设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚。例如:指定单一异常类名称@Transactional(rollbackForClassName=”RuntimeException”)指定多个异常类名称:@Transactional(rollbackForClassName={“RuntimeException”,”Exception”})

  • noRollbackFor

    该属性用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚。例如:指定单一异常类:@Transactional(noRollbackFor=RuntimeException.class)指定多个异常类:@Transactional(noRollbackFor={RuntimeException.class, Exception.class})

  • noRollbackForClassName

    该属性用于设置不需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,不进行事务回滚。例如:指定单一异常类名称:@Transactional(noRollbackForClassName=”RuntimeException”)指定多个异常类名称:@Transactional(noRollbackForClassName={“RuntimeException”,”Exception”})

  • propagation

    该属性用于设置事务的传播行为。例如:@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true)

  • isolation

    该属性用于设置底层数据库的事务隔离级别,事务隔离级别用于处理多事务并发的情况,通常使用数据库的默认隔离级别即可,基本不需要进行设置

  • timeout

    该属性用于设置事务的超时秒数,默认值为-1表示永不超时

3.7 事物注意事项

  • 要根据实际的需求来决定是否要使用事物,最好是在编码之前就考虑好,不然到以后就难以维护
  • 如果使用了事物,请务必进行事物测试,因为很多情况下以为事物是生效的,但是实际上可能未生效
  • 事物@Transactional的使用要放再类的公共(public)方法中,需要注意的是在 protected、private 方法上使用 @Transactional 注解,它也不会报错(IDEA会有提示),但事务无效
  • 事物@Transactional是不会对该方法里面的子方法生效!也就是你在公共方法A声明的事物@Transactional,但是在A方法中有个子方法B和C,其中方法B进行了数据操作,但是该异常被B自己处理了,这样的话事物是不会生效的!反之B方法声明的事物@Transactional,但是公共方法A却未声明事物的话,也是不会生效的!如果想事物生效,需要将子方法的事务控制交给调用的方法,在子方法中使用rollbackFor注解指定需要回滚的异常或者将异常抛出交给调用的方法处理。一句话就是在使用事物的异常由调用者进行处理
  • 事物@Transactional由spring控制的时候,它会在抛出异常的时候进行回滚。如果自己使用catch捕获了处理了,是不生效的,如果想生效可以进行手动回滚或者在catch里面将异常抛出,比如throw new RuntimeException();

3.8 失效场景

  • @Transactional 应用在非 public 修饰的方法上
  • 数据库引擎要不支持事务
  • 由于propagation 设置错误,导致注解失效
  • rollbackFor 设置错误,@Transactional 注解失效
  • 方法之间的互相调用也会导致@Transactional失效
  • 异常被你的 catch“吃了”导致@Transactional失效

3.9 select for update

for update是一种行级锁,又叫排它锁。一旦用户对某个行施加了行级加锁,则该用户可以查询也可以更新被加锁的数据行,其它用户只能查询但不能更新被加锁的数据行。

  • 修改sql:在 selectsql 尾部添加 for update。如:select * from job_info where id = 1 for update;
  • 启用事务:为 service 添加注解 @Transactional

只有当出现如下之一的条件,才会释放共享更新锁:

  1. 执行提交(COMMIT)语句
  2. 退出数据库(LOG OFF)
  3. 程序停止运行

假设有个表单products ,里面有id 跟name 二个栏位,id 是主键。

-- 例1: 明确指定主键,并且有此数据,row lock
SELECT * FROM products WHERE id='3' FOR UPDATE;
-- 例2: 明确指定主键,若查无此数据,无lock
SELECT * FROM products WHERE id='-1' FOR UPDATE;
-- 例2: 无主键,table lock
SELECT * FROM products WHERE name='Mouse' FOR UPDATE;
-- 例3: 主键不明确,table lock
SELECT * FROM products WHERE id<>'3' FOR UPDATE;
-- 例4: 主键不明确,table lock
SELECT * FROM products WHERE id LIKE '3' FOR UPDATE;

注意

  • FOR UPDATE 仅适用于InnoDB,且必须在事务区块(start sta/COMMIT)中才能生效
  • 要测试锁定的状况,可以利用MySQL 的Command Mode ,开二个视窗来做测试