简单谈谈MySQL的事务

172 阅读10分钟

简单谈谈MySQL的事务

事务的基本特性和隔离级别

基本特性 ACID

事务的基本特性就是==ACID==

ACID,是指数据库管理系统(DBMS)在写入或更新资料的过程中,为保证事务(transaction)是正确可靠的,所必须具备的四个特性:

  • 原子性(atomicity,[ætəˈmɪsəti],或称不可分割性)
    • 一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节
  • 一致性(consistency,[kənˈsɪstənsi])
    • 在事务开始之前和事务结束以后,数据库的完整性没有被破坏
  • 隔离性(isolation,[aɪsəˈleɪʃn],又称独立性)
    • 一个事务的修改在最终提交前,对其他事务是不可见的
    • 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以**防止多个事务并发执行时由于交叉执行而导致数据的不一致 **
      • 事务隔离分为4个不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)
  • 持久性(durability,[djʊərəˈbɪlɪti])
    • 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失

我们需要根据事务的原子性、隔离性和持久性来保证数据的一致性

那么多个事务访问同一个资源时,如何保证资源的可见性呢,这里需要谈到隔离级别了

4个隔离级别

  • read uncommit读未提交也叫做脏读可能会读到其他事务未提交的数据
    • A向B转账,B本来有300,如果收了200就为500了,但是有可能事务会出现异常则修改失败应该回滚为300,可是此时另一个事务读取B的余额为500的脏数据,这就是脏读
  • read commit读已提交也叫做不可重复读两次读取结果不一致,oracle的默认级别
    • 不可重复读解决了脏读的问题,他只会读取已经提交的事务
    • 用户开启事务读取id=1的用户,查询到age=10,其他事务修改并提交了之后,再次读取发现结果=20
    • 在同一个事务里同一个查询读取到不同的结果叫做不可重复读
  • repeatable read可重复读,这是mysql的默认级别,就是每次读取结果都一样
    • 但是有可能产生幻读,使用间隙锁解决
  • serializable串行,一般不使用。给每一行读取的数据加锁, 会导致大量超时和锁竞争的问题

隔离级别中可能出现的问题

  • 脏读(Drity Read):可以给数据上写锁,让其他事务不能并发读和并发写,解决了脏数据的问题
    • 某个事务已更新一份数据, 另一个事务在此时读取了同一份数据,由于某些原因,前一个回滚了操作,则后一个事务所读取的数据就会是不正确的。
  • 不可重复读(Non-repeatable read):可以给数据上读锁,让其他事务可以并发读,但是不能写,解决了读取结果不一致的问题
    • 在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
  • 幻读(Phantom Read):使用间隙锁解决幻读问题(for update)
    • 在一次事务里面,多次查询之后,结果集的个数不一致的情况叫做幻读。而多或者少的那一行被叫做幻行
    • 例如事务1查询到3条记录但是不commit提交,事务2insert后就commit提交了,事务1再查询还是3条,但是执行全表update时却更新了4条,在更新完后再查询select,发现查询出来了4条数据,那么这就是产生了幻读,多出来的那条数据就是幻行
      • 因为select是快照读,而update是当前读
      • 如果加上锁之后,别的事务无法操作数据,也就不会出现幻读问题了
      • 间隙锁本质上是用于阻止其他事务在该间隙内插入新记录,而自身事务是允许在该间隙内插入数据的

ACID是靠什么保证的

  • 原子性由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql

  • 一致性由其他三大特性保证,程序代码要保证业务层面上的数据一致性

  • 隔离性由MVCC来保证

  • 持久性由内存、redo log来保证,mysql修改数据同时在内存和redo log记录这次操作,宕机的时候可以从redo log恢复

    • redo log的刷盘会在系统空闲时进行

    • 将新数据写入内存,然后对InnoDB的redo log写盘,redolog中事务进入prepare状态

      如果前面prepare成功,bin log写盘,将事务日志持久化到bin log

      如果持久化成功,那么在redo log里面写一个commit 记录,表示InnoDB事务进入commit状态

    • 如果redolog写入完,变为prepare状态,可是断电了,没来得及写入bin log的话,在重新开启后会抛弃已经prepare的事务(因为它没有完成提交,所以不作数)

在innodb中有两个日志,undo log和redo log

在mysql server中有一个叫bin log的日志,也和事务有关

事务的成功不仅要在bin log中留下记录,而且也在redo log中写入commit提交状态的记录,表示事务已经完成提交

什么是MVCC

MVCC,全称 Multi-Version Concurrency Control ,即多版本并发控制

读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了,不同的事务session会看到自己特定版本的数据——版本链

MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存

MVCC 在 MySQL InnoDB 中的实现主要是为了提高数据库并发性能,用更好的方式去处理读写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读

MVCC只在RC和RR两钟隔离级别下工作,即read commit不可重复读和repeatable read可重复读

脏读每次读的都是最新的数据,不符合

串行会给所有读取的行都加锁,也不符合

一般我们认为MVCC有下面几个特点:

  • 每行数据都存在一个版本,每次事务更新数据时都更新该版本号
  • 修改时Copy出当前版本随意修改,和事务之间无干扰
  • 保存时比较版本号,如果成功(commit),则覆盖原记录;失败则放弃copy(rollback)

在InnoDB中,会在每行数据后添加两个额外的隐藏的字段来实现MVCC ,一条记录除了包括各个字段值,还包括了当前事务id(trx_id)一个回滚指针(roll_pointer)

  • trx_id:生成这条记录(update/delete)的事务id
  • roll_pointer:指向了undo_log中保存着的原来的记录,根据一条条的记录中的回滚指针从而构成版本链

注:一个事务的事务id在第一次insert/delete/update时生成

readview是什么

其实Read View的最大作用是用来做可见性判断

也就是说当某个事务在执行快照读的时候,对该记录创建一个Read View的视图,把它当作条件去判断当前事务能够看到哪个版本的数据

有可能读取到的是最新的数据,也有可能读取的是当前行记录的undolog中某个版本的数据

事务开启后会创建readview,用于维护当前活动的事务的id(活动=未提交的事务),并排序生成一个数组

readview中存储了当前Read View中最大的ID及最小的ID

之前提到了在数据的隐藏列中trx_id用于储存了事务id(获取的是最大的那个id,即最新的id),在读取数据时,会拿到这个事务id去和readview中的比对

  • up_limit_id是当时活跃事务列表的最小事务id

    • 如果row的db_trx_id<up_limit_id,说明这些数据在事务创建id的时都已经提交,如注释中的描述,这些数据均可见
    • 小于 可见
  • ow_limit_id是当时活跃事务的最大事务id

    • 如果读到row的db_trx_id>=low_limit_id,说明这些id在此之前的数据都没有提交,如注释中的描述,这些数据都不可见
    • 大于等于 不可见

MVCC是如何实现不可重复读和可重复读的

读已提交(不可重复读)隔离级别下的事务在查询的开始会创建一个独立的readview

可重复读隔离级别下的事务在第一次读的时候生成一个readview,之后的读都是复用的这个readview

即:通过不同的readview生成策略来实现不同的隔离级别

mysql的原子性和持久性是如何实现的

原子性

  • 根据回滚指针roll_pointer指向的undolog中的上一条记录进行回滚

持久性

  • 将新数据写入内存,然后对InnoDB的redo log写盘,redolog中事务进入prepare状态
  • 如果前面prepare成功,bin log写盘,将事务日志持久化到bin log
    • 如果持久化成功,那么在redo log里面写一个commit 记录,表示InnoDB事务进入commit状态
    • 如果持久化失败(断电了),如果redolog写入完,变为prepare状态,可是断电了,没来得及写入bin log的话,在重新开启后会抛弃已经prepare的事务(因为它没有完成提交,所以不作数)

当前读、快照读,到底读的是什么

当前读:读取的是数据的最新版本,总是能读取到最新的数据

快照读:读取的是历史版本的快照记录

为什么mysql中会有历史版本的记录呢?

原子性由undo log日志保证,它记录了需要回滚的日志信息,有时候可能会需要回滚,所以才将其记录了下来

在mysql默认的RR不可重复读隔离级别中,select读是快照读,读取的是历史版本的快照记录

上锁或对表的操作是当前读

lock in share mode

for update

update

insert

delete

共享锁、排他锁

  • 共享锁

    • 简称S锁,其他线程可以继续获取共享锁
    • IN SHARE MODE获得共享锁,主要用在需要数据依存关系时来确认某行记录是否存在,并确保没有人对这个记录进行UPDATE或者DELETE操作
  • 排他锁

    • 简称X锁,只允许获取一次,不容许其他线程获取独占锁。
    • 所以像select for update就会独占线程锁死这行
    • 但是如果当前事务也需要对该记录进行更新操作,则很有可能造成死锁,对于锁定行记录后需要进行更新操作的应用,应该使用FOR UPDATE方式获得排他锁
    • 使用排它锁的事务不进行commit的话,别的事务是无法对被锁住的事务进行操作的
  • delete:

    • X锁
  • insert:

    • 一般不加锁,隐式锁
  • update:

    • 不更新主键的情况下:
      • X锁:就地更新
    • 更新主键的情况下:
      • X锁:更新主键的情况下,先执行delete操作,(隐式锁)再执行insert操作
# 共享锁(S):
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE

# 排他锁(X):
SELECT * FROM table_name WHERE ... FOR UPDATE

关于mysql锁其实还有很多,可以查看一下这个博客www.cnblogs.com/zhuwenjoyce…