首先我们先介绍一下锁的分类,再进入今天的正题。

一、锁分类:

  • 1.从性能上分:乐观锁、悲观锁。乐观锁(用版本号对比或CAS机制)适用于读比较多的场景,悲观锁适用于写比较多的场景。如果在写比较多的场景使用乐观锁,会导致对比次数过多,影响性能。

  • 2.从对数据的粒度上分:表锁、页锁、行锁。

  • 3.从对数据库的操作上分:读锁(悲观锁)、写锁(悲观锁)、意向锁。

    • 读锁(共享锁,S锁(shared)):针对同一条数据加了读锁之后,其他读操作可以同时进行,不受影响。

      比如:select * from A where id = 1 lock in share mode.
    • 写锁(排他锁,X锁(exclusive)):针对同一条数据加了写锁之后,其他的事务不能写和读。insert、update、delete会加写锁,查询可以通过加for update加写锁。

      比如:select * from A where id = 1 for update.
    • 意向锁:又称I锁,表锁。主要是为了提高加表锁的效率,mysql提供的。
  • 间隙锁和临键锁:

    间隙锁的目的主要是为了防止幻读,以满足相关隔离级别的要求。间隙锁主要通过两个方面实现这个目的:一是防止间隙内有新数据被插入,二是防止已存在的数据被更新成间隙内的数据。
    
    1.间隙锁:锁的是两个值之间的间隙,在可重复读隔离级别下生效,mysql默认是RR级别,RR级别下有幻读问题,间隙锁就是为了解决幻读问题出现的。
    2.临键锁:行锁和间隙锁的组合。

    注意:关于RR级别行锁升级为表锁的原因:

    在RR级别下,需要解决不可重复读和幻读问题。在遍历扫描聚集索引记录时,为了防止扫描过的索引被其他事务修改(不可重复读问题) 或 间隙被其他事务插入记录(幻读问题),从而导致数据不一致,索引mysql的解决方案就是把所有扫描过的索引记录和间隙都锁上,这里并不是直接将整张表加表锁,因为不一定能加上表锁,可能会有其他的事务锁住表里的其他记录。

二、MVCC多版本并发控制:

首先先介绍几个相关的概念:

  • 1.mvcc定义和核心思想:

    定义:mvcc是一种并发控制机制,用来控制并发执行的事务,保证事务之间的隔离性。
    核心思想:mvcc是通过保存某个时间点的数据生成快照版本来定义的。mvcc允许同一条记录有不同的快照版本,不同事务在查询时通过添加一些约束条件,就可以得到对于某个时刻快照版本的数据。
    mysql在读已提交和可重复读隔离级别下都实现了mvcc机制。
  • 2.快照读和当前读:

    1.快照读:基于mvcc机制和undo log实现的,适用于简单的select语句。
    2.当前读:基于临键锁(行锁+间隙锁)实现的,适用于update、insert、delete、select...for update、select...lock in share mode及加锁的select语句。
  • 3.undo日志版本链:

    undo日志版本链是指一行数据被每个事务依次修改过后,在每个事务修改完后,会保留修改前的数据到undo log日志中,通过trx_id和roll_pointer字段将undo log日志串连成一个历史记录日志版本链。
  • 4.MVCC版本对比规则:

    在可重复读隔离级别下,当事务开启时,任何查询的sql在第一次select时都会生成一致性视图read-view,并且这个视图在事务结束前都不会改变。这个视图是由所有未提交的事务组成的事务组,事务组里面的事务查询sql都需要都对应记录版本链的最新记录逐条和read-view做对比,最终得到想要的快照结果。
    
    注:在读已提交的隔离级别下,每次select查询都会生成一个一致性视图。
    
    a.create_trx_id : 当前事务id
    b.trxs组:当前所有未提交事务
    c.min_trx_id: 最小未提交事务id
    d.max_trx_id:最大未提交事务id
    # 版本对比规则:
    1.当trx_id = create_trx_id可见。
    2.当trx_id < min_trx_id,表示这个版本由已提交事务生成的,可见。
    3.当trx_id > max_trx_id,表示这个版本是由将来的事务生成的,可见。
    4.当min<= trx_id <= max_trx_id,分两种情况:
    a.当trx_id在这个trxs组内,说明这个版本是未提交的事务,不可见。
    b.当trx_id不再这个trxs组内,说明这个版本是由已提交事务生成的,可见。

三、幻读问题的解决:

首先回顾一下事务的四大特性和事务的隔离级别,以及不同的隔离级别会出现什么样的问题。

事务的四大特性:原子性、一致性、隔离性、持久性。
事务的隔离级别:读未提交、读已提交、不可重复读、串行化。 读未提交:会出现脏读(当前内存读),不可重复读,幻读问题。
读已提交:不会出现脏读问题,但会出现不可重复读,幻读问题。
不可重复读:不会出现脏读、不可重复读问题,但会出现幻读问题(mvcc机制和锁可以彻底解决这个问题)。
串行化:串行化读取数据,但是事务的并发度就没有了。 * 脏读:读取到其他事务未提交的数据
* 不可重复读问题:指的是在同一个事务中,多次查询同一条数据(已经存在的数据),由于其他事务的修改,导致查询的结果不一样。
* 幻读:指的是在一个事务中,查询一个范围内的数据,一般是count,多次返回的数量不一样,查询到其他事务新插入的数据。
  • 结论:先说一下结论,在RR隔离级别下,幻读问题可以通过mvcc机制和间隙锁或临键锁解决(必需让查询语句使用当前读,不能使用快照读)。

    下面举了一些示例:

    CREATE TABLE `gap_test` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
    `number` int(11) NOT NULL COMMENT '数字',
    PRIMARY KEY (`id`) USING BTREE,
    KEY `index_number` (`number`)
    ) ENGINE=InnoDB AUTO_INCREMENT=31 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='间隙锁表'

  • 1.成功解决幻读的问题示例:

    #间隙锁示例:(间隙锁之间是兼容的,高并发的场景下,不正确的使用可能会导致死锁)
    session 1:
    begin;
    select * from gap_test where number = 8 from update; // 记录不存在,执行成功 session 2:
    begin;
    select * from gap_test where number = 8 from update; // 记录不存在,执行成功
    insert into gap_test value (11,11);//阻塞
    insert into gap_test value (7,5);//阻塞
    insert into gap_test value (2,5);//执行成功
    insert into gap_test value (14,11);//执行成功 这时会对number=8进行检索,检索不到记录,会向左取最近的值number=5作为左区间,向右取最近的值number=11作为右区间,所以session 1锁的间隙范围为:number(5,11)。即在(id=6,number=5)和(id=13,number=11)这个间隙范围内不能插入任何数据。 #临键锁示例:
    session 1:
    begin;
    select * from gap_test where number = 5 from update;//记录存在,执行成功(会加写锁) session 2:
    begin;
    select * from gap_test where number = 5 from update;//阻塞(写锁互斥)
    insert into gap_test value (4,4);//阻塞
    insert into gap_test value (4,5);//阻塞
    insert into gap_test value (8,8);//阻塞
    insert into gap_test value (11,11);//阻塞
    insert into gap_test value (2,4);//执行成功
    insert into gap_test value (14,11);//执行成功 这时会对number = 5进行检索,检索到记录之后,会对number =5的记录加写锁,然后向左取最近的值number=4作为左区间,向右取最近的值number=11作为右区间,所以session 1锁的间隙范围为:number(4,5),number(5,11)。即在(id=3,number=4)和(id=6,number=5)、(id=6,number=5)和(id=13,number=11)之间的间隙范围内不能插入任何数据。
  • 2.没有解决幻读问题的示例:



    • 上面的示例产生了幻读问题。事务A和事务B同时执行,事务A修改了事务B的新提交的记录,再查询时查到上次没有查到的记录,产生了幻读。要彻底解决幻读问题,查询语句需要加锁,由快照读变为当前读。
  • 3.高并发的场景下,不正确的使用可能会导致死锁示例:



    • 事务A和事务B同时开启事务执行查询语句,这个间隙锁的范围是(30, +oo),事务B先在间隙锁范围内插入一条语句,事务A也在间隙锁范围内插入一条语句,然后发生了死锁。
    • 我们执行一下SHOW ENGINE INNODB STATUS语句查看死锁日志:

    =====================================
    2024-01-12 16:42:35 4af4 INNODB MONITOR OUTPUT
    =====================================
    Per second averages calculated from the last 34 seconds
    -----------------
    BACKGROUND THREAD
    -----------------
    srv_master_thread loops: 661 srv_active, 0 srv_shutdown, 239375 srv_idle
    srv_master_thread log flush and writes: 240027
    ----------
    SEMAPHORES
    ----------
    OS WAIT ARRAY INFO: reservation count 156
    OS WAIT ARRAY INFO: signal count 155
    Mutex spin waits 228, rounds 1256, OS waits 27
    RW-shared spins 125, rounds 3674, OS waits 121
    RW-excl spins 4, rounds 264, OS waits 4
    Spin rounds per wait: 5.51 mutex, 29.39 RW-shared, 66.00 RW-excl
    ------------------------
    ### 发生死锁
    LATEST DETECTED DEADLOCK
    ------------------------
    2024-01-12 16:40:30 48e8
    *** (1) TRANSACTION:
    TRANSACTION 1488272, ACTIVE 67 sec inserting
    mysql tables in use 1, locked 1
    LOCK WAIT 3 lock struct(s), heap size 360, 2 row lock(s), undo log entries 1
    MySQL thread id 39, OS thread handle 0x5c68, query id 2627 ::1 root update
    insert into gap_test value (40,36)
    *** (1) WAITING FOR THIS LOCK TO BE GRANTED:
    RECORD LOCKS space id 2067 page no 4 n bits 88 index `index_number` of table `test`.`gap_test` trx id 1488272 lock_mode X insert intention waiting
    Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
    0: len 8; hex 73757072656d756d; asc supremum;; *** (2) TRANSACTION:
    TRANSACTION 1488273, ACTIVE 48 sec inserting
    mysql tables in use 1, locked 1
    3 lock struct(s), heap size 360, 2 row lock(s), undo log entries 1
    MySQL thread id 38, OS thread handle 0x48e8, query id 2628 ::1 root update
    insert into gap_test value (41,37)
    *** (2) HOLDS THE LOCK(S):
    RECORD LOCKS space id 2067 page no 4 n bits 88 index `index_number` of table `test`.`gap_test` trx id 1488273 lock_mode X
    Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
    0: len 8; hex 73757072656d756d; asc supremum;; *** (2) WAITING FOR THIS LOCK TO BE GRANTED:
    RECORD LOCKS space id 2067 page no 4 n bits 88 index `index_number` of table `test`.`gap_test` trx id 1488273 lock_mode X insert intention waiting
    Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
    0: len 8; hex 73757072656d756d; asc supremum;; *** WE ROLL BACK TRANSACTION (2)
    ------------
    TRANSACTIONS
    ------------
    Trx id counter 1488278
    Purge done for trx's n:o < 1488278 undo n:o < 0 state: running but idle
    History list length 583
    LIST OF TRANSACTIONS FOR EACH SESSION:
    ---TRANSACTION 0, not started
    MySQL thread id 41, OS thread handle 0x4ac8, query id 2577 ::1 root cleaning up
    ---TRANSACTION 1488267, not started
    MySQL thread id 40, OS thread handle 0x4af4, query id 2629 ::1 root init
    SHOW ENGINE INNODB STATUS
    ---TRANSACTION 1488273, not started
    MySQL thread id 38, OS thread handle 0x48e8, query id 2628 ::1 root cleaning up
    ---TRANSACTION 1488272, ACTIVE 192 sec
    4 lock struct(s), heap size 1184, 3 row lock(s), undo log entries 1
    MySQL thread id 39, OS thread handle 0x5c68, query id 2627 ::1 root cleaning up
    --------
    FILE I/O
    --------
    I/O thread 0 state: wait Windows aio (insert buffer thread)
    I/O thread 1 state: wait Windows aio (log thread)
    I/O thread 2 state: wait Windows aio (read thread)
    I/O thread 3 state: wait Windows aio (read thread)
    I/O thread 4 state: wait Windows aio (read thread)
    I/O thread 5 state: wait Windows aio (read thread)
    I/O thread 6 state: wait Windows aio (write thread)
    I/O thread 7 state: wait Windows aio (write thread)
    I/O thread 8 state: wait Windows aio (write thread)
    I/O thread 9 state: wait Windows aio (write thread)
    Pending normal aio reads: 0 [0, 0, 0, 0] , aio writes: 0 [0, 0, 0, 0] ,
    ibuf aio reads: 0, log i/o's: 0, sync i/o's: 0
    Pending flushes (fsync) log: 0; buffer pool: 0
    3071 OS file reads, 882 OS file writes, 559 OS fsyncs
    0.00 reads/s, 0 avg bytes/read, 0.00 writes/s, 0.00 fsyncs/s
    -------------------------------------
    INSERT BUFFER AND ADAPTIVE HASH INDEX
    -------------------------------------
    Ibuf: size 1, free list len 0, seg size 2, 0 merges
    merged operations:
    insert 0, delete mark 0, delete 0
    discarded operations:
    insert 0, delete mark 0, delete 0
    Hash table size 276707, node heap has 8 buffer(s)
    0.00 hash searches/s, 0.00 non-hash searches/s
    ---
    LOG
    ---
    Log sequence number 1239834765
    Log flushed up to 1239834765
    Pages flushed up to 1239834765
    Last checkpoint at 1239834765
    0 pending log writes, 0 pending chkp writes
    251 log i/o's done, 0.00 log i/o's/second
    ----------------------
    BUFFER POOL AND MEMORY
    ----------------------
    Total memory allocated 137428992; in additional pool allocated 0
    Dictionary memory allocated 4504559
    Buffer pool size 8192
    Free buffers 5820
    Database pages 2364
    Old database pages 888
    Modified db pages 0
    Pending reads 0
    Pending writes: LRU 0, flush list 0, single page 0
    Pages made young 0, not young 0
    0.00 youngs/s, 0.00 non-youngs/s
    Pages read 2339, created 25, written 525
    0.00 reads/s, 0.00 creates/s, 0.00 writes/s
    No buffer pool page gets since the last printout
    Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
    LRU len: 2364, unzip_LRU len: 0
    I/O sum[0]:cur[0], unzip sum[0]:cur[0]
    --------------
    ROW OPERATIONS
    --------------
    0 queries inside InnoDB, 0 queries in queue
    0 read views open inside InnoDB
    Main thread id 7344, state: sleeping
    Number of rows inserted 34, updated 25, deleted 0, read 3991433
    0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s
    ----------------------------
    END OF INNODB MONITOR OUTPUT
    ============================

后面一章,我们会着重分析一下mysql在RR隔离级别下的加锁过程,敬请期待!

MVCC多版本并发控制和幻读问题的解决的更多相关文章

  1. InnoDB学习(五)之MVCC多版本并发控制

    MVCC多版本并发控制,是一种数据库管理系统并发控制的方法.MVCC多版本并发控制下,数据库中的数据会有多个版本,分别对应不同的事务,从而达到事务之间并发数据的隔离.MVCC最大的优势是读不加锁,读写 ...

  2. MVCC多版本并发控制

    MVCC多版本并发控制 爱情小傻蛋关注 82019.09.28 23:23:37字数 4,740阅读 91,421 前提概要 什么是MVCC 什么是当前读和快照读? 当前读,快照读和MVCC的关系 M ...

  3. mysql的MVCC多版本并发控制机制

    MVCC多版本并发控制机制 全英文名:Multi-Version Concurrency Control MVCC不会通过加锁互斥来保证隔离性,避免频繁的加锁互斥. 而在串行化隔离级别为了保证较高的隔 ...

  4. 【Mysql】深入理解 MVCC 多版本并发控制

    MVCC MVCC(Multi-Version Concurrency Control),即多版本并发控制.是 innodb 实现事务并发与回滚的重要功能.锁机制可以控制并发操作,但是其系统开销较大, ...

  5. MySQL MVCC(多版本并发控制)

    概述 为了提高并发MySQL加入了多版本并发控制,它把旧版本记录保存在了共享表空间(undolog),当事务提交之后将重做日志写入磁盘(前提innodb_flush_log_at_trx_commit ...

  6. MVCC多版本并发控制的理解

    前置知识 当前读与快照读 当前读 什么是当前读:读取的是最新的数据,不会读到老数据. 何时触发:update.insert.delete.select lock in share mode.selec ...

  7. [MySQL] MVCC 多版本并发控制实现的事务

    1.没有一个统一的实现标准,实现了非阻塞的读操作,写操作也只锁定必要的行2.通过保存数据在某个时间点的快照实现的3.典型的有乐观并发控制和悲观并发控制4.innodb的mvcc是每次事务都有递增的版本 ...

  8. 聊聊MVCC多版本并发控制

    一.介绍 MVCC只在RR和RC 2个隔离级别下才能工作.MySQL的大多数事务存储引擎实现的都不是简单的行级锁机制.基于提升并发性能的考虑,它们一般都同时实现了MVCC. 通俗的来讲,MVCC是行级 ...

  9. 针对MySQL的MVCC多版本并发控制的一些总结

    MVCC MVCC细节太多,我直接备忘一下总结: MVCC就是通过事务的ID与行数据的版本(修改事务的ID)进行比较(通过redo log可以回溯版本)得出哪些版本的行数据可见和不可见而实现的事务隔离 ...

  10. MVCC 多版本并发控制

    关于事务的介绍暂且不谈. InnoDB行级锁,虽然在很大程度上提高了事务的并发性,但是终究还是要耗费很大的.为了更进一步的提高并发性同时降低开销,存储引擎会同时实现MVCC. InnoDB实现MVCC ...

随机推荐

  1. 两款轻便且功能强大的gif截取工具 [ScreenToGif] 和 [GifCam]

    轻便且强大 提示 下述工具下载链接为官方或github地址,可能会由于你懂得的原因,而无法打开. 一.ScreenToGif 软件简介: ScreenToGif 也是一款非常轻便的.完全免费的.没广告 ...

  2. 推荐一个 AI 绘图工具!将草图变成精美的图片!

    大家好,我是 Java陈序员. 要说 2023 年科技圈什么最火,当属 ChatGPT!自从 ChatGPT 爆火之后,各种 AI 工具层出不穷.AI 对话.AI 写文案.AI 写代码..... 今天 ...

  3. Educational Codeforces Round 102 (Rated for Div. 2) A~D题

    写在前边 链接:Educational Codeforces Round 102 (Rated for Div. 2) 第一场打CF,过了AB两题,不过速度挺慢,C题属实没看懂,D题写了个常规做法之后 ...

  4. 在ASP.NET Core 中使用 .NET Aspire 消息传递组件

    前言 云原生应用程序通常需要可扩展的消息传递解决方案,以提供消息队列.主题和订阅等功能..NET Aspire 组件简化了连接到各种消息传递提供程序(例如 Azure 服务总线)的过程.在本教程中,小 ...

  5. Java 21 官方速览:全面拥抱虚拟线程

    前言 首先,感谢一下不少xdm私信关心我的身体状况,我也不是什么厉害的大佬,点开通知看到一堆私信还是蛮感动的. 近来有意大幅缩短了更新频率,增加了日常调养身体的时间,淋巴结确实变小了,睡眠也逐渐正常, ...

  6. IDEA编译器的永久试用设置与基本使用

    参考视频: 最通俗易懂的JDK.IDEA的安装使用权威指南 2023新版前端Web开发HTML5+CSS3+移动web视频教程,前端web入门首选黑马程序员 一.安装包下载与安装 官网下载地址 个人使 ...

  7. DataGridView合并单元格,重绘后选中内容被覆盖的解决办法

    DataGridView合并单元格只能进行重绘,网上基本上使用的是下面的方法: 1 /// <summary> 2 /// 说明:纵向合并单元格 3 /// 参数:dgv DataGrid ...

  8. 2023振兴杯-Crypto wp

    crypto1 题目 from flag import flag def encrypt(x, y): key='zxb' result='' for i in range(len(x)): resu ...

  9. .NET中如何实现高精度定时器

    .NET中有多少种定时器一文介绍过.NET中至少有6种定时器,但精度都不是特别高,一般在15ms~55ms之间.在一些特殊场景,可能需要高精度的定时器,这就需要我们自己实现了.本文将讨论高精度定时器实 ...

  10. 36. 干货系列从零用Rust编写负载均衡及代理,内网穿透中内网代理的实现

    wmproxy wmproxy已用Rust实现http/https代理, socks5代理, 反向代理, 静态文件服务器,四层TCP/UDP转发,七层负载均衡,内网穿透,后续将实现websocket代 ...