mysql中的锁

对于大家同时要使用的临时资源(并发问题),我们需要合理地控制资源的访问规则;锁是控制访问规则的重要结构。

mysql中的锁:全局锁,表锁、元数据锁(MDL),行锁、间隙锁等

** 加锁 ** 这个动作是存储引擎才能做的

全局锁

适用场景:全局逻辑备份 // 一般在备库上执行

Flush table with read lock(FTWRL)命令,让整个库处于只读状态

等待:进行中的读写事务完成,脏页完成刷盘 // 保证数据一致性

全局锁问题:主库加锁,业务停摆;从库备份加锁,停止同步主库binlog,主从延迟;

不能使用readonly来代替全局锁,原因:

  • readonly一般有逻辑业务含义,比如表示备库,只读
  • 全局锁在服务崩溃时释放;readonly崩溃后需要手动恢复,带来风险

可重复读快照实现备份

innodb有MVCC,可以使用一致性事务(快照)进行备份。mysqldump -single-transaction 启动可重复读级别的事务备份数据库,不阻塞库上读写。

* : innodb mvcc 快照是整库的,所以才能做数据备份

备库用-single-transaction做逻辑备份的时候,如果从主库的binlog传来DDL语句会发生什么?

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 */

时刻 1及之前传来,没影响,用的是DDL后的表;

Q5及之后传来没影响,用的是DDL前的表;

Q4和时刻2传来有影响,表结构和表数据会对不上;

表锁

lock tables … read/write,unlock table可以显示解锁。不支持行锁(innodb支持行锁)的存储引擎,使用表锁保护数据。

示例:lock tables t1 read, t2 write; 其他线程只能对t1进行读操作,不能对t2操作;同时当前线程也只能读t1,不能写t1,可以写t2;

元数据锁 MDL

MDL无需显示使用,访问表时自动添加;增删改数据时加MDL读锁,增删改表结构时加写锁。

在语句执行时申请,事务commit时释放 // 不是在语句执行完成时释放

阻塞链问题

sessionAsessionBsessionCsessionD
begin;
select * from t1; // 加读锁
beigin;
select * from t1; //加读锁
alter table t add c int; // 阻塞,期望加写锁
select * from t1; // 被期望加写锁阻塞,不能读

*: MDL队列是先进先出的公平队列,MDL是原子操作

此时,整个表不能读写,等待sessionA、sessionB的完成;更严重的情况:客户端查询频繁且有重试机制,很快会把线程打满。

解决方案

执行alter table t add c int写操作前,判断前面是否有长事务(大查询),选择一:等待长事务执行完成;选择二:kill掉长事务

如果需要写热点表,虽然数据量不大,但请求频繁,需要设置任务超时时间,改不了也不要影响其他语句,等有空再改;

表加字段需要全表扫描原因

存储格式一致性:

  • 所有行需要保证存储格式一致
  • 新增字段后,所有现有字段都需要更新以包含新字段的存储空间

索引重建:

  • 修改字段可能影响现有索引
  • 添加索引需要读取现有全部数据来构建

Online DDL 实现步骤

  1. 加MDL写锁
  2. 降级为读锁
  3. 真正做DDL // 这时整个表可以正常的读写
  4. 升级写锁
  5. 释放锁

Online DDL减少了锁持有的时间,调高了并发度。

重建表时允许增删改也使用了类似的思想。

行锁

  • 行锁在语句执行时添加,在事务提交时释放
    • 为提高并发度,减少锁持有的时间,所以尽可能将容易造成锁冲突、要锁住多个行的操作往后放
  • 行锁加锁不是原子操作:先获取意向锁(表级锁),再获取实际行锁
    • 表级锁 IS锁:意向共享锁;IX锁:意向排他锁

死锁问题

sessionAsessionB
begin;
update t set k=k+1 where id=1; // id=1这行加写锁
begin;
update t set k=k+1 where id=2; // id=2这行加写锁
update t set k=k+1 where id=2; // 阻塞,等待sessionB提交释放id=2这行的写锁
update t set k=k+1 where id=1; // 阻塞,等待sessionA提交释放id=2这行的写锁

如上,互相等待,死锁形成;

解决方案
超时控制

配置:innodb_lock_wait_timeut

问题

超时时间不好设置,太短容易误伤业务,太长也不好,都等待很长时间,都没执行 // 业务有损

死锁检测

发现死锁时,主动回滚一个事务

配置:innodb_deadlock_detect=on

问题

CPU计算成本高。每个事务加锁的时候,都套看看他所依赖的线程有没有被别人锁住; 可能出现热点行没执行几个事务,但CPU利用率很高;

解决方案

控制并发度 =》 死锁检测成本低

基本思路:对于相同行的更新在引擎前排队

实现位置:中间件、数据库服务端

间隙锁 gap lock

在可重复读隔离级别下还是有insert幻读的问题;幻读会导致主库数据和binlog不一致,进而导致主从数据不一致。

binlog记录是有顺序的。对于没有冲突的事务,先后顺序变化不会改变数据的最终结果,是可以并发的,哪个先完成先写入binlog都ok。但对于有冲突的事务,不同的执行顺序会造成数据的结果不一样,所以就需要锁来控制住先后的顺序,比如同时操作一行时,行锁会阻塞住后来的事务,等待当前事务提交再执行。

但行锁只能锁住已存在的数据,对插入数据不管用。所以需要间隙锁,锁住两行数据间的间隙,不允许插入,进而保证数据写入和binlog写入的一致性。

* 间隙锁:可重复读级别下才有间隙锁。读已提交级别下一般没有间隙锁,所以读已提交时需要设置binlog=row来保证数据一致性 // 读提交隔离级别下,在外键场景下有间隙锁

* 可重复读级别下,间隙锁等到事务提交才释放;读提交级别下 优化:语句执行过程加行锁,在语句执行完成后,就把不满足条件的行上的行锁直接释放,不需要等待事务提交 // 并发能力更好;

为什么需要加锁的示例

当前表数据:(0,0,0) (5,5,5) (10,10,10) (15,15,15) (20,20,20) (25,25,25) // id c d

sessionAsessionBsessionC
begin;
select * from t where d=5 for update; // (5,5,5);
update t set d=100 where d=5;
update t set d=5 where id=0;
update t set c=5 where id=0;
select * from t where d=5 for update;
insert into t values(1,1,5) ;
update t set c=5 where id=1;
select * from t where d=5 for update;
commit;

执行结果:

select * from t where d=5 for update; // (5,5,5);
update t set d=100 where d=5; // (5,5,100)

update t set d=5 where id=0; // (0,0,5)
update t set c=5 where id=0; // (0,5,5)

insert into t values(1,1,5) ; // (1,1,5)
update t set c=5 where id=1; // (1,5,5)

// 结果:(0,5,5) (1,5,5) (5,5,100) ...

如果for update没有对其他行锁,binlog中的顺序是:sessionB、sessionC、sessionA

// binlog=statement

update t set d=5 where id=0; // (0,0,5)
update t set c=5 where id=0; // (0,5,5)

insert into t values(1,1,5) ; // (1,1,5)
update t set c=5 where id=1; // (1,5,5)

// 注意:binlog是顺序写入,顺序执行(没优化的时候),binlog在事务提交时写入
select * from t where d=5 for update; // (0,5,5), (1,5,5),(5,5,5); 
update t set d=100 where d=5; // (0,5,100), (1,5,100),(5,5,100)

// 结果:(0,5,100), (1,5,100),(5,5,100) ...

主库事务的执行结果和binlog回放的结果不一致。所以,得加锁

加锁规则

间隙锁+行锁=next-key lock

  • 原则:加锁的单位是next-key lock,前开后闭
    • 间隙是由查到的右边的数据决定的
  • 原则:查找过程中访问到的对象都会加锁
  • 优化:等值查询时,最后一个不包含退化为间隙锁,右闭=>右开
  • 优化:如果是唯一索引,就退化成行锁
  • bug:唯一索引多锁一段,唯一索引的范围查询会访问到不满足条件的第一个值位置 // 因为是范围查询,多锁住了不满足条件的那行

死锁

间隙锁和插入数据冲突,间隙锁和间隙锁之间不冲突。所以不同事务可以对同一间隙加锁。

如果sessionA正序加锁,sessionB倒序加锁,就容易出现死锁。所以最好同一顺序处理。

死锁示例

当前表数据:(0,0,0) (5,5,5) (10,10,10) // id c d

sessionAsessionB
begin;
select * from t order by c for update;等sessionB释放id=10的锁
beigin;
select * from t order by c desc for update; // 等sessionA释放id=0的锁

二级索引加锁

覆盖索引

select id, name from t where name=‘zly’ lock in share mode;

name上有索引,这就是个覆盖索引,那么锁就会只加在二级索引上,主键索引上不会加锁,主键索引上能并发写。

如果需要阻塞主键索引的并发写,就需要绕过覆盖索引,比如添加查询一个不存在的字段。

select id, name from t where name=‘zly’ for update; // for update写锁会锁主键索引

回表

select id, name, age from t where name=‘zly’ lock in share mode;

只有name上有索引,就需要回表。回表是一个个回表查询加锁的。

*: 二级索引和主键索引上的间隙不同

优化

删除数据加limit能够减小加锁的范围,提高并发能力,控制删除数量;但只用limit删除,在binlog=statement下,从库删除的数据可能和主库不是同一个,所以开启binlog=row更安全。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值