技术开发 频道

B-Tree 索引中的数据块分裂

  尽管索引分裂是由递归事务控制的,其资源的请求与释放都很短暂,不受用户事务是否结束的影响,但是,在并发环境中,索引分裂仍然会导致一些等待事件。

  enq: TX - Index contention

  首先一个与索引分裂直接相关的等待事件,也是仅仅因为索引分裂才会导致的等待事件是"enq: TX - Index contention"。这一等待是一种TX队列等待,在10g之前,被笼统的归入TX队列等待中,10g之后,才有了更细致的划分。

  当一个更新事务需要插入/删除某个索引块上的数据,而这个数据块正在被另外一个事务分裂,则需要等待分裂完成后才能修改上面的数据,此时就会发生“enq: TX - Index contention”等待事件:

 HELLODBA.COM> create table tx_index_contention (a number, b varchar2(1446), c date);

  
Table created.

  HELLODBA.COM
> create index tx_index_contention_idx1 on tx_index_contention (c, b) tablespace idx_2k pctfree 10;

  
Index created.

  
--session 1,产生大量的索引块分裂:

  HELLODBA.COM
> conn demo/demo

  Connected.

  HELLODBA.COM
> select distinct sid from v$mystat;

  SID

  
----------

  
320

  HELLODBA.COM
> begin

  
2 for i in 1..2000

  
3 loop

  
4 insert into tx_index_contention (a, b, c) values (i, lpad('A', 1000, 'A'), sysdate);

  
5 end loop;

  
6 end;

  
7 /

  
--session 2, 在索引分裂的同时,插入数据:

  HELLODBA.COM
> conn demo/demo

  Connected.

  HELLODBA.COM
> select distinct sid from v$mystat;

  SID

  
----------

  
307

  HELLODBA.COM
> begin

  
2 for i in 1..1000

  
3 loop

  
4 insert into tx_index_contention (a, b, c) values (i, lpad('A', 20, 'A'), sysdate);

  
5 end loop;

  
6 end;

  
7 /

  PL
/SQL procedure successfully completed.

  可以看到,第二个会话中出现了"enq: TX - Index contention"等待:

HELLODBA.COM> select sid, event, total_waits from v$session_event where sid=307 and event = 'enq: TX - indexcontention';    
  
       SID EVENT                           TOTAL_WAITS    
---------- ------------------------------- ----------------    
       307 enq: TX - index contention      8  

       enq: TX - allocate ITL entry

  这一等待也是属于TX队列等待。

  在索引数据块上,有2种情形会导致发生“enq: TX - allocate ITL entry”等待:1、达到数据块上最大事务数限制;2、递归事务ITL争用。很显然,第二种情形是由索引分裂引起的:当一个事务中递归事务请求分裂一个数据块时,该数据块正在被另外一个事务的递归事务分裂,就发生“enq: TX - allocate ITL entry”等待。我们前面提过,无论在叶子节点数据块上还是在枝节点数据块上,有且只有一个ITL slot(枝节点上的唯一ITL slot,叶子节点上的第一条ITL slot)是用于递归事务的,当2个递归事务同时要请求该ITL slot,后发出请求的事务就需要等待:

 HELLODBA.COM> truncate table tx_index_contention;

  
Table truncated.

  
--Session 1, 发生大量索引块分裂

  HELLODBA.COM
> conn demo/demo

  Connected.

  HELLODBA.COM
> select distinct sid from v$mystat;

  SID

  
----------

  
312

  HELLODBA.COM
> begin

  
2 for i in 1..2000

  
3 loop

  
4 insert into tx_index_contention (a, b, c) values (i, lpad('A', 1000, 'A'), sysdate);

  
5 end loop;

  
6 end;

  
7 /

  
-- Session 2 中同时发生分裂

  HELLODBA.COM
> conn demo/demo

  Connected.

  HELLODBA.COM
> select distinct sid from v$mystat;

  SID

  
----------

  
307

  HELLODBA.COM
> begin

  
2 for i in 1..2000

  
3 loop

  
4 insert into tx_index_contention (a, b, c) values (i, lpad('A', 1000, 'A'), sysdate);

  
5 end loop;

  
6 end;

  
7 /

  可以看到两个会话中都发生了“enq: TX - allocate ITL entry”等待:

HELLODBA.COM> select sid, event, total_waits from v$session_event where sid in (312,307) and event = 'enq: TX - allocate ITL entry';

  SID EVENT TOTAL_WAITS

  
---------- ------------------------------ -----------------

  
307 enq: TX - allocate ITL entry 10

  
312 enq: TX - allocate ITL entry 8

  db file sequential read

  “db file sequential read”是因为Oracle从磁盘上读取单个数据块到内存中发生的等待——索引的读取就是单个数据块的读取(Fast Full Index Scan除外)。

  当发生索引块分裂,新数据块立即被加入索引树结构(和事务是否结束无关),这些新数据块被放入LRU链表中,Touch Count为1——因此容易被从buffer中置换出。此时,如果发生对索引的读,这些新数据块也会被读取——如果此时它们已经不在内存中,则会导致“db file sequential read”等待的增加:

HELLODBA.COM> conn demo/demo

  Connected.

  
--事务未被提交,内存块被释放

  HELLODBA.COM
> begin

  
2 for i in 1..100

  
3 loop

  
4 insert into tx_index_contention (a, b, c) values (i, lpad('A', 1000, 'A'), sysdate);

  
5 end loop;

  
6 end;

  
7 /

  PL
/SQL procedure successfully completed.

  HELLODBA.COM
> alter system flush buffer_cache;

  System altered.

  此时(在另外会话中)读取索引,发生db file sequential read等待

 HELLODBA.COM> conn demo/demo

  Connected.

  HELLODBA.COM
> set autot trace stat

  HELLODBA.COM
> select /*+index(t tx_index_contention_idx1)*/* from tx_index_contention t where c

  no rows selected

  
Statistics

  
----------------------------------------------------------

  
9 recursive calls

  
0 db block gets

  
756 consistent gets

  
147 physical reads

  
14648 redo size

  
372 bytes sent via SQL*Net to client

  
374 bytes received via SQL*Net from client

  
1 SQL*Net roundtrips to/from client

  
0 sorts (memory)

  
0 sorts (disk)

  
0 rows processed

  HELLODBA.COM
> set autot off

  HELLODBA.COM
> select sid, event, total_waits from v$session_event where sid in (307) and event = 'db file sequential read';

  SID EVENT TOTAL_WAITS

  
---------- -------------------------- -------------

  
307 db file sequential read 133

  这种情况下db file sequential read等待和并发的索引块分裂是无关的——是因为分裂导致索引段的数据块增加。但下面这种情况,就和并发索引分裂相关。

  前面提过,当数据块上当大量数据被删除,或者插入数据的事务被回滚,会在索引结构中留下大量空数据块被放入freelist,此时发生索引分裂,将可能会引起更多“db file sequential read”等待:当一个事务进行分裂时,从freelist的前列读取到空闲数据块——该数据块是由其它事务删除数据或者回滚而被放入freelist的,而如果此时该空闲数据块状态异常(删除事务未提交、或者有新的数据被重新插入该数据块),则分裂事务需要再重新读取空闲数据块。比较下面2段代码:

  1、索引构建好后,从buffer中置换出:

 HELLODBA.COM> truncate table tx_index_contention;

  
Table truncated.

  HELLODBA.COM
> conn demo/demo

  Connected.

  HELLODBA.COM
> begin

  
2 for i in 1..100

  
3 loop

  
4 insert into tx_index_contention (a, b, c) values (i, lpad('A', 1000, 'A'), sysdate);

  
5 end loop;

  
6 end;

  
7 /

  PL
/SQL procedure successfully completed.

  HELLODBA.COM
> commit;

  
Commit complete.

  HELLODBA.COM
> alter system flush buffer_cache;

  System altered.

  此时另外一个事务分裂索引会导致60的db file sequential read等待:

 HELLODBA.COM> conn demo/demo

  Connected.

  HELLODBA.COM
> begin

  
2 for i in 1..100

  
3 loop

  
4 insert into tx_index_contention (a, b, c) values (i, lpad('A', 1000, 'A'), sysdate);

  
5 end loop;

  
6 end;

  
7 /

  PL
/SQL procedure successfully completed.

  HELLODBA.COM
> select sid, event, total_waits from v$session_event where sid in (select sid from v$mystat) and event = 'db file sequential read';

  SID EVENT TOTAL_WAITS

  
---------- -------------------------- ------------

  
307 db file sequential read 60

  2、而如果事务将构建好的索引数据删除,相应数据块被放到freelist中去了,此时事务未提交,这些数据块的状态不适合作为分裂时的新数据块:

 HELLODBA.COM> truncate table tx_index_contention;

  
Table truncated.

  HELLODBA.COM
> conn demo/demo

  Connected.

  HELLODBA.COM
> begin

  
2 for i in 1..100

  
3 loop

  
4 insert into tx_index_contention (a, b, c) values (i, lpad('A', 1000, 'A'), sysdate);

  
5 end loop;

  
6 end;

  
7 /

  PL
/SQL procedure successfully completed.

  HELLODBA.COM
> commit;

  
Commit complete.

  HELLODBA.COM
> delete from tx_index_contention;

  
100 rows deleted.

  HELLODBA.COM
> alter system flush buffer_cache;

  System altered.

  另外一个事务需要进行分裂时,会先读取到freelist上的数据——发现不能被作为新数据块,需重新读取空闲数据块,造成db file sequential read等待增加:

HELLODBA.COM> conn demo/demo

  Connected.

  HELLODBA.COM
> begin

  
2 for i in 1..100

  
3 loop

  
4 insert into tx_index_contention (a, b, c) values (i, lpad('A', 1000, 'A'), sysdate);

  
5 end loop;

  
6 end;

  
7 /

  PL
/SQL procedure successfully completed.

  HELLODBA.COM
> select sid, event, total_waits from v$session_event where sid in (select sid from v$mystat) and event in = 'db file sequential read';

  SID EVENT TOTAL_WAITS

  
---------- -------------------------- -----------------

  
307 db file sequential read 175

  这种情况下,还会可能导致连锁等待:分裂事务会对被分裂数据块加共享锁,此时如果有其它事务需要向该数据块写入数据,那么这些事务就会进入等待队列,并记录"enq: TX - index contention",直到分裂事务找到可用数据块、完成分裂。

0
相关文章