首页 > 其他分享 >MVCC多版本并发控制和幻读问题的解决

MVCC多版本并发控制和幻读问题的解决

时间:2024-01-15 23:45:44浏览次数:38  
标签:事务 thread 幻读 number gap 并发 MVCC test id

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

一、锁分类:

  • 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隔离级别下的加锁过程,敬请期待!

标签:事务,thread,幻读,number,gap,并发,MVCC,test,id
From: https://www.cnblogs.com/ruobilin/p/17963507

相关文章

  • 还不敢写多线程程序?看看Go如何让并发控制简单有趣
    还不敢写多线程程序?看看Go如何让并发控制简单有趣原创 萤火架构 萤火架构 2024-01-1219:50 发表于北京 听全文所谓并发控制,就是同一程序进程内不同线程间访问相同资源时的冲突处理,有时也称为进程内同步。比如一个简单的内存累加计数操作,如果不进行同步,不同的线程可......
  • 高并发场景下如何实现系统限流?
    限流要结合容量和压测来进行,当外部请求接近或者达到系统最大阈值时,触发限流,采取其他手段进行降级,保证系统不被压垮,常见降级策略包括延迟处理,拒绝服务,随机拒绝等。 计数器法:将时间划分固定窗口大小,如1s设定100请求,该窗口时间之后的请求进行丢弃处理滑动窗口计数:将时间拆分......
  • 如何设计一个高并发系统?
    所谓高并发系统,是指能同时处理大量并发请求,并及时响应,从而保证系统的高性能和高可用那么我们在设计一个高并发系统时,应该考虑哪些方面呢?1.搭建集群如果你只部署一个应用,只部署一台服务器,那抗住的流量请求是非常有限的。并且,单体的应用,有单点的风险,如果它挂了,那服务就不可用了......
  • 精确掌控并发:滑动时间窗口算法在分布式环境下并发流量控制的设计与实现
    这是《百图解码支付系统设计与实现》专栏系列文章中的第(15)篇,也是流量控制系列的第(2)篇。点击上方关注,深入了解支付系统的方方面面。上一篇介绍了固定时间窗口算法在支付渠道限流的应用以及使用redis实现的核心代码。本篇重点讲清楚分布式环境下滑动时间窗口算法原理和应用场景,以及使......
  • 并发重要知识点—线程池详解
    https://blog.csdn.net/qq_40270751/article/details/78843226 创建线程的另一种方法是实现Runnable接口。Runnable接口中只有一个run()方法,它非Thread类子类的类提供的一种激活方式。一个类实现Runnable接口后,并不代表该类是一个“线程”类,不能直接运行,必须通过Thread实例才......
  • 精确掌控并发:固定时间窗口算法在分布式环境下并发流量控制的设计与实现
    这是《百图解码支付系统设计与实现》专栏系列文章中的第(14)篇。点击上方关注,深入了解支付系统的方方面面。本篇主要介绍分布式场景下常用的并发流量控制方案,包括固定时间窗口、滑动时间窗口、漏桶、令牌桶、分布式消息中间件等,并重点讲清楚固定时间窗口应用原理和应用场景,以及使用re......
  • JUC并发编程 CompletableFuture 获得结果和触发计算 常用方法(二)
    1获得结果和触发计算1.1获取结果publicTget()publicTget(longtimeout,TimeUnitunit)publicTjoin()--->和get一样的作用,只是不需要抛出异常publicTgetNow(TvaluelfAbsent)--->计算完成就返回正常值,否则返回备胎值(传入的参数),立即获取结果不阻塞1......
  • JUC并发编程 CompletableFuture 业务代码实战
    1需求电商网站比价需求分析:1.1需求说明:a.同一款产品,同时搜索出同款产品在各大电商平台的售价b.同一款产品,同时搜索出本产品在同一个电商平台下,各个入驻卖家售价是多少1.2输出返回:a.出来结果希望是同款产品的在不同地方的价格清单列表,返回一个List例如:《Mysql》......
  • JUC并发编程 用CompletableFuture 创建异步任务
    1CompletableFuture对Future的改进1.1CompletableFuture为什么会出现get()方法在Future计算完成之前会一直处在阻塞状态下,阻塞的方式和异步编程的设计理念相违背。isDene()方法容易耗费cpu资源(cpu空转),对于真正的异步处理我们希望是可以通过传入回调函数,在Future结束时自动......
  • C++多线程并发(一)--- 线程创建与管理
    目录进程和线程的区别何为并发?C++11线程基本操作C++11新标准多线程支持库std::thread类成员函数std::thread的关键总结C++中多线程创建C++的多线程可以充分利用计算机资源,提高代码运行效率。在这里总结了一些多线程应用过程中的基本概念和用法。进程和线程的区别进程是一......