我的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;范式编程,反模式,然后第二个缺点就是表现当然。)
这个问题还有其他解决办法吗?
答案 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现在将等待第一个会话将要执行的操作:
让我们转到会话#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查询,所以你不会选择错误的数据。
就个人而言,我承认你的一项要求必须改变。
如果有任何添加其他应用程序的计划,我会接受性能下降并使用排队机制连续对此表执行更新。
如果没有添加其他应用程序的计划,请立即采用简单方法并开始使用锁定策略(不是很好),只处理一些已知错误。