数据库事务管理与锁

数据库的事务与锁

事务及其性质

事务必须满足:ACID(原子性,一致性,隔离性,持久性)四特性,事务是恢复和并发控制的基本单位。

  1. 原子性指的是事务是数据库的逻辑工作单位,事务中操作要么都做,要么都不做;
  2. 一致性指的是事务的执行结果必须是使数据库从一个一致性状态变成另一个一致性状态,一致性和原子性是密切相关的;
  3. 隔离性指的是一个事务执行不能被其他事务干扰;
  4. 持久性指的是一个事务一旦提交,他对数据库中数据的改变就是永久性的。

事务隔离级别

数据库的隔离级别可以分为:读未提交、读提交、可重复读和串行化

在学习这里的时候,总感觉说得满头雾水的,怎么也不能理解,因为它又联系着三个现象: 脏读、不可重复读、幻读

当时很难理解隔离级别与现象之间的关系,现象可以理解为多个事务并发时候如不哦不加控制而导致的问题。隔离级别是为了避免其中的某种或某几种问题而设置的一种执行要求执行流程

我们假设有两个事务 A 和 B 在进行读写操作。先来看现象:

  • 脏读: B读到了A未提交的修改数据 A修改未提交—B读—A回滚 (A写的时候B读),这时候B读到的就是错误的数据。

  • 不可重复读: 在两次读之间插入了另一个事务的修改操作,并提交,导致两次读取不一致。 (A读的时候B写)

  • 幻读:在两次读取之间,出现了 INSERT 操作。 A读—–B插入—–A读。两次读取不一致。

不可重复读与幻读之间的区别在于,A 两次读之间的差别,是由于 UPDATE 操作还是由 INSERT 操作引起的。

那么为了避免这些问题的发送,数据库设计了不同的事务隔离级别。

  • 读未提交:一个事务可以读取另一个未提交事务的数据

    这种情况下不会带来任何的隔离,三种情况都可能发生。

  • 读提交:一个事务要等另一个事务提交后才能读取数据(避免脏读)(给修改加锁禁止读)

    B无法读到A未提交的修改,避免了脏读。

  • 可重复读:读取事务开始后,不允许修改(避免不可重复读)(读加锁禁止修改)

    A在读取的时候,B只能读,不能改,可以保证A读取的过程的一致性。

    但是只能锁住表中已有的行,而不能阻止其他事务的插入,无法避免幻读。

  • 串行化:每个事务完全串行化进行。(避免幻读

MySQL 默认的事务隔离级别为 可重复读。

事务隔离级别和可能存在的问题如下:

脏读 不可重复读 幻读
读未提交
读已提交
可重复读
串行化

数据库锁出现的原因是为了处理并发问题。数据库并发需要使用事务来控制,事务并发问题需要数据库锁来控制。

数据库的锁根据不同的分类角度可以由不同的分类:

  • 从使用角度: 乐观锁与悲观锁
  • 从数据库机制上:共享锁与排它锁
  • 从锁的粒度上: 行级锁和表级锁
悲观锁与乐观锁
  • 悲观锁:假定会发生并发冲突,在最整个数据处理过程中都将数据处于锁定的状态。 适用于写操作多的场景。
  • 乐观锁:假设不会发生并发冲突,只有在提交时检查是否违反数据的完整性。适用于读操作多的场景。
排它锁与共享锁
  • 排它锁:也叫写锁,表示对数据进行写操作。如果一个事务对对象加了排他锁,其他事务就不能再给它加任何锁了。
  • 共享锁:也叫读锁,用于所有的只读数据操作。共享锁是非独占的,允许多个并发事务读取其锁定的资源。

行级锁与表级锁

  • 行级锁: 是粒度最小的一种锁,只会对当前进行操作的行进行加锁,能够大大减少数据库操作的冲突,并发度最高,同时有可能出现死锁。但是加锁慢,开销也最大。
  • 表级锁:是粒度最大的一种锁,会对当前进行操作的整个数据表进行加锁。但是实现简单,加锁快,消耗资源少。发生锁冲突的概率最高,并发度最低。
  • 页面锁:粒度介于行级锁和表级锁之间;会出现死锁,并发度一般。
二阶段锁

每个事务分两个阶段提出加锁和解锁申请。最初处于加锁阶段,事务根据需要获得锁。一旦该事务释放了锁,它就进入了缩减阶段,并且不能再发出加锁请求。二阶段封锁并不会保证不发生死锁(如果请求锁的数据顺序不同)。

引入2PL是为了保证事务的隔离性,保证并发调度的准确性,多个事务在并发的情况下依然是串行的。

锁与隔离级别

  • 在 RU 中,读取数据不需要加锁,这样读写都没有被保护。
  • 在 RC 中,写过程需要排他锁,禁止读进程读取到未提交的修改。
  • 在 RR 中,读过程需要加共享锁,这是可以几个进程共享地读,但是禁止被修改,可以保证一个事务中读到的数据是一致的。

MVCC版本控制

多版本并发控制(MVCC) 是通过保存数据在某个时间点的快照来实现并发控制的。也就是说,不管事务执行多长时间,事务内部看到的数据是不受其它事务影响的,根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。简单来说,MVCC 的思想就是保存数据的历史版本,通过对数据行的多个版本管理来实现数据库的并发控制。 是乐观锁的一种实现方式。

MVCC 是通过在每行记录后面保存两个隐藏的列来实现的。一个保存了行的创建版本号,一个保存了行的删除版本号。每开启一个新的事务,系统版本号都会自动递增,作为事务的版本号。

  • MVCC读写不冲突不加锁,写写之间互相冲突需要加锁串联执行。(乐观锁)

  • select

    只会查找版本号小于等于当前事务版本的数据行。这样保证事务读取的是之前已经存在的或是事务自身插入的。

  • insert

    每插入新的一行,都保存当前的系统版本号作为行的版本号。

  • delete

    为删除的每一行保存当前系统版本号作为行删除版本号。

  • update

    插入一条新的记录,保存当前系统版本号为行版本号,同时保存当前系统版本号到原来的行作为删除版本号。

MVCC 解决了哪些问题
  1. 读写阻塞问题:MVCC 可以让读写互相不阻塞,即读不阻塞写,写不阻塞读,这样就可以提升事务并发处理能力。
  2. 降低了死锁的概率:读取数据时并不需要加锁,对于写操作,也只锁定必要的行。
ReadView

在 RC 级别下,每一次查询都会生成以个 ReadView,在 RR 级别下,只会在事务开启时生成一个 ReadView。View 包含了一个 low_limit_id, up_limit_id 和一个数组。数组中包含了事务开启时,所有未提交的活跃事务的 ID,低水位是当前活跃事务 ID 集合中最小的一个。高水位是下一次即将分配的事务 ID。

在读取时,比如事务 T 要访问数据 A ,则先获取记录 A 的事务 ID,然后与 readView 对比:

  • 如果在 readView 左侧,比它都小,这说明 A 在事务 T 开始前已经提交,记录是可见的。
  • 如果在 readView 右侧,比它都大,这说明 A 在事务 T 之后的事务修改的,记录是不可见的。
  • 如果在 readView 之中,则与数组中的事务 ID 对比,如果是已提交的,则可见;如果未提交的则不可见。
  • 对于不可见的事务,通过回滚指针,获取上一版本的记录,再进行对比。
MVCC 能解决幻读问题吗?

MVCC 只会工作在 读已提交 和 可重复读 两种隔离级别下。在 RR 时,还有另外一种机制可以避免幻读,即间隙锁。 间隙锁和 MVCC 一同工作实现事务。

间隙锁锁定索引记录间隙,确保索引记录的间隙不变。作用就是防止其他事务的插入操作,以防止幻读的发生。

快照读与当前度

MVCC 版本控制引入了一个新的概念:即快照读与当前读的区别。

快照读(SnapShot Read) 是一种一致性不加锁的读,是InnoDB并发如此之高的核心原因之一。

这里的一致性是指,事务读取到的数据,要么是事务开始前就已经存在的数据,要么是事务自身插入或者修改过的数据

不加锁的简单的 SELECT 都属于快照读

快照读 相对应的则是 当前读当前读就是读取最新数据,而不是历史版本的数据。加锁的 SELECT 就属于当前读,例如: