悲观锁和乐观锁

  • 悲观锁

正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。

  • 乐观锁

相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。
而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
要说明的是,MVCC的实现没有固定的规范,每个数据库都会有不同的实现方式,这里讨论的是InnoDB的MVCC

MVCC在innodb

在innodb中, 会在每行数据后添加两个额外的隐藏的值来实现MVCC, 这两个值一个记录这行数据何时被创建,另一个记录这行数据何时过期(或者被删除).在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务版本号就会递增. 在可重复读Repeatable reads事务隔离级别下:

  • select时, 读取 创建版本号 <= 当前事务版本号, 删除版本号为空 或者>当前事务版本号

数据行的版本号要小于或等于当前是事务的系统版本号,这样也就确保了读取到的数据是当前事务开始前已经存在的数据,或者是自身事务改变过的数据
确保查询出来的数据行记录在事务开启之前没有被删除

  • insert时, 保存当前事务版本号为行的创建版本号
  • delete时,保存当前事务版本号为行的删除版本号
  • update时, 插入一条新记录, 保存当前事务版本号为行创建版本号, 同时保存当前事务版本号到原来删除的行

通过MVCC,虽然每行记录都需要额外的存储空间,更多的行检查工作以及一些额外的维护工作,但可以减少锁的使用,大多数读操作都不用加锁,读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行,也只锁住必要行

MVCC逻辑流程-插入

在MySQL中建表时,每个表都会有三列隐藏记录,其中和MVCC有关系的有两列

  • 数据行的版本号 (DB_TRX_ID)
  • 删除版本号 (DB_ROLL_PT) | id | test_id | DB_TRX_ID | DB_ROLL_PT | | :—-: | :—-: | :—-: | :—-: | | | | | |

在插入数据的时候,假设系统的全局事务ID从1开始,以下SQL语句执行分析参考注释信息:

  1. begin;-- 获取到全局事务ID
  2. insert into `test_zq` (`id`, `test_id`) values('5','68');
  3. insert into `test_zq` (`id`, `test_id`) values('6','78');
  4. commit;-- 提交事务

当执行完以上SQL语句之后,表格中的内容会变成:

id test_id DB_TRX_ID DB_ROLL_PT
5 68 1 NULL
6 78 1 NULL

可以看到,插入的过程中会把全局事务ID记录到列 DB_TRX_ID 中去

MVCC逻辑流程-删除

对上述表格做删除逻辑,执行以下SQL语句(假设获取到的事务逻辑ID为 3)

  1. begin;--获得全局事务ID = 3
  2. delete test_zq where id = 6;
  3. commit;

执行完上述SQL之后数据并没有被真正删除,而是对删除版本号做改变,如下所示:

id test_id DB_TRX_ID DB_ROLL_PT
5 68 1 NULL
6 78 1 3

MVCC逻辑流程-修改

修改逻辑和删除逻辑有点相似,修改数据的时候 会先复制一条当前记录行数据,同事标记这条数据的数据行版本号为当前是事务版本号,最后把原来的数据行的删除版本号标记为当前是事务。
执行以下SQL语句:

  1. begin;-- 获取全局系统事务ID 假设为 10
  2. update test_zq set test_id = 22 where id = 5;
  3. commit;

执行后表格实际数据应该是:

id test_id DB_TRX_ID DB_ROLL_PT
5 68 1 10
6 78 1 3
5 22 10 NULL

MVCC逻辑-删除

select时, 读取 创建版本号 <= 当前事务版本号, 删除版本号为空 或者>当前事务版本号

数据行的版本号要小于或等于当前是事务的系统版本号,这样也就确保了读取到的数据是当前事务开始前已经存在的数据,或者是自身事务改变过的数据
确保查询出来的数据行记录在事务开启之前没有被删除


为了隔离级别, 那是当做不知道的

select要查事务id, 那肯定发现了

image.png