非规范化表格和重复数据

时间:2018-01-01 13:19:24

标签: java sql database oracle jdbc

我的Java项目使用纯JDBC与Oracle DB进行交互(第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驱动程序不会序列化查询,但会采用乐观的方法,并且在并发修改的情况下会出错,I不喜欢这样,它通过例外和#34;范式编程,反模式,然后第二个缺点就是表现当然。)

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

2 个答案:

答案 0 :(得分: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&#34;挂起&#34; (处于保持状态),因为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提交的更改 !!! 这是因为Read Committed Isolation Level

  

在读提交的隔离级别(默认值为每个级别)中   由事务执行的查询仅查看在之前提交的数据   查询 - 不是交易开始。这种隔离程度是   适用于少数交易的数据库环境   可能会发生冲突。

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

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

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

答案 1 :(得分: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查询,所以你不会选择错误的数据。

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

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

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