MySQL之事务

什么是事务?

事务是一个或一组数据库操作的集合。


 

事务的ACID

原子性:事务要么都做,要么都不做。如果有一个操作执行失败,其余操作必须取消执行。

一致性:事务必须从一个一致性状态变到另一个一致性状态,它和原子性是密不可分的。如果一个事务是将A的值转移到B,那么B增加的同时A必须得减少一致的数量

隔离性:即使是在并发条件下,事务也不可以被其他事务所干扰

持久性:一个事务提交成功后,对数据的改变就应该是永久的了,不可以被其他操作或故障所影响

MySQL是通过WAL方式,来保证数据库事务的一致性和持久性

WAL(Write-Ahead Logging)是一种实现事务日志的标准方法,具体而言就是:

1、修改记录前,一定要先写日志;

2、事务提交过程中,一定要保证日志先落盘,才能算事务提交完成。

通过WAL方式,在保证事务特性的情况下,可以提高数据库的性能。

日志文件:undo和redo

undo用来存放修改前的数据,redo用来存放修改后的数据

假设有A、B两个数据,值分别为1,2,开始一个事务,事务的操作内容为:把1修改为3,2修改为4,那么实际的记录如下(简化): 

A.事务开始. 
B.记录A=1到undo log. 
C.修改A=3. 
D.记录A=3到redo log. 
E.记录B=2到undo log. 
F.修改B=4. 
G.记录B=4到redo log. 
H.将redo log写入磁盘。 
I.事务提交


事务的隔离级别

分为四类:

1、读未提交RU:一个事务可以读取到其他事物未提交的数据,这种现象叫做脏读

2、读已提交RC:又叫不可重复读,一个事务在进行中执行读取同一条数据的前后结果不一致,所以叫做不可重复读,这是oracle及大多数数据库的隔离级别

3、可重复读RR:解决了不重复读,保证了同一个事务里,查询的结果都是事务开始时的状态(一致性)。但是,如果另一个事务同时提交了新数据,本事务再更新时,就会“惊奇的”发现了这些新数据,貌似之前读到的数据是“鬼影”一样的幻觉

4、序列化Serial:对数据库施加表级锁

脏读:脏读是读到了别的事务回滚前的脏数据。比如事务B执行过程中修改了数据X,在未提交前,事务A读取了X,而事务B却回滚了,这样事务A就形成了脏读。

也就是说,当前事务读到的数据是别的事务想要修改成为的但是没有修改成功的数据。

不可重复读:事务A首先读取了一条数据,然后执行逻辑的时候,事务B将这条数据改变了,然后事务A再次读取的时候,发现数据不匹配了,就是所谓的不可重复读了。

也就是说,当前事务先进行了一次数据读取,然后再次读取到的数据是别的事务修改成功的数据,导致两次读取到的数据不匹配,也就照应了不可重复读的语义。

幻读:事务A首先根据条件索引得到N条数据,然后事务B改变了这N条数据之外的M条或者增添了M条符合事务A搜索条件的数据,导致事务A再次搜索发现有N+M条数据了,就产生了幻读。


存储引擎:

mysql内置多种存储引擎,其中主要的有InnoDB,MyISAM两种,前者为默认存储引擎

存储引擎和索引会单独拿出一篇文章来讲述,这里先不深入。


 事务的执行语句:

BEGIN TRANSACTION  ...  COMMIT/ROLLBACK

show variables like 'autocommit';   查询自动提交状态  自动提交状态指不需要显示的开启一个事务,它可以让我们写的每条sql语句自动在事务状态下运行

set autocommit=off;


 innodb下如何解决幻读问题:

1、将隔离级别改为可序列化读

2、RR并没有解决幻读问题,但InnoDB的RR也并未解决幻读(唉,查了很多资料,这里咬文嚼字真心没意义。。),通过多版本并发控制MVCC和间隙锁Next-Key Lock:

MVCC可以理解为是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。

MVCC的实现,是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,每个事务看到的数据都是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。如果之前没有这方面的概念,这句话听起来就有点迷惑。熟悉了以后会发现,这句话其实还是很容易理解的。

      前面说到不同存储引擎的MVCC实现是不同的,典型的有乐观(optimistic)并发控制控制和悲观(pessimistic)并发控制。下面我们通过InnoDB的简化版行为来说明MVCC是如何工作的。

       InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是系统版本号(systemversionnumber)。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。下面看一下在REPEATABLEREAD隔离级别下,MVCC具体是如何操作的。

SELECT    InnoDB会根据以下两个条件检查每行记录:InnoDB只查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。只有符合上述两个条件的记录,才能返回作为查询结果。

INSERT    InnoDB为新插入的每一行保存当前系统版本号作为行版本号。

DELETE    InnoDB为删除的每一行保存当前系统版本号作为行删除标识。

UPDATE   InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。保存这两个额外系统版本号,使大多数读操作都可以不用加锁。这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行。不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。MVCC只在REPEATABLEREAD和READCOMMITTED两个隔离级别下工作。其他两个隔离级别都和MVCC不兼容(4),因为READUNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE则会对所有读取的行都加锁。   ----《高性能MySQL》


InnoDB支持三种行锁定方式:

行锁(Record Lock):锁直接加在索引记录上面,锁住的是key。

间隙锁(Gap Lock):锁定索引记录间隙,确保索引记录的间隙不变。间隙锁是针对事务隔离级别为可重复读或以上级别而已的。

Next-Key Lock :行锁和间隙锁组合起来就叫Next-Key Lock。


 

快照度和当前度

快照读历史数据-mvcc

innodb的默认事务隔离级别是rr(可重复读)。它的实现技术是mvcc。基于版本的控制协议。该技术不仅可以保证innodb的可重复读,而且可以防止幻读。但是它是快照读,也就是读取的数据虽然是一致的,但是数据是历史数据。

当前读最新数据-next key

next-key,也就是结合gap锁与行锁。当使用索引进行插入(或select,update等)的时候,innodb会将当前的节点(record_lock)和上一个节点(gap lock)加锁。这样当进行select的时候,就不允许加写锁。那么在进行该事务的时候,读取的就是最新的数据。当select in share mode时加next-key锁,视查询索引是否唯一而定一片数据的insert和update阻塞,当两个当前读在一个事务中,别的update insert delete就无法打断,也就不会出现重复读和幻读。

实现:

1. 快照读(snapshot read)

简单的select操作(不包括 select ... lock in share mode, select ... for update)

2.当前读(current read)

select ... lock in share mode

select ... for update

insert

update

delete

在RR级别下,快照读是通过MVVC(多版本控制)和undo log来实现的,当前读是通过加record lock(记录锁)和gap lock(间隙锁)来实现的。

在mysql中,提供了两种事务隔离技术,第一个是mvcc,第二个是next-key技术。这个在使用不同的语句的时候可以动态选择。不加lock inshare mode之类的快照读就使用mvcc。否则 当前读使用next-key。mvcc的优势是不加锁,并发性高。缺点是不是实时数据。next-key的优势是获取实时数据,但是需要加锁。

在rr级别下,mvcc完全解决了重复读,但并不能真正的完全避免幻读,只是在部分场景下利用历史数据规避了幻读

对于快照读,mysql使用mvcc利用历史数据部分避免了幻读(在某些场景看上去规避了幻读)

要完全避免,需要手动加锁(InnoDB提供了next-key locks,但并不是对普通索引默认加锁,不然和序列化就没区别了。Serializable 就是 mysql 在 RR 的基础上隐式的自动 + gap lock实现的)将快照读调整为当前读(mysql不会自动加锁),然后mysql使用next-key完全避免了幻读,比如rr下,锁1(0,2,3,4),另一个线程的insert 3即被阻塞,在rc下,另一个线程仍然可以大摇大摆的插入,如本线程再次查询比如count,则会不一致


 

对于主键索引或唯一索引会加gap锁吗?

  • 如果where条件全部命中则不会加gap锁,只会加记录锁,
  • 如果where部分命中或者全未命中,则会加nextkeylock

如下面语句的id列有唯一索引,此时只会对id值为10的行使用记录锁。

select * from t where id = 10 for update;// 注意:普通查询是快照读,是不会加间隙锁的,只有当前读才会考虑间隙锁

如果,上面语句中id列没有建立索引或者是非唯一索引时,则此语句会产生间隙锁。

如果,搜索条件里有多个查询条件(即使每个列都有唯一索引),也是会有间隙锁的

gap锁只会出现在非唯一索引或未走索引的当前读中!

参考:https://segmentfault.com/a/1190000012669504?utm_source=tag-newest

https://blog.csdn.net/weixin_43258908/article/details/89076669等博客

posted @ 2019-08-13 21:32  zohy  阅读(161)  评论(0编辑  收藏  举报