MySQL总结(五)

MySQL总结(五)事务ACID的底层实现原理

隔离性由锁实现

redo log 实现 持久性

undo log 实现一致性,原子性

1. Why undo log?

数据库和内存之间进行IO的时候,事务正在进行,如果数据库宕机,那这时候数据就会不一致,就要回滚到以前的数据,undo log就是实现这个功能的。

undo log 记录的是数据原来的值,所以只需要把对数据库的逆操作记录就行了

  • insert 一条记录,我们就delete
  • update 一条记录, 我们就把它以前的值记下来update
  • delete 一条记录,我们就insert

undo能实现啥?

  1. 原子性
  2. MVCC(多版本并发控制)后边会介绍

2. Buffer pool

当我修改数据库的记录时,会把这条记录先加载到缓存中,修改缓存的记录,所在的页为脏页(与磁盘的数据不一致),不会立刻写入磁盘,会等待一个时机写入磁盘。

读数据时也会先到缓存中找,没有的话会到磁盘中找。

3. Why Redo log?

上边说到,修改后的数据不会立刻写入磁盘,但内存级别是不安全的,万一停电了,数据就没了。事务要有持久性。

那可以直接把修改的数据更新到磁盘,不在缓冲池?

这样会增加大量的IO操作,而且还是随机IO

redo log file 是顺序写入的,是顺序IO

redo log file 记录的是 xx表空间xx页xx偏移量处修改为xx。

redo log 也不是直接写入磁盘,有一个redo log buffer 在一定时机下会写入磁盘

MySQL把对底层页面中的一次原子访问过程称之为一个Mini-Transaction,简称mtr,比如,向某个索引对应的B+树中插入一条记录的过程就是一个Mini-Transaction。一个所谓的mtr可以包含一组redo日志,在进行崩溃恢复时这一组redo日志可以作为一个不可分割的整体。

alt
alt
alt

写入磁盘

alt

一下情况会写入从redo log buffer 写入 redo log file

  • MySQL 正常关闭时;

  • 当 redo log buffer 中记录的写入量大于 redo log buffer 内存空间的一半时,会触发落盘;

  • InnoDB 的后台线程每隔 1 秒,将 redo log buffer 持久化到磁盘。

  • 每次事务提交时都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘(innodb_flush_log_at_trx_commit

    innodb_flush_log_at_trx_commit = 0 时,每次提交事务时,redo log 不会写入磁盘

    innodb_flush_log_at_trx_commit = 1 时,每次提交事务时,redo log 写入磁盘

    innodb_flush_log_at_trx_commit = 2 时,每次提交事务时,从redo log buffer 写入redo log,但是不写入磁盘

锁有表,页,行锁之分

读写锁

select column from table for select # 加共享锁
select column from table for update # 加排他锁

1. 表锁

  1. S锁和X锁
  • S锁是读锁,也叫共享锁
  • X锁是写锁,也叫排他锁

加锁

LOCK TABLES t READ  #InnoDB存储引擎会对表 t 加表级别的 `S锁 `。

LOCK TABLES t WRITE #InnoDB存储引擎会对表 t 加表级别的 `X锁` 。
alt

兼容性

SX
Syesno
Xnono
  1. 意向锁

    InnoDB 支持 多粒度锁(multiple granularity locking) ,它允许 行级锁表级锁 共存,而意向锁就是其中的一种 表锁

    1. 意向锁的存在是为了协调行锁和表锁的关系,支持多粒度(表锁和行锁)的锁并存。
    2. 意向锁是一种 不与行级锁冲突表级锁,这一点非常重要。
    3. 表明 某个事务正在某些行持有了锁或该事务准备去持有锁
    • 意向共享锁(intention shared lock, IS):事务有意向对表中的某些行加共享锁(S锁)

      -- 事务要获取某些行的 S 锁,必须先获得表的 IS 锁。
      SELECT column FROM table ... LOCK IN SHARE MODE;
      
    • 意向排他锁(intention exclusive lock, IX):事务有意向对表中的某些行加排他锁(X锁)

      -- 事务要获取某些行的 X 锁,必须先获得表的 IX 锁。
      SELECT column FROM table ... FOR UPDATE;
      

    即:意向锁是由存储引擎 自己维护的 ,用户无法手动操作意向锁,在为数据行加共享 / 排他锁之前, InooDB 会先获取该数据行 所在数据表的对应意向锁

    作用:

    如果有行锁了,另一个事务想加一个表锁。没有意向锁的话,就要看有没有页锁,有没有行锁。

    有意向锁的话,就立刻知道有锁,不要在加了。

    意向锁之间全部兼容

    ISIX
    ISYESYES
    IXYESYES

    意向锁和S以及X锁的兼容性

    ISIX
    SYESNO
    XNONO

    结论

    1. InnoDB 支持 多粒度锁 ,特定场景下,行级锁可以与表级锁共存。
    2. 意向锁之间互不排斥,但除了 IS 与 S 兼容外, 意向锁会与 共享锁 / 排他锁 互斥
    3. IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突。
    4. 意向锁在保证并发性的前提下,实现了 行锁和表锁共存满足事务隔离性 的要求。
  2. 自增锁

    对有 auto_increment的字段,三种insert

    • simple inserts

      预先知道要插入的行数

    • bulk inserts

      不知道插入的行数,一行一行的处理

    • mixed-mode inserts

      一部分有,一部分没有values(null, 'lang',20),(2,'laaang',23);

      这时候是混合模式

  3. 元数据锁

    当对一个表做增删改查操作的时候,加 MDL读锁;当要对表做结构变更操作的时候,加 MDL 写锁

    读锁之间不互斥,因此你可以有多个线程同时对一张表增删查改。读写锁之间、写锁之间都是互斥的,用来保证变更表结构操作的安全性,解决了DML和DDL操作之间的一致性问题。不需要显式使用,在访问一个表的时候会被自动加上。

2. 行锁

  1. 记录锁(Record locks)

    事务A

    mysql> select * from stu;
    +----+------+------+
    | id | name | age  |
    +----+------+------+
    |  1 | lang |    1 |
    |  2 | he   |   18 |
    |  3 | zhao |   18 |
    +----+------+------+
    3 rows in set (0.01 sec)
    
    mysql> set autocommit = 0;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> update stu set name = 'laaang' where id = 2;
    Query OK, 1 row affected (0.03 sec)
    Rows matched: 1  Changed: 1  Warnings: 0
    

    事务B

    mysql> select * from stu;
    +----+------+------+
    | id | name | age  |
    +----+------+------+
    |  1 | lang |    1 |
    |  2 | he   |   18 |
    |  3 | zhao |   18 |
    +----+------+------+
    3 rows in set (0.01 sec)
    
    mysql> set autocommit = 0;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> update stu set name = 'Zhao' where id = 3;
    Query OK, 1 row affected (0.00 sec)
    Rows matched: 1  Changed: 1  Warnings: 0
    
    mysql> update stu set name = 'lang' where id = 2;
    # 阻塞
    

    记录锁是有S锁和X锁之分的,称之为 S型记录锁X型记录锁

    • 当一个事务获取了一条记录的S型记录锁后,其他事务也可以继续获取该记录的S型记录锁,但不可以继续获取X型记录锁;
    • 当一个事务获取了一条记录的X型记录锁后,其他事务既不可以继续获取该记录的S型记录锁,也不可以继续获取X型记录锁。
  2. 间隙锁(Gap Locks)

    mysql> select * from stu;
    +----+--------+------+
    | id | name   | age  |
    +----+--------+------+
    |  1 | lang   |    1 |
    |  2 | laaang |   18 |
    |  3 | zhao   |   18 |
    |  5 | lang   |   22 |
    +----+--------+------+
    4 rows in set (0.00 sec)
    

    在这个表中,如果给id = 5 的记录加上间隙锁,在(3,5)之家是加不上数据的。

    加锁

    select * from stu where id = 5 lock in share mode;
    
    select * from stu where id = 5 for update;
    

    这样加的是在一个区间(3,5)那再后边的区间不让insert咋办

    • Infimum记录,表示该页面中最小的记录。
    • Supremum记录,表示该页面中最大的记录。

    在supermum的记录上加锁,就是(,+∞)不可以insert

  3. 临键锁(next-key locks)

    next-key锁的本质就是一个记录锁和一个gap锁的合体,它既能保护该条记录,又能阻止别的事务将新记录插入被保护记录前边的间隙

    begin;
    select * from stu where id <=8 and id > 3 for update;
    
  4. 插入意向锁(insert intention locks)

alt

悲观锁(Pessimistic Locking)

悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 阻塞 直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞, 用完后再把资源转让给其它线程)。比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,当其他线程想要访问数据时,都需要阻塞挂起。Java中 synchronizedReentrantLock 等独占锁就是悲观锁思想的实现。

乐观锁(Optimistic Locking)

乐观锁认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,也就是不采用数据库自身的锁机制,而是通过程序来实现。在程序上,我们可以采用 版本号机制 或者 CAS机制 实现。乐观锁适用于多读的应用类型, 这样可以提高吞吐量

  • 版本号实现

    加入version字段

    # 当修改字段时,判断一下版本号,所以现需要查一下version
    select version from table
    UPDATE ... SET version=version+1 WHERE version=version;
    
    
  1. 乐观锁 适合 读操作多 的场景,相对来说写的操作比较少。它的优点在于 程序实现不存在死锁 问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。
  2. 悲观锁 适合 写操作多 的场景,因为写的操作具有 排它性 。采用悲观锁的方式,可以在数据库层 面阻止其他事务对该数据的操作权限,防止 读 - 写写 - 写 的冲突。

死锁

两个事务都有锁,并且需要对方先释放锁;

MVCC

Multiversion Concurrency Control 多版本并发控制。MVCC 是通过数据行的多个版本管理来实现数据库的 并发控制 。这项技术使得在InnoDB的事务隔离级别下执行 一致性读 操作有了保证。换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值,这样在做查询的时候就不用等待另一个事务释放锁。

  1. 快照读
  2. 当前读

ReadView

用到了undo

字段:

  1. creator_trx_id:创建这个 Read View 的事务 ID。

    只有修改操作才会分配这个属性。

  2. trx_ids:事务id列表

  3. up_limit_id:列表最小的事务id

  4. low_limit_id:表示生成ReadView时系统中应该分配给下一个事务的 id 值,也就是以前有过的最大的id+1;

    如果列表有1, 2, 3那么此字段就是不一定时4,因为4事务可能是已经有了,但是提交过了,这个字段必须是列表没有出现过的,下一次将要进来的。

readview的操作流程

当前事务id用trx_id表示

  • trx_id = creator_trx_id

    是当前的事务,(当前事务访问他自己修改的数据)可以访问

  • trx_id < up_limit_id

    已经提交的事务,可以访问

  • trx_id >= low_limit_id

    当前readview创建的事务,不可以访问

  • trx_id 属于up_limit_idlow_limit_id之间

    在 :列表中,活跃事务,不可以访问。

    不在:列表中,已经提交的事务,可以访问。

Example

  1. READ COMMITTED

    READ COMMITTED每一次SELECT 都会有新的raedview

    注意,此时同样的查询语句都会重新获取一次 Read View,这时如果 Read View 不同,就可能产生不可重复读或者幻读的情况。

  2. REPEATABLE READ

    只生成一个readview

总结

  • READ COMMITTD 在每一次进行普通SELECT操作前都会生成一个ReadView
  • REPEATABLE READ 只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复 使用这个ReadView就好了。

本文由 mdnice 多平台发布

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值