【问题标题】:Concurrent process inserting data in database并发进程在数据库中插入数据
【发布时间】:2010-11-10 15:21:45
【问题描述】:

考虑 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 的应用程序,其唯一目的是将数据写入目标数据库。并且可能有并发进程这样做,并且某些进程可能会尝试写入相同的数据的可能性很高(如上例所示)。

【问题讨论】:

  • 为什么会出现这样的问题?如果您将应用程序设计为多个线程/进程可以插入相同的数据,那么您希望其他事务失败。

标签: java postgresql jdbc transactions


【解决方案1】:

一个常用的系统是有一个主键,它是一个 UUID(唯一通用 ID)和一个 UUIDGenerator, 见http://jug.safehaus.org/或类似的东西谷歌有很多答案

这将防止唯一键约束发生

但是,offcourse 只是你问题的一部分,你的 tx_email_address 仍然必须是唯一的,没有什么能解决这个问题。

没有办法防止违反约束的发生,只要你有并发就会遇到它,这本身就没有问题。

【讨论】:

  • 这里的问题不是入口的唯一标识,而是冗余数据。同样,这个过程涉及为几乎 12 个不同的表格编写数据,这些表格后来用于生成报告和其他内容。这种冗余数据可能会导致严重的问题。此外,该应用程序已经处于生产阶段,我们试图解决的唯一问题是并发写入场景,这再次非常关键,因为该应用程序或多或少是数据驱动的。在这一点上进行设计更改可以打开新的蠕虫罐。有什么解决方案可以在 DB 端解决这个问题。
【解决方案2】:

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

【讨论】:

    【解决方案3】:

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

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

    这样可以防止一个线程在另一个线程查询和插入表时查询表。当第一个线程完成时,第二个线程进入同步块。但是,此时每次选择尝试都会返回数据,因此线程不会尝试插入数据。

    编辑

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

    【讨论】:

    • 只有在所有事务都在同一个虚拟机中启动时才有效如果你有多个客户端同步没有任何用处
    • 这是一个公平的观点。但是,我通常会设计一个系统,以便一个进程是数据的保管人并负责插入数据(尽管我有时允许多个读取器)。
    • 我的作者是POJO webservice,同样可以集群。所以这个选项不可用。有什么可以在数据库级别完成的吗?
    • @Salman:查看我最近的编辑。不过,您必须查看 Postgres 文档以了解如何准确获取锁。
    【解决方案4】:

    最简单的方法似乎是使用事务隔离级别“可序列化”,它可以防止幻读(其他人在您的事务期间插入满足先前 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.

    【讨论】:

      【解决方案5】:

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

      如果做不到这一点,我会在每次插入后捕获并忽略“唯一键约束违规”。当然,记录它的发生可能仍然是一个好主意。

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

      • 在中转表上执行更新语句,将多行的“中转状态”设置为“正在进行中”。这个数字有多大,或者是否所有当前的新行都被标记需要考虑一下。
      • 执行一条更新语句,将“transit status”设置为“duplicate”,对所有数据已在“employee”表中且“transit status”不在(“duplicate”、“processed”)中的行进行设置李>
      • 只要中转表中有“transit status”=“work in progress”的行就重复:
        • 从“transit status”=“work in progress”的中转表中选择一行。
        • 将该行数据插入“员工”表。
        • 将此行“运输状态”设置为“已处理”。
        • 使用与当前处理的行相同的数据更新中转表中的所有行,并将“中转状态”=“进行中的工作”更新为“中转状态”=“重复”。

      我很可能希望另一个工作定期删除(“重复”,“已处理”)中带有“运输状态”的行

      如果 postgres 不知道数据库作业,则可以使用 os 端作业。

      【讨论】:

        【解决方案6】:

        解决此特定问题的一种方法是确保每个单独的线程/实例以互斥方式处理行。换句话说,如果实例 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 中选择的行。

        <END LOOP>

        按照上述步骤顺序可确保所有 VM 实例获得不同的行来处理,并且不会出现死锁。必须在选择之前进行更新以使操作具有原子性。反过来可能会导致并发问题。

        希望对你有帮助

        【讨论】:

          【解决方案7】:

          一种解决方案是使用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
          

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

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2013-12-27
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-11-19
            • 2015-10-18
            相关资源
            最近更新 更多