Skip to content

Latest commit

 

History

History
280 lines (175 loc) · 10.8 KB

数据库——3_锁管理.md

File metadata and controls

280 lines (175 loc) · 10.8 KB

3. 锁管理

锁管理

1.提出问题。

1.1 在研究索引管理之前,我们当然也先提出几个问题

  • MyISAM 与 InnoDB 关于锁方面的区别是什么?

  • 数据库的四大特性?

  • 事务隔离级别以及各级别下的并发访问问题?

  • InnoDB可重复读隔离级别下如何避免幻读?

  • RC、RR级别下的InnoDB的非阻塞度如何实现?

2. 寻找答案

2.1 MyISAM 与 InnoDB 关于锁方面的区别是什么?

  • MyISAM 默认用的是表级锁,不支持行级锁

  • InnoDB 默认用的是行级锁,也支持表级锁

锁级别:

read锁 又可叫共享锁

write锁 又可叫排它锁

IS 共享读锁 IX 排他写锁

共享锁和排斥锁的兼容性

- X S
X 冲 突 冲 突
S 冲 突 兼 容

2.2 适合的场景

2.2.1 MyISAM 适合的场景

  • 频繁执行全部 count 语句

  • 对数据进行增删改的频率不高,查询非常频繁

  • 没有事务

2.2.2 InnoDB 适合的场景

  • 数据增删改查都相当频繁

  • 可靠性要求比较高,要求支持事务

2.3 数据库锁的分类

  • 按锁的粒度划分,可分为表级锁、行级锁、页级锁

  • 按锁的级别划分,可分为共享锁、排它锁

  • 按加锁的方式划分,可分为自动锁、显示锁

  • 按操作划分,可分为 DML 锁、DDL 锁

  • 按使用方式划分,可分为 乐观锁、悲观锁

3. 数据库的四大特性?

ACID

  • 原子性(Atomic)

  • 一致性(Consistency)

  • 隔离性(Isolation)

  • 持久性(Durability)

3.1 事务隔离级别以及各级别下的并发访问问题

3.1.1 事务并发访问引起的问题以及如何避免

  • 更新丢失—— mysql 所有事务隔离级别在数据库层面上均可避免

  • 脏读—— READ-COMMITTED 事务隔离级别以上可避免

  • 不可重复读—— REPEATABLE-READ 事务隔离级别以上可避免

  • 幻读—— SERIALIZABLE 事务隔离级别可避免

事务隔离级别 更新丢失 脏读 不可重复读 幻读
未提交读 避免 发生 发生 发生
已提交读 避免 避免 发生 发生
可重复读 避免 避免 避免 发生
串行化 避免 避免 避免 避免

注:

  • oracle 默认隔离级别:read-commited
  • mysql 默认隔离级别:repeatable-read

4. 锁模块之 当前读 和 快照读

4.1 InnoDB 可重复读 隔离级别下如何避免幻读

  • 表象:快照度(非阻塞读)—— 为 MVCC

  • 内在:next-key 锁(行锁+gap锁)

4.2 当前读和快照读

  • 当前读:select...lock in share mode , select...for update

  • 当前读:update , delete , insert

  • 快照读:不加锁的非阻塞读,select

Alt text

    • RC 级别下
      • 快照读 和 当前读 读到的数据版本是一样的 (新版本)
    • RR 级别下
      • 快照读
        • 有可能读到的是历史版本
        • 也有可能读到最新版本(事务首次调用快照读的地方很关键。创建快照的时机,决定读取数据的版本)
          • 开启 事务1后,先不执行 select
          • 等 事务2 提交后;再 select 获取的就是最新数据
        • 还是有可能具备不让我们看到别的事务新增的**行的能力
      • 当前读 读到的是最新版本

4.3 RR、RC 级别下的 InnoDB 的非阻塞读如何实现

  • 每行数据行里的 DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID 字段

    • DB_TRX_ID 最后一次对本行记录做 UPDATE 的 事务标识符
      • DELETE 也是 UPDATE。有个 delete 隐藏列,会标识
    • DB_ROLL_PTR 回滚指针
      • 指写入回滚段(rollback segment) 的 undo 日志记录
      • 如果一行记录被更新,则 undo log record 包含 重建该行记录,被更新记录之前内容所必需的信息
    • DB_ROW_ID 行号
      • 包含随着新行插入,单调递增的行ID (越新开启的事务ID,越大)
      • 当有 Innodb 自动生成 聚集索引,聚集索引会包含 DB_ROW_ID;否则 DB_ROW_ID 不会出现在任何索引中
      • 如果 Innodb 中的表,既没有 主键 也 唯一键,Innodb 会自动隐式创建一个 自增的 隐藏主键字段,既这里的 DB_ROW_ID
  • undo 日志

    • 当对数据进行变更操作时,就会产生 undo log 记录
    • undo 中存储的是老版数据
      • 当一个旧的事务,需要读取数据时,为了能读取老版本数据,需要顺着 undo 链,找到满足其可见性的记录
    • undo log 分为两种
      • insert undo log
        • 表示 insert 新记录产生的 undo log
        • 只在事务回滚时需要,并且在事务提交后,就可以立即丢弃
      • update undo log
        • 事务在 delete 和 update 时产生的 undo log
        • 此类 undo log,不仅在事务回滚时需要,快照读也需要,所以不能随便删除
        • 只有当数据库所使用的快照中,不涉及该日志记录,对应的回滚日志,才会被 purge 线程删除

Alt text

事务对行记录的更新过程

  • 被事务 a 修改,将 Field2 由 12-> 32

    • 首先,用排它锁,锁定该行
    • 随后,把该行修改前的值 copy 一份到 undo log 中
    • 之后,修改当前行的值(Field2),填写事务id(DB_TRX_ID),使用回滚指针(DB_ROLL_PTR)指向 undo log 中被修改前的行
    • 在这之后,假设数据库还有别的事务,再用 快照读 来读取该日志记录
      • 那么对应的 undo log 还没有被清除,此时某个事务又对同一行数据做了修改
      • 那么和前面一样,又多了一行 undo log,它们通过 DB_ROLL_PTR 给连接起来
  • read view

    • 主要用来做可见性判断
      • 即当一个事务去做 快照读 select 的时候,会针对 查询的数据,创建出一个 read view
      • read view 来决定当前的事务能看到的是哪个版本的数据
        • 有可能是当前最新版本的数据
        • 也有可能只允许看 undo log 里,某个版本的数据
    • read view 遵循一个可见性算法
      • 主要是将要修改的数据的 DB_TRX_ID 取出来,与系统其他活跃事务ID(DB_TRX_ID) 做对比
      • 如果 大于或等于 这些 DB_TRX_ID, 就通过 DB_ROLL_PTR 取出 undo log
      • 上一层的 DB_TRX_ID,直到小于这些活跃事务ID(DB_TRX_ID)为止,这样就保证了,事务所获取的数据版本,是当前可见的最稳定的版本

MySQL部分源码

Alt text

涉及主要变量

Class ReadView: 

...

private: 
    /** the read should not see any transaction with trx id >= this value.
     In other words, this is the "high water mark". */
    trx_id_t m_low_limit_id  // 活动事务的最大id  
    /** The read should see all trx ids which are strictly smaller (<) than this value. 
    In other words, this is the "low water mark". */
    trx_id_t m_up_limit_id   // 活动事务的最小id   
  • 主要通过 m_low_limit_idm_up_limit_id 与 当前事务的 DB_TRX_ID 做对比,决定是否回溯到 undo log,去取出适应该版本的数据版本。

注:每当 start 一个 Transaction 时,事务ID 会递增。即:越新开启的事务ID,越大

  • 正因为生成的时机不同,造成了 RC、RR 两种隔离级的不同可见性

    • 在 repeatable read 级别下
      • 在 start transaction 后的第一条快照读,会创建一个 快照,即 read view
      • 将当前系统中活跃的其他事务,记录起来
      • 此后在调用快照读时,还是使用 同一个 read view
    • 在 repeatable committed 级别下
      • 事务每条 select 语句,即每次调用快照读,都会创建一个新的快照
      • 这就是为什么 RC 级别下,能用快照读 看到别的事务,已提交的对表记录的增、删、改
  • 而在 RR 下,如果首次使用快照读,是在别的事务的数据进行增删改并提交之前的,此后即便别的事务对数据做了增删改并提交,还是读不到数据变更的原因,对于 RR 来说,首次 事务 Select 的时机相当重要

  • 正因为上面三个因素,使得 innodb 在 RR 或者 RC 级别下,支持非阻塞读。而读取事务时的非阻塞就是所谓的 MVCC

  • 而 innodb 的非阻塞机制,实现了仿照版的 MVCC(多版本并发控制) 读不加锁,读写无冲突;在读多写少的

  • 思考

    • OLTP 应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能,为什么 innodb 仅实现了 伪 MVCC 机制?
      • 因为 innodb,并没有实现核心的多版本共存,undo log 的内容只是串行化的结果
      • 记录了多个多个事务的过程,不属于多版本共存

5. 锁模块之 RR 如何避免幻读

5.1 next-key 锁(行锁+gap锁)

  • 行锁

    • 对单个行的锁
  • Gap 锁(Gap Locks

    • 间隙锁,索引树中插入新纪录的空隙,但不包括记录本身
    • 主要避免同一事务的两次当前读,出现幻读的情况
    • RR 和 串行化 默认支持 gap锁
  • 以下对 RR 下,gap锁 出现的场景

5.2 对主键索引或者唯一索引是否会用 Gap 锁

  • 如果 where 条件全部命中,则不会使用 Gap 锁,只会加 记录锁(Record Locks

Alt Text

  • 如何 where 条件部分命中或者全不命中,则会加 Gap锁

Alt Text

Alt Text

**锁了 (6,11],间隙锁范围还和主键的字母序大小有关 锁了 'c',6,插入 'bb',6 成功;而 'dd', 6 失败 **

Alt Text

Alt Text

TODO

感谢

xiangzepro

MySQL 官方文档