위키백과에 따르면 데이터베이스 관리 시스템의 격리 수준은 '한 작업의 변경사항을 다른 동시 작업에서 볼 수 있는 방법과 시기를 정의'합니다.
이 문서의 목표는 App Engine에서 사용하는 Cloud Datastore의 쿼리 및 트랜잭션 격리를 설명하는 것입니다. 이 문서를 읽고 나면 트랜잭션 내부 및 외부에서 동시 읽기와 쓰기 작동 방식을 더 잘 이해할 수 있습니다.
트랜잭션 내부: 직렬화 가능
가장 강한 격리부터 가장 약한 격리 순서로 4개의 격리 수준은 직렬화 가능, 반복 가능 읽기, 커밋된 읽기, 커밋되지 않은 읽기입니다. Datastore 트랜잭션은 직렬화 가능 격리 수준을 충족합니다. 각 트랜잭션은 다른 모든 Datastore 트랜잭션 및 작업과 완전히 격리됩니다.
주어진 항목 그룹의 트랜잭션은 차례에 따라 연속적으로 실행됩니다.
자세한 내용은 트랜잭션 문서의 격리 및 일관성 섹션과 스냅샷 격리에 대한 위키백과 문서를 참조하세요.
트랜잭션 외부: 커밋된 읽기
트랜잭션 외부의 Datastore 작업은 커밋된 읽기 수준과 유사합니다. 쿼리 또는 가져오기로 Datastore에서 검색된 항목은 커밋된 데이터만 봅니다. 검색된 항목은 부분적으로 커밋된 데이터(커밋 이전과 이후의 일부 데이터)를 갖지 않습니다. 그러나 쿼리와 트랜잭션 간의 상호작용은 그렇게 단순하지 않습니다. 이를 이해하려면 커밋 프로세스를 더 자세히 살펴봐야 합니다.
커밋 프로세스
커밋이 성공적으로 반환되면 트랜잭션 적용이 보장되지만 이것이 쓰기의 결과가 리더에 즉시 표시된다는 의미는 아닙니다. 트랜잭션 적용은 다음의 두 마일스톤으로 구성됩니다.
마일스톤 A - 항목에 대한 변경사항이 적용된 지점
마일스톤 B - 해당 항목의 색인에 대한 변경사항이 적용된 지점
Cloud Datastore에서 트랜잭션은 일반적으로 커밋이 반환된 후 수백 밀리초 내에 완전히 적용됩니다. 완전히 적용되지 않은 경우에도 후속 읽기, 쓰기, 상위 쿼리는 항상 커밋의 결과를 반영합니다. 이러한 작업은 실행 전에 모든 미해결 수정을 적용하기 때문입니다. 단, 여러 항목 그룹에 걸친 쿼리는 실행 전에 해결되지 않은 수정사항이 있는지 여부를 확인할 수 없으며 비활성 결과 또는 일부만 적용된 결과를 반환할 수 있습니다.
마일스톤 A 이후 시점에서 업데이트된 항목을 키로 조회하는 요청은 해당 항목의 최신 버전을 보게 됩니다. 그러나 동시 요청에서 조건자(SQL/GQL 사용자를 위한 WHERE 절)가 업데이트 전 항목에 의해 충족되지 않지만 업데이트 후 항목에 의해 충족되는 쿼리를 실행하는 경우 쿼리가 적용 작업이 마일스톤 B에 도달한 후에 실행된 경우에 한해 결과 집합의 일부로 항목이 포함됩니다.
즉, 키에 의한 조회 결과 속성이 쿼리 조건자를 충족하는 항목이 짧은 기간 동안 결과 집합에 포함되지 않을 가능성이 있습니다. 또한 마찬가지로 키에 의한 조회 결과 속성이 쿼리 조건자를 충족하지 않는 항목이 결과 집합에 포함될 가능성도 있습니다. 쿼리는 반환할 항목을 결정할 때 마일스톤 A와 마일스톤 B 사이에 있는 트랜잭션을 고려할 수 없습니다. 비활성 데이터에 대해 수행되지만 반환된 키에 대한 get() 작업은 항상 해당 항목의 최신 버전을 가져옵니다. 즉, 쿼리와 일치하는 결과가 누락되거나 해당 항목을 가져온 후 일치하지 않는 결과를 얻을 수 있습니다.
Cloud Datastore의 모든 상위 쿼리와 같이 쿼리가 실행되기 전에 대기중인 모든 수정사항이 완전히 적용되는 시나리오가 있습니다. 이 경우 쿼리 결과는 항상 최신, 일관적 상태로 유지됩니다.
예시
동시 업데이트와 쿼리가 상호작용하는 방식을 개략적으로 설명했지만 구체적인 예시를 보면 이러한 개념을 더 쉽게 이해할 수 있습니다. 몇 가지를 살펴보겠습니다. 간단한 예시부터 시작한 다음 더 흥미로운 예시로 마무리하겠습니다.
Person 항목을 저장하는 애플리케이션이 있다고 가정해 보겠습니다. Person에는 다음과 같은 속성이 있습니다.
이름
Height
이 애플리케이션은 다음 작업을 지원합니다.
updatePerson()
getTallPeople(): 신장이 72인치를 초과하는 모든 사람을 반환합니다.
Datastore에 2개의 Person 항목이 있습니다.
Adam, 신장 68인치
Bob, 신장 73인치
예시 1 - Adam 키 늘리기
애플리케이션이 사실상 동시에 2개의 요청을 받는다고 가정해 보겠습니다.
첫 번째 요청은 Adam의 신장을 68인치에서 74인치로 업데이트합니다.
급성장이군요! 두 번째 요청은 getTallPeople()을 호출합니다. getTallPeople()은 무엇을 반환할까요?
답은 요청 1에 의해 트리거된 두 커밋 마일스톤과 요청 2에 의해 실행된 getTallPeople() 쿼리 사이의 관계에 따라 달라집니다.
다음과 같이 가정해 보겠습니다.
요청 1, put()
요청 2, getTallPeople()
요청 1, put()-->commit()
요청 1, put()-->commit()-->마일스톤 A
요청 1, put()-->commit()-->마일스톤 B
이 시나리오에서 getTallPeople()은 Bob만 반환합니다. 왜일까요? Adam의 키를 늘리는 업데이트가 아직 커밋되지 않았고, 따라서 요청 2에서 실행한 쿼리가 아직 변경사항을 볼 수 없기 때문입니다.
이제 다음과 같이 가정해 보겠습니다.
요청 1, put()
요청 1, put()-->commit()
요청 1, put()-->commit()-->마일스톤 A
요청 2, getTallPeople()
요청 1, put()-->commit()-->마일스톤 B
이 시나리오에서 쿼리는 요청 1이 마일스톤 B에 도달하기 전에 실행되므로 Person 색인의 업데이트가 아직 적용되지 않았습니다. 따라서 getTallPeople()은 Bob만 반환합니다. 이는 속성이 쿼리 조건자를 충족하는 항목이 제외되는 결과 집합의 예시입니다.
예시 2 - Bob 신장 줄이기(Bob에게는 미안하지만)
이 예시에서 요청 1은 다른 작업을 수행합니다. Adam의 신장을 68인치에서 74인치로 늘리는 대신 Bob의 신장을 73인치에서 65인치로 줄입니다. 이번에 getTallPeople()은
무엇을 반환할까요?
요청 1, put()
요청 2, getTallPeople()
요청 1, put()-->commit()
요청 1, put()-->commit()-->마일스톤 A
요청 1, put()-->commit()-->마일스톤 B
이 시나리오에서 getTallPeople()은 Bob만 반환합니다. 왜일까요? Bob의 키를 줄이는 업데이트가 아직 커밋되지 않았고, 따라서 요청 2에서 실행한 쿼리가 아직 변경사항을 볼 수 없기 때문입니다.
이제 다음과 같이 가정해 보겠습니다.
요청 1, put()
요청 1, put()-->commit()
요청 1, put()-->commit()-->마일스톤 A
요청 1, put()-->commit()-->마일스톤 B
요청 2, getTallPeople()
이 시나리오에서는 getTallPeople()은 누구도 반환하지 않습니다. 왜일까요? 요청 2에서 쿼리를 실행한 시점에 Bob의 키를 줄이는 업데이트는 커밋된 상태이기 때문입니다.
이제 다음과 같이 가정해 보겠습니다.
요청 1, put()
요청 1, put()-->commit()
요청 1, put()-->commit()-->마일스톤 A
요청 2, getTallPeople()
요청 1, put()-->commit()-->마일스톤 B
이 시나리오에서 쿼리는 마일스톤 B 전에 실행되므로 Person 색인에 대한 업데이트가 아직 적용되지 않았습니다. 결과적으로 getTallPeople()은 여전히 Bob을 반환하지만 반환되는 Person 항목의 height 속성은 업데이트된 값인 65입니다. 이는 속성이 쿼리 조건자를 충족하지 않는 항목이 결과 집합에 포함되는 예시입니다.
결론
위 예시에서 볼 수 있듯이 Cloud Datastore의 트랜잭션 격리 수준은 커밋된 읽기와 매우 비슷합니다.
물론 유의미한 차이점이 있습니다. 이제 이러한 차이점과 그 이유를 이해했으므로 여러분의 애플리케이션에서 더 현명한 Datastore 관련 의사 결정을 내릴 수 있을 것입니다.
[[["이해하기 쉬움","easyToUnderstand","thumb-up"],["문제가 해결됨","solvedMyProblem","thumb-up"],["기타","otherUp","thumb-up"]],[["이해하기 어려움","hardToUnderstand","thumb-down"],["잘못된 정보 또는 샘플 코드","incorrectInformationOrSampleCode","thumb-down"],["필요한 정보/샘플이 없음","missingTheInformationSamplesINeed","thumb-down"],["번역 문제","translationIssue","thumb-down"],["기타","otherDown","thumb-down"]],["최종 업데이트: 2025-07-30(UTC)"],[[["\u003cp\u003eCloud Datastore transactions operate at the Serializable isolation level, ensuring each transaction is completely isolated from others and that transactions within an entity group are executed sequentially.\u003c/p\u003e\n"],["\u003cp\u003eOutside of transactions, Datastore operations resemble the Read Committed isolation level, meaning retrieved entities will only reflect committed data and never show partially committed information.\u003c/p\u003e\n"],["\u003cp\u003eThe commit process in Cloud Datastore involves two milestones: the application of changes to the entity (Milestone A) and the application of changes to indices (Milestone B), and while commits are typically applied within milliseconds, reads may not immediately reflect changes until both milestones are reached.\u003c/p\u003e\n"],["\u003cp\u003eQueries that span multiple entity groups might return stale or partially applied results because they cannot determine if there are outstanding modifications before execution, whereas ancestor queries will always return current and consistent data.\u003c/p\u003e\n"],["\u003cp\u003eDepending on the timing of query execution relative to the commit milestones, result sets may exclude entities that satisfy the query predicate or include entities that no longer satisfy it, however, using a \u003ccode\u003eget()\u003c/code\u003e operation will always get the most up to date data.\u003c/p\u003e\n"]]],[],null,["# Transaction Isolation in App Engine\n\n*Max Ross* \nAccording to [Wikipedia](https://wikipedia.org/w/index.php?title=Isolation_%28database_systems%29&oldid=208946883),\nthe isolation level of a database management system \"defines how/when the\nchanges made by one operation become visible to other concurrent operations.\"\nThe goal of this article is to explain\n[query](/appengine/docs/python/datastore/queries) and\n[transaction](/appengine/docs/python/datastore/transactions)\nisolation in the Cloud Datastore used by App Engine. After reading this article\nyou should have a better understanding of how concurrent reads and writes\nbehave, both in and out of transactions.\n\nInside Transactions: Serializable\n---------------------------------\n\nIn order from strongest to weakest, the four isolation levels are\nSerializable, Repeatable Read, Read Committed, and Read Uncommitted. Datastore\ntransactions satisfy the Serializable isolation level. Each transaction is\ncompletely isolated from all other datastore transactions and operations.\nTransactions on a given entity group are executed serially, one after\nanother.\n\nSee the\n[Isolation\nand Consistency](/appengine/docs/python/datastore/transactions#Python_Isolation_and_consistency)\nsection of the transaction documentation for more information, as well as the Wikipedia article on\n[snapshot\nisolation](https://wikipedia.org/wiki/Snapshot_isolation).\n\nOutside Transactions: Read Committed\n------------------------------------\n\nDatastore operations *outside* transactions most closely resemble the\nRead Committed isolation level. Entities retrieved from the datastore by\nqueries or gets will only see committed data. A\nretrieved entity will never have partially committed data (some from before\na commit and some from after). The interaction between queries and transactions\nis a bit more subtle, though, and in order to understand it we need to look at\nthe commit process in more depth.\n\nThe Commit Process\n------------------\n\nWhen a commit returns successfully, the transaction is\nguaranteed to be applied, but that does not mean the result of your write is\nimmediately visible to readers. Applying a transaction consists of two milestones:\n\n- **Milestone A** -- the point at which changes to an entity have been applied\n- **Milestone B** -- the point at which changes to indices for that entity have been applied\n\n\u003cbr /\u003e\n\nIn Cloud Datastore, the transaction typically is completely applied within a few hundred milliseconds after the commit returns. However, even if it is not completely applied, subsequent reads, writes, and [ancestor queries](/appengine/docs/python/datastore/queries#Python_Ancestor_queries) will always reflect the results of the commit, because these operations apply any outstanding modifications before executing. However, queries that span multiple entity groups cannot determine whether there are any outstanding modifications before executing and may return stale or partially applied results.\n\nA request that looks up an updated entity by its key at a time after milestone A is guaranteed to see the latest version of that entity. However, if a concurrent request executes a query whose predicate (the `WHERE` clause, for you SQL/GQL fans out there) is not satisfied by the pre-update entity but is satisfied by the post-update entity, the entity will be part of the result set only if the query executes after the apply operation has reached milestone B.\n\nIn other words, during brief windows, it is possible for a result set not to include an entity whose properties, according to the result of a lookup by key, satisfy the query predicate. It is also possible for a result set to include an entity whose properties, again according to the result of a lookup by key, fail to satisfy the query predicate. A query cannot take into account transactions that are in between milestone A and milestone B when deciding which entities to return. It will be performed against stale data, but doing a `get()` operation on the returned keys will always get the latest version of that entity. This means that you could be either missing results that match your query or get results that do not match once you get the corresponding entity.\n\nThere are scenarios in which any pending modifications are guaranteed to be completely applied before the query executes, such as *any* ancestor queries in Cloud Datastore. In this case, query results will always be current and consistent.\n\nExamples\n--------\n\nWe've provided a general explanation of how concurrent updates and queries\ninteract, but if you're like me, you typically find it easier to get your\nhead around these concepts by working through concrete examples. Let's\nwalk through a few. We'll start with some simple examples and then\nfinish up with the more interesting ones.\n\nLet's say we have an application that stores Person entities. A Person\nhas the following properties:\n\n- Name\n- Height\n\nThis application supports the following operations:\n\n- `updatePerson()`\n- `getTallPeople()`, which returns all people over 72 inches tall.\n\nWe have 2 Person entities in the datastore:\n\n- Adam, who is 68 inches tall.\n- Bob, who is 73 inches tall.\n\nExample 1 - Making Adam Taller\n------------------------------\n\nSuppose an application receives two requests at essentially the same time.\nThe first request updates the height of Adam from 68 inches to 74 inches.\nA growth spurt! The second request calls getTallPeople().\nWhat does getTallPeople() return?\n\nThe answer depends on the relationship between the two commit milestones\ntriggered by Request 1 and the getTallPeople() query executed by Request 2.\nSuppose it looks like this:\n\n- Request 1, `put()`\n- Request 2, `getTallPeople()`\n- Request 1, `put()`--\\\u003e`commit()`\n- Request 1, `put()`--\\\u003e`commit()`--\\\u003emilestone A\n- Request 1, `put()`--\\\u003e`commit()`--\\\u003emilestone B\n\nIn this scenario, `getTallPeople()` will only return Bob. Why? Because the\nupdate to Adam that increases his height has not yet been committed, so\nthe change is not yet visible to the query we issue in Request 2.\n\nNow suppose it looks like this:\n\n- Request 1, `put()`\n- Request 1, `put()`--\\\u003e`commit()`\n- Request 1, `put()`--\\\u003e`commit()`--\\\u003emilestone A\n- Request 2, `getTallPeople()`\n- Request 1, `put()`--\\\u003e`commit()`--\\\u003emilestone B\n\nIn this scenario, the query executes before Request 1 reaches milestone B,\nso the updates to the Person indices have not yet been applied. As a result,\ngetTallPeople() only returns Bob. This is an example of a result set that\nexcludes an entity whose properties satisfy the query predicate.\n\nExample 2 - Making Bob Shorter (Sorry, Bob)\n-------------------------------------------\n\nIn this example we'll have Request 1 do something different. Instead of\nincreasing Adam's height from 68 inches to 74 inches, it will reduce Bob's\nheight from 73 inches to 65 inches. Once again, what does\n`getTallPeople()`\nreturn?\n\n- Request 1, `put()`\n- Request 2, `getTallPeople()`\n- Request 1, `put()`--\\\u003e`commit()`\n- Request 1, `put()`--\\\u003e`commit()`--\\\u003emilestone A\n- Request 1, `put()`--\\\u003e`commit()`--\\\u003emilestone B\n\nIn this scenario, `getTallPeople()` will return only Bob. Why? Because\nthe update to Bob that decreases his height has not yet been committed,\nso the change is not yet visible to the query we issue in Request 2.\n\nNow suppose it looks like this:\n\n- Request 1, `put()`\n- Request 1, `put()`--\\\u003e`commit()`\n- Request 1, `put()`--\\\u003e`commit()`--\\\u003emilestone A\n- Request 1, `put()`--\\\u003e`commit()`--\\\u003emilestone B\n- Request 2, `getTallPeople()`\n\n\nIn this scenario, `getTallPeople()` will return no one. Why? Because the\nupdate to Bob that decreases his height has been committed by the time\nwe issue our query in Request 2.\n\nNow suppose it looks like this:\n\n- Request 1, `put()`\n- Request 1, `put()`--\\\u003e`commit()`\n- Request 1, `put()`--\\\u003e`commit()`--\\\u003emilestone A\n- Request 2, `getTallPeople()`\n- Request 1, `put()`--\\\u003e`commit()`--\\\u003emilestone B\n\nIn this scenario, the query executes before milestone B, so the updates\nto the Person indices have not yet been applied. As a result,\n`getTallPeople()` still returns Bob, but the height property of the Person\nentity that comes back is the updated value: 65. This is an example of\na result set that includes an entity whose properties fail to satisfy\nthe query predicate.\n\nConclusion\n----------\n\nAs you can see from the above examples, the transaction isolation level\nof the Cloud Datastore is pretty close to Read Committed.\nThere are, of course, meaningful differences, but now that you understand\nthese differences and the reasons behind them, you should be in a better\nposition to make intelligent, datastore-related design decisions in your\napplications."]]