Mysql锁机制与优化实战以及MVCC底层原理剖析

162 阅读9分钟

Mysql各种锁

  1. 从性能上分为乐观锁和悲观锁
  • 乐观锁

适用于读操作较多的场景,如果写操作较多的场景使用乐观锁会导致比对次数过多,影响性能

  • 悲观锁

适用于写操作过多的场景

  1. 从对数据库操作的类型分为:读锁和写锁(都属于悲观锁)、意向锁
  • 读锁:也叫共享锁,针对同一份数据,索格读操作可以同时进行而不会相互影响
  • 写锁:也叫排他锁,数据修改操作都会加上写锁,当前写曹祖没有完成之前,会阻塞其他的写锁和读锁
  • 意向锁:一个标识,告诉其他的事务,这张表里面有事务加锁了,你不用来加锁了

为了避免多个事务加锁冲突,提高加表锁的效率。假如一个事务对表中的一行记录加了行锁,另外一个事务要来对这张表加表锁,正常情况下需要判断每一行记录的索引是否加了锁,都没加锁的话才能加表锁,这样太慢了。意向锁就设置一个标识,标识当前事务有在表中加行锁,当其他事务想要来加写锁的时候,判断意向锁是否已经加了,如果加了,就不用再遍历每行记录是否加过锁了

image.png 3. 从对数据操作的粒度分为:表锁、行锁、页锁

  • 表锁

开销小,加锁快;不会出现死锁;锁的粒度大,发生锁冲突的概率比较最高。并发度最低;一般用在整表数据迁移的场景

  • 页锁

只有BDB存储引擎中的,解决表锁的粒度太大

  • 行锁

加行锁之前得先找到这行记录,是有更多的开销的,因此开销大,加锁慢,锁的粒度最小,发生死锁的冲突概率最低,并发度最高

注意:行锁实际上是针对索引加的锁(在索引对应的索引项上面做标记),不是针对整个行记录加锁。并且该索引不能够失效,否则会升级到表锁(RR级别会升级,RC级别不会升级) 比如在RR级别下执行下面的sql语句:

select * from account where name = 'lilei' for update;  --where条件中的name字段无索引

因此在事务提交之前其他事务不能对该表的任意一行记录做修改操作

image.png

InnoDB相对于MyIsam的最大不同有两点:

  • InnoDB支持事务
  • InnoDB支持行级别的锁

间隙锁

在不想其他事务插入的区间锁一条记录,则整个间隙都锁住了,再打一个标记

| (3,10) | 1 |

| (10,20)| 0 |

| (20,正无穷) | 0 |

间隙之间加锁了就打一个标记1 间隙锁在可重复读隔离级别下才会好生效,锁的是两个值之间的间隙,Mysql默认的隔离级别是repeatable read,有幻读问题,MYSQL通过间隙锁解决了幻读

image.png

针对上面表 那么间隙就有 id 为 (3,10),(10,20),(20,正无穷) 这三个区间,在Session_1下面执行如下sql:

select * from account where id = 18 for update;

则其他Session没法在这个(10,20)这个间隙范围里插入任何数据。

如果执行下面这条sql:

select * from account where id = 25 for update;

则其他Session没法在这个(20,正无穷)这个间隙范围里插入任何数据。

也就是说,只要在间隙范围内锁了一条不存在的记录会锁住整个间隙范围,不锁边界记录,这样就能防止其它Session在这个间隙范围内插入数据,就解决了可重复读隔离级别的幻读问题。

临间锁

间隙锁和行锁的组合

image.png

总结:

  • MyISAM在执行查询语句SELECT前,会自动给涉及的所有表加读锁,在执行update、insert、delete操作会自动给涉及的表加写锁。
  • InnoDB在执行查询语句SELECT时(非串行隔离级别),不会加锁。但是update、insert、delete操作会加行锁。另外,读锁会阻塞写,但是不会阻塞读。而写锁则会把读和写都阻塞。
  • Innodb存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会要更高一些,但是在整体并发处理能力方面要远远优于MYISAM的表级锁定的。当系统并发量高的时候,Innodb的整体性能和MYISAM相比就会有比较明显的优势了。但是,Innodb的行级锁定同样也有其脆弱的一面,当我们使用不当的时候,可能会让Innodb的整体性能表现不仅不能比MYISAM高,甚至可能会更差。

锁优化实践

  • 少用用非索引字段做一些事情,尽可能的让所有的数据检索通过索引来完成,避免无索引行锁升级为表锁
  • 合理设计索引,尽量缩小锁的范围
  • 尽可能减少检索条件的范围,避免间隙锁
  • 尽量控制事物的大小,减少锁定资源量和时间的大小,涉及事务加锁的sql尽量放到事务最后去执行
  • 尽可能用低的事物隔离级别

MVCC多版本并发控制

解决RR和RC两个隔离级别,对于同一条数据读写并发冲突的问题 不同的事务操作同一条数据的时候并行执行

  • 读历史数据,写最新的数据

MVCC是为了解决事务操作过程中并发安全问题的无锁并发控制技术,解决并发冲突,提高并发性能 实际情况中读的场景还是比较多的,写的场景比较少,MVCC支持并发场景下不同的事务访问到不同的数据版本。    MVCC采用乐观锁实现,降低死锁的概率  

  • 数据库行数据中两个重要的隐藏字段:
  1. DB_TRX_ID:创建或者修改数据的事务ID;
  2. DB_ROLL_POINT:回滚指针,指向记录的上一个版本

只有最新的日志会在数据里头,其他数据在回滚日志undolog中。

当前读:读取记录的当前最新版本,占有式的读,其他事务不能修改,执行以下语句可以实现当前读

  1. select .....for update
  2. select.....lock in share mode
  3. insert
  4. update
  5. delete

快照读:执行普通的select语句查询到的数据结果

还有一个核心read-view

 对下面的数据版本连进行分析

 205,255,300这三个活跃的事务都是没有提交的,也就是说事务300去修改name = mvcc,这条修改是未提交的状态。那么数据库中的任何一个事务读数据到底读哪个版本呢,针对上图如果说此时创建一个事务301来进行读取数据,因为事务300对数据进行了修改name改成了mvcc,300这个事务没有提交,此时301这个事务是不能看到修改的mvcc的值的,此时就会在undolog中找历史版本,会根据回滚指针往下找,一看找到事务200对应的name值为sql,然后对比一下他没在活跃事务组中即【205,255,300】,说明事务200已经提交,在读已提交的隔离级别下301这个事务是能够读到这条记录的即读到name = sql.  再看最左边如果对于事务150   一开始查肯定也是查到300这条事务对应的记录,但是发现300比150要大,显然300这个事务是在150这个事务查询之后对数据进行修改,150这个事务先有,然后150这个事务之后又有人改了即300这个事务将name改为mvcc,显然150这个事务只能查到在他之前的已经提交的事务修改的数据的值,查看一下版本链,往下找找到事务100对应的数据name=mysql,查到结果返回。因为100小于150说明在事务150之前对数据进行了修改而且不在活跃事务里头说明100这个事务已经提交,可以被150这个事务读到。 如果事务在205到300之间,那么刚好等于活动事务id为300,说明就是我事务300本身,我正在update,显然是可见的,这种情况查到的数据就是name = mvcc.  上面这种情况就是说299这个事务不在活跃事务里头,而且299比300小,现在查出来的第一条记录是300,但是300这条事务没有提交,显然数据是不可见的。 可重复读是利用mvcc和间隙锁机制解决幻读的。 读已提交的隔离级别下,如果上述活跃事务300已经提交了,那么事务299能够读到事务300提交的最新数据

 可重复读隔离级别下read-view只创建一次,读已提交情况下read-view创建好多次

 对下面的数据版本连进行分析

image.png

image.png RR和RC隔离级别都是通过MVCC机制来保证并发事务修改下数据的可见性的,只不过是RR级别下,read-view视图只创建一次,RC隔离级别下每执行一次sql查询语句操作都会创建。 Rc隔离级别下,如果其他事务没有执行commit操作,生成的read-view视图和RR级别下是一样的

RR级别:不同的read-view代表当时事务提交与未提交的一个状态。