事务

事务的基本属性(ACID)

  1. 原子性(Atomicity):事务内的所有操作要么一起成功,要么一起失败,是一个不可分割的整体
  2. 一致性(Consistency):事务的执行不会破坏数据库的完整性约束,包括数据的完整性和业务逻辑的完整性
  3. 隔离性(Isolation):不同事务之间互不干扰,彼此隔离
  4. 持久性(Durability):事务执行的结果应该是被数据库保存的,不会回滚

事务的并发问题

  1. 脏读:事务A未提交的数据被另一个事务B读取,事务A被回滚后,事务B读取到的就是脏数据,称为脏读
  2. 不可重复读:事务A查询到数据后,事务B更新了数据并提交,事务A再查询数据时,结果就不一样了,就叫不可重复读
  3. 幻读:事务A将某个条件下数据更新时,事务B又插入了一条符合条件的数据,在事务A结束后,发现还有未更新的数据,跟发生幻觉一样,这就叫幻读

区分不可重复读和幻读
不可重复读侧重于修改,幻读侧重于新增或者删除
不可重复读侧重于事务内获取数据的变化,幻读侧重于事务执行后的变化
要解决不可重复读,锁住相应的行即可,要解决幻读则需要锁表

事务的隔离级别

  1. 读未提交:这种隔离级别下,事务之间没有隔离,会出现脏读、不可重复读、幻读
  2. 读已提交:这种隔离级别下,事务内部是隔离的,但是并行的事务会互相影响,会出现不可重复读、幻读
  3. 可重复读:这种隔离级别下,事务通过MVCC机制根据事务的版本区分是否可以获取数据,不会出现不可重复读,但是还会有幻读
  4. 串行化:事务按序执行,不互相影响,不会出现幻读

开启事务的两种方式

  • begin/end transaction

    执行begin transaction 语句之后,并不会立即开启事务,而是当有sql执行的时候才正式开启事务
  • start transaction with consistence snapshot

    执行语句之后,立即开启事务
    

如何解决事务并发的问题

  • 脏读:RC以及以上隔离级别,通过MVCC机制,读取当前sql可见的数据快照,即可避免脏读
  • 不可重复读:在RR及以上隔离界别,通过MVCC机制,读取当前事务可见数据快照,即可避免不可重复读
  • 幻读:RR级别的next-key lock可以避免幻读,串行化隔离级别下,所有事务无法并行,可以避免幻读

索引

索引是一种排好序的用于提高查询效率的数据结构。

Mysql的数据是存储在磁盘中的,而从磁盘查找数据最大的成本是磁盘的寻道操作,所以一般来说随机的磁盘IO的查询数据最大的性能瓶颈,由此,Mysql借助索引来实现高效查找。

为什么Mysql使用B+树作为索引结构?
Mysql中的索引是由B+树实现的,B+树实质上是一颗多路平衡查找树,类似的查找树有很多数据结构,比如平衡二叉树、红黑树,还有同样可以高效查找的hash表、跳表,之所以选择B+树而不是其他的数据结构,最重要的原因是B+树查询所需要的随机磁盘IO最少。

那为什么说B+树的查询效率高呢?可以通过3层的B+树可查找的数据量来体现:

假设主键是bigint类型(8个字节),每个索引项的指针是6个字节,索引上的每个节点都是一个页游16KB,也就是一个节点可以容纳16KB/(8+6)B=1142个索引项,如果B+树为3层,那有2层非叶子结点作为索引,最后1层叶子结点存放了数据,假设一条记录1KB,也就是一个节点可以存放16KB/1KB=16条数据,总共1142114216约为2kw,也就是3次随机磁盘IO下可检索的数据量为2kw,可以满足大多数场景了

那为什么也不选择B树呢?再对比B树和B+树:

  1. B树的非叶子结点和叶子结点都包含数据(索引+记录的磁盘地址),B+树只有叶子结点有数据,非叶子结点只有索引,所以相同数据量下,B树需要的节点更多,可能会有更大的树高
  2. B树中要做范围查询,需要做中序遍历,而在B+树中,叶子结点之间由双向指针形成了双向链表,做范围查询时,只需在叶子节点链表中遍历即可,涉及的磁盘IO更少
  3. B树的插入删除都可能需要重新做平衡,B+树因为有数据都在叶子结点上,而且所有的非叶子节点都在叶子结点上冗余了,所以插入删除操作大多数都可以直接在叶子节点上完成,不需要对B+树的结构做调整,只有在插入后节点饱存在节点分裂,相比起来插入删除效率更高

在Mysql中,不同的存储引擎的索引有所差别:

InnoDB

InnoDB的索引可分为主键索引和二级索引,其中主键索引是存放了数据的聚集索引(也叫聚簇索引),二级索引则是非聚集索引,依靠叶子结点上的主键值,去主键索引中进行回表查询来获取完整的数据。

当然,如果二级索引是复合索引,sql中查询的字段都在复合索引中可以找到,则不会再去回表查询,这也叫覆盖索引

MyISAM

MyISAM的索引和数据是分别存储的,所以没有聚集索引,所有的索引都只能从索引树中获取到指向磁盘的地址,再进行磁盘IO获取到数据

大多数时候,为了性能,我们都不会在在业务中使用串行化的事务隔离级别,所以必然存在事务并发的问题,对同一个数据的并发处理,很明显会发生竞态条件,产生不可预测的结果,对于需要确定性的程序来说是不可接受的,所以需要锁来保证并发操作下的数据正确性

按锁的定义可分为:

  • 共享锁(S Lock)
  • 排他锁(X Lock)

共享锁一般是读锁,当一个事务获取了共享锁之后,另一个事务也可以对相同的数据获取共享锁,但是如果此时另一个事务想要获取相同数据的排他锁则会被阻塞,必须等待共享锁的释放

排他锁一般是写锁,当一个事务获取了排他锁后,另一个事务无论获取排他锁还是共享锁都会阻塞,需要等待排他锁匙放

也就是共享锁共享锁兼容,排他锁与任何锁都不兼容

InnoDB实现了行级的排他锁共享锁,而为了支待在不同粒度上进行加锁操作,InnoDB允许行级锁与表级锁共存,实现了一种称为意向锁的额外的加锁方式,意向锁是表级的锁,用于表明是否有事物请求对数据加行级锁:

  • 意向共享锁(IS Lock):事务想要获得一张表中某几行的共享锁
  • 意向排他锁(IX Lock):事务想要获得一张表中某几行的排他锁

当事务对一行数据请求排他锁时,会同时对表请求意向排他锁,当另一个事务请求对全表进行修改,没有意向排他锁时,需要一行一行地扫描是否有行被锁定,而有意向排他锁时,则不需要判断,直接等待行锁匙放就行了。

InnoDB三种行锁算法:

  1. 行记录锁(Record Lock

    
    在行记录上加锁,防止事务间删除或者修改数据
    
  2. 间隙锁(Gap Lock

    
    锁住各个记录之间的间隙,不锁记录本身,为了防止幻读
    
  3. 临键锁(Next-Key Lock)

    
    锁住行记录本身与各个记录之间的间隙,为了防止幻读
    

注:可重复读隔离级别(RR)下,MVCC就可以解决快照读的幻读问题,为什么还要next-key 锁来防止并发呢?因为除了RR下的简单select语句是快照读,select … for update、select … lock in share mode都是当前读,还有inert、update、delete都是当前读,所以还需要next-key来防止幻读。

Next-Key Lock是Recod Lock 与 Gap Lock的组合,主要是为了防止幻读,是RR隔离级别下默认的行级锁算法:

  • 唯一索引的等值检索时

    • 如果查询记录存在,next-key lock会退化成record Lock
    • 如果查询记录不存在,next-key lock会退化成gap Lock
  • 唯一索引的范围检索时,next-key lock 退化为间隙锁和记录锁
  • 非唯一索引的等值检索时

    • 如果查询记录存在,除了加next-key lock外,还会加gap lock
    • 如果查询记录不存在,只会加next-key lock,然后退化成gap lock
  • 非唯一索引的范围检索时,next-key lock 不会退化为间隙锁和记录锁
  • 非索引检索则会加上全表next-key lock,也就是锁全表

InnoDB之所以可以实现行级锁,是因为有聚簇索引,基于聚簇索引可以实现在一棵索引树上产生竞态条件,从而实现行级锁

MyISAM中只支持表级锁,表级的共享锁、排他锁锁是Mysql的server层通过一种元数据锁的结构实现的。

InnoDB中依靠锁实现了一个事务的写操作与另一个事务的写操作的隔离性,以下是不同隔离级别下所使用的锁机制:

  • RC:insert、update、delete加Recode Lock
  • RR:iselect、nsert、update、delete都加Recod Lock
  • 串行化:读写都加表锁

MVCC

mvcc是多版本兵法控制,用来在数据库中控制事务并发读写的方法,mvcc只在隔离级别为RC、RR有效,是通过undo log中的版本链与ReadView一致性视图来实现的,mvcc就是在多个事物同时存在时,select语句找到具体版本链上的哪个版本,然后在找到的版本上返回所记录的数据。

原理

通过在每行记录上增加两个隐藏字段,分别保存了记录的创建时的版本号,和删除时的版本号,每开启一个新的事务版本号都会递增。

INSERT

插入的时候,将当前事务的ID作为创建的版本号

SELECT

根据两个条件查找记录:

  • 创建版本号小于当前事务ID
  • 删除版本号不存在或者大于当前事务ID

UPDATE

实际上是插入了一个新行,并且删除了原来记录行,插入的新行创建版本号为当前事务ID,删除的旧行的删除版本号为当前事务ID

DELETE

将当前事务ID作为行的删除版本号

上面的机制保证了,一个事务不会读取到另一个事务新产生的变更。

InnoDB的MVCC实现

InnoDB中会增加三个隐藏的列:

  1. DB_TRX_ID

    记录最近更新这条数据的事务ID
    
  2. DB_ROLL_PTR

    回滚指针,用于配合undo log找到该行的所有旧版本,形成一个版本链
    
  3. DB_ROW_ID

    隐藏的主键,表没有主键或者非NULL唯一列时会创建
    

ReadView是事务在快照读时创建的读视图,记录了事务快照图那一刻,系统中活跃的事务的ID,用于做可见性判断,即当前事务执行快照读的时候,创建一个ReadView,用于判断当前事务可以看到哪些版本的数据,如果当前活跃的事务不符合可见性,还会通过DB_ROLL_PTR回滚指针去undo log中取出历史版本的DB_TRX_ID来做比较,直到获取到满足条件的版本。

ReadView的结构:

  • trx_list

    
    当前活跃的事务列表
    
  • up_limit_id

    
    活跃事务列表中最小的事务ID
    
  • low_limit_id

    
    当前系统还没有分配的下一个事务ID
    
    

可见性判断:

  1. 首先比较DB_TRX_ID < up_limit_id则符合可见性,否则进入下一个判断
  2. 判断DB_TRX_ID ≥ low_limit_id,成立则说明这个版本是在当前ReadView之后才创建的,不可见,进入下一个判断
  3. 最后判断DB_TRX_ID是否在trx_list中,如果不在则说明该版本在创建ReadView之前就commit了,符合可见性,否则说明在创建ReadView时这个版本的事务还是活跃的,没有commit,对应的数据也不可见

不同的隔离级别下,创建ReadView的时机不同

  • RC下,每次快照读都会创建ReadView
  • RR下:只有第一次快照读会创建ReadView,后面

RC隔离级别下,每次快照读都会创建ReadView,也就导致在两次快照读之间,另一个事务的变更会被读取,所以存在 不可重复读

mvcc的优点,读操作不阻塞写操作,写操作也不阻塞读操作,实现了不加锁的并发读写

标签: 事务, , 索引, MVCC

添加新评论