事务和锁

  • 此篇博文主要是结合例子介绍数据库事务。
  • 同时介绍数据库相关锁协议。
  • 后续会对相关实现机制和核心思想进行深入探讨。

事务的定义

事务(Transaction),一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。 事务通常由高级数据库操纵语言或编程语言(如SQL,C++或Java)书写的用户程序的执行所引起,并用形如begin transaction和end transaction语句(或函数调用)来界定。事务由事务开始(begin transaction)和事务结束(end transaction)之间执行的全体操作组成。--来自百度百科

简而言之: 事务(Transaction)是并发控制的基本单位。
  • 所谓的事务,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。

举个例子:银行转账问题中,账户A向账户B转账,对应的会有两个操作,账户A减去对应的转账金额,账户B增加对应的转账金额。这两个操作要么都执行要么都不执行,此时应该把这两个操作看做一个事务,从而保证数据一致性。

事务的特点 ACID

  • 原子性(Atomicity):一个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。
  • 一致性(Consistency):事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。
  • 隔离性(Isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
  • 持久性(Durability):持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

事务对应的语句

  • BEGIN TRANSACTION 开始事务
  • COMMIT TRANSACTION 提交事务
  • ROLLBACK TRANSACTION 回滚事务

事务并发控制

事务不考虑隔离性引发的问题

  • 脏读:此种异常时因为一个事务读取了另一个事务修改了但是未提交的数据,当修改的事务进行回滚操作时将造成读取事务异常。
  • 不可重复读:在一个事务内读取表中的某一行数据,多次读取结果不同。(一个事务读取到了另外一个事务提交的数据)
  • 幻读(虚读):指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。例如读整个表,即表的行数,例如第一次读某个表有3条记录,第二次读该表又有4条记录 (和不可重复读的不同:不可重复读针对的是数据的值,幻读针对的是数据的数量

数据库事务隔离级别(SQL标准定义)

  • READ UNCOMMITTED(未提交读):事务中的修改,即使没有提交,其他事务也可以看得到。很容易导致脏读等众多问题,如无必要,很少使用
  • READ COMMITTED(提交读):大多数数据库系统默认的隔离级别(除Mysql等)。这种隔离级别就是一个事务的开始,只能看到已经完成的事务的结果,正在执行的,是无法被其他事务看到的。这种级别会出现读取旧数据的现象,即不可重复读的问题。
  • REPEATABLE READ(可重复读):解决了脏读的问题,该级别保证了每行的记录的结果是一致的,也就是上面说的读了旧数据的问题,但是却无法解决另一个问题,幻行,顾名思义就是突然蹦出来的行数据。指的就是某个事务在读取某个范围的数据,但是另一个事务又向这个范围的数据去插入数据,导致多次读取的时候,数据的行数不一致。即幻读--MYSQL默认隔离级别
  • SERIALIZABLE(可串行化):最高的隔离级别,它通过强制事务串行执行(注意是串行),避免了前面的幻读情况,由于他大量加上锁,导致大量的请求超时,因此性能会比较底下,在特别需要数据一致性且并发量不需要那么大的时候才可能考虑这个隔离级别
隔离级别 脏读可能性 不可重复读可能性 幻读可能性 加锁读
READ UNCOMMITTED Yes Yes Yes No
READ COMMITTED No Yes Yes No
REPEATABLE READ No No Yes No
SERIALIZABLE No No No Yes

数据库锁

数据库锁的基本类型:
  • X锁exclusive 用于写操作
    - 某数据对象在没有加任何锁的情况下,一个事务可以对其加X锁,而其他事务就不能对其再加任何锁
  • S锁:share 用于读操作
    - 一个事务对某数据对象加了S 锁后,其他事务就不能对其加X锁,但可以加S锁
  • U锁:update
    - 事务要更新数据对象时,先申请该对象的U 锁。对象加了U锁,允许其他事务对它加S锁。在最后写入时,再申请将U锁升级为X锁。不必在全过程中加X
不同级别的加锁协议
  • 一级封锁协议(脏数据、不可重复读)
    • 任一事务在写某数据前,必须对其加上X锁,该事务结束后才释放。不采用S锁,读数据不用加锁。
    • 事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK)。
  • 二级封锁协议(不可重复读)
    • 满足一级封锁协议,且任一事务在读取某数据前,必须对其加上S锁,读完后 就释放
  • 三级封锁协议()
    • 满足一级封锁协议,且任一事务在读取某数据前,必须对其加上S锁,事务结束后 释放锁

对应的我们便可以看到 隔离级别和加锁协议之间的关系
一级封锁协议 -> READ UNCOMMITTED
二级封锁协议 -> READ COMMITTED
三级封锁协议 -> REPEATABLE READ

其他加锁协议
  • 两阶段加锁协议:

    • 整个事务分为两个阶段,前一个阶段为加锁,后一个阶段为解锁。在加锁阶段,事务只能加锁,也可以操作数据,但不能解锁,直到事务释放第一个锁,就进入解锁阶段,此过程中事务只能解锁,也可以操作数据,不能再加锁。两阶段锁协议使得事务具有较高的并发度,因为解锁不必发生在事务结尾。它的不足是没有解决死锁的问题,因为它在加锁阶段没有顺序要求。如两个事务分别申请了A, B锁,接着又申请对方的锁,此时进入死锁状态。
    • 定理:若所有事务均遵守两段锁协议,则这些事务的所有交叉调度都是可串行化的。
  • 多粒度加锁协议

    • 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。只在存储引擎层实现
    • 页级锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般
    • 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。