MySQL

Mysql的几个灵魂拷问(三)

2020-10-15  本文已影响0人  千淘萬漉

今天这篇就来讲讲Mysql中比较高频的锁和事务吧。

一、Mysql锁事

1、锁的类型有哪些呢

总的来说,InnoDB共有七种类型的锁:

要记住这么多类型的锁不容易,但是总结一下来看,可以按照几个维度将锁分做一个分类,按照锁的粒度分类,分为表级锁和行级锁,

虽然使用行级索具有粒度小、并发度高等特点,但是表级锁有时候也是非常必要的:事务更新大表中的大部分数据直接使用表级锁效率更高;事务比较复杂,使用行级索很可能引起死锁导致回滚。

锁按照是否互斥划分为 共享锁(Share Locks,记为S锁)和排他锁(eXclusive Locks,记为X锁)。

2、InnoDB的行锁模式和加锁方法

InnoDB的行锁模式就是分为如下的两种:

为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks):意向共享锁(IS),意向排他锁(IX)。这两种意向锁都是表锁。 (意向锁了解即可)

InnoDB行锁模式兼容性列表:

意向锁是InnoDB自动加的;对于UPDATE、DELETE和INSERT语句,InnoDB会自动给设计数据集加排他锁(X);对于普通的SELECT语句,InnoDB不会加锁。 可以通过以下语句显示给记录集加共享锁或排他锁:

共享锁(S):SELECT * FROM TABLE_NAME WHERE ... LOCK IN SHARE MODE.
排他锁(X):SELECT * FROM TABLE_NAME WHERE ... FOR UPDATE.
以上使用完后记得要提交事务。

SELECT ... IN SHARE MODE获得共享锁,主要用在需要数据依存关系时来确认某行记录是否存在,并确保没有人对这个记录进行UPDATE或者DELETE操作。但是如果当前事务也需要对该记录进行更新操作,则很有可能造成死锁,对于锁定行记录后需要进行更新操作的应用,应该使用SELECT... FOR UPDATE方式获得排他锁。

所以在使用共享锁模式下,查询完数据后不要进行更新操作,不然又可能会造成死锁;要更新数据,应该使用排他锁模式。mysql锁机制详解

InnoDB的行锁原理与使用

InnoDB行锁是通过给索引上的索引项加锁来实现的,这意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!(这个问题遇到过,由于没加索引,行锁变表锁)

实际使用案例,此处就是用的行锁来实现一个事务更新操作。

//0.开始事务
begin;
//1.查询出商品信息
select status from t_goods where id=1 for update;
//2.根据商品信息生成订单
insert into t_orders (id,goods_id) values (null,1);
//3.修改商品status为2
update t_goods set status=2;
//4.提交事务
commit;

关于行锁的使用和注意可以看看这篇文章。你知道吗?使用 MySQL 中InnoDB行锁需要避免这些坑?

锁与事务

依赖锁的标准事务

标准SQL事务隔离级别的实现是依赖锁的,但是Mysql在只使用锁来实现隔离级别的控制的时候,需要频繁的加锁解锁,而且很容易发生读写的冲突。为了不加锁解决读写冲突的问题,MySQL引入了MVCC机制,详细可见:微信文章深入理解 MySQL 中事务隔离级别的实现原理

二、Mysql事务


事务的基本特性

事务的隔离级别

而隔离性有4个隔离级别,分别是:

事务隔离级别

更新丢失(Lost Update):当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题 —— 最后的更新覆盖了其他事务所做的更新。如何避免这个问题呢,最好在一个事务对数据进行更改但还未提交时,其他事务不能访问修改同一个数据。

脏读(Dirty Reads):一个事务正在对一条记录做修改,在这个事务并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些尚未提交的脏数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做 “脏读”。

不可重复读(Non-Repeatable Reads):一个事务在读取某些数据已经发生了改变、或某些记录已经被删除了!这种现象叫做“不可重复读”。

幻读(Phantom Reads):一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为 “幻读”。

隔离线和问题

ACID靠什么保证

快照读、当前读?

快照读,读取的是快照数据,不加锁的简单 Select 都属于快照读.

SELECT * FROM player WHERE ...

当前读就是读的是最新数据,而不是历史的数据,加锁的 SELECT,或者对数据进行增删改都会进行当前读。

SELECT FROM player FOR UPDATE;
INSERT INTO player values ...
DELETE FROM player WHERE ...
UPDATE player SET ...

什么是MVCC?

解决更新丢失可以交给应用,但是后三者问题的解决需要数据库提供事务间的隔离机制来解决。实现隔离机制的方法主要有两种:加读写锁、MVCC

MVCC 的英文全称是 Multiversion Concurrency Control ,中文意思是多版本并发控制技术。简单来说就是保存数据的历史版本。可以通过比较版本号决定数据是否显示出来。读取数据的时候不需要加锁可以保证事务的隔离效果(乐观锁)。每个行记录都有隐藏的三列:

行记录的列

InnoDB 将行记录快照保存在 Undo Log 里。

如果一个事务要查询行记录,需要读取哪个版本的行记录呢。Read View 保存了当前事务开启时所有活跃的事务列表,可以理解为: Read View 保存了不应该让这个事务看到的其他事务 ID 列表。

如果当前事务的 creator_trx_id 想要读取某个行记录,这个行记录ID 的trx_id

MVCC如何查询一条记录

获取事务自己的版本号,即 事务ID
获取 Read View
查询得到的数据,然后 Read View 中的事务版本号进行比较。
如果不符合 ReadView 规则, 那么就需要 UndoLog 中历史快照;
最后返回符合规则的数据

InnoDB 实现多版本控制 (MVCC)是通过 ReadView+ UndoLog 实现的,UndoLog 保存了历史快照,ReadView 规则帮助判断当前版本的数据是否可见。

微信文章:MVCC 原理

MVCC能解决幻读问题吗?

MVCC并不能解决幻读问题。间隙锁是可重复读级别下才会有的锁,结合MVCC和间隙锁可以解决幻读的问题。

begin;
#假设users表为空,下面查出来的数据为空
select * from users; #没有加锁
#此时另一个事务提交了,且插入了一条id=1的数据
select * from users; #读快照,查出来的数据为空
update users set name='mysql' where id=1;#update是当前读,所以更新成功,并生成一个更新的快照
select * from users; #读快照,查出来id为1的一条记录,因为MVCC可以查到当前事务生成的快照
commit;

可以看到前后查出来的数据行不一致,发生了幻读。所以说只有MVCC是不能解决幻读问题的,解决幻读问题靠的是间隙锁。啥叫间隙锁:当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁。例如,来看下下面这个案例:

begin;
#假设users表为空,下面查出来的数据为空
select * from users for update; #加上共享锁
#此时另一个事务B想提交且插入了一条id=1的数据,由于有间隙锁,所以要等待
select * from users; #读快照,查出来的数据为空
update users set name='mysql' where id=1;#update是当前读,由于不存在数据,不进行更新
select * from users; #读快照,查出来的数据为空
commit;
#事务B提交成功并插入数据

注意,Mysql默认是RR(repeatable read)级别下想解决幻读问题,需要我们显式加锁,不然查询的时候还是不会加锁的。

上一篇 下一篇

猜你喜欢

热点阅读