zoukankan      html  css  js  c++  java
  • mysql事务和锁

    Innodb引擎的锁机制

    InnoDB与MyISAM的最大不同有两点:一是支持事务(TRANSACTION);二是采用了行级锁

    事务的ACID

    事务是为了实现业务上完整性而实现了,他可以由多条sql语句组成,这些语句要么全部成功,否则发生任何错误都将会回滚。事务具有4个属性。称为事务的ACID属性

    • 原子性(Actomicity):事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
    • 一致性(Consistent):在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持完整性;事务结束时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的。并且在操作数据的过程中,无论使用并行或者是串行的方式执行,得到的最后的结果应该是一致的。
    • 隔离(Isolation):隔离性是描述多个事务之间操作共同资源时如何处置的问题,选择哪一种隔离方式。数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。
    • 持久性(Durable):事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。

    并发事务带来的问题

    相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从而可以支持可以支持更多的用户。但并发事务处理也会带来一些问题,主要包括以下几种情况。

    • 更新丢失(Lost Update):当两个事务同时更新一个数据时候,由于同时读取数据表中的数据,并且两个事务都以自己读到的数据为基础进行更新,在写入表时候,后写入的值会覆盖先写入的值,原本应该两次更新的操作丢失了一次。“更新丢失”通常应该是完全避免的,但不能单靠数据库控制器解决,而应该时应用层的责任。
    • 脏读(Dirty Reads):一个事务读取到了另一个事务未提交的数据,该数据可能会发生回滚而失效。
    • 不可重复读(Non-Repeatable Reads):一个事务执行时,前后使用相同的查询语句,第二次读取时发现数据被修改,两次读取到的数据不一致。
    • 幻读(Phantom Reads):一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。

    不可重复读和幻读都是两次的查询结果不同,但是不可重复读是倾向于行数据被修改,这个更好解决,对该行加锁即可。而幻读强调的是执行相同的查询时增加或者删除了一整行的数据。处理这个问题锁单行是无法解决的,需要通过锁表实现。这也是两个问题分开提出的原因。

    事务的隔离级别

    上面的问题在所有关系型数据中都是存在,所以在ISO/ANSI SQL92标准中定义了4个事务隔离级别来分别解决以上的问题。

    隔离级别

    脏读

    不可重复读

    幻读

    读未提交

    存在

    存在

    存在

    读已提交

    解决

    存在

    存在

    可重复读

    解决

    解决

    存在

    序列化

    解决

    解决

    解决

    MySQL的隔离级别的具体实现和上面sql标准中定义的有一个区别的地方,可重复读的隔离级别下,MySQL解决了幻读的问题,而MySQL的默认隔离级别为可重复读,该级别可以避免上述的所有问题

    隔离级别的实现

    实现事务的隔离级别,一般可以通过两种方式实现

    • 使用锁,通过行锁或者表锁来保证共享的数据只能被一个事务获取,其他事务只能等待前一个事务完成后才能进行处理。
    • MVCC并发控制通过一定机制生成一个与请求时间点一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一致性读取。从用户的角度,好像是数据库可以提供同一数据的多个版本,因此,这种技术叫做数据多版本并发控制(MultiVersion Concurrency Control,简称MVCC或MCC),也经常称为多版本数据库。

    Innodb的锁机制实现

    行锁

    innodb实现了两种类型的行锁:

    • 共享锁(读锁):一个事务获取了该行数据的共享读锁, 其他事务还可以继续获取该数据的共享读锁,但是无法获取排他的写锁。
    • 排他锁(写锁):任意一个事务获取了该行数据的排他写锁后,该行数据共享锁和排他锁都无法被获取。直到该锁被释放。

    一个事务中通过update,insert,delete等语句去修改一行数据,MySQL都会自动尝试的去获取一个排他性的写锁,这个获取锁的操作就会与其他获取同一个锁的事务竞争,只会有一个事务得到锁而执行操作,另一个继续竞争。而直接使用select 语句进行查询是不用获取锁的,可以随时进行查询,但是这也可能会造成获取到其他事务正在修改的数据(如果并不关心这一点可以使用该方式)。也可以在查询数据时使用锁去获取。

    select * from table ..... for update:对获取的这些数据添加一个排他的写锁,在事务提交前,其他事务都不能读写这些被加锁的数据。

    select * from table ..... lock in share mode:对获取的这些数据添加一个共享读锁,其他事务可以读取,但是不能修改数据。

    行锁的实现方式

    MySQL行锁是通过给索引上的索引项加锁来实现的,这一点与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。MySQL这种行锁实现特点意味着:只有通过索引条件检索数据,才使用行级锁,否则,InnoDB将使用表锁! 

    例如 select * from table where name=tom,如果name字段没有索引,将会进行全表扫描来获得数据,innodb在扫描之前将会锁住整个表,直到事务提交。如果name字段有索引,MySQL实际会在索引上锁,但是有多个索引时,也会对数据进行上锁,以避免使用不同的索引获取值时出现数据的并发访问问题。

    但是并不是使用了索引字段进行查询就能保证查询一定走了索引,某些情况下,使用索引可能没有直接全表扫描的效率高,应该使用explain查看查询计划

    意向锁(表锁)

    意向锁也分为两种:

    • 意向共享锁(IS):事务打算给数据行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
    • 意向排他锁(IX):事务打算给数据行加排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。

    意向锁是InnoDB自动加的,不需用户干预。例如进行全表扫描时,将会自动意向锁加锁。同时获取表锁前,也会确保该表中不存在行锁才能获取,否则任然可能会造成锁冲突。这四种锁的冲突与兼容情况如下:

    X

    IX

    S

    IS

    X

    冲突

    冲突

    冲突

    冲突

    IX

    冲突

    兼容

    冲突

    兼容

    S

    冲突

    冲突

    兼容

    兼容

    IS

    冲突

    兼容

    兼容

    兼容

     

    其他事务想要获取的锁的类型,必须和当前事务以获取锁的类型相互兼容,其他事务才能正常的获取,否则将会阻塞等待锁释放。

    间隙锁

    当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的 索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁 (Next-Key锁)。

    例如我们进行范围查询时候, select * from id > 20此时我们指定的范围是id > 20,表中的这些数据都会被加锁而避免被修改,同时,他们这些数据之间间隙也是会被加锁的,如果插入一个id = 30的数据(表中没有改数据),人仍然会被阻塞而禁止插入,这也是解决幻读问题一种实现。

    所以避免范围查询也是一个数据库优化方式,如果可以,应该尽量的减小这个范围, 最好使用相等的条件查询。

    几种使用锁的情况

    以上的现象是在MySQL的默认隔离级别repeated read下,使用read commited隔离级别时有不同表现。

    read commited 隔离下锁的表现:

    • 通过主键或者索引加锁,只会对查询的行加锁,其余未加锁行不受任何影响,其他事务可以自由访问。
    • 使用for update和全表扫描的方式加锁,同样只会锁住被查出的行,其余行没有被写锁锁住,可以正常获取。
    • 插入数据不会受锁影响,可直接插入数据,也不能避免幻读

    repeated read隔离级别下锁表现

    • 通过主键或者索引加锁,只会对查询的行加锁,其余未加锁行不受任何影响,其他事务可以自由访问。
    • 使用全表扫描的方式加排他锁,表所有的行都会被锁住,即使不是被查询的数据,因为使用了表锁。
    • 插入数据受到间隙锁的控制。加锁时候,根据where或者其他条件进行的查询,在这个where条件的范围内部的数据,都不能被修改,并且其他事务想要插入一条数据,必须不在这个范围内部,否则将会触发锁而阻塞。
    • 全表扫描的情况下,全部的数据和间隙都会被锁住,也就无法插入数据,从而避免幻读。

    同时还需要注意以下问题:

    • 当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
    • 由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。例如使用查询条件where name=tom and age=18where name=tom and age=20,如果name字段建立了索引,虽然查询行数据时两行数据,但是由于使用的索引字段的条件时相同的,所以任然会锁冲突。

    查看锁争抢情况

    通过一个MySQL内部状态量来查看争用情况show status like 'innodb_row_lock%';

    +-------------------------------+-------+
    | Variable_name                 | Value |
    +-------------------------------+-------+
    | Innodb_row_lock_current_waits | 0     |
    | Innodb_row_lock_time          | 45336 |
    | Innodb_row_lock_time_avg      | 6476  |
    | Innodb_row_lock_time_max      | 12442 |
    | Innodb_row_lock_waits         | 7     |
    +-------------------------------+-------+

    如果InnoDB_row_lock_waits和InnoDB_row_lock_time_avg的值比较高,说明锁竞争越严重(以上数据是测试了许多锁竞争后情况后的数值),我们需要分析锁竞争的原因,使用方案优化

    MVCC并发控制的实现

    仅仅使用上面的锁方案,在实现各个级别隔离性前提下,只能做到多个事务同时并发读取,而多个事务同时读写或多个事务同时写都会触发锁竞争,使得这些事务串行化,从而降低了并发量。而使用mvcc版本,可以实现同时读写时的并发执行。

    mvcc想要实现的是读写之间的并发,也就是一个事务在读取内容的时候,可以有其他的一个写操作的事务(仅此一个,否则多个写操作并发)在执行,并且这不会破坏数据的一致性,同时达到隔离级别的要求。

    快照读和当前读

    数据库的更改会在undo log中形成一个版本链的关系,每次insert 或 update一个数据,都会创建该记录的新的版本 (delete也看作update操作,他会把改变记录的deleted位标记),并在该记录的行信息中修改该记录的事务的唯一id,将来作为事务可见性的判断。基于这个版本链,在MVCC并发控制中,每次写操作会创建一个新的版本,而读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。

    快照读:当开启一个事务之后,第一次执行普通的select语句时(不加锁的select语句),会将本次在版本连中最新的一次已提交的记录做一次快照并暂存,且这个快照只有该事务可见,其他事务可以在原始的版本链中修改数据,但该事务的快照内容不会修改。

    当前读:读取的是版本链中记录的最新的已提交的版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。

    快照读和当前读分别包括以下语句:

    快照读:使用普通的 select 语句(不包括 select ... lock in share mode, select ... for update)都是快照读。

    当前读:select ... lock in share modeselect ... for update,insertupdatedelete 语句,这些语句获取的是数据库中的最新数据。

    InnoDB存储引擎默认事务隔离级别是RR(可重复读),是通过 "行级锁+MVCC"一起实现的,正常读的时候不加锁,写的时候加锁。而 MCVV 的实现依赖:隐藏字段、Read View、Undo log

    隐藏字段

    列名

    是否必须

    占用空间

    描述

    row_id

    6

    行唯一标识,递增

    transaction_id

    6

    表示最近一次对本记录行作修改(insert | update)的事务ID,delete操作是一个update操作,非真删除,会更改删除位,deleted

    roll_pointer

    7

    回滚指针,指向当前记录行的undo log信息

     

    Read View

    这是一个读视图,也就是一个快照,当开启一个事务之后,使用普通的select第一次读取内容时,就会创建一个readview快照信息,记录当前时间点一些信息。主要包括

    • 当前出现过的最大事务id+1,
    • 当前活跃的事务列表,逆序排列,trx_ids
    • 活跃事务列表中的最小id
    • 创建的当前事务的id。

    Read view主要配合undo log版本链做数据的可见性分析。

    Undo log

    undo log使用一个版本链来记录当前版本的数据和历史版本的数据,事务可以进行回滚也是利用undo log来实现的。

    undo log主要会记录数据的insert和update操作,至于delete也是update操作,是将标记删除位的假删除。每次操作都会创建创建一个新的版本数据,并在数据的隐藏字段中记录了修改该数据事务id。这样即使该事务操过程中其他的事务对数据进行了修改,该线程比较这个事务id的不同,可以判断出哪个版本的数据是在本事务开启后才进行修改,从而不去读取哪些数据,也就是那些数据对本事务来说是不可见的,然后从版本链中找到由本事务最新修改的内容,在该事务读取数据的时候,不用对数据加锁。

    简要的过程为

    undo log中记录了一个版本链信息,数据的每一个版本都记录了修改他的事务id,当在一个事务中使用普通的查询语句时,将会生成一个Readview信息,这里有两种情况:

    • 在RR,可重复读的模式下,readview的内容是不变,假如事务1第一次读取了数据,创建read view并将活跃事务2记录,然后事务2写入数据将其修改了并提交,事务1从新读取数据,由于视图没有修改,会将事务2修改的数排除在外,读取的还是事务1第一次读取的内容。这就不会出现不可重复读的情况,满足可重复读的隔离条件要求。
    • 在RC,读已提交的模式下,每次新的查询都会生成新的read view,事务1第一次查询,生成第一个视图,并将事务2活跃线程记录,在其提交前都无法读取事务2修改的数据,当事务2提交后,事务2已经不是活跃线程,重新建立的视图中的活跃事务列表中没有记录事务2,则事务2的数据对事务1变得可见。这也满足读已提交的隔离条件,并会发生不可重复读问题。
  • 相关阅读:
    浏览器缓存机制
    vim 同时操作多行
    webpack打包性能优化
    测试 IE 浏览器兼容性 VirtualBox + modern.ie
    react-redux 学习笔记
    Redux 学习笔记
    Mac 下抓包工具 Charles 修改特定请求
    PyUsb的使用记录--window中
    Java 通过Jna调用dll路径问题
    OpenJdk14精简jdk运行javaFx样式丢失问题
  • 原文地址:https://www.cnblogs.com/k5210202/p/13073899.html
Copyright © 2011-2022 走看看