参考:高性能Mysql(第三版)
java3y:https://segmentfault.com/a/1190000015738121#articleHeader12
https://blog.csdn.net/hxpjava1/article/details/79407961

介绍锁之前先来简单介绍一下MySQL存储引擎

我们知道mysql常见的存储引擎为MyISAM和InnoDB这两种

这2个最大的区别就是InnoDB支持事务和行锁,MyISAM却不支持,而且它最大的缺陷就是崩溃后无法安全恢复

MyISAM存储引擎只支持表锁,
InnodB存储引擎既支持表锁也支持行锁。

因为表锁在Mysql中是由服务器层是实现的,
而行锁是由各个存储引擎自己实现的。
我们知道mysql的服务器层和存储引擎是相分离的,mysql提供了插件式的存储引擎。我们可以根据业务场景的不同选择更适合的存储引擎,每个存储引擎都可以实现自己的锁策略和锁粒度。

在mysql5.5以后默认的存储引擎就是InnoDB引擎
所以现在在大多数的情况下我们使用的就是InnoDB引擎
除非需要用到某些InnoDB不具备的特性,并且没有其他方法替代,否则都应该使用InnoDB存储引擎

详细请看: https://juejin.im/post/5b1685bef265da6e5c3c1c34

mysql的锁大致分成全局锁、表锁、行锁

表锁

表锁又分为:

  • 表读锁 (共享锁) 当 当前事务对当前表开启表读锁时,不会阻塞别的事务对该表的读操作,但会阻塞别的事务对该表的增删改;
  • 表写锁 (独占锁) 当 当前事务对当前表开启表写锁时,会阻塞别的事务对当前表的读和增删改;

也就是说:读读不阻塞,读写阻塞,写写阻塞!

表锁的开销很小,加锁也很快,不会发生死锁,但是因为他锁的是整张表,粗粒度很大,所以支持的并发度很低

另外如果有2个事务一个准备加读锁,一个准备加写锁,写锁请求可能会排到读锁请求前面
写锁比读锁有更高的优先级

MyISAM存储引擎对于我们平时写的UPDATE、DELETE、INSERT语句,会自动给相应的表直接加上表写锁,
而对于SELECT语句会自动给对应的表加上表读锁

我们也可以手动的添加表读锁和表写锁

lock table student read // 加表读锁

案例:
seeesion 1
mysql学习篇之锁
session 2
mysql学习篇之锁
可以看到当事务1开启表读锁后

session1

  • 对该表查询成功,修改报错
  • 对他表查询修改都报错

session2

  • 对该表查询成功,修改阻塞
  • 对他表查询修改肯定是成功的,虽然没测试

这里用了ctrl + c退出阻塞状态
只有当session 1用 unlock tables 命令释放锁后 事务2才会正常恢复阻塞状态然后修改成功

lock table student read; // 加表写锁

session1
mysql学习篇之锁
session 2
mysql学习篇之锁
可以看到session1 对student加表写锁后

session 1

  • 对该表查询修改 成功
  • 对他表查询修改会报错

session2

  • 对该表查询修改会阻塞
  • 对别表坑定增删查改都是成功的,虽然没测试

这里同样用了ctrl + c退出阻塞状态
同样的只有当session 1用 unlock tables 命令释放锁后 事务2才会正常恢复阻塞状态然后执行成功

行锁

行锁也分为2种

  • 共享锁(S 锁)
    当前事务对该数据行加共享锁会阻塞其他事务对该数据行加排他锁锁,不阻塞加共享锁锁
    也就是读锁,其他事务也可以读但不能写;

  • 排他锁(X 锁)
    当前事务对该数据行加排他锁,会阻塞其他事务对该数据行加共享锁和排他锁
    也就是写锁,阻塞其他事务的读和写;

注意!!!! InnoDB行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据,InnoDB才使用行级锁,否则,会升级为表锁

意向锁 https://blog.csdn.net/zcl_love_wx/article/details/82015281

需要注意下面4点

  • 在不通过索引条件查询的时候,InnoDB使用的是表锁,而不是行锁。

  • 由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。

  • 当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。

  • 即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。

因为行锁锁的是一行数据,所以他支持的并发度很高,但是因为需要更多的加锁解锁,他的锁开销就会很大
而且可能会导致死锁

在InnoDB中对于我们平时写的UPDATE、DELETE、INSERT语句,会自动给相应的行加上排他锁也就是写锁
而对于SELECT语句。InnoDB不会加任何锁。

之前我错误的认为InnoDB会对select语句加上共享锁,困扰了我很久

session 1
mysql学习篇之锁
session 2
mysql学习篇之锁
可以看到
事务1 select之后,事务2对该行的修改没有被阻塞,竟然成功了。
所以 InnoDB 对于SELECT语句,不会加任何锁

我们可以手动的添加行锁
显示加锁:

共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE

排他锁(X) :SELECT * FROM table_name WHERE ... FOR UPDATE

那mysql怎么解决当有多个事务产生的问题,怎么保证事务的隔离级别的呢?

关于事务产生的问题和隔离级别
请看 https://github.com/Snailclimb/JavaGuide/blob/master/docs/database/事务隔离级别(图文详解).md

比如脏读
在上面的例子中事务1 再次select 会读到事务2修改之后但没有提交的记录。
原因就是因为 select语句不会加共享锁,就算事务2的update语句加了排他锁,事务1还是可以进行select

在读已提交和可重复度隔离级别下,InnoDB采用了MVCC(多版本控制)解决了脏读、不可重复读,并且因为没有对读操作加锁实现了读写不阻塞,从而大大提升了并行度。

MVCC

MVCC实现的读写不阻塞正如其名:

  • 多版本并发控制—>通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一致性读取。从用户的角度来看,好像是数据库可以提供同一数据的多个版本。

在InnoDB中,给每行增加两个隐藏字段来实现MVCC,一个用来记录数据行的创建时间,另一个用来记录行的过期时间(删除时间)。在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务的版本号就会递增。

于是乎,默认的隔离级别(REPEATABLE READ)下,增删查改变成了这样:

  • SELECT 读取创建版本小于或等于当前事务版本号,并且删除版本为空或大于当前事务版本号的记录。这样可以保证在读取之前记录是存在的。
  • INSERT 将当前事务的版本号保存至行的创建版本号
  • UPDATE 新插入一行,并以当前事务的版本号作为新行的创建版本号,同时将原记录行的删除版本号设置为当前事务版本号
  • DELETE 将当前事务的版本号保存至行的删除版本号

快照读和当前读

快照读:读取的是快照版本,也就是历史版本

当前读:读取的是最新版本

普通的SELECT就是快照读,而UPDATE、DELETE、INSERT、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE是当前读。

InnoDB用多版本来提供查询数据库在某个时间点的快照。如果隔离级别是REPEATABLE READ,那么在同一个事务中的所有一致性读都读的是事务中第一个这样的读读到的快照;
如果是READ COMMITTED,是依据最新的版本号来读取的(当事务B执行commit了之后,会生成一个新的版本号),如果事务B还没有commit,那事务A读取的还是之前版本号的数据。

串行化的隔离级别下的select 语句是加了锁的,这也就导致了事务的串行化,事务2的update语句必须等待事务1提交事务释放锁之后才能被唤醒继续执行。

mysql学习篇之锁

相关文章:

  • 2023-03-30
  • 2021-08-19
  • 2021-09-09
  • 2021-08-14
  • 2021-09-08
猜你喜欢
  • 2021-07-17
  • 2021-10-03
  • 2021-08-31
  • 2022-01-23
  • 2021-05-24
  • 2021-05-26
  • 2022-01-16
相关资源
相似解决方案