엄범


포인트 읽기 - 포인트 차감 순으로 DB 작업이 발생하는 상황이었다.

```java

@Transactional

public foo bar() {

  // SELECT

MemberRestMileageInfo member = getMemberMileageByKey(memberKey);  

...

  // UPDATE

memberInfoMapper.update(member);  

}

```


이런 상황에서 같은 `` memberKey`` 를 대상으로 빠르게 두 번 요청하면 갱신 손실 문제가 발생한다.
```
        T1                                                    T2
start transaction                     
                                                    start transaction
SELECT<point : 10000>                                          
                                                    SELECT<point : 10000>
UPDATE<point : 5000>
                                                    UPDATE<point : 9000>
```
즉, 먼저 들어온 트랜잭션이 UPDATE 하기 전에 다른 트랜잭션이 SELECT를 해버리면, 먼저 들어온 트랜잭션 T1의 UPDATE는 덮어써져서 사라져버린다.

그래서 두 트랜잭션이 동시에 실행되지 않도록 하기 위해, 처음에는 고립 수준(isolation level)을 SERIALIZABLE로 조정했다. 그러나 이는 deadlock을 유발했다.

트랜잭션 고립 수준을 SERIALIZABLE 로 설정했을 때 발생하는 deadlock 문제

SERIALIZABLE은 SELECT로 가져온 row에 대해 read lock(타 스레드에서 read는 가능, write 시 lock이 풀릴 때 까지 pending)을 걸어준다. 이후 UPDATE를 만나면 write lock을 획득하려 시도한다.

```

        T1                                                    T2

start transaction                     

                                                    start transaction

SELECT<read_L1 획득>                                          

                                                    SELECT<read_L2 획득>

UPDATE<write_L1 획득하기 위해 read lock2 해제 대기>

                                                    UPDATE<wirte_L2 획득 위해 read_L1 해제 대기. deadlock 발생>

```

http://www.gurubee.net/lecture/2396 2.가. Lock 종류 부분에 이러한 문제가 잘 나와 있다. 


sol

위와 같은 문제를 해결하기 위해서는 고립 수준은 기본으로 두고, 처음에 SELECT 할 때 부터 read lock(shared lock) 대신 write lock(update lock)을 획득하면 된다.


write lock은 배타적 lock이므로, 어떤 트랜잭션이 실행 중이라면 다른 트랜잭션은 기존 트랜잭션이 lock을 release할 때 까지 pending 상태가 된다.


MySQL에서 update lock을 획득하기. FOR UPDATE


Lock이 걸리는 범위?

WHERE에서 key column(index)에 대해 거르면 해당 row에 대해서만 lock이 걸린다.

index가 걸려있다면, 선택한 row들만 lock이 걸린다.

index가 안걸려 있어 전체 테이블을 서치하는 경우 테이블 전체에 lock이 걸린다.



이런 문제를 DB단이 아니라 프로그램 단에서 lock을 걸어서 해결하는 것도 가능은 하다.


1. 아예 메서드 전체에 lock을 거는 식으로 하면, 위 상황 자체를 예방할 수 있지만... 문제는 한 순간에 한 스레드만 이 메서드를 실행할 수 있게 되므로, 성능이 엄청나게 저하된다.

2. SELECT - UPDATE 작업이 어떤 Key와 관계된다면, ( 예를 들어 회원 정보 업데이트같이 회원 키로 SELECT하고 해당 레코드를 UPDATE하는 상황) Map<Key, Lock> 을 사용해서 동일 회원에 대해서만 두 스레드가 동시에 작업하지 못하게 막을 수는 있다. 이렇게 하면 여러 회원에 대한 작업은 동시에 실행될 수 있긴 하다.

근데 이 방법도, 인메모리에 Map을 유지해야 한다는 단점이 있고... redis같은걸 쓰면 좀 낫겠지만... 회원 수가 많으면? 등등 여러모로 애매하기 때문에 DB단에서 트랜잭션으로 제어하는게 좋아보인다.


SELECT - UPDATE 로직의 무결성을 유지하자.

별도의 로직이 필요 없는 경우라면 다음과 한 쿼리로 바로 더해버려도 되지만...
```sql
UPDATE user_point SET user_point + ${amount} WHERE id = 1;
```
SELECT 해서 뭔가를 가져오고 검증한 다음에 UPDATE를 해줘야 되는 경우, SELECT - UPDATE 사이에 읽어온 row가 DB에서 변경되지 않도록 보장해줘야 한다.