zoukankan      html  css  js  c++  java
  • 【MySQL】MySQL锁和隔离级别浅析二 之 INSERT

    最近在整理线上性能时,发现一台线上DB出现两个insert产生的死锁问题

    ------------------------
    LATEST DETECTED DEADLOCK
    ------------------------
    150119 10:55:08
    *** (1) TRANSACTION:
    TRANSACTION 578E79C8, ACTIVE 0 sec inserting
    mysql tables in use 1, locked 1
    LOCK WAIT 7 lock struct(s), heap size 1248, 4 row lock(s), undo log entries 5
    MySQL thread id 32094912, query id 2210940713 10.10.10.2 database_1 update
    insert into table_1
                     (DATA_KEY,JOB_TYPE,FAILURE_QTY,OPT_STATUS,WAVE_NO,BIZ_TYPE,ORG_NO,DISTRIBUTE_NO,WAREHOUSE_NO,CREATE_TIME,UPDATE_TIME,CREATE_USER,UPDATE_USER,YN, REGION) 
                     values 
    ('8204593954',1009,0,0,'BC38015011900000062',10,'3','3','80',now(),null,'taskAssign-sys',null,0,6) *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 0 page no 2784161 n bits 376 index `unique` of table `database_1`.`table_1` trx id 578E79C8 lock mode S waiting Record lock, heap no 308 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 0: len 10; hex 38323034353933393534; asc 8204593954;; 1: len 19; hex 42433338303135303131393030303030303632; asc BC38015011900000062;; 2: len 4; hex 800003f1; asc ;; 3: len 8; hex 8000000000894c76; asc Lv;; *** (2) TRANSACTION: TRANSACTION 578E79CA, ACTIVE 0 sec inserting, thread declared inside InnoDB 500 mysql tables in use 1, locked 1 7 lock struct(s), heap size 1248, 4 row lock(s), undo log entries 8 MySQL thread id 32094907, query id 2210940717 10.10.10.2 database_1 update insert into table_1 (DATA_KEY,JOB_TYPE,FAILURE_QTY,OPT_STATUS,WAVE_NO,BIZ_TYPE,ORG_NO,DISTRIBUTE_NO,WAREHOUSE_NO,CREATE_TIME,UPDATE_TIME,CREATE_USER,UPDATE_USER,YN, REGION) values
    ('8204593814',1009,0,0,'BC38015011900000062',10,'3','3','80',now(),null,'taskAssign-sys',null,0,8) *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 0 page no 2784161 n bits 376 index `unique` of table `database_1`.`table_1` trx id 578E79CA lock_mode X locks rec but not gap Record lock, heap no 308 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 0: len 10; hex 38323034353933393534; asc 8204593954;; 1: len 19; hex 42433338303135303131393030303030303632; asc BC38015011900000062;; 2: len 4; hex 800003f1; asc ;; 3: len 8; hex 8000000000894c76; asc Lv;; *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 0 page no 2784161 n bits 376 index `index_otm_unique` of table `database_1`.`table_1` trx id 578E79CA lock_mode X locks gap before rec insert intention waiting Record lock, heap no 308 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 0: len 10; hex 38323034353933393534; asc 8204593954;; 1: len 19; hex 42433338303135303131393030303030303632; asc BC38015011900000062;; 2: len 4; hex 800003f1; asc ;; 3: len 8; hex 8000000000894c76; asc Lv;; *** WE ROLL BACK TRANSACTION (1)

    表结构

    primary(id)
    unique
    (DATA_KEY,WAVE_NO,JOB_TYPE) idx_update_time(update_time) idx_create_time(create_time)

    死锁本质原因是由于两个事务以相反顺序锁住了相同的数据,如下图:

    对于上面两个insert产生的死锁,分析一下insert锁上面的逻辑,但是对于上面的死锁原因暂时还没有头绪。

    姜承尧《MySQL内核.InnoDB存储引擎》中对于插入的说明(目录节点9.7.1 插入):

    对InnoDB存储引擎表进行插入操作时,需进行如下步骤的操作:
    1.首先对表加上IX表。(应该是IX锁)
    2.根据查询模式PAGE_CUR_LE定位记录next_rec。
    3.判断记录next_rec是否有锁,有的话等待锁的释放,否则直接插入。
    插入操作需要定位插入记录的下一条记录,这是next-key locking算法所要求,因为该算法下锁定的不仅仅是记录本身,锁定的是区间。
    例如下面的记录:
    1234578
    若要插入6这个记录,首先根据查询模式PAGE_CUR_LE定位到记录5,接着判断5这条记录的下一条记录是否有锁,因为如果有锁,则根据next-key locking算法,其表示锁定的范围是:(5,7]或者是(5,7)(gap标志位为1)。因此若记录7上有锁,则不允许在这个范围内进行插入操作。所以插入记录6的操作将被阻塞。对于InnoDB存储引擎而言,若记录next_rec上没有锁,则直接插入,不产生任何的锁对象。否则调用函数lock_rec_enqueue_waiting,等待记录next_rec上锁的释放,这时会产生锁的对象,锁定的记录为next_rec,锁的类型为LOCK_X|LOCK_GAP。
    此外,若下一条记录next_rec上有锁,不管持有该锁是否为插入操作事务本身,当插入操作完成后(无需事务提交),需要调用函数lock_update_insert来更新锁定的范围。例如上面的例子,若插入了6这条记录,则原来锁定的范围从(5,7]更新为了(5,6),(6,7]。这样就阻止了其他事物在(5,6)的范围内进行插入操作。
    还需要注意的是,若插入的表上有辅助索引,那么还需要对辅助索引记录进行锁的判断,其方法与步骤2、步骤3相同。只是在判断可以进行插入后,还需要更新辅助索引页page header中PAGE_MAX_TRX_ID的值。
    函数lock_rec_insert_check_and_lock用来判断next_rec上的锁,参数inherit用来判断是否在插入完成后调用函数lock_update_insert来对已经锁定的范围进行更新。

    对于上面的描述,做以下几组测试:

    1、插入的rec列中没有索引

    CREATE TABLE `test1` (
      `ID` bigint(20) NOT NULL AUTO_INCREMENT,
      `c1` int(10) DEFAULT NULL,
      PRIMARY KEY (`ID`)
    )
    
    ## SESSION 1  
    mysql> begin;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> select * from test1 where id between 1 and 3 for update ; ## 条件使用c1会锁住全部数据以及全部间隙,所以使用主键上锁
    +----+------+
    | ID  | c1  |
    +----+------+
    |  1  | 1   |
    |  2  | 2   |
    |  3  | 10  |
    |  4  | 11  |
    +----+------+
    4 rows in set (0.01 sec)
    
    ## SESSION 2
    mysql> begin;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> insert into test1(c1) values (9);
    Query OK, 1 row affected (0.00 sec)
    
    mysql> select * from test1;
    +----+------+
    | ID  | c1  |
    +----+------+
    |  1  | 1   |
    |  2  | 2   |
    |  3  | 10  |
    |  4  | 11  |
    |  5  | 9   |
    +----+------+
    7 rows in set (0.00 sec)
    
    ## SESSION 1/2
    mysql> commit;

    ### 上面这个情况和之前的理解有出入,因为之前认为作为聚集索引的主键区域(或next-rec)被锁住会影响到对应的数据页,但实际上不是。
    从上面的测试看,插入的rec列中如果没有索引,不受next_rec的主键(辅助索引)上锁影响(除next-key locking将自增字段的最大值区域锁住是LOCK_X和LOCK_GAP!)。
    “对于InnoDB存储引擎而言,若记录next_rec上没有锁,则直接插入,不产生任何的锁对象。”测试结果next-rec和间隙都不会有影响

    2、插入的rec中有普通索引

    若下一条记录next_rec上有锁,不管持有该锁是否为插入操作事务本身,当插入操作完成后(无需事务提交),需要调用函数lock_update_insert来更新锁定的范围。例如上面的例子,若插入了6这条记录,则原来锁定的范围从(5,7]更新为了(5,6),(6,7]。这样就阻止了其他事物在(5,6)的范围内进行插入操作。
    ### 但是测试并不是书中描述的样子
    mysql> alter table test1 add index idx_c1(c1);
    
    
    ## SESSION 1
    mysql> select * from test1 where c1=9 for update ;
    ## SESSION 2
    mysql> insert into test1(c1) values (8);
    ## SESSION 3
    mysql> insert into test1(c1) values (7);
    按书中描述,SESSION2会被SESSION1阻塞,SESSION3会被SESSION2阻塞,但实际测试SESSION1释放锁以后,SESSION2和3同时插入完成。
    另外
    不受next_rec的主键上的锁影响。
    还需要注意的是,若插入的表上有辅助索引,那么还需要对辅助索引记录进行锁的判断,其方法与步骤2、步骤3相同。只是在判断可以进行插入后,还需要更新辅助索引页page header中PAGE_MAX_TRX_ID的值。

     

    姜承尧《MySQL技术内幕.InnoDB存储引擎》中对于自增长字段与锁的说明(第一版6.2.4,第二版6.3.4):

    自增长在数据库中是非常常见的一种属性,也是很多DBA或开发人员首选的主键方式。在InnoDB存储引擎的内存结构中,对每个含有自增长值的表都有一个自增长计数器(auto-increment counter)。当对含有自增长计数器的表进行插入时,这个计数器会被初始化,会执行如下的语句来得到计数器的值:
    ”SELECT MAX(auto_inc_col) FROM t FOR UPDATE;”
    插入操作会更具这个自增长的计数器值加1赋予自增长列,这个实现方式叫做AUTO-INC Locking。这种锁其实是采用一种特殊的表锁机制,为了提高插入的性能,锁不是在一个事务完成后才释放,而是在完成对自增长值插入的SQL语句后立即释放。
    虽然AUTO-INC Locking从一定程度上提高了并发插入的效率,但这里还是存在一些问题。首先,对于有自增长值的列的并发插入性能较差,所以必须等待前一个插入的完成(虽然不用等待事务的完成)。其次,对于INSERT...SELECT的大数据量的插入,会影响插入的性能,因为另一个事务中的插入会被阻塞。
    从MySQL 5.1.22版本开始,InnoDB存储引擎提供了一种轻量级互斥量的自增长实现机制,这种机制大大提高了自增长值插入的性能。并且从MySQL 5.1.22开始,InnoDB存储引擎提供了一个参数    innodb_autoinc_lock_mode,默认值为1。在继续讨论新的自增长实现方式之前,我们需要对自增长的插入进行分类:
    ~INSERT-likeINSERT-like指所有的插入语句,如INSERT、REPLACEINSERT...SELECT、RELPACE...SELECTLOAD DATA等
    ~Simple inserts:Simple inserts指能在插入前就确定插入行数的语句。这些语句包括INSERT、REPLACE等。需要注意的是:Simple inserts不包含INSERT...ON DUPLICATE KEY UPDATE这类SQL语句。
    ~Bulk inserts:Bulk inserts指在插入前不能确定得到插入行数的语句,如INSERT...SELECTREPLACE...SELECTLOAD DATA。
    ~Mixed-mode inserts:Mixed-mode inserts指插入中有一部分的值时自增长的。有一部分是确定的,如:INSERT INTO t1(c1,c2) VALUES (1,'a'),(NULL,'b'),(5,'c'),(NULL,'d'),也可以是指INSERT...ON DUPLICATE KEY UPDATE这类SQL语句。
    参数innodb_autoinc_lock_mode有三个可选值,无法动态修改:
    ##innodb_autoinc_lock_mode=0 这是5.1.22版本之前自增长的实现方式,即通过表锁的AUTO-INC Locking方式。因为有了新的自增长实现方式,所以0这个选项不应该是你的首选项。
    ##
    innodb_autoinc_lock_mode=1 这是该参数的默认值。对于“SIMPLE INSERT”,该值会用互斥量(mutex)去对内存中的计数器进行累加的操作。对于“BULK INSERT”,还是使用传统的表锁的AUTO-INC Locking方式。这样做,如果不考虑回滚操作,对于自增长的增长还是连续的。而且这种方式下,Statement-Based方式的Replication还是能很好的工作,需要注意的是,如果已经使用AUTO-INC Locking的方式产生自增长的值,而这时需要再进行“SIMPLE INSERT”的操作时,还是要等待AUTO-INC Locking的释放。 ##innodb_autoinc_lock_mode=2 在这个模式下,对于所有的“INSERT-like”自增长值的产生都是通过互斥量,而不是AUTO-INC Locking的方式。显然,这是最高性能的方式。然而,这会带来一定的问题。因为并发插入的存在,所以每次插入时,自增长的值可能不是连续的。此外,最重要的是,基于Statement-Based Replication会出现问题。因此,使用这个模式,任何时候都应该使用Row-Base Replication。这样才能保证最大的并发性能和Replication数据的同步。 对于自增长另外需要注意的是,InnoDB存储引擎中的实现和MyISAM不同,MyISAM是表锁的,自增长不用考虑并发插入的问题,因此在Master用InnoDB存储引擎,Slave用MyISAM存储引擎的Replication架构下你必须考虑这种情况。 另外,InnoDB存储引擎下,自增长值的列必须是索引,并且是索引的第一列,如果是第二个列则会报错;而MyISAM存储引擎则没有这个问题。
  • 相关阅读:
    HDU 6071
    HDU 6073
    HDU 2124 Repair the Wall(贪心)
    HDU 2037 今年暑假不AC(贪心)
    HDU 1257 最少拦截系统(贪心)
    HDU 1789 Doing Homework again(贪心)
    HDU 1009 FatMouse' Trade(贪心)
    HDU 2216 Game III(BFS)
    HDU 1509 Windows Message Queue(队列)
    HDU 1081 To The Max(动态规划)
  • 原文地址:https://www.cnblogs.com/jiangxu67/p/4242346.html
Copyright © 2011-2022 走看看