美文网首页
表锁和行锁

表锁和行锁

作者: 无力韬韬 | 来源:发表于2020-11-21 11:13 被阅读0次

    MySQL中的锁总体可以分为悲观锁和乐观锁。
    悲观锁MySQL中有自带的锁。乐观锁需要自己写程序控制来实现乐观锁的功能。

    悲观锁

    表级锁

    表锁

    表锁属于mysql服务层,手动添加。

    • 读锁:
    lock table mylock read;
    

    加读锁后,可以继续加读锁,但不能加写锁。
    当一个线程拥有了读锁之后,那么当前的线程只能访问加锁了的表。
    其他线程既可以访问加锁的表,也可以访问其他的表。
    但是,其他线程如果想要修改加了读锁的表中的内容,必须要等当前线程释放表锁。

    UNLOCK tables;
    
    • 写锁:
    lock table mylock write;
    

    加了写锁之后,其他进程既不能修改也不能查询加锁的表。

    元数据锁

    元数据锁属于MySQL服务层,自动添加。
    元数据锁是自动添加,顾名思义,是用来保护表的元数据的。
    在进行增删改查的操作时,加元数据读锁。在进行DDL操作时,即对表的结构进行修改的时候加元数据写锁。
    读锁是为了,当一个线程正在对表进行增删改查,不允许其他进程修改表的结构。
    写锁则是为了当一个线程正在修改表的结构时,其他线程既不能访问表的数据也不能修改表的结构。
    元数据锁在事务提交之后才会释放。

    begin; //Navicat中开启事务
    select * from mylock;//Navicat中查询表中数据
    mysql> alter table mylock add f int;//命令行中修改表结构,发生阻塞。
    commit;//Navicat中提交事务
    Query OK, 0 rows affected (2 min 37.60 sec)//命令行中提示修改完成
    Records: 0  Duplicates: 0  Warnings: 0
    
    意向锁

    属于Innodb存储引擎层。内部使用。

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

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

    意向锁的主要作用是为了【全表更新数据】时的性能提升。否则在全表更新数据时,需要先检索该表是否某些记录上面有行锁。

    行级锁(InnoDB实现)

    行级锁是由Innodb存储引擎实现的。
    行级锁按锁定范围来说分为三种:

    记录锁

    锁定索引中的一条记录。

    间隙锁

    锁定记录前,记录中,记录后的行。
    RR隔离级(可重复读)--MySQL默认隔离级别

    Next-Key锁

    记录锁+间隙锁

    按功能来说分为两种:

    共享读锁

    允许一个事务去读一行,阻止其他事务活动相同数据集的排他锁。
    也就是说允许当前事务或其他事务读,不允许其他事务写。

    SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE -- 共享读锁 手动添加
    select * from table -- 无锁
    
    排他写锁

    允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。
    也就是允许当前事务写,不允许其他事务读或者写。
    排他写锁对于UPDATE DELETE INSERT 语句,InnoDB会自动给涉及数据集加排他锁。
    手动加:

    SELECT * FROM table_name WHERE ... FOR UPDATE
    
    两阶段锁

    锁操作分为两个阶段:加锁阶段与解锁阶段
    加锁阶段与解锁阶段不相交。加锁阶段只加锁不放锁。放锁阶段只放锁不加锁。
    加锁和解锁不能在一个sql语句中实现。

    行锁演示

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

    show status like 'innodb_row_lock%';//查看行锁状态
    
    • Innodb_row_lock_current_waits:当前正在等待锁定的数量;
    • Innodb_row_lock_time:从系统启动到现在锁定总时间长度;
    • Innodb_row_lock_time_avg:每次等待所花平均时间;
    • Innodb_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间;
    • Innodb_row_lock_waits:系统启动后到现在总共等待的次数;
    begin;
    select * from mylock where id=1 lock in SHARE MODE;//在Navicat中开启事务,但未提交
    //此时在id=1的行已经被加了行共享读锁。
    
    命令行中可以访问id为1的行
    命令行中可以修改其他行的数据
    命令行中修改id=1的行等待锁释放时间超时

    如果使用了没有索引的列,行锁会升级为表锁
    上面是读锁,下面分析写锁。

    BEGIN;
    SELECT * from mylock where id=1 for update;//同样在Navicat中加上写锁。
    //对于 UPDATE DELETE INSERT 语句来说,InnoDB会自动加行写锁。
    //而SELECT语句则需要手动加 
    
    直接访问是可以访问的,但是加读锁就会堵塞

    select 没有锁的时候即使要查询的行已经有写锁了还是能访问。但是加上了行读锁就发生阻塞了。

    间隙锁

    间隙锁的产生:
    主键索引的范围 ,辅助索引的等值和范围。


    表结构
    begin;
    update news set number=3 where number=4;//Navicat中操作
    

    此时的id间隙范围为[1,6),number范围是[2,5),类型是辅助索引的等值。

    start transaction ;//命令行中操作
    insert into news value(2,3);#(均在间隙内,阻塞)
    insert into news value(7,8);#(均在间隙外,成功)
    insert into news value(2,8);#(id在间隙内,number在间隙外,成功)
    insert into news value(4,8);#(id在间隙内,number在间隙外,成功)
    insert into news value(7,3);#(id在间隙外,number在间隙内,阻塞)
    insert into news value(7,2);# (id在间隙外,number为上边缘数据,阻塞)
    insert into news value(2,2);#(id在间隙内,number为上边缘数据,阻塞)
    insert into news value(7,5);#(id在间隙外,number为下边缘数据,成功)
    insert into news value(4,5);#(id在间隙内,number为下边缘数据,阻塞)
    

    只要number在间隙内,就一定会阻塞。如果number在下边缘数据并且id在间隙内的话也会阻塞。

    start transaction ;
    update news set number=3 where number=13 ;
    

    此时number间隙锁范围(11,无穷)。
    此时只有当id也满足间隙锁范围,number=11时才会发生阻塞。

    两阶段锁

    两阶段锁☞锁的产生和释放是两个阶段,锁在语句执行完毕之后不会立即释放,而是在事务提交之后才释放。

    死锁

    由于两阶段锁的特性,会产生死锁的问题。



    由于线程1先获得了行id=1的锁,线程2获得了 id=2的锁,并且都没有提交,此时锁都没有释放。
    当线程1再去修改行id=2的信息时,就会发生阻塞,同时线程2想修改行id=1的信息,同样被阻塞,两个线程都在等对方释放锁,形成了死锁。

    死锁检测

    发生死锁后,有两种解决策略。
    一种是直接进入等待直至超时。超时的事务回滚。超时的时间限制可以通过innodb_lock_wait_timeout设置。
    另一种就是死锁检测。也就是主动检测是否时锁,一旦发现死锁就回滚导致死锁的某一条事务。
    死锁的检测。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。
    采用第一种策略没有额外的性能消耗,但是等待的时间过长影响使用体验,太短又容易误杀其他没有死锁只是在等待锁释放的事务。
    第二种策略可以根据是否死锁回滚事务,发现死锁就回滚,但是寻找死锁的过程需要额外的性能消耗,每当一个事务被锁的时候,就要看看它所依赖的线程有没有被别人锁住,如此循环,最后判断是否出现了循环等待,也就是死锁。
    例如:新来的线程F,被锁了后就要检查锁住F的线程(假设为D)是否被锁,如果没有被锁,则没有死锁,如果被锁了,还要查看锁住线程D的是谁,如果是F,那么肯定死锁了,如果不是F(假设为B),那么就要继续判断锁住线程B的是谁,一直走知道发现线程没有被锁(无死锁)或者被F锁住(死锁)才会终止。

    相关文章

      网友评论

          本文标题:表锁和行锁

          本文链接:https://www.haomeiwen.com/subject/hgujiktx.html