MySQL锁与MVCC

MySQL 锁与MVCC

前提概念

数据库并发三种场景

  • 读-读:不存在任何问题,无需并发控制
  • 读-写:有隔离性问题,可能出现脏读、不可重复度和幻读
  • 写-写:可能存在更新丢失问题,比如第一类更新丢失,第二类更新丢失

悲观锁和乐观锁

  • 乐观锁和悲观锁是一种锁设计思想

悲观锁(PCC)

  • 默认数据被外界访问时,必然产生冲突,以加锁方式,保证同一时间只有一个线程访问到数据,实现排他性

解决读-写冲突写-写冲突

悲观锁的实现

利用数据库本身提供的锁实现
  • 外界访问数据,首先向数据库申请该数据的锁
  • 若成功,操作该数据,其他客户端无法操作该数据
  • 若失败,代表同一时间其他客户端获得该锁,必须等待期释放锁

优点:适合于写多读少,提高数据安全性

缺点:加锁增加系统开销,数据吞吐量低,不适合读多写少下使用

乐观锁(OCC)

  • 数据提交更新时,对数据冲突与否进行检测,若冲突则返回冲突信息,让用户决定去做下一步,比如重试,直至成功

解决写-写场景,无锁方式解决并发问题

CAS思想

  • CAS指令需要3个操作数,分别是内存位置V,旧的预期值A和新值B,CAS指令执行时,当读取的内置位置V的现值等于旧预期值A时,处理器会将新值B去更新内置位置V的值。否则不执行更新。

乐观锁的实现(基于CAS思想设计)

  • 数据版本(version)实现
  • 表中添加version字段,每行数据的版本标识,每次对数据操作提交version+1
  • 当对数据更新提交时,将取得version与数据库中的version比对,若大于则表示没有其他线程修改过此数据。否则其他线程修改过,返回冲突信息,让用户决定下一步动作
update table set num = num + 1, version = version + 1 where version = #{version} and id = #{id}
  • 时间戳实现
  • 表中添加update_time字段使用时间戳
  • 当对数据更新提交时,将取得时间与数据库中的update_time比对,若一致直则代表没有冲突,可以提交更新,同时时间戳更新为当前时间。否则其他线程修改过,返回冲突信息,让用户决定下一步动作
update table set num = num +1, update_time = unix_timestamp(now()) where id = #{id} and update_time = #{updateTime}

保证CAS多个操作的原子性,即获得数据库数据的版本,数据库版本与取得版本比较以及更新数据等多个sql操作应处在同一事务中

  • 优点
    • 读多写少并发场景下,避免数据库加锁开销,提高Dao层响应性能
  • 缺点
    • 写多读少下,易导致CAS多次重试,冲突频率过高,导致开销比悲观锁更高

MVCC 多版本并发控制机制

当前读和快照读

  • 当前读

类似select lock in share mode(共享锁)、select for update、update、insert、delete(排它锁)属于当前读,读取的记录是最新版本,保证其他并发事务不能修改当前记录,对读取的记录进行加锁

  • 快照读

类似不加锁的select属于快照读,即不加锁的非阻塞读。读取的不一定是最新版本数据,可能是历史版本数据

解决问题

  • 实现读-写冲突不加锁,读指快照读非当前读,当前读实际上是一种悲观锁的实现

MVCC实现原理

  • 实现原理依赖记录中3个隐式字段,undo日志,Read View

隐式字段

每行记录除了自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段

  • DB_TRX_ID
    • 6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID
  • DB_ROLL_PTR
    • 7byte,回滚指针,指向这条记录的上一版本(存储于rollback segment里)
  • DB_ROW_ID
    • 6byte,隐含的自增ID(隐藏主键),若数据表没有主键,InnoDB自动以DB_ROW_ID产生一个聚簇索引
  • 删除flag隐藏字段,即记录被更新或删除并不代表真的删除,而是flag删除变了

undo日志

  • insert undo log
    • 代表事务在insert新纪录时产生undo log,只在事务回滚时需要,在事务提交后可被立即丢弃
  • update undo log
    • 事务在update或delete时产生undo log,回滚、快照读均需要,只有在快照读或事务回滚不涉及该日志时,对应日志才被purge线程统一清除

purge

  • 若某个记录deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录可被安全删除

undo log实际上存在rollback segment中旧记录链,执行流程如下:

  • 向数据库获取排它锁
  • 将当前行数据copy到undo log作为旧记录,即在undo log中有当前行的copy副本
  • 修改数据修改隐藏字段的事务ID,默认从1开始递增,回滚指针指向copy到的undo log副本记录
  • 事务提交释放锁

Read View(读视图)

  • 事务快照读时产生的读视图,用于记录并维护系统当前活跃事务的ID
  • 将要被修改的数据的最新记录中的DB_TRX_ID(即当前事务ID)取出与当前其他事务ID对比,若DB_TRX_ID与Read View的属性做某些比较,不符合可见性,则通过DB_ROLL_PTR回滚指针根据Undo Log遍历链表中的DB_TRX_ID作比较直至满足条件,DB_TRX_ID所在旧记录就是当前事务可看见的最新的老版本
  • trx_list:一个数值列表,用于维护Read View生成时刻系统正活跃的事务ID
  • up_limit_id:记录trx_list列表中的事务最小ID
  • low_limit_id:ReadView生成时刻系统尚未分配的下一事务ID,即目前已出现过的事务ID的最大值+1
  • DB_TRX_ID < up_limit_id,则当前事务能看到DB_TRX_ID所在记录,若大于等于进入下个判断
  • DB_TRX_ID 大于等于 low_limit_id,则代表DB_TRX_ID所在记录在Read View生成后才出现,若小于则进入下一判断
  • 判断DB_TRX_ID是否在活跃事务中trx_list.contains(DB_TRX_ID),若在表示当前事务在活跃当中,但为commit,其他事务无法看见。若不在则说明当前事务在Read View生成之前就Commit,其他事务可见

整体流程

  • 事务2对某行数据执行快照读,数据库为该行生成Read View读视图,此时事务1和事务3在活跃中,事务4在事务2快照读前一刻提交更新,所以trx_list此时维护系统当前活跃事务1,3的ID
事务1事务2事务3事务4
事务开始事务开始事务开始事务开始
修改且提交
进行中快照读进行中

  • 首先DB_TRX_ID字段记录事务ID4与Read View的up_limit_id比较,4是否小于up_limit_id(1),不符合条件,继续判断4是否大于等于low_limit_id(5),不符合条件,最后判断4是否处于trx_list中的活跃事务,最后事务ID为4不在当前活跃事务列表中,符合可见条件,因此事务4修改提交的最新结果事务2快照读时可见,事务2读取的最新数据记录时事务4所提交的版本

MVCC相关问题

当前读和快照读在RR级别下的区别:
  • 表1:
事务A事务B
开启事务事务开始
快照读(无影响)查询金额为500快照读
更新金额为400
提交事务
select 快照读金额为500
select lock in share mode 当前读 金额为400

事务B在事务A提交修改后的快照读时旧版本数据,而当前读是实时新数据400

  • 表2:
事务A事务B
开启事务事务开始
快照读(无影响)查询金额为500快照读
更新金额为400
提交事务
select 快照读金额为400
select lock in share mode 当前读 金额为400

表1的事务B在事务A修改金额前快照读过一次金额数据,而表2事务B在事务A修改金额前没有进行过快照读

  • RR级别下的当前读,会出现不可重复读
  • RR级别下,没有提前查询快照读会出现不可重复读(不可重复读强调:事务B要提前查询一次,后面再查询一次,发现不一致;而没有提前查询的快照读,只有一次查询,本质是没有前后数据对比,从概念上不属于不可重复度问题)

RC,RR级别下的InnoDB快照读

  • RR级别下,同一事务中的第一个快照读会创建Read View,之后的快照读获取的都是同一个Read View
  • RC级别下,每个快照都会生成并获得最新的Read View

PCC、OCC和MVCC三者关系

  • 悲观锁(PCC)解决读-写冲突写-写冲突的加锁机制,可解决脏读、不可重复读、幻读、第一类更新丢失、第二类更新丢失,但其增加系统开销,数据吞吐量低不适合读多写少场景
  • 乐观锁(OCC)解决写-写冲突无锁机制,提交事务前,检查事务开始后是否有新提交改变,若没有则提交,否则放弃重试。无法解决脏读、不可重复读、幻读,可解决更新丢失问题
  • 多版本并发控制(MVCC)解决读-写冲突无锁机制,为事务分配增长时间戳,为每次修改保存版本号,版本和时间戳关联,可解决脏读、不可重复度、幻读不可解决更新丢失问题

数据库组合

  • MVCC+悲观锁
    • MVCC解决读写冲突,悲观锁解决写写冲突
  • MVCC+乐观锁
    • MVCC解决读写冲突,乐观锁解决写写冲突

MySQL SQL命令模拟测试

//查询当前会话的事务隔离级别
select @@tx_isolation

//查看当前数据库的全局事务隔离界别
select @@global.tx_isolation

//设置当前会话隔离级别
set session transaction isolation level repeatable read

//设置数据库全局隔离级别
set global transaction isolation level repeatable read

// 设置会话级别
// 0表示取消自动提交,即当执行语句commit或rollback时才结束
// 1表示数据库默认自动提交
set autocommit = 0 \ set autocommit = 1

// 查看是否开启自动提交
show variables like '%autocommit%'

// 开启事务
begin / start transction

// 回滚事务
rollback / rollback work

// 查看当前正执行的事务
select * from information_schema.INNODB_TRX
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页