Атомная блокировка всех строк пакетного обновления JDBC - PullRequest
4 голосов
/ 11 апреля 2019

У меня есть два потока, в которых параллельные обновления выполняются для таблицы, аналогичной:

CREATE TABLE T (
  SEQ NUMBER(10) PRIMARY KEY,
  VAL1 VARCHAR2(10),
  VAL2 VARCHAR2(10)
)

Таблица содержит большое количество записей, обновления которых похожи на:

UPDATE T SET VAL1 = ? WHERE SEQ < ?
UPDATE T SET VAL2 = ? WHERE SEQ = ?

Оба оператора выполняются в двух разных транзакциях в виде пакетных обновлений JDBC по 1000 строк в каждом.При этом я сталкиваюсь с ORA-00060: обнаружена тупиковая ситуация при довольно быстром ожидании ресурса .Я предполагаю, что обе транзакции частично затронут одни и те же строки, где обеим транзакциям удалось заблокировать некоторые строки раньше другой.

Есть ли способ избежать этого, сделав блокировку атомарной, или мне нужно было бы ввести некоторую формуявной блокировки между двумя потоками?

Ответы [ 5 ]

5 голосов
/ 11 апреля 2019

Когда вы обновляете запись, берется блокировка для предотвращения грязных записей, которые могут поставить под угрозу атомарность.

Однако в вашем случае вы можете использовать SKIP LOCKED .Таким образом, прежде чем пытаться выполнить обновление, вы пытаетесь получить блокировку FOR UPDATE с помощью SKIP LOCKED.Это позволит вам заблокировать записи, которые вы планируете изменить, а также пропустить записи, которые уже заблокированы другими параллельными транзакциями.

Проверьте SkipLockJobQueueTest в моем High-PerformanceJava Persistence GitHub репозиторий для примера того, как вы можете использовать SKIP LOCKED.

Для получения дополнительной информации о SKIP LOCKED, прочитайте эту статью .

1 голос
/ 17 апреля 2019

Одним из простых решений является блокировка таблицы в режиме общего доступа, чтобы гарантировать отсутствие одновременных записей перед самым крупным обновлением с помощью LOCK TABLE ... IN SHARE MODE.

Вот мои два сценария, если вы хотите воспроизвести: Основной создает таблицу и запускает контрольный пример - /tmp/sql1.sql:

set echo on time on define off sqlprompt "SQL1> " linesize 69 pagesize 1000
set sqlformat ansiconsole
connect sys/oracle@//localhost/PDB1 as sysdba
grant dba to scott identified by tiger;
connect scott/tiger@//localhost/PDB1
exec begin execute immediate 'drop table T'; exception when others then null; end;
CREATE TABLE T (
  SEQ NUMBER(10) constraint T_SEQ PRIMARY KEY,
  VAL1 VARCHAR2(10),
  VAL2 VARCHAR2(10)
);
insert into T select rownum , 0 , 0 from xmltable('1 to 5');
commit;
-- -------- start session 1
connect scott/tiger@//localhost/PDB1
select sys_context('userenv','sid') from dual;
variable val number
variable seq number;
exec :seq:=4; :val:=2;
UPDATE T SET VAL2 = :val WHERE SEQ = :seq;
-- -------- call session 2
host sql /nolog @/tmp/sql2.sql < /dev/null & :
host sleep 5
select session_id,lock_type,mode_held,mode_requested,lock_id1,lock_id2,blocking_others from dba_locks where lock_type in ('DML','Transaction','PL/SQL User Lock');
-- -------- continue session 1 while session 2 waits
exec :seq:=1; :val:=3;
UPDATE T SET VAL2 = :val WHERE SEQ = :seq;
host sleep 1
commit;
select * from T;
-- -------- end session 1

Второй вызывается в главном для одновременного запуска - /tmp/sql2.sql:

set echo on time on define off sqlprompt "SQL2> "
-- -------- start session 2 -------- --
host sleep 1
connect scott/tiger@//localhost/PDB1
select sys_context('userenv','sid') from dual;
variable val number
variable seq number;
exec :seq:=5; :val:=1;
/* TM lock solution */ lock table T in share mode;
UPDATE T SET VAL1 = :val WHERE SEQ < :seq;
commit;
select * from T;
-- -------- end session 2

Вот прогон с общей блокировкой, где мы видим блокировку DML 'Share', заблокированную 'Row-X' (которая автоматически получается при обновлении):

SQLcl: Release 18.4 Production on Wed Apr 17 09:32:04 2019

Copyright (c) 1982, 2019, Oracle.  All rights reserved.

SQL>
SQL> set echo on time on define off sqlprompt "SQL1> " linesize 69 pagesize 1000
09:32:04 SQL1> set sqlformat ansiconsole
09:32:04 SQL1> connect sys/oracle@//localhost/PDB1 as sysdba
Connected.
09:32:05 SQL1>
09:32:05 SQL1> grant dba to scott identified by tiger;

Grant succeeded.

09:32:05 SQL1> connect scott/tiger@//localhost/PDB1
Connected.
09:32:08 SQL1>
09:32:08 SQL1> exec begin execute immediate 'drop table T'; exception when others then null; end;

PL/SQL procedure successfully completed.

09:32:09 SQL1> CREATE TABLE T (
  2    SEQ NUMBER(10) constraint T_SEQ PRIMARY KEY,
  3    VAL1 VARCHAR2(10),
  4    VAL2 VARCHAR2(10)
  5  );

Table created.

09:32:09 SQL1> insert into T select rownum , 0 , 0 from xmltable('1 to 5');

5 rows created.

09:32:09 SQL1> commit;

Commit complete.

09:32:09 SQL1> -- -------- start session 1
09:32:09 SQL1> connect scott/tiger@//localhost/PDB1
Connected.
09:32:09 SQL1>
09:32:09 SQL1> select sys_context('userenv','sid') from dual;
SYS_CONTEXT('USERENV','SID')
4479


09:32:09 SQL1> variable val number
09:32:09 SQL1> variable seq number;
09:32:09 SQL1> exec :seq:=4; :val:=2;

PL/SQL procedure successfully completed.

09:32:09 SQL1> UPDATE T SET VAL2 = :val WHERE SEQ = :seq;

1 row updated.

09:32:09 SQL1> -- -------- call session 2
09:32:09 SQL1> host sql /nolog @/tmp/sql2.sql < /dev/null & :

09:32:09 SQL1> host sleep 5

SQLcl: Release 18.4 Production on Wed Apr 17 09:32:10 2019

Copyright (c) 1982, 2019, Oracle.  All rights reserved.

09:32:10 SQL2> -- -------- start session 2 -------- --
09:32:10 SQL2> host sleep 1

09:32:11 SQL2> connect scott/tiger@//localhost/PDB1
Connected.
09:32:11 SQL2> select sys_context('userenv','sid') from dual;
SYS_CONTEXT('USERENV','SID')
4478


09:32:12 SQL2> variable val number
09:32:12 SQL2> variable seq number;
09:32:12 SQL2> exec :seq:=5; :val:=1;

PL/SQL procedure successfully completed.

09:32:12 SQL2> /* TM lock solution */
09:32:12 SQL2>  lock table T in share mode;

09:32:14 SQL1> select session_id,lock_type,mode_held,mode_requested,lock_id1,lock_id2,blocking_others from dba_locks where lock_type in ('DML','Transaction','PL/SQL User Lock');
  SESSION_ID LOCK_TYPE     MODE_HELD    MODE_REQUESTED   LOCK_ID1   LOCK_ID2   BLOCKING_OTHERS
        4478 DML           None         Share            73192      0          Not Blocking
        4479 DML           Row-X (SX)   None             73192      0          Blocking
        4479 Transaction   Exclusive    None             655386     430384     Not Blocking


09:32:14 SQL1> -- -------- continue session 1 while session 2 waits
09:32:14 SQL1> exec :seq:=1; :val:=3;

PL/SQL procedure successfully completed.

09:32:17 SQL1> UPDATE T SET VAL2 = :val WHERE SEQ = :seq;

1 row updated.

09:32:17 SQL1> host sleep 1

09:32:18 SQL1> commit;

Lock succeeded.


Commit complete.

09:32:18 SQL2> UPDATE T SET VAL1 = :val WHERE SEQ < :seq;
09:32:18 SQL1> select * from T;

4 rows updated.

09:32:18 SQL2> commit;

Commit complete.

09:32:18 SQL2> select * from T;
  SEQ VAL1   VAL2
    1 1      3
    2 1      0
    3 1      0
    4 1      2
    5 0      0


09:32:18 SQL1> -- -------- end session 1

  SEQ VAL1   VAL2
    1 1      3
    2 1      0
    3 1      0
    4 1      2
    5 0      0


09:32:18 SQL2> -- -------- end session 2

09:32:18 SQL2>
Disconnected from Oracle Database 19c Enterprise Edition Release 19.0.0.0.0 - Production
Version 19.2.0.0.0

И тот же пример без разделяемой блокировки, где мы видим исключительную блокировку транзакции (которая является той, когда обновление встречает строку, заблокированную другой транзакцией), ведущую к взаимоблокировке:

SQLcl: Release 18.4 Production on Wed Apr 17 09:39:35 2019

Copyright (c) 1982, 2019, Oracle.  All rights reserved.

SQL>
SQL> set echo on time on define off sqlprompt "SQL1> " linesize 69 pagesize 1000
09:39:35 SQL1> set sqlformat ansiconsole
09:39:35 SQL1> connect sys/oracle@//localhost/PDB1 as sysdba
Connected.
09:39:36 SQL1>
09:39:36 SQL1> grant dba to scott identified by tiger;

Grant succeeded.

09:39:36 SQL1> connect scott/tiger@//localhost/PDB1
Connected.
09:39:36 SQL1>
09:39:36 SQL1> exec begin execute immediate 'drop table T'; exception when others then null; end;

PL/SQL procedure successfully completed.

09:39:37 SQL1> CREATE TABLE T (
  2    SEQ NUMBER(10) constraint T_SEQ PRIMARY KEY,
  3    VAL1 VARCHAR2(10),
  4    VAL2 VARCHAR2(10)
  5  );

Table created.

09:39:37 SQL1> insert into T select rownum , 0 , 0 from xmltable('1 to 5');

5 rows created.

09:39:37 SQL1> commit;

Commit complete.

09:39:37 SQL1> -- -------- start session 1
09:39:37 SQL1> connect scott/tiger@//localhost/PDB1
Connected.
09:39:37 SQL1>
09:39:37 SQL1> select sys_context('userenv','sid') from dual;
SYS_CONTEXT('USERENV','SID')
4479


09:39:37 SQL1> variable val number
09:39:37 SQL1> variable seq number;
09:39:37 SQL1> exec :seq:=4; :val:=2;

PL/SQL procedure successfully completed.

09:39:37 SQL1> UPDATE T SET VAL2 = :val WHERE SEQ = :seq;

1 row updated.

09:39:37 SQL1> -- -------- call session 2
09:39:37 SQL1> host sql /nolog @/tmp/sql2.sql < /dev/null & :

09:39:37 SQL1> host sleep 5

SQLcl: Release 18.4 Production on Wed Apr 17 09:39:38 2019

Copyright (c) 1982, 2019, Oracle.  All rights reserved.

09:39:38 SQL2> -- -------- start session 2 -------- --
09:39:38 SQL2> host sleep 1

09:39:39 SQL2> connect scott/tiger@//localhost/PDB1
Connected.
09:39:39 SQL2> select sys_context('userenv','sid') from dual;
SYS_CONTEXT('USERENV','SID')
4478


09:39:40 SQL2> variable val number
09:39:40 SQL2> variable seq number;
09:39:40 SQL2> exec :seq:=5; :val:=1;

PL/SQL procedure successfully completed.

09:39:40 SQL2> /* TM lock solution */
09:39:40 SQL2>  --lock table T in share mode;
09:39:40 SQL2> UPDATE T SET VAL1 = :val WHERE SEQ < :seq;

09:39:42 SQL1> select session_id,lock_type,mode_held,mode_requested,lock_id1,lock_id2,blocking_others from dba_locks where lock_type in ('DML','Transaction','PL/SQL User Lock');
  SESSION_ID LOCK_TYPE     MODE_HELD    MODE_REQUESTED   LOCK_ID1   LOCK_ID2   BLOCKING_OTHERS
        4478 Transaction   None         Exclusive        655368     430383     Not Blocking
        4479 DML           Row-X (SX)   None             73194      0          Not Blocking
        4478 DML           Row-X (SX)   None             73194      0          Not Blocking
        4479 Transaction   Exclusive    None             655368     430383     Blocking
        4478 Transaction   Exclusive    None             589838     281188     Not Blocking


09:39:46 SQL1> -- -------- continue session 1 while session 2 waits
09:39:46 SQL1> exec :seq:=1; :val:=3;

PL/SQL procedure successfully completed.

09:39:46 SQL1> UPDATE T SET VAL2 = :val WHERE SEQ = :seq;

1 row updated.

09:39:47 SQL1> host sleep 1

UPDATE T SET VAL1 = :val WHERE SEQ < :seq
             *
ERROR at line 1:
ORA-00060: deadlock detected while waiting for resource

09:39:47 SQL2> commit;

Commit complete.

09:39:47 SQL2> select * from T;
  SEQ VAL1   VAL2
    1 0      0
    2 0      0
    3 0      0
    4 0      0
    5 0      0


09:39:47 SQL2> -- -------- end session 2

09:39:47 SQL2>
Disconnected from Oracle Database 19c Enterprise Edition Release 19.0.0.0.0 - Production
Version 19.2.0.0.0

09:39:48 SQL1> commit;

Commit complete.

09:39:48 SQL1> select * from T;
  SEQ VAL1   VAL2
    1 0      3
    2 0      0
    3 0      0
    4 0      2
    5 0      0


09:39:48 SQL1> -- -------- end session 1

Эта блокировка общего ресурса предотвращает все одновременные модификации, даже некоторые модификации таблиц, связанных ссылочной целостностью, поэтому позаботьтесь об общей активности записи в них. Другое решение заключается в использовании пользовательской блокировки пользователя с dbms_lock для сериализации двух наборов обновлений.

С уважением, Franck.

1 голос
/ 11 апреля 2019

Я предполагаю, что обе транзакции частично затронут одни и те же строки, где обеим транзакциям удалось заблокировать некоторые строки раньше другой.

Это верно.Я могу предложить два варианта, чтобы избежать этого:

1) Используйте условие SELECT ... FOR UPDATE перед обновлением:

SELECT * FROM T WHERE SEQ < ? FOR UPDATE;
UPDATE T SET VAL1 = ? WHERE SEQ < ?

SELECT * FROM T WHERE SEQ = ? FOR UPDATE;
UPDATE T SET VAL2 = ? WHERE SEQ = ?

Предикаты должны быть одинаковыми, чтобы влиять на те же строки.
FOR UPDATE предложение заставляет Oracle блокировать запрошенные строки.И до тех пор, пока в другом сеансе также используется предложение FOR UPDATE для SELECT, он блокируется до тех пор, пока предыдущая транзакция не будет зафиксирована \ откатана.

2) Используйте пакет DBMS_LOCK для созданияи контролировать пользовательский замок.Получение и снятие блокировки должно быть выполнено вручную.

1 голос
/ 11 апреля 2019

В этой ситуации, если ваши потоки не могут контролироваться, чтобы не перекрывать данные, тогда единственным решением будет блокировка всей таблицы, что не является отличным решением, как другой поток (или что-либо еще, выполняющее DML на таблица) зависнет, пока сеанс блокировки не завершится или не откатится назад. Другая вещь, которую вы могли бы попробовать, это заставить "меньшего" парня (тот, который обновляет одну строку) фиксировать чаще (возможно, каждую строку / выполнение), таким образом позволяя ситуации взаимоблокировки (или ожидания блокировки) потенциально возникать реже , Это имеет побочные эффекты производительности для "меньшего" парня.

Управляй своими обезьянами!

-Джит

0 голосов
/ 16 апреля 2019

Я нашел решение, которое потребовало немного переделать со стороны вставки, но по существу все еще делает то же самое, что и раньше.Я разделил таблицу на две таблицы:

CREATE TABLE T1 (
  SEQ NUMBER(10) PRIMARY KEY,
  VAL1 VARCHAR2(10)
);

CREATE TABLE T2 (
  SEQ NUMBER(10) PRIMARY KEY,
  VAL2 VARCHAR2(10)
);

Теперь я могу обновлять столбцы, не блокируя одну и ту же строку, так как я эмулирую блокировку столбца, делая это.Это, конечно, было бы серьезным изменением, но, к счастью, Oracle позволяет определить материализованное представление, чтобы избежать изменения каких-либо выборов:

CREATE MATERIALIZED VIEW LOG ON T1 WITH ROWID INCLUDING NEW VALUES;
CREATE MATERIALIZED VIEW LOG ON T2 WITH ROWID INCLUDING NEW VALUES;

CREATE MATERIALIZED VIEW T 
REFRESH FAST ON COMMIT
AS
SELECT SEQ, VAL1, VAL2, T1.ROWID AS T1_ROWID, T2.ROWID AS T2_ROWID
FROM T1
NATURAL JOIN T2;

Таким образом, я смог сохранить все индексы в базовой таблице T, которыеобычно содержал как VAL1, так и VAL2.

. До этого мне удалось резко уменьшить количество мертвых блокировок, применяя пакетные обновления в заданном порядке (от самого высокого SEQ до самого низкого).В результате Oracle, казалось, часто использовал порядок индексов для блокировки таблиц, но это также не было надежно на 100%.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...