# ๐ ์ฐจ๊ทผ์ฐจ๊ทผ ๋์์ฑ ์ด์ ํด๊ฒฐํ๊ธฐ (4)
๐ ์ฐจ๊ทผ์ฐจ๊ทผ ๋์์ฑ ์ด์ ํด๊ฒฐํ๊ธฐ (1) - synchronized (opens new window)
๐ ์ฐจ๊ทผ์ฐจ๊ทผ ๋์์ฑ ์ด์ ํด๊ฒฐํ๊ธฐ (2) - Pessimistic Lock (opens new window)
๐ ์ฐจ๊ทผ์ฐจ๊ทผ ๋์์ฑ ์ด์ ํด๊ฒฐํ๊ธฐ (3) - Optimistic Lock (opens new window)
๐ ๐ ์ฐจ๊ทผ์ฐจ๊ทผ ๋์์ฑ ์ด์ ํด๊ฒฐํ๊ธฐ (4) - Named Lock (opens new window)
์์ฑ์ ์ฌ์ฉ๋ ์์ ์ฝ๋๋ concurrency-named-lock (opens new window)์์ ํ์ธํด๋ณผ ์ ์๋ค.
# ๋ค์๋ ๋ฝ
๋ค์๋ ๋ฝ์ GET_LOCK()
ํจ์๋ฅผ ํ์ฉํ์ฌ ์์์ ๋ฌธ์์ด์ ๋ํ ์ ๊ธ์ ์ค์ ํ ์ ์๋ค. ๋จ์ํ ์ฌ์ฉ์๊ฐ ์ง์ ํ ๋ฌธ์์ด์ ๋ํ ๋ฝ์ ํ๋ํ๊ณ ๋ฐ๋ฉํ๋ค.
# GET_LOCK(str, timeout)
๋ฌธ์์ด(str)์ ํด๋นํ๋ ๋ฝ์ ํ๋ํ๋ค.
return 1
: ๋ฝ ํ๋์ ์ฑ๊ณตํ๋ค.return 0
: timeout ๋์ ๋ฝ์ ํ๋ํ์ง ๋ชปํ๋ค.
mysql
>
SELECT GET_LOCK('1', 5);
+------------------+
| GET_LOCK('1', 5) |
+------------------+
| 1 |
+------------------+
๋จผ์ ํฐ๋ฏธ๋์ ์ ์ํ์ฌ ๋ ๊ฐ์ ์ธ์
์ ์์ฑํ๋ค. ํฐ๋ฏธ๋(1)์๋ GET_LOCK()
์ ํตํด ๋ฝ์ ํ๋ํ๋ค. ํฐ๋ฏธ๋(2)๋ ๋์ผํ๊ฒ ์ํํ๋ค.
๊ฒฐ๊ณผ์ ์ผ๋ก 1
์ ํ์ฌ ๋ค์๋ ๋ฝ์ ์ค์ ํ๊ธฐ ๋๋ฌธ์ ํฐ๋ฏธ๋(2)์์๋ ๋ฝ ํ๋์ด ๋ถ๊ฐ๋ฅํ์ฌ timeout ์ดํ 0์ ๋ฐํํ๋ค. ์ฆ ๋ฝ์ ํ๋ํ์ง ๋ชปํ์์ ์๋ฏธํ๋ค.
# RELEASE_LOCK(str)
๋ฌธ์์ด(str)์ ํด๋นํ๋ ๋ฝ์ ํด์ ํ๋ค.
return 1
: ๋ฝ์ ํด์ ํ๋ค.return null
: ํด์ ํ ๋ฝ์ด ์กด์ฌํ์ง ์๋๋ค.
๋ฝ์ ์งํํ ํฐ๋ฏธ๋(1)์์ RELEASE_LOCK()
์ ํตํด ํด์ ํ๋ฉด ํฐ๋ฏธ๋(2)์์ 1
๋ฝ์ ํ๋ํ ์ ์๋ค. ๋ฝ ํด์ ๋ ์ค์ง ๋ฝ์ ํ๋ํ ์ธ์
์์๋ง ๊ฐ๋ฅ
ํ๋ค๋ ๊ฒ์ ์ผ๋ํด๋์ด์ผ ํ๋ค.
# ๋ค์๋ ๋ฝ ์ ์ฉ
๋ค์๋ ๋ฝ์ ๋ณ๋์ DataSource
๋ฅผ ์ฐ๋ํ์ฌ Lock
์ ์ง์ ํ๋ ๊ฒ์ด ๋ฐ๋์งํ๋ค. ๋ง์ฝ ๋ฐ์ดํฐ๊ฐ ๋ด๊ธด DataSource
๋ฅผ ๊ณต์ ํด์ ์ฌ์ฉํ ๊ฒฝ์ฐ ๋ฝ์ ๊ฑธ๊ธฐ ์ํด ๋ณ๋์ ์ปค๋ฅ์
์ด ํ์ํ๋ฉฐ ์ ์ ํ๋
์๊ฐ์ด ๊ธธ์ด์ง๋ฉด ํ์ ๋ ์์์ธ ์ปค๋ฅ์
ํ ๋ถ์กฑ์ผ๋ก ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์๋ค.
๋จผ์ ์ผ๋ฐ์ ์ธ ๋ฐ์ดํฐ ์ ์ฅ์ ์ํ DataSource
์ค์ ๊ณผ ๋ฝ ๊ด๋ จ DataSource
์ ๋ณด๋ฅผ ๋ช
์ํ๋ค.
spring:
# ...
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://127.0.0.1:3306/products
username: root
password: root
user-lock:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://127.0.0.1:3307/user_lock
username: root
password: root
# ...
spring.datasource
๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ฌ์ฉ๋๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ๋ณด๋ฅผ ์ค์ ํ๋ค. user-lock.datasource
์๋ ๋ค์๋ ๋ฝ์ ์ฌ์ฉํ๊ธฐ ์ํ DataSource
๋ฅผ ์ค์ ํ๋ค. ๋ณ๋๋ก ์ค์ ์ ๋ณด๋ฅผ
๊ด๋ฆฌํ๋ฉด ๊ฐ๊ฐ ์ปค๋ฅ์
ํ์ ๊ฐ์๋ฅผ ๊ด๋ฆฌํ ์ ์๋ค.
์ด์ ์ค์ ์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก DataSource
๋ฅผ Bean
์ผ๋ก ๋ฑ๋กํ๋ค.
@Configuration
public class DataSourceConfig {
@Bean
@Primary
@ConfigurationProperties("spring.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create()
.build();
}
@Bean
@ConfigurationProperties("user-lock.datasource")
public DataSource lockDataSource() {
return DataSourceBuilder.create()
.build();
}
}
์ฃผ ์ฌ์ฉ๋ DataSource
๋ @Primary
์ ๋
ธํ
์ด์
์ ํ์ฉํ๋ค. ์๋๋ ๋ฝ ๊ด๋ จ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ๊ทผ์ ์ํ lockDataSource
์ด๋ค.
์ด์ ๋ณต์กํ ์ฝ๋๊ฐ ๋ค์ ๋ฑ์ฅํ ์์ ์ด๋ค. ์์ ์ธ๊ธํ ๊ฒ ์ฒ๋ผ ๋ฝ์ ํ๋๊ณผ ์ ๊ฑฐ๋ ๊ฐ์ ์ธ์
์์ ์ด๋ฃจ์ด์ ธ์ผ ํ๋ค. ์ฆ ๊ฐ์ ์ปค๋ฅ์
๋ด๋ถ์์ ์ผ์ด๋์ผ ํ๋ ์ผ๋ จ์ ๊ณผ์ ์ด๋ค. ํธ๋์ญ์
์ ํ์ฉํ๋ฉด ๋น ๋ฅด๊ฒ ๋์์ด
๊ฐ๋ฅ ํ๊ฒ ์ง๋ง DataSource
๊ฐ ๋๊ฐ ์ด๋ฏ๋ก ๋ฐ์ดํฐ ์กฐ์์ ์ํ ๋ก์ง๊ณผ ๋ฝ ๊ด๋ จ ๋ก์ง์ ๋ณ๋์ ํธ๋์ญ์
์ผ๋ก ๋ถ๋ฆฌํด์ผ ํ๋ค.
๋จผ์ ๋น์ฆ๋์ค ๋ก์ง์ด ๋ด๊ธด ProductService
์ด๋ค.
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(final ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Transactional
public void purchase(final Long id, final Long quantity) {
var foundProduct = getProduct(id);
foundProduct.decrease(quantity);
}
private Product getProduct(final Long id) {
return productRepository.findById(id)
.orElseThrow(NoSuchElementException::new);
}
}
์ด์ ์๊ฐ์ ๋ค๋ฃฌ Service
์ ๊ฑฐ์ ์ ์ฌํ๋ค. @Transactional
์ ์ฐ๋ฆฌ๊ฐ @Primary
๋น์ผ๋ก ๋ฑ๋กํ DataSource๋ก ์ค์ ํ ์ปค๋ฅ์
์ ํ๋ํ ๊ฒ์ด๋ค. ์ด์ ๋ฝ ๊ด๋ จ ๋ก์ง์ ์ถ๊ฐํ์.
UserLockProductFacade
๋ ๋ฝ ๊ด๋ จ ๊ธฐ๋ฅ์ด ์ถ๊ฐ๋ Facade
์ด๋ค. UserLockTemplate
์ executeWithLockWithoutResult()
๋ฅผ ํตํด ๋ฝ ์ค์ ์ ๋ณด์ ํ์๋ฅผ
์ ๋ฌํ๊ณ ์๋ค. ์ด์ executeWithLockWithoutResult()
๋ด๋ถ๋ฅผ ์ดํด๋ณด์.
@Component
public class UserLockTemplate {
private static final String GET_LOCK = "SELECT GET_LOCK(?, ?)";
private static final String RELEASE_LOCK = "SELECT RELEASE_LOCK(?, ?)";
private final DataSource dataSource;
public UserLockTemplate(@Qualifier("lockDataSource") final DataSource dataSource) {
this.dataSource = dataSource;
}
public void executeWithLockWithoutResult(final String userLockName, double timeout, final Executor callback) {
executeWithLock(userLockName, timeout, () -> {
callback.execute();
return null;
});
}
public <T> T executeWithLock(final String userLockName, double timeout, final Supplier<T> supplier) {
try (var connection = dataSource.getConnection()) {
return execute(connection, userLockName, timeout, supplier);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private <T> T execute(final Connection connection, final String userLockName, final double timeout,
final Supplier<T> supplier) {
try {
getLock(connection, userLockName, timeout);
return supplier.get();
} finally {
releaseLock(connection, userLockName);
}
}
private void getLock(final Connection connection, final String userLockName, double timeout) {
try (var preparedStatement = connection.prepareStatement(GET_LOCK)) {
preparedStatement.setString(1, userLockName);
preparedStatement.setDouble(2, timeout);
preparedStatement.executeQuery();
} catch (final SQLException e) {
throw new RuntimeException(e);
}
}
private void releaseLock(final Connection connection, final String userLockName) {
try (var preparedStatement = connection.prepareStatement(RELEASE_LOCK)) {
preparedStatement.setString(1, userLockName);
preparedStatement.executeQuery();
} catch (final SQLException e) {
throw new RuntimeException(e);
}
}
}
UserLockTemplate
์ ๋ฝ๊ณผ ํจ๊ป ํ์๋ฅผ ์ํํ๊ธฐ ์ํ ๊ฐ์ฒด์ด๋ค. TransactionTemplate
๊ณผ ์ ์ฌํ ๋ฐฉ๋ฒ์ผ๋ก ๊ตฌํ ํด๋ณด์๋ค.
@Qualifier("lockDataSource") final DataSource dataSource
:@Primary
๋ก ์ ํํ dataSource๊ฐ ์๋lockDataSource
๋ฅผ ์ฃผ์ ๋ฐ๋๋ค.executeWithLockWithoutResult()
: ๋ฐํ์ด ํ์ํ์ง ์๋ ํ์๋ฅผ ์ ๋ฌํ ๋ ์ฌ์ฉํ๋ ํธ์ ๋ฉ์๋์ด๋ค.executeWithLock()
: ๋ฝ ๊ธฐ๋ฅ๊ณผ ํจ๊ป ์ํํ๊ธฐ ์ํ ๋ฉ์๋์ด๋ค. connection ๊ฐ์ฒด๋ฅผ ์์ ํ๊ฒ ๋ซ๊ธฐ ์ํ ์ฑ ์์ ๊ฐ์ง๊ณ ์๋ค.execute()
: ์ ๋ฌ๋ ํ์๋ฅผ ์ค์ง์ ์ผ๋ก ๋ฝ๊ณผ ํจ๊ป ์ํํ๊ธฐ ์ํ ๋ฉ์๋์ด๋ค. ํต์ฌ์finally
์ ๋ฝ ๋ฐ๋ฉ ๋ฉ์๋๋ฅผ ์์น ์์ผ ๋ฌด์กฐ๊ฑด ๋ฐ๋ฉํจ์ ๋ณด์ฅํ๋ค.getLock()
: ๋ฝ์ ํ๋ํ๋ค.releaseLock()
: ๋ฝ์ ๋ฐ๋ฉํ๋ค.
์ฌ์ฉ ๋ฐฉ๋ฒ์ ๊ฐ๋จํ๋ค. ๋ฝ์ ์ง์ ํ๊ณ ์ถ์ ํ์๋ฅผ ์ ๋ฌํ๊ธฐ๋ง ํ๋ฉด ๋๋ค.
@Component
public class UserLockProductFacade {
private final UserLockTemplate userLockTemplate;
private final ProductService productService;
public UserLockProductFacade(final UserLockTemplate userLockTemplate, final ProductService productService) {
this.userLockTemplate = userLockTemplate;
this.productService = productService;
}
public void purchase(final Long id, final Long quantity) {
userLockTemplate.executeWithLockWithoutResult(generateUserLockName(id), 5, () -> {
productService.purchase(id, quantity);
});
}
private String userLockNameGenerate(final Long id) {
return id.toString();
}
}
๋ค์๋ ๋ฝ์ ์ด๋ฆ์ id
๋ฅผ ํ์ฉ ํ๊ณ , timeout
์ 5์ด
๋ฅผ ์ง์ ํ๋ค.
์ด์ ํ ์คํธ๋ฅผ ์ํํด๋ณด์! ํ ์คํธ ์ฝ๋๋ ์ด์ ๊ณผ ๊ฑฐ์ ์ ์ฌํ๋ค.
@SpringBootTest
@DisplayNameGeneration(ReplaceUnderscores.class)
class UserLockProductFacadeTest {
private final ProductRepository productRepository;
private final UserLockProductFacade userLockProductFacade;
@Autowired
UserLockProductFacadeTest(final ProductRepository productRepository,
final UserLockProductFacade userLockProductFacade) {
this.productRepository = productRepository;
this.userLockProductFacade = userLockProductFacade;
}
@Test
void ๋์์_100๊ฐ์_์ํ์_๊ตฌ๋งคํ๋ค() throws InterruptedException {
var product = productRepository.save(new Product("์นํจ", 100L));
var executorService = Executors.newFixedThreadPool(10);
var countDownLatch = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
executorService.submit(() -> process(product, countDownLatch));
}
countDownLatch.await();
var actual = productRepository.findById(product.getId()).orElseThrow();
assertThat(actual.getQuantity()).isEqualTo(0L);
}
private void process(final Product product, final CountDownLatch countDownLatch) {
try {
userLockProductFacade.purchase(product.getId(), 1L);
} finally {
countDownLatch.countDown();
}
}
}
ํ ์คํธ๋ ์ ์์ ์ผ๋ก ์ํ๋๋ค!
# ๋จ์ ๋ฌธ์ ๋ค
๋ค๋ง ํ ๊ฐ์ง ๋ฌธ์ ๊ฐ ๋จ์์๋ค. ์ง๊ธ์ ๋ฝ์ ํ๋ํ๊ธฐ ์ํ ๋๊ธฐ ์๊ฐ์ด ์ต๋ 5์ด์ด๋ค. ๋ง์ฝ 5์ด ์ด์์ด ๊ฑธ๋ฆฌ๋ ๋ก์ง์ด๋ผ๋ฉด ๊ฐ์ฐจ ์์ด ๋ฝ์ ํ๋ํ์ง ๋ชปํ๊ณ ์์ธ๋ฅผ ๋์ง ๊ฒ์ด๋ค.
๊ฐ๋จํ ํ์ธ์ ์ํด ๋ฝ ๋๊ธฐ ์๊ฐ์ 0.1
์ด๋ก ์กฐ์ ํด๋ณธ ๋ค ํ
์คํธ๋ฅผ ์งํํด๋ณธ๋ค.
@Component
public class UserLockProductFacade {
// ...
public void purchase(final Long id, final Long quantity) {
userLockTemplate.executeWithLockWithoutResult(generateUserLockName(id), 0.1, () -> {
productService.purchase(id, quantity);
});
}
// ...
}
์์ฝ๊ฒ ํ ์คํธ๊ฐ ์คํจํ๋ค. ๋ฝ ํ๋์ ์ํ ๋๊ธฐ ์๊ฐ์ ์ถฉ๋ถํ ์ค์ ํด์ฃผ๋ ๊ฒ์ด ์ข์ ๋ณด์ธ๋ค.
# ์ ๋ฆฌ
์ง๊ธ ๊น์ง ๋ค์๋ ๋ฝ์ ํ์ฉํ ๋์์ฑ ์ด์๋ฅผ ํด๊ฒฐํ์๋ค. ๋ค์๋ ๋ฝ๋ ๋ถ์ฐ๋ฝ
์ ์ผ์ข
์ด๋ค. ๋ถ์ฐ๋ฝ์ ์ ์ ์์ฒด๊ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๊ฐ์ ๊ณตํต๋ ์ ์ฅ์๋ฅผ ํตํด ์์์ด ์ฌ์ฉ ์ค์ธ์ง ์ฌ๋ถ๋ฅผ ํ์ธํ๋ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ด๋ค.
๋ค๋ง ๋ณ๋์ DataSource
๋ก ๊ตฌ์ฑํ ๊ฒฝ์ฐ ๊ฐ๋ฐ์๊ฐ ์ถ๊ฐ์ ์ผ๋ก ์ค์ ํด์ค์ผ ํ๋ ๋ก์ง์ด ๋๋ฌด ๋ง๋ค. ๋ํ ๋ฝ ํ๋์ ์คํจ ํ์ ๋์ ๋ํ ๋ก์ง๋ ๊ณ ๋ คํด์ผ ํ๋ค. ๋ ๋ค๋ฅธ ๋จ์ ์ผ๋ก๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ๊ทผ์ ํ๋ค๋ณด๋
๋ฉ๋ชจ๋ฆฌ ์ ์ฅ์์ ๋นํด ๋๋ฆฐ ์๋๋ฅผ ๊ฐ์ง ๊ฒ์ด๋ค. ๋ํ ํน์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ข
์์ ์ธ ๊ตฌ์กฐ๊ฐ ๋์ฌ ์ ๋ฐ์ ์๋ค. ์ ์ฐํ๊ฒ ์ฌ์ฉํ๊ธฐ ์ํด์๋ ๊ฐ๋ฐ์๊ฐ ์ถ๊ฐ์ ์ผ๋ก ์ถ์ํ ๊ณผ์ ์ ๊ฑฐ์ณ์ผ ํ ๊ฒ ๊ฐ๋ค.
๋จ์ ๋ง ๋์ดํ ๊ฒ ๊ฐ์ง๋ง MySQL์ ๋ํ ์ง์์ด ์ถฉ๋ถํ๋ค๋ฉด ๊ณ ๋ ค ํด๋ณผ๋ง ํ๋ค. ๊ฒฐ๊ตญ ๋ถ์ฐ๋ฝ์ด๊ธฐ ๋๋ฌธ์ ๋ค์ค ์ ํ๋ฆฌ์ผ์ด์ ์๋ฒ์ ๋์์ด ๊ฐ๋ฅํด์ง๋ค.
์ด๋ฒ ๊ณผ์ ์ ํตํด์ ์ ๋งคํ๊ฒ ์๊ณ ์๋ ๋ค์๋ ๋ฝ์ ๋ํ ์ง์๊ณผ ํ์ฉ ๋ฐฉ๋ฒ์ ์ต๋ํ ์ ์์๋ค. ๋ค์ ์๊ฐ์๋ redis๋ฅผ ํ์ฉํ ๋ถ์ฐ๋ฝ์ ๋ํด ์์๋ณด๋ ค ํ๋ค.
# References.
MySQL์ ์ด์ฉํ ๋ถ์ฐ๋ฝ์ผ๋ก ์ฌ๋ฌ ์๋ฒ์ ๊ฑธ์น ๋์์ฑ ๊ด๋ฆฌ (opens new window)
์ฌ๊ณ ์์คํ
์ผ๋ก ์์๋ณด๋ ๋์์ฑ์ด์ ํด๊ฒฐ๋ฐฉ๋ฒ (opens new window)