mysql学习篇之锁

参考:高性能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提供了插件式的存储引擎。我们可以根据业务场景的不同选择更适合的存储引擎,每个存储引擎都可以实现自己的锁策略和锁粒度。

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

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

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

表锁

表锁又分为:

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

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

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

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

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

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

lock table student read // 加表读锁

案例:
seeesion 1
在这里插入图片描述
session 2
在这里插入图片描述
可以看到当事务1开启表读锁后

session1

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

session2

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

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

lock table student read; // 加表写锁

session1
在这里插入图片描述
session 2
在这里插入图片描述
可以看到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
在这里插入图片描述
session 2
在这里插入图片描述
可以看到
事务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(多版本控制)解决了脏读、不可重复读,并且因为没有对读操作加锁实现了读写不阻塞,从而大大提升了并行度。

undo日志

为了保证事务的原子性,一个事务中的所有操作要么全部成功,要么全部失败,当一个事务发生了一半的时候某个操作发生了错误,或者执行到一半系统发生了错误,突然宕机断电,我们必须回滚这个事务之前发生所有的操作,这样就可以造成⼀个假象:这个事务看起来什么都没做,所以符合原⼦性要求。
怎么实现回滚呢?
每当我们要对⼀条记录做改动时 把回滚时所需的东⻄都给记下来就好了。
⽐⽅说:

  • 插⼊⼀条记录时,⾄少要把这条记录的主键值记下来,回滚时只需要把这个主键值对应的记录删掉。
  • 删除了⼀条记录,⾄少要把这条记录中的内容都记下来,回滚时再把由这些内容组成的记录插⼊到表中。
  • 修改了⼀条记录,⾄少要把修改这条记录前的旧值都记录下 来,回滚时再把这条记录更新为旧值。

这些需要记录下来的信息称为undo日志(撤销日志)

事务ID

在某个事务执⾏过程中第⼀次对某个表执⾏增、删、改操作时会为这个事务分配⼀个事 务id,否则的话也是不分配事务id的。 有的时候虽然我们开启了⼀个事务,但是在这个事务中全是查询语句,并没有执⾏增、删、改的语句,那也就意味着这个事务并不会被分配⼀个事务id。

事务ID是⼀个全局递增的数字。先被分配id的事务得到的是较⼩的事务id,后被分配id的事务得到的 是较⼤的事务id。
这个事务ID在每行记录的trx_id隐藏列中存着。

MVCC

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

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

在InnoDB中它的聚簇索引 记录中都包含两个必要的隐藏列

  • trx_id:每次⼀个事务对某条聚簇索引记录进⾏改动时,都 会把该事务的事务id赋值给trx_id隐藏列。

  • roll_pointer:每次对某条聚簇索引记录进⾏改动时,都会 把旧的版本写⼊到undo⽇志中,然后这个隐藏列就相当于⼀个指针,可以通过它来找到该记录修改前的信息。

每次对记录进⾏改动,都会记录⼀条undo⽇志,每条undo⽇志也都 有⼀个roll_pointer属性(INSERT操作对应的undo⽇志没有该 属性,因为该记录并没有更早的版本),每条日志就是该记录的一个版本,随着改动次数的增多,所有的版本都会被 roll_pointer属性连接成⼀个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。另外,每个版本中还包 含⽣成该版本时对应的事务id。

对于读已提交和可重复度隔离级别来说,必须保证读到已经提交了的事务修改过的记录,也就是说假如 另⼀个事务已经修改了记录但是尚未提交,是不能直接读取最新版本 的记录的,核⼼问题就是:需要判断⼀下版本链中的哪个版本是当前事务可⻅的。

ReadView

InnoDB在读取数据的时候会生成一个ReadView
ReadView包含4个内容

  • m_ids:表示在⽣成ReadView时当前系统中还未提交的读写事务ID列表。
  • min_trx_id:表示在⽣成ReadView时当前系统中未提交读 写事务中最⼩的事务id,也就是m_ids中的最小值
  • max_trx_id:表示⽣成ReadView时系统中应该分配给下⼀ 个事务的id值,并不是m_ids中的最⼤值!
  • creator_trx_id:表示⽣成该ReadView的事务的事务id。

注意:max_trx_id,事务id是递增分 配的。⽐⽅说现在有id为1,2,3这三个事务,之后id为3的 事务提交了。那么⼀个新的读事务在⽣成ReadView时, m_ids就包括1和2,min_trx_id的值就是1,max_trx_id 的值就是4。

有了这个ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可⻅

  • 如果被访问版本的trx_id属性值与ReadView中的 creator_trx_id值相同,意味着当前事务在访问它⾃⼰修改
    过的记录,所以该版本可以被当前事务访问。
  • 如果被访问版本的trx_id属性值⼩于ReadView中的 min_trx_id值,表明⽣成该版本的事务在当前事务⽣
    成ReadView前已经提交,所以该版本可以被当前事务访问。
  • 如果被访问版本的trx_id属性值⼤于ReadView中的 max_trx_id值,表明⽣成该版本的事务在当前事务⽣
    成ReadView后才开启,所以该版本不可以被当前事务访问。
  • 如果被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间,那就需要判断⼀下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时⽣成该版 本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时⽣成该版本的事务已经被提交,该版本可以被访问。

如果某个版本的数据对当前事务不可⻅的话,那就顺着版本链找到下 ⼀个版本的数据,继续按照上边的步骤判断可⻅性,依此类推,直到 版本链中的最后⼀个版本。如果最后⼀个版本也不可⻅的话,那么就 意味着该条记录对该事务完全不可⻅,查询结果就不包含该记录

读已提交和可重复读最大的区别就是⽣成ReadView的时机不同
读已提交在每⼀次进⾏普通SELECT操作前都会⽣成⼀个ReadView,可重复读只在第⼀次进⾏普通SELECT操作前⽣成⼀个ReadView,之后的查询 操作都重复使⽤这个ReadView。

快照读和当前读

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

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

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

间隙锁

幻读只发生在当前读操作中,而快照读读取的是事务开始时的版本 所以不会看到多出来的数据,也就不会产生幻读。
针对幻读InnoDB用了间隙锁来解决,它会给每行的记录中的间隙也加上锁,保证别的事务不会在间隙中插入数据

那什么时候才会加间隙锁呢?
注意!只有可重复读隔离级别下才会用到间隙锁。所以可重复读读 也解决了幻读。

列举几个情况:
//表结构和数据

CREATE TABLE `t` (  `id` int(11) NOT NULL,  `c` int(11) DEFAULT NULL,  `d` int(11) DEFAULT NULL,  PRIMARY KEY (`id`),  KEY `c` (`c`) ) ENGINE=InnoDB;
 
insert into t values(0,0,0),(5,5,5), (10,10,10),(15,15,15),(20,20,20),(25,25,25);

顾名思义,间隙锁,锁的就是两个值之间的空隙。这个表表 t,初始化插入了 6 个记录,这就产生了 7 个间隙。
在这里插入图片描述

  • 当对一个非索引列进行等值查询时,上面说到行锁会失效升级为表锁,也可认为InnoDB为了找到记录需要进行全表扫描,会给每一行都加上行锁。但是这样还不能解决幻读的问题,InnoDB还会给每行的间隙加上锁,保证别的事务不会插入数据。这样,当你执行 select * from t where d=5 for update 的时候,就不止是给数据库中已 有的 6 个记录加上了行锁,还同时加了 7 个间隙锁。这样就确保了无法再插入新的记录。
    在这里插入图片描述
    在这里插入图片描述
    可以看到3个插入操作全部阻塞 我用了ctrl + c 退出

    再说一句。。普通的select查询是快照读 不会加任何锁,更不会加间隙锁,更更不会产生幻读问题

  • 当对一个唯一索引列进行等值查询时 因为该索引列上的数据是唯一的 所以不会加间隙锁,只会给改行加上锁。特殊情况 该查询的记录不存在,会给当前范围加入间隙锁。

恕我无能,还有很多加锁规则我现在消化不了无福消受具体请看,极客时间Mysql实战45讲20,21讲

虽然间隙锁保证了幻读,但也因为锁住了更大的范围,从而影响了并发度

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

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值