初学MySQL—全局锁、表锁、行锁


数据库中表设计的初衷就是处理并发问题,当出现并发访问的时候,锁就是数据集库用来合理控制访问资源的访问规则。

根据加锁的范围,MySQL中的锁大致分为全局锁表级锁行锁

全局锁

全局锁就是对整个数据库实例加锁,MySQL中提供的加锁的命令是:

Flush tables with read lock(FTWRL)

使用这个命令可以让整个库处于只读状态,其它线程例如:(增删改操作)都会被阻塞;

使用场景

全库逻辑备份,把整库每个表都select出来存成文本;

整库只读:

  • 从主库上备份,在备份期间不能执行更新,业务基本要停摆;
  • 从主库上备份,在备份期间从库不能执行主库同步过来的binlog,导致主从延迟;

思考以下不加锁的结果?
现在一个电商网站发起逻辑备份,在备份期间呢,你买了一双球鞋,在业务逻辑中先扣掉余额,然后在已购买的列表里添加球鞋;

从时间顺序上,肯定是先备份了用户的余额,然后用户购买,在备份相应的购买商品;

出现的问题就是,用户的账号内的余额没扣,但是已经买到了鞋子;

那么不加锁出现的问题就是备份系统备份得到的库不是一个逻辑时间点的,这个视图逻辑不一致;

官方自带的逻辑备份工具是mysqldump,使用参数-single-transaction, 在导数据前会启动一个事务,来确保拿到一致性视图,由于MVCC的支持,数据是可以正常更新的;single-transaction方法 只适用于所有表使用事务引擎的库,一致性读的前提是存储引擎支持这个隔离级别;

对于MyISAM这种不支持事务的引擎,在备份中有更新,破坏备份的一致性就必须使用FTWRL命令;

为何不使用readonly

既然表级锁是全库只读,为何不直接set global readonly = true

readonly是可以让全库进入只读状态,还是建议使用FTWRL,原因如下:

  • 在一些系统中,readonly的值会用作其它逻辑,修改global变量的方式影响面更大;
  • 在异常处理机制中,执行FTWRL命令之后,客户端异常重启,MySQL会释放这个锁,整个库可以到正常更新的状态;设置为readonly,客户端发生异常,数据库就会一直保持不可写状态,风险较高;

表级锁

表锁

表锁的语法是:lock tables…read/write,可以采用unlock tables主动释放锁,也可以在客户端断开的时候自动释放;lock tables除了限制别的线程读写外,也会限定本线程接下来的操作:

例如,如果某个线程A执行语句lock tables t1 read,t2 write;其它线程写t1,读写t2的语句都会被阻塞;同时,线程A在执行unlock tables之前,也只能执行读t1,读写t2的操作;

元数据锁(metadata lock)

MDL不需要显示使用,在访问一个表的时候会自动加上,MDL的作用是保证读写的正确性;

当对一个表做增删改查操作的时候,加入MDL读锁;当要对表结构变更操作的时候,加入MDL写锁;

  • 读锁之前不互斥,多线程可同时对一张表增删改查;
  • 读写锁之间、写锁之间是互斥的,用来保证变更表结操作的安全性,如果有两个线程同时对一个表加字段,其中一定一定要等另一个执行完才能开始执行;

思考一下,如何安全的给一个小表加字段?
在这里插入图片描述
sessionA先启动会对表t加一个MDL读锁;
sessionB需要的也是MDL读锁,都可以执行;
sessionC会被阻塞,因为sessionA的MDL读锁还未被释放;
随后的要在表t上新申请MDL读锁的操作都会被sessionC锁阻塞,如果客户端有重传机制,查询请求又很频繁,这个数据库的线程会爆满;

回到上面的问题,如何安全的给小表加字段呢?

  • 先解决长事务,在MySQL中information_schema库的innodb_trx表中,查询当前执行的事务,可以考虑暂停DDL或者kill长事务;
  • 对于热点表(数据量不大,请求频繁),在alter table语句内设置等待时间,如果在等待时间内拿到MDL写锁最好,拿不到也别阻塞后面的业务语句;
MariaDB已经合并了AliSQL,都支持DDL NOWAIT/WAIT n这个语法
ALTER TABLE tbl_name NOWAIT add column ...
ALTER TABLE tbl_nme WAIT N add column...

行锁

MySQL的行锁是在引擎层的各个引擎自己实现的,并不是所有的引擎都支持行锁,InnoDB是支持行锁的,这也是其取代MyISAM的一个重要原因之一,如何通过减少锁冲突来提升业务并发度。

行锁就是针对数据表中行记录的锁,比如事务A更新了一行,这个时候事务B也更新了一行,必须等到事务A的操作完成之后才可以更新;

两阶段锁

两阶段锁
实际上事务B的update语句会被阻塞,直到事务A执行commit之后,事务B才可以继续执行;在InnoDB中,行锁是在需要的时候才加上,但是要等到事务结束时才释放,这就是两阶段协议

那么如果在事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。

死锁和死锁检测

在并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源,就会导致这几个线程都会进入无限等待的状态,就是死锁。
死锁和死锁检测
上图就是,事务A在等待事务B释放id=2的行锁,事务B在等待事务A释放id=1的行锁,二者互相等待对方的资源释放,就进入了死锁状态,当出现死锁时有两种应对策略:

  • 第一种直接进入等到,直到超时,超时时间参数为innodb_lock_wait_timeout;
  • 第二种发起死锁检测,发现死锁时,主动回滚死锁链条中的某个事务,让其它事务得以继续执行,将innodb_deadlock_detect设置为on,表示开启这个逻辑。

innodb_lock_wait_timeout的默认时间是50s,第一个锁住的线程要过50s才会超时退出,其它线程才可以继续执行,太长太短都不好,建议采用第二种策略。

每个新加入的线程都会判断是不是由于自己的加入导致了死锁,这是一个时间复杂度为O(n)的操作,假设有1000个并发线程同时更新同一行,那么死锁检测的操作百万量级的。即便最终没有检测到死锁,但是耗费了大量的CPU资源,导致CPU的利用率很高,每秒却无法执行多个事务。

如果能够控制并发度,一行最多只有10个线程在更新,死锁检测的成本低就不会出现这个问题,并发控制要在数据库服务端,也可以考虑在中间件中实现。在MySQL中实现就是在进入引擎前排队,这样InnoDB内部就不会有大量的死锁检测工作了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值