Одним из простых решений является блокировка таблицы в режиме общего доступа, чтобы гарантировать отсутствие одновременных записей перед самым крупным обновлением с помощью 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.