侧边栏壁纸
  • 累计撰写 247 篇文章
  • 累计创建 16 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

悲观锁、乐观锁和数据库事务隔离性的实现

kaixindeken
2021-07-27 / 0 评论 / 0 点赞 / 139 阅读 / 2,362 字

悲观锁

悲观锁就是在加锁时很悲观,具体而言,就是对数据被外界(当前事务之外的其他事务,以及外部系统事务)修改持保守态度,总是假设最坏的情况,每次读取数据时都认为会被其他事务修改,所以都会加锁(悲观锁),从而将数据处于锁定状态。

悲观锁往往依靠数据库提供的锁机制,也就是我们在上篇教程介绍的锁,比如共享锁、排它锁。一旦加了悲观锁,不同线程(事务)同时执行时,只能有一个线程执行,其他的线程在入口处等待,直到锁被释放。

在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读:即读取数据时给对应记录加锁,其它事务无法修改这些数据;删除数据时也要加锁,其它事务无法读取这些数据。

注:与之相对的,MVCC 是一致性快照读,读数据的时候没有加锁。

比如事务隔离级别中的串行化(Serializable)就是典型的使用悲观锁的例子,在读操作时加共享锁,在写操作时加排它锁,读写互斥,虽然简单粗暴且安全,但是性能太差了。

乐观锁
乐观锁就是在加锁时很乐观,认为操作不会产生并发问题(不会有其他事务对数据进行修改),因此读取数据时不会上锁。但是在更新时会判断其他事务在这之前有没有对数据进行修改,一般会使用版本号机制或 CAS 算法实现。

悲观锁大多数情况下依靠数据库的锁机制实现,以保证事务操作最大程度的独占性。但导致的问题就是数据库性能的开销很大,特别是对长事务而言,这样的开销往往无法承受。而乐观锁机制在一定程度上解决了这个问题。

我们以版本号为例,演示下一个乐观锁的基本实现:

  • 读取记录时,获取当前记录的 version;
  • 执行更新时,对 version 进行更新:set version = newVersion where version = oldVersion,这里的 oldVersion 对应上一步取到的version;
  • 如果 version 不匹配,也就是通过 version = oldVersion 查询不到对应记录,表明这行记录被其他事务更新了,此次更新就会失败,需要回滚事务。

我们可以编写一个示例更新 SQL 语句如下:

update users set name = '学院君', version = version + 1 where id = #{id} and version = #{version};

注:InnoDB 引擎实现的 MVCC 也是一种乐观锁,但是这里需要注意限定哈,MVCC 作为一个通用概念,不同数据库和数据库引擎实现是不一样的,InnoDB 的 MVCC 是乐观锁,不代表其他引擎实现的 MVCC 也是乐观锁。

如何确保并发事务的隔离性

接下来,我们就来看看 MySQL 如何通过不同的锁在兼顾并发性能的情况下确保数据库事务的隔离性的。

单纯论隔离性和并发安全,显然串行化无出其右。

不同事务之间的 SQL 操作组合分为读-读、读-写、写-读和写-写这四种情况,我们将以 InnoDB 引擎为例分别进行介绍。

并发读取

由于读操作对数据表记录没有任何影响和副作用,所以多个只包含读操作的事务同时执行不存在并发问题,事实上,事务 ID 也是在事务中第一个更新操作时生成的。

并发读写

读-写和写-读本质上是一样的,只是先后顺序不同而已,如果并发执行的两个事务一个事务包含多某条记录的读操作,另一个事务包含对该记录的写操作,这个时候 MySQL 是通过 MVCC 解决并发读问题的,以可重复读为例,在事务第一条查询语句执行前会生成所有已提交事务的数据库快照,直到该事务提交之前,后面的读操作都会复用这个快照,其他执行中的未提交事务修改在该事务中是看不到的,我们将这种读取数据的方式叫做快照读。

但是如果是写操作,是不能基于快照进行的,否则就可能导致脏写,因此写操作必须基于最新版本数据,这种读取数据的方式叫做当前读,或者叫做锁定读,也就是说操作之前会给记录加锁。比如以下这些操作都是锁定读:

  • select * from table where ? lock in share mode;
  • select * from table where ? for update;
  • insert;
  • update ;
  • delete;

写操作执行前会给对应记录加上排它锁,直到当前事务提交才会释放,所以并发的读写操作中 MVCC 快照读和加锁后的锁定读并不冲突。

注:如果更深入去区分的话,更新和删除都会加上排它锁,而插入操作会加隐式锁,因为插入之前这条记录还不存在,隐式锁也会阻止其他事务加 S 锁和 X 锁。

并发写入

如果两个事务都包含对同一条记录的写入操作,那么和上面并发读写同一条记录的写操作一样,只需要将第一个获取到排它锁的操作加锁,直到该事务提交释放锁,另一个事务才可以开始执行。

注意到上述所有并发操作都是针对同一行记录的读写,因为 InnoDB 引擎支持行锁,如果不是同一行记录,或者不同表的记录,是不存在并发问题的,只有在类似 MyISAM 这种仅支持表级锁的引擎中才要考虑这些问题,显然,支持行级锁的 InnoDB 在写入操作上性能大大优于 MyISAM,因为加锁这种操作是很消耗性能的,也正因如此,MySQL 才引入 MVCC 实现快照读,通过不加锁的方式解决并发读写中的读问题。

为什么要引入锁来确保并发安全

为什么已经有了数据库事务默认隔离级别来解决事务的并发问题,还要在业务代码中引入锁来确保并发安全呢?

InnoDB 引擎事务隔离级别控制的并发安全维度是表记录级的(行级),即不能读写执行中但未提交事务的表记录更改结果(脏写、脏读、不可重复读、幻读均源于此),但是对于字段级别的并发处理,并没有锁去控制,以转账为例,并发执行的事务 A 和事务 B 读取到的余额初始值一样,然后去修改余额,事务 A 不会在事务 B 基础上去修改,而是在事务 A 开始时读取到的余额值基础上去修改,这是符合可重复读这一事务隔离级别的,但是事务并发执行完毕后,就会出现一个事务修改结果覆盖另一个的修改结果,导致并发安全问题,要解决这个问题,就要引入乐观锁或者悲观锁了。

0

评论区