并发进程在数据库中插入数据

时间:2009-07-14 12:42:40

标签: java postgresql jdbc transactions

在postgres数据库中考虑以下模式。

CREATE TABLE employee
(
  id_employee serial NOT NULL PrimarKey,
  tx_email_address text NOT NULL Unique,
  tx_passwd character varying(256)
)

我有一个跟随

的java类
conn.setAutoComit(false);

ResultSet rs = stmt.("select * from employee where tx_email_address = 'test1'");
if (!rs.next()) {
    Insert Into employee Values ('test1', 'test1');
}
ResultSet rs = stmt.("select * from employee where tx_email_address = 'test2'");
if (!rs.next()) {
    Insert Into employee Values ('test2', 'test2');
}
ResultSet rs = stmt.("select * from employee where tx_email_address = 'test3'");
if (!rs.next()) {
    Insert Into employee Values ('test3', 'test3');
}
ResultSet rs = stmt.("select * from employee where tx_email_address = 'test4'");
if (!rs.next()) {
    Insert Into employee Values ('test4', 'test4');
}

conn.commit();
conn.setAutoComit(true);

这里的问题是如果有两个或更多上述事务的并发实例试图写入数据。只有一个事务最终会成功,而休息会抛出SQLException“唯一键约束违规”。我们如何解决这个问题。

PS:我只选择了一个表和简单的插入查询来演示问题。我的应用程序是基于Java的应用程序,其唯一目的是将数据写入目标数据库。可能会有并发进程这样做,某些进程可能会尝试写入相同的数据(如上例所示)。

7 个答案:

答案 0 :(得分:1)

最简单的方法似乎是使用事务隔离级别'serializable',它可以防止幻像读取(其他人在您的事务中插入满足先前SELECT的数据)。

if (!conn.getMetaData().supportsTransactionIsolationLevel(Connection.TRANSACTION_SERIALIZABLE)) {
    // OK, you're hosed. Hope for your sake your drivers supports this isolation level 
}
conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);

还有像Oracle的“MERGE”语句这样的技术 - 单个语句可以“插入或更新”,具体取决于数据是否存在。我不知道Postgres是否有相同的功能,但有一些技术可以“伪造” - 例如, How to write INSERT IF NOT EXISTS queries in standard SQL

答案 1 :(得分:1)

我首先尝试以一种只有一个事务将获得一个数据实例的方式设计数据流。在那种情况下,“唯一键约束违规”永远不会发生,因此表明存在真正的问题。

如果失败了,我会在每次插入后捕获并忽略“唯一键约束违规”。当然,记录它发生的事情仍然是一个好主意。

如果两种方法由于某种原因都不可行,那么我很可能会创建一个与“employee”结构相同的转接表,但没有主键约束和“转接状态”字段。插入到此传输表中不会发生“唯一键约束违规”。 需要一份工作,读取该传输表并将数据传输到“员工”表中。此作业将利用“运输状态”来跟踪已处理的行。我会让工作每次都做不同的事情:

  • 在传输表上执行更新语句,以便为多个行设置“传输状态”为“正在进行中”。这个数字有多大,或者当前所有新行都被标记出来需要一些思考。
  • 执行更新语句,将所有行的“运输状态”设置为“重复”,其数据已在“员工”表中,且“运输状态”不在(“重复”,“已处理”)
  • 只要转接表中有行“transit status”=“正在进行中”,就会重复:
    • 使用“transit status”=“正在进行中”从传输表中选择一行。
    • 将行数据插入“employee”表。
    • 将此行设置为“过境状态”为“已处理”。
    • 使用与当前处理的行相同的数据更新传输表中的所有行,并将“传输状态”=“正在进行中”更新为“传输状态”=“重复”。

我很可能想要另一份工作来定期删除“运输状态”中的行(“复制”,“已处理”)

如果postgres不知道数据库作业,那么os方面的工作就可以了。

答案 2 :(得分:1)

解决方案是使用table level exclusive lock,使用命令LOCK锁定写入,同时允许并发读取。 伪SQL码:

select * from employee where tx_email_address = 'test1';
if not exists
   lock table employee in exclusive mode;
   select * from employee where tx_email_address = 'test1';
   if still not exists //may be inserted before lock
      insert into employee values ('test1', 'test1');
      commit; //releases exclusive lock

请注意,使用此方法将阻止所有其他写入,直到锁定被释放,从而降低吞吐量。

如果所有插入都依赖于父行,那么更好的方法是仅锁定父行,序列化子插入,而不是锁定整个表。

答案 3 :(得分:0)

您可以公开一个公共方法,该方法对写入操作进行排队并处理队列并发,然后创建另一个方法以在另一个实际执行串行写入的线程(或完全另一个进程)上运行。

答案 4 :(得分:0)

您可以通过使代码成为关键部分来在应用程序级别添加并发控制:

synchronized(lock) {
  // Code to perform selects / inserts within database transaction.
}

这样,一个线程被阻止查询表,而另一个线程正在查询并插入表中。当第一个线程完成时,第二个线程进入同步块。但是,此时每次选择尝试都将返回数据,因此线程不会尝试插入数据。

修改

如果您有多个进程插入同一个表,您可以考虑在执行事务时取出表锁以防止其他事务开始。这实际上与上面的代码(即序列化两个事务)相同,但是在数据库级别。显然,这样做可能会对性能产生影响。

答案 5 :(得分:0)

解决此特定问题的一种方法是确保每个单独的线程/实例以互斥的方式处理行。换句话说,如果实例1处理tx_email_address='test1'的行,则其他实例不应再次处理这些行。

这可以通过在实例启动时生成唯一的服务器ID并使用此服务器ID标记要处理的行来实现。做到这一点的方法是 -

<LOOP>

  1. 向员工表添加2列statusserver_id
  2. update employee set status='In Progress', server_id='<unique_id_for_instance>' where status='Uninitialized' and rownum<2
  3. 提交
  4. select * from employee where server_id='<unique_id_for_instance>' and status='In Progress'
  5. 处理在步骤4中选择的行。
  6. <END LOOP>

    遵循上述步骤顺序可确保所有VM实例获得要处理的不同行,并且没有死锁。在选择之前必须进行更新以使操作成为原子。反之亦然可能导致并发问题。

    希望这有帮助

答案 6 :(得分:-1)

一个经常使用的系统是拥有一个UUID(唯一通用ID)和UUIDGenerator的主键, 看到http://jug.safehaus.org/或类似的事情谷歌有很多答案

这将阻止发生唯一键约束

但是,这种情况只是你问题的一部分,你tx_email_address仍然必须是唯一的,没有什么可以解决这个问题。

没有办法阻止约束违规发生,只要你有并发性就会遇到它,而且这本身就没有问题。