深入浅出MySQL的事务和锁

MySQL 锁的种类

根据MySQL加锁的粒度,从高到低依次可以分为,全局锁,表级锁和行锁。

根据加锁的方式,又可以分为读锁(又称为共享锁,简称S锁),和写锁(又称为排他锁,简称X锁)。

全局锁

全局锁就是对整个数据库加锁,使其处于只读状态,可以用Flush tables with read lock (FTWRL)语句来实现。

使用场景是对数据库做逻辑备份,试想如果有2个相关联的数据库要做逻辑备份(相关联是指,如果更新了一个数据库中的数据,另一个数据库中的数据也要随之更新),当备份其中一个数据库的时候,另一个数据库必须处于只读状态,否则会造成数据在逻辑上的不一致。

但是如果使用全局锁来做备份,性能必然会很差,并且有停服的风险。MySQL自带的逻辑备份工具mysqldump其实是用一致性试图来做备份的,在开始备份前会拿到一个一致性试图,由于MVCC的支持,备份的过程中数据也可以正常写入。这样的方式是比较安全的。但是对于一些不支持事务的引擎,比如MyISAM就只能用全局锁来做了。

表级锁

表级锁就是锁的粒度是表级别的,MySQL的表级锁有2中,分别是:表锁,元数据锁(MDL)。这边要提一下MDL,MDL是锁住表结构,不需要显示使用,在访问MySQL表的时候会被自动加上读锁,如果要修改表结构,那么需要加写锁。

行锁

在还没有出现行级锁的时候,表级锁是常用的方式。现在InnoDB会优先加行锁。

S锁和X锁

拿到S锁的事务可以读数据,但是不能写数据。拿到X锁可以读写数据。其中S锁和S锁不互斥,其余的组合都互斥。

两阶段锁协议

简言之,就是规定了在一个事务中,锁什么时候被加上,什么时候会释放。

加锁的时间点无非有下面2个:

  1. (A) 事务开始的时候加锁,对所有涉及到的行加锁
  2. (A) 扫描到相关行的时候加锁

释放的时间点也有2个:

  1. (B) 事务结束的时候释放所有的锁
  2. (B) 对相关行操作结束的时候释放锁

排列组合一下,一共有4中组合。其实不难发现,1(A)和2(B)是不合理的。

如果在事务开始的时候加上所有锁,会导致格外的性能开销,并且加了锁也要等后面可能才会用到,并不会有安全性上的提升。

其次,如果在相关行操作结束后就释放锁,那么当这个事务没有结束的时候,他的修改就可以被别的事务利用,如果事务回滚,会非常麻烦。

所以结论就是,InnoDB的行锁在需要的时候被加上,在事务结束的时候会释放。

所以在一个事务里面尽量把不需要加锁的操作写到前面,这样可以减少事务中锁的持有时间,以提升性能。

MySQL 事务的支持

事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )。

事务隔离的实现

如果要手写事务隔离的实现,最简单暴力的方式是,在事务内部保存一个数据的副本。

  1. 对于RU,当一个事务读取某行数据的时候,我们需要检查其余所有事务,并且取出最新的数据。为了取出最新数据,我们需要在数据上保存一个时间戳。
  2. 对于RC,可以直接从DB读,但是更新需要保存成一个副本。因为所有DB中的数据,都是已经提交的。
  3. 对于RR,在事务开始的时候,需要保存所有可能用到的数据的副本。
  4. 对于serializable,只要控制事务是串行的即可。

优化

–未完待续–