проблема изоляции транзакций или неправильный подход? - PullRequest
7 голосов
/ 29 сентября 2008

Я помогал некоторым моим коллегам с проблемой SQL. В основном они хотели переместить все строки из таблицы A в таблицу B (обе таблицы имеют одинаковые столбцы (имена и типы)). Хотя это было сделано в Oracle 11g, я не думаю, что это действительно имеет значение.

Их первоначальная наивная реализация была похожа на

BEGIN
  INSERT INTO B SELECT * FROM A
  DELETE FROM A
  COMMIT;
END

Их беспокоило, были ли INSERT, сделанные в таблицу A во время копирования из A в B, и «DELETE FROM A» (или TRUNCATE для того, что стоило) вызвало бы потерю данных (удаление новых вставленных строк в A).

Конечно, я быстро порекомендовал сохранить идентификаторы скопированных строк во временной таблице, а затем удалить только те строки в A, которые соответствуют IDS во временной таблице.

Однако для любопытства мы поставили небольшой тест, добавив команду ожидания (не помню синтаксис PL / SQL) между INSERT и DELETE. Затем из другого соединения мы вставили бы строки ВО ВРЕМЯ ОЖИДАНИЯ .

Мы заметили, что это потеря данных. Я воспроизвел весь контекст в SQL Server и обернул все это в транзакцию, но все же свежие новые данные также были потеряны в SQL Server. Это заставило меня думать, что в первоначальном подходе есть систематическая ошибка / недостаток.

Однако я не могу сказать, был ли это тот факт, что TRANSACTION не был (каким-то образом?) Изолирован от новых новых INSERT или тот факт, что INSERT пришли во время команды WAIT.

В конце концов, это было реализовано с использованием предложенной мной временной таблицы, но мы не смогли получить ответ на вопрос «Почему потеря данных». Ты знаешь почему?

Ответы [ 11 ]

8 голосов
/ 29 сентября 2008

В зависимости от уровня изоляции, выбор всех строк в таблице не запрещает новые вставки, а просто блокирует прочитанные вами строки. В SQL Server, если вы используете уровень изоляции Serializable, он предотвратит появление новых строк, если бы они были включены в ваш запрос select.

http://msdn.microsoft.com/en-us/library/ms173763.aspx -

SERIALIZABLE Определяет следующее:

  • Операторы не могут читать данные, которые были изменены, но еще не зафиксированы другими транзакциями.

  • Никакие другие транзакции не могут изменять данные, считанные текущей транзакцией, до тех пор, пока текущая транзакция не завершится.

  • Другие транзакции не могут вставлять новые строки со значениями ключей, которые попадают в диапазон ключей, считываемых любыми инструкциями в текущей транзакции, пока текущая транзакция не завершится.

7 голосов
/ 29 сентября 2008

Я не могу говорить о стабильности транзакции, но альтернативным подходом было бы удаление второго шага из исходной таблицы, где она существует (выберите идентификаторы из целевой таблицы).

Простите за синтаксис, я не тестировал этот код, но вы должны быть в состоянии понять:

INSERT INTO B SELECT * FROM A;

DELETE FROM A WHERE EXISTS (SELECT B.<primarykey> FROM B WHERE B.<primarykey> = A.<primarykey>);

Таким образом, вы используете реляционный движок, чтобы гарантировать, что более новые данные не будут удалены, и вам не нужно выполнять два шага в транзакции.

Обновление: исправлен синтаксис в подзапросе

5 голосов
/ 29 сентября 2008

Этого можно добиться в Oracle, используя:

Alter session set isolation_level=serializable;

Это может быть установлено в PL / SQL с помощью EXECUTE IMMEDIATE:

BEGIN
    EXECUTE IMMEDIATE 'Alter session set isolation_level=serializable';
    ...
END;

См. Спросите Тома: об уровнях изоляции транзакций

2 голосов
/ 29 сентября 2008

Это просто способ работы транзакций. Вы должны выбрать правильный уровень изоляции для поставленной задачи.

Вы делаете INSERT и DELETE в одной транзакции. Вы не упоминаете, что транзакция в режиме изоляции использует, но, вероятно, она «прочитана». Это означает, что команда DELETE увидит записи, которые были зафиксированы за это время. Для такой работы гораздо лучше использовать транзакцию типа «моментальный снимок», потому что тогда и INSERT, и DELETE будут знать об одном и том же наборе записей - только о них и ничего больше.

1 голос
/ 30 сентября 2008

В Oracle уровень изоляции транзакции по умолчанию считывается зафиксированным. По сути, это означает, что Oracle возвращает результаты в том виде, в котором они существовали в SCN (номер изменения системы), когда ваш запрос начинался. Установка уровня изоляции транзакции для сериализуемого означает, что SCN перехватывается в начале транзакции, поэтому все запросы в вашей транзакции возвращают данные с этого SCN. Это обеспечивает стабильные результаты независимо от того, что делают другие сеансы и транзакции. С другой стороны, может оказаться, что Oracle может решить, что не сможет сериализовать вашу транзакцию из-за активности, выполняемой другими транзакциями, поэтому вам придется обрабатывать ошибки такого рода.

Ссылка Тони на обсуждение AskTom дает гораздо более подробную информацию обо всем этом - я настоятельно рекомендую это.

1 голос
/ 29 сентября 2008

Вам необходимо установить уровень изоляции транзакции, чтобы вставки из другой транзакции не влияли на вашу транзакцию. Я не знаю, как это сделать в Oracle.

1 голос
/ 29 сентября 2008

я не знаю, относится ли это к делу, но в SQL Server синтаксис

begin tran
....
commit

не просто «начать»

0 голосов
/ 04 августа 2015
    I have written a sample code:-

    First run this on Oracle DB:-


     Create table AccountBalance
        (
              id integer Primary Key,
              acctName varchar2(255) not null,
              acctBalance integer not null,
              bankName varchar2(255) not null
        );

        insert into AccountBalance values (1,'Test',50000,'Bank-a');

    Now run the below code 





 package com.java.transaction.dirtyread;
        import java.sql.Connection;
        import java.sql.DriverManager;
        import java.sql.SQLException;

        public class DirtyReadExample {

         /**
          * @param args
         * @throws ClassNotFoundException 
          * @throws SQLException 
          * @throws InterruptedException 
          */
         public static void main(String[] args) throws ClassNotFoundException, SQLException, InterruptedException {

             Class.forName("oracle.jdbc.driver.OracleDriver");
             Connection connectionPayment = DriverManager.getConnection(
                        "jdbc:oracle:thin:@localhost:1521:xe", "hr",
                        "hr");
             Connection connectionReader = DriverManager.getConnection(
                        "jdbc:oracle:thin:@localhost:1521:xe", "hr",
                        "hr");

          try {
              connectionPayment.setAutoCommit(false);
              connectionPayment.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);


          } catch (SQLException e) {
           e.printStackTrace();
          }


          Thread pymtThread=new Thread(new PaymentRunImpl(connectionPayment));
          Thread readerThread=new Thread(new ReaderRunImpl(connectionReader));

          pymtThread.start();
          Thread.sleep(2000);
          readerThread.start();

         }

        }



        package com.java.transaction.dirtyread;

        import java.sql.Connection;
        import java.sql.PreparedStatement;
        import java.sql.ResultSet;
        import java.sql.SQLException;

        public class ReaderRunImpl  implements Runnable{

         private Connection conn;

         private static final String QUERY="Select acctBalance from AccountBalance where id=1";

         public ReaderRunImpl(Connection conn){
          this.conn=conn;
         }

         @Override
         public void run() {
          PreparedStatement stmt =null; 
          ResultSet rs =null;

          try {
           stmt = conn.prepareStatement(QUERY);
           System.out.println("In Reader thread --->Statement Prepared");
           rs = stmt.executeQuery();
           System.out.println("In Reader thread --->executing");
           while (rs.next()){

            System.out.println("Balance is:" + rs.getDouble(1));

           }
           System.out.println("In Reader thread --->Statement Prepared");
           Thread.sleep(5000);
           stmt.close();
           rs.close();
           stmt = conn.prepareStatement(QUERY);
           rs = stmt.executeQuery();
           System.out.println("In Reader thread --->executing");
           while (rs.next()){

            System.out.println("Balance is:" + rs.getDouble(1));

           }
           stmt.close();
           rs.close();
           stmt = conn.prepareStatement(QUERY);
           rs = stmt.executeQuery();
           System.out.println("In Reader thread --->executing");
           while (rs.next()){

            System.out.println("Balance is:" + rs.getDouble(1));

           }
          } catch (SQLException | InterruptedException e) {
           e.printStackTrace();
          }finally{
           try {
            stmt.close();
            rs.close();
           } catch (SQLException e) {
            e.printStackTrace();
           }   
          }
         }

        }

        package com.java.transaction.dirtyread;
        import java.sql.Connection;
        import java.sql.PreparedStatement;
        import java.sql.SQLException;

        public class PaymentRunImpl implements Runnable{

         private Connection conn;

         private static final String QUERY1="Update AccountBalance set acctBalance=40000 where id=1";
         private static final String QUERY2="Update AccountBalance set acctBalance=30000 where id=1";
         private static final String QUERY3="Update AccountBalance set acctBalance=20000 where id=1";
         private static final String QUERY4="Update AccountBalance set acctBalance=10000 where id=1";

         public PaymentRunImpl(Connection conn){
          this.conn=conn;
         }

         @Override
         public void run() {
          PreparedStatement stmt = null;

          try {   
           stmt = conn.prepareStatement(QUERY1);
           stmt.execute();
           System.out.println("In Payment thread --> executed");
           Thread.sleep(3000);
           stmt = conn.prepareStatement(QUERY2);
           stmt.execute();
           System.out.println("In Payment thread --> executed");
           Thread.sleep(3000);
           stmt = conn.prepareStatement(QUERY3);
           stmt.execute();
           System.out.println("In Payment thread --> executed");
           stmt = conn.prepareStatement(QUERY4);
           stmt.execute();
           System.out.println("In Payment thread --> executed");

           Thread.sleep(5000);
            //case 1
           conn.rollback();
           System.out.println("In Payment thread --> rollback");
          //case 2
           //conn.commit();
          // System.out.println("In Payment thread --> commit");
          } catch (SQLException e) {
           e.printStackTrace();
          } catch (InterruptedException e) {    
           e.printStackTrace();
          }finally{
           try {
            stmt.close();
           } catch (SQLException e) {
            e.printStackTrace();
           }
          }
         }

        }

    Output:-
    In Payment thread --> executed
    In Reader thread --->Statement Prepared
    In Reader thread --->executing
    Balance is:50000.0
    In Reader thread --->Statement Prepared
    In Payment thread --> executed
    In Payment thread --> executed
    In Payment thread --> executed
    In Reader thread --->executing
    Balance is:50000.0
    In Reader thread --->executing
    Balance is:50000.0
    In Payment thread --> rollback

Вы можете проверить это, вставив новые строки, как определено оракулом: - Фантомное считывание происходит, когда транзакция A извлекает набор строк, удовлетворяющих заданному условию, транзакция B впоследствии вставляет или обновляет строку так, что строка теперь соответствует условию в транзакции A, а транзакция A позже повторяет условное извлечение. Транзакция A теперь видит дополнительную строку. Этот ряд называется фантомом. Это позволит избежать вышеописанного сценария, так как я использовал TRANSACTION_SERIALIZABLE. Это установит самую строгую блокировку на Oracle. Oracle поддерживает только два типа уровней изоляции транзакций: - TRANSACTION_READ_COMMITTED и TRANSACTION_SERIALIZABLE.

0 голосов
/ 13 июля 2009

Кроме того, вы можете использовать изоляцию моментального снимка для обнаружения потерянных обновлений:

Когда изоляция снимка помогает и когда болит

0 голосов
/ 30 сентября 2008

Это стандартное поведение режима фиксации чтения по умолчанию, как упомянуто выше. Команда WAIT просто вызывает задержку обработки, нет ссылки на обработку транзакций БД.

Чтобы устранить проблему, вы можете:

  1. установите уровень изоляции на сериализуемый, но тогда вы можете получить ошибки ORA, которые вам необходимо обработать при повторных попытках! Кроме того, вы можете получить серьезный удар по производительности.
  2. сначала использовать временную таблицу для хранения значений
  3. если данные не слишком велики, чтобы уместиться в память, вы можете использовать предложение RETURNING, чтобы НАЛИЧНО СОБИРАТЬ В вложенную таблицу и удалять, только если строка присутствует во вложенной таблице.
...