fix: UUID 생성 시 clockSequence 경합으로 중복 UUID가 생성되는 문제 수정 - #299
Open
z3rotig4r wants to merge 1 commit into
Open
Conversation
TimeBasedUUIDGenerator.generateIdFromTimestamp()는 clockSequence를 락 안에서 증가시키면서 그 값은 락 밖에서 읽었다. 같은 밀리초 안에서 유일성을 담보하는 요소가 clockSequence뿐이므로, 두 스레드가 같은 값을 읽으면 time 성분도 같아져 완전히 동일한 UUID가 생성된다. 락 안에서 (lastTime, clockSequence)를 지역변수로 함께 스냅샷하고 이후 계산은 스냅샷 값만 사용하도록 수정했다. 시간이 역전된 호출(스레드 지연으로 더 오래된 System.currentTimeMillis() 값이 뒤늦게 도착)에도 lastTime 기준으로 계산하므로 (시각, 순번) 쌍의 유일성이 보장된다. 16스레드 x 4,000건 동시 생성 기준 수정 전 중복 283~1,567건, 수정 후 0건이다. 시각이 단조 증가하는 기존 호출 경로에서는 생성 결과가 이전과 완전히 동일하다.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
변경 이유
EgovUUIdGnrServiceImpl의 MAC 주소 기반 UUID 생성은 같은 파일 안의TimeBasedUUIDGenerator.generateIdFromTimestamp()가 담당합니다. 시간 필드가 밀리초 단위라서, 같은 밀리초에 들어온 호출끼리 UUID를 갈라주는 요소는clockSequence하나뿐입니다.그런데 이 값을 증가시키는 자리와 읽는 자리가 다릅니다. 증가는
synchronized (LOCK)안에서 하는데 UUID에 실을 값을 읽는 코드는 락 밖에 있습니다. 필드는 volatile도 아닌 static long입니다.두 스레드가 락을 나온 뒤 같은
clockSequence를 읽으면 시간 필드까지 같아지므로, 완전히 동일한 UUID가 나옵니다.16개 스레드를
CyclicBarrier로 동시에 출발시켜 각각 5,000건씩 모두 80,000건을 생성하면 수정 전에는 5회 실행이 전부 실패합니다. 실행마다 수백 건에서 1,700건대까지 중복이 나옵니다.변경 내용
고친 곳은
generateIdFromTimestamp()한 메서드이고, 수정은 두 갈래입니다. 두 번째를 빼면 중복이 남습니다.clockSequence읽기를 락 안으로 옮겨 지역 변수sequence에 담습니다.currentTimeMillis에서 락 안에서 확정된lastTime(지역 변수timestamp)으로 바꿉니다.2번이 필요한 이유는 이렇습니다. 각 스레드는 락에 들어가기 전에 시계를 읽어 둡니다. 늦게 락에 진입한 스레드는 그사이 시각이 넘어가면서 0으로 리셋된 시퀀스를, 자기가 들고 있던 낡은 시각과 함께 쓰게 됩니다. 그 조합이 앞서 지나간 (시각, 시퀀스) 쌍과 겹칩니다. 락 안에서 확정된
lastTime을 쓰면 이 값이 단조증가하므로 한 번 지나간 쌍은 다시 나오지 않습니다.같은 부하(16 스레드 × 5,000건)로 세 버전을 5회씩 돌린 결과입니다.
수정본은 스레드 2·4·8·16·32·64개, 1만~32만 건 범위에서 20회 넘게 돌려도 중복이 0건입니다.
동시성 검증 테스트
EgovUUIdGnrServiceConcurrencyTest도 함께 추가했습니다. 16 스레드 × 4,000건(총 64,000건)을 동시에 생성해 중복이 없는지 확인합니다. UUID version이 1인지, 하위 48비트에 설정한 MAC 주소가 그대로 실리는지도 같은 클래스에서 확인합니다.테스트 방법
mvn -B -o -pl Foundation/org.egovframe.rte.fdl.idgnr test@Timeout(30)을 걸어 두었습니다.영향 범위
EgovUUIdGnrServiceImpl.java아래쪽의 package-private 클래스TimeBasedUUIDGenerator입니다. 같은 파일 밖에서 호출하는 코드는 없습니다.lastTime이 곧 호출자가 넘긴currentTimeMillis입니다.남은 한계
같은 밀리초 안에서 65,536건을 넘겨 생성하면
clockSequence << 48이 넘쳐 중복이 생길 수 있습니다. 시각을 고정하고 70,000회를 돌리면 수정 전과 수정 후 모두 65,536번째부터 중복 4,464건으로, 수치까지 같게 나옵니다. 기존 자료구조에서 비롯한 한계라 이번 수정으로 달라지지 않았고, 해소하려면 시퀀스 폭이나 시각 해상도를 함께 바꿔야 해서 이번 PR 범위에는 넣지 않았습니다.