[InnoDB] 事务特性ACID、隔离级别、悲观锁乐观锁

 

事务特性

隔离级别越低,事务请求的锁越少或保持锁的时间就越短。

事务具有的4个特性。

原子性(atomicity)

1个事务要么全部提交成功,要么全部失败回滚,不能只执行其中一部分操作。

一致性(consistency)

事务将数据库从一种状态转变为一种一致的状态。

如果数据库运行中发生故障,有些事务被迫中断,未完成的事务对数据库所做的修改有一部分已经写入数据库,这时数据库就处于不一致的状态。

完整性约束也不能被破坏。

隔离性(isolation)

是指在并发环境中,事务是相互隔离的,1个事务的执行不能被其它事务干扰。

不同事务操作的数据对其它并发的事务是隔离的。

持久性(durability)

事务一旦提交,那么它对该数据的变更就会永久保存在数据库中。

即使发生故障,数据库也能保证恢复后提交的数据都不会丢失。

若不是数据库本身发生的故障,而是外部原因导致存储介质遭到破坏,那么数据可能会丢失。

事务隔离级别

查看当前会话事务隔离级别,系统隔离级别:

mysql> select @@tx_isolation, @@global.tx_isolation;
+-----------------+-----------------------+
| @@tx_isolation  | @@global.tx_isolation |
+-----------------+-----------------------+
| REPEATABLE-READ | REPEATABLE-READ       |
+-----------------+-----------------------+

SQL标准定义了4个事务隔离级别:

未授权读,读未提交(read uncommited)

如果一个事务已经开始写数据,则另外一个事务则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过排他写锁实现。这样就避免了更新丢失,却可能出现脏读。也就是说事务B读取到了事务A未提交的数据。

授权读取,读提交(read commited) (实际采用)

读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。该隔离级别避免了脏读,但是却可能出现不可重复读。事务A事先读取了数据,事务B紧接了更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。

可重复读(repeatable read)

可重复读是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,即使第二个事务对数据进行修改,第一个事务两次读到的的数据是一样的。这样就发生了在一个事务内两次读到的数据是一样的,因此称为是可重复读。

有事务进行写时,允许其它事务读旧版的数据,但不允许其它事务写。这样避免了不可重复读取和脏读,但是有时可能出现幻象读

读取数据的事务,可以通过共享读锁排他写锁实现。

InnoDB存储引擎通过Next-Key Lock避免幻读问题

序列化(serializable)

提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行。如果仅仅通过行级锁是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。序列化是最高的事务隔离级别,同时代价也花费最高,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻象读。

InnoDB会对每个SELECT语句后自动加上LOCK IN SHARE MODE,即为每个读取操作加一个共享锁。读占用了锁,对非一致性读不再给予支持。

SERIALIABLE事务隔离级别主要用于InnoDB存储引擎的分布式事务

悲观锁、乐观锁

悲观锁

悲观锁,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态

悲观锁的原理就是,当我们在查询出表信息后就把当前的数据锁定,直到我们修改完毕后再解锁。那么在这个过程中,因为表被锁定了,就不会出现有第三者来对其进行修改了。需要注意的是,要使用悲观锁,我们必须关闭mysql数据库的自动提交属性,因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。

总结

悲观锁并不是适用于任何场景,它也有它存在的一些不足,因为悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。如果加锁的时间过长,其他用户长时间无法访问,影响了程序的并发访问性,同时这样对数据库性能开销影响也很大,特别是对长事务而言,这样的开销往往无法承受。所以与悲观锁相对的,我们有了乐观锁。

乐观锁

乐观锁(Optimistic Locking) 相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以只会在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回用户错误的信息,让用户决定如何去做。实现乐观锁一般来说有以下2种方式:

  • 使用数据版本(Version):记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的”version”字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
  • 使用时间戳:乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。

当主键不明确时(包含范围选取),使用table lock,主键明确时,使用row lock

区别

不可重复读、幻象读的区别

不可重复读 幻象读
2次读取之间,其它事务对于某一行数据的修改所造成 2次读取之间,其它事务添加、删除数据行
行级锁可以解决 表级锁才可解决
只需要锁住满足条件的记录 需要锁住满足条件及其相近的记录

隔离级别比较

隔离级别 脏读 不可重复读 幻象读
READ_UNCOMMITED o o o
READ_COMMITED x o o
REPEATABLE READ x x o
SERIALIZABLE x x x

悲观锁、乐观锁的区别

  • 悲观锁 乐观锁
  • 原理 对数据加锁,不允许其它事务访问该数据 认为数据不会冲突,仅在数据进行提交时进行冲突检测
    实现 数据列增加标志区别