sql - 我的中频条件在哪里?

标签 sql oracle if-statement plsql race-condition

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

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

  • 缺少对相应列的唯一约束
  • 在一个遗留点中,PL / SQL中难以捉摸的Heisenbug
    执行INSERT的例程。

  • 企业可以在开发和测试环境数据库中强制实施建议的唯一约束。但是由于各种原因,在生产数据库中强制执行唯一约束是不可接受的。因此,我还建议引入一些辅助例程作为解决方案,这些例程旨在在INSERT点充当“Big Burly Bouncers”。这些拟议的“关守”例程旨在通过“记住”已插入的内容以编程方式防止重复,然后仅在上述“内存”中无法解释具有当前ID的记录时才允许插入。

    我已经对这些例程进行了单元测试。我已经对它们进行了独立的单元测试(每个都单独测试)。我已经用«prevent_duplicates()»调用了«already_exists()»例程对它们进行了单元测试。我已经在纯PL / SQL和Java(using Spring's StoredProcedure abstraction)中对这两个模块进行了测试。我还在纯PL / SQL中对原始的“旧版”例程进行了单元测试,该例程被重构为调用“prevent_duplicates()”,而后者又调用了“already_exists()”。在我的每一个单元测试中,所有例程都能成功完成我所期望的工作。

    但是,只有从Web应用程序远程调用例程时,重复项才能通过«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
    ...
    

    ...这些是我在上述例程中整理的一些假设...
  • 我假设如果在同一个Oracle事务中完成SELECT
    前面的INSERT是在几秒钟前执行的,SELECT
    本来可以成功打败以前插入的
    记录-如果SELECT的WHERE子句和INSERT的子句中的值
    values子句完全相同。
  • 我假设«prevent_duplicates()»两个IF语句与
    关联数组-已插入的记录的ID被“存储”。
  • 我假设他们之间应该已经确定了
    已经被插入。
  • 我假设INSERT永远不会被调用两次
    相同的值。
  • 我假设«prevent_duplicates()»IF测试阻止了这种情况。但事实并非如此。
  • 我假设SELECT和INSERT都在
    与调用例程相同的事务,因为原始的“旧式”例程是在事务边界内从Java调用的(请参见上面的代码块)

  • 另外,我正在开发和测试环境数据库上复制重复缺陷,在该数据库中,我是通过调用Web应用程序执行例程的唯一的用户。的确,我在傍晚和周末都在办公室里独自一人,试图解决这个问题。因此,由于这个原因(以及我对Oracle promise 的READ一致性的假设),我看不到它如何与并发相关。但是,我要承认,我对Oracle隔离级别的记忆有些模糊。尽我的最大记忆,我以为自己被覆盖了。

    所以我需要帮助弄清楚我所做的错误假设。请?先感谢您。

    P.S. 我无法使用调试器来逐步完成PL / SQL,因为由于安全策略以及我正在工作的商店没有执行任何操作而禁用了远程调试。
    ...
    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
    ...
    

    最佳答案

    您已经测试并确信自己(据我所知正确),您编写的内容适用于单个会话。因此,问题一定来自并发会话中的并发调用,就像您可能是从使用多个连接的多线程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检查实际上并未在此处添加任何内容,因为其内容始终是特定于会话的;如果插入是在另一个会话中完成的,则您将看不到它,并且由于插入将在插入集合中时提交,因此即使是跨会话也不会告诉您任何其他信息。

    似乎您正在尝试静默阻止尝试插入重复项。如果允许您使用唯一约束-并且我假设这是出于某种原因而不允许在现场发布的约束,并且您所显示的是在其中可以找到漏洞的约束的dev / test版本-您可以跳过already_existsp_memory检查,而只是捕获并忽略(或记录)ORA-00001。关于捕捉和忽略与插入前检查的好处还有另外的争论,这将使话题变得更加离题。

    但是没有唯一约束,您必须通过锁定整个表或所有会话可以看到并尝试锁定的某些其他唯一令牌来手动序列化插入,这很可能会影响性能。您将以一种不太有效的方式重新实现唯一性。

    甚至更离题,您也无法进一步了解或修复原始的Heisenbug,您只是在潜在的副作用方面对其进行了更好的处理,或者在潜在的副作用方面对其进行了忽略。如果我了解您在做什么,那就是。您似乎正在试图隐藏一个缺陷-正在插入重复项-而不是修复它。您正在尝试使用一种也可能会遇到多会话问题的方法来解决可能与多会话有关的未知问题,而这种方法可能不那么频繁。

    关于sql - 我的中频条件在哪里?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28003492/

    相关文章:

    php - 如果不存在且没有唯一键,则插入记录

    php - 如何链接到搜索结果中主题的正确页面?

    sql - 从字符串中删除特定单词

    c# - 如何将空值传递给sql中的存储过程

    javascript - 如何在 oracle apex 的动态操作中从 PLSQL 代码内部调用 Javascript

    javascript - 如何使用 jquery 连续检查输入的值?

    python - 评估列表python中的变量

    php - 如何仅存储月份和年份并将所有日期的日期默认设置为 1?

    sql - 计算列在 postgres 中更改为特定值的位置

    c++ - 警告C6236:(||)始终为非零常量