对 JDBC 批量更新的所有行进行原子锁定
Posted
技术标签:
【中文标题】对 JDBC 批量更新的所有行进行原子锁定【英文标题】:Atomic locking of all rows of a JDBC batch update 【发布时间】:2019-04-11 12:57:08 【问题描述】:我有两个线程在一个表上运行并发更新,类似于:
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:在等待资源时检测到死锁。我假设两个事务都会部分影响相同的行,其中两个事务设法在另一个之前锁定一些行。
有没有办法通过使锁定成为原子来避免这种情况,或者我是否需要在两个线程之间引入某种形式的显式锁定?
【问题讨论】:
如果这些死锁不经常发生,您可以重复该事务直到它通过。只需捕获 SQLRecoverableException / SQLRecoverableException。 感谢您的建议,我现在已经尝试过了,这实际上是解决小争用的最佳方法。通过以相反的顺序按 SEQ 排序第二批,我还能够大大减少死锁的数量。 Oracle 为 SEQ 使用了一个索引,并且它似乎在大多数情况下以与索引相反的顺序锁定行。 @Benjamin 这并不是真正的解决方案。这是修修补补。 【参考方案1】:当你更新一条记录时,会使用一个锁来防止脏写,这会损害原子性。
但是,在您的情况下,您可以使用SKIP LOCKED
。这样,在您尝试进行更新之前,您会尝试使用 SKIP LOCKED 获取 FOR UPDATE 锁。这将允许您锁定您计划修改的记录,并跳过已被其他并发事务锁定的记录。
查看我的高性能 Java 持久性 GitHub 存储库中的 SkipLockJobQueueTest
,了解如何使用 SKIP LOCKED 的示例。
【讨论】:
我使用您的文章实现了一个作业队列,并在 mysql 8 上对其进行了测试。在我的实现中,大部分时间的作业最终都会将一个新作业插入到作业队列表中。当我扩展到更多节点 (2-3) 时,插入语句出现死锁异常(SQL 错误 1205 和 1213)。我不会花很多时间调试它。现在我只是改用 JMS 解决方案。 INSERT 语句仅对行本身进行锁定,因此我看不出您如何为单个任务陷入死锁。如果你能提供一个可复制的测试用例,我可以看看问题是什么。【参考方案2】:在这种情况下,如果您的线程无法控制为不重叠数据,那么唯一的解决方案是锁定整个表,这不是一个很好的解决方案,因为其他线程(或其他任何执行 DML 的表)将挂起,直到锁定会话提交或回滚。您可以尝试的另一件事是让“较小”的人(更新单行的人)更频繁地提交(可能是每一行/执行),从而允许死锁(或锁等待)情况发生的频率降低.这对“较小”的人有性能副作用。
控制你的猴子!
-吉姆
【讨论】:
【参考方案3】:我假设两个事务都会部分影响相同的行 两个事务都设法在另一个之前锁定了一些行。
没错。我可以建议两个选项来避免这种情况:
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 锁定请求的行。并且只要另一个会话也对SELECT
使用FOR UPDATE
子句,它就会被阻塞,直到前一个事务被提交\回滚。
2) 使用DBMS_LOCK 包创建和控制自定义锁。获取和释放锁必须手动执行。
【讨论】:
【参考方案4】:一个简单的解决方案是在共享模式下锁定表,以确保在最大更新之前没有并发写入,使用 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
第二个在main中调用并发运行-/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 的自定义用户锁来序列化两组更新。
问候, 弗兰克。
【讨论】:
【参考方案5】:我找到了一个解决方案,该解决方案需要在插入端进行一些重新设计,但本质上仍然与以前一样。我已将表拆分为两个表:
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% 可靠的。
【讨论】:
也许使用动态视图和select * from T for update of VAL1;
也适用于您的情况。 asktom.oracle.com/pls/asktom/…以上是关于对 JDBC 批量更新的所有行进行原子锁定的主要内容,如果未能解决你的问题,请参考以下文章