非规范化表和重复数据[关闭]

问题描述 投票:2回答:2

我的Java项目使用纯JDBC与Oracle DB进行交互(v.12)。事务隔离级别为Read Committed。

我有一个高度非规范化的表,它将一个实体存储在一组行中。我无法改变这一点。不幸的是,这个表必须保持这种方式,原因与我无关。

+------+------+---------+
| date | hash | ....... |
+------+------+---------+
| date | xyz  | ....... |
| date | xyz  | ....... |
| date | xyz  | ....... |

我有两列标识实体 - 日期和哈希。由于每个实体都存储为多个行,因此这些列实际上不是唯一的,也不是主键,而只是索引列。我仍然希望强制执行一种“唯一性”,这意味着当时只存在一个实体,无论它有多少行。

这样的实体可以每天更新几次,产生不同的值,但也有不同的行数。

为了实现这一切,每次更新实体时,我都会在单个事务中执行两个或更多查询:

delete from "table" where "date" = ? and "hash" = ?
insert into "table" values (?, ?, .....)
insert into "table" ....
... -- as many inserts as needed to store whole entity

这适用于单个应用程序实例。不幸的是,我有2个实例同时工作,试图几乎同时存储完全相同的数据(它们只是主要备份实例,但备份也是持久的 - 这对我也没有影响)。

如果这是规范化表,那么解决方案将是使用MERGE语句,但它在这里不起作用。

我目前的解决方案

到目前为止我尝试做的是添加一个列,实例的ID持久化,然后使用SELECT作为数据源执行INSERT语句,并将条件置于SELECTs,这个日期/哈希和应用程序ID必须没有数据,否则SELECT不提供要插入的数据。

我认为它会起作用,但显然它没有。我仍然看到重复。我认为这是因为两个事务一开始就删除它们,仍然看不到其他事务尚未提交的数据,因此自己执行插入操作。然后“提交”执行并繁荣。两个事务都插入其数据。

我考虑过的其他方法:

我猜乐观锁定也不起作用,因为在最终版本检查时,两个事务仍然可以认为版本不会被更改,而它们实际上同时被两个事务更改并且即将以这种方式提交。

我知道我可以将事务隔离切换到SERIALIZABLE,但它也不是完美的(首先,Oracle驱动程序不会序列化查询,但会采用乐观方法,并且在并发修改的情况下会出现错误,我不喜欢,它是一种“异常编程”范式,一种反模式,然后第二个缺点就是性能当然)。

这样的问题还有其他解决办法吗?

java sql database oracle jdbc
2个回答
2
投票

在我阅读它们时,您的要求是:

  • 数据库结构不能改变
  • 两个应用程序必须同时更新完全相同的数据
  • 乐观锁定是因为它可能导致错误或性能下降
  • 悲观锁定出于与乐观锁定相同的原因

看起来最重要的不是你正在改变什么数据,而是你正在读什么数据。您需要一种方法来确定系统用户的数据(我无法判断这些应用程序是仅维护数据还是使用它)。

我假设您当前对提供数据的查询是这样的:

select * from table where date = :1 and hash = :2

如果您将此更改为以下内容,那么您将始终选择最新的数据,如果有重复的时间,您将选择第一个应用程序(基本上是随机的 - 更改为您想要的任何顺序)

select *
  from ( select t.*
              , rank() over (partition by hash 
                                 order by date desc, app_id desc) as rnk
           from table t
                )
 where rnk = 1

你可以把它放在一个视图中吗?

然后,您基本上可以在一个表中运行两个单独的表。您可以使用MERGE等,并可以将DELETE / INSERT语句更改为:

merge into table o
using (select :1, :2 ... ) n
   on ( o.date = n.date
       and o.hash = n.hash
       and o.app_id = n.app_id
           )
 when matched then
      update
         set ...
 when not matched then
      insert (...

commit;

delete from table
 where date < :1 
   and hash = :2

commit;

您在MERGE语句中使用相同日期和哈希的位置。如果DELETE失败,你不介意 - 因为你已经改变了SELECT查询,所以你不会选择错误的数据。


就个人而言,我承认你的一项要求必须改变。

如果有任何添加其他应用程序的计划,我会接受性能下降并使用排队机制对该表进行串行更新。

如果没有添加其他应用程序的计划,请立即采用简单方法并开始使用锁定策略(不是很好),只处理一些已知错误。


3
投票

为了序列化这两个事务,我将创建一个额外的表:

CREATE TABLE locktable(
  my_date date,
  my_hash number,
  primary key (my_date, my_hash)
);

并将以下面的方式更改整个交易:

INSERT INTO locktable( my_date, my_hash ) VALUES ( date_value, hash_value );

delete from "table" where "date" = date_value and "hash" = hash_value;
insert ....
insert ....

DELETE FROM locktable WHERE my_date = date_value AND my_hash = hash_value;
COMMIT;

由于现有的主键约束阻止将两个重复记录插入表中,因此第一个INSERT语句将序列化事务。 您可以通过使用两个不同的会话和默认隔离级别READ COMMITED运行简单测试来了解它是如何工作的。


首先,让我们创建测试数据:

CREATE TABLE my_table(
  my_date date,
  my_hash number,
  somevalue int
);

INSERT INTO my_table( my_date, my_hash, somevalue)
SELECT trunc( sysdate ), 123, 111 FROM dual
CONNECT BY level <= 3;
commit;

CREATE TABLE locktable(
  my_date date,
  my_hash number,
  primary key (my_date, my_hash)
);

Sesion#1 - 看到了原始数据。 我们将把记录插入locktable,然后删除旧记录并插入新记录。

SQL> select * from my_table;

MY_DATE      MY_HASH  SOMEVALUE
--------- ---------- ----------
01-JAN-18        123        111
01-JAN-18        123        111
01-JAN-18        123        111

SQL> INSERT INTO locktable( my_date, my_hash ) VALUES ( trunc( sysdate), 123 );

1 row created.

SQL> DELETE FROM my_table WHERE my_date = trunc( sysdate ) AND my_hash = 123;

3 rows deleted.

SQL> INSERT INTO my_table( my_date, my_hash, somevalue)
  2  SELECT trunc( sysdate ), 123, 222 FROM dual CONNECT BY level <= 3;

3 rows created.

会话#2 - 此会话不会看到会话#1插入的记录,因为它尚未提交(somevalue = 111):

SQL> select * from my_table;

MY_DATE      MY_HASH  SOMEVALUE
--------- ---------- ----------
01-JAN-18        123        111
01-JAN-18        123        111
01-JAN-18        123        111

SQL> INSERT INTO locktable( my_date, my_hash ) VALUES ( trunc( sysdate), 123 );

当执行INSERT时,会话#2“挂起”(处于保持状态),因为Oracle检测到另一个会话插入的表locktable中存在重复记录,该记录尚未解除。 Oracle现在将等待第一个会话将执行的操作:

  • 如果第一个会话将执行COMMIT,则会在会话#2中抛出重复错误
  • 如果第一个会话将执行ROLLBACK,或者将删除此行并执行COMMIT,则sessin#2将被取消,并且将插入该行

我们去参加会议#1并做:

SQL> DELETE FROM  locktable WHERE my_date = trunc( sysdate) AND my_hash = 123;

1 row deleted.

SQL> commit;

Commit complete.

现在让我们看看会议#2中发生了什么:

SQL> INSERT INTO locktable( my_date, my_hash ) VALUES ( trunc( sysdate), 123 );

1 row created.

SQL>

会议已取消阻止并继续工作。 我们再做一次检查:

SQL> select * from my_table;

MY_DATE      MY_HASH  SOMEVALUE
--------- ---------- ----------
01-JAN-18        123        222
01-JAN-18        123        222
01-JAN-18        123        222

现在会话#2看到会话#1提交的更改! 这是因为在qazxsw poi中:

在读取提交的隔离级别(默认情况下)中,事务执行的每个查询仅查看在查询之前提交的数据,而不是事务开始时提交的数据。这种隔离级别适用于很少有事务可能发生冲突的数据库环境。

即 - 第一个事务被提交,然后第二个事务被解除阻塞,然后第二个事务看到第一个事务所做的更改,尽管事实上第二个事务的开始时间晚于第一个事务。


现在我们可以继续在第二个事务中工作(删除旧数据并插入新数据)。如果另一个(第三个)事务开始(具有相同的日期和哈希),由于Read Committed Isolation Level表中的现有记录,它将再次被搁置。


上述方法将确保只有这一个事务的正确序列化。 如果应用程序也在其他位置插入或删除记录,则除非相应地更改其他位置,否则它将无法正常工作。

© www.soinside.com 2019 - 2024. All rights reserved.