mysql-锁

Posted on By xqw

锁类型

  • 全局锁
    全局锁就是对整个数据库实例加锁。MySQL提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。
    全局锁的典型使用场景是,做全库逻辑备份。也就是把整库每个表都select出来存成文本。
    以前有一种做法,是通过FTWRL确保不会有其他线程对数据库做更新,然后对整个库做备份。注意,在备份过程中整个库完全处于只读状态。
    mysqldump。当mysqldump使用参数–single-transaction的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于MVCC的支持,这个过程中数据是可以正常更新的。
    但是MySAM不支持可重复读,只能通过FTWRL命令
  • 表级锁 MySQL里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。
    表锁的语法是 lock tables … read/write,与FTWRL类似,可以用unlock tables主动释放锁,也可以在客户端断开的时候自动释放。需要注意,lock tables语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象。
    在MySQL 5.5版本中引入了MDL,当对一个表做增删改查操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加MDL写锁。读与读不互斥,读写互斥
  • 行锁 S锁 共享锁(读锁) select … lock in share mode;
    X锁 排他锁(写锁) select … for update;
    IS IX 意向锁
    若对某一行加X锁,那么首先对对应数据库、表加IX锁,那么后面的想对表再加X/S,表上有IX锁,说明本表或行上有X锁在,就不要遍历每一行了

      IS IX S X
    IS 兼容 兼容 兼容 不兼容
    IX 兼容 兼容 不兼容 不兼容
    S 兼容 不兼容 兼容 不兼容
    X 不兼容 不兼容 不兼容 不兼容

行锁算法

Record Lock 单个行记录的锁(READ_COMMITTED级别默认)
Gap Lock 间隙锁,锁一个区间,不包含边界
Next-Key Lock 锁一个区间,前开后闭(REPEATABLE_READ级别默认,加行锁默认都是next key,只不过会优化成间隙锁和行锁)

加锁原则:

  1. 加锁的基本单位是:Next-Key Lock,在索引上加锁,加锁被访问到的数据
  2. 加完Next-Key Lock后,唯一索引上等值查询退化为行锁,非唯一索引退化为间隙锁

在主键上查询 select * from table where id = 1 for update;会聚集索引上加Record Lock
在辅助索引上查询 select * from table where test_index = 1 for update;会在主键/唯一索引上加Record Lock,辅助索引上加Next-Key Lock,
若test_index数据有{0,1,3},区间为不包含边界的上下两个值(0,3),所有在两个值时间再做插入{2}会阻塞
若两个事务查询 select * from table where test_index = 2 for update;,因为{2}不存在,两个事务均会获得Next-Key Lock,区间(1,3),而后同时插入{2}会发生死锁
在辅助索引上查询 select id from table where test_index = 1 for update;select id from table where test_index = 1 lock in share mode;会不一样,由于只查询了id,所以不经过聚集索引,共享锁只会锁住辅助索引,排它锁会顺便锁住主键 因此 update t set d = d + 1 where id = 1;在排他锁时会阻塞,共享锁不会阻塞

深入理解与例子:加锁几个规则与优化 MySQL实战45讲/21讲

快照读 & 当前读

  1. 快照读(snapshot read) 简单的select操作(不包括 select … lock in share mode, select … for update)
    快照读通过mvvc和undo log实现,读取事务开始时的数据
  2. 当前读(current read)
    select ... lock in share mode
    select ... for update
    insert
    update
    delete
    

在上面的语句中会读取最新数据,并加上 next-key锁, 因此在应用层面没有使用当前读可能会造成丢失更新,如

//若findById 没有使用当前读,没有加上锁,那么后面的依赖历史数据的更新(user.getCount() + 1)会丢失
User user = userDao.findById(id);
user.setCount(user.getCount() + 1);
userDao.save(user);
//解决:1.用共享锁或排他锁,使之变成当前读 2.update user set count = count + 1;

锁使用场景

1、多对多关系需要相互关联的时候

需求:新增用户时需要关注某一类商品,用户、商品时多对多的关系
问题:当新增商品和新增用户并发时,新增用户时查询不到正在新增的商品
解决:"商品类别"加索引,新增某类商品时加上共享锁,此时会加上间隙锁

2、基于更新时间戳增量处理数据

需求:定时器每次处理某个时间段内的数据
问题:新增数据和扫描数据并发时,定时器获取到的数据会漏掉正在新增的数据
解决: 时间字段加索引,扫描数据时加上共享锁,也就是某个时间段内的间隙锁

死锁发生例子

  1. 两个事务先获取S锁,再进行写操作,X锁

    T1 T2
    获取a表S锁  
      获取a表S锁
    修改数据 X锁(阻塞)  
      修改数据 X锁(死锁抛异常,rollback)
    修改成功  
  2. 某个事务需要同时得到两个锁

    T1 T2
    获取行a X锁  
      获取行b X锁
    获取行b X锁(阻塞)  
      获取行a X锁 (死锁抛异常,rollback)
    获取两个锁 查询成功  
  3. X 和 范围S锁

死锁检测抛异常,默认将undo量大的回滚

|T1|T2
|:---:|:---:|
|select * from company_file where id = 2 for update;          | 
|                   | select * from company_file where id < 20 lock in share mode;(阻塞,占了(*,2)的锁)
|insert into company_file select 0; (死锁抛异常,rollback)  | 
|                   | 查询成功 
   
|T1|T2
|:---:|:---:|
|select * from company_file where id = 2 for update;          | 
|                   | select * from company_file where id < 20 lock in share mode;(阻塞,占了(*,2)的锁)
|insert into company_file select 19; (正常)  | 
|insert into company_file select 0; (死锁,但正常提交,检测到了死锁,undo量大,将T2回滚)  | 
|                   | 死锁抛异常,rollback 
  
|T1|T2
|:---:|:---:|
|select * from company_file where id = 2 for update;          | 
|insert into company_file select 0; (正常)  | 
|                   | select * from company_file where id < 20 lock in share mode;(阻塞,占了(*,0)的锁)
|insert into company_file select 1; (正常)  | 
|insert into company_file select -1; (死锁,但正常提交,优先死锁之前有操作成功的)  | 
|                   | 死锁抛异常,rollback 

死锁检测,wait-for graph,将等待的事务画图,若T1等T2,则画T1->T2,若同时T2->T1,成环,则回滚undo量最小的事务

锁升级

innodb 根据页进行加锁,资源(内存)开销小,不会进行锁升级
若表有3 000 000数据页,每页100条数据
若在数据上加锁,每个锁10字节,内存3G(sqlServer,升级为表锁)
页上加锁,每个30字节,90M