<개요>

- Designing Data Intensive Applications 를 읽고 그 중 Transactions에 대한 내용을 정리.

- 이전글에서 Weak Isolation Levels 중 Read Commited, Snapshot Isolation(Repeatable Read)에 관련된 내용을 살펴보았고

 이번 글에서는 Preventing Lost Updates, Write Skew (Phantoms) 에 대해서 살펴본다.

- 이전글 https://icthuman.tistory.com/entry/Transactions-2

 

Transactions #2 (Atomicity, Isolation)

- Designing Data Intensive Applications 를 읽고 그 중 Transactions에 대한 내용을 정리. - 이전글 https://icthuman.tistory.com/entry/Transactions-개념정리-1 Transactions #1 (Basic) - Designing Data Intensive Applications 를 읽고 그

icthuman.tistory.com

 

<내용>

Weak Isolation Levels

 

Preventing Lost Updates

- read commited 와 snapshot isolation은 주로 동시 쓰기가 있는 경우, 읽기전용 트랜잭션의 볼수 있는 내용을 보장하는 것이다.

- 이전 글에서는 두 트랜잭션이 동시에 쓰는것에 대해서는 다루지 않았고, 특정 유형의 쓰기-쓰기 에 대해서만 살펴봤다.

- 몇 가지 다른유형의 충돌을 살펴 볼텐데 그 중 가장 유명한 것이 Lost Updates 이다.

- 아래 그림을 살펴보자.

TimeLine 1 2 3 4
User #1 getCounter 42 + 1 setCounter 43  
Data 42 42 43 43
User #2   getCounter 42 + 1 setCounter 43
 

- 두 Client간의 race condition이 발생하였다. 주로 App이 read - modify - write의 사이클을 가지고 있을때 발생한다.

- 즉 write가 발생하기 전에 그 사이에 일어난 modification을 포함하지 않기 때문이다.

 

Solutions

Atomic write Operations

- 많은 DB가 atomic update operations를 제공하기 때문에 App에서 해당 로직을 구현하지 않는 것이 좋다. 일반적인 Best Solution

 예) UPDATE counters SET val = val + 1 WHERE key = 'foo';

- 일반적으로 DB내부에서 execlusive lock을 통해서 구현하기 때문에 (update할때 읽을 수 없다!) "Cursor Stability 유지" 

- 다른 방법으로 all atomic operations를 single thread 에서 수행하는 방법도 있다. (성능 고려)

 

Explicit Locking

- DB에서 해당 기능을 제공하지 않는다면 App에서 명시적으로 update 될 object를 잠그는 방법이다. 

- 잠금을 수행후 read - modify - write 를 수행할 수 있으며, 다른 트랜잭션이 같은 object 에 접근할 경우에 첫 번째 read - modify - write 가 완료될 때 까지 강제로 대기한다.

예 )

BEGIN TRANSACTION

SELECT * FROM ... WHERE ... FOR UPDATE;  (해당쿼리로 반환된 all rows lock !)
UPDATE ... 

COMMIT;

 

Automatically detecting lost updates

- atomic operations & locks : read - modify - write 를 순차적으로 하여 lost updates 를 막는다.

- 대안 : 병렬 수행을 허락하고, Transaction Manager 가 lost update를 감지하면, 트랜잭션을 중단하고 강제로 read - modify - write 를 retry 한다!

- 장점 : DB가 이 검사를 효율적으로 수행할 수 있다는 것. with Snapshot Isolation

PostgreSQL repeatable read automatically detect
and abort the offending transaction
Orale serializable
SQL snapshot isolation
MySQL, InnoDB repetable read X

 

Compare-and-Set

- 우리가 CAS연산이라고 부르는 방법이다. DB가 Transcations를 제공하지 않는 atomic compare-and-set을 찾는 것이다. (Single-object writes)

- 마지막으로 값을 읽은 후 값이 변경되지 않았을때에만 업데이트가 발생할 수 있도록 허용하는 것이다.

- 만약 변경이 일어났다면? read-modify-write연산을 재시도한다. 반드시!

 

주의! Conflict replication

- Locks and Compared and Set은 Single up-to-date, copy of the data를 가정한다.

- replicated DB에서는 여러 노드에 복사본이 존재하고, 데이터 수정이 다른 노드에서 발생할 수 있기 때문에 다른차원의 접근이 필요하다.  즉, 다시 말하면 multi leader 또는 leaderless replication에서는 write가 동시에 발생하고, 비동기 연산이 있다면 보장할 수 없다. (Linearizability)

- 대신 "Detecting Concurrent Writes" 챕터에서 살펴본 내용처럼 concurrent writes 가 충돌된 값의 버전들을 생성하고 (App 또는 별도의 자료구조활용), 이러한 충돌을 versions를 통해서 reslove , merge하는 방법이 가능하다.

- Atomic Operations는 영향을 받지 않는다. (특히 Commutative한 Actions이라면 !)

- 슬프게도.. 많은 replicated DB에서는 기본값으로 Last Write Wins 이다.

 

<정리>

- 개인적으로 매우 유익했던 챕터이다. 결국 두 개 이상의 동시쓰기가 발생한다면 해결방법은 아래와 같이 정리할 수 있다.

 1) 해당 사이클을 통째로 묶는다.

 2) 동시수행을 제한한다. ( Lock or Single Thread )

 3) 일단 진행시켜! 에러나면 다시 시도

 

- 멀티 노드를 가지는 Database라면 여러 곳에서 동시다발적으로 데이터에 대한 복제 / 연산이 일어나기 때문에

 1) Single Leader를 통해서 제어하던지(Hbase 같은)

 2) 마지막에 Write한 값으로 저장

 3) 별도의 Application이나 자료구조를 활용하여 충돌버전을 관리하고 resolve / merge 

- 그래서 대부분의 분산병렬처리 오픈소스 진영에서 zookeeper를 사용하고 있는 듯 하다.

 

- 다음 글에서는 이 글에서 다루지 못한 Isolation Level ( Write Skew, Phantoms read )을 좀 더 자세히 살펴보고 분산환경의 Consistency 에 대해서 정리하도록 해야겠다.

<개요>

volatile은 컴파일러가 특정변수에 대해서 옵티마이져에 대해서 캐슁을 하지 않고, 리오더링을 하지 않도록 해주는 키워드라고 한다.

그로 인해 멀티쓰레드 환경에서 동기화문제를 만날 때 사용하는 경우가 많다.

 

<내용>

예를 살펴보자

기본적으로 자바기본형은 thread-safe하지만 개발환경에 따라 특이한 경우가 발생할 수 있다.

32bit환경에서 long이나 double처럼 64bit 값을 처리할 때 assign이 두단계에 걸쳐서 일어나게 된다. 

(address의 한계때문에 발생한다.


volatile키워드를 사용할 경우 이에 대한 원자성을 보장하여 오류를 방지해준다.


그렇다면 synchronized 는 언제 사용할까? 위와 같이 volatile로 선언하더라도

public void add(int i){

    longResult += i;

}

와 같은 메소드를 수행하면 + 연산까지 원자성을 보장하지는 않기 때문에 마찬가지로 오류가 발생할 수 있다.


이때 add 메소드에 대해서 synchronized 키워드를 사용하여 동기화를 적용한다.


하지만 해보신분들은 알겠지만 synchronized를 사용하는 순간 엄청나게 느려진다. 어찌보면 당연한 결과이다...

 

<정리>

이를 보완하기 위해서 JDK 1.5이후에서는 Atomic 타입을 제공

Class Description
AtomicBoolean
A boolean value that may be updated atomically.
AtomicInteger
An int value that may be updated atomically.
AtomicIntegerArray
An int array in which elements may be updated atomically.
AtomicIntegerFieldUpdater<T>
A reflection-based utility that enables atomic updates to designated volatile int fields of designated classes.
AtomicLong
A long value that may be updated atomically.
AtomicLongArray
A long array in which elements may be updated atomically.
AtomicLongFieldUpdater<T>
A reflection-based utility that enables atomic updates to designated volatile long fields of designated classes.
AtomicMarkableReference<V>
An AtomicMarkableReference maintains an object reference along with a mark bit, that can be updated atomically.
AtomicReference<V>
An object reference that may be updated atomically.
AtomicReferenceArray<E>
An array of object references in which elements may be updated atomically.
AtomicReferenceFieldUpdater<T,V>
A reflection-based utility that enables atomic updates to designated volatile reference fields of designated classes.
AtomicStampedReference<V>
An AtomicStampedReference maintains an object reference along with an integer "stamp", that can be updated atomically.

public class AtomicLong extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 1927816293512124184L;

    // setup to use Unsafe.compareAndSwapLong for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

<중략>

private volatile long value;  /**
     * Creates a new AtomicLong with the given initial value.
     *
     * @param initialValue the initial value
     */
    public AtomicLong(long initialValue) {
        value = initialValue;
    }

/**
     * Atomically adds the given value to the current value.
     *
     * @param delta the value to add
     * @return the previous value
     */
    public final long getAndAdd(long delta) {
        return unsafe.getAndAddLong(this, valueOffset, delta);
    }

 a. 예를 들면Long대신에 AtomictLong을 사용하고 제공되는 addAndGet메소드를 사용할 경우 멀티쓰레드 간의 값을 보장하면서 속도도 매우 빨라진다.


 b. 내부적으로 코드를 살펴보면 AtomicLong은 Number를 확장하며 Serializable을 impl한다 (어찌보면 당연하다..)

 c. 멤버변수로 private volatile long value를 가지고 있으며 addAndGet메소드는 final로 선언되어 무한루프를 반복한다.

 d. Unsafe class의 compareAndSwapLong 메소드를 활용한 compareAndSet메소드를 사용한다. 모든 연산메소드가 동일하다.

 

<참고사이트>

https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/package-summary.html

+ Recent posts