跳至主要內容

酷风大约 8 分钟

  • Mysql存储引擎
    • MyISAM: 支持表级锁
    • InnoDB: 不仅支持表级锁,还支持更细粒度的行级锁

锁分类


  • 全局锁
    • 加锁 flush tables with read lock
    • 整个数据库就处于只读状态了: 数据和结构不允许更改
    • unlock tables
    • 主要应用于做全库逻辑备份
    • 避免全局锁影响业务,如何备份数据?
      • 开启事务隔离级别 可重复读,操作前先开启事务
      • 如 mysqldump 参数 –single-transaction 会在备份数据库之前先开启事务
      • 备份期间备份的数据一直是在开启事务时的数据
  • 表锁

  • 行级锁
    • 按隔离级别分
      • 读已提交隔离级别:记录锁
      • 可重复读隔离级别:记录锁、Gap Lock、Next-Key Lock
    • Record Lock 记录锁
      • 有 S 锁和 X 锁之分
        • 一个事务 对一条记录 加 S锁,其他事务也 可以加 S锁,不可加 X 锁;
          • S 型与 S 锁 兼容
          • S 型与 X 锁 不兼容
        • 一个事务 对一条记录 加 X锁,其他事务 不能加 X锁,也不可加 S锁;
          • X 型与 X 锁 不兼容
        • 当事务执行 commit 后,事务过程中生成的锁都会被释放。
    • Gap Lock 间隙锁
      • 只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象;
      • 锁定一个范围,如锁定ID范围为(3,5),其他事务就无法插入ID 4
      • 也有 有 S 锁和 X 锁之分,但是兼容的,无互斥关系;
        • 因为间隙锁的目的是防止插入幻影记录而提出的。
    • Next-Key Lock
      • 临键锁
      • Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
      • 既不能 插入ID 4,也不能修改 ID 5 记录
        • 包含间隙锁+记录锁,
          • 如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的。
    • 插入意向锁
      • 名字虽然有意向锁,但是它并不是意向锁,它是一种特殊的间隙锁。
      • 该锁只用于并发插入操作
      • 当事务B 提交插入新纪录,但被事务A加了 间隙锁
        • 事物 B 会生成一个插入意向锁,然后将锁的状态设置为等待状态
      • 如果说间隙锁锁住的是一个区间,那么「插入意向锁」锁住的就是一个点。
      • 在间隙锁区间内与插入意向锁是不兼容的,反之可以
        • 两个事务在同一时间内,一个拥有间隙锁,另一个无法拥有该间隙区间内的插入意向锁

加锁

  • 普通的 select 语句是不会对记录加锁的(除了串行化隔离级别)
    • 因为它属于快照读,是通过 MVCC(多版本并发控制)实现的
  • 查询时加行级锁:称为锁定读
    • 语句必须在一个事务中,因为当事务提交了,锁就会被释放
    • select ... lock in share mode; 对读取的记录加共享锁(S型锁)
    • select ... for update; 对读取的记录加独占锁(X型锁)
  • update table .... where id = 1; 对操作的记录加独占锁(X型锁)
  • delete from table where id = 1; 对操作的记录加独占锁(X型锁)

  • 加锁的对象是索引,加锁的基本单位是 next-key lock
  • next-key lock 是前开后闭区间
  • 间隙锁 是 前开后开区间
  • next-key lock 在一些场景下会退化成记录锁或间隙锁
    • 在能使用记录锁或者间隙锁就能避免幻读现象的场景下, next-key lock 就会退化成记录锁或间隙锁。
  • 假设 user 表,id主键索引,age 普通索引,name 无索引
    • 唯一索引等值查询
      • 当查询的记录是「存在」的,在索引树上定位到这一条记录后,将该记录的索引中的 next-key lock 会退化成「记录锁」
        • 原因就是在唯一索引等值查询并且查询记录存在的场景下,仅靠记录锁也能避免幻读的问题。
      • 当查询的记录是「不存在」的,在索引树找到第一条大于该查询记录的记录后,将该记录的索引中的 next-key lock 会退化成「间隙锁」
        • 唯一索引等值查询并且查询记录不存在的场景下,仅靠间隙锁就能避免幻读的问题。
    • 唯一索引范围查询
      • 大于等于:存在等值查询的条件,
        • 如果等值查询的记录是存在于表中,那么该记录的索引中的 next-key 锁会退化成记录锁
      • 小于或者小于等于
        • 当条件值的记录不在表中:
          • 不管是「小于」还是「小于等于」条件的范围查询,扫描到终止范围查询的记录时,该记录的索引的 next-key 锁会退化成间隙锁
          • 其他扫描到的记录,都是在这些记录的索引上加 next-key 锁。
        • 当条件值的记录在表中:
          • 如果是「小于」条件的范围查询
            • 扫描到终止范围查询的记录时,该记录的索引的 next-key 锁会退化成间隙锁
            • 其他扫描到的记录,都是在这些记录的索引上加 next-key 锁;
          • 如果「小于等于」条件的范围查询
            • 扫描到终止范围查询的记录时,该记录的索引 next-key 锁不会退化成间隙锁。
            • 其他扫描到的记录,都是在这些记录的索引上加 next-key 锁。
    • 非唯一索引等值查询 ,存在两个索引,一个是主键索引,一个是非唯一索引
      • 查询的记录「存在」时
        • 对扫描到的二级索引记录加的是 next-key 锁
        • 对于第一个不符合条件的二级索引记录,next-key 锁会退化成间隙锁
        • 在符合查询条件的记录的主键索引上加记录锁
      • 当查询的记录「不存在」时
        • 第一条不符合条件的二级索引记录,next-key 锁会退化成间隙锁
        • 因为不存在满足查询条件的记录,所以不会对主键索引加锁。
    • 非唯一索引范围查询
      • 非唯一索引范围查询,索引的 next-key lock 不会有退化为间隙锁和记录锁的情况
      • 非唯一索引进行范围查询时,对二级索引记录加锁都是加 next-key 锁。
    • 没有加索引的查询
      • 每一条记录的索引上都会加 next-key 锁
      • 相当于锁住的全表,这时如果其他事务对该表进行增、删、改操作的时候,都会被阻塞。
      • update 和 delete 语句如果查询条件不加索引
        • 那么由于扫描的方式是全表扫描,于是就会对每一条记录的索引上都会加 next-key 锁,这样就相当于锁住的全表。

锁兼容关系

加锁分析

  • select ... for update 独占锁(X型锁)
    • 首先:加锁的基本单位是 next-key lock
    • 通过数据库查询锁参考
      • 查询实际加锁情况,是否是next-key lock,还是退化成其他锁;
      • 不同索引,不同查询条件锁情况分析

锁查询

  • 查询事务的锁:select * from performance_schema.data_locks

  • LOCK_TYPE:锁类型

    • RECORD 表示行级锁,而不是记录锁的意思
    • TABLE 表锁
    • LOCK_MODE:
      • X:X 型的 next-key 锁
      • X, REC_NOT_GAP: X 型的记录锁
      • X, GAP: X 型的间隙锁
      • IX: X 型的意向锁
      • X, GAP, INSERT_INTENTION: 插入意向锁
    • LOCK_DATA: 锁的范围最右值,如间隙锁,例如 LOCK_DATA:30,
      • 最左值为:表中已有记录中30之前的记录值
  • LOCK_DATA: 30 范围为 [20, 30]

死锁

  • 场景前提:InnonDB存储引擎,可重复读隔离级别,关闭死锁检测
  • 举例订单业务
    • 新增订单,增加幂等性校验,防止订单重复
    • 同时有两个事务,先查询,再新增
    • select ... for update
      • select .. update 防止事务执行的过程中,有其他事务插入了记录,而出现幻读的问题
      • 如果使用普通的select会出现幻读问题:不会对记录加锁
    • select ... update 加了什么锁:
      • 表锁:X 类型的意向锁
      • 行锁:X 类型的间隙锁
        • 间隙锁与间隙锁之间是兼容
        • 所以所以两个事务中 select ... for update 语句并不会相互影响。
    • 同时有两个事务进行新增订单
    • 陷入了等待状态,即死锁
    • 都无法进行 insert
      • 为了获取到插入意向锁,都在等待对方事务的间隙锁释放,于是就造成了循环等待,导致死锁。

  • 行锁的释放
    • 在事务提交(commit)后,锁就会被释放,并不是一条语句执行完就释放行锁。

避免死锁

  • 数据库方面
  1. 设置事务等待锁的超时时间,InnoDB,innodb_lock_wait_timeout ,默认值时 50 秒,超时后会回滚该事务从而释放锁;
  2. 开启主动死锁检测,主动回滚死锁链条中的某一个事务,innodb_deadlock_detect为on;
  • 业务层面
    • 如新增订单,先查询select .. update,后新增,造成死锁
      • 可以进行订单号加唯一索引,直接update,重复抛出异常,完成订单的幂等性校验逻辑;