home

Select for update 종류

일반 SELECT

  • 단순 읽기 작업
  • 다른 트랜잭션의 읽기/쓰기를 막지 않음
  • 동시에 여러 트랜잭션이 같은 데이터를 읽을 수 있음

SELECT FOR UPDATE

BEGIN;
SELECT * FROM product_option WHERE id = 1 FOR UPDATE;
COMMIT;
  • 배타적 잠금(쓰기 잠금)을 획득
  • 다른 트랜잭션이 잠금을 이미 보유중이면 해제될 때까지 대기
  • 데드락 위험이 있음

SELECT FOR UPDATE NOWAIT

BEGIN;
SELECT * FROM product_option WHERE id = 1 FOR UPDATE NOWAIT;
COMMIT;
  • 즉시 잠금을 획득하려고 시도
  • 잠금을 획득할 수 없으면 즉시 에러 발생 → 대기하지 않음

SELECT FOR UPDATE SKIP LOCKED

BEGIN;
SELECT * FROM product_option WHERE id IN (1,2,3) FOR UPDATE SKIP LOCKED;
COMMIT;
  • 이미 잠겨있는 행을 건너뛰고 잠금이 가능한 행만 선택
  • 큐 시스템이나 작업 분배에 유용

SELECT FOR SHARE

BEGIN;
SELECT * FROM product_option WHERE id = 1 FOR SHARE;
COMMIT;
  • 공유 잠금(읽기 잠금)을 획득
  • 다른 트랜잭션의 읽기는 허용하지만 쓰기는 차단
  • 오히려 많은 트랜잭션이 공유락을 들고 있으면 쓰기가 오래 차단될 수 있기 때문에 락 시간을 최소화하거나 타임아웃을 잘 정해두어야 한다.
  • 정리하면 다음과 같다.
특징 FOR UPDATE NOWAIT SKIP LOCKED FOR SHARE
락 타입 배타적 배타적 배타적 공유
대기 여부 대기함 대기안함 건너뜀 대기함
동시성 낮음 중간 높음 높음
데드락 위험 높음 낮음 낮음 중간
용도 단일 레코드 업데이트 빠른 실패 필요 시 작업 분배 시스템 읽기 정합성 보장

SELECT FOR UPDATE SKIP LOCKED 예시

  • 정확히 어떻게 작동하는지 알기 위해서 또 파이썬 코드를 가져왔습니다.

      def transaction1():
          with engine.connect() as conn:
              with conn.begin():
                  print("Transaction 1 - Starting")
        
                  result = conn.execute(
                      text("SELECT * FROM deposits WHERE status = 'PENDING' FOR UPDATE")
                  )
                  print("Transaction 1 - Selected rows:", result.mappings().all())
        
                  time.sleep(3)
        
                  conn.execute(
                      text("UPDATE deposits SET amount = amount - 500 WHERE status = 'PENDING'")
                  )
                  print("Transaction 1 - UPDATE executed")
        
                  print("Transaction 1 - Committing")
        
      def transaction2():
          with engine.connect() as conn:
              with conn.begin():
                  print("Transaction 2 - Starting")
        
                  result = conn.execute(
                      text("SELECT * FROM deposits WHERE status = 'PENDING' FOR UPDATE SKIP LOCKED")
                  )
                  rows = result.mappings().all()
                  print("Transaction 2 - Selected rows (SKIP LOCKED):", rows)
        
                  if rows:
                      conn.execute(
                          text("UPDATE deposits SET status = 'SUCCESS' WHERE status = 'PENDING' FOR UPDATE SKIP LOCKED")
                      )
                      print("Transaction 2 - UPDATE executed")
        
                  result = conn.execute(text("SELECT * FROM deposits"))
                  print("Final state in Transaction 2:", result.mappings().all())
        
      def run_test():
          with engine.connect() as conn:
              with conn.begin():
                  conn.execute(text("DELETE FROM deposits"))
                  conn.execute(text("""
                      INSERT INTO deposits (hash, amount, status)
                      VALUES
                          ('user1', 1000, 'PENDING'),
                          ('user2', 1000, 'PENDING'),
                          ('user3', 1000, 'PENDING')
                  """))
        
          with engine.connect() as conn:
              result = conn.execute(text("SELECT * FROM deposits"))
              print("Initial state:", result.mappings().all())
        
          thread1 = threading.Thread(target=transaction1)
          thread2 = threading.Thread(target=transaction2)
        
          thread1.start()
          time.sleep(1)
          thread2.start()
        
          thread1.join()
          thread2.join()
        
          with engine.connect() as conn:
              result = conn.execute(text("SELECT * FROM deposits"))
              print("\\nFinal state after both transactions:", result.mappings().all())
        
      if __name__ == "__main__":
          run_test()
        
    
  • 시나리오
    • deposits 테이블에 3개의 PENDING 상태 row를 생성
    • transaction1이 먼저 시작되어 모든 PENDING 상태의 row에 대해 FOR UPDATE로 lock을 건다.
    • transaction2는 FOR UPDATE SKIP LOCKED를 사용하여 잠기지 않은 row만 선택하려 시도
  • 결과

      Initial state: [{'id': 68, 'hash': 'user1', 'amount': 1000, 'status': 'PENDING'}, {'id': 69, 'hash': 'user2', 'amount': 1000, 'status': 'PENDING'}, {'id': 70, 'hash': 'user3', 'amount': 1000, 'status': 'PENDING'}]
      Transaction 1 - Starting
      Transaction 1 - Selected rows: [{'id': 68, 'hash': 'user1', 'amount': 1000, 'status': 'PENDING'}, {'id': 69, 'hash': 'user2', 'amount': 1000, 'status': 'PENDING'}, {'id': 70, 'hash': 'user3', 'amount': 1000, 'status': 'PENDING'}]
      Transaction 2 - Starting
      Transaction 2 - Selected rows (SKIP LOCKED): []
      Final state in Transaction 2: [{'id': 68, 'hash': 'user1', 'amount': 1000, 'status': 'PENDING'}, {'id': 69, 'hash': 'user2', 'amount': 1000, 'status': 'PENDING'}, {'id': 70, 'hash': 'user3', 'amount': 1000, 'status': 'PENDING'}]
      Transaction 1 - UPDATE executed
      Transaction 1 - Committing
      Final state after both transactions: [{'id': 68, 'hash': 'user1', 'amount': 500, 'status': 'PENDING'}, {'id': 69, 'hash': 'user2', 'amount': 500, 'status': 'PENDING'}, {'id': 70, 'hash': 'user3', 'amount': 500, 'status': 'PENDING'}]
    
    • Transaction 2 - Selected rows (SKIP LOCKED): [] 에서 보면 알 수 있듯이, 트랜잭션1에서 lock을 걸고 있는 동안에는 트랜잭션 2에서는 아무 것도 select 해오지 못한다.
    • 데드락은 피할 수 있을 것 같은데, 재고 문제를 해결하기에는 조금 애매한 것 같다. 이유는 다음과 같다.
      • 먼저 주문한 사람보다 나중에 주문한 사람이 구매할 수 있음
      • 대기 시간과 무관하게 우연히 락이 해제된 시점의 요청이 성공
    • 이 얼마나 불합리한가! 역시 빠른 놈 위에 운 좋은 놈이다.