MySQL 必知概念

Delete、Drop 和 Truncate

  • delete、truncate 仅仅删除表里面的数据,drop会把表的结构也删除
  • delete 是 DML 语句,操作完成后,可以回滚,truncate 和 drop 是 DDL 语句,删除之后立即生效,不能回滚
  • 执行效率:drop > truncate > delete

MyISAM 与 InnoDB

  • InnoDB 支持事务,MyISAM 不支持
  • InnoDB 支持外键,MyISAM 不支持
  • InnoDB 是聚集索引,数据文件是和索引绑定一起的
  • MyISAM 是非聚簇索引,索引和数据文件是分离的,索引保存的是数据的指针
  • InnoDB 不保存表的具体行数,执行 select count(*) from table 时需要全表扫描
  • MyISAM 用一个变量保存整个表的行数,执行上述语句时只需要读出改变量即可,速度很快
  • InnoDB 支持表、行(默认)级锁,MyISAM 支持表级锁

Join 语句

left join、right join、inner join 的区别:

left join(左连接):

  • 返回包括左表中的所有记录和右表中联结字段相等的记录
  • 左表是驱动表,右表是被驱动表

right join(右连接):

  • 返回包括右表中的所有记录和左表中联结字段相等的记录
  • 右表是驱动表,左表是被驱动表

innner join(等值连接):

  • 只返回两个表中联结字段相等的行
  • 数据量比较小的表作为驱动表,大表作为被驱动表

join 查询在有索引条件下

  • 驱动表有索引不会使用到索引
  • 被驱动表建立索引会使用到索引、

所以在以小表驱动大表的情况下,给大表建立索引会大大提高查询效率

Join 原理

Simple Nested-Loop:

  • 驱动表中的每一条记录与被驱动表中的记录进行比较判断(笛卡尔积)
  • 对于两表联结来说,驱动表只会被访问一遍,但驱动表却要被访问到好多遍

Index Nested-Loop:

  • 基于索引进行连接的算法
  • 他要求被动表驱动表上有索引,可以通过索引来加速查询

Block Nested-Loop:

  • 它使用 Join Buffer 来减少内部循环读取表的次数
  • Join Buffer 用以缓存联接需要的列

选择 Join 算法优先级:

  • Index Nested-LoopJoin > Block Nested-Loop Join > Simple Nested-Loop Join

当不使用 Index Nested-Loop Join 的时候,默认使用 Block Nested-Loop Join

分页查询优化

select * from table 
where 
type = 2 
and level = 9 
order by id asc 
limit 190289,10;

延迟关联:

  • 通过 where 条件提取出主键,再将该表与原数据表关联,通过主键 id 提取数据行,而不是通过原来的二级索引提取数据行

  • select a.* from table a,
    (
        select id from table 
        where type = 2 
        and level =9 
        order by id asc 
        limit 190289,10
    ) b 
    where a.id = b.id;
    

书签方式:

  • 找到 limit 第一个参数对应的主键值, 在根据这个主键值再去过滤并 limit

  • select * from table 
    where 
    id > (
        select * from table 
        where type = 2 
        and level = 9 
        order by id asc 
        limit 190289, 1
    	) 
    limit 10;
    

事务

ACID

  • 原子性(atomicity):指事务是一个不可分割的组织,要么全部提交,要么全部失败回滚
  • 一致性(consistency):指事务执行前后,数据从一个合法性状态变换到另外一个合法性状态(状态与业务相关)
  • 隔离型(isolation):指一个事物的执行不能被其他事务干扰;一个事物内部的操作 及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰
  • 持久性(durability):指一个事务一旦被提交,ta 对数据库中数据的改变就是永久性的,其他操作和数据库故障并应该对其有任何影响

数据并发产生的问题

丢失更新(Lost Update):

  • 两个事务,如 Session A、Session B,如果事务 Session A 修改了另一个未提交事务 Session B 修改过的数据,意味着发生了丢失更新

脏读(Dirty Read):

  • 两个事务,如 Session A、Session B,Session A读取了已经被 Session B更新但还没有被提交的字段,如果后来 Session B 回滚,那么 Session A 读取的内容就是临时且无效的

不可重复读(Non-Repeatable Read):

  • 两个事务,如 Session A、Session B,Session A 读取了一个字段,然后 Session B更新了该字段,如果后来 Session A 再次读取同一个字段,该字段的值发生了变化

幻读(Phantom):

  • 两个事务 Session A、Session B,Session A 从一个表中读取了一个字段,然后 Session B在该表中插入了一些新的行,如果后来 Session A 再次读取同一个表,就会多出几行

事务隔离级别

READ UNCOMMITTED(读未提交):

  • 所有事务都可以看到其他未提交事物的执行结果, 不能避免脏读、不可重复读、幻读

READ COMMITTED(读已提交):

  • 一个事务只能看见已经提交的事务所做的改变,可以避免脏读,但不可重复读、幻读问题仍然存在

REPEATABLE READ(重复读):

  • 事务 A 在读到一条数据后,此时事务 B 对该数据进行了修改并提交,事务 A 在读该数据,读取到的还是原来的内容,可以避免脏读、不可重复读,但幻读问题仍然存在,且是 MySQL 默认隔离级别

SERIALIZABLE(可串行化):

  • 确保事务可以从一个表中读取相同的行
隔离级别 丢失更新 脏读 不可重复读 幻读
Read Uncommitted(RU) ×
Read Committed(RC) × ×
Repeatable Read(默认) × × ×
Serializable × × × ×

MySQL 默认隔离级别为什么是可重复读(RR)

MySQL 在 5.0 之前,binlog 只支持 STATEMENT 这中格式,而这种格式在读已提交(RC)这个隔离级别下主从复制是有 bug 的,因此 MySQL 将可重复读(RR)作为默认的隔离级别

STATEMENT 主从复制的 bug

  • 在 master 上执行的顺序为先删后插,而此时 binlog 为 STATEMENT 格式,ta 记录的顺序为先插后删
  • 从(slave)同步的是 binlog,如果从机执行的顺序 和主机不一致,就会出现主从不一致

隔离级别设为可重复读(RR),在该隔离级别下引入间隙锁

  • 当 Session A 执行 delete 语句时,会锁住间隙
  • 那么,Session B 执行插入语句就会阻塞住

为什么大家将隔离级别设为读已提交(RC)?

在 RR 隔离级别下,存在间隙锁,导致出现死锁的几率比 RC 大的多

在 RR 隔离级别下,条件为命中索引会锁表,而在 RC 隔离级别下,只锁行

在 RC 隔离级别下,半一致性读(semi-consistent)特性增加了 update 操作的并发性

半一致性读:一个 update 语句,如果读到一行已经加锁的记录,此时 InnoDB 返回记录最近提交的版本,有MySQL 上层判断此版本是否满足 update 的 where 条件

  • 若满足(需求更新),则 MySQL 会重新发作一次读操作,此时会读取行的最新版本(并加锁)

如下 Session A,Session B代码:

Sesssion A 执行:

update test set color = 'blue' where color = 'red';

先不 Commit 事务;

与此同时,Session B 执行:

update test set color = 'blue' where color = 'white';

Session B 尝试加锁的时候,发现已经存在锁,InnoDB 会开启 semi-consistent read,返回最新的 committed 版本(1,red),(2,white),(5,red),(7,white);

MySQL 会重新发起一次读操作,此时会读取行的最新版本(并加锁);

而在 RR 隔离级别下, Session B 只能等待

互联网项目推荐使用:读已提交(Read Commited)隔离级别

事务实现原理

原子性:使用 undo log(回滚日志),undo log 记录了回滚需要的信息

  • 当实物执行失败或调用了 rollback,导致事务需要回滚,便可以 undo log 中的信息将数据回滚到修改前的样子

隔离性:使用悲观锁和乐观锁对事务处理

持久性:使用 redo log (重写日志)

一致性:通过原子性、隔离性、持久性来保证一致性

MVCC

MVCC 是为了解决 读-写 之间阻塞的问题(排它锁会阻塞读操作),写操作还是需要加锁(Next-Key Lock);

如果没有 MVCC,那么修改数据的操作会加排它锁,其他的读写操作都会阻塞,这样的话效率会比较低。

MVCC 通过 Undo Log + Read View 进行数据读取

  • Undo Log 保存了历史快照
  • Read View 规则判断当前版本的数据是否可见

快照读与当前读

快照读:

快照读读取的是快照数据:

  • 不加锁的简单的 SELECT 都属于快照读,即不加锁的非阻塞读

快照读的实现是基于 MVCC,ta 在很多情况下,避免了加锁操作,降低了开销:

  • 既然是基于多版本,那么快照读读取到的并不一定是数据的最新版本,而有可能是之前的历史版本

快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读

select * from goods where ...

当前读:

读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁

加锁的 SELECT ,或者对数据进行增删改都会进行当前读

select * from goods lock in share mode; # 共享锁
select * from goods for update: # 排它锁
insert into goods values ... # 排它锁
delete from goods where ... # 排它锁
update goods set ... where ... # 排它锁

Undo Log 版本链

InnoDB 聚簇索引记录中包含 3 哥隐藏的列:

  • DB_ROW_ID(隐藏的自增 ID):
    如果表没有主键,InnoDB 会自动按 ROW ID 产生一个聚集索引树
  • DB_TRX_ID(事务 ID):
    记录最近更新这条记录的事务 ID,大小为 6 个字节
  • DB_ROLL_PTR(回滚指针):
    指向该行回滚段的指针,大小为 7 个字节;该行记录上所有旧版本,在 undo 中都通过连败哦的形式组织

每次对该记录进行改动,都会记录一条 undo 日志,每条 undo 日志都已一个 roll_pointer 属性

会将这些 undo 日志都连起来,串成一个链表,这个链表被称为版本链

  • 版本链的头节点,就是当前记录最新的值
  • 每个版本中包含生成该版本时对应的 事务 id

ReadView

ReadView 就是事务在使用 MVCC 机制进行快照读操作时产生的读视图;

判断版本链中的哪个版本是当前事务可见的,这是 ReadView 要解决的主要问题

ReadView 主要包含 4 个内容

  • creator_trx_id:创建这个 Read View 的事务 ID
  • trx_ids:在生成 ReadView 时当前系统中活跃的读写事务的事务 id 列表(活跃:启动了但还没提交)
  • up_limit_id:活跃的事务最小的事务 ID
  • low_limit_id:表示生成 ReadView 时系统中应该分配给下一个事务的 id 值

ReadView 规则

  • 若被访问版本 trx_id 属性值与 ReadView 中的 creator_trx_id 值相同:
    则当前事务在访问 ta 自己修改的记录,所以该版本 可以 被当前事务访问
  • 若被访问版本 trx_id 属性值 小于 ReadView 中的 up_limit_id 值:
    则生成该版本的事务与当前事务生成 ReadView 前已经提交,所以该版本 可以 被当前事务访问
  • 若被访问版本的 trx_id 属性值 大于或等于 ReadView 中的 low_limit_id 值:
    则生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本 不可以 被当前事务访问
  • 若访问版本的 trx_id 属性值在 ReadView 的 up_limit_idlow_limited 之间,
    需要判断一下 trx_id 属性值是不是在 trx_ids 列表:
    • 若在,则创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问
    • 若不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问

MVCC整体操作流程

首先 获取事务自己的版本号,也就是事务 ID;

获取 ReadView ;

查询到的数据,然后与 ReadView 中的事务版本号进行对比;

如果不符合 ReadView 规则,就需要从 Undo Log 中获取历史快照;

最后 返回不符合规则的数据;

如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,知道版本链中的最后一个版本;

如果最后一个版本也不可见的话,那么就意味着该条记录对该事务不完全不可见,查询结果就不包含该记录;

READ COMMITTED 事务,在每次查询开始时都会生成一个独立的 ReadView;

REPEATABLE READ 事务,只会在第一次执行查询语句时生成一个 ReadView,之后的查询就不会重复生成了;

MVCC 幻读被彻底解决了嘛

可重复读隔离级别(默认隔离级别),根据不同的查询方式,分别提出了避免幻读的方案:

  • 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读
  • 针对当前读(select … for update 等语句),是通过 next-key lockt(记录锁 + 间隙锁)方式解决幻读
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容