数据库原理二---MySQL事务与锁

2021/10/2 19:12:38

本文主要是介绍数据库原理二---MySQL事务与锁,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

数据库事务的四大特性

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

  • 一致性C
    执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的

  • 隔离性I
    并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的

  • 持久性D一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响

原子性和持久性定义了事务的边界,行为的开始和结束,一致性和隔离性即是对事务中间状态的管理。

ACID的核心是C,大家都是为得到C而提出的不同纬度的限制和规范
A确定一个功能的完整性,D对状态负责,I作为C的等级系数,不同的I策略会出现不同的C。
隔离性I的设定就是对一致性不同程度的破坏,事实上,如果我们顺序对数据进行读写,ACD是完全可以保证的,但这样效率会非常低下。
选择合适的隔离策略是为了在一致性和性能之间平衡,取得最好的综合表现。

脏读、幻读、不可重复读

  • 脏读(Drity Read)
    某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。

  • 不可重复读(Non-repeatable read)
    在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。

  • 幻读(Phantom Read)
    在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。

幻读,并不是说两次读取获取的结果集不同,幻读侧重的方面是某一次的 select 操作得到的结果所表征的数据状态无法支撑后续的业务操作。
更为具体一些:select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读。

事务的隔离级别

为了达到事务的四大特性,数据库定义了4种不同的事务隔离级别

  • READ-UNCOMMITTED(读取未提交)
    最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。

  • READ-COMMITTED(读取已提交)
    允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。

  • REPEATABLE-READ(可重复读)
    对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。

  • SERIALIZABLE(可串行化)
    最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。

Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别

MySQL的锁机制

MySQL数据库锁机制(乐观锁,悲观锁)

数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。
乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段
要把乐观并发控制和悲观并发控制狭义的理解为DBMS中的概念,更不要把他们和数据中提供的锁机制(行锁、表锁、页锁、排他锁、共享锁)混为一谈。

悲观锁

悲观并发控制(悲观锁)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。
如果一个事务执行的操作对其行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。
悲观锁主要用于数据争用激烈的环境。

悲观锁的具体流程
在对任意记录进行修改前,先尝试为该记录加上排他锁。
如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。
如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
期间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或者直接抛出异常。

悲观锁的优点和不足
悲观锁实际上是采取了“先取锁在访问”的策略,为数据的处理安全提供了保证。
但是在效率方面,由于额外的加锁机制产生了额外的开销,并且增加了死锁的机会。
并且降低了并发性;当一个事物所以一行数据的时候,其他事物必须等待该事务提交之后,才能操作这行数据。

乐观锁

乐观并发控制(乐观锁)是一种并发控制的方法。它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。
在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。

乐观锁相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。
在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。

数据版本,为数据增加的一个版本标识。当读取数据时,将版本标识的值一同读出,数据每更新一次同时对版本标识进行更新。
当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的版本标识进行比对,如果数据库表当前版本号与第一次取出来的版本标识值相等,则予以更新,否则认为是过期数据。

乐观锁的优点和不足
乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。
但如果直接简单这么做,还是有可能会遇到不可预期的结果,例如两个事务都读取了数据库的某一行,经过修改以后写回数据库,这时就遇到了问题。

MySQL数据锁机制(行锁,页锁,表锁)

MySQL数据锁按照锁的粒度划分可分为:行锁、表锁、页锁

  • 行锁
    行级锁是MySQL中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。
    行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。有可能会出现死锁的情况。
    行级锁按照使用方式分为共享锁和排他锁。

    • 共享锁
      共享锁允许一个事务读数据,不允许修改数据,如果其他事务要对该行加锁,只能加共享锁。即共享锁允许多个线程同时获取一个锁,一个锁可以同时被多个线程拥有。
    • 排他锁
      排他锁是修改数据时加的锁,可以读取和修改数据,一旦一个事务对该行数据加锁,其他事务将不能再对该数据加任务锁。即一个锁在某一时刻只能被一个线程占有,其他线程必须等待锁被释放之后才可能获取到锁。
  • 表锁
    表级锁是MySQL锁中粒度最大的一种锁,表示当前的操作对整张表加锁,资源开销比行锁少,不会出现死锁的情况,但是发生锁冲突的概率很大。
    被大部分MySQL引擎支持,MyISAM和InnoDB都支持表级锁,但是InnoDB默认是行级锁。

  • 页锁
    页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级锁冲突少,但速度慢。
    所以取了折中的页级,一次锁定相邻的一组记录。BDB支持页级锁。

隔离级别与锁的关系

在Read Uncommitted级别下,读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突

在Read Committed级别下,读操作需要加共享锁,但是在语句执行完以后释放共享锁;
在悲观锁的情况下,由于加了共享锁,所以事务1对数据A修改时,事务2不会读入数据A,不会出现脏读。

在Repeatable Read级别下,读操作需要加共享锁,但是在事务提交之前并不释放共享锁,也就是必须等待事务执行完毕以后才释放共享锁。
在悲观锁的条件下,事务依次执行,避免了在单条事务执行期间数据发生改变,不会出现不可重复读

SERIALIZABLE 是限制性最强的隔离级别,因为该级别锁定整个范围的键,并一直持有锁,直到事务完成。

乐观锁的情况下,在下文MVCC中补充。

当前读&快照读

  • 当前读
    读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。

  • 快照读
    像不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别的快照读会退化成当前读,之所以出现快照读的情况是基于提高并发性能的考虑。
    快照读的实现是基于MVCC

MVCC

MVCC(Multiversion concurrency control),多版本并发控制,提供并发访问数据库时,对事务内读取的到的内存做处理,用来避免写操作堵塞读操作的并发问题。
一个支持MVCC的数据库,在更新某些数据时,并非使用新数据覆盖旧数据,而是标记旧数据是过时的,同时在其他地方新增一个数据版本。因此,同一份数据有多个版本存储,但只有一个是最新的。

MVCC提供了 时间一致性的 处理思路,在MVCC下读事务时,通常使用一个时间戳或者事务ID来确定访问哪个状态的数据库及哪些版本的数据。读事务跟写事务彼此是隔离开来的,彼此之间不会影响。假设同一份数据,既有读事务访问,又有写事务操作,实际上,写事务会新建一个新的数据版本,而读事务访问的是旧的数据版本,直到写事务提交,读事务才会访问到这个新的数据版本。

MVCC有两种实现方式
第一种实现方式是将数据记录的多个版本保存在数据库中,当这些不同版本数据不再需要时,垃圾收集器回收这些记录。这个方式被PostgreSQL和Firebird/Interbase采用,SQL Server使用的类似机制,所不同的是旧版本数据不是保存在数据库中,而保存在不同于主数据库的另外一个数据库tempdb中。
第二种实现方式只在数据库保存最新版本的数据,但是会在使用undo时动态重构旧版本数据,这种方式被Oracle和MySQL/InnoDB使用。

MVCC在MySQL中只在 Read Committed 和 Repeatable Read两个隔离级别下工作。其他两个隔离级别和MVCC不兼容,Read Uncommitted总是读取最新的记录行,不需要MVCC的支持;Serializable 则会对所有读取的记录行都加锁,单靠MVCC无法完成。

InnoDB的MVCC实现机制

InnoDB的MVCC实现,是通过保存数据在某个时间点的快照来实现的。一个事务,不管其执行多长时间,其内部看到的数据是一致的。也就是事务在执行的过程中不会相互影响。
InnoDB的MVCC,通过在每行记录后面保存两个隐藏的列来实现:一个保存了行的创建时间,一个保存行的过期时间(删除时间),当然,这里的时间并不是时间戳,而是系统版本号,每开始一个新的事务,系统版本号就会递增。

RC , RR 级别下的 InnoDB MVVC实现的快照读有什么不同

在 RR 级别下的某个事务的对某条记录的第一次快照读会创建一个快照及 Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的是同一个 Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个 Read View,所以对之后的修改不可见;
即 RR 级别下,快照读生成 Read View 时,Read View 会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见。(所以在RR级别下,不会出现不可重复读)
而在 RC 级别下的,事务中,每次快照读都会新生成一个快照和 Read View , 这就是我们在 RC 级别下的事务中可以看到别的事务提交的更新的原因(所以在RC级别下,也可能出现不可重复读)



这篇关于数据库原理二---MySQL事务与锁的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程