Mysql基础课六:数据库锁

数据库锁

  1. 根据加锁范围分为:全局锁,表级锁和行锁;

  2. 根据锁类型分为:共享锁,排他锁和意向共享锁,意向排他锁;

全局锁和表级锁

  1. 全局锁,是指对整个数据库加锁,此时整个库就处于只读状态了,一般在备份数据时使用;
	命令 flush tables with read lock
	如果表都是 InnoDB 引擎,建议备份数据,使用 mysqldump 工具,并且设置 –single-transaction 参数,对应用会更友好;
  1. 表级锁,分为表锁和元数据锁(MDL);
	表锁,命令 lock tables table_name read/write,通常在数据库不支持行锁时使用,如 MyISAM 引擎,加锁后,该表就只能被加锁 session 进行更新操作,其他 session 是无法进行更新操作的

	元数据锁 MDL,是表操作自动添加的一种表锁,其中对表数据的增删改查,添加的是读锁,而对表结构变更时,添加的是写锁,读读之间不互斥,读写之间互斥,写写之间互斥;
  1. 注意 MDL 锁,事务提交才会释放,可能导致,表结构的变更锁住了线上查询和更新,导致数据库线程爆满,甚至不可用,所以建议执行表结构变更时,先查看执行事务;
	建议,执行表结构变更时,首先在 MySQL 的 information_schema 库的 innodb_trx 表中,搜索当前执行中的事务,如果有长事务在执行,考虑先暂停表结构变更,或者 kill 掉这个长事务;
	如果是一个热点表,请求很频繁,kill 掉后新的请求会马上到来,考虑在 alter table 语句里面设定等待时间,在等待时间内没有拿到 MDL 写锁,就放弃变更,之后再重试;
  1. 通过语句 show status like ‘table%’ ,查看表锁争夺,如果 table_locks_waited 比较高,说明争夺严重;

  2. MyISAM的表锁,是先获取 sql 中的所有表的表锁,所以不会出现死锁,默认 select 操作加读表锁,update,delete,insert 加写表锁,执行完就会释放;

行锁

  1. 行锁,即对数据库表的行记录加锁,由存储引擎层实现,InnoDB支持,MyISAM不支持,不支持的引擎,在并发时就只能使用表锁;

  2. 共享锁和排他锁,语句 select 不会加锁,update,insert,delete 会加排他锁,for update 会加排他锁,lock in share mode 加共享锁;

	如果事务 T1 对数据 A 加了共享锁,那么其他事务可以读数据 A,但不能修改数据A
	如果事务 T1 对数据 A 加了排他锁,那么其他事务,既不能读,也不能修改数据 A
  1. 意向共享锁和意向排他锁,是指任何一个事务想要加锁之前,都需要现在表上先添加意向锁,意向锁的目的,是在添加表锁时,可直接通过意向锁是否存在,来判断是否有锁冲突;

  2. 并且 InnoDB中,是在索引树上加锁,如果没有通过索引查找,那么会加表锁,并且如果不同行的记录,有相同的索引值,是需要等待锁释放,如果行记录有不同的索引,可能因为另一个索引加锁,需要锁释放;

	InnoDB 中,通过 show status like 'innodb_row_lock%' 来查看行锁争夺
	通过表 information_schema.innodb_locks 查看具体锁争夺
  1. 为解决幻读的问题,InnoDB 引入了间隙锁,保证了在一个事务在前后两次查询中,如果有其他事务插入了新的行,而后一次查询,也能看到前一次查询没有看到的新行;

间隙锁

  1. 间隙锁是对行记录间的空隙进行加锁,使得无法在该间隙中插入数据,注意间隙锁之间是不存在冲突的,即两个事务都可以对同一个间隙加锁;

  2. 间隙锁和行锁都加锁,合称 next-key lock,每个 next-key lock 是前开后闭区间,如果用 select * from t for update 把整个表所有记录锁起来,就形成了 7 个 next-key lock,分别是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum];

加锁规则

  1. 一:update ,delete,insert 语句 和 select … for update 加锁是一致的,都是排他锁,select … in share mode 会添加共享锁;

  2. 二:普通 select 语句只有在串行化的隔离级别,才会添加行锁和间隙锁,其他隔离级别不加锁;

  3. 三:读已提交的隔离级别,只会添加行锁,并且在语句执行完成后,就会把不满足条件的行上的行锁直接释放了,不需要等到事务提交;

  4. 四:在可重复读和串行化的隔离级别,会添加行锁和间隙锁;并且遵守两阶段锁协议;

  5. 五:查找过程中访问到的对象都会加锁,并且锁是加在索引树的节点的,读已提交下,加行锁,可重复读和串行化,是以 next-key lock 为单位加锁的, next-key lock 即行锁加上间隙锁;

  6. 六:如果是给唯一索引加锁,next-key lock 会退化为行锁,索引上的等值查询,向右遍历的最后一个不满足等值条件,next- key lock 会退化为间隙锁;唯一索引上的范围查询,会访问到不满足条件的第一个值为止;

  7. MySQL 后面的版本可能会改变加锁策略,所以这个规则只限于 5.x 系列 <=5.7.24,8.0 系列 <=8.0.13;

  8. 建议事务中,需要锁多个行,要把最可能造成锁冲突、影响并发度的操作尽量在事务后半段执行,使这部分上锁时间尽可能短;

加锁示例

  1. 举例,创建表 t,有字段 id,c,d,其中 id 是主键,c 上建立了索引,插入 6 条记录,分别是 (0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25),在可重复读的隔离级别;

  2. 语句 update t set d = d + 1 where id = 7,根据加锁规则五,首先会添加 (5, 10] 的 next-key lock,根据加锁规则六, 退化为间隙锁,所以加 (5,10) 的间隙锁;

  3. 语句 select id from t where c = 5 lock in share mode,根据加锁规则五,首先添加 (0, 5] 的 next-key lock,根据加锁规则六,会添加(5,10) 的间隙锁,并且注意这个查询,不需要回表,所以不会对主键索引树加锁,所以是 (0, 5] 和(5,10);

  4. 语句 select * from t where id>=10 and id<11 for update,根据加锁规则五,首先添加 (5, 10] 和 (10, 15] 的next-key lock,根据加锁规则六,等值查询10退化成行锁,所以是 10的行锁和 (10, 15] 的间隙锁;

  5. 语句 select * from t where c>=10 and c<11 for update,根据加锁规则五,首先添加 (5, 10] 和 (10, 15] 的next-key lock,

  6. 更多参考:https://time.geekbang.org/column/article/75659;

死锁

  1. 死锁,举例如下,事务 A 持有 id = 1 的行锁,等待获取 id = 2 的行锁,而事务 B 持有 id = 2 的行锁,等待获取 id = 1 的行锁,于是就发生了死锁;
    Mysql基础课六:数据库锁_第1张图片

  2. 解决死锁,一种方案是,锁等待超时退出,通过参数 innodb_lock_wait_timeout 设置,默认值是 50s,但往往这个值偏大,设置太小,又会造成非死锁退出;

  3. 另一种方案是,主动进行死锁检测,通过参数 innodb_deadlock_detect 设置,默认值是开启 on,这种方案能快速发现并进行处理的死锁;

  4. 死锁检测,在面临热点行,同时被 n 个线程执行更新操作,会触发 n^2 的死锁检测,会导致占满 cpu 资源,Mysql 执行很慢,这种情况下,推荐,在数据库层级做并发控制,需要引入中间间或者修改 Mysql 源码,或者,是在业务上做变通,将一行操作变成多行操作,如,更新大商户的余额,很多线程都更新一个值,导致死锁检测很频繁,可以设计余额由多行保存,来减少每行的冲突几率;

  5. 应用中避免死锁,不同session采用相同的顺序操作表,在批量操作中,将操作排序后有序处理,程序设计中捕获死锁异常并处理;

你可能感兴趣的:(Mysql)