MySQL的锁设计
MySQL数据库的锁设计初衷是处理并发问题,数据库需要合理的控制资源的访问规则。
根据加锁的番位,MySQL里的锁大致可以分为全局锁、表级锁和行锁三类。
全局锁
全局锁是对整个数据库实例的加锁。
MySQL提供了全局读锁的方法,使用命令Flush tables with read lock(FTWRL)
。
对数据库加全局读锁后,数据库处于只读状态,所有修改命令都会被阻塞。
全局锁的经典场景就是全库的逻辑备份。也就是把整个库的数据select出来存档。
但是让数据库处于只读状态,听上去就很危险:
- 如果在主库上备份,那么整个备份期间,业务基本上无法正常使用。
- 如果在从库上备份,那么备份期间从库就不能执行从主库同步过来的binlog,导致主从延迟。
那为什么还需要全局锁呢?
MySQL自带的逻辑备份根据是mysqldump。使用参数-single-transaction
,导数据之前会启动一个事务,这样就确保了一致性视图,通过MVCC的支持,就能实现数据一致。
但是这是数据库引擎支持的, 对于MyISAM这种不支持事务的引擎,就需要用到FTWRL命令了。
表级锁
MySQL中的表级锁又分为两种:一种是表锁,一种是元数据锁(meta data lock,MDL)
表锁
表锁的语法是lock tables...read/write
。 与FTWRL类似,可以使用unlock tables
主动释放锁,可以在客户端断开的时候自动释放。lock tables
语法除了会限制别的线程的读写外,也会限定本线程接下来的操作对象;即只能访问它持有的锁资源。
所以锁表的操作一般不会使用,影响面还是太大了。
MDL元数据锁
MDL不需要显示的使用,在访问一个表的时候会自动加锁。 MDL的作用是保证读写的正确性。
比如一个查询正在遍历一个表的数据,而期间另一个线程对这个表结构做了变更,删除了一个字段,那么查询拿到的数据跟表结构对不上了。
MDL锁就是当一个表做增删改操作的时候,会加上MDL读锁,当对表做结构变更时,加MDL写锁。
行级锁
行级锁是由引擎层各自实现的,但并不是所有有的引擎都支持行锁。MyISAM就不支持行锁。不支持行锁就意味着,同一张表同时只能有一个更新在执行,这样就无法应对并发需求。
InnoDB支持行锁,所谓行锁就是正对一张表中的行记录的锁。
在InnoDB的事务中,行锁是在需要的时候才加上,但是并不是不需要的时候就立刻释放的。 它要等到事务结束才会释放。 因此,如果事务中需要锁多个行,那么要把最可能造成锁冲突,最可能影响并发的锁放在后面。 也就是说,把最可能被多个事务同时加锁的操作放在最后。这样对于一个事务来说,它占用这个热点锁的时间就少,也就减少了其他事务等待的时间。
死锁和死锁检测
现在有这样一个场景。
线程A开启事务
->修改记录1
->线程B开启事务
->修改记录2
->线程A修改记录2(记录2已被线程B加锁,线程A进入等待)
->线程B修改记录1(记录1已被线程A加锁,线程B进入等待)
->死锁
当出现死锁时,有两种解决策略:
- 第一等待直到超时。这个等待阈值可以通过mysql参数
innodb_lock_wait_timeout
来设置。 - 发起死锁检测,当出现死锁时,主动回滚死锁链中的某一个事务,让它为其他事务让道。通过mysql参数
innodb_deadlock_detect
设为on,开启死锁检测。
一般会采用第二种策略,因为第一种如果设置等待时间过长,那线程的等待时间过长,是无法接受的。但如果设置等待时间过短,又可能会影响到正常业务。
但第二种策略也有问题,当每一个事务要访问的行被锁的时候,就要遍历它所等待的线程要访问的行有没有被别人锁住,如此循环,最后判断时候出现循环等待(即死锁)。虽然解决了死锁,但是死锁检测的操作占用了巨大资源。
如何解决这一问题? 一种思路是控制并发度。比如同一行最多只能有10个线程来更新,那么死锁检测的成本就不会太高。
这个并发控制如果做到数据库服务端,可以考虑用中间件实现,或者有能力的修改MySQL源码,使其在进入引擎之前排队。
那有什么办法可以从业务的角度解决这一问题?
那就是尽量细化记录,而不是记录一个行。比如账户的金额,可以用多个记录的合来表示。