mysql锁与事务

概念

事务是一系列操作所构成的执行单元,具有原子性,隔离性等的特点。
锁是为了防止多线程读写操作的并发问题而引入的解决方案。

锁分类

锁从不同角度划分,可以分为以下几类

乐观锁和悲观锁
  • 乐观锁认为不需要加锁,当多线程共同写操作时,引入version版本号机制。当读取的版本号与写入时的版本号不一致,则认为该数据已经被修改,写操作失败。mysql本身没有乐观锁,需要自己实现。
  • 悲观锁认为任何时候都需要加锁,所以多线程操作时,只有一个线程释放锁后,其他线程才能执行获取锁或者访问(需要区别读锁和写锁)。数据库的锁基本都是悲观锁。当然我们也可以使用乐观锁解决并发问题https://www.cnblogs.com/laoyeye/p/8097684.html
读锁和写锁
  • 读锁也叫共享锁(s锁),当一个事务(或者线程)对某一个对象加读锁时,其他事务可以再加读锁,但是不能加写锁select ... lock in share mode;
  • 写锁也叫排他锁(x锁),当一个事务(或者线程)对某一个对象加写锁时,其他事务不能再加读锁或者写锁。
    select ... for update 或者update,delete等
    Note:select不加任何锁,所以select查询任何时候都不会被阻塞。
按照锁范围分为全局锁,表锁,行锁,DML锁
  • 全局锁:全局锁就是对整个数据库实例加锁,加锁后所有数据库的所有表就处于只读状态,后续的DML,DDL写操作都将被阻塞。从而获取一致性视图,保证数据的完整性。但是全局锁对性能影响很大,会导致mysql hang住
    全局锁主要用于数据库备份操作,例如使用mysqldump命令是就会使用全局锁。该命令对于使用Innodb引擎的数据库可以使用--single-transaction参数来完成不加锁的一致性数据备份。

--single-transaction如何实现不加全局锁,保证一致性?
它会事务设置成repeatable read(可重复读),然后利用MVVC机制实现当前读数据,不对数据库本身进行锁表,不影响数据库的其他业务。

  • 表锁:锁住整张表,两个事务不能同时对一张表做更新操作,这样会降低并发性,但是不会出现死锁,也是大多数数据库引擎支持的类型。

  • 行锁是表中的一行或者多行加锁,这是Innodb的一个特征,行锁是根据索引加锁,所以如果查询没有使用索引,则会退化为表锁。
    Note:如果使用了相同的索引值,即使是不同行也会冲突,造成不能加锁或访问。

行锁的分类(记录锁,间隙锁,临间锁(记录锁+间隙锁))

  • 记录锁:锁住某几行记录
  • 间隙锁:对索引之间的范围加锁,使得其他事务不能在该范围内更新,为了解决可重复读中的幻读问题,所以只有可重复读和串行化中存在间隙锁。
    间隙锁加锁情况:
    ①当索引是主键索引和唯一索引时精确匹配时,只锁住该行(=或者in),当查询值不存在时会退化为间隙锁
    ②当索引是普通索引时,通过精确匹配会锁住满足检索条件的范围(=后者in)例如,索引是2,5,11,23,45,78,当查询in(5, 45)时会锁住[2, 5)和[5, 11),[23, 45)和[45, 78)当插入[11, 23)时不会阻塞。如果查询值为10,则锁住[5, 11)
    ③范围查找时会锁住指定范围,例如id>10 and id<30将锁住,如果10,30不是索引,索引值为4, 15, 17, 27, 35, 56将锁住[4, 35)
MDL锁

MDL锁:(metadata lock),又名元数据锁, MDL锁是表锁的一种。分为 MDL读锁和写锁。

  • MDL读锁:所有的DML语句(insert,update,delete操作)
  • MDL写锁:修改表结构的操作,包括增删改列,增删改索引等

MDL锁的兼容性和普通读写锁一致。也即:读锁之间相互兼容,写锁与读锁或写锁互斥。
Online-DDL
对于MDL锁出现的阻塞DML的执行问题 ,产生了Online-DDL 【MySQL】Online DDL详解
Online-DDL包括三种算法,需要在alter table t1 add column ALGORITHM=?;时指定算法

  • COPY —— MySQL 5.6之前修改表结构的默认算法,全程无法并行DML。
  • INPLACE —— MySQL 5.6出现的,在开始和提交的短时间里,不能并行DML,其他部分时间可以并行执行。
  • INSTANT —— MySQL 8.0.12出现的,唯一会阻塞只读的时机是在清理旧表结构和表定义缓存时。

①COPY采用复制临时表的方式实现修改表结构,所以旧表向新表复制数据时需要添加MDL写锁,并发性能低
②INPLACE 在原表上进行更改,不需要生成临时表,不需要进行数据 copy 的过程。根据是否变更行记录格式,分为两类:

  • rebuild:需要重建表。比如 添加/删除主键,删除列、列重排序、修改列 NULL/NOT NULL 属性等;
  • no-rebuild:不需要重建表,只需要修改表的元数据,比如 添加列、添加/删除索引、修改列名、修改列默认值、修改列自增值,列VARCHAR长度变更等
    对于需要rebuild 的DDL,需要使用MDL锁。在此过程中执行DML会被缓存下来,等待MDL锁释放后再执行。而且INPLACE 只会在准备和提交的短时间内添加MDL排他锁,所以INPLACE 性能相对于COPY有很大提升。

③INSTANT 只需修改数据字典中的元数据,无需拷贝数据也无需重建整表,同样,也无需加排他MDL锁,原表数据也不受影响。整个DDL过程几乎是瞬间完成的,也不会阻塞DML。目前还不支持全部的DDL操作。

执行DDL时,该如何选择
执行DDL操作时,ALGORITHM选项可以不指定,这时候MySQL按照INSTANT、INPLACE、COPY的顺序自动选择合适的模式。也可以指定ALGORITHM=DEFAULT,也是同样的效果。如果指定了ALGORITHM选项,但不支持的话,会直接报错。

注意事项
执行大表DDL操作,更能造成的风险
①如果在频繁查询的表中执行DDL(表修改操作),可能会造成数据库长时间阻塞
例如在alter操作后,执行大量查询操作。show processlist会发现大量waiting for table metadata lock的线程,数据库连接很快就会消耗完,导致业务系统无法正常响应。解决方法:此时需要手动kill掉DDL的事务ID,使后续的DML操作可以正常进行。
②而且容易导致数据库CPU、IO性能损耗,降低MySQL服务性能;
③中途写入失败需要进行回滚,回滚这段时间也是不可写入;

意向锁 意向锁详解

意向锁的出现是为了解决共享锁和排他锁查询的效率问题

比如,如果事务A执行update user set name='test',想对user表添加 表级 排他锁,注意是表级排他锁。需要执行以下操作
①先判断user表上有没有共享锁和排他锁
②再遍历user表中的索引树上的节点有没有共享锁和排他锁
这样做会降低加锁的效率,所以就出现了意向锁

意向锁说明:如果需要对行锁添加共享锁,就需要在相应的表上添加意向共享锁;如果对行锁添加排他锁,需要在相应的表上添加排他锁。需要注意是行锁才会添加意向锁,如果是表锁的不需要,因为表锁加在表上,直接就可以判断。

这是当事务A添加表锁时先判断表上是否有意向锁,再决定是否可以加锁,意向锁与读写锁的互斥性与读写锁之间的互斥性相同。


意向锁与读写锁的互斥性

意向锁之间是相互兼容的,也就是说如果已经存在意向锁,还是可以再次添加意向锁的。比如:
如果事务A执行select * from user where id=1 lock in share mode,需要对user表添加行锁。此时的判断逻辑
①先判断user表是否添加了表锁
②再判断user中索引为1的节点是否存在行锁,再决定是否要加锁
③如果可以加锁,则添加意向排他锁以及节点为1的共享锁,不需要判断是否存在意向锁

事务

事务的种类:读未提交,读已提交,可重复读,串行化
  • 读未提交:一个事务读取了另一个事务未提交的更新,会导致出现脏读。事务a进行更新时,事务b使用select(任何时候都可以读)查询到了修改的记录,但事务a并未提交,导致事务b读取到了脏数据。
  • 读已提交:读取一个事务已提交的更新,可以解决脏读。读已提交使用mvcc机制实现:

MVCC(多版本并发控制):数据库会生成ReadView(当前活跃的事务id)和版本链(版本链中的每个记录都会有一个事务id(trx_id)和指向上一次修改的指针roll_pointer),事务读取记录时,如果发现事务a的id小于要查询记录的版本链中中的事务id,说明这个版本记录中的事务已经提交了,可以使用;如果发现查询记录中版本链中的事务id在ReadView中,则说明该事务还未提交,则不能使用,然后查找roll_pointer指向的下一条记录,然后比较事务id是否在ReadView中。

  • 可重复读(innodb的默认隔离级别):可重复读解决了在同一个事务中多次读取数据不一致的情况。包括不可重复读以及幻读的问题。解决方法分为select实现和非select实现。

可重复读之前存在的问题:

  • 不可重复读:一个事务中多次读取的内容不一致。例如:数据表中有三条记录(id=1,2,3),事务a查询id小于10的记录,总共三条,但是锁住的只是id为123的记录,事务b插入新的记录4不阻塞,则事务a再次查询时会出现四条记录。
  • 幻读:数据表中没有id为2的记录,事务a查询后insert一条id为2的记录,但是事务b也insert一条id为2的记录,这时由于查询时没有锁住id为2的行,所以事务a会插入失败,提示id冲突,即见鬼了。

解决方法:

  • select实现(快照读):select不加锁,为了让select实现可重复读,RR将RC(读已提交的)的mvcc拿过来,但是同一事务中每次select都使用同一个ReadView所以,读取的是历史数据,所以此时select也被叫做快照读。
  • 非select实现(当前读):RR中除了select之外的都会更新ReadView,所以也叫当前读。但是会出现不可重复读和幻读的问题(详见下),所以非select方式(当前读)使用临间锁(记录锁+间隙锁)(详见下)实现。即通过锁不让其他事务在指定范围内更新数据。既保证了数据新鲜又能可重复读。

Note:读已提交和可重复读都使用了mvcc但是使用读已提交时,每次select都会生成新的ReadView,所以第二次select可以查到与第一次不同的数据。但是可重复读为了不解决一个食物中不可重复读的问题,不生成新的ReadView,这样一个事务中两次select的是同样的数据。


mvcc中记录的版本链示意图
  • 串行化:会将所有的select语句加上共享锁lock in share mode,这将导致读写不能并发,效率低。

参照:MySQL的锁机制和加锁原理
mvcc机制
Innodb锁机制:Next-Key Lock 浅谈
面试中的老大难-mysql事务和锁,一次性讲清楚! - 掘金 (juejin.cn)

你可能感兴趣的:(mysql锁与事务)