快捷搜索:

MySQL锁的分类 以及并发事务下存在的问题以及问题的解决方式

引言

无论何时,多个查询在同一时间修改数据,就会产生并发控制的问题,为了解决并发控制可以使用锁机制来保证数据的安全性;

在 MySQL 中存在不同种类的锁,对于数据库性能调优以及选择合适的存储引擎来存储数据,了解这些锁是十分有必要的;

MySQL 按照锁的粒度划分为表锁,页锁,行锁;按照锁的类型分为共享锁,排他锁,意向锁;

意向锁分为意向排他锁,意向共享锁;

上面锁的分类可以使用下面思维导图总结 总体来说 MySQL 支持的锁的粒度有行锁,页锁,表锁,但是不同的存储引擎支持的粒度是不一样的;

行锁 表锁 页锁 InnoDB ✔️ ✔️ MyISAM ✔️ BDB ✔️ ✔️

从上表来看,单纯的看 InnoDB 这个默认存储引擎的话,只是支持行锁以及表锁的,这一点是需要注意的;

共享锁

共享锁也叫做读锁,当一个线程获得了表的共享锁之后,其他的线程可以读取表中的数据,但是不能在表中写入数据的;

每一个读的操作都需要获得共享锁,获得了共享锁的线程之间是可以同时运行的,但是会组织写操作;

排他锁

排他锁同时也叫做写锁,当一个排他锁开始工作的时候,也就是一个写锁开始工作的时候,其他的线程想要读取表中数据或者写表中的数据是不被允许的,一直到锁被释放之前,只有一个线程可以访问数据库中的表,其他的线程不管是想要读取数据还是修改数据还是添加数据都是不被允许的,其他所有的线程都是需要排队等待的;

在 MyISAM 是使用表一个级别的锁的,所有的线程想要访问这个表必须排队顺序的等待;因此这个存储引擎用来存储大的数据,比如 blog 字段,这个字段可以存储大型的数据;

在访问数据库的时候,刚开始使用的是共享锁访问数据,但是此时想改变共享锁使用排他锁访问数据,这个时候,无论是共享锁还是排他锁,都是表级别的锁;

Intention locks 意向锁

意向锁是一种表级别的锁; 意向锁,这个锁代表的是一种指示:这个锁代表着在数据库表中的某个位置存在一个常规锁,这个常规锁代表的可以是共享锁,也可以是排他锁;

举例:当一个线程想要得到表中的某行的共享锁,那么它必须先需要获取到表中的 Intention Lock;

意向锁定减少了管理锁所需的处理,同时允许并发锁之间的高度兼容性,所以意向锁被用来管理其他的常规锁,并且使得常规锁之间是兼容的;

引入了意向锁之后,可以更加容易的检测到表锁以及行级锁之间的冲突;

例如,一个线程在整个表上获得了一个排他锁,那么该表中的任何行都不应该被其他线程读取或写入,无论它们是共享锁还是排他锁。如果没有意向锁,系统需要分别检查行级锁和表级锁的兼容性。有了意向锁,只需要检查意向锁之间的兼容性即可。

使用意向锁的规则是: 当一个事务想要获取共享锁之前,必须先获取表中的意向锁;当一个事务想要在表中的某行数据获取排他锁的时候,必须先要获取到表中的意向锁;

如果线程A持有一张表的排他锁,那么就无法获取表中的行锁,有了意向锁,系统只需要检查表锁和意向锁的兼容性,不需要检查行级锁和表锁的兼容性。这加快了冲突检查。

如果没有意向锁,事情可能会变得复杂。例如,在一个事务中,需要进行多次行更新操作,因为行级锁不应该阻塞其他行级锁,所以表不应该被锁定。在这个事务的中间,另一个事务可能需要锁表(例如模式改变),因为没有人锁表,它可以获取表锁。这与之前的交易有冲突。换句话说,没有简单的方法来检测表锁和行级锁之间的冲突。

行锁

行锁是 InnoDB 里面粒度最小的锁,同时并发度也是最高的,适合高并发的场景使用;

行锁的分类有: 记录锁:单行记录上的锁。 间隙锁:锁定范围,但是不包含记录本身。 Next-key Lock(也叫临键锁):锁定范围并锁定记录本身(左开右闭)。

关于 InnoDB 行级锁注意事项

InnoDB 中,行级锁是是基于索引的,给的是索引项加锁也就是 key 进行加锁,没有索引的话,会使用表锁;oracle 使用的对数据项加锁,实现与 MySQL的 InnoDB 是有区别的;

在 InnoDB 中是可能产生死锁的,但是MyISAM 中不会产生死锁,因为MyISAM 一次性获得所有的锁,要么全部获得,要么全部等待,但是 InnoDB 的锁是逐步获得的,有造成死锁的可能性;

在 MySQL 中,行级别的锁先锁定索引,从索引字段特性进行分类的话,索引分为主键索引以及非主键索引;

有一个 SQL 语句操作了主键索引,那么 MySQL 就会锁定这个主键索引;如果一个 SQL 语句操作了非主键索引,那么就会先锁定非主键索引,然后锁定主键索引,那么这个时候,如果两个线程一个锁定了主键索引,等待非主键索引,另外一个线程锁定了非主键索引想要获取主键索引,那么就会产生死锁;

一般InnoDB 可以检测到死锁的存在,有下面的方式可以避免死锁: 1、同一个事务中,尽可能锁定所有需要的资源; 2、对于容易产生死锁的业务可以考虑使用表锁; 3、多个线程想要并发访问多个表,尽量约定按照顺序访问表,这样可以减少死锁的产生;

页锁

开销和加锁速度介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度一般;

表锁

当多事务并发访问数据库的时候,假设 A 事务最先访问数据库的表并且获取到表锁,其他的事务想要访问事务 A 持有表锁的表的时候,必须进入等待对列,直到事务 A 事务执行结束,释放了表锁之后,其他的事务才能通过竞争获取到表锁,访问数据;

表锁的粒度是最大的,同时并发度也是最低的;

多事务并发访问数据库存在的问题

脏读:事务 A 修改了数据库表中的数据,还没有提交,这个时候事务 B 读取了 事务 A 未提交的数据,读取了脏数据

丢失修改:事务 A 修改了数据库表中的数据,此时事务 B 进来了,对同一个数据进行了修改,此时事务 A 的修改丢失了,也就是丢失修改

不可重复读:事务 A 对于数据库表中的数据做了读取,此时事务 B 读取相同的数据并且修改了数据,事务 A 再次读取的时候,发现数据不一样了,也就是不可重复读

幻读:事务 A 读取数据库表中的数据,此时事务 B 访问同样的表,同时添加了几条数据,事务 A 再次读取的时候,发现无缘无故多了几条数据,产生了幻觉,也就是幻读的问题;

解决多并发访问数据库存在的问题的方法 - 使用不同的隔离级别

读未提交:允许事务 B 读取事务 A 没有提交的数据,隔离级别最低,最容易出现问题,容易出现脏读,幻读,不可重复读;

读已提交:允许事务 B 读取事务 A 已经提交的数据,避免了脏读,但是可能出现不可重复度以及幻读;

可重复读:较高的隔离级别,不会出现脏读以及不可重复读,但是可能出现幻读;

序列化读:最高的隔离级别,所有的事务 完全按照 ACID 的标准读取数据,不会出现上面提到的脏读,不可重读读以及幻读;

将上面的隔离级别以及多事务并发下可能产生的问题进行总结

经验分享 程序员 职场和发展