• 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变得可见。这也满足读已提交的隔离条件,并会发生不可重复读问题。
  • 相关阅读:
    HDU4628+状态压缩DP
    Javascript 去掉字符串前后空格的五种方法
    Javascript 数组之判断取值和数组取值
    ASP.NET MVC 出现错误 “The view 'XXX' or its master was not found or no view engine support”
    ASP.NET MVC 页面调整并传递参数
    ASP.NET MV3 部署网站 报"Could not load file or assembly ' System.Web.Helpers “ 错的解决方法
    ASP.NET MVC 控制器向View传值的三种方法
    CSharp 如何通过拼接XML调用存储过程来查询数据
    SQLServer : EXEC和sp_executesql的区别
    关于SQLServer2005的学习笔记—异常捕获及处理
  • 原文地址:https://www.cnblogs.com/k5210202/p/13073899.html
Copyright © 2020-2023  润新知