[数据库]数据库中的事务

一、数据库的事务

数据库事务是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。并非任意的对数据库的操作序列都是数据库事务。数据库事务拥有以下四个特性,习惯上被称之为ACID特性:

  • 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行;
  • 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束;
  • 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行;
  • 持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中;

二、事务中的并发问题

由于事务是一个操作序列,在事务进行的过程中,由于并发,可能会出现如下问题:
– 更新丢失:两个事务都同时更新一行数据,但是第二个事务却中途失败退出,导致对数据的两个修改都失效了。这是因为系统没有执行任何的锁操作,因此并发事务并没有被隔离开来
– 脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
– 不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
– 幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
PS: 不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

三、MYSQL事务隔离级别

为了解决事务中的并发问题,同时为了平衡效率,MYSQL提供了不同的隔离级别,可以由使用者自行选择:

  • 读未提交(read-uncommitted)
    事务读不阻塞其他事务读和写,事务写阻塞其他事务写但不阻塞读。可以通过写操作加“持续-X锁”实现。

  • 不可重复读(read-committed)
    事务读不会阻塞其他事务读和写,事务写会阻塞其他事务读和写。可以通过写操作加“持续-X”锁,读操作加“临时-S锁”实现。

  • 可重复读(repeatable-read)
    事务读会阻塞其他事务事务写但不阻塞读,事务写会阻塞其他事务读和写。可以通过写操作加“持续-X”锁,读操作加“持续-S锁”实现。

  • 串行化(serializable)
    “行级锁”做不到,需使用“表级锁”。

其中mysql默认的隔离等级是可重复读(repeatable-read)
blob.jpg

为了解决上面提到的这些问题,可能会使用到如下技术
1. 版本检查
在数据库中保留“版本”字段,跟随数据同时读写,以此判断数据版本。版本可能是时间戳或状态字段。
下例中的 WHERE 子句就实现了简单的版本检查:
UPDATE table SET status = 1 WHERE id=1 AND status = 0;
版本检查能够作为“乐观锁”,解决更新丢失的问题。
2. 锁
– 共享锁与排它锁
– 共享锁(Shared locks, S-locks)
– 共享锁又称读锁,是读取操作创建的锁。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。
– 能给未加锁和添加了S锁的对象添加S锁。对象可以接受添加多把S锁。
– 如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。
– 用法:
SELECT ... LOCK IN SHARE MODE;
在查询语句后面增加LOCK IN SHARE MODE,Mysql会对查询结果中的每行都加共享锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请共享锁,否则会被阻塞。其他线程也可以读取使用了共享锁的表,而且这些线程读取的是同一个版本的数据。

- 排它锁(Exclusive locks, X-locks)

排他锁又称写锁,如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获得排他锁的事务既能读数据,又能修改数据。
– 只能给未加锁的对象添加X锁。对象只能接受一把X锁。加X锁的对象不能再加任何锁。
– 用法:
SELECT … FOR UPDATE;
在查询语句后面增加FOR UPDATE,Mysql会对查询结果中的每行都加排他锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请排他锁,否则会被阻塞。
– 对于insert、update、delete,InnoDB会自动给涉及的数据加排他锁(X);对于一般的Select语句,InnoDB不会加任何锁,事务可以通过以下语句给显示加共享锁或排他锁。
共享锁:SELECT … LOCK IN SHARE MODE;
排他锁:SELECT … FOR UPDATE;

  • 意向锁
    InnoDB还有两个表锁:
    意向共享锁(IS):表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁前必须先取得该表的IS锁。
    意向排他锁(IX):类似上面,表示事务准备给数据行加入排他锁,说明事务在一个数据行加排他锁前必须先取得该表的IX锁。
    意向锁是InnoDB自动加的,不需要用户干预。

  • 临时锁
    锁的时效性,指明了加锁生效期是到当前语句结束还是当前事务结束。

  • 表级锁与行级锁
    锁的粒度,指明了加锁的对象是当前表还是当前行。

  • 悲观锁与乐观锁

    • 悲观锁(Pessimistic Locking)
      悲观锁假定当前事务操纵数据资源时,肯定还会有其他事务同时访问该数据资源,为了避免当前事务的操作受到干扰,先锁定资源。悲观锁需使用数据库的锁机制实现,如使用行级排他锁或表级排它锁。 尽管悲观锁能够防止丢失更新和不可重复读这类问题,但是它非常影响并发性能,因此应该谨慎使用。
    • 乐观锁(Optimistic Locking)
      乐观锁假定当前事务操纵数据资源时,不会有其他事务同时访问该数据资源,因此不在数据库层次上的锁定。乐观锁使用由程序逻辑控制的技术来避免可能出现的并发问题。
      唯一能够同时保持高并发和高可伸缩性的方法就是使用带版本检查的乐观锁。
      乐观锁不能解决脏读的问题,因此仍需要数据库至少启用“读已提交”的事务隔离级别
  • 间隙锁
    当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。
    间隙锁的目的是为了防止幻读,其主要通过两个方面实现这个目的:
    (1)防止间隙内有新数据被插入
    (2)防止已存在的数据,更新成间隙内的数据(例如防止numer=3的记录通过update变成number=5)

  • next-key
    next-key锁其实包含了记录锁和间隙锁,即锁定一个范围,并且锁定记录本身,InnoDB默认加锁方式是next-key 锁,next-key锁锁定的范围为间隙锁+记录锁

MVCC

在数据库的并发处理中,还有一种很重要的方式,MVCC,Multi-Version Concurrency Control,多版本并发控制。

  • 一句话讲,MVCC就是用 同一份数据临时保留多版本的方式 的方式,实现并发控制。这里留意到 MVCC 关键的两个点:
    • 在读写并发的过程中如何实现多版本
    • 在读写并发之后,如何实现旧版本的删除(毕竟很多时候只需要一份最新版的数据就够了)
  • 如果有人从数据库中读数据的同时,有另外的人写入数据,有可能读数据的人会看到『半写』或者不一致的数据。有很多种方法来解决这个问题,叫做并发控制方法。最简单的方法,通过加锁,让所有的读者等待写者工作完成,但是这样效率会很差。MVCC 使用了一种不同的手段,每个连接到数据库的读者,在某个瞬间看到的是数据库的一个快照,写者写操作造成的变化在写操作完成之前(或者数据库事务提交之前)对于其他的读者来说是不可见的。

  • 当一个 MVCC 数据库需要更一个一条数据记录的时候,它不会直接用新数据覆盖旧数据,而是将旧数据标记为过时(obsolete)并在别处增加新版本的数据。这样就会有存储多个版本的数据,但是只有一个是最新的。这种方式允许读者读取在他读之前已经存在的数据,即使这些在读的过程中半路被别人修改、删除了,也对先前正在读的用户没有影响。这种多版本的方式避免了填充删除操作在内存和磁盘存储结构造成的空洞的开销,但是需要系统周期性整理(sweep through)以真实删除老的、过时的数据。对于面向文档的数据库(Document-oriented database,也即半结构化数据库)来说,这种方式允许系统将整个文档写到磁盘的一块连续区域上,当需要更新的时候,直接重写一个版本,而不是对文档的某些比特位、分片切除,或者维护一个链式的、非连续的数据库结构。

  • MVCC 提供了时点(point in time)一致性视图。MVCC 并发控制下的读事务一般使用时间戳或者事务 ID去标记当前读的数据库的状态(版本),读取这个版本的数据。读、写事务相互隔离,不需要加锁。读写并存的时候,写操作会根据目前数据库的状态,创建一个新版本,并发的读则依旧访问旧版本的数据

  • MVCC实现

    • MVCC 使用时间戳(TS)、递增的事务 ID(T)实现事务一致性。

    • MVCC 通过维护多版本数据,保证一个读事务永远不会被阻塞。对象 P 维护有多个版本,每个版本会有一个读时间戳(Read TimeStamp, RTS)和 写时间戳(Write TimeStamp, WTS),事务 Ti 读对象 P 的最新版本,该版本早于事务 Ti 的读时间戳 RTS(Ti)。

    • 事务 Ti 要对 P 执行写操作,如果有其他事务 Tk 同时对 P 操作,则 RTS(Ti)必须要早于 RTS(Tk),即有 RTS(Ti) < RTS(Tk),这样对 Ti 对 P 的写操作才能完成。一般地,如果其他事务拥有 P 的一个更早的读时间戳的情况下,写操作是不能完成的。打个比方就是在存储前面有一道线,只有等你前面的人的完成了他们的事务,你的修改事务才可以提交完成。

    • 重复说一下:每个对象 P 有一个时间戳 TS,如果事务 Ti 想要对 P 执行写操作,(写要先读)事务的读时间戳是 RTS(Ti),如果有其他事务拥有一个比较早的时间戳,有 TS(P) < RTS(Ti),这时事务 Ti 会退出并重新开始。否则,事务 Ti 创建一个 P 的新版本,并设置新版本 P 的时间戳,似的 TS = TS(Ti)。

    • MVCC 系统明显的缺点是会存储多个版本数据的冗余开销。但同时,读操作永不会被阻塞,这对那些以读操作为主的数据库来说非常重要。MVCC 实现了真的快照隔离(snapshot isolation),然后其他的并发控制方法要么是不完整的快照隔离方式,要么需要较高的性能损耗。

  • 参考
    关于MVCC的基础
    数据库并发事务中的问题与解决方案
    mysql锁 innodb下的记录锁,间隙锁,next-key锁
    事务并发的可能问题与其解决方案

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注