проблема с изоляцией транзакций или неправильный подход?

StackOverflow https://stackoverflow.com/questions/150177

Вопрос

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

Их первоначальная наивная реализация была чем-то вроде

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

Их беспокоило, были ли сделаны вставки в таблицу A во время копирования из A в B, и "УДАЛИТЬ ИЗ A" (или ОБРЕЗАТЬ, если это того стоило) привело бы к потере данных (если бы новые вставленные строки были удалены).

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

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

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

Однако я не могу сказать, было ли это из-за того, что ТРАНЗАКЦИЯ не была (каким-то образом?) изолирована от свежих новых вставок или из-за того, что вставки пришли во время команды WAIT.

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

Это было полезно?

Решение

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

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

SERIALIZABLE Указывает следующее:

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

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

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

Другие советы

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

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

INSERT INTO B SELECT * FROM A;

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

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

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

Это может быть достигнуто в Oracle с помощью:

Alter session set isolation_level=serializable;

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

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

Видишь Спроси Тома:Об Уровнях изоляции транзакций

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

Вы выполняете ВСТАВКУ и УДАЛЕНИЕ в одной транзакции.Вы не упоминаете, какой режим изоляции использует транзакция, но, вероятно, это "зафиксированное чтение".Это означает, что команда УДАЛЕНИЯ увидит записи, которые были зафиксированы за это время.Для такого рода заданий гораздо лучше использовать транзакцию типа "моментальный снимок", потому что тогда и INSERT, и DELETE будут знать об одном и том же наборе записей - только их и ничего больше.

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

begin tran
....
commit

не просто "начать"

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

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

Ссылка Тони на обсуждение AskTom содержит значительно больше деталей обо всем этом - я настоятельно рекомендую это сделать.

Да, Милан, я не указал уровень изоляции транзакции.Я полагаю, что это уровень изоляции по умолчанию, который я не знаю, что это такое.Ни в Oracle 11g, ни в SQL Server 2005.

Кроме того, ВСТАВКА, которая была сделана во время команды ОЖИДАНИЯ (при 2-м соединении), была НЕТ внутри транзакции.Должно ли это было быть сделано для предотвращения потери данных?

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

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

  1. установите уровень изоляции на serializable, но тогда вы можете получить ORA-ошибки, которые вам нужно обрабатывать с помощью повторных попыток!Кроме того, вы можете получить серьезное снижение производительности.
  2. сначала используйте временную таблицу для сохранения значений
  3. если данные не слишком велики, чтобы поместиться в памяти, вы можете использовать предложение RETURNING для МАССОВОГО СБОРА Во вложенную таблицу и удаления только в том случае, если строка присутствует во вложенной таблице.

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

Когда Изоляция моментальных снимков Помогает, а когда причиняет боль

    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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top