5

MVCC多版本并发控制和幻读问题的解决 - 天~若比邻

 8 months ago
source link: https://www.cnblogs.com/ruobilin/p/17963507
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

一、锁分类:

  • 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版本对比规则:

    2725007-20240114163307768-241943022.png
    在可重复读隔离级别下,当事务开启时,任何查询的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='间隙锁表'
    
    2725007-20240115231434106-1485833526.png
  • 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.没有解决幻读问题的示例:

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

    2725007-20240115232525165-641763546.png
    2725007-20240115232610227-1299493810.png
    • 事务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隔离级别下的加锁过程,敬请期待!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK