zoukankan      html  css  js  c++  java
  • 第十五节:MySQL中的锁

    锁的简介

    1. 锁:是计算机协调多个进程或者线程并发访问某一资源的机制(计算机的资源包括CPU、I/O和内存,在数据库中数据也是一种资源)
    2. 事务提交之后会释放所有的锁
    3.  Mysql 在5.5之前默认使用 MyISAM 存储引擎,之后使用 InnoDB 
    4. 查看当前存储引擎:
      1. show engines;
      2. show variables like '%storage_engine%';
    5. mysql中不同的存储引擎支持不同的锁机制,锁的用法会有所不同
    6. MyISAM和memory:采用表级锁 
    7. InnoDB:既支持行级锁也支持表级锁,默认使用行级锁
    8. mysql中锁的相关名称:
      1. 悲观锁:抽象性,不是真实存在的锁,就是一种思想,就是在操作之前先上锁,是通过mysql系统自带的锁机制(共享锁和排他锁)来实现的
      2. 乐观锁:抽象性,不是真实存在的锁,就是一种思想,是通过版本控制来实现的
      3. 共享锁:MyISAM 中叫读锁,不同的事务可以对同一个数据集添加读锁
      4. 排他锁:MyISAM 中叫写锁,事务A对数据集添加写锁,其他事务则不可以对数据集添加锁
      5. 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。(锁定整个表)
      6. 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。(锁定一行)
      7. 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般(锁定一页)
    9.  注意点:
      1. MyISAM中的所有的锁都是表锁,所有性能比较低下,并发不高,也不会存在死锁问题
      2. InnoDB支持事务,支持行级锁和表级锁,性能会有所提高,也会导致一些死锁等问题
      3. InnoDB中的行锁并不是直接锁定记录,而是锁定索引,所以查询的条件是根据索引查询才有可能是行锁,如果不是索引查询则是表锁(如果一条sql 语句操作了主键索引,Mysql 就会锁定这条主键索引;如果一条语句操作了非主键索引,MySQL会先锁定该非主键索引,再锁定相关的主键索引) 

    InnDB中的锁

    共享锁和排它锁

    1. 共享锁的特点:
      1. 事务A中对数据集添加了共享锁,其他事务也可以对事务添加共享锁,但是不能添加排它锁
      2. 事务A可以读数据集中的数据,如果事务A对数据集进行更新,则读锁会自动升级为写锁。其他事务只可以读数据集中的数据,不可以更新数据集中的数据
    2.  排它锁的特点:
      1. 事务A对数据集添加了排它锁,其他事务不可以对数据集添加任何锁
      2.  事务A可以对数据集进行增删改查操作;其他事务只能对数据集进行查询操作,不能进行增删改操作
    3. 注意点:
      1. update、delete和insert语句都会默认给涉及到的数据集添加排它锁,所以增删改操作一定是有排他锁的
      2. select语句不会默认添加任何锁,查询操作可以显示决定是共享锁还是排他锁,所以共享锁和排他锁只有对查询语句有效
      3. 查询操作不会受到锁的影响,事务A无论加不加锁事务B都可以读到数据
    4. 显示增加共享锁和排它锁:
      1. 语法:
        1. 共享锁: SQL语句后面加上 lock in share mode
        2. 排它锁:SQL语句后面加上 for update
      2. 实例:
        1. 共享锁:select * from table_name lock in share mode
        2. 排它锁:select * from table_name for update
      3. 注意:
        1. 增删改语句也可以显示的添加共享锁和排他锁,但是增删改语句默认会增加排它锁,所以显示添加共享锁和排它锁没有任何意义
        2. update,insert,delete语句中显示添加的共享锁会自动升级为排它锁
        3. select加读锁:防止数据在被读取的时候被其他的事务加上写锁
    5. 实例1: T1表示Session1中的事务,T2表示Session2中的事务。( create table Student(id int(11),name varchar(25),age int(11));insert into Student values(1,'Tom',25),(2,'Jack',26),(3,'Weiking',27);)
      T1 T2

      start transaction; #开启一个事务,事务不结束(提交或回滚),事务所申请的资源就不会释放
      select * from Student lock in share mode; #给查询语句添加一个共享锁

       
       

      select * from Student; #查询操作可以执行不需要要等待
      update Student set age = 127 where id = 3; #更新操作处于等待状态。

      T2之所以要等待是因为T2在更新之前需要加上排他锁,而数据已经被T1加上了共享锁,所以T2需要等待T1释放共享锁才能对数据加上排它锁

       commit;#T1提交事务    T1事务已经释放了共享锁,T2的更新操作得到执行
        select * from Student # 返回更新后的数据
    6.  实例2:T1表示Session1中的事务,T2表示Session2中的事务
       T1  T2

      start transaction; #开启一个事务
      select * from Student lock in share mode; #给查询语句添加一个共享锁

       
        select * from Student; #可以查询,但是更新删除增加操作需要等待

      update Student set age = 126 where id = 2; #执行更新操作会自动将共享锁升级为排它锁
      select * from Student;#可以执行查询操作,查询返回更新后的数据

       
       

      select * from Student lock in share mode;# 加锁查询进入等待,因为T1的update操作将共享锁升级为排它锁,T2加共享锁要等T1将排它锁释放之后才可以

       commit;        T1事务提交,T2的查询加锁操作可以执行

    行锁和表锁

    1. 行锁:就是给某一行数据加上锁
    2. 表锁:就是给整个表加上锁
    3. 注意点:
      1. Oracle数据库中的行锁是通过对数据集中相应的数据行加锁来实现的
      2.  mysql中的行锁是通过给索引上的索引项加锁来实现的,所以只有通过索引检索数据才会使用行级锁,否则就是用表级锁
    4. 实例1:在不通过索引条件查询的时候,InnoDB使用的是表锁,而不是行锁。(create table Teacher(id int(11), age varchar(11))engine = innodb;insert into Teacher values(1,'25'),(2,'26'),(3,'27');)
      Session 1    Session 2

      begin;#显示开启事务
      select * from Teacher;

      begin;#显示开启事务
      select * from Teacher;

      select * from Teacher where id = 1 for update;  
        select * from Teacher where id = 2 for update;   # 会进入等待状态。Session1虽然看起来只是个id这行加上排他锁,但是id这列没有加索引,for update会加表锁
    5.  实例2 : 通过索引条件来检索数据的时候,使用的就是行锁(create table Teacher1(id int(11) primary key, age varchar(11))engine = innodb;insert into Teacher1 values(1,'25'),(2,'26'),(3,'27');)

      Session 1 Session2

      begin;#显示开启事务
      select * from Teacher1;

      begin;#显示开启事务
      select * from Teacher1;

      select * from Teacher1 where id = 1 for update;  
       

      select * from Teacher1 where id = 2 for update; # 不需要等待,可以直接加锁查询,id是主键含有索引,Session1加锁的时候只是对id为1这一行加了锁行,所以Session2可以对id为2的这一行加锁

       

    6. 实例3:由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以即使访问了不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的(create table Teacher2(id int(11) , age varchar(11))engine = innodb;alter table Teacher2 add index index_name (id); insert into Teacher2 values(1,'25'),(1,'26'),(3,'27');)
      Session 1  Session 2 

      begin;#显示开启事务
      select * from Teacher2 where id = 1;

      begin;#显示开启事务

      select * from Teacher2 where id = 1;

      select * from Teacher2 where id = 1 and age = '25' for update;  
        select * from Teacher2 where id = 1 and age = '26' for update;   # 需要等待,虽然查询的是不同行的数据,但是由于索引都是id为1,所有需要等待Session1释放锁
    7.  实例4:当表中有多种不同的索引的时候,不同的事务可以使用不同的索引锁定不同的行(create table Teacher3(id int(11) primary key, age varchar(11))engine = innodb;alter table Teacher3 add index index_name (age);insert into Teacher3 values(1,'25'),(2,'26'),(3,'27');)
       Session 1  Session2

      begin;#显示开启事务
      select * from Teacher3;

      begin;#显示开启事务
      select * from Teacher3;

      select * from Teacher3 where id = 1 for update;  
        select * from Teacher3 where age = '26' for update;   # 不需要等待,可以直接加锁查询,id有主键索引,Session1加锁的时候只是对id为1这一行加了锁行,Session2中对普通索引age进行加锁,锁定的是age为26这这一行

    悲观锁和乐观锁

    1. 乐观锁和悲观锁并不是mysql数据库中真正的锁机制而是一种思想
    2. 乐观锁:指事务进行处理数据的时候,对加锁持有一种乐观的态度,不对查询的数据集进行加锁,认为在该事务中更新该数据集的时候不会与其他事务发生冲突
    3. 悲观锁:指事务进行处理数据的时候,对加锁持有一种悲观的态度。对查询到的数据集进行加锁,认为该事务更新该数据集时候会与其他事务冲突
    4. 乐观锁的实现方式:
      1. 乐观锁并不是数据库自带的锁机制实现的
      2. 乐观锁是用数据版本记录机制实现的,这是乐观锁最常见的一种实现方式,需要用户手动去实现
      3. 乐观锁在表中的数据进行操作时(更新),先给数据表加一个版本(version)字段,每操作一次,将那条记录的版本号加1。也就是先查询出那条记录,获取出version字段,如果要对那条记录进行操作(更新),则先判断此刻version的值是否与刚刚查询出来时的version的值相等,如果相等,则说明这段期间,没有其他程序对其进行操作,则可以执行更新,将version字段的值加1;如果更新时发现此刻的version值与刚刚获取出来的version的值不相等,则说明这段期间已经有其他程序对其进行操作了,则不进行更新操作。
    5. 悲观锁的实现方式:悲观锁是通过数据库自带的锁机制(共享锁和排它锁)实现的,要用的时候只需要调用相关的语句即可(如:lock in share mode)
    6. 数据版本:就是为数据增加一个版本标识,一般就是通过为数据库表增加一个version字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加1。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
    7. 实例:假设有一张商品表 shop,它包含 id,商品名称,库存量三个字段,id 为主键
      1. 并发导致数据一致性的问题:如果有A、B两个用户需要购买id=1的商品,AB都查询商品数量是1000,A购买后修改商品的数量为999,B购买后修改数量为999,很显然这是错误的。
      2. 用乐观锁的解决方案:
        begin #开启事务
        select id,num,Stock as old_Stock from shop;  #每次获取商品时,不对该商品加锁
        update table set num=num-1 where id=1 and Stock= old_Stock  #在更新数据前通过where语句判断old_Stock和Stock是否相等。如果相等则表示其他执行程序肯定未进行修改,然后进行更新;如果不相等则表示在更新操作之前有其他执行程序已经更新了该库存数,那么需要重新获取库存量在次进行比较(就是重新执行select语句和update语句)
      3. 悲观锁方案:每次获取商品时,对该商品加排他锁。 也就是在用户A获取获取 id=1 的商品信息时对该行记录加锁,期间其他用户阻塞等待访问该记录。
        begin; #开启事务
        select id,num,version from shop  where id=1 for update; # 事务T在查询库存数时就直接使用了排他锁
        update shop set stock=stock-1 where id=1;
    8. 总结:
      1. 乐观锁在事务开始进行查询时候不加锁,悲观锁在事务开始进行查询是就直接加锁
      2. 乐观锁更新有可能会失败,甚至是更新几次都失败,这是有风险的。所以如果写入居多,对吞吐要求不高,可使用悲观锁。
      3. 读用乐观锁,写用悲观锁

    意向锁

    1. 意向锁分为意向共享锁和意向排他锁,是InnoDB 存储引擎锁特有的
    2. 意向共享锁(IS):事务在给一个数据行加共享锁前必须先取得该表的意向共享锁(就是添加行锁之前会给该表添加意向共享锁)
    3. 意向排他锁(IX):事务在给一个数据行加排他锁前必须先取得该表的意向排他锁(就是添加行锁之前会给该表添加意向共享锁)
    4. 意向锁的作用:
      1. 没有意向锁的时候:事务A如果要对table1添加表锁(排他锁),第一步是要确定该表没有被其他的事务添加表锁;第二步是要确定该表中的每一行数据没被其他的事务添加行锁(这步需要全表扫描每一行比较浪费资源)
      2. 有意向锁的时候:如果其他事务对数据添加行锁之前加上对应的的意向锁,第一步还是要确定该表没有被其他的事务添加表锁;第二步就不需要全表扫描每一行判断是否被其他事务添加行锁,直接判断是否有对应的意向锁就可以了
    5. 注意点:
      1. 意向锁是InnoDB自动加的,不需用户干预
      2. 事务A申请一行的行锁的时候,数据库会自动先开始申请表的意向锁

    死锁

    1. 死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程
    2. 死锁产生的原因:由于资源占用是互斥的,当某个进程提出申请资源后,使得有关进程在无外力协助下,永远分配不到必需的资源而无法继续运行,这就产生了一种特殊现象死锁
    3. 解除死锁状态的两种方法:
      1. 第一种:
        1. 查询是否锁表:show OPEN TABLES where In_use > 0;
        2. 查询进程:show processlist;
        3. 杀死进程id(就是上面命令的id列):kill id
      2. 第二种:
        1. 查看当前的事务:SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
        2. 查看当前锁定的事务:SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS
        3. 查看当前等锁的事务:SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
        4. 杀死进程:kill 进程ID
    4. 产生死锁的四个必要条件:
      1. 互斥条件:一个资源每次只能被一个进程使用
      2. 请求与保持条件:事务因为请求资源而阻塞时,保持已有的资源不释放
      3. 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺
      4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
    5. 产生死锁的实例:
      1. T1 和 T2 都对 Student 进行查询使用了共享锁,然后T1准备执行update操作时,由于锁机制,T1的共享锁必须升级为排他锁才能执行update操作,在升级排他锁之前需要T2中的共享锁释放,同理T2上执行update操作也需要T1中的锁释放,于是就产生了死锁
      2. 解决方案:将T1和T2 中的查询操作的的共享锁换为排它锁即可(select * from Student for update)
      3. T1 T2
         begin;   begin; 
        select * from Student  lock in share mode   #  为Student表设置共享锁   select * from Student lock  in share mode; #  为Student表设置共享锁
        update Student set age = 225 where id= 1;#更新操作会进行等待    update Student set age = 225 where id= 1; #更新操作会进入等待
  • 相关阅读:
    CF703D Mishka and Interesting sum
    CF697D Puzzles
    SCOI2017酱油记
    [BZOJ4730][清华集训2016][UOJ266] Alice和Bob又在玩游戏
    BZOJ4311:向量
    BZOJ4520: [Cqoi2016]K远点对
    BZOJ4555: [Tjoi2016&Heoi2016]求和
    [Codechef November Challenge 2012] Arithmetic Progressions
    agc040
    补题
  • 原文地址:https://www.cnblogs.com/WeiKing/p/12077640.html
Copyright © 2011-2022 走看看