동시성 제어 - MySQL 의 Named Lock

this-is-spear·2023년 1월 29일
0

Named Lock이란

Named Lock은 고유한 이름으로 식별되는 잠금이며 잠금을 획득해 공유 자원 접근을 동기화할 수 있습니다.

Named Lock은 다른 서비스를 거치지 않고 MySQL 서버의 메모리에 직접 동작해 낮은 오버헤드로 잠금 획득 및 해제가 가능합니다. 하지만, MySQL은 Named Lock을 제공하고 MySQL 레벨에서 관리가 되는데, 모든 시스템이 동일한 물리 메모리에 접근할 수 있다고 가정하는 공유 메모리 모델에 의존하기 때문에 분산 시스템에 적합하지 않습니다.

Named Lock은 exclusive한 특성을 가지고 테이블 단위로 잠금을 획득합니다. 그렇기 때문에 테이블 단위로 잠금을 획득하기 때문에 성능에 영향을 미칠 가능성이 높습니다. 또한 잠금을 획득할 수 있는 경우 대기 상태가 되므로 데드락이 발생할 가능성이 있으니 주의해야 합니다.

결론은, Named Lock은 서버 내 공유 자원에 대한 접근을 동기화하는데 유용하지만, 분산 시스템에서 적합하지 않을 수 있으므로 다른 분산 락 메커니즘을 고려해야 합니다.

Named Lock 장점

그럼 왜 Named Lock을 사용하는지 살펴보기 위해 장점을 정리했습니다.

간단하다.

MySQL 기본 제공 기능이므로 추가 소프트웨어나 라이브러리가 필요없기에 간단합니다.

빠르다.

다른 서비스를 거치지 않고 MySQL 서버 메모리에서 동작하기 때문에 빠릅니다.

쉽다.

쿼리를 이용해 잠금을 획득하고 해제할 수 있는 등 Named Lock 관리가 쉽습니다.

Named Lock 단점

Named Lock을 사용하기 위해서는 단점을 파악해 보완할 필요가 있죠.

제한적이다.

MySQL에서만 사용가능하다는 단점이 있습니다.

제한적인 상황을 보완하기 위해

다른 데이터베이스를 사용하는 경우에는 해당 데이터베이스 많의 잠금 기능을 이용하거나 애플리케이션 레벨에서 잠금을 제공할 필요가 있습니다. 애플리케이션 레벨에서 데이터베이스 단위로 잠금 기능을 관리한다면 쉽게 문제를 해결할 수 있습니다.

분산 시스템에서는 적합하지 않다.

MySQL 자체가 독립적으로 실행하는 RDBMS이기 때문에 클러스터 구성에서 제공하는 분산 잠금 기능이 없습니다.

분산 시스템 상황을 보완하기 위해

분산 시스템의 복잡성을 처리하기 위해서는 추가적인 소프트웨어 또는 인프라가 필요합니다 예를 들어 Zookeeper, Consul 같은 분산 잠금 관리자를 사용해 클러스터 구성을 관리할 수 있습니다.

Named Lock 메커니즘

Named Lock의 잠금을 획득하고 해제하는 과정을 쉽게 이미지로 정리했습니다.

1. 잠금을 획득한다.

사용자가 작업을 진행하기 위해 Named Lock 잠금을 획득합니다. 만약 다른 사용자가 잠금을 획득했다면 대기해야 합니다.

쉽게 쿼리 문을 이용해 잠금을 획득할 수 있습니다.

SELECT GET_LOCK(lockName, timeout)

2. 작업을 처리한다.

잠금을 가지는 동안 작업을 처리합니다.

잠금을 가지는 동안 다른 사용자가 해당 잠금을 획득하기 위해서는 대기해야 합니다.

3. 잠금을 해제한다.

작업을 완료하면 가지고 있는 잠금을 해제하기 위해 해제 요청을 보냅니다. 또한 커넥션이 종료되더라도 잠금이 해제됩니다.

쿼리 문을 이용해 잠금을 해제할 수 있습니다.

DO RELEASE_LOCK(lockName)

Named Lock 기능

Named Lock은 MySQL 레벨에서 제공하는 기능입니다. 쿼리 문을 이용해 잠금을 획득하고 해제할 수 있습니다.

  • GET_LOCK(lock_name, timeout)
    • lock_name으로 Named Lock을 획득하려고 시도합니다. timeout 매개변수는 잠금을 획득할 수 없는 경우 오류를 반환하기 전 함수가 기다리는 시간을 지정합니다.
  • RELEASE_LOCK(lock_name)
    • lock_name으로 Named Lock을 해제합니다.
  • IS_FREE_LOCK(lock_name)
    • lock_name의 Named Lock이 사용 가능한지 확인합니다.
  • IS_USED_LOCK(lock_name)
    • lock_name의 Named Lock이 사용 중인지 확인합니다.

추가적으로 8.0 이전에는 Named Lock을 중첩해서 걸 수 없엇지만, 8.0 이후부터는 중첩해서 걸 수 있게 되면서 조금 더 복잡한 로직을 처리할 수 있게 됐습니다.

Named Lock을 활용할 수 있는 곳

개인적인 생각

  • 여러 프로세스에서 동시적으로 요청해 MySQL 인스턴스에 접근할 때, 공유 리소스에 대한 액세스를 동기화해야 하는 경우에 사용할 수 있습니다.
  • 여러 스레드가 동일한 시스템에서 실행 중인 상황에서 처리하고 싶은 요청이 많고, 많은 범위를 잠금하게 된다면 데드락이 발생할 수 있는데, 네임드 락을 통해 순차적으로 처리한다면 데드락을 막을 수 있습니다.
  • 중단되어서는 안되는 작업을 수행해야 하는 경우에 중요한 범위를 잠궈 다른 클라이언트가 실행하지 못하도록 설정할 수 있습니다.

실제 사례

우아한형제들 기술블로그에서 광고시스템은 Named Lock을 활용해 분산락을 구현하고 있었습니다.

글 내용을 정리하자면, 분산 락을 구현할 필요가 있었는데 다른 소프트웨어나 인프라를 사용하면 발생하는 비용과 유지 보수 비용을 생각했고, 이전부터 MySQL을 사용해왔기에 Named Lock을 이용하면 별다른 비용 없이 분산 락을 구현할 수 있어서 Named Lock을 이용했다고 합니다.

Named Lock 주의점

공부를 하면서 MySQL 특성상 Named Lock을 분산 환경에서는 적합하지 않다는 제약 사항이 있었습니다.

Based-connection 특성을 가진다.

커넥션이 종료되면 잠금이 해제되는 문제가 있습니다. 우아한형제들 기술블로그에 적힌 내용으로 JDBC 템플릿은 트랜잭션이 종료되면 커넥션을 반환하게 되면서 획득한 Lock을 반환하지 못하는 경우가 발생할 수 있다고 합니다.

MySQL 공식 문서에 작성되어 있듯이 커넥션이 종료되면 자동으로 잠금이 해제된다고 하는데, 잠금이 반환되지 않는 문제는 없어보이지만, 작성한 코드 동작이 실제와 다른 괴리가 발생하는 문제가 있습니다.

트랜잭션과는 별개로 동작한다.

트랜잭션이 롤백되거나 커밋되어도 잠금이 해제되지 않기 때문에 직접 잠금 해제를 구현해 신뢰가 높은 메커니즘을 제공할 필요가 있습니다.

즉, Named Lock을 사용할 때는 트랜잭션과는 별개로 신경써서 사용할 필요가 있습니다.

적절한 대기 시간 설정 필요하다.

다른 클라이언트가 잠금을 보유한 상황에서 잠금 획득을 시도할 때 대기하게 되는데, 이러한 케이스 많다면 성능에 영향을 미칠 수 있으므로 잠금 획득을 위한 대기 시간을 적절한 값으로 설정해야 합니다.

테이블 기반 잠금으로 동작한다.

Named Lock을 Table 기반으로 잠금을 획득하기 때문에 많은 클라이언트가 기다리는 상황이 발생할 수 있고, 그로 인해 성능에 영향을 미칠 수 있으니 주의해서 사용해야 합니다.

분산 락으로 구현할 때 신경써야 한다.

Named Lock은 MySQL 인스턴스 내부의 메모리를 기반으로 동작하기 때문에 분산 시스템에 적합하지 않습니다. 분산 시스템의 장애를 관리하기 위해서는 분산 락 관리자(Zookeeper 등등)를 사용할 필요가 있습니다.

Named Lock 사용 방법

JSP를 활용

JSP를 활용하게 된다면 JSP 요청 기간 동안 획득이 가능합니다. 즉, 요청이 완료되면 잠금이 바로 해제되죠. 여러 요청에서 잠금을 유지하기 위해서는 DB에서 메커니즘을 구현해야 하는 문제점이 있습니다.

<%@ page import="java.sql.*" %>
<%
  // Connect to the database
  String url = "jdbc:mysql://localhost:3306/mydb";
  String username = "root";
  String password = "password";
  Connection con = DriverManager.getConnection(url, username, password);

  // Acquire the named lock
  String lockName = "my_lock";
  int timeout = 10; // seconds
  Statement stmt = con.createStatement();
  ResultSet rs = stmt.executeQuery("SELECT GET_LOCK('" + lockName + "', " + timeout + ")");
  if (rs.next() && rs.getInt(1) == 1) {
    // Lock acquired successfully
    // Perform your critical section of code here
    // ...

    // Release the named lock
    stmt.executeUpdate("DO RELEASE_LOCK('" + lockName + "')");
  } else {
    // Lock not acquired
    // Handle the error or wait for the lock to be released
  }

  // Close the database connection
  con.close();
%>

JPA를 활용해서

JPA는 Named Lock 기능을 제공해주지 않기 떄문에 직접적으로 구현해야 합니다. 네이티브 쿼리를 이용해 Named Lock을 제공해야 하는데, 이 방법은 트랜잭션 동안만 유지되는 한계가 있습니다. 여러 트랜잭션에서 잠금을 유지하기 위해서는 잠금 상태를 저장하는 메커니즘을 구현할 필요가 있습니다.

@Transactional
public int performCriticalSection(String userLockName, int timeoutSeconds) {
  // Acquire the named lock
  Query query = entityManager.createNativeQuery("SELECT GET_LOCK(:userLockName, :timeoutSeconds)")
    .setParameter("lockName", lockName)
    .setParameter("timeout", timeout);
  return (int) query.getSingleResult();
}

@Transactional
public void releaseLock(String userLockName) {
  // Release the named lock
  Query query = entityManager.createNativeQuery("DO RELEASE_LOCK(:userLockName)")
    .setParameter("lockName", lockName);
  query.executeUpdate();
}

public void execute(String userLockName,
                    int timeoutSeconds) {
    try {
         int result = getLock(userLockName, timeoutSeconds);
         if (result == 1) {
			    // Lock acquired successfully
			    // Perform your critical section of code here
			    // ...
			  } else {
			    // Lock not acquired
			    // Handle the error or wait for the lock to be released
			  }
    } catch (SQLException | RuntimeException e) {
        throw new RuntimeException(e.getMessage(), e);
    } finally {
        releaseLock(userLockName);
    }
}

JDBC 템플릿이 트랜잭션이 끝나자마자 커넥션을 반환할 수 있기 때문에 잠금이 제대로 회수되지 않을 가능성이 있습니다. 이런 경우 잠금을 획득하고 해제하는 메서드를 트랜잭션 단위로 묶어서 처리하게 된다면 문제가 발생하지 않습니다.

마지막으로

정리하게 된 계기

Real MySQL책을 읽으면서 Named Lock을 학습해 정리하고 싶었고, Named Lock을 활용해 분산 락을 구현한 내용을 이해하고 싶었습니다.

정리하자면

Named Lock은 MySQL에서 제공해주는 잠금 기능이며 잠금 기능 중 오버헤드가 적고 빠른 편에 속합니다. 하지만, 성능에 영향을 끼치는 요인이 많기 때문에 주의할 필요가 있습니다.

참고자료

profile
익숙함을 경계하자

2개의 댓글

comment-user-thumbnail
2023년 6월 22일

안녕하세요 namedlock(user level lock)에 관한 자세한 설명과 코드로 도움이 많이 되었습니다.
한가지 궁금한 것이 포스팅에는 "테이블 기반 잠금으로 동작한다."으로 하셨는데
mysql8.0기준 (realmysql)책과 공식문서에는 테이블과 상관없다고 설명이 되어있어서
어떤것이 맞는지 궁금합니다 ~

https://dev.mysql.com/doc/refman/8.0/en/lock-tables.html

You can also avoid locking tables in some cases by using the user-level advisory lock functions GET_LOCK() and RELEASE_LOCK().

1개의 답글