浅析事务,锁和索引


MySQL的默认存储引擎为InnoDB而不是MyISAM的一大原因就是InnoDB是支持事务的,而MyISAM不支持事务。(我觉得前者强调安全,而后者性能更好,当然在要求并发量的当下,不足以成为被选择的理由,所以也渐渐被InnoDB淘汰)。

事务

事务具有ACID四大特性:

  1. 原子性(Atomicity):有点像std::atomic(当然不完全一样)是一个最小的原子单元,是一个不可分割的的一条(或多条)SQL命令集合,要么全部完成,要么全部失败,执行失败就会回滚(rollback)
  2. 一致性(Consistency):事务的一致性说的是事务要保证把数据库从一个正确的状态迁移到另一个正确的状态。正确状态就是说数据库里的数据满足其约束条件。
  3. 隔离性(Isolation):一个事务不会影响其他事务的运行,在多个事务运行的情况下,隔离性保证了并发基础,当然实际上还需要配合锁来保证并发下的数据安全。
  4. 持久性(Durability):事务完成后,对数据库的更改永久保存,不会因为故障丢失。

每条SQL语句都默认封装为了一个事务,自动提交,影响速度,所以为了效率可以将多个SQL命令使用begincommit封装为一个事务

一致性中,数据库从一个正确的状态迁移到另一个正确的状态,指的是数据处于一种语义上有意义且正确的状态,
1.原子性和一致性的的侧重点不同:原子性关注状态,要么全部成功,要么全部失败,不存在部分成功的状态。而一致性关注数据的可见性,中间状态的数据对外部不可见,只有最初状态和最终状态的数据对外可见。
2.在未提交读的隔离级别下,会造成脏读,这就是因为一个事务读到了另一个事务操作内部的数据。ACID中是的一致性描述的是一个最理想的事务应该怎样的,是一个强一致性状态,如果要做到这点,需要使用排它锁把事务排成一队,即Serializable的隔离级别,这样性能就大大降低了。现实是骨感的,所以使用隔离性的不同隔离级别来破坏一致性,来获取更好的性能。

为了控制多个事务对数据的并发操作,我们需要加锁,否则有可能发生数据的丢失或者错乱

  • 乐观锁和悲观锁是两种应用层去实现的并发模式
    1. 悲观锁就是针对一切数据修改都加锁
    2. 乐观锁的实现可以基于版本号或时间戳来实现
  • 共享锁和排他锁是InnoDB实现的两种行锁
  • 锁粒度越小,冲突发生的概率越小,但代价越高,就像打扫房间,“粒度”越小,细致的打扫整个房间,付出的精力当然更大,但相对应的打扫的也更干净
  • InnoDB的行锁又有三种实现方法
    1. 记录锁,加在行记录的索引上,封锁该行的索引记录
    2. 间隙锁,锁住一个区间,锁住的的索引的间隙,确保其索引记录之间的间隙不变
      现假设有表 table,
      在这里插入图片描述
      当我们执行:
Session_1 : 
START TRANSACTION;
SELECT * from table WHERE number = 4 FOR UPDATE;
…..
Session_2:
START TRANSACTION;
INSERT into table values (2, 2);		//阻塞
INSERT into table values (2, 4);		//阻塞
INSERT into table values (4, 5);		//阻塞
INSERT into table values (6, 7);		//执行成功
INSERT into table values (11, 12);	//执行成功
…
  1. Next-key锁,记录锁+间隙锁,锁住间隙的同时锁住改行本身
    InnoDB的行锁算法都是基于索引实现的,锁定的也都是索引或索引区间

隔离级别的原理

讨论完事务和锁之后,就可以探讨一下隔离级别
隔离级别决定的是一个session中的事务对另一个session中事务的影响程度

  1. 读未提交(Read Uncommitted):所有事务都能看到其他事务未提交的执行结果,据我所知该隔离级别应该在实际应用中很少使用,而且他并没有明显的性能优势,还不能避免脏读,不可重复读,幻读这些数据丢失和错乱的情况
    原理:事务读当前数据时不加锁,在更改数据的时候,对其加行级共享锁,结束时释放。
    表现

    • 事务1读取某行数据时,事务2也能读取该行数据;
    • 事务2更新该行数据时,事务1能读到更新后的版本,即使尚未被提交;
    • 事务1对某行数据进行更新时,事务2不能更新该行数据,直到事务1结束。
  2. 读已提交(Read Committed):一个事务开始后,只能看见已经结束的事务的结果,正在执行的事务无法看到,
    原理:事务对当前被读取的数据加行级共享锁(读到时加),读完释放;事务在更新数据时加行级排他锁,事物结束后释放
    表现

    • 事务1读取某行数据,事务2也能同时读取;
    • 事务2更改某行数据,事务1要么读取到其commit前的要么读取到其commit后的;
    • 事务1更新某数据时,事务2不能对其进行更新,直到事务1结束
  3. 可重复读(Repeatable Read):该级别保证了每行数据的一致
    原理:事务在读取某数据时,加行级共享锁(开始读时加),直到事务结束时释放;
    事务在更新某数据时,加行级排他锁
    表现

    • 事务1读取某行数据时,事务2也能对该行数据进行读取,更新;
    • 当事务2更新某行数据时,事务1读取该行数据仍然是第一次读取到的版本;
    • 事务1更新某行数据,事务2不能对其进行更新,直到事务1结束
  4. 可串行化(Serilaizable):最高隔离级别,强制事务串行执行
    原理:事务在读取数据时,加表级共享锁,结束时释放;
    在更新数据时,加表级排他锁,结束时释放
    表现

    • 事务1正在读取A表中的记录时,则事务2也能读取A表,但不能对A表做更新、新增、删除,直到事务1结束。
    • 事务1正在更新A表中的记录时,则事务2不能读取A表的任意记录,也不能更新,直到事务1结束。

而InnoDB默认工作模式为RR(Repeatable Read)模式,默认加锁为next-key锁,这样防止了幻读的发生(不能对该行数据进行修改,或者插入记录)

幻读就是说事务1读取了数据,然后这之间事务2插入了一行或者多行的满足事务1的选择条件,导致事务1再次使用相同的选择条件读取时,得到了比第一次更多的数据(发生了幻觉)

索引

(就不一一列出B-Tree和B+Tree的实现了,只讨论其特性)

索引是一种数据结构,存储指向列值,包含指向行的指针,通过对其这些指针进行排序,达到一个能够快速查找的目的。
在没有索引的情况下,我们进行查找的话,自然是需要遍历的,这就是一个O(n)的复杂度,那么当数据量上去的情况下,耗时就会比较高了,在要求性能的时候,这是不被允许的(况且还有磁盘IO 的代价)。
MySQL的索引实现是一个树形结构,而具体的底层实现上选用的是B+Tree

为什么选用B+Tree
数据库的索引存储在磁盘中,所以要考虑的不仅仅是查找的效率,还要考虑磁盘IO的效率,如果我们使用二叉查找树,那么在最坏的情况下,磁盘IO次数是树的高度,所以选用了B+Tree这种多叉查找树,在同等数据量的情况下,降低了树的高度,让他更加“矮胖”(意味着更少的磁盘 IO 次数,这也是较之B-Tree的一个优点,B+Tree中间节点不存储指向行的指针,而都位于叶子节点,所以同等数据量下,B+Tree比B-Tree更加“矮胖”)
而且因为B+Tree的查询最终都要在由叶子节点组成的链表中,每次查询的时间复杂度也很平均

我记得MongoDB的索引使用的B-Tree

为什么不用哈希索引?
哈希索引的优点就是快,O(1)的查找时间复杂度
但是缺点也很明显:

  1. 要考虑发生哈希冲突的时候的解决,提高了成本
  2. 可能会有空出的哈希空间,造成浪费
    (记得以前讨论过为什么epoll使用红黑树而不是哈希表,原因类似)
  3. 不能做范围操作
    (Redis就是一个哈希表搭配一个跳跃表实现的,结合了两者的优点才同时做到快速和范围查询,保证了效率)

MVCC

如果数据库的事务都是串行执行的,那么很多问题就不用考虑了,但是这样做必然会导致性能上的瓶颈,所以我们要使用多事务并发,那么这样一来就要面临数据的一致性和数据的安全性等问题,如果不应对好,可能还没有享受并发带来的性能提升,就已经走远了…

而控制并发有三种途径,分别是悲观锁,乐观锁(上篇周报中提到的)以及 MVCC,也就是——多版本并发控制
每个写操作会创建一个新的数据版本,然后读操作从有限多个版本中挑选一个合适的版本返回,那么这样一来,读写之间的冲突被忽略,而怎么管理和挑选合适的数据版本才是 MVCC 需要关注的问题。

MVCC 的特点是:读不上锁
这样一来,针对读多写少的场景,性能大大提高,

解决了什么?

MVCC 读不上锁,所以可以该机制可以代替行级锁使用,降低系统开销(锁就算粒度再大,也是有不可忽视的系统代价的)

如何实现?

而 MVCC 是通过保存数据在某一个时间点的快照(是不是感觉有点像Redis的RDB持久化?)实现的

有点像每行数据都存在着有限个平行宇宙

  1. 读取数据:
    每个版本的数据行都有着唯一的时间戳,读取哪行就在多个版本中选择一个最大的返回
    在这里插入图片描述

  2. 更新数据:
    事务读取最新版本数据并进行sql命令操作得到数据更新后的结果,然后以此创建一个新版本的数据,其时间戳为当前最大时间戳+1(创建出来的数据的数据行时间戳保持最大)
    在这里插入图片描述
    而MySQL也会定期将最低版本的数据删除

刚才说了,读操作有快照读,还有一个是当前读,因为快照度选择的可见范围内的最大时间戳,于是可能读到的是已经过期的数据,当前读保证的就是读取到的是最新的数据(需要加锁来实现)

我觉得快照读取就像是拍照片一样,就像是当前事务的数据来源于一张旧照片(之前某个时间点生成的数据快照),然后当前事务结束前,其他事务怎么改变数据,都不会影响到这张照片,这就实现了可重复读

  1. select就是快照读(照片的生成时间是执行select的时间,不是事务开始的时间)
  2. update,delete,insert是当前读
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章