MySQL中的事务和锁

本文深入探讨了数据库事务的ACID特性,分析了并发环境下可能出现的问题如脏读、不可重复读和幻读,并介绍了四种事务隔离级别。重点讲解了MySQL中的MVCC多版本并发控制,以及InnoDB的行锁类型RecordLock、GapLock和Next-KeyLock。此外,还对比了悲观锁和乐观锁的工作原理及其在实际应用中的差异。
摘要由CSDN通过智能技术生成


一、Mysql中的事务

事务一般要求有ACID四大特性:

  • 原子性: 事务的执行为一个原子 操作,对于数据的操作要么都执行,要么都不执行,这依赖于Log中的Redo Log和Undo Log。
  • 持久性: 事务一旦提交成功之后,对于数据的操作将是永久性的,后面再发生错误将不会影响到对应数据。
  • 隔离性: 事务之间的操作是相互隔离的,在并发情况下多个事务之间的操作互不影响。
  • 一致性: 事务开始之前和事务结束之后,数据库的完整性限制没有被破坏。

我们这样理解,把事务当做一个线程,当多个事务同时工作的时候就变成了多线程并发问题,对于数据库在并发情况下会产生下面问题。

  • 脏读: 一个事务读取到了另一个事务修改但是还没有提交的数据,正常情况下这个情况必须需要避免的,因为修改数据事务没有提交情况下事务有可能回滚,数据修改可能会被放弃,所以读出来的数据是没有意义的。
  • 幻读: 一个事务多次按照同一条件读取数据读取的结果不一样,有事务插入或者删除了对应查询条件的数据,这个强调的是读取的条数变多或少了。
  • 不可重复读: 一个事务多次读取同一条数据的结果不一样,这个强调的是同一条数据被修改了。

为了解决事务之间的并发问题,数据库通过事务之间的隔离来解决这些问题,数据库系统一般都会提供下面四种隔离级别:

  • 读未提交(READ UNCOMMITTED): 读未提交顾名思义可以读取其他事务操作但是没有提交的数据,这不就是脏读里面描述的现象。
  • 已提交读(READ COMMITTED): 只能读取到其他会话中已经提交的数据,解决了脏读。但可能发生不可重复读现象,也就是可能在一个事务中两次查询结果不一致。
  • 可重复读(REPEATABLE READ): 解决了不可重复读,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行,但是这可能会导致幻读。
  • 串行化(SERIALIZABLE): 通过设置所有的事务串行执行执行,强制事务排序,解决相互冲突,解决幻度的问题。这个级别可能导致大量的超时现象的和锁竞争,效率低下。

根据不同事务隔离级别是否会产生对应的并发问题,我们可以列出下面表格

事务隔离级别脏读不可重复读幻读
读未提交
读已提交不会
可重复读不会不会
串行化不会不会不会

可以看到串行化是最安全的事务隔离级别,但是我们主要是解决并发问题的,串行化之后事务成了单线程操作了,效率太低了,这已经违背了初衷,所以所以一般数据库没有选择串行化的,综合考虑性能与业务之间的情况,Mysql中默认使用的是可重复度的事务隔离级别,而Oracle和SQLServer采用读已提交的事务隔离级别。虽然数据库默认的这些隔离级别还是会产生一定的问题,但是我们可以通过使用上的处理来避免这些问题。

二、MVCC版本控制原理

我们知道Mysql默认使用了可重复读的隔离级别,它是怎么实现的呢?正常情况下在多线程中可能通过加锁来保证数据的安全性,在Mysql中没有使用锁,而是底层是基于MVCC多版本控制机制实现,读取原来快照数据。

  • 快照读:读取的是记录的快照版本(有可能是历史版本),不用加锁。
  • 当前读:读取的是记录的最新版本,并且当前读返回的记录,都会加锁,保证其他事务不会再并发修改这条记录。
  • MVCC:Multi-Version Concurrency Control,多版本并发控制。MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问;在编程语言中实现事务内存,就是同一份数据临时保留多版本的一种方式,进而实现并发控制。

在Mysql一张表中,除了我们可以直接看到的字段还有四个隐藏列,其中三个与MVCC有关:

  • DB_ROW_ID: 在数据表中我们一般建立有主键ID,假设没有主键ID那么数据库是用什么来识别这个数据的呢?就是使用的DB_ROW_ID自动产生一个聚簇索引。
  • DB_TRX_ID: 记录了当前数据最后一次操作(修改/删除)的事务ID。
  • DB_ROLL_PTR: 这是回滚指针,该指针指向了当前记录的上一个版本数据。

怎么说呢?MVCC版本控制其实优点类似于乐观锁。画个图来演示一下修改数据是怎么操作的:
在这里插入图片描述
这么看这张图就能清楚了,全局事务ID是一个自增的数字,就相当于乐观锁中的版本,每次修改数据之后都会+1,因为事务可以回滚,原来的数据保存在了Undo log日志里面,我们需要当前数据跟备份数据建立起联系,所以回滚指针指向了日志备份里面数据,在事务回滚的时候可以直接找到对应的数据。

为什么另一个事务无法读取到被操作但是没有提交的数据呢?因为在SQL查询的时候内部默认给查询语句后面增加了个WHERE条件,要求数据的事务ID要小于等于当前事务ID,并且读取的数据是快照数据(这个可以理解为一个原来数据的视图),所以不会读取到操作的数据。

三、锁

我们知道InnoDB中采用的是行锁来保证数据的安全性的,InnoDB中的行锁涉及到3中算法:

  • RecordLock锁:记录锁,用来锁定单个记录。
  • GapLock锁:间隙锁也叫范围锁,可以锁住记录的范围,举个例子,假设对ID为18到22的数据加锁的时候,间隙锁就会锁住18~22范围内的数据。
  • Next-key Lock 锁:间隙锁和记录锁的组合,可以同时锁住单个记录以及记录的前后范围。

Mysql中默认的可重复读隔离级别在加锁的时候,默认使用的Next-Key Lock锁,假设SQL操作含有唯一索引时,Innodb会对Next-Key Lock进行优化,降级为RecordLock只锁住当前记录本身而非范围。因为ID是唯一的,所以可以通过ID对数据进行加锁。

悲观锁

我们知道数据库中可以在SQL末尾加上FOR UPDATE来设置为悲观锁,具体是什么个情况呢?来演示一下悲观锁:
开启事务线程1:

begin;
SELECT * FROM user where id = 4 FOR UPDATE;

正常情况下update才会锁住ID为4的这条数据,用上悲观锁之后SELECT也可以锁住这条数据了,开启事务线程2进行修改ID为4的数据:
在这里插入图片描述
可以看到对ID为4的数据进行操作的时候一直处于等待状态,因为在另一个事务里面对它上了锁,只有对事务进行提交或者回滚之后才会释放锁。
在代码里面使用悲观锁就是进入Service方法中后先for update查询一下数据,然后再进行后面的一些列操作,最后提交事务。

乐观锁

乐观锁虽然叫锁其实是没有锁的,其原理就是在数据的末尾增加version字段,每次数据进行修改的时候对version字段+1操作。
简单的看一下在代码中怎么实现的:

 method(){
    select * from user where id=1;
    //这时候获取出来的信息是带有version字段的
    ...这里是一系列的其他操作
    update user set name = '张三' where id =1 and version = #{version}
    //在修改数据的时候增加version 的判断,看是否与刚取出来的时候一致。
 }

乐观锁与悲观锁不同的地方在于悲观锁在等待另一个事务提交之后就可以继续修改数据了,而乐观锁这里需要手写代码是否需要继续提交,还是直接返回更新条数为0条更新失败。如果需要提交的时候需要将代码设置自旋,在并发量小的情况下可以,并发量非常大的情况下还不如悲观锁效率高呢,因为一直自旋判断处理是需要消耗CPU的。

我们知道事务如果开启之后不提交其他事务线程将无法对该数据进行修改删除操作,那么假设忘记提交或者回滚了,然后没有管,那怎么去释放该数据呢?可以通过下面SQL查询当前的事务:

select *  from information_schema.innodb_trx t

可以看到在查询结果里面有个当前事务线程ID
在这里插入图片描述
然后通过Kill杀死该线程,再次查询就会发现该事务线程已经清除,可以对事务行数据进行操作了。

kill trx_mysql_thread_id
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值