对 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 上的所有索引,该表通常同时包含 VAL1VAL2

在此之前,我能够通过按给定顺序(从最高 SEQ 到最低)应用批量更新来大幅减少死锁的数量。因此,Oracle 似乎经常使用索引顺序来锁定表,但这也不是 100% 可靠的。

【讨论】:

也许使用动态视图和select * from T for update of VAL1; 也适用于您的情况。 asktom.oracle.com/pls/asktom/…

以上是关于对 JDBC 批量更新的所有行进行原子锁定的主要内容,如果未能解决你的问题,请参考以下文章

jdbc-批量插入批量删除批量更新

如何为 5500 万条记录批量更新 postgres 中的单个列

mysql jdbc连接器批量更新异常更新计数不如预期

如何确保以原子方式完成 JDBC 批量插入?

Grails批量处理锁定在桌子上

spring批量更新数据 ---- BatchPreparedStatementSetter