lock

[Anifriends] ๋ด‰์‚ฌ ์‹ ์ฒญ์—์„œ ๋ฐœ์ƒํ•œ ๋™์‹œ์„ฑ ์ด์Šˆ ํ•ด๊ฒฐ๊ธฐ

pushedrumex 2024. 2. 13. 00:55

๐Ÿš€  ๊ฐœ์š”

๋ฐ๋ธŒ์ฝ”์Šค 4๊ธฐ์—์„œ ๋ด‰์‚ฌ ํ”Œ๋žซํผ์ธ Anifriends ๋ฅผ ๊ฐœ๋ฐœํ•˜๋ฉด์„œ ๊ฒช์€ ๋™์‹œ์„ฑ ์ด์Šˆ ํ•ด๊ฒฐ๊ธฐ์ด๋‹ค. ๋ณดํ˜ธ์†Œ๊ฐ€ ๊ฒŒ์‹œํ•œ ๋ด‰์‚ฌ ๋ชจ์ง‘์—๋Š” ์ •์›์ด ์กด์žฌํ•˜๋ฉฐ ๋ด‰์‚ฌ์ž๋“ค์ด ๋™์‹œ์— ๋™์ผํ•œ ๋ด‰์‚ฌ ๋ชจ์ง‘์— ๋ด‰์‚ฌ ์‹ ์ฒญ์„ ํ•˜๋Š” ๊ฒฝ์šฐ ๋™์‹œ์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์—๋Š” ์—ฌ๋Ÿฌ๊ฐ€์ง€๊ฐ€ ์žˆ๋‹ค. ๋น„๊ด€์ ๋ฝ, ๋‚™๊ด€์ ๋ฝ, Atomic Query, MySQL Named lock, Redis (Lettuce, Redisson)์„ ์‚ฌ์šฉํ•˜์—ฌ ์„ฑ๋Šฅ์„ ๋น„๊ตํ•ด๋ณด๊ณ ์žํ•œ๋‹ค.

๐Ÿšจ ๋™์‹œ์„ฑ ๋ฌธ์ œ ๋ฐœ์ƒ

๋ด‰์‚ฌ๋ฅผ ์‹ ์ฒญํ–ˆ์„ ๋•Œ ์ฒ˜๋ฆฌ๋˜๋Š” ๋กœ์ง์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

@Transactional
public void registerApplicant(Long recruitmentId, Long volunteerId) {

    Recruitment recruitment = getRecruitment(recruitmentId);
    Volunteer volunteer = getVolunteer(volunteerId);

    Applicant applicant = new Applicant(recruitment, volunteer);

    applicantRepository.save(applicant);

}
  1. recruitmentId ์— ํ•ด๋‹นํ•˜๋Š” recruitment ๋ฅผ ์กฐํšŒ
  2. volunteerId ์— ํ•ด๋‹นํ•˜๋Š” volunteer ๋ฅผ ์กฐํšŒ
  3. applicant ์ƒ์„ฑ
    1. ์ฐธ์—ฌ ๊ฐ€๋Šฅํ•œ(์ •์›์ด ๊ฐ€๋“์ฐจ์ง€ ์•Š์€) recruitment ์ธ์ง€ ๊ฒ€์ฆ
    2. recruitment ์˜ applicantCount + 1
  4. ์ƒ์„ฑ๋œ applicant ์ €์žฅ

๐Ÿงช  ๋™์‹œ์„ฑ ํ…Œ์ŠคํŠธ

์ •์›์ด 50๋ช…์ธ ๋ด‰์‚ฌ ๋ชจ์ง‘์— ๋ด‰์‚ฌ์ž 100๋ช…์ด ๋™์‹œ์—  ๋ด‰์‚ฌ ์‹ ์ฒญ์„ ํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜์˜€๋‹ค. ์ •์ƒ์ ์ธ ๋กœ์ง์—์„œ๋Š” 50๋ช…์˜ ๋ด‰์‚ฌ์ž๊ฐ€ ๋ด‰์‚ฌ ์‹ ์ฒญ์„ ์„ฑ๊ณตํ•˜์—ฌ ํ•ด๋‹น ๋ด‰์‚ฌ ๋ชจ์ง‘์˜ ์‹ ์ฒญ์ž๋Š” 50๋ช…์ด์–ด์•ผํ•œ๋‹ค.

@Test
@DisplayName("์„ฑ๊ณต: 50๋ช… ์ •์›, 100๋ช… ๋™์‹œ ์‹ ์ฒญ")
void registerApplicant() throws InterruptedException {
    // given
    int capacity = 50;
    List<Volunteer> volunteers = VolunteerFixture.volunteers(capacity);
    Recruitment recruitment = RecruitmentFixture.recruitment(shelter, capacity);
    volunteerRepository.saveAll(volunteers);
    recruitmentRepository.save(recruitment);

    int poolSize = 100;
    ExecutorService executorService = Executors.newFixedThreadPool(poolSize);
    CountDownLatch latch = new CountDownLatch(poolSize);

    // when
    for (int i = 0; i < poolSize; i++) {
        int finalI = i;
        executorService.submit(() -> {
            try {
                applicantService.registerApplicant(shelter.getShelterId(),volunteers.get(finalI).getVolunteerId());
            } finally {
                latch.countDown();
            }
        });
    }
    
    latch.await();

    // then
    Recruitment persistRecruitment = recruitmentRepository.findById(recruitment.getRecruitmentId()).get();
    
    SoftAssertions.assertSoftly(softly -> {
        softly.assertThat(getApplicants(persistRecruitment)).hasSize(capacity);
        softly.assertThat(persistRecruitment.getApplicantCount()).isEqualTo(capacity);
    });
    
}

์‹คํ–‰ ๊ฒฐ๊ณผ

ํ•˜์ง€๋งŒ ๊ฒฐ๊ณผ๋Š” 50๋ช…์ด์–ด์•ผํ•˜๋Š” applicant๋Š” 9๋ช…, recruitment์˜ applicant_count๋Š” 5๋ช…์ด๋‹ค.

๊ทธ ์ด์œ ๋Š” ๋‘ ๊ฐœ ์ด์ƒ์˜ ํŠธ๋žœ์žญ์…˜์—์„œ ๊ณต์œ ์ž์›์ธ recruitment ์— ๋™์‹œ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์ด๋Ÿฌํ•œ ์ƒํ™ฉ์€ ํฐ ๋ฌธ์ œ๋ฅผ ์ดˆ๋ž˜ํ•œ๋‹ค.

์ฒซ ๋ฒˆ์งธ๋Š” ์—…๋ฐ์ดํŠธ ๋ˆ„๋ฝ์ด๋‹ค. ๋งŒ์•ฝ ์ •์›์ด 1๋ช…์ธ ๋ด‰์‚ฌ ๋ชจ์ง‘์— 2๋ช…์˜ ์‚ฌ์šฉ์ž๊ฐ€ ๋™์‹œ์— ์‹ ์ฒญ์„ ํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜์ž. ๊ทธ๋ ‡๋‹ค๋ฉด ๋‘๊ฐœ์˜ ํŠธ๋žœ์žญ์…˜์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‹ ์ฒญ ์ธ์›์ด 0์ธ recruitment ๋ฅผ ์กฐํšŒํ•˜๊ณ  ์‹ ์ฒญ ์ธ์›์„ (0 + 1) ๋ช…์œผ๋กœ ๊ฐฑ์‹ ํ•œ๋‹ค. ๋ถ„๋ช… ํ•ด๋‹น ๋ชจ์ง‘์˜ ์ •์›์€ 1๋ช…์ธ๋ฐ 2๋ช…์˜ ๋ด‰์‚ฌ์ž๊ฐ€ ์‹ ์ฒญ์„ ์„ฑ๊ณตํ•˜๊ฒŒ ๋œ๋‹ค. ์ตœ์ข…์ ์œผ๋กœ ์‹ ์ฒญ ์ธ์›์€ 1๋ช…์œผ๋กœ ์ €์žฅ๋˜๋ฉฐ ์—…๋ฐ์ดํŠธ๊ฐ€ ๋ˆ„๋ฝ๋œ๋‹ค. ์ด ๊ฒฝ์šฐ, ์ •ํ•ฉ์„ฑ์ด ๋งž์ง€ ์•Š๊ฒŒ ๋˜๊ณ  ๋ด‰์‚ฌ ๋ชจ์ง‘์˜ ์ •์›์„ ์ดˆ๊ณผํ•˜์—ฌ ๋ด‰์‚ฌ ์‹ ์ฒญ์„ ํ•˜๊ฒŒ ๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

๋‘ ๋ฒˆ์งธ๋Š” ๋ฐ๋“œ๋ฝ์ด๋‹ค. ๋กœ๊ทธ๋ฅผ ์‚ดํŽด๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ฐ๋“œ๋ฝ์ด ๋ฐœ์ƒํ•œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ๋ฐ๋“œ๋ฝ์€ ๋ฌด์—‡์ด๋ฉฐ ์™œ ๋ฐœ์ƒํ•˜๋Š”๊ฑธ๊นŒ?

๐Ÿ”„ ๋ฐ๋“œ๋ฝ

MySQL ๊ณต์‹ ๋ฌธ์„œ์— ๋‚˜์™€์žˆ๋Š” ๋ฐ๋“œ๋ฝ์˜ ์ •์˜๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

A deadlock is a situation where different transactions are unable to proceed because each holds a lock that the other needs. Because both transactions are waiting for a resource to become available, neither ever release the locks it holds.

์„œ๋กœ ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์ด ์„œ๋กœ์—๊ฒŒ ํ•„์š”ํ•œ ์ž ๊ธˆ์„ ๋ณด์œ ํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ง„ํ–‰ํ•  ์ˆ˜ ์—†๋Š” ์ƒํ™ฉ์ด๋‹ค. ๋‘ ํŠธ๋žœ์žญ์…˜ ๋ชจ๋‘ ๋ฆฌ์†Œ์Šค๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜๊ธฐ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์–ด๋А ์ชฝ๋„ ๋ณด์œ ํ•˜๊ณ  ์žˆ๋Š” ์ž ๊ธˆ์„ ํ•ด์ œํ•˜์ง€ ๋ชปํ•œ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด ํŠธ๋žœ์žญ์…˜์ด ์–ด๋–ค ์ž ๊ธˆ์„ ๋ณด์œ ํ–ˆ๊ธธ๋ž˜ ๋ฐ๋“œ๋ฝ์ด ๋ฐœ์ƒํ•œ ๊ฑธ๊นŒ? ๋‹ค์Œ ๋ช…๋ น์–ด๋ฅผ ํ†ตํ•ด ๊ฐ€์žฅ ์ตœ๊ทผ์— ๋ฐœ์ƒํ•œ ๋ฐ๋“œ๋ฝ ์ •๋ณด๋ฅผ ํ™•์ธํ•ด๋ณด์ž.

show engine innodb status;

.... ๋ณต์žกํ•˜๊ณ  ๋ˆˆ ์•„ํ”„๋‹ค. ์ค‘์š”ํ•œ ๋ถ€๋ถ„๋งŒ ํ™•์ธํ•ด๋ณด์ž.

1. 90105675๋ฒˆ ํŠธ๋žœ์žญ์…˜์ด recruitment ์— s-lock ์„ ๋ณด์œ ํ•œ๋‹ค.

2. 90105676๋ฒˆ ํŠธ๋žœ์žญ์…˜์ด recruitment ์— s-lock ์„ ๋ณด์œ ํ•œ๋‹ค.

3. 90105675๋ฒˆ ํŠธ๋žœ์žญ์…˜์ด recruitment ์˜ x-lock ์„ ์–ป๊ธฐ ์œ„ํ•ด ๋Œ€๊ธฐ ์ค‘์ด๋‹ค.

4. 90105676๋ฒˆ ํŠธ๋žœ์žญ์…˜์ด recruitment ์— x-lock ์„ ์–ป๊ธฐ ์œ„ํ•ด ๋Œ€๊ธฐ ์ค‘์ด๋‹ค.

5. ๋‘ ๋ฒˆ์งธ ํŠธ๋žœ์žญ์…˜ ์ฆ‰, 90105676๋ฒˆ ํŠธ๋žœ์žญ์…˜์„ ๋กค๋ฐฑํ•œ๋‹ค.

์ •๋ฆฌํ•ด๋ณด๋ฉด ๋‘ ๊ฐœ์˜ ํŠธ๋žœ์žญ์…˜์ด recruitment ์— ๋Œ€ํ•œ s-lock ์„ ๋ณด์œ ํ•˜๊ณ  ์žˆ๋Š” ์ƒํƒœ์—์„œ x-lock ์„ ์–ป๊ธฐ๋ฅผ ๋Œ€๊ธฐํ•˜๊ณ  ์žˆ๋‹ค. x-lock ์„ ์–ป๊ธฐ ์œ„ํ•ด์„  ์„œ๋กœ์˜ s-lock ๋ฐ˜๋‚ฉ์ด ํ•„์š”ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฐ๋“œ๋ฝ์ด ๋ฐœ์ƒํ•œ ๊ฒƒ์ด๋‹ค.

๋ฐ๋“œ๋ฝ ๋ฐœ์ƒ ์›์ธ์— ๋Œ€ํ•ด์„œ๋Š” ํŒŒ์•…ํ–ˆ์ง€๋งŒ, ํ•œ๊ฐ€์ง€ ์˜๋ฌธ์ด ์žˆ๋‹ค. recruitment ์— s-lock ์„ค์ •์„ ๋”ฐ๋กœ ํ•˜์ง€ ์•Š์•˜๋Š”๋ฐ s-lock ์ด ๊ฑธ๋ฆฐ ์ด์œ ๊ฐ€ ๋ฌด์—‡์ผ๊นŒ?

๊ทธ ์ด์œ ๋Š” FK ์ œ์•ฝ ์กฐ๊ฑด์— ์žˆ๋‹ค. MySQL ๊ณต์‹๋ฌธ์„œ์— ๋”ฐ๋ฅด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋‚ด์šฉ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

In an SQL statement that inserts, deletes, or updates many rows, foreign key constraints (like unique constraints) are checked row-by-row. When performing foreign key checks, InnoDB sets shared row-level locks on child or parent records that it must examine.

๋งŽ์€ ํ–‰์„ ์‚ฝ์ž…, ์‚ญ์ œ ๋˜๋Š” ๊ฐฑ์‹ ํ•˜๋Š” SQL๋ฌธ์—์„œ ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ์กฐ๊ฑด(๊ณ ์œ ์ œ์•ฝ์กฐ๊ฑด ๋“ฑ)์€ ํ–‰๋‹จ์œ„๋กœ ๊ฒ€์‚ฌ๋œ๋‹ค. ์™ธ๋ž˜ํ‚ค ๊ฒ€์‚ฌ๋ฅผ ์ˆ˜ํ–‰ํ• ๋•Œ InnoDB๋Š” ๊ฒ€์‚ฌํ•ด์•ผํ•˜๋Š” ์ž์‹ ๋˜๋Š” ๋ถ€๋ชจ ๋ ˆ์ฝ”๋“œ์— shared row-level lock์„ ์„ค์ •ํ•œ๋‹ค.

Anifriends ์˜ ERD ์ผ๋ถ€๋ถ„์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค. recruitment(๋ชจ์ง‘๊ธ€)์™€ applicant(๋ด‰์‚ฌ ์‹ ์ฒญ์ž)๊ฐ€ 1:N ๊ด€๊ณ„์ด๋ฉฐ applicant ๋Š” recruitment_id ๋ฅผ FK ๋กœ ๊ฐ–๋Š”๋‹ค.

๋”ฐ๋ผ์„œ applicant ๋ฅผ insert ํ•  ๋•Œ FK ์ธ recruitment ์— s-lock ์ด ๊ฑธ๋ฆฌ๊ฒŒ ๋˜๋Š” ๊ฒƒ์ด๋‹ค. ๋ด‰์‚ฌ ์‹ ์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋กœ์ง์„ ๋‹ค์‹œ ํ•œ๋ฒˆ ์‚ดํŽด๋ณด์ž.

@Transactional
public void registerApplicant(Long recruitmentId, Long volunteerId) {

    Recruitment recruitment = getRecruitment(recruitmentId);
    Volunteer volunteer = getVolunteer(volunteerId);

    Applicant applicant = new Applicant(recruitment, volunteer);

    applicantRepository.save(applicant);

}

์ฐธ๊ณ ๋กœ Hibernate flush() ์˜ ์ฟผ๋ฆฌ ๋™์ž‘ ์ˆœ์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

๋ฐ๋“œ๋ฝ์ด ๋ฐœ์ƒํ•œ ๊ณผ์ •์€ ๊ฐ„๋‹จํ•˜๊ฒŒ ์ •๋ฆฌํ•ด๋ณด์ž.

  1. 1๋ฒˆ ํŠธ๋žœ์žญ์…˜์ด applicant ๋ฅผ insert ํ•˜๋Š” ๊ณผ์ •์—์„œ FK ์ธ recruitment ์— s-lock ํš๋“
  2. 2๋ฒˆ ํŠธ๋žœ์žญ์…˜์ด applicant ๋ฅผ insert ํ•˜๋Š” ๊ณผ์ •์—์„œ FK ์ธ recruitment ์— s-lock ํš๋“
  3. 1๋ฒˆ ํŠธ๋žœ์žญ์…˜์ด recruitment ์˜ applicant_count ๋ฅผ update ํ•˜๊ธฐ ์œ„ํ•ด x-lock ํš๋“ ๋Œ€๊ธฐ
  4. 2๋ฒˆ ํŠธ๋žœ์žญ์…˜์ด recruitment ์˜ applicant_count ๋ฅผ update ํ•˜๊ธฐ ์œ„ํ•ด x-lock ํš๋“ ๋Œ€๊ธฐ
  5. ๋ฐ๋“œ๋ฝ ๋ฐœ์ƒ
  6. 1, 2๋ฒˆ ํŠธ๋žœ์žญ์…˜ ์ค‘ ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ (MySQL Deadlock Detection)

โœ… ๋น„๊ด€์  ๋ฝ

๋น„๊ด€์  ๋ฝ์ด๋ž€ ์ž์› ์š”์ฒญ์— ๋”ฐ๋ฅธ ๋™์‹œ์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ๊ฒƒ์ด๋ผ๊ณ  ์˜ˆ์ƒํ•˜๊ณ  DB ๋ฝ์„ ๊ฑธ์–ด๋ฒ„๋ฆฌ๋Š” ๋ฐฉ์‹์ด๋‹ค. ๋จผ์ € ๋ฝ์„ ํš๋“ํ•œ ์Šค๋ ˆ๋“œ๋งŒ์ด ํ•ด๋‹น ์ž์›์ด ์ ‘๊ทผํ•  ๊ถŒํ•œ์„ ๊ฐ–๊ฒŒ ๋˜๊ณ , ๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ๋“ค์€ ๋ฝ์„ ํš๋“ํ•˜๊ธฐ ์œ„ํ•ด ๋Œ€๊ธฐํ•˜๊ฒŒ ๋œ๋‹ค.

LockModeType ์ข…๋ฅ˜๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  • PESSIMISTIC_READ: s-lock ์„ ํš๋“ํ•˜๊ณ  ๋ฐ์ดํ„ฐ๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜๊ฑฐ๋‚˜ ์‚ญ์ œ๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€
  • PESSIMISTIC_WRITE: x-lock ์„ ํš๋“ํ•˜๊ณ  ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ(locking ์กฐํšŒ์ผ ๊ฒฝ์šฐ), ์—…๋ฐ์ดํŠธ ๋˜๋Š” ์‚ญ์ œ๋ฅผ ๋ฐฉ์ง€
  • PESSIMISTIC_FORCE_INCREMENT: PESSIMISTIC_WRITE ์ฒ˜๋Ÿผ ์ž‘๋™ํ•˜๋ฉฐ ๋ฒ„์ „์ด ์ง€์ •๋œ ์—”ํ„ฐํ‹ฐ์˜ ๋ฒ„์ „ ์†์„ฑ์„ ์ถ”๊ฐ€๋กœ ์ฆ๊ฐ€

์กฐํšŒ ์‹œ x-lock ์„ ํš๋“ํ•˜๋„๋ก ํ•˜๋Š”๊ฒŒ ๋ชฉ์ ์ด๊ธฐ ๋•Œ๋ฌธ์— PESSIMISTIC_WRITE ๋กœ ์„ค์ •ํ•œ๋‹ค. (์ฐธ๊ณ ๋กœ Mysql ์—์„œ๋Š” PESSIMISTIC_READ ๋„ PESSIMISTIC_WRITE ์ฒ˜๋Ÿผ ๋™์ž‘ํ•œ๋‹ค.)

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select r from Recruitment r where r.recruitmentId = :recruitmentId")
Optional<Recruitment> findByIdPessimistic(@Param("recruitmentId") Long recruitmentId);

๋น„๊ด€์  ๋ฝ์„ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ select for update ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. select for update ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด Exclusive lock (x lock)์ด ๊ฑธ๋ฆฌ๊ฒŒ ๋œ๋‹ค. x lock์ด ๊ฑธ๋ฆฌ๊ฒŒ ๋˜๋ฉด, ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์—์„œ s lock ์ด๋‚˜ x lock ์„ ํš๋“ํ•  ์ˆ˜ ์—†๋‹ค. ์ฆ‰, ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์—์„œ๋Š” ์กฐํšŒ, ์ˆ˜์ •, ์‚ญ์ œ ๋ชจ๋‘ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค.(๋‹จ, non-locking ์กฐํšŒ๋Š” ๊ฐ€๋Šฅ) ์ฆ‰, x lock์„ ํš๋“ํ•˜์ง€ ๋ชปํ•œ ํŠธ๋žœ์žญ์…˜์€ x lock์„ ํš๋“ํ•œ ํŠธ๋žœ์žญ์…˜์ด ๋๋‚  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐํ•˜๊ฒŒ ๋œ๋‹ค.

๋”ฐ๋ผ์„œ ๋‘ ๊ฐœ ์ด์ƒ์˜ ํŠธ๋žœ์žญ์…˜์ด ๋™์‹œ์— recruitment ๋ฅผ ์กฐํšŒํ•˜์—ฌ ์ˆ˜์ •ํ•˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๊ฒŒ ๋œ๋‹ค.

โœ… ๋‚™๊ด€์ ๋ฝ

๋‚™๊ด€์  ๋ฝ์ด๋ž€ DB ๋ฝ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‹จ์—์„œ version์„ ์ด์šฉํ•œ๋‹ค. ๋น„๊ด€์ ๋ฝ๊ณผ ๋‹ฌ๋ฆฌ ๋™์‹œ์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๊ณ  ๋™์‹œ์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๊ทธ๋•Œ ๊ฐ€์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค. ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ˆ˜์ •ํ•  ๋•Œ(update ์ฟผ๋ฆฌ๊ฐ€ ๋‚ ์•„๊ฐˆ ๋•Œ) ๋ฒ„์ „์ด ์ฆ๊ฐ€ํ•œ๋‹ค. ์ด๋•Œ, ์กฐํšŒ ๋‹น์‹œ์˜ version๊ณผ ๋™์ผํ•œ version์ด๋ผ๋ฉด ์ •์ƒ์ ์œผ๋กœ ๊ฐฑ์‹ ๋˜๊ณ  ์•„๋‹ˆ๋ผ๋ฉด ์Šคํ”„๋ง ์ถ”์ƒ ์˜ˆ์™ธ์ธ ObjectOptimisticLockingFailureException ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

Recruitment Entity ์— version ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณด์ž.

@Entity
public class Recruitment extends BaseTimeEntity {

    @Version
    private Long version;
    
}

์ด์ œ ๋ชจ์ง‘ ์—”ํ‹ฐํ‹ฐ๊ฐ€ ๊ฐฑ์‹ ๋  ๋•Œ ๋งˆ๋‹ค version์ด 1์”ฉ ์ฆ๊ฐ€ํ•œ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด recruitment_id ๊ฐ€ 1์ธ ๋ชจ์ง‘์„ ์กฐํšŒํ–ˆ์„ ๋•Œ version ์ด 0์ด์—ˆ๋‹ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฐฑ์‹ ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

update recruitment
set applicant_count = 1,
    version=1 // 0 + 1
where recruitment_id=1 and version=0

์ด๋•Œ, version์ด 0์ด๋ผ๋ฉด 1๋กœ ๊ฐฑ์‹ ๋˜๊ณ , version์ด 0์ด ์•„๋‹ˆ๋ผ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

๋‚™๊ด€์  ๋ฝ์€ DB์˜ ๋ฝ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‹จ์—์„œ version ํ•„๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ๊ฐœ๋ฐœ์ž๋Š” ์ถ”๊ฐ€์ ์œผ๋กœ ๋™์‹œ ์š”์ฒญ์œผ๋กœ ์ธํ•œ ์ถฉ๋Œ์ด ๋ฐœ์ƒํ–ˆ์„ ๋•Œ๋ฅผ ์ฒ˜๋ฆฌํ•ด์ค˜์•ผ ํ•œ๋‹ค. ๋‹ค์Œ๊ณผ ๊ฐ™์ด update๋ฅผ ํ•  ๋•Œ version ์ฐจ์ด๋กœ ์ธํ•ด ๋ฐœ์ƒํ•œ ์˜ˆ์™ธ๋ฅผ catch ํ•˜์—ฌ ์žฌ์‹œ๋„ํ•˜๋„๋ก ์ฒ˜๋ฆฌํ•˜์˜€๋‹ค.

๊ฐฑ์‹  ์‹œ์— ๋ฐœ์ƒํ•˜๋Š” ObjectOptimisticLockingFailureException ๋ฅผ ์™ธ๋ถ€์—์„œ catch ํ•˜๊ณ , ์žฌ์‹œ๋„๋ฅผ ํ•  ๋•Œ ๋งˆ๋‹ค ๊ฐฑ์‹ ๋œ version ์˜ recruitment ๋ฅผ ์กฐํšŒํ•˜๊ธฐ ์œ„ํ•ด ํŠธ๋žœ์žญ์…˜ ์ „ํŒŒ์†์„ฑ์ธ REQUIRES_NEW ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํŠธ๋žœ์žญ์…˜์„ ๋ฌผ๋ฆฌ์ ์œผ๋กœ ๋ถ„๋ฆฌํ•˜์˜€๋‹ค. recruitment ๋ฅผ ๊ฐฑ์‹ ํ•˜๋Š” ์ฟผ๋ฆฌ๋„ ๋ถ„๋ฆฌ๋œ ํŠธ๋žœ์žญ์…˜์—์„œ ์ฒ˜๋ฆฌ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฐ๋“œ๋ฝ์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค.

@Service
@RequiredArgsConstructor
public class OptimisticLockQuantityService {

    private final RecruitmentRepository recruitmentRepository;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Recruitment increaseApplicantCount(long recruitmentId) {
        Recruitment recruitment = getRecruitment(recruitmentId);
        
        validateApply(recruitment);
        
        recruitment.increaseApplicantCount();
        return recruitment;
    }
 }

์กฐํšŒ ๋‹น์‹œ์˜ version๊ณผ ๋™์ผํ•œ version์ด๋ผ๋ฉด ์ •์ƒ์ ์œผ๋กœ ๊ฐฑ์‹ ๋˜๊ณ  ์•„๋‹ˆ๋ผ๋ฉด ์˜ˆ์™ธ๋ฅผ catch ํ•˜์—ฌ ์žฌ์‹œ๋„ํ•˜๋„๋ก ๋กœ์ง์„ ์ˆ˜์ •ํ•œ๋‹ค.

@Transactional
public void registerApplicantWithOptimisticLock(Long recruitmentId, Long volunteerId) {
    Volunteer volunteer = getVolunteer(volunteerId);
    while (true) {
        try {
            Recruitment recruitment = optimisticLockQuantityService.increaseApplicantCount(recruitmentId);
            
            Applicant applicant = new Applicant(recruitment, volunteer);
            applicantRepository.save(applicant);
            break;
        } catch (ObjectOptimisticLockingFailureException e) {
            log.info("์ถฉ๋Œ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์žฌ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.");
        }

    }
}

โœ… Atomic Query

Atomic Query ๋ž€ ๋ฐ์ดํ„ฐ๋ฅผ update ํ•˜๋Š” ์ž‘์—…์—์„œ ๊ธฐ์กด ๋ฐ์ดํ„ฐ๋ฅผ ์ด์šฉํ•ด์„œ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•œ๋‹ค.

where ์ ˆ์— applicant_count < capacity ์กฐ๊ฑด์„ ๊ฑธ์–ด ํ˜„์žฌ ๋นˆ์ž๋ฆฌ๊ฐ€ ์žˆ๋Š” ๋ด‰์‚ฌ ๋ชจ์ง‘์˜ ์‹ ์ฒญ์ž ์ˆ˜๋ฅผ + 1 ํ•˜๋Š” Atomic Query ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. ํ•ด๋‹น ์ฟผ๋ฆฌ๊ฐ€ ์‹คํ–‰๋œ ํ›„, update ๋œ ํ–‰์˜ ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

(์ฟผ๋ฆฌ ๋ฌธ์ด ์‹คํ–‰๋˜๋ฉด 1์ฐจ ์บ์‹œ์— ์žˆ๋Š” recruitment ์™€ db์˜ recruitment ์˜ applicant_count ๊ฐ’์ด ๋‹ฌ๋ผ์ง€๊ธฐ ๋•Œ๋ฌธ์— clearAutomatically=true ๋ฅผ ์„ค์ •ํ•˜์—ฌ 1์ฐจ ์บ์‹œ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ด์ฃผ์—ˆ๋‹ค.)

public interface RecruitmentRepository extends JpaRepository<Recruitment, Long> {

    @Modifying(flushAutomatically = true, clearAutomatically = true)
    @Query("update Recruitment r set r.applicantCount.applicantCount = r.applicantCount.applicantCount + 1 "
            + "where r.recruitmentId = :recruitmentId "
            + "and r.applicantCount.applicantCount < r.info.capacity")
    int increaseApplicantCount(@Param("recruitmentId") long recruitmentId);

}

์ด์— ๋”ฐ๋ผ ๋ณ€๊ฒฝ๋˜๋Š” ์„œ๋น„์Šค ๋กœ์ง์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค. ์ด์ œ recruitment ๋ฅผ update ํ•˜๊ธฐ ์œ„ํ•ด x-lock ์ด ๊ฑธ๋ฆฌ๊ฒŒ๋˜๊ณ  ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜๋“ค์€ ๋Œ€๊ธฐํ•˜๊ฒŒ ๋œ๋‹ค. recruitment  ๊ฐฑ์‹ ์— ์„ฑ๊ณตํ•œ(๋ด‰์‚ฌ ์‹ ์ฒญ ๊ฐ€๋Šฅ) ํŠธ๋žœ์žญ์…˜์€ Applicant ๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ์ €์žฅํ•œ๋‹ค.

@Transactional
public void registerApplicant(Long recruitmentId, Long volunteerId) {

    int updateCount = recruitmentRepository.increaseApplicantCount(recruitmentId);
    if (updateCount == 0) {
        throw new RuntimeException("๋ชจ์ง‘์ด ๋งˆ๊ฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.");
    }

    Recruitment recruitment = getRecruitment(recruitmentId);
    Volunteer volunteer = getVolunteer(volunteerId);
    Applicant applicant = new Applicant(recruitment, volunteer);

    applicantRepository.save(applicant);
}

โœ… MySQL Named Lock

Named Lock ์ด๋ž€ ๊ณ ์œ ํ•œ ์ด๋ฆ„์œผ๋กœ ์‹๋ณ„๋˜๋Š” ์ž ๊ธˆ์ด๋‹ค. MySQL ์ด ์ œ๊ณตํ•ด์ฃผ๋Š” ํ•จ์ˆ˜๋กœ ์‚ฌ์šฉ๊ฐ€๋Šฅํ•˜๋‹ค. ์ œ๊ณต๋˜๋Š” ํ•จ์ˆ˜์™€ ์„ค๋ช…์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

GET_LOCK() 

  • ํƒ€์ž„ ์•„์›ƒ์„ ์‚ฌ์šฉํ•˜์—ฌ named lock ์„ ์–ป์œผ๋ ค๊ณ  ์‹œ๋„ํ•œ๋‹ค (ํƒ€์ž„์•„์›ƒ์ด ์Œ์ˆ˜์ผ ๊ฒฝ์šฐ ๋ฌดํ•œ ํƒ€์ž„์•„์›ƒ)
  • ๋ฐ˜ํ™˜ ๊ฐ’๊ณผ ์˜๋ฏธ
    • 1: ์ž ๊ธˆ์„ ์„ฑ๊ณต์ ์œผ๋กœ ์–ป์Œ
    • 0: ํƒ€์ž„ ์•„์›ƒ
    • NULL: ์˜ค๋ฅ˜ ๋ฐœ์ƒ
  • named lock ํ•ด์ œ ์กฐ๊ฑด
    1. GET_LOCK()์„ ํ•œ ๋™์ผํ•œ ์„ธ์…˜์—์„œ RELEASE_LOCK()์„ ์‹คํ–‰ํ•˜์—ฌ ๋ช…์‹œ์ ์œผ๋กœ ํ•ด์ œ
    2. ์„ธ์…˜์ด ์ข…๋ฃŒ๋  ๋•Œ(์ •์ƒ ๋˜๋Š” ๋น„์ •์ƒ์ ์œผ๋กœ) ์•”์‹œ์ ์œผ๋กœ ํ•ด์ œ
  • GET_LOCK()์œผ๋กœ ์–ป์€ ์ž ๊ธˆ์€ ํŠธ๋žœ์žญ์…˜์ด ์ปค๋ฐ‹๋˜๊ฑฐ๋‚˜ ๋กค๋ฐฑ๋  ๋•Œ ํ•ด์ œ๋˜์ง€ ์•Š๋Š”๋‹ค
  • ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋™์‹œ ์ž ๊ธˆ์„ ํš๋“ํ•  ์ˆ˜ ์žˆ๋‹ค

RELEASE_LOCK()

  • GET_LOCK()์œผ๋กœ ์–ป์€ ๋ฌธ์ž์—ด๋กœ ๋ช…๋ช…๋œ ์ž ๊ธˆ์„ ํ•ด์ œํ•œ๋‹ค.
  • ๋ฐ˜ํ™˜ ๊ฐ’๊ณผ ์˜๋ฏธ
    • 1: ์ž ๊ธˆ์ด ํ•ด์ œ๋จ
    • 0: ์ด ์„ธ์…˜์— ์˜ํ•ด ์ž ๊ธˆ์ด ์„ค์ •๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ(์ด ๊ฒฝ์šฐ ์ž ๊ธˆ์ด ํ•ด์ œ๋˜์ง€ ์•Š์Œ)
    • NULL: ๋ช…๋ช…๋œ ์ž ๊ธˆ์ด ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ

์ด์ œ GET_LOCK()๊ณผ RELEASE_LOCK() ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ด๋ณด์ž.

์šฐ์„  Named lock ํš๋“๊ณผ ๋ฐ˜ํ™˜์„ ํ•˜๋Š” RecruitmentLockRepository ๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค.

public interface RecruitmentLockRepository extends JpaRepository<Recruitment, Long> {

    @Query(value = "SELECT GET_LOCK(:name, -1)", nativeQuery = true)
    int getLock(@Param("name") String name);

    @Query(value = "SELECT RELEASE_LOCK(:name)", nativeQuery = true)
    int releaseLock(@Param("name") String name);

}

์ด์ œ ๋ด‰์‚ฌ ์‹ ์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์„œ๋น„์Šค ๋กœ์ง์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •ํ•ด๋ณด์ž.

@Transactional
public void registerApplicant(Long recruitmentId, Long volunteerId) {
    String lockName = LOCK_NAME_PREFIX + recruitmentId;

    try {
        int lock = recruitmentLockRepository.getLock(lockName);
        validateLock(lock);

        Recruitment recruitment = getRecruitment(recruitmentId);
        Volunteer volunteer = getVolunteer(volunteerId);
        Applicant applicant = new Applicant(recruitment, volunteer);

        applicantRepository.save(applicant);

    } finally {
        recruitmentLockRepository.releaseLock(lockName);
    }

}

์ด์ œ ๋ด‰์‚ฌ ์‹ ์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋กœ์ง์ด ์‹คํ–‰๋˜๊ธฐ ์œ„ํ•ด์„ , GET_LOCK() ์„ ํ†ตํ•ด lock ์„ ์–ป์–ด์•ผํ•œ๋‹ค. ํ•˜์ง€๋งŒ ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด๋ณธ ๊ฒฐ๊ณผ ์—ฌ์ „ํžˆ ๋™์‹œ์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. ๋˜ํ•œ ์ด์ƒํ•œ ์ ์€ applicant ์ˆ˜๋Š” 50๋ช…์œผ๋กœ ํ†ต๊ณผ๋˜์—ˆ์ง€๋งŒ, recruitment ์˜ applicant_count ๋Š” 50 ๋ฏธ๋งŒ์˜ ๊ฐ’์œผ๋กœ ๊ฐฑ์‹  ์†์‹ค์ด ๋ฐœ์ƒํ•˜์˜€๋‹ค. 

๊ทธ ์ด์œ ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด Named lock ์ด ๋ฐ˜ํ™˜๋˜๋Š” ์‹œ์ ๊ณผ ํŠธ๋žœ์žญ์…˜์ด ์ปค๋ฐ‹๋˜๋Š” ์‹œ์  ์‚ฌ์ด์— ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์ด Named lock ์„ ํš๋“ํ•˜์—ฌ ๊ฐฑ์‹ ๋˜์ง€ ์•Š์€ recruitment ๋ฅผ ์กฐํšŒํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

Named lock ์„ ํš๋“/๋ฐ˜ํ™˜ํ•˜๋Š” ํŠธ๋žœ์žญ์…˜๊ณผ ๋ด‰์‚ฌ ์‹ ์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ํŠธ๋žœ์žญ์…˜์„ ๋ถ„๋ฆฌํ•˜์—ฌ ๋ด‰์‚ฌ ์‹ ์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ํŠธ๋žœ์žญ์…˜์ด ์ปค๋ฐ‹๋œ ํ›„์— Named lock ์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์ˆ˜์ •ํ•ด๋ณด์ž.

Named lock ์„ ํš๋“/๋ฐ˜ํ™˜ํ•˜๋Š” ApplicantLockService.registerApplicant() ๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค.

@Service
@RequiredArgsConstructor
public class ApplicantLockService {

    private static final String LOCK_NAME_PREFIX = "recruitment_";
    
    private final RecruitmentLockRepository recruitmentLockRepository;
    private final ApplicantService applicantService;

    @Transactional
    public void registerApplicant(Long recruitmentId, Long volunteerId) {
        
        String lockName = LOCK_NAME_PREFIX + recruitmentId;
        
        try {
            int lock = recruitmentLockRepository.getLock(lockName);
            validateLock(lock);

            applicantService.registerApplicant(recruitmentId, volunteerId);
        } finally {
            recruitmentLockRepository.releaseLock(lockName);
        }
    }

}

ApplicantService.registerApplicant() ์˜ ํŠธ๋žœ์žญ์…˜ ์ „ํŒŒ ์†์„ฑ์„ REQUIRES_NEW ๋กœ ์„ค์ •ํ•˜์—ฌ ํŠธ๋žœ์žญ์…˜์„ ๋ถ„๋ฆฌํ•œ๋‹ค.

@Transactional(propagation = REQUIRES_NEW)
public void registerApplicant(Long recruitmentId, Long volunteerId) {

    Recruitment recruitment = getRecruitment(recruitmentId);
    Volunteer volunteer = getVolunteer(volunteerId);
    Applicant applicant = new Applicant(recruitment, volunteer);

    applicantRepository.save(applicant);

}

ํŠธ๋žœ์žญ์…˜์„ ๋ถ„๋ฆฌํ•˜์˜€๊ธฐ ๋•Œ๋ฌธ์— ์ปค๋„ฅ์…˜ ์‚ฌ์šฉ์ด ๋‘ ๋ฐฐ๊ฐ€ ๋˜๋ฏ€๋กœ ์ปค๋„ฅ์…˜ํ’€์„ 100์œผ๋กœ ๋Š˜๋ ค์คฌ๋‹ค.

spring:
  datasource:
    hikari:
      maximum-pool-size: 100

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ RecruitmentLockService.registerApplicant() ๋ฅผ ํ˜ธ์ถœํ•˜๋„๋ก ์ˆ˜์ •ํ•˜๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ ํ†ต๊ณผํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

โœ… Redis

Redis ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค. Java ์˜ Redis Client ์ธ Lettuce ์™€ Redisson ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฝ์„ ๊ตฌํ˜„ํ•˜์˜€๋‹ค.

Lettuce

Lettuce ์—์„œ๋Š” SETNX ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฝ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค. SETNX ์˜ ๋ฐ˜ํ™˜๊ฐ’๊ณผ ๊ทธ ์˜๋ฏธ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  • false: key ๊ฐ€ ์ด๋ฏธ ์กด์žฌํ•จ
  • true: key ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๊ณ , ์ƒˆ๋กœ ์ƒ์„ฑํ•จ

key ๋ฅผ ํ†ตํ•ด Lock ์„ ํš๋“/๋ฐ˜ํ™˜ํ•˜๊ธฐ ์œ„ํ•ด RedisLockRepository ๋ฅผ ์ƒ์„ฑํ•ด๋ณด์ž. getLock() ์„ ํ†ตํ•ด key ์— ํ•ด๋‹นํ•˜๋Š” lock ์„ ํš๋“ํ•˜๊ณ  releaseLock() ์„ ํ†ตํ•ด key ์— ํ•ด๋‹นํ•˜๋Š” lock ์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

@Repository
public class RedisLockRepository {

    private final RedisTemplate<String, Object> redisTemplate;
    private final ValueOperations<String, Object> valueOperations;

    public RedisLockRepository(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
        this.valueOperations = redisTemplate.opsForValue();
    }

    public Boolean getLock(String key) {
        return valueOperations.setIfAbsent(key, true);
    }

    public Boolean releaseLock(String key) {
        return redisTemplate.delete(key);
    }
}

์ด์ œ RedisLockRepository ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ApplicantService.registerApplicant ๋ฅผ ๊ตฌํ˜„ํ•ด๋ณด์ž. getLock(String key) ์œผ๋กœ key ์— ํ•ด๋‹นํ•˜๋Š” lock ์ด ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ lock ์„ ํš๋“ํ•  ์ˆ˜ ์žˆ์„ ๋•Œ๊นŒ์ง€ ๋ฐ˜๋ณต๋ฌธ์„ ๋Œ๋ฆฐ๋‹ค. lock ์„ ํš๋“ํ–ˆ์„ ๊ฒฝ์šฐ ๋ด‰์‚ฌ ์‹ ์ฒญ ๋กœ์ง์ด ์‹คํ–‰๋˜๊ณ  lock ์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

@Service
@Slf4j
@RequiredArgsConstructor
public class ApplicantLockService {

    private static final String LOCK_NAME_PREFIX = "recruitment:lock:";

    private final RedisLockRepository redisLockRepository;
    private final ApplicantService applicantService;

    public void registerApplicant(Long recruitmentId, Long volunteerId) {

        String key = LOCK_NAME_PREFIX + recruitmentId;

        while (!redisLockRepository.getLock(key)) {
            log.info("Locking... {}", key);
        }

        try {
            applicantService.registerApplicant(recruitmentId, volunteerId);
        } finally {
            redisLockRepository.releaseLock(key);
        }
    }

}

Redisson

Redisson ์€ Redis pub/sub ๊ธฐ๋ฐ˜์˜ ๋ฝ์„ ์ œ๊ณตํ•œ๋‹ค. ๋”ฐ๋ผ์„œ Lettuce ์™€ ๊ฐ™์ด ์Šคํ•€๋ฝ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค. trylock() ์„ ํ†ตํ•ด timeout ๋™์•ˆ lock ํš๋“ ์‹œ๋„ํ•˜๊ณ  lock ์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ๊ตฌํ˜„ํ•ด๋ณด์ž.

@Service
@Slf4j
@RequiredArgsConstructor
public class ApplicantLockService {

    private static final String LOCK_NAME_PREFIX = "recruitment:lock:";

    private final RedissonClient redissonClient;
    private final ApplicantService applicantService;

    public void registerApplicant(Long recruitmentId, Long volunteerId) {

        String key = LOCK_NAME_PREFIX + recruitmentId;

        RLock lock = redissonClient.getLock(key);

        try {
            boolean acquireLock = lock.tryLock(10, TimeUnit.SECONDS);

            if (!acquireLock) {
                return;
            }

            applicantService.registerApplicant(recruitmentId, volunteerId);

        } catch (InterruptedException e) {
            log.error("Failed to acquire lock", e);
            throw new RuntimeException("Failed to acquire lock");
        } finally {
            lock.unlock();
        }
    }

}

๐Ÿงช  ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ

NGrinder ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ–ˆ๋‹ค. ์ •์›์ด 100๋งŒ๋ช…์ธ ๋ชจ์ง‘์— 20๋ช…์ด 1๋ถ„ ๋™์•ˆ ๋™์‹œ์— ๋ด‰์‚ฌ ์‹ ์ฒญ์„ ํ•˜๋„๋ก ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ž‘์„ฑํ•˜์˜€๋‹ค. ์‹ค์ œ ๋ด‰์‚ฌ ๋ชจ์ง‘์˜ ์ตœ๋Œ€๊ฐ’์€ 99๋ช…์ด์ง€๋งŒ, ๋™์‹œ ์š”์ฒญ์ด ๋ฐœ์ƒํ–ˆ์„ ๋•Œ race condition ์„ ๋ฐœ์ƒ์‹œํ‚ค์ง€ ์•Š์œผ๋ฉด์„œ ์–ผ๋งˆ ๋งŒํผ์˜ ์„ฑ๋Šฅ์„ ๋‚ผ ์ˆ˜ ์žˆ๋Š” ์ง€์— ์ค‘์ ์„ ๋‘” ์„ฑ๋Šฅํ…Œ์ŠคํŠธ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋„‰๋„‰ํžˆ 100๋งŒ๋ช…์œผ๋กœ ์„ค์ •ํ•˜์˜€๋‹ค. ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

๋น„๊ด€์ ๋ฝ ๋‚™๊ด€์ ๋ฝ Atomic Query
 
Named Lock Redis(Lettuce) Redis(Redisson)

 

๐Ÿ‘ ๊ฒฐ๋ก 

๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด 6๊ฐ€์ง€ ๋ฐฉ๋ฒ•์œผ๋กœ ๊ตฌํ˜„ํ•ด๋ณด๊ณ  ์„ฑ๋Šฅํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜์˜€๋‹ค. ๊ฐ ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ๋‚ด๊ฐ€ ์ƒ๊ฐํ•˜๋Š” ์žฅ์ ๊ณผ ๋‹จ์ ์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

๋น„๊ด€์ ๋ฝ

  • ์žฅ์ 
    • ๋ถ„์‚ฐ ์›น ์„œ๋ฒ„ ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค.
    • ๊ตฌํ˜„์ด ๋‹จ์ˆœํ•˜๋‹ค.
    • ์ถฉ๋Œ๋กœ ์ธํ•œ ๋กค๋ฐฑ์„ ํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค.
  • ๋‹จ์ 
    • ๋ถ„์‚ฐ db ์„œ๋ฒ„ ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค.
    • ๋ฝ์„ ๊ฑธ๊ธฐ ๋•Œ๋ฌธ์— locking ์กฐํšŒ๊ฐ€ ํ•„์š”ํ•œ ๋‹ค๋ฅธ ์„œ๋น„์Šค ๋กœ์ง๋„ ๋Œ€๊ธฐํ•ด์•ผํ•œ๋‹ค.

๋‚™๊ด€์ ๋ฝ

  • ์žฅ์ 
    • ๋ถ„์‚ฐ ์›น ์„œ๋ฒ„์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค.
    • ์ถฉ๋Œ์ด ์ ๊ฒŒ ๋ฐœ์ƒํ•œ๋‹ค๋ฉด ์ข‹์€ ์„ฑ๋Šฅ์„ ๋‚ผ ์ˆ˜ ์žˆ๋‹ค.
  • ๋‹จ์ 
    • ๋ถ„์‚ฐ db ์„œ๋ฒ„ ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค.
    • ์ถฉ๋Œ์ด ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์žฌ์‹œ๋„ ๋กœ์ง์„ ๊ตฌํ˜„ํ•ด์•ผํ•œ๋‹ค.
    • ๋กค๋ฐฑ ๋กœ์ง์„ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ๊ตฌํ˜„ํ•ด์•ผํ•œ๋‹ค.
    • ๋งค๋ฒˆ version ์ด ๊ฐฑ์‹ ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ•„์š”ํ•  ๊ฒฝ์šฐ ์žฌ์‹œ๋„๋ฅผ ํ•  ๋•Œ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•ด์™€์•ผํ•œ๋‹ค.

Atomic Query

  • ์žฅ์ 
    •  ๋ถ„์‚ฐ ์›น ์„œ๋ฒ„์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค.
    • ํ•œ๋ฒˆ์˜ ์ฟผ๋ฆฌ๋กœ ์กฐ๊ฑด ํ™•์ธ + ๊ฐฑ์‹ ์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋‹จ์ 
    • ๋ถ„์‚ฐ db ์„œ๋ฒ„ ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค.
    • ๋„๋ฉ”์ธ ๋กœ์ง์ด db ์ฟผ๋ฆฌ์— ์˜์กดํ•˜๊ฒŒ ๋œ๋‹ค.
    • ๋กค๋ฐฑ ๋กœ์ง์„ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ๊ตฌํ˜„ํ•ด์•ผํ•œ๋‹ค.
    • ์ฟผ๋ฆฌ๋กœ ์กฐ๊ฑด์„ ํ™•์ธํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ช…ํ™•ํ•œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค.
    • 1์ฐจ ์บ์‹œ๋ฅผ ๋น„์›Œ์ค˜์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ง€์—ฐ ๋กœ๋”ฉ์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ์ถ”๊ฐ€ ์กฐํšŒ๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

Named Lock

  • ์žฅ์ 
    • db lock ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค.
    • ๋ถ„์‚ฐ ์›น ์„œ๋ฒ„, db ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค.
  • ๋‹จ์ 
    • lock ์„ ํš๋“/๋ฐ˜๋‚ฉ ์œ„ํ•ด db ์„œ๋ฒ„์™€์˜ ํ†ต์‹ ์ด ํ•„์š”ํ•˜๋‹ค.
    • ์ถ”๊ฐ€์ ์ธ ์ปค๋„ฅ์…˜์ด ํ•„์š”ํ•˜๋‹ค.
    • MySQL ์— ์ข…์†๋œ๋‹ค.

Redis Lettuce

  • ์žฅ์ 
    • db lock ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค.
    • ๋ถ„์‚ฐ ์›น ์„œ๋ฒ„, db ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค.
  • ๋‹จ์ 
    • ์Šคํ•€๋ฝ ๋ฐฉ์‹์œผ๋กœ ์ธํ•ด Redis ์˜ ๋ถ€ํ•˜๊ฐ€ ์ปค์ง„๋‹ค.

Redis Redisson

  • ์žฅ์ 
    • db lock ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค.
    • ๋ถ„์‚ฐ ์›น ์„œ๋ฒ„, db ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค.
    • ์‚ฌ์šฉํ•˜๊ธฐ ์‰ฝ๋‹ค.
    • ๋ณ„๋„์˜ ๋ฝ ํš๋“ ๋กœ์ง์ด ํ•„์š”ํ•˜์ง€ ์•Š๋‹ค.
  • ๋‹จ์ 
    • ๋ณ„๋„์˜ ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด์•ผํ•œ๋‹ค.

๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ด์œ  ๋•Œ๋ฌธ์— ์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋น„๊ด€์ ๋ฝ์„ ์„ ํƒํ•˜์˜€๋‹ค.

  1. ๋ถ„์‚ฐ db ํ™˜๊ฒฝ์ด ์•„๋‹ˆ๋ฏ€๋กœ ์„ฑ๋Šฅ์ด ๋‚ฎ์€ ์ชฝ์— ์†ํ•˜๋Š” Redis ๋ฝ๊ณผ Named lock ์€ ์ œ์™ธํ•˜์˜€๋‹ค.
  2. ๋ด‰์‚ฌ ์‹ ์ฒญ๊ฐ™์€ ๊ฒฝ์šฐ ํŠน์ • ์š”์ผ์ด๋‚˜ ํŠน์ • ๋ณดํ˜ธ์†Œ์˜ ๋ด‰์‚ฌ ๋ชจ์ง‘์—์„œ ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์œผ๋ฏ€๋กœ ๋‚™๊ด€์ ๋ฝ์€ ์ œ์™ธํ•˜์˜€๋‹ค.
  3. Anifriends ์˜ ๋ด‰์‚ฌ ์‹ ์ฒญ์—๋Š” ์‹ ์ฒญ ์ธ์› ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๊ฒ€์ฆํ•ด์•ผํ•  ์‚ฌํ•ญ(์‹ ์ฒญ ๋งˆ๊ฐ ์—ฌ๋ถ€, ์‹ ์ฒญ ๋งˆ๊ฐ ์‹œ๊ฐ„ ๋“ฑ)์ด ํ•„์š”ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋น„์ง€๋‹ˆ์Šค ๋กœ์ง์ด ์ฟผ๋ฆฌ์— ์˜์กดํ•˜๋Š” ๊ฒƒ์„ ๋ง‰๊ณ  ๋ช…ํ™•ํ•œ ์˜ˆ์™ธ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๊ธฐ ์œ„ํ•ด Atomic Query ๋Š” ์ œ์™ธํ•˜์˜€๋‹ค.
  4. ์„ฑ๋Šฅ์ด ๋†’์€ ์ชฝ์— ์†ํ•˜๊ณ  ๋น„์ง€๋‹ˆ์Šค ๋กœ์ง์„ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‹จ์—์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๋น„๊ด€์ ๋ฝ์„ ์„ ํƒํ–ˆ๋‹ค.

๋น„๊ด€์ ๋ฝ์„ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ x-lock ์œผ๋กœ ์ธํ•ด locking ์กฐํšŒ๊ฐ€ ํ•„์š”ํ•œ ๋‹ค๋ฅธ ๋กœ์ง๋„ ๋Œ€๊ธฐํ•ด์•ผํ•œ๋‹ค๋Š” ๋‹จ์ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ํ˜„์žฌ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์ด์— ํ•ด๋‹นํ•˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๋กœ์ง์ด ์—†๋‹ค. ์ถ”ํ›„ ํ•ด๋‹น ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค๋ฉด FK ์ œ์•ฝ์กฐ๊ฑด์„ ์ œ๊ฑฐํ•˜์—ฌ ๋™์‹œ์„ฑ์„ ๋†’์ด๋Š” ๋ฐฉ๋ฒ•์„ ๊ณ ๋ คํ•  ๊ฒƒ ๊ฐ™๋‹ค.