• mysql 3 锁分类和介绍


    本文章向大家介绍MySQL锁详细讲解,包括数据库锁基本知识、表锁、表读锁、表写锁、行锁,事务的隔离级别、悲观锁、乐观锁、间隙锁GAP、死锁等等

    数据库锁知识

    不少人在开发的时候,应该很少会注意到这些锁的问题,也很少会给程序加锁(除了库存这些对数量准确性要求极高的情况下),即使我们不会这些锁知识,我们的程序在一

    般情况下还是可以跑得好好的。因为这些锁数据库隐式帮我们加了,只会在某些特定的场景下才需要手动加锁。

    • 对于UPDATE、DELETE、INSERT语句,InnoDB自动给涉及数据集加排他锁(X)
    • MyISAM在执行查询语句SELECT前,会自动给涉及的所有表加读锁,在执行增、删、改操作前,会自动给涉及的表加写锁,这个过程并不需要用户干预
    首先要明确的是,用户很少手动加表锁

    首先,从锁的粒度,我们可以分成两大类:

    • 表锁
      • 开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低
    • 行锁
      • 开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高

    不同的存储引擎支持的锁粒度是不一样的:InnoDB行锁和表锁都支持、MyISAM只支持表锁!InnoDB只有通过索引条件检索数据才使用行级锁,否则,InnoDB使用表

    锁也就是说,InnoDB的行锁是基于索引的!

    一  表锁

    表锁下又分为两种模式

    • 表读锁(Table Read Lock)
    • 表写锁(Table Write Lock)
    • 从下图可以清晰看到,在表读锁和表写锁的环境下:读读不阻塞,读写阻塞,写写阻塞
    1. 读读不阻塞:当前用户在读数据,其他的用户也在读数据,不会加锁
    2. 读写阻塞:当前用户在读数据,其他的用户不能修改当前用户读的数据,会加锁!
    3. 写写阻塞:当前用户在修改数据,其他的用户不能修改当前用户正在修改的数据,会加锁

    从上面已经看到了:读锁和写锁是互斥的,读写操作是串行

    • 如果某个进程想要获取读锁,同时另外一个进程想要获取写锁。在mysql中,写锁是优先于读锁的
    • 写锁和读锁优先级的问题是可以通过参数调节的:max_write_lock_count和low-priority-updates
    注:

     

    二  行锁

    InnoDB和MyISAM有两个本质的区别:InnoDB支持行锁、InnoDB支持事务

    InnoDB实现了以下两种类型的行锁:

    • 共享锁(S锁、读锁):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。即多个客户可以同时读取同一个资源,但不允许其他客户修改
    • 排他锁(X锁、写锁):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的读锁和写锁。写锁是排他的,写锁会阻塞其他的写锁和读锁

    另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁:

    • 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
    • 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
    • 意向锁也是数据库隐式帮我们做了,不需要程序员关心!

    对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁;

    事务可以通过以下语句显式给记录集加共享锁或排他锁。

    共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
    排他锁(X):SELECT * FROM table_name WHERE ... FOR UPDATE

    顺序为
    1 begin//开启事务
    2 加锁
    3 处理数据
    4 commit//事务提交
    
    

    InnoDB行锁的实现原理

          InnoDB行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁

    在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。

    
    

    下面通过一些实际例子来加以说明。

    
    

    (1)在不通过索引条件查询的时候,InnoDB确实使用的是表锁,而不是行锁。

    (2)由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。

    (3)当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。

    (4)即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。

     间隙锁(Next-Key锁  GAP)

          当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;

    对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。

    例:

    假如emp表中只有101条记录,其empid的值分别是 1,2,...,100,101,下面的SQL:

    mysql> select * from emp where empid > 100 for update;

    是一个范围条件的检索,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。

    InnoDB使用间隙锁的目的:

    (1)防止幻读,以满足相关隔离级别的要求。对于上面的例子,要是不使用间隙锁,如果其他事务插入了empid大于100的任何记录,那么本事务如果再次执行上述语句,就会发生幻读;

    (2)为了满足其恢复和复制的需要。

    很显然,在使用范围条件检索并锁定记录时,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下这可能会对性能造成很大的危害。

    除了间隙锁给InnoDB带来性能的负面影响之外,通过索引实现锁定的方式还存在其他几个较大的性能隐患:

    (1)当Query无法利用索引的时候,InnoDB会放弃使用行级别锁定而改用表级别的锁定,造成并发性能的降低;

      (2)当Query使用的索引并不包含所有过滤条件的时候,数据检索使用到的索引键所执行的数据可能有部分并不属于该Query的结果集的行列,但是也会被锁定,因为间隙锁锁定的是一个范围,而不是具体的索引键;

    (3)当Query在使用索引定位数据的时候,如果使用的索引项一样但访问的数据行不同的时候(索引只是过滤条件的一部分),一样会出现锁冲突。

    因此,在实际应用开发中,尤其是并发插入比较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件

    还要特别说明的是,InnoDB除了通过范围条件加锁时使用间隙锁外,如果使用相等条件请求给一个不存在的记录加锁,InnoDB也会使用间隙锁。

     

    三  死锁

         上文讲过,MyISAM表锁是deadlock free的,这是因为MyISAM总是一次获得所需的全部锁,要么全部满足,要么等待,因此不会出现死锁。但在InnoDB中,除单个SQL组成的事务外,锁是逐步获得的,当两个事务都需要获得对方持有的排他锁才能继续完成事务,这种循环锁等待就是典型的死锁。

    在InnoDB的事务管理和锁定机制中,有专门检测死锁的机制,会在系统中产生死锁之后的很短时间内就检测到该死锁的存在。当InnoDB检测到系统中产生了死锁之后,InnoDB会通过相应的判断来选这产生死锁的两个事务中较小的事务来回滚,而让另外一个较大的事务成功完成。

    那InnoDB是以什么来为标准判定事务的大小的呢?

          MySQL官方手册中也提到了这个问题,实际上在InnoDB发现死锁之后,会计算出两个事务各自插入、更新或者删除的数据量来判定两个事务的大小。也就是说哪个事务所改变的记录条数越多,在死锁中就越不会被回滚掉。

    但是有一点需要注意的就是,当产生死锁的场景中涉及到不止InnoDB存储引擎的时候,InnoDB是没办法检测到该死锁的,这时候就只能通过锁定超时限制参数InnoDB_lock_wait_timeout来解决。

    需要说明的是,这个参数并不是只用来解决死锁问题,在并发访问比较高的情况下,如果大量事务因无法立即获得所需的锁而挂起,会占用大量计算机资源,造成严重性能问题,甚至拖跨数据库。我们通过设置合适的锁等待超时阈值,可以避免这种情况发生。

    通常来说,死锁都是应用设计的问题,通过调整业务流程、数据库对象设计、事务大小,以及访问数据库的SQL语句,绝大部分死锁都可以避免。

     死锁问题分析:

    https://www.cnblogs.com/LBSer/p/5183300.html

    下面就通过实例来介绍几种避免死锁的常用方法:

    (1)在应用中,如果不同的程序会并发存取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会。

    (2)在程序以批量方式处理数据的时候,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低出现死锁的可能。

    (3)在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁,更新时再申请排他锁,因为当用户申请排他锁时,其他事务可能又已经获得了相同记录的共享锁,从而造成锁冲突,甚至死锁。

    (4)在REPEATABLE-READ隔离级别下,如果两个线程同时对相同条件记录用SELECT...FOR UPDATE加排他锁,在没有符合该条件记录情况下,两个线程都会加锁成功。程序发现记录尚不存在,就试图插入一条新记录,如果两个线程都这么做,就会出现死锁。这种情况下,将隔离级别改成READ COMMITTED,就可避免问题。

    (5)当隔离级别为READ COMMITTED时,如果两个线程都先执行SELECT...FOR UPDATE,判断是否存在符合条件的记录,如果没有,就插入记录。此时,只有一个线程能插入成功,另一个线程会出现锁等待,当第1个线程提交后,第2个线程会因主键重出错,但虽然这个线程出错了,却会获得一个排他锁。这时如果有第3个线程又来申请排他锁,也会出现死锁。对于这种情况,可以直接做插入操作,然后再捕获主键重异常,或者在遇到主键重错误时,总是执行ROLLBACK释放获得的排他锁。

    四  什么时候使用表锁

    对于InnoDB表,在绝大部分情况下都应该使用行级锁,因为事务和行锁往往是我们之所以选择InnoDB表的理由。但在个别特殊事务中,也可以考虑使用表级锁:

    (1)事务需要更新大部分或全部数据,表又比较大,如果使用默认的行锁,不仅这个事务执行效率低,而且可能造成其他事务长时间锁等待和锁冲突,这种情况下可以考虑使用表锁来提高该事务的执行速度。

    (2)事务涉及多个表,比较复杂,很可能引起死锁,造成大量事务回滚。这种情况也可以考虑一次性锁定事务涉及的表,从而避免死锁、减少数据库因事务回滚带来的开销。

    在InnoDB下,使用表锁要注意以下两点。

    (1)使用LOCK TABLES虽然可以给InnoDB加表级锁,但必须说明的是,表锁不是由InnoDB存储引擎层管理的,而是由其上一层──MySQL Server负责的,仅当autocommit=0、InnoDB_table_locks=1(默认设置)时,InnoDB层才能知道MySQL加的表锁,MySQL Server也才能感知InnoDB加的行锁,这种情况下,InnoDB才能自动识别涉及表级锁的死锁,否则,InnoDB将无法自动检测并处理这种死锁。

    autocommit解释

         当autocommit=1 为开启状态时,即使没有手动start transaction开启事务,mysql默认也会将用户的操作当做事务即时提交,将autocommit设置为OFF之后,系统默认开始了事务,但是并没有默认帮你提交了事务,因此如果我们在A客户端执行commit之后,B客户端就能查询到新的数据

    (2)在用 LOCK TABLES对InnoDB表加锁时要注意,要将AUTOCOMMIT设为0,否则MySQL不会给表加锁事务结束前,不要用UNLOCK TABLES释放表锁,因为UNLOCK TABLES会隐含地提交事务;COMMIT或ROLLBACK并不能释放用LOCK TABLES加的表级锁,必须用UNLOCK TABLES释放表锁。正确的方式见如下语句:

    例如,如果需要写表t1并从表t读,可以按如下做:

    SET AUTOCOMMIT=0;
    LOCK TABLES t1 WRITE, t2 READ, ...;
    [do something with tables t1 and t2 here];
    COMMIT;
    UNLOCK TABLES;

    五  InnoDB行锁优化建议

          InnoDB存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会要更高一些,但是在整体并发处理能力方面要远远优于MyISAM的表级锁定的当系统并发量较高的时候,InnoDB的整体性能和MyISAM相比就会有比较明显的优势但是,InnoDB的行级锁定同样也有其脆弱的一面,当我们使用不当的时候,可能会让InnoDB的整体性能表现不仅不能比MyISAM高,甚至可能会更差。

    (1)要想合理利用InnoDB的行级锁定,做到扬长避短,我们必须做好以下工作:

    a)尽可能让所有的数据检索都通过索引来完成,从而避免InnoDB因为无法通过索引键加锁而升级为表级锁定;

    b)合理设计索引,让InnoDB在索引键上面加锁的时候尽可能准确,尽可能的缩小锁定范围,避免造成不必要的锁定而影响其他Query的执行;

    c)尽可能减少基于范围的数据检索过滤条件,避免因为间隙锁带来的负面影响而锁定了不该锁定的记录;

    d)尽量控制事务的大小,减少锁定的资源量和锁定时间长度;

    e)在业务环境允许的情况下,尽量使用较低级别的事务隔离,以减少MySQL因为实现事务隔离级别所带来的附加成本。

    (2)由于InnoDB的行级锁定和事务性,所以肯定会产生死锁,下面是一些比较常用的减少死锁产生概率的小建议:

    a)类似业务模块中,尽可能按照相同的访问顺序来访问,防止产生死锁;

    b)在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;

    c)对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率。

    (3)可以通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况:

    show status like 'InnoDB_row_lock%';
    +-------------------------------+-------+
    | Variable_name | Value |
    +-------------------------------+-------+
    | InnoDB_row_lock_current_waits | 0 |
    | InnoDB_row_lock_time | 0 |
    | InnoDB_row_lock_time_avg | 0 |
    | InnoDB_row_lock_time_max | 0 |
    | InnoDB_row_lock_waits | 0 |
    +-------------------------------+-------+
    InnoDB 的行级锁定状态变量不仅记录了锁定等待次数,还记录了锁定总时长,每次平均时长,以及最大时长,此外还有一个非累积状态量显示了当前正在等待锁定的等待数量。
    对各个状态量的说明如下:
    InnoDB_row_lock_current_waits:当前正在等待锁定的数量;
    
    InnoDB_row_lock_time:从系统启动到现在锁定总时间长度;
    
    InnoDB_row_lock_time_avg:每次等待所花平均时间;
    
    InnoDB_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间;
    
    InnoDB_row_lock_waits:系统启动后到现在总共等待的次数;

          对于这5个状态变量,比较重要的主要是InnoDB_row_lock_time_avg(等待平均时长),InnoDB_row_lock_waits(等待总次数)以及InnoDB_row_lock_time(等待总时长)这三项。尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手指定优化计划。

    如果发现锁争用比较严重,如InnoDB_row_lock_waits和InnoDB_row_lock_time_avg的值比较高,还可以通过设置InnoDB Monitors (监视器)进一步观察发生锁冲突的表、数据行等,并分析锁争用的原因

    锁冲突的表、数据行等,并分析锁争用的原因。具体方法如下:

    mysql> create table InnoDB_monitor(a INT) engine=InnoDB;

    然后就可以用下面的语句来进行查看:

    mysql> show engine InnoDB status;

    监视器可以通过发出下列语句来停止查看:

    mysql> drop table InnoDB_monitor;

          设置监视器后,会有详细的当前锁等待的信息,包括表名、锁类型、锁定记录的情况等,便于进行进一步的分析和问题的确定。可能会有读者朋友问为什么要先创建一个叫InnoDB_monitor的表呢?因为创建该表实际上就是告诉InnoDB我们开始要监控他的细节状态了,然后InnoDB就会将比较详细的事务以及锁定信息记录进入MySQL的errorlog中,以便我们后面做进一步分析使用。打开监视器以后,默认情况下每15秒会向日志中记录监控的内容,如果长时间打开会导致.err文件变得非常的巨大,所以用户在确认问题原因之后,要记得删除监控表以关闭监视器,或者通过使用“--console”选项来启动服务器以关闭写日志文件。

    六   事务的隔离级别 (本质是锁的不同使用)

        事务的隔离级别就是通过锁的机制来实现,锁的应用最终导致不同事务的隔离级别,只不过隐藏了加锁细节,事务的隔离级别有4种:

    • Read uncommitted:会出现脏读,不可重复读,幻读
    • Read committed:会出现不可重复读,幻读
    • Repeatable read:会出现幻读(Mysql默认的隔离级别,但是Repeatable read配合gap锁不会出现幻读!)
    • Serializable:串行,避免以上的情况

    1 Read uncommitted:出现的现象--->脏读

           一个事务读取到另外一个事务未提交的数据,例子:A向B转账,A执行了转账语句,但A还没有提交事务,B读取数据,发现

    自己账户钱变多了!B跟A说,我已经收到钱了。A回滚事务【rollback】,等B再查看账户的钱时,发现钱并没有多...

    出现脏读的本质就是因为操作(修改)完该数据就立马释放掉锁,导致读的数据就变成了无用的或者是错误的数据

    2 Read committed:出现的现象--->不可重复读

          一个事务读取到另外一个事务已经提交的数据,也就是说一个事务可以看到其他事务所做的修改,例如:A查询数据库得到

    数据,B去修改数据库的数据,导致A多次查询数据库的结果都不一样【危害:A每次查询的结果都是受B的影响的,那么A查询出来的信息就没有意思了】

    3 Repeatable read:避免不可重复读是事务级别的快照!每次读取的都是当前事务的版本,即使被修改了,也只会读取当前事务版本的数据

    4 Serializable:串行,避免以上的情况(包括脏读,不可重复读,幻读)

    至于虚读(幻读)

          是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。和不可重复读类似,但虚读(幻读)会读到其他事务的插入的数据,导致前后读取不

    一致,幻读的重点在于新增或者删除 (数据条数变化),不可重复读的重点是修改.

    七  乐观锁,悲观锁

    无论是Read committed还是Repeatable read隔离级别,都是为了解决读写冲突的问题,现在考虑一个问题:有一张数据库表USER,只有id、name字段,现在有2个请求

    同时候操作表A,过程如下:(模拟更新丢失,虽然不是很恰当)

     1. 操作1查询出name="zhangsan" 

     2. 操作2也查询出name="zhangsan" 

     3. 操作1把name字段数据修改成lisi并提交

     4. 操作2把name字段数据修改为wangwu并提交

    那么操作1的更新丢失啦,即一个事务的更新覆盖了其它事务的更新结果,解决上述更新丢失的方式有如下3种:

    如下3种:

    • 使用Serializable隔离级别,事务是串行执行的!
    • 乐观锁
    • 悲观锁

    悲观锁

          顾名思义,悲观锁是基于一种悲观的态度类来防止一切数据冲突,它是以一种预防的姿态在修改数据之前把数据锁住,然后再对数据进行读写,在它释放锁之前任何人都不能对其数据进行操作,直到前面一个人把锁释放后下一个人数据加锁才可对数据进行加锁,然后才可以对数据进行操作,一般数据库本身锁的机制都是基于悲观锁的机制实现的;

    特点

         可以完全保证数据的独占性和正确性,因为每次请求都会先对数据进行加锁, 然后进行数据操作,最后再解锁,而加锁释放锁的过程会造成消耗,所以性能不高;

    我们使用悲观锁的话其实很简单(手动加行锁就行了):select * from xxxx for update,在select 语句后边加了for update 相当于加了排它锁(写锁),加了写锁以后,其

    他事务就不能对它修改了!需要等待当前事务修改完之后才可以修改.也就是说,如果操作1使用select ... for update,操作2就无法对该条记录修改了,即可避免更新丢失。

    乐观锁

          乐观锁是对于数据冲突保持一种乐观态度,操作数据时不会对操作的数据进行加锁(这使得多个任务可以并行的对数据进行操作),只有到数据提交的时候才通过一种机制来验证数据是否存在冲突(一般实现方式是通过加版本号然后进行版本号的对比方式实现);

    特点:

          乐观锁是一种并发类型的锁,其本身不对数据进行加锁通而是通过业务实现锁的功能,不对数据进行加锁就意味着允许多个请求同时访问数据,同时也省掉了对数据加锁和解锁的过程,这种方式大大的提高了数据操作的性能;

    乐观锁不是数据库层面上的锁,需要用户手动去加的锁。一般我们在数据库表中添加一个版本字段version来实现,例如操作1和操作2在更新User表的时,执行语句如下:

    update A set Name=lisi,version=version+1 where ID=#{id} and version=#{version},此时即可避免更新丢失。

    1、乐观锁失败后会报:ObjectOptimisticLockFailureException

    2、处理方案:捕获到对应乐观锁失败异常后进行重试

        乐观锁是一种思想,即认为读多写少,遇到并发写的可能性比较低,所以采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。此处和JDK里面的 CAS里 AtomicStampedReference很像 都是加了版本号来判断数据。

  • 相关阅读:
    CSS
    JavaScript
    Spring
    【Linux__FTP】Linux安装ftp组件
    【SSM__分页】MyBatis 分页插件
    【SSM__utils】实用代码风格收录
    【SSM__整合】ssm整合思路
    【Spring__父子容器】Spring父子容器关系
    stm32定时器_输出比较+输入捕获
    Redis的安装和基本操作01
  • 原文地址:https://www.cnblogs.com/hup666/p/13369748.html
Copyright © 2020-2023  润新知