๐ค ์๋ก
์ด์ ๊ธ์์๋ ๋น๊ด์ ๋ฝ(Pessimistic Lock)๊ณผ ๋๊ด์ ๋ฝ(Optimistic Lock)์ ํตํด ๋ถ์ฐ๋ฝ์ ๊ตฌํํ์ฌ ๋จ์ผ ์๋ฒ๋ ๋ฌผ๋ก ๋ค์ค ์๋ฒ์์๋ ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ์์ต๋๋ค.
์ด๋ฒ ๊ธ์์๋ ๋น๊ด์ ๋ฝ๊ณผ ๋น์ทํ MySQL์ USER-LEVEL Lock(Named Lock)์ ํตํด์ ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํด ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
(์์ผ๋ก๋ user-level lock์ด๋ผ๊ณ ๋ถ๋ฅด๋๋ก ํ๊ฒ ์ต๋๋ค.)
๐ค USER-LEVEL Lock (Named Lock)
MySQL ๊ณต์ ๋ฌธ์๋ฅผ ๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ user level lock์ ์ง์ํด์ฃผ๋ ๊ฒ์ ์ ์ ์์ต๋๋ค.
๐ณ GET_LOCK(str, timeout)
์ฃผ์ด์ง ์ด๋ฆ(str)์ ๋ํ Lock ํ๋์ ์๋ํฉ๋๋ค. ์ด๋ ๋ฌธ์์ด์ ๊ธธ์ด๋ ์ต๋ 64๊ธ์์ ๋๋ค.
์ด๋ฏธ ๋ค๋ฅธ session์์ ๋์ผํ ์ด๋ฆ์ Lock์ ํ๋ํ ๊ฒฝ์ฐ Lock ํ๋์ ์ํด ๋๊ธฐํ๊ฒ ๋๋๋ฐ, ์ด๋ ์ต๋ timeout์ ๋ช ์๋ ์๊ฐ ๋งํผ ๋๊ธฐํฉ๋๋ค.
๋ง์ฝ timeout์ด ์์์ธ ๊ฒฝ์ฐ, ๋ฌดํ์ ๊ธฐ๋ค๋ฆฌ๊ฒ ๋ฉ๋๋ค.
- ํด๋น ๋ฝ์ ๋ฐฐํ์ (exclusive)์ผ๋ก ๋์ํฉ๋๋ค.
- ํ๋์ ์ธ์ ์์ named lock์ ์ป์ ๊ฒฝ์ฐ, ๋ค๋ฅธ ์ธ์ ์์๋ ๋์ผํ ์ด๋ฆ์ lock์ ํ๋ํ ์ ์์ต๋๋ค.
- ๋ฐํ๊ฐ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- 1 : named lock์ ์ฑ๊ณต์ ์ผ๋ก ํ๋ํ ๊ฒฝ์ฐ 1์ด ๋ฐํ๋ฉ๋๋ค.
- 0 : timeout์ด ์ง๋๋๋ก lock์ ํ๋ํ์ง ๋ชปํ ๊ฒฝ์ฐ 0์ด ๋ฐํ๋ฉ๋๋ค.
- null : ์๋ฌ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ(out of memory ๋ฑ) null์ด ๋ฐํ๋ฉ๋๋ค.
- GET_LOCK()์ ํตํด ํ๋ํ Lock์ RELEASE_LOCK()์ ํธ์ถํจ์ผ๋ก์จ ๋ช
์์ ์ผ๋ก ํด์ ์ํฌ ์ ์์ผ๋ฉฐ, ์ธ์
์ด ์ข
๋ฃ๋๋ ๊ฒฝ์ฐ์๋ ์์์ ์ผ๋ก ๋ฐํ๋ฉ๋๋ค.
- ํธ๋์ญ์ ์ ์ปค๋ฐ ํน์ ๋กค๋ฐฑ์ผ๋ก๋ GET_LOCK()์ผ๋ก ์ป์ ์ ๊ธ์ด ํด์ ๋์ง ์์ต๋๋ค.
- GET_LOCK()์ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ์ ๊ธ(MDL)์ ์ฌ์ฉํ์ฌ ๊ตฌํ๋ฉ๋๋ค.
- ํ๋์ ์ธ์
์ด ๋ค๋ฅธ ์ด๋ฆ์ Lock์ ๋ฌผ๋ก , ๋์ผํ ์ด๋ฆ์ Lock์ ์ฌ๋ฌ๊ฐ ํ๋ํ๋ ๊ฒ๋ ๊ฐ๋ฅํฉ๋๋ค.
- ์ฆ ๋ค์๊ณผ ๊ฐ์ ์๋๋ฆฌ์ค๋ ๊ฐ๋ฅํฉ๋๋ค.
# ์ธ์
1
SELECT GET_LOCK('test', 100);
SELECT GET_LOCK('test', 100);
# ์ธ์
2
SELECT GET_LOCK('test', -1); # ๋๊ธฐ ์ค
# ์ธ์
1
SELECT RELEASE_LOCK('test'); # ์ธ์
2๋ ์ฌ์ ํ ๋๊ธฐ ์ค
SELECT RELEASE_LOCK('test'); # ์ธ์
2์์ Lock ํ๋
(์ ์ฌ์ฉํ ์ผ์ด ์์์ง๋ ๋ชจ๋ฅด๊ฒ ์ต๋๋ค)
- GET_LOCK()์ ํตํด ์ฌ๋ฌ ๊ฐ์ ์ ๊ธ์ ํ๋ํ ์ ์์ผ๋ฏ๋ก, ์ด๋ก ์ธํด ๋ฐ๋๋ฝ์ด ๋ฐ์ํ ๊ฐ๋ฅ์ฑ์ด ์๊น๋๋ค.
- ๋ฐ๋๋ฝ์ด ๋ฐ์ํ๋ ์ํฉ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
# ์ธ์
1
SELECT GET_LOCK('mallang', -1);
# ์ธ์
2
SELECT GET_LOCK('donghun', -1);
# ์ธ์
1
SELECT GET_LOCK('donghun', -1); # ์ธ์
2๊ฐ donghun ์ ๋ํ Lock์ ๊ฐ์ง๊ณ ์์ผ๋ฏ๋ก ๋ฌดํ ๋๊ธฐ
# ์ธ์
2
SELECT GET_LOCK('mallang', -1); # ์ธ์
1์ด mallang ์ ๋ํ Lock์ ๊ฐ์ง๊ณ ์์ผ๋ฏ๋ก ๋ฌดํ ๋๊ธฐ
# -> ๋ฐ๋๋ฝ ๋ฐ์
- ์์ ๊ฐ์ด ๋ฐ๋๋ฝ์ด ๋ฐ์ํ๋ฉด, mysql์ ERROR 3058(ER_USER_LOCK_DEADLOCK)์ ๋ฐ์์์ผ ์ ๊ธ ํ๋ ์์ฒญ์ ์ข ๋ฃํฉ๋๋ค. ์ด๋ก ์ธํด ํธ๋์ญ์ ์ด ๋กค๋ฐฑ๋์ง๋ ์์ต๋๋ค.
- ์ฌ๋ฌ ํด๋ผ์ด์ธํธ์์ ์ ๊ธ ํ๋์ ๊ธฐ๋ค๋ฆฌ๋ ๊ฒฝ์ฐ, ์ ๊ธ์ ํ๋ํ๊ฒ ๋๋ ์์๋ ์ ์๋์ง ์์ต๋๋ค.
- ๋ฐ๋ผ์ ์ ๊ธ ์์ฒญ ์์๋๋ก ์ ๊ธ์ ํ๋ํ ๊ฒ์ด๋ผ ๊ฐ์ ํ๊ณ ์ฌ์ฉํด์๋ ์๋ฉ๋๋ค.
- GET_LOCK()์ statement-based replication์ ๋ํด ์์ ํ์ง ์์ต๋๋ค.
- GET_LOCK()์ ๋จ์ผ MySQL ์๋ฒ(mysqld)์ ๋ํด์๋ง ์ ๊ทธ๊ธฐ ๋๋ฌธ์, NDB ํด๋ฌ์คํฐ์ ์ฌ์ฉํ๊ธฐ์๋ ์ ํฉํ์ง ์์ต๋๋ค.
๐ณ RELEASE_LOCK(str)
์ ๋ ฅ๋ฐ์ ์ด๋ฆ(str)์ ๊ฐ์ง Lock์ ํด์ ํฉ๋๋ค.
- ๋ฐํ๊ฐ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- 1 : named lock์ ์ฑ๊ณต์ ์ผ๋ก ํด์ ํ ๊ฒฝ์ฐ 1์ด ๋ฐํ๋ฉ๋๋ค.
- 0 : ํด๋น ์ฐ๋ ๋์์ ํ๋ํ Lock์ด ์๋ ๊ฒฝ์ฐ 0์ด ๋ฐํ๋ฉ๋๋ค. (์ด ๊ฒฝ์ฐ Lock์ ํด์ ๋์ง ์์ต๋๋ค.)
- null : ํด๋น ์ด๋ฆ์ ์ ๊ธ์ด ์กด์ฌํ์ง ์์ผ๋ฉด null์ด ๋ฐํ๋ฉ๋๋ค.
๋๋จธ์ง๋ ์ด๋ฒ ๊ธ์์ ์ฌ์ฉํ์ง ์์ผ๋ฏ๋ก ์ดํด๋ณด์ง ์๊ณ ๋์ด๊ฐ๊ฒ ์ต๋๋ค.
๊ถ๊ธํ๋ค๋ฉด ๋ฐ๋ก ๊ณต์ ๋ฌธ์๋ฅผ ํตํด ํ์ธํด์ฃผ์ธ์ :)
๐ค USER-LEVEL Lock์ ํตํ ๋์์ฑ ๋ฌธ์ ํด๊ฒฐ
์ฐ์ ์ด์ ๊ธ์ ๋ฐ๋ผํ๋ค๋ฉด, Stock ํด๋์ค์์ @Version๊ณผ ๋ฒ์ ํ๋๋ฅผ ์ ๊ฑฐํด์ค๋๋ค.
user-level lock์ ํ๋๊ณผ ๋ฐํ์ ์ํด LockRepository๋ฅผ ๊ตฌํํฉ๋๋ค.
public interface LockRepository extends JpaRepository<Stock, Long> {
@Query(value = "SELECT GET_LOCK(:key, 3000)", nativeQuery = true)
Integer getLock(String key);
@Query(value = "SELECT RELEASE_LOCK(:key)", nativeQuery = true)
Integer releaseLock(String key);
}
์ด์ ์ฌ๊ณ ๊ฐ์ ๋ก์ง ์ ํ๋ก Lock๋ฅผ ํ๋ํ๊ณ ๋ฐํํด์ฃผ๋ ์์ ์ ์ฒ๋ฆฌํ๊ธฐ ์ํด NamedLockStockFacade๋ฅผ ์์ฑํด์ฃผ๋๋ก ํ๊ฒ ์ต๋๋ค.
@RequiredArgsConstructor
@Service
public class NamedLockStockFacade {
private final LockRepository lockRepository;
private final StockService stockService;
@Transactional
public void decrease(Long id) {
try {
Integer acquiredLock = lockRepository.getLock(String.valueOf(id));
if (acquiredLock != 1) {
throw new RuntimeException("Lock ํ๋์ ์คํจํ์ต๋๋ค. [id: %d]".formatted(id));
}
stockService.decrease(id);
} finally {
lockRepository.releaseLock(String.valueOf(id));
}
}
}
user-level lock์ ํธ๋์ญ์ ์ข ๋ฃ ์ ๋ฐํ๋์ง ์๊ธฐ ๋๋ฌธ์, RELEASE_LOCK()์ ํธ์ถํ์ฌ ๋ช ์์ ์ผ๋ก ํด์ ํด์ฃผ์ด์ผ ํฉ๋๋ค.
์ด์ StockService๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ๋ณ๊ฒฝํฉ๋๋ค.
@RequiredArgsConstructor
@Service
public class StockService {
private final StockRepository stockRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void decrease(Long id) {
Stock stock = stockRepository.getById(id);
stock.decrease();
stockRepository.saveAndFlush(stock);
}
}
propagation์ REQUIRES_NEW๋ก ์ค์ ํ์ฌ ์๋ก์ด ํธ๋์ญ์ ์์ ๋์ํ๋๋ก ๊ตฌํํ์์ต๋๋ค.
๋ง์ฝ ๋ฝ ํ๋๊ณผ ์ฌ๊ณ ๊ฐ์ ๋ก์ง์ด ๋์ผ ํธ๋์ญ์ ์์ ๋ฐ์ํ๋ค๋ฉด ์๋์ ๊ฐ์ด ์ฌ์ ํ ๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๊ฒ ๋ฉ๋๋ค.
์ด์ ๋ ์๋ USER-LELEV Lock ์ฌ์ฉ ์ ์ฃผ์์ ๋ถ๋ถ์์ ํ์ธํ๋๋ก ํ๊ฒ ์ต๋๋ค.
ํ ์คํธ์ฝ๋๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
@SpringBootTest
class NamedLockStockFacadeTest {
@Autowired
private NamedLockStockFacade namedLockStockFacade;
@Autowired
private StockRepository stockRepository;
private Long stockId;
@BeforeEach
void setUp() {
stockId = stockRepository.saveAndFlush(new Stock(1L, 100))
.getId();
}
@AfterEach
void tearDown() {
stockRepository.deleteAll();
}
@Test
void decrease_with_100_request_name_lock() throws InterruptedException {
// given
int threadCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch latch = new CountDownLatch(threadCount);
// when
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
try {
namedLockStockFacade.decrease(stockId);
} catch (Exception e) {
System.out.println(e);
} finally {
latch.countDown();
}
});
}
latch.await();
// then
Stock stock = stockRepository.getById(stockId);
assertThat(stock.getQuantity()).isEqualTo(0);
}
}
์ด๋ฅผ ๋ฐ๋ก ์คํํ๊ฒ ๋๋ฉด REQUIRES_NEW๋ก ์ธํด ์ปค๋ฅ์ ์ ์๋ชจ๊ฐ ์ฆ๊ฐํ์ฌ ์ฌ๋ฐ๋ฅด๊ฒ ๋์ํ์ง ์์ต๋๋ค.
์ผ๋ฐ์ ์ธ ์ ํ๋ฆฌ์ผ์ด์ ์ด๋ผ๋ฉด ์ปค๋ฅ์ ํ(DataSource)๋ฅผ ๋ถ๋ฆฌํด ์ฃผ๋ ๊ฒ์ด ์ข์ง๋ง, ์ด๋ ํ ์คํธ์ด๋ฏ๋ก ๊ฐ๋จํ ์ปค๋ฅ์ ํ์ ๋๋ ค์ ์ฒ๋ฆฌํ๋๋ก ํ๊ฒ ์ต๋๋ค.
application.yml ์ค์ ์ ๋ค์๊ณผ ๊ฐ์ด ์ปค๋ฅ์ ํ ์ฌ์ด์ฆ๋ฅผ ๋ช ์ํฉ๋๋ค.
spring:
jpa:
hibernate:
ddl-auto: create
show-sql: true
properties:
hibernate:
format_sql: true
use_sql_comments: true
highlight_sql: true
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/stock
username: root
password: 1234
hikari:
maximum-pool-size: 40 # ์ถ๊ฐ
logging:
level:
org:
hibernate:
SQL: DEBUG
type:
descriptor:
sql:
BasicBinder: TRACE
์ฌ๋ฐ๋ฅด๊ฒ ๋์ํจ์ ํ์ธํ ์ ์์ต๋๋ค.
๐ค USER-LEVEL Lock ์ฌ์ฉ ์ ์ฃผ์์
USER-LEVEL Lock์ ์ฌ์ฉํ๋ค๋ฉด Lock์ ์ป๋ ํธ๋์ญ์ ๊ณผ ๋ก์ง์ ์ํํ๋๋ฐ ์ฌ์ฉ๋๋ ํธ๋์ญ์ ์ ๋ถ๋ฆฌ์์ผ์ผ ํฉ๋๋ค.
๊ทธ๋ฆฌ๊ณ ์ด๋ค์ด ๋ถ๋ฆฌ๋๋ค๋ฉด ๊ทธ์ ๋ฐ๋ผ ์ปค๋ฅ์ ์ฌ์ฉ๋์ด ๋ง์์ง๋ฏ๋ก(Lock ํ๋์ ํ์ํ ์ปค๋ฅ์ 1๊ฐ, ๋ก์ง์ ํ์ํ ์ปค๋ฅ์ 1๊ฐ) , Lock์ ์ป๋๋ฐ ์ฌ์ฉํ๋ Connection Pool(Datasource)๊ณผ, ๋ก์ง์ ์ํํ๋๋ฐ ์ฌ์ฉ๋๋ ConnectionPool(Datasource)์ ๋ถ๋ฆฌํด ์ฃผ๋ ๊ฒ์ด ์ข์ต๋๋ค.
์ฐ์ ์ ๋ ํธ๋์ญ์ ์ ๋ถ๋ฆฌํด์ผ ํ๋์ง์ ๋ํด ์์๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
ํธ๋์ญ์ ์ด ๋ถ๋ฆฌ๋์ง ์์ ๊ฒฝ์ฐ, ๋ค์๊ณผ ๊ฐ์ ์ํฉ์ด ๋ฐ์ํ ์ ์์ต๋๋ค.
๋ฝ์ด release๋๋ ์์ ๊ณผ ํธ๋์ญ์ ์ด ์ปค๋ฐ๋๋ ์์ ์ฌ์ด์ ์๋ก์ด ์์ฒญ์ด ๋ค์ด์ ์๋ฃ๋๋ค๋ฉด ๋ค์๋๋ฝ์ ์ฌ์ฉํ๊ธฐ ์ ๊ณผ ๋ง์ฐฌ๊ฐ์ง๋ก ๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๊ฒ ๋๋ฏ๋ก, ๋ ํธ๋์ญ์ ์ ๋ถ๋ฆฌ๋์ด์ผ ํฉ๋๋ค.
๊ทธ๋ฆฌ๊ณ ํธ๋์ญ์ (์ปค๋ฅ์ )์ ๋ถ๋ฆฌํ๊ฒ ๋๋ค๋ฉด, ํ๋์ ์์ฒญ์ ๋ํด ์ต์ 2๊ฐ์ ์ปค๋ฅ์ ์ด ์ฌ์ฉ๋๊ธฐ ๋๋ฌธ์ ์ปค๋ฅ์ ๋ถ์กฑ์ผ๋ก ์ธํด ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.
์ด๋ฅผ ์๋ฐฉํ๊ธฐ ์ํด ConnectionPool(DataSource) ์์ฒด๋ฅผ ๋ถ๋ฆฌํ์ฌ ์ฌ์ฉํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.
๐ค USER-LEVEL Lock VS ๋น๊ด์ ๋ฝ
ํํ ์ด ๋์ ์ฐจ์ด๋ฅผ ์ฐพ์๋ณด๊ฒ ๋๋ฉด USER-LEVEL Lock์ timeout ์ค์ ์ด ๊ฐํธํ๋ค๋ ์ฅ์ ์ด ์๋ค๋ ์ค๋ช ์ด ๋ง์ต๋๋ค.
๊ทธ๋ฌ๋ ๋น๊ด์ ๋ฝ ์ญ์๋ @QueryHint ๋ฑ์ ํตํด์ timeout ์ค์ ์ ์ค ์ ์๊ธฐ ๋๋ฌธ์, timeout์ ์ฅ์ ์ผ๋ก ๋ณด๊ธฐ์๋ ์กฐ๊ธ ๋ถ์กฑํ ๋ถ๋ถ์ด ์๋ ๊ฒ ๊ฐ์ต๋๋ค.
user-level lock์ ๊ฒฝ์ฐ ๋ฝ์ ๋์์ด ํ ์ด๋ธ์ด๋ ๋ ์ฝ๋ ๋๋ AUTO_INCREMENT์ ๊ฐ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ฐ์ฒด๊ฐ ์๋, ๋จ์ํ ์ฌ์ฉ์๊ฐ ์ง์ ํ ์ด๋ฆ์ ๋ํด์ ๋ฝ์ ๊ฑฐ๋ ๊ฒ์ด ํน์ง์ ๋๋ค.
๋น๊ด์ ๋ฝ์ ๋ ์ฝ๋ ์์ค์ ๋ฝ์ ๊ฑธ๊ธฐ ๋๋ฌธ์, ๋ค๋ฅธ ์์ ๋ค์ ์ฒ๋ฆฌํ๊ธฐ ์ํ ์ปค๋ฅ์ ๋ค๋ ๋ฝ์ด ๊ฑธ๋ฆฐ ๋ ์ฝ๋์ ์ ๊ทผํ์ง ๋ชปํ๊ฒ ๋ฉ๋๋ค.
์ด๋ฌํ ๋ฌธ์ ๋ user-level lock์ ์ฌ์ฉํ๋ฉด ๋ฐ์ํ์ง ์์ผ๋ฉฐ, ์ด๊ฒ์ด ์ด ๋ ์ฌ์ด์ ๊ฐ์ฅ ํฐ ์ฐจ์ด์ ์ด์ง ์์๊น ์๊ฐํฉ๋๋ค.. ใ ใ ;;
๐ Reference
https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html#function_get-lock
[์ฌ๊ณ ์์คํ ์ผ๋ก ์์๋ณด๋ ๋์์ฑ์ด์ ํด๊ฒฐ๋ฐฉ๋ฒ]
[Real MySQL 8.0 1ํธ]
https://www.inflearn.com/questions/1067435/named-lock-vs-%EB%B9%84%EA%B4%80%EC%A0%81-%EB%9D%BD
https://techblog.woowahan.com/2631/
(์ด๊ณณ์ ๋๊ธ)