MySQL-锁机制

MySQL-锁机制

在这里插入图片描述
在这里插入图片描述

当程序中可能出现并发的情况时,就需要保证在并发情况下数据的准确性,以此确保当前用户和其他用户一起操作时,所得到的结果和他单独操作时的结果是一样的。这就叫做并发控制。并发控制的目的是保证一个用户的工作不会对另一个用户的工作产生不合理的影响。锁机制是在多用户并发环境下,用于保证数据库完整性和一致性的重要技术手段。数据库管理系统使用锁对象是事务,通常在事务中获得锁,在事务提交或者回滚时释放锁。没有做好并发控制,就可能导致脏读、幻读和不可重复读等问题。

1. 悲观锁和乐观锁

乐观锁比较适用于读多写少的情况(多读场景),悲观锁比较适用于写多读少的情况(多写场景)。

1.1 悲观锁

理解概念

悲观锁具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
之所以叫做悲观锁,是因为这是一种对数据的修改持有悲观态度的并发控制方式。总是假设最坏的情况,每次读取数据的时候都默认其他线程会更改数据,因此需要进行加锁操作,当其他线程想要访问数据时,都需要阻塞挂起。悲观锁的实现:

  • 传统的关系型数据库使用这种锁机制,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。
  • Java 里面的同步 synchronized 关键字的实现。
悲观锁主要分为共享锁和排他锁:
  • 共享锁【shared locks】又称为读锁,简称 S锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
  • 排他锁【exclusive locks】又称为写锁,简称 X 锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁。获取排他锁的事务可以对数据行读取和修改。
说明

悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。

1.2 乐观锁

理解概念

乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量。

乐观锁采取了更加宽松的加锁机制。也是为了避免数据库幻读、业务处理时间过长等原因引起数据处理错误的一种机制,但乐观锁不会刻意使用数据库本身的锁机制,而是依据数据本身来保证数据的正确性。乐观锁的实现:

  • CAS 实现:Java 中java.util.concurrent.atomic包下面的原子变量使用了乐观锁的一种 CAS 实现方式。
  • 版本号控制:一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会 +1。当线程 A 要更新数据时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
说明

乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。

1.3 具体实现

1.3.1 悲观锁的实现方式

悲观锁的实现,往往依靠数据库提供的锁机制。在数据库中,悲观锁的流程如下:

  • 在对记录进行修改前,先尝试为该记录加上排他锁(exclusive locks)。
  • 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。
  • 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
  • 期间如果有其他对该记录做修改或加排他锁的操作,都会等待解锁或直接抛出异常。

以MySQL中的InnoDBy引擎为例:
要使用悲观锁,必须关闭 MySQL 数据库的自动提交属性set autocommit=0。因为 MySQL 默认使用 autocommit 模式,也就是说,当执行一个更新操作后,MySQL 会立刻将结果进行提交。
扣减库存的案例如下:

//0.开始事务
begin;
//1.查询商品库存信息
select quantity from items where id = 1 for update;
//2.修改商品库存为2
update items set quantity = 2 where id = 1;
//3.提交事务
commit;

在对 id = 1 的记录修改前,先通过 for update 的方式进行加锁,然后再进行修改。这就是比较典型的悲观锁策略。

如果发生并发,同一时间只有一个线程可以开启事务并获得 id=1 的锁,其它的事务必须等本次事务提交之后才能执行。这样可以保证当前的数据不会被其它事务修改。
【注意】使用select … for update 锁数据,需要注意锁的级别,MySQL InnoDB 默认为行级锁。行级锁都是基于索引的,如果一条SQL 语句用不到索引的话是不会使用行级锁的,此时会使用表级锁将整张表锁住,这点需要注意。

1.3.2 乐观锁实现反方式

乐观锁不需要借助数据库的锁机制
注意就是两个步骤:冲突检测和数据更新。
典型的就是CAS(Compare and Swap)在大多数处理器指令中,CAS操作是一条(而不是多条)cpu指令,正是这个原因,所以CAS相关的指令是具备原子性的,这个组合操作在执行期间不会被打断,这样就保证了并发安全。
CAS的英文全称是Compare-And-Swap,意思就是比较并交换,他是原子类的底层原理,同时也是乐观锁的原理,CAS的特点是避免使用互斥锁,当多个线程同时更新同一个变量时,只有一个线程可以更新成功,其他的线程都会更新失败,和同步互斥锁不同的是,更新失败的线程并不会被阻塞,而是被告知此次竞争失败,下次还可以继续竞争。
Java 中,sun.misc.Unsafe 类提供了硬件级别的原子操作来实现这个 CAS。java.util.concurrent包下大量的类都使用了这个 Unsafe.java 类的 CAS 操作。

比如前面的扣减库存问题,通过乐观锁可以实现如下:

//查询出商品库存信息,quantity = 3
select quantity from items where id = 1
//修改商品库存为2
update items set quantity = 2 where id = 1 and quantity = 3;

在更新之前,先查询一下库存表中当前库存数(quantity),然后在做 update 的时候,以库存数作为一个修改条件。当提交更新的时候,判断数据库表对应记录的当前库存数与第一次取出来的库存数进行比对,如果数据库表当前库存数与第一次取出来的库存数相等,则予以更新,否则认为是过期数据。
这种情况下依旧存在ABA问题,此问题可以通过版本号的方法来解决。

1.4 如何选择乐观锁和悲观锁

在乐观锁与悲观锁的选择上面,主要看下两者的区别以及适用场景就可以了。

  1. 响应效率:如果需要非常高的响应速度,建议采用乐观锁方案,成功就执行,不成功就失败,不需要等待其他并发去释放锁。乐观锁并未真正加锁,效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。
  2. 冲突频率:如果冲突频率非常高,建议采用悲观锁,保证成功率。冲突频率大,选择乐观锁会需要多次重试才能成功,代价比较大。
  3. 重试代价:如果重试代价大,建议采用悲观锁。悲观锁依赖数据库锁,效率低。更新失败的概率比较低。
  4. 乐观锁如果有人在你之前更新了,你的更新应当是被拒绝的,可以让用户从新操作。悲观锁则会等待前一个更新完成。这也是区别。
    随着互联网三高架构(高并发、高性能、高可用)的提出,悲观锁已经越来越少的被应用到生产环境中了,尤其是并发量比较大的业务场景。

2. 共享锁与排它锁(按排他性区分)

2.1 共享锁(S锁,读锁)

用于不更改或不更新数据的操作,如SELECT。其它事务可以读,但不能写,阻止其他事务获得相同数据集的排他锁。例如,如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。

2.2 排它锁(X锁,写锁)

用于数据修改操作,例如 INSERT、UPDATE 或 DELETE。如果事务T对数据A加上排他锁后,则其他事务不能再对A加任何类型的锁。获准排他锁的事务既能读数据,又能修改数据。

/加S锁加X锁
S锁YNYN
X锁YNNN

两者之间的区别:

  • 共享锁(S锁):如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获取共享锁的事务只能读数据,不能修改数据。
  • 排他锁(X锁):如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获取排他锁的事务既能读数据,又能修改数据。

S 锁之间不冲突,X 锁则为独占锁,所以 X 之间会冲突, X 和 S 也会冲突。

3. InnoDB的表锁

innoDB支持多粒度锁,他允许行级锁和表锁共存

  • 1、意向锁的存在是为了协调行锁和表锁的关系,支持多粒度(表锁与行锁)的锁并存。
  • 2、意向锁是一种不与行级锁冲突表级锁,这一点非常重要。
  • 3、表明“某个事务正在某些行持有了锁或该事务准备去持有锁”
    意向锁分为两种:
  • 意向共享锁(intention shared lock, IS) :事务有意向对表中的某些行加共享锁(S锁)
  • 意向排他锁(intention exclusive lock, IX) :事务有意向对表中的某些行加排他锁(X锁)
    即:意向锁是由储存引擎自己维护的,用户无法手动操作意向锁,在为数据加共享/排它锁之前,InnDB会先获取数据行所在数据表的对应意向锁。

假如说我对这个表的数据上表锁,我首先要确定我有没有对其中一行上行级锁,这时候如果没有行级锁你就需要一条一条的判断了,而当你有行级锁时,会自动加意向锁,这时候加表锁的时候,就可以直接判断了。

从上面的案例可以得到如下结论:

  1. InnoDB 支持多粒度锁,特定场景下,行级锁可以与表级锁共存。
  2. 意向锁之间互不排斥,但除了IS与S兼容外,意向锁会与共享锁/排他锁互斥。 IX,
  3. IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,s发生冲突。
  4. 意向锁在保证并发性的前提下,实现了行锁和表锁共存且满足事务隔离性的要求。

4. 行锁【Record Lock记录锁、Gap Lock间隙锁、Next-key Lock临键锁】

4.1 行锁:(行锁分为S锁和X锁)(按粒度分为行锁、表锁)也称为记录锁,顾名思义,就是锁住某一行(某条记录row)。需要的注意的事,MySQL服务器层并没有实行行锁机制,行级锁只在储存引擎实现。

优点:锁的力度小,发生锁冲突概率低,可以实现高并发。

缺点:对于锁的开销比较大,加锁会比较慢,容易出现死锁情况。

InnoDB与MySAM的最大不同有两点:一是支持事务(TRANSACTION);二是采用了行级锁。

4.2 记录锁:记录锁也就是仅仅把一条记录锁上,官方的类型名称为: LOCK_ REC NOT_ GAP。比如我们把id值为8的那条记录加一个记录锁的示意图如图所示。仅仅是锁住了id值为8的记录,对周围的数据没有影响。
  • 当一个事务获取了一条记录的S型记录锁后,其他事务也可以继续获取该记录的s型记录锁,但不可以继续获取x型记录锁;
  • 当一个事务获取了一条记录的x型记录锁后,其他事务既不可以继续获取该记录的s型记录锁,也不可以继续获取x型记录锁。
4.3 间隙锁:锁定一个范围,但不包含记录本身,主要是为了在可重复读事务隔离级别中,解决幻读问题

间隙锁锁定的是索引BTree+叶子节点的next指针
幻读是指,当一个事务先后两次查询同一个范围的时候,查到的结果不同,这是因为第二次这个事务查到了其他事务对数据所做的更改。

4.4 Next-Key锁:记录锁+间隙锁,锁定一个范围,并且锁定记录本身。

Next-key锁是记录锁和间隙锁的组合,它指的是加在某条记录以及这条记录前面间隙上的锁。

【总结】
  • InnoDB 中的行锁的实现依赖于索引,一旦某个加锁操作没有使用到索引,那么该锁就会退化为表锁。
  • 记录锁存在于包括主键索引在内的唯一索引中,锁定单条索引记录。
  • 间隙锁存在于非唯一索引中,锁定开区间范围内的一段间隔,它是基于临键锁实现的。
  • 临键锁存在于非唯一索引中,该类型的每条记录的索引上都存在这种锁,它是一种特殊的间隙锁,锁定一段左开右闭的索引区间。
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

智博的自留地

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值