mysql 中事务有四大隔离级别,串行化最安全,但是开发中几乎是不用此隔离级别的,因为性能太差,一般用到读提交的隔离级别。与mysql 事务紧密联系的有 mysql 的锁机制

写在前面

mysql 采用 innoDB 作为数据库存储引擎,虽然 mysql 支持的事物级别是重复读,会有幻读的问题(读到没问题的值,但是更改有问题的幻像),但是实际上 innoDB 解决了此问题,怎么解决的呢?通过锁机制,mysql 的增删改是默认加上排他锁的,一个事务在操作 id=1 的行时候,另一个事务同时操作 id=1 的行会被锁住

  • 表级锁和行级锁是针对于索引的相关知识而言的,行锁锁住的其实是索引

  • 悲观锁和乐观锁都是针对于 select 而言的,悲观锁说白了使用的就是排他锁,乐观锁本质并不是锁,而是一种锁的理念,通过表结构设计来实现

  • 共享锁和排他锁

  • 其他

锁类别

行级锁

行级锁,锁的是索引的指定区间,行级锁有可能发生死锁,表级锁不会,看下面

其中 id 为主键,这是第一个事务:

--T1时刻
BEGIN;
 
--行级锁 id=1 的记录
select * from test1 where id=1 for update ;
 
--T3时刻
--更新 id=2 的记录
update test1 set id=id where id=2;

这是第二个事务:

    --T2时刻
    BEGIN;
     
    --行级锁 id=2 的记录
    select * from test1 where id=2 for update ;
     
    --T4时刻
    --更新 id=1 的记录
    update test1 set id=id where id=1;

这就有可能死锁了

表级锁

表级锁,锁的是表中每一行索引指定的区间。表级锁往往是没有指定索引,然后会用隐藏列字段 rowid 作聚集索引,锁住每一个 rowid 索引指向的空间。

有人会问既然有隐藏的列 rowid,虽然查询条件是没有索引的,那为什么不锁住该行的 rowid 索引,而去锁住全表呢?这是因为,由于条件中的字段没加索引,所以它不是一个 B+ 树(不满足二分查找的高效性),只能依据表数据逐行匹配(低效率),也就是说依据通过此字段查找其实操作的是整个表的资源,因此需要把表锁住

抽象概念的锁

悲观锁

数据锁住不允许操作

两个事务同时 begin; 先执行事务1的查询,for update 锁住行或者表(看索引)

由于加了悲观锁,如果查询条件没有设置索引,将会锁住表,所以我们建议对于查询较少写入居多,对吞吐量要求不高的场景使用悲观锁(查询多的情况容易导致悲观锁锁住表)

-- 并发事务1
SELECT1 FROMWHERE2='值2' FOR UPDATE;

再执行并发事务2,会发现被锁住,非要等事务1 commit 之后,事务2才会有结果

-- 并发事务2
SELECT1 FROMWHERE2='值2' FOR UPDATE;

乐观锁

数据锁住操作,操作不会成功

乐观锁通过加一个版本号的条件筛选来处理并发问题,比如两个事务同时开启,第一个事务执行操作条件是版本号为 0,执行后版本号自动加一,第二个事务还在执行,执行的时候条件为版本号为 0,可是执行后发现执行不成功,因为版本号已经被第一个事务改为了 1

查询较多的场景推荐使用乐观锁

两个事务并发执行,同时 begin 开启,事务1开始执行并 commit

-- 并发事务1
UPDATESET1='值1', version=version+1 where version=0

这个时候并发事务2开始执行,事务2没有更新成功,因为条件 version 已经被改变了

-- 并发事务2
UPDATESET1='值2', version=version+1 where version=0

乐观锁中第二个事务会发现更新失败了,我们怎么办呢?可以通过尝试重试来保证更新成功,阿里巴巴开发手册明确表示重试次数不少于 3 次

实际中的锁

共享锁

锁住后只允许读,不能写操作。行级别锁

共享锁又称 S 锁,读锁,多个事务对同一数据可以共享一把锁,多个事务可以对数据加共享锁但是不能加排他锁。其他事务都能访问数据,但是只能读不能修改。其他事务要想修改只有等当前阻塞队列中前面所有的共享锁得到释放。共享就是锁住数据,自己来修改,修改过程中别的事务只能读不能改。

在一个事务内,共享锁和排他锁不互斥

共享锁比较适用于两张表存在业务关系的一致性要求

SELECT * FROMWHERE='值' LOCK IN SHARE MODE;

共享锁的释放通过 commit 后者回滚

注意,在有外键的表中,我们进行数据的改操作,它会加上共享锁给父表,这样在高并发的大型系统里头,是很影响性能的,因此这也是很多系统不设主键外键的原因

意向共享锁

表级别锁

由数据引擎自己维护,用户不用关注,目的是一个事务要想锁住一张表首先要确认没有其他事务锁定住其中任何一行(锁表是锁住每一行),给行添加了共享锁,首先会给表添加一个意向共享锁,表示这张表我在用。后序其他事务就不需要逐行扫描表中的行了,加快了效率

排他锁

锁住后完全排他,但可以用普通 select 查询,select 默认不加任何锁机制。行级别锁(不加索引会锁表,对于行锁锁住的就是索引)

排他锁又称 X 锁,写锁,排他锁不能和其他锁并存,一个事务获取了排他锁,此时其他的事务就不能再获取该行的锁,直到资源释放。delete,insert,update 默认是有排他锁的。update 加了排他锁,共享锁也是不执行的

在一个事务内,共享锁和排他锁不互斥

排他锁适用于操作同一张表时的一致性要求

手动方式如下

SELECT * FROMWHERE='值' FOR UPDATE;

意向排他锁

表级别锁

请看上面的意向共享锁,作用类似

排他锁中的各种锁算法

mysql中各种类型的锁_innodb

记录锁

仅仅锁住一个记录

-- 仅仅锁住 id=4 的记录
SELECT * FROMWHERE id=4 FOR UPDATE;

-- 另一个事务执行被锁住
SELECT * FROMWHERE id=4 FOR UPDATE;

间隙锁

若 id=3 则在 1-4 区间内,这时候是间隙锁,下一次执行 id=2 会被锁住,因为也在这个间隙中。间隙锁锁插入,不会锁查询,因为间隙没有数据,锁住也没有意义,只锁插入

-- 锁住间隙 1-4 之间,此时 id=3
SELECT * FROMWHERE id=3 FOR UPDATE;

-- 另一个事务执行被锁住,此时 id=2
INSERT INTO(id) VALUES('2'); 

邻键锁

mysql innoDB 数据库存储引擎默认的行锁的算法,没有满足记录锁和间隙锁,默认都是邻键锁,锁住的是区间+记录,如下

-- 锁住区间+事务,锁住的是 (4,7] 和 (7,10]
SELECT * FROMWHERE id>5 AND id<9 FOR UPDATE;

-- 另一个事务执行被锁住,此时 id=10
SELECT * FROMWHERE id>5 AND id<9 FOR UPDATE;
辅助索引锁住主键索引

为什么辅助索引加锁后会锁住主键索引?
mysql中各种类型的锁_排他锁_02

因为 mysql 使用 b+ 树,叶子结点才存放卫星数据,对于主键索引的 b+ 树,叶子存放索引和数据,辅助索引的 b+ 树叶子则存放二级索引和主键,所以还是得经过一个主键索引的 b+ 树,所以锁住辅助索引的时候,主键索引也会被锁住了