事务:由N步数据库操作序列组成的逻辑执行单元,这系列操作要么全执行,要么全放弃执行。

事物的特性:(ACID特性)

  1. 原子性:事务是应用中不可分割的最小执行体
  2. 一致性:事务执行的结果须使得数据从一个一致的状态,变为另一个一致的状态
  3. 隔离性:各个事物的执行互不干扰,任何事物的内部操作对其它的事务都是隔离的
  4. 持久性:事务一旦提交,对数据所做的任何改变都要记录到永久存储器中

事务的 ACID 特性概念简单,但不是很好理解,主要是因为这几个特性不是一种平级关系:

  • 只有满足一致性,事务的执行结果才是正确的。
  • 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。
  • 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。
  • 事务满足持久化是为了能应对系统崩溃的情况。
    图片说明
    图片与说明来源cyc2018的GitHub分享:https://cyc2018.github.io/CS-Notes/#/README

AUTOCOMMIT

  • MySQL 默认采用自动提交模式。也就是说,如果不显式使用START TRANSACTION语句来开始一个事务,那么每个查询操作都会被当做一个事务并自动提交。(在无并发的条件下保证了原子性,一致性一定满足,因此问题一般出在并发条件下。在并发环境下,事务的隔离性很难保证,因此会出现很多并发一致性问题。

常见的并发异常(事物的隔离性就是为了解决这些问题的)
1.更新错误:第一类更新丢失、第二类更新丢失
2.读取错误:脏读、不可重复读、幻读

第一类更新丢失:某一个事务的回滚, 导致另外一个事务已更新的数据丢失了。
图片说明

第二类丢失更新:某一个事务的提交, 导致另外一个事务已更新的数据丢失了。
图片说明

脏读:某一个事务, 读取了另外一个事务未提交的数据。
图片说明

不可重复读:某一个事务, 对同一个数据前后读取的结果不一致。(单条数据的update和delete行为)
图片说明

幻读:某一个事务, 对同一个表前后查询到的行数不一致。(多条数据的insert行为)
图片说明

很多人容易搞混不可重复读和幻读,确实这两者有些相似。但不可重复读重点在于update和delete,而幻读的重点在于insert。
如果使用锁机制来实现这两种隔离级别,在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复 读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会 发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要Serializable隔离级别 ,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力。
所以说不可重复读和幻读最大的区别,就在于如何通过锁机制来解决他们产生的问题。
作者:HelloDake
链接:https://www.jianshu.com/p/4b6e0103a13c
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

为了解决这些常见的并发异常,设置了一些隔离级别(分层级解决并发异常,按需要解决对应的某些异常而不一定是全部,因为解决异常的代价是性能的降低)

  1. Read Uncommitted
  2. Read Committed
  3. Repeatable Read
  4. Serializable
    事务隔离级别
    事实上,1.读取未提交因为安全性最低,通常不去使用。4.串行化安全性高但是性能很低(银行等某些对安全性极高的业务情况可能会使用)(MySQL的默认隔离级别为3,实现4需要加表锁,效率很低)。2、3的使用更为常见,3可重复读中的幻读不能规避,但是幻读作为查询多条数据一般是在统计的情况下使用,一般是容易接受的(选择在深夜4点进行,或者多次统计)2.读取已提交数据主要也是在性能要求很高的情况下使用的,牺牲一些安全性。

事务隔离性的实现机制

  1. 悲观锁(数据库):悲观的看待问题,认为
  • 共享锁(S锁,又叫读锁) 事务A对某数据加了共享锁后,该事务只读不可写,其他事务只能对该数据加共享锁,但不能加排他锁。
  • 排他锁(X锁,又叫写锁) 事务A对某数据加了排他锁后,该事务不能读也不能写,其他事务对该数据既不能加共享锁,也不能加排他锁。

    注:要使用悲观锁,我们必须关闭mysql数据库的自动提交属性,因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。我们可以使用命令设置MySQL为非autocommit模式:set autocommit=0;设置完autocommit后,我们就可以执行我们的正常业务了。

    读未提交:可以通过写操作加“持续-X锁”实现。
    读已提交:可以通过写操作加“持续-X”锁,读操作加“临时-S锁”实现。
    可重复读:可以通过写操作加“持续-X”锁,读操作加“持续-S锁”实现。
    串行化:“行级锁”做不到,需使用“表级锁”。

  1. 乐观锁(自定义):乐观的看待问题,即使并发了通常也不会有问题,当提交时我看数据是否变化过了,如果变化了说明有人改了,我就放弃操作,如果没人改我就提交。
  • 版本号、时间戳等 在更新数据前,检查版本号是否发生变化。若变化则取消本次更新,否则就更新数据(版本号+1)。

    注:乐观锁不能解决脏读的问题,因此仍需要数据库至少启用“读已提交”的事务隔离级别

串行化使用行级锁无法实现,必须要表级锁,行级锁和表级锁属于不同的锁粒度,什么是锁和锁粒度呢?

为什么要加锁?加锁是为了防止不同的线程访问同一共享资源造成混乱。

  • 打个比方:人是不同的线程,卫生间是共享资源
  • 你在上洗手间的时候肯定要把门锁上吧,这就是加锁,只要你在里面,这个卫生间就被锁了,只有你出来之后别人才能用。想象一下如果卫生间的门没有锁会是什么样?

什么是加锁粒度呢?所谓加锁粒度就是你要锁住的范围是多大。

数据库的锁粒度讲解:

数据库引擎具有多粒度锁定,允许一个事务锁定不同类型的资源。 为了尽量减少锁定的开销,数据库引擎自动将资源锁定在适合任务的级别。 锁定在较小的粒度(例如行)可以提高并发度,但开销较高,因为如果锁定了许多行,则需要持有更多的锁。 锁定在较大的粒度(例如表)会降低了并发度,因为锁定整个表限制了其他事务对表中任意部分的访问。 但其开销较低,因为需要维护的锁较少。
数据库引擎通常必须获取多粒度级别上的锁才能完整地保护资源。 这组多粒度级别上的锁称为锁层次结构。 例如,为了完整地保护对索引的读取,数据库引擎实例可能必须获取行上的共享锁以及页和表上的意向共享锁。
MySQL有三种锁的级别:页级、表级、行级。

  • MyISAM和MEMORY存储引擎采用的是表级锁(table-level locking);BDB存储引擎采用的是页面锁(page-level locking),但也支持表级锁;InnoDB存储引擎既支持行级锁(row-level locking),也支持表级锁,但默认情况下是采用行级锁。

MySQL这3种锁的特性可大致归纳如下:

  • 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
  • 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
  • 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。表级锁加锁速度快,但冲突多,行级冲突少,但加锁速度慢。因此,采取了折衷的页级锁,一次锁定相邻的一组记录

什么是死锁:
死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
死锁

什么是锁冲突:
参考:https://blog.csdn.net/u011820505/article/details/84807264