我的IF条件中的漏洞在哪里?

时间:2015-01-17 19:41:49

标签: sql oracle if-statement plsql race-condition

Oracle数据库11g企业版11.2.0.3.0版 - 64位生产

我最近加入了一个项目,我的主要工作是清除大量积压的缺陷。其中一个缺陷是不必要的记录重复。我追查了罪魁祸首......

  1. 缺少对相应列的唯一约束 表
  2. 一个传统中PL / SQL中难以捉摸的 Heisenbug 执行INSERT的例程。
  3. 企业可以在开发和测试环境dbs中强制执行建议的唯一约束。但由于各种原因,在生产数据库中强制执行唯一约束是不可接受的。正因为如此,我还提出了一个解决方案,即在INSERT点引入一些辅助例程,用作« Big Burly Bouncers »。意图是这些提议的«网守»例程通过«记住»以编程方式防止重复“已经INSERT的内容然后只允许INSERT,如果具有当前id的记录不能被计算因为在前面提到的«记忆»。

    我已经对这些例程中的地狱进行了单元测试。我已经单独对它们进行了单元测试(每个单独测试)。我用« prevent_duplicates()»调用« already_exists()»例程对它们进行了单元测试。这两个我在纯PL / SQL和Java( using Spring's StoredProcedure abstraction )中进行了单元测试。我还在纯PL / SQL中进行了单元测试,原来的«遗产»例程重构为调用« prevent_duplicates()»,后者又调用« already_exists() »。在我的每个单元测试中,所有例程都成功地完成了我的期望。

    但是,只有当从webapp远程调用例程时,复制才能通过« prevent_duplicates()»进行IF检查。我在帖子的底部粘贴了一个堆栈跟踪片段。

    所以也许我的问题集中在我期待的事情上。也许我太接近这个问题了,因此我可能会做出一些天真的假设,那些新鲜的眼睛(和更多知识渊博的PL / SQLers )可能乍看之下......

    FUNCTION already_exists (
     p_main_thing IN lorem_ipsum.main_id%TYPE, -- NUMBER(10)
     p_type IN lorem_ipsum.entity_type%TYPE, -- VARCHAR(256) 
     p_location IN lorem_ipsum.another_id%TYPE, -- NUMBER(10)
     p_start IN lorem_ipsum.start_using%TYPE, -- DATE
     p_stop IN lorem_ipsum.stop_using%TYPE -- DATE NULLABLE
     ) RETURN NUMBER AS
      m_counter NUMBER := 0;
    BEGIN
    
    SELECT count(eg.pk_id) INTO m_counter
        FROM lorem_ipsum eg
        WHERE eg.main_id = p_main_thing
        AND eg.entity_type  = p_type
        AND eg.another_id  = p_location
        AND eg.start_using = p_start
        AND NVL(eg.stop_using, TRUNC(SYSDATE-1000000)) = NVL(p_stop, TRUNC(SYSDATE-    1000000));
        commit;
        IF m_counter > 0 THEN
          RETURN 1; -- TRUE
        ELSE
          RETURN 0; -- FALSE
        END IF; 
    
    END already_exists;
    
    
    ================================================================================
    PROCEDURE prevent_duplicates(
     p_main_thing IN lorem_ipsum.main_id%TYPE,
     p_type IN lorem_ipsum.entity_type%TYPE, 
     p_location IN lorem_ipsum.another_id%TYPE, 
     p_start IN lorem_ipsum.start_using%TYPE, 
     p_stop IN lorem_ipsum.stop_using%TYPE,
     p_new_pk_id OUT lorem_ipsum.pk_id%TYPE, -- NUMBER(10)
     p_memory IN OUT NOCOPY short_term_memory ) -- TYPE short_term_memory IS TABLE OF BOOLEAN INDEX BY PLS_INTEGER;
     IS
    
     m_new_pk_id lorem_ipsum.pk_id%TYPE;
    
     BEGIN
    
     IF ( already_exists(p_main_thing, p_type, p_location, p_start, p_stop ) = 0 ) THEN
       IF ( NOT p_memory.EXISTS( p_main_thing ) ) THEN
            m_new_pk_id := pk_id_seq.nextval; -- allowed in 11g ; but not in 10g or lower
            insert into lorem_ipsum (pk_id, entity_type, another_id, start_using,     stop_using, main_id) values (m_new_pk_id, p_type, p_location, p_start, p_stop,     p_main_thing);
            commit;
            p_memory(p_main_thing) := TRUE;
            -- return the new pk_id to the caller
            p_new_pk_id := m_new_pk_id;
      END IF;
     END IF;
    
    -- EXCEPTION
    -- ... trap ORA-00001/raise user-defined exception -20999
    
    END prevent_duplicates;
    
    ...
    org.hibernate.Session hibernate = ...
    ...
    hibernate.beginTransaction();
    String orginalLegacyRoutine = "{call myapp.original_legacy_routine("+parentId+",   666)}";
    hibernate.createSQLQuery(orginalLegacyRoutine).executeUpdate();
    hibernate.getTransaction().commit();
    ...
    hibernate.close
    ...
    

    ......这些是我在上述例程中编纂的一些假设......

    • 我假设如果SELECT在同一个Oracle事务中完成 前面的INSERT在几秒前执行,即SELECT 会成功地击中之前的INSERTed 记录 - 如果是SELECT的WHERE子句和INSERT中的值 values子句完全相同。
    • 我假设« prevent_duplicates()»两个IF语句与cahoots 关联数组,其中已插入记录的ID为“记忆”。
    • 我假设他们之间应该已经确定了什么 已被插入。
    • 我假设INSERT永远不会被调用两次 相同的价值观。
    • 我假设« prevent_duplicates()»IF测试阻止了这种情况。但他们不是。
    • 我假设SELECT& INSERT都在执行中 与调用例程相同的事务,因为原始的«遗产»例程是在事务边界内从Java调用的(参见上面的代码块

    另外,我正在重现开发和测试环境dbs上的复制缺陷,其中我是通过调用Web应用程序执行例程的唯一用户。事实上,我在办公室里一直都是在晚上和周末都很寂寞,试图解决这个问题。因此,以及我对Oracle对READ一致性的承诺的假设 - 我无法看到它与并发相关的方式。但我承认,我对Oracle的隔离级别的记忆有点模糊。尽管如此,我还是认为我被覆盖了。

    所以我需要帮助弄清楚我做了哪些不正确的假设。请?提前谢谢。

    PS 我无法通过调试器逐步执行PL / SQL,因为安全策略禁用了远程调试,而且我正在工作的商店还有其他功能。< / em>的

    ...
    INFO   | jvm 15   | 2015/01/16 20:00:51 | Jan 16, 2015 8:00:51 PM org.apache.catalina.core.ApplicationContext log
    INFO   | jvm 15   | 2015/01/16 20:00:51 | SEVERE: Exception while dispatching incoming RPC call
    INFO   | jvm 15   | 2015/01/16 20:00:51 | com.google.gwt.user.server.rpc.UnexpectedException: Service method 'public abstract     dba.stackexchange.com.questions.ask.MyApp dba.stackexchange.com.questions.ask.MyAppRPC.addLoremIpsum(dba.stackexchange.com.questions.ask.LoremIpsum,java.lang.String)' threw an unexpected exception: org.hibernate.QueryTimeoutException: could not execute native bulk manipulation query
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at com.google.gwt.user.server.rpc.RPC.encodeResponseForFailure(RPC.java:389)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at com.google.gwt.user.server.rpc.RPC.invokeAndEncodeResponse(RPC.java:579)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at com.google.gwt.user.server.rpc.RemoteServiceServlet.processCall(RemoteServiceServlet.java:208)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at com.google.gwt.user.server.rpc.RemoteServiceServlet.processPost(RemoteServiceServlet.java:248)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at com.google.gwt.user.server.rpc.AbstractRemoteServiceServlet.doPost(AbstractRemoteServiceServlet.java:62)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at javax.servlet.http.HttpServlet.service(HttpServlet.java:637)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at javax.servlet.http.HttpServlet.service(HttpServlet.java:717)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:290)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:233)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:191)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:127)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at com.googlecode.psiprobe.Tomcat60AgentValve.invoke(Tomcat60AgentValve.java:30)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:102)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:555)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:298)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at org.apache.jk.server.JkCoyoteHandler.invoke(JkCoyoteHandler.java:190)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at org.apache.jk.common.HandlerRequest.invoke(HandlerRequest.java:291)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at org.apache.jk.common.ChannelSocket.invoke(ChannelSocket.java:769)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at org.apache.jk.common.ChannelSocket.processConnection(ChannelSocket.java:698)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at org.apache.jk.common.ChannelSocket$SocketConnection.runIt(ChannelSocket.java:891)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at org.apache.tomcat.util.threads.ThreadPool$ControlRunnable.run(ThreadPool.java:690)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at java.lang.Thread.run(Thread.java:679)
    INFO   | jvm 15   | 2015/01/16 20:00:51 | Caused by: org.hibernate.QueryTimeoutException: could not execute native bulk manipulation query
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:124)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:66)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at org.hibernate.engine.query.NativeSQLQueryPlan.performExecuteUpdate(NativeSQLQueryPlan.java:219)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at org.hibernate.impl.SessionImpl.executeNativeUpdate(SessionImpl.java:1310)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at org.hibernate.impl.SQLQueryImpl.executeUpdate(SQLQueryImpl.java:396)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at dba.stackexchange.com.questions.ask.MyAppRPCImpl.addLoremIpsum(Unknown Source)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at java.lang.reflect.Method.invoke(Method.java:616)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at com.google.gwt.user.server.rpc.RPC.invokeAndEncodeResponse(RPC.java:561)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   ... 22 more
    INFO   | jvm 15   | 2015/01/16 20:00:51 | Caused by: java.sql.SQLException: ORA-20999: An attempt to insert lorem_ipsum.pk_id: 47396 violated DEDUPE_UNIQUE constraint with: main_id := 6459 , entity_type := FOO, another_id := 858, start_using := 04-JUL-08, stop_using :=
    INFO   | jvm 15   | 2015/01/16 20:00:51 | ORA-06512: at "SCOTT.MYAPP", line 504
    INFO   | jvm 15   | 2015/01/16 20:00:51 | ORA-06512: at "SCOTT.MYAPP", line 741
    INFO   | jvm 15   | 2015/01/16 20:00:51 | ORA-06512: at "SCOTT.MYAPP", line 538
    INFO   | jvm 15   | 2015/01/16 20:00:51 | ORA-06512: at line 1
    INFO   | jvm 15   | 2015/01/16 20:00:51 | 
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at oracle.jdbc.driver.T4CTTIoer.processError(T4CTTIoer.java:445)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at oracle.jdbc.driver.T4CTTIoer.processError(T4CTTIoer.java:396)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at oracle.jdbc.driver.T4C8Oall.processError(T4C8Oall.java:879)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at oracle.jdbc.driver.T4CTTIfun.receive(T4CTTIfun.java:450)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at oracle.jdbc.driver.T4CTTIfun.doRPC(T4CTTIfun.java:192)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at oracle.jdbc.driver.T4C8Oall.doOALL(T4C8Oall.java:531)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at oracle.jdbc.driver.T4CPreparedStatement.doOall8(T4CPreparedStatement.java:207)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at oracle.jdbc.driver.T4CPreparedStatement.executeForRows(T4CPreparedStatement.java:1044)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at oracle.jdbc.driver.OracleStatement.doExecuteWithTimeout(OracleStatement.java:1329)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at oracle.jdbc.driver.OraclePreparedStatement.executeInternal(OraclePreparedStatement.java:3584)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at oracle.jdbc.driver.OraclePreparedStatement.executeUpdate(OraclePreparedStatement.java:3665)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at oracle.jdbc.driver.OraclePreparedStatementWrapper.executeUpdate(OraclePreparedStatementWrapper.java:1352)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at com.mchange.v2.c3p0.impl.NewProxyPreparedStatement.executeUpdate(NewProxyPreparedStatement.java:105)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   at org.hibernate.engine.query.NativeSQLQueryPlan.performExecuteUpdate(NativeSQLQueryPlan.java:210)
    INFO   | jvm 15   | 2015/01/16 20:00:51 |   ... 30 more
    ...
    

1 个答案:

答案 0 :(得分:4)

您已经测试并确信自己(正确地说,就我所知)您所撰写的内容适用于单个会话。因此,问题必须来自同步会话的并发调用,因为您可能会从使用多个连接的多线程Web应用程序中获得。

你的逻辑中的漏洞是,如果从两个会话中调用例程,你仍然有竞争条件;您可以在支票和插页之间插入的一小段时间,例如:

Session A                     Session B
----------------------------  ----------------------------
calls prevent_duplicates()
                              calls prevent_duplicates()
calls already_exists()
gets zero (false)
                              calls already_exists()
                              gets zero (false)
checks p_memory.exists()
gets false
                              checks p_memory.exists()
                              gets false
performs insert
commits
                              performs insert
                              gets constraint violation

其他一些观察结果与您所遇到的问题没有直接关系......

您的p_memory支票并未在此处添加任何内容,因为其内容无论如何都是特定于会话的;如果插入在另一个会话中完成,你将无法看到它,并且因为插入将在它被放入集合时提交,所以即使它是跨会话也不会告诉你任何额外的

您似乎试图默默地阻止尝试插入副本。如果您被允许使用唯一约束 - 并且我假设这是由于某种原因您不能被允许的那个,并且您所显示的是开发/测试版本,其中约束是找到漏洞 - 您可以跳过already_existsp_memory检查,然后捕获并忽略(或记录)ORA-00001。关于catch-and-ignore与插入前检查的好处还有一个单独的争论,这个讨论更偏离主题......

但是如果没有唯一约束,则必须通过锁定整个表或者所有会话可以查看并尝试锁定的其他唯一令牌来手动序列化插入,这可能会影响性能。你将以一种效率较低的方式重新实现独特性。

即使是更偏离主题,你也不能更接近理解或修复原始的Heisenbug,你只是稍微处理它有潜在的副作用,或忽略潜在的副作用。如果我明白你在做什么,那就是。您似乎试图隐藏缺陷 - 正在插入重复项 - 而不是修复它。您尝试解决您的未知问题,这个问题本身可能与多会话相关,其方法也会受到多会话问题的影响 - 可能不那么频繁。