├── .gitignore ├── LICENSE ├── README.md └── stock ├── .gitignore ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── java │ └── com │ │ └── example │ │ └── stock │ │ ├── StockApplication.java │ │ ├── domain │ │ └── Stock.java │ │ ├── facade │ │ ├── LettuceLockStockFacade.java │ │ ├── NamedLockStockFacade.java │ │ ├── OptimisticLockStockFacade.java │ │ └── RedissonLockStockFacade.java │ │ ├── repository │ │ ├── NamedLockRepository.java │ │ ├── OptimisticStockRepository.java │ │ ├── PessimisticStockRepository.java │ │ ├── RedisRepository.java │ │ └── StockRepository.java │ │ └── service │ │ ├── NamedLockStockService.java │ │ ├── OptimisticLockStockService.java │ │ ├── PessimisticLockStockService.java │ │ ├── StockBusinessInterface.java │ │ ├── StockNonSynchronizedService.java │ │ └── StockService.java └── resources │ └── application.yaml └── test └── java └── com └── example └── stock ├── StockApplicationTests.java └── service └── StockServiceTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 간단한 재고 시스템으로 학습하는 동시성 이슈 2 | 3 | 본 GitHub 저장소는 [다음 인프런 강의](https://www.inflearn.com/course/%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C#reviews)를 학습한 정리 노트이다. 4 | 5 | ## 학습 목표 6 | 7 | * Quantity 변수의 기존 값은 1000이었다. 이를 1000개의 쓰레드가 각각 개별로 1씩 감소하여 0으로 만드는 테스트를 작성하고, 이를 통과시키는 비즈니스 로직을 작성하여라. 8 | 9 | ## 테스트 환경 10 | 11 | * Apple Macbook Pro M1 12 | * Docker Compose-based MySQL 13 | * Docker Compose-based Redis 14 | * JDK 11 15 | * IntelliJ & JUnit 5 16 | 17 | ## 테스트 결과 18 | 19 | * Java 11의 ExecutorService와 CountDownLatch API를 이용하여 1000개의 쓰레드를 띄운 후 테스트 하였다. 20 | * 테스트 성능: Pessimistic Lock > Synchronized Lock > Named Lock > Optimistic Lock > Lettuce Lock 21 | 22 | ``` 23 | pessimistic lock을 사용한 재고 감소 - 동시에 1000개 테스트 | 12.415s 소요 24 | SYNCHRONIZED를 사용한 재고 감소 - 동시 1000개 테스트 | 16.994s 소요 25 | named lock 을 사용한 재고 감소 - 동시에 1000개 테스트 | 21.857s 소요 26 | optimistic lock을 사용한 재고 감소 - 동시에 1000개 테스트 | 36.494s 소요 27 | redis lettuce lock 을 사용한 재고 감소 - 동시에 1000개 테스트 | 49.581s 소요 28 | ``` 29 | 30 | ## 테스트 로직 31 | 32 | ### 1. StockNonSynchronizedService: 동시성을 고려하지 않은 기본적인 로직 33 | 34 | * Method Level에 `@Transactional` 어노테이션 적용 35 | 36 | ### 2. StockService: Synchronized 키워드를 적용한 동기화 로직 37 | 38 | * Method Level에 `Synchronized` 를 사용하였으나, 해당 키워드는 같은 프로세스 단위에서만 동시성을 보장한다. 39 | * 따라서 서버가 1대일 때는 동시성 이슈가 해결되는 듯 하나, 여러 대의 서버를 활용하면 여러 개의 인스턴스가 존재하는 것과 동일하기 때문에 동시성을 보장하지 못한다. 40 | 41 | ### 3. Pessimistic Lock의 사용 42 | 43 | * 실제로 데이터에 Lock을 걸어서 정합성을 맞추는 방법이다. Exclusive Lock을 적용하게 되면 다른 트랜잭션에서는 Lock이 해제되기 전까지 데이터를 가져갈 수 없으므로 데이터 정합성을 보장한다. 44 | * Method Level에 `@Transactional` 및 DB 조회 시에 `@Lock(LockModeType.PESSIMISTIC_WRITE)` 을 사용하여 트랜잭션이 시작할 때 Shared/Exclusive Lock을 적용하게 된다. 45 | * Pessimistic Lock은 동시성 충돌이 잦을 것으로 예상되어 동시성을 **강력하게** 지켜야 할 때 사용하여야 한다. 46 | * 충돌이 빈번하게 일어난다면 Optimistic Lock보다 성능 좋고, 데이터 정합성이 안정적이다. 47 | * 하지만 별도의 Lock을 잡기 때문에 속도가 느리고, 경우에 따라 Dead Lock의 위험성이 있다. 48 | 49 | ### 4. Optimistic Lock의 사용 50 | 51 | ![img](https://cdn.discordapp.com/attachments/927446963614531624/1010730849752334476/unknown.png) 52 | 53 | * 실제로 Lock을 사용하지 않고 Version을 이용함으로써 데이터 정합성을 준수하는 방법이다. 54 | * 먼저 데이터를 읽은 후에 update를 실행하고, 이 때 현재 내가 읽은 버전이 맞는 지 확인하는 Query를 조회한다. 55 | * 내가 읽은 Version에서 수정사항이 생겨서 Version의 값이 증가하였다면, 새롭게 Application에서 데이터를 다시 읽은 후에 작업을 수행해야 한다. 56 | * Method Level에 `@Transactional` 및 DB 조회시 `@Lock(LockModeType.OPTIMISTIC)` 사용. 57 | * 하지만 추가 기능을 별도로 구현해야 하는 번거러움이 존재한다. 58 | * Version 관리를 위하여 테이블을 마이그레이션 하여야 한다. 59 | * Version 충돌 시 재시도 로직을 구현해야 한다. 60 | * DB 트랜잭션을 활용하지 않기 때문에 롤백을 직접 구현해야 한다. 61 | * 실제로 이번 테스트 케이스에서는 Version 충돌이 많기에 Optimistic Lock의 성능이 가장 좋지 않다. 62 | 63 | ### 5. Named Lock의 사용 64 | 65 | ![img](https://cdn.discordapp.com/attachments/927446963614531624/1010778948403212318/unknown.png) 66 | 67 | * MySQL의 Native Named Lock을 사용한다. 68 | * MySQL에서 `GET_LOCK`과 `RELEASE_LOCK` 으로 분산 락(distributed lock)을 구현할 수 있다. [참고](https://kwonnam.pe.kr/wiki/database/mysql/user_lock) 69 | * Named Lock을 활용할 때 데이터소스를 분리하지않고 하나로 사용하게되면 커넥션풀이 부족해지는 현상을 겪을 수 있어서 락을 사용하지 않는 다른 서비스까지 영향을 끼칠 수 있다. 70 | * Named Lock을 활용하면 분산 락을 구현할 수 있고 Pessmistic Lock은 타임아웃을 구현하기 쉽지만 Named Lock은 타임아웃을 구현하기 쉽다. 그리고 데이터 정합성을 받춰야 하는 경우에도 Named Lock이 좋다. 71 | * 하지만 트랜잭션 종료 시에 Lock 해제와 세션 관리 (데이터 소스 분리 시) 관리가 수동으로 진행되어야 하고 일일이 수동으로 해야 한다는 불편한 점이 있어 실무 구현에서는 좀 빡세다. 72 | * Pessmistic Lock은 column/row 단계에서 Lock을 걸지만, Named Lock은 metadata 단위에 lock을 건다. 73 | * Named Lock에서는 Thread가 아니라 Session이라고 부른다. 74 | 75 | ### 6. Distributed Lock - Lettuce의 사용 76 | 77 | ![img](https://cdn.discordapp.com/attachments/927446963614531624/1010789773864087552/unknown.png) 78 | 79 | * Lettuce 방식은 setnx (set when not exists) 명령어를 사용하여 분산락을 구현하는 방식이다. 80 | * spin lock 방식을 구현하여야 하는데, 이는 lock을 해제할 수 있는지 일정 주기에 따라 확인하는 방법이다. 81 | * Named Lock과 달리 Redis를 사용하면 트랜잭션에 따라 대응되는 현재 트랜잭션 풀 세션 관리를 하지 않아도 되므로 구현이 편리하다. 82 | * Spin Lock 방식이므로 Sleep Time이 적을 수록 Redis에 부하를 줄 수 있어서 thread busy waiting의 요청 간의 시간을 적절히 주어야 한다. 83 | * Lettuce 는 Spring Data Redis에서 기존 인터페이스를 제공하기 때문에 러닝 커브가 빠르다. 84 | * 반드시 수동으로 Lock을 unlock 해주어야 한다. 85 | 86 | ### 7. Distributed Lock - Redisson의 사용 87 | 88 | ```markdown 89 | // https://devroach.tistory.com/83 90 | 1. 스핀락은 계속해서 Lock 을 획득하기 위해 순회하기 때문에 만약 Lock 을 획득한 스레드나 프로세스가 Lock 을 정상적으로 해제해주지 못한다면 현재 스레드는 계속해서 락을 획득하려 시도하느라 어플리케이션이 중지될 것입니다. 91 | 2. 대표적으로 순회 횟수를 5회로 제한한다거나, 아니면 시간으로 제한한다거나를 택할 수 있을 겁니다. 92 | 3. setnx 메소드는 만약 키가 존재하지 않는다면 설정하게 되는 것이므로 Redis 에 계속해서 LockKeyName 이 존재하는지 확인해야만 합니다. 따라서 순회하는 동안 계속해서 Redis 에 요청을 보내게 되는 것이므로 스레드 혹은 프로세스가 많다면 Redis 에 부하가 가게 될 것입니다. 93 | 4. Lettuce 에서는 Lock 에 대한 기능을 별도로 제공하지 않고, 기존 key-value 를 Setting 하는 방법과 동일하게 사용합니다. 하지만 Redisson 에서는 RLock 이라는 클래스를 따로 제공합니다. 94 | ``` 95 | 96 | ![img](https://cdn.discordapp.com/attachments/927446963614531624/1010790056031686706/unknown.png) 97 | 98 | * Pub-sub 기반으로 Lock을 구현. 채널을 하나 만들고 락을 점유하고 있는 쓰레드가 락을 받으려는 쓰레드에게 점유 해제를 공지한다. 99 | * 별도의 retry 로직이 필요없다. 100 | * pub-sub 방식을 통하여 분산 락을 획득한다. 101 | 102 | ```  103 | (Session 1) $ docker exec -it 6c7c0a47dd34 redis-cli 104 | (Session 2) $ docker exec -it 6c7c0a47dd34 redis-cli 105 | 106 | (Session 1) $ subscribe ch1 107 | // Reading messages... (press Ctrl-C to quit) 108 | // 1) "subscribe" 109 | // 2) "ch1" 110 | // 3) (integer) 1 111 | 112 | (Session 2) $ publish ch1 hello 113 | // (integer) 1 114 | 115 | (Session 1) $ 116 | // 1) "message" 117 | // 2) "ch1" 118 | // 3) "hello" 119 | ``` 120 | 121 | * Redisson은 Lettuce와 달리 별도의 인터페이스이기 때문에 Gradle 의존 패키지 설치 및 별도 Facade 작성이 필요하다. 122 | * leaseTime을 잘못 잡으면 작업 도중 Lock이 해제될 수도 있으니 주의하도록 한다. 이를 `IllegalMonitorStateException` 이라고 부른다. 123 | 124 | ```markdown 125 | // https://devroach.tistory.com/83 126 | Lock 을 해제하는 과정 중 정상적으로 Lock 이 해제가 되지 않는다면 문제가 발생할 수 있는데요. 그래서 Redisson 에서는 LockExpire 를 설정할 수 있도록 해줍니다. 그래서 Redison 의 tryLock Method 에서는 leaseTime 을 설정할 수 있습니다. 127 | 128 | Lock 경과시간 만료후 Lock 에 접근하게 될 수도 있습니다. 129 | 만약 A 프로세스가 Lock 을 취득한 후 leaseTime 을 1초로 설정했다고 해봅시다. 130 | 근데 A 프로세스의 작업이 2초가 걸리는 작업이였다면 이미 Lock 은 leaseTime 이 경과하여 도중에 해제가 되었을 테고, A 프로세스는 Lock 에 대해서 Monitor 상태가 아닌데 Lock 을 해제하려고 할 것 입니다. 131 | 따라서 IllegalMonitorStateException 이 발생하게 됩니다. 132 | ``` 133 | 134 | * Lock 획득이 실패하고 재시도가 반드시 필요하지 않은 경우에는 Lettuce를 사용하고, 재시도가 반드시 필요한 경우에는 Redisson을 활용하도록 하자. 135 | 136 | ## 참고 137 | 138 | ### Pessimistic Lock vs Optimistic Lock 139 | 140 | * 충돌이 적은 경우 optimistic lock 이 빠르지만, 충돌이 많다면 pessimistic lock 이 더 빠르므로, 경우에 따라 다르다. 141 | 142 | ### Facade? Helper? 143 | 144 | * Facade는 내부 로직을 캡슐화하는 디자인 패턴. 사실 우리 구현사항에서 Facade에는 락을 얻는 행위만 있으므로 다른 패턴이 더 적합할 수 있지만, 구현이 매우 쉬워서 실무에서 자주 쓰는 편이다. 145 | 146 | ### MySQL? Redis? 147 | 148 | * 이미 MySQL 을 사용하고 있다면 별도의 비용 없이 사용가능하다. 어느 정도의 트래픽까지는 문제 없이 활용이 가능하다. ㅏ하지만 Redis 보다는 성능이 좋지 않다. 149 | * 만약 현재 활용중인 Redis 가 없다면 별도의 구축비용과 인프라 관리비용이 발생한다. 하지만, MySQL 보다 성능이 좋다. 150 | 151 | ### Version 주입할 시의 어노테이션 152 | 153 | ```java 154 | import javax.persistence.Version; 155 | ``` 156 | 157 | ### 더 살펴보기 158 | 159 | * [What is the purpose of await() in CountDownLatch?](https://stackoverflow.com/questions/41866691/what-is-the-purpose-of-await-in-countdownlatch) 160 | * [MySQL에서 사용하는 Lock 이해](http://web.bluecomtech.com/saltfactory/blog.saltfactory.net/database/introduce-mysql-lock.html) 161 | 162 | ## 참고 문헌 163 | 164 | * https://devroach.tistory.com/83 165 | * https://github.com/Hyune-c/manage-stock-concurrency 166 | -------------------------------------------------------------------------------- /stock/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | -------------------------------------------------------------------------------- /stock/README.md: -------------------------------------------------------------------------------- 1 | ## 생각해보기: 레이스 컨디션(Race Condition) 이란 ? 2 | * 둘 이상의 스레드가 공유 데이터에 액세스할 수 있고 동시에 변경하려고 할 때 발생하는 문제 3 | * 둘 이상의 스레드 : 요청 4 | * 공유 데이터 : 재고 데이터 5 | * 동시에 변경하려고 할 때 : 업데이트 할때 6 | * 발생하는 문제 : 값이 정상적으로 바뀌지 않는 문제 7 | * 해결방법 1 8 | * 하나의 스레드만 데이터에 액세스 할 수 있도록 한다. 9 | -------------------------------------------------------------------------------- /stock/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.7.3' 3 | id 'io.spring.dependency-management' version '1.0.13.RELEASE' 4 | id 'java' 5 | } 6 | 7 | group = 'com.example' 8 | version = '0.0.1-SNAPSHOT' 9 | sourceCompatibility = '11' 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 17 | implementation 'org.springframework.boot:spring-boot-starter-web' 18 | implementation 'org.springframework.boot:spring-boot-starter-data-redis' 19 | implementation 'org.redisson:redisson-spring-boot-starter:3.17.5' 20 | runtimeOnly 'mysql:mysql-connector-java' 21 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 22 | } 23 | 24 | tasks.named('test') { 25 | useJUnitPlatform() 26 | } 27 | -------------------------------------------------------------------------------- /stock/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigridjineth/concurrency-stock-spring/4d468f921e180a1d0bb35abfab6fc9e43090a6f2/stock/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /stock/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /stock/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Stop when "xargs" is not available. 209 | if ! command -v xargs >/dev/null 2>&1 210 | then 211 | die "xargs is not available" 212 | fi 213 | 214 | # Use "xargs" to parse quoted args. 215 | # 216 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 217 | # 218 | # In Bash we could simply go: 219 | # 220 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 221 | # set -- "${ARGS[@]}" "$@" 222 | # 223 | # but POSIX shell has neither arrays nor command substitution, so instead we 224 | # post-process each arg (as a line of input to sed) to backslash-escape any 225 | # character that might be a shell metacharacter, then use eval to reverse 226 | # that process (while maintaining the separation between arguments), and wrap 227 | # the whole thing up as a single "set" statement. 228 | # 229 | # This will of course break if any of these variables contains a newline or 230 | # an unmatched quote. 231 | # 232 | 233 | eval "set -- $( 234 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 235 | xargs -n1 | 236 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 237 | tr '\n' ' ' 238 | )" '"$@"' 239 | 240 | exec "$JAVACMD" "$@" 241 | -------------------------------------------------------------------------------- /stock/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /stock/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'stock' 2 | -------------------------------------------------------------------------------- /stock/src/main/java/com/example/stock/StockApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.stock; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class StockApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(StockApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /stock/src/main/java/com/example/stock/domain/Stock.java: -------------------------------------------------------------------------------- 1 | package com.example.stock.domain; 2 | 3 | import javax.persistence.Version; 4 | import javax.persistence.Entity; 5 | import javax.persistence.GeneratedValue; 6 | import javax.persistence.GenerationType; 7 | import javax.persistence.Id; 8 | 9 | @Entity 10 | public class Stock { 11 | @Id 12 | @GeneratedValue(strategy = GenerationType.IDENTITY) 13 | private Long id; 14 | 15 | private Long productId; 16 | 17 | private Long quantity; 18 | 19 | public Stock() {} 20 | 21 | public Stock(Long productId, Long quantity) { 22 | this.productId = productId; 23 | this.quantity = quantity; 24 | } 25 | 26 | @Version 27 | private Long version; 28 | 29 | public Long getQuantity() { 30 | return quantity; 31 | } 32 | 33 | public Long decrease(Long quantity) { 34 | if (this.quantity < quantity) { 35 | throw new IllegalArgumentException("Not enough stock"); 36 | } 37 | 38 | this.quantity -= quantity; 39 | return this.quantity; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /stock/src/main/java/com/example/stock/facade/LettuceLockStockFacade.java: -------------------------------------------------------------------------------- 1 | package com.example.stock.facade; 2 | 3 | import com.example.stock.repository.RedisRepository; 4 | import com.example.stock.service.StockNonSynchronizedService; 5 | import com.example.stock.service.StockService; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | public class LettuceLockStockFacade { 10 | 11 | private final RedisRepository redisRepository; 12 | 13 | private final StockNonSynchronizedService stockService; 14 | 15 | public LettuceLockStockFacade(final RedisRepository redisRepository, final StockNonSynchronizedService stockService) { 16 | this.redisRepository = redisRepository; 17 | this.stockService = stockService; 18 | } 19 | 20 | public void decrease(final Long productId, final Long quantity) throws InterruptedException { 21 | while (!redisRepository.lock(productId)) { 22 | Thread.sleep(100); // 부하를 줄여줘본다. 23 | } 24 | 25 | try { 26 | stockService.decrease(productId, quantity); 27 | } finally { 28 | redisRepository.unlock(productId); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /stock/src/main/java/com/example/stock/facade/NamedLockStockFacade.java: -------------------------------------------------------------------------------- 1 | package com.example.stock.facade; 2 | 3 | import com.example.stock.repository.NamedLockRepository; 4 | import com.example.stock.service.NamedLockStockService; 5 | import org.springframework.stereotype.Component; 6 | 7 | // lettuce lock 8 | @Component 9 | public class NamedLockStockFacade { 10 | 11 | private NamedLockRepository namedLockRepository; 12 | 13 | private NamedLockStockService namedLockStockService; 14 | 15 | public NamedLockStockFacade(final NamedLockRepository namedLockRepository, final NamedLockStockService namedLockStockService) { 16 | this.namedLockRepository = namedLockRepository; 17 | this.namedLockStockService = namedLockStockService; 18 | } 19 | 20 | public void decrease(Long id, Long quantity) { 21 | try { 22 | namedLockRepository.getLock(id.toString()); 23 | namedLockStockService.decrease(id, quantity); 24 | } finally { 25 | namedLockRepository.releaseLock(id.toString()); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /stock/src/main/java/com/example/stock/facade/OptimisticLockStockFacade.java: -------------------------------------------------------------------------------- 1 | package com.example.stock.facade; 2 | 3 | import com.example.stock.service.OptimisticLockStockService; 4 | import org.springframework.stereotype.Service; 5 | 6 | @Service 7 | public class OptimisticLockStockFacade { 8 | 9 | private final OptimisticLockStockService optimisticLockStockService; 10 | 11 | public OptimisticLockStockFacade(OptimisticLockStockService optimisticLockStockService) { 12 | this.optimisticLockStockService = optimisticLockStockService; 13 | } 14 | 15 | public void decrease(final Long id, final Long quantity) throws InterruptedException { 16 | while (true) { 17 | try { 18 | optimisticLockStockService.decrease(id, quantity); 19 | break; 20 | } catch (Exception e) { 21 | // retry 22 | System.out.println("OPTIMISTIC LOCK VERSION CONFLICT !!!"); 23 | System.out.println(e.getMessage()); 24 | Thread.sleep(1); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /stock/src/main/java/com/example/stock/facade/RedissonLockStockFacade.java: -------------------------------------------------------------------------------- 1 | package com.example.stock.facade; 2 | 3 | import com.example.stock.service.StockNonSynchronizedService; 4 | import com.example.stock.service.StockService; 5 | import org.redisson.api.RLock; 6 | import org.redisson.api.RedissonClient; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.util.concurrent.TimeUnit; 10 | 11 | @Component 12 | public class RedissonLockStockFacade { 13 | 14 | private final RedissonClient redissonClient; 15 | 16 | private final StockNonSynchronizedService stockService; 17 | 18 | public RedissonLockStockFacade(final RedissonClient redissonClient, final StockNonSynchronizedService stockService) { 19 | this.redissonClient = redissonClient; 20 | this.stockService = stockService; 21 | } 22 | 23 | public void decrease(final Long productId, final Long quantity) throws InterruptedException { 24 | final RLock lock = redissonClient.getLock(productId.toString()); 25 | 26 | try { 27 | // boolean isAvailable = lock.tryLock(10, 1, TimeUnit.SECONDS); 28 | 29 | if (!lock.tryLock(30, 1, TimeUnit.SECONDS)) { 30 | System.out.println("redisson getLock timeout"); 31 | return; 32 | } 33 | 34 | stockService.decrease(productId, quantity); 35 | 36 | } finally { 37 | // unlock the lock object 38 | lock.unlock(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /stock/src/main/java/com/example/stock/repository/NamedLockRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.stock.repository; 2 | 3 | import com.example.stock.domain.Stock; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.jpa.repository.Query; 6 | 7 | // lettuce lock 8 | public interface NamedLockRepository extends JpaRepository { 9 | 10 | @Query(value = "select get_lock(:key, 1000)", nativeQuery = true) 11 | void getLock(String key); 12 | 13 | @Query(value = "select release_lock(:key)", nativeQuery = true) 14 | void releaseLock(String key); 15 | } 16 | -------------------------------------------------------------------------------- /stock/src/main/java/com/example/stock/repository/OptimisticStockRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.stock.repository; 2 | 3 | import com.example.stock.domain.Stock; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.jpa.repository.Lock; 6 | 7 | import javax.persistence.LockModeType; 8 | 9 | public interface OptimisticStockRepository extends JpaRepository { 10 | @Lock(LockModeType.OPTIMISTIC) 11 | Stock getByProductId(Long productId); 12 | } 13 | -------------------------------------------------------------------------------- /stock/src/main/java/com/example/stock/repository/PessimisticStockRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.stock.repository; 2 | 3 | import com.example.stock.domain.Stock; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.jpa.repository.Lock; 6 | 7 | import javax.persistence.LockModeType; 8 | 9 | public interface PessimisticStockRepository extends JpaRepository { 10 | 11 | @Lock(LockModeType.PESSIMISTIC_WRITE) 12 | Stock getByProductId(Long productId); 13 | } -------------------------------------------------------------------------------- /stock/src/main/java/com/example/stock/repository/RedisRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.stock.repository; 2 | 3 | import org.springframework.data.redis.core.RedisTemplate; 4 | import org.springframework.stereotype.Component; 5 | 6 | import java.time.Duration; 7 | 8 | @Component 9 | public class RedisRepository { 10 | 11 | private final RedisTemplate redisTemplate; 12 | 13 | public RedisRepository(final RedisTemplate redisTemplate) { 14 | this.redisTemplate = redisTemplate; 15 | } 16 | 17 | public Boolean lock(final Long key) { 18 | String generatedKey = generateKey(key); 19 | return redisTemplate 20 | .opsForValue() 21 | .setIfAbsent(generatedKey, "lock", Duration.ofMillis(3_000)); 22 | } 23 | 24 | public Boolean unlock(final Long key) { 25 | String generatedKey = generateKey(key); 26 | return redisTemplate.delete(generatedKey); 27 | } 28 | 29 | public String generateKey(final Long key) { 30 | return key.toString(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /stock/src/main/java/com/example/stock/repository/StockRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.stock.repository; 2 | 3 | import com.example.stock.domain.Stock; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.jpa.repository.Lock; 6 | import org.springframework.data.jpa.repository.Query; 7 | 8 | import javax.persistence.LockModeType; 9 | 10 | public interface StockRepository extends JpaRepository { 11 | 12 | /* 13 | 충돌이 빈번하게 일어난다면 Optimistic Lock보다 성능 좋고, 데이터 정합성이 안정적 14 | 단점: 별도의 Lock을 잡기 때문에 성능이 감소한다. 15 | */ 16 | Stock getByProductId(Long productId); 17 | } 18 | -------------------------------------------------------------------------------- /stock/src/main/java/com/example/stock/service/NamedLockStockService.java: -------------------------------------------------------------------------------- 1 | package com.example.stock.service; 2 | 3 | import com.example.stock.domain.Stock; 4 | import com.example.stock.repository.StockRepository; 5 | import org.springframework.stereotype.Service; 6 | import org.springframework.transaction.annotation.Propagation; 7 | import org.springframework.transaction.annotation.Transactional; 8 | 9 | @Service 10 | public class NamedLockStockService implements StockBusinessInterface{ 11 | 12 | private final StockRepository stockRepository; 13 | 14 | public NamedLockStockService(final StockRepository stockRepository) { 15 | this.stockRepository = stockRepository; 16 | } 17 | 18 | @Transactional(propagation = Propagation.REQUIRES_NEW) 19 | public synchronized void decrease(final Long id, final Long quantity) { 20 | final Stock stock = stockRepository.getByProductId(id); 21 | stock.decrease(quantity); 22 | stockRepository.saveAndFlush(stock); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /stock/src/main/java/com/example/stock/service/OptimisticLockStockService.java: -------------------------------------------------------------------------------- 1 | package com.example.stock.service; 2 | 3 | import com.example.stock.domain.Stock; 4 | import com.example.stock.repository.OptimisticStockRepository; 5 | import com.example.stock.repository.StockRepository; 6 | import org.springframework.stereotype.Service; 7 | 8 | import javax.transaction.Transactional; 9 | 10 | @Service 11 | public class OptimisticLockStockService implements StockBusinessInterface { 12 | 13 | private final OptimisticStockRepository stockRepository; 14 | 15 | public OptimisticLockStockService(final OptimisticStockRepository stockRepository) { 16 | this.stockRepository = stockRepository; 17 | } 18 | 19 | @Transactional 20 | public void decrease(final Long id, final Long quantity) { 21 | final Stock stock = stockRepository.getByProductId(id); 22 | stock.decrease(quantity); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /stock/src/main/java/com/example/stock/service/PessimisticLockStockService.java: -------------------------------------------------------------------------------- 1 | package com.example.stock.service; 2 | 3 | import com.example.stock.domain.Stock; 4 | import com.example.stock.repository.PessimisticStockRepository; 5 | import com.example.stock.repository.StockRepository; 6 | import org.springframework.stereotype.Service; 7 | 8 | import javax.transaction.Transactional; 9 | 10 | @Service 11 | public class PessimisticLockStockService implements StockBusinessInterface { 12 | 13 | private PessimisticStockRepository stockRepository; 14 | 15 | public PessimisticLockStockService(final PessimisticStockRepository stockRepository) { 16 | this.stockRepository = stockRepository; 17 | } 18 | 19 | @Transactional 20 | public void decrease(final Long id, final Long quantity) { 21 | Stock stock = stockRepository.getByProductId(id); 22 | 23 | stock.decrease(quantity); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /stock/src/main/java/com/example/stock/service/StockBusinessInterface.java: -------------------------------------------------------------------------------- 1 | package com.example.stock.service; 2 | 3 | public interface StockBusinessInterface { 4 | void decrease(Long id, Long quantity); 5 | } 6 | -------------------------------------------------------------------------------- /stock/src/main/java/com/example/stock/service/StockNonSynchronizedService.java: -------------------------------------------------------------------------------- 1 | package com.example.stock.service; 2 | 3 | import com.example.stock.domain.Stock; 4 | import com.example.stock.repository.StockRepository; 5 | import org.springframework.stereotype.Service; 6 | import org.springframework.transaction.annotation.Transactional; 7 | 8 | @Service 9 | public class StockNonSynchronizedService implements StockBusinessInterface { 10 | private final StockRepository stockRepository; 11 | 12 | public StockNonSynchronizedService(final StockRepository stockRepository) { 13 | this.stockRepository = stockRepository; 14 | } 15 | 16 | @Transactional 17 | public synchronized void decrease(final Long productId, final Long quantity) { 18 | final Stock stock = stockRepository.getByProductId(productId); 19 | stock.decrease(quantity); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /stock/src/main/java/com/example/stock/service/StockService.java: -------------------------------------------------------------------------------- 1 | package com.example.stock.service; 2 | 3 | import com.example.stock.domain.Stock; 4 | import com.example.stock.repository.StockRepository; 5 | import org.springframework.stereotype.Service; 6 | 7 | import javax.transaction.Transactional; 8 | 9 | @Service 10 | public class StockService implements StockBusinessInterface { 11 | 12 | private StockRepository stockRepository; 13 | 14 | public StockService(final StockRepository stockRepository) { 15 | this.stockRepository = stockRepository; 16 | } 17 | 18 | /* 19 | 만약 스프링의 @Transactional 어노테이션을 사용한다면? 20 | 21 | // TransactionStockService와 같이 실행할 때 wrapping해서 새로 실행한다. 22 | // 트랜잭션 종료 시점에 commit하는데 여기서 발생한다. 23 | // decrease method 담긴 thread가 트랜잭션 종료 전에 DB에 접근하여 race condition이 일어날 수 있다. 24 | 25 | // 그렇다면 왜 @Transactional 어노테이션을 사용하는가? 26 | // https://okky.kr/article/437870 27 | // @Transactional을 꼭 사용할 필요는 없습니다. 28 | // 물론 직접 if문 내지는 try catch문 사용해서 관리해도 된다. 29 | // catch(Exception e) 잡아서 그냥 직접 rollback하면 된다. 30 | // 그런데 프로젝트가 커지고 DAO, Service가 많으면 많아질수록 중복되는 if, try catch 코드가 점점 많아지겠다. 31 | // 그래서 중복되는 부분은 한군데서 관리하고싶다는 요구사항이 발생했을 것이다. 32 | // 이 때 @Transactional 어노테이션을 붙이면 그 try catch 코드들을 직접 짤 필요 없이 알아서 자동으로 붙여주기 때문에 33 | // 중복되는 코드가 줄어들고 보기 쉬워지죠. 이러한 이유에서 @Transactional을 사용한다. 34 | */ 35 | //@Transactional 36 | public synchronized void decrease(final Long id, final Long quantity) { 37 | // 1. get stock 38 | // 2. decrease stock 39 | // 3. save stock 40 | 41 | final Stock stock = stockRepository.getByProductId(id); 42 | stock.decrease(quantity); 43 | stockRepository.saveAndFlush(stock); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /stock/src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | hibernate: 4 | ddl-auto: create 5 | show-sql: true 6 | datasource: 7 | driver-class-name: com.mysql.cj.jdbc.Driver 8 | url: jdbc:mysql://127.0.0.1:3306/stock_example 9 | username: root 10 | password: password 11 | 12 | logging: 13 | level: 14 | org: 15 | hibernate: 16 | SQL: DEBUG 17 | type: 18 | descriptor: 19 | sql: 20 | BasicBinder: TRACE 21 | -------------------------------------------------------------------------------- /stock/src/test/java/com/example/stock/StockApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example.stock; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class StockApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /stock/src/test/java/com/example/stock/service/StockServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.example.stock.service; 2 | 3 | import com.example.stock.domain.Stock; 4 | import com.example.stock.facade.LettuceLockStockFacade; 5 | import com.example.stock.facade.NamedLockStockFacade; 6 | import com.example.stock.facade.OptimisticLockStockFacade; 7 | import com.example.stock.facade.RedissonLockStockFacade; 8 | import com.example.stock.repository.StockRepository; 9 | import org.junit.jupiter.api.AfterEach; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.DisplayName; 12 | import org.junit.jupiter.api.Test; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.boot.test.context.SpringBootTest; 15 | 16 | import java.util.concurrent.CountDownLatch; 17 | import java.util.concurrent.ExecutorService; 18 | import java.util.concurrent.Executors; 19 | import java.util.stream.IntStream; 20 | 21 | import static org.assertj.core.api.Assertions.assertThat; 22 | 23 | @SpringBootTest 24 | class StockServiceTest { 25 | 26 | @Autowired 27 | private StockService stockService; 28 | 29 | @Autowired 30 | private PessimisticLockStockService pessimisticLockStockService; 31 | 32 | @Autowired private OptimisticLockStockFacade stockOptimisticLockFacade; 33 | 34 | @Autowired private NamedLockStockFacade namedLockStockFacade; 35 | 36 | @Autowired private LettuceLockStockFacade lettuceLockStockFacade; 37 | 38 | @Autowired private RedissonLockStockFacade redissonLockStockFacade; 39 | 40 | @Autowired private StockRepository stockRepository; 41 | 42 | private final int threadCount = 1000; 43 | private final long productId = 1000L; 44 | private final long quantity = 1L; 45 | private final long initQuantity = 1000L; 46 | 47 | private ExecutorService executorService; 48 | private CountDownLatch countDownLatch; 49 | 50 | @BeforeEach 51 | public void beforeEach() { 52 | stockRepository.save(new Stock(productId, initQuantity)); 53 | 54 | executorService = Executors.newFixedThreadPool(threadCount); 55 | countDownLatch = new CountDownLatch(threadCount); 56 | } 57 | 58 | @AfterEach 59 | public void afterEach() { 60 | stockRepository.deleteAll(); 61 | } 62 | 63 | @DisplayName("SYNCHRONIZED를 사용한 재고 감소 - 동시 1000개 테스트 | 16.994s 소요") 64 | @Test 65 | void SYNCHRONIZED를_사용한_재고_감소() throws InterruptedException { 66 | // given 67 | 68 | // when 69 | IntStream.range(0, threadCount).forEach(e -> executorService.submit(() -> { 70 | try { 71 | stockService.decrease(productId, quantity); 72 | } finally { 73 | countDownLatch.countDown(); 74 | } 75 | } 76 | )); 77 | 78 | countDownLatch.await(); 79 | 80 | // then 81 | final Long afterQuantity = stockRepository.getByProductId(productId).getQuantity(); 82 | System.out.println("### SYNCHRONIZED 동시성 처리 이후 수량 ###" + afterQuantity); 83 | assertThat(afterQuantity).isZero(); 84 | } 85 | 86 | @DisplayName("pessimistic lock을 사용한 재고 감소 - 동시에 1000개 테스트 | 12.415s 소요") 87 | @Test 88 | void PESSIMISTIC_LOCK을_사용한_재고_감소() throws InterruptedException { 89 | // given 90 | 91 | // when 92 | IntStream.range(0, threadCount).forEach(e -> executorService.submit(() -> { 93 | try { 94 | pessimisticLockStockService.decrease(productId, quantity); 95 | } finally { 96 | countDownLatch.countDown(); 97 | } 98 | } 99 | )); 100 | 101 | countDownLatch.await(); 102 | 103 | // then 104 | final Long afterQuantity = stockRepository.getByProductId(productId).getQuantity(); 105 | System.out.println("### PESSIMISTIC LOCK 동시성 처리 이후 수량 ###" + afterQuantity); 106 | assertThat(afterQuantity).isZero(); 107 | } 108 | 109 | @DisplayName("optimistic lock을 사용한 재고 감소 - 동시에 1000개 테스트 | 36.494s 소요") 110 | // 충돌이 빈번하게 일어나지 않을 것이라면 Optimistic Lock을 사용한다. 111 | @Test 112 | void OPTIMISTIC_LOCK을_사용한_재고_감소() throws InterruptedException { 113 | // given 114 | 115 | // when 116 | IntStream.range(0, threadCount).forEach(e -> executorService.submit(() -> { 117 | try { 118 | stockOptimisticLockFacade.decrease(productId, quantity); 119 | } catch (final InterruptedException ex) { 120 | throw new RuntimeException(ex); 121 | } finally { 122 | countDownLatch.countDown(); 123 | } 124 | } 125 | )); 126 | 127 | countDownLatch.await(); 128 | 129 | // then 130 | final Long afterQuantity = stockRepository.getByProductId(productId).getQuantity(); 131 | System.out.println("### OPTIMISTIC LOCK 동시성 처리 이후 수량 ###" + afterQuantity); 132 | assertThat(afterQuantity).isZero(); 133 | } 134 | 135 | @DisplayName("named lock 을 사용한 재고 감소 - 동시에 1000개 테스트 | 21.857s 소요") 136 | // 데이터 소스를 분리하지 않고 하나로 사용할 경우 커넥션 풀이 부족해질 수 있으므로 분리하는 것을 추천한다. 137 | @Test 138 | void NAMED_LOCK을_사용한_재고_감소() throws InterruptedException { 139 | // given 140 | 141 | // when 142 | IntStream.range(0, threadCount).forEach(e -> executorService.submit(() -> { 143 | try { 144 | namedLockStockFacade.decrease(productId, quantity); 145 | } finally { 146 | countDownLatch.countDown(); 147 | } 148 | } 149 | )); 150 | 151 | countDownLatch.await(); 152 | 153 | // then 154 | final Long afterQuantity = stockRepository.getByProductId(productId).getQuantity(); 155 | System.out.println("### NAMED LOCK 동시성 처리 이후 수량 ###" + afterQuantity); 156 | assertThat(afterQuantity).isZero(); 157 | } 158 | 159 | @DisplayName("redis lettuce lock 을 사용한 재고 감소 - 동시에 1000개 테스트 | 49.581s 소요") 160 | // Redis를 사용하면 트랜잭션에 따라 대응되는 현재 트랜잭션 풀 세션 관리를 하지 않아도 되므로 구현이 편리하다. 161 | // Spin Lock 방식이므로 부하를 줄 수 있어서 thread busy waiting을 통하여 요청 간의 시간을 주어야 한다. 162 | @Test 163 | void LETTUCE_LOCK을_사용한_재고_감소() throws InterruptedException { 164 | // given 165 | 166 | // when 167 | IntStream.range(0, threadCount).forEach(e -> executorService.submit(() -> { 168 | try { 169 | lettuceLockStockFacade.decrease(productId, quantity); 170 | } catch (InterruptedException ex) { 171 | throw new RuntimeException(ex); 172 | } finally { 173 | countDownLatch.countDown(); 174 | } 175 | } 176 | )); 177 | 178 | countDownLatch.await(); 179 | 180 | // then 181 | final Long afterQuantity = stockRepository.getByProductId(productId).getQuantity(); 182 | System.out.println("### LETTUCE LOCK 동시성 처리 이후 수량 ###" + afterQuantity); 183 | assertThat(afterQuantity).isZero(); 184 | } 185 | 186 | @DisplayName("redis reddison lock 을 사용한 재고 감소 - 동시에 1000개 테스트 | 17.23s 소요") 187 | @Test 188 | void REDISSON_LOCK을_사용한_재고_감소() throws InterruptedException { 189 | // given 190 | 191 | // when 192 | IntStream.range(0, threadCount).forEach(e -> executorService.submit(() -> { 193 | try { 194 | redissonLockStockFacade.decrease(productId, quantity); 195 | } catch (InterruptedException ex) { 196 | throw new RuntimeException(ex); 197 | } finally { 198 | countDownLatch.countDown(); 199 | } 200 | } 201 | )); 202 | 203 | countDownLatch.await(); 204 | 205 | // then 206 | final Long afterQuantity = stockRepository.getByProductId(productId).getQuantity(); 207 | System.out.println("### REDDISON LOCK 동시성 처리 이후 수량 ###" + afterQuantity); 208 | assertThat(afterQuantity).isZero(); 209 | } 210 | } --------------------------------------------------------------------------------