欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

MySQL 笔记整理(7) --行锁功能:怎么减少行锁对性能的影响?

程序员文章站 2022-11-22 09:23:51
笔记记录自林晓斌(丁奇)老师的《MySQL实战45讲》 7) --行锁功能:怎么减少行锁对性能的影响? MySQL的行锁是在引擎层由各个引擎自己实现的。因此,并不是所有的引擎都支持行锁,如MyISAM引擎就不支持行锁。对于不支持行锁的引擎,只能使用表锁来进行并发控制。对于这种引擎的表,同一张表上任何 ......

笔记记录自林晓斌(丁奇)老师的《mysql实战45讲》

7) --行锁功能:怎么减少行锁对性能的影响?

  mysql的行锁是在引擎层由各个引擎自己实现的。因此,并不是所有的引擎都支持行锁,如myisam引擎就不支持行锁。对于不支持行锁的引擎,只能使用表锁来进行并发控制。对于这种引擎的表,同一张表上任何时刻只能有一个更新在执行,这就会影响到业务并发度。innodb是支持行锁的,这也是myisam被它替代的一个重要原因。

  顾名思义,行锁就是针对数据表中行记录的锁。比如事务a要更新某一行,同时事务b也要更新这一行,则b必须等待a的操作完成后才能进行更新。

两阶段锁协议:

  在innodb事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。因此,如果你的事务中需要锁多个行,要把最可能造成锁冲突,最可能影响并发度的锁尽量往后放。

  举一个具体的例子:

  客户a要在电影院b购买电影票,这可能涉及三个操作:

  1. 在a的账户余额中扣除电影票的票价
  2. 在影院b的余额中增加这张电影票的票价
  3. 记录一条交易记录。

  当然为了保证交易的原子性,我们要把这三个操作放在同一个事务中。那么你应该怎样安排上面的语句呢?(两条update,一条insert)。根据两阶段锁协议,不管你怎样安排顺序,所有的锁都是在需要时加上,事务提交时释放。因此,如果你把更新影院b余额的操作放在最后,那么影院余额这一行的锁的时间就最少,最大程度地减少了事务直接的锁等待,提升了并发度。

死锁和死锁检测:

  当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致几个线程都在进入无限等待的状态,称为死锁。当出现死锁后我们有两种策略:

  • 直接进入等待,直到超时,这个超时时间可以通过参数innodb_lock_wait_timeout来设置。
  • 另一种策略是,发起死锁监测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数innodb_deadlock_detect设置为on,表示开启这个逻辑。

  第一种策略的默认值是50s,显然在生产环境这个值是无法接受的。但如果你把这个值设置的很小,如1s,当出现死锁时确实可以很快解开,但也会出现很多误伤。因此我们常采用第二种策略:主动死锁检测。并且,innodb_deadlock_detect的默认值本身就是on。主动死锁检测在发生死锁的时候能够快速发现并进行处理,但是它也有额外负担。例如有1000个线程并发更新同一行,那么死锁检测会这样工作,这1000个线程每个的事务被锁的时候都要去看一下它所依赖的线程有没有被别人锁住,如此循环来判断是否出现了循环等待。那么此时死锁检测的就会消耗大量cpu资源,你就会看到cpu利用率很高,但是每秒执行不了几个事务。

  那么怎么解决这种热点行更新导致的性能问题呢?问题的根源在于,死锁检测要耗费大量的cpu资源。一种治标的方式是,如果你能确保事务一定不会出现死锁,可以临时把死锁检测关掉。当然这个操作伴随着一定的风险,另一个思路是控制并发度。根据上面的分析,你会发现如果并发能够控制住,比如同一行在同一时间最多只有10个线程在更新,那么死锁检测的成本就是可以接受的。当然你也可以从设计上来优化这里的逻辑。

上篇问题:

  备份一般都会在备库上执行,你在用-single-transaction方法做逻辑备份的过程中,如果主库上的一个小表做了一个ddl,比如给一个表上加了一列。这时候,从备库上会看到什么现象呢?

  (dml: data manipulation language,数据操纵语言,增删改查等;ddl:data definition language,数据库定义语言,修改表结构等;dcl:data control language数据库控制语言,修改用户权限等)

  假设这个ddl是针对表t1的,这里把备份过程中几个关键的语句列出来:

q1:set session transaction isolation level repeatable read;
q2:start transaction  with consistent snapshot;
/* other tables */
q3:savepoint sp;
/* 时刻 1 */
q4:show create table `t1`;
/* 时刻 2 */
q5:select * from `t1`;
/* 时刻 3 */
q6:rollback to savepoint sp;
/* 时刻 4 */
/* other tables */

  在备份开始的时候,为了确保可重复读隔离级别,再设置一次隔离级别(q1时刻)。启动事务,使用with consistent snapshot确保这个语句执行完成后就可以得到一个一致性视图。(q2时刻)设置一个保存点.(q3时刻)。拿到t1的表结构(q4时刻)。正式导数据(q5时刻)。回滚到savepoint sp,在这里的作用是释放t1的mdl锁。

我们题目设定中t1是小表,假定ddl到达后如果开始执行则很快就能执行完成。

参考答案如下:

  1. 如果在q4语句执行之前主库的ddl到达,现象:没有影响,备份拿到的是ddl后的表结构。
  2. 如果在‘时刻2’到达,则表结构被改过,q5执行的时候,报table defination has changed,please retry transaction,现象mysqldump终止。
  3. 如果在“时刻2”和“时刻3”之间到达,mysqldump占着t1的mdl读锁,binlog被阻塞,现象:主从延迟,直到q6执行完成。
  4. 从“时刻4”开始,mysqldump释放了mdl读锁,现象:没有影响,备份拿到的是ddl之前的表结构。

问题:

  如果你要删除一个表里面的10000行数据,有以下三种方式:

  1. 直接执行 delete from t limit 10000;
  2. 在一个连接中循环执行20次delete from t limit 500;
  3. 在20个连接中同时执行delete from t limilt 500;

  你会选择哪种方式,为什么呢?