트랜잭션

Datastore는 트랜잭션을 지원합니다. 트랜잭션은 원자성(트랜잭션의 모든 작업이 발생하거나 발생하지 않음)인 하나의 작업 또는 작업 집합입니다. 애플리케이션은 단일 트랜잭션으로 여러 작업과 계산을 수행할 수 있습니다.

트랜잭션 사용

트랜잭션은 하나 이상의 항목에 대한 Datastore 작업 집합입니다. 각 트랜잭션에서는 원자성이 보장됩니다. 이는 트랜잭션의 일부분만 적용되는 경우는 없음을 의미합니다. 트랜잭션의 모든 작업이 적용되거나 하나도 적용되지 않거나, 둘 중 하나입니다. 트랜잭션의 최대 지속 시간은 60초이며, 30초 후에 10초의 유휴 만료 시간이 있습니다.

다음과 같은 경우 작업이 실패할 수 있습니다.

  • 동일한 항목 그룹에서 동시에 실행되는 수정 시도가 너무 많은 경우
  • 트랜잭션이 리소스 한도를 초과하는 경우
  • Datastore에 내부 오류가 발생하는 경우

이러한 모든 경우 Cloud Datastore API에서 예외가 발생합니다.

트랜잭션은 Datastore의 선택적 기능이므로 Datastore 작업을 수행하기 위해 반드시 트랜잭션을 사용할 필요는 없습니다.

다음은 이름이 JoeEmployee 종류의 항목에서 vacationDays라는 필드를 업데이트하는 예시입니다.

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Transaction txn = datastore.beginTransaction();
try {
  Key employeeKey = KeyFactory.createKey("Employee", "Joe");
  Entity employee = datastore.get(employeeKey);
  employee.setProperty("vacationDays", 10);

  datastore.put(txn, employee);

  txn.commit();
} finally {
  if (txn.isActive()) {
    txn.rollback();
  }
}

예시를 보다 간결하게 유지하기 위해 트랜잭션이 여전히 활성 상태인 경우 롤백을 수행하는 finally 블록을 생략하기도 합니다. 프로덕션 코드에서는 모든 트랜잭션이 명시적으로 커밋되거나 롤백되도록 만드는 것이 중요합니다.

항목 그룹

모든 항목은 하나의 트랜잭션에서 조작 가능한 하나 이상의 항목 집합인 항목 그룹에 속합니다. 항목 그룹 관계는 App Engine에 분산된 네트워크의 동일한 부분에 여러 항목을 저장하도록 지시합니다. 트랜잭션은 항목 그룹의 Datastore 작업을 설정하며, 모든 작업은 그룹으로 적용되거나 트랜잭션이 실패하는 경우 적용되지 않습니다.

애플리케이션은 항목을 만들 때 다른 항목을 새 항목의 상위 요소로 할당할 수 있습니다. 새 항목에 상위 요소를 할당하면 같은 항목 그룹의 새 항목이 상위 항목이 됩니다.

상위 요소가 없는 항목은 루트 항목입니다. 다른 항목의 상위 요소인 항목에도 상위 요소가 있을 수 있습니다. 항목에서 루트까지 상위 항목의 체인은 그 항목의 경로이며 경로의 구성원은 항목의 상위입니다. 항목의 상위 요소는 항목이 만들어질 때 정의되며 이후 변경될 수 없습니다.

루트 항목이 상위로 부여된 모든 항목은 같은 항목 그룹에 속합니다. 그룹의 모든 항목은 동일한 Datastore 노드에 저장됩니다. 하나의 트랜잭션에서 단일 그룹의 여러 항목을 수정하거나, 새 항목의 상위 요소를 그룹의 기존 항목으로 만들어 그룹에 새 항목을 추가할 수 있습니다. 다음 코드는 다양한 항목 유형의 트랜잭션을 보여줍니다.

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Entity person = new Entity("Person", "tom");
datastore.put(person);

// Transactions on root entities
Transaction txn = datastore.beginTransaction();

Entity tom = datastore.get(person.getKey());
tom.setProperty("age", 40);
datastore.put(txn, tom);
txn.commit();

// Transactions on child entities
txn = datastore.beginTransaction();
tom = datastore.get(person.getKey());
Entity photo = new Entity("Photo", tom.getKey());

// Create a Photo that is a child of the Person entity named "tom"
photo.setProperty("photoUrl", "http://domain.com/path/to/photo.jpg");
datastore.put(txn, photo);
txn.commit();

// Transactions on entities in different entity groups
txn = datastore.beginTransaction();
tom = datastore.get(person.getKey());
Entity photoNotAChild = new Entity("Photo");
photoNotAChild.setProperty("photoUrl", "http://domain.com/path/to/photo.jpg");
datastore.put(txn, photoNotAChild);

// Throws IllegalArgumentException because the Person entity
// and the Photo entity belong to different entity groups.
txn.commit();

특정 항목 그룹에 항목 만들기

애플리케이션이 새 항목을 만들 때 다른 항목의 키를 제공하여 항목 그룹에 할당할 수 있습니다. 아래 예시에서는 MessageBoard 항목의 키를 만든 다음 이 키를 사용하여 MessageBoard와 같은 항목 그룹에 위치하는 Message 항목을 만들고 지속합니다.

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

String messageTitle = "Some Title";
String messageText = "Some message.";
Date postDate = new Date();

Key messageBoardKey = KeyFactory.createKey("MessageBoard", boardName);

Entity message = new Entity("Message", messageBoardKey);
message.setProperty("message_title", messageTitle);
message.setProperty("message_text", messageText);
message.setProperty("post_date", postDate);

Transaction txn = datastore.beginTransaction();
datastore.put(txn, message);

txn.commit();

교차 그룹 트랜잭션 사용

교차 그룹 트랜잭션(XG 트랜잭션이라고도 함)은 여러 항목 그룹 간에 걸쳐 작동하며 위에 설명한 단일 그룹 트랜잭션과 동일하게 작동하지만 코드가 둘 이상의 항목 그룹에서 항목을 업데이트하려고 시도하더라도 실패하지 않습니다.

교차 그룹 트랜잭션 사용은 단일 그룹 트랜잭션 사용과 유사하지만 TransactionOptions를 사용하여 트랜잭션 시작 시 트랜잭션이 교차 그룹이 되도록 지정해야 합니다.

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
TransactionOptions options = TransactionOptions.Builder.withXG(true);
Transaction txn = datastore.beginTransaction(options);

Entity a = new Entity("A");
a.setProperty("a", 22);
datastore.put(txn, a);

Entity b = new Entity("B");
b.setProperty("b", 11);
datastore.put(txn, b);

txn.commit();

트랜잭션에서 가능한 작업

Datastore는 단일 트랜잭션 내부에서 가능한 작업을 제한합니다.

트랜잭션이 단일 그룹 트랜잭션인 경우 트랜잭션의 모든 Datastore 작업은 같은 항목 그룹의 항목에서 작동해야 합니다. 또는 트랜잭션이 교차 그룹 트랜잭션인 경우 최대 25개 항목 그룹의 항목에서 작동해야 합니다. 여기에는 상위 그룹별 항목 쿼리, 키별 항목 쿼리, 항목 업데이트, 항목 삭제가 포함됩니다. 각 루트 항목은 별도의 항목 그룹에 속하므로 교차 그룹 트랜잭션이 아닌 한 두 개 이상의 루트 항목에서 단일 트랜잭션을 생성 또는 작동할 수 없습니다.

2개 이상의 트랜잭션이 동시에 하나 이상의 공통된 항목 그룹에 있는 항목을 수정하려고 시도하면 변경 사항을 커밋한 첫 번째 트랜잭션만 성공하고 나머지 모든 항목은 커밋에 실패합니다. 설계상 항목 그룹을 사용하면 임의의 그룹 항목에 동시에 실행할 수 있는 쓰기 수가 제한됩니다 트랜잭션이 시작되면 Datastore에서는 트랜잭션에 사용되는 항목 그룹의 최종 업데이트 시간을 확인하여 최적의 동시 실행 제어를 수행합니다. 항목 그룹에 트랜잭션을 커밋하면 Datastore가 트랜잭션에 사용된 항목 그룹의 최종 업데이트 시간을 다시 확인합니다. 최초 확인 이후 변경된 경우 예외가 발생합니다.

앱은 상위 필터를 포함한 경우에만 트랜잭션 중에 쿼리를 수행할 수 있습니다. 또한 앱은 트랜잭션 중에 키별로 Datastore 항목을 가져올 수 있습니다. 트랜잭션 전에 키를 준비하거나 트랜잭션 내에서 키 이름 또는 ID를 사용하여 키를 빌드할 수 있습니다.

격리 및 일관성

트랜잭션 외에 Datastore의 격리 수준은 커밋된 읽기에 가장 근접합니다. 트랜잭션 내에서 직렬 가능한 격리가 적용됩니다. 즉, 다른 트랜잭션은 이 트랜잭션에서 읽거나 수정한 데이터를 동시에 수정할 수 없습니다.

트랜잭션의 모든 읽기에는 트랜잭션이 시작된 시점에 Datastore의 현재 일관된 상태가 반영됩니다. 트랜잭션 내 쿼리 및 조회를 통해 트랜잭션 시작 시점을 기준으로 Datastore의 일관된 단일 스냅샷을 확인할 수 있습니다. 트랜잭션 항목 그룹 내의 항목 및 색인 행은 완전히 업데이트되므로 트랜잭션 외부의 쿼리에서 발생할 수 있는 거짓 양성 또는 거짓 음성 없이 완전하고 정확한 결과 항목 집합이 반환됩니다.

이 일관된 스냅샷 뷰는 트랜잭션 내 쓰기 후 읽기까지도 확장됩니다. 대부분의 데이터베이스와 달리 Datastore 트랜잭션 내 쿼리 및 가져오기는 트랜잭션 내에서 이전에 발생한 쓰기 결과를 보여주지 않습니다. 특히 트랜잭션 내에서 항목이 수정되거나 삭제된 경우 쿼리 또는 가져오기를 수행하면 트랜잭션 시작 시점의 원래 버전 항목이 반환되거나, 그 당시에 항목이 존재하지 않았다면 아무것도 반환되지 않습니다.

트랜잭션 용도

이 예시에서는 트랜잭션의 용도 중 하나인 항목을 현재 값과 관련된 새로운 속성 값으로 업데이트하는 것을 보여줍니다. Datastore API는 트랜잭션을 다시 시도하지 않으므로 다른 요청이 동일한 MessageBoard 또는 그 Messages를 동시에 업데이트하는 경우 트랜잭션이 다시 시도되도록 하는 논리를 추가할 수 있습니다.

int retries = 3;
while (true) {
  Transaction txn = datastore.beginTransaction();
  try {
    Key boardKey = KeyFactory.createKey("MessageBoard", boardName);
    Entity messageBoard = datastore.get(boardKey);

    long count = (Long) messageBoard.getProperty("count");
    ++count;
    messageBoard.setProperty("count", count);
    datastore.put(txn, messageBoard);

    txn.commit();
    break;
  } catch (ConcurrentModificationException e) {
    if (retries == 0) {
      throw e;
    }
    // Allow retry to occur
    --retries;
  } finally {
    if (txn.isActive()) {
      txn.rollback();
    }
  }
}

여기서는 이 코드가 객체를 불러온 후, 수정된 객체를 저장하기 전에 다른 사용자에 의해 값이 업데이트될 수 있으므로 트랜잭션이 필요합니다. 트랜잭션이 없으면 사용자의 요청은 다른 사용자의 업데이트 전에 count 값을 사용하고 새 값을 덮어씁니다. 트랜잭션이 있으면 애플리케이션에 다른 사용자의 업데이트가 전달됩니다. 트랜잭션 중에 항목이 업데이트되는 경우 ConcurrentModificationException이 발생하고 트랜잭션이 실패합니다. 애플리케이션은 트랜잭션을 반복하여 새 데이터를 사용할 수 있습니다.

트랜잭션의 다른 일반적인 용도는 명명된 키로 항목을 가져오거나 아직 존재하지 않으면 생성하는 것입니다.

Transaction txn = datastore.beginTransaction();
Entity messageBoard;
Key boardKey;
try {
  boardKey = KeyFactory.createKey("MessageBoard", boardName);
  messageBoard = datastore.get(boardKey);
} catch (EntityNotFoundException e) {
  messageBoard = new Entity("MessageBoard", boardName);
  messageBoard.setProperty("count", 0L);
  boardKey = datastore.put(txn, messageBoard);
}
txn.commit();

이전과 마찬가지로 트랜잭션은 다른 사용자가 동일한 문자열 ID의 항목을 만들거나 업데이트하려 하는 경우를 처리해야 합니다. 트랜잭션이 없는 경우 항목이 없는 상태에서 사용자 두 명이 항목을 만들려고 하면 두 번째 항목이 첫 번째 생성 사실을 알지 못한 채 첫 번째 항목을 덮어씁니다. 트랜잭션에서 두 번째 시도는 원자적으로 실패합니다. 적절한 경우 애플리케이션은 다시 시도하여 항목을 불러와 업데이트할 수 있습니다.

트랜잭션이 실패하는 경우 성공할 때까지 재시도하도록 앱을 설정하거나 앱의 사용자 인터페이스 레벨에 전달하여 사용자가 오류에 대처하도록 할 수 있습니다. 모든 트랜잭션에서 재시도 루프를 생성할 필요는 없습니다.

마지막으로 트랜잭션을 사용하여 Datastore의 일관된 스냅샷을 읽을 수 있습니다. 이는 페이지를 렌더링하거나 일관성이 필요한 데이터를 내보내기 위해 여러 번의 읽기가 필요한 경우 유용할 수 있습니다. 이러한 종류의 트랜잭션은 쓰기를 수행하지 않으므로 보통 읽기 전용 트랜잭션이라고 합니다. 읽기 전용 단일 그룹 트랜잭션은 동시 수정으로 인해 실패하는 경우가 없으므로 실패 시 재시도를 구현할 필요가 없습니다. 그러나 교차 그룹 트랜잭션은 동시 수정으로 인해 실패할 수 있으므로 재시도를 포함해야 합니다. 읽기 전용 트랜잭션의 커밋과 롤백은 모두 노옵스(no-ops)입니다.

DatastoreService ds = DatastoreServiceFactory.getDatastoreService();

// Display information about a message board and its first 10 messages.
Key boardKey = KeyFactory.createKey("MessageBoard", boardName);

Transaction txn = datastore.beginTransaction();

Entity messageBoard = datastore.get(boardKey);
long count = (Long) messageBoard.getProperty("count");

Query q = new Query("Message", boardKey);

// This is an ancestor query.
PreparedQuery pq = datastore.prepare(txn, q);
List<Entity> messages = pq.asList(FetchOptions.Builder.withLimit(10));

txn.commit();

큐에 트랜잭션 태스크 추가

Datastore 트랜잭션의 일부로 큐에 태스크를 추가하면 트랜잭션이 성공적으로 커밋되는 경우에만 큐에 태스크가 추가됩니다. 트랜잭션이 커밋되면 태스크가 큐에 추가됩니다. 대기열에 추가된 경우 작업은 즉시 실행이 보장되지 않으며 작업 실행 내에 수행되는 모든 작업은 원래의 트랜잭션으로부터 독립적입니다. 작업은 성공할 때까지 재시도됩니다. 이는 트랜잭션 컨텍스트에서 큐에 추가된 모든 태스크에 적용됩니다.

트랜잭션 태스크를 사용하면 Datastore 이외의 태스크를 Datastore 트랜잭션에 등록할 수 있으므로 유용합니다(예를 들어 구매 확인을 위해 이메일 전송). 또한 트랜잭션이 성공하는 경우에만 트랜잭션 외부의 항목 그룹에 대한 변경을 커밋할 수 있는 것처럼 Datastore 태스크를 트랜잭션에 연결할 수 있습니다.

애플리케이션은 단일 트랜잭션 중에 태스크 큐트랜잭션 태스크를 5개를 초과하여 삽입할 수 없습니다. 트랜잭션 태스크의 이름은 사용자가 지정한 이름이 아니어야 합니다.

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Queue queue = QueueFactory.getDefaultQueue();
Transaction txn = datastore.beginTransaction();
// ...

queue.add(txn, TaskOptions.Builder.withUrl("/path/to/handler"));

// ...

txn.commit();